mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-01 18:33:33 +00:00
Add hotkeys manager
- Add configuration to settings store - Initialize global hotkeys on startup from renderer
This commit is contained in:
parent
6056504f00
commit
d7f24262fd
7 changed files with 432 additions and 12 deletions
|
|
@ -0,0 +1,222 @@
|
|||
import { Group } from '@mantine/core';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { RiDeleteBinLine, RiEditLine, RiKeyboardBoxLine } from 'react-icons/ri';
|
||||
import styled from 'styled-components';
|
||||
import { Button, TextInput, Checkbox } from '/@/renderer/components';
|
||||
import isElectron from 'is-electron';
|
||||
import { BindingActions, useHotkeySettings, useSettingsStoreActions } from '/@/renderer/store';
|
||||
import { SettingsOptions } from '/@/renderer/features/settings/components/settings-option';
|
||||
|
||||
const ipc = isElectron() ? window.electron.ipc : null;
|
||||
|
||||
const BINDINGS_MAP: Record<BindingActions, string> = {
|
||||
globalSearch: 'Global search',
|
||||
localSearch: 'In-page search',
|
||||
next: 'Next track',
|
||||
pause: 'Pause',
|
||||
play: 'Play',
|
||||
playPause: 'Play / Pause',
|
||||
previous: 'Previous track',
|
||||
skipBackward: 'Skip backward',
|
||||
skipForward: 'Skip forward',
|
||||
stop: 'Stop',
|
||||
toggleFullscreenPlayer: 'Toggle fullscreen player',
|
||||
toggleQueue: 'Toggle queue',
|
||||
toggleRepeat: 'Toggle repeat',
|
||||
toggleShuffle: 'Toggle shuffle',
|
||||
volumeDown: 'Volume down',
|
||||
volumeMute: 'Volume mute',
|
||||
volumeUp: 'Volume up',
|
||||
};
|
||||
|
||||
const HotkeysContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
|
||||
button {
|
||||
padding: 0 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
export const HotkeyManagerSettings = () => {
|
||||
const { bindings, globalMediaHotkeys } = useHotkeySettings();
|
||||
const { setSettings } = useSettingsStoreActions();
|
||||
const [selected, setSelected] = useState<BindingActions | null>(null);
|
||||
|
||||
const handleSetHotkey = useCallback(
|
||||
(binding: BindingActions, e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
e.preventDefault();
|
||||
const IGNORED_KEYS = ['Control', 'Alt', 'Shift', 'Meta', ' ', 'Escape'];
|
||||
const keys = [];
|
||||
if (e.ctrlKey) keys.push('mod');
|
||||
if (e.altKey) keys.push('alt');
|
||||
if (e.shiftKey) keys.push('shift');
|
||||
if (e.metaKey) keys.push('meta');
|
||||
if (e.key === ' ') keys.push('space');
|
||||
if (!IGNORED_KEYS.includes(e.key)) {
|
||||
if (e.code.includes('Numpad')) {
|
||||
if (e.key === '+') keys.push('numpadadd');
|
||||
else if (e.key === '-') keys.push('numpadsubtract');
|
||||
else if (e.key === '*') keys.push('numpadmultiply');
|
||||
else if (e.key === '/') keys.push('numpaddivide');
|
||||
else if (e.key === '.') keys.push('numpaddecimal');
|
||||
else keys.push(`numpad${e.key}`.toLowerCase());
|
||||
} else {
|
||||
keys.push(e.key?.toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
const bindingString = keys.join('+');
|
||||
|
||||
const updatedBindings = {
|
||||
...bindings,
|
||||
[binding]: { ...bindings[binding], hotkey: bindingString },
|
||||
};
|
||||
|
||||
setSettings({
|
||||
hotkeys: {
|
||||
bindings: updatedBindings,
|
||||
globalMediaHotkeys,
|
||||
},
|
||||
});
|
||||
|
||||
ipc?.send('set-global-shortcuts', updatedBindings);
|
||||
},
|
||||
[bindings, globalMediaHotkeys, setSettings],
|
||||
);
|
||||
|
||||
const handleSetGlobalHotkey = useCallback(
|
||||
(binding: BindingActions, e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const updatedBindings = {
|
||||
...bindings,
|
||||
[binding]: { ...bindings[binding], isGlobal: e.currentTarget.checked },
|
||||
};
|
||||
|
||||
console.log('updatedBindings :>> ', updatedBindings);
|
||||
|
||||
setSettings({
|
||||
hotkeys: {
|
||||
bindings: updatedBindings,
|
||||
globalMediaHotkeys,
|
||||
},
|
||||
});
|
||||
|
||||
ipc?.send('set-global-shortcuts', updatedBindings);
|
||||
},
|
||||
[bindings, globalMediaHotkeys, setSettings],
|
||||
);
|
||||
|
||||
const handleClearHotkey = useCallback(
|
||||
(binding: BindingActions) => {
|
||||
const updatedBindings = {
|
||||
...bindings,
|
||||
[binding]: { ...bindings[binding], hotkey: '', isGlobal: false },
|
||||
};
|
||||
|
||||
setSettings({
|
||||
hotkeys: {
|
||||
bindings: updatedBindings,
|
||||
globalMediaHotkeys,
|
||||
},
|
||||
});
|
||||
|
||||
ipc?.send('set-global-shortcuts', updatedBindings);
|
||||
},
|
||||
[bindings, globalMediaHotkeys, setSettings],
|
||||
);
|
||||
|
||||
const duplicateHotkeyMap = useMemo(() => {
|
||||
const countPerHotkey = Object.values(bindings).reduce((acc, key) => {
|
||||
const hotkey = key.hotkey;
|
||||
if (!hotkey) return acc;
|
||||
|
||||
if (acc[hotkey]) {
|
||||
acc[hotkey] += 1;
|
||||
} else {
|
||||
acc[hotkey] = 1;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
const duplicateKeys = Object.keys(countPerHotkey).filter((key) => countPerHotkey[key] > 1);
|
||||
|
||||
return duplicateKeys;
|
||||
}, [bindings]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsOptions
|
||||
control={<></>}
|
||||
description="Configure application hotkeys. Toggle the checkbox to set as a global hotkey (desktop only)"
|
||||
title="Application hotkeys"
|
||||
/>
|
||||
<HotkeysContainer>
|
||||
{Object.keys(bindings)
|
||||
.filter((binding) => BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP])
|
||||
.map((binding) => (
|
||||
<Group
|
||||
key={`hotkey-${binding}`}
|
||||
noWrap
|
||||
>
|
||||
<TextInput
|
||||
readOnly
|
||||
style={{ userSelect: 'none' }}
|
||||
value={BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
|
||||
/>
|
||||
<TextInput
|
||||
readOnly
|
||||
icon={<RiKeyboardBoxLine />}
|
||||
id={`hotkey-${binding}`}
|
||||
style={{
|
||||
opacity: selected === (binding as BindingActions) ? 0.8 : 1,
|
||||
outline: duplicateHotkeyMap.includes(
|
||||
bindings[binding as keyof typeof BINDINGS_MAP].hotkey!,
|
||||
)
|
||||
? '1px dashed red'
|
||||
: undefined,
|
||||
}}
|
||||
value={bindings[binding as keyof typeof BINDINGS_MAP].hotkey}
|
||||
onBlur={() => setSelected(null)}
|
||||
onChange={() => {}}
|
||||
onKeyDownCapture={(e) => {
|
||||
if (selected !== (binding as BindingActions)) return;
|
||||
handleSetHotkey(binding as BindingActions, e);
|
||||
}}
|
||||
/>
|
||||
{isElectron() && (
|
||||
<Checkbox
|
||||
checked={bindings[binding as keyof typeof BINDINGS_MAP].isGlobal}
|
||||
disabled={bindings[binding as keyof typeof BINDINGS_MAP].hotkey === ''}
|
||||
size="xl"
|
||||
style={{
|
||||
opacity: bindings[binding as keyof typeof BINDINGS_MAP].allowGlobal ? 1 : 0,
|
||||
}}
|
||||
onChange={(e) => handleSetGlobalHotkey(binding as BindingActions, e)}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
variant="default"
|
||||
w={100}
|
||||
onClick={() => {
|
||||
setSelected(binding as BindingActions);
|
||||
document.getElementById(`hotkey-${binding}`)?.focus();
|
||||
}}
|
||||
>
|
||||
<RiEditLine />
|
||||
</Button>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => handleClearHotkey(binding as BindingActions)}
|
||||
>
|
||||
<RiDeleteBinLine />
|
||||
</Button>
|
||||
</Group>
|
||||
))}
|
||||
</HotkeysContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,10 +1,13 @@
|
|||
import { Stack } from '@mantine/core';
|
||||
import { Divider, Stack } from '@mantine/core';
|
||||
import { WindowHotkeySettings } from './window-hotkey-settings';
|
||||
import { HotkeyManagerSettings } from '/@/renderer/features/settings/components/hotkeys/hotkey-manager-settings';
|
||||
|
||||
export const HotkeysTab = () => {
|
||||
return (
|
||||
<Stack spacing="md">
|
||||
<WindowHotkeySettings />
|
||||
<Divider />
|
||||
<HotkeyManagerSettings />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue