Files
readest/apps/readest-app/src/utils/bridge.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

371 lines
10 KiB
TypeScript

import { invoke, Channel } from '@tauri-apps/api/core';
export interface CopyURIRequest {
uri: string;
dst: string;
}
export interface CopyURIResponse {
success: boolean;
error?: string;
}
export interface SaveImageToGalleryRequest {
srcPath: string;
fileName: string;
mimeType: string;
albumName?: string;
}
export interface SaveImageToGalleryResponse {
success: boolean;
uri?: string;
error?: string;
}
export interface UseBackgroundAudioRequest {
enabled: boolean;
}
export interface InstallPackageRequest {
path: string;
}
export interface InstallPackageResponse {
success: boolean;
error?: string;
}
export interface SetSystemUIVisibilityRequest {
visible: boolean;
darkMode: boolean;
}
export interface SetSystemUIVisibilityResponse {
success: boolean;
error?: string;
}
export interface GetStatusBarHeightResponse {
height: number;
error?: string;
}
export interface GetSystemFontsListResponse {
fonts: Record<string, string>; // { fontName: fontFamily }
error?: string;
}
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 {
orientation: 'portrait' | 'landscape' | 'auto';
}
export interface GetSystemColorSchemeResponse {
colorScheme: 'light' | 'dark';
error?: string;
}
export interface GetSafeAreaInsetsResponse {
top: number;
right: number;
bottom: number;
left: number;
error?: string;
}
interface GetScreenBrightnessResponse {
brightness: number; // 0.0 to 1.0
error?: string;
}
interface SetScreenBrightnessRequest {
brightness: number; // 0.0 to 1.0
}
interface SetScreenBrightnessResponse {
success: boolean;
error?: string;
}
interface GetExternalSDCardPathResponse {
path: string | null;
error?: string;
}
interface SelectDirectoryResponse {
cancelled?: boolean;
uri?: string;
path?: string;
error?: string;
}
export interface GetStorefrontRegionCodeResponse {
regionCode?: string;
error?: string;
}
export interface RefreshEinkScreenResponse {
success: boolean;
error?: string;
}
export async function copyURIToPath(request: CopyURIRequest): Promise<CopyURIResponse> {
const result = await invoke<CopyURIResponse>('plugin:native-bridge|copy_uri_to_path', {
payload: request,
});
return result;
}
export async function saveImageToGallery(
request: SaveImageToGalleryRequest,
): Promise<SaveImageToGalleryResponse> {
return await invoke<SaveImageToGalleryResponse>('plugin:native-bridge|save_image_to_gallery', {
payload: request,
});
}
export async function invokeUseBackgroundAudio(request: UseBackgroundAudioRequest): Promise<void> {
await invoke('plugin:native-bridge|use_background_audio', {
payload: request,
});
}
export async function installPackage(
request: InstallPackageRequest,
): Promise<InstallPackageResponse> {
const result = await invoke<InstallPackageResponse>('plugin:native-bridge|install_package', {
payload: request,
});
return result;
}
export async function setSystemUIVisibility(
request: SetSystemUIVisibilityRequest,
): Promise<SetSystemUIVisibilityResponse> {
const result = await invoke<SetSystemUIVisibilityResponse>(
'plugin:native-bridge|set_system_ui_visibility',
{
payload: request,
},
);
return result;
}
export async function getStatusBarHeight(): Promise<GetStatusBarHeightResponse> {
const result = await invoke<GetStatusBarHeightResponse>(
'plugin:native-bridge|get_status_bar_height',
);
return result;
}
let cachedSysFontsResult: GetSystemFontsListResponse | null = null;
export async function getSysFontsList(): Promise<GetSystemFontsListResponse> {
if (cachedSysFontsResult) {
return cachedSysFontsResult;
}
const result = await invoke<GetSystemFontsListResponse>(
'plugin:native-bridge|get_sys_fonts_list',
);
cachedSysFontsResult = result;
return result;
}
export async function interceptKeys(request: InterceptKeysRequest): Promise<void> {
await invoke('plugin:native-bridge|intercept_keys', {
payload: request,
});
}
export async function lockScreenOrientation(request: LockScreenRequest): Promise<void> {
await invoke('plugin:native-bridge|lock_screen_orientation', {
payload: request,
});
}
export async function getSystemColorScheme(): Promise<GetSystemColorSchemeResponse> {
const result = await invoke<GetSystemColorSchemeResponse>(
'plugin:native-bridge|get_system_color_scheme',
);
return result;
}
export async function getSafeAreaInsets(): Promise<GetSafeAreaInsetsResponse> {
const result = await invoke<GetSafeAreaInsetsResponse>(
'plugin:native-bridge|get_safe_area_insets',
);
return result;
}
export async function getScreenBrightness(): Promise<GetScreenBrightnessResponse> {
const result = await invoke<GetScreenBrightnessResponse>(
'plugin:native-bridge|get_screen_brightness',
);
return result;
}
export async function setScreenBrightness(
request: SetScreenBrightnessRequest,
): Promise<SetScreenBrightnessResponse> {
const result = await invoke<SetScreenBrightnessResponse>(
'plugin:native-bridge|set_screen_brightness',
{
payload: request,
},
);
return result;
}
export async function getExternalSDCardPath(): Promise<GetExternalSDCardPathResponse> {
const result = await invoke<GetExternalSDCardPathResponse>(
'plugin:native-bridge|get_external_sdcard_path',
);
return result;
}
export async function selectDirectory(): Promise<SelectDirectoryResponse> {
const result = await invoke<SelectDirectoryResponse>('plugin:native-bridge|select_directory');
return result;
}
export async function getStorefrontRegionCode(): Promise<GetStorefrontRegionCodeResponse> {
const result = await invoke<GetStorefrontRegionCodeResponse>(
'plugin:native-bridge|get_storefront_region_code',
);
return result;
}
/**
* Trigger a deep e-ink full screen refresh (GC / GC16 waveform) to clear
* ghosting. Android-only; the native side probes several vendor mechanisms
* via reflection and returns `success: false` on devices with no e-ink
* controller. Other platforms reject with an unsupported-platform error.
*/
export async function refreshEinkScreen(): Promise<RefreshEinkScreenResponse> {
return await invoke<RefreshEinkScreenResponse>('plugin:native-bridge|refresh_eink_screen');
}
// ── Sync passphrase keychain ────────────────────────────────────────────
// Tauri-only. Wired into the TauriPassphraseStore (src/libs/crypto/
// passphrase.ts) so the user's sync passphrase persists across app
// launches via the OS keychain (macOS Keychain, Windows Credential
// Manager, Linux libsecret, iOS Keychain, Android EncryptedSharedPrefs).
export interface SetSyncPassphraseRequest {
passphrase: string;
}
export interface SyncPassphraseResponse {
success: boolean;
error?: string;
}
export interface GetSyncPassphraseResponse {
passphrase?: string;
error?: string;
}
export interface SyncKeychainAvailableResponse {
available: boolean;
error?: string;
}
export async function setSyncPassphrase(
request: SetSyncPassphraseRequest,
): Promise<SyncPassphraseResponse> {
return invoke<SyncPassphraseResponse>('plugin:native-bridge|set_sync_passphrase', {
payload: request,
});
}
export async function getSyncPassphrase(): Promise<GetSyncPassphraseResponse> {
return invoke<GetSyncPassphraseResponse>('plugin:native-bridge|get_sync_passphrase');
}
export async function clearSyncPassphrase(): Promise<SyncPassphraseResponse> {
return invoke<SyncPassphraseResponse>('plugin:native-bridge|clear_sync_passphrase');
}
export async function isSyncKeychainAvailable(): Promise<SyncKeychainAvailableResponse> {
return invoke<SyncKeychainAvailableResponse>('plugin:native-bridge|is_sync_keychain_available');
}
// ── Keyed secure key-value store ─────────────────────────────────────────
// Tauri-only. A generic, keyed secret store over the same OS keychain backends
// as the sync passphrase above, so secrets that aren't the single sync
// passphrase (the Google Drive OAuth token set, and any future cloud
// provider's refresh token) get the same XSS-free cross-launch persistence
// without each needing its own native command. Availability is the same probe
// as `is_sync_keychain_available`.
export interface SetSecureItemRequest {
key: string;
value: string;
}
export interface GetSecureItemRequest {
key: string;
}
export interface SecureItemResponse {
success: boolean;
error?: string;
}
export interface GetSecureItemResponse {
value?: string;
error?: string;
}
export async function setSecureItem(request: SetSecureItemRequest): Promise<SecureItemResponse> {
return invoke<SecureItemResponse>('plugin:native-bridge|set_secure_item', { payload: request });
}
export async function getSecureItem(request: GetSecureItemRequest): Promise<GetSecureItemResponse> {
return invoke<GetSecureItemResponse>('plugin:native-bridge|get_secure_item', {
payload: request,
});
}
export async function clearSecureItem(request: GetSecureItemRequest): Promise<SecureItemResponse> {
return invoke<SecureItemResponse>('plugin:native-bridge|clear_secure_item', { payload: request });
}
// ── Nightly updater (main-app commands, no native-bridge prefix) ─────────
// `verify_update_signature` gates the custom install flows (portable /
// AppImage / Android); `install_nightly_update` drives the Tauri updater for
// the platform keys it natively installs (macOS / Windows-NSIS).
export async function verifyUpdateSignature(
path: string,
signature: string,
pubKey: string,
): Promise<boolean> {
return invoke<boolean>('verify_update_signature', { path, signature, pubKey });
}
export interface NightlyProgress {
event: 'progress' | 'finished';
downloaded: number;
contentLength: number;
}
export async function installNightlyUpdate(
endpoint: string,
onProgress?: (p: NightlyProgress) => void,
): Promise<void> {
const channel = new Channel<NightlyProgress>();
if (onProgress) channel.onmessage = onProgress;
await invoke<void>('install_nightly_update', { endpoint, channel });
}