mirror of
https://github.com/kilyabin/psysonic.git
synced 2026-07-22 06:25:41 +00:00
feat(orbit): show track attribution in host queue + host label in guest queue
Before: the guest queue already labelled guest-suggested tracks with
"Suggested by {user}", but host picks had no label — and the host's own
queue had no attribution at all. The host couldn't tell which upcoming
rows came from guests without checking the pending-approvals list.
- Guest queue: host-pick rows now show "Added by host" instead of hiding
the attribution line.
- Host queue: each row (and the current track) shows "Added by you" or
"Added by {user}" while an Orbit session is active. Lookup uses the
existing OrbitState.queue / currentTrack addedBy field, so no new
protocol fields.
New i18n keys (en+de+fr+nl+zh+nb+ru+es):
orbit.queueAddedByHost · queueAddedByYou · queueAddedByUser
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -169,11 +169,11 @@ export default function OrbitGuestQueue() {
|
||||
<div className="orbit-guest-queue__track-artist">
|
||||
{song?.artist ?? ''}
|
||||
</div>
|
||||
{!isHostPick && (
|
||||
<div className="orbit-guest-queue__submitter">
|
||||
{t('orbit.guestSubmitter', { user: q.addedBy })}
|
||||
</div>
|
||||
)}
|
||||
<div className="orbit-guest-queue__submitter">
|
||||
{isHostPick
|
||||
? t('orbit.queueAddedByHost')
|
||||
: t('orbit.guestSubmitter', { user: q.addedBy })}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -251,6 +251,28 @@ function QueuePanelHostOrSolo() {
|
||||
const navigate = useNavigate();
|
||||
const orbitRole = useOrbitStore(s => s.role);
|
||||
const orbitState = useOrbitStore(s => s.state);
|
||||
/** trackId → addedBy (host username or guest username) — only populated while
|
||||
* hosting an Orbit session, so the queue rows can surface attribution. */
|
||||
const orbitAddedByByTrack = useMemo(() => {
|
||||
const map = new Map<string, string>();
|
||||
if (orbitRole !== 'host' || !orbitState) return map;
|
||||
if (orbitState.currentTrack) {
|
||||
map.set(orbitState.currentTrack.trackId, orbitState.currentTrack.addedBy);
|
||||
}
|
||||
for (const q of orbitState.queue) map.set(q.trackId, q.addedBy);
|
||||
return map;
|
||||
}, [orbitRole, orbitState]);
|
||||
const orbitHostUsername = orbitState?.host ?? '';
|
||||
/** Attribution label for a queue row / current track while hosting. Null when
|
||||
* not hosting, when the track isn't part of the Orbit state, or when it
|
||||
* isn't relevant for this user (non-host and we haven't mapped the added-by). */
|
||||
const orbitAttributionLabel = (trackId: string): string | null => {
|
||||
if (orbitRole !== 'host') return null;
|
||||
const addedBy = orbitAddedByByTrack.get(trackId);
|
||||
if (!addedBy) return null;
|
||||
if (addedBy === orbitHostUsername) return t('orbit.queueAddedByYou');
|
||||
return t('orbit.queueAddedByUser', { user: addedBy });
|
||||
};
|
||||
const queue = usePlayerStore(s => s.queue);
|
||||
const queueIndex = usePlayerStore(s => s.queueIndex);
|
||||
const currentTrack = usePlayerStore(s => s.currentTrack);
|
||||
@@ -571,6 +593,10 @@ function QueuePanelHostOrSolo() {
|
||||
{currentTrack.year && (
|
||||
<div className="queue-current-sub">{currentTrack.year}</div>
|
||||
)}
|
||||
{(() => {
|
||||
const label = orbitAttributionLabel(currentTrack.id);
|
||||
return label ? <div className="queue-current-sub queue-current-attribution">{label}</div> : null;
|
||||
})()}
|
||||
{renderStars(userRatingOverrides[currentTrack.id] ?? currentTrack.userRating)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -748,6 +774,10 @@ function QueuePanelHostOrSolo() {
|
||||
<span className="truncate">{track.title}</span>
|
||||
</div>
|
||||
<div className="queue-item-artist truncate">{track.artist}</div>
|
||||
{(() => {
|
||||
const label = orbitAttributionLabel(track.id);
|
||||
return label ? <div className="queue-item-attribution truncate">{label}</div> : null;
|
||||
})()}
|
||||
</div>
|
||||
<div className="queue-item-duration">
|
||||
{formatTime(track.duration)}
|
||||
|
||||
@@ -1650,6 +1650,9 @@ export const deTranslation = {
|
||||
approvalDecline: 'Ablehnen',
|
||||
guestUpNextMore: '+ {{count}} weitere in der Host-Warteschlange',
|
||||
guestSubmitter: 'von {{user}}',
|
||||
queueAddedByHost: 'Vom Host hinzugefügt',
|
||||
queueAddedByYou: 'Von dir hinzugefügt',
|
||||
queueAddedByUser: 'Hinzugefügt von {{user}}',
|
||||
guestEmpty: 'Noch keine Vorschläge. Öffne bei einem Song das Kontextmenü und wähle „Zur Orbit-Session hinzufügen".',
|
||||
guestFooter: 'Die Wiedergabe wird vom Gastgeber gesteuert — du kannst die Liste nicht ändern.',
|
||||
exitKickedTitle: 'Du wurdest aus der Session gebannt',
|
||||
|
||||
@@ -1654,6 +1654,9 @@ export const enTranslation = {
|
||||
approvalDecline: 'Decline',
|
||||
guestUpNextMore: '+ {{count}} more in the host\'s queue',
|
||||
guestSubmitter: 'by {{user}}',
|
||||
queueAddedByHost: 'Added by host',
|
||||
queueAddedByYou: 'Added by you',
|
||||
queueAddedByUser: 'Added by {{user}}',
|
||||
guestEmpty: 'No suggestions yet. Open a track\'s context menu and pick "Add to Orbit session".',
|
||||
guestFooter: "The host controls playback — you can't change the list.",
|
||||
exitKickedTitle: 'You were banned from the session',
|
||||
|
||||
@@ -1637,6 +1637,9 @@ export const esTranslation = {
|
||||
approvalDecline: 'Rechazar',
|
||||
guestUpNextMore: '+ {{count}} más en la cola del anfitrión',
|
||||
guestSubmitter: 'de {{user}}',
|
||||
queueAddedByHost: 'Añadido por el anfitrión',
|
||||
queueAddedByYou: 'Añadido por ti',
|
||||
queueAddedByUser: 'Añadido por {{user}}',
|
||||
guestEmpty: 'Aún no hay sugerencias. Abre el menú contextual de una canción y elige "Añadir a la sesión Orbit".',
|
||||
guestFooter: 'El anfitrión controla la reproducción — no puedes cambiar la lista.',
|
||||
exitKickedTitle: 'Has sido baneado de la sesión',
|
||||
|
||||
@@ -1632,6 +1632,9 @@ export const frTranslation = {
|
||||
approvalDecline: 'Refuser',
|
||||
guestUpNextMore: '+ {{count}} autres dans la file de l\'hôte',
|
||||
guestSubmitter: 'par {{user}}',
|
||||
queueAddedByHost: 'Ajouté par l\'hôte',
|
||||
queueAddedByYou: 'Ajouté par toi',
|
||||
queueAddedByUser: 'Ajouté par {{user}}',
|
||||
guestEmpty: 'Aucune suggestion. Ouvre le menu contextuel d\'un morceau et choisis « Ajouter à la session Orbit ».',
|
||||
guestFooter: 'L\'hôte contrôle la lecture — tu ne peux pas modifier la liste.',
|
||||
exitKickedTitle: 'Tu as été banni de la session',
|
||||
|
||||
@@ -1631,6 +1631,9 @@ export const nbTranslation = {
|
||||
approvalDecline: 'Avvis',
|
||||
guestUpNextMore: '+ {{count}} flere i vertens kø',
|
||||
guestSubmitter: 'av {{user}}',
|
||||
queueAddedByHost: 'Lagt til av verten',
|
||||
queueAddedByYou: 'Lagt til av deg',
|
||||
queueAddedByUser: 'Lagt til av {{user}}',
|
||||
guestEmpty: 'Ingen forslag ennå. Åpne et spors kontekstmeny og velg "Legg til i Orbit-økt".',
|
||||
guestFooter: 'Verten kontrollerer avspillingen — du kan ikke endre listen.',
|
||||
exitKickedTitle: 'Du ble utestengt fra økten',
|
||||
|
||||
@@ -1631,6 +1631,9 @@ export const nlTranslation = {
|
||||
approvalDecline: 'Weigeren',
|
||||
guestUpNextMore: '+ {{count}} meer in de wachtrij van de host',
|
||||
guestSubmitter: 'door {{user}}',
|
||||
queueAddedByHost: 'Toegevoegd door host',
|
||||
queueAddedByYou: 'Toegevoegd door jou',
|
||||
queueAddedByUser: 'Toegevoegd door {{user}}',
|
||||
guestEmpty: 'Nog geen suggesties. Open het contextmenu van een nummer en kies "Aan Orbit-sessie toevoegen".',
|
||||
guestFooter: 'De host bedient de weergave — je kunt de lijst niet wijzigen.',
|
||||
exitKickedTitle: 'Je bent uit de sessie gebannen',
|
||||
|
||||
@@ -1713,6 +1713,9 @@ export const ruTranslation = {
|
||||
approvalDecline: 'Отклонить',
|
||||
guestUpNextMore: '+ ещё {{count}} в очереди хоста',
|
||||
guestSubmitter: 'от {{user}}',
|
||||
queueAddedByHost: 'Добавил хост',
|
||||
queueAddedByYou: 'Добавил(а) ты',
|
||||
queueAddedByUser: 'Добавил {{user}}',
|
||||
guestEmpty: 'Предложений пока нет. Открой контекстное меню трека и выбери «Добавить в сессию Orbit».',
|
||||
guestFooter: 'Хост управляет воспроизведением — ты не можешь изменять список.',
|
||||
exitKickedTitle: 'Тебя забанили в сессии',
|
||||
|
||||
@@ -1624,6 +1624,9 @@ export const zhTranslation = {
|
||||
approvalDecline: '拒绝',
|
||||
guestUpNextMore: '+ 主持人队列中还有 {{count}} 首',
|
||||
guestSubmitter: '来自 {{user}}',
|
||||
queueAddedByHost: '由主持人添加',
|
||||
queueAddedByYou: '由你添加',
|
||||
queueAddedByUser: '由 {{user}} 添加',
|
||||
guestEmpty: '还没有建议。打开曲目的上下文菜单并选择"添加到 Orbit 会话"。',
|
||||
guestFooter: '主持人控制播放 — 你无法更改列表。',
|
||||
exitKickedTitle: '你已被会话封禁',
|
||||
|
||||
@@ -2160,6 +2160,26 @@ html[data-platform="windows"] .player-bar.floating {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.queue-item-attribution,
|
||||
.queue-current-attribution {
|
||||
font-size: 10px;
|
||||
color: var(--text-muted);
|
||||
opacity: 0.75;
|
||||
margin-top: 1px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.queue-item:hover .queue-item-attribution,
|
||||
.queue-item.context-active .queue-item-attribution {
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.queue-item.active .queue-item-attribution {
|
||||
color: inherit;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════════════
|
||||
MOBILE LAYOUT (< 800px)
|
||||
Controller: data-mobile attribute set by useIsMobile hook
|
||||
|
||||
Reference in New Issue
Block a user