Files
readest/apps/readest-app/src/utils/keybinding.ts
T
Huang Xin 324bb8a366 feat(reader): add e-ink screen refresh page-turner action (#4687) (#4822)
Add a bindable "Refresh Page" action to Settings > Behavior > Page Turner
that triggers a deep e-ink full refresh (GC16) to clear screen ghosting,
gated to e-ink mode on Android.

It reuses the existing hardware page-turner key-binding machinery: a new
'refresh' slot in HardwarePageTurnerSettings, shown only when isAndroidApp
and the e-ink view setting is on. Pressing the bound key calls a new native
bridge command instead of paginating.

The native side is device-agnostic: EinkRefreshController probes each vendor
mechanism via reflection and stops at the first that works, covering Onyx
BOOX (Qualcomm View.refreshScreen), Tolino/Nook (NTX postInvalidateDelayed)
and Boyue-style Rockchip (requestEpdMode) without bundling any vendor SDK.
A success:false result is a soft no-op on non-e-ink hardware. iOS gets a stub.

Verified on an Onyx BOOX Leaf5: the Onyx path fires and performs a visible
full GC16 refresh.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 11:18:11 +02:00

78 lines
2.4 KiB
TypeScript

import { HardwarePageTurnerSettings, KeyBinding } from '@/types/settings';
import { stubTranslation as _ } from '@/utils/misc';
export type KeyCandidate = { source: 'native' | 'dom'; id: string };
// `refresh` is an e-ink-only action (full screen refresh to clear ghosting);
// the others navigate. All share the same key-binding machinery.
export type PageTurnAction = 'pagePrev' | 'pageNext' | 'sectionPrev' | 'sectionNext' | 'refresh';
export const PAGE_TURN_ACTIONS: PageTurnAction[] = [
'pagePrev',
'pageNext',
'sectionPrev',
'sectionNext',
'refresh',
];
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 | undefined,
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;
};