* 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>
* feat(annotations): deep links for highlight exports
Embed an HTTPS deep link in markdown export so clicking a highlight in
Obsidian / Notion / Mail launches Readest at the exact CFI position.
Mobile App Links / Universal Links open the native app silently when
installed; desktop attempts the readest:// scheme automatically with a
manual fallback.
- Markdown export wraps the page-number text in a per-annotation link:
https://web.readest.com/o/book/{hash}/annotation/{id}?cfi=...
- New /o/... smart landing page handles platform routing (intent:// on
Android Chrome, scheme + visibility-cancel on other Android, auto
scheme + 1 s fallback on desktop, manual button on iOS).
- Reader honors a ?cfi= query param on initial load (overrides the
saved last-position for the primary book only).
- New useOpenAnnotationLink hook handles incoming readest:// and
https://web.readest.com/o/... URLs, including cold-start (getCurrent)
and library-load deferral; supports the legacy flat shape
readest://annotation/{hash}/{id} from previous Readwise syncs.
- ReadwiseClient now emits the HTTPS deep link instead of the legacy
custom scheme.
- AASA extended with /o/* matcher; Android intent-filter for the host
has no pathPrefix so it already covers it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* i18n: translate annotation deep-link strings across all locales
Translates the 13 new keys introduced for the annotation deep-link
feature into all 31 supported locales. Replaces all 403
__STRING_NOT_TRANSLATED__ placeholders.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(reader): custom dictionaries (StarDict + MDict)
Adds a pluggable dictionary provider system. Built-in Wiktionary +
Wikipedia (extracted from the legacy single-popup model into a tabbed
shell) plus user-importable StarDict (.ifo/.idx/.dict.dz/.syn) and MDict
(.mdx/.mdd) bundles.
Settings → Language → Dictionaries: import / enable / drag-reorder /
delete (delete-mode toggle mirrors CustomFonts). Drag uses @dnd-kit with
pointer/touch/keyboard sensors. Reader popup: tabbed UI, per-tab lookup
history, scroll-aware back button, last-active tab persists. Tabs grow
to natural width up to a cap, truncate with ellipsis when crowded; phantom
bold layer prevents layout shift on focus.
StarDict reader is self-contained (replaces unused foliate-js/dict.js),
with lazy random-access binary search on .idx + .syn (~420 KB Int32Array
of byte offsets vs ~10 MB of parsed JS objects), lazy DictZip chunk
decompression via fflate streaming Inflate (cmudict/eng-nld both
chunked), and an optional .idx.offsets sidecar generated at import to
skip the init scan. Cmudict 105K-entry init drops from ~10 MB heap and
2 MB IO to ~1.7 MB heap and ~500 KB IO.
MDict uses the readest/js-mdict fork (added as a submodule, consumed via
tsconfig paths so deps stay out of readest's pnpm-lock) which adds a
browser-friendly BlobScanner reading via blob.slice(...).arrayBuffer()
— slices are lazy when the Blob is Readest's NativeFile / RemoteFile.
encrypt=2 (key-info-only) MDX is fully supported via ripemd128-based
mdxDecrypt; encrypt=1 (record-block, needs user passcode) surfaces as
unsupported.
Wikipedia annotation tool removed (Wikipedia is now a tab inside the
unified popup); legacy WiktionaryPopup / WikipediaPopup deleted. Stale
annotationQuickAction === 'wikipedia' coerced to 'dictionary' on settings
load. iOS-friendly external links: skip target="_blank" on Tauri to
avoid the WebView's "open externally" path triggering the shell scope
error; the popup's container click handler routes through openUrl.
i18n: 939 strings translated across 31 locales (30 base keys + CLDR
plural forms for ar/he/sl/pl/ru/uk/ro/it/pt/fr/es).
Test fixtures bundled: cmudict (StarDict, 105K entries), eng-nld
(StarDict, smaller), and a Longman Phrasal Verbs MDX (encrypt=2).
3396 unit tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(stardict): resolve fflate from js-mdict source for vitest + Next
js-mdict is consumed as TypeScript source via tsconfig paths from
packages/js-mdict/src/. Its sources `import 'fflate'` directly, but
fflate is only installed under apps/readest-app/node_modules — so
vite's import-analysis (and Next/Turbopack's resolver) can't find
fflate when it walks up from the redirected js-mdict source location.
CI's fresh checkout exposes this; locally a leftover
packages/js-mdict/node_modules/fflate from the old workspace setup
masked it.
Pin fflate resolution to apps/readest-app/node_modules/fflate in:
- vitest.config.mts (Vite alias)
- next.config.mjs (webpack alias + Turbopack resolveAlias — Turbopack
rejects absolute paths so use a project-relative form)
- tsconfig.json (paths entry so tsgo / Biome see it)
Verified by deleting packages/js-mdict/node_modules locally and
re-running pnpm test (3396 pass), pnpm lint (clean), and both
pnpm build-web and a tauri-platform Next build (clean).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a book's underlying file is missing, opening it in a dedicated
reader window showed an error toast then navigated that window to
/library, leaving a leftover library-in-reader-window the user had
to close manually. Route the recovery through a new
closeReaderWindowOrGoToLibrary() that closes the dedicated reader
window (after ensuring the main library window is visible) and only
falls back to /library navigation in the main window or on web.
Also fix a related macOS issue: the reader's CloseRequested handler
was running handleCloseBooks and calling currentWindow.destroy() on
the main window, which tore down the active book and bypassed the
Rust close-to-hide handler — making Cmd+W / traffic-light close
quit the app from the reader page (vs. correctly hiding from the
library page) and lose the active book even when the window did
hide. Skip both the cleanup and destroy on macOS for the main
window so the Rust handler hides it with the book intact, matching
the macOS minimize-to-dock convention.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
On Android, long-press selects text via selectionchange while the finger
is still on the screen. The quick action handler was gated by
androidTouchEndRef and silently returned, so no popup ever opened. After
the user lifted, nothing re-ran the gated action.
Track the gated action in a small DeferredActionState ref and flush it
from the native touchend handler, so instant copy/dictionary/wikipedia/
search/translate/tts now fire on the first long-press release.
The browser delivers one large quantised delta per wheel notch, which
Chromium scrolls without interpolation — producing the jerky one-step
motion reported on Windows. Detect mouse-wheel-shaped events inside
the iframe (line-mode, or single-axis with |deltaY| ≥ 50), suppress
the native scroll, and replay the delta as an rAF exponential lerp on
the renderer's container. Trackpad / high-resolution input is left to
native scrolling so its momentum and 2-axis behaviour are preserved.
When the main window has been destroyed (Windows/Linux default close), the
reader's "go to library" button only closed the reader, leaving no library
visible. Add ensureMainLibraryWindow() that shows an existing main window
or recreates one with the 'main' label so the existing close-reader-window
wiring keeps working. Also grant the cross-window show/unminimize permissions
the call now needs.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* 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.
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>
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