Insert a "Current position" row in the TOC sidebar directly under the
highlighted section, indented one level deeper, with an open-book icon
and the live reading page number. Clicking it navigates to the exact
current reading location (progress.location) — distinct from the section
header, which jumps to the section start.
Implemented via a pure buildTOCDisplayItems() helper that injects the
synthetic row after the active item, keeping the active item's index
stable so the existing TOC auto-scroll logic stays untouched. The page
number uses the same muted color as the other rows.
Also fills in the missing "File Path" i18n translations across all
locales (surfaced by i18n:extract) and records project memory notes.
Closes#4513.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Virtualizing BooknoteView (#4352) dropped the auto-scroll that centers the
note nearest the current reading position, leaving the list stranded at the
top. The single scrollToIndex that replaced the per-item useScrollToItem was
missing the machinery TOCView already uses for virtualized auto-scroll:
- Re-apply the scroll inside the OverlayScrollbars `initialized` callback
(read via a ref): its deferred init resets the viewport scrollTop to 0, and
the lastScrolledCfiRef guard otherwise blocked any retry (reload case).
- Mount Virtuoso natively centered via initialTopMostItemIndex with a
skip-gate, so opening the panel while reading doesn't fire a scrollToIndex
that races and wedges the freshly mounted, unmeasured list (tab-switch case).
- Jump instantly (behavior 'auto') for far moves and on eink, animating
'smooth' only for short in-session updates — mirroring TOCView.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(agent): add agent notes for cache, reading-ruler, foliate touch
Add project-memory notes and index entries:
- manage-cache-ios-layout: iOS container layout and what Manage Cache clears
- reading-ruler-line-aware: line/column-aware reading ruler internals
- foliate-touch-listener-capture-phase: capture-phase gesture suppression
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(reader): pad sidebar and notebook for the device status bar (#4089)
Top-anchored slide-in panels (sidebar, notebook) only applied status-bar
top padding when isFullHeightInMobile was true. On a tablet/desktop
(isMobile === false) that gate collapsed the padding to 0, so a visible
system status bar overlapped the panel's top toolbar and made its icons
inaccessible.
Extract the inset math into getPanelTopInset() and gate it on
(!isMobile || isFullHeightInMobile) so non-mobile panels clear the status
bar like the reader header, while a partial-height mobile bottom sheet
(which doesn't reach the top of the screen) stays flush.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(reader): keep footer bar clear of the pinned sidebar
On a mobile tablet in portrait, forceMobileLayout renders the footer bar
with position: fixed, anchored to the viewport, so left-0 w-full spans
the whole window and slides under a pinned sidebar — the progress / font
/ TTS controls end up obscured.
Anchor the footer inside the book's grid cell (position: absolute) when
the sidebar is pinned, mirroring the header bar. The flex layout already
offsets the grid cell by the sidebar's real rendered width, which honors
the sidebar's min-w-60 floor and 45% cap that a stored-width offset would
miss. The slide-up panels are absolute within the footer container, so
they shift and narrow with it and their animation is unchanged. The
switch only happens when the sidebar is pinned, so phone (< 640px) and
unpinned tablet-portrait class names stay identical.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(ui): refine reader side panels and their empty states
Closes#4089
Add a shared EmptyState component (large muted icon, title, and an
optional hint or action) and use it for the empty annotations, bookmarks,
and notes panels in the sidebar and notebook, replacing the ad-hoc
"No … yet" placeholders.
Polish the surrounding chrome: switch the bookmark toggler to the Ri icon
set with responsive sizing, crop the HighlighterIcon viewBox to its
artwork to remove the asymmetric bottom padding, and tune mobile sizing
and spacing across the panel headers, tab navigation, and footer nav bar.
Translate the new empty-state strings (No Notes, No Annotations, No
Bookmarks, and their hints/action) across all 33 locales and drop the
obsolete "No … yet" keys.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Switching the annotation/bookmark sidebar to a flat virtualized list eliminates
the per-item layout reads that caused multi-second jank when toggling tabs on
books with hundreds of notes.
A. Virtualize the list with react-virtuoso
- Flatten group headers + notes into a single FlatBooknoteRow array.
- Embed an OverlayScrollbars instance inside the tab so scrollbar styling
is preserved while Virtuoso owns the viewport (same nested pattern as
TOCView).
- Track the parent scroll-container's height with ResizeObserver to give
Virtuoso a bounded viewport.
- Replace the per-item useScrollToItem (which called getBoundingClientRect
and closest() on every BooknoteItem on every progress tick — O(n) sync
reflow on 1000+ items) with a single virtuosoRef.scrollToIndex driven by
nearestCfi.
B. Stabilize derivations with useMemo / useCallback
- filteredNotes, sortedGroups, flatItems, nearestCfi all useMemo so an
unrelated config change (e.g. viewSettings autosave) no longer triggers
a full sort + group rebuild.
- handleBrowseBookNotes is now useCallback so BooknoteItem's React.memo
can hit on prop equality.
C. Memoize BooknoteItem
- Wrap the component in React.memo. With stable item / onClick references
from the parent, re-renders triggered by sibling progress updates no
longer cascade across every visible row.
- Cache marked.parse(item.note) and dayjs(item.createdAt).fromNow() in
useMemo. marked is the dominant per-render cost for note rows.
- isCurrent moves to a useMemo over isCfiInLocation; the per-item
scrollIntoView is removed since BooknoteView now drives scrolling.
useScrollToItem is intentionally left intact — SearchResults still uses it
and its smaller list does not exhibit the same jank.
On a hard refresh the TOC sidebar occasionally (~1 in 10) scrolled to and
highlighted the current chapter, then rewound to the very top of the list.
It is a scroll-position race in TOCView, not a progress/sectionHref reset (the
reading position stays correct throughout). OverlayScrollbars resets the wrapped
Virtuoso viewport's scrollTop to 0 when it initializes (deferred). Its
`initialized` callback re-scrolled only to `initialScrollTarget.index`, captured
at mount — and on a fresh refresh `progress` is not available yet, so that index
is 0 and the reset is never corrected. Whether OverlayScrollbars initializes
before or after the auto-scroll to the reading position is the timing race that
made it intermittent.
Re-apply the scroll to the current active item (via refs mirroring the live
flatItems/activeHref) in the `initialized` callback, falling back to the
mount-time index. Refs are used because OverlayScrollbars binds the callback at
mount and fires it later, so it must read the latest active item.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(export): make annotation export link type configurable
Add an Annotation Link selector (App / Web) to the Export Annotations
dialog. Defaults to the app deeplink in the native app and the universal
web link on the web, so web exports no longer emit readest:// links that
only the desktop/mobile app can open. The default markdown template now
uses the configurable annotation.link variable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(annotations): move Moon+ Reader import into a dedicated Import Annotations modal
Replace the single 'Import from Moon+ Reader' menu item with an 'Import
Annotations' entry (below 'Export Annotations') that opens a dedicated
modal listing import sources. Currently lists Moon+ Reader; the boxed-list
layout makes adding future providers a one-row change.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add empty state hints and loading indicators for annotations, bookmarks, notes, font import, and Moon+ Reader import
- BooknoteView: show 'No annotation yet' / 'No bookmark yet' when empty
- Notebook: show 'No note yet' when no notes/excerpts exist
- Annotator: add loading overlay with spinner during Moon+ Reader import
- mrexpt: yield to event loop every 5 entries to keep spinner animating
- CustomFonts: show in-place loading card during font import, spinner transitions to font name without layout jump
* fix(ui): respect e-ink overlay styling and drop dead className branch
- Annotator: use modal-box on the mrexpt import overlay so eink picks up
the no-shadow + 1px border override automatically.
- CustomFonts: collapse importing-card clsx ternary whose branches were
identical into a flat className.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The floating 'New Chat' button in the chat history sidebar suffered from
two issues on mobile:
1. On Android (e.g. Pixel 9 with the gesture pill) the button rendered
underneath the system navigation indicator because its position used
plain bottom-4 with no safe-area inset.
2. With bg-base-300 / text-base-content the pill could collapse to a
nearly invisible solid black shape under some themes / contexts where
text-base-content was inherited as a near-background color, hiding
the icon and label entirely.
Fixes:
- Offset the wrapper by env(safe-area-inset-bottom) + 1rem so the button
sits above the Android gesture pill and iOS home indicator.
- Switch the button to bg-primary / text-primary-content with shadow-md
to guarantee strong contrast across all themes.
- Add pointer-events-none on the positioning wrapper and pointer-events-auto
on the button itself so the floating layer never blocks list interaction.
* fix(macos): place traffic lights via Tauri trafficLightPosition
Replaces the cocoa private-API positioning that drove traffic light placement through IPC with Tauri's supported trafficLightPosition window option, which routes through wry's macOS API and stays correct across versions including macOS 26 (Tahoe).
Position is now declared once at window creation: WebviewWindowBuilder.traffic_light_position in src-tauri/src/lib.rs for the initial main window, and trafficLightPosition on new WebviewWindow(...) in utils/nav.ts for reader windows and the recreated main window. The reader path mattered — those windows used to rely on the cocoa hack to place buttons after the on_window_ready hook fired, so any path that bypassed it left the buttons in AppKit's overlay default position (off-screen on macOS 26 until a resize).
The IPC surface narrows accordingly. set_traffic_lights now takes only visible: position is no longer a parameter and the WINDOW_CONTROL_PAD_X/Y static muts go away; setTrafficLightVisibility drops its position arg in trafficLightStore; useTrafficLight and HeaderBar drop their hard-coded { x: 10, y: 20 } magic numbers. position_traffic_lights stops touching the per-button NSWindowButton frames entirely and only collapses or restores the title-bar container view to hide / show buttons during reader chrome auto-hide. A short-circuit on the no-op transition keeps the cocoa setFrame from racing AppKit's own traffic-light tracking on every IPC call.
useTrafficLight stays — it still owns full-screen visibility synchronisation, the auto-hide visibility toggle, and feeds isTrafficLightVisible to the self-drawn <WindowButtons /> in the auth, library, OPDS, reader-sidebar, and user headers. None of those have an equivalent in the new declarative API. Only its 'where do the buttons sit' responsibility was moved out.
A single named constant TRAFFIC_LIGHT_RESTORE_Y_INSET is left behind in traffic_light.rs, used solely by the visible: false → true restore path to recompute the title-bar container height. It must agree with the y component of the two declarative trafficLightPosition values; a doc comment makes that contract explicit. Caching each window's natural title-bar height before the first collapse would let us delete the constant entirely, but the per-window state machine that requires is not worth the win for a single number.
y is tuned by eye to 24 to vertically center the buttons inside readest's ~48px header bar on macOS 26.1.
* fix(macos): center traffic lights from live AppKit offset, no version check
Restores the pre-PR cocoa-driven positioning that worked on macOS 15
while keeping the macOS 26 fix this PR was originally about: the
plugin owns `position_traffic_lights`, which now sizes the title-bar
container *and* sets each window button's frame.origin on every
on_window_ready / resize / theme-change / full-screen-exit event. Tao's
runtime `inset_traffic_lights` never fires (we never declare
`trafficLightPosition` or call `set_traffic_light_position`), so there
is no second code path fighting us on drawRect.
The y inset that visually centers the close button is computed at
runtime as
y = (header_height - button_height) / 2 + button_origin_y
where `button_origin_y` is the close button's natural rest position
inside the title-bar container. Apple shifted that rest position by
~2pt on macOS Tahoe (26), so the same formula yields y=22 on macOS 15.6
and y=24 on macOS 26.1 with a 48px header — no `NSProcessInfo` lookup
and no hardcoded per-OS offset. The natural origin.y is read once and
cached via `OnceLock` so any post-resize autoresize that AppKit might
apply doesn't feed back into the centering math.
Frontend plumbing: `set_traffic_lights` IPC now carries `headerHeight`;
the zustand store remembers it across visibility toggles; the
`useTrafficLight` hook accepts a header ref, mirrors `ref.current`
into local state (so the effect re-runs when LibraryHeader's
conditional render flips the ref from null to the live node), measures
the border-box height on mount, and observes via ResizeObserver to
re-push on responsive breakpoint / safe-area changes. LibraryHeader,
sidebar Header, OPDS Navigation, and the reader HeaderBar each pass
their own ref so y is computed against the chrome each page actually
renders.
Library header is normalised to h-[44px] desktop to match the reader's
h-11 and drops the `-2px` macOS marginTop workaround, since the runtime
centering removes the need for it.
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
* feat(reader): import annotations from Moon+ Reader (.mrexpt)
Add a new menu entry under the reader sidebar 'More' menu that lets users import highlights and notes exported from the Moon+ Reader Android app.
Implementation:
- utils/mrexpt.ts: parser for the .mrexpt plaintext format (entry id, NCX navPoint index b4, character offset b6, type marker, word and note).
- services/annotation/providers/mrexpt.ts: convert mrexpt entries to BookNote[] using bookDoc. Locate the chapter via b4 -> toc -> spine, then TreeWalker-search the section DOM for the highlighted word with English suffix tolerance (ing/ed/s/...). Falls back to a section-level CFI when the exact word can't be located. Re-imports are deduplicated by a stable id derived from entryId.
- BookMenu: add 'Import from Moon+ Reader' menu item dispatching the 'import-mrexpt' event.
- Annotator: handle 'import-mrexpt' — pick the file (Web File / Tauri path), parse, convert against the live bookDoc, merge into booknotes (latest updatedAt wins), persist via saveConfig, and apply to all live views so highlights appear immediately. User feedback via toasts (importing / imported N / N unmatched / nothing new).
* refactor(reader): simplify Moon+ Reader import notifications
Reworks the .mrexpt import UX so it shows exactly one toast per run
instead of up to two, and removes redundant intermediate notices.
- Drop the intermediate "Importing N annotations…" toast. The toast
system shows one toast at a time, so it merely flashed and was
replaced by the result toast.
- Drop the duplicate "Failed to read the selected file." toast in the
read catch block; it falls through to the existing empty-content
check which surfaces the same message.
- Collapse the three-way result toast (already imported / N unmatched /
N imported) into one: "Imported {{count}} annotations" or
"No new annotations to import".
- Fix a result-message bug: when every converted note was already
imported and nothing was unmatched, the toast read "Imported 0
annotations." It now reports "No new annotations to import".
- Pluralize the success message via i18n `count` (the previous `{{n}}`
placeholder never pluralized, e.g. "Imported 1 annotations").
- Extract the dedupe/merge logic into a pure, unit-tested
`mergeImportedBookNotes` helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(i18n): translate Moon+ Reader import strings
Run i18next extraction and translate the new .mrexpt import strings
across all 33 locales (340 keys). The import feature added in this PR
introduced translatable strings that had not yet been extracted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a "Clear Annotations" item to the book menu. Picking it opens a confirm dialog and, on confirm, soft-deletes every type='annotation' booknote on the active book by stamping deletedAt, removes overlays from live views, persists via saveConfig, and resets sidebar browse state. Bookmarks and excerpts are untouched. The dialog lives in Annotator (per-book, long-lived) and is wired up via a new 'clear-annotations' event so it survives the dropdown menu unmounting.
* ui/ux: codify design system and migrate settings to shared primitives
Document Readest's design language in DESIGN.md (Adwaita-aligned, e-ink-first,
RTL-correct) and migrate every settings panel onto a small set of primitives
(BoxedList, SettingsRow, SettingsSwitchRow, SettingsSelect, SettingsInput,
NavigationRow, Tips, SubPageHeader). AGENTS.md links to DESIGN.md so contributors
land there before inventing new chassis classes.
Replace the standalone KOReader/Readwise/Hardcover Config dialogs with a single
Integrations panel (Reading Sync + Content Sources sub-pages). The reader's
BookMenu now hides each provider until it's configured, and Hardcover's per-book
"Enable for This Book" toggle is dropped — there's no auto-sync to gate, so the
flag was just extra clicks.
Refresh highlight colors (two-trigger swatch + label, translatable default
names), background texture / theme color selectors (border-current keeps
selection legible on any backdrop), CustomFonts/CustomDictionaries (quiet
list-extension style + shared Tips primitive), the OPDS catalog manager
(debounced auto-download, right-aligned Browse), Set PIN, and the KOSync
conflict resolver. Translate the ~30 new strings across all 33 locales.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ui/ux: responsive typography, OPDS card polish, deep-link return paths
Restore the .settings-content responsive cascade (14px desktop / 16px
mobile) the legacy panels relied on by dropping hardcoded `text-sm`/`text-xs`
from the new primitives. Secondary text moves to em-relative `text-[0.85em]`
so it scales with the parent. Form controls (`<input>`, `<select>`) re-apply
the cascade explicitly via the `settings-content` class since browsers don't
inherit font-size onto form elements.
Extract `<SectionTitle>` primitive (caseless-language aware via
`isCaselessUILang`/`isCaselessLang`) and route every uppercase tag-style
header through it: BoxedList groups, Reading Sync, Content Sources, Theme
Color, Background Image, integration form labels, KOSyncResolver device
labels, and the OPDS My Catalogs / Popular Catalogs sections. CJK / Arabic
/ Hebrew / Indic / Thai / Tibetan locales bump to `1em` since `uppercase`
is a no-op on those scripts.
Redesign the OPDS My Catalogs cards: whole card becomes the browse trigger
(role='button'), edit/delete collapse into a 3-dot dropdown menu, and the
sync-status moves to a sub-line under Auto-download so the card height stays
constant whether the toggle is on/off or sync data has arrived.
Plumb a `from=settings-integrations` URL marker through the OPDS browser so
both manual close and auto-close-on-failure (preserved as `router.back()`
for transient failures, paired with a new `stashOPDSReturnTarget` helper)
return the user to Settings -> Integrations -> OPDS Catalogs sub-page
rather than the dialog's top level. Backed by new `requestedSubPage`
deep-link store field.
Skip the OPDS catalog passphrase prompt when credentials sync is disabled
-- `replicaPublish` already drops encrypted fields at the wire, so prompting
was both pointless and confusing.
Fix `SettingsDialog` calling `setRequestedPanel(null)` inside a `useState`
lazy initializer (zustand setter during render -> React warning); move the
clear into a one-shot `useEffect`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ui/ux: opt Settings into OverlayScrollbars + caseless typography polish
Add an opt-in `useOverlayScroll` prop to `<Dialog>` that swaps the body's
native `overflow-y-auto` for `<OverlayScrollbarsComponent>` (autohide,
click-scroll, no native overlaid bars). SettingsDialog flips it on so the
long Layout / Color panels keep a visible, theme-aware scroll track on
Android / iOS webviews where native scrollbars auto-hide entirely. Other
short-modal callers stay on the native scrollbar.
Drop the `uppercase tracking-wider` SectionTitle styling for caseless
scripts and pair it with body-weight `font-medium` instead — those
typographic effects are no-ops on Han / Hangul / Devanagari / Thai etc.,
so a plain medium-weight body-size title reads more correctly than a
shrunken pseudo-uppercase one. SettingsRow / NavigationRow primary labels
follow the same rule (drop `font-medium` in caseless locales since the
inherited body weight already carries; CJK fonts bold poorly at body
size).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ui/ux: SettingLabel primitive + KOSyncForm select polish + Tips alignment
Add `<SettingLabel>` primitive — caseless-aware row/field label that pairs
with `<SectionTitle>` (groups) for per-item labels. Cased scripts get
`font-medium`; caseless scripts (CJK / Arabic / Hebrew / Indic / Thai /
Tibetan) drop the weight since Han / Hangul / Devanagari etc. bold poorly
at body size. No font-size class so it inherits the `.settings-content`
14/16 cascade. Routed through `SettingsRow`, `NavigationRow`, and the
~12 ad-hoc inline `text-sm font-medium` callsites in AIPanel / FontPanel
/ ColorPanel / IntegrationsPanel / KOSync / Readwise / Hardcover forms.
Refactor KOSyncForm's Sync Strategy + Checksum Method rows onto the
shared `<SettingsSelect>` primitive — the inline 17-line div/select/
MdArrowDropDown chassis becomes a single SettingsSelect call with an
options array. Drops the unused MdArrowDropDown import and ~25 lines.
Fix Tips list-item alignment: callers traditionally pass `<li>` elements
(semantic) but the primitive was double-wrapping into `<li><span><li>...</li></span></li>` — invalid HTML, and the inner `<li>`'s
`display: list-item` broke line-wrap alignment on multi-line items.
Unwrap caller `<li>` to its content; add `flex-1` on the text span so
wrapped lines align under the first line instead of falling back to the
bullet column. Bullet container switches to `h-[1.4em]` so it tracks the
text line-height and pins to the first line's optical center via
`items-center` regardless of how much the content wraps.
DESIGN.md §5 typography updated to point primary-label callers at
`<SettingLabel>`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
* 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>
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>
* 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
Extract useSwipeToDismiss and usePanelResize hooks from duplicated code
in SideBar and Notebook. Add mobile swipe-to-dismiss drag handle to
Notebook matching SideBar's existing behavior. Fix drag cursor showing
col-resize instead of row-resize during vertical drags.
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: bump nodejs version to 24
* fix(sidebar): use position fixed and transform for mobile sidebar
Use position: fixed to prevent horizontal scrolling on the mobile
bottom sheet, and replace style.top with transform: translateY() for
smooth drag performance. Cache element refs to avoid
document.querySelector on every drag frame. Apply the same position:
fixed fix to the notebook panel. Closes#3492
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(library): implement grouping of books by series or author
- Added functionality to group books in the library by series or author.
- Introduced new utility functions for creating book groups and parsing author strings.
- Updated the LibraryPageContent to track the current group for navigation.
- Created a new GroupHeader component to display the current group context.
- Enhanced the Bookshelf component to support displaying grouped books.
- Updated settings to include grouping options in the view menu.
New files:
- src/__tests__/utils/libraryUtils.test.ts - Unit tests for new utility functions.
- src/app/library/components/GroupHeader.tsx - Component for displaying group context.
- src/app/library/utils/libraryUtils.ts - Utility functions for grouping and parsing authors.
* Use group by and sort by constants, and split sort/group by menus into their own elements
* Format code with autoformatter
* Remove any casting from tests
* Translate missing strings
* refactor: cleaner layout with collapsible view menu options
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Move Radix/shadcn-based UI primitives into a dedicated
`components/primitives` directory to better reflect
their low-level, composable nature.
Imports remain ergonomic via an alias to `components/ui`.
App-level components should continue to use PascalCase filenames.
* refactor(ai): remove chat icons from conversation list items
* feat(ai): add scroll-to-bottom button with animations and dynamic spacer
* fix(ai): align connection status icon with text in settings
* feat(ai): scroll to bottom on chat panel open and improve scroll behavior
* feat(ai): add loading overlay with blur transition for chat history
* feat(ai): pass loading state through component chain for history loading
* feat(ai): add dependencies
* chore: bump zod version to default version
* feat(ai): define types and model constants
* feat(ai): ollama provider for local LLM
* feat(ai): implement openrouter provider for cloud models
* feat(settings): register ai settings panel in global dialog
* refactor(ai): expose provider factory and service layer entry point
* test(ai): add unit tests for the providers
* test(ai): add unit tests for the providers
* feat(ai): settings panel for ai configurations
* refactor(ai): rewrite aipanel with autosave and greyed out disabled state
* fix: remove unused onClose prop from aipanel
* test(ai): update mock data
* refactor(ai): remove models
* refactor: use centralised defaults in system defaults
* chore(ai): remove comments
* fix(ai): merge default ai settings on load to prevent undefined values
* refactor(ai): rewrite settings panel with autosave and model input
* feat(ai): add ai tab with simplified highlighting
* feat(sidebar): render AIAssistant for ai tab
* feat(ai): add chat UI
* feat(ai); add chat service with RAG context
* feat(ai): temp debug logger
* feat(ai): add RAG service
* feat(ai): add text chunking utility
* feat(ai): add structured method
* feat(ai): add chatstructured method
* feat(ai): add rag types nd structured output schema
* feat(ai): add aistore, indexdb, bm25
* fix: update lock file
* feat(ai): update types for AI SDK v5
* feat(ai): add placeholder gateway model constants
* refactor(ai): update OllamaProvider for AI SDK
* feat(ai): add native gateway provider
* refactor(ai): update provider exports
* refactor(ai): use streamText from AI SDK
* refactor(ai): use embed from AI sdk
* refactor(ai): update provider factory exports
* feat(ai): add AI Elements and shadcn components
* config: add shadcn component config
* deps: add AI SDK and AI Elements dependencies
* config: add ai packages to transpilePackages
* refactor(ai): remove OpenRouterProvider and old tests
* feat(ai):add assistant-ui components
* feat(ai): add TauriChatAdapter for assistant-ui runtime
* refactor(ai): remove ai-elements components
* dep(ai): install assistant-ui and update next config
* chore(ai): export adapters from service index
* feat(ui): enhance ui components for assistant integration
* feat(settings): migrate ai settings to gateway
* feat(sidebar): integrate assistant-ui
* feat: add ai settings toggle to sidebar content
* feat: conditionally show ai tab in sidebar navigation
* feat: update ai model constants for cheaper options
* feat: add gateway provider with proxied embedding
* feat: add timeouts to ollama provider health checks
* feat: add retry logic to rag service embeddings
* feat: add error recovery to ai store
* feat: add ai feature tests
* feat: add ai api endpoints
* feat: add proxied gateway embedding provider
* feat: add ai runtime utilities
* feat: add ai retry utilities
* feat: add tauri env example template
* feat: add web env example template
* chore: add env
* feat(ai): update models and pricing, remove GLM-4.7-FlashX
* feat(ai): improve system prompt with official headings and no numeric citations
* feat(ai): optimize system prompt for tauri chat
* feat(ui): refine ai chat UI and relocate sources
* feat(ui): update ai settings panel with model pricing and custom model support
* feat(ai): add custom model support to ai settings
* test(ai): update constants tests for removed model
* feat(api): implement ai chat proxy route
* feat(api): implement ai embedding proxy route
* feat(ai): implement ai gateway health check and proxy logic
* feat(ai): simplify proxied embedding provider
* feat(ui): improve markdown text rendering
* feat(ui): add input group component
* test(ai): update ai provider tests
* feat(ai): add pageNumber to text chunk schema
* feat(ai): implement page-based chunking with 1500 char formula
* feat(ai): bump db to v2 and add store reset migration
* feat(ai): transition rag pipeline to page level spoiler filtering
* feat(ai): overhaul readest persona and antijailbreak prompt
* feat(ai): update tauri adapter for page tracking and persona
* chore(ai): export aiStore and logger from core index
* feat(reader): integrate page tracking and manual index reset
* feat(ui): add re-index action and reset logic to chat
* chore: sync pnpm lockfile with ai dependencies
* feat(utils): add browser-safe file utilities for web builds
* refactor(utils): use dynamic tauri fs import to prevent web crashes
* refactor(services): defer osType call to init() for web compatibility
* refactor(services): import RemoteFile from file.web
* refactor(services): import ClosableFile from file.web
* fix(libs): cast Entry to any for getData access
* fix(annotator): cast Overlayer to any for bubble access
* refactor(ai): replace SparklesIcon with BookOpenIcon for index prompt
* test(ai): add pageNumber to TextChunk mocks
* test(ai): fix chunkSection signature in tests
* chore: update files
* fix(ai): prevent useLocalRuntime crash when adapter is null
* refactor: optimize annotator overlay drawing
* feat: stabilize AI assistant runtime and adapter
* refactor: improve document zip loader type safety
* feat: update tauri chat adapter for dynamic options
* fix: restore architecture comments and refine platform properties
* build: update lockfile with assistant-ui patch
* fix(library): patch @assistant-ui/react for runtime initialization
* build: update dependencies in readest-app
* build: update root dependencies and patch configuration
* fix(ai): patch @assistant-ui/react for thread deletion and runtime init
* fix(ai): update assistant-ui patch with dist guards and deletion fallback
* build: sync lockfile with assistant-ui patch updates
* chore(env): update .gitignore by removing .env files from it
* chore(env): update .gitignore by adding .env.local
* chore(env): update .gitignore by adding .env*.local
* fix: restore static osType import
* chore: sync submodules with upstream/main
* refactor: remove redundant file.web module and revert import
* chore: update pnpm-lock.yaml
* refactor: revert guards
* refactor; remove deprecated codes and extract prompts.ts
* refactor(ai): remove unused ragservice exports
* refactor: remove unused ollama and embedding models
* refactor: remove unused type
* test: remove test for the now deleted constants
* refactor: remove unused export
* style: fix ui component formatting
* style: fix core and style file formatting
* test: fix broken ai provider import
* fix: typescript error
* fix: add eslint disable command
* fix(deps): remove unused ai sdk provider util after v6 ai sdk migration
* fix(patch): add lookbehind regex patch
* feat(dep): upgrade vercel ai sdk to v6 and ai-sdk-ollama to v3
* chore: update lockfile for vercel ai sdk v6
* refactor(ai): remove EmbeddingModel generic for ai sdk v6
* refactor(ai): remove EmbeddingModel generic for ai sdk v6
* test(ai): update mock to use embeddingModel
* fix(patch): add lookbehind regex patch for email autolinks in markdown
* refactor(ai): use ai sdk v6 syntax
* fix: prettier formatting
* chore: revert cargo.lock
* fix(ai): update proxied embedding model to v3 spec
* feat(ai): add aiconversation types for chat persistence
* feat(ai): add conversation/message indexeddb and crud operations
* feat(ai): create aiChatStore zustand store for chat state management
* feat(notebook): add notebookactivetab state for Notes/AI
* refactor(ai): refine conversation and message types for persistence
* feat(types): add notebookActiveTab to ReadSettings type
* chore: update deps
* feat: add notebookactive tab default value
* feat: add hook for ai chat
* feat: update left side panel with history/chat icon
* feat: integrate ChatHistoryView into sidebar content
* feat: create UI for managing AI chat history
* feat: implement persistent history with assistant-ui adapter
* feat: create tab navigation component for notes and AI
* feat: add tab navigation and AI assistant view
* feat: update header to display active tab title
* fix: formatting
* feat: remove title and update new chat button
* fix: formatting
* fix: revert tooltip and styling
* feat: implement cross-platform ask dialog bridge
* feat(ai): preserve history during ui clear & use native dialogs
* fix: align notebook navigation height with sidebar tabs
* fix(ai): add missing dependency to handleDeleteConversation hook
* docs: update PROJECT.md with session highlights
* chore: delete projectmd
* chore: update package.json and lock file
* chore: update package.json
* chore: remove patch
* chore: upgrade react types to 19 and show ai features only in development mode for now
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>