* feat(warichu): support warichu (割注) inline annotation layout
- Add warichu HTML transformer that converts <span class="warichu"> and
<warichu> elements into .warichu-pending placeholders during content load
- Implement runtime layout (layoutWarichu / relayoutWarichu) that measures
column position and splits text into small inline-block chunks (2 chars
each) so CSS column boundaries can break between them, preventing large
blank gaps in vertical-rl pagination
- Use column stride (column-width + column-gap) for accurate position
measurement across column boundaries
- Hook into stabilized event for initial layout and relayout on resize
- Add warichu CSS styles (inline-block chunks, half-size font, vertical align)
* fix(warichu): correct HTML slicing edge cases
- sliceHtml: re-emit tags that were already open before the slice start
so the result stays well-formed. Previously a slice past an opening
tag produced an orphan closing tag (e.g. "<b>Hello</b>"[3,5] →
"lo</b>" instead of "<b>lo</b>").
- sliceHtml / removeFirstVisibleChar / removeLastVisibleChar: treat HTML
entities (e.g. &) as one visible character so they aren't split or
truncated mid-entity (e.g. removeFirstVisibleChar("&rest") →
"amp;rest").
- buildNodes: drop a duplicate chunk.appendChild(l2).
- Add unit tests covering the above for the three pure helpers.
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
* Fix ZoomLabel not showing during pinch zoom
* fix: exponential wheel zoom and show ZoomLabel during pinch
- Use exponential wheel zoom (Math.exp) instead of additive for smoother PC scroll
- Show ZoomLabel during pinch start and pinch move
- Clean up unused setZoomSpeed state for wheel handler
The TOC occasionally flashed a scroll to the current item and then
snapped back to the top, and on slow mobile first-opens sometimes
stayed at the top entirely.
Root cause: `useOverlayScrollbars({ defer: true })` schedules OS
construction via `requestIdleCallback` with a ~2233 ms timeout. On a
busy first open the timeout fires before the browser goes idle, so OS
wraps the viewport late — and the wrap step resets the scroller's
`scrollTop` synchronously, undoing Virtuoso's earlier scroll to the
current item. Virtuoso's `rangeChanged` / `onScroll` don't propagate
the reset for another frame, so any guard based on tracked scroll
state reads stale.
* 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>
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>
* 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>
* 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>
The Wiktionary REST API does not support Chinese entries — simplified
characters return 404, traditional characters omit Chinese results, and
no pronunciation data is ever included. Switch Chinese lookups to the
Wiktionary Action API which returns full wikitext with pinyin and
definitions. Also add encodeURIComponent to the REST API URL for all
other languages.
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix(ui): restore highlight options layout and clean up color name editing
Restore the pre-#3741 layout for both the AnnotationPopup highlight
options row and the HighlightColorsEditor settings panel.
HighlightOptions: re-add `justify-between` on the outer container and
remove `flex-1` from the color strip so the gap between the style box
and color strip responds to the number of colors.
HighlightColorsEditor: restore `grid-cols-3 sm:grid-cols-5` grid,
remove always-visible name inputs, and add a click-to-edit popover on
each color circle with hover tooltip for the label.
Add a browser screenshot test that renders the real AnnotationPopup
component with Tailwind CSS and compares against baseline PNGs for
5, 10, and 15 colors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The translate.toil.cc relay that backs the yandex provider is currently
unavailable. Introduce a `disabled` flag on TranslationProvider and
filter disabled providers out of both `getTranslators()` and
`getTranslator()` so the settings panel, translator popup, and the
fallback logic in `useTranslator` all agree the provider doesn't exist —
no per-callsite changes needed. Flip the flag back (or delete the line)
on `yandexProvider` to re-enable once the upstream is healthy.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Guard handleTTSSpeak with a single-flight ref so a second tts-speak
event that arrives while the first is still inside its initial awaits
(initMediaSession / backgroundAudio / TTSController.init) is ignored
instead of racing ahead to construct a second TTSController. Without
this, rapid clicks produced two concurrent speakers talking over each
other because viewState.ttsEnabled is only set at the end of the first
invocation, so the footer bar would dispatch tts-speak twice.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
On mobile tablets/foldables (Android or iOS) in portrait, the viewport
width can exceed the Tailwind \`sm:\` breakpoint (640px), causing the
desktop footer bar to show and hiding the brightness / font size /
color / progress panels that only exist in the mobile layout.
The previous fix in #3746 introduced a regression on phones: wrapping
MobileFooterBar's children in a \`<div>\` changed the flex layout of
the footer container, and panels no longer slid cleanly behind the
navigation bar on dismissal. That PR was reverted.
This re-implementation scopes the override narrowly:
- \`forceMobileLayout\` is only true for mobile devices in portrait
with innerWidth >= 640. Phones (innerWidth < 640) always get
\`false\`, so every \`!forceMobileLayout && '…'\` expression
evaluates to the original class string — phone classNames are
set-equal to the pre-#3746 version.
- MobileFooterBar keeps its Fragment return; no wrapper div is
introduced anywhere in the tree, preserving the panel slide-down
animation exactly as before.
- DesktopFooterBar, NavigationBar, and the three panels
(ColorPanel / FontLayoutPanel / NavigationPanel) gate their
\`sm:hidden\` / \`sm:flex\` classes on \`!forceMobileLayout\` so
they correctly show/hide on wide portrait tablets.
- The orphaned \`.force-mobile-layout\` CSS override in globals.css
is removed since we no longer rely on a class-based escape hatch.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The sidebar delete handler always removed annotations through the note
bubble overlay key (`NOTE_PREFIX + cfi`), but highlights are keyed by
the raw CFI. Deleting a highlight from the sidebar therefore left its
overlay drawn until the book was reopened, while the popup path — which
lets `wrappedFoliateView` default the value to the CFI — worked.
Introduce `removeBookNoteOverlays` that mirrors the draw filters in
`Annotator.tsx` and clears every overlay a BookNote can own (highlight
and/or note bubble), and route the sidebar delete through it.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use string-parsed year to avoid UTC/timezone boundary issues
* feat: Add option to split words in RSVP mode
* fix: change the checkbox to a toggle
* fix(rsvp): speed up countdown from 2.4s to 1.5s
* feat(rsvp): skip to next page when no readable text is found
* fix(rsvp): replace lookbehind regex with lookahead-only split in getHyphenParts
* fix: add highlight color label fields
* fix: add default highlight label sync fields
* fix: add highlight prefs sync helpers
* fix: add highlight color name inputs
* fix: persist and sync highlight color names
* fix: add highlight color label helpers
* fix: add long press highlight label preview
* fix: pull highlight color prefs during library sync
* test: cover highlight color label helpers
* fix: widen highlight color name inputs
* fix: show highlight names on hover and touch hold
* fix: prevent highlight name input overlap
* fix: improve highlight name input responsiveness
* fix: support drag scrolling for highlight colors
* fix: batch custom color and label updates
* fix: serialize highlight prefs saves
* fix: align color strip drag and restore color clicks
* fix: translate default highlight color labels
* refactor: remove highlight preference sync wiring
* fix: align highlight option i18n with existing pattern
* fix: remove redundant english highlight keys
* fix: support raw and normalized highlight label keys
* fix: use underscore translator in highlight options
* fix: translate custom highlight color labels in editor
* refactor: simplify highlight settings persistence
* fix: maintianer review
* refactor: simplify highlight prefs save and clean up editor
- Drop the skipUserColors/skipLabels options from handleHighlightPrefsChange
and always persist both arrays; the flags only masked a no-op caller.
- Type handleHighlightColorsChange with Record<HighlightColor, string>
instead of typeof so the signature reads clearly.
- Remove the always-true `|| true` guard around the custom colors section.
- Stop wrapping user-typed custom color labels with _(), matching the
built-in color inputs and keeping the input value equal to what the
user typed.
* refactor: couple highlight labels to their colors
Replace the parallel `highlightColorLabels: Record<string, string>` map
with label storage that lives next to each color. This removes the hex
key normalization layer and its whole class of orphan/case-drift bugs.
Data model:
userHighlightColors: string[] -> UserHighlightColor[]
({ hex, label? })
highlightColorLabels: Record<string, string> -> (removed)
defaultHighlightLabels:
Partial<Record<DefaultHighlightColor, string>>
A `migrateHighlightColorPrefs` helper runs during `loadSettings` and
handles both the shipped `string[]` layout and the draft-build
`highlightColorLabels` layout: hex-keyed labels attach to matching user
colors, name-keyed labels move into `defaultHighlightLabels`. Malformed
entries are dropped.
Editor:
- Label inputs commit on blur (Enter also commits), so typing a long
label no longer fires `setSettings`/`saveSettings` on every
keystroke. A small `LabelInput` component owns the draft state and
syncs if the prop changes externally.
- Three explicit callbacks (`onCustomHighlightColorsChange`,
`onUserHighlightColorsChange`, `onDefaultHighlightLabelsChange`)
replace the previous single callback with opaque skip flags.
Picker:
- Extracted a reusable `useDragScroll` hook (mouse only, 6px
threshold, 120ms click suppression) from the inline state machine
in `HighlightOptions`. The picker drops to ~280 lines.
- Long-press preview stays touch/pen only. It now reads labels via
`getHighlightColorLabel` (which returns `undefined` when no user
label is set), letting the component layer decide whether to fall
back to a translated default name.
i18n:
- Default color names ('red' | 'yellow' | 'green' | 'blue' | 'violet')
are registered once at module scope via `stubTranslation` and
translated at the picker layer through `useTranslation`. User-typed
labels are never run through `_()`, so what the user types is what
the editor shows.
Tests:
- `annotator-util.test.ts`: rewritten around the new helper contract
(user label -> undefined fallback) and case-insensitive hex matching.
- `settings-highlight-migration.test.ts`: new, covers legacy
`string[]`, already-migrated entries, malformed hex filtering, and
the two draft-label fold paths.
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
On Android tablets/foldables in portrait, screen width can exceed 640px
causing the desktop footer bar to show. Now use portrait orientation
detection to force the mobile footer bar layout on Android regardless
of screen width.
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>
Add "Full sync all annotations" menu item that pushes and pulls all
annotations regardless of the last sync timestamp, enabling users to
sync old highlights that were created before the plugin was installed.
Changes:
- Add full_sync parameter to push/pull that bypasses timestamp filter
and uses since=0 for pulling all server annotations
- Deduplicate by annotation ID alongside position-based dedup
- Store server ID on pulled annotations and reuse it when pushing
- Parse ISO 8601 timestamps from server to preserve original
created/updated dates instead of using current time
- Resolve KOReader page numbers from xpointers via getPageFromXPointer
- Resolve Readest page numbers from CFI via getCFIProgress on pull
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>