chore(orbit): participant strip at the top of the queue for host and guest

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Psychotoxical
2026-04-24 14:29:03 +02:00
parent 6c7f455e66
commit 1a470b51b5
6 changed files with 45 additions and 14 deletions
+3 -7
View File
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { Radio, Users } from 'lucide-react'; import { Radio } from 'lucide-react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useOrbitStore } from '../store/orbitStore'; import { useOrbitStore } from '../store/orbitStore';
import { import {
@@ -9,6 +9,7 @@ import {
type SubsonicSong, type SubsonicSong,
} from '../api/subsonic'; } from '../api/subsonic';
import CachedImage from './CachedImage'; import CachedImage from './CachedImage';
import OrbitQueueHead from './OrbitQueueHead';
/** /**
* Orbit — guest-side queue view. * Orbit — guest-side queue view.
@@ -67,12 +68,7 @@ export default function OrbitGuestQueue() {
return ( return (
<div className="orbit-guest-queue"> <div className="orbit-guest-queue">
<div className="orbit-guest-queue__head"> <OrbitQueueHead state={state} />
<h2 className="orbit-guest-queue__title">{state.name}</h2>
<div className="orbit-guest-queue__meta">
<Users size={11} /> {state.participants.length + 1} · {t('orbit.guestHost', { name: state.host })}
</div>
</div>
{currentTrack && ( {currentTrack && (
<div className="orbit-guest-queue__current"> <div className="orbit-guest-queue__current">
+24
View File
@@ -0,0 +1,24 @@
import { Users } from 'lucide-react';
import type { OrbitState } from '../api/orbit';
interface Props {
state: OrbitState;
}
/**
* Shared Orbit head strip rendered at the top of the queue for both host
* and guest. Shows the session name and a comma-separated list of every
* participant (host first, then guests in join order).
*/
export default function OrbitQueueHead({ state }: Props) {
const names = [state.host, ...state.participants.map(p => p.user)];
return (
<div className="orbit-queue-head">
<h2 className="orbit-queue-head__title">{state.name}</h2>
<div className="orbit-queue-head__meta">
<Users size={11} />
<span className="orbit-queue-head__names">{names.join(', ')}</span>
</div>
</div>
);
}
+6
View File
@@ -2,6 +2,7 @@ import React, { useState, useRef, useMemo, useEffect } from 'react';
import { Track, usePlayerStore, songToTrack } from '../store/playerStore'; import { Track, usePlayerStore, songToTrack } from '../store/playerStore';
import { useOrbitStore } from '../store/orbitStore'; import { useOrbitStore } from '../store/orbitStore';
import OrbitGuestQueue from './OrbitGuestQueue'; import OrbitGuestQueue from './OrbitGuestQueue';
import OrbitQueueHead from './OrbitQueueHead';
import { Play, Music, Star, X, Trash2, Save, FolderOpen, Shuffle, Infinity, Waves, MicVocal, ListMusic, Check, ListPlus, MoveRight, Radio, HardDrive, ChevronDown, Info, Share2 } from 'lucide-react'; import { Play, Music, Star, X, Trash2, Save, FolderOpen, Shuffle, Infinity, Waves, MicVocal, ListMusic, Check, ListPlus, MoveRight, Radio, HardDrive, ChevronDown, Info, Share2 } from 'lucide-react';
import { buildCoverArtUrl, coverArtCacheKey, getAlbum, getPlaylists, getPlaylist, updatePlaylist, deletePlaylist, SubsonicPlaylist } from '../api/subsonic'; import { buildCoverArtUrl, coverArtCacheKey, getAlbum, getPlaylists, getPlaylist, updatePlaylist, deletePlaylist, SubsonicPlaylist } from '../api/subsonic';
import { usePlaylistStore } from '../store/playlistStore'; import { usePlaylistStore } from '../store/playlistStore';
@@ -247,6 +248,8 @@ export default function QueuePanel() {
function QueuePanelHostOrSolo() { function QueuePanelHostOrSolo() {
const { t } = useTranslation(); const { t } = useTranslation();
const navigate = useNavigate(); const navigate = useNavigate();
const orbitRole = useOrbitStore(s => s.role);
const orbitState = useOrbitStore(s => s.state);
const queue = usePlayerStore(s => s.queue); const queue = usePlayerStore(s => s.queue);
const queueIndex = usePlayerStore(s => s.queueIndex); const queueIndex = usePlayerStore(s => s.queueIndex);
const currentTrack = usePlayerStore(s => s.currentTrack); const currentTrack = usePlayerStore(s => s.currentTrack);
@@ -465,6 +468,9 @@ function QueuePanelHostOrSolo() {
borderLeftWidth: isQueueVisible ? 1 : 0, borderLeftWidth: isQueueVisible ? 1 : 0,
}} }}
> >
{orbitRole === 'host' && orbitState && (
<OrbitQueueHead state={orbitState} />
)}
<QueueHeader <QueueHeader
queue={queue} queue={queue}
queueIndex={queueIndex} queueIndex={queueIndex}
-1
View File
@@ -1552,7 +1552,6 @@ export const deTranslation = {
guestUpNext: 'Als Nächstes', guestUpNext: 'Als Nächstes',
guestUpNextEmpty: 'Nichts in der Warteschlange. Öffne bei einem Song das Kontextmenü und wähle „Zur Orbit-Session hinzufügen", um etwas vorzuschlagen.', guestUpNextEmpty: 'Nichts in der Warteschlange. Öffne bei einem Song das Kontextmenü und wähle „Zur Orbit-Session hinzufügen", um etwas vorzuschlagen.',
guestUpNextMore: '+ {{count}} weitere in der Host-Warteschlange', guestUpNextMore: '+ {{count}} weitere in der Host-Warteschlange',
guestHost: 'Gastgeber: {{name}}',
guestSubmitter: 'von {{user}}', guestSubmitter: 'von {{user}}',
guestEmpty: 'Noch keine Vorschläge. Öffne bei einem Song das Kontextmenü und wähle „Zur Orbit-Session hinzufügen".', 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.', guestFooter: 'Die Wiedergabe wird vom Gastgeber gesteuert — du kannst die Liste nicht ändern.',
-1
View File
@@ -1555,7 +1555,6 @@ export const enTranslation = {
guestUpNext: 'Up next', guestUpNext: 'Up next',
guestUpNextEmpty: 'Nothing queued. Open a track\'s context menu and pick "Add to Orbit session" to suggest one.', guestUpNextEmpty: 'Nothing queued. Open a track\'s context menu and pick "Add to Orbit session" to suggest one.',
guestUpNextMore: '+ {{count}} more in the host\'s queue', guestUpNextMore: '+ {{count}} more in the host\'s queue',
guestHost: 'Host: {{name}}',
guestSubmitter: 'by {{user}}', guestSubmitter: 'by {{user}}',
guestEmpty: 'No suggestions yet. Open a track\'s context menu and pick "Add to Orbit session".', 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.", guestFooter: "The host controls playback — you can't change the list.",
+12 -5
View File
@@ -12267,12 +12267,12 @@ html[data-psy-native-hidden="true"] *::after {
flex: 1; flex: 1;
} }
.orbit-guest-queue__head { .orbit-queue-head {
padding: 14px 14px 10px; padding: 14px 14px 10px;
border-bottom: 1px solid var(--border-subtle); border-bottom: 1px solid var(--border-subtle);
flex-shrink: 0; flex-shrink: 0;
} }
.orbit-guest-queue__title { .orbit-queue-head__title {
margin: 0 0 4px; margin: 0 0 4px;
font-size: 15px; font-size: 15px;
font-weight: 700; font-weight: 700;
@@ -12281,14 +12281,21 @@ html[data-psy-native-hidden="true"] *::after {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
} }
.orbit-guest-queue__meta { .orbit-queue-head__meta {
display: inline-flex; display: flex;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
font-size: 11px; font-size: 11px;
color: var(--text-muted); color: var(--text-muted);
min-width: 0;
}
.orbit-queue-head__meta svg { color: var(--accent); flex-shrink: 0; }
.orbit-queue-head__names {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
} }
.orbit-guest-queue__meta svg { color: var(--accent); }
.orbit-guest-queue__current { .orbit-guest-queue__current {
position: relative; position: relative;