787bbf2103
* feat(reader): add custom hardware-button page turning (#4139) Lets users bind hardware remote keys (media keys, D-pad/arrow keys) to previous/next page via a learn-mode capture UI in reader settings — an accessibility feature for page-turner remotes. - New global hardwarePageTurner system setting (enabled + key bindings). - hardwareKeys.ts: key normalization, matching, and page-turn resolution. - deviceStore: reference-counted media-key interception + learn mode. - usePagination: flips pages from bound media keys (native bridge) and D-pad/keyboard keys (DOM keydown), scoped to the active book and suppressed while the toolbar is visible. - Page Turner settings section on all platforms; web/desktop bind keys via DOM keydown only, native media-key interception stays mobile-only. - Android: intercept media + learn-mode keys in dispatchKeyEvent. - iOS: forward media keys via MPRemoteCommandCenter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(i18n): add and translate hardware page turner strings (#4139) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(reader): refine hardware page turner (#4139) - Handle book-iframe key events (iframe-keydown messages) so custom bindings work as soon as a book is open, not only after the settings panel has been shown. - Add Previous/Next Section bindings alongside the page bindings. - Rename the hardwareKeys util to keybinding. - Wire the Page Turner section into the settings Reset action. - Drop the focus ring on the capture buttons; BoxedList gains an optional description. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(i18n): translate page turner section and key strings (#4139) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
73 lines
2.2 KiB
TypeScript
73 lines
2.2 KiB
TypeScript
import { HardwarePageTurnerSettings, KeyBinding } from '@/types/settings';
|
|
import { stubTranslation as _ } from '@/utils/misc';
|
|
|
|
export type KeyCandidate = { source: 'native' | 'dom'; id: string };
|
|
export type PageTurnAction = 'pagePrev' | 'pageNext' | 'sectionPrev' | 'sectionNext';
|
|
|
|
export const PAGE_TURN_ACTIONS: PageTurnAction[] = [
|
|
'pagePrev',
|
|
'pageNext',
|
|
'sectionPrev',
|
|
'sectionNext',
|
|
];
|
|
|
|
const NATIVE_KEY_LABELS: Record<string, string> = {
|
|
MediaNext: _('Media Next'),
|
|
MediaPrevious: _('Media Previous'),
|
|
MediaPlayPause: _('Media Play/Pause'),
|
|
MediaFastForward: _('Media Fast Forward'),
|
|
MediaRewind: _('Media Rewind'),
|
|
VolumeUp: _('Volume Up'),
|
|
VolumeDown: _('Volume Down'),
|
|
};
|
|
|
|
const DOM_KEY_LABELS: Record<string, string> = {
|
|
ArrowLeft: _('Arrow Left'),
|
|
ArrowRight: _('Arrow Right'),
|
|
ArrowUp: _('Arrow Up'),
|
|
ArrowDown: _('Arrow Down'),
|
|
PageUp: _('Page Up'),
|
|
PageDown: _('Page Down'),
|
|
Space: _('Space'),
|
|
Enter: _('Enter'),
|
|
MediaTrackNext: _('Media Next'),
|
|
MediaTrackPrevious: _('Media Previous'),
|
|
MediaPlayPause: _('Media Play/Pause'),
|
|
};
|
|
|
|
/** Normalize a native key name (from the OS bridge) into a `KeyBinding`. */
|
|
export const normalizeNativeKey = (name: string): KeyBinding => ({
|
|
source: 'native',
|
|
id: name,
|
|
label: NATIVE_KEY_LABELS[name] ?? name,
|
|
});
|
|
|
|
/** Normalize a DOM `KeyboardEvent` into a `KeyBinding`. */
|
|
export const normalizeDomKeyEvent = (event: KeyboardEvent): KeyBinding => {
|
|
const id = event.code || event.key;
|
|
return {
|
|
source: 'dom',
|
|
id,
|
|
label: DOM_KEY_LABELS[id] ?? id,
|
|
};
|
|
};
|
|
|
|
/** True when `candidate` is the key described by `binding`. */
|
|
export const matchesBinding = (binding: KeyBinding | null, candidate: KeyCandidate): boolean =>
|
|
!!binding && binding.source === candidate.source && binding.id === candidate.id;
|
|
|
|
/**
|
|
* Decide which page-turn action an incoming key triggers. Returns the
|
|
* action, or `null` when the feature is disabled or the key is unbound.
|
|
*/
|
|
export const resolvePageTurn = (
|
|
settings: HardwarePageTurnerSettings,
|
|
candidate: KeyCandidate,
|
|
): PageTurnAction | null => {
|
|
if (!settings.enabled) return null;
|
|
for (const action of PAGE_TURN_ACTIONS) {
|
|
if (matchesBinding(settings.bindings[action], candidate)) return action;
|
|
}
|
|
return null;
|
|
};
|