* 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>
* feat: provide reading progress indicator for each books in the bookshelf
* refactor: simplify bookshelf component and remove unused hook; enhance reading progress calculation
- Removed the `useBookConfigLoader` hook as it was no longer needed.
- Simplified the bookshelf items generation by eliminating the use of `useMemo`.
- Improved the reading progress calculation in the `ReadingProgress` component to directly use the book's progress data.
- Updated the `Book` type to include a `progress` field for better tracking of reading status.
* fix formatting for readerStore
* Add H and L keyboard shortcuts for back and forward navigation, consistent with Vimium
* Fallback bookmark text to page number if current page has no text, closes#51
* Add delete and edit buttons for highlights and booknotes in the sidebar, closes#57
* Each book now has its own sidebar default tab