feat(reader): custom hardware-button page turning (#4177)

* 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>
This commit is contained in:
Huang Xin
2026-05-16 01:57:33 +08:00
committed by GitHub
parent f5e729a174
commit 787bbf2103
48 changed files with 1569 additions and 154 deletions
+4
View File
@@ -46,6 +46,10 @@ export interface GetSystemFontsListResponse {
export interface InterceptKeysRequest {
volumeKeys?: boolean;
backKey?: boolean;
/** Intercept media keys (next/previous/play-pause) for the hardware page turner. */
pageTurnerKeys?: boolean;
/** Forward every key press to JS so the settings UI can capture a binding. */
learnMode?: boolean;
}
export interface LockScreenRequest {
+72
View File
@@ -0,0 +1,72 @@
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;
};