import { getPlaylist, updatePlaylist } from '../api/subsonicPlaylists'; import { songToTrack } from '../utils/playback/songToTrack'; import type { Track } from '../store/playerStoreTypes'; import { useState, useRef, useMemo } from 'react'; import { usePlayerStore } from '../store/playerStore'; import { useOrbitStore } from '../store/orbitStore'; import OrbitGuestQueue from './OrbitGuestQueue'; import OrbitQueueHead from './OrbitQueueHead'; import HostApprovalQueue from './HostApprovalQueue'; import { usePlaylistStore } from '../store/playlistStore'; import { useCachedUrl } from './CachedImage'; import { useTranslation } from 'react-i18next'; import { usePlaybackLibraryNavigate } from '../hooks/usePlaybackLibraryNavigate'; import { useAuthStore } from '../store/authStore'; import { encodeSharePayload } from '../utils/share/shareLink'; import { copyTextToClipboard } from '../utils/server/serverMagicString'; import { showToast } from '../utils/ui/toast'; import { useThemeStore } from '../store/themeStore'; import { useLyricsStore } from '../store/lyricsStore'; import LyricsPane from './LyricsPane'; import NowPlayingInfo from './NowPlayingInfo'; import { TFunction } from 'i18next'; import { useLuckyMixStore } from '../store/luckyMixStore'; import { useQueueToolbarStore } from '../store/queueToolbarStore'; import { SavePlaylistModal } from './queuePanel/SavePlaylistModal'; import { LoadPlaylistModal } from './queuePanel/LoadPlaylistModal'; import { QueueHeader } from './queuePanel/QueueHeader'; import { QueueCurrentTrack } from './queuePanel/QueueCurrentTrack'; import { useQueuePanelDrag } from '../hooks/useQueuePanelDrag'; import { useQueueLufsTgtPopover } from '../hooks/useQueueLufsTgtPopover'; import { QueueToolbar } from './queuePanel/QueueToolbar'; import { QueueList } from './queuePanel/QueueList'; import { QueueTabBar } from './queuePanel/QueueTabBar'; import { useQueueAutoScroll } from '../hooks/useQueueAutoScroll'; import { usePlaybackCoverArt } from '../hooks/usePlaybackCoverArt'; export default function QueuePanel() { const orbitRole = useOrbitStore(s => s.role); if (orbitRole === 'guest') { return ( ); } return ; } function QueuePanelHostOrSolo() { const { t } = useTranslation(); const navigatePlaybackLibrary = usePlaybackLibraryNavigate(); 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 in a hosted session. Bulk-adds (album / playlist enqueue) bypass * `hostEnqueueToOrbit` and therefore never land in `state.queue`, so we * default those to "Added by you" rather than showing nothing. */ const orbitAttributionLabel = (trackId: string): string | null => { if (orbitRole !== 'host' || !orbitState) return null; const addedBy = orbitAddedByByTrack.get(trackId); if (!addedBy || 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); const userRatingOverrides = usePlayerStore(s => s.userRatingOverrides); const { src: currentCoverFetchUrl, cacheKey: currentCoverCacheKey } = usePlaybackCoverArt( currentTrack?.coverArt, 128, ); const currentCoverSrc = useCachedUrl(currentCoverFetchUrl, currentCoverCacheKey); const isQueueVisible = usePlayerStore(s => s.isQueueVisible); const playTrack = usePlayerStore(s => s.playTrack); const clearQueue = usePlayerStore(s => s.clearQueue); const reorderQueue = usePlayerStore(s => s.reorderQueue); const removeTrack = usePlayerStore(s => s.removeTrack); const shuffleQueue = usePlayerStore(s => s.shuffleQueue); const enqueue = usePlayerStore(s => s.enqueue); const enqueueAt = usePlayerStore(s => s.enqueueAt); const contextMenu = usePlayerStore(s => s.contextMenu); // When the user picks a track *from* the queue list, suppress the // upcoming auto-scroll so their click target stays in view instead of // the list rebasing onto the next track. Auto-advance (natural playback) // never sets this flag, so it keeps its original "show what's next" behavior. const suppressNextAutoScrollRef = useRef(false); const playbackSource = usePlayerStore(s => s.currentPlaybackSource); const normalizationNowDb = usePlayerStore(s => s.normalizationNowDb); const normalizationTargetLufs = usePlayerStore(s => s.normalizationTargetLufs); const normalizationEngineLive = usePlayerStore(s => s.normalizationEngineLive); const crossfadeEnabled = useAuthStore(s => s.crossfadeEnabled); const crossfadeSecs = useAuthStore(s => s.crossfadeSecs); const gaplessEnabled = useAuthStore(s => s.gaplessEnabled); const infiniteQueueEnabled = useAuthStore(s => s.infiniteQueueEnabled); const setCrossfadeEnabled = useAuthStore(s => s.setCrossfadeEnabled); const setCrossfadeSecs = useAuthStore(s => s.setCrossfadeSecs); const setGaplessEnabled = useAuthStore(s => s.setGaplessEnabled); const setInfiniteQueueEnabled = useAuthStore(s => s.setInfiniteQueueEnabled); const normalizationEngine = useAuthStore(s => s.normalizationEngine); const activeTab = useLyricsStore(s => s.activeTab); const setTab = useLyricsStore(s => s.setTab); const luckyRolling = useLuckyMixStore(s => s.isRolling); const isNowPlayingCollapsed = useAuthStore(s => s.queueNowPlayingCollapsed); const setIsNowPlayingCollapsed = useAuthStore(s => s.setQueueNowPlayingCollapsed); const toolbarButtons = useQueueToolbarStore(s => s.buttons); const durationMode = useAuthStore(s => s.queueDurationDisplayMode); const setDurationMode = useAuthStore(s => s.setQueueDurationDisplayMode); const expandReplayGain = useThemeStore(s => s.expandReplayGain); const setExpandReplayGain = useThemeStore(s => s.setExpandReplayGain); const reanalyzeLoudnessForTrack = usePlayerStore(s => s.reanalyzeLoudnessForTrack); const authLoudnessTargetLufs = useAuthStore(s => s.loudnessTargetLufs); const setLoudnessTargetLufs = useAuthStore(s => s.setLoudnessTargetLufs); const loudnessPreAnalysisAttenuationDb = useAuthStore(s => s.loudnessPreAnalysisAttenuationDb); const { lufsTgtOpen, setLufsTgtOpen, lufsTgtPopStyle, lufsTgtBtnRef, lufsTgtMenuRef, } = useQueueLufsTgtPopover(expandReplayGain); const queueListRef = useRef(null); const asideRef = useRef(null); const { psyDragFromIdxRef, externalDropTarget, externalDropTargetRef, setExternalDropTarget, isQueueDrag, startDrag, } = useQueuePanelDrag({ asideRef, isQueueVisible, reorderQueue, enqueueAt, removeTrack, }); useQueueAutoScroll({ queue, queueIndex, currentTrack, activeTab, queueListRef, suppressNextAutoScrollRef, }); const [activePlaylist, setActivePlaylist] = useState<{ id: string; name: string } | null>(null); const [saveState, setSaveState] = useState<'idle' | 'saving' | 'saved'>('idle'); const [saveModalOpen, setSaveModalOpen] = useState(false); const [loadModalOpen, setLoadModalOpen] = useState(false); const handleSave = async () => { if (queue.length === 0) return; if (activePlaylist) { setSaveState('saving'); try { await updatePlaylist(activePlaylist.id, queue.map(t => t.id)); setSaveState('saved'); setTimeout(() => setSaveState('idle'), 1500); } catch (e) { console.error('Failed to update playlist', e); setSaveState('idle'); } } else { setSaveModalOpen(true); } }; const handleLoad = () => { setLoadModalOpen(true); }; const handleClear = () => { clearQueue(); setActivePlaylist(null); }; const handleCopyQueueShare = async () => { if (queue.length === 0) { showToast(t('queue.shareQueueEmpty'), 3000, 'info'); return; } const srv = useAuthStore.getState().getBaseUrl(); if (!srv) return; const ids = queue.map(t => t.id); const ok = await copyTextToClipboard(encodeSharePayload({ srv, k: 'queue', ids })); if (ok) showToast(t('contextMenu.shareCopied')); else showToast(t('contextMenu.shareCopyFailed'), 4000, 'error'); }; return ( ); }