From 6d2c084355c49c0d0602e01ef977cbcab78fb3f9 Mon Sep 17 00:00:00 2001 From: York Date: Sun, 8 Mar 2026 12:23:19 +0800 Subject: [PATCH] fix: sleep timer end-of-song mode (#1706) --- .../player/audio-player/web-player.tsx | 29 +++++++++++++++++-- .../player/components/sleep-timer-button.tsx | 23 ++++++--------- src/renderer/store/player.store.ts | 17 ++++++++++- 3 files changed, 52 insertions(+), 17 deletions(-) diff --git a/src/renderer/features/player/audio-player/web-player.tsx b/src/renderer/features/player/audio-player/web-player.tsx index 529edff08..bcadf3337 100644 --- a/src/renderer/features/player/audio-player/web-player.tsx +++ b/src/renderer/features/player/audio-player/web-player.tsx @@ -20,6 +20,7 @@ import { usePlayerData, usePlayerMuted, usePlayerProperties, + usePlayerStoreBase, usePlayerVolume, } from '/@/renderer/store'; import { toast } from '/@/shared/components/toast/toast'; @@ -180,7 +181,15 @@ export function WebPlayer() { promise.then(() => { playerRef.current?.player1()?.ref?.getInternalPlayer().pause(); - playerRef.current?.setVolume(volume); + + // If mediaAutoNext resulted in a paused state (e.g. end of queue, + // or pauseOnNextSongEnd flag), stop all audio instead of restoring volume. + const currentStatus = usePlayerStoreBase.getState().player.status; + if (currentStatus === PlayerStatus.PAUSED) { + playerRef.current?.pause(); + } else { + playerRef.current?.setVolume(volume); + } setIsTransitioning(false); }); }, [mediaAutoNext, volume]); @@ -193,7 +202,13 @@ export function WebPlayer() { promise.then(() => { playerRef.current?.player2()?.ref?.getInternalPlayer().pause(); - playerRef.current?.setVolume(volume); + + const currentStatus = usePlayerStoreBase.getState().player.status; + if (currentStatus === PlayerStatus.PAUSED) { + playerRef.current?.pause(); + } else { + playerRef.current?.setVolume(volume); + } setIsTransitioning(false); }); }, [mediaAutoNext, volume]); @@ -527,6 +542,11 @@ function crossfadeHandler(args: { if (!isTransitioning) { if (duration > 0 && currentTime > duration - crossfadeDuration) { + // Skip pre-starting next player if pauseOnNextSongEnd is set + if (usePlayerStoreBase.getState().player.pauseOnNextSongEnd) { + return; + } + nextPlayer.setVolume(0); nextPlayer.ref?.getInternalPlayer().play(); return setIsTransitioning(player); @@ -616,6 +636,11 @@ function gaplessHandler(args: { const durationPadding = getDurationPadding(isFlac); if (currentTime + durationPadding >= duration) { + // Skip pre-starting next player if pauseOnNextSongEnd is set + if (usePlayerStoreBase.getState().player.pauseOnNextSongEnd) { + return null; + } + return nextPlayer.ref ?.getInternalPlayer() ?.play() diff --git a/src/renderer/features/player/components/sleep-timer-button.tsx b/src/renderer/features/player/components/sleep-timer-button.tsx index d1f344bdb..e561ffd17 100644 --- a/src/renderer/features/player/components/sleep-timer-button.tsx +++ b/src/renderer/features/player/components/sleep-timer-button.tsx @@ -96,24 +96,19 @@ const useSleepTimer = () => { [handleOnCurrentSongChange, handleOnPlayerProgress], ); - // End-of-song mode: subscribe to player index changes + // End-of-song mode: set the pauseOnNextSongEnd flag so that + // mediaAutoNext returns PAUSED status when the current song ends. + // This is a generic player mechanism — the web player handles it + // without needing to know about the sleep timer. useEffect(() => { if (!active || mode !== 'endOfSong') return; - const initialIndex = usePlayerStoreBase.getState().player.index; + usePlayerStoreBase.getState().setPauseOnNextSongEnd(true); - const unsub = usePlayerStoreBase.subscribe( - (state) => state.player.index, - (index) => { - if (index !== initialIndex) { - cancelTimer(); - mediaPauseRef.current(); - } - }, - ); - - return () => unsub(); - }, [active, mode, cancelTimer]); + return () => { + usePlayerStoreBase.getState().setPauseOnNextSongEnd(false); + }; + }, [active, mode]); }; export const SleepTimerHookInner = () => { diff --git a/src/renderer/store/player.store.ts b/src/renderer/store/player.store.ts index a412395b8..e94885f4e 100644 --- a/src/renderer/store/player.store.ts +++ b/src/renderer/store/player.store.ts @@ -67,6 +67,7 @@ interface Actions { moveSelectedToTop: (items: QueueSong[]) => void; setCrossfadeDuration: (duration: number) => void; setCrossfadeStyle: (style: CrossfadeStyle) => void; + setPauseOnNextSongEnd: (value: boolean) => void; setQueue: (data: Song[], index?: number, position?: number) => void; setRepeat: (repeat: PlayerRepeat) => void; setShuffle: (shuffle: PlayerShuffle) => void; @@ -91,6 +92,7 @@ interface State { crossfadeStyle: CrossfadeStyle; index: number; muted: boolean; + pauseOnNextSongEnd: boolean; playerNum: 1 | 2; repeat: PlayerRepeat; seekToTimestamp: string; @@ -295,6 +297,7 @@ const initialState: State = { crossfadeStyle: CrossfadeStyle.EQUAL_POWER, index: -1, muted: false, + pauseOnNextSongEnd: false, playerNum: 1, repeat: PlayerRepeat.NONE, seekToTimestamp: uniqueSeekToTimestamp(0), @@ -882,13 +885,19 @@ export const usePlayerStoreBase = createWithEqualityFn()( playbackLength, repeat, ); - const newStatus = shouldPause ? PlayerStatus.PAUSED : PlayerStatus.PLAYING; + const pauseOnNext = player.pauseOnNextSongEnd; + const newStatus = + shouldPause || pauseOnNext ? PlayerStatus.PAUSED : PlayerStatus.PLAYING; set((state) => { state.player.index = nextPlaybackIndex; state.player.playerNum = newPlayerNum; setTimestampStore(0); state.player.status = newStatus; + + if (pauseOnNext) { + state.player.pauseOnNextSongEnd = false; + } }); if (repeat === PlayerRepeat.ONE && nextPlaybackIndex === currentIndex) { @@ -1315,6 +1324,11 @@ export const usePlayerStoreBase = createWithEqualityFn()( state.player.crossfadeStyle = style; }); }, + setPauseOnNextSongEnd: (value: boolean) => { + set((state) => { + state.player.pauseOnNextSongEnd = value; + }); + }, setRepeat: (repeat: PlayerRepeat) => { set((state) => { state.player.repeat = repeat; @@ -1633,6 +1647,7 @@ export const usePlayerActions = () => { moveSelectedToTop: state.moveSelectedToTop, setCrossfadeDuration: state.setCrossfadeDuration, setCrossfadeStyle: state.setCrossfadeStyle, + setPauseOnNextSongEnd: state.setPauseOnNextSongEnd, setQueue: state.setQueue, setRepeat: state.setRepeat, setShuffle: state.setShuffle,