Add hotkey controls to relevant pages

This commit is contained in:
jeffvli 2023-05-13 00:59:19 -07:00 committed by Jeff
parent d7f24262fd
commit 4c98afb613
6 changed files with 155 additions and 37 deletions

View file

@ -3,6 +3,8 @@ import { TextInputProps } from '@mantine/core';
import { useFocusWithin, useHotkeys, useMergedRef } from '@mantine/hooks'; import { useFocusWithin, useHotkeys, useMergedRef } from '@mantine/hooks';
import { RiSearchLine } from 'react-icons/ri'; import { RiSearchLine } from 'react-icons/ri';
import { TextInput } from '/@/renderer/components/input'; import { TextInput } from '/@/renderer/components/input';
import { useSettingsStore } from '/@/renderer/store';
import { shallow } from 'zustand/shallow';
interface SearchInputProps extends TextInputProps { interface SearchInputProps extends TextInputProps {
initialWidth?: number; initialWidth?: number;
@ -18,18 +20,12 @@ export const SearchInput = ({
}: SearchInputProps) => { }: SearchInputProps) => {
const { ref, focused } = useFocusWithin(); const { ref, focused } = useFocusWithin();
const mergedRef = useMergedRef<HTMLInputElement>(ref); const mergedRef = useMergedRef<HTMLInputElement>(ref);
const binding = useSettingsStore((state) => state.hotkeys.bindings.localSearch, shallow);
const isOpened = focused || ref.current?.value; const isOpened = focused || ref.current?.value;
const showIcon = !isOpened || (openedWidth || 100) > 100; const showIcon = !isOpened || (openedWidth || 100) > 100;
useHotkeys([ useHotkeys([[binding.hotkey, () => ref.current.select()]]);
[
'ctrl+F',
() => {
ref.current.select();
},
],
]);
const handleEscape = (e: KeyboardEvent<HTMLInputElement>) => { const handleEscape = (e: KeyboardEvent<HTMLInputElement>) => {
if (e.code === 'Escape') { if (e.code === 'Escape') {

View file

@ -1,4 +1,5 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useHotkeys } from '@mantine/hooks';
import formatDuration from 'format-duration'; import formatDuration from 'format-duration';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { IoIosPause } from 'react-icons/io'; import { IoIosPause } from 'react-icons/io';
@ -25,7 +26,11 @@ import {
useShuffleStatus, useShuffleStatus,
useCurrentTime, useCurrentTime,
} from '/@/renderer/store'; } 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 { PlayerStatus, PlaybackType, PlayerShuffle, PlayerRepeat } from '/@/renderer/types';
import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider'; import { PlayerbarSlider } from '/@/renderer/features/player/components/playerbar-slider';
@ -78,6 +83,7 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
const setCurrentTime = useSetCurrentTime(); const setCurrentTime = useSetCurrentTime();
const repeat = useRepeatStatus(); const repeat = useRepeatStatus();
const shuffle = useShuffleStatus(); const shuffle = useShuffleStatus();
const { bindings } = useHotkeySettings();
const { const {
handleNextTrack, handleNextTrack,
@ -88,6 +94,9 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
handleSkipForward, handleSkipForward,
handleToggleRepeat, handleToggleRepeat,
handleToggleShuffle, handleToggleShuffle,
handleStop,
handlePause,
handlePlay,
} = useCenterControls({ playersRef }); } = useCenterControls({ playersRef });
const currentTime = useCurrentTime(); const currentTime = useCurrentTime();
@ -113,6 +122,25 @@ export const CenterControls = ({ playersRef }: CenterControlsProps) => {
const [seekValue, setSeekValue] = useState(0); 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 ( return (
<> <>
<ControlsContainer> <ControlsContainer>

View file

@ -1,5 +1,6 @@
import React, { MouseEvent } from 'react'; import React, { MouseEvent } from 'react';
import { Center, Group } from '@mantine/core'; import { Center, Group } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
import { motion, AnimatePresence, LayoutGroup } from 'framer-motion'; import { motion, AnimatePresence, LayoutGroup } from 'framer-motion';
import { RiArrowUpSLine, RiDiscLine, RiMore2Fill } from 'react-icons/ri'; import { RiArrowUpSLine, RiDiscLine, RiMore2Fill } from 'react-icons/ri';
import { generatePath, Link } from 'react-router-dom'; import { generatePath, Link } from 'react-router-dom';
@ -12,6 +13,7 @@ import {
useSetFullScreenPlayerStore, useSetFullScreenPlayerStore,
useFullScreenPlayerStore, useFullScreenPlayerStore,
useSidebarStore, useSidebarStore,
useHotkeySettings,
} from '/@/renderer/store'; } from '/@/renderer/store';
import { fadeIn } from '/@/renderer/styles'; import { fadeIn } from '/@/renderer/styles';
import { LibraryItem } from '/@/renderer/api/types'; import { LibraryItem } from '/@/renderer/api/types';
@ -91,6 +93,7 @@ export const LeftControls = () => {
const currentSong = useCurrentSong(); const currentSong = useCurrentSong();
const title = currentSong?.name; const title = currentSong?.name;
const artists = currentSong?.artists; const artists = currentSong?.artists;
const { bindings } = useHotkeySettings();
const isSongDefined = Boolean(currentSong?.id); const isSongDefined = Boolean(currentSong?.id);
@ -99,16 +102,23 @@ export const LeftControls = () => {
SONG_CONTEXT_MENU_ITEMS, SONG_CONTEXT_MENU_ITEMS,
); );
const handleToggleFullScreenPlayer = (e: MouseEvent<HTMLDivElement>) => { const handleToggleFullScreenPlayer = (e?: MouseEvent<HTMLDivElement> | KeyboardEvent) => {
e.stopPropagation(); e?.stopPropagation();
setFullScreenPlayerStore({ expanded: !isFullScreenPlayerExpanded }); setFullScreenPlayerStore({ expanded: !isFullScreenPlayerExpanded });
}; };
const handleToggleSidebarImage = (e: MouseEvent<HTMLButtonElement>) => { const handleToggleSidebarImage = (e?: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation(); e?.stopPropagation();
setSideBar({ image: true }); setSideBar({ image: true });
}; };
useHotkeys([
[
bindings.toggleFullscreenPlayer.allowGlobal ? '' : bindings.toggleFullscreenPlayer.hotkey,
handleToggleFullScreenPlayer,
],
]);
return ( return (
<LeftControlsContainer> <LeftControlsContainer>
<LayoutGroup> <LayoutGroup>

View file

@ -1,5 +1,6 @@
import { MouseEvent } from 'react'; import { MouseEvent } from 'react';
import { Flex, Group } from '@mantine/core'; import { Flex, Group } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';
import { HiOutlineQueueList } from 'react-icons/hi2'; import { HiOutlineQueueList } from 'react-icons/hi2';
import { import {
RiVolumeUpFill, RiVolumeUpFill,
@ -12,6 +13,7 @@ import {
useAppStoreActions, useAppStoreActions,
useCurrentServer, useCurrentServer,
useCurrentSong, useCurrentSong,
useHotkeySettings,
useMuted, useMuted,
useSidebarStore, useSidebarStore,
useVolume, useVolume,
@ -30,7 +32,9 @@ export const RightControls = () => {
const currentSong = useCurrentSong(); const currentSong = useCurrentSong();
const { setSideBar } = useAppStoreActions(); const { setSideBar } = useAppStoreActions();
const { rightExpanded: isQueueExpanded } = useSidebarStore(); const { rightExpanded: isQueueExpanded } = useSidebarStore();
const { handleVolumeSlider, handleVolumeWheel, handleMute } = useRightControls(); const { bindings } = useHotkeySettings();
const { handleVolumeSlider, handleVolumeWheel, handleMute, handleVolumeDown, handleVolumeUp } =
useRightControls();
const updateRatingMutation = useSetRating({}); const updateRatingMutation = useSetRating({});
const addToFavoritesMutation = useCreateFavorite({}); const addToFavoritesMutation = useCreateFavorite({});
@ -94,9 +98,20 @@ export const RightControls = () => {
} }
}; };
const handleToggleQueue = () => {
setSideBar({ rightExpanded: !isQueueExpanded });
};
const isSongDefined = Boolean(currentSong?.id); const isSongDefined = Boolean(currentSong?.id);
const showRating = isSongDefined && server?.type === ServerType.NAVIDROME; 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 ( return (
<Flex <Flex
align="flex-end" align="flex-end"
@ -147,7 +162,7 @@ export const RightControls = () => {
icon={<HiOutlineQueueList size="1.1rem" />} icon={<HiOutlineQueueList size="1.1rem" />}
tooltip={{ label: 'View queue', openDelay: 500 }} tooltip={{ label: 'View queue', openDelay: 500 }}
variant="secondary" variant="secondary"
onClick={() => setSideBar({ rightExpanded: !isQueueExpanded })} onClick={handleToggleQueue}
/> />
<Group <Group
noWrap noWrap

View file

@ -564,6 +564,14 @@ export const useCenterControls = (args: { playersRef: any }) => {
mpvPlayerListener.rendererQuit(() => { mpvPlayerListener.rendererQuit(() => {
handleQuit(); handleQuit();
}); });
mpvPlayerListener.rendererToggleShuffle(() => {
handleToggleShuffle();
});
mpvPlayerListener.rendererToggleRepeat(() => {
handleToggleRepeat();
});
} }
return () => { return () => {
@ -576,6 +584,8 @@ export const useCenterControls = (args: { playersRef: any }) => {
ipc?.removeAllListeners('renderer-player-current-time'); ipc?.removeAllListeners('renderer-player-current-time');
ipc?.removeAllListeners('renderer-player-auto-next'); ipc?.removeAllListeners('renderer-player-auto-next');
ipc?.removeAllListeners('renderer-player-quit'); ipc?.removeAllListeners('renderer-player-quit');
ipc?.removeAllListeners('renderer-player-toggle-shuffle');
ipc?.removeAllListeners('renderer-player-toggle-repeat');
}; };
}, [ }, [
autoNext, autoNext,
@ -587,6 +597,8 @@ export const useCenterControls = (args: { playersRef: any }) => {
handlePrevTrack, handlePrevTrack,
handleQuit, handleQuit,
handleStop, handleStop,
handleToggleRepeat,
handleToggleShuffle,
isMpvPlayer, isMpvPlayer,
next, next,
pause, pause,
@ -684,6 +696,8 @@ export const useCenterControls = (args: { playersRef: any }) => {
return { return {
handleNextTrack, handleNextTrack,
handlePause,
handlePlay,
handlePlayPause, handlePlayPause,
handlePrevTrack, handlePrevTrack,
handleSeekSlider, handleSeekSlider,

View file

@ -1,9 +1,35 @@
import { useEffect, WheelEvent } from 'react'; import { useCallback, useEffect, WheelEvent } from 'react';
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import { useMuted, usePlayerControls, useVolume } from '/@/renderer/store'; import { useMuted, usePlayerControls, useVolume } from '/@/renderer/store';
import { useGeneralSettings } from '/@/renderer/store/settings.store'; import { useGeneralSettings } from '/@/renderer/store/settings.store';
const mpvPlayer = isElectron() ? window.electron.mpvPlayer : null; 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 = () => { export const useRightControls = () => {
const { setVolume, setMuted } = usePlayerControls(); const { setVolume, setMuted } = usePlayerControls();
@ -33,37 +59,66 @@ export const useRightControls = () => {
setVolume(e); setVolume(e);
}; };
const handleVolumeWheel = (e: WheelEvent<HTMLDivElement>) => { const handleVolumeDown = useCallback(() => {
let volumeToSet; const volumeToSet = calculateVolumeDown(volume, volumeWheelStep);
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;
}
}
mpvPlayer?.volume(volumeToSet); mpvPlayer?.volume(volumeToSet);
setVolume(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<HTMLDivElement>) => {
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); setMuted(!muted);
mpvPlayer?.mute(); 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 { return {
handleMute, handleMute,
handleVolumeDown,
handleVolumeSlider, handleVolumeSlider,
handleVolumeSliderState, handleVolumeSliderState,
handleVolumeUp,
handleVolumeWheel, handleVolumeWheel,
}; };
}; };