diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index 0e4820ba..08969c10 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -1,6 +1,12 @@ import React, { useState, useRef, useMemo, useEffect, useLayoutEffect } from 'react'; import { createPortal } from 'react-dom'; -import { Track, usePlayerStore, songToTrack } from '../store/playerStore'; +import { + Track, + usePlayerStore, + songToTrack, + registerQueueListScrollTopReader, + consumePendingQueueListScrollTop, +} from '../store/playerStore'; import { useOrbitStore } from '../store/orbitStore'; import OrbitGuestQueue from './OrbitGuestQueue'; import OrbitQueueHead from './OrbitQueueHead'; @@ -414,6 +420,21 @@ function QueuePanelHostOrSolo() { const queueListRef = useRef(null); + useLayoutEffect(() => { + registerQueueListScrollTopReader(() => queueListRef.current?.scrollTop); + return () => registerQueueListScrollTopReader(null); + }, []); + + useLayoutEffect(() => { + const top = consumePendingQueueListScrollTop(); + if (top === undefined) return; + const el = queueListRef.current; + if (!el) return; + suppressNextAutoScrollRef.current = true; + el.scrollTop = top; + el.dispatchEvent(new Event('scroll', { bubbles: false })); + }, [queue, queueIndex, currentTrack?.id]); + const asideRef = useRef(null); const { isDragging: isPsyDragging, startDrag, payload: psyPayload } = useDragDrop(); diff --git a/src/store/playerStore.ts b/src/store/playerStore.ts index ab810aff..e5284862 100644 --- a/src/store/playerStore.ts +++ b/src/store/playerStore.ts @@ -362,15 +362,38 @@ type QueueUndoSnapshot = { currentTime?: number; progress?: number; isPlaying?: boolean; + /** Main queue panel list `scrollTop` when the snapshot was taken. */ + queueListScrollTop?: number; }; const queueUndoStack: QueueUndoSnapshot[] = []; const queueRedoStack: QueueUndoSnapshot[] = []; +/** QueuePanel registers a reader so undo snapshots capture list scroll position. */ +let queueListScrollTopReader: (() => number | undefined) | null = null; + +export function registerQueueListScrollTopReader(reader: (() => number | undefined) | null): void { + queueListScrollTopReader = reader; +} + +function readQueueListScrollTopForUndo(): number | undefined { + return queueListScrollTopReader?.() ?? undefined; +} + +/** Set in applyQueueHistorySnapshot; QueuePanel consumes in useLayoutEffect after commit. */ +let pendingQueueListScrollTop: number | undefined; + +export function consumePendingQueueListScrollTop(): number | undefined { + const v = pendingQueueListScrollTop; + pendingQueueListScrollTop = undefined; + return v; +} + function shallowCloneQueueTracks(queue: Track[]): Track[] { return queue.map(t => ({ ...t })); } function queueUndoSnapshotFromState(s: PlayerState): QueueUndoSnapshot { + const scrollTop = readQueueListScrollTopForUndo(); return { queue: shallowCloneQueueTracks(s.queue), queueIndex: s.queueIndex, @@ -378,6 +401,7 @@ function queueUndoSnapshotFromState(s: PlayerState): QueueUndoSnapshot { currentTime: s.currentTime, progress: s.progress, isPlaying: s.isPlaying, + ...(scrollTop !== undefined ? { queueListScrollTop: scrollTop } : {}), }; } @@ -1926,6 +1950,9 @@ export const usePlayerStore = create()( invoke('audio_stop').catch(console.error); isAudioPaused = false; syncQueueToServer(nextQueue, null, 0); + if (typeof snap.queueListScrollTop === 'number' && Number.isFinite(snap.queueListScrollTop)) { + pendingQueueListScrollTop = Math.max(0, snap.queueListScrollTop); + } return true; } @@ -1946,6 +1973,9 @@ export const usePlayerStore = create()( wantPlaying: playingRestore, }); } + if (typeof snap.queueListScrollTop === 'number' && Number.isFinite(snap.queueListScrollTop)) { + pendingQueueListScrollTop = Math.max(0, snap.queueListScrollTop); + } syncQueueToServer(nextQueue, nextTrack, tRestore); return true; }