Support "produced characters" as hotkeys

"Produced characters" were not supported as hotkeys. I.e., using "!" as a hotkey was not possible.
This commit is contained in:
schroda
2026-05-13 18:57:36 +02:00
parent 4558863d19
commit bc66d52701
7 changed files with 508 additions and 486 deletions

View File

@@ -12,11 +12,11 @@ import IconButton from '@mui/material/IconButton';
import { useQueryParam, StringParam } from 'use-query-params';
import { useLocation } from 'react-router-dom';
import { useTheme } from '@mui/material/styles';
import { useHotkeys } from 'react-hotkeys-hook';
import { useLingui } from '@lingui/react/macro';
import { CustomTooltip } from '@/base/components/CustomTooltip.tsx';
import { SearchTextField } from '@/base/components/inputs/SearchTextField.tsx';
import { SearchParam } from '@/base/Base.types.ts';
import { useHotkeys } from '@/lib/react-hotkeys-hook/useHotkeys.ts';
interface IProps {
isClosable?: boolean;
@@ -79,13 +79,9 @@ export const AppbarSearch: React.FunctionComponent<IProps> = (props) => {
}
};
useHotkeys(
'ctrl+f, F3',
() => {
updateSearchOpenState(true);
},
{ preventDefault: true },
);
useHotkeys('ctrl+f, F3', () => {
updateSearchOpenState(true);
});
if (isOpen) {
return (

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useHotkeys as useHotKeysHook, useHotkeysContext } from 'react-hotkeys-hook';
import { useHotkeysContext } from 'react-hotkeys-hook';
import { useEffect } from 'react';
import { HOTKEY_SCOPES } from '@/features/hotkeys/Hotkeys.constants.ts';
import { ReaderService } from '@/features/reader/services/ReaderService.ts';
@@ -30,10 +30,11 @@ import {
getReaderSettingsStore,
useReaderSettingsStore,
} from '@/features/reader/stores/ReaderStore.ts';
import { useHotkeys } from '@/lib/react-hotkeys-hook/useHotkeys.ts';
const useHotkeys = (...args: Parameters<typeof useHotKeysHook>): ReturnType<typeof useHotKeysHook> => {
const useReaderHotkeys = (...args: Parameters<typeof useHotkeys>): ReturnType<typeof useHotkeys> => {
const [keys, callback, options, dependencies] = args;
return useHotKeysHook(keys, callback, { preventDefault: true, ...options, ...HOTKEY_SCOPES.reader }, dependencies);
return useHotkeys(keys, callback, { ...options, ...HOTKEY_SCOPES.reader }, dependencies);
};
const updateSettingCycleThrough = <Setting extends keyof IReaderSettings>(
@@ -69,9 +70,9 @@ export const ReaderHotkeys = ({
const hotkeys = useReaderSettingsStore('hotkeys');
const exitReader = ReaderService.useExit();
useHotkeys(hotkeys[ReaderHotkey.PREVIOUS_PAGE], () => ReaderControls.openPage('previous'));
useHotkeys(hotkeys[ReaderHotkey.NEXT_PAGE], () => ReaderControls.openPage('next'));
useHotkeys(hotkeys[ReaderHotkey.SCROLL_BACKWARD], () => {
useReaderHotkeys(hotkeys[ReaderHotkey.PREVIOUS_PAGE], () => ReaderControls.openPage('previous'));
useReaderHotkeys(hotkeys[ReaderHotkey.NEXT_PAGE], () => ReaderControls.openPage('next'));
useReaderHotkeys(hotkeys[ReaderHotkey.SCROLL_BACKWARD], () => {
const autoScroll = getReaderAutoScrollStore();
const { readingMode, readingDirection, scrollAmount } = getReaderSettingsStore();
@@ -93,7 +94,7 @@ export const ReaderHotkeys = ({
scrollAmount,
);
});
useHotkeys(hotkeys[ReaderHotkey.SCROLL_FORWARD], () => {
useReaderHotkeys(hotkeys[ReaderHotkey.SCROLL_FORWARD], () => {
const autoScroll = getReaderAutoScrollStore();
const { readingMode, readingDirection, scrollAmount } = getReaderSettingsStore();
@@ -115,20 +116,20 @@ export const ReaderHotkeys = ({
scrollAmount,
);
});
useHotkeys(
useReaderHotkeys(
hotkeys[ReaderHotkey.PREVIOUS_CHAPTER],
() => ReaderControls.openChapter(getOptionForDirection('previous', 'next', readerThemeDirection)),
[readerThemeDirection],
);
useHotkeys(
useReaderHotkeys(
hotkeys[ReaderHotkey.NEXT_CHAPTER],
() => ReaderControls.openChapter(getOptionForDirection('next', 'previous', readerThemeDirection)),
[readerThemeDirection],
);
useHotkeys(hotkeys[ReaderHotkey.TOGGLE_MENU], () =>
useReaderHotkeys(hotkeys[ReaderHotkey.TOGGLE_MENU], () =>
getReaderOverlayStore().setIsVisible(!getReaderOverlayStore().isVisible),
);
useHotkeys(
useReaderHotkeys(
hotkeys[ReaderHotkey.CYCLE_SCALE_TYPE],
() => {
updateSettingCycleThrough(
@@ -141,13 +142,13 @@ export const ReaderHotkeys = ({
},
[],
);
useHotkeys(hotkeys[ReaderHotkey.STRETCH_IMAGE], () =>
useReaderHotkeys(hotkeys[ReaderHotkey.STRETCH_IMAGE], () =>
ReaderService.updateSetting('shouldStretchPage', !getReaderSettingsStore().shouldStretchPage.value),
);
useHotkeys(hotkeys[ReaderHotkey.OFFSET_SPREAD_PAGES], () =>
useReaderHotkeys(hotkeys[ReaderHotkey.OFFSET_SPREAD_PAGES], () =>
ReaderService.setOffsetDoubleSpreads(!getReaderSettingsStore().shouldOffsetDoubleSpreads.value),
);
useHotkeys(hotkeys[ReaderHotkey.CYCLE_READING_MODE], () => {
useReaderHotkeys(hotkeys[ReaderHotkey.CYCLE_READING_MODE], () => {
updateSettingCycleThrough(
'readingMode',
getReaderSettingsStore().readingMode.value,
@@ -156,7 +157,7 @@ export const ReaderHotkeys = ({
true,
);
});
useHotkeys(hotkeys[ReaderHotkey.CYCLE_READING_DIRECTION], () => {
useReaderHotkeys(hotkeys[ReaderHotkey.CYCLE_READING_DIRECTION], () => {
updateSettingCycleThrough(
'readingDirection',
getReaderSettingsStore().readingDirection.value,
@@ -165,20 +166,20 @@ export const ReaderHotkeys = ({
true,
);
});
useHotkeys(hotkeys[ReaderHotkey.TOGGLE_AUTO_SCROLL], () => getReaderAutoScrollStore().toggleActive(), {});
useHotkeys(hotkeys[ReaderHotkey.AUTO_SCROLL_SPEED_DECREASE], () =>
useReaderHotkeys(hotkeys[ReaderHotkey.TOGGLE_AUTO_SCROLL], () => getReaderAutoScrollStore().toggleActive(), {});
useReaderHotkeys(hotkeys[ReaderHotkey.AUTO_SCROLL_SPEED_DECREASE], () =>
ReaderService.updateSetting('autoScroll', {
...getReaderSettingsStore().autoScroll,
value: Math.min(AUTO_SCROLL_SPEED.max, getReaderSettingsStore().autoScroll.value + AUTO_SCROLL_SPEED.step),
}),
);
useHotkeys(hotkeys[ReaderHotkey.AUTO_SCROLL_SPEED_INCREASE], () =>
useReaderHotkeys(hotkeys[ReaderHotkey.AUTO_SCROLL_SPEED_INCREASE], () =>
ReaderService.updateSetting('autoScroll', {
...getReaderSettingsStore().autoScroll,
value: Math.max(AUTO_SCROLL_SPEED.min, getReaderSettingsStore().autoScroll.value - AUTO_SCROLL_SPEED.step),
}),
);
useHotkeys(hotkeys[ReaderHotkey.EXIT_READER], exitReader, [exitReader]);
useReaderHotkeys(hotkeys[ReaderHotkey.EXIT_READER], exitReader, [exitReader]);
useEffect(() => {
enableScope(HotkeyScope.READER);

View File

@@ -6,7 +6,7 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
import { useEffect } from 'react';
import { useEffect, useState } from 'react';
import Stack from '@mui/material/Stack';
import { useRecordHotkeys } from 'react-hotkeys-hook';
import Dialog from '@mui/material/Dialog';
@@ -17,6 +17,7 @@ import Button from '@mui/material/Button';
import Typography from '@mui/material/Typography';
import { Trans, useLingui } from '@lingui/react/macro';
import { Hotkey } from '@/features/reader/hotkeys/settings/components/Hotkey.tsx';
import { CheckboxInput } from '@/base/components/inputs/CheckboxInput.tsx';
export const RecordHotkey = ({
onClose,
@@ -29,7 +30,10 @@ export const RecordHotkey = ({
}) => {
const { t } = useLingui();
const [recordedKeys, { start, stop, resetKeys }] = useRecordHotkeys();
const [recordPhysicalKeys, setRecordPhysicalKeys] = useState(true);
const [recordedKeys, { start, stop, resetKeys }] = useRecordHotkeys(!recordPhysicalKeys);
const keys = [[...recordedKeys].join('+')];
const isExistingKey = keys.some((key) =>
existingKeys.map((existingKey) => existingKey.toLowerCase()).includes(key.toLowerCase()),
@@ -41,7 +45,7 @@ export const RecordHotkey = ({
return () => {
stop();
};
}, []);
}, [start, stop]);
return (
<Dialog open onClose={onClose} fullWidth>
@@ -54,6 +58,32 @@ export const RecordHotkey = ({
</Trans>
</Stack>
{isExistingKey && <Typography color="error">{t`Hotkey already exists`}</Typography>}
<CheckboxInput
label={
<Stack
sx={{
// Padding comes from the MUI Checkbox component
pt: '9px',
}}
>
<Typography>{t`Record physical keys`}</Typography>
<Typography variant="body2" color="textSecondary" sx={{ display: 'flex', gap: 1 }}>
<Trans>
enabled: records <Hotkey keys={['shift+1']} /> when the user presses Shift+1
</Trans>
</Typography>
<Typography variant="body2" color="textSecondary" sx={{ display: 'flex', gap: 1 }}>
<Trans>
disabled: records <Hotkey keys={['!']} /> when the user presses Shift+1 on a US
layout
</Trans>
</Typography>
</Stack>
}
checked={recordPhysicalKeys}
onChange={(_, checked) => setRecordPhysicalKeys(checked)}
sx={{ mt: 2, alignItems: 'start' }}
/>
</DialogContent>
<DialogActions>
<Stack

View File

@@ -1329,6 +1329,10 @@ msgstr "Disable authentication"
msgid "Disabled"
msgstr "Disabled"
#: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx
msgid "disabled: records <0/> when the user presses Shift+1 on a US layout"
msgstr "disabled: records <0/> when the user presses Shift+1 on a US layout"
#: src/features/settings/components/koreaderSync/KoreaderSyncSettings.tsx
msgid "Disconnect from KOReader Sync server"
msgstr "Disconnect from KOReader Sync server"
@@ -1500,6 +1504,10 @@ msgstr "Enable debug logs"
msgid "Enable page read progress"
msgstr "Enable page read progress"
#: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx
msgid "enabled: records <0/> when the user presses Shift+1"
msgstr "enabled: records <0/> when the user presses Shift+1"
#: src/features/downloads/screens/DownloadSettings.tsx
msgid "Entries in excluded categories will not be downloaded even if they are also in included categories"
msgstr "Entries in excluded categories will not be downloaded even if they are also in included categories"
@@ -2779,6 +2787,10 @@ msgstr "Recently read"
msgid "Record keybind"
msgstr "Record keybind"
#: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx
msgid "Record physical keys"
msgstr "Record physical keys"
#. placeholder {0}: recordedKeys.size ? <Hotkey keys={keys} /> : <Typography>{t`Press keys`}</Typography>
#: src/features/reader/hotkeys/settings/components/RecordHotkey.tsx
msgid "Recorded hotkeys: {0}"

View File

@@ -0,0 +1,24 @@
/*
* Copyright (C) Contributors to the Suwayomi project
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
// oxlint-disable-next-line no-restricted-imports
import { useHotkeys as orgUseHotkeys } from 'react-hotkeys-hook';
export const useHotkeys = (...args: Parameters<typeof orgUseHotkeys>): ReturnType<typeof orgUseHotkeys> => {
const [keys, callback, options, dependencies] = args;
return orgUseHotkeys(
keys,
callback,
{
preventDefault: true,
useKey: true,
...options,
},
dependencies,
);
};