mirror of
https://github.com/jeffvli/feishin.git
synced 2026-07-23 19:06:32 +02:00
Support using alt+click on prev/next to jump to previous/next album in queue (#2220)
This commit is contained in:
@@ -678,6 +678,7 @@
|
|||||||
"mute": "Mute",
|
"mute": "Mute",
|
||||||
"muted": "Muted",
|
"muted": "Muted",
|
||||||
"next": "Next",
|
"next": "Next",
|
||||||
|
"nextAlbum": "Alt+click for next album",
|
||||||
"play": "Play",
|
"play": "Play",
|
||||||
"playbackFetchCancel": "This is taking a while… close the notification to cancel",
|
"playbackFetchCancel": "This is taking a while… close the notification to cancel",
|
||||||
"playbackFetchInProgress": "Loading songs…",
|
"playbackFetchInProgress": "Loading songs…",
|
||||||
@@ -686,6 +687,7 @@
|
|||||||
"playRandom": "Play random",
|
"playRandom": "Play random",
|
||||||
"playSimilarSongs": "Play similar songs",
|
"playSimilarSongs": "Play similar songs",
|
||||||
"previous": "Previous",
|
"previous": "Previous",
|
||||||
|
"previousAlbum": "Alt+click for previous album",
|
||||||
"queue_clear": "Clear queue",
|
"queue_clear": "Clear queue",
|
||||||
"queue_moveToBottom": "Move selected to bottom",
|
"queue_moveToBottom": "Move selected to bottom",
|
||||||
"queue_moveToTop": "Move selected to top",
|
"queue_moveToTop": "Move selected to top",
|
||||||
@@ -927,10 +929,12 @@
|
|||||||
"hotkey_listShowPlayingSong": "Show playing song in list",
|
"hotkey_listShowPlayingSong": "Show playing song in list",
|
||||||
"hotkey_navigateHome": "Navigate to home",
|
"hotkey_navigateHome": "Navigate to home",
|
||||||
"hotkey_playbackNext": "Next track",
|
"hotkey_playbackNext": "Next track",
|
||||||
|
"hotkey_playbackNextAlbum": "Next album",
|
||||||
"hotkey_playbackPause": "Pause",
|
"hotkey_playbackPause": "Pause",
|
||||||
"hotkey_playbackPlay": "Play",
|
"hotkey_playbackPlay": "Play",
|
||||||
"hotkey_playbackPlayPause": "Play / pause",
|
"hotkey_playbackPlayPause": "Play / pause",
|
||||||
"hotkey_playbackPrevious": "Previous track",
|
"hotkey_playbackPrevious": "Previous track",
|
||||||
|
"hotkey_playbackPreviousAlbum": "Previous album",
|
||||||
"hotkey_playbackStop": "Stop",
|
"hotkey_playbackStop": "Stop",
|
||||||
"hotkey_rate0": "Rating clear",
|
"hotkey_rate0": "Rating clear",
|
||||||
"hotkey_rate1": "Rating 1 star",
|
"hotkey_rate1": "Rating 1 star",
|
||||||
|
|||||||
@@ -875,10 +875,12 @@ enum BindingActions {
|
|||||||
LOCAL_SEARCH = 'localSearch',
|
LOCAL_SEARCH = 'localSearch',
|
||||||
MUTE = 'volumeMute',
|
MUTE = 'volumeMute',
|
||||||
NEXT = 'next',
|
NEXT = 'next',
|
||||||
|
NEXT_ALBUM = 'nextAlbum',
|
||||||
PAUSE = 'pause',
|
PAUSE = 'pause',
|
||||||
PLAY = 'play',
|
PLAY = 'play',
|
||||||
PLAY_PAUSE = 'playPause',
|
PLAY_PAUSE = 'playPause',
|
||||||
PREVIOUS = 'previous',
|
PREVIOUS = 'previous',
|
||||||
|
PREVIOUS_ALBUM = 'previousAlbum',
|
||||||
SHUFFLE = 'toggleShuffle',
|
SHUFFLE = 'toggleShuffle',
|
||||||
SKIP_BACKWARD = 'skipBackward',
|
SKIP_BACKWARD = 'skipBackward',
|
||||||
SKIP_FORWARD = 'skipForward',
|
SKIP_FORWARD = 'skipForward',
|
||||||
@@ -906,11 +908,15 @@ const HOTKEY_ACTIONS: Record<BindingActions, () => void> = {
|
|||||||
[BindingActions.LOCAL_SEARCH]: () => {},
|
[BindingActions.LOCAL_SEARCH]: () => {},
|
||||||
[BindingActions.MUTE]: () => getMainWindow()?.webContents.send('renderer-player-volume-mute'),
|
[BindingActions.MUTE]: () => getMainWindow()?.webContents.send('renderer-player-volume-mute'),
|
||||||
[BindingActions.NEXT]: () => getMainWindow()?.webContents.send('renderer-player-next'),
|
[BindingActions.NEXT]: () => getMainWindow()?.webContents.send('renderer-player-next'),
|
||||||
|
[BindingActions.NEXT_ALBUM]: () =>
|
||||||
|
getMainWindow()?.webContents.send('renderer-player-next-album'),
|
||||||
[BindingActions.PAUSE]: () => getMainWindow()?.webContents.send('renderer-player-pause'),
|
[BindingActions.PAUSE]: () => getMainWindow()?.webContents.send('renderer-player-pause'),
|
||||||
[BindingActions.PLAY]: () => getMainWindow()?.webContents.send('renderer-player-play'),
|
[BindingActions.PLAY]: () => getMainWindow()?.webContents.send('renderer-player-play'),
|
||||||
[BindingActions.PLAY_PAUSE]: () =>
|
[BindingActions.PLAY_PAUSE]: () =>
|
||||||
getMainWindow()?.webContents.send('renderer-player-play-pause'),
|
getMainWindow()?.webContents.send('renderer-player-play-pause'),
|
||||||
[BindingActions.PREVIOUS]: () => getMainWindow()?.webContents.send('renderer-player-previous'),
|
[BindingActions.PREVIOUS]: () => getMainWindow()?.webContents.send('renderer-player-previous'),
|
||||||
|
[BindingActions.PREVIOUS_ALBUM]: () =>
|
||||||
|
getMainWindow()?.webContents.send('renderer-player-previous-album'),
|
||||||
[BindingActions.SHUFFLE]: () =>
|
[BindingActions.SHUFFLE]: () =>
|
||||||
getMainWindow()?.webContents.send('renderer-player-toggle-shuffle'),
|
getMainWindow()?.webContents.send('renderer-player-toggle-shuffle'),
|
||||||
[BindingActions.SKIP_BACKWARD]: () =>
|
[BindingActions.SKIP_BACKWARD]: () =>
|
||||||
|
|||||||
@@ -118,6 +118,10 @@ const rendererNext = (cb: (data: PlayerData) => void) => {
|
|||||||
ipcRenderer.on('renderer-player-next', (_, data) => cb(data));
|
ipcRenderer.on('renderer-player-next', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rendererNextAlbum = (cb: (data: PlayerData) => void) => {
|
||||||
|
ipcRenderer.on('renderer-player-next-album', (_, data) => cb(data));
|
||||||
|
};
|
||||||
|
|
||||||
const rendererPause = (cb: (data: PlayerData) => void) => {
|
const rendererPause = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-pause', (_, data) => cb(data));
|
ipcRenderer.on('renderer-player-pause', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
@@ -134,6 +138,10 @@ const rendererPrevious = (cb: (data: PlayerData) => void) => {
|
|||||||
ipcRenderer.on('renderer-player-previous', (_, data) => cb(data));
|
ipcRenderer.on('renderer-player-previous', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const rendererPreviousAlbum = (cb: (data: PlayerData) => void) => {
|
||||||
|
ipcRenderer.on('renderer-player-previous-album', (_, data) => cb(data));
|
||||||
|
};
|
||||||
|
|
||||||
const rendererStop = (cb: (data: PlayerData) => void) => {
|
const rendererStop = (cb: (data: PlayerData) => void) => {
|
||||||
ipcRenderer.on('renderer-player-stop', (_, data) => cb(data));
|
ipcRenderer.on('renderer-player-stop', (_, data) => cb(data));
|
||||||
};
|
};
|
||||||
@@ -215,11 +223,13 @@ export const mpvPlayerListener = {
|
|||||||
rendererError,
|
rendererError,
|
||||||
rendererMpvReconnect,
|
rendererMpvReconnect,
|
||||||
rendererNext,
|
rendererNext,
|
||||||
|
rendererNextAlbum,
|
||||||
rendererPause,
|
rendererPause,
|
||||||
rendererPlay,
|
rendererPlay,
|
||||||
rendererPlayerFallback,
|
rendererPlayerFallback,
|
||||||
rendererPlayPause,
|
rendererPlayPause,
|
||||||
rendererPrevious,
|
rendererPrevious,
|
||||||
|
rendererPreviousAlbum,
|
||||||
rendererQuit,
|
rendererQuit,
|
||||||
rendererSkipBackward,
|
rendererSkipBackward,
|
||||||
rendererSkipForward,
|
rendererSkipForward,
|
||||||
|
|||||||
@@ -56,13 +56,25 @@ export const useMainPlayerListener = () => {
|
|||||||
|
|
||||||
mpvPlayerListener.rendererNext(() => {
|
mpvPlayerListener.rendererNext(() => {
|
||||||
if (!isRadioActive) {
|
if (!isRadioActive) {
|
||||||
mediaNext();
|
mediaNext(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mpvPlayerListener.rendererNextAlbum(() => {
|
||||||
|
if (!isRadioActive) {
|
||||||
|
mediaNext(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
mpvPlayerListener.rendererPrevious(() => {
|
mpvPlayerListener.rendererPrevious(() => {
|
||||||
if (!isRadioActive) {
|
if (!isRadioActive) {
|
||||||
mediaPrevious();
|
mediaPrevious(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mpvPlayerListener.rendererPreviousAlbum(() => {
|
||||||
|
if (!isRadioActive) {
|
||||||
|
mediaPrevious(true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
useSkipButtons,
|
useSkipButtons,
|
||||||
} from '/@/renderer/store';
|
} from '/@/renderer/store';
|
||||||
import { Icon } from '/@/shared/components/icon/icon';
|
import { Icon } from '/@/shared/components/icon/icon';
|
||||||
|
import { Stack } from '/@/shared/components/stack/stack';
|
||||||
|
import { Text } from '/@/shared/components/text/text';
|
||||||
import { PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
|
import { PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types';
|
||||||
|
|
||||||
export const CenterControls = () => {
|
export const CenterControls = () => {
|
||||||
@@ -163,9 +165,18 @@ const PreviousButton = ({ disabled }: { disabled?: boolean }) => {
|
|||||||
<PlayerButton
|
<PlayerButton
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
icon={<Icon fill="default" icon="mediaPrevious" size={buttonSize} />}
|
icon={<Icon fill="default" icon="mediaPrevious" size={buttonSize} />}
|
||||||
onClick={mediaPrevious}
|
onClick={(e) => mediaPrevious(e.altKey)}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('player.previous'),
|
label: (
|
||||||
|
<Stack gap="xs" justify="center">
|
||||||
|
<Text fw={500} ta="center">
|
||||||
|
{t('player.previous')}
|
||||||
|
</Text>
|
||||||
|
<Text fw={500} isMuted size="xs" ta="center">
|
||||||
|
{t('player.previousAlbum')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
openDelay: 0,
|
openDelay: 0,
|
||||||
}}
|
}}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
@@ -239,9 +250,18 @@ const NextButton = ({ disabled }: { disabled?: boolean }) => {
|
|||||||
<PlayerButton
|
<PlayerButton
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
icon={<Icon fill="default" icon="mediaNext" size={buttonSize} />}
|
icon={<Icon fill="default" icon="mediaNext" size={buttonSize} />}
|
||||||
onClick={mediaNext}
|
onClick={(e) => mediaNext(e.altKey)}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('player.next'),
|
label: (
|
||||||
|
<Stack gap="xs" justify="center">
|
||||||
|
<Text fw={500} ta="center">
|
||||||
|
{t('player.next')}
|
||||||
|
</Text>
|
||||||
|
<Text fw={500} isMuted size="xs" ta="center">
|
||||||
|
{t('player.nextAlbum')}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
),
|
||||||
openDelay: 0,
|
openDelay: 0,
|
||||||
}}
|
}}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const MobileFullscreenPlayerControls = memo(
|
|||||||
<div className={styles.controlsContainer}>
|
<div className={styles.controlsContainer}>
|
||||||
<PlayerButton
|
<PlayerButton
|
||||||
icon={<Icon fill="default" icon="mediaPrevious" size="xl" />}
|
icon={<Icon fill="default" icon="mediaPrevious" size="xl" />}
|
||||||
onClick={mediaPrevious}
|
onClick={(e) => mediaPrevious(e.altKey)}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('player.previous'),
|
label: t('player.previous'),
|
||||||
openDelay: 0,
|
openDelay: 0,
|
||||||
@@ -71,7 +71,7 @@ export const MobileFullscreenPlayerControls = memo(
|
|||||||
/>
|
/>
|
||||||
<PlayerButton
|
<PlayerButton
|
||||||
icon={<Icon fill="default" icon="mediaNext" size="xl" />}
|
icon={<Icon fill="default" icon="mediaNext" size="xl" />}
|
||||||
onClick={mediaNext}
|
onClick={(e) => mediaNext(e.altKey)}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('player.next'),
|
label: t('player.next'),
|
||||||
openDelay: 0,
|
openDelay: 0,
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ export const MobilePlayerbar = () => {
|
|||||||
icon={<Icon fill="default" icon="mediaPrevious" size="md" />}
|
icon={<Icon fill="default" icon="mediaPrevious" size="md" />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
mediaPrevious();
|
mediaPrevious(e.altKey);
|
||||||
}}
|
}}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('player.previous'),
|
label: t('player.previous'),
|
||||||
@@ -223,7 +223,7 @@ export const MobilePlayerbar = () => {
|
|||||||
icon={<Icon fill="default" icon="mediaNext" size="md" />}
|
icon={<Icon fill="default" icon="mediaNext" size="md" />}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
mediaNext();
|
mediaNext(e.altKey);
|
||||||
}}
|
}}
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('player.next'),
|
label: t('player.next'),
|
||||||
|
|||||||
@@ -61,11 +61,11 @@ export interface PlayerContext {
|
|||||||
clearSelected: (items: QueueSong[]) => void;
|
clearSelected: (items: QueueSong[]) => void;
|
||||||
decreaseVolume: (amount: number) => void;
|
decreaseVolume: (amount: number) => void;
|
||||||
increaseVolume: (amount: number) => void;
|
increaseVolume: (amount: number) => void;
|
||||||
mediaNext: () => void;
|
mediaNext: (toNextAlbum: boolean) => void;
|
||||||
mediaPause: () => void;
|
mediaPause: () => void;
|
||||||
mediaPlay: (id?: string) => void;
|
mediaPlay: (id?: string) => void;
|
||||||
mediaPlayByIndex: (index: number) => void;
|
mediaPlayByIndex: (index: number) => void;
|
||||||
mediaPrevious: () => void;
|
mediaPrevious: (toPreviousAlbum: boolean) => void;
|
||||||
mediaSeekToTimestamp: (timestamp: number) => void;
|
mediaSeekToTimestamp: (timestamp: number) => void;
|
||||||
mediaSkipBackward: () => void;
|
mediaSkipBackward: () => void;
|
||||||
mediaSkipForward: () => void;
|
mediaSkipForward: () => void;
|
||||||
@@ -579,13 +579,16 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
[storeActions],
|
[storeActions],
|
||||||
);
|
);
|
||||||
|
|
||||||
const mediaNext = useCallback(() => {
|
const mediaNext = useCallback(
|
||||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaNext, {
|
(toNextAlbum: boolean) => {
|
||||||
category: LogCategory.PLAYER,
|
logFn.debug(logMsg[LogCategory.PLAYER].mediaNext, {
|
||||||
});
|
category: LogCategory.PLAYER,
|
||||||
|
});
|
||||||
|
|
||||||
storeActions.mediaNext();
|
storeActions.mediaNext(toNextAlbum);
|
||||||
}, [storeActions]);
|
},
|
||||||
|
[storeActions],
|
||||||
|
);
|
||||||
|
|
||||||
const mediaPause = useCallback(() => {
|
const mediaPause = useCallback(() => {
|
||||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaPause, {
|
logFn.debug(logMsg[LogCategory.PLAYER].mediaPause, {
|
||||||
@@ -619,13 +622,16 @@ export const PlayerProvider = ({ children }: { children: React.ReactNode }) => {
|
|||||||
[storeActions],
|
[storeActions],
|
||||||
);
|
);
|
||||||
|
|
||||||
const mediaPrevious = useCallback(() => {
|
const mediaPrevious = useCallback(
|
||||||
logFn.debug(logMsg[LogCategory.PLAYER].mediaPrevious, {
|
(toPreviousAlbum: boolean) => {
|
||||||
category: LogCategory.PLAYER,
|
logFn.debug(logMsg[LogCategory.PLAYER].mediaPrevious, {
|
||||||
});
|
category: LogCategory.PLAYER,
|
||||||
|
});
|
||||||
|
|
||||||
storeActions.mediaPrevious();
|
storeActions.mediaPrevious(toPreviousAlbum);
|
||||||
}, [storeActions]);
|
},
|
||||||
|
[storeActions],
|
||||||
|
);
|
||||||
|
|
||||||
const mediaStop = useCallback(
|
const mediaStop = useCallback(
|
||||||
(options?: { reset?: boolean }) => {
|
(options?: { reset?: boolean }) => {
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export const useMediaSession = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
playerRef.current.mediaNext();
|
playerRef.current.mediaNext(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaSession.setActionHandler('pause', () => {
|
mediaSession.setActionHandler('pause', () => {
|
||||||
@@ -116,7 +116,7 @@ export const useMediaSession = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
playerRef.current.mediaPrevious();
|
playerRef.current.mediaPrevious(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
mediaSession.setActionHandler('seekto', (e) => {
|
mediaSession.setActionHandler('seekto', (e) => {
|
||||||
|
|||||||
@@ -14,11 +14,13 @@ export const usePlaybackHotkeys = () => {
|
|||||||
binding: (typeof bindings)[keyof typeof bindings];
|
binding: (typeof bindings)[keyof typeof bindings];
|
||||||
handler: () => void;
|
handler: () => void;
|
||||||
}> = [
|
}> = [
|
||||||
{ binding: bindings.next, handler: () => player.mediaNext() },
|
{ binding: bindings.next, handler: () => player.mediaNext(false) },
|
||||||
|
{ binding: bindings.nextAlbum, handler: () => player.mediaNext(true) },
|
||||||
{ binding: bindings.pause, handler: () => player.mediaPause() },
|
{ binding: bindings.pause, handler: () => player.mediaPause() },
|
||||||
{ binding: bindings.play, handler: () => player.mediaPlay() },
|
{ binding: bindings.play, handler: () => player.mediaPlay() },
|
||||||
{ binding: bindings.playPause, handler: () => player.mediaTogglePlayPause() },
|
{ binding: bindings.playPause, handler: () => player.mediaTogglePlayPause() },
|
||||||
{ binding: bindings.previous, handler: () => player.mediaPrevious() },
|
{ binding: bindings.previous, handler: () => player.mediaPrevious(false) },
|
||||||
|
{ binding: bindings.previousAlbum, handler: () => player.mediaPrevious(true) },
|
||||||
{ binding: bindings.skipBackward, handler: () => player.mediaSkipBackward() },
|
{ binding: bindings.skipBackward, handler: () => player.mediaSkipBackward() },
|
||||||
{ binding: bindings.skipForward, handler: () => player.mediaSkipForward() },
|
{ binding: bindings.skipForward, handler: () => player.mediaSkipForward() },
|
||||||
{ binding: bindings.stop, handler: () => player.mediaStop() },
|
{ binding: bindings.stop, handler: () => player.mediaStop() },
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ const BINDINGS_MAP: Record<BindingActions, string> = {
|
|||||||
context: 'navigateHome',
|
context: 'navigateHome',
|
||||||
}),
|
}),
|
||||||
next: i18n.t('setting.hotkey', { context: 'playbackNext' }),
|
next: i18n.t('setting.hotkey', { context: 'playbackNext' }),
|
||||||
|
nextAlbum: i18n.t('setting.hotkey', { context: 'playbackNextAlbum' }),
|
||||||
pause: i18n.t('setting.hotkey', { context: 'playbackPause' }),
|
pause: i18n.t('setting.hotkey', { context: 'playbackPause' }),
|
||||||
play: i18n.t('setting.hotkey', { context: 'playbackPlay' }),
|
play: i18n.t('setting.hotkey', { context: 'playbackPlay' }),
|
||||||
playPause: i18n.t('setting.hotkey', {
|
playPause: i18n.t('setting.hotkey', {
|
||||||
@@ -78,6 +79,7 @@ const BINDINGS_MAP: Record<BindingActions, string> = {
|
|||||||
previous: i18n.t('setting.hotkey', {
|
previous: i18n.t('setting.hotkey', {
|
||||||
context: 'playbackPrevious',
|
context: 'playbackPrevious',
|
||||||
}),
|
}),
|
||||||
|
previousAlbum: i18n.t('setting.hotkey', { context: 'playbackPreviousAlbum' }),
|
||||||
rate0: i18n.t('setting.hotkey', { context: 'rate0' }),
|
rate0: i18n.t('setting.hotkey', { context: 'rate0' }),
|
||||||
rate1: i18n.t('setting.hotkey', { context: 'rate1' }),
|
rate1: i18n.t('setting.hotkey', { context: 'rate1' }),
|
||||||
rate2: i18n.t('setting.hotkey', { context: 'rate2' }),
|
rate2: i18n.t('setting.hotkey', { context: 'rate2' }),
|
||||||
|
|||||||
@@ -52,11 +52,11 @@ interface Actions {
|
|||||||
isFirstTrackInQueue: () => boolean;
|
isFirstTrackInQueue: () => boolean;
|
||||||
isLastTrackInQueue: () => boolean;
|
isLastTrackInQueue: () => boolean;
|
||||||
mediaAutoNext: () => PlayerData;
|
mediaAutoNext: () => PlayerData;
|
||||||
mediaNext: () => void;
|
mediaNext: (toNextAlbum: boolean) => void;
|
||||||
mediaPause: () => void;
|
mediaPause: () => void;
|
||||||
mediaPlay: (id?: string) => void;
|
mediaPlay: (id?: string) => void;
|
||||||
mediaPlayByIndex: (index: number) => void;
|
mediaPlayByIndex: (index: number) => void;
|
||||||
mediaPrevious: () => void;
|
mediaPrevious: (toPreviousAlbum: boolean) => void;
|
||||||
mediaSeekToTimestamp: (timestamp: number) => void;
|
mediaSeekToTimestamp: (timestamp: number) => void;
|
||||||
mediaSkipBackward: (offset?: number) => void;
|
mediaSkipBackward: (offset?: number) => void;
|
||||||
mediaSkipForward: (offset?: number) => void;
|
mediaSkipForward: (offset?: number) => void;
|
||||||
@@ -1044,7 +1044,7 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
status: newStatus,
|
status: newStatus,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mediaNext: () => {
|
mediaNext: (toNextAlbum) => {
|
||||||
const state = get();
|
const state = get();
|
||||||
const currentIndex = state.player.index;
|
const currentIndex = state.player.index;
|
||||||
const player = state.player;
|
const player = state.player;
|
||||||
@@ -1072,11 +1072,30 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { nextIndex, shouldStop } = calculateNextIndex(
|
const nextIndexProps = calculateNextIndex(currentIndex, playbackLength, repeat);
|
||||||
currentIndex,
|
let { nextIndex } = nextIndexProps;
|
||||||
playbackLength,
|
const { shouldStop } = nextIndexProps;
|
||||||
repeat,
|
|
||||||
);
|
if (toNextAlbum && !shouldStop) {
|
||||||
|
const currentItem = queue.items[currentIndex];
|
||||||
|
const [start, end] = findLastAlbumRange(queue.items);
|
||||||
|
const isOnLastAlbum = start <= currentIndex && currentIndex <= end;
|
||||||
|
if (isOnLastAlbum) {
|
||||||
|
const nextIndexWithNextAlbum = queue.items.findIndex(
|
||||||
|
(i) => i.albumId !== currentItem.albumId,
|
||||||
|
);
|
||||||
|
|
||||||
|
nextIndex = nextIndexWithNextAlbum;
|
||||||
|
} else {
|
||||||
|
const queueStartingFromCurrent = queue.items.slice(currentIndex);
|
||||||
|
const nextIndexWithNextAlbum = queueStartingFromCurrent.findIndex(
|
||||||
|
(i) => i.albumId !== currentItem.albumId,
|
||||||
|
);
|
||||||
|
nextIndex =
|
||||||
|
nextIndexWithNextAlbum +
|
||||||
|
(queue.items.length - queueStartingFromCurrent.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (shouldStop) {
|
if (shouldStop) {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -1194,7 +1213,7 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mediaPrevious: () => {
|
mediaPrevious: (toPreviousAlbum) => {
|
||||||
const currentIndex = get().player.index;
|
const currentIndex = get().player.index;
|
||||||
const player = get().player;
|
const player = get().player;
|
||||||
const queue = get().getQueueOrder();
|
const queue = get().getQueueOrder();
|
||||||
@@ -1217,6 +1236,11 @@ export const usePlayerStoreBase = createWithEqualityFn<PlayerState>()(
|
|||||||
} else if (player.repeat === PlayerRepeat.NONE && isFirstTrack) {
|
} else if (player.repeat === PlayerRepeat.NONE && isFirstTrack) {
|
||||||
// Repeat none: stay on first track if already there
|
// Repeat none: stay on first track if already there
|
||||||
previousIndex = currentIndex;
|
previousIndex = currentIndex;
|
||||||
|
} else if (toPreviousAlbum) {
|
||||||
|
previousIndex = Math.max(
|
||||||
|
0,
|
||||||
|
findIndexWithPreviousAlbum(queue.items, currentIndex),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
// Otherwise, go to previous track
|
// Otherwise, go to previous track
|
||||||
previousIndex = Math.max(0, currentIndex - 1);
|
previousIndex = Math.max(0, currentIndex - 1);
|
||||||
@@ -2262,6 +2286,47 @@ function cleanupOrphanedSongs(state: any): boolean {
|
|||||||
return hasOrphans;
|
return hasOrphans;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findIndexWithPreviousAlbum(queueItems: QueueSong[], currentIndex: number) {
|
||||||
|
const queueBeforeCurrent = queueItems.slice(0, currentIndex);
|
||||||
|
const currentItem = queueItems[currentIndex];
|
||||||
|
|
||||||
|
const previousAlbumIdInQueue = queueBeforeCurrent.findLast(
|
||||||
|
(i) => i.albumId !== currentItem.albumId,
|
||||||
|
)?.albumId;
|
||||||
|
|
||||||
|
let prevIndex = -1;
|
||||||
|
|
||||||
|
if (previousAlbumIdInQueue) {
|
||||||
|
for (let index = queueBeforeCurrent.length - 1; index > -1; index--) {
|
||||||
|
const element = queueBeforeCurrent[index];
|
||||||
|
if (element.albumId === previousAlbumIdInQueue) {
|
||||||
|
prevIndex = index;
|
||||||
|
}
|
||||||
|
if (prevIndex > -1 && element.albumId !== previousAlbumIdInQueue) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return prevIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findLastAlbumRange(queueItems: QueueSong[]) {
|
||||||
|
const lastAlbumId = queueItems.at(-1)?.albumId;
|
||||||
|
const rangeEnd = queueItems.length - 1;
|
||||||
|
let rangeStart = rangeEnd;
|
||||||
|
|
||||||
|
for (let index = rangeEnd; index > -1; index--) {
|
||||||
|
const element = queueItems[index];
|
||||||
|
rangeStart = index;
|
||||||
|
if (element.albumId !== lastAlbumId) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [rangeStart + 1, rangeEnd];
|
||||||
|
}
|
||||||
|
|
||||||
function parseUniqueSeekToTimestamp(timestamp: string) {
|
function parseUniqueSeekToTimestamp(timestamp: string) {
|
||||||
return Number(timestamp.split('-')[0]);
|
return Number(timestamp.split('-')[0]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,10 +145,12 @@ const BindingActionsSchema = z.enum([
|
|||||||
'volumeMute',
|
'volumeMute',
|
||||||
'navigateHome',
|
'navigateHome',
|
||||||
'next',
|
'next',
|
||||||
|
'nextAlbum',
|
||||||
'pause',
|
'pause',
|
||||||
'play',
|
'play',
|
||||||
'playPause',
|
'playPause',
|
||||||
'previous',
|
'previous',
|
||||||
|
'previousAlbum',
|
||||||
'rate0',
|
'rate0',
|
||||||
'rate1',
|
'rate1',
|
||||||
'rate2',
|
'rate2',
|
||||||
@@ -847,10 +849,12 @@ export enum BindingActions {
|
|||||||
MUTE = 'volumeMute',
|
MUTE = 'volumeMute',
|
||||||
NAVIGATE_HOME = 'navigateHome',
|
NAVIGATE_HOME = 'navigateHome',
|
||||||
NEXT = 'next',
|
NEXT = 'next',
|
||||||
|
NEXT_ALBUM = 'nextAlbum',
|
||||||
PAUSE = 'pause',
|
PAUSE = 'pause',
|
||||||
PLAY = 'play',
|
PLAY = 'play',
|
||||||
PLAY_PAUSE = 'playPause',
|
PLAY_PAUSE = 'playPause',
|
||||||
PREVIOUS = 'previous',
|
PREVIOUS = 'previous',
|
||||||
|
PREVIOUS_ALBUM = 'previousAlbum',
|
||||||
RATE_0 = 'rate0',
|
RATE_0 = 'rate0',
|
||||||
RATE_1 = 'rate1',
|
RATE_1 = 'rate1',
|
||||||
RATE_2 = 'rate2',
|
RATE_2 = 'rate2',
|
||||||
@@ -1322,10 +1326,12 @@ const initialState: SettingsState = {
|
|||||||
localSearch: { allowGlobal: false, hotkey: 'mod+f', isGlobal: false },
|
localSearch: { allowGlobal: false, hotkey: 'mod+f', isGlobal: false },
|
||||||
navigateHome: { allowGlobal: false, hotkey: '', isGlobal: false },
|
navigateHome: { allowGlobal: false, hotkey: '', isGlobal: false },
|
||||||
next: { allowGlobal: true, hotkey: '', isGlobal: false },
|
next: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
|
nextAlbum: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
pause: { allowGlobal: true, hotkey: '', isGlobal: false },
|
pause: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
play: { allowGlobal: true, hotkey: '', isGlobal: false },
|
play: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
playPause: { allowGlobal: true, hotkey: 'space', isGlobal: false },
|
playPause: { allowGlobal: true, hotkey: 'space', isGlobal: false },
|
||||||
previous: { allowGlobal: true, hotkey: '', isGlobal: false },
|
previous: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
|
previousAlbum: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
rate0: { allowGlobal: true, hotkey: '', isGlobal: false },
|
rate0: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
rate1: { allowGlobal: true, hotkey: '', isGlobal: false },
|
rate1: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
rate2: { allowGlobal: true, hotkey: '', isGlobal: false },
|
rate2: { allowGlobal: true, hotkey: '', isGlobal: false },
|
||||||
|
|||||||
@@ -342,31 +342,31 @@ export const sortSongsByFetchedOrder = (
|
|||||||
|
|
||||||
// Group songs by the fetched ID they belong to
|
// Group songs by the fetched ID they belong to
|
||||||
const songsByFetchedId = new Map<string, Song[]>();
|
const songsByFetchedId = new Map<string, Song[]>();
|
||||||
|
const fetchedIdsSet = new Set(fetchedIds);
|
||||||
|
|
||||||
for (const song of songs) {
|
for (const song of songs) {
|
||||||
let matchedId: string | undefined;
|
let matchedId: string | undefined;
|
||||||
|
|
||||||
switch (itemType) {
|
switch (itemType) {
|
||||||
case LibraryItem.ALBUM:
|
case LibraryItem.ALBUM:
|
||||||
matchedId = fetchedIds.find((id) => song.albumId === id);
|
matchedId = fetchedIdsSet.has(song.albumId) ? song.albumId : undefined;
|
||||||
break;
|
break;
|
||||||
case LibraryItem.ALBUM_ARTIST:
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
matchedId = fetchedIds.find((id) =>
|
matchedId = song.albumArtists.find((a) => fetchedIdsSet.has(a.id))?.id;
|
||||||
song.albumArtists.some((artist) => artist.id === id),
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case LibraryItem.ARTIST:
|
case LibraryItem.ARTIST:
|
||||||
matchedId = fetchedIds.find((id) =>
|
matchedId = song.artists.find((a) => fetchedIdsSet.has(a.id))?.id;
|
||||||
song.artists.some((artist) => artist.id === id),
|
|
||||||
);
|
|
||||||
break;
|
break;
|
||||||
case LibraryItem.GENRE:
|
case LibraryItem.GENRE:
|
||||||
matchedId = fetchedIds.find((id) => song.genres.some((genre) => genre.id === id));
|
matchedId = song.genres.find((a) => fetchedIdsSet.has(a.id))?.id;
|
||||||
break;
|
break;
|
||||||
case LibraryItem.PLAYLIST:
|
case LibraryItem.PLAYLIST:
|
||||||
// For playlists, we might need to track which playlist each song came from
|
// For playlists, we might need to track which playlist each song came from
|
||||||
// This is a simplified approach - you may need to adjust based on your data structure
|
// This is a simplified approach - you may need to adjust based on your data structure
|
||||||
matchedId = fetchedIds.find((id) => song.playlistItemId === id);
|
matchedId =
|
||||||
|
song.playlistItemId && fetchedIdsSet.has(song.playlistItemId)
|
||||||
|
? song.playlistItemId
|
||||||
|
: undefined;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
Reference in New Issue
Block a user