Commit Graph

81 Commits

Author SHA1 Message Date
Huang Xin 5e366018df fix(cbz): ComicInfo metadata + CBZ page count + WebDAV i18n (#4282)
* 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>
2026-05-23 15:59:29 +02:00
Huang Xin fb37406b31 feat(annotations): preview mode for deep-link landings (#4019)
* 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>
2026-05-01 17:37:50 +02:00
Zeedif ca8f0fe9f6 feat(opds): add OPDS-PSE streaming support and custom OPDS 2.0 parser (#3951)
* 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>
2026-04-28 15:14:44 +02:00
Huang Xin 3e292af990 refactor(nav): refactor book nav service with TOC enrichment (#3874) 2026-04-15 17:08:17 +02:00
Huang Xin b0cc5461af refactor(toc): cache navigable structure per book (#3869)
* 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>
2026-04-14 20:15:10 +02:00
Lex Moulton 030a7c0823 perf: optimize library operations for large collections (#3827)
* 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>
2026-04-11 16:32:35 +02:00
Huang Xin 1936136596 fix: resolve various tracked exceptions in ph (#3584)
* fix: handle synced bookmarks without cfi

* chore: clean up some exceptions in ph
2026-03-22 08:16:38 +01:00
Huang Xin 0e8605d298 feat(reader): import Foliate annotations on book open (Linux only), closes #2180 (#3574)
* 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>
2026-03-21 05:04:46 +01:00
Huang Xin 64a71e25e4 fix(layout): make horizontal scroll bar visible in fixed-layout EPUB, closes #3506 (#3512)
And fix some text not shown in fixed-layout EPUB.
2026-03-11 15:03:05 +01:00
Huang Xin 8850e6c00f feat(pdf): support TTS and annotation on PDFs, closes #2149 & #3462 (#3493)
* chore: bump jsdom to the latest version

* feat(pdf): support TTS and annotation on PDFs, closes #2149 & closes #3462
2026-03-09 10:28:19 +01:00
Huang Xin a74c1a56a9 fix(metadata): parse series info from epub only when book series info not available (#3478) 2026-03-05 09:53:28 +01:00
Huang Xin 0d2e5b7c76 fix(translation): reduce initial layout shift in the translation view, closes #3078 (#3428)
* fix(css): allow overriding the padding of the root html

* fix(translation): reduce initial layout shift in the translation view, closes #3078
2026-03-01 09:12:02 +01:00
Huang Xin c533da498d feat(annotator): add page number for annotations, closes #3082 (#3405)
* feat(annotator): add page number in annotation

* feat: add page number for annotations, closes #3082
2026-02-28 08:08:56 +01:00
Huang Xin 411f9e236d feat(metadata): support parsing series info from calibre exported EPUBs, closes #3259 (#3267) 2026-02-11 15:39:48 +01:00
Mohammed Efaz 01b4e3530d feat: toggle finished manually (#3091) 2026-01-28 06:26:57 +01:00
Huang Xin 8de6fa267e fix(cache): invalidate config and doc cache, closes #2595 and closes #2572 (#2623) 2025-12-05 07:03:57 +01:00
Huang Xin c792c18e01 feat: support text conversion between simplifed and traditional Chinese, closes #2508 (#2520) 2025-11-23 09:56:12 +01:00
Huang Xin 273bcafe01 fix: resolve endless loading indicator (#2502) 2025-11-21 14:58:55 +01:00
Huang Xin 123293dc0e fix: fixed crash when switching allow script (#2351) 2025-10-29 03:17:17 +01:00
Huang Xin 60ddb17547 feat: add support for per-book background image settings in addition to global settings, also closes #2223 (#2225) 2025-10-14 08:54:26 +02:00
Huang Xin bdef593cdd feat: support global settings from library menu, closes #2140 (#2151) 2025-10-01 09:59:37 +02:00
Huang Xin 48dc570809 fix: handle invalid bookkey for viewState, closes #2073 (#2074) 2025-09-19 11:09:32 +02:00
Huang Xin c0bf2d40dd fix: jump to current item in virtualized TOC list, closes #2055 (#2066) 2025-09-18 18:38:09 +02:00
Huang Xin 91a5454b46 feat(sync): aggregate books with metadata hash for progress and annotation sync (#2062) 2025-09-18 14:05:16 +02:00
Huang Xin 71c2143cba fix: avoid pagination until book view is inited, closes #1983 (#1994) 2025-09-07 14:47:35 +02:00
Huang Xin f4b00d6dec fix: get rid of invalid grid insets (#1943) 2025-09-01 07:05:48 +02:00
Huang Xin d390209dab feat: support importing TTF/ODF fonts, closes #979 and closes #1708 (#1864) 2025-08-21 21:49:08 +02:00
Huang Xin 3e5e4d2946 compat: detect book language from text for invalid language code in metadata (#1743) 2025-08-04 14:55:18 +02:00
Huang Xin 511093a6ef tts: tts now works when language codes are mistakenly set with language names (#1737) 2025-08-02 16:52:32 +02:00
Huang Xin d9199d1182 sync: also sync book metadata (#1611) 2025-07-18 17:59:05 +02:00
Huang Xin e3c411f043 feat: support to edit book metadata, closes #753 (#1583) 2025-07-14 19:46:57 +02:00
Huang Xin 31aeb2289c settings: more reasonable constraints for top and bottom margins (#1428) 2025-06-19 17:01:20 +02:00
Huang Xin ed68d25b15 fix: restore full view settings when reopening book (#1400) 2025-06-15 16:07:51 +02:00
Huang Xin 97d7eb659c bump: sync with upstream foliate-js (#1381) 2025-06-11 05:11:06 +02:00
Huang Xin 43532a0ee8 feat: add an option to show remaining in minutes of the current chapter, closes #406 (#1326) 2025-06-03 15:37:55 +02:00
Huang Xin a9965fd52c config: add an option to sort TOC by page number, closes #1304 (#1308) 2025-06-02 07:23:59 +02:00
Huang Xin 6ec3a5b332 feat: add an option to enable JavaScript in EPUB, closes #1278 (#1295) 2025-05-31 18:25:25 +02:00
Huang Xin 77fa57438e tts: show speaking sentence and chapter info in tts media session (#1289) 2025-05-31 09:21:17 +02:00
Huang Xin 8167f27156 feat: add location info for each entry in table of contents (#1016) 2025-05-02 15:08:20 +02:00
Huang Xin 172ab382bc fix: query status bar height on Android, closes #943 (#947) 2025-04-23 14:01:37 +02:00
Huang Xin 4dc943d23e fix: tts now fallbacks to metadata language of the book, closes #916 (#945) 2025-04-22 18:12:14 +02:00
Huang Xin cf66665096 feat: immersive UI in reader page on iOS and Android, closes #886 and closes #864 (#911) 2025-04-19 16:43:39 +02:00
Huang Xin 72e6c058f5 fix: completed books now have 100% progress (#625) 2025-03-18 06:00:33 +01:00
Huang Xin c0d715bf77 sync: check local file availability before opening (#262) 2025-01-28 21:02:19 +01:00
QP Hou 7fcaab8b9f mobile: swipe up to toggle header/footer (#205) 2025-01-21 07:17:36 +01:00
Egan Gumiwang Pratama Bisma 46fdea3522 feat: provide reading progress indicator for each books in the bookshelf (#153)
* 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
2025-01-15 17:17:07 +01:00
Christian Clauss 572ddd923c Fix typos discovered by codespell (#80) 2025-01-02 15:29:27 +01:00
Huang Xin 71ffe76e29 Various enhancements (#58)
* 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
2024-12-27 20:53:30 +01:00
Huang Xin 9be9bc8a34 Support reading progress synchronization across platforms (#34) 2024-12-24 12:31:02 +01:00
chrox d7ccbbcaa2 Use splitTOCHref to support other ebook formats as discussed in foliate-js#45 2024-12-20 22:44:35 +01:00