From 13a8f696d0852b9601541be34fa01f87e4a14559 Mon Sep 17 00:00:00 2001 From: nisarg <84626554+nisarg-78@users.noreply.github.com> Date: Mon, 6 Apr 2026 22:41:03 +0530 Subject: [PATCH] fix: queue auto-scroll to current track and fix re-renders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * stop queue panel from constant rerendering * add queueHeader type * fix(queue): move hook before early return, fix auto-scroll target - usePlayerStore must be called before conditional return (Rules of Hooks) - Auto-scroll to songs[queueIndex] not [queueIndex+1] — current track, not the next one Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Psychotoxical Co-authored-by: Claude Sonnet 4.6 --- src/components/QueuePanel.tsx | 113 ++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 40 deletions(-) diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index d1037dbb..ad75ccf3 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -10,6 +10,7 @@ import { useAuthStore } from '../store/authStore'; import { useLyricsStore } from '../store/lyricsStore'; import { useDragDrop } from '../contexts/DragDropContext'; import LyricsPane from './LyricsPane'; +import { TFunction } from 'i18next'; function formatTime(seconds: number): string { if (!seconds || isNaN(seconds)) return '0:00'; @@ -153,13 +154,66 @@ function LoadPlaylistModal({ onClose, onLoad }: { onClose: () => void, onLoad: ( ); } +interface QueueHeaderProps { + queue: Track[]; + queueIndex: number; + showRemainingTime: boolean; + setShowRemainingTime: React.Dispatch>; + activePlaylist: { id: string; name: string } | null; + t: TFunction; +} +function QueueHeader({ queue, queueIndex, showRemainingTime, setShowRemainingTime, activePlaylist, t }: QueueHeaderProps) { + const currentTime = usePlayerStore((s) => s.currentTime); + + if (queue.length === 0) return null; + const totalSecs = queue.reduce((acc: number, t: any) => acc + (t.duration || 0), 0); + const remainingSecs = Math.max(0, (queue[queueIndex]?.duration ?? 0) - currentTime + queue.slice(queueIndex + 1).reduce((acc: number, t: any) => acc + (t.duration || 0), 0)); + + const fmt = (secs: number) => { + const h = Math.floor(secs / 3600); + const m = Math.floor((secs % 3600) / 60); + const s = secs % 60; + return h > 0 ? `${h}:${m.toString().padStart(2, "0")}:${s.toString().padStart(2, "0")}` : `${m}:${s.toString().padStart(2, "0")}`; + }; + + const dur = showRemainingTime ? `-${fmt(Math.floor(remainingSecs))}` : fmt(Math.floor(totalSecs)); + + return ( +
+
+
+

{t("queue.title")}

+ setShowRemainingTime((v: boolean) => !v)} + data-tooltip={showRemainingTime ? t("queue.showTotal") : t("queue.showRemaining")} + style={{ + fontSize: "13px", + color: "var(--accent)", + whiteSpace: "nowrap", + cursor: "pointer", + userSelect: "none", + }} + > + {queue.length} {queue.length === 1 ? t("queue.trackSingular") : t("queue.trackPlural")} · {dur} + +
+ {activePlaylist && ( +
+ + {activePlaylist.name} +
+ )} +
+
+ ); +} + export default function QueuePanel() { const { t } = useTranslation(); const navigate = useNavigate(); const queue = usePlayerStore(s => s.queue); const queueIndex = usePlayerStore(s => s.queueIndex); const currentTrack = usePlayerStore(s => s.currentTrack); - const currentTime = usePlayerStore(s => s.currentTime); const currentCoverFetchUrl = useMemo( () => currentTrack?.coverArt ? buildCoverArtUrl(currentTrack.coverArt, 128) : '', [currentTrack?.coverArt] @@ -275,6 +329,15 @@ export default function QueuePanel() { return () => aside.removeEventListener('psy-drop', onPsyDrop); }, [enqueueAt]); + useEffect(function queueAutoScroll() { + if (!queueListRef.current || queueIndex < 0) return; + if (activeTab !== 'queue') return; + const songs = queueListRef.current!.querySelectorAll('[data-queue-idx]'); + const nextSong = songs[queueIndex]; + if (!nextSong) return; + nextSong.scrollIntoView({ block: "start", behavior: "instant" }); + }, [currentTrack, activeTab]); + const [activePlaylist, setActivePlaylist] = useState<{ id: string; name: string } | null>(null); const [saveState, setSaveState] = useState<'idle' | 'saving' | 'saved'>('idle'); const [saveModalOpen, setSaveModalOpen] = useState(false); @@ -333,47 +396,17 @@ export default function QueuePanel() { } }} style={{ - borderLeftWidth: isQueueVisible ? 1 : 0 + borderLeftWidth: isQueueVisible ? 1 : 0, }} > -
-
-
-

{t('queue.title')}

- {queue.length > 0 && (() => { - const totalSecs = queue.reduce((acc, t) => acc + (t.duration || 0), 0); - const remainingSecs = Math.max(0, - (queue[queueIndex]?.duration ?? 0) - currentTime - + queue.slice(queueIndex + 1).reduce((acc, t) => acc + (t.duration || 0), 0) - ); - const fmt = (secs: number) => { - const h = Math.floor(secs / 3600); - const m = Math.floor((secs % 3600) / 60); - const s = secs % 60; - return h > 0 - ? `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}` - : `${m}:${s.toString().padStart(2, '0')}`; - }; - const dur = showRemainingTime ? `-${fmt(Math.floor(remainingSecs))}` : fmt(Math.floor(totalSecs)); - return ( - setShowRemainingTime(v => !v)} - data-tooltip={showRemainingTime ? t('queue.showTotal') : t('queue.showRemaining')} - style={{ fontSize: '13px', color: 'var(--accent)', whiteSpace: 'nowrap', cursor: 'pointer', userSelect: 'none' }} - > - {queue.length} {queue.length === 1 ? t('queue.trackSingular') : t('queue.trackPlural')} · {dur} - - ); - })()} -
- {activePlaylist && ( -
- - {activePlaylist.name} -
- )} -
-
+ {currentTrack && (