Files
readest/apps/readest-app/.claude/memory/MEMORY.md
T
2026-06-16 15:52:29 +02:00

26 KiB
Raw Blame History

Readest Project Memory

Key Reference Documents

Paginator Scroll Knowledge

  • Issue #4112 scroll-anchoring — RESOLVED (PR #4349). Scroll-anchoring suppressed at scrollTop 0 when prepending a section in scrolled mode; fix patterns (prepend compensation, eager backward preload, no-blank nav) + test & dev-server gotchas
  • Reading ruler line/column-aware — ruler snaps to real lines; multi-column band spans one column; Range.getClientRects() returns tall block boxes that must be dropped; iframe frame-offset mapping; synthetic-key throttling
  • TOC expand + auto-scroll — #4059 collapse-by-default policy in tocTree.ts; pinned-sidebar mounts before progress → dynamic expansion breaks scroll-to-current via (1) spurious onScroll clearing pending and (2) Virtuoso scrollToIndex landing short after row growth (re-assert on rAF)
  • BooknoteView auto-scroll (#4352) — virtualizing the annotation/bookmark list dropped auto-scroll-to-nearest; two paths (reload: OverlayScrollbars resets scrollTop → re-apply in initialized via ref; tab-switch: synchronous scrollToIndex on fresh-mounted list wedges Virtuoso → use initialTopMostItemIndex + skip-gate). Mirrors TOCView. Includes dev-server/Fast-Refresh/screenshot-vs-DOM verification gotchas
  • TOC current-position row — synthetic "Current position" row (open-book icon + live progress.page) injected one level deeper under the active TOC item via buildTOCDisplayItems in TOCItem.tsx. INVARIANT: insert AFTER the active item so its flatItems index stays valid for the auto-scroll effects
  • Swipe page-turn bg flash — 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 — #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 — #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) — 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) — 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)

  • src/utils/style.ts - Central EPUB CSS transformation hub (14+ bug fixes)
  • packages/foliate-js/paginator.js - Page layout, image sizing, backgrounds
  • src/services/tts/TTSController.ts - TTS state machine, section tracking
  • src/hooks/useSafeAreaInsets.ts - Safe area inset management
  • src/app/reader/components/FoliateViewer.tsx - Reader view orchestration
  • src/app/reader/components/annotator/Annotator.tsx - Annotation lifecycle

Sync Notes

  • KOSync CFI spine resolution — convert via the CFI's own spine (getXPointerFromCFI/getCFIFromXPointer), never new XCFI(primaryDoc, primaryIndex); primaryIndex lags during scroll → spine-mismatch throw
  • Empty-start CFI sync bugepubcfi(/6/24!/4,,/20/1:58) (empty-start range) from the cfi-inert skip-link transitional window; jumps to wrong section end; isMalformedLocationCfi → discard the synced value in useProgressSync (NOT the local open path); foliate fix doesn't repair already-synced values
  • Custom fonts disappear on cloud sync (#4410) — CRDT remove-wins: re-import-after-delete needs a reincarnation token or the pull re-applies the tombstone; addFont/addTexture minted none; fix mirrors dictionary (both cases) + OPDS token style; coverage matrix per kind

Testing

  • Nightly updater Android E2E — real Xiaomi/HyperOS test of #4577 self-updater; pnpm dev-android (--features devtools) for CDP, raw-socket CDP discovery, nightly>stable comparator, MIUI 单次安装授权 install gates
  • Android CDP e2e lanepnpm test:android: adb+CDP drives the installed app on device/emulator; discover-don't-assume targeting, injected hyphenation, MediaStore VIEW transient open (canonical _data path gotcha), per-section frame restore; CI workflow with KVM emulator + debug x86_64 APK (no signing secrets)
  • CDP Android WebView profiling — drive the on-device Readest WebView via adb+CDP to run JS probes/benchmarks inside the live app (no rebuild); gotchas: locked device freezes fetch (not invoke), visible:false throttles setTimeout, __TAURI_INTERNALS__.convertFileSrc/invoke always present, books in internal /data/user/0/..., fs read{rid,len} last-8-bytes=nread, fs|close not ACL-allowed, curl mishandles WebView HTTP framing
  • Tauri Rust↔JS parser parity tests — #4369 native Rust EPUB/MOBI parser; how to cross-check vs foliate-js in the .tauri.test.ts WebView suite (CWD disk path for Rust, Vite URL for JS, normalizer-based compare, cover presence-only, desc whitespace-collapse); the dcterms:modifiedpublished divergence fix
  • TTS browser e2e harness — faithful auto-advance test (real <foliate-view> + real useTTSControl + mock ONLY the 3 client modules; mock speak() yields end to drive the real forward() walk); seed readerStore/bookDataStore + settings.globalViewSettings (else getMergedRules crash stops TTS); reproduce FoliateViewer relocate→setProgress; sample-alice Ch4=section 6/Ch5=section 7; assert badge false BEFORE tts-stop
  • TTS sync chrome verification — Edge TTS WORKS in claude-in-chrome (WebSpeech errors there with InvalidStateError); use an Edge voice to verify TTS-driven features live (RSVP followed at ~171 wpm). Synthetic-CFI debug recipe (expose controller, syncToCfi(view.getCFI(docIndex, word.range))). Exposed the #3235 cross-realm instanceof Range bug (frozen RSVP/paragraph follow) → isRangeLike() duck-type fix
  • TTS sync paragraph+RSVP (#3235, PR #4576) — TTS-is-clock follow: canonical tts-position{cfi,kind:word|sentence,sectionIndex,sequence}; in-mode 🔊 audio toggle (build{Paragraph,Rsvp}TtsSpeakDetail, live-range gate); current word/sentence highlight painted on the overlay CLONE via CSS Custom Highlight API (no DOM mutation, spans inline; offsets relative to para-start map 1:1 to clone, getTextSubRange reuse, index-tagged vs stale); kind-gating decideParagraphTtsHighlight (Edge word wins, skip coarse sentence); ::highlight() from ttsHighlightOptions

Build & Vendoring

  • R2 rclone CreateBucket 403 (#4588) — single-file rclone copyto/moveto probes CreateBucket → 403 on object-scoped R2 token; use a directory rclone copy (or no_check_bucket=true); broke nightly assemble, not the release flow
  • pdfjs vendor wasm decoders — scanned PDFs blank in CI build only (0.11.2 regression); pdfjs 5.7.x moved JBIG2 to jbig2.wasm, copy-pdfjs-wasm allow-list dropped it; cpx no-errors on empty glob; local stale public/vendor (gitignored, not refreshed by tauri build) masked it; fix = copy wasm/*

Platform Compat

  • Android hyphen selection bounds (#1553) — Blink paints the start handle on the paragraph's LAST hyphen when a touch selection starts at the first word of a hyphenated paragraph (ComputePaintingSelectionStateForCursor lacks the generated-text offset remap, hyphen offsets {0,1}); drag-extend re-anchors base there. Fix = repair jumped anchor + suppress handles (empty-commit needs one painted frame) + SelectionRangeEditor custom handles; multicol NOT required; desktop/iOS unaffected
  • Android NativeFile vs RemoteFile I/O — 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) — 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) — "Open with"/"Send to" pipeline: NativeBridgePlugin.kt::handleIntentshared-intentuseAppUrlIngressuseOpenWithBooks (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) — VIVO system-dict lookup opened the browser not Eudic. PRIMARY: no <queries> for ACTION_PROCESS_TEXT under targetSdk36 → dictionary apps invisible, only auto-visible browser returned (fix = add <queries> 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_COMPONENTLookupChoiceReceiver→SharedPreferences (ACTION_CHOOSER has no native Always); reset row in CustomDictionaries.tsx
  • Android sideload same versionCode — sideloaded APK reinstall allows EQUAL versionCode (only strictly-lower blocked); Play Store's increment rule does NOT apply to sideload. Nightly APKs share base versionCode and still install. Corrects a plausible-but-wrong review claim

Feature Notes

  • Tap to open image/table (#4600) — single-tap opens gallery/table-zoom in reflowable EPUBs (long-press unchanged); iframe-long-press message renamed to iframe-open-media, hook useLongPressEventuseOpenMediaEvent; shared detectMediaTarget; handleClick got isFixedLayout
  • #4584 tap-death investigation — UNFIXED; isPopuped self-heals (RED HERRING, don't "fix" it); likely WebView-148-specific (emulator=133 can't repro); Android emulator/CDP gesture-verification gotchas (swiftshader ANR=artifact, CDP can't native-select, screenX=0)
  • Dictionary lemmatization (#4574) — inflected selections (ran/mice/analyses) resolve to base headwords (run/mouse/analysis) in dicts that store only lemmas (ODE). Pluggable lemmatize/ registry (default English, explicit non-English no-op), English rules+irregulars, appended to tail of buildLookupCandidates so exact match wins; over-generate + dict-validates; -ses→-sis ordered before -es
  • Word Wise inline gloss (feat/word-wise) — Kindle-style native-language hint above hard words; CFI-safe via <ruby cfi-skip>…<rt cfi-inert> (epubcfi hoist+merge, NOT just tree-walk); TTS/search isolation (tags:['rt'] + rangeTextExcludingInert + search attributes:['cfi-inert']); gloss data = curated starters, full asset built by build-wordwise-data.mjs (ECDICT/CC-CEDICT+HSK)
  • Edge TTS word highlighting (#4017, PR #4566) — 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 = <path> in FOLIATE-PAGINATOR shadow root; dev-web verify recipe (browse --proxy + UA spoof, never Origin header)
  • Reference Pages (#672+#4542, PR #4549) — '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) — MEK feed has junk after </feed>; Firefox DOMParser → <parsererror> (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)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
  • OPDS HTML description (#4503) — detail-view descriptions showed raw <p>/&quot; tags; aggregator double-escapes type="text" summary + PublicationView dumped it into unsanitized dangerouslySetInnerHTML; fix = getOPDSDescriptionHtml (decode-one-level-iff-fully-escaped, then sanitizeHtml)
  • Manage Cache + iOS container layout'Cache' base = Library/Caches/<bundle> only (not all of Caches); iOS Documents/Inbox cleared too; WebKit cache + tmp out of reach; never touch App Support
  • D-pad Navigation — Android TV remote / keyboard arrow navigation design, key files, and pitfalls
  • Cloudflare Workers WebSocket — use fetch() Upgrade pattern (not ws npm); CF delivers binary frames as Blob (must serialize async decodes)
  • Share-a-Book Feature (in progress) — locked decisions for the /s/{token} share-link feature; plan at ~/.claude/plans/ok-we-will-learn-cosmic-acorn.md
  • readest.koplugin i18n — gettext loader at apps/readest.koplugin/i18n.lua, .po catalog at locales/<i18next-code>/translation.po, extract/apply scripts in scripts/
  • koplugin cover upload — #4374 uploadBook only shipped cached cloud covers; local-origin books uploaded blank. Fix = extractLocalCover via FileManagerBookInfo:getCoverImage(nil, file)writeToFile(path,"png"). KOReader checkout at /Users/chrox/dev/koreader

Patterns

  • Virtuoso + OverlayScrollbars — useOverlayScrollbars hook integration for overlay scrollbars on mobile webviews
  • Design system → DESIGN.md — codify recurring UI/UX rules in apps/readest-app/DESIGN.md; never pl/pr/ml/mr/text-left/text-right (RTL); §5 boxed list anatomy has uniform min-h-14 rows and chromeless controls

Reader UI Fixes

  • Global annotation page-turn lag (#4575) — highlighting recurring names = global highlights re-fanned-out (TreeWalker + getCFI/occurrence + SVG churn) EVERY page turn (~25-45ms desktop, ×mobile); fix = WeakMap<Document,...> memo in globalAnnotations.ts skips already-expanded sections; live-profiled via dev-web foliate-view; GBK-TXT synthetic-drop import recipe
  • Overlayer splitRange by text nodes — highlight SVG missed bullet-list text when range also touched a <p>: #splitRangeByParagraph's 'p,h1-h4' selector dropped li (3rd whack-a-mole after f087826/920676b); fix = walk text nodes + img,svg in overlayer.js, never block-tag selectors; jsdom test stubs Range.prototype.getClientRects
  • Android image callout freeze — long-press <img> fires WebView native callout that collides with app touch handlers → whole-app freeze; -webkit-touch-callout: none doesn't inherit so put .no-context-menu on an ancestor of the image (.no-context-menu img rule in globals.css); seen on book covers (#4345) + image preview/zoom (#4420, ImageViewer.tsx)
  • ProgressBar focus-ring line (#4397) — 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)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) — 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 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) — 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) — 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) — 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 absolutefixed: 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) — 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) — 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
  • TXT author recognition (#4390) — 【】-titled Chinese web-novels show author missing/garbage; they're TXT→EPUB (title==full filename is the tell, check txt.ts not foliate-js); extractTxtFilenameMetadata only handled 《》 + greedy header capture grabbed metadata blobs; fix = parseLabeledAuthor for any filename + isPlausibleAuthorName guard
  • Cover stale until refresh (in-place mutation vs React.memo) — editing a book cover in details + Save left the library cover stale until reload; handleUpdateMetadata mutated book IN PLACE so memoized <BookCover>'s prev snapshot pointed at the same object → comparator saw no change → skip; fix = pure getBookWithUpdatedMetadata returns a NEW book object. Cloning in updateBook wouldn't help (original already mutated). Verified live on emulator via CDP fiber-store extraction (A: mutate→stale, B: new obj→updates)

Library Architecture

  • Book action platform surfaces — library context menu is Tauri-desktop-only (hasContextMenu false on web + iOS/Android); cross-platform book actions go in BookDetailView's icon row. #4543 Goodreads search added both surfaces + a built-in web-search provider for highlighted-text lookup

Architecture Notes

  • foliate-js is a git submodule at packages/foliate-js/
  • Multiview paginator: loads adjacent sections in background, multiple View/Overlayer instances per book
  • Style overrides: getLayoutStyles() (always), getColorStyles() (when overriding color)
  • transformStylesheet() does regex-based EPUB CSS rewriting at load time
  • TTS uses independent section tracking (#ttsSectionIndex) decoupled from view
  • Safe area insets flow: Native plugin -> useSafeAreaInsets hook -> component styles
  • Dropdown menus use DropdownContext (not blur-based) for screen reader compat
  • Foliate touch-listener capture phase — to suppress reader gestures from the app, use {capture:true}; the paginator registers bubble-phase doc listeners first (during view.open())
  • iframe cross-realm instanceof — app-bundle code (style.ts, iframeEventHandlers.ts) runs in top realm; iframeEl instanceof Element is ALWAYS false → guards silently drop all iframe elements (passes jsdom, dead in app). Duck-type 'closest' in target instead. Bit PR #4391's touch routing + applyTableStyle dedupe

Workflow

  • Test file filter — use pnpm test <path> without -- to run a single file
  • Always rebase before PR — rebase onto origin/main before creating PRs
  • New branch per PR — always create a fresh branch from main for each new PR/issue
  • Upgrade gstack locally — always upgrade from the project's .claude/skills/gstack, not global
  • No lookbehind regex — never use (?<=) or (?<!) in JS/TS; build check rejects them
  • Use worktree — never git worktree add directly; always pnpm worktree:new before PR review, issue fix, or feature work
  • en/translation.json holds ONLY plural variants + proper nouns — non-plural strings stay out (defaultValue: key is the en source); plural strings (_('...', { count })) need hand-added _one/_other entries or the singular renders as "1 days"
  • Never push on every change — hold pushes during active bug iteration; commit locally only until user confirms or work hits a clean done-state
  • No test seams in production code — production must never import or call __reset*ForTests; cross-module test resets belong in the test file's beforeEach/afterEach
  • Dependabot transitive fixes — pin patched min-version in pnpm-workspace.yaml overrides: (NOT package.json pnpm.overrides, which pnpm 9+ ignores); watch for existing too-low pins; alert#≠issue# so no Closes # (PR #4523)
  • CI/PR delivery + push keepalive — package small PRs from a dirty dev tree via temp-index plumbing (no worktree); slow pre-push hook (~55s full suite) + SOCKS-proxy SSH → idle "Broken pipe", fixed with ServerAliveInterval; --no-verify safe once the hook already passed (always git ls-remote to confirm a push landed)