From 4c98afb6139b7f563174778a39baf37bea70f26c Mon Sep 17 00:00:00 2001 From: jeffvli Date: Sat, 13 May 2023 00:59:19 -0700 Subject: [PATCH] Add hotkey controls to relevant pages --- .../components/search-input/index.tsx | 12 +-- .../player/components/center-controls.tsx | 30 +++++- .../player/components/left-controls.tsx | 18 +++- .../player/components/right-controls.tsx | 19 +++- .../player/hooks/use-center-controls.ts | 14 +++ .../player/hooks/use-right-controls.ts | 99 ++++++++++++++----- 6 files changed, 155 insertions(+), 37 deletions(-) diff --git a/src/renderer/components/search-input/index.tsx b/src/renderer/components/search-input/index.tsx index a3580860..f081d3f6 100644 --- a/src/renderer/components/search-input/index.tsx +++ b/src/renderer/components/search-input/index.tsx @@ -3,6 +3,8 @@ import { TextInputProps } from '@mantine/core'; import { useFocusWithin, useHotkeys, useMergedRef } from '@mantine/hooks'; import { RiSearchLine } from 'react-icons/ri'; import { TextInput } from '/@/renderer/components/input'; +import { useSettingsStore } from '/@/renderer/store'; +import { shallow } from 'zustand/shallow'; interface SearchInputProps extends TextInputProps { initialWidth?: number; @@ -18,18 +20,12 @@ export const SearchInput = ({ }: SearchInputProps) => { const { ref, focused } = useFocusWithin(); const mergedRef = useMergedRef(ref); + const binding = useSettingsStore((state) => state.hotkeys.bindings.localSearch, shallow); const isOpened = focused || ref.current?.value; const showIcon = !isOpened || (openedWidth || 100) > 100; - useHotkeys([ - [ - 'ctrl+F', - () => { - ref.current.select(); - }, - ], - ]); + useHotkeys([[binding.hotkey, () => ref.current.select()]]); const handleEscape = (e: KeyboardEvent) => { if (e.code === 'Escape') { diff --git a/src/renderer/features/player/components/center-controls.tsx b/src/renderer/features/player/components/center-controls.tsx index 0b74d082..35daed99 100644 --- a/src/renderer/features/player/components/center-controls.tsx +++ b/src/renderer/features/player/components/center-controls.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { useHotkeys } from '@mantine/hooks'; import formatDuration from 'format-duration'; import isElectron from 'is-electron'; import { IoIosPause } from 'react-icons/io'; @@ -25,7 +26,11 @@ import { useShuffleStatus, useCurrentTime, } from '/@/renderer/store'; -import { usePlayerType, useSettingsStore } from '/@/renderer/store/settings.store'; +import { + useHotkeySettings, + usePlayerType, + useSettingsStore, +} from '/@/renderer/store/settings.store'; import { PlayerStatus, PlaybackType, PlayerShuffle, PlayerRepeat } from '/@/renderer/types'; import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider'; @@ -78,6 +83,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { const setCurrentTime = useSetCurrentTime(); const repeat = useRepeatStatus(); const shuffle = useShuffleStatus(); + const { bindings } = useHotkeySettings(); const { handleNextTrack, @@ -88,6 +94,9 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { handleSkipForward, handleToggleRepeat, handleToggleShuffle, + handleStop, + handlePause, + handlePlay, } = useCenterControls({ playersRef }); const currentTime = useCurrentTime(); @@ -113,6 +122,25 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => { 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), + ], + ]); + return ( <> diff --git a/src/renderer/features/player/components/left-controls.tsx b/src/renderer/features/player/components/left-controls.tsx index e1b7f422..5081b3df 100644 --- a/src/renderer/features/player/components/left-controls.tsx +++ b/src/renderer/features/player/components/left-controls.tsx @@ -1,5 +1,6 @@ import React, { MouseEvent } from 'react'; import { Center, Group } from '@mantine/core'; +import { useHotkeys } from '@mantine/hooks'; import { motion, AnimatePresence, LayoutGroup } from 'framer-motion'; import { RiArrowUpSLine, RiDiscLine, RiMore2Fill } from 'react-icons/ri'; import { generatePath, Link } from 'react-router-dom'; @@ -12,6 +13,7 @@ import { useSetFullScreenPlayerStore, useFullScreenPlayerStore, useSidebarStore, + useHotkeySettings, } from '/@/renderer/store'; import { fadeIn } from '/@/renderer/styles'; import { LibraryItem } from '/@/renderer/api/types'; @@ -91,6 +93,7 @@ export const LeftControls = () => { const currentSong = useCurrentSong(); const title = currentSong?.name; const artists = currentSong?.artists; + const { bindings } = useHotkeySettings(); const isSongDefined = Boolean(currentSong?.id); @@ -99,16 +102,23 @@ export const LeftControls = () => { SONG_CONTEXT_MENU_ITEMS, ); - const handleToggleFullScreenPlayer = (e: MouseEvent) => { - e.stopPropagation(); + const handleToggleFullScreenPlayer = (e?: MouseEvent | KeyboardEvent) => { + e?.stopPropagation(); setFullScreenPlayerStore({ expanded: !isFullScreenPlayerExpanded }); }; - const handleToggleSidebarImage = (e: MouseEvent) => { - e.stopPropagation(); + const handleToggleSidebarImage = (e?: MouseEvent) => { + e?.stopPropagation(); setSideBar({ image: true }); }; + useHotkeys([ + [ + bindings.toggleFullscreenPlayer.allowGlobal ? '' : bindings.toggleFullscreenPlayer.hotkey, + handleToggleFullScreenPlayer, + ], + ]); + return ( diff --git a/src/renderer/features/player/components/right-controls.tsx b/src/renderer/features/player/components/right-controls.tsx index 7753bcf9..56629717 100644 --- a/src/renderer/features/player/components/right-controls.tsx +++ b/src/renderer/features/player/components/right-controls.tsx @@ -1,5 +1,6 @@ import { MouseEvent } from 'react'; import { Flex, Group } from '@mantine/core'; +import { useHotkeys } from '@mantine/hooks'; import { HiOutlineQueueList } from 'react-icons/hi2'; import { RiVolumeUpFill, @@ -12,6 +13,7 @@ import { useAppStoreActions, useCurrentServer, useCurrentSong, + useHotkeySettings, useMuted, useSidebarStore, useVolume, @@ -30,7 +32,9 @@ export const RightControls = () => { const currentSong = useCurrentSong(); const { setSideBar } = useAppStoreActions(); const { rightExpanded: isQueueExpanded } = useSidebarStore(); - const { handleVolumeSlider, handleVolumeWheel, handleMute } = useRightControls(); + const { bindings } = useHotkeySettings(); + const { handleVolumeSlider, handleVolumeWheel, handleMute, handleVolumeDown, handleVolumeUp } = + useRightControls(); const updateRatingMutation = useSetRating({}); const addToFavoritesMutation = useCreateFavorite({}); @@ -94,9 +98,20 @@ export const RightControls = () => { } }; + const handleToggleQueue = () => { + setSideBar({ rightExpanded: !isQueueExpanded }); + }; + const isSongDefined = Boolean(currentSong?.id); const showRating = isSongDefined && server?.type === ServerType.NAVIDROME; + useHotkeys([ + [bindings.volumeDown.isGlobal ? '' : bindings.volumeDown.hotkey, handleVolumeDown], + [bindings.volumeUp.isGlobal ? '' : bindings.volumeUp.hotkey, handleVolumeUp], + [bindings.volumeMute.isGlobal ? '' : bindings.volumeMute.hotkey, handleMute], + [bindings.toggleQueue.isGlobal ? '' : bindings.toggleQueue.hotkey, handleToggleQueue], + ]); + return ( { icon={} tooltip={{ label: 'View queue', openDelay: 500 }} variant="secondary" - onClick={() => setSideBar({ rightExpanded: !isQueueExpanded })} + onClick={handleToggleQueue} /> { mpvPlayerListener.rendererQuit(() => { handleQuit(); }); + + mpvPlayerListener.rendererToggleShuffle(() => { + handleToggleShuffle(); + }); + + mpvPlayerListener.rendererToggleRepeat(() => { + handleToggleRepeat(); + }); } return () => { @@ -576,6 +584,8 @@ export const useCenterControls = (args: { playersRef: any }) => { ipc?.removeAllListeners('renderer-player-current-time'); ipc?.removeAllListeners('renderer-player-auto-next'); ipc?.removeAllListeners('renderer-player-quit'); + ipc?.removeAllListeners('renderer-player-toggle-shuffle'); + ipc?.removeAllListeners('renderer-player-toggle-repeat'); }; }, [ autoNext, @@ -587,6 +597,8 @@ export const useCenterControls = (args: { playersRef: any }) => { handlePrevTrack, handleQuit, handleStop, + handleToggleRepeat, + handleToggleShuffle, isMpvPlayer, next, pause, @@ -684,6 +696,8 @@ export const useCenterControls = (args: { playersRef: any }) => { return { handleNextTrack, + handlePause, + handlePlay, handlePlayPause, handlePrevTrack, handleSeekSlider, diff --git a/src/renderer/features/player/hooks/use-right-controls.ts b/src/renderer/features/player/hooks/use-right-controls.ts index fec44cea..a307e1a1 100644 --- a/src/renderer/features/player/hooks/use-right-controls.ts +++ b/src/renderer/features/player/hooks/use-right-controls.ts @@ -1,9 +1,35 @@ -import { useEffect, WheelEvent } from 'react'; +import { useCallback, useEffect, WheelEvent } from 'react'; import isElectron from 'is-electron'; import { useMuted, usePlayerControls, useVolume } from '/@/renderer/store'; import { useGeneralSettings } from '/@/renderer/store/settings.store'; const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; +const mpvPlayerListener = isElectron() ? window.electron.mpvPlayerListener : null; +const ipc = isElectron() ? window.electron.ipc : 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 useRightControls = () => { const { setVolume, setMuted } = usePlayerControls(); @@ -33,37 +59,66 @@ export const useRightControls = () => { setVolume(e); }; - const handleVolumeWheel = (e: WheelEvent) => { - let volumeToSet; - if (e.deltaY > 0) { - const newVolumeLessThanZero = volume - volumeWheelStep < 0; - if (newVolumeLessThanZero) { - volumeToSet = 0; - } else { - volumeToSet = volume - volumeWheelStep; - } - } else { - const newVolumeGreaterThanHundred = volume + volumeWheelStep > 100; - if (newVolumeGreaterThanHundred) { - volumeToSet = 100; - } else { - volumeToSet = volume + volumeWheelStep; - } - } - + const handleVolumeDown = useCallback(() => { + const volumeToSet = calculateVolumeDown(volume, volumeWheelStep); mpvPlayer?.volume(volumeToSet); setVolume(volumeToSet); - }; + }, [setVolume, volume, volumeWheelStep]); - const handleMute = () => { + const handleVolumeUp = useCallback(() => { + const volumeToSet = calculateVolumeUp(volume, volumeWheelStep); + mpvPlayer?.volume(volumeToSet); + setVolume(volumeToSet); + }, [setVolume, volume, volumeWheelStep]); + + const handleVolumeWheel = useCallback( + (e: WheelEvent) => { + let volumeToSet; + if (e.deltaY > 0) { + volumeToSet = calculateVolumeDown(volume, volumeWheelStep); + } else { + volumeToSet = calculateVolumeUp(volume, volumeWheelStep); + } + + mpvPlayer?.volume(volumeToSet); + setVolume(volumeToSet); + }, + [setVolume, volume, volumeWheelStep], + ); + + const handleMute = useCallback(() => { setMuted(!muted); mpvPlayer?.mute(); - }; + }, [muted, setMuted]); + + useEffect(() => { + if (isElectron()) { + mpvPlayerListener?.rendererVolumeMute(() => { + handleMute(); + }); + + mpvPlayerListener?.rendererVolumeUp(() => { + handleVolumeUp(); + }); + + mpvPlayerListener?.rendererVolumeDown(() => { + handleVolumeDown(); + }); + } + + return () => { + ipc?.removeAllListeners('renderer-player-volume-mute'); + ipc?.removeAllListeners('renderer-player-volume-up'); + ipc?.removeAllListeners('renderer-player-volume-down'); + }; + }, [handleMute, handleVolumeDown, handleVolumeUp]); return { handleMute, + handleVolumeDown, handleVolumeSlider, handleVolumeSliderState, + handleVolumeUp, handleVolumeWheel, }; };