From f7f85330ae6b22d7c1595614790903a57565f975 Mon Sep 17 00:00:00 2001 From: Huang Xin Date: Mon, 6 Jul 2026 14:29:34 +0900 Subject: [PATCH] chore(agent): update agent memories (#4958) --- apps/readest-app/.claude/memory/MEMORY.md | 253 ++++++++---------- .../android-e2e-doubletap-cdp-gesture.md | 56 ++++ .../memory/calibre-custom-columns-4811.md | 24 ++ .../memory/calibre-plugin-push-4863.md | 2 +- .../memory/dict-popup-tts-speak-4876.md | 46 ++++ .../memory/edge-tts-webaudio-engine.md | 25 ++ .../ios-share-txt-stuck-supportstext.md | 18 ++ .../ios-widget-cover-bright-edge-line.md | 20 ++ ...plugin-library-stale-synced-cursor-4934.md | 23 ++ .../memory/middle-click-autoscroll-4951.md | 48 ++++ .../opds-autodownload-subdir-crawl-4272.md | 16 ++ .../page-turn-styles-viewtransitions-555.md | 28 ++ .../pdf-text-selection-fontscale-4480.md | 18 ++ .../memory/pinch-vs-twofinger-scroll-4858.md | 22 ++ .../memory/sentry-crash-reporting-4914.md | 32 +++ .../transfer-queue-clear-persistence.md | 18 ++ .../tts-background-session-decoupling.md | 18 ++ .../memory/turso-concurrent-use-forbidden.md | 21 ++ .../webdav-group-membership-sync-4942.md | 16 ++ .../xcode26-swiftrs-ios-build-broken.md | 18 ++ .../gen/apple/ReadestWidget/Info.plist | 2 +- 21 files changed, 587 insertions(+), 137 deletions(-) create mode 100644 apps/readest-app/.claude/memory/android-e2e-doubletap-cdp-gesture.md create mode 100644 apps/readest-app/.claude/memory/calibre-custom-columns-4811.md create mode 100644 apps/readest-app/.claude/memory/dict-popup-tts-speak-4876.md create mode 100644 apps/readest-app/.claude/memory/edge-tts-webaudio-engine.md create mode 100644 apps/readest-app/.claude/memory/ios-share-txt-stuck-supportstext.md create mode 100644 apps/readest-app/.claude/memory/ios-widget-cover-bright-edge-line.md create mode 100644 apps/readest-app/.claude/memory/koplugin-library-stale-synced-cursor-4934.md create mode 100644 apps/readest-app/.claude/memory/middle-click-autoscroll-4951.md create mode 100644 apps/readest-app/.claude/memory/opds-autodownload-subdir-crawl-4272.md create mode 100644 apps/readest-app/.claude/memory/page-turn-styles-viewtransitions-555.md create mode 100644 apps/readest-app/.claude/memory/pdf-text-selection-fontscale-4480.md create mode 100644 apps/readest-app/.claude/memory/pinch-vs-twofinger-scroll-4858.md create mode 100644 apps/readest-app/.claude/memory/sentry-crash-reporting-4914.md create mode 100644 apps/readest-app/.claude/memory/transfer-queue-clear-persistence.md create mode 100644 apps/readest-app/.claude/memory/tts-background-session-decoupling.md create mode 100644 apps/readest-app/.claude/memory/turso-concurrent-use-forbidden.md create mode 100644 apps/readest-app/.claude/memory/webdav-group-membership-sync-4942.md create mode 100644 apps/readest-app/.claude/memory/xcode26-swiftrs-ios-build-broken.md diff --git a/apps/readest-app/.claude/memory/MEMORY.md b/apps/readest-app/.claude/memory/MEMORY.md index 2a14bd03..42b99630 100644 --- a/apps/readest-app/.claude/memory/MEMORY.md +++ b/apps/readest-app/.claude/memory/MEMORY.md @@ -1,149 +1,132 @@ # Readest Project Memory ## Key Reference Documents (aggregators) -- [Bug Patterns](bug-patterns.md) · [CSS & Style](css-style-fixes.md) — EPUB CSS + style.ts · [TTS](tts-fixes.md) -- [Layout & UI](layout-ui-fixes.md) — insets/z-index · [Platform Compat](platform-compat-fixes.md) — Android/iOS/Linux/macOS · [Annotator & Reader](annotator-reader-fixes.md) +- [Bug Patterns](bug-patterns.md) · [CSS & Style](css-style-fixes.md) EPUB CSS + style.ts · [TTS](tts-fixes.md) +- [Layout & UI](layout-ui-fixes.md) insets/z-index · [Platform Compat](platform-compat-fixes.md) · [Annotator & Reader](annotator-reader-fixes.md) ## Safety & Security -- [In-place delete wiped originals](in-place-delete-wiped-originals.md) — never `fs.removeFile` `external` source; -- [Backup zip Windows paths (#4703)](backup-windows-zip-paths-4703.md) — `\` entry names broke restore; normalize -- [download_file scope Android (#4639)](download-file-scope-android-regression.md) — strict `is_allowed` broke -- [Security advisories 2026-06](security-advisories-web-2026-06.md) — 4 GHSA #4638 (OPDS SSRF, storage key, Stripe +- [In-place delete wiped originals](in-place-delete-wiped-originals.md) never `fs.removeFile` `external` · [Backup zip Windows paths #4703](backup-windows-zip-paths-4703.md) normalize `\` · [download_file scope Android #4639](download-file-scope-android-regression.md) +- [Security advisories 2026-06](security-advisories-web-2026-06.md) 4 GHSA #4638 ## Paginator & Scroll -- [Reading ruler line-aware](reading-ruler-line-aware.md) — snaps to real lines; iframe frame-offset map -- [TOC expand + auto-scroll](toc-expand-and-autoscroll.md) — #4059 collapse-default breaks scroll-to-current -- [BooknoteView auto-scroll (#4352)](booknote-view-autoscroll-4352.md) — reload via `initialized` ref; -- [TOC current-position row](toc-current-position-row.md) — synthetic row; insert AFTER to keep flatItems index -- [Swipe page-turn bg flash](paginator-swipe-bg-flash.md) — static `#background`; `computeBackgroundSegments` per-rAF -- [Paginated texture occlusion (#4399)](paginated-texture-occlusion-4399.md) — opaque bg occludes texture; `hasTexture -- [Background overflows column (#4394)](paginator-gutter-bleed-asymmetry-4394.md) — bg bled into gutter; clamp to -- [Inline-block column overflow](inline-block-column-overflow.md) — body can't fragment; `#demoteUnfragmentableBoxes` -- [FXL fit-width scroll reset (#4683)](fixed-layout-paginated-scroll-reset-4683.md) — scrollTop not reset; -- [PDF spread 1px seam (#4587)](pdf-spread-canvas-seam-4587.md) — fractional dpr truncates; pin `canvas.style=viewport` -- [PDF scrolled wheel double (#4727)](pdf-scroll-mode-wheel-double-4727.md) — drop manual `scrollBy` over native -- [Scrolled header title center (#4436)](scrolled-header-title-center-4436.md) — view covering `renderedStart + size/2` -- [Duokan fullscreen cover scroll](duokan-fullscreen-cover-scroll.md) — #4379 cover collapses; gate on `this.#column` -- [TOC table heading clip (#4439)](toc-table-heading-clip-4439.md) — #4400 scroll-wrapper `overflow:auto` clips -- [Page-turn bg-replace reflow (#4785)](pageturn-bg-replace-reflow-4785.md) — `#replaceBackground` rebuilt context per +- Reading ruler: [line-aware](reading-ruler-line-aware.md) frame-offset map; [vertical-rl backwards #4865](reading-ruler-vertical-rtl-4865.md) +- [Vertical-rl horizontal pagination (#624)](vertical-rl-horizontal-pagination-624.md) — horizontal inputs + two-phase slide; rtl gated `!vertical` +- [Slide/curl turn styles via VT (#555)](page-turn-styles-viewtransitions-555.md) — VT turns gated on nested-VT-groups support (iOS 18 WebKit crashes despite having the API); Tauri fallback = `CapturedPageTurn` capture pipeline (WebGL mesh curl + flat canvas slide, full-gridcell capture, instant nav = drop `animated`); MERGED #4940 (2026-07-05), verified live mac/iOS/Android (mobile = JPEG capped 2x, PNG was 1.5s/turn); Win/Linux capture still open +- TOC: [expand + auto-scroll](toc-expand-and-autoscroll.md); [current-position row](toc-current-position-row.md); [table heading clip #4439](toc-table-heading-clip-4439.md); [BooknoteView auto-scroll #4352](booknote-view-autoscroll-4352.md) +- Paginated bg: [swipe flash](paginator-swipe-bg-flash.md); [texture occlusion #4399](paginated-texture-occlusion-4399.md); [gutter bleed #4394](paginator-gutter-bleed-asymmetry-4394.md); [bg-replace reflow #4785](pageturn-bg-replace-reflow-4785.md) +- [Inline-block column overflow](inline-block-column-overflow.md) `#demoteUnfragmentableBoxes` +- FXL/PDF: [fit-width scroll reset #4683](fixed-layout-paginated-scroll-reset-4683.md); [PDF spread seam #4587](pdf-spread-canvas-seam-4587.md); [spine seam #4857](fxl-spread-spine-seam-4857.md) +- Scrolled: [PDF wheel double #4727](pdf-scroll-mode-wheel-double-4727.md); [header title center #4436](scrolled-header-title-center-4436.md); [Duokan fullscreen cover](duokan-fullscreen-cover-scroll.md) ## Critical Files (Most Bug-Prone) -- `src/utils/style.ts` EPUB CSS hub · `packages/foliate-js/paginator.js` layout/image/bg · `src/services/tts/TTSController.ts` TTS state machine -- `src/hooks/useSafeAreaInsets.ts` insets · `src/app/reader/components/FoliateViewer.tsx` view orchestration · `.../annotator/Annotator.tsx` annotation lifecycle +- `src/utils/style.ts` EPUB CSS hub · `packages/foliate-js/paginator.js` · `src/services/tts/TTSController.ts` +- `src/hooks/useSafeAreaInsets.ts` · `src/app/reader/components/FoliateViewer.tsx` · `.../annotator/Annotator.tsx` ## Sync Notes -- [Grimmory native sync](grimmory-native-sync.md) — Booklore-fork REVERTED; id by ISBN/ASIN + koreader-hash -- KOSync: [CFI spine resolution](kosync-cfi-spine-resolution.md) convert via CFI's own spine; [connect() false-positive #4692](kosync-connect-false-positive-4692.md) -- [Empty-start CFI sync](empty-start-cfi-sync.md) — `isMalformedLocationCfi` → discard synced value -- [Custom fonts vanish on sync (#4410)](custom-fonts-reincarnation-4410.md) — CRDT remove-wins; re-import -- koplugin: [note deletion](koplugin-note-deletion-sync.md) `recordDeletion` tombstone; [stats #4666](koplugin-stats-sync.md) statistics.sqlite3 delta LuaSettings; [bulk download #4751](koplugin-bulk-download-4751.md) -- [Statusless books re-pin top (#4677)](sync-statusless-book-rebump-4677.md) — `(a??null)!==(b??null)` -- [Pull cursor via synced_at (#4678)](sync-synced-at-cursor-4678.md) — books `synced_at` + BEFORE trigger -- [WebDAV metadata sync (#4756)](webdav-metadata-sync-4756.md) — PR #4776; LWW on `book.updatedAt` + -- [File-sync refactor (#4784)](webdav-filesync-refactor-plan.md) — `FileSyncEngine`/`FileSyncProvider`/`merge.ts`; -- [Third-party library auto-sync (#4835)](third-party-library-autosync-4835.md) — `useLibraryFileSync` parity w/ -- [WebDAV connect nullified (#4780)](webdav-connect-nullified-4780.md) — catch+finally saved STALE closure; -- [WebDAV credential sync (#4810)](webdav-credential-sync-4810.md) — `webdav.*` missing from -- [Multi-window settings clobber (#4580)](multiwindow-settings-clobber-4580.md) — stale window overwrites shared -- Google Drive: [research](gdrive-sync-provider-research.md) Drive as `FileSyncProvider` token-persist+resumable; [multi-PR status](gdrive-provider-multipr-status.md) -- [Hardcover progress edition_id (#4792)](hardcover-progress-edition-id-4792.md) — `edition_id` fell back to +- [Grimmory native sync](grimmory-native-sync.md) Booklore-fork REVERTED +- KOSync: [CFI spine resolution](kosync-cfi-spine-resolution.md); [connect() false-positive #4692](kosync-connect-false-positive-4692.md) +- [Empty-start CFI sync](empty-start-cfi-sync.md) · [Custom fonts vanish #4410](custom-fonts-reincarnation-4410.md) CRDT remove-wins +- koplugin: [note deletion](koplugin-note-deletion-sync.md) tombstone; [stats #4666](koplugin-stats-sync.md); [bulk download #4751](koplugin-bulk-download-4751.md); [dup book rows #4861](koplugin-stats-duplicate-book-rows-4861.md) +- [Statusless re-pin #4677](sync-statusless-book-rebump-4677.md) · [pull cursor synced_at #4678](sync-synced-at-cursor-4678.md) +- [koplugin library stale #4934](koplugin-library-stale-synced-cursor-4934.md) pull cursor updated_at→synced_at + split push watermark + v2→v3 heal migration +- WebDAV: [metadata #4756](webdav-metadata-sync-4756.md) LWW; [group membership #4942](webdav-group-membership-sync-4942.md) mergeBookMetadata dropped groupId/groupName; [credentials #4810](webdav-credential-sync-4810.md); [connect nullified #4780](webdav-connect-nullified-4780.md) stale closure +- [WebDAV deletion + upload-after-enable (#4860/#4856)](webdav-deletion-and-upload-after-enable-4860-4856.md) edit-wins LWW + tombstone union +- File sync: [refactor #4784](webdav-filesync-refactor-plan.md) `FileSyncEngine`; [third-party auto-sync #4835](third-party-library-autosync-4835.md) +- [Transfer Queue clear not persisted](transfer-queue-clear-persistence.md) hook mutated store directly, skipped `persistQueue()`; route clears through `transferManager` +- [Multi-window settings clobber (#4580)](multiwindow-settings-clobber-4580.md) +- Google Drive: [research](gdrive-sync-provider-research.md); [multi-PR status](gdrive-provider-multipr-status.md) +- [Hardcover edition_id (#4792)](hardcover-progress-edition-id-4792.md) ## Build, Testing & CI -- [format:check separate gate](verify-format-check-gate.md) — `pnpm format:check` own gate before push -- [Worktree rebase submodule drift](worktree-rebase-submodule-drift.md) — rebase leaves foliate-js submodule old; `git -- [Android CDP e2e lane](android-cdp-e2e-lane.md) — `pnpm test:android` adb+CDP; CI KVM emulator -- [CDP Android WebView profiling](cdp-android-webview-profiling.md) — adb+CDP JS probes; locked-device freezes fetch -- [Tauri Rust↔JS parser parity](tauri-parser-parity-tests.md) — #4369 cross-check; `dcterms:modified`→`published` -- [TTS browser e2e harness](tts-browser-e2e-harness.md) — seed `settings.globalViewSettings` or getMergedRules crashes -- [TTS paragraph+RSVP sync (#3235)](tts-sync-paragraph-rsvp-3235.md) — TTS-is-clock; overlay CLONE via CSS Custom -- [fastlane App Store](fastlane-apple-appstore-submission.md) — keep `APPLE_API_KEY_PATH` out of macOS build env -- [Turbopack cache OOM (#4619)](turbopack-build-cache-oom-docker-standalone.md) — partial cache freeze; gate on -- [Deps override workflow](deps-security-overrides-workflow.md) — overrides in `pnpm-workspace.yaml`; tauri-plugins -- [pdfjs vendor wasm](pdfjs-vendor-wasm-decoders.md) — pdfjs 5.7 moved JBIG2 to `jbig2.wasm`; copy `wasm/*` -- [CI/PR delivery + push keepalive](ci-pr-delivery-and-push.md) — temp-index plumbing; SSH `ServerAliveInterval` +- [Nightly quick-sharun hang #4906](nightly-quick-sharun-hang-4906.md) pin via cache pre-seed + step timeouts +- [format:check separate gate](verify-format-check-gate.md) · [Worktree rebase submodule drift](worktree-rebase-submodule-drift.md) +- Android CDP: [e2e lane](android-cdp-e2e-lane.md) `pnpm test:android`; [WebView profiling](cdp-android-webview-profiling.md); [double-tap gesture](android-e2e-doubletap-cdp-gesture.md) +- [Tauri Rust↔JS parser parity](tauri-parser-parity-tests.md) +- TTS tests: [browser e2e harness](tts-browser-e2e-harness.md); [paragraph+RSVP sync #3235](tts-sync-paragraph-rsvp-3235.md) TTS-is-clock +- [fastlane App Store](fastlane-apple-appstore-submission.md) `APPLE_API_KEY_PATH` out of build env +- [Turbopack cache OOM (#4619)](turbopack-build-cache-oom-docker-standalone.md) +- [Deps override workflow](deps-security-overrides-workflow.md) `pnpm-workspace.yaml` +- [Xcode 26.2 broke iOS builds (swift-rs)](xcode26-swiftrs-ios-build-broken.md) — phantom `Bundle.main`/`privacy:` errors; vendored `packages/swift-rs` `--triple`/`--sdk`; Package.swift platforms floor now enforced +- [pdfjs vendor wasm](pdfjs-vendor-wasm-decoders.md) copy `wasm/*` +- [CI/PR delivery + push keepalive](ci-pr-delivery-and-push.md) ## Platform Compat -- [Android hyphen selection (#1553)](android-hyphen-selection-bounds-1553.md) — Blink start handle on last hyphen; -- [NativeFile vs RemoteFile I/O](android-nativefile-remotefile-io.md) — NativeFile slow; asset Range broken; -- [Window-state sanitizer (#4398)](window-state-sanitize-4398.md) — invalid json crashes WebView2; sanitize before -- [Android Open-with intent (#4521)](android-open-with-intent-flow.md) — VIEW gated by `autoImportBooksOnOpen` -- [Dict lookup browser hijack (#4559)](dict-lookup-browser-hijack-4559.md) — missing `` sdk36; filter in -- [Large-PDF OOM range flood (#3470)](pdf-oom-range-flood-3470.md) — un-awaited ranges OOM; MAX_CONCURRENT_RANGES=6 -- [Android themed icon (#4733)](android-themed-icon-4733.md) — no monochrome → force-commit; tint=SRC_IN +- Android: [hyphen selection #1553](android-hyphen-selection-bounds-1553.md); [NativeFile vs RemoteFile I/O](android-nativefile-remotefile-io.md) +- [Window-state sanitizer #4398](window-state-sanitize-4398.md) · [Android themed icon #4733](android-themed-icon-4733.md) +- [Open-with intent #4521](android-open-with-intent-flow.md) · [dict lookup hijack #4559](dict-lookup-browser-hijack-4559.md) +- [Large-PDF OOM range flood (#3470)](pdf-oom-range-flood-3470.md) MAX_CONCURRENT_RANGES=6 +- [Black screen external cache (#4853)](android-black-screen-external-cache-4853.md) FIXED PR#4889; drop `$CACHE` grants +- [macOS 26 Tahoe close→black window (#4875)](macos26-tahoe-close-black-screen-4875.md) `minimize()` not `hide()` +- [Linux app invisible after backup (#3682)](linux-transparent-window-invisible-3682.md) FIXED PR#4904 opaque window +- [Apple OAuth expired + deeplink swallowed (#4881)](oauth-deeplink-error-swallowed-4881.md) +- [Annotation link ignored when reader open #4887](deeplink-drop-running-macos-4887.md) `open-book-in-reader` event +- [iOS auto-brightness locks (#4885)](ios-brightness-lock-background-4885.md) brightness GLOBAL; release on bg +- [iOS share .txt stuck #4917](ios-share-txt-stuck-supportstext.md) drop SupportsText in `project.yml` (xcodegen src) +- [Updater disable non-AppImage (#4874)](updater-disable-nonappimage-linux-4874.md) +- [Fullscreen no-op Phosh (#4034)](fullscreen-maximized-phosh-4034.md) drop `isMaximized` branch ## Reader Features & UI -- [Mobile reading widgets (#1602/PR#4842)](mobile-reading-widgets.md) — home-screen widget iOS+Android; gotchas: iOS -- [PDF scrolled-mode lag (#4795/#4031)](pdf-scroll-lag-preload-4795.md) — 415ms/page render vs 50% margin; widen to -- [Scrolled-PDF pinch-zoom (#4817)](scrolled-pdf-pinch-zoom-4817.md) — live pinch + no-shift (gap×`--scroll-zoom` + -- [Search modes #4560 + spoiler-bound bug](search-modes-4560-and-spoiler-bound-bug.md) — regex + nearby-words; -- [OPDS groups carousel (#4750)](opds-groups-carousel-4750.md) — >=2 groups → virtuoso carousel; `scrollToIndex` -- [WebDAV browser sort + search (#4724)](webdav-browse-sort-search-4724.md) — PR #4786; -- [Image zoom trackpad flicker (#4742)](image-zoom-trackpad-flicker-4742.md) — macOS pinch=`ctrl+wheel`; -- [Instant Highlight ate tap/swipe](instant-highlight-tap-paginate.md) — preventDefault killed tap-paginate; -- [Keyboard selection adjust (#4728)](keyboard-selection-adjust-4728.md) — Shift+←/→ char, Ctrl/Alt+Shift word; -- [Double-click word select](iframe-double-click-word-select.md) — orphaned `iframe-double-click` → -- [Cross-page selection auto-turn (#4741)](cross-page-selection-autoturn-4741.md) — `useAutoPageTurn` dwell; -- [Annotator onLoad listener leak (#4735)](annotator-onload-listener-leak-paragraph-mode.md) — `useRendererInputListener -- [Paragraph mode toggle/resume (#4717)](paragraph-mode-toggle-resume-4717.md) — snapshot live Set; resume from fresh -- [Paragraph-mode accidental exit (#4474)](paragraph-mode-accidental-exit-4474.md) — `paragraph-show-controls`; bar -- [#4584 tap-death](issue-4584-tap-death-investigation.md) — UNFIXED; `isPopuped` RED HERRING; likely WebView-148 -- [Dblclick-drag turns page (#4524)](dblclick-drag-pageturn-4524.md) — `isMouseDown` gates `postSingleClick` -- [Tap to open image/table (#4600)](tap-to-open-image-table-4600.md) — `iframe-open-media` + `detectMediaTarget` -- [PDF/CBZ Contrast view-menu](pdf-cbz-contrast-view-menu.md) — per-book `contrast`; ONE `filter:` (invert+contrast); -- [iOS instant-dict double popup](ios-instant-dict-double-popup.md) — once-per-gesture latch; `isLongPressHold` 300ms -- Dict: [popup font size #4443](dict-popup-font-size-4443.md) `--dict-font-scale` MDict `::part(dict-content)`; [lemmatization #4574](dict-lemmatization-4574.md) -- Word Lens: [inline gloss](wordlens-feature.md) CFI-safe `` TTS/search isolation; [en-en](wordlens-en-en.md) -- [Stripe highest-active plan (#4694)](stripe-plan-highest-active-4694.md) — `plans.plan` = MAX over active subs -- [Save image to gallery (#4680)](save-image-to-gallery-android.md) — MediaStore; sharekit 0-byte self-copy -- [Webtoon Mode (#3647)](webtoon-mode-3647.md) — no-gap scrolled images; FXL fit-width; `--scroll-page-gap` -- [Biometric app-lock (#4645)](biometric-app-lock-4645.md) — flag from `appLockStore`; plugin `cfg(mobile)` -- [Reference Pages (#4542)](reference-pages-672-4542.md) — 'reference' progressStyle; `referencePageCount` -- [E-ink screen refresh page-turner (#4687)](eink-screen-refresh-pageturner-4687.md) — bindable 'refresh' action; -- [Share intent + toolbar (#4014)](annotation-share-toolbar-4014.md) — Share gated mobile+macOS; -- [Instant highlight delete orphan (#4773)](instant-highlight-delete-orphan-4773.md) — stale memoized index + in-place -- [Empty highlight leak on annotate cancel (#4791)](empty-highlight-leak-on-annotate-cancel-4791.md) — Annotate -- [Customize Toolbar global (#4760)](customize-toolbar-global-serializeconfig.md) — `serializeConfig` ref-compare → -- [Customize Toolbar e-ink black bar (#4839)](customize-toolbar-eink-black-bar-4839.md) — preview Zone copied -- Native TTS: [iOS #4676](native-ios-tts-4676.md) AVSpeechSynthesizer pause==stop rate `pow^(1/2.5)`; [offline halt #4613](native-tts-offline-autoadvance-4613.md) -- Edge TTS: [word highlight #4017](edge-tts-word-highlighting-4017.md) `audio.metadata` WordBoundary by rAF gate on UA; [drift](tts-word-highlight-singletextnode-drift.md) -- [TTS highlight granularity setting](tts-highlight-granularity-setting.md) — Word/Sentence; gate `prepareSpeakWords` -- [TTS start-from-selection](tts-start-from-selection.md) — use last mark at/before sel; cloneRange+deselect -- [Reuse TTS session on mode entry](tts-reuse-session-mode-entry.md) — `redispatchPosition()` + `tts-sync-request` -- RSVP: [control-bar overlap REVERT](rsvp-control-bar-overlap-revert.md) #4585 fixed/#4589 stale-reverted; [font face/family #4519](rsvp-font-settings-4519.md) `getBaseFontFamily` overlay top doc; [RTL word #4630](rsvp-rtl-word-display-4630.md) -- [Overlay z-index scale](zindex-overlay-scale.md) — RSVP 100 / Settings 110 / ModalPortal 120 / toast 130 / app-lock -- [Global annotation page-turn lag (#4575)](global-annotation-pageturn-perf-4575.md) — `global` re-fanned every turn; -- [Overlayer splitRange text nodes](overlayer-splitrange-textnodes.md) — `'p,h1-h4'` dropped `li`; walk text nodes + -- [Android image callout freeze](android-image-callout-freeze.md) — `.no-context-menu` on ANCESTOR -- [Table dark-mode tint (#4419)](table-dark-mode-tint-4419.md) — `blockquote, table *` tint gated on `overrideColor` -- [Footnote aside border line (#4438)](footnote-aside-namespace-order-4438.md) — @font-face before @namespace; hoist -- Proofread: [enhancements #4700](proofread-enhancements-4700.md) regex UI/Ctrl+P reuse/`wholeWord` no-op; [per-book CRDT #4781](proofread-per-book-crdt-sync.md) -- [Russian hanging-preposition NBSP (#4769)](russian-hanging-prepositions-nbsp-4769.md) — generic `nbsp` + -- OPDS: [Firefox strict-XML #4479](opds-firefox-strict-xml-4479.md) junk after `` slice to last close; [JSON search #4502](opds2-json-search-4502.md) expand `{?query}` BEFORE resolveURL; [HTML desc #4503](opds-html-description-4503.md) decode-once+sanitize; [self-link meta #4749](opds-self-link-metadata-4749.md) `rel:self` deref; [popular dedup #4782](opds-popular-catalog-dedup-4782.md) -- [D-pad Navigation](dpad-navigation.md) — Android TV remote / arrow-key nav -- [koplugin cover upload (#4374)](koplugin-cover-upload.md) — `extractLocalCover` via `getCoverImage` -- [Calibre plugin push #4863](calibre-plugin-push-4863.md) apps/readest-calibre-plugin; OAuth localhost relay + /sync carry-over +- [Android Auto TTS #3919/PR#4907](android-auto-tts-3919.md) MERGED; CarPlay blocked on entitlement +- Widgets: [mobile reading #1602/PR#4842](mobile-reading-widgets.md); [iOS App Group stripped PR#4891](ios-widget-appgroup-stripped-appstore.md); [cover bright right-edge line](ios-widget-cover-bright-edge-line.md) fractional resize → round target to whole px +- PDF: [scrolled lag #4795](pdf-scroll-lag-preload-4795.md); [scrolled pinch-zoom #4817](scrolled-pdf-pinch-zoom-4817.md); [pinch vs two-finger scroll #4858](pinch-vs-twofinger-scroll-4858.md); [text selection misplaced w/ OS font scale #4480](pdf-text-selection-fontscale-4480.md) divide `--total-scale-factor` by detected font scale +- [Search modes #4560](search-modes-4560-and-spoiler-bound-bug.md) +- [OPDS groups carousel #4750](opds-groups-carousel-4750.md) · [WebDAV browser sort+search #4724](webdav-browse-sort-search-4724.md) +- [Image zoom trackpad flicker (#4742)](image-zoom-trackpad-flicker-4742.md) macOS pinch=`ctrl+wheel` +- Instant highlight: [ate tap/swipe](instant-highlight-tap-paginate.md); [delete orphan #4773](instant-highlight-delete-orphan-4773.md); [empty leak #4791](empty-highlight-leak-on-annotate-cancel-4791.md) +- Selection: [keyboard adjust #4728](keyboard-selection-adjust-4728.md); [cross-page auto-turn #4741](cross-page-selection-autoturn-4741.md) +- Click/tap: [double-click word select](iframe-double-click-word-select.md); [dblclick-drag #4524](dblclick-drag-pageturn-4524.md); [tap open image/table #4600](tap-to-open-image-table-4600.md) +- [Annotator onLoad listener leak (#4735)](annotator-onload-listener-leak-paragraph-mode.md) +- Paragraph mode: [toggle/resume #4717](paragraph-mode-toggle-resume-4717.md); [accidental exit #4474](paragraph-mode-accidental-exit-4474.md) +- [#4584 tap-death](issue-4584-tap-death-investigation.md) UNFIXED; likely WebView-148 +- [PDF/CBZ Contrast view-menu](pdf-cbz-contrast-view-menu.md) ONE `filter:` +- [Header/footer over light PDF in dark (#4901)](pdf-header-footer-contrast-blend-4901.md) `mix-blend-difference`, FIXED light anchor +- [iOS instant-dict double popup](ios-instant-dict-double-popup.md) once-per-gesture latch +- Dict: [popup font size #4443](dict-popup-font-size-4443.md); [lemmatization #4574](dict-lemmatization-4574.md); [popup speak button #4876](dict-popup-tts-speak-4876.md) standalone wordPronouncer, Edge-direct + dedicated WebAudio ctx +- Word Lens: [inline gloss](wordlens-feature.md) CFI-safe ruby; [en-en](wordlens-en-en.md) +- [Stripe highest-active plan (#4694)](stripe-plan-highest-active-4694.md) +- [Save image to gallery (#4680)](save-image-to-gallery-android.md) MediaStore +- [Webtoon Mode (#3647)](webtoon-mode-3647.md) +- [Middle-click autoscroll #4951](middle-click-autoscroll-4951.md) Autoscroller RAF core; `containerPosition +=`; armed-books preventDefault +- [Biometric app-lock #4645](biometric-app-lock-4645.md) · [Reference Pages #4542](reference-pages-672-4542.md) · [e-ink refresh page-turner #4687](eink-screen-refresh-pageturner-4687.md) +- [Share intent + toolbar (#4014)](annotation-share-toolbar-4014.md) +- Customize Toolbar: [global serializeConfig #4760](customize-toolbar-global-serializeconfig.md); [e-ink black bar #4839](customize-toolbar-eink-black-bar-4839.md) +- [Edge TTS Web Audio engine (#3851)](edge-tts-webaudio-engine.md) gapless WebAudioPlayer + WSOLA +- [Background TTS sessions PR#4941](tts-background-session-decoupling.md) — hash-keyed session manager, detach/attach, NowPlayingBar; header X routes `onCloseBook` NOT `onGoToLibrary` +- Native TTS: [iOS #4676](native-ios-tts-4676.md) pause==stop; [offline halt #4613](native-tts-offline-autoadvance-4613.md) +- Edge TTS: [word highlight #4017](edge-tts-word-highlighting-4017.md); [drift](tts-word-highlight-singletextnode-drift.md) +- TTS UX: [highlight granularity](tts-highlight-granularity-setting.md); [start-from-selection](tts-start-from-selection.md); [reuse session](tts-reuse-session-mode-entry.md) +- RSVP: [control-bar REVERT](rsvp-control-bar-overlap-revert.md); [font #4519](rsvp-font-settings-4519.md); [RTL word #4630](rsvp-rtl-word-display-4630.md) +- [Overlay z-index scale](zindex-overlay-scale.md) RSVP 100 → app-lock +- [Global annotation page-turn lag (#4575)](global-annotation-pageturn-perf-4575.md) +- [Overlayer splitRange text nodes](overlayer-splitrange-textnodes.md) +- [Android image callout freeze](android-image-callout-freeze.md) `.no-context-menu` ANCESTOR +- [Inline-img vertical-align (#4866)](inline-img-vertical-align-4866.md) gated on computed valign +- [Table dark-mode tint #4419](table-dark-mode-tint-4419.md) · [footnote aside border #4438](footnote-aside-namespace-order-4438.md) +- Proofread: [enhancements #4700](proofread-enhancements-4700.md); [per-book CRDT #4781](proofread-per-book-crdt-sync.md); [edit Find + toggle #4859](proofread-edit-toggle-4859.md) +- [Russian NBSP (#4769)](russian-hanging-prepositions-nbsp-4769.md) +- OPDS: [Firefox strict-XML #4479](opds-firefox-strict-xml-4479.md); [JSON search #4502](opds2-json-search-4502.md); [HTML desc #4503](opds-html-description-4503.md); [self-link #4749](opds-self-link-metadata-4749.md); [popular dedup #4782](opds-popular-catalog-dedup-4782.md); [auto-download subdir crawl #4272](opds-autodownload-subdir-crawl-4272.md) bounded BFS, never crawl newest-feed catalogs +- [D-pad Navigation](dpad-navigation.md) +- [koplugin cover upload (#4374)](koplugin-cover-upload.md) +- [Calibre plugin push #4863](calibre-plugin-push-4863.md) OAuth localhost relay +- [Calibre custom columns #4811](calibre-custom-columns-4811.md) `metadata.calibreColumns` ## Library Fixes -- [Book action platform surfaces](book-actions-platform-surfaces.md) — context menu Tauri-desktop-only; cross-platform -- [Tauri menu append race (#4389)](tauri-menu-append-race-4389.md) — single `await Menu.new({ items })` -- TXT: [author recognition #4390](txt-author-recognition-4390.md) `parseLabeledAuthor`+`isPlausibleAuthorName`; [chapter measure-word FP #4658](txt-chapter-measure-word-4658.md) -- [Cover stale (in-place mutation)](cover-stale-inplace-mutation-memo.md) — pure `getBookWithUpdatedMetadata` -- [Series/author back no-op (#4437)](series-folder-back-noop-4437.md) — Next 16.2 empty-search no-op; `handleBack` -- [Library/reader separate texture (#4743)](library-reader-separate-texture-4743.md) — `libraryBackground*` -- [List view series overflow (#4796)](list-view-series-overflow-4796.md) — fixed `h-28` clipped series+description -- [Recently-read shelf (#3797)](recent-read-shelf-3797.md) — PR #4829; reuse `BookItem` + shared `useOpenBook`; flex +- [Book action platform surfaces](book-actions-platform-surfaces.md) · [menu append race #4389](tauri-menu-append-race-4389.md) +- TXT: [author recognition #4390](txt-author-recognition-4390.md); [chapter measure-word FP #4658](txt-chapter-measure-word-4658.md) +- [Cover stale (in-place mutation)](cover-stale-inplace-mutation-memo.md) +- [Series/author back no-op (#4437)](series-folder-back-noop-4437.md) +- [Library/reader separate texture #4743](library-reader-separate-texture-4743.md) · [list view series overflow #4796](list-view-series-overflow-4796.md) +- [Recently-read shelf (#3797)](recent-read-shelf-3797.md) +- [Auto-import watched folders (#3889)](auto-import-watched-folders-3889.md) per-folder opt-in ## Architecture & Patterns - foliate-js submodule at `packages/foliate-js/`; multiview paginator preloads adjacent sections -- [Markdown .md support (#774)](markdown-md-support-774.md) — in-memory foliate book (no EPUB) in `src/utils/md.ts`; -- Style: `getLayoutStyles()` always, `getColorStyles()` when overriding color; `transformStylesheet()` rewrites EPUB CSS at load -- TTS independent section tracking (`#ttsSectionIndex`); safe insets: native plugin → useSafeAreaInsets → styles; Dropdowns `DropdownContext` -- Stale settings closure: store-hook `settings` stale across `await`; persist `useSettingsStore.getState().settings` ([#4780](webdav-connect-nullified-4780.md) -- [Foliate touch-listener capture phase](foliate-touch-listener-capture-phase.md) — suppress gestures via -- [iframe cross-realm instanceof](iframe-cross-realm-instanceof.md) — `instanceof Element` false; duck-type `'closest' -- [Virtuoso + OverlayScrollbars](virtuoso_overlayscrollbars.md) — useOverlayScrollbars for mobile webviews -- [Design system → DESIGN.md](feedback_design_system_doc.md) — never `pl/pr/ml/mr/text-left/right` (RTL) +- [Turso "concurrent use forbidden"](turso-concurrent-use-forbidden.md) `op_lock` async mutex +- Markdown: [.md support #774](markdown-md-support-774.md); [resume position #4862](markdown-resume-position-4862.md) +- Style: `getLayoutStyles()` always, `getColorStyles()` when overriding; `transformStylesheet()` rewrites EPUB CSS +- TTS `#ttsSectionIndex`; insets: native plugin → useSafeAreaInsets → styles; Dropdowns `DropdownContext` +- Stale settings closure: persist `useSettingsStore.getState().settings` ([#4780](webdav-connect-nullified-4780.md)) +- [Page margins not live #4898](page-margin-live-update-4898.md) in-place mutation froze memo +- [Foliate touch-listener capture phase](foliate-touch-listener-capture-phase.md) +- [iframe cross-realm instanceof](iframe-cross-realm-instanceof.md) duck-type `'closest'` +- [Virtuoso + OverlayScrollbars](virtuoso_overlayscrollbars.md) +- [Design system → DESIGN.md](feedback_design_system_doc.md) never `pl/pr/ml/mr` (RTL) ## Workflow & Feedback -- [Commit messages English-only](feedback-commit-message-english-only.md) — English only (no CJK, no em/en dashes) -- [Test file filter](feedback_test_file_filter.md) — `pnpm test ` without `--` runs one file -- [Rebase before PR](feedback_pr_rebase.md) — rebase onto origin/main before PRs -- [New branch per PR](feedback_pr_new_branch.md) — fresh branch from main per PR/issue -- [Use worktree](feedback_use_worktree.md) — never `git worktree add`; always `pnpm worktree:new` -- [Never push every change](feedback_dont_push_every_change.md) — commit locally until user confirms -- [No test seams in prod](feedback_no_test_seams_in_prod.md) — prod never imports `__reset*ForTests` -- [No lookbehind regex](feedback_no_lookbehind_regex.md) — never `(?<=)`/`(?` no `--` +- [No test seams in prod](feedback_no_test_seams_in_prod.md) · [no lookbehind regex](feedback_no_lookbehind_regex.md) +- i18n: [en plurals manual](feedback_en_plurals_manual.md); [i18n:extract prunes keys](i18n-extract-prunes-keys.md) +- [Dependabot transitive fixes](dependabot-pnpm-overrides.md) `pnpm-workspace.yaml` `overrides:` +- [Upgrade gstack locally](feedback_gstack_upgrade.md) diff --git a/apps/readest-app/.claude/memory/android-e2e-doubletap-cdp-gesture.md b/apps/readest-app/.claude/memory/android-e2e-doubletap-cdp-gesture.md new file mode 100644 index 00000000..be8e2649 --- /dev/null +++ b/apps/readest-app/.claude/memory/android-e2e-doubletap-cdp-gesture.md @@ -0,0 +1,56 @@ +--- +name: android-e2e-doubletap-cdp-gesture +description: Nightly Android E2E double-tap test failed since +metadata: + node_type: memory + type: project + originSessionId: eafe11ed-faac-4406-957d-1674353f081c +--- + +Nightly `Android E2E (CDP)` failed every night from 2026-06-28 (first night after +PR #4846 merged) with `timed out waiting for selection of "..." (last: null)` in +`double-click.android.test.ts`. The test never passed on CI. Fixed 2026-07-04 — +harness-only, no app code changed. + +**Two stacked root causes:** + +1. **PRIMARY — feature is opt-in on mobile.** `DEFAULT_MOBILE_VIEW_SETTINGS` + ships `disableDoubleClick: true` (double-click detection delays single-tap + page turns by the 250ms disambiguation window, so mobile opts out). + `handleClick` (iframeEventHandlers.ts) then posts `iframe-single-click` + IMMEDIATELY and never arms the double-click window — the #4846 double-tap + word selection is deliberately "gated by the user's double-click setting" + (Annotator comment). The e2e assumed default config → could never pass on a + fresh device. Diagnostic signature: `iframe-single-click` ~20ms after click + (window disabled) vs ~250ms after (window armed, no second click). +2. **SECONDARY — adb double-tap can't hit the window.** The old helper ran + `input tap x y && input tap x y`; each `input` invocation spawns a fresh + app_process JVM (measured 0.9–1.05s each, 28s cold). Warm fast host ≈130ms + click gap (passes), loaded CI emulator >250ms (always fails). + +**Fix (all in `src/__tests__/android/`):** +- `reader.ts patchGlobalViewSettings(patch)` — force-stop app, read/patch + `settings.json` `globalViewSettings` via `adb shell run-as` (debug builds + only; file lives at the app data dir ROOT, not files/), write back via + base64 pipe, return previous values for restore in afterAll. Missing + settings.json is fine: `loadSettings` deep-merges partial file over defaults. +- `cdp.ts CdpPage.doubleTap(cssX, cssY)` — ONE + `Input.synthesizeTapGesture {tapCount: 2, duration: 20, gestureSourceType: 'touch'}`; + renderer-internal timing gives ~200ms click gap on a busy emulator. TWO + sequential synthesizeTapGesture commands are too slow (~535ms gap — each + resolves long after its gesture). Raw `Input.dispatchTouchEvent` delivers + touch events but does NOT reliably synthesize clicks on Android WebView. +- Word finder requires `range.getClientRects().length === 1`: a + hyphenated/wrapped word's bounding rect spans two lines, so its center taps + between lines and selects the neighboring word (saw `'party' !== 'sensation'`). +- `dismissSelection` picks a mid-column tap spot (0.78H or 0.25H) that the + `.selection-popup` doesn't cover — a blind 0.78H tap can press a toolbar + button when the selection sits low. + +**Other gotchas:** headless emulator display sleeps → adb `input` no-ops while +CDP input still works (`input keyevent KEYCODE_WAKEUP`); a leaked single-click +(broken double-tap) toggles header or opens the media viewer, contaminating the +session; local repro = `pnpm tauri android build --debug --target aarch64` + +`adb install -r` + `pnpm test:android`. + +Related: [[android-cdp-e2e-lane]], [[iframe-double-click-word-select]] diff --git a/apps/readest-app/.claude/memory/calibre-custom-columns-4811.md b/apps/readest-app/.claude/memory/calibre-custom-columns-4811.md new file mode 100644 index 00000000..e761d75d --- /dev/null +++ b/apps/readest-app/.claude/memory/calibre-custom-columns-4811.md @@ -0,0 +1,24 @@ +--- +name: calibre-custom-columns-4811 +description: "Surface Calibre custom columns from OPF user metadata (#4811) - parse formats, calibreColumns field, details UI, library search" +metadata: + node_type: memory + type: project + originSessionId: 7f74fc26-9614-4fe3-987b-66c8ce412523 +--- + +Feature #4811 SHIPPED (app PR #4939 merged 2026-07-05 as `ec45a08`; foliate-js#47 merged as `8485e93`): surface Calibre custom columns embedded in EPUB OPFs. Worktree and branches cleaned up. + +**Calibre OPF encodings (verified against calibre source opf2.py/opf3.py):** +- OPF2: one `` per column; label must start with `#` +- OPF3: a single `{"#label": {...}}` (raw property attr always literally `calibre:user_metadata`; the `calibre:` prefix maps to `https://calibre-ebook.com` but foliate's URL-resolution concatenates without `:` so match the RAW attr, not the resolved one). Calibre prefers OPF3 over OPF2 when both present (`read_user_metadata3 || read_user_metadata2`). +- Value in `#value#` (array for multi-value), series index in `#extra#`; datetimes wrapped `{"__class__": "datetime.datetime", "__value__": ""}`, unset date = `0101-01-01`; embedded files carry EVERY library column so empty values (null/''/[]/rating 0/undefined-date) must be dropped at parse time. + +**Where things live:** +- Parser: `getCalibreUserMetadata` in foliate-js `epub.js`, attached AFTER `tidy()` (tidy would collapse single-element value arrays) as `metadata.calibreColumns` `[{label, name, datatype, value, extra?}]` +- Type: `CalibreCustomColumn` in `src/libs/document.ts`; `BookMetadata.calibreColumns` +- Formatter: `formatCalibreColumnValue` in `src/utils/book.ts` (rating → ★ half-stars /2, series → `Name [idx]`, bool → ✓/✗, comments → strip tags, datetime → formatDate) +- UI: extra grid cells in `BookDetailView.tsx` Metadata section after Identifier (column names are user content, NOT i18n keys) +- Search: `getCalibreColumnsText` in `src/app/library/utils/libraryUtils.ts` `createBookFilter` (both regex and substring branches) + +**Why safe:** metaHash dedupe uses only title/authors/identifiers; metadata editor spreads `{...metadata}` so the field survives edits; import assigns `loadedBook.metadata` as-is. Calibre plugin pushes already embed user metadata via calibre `set_metadata`, so plugin-pushed books get columns through the same OPF parse (the plugin's flat `customColumns` wire field is a DIFFERENT shape and stays unused). E2E-verified on the real sample (Elena Sabe, OPF3, 11 columns → 7 shown, search "CT1" filters). Related: [[calibre-plugin-push-4863]]. diff --git a/apps/readest-app/.claude/memory/calibre-plugin-push-4863.md b/apps/readest-app/.claude/memory/calibre-plugin-push-4863.md index beac2b7f..5a15bbc4 100644 --- a/apps/readest-app/.claude/memory/calibre-plugin-push-4863.md +++ b/apps/readest-app/.claude/memory/calibre-plugin-push-4863.md @@ -7,7 +7,7 @@ metadata: originSessionId: 5d4d83a0-0aee-4200-852f-555df5243bed --- -`apps/readest-calibre-plugin/` (added 2026-07-04, commit 29a42853d on dev) implements #4863: calibre GUI plugin pushing selected books + metadata into the Readest cloud, modeled on BookFusion's plugin. +`apps/readest-calibre-plugin/` implements #4863: calibre GUI plugin pushing selected books + metadata into the Readest cloud, modeled on BookFusion's plugin. MERGED to main via PR #4918 (2026-07-04, merge 6b403d019); packaged in releases as `Readest-.calibre-plugin.zip` by release.yml's `build-calibre-plugin` job. Design decisions and hard-won protocol facts: - **Identity**: `Book.hash` = partial MD5 (KOReader algorithm; JS `1024 << -2` wraps to 0, so offsets are 0, 1024, 4096, ... 1024<<20). metaHash = `md5(NFC("title|authors,|ids,"))`, preferred id scheme uuid > calibre > isbn; Python impl verified byte-identical to `js-md5` output. diff --git a/apps/readest-app/.claude/memory/dict-popup-tts-speak-4876.md b/apps/readest-app/.claude/memory/dict-popup-tts-speak-4876.md new file mode 100644 index 00000000..fc0efcf8 --- /dev/null +++ b/apps/readest-app/.claude/memory/dict-popup-tts-speak-4876.md @@ -0,0 +1,46 @@ +--- +name: dict-popup-tts-speak-4876 +description: "Dictionary popup speaker button pronounces the headword via Edge TTS (#4876), with a standalone pronouncer that bypasses TTSController" +metadata: + node_type: memory + type: project + originSessionId: 98d0ef1c-84c2-4a16-85a0-0abad0010923 +--- + +Issue #4876: add a "speak" button to the dictionary popup so a looked-up word +can be pronounced. Implemented on branch `feat/dict-popup-tts` (commit +f2acafb4b, 2026-07-06). Button-only (no auto-speak); speaker icon sits inline +left of the headword in the shared `DictionaryResultsHeader`, so it covers both +the desktop `DictionaryPopup` and mobile `DictionarySheet`. + +Key file: `src/services/tts/wordPronouncer.ts` — a standalone single-word +pronouncer, deliberately independent of the reader's `TTSController`: +- **Speak ASAP**: never calls `EdgeTTSClient.init()` (which wastes a round trip + synthesizing "test"). Calls `EdgeSpeechTTS.createAudioData()` directly; its + static LRU MP3 cache makes repeat words instant. +- **Dedicated Web Audio context** (`new WebAudioPlayer(() => new AudioContext())`, + NOT the module-shared context the reader uses) so pronouncing a word can never + resume/suspend or overlap an active read-aloud session. One extra AudioContext, + fine under WebKit's ~4 cap. +- **Gesture warmup**: `warmWordAudio()` must be called synchronously in the click + handler (the hook's `speakWord` does this) because `pronounceWord` resumes the + context only after a network await, outside WebKit's autoplay gesture window. +- **Engine order**: Edge wss -> Edge https proxy (`fetchWithAuth`, throws "Not + authenticated" when logged out) -> platform fallback. Fallback reuses the + existing `WebSpeechClient` (desktop/web) / `NativeTTSClient` (mobile app) + standalone via `genSSMLRaw(word)` + `setPrimaryLang(lang)`; the SSML default + `xml:lang="en"` is overridden by `parseSSMLMarks(ssml, primaryLang)`. +- `requestToken` guards staleness so a superseded in-flight synth bails. + +Hook: `useDictionaryResults` gained `isSpeaking` + `speakWord`; cancels on word +change / unmount. Voice pick = `TTSUtils.getPreferredVoice('edge-tts', lang)` +then first `isSameLang` match then `en-US-AriaNeural`. + +Tests: `src/__tests__/services/tts/wordPronouncer.test.ts` (Edge-first / fallback +contract; jsdom has no AudioContext so `getPlayer()` returns null unless +`globalThis.AudioContext` is stubbed + `WebAudioPlayer` mocked). Speak-button +wiring test added to `DictionarySheet.test.tsx` (mocks the pronouncer module). + +NOT verified live: real audio playback + iOS gesture warmup (not unit-testable). +Related: [[edge-tts-webaudio-engine]] (the WebAudio refactor that replaced the old +blob-URL `createAudio` with `createAudioData`), [[ios-instant-dict-double-popup]]. diff --git a/apps/readest-app/.claude/memory/edge-tts-webaudio-engine.md b/apps/readest-app/.claude/memory/edge-tts-webaudio-engine.md new file mode 100644 index 00000000..f0999e6a --- /dev/null +++ b/apps/readest-app/.claude/memory/edge-tts-webaudio-engine.md @@ -0,0 +1,25 @@ +--- +name: edge-tts-webaudio-engine +description: "Edge TTS Web Audio refactor (#3851/#2033) — gapless engine, WSOLA rate, section timeline + scrubber; branch feat/edge-tts-webaudio; release gates and design invariants" +metadata: + node_type: memory + type: project + originSessionId: 97e57af9-5961-4c92-a63e-4582178bf798 +--- + +Branch `feat/edge-tts-webaudio` (worktree `/Users/chrox/dev/readest-feat-edge-tts-webaudio`, built 2026-07-04, NOT pushed) replaces Edge TTS per-sentence `