26 KiB
26 KiB
Readest Project Memory
Key Reference Documents
- Bug Fixing Patterns - Common bug categories, root causes, and fix strategies
- CSS & Style Fixes - EPUB CSS override patterns and the style.ts pipeline
- TTS Fixes - Text-to-Speech architecture and bug patterns
- Layout & UI Fixes - Safe insets, z-index, platform-specific UI issues
- Platform Compat Fixes - Android, iOS, Linux, macOS platform-specific bugs
- Annotator & Reader Fixes - Highlight, selection, accessibility bugs
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
initializedvia ref; tab-switch: synchronous scrollToIndex on fresh-mounted list wedges Virtuoso → useinitialTopMostItemIndex+ 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 viabuildTOCDisplayItemsinTOCItem.tsx. INVARIANT: insert AFTER the active item so itsflatItemsindex stays valid for the auto-scroll effects - Swipe page-turn bg flash — white↔black flash on swipe+animation only;
#backgroundwas 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-fullscreencover pinnedposition:absolute height:100%collapses against auto-height scroll container; gate fullscreen onthis.#column+ reset stale absolute props on toggle (setImageSizein paginator.js) - Paginated texture occlusion — #4399 host
.foliate-viewer::beforetexture absent in paginated (shown in scrolled); opaque#backgroundcontainer (= fallbackBg) from the swipe-flash fix occludes it; sharedtextureAwareBackgroundhelper +hasTexture ? '' : fallbackBgcontainer - 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 + poisoneddocBackgroundcapture → opaque segments/view bgs; fix =transparent !importantUNCONDITIONALLY (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-mingutter → mixed cover/title 2-up spread shifted off-centre (~250px at 1920px). KEEP the grid (--_outer-minkeeps margins symmetric); fix = clampcomputeBackgroundSegmentsto[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, backgroundssrc/services/tts/TTSController.ts- TTS state machine, section trackingsrc/hooks/useSafeAreaInsets.ts- Safe area inset managementsrc/app/reader/components/FoliateViewer.tsx- Reader view orchestrationsrc/app/reader/components/annotator/Annotator.tsx- Annotation lifecycle
Sync Notes
- KOSync CFI spine resolution — convert via the CFI's own spine (
getXPointerFromCFI/getCFIFromXPointer), nevernew XCFI(primaryDoc, primaryIndex); primaryIndex lags during scroll → spine-mismatch throw - Empty-start CFI sync bug —
epubcfi(/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 inuseProgressSync(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
reincarnationtoken or the pull re-applies the tombstone;addFont/addTextureminted 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 lane —
pnpm test:android: adb+CDP drives the installed app on device/emulator; discover-don't-assume targeting, injected hyphenation, MediaStore VIEW transient open (canonical_datapath 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/invokealways present, books in internal/data/user/0/..., fsread{rid,len}last-8-bytes=nread,fs|closenot 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.tsWebView suite (CWD disk path for Rust, Vite URL for JS, normalizer-based compare, cover presence-only, desc whitespace-collapse); thedcterms:modified→publisheddivergence fix - TTS browser e2e harness — faithful auto-advance test (real
<foliate-view>+ realuseTTSControl+ mock ONLY the 3 client modules; mockspeak()yieldsendto drive the realforward()walk); seed readerStore/bookDataStore +settings.globalViewSettings(elsegetMergedRulescrash stops TTS); reproduce FoliateViewer relocate→setProgress; sample-alice Ch4=section 6/Ch5=section 7; assert badgefalseBEFORE 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-realminstanceof Rangebug (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,getTextSubRangereuse, index-tagged vs stale); kind-gatingdecideParagraphTtsHighlight(Edge word wins, skip coarse sentence);::highlight()fromttsHighlightOptions
Build & Vendoring
- R2 rclone CreateBucket 403 (#4588) — single-file
rclone copyto/movetoprobes CreateBucket → 403 on object-scoped R2 token; use a directoryrclone copy(orno_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-wasmallow-list dropped it;cpxno-errors on empty glob; local stalepublic/vendor(gitignored, not refreshed bytauri build) masked it; fix = copywasm/*
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 (
ComputePaintingSelectionStateForCursorlacks 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) +SelectionRangeEditorcustom 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(-32000minimized sentinel /0×0); our plugin already has upstream #253 fix so bad files are stale; defense-in-depthwindow-state-sanitizerplugin 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::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) — VIVO system-dict lookup opened the browser not Eudic. PRIMARY: no
<queries>forACTION_PROCESS_TEXTunder 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 puredecideLookupDispatch(explicit/chooser/unavailable). Remember-the-pick viaIntentSender+EXTRA_CHOSEN_COMPONENT→LookupChoiceReceiver→SharedPreferences (ACTION_CHOOSERhas no native Always); reset row inCustomDictionaries.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-pressmessage renamed toiframe-open-media, hookuseLongPressEvent→useOpenMediaEvent; shareddetectMediaTarget;handleClickgotisFixedLayout - #4584 tap-death investigation — UNFIXED;
isPopupedself-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). Pluggablelemmatize/registry (default English, explicit non-English no-op), English rules+irregulars, appended to tail ofbuildLookupCandidatesso exact match wins; over-generate + dict-validates;-ses→-sisordered 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 bybuild-wordwise-data.mjs(ECDICT/CC-CEDICT+HSK) - Edge TTS word highlighting (#4017, PR #4566) — keep sentence marks, add word highlight via
audio.metadataWordBoundary (verbatim input span, 100-ns ticks) synced toaudio.currentTimeby rAF; readaloud endpoint gates on UA (Edg, non-headless) NOT Origin; fixed browsernew 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-bookreferencePageCountvia 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;parseOPDSXMLslices root start→last close tag; jsdom mirrors Firefox; wired into page.tsx + validateOPDSURL + feedChecker (latter also #4181looksLikeXMLContentswap) - OPDS 2.0 JSON search greyed out (#4502) —
isSearchLinkignored templatedapplication/opds+jsonlinks →hasSearchfalse → disabled navbar input; addMIME.OPDS2+templated,expandOPDSSearchTemplate(foliateuri-template.js), handleSearch OPDS2 branch. Gotcha:resolveURLmangles{?query}braces — expand template BEFORE resolving - OPDS HTML description (#4503) — detail-view descriptions showed raw
<p>/"tags; aggregator double-escapestype="text"summary +PublicationViewdumped it into unsanitizeddangerouslySetInnerHTML; fix =getOPDSDescriptionHtml(decode-one-level-iff-fully-escaped, thensanitizeHtml) - Manage Cache + iOS container layout —
'Cache'base =Library/Caches/<bundle>only (not all of Caches); iOSDocuments/Inboxcleared 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
wsnpm); 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,.pocatalog atlocales/<i18next-code>/translation.po, extract/apply scripts inscripts/ - koplugin cover upload — #4374 uploadBook only shipped cached cloud covers; local-origin books uploaded blank. Fix =
extractLocalCoverviaFileManagerBookInfo: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; neverpl/pr/ml/mr/text-left/text-right(RTL); §5 boxed list anatomy has uniformmin-h-14rows and chromeless controls
Reader UI Fixes
- Global annotation page-turn lag (#4575) — highlighting recurring names =
globalhighlights re-fanned-out (TreeWalker + getCFI/occurrence + SVG churn) EVERY page turn (~25-45ms desktop, ×mobile); fix =WeakMap<Document,...>memo inglobalAnnotations.tsskips 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 droppedli(3rd whack-a-mole after f087826/920676b); fix = walk text nodes +img,svgin overlayer.js, never block-tag selectors; jsdom test stubsRange.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: nonedoesn't inherit so put.no-context-menuon an ancestor of the image (.no-context-menu imgrule in globals.css); seen on book covers (#4345) + image preview/zoom (#4420,ImageViewer.tsx) - ProgressBar focus-ring line (#4397) — decorative
.progressinfofooter wastabIndex={-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-visibleconfirmation - Table dark-mode tint regression (#4419) —
blockquote, table *color-mix tint ingetColorStylesmust stay gated onoverrideColor(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 =
isMouseDownflag iniframeEventHandlers.tsgates the deferredpostSingleClick; 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 viagetBaseFontFamily(viewSettings)(new export instyle.ts, sharesbuildFontFamilyListswithgetFontStyles). 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 whenisCJKEnv() - Footnote aside border line (#4438) — v0.11.4 regression: stray horizontal line below footnote marker. #4383 inlined custom
@font-faceBEFORE the@namespace epub(which lived ingetPageLayoutStyles), invalidating it per CSS spec → namespacedaside[epub|type~="footnote"]{display:none}dropped → book'saside{border:3px double}showed. Only with custom fonts loaded. Fix = hoist@namespaceto front ofgetStyles. Repro needs XHTML (epub:typenamespaced only in XML); PlaywrightsetContentparses 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);
ParagraphBaronly reshows on mousemove (no touch reshow) so can't just delete tap-exits → newparagraph-show-controlsevent reveals the bar instead. Also barabsolute→fixed: it centered on the gridcell which a pinned sidebar shifts right, while the paragraph centers on thefixed inset-0overlay/viewport - Share intent + customizable toolbar (#4014) — Share tool in the selection toolbar (sharekit gated mobile+macOS only re: #4343 Windows freeze;
canShareText/shareSelectedTextin dual-purposeshare.ts) + drag-and-drop customizer sub-page;annotationToolbarItemsview setting (Share hidden by default); pure helpers inannotationToolbar.ts
Library Fixes
- Tauri menu append race (#4389) — un-awaited
Menu.append()(async IPC) inBookshelfItem.tsx→ context-menu items shuffle order every open (native only, invisible in jsdom); fix = singleawait Menu.new({ items })of orderedMenuItemOptions; order/inclusion extracted to puregetBookContextMenuItemIdsfor 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.tsnot foliate-js);extractTxtFilenameMetadataonly handled 《》 + greedy header capture grabbed metadata blobs; fix =parseLabeledAuthorfor any filename +isPlausibleAuthorNameguard - Cover stale until refresh (in-place mutation vs React.memo) — editing a book cover in details + Save left the library cover stale until reload;
handleUpdateMetadatamutatedbookIN PLACE so memoized<BookCover>'s prev snapshot pointed at the same object → comparator saw no change → skip; fix = puregetBookWithUpdatedMetadatareturns a NEW book object. Cloning inupdateBookwouldn'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 (
hasContextMenufalse on web + iOS/Android); cross-platform book actions go inBookDetailView'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 (duringview.open()) - iframe cross-realm instanceof — app-bundle code (style.ts, iframeEventHandlers.ts) runs in top realm;
iframeEl instanceof Elementis ALWAYS false → guards silently drop all iframe elements (passes jsdom, dead in app). Duck-type'closest' in targetinstead. 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 adddirectly; alwayspnpm worktree:newbefore 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/_otherentries 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.yamloverrides:(NOT package.jsonpnpm.overrides, which pnpm 9+ ignores); watch for existing too-low pins; alert#≠issue# so noCloses #(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-verifysafe once the hook already passed (alwaysgit ls-remoteto confirm a push landed)