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() {
- {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