diff --git a/src/components/OrbitGuestQueue.tsx b/src/components/OrbitGuestQueue.tsx index 76906150..1a2e9056 100644 --- a/src/components/OrbitGuestQueue.tsx +++ b/src/components/OrbitGuestQueue.tsx @@ -169,11 +169,11 @@ export default function OrbitGuestQueue() {
{song?.artist ?? ''}
- {!isHostPick && ( -
- {t('orbit.guestSubmitter', { user: q.addedBy })} -
- )} +
+ {isHostPick + ? t('orbit.queueAddedByHost') + : t('orbit.guestSubmitter', { user: q.addedBy })} +
); diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index a5dc46df..855c97c7 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -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(); + 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 && (
{currentTrack.year}
)} + {(() => { + const label = orbitAttributionLabel(currentTrack.id); + return label ?
{label}
: null; + })()} {renderStars(userRatingOverrides[currentTrack.id] ?? currentTrack.userRating)} @@ -748,6 +774,10 @@ function QueuePanelHostOrSolo() { {track.title}
{track.artist}
+ {(() => { + const label = orbitAttributionLabel(track.id); + return label ?
{label}
: null; + })()}
{formatTime(track.duration)} diff --git a/src/locales/de.ts b/src/locales/de.ts index 4adb34e0..ff65ed11 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -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', diff --git a/src/locales/en.ts b/src/locales/en.ts index 7f6728f3..4413a6ff 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -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', diff --git a/src/locales/es.ts b/src/locales/es.ts index a58bd45d..d35494b3 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -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', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 6f31b302..5dc79899 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -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', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 3a89544c..a64f9e31 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -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', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 533d93f4..4dc3fffd 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -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', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 4c5a529c..a2a65043 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -1713,6 +1713,9 @@ export const ruTranslation = { approvalDecline: 'Отклонить', guestUpNextMore: '+ ещё {{count}} в очереди хоста', guestSubmitter: 'от {{user}}', + queueAddedByHost: 'Добавил хост', + queueAddedByYou: 'Добавил(а) ты', + queueAddedByUser: 'Добавил {{user}}', guestEmpty: 'Предложений пока нет. Открой контекстное меню трека и выбери «Добавить в сессию Orbit».', guestFooter: 'Хост управляет воспроизведением — ты не можешь изменять список.', exitKickedTitle: 'Тебя забанили в сессии', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 217777f4..3aa516cc 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -1624,6 +1624,9 @@ export const zhTranslation = { approvalDecline: '拒绝', guestUpNextMore: '+ 主持人队列中还有 {{count}} 首', guestSubmitter: '来自 {{user}}', + queueAddedByHost: '由主持人添加', + queueAddedByYou: '由你添加', + queueAddedByUser: '由 {{user}} 添加', guestEmpty: '还没有建议。打开曲目的上下文菜单并选择"添加到 Orbit 会话"。', guestFooter: '主持人控制播放 — 你无法更改列表。', exitKickedTitle: '你已被会话封禁', diff --git a/src/styles/layout.css b/src/styles/layout.css index 1b55396a..d1e4c62b 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -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