preloadNextSSML() called tts.next() interleaved with await, creating async
gaps where the concurrent #speak() could dispatch marks against corrupted
#ranges state (replaced by next() for a different block). Since mark names
restart at "0" per block, marks resolved to the wrong text position, causing
accidental page turns and highlights far from the current sentence.
Fix: gather all tts.next() and tts.prev() calls synchronously (no await
between them) so no async code can see the intermediate #ranges state.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(android): add D-pad navigation for Android TV remote controller
Add basic D-pad/arrow key navigation support for Android TV Bluetooth
remote controllers, covering the full flow: library grid browsing,
reader page turning, toolbar interaction, and TTS toggle.
Library:
- Custom spatial navigation hook for grid D-pad navigation
- Arrow keys move between BookshelfItem elements with auto column detection
- ArrowDown from header enters the bookshelf grid
Reader:
- Enter key toggles header/footer toolbar visibility
- Left/Right navigate between toolbar buttons, Up/Down moves between
header and footer bars
- Back button dismisses toolbar (with blur) before sidebar/library
- Focus-probe technique for reliable button visibility detection
across fixed/hidden containers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: only auto-focus toolbar buttons on keyboard activation, not mouse hover
Track pointer activity to distinguish keyboard vs mouse toolbar activation.
When the toolbar appears due to mouse hover, skip auto-focus to prevent
unwanted focus outlines on desktop.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: track keyboard events instead of pointer events for auto-focus guard
Pointer events from iframes don't bubble to the main document, so
tracking pointermove/pointerdown missed hover interactions over book
content. Track keydown instead — auto-focus only when a recent keyboard
event (Enter key) triggered the toolbar, not mouse hover.
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(hardcover): add one-way Hardcover sync integration
- Add HardcoverClient with ISBN/title-based book lookup, progress push, and note sync
- Add HardcoverSyncMapStore for persistent local mapping of note IDs to Hardcover journal IDs
- Add GraphQL queries/mutations for Hardcover API (insert/update/recreate journal entries)
- Add DB migration for hardcover_note_mappings table (schema: hardcover-sync)
- Add HardcoverSettings dialog component (connect/disconnect/enable toggle)
- Add useHardcoverSync hook wired in Annotator for push-notes and push-progress events
- Add Hardcover Sync section in BookMenu with per-book toggle (persisted in BookConfig)
- Add HardcoverSettings type and DEFAULT_HARDCOVER_SETTINGS to system settings
- Add hardcoverSyncEnabled per-book flag to BookConfig
- Mount HardcoverSettingsWindow in Reader alongside KOSync and Readwise windows
Sync is one-way (Readest → Hardcover), manual-action only, per-book opt-in.
Supports idempotent note sync: insert new, skip unchanged, update changed, recreate on stale remote ID.
* fix(hardcover): proxy API calls server-side to fix CORS, normalize Bearer token
- Add /api/hardcover/graphql Next.js route that proxies POST requests server-side,
bypassing CORS restrictions (Hardcover API has no Access-Control-Allow-Origin header)
- On Tauri (desktop), calls Hardcover directly; on web, routes through the proxy
- Normalize token in HardcoverClient: accept raw JWT or 'Bearer <jwt>' format
- Update helper text to point to hardcover.app → Settings → API
* fix(hardcover): surface note-sync no-ops and harden book resolution
- Show toast feedback when book data is still loading, Hardcover is not configured,
or the current book has no annotations/excerpts to sync
- Show an explicit info toast when note sync finds no new changes
- Parse Hardcover search results in the current hits/document response shape
- Resolve note sync through ensureBookInLibrary for parity with progress sync
- Add console logging for note/progress sync failures
* debug(hardcover): add runtime instrumentation for note sync
* feat(metadata): keep identifier stable and store ISBN separately
- Add dedicated metadata.isbn field
- Expose ISBN as its own editable field in book metadata
- Preserve identifier semantics for existing source IDs and hashes
- Route metadata auto-retrieval ISBN handling through the new field
- Prefer metadata.isbn for Hardcover matching
* fix(hardcover): avoid wasm sqlite for note mappings on web
- Store Hardcover note sync mappings in localStorage on web
- Keep sqlite-backed mappings for desktop/native environments
- Remove the web-only database dependency from manual note sync
* fix(hardcover): dedupe notes by payload hash across unstable note IDs
- Add payload-hash lookup in HardcoverSyncMapStore
- Reuse existing journal mapping when payload already synced
- Prevent duplicate insertions when note IDs change or duplicate locally
* fix(hardcover): avoid duplicate quote export when annotation note exists
- Detect excerpt+annotation pairs for the same highlight
- Skip standalone excerpt export when annotation has note text
- Keep annotation export as the single source of truth
* docs(hardcover): add consolidated change summary for review
* fix(hardcover): suppress excerpt export by CFI when annotation note exists
* fix(hardcover): suppress empty-note annotation duplicates when note exists
* fix(hardcover): deduplicate notes by text and cfi base node
* refactor(hardcover): optimize sync performance, add rate limiting and clean up debug tools
* chore: remove dev-only change log from main
* test(hardcover): add unit tests for sync mapping and client logic
* chore: custom deployment and UI fixes
* fix(hardcover): use timestamptz for accurate annotation time
* fix(hardcover): use date scalar and RFC 3339 formatting for journal entries
* Revert "chore: custom deployment and UI fixes"
This reverts commit 0329aba7129d1e1ebf2c663804b8fba9a9f87b91.
* Fix hardcover progress dates and surface imported ISBNs
* Fix Hardcover currently reading sync
* fix(hardcover): avoid promoting note sync status
* style(hardcover): apply prettier formatting
* test(hardcover): fix strict TypeScript assertions
* test(hardcover): harden sync regression coverage
* test(hardcover): fix lint and formatting regressions
* fix(hardcover): narrow note dedupe range matching
* refactor(hardcover): extract note dedupe helpers
* refactor(isbn): extract metadata normalization helpers
* feat(hardcover): improve synced quote formatting
* fix(rsvp): rename pause setting to punctuation delay for clarity
* fix(rsvp): update chapter header as user crosses TOC sections
* fix(rsvp): stop playback at end of book instead of restarting last chapter
* feat(rsvp): convert WPM badge to speed selector dropdown
* feat(rsvp): scroll active chapter to center when chapter dropdown opens
* fix(rsvp): show correct chapter name in header instead of "Select Chapter"
* fix(rsvp): start from selected chapter when switching via dropdown
* fix(rsvp): restore saved position when resuming from a different section
* fix(rsvp): remove shrink animation from header buttons on click
* refactor(rsvp): process one spine section at a time, greadtly reducing complexity
* refactor(rsvp): remove dead state and derive progress from currentIndex
* feat(rsvp): allow pausing during countdown
* fix(rsvp): use relocate event instead of fixed 500ms delay for chapter transitions
* fix(rsvp): persist WPM and punctuation pause settings across sessions
* fix(rsvp): prefix unused bookKey field with underscore to satisfy lint
* Revert "fix(rsvp): prefix unused bookKey field with underscore to satisfy lint"
This reverts commit 7b3a34813f0c3771e9af8d00c7c7dc77c8db161c.
* fix(rsvp): remove unused bookKey field from RSVPController
* feat(rsvp): make progress bar draggable
* fix: revert settings.json changes
* fix(rsvp): restore genuinely useful comments
* i18n: update translations
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
* fix(library): always sort series books by index ascending, closes#3709
When grouping by series, the sort direction (asc/desc) was applied to
within-series book sorting, causing books to appear in reverse series
index order when the library was sorted descending. Now series index
sort is always ascending (1, 2, 3…) regardless of the global sort
direction. Also sort series groups by name when "Sort by Series" is
selected instead of falling back to updatedAt.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(library): skip merge sort when inside a series group
The allItems.sort at the end of sortedBookshelfItems was re-sorting
books using the regular bookSorter (e.g. Date Read), which overrode
the within-group sort that correctly prioritized series index. Now
when inside a group, the already-sorted books are returned directly.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add ~290 new unit tests covering utils (lru, diff, css, a11y, walk,
usage, txt-worker), services (EdgeTTSClient, transformers), and
suppress noisy console output across all test files. Rename 27
camelCase test files to dash-case for consistency.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a new TTS tab in settings with media metadata update frequency control
(sentence/paragraph/chapter) to reduce Bluetooth notification spam, and move
TTS highlight settings from Color tab to the new TTS tab. Also add a highlight
opacity setting with live preview in the Color tab.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When TOC entries use fragment-suffixed hrefs (e.g. ch01.xhtml#ch01),
the sectionsMap lookup matched subitems with cfi: undefined instead of
parent sections with valid CFIs. This caused findTocItemBS to map every
annotation to the last TOC entry. Now skip subitems lacking a CFI and
fall back to the base section.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(shortcuts): change bookmark shortcut from Ctrl+D to Ctrl+B, closes#3675
Ctrl+D was bound to both Toggle Bookmark (General) and Dictionary
Lookup (Selection). When text was selected and Ctrl+D pressed, both
actions fired — opening the dictionary AND adding an unwanted bookmark.
Changed bookmark to Ctrl+B/Cmd+B to resolve the conflict. Added a
unit test that detects identical keybinding lists across actions to
prevent this class of bug in the future.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* i18n: mark shortcut section names for translation
Wrap SHORTCUT_SECTIONS values with stubTranslation() so i18next-scanner
picks them up. Translate the 4 new keys (General, Text to Speech, Zoom,
Window) across all 29 locales.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add a keyboard shortcuts help dialog toggled with '?' that displays all
shortcuts grouped by section with platform-appropriate key rendering.
Restructure DEFAULT_SHORTCUTS to include i18n descriptions and section
metadata. Translate 37 new keys across 29 locales.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add bidirectional annotation/highlight sync between KOReader and Readest:
- Add xpointer0/xpointer1 fields to BookNote and DBBookNote types for
KOReader XPointer positions alongside Readest's CFI format
- Extend transform layer to pass through xpointer fields to/from DB
- Convert CFI→XPointer on push and XPointer→CFI on pull in useNotesSync,
discarding notes that fail conversion
- Support KOReader's text()[K].N indexed text node format in xcfi.ts for
paragraphs with inline elements (e.g. <a> page anchors)
- Generate KOReader-compatible XPointers: text().N for single text nodes,
text()[K].N only when multiple direct text nodes exist
- Skip cfi-inert elements (injected by Readest at runtime) in XPointer
path building and resolution
- Map highlight colors between KOReader and Readest color systems
- Implement KOReader plugin annotation push/pull with deterministic IDs,
auto-sync on document open/close, and UIManager refresh on pull
- Refactor koplugin into focused modules: syncauth, syncconfig,
syncannotations, selfupdate
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>
Add a "Refresh Metadata" option under Advanced Settings that re-parses
book files to update metadata (series info, language, etc.) for books
imported before these fields were supported.
- Add refreshBookMetadata() in bookService that opens a book file,
re-parses with DocumentLoader, and updates metadata/metaHash/series
without touching updatedAt or user-edited fields
- Add menu item in SettingsMenu with progress display
- Add unit tests for single-book metadata refresh
- Translate new i18n strings across all 29 locales
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs in createChapterRegexps for English txt-to-epub conversion:
1. numberPattern had a capturing group (\d+|...) that leaked single
digits into split() results instead of chapter titles. Changed to
non-capturing (?:\d+|...).
2. combinedPattern wrapped the heading in (?:...) so split() consumed
the title without returning it. Changed to (...) capturing group,
matching the Chinese regex structure.
3. The prefix (?:^|\n|\s) allowed matching Chapter/Section after any
whitespace, causing mid-sentence false positives. Changed to
(?:^|\n) for line-start only.
Added a second-tier regex for bare numbered headings (e.g. "1.1The
Elements of Programming", "1Building Abstractions") commonly found
in academic texts. Dotted numbers allow optional space before title;
single bare digits require immediate title to avoid footnote matches.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add formatBlockQuote utility and blockquote nunjucks filter so both
default and custom template exports prefix every line with `>`.
Document all available formatters in the custom template help panel.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Sort author groups by last name instead of first name when sorting by author
- Sort series groups by book author instead of series name when sorting by author
- Place ungrouped books before custom groups instead of mixing them together
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
RemoteFile (used on all desktop platforms) extended File([]) with empty
data and overrode slice(), text(), arrayBuffer() but not stream(). The
large file path (>8MB) introduced in #3320 uses file.stream() to read
content incrementally, which returned empty data from the base File
class, causing "No chapters detected" for large TXT files.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Set up WebDriver-based testing for the Tauri app with two tiers:
- Vitest browser-mode tests (*.tauri.test.ts) running inside the Tauri WebView
for plugin IPC testing (libsql, smoke tests)
- WDIO E2E tests (*.e2e.ts) for UI-level interaction testing
Key changes:
- Add webdriver Cargo feature gating tauri-plugin-webdriver
- Add runtime capability for remote URLs (webdriver builds only)
- Add vitest.tauri.config.mts and wdio.conf.ts connecting to embedded
WebDriver server on port 4445
- Add shared tauri-invoke helper for IPC from Vitest iframe context
- Add testing documentation in docs/testing.md