From f0d475ea2e13f7ecd7d1a5f3ff5a733c44317c81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Tue, 28 Jul 2026 12:33:31 +0000 Subject: [PATCH] fix: gapless player wiped the queue on natural track end (stale closure) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The engine is created once (mount effect), so its onIndex/onEndOfQueue callbacks captured the first-render syncIndex/setPlaying. useGlobal's setData reads the `data` from the render that created the setter when given a functional updater — that's the INITIAL empty state. So when a track ended and the engine called syncIndex(i) → setState(s => ({...s, index:i})), `s` was {queue:[], index:0}: the queue got wiped, `current` went undefined, playback stopped and the dock vanished. Manual track selection was unaffected because playQueue passes a plain object (no stale `data` read) — which is why it seemed to work. Route the two engine-invoked setters through refs kept current each render, so a natural advance mirrors into the live state instead of the stale initial one. Co-Authored-By: Claude Opus 4.8 --- .../officerdev/src/MusicPlayer/MusicPlayerHost.tsx | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/workspaces/officerdev/src/MusicPlayer/MusicPlayerHost.tsx b/src/workspaces/officerdev/src/MusicPlayer/MusicPlayerHost.tsx index 1453d9be..461b1d2e 100644 --- a/src/workspaces/officerdev/src/MusicPlayer/MusicPlayerHost.tsx +++ b/src/workspaces/officerdev/src/MusicPlayer/MusicPlayerHost.tsx @@ -60,6 +60,13 @@ export const MusicPlayerHost = () => { const isRestoringRef = useRef(false); const seekToRef = useRef(null); const engineIndexRef = useRef(0); + // The engine is created once, so its callbacks would capture first-render closures. useMusicPlayer's + // functional setters (syncIndex/setPlaying) read the state captured at THAT render (the initial EMPTY + // queue) — calling them from a stale closure wipes the queue. Route them through refs kept current. + const syncIndexRef = useRef(syncIndex); + syncIndexRef.current = syncIndex; + const setPlayingRef = useRef(setPlaying); + setPlayingRef.current = setPlaying; const withToken = (u: string) => (token ? `${u}${u.includes('?') ? '&' : '?'}token=${encodeURIComponent(token)}` : u); const streamUrl = (t: PlayerTrack) => @@ -92,9 +99,9 @@ export const MusicPlayerHost = () => { }, onIndex: (i) => { engineIndexRef.current = i; // engine advanced on its own → mirror to UI without restarting - syncIndex(i); + syncIndexRef.current(i); }, - onEndOfQueue: () => setPlaying(false), + onEndOfQueue: () => setPlayingRef.current(false), onLoadingChange: setLoading, }); engineRef.current = engine;