From 515496ab85b85bc164edde1f8c45255eec03911b Mon Sep 17 00:00:00 2001 From: jeffvli Date: Tue, 4 Nov 2025 21:29:09 -0800 Subject: [PATCH] re-implement playerbar controls --- src/renderer/app.tsx | 7 +- .../audio-player/engine/web-player-engine.tsx | 86 ++- .../audio-player/listener/player-events.tsx | 5 +- .../player/audio-player/utils/player-utils.ts | 3 + .../player/audio-player/web-player.tsx | 22 +- .../components/center-controls.module.css | 25 - .../player/components/center-controls.tsx | 177 +----- .../components/playerbar-slider.module.css | 25 + .../player/components/playerbar-slider.tsx | 119 +++- .../features/player/components/playerbar.tsx | 45 +- .../player/components/right-controls.tsx | 127 +++- .../player/context/player-context.tsx | 225 ++++++- src/renderer/store/player.store.ts | 573 ++++++++++-------- 13 files changed, 873 insertions(+), 566 deletions(-) create mode 100644 src/renderer/features/player/audio-player/utils/player-utils.ts diff --git a/src/renderer/app.tsx b/src/renderer/app.tsx index 65b673def..206c5124e 100644 --- a/src/renderer/app.tsx +++ b/src/renderer/app.tsx @@ -16,6 +16,7 @@ import 'overlayscrollbars/overlayscrollbars.css'; import i18n from '/@/i18n/i18n'; import { useDiscordRpc } from '/@/renderer/features/discord-rpc/use-discord-rpc'; +import { PlayerProvider } from '/@/renderer/features/player/context/player-context'; import { WebAudioContext } from '/@/renderer/features/player/context/webaudio-context'; import { getMpvProperties } from '/@/renderer/features/settings/components/playback/mpv-settings'; import { useServerVersion } from '/@/renderer/hooks/use-server-version'; @@ -31,8 +32,8 @@ import { import { useAppTheme } from '/@/renderer/themes/use-app-theme'; import { sanitizeCss } from '/@/renderer/utils/sanitize'; import { toast } from '/@/shared/components/toast/toast'; -import { PlayerType, WebAudio } from '/@/shared/types/types'; import '/styles/overlayscrollbars.css'; +import { PlayerType, WebAudio } from '/@/shared/types/types'; ModuleRegistry.registerModules([ClientSideRowModelModule, InfiniteRowModelModule]); @@ -193,7 +194,9 @@ export const App = () => { zIndex={50000} /> - + + + diff --git a/src/renderer/features/player/audio-player/engine/web-player-engine.tsx b/src/renderer/features/player/audio-player/engine/web-player-engine.tsx index 3f36f182c..4f30a4834 100644 --- a/src/renderer/features/player/audio-player/engine/web-player-engine.tsx +++ b/src/renderer/features/player/audio-player/engine/web-player-engine.tsx @@ -4,6 +4,7 @@ import { useImperativeHandle, useRef, useState } from 'react'; import ReactPlayer from 'react-player'; import { AudioPlayer } from '/@/renderer/features/player/audio-player/types'; +import { convertToLogVolume } from '/@/renderer/features/player/audio-player/utils/player-utils'; import { PlayerStatus } from '/@/shared/types/types'; export interface OnProgressProps { @@ -120,52 +121,49 @@ export const WebPlayerEngine = (props: WebPlayerEngineProps) => { }, })); + const volume1 = convertToLogVolume(internalVolume1); + const volume2 = convertToLogVolume(internalVolume2); + return ( - <> - {Boolean(src1) && ( - onEndedPlayer1() : undefined} - onProgress={onProgressPlayer1} - playbackRate={speed || 1} - playing={playerNum === 1 && playerStatus === PlayerStatus.PLAYING} - progressInterval={isTransitioning ? 10 : 250} - ref={player1Ref} - url={src1 || EMPTY_SOURCE} - volume={convertToLogVolume(internalVolume1)} - width={0} - /> - )} - {Boolean(src2) && ( - onEndedPlayer2() : undefined} - onProgress={onProgressPlayer2} - playbackRate={speed || 1} - playing={playerNum === 2 && playerStatus === PlayerStatus.PLAYING} - progressInterval={isTransitioning ? 10 : 250} - ref={player2Ref} - url={src2 || EMPTY_SOURCE} - volume={convertToLogVolume(internalVolume2)} - width={0} - /> - )} - +
+ onEndedPlayer1() : undefined} + onProgress={onProgressPlayer1} + playbackRate={speed || 1} + playing={playerNum === 1 && playerStatus === PlayerStatus.PLAYING} + progressInterval={isTransitioning ? 10 : 250} + ref={player1Ref} + url={src1 || EMPTY_SOURCE} + volume={volume1} + width={0} + /> + onEndedPlayer2() : undefined} + onProgress={onProgressPlayer2} + playbackRate={speed || 1} + playing={playerNum === 2 && playerStatus === PlayerStatus.PLAYING} + progressInterval={isTransitioning ? 10 : 250} + ref={player2Ref} + url={src2 || EMPTY_SOURCE} + volume={volume2} + width={0} + /> +
); }; WebPlayerEngine.displayName = 'WebPlayerEngine'; - -function convertToLogVolume(linearVolume: number) { - return Math.pow(linearVolume, 2.0); -} diff --git a/src/renderer/features/player/audio-player/listener/player-events.tsx b/src/renderer/features/player/audio-player/listener/player-events.tsx index b3a36f684..bfd467cce 100644 --- a/src/renderer/features/player/audio-player/listener/player-events.tsx +++ b/src/renderer/features/player/audio-player/listener/player-events.tsx @@ -30,10 +30,7 @@ export interface PlayerEventsCallbacks { prev: { timestamp: number }, ) => void; onPlayerSpeed?: (properties: { speed: number }, prev: { speed: number }) => void; - onPlayerStatus?: ( - properties: { song: QueueSong | undefined; status: PlayerStatus }, - prev: { song: QueueSong | undefined; status: PlayerStatus }, - ) => void; + onPlayerStatus?: (properties: { status: PlayerStatus }, prev: { status: PlayerStatus }) => void; onPlayerVolume?: (properties: { volume: number }, prev: { volume: number }) => void; } diff --git a/src/renderer/features/player/audio-player/utils/player-utils.ts b/src/renderer/features/player/audio-player/utils/player-utils.ts new file mode 100644 index 000000000..66c0249c4 --- /dev/null +++ b/src/renderer/features/player/audio-player/utils/player-utils.ts @@ -0,0 +1,3 @@ +export const convertToLogVolume = (linearVolume: number) => { + return Math.pow(linearVolume, 2.0); +}; diff --git a/src/renderer/features/player/audio-player/web-player.tsx b/src/renderer/features/player/audio-player/web-player.tsx index 0c691d11b..4d675af8e 100644 --- a/src/renderer/features/player/audio-player/web-player.tsx +++ b/src/renderer/features/player/audio-player/web-player.tsx @@ -21,13 +21,13 @@ const PLAY_PAUSE_FADE_INTERVAL = 10; export function WebPlayer() { const playerRef = useRef(null); - const { player, player1, player2 } = usePlayerData(); + const { num, player1, player2, status } = usePlayerData(); const { mediaAutoNext, setProgress } = usePlayerActions(); const { crossfadeDuration, speed, transitionType } = usePlayerProperties(); const isMuted = usePlayerMuted(); const volume = usePlayerVolume(); - const [localPlayerStatus, setLocalPlayerStatus] = useState(player.status); + const [localPlayerStatus, setLocalPlayerStatus] = useState(status); const [isTransitioning, setIsTransitioning] = useState(false); const fadeAndSetStatus = useCallback( @@ -68,7 +68,7 @@ export function WebPlayer() { const onProgressPlayer1 = useCallback( (e: OnProgressProps) => { - if (transitionType === 'crossfade' && player.playerNum === 1) { + if (transitionType === 'crossfade' && num === 1) { setProgress(Number(e.playedSeconds.toFixed(0))); } else if (transitionType === 'gapless') { setProgress(Number(e.playedSeconds.toFixed(0))); @@ -83,7 +83,7 @@ export function WebPlayer() { crossfadeHandler({ crossfadeDuration: crossfadeDuration, currentPlayer: playerRef.current.player1(), - currentPlayerNum: player.playerNum, + currentPlayerNum: num, currentTime: e.playedSeconds, duration: getDuration(playerRef.current.player1().ref), isTransitioning, @@ -105,12 +105,12 @@ export function WebPlayer() { break; } }, - [crossfadeDuration, isTransitioning, player.playerNum, setProgress, transitionType, volume], + [crossfadeDuration, isTransitioning, num, setProgress, transitionType, volume], ); const onProgressPlayer2 = useCallback( (e: OnProgressProps) => { - if (transitionType === PlayerStyle.CROSSFADE && player.playerNum === 2) { + if (transitionType === PlayerStyle.CROSSFADE && num === 2) { setProgress(Number(e.playedSeconds.toFixed(0))); } else if (transitionType === PlayerStyle.GAPLESS) { setProgress(Number(e.playedSeconds.toFixed(0))); @@ -125,7 +125,7 @@ export function WebPlayer() { crossfadeHandler({ crossfadeDuration: crossfadeDuration, currentPlayer: playerRef.current.player2(), - currentPlayerNum: player.playerNum, + currentPlayerNum: num, currentTime: e.playedSeconds, duration: getDuration(playerRef.current.player2().ref), isTransitioning, @@ -147,7 +147,7 @@ export function WebPlayer() { break; } }, - [crossfadeDuration, isTransitioning, player.playerNum, setProgress, transitionType, volume], + [crossfadeDuration, isTransitioning, num, setProgress, transitionType, volume], ); const handleOnEndedPlayer1 = useCallback(() => { @@ -180,7 +180,7 @@ export function WebPlayer() { { onPlayerSeekToTimestamp: (properties) => { const timestamp = properties.timestamp; - if (player.playerNum === 1) { + if (num === 1) { playerRef.current?.player1()?.ref?.seekTo(timestamp); } else { playerRef.current?.player2()?.ref?.seekTo(timestamp); @@ -199,7 +199,7 @@ export function WebPlayer() { playerRef.current?.setVolume(volume); }, }, - [volume, player.playerNum, isTransitioning], + [volume, num, isTransitioning], ); return ( @@ -210,7 +210,7 @@ export function WebPlayer() { onEndedPlayer2={handleOnEndedPlayer2} onProgressPlayer1={onProgressPlayer1} onProgressPlayer2={onProgressPlayer2} - playerNum={player.playerNum} + playerNum={num} playerRef={playerRef} playerStatus={localPlayerStatus} speed={speed} diff --git a/src/renderer/features/player/components/center-controls.module.css b/src/renderer/features/player/components/center-controls.module.css index 7199d7d98..0f0ead2fe 100644 --- a/src/renderer/features/player/components/center-controls.module.css +++ b/src/renderer/features/player/components/center-controls.module.css @@ -4,31 +4,6 @@ align-items: center; } -.slider-container { - display: flex; - width: 95%; - height: 20px; -} - -.slider-value-wrapper { - display: flex; - flex: 1; - align-self: center; - justify-content: center; - max-width: 50px; - - @media (width < 768px) { - display: none; - } -} - -.slider-wrapper { - display: flex; - flex: 6; - align-items: center; - height: 100%; -} - .controls-container { display: flex; align-items: center; diff --git a/src/renderer/features/player/components/center-controls.tsx b/src/renderer/features/player/components/center-controls.tsx index 9cdd889ca..04895a646 100644 --- a/src/renderer/features/player/components/center-controls.tsx +++ b/src/renderer/features/player/components/center-controls.tsx @@ -1,7 +1,4 @@ import { useQueryClient } from '@tanstack/react-query'; -import formatDuration from 'format-duration'; -import isElectron from 'is-electron'; -import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import styles from './center-controls.module.css'; @@ -9,126 +6,48 @@ import styles from './center-controls.module.css'; import { PlayButton, PlayerButton } from '/@/renderer/features/player/components/player-button'; import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider'; import { openShuffleAllModal } from '/@/renderer/features/player/components/shuffle-all-modal'; +import { usePlayerContext } from '/@/renderer/features/player/context/player-context'; import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add'; import { - useAppStore, - useAppStoreActions, - useHotkeySettings, - usePlaybackType, - usePlayerNum, usePlayerRepeat, usePlayerShuffle, usePlayerSong, usePlayerStatus, - usePlayerTimestamp, useSettingsStore, } from '/@/renderer/store'; import { Icon } from '/@/shared/components/icon/icon'; -import { Text } from '/@/shared/components/text/text'; -import { PlaybackSelectors } from '/@/shared/constants/playback-selectors'; -import { PlayerRepeat, PlayerShuffle, PlayerStatus, PlayerType } from '/@/shared/types/types'; +import { PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types'; -interface CenterControlsProps { - playersRef: any; -} - -export const CenterControls = ({ playersRef }: CenterControlsProps) => { +export const CenterControls = () => { const { t } = useTranslation(); const queryClient = useQueryClient(); - const [isSeeking, setIsSeeking] = useState(false); const currentSong = usePlayerSong(); const skip = useSettingsStore((state) => state.general.skipButtons); const buttonSize = useSettingsStore((state) => state.general.buttonSize); - const playbackType = usePlaybackType(); - const player1 = playersRef?.current?.player1; - const player2 = playersRef?.current?.player2; const status = usePlayerStatus(); - const player = usePlayerNum(); - // const setCurrentTime = useSetCurrentTime(); const repeat = usePlayerRepeat(); const shuffle = usePlayerShuffle(); - const { bindings } = useHotkeySettings(); - const { showTimeRemaining } = useAppStore(); - const { setShowTimeRemaining } = useAppStoreActions(); - // const { - // handleNextTrack, - // handlePause, - // handlePlay, - // handlePlayPause, - // handlePrevTrack, - // handleSeekSlider, - // handleSkipBackward, - // handleSkipForward, - // handleStop, - // handleToggleRepeat, - // handleToggleShuffle, - // } = useCenterControls({ playersRef }); + const { + mediaNext, + mediaPrevious, + mediaSkipBackward, + mediaSkipForward, + mediaStop, + mediaTogglePlayPause, + toggleRepeat, + toggleShuffle, + } = usePlayerContext(); + const handlePlayQueueAdd = usePlayQueueAdd(); - const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0; - const currentTime = usePlayerTimestamp(); - const currentPlayerRef = player === 1 ? player1 : player2; - const formattedDuration = formatDuration(songDuration * 1000 || 0); - const formattedTimeRemaining = formatDuration((currentTime - songDuration) * 1000 || 0); - const formattedTime = formatDuration(currentTime * 1000 || 0); - - useEffect(() => { - let interval: ReturnType; - - if (status === PlayerStatus.PLAYING && !isSeeking) { - if (!isElectron() || playbackType === PlayerType.WEB) { - // Update twice a second for slightly better performance - interval = setInterval(() => { - if (currentPlayerRef) { - // setCurrentTime(currentPlayerRef.getCurrentTime()); - } - }, 500); - } - } - - return () => clearInterval(interval); - }, [currentPlayerRef, isSeeking, playbackType, status]); - - const [seekValue, setSeekValue] = useState(0); - - // useHotkeys([ - // [bindings.playPause.isGlobal ? '' : bindings.playPause.hotkey, handlePlayPause], - // [bindings.play.isGlobal ? '' : bindings.play.hotkey, handlePlay], - // [bindings.pause.isGlobal ? '' : bindings.pause.hotkey, handlePause], - // [bindings.stop.isGlobal ? '' : bindings.stop.hotkey, handleStop], - // [bindings.next.isGlobal ? '' : bindings.next.hotkey, handleNextTrack], - // [bindings.previous.isGlobal ? '' : bindings.previous.hotkey, handlePrevTrack], - // [bindings.toggleRepeat.isGlobal ? '' : bindings.toggleRepeat.hotkey, handleToggleRepeat], - // [bindings.toggleShuffle.isGlobal ? '' : bindings.toggleShuffle.hotkey, handleToggleShuffle], - // [ - // bindings.skipBackward.isGlobal ? '' : bindings.skipBackward.hotkey, - // () => handleSkipBackward(skip?.skipBackwardSeconds || 5), - // ], - // [ - // bindings.skipForward.isGlobal ? '' : bindings.skipForward.hotkey, - // () => handleSkipForward(skip?.skipForwardSeconds || 5), - // ], - // ]); - - // useMediaSession({ - // handleNextTrack, - // handlePause, - // handlePlay, - // handlePrevTrack, - // handleSeekSlider, - // handleSkipBackward, - // handleSkipForward, - // handleStop, - // }); - return ( <>
} - // onClick={handleStop} + onClick={mediaStop} tooltip={{ label: t('player.stop', { postProcess: 'sentenceCase' }), openDelay: 0, @@ -144,7 +63,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { /> } isActive={shuffle !== PlayerShuffle.NONE} - // onClick={handleToggleShuffle} + onClick={toggleShuffle} tooltip={{ label: shuffle === PlayerShuffle.NONE @@ -159,7 +78,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { /> } - // onClick={handlePrevTrack} + onClick={mediaPrevious} tooltip={{ label: t('player.previous', { postProcess: 'sentenceCase' }), openDelay: 0, @@ -171,7 +90,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { icon={ } - // onClick={() => handleSkipBackward(skip?.skipBackwardSeconds)} + onClick={mediaSkipBackward} tooltip={{ label: t('player.skip', { context: 'back', @@ -186,12 +105,12 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { {skip?.enabled && ( } - // onClick={() => handleSkipForward(skip?.skipForwardSeconds)} + onClick={mediaSkipForward} tooltip={{ label: t('player.skip', { context: 'forward', @@ -205,7 +124,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { )} } - // onClick={handleNextTrack} + onClick={mediaNext} tooltip={{ label: t('player.next', { postProcess: 'sentenceCase' }), openDelay: 0, @@ -225,7 +144,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { ) } isActive={repeat !== PlayerRepeat.NONE} - // onClick={handleToggleRepeat} + onClick={toggleRepeat} tooltip={{ label: `${ repeat === PlayerRepeat.NONE @@ -263,57 +182,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { />
-
-
- - {formattedTime} - -
-
- formatDuration(value * 1000)} - max={songDuration} - min={0} - onChange={(e) => { - setIsSeeking(true); - setSeekValue(e); - }} - onChangeEnd={(e) => { - // There is a timing bug in Mantine in which the onChangeEnd - // event fires before onChange. Add a small delay to force - // onChangeEnd to happen after onCHange - setTimeout(() => { - // handleSeekSlider(e); - setIsSeeking(false); - }, 50); - }} - size={6} - value={!isSeeking ? currentTime : seekValue} - w="100%" - /> -
-
- setShowTimeRemaining(!showTimeRemaining)} - role="button" - size="xs" - style={{ cursor: 'pointer', userSelect: 'none' }} - > - {showTimeRemaining ? formattedTimeRemaining : formattedDuration} - -
-
+ ); }; diff --git a/src/renderer/features/player/components/playerbar-slider.module.css b/src/renderer/features/player/components/playerbar-slider.module.css index 2f4edc521..2d7f8d20e 100644 --- a/src/renderer/features/player/components/playerbar-slider.module.css +++ b/src/renderer/features/player/components/playerbar-slider.module.css @@ -49,3 +49,28 @@ right: calc(0.1rem * -1); } } + +.slider-container { + display: flex; + width: 95%; + height: 20px; +} + +.slider-value-wrapper { + display: flex; + flex: 1; + align-self: center; + justify-content: center; + max-width: 50px; + + @media (width < 768px) { + display: none; + } +} + +.slider-wrapper { + display: flex; + flex: 6; + align-items: center; + height: 100%; +} diff --git a/src/renderer/features/player/components/playerbar-slider.tsx b/src/renderer/features/player/components/playerbar-slider.tsx index 7af87521d..31c7ce9fa 100644 --- a/src/renderer/features/player/components/playerbar-slider.tsx +++ b/src/renderer/features/player/components/playerbar-slider.tsx @@ -1,8 +1,123 @@ +import formatDuration from 'format-duration'; +import { useEffect, useRef, useState } from 'react'; + import styles from './playerbar-slider.module.css'; +import { WebPlayer } from '/@/renderer/features/player/audio-player/web-player'; +import { usePlayerContext } from '/@/renderer/features/player/context/player-context'; +import { + useAppStore, + useAppStoreActions, + usePlaybackType, + usePlayerSong, + usePlayerTimestamp, +} from '/@/renderer/store'; import { Slider, SliderProps } from '/@/shared/components/slider/slider'; +import { Text } from '/@/shared/components/text/text'; +import { PlaybackSelectors } from '/@/shared/constants/playback-selectors'; +import { PlayerType } from '/@/shared/types/types'; export const PlayerbarSlider = ({ ...props }: SliderProps) => { + const playbackType = usePlaybackType(); + const currentSong = usePlayerSong(); + + const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0; + const [isSeeking, setIsSeeking] = useState(false); + const [seekValue, setSeekValue] = useState(0); + const currentTime = usePlayerTimestamp(); + const seekTimeoutRef = useRef(null); + + const formattedDuration = formatDuration(songDuration * 1000 || 0); + const formattedTimeRemaining = formatDuration((currentTime - songDuration) * 1000 || 0); + const formattedTime = formatDuration(currentTime * 1000 || 0); + + const { showTimeRemaining } = useAppStore(); + const { setShowTimeRemaining } = useAppStoreActions(); + + const { mediaSeekToTimestamp } = usePlayerContext(); + + const handleSeekToTimestamp = (timestamp: number) => { + mediaSeekToTimestamp(timestamp); + }; + + useEffect(() => { + return () => { + if (seekTimeoutRef.current) { + clearTimeout(seekTimeoutRef.current); + } + }; + }, []); + + return ( + <> +
+
+ + {formattedTime} + +
+
+ formatDuration(value * 1000)} + max={songDuration} + min={0} + onChange={(e) => { + // Cancel any pending timeout if user starts seeking again + if (seekTimeoutRef.current) { + clearTimeout(seekTimeoutRef.current); + seekTimeoutRef.current = null; + } + setIsSeeking(true); + setSeekValue(e); + }} + onChangeEnd={(e) => { + setSeekValue(e); + handleSeekToTimestamp(e); + + // Delay resetting isSeeking to allow currentTime to catch up + // This prevents the slider from flickering back and forth + seekTimeoutRef.current = setTimeout(() => { + setIsSeeking(false); + seekTimeoutRef.current = null; + }, 300); + }} + onClick={(e) => { + e?.stopPropagation(); + }} + size={6} + value={!isSeeking ? currentTime : seekValue} + w="100%" + /> +
+
+ setShowTimeRemaining(!showTimeRemaining)} + role="button" + size="xs" + style={{ cursor: 'pointer', userSelect: 'none' }} + > + {showTimeRemaining ? formattedTimeRemaining : formattedDuration} + +
+
+ {playbackType === PlayerType.WEB && } + + ); +}; + +export const CustomPlayerbarSlider = ({ ...props }: SliderProps) => { return ( { track: styles.track, }} {...props} - onClick={(e) => { - e?.stopPropagation(); - }} + size={6} /> ); }; diff --git a/src/renderer/features/player/components/playerbar.tsx b/src/renderer/features/player/components/playerbar.tsx index 155dc3b51..4b6e014a0 100644 --- a/src/renderer/features/player/components/playerbar.tsx +++ b/src/renderer/features/player/components/playerbar.tsx @@ -3,44 +3,16 @@ import { MouseEvent } from 'react'; import styles from './playerbar.module.css'; -import { AudioPlayer } from '/@/renderer/features/player/audio-player'; import { CenterControls } from '/@/renderer/features/player/components/center-controls'; import { LeftControls } from '/@/renderer/features/player/components/left-controls'; import { RightControls } from '/@/renderer/features/player/components/right-controls'; import { usePowerSaveBlocker } from '/@/renderer/features/player/hooks/use-power-save-blocker'; -import { PlayersRef } from '/@/renderer/features/player/ref/players-ref'; -import { - useFullScreenPlayerStore, - usePlayerData, - // usePlayer1Data, - // usePlayer2Data, - // usePlayerControls, - usePlayerMuted, - usePlayerNum, - usePlayerStatus, - usePlayerVolume, - useSetFullScreenPlayerStore, -} from '/@/renderer/store'; -import { - useGeneralSettings, - usePlaybackType, - useSettingsStore, -} from '/@/renderer/store/settings.store'; +import { useFullScreenPlayerStore, useSetFullScreenPlayerStore } from '/@/renderer/store'; +import { useGeneralSettings } from '/@/renderer/store/settings.store'; import { PlaybackSelectors } from '/@/shared/constants/playback-selectors'; -import { PlayerType } from '/@/shared/types/types'; export const Playerbar = () => { - const playersRef = PlayersRef; - const settings = useSettingsStore((state) => state.playback); const { playerbarOpenDrawer } = useGeneralSettings(); - const playbackType = usePlaybackType(); - const volume = usePlayerVolume(); - // const player1 = usePlayer1Data(); - // const player2 = usePlayer2Data(); - const status = usePlayerStatus(); - const player = usePlayerNum(); - const muted = usePlayerMuted(); - // const { autoNext } = usePlayerControls(); const { expanded: isFullScreenPlayerExpanded } = useFullScreenPlayerStore(); const setFullScreenPlayerStore = useSetFullScreenPlayerStore(); @@ -51,13 +23,6 @@ export const Playerbar = () => { setFullScreenPlayerStore({ expanded: !isFullScreenPlayerExpanded }); }; - const { player1, player2 } = usePlayerData(); - - // const autoNextFn = useCallback(() => { - // const playerData = autoNext(); - // updateSong(playerData.current.song); - // }, [autoNext]); - return (
{
- +
- {playbackType === PlayerType.WEB && ( + {/* {playbackType === PlayerType.WEB && ( { style={settings.style as any} volume={(volume / 100) ** 2} /> - )} + )} */} ); }; diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx index 7e9dbf490..1b9a018cf 100644 --- a/src/renderer/features/player/components/right-controls.tsx +++ b/src/renderer/features/player/components/right-controls.tsx @@ -1,21 +1,22 @@ import { useHotkeys, useMediaQuery } from '@mantine/hooks'; import isElectron from 'is-electron'; -import { useEffect } from 'react'; +import { useCallback, useEffect, WheelEvent } from 'react'; import { useTranslation } from 'react-i18next'; -import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider'; -import { useRightControls } from '/@/renderer/features/player/hooks/use-right-controls'; +import { CustomPlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider'; +import { usePlayerContext } from '/@/renderer/features/player/context/player-context'; import { useCreateFavorite } from '/@/renderer/features/shared/mutations/create-favorite-mutation'; import { useDeleteFavorite } from '/@/renderer/features/shared/mutations/delete-favorite-mutation'; import { useSetRating } from '/@/renderer/features/shared/mutations/set-rating-mutation'; import { useAppStoreActions, useCurrentServer, + useGeneralSettings, useHotkeySettings, usePlaybackSettings, usePlaybackType, + usePlayerData, usePlayerMuted, - usePlayerSong, usePlayerSpeed, usePlayerVolume, useSettingsStore, @@ -31,38 +32,53 @@ import { Rating } from '/@/shared/components/rating/rating'; import { Slider } from '/@/shared/components/slider/slider'; import { Switch } from '/@/shared/components/switch/switch'; import { LibraryItem, QueueSong, ServerType, Song } from '/@/shared/types/domain-types'; -import { PlaybackType } from '/@/shared/types/types'; +import { PlayerType } from '/@/shared/types/types'; const ipc = isElectron() ? window.api.ipc : null; const remote = isElectron() ? window.api.remote : null; +const calculateVolumeUp = (volume: number, volumeWheelStep: number) => { + let volumeToSet; + const newVolumeGreaterThanHundred = volume + volumeWheelStep > 100; + if (newVolumeGreaterThanHundred) { + volumeToSet = 100; + } else { + volumeToSet = volume + volumeWheelStep; + } + + return volumeToSet; +}; + +const calculateVolumeDown = (volume: number, volumeWheelStep: number) => { + let volumeToSet; + const newVolumeLessThanZero = volume - volumeWheelStep < 0; + if (newVolumeLessThanZero) { + volumeToSet = 0; + } else { + volumeToSet = volume - volumeWheelStep; + } + + return volumeToSet; +}; + export const RightControls = () => { const { t } = useTranslation(); const isMinWidth = useMediaQuery('(max-width: 480px)'); const volume = usePlayerVolume(); const muted = usePlayerMuted(); const server = useCurrentServer(); - const currentSong = usePlayerSong(); - // const previousSong = usePreviousSong(); + const { currentSong, previousSong } = usePlayerData(); const { setSideBar } = useAppStoreActions(); const { rightExpanded: isQueueExpanded } = useSidebarStore(); const { bindings } = useHotkeySettings(); - const { - handleMute, - handleSpeed, - handleVolumeDown, - handleVolumeSlider, - handleVolumeUp, - handleVolumeWheel, - } = useRightControls(); const { setSettings } = useSettingsStoreActions(); const playbackSettings = usePlaybackSettings(); const playbackType = usePlaybackType(); - + const { volumeWheelStep } = useGeneralSettings(); const speed = usePlayerSpeed(); const volumeWidth = useSettingsStore((state) => state.general.volumeWidth); const speedPreservePitch = useSettingsStore((state) => state.playback.preservePitch); - + const { mediaToggleMute, setSpeed, setVolume } = usePlayerContext(); const updateRatingMutation = useSetRating({}); const addToFavoritesMutation = useCreateFavorite({}); const removeFromFavoritesMutation = useDeleteFavorite({}); @@ -113,6 +129,46 @@ export const RightControls = () => { } }; + const handleVolumeDown = useCallback(() => { + setVolume(volume - 1); + }, [setVolume, volume]); + + const handleVolumeUp = useCallback(() => { + setVolume(volume + 1); + }, [setVolume, volume]); + + const handleMute = useCallback(() => { + mediaToggleMute(); + }, [mediaToggleMute]); + + const handleSpeed = useCallback( + (e: number) => { + setSpeed(e); + }, + [setSpeed], + ); + + const handleVolumeSlider = useCallback( + (e: number) => { + setVolume(e); + }, + [setVolume], + ); + + const handleVolumeWheel = useCallback( + (e: WheelEvent) => { + let volumeToSet; + if (e.deltaY > 0 || e.deltaX > 0) { + volumeToSet = calculateVolumeDown(volume, volumeWheelStep); + } else { + volumeToSet = calculateVolumeUp(volume, volumeWheelStep); + } + + remote?.updateVolume(volumeToSet); + }, + [volume, volumeWheelStep], + ); + const handleToggleQueue = () => { setSideBar({ rightExpanded: !isQueueExpanded }); }; @@ -147,18 +203,18 @@ export const RightControls = () => { bindings.favoriteCurrentToggle.isGlobal ? '' : bindings.favoriteCurrentToggle.hotkey, () => handleToggleFavorite(currentSong), ], - // [ - // bindings.favoritePreviousAdd.isGlobal ? '' : bindings.favoritePreviousAdd.hotkey, - // () => handleAddToFavorites(previousSong), - // ], - // [ - // bindings.favoritePreviousRemove.isGlobal ? '' : bindings.favoritePreviousRemove.hotkey, - // () => handleRemoveFromFavorites(previousSong), - // ], - // [ - // bindings.favoritePreviousToggle.isGlobal ? '' : bindings.favoritePreviousToggle.hotkey, - // () => handleToggleFavorite(previousSong), - // ], + [ + bindings.favoritePreviousAdd.isGlobal ? '' : bindings.favoritePreviousAdd.hotkey, + () => handleAddToFavorites(previousSong), + ], + [ + bindings.favoritePreviousRemove.isGlobal ? '' : bindings.favoritePreviousRemove.hotkey, + () => handleRemoveFromFavorites(previousSong), + ], + [ + bindings.favoritePreviousToggle.isGlobal ? '' : bindings.favoritePreviousToggle.hotkey, + () => handleToggleFavorite(previousSong), + ], [bindings.rate0.isGlobal ? '' : bindings.rate0.hotkey, () => handleUpdateRating(0)], [bindings.rate1.isGlobal ? '' : bindings.rate1.hotkey, () => handleUpdateRating(1)], [bindings.rate2.isGlobal ? '' : bindings.rate2.hotkey, () => handleUpdateRating(2)], @@ -186,9 +242,9 @@ export const RightControls = () => { query: { item: [ { + _serverId: currentSong?._serverId || '', id, itemType: LibraryItem.SONG, - _serverId, } as Song, // This is not a type-safe cast, but it works because those are all the prop ], rating, @@ -203,7 +259,12 @@ export const RightControls = () => { } return () => {}; - }, [addToFavoritesMutation, removeFromFavoritesMutation, updateRatingMutation]); + }, [ + addToFavoritesMutation, + currentSong?._serverId, + removeFromFavoritesMutation, + updateRatingMutation, + ]); return ( @@ -236,7 +297,7 @@ export const RightControls = () => { /> - {playbackType === PlaybackType.WEB && ( + {playbackType === PlayerType.WEB && (