Commit graph

1952 commits

Author SHA1 Message Date
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
Yuan Tong
7d852518a3
feat(windows): use overlay scrollbar (#3868)
* feat(windows): use overlay scrollbar

* fix format
2026-04-14 19:03:07 +02:00
Huang Xin
73d30c103f
fix(toc): fix page number of some TOC items from section fragments (#3867) 2026-04-14 16:22:06 +02:00
Huang Xin
8cdf378b47
fix(i18n): fix translations in RU (#3866) 2026-04-14 08:54:05 +02:00
Huang Xin
1088af023b
release: version 0.10.6 (#3861) 2026-04-13 12:55:13 +02:00
Huang Xin
011ad18a02
fix(android): use stable safe area insets to avoid unnecessary layout shift, closes #3670 (#3859) 2026-04-13 12:04:41 +02:00
Huang Xin
e9d71b2936
feat(settings): add an option to avoid overriding paragraph layout, closes #3824 (#3858) 2026-04-13 09:42:53 +02:00
Huang Xin
ec32614539
fix(settings): fixed color picker for custom highlight colors, closes #3796 (#3857) 2026-04-13 08:25:48 +02:00
Huang Xin
96678d85ec
refactor(settings): persist the apply-globally toggle per book (#3856) 2026-04-13 07:45:24 +02:00
Huang Xin
41b5e92563
feat(annotator): support instant copy operation for selected text, closes #3828 (#3854) 2026-04-13 06:00:05 +02:00
Huang Xin
ef97a8ed02
fix(ux): optimize scrolling UX for the bookshelf and sidebar content (#3849) 2026-04-12 20:52:12 +02:00
Huang Xin
8df8bc8b4a
chore(agent): use claude in chrome for web based qa (#3847) 2026-04-12 12:26:20 +02:00
DrSheppard
f0e23a1503
fix(linux): update package installation for Linux-x64 (#3845) 2026-04-12 11:01:00 +02:00
Huang Xin
4e1464ef17
fix(macOS): don't show window button when traffic lights are on the header, closes #3831 (#3843) 2026-04-12 10:05:27 +02:00
Huang Xin
7b60b1bb0c
fix(ios): reduce GPU memory pressure to prevent WebKit GPU process crash (#3842)
On iOS, navigating to a book group in the library caused the WebKit GPU
process to exceed its 300 MB jetsam limit (peaking at ~328 MB), resulting
in a blank screen flash and broken scroll state.

Three changes reduce peak GPU memory usage:

- Add overscan={200} to VirtuosoGrid/Virtuoso so only items within 200px
  of the viewport are rendered, limiting simultaneous image decoding
- Add loading="lazy" to both Image components in BookCover so the browser
  defers decoding offscreen cover images
- Conditionally mount the <video> and <audio> elements in AtmosphereOverlay
  only when atmosphere mode is active, eliminating idle H.264 decoder
  memory overhead

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 09:06:16 +02:00
Huang Xin
cc780712b9
fix(deps): add pnpm override for qs >=6.14.2 (Dependabot #71) (#3841)
Fixes DoS vulnerability from arrayLimit bypass in comma parsing.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 06:58:31 +02:00
Huang Xin
95ff526140
fix(deps): bump dependencies to resolve 13 Dependabot security alerts (#3840)
Update next (16.2.3), vite (7.3.2+), react/react-dom (19.2.5),
@vitejs/plugin-rsc (0.5.23), react-server-dom-webpack (19.2.5),
and add overrides for lodash (4.18.0), lodash-es (4.18.0),
basic-ftp (5.2.2) to fix high/medium severity vulnerabilities
including DoS, code injection, prototype pollution, CRLF injection,
arbitrary file read, and path traversal.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 06:44:30 +02:00
Huang Xin
f86bbbcc22
perf(library): virtualize grid and list of book items when rendering library page (#3835) 2026-04-11 21:25:06 +02:00
Huang Xin
20940105fb
feat(library): navigate to previous group with the Back button on Android, closes #2675 (#3833) 2026-04-11 20:08:00 +02:00
Huang Xin
2a49e93cf7
fix(library): fixed the All button in groups breadcrumbs navigation bar, closes #3782 (#3832) 2026-04-11 19:55:08 +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
7bf4822b27
fix(library): restore breadcrumb 'All' navigation by bypassing next-view-transitions, closes #3782 (#3829)
The breadcrumb "All" button was broken on first click after entering a
group because next-view-transitions@0.3.5's useTransitionRouter wraps
router.replace() in startTransition + document.startViewTransition, and
this combination is incompatible with Next.js 16.2 RSC navigation when
only the search params change for the same pathname (e.g.
/library?group=foo -> /library). The navigation silently never commits.

Extract the library navigation logic into a useLibraryNavigation hook
that uses plain useRouter from next/navigation. The data-nav-direction
attribute is still set so existing directional CSS keeps working when
view transitions fire via popstate.

See https://github.com/shuding/next-view-transitions/issues/65 for the
upstream incompatibility.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-11 16:04:37 +02:00
Huang Xin
de11511c30
fix(layout): fixed bleed layout of images (#3823) 2026-04-10 19:33:56 +02:00
Huang Xin
ab7da981da
fix(eink): remove scroll animation in eink mode and optimize eink detection (#3822)
* fix(eink): remove scroll animation in eink mode

* fix(android): fix startup ANR on e-ink devices from getprop subprocesses
2026-04-10 18:22:06 +02:00
Huang Xin
3df75a67f9
feat(tts): support edge tts on cloudflare worker (#3819) 2026-04-10 15:32:06 +02:00
Huang Xin
07e3248780
fix: apply disable click to paginate also for non-iframe clicks (#3818) 2026-04-10 12:27:17 +02:00
Lex Moulton
23d5f3363d
fix(rtl): fix page navigation for Arabic books (#3817)
* fix(rtl): fix page navigation for Arabic books

* fix(rtl): unified navigation handlers for rtl and ltr

---------

Co-authored-by: Huang Xin <chrox.huang@gmail.com>
2026-04-10 11:54:30 +02:00
Zeedif
c6daf72da9
feat(opds): allow editing of registered catalogs (#3814)
* feat(opds): allow editing of registered catalogs

* chore(i18n): add translations for catalog edit feature

Translate "Remove", "Edit OPDS Catalog", and "Save Changes" across all
31 locales.

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-10 06:09:30 +02:00
Zeedif
bd866cb049
fix(opds): harden Content-Disposition filename parsing for complex names and encoding (#3816) 2026-04-10 06:08:57 +02:00
Huang Xin
a5690e9a84
fix(tts): skip br elements in PDF text layer to prevent TTS interruptions at line breaks, closes #3771 (#3811)
PDF.js TextLayer renders <br> between text spans for visual line wrapping.
The TTS SSML generator was converting these to <break> elements, causing
TTS engines to pause at every PDF line break within paragraphs. Fix by
rejecting <br> (along with canvas and annotationLayer) via the node filter
when the document is detected as a PDF.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:34:38 +02:00
Huang Xin
ed7cfc31fc
fix(layout): fix off-by-one page count on fractional DPR devices, closes #3787 (#3813)
The `pages` getter used Math.ceil(viewSize / containerSize) which inflates
the count by 1 when floating-point drift makes the ratio slightly above an
integer (e.g. 4.00000001 → 5). Use Math.round to absorb sub-pixel drift,
matching the approach already used in #getPagesBeforeView.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 20:08:55 +02:00
Huang Xin
a07bf23e18
chore(docs): add worktree management for isolated PR review and feature work (#3810)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 19:42:47 +02:00
Zeedif
41d014914b
fix(opds): handle spaces and quotes in Content-Disposition filename parsing (#3812) 2026-04-09 19:42:29 +02:00
Lex Moulton
baee85e7c8
feat(rsvp): sync reading position to cloud via book_configs (#3801) 2026-04-09 15:52:49 +02:00
Huang Xin
1e259e87b2
refactor(reader): introduce priority-based touch interceptor for gesture handling (#3809)
Replace the inline touch-swipe event dispatching with a module-level
interceptor registry. This lets the reading ruler (priority 10) claim
drag gestures before the swipe-to-flip handler (priority 0), preventing
conflicts when dragging the ruler on touch devices.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:48:21 +02:00
Huang Xin
4abbc17f66
fix(annotator): fixed instant annotation in scrolled mode, closes #3769 (#3808) 2026-04-09 10:34:28 +02:00
Huang Xin
d7fd06ca82
chore: add explicit permissions to GitHub Actions workflows (#3807)
Fixes code scanning alerts #1, #2, #3, #4
(actions/missing-workflow-permissions).

- retry-workflow.yml rerun job: actions: write (to rerun workflows)
- upload-to-r2.yml upload-to-r2 job: contents: read (to download releases)
- upload-to-r2.yml retry-on-failure job: actions: write (to trigger retry)
- release.yml upload-to-r2 job: contents: read, actions: write

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 07:33:25 +02:00
Huang Xin
e43e533aca
security: fix complete multi-character sanitization for HTML comments in txt.ts (#3806)
* security: fix for code scanning alert no. 11: Incomplete multi-character sanitization

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fix: use dotAll flag to match multi-line HTML comments

Add the 's' flag to the comment-stripping regex so '.' matches
newlines, ensuring comments spanning multiple lines are also removed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: iterative dotAll sanitization in extractChaptersFromSegment

Fixes code scanning alert #10 (incomplete multi-character sanitization).
Apply the same fix as alert #11: replace one-shot comment stripping
with an iterative loop using the 's' (dotAll) flag so nested and
multi-line HTML comments are fully removed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* fix: iterative HTML tag sanitization in cleanDescription

Fixes code scanning alert #9 (incomplete multi-character sanitization).
Replace one-shot tag stripping with an iterative loop so crafted inputs
like nested/overlapping tags cannot leave '<script' behind after a single
replacement pass.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 07:20:56 +02:00
Huang Xin
dc788283ad
security: fix for code scanning alert no. 11: Incomplete multi-character sanitization (#3804)
* security: fix for code scanning alert no. 11: Incomplete multi-character sanitization

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* fix: use dotAll flag to match multi-line HTML comments

Add the 's' flag to the comment-stripping regex so '.' matches
newlines, ensuring comments spanning multiple lines are also removed.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-09 06:54:20 +02:00
dependabot[bot]
373420bb0c
chore(deps): bump actions/checkout in the github-actions group (#3805)
Bumps the github-actions group with 1 update: [actions/checkout](https://github.com/actions/checkout).


Updates `actions/checkout` from 4 to 6
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-09 06:47:57 +02:00
Huang Xin
6072b0dcbd
security: fix for code scanning alert no. 12: Use of externally-controlled format string (#3803)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-09 06:23:08 +02:00
Huang Xin
13ff96db85
security: potential fix for code scanning alert no. 19: DOM text reinterpreted as HTML (#3802)
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-04-09 06:10:49 +02:00
Lex Moulton
bfbe92f355
refactor(sidebar): replace react-window and OverlayScrollbars with react-virtuoso and CSS scrollbars (#3798)
* feat: Add option to split words in RSVP mode

* fix(rsvp): replace lookbehind regex with lookahead-only split in getHyphenParts

* feat: Add option to split words in RSVP mode

* fix(theme): update data-theme and themeCode when system theme changes

* feat(rsvp): split on ellipsis between letters and preserve delimiter type

* fix(rsvp): fix incorrect merged line

* feat(rsvp): insert blank frame between consecutive identical words

* refactor(sidebar): replace react-window and OverlayScrollbars with react-virtuoso and CSS scrollbars

* feat(toc): smooth scroll to active chapter on sidebar open

* test(theme-store): expand dark theme palette fixture with full color tokens

* refactor: remove dead code and consolidate duplicate CSS scrollbar rules

* fix(toc): fix auto-scroll on sidebar open and improve scroll behavior

- Add isSideBarVisible to scroll effect deps so it fires on sidebar open
- Use setTimeout delay for Virtuoso to finish layout before scrolling
- Add 10s cooldown on user scroll to re-enable auto-scroll
- Use smooth scroll for short distances (<16 items), instant for longer
- Track visible center via rangeChanged for accurate distance calculation
- Fix tab navigation background opacity (bg-base-200 instead of /20)

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-08 16:44:45 +02:00
Huang Xin
799db40763
fix(pdf): add an option to apply theme colors to PDF, closes #3778 (#3799) 2026-04-08 07:29:44 +02:00
Huang Xin
932c82aa49
chore(security): update CodeQL workflow to remove languages (#3794)
* chore(security): update CodeQL workflow to remove languages

Removed unsupported languages from CodeQL workflow.

* style: format codeql.yml with Prettier

Fix CI format check failure by applying Prettier formatting to the
CodeQL workflow file.

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-04-07 21:36:46 +02:00
Huang Xin
184de9210d
fix(security): prevent SSRF in kosync proxy (CWE-918) (#3793)
Validate serverUrl in the kosync API proxy to block requests to
private/internal addresses and non-http(s) schemes. Also fix isLanAddress
to detect 0.0.0.0 and bracket-wrapped IPv6 private addresses.

Closes code-scanning alert #14.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 21:21:51 +02:00
Zach Bean
50a2957e35
feat(kosync): support HTTP Basic auth for CWA KOSync servers (#3792)
* Support HTTP Basic auth for kosync connections

* fix(kosync): use separate password field for HTTP Basic auth

- Add `password?` field to KOSyncSettings to store the plain password
  alongside the existing `userkey` (md5 hash), preserving backward
  compatibility for existing users
- Replace Buffer.from() with btoa() for browser-compatible Base64 encoding
- Simplify buildHeaders() to use config fields directly instead of
  the useAuth union type

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-07 21:07:01 +02:00
Zach Bean
637b7462c4
fix(kosync): use Fragment keys to prevent form field issues (#3791)
* Use key attrs to prevent form field issues

Fixes #3790.

* PR feedback: use Fragment keys instead of individual element keys
2026-04-07 21:05:14 +02:00
Huang Xin
82deb85c64
docs: add threat model and incident response plan to SECURITY.md (#3788)
- Add Threat Model section covering assets, threat actors, attack
  surfaces, and mitigations for ebook parsing, cloud sync, OPDS
  integration, rendered HTML/JS, supply chain, and Tauri IPC
- Add Incident Response Plan with triage, containment, remediation,
  disclosure, and post-incident review steps
- Add severity definitions table

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-04-07 19:17:56 +02:00
Huang Xin
db35a4e203
fix(style): clamp oversized hardcoded pixel widths and fix browser test flakiness (#3785)
Closes #3770.

Add transformStylesheet rule that clips CSS width declarations exceeding the
viewport with max-width and border-box sizing. Also add @testing-library/react
to vitest browser optimizeDeps.include to prevent mid-test Vite reloads on CI.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:23:30 +02:00