From 64350ca63231f233433253bf297199d94c874cb4 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Fri, 12 Jun 2026 01:55:10 +0800 Subject: [PATCH] fix(reader): keep scrolled-mode scrollbar visible after opening a book (#4470) (#4538) Bump foliate-js to include readest/foliate-js#22. The scrolled-mode scroll container (#container) lost its compositing layer in the GPU-hint cleanup, so on Windows' always-on scrollbars the scrollbar appeared on open then vanished once adjacent-section preloading changed the content height. Restoring transform: translateZ(0) on the scrolled #container keeps the scrollbar composited so it repaints across content-size changes. Closes #4470 Co-authored-by: Claude Opus 4.8 (1M context) --- apps/readest-app/.claude/memory/MEMORY.md | 2 + .../memory/android-open-with-intent-flow.md | 26 +++++++++++++ .../.claude/memory/rsvp-font-settings-4519.md | 37 +++++++++++++++++++ packages/foliate-js | 2 +- 4 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 apps/readest-app/.claude/memory/android-open-with-intent-flow.md create mode 100644 apps/readest-app/.claude/memory/rsvp-font-settings-4519.md diff --git a/apps/readest-app/.claude/memory/MEMORY.md b/apps/readest-app/.claude/memory/MEMORY.md index e65e2559..db6513ac 100644 --- a/apps/readest-app/.claude/memory/MEMORY.md +++ b/apps/readest-app/.claude/memory/MEMORY.md @@ -42,6 +42,7 @@ ## Platform Compat - [Android NativeFile vs RemoteFile I/O](android-nativefile-remotefile-io.md) — why NativeFile is slow (4-IPC/chunk + bridge serialization, tauri#9190); RemoteFile CANNOT replace it on Android (asset-protocol Range broken: start>0 → "Failed to fetch", start-0 capped at 1,024,000; plain no-Range fetch returns full file at 281 MB/s); measured 44/100/281 MB/s; speedups = handle-reuse (2.3×), whole-file asset loader (6.3×), or fix wry upstream. Verified live via CDP. - [Window-state sanitizer (#4398)](window-state-sanitize-4398.md) — Windows launch crash (WebView2 0x80070057) from invalid `.window-state.json` (`-32000` minimized sentinel / `0×0`); our plugin already has upstream #253 fix so bad files are stale; defense-in-depth `window-state-sanitizer` plugin registered BEFORE window-state (plugin init = registration order); coord threshold `-16000` (~halfway to the -32000 sentinel; real desktops sit a few thousand px off origin) keeps multi-monitor negatives +- [Android Open-with intent flow (#4521)](android-open-with-intent-flow.md) — "Open with"/"Send to" pipeline: `NativeBridgePlugin.kt::handleIntent` → `shared-intent` → `useAppUrlIngress` → `useOpenWithBooks` (VIEW=transient→reader, SEND=library+upload). Telegram fails where file-manager works on TWO axes: cold-start delivery (fixed by #4527, on dev NOT released v0.11.4) + foreign-private-file read (Telegram FileProvider non-persistable grant vs shared-storage FUSE real-path). adb MediaStore VIEW repro tests pipeline but CANNOT reproduce the read axis (MANAGE_EXTERNAL_STORAGE bypasses grant) ## Feature Notes - [OPDS Firefox strict-XML parse (#4479)](opds-firefox-strict-xml-4479.md) — MEK feed has junk after ``; Firefox DOMParser → `` (silent back-nav), Chrome lenient; `parseOPDSXML` slices root start→last close tag; jsdom mirrors Firefox; wired into page.tsx + validateOPDSURL + feedChecker (latter also #4181 `looksLikeXMLContent` swap) @@ -63,6 +64,7 @@ - [ProgressBar focus-ring line (#4397)](progressbar-focus-ring-4397.md) — decorative `.progressinfo` footer was `tabIndex={-1}` → Android long-press focused it → stray content-width focus-ring line at the bottom every page; fix = drop tabIndex (role='presentation' must not be focusable); ffmpeg-the-video debugging + live-browser `:focus-visible` confirmation - [Table dark-mode tint regression (#4419)](table-dark-mode-tint-4419.md) — `blockquote, table *` color-mix tint in `getColorStyles` must stay gated on `overrideColor` (gate added #2377, removed #4055, re-broke → #4419); safe now that #4392 light-bg rewriters handle #4028 zebra legibility; SAME rule paints vertical-TOC `.space`/▉ spacer cells (▉ U+2589 = blank glyph, contours=0) → "spacing changes" symptom; both fixed by the gate - [Double-click-drag turns page (#4524)](dblclick-drag-pageturn-4524.md) — web double-click+drag selection also turned the page; 1st click's deferred single-click (250ms) fires mid-drag while 2nd-click button held; fix = `isMouseDown` flag in `iframeEventHandlers.ts` gates the deferred `postSingleClick`; synthetic-repro gotchas (shadow-DOM iframe walk, chained-repro timing pollution, reload to re-bind listeners) +- [RSVP font face/family (#4519)](rsvp-font-settings-4519.md) — RSVP word was hardcoded `font-mono`; now mirrors the reader font via `getBaseFontFamily(viewSettings)` (new export in `style.ts`, shares `buildFontFamilyLists` with `getFontStyles`). Overlay renders in the TOP document (portal to body) where custom + basic Google fonts are mounted; known gap = built-in CJK web fonts only in top doc when `isCJKEnv()` ## Library Fixes - [Tauri menu append race (#4389)](tauri-menu-append-race-4389.md) — un-awaited `Menu.append()` (async IPC) in `BookshelfItem.tsx` → context-menu items shuffle order every open (native only, invisible in jsdom); fix = single `await Menu.new({ items })` of ordered `MenuItemOptions`; order/inclusion extracted to pure `getBookContextMenuItemIds` for unit testing diff --git a/apps/readest-app/.claude/memory/android-open-with-intent-flow.md b/apps/readest-app/.claude/memory/android-open-with-intent-flow.md new file mode 100644 index 00000000..e8b82b28 --- /dev/null +++ b/apps/readest-app/.claude/memory/android-open-with-intent-flow.md @@ -0,0 +1,26 @@ +--- +name: android-open-with-intent-flow +description: "Android \"Open with/Send to Readest\" intent pipeline + why Telegram/cloud-app opens differ from file-manager opens (issue" +metadata: + node_type: memory + type: project + originSessionId: 73ee84b7-27a0-4232-981c-de8235a15f07 +--- + +Android "Open with Readest" / "Send to Readest" file-intent pipeline and the #4521 ("open book from Telegram") diagnosis. Confirmed fixed by #4527. + +**Pipeline (ACTION_VIEW = "Open with", ACTION_SEND = "Share"):** +- `NativeBridgePlugin.kt::handleIntent` is the real handler (NOT `MainActivity.kt` — its ACTION_SEND branch is legacy/redundant). → `emitSharedIntent("VIEW"|"SEND", uris)` → JS `useAppUrlIngress` `shared-intent` plugin listener → `app-incoming-url` event → `useOpenWithBooks`. +- VIEW → `openTransient` → straight to reader (ephemeral book, `deletedAt` set, `filePath` = the content:// URI, no library write/upload). SEND → `window.OPEN_WITH_FILES` → `library/page.tsx::processOpenWithFiles` (full ingest + force cloud upload on mobile). +- content:// read: `nativeAppService.openFile` → if URI contains `com.android.externalstorage` → direct `NativeFile` (real path); else `copyURIToPath` → `contentResolver.openInputStream` → copy to Cache → `NativeFile`. `basename` here is LEXICAL (`@tauri-apps/api/path`), not a ContentResolver `DISPLAY_NAME` query — but EPUB format is sniffed by zip magic (`document.ts isZip()`), so an extension-less content URI still opens. +- The Tauri deep-link plugin's `getCurrent()`/`onOpenUrl` only fire for configured deep-link domains (`https://web.readest.com`, `readest:`); `content://`/`file://` VIEW intents are filtered out by `DeepLinkPlugin.isDeepLink()`, so file opens flow ONLY through the native `shared-intent` channel, never the deep-link plugin. + +**#4521 root cause (Telegram open fails, file-manager works) — TWO independent axes:** +1. **Cold-start delivery.** On cold launch the ACTION_VIEW intent reaches `handleIntent` before the JS `shared-intent` listener registers; upstream `Plugin.trigger()` drops events with no listener. **#4527** added queue+replay (`emitOrQueue`/`pendingEvents` + `registerListener` override) to fix it. **#4527 is on `dev` but NOT in released v0.11.4** (v0.11.4 has #4407 only) — the reporter's likely cause. Logs show `Queued shared-intent payload (no listener yet)` then `Replaying 1 queued event(s) after registerListener`. +2. **Foreign-private-file read.** File-manager/MediaStore opens point at SHARED storage → Readest reads via real path / MediaProvider FUSE (it holds `MANAGE_EXTERNAL_STORAGE`), so the URI grant is irrelevant. Telegram/Gmail/Drive serve an APP-PRIVATE file via their own FileProvider with a TEMPORARY, non-persistable grant (`takePersistableUriPermission` throws → caught) → readable only in-session via `openInputStream`; the transient book's `content://` filePath then breaks on later reopen once the grant dies. + +**Verification gotcha (adb, no Telegram):** an adb MediaStore content-URI VIEW intent +`adb shell am start -a android.intent.action.VIEW -d content://media/external/file/ -t application/epub+zip --grant-read-uri-permission -n com.bilingify.readest/.MainActivity` +tests the pipeline (Axis 1) but CANNOT reproduce Axis 2 — shared-storage reads via FUSE bypass the grant, so it always succeeds. To reproduce the real failure use a foreign FileProvider source (Gmail/Drive/Outlook attachment "Open with Readest") or a tiny helper APK with its own FileProvider. Get the MediaStore `_id` via `adb shell content query --uri content://media/external/file --projection _id:_data | grep ` (MIUI `--where` chokes on `/storage/...` path tokens). Watch `adb logcat | grep -iE "NativeBridgePlugin|Open with FUSE|Failed to (open|import)|Queued|Replaying"`. + +Critical files: `src-tauri/plugins/tauri-plugin-native-bridge/android/src/main/java/NativeBridgePlugin.kt`, `src/hooks/useAppUrlIngress.ts`, `src/hooks/useOpenWithBooks.ts`, `src/services/nativeAppService.ts::openFile`, `src/services/bookContent.ts::resolveBookContentSource`. diff --git a/apps/readest-app/.claude/memory/rsvp-font-settings-4519.md b/apps/readest-app/.claude/memory/rsvp-font-settings-4519.md new file mode 100644 index 00000000..a84a3887 --- /dev/null +++ b/apps/readest-app/.claude/memory/rsvp-font-settings-4519.md @@ -0,0 +1,37 @@ +--- +name: rsvp-font-settings-4519 +description: "RSVP word uses the reader's font face/family via getBaseFontFamily; overlay renders in the top document where fonts are mounted" +metadata: + node_type: memory + type: project + originSessionId: 18ca3271-7e08-4b4f-bf33-a135370d5844 +--- + +#4519 — RSVP (speed-reading) word display now mirrors the reader's font face/family +settings instead of a hardcoded `font-mono`. + +Key facts (non-obvious): +- The RSVP overlay (`RSVPOverlay.tsx`, rendered via `createPortal` to + `document.body` from `RSVPControl.tsx`) lives in the **top document**, NOT an + iframe. So it cannot read the iframe's `--serif`/`--sans-serif` CSS variables + that `getFontStyles` sets. Instead apply a resolved `font-family` string. +- `getBaseFontFamily(viewSettings)` in `src/utils/style.ts` returns the resolved + body font chain (serif or sans-serif per `defaultFont`, including the chosen + typeface, `defaultCJKFont`, and any custom font selected as serif/sans). It + reuses the shared `buildFontFamilyLists` helper extracted from `getFontStyles`. +- This works because fonts are already mounted in the **top document**: + `FoliateViewer.tsx` calls `mountCustomFont(document, font)` for user-imported + fonts, and `Reader.tsx` calls `mountAdditionalFonts(document)` for the basic + Google fonts. +- KNOWN GAP: `Reader.tsx` calls `mountAdditionalFonts(document)` WITHOUT the + book-language CJK flag, so the built-in CJK *web* fonts (LXGW WenKai, Noto + Serif JP, etc.) only mount in the top document when `isCJKEnv()` is true. A + non-CJK-env user reading a CJK book may see a system CJK fallback in the RSVP + word rather than the exact web font. Latin fonts and user-imported custom + fonts are unaffected (always mounted). The iframe loads CJK fonts per-doc via + `mountAdditionalFonts(detail.doc, isCJKLang(...))` regardless of env. +- RSVP keeps its own font *size* control (localStorage `readest_rsvp_fontsize`); + only face/family were wired up. Font weight is intentionally left as the RSVP + design (word `font-medium`, ORP char `font-bold`). + +Related: [[iframe-cross-realm-instanceof]] (top-realm vs iframe-realm distinction). diff --git a/packages/foliate-js b/packages/foliate-js index dbe70677..a281b5db 160000 --- a/packages/foliate-js +++ b/packages/foliate-js @@ -1 +1 @@ -Subproject commit dbe70677c6e1925950dfaf526c307d8e5057ee35 +Subproject commit a281b5dbcbe55d73383efc77b7ebb003fbc21ca5