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>
This commit is contained in:
Huang Xin
2026-06-27 17:18:11 +08:00
committed by GitHub
parent f8916e128e
commit 324bb8a366
55 changed files with 470 additions and 41 deletions
@@ -273,3 +273,10 @@ pub(crate) async fn clear_secure_item<R: Runtime>(
) -> Result<SecureItemResponse> {
app.native_bridge().clear_secure_item(payload)
}
#[command]
pub(crate) async fn refresh_eink_screen<R: Runtime>(
app: AppHandle<R>,
) -> Result<RefreshEinkScreenResponse> {
app.native_bridge().refresh_eink_screen()
}
@@ -345,6 +345,12 @@ impl<R: Runtime> NativeBridge<R> {
}),
}
}
/// E-ink panels exist only on the mobile (Android) side. Desktop has no
/// e-ink controller, so this is unsupported here.
pub fn refresh_eink_screen(&self) -> crate::Result<RefreshEinkScreenResponse> {
Err(crate::Error::UnsupportedPlatformError)
}
}
const KEYRING_SERVICE: &str = "Readest Safe Storage";
@@ -88,6 +88,7 @@ pub fn init<R: Runtime>() -> TauriPlugin<R> {
commands::set_secure_item,
commands::get_secure_item,
commands::clear_secure_item,
commands::refresh_eink_screen,
])
.setup(|app, api| {
#[cfg(mobile)]
@@ -332,6 +332,14 @@ impl<R: Runtime> NativeBridge<R> {
}
}
impl<R: Runtime> NativeBridge<R> {
pub fn refresh_eink_screen(&self) -> crate::Result<RefreshEinkScreenResponse> {
self.0
.run_mobile_plugin("refresh_eink_screen", ())
.map_err(Into::into)
}
}
impl<R: Runtime> NativeBridge<R> {
/// Open a full-screen `WKWebView` / `WebView` over the main app,
/// navigate to `payload.url` with a real Chrome UA, wait for load
@@ -395,3 +395,13 @@ pub struct GetSecureItemResponse {
pub value: Option<String>,
pub error: Option<String>,
}
/// Result of a deep e-ink full screen refresh. `success: false` means no
/// known e-ink controller responded on this device (e.g. a non-e-ink
/// Android phone) — not a hard error.
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct RefreshEinkScreenResponse {
pub success: bool,
pub error: Option<String>,
}