Teleprompter-style continuous scrolling toggled from the View menu
(Shift+A), available only in scrolled mode. A PacedScroller drives
whole-pixel forward steps at a constant, user-adjustable velocity;
the speed (25-500 percent, persisted as autoScrollSpeed) is tuned
from a floating control pill that also offers pause/resume and exit,
and fades away while scrolling to keep the mode immersive.
Tapping the page pauses and resumes instead of turning pages or
toggling the bars; manual wheel or drag input simply composes with
the paced scrolling. Escape or leaving scrolled mode ends the
session. When forward progress stalls the session hops to the next
section (single-section scroll mode) or stops with a toast at the
end of the book. Vertical-writing books scroll along the horizontal
axis with the sign convention foliate uses for scrolled offsets.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an always-visible, opt-in progress bar with chapter tick marks in the
persistent footer, so reading progress no longer disappears like the hover
footer slider does.
- New StickyProgressBar: a 1px rounded-border capsule with a fill and
chapter tick marks; display-only, e-ink aware, and RTL safe. Ticks render
inside the clipped track so the rounded ends crop them and they never
exceed the border.
- Chapter ticks come from the TOC, mapped to spine-section start fractions
(getChapterTickFractions); the first and last ticks are trimmed so they
do not crowd the rounded ends.
- Thread the overall size-domain reading fraction through setProgress so the
bar fill aligns with the tick domain.
- Footer layout: when enabled the bar grows on the left and the info widgets
group to the right with even spacing; otherwise the existing layout is
unchanged.
- Horizontal writing mode only; vertical keeps the current footer.
- Add the showStickyProgressBar view setting, a LayoutPanel toggle, and i18n.
Closes#1616.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setProgress was called multiple times per swipe burst, each call writing
into readerStore.viewStates[key].progress. ~65 places in the reader
subtree subscribed to useReaderStore() without a selector, so every
setProgress fan-out re-rendered all of them -- even the 51 that didn't
care about progress. On Android release builds this showed up as
Layout = 9.8% and Function Call = 9.6% of main-thread self time in
Chrome DevTools' Bottom-Up profile during a reading session.
Fix:
- New tiny store store/readerProgressStore.ts holds the per-book
BookProgress map. setBookProgress only fires its own subscribers.
- readerStore.setProgress now writes progress to the new store and only
touches bookDataStore for the primary view (secondary parallel views
shouldn't overwrite the shared config).
- readerStore.getProgress is kept as a delegating facade so existing
imperative call sites don't break.
- Components / hooks that genuinely need to react to progress changes
subscribe via the new useBookProgress(bookKey) hook. The handful of
call sites that just want a one-shot read use getBookProgress(key) so
they don't subscribe at all.
- readerStore.clearViewState calls clearBookProgress so the map doesn't
grow unbounded across book opens/closes.
See store/readerProgressStore.ts header for the full rationale.
Add a 'Reference Pages' reading progress style that shows physical book
page numbers in the footer progress info:
- When the book carries a page list (EPUB3 nav page-list or EPUB2 NCX
pageList — foliate-js already parses both and resolves the current
pageItem on relocate; it was just never consumed), display the current
page label and use the highest numeric label as the total, so a
trailing roman-numeral index page can't corrupt the total (#672).
- When the book has none, a per-book 'Reference Page Count' input
appears; the reading fraction is mapped linearly onto the entered
count (#4542). The count is saved per book only and never propagates
to global view settings.
- Falls back to percentage display when neither source is available.
Verified with the sample books from #672: Caleb's Crossing (EPUB3
page-list, 419 pages) and Count Zero (EPUB2 NCX pageList/page-map,
346 pages — chapter 2 lands exactly on page 22 per its page-map), plus
a stripped no-pagelist copy for the manual-count path (175/350 at 50%).
Closes#672Closes#4542
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* perf(epub): add native EPUB parser in Rust
Introduce a Rust-side EPUB pre-parser exposing three Tauri commands:
* parse_epub_metadata - title/author/cover + partialMD5 in one
shot, for the import hot path
* parse_epub_full - OPF + nav.xhtml + toc.ncx bytes plus a
manifest size table, for the reader open
hot path
* extract_epub_cover_full - full-resolution cover bytes, for the
lock-screen wallpaper writer
All three avoid ferrying multi-MB blobs across the JS<->Rust IPC
boundary. Cover bytes returned by parse_epub_metadata are downscaled
to a webview-friendly JPEG when the long edge exceeds the library
thumbnail size.
No JS callers yet -- wired up in the following commits.
* perf(import): use native EPUB parser and downscale covers on Tauri targets
On Tauri (desktop/iOS/Android), importBook now forwards EPUB
metadata + cover extraction to the Rust parse_epub_metadata
command and reuses the partialMD5 it returns, skipping the
foliate-js full archive parse and the second pass over the file
for hashing.
As a side effect, the cover written to cover.png is downscaled
to a webview-friendly JPEG (long edge <= 512px), shrinking the
on-disk thumbnail from multi-MB to ~30-60KB per book. To keep
the lock-screen wallpaper feature unchanged, useAutoSaveBookCover
now pulls the original full-resolution cover via the Rust
extract_epub_cover_full command instead of copying the (now
downscaled) cover.png; falls back to the thumbnail when the
native path is unavailable.
Web targets and non-EPUB formats keep the existing path.
* perf(reader): prefetch EPUB OPF/nav from Rust on book open
When opening an EPUB on Tauri targets, DocumentLoader now calls the
Rust parse_epub_full command up-front to pull the OPF, EPUB3 nav,
NCX and the central-directory size map in a single IPC. The
foliate-js zip loader is wrapped so that loadText() of these
entries (and a synthetic META-INF/container.xml) is served from
that in-memory cache without inflating through zip.js, while
all other assets keep flowing through the original loader.
A small in-flight dedupe is added to the spine-text loader so the
nav pipeline (loadText + createDocument back-to-back on the same
href) doesn't pay for two zip.js inflate calls per chapter on
first open.
Reader store / app service plumbing: readerStore.openBook now
resolves an absolute on-disk path via the new
appService.resolveNativeBookFilePath / bookService.resolveNativeBookFilePath
helper and threads it into DocumentLoader as nativeFilePath so
the prefetch can fire. Web targets, non-EPUB formats and books
without a managed/external on-disk path skip the prefetch and
take the original code path.
* perf(nav): parallelize section scans and memoize fragment lookups
computeBookNav now processes sections via Promise.all instead of
a sequential for-loop, and within each section issues loadText()
and createDocument() concurrently. Combined with the in-flight
loadText dedupe added to the zip loader, each chapter pays for a
single zip inflate per nav build, and the inflates of different
chapters overlap.
enrichTocFromNavElements is restructured into two concurrent
phases: a cheap '<nav' substring filter on the inflated text, and
a parsed-document walk for the survivors. Most chapters fall out
in phase 1 without ever being parsed.
In fragments.ts, calculateFragmentSize now consults a
per-section position cache (makeFragmentPositionCache) so the
N-fragment loop is O(N) over the chapter HTML instead of O(N²).
A small isCfiAddressable guard is added to skip elements that
foliate-js's CFI generator can't address (documentElement, body
itself, detached nodes, nodes outside <body>) — these previously
threw and spammed console.warn for every fragment, now they
silently fall back to the section CFI.
* perf(import): use native MOBI/AZW/AZW3 parser on Tauri targets
On Tauri (desktop/iOS/Android), importBook now forwards
MOBI/AZW/AZW3/PRC metadata + cover extraction to the Rust
parse_mobi_metadata command and reuses the partialMD5 it returns,
skipping the foliate-js full-buffer parse and the second pass over
the file for hashing. Mirrors the existing EPUB native fast-path
added in e3fc4767 — bookService tries EPUB first, then MOBI; both
bridges fall back to the foliate-js DocumentLoader when the native
path is unavailable (web target, parse error, format mismatch).
The new mobi_parser is built on the mobi crate (KF7+KF8 reader,
zero JS-side touch). It reads title, author, publisher, ISBN, ASIN,
publish date, language, subjects and description from the MobiHeader
+ EXTH records, resolves the EXTH 201 cover offset against the PDB
image-record table (with ThumbOffset / first-image fallbacks), and
strips KindleGen's HTML wrapping in EXTH 103 so the description goes
into the library DB as plain text. The parsed cover is funneled
through the same maybe_resize_cover path as EPUB, so MOBI library
thumbnails are also clamped to a 512px-long-edge JPEG.
Cover-resize / partialMD5 / RawCoverImage are extracted into a new
parser_common module shared between epub_parser and mobi_parser, so
a single tweak (e.g. raising the thumbnail target) applies to every
native importer and the partialMD5 implementation can't drift between
the two paths (a divergent algorithm would silently re-import every
existing book under a new hash on the first run).
Web targets and non-Kindle formats keep the existing path.
* test(tauri): verify native Rust EPUB parser parity with foliate-js
Add a Tauri WebView parity suite (epub-parser-parity.tauri.test.ts) that
cross-checks the native Rust parser against foliate-js on the same fixtures:
parse_epub_metadata / parse_epub_full (title, author, language, identifier,
publisher, published, subjects, partialMD5, OPF + per-entry size table), and
that opening with the native prefetch produces the same BookDoc and
computeBookNav (TOC) output as the pure-JS path.
Fix a parity divergence the suite caught: the Rust OPF parser mapped
dcterms:modified onto `published`, but foliate-js keeps them separate and
leaves `published` empty -- so EPUB3 books carrying only the mandatory
dcterms:modified got a bogus publication date on the native import path. Map
only dc:date now; add regression tests.
Test infra:
- vitest.tauri.config.mts: add optimizeDeps (mirroring vitest.browser.config)
so foliate-js-importing tauri tests load -- otherwise esbuild's dep scan
can't resolve '@pdfjs/pdf.min.mjs', pre-bundling is skipped, and the CJS
deps fail to import ("Importing a module script failed").
- capabilities-extra/webdriver.json: fix __test__ -> __tests__ fs scope typo
so import tests can open fixtures under src/__tests__/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* refactor(import): foliate-js owns EPUB/MOBI metadata via standalone extractors
Rust contributes only the mechanical work that's expensive on a
WebView — partialMD5, the downscaled cover, and (for EPUB) the raw
OPF bytes Rust already had to read for cover resolution. Metadata
extraction is delegated to foliate-js's two new standalone entry
points (`parseEpubMetadataFromXML`, `readMobiMetadata`) so the
import-path BookDoc and the reader-path BookDoc share a single
parser implementation.
EPUB
- `parse_epub_metadata` returns
`{ partialMd5, cover, coverMime, opfPath, opfBytes }`. OPF bytes
are a free byproduct of the cover-resolution scan.
- `tryNativeParseEpub` runs `parseEpubMetadataFromXML` on the OPF
bytes and assembles a lightweight BookDoc stub (metadata +
getCover). The importer doesn't drive `DocumentLoader.open()`, so
no zip central-directory scan, no nav/ncx inflate, no spine walk.
- `coverMime` is preserved so `bookService.importBook`'s
`cover.type === 'image/svg+xml'` branch still routes SVG covers
through svg2png.
MOBI / AZW / AZW3 / PRC
- `parse_mobi_metadata` returns `{ partialMd5, cover, coverMime }`.
`tryNativeParseMobi` runs foliate's `readMobiMetadata` on the
same File, which uses `MOBI.open(file, { metadataOnly: true })`
to parse PalmDB + MobiHeader + EXTH and short-circuit before the
MOBI6 / KF8 init() that walks every text record.
- `Book.metadata.identifier` is foliate's `mobi.uid.toString()`
(PalmDB UID), the canonical MOBI identifier the reader path uses.
bookService.importBook
- EPUB and MOBI native branches consume the bridge's BookDoc stub
directly. The stub's `getCover()` returns the Rust-downscaled
blob, falling back to foliate's own `getCover` thunk when Rust
didn't extract a cover.
Other
- Drop the unused `base64` Rust dependency: cover bytes go over IPC
as `Vec<u8>` (Tauri 2 transports them natively, like opfBytes /
navBytes / ncxBytes).
- Drop the `nativePrefetch` option on `DocumentLoaderOptions`; no
caller passes it. `nativeFilePath` keeps driving `parse_epub_full`
on the open hot path.
Tests
- vitest.tauri parity test asserts byte-equal partialMD5, cover
presence parity, OPF bytes that decode to a real `<package>`
document, and that `parseEpubMetadataFromXML` on those bytes
produces the same user-visible metadata fields (title / author /
language / identifier / published) as `DocumentLoader.open()`.
* test(tauri): add War and Peace MOBI fixture for native parser parity
The .tauri parser-parity suite previously had no .mobi/.azw3 asset, so the native MOBI parser (metadata + EXTH cover resolution) was uncovered. Adds a real KF8 MOBI ("War and Peace") to enable MOBI parity coverage against foliate-js.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(foliate-js): bump submodule to readest/foliate-js main (91191ca)
Replaces the ad-hoc 02f435a with the merged main commit 91191ca, which lands the standalone OPF/MOBI metadata extractors (parseEpubMetadataFromXML, readMobiMetadata) the import fast-path depends on (foliate#19), plus the RTL multi-view rect-mapper fix (foliate#20). The extractor code is byte-identical to 02f435a, so the bridges are unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(cbz,i18n): ComicInfo metadata + CBZ page count + WebDAV i18n
Closes#4253 (ComicInfo.xml not read) and #4255 (CBZ shows "1 page left").
CBZ / ComicInfo (foliate-js submodule + Readest derivation):
- comic-book.js: find ComicInfo.xml in subdirectories too, parse
description / subject / identifier / published / series fields
beyond the prior name+position pair. Series Count populates the
canonical `belongsTo.series.total`; no top-level duplication.
- bookService.ts / readerStore.ts: derive `metadata.seriesTotal`
from `belongsTo.series.total` in parallel to the existing
series / seriesIndex derivation.
- ProgressBar / FooterBar / DesktopFooterBar: drop the hard-coded
`pagesLeft = 1` for fixed-layout books and compute it from
`section.total - section.current`. FooterBar uses
`FIXED_LAYOUT_FORMATS.has(bookFormat)` so CBZ picks `section`
(correct image count) instead of `pageinfo` (locations).
- ProgressBar: switch the remaining-pages text to "in book" for
fixed-layout titles (no chapter structure) and keep
"in chapter" for reflowable books.
WebDAV refactor for translation coverage:
- WebDAVBrowsePane / SyncHistoryPanel called `t(...)` (passed as a
prop) instead of `_(...)`. The i18next-scanner only looks for
`_`, so ~53 strings were unreachable and shipped in English to
every locale. Switched both components to call
`useTranslation()` themselves; helpers that aren't React FCs
take `_: TranslationFunc` so the scanner sees the literal calls.
- WebDAVClient.checkConnection now returns a `code` discriminator
(`SERVER_URL_REQUIRED` / `AUTH_FAILED` / `ROOT_NOT_FOUND` /
`UNEXPECTED_STATUS` / `NETWORK`); raw English `message` is
reserved for the dev console. New `formatConnectError` and
`formatSyncError` helpers in WebDAVForm translate via a switch
where each branch is a literal `_('...')`. Same treatment for
the sync-failure path that previously surfaced raw e.message.
- "Syncing 0 / {{total}}" is now parameterized as
"Syncing {{n}} / {{total}}" with n=0 at startup so the digit
formats naturally and the template can be reused mid-sync.
- "Cleanup · {{count}} book(s)" hard-coded options used unsupported
ternary; rewrote as plural-aware key.
i18n scanner fix (i18next-scanner.config.cjs):
- vinyl-fs walked into directories whose names end in source-file
extensions (Next.js route folder `runtime-config.js/`, Playwright
screenshot folder `*.test.tsx/`) and crashed with EISDIR.
Resolved by expanding globs via `fs.globSync` and filtering to
files only before handing to the scanner.
TypeScript-syntax sites that broke esprima during extraction:
- WebDAVBrowsePane / WebDAVForm: `(e as Error).message` and
`failed[0]!.title` inside `_(..., options)` arguments. Replaced
with `e instanceof Error ? e.message : String(e)` and
`failed[0]?.title ?? ''` — also runtime-safer.
User-facing em-dash cleanup:
- Removed em-dashes from translation keys across SyncHistoryPanel /
WebDAVForm / WebDAVBrowsePane / SyncPassphraseSection / send/page /
replicaCryptoMiddleware / AIPanel. Tagline in `layout.tsx` kept.
Locale translations:
- ~2400 translations applied across all 33 locales for the keys
that were either newly extractable, freshly worded, or
pre-existing but untranslated. Zero `__STRING_NOT_TRANSLATED__`
remain after the run.
Misc:
- next.config.mjs: drop `eslint.ignoreDuringBuilds: true` so build
runs the same lint as CI.
- Collection type: add `total?: string` for ComicInfo series count.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci(test): fix vitest invocation, run with 4 workers
`pnpm test:pr:web` was chaining `pnpm test -- --watch=false`, which
pnpm expanded into:
dotenv -e .env -e .env.test.local -- vitest -- --watch=false
The second `--` made vitest treat `--watch=false` as a positional
file pattern, not a flag. Vitest then fell back to defaults (in CI's
non-TTY env that still meant a one-shot run, so the suite passed),
but the worker pool was effectively serialized for big chunks of the
243-file run — wall ~90 s on a 4-vCPU runner where the parallel-sum
of phases was ~236 s (≈2.6× effective parallelism).
Replace the chained pnpm invocation with a direct call to
`vitest run --maxWorkers=4`, matching the 4 vCPUs the GH Actions
ubuntu-latest runner provides.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(annotations): preview mode for deep-link landings
When the reader opens at a deep-link CFI (e.g. clicking an exported
highlight from Obsidian), the position should not be persisted as the
user's reading progress until they actually start reading. Otherwise
the deep-link visit overwrites their last-read position and propagates
that across all sync targets.
Adds a per-book `previewMode` flag in the reader store that:
- Is set to true in FoliateViewer when the URL's `?cfi=` overrides the
saved last-position.
- Is cleared on the first user-initiated relocate (page turn / scroll),
reusing the existing reason filter in `docRelocateHandler`.
- Gates the auto progress writers:
- useProgressAutoSave — skip local config persist
- useProgressSync — skip auto-push and skip the remote-progress
view.goTo (so cloud pull doesn't yank the
user away from the previewed annotation)
- useKOSync — skip auto-push (manual pushes still respected)
Hardcover sync and Discord presence are unaffected: hardcover only
fires on explicit user button press, and Discord presence carries no
position information.
Also picks up the regenerated AndroidManifest.xml change from the
existing tauri.conf.json deep-link config (registers readest:// scheme
on Android so the smart landing page's intent:// launch resolves).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(annotations): jump in place when target book is already open
When an annotation deep link arrives while the user is already in the
reader (most common case on mobile App Links), navigateToReader was
pushing the same /reader path with a different cfi query param. The
reader's init useEffect has [] deps, so it doesn't re-run, and
FoliateViewer doesn't re-read the cfi — the view stayed put.
Detect a mounted view for the target book hash by walking
viewStates and matching the hash prefix on the bookKey. If found,
call view.goTo(cfi) directly and set previewMode so the existing
gates fire. Falls back to navigateToReader when no view is open.
Also adds a console.log on each parsed deep link to make this path
easier to debug from device logs in the future.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(opds): add OPDS-PSE streaming support and custom OPDS 2.0 parser
* refactor(opds): remove custom parser, use updated foliate-js dependency
* fix(opds): resolve PSE auth at fetch time, drop credentials from book.url
Previously the streaming `pse://` virtual file baked the proxy URL with
the basic auth header into `book.url`, which (a) failed on desktop where
no proxy is used because the auth header was never applied to the page
fetch, and (b) leaked the credential to the sync server because transient
books still get pushed on first sync.
Now the `pse://` payload stores only the upstream OPDS template URL plus
the catalog id. A new `createPseStreamPageLoader` looks up the catalog
from settings on each open, probes auth once (cached for the session),
and applies the auth header via `tauriFetch` on desktop or via the proxy
URL on web.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(toc): cache TOC + section fragments per book
Moves the TOC regrouping and section-fragment computation out of
foliate-js/epub.js #updateSubItems into the readest client as
computeBookNav / hydrateBookNav in utils/toc.ts. The result is
persisted to Books/{hash}/nav.json — capturing the book's full
navigable structure (TOC hierarchy + sections with hierarchical
fragments). Compute once, persist locally, hydrate on subsequent
opens. Designed to serve current human-facing navigation (TOC
sidebar, progress math) and future agentic navigation (LLM-driven
seeking by structural location).
Versioned by BOOK_NAV_VERSION for forward invalidation. Existing
books regenerate transparently on next open.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: update worktree scripts
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* perf(store): decouple page turn from full library rewrite for large collections
Previously every page turn triggered setLibrary() which copied the entire
library array, ran refreshGroups() with MD5 hashing over all books, and
caused cascading re-renders. With ~2800 books this made reading unusable.
- Add hash-indexed Map to libraryStore for O(1) book lookups
- Add lightweight updateBookProgress() that skips array copy and refreshGroups
- Use hash index in setProgress, saveConfig, and initViewState
- Batch cover URL generation with concurrency limit on library load
Addresses #3714
* perf(import): replace filter()[0] with find() to short-circuit on first match
* fix(store): replace Object.assign state mutation with immutable spread in setConfig
* perf(persistence): remove JSON pretty-printing to reduce serialization overhead
* fix(reader): stabilize debounce reference in useIframeEvents to prevent timer reset on re-render
* perf(context): memoize provider values to prevent unnecessary consumer re-renders
* perf(store): cache visible library to avoid refiltering on every access
* perf(library): remove redundant refreshGroups call already triggered by setLibrary
* perf(import): replace O(n) splice(0,0) with O(1) push for new book insertion
* perf(import): defer library persistence to end of import batch instead of every 4 books
* perf(library): skip full library reload on reader close since store is already in sync
* fix: address PR review feedback for library perf optimizations
Correctness fixes for issues found in code review:
- fix(library): restore library reload on close-reader-window. Reader
windows are independent Tauri webviews with their own libraryStore
instance, so progress / readingStatus / move-to-front updates from
the reader do not propagate to the main window. Reload from disk
so the library reflects the changes the reader just persisted.
- perf(import): wire BookLookupIndex into importBooks. The lookupIndex
parameter on bookService.importBook had no caller, leaving the
Map-based dedup path dead. Build the index once per import batch
in app/library/page.tsx and thread it through appService.importBook
so the O(1) dedup path is actually exercised.
- perf(import): defer library save to end of batch. Add a skipSave
option to libraryStore.updateBooks and call appService.saveLibraryBooks
once after the entire import loop, instead of once per concurrency-4
sub-batch.
- fix(store): make updateBookProgress immutable. The previous in-place
mutation reused the same library array reference, bypassing Zustand
change detection AND leaving the visibleLibrary cache holding stale
Book references. Now slice the array, update the entry, and refresh
visibleLibrary. Also make readingStatus a required parameter so
future callers cannot accidentally clear it by omitting the argument.
- fix(store): make saveConfig immutable. It previously mutated the Book
object's progress / timestamps in place and used splice/unshift on
the shared library array. Now spread to a new book object and rebuild
via setLibrary. Also corrects the interface signature to return
Promise<void> (the implementation was already async).
- fix(store): make updateBook immutable for the same reason — it was
mutating the previous-state library array before spreading.
- fix(context): wrap AuthContext login/logout/refresh in useCallback.
Without this, the useMemo deps array changed every render and the
memo was a no-op, defeating the optimization the PR was trying to
add.
- fix(reader): use a ref for handlePageFlip in useMouseEvent's debounce.
The empty-deps useMemo froze the first-render handler; with the ref
the debounced wrapper always invokes the latest closure.
Test coverage added:
- library-store: immutable updateBookProgress, visibleLibrary cache
refresh, deleted-book filtering, updateBooks skipSave option
- book-data-store: immutable saveConfig, move-to-front correctness,
visibleLibrary order, persistence behavior
- import-metahash: BookLookupIndex update on new import, lookup-index
consultation before scanning books array
- auth-context (new file): context value identity stability across
re-renders, callback identity stability
- useIframeEvents (new file): debounced wheel handler dispatches to
the latest handlePageFlip after re-render
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(types): move BookLookupIndex to types/book.ts
Avoids the inline `import('@/services/bookService').BookLookupIndex`
type annotation in types/system.ts. Both the AppService interface and
the bookService implementation now import BookLookupIndex from the
canonical location alongside Book.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(import): convert importBook params to options object
Replace the long positional-argument list on appService.importBook
(saveBook, saveCover, overwrite, transient, lookupIndex) with a
single options object so callers no longer need to pad with
`undefined, undefined, undefined, undefined` to reach the parameter
they actually want to set.
Before:
await appService.importBook(file, library, undefined, undefined,
undefined, undefined, lookupIndex);
After:
await appService.importBook(file, library, { lookupIndex });
The underlying bookService.importBook is also refactored to take an
options object: required AppService callbacks (saveBookConfig,
generateCoverImageUrl) are bundled with the optional flags via an
ImportBookInternalOptions interface that extends the public
ImportBookOptions defined in types/book.ts.
All existing call sites updated to the new shape.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(reader): import Foliate annotations on book open (Linux only), closes#2180
Automatically imports annotations, bookmarks, and reading progress from
Foliate's data files when opening a book on Linux. Uses foliateImportedAt
flag to prevent re-importing on subsequent opens.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(annotation): move Foliate import into annotation provider pattern
Restructure annotation import as a multi-provider service following the
translators pattern. Foliate becomes a provider under
services/annotation/providers/, making it easy to add more import sources.
Each provider controls its own availability check and skip-if-imported guard.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>