cfe2bb9116f07620a0509da31108e58186d1a002
839 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
cfe2bb9116 | fix(reader): Android text selection breaks on the first word of hyphenated paragraphs (#4545) | ||
|
|
390c711070 |
feat(rsvp): configurable start delay, word stepping, context dictionary lookup, and keyboard shortcut (#4541)
- #4478: add a configurable pre-start countdown (Off / 1s / 2s / 3s, default 3s) that now ticks at honest one-second intervals and applies to start, resume, and page loads; Off starts instantly. - #4476: add manual next/previous word controls (buttons flanking Play plus the "," / "." keys) that pause playback and step exactly one word. - #4475: allow selecting text in the context panel to look it up in the dictionary (anchored popup on desktop, bottom sheet on small screens), reusing the reader's dictionary view; auto-scroll/seek are suppressed during selection and an outside click dismisses the popup. - #4473: add a Speed Reading keyboard shortcut (Shift+V), shown on the View menu item; ignore repeat triggers while a session is active. Adds i18n strings for the new UI across all locales. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
755bee1ee6 |
fix(reader): prevent accidental paragraph-mode exit and center its bar (#4474) (#4539)
Paragraph mode could be exited by accident just by tapping a bit too high or low on the screen: a tap on the empty area around the centered paragraph hit the overlay backdrop, which closed the mode. Tapping the neutral center of the paragraph did nothing, and once the control bar auto-hid there was no touch gesture to bring it back — so removing the stray-tap exits alone would have stranded touch users with no way out. - Backdrop and center-zone taps now dispatch `paragraph-show-controls` instead of exiting; the bar re-appears so the explicit exit button stays reachable on touch. - ParagraphBar listens for that event (scoped by bookKey) and re-shows. - Exit now only happens via the bar's exit button, Escape/Backspace, or a deliberate double-tap on the paragraph (kept as a power-user shortcut). - Center the bar with `fixed` instead of `absolute`: it was centered on the gridcell, which a pinned sidebar pushes off-center, while the paragraph centers on the viewport via the `fixed inset-0` overlay. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
cf41e7d50d |
feat(rsvp): apply reader font face/family settings to the RSVP word (#4519) (#4537)
RSVP displayed the focal word in a hardcoded monospace font, ignoring the reader's configured font. Resolve the reader's body font-family (serif or sans-serif chain, per the "Default Font" setting, including the chosen typeface, CJK font, and any user-imported custom font) and apply it to the RSVP word display. Custom and additional fonts are already mounted in the top document where the overlay renders, so the resolved family resolves the same typeface. The monospace fallback is kept only when no font setting is available. Extracts the font-family list building from getFontStyles into a shared buildFontFamilyLists helper and exposes getBaseFontFamily for top-level UI. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
d165e8df2c |
fix(reader): turn automatically when highlighting across pages (#4487)
* fix(reader): turn automatically when highlighting across pages (closes #1354) * refact: Refactor time retrieval to use Date.now() * fix(reader): rework auto page-turn as a corner-dwell gesture Rework the initial #1354 implementation into a deliberate corner-dwell gesture that works across platforms, and fix popup positioning for cross-page selections. Trigger: - While a text selection is active, hold any engagement signal — the pointer (web/desktop/iOS), the Android native touchmove, or the selection caret — inside a screen corner for 500ms to turn one page: bottom-right goes to the next page, top-left to the previous. - One turn per engagement: a signal must leave the corner and return to turn another page, so the user controls it one page at a time. - The corner is a quarter-ellipse of radius 15% of each axis, measured against the reading frame (the <foliate-view> rect) inset by the page content margins, so the zone lands on the text — not the margin/footer or a sidebar — and the pointer can actually reach it. Per-platform signals: - web/desktop/iOS: the iframe pointermove, mapped to window coordinates via the iframe element's on-screen rect. - Android: the selection caret (the only signal during a native handle drag, where the handles live in a separate window so their touches never reach the Activity) plus a throttled (~10/s) native touchmove added in MainActivity.dispatchTouchEvent for content drags. Android scroll-pin (#873): an active selection pins the container scroll, which reverted the turn; suspend the pin during the turn and re-anchor it to the page we land on. Popup positioning: getPosition decided which selection end was on-screen using window bounds, so a cross-page selection's off-screen start (which maps behind the sidebar but inside the window) read "in view" and pinned the popup off the visible page. Test visibility against the reading frame instead, and for a multi-page selection anchor to the last on-screen line. Also: logical view.prev()/next() (RTL-correct); skip in scrolled mode; pass contentInsets down to the annotator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Huang Xin <chrox.huang@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2ade769956 |
feat(toc): show current reading page under the active item (#4513) (#4525)
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> |
||
|
|
c15e850252 |
fix(reader): shrink hidden page-nav buttons on Android so they don't eat long-press (#4501)
On Android, the four 80x80 page-navigation buttons stay mounted on top of the foliate viewer even when hidden (opacity-0). pointer-events:none can't be used on Android (it breaks touch propagation to the iframe), so the prev/next-section buttons already have an h-4 w-4 fallback for the hidden state. The prev-page / next-page buttons were missing this fallback and therefore kept covering ~80x80 hot zones in the lower-left and lower-right of the page, swallowing long-press touches on the first/last words of the bottom two lines so they could neither be highlighted nor open the toolbar. Apply the same h-4 w-4 fallback to those two buttons. |
||
|
|
4d1205fdf5 |
fix(reader): stop zoomed image pan from flickering on desktop, closes #4451 (#4465)
The desktop mouse-drag handlers were bound to the moving <img>, so the cursor crossing the (transition-lagged) image boundary fired onMouseLeave and repeatedly aborted/restarted the drag — the flicker. Touch was fine because it tracks on the full-screen container. Track the drag on `window` while dragging (mirroring the touch path), disable the transform transition during the drag so the pan is 1:1, and set will-change: transform (the transform-gpu class is overridden by the inline transform, so its GPU hint was lost). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
578b7ba14f |
fix(reader): restore annotation list auto-scroll to the nearest item (#4428)
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> |
||
|
|
7128e8964e |
fix(reader): suppress Android image callout freezing the image viewer (#4425)
Long-pressing an image in the zoom viewer on Android triggers the WebView's native image callout (context menu / drag / magnifier) at the same time as the viewer's own pinch/pan/zoom touch handlers, locking up the whole app until restart. Same root cause as the book-cover freeze (PR #4345): `-webkit-touch-callout: none` doesn't inherit, so the class must sit on an ancestor of the `<img>`. Apply the existing `.no-context-menu` class to the viewer container so the `.no-context-menu img` rule reaches the zoomed image and disables the native callout. Harmless on desktop (the property is a no-op there and right-click-save still works). Closes #4420 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4abbc0254c |
fix(reader): stop footer progress info painting a stray focus ring (#4397) (#4418)
The always-on page-info footer (`.progressinfo`) is a decorative
`role='presentation'` element, but it carried `tabIndex={-1}`, which made
it focusable. On Android, long-pressing the footer focused the div and the
WebView painted its default focus ring (`outline: auto`). Because the
element is pinned `absolute bottom-0` at book-view width, the ring rendered
as a content-column-wide line across the bottom of every page and persisted
until focus cleared.
Remove the `tabIndex` so the decorative element can no longer receive
focus. The `onClick` (tap-to-cycle progress mode / dismiss popup) still
fires regardless of tabindex, nothing focuses it programmatically, and the
translated `aria-label` keeps it exposed to screen readers unchanged.
Closes #4397
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
726f53a64b |
fix(reader): use Tauri clipboard plugin for copy on Android (#4409)
navigator.clipboard.writeText is unreliable inside the Tauri Android WebView, so tapping Copy in the Reader's selection popup silently no-ops. Route the write through @tauri-apps/plugin-clipboard-manager on Tauri targets (Android, iOS, macOS, Windows, Linux), with a graceful navigator.clipboard / execCommand fallback for the web build and older WebViews. - Add tauri-plugin-clipboard-manager (Rust + JS) - Register the plugin in lib.rs - Grant clipboard-manager:allow-write-text / allow-read-text - New utils/clipboard.ts wrapper with platform-aware fallback chain - Annotator handleCopy and handleConfirmExport use the wrapper |
||
|
|
3a81e09911 |
fix(reader): scroll oversized blocks in-place instead of turning the page (#4400) (#4415)
Wide or tall tables, code blocks and display equations overflowed the reading column and a scroll gesture over them turned the page instead of scrolling the content (#4400). - Wrap tables and display equations in a horizontally/vertically scrollable container; route touch + wheel along the box's scrollable axis so it scrolls the box and never turns the page, even at the edge (both axes). - A box that fits its column is marked fit (overflow:visible) so it never clips or captures gestures; the fit decision is measured once after layout via a self-disconnecting ResizeObserver, so it never relayerizes during a page turn. - The scroll wrapper carries a new cfi-skip attribute that makes it transparent to CFI: epubcfi.js hoists a cfi-skip node's children into its parent (unlike cfi-inert which drops the subtree), and xcfi.ts mirrors this for CFI<->XPointer so existing highlights, bookmarks and KOSync positions inside a wrapped table or equation still resolve. The sanitizer whitelists cfi-skip. - Bump foliate-js submodule (cfi-skip support + raf fallback for large sections). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
458ad7510c |
fix(reader): scroll wide EPUB tables horizontally (#4391)
* fix(reader): scroll wide tables horizontally instead of scaling Why: - Wide EPUB tables (e.g. many columns without cell widths) overflowed the page because CSS scale only applied when widths were known. - Paginated mode stole horizontal swipes for page turns over table content. Refs: - Replaces transform-based applyTableStyle scaling with a scroll wrapper and capture-phase touch routing (same pattern as gesture brightness). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(reader): keep wheel/trackpad table scrolling from turning the page Horizontal scrolling of a wide table in paginated mode also turned the page: - applyTableTouchScroll only routed touch events. Trackpad/mouse wheel (readest forwards iframe wheel -> 'iframe-wheel' -> pagination) was never intercepted, so a horizontal wheel both scrolled the table and flipped the page. - findWrapper used `instanceof Element`, which is always false for iframe event targets because this module runs in the top-window realm. The touch routing therefore never fired either. Add a capture-phase wheel handler that consumes horizontal wheels over a scrollable table -- including at the scroll edge, so the gesture (and trackpad momentum) never chains into a page turn -- and make findWrapper cross-realm safe via duck-typing on `closest`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reader): don't show a spurious scrollbar on layout tables Wrapping every table for horizontal scrolling made tables that fit the column (e.g. character/glossary layout tables) show a spurious horizontal scrollbar, because the wrapper was always scrollable. Now the wrapper clips (no scrollbar) for a table that fits within a few px of the column, and a ResizeObserver re-evaluates this as the column width settles. A table genuinely wider than the column always scrolls and is never clipped; one that wraps to fit shows no scrollbar. Touch/wheel routing engages only scrollable wrappers. Add a Chromium browser test over sample-table-layout.epub (layout tables must not scroll) and sample-table-wide.epub (a too-wide table must scroll, not clip). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Huang Xin <chrox.huang@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
97191a57c0 |
fix(reader): stop reading ruler creeping down on scroll (#4386) (#4388)
In scrolled mode the ruler's geometry-cache effect re-ran on every relocate — including those fired continuously while scrolling — and re-snapped the band to the next line forward from a screen-fixed anchor, so the band crept down the page as the reader scrolled. Place the band once on mount (and after a viewport-dimension change), but never re-snap on a plain scroll relocate. Click-driven snapping (the reading-ruler-move handler + pendingScrollAlign realign) is unchanged, and paginated mode is unaffected. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
e8675fb7eb |
fix(reader): inline custom @font-face rules in iframe stylesheet (#4383)
* fix(reader): inline custom @font-face rules in iframe stylesheet The reader iframe's first paint resolved with the default serif/sans fallback and only swapped to the user's configured custom font a moment later, producing a visible font flash when opening a book. Custom font @font-face rules were registered via mountCustomFont on the host document only, while paginator's setStyles writes its CSS into the iframe synchronously before the first 'load' event. The iframe had no knowledge of the user's custom fonts at that point, so font-family declarations in the stylesheet matched nothing and fell back. Inline the @font-face rules for every loaded custom font (blob URLs already in memory, no network round-trip) at the front of getStyles output, so paginator delivers them to the iframe atomically with the rest of the stylesheet. Defensive try/catch around createFontCSS keeps a single bad font from breaking the whole stylesheet. * refactor(reader): pass custom fonts into getStyles instead of reading the store getStyles lives in src/utils, where every other file is a pure function; it was the only one importing a store (useCustomFontStore). Keep the util pure: accept the loaded custom fonts as a parameter and let the reader components — which already own the font store — supply them. - style.ts drops the useCustomFontStore import and the SSR/store-error guards in getCustomFontFaces; the helper is now a pure CustomFont[] -> CSS transform. - getStyles(viewSettings, themeCode?, customFonts = []) inlines the @font-face rules for the passed fonts. - The first-paint call sites (FoliateViewer, FootnotePopup) pass getLoadedFonts(); settings-panel re-styles keep the default [] since custom fonts are already mounted as persistent <style> elements there. - Add tests that exercise the font-face inlining path (the existing suite never did, since the store is empty under jsdom). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Huang Xin <chrox.huang@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ef603852b7 |
feat(tts): hotkey to highlight the currently-spoken sentence (#4085) (#4368)
Add a "Highlight Current Sentence" keyboard action (default Shift+M, in the Text to Speech shortcut section) that persists the sentence TTS is reading aloud as a normal highlight using the user's default style/color — no text selection, eyes-off, silent, and idempotent (a repeat press on the same sentence is a no-op rather than a duplicate). Flow: the shortcut handler in useBookShortcuts dispatches tts-highlight-sentence → useTTSControl (which owns the TTSController) resolves the current sentence via the new TTSController.getSpokenSentence() and relays create-tts-highlight → Annotator builds the BookNote with the pure, unit-tested buildTTSSentenceHighlight helper and persists/renders it like any other highlight. Closes #4085 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
11666be5ee | fix(reader): collapse TOC to the current chapter's path by default (#4366) | ||
|
|
aa318904b5 |
fix(reader): show full bookmark ribbon in scrolled mode header (#4365)
In scrolled mode, SectionInfo paints a solid `bg-base-100` `notch-area` mask over the top safe-area strip at z-10. The Ribbon was also z-10 but rendered earlier in the DOM, so the equal-z mask painted over the ribbon's upper (unsafe-area) half — only the lower 44px showed. In paginated mode the mask has no background, so the ribbon showed fully. Raise the ribbon to z-20 so the whole ribbon stays visible above the mask, and mark it pointer-events-none so taps still fall through to the notch mask's scroll-to-top handler. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
78794499a2 |
fix(dictionary): correct System Dictionary platform gating on web and iPad (#4362)
* fix(dictionary): keep other dictionaries usable when System Dictionary syncs to an unsupported platform `dictionarySettings.providerEnabled` is whole-field synced across devices, so enabling System Dictionary on macOS/iOS sets the flag on web/Linux/Windows too. There the row is hidden and the feature is a no-op, but the settings UI read the raw flag and locked every other dictionary's toggle read-only. Gate the lock on `isSystemDictionaryEnabled(settings)` — the same platform-aware check the annotator uses — so it matches real lookup behavior and never triggers where the system dictionary can't run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dictionary): dispatch system dictionary handoff by native OS (fixes iPad) iPadOS sends a desktop "Macintosh" user agent, so the UA-based `getOSPlatform()` reported iPad as 'macos' and the handoff invoked the macOS-only `show_lookup_popover` Rust command that iOS never registers ("Command show_lookup_popover not found"). Derive the OS from the app service's `is*App` capability flags (sourced from the Tauri OS plugin, correct on iPad) via a new synchronous `getInitializedAppService()` accessor, so iPad routes to the iOS plugin command path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(ui): constrain reader View Options dropdown to h-8 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(agent): note System Dictionary platform-detection and synced-flag patterns Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f1ae050768 |
fix(ui): refine reader side panels and their empty states (#4361)
* 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> |
||
|
|
789d031222 |
feat(reader): line-aware reading ruler (#4358)
Snap the reading ruler to real rendered text lines instead of stepping by a fixed arithmetic height, so the band always frames whole lines. - Snap to actual line geometry from the relocate range; the band is sized dynamically to the text block plus symmetric padding (round(fontSize * lineHeight * 0.3)), capped at (lines + 1) line heights so a tall image inside a block can't expand it to cover the whole figure. - Drop block/container rects (Range.getClientRects aggregates multi-line element borders) so paragraphs aren't merged into one giant line and skipped. - Column-aware in multi-column layouts: the band spans one column at a time and advances column by column. - Confine the band to lines at least half visible within the viewport. - Scrolled mode: snap to lines, and at a view edge scroll the view and realign the band to the start/end of the new view (works for vertical-rl too, which scrolls horizontally); paging snaps the view edge between lines so text isn't cut or repeated. - Vertical writing mode: correct band centering and drag direction; Up/Down keys move the ruler while Left/Right turn pages (taps always move the ruler). - Page turns keep the first/last line: forward lands on the first line of the new page, backward on the last line; the relayout re-snap anchors on the band's leading edge so it never skips a line. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
36e11de332 |
feat(reader): swipe-to-adjust brightness gesture on mobile (#3021) (#4356)
* docs: design spec for gesture-based brightness control (#3021) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs: revise brightness-gesture spec per /autoplan review (#3021) CEO+Design+Eng dual-voice review. Key fixes: capture-phase listener (bubble-phase could not suppress foliate paginator), opt-out toggle, 18px threshold, selection guard, brightness seed race, rAF teardown, e-ink stepped overlay, contrast capsule, perceptual curve reuse, listener-level test harness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reader): swipe-to-adjust brightness gesture on mobile (#3021) Left-edge vertical swipe adjusts screen brightness on iOS/Android, with a Sun-icon progress overlay. Capture-phase non-passive listener suppresses the foliate paginator / page-flip / UI-toggle handlers; selection guard, strip reservation in scrolled mode, eager brightness seed, rAF throttle + teardown. Opt-out toggle in Settings > Behavior > Device (default on). Perceptual curve shared with the menu slider. Pure-helper + listener-level tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reader): detect brightness-swipe edge by screenX; i18n + shorter label (#3021) On-device fix: paginated mode lays the iframe doc out as wide side-by-side columns, so clientX/documentElement.clientWidth are document coordinates and a left-edge touch on a later page never fell inside the strip (armed stayed false). Detect with screenX against the parent window width, matching usePagination. Also: translate the two new setting strings across all locales and shorten the toggle description to 'Slide along the left edge'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
7bdd3ecdee |
perf(sidebar): virtualize BooknoteView and memoize derivations (#4352)
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.
|
||
|
|
6405ba31c8 |
fix(reader): keep TOC scrolled to the current chapter on refresh (#4353)
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> |
||
|
|
2f5e583653 |
feat(annotations): configurable export link type + dedicated Import Annotations modal (#4350)
* 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> |
||
|
|
3c134380b7 |
feat: add empty state hints and loading indicators for annotations, bookmarks, notes, font import, and Moon+ Reader import (#4338)
* 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> |
||
|
|
648c35b334 |
feat(reader): add disableSwipe option to disable swipe-to-paginate (#4335)
Issue #4288: users with hardware page-turn buttons (e.g. e-ink readers) want to disable swipe-to-paginate so accidental finger drags during highlight selection don't flip the page mid-annotation. The existing "Tap to Paginate" toggle only covers taps; swipe was always on. - New `disableSwipe: boolean` on `BookLayout` (default `false`, declared right after `disableClick`). - foliate-js submodule bump: paginator gates `#onTouchMove` and `#onTouchEnd`'s snap-to-page on a new `no-swipe` attribute, so native touch behaviour (text selection) stays intact. - `FoliateViewer` sets/removes the `no-swipe` attribute alongside `animated`, and the `ControlPanel` toggle pushes the change to the live renderer so it takes effect without a viewer reset. - The fixed-layout swipe interceptor in `usePagination` also bails when `disableSwipe` is on, covering both reflowable and fixed- layout books. - New "Swipe to Paginate" UI row directly below "Tap to Paginate"; both can be off simultaneously. - i18n: 33 locales translated. Also polish: rephrase the two helper texts under "Read books in place" in `ImportFromFolderDialog`. The previous copy ("Copy no book into the library to save space.") used an awkward double-negative; the new wording is clearer and the locked variant drops "registered as" / em-dash for a plain two-sentence form. i18n updated. Closes #4288 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4e01e13ee7 |
fix(library): make bookitem-main shrink to match cover in fit mode (#4331)
* fix(library): make bookitem-main shrink to match cover in fit mode Closes #4234. In fit mode the bookitem-main kept its 28/41 aspect regardless of the cover image's natural aspect, leaving extra padding beside (portrait covers) or above (landscape covers) the cover. The select-mode overlay and icons drifted away from the cover edge. BookCover now reports the loaded image's natural aspect ratio. BookItem overrides the bookitem-main's aspect-ratio with the cover's aspect so the box hugs the cover exactly, and proportionally shrinks book-item width for portrait covers so the info row icons align with the cover's right edge. Also wrap the TTS "Back to TTS Location" pill with whitespace-nowrap so long translations (e.g. German "Zurück zur TTS-Position") expand the button width instead of overflowing the fixed height. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(library): scope cover shrink to bookitem-main, leave info row at cell width Per review, the width shrink should only apply to the cover row so the title and info icons keep their original cell-wide layout. Move the width style from .book-item to .bookitem-main alongside its aspectRatio override. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
e0ce6c8c22 | feat(reedy): Appendix A · Phase 4 — custom thread UI on AgentRuntime (#4308) | ||
|
|
6bc4a96b99 |
feat(reedy): Phase 1B — wire Reedy into the chat, settings, and Sources UI (#4296)
* feat(reedy): wire RetrievalBackend interface + metrics into the chat adapter
Phase 1B backend integration. Adds a RetrievalBackend interface so
TauriChatAdapter holds a uniform reference instead of branching across the
file, with two impls — LegacyIdbBackend (wraps the existing IDB ragService
unchanged) and ReedyBackend (lazy-opens reedy.db, adapts the active
provider's embedding model to Reedy's narrower shape, exposes a Vercel
`lookupPassage` tool). selectBackend() gates Reedy behind both
aiSettings.reedy.enabled AND isTauriAppPlatform() per plan D15 so the MVP
cohort is desktop-only.
The Reedy path streams via streamText({ tools: { lookupPassage }, stopWhen:
stepCountIs(3) }) with a status-aware system prompt that tells the model
how to phrase responses for each RetrieverStatus value.
Replaces the module-global `lastSources` + 500ms poll with a per-instance
ReedySourceStore keyed by a synthetic per-turn id the adapter generates,
so the Sources dropdown stops racing on global state. Both legacy and
Reedy backends now feed citations through the same store; the UI is
backend-agnostic via a shared SourceItem shape both ScoredChunk and
RetrievedChunk satisfy.
Adds the reedy_metrics table to the reedy migration (versioned with
app_version + session_id + turn_id per row) plus a ReedyMetrics writer
that ReedyBackend uses to record indexing-lifecycle and tool-use events.
Always-on local; no network egress. NoopReedyMetrics keeps construction
cheap before the DB opens.
Tests: retrievalBackend selectBackend gates, ReedySourceStore semantics
(append/replace/subscribe/clear), ReedyMetrics debounced batching +
exportBundle, and a TauriChatAdapter contract test that asserts the
Reedy/legacy code-paths pass the right args to streamText.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(reedy): UI wiring — settings toggle, clickable sources, feedback bundle
Phase 1B UI integration. AIAssistant now constructs the active backend
(legacy or Reedy) via selectBackend with the platform gate from M1.7,
wires a ReedySourceStore for the chat adapter, and routes Sources-dropdown
clicks to `getView(bookKey)?.goTo(source.cfi)` when the source has a CFI.
Legacy-path sources still render as static rows because they have no CFI.
AIPanel grows a 'Reedy Retrieval (Beta)' BoxedList with the toggle and a
'Send Reedy feedback' button that calls exportReedyMetricsBundle and
triggers a JSON download of the last 90 days of events. The toggle is
disabled on web with an explanatory description per plan D15.
Thread accepts an onSourceClick callback and renders each source as a
button when its source carries a CFI, otherwise as a static div — so the
Sources dropdown is backend-agnostic via the shared SourceItem shape.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
ca17131f2a |
fix(reader): keep New Chat button visible above Android nav bar and force theme contrast (#4287)
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. |
||
|
|
5e366018df |
fix(cbz): ComicInfo metadata + CBZ page count + WebDAV i18n (#4282)
* fix(cbz,i18n): ComicInfo metadata + CBZ page count + WebDAV i18n Closes #4253 (ComicInfo.xml not read) and #4255 (CBZ shows "1 page left"). CBZ / ComicInfo (foliate-js submodule + Readest derivation): - comic-book.js: find ComicInfo.xml in subdirectories too, parse description / subject / identifier / published / series fields beyond the prior name+position pair. Series Count populates the canonical `belongsTo.series.total`; no top-level duplication. - bookService.ts / readerStore.ts: derive `metadata.seriesTotal` from `belongsTo.series.total` in parallel to the existing series / seriesIndex derivation. - ProgressBar / FooterBar / DesktopFooterBar: drop the hard-coded `pagesLeft = 1` for fixed-layout books and compute it from `section.total - section.current`. FooterBar uses `FIXED_LAYOUT_FORMATS.has(bookFormat)` so CBZ picks `section` (correct image count) instead of `pageinfo` (locations). - ProgressBar: switch the remaining-pages text to "in book" for fixed-layout titles (no chapter structure) and keep "in chapter" for reflowable books. WebDAV refactor for translation coverage: - WebDAVBrowsePane / SyncHistoryPanel called `t(...)` (passed as a prop) instead of `_(...)`. The i18next-scanner only looks for `_`, so ~53 strings were unreachable and shipped in English to every locale. Switched both components to call `useTranslation()` themselves; helpers that aren't React FCs take `_: TranslationFunc` so the scanner sees the literal calls. - WebDAVClient.checkConnection now returns a `code` discriminator (`SERVER_URL_REQUIRED` / `AUTH_FAILED` / `ROOT_NOT_FOUND` / `UNEXPECTED_STATUS` / `NETWORK`); raw English `message` is reserved for the dev console. New `formatConnectError` and `formatSyncError` helpers in WebDAVForm translate via a switch where each branch is a literal `_('...')`. Same treatment for the sync-failure path that previously surfaced raw e.message. - "Syncing 0 / {{total}}" is now parameterized as "Syncing {{n}} / {{total}}" with n=0 at startup so the digit formats naturally and the template can be reused mid-sync. - "Cleanup · {{count}} book(s)" hard-coded options used unsupported ternary; rewrote as plural-aware key. i18n scanner fix (i18next-scanner.config.cjs): - vinyl-fs walked into directories whose names end in source-file extensions (Next.js route folder `runtime-config.js/`, Playwright screenshot folder `*.test.tsx/`) and crashed with EISDIR. Resolved by expanding globs via `fs.globSync` and filtering to files only before handing to the scanner. TypeScript-syntax sites that broke esprima during extraction: - WebDAVBrowsePane / WebDAVForm: `(e as Error).message` and `failed[0]!.title` inside `_(..., options)` arguments. Replaced with `e instanceof Error ? e.message : String(e)` and `failed[0]?.title ?? ''` — also runtime-safer. User-facing em-dash cleanup: - Removed em-dashes from translation keys across SyncHistoryPanel / WebDAVForm / WebDAVBrowsePane / SyncPassphraseSection / send/page / replicaCryptoMiddleware / AIPanel. Tagline in `layout.tsx` kept. Locale translations: - ~2400 translations applied across all 33 locales for the keys that were either newly extractable, freshly worded, or pre-existing but untranslated. Zero `__STRING_NOT_TRANSLATED__` remain after the run. Misc: - next.config.mjs: drop `eslint.ignoreDuringBuilds: true` so build runs the same lint as CI. - Collection type: add `total?: string` for ComicInfo series count. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci(test): fix vitest invocation, run with 4 workers `pnpm test:pr:web` was chaining `pnpm test -- --watch=false`, which pnpm expanded into: dotenv -e .env -e .env.test.local -- vitest -- --watch=false The second `--` made vitest treat `--watch=false` as a positional file pattern, not a flag. Vitest then fell back to defaults (in CI's non-TTY env that still meant a one-shot run, so the suite passed), but the worker pool was effectively serialized for big chunks of the 243-file run — wall ~90 s on a 4-vCPU runner where the parallel-sum of phases was ~236 s (≈2.6× effective parallelism). Replace the chained pnpm invocation with a direct call to `vitest run --maxWorkers=4`, matching the 4 vCPUs the GH Actions ubuntu-latest runner provides. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
5c82351ab9 |
feat(integrations): add WebDAV sync to Reading Sync settings (#4204)
* feat(integrations): add WebDAV sync to Reading Sync settings Adds a WebDAV entry under Settings -> Integrations -> Reading Sync with configure/browse UI, library-wide Sync now, and per-book sync of progress, annotations and (opt-in) book files + covers. Reading progress and annotations are always synced when WebDAV is enabled; only Sync Book Files stays as a toggle since it's bandwidth-heavy. * feat(webdav): add diagnostic sync history panel and document viewSettings invariant Surface a per-run history for the WebDAV "Sync now" button so users can self-triage failures without rummaging through the dev console — a screenshot of the panel is now enough to file a useful bug report. The same change tightens the docs around viewSettings so the "device-local UI preferences" boundary is impossible to misread on the next refactor pass. Sync history panel: * New WebDAVSettings.syncLog ring buffer (cap 10), persisted alongside the rest of settings so a screenshot survives across app restarts. WebDAVSyncLogEntry captures startedAt, finishedAt, status (success / partial / failure), trigger, the eight counters from SyncLibraryResult, the toast text, and an optional per-book failure list with a phase tag (download / upload-config / upload-file). * SyncLibraryResult gains a failedBooks: SyncFailureEntry[] field. The two existing failure points in syncLibrary (download catch, upload catch) now record per-book reason+phase via formatFailureReason(), which keeps the persisted blob small by stripping stacks/whitespace and capping length at 200 chars. * WebDAVForm.handleSyncNow now timestamps the run, builds an entry from the result on success/partial paths and from the caught error on failure paths, and appends through a fresh-read appendSyncLogEntry() so concurrent toggle changes can't clobber the log. * New SyncHistoryPanel + SyncStatusBadge + SyncHistoryDetails components render the log inline in the Settings page. The detail row groups counters into three semantic columns (activity, skipped, outcome) on a six-column grid so labels can wrap freely while numbers stay tabular and right-aligned. Per-book failures render as a separate stack below the counters. viewSettings invariant: * buildRemotePayload and pullBookConfig already implement the right thing — only progress/location/xpointer/booknotes travel; viewSettings stays device-local. Comments now spell out the contract on both sides so future contributors don't reintroduce viewSettings on the wire by mistake. * fix(webdav): preserve prior state across reconnect, drop stale closure in ensureDeviceId Two bugs in the WebDAV sync flow surfaced during review: 1. WebDAVForm.handleConnect rebuilt the entire `webdav` settings block from the four credential fields the user just typed, dropping `deviceId`, `syncBooks`, `strategy`, `syncProgress`, `syncNotes`, `lastSyncedAt`, and `syncLog` on every reconnect. Most concerning is the deviceId rotation: a disconnect + reconnect made the next sync look like a brand-new device, defeating the cross-device clobber detection encoded in `RemoteBookConfig.writerDeviceId`. Extract a pure helper `buildWebDAVConnectSettings` that spreads the previous webdav object first so reconnect is non-destructive, matching the sibling pattern in KOSyncForm. 2. useWebDAVSync.ensureDeviceId merged the new deviceId into the closure variable `settings`, which can be stale when `pullNow → pushNow` fires back-to-back on book open or when the settings panel writes a sibling field concurrently. Read latest settings via `useSettingsStore.getState()` to match the pattern already used in `updateLastSyncedAt` and `persistWebdav`. Adds three unit tests for the new helper, including the reconnect preservation invariant. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(webdav): address review observations on encodePath, pull skip, and remote GC Three follow-ups from the review pass on top of 3f721d04. Each one was called out as a smaller observation the reviewer noted but did not push: * WebDAVClient.encodePath silently re-escaped literal % characters despite a comment claiming existing %-escapes are preserved. A caller that pre-encoded a space as %20 would see %20 become %2520 in the request URL, breaking any path that came in already escaped. Tokenise each segment into already-escaped %XX runs and everything-else, and only run encodeURIComponent on the latter. Add four unit tests exercising pure-unicode, pure-pre-escaped, mixed, and root-slash paths. Implementation note: two regexes are needed because a /g RegExp.test is stateful and would skip every other token in this map; the split regex has /g for the iteration, the classifier regex is anchored without /g for the per-token check. * OPEN_PULL_SKIP_MS doc-comment claimed it catches the close-then-reopen flow, but useWebDAVSync unmounts on reader close so lastPulledAtRef resets to 0 — the new instance always passes the cooldown check on remount. The guard actually only fires on re-invocations of the open-book effect inside one hook lifetime (book-to-book navigation, double-render before hasPulledOnce flips). Rewrite both the constant's doc-comment and the call-site comment to match the real semantics. * WebDAVSync push path doesn't DELETE the per-hash directory of a tombstoned book. The deletion *is* propagated through library.json so other devices hide the book, but storage on the WebDAV server grows monotonically. Add a TODO at the pushLibraryIndex call with a sketch of what a future garbage-collection sweep would need (a per- device acknowledgment field on RemoteLibraryIndex so we don't wipe data a peer hasn't seen the deletion for yet). * refactor(webdav): extract WebDAVBrowsePane and SyncHistoryPanel from WebDAVForm The WebDAV settings form was nearing 1500 lines and hosted three loosely related surfaces — credential entry, sync controls + manual trigger, and the in-app file browser — that didn't share much state. Reviewer flagged it as a refactor candidate; this commit does the actual split. * WebDAVBrowsePane (new, 534 lines): owns currentPath, the directory listing, per-entry download status, the navigation handlers and the per-file icon / filename helpers. Reads credentials from the settings prop and otherwise reaches for envConfig / useLibraryStore / useAuth itself rather than threading them through props (matches how the rest of the integrations panels are wired). * SyncHistoryPanel (new, 293 lines): the diagnostic history surface plus its three private helpers (SyncStatusBadge, formatSyncSummary Line, formatSyncTimestamp, SyncHistoryDetails). Moved verbatim from the inline definitions at the bottom of WebDAVForm — the component was already presentation-only and accepting the translation fn as a prop, so no API change. * WebDAVForm (676 lines, down from 1456): keeps the mode switch (configured vs. not), the credential form, the sync sub-controls (Upload Book Files / Sync Strategy / Sync now button), and the large handleSyncNow effect — those last two are intrinsically tied to the settings store and would have just been pushed back up the prop chain by any extraction. The standalone SyncHistoryPanel and WebDAVBrowsePane are now mounted as siblings inside the configured branch. No behavioural change — both new files run the same effects, build the same JSX, and read/write the same store fields as before. All existing webdav-related unit tests still pass. Resolves the last of the reviewer's smaller observations on 3f721d04 (file length). * fix(webdav): stream book uploads to avoid renderer OOM on large files Both syncLibrary (manual Sync now in WebDAVForm) and useWebDAVSync (per-book auto/manual sync triggered on book open) materialised the full book binary as an ArrayBuffer in the V8 heap before PUTting it. With multi-hundred-megabyte PDFs / scanned books, the renderer either accumulates buffers across sequential pushes (library sync) or blows its heap ceiling on a single book (per-book sync), surfacing as a blank white screen on desktop and a binder-OOM kill of the WebView on Android. Add a BookFileStreamingLoader option to pushBookFile that, on Tauri targets, hands the file path off to tauriUpload's Rust-side streamer so bytes never enter JS. The HEAD short-circuit is shared across both paths, so steady-state syncs still cost a single round-trip per book. Web targets keep the buffered fallback (no streaming HTTP primitive available there). Wire the streaming loader through SyncLibraryOptions.loadBookFileStreaming for the library Sync now path, and inline it in useWebDAVSync.pushBookFileNow for the per-book path. Covers stay on the buffered loader — they're capped at a few hundred KB and don't justify widening the API. * fix(webdav): keep Sync now state alive across Settings navigation/close WebDAVForm tracked the library-wide Sync now run in component state, so any navigation that unmounted the form (drilling back to the Integrations list, or closing the SettingsDialog entirely) destroyed the in-flight indicator while syncLibrary's promise kept running off-thread. On return the user saw a re-enabled button with no progress affordance, an empty Sync History (until the run finally finished), and could trigger a second concurrent syncLibrary against the server. Hoist isSyncing / progressLabel into a process-local zustand store (webdavSyncStore) and consume it from WebDAVForm. The store outlives any single mount, so re-mounting the form picks up the running sync's state on first render — button stays disabled, progress label keeps ticking, and the re-entrancy gate (now reading the live store rather than a stale closure) blocks duplicate clicks. Also surface 'Syncing…' in the IntegrationsPanel row so users get the cue without drilling into the sub-page. Not persisted: the store dies with the renderer, which is the right semantic — a sync killed by app exit shouldn't look like it's still going on next launch. * feat(webdav): cleanup mode for orphan book directories on the server WebDAV pushes set Book.deletedAt as a tombstone but never DELETE the per-hash directory on the server, so the remote Readest/books/ tree accumulates dead entries from books the user deleted long ago. Add a dedicated cleanup mode in the WebDAV browser to evict them in batch. Cleanup mode is reached via a new sweep button next to Refresh. Entering it pins the listing to Readest/books/, filters down to directories whose local Book carries deletedAt, and replaces the per-row icon with a checkbox. The footer carries a single right-aligned Delete from server action; selecting one or more rows and clicking it sends a confirm dialog (appService.ask, so it actually blocks on Tauri) and then runs sequential DELETEs against the server. Each row splices out of the listing the moment its DELETE returns, so the listing itself is the progress indicator; the button keeps a stable width by always reserving space for the spinner via the invisible class. The local library is left untouched. Book.deletedAt is the authoritative deletion signal in readest's sync model — clearing or rewriting it here would cause sibling devices to either resurrect the book or lose the deletion event. Restore is therefore not offered: the per-entry download button already provides full recovery (tauriDownload + ingestFile streams the file back, ingestFile clears deletedAt as a side-effect, and the next sync round-trip merges remote progress and notes), and a metadata-only restore would leave users staring at unopenable shelf rows whenever the bytes had been GCed off local disk. Browse mode is friendlier too. Per-hash subdirectory rows under Readest/books/ resolve their hash to the local library's title and short-form hash for skimmability; soft-deleted entries get a folder-off icon plus a 60% dimmed title (a redundant signal for touch platforms where the desktop-only hover tooltip doesn't fire). Cleanup runs are persisted into the existing sync history with a kind: 'cleanup' discriminator and a booksDeleted counter, so destructive batch operations are auditable alongside regular Sync now runs without polluting the common case (the new counter is zero-suppressed on plain sync entries). * test(webdav): cover deleteDirectory and deleteRemoteBookDir Pin the contract of the cleanup-mode delete plumbing: HTTP method, Depth: infinity header, Authorization header and target URL on the low-level deleteDirectory; success/failure/auth-failure routing and per-hash path construction on the high-level deleteRemoteBookDir. Status-code semantics are exercised end to end (200/204 ok, 404 idempotent, 401/403 AUTH_FAILED, 5xx generic, network throw NETWORK), so a future refactor can't silently drop the explicit Depth header or merge the auth-failure path into the per-book result struct without tripping a regression. --------- Co-authored-by: Huang Xin <chrox.huang@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ae81cd0151 |
feat(annotator): support global highlights that fan out across all matching positions (#4257)
Introduces a 'global' annotation flag so a highlight/note created on one occurrence of a phrase is automatically applied to every matching occurrence in the book (and stays applied across reloads). Renders these expansions as transient overlays without creating duplicate persisted notes. This flag will not show when the book is fixed layout like PDF or CBZ. - types: add 'global?: boolean' to BookNote and DBBookNote; transform layer round-trips the field, with regression coverage ensuring older clients do not clobber it on write-back. - db: new migration 013_add_book_notes_global.sql adds nullable 'global' column to public.book_notes; init schema.sql updated to match. - annotator: new utils/globalAnnotations.ts handles cfi expansion / text-match search across the spine and overlay synthesis. Annotator.tsx fans out global notes on load and on overlay creation; AnnotationPopup and HighlightOptions expose a toggle to mark a highlight as global. - sync path is transparent: a global note created on another device is fanned out locally on next render with no extra UI required. |
||
|
|
dabdcdcc53 |
fix(macos): fix traffic lights position on macOS 26 (#4247)
* 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>
|
||
|
|
05da6bdf43 |
feat(dictionary): add system dictionary provider for macOS, iOS, and Android (#4219)
Hand selected words off to the platform's native dictionary surface when the user opts into the new "System Dictionary" entry under Settings → Languages → Dictionaries. The setting is exclusive: enabling it disables all other providers (and vice versa) so the in-app lookup button either always opens the popup or always invokes the OS — no mixed states. Per platform: - macOS: AppKit's -[NSView showDefinitionForAttributedString:atPoint:] via a top-level Tauri command in src-tauri/src/macos/system_dictionary.rs. Anchored at the selection's bottom-center (CSS pixels mapped into NSView coords), so the inline Lookup HUD appears just below the highlighted text without raising Dictionary.app to the foreground. - iOS: UIReferenceLibraryViewController presented as a half-detent pageSheet on iPhone (medium → large drag-to-expand) and as a formSheet on iPad. Implemented in the native-bridge plugin. - Android: ACTION_PROCESS_TEXT intent with EXTRA_PROCESS_TEXT_READONLY, dispatched without createChooser so users get the standard system disambiguation dialog with "Just once / Always" buttons. Reports unavailable=true when no app handles the intent so the TS layer can silently skip rather than open an empty chooser. Web/Linux/Windows hide the row entirely. The provider is a sentinel — the registry filters it out of the popup tab list (it has no in-popup UI) and the annotator's handleDictionary checks isSystemDictionaryEnabled to dispatch directly to the native bridge before opening the in-app DictionaryPopup. |
||
|
|
c8fabd331c |
fix(reader): resolve KOReader sync conflict against non-KOReader servers (#4205)
The sync-conflict dialog had two issues with servers other than KOReader (e.g. Kavita's KOReader-compatible sync endpoint): - "This device" preview rendered a bare "undefined" because reflowable books built the string from `sectionLabel`, which is empty for spine items with no matching TOC entry. It now falls back to the page count. - Choosing "use remote" closed the dialog but never moved the reader: `applyRemoteProgress` only knew how to navigate via CREngine XPointers, so non-XPointer progress strings were silently ignored. It now falls back to `view.goToFraction` using the reported percentage. Also fixes the section-title indentation in the dialog (SectionTitle bakes in `ps-4`, which misaligned the labels against their values). Closes #4200 Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ba6e5899e5 |
feat(reader): RSVP CJK character mode and whole-word highlight (#4199)
* feat(reader): add RSVP CJK character mode and whole-word highlight, closes #4131 Add two CJK-only options to the RSVP overlay settings row: - Character Mode: split CJK text per-character instead of by jieba/Intl word segmentation, restoring one-character-per-flash reading. - Highlight Word: render a CJK word as a single centered, fully-colored span, fixing the focus-only highlight and even-length left-shift. The focus point now skips trailing CJK punctuation so tokens like "是。" highlight the character, not the punctuation. Both toggles appear only for sections that contain CJK text. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * i18n: extract RSVP CJK character mode and highlight word strings Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
3620c61038 |
feat(reader): import annotations from Moon+ Reader (.mrexpt) (#4174)
* 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>
|
||
|
|
8dfc0e945e |
fix(dictionary): normalize lookup query with trim + case fallback (#4192)
A double-click selection can carry trailing whitespace and most imported dictionaries store headwords lowercased, so an exact match on the raw selection often misses (e.g. `Hello` or `world ` fail to resolve `hello`/`world`). Case-sensitive formats like mdict are hit hardest since their reader compares the raw word. Seed the lookup history with a trimmed word and try ordered query variants (trimmed, lowercase, title-case, uppercase) per provider, keeping the first hit. Closes #4176. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
411d3ad687 |
fix: export annotations even without TOC, closes #4186 (#4188)
* i18n(ios): add more localized languages in plist * fix: export annotations even without TOC, closes #4186 |
||
|
|
1a3d393e74 |
feat(reader): add "Clear Annotations" entry to the book menu (#4175)
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. |
||
|
|
f5e729a174 |
fix(reader): revert smooth mouse-wheel scrolling in scroll mode, closes #4130 (#4172)
The smooth-wheel feature (#3974, closing #3966) intercepts mouse-wheel events in scroll mode: it makes the wheel listener non-passive, preventDefault()s the native scroll, and replays the delta through a main-thread rAF animation against the renderer container. That regressed normal mouse scrolling on Windows (#4130): fast wheel bursts were discarded entirely, and the JS replay is structurally worse than native scrolling -- a non-passive wheel listener forces every wheel event (mouse and trackpad) off the compositor thread, and the postMessage hop plus main-thread animation add latency and jank that native compositor scrolling does not have. High-resolution scrolling (e.g. Logitech MX Master, the mouse in #3966) needs no special API: the OS/driver just delivers regular wheel events with smaller, more frequent deltas, and the browser scrolls them natively. #3966's own report ("smooth scrolling works with all applications apart from yours") points at the interception, not a missing capability. Restore native wheel scrolling in scroll mode. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
7716f189c3 |
fix(layout): keep header/footer transparent and fixed in scrolled mode, closes #4157 (#4168)
Remove the redundant "Apply also in Scrolled Mode" options for bars and margins so scrolled mode renders the header/footer consistently with paginated mode: transparent, fixed in position, and not obscuring content. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
54aa20d4f8 |
fix(footnote): don't treat in-book numeric chapter/verse links as footnotes (#4152)
closes #4140 The bare-numeric-text heuristic added in #3894 to detect non-superscript footnotes (`/^.{0,2}\d+$/` over `anchor.textContent`) was too permissive: in-book TOCs that list chapter/verse links such as `<a>1</a>, <a>2</a>, ...` all match the regex, so clicking them sets `check=true` and the footnote handler renders the destination as a popup instead of letting the link navigate. The OSB v2 verse-index and OSB v4 chapter-index from the bug report both hit this. Reject the `check` heuristic when the clicked link sits inside a numeric link list (2+ sibling links with the same short-numeric pattern within three ancestor levels). A real body paragraph with a couple of footnote markers still passes; a flat TOC of numeric links does not. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
9a05935caf |
feat(reader): improve Japanese selection UX by disabling furigana selection (#4137)
* feat: add default ruby rt styles with user-select: none
* fix: prevent furigana text from being copied via ruby transformer
* fix: register ruby transformer in FoliateViewer pipeline; use span wrapper for reliable ::before rendering
* refactor(reader): simplify furigana copy exclusion
Drop the ruby transformer and the .rt-text::before pseudo-element
wrapping. Instead, pass ['rt'] to getTextFromRange unconditionally so
furigana is excluded from annotator/translation/copy text extraction,
and let `rt { user-select: none }` handle the native selection cursor.
Avoids DOM rewriting and HTML-entity round-tripping in the data-text
attribute, and keeps <rt> text in the DOM for TTS, in-page find, and
screen readers.
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>
|
||
|
|
d326e1c73d |
fix: hide popup triangle when inside popup + EPUB image-only paragraph rendering (#4121)
- Popup: hide the inner triangle when its anchor point lands inside the popup body. Extracted as a generic `isPointInRect` helper in `sel.ts` (with a default 1px padding so edge cases stay visible). - style.ts: handle `<p[width][height]><img></p>` (common in some MOBI conversions) — clear hardcoded width/height and apply multiply blend for dark themes so the image doesn't sit on a colored box. - Annotator: shrink dict popup height from 480 to 360 to fit smaller screens. - foliate-js: submodule bump. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
772bb73b46 |
ui/ux: codify design system and migrate settings to shared primitives (#4116)
* 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> |
||
|
|
295a588988 |
feat(share): route annotation exports through the system share sheet (#4107)
Adds a `share` flag and `sharePosition` to `saveFile` across the app
services. On iOS/Android/macOS/Windows the annotation export now calls
the sharekit `shareFile` (writing the markdown/txt to `$TEMP` first when
no `filePath` is provided), so users get the system "Share via…" sheet
that drops the export into Mail, Notes, Messages, etc. Linux desktop
keeps the existing save dialog, since sharekit has no Linux backend.
On the web, `saveFile` now prefers `navigator.share({ files })` when the
browser advertises support via `canShare`. AbortError (user dismissed)
is treated as a deliberate "don't share" choice; any other rejection
(e.g., Chrome desktop's `NotAllowedError` despite a positive `canShare`)
falls through to the `<a download>` fallback so a save still happens.
Also fixes the macOS share popover anchoring: `preferredEdge: 'top'`
maps to `NSMaxYEdge`, which is the rect's bottom edge in WKWebView's
flipped coords, so the picker rendered below the trigger button. The
annotations export only got away with it because its dialog has no room
below — macOS auto-flipped above. Switching to `preferredEdge: 'bottom'`
(`NSMinYEdge` → top edge in flipped coords) anchors the popover above
the button consistently. Adds `$TEMP/**/*` to the Tauri fs capabilities
so the writable temp share file is permitted.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|