diff --git a/apps/readest-app/.claude/memory/MEMORY.md b/apps/readest-app/.claude/memory/MEMORY.md index ad55ee70..efaec132 100644 --- a/apps/readest-app/.claude/memory/MEMORY.md +++ b/apps/readest-app/.claude/memory/MEMORY.md @@ -17,6 +17,7 @@ - [Swipe page-turn bg flash](paginator-swipe-bg-flash.md) — white↔black flash on swipe+animation only; `#background` was static screen-space and didn't track content during drag/snap; fix = sliding per-view full-bleed segments (`computeBackgroundSegments`) rebuilt on scroll + per-rAF synced to the view transform during snap - [Duokan fullscreen cover hidden in scroll mode](duokan-fullscreen-cover-scroll.md) — #4379 `data-duokan-page-fullscreen` cover pinned `position:absolute height:100%` collapses against auto-height scroll container; gate fullscreen on `this.#column` + reset stale absolute props on toggle (`setImageSize` in paginator.js) - [Paginated texture occlusion](paginated-texture-occlusion-4399.md) — #4399 host `.foliate-viewer::before` texture absent in paginated (shown in scrolled); opaque `#background` container (`= fallbackBg`) from the swipe-flash fix occludes it; shared `textureAwareBackground` helper + `hasTexture ? '' : fallbackBg` container +- [Dark-mode texture occluded by body bg (#4446)](dark-mode-texture-body-bg-4446.md) — RESOLVED (PR #4564): `body.theme-dark{bg !important}` from #4392 (v0.11.4, NOT foliate-js) painted iframe bodies opaque dark → occluded host texture + poisoned `docBackground` capture → opaque segments/view bgs; fix = `transparent !important` UNCONDITIONALLY (texture-gating would go stale: capture is once-per-section-load); CDP gotchas = patch ALL multiview iframes, stale preload views survive navigation ±2, load-listener sees exact capture-time state - [Background overflows column (#4394, PR #4429)](paginator-gutter-bleed-asymmetry-4394.md) — paginated page bg stretched into the outer `--_outer-min` gutter → mixed cover/title 2-up spread shifted off-centre (~250px at 1920px). KEEP the grid (`--_outer-min` keeps margins symmetric); fix = clamp `computeBackgroundSegments` to `[containerStart,containerEnd]` (Math.max/Math.min) so bg stays in its column. 2 wrong tries first (bleed-gating, "page shouldn't be yellow"); foliate submodule needs dev-server RESTART to pick up edits ## Critical Files (Most Bug-Prone) @@ -45,8 +46,10 @@ - [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) +- [Dict lookup → OEM browser hijack (#4559)](dict-lookup-browser-hijack-4559.md) — VIVO system-dict lookup opened the browser not Eudic. PRIMARY: no `` for `ACTION_PROCESS_TEXT` under targetSdk36 → dictionary apps invisible, only auto-visible browser returned (fix = add `` to plugin manifest). SECONDARY: browser registers PROCESS_TEXT + is default → filter browsers in pure `decideLookupDispatch` (explicit/chooser/unavailable). Remember-the-pick via `IntentSender`+`EXTRA_CHOSEN_COMPONENT`→`LookupChoiceReceiver`→SharedPreferences (`ACTION_CHOOSER` has no native Always); reset row in `CustomDictionaries.tsx` ## Feature Notes +- [Edge TTS word highlighting (#4017, PR #4566)](edge-tts-word-highlighting-4017.md) — keep sentence marks, add word highlight via `audio.metadata` WordBoundary (verbatim input span, 100-ns ticks) synced to `audio.currentTime` by rAF; readaloud endpoint gates on UA (Edg, non-headless) NOT Origin; fixed browser `new WebSocket(url,{headers})` SyntaxError (wss never worked on web); overlay = `` in FOLIATE-PAGINATOR shadow root; dev-web verify recipe (browse --proxy + UA spoof, never Origin header) - [Reference Pages (#672+#4542, PR #4549)](reference-pages-672-4542.md) — 'reference' progressStyle from foliate `pageItem`/`book.pageList` (numeric-max total rule, roman-tail safe); per-book `referencePageCount` via skipGlobal save; verification EPUBs + dev-web synthetic drag-drop import trick; locale-tail rebase-conflict recipe (checkout --ours → re-extract → re-translate) - [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) - [OPDS 2.0 JSON search greyed out (#4502)](opds2-json-search-4502.md) — `isSearchLink` ignored templated `application/opds+json` links → `hasSearch` false → disabled navbar input; add `MIME.OPDS2`+`templated`, `expandOPDSSearchTemplate` (foliate `uri-template.js`), handleSearch OPDS2 branch. Gotcha: `resolveURL` mangles `{?query}` braces — expand template BEFORE resolving @@ -70,7 +73,9 @@ - [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()` - [Footnote aside border line (#4438)](footnote-aside-namespace-order-4438.md) — v0.11.4 regression: stray horizontal line below footnote marker. #4383 inlined custom `@font-face` BEFORE the `@namespace epub` (which lived in `getPageLayoutStyles`), invalidating it per CSS spec → namespaced `aside[epub|type~="footnote"]{display:none}` dropped → book's `aside{border:3px double}` showed. Only with custom fonts loaded. Fix = hoist `@namespace` to front of `getStyles`. Repro needs XHTML (`epub:type` namespaced only in XML); Playwright `setContent` parses HTML and won't reproduce +- [Scrolled-mode notch mask vs texture (#4486)](notch-mask-texture-4486.md) — top inset mask occluded the bg texture; full-cell + clip-path paint-box-matching for tile alignment; CDP-inject + MAE seam verification on device; adb taps in status-bar region eaten by SystemUI - [Paragraph-mode accidental exit + off-center bar (#4474)](paragraph-mode-accidental-exit-4474.md) — backdrop/center taps exited focus mode (stray "too high/low" taps); `ParagraphBar` only reshows on mousemove (no touch reshow) so can't just delete tap-exits → new `paragraph-show-controls` event reveals the bar instead. Also bar `absolute`→`fixed`: it centered on the gridcell which a pinned sidebar shifts right, while the paragraph centers on the `fixed inset-0` overlay/viewport +- [Share intent + customizable toolbar (#4014)](annotation-share-toolbar-4014.md) — Share tool in the selection toolbar (sharekit gated mobile+macOS only re: #4343 Windows freeze; `canShareText`/`shareSelectedText` in dual-purpose `share.ts`) + drag-and-drop customizer sub-page; `annotationToolbarItems` view setting (Share hidden by default); pure helpers in `annotationToolbar.ts` ## 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/annotation-share-toolbar-4014.md b/apps/readest-app/.claude/memory/annotation-share-toolbar-4014.md new file mode 100644 index 00000000..04b51915 --- /dev/null +++ b/apps/readest-app/.claude/memory/annotation-share-toolbar-4014.md @@ -0,0 +1,42 @@ +--- +name: annotation-share-toolbar-4014 +description: "Share intent in the selection toolbar + drag-and-drop toolbar customizer (#4014)" +metadata: + node_type: memory + type: project + originSessionId: 507a0166-cb55-4f33-b633-3230c0c514ff +--- + +#4014 — added a native "Share" tool to the in-reader text-selection toolbar plus a +drag-and-drop customizer (show/hide + reorder tools). Branch +`feat/annotation-share-toolbar-4014`; spec + plan in +`docs/superpowers/{specs,plans}/2026-06-13-annotation-share-toolbar*`. + +Key facts / gotchas: +- **`src/utils/share.ts` is dual-purpose** — it already held share-LINK helpers + (`buildShareUrl`/`parseShareDeepLink` for the `/s/{token}` feature). Text-share was + added there: `shareSelectedText(text, position?, appService?)` and + `canShareText(appService)`. +- **Native share is gated to mobile + macOS only** (`isMobileApp || isMacOSApp`). + Windows/Linux desktop are excluded because `@choochmeque/tauri-plugin-sharekit-api`'s + share UI can FREEZE the app on Windows (issue #4343) — `nativeAppService.saveFile` + gates `shareFile` the same way. Ladder: native → `navigator.share` → clipboard. + `canShareText` = that OR web `navigator.share`; used to gate Share's visibility in + toolbar + customizer + the quick-action dropdown. +- **Toolbar order is a view setting**: `AnnotatorConfig.annotationToolbarItems` + (`src/types/book.ts`), default in `DEFAULT_ANNOTATOR_CONFIG` = the original 8 tools, + **Share hidden by default** (starts in the "Available" tray). No migration needed: + the `{...getDefaultViewSettings(ctx), ...saved}` merge in `settingsService.ts` + + `getToolbarToolTypes(undefined,...)` fallback both yield the default. +- **Pure helpers** in `src/utils/annotationToolbar.ts` (unit-tested) own all + order/visibility logic: `getToolbarToolTypes`/`getAvailableToolTypes` (canShare-gated, + dedup, drop-unknown), `add/remove/reorderToolbar`. `ALL_ANNOTATION_TOOL_TYPES` is + asserted to match the `annotationToolButtons` registry order by a test. +- **Customizer** = `src/components/settings/AnnotationToolbarCustomizer.tsx`, a sub-page + off `ControlPanel` (Behavior panel) via `NavigationRow`, two `@dnd-kit` zones (pattern + copied from `CustomDictionaries.tsx`). Chips are tap-to-toggle AND drag (e-ink/keyboard + a11y, since no KeyboardSensor). Cross-platform guard: when editing on a `!canShare` + device, `persist` re-appends a `share` that was synced-in but hidden, so it isn't + dropped for the user's share-capable devices. +- Adding a tool to the union (`AnnotationToolType`) is compile-checked: the + `createAnnotationToolButtons` generic in `AnnotationTools.tsx` requires every member. diff --git a/apps/readest-app/.claude/memory/dark-mode-texture-body-bg-4446.md b/apps/readest-app/.claude/memory/dark-mode-texture-body-bg-4446.md new file mode 100644 index 00000000..c04d0b01 --- /dev/null +++ b/apps/readest-app/.claude/memory/dark-mode-texture-body-bg-4446.md @@ -0,0 +1,73 @@ +--- +name: dark-mode-texture-body-bg-4446 +description: "#4446 dark-mode bg texture occluded by body.theme-dark opaque bg !important (style.ts getDarkModeLightBackgroundOverrides); verified on Xiaomi via CDP; multiview = patch ALL section iframes" +metadata: + node_type: memory + type: project + originSessionId: 61f864c0-488e-466c-89e9-86df66b57d42 +--- + +RESOLVED — PR #4564 MERGED 2026-06-12 (`fix/dark-mode-texture-4446`, built on origin/main +via temp-index plumbing from the dirty dev tree; branch + local mods cleaned after merge). +User-verified on the Xiaomi via `pnpm dev-android` (full aarch64 build + `adb install -r`, +~7 min). + +#4446 remaining case (after [[paginated-texture-occlusion-4399]] fixed light mode): in +**dark mode** the bg texture is absent in paginated entirely, and absent in the scrolled +text area while header/footer still show it. + +**Root cause (verified live on Xiaomi 2211133C via CDP, no rebuild).** +`getDarkModeLightBackgroundOverrides` (style.ts ~194) emits +`body.theme-dark { background-color: ${bg} !important; }`, applied when +`isDarkMode && !overrideColor` (style.ts ~320). That paints every section iframe's body +opaque dark (`rgb(34,34,34)`), which occludes the host `.foliate-viewer::before` texture. +Downstream it also poisons foliate: `resolveBackground(view.docBackground)` resolves the +opaque body color, so `textureAwareBackground` keeps the paginated `#background` segment +and the scrolled `view.element` inline bg opaque too. The #4399 fix is intact (container +is transparent); this is a second, dark-mode-only occluder one layer deeper. + +- Paginated: segment + iframe body span the full viewer → no texture anywhere. +- Scrolled: iframes cover only the text column → header/footer strips keep texture + (grid-cell + the #4486 notch overlay, see [[notch-mask-texture-4486]]). + +**Proof:** injecting `body.theme-dark{background-color:transparent !important}` into ALL +section iframes + clearing the segment/view inline bgs reveals the leaves texture in both +modes instantly. + +**Regression source (git-proven):** commit `176b950c9` = PR #4392 (2026-06-01, shipped +v0.11.4) added the rule. NOT foliate-js — the foliate swipe-flash regression (#4399, +167757a→142bf11) broke LIGHT-mode textures in the same release window, which is why it +looked like one. The transformStylesheet light-bg rewriter (also #4392) was EXONERATED +for the repro book: Alice's stylesheets have zero body/html background rules (verified by +on-device stylesheet enumeration). **No #4392 revert needed** — its callout attribute +selectors + rewriter fix real legibility bugs (#4028; #4419/#4426 build on them). + +**Fix (applied):** make the rule `background-color: transparent !important` — +UNCONDITIONALLY, not gated on hasBackgroundTexture, because foliate captures +`docBackground` once per section load (paginator.js `load` listener; `setStyles` re-runs +`#replaceBackground` but never re-captures), so a texture-gated body bg would go stale on +live texture toggling. Visuals without texture are identical: the dark fill comes from the +paginator container `fallbackBg` / reader grid cell. Book-forced light page bgs stay +neutralized (theme-dark fill shows through); page-level rewriter output is cascade-beaten +by our `!important` body rule and later-in-head `html` rule; only book `!important` +page rules survive — consistent with the #4399 "book-forced opaque page wins" policy. +Test: `style-get-styles.test.ts` "#4446" cases. E2E device-verified: with the fixed CSS +present at load, capture is transparent and the paginated segments array comes out EMPTY. + +**Verification gotchas (cost ~30 min):** +- **Multiview!** The renderer shadow root holds MULTIPLE section iframes (adjacent preload). + Patching `sr.querySelector('iframe')` hits a section possibly 18k px off-viewport → + "fix didn't work". Patch every `sr.querySelectorAll('iframe')`. +- `elementsFromPoint` reports the iframe ELEMENT's computed bg (transparent) — the + occluding paint is its content document's body, invisible to the top-doc/shadow stack walk. +- Switching `renderer.setAttribute('flow', ...)` can reload section docs and silently wipe + styles injected into them (but not always — re-check after every flow switch). +- Pseudo-element paint test: patch the `#background-texture` style text with + `background-color: red` — if red doesn't show, the pseudo is occluded, not broken. +- **Stale preload views**: after patching styles via `renderer.setStyles`, views loaded + PRE-patch keep their opaque `docBackground` and `#clearViewsExcept` keeps `|i−index|≤2` + across navigation — an opaque segment can come from a kept old view, not the fresh one. + Jump ≥3 sections to guarantee fresh captures. +- **Capture-time instrumentation**: the paginator dispatches `load` synchronously right + BEFORE `docBackground = getBackground(doc)` — an event listener on the renderer sees + exactly what the capture will see (class list, computed bgs, active rules). diff --git a/apps/readest-app/.claude/memory/dict-import-contenturi-filename-4489.md b/apps/readest-app/.claude/memory/dict-import-contenturi-filename-4489.md new file mode 100644 index 00000000..ac106931 --- /dev/null +++ b/apps/readest-app/.claude/memory/dict-import-contenturi-filename-4489.md @@ -0,0 +1,45 @@ +--- +name: dict-import-contenturi-filename-4489 +description: "Android dict import \"incomplete bundle\" — ext-less content URIs; classify used getFilename (string) not basename (content resolver)" +metadata: + node_type: memory + type: project + originSessionId: 92f1011c-89b9-4ae1-b128-f24cfb4462a8 +--- + +#4489 / #4472: importing a StarDict bundle (`.ifo`+`.idx`+`.dict.dz`) on **some** Android +devices fails with "Skipped incomplete bundles"; works on Xiaomi/Boox and web. + +**Root cause — two filename resolvers diverged.** Tauri's Android `path.file_name` +(used by `basename`) special-cases `content://`/`file://` URIs and calls the native +`getFileNameFromUri` plugin → **content resolver DISPLAY_NAME** (real filename WITH ext). +See `tauri-2.11.2/src/path/android.rs`. Our `getFilename()` (`src/utils/path.ts`) is pure +JS string-parse of the URI. On devices whose SAF URI is an **opaque ext-less document id** +(e.g. `content://com.android.providers.downloads.documents/document/msf%3A20`), getFilename +→ `msf%3A20` (no ext) while basename → `21cen.dict.dz`. Working devices return +`primary%3ADictionaries%3A21cen.dict.dz` (ext in the URI) so getFilename happens to work. + +The OLD `selectFileTauri` extension filter ALREADY used `basename` (so files passed the +filter and got imported), but **threw the resolved name away** and returned the raw URI. +Then `dictionaryService.classify()` re-derived the name with `getFilename()` → no ext → +every file orphaned → "incomplete bundle". The bug is the divergence, not the picker. + +**Fix (PR for #4489):** +- `SelectedFile.name?: string` added (`useFileSelector.ts`). +- `resolveTauriFileName(path, appService)`: `basename` for `content://` / iOS `file://`, + else `getFilename` — resolved ONCE in `selectFileTauri`, reused by the ext filter AND + stored on `SelectedFile.name`. Removed `processTauriFiles`. +- `classify()` (`dictionaryService.ts`): `source.file?.name ?? source.name ?? getFilename(path)`. +- Test: `groupBundlesByStem.test.ts` — ext-less content URIs + `name` form a complete bundle. +- Also fixes #4472 issue 3 (uploaded dict files renamed to the SAF dir path). + +**Emulator repro (real granted URI, no rebuild needed):** drive SAF picker → "Downloads" +location → returns `content://...downloads.documents/document/msf%3A` (ext-less). +basename resolves the real name ONLY for granted URIs (synthetic URIs → "path does not have +a basename"). Verified via CDP `invoke('plugin:path|basename',{path,ext:null})`. See +[[cdp-android-webview-profiling]] for the CDP harness (`src/__tests__/android/helpers/`). + +**Also fixed same turn:** e-ink "black spot" on the Settings→Dictionaries `+` badges +(Import Dictionary / Add Web Search) — add `eink-inverted` to the round badge span, mirroring +the font import button #4454 (`globals.css` `[data-eink] .eink-inverted` → base-content bg + +base-100 icon). diff --git a/apps/readest-app/.claude/memory/dict-lookup-browser-hijack-4559.md b/apps/readest-app/.claude/memory/dict-lookup-browser-hijack-4559.md new file mode 100644 index 00000000..c38ce250 --- /dev/null +++ b/apps/readest-app/.claude/memory/dict-lookup-browser-hijack-4559.md @@ -0,0 +1,17 @@ +--- +name: dict-lookup-browser-hijack-4559 +description: Android system-dictionary lookup landing in the OEM browser instead of Eudic/欧路 — package-visibility + PROCESS_TEXT browser hijack +metadata: + node_type: memory + type: project + originSessionId: 5e397668-b766-439c-873c-00ccb1da715a +--- + +#4559 (PR #4568): on VIVO/iQOO (OriginOS) the system-dictionary lookup opened `com.vivo.browser/.BrowserActivity` instead of an installed dictionary. TWO root causes, both in the Android half of `show_lookup_popover` (`tauri-plugin-native-bridge/.../NativeBridgePlugin.kt`): + +1. **Package-visibility filtering (primary).** App is `targetSdk 36`, but the native-bridge manifest had NO `` for `ACTION_PROCESS_TEXT`. Under Android 11+ filtering, `queryIntentActivities(PROCESS_TEXT)` then returns only *auto-visible* apps — web browsers are auto-visible (web-intent exception), arbitrary dictionary apps (Eudic/欧路/GoldenDict) are NOT. So the query returned just the browser. Fix = add `` to the plugin manifest (mirrors the native-tts `TTS_SERVICE` pattern). This alone is likely the whole user-visible fix. +2. **Browser hijack.** Even when visible, an OEM browser registering `ACTION_PROCESS_TEXT` can be the system default and swallow a plain `startActivity`. Fix = filter browsers out of the handler set in a pure `decideLookupDispatch(handlers, browserPackages, remembered)` (new `LookupDispatch.kt`, JUnit-tested): no-browser → implicit (unchanged, keeps native "Always"); browser+1 dict → explicit `setClassName` direct launch; browser+≥2 → `createChooser` + `EXTRA_EXCLUDE_COMPONENTS`; browser-only → `unavailable:true`. Browsers detected via `queryIntentActivities(ACTION_VIEW https + BROWSABLE)` (auto-visible, no `` needed). + +**Remember-the-choice** (the maintainer wanted "smooth once chosen"): `ACTION_CHOOSER` has NO native "Always" button (mutually exclusive with `EXTRA_EXCLUDE_COMPONENTS` — the resolver that *has* Always can't exclude and obeys the browser default). Re-implemented Always: pass an `IntentSender` to `createChooser`; system returns `EXTRA_CHOSEN_COMPONENT` to a manifest `LookupChoiceReceiver` (exported=false; explicit intra-app PendingIntent so non-exported is fine; FLAG_MUTABLE on S+) which persists pkg/class to a plain SharedPreferences (`readest_lookup_dictionary_v1`). Next lookup fast-paths it. Reset UI: `get_lookup_dictionary`/`clear_lookup_dictionary` commands (build.rs COMMANDS + default.toml + autogenerated TOMLs/schema/reference regenerate on `cargo build -p tauri-plugin-native-bridge`) → Android-only conditional reset row in `CustomDictionaries.tsx`, only shown when something is actually remembered (`getRememberedLookupApp` returns null otherwise, so no clutter). + +Maintainer DECLINED a settings picker to pre-pick a specific app ("we won't call the app directly"); browser-exclusion respects that (dynamic, not a user-set hardcode). Verified: gradle JUnit (7 cases) + vitest (12). `test:rust`/`clippy` were blocked by UNRELATED stale shared-`target/` cache (deleted sibling worktree `readest-feat-android-rangefile-protocol` path in `fs` plugin permission scan) — not my change; validated Rust via targeted plugin build. Related: [[android-open-with-intent-flow]], [[android-nativefile-remotefile-io]]. diff --git a/apps/readest-app/.claude/memory/edge-tts-word-highlighting-4017.md b/apps/readest-app/.claude/memory/edge-tts-word-highlighting-4017.md new file mode 100644 index 00000000..3eb05f35 --- /dev/null +++ b/apps/readest-app/.claude/memory/edge-tts-word-highlighting-4017.md @@ -0,0 +1,23 @@ +--- +name: edge-tts-word-highlighting-4017 +description: "Edge TTS word-by-word highlighting (#4017, PR" +metadata: + node_type: memory + type: project + originSessionId: afd9b381-c17d-4988-b287-07263d8bea0b +--- + +# Edge TTS word-by-word highlighting (#4017, PR #4566) + +**Design: keep sentence granularity, add word highlight on top.** All clients still report `getGranularities() = ['sentence']` — switching foliate to word marks would regress media-session metadata (one word on lock screen), byMark seek (word steps), `getSpokenSentence`, and per-word synthesis. Instead: `EdgeSpeechTTS.createAudio()` returns `{url, boundaries}` (cached per payload-hash next to the blob URL), `EdgeTTSClient` runs a rAF loop syncing `audio.currentTime` (media time → playbackRate/pause-safe) against boundary ticks, and `TTSController.prepareSpeakWords/dispatchSpeakWord` match words sequentially (`indexOf` with a moving cursor; unmatched word = skip WITHOUT advancing cursor) against the sentence range text, then highlight the sub-range via the existing `#getHighlighter`. + +**Edge wire facts** (verified with raw WS probe + live): +- `audio.metadata` frames: `{"Metadata":[{"Type":"WordBoundary","Data":{"Offset":1000000,"Duration":4250000,"text":{"Text":"Dr.","Length":3}}}]}` — one word/frame, ticks = 100 ns (1e7/s), offsets relative to this request's audio stream. +- `Text` is the **verbatim input span** ("Dr.", "23", "$5.50" keep punctuation; trailing sentence punctuation stripped) → sequential indexOf matching is robust. Works for zh too. +- The readaloud endpoint gates on **User-Agent (needs Edg/non-headless), NOT Origin** — a localhost Origin with Edg UA is accepted; default HeadlessChrome UA is rejected (close 1006). + +**Pre-existing bug fixed in the same PR:** browser branch did `new WebSocket(url, {headers})` → native WebSocket parses the object as a subprotocol → `SyntaxError` → on web the wss path could NEVER work (always https-proxy fallback, which strips boundaries). Node-only options now. + +**Probe gotchas:** Overlayer draws the highlight as a `` inside `` (NOT `` — rect-only DOM probes miss it); the overlayer svg lives in `FOLIATE-PAGINATOR`'s open shadow root (sibling layer of the iframe, not inside it). TTS auto-advance creates new views — re-query svgs per sample, never cache the list. + +**dev-web live-verify recipe:** gstack `browse --proxy http://127.0.0.1:8118` (flag needed on EVERY invocation; this machine's external net needs the local proxy, headless Chromium doesn't inherit it) + `browse useragent '...Edg/143...'` (context-level, doesn't break Next) — do NOT use `browse header Origin:...` (extra headers hit localhost too → Next dev 403s ALL chunks → blank page; headers can't be removed without daemon restart). Import books via synthetic drop: fetch epub from `public/`, `DataTransfer` + `DragEvent('drop')` on `.library-page` (in-memory only — re-import after every reload/restart). Patch `content.overlayer.add/remove` to log the real highlight calls — the ground truth when screenshots race. Related: [[tts-fixes]] diff --git a/apps/readest-app/.claude/memory/notch-mask-texture-4486.md b/apps/readest-app/.claude/memory/notch-mask-texture-4486.md new file mode 100644 index 00000000..a993d030 --- /dev/null +++ b/apps/readest-app/.claude/memory/notch-mask-texture-4486.md @@ -0,0 +1,40 @@ +--- +name: notch-mask-texture-4486 +description: "Scrolled-mode top inset mask occluded the bg texture; clip-path full-cell trick aligns the mask's texture tiles with the viewer's; CDP-inject + MAE seam verify" +metadata: + node_type: memory + type: project + originSessionId: 47d2276d-0e04-455c-99b5-4fd0a651b579 +--- + +#4486 (PR #4563): in scrolled mode the `notch-area` in `SectionInfo.tsx` masks the top +safe-area inset with opaque `bg-base-100` at z-10 (hides content scrolling under the +status bar) — and painted over the texture (`.foliate-viewer::before` lives at the z-0 +paint layer, see [[paginated-texture-occlusion-4399]]). Flat untextured strip across the +unsafe header area. + +**Fix pattern (paint-box matching).** A texture `::before` only tile-aligns with the +viewer's when `background-size: cover/contain` resolves against the SAME element box. +So: make the mask span the grid cell (`inset-0`) and clip the visible+hit area to the +strip with `clip-path: inset(0 0 calc(100% - topInsetPx) 0)`; add `.notch-masked::before` +to the selector group in `styles/textures.ts`, gated by a conditional class only in +scrolled-horizontal mode (paginated/vertical notch is transparent — texturing it would +double-texture over the viewer's). `mix-blend-mode: multiply` blends against the mask's +own opaque bg inside its z-10 stacking context → identical color math to the viewer area. +clip-path clips hit-testing too, so the click target stays the strip (verified with +`elementsFromPoint`: notch present in stack inside strip, absent mid-screen). + +**Still texture-unaware** (same flaw, not yet reported): the vertical scrolled-mode side +masks in `BooksGrid.tsx` (`bg-base-100 absolute left-0/right-0 h-full` when +vertical+scrolled). Same trick applies if reported. + +**Verification technique (no rebuild).** Drive the installed app on the device via +adb+CDP ([[cdp-android-webview-profiling]]) and inject the exact CSS artifacts the fix +produces (patch the `#background-texture` style text + classList/style edits — React +won't wipe manual DOM edits unless its computed className/style prop string changes +between renders). Quantify the seam: `magick compare -metric MAE` between adjacent +1px rows across the boundary — buggy hard seam was MAE 11913 (16-bit scale), fixed +seam 230 ≈ ordinary texture-row variation. Gotchas: adb taps at status-bar y-coords are +consumed by SystemUI (never reach the app) — use elementFromPoint or in-page click +counters for hit-testing; a mid-screen tap toggles the header/footer chrome which then +sits ABOVE the notch and pollutes hit stacks (toggle it off before probing). diff --git a/apps/readest-app/.claude/memory/tts-fixes.md b/apps/readest-app/.claude/memory/tts-fixes.md index 0e56a189..a6fe08bc 100644 --- a/apps/readest-app/.claude/memory/tts-fixes.md +++ b/apps/readest-app/.claude/memory/tts-fixes.md @@ -39,6 +39,7 @@ TTS tracks its own section independently from the view via `#ttsSectionIndex`: | #3291 | TTS fails without lang attribute | Invalid SSML from missing lang | Set lang/xml:lang on html element from `ttsLang` | | #3292 | Can't restart TTS from annotation | `ttsOnRef` blocks re-entry | Removed the guard ref entirely | | #3400 | TTS highlight stops after restart | `view.tts` not nulled on shutdown | Added `this.view.tts = null` in `shutdown()` | +| #4033 | Voice count flip-flops within one book (17↔5) | All 3 clients filtered voices by full locale (`v.lang.startsWith(locale)`); panel lang refreshes from the speaking mark (`getSpeakingLang`), and books mix region variants — Standard Ebooks boilerplate is `en-US` (17 Edge voices), body `en-GB` (5 Edge voices) | PR #4565: filter by primary lang (`isSameLang`) in Edge/Web/Native `getVoices`; new `TTSUtils.sortVoicesPreferLocaleFunc(locale)` keeps exact-locale voices first so `getVoiceIdFromLang` default stays region-aware. Also fixed `zh-Hans` → empty Edge list | ## Debugging TTS Issues