import { useHotkeys } from '@mantine/hooks'; 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'; 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 { useCenterControls } from '/@/renderer/features/player/hooks/use-center-controls'; import { useMediaSession } from '/@/renderer/features/player/hooks/use-media-session'; import { usePlayQueueAdd } from '/@/renderer/features/player/hooks/use-playqueue-add'; import { useAppStore, useAppStoreActions, useCurrentPlayer, useCurrentSong, useCurrentStatus, useCurrentTime, useHotkeySettings, usePlaybackType, useRepeatStatus, useSetCurrentTime, useSettingsStore, useShuffleStatus, } 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 { PlaybackType, PlayerRepeat, PlayerShuffle, PlayerStatus } from '/@/shared/types/types'; interface CenterControlsProps { playersRef: any; } export const CenterControls = ({ playersRef }: CenterControlsProps) => { const { t } = useTranslation(); const queryClient = useQueryClient(); const currentSong = useCurrentSong(); const skip = useSettingsStore((state) => state.general.skipButtons); const buttonSize = useSettingsStore((state) => state.general.buttonSize); const player1 = playersRef?.current?.player1; const player2 = playersRef?.current?.player2; const status = useCurrentStatus(); const repeat = useRepeatStatus(); const shuffle = useShuffleStatus(); const { bindings } = useHotkeySettings(); const { handleNextTrack, handlePause, handlePlay, handlePlayPause, handlePrevTrack, handleSeekSlider, handleSkipBackward, handleSkipForward, handleStop, handleToggleRepeat, handleToggleShuffle, } = useCenterControls({ playersRef }); const handlePlayQueueAdd = usePlayQueueAdd(); 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} tooltip={{ label: t('player.stop', { postProcess: 'sentenceCase' }), openDelay: 0, }} variant="tertiary" /> } isActive={shuffle !== PlayerShuffle.NONE} onClick={handleToggleShuffle} tooltip={{ label: shuffle === PlayerShuffle.NONE ? t('player.shuffle', { context: 'off', postProcess: 'sentenceCase', }) : t('player.shuffle', { postProcess: 'sentenceCase' }), openDelay: 0, }} variant="tertiary" /> } onClick={handlePrevTrack} tooltip={{ label: t('player.previous', { postProcess: 'sentenceCase' }), openDelay: 0, }} variant="secondary" /> {skip?.enabled && ( } onClick={() => handleSkipBackward(skip?.skipBackwardSeconds)} tooltip={{ label: t('player.skip', { context: 'back', postProcess: 'sentenceCase', }), openDelay: 0, }} variant="secondary" /> )} {skip?.enabled && ( } onClick={() => handleSkipForward(skip?.skipForwardSeconds)} tooltip={{ label: t('player.skip', { context: 'forward', postProcess: 'sentenceCase', }), openDelay: 0, }} variant="secondary" /> )} } onClick={handleNextTrack} tooltip={{ label: t('player.next', { postProcess: 'sentenceCase' }), openDelay: 0, }} variant="secondary" /> ) : ( ) } isActive={repeat !== PlayerRepeat.NONE} onClick={handleToggleRepeat} tooltip={{ label: `${ repeat === PlayerRepeat.NONE ? t('player.repeat', { context: 'off', postProcess: 'sentenceCase', }) : repeat === PlayerRepeat.ALL ? t('player.repeat', { context: 'all', postProcess: 'sentenceCase', }) : t('player.repeat', { context: 'one', postProcess: 'sentenceCase', }) }`, openDelay: 0, }} variant="tertiary" /> } onClick={() => openShuffleAllModal({ handlePlayQueueAdd, queryClient, }) } tooltip={{ label: t('player.playRandom', { postProcess: 'sentenceCase' }), openDelay: 0, }} variant="tertiary" />
); }; const PlayerSeekSlider = ({ handleSeekSlider, player1, player2, }: { handleSeekSlider: (e: any | number) => void; player1: any; player2: any; }) => { const player = useCurrentPlayer(); const playbackType = usePlaybackType(); const setCurrentTime = useSetCurrentTime(); const status = useCurrentStatus(); const currentSong = useCurrentSong(); const songDuration = currentSong?.duration ? currentSong.duration / 1000 : 0; const currentTime = useCurrentTime(); const currentPlayerRef = player === 1 ? player1 : player2; const [isSeeking, setIsSeeking] = useState(false); useEffect(() => { let interval: ReturnType; if (status === PlayerStatus.PLAYING && !isSeeking) { if (!isElectron() || playbackType === PlaybackType.WEB) { // Update twice a second for slightly better performance interval = setInterval(() => { if (currentPlayerRef) { setCurrentTime(currentPlayerRef.getCurrentTime()); } }, 500); } } return () => clearInterval(interval); }, [currentPlayerRef, isSeeking, setCurrentTime, playbackType, status]); const { showTimeRemaining } = useAppStore(); const { setShowTimeRemaining } = useAppStoreActions(); const formattedDuration = formatDuration(songDuration * 1000 || 0); const formattedTimeRemaining = formatDuration((currentTime - songDuration) * 1000 || 0); const formattedTime = formatDuration(currentTime * 1000 || 0); const [seekValue, setSeekValue] = useState(0); return (
{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}
); };