From 587fa2422aa63780b0ce6dc22c7f658000458558 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 8 Nov 2022 00:20:39 -0800 Subject: [PATCH] Handle web player --- src/main/features/core/player/index.ts | 2 +- src/main/preload.ts | 3 + .../audio-player/utils/list-handlers.ts | 2 +- .../player/components/center-controls.tsx | 34 +- .../features/player/components/playerbar.tsx | 9 +- .../player/components/right-controls.tsx | 4 +- .../player/hooks/use-center-controls.ts | 119 ++++-- .../player/hooks/use-playqueue-handler.ts | 26 +- .../player/hooks/use-right-controls.ts | 14 +- .../utils/{mpvPlayer.ts => mpv-player.ts} | 15 +- .../settings/components/general-tab.tsx | 37 ++ .../settings/components/playback-tab.tsx | 284 +++++++++++++ .../settings/components/settings-option.tsx | 52 +++ .../features/settings/components/settings.tsx | 38 ++ src/renderer/features/settings/index.ts | 3 +- src/renderer/store/player.store.ts | 383 +++++++++--------- src/renderer/store/settings.store.ts | 86 ++++ 17 files changed, 835 insertions(+), 276 deletions(-) rename src/renderer/features/player/utils/{mpvPlayer.ts => mpv-player.ts} (80%) create mode 100644 src/renderer/features/settings/components/general-tab.tsx create mode 100644 src/renderer/features/settings/components/playback-tab.tsx create mode 100644 src/renderer/features/settings/components/settings-option.tsx create mode 100644 src/renderer/features/settings/components/settings.tsx create mode 100644 src/renderer/store/settings.store.ts diff --git a/src/main/features/core/player/index.ts b/src/main/features/core/player/index.ts index c70e5cdeb..e9f80c290 100644 --- a/src/main/features/core/player/index.ts +++ b/src/main/features/core/player/index.ts @@ -32,7 +32,7 @@ mpv.on('status', (status: any) => { }); // Automatically updates the play button when the player is playing -mpv.on('started', () => { +mpv.on('resumed', () => { getMainWindow()?.webContents.send('renderer-player-play'); }); diff --git a/src/main/preload.ts b/src/main/preload.ts index b4777332f..bf2987e66 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -87,6 +87,9 @@ contextBridge.exposeInMainWorld('electron', { SETTINGS_SET(data: { property: string; value: any }) { ipcRenderer.send('settings-set', data); }, + removeAllListeners(value: string) { + ipcRenderer.removeAllListeners(value); + }, windowClose() { ipcRenderer.send('window-close'); }, diff --git a/src/renderer/components/audio-player/utils/list-handlers.ts b/src/renderer/components/audio-player/utils/list-handlers.ts index 0aaa28686..06daae9ea 100644 --- a/src/renderer/components/audio-player/utils/list-handlers.ts +++ b/src/renderer/components/audio-player/utils/list-handlers.ts @@ -1,6 +1,6 @@ /* eslint-disable no-nested-ternary */ import { Dispatch } from 'react'; -import { CrossfadeStyle } from '../../../../types'; +import { CrossfadeStyle } from '@/renderer/types'; export const gaplessHandler = (args: { currentTime: number; diff --git a/src/renderer/features/player/components/center-controls.tsx b/src/renderer/features/player/components/center-controls.tsx index 0d1871822..c2a77c830 100644 --- a/src/renderer/features/player/components/center-controls.tsx +++ b/src/renderer/features/player/components/center-controls.tsx @@ -5,13 +5,16 @@ import { IoIosPause } from 'react-icons/io'; import { RiPlayFill, RiRepeat2Fill, + RiRewindFill, RiShuffleFill, RiSkipBackFill, RiSkipForwardFill, + RiSpeedFill, } from 'react-icons/ri'; import styled from 'styled-components'; import { Text } from '@/renderer/components'; import { usePlayerStore } from '@/renderer/store'; +import { useSettingsStore } from '@/renderer/store/settings.store'; import { Font } from '@/renderer/styles'; import { PlaybackType, PlayerStatus } from '@/renderer/types'; import { useCenterControls } from '../hooks/use-center-controls'; @@ -56,10 +59,11 @@ const SliderWrapper = styled.div` export const CenterControls = ({ playersRef }: CenterControlsProps) => { const [isSeeking, setIsSeeking] = useState(false); const playerData = usePlayerStore((state) => state.getPlayerData()); + const skip = useSettingsStore((state) => state.player.skipButtons); + const playerType = useSettingsStore((state) => state.player.type); const player1 = playersRef?.current?.player1?.player; const player2 = playersRef?.current?.player2?.player; const { status, player } = usePlayerStore((state) => state.current); - const settings = usePlayerStore((state) => state.settings); const setCurrentTime = usePlayerStore((state) => state.setCurrentTime); const { @@ -67,6 +71,8 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { handlePlayPause, handlePrevTrack, handleSeekSlider, + handleSkipBackward, + handleSkipForward, } = useCenterControls({ playersRef }); const currentTime = usePlayerStore((state) => state.current.time); @@ -78,7 +84,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { let interval: any; if (status === PlayerStatus.PLAYING && !isSeeking) { - if (!isElectron() || settings.type === PlaybackType.WEB) { + if (!isElectron() || playerType === PlaybackType.WEB) { interval = setInterval(() => { setCurrentTime(currentPlayerRef.getCurrentTime()); }, 1000); @@ -88,7 +94,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { } return () => clearInterval(interval); - }, [currentPlayerRef, isSeeking, setCurrentTime, settings.type, status]); + }, [currentPlayerRef, isSeeking, setCurrentTime, playerType, status]); return ( <> @@ -106,6 +112,17 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { variant="secondary" onClick={handlePrevTrack} /> + {skip?.enabled && ( + } + tooltip={{ + label: `Skip backwards ${skip?.skipBackwardSeconds} seconds`, + openDelay: 500, + }} + variant="secondary" + onClick={() => handleSkipBackward(skip?.skipBackwardSeconds)} + /> + )} { variant="main" onClick={handlePlayPause} /> + {skip?.enabled && ( + } + tooltip={{ + label: `Skip forwards ${skip?.skipBackwardSeconds} seconds`, + openDelay: 500, + }} + variant="secondary" + onClick={() => handleSkipForward(skip?.skipForwardSeconds)} + /> + )} } tooltip={{ label: 'Next track', openDelay: 500 }} diff --git a/src/renderer/features/player/components/playerbar.tsx b/src/renderer/features/player/components/playerbar.tsx index 4045fe416..b73393a06 100644 --- a/src/renderer/features/player/components/playerbar.tsx +++ b/src/renderer/features/player/components/playerbar.tsx @@ -1,6 +1,6 @@ import { useRef } from 'react'; -import isElectron from 'is-electron'; import styled from 'styled-components'; +import { useSettingsStore } from '@/renderer/store/settings.store'; import { PlaybackType } from '@/renderer/types'; import { AudioPlayer } from '../../../components'; import { usePlayerStore } from '../../../store'; @@ -45,8 +45,8 @@ const CenterGridItem = styled.div` export const Playerbar = () => { const playersRef = useRef(); - const settings = usePlayerStore((state) => state.settings); - const volume = usePlayerStore((state) => state.settings.volume); + const settings = useSettingsStore((state) => state.player); + const volume = usePlayerStore((state) => state.volume); const player1 = usePlayerStore((state) => state.player1()); const player2 = usePlayerStore((state) => state.player2()); const status = usePlayerStore((state) => state.current.status); @@ -66,7 +66,7 @@ export const Playerbar = () => { - {(!isElectron() || settings.type === PlaybackType.WEB) && ( + {settings.type === PlaybackType.WEB && ( { crossfadeStyle={settings.crossfadeStyle} currentPlayer={player} muted={settings.muted} + playbackStyle={settings.style} player1={player1} player2={player2} status={status} diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx index 1951353ba..a8f4ea18e 100644 --- a/src/renderer/features/player/components/right-controls.tsx +++ b/src/renderer/features/player/components/right-controls.tsx @@ -36,8 +36,8 @@ const MetadataStack = styled.div` `; export const RightControls = () => { - const volume = usePlayerStore((state) => state.settings.volume); - const muted = usePlayerStore((state) => state.settings.muted); + const volume = usePlayerStore((state) => state.volume); + const muted = usePlayerStore((state) => state.muted); const setSidebar = useAppStore((state) => state.setSidebar); const isQueueExpanded = useAppStore((state) => state.sidebar.rightExpanded); const { handleVolumeSlider, handleVolumeSliderState, handleMute } = diff --git a/src/renderer/features/player/hooks/use-center-controls.ts b/src/renderer/features/player/hooks/use-center-controls.ts index 04ae05429..f6744c9f3 100644 --- a/src/renderer/features/player/hooks/use-center-controls.ts +++ b/src/renderer/features/player/hooks/use-center-controls.ts @@ -1,12 +1,16 @@ -import { useCallback } from 'react'; +import { useCallback, useEffect } from 'react'; +import isElectron from 'is-electron'; import { PlaybackType, PlayerStatus } from '@/renderer/types'; import { usePlayerStore } from '../../../store'; -import { mpvPlayer } from '../utils/mpvPlayer'; +import { useSettingsStore } from '../../../store/settings.store'; +import { mpvPlayer } from '../utils/mpv-player'; + +const ipc = isElectron() ? window.electron.ipcRenderer : null; export const useCenterControls = (args: { playersRef: any }) => { const { playersRef } = args; - const settings = usePlayerStore((state) => state.settings); + const settings = useSettingsStore((state) => state.player); const play = usePlayerStore((state) => state.play); const pause = usePlayerStore((state) => state.pause); const prev = usePlayerStore((state) => state.prev); @@ -41,26 +45,28 @@ export const useCenterControls = (args: { playersRef: any }) => { resetPlayers(); }; + const isMpvPlayer = isElectron() && settings.type === PlaybackType.LOCAL; + const handlePlay = useCallback(() => { - if (settings.type === PlaybackType.LOCAL) { + if (isMpvPlayer) { mpvPlayer.play(); } else { currentPlayerRef.getInternalPlayer().play(); } play(); - }, [currentPlayerRef, play, settings]); + }, [currentPlayerRef, isMpvPlayer, play]); const handlePause = useCallback(() => { - if (settings.type === PlaybackType.LOCAL) { + if (isMpvPlayer) { mpvPlayer.pause(); } pause(); - }, [pause, settings]); + }, [isMpvPlayer, pause]); const handleStop = () => { - if (settings.type === PlaybackType.LOCAL) { + if (isMpvPlayer) { mpvPlayer.stop(); } else { stopPlayback(); @@ -73,7 +79,7 @@ export const useCenterControls = (args: { playersRef: any }) => { const handleNextTrack = useCallback(() => { const playerData = next(); - if (settings.type === PlaybackType.LOCAL) { + if (isMpvPlayer) { mpvPlayer.setQueue(playerData); mpvPlayer.next(); } else { @@ -81,12 +87,12 @@ export const useCenterControls = (args: { playersRef: any }) => { } setCurrentTime(0); - }, [next, resetPlayers, setCurrentTime, settings]); + }, [isMpvPlayer, next, resetPlayers, setCurrentTime]); const handlePrevTrack = useCallback(() => { const playerData = prev(); - if (settings.type === PlaybackType.LOCAL) { + if (isMpvPlayer) { mpvPlayer.setQueue(playerData); mpvPlayer.previous(); } else { @@ -94,7 +100,7 @@ export const useCenterControls = (args: { playersRef: any }) => { } setCurrentTime(0); - }, [prev, resetPlayers, setCurrentTime, settings]); + }, [isMpvPlayer, prev, resetPlayers, setCurrentTime]); const handlePlayPause = useCallback(() => { if (queue) { @@ -108,30 +114,26 @@ export const useCenterControls = (args: { playersRef: any }) => { return null; }, [handlePause, handlePlay, playerStatus, queue]); - const handleSkipBackward = () => { - const skipBackwardSec = 5; - - if (settings.type === PlaybackType.LOCAL) { - const newTime = currentTime - skipBackwardSec; - mpvPlayer.seek(-skipBackwardSec); + const handleSkipBackward = (seconds: number) => { + if (isMpvPlayer) { + const newTime = currentTime - seconds; + mpvPlayer.seek(-seconds); setCurrentTime(newTime < 0 ? 0 : newTime); } else { - const newTime = currentPlayerRef.getCurrentTime() - skipBackwardSec; + const newTime = currentPlayerRef?.getCurrentTime() - seconds; resetNextPlayer(); setCurrentTime(newTime); currentPlayerRef.seekTo(newTime); } }; - const handleSkipForward = () => { - const skipForwardSec = 5; - - if (settings.type === PlaybackType.LOCAL) { - const newTime = currentTime + skipForwardSec; - mpvPlayer.seek(skipForwardSec); + const handleSkipForward = (seconds: number) => { + if (isMpvPlayer) { + const newTime = currentTime + seconds; + mpvPlayer.seek(seconds); setCurrentTime(newTime); } else { - const checkNewTime = currentPlayerRef.getCurrentTime() + skipForwardSec; + const checkNewTime = currentPlayerRef?.getCurrentTime() + seconds; const songDuration = currentPlayerRef.player.player.duration; const newTime = @@ -147,26 +149,67 @@ export const useCenterControls = (args: { playersRef: any }) => { (e: number | any) => { setCurrentTime(e); - if (settings.type === PlaybackType.LOCAL) { + if (isMpvPlayer) { mpvPlayer.seekTo(e); } else { currentPlayerRef.seekTo(e); } }, - [currentPlayerRef, setCurrentTime, settings] + [currentPlayerRef, isMpvPlayer, setCurrentTime] ); - // const handleVolumeSlider = useCallback( - // (e: number | any) => { - // // dispatch(setVolume(e)); - // if (settings.type === PlaybackType.Local) { - // // playerApi.volume(currentTime, e); - // } + useEffect(() => { + ipc?.RENDERER_PLAYER_PLAY_PAUSE(() => { + const { status } = usePlayerStore.getState().current; + if (status === PlayerStatus.PAUSED) { + play(); - // setSettings({ volume: (e / 100) ** 2 }); - // }, - // [currentTime, setSettings, settings] - // ); + if (isMpvPlayer) { + mpvPlayer.play(); + } + } else { + pause(); + if (isMpvPlayer) { + mpvPlayer.pause(); + } + } + }); + + ipc?.RENDERER_PLAYER_NEXT(() => { + const playerData = next(); + + if (isMpvPlayer) { + mpvPlayer.setQueue(playerData); + mpvPlayer.next(); + } + }); + + ipc?.RENDERER_PLAYER_PREVIOUS(() => { + const playerData = prev(); + if (isMpvPlayer) { + mpvPlayer.setQueue(playerData); + mpvPlayer.previous(); + } + }); + + ipc?.RENDERER_PLAYER_PLAY(() => play()); + + ipc?.RENDERER_PLAYER_PAUSE(() => pause()); + + ipc?.RENDERER_PLAYER_STOP(() => pause()); + + ipc?.RENDERER_PLAYER_CURRENT_TIME((_event, time) => setCurrentTime(time)); + + return () => { + ipc?.removeAllListeners('renderer-player-play-pause'); + ipc?.removeAllListeners('renderer-player-next'); + ipc?.removeAllListeners('renderer-player-previous'); + ipc?.removeAllListeners('renderer-player-play'); + ipc?.removeAllListeners('renderer-player-pause'); + ipc?.removeAllListeners('renderer-player-stop'); + ipc?.removeAllListeners('renderer-player-current-time'); + }; + }, [isMpvPlayer, next, pause, play, prev, setCurrentTime]); return { handleNextTrack, diff --git a/src/renderer/features/player/hooks/use-playqueue-handler.ts b/src/renderer/features/player/hooks/use-playqueue-handler.ts index a559fe8d3..4ffe49c66 100644 --- a/src/renderer/features/player/hooks/use-playqueue-handler.ts +++ b/src/renderer/features/player/hooks/use-playqueue-handler.ts @@ -3,14 +3,22 @@ import { api } from '@/renderer/api'; import { queryKeys } from '@/renderer/api/query-keys'; import { useServerCredential } from '@/renderer/features/shared'; import { useAuthStore, usePlayerStore } from '@/renderer/store'; -import { LibraryItem, Play, PlayQueueAddOptions } from '@/renderer/types'; +import { useSettingsStore } from '@/renderer/store/settings.store'; +import { + LibraryItem, + Play, + PlaybackType, + PlayQueueAddOptions, +} from '@/renderer/types'; import { mpvPlayer } from '../utils/mpv-player'; export const usePlayQueueHandler = () => { const queryClient = useQueryClient(); const serverId = useAuthStore((state) => state.currentServer?.id) || ''; const { serverToken, isImageTokenRequired } = useServerCredential(); + const play = usePlayerStore((state) => state.play); const addToQueue = usePlayerStore((state) => state.addToQueue); + const playerType = useSettingsStore((state) => state.player.type); const handlePlayQueueAdd = async (options: PlayQueueAddOptions) => { if (options.byData) { @@ -55,10 +63,18 @@ export const usePlayQueueHandler = () => { const playerData = addToQueue(songs, options.play); if (options.play === Play.NEXT || options.play === Play.LAST) { - mpvPlayer.setQueueNext(playerData); - } else { - mpvPlayer.setQueue(playerData); - mpvPlayer.play(); + if (playerType === PlaybackType.LOCAL) { + mpvPlayer.setQueueNext(playerData); + } + } + + if (options.play === Play.NOW) { + if (playerType === PlaybackType.LOCAL) { + mpvPlayer.setQueue(playerData); + mpvPlayer.play(); + } + + play(); } } }; diff --git a/src/renderer/features/player/hooks/use-right-controls.ts b/src/renderer/features/player/hooks/use-right-controls.ts index 47c43fcdc..d20cac422 100644 --- a/src/renderer/features/player/hooks/use-right-controls.ts +++ b/src/renderer/features/player/hooks/use-right-controls.ts @@ -1,11 +1,12 @@ import { useEffect } from 'react'; import { usePlayerStore } from '../../../store'; -import { mpvPlayer } from '../utils/mpvPlayer'; +import { mpvPlayer } from '../utils/mpv-player'; export const useRightControls = () => { - const setSettings = usePlayerStore((state) => state.setSettings); - const volume = usePlayerStore((state) => state.settings.volume); - const muted = usePlayerStore((state) => state.settings.muted); + const setVolume = usePlayerStore((state) => state.setVolume); + const volume = usePlayerStore((state) => state.volume); + const muted = usePlayerStore((state) => state.muted); + const setMuted = usePlayerStore((state) => state.setMuted); // Ensure that the mpv player volume is set on startup useEffect(() => { @@ -19,14 +20,15 @@ export const useRightControls = () => { const handleVolumeSlider = (e: number) => { mpvPlayer.volume(e); + setVolume(e); }; const handleVolumeSliderState = (e: number) => { - setSettings({ volume: e }); + setVolume(e); }; const handleMute = () => { - setSettings({ muted: !muted }); + setMuted(!muted); mpvPlayer.mute(); }; diff --git a/src/renderer/features/player/utils/mpvPlayer.ts b/src/renderer/features/player/utils/mpv-player.ts similarity index 80% rename from src/renderer/features/player/utils/mpvPlayer.ts rename to src/renderer/features/player/utils/mpv-player.ts index 89a8ea8c7..245eb9769 100644 --- a/src/renderer/features/player/utils/mpvPlayer.ts +++ b/src/renderer/features/player/utils/mpv-player.ts @@ -34,20 +34,7 @@ const volume = (value: number) => ipc?.PLAYER_VOLUME(value); const mute = () => ipc?.PLAYER_MUTE(); -const { - setCurrentTime, - play: setPlay, - pause: setPause, - autoNext, -} = usePlayerStore.getState(); - -ipc?.RENDERER_PLAYER_PLAY(() => setPlay()); - -ipc?.RENDERER_PLAYER_PAUSE(() => setPause()); - -ipc?.RENDERER_PLAYER_STOP(() => setPause()); - -ipc?.RENDERER_PLAYER_CURRENT_TIME((_event, time) => setCurrentTime(time)); +const { autoNext } = usePlayerStore.getState(); ipc?.RENDERER_PLAYER_AUTO_NEXT(() => { const playerData = autoNext(); diff --git a/src/renderer/features/settings/components/general-tab.tsx b/src/renderer/features/settings/components/general-tab.tsx new file mode 100644 index 000000000..9d43c2abe --- /dev/null +++ b/src/renderer/features/settings/components/general-tab.tsx @@ -0,0 +1,37 @@ +import { Select, Stack } from '@mantine/core'; +import { SettingsOptions } from '@/renderer/features/settings/components/settings-option'; + +export const GeneralTab = () => { + const options = [ + { + control: , + description: 'Theme for the application', + title: 'Theme', + }, + { + control: + ), + description: 'Font for the application', + title: 'Titlebar style', + }, + ]; + + return ( + + {options.map((option) => ( + + ))} + + ); +}; diff --git a/src/renderer/features/settings/components/playback-tab.tsx b/src/renderer/features/settings/components/playback-tab.tsx new file mode 100644 index 000000000..003358436 --- /dev/null +++ b/src/renderer/features/settings/components/playback-tab.tsx @@ -0,0 +1,284 @@ +import { useEffect, useState } from 'react'; +import { Divider, Group, SelectItem, Stack } from '@mantine/core'; +import isElectron from 'is-electron'; +import { + NumberInput, + SegmentedControl, + Select, + Slider, + Switch, + toast, + Tooltip, +} from '@/renderer/components'; +import { mpvPlayer } from '@/renderer/features/player/utils/mpv-player'; +import { SettingsOptions } from '@/renderer/features/settings/components/settings-option'; +import { usePlayerStore } from '@/renderer/store'; +import { useSettingsStore } from '@/renderer/store/settings.store'; +import { + Play, + PlaybackStyle, + PlaybackType, + PlayerStatus, +} from '@/renderer/types'; + +const getAudioDevice = async () => { + const devices = await navigator.mediaDevices.enumerateDevices(); + return (devices || []).filter( + (dev: MediaDeviceInfo) => dev.kind === 'audiooutput' + ); +}; + +const ipc = isElectron() ? window.electron.ipcRenderer : null; + +const set = (property: string, value: any) => { + ipc?.SETTINGS_SET({ property, value }); +}; + +export const PlaybackTab = () => { + const settings = useSettingsStore((state) => state.player); + const update = useSettingsStore((state) => state.setSettings); + const status = usePlayerStore((state) => state.current.status); + const [audioDevices, setAudioDevices] = useState([]); + + useEffect(() => { + const getAudioDevices = () => { + getAudioDevice() + .then((dev) => + setAudioDevices( + dev.map((d) => ({ label: d.label, value: d.deviceId })) + ) + ) + .catch(() => toast.error({ message: 'Error fetching audio devices' })); + }; + + getAudioDevices(); + }, []); + + const playerOptions = [ + { + control: ( + { + update({ player: { ...settings, type: e as PlaybackType } }); + if (isElectron() && e === PlaybackType.LOCAL) { + const queueData = usePlayerStore.getState().getPlayerData(); + mpvPlayer.setQueue(queueData); + } + }} + /> + ), + description: 'The audio player to use for playback (desktop only)', + isHidden: !isElectron(), + note: + status === PlayerStatus.PLAYING ? 'Player must be paused' : undefined, + title: 'Audio player', + }, + { + control: ( +