bfb85c2f68e45fbc3e3d825d1bb7fe8e7f77167d
50 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
bfb85c2f68 |
feat(reader): sync paragraph mode & speed reader with TTS read-along (#3235) (#4576)
* docs(reader): TTS-sync design spec for paragraph mode + RSVP (#3235) Hardened via brainstorming + /autoplan (CEO/Design/Eng dual-voice review). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tts): emit canonical tts-position event from TTSController (#3235) Controller emits { cfi, kind, sectionIndex, sequence } alongside the existing tts-highlight-mark/-word events. Monotonic sequence lets downstream consumers (paragraph mode, RSVP — later slices) drop out-of-order positions. Additive; existing events untouched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reader): containment+cursor CFI->index mappers for TTS sync (#3235) RSVPController.syncToCfi + setExternallyDriven: containment match (fixes mid-token skip), monotonic cursor + binary search (avoids O(N)-per-word jank, no per-word getCFI), -1/no-op on no match (no silent jump to word 0), timer suspension while externally driven. ParagraphIterator.findIndexByRange: hinted + binary-search containment mapper returning -1 on no match (never first()). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(tts): forward tts-position + tts-playback-state onto the app bus (#3235) useTTSControl republishes the controller's canonical tts-position (tagged with bookKey) via a dedicated listener — NOT inside the suppression-gated highlight handlers, so page-follow suppression can't silently desync the modes. Adds tts-playback-state (playing/paused/stopped) so RSVP can track playback without the hook-local isPlaying. Verified by extending the real-foliate-view browser harness. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reader): paragraph mode follows TTS playback (#3235) When paragraph mode + TTS are both active, the focused paragraph follows the spoken position (sentence granularity, all engines). Section-generation contract (stash cross-section position, apply after the iterator re-inits); sync-focus path that does NOT arm isFocusingRef (avoids the relocate-eaten wrong-section paragraph-0 bug); stale-sequence drop; decouple on manual nav, re-engage on next playing. Start-alignment + visible indicator deferred to later slices. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(rsvp): speed reader follows TTS playback (#3235) Edge word-boundary voices: RSVP shows the spoken word via syncToCfi. Non-Edge (sentence-only) voices: sentence-paced estimator (clamp 60..600 wpm from voice rate, hold at +60 words cap, snap to first word on each new sentence mark). RSVP auto-advance suspended while TTS-driven. Decouple on manual nav via a rsvp-manual-nav signal; re-engage on next playing. Cross-section positions re-extract then apply. Pure decideRsvpTtsPosition helper unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reader): fixed-layout gate + ttsSyncStatus for TTS sync (#3235) Gate sync to reflowable books (D7): fixed-layout reports 'unsupported' and never engages. Both modes expose ttsSyncStatus (idle/following/syncing/ decoupled/unsupported) as the data source for the upcoming indicator. RSVPControl now forwardRef-exposes the status via an imperative handle. Cross-bookKey events ignored (regression-tested). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reader): 'following audio' indicator for TTS sync (#3235) 5-state pill (following/syncing/decoupled, idle+unsupported render null) shown top-center in the paragraph overlay and as a status row in the RSVP overlay. Decoupled state is the tap-to-resume control; first decouple fires a one-time toast. eink-bordered, glyph+text (no color-only), RTL logical props, touch targets, safe-area top inset. RSVP 'plain' variant matches its themed surface; non-Edge shows '· estimated'. New i18n keys need extraction before merge. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(rsvp): in-overlay TTS toggle + audio-paced speed control (#3235) Voice-glyph audio toggle in the RSVP control row (trailing, by the gear) starts/ stops read-along from inside the full-screen overlay, start-aligned to the current word (range validated against the live doc). While TTS-driven, the WPM control shows a locked 'Audio pace' affordance that opens a compact rate picker; rate changes go through a new tts-set-rate bus event reusing the existing throttled setRate path. Pure buildRsvpTtsSpeakDetail helper unit-tested. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(reader): e2e paragraph mode follows TTS across a section boundary (#3235) Real <foliate-view> browser e2e: with paragraph mode active, the focused paragraph follows the TTS walk and re-targets to the new section after a Ch4->Ch5 boundary (proves no stuck wrong-section paragraph-0 / isFocusingRef trap). Asserts on the owning section of the current range. Test-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * i18n(reader): translate TTS-sync strings across 33 locales (#3235) Following audio / · estimated / Resume audio / Stopped following audio / Play audio / Pause audio / Audio pace / Speed follows audio. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(reader): resolve TTS CFI anchors across iframe realms (#3235) RSVP and paragraph follow silently failed to track the spoken word: the CFI anchor from view.resolveCFI(...).anchor(doc) is a Range created in the book iframe's realm, so 'anchor instanceof Range' (top realm) was always false (cross-realm instanceof) -> resolveCfiToRange/applySyncCfi returned null -> syncToCfi never advanced. Add isRangeLike() duck-type (cloneRange is unique to Range) and use it at all 4 CFI-resolution sites. Confirmed live via CDP: before = syncToCfi false (frozen); after = exact word map + RSVP follows Edge TTS at ~171 wpm (audio pace). Unit tests reproduce the cross-realm anchor (jsdom is single-realm so the old code passed there but died in the app). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(rsvp): stop estimator/word fight + map transport to TTS play/pause (#3235) Two read-along refinements (verified live via CDP with Edge TTS): 1. No more jump-ahead-then-snap-back flashing. Word-boundary engines (Edge) emit BOTH sentence marks and word boundaries; RSVP was routing sentence -> the estimator (self-paces ~190xrate, up to +60 words ahead) while word positions snapped it back. Now once a word position is seen, sentence positions are ignored and any running estimator is stopped, so words alone drive RSVP. 2. The RSVP transport (center play/pause, Space, center-tap) maps to TTS play/pause while read-along is engaged (tts-toggle-play), instead of RSVP's own suspended timer. Pausing TTS keeps RSVP suspended (no runaway); a full stop releases it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(rsvp): keep indicator on pause + reach dict management from RSVP (#3235) - Pausing read-along no longer dismisses the 'following audio' indicator / 'Audio pace' lock (layout shift). New 'paused' sync status keeps the indicator row and WPM lock present while TTS is engaged-but-paused; only a full stop clears them. Verified live via CDP: pause keeps the layout, no shift. - Dict management is reachable from RSVP: the settings dialog is z-50, far below the full-screen RSVP overlay (z-[10000]), so it opened invisibly behind it. handleManageDictionary now exits RSVP first (position saved/resumable) so management shows over the reader. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(rsvp): show dict management over RSVP instead of exiting it (#3235) Per feedback: opening dictionary management from the RSVP lookup popup no longer closes the speed reader. The settings dialog is raised above the full-screen RSVP overlay (z-[10000] -> SettingsDialog !z-[10050]) so it shows on top, and RSVP's capture-phase keyboard handler bails while the settings dialog is open so its inputs accept Space and Escape closes settings (not RSVP). Verified live via CDP: management opens over RSVP, RSVP stays active behind it, Escape returns to it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(dict): only apply drag-handle margin compensation when the handle shows (#3235) The dictionary sheet header used -mt-4 to compensate for Dialog's drag handle, but that handle is sm:hidden (shown only below sm). On sm+ the handle is display:none, so -mt-4 pulled the header up into the top edge (broken layout when the lookup renders as a sheet on a short/wide window). Mirror the handle's breakpoint: -mt-4 sm:mt-0. Verified live via CDP. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reader): in-mode TTS audio toggle for paragraph mode (#3235) Paragraph mode already follows TTS, but there was no way to start read-along from inside it. Add an audio toggle to the ParagraphBar (mirroring RSVP's): it starts TTS start-aligned to the focused paragraph (range validated live, + section index) and stops it. Track session-active vs playing so a pause keeps the indicator ('paused' status) instead of collapsing to idle. Pure buildParagraphTtsSpeakDetail helper unit-tested. Verified live via CDP: tapping the icon starts audio from the focused paragraph and the focus follows speech. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(reader): highlight current TTS word/sentence in paragraph mode (#3235) Paragraph mode follows TTS by advancing the focused paragraph, but the spoken word wasn't highlighted within it like normal mode. The overlay renders a CLONE of the paragraph, so the iframe's TTS highlight isn't visible there — reproduce it on the clone with the CSS Custom Highlight API (no DOM mutation, spans inline boundaries natively, leaves the fade-in animation untouched). - TTSController already tags tts-position with kind word|sentence. The hook decides granularity: word boundaries (Edge) drive a per-word highlight; once seen, the coarse sentence event is skipped so the whole sentence doesn't flicker over the current word. Engines without word boundaries (WebSpeech/Native) fall back to the sentence highlight. - Offsets are computed relative to the paragraph start (so they map 1:1 onto the clone's text) and tagged with the paragraph index so a stale highlight never paints the wrong paragraph. Cleared on stop / section change / disabled. - The ::highlight() style mirrors the user's ttsHighlightOptions color+style. Pure helpers (offset math, word/sentence decision, css builder) unit-tested. Verified live via CDP: word highlight tracks Edge word-by-word and follows across paragraph boundaries (news -> ... -> ladies), matching the TTS color. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
5a8f0873fa |
fix(library): refresh book cover after editing metadata (#4572)
* fix(library): refresh book cover after editing metadata Editing a book's cover in Book Details and saving showed the old cover until a full reload, in two render paths: - Library grid: handleUpdateMetadata mutated the book object in place, so the memoized <BookCover> compared fields off the same (mutated) reference and skipped re-rendering. Build a new book object via the new getBookWithUpdatedMetadata helper instead of mutating. - Book Details view: BookDetailView renders cover/title/author from the modal's `book` prop, which the parent never re-passed after save. BookDetailModal now tracks the saved book locally (displayBook) and renders the view from it. Adds a unit test for the immutable helper, a BookDetailModal regression test (edit cover -> save -> view reflects it), and a sample-alice.txt fixture for TXT import testing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * chore(agent): add cover-refresh stale-render memory Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
4b0bbc77b0 |
fix(reader): open TXT files shared via "Open with" (#4571)
* fix(reader): open TXT files shared via "Open with" by converting to EPUB
The Android "Open with Readest" (VIEW intent) transient path hands the
reader the original .txt file (its filePath points at the content:// URI),
unlike the managed library which stores the already-converted EPUB. The
DocumentLoader had no branch for a raw .txt, so open() returned
{ book: null } and initViewState crashed with
"TypeError: Cannot read properties of null (reading 'metadata')",
leaving the user stuck on the library splash.
Add an isTxt() check that converts the raw .txt to EPUB in-memory (the
same TxtToEpubConverter the import path runs) and parses that. The
converter emits a .epub-named file, so the importer's own
DocumentLoader.open() on the converted file is unaffected.
Verified on-device (emulator, warm + cold start): the TXT now opens and
renders in the reader instead of crashing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(settings): allow adjusting highlight opacity in e-ink mode
Drop the isEink prop that disabled the highlight Opacity slider under
e-ink. Opacity is still meaningful on e-ink, so let users change it.
Removes the prop from HighlightColorsEditor, its ColorPanel call site,
and the test render helper.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(send): mock fetch to fix flaky article conversion test
The article/page conversion paths fetch a favicon + author image for the
synthetic cover via globalThis.fetch. In jsdom that hit the real network:
a live fetch to the sample URL can hang up to faviconFetcher's 6s timeout,
exceeding the 5s test timeout and intermittently failing the suite. Stub
fetch so the cover falls back to its initial-letter tile (the pattern other
tests in this suite already use). Article test: ~5003ms hang -> ~80ms.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(agent): update annotation-share-toolbar memory
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
67c22c770b |
feat(reader): Share intent + customizable annotation toolbar (#4014) (#4570)
* docs(spec): annotation Share tool + customizable toolbar (#4014) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(plan): implementation plan for Share tool + customizable toolbar (#4014) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(annotator): add 'share' annotation tool type and button (#4014) * feat(annotator): add pure toolbar order/visibility helpers (#4014) * feat(annotator): add annotationToolbarItems view setting (#4014) * feat(annotator): add shareSelectedText ladder helper (#4014) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(annotator): render Share tool and honor toolbar order in selection popup (#4014) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(settings): add drag-and-drop annotation toolbar customizer (#4014) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(settings): open the toolbar customizer from the Behavior panel (#4014) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(i18n): extract and translate annotation share/toolbar strings (#4014) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(annotator): extract canShareText helper, preserve hidden Share on cross-platform edit (#4014) Addresses final-review findings: de-duplicate the triplicated canShare definition into share.ts::canShareText, trim ShareCapableService to the fields actually read, and stop the toolbar customizer from dropping a synced 'share' tool when edited on a non-share-capable device. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(settings): WYSIWYG drag-and-drop toolbar customizer (#4014) Rework the customizer per live testing: - Render 'In toolbar' as a faithful, content-width, start-aligned preview of the real selection popup (gray bar, icon-only buttons); 'Available' tools show as labeled chips. - Multi-container dnd-kit pattern: in-place dragging (no DragOverlay, which a transformed modal offsets), pointerWithin collision so empty zones accept drops, live onDragOver reparent, itemsRef to dodge dnd-kit's drag-start handler-capture stale closure. - Add 'Add all' (canonical predefined order) and 'Clear all' shortcuts. - Align zone labels with the SubPageHeader breadcrumb. - Empty toolbar now suppresses the selection popup entirely (no empty bar), while still allowing highlight-edit/notes popups. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore(i18n): translate Add all / Clear all toolbar shortcuts (#4014) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(annotator): size selection popup to visible tool count (#4014) With the customizable toolbar a fixed-width popup looked sparse for a 2-3 tool toolbar (buttons spread to the corners). Size the popup to the number of visible tools (responsive) capped at the previous max; annotated selections keep the max width since they show highlight options / notes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(settings): reword empty-toolbar hint to 'No tools, drag one here' (#4014) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * test(annotator): render default tools (not Share) in popup layout screenshot (#4014) The visual regression test rendered every annotationToolButtons entry, so adding the Share tool shifted the toolbar to 9 buttons and broke the baselines. Share is hidden by default (added via Customize Toolbar), so the popup screenshot should mirror the default-enabled set — filter to DEFAULT_ANNOTATION_TOOLBAR_ITEMS, keeping the existing baselines valid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
59d4f0aa33 |
perf(reader): split progress into its own store to cut React commit storm (#4557)
setProgress was called multiple times per swipe burst, each call writing into readerStore.viewStates[key].progress. ~65 places in the reader subtree subscribed to useReaderStore() without a selector, so every setProgress fan-out re-rendered all of them -- even the 51 that didn't care about progress. On Android release builds this showed up as Layout = 9.8% and Function Call = 9.6% of main-thread self time in Chrome DevTools' Bottom-Up profile during a reading session. Fix: - New tiny store store/readerProgressStore.ts holds the per-book BookProgress map. setBookProgress only fires its own subscribers. - readerStore.setProgress now writes progress to the new store and only touches bookDataStore for the primary view (secondary parallel views shouldn't overwrite the shared config). - readerStore.getProgress is kept as a delegating facade so existing imperative call sites don't break. - Components / hooks that genuinely need to react to progress changes subscribe via the new useBookProgress(bookKey) hook. The handful of call sites that just want a one-shot read use getBookProgress(key) so they don't subscribe at all. - readerStore.clearViewState calls clearBookProgress so the map doesn't grow unbounded across book opens/closes. See store/readerProgressStore.ts header for the full rationale. |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
88d8aa285f |
feat(metadata): show file path for in-place imported books (#4508)
In-place imports point at a file the user keeps under one of their external library folders (book.filePath set), as opposed to hash-copy imports that live anonymously under Books/<hash>/. The book details view didn't surface where an entry actually lives on disk, so users had no way to tell the two storage modes apart or locate the source file. Add a 'File Path' row to the metadata grid that renders only when book.filePath is set, breaks long paths across lines, and exposes the full string via a hover title for paths that overflow the row. |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
1705006b6b |
fix(mobile): iOS PIN keyboard UX + Safari font line-height in EPUBs (#4120)
- AppLockScreen: pad the lock screen bottom by the on-screen keyboard height tracked via visualViewport, so the flex-centered PIN sits above the keyboard on iOS WKWebView where dvh does not shrink. - AppLockScreen: skip stickyFocus on mobile. iOS will not pop the keyboard from a programmatic .focus(), so the cursor would blink with no input — wait for the user's tap instead. - PinInput: forward autoFocus to the input when autoFocus or stickyFocus is set, for more reliable mount-time focus. - style.ts: give legacy <p><font>...</font></p> its own block context so iOS Safari applies the inherited line-height. 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> |
||
|
|
ae42dcb53a |
fix(txt): parse author from txt filename and use edited metadata on fallback cover (#4095) (#4102)
When importing a `.txt` file the author field stayed empty unless the text content itself contained an `作者:…` header, even when the filename already encoded it. Common Chinese naming patterns like `《书名》作者:张三.txt`, `《书名》[张三].txt`, or `《书名》张三.txt` now contribute the author when the file body doesn't. - Added `extractTxtFilenameMetadata` in `utils/txt.ts` and replaced the ad-hoc `extractBookTitle` regex used by both convertSmallFile and convertLargeFile. Content-extracted author still wins; the filename author is the next fallback before the caller-provided one. - `BookCover` now reads `book.author || book.metadata?.author` so the author typed into the metadata edit dialog shows on auto-generated fallback covers when the original `book.author` was empty. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
77a85cee09 |
feat(account): show daily reset countdown under translation quota bar (#4082)
Adds a row beneath the translation characters bar on the user profile page with "X% used" (start) and "Resets in H hr m min" (end). The countdown points to the next UTC midnight, matching the server-side daily-usage key in UsageStatsManager. Formatting goes through the dayjs duration plugin and ticks every minute while the page is open. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
cbdc3b8f52 |
feat(sync): wire dictionary store through replica sync (follow-up to #4075) (#4076)
* feat(sync): cross-device dictionary sync Custom MDict / StarDict / DICT / SLOB dictionaries now sync across signed-in devices via the replica layer. - Store mutations publish replica rows with field-level LWW + tombstones. - Re-importing the same content (renamed or after delete) preserves the user's label and reincarnates the server row instead of duplicating. - Manifest commits after binary upload so other devices never see a row whose binaries aren't on cloud storage yet. - Pull-side orchestrator creates a placeholder dict, queues the binaries via TransferManager, and clears the unavailable flag on completion. - Toast copy branches by transfer kind so dict uploads don't read "Book uploaded". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(sync): boot pull and binary download path - Defer the boot pull until TransferManager is initialized so download enqueues aren't dropped. - Auto-persist the local dict store after applyRemoteDictionary; otherwise the next loadCustomDictionaries wipes the in-memory rows. - Boot pull passes since=null so a device whose cursor advanced past unpersisted rows can still recover. - Skip pulling when not authenticated instead of logging "SyncError: Not authenticated" on every boot of a signed-out device. - downloadReplicaFile resolves the destination against the kind's base dir; binaries previously landed at the literal lfp and openFile then failed with "File not found". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor(sync): per-page useReplicaPull hook Lifts the boot-time pull out of EnvContext into a hook each page mounts for the kinds it needs: useReplicaPull({ kinds: ['dictionary'] }). Library page and the shared Reader component opt in. The hook fires 10s after page load (so feature mounts hydrate first), dedups per-kind across navigation, and releases the slot on failure so a later mount can retry. Future kinds plug into the hook's per-kind switch. Also closes two refresh-loop bugs: - Hydrate the dict store from settings BEFORE the apply loop, so the auto-persist doesn't clobber persisted rows that the in-memory store hadn't yet read. Library-page refresh was the visible victim. - Skip the download queue when every manifest file is already on disk under the resolved bundle dir. Refreshing is a no-op; partial- download recovery still queues because some files would be missing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
06aec0b597 |
fix(reader): revert footer to default visibility when tap-to-toggle is disabled (#4065)
Tapping the footer with `tapToToggleFooter` on cycles `progressInfoMode` through values including 'none', which persists to view settings. When the user later disabled the toggle in settings, nothing reverted the saved mode — so the footer stayed hidden with no UI path back to a visible state, only re-enabling the toggle and tap-cycling forward. ProgressBar now self-heals: when `tapToToggleFooter` is off and the current mode isn't already 'all', it resets to 'all'. Fires both at mount (book opened with stuck 'none') and on the toggle transition. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d66fedcab7 | feat(reader): manage rules shortcut in proofread popup (#4062) | ||
|
|
293d5b5f5d |
fix(rsvp): cross-device resume seeding + mobile slider drag (#4004)
* fix(rsvp): seed local position from synced BookConfig on resume * refactor(rsvp): simplify seedPosition Consolidate the matched/mismatched write paths into one localStorage write, extract stripCfiPath() to a module-level helper, and trim the comments around it. * fix(rsvp): make progress bar draggable on mobile Three coordinated fixes so the RSVP overlay's seek bar works reliably under touch: - Add `touch-action: none` on the slider so the mobile browser stops claiming the gesture for scroll/pan and firing pointercancel mid-drag. - Hoist the `.rsvp-controls`/`.rsvp-header` exclusion to the top of the overlay's touchend handler so a successful drag isn't immediately re-interpreted as a speed-change swipe. - Guard `releasePointerCapture` with `hasPointerCapture` so pointercancel arriving after the browser has already released capture (multitouch, app backgrounding) no longer throws NotFoundError. |
||
|
|
5a0a70a30a |
feat(reader): custom dictionaries (StarDict + MDict) (#4012)
* feat(reader): custom dictionaries (StarDict + MDict) Adds a pluggable dictionary provider system. Built-in Wiktionary + Wikipedia (extracted from the legacy single-popup model into a tabbed shell) plus user-importable StarDict (.ifo/.idx/.dict.dz/.syn) and MDict (.mdx/.mdd) bundles. Settings → Language → Dictionaries: import / enable / drag-reorder / delete (delete-mode toggle mirrors CustomFonts). Drag uses @dnd-kit with pointer/touch/keyboard sensors. Reader popup: tabbed UI, per-tab lookup history, scroll-aware back button, last-active tab persists. Tabs grow to natural width up to a cap, truncate with ellipsis when crowded; phantom bold layer prevents layout shift on focus. StarDict reader is self-contained (replaces unused foliate-js/dict.js), with lazy random-access binary search on .idx + .syn (~420 KB Int32Array of byte offsets vs ~10 MB of parsed JS objects), lazy DictZip chunk decompression via fflate streaming Inflate (cmudict/eng-nld both chunked), and an optional .idx.offsets sidecar generated at import to skip the init scan. Cmudict 105K-entry init drops from ~10 MB heap and 2 MB IO to ~1.7 MB heap and ~500 KB IO. MDict uses the readest/js-mdict fork (added as a submodule, consumed via tsconfig paths so deps stay out of readest's pnpm-lock) which adds a browser-friendly BlobScanner reading via blob.slice(...).arrayBuffer() — slices are lazy when the Blob is Readest's NativeFile / RemoteFile. encrypt=2 (key-info-only) MDX is fully supported via ripemd128-based mdxDecrypt; encrypt=1 (record-block, needs user passcode) surfaces as unsupported. Wikipedia annotation tool removed (Wikipedia is now a tab inside the unified popup); legacy WiktionaryPopup / WikipediaPopup deleted. Stale annotationQuickAction === 'wikipedia' coerced to 'dictionary' on settings load. iOS-friendly external links: skip target="_blank" on Tauri to avoid the WebView's "open externally" path triggering the shell scope error; the popup's container click handler routes through openUrl. i18n: 939 strings translated across 31 locales (30 base keys + CLDR plural forms for ar/he/sl/pl/ru/uk/ro/it/pt/fr/es). Test fixtures bundled: cmudict (StarDict, 105K entries), eng-nld (StarDict, smaller), and a Longman Phrasal Verbs MDX (encrypt=2). 3396 unit tests pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix(stardict): resolve fflate from js-mdict source for vitest + Next js-mdict is consumed as TypeScript source via tsconfig paths from packages/js-mdict/src/. Its sources `import 'fflate'` directly, but fflate is only installed under apps/readest-app/node_modules — so vite's import-analysis (and Next/Turbopack's resolver) can't find fflate when it walks up from the redirected js-mdict source location. CI's fresh checkout exposes this; locally a leftover packages/js-mdict/node_modules/fflate from the old workspace setup masked it. Pin fflate resolution to apps/readest-app/node_modules/fflate in: - vitest.config.mts (Vite alias) - next.config.mjs (webpack alias + Turbopack resolveAlias — Turbopack rejects absolute paths so use a project-relative form) - tsconfig.json (paths entry so tsgo / Biome see it) Verified by deleting packages/js-mdict/node_modules locally and re-running pnpm test (3396 pass), pnpm lint (clean), and both pnpm build-web and a tauri-platform Next build (clean). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
4b0720a3e3 |
perf(rsvp): windowed context, extraction caching and lazy CFI for sections with thousands of words, closes #3953 (#3984)
* perf(rsvp): windowed context, extraction caching and lazy CFI for sections with thousands of words, closes #3953 * i18n: update translations |
||
|
|
6d5e59c79a | fix(rsvp): resume at stop word, prevent section replay, restore full context (#3960) | ||
|
|
63b0b87028 | fix(layout): fixed dropdown menu layout for the delete button in details, closes #3940 (#3976) | ||
|
|
ec32614539 | fix(settings): fixed color picker for custom highlight colors, closes #3796 (#3857) | ||
|
|
7b60b1bb0c |
fix(ios): reduce GPU memory pressure to prevent WebKit GPU process crash (#3842)
On iOS, navigating to a book group in the library caused the WebKit GPU
process to exceed its 300 MB jetsam limit (peaking at ~328 MB), resulting
in a blank screen flash and broken scroll state.
Three changes reduce peak GPU memory usage:
- Add overscan={200} to VirtuosoGrid/Virtuoso so only items within 200px
of the viewport are rendered, limiting simultaneous image decoding
- Add loading="lazy" to both Image components in BookCover so the browser
defers decoding offscreen cover images
- Conditionally mount the <video> and <audio> elements in AtmosphereOverlay
only when atmosphere mode is active, eliminating idle H.264 decoder
memory overhead
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||
|
|
ae2c421938 |
fix(ui): restore highlight options layout and clean up color name editing (#3776)
* fix(ui): restore highlight options layout and clean up color name editing Restore the pre-#3741 layout for both the AnnotationPopup highlight options row and the HighlightColorsEditor settings panel. HighlightOptions: re-add `justify-between` on the outer container and remove `flex-1` from the color strip so the gap between the style box and color strip responds to the number of colors. HighlightColorsEditor: restore `grid-cols-3 sm:grid-cols-5` grid, remove always-visible name inputs, and add a click-to-edit popover on each color circle with hover tooltip for the label. Add a browser screenshot test that renders the real AnnotationPopup component with Tailwind CSS and compares against baseline PNGs for 5, 10, and 15 colors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
b9a2b10fac | fix(a11y): fixed keyboard activation of dropdown menu (#3762) | ||
|
|
888f4afde9 |
fix: preserve paragraph mode reading layouts and other UI/UX fixes (#3730)
* fix: paragraph mode * test: paragraph mode * test: remove paragraph mode * fix: paragraph utils * fix: paragraph hook * fix: paragraph overlay * fix: paragraph bar * fix: paragraph control * fix: paragraph shortcuts * test: paragraph utils * test: paragraph hook * test: paragraph overlay * test: paragraph shortcuts * fix: paragraph overlay * test: paragraph mode * test: shortcuts * test: remove overlay * test: remove hook * test: remove utils * fix: paragraph overlay * fix: paragraph overlay * feat: paragraph overlay * fix: vertical container sizing * test: paragraph container * fix: paragraph animation * fix: paragraph text animation * fix: remove container morph |
||
|
|
9ecb9b24d2 |
feat: make reading ruler selection and step navigation coherent (#3722)
* refactor(reader): extract reading ruler math helpers * fix(reader): keep text selectable inside reading ruler * feat(reader): route taps to ruler step navigation * feat(reader): route keyboard navigation to ruler steps * fix(reader): animate ruler mask and frame together * fix(reader): preserve drag anchor for ruler handles * fix(reader): fall back to page turns at ruler edges |
||
|
|
956c71cd7b |
chore: migrate from ESLint to Biome for linting (#3694)
Replace ESLint with Biome for ~14x faster linting (~360ms vs ~5s). - Add biome.json with rules matching ESLint parity (Next.js, a11y, TypeScript, unused vars/imports) - Remove eslint, @typescript-eslint/*, eslint-config-next, eslint-plugin-jsx-a11y, @eslint/* from deps - Remove eslint.config.mjs - Update lint script to: tsgo --noEmit && biome check . - Fix 11 real code issues caught by Biome (banned types, explicit any, unsafe finally, unreachable code, shadowed names) - Disable Biome formatter (Prettier stays for Tailwind class sorting) Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
e0cf7e8d9f | fix: handle number input value clip in settings on mobile devices (#3582) | ||
|
|
907b672313 | fix(a11y): improve TOC screen reader accessibility for NVDA, closes #3477 (#3509) | ||
|
|
8850e6c00f |
feat(pdf): support TTS and annotation on PDFs, closes #2149 & #3462 (#3493)
* chore: bump jsdom to the latest version * feat(pdf): support TTS and annotation on PDFs, closes #2149 & closes #3462 |
||
|
|
ce53cd2b47 | feat: Readwise highlights sync (#3311) | ||
|
|
d9a6cffe78 | feat(discord): support show reading status with Discord Rich Presence, closes #1538 (#2998) | ||
|
|
3146ae48a7 | fix(proofread): support replace text with space (#2965) | ||
|
|
7d97826e4b | feat(tts): replace words for TTS, closes #2057 (#2952) | ||
|
|
6984393ed1 | refactor(proofread): refactor UI and i18n for proofread tool (#2783) | ||
|
|
8a4e22e423 | refactor: temporarily disable the proofreading feature for a hotfix release ahead of a major refactor (#2742) | ||
|
|
54fdf5f1fd |
feat(replacement): text replacement feature for EPUB books (#2725)
* add: basic ui replacement menu * feat(replacement): modified ViewSettings interface and added Replacement type * add: frontend menu ui to annotation settings - create replacementoptions file for 4 fix options: fix once, fix in library, fix in book, fix in library -integrate with annotator.tsx only frontend changes, but initialzied in backend * add: delete global option and click gear option to get rid of menu * docs: add test cases for replacementoptions file * edits to enable readest to build * basic changes for rule types * replacement transformer file added * additional support code added * interim updates to replacement.ts file * adding console log statements to confirm functionality without frontend * adding more console logs for debugging; i think i got my replacement working, will clean console logs and add actual tests now. * figured out how to get my transformer to work. replacement doesnt actually work yet. figuring that out rn. committing before i destroy something, lol * replcement logic working with hard coded tests. code is cleaned up with minimal console logs. actual replacement logic + testing is next :) * test suite built, and fully passing. made consle log edits too. * added more replacement rules, but figuring out why they arent being implemented by my code. * cleaning up test suite to not break when there are 0 rules; test is commited with 1 local rule. not sure if that rule is going to copy over when i merge. * feat(replacement): Add text field, case sensitivity checkbox, and confirmation dialog to ReplacementOptions - Add text input field for replacement text with placeholder - Add 'Case Sensitive' checkbox (default: unchecked/case-insensitive) - Implement two-step confirmation flow with Back/Confirm buttons - Show preview of original text, replacement text, scope, and case sensitivity - Disable scope buttons until replacement text is entered - Display truncated preview for long selected text (>50 chars) - Export ReplacementConfig type for use in parent components * feat(replacement): Add 30-word limit and integrate new ReplacementOptions component - Add MAX_REPLACEMENT_WORDS constant (30 words) - Add getWordCount() utility function for word counting - Show warning toast when word limit exceeded on Text Replacement click - Replace old fix handlers with single handleReplacementConfirm() - Integrate with new ReplacementConfig (replacementText, caseSensitive, scope) - Display success toast with scope and case sensitivity info on confirm * fix(build): Add ReplacementMenu placeholder component - Create placeholder component to fix missing import error in reader/page.tsx - Component returns null for now, to be implemented with global replacement rules * test(replacement): Add comprehensive tests for ReplacementOptions and word limit ReplacementOptions.test.tsx: - Test rendering of text input, checkbox, and scope buttons - Test case sensitivity checkbox toggle and state - Test disabled buttons when no replacement text entered - Test confirmation dialog flow and Back/Confirm buttons - Test click outside and Cancel button behavior - Test full replacement flow with all options wordLimit.test.ts: - Test word counting with various inputs (spaces, newlines, unicode) - Test 30-word limit boundary conditions - Test case-sensitive vs case-insensitive matching logic - Test edge cases (empty string, long words, punctuation) * refactor: removed unused initial definition of Replacement * feat: added replacement rules window in bookmenu * test: added tests to verify the replacement rules window renders book and global replacement rules, and it opens when bookmenu item is clicked * feat: added Replacement tab in SettingsDialog, displays global rules * feat(replacement): connected front-end to functions. todo: fix the automatic reload functionality. * fix(replacement): simplified re-rendering logic, doesn't fail on epubs anymore. * test: add integration tests for text replacement functionality * fix: added single rules section to ReplacementRulesWindow * fix(replacement): added null checks to some unsafe calls in integration tests * fix(replacements): added non-null assertion operator for a previously initialized variable * refactor: created ReplacementPanel and edited style of inputs * feat: disable the edit feature for selected phrase * refactor: use toast instead of banner for confirmation msg * feat: automatically reload the page to apply changes * feat: disable global rule for book if deleted in book view * fix(replacement): Improve popup positioning and eliminate ghost animation - Add viewport boundary detection to keep popup within visible area - Calculate position only once on mount to prevent jumping when other UI appears - Use visibility: hidden until position is calculated to eliminate ghost animation - Add max-height with overflow-y: auto for scrollable content - Popup now appears directly in correct position without two-step animation * fix: implement single-instance replacement with persistence - Add sectionHref to TransformContext for section tracking - Add singleInstance, sectionHref, occurrenceIndex fields to ReplacementRule - Pass section name from FoliateViewer to transformer context - Switch transformer from DOM-based to string-based replacement - Handle single-instance rules with section matching and occurrence tracking - Update Annotator to track occurrence index and apply direct DOM changes - Persist single-instance rules for refresh survival Single-instance replacements now: 1. Apply immediately via direct DOM modification 2. Store occurrence index and section for precise targeting 3. Persist across page refreshes * fix: allow multiple single-instance replacements for same word Single-instance rules now always create new entries instead of merging. This fixes the issue where replacing multiple occurrences of the same word would overwrite previous rules. The transformer applies rules in sequence, so each rule targets occurrence index 0 of the current (modified) string, allowing cascading replacements to work correctly after refresh. * fix: prevent cascading replacements and add wholeWord support - Add wholeWord field to ReplacementRule for word boundary matching - Track replaced regions to prevent replacement text from being re-matched - Fix cascading replacement issue where replacement text was matched again - Apply replacements from right to left to preserve positions - Support whole word matching with \b boundaries for both single-instance and regular rules * Fix whole-word matching for replacement rules - Auto-enforce whole-word matching for simple word patterns (letters only) - Add HTML tag boundary checks to prevent matching across tags - Add double-check validation for whole-word matches - Prevent matching 'and' inside words like 'England', 'stand', 'understand' - Add comprehensive logging for debugging replacement issues * test: added rAF in setup to for ReplacementOptions tests * fix: only allow replacement for epubs, remove replacement rendering for non-epubs, add test cases * refactor: refactored replacement logic for case sensitivity and word boundaries * test: added tests for scope precedence and case sensitivity across scopes * refactor: removed unnecessary code from testing * feat: able to display, edit, and delete single-instance rules in book settings * fix: connected case sensitive checkbox to backend, fixed merge and delete logic * test: updated test cases to reflect changes on case sensitivity and rules rendering * test: modified ReplacementOptions test to remove unnecessary case sensitive check from merge * fix: add logic for grayed out button for non-epubs * chore: update foliate-js submodule from upstream merge * fix: resolve all TypeScript/ESLint linting errors - Fix prefer-const error in ReplacementOptions.tsx - Fix set-state-in-effect error in ReplacementRulesWindow.tsx (use lazy initializer) - Replace all @typescript-eslint/no-explicit-any with proper types (ReplacementRule, unknown, etc.) - Fix unused error variables in replacement.ts (prefix with _) - Remove unused eslint-disable directives - Add missing ReplacementRule import in ReplacementPanel.tsx * fix: add localStorage mock to vitest setup - Fixes test failures in ReplacementRulesWindow and SettingsDialog tests - localStorage mock ensures all Storage API methods are available in test environment * fix: resolve ESLint and TypeScript build errors - Fix all remaining @typescript-eslint/no-explicit-any errors in test files - Fix unused error variables in replacement.ts (prefix with _) - Fix TypeScript error in ReplacementRulesWindow.tsx (move @ts-ignore to correct location) - All ESLint checks now pass - Web and Tauri builds compile successfully * fix: remove lookbehind regex for browser compatibility - Replace lookbehind assertions (?<!...) with manual boundary checking - Add isUnicodeWordChar helper function for manual Unicode word boundary detection - Apply manual boundary checks in applyMultiReplacement and applySingleInstance - Fixes build_web_app check failures by avoiding lookbehind in compiled output - Maintains whole-word matching functionality for both ASCII and Unicode patterns * fix: update tauri-utils version to 2.8.1 to resolve duplicate symbol error - Update local tauri-utils version from 2.8.0 to 2.8.1 to match crates.io version - Fixes duplicate symbol __TAURI_BUNDLE_TYPE linker error - Ensures all dependencies use the same tauri-utils version * fix: use local tauri path directly to resolve version conflicts - Change tauri dependency to use local path instead of version requirement - This ensures all dependencies use the same local tauri version (2.9.3) - Fixes 'links = Tauri' conflict error in Rust linting - The patch.crates-io should still work for transitive dependencies * fix: use version requirement with patch for tauri dependency - Revert to using version requirement '2' instead of direct path - Rely on [patch.crates-io] to use local tauri version - Remove Cargo.lock to force fresh dependency resolution - This should resolve the 'links = Tauri' conflict by ensuring all tauri dependencies (direct and transitive) use the patched version * fix: remove plugin patches that cause resolution errors - Remove all tauri-plugin git patches from [patch.crates-io] - Keep only tauri, tauri-utils, and tauri-build patches - Plugins from crates.io will use the patched tauri via transitive dependencies - Fixes error: patch for tauri-plugin-oauth failed to resolve * fix: update tauri submodule with tauri-utils version fixes * fix: revert tauri submodule and update tauri-utils to 2.8.0 - Revert submodule changes that can't be pushed to remote - Update local tauri-utils version to 2.8.0 to match other packages - This avoids the need to modify the submodule * fix: add tauri-plugin to workspace and patch to resolve duplicate symbol error - Add packages/tauri/crates/tauri-plugin to workspace members - Add tauri-plugin patch to [patch.crates-io] - This ensures all tauri dependencies use local versions - Fixes duplicate symbol __TAURI_BUNDLE_TYPE linking error * chore: restore Cargo.lock from upstream - Restore the original Cargo.lock from readest/readest main branch - This ensures reproducible builds and matches upstream - The lock file will be updated by cargo when dependencies change * fix: resolve TypeScript errors in test files - Fix ReplacementOptions.test.tsx: add optional chaining for possibly undefined values - Fix ReplacementRulesWindow.test.tsx: use proper type assertions for store setState calls - Use (store.setState as unknown as (state: unknown) => void) pattern for partial state updates * fix: prevent race condition when deleting replacement rules rapidly - Add isReloading state to track ongoing delete/edit operations - Prevent multiple rapid deletions that cause runtime errors during page reload - Show warning toast when user tries to delete while reload is in progress - Add finally blocks to ensure isReloading is always reset - This prevents the 'book doesn't finish rerendering' error * fix: allow phrases and lines with quotes for single-instance replacements - Updated isWholeWord() to allow phrases (text with spaces or punctuation) - Phrases are always allowed for single-instance replacements - Only single words are checked for partial word matches - Fixes issue where lines with quotes couldn't be replaced - Added detailed logging for debugging phrase detection * fix: allow selections with boundary punctuation and fix pattern matching for punctuation - Updated isWholeWord() to explicitly allow selections that start or end with punctuation (e.g., 'tis, off;, look,) - Fixed normalizePattern() to handle patterns with leading/trailing punctuation correctly - Word boundaries are now only added around the word part, not the punctuation - Fixes issue where replacements like 'scholar;' were not matching correctly * fix: escape HTML entities in replacement text to preserve angle brackets - Added escapeHtmlEntities() function to escape HTML special characters - Apply HTML escaping to replacement text in both multi and single-instance replacements - Fixes issue where replacement text like '<<AND>>' was being interpreted as HTML tags - Angle brackets and other HTML entities are now properly escaped and displayed correctly * fix: revert Tauri backend changes and resolve package.json conflict - Revert Cargo.toml and src-tauri/Cargo.toml to match upstream/main - Resolve @tauri-apps/cli version conflict (2.9.5 -> 2.9.6) - These changes are not related to the replacement feature implementation * fix: update pnpm-lock.yaml to match @tauri-apps/cli 2.9.6 * removed useless tests and backend tests from ReplacementOptions integration testing suite * chore: revert foliate-js submodule to match readest/readest main * fix: refactored wordLimit logic into a separate util file * fix: removed additional pr description * refactor: rewrite replacement transformer to use DOM-based approach replace string manipulation with DOMParser and TreeWalker follow pattern from simpleecc transformer * style: format code with prettier * fix: remove unused string-manipulation functions * fix: refactored display dialog logic to match other dialogs * fix: enabled global rule deletion in book menu * fix: removed ReplacementPanel from library settings * fix: deleted SettingsDialog.replacement.test.tsx since we no longer need to display replacements in library settings * fix: removed text replacement tab from settings dialog * fix: applied prettier code formatter to replacement rules window * chore: fix formatting and remove unused file listed by chrox * chore: format all changed files from pr 2693 and revert pnpm-lock * rebased Cargo.lock, package.json, pnpm-lock.yaml to upstream main edits to enable readest to build * basic changes for rule types * replacement transformer file added * additional support code added * interim updates to replacement.ts file * adding console log statements to confirm functionality without frontend * adding more console logs for debugging; i think i got my replacement working, will clean console logs and add actual tests now. * figured out how to get my transformer to work. replacement doesnt actually work yet. figuring that out rn. committing before i destroy something, lol * replcement logic working with hard coded tests. code is cleaned up with minimal console logs. actual replacement logic + testing is next :) * test suite built, and fully passing. made consle log edits too. * added more replacement rules, but figuring out why they arent being implemented by my code. * cleaning up test suite to not break when there are 0 rules; test is commited with 1 local rule. not sure if that rule is going to copy over when i merge. * add: basic ui replacement menu * add: frontend menu ui to annotation settings - create replacementoptions file for 4 fix options: fix once, fix in library, fix in book, fix in library -integrate with annotator.tsx only frontend changes, but initialzied in backend * add: delete global option and click gear option to get rid of menu * docs: add test cases for replacementoptions file * feat(replacement): modified ViewSettings interface and added Replacement type feat(replacement): modified viewsettings interface and added ReplacementRulesConfig * feat(replacement): Add text field, case sensitivity checkbox, and confirmation dialog to ReplacementOptions - Add text input field for replacement text with placeholder - Add 'Case Sensitive' checkbox (default: unchecked/case-insensitive) - Implement two-step confirmation flow with Back/Confirm buttons - Show preview of original text, replacement text, scope, and case sensitivity - Disable scope buttons until replacement text is entered - Display truncated preview for long selected text (>50 chars) - Export ReplacementConfig type for use in parent components * feat(replacement): Add 30-word limit and integrate new ReplacementOptions component - Add MAX_REPLACEMENT_WORDS constant (30 words) - Add getWordCount() utility function for word counting - Show warning toast when word limit exceeded on Text Replacement click - Replace old fix handlers with single handleReplacementConfirm() - Integrate with new ReplacementConfig (replacementText, caseSensitive, scope) - Display success toast with scope and case sensitivity info on confirm * fix(build): Add ReplacementMenu placeholder component - Create placeholder component to fix missing import error in reader/page.tsx - Component returns null for now, to be implemented with global replacement rules * test(replacement): Add comprehensive tests for ReplacementOptions and word limit ReplacementOptions.test.tsx: - Test rendering of text input, checkbox, and scope buttons - Test case sensitivity checkbox toggle and state - Test disabled buttons when no replacement text entered - Test confirmation dialog flow and Back/Confirm buttons - Test click outside and Cancel button behavior - Test full replacement flow with all options wordLimit.test.ts: - Test word counting with various inputs (spaces, newlines, unicode) - Test 30-word limit boundary conditions - Test case-sensitive vs case-insensitive matching logic - Test edge cases (empty string, long words, punctuation) * refactor: removed unused initial definition of Replacement * feat: added replacement rules window in bookmenu * test: added tests to verify the replacement rules window renders book and global replacement rules, and it opens when bookmenu item is clicked * feat: added Replacement tab in SettingsDialog, displays global rules * fix: added single rules section to ReplacementRulesWindow * refactor: created ReplacementPanel and edited style of inputs * feat(replacement): connected front-end to functions. todo: fix the automatic reload functionality. * fix(replacement): simplified re-rendering logic, doesn't fail on epubs anymore. * test: add integration tests for text replacement functionality * fix(replacement): added null checks to some unsafe calls in integration tests * fix(replacements): added non-null assertion operator for a previously initialized variable * feat: disable the edit feature for selected phrase * refactor: use toast instead of banner for confirmation msg * feat: automatically reload the page to apply changes * feat: disable global rule for book if deleted in book view * fix(replacement): Improve popup positioning and eliminate ghost animation - Add viewport boundary detection to keep popup within visible area - Calculate position only once on mount to prevent jumping when other UI appears - Use visibility: hidden until position is calculated to eliminate ghost animation - Add max-height with overflow-y: auto for scrollable content - Popup now appears directly in correct position without two-step animation * fix: only allow replacement for epubs, remove replacement rendering for non-epubs, add test cases * fix: add logic for grayed out button for non-epubs * fix: resolve all TypeScript/ESLint linting errors - Fix prefer-const error in ReplacementOptions.tsx - Fix set-state-in-effect error in ReplacementRulesWindow.tsx (use lazy initializer) - Replace all @typescript-eslint/no-explicit-any with proper types (ReplacementRule, unknown, etc.) - Fix unused error variables in replacement.ts (prefix with _) - Remove unused eslint-disable directives - Add missing ReplacementRule import in ReplacementPanel.tsx * fix: add localStorage mock to vitest setup - Fixes test failures in ReplacementRulesWindow and SettingsDialog tests - localStorage mock ensures all Storage API methods are available in test environment * fix: implement single-instance replacement with persistence - Add sectionHref to TransformContext for section tracking - Add singleInstance, sectionHref, occurrenceIndex fields to ReplacementRule - Pass section name from FoliateViewer to transformer context - Switch transformer from DOM-based to string-based replacement - Handle single-instance rules with section matching and occurrence tracking - Update Annotator to track occurrence index and apply direct DOM changes - Persist single-instance rules for refresh survival Single-instance replacements now: 1. Apply immediately via direct DOM modification 2. Store occurrence index and section for precise targeting 3. Persist across page refreshes * fix: allow multiple single-instance replacements for same word Single-instance rules now always create new entries instead of merging. This fixes the issue where replacing multiple occurrences of the same word would overwrite previous rules. The transformer applies rules in sequence, so each rule targets occurrence index 0 of the current (modified) string, allowing cascading replacements to work correctly after refresh. * fix: prevent cascading replacements and add wholeWord support - Add wholeWord field to ReplacementRule for word boundary matching - Track replaced regions to prevent replacement text from being re-matched - Fix cascading replacement issue where replacement text was matched again - Apply replacements from right to left to preserve positions - Support whole word matching with \b boundaries for both single-instance and regular rules * Fix whole-word matching for replacement rules - Auto-enforce whole-word matching for simple word patterns (letters only) - Add HTML tag boundary checks to prevent matching across tags - Add double-check validation for whole-word matches - Prevent matching 'and' inside words like 'England', 'stand', 'understand' - Add comprehensive logging for debugging replacement issues * refactor: refactored replacement logic for case sensitivity and word boundaries * test: added tests for scope precedence and case sensitivity across scopes * refactor: removed unnecessary code from testing * feat: able to display, edit, and delete single-instance rules in book settings * fix: connected case sensitive checkbox to backend, fixed merge and delete logic * test: updated test cases to reflect changes on case sensitivity and rules rendering * test: modified ReplacementOptions test to remove unnecessary case sensitive check from merge * fix: resolve ESLint and TypeScript build errors - Fix all remaining @typescript-eslint/no-explicit-any errors in test files - Fix unused error variables in replacement.ts (prefix with _) - Fix TypeScript error in ReplacementRulesWindow.tsx (move @ts-ignore to correct location) - All ESLint checks now pass - Web and Tauri builds compile successfully * fix: update tauri-utils version to 2.8.1 to resolve duplicate symbol error - Update local tauri-utils version from 2.8.0 to 2.8.1 to match crates.io version - Fixes duplicate symbol __TAURI_BUNDLE_TYPE linker error - Ensures all dependencies use the same tauri-utils version * fix: use local tauri path directly to resolve version conflicts - Change tauri dependency to use local path instead of version requirement - This ensures all dependencies use the same local tauri version (2.9.3) - Fixes 'links = Tauri' conflict error in Rust linting - The patch.crates-io should still work for transitive dependencies * fix: use version requirement with patch for tauri dependency - Revert to using version requirement '2' instead of direct path - Rely on [patch.crates-io] to use local tauri version - Remove Cargo.lock to force fresh dependency resolution - This should resolve the 'links = Tauri' conflict by ensuring all tauri dependencies (direct and transitive) use the patched version * fix: remove plugin patches that cause resolution errors - Remove all tauri-plugin git patches from [patch.crates-io] - Keep only tauri, tauri-utils, and tauri-build patches - Plugins from crates.io will use the patched tauri via transitive dependencies - Fixes error: patch for tauri-plugin-oauth failed to resolve * fix: add tauri-plugin to workspace and patch to resolve duplicate symbol error - Add packages/tauri/crates/tauri-plugin to workspace members - Add tauri-plugin patch to [patch.crates-io] - This ensures all tauri dependencies use local versions - Fixes duplicate symbol __TAURI_BUNDLE_TYPE linking error * chore: restore Cargo.lock from upstream - Restore the original Cargo.lock from readest/readest main branch - This ensures reproducible builds and matches upstream - The lock file will be updated by cargo when dependencies change * fix: resolve TypeScript errors in test files - Fix ReplacementOptions.test.tsx: add optional chaining for possibly undefined values - Fix ReplacementRulesWindow.test.tsx: use proper type assertions for store setState calls - Use (store.setState as unknown as (state: unknown) => void) pattern for partial state updates * fix: allow selections with boundary punctuation and fix pattern matching for punctuation - Updated isWholeWord() to explicitly allow selections that start or end with punctuation (e.g., 'tis, off;, look,) - Fixed normalizePattern() to handle patterns with leading/trailing punctuation correctly - Word boundaries are now only added around the word part, not the punctuation - Fixes issue where replacements like 'scholar;' were not matching correctly * fix: prevent race condition when deleting replacement rules rapidly - Add isReloading state to track ongoing delete/edit operations - Prevent multiple rapid deletions that cause runtime errors during page reload - Show warning toast when user tries to delete while reload is in progress - Add finally blocks to ensure isReloading is always reset - This prevents the 'book doesn't finish rerendering' error * fix: escape HTML entities in replacement text to preserve angle brackets - Added escapeHtmlEntities() function to escape HTML special characters - Apply HTML escaping to replacement text in both multi and single-instance replacements - Fixes issue where replacement text like '<<AND>>' was being interpreted as HTML tags - Angle brackets and other HTML entities are now properly escaped and displayed correctly * fix: revert Tauri backend changes and resolve package.json conflict - Revert Cargo.toml and src-tauri/Cargo.toml to match upstream/main - Resolve @tauri-apps/cli version conflict (2.9.5 -> 2.9.6) - These changes are not related to the replacement feature implementation * fix: update pnpm-lock.yaml to match @tauri-apps/cli 2.9.6 * removed useless tests and backend tests from ReplacementOptions integration testing suite * chore: revert foliate-js submodule to match readest/readest main * fix: refactored display dialog logic to match other dialogs * fix: enabled global rule deletion in book menu * fix: removed ReplacementPanel from library settings * fix: deleted SettingsDialog.replacement.test.tsx since we no longer need to display replacements in library settings * fix: removed text replacement tab from settings dialog * fix: applied prettier code formatter to replacement rules window * fix: refactored wordLimit logic into a separate util file * fix: removed additional pr description * style: format code with prettier * chore: fix formatting and remove unused file listed by chrox * chore: format all changed files from pr 2693 and revert pnpm-lock * fix: fixed inconsistencies from rebase * refactor: removed unused code * refactor: removed unintentional formatting changes * fix: set upstream for packages/tauri-plugins to the readest branch * fix: used original Cargo.lock file * fix: got Cargo.lock from upstream * fix: fetched SettingsDialog from upstream main * fix: pointed tauri-plugins to the same commit as upstream * chore: remove unnecssary comments from replacement.ts * chore: fixed more unnecessary comments --------- Co-authored-by: fatbiscuit247 <fatbiscuit247@github.com> Co-authored-by: joon <your.email@example.com> Co-authored-by: jarchenn <jerryc2@andrew.cmu.edu> Co-authored-by: joon0429 <68578999+joon0429@users.noreply.github.com> Co-authored-by: Jerry Chen <50bmg@Jerrys-MacBook-Pro-9.local> Co-authored-by: Alicia Chen <aliciach@andrew.cmu.edu> Co-authored-by: Jerry Chen <50bmg@MacBook-Pro-7.local> Co-authored-by: Jerry Chen <50bmg@macbook-pro-158.wifi.local.cmu.edu> Co-authored-by: fatbiscuit247 <136537548+fatbiscuit247@users.noreply.github.com> |