diff --git a/src/renderer/features/player/hooks/use-center-controls.ts b/src/renderer/features/player/hooks/use-center-controls.ts index f6744c9f3..2e7841cb3 100644 --- a/src/renderer/features/player/hooks/use-center-controls.ts +++ b/src/renderer/features/player/hooks/use-center-controls.ts @@ -1,6 +1,11 @@ import { useCallback, useEffect } from 'react'; import isElectron from 'is-electron'; -import { PlaybackType, PlayerStatus } from '@/renderer/types'; +import { + PlaybackType, + PlayerRepeat, + PlayerShuffle, + PlayerStatus, +} from '@/renderer/types'; import { usePlayerStore } from '../../../store'; import { useSettingsStore } from '../../../store/settings.store'; import { mpvPlayer } from '../utils/mpv-player'; @@ -11,14 +16,21 @@ export const useCenterControls = (args: { playersRef: any }) => { const { playersRef } = args; const settings = useSettingsStore((state) => state.player); + const setShuffle = usePlayerStore((state) => state.setShuffle); + const setRepeat = usePlayerStore((state) => state.setRepeat); const play = usePlayerStore((state) => state.play); const pause = usePlayerStore((state) => state.pause); const prev = usePlayerStore((state) => state.prev); const next = usePlayerStore((state) => state.next); + const setCurrentIndex = usePlayerStore((state) => state.setCurrentIndex); + const autoNext = usePlayerStore((state) => state.autoNext); const queue = usePlayerStore((state) => state.queue.default); const playerStatus = usePlayerStore((state) => state.current.status); const currentPlayer = usePlayerStore((state) => state.current.player); - const currentTime = usePlayerStore((state) => state.current.time); + const repeat = usePlayerStore((state) => state.repeat); + const shuffle = usePlayerStore((state) => state.shuffle); + const playerType = useSettingsStore((state) => state.player.type); + const setCurrentTime = usePlayerStore((state) => state.setCurrentTime); const player1Ref = playersRef?.current?.player1; @@ -39,11 +51,11 @@ export const useCenterControls = (args: { playersRef: any }) => { nextPlayerRef.getInternalPlayer().pause(); }, [currentPlayerRef, nextPlayerRef]); - const stopPlayback = () => { + const stopPlayback = useCallback(() => { player1Ref.getInternalPlayer().pause(); player2Ref.getInternalPlayer().pause(); resetPlayers(); - }; + }, [player1Ref, player2Ref, resetPlayers]); const isMpvPlayer = isElectron() && settings.type === PlaybackType.LOCAL; @@ -65,7 +77,7 @@ export const useCenterControls = (args: { playersRef: any }) => { pause(); }, [isMpvPlayer, pause]); - const handleStop = () => { + const handleStop = useCallback(() => { if (isMpvPlayer) { mpvPlayer.stop(); } else { @@ -74,33 +86,335 @@ export const useCenterControls = (args: { playersRef: any }) => { setCurrentTime(0); pause(); - }; + }, [isMpvPlayer, pause, setCurrentTime, stopPlayback]); + + const handleToggleShuffle = useCallback(() => { + if (shuffle === PlayerShuffle.NONE) { + const playerData = setShuffle(PlayerShuffle.TRACK); + return mpvPlayer.setQueueNext(playerData); + } + + const playerData = setShuffle(PlayerShuffle.NONE); + return mpvPlayer.setQueueNext(playerData); + }, [setShuffle, shuffle]); + + const handleToggleRepeat = useCallback(() => { + if (repeat === PlayerRepeat.NONE) { + const playerData = setRepeat(PlayerRepeat.ALL); + return mpvPlayer.setQueueNext(playerData); + } + + if (repeat === PlayerRepeat.ALL) { + const playerData = setRepeat(PlayerRepeat.ONE); + return mpvPlayer.setQueueNext(playerData); + } + + return setRepeat(PlayerRepeat.NONE); + }, [repeat, setRepeat]); + + const checkIsLastTrack = useCallback(() => { + const currentIndex = + shuffle === PlayerShuffle.NONE + ? usePlayerStore.getState().current.index + : usePlayerStore.getState().current.shuffledIndex; + + const queueLength = queue.length; + + return currentIndex >= queueLength - 1; + }, [queue.length, shuffle]); + + const checkIsFirstTrack = useCallback(() => { + const currentIndex = + shuffle === PlayerShuffle.NONE + ? usePlayerStore.getState().current.index + : usePlayerStore.getState().current.shuffledIndex; + + return currentIndex === 0; + }, [shuffle]); + + const handleAutoNext = useCallback(() => { + const isLastTrack = checkIsLastTrack(); + + const handleRepeatAll = { + local: () => { + const playerData = autoNext(); + mpvPlayer.playerAutoNext(playerData); + play(); + }, + web: () => { + autoNext(); + }, + }; + + const handleRepeatNone = { + local: () => { + if (isLastTrack) { + const playerData = setCurrentIndex(0); + mpvPlayer.setQueue(playerData); + mpvPlayer.pause(); + pause(); + } else { + const playerData = autoNext(); + mpvPlayer.playerAutoNext(playerData); + play(); + } + }, + web: () => { + if (isLastTrack) { + resetPlayers(); + } else { + next(); + resetPlayers(); + } + }, + }; + + const handleRepeatOne = { + local: () => { + const playerData = autoNext(); + mpvPlayer.playerAutoNext(playerData); + play(); + }, + web: () => { + if (isLastTrack) { + resetPlayers(); + } else { + next(); + resetPlayers(); + } + }, + }; + + switch (repeat) { + case PlayerRepeat.NONE: + handleRepeatNone[playerType](); + break; + case PlayerRepeat.ALL: + handleRepeatAll[playerType](); + break; + case PlayerRepeat.ONE: + handleRepeatOne[playerType](); + break; + + default: + break; + } + }, [ + autoNext, + checkIsLastTrack, + next, + pause, + play, + playerType, + repeat, + resetPlayers, + setCurrentIndex, + ]); const handleNextTrack = useCallback(() => { - const playerData = next(); + const isLastTrack = checkIsLastTrack(); - if (isMpvPlayer) { - mpvPlayer.setQueue(playerData); - mpvPlayer.next(); - } else { - resetPlayers(); + const handleRepeatAll = { + local: () => { + const playerData = next(); + mpvPlayer.setQueue(playerData); + mpvPlayer.next(); + }, + web: () => { + next(); + }, + }; + + const handleRepeatNone = { + local: () => { + if (isLastTrack) { + const playerData = setCurrentIndex(0); + mpvPlayer.setQueue(playerData); + mpvPlayer.pause(); + pause(); + } else { + const playerData = next(); + mpvPlayer.setQueue(playerData); + mpvPlayer.next(); + } + }, + web: () => { + if (isLastTrack) { + setCurrentIndex(0); + resetPlayers(); + pause(); + } else { + next(); + resetPlayers(); + } + }, + }; + + const handleRepeatOne = { + local: () => { + const playerData = next(); + mpvPlayer.setQueue(playerData); + mpvPlayer.next(); + }, + web: () => { + if (!isLastTrack) { + resetPlayers(); + } else { + next(); + resetPlayers(); + } + }, + }; + + switch (repeat) { + case PlayerRepeat.NONE: + handleRepeatNone[playerType](); + break; + case PlayerRepeat.ALL: + handleRepeatAll[playerType](); + break; + case PlayerRepeat.ONE: + handleRepeatOne[playerType](); + break; + + default: + break; } setCurrentTime(0); - }, [isMpvPlayer, next, resetPlayers, setCurrentTime]); + }, [ + checkIsLastTrack, + next, + pause, + playerType, + repeat, + resetPlayers, + setCurrentIndex, + setCurrentTime, + ]); const handlePrevTrack = useCallback(() => { - const playerData = prev(); + const currentTime = isMpvPlayer + ? usePlayerStore.getState().current.time + : currentPlayerRef.getCurrentTime(); - if (isMpvPlayer) { - mpvPlayer.setQueue(playerData); - mpvPlayer.previous(); - } else { - resetPlayers(); + // Reset the current track more than 10 seconds have elapsed + if (currentTime >= 10) { + if (isMpvPlayer) { + return mpvPlayer.seekTo(0); + } + return currentPlayerRef.seekTo(0); } - setCurrentTime(0); - }, [isMpvPlayer, prev, resetPlayers, setCurrentTime]); + const isFirstTrack = checkIsFirstTrack(); + + const handleRepeatAll = { + local: () => { + if (!isFirstTrack) { + const playerData = prev(); + mpvPlayer.setQueue(playerData); + mpvPlayer.previous(); + } else { + const playerData = setCurrentIndex(queue.length - 1); + mpvPlayer.setQueue(playerData); + mpvPlayer.previous(); + } + }, + web: () => { + if (!isFirstTrack) { + prev(); + resetPlayers(); + } else { + setCurrentIndex(queue.length - 1); + resetPlayers(); + } + }, + }; + + const handleRepeatNone = { + local: () => { + const playerData = prev(); + mpvPlayer.setQueue(playerData); + mpvPlayer.previous(); + }, + web: () => { + if (!isFirstTrack) { + prev(); + resetPlayers(); + } else { + resetPlayers(); + pause(); + } + }, + }; + + const handleRepeatOne = { + local: () => { + if (!isFirstTrack) { + const playerData = prev(); + mpvPlayer.setQueue(playerData); + mpvPlayer.previous(); + } else { + mpvPlayer.stop(); + } + }, + web: () => { + if (!isFirstTrack) { + prev(); + resetPlayers(); + } else { + resetPlayers(); + pause(); + } + }, + }; + + switch (repeat) { + case PlayerRepeat.NONE: + handleRepeatNone[playerType](); + break; + case PlayerRepeat.ALL: + handleRepeatAll[playerType](); + break; + case PlayerRepeat.ONE: + handleRepeatOne[playerType](); + break; + + default: + break; + } + + // if (isMpvPlayer) { + // if (shuffle === PlayerShuffle.TRACK) { + // // const playerData = setCurrentIndex(shuffleTrackPrevious()); + // // mpvPlayer.setQueue(playerData); + // mpvPlayer.previous(); + // } else if (shuffle === PlayerShuffle.ALBUM) { + // } else { + // const playerData = prev(); + // mpvPlayer.setQueue(playerData); + // mpvPlayer.previous(); + // } + // } + + // if (!isMpvPlayer) { + // resetPlayers(); + // } + + return setCurrentTime(0); + }, [ + checkIsFirstTrack, + currentPlayerRef, + isMpvPlayer, + pause, + playerType, + prev, + queue.length, + repeat, + resetPlayers, + setCurrentIndex, + setCurrentTime, + ]); const handlePlayPause = useCallback(() => { if (queue) { @@ -115,12 +429,16 @@ export const useCenterControls = (args: { playersRef: any }) => { }, [handlePause, handlePlay, playerStatus, queue]); const handleSkipBackward = (seconds: number) => { + const currentTime = isMpvPlayer + ? usePlayerStore.getState().current.time + : currentPlayerRef.getCurrentTime(); + if (isMpvPlayer) { const newTime = currentTime - seconds; mpvPlayer.seek(-seconds); setCurrentTime(newTime < 0 ? 0 : newTime); } else { - const newTime = currentPlayerRef?.getCurrentTime() - seconds; + const newTime = currentTime - seconds; resetNextPlayer(); setCurrentTime(newTime); currentPlayerRef.seekTo(newTime); @@ -128,12 +446,16 @@ export const useCenterControls = (args: { playersRef: any }) => { }; const handleSkipForward = (seconds: number) => { + const currentTime = isMpvPlayer + ? usePlayerStore.getState().current.time + : currentPlayerRef.getCurrentTime(); + if (isMpvPlayer) { const newTime = currentTime + seconds; mpvPlayer.seek(seconds); setCurrentTime(newTime); } else { - const checkNewTime = currentPlayerRef?.getCurrentTime() + seconds; + const checkNewTime = currentTime + seconds; const songDuration = currentPlayerRef.player.player.duration; const newTime = @@ -160,46 +482,61 @@ export const useCenterControls = (args: { playersRef: any }) => { useEffect(() => { ipc?.RENDERER_PLAYER_PLAY_PAUSE(() => { - const { status } = usePlayerStore.getState().current; - if (status === PlayerStatus.PAUSED) { - play(); + handlePlayPause(); - if (isMpvPlayer) { - mpvPlayer.play(); - } - } else { - pause(); - if (isMpvPlayer) { - mpvPlayer.pause(); - } - } + // const { status } = usePlayerStore.getState().current; + // if (status === PlayerStatus.PAUSED) { + // play(); + + // if (isMpvPlayer) { + // mpvPlayer.play(); + // } + // } else { + // pause(); + // if (isMpvPlayer) { + // mpvPlayer.pause(); + // } + // } }); ipc?.RENDERER_PLAYER_NEXT(() => { - const playerData = next(); + handleNextTrack(); + // const playerData = next(); - if (isMpvPlayer) { - mpvPlayer.setQueue(playerData); - mpvPlayer.next(); - } + // if (isMpvPlayer) { + // mpvPlayer.setQueue(playerData); + // mpvPlayer.next(); + // } }); ipc?.RENDERER_PLAYER_PREVIOUS(() => { - const playerData = prev(); - if (isMpvPlayer) { - mpvPlayer.setQueue(playerData); - mpvPlayer.previous(); - } + handlePrevTrack(); + + // const playerData = prev(); + // if (isMpvPlayer) { + // mpvPlayer.setQueue(playerData); + // mpvPlayer.previous(); + // } }); - ipc?.RENDERER_PLAYER_PLAY(() => play()); + ipc?.RENDERER_PLAYER_PLAY(() => handlePlay()); - ipc?.RENDERER_PLAYER_PAUSE(() => pause()); + ipc?.RENDERER_PLAYER_PAUSE(() => handlePause()); - ipc?.RENDERER_PLAYER_STOP(() => pause()); + ipc?.RENDERER_PLAYER_STOP(() => handleStop()); ipc?.RENDERER_PLAYER_CURRENT_TIME((_event, time) => setCurrentTime(time)); + ipc?.RENDERER_PLAYER_AUTO_NEXT(() => { + handleAutoNext(); + // const playerData = autoNext(); + // console.log('playerData', playerData); + // if (playerData.queue.next) { + // mpvPlayer.playerAutoNext(playerData); + // play(); + // } + }); + return () => { ipc?.removeAllListeners('renderer-player-play-pause'); ipc?.removeAllListeners('renderer-player-next'); @@ -208,8 +545,24 @@ export const useCenterControls = (args: { playersRef: any }) => { ipc?.removeAllListeners('renderer-player-pause'); ipc?.removeAllListeners('renderer-player-stop'); ipc?.removeAllListeners('renderer-player-current-time'); + ipc?.removeAllListeners('renderer-player-auto-next'); }; - }, [isMpvPlayer, next, pause, play, prev, setCurrentTime]); + }, [ + autoNext, + handleAutoNext, + handleNextTrack, + handlePause, + handlePlay, + handlePlayPause, + handlePrevTrack, + handleStop, + isMpvPlayer, + next, + pause, + play, + prev, + setCurrentTime, + ]); return { handleNextTrack, @@ -219,5 +572,7 @@ export const useCenterControls = (args: { playersRef: any }) => { handleSkipBackward, handleSkipForward, handleStop, + handleToggleRepeat, + handleToggleShuffle, }; }; diff --git a/src/renderer/features/player/utils/mpv-player.ts b/src/renderer/features/player/utils/mpv-player.ts index 245eb9769..a7eef6558 100644 --- a/src/renderer/features/player/utils/mpv-player.ts +++ b/src/renderer/features/player/utils/mpv-player.ts @@ -4,7 +4,7 @@ // renderer/preload.d.ts import isElectron from 'is-electron'; -import { PlayerData, usePlayerStore } from '../../../store'; +import { PlayerData } from '../../../store'; const ipc = isElectron() ? window.electron.ipcRenderer : null; @@ -34,15 +34,6 @@ const volume = (value: number) => ipc?.PLAYER_VOLUME(value); const mute = () => ipc?.PLAYER_MUTE(); -const { autoNext } = usePlayerStore.getState(); - -ipc?.RENDERER_PLAYER_AUTO_NEXT(() => { - const playerData = autoNext(); - if (playerData.queue.next) { - playerAutoNext(playerData); - } -}); - export const mpvPlayer = { currentTime, mute, diff --git a/src/renderer/store/player.store.ts b/src/renderer/store/player.store.ts index df4a240cf..c6453a344 100644 --- a/src/renderer/store/player.store.ts +++ b/src/renderer/store/player.store.ts @@ -1,158 +1,422 @@ /* eslint-disable prefer-destructuring */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import produce from 'immer'; import map from 'lodash/map'; +import shuffle from 'lodash/shuffle'; import { nanoid } from 'nanoid/non-secure'; import create from 'zustand'; import { devtools, persist } from 'zustand/middleware'; +import { immer } from 'zustand/middleware/immer'; import { Song } from '@/renderer/api/types'; -import { Play, PlayerStatus, UniqueId } from '@/renderer/types'; +import { + Play, + PlayerRepeat, + PlayerShuffle, + PlayerStatus, + UniqueId, +} from '@/renderer/types'; type QueueSong = Song & UniqueId; export interface PlayerState { current: { index: number; + nextIndex: number; player: 1 | 2; - song: QueueSong; + previousIndex: number; + shuffledIndex: number; + song?: QueueSong; status: PlayerStatus; time: number; }; muted: boolean; queue: { default: QueueSong[]; - previousNode: QueueSong; - shuffled: QueueSong[]; + previousNode?: QueueSong; + shuffled: string[]; sorted: QueueSong[]; }; + repeat: PlayerRepeat; + shuffle: PlayerShuffle; volume: number; } export interface PlayerData { current: { index: number; + nextIndex?: number; player: 1 | 2; - song: QueueSong; + previousIndex?: number; + shuffledIndex: number; + song?: QueueSong; status: PlayerStatus; }; - player1: QueueSong; - player2: QueueSong; + player1?: QueueSong; + player2?: QueueSong; queue: QueueData; } export interface QueueData { - current: QueueSong; - next: QueueSong; - previous: QueueSong; + current?: QueueSong; + next?: QueueSong; + previous?: QueueSong; } export interface PlayerSlice extends PlayerState { addToQueue: (songs: Song[], type: Play) => PlayerData; autoNext: () => PlayerData; + checkIsFirstTrack: () => boolean; + checkIsLastTrack: () => boolean; + // getNextTrack: () => QueueSong; + // getPreviousTrack: () => QueueSong; getPlayerData: () => PlayerData; getQueueData: () => QueueData; next: () => PlayerData; pause: () => void; play: () => void; - player1: () => QueueSong; - player2: () => QueueSong; + player1: () => QueueSong | undefined; + player2: () => QueueSong | undefined; prev: () => PlayerData; setCurrentIndex: (index: number) => PlayerData; setCurrentTime: (time: number) => void; setMuted: (muted: boolean) => void; + setRepeat: (type: PlayerRepeat) => PlayerData; + setShuffle: (type: PlayerShuffle) => PlayerData; + setShuffledIndex: (index: number) => PlayerData; + setStore: (data: Partial) => void; setVolume: (volume: number) => void; } export const usePlayerStore = create()( persist( devtools( - (set, get) => ({ + immer((set, get) => ({ addToQueue: (songs, type) => { + const shuffledIndex = get().current.shuffledIndex; + const shuffledQueue = get().queue.shuffled; const queueSongs = map(songs, (song) => ({ ...song, uniqueId: nanoid(), })); if (type === Play.NOW) { - set( - produce((state) => { + if (get().shuffle === PlayerShuffle.TRACK) { + const shuffledSongs = shuffle(queueSongs); + const foundIndex = queueSongs.findIndex( + (song) => song.uniqueId === shuffledSongs[0].uniqueId + ); + set((state) => { + state.queue.shuffled = shuffledSongs.map( + (song) => song.uniqueId + ); + }); + + set((state) => { + state.queue.default = queueSongs; + state.current.time = 0; + state.current.player = 1; + state.current.index = foundIndex; + state.current.shuffledIndex = 0; + state.current.song = shuffledSongs[0]; + }); + } else { + set((state) => { state.queue.default = queueSongs; state.current.time = 0; state.current.player = 1; state.current.index = 0; + state.current.shuffledIndex = 0; state.current.song = queueSongs[0]; - }) - ); + }); + } } else if (type === Play.LAST) { - set( - produce((state) => { - state.queue.default = [...get().queue.default, ...queueSongs]; - }) - ); + // Shuffle the queue after the current track + const shuffledQueueWithNewSongs = + get().shuffle === PlayerShuffle.TRACK + ? [ + ...shuffledQueue.slice(0, shuffledIndex + 1), + ...shuffle([ + ...queueSongs.map((song) => song.uniqueId), + ...shuffledQueue.slice(shuffledIndex + 1), + ]), + ] + : []; + + set((state) => { + state.queue.default = [...get().queue.default, ...queueSongs]; + state.queue.shuffled = shuffledQueueWithNewSongs; + }); } else if (type === Play.NEXT) { const queue = get().queue.default; const currentIndex = get().current.index; - set( - produce((state) => { - state.queue.default = [ - ...queue.slice(0, currentIndex + 1), - ...queueSongs, - ...queue.slice(currentIndex + 1), - ]; - }) - ); + // Shuffle the queue after the current track + const shuffledQueueWithNewSongs = + get().shuffle === PlayerShuffle.TRACK + ? [ + ...shuffledQueue.slice(0, shuffledIndex + 1), + ...shuffle([ + ...queueSongs.map((song) => song.uniqueId), + ...shuffledQueue.slice(shuffledIndex + 1), + ]), + ] + : []; + + set((state) => { + state.queue.default = [ + ...queue.slice(0, currentIndex + 1), + ...queueSongs, + ...queue.slice(currentIndex + 1), + ]; + state.queue.shuffled = shuffledQueueWithNewSongs; + }); } return get().getPlayerData(); }, autoNext: () => { - set( - produce((state) => { + const isLastTrack = get().checkIsLastTrack(); + const repeat = get().repeat; + + if (repeat === PlayerRepeat.ONE) { + const nextIndex = get().current.index; + + set((state) => { state.current.time = 0; - state.current.index += 1; + state.current.index = nextIndex; + state.current.shuffledIndex = get().current.shuffledIndex; state.current.player = state.current.player === 1 ? 2 : 1; - state.current.song = state.queue.default[state.current.index]; + state.current.song = get().queue.default[nextIndex]; state.queue.previousNode = get().current.song; - }) - ); + }); + } else if (get().shuffle === PlayerShuffle.TRACK) { + const nextShuffleIndex = isLastTrack + ? 0 + : get().current.shuffledIndex + 1; + + const nextSong = get().queue.default.find( + (song) => song.uniqueId === get().queue.shuffled[nextShuffleIndex] + ); + + const nextSongIndex = get().queue.default.findIndex( + (song) => song.uniqueId === nextSong!.uniqueId + ); + + set((state) => { + state.current.time = 0; + state.current.index = nextSongIndex!; + state.current.shuffledIndex = nextShuffleIndex; + state.current.player = state.current.player === 1 ? 2 : 1; + state.current.song = nextSong!; + state.queue.previousNode = get().current.song; + }); + } else { + const nextIndex = isLastTrack ? 0 : get().current.index + 1; + + set((state) => { + state.current.time = 0; + state.current.index = nextIndex; + state.current.player = state.current.player === 1 ? 2 : 1; + state.current.song = get().queue.default[nextIndex]; + state.queue.previousNode = get().current.song; + }); + } return get().getPlayerData(); }, + checkIsFirstTrack: () => { + const currentIndex = + get().shuffle === PlayerShuffle.TRACK + ? get().current.shuffledIndex + : get().current.index; + + return currentIndex === 0; + }, + checkIsLastTrack: () => { + const currentIndex = + get().shuffle === PlayerShuffle.TRACK + ? get().current.shuffledIndex + : get().current.index; + + return currentIndex === get().queue.default.length - 1; + }, current: { index: 0, + nextIndex: 0, player: 1, + previousIndex: 0, + shuffledIndex: 0, song: {} as QueueSong, status: PlayerStatus.PAUSED, time: 0, }, + // getNextTrack: () => { + // const shuffle = get().shuffle; + // const queue = get().queue.default; + // const shuffledQueue = get().queue.shuffled; + + // if (shuffle === PlayerShuffle.TRACK) { + + // } + + // const currentIndex = + // shuffle === PlayerShuffle.TRACK + // ? get().current.shuffledIndex + // : get().current.index; + + // const current = queue.find( + // (song) => song.uniqueId === queue[currentIndex] + // ) as QueueSong; + + // let nextSongIndex: number | undefined; + // if (repeat === PlayerRepeat.ALL) { + // if (isLastTrack) nextSongIndex = 0; + // else nextSongIndex = currentIndex + 1; + // } + + // if (repeat === PlayerRepeat.ONE) { + // nextSongIndex = currentIndex; + // } + + // if (repeat === PlayerRepeat.NONE) { + // if (isLastTrack) nextSongIndex = undefined; + // else nextSongIndex = currentIndex + 1; + // } + + // const next = nextSongIndex + // ? (queue.find( + // (song) => song.uniqueId === queue[nextSongIndex as number] + // ) as QueueSong) + // : undefined; + // }, getPlayerData: () => { const queue = get().queue.default; const currentPlayer = get().current.player; + const repeat = get().repeat; + const isLastTrack = get().checkIsLastTrack(); + const isFirstTrack = get().checkIsFirstTrack(); - const player1 = - currentPlayer === 1 - ? queue[get().current.index] - : queue[get().current.index + 1]; + let player1; + let player2; + if (get().shuffle === PlayerShuffle.TRACK) { + const shuffledQueue = get().queue.shuffled; + const shuffledIndex = get().current.shuffledIndex; + const current = queue.find( + (song) => song.uniqueId === shuffledQueue[shuffledIndex] + ) as QueueSong; - const player2 = + let nextSongIndex: number | undefined; + let previousSongIndex: number | undefined; + if (repeat === PlayerRepeat.ALL) { + if (isLastTrack) nextSongIndex = 0; + else nextSongIndex = shuffledIndex + 1; + + if (isFirstTrack) previousSongIndex = queue.length - 1; + else previousSongIndex = shuffledIndex - 1; + } + + if (repeat === PlayerRepeat.ONE) { + nextSongIndex = shuffledIndex; + previousSongIndex = shuffledIndex; + } + + if (repeat === PlayerRepeat.NONE) { + if (isLastTrack) nextSongIndex = undefined; + else nextSongIndex = shuffledIndex + 1; + + if (isFirstTrack) previousSongIndex = undefined; + else previousSongIndex = shuffledIndex - 1; + } + + const next = nextSongIndex + ? (queue.find( + (song) => + song.uniqueId === shuffledQueue[nextSongIndex as number] + ) as QueueSong) + : undefined; + + const previous = queue.find( + (song) => song.uniqueId === shuffledQueue[shuffledIndex - 1] + ) as QueueSong; + + player1 = currentPlayer === 1 ? current : next; + player2 = currentPlayer === 1 ? next : current; + + return { + current: { + index: get().current.index, + nextIndex: nextSongIndex, + player: get().current.player, + previousIndex: previousSongIndex, + shuffledIndex: get().current.shuffledIndex, + song: get().current.song, + status: get().current.status, + }, + player1, + player2, + queue: { + current, + next, + previous, + }, + }; + } + + const currentIndex = get().current.index; + + let nextSongIndex; + let previousSongIndex; + if (repeat === PlayerRepeat.ALL) { + if (isLastTrack) nextSongIndex = 0; + else nextSongIndex = currentIndex + 1; + + if (isFirstTrack) previousSongIndex = queue.length - 1; + else previousSongIndex = currentIndex - 1; + } + + if (repeat === PlayerRepeat.ONE) { + nextSongIndex = currentIndex; + previousSongIndex = currentIndex; + } + + if (repeat === PlayerRepeat.NONE) { + if (isLastTrack) nextSongIndex = undefined; + else nextSongIndex = currentIndex + 1; + + if (isFirstTrack) previousSongIndex = undefined; + else previousSongIndex = currentIndex - 1; + } + + player1 = currentPlayer === 1 - ? queue[get().current.index + 1] - : queue[get().current.index]; + ? queue[currentIndex] + : nextSongIndex !== undefined + ? queue[nextSongIndex] + : undefined; + + player2 = + currentPlayer === 1 + ? nextSongIndex !== undefined + ? queue[nextSongIndex] + : undefined + : queue[currentIndex]; return { current: { - index: get().current.index, + index: currentIndex, + nextIndex: nextSongIndex, player: get().current.player, + previousIndex: previousSongIndex, + shuffledIndex: get().current.shuffledIndex, song: get().current.song, status: get().current.status, }, player1, player2, queue: { - current: queue[get().current.index], - next: queue[get().current.index + 1], - previous: queue[get().current.index - 1], + current: queue[currentIndex], + next: + nextSongIndex !== undefined ? queue[nextSongIndex] : undefined, + previous: queue[currentIndex - 1], }, }; }, @@ -166,31 +430,60 @@ export const usePlayerStore = create()( }, muted: false, next: () => { - set( - produce((state) => { + const isLastTrack = get().checkIsLastTrack(); + const repeat = get().repeat; + + if (get().shuffle === PlayerShuffle.TRACK) { + const nextShuffleIndex = isLastTrack + ? 0 + : get().current.shuffledIndex + 1; + + const nextSong = get().queue.default.find( + (song) => song.uniqueId === get().queue.shuffled[nextShuffleIndex] + ); + + const nextSongIndex = get().queue.default.findIndex( + (song) => song.uniqueId === nextSong?.uniqueId + ); + + set((state) => { state.current.time = 0; - state.current.index += 1; + state.current.index = nextSongIndex!; + state.current.shuffledIndex = nextShuffleIndex; state.current.player = 1; - state.current.song = state.queue.default[state.current.index]; + state.current.song = nextSong!; state.queue.previousNode = get().current.song; - }) - ); + }); + } else { + const nextIndex = + repeat === PlayerRepeat.ALL + ? isLastTrack + ? 0 + : get().current.index + 1 + : isLastTrack + ? get().current.index + : get().current.index + 1; + + set((state) => { + state.current.time = 0; + state.current.index = nextIndex; + state.current.player = 1; + state.current.song = get().queue.default[nextIndex]; + state.queue.previousNode = get().current.song; + }); + } return get().getPlayerData(); }, pause: () => { - set( - produce((state) => { - state.current.status = PlayerStatus.PAUSED; - }) - ); + set((state) => { + state.current.status = PlayerStatus.PAUSED; + }); }, play: () => { - set( - produce((state) => { - state.current.status = PlayerStatus.PLAYING; - }) - ); + set((state) => { + state.current.status = PlayerStatus.PLAYING; + }); }, player1: () => { return get().getPlayerData().player1; @@ -199,61 +492,154 @@ export const usePlayerStore = create()( return get().getPlayerData().player2; }, prev: () => { - set( - produce((state) => { + const isFirstTrack = get().checkIsFirstTrack(); + const repeat = get().repeat; + + if (get().shuffle === PlayerShuffle.TRACK) { + const prevShuffleIndex = isFirstTrack + ? 0 + : get().current.shuffledIndex - 1; + + const prevSong = get().queue.default.find( + (song) => song.uniqueId === get().queue.shuffled[prevShuffleIndex] + ); + + const prevIndex = get().queue.default.findIndex( + (song) => song.uniqueId === prevSong?.uniqueId + ); + + set((state) => { state.current.time = 0; - state.current.index = - state.current.index - 1 < 0 ? 0 : state.current.index - 1; + state.current.index = prevIndex!; + state.current.shuffledIndex = prevShuffleIndex; + state.current.player = 1; + state.current.song = prevSong!; + state.queue.previousNode = get().current.song; + }); + } else { + let prevIndex: number; + if (repeat === PlayerRepeat.ALL) { + prevIndex = isFirstTrack + ? get().queue.default.length - 1 + : get().current.index - 1; + } else { + prevIndex = isFirstTrack ? 0 : get().current.index - 1; + } + + set((state) => { + state.current.time = 0; + state.current.index = prevIndex; state.current.player = 1; state.current.song = state.queue.default[state.current.index]; state.queue.previousNode = get().current.song; - }) - ); + }); + } return get().getPlayerData(); }, queue: { default: [], + played: [], previousNode: {} as QueueSong, shuffled: [], sorted: [], }, + repeat: PlayerRepeat.NONE, setCurrentIndex: (index) => { - set( - produce((state) => { + if (get().shuffle === PlayerShuffle.TRACK) { + const foundSong = get().queue.default.find( + (song) => song.uniqueId === get().queue.shuffled[index] + ); + const foundIndex = get().queue.default.findIndex( + (song) => song.uniqueId === foundSong?.uniqueId + ); + set((state) => { + state.current.time = 0; + state.current.index = foundIndex!; + state.current.shuffledIndex = index; + state.current.player = 1; + state.current.song = foundSong!; + state.queue.previousNode = get().current.song; + }); + } else { + set((state) => { state.current.time = 0; state.current.index = index; state.current.player = 1; state.current.song = state.queue.default[index]; state.queue.previousNode = get().current.song; - }) - ); + }); + } return get().getPlayerData(); }, setCurrentTime: (time) => { - set( - produce((state) => { - state.current.time = time; - }) - ); + set((state) => { + state.current.time = time; + }); }, setMuted: (muted: boolean) => { - set( - produce((state) => { - state.muted = muted; - }) + set((state) => { + state.muted = muted; + }); + }, + setRepeat: (type: PlayerRepeat) => { + set((state) => { + state.repeat = type; + }); + + return get().getPlayerData(); + }, + setShuffle: (type: PlayerShuffle) => { + if (type === PlayerShuffle.NONE) { + set((state) => { + state.shuffle = type; + state.queue.shuffled = []; + }); + + return get().getPlayerData(); + } + + const currentSongId = get().current.song?.uniqueId; + + const queueWithoutCurrentSong = get().queue.default.filter( + (song) => song.uniqueId !== currentSongId ); + + const shuffledSongIds = shuffle(queueWithoutCurrentSong).map( + (song) => song.uniqueId + ); + + set((state) => { + state.shuffle = type; + state.current.shuffledIndex = 0; + state.queue.shuffled = [currentSongId!, ...shuffledSongIds]; + }); + + return get().getPlayerData(); + }, + setShuffledIndex: (index) => { + set((state) => { + state.current.time = 0; + state.current.shuffledIndex = index; + state.current.player = 1; + state.current.song = state.queue.default[index]; + state.queue.previousNode = get().current.song; + }); + + return get().getPlayerData(); + }, + setStore: (data) => { + set({ ...get(), ...data }); }, setVolume: (volume: number) => { - set( - produce((state) => { - state.volume = volume; - }) - ); + set((state) => { + state.volume = volume; + }); }, + shuffle: PlayerShuffle.NONE, volume: 50, - }), + })), { name: 'store_player' } ), { name: 'store_player' } diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index 4ee3ec7ea..1d98dbbb6 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -9,7 +9,6 @@ import { Play, PlaybackStyle, PlaybackType, - PlayerRepeat, } from '@/renderer/types'; export interface SettingsState { @@ -20,12 +19,10 @@ export interface SettingsState { globalMediaHotkeys: boolean; muted: boolean; playButtonBehavior: Play; - repeat: PlayerRepeat; scrobble: { enabled: boolean; scrobbleAtPercentage: number; }; - shuffle: boolean; skipButtons: { enabled: boolean; skipBackwardSeconds: number; @@ -33,7 +30,6 @@ export interface SettingsState { }; style: PlaybackStyle; type: PlaybackType; - volume: number; }; tab: 'general' | 'playback' | 'view' | string; } @@ -53,12 +49,10 @@ export const useSettingsStore = create()( globalMediaHotkeys: true, muted: false, playButtonBehavior: Play.NOW, - repeat: PlayerRepeat.NONE, scrobble: { enabled: false, scrobbleAtPercentage: 75, }, - shuffle: false, skipButtons: { enabled: true, skipBackwardSeconds: 10, @@ -66,7 +60,6 @@ export const useSettingsStore = create()( }, style: PlaybackStyle.GAPLESS, type: PlaybackType.LOCAL, - volume: 50, }, setSettings: (data) => { set({ ...get(), ...data }); diff --git a/src/renderer/types.ts b/src/renderer/types.ts index 018a8ebad..ab8a6fc4b 100644 --- a/src/renderer/types.ts +++ b/src/renderer/types.ts @@ -53,6 +53,12 @@ export enum PlayerRepeat { ONE = 'one', } +export enum PlayerShuffle { + ALBUM = 'album', + NONE = 'none', + TRACK = 'track', +} + export enum Play { LAST = 'last', NEXT = 'next',