fix: sleep timer end-of-song mode (#1706)

This commit is contained in:
York
2026-03-08 12:23:19 +08:00
committed by GitHub
parent 6e3f0f2253
commit 6d2c084355
3 changed files with 52 additions and 17 deletions
@@ -20,6 +20,7 @@ import {
usePlayerData, usePlayerData,
usePlayerMuted, usePlayerMuted,
usePlayerProperties, usePlayerProperties,
usePlayerStoreBase,
usePlayerVolume, usePlayerVolume,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { toast } from '/@/shared/components/toast/toast'; import { toast } from '/@/shared/components/toast/toast';
@@ -180,7 +181,15 @@ export function WebPlayer() {
promise.then(() => { promise.then(() => {
playerRef.current?.player1()?.ref?.getInternalPlayer().pause(); 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); setIsTransitioning(false);
}); });
}, [mediaAutoNext, volume]); }, [mediaAutoNext, volume]);
@@ -193,7 +202,13 @@ export function WebPlayer() {
promise.then(() => { promise.then(() => {
playerRef.current?.player2()?.ref?.getInternalPlayer().pause(); 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); setIsTransitioning(false);
}); });
}, [mediaAutoNext, volume]); }, [mediaAutoNext, volume]);
@@ -527,6 +542,11 @@ function crossfadeHandler(args: {
if (!isTransitioning) { if (!isTransitioning) {
if (duration > 0 && currentTime > duration - crossfadeDuration) { 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.setVolume(0);
nextPlayer.ref?.getInternalPlayer().play(); nextPlayer.ref?.getInternalPlayer().play();
return setIsTransitioning(player); return setIsTransitioning(player);
@@ -616,6 +636,11 @@ function gaplessHandler(args: {
const durationPadding = getDurationPadding(isFlac); const durationPadding = getDurationPadding(isFlac);
if (currentTime + durationPadding >= duration) { if (currentTime + durationPadding >= duration) {
// Skip pre-starting next player if pauseOnNextSongEnd is set
if (usePlayerStoreBase.getState().player.pauseOnNextSongEnd) {
return null;
}
return nextPlayer.ref return nextPlayer.ref
?.getInternalPlayer() ?.getInternalPlayer()
?.play() ?.play()
@@ -96,24 +96,19 @@ const useSleepTimer = () => {
[handleOnCurrentSongChange, handleOnPlayerProgress], [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(() => { useEffect(() => {
if (!active || mode !== 'endOfSong') return; if (!active || mode !== 'endOfSong') return;
const initialIndex = usePlayerStoreBase.getState().player.index; usePlayerStoreBase.getState().setPauseOnNextSongEnd(true);
const unsub = usePlayerStoreBase.subscribe( return () => {
(state) => state.player.index, usePlayerStoreBase.getState().setPauseOnNextSongEnd(false);
(index) => { };
if (index !== initialIndex) { }, [active, mode]);
cancelTimer();
mediaPauseRef.current();
}
},
);
return () => unsub();
}, [active, mode, cancelTimer]);
}; };
export const SleepTimerHookInner = () => { export const SleepTimerHookInner = () => {
+16 -1
View File
@@ -67,6 +67,7 @@ interface Actions {
moveSelectedToTop: (items: QueueSong[]) => void; moveSelectedToTop: (items: QueueSong[]) => void;
setCrossfadeDuration: (duration: number) => void; setCrossfadeDuration: (duration: number) => void;
setCrossfadeStyle: (style: CrossfadeStyle) => void; setCrossfadeStyle: (style: CrossfadeStyle) => void;
setPauseOnNextSongEnd: (value: boolean) => void;
setQueue: (data: Song[], index?: number, position?: number) => void; setQueue: (data: Song[], index?: number, position?: number) => void;
setRepeat: (repeat: PlayerRepeat) => void; setRepeat: (repeat: PlayerRepeat) => void;
setShuffle: (shuffle: PlayerShuffle) => void; setShuffle: (shuffle: PlayerShuffle) => void;
@@ -91,6 +92,7 @@ interface State {
crossfadeStyle: CrossfadeStyle; crossfadeStyle: CrossfadeStyle;
index: number; index: number;
muted: boolean; muted: boolean;
pauseOnNextSongEnd: boolean;
playerNum: 1 | 2; playerNum: 1 | 2;
repeat: PlayerRepeat; repeat: PlayerRepeat;
seekToTimestamp: string; seekToTimestamp: string;
@@ -295,6 +297,7 @@ const initialState: State = {
crossfadeStyle: CrossfadeStyle.EQUAL_POWER, crossfadeStyle: CrossfadeStyle.EQUAL_POWER,
index: -1, index: -1,
muted: false, muted: false,
pauseOnNextSongEnd: false,
playerNum: 1, playerNum: 1,
repeat: PlayerRepeat.NONE, repeat: PlayerRepeat.NONE,
seekToTimestamp: uniqueSeekToTimestamp(0), seekToTimestamp: uniqueSeekToTimestamp(0),
@@ -882,13 +885,19 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
playbackLength, playbackLength,
repeat, repeat,
); );
const newStatus = shouldPause ? PlayerStatus.PAUSED : PlayerStatus.PLAYING; const pauseOnNext = player.pauseOnNextSongEnd;
const newStatus =
shouldPause || pauseOnNext ? PlayerStatus.PAUSED : PlayerStatus.PLAYING;
set((state) => { set((state) => {
state.player.index = nextPlaybackIndex; state.player.index = nextPlaybackIndex;
state.player.playerNum = newPlayerNum; state.player.playerNum = newPlayerNum;
setTimestampStore(0); setTimestampStore(0);
state.player.status = newStatus; state.player.status = newStatus;
if (pauseOnNext) {
state.player.pauseOnNextSongEnd = false;
}
}); });
if (repeat === PlayerRepeat.ONE && nextPlaybackIndex === currentIndex) { if (repeat === PlayerRepeat.ONE && nextPlaybackIndex === currentIndex) {
@@ -1315,6 +1324,11 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
state.player.crossfadeStyle = style; state.player.crossfadeStyle = style;
}); });
}, },
setPauseOnNextSongEnd: (value: boolean) => {
set((state) => {
state.player.pauseOnNextSongEnd = value;
});
},
setRepeat: (repeat: PlayerRepeat) => { setRepeat: (repeat: PlayerRepeat) => {
set((state) => { set((state) => {
state.player.repeat = repeat; state.player.repeat = repeat;
@@ -1633,6 +1647,7 @@ export const usePlayerActions = () => {
moveSelectedToTop: state.moveSelectedToTop, moveSelectedToTop: state.moveSelectedToTop,
setCrossfadeDuration: state.setCrossfadeDuration, setCrossfadeDuration: state.setCrossfadeDuration,
setCrossfadeStyle: state.setCrossfadeStyle, setCrossfadeStyle: state.setCrossfadeStyle,
setPauseOnNextSongEnd: state.setPauseOnNextSongEnd,
setQueue: state.setQueue, setQueue: state.setQueue,
setRepeat: state.setRepeat, setRepeat: state.setRepeat,
setShuffle: state.setShuffle, setShuffle: state.setShuffle,