7185dca1a26661947c46e5f7b12d98eaceb0bf8e
196 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
e327d0c992 |
feat(tts): reuse the speaking session across paragraph & RSVP modes (#4657)
Switching into Paragraph or RSVP mode while TTS is already playing now syncs to the live session instead of forcing a stop + restart inside the mode. Bundles several related TTS fixes uncovered along the way. Session reuse (enter from normal mode): - TTSController.redispatchPosition() re-emits the current position on the canonical tts-position signal with a fresh sequence. - useTTSControl answers a new tts-sync-request by replaying the current position then playback state (position-first so RSVP's paused handler can't discard it). - Paragraph & RSVP engage following on entry and dispatch the request; no-op when no session exists. RSVP refinements: - Reusing a session skips the start dialog and the get-ready countdown (starts externally driven); gated on a live tts-playback-state signal so the countdown can't flash. - Stopping TTS now pauses RSVP instead of resuming its own pacing. Word-sync fixes: - rangeTextExcludingInert honours the range offsets inside a single text node, fixing word-highlight drift on middle sentences of single-<span> paragraphs (Edge word highlighting). - foliate-js TTS.from() starts at the sentence containing the selection, not the next one (submodule bump). - Selecting a word and starting TTS now clears the selection. - Dev-only [TTS] word-sync trace (stripped from production builds). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
ff96c6d3f7 |
feat(annotations): unify highlights and annotations into one record (#3870, #4511) (#4647)
A highlight and its note are now a single BookNote. Adding a note attaches it to the highlight at that CFI (or creates one with the current global style) instead of creating a second record, and a unified record renders as both a highlight overlay and a note bubble. - onDrawAnnotation chooses the draw kind from the overlay value prefix (cfi -> highlight, NOTE_PREFIX -> bubble) instead of annotation.note, so a record with both a style and a note draws both. Fixes notes synced from KOReader losing their highlight (#4511). - handleSaveNote updates the existing annotation at the CFI rather than pushing a new record (#3870); re-styling preserves the note. - unifyAnnotations migration (book config schema v1 -> v2, run in deserializeConfig) collapses existing split highlight+note records into one survivor and tombstones the redundant record (deletedAt) so the merge syncs to the cloud and KOReader. - Sidebar: a note's quoted highlight text uses the theme foreground so it stays legible on the highlight background. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
38a6d3d9ba |
fix(dict): stop iOS instant system dictionary popping multiple times (#4644)
On iOS a single long-press emits several selectionchange events, so the instant quick action fired the system dictionary 2-3 times, stacking UIReferenceLibraryViewController sheets. Add a once-per-gesture latch to deferredAction (re-armed by beginGesture on touchstart/pointerdown) so the action runs at most once per gesture, mirroring the Android defer-to-touchend coalescing. Also fix two related cases: - Tapping outside to deselect after dismissing the dictionary occasionally re-opened it (~1/3): the deselect tap re-armed the latch and a racy lingering selectionchange re-fired. Gate the instant action on a long-press hold (isLongPressHold, 300ms, touch only) so a quick tap can't trigger it. - A Word Lens gloss tap ignored the system-dictionary setting (always opened the in-app popup); route it through handleDictionary so it honors the system dictionary like the toolbar and instant-quick-action paths. Verified on Android (Xiaomi) that the instant dictionary still fires once per long-press and re-arms for the next gesture; iOS double-popup and tap-to-deselect re-fire confirmed fixed. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c2ac207945 |
refactor(wordlens): rename "Word Wise" to "Word Lens" (#4633)
"Word Wise" is a Kindle trademark, so rename the inline-gloss feature to
"Word Lens" throughout the product.
- User-facing strings → "Word Lens" across all 34 locales; brand translated
for Chinese (zh-CN 单词透镜, zh-TW 單詞透鏡) and German (Word-Lens-Daten).
- Code identifiers: WordWise→WordLens, wordWise→wordLens, WORD_WISE→WORD_LENS.
- Files/dirs: src/services/wordwise→wordlens, WordWisePanel→WordLensPanel,
wordwise{Ruby,Section}.ts, build/sync scripts, test dirs/fixtures,
data/wordwise→data/wordlens.
- Storage paths: CDN base, R2 key, on-device cache dir, WORDLENS_R2_BUCKET env,
pnpm wordlens:{manifest,sync}. manifest.json is path-agnostic so its
sha256/bytes stay valid (verified).
- biome.json: point the formatter-ignore at data/wordlens so the generated
one-line gloss packs aren't pretty-printed on commit.
Migration notes:
- Re-run `pnpm wordlens:sync` to upload packs to cdn.readest.com/wordlens/.
- Persisted view-settings keys renamed (wordWiseEnabled/Level/HintLang and
wordWiseAutoDownload) — saved values reset to defaults once on upgrade.
- Cached packs under the old Data/wordwise/ orphan (harmless re-download).
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
4908245042 |
feat(reader): Word Wise inline vocabulary hints (#4589)
* feat(reader): Word Wise — inline native-language vocabulary hints Kindle-style Word Wise: a short native-language gloss renders above difficult words as you read (always-on ruby), gated by a CEFR vocabulary-level slider (A1–C2); tapping a glossed word opens the existing dictionary. - Pipeline: CEFR→frequency-rank difficulty, inflection-aware gloss index, pure offset-aware planner (EN regex + jieba for CJK). - Rendering: <ruby cfi-skip>…<rt cfi-inert> injected per occurrence — CFI-transparent (verified), so highlights/bookmarks/progress are unaffected; kept out of TTS word offsets and find-in-book. - Delivery: gloss packs are version-controlled in data/wordwise/, mirrored to R2, and downloaded on demand into local storage (sha-verified, single-flight) when enabled. - Settings: a Word Wise sub-page under Settings → Language (enable, level, hint language, per-pack download/manage, auto-download toggle). - Build tooling: scripts/build-wordwise-data.mjs (ECDICT / CC-CEDICT+HSK / WikDict + FrequencyWords, with lemmatization) and scripts/sync-wordwise-r2.mjs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * data(wordwise): bundled gloss packs + manifest + attribution 13 frequency-trimmed gloss packs (en↔中文 + es/fr/de/pt/it/ru↔en, ~19 MB) generated by build-wordwise-data.mjs from ECDICT (MIT), CC-CEDICT + HSK, and WikDict + FrequencyWords (CC-BY-SA). Source of truth, mirrored to the CDN via `pnpm wordwise:sync`. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
aab721b219 |
feat(dictionary): lemmatize inflected words before lookup (#4574) (#4582)
Dictionaries that store only base headwords (e.g. Oxford Dictionary of English) miss inflected selections like `ran`, `mice`, `children`, or `analyses` even though the lemma (`run`, `mouse`, `child`, `analysis`) is present. Add a language-aware lemmatizer whose base-form candidates are appended to the existing lookup candidate chain, after the exact/case variants, so an exact/case match always wins and the lemma is only tried once those miss. - New pluggable `lemmatize/` registry keyed by primary language subtag; add a language by registering one lemmatizer, no caller changes. - English lemmatizer: irregular-form table (suppletive verbs, irregular plurals/comparatives) + regular suffix rules (plural/past/gerund/ comparative/possessive). Over-generates on purpose — the dictionary lookup is the validator, so bogus stems simply miss. - Unknown/missing book language defaults to English (no-op on non-ASCII); an explicit non-English language with no registered lemmatizer is a no-op. - Applies centrally to all definition providers (mdict/stardict/dict/slob and the online builtins) via `buildLookupCandidates`. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
7f57af8f90 |
perf(cfi): bucket booknotes per chapter and batch-collapse location matcher (#4561)
* perf(cfi): bucket booknotes per chapter and batch-collapse location matcher
When iterating a list of CFIs against the same currentLocation (Annotator
on every page turn, useSearchNav, useBooknotesNav), the standalone
isCfiInLocation collapses the location twice per CFI. With 1000+
booknotes -- which a heavy user reported -- that's 2000 CFI parses
per page turn. The foliate epubcfi.js chunk showed up as ~15% of
self time in Bottom-Up profiles of the release Android build.
Fix:
- createCfiLocationMatcher(location) collapses once and returns a
matches(cfi) predicate that reuses the cached bounds. O(N) calls
become 1 collapse + N compares.
- getCfiSpinePrefix(cfi) extracts the spine path via pure string ops
(no CFI.parse round-trip) for use as a chapter bucket key.
- Annotator builds annotationIndex = { bySection, globals } via
useMemo([config.booknotes]) once when booknotes change, not per
page turn. The progress-driven effect then only scans the current
chapter's bucket -- ~50 CFIs in a typical book instead of all 1000.
globals are pre-filtered too.
- useSearchNav / useBooknotesNav switch to the batched matcher for
the same reason.
Includes parity tests covering empty/malformed inputs, equality
shortcut, prefix shortcut, in-range, and out-of-range cases.
* fix(annotator): keep note-only annotations in the per-chapter bucket
The booknote bucketing gated entries on `item.style`, which dropped
note-only annotations (a `note` with no highlight style/color, created
via the Notebook flow) from the per-relocate re-apply path. Their note
bubble was no longer redrawn on relocate or when booknotes changed while
a section stayed rendered.
Restore the original two-list semantics: bucket on style OR note, then
classify per location (annotations need a style, notes need a note).
Extract the logic into a dedicated, unit-tested `annotationIndex` module
(buildAnnotationIndex + selectLocationAnnotations) instead of inlining it
in Annotator, matching the reader/utils domain-named convention.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
1ce79d9abf |
perf(reader): reduce open-book TBT by batching layout-thrashing reads/writes and deferring annotation page back-fill (#4554)
* perf(reader): batch keepTextAlignment reads/writes to avoid layout thrashing
keepTextAlignment iterates every <div>, <p>, <blockquote>, <dd> in a
freshly-loaded section and tags each with an aligned-{center,left,
right,justify} class based on its computed text-align. The previous
implementation read getComputedStyle and wrote classList.add inside
the SAME forEach pass, which is the textbook layout-thrashing
anti-pattern: classList.add invalidates the document's style cache
(class-based selectors can affect descendants), so the next
getComputedStyle call forces the browser to recompute style for the
whole document.
For a long chapter (~hundreds of p/div/blockquote/dd elements — a
typical Harry Potter section), that turned the loop into N x layout
recalcs. On a release Android build it surfaced as:
- Browser console violation: 'Forced reflow while executing
JavaScript took 1210ms'
- The dominant chunk of the open-book Bottom-Up profile's
Layout = 32.8% / Recalculate Style = 17.5% of TBT (2503ms total)
- The 'load' handler also tripped a 1249ms violation, dominated by
keepTextAlignment running inside it
Fix: split into a read pass (O(N) getComputedStyle into an array) +
a write pass (O(N) classList.add). The browser computes style once
for the document at the start of the read pass and reuses that
result for every subsequent getComputedStyle call; the write pass
then batches all class mutations together so style invalidation
happens at most once at the end.
* perf(reader): back-fill annotation pages off the open-book hot window
Each call to view.getCFIProgress(cfi) synchronously decompresses the matching section's XHTML from the EPUB zip and walks its text nodes (foliate-js progress.js #getCache), costing 100-300ms per cold section on a release Android build. For users with annotations spread across many chapters that's seconds of zip-IPC + main-thread work that was happening inside the open-book TBT window.
First attempt scheduled the back-fill via requestIdleCallback. On Android Tauri the WebView fires rIC aggressively while the main thread is still doing layout/style work for the freshly-opened book — the Bottom-Up profile after that change still showed 1.5s+ of sendIpcMessage -> readData -> loadDocument -> getCFIProgress chains nested under "Fire Idle Callback" inside the same hot window.
New strategy:
- Hard gate on the renderer's first 'stabilized' event so the back-fill can't possibly start before the open-book paint settles.
- Add a 5s grace timer after stabilized so the user's first page-turns and paginator's adjacent-section preload can finish without contention.
- Process annotations one at a time with a 250ms setTimeout gap between each, instead of chained idle callbacks. Each getCFIProgress shows up as its own short task with input-handling slots in between.
- 10s safety-net fallback if 'stabilized' never arrives, plus full cleanup on unmount.
- Batch the saveConfig write at the end (one IPC instead of N).
- Skip entirely when there are no annotations missing a page.
The page field still only feeds the secondary 'p NN ·' label in the sidebar BooknoteItem, so the on-screen highlight rendering paths (progress-driven addAnnotation in the [progress] effect, plus onCreateOverlay on section load) are completely independent and unaffected by this change.
|
||
|
|
cfe2bb9116 | fix(reader): Android text selection breaks on the first word of hyphenated paragraphs (#4545) | ||
|
|
d165e8df2c |
fix(reader): turn automatically when highlighting across pages (#4487)
* fix(reader): turn automatically when highlighting across pages (closes #1354) * refact: Refactor time retrieval to use Date.now() * fix(reader): rework auto page-turn as a corner-dwell gesture Rework the initial #1354 implementation into a deliberate corner-dwell gesture that works across platforms, and fix popup positioning for cross-page selections. Trigger: - While a text selection is active, hold any engagement signal — the pointer (web/desktop/iOS), the Android native touchmove, or the selection caret — inside a screen corner for 500ms to turn one page: bottom-right goes to the next page, top-left to the previous. - One turn per engagement: a signal must leave the corner and return to turn another page, so the user controls it one page at a time. - The corner is a quarter-ellipse of radius 15% of each axis, measured against the reading frame (the <foliate-view> rect) inset by the page content margins, so the zone lands on the text — not the margin/footer or a sidebar — and the pointer can actually reach it. Per-platform signals: - web/desktop/iOS: the iframe pointermove, mapped to window coordinates via the iframe element's on-screen rect. - Android: the selection caret (the only signal during a native handle drag, where the handles live in a separate window so their touches never reach the Activity) plus a throttled (~10/s) native touchmove added in MainActivity.dispatchTouchEvent for content drags. Android scroll-pin (#873): an active selection pins the container scroll, which reverted the turn; suspend the pin during the turn and re-anchor it to the page we land on. Popup positioning: getPosition decided which selection end was on-screen using window bounds, so a cross-page selection's off-screen start (which maps behind the sidebar but inside the window) read "in view" and pinned the popup off the visible page. Test visibility against the reading frame instead, and for a multi-page selection anchor to the last on-screen line. Also: logical view.prev()/next() (RTL-correct); skip in scrolled mode; pass contentInsets down to the annotator. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Huang Xin <chrox.huang@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
726f53a64b |
fix(reader): use Tauri clipboard plugin for copy on Android (#4409)
navigator.clipboard.writeText is unreliable inside the Tauri Android WebView, so tapping Copy in the Reader's selection popup silently no-ops. Route the write through @tauri-apps/plugin-clipboard-manager on Tauri targets (Android, iOS, macOS, Windows, Linux), with a graceful navigator.clipboard / execCommand fallback for the web build and older WebViews. - Add tauri-plugin-clipboard-manager (Rust + JS) - Register the plugin in lib.rs - Grant clipboard-manager:allow-write-text / allow-read-text - New utils/clipboard.ts wrapper with platform-aware fallback chain - Annotator handleCopy and handleConfirmExport use the wrapper |
||
|
|
ef603852b7 |
feat(tts): hotkey to highlight the currently-spoken sentence (#4085) (#4368)
Add a "Highlight Current Sentence" keyboard action (default Shift+M, in the Text to Speech shortcut section) that persists the sentence TTS is reading aloud as a normal highlight using the user's default style/color — no text selection, eyes-off, silent, and idempotent (a repeat press on the same sentence is a no-op rather than a duplicate). Flow: the shortcut handler in useBookShortcuts dispatches tts-highlight-sentence → useTTSControl (which owns the TTSController) resolves the current sentence via the new TTSController.getSpokenSentence() and relays create-tts-highlight → Annotator builds the BookNote with the pure, unit-tested buildTTSSentenceHighlight helper and persists/renders it like any other highlight. Closes #4085 Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2f5e583653 |
feat(annotations): configurable export link type + dedicated Import Annotations modal (#4350)
* feat(export): make annotation export link type configurable Add an Annotation Link selector (App / Web) to the Export Annotations dialog. Defaults to the app deeplink in the native app and the universal web link on the web, so web exports no longer emit readest:// links that only the desktop/mobile app can open. The default markdown template now uses the configurable annotation.link variable. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(annotations): move Moon+ Reader import into a dedicated Import Annotations modal Replace the single 'Import from Moon+ Reader' menu item with an 'Import Annotations' entry (below 'Export Annotations') that opens a dedicated modal listing import sources. Currently lists Moon+ Reader; the boxed-list layout makes adding future providers a one-row change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
3c134380b7 |
feat: add empty state hints and loading indicators for annotations, bookmarks, notes, font import, and Moon+ Reader import (#4338)
* feat: add empty state hints and loading indicators for annotations, bookmarks, notes, font import, and Moon+ Reader import - BooknoteView: show 'No annotation yet' / 'No bookmark yet' when empty - Notebook: show 'No note yet' when no notes/excerpts exist - Annotator: add loading overlay with spinner during Moon+ Reader import - mrexpt: yield to event loop every 5 entries to keep spinner animating - CustomFonts: show in-place loading card during font import, spinner transitions to font name without layout jump * fix(ui): respect e-ink overlay styling and drop dead className branch - Annotator: use modal-box on the mrexpt import overlay so eink picks up the no-shadow + 1px border override automatically. - CustomFonts: collapse importing-card clsx ternary whose branches were identical into a flat className. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Huang Xin <chrox.huang@gmail.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
ae81cd0151 |
feat(annotator): support global highlights that fan out across all matching positions (#4257)
Introduces a 'global' annotation flag so a highlight/note created on one occurrence of a phrase is automatically applied to every matching occurrence in the book (and stays applied across reloads). Renders these expansions as transient overlays without creating duplicate persisted notes. This flag will not show when the book is fixed layout like PDF or CBZ. - types: add 'global?: boolean' to BookNote and DBBookNote; transform layer round-trips the field, with regression coverage ensuring older clients do not clobber it on write-back. - db: new migration 013_add_book_notes_global.sql adds nullable 'global' column to public.book_notes; init schema.sql updated to match. - annotator: new utils/globalAnnotations.ts handles cfi expansion / text-match search across the spine and overlay synthesis. Annotator.tsx fans out global notes on load and on overlay creation; AnnotationPopup and HighlightOptions expose a toggle to mark a highlight as global. - sync path is transparent: a global note created on another device is fanned out locally on next render with no extra UI required. |
||
|
|
05da6bdf43 |
feat(dictionary): add system dictionary provider for macOS, iOS, and Android (#4219)
Hand selected words off to the platform's native dictionary surface when the user opts into the new "System Dictionary" entry under Settings → Languages → Dictionaries. The setting is exclusive: enabling it disables all other providers (and vice versa) so the in-app lookup button either always opens the popup or always invokes the OS — no mixed states. Per platform: - macOS: AppKit's -[NSView showDefinitionForAttributedString:atPoint:] via a top-level Tauri command in src-tauri/src/macos/system_dictionary.rs. Anchored at the selection's bottom-center (CSS pixels mapped into NSView coords), so the inline Lookup HUD appears just below the highlighted text without raising Dictionary.app to the foreground. - iOS: UIReferenceLibraryViewController presented as a half-detent pageSheet on iPhone (medium → large drag-to-expand) and as a formSheet on iPad. Implemented in the native-bridge plugin. - Android: ACTION_PROCESS_TEXT intent with EXTRA_PROCESS_TEXT_READONLY, dispatched without createChooser so users get the standard system disambiguation dialog with "Just once / Always" buttons. Reports unavailable=true when no app handles the intent so the TS layer can silently skip rather than open an empty chooser. Web/Linux/Windows hide the row entirely. The provider is a sentinel — the registry filters it out of the popup tab list (it has no in-popup UI) and the annotator's handleDictionary checks isSystemDictionaryEnabled to dispatch directly to the native bridge before opening the in-app DictionaryPopup. |
||
|
|
3620c61038 |
feat(reader): import annotations from Moon+ Reader (.mrexpt) (#4174)
* feat(reader): import annotations from Moon+ Reader (.mrexpt)
Add a new menu entry under the reader sidebar 'More' menu that lets users import highlights and notes exported from the Moon+ Reader Android app.
Implementation:
- utils/mrexpt.ts: parser for the .mrexpt plaintext format (entry id, NCX navPoint index b4, character offset b6, type marker, word and note).
- services/annotation/providers/mrexpt.ts: convert mrexpt entries to BookNote[] using bookDoc. Locate the chapter via b4 -> toc -> spine, then TreeWalker-search the section DOM for the highlighted word with English suffix tolerance (ing/ed/s/...). Falls back to a section-level CFI when the exact word can't be located. Re-imports are deduplicated by a stable id derived from entryId.
- BookMenu: add 'Import from Moon+ Reader' menu item dispatching the 'import-mrexpt' event.
- Annotator: handle 'import-mrexpt' — pick the file (Web File / Tauri path), parse, convert against the live bookDoc, merge into booknotes (latest updatedAt wins), persist via saveConfig, and apply to all live views so highlights appear immediately. User feedback via toasts (importing / imported N / N unmatched / nothing new).
* refactor(reader): simplify Moon+ Reader import notifications
Reworks the .mrexpt import UX so it shows exactly one toast per run
instead of up to two, and removes redundant intermediate notices.
- Drop the intermediate "Importing N annotations…" toast. The toast
system shows one toast at a time, so it merely flashed and was
replaced by the result toast.
- Drop the duplicate "Failed to read the selected file." toast in the
read catch block; it falls through to the existing empty-content
check which surfaces the same message.
- Collapse the three-way result toast (already imported / N unmatched /
N imported) into one: "Imported {{count}} annotations" or
"No new annotations to import".
- Fix a result-message bug: when every converted note was already
imported and nothing was unmatched, the toast read "Imported 0
annotations." It now reports "No new annotations to import".
- Pluralize the success message via i18n `count` (the previous `{{n}}`
placeholder never pluralized, e.g. "Imported 1 annotations").
- Extract the dedupe/merge logic into a pure, unit-tested
`mergeImportedBookNotes` helper.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore(i18n): translate Moon+ Reader import strings
Run i18next extraction and translate the new .mrexpt import strings
across all 33 locales (340 keys). The import feature added in this PR
introduced translatable strings that had not yet been extracted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
8dfc0e945e |
fix(dictionary): normalize lookup query with trim + case fallback (#4192)
A double-click selection can carry trailing whitespace and most imported dictionaries store headwords lowercased, so an exact match on the raw selection often misses (e.g. `Hello` or `world ` fail to resolve `hello`/`world`). Case-sensitive formats like mdict are hit hardest since their reader compares the raw word. Seed the lookup history with a trimmed word and try ordered query variants (trimmed, lowercase, title-case, uppercase) per provider, keeping the first hit. Closes #4176. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
411d3ad687 |
fix: export annotations even without TOC, closes #4186 (#4188)
* i18n(ios): add more localized languages in plist * fix: export annotations even without TOC, closes #4186 |
||
|
|
1a3d393e74 |
feat(reader): add "Clear Annotations" entry to the book menu (#4175)
Adds a "Clear Annotations" item to the book menu. Picking it opens a confirm dialog and, on confirm, soft-deletes every type='annotation' booknote on the active book by stamping deletedAt, removes overlays from live views, persists via saveConfig, and resets sidebar browse state. Bookmarks and excerpts are untouched. The dialog lives in Annotator (per-book, long-lived) and is wired up via a new 'clear-annotations' event so it survives the dropdown menu unmounting. |
||
|
|
9a05935caf |
feat(reader): improve Japanese selection UX by disabling furigana selection (#4137)
* feat: add default ruby rt styles with user-select: none
* fix: prevent furigana text from being copied via ruby transformer
* fix: register ruby transformer in FoliateViewer pipeline; use span wrapper for reliable ::before rendering
* refactor(reader): simplify furigana copy exclusion
Drop the ruby transformer and the .rt-text::before pseudo-element
wrapping. Instead, pass ['rt'] to getTextFromRange unconditionally so
furigana is excluded from annotator/translation/copy text extraction,
and let `rt { user-select: none }` handle the native selection cursor.
Avoids DOM rewriting and HTML-entity round-tripping in the data-text
attribute, and keeps <rt> text in the DOM for TTS, in-page find, and
screen readers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
d326e1c73d |
fix: hide popup triangle when inside popup + EPUB image-only paragraph rendering (#4121)
- Popup: hide the inner triangle when its anchor point lands inside the popup body. Extracted as a generic `isPointInRect` helper in `sel.ts` (with a default 1px padding so edge cases stay visible). - style.ts: handle `<p[width][height]><img></p>` (common in some MOBI conversions) — clear hardcoded width/height and apply multiply blend for dark themes so the image doesn't sit on a colored box. - Annotator: shrink dict popup height from 480 to 360 to fit smaller screens. - foliate-js: submodule bump. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
772bb73b46 |
ui/ux: codify design system and migrate settings to shared primitives (#4116)
* ui/ux: codify design system and migrate settings to shared primitives Document Readest's design language in DESIGN.md (Adwaita-aligned, e-ink-first, RTL-correct) and migrate every settings panel onto a small set of primitives (BoxedList, SettingsRow, SettingsSwitchRow, SettingsSelect, SettingsInput, NavigationRow, Tips, SubPageHeader). AGENTS.md links to DESIGN.md so contributors land there before inventing new chassis classes. Replace the standalone KOReader/Readwise/Hardcover Config dialogs with a single Integrations panel (Reading Sync + Content Sources sub-pages). The reader's BookMenu now hides each provider until it's configured, and Hardcover's per-book "Enable for This Book" toggle is dropped — there's no auto-sync to gate, so the flag was just extra clicks. Refresh highlight colors (two-trigger swatch + label, translatable default names), background texture / theme color selectors (border-current keeps selection legible on any backdrop), CustomFonts/CustomDictionaries (quiet list-extension style + shared Tips primitive), the OPDS catalog manager (debounced auto-download, right-aligned Browse), Set PIN, and the KOSync conflict resolver. Translate the ~30 new strings across all 33 locales. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ui/ux: responsive typography, OPDS card polish, deep-link return paths Restore the .settings-content responsive cascade (14px desktop / 16px mobile) the legacy panels relied on by dropping hardcoded `text-sm`/`text-xs` from the new primitives. Secondary text moves to em-relative `text-[0.85em]` so it scales with the parent. Form controls (`<input>`, `<select>`) re-apply the cascade explicitly via the `settings-content` class since browsers don't inherit font-size onto form elements. Extract `<SectionTitle>` primitive (caseless-language aware via `isCaselessUILang`/`isCaselessLang`) and route every uppercase tag-style header through it: BoxedList groups, Reading Sync, Content Sources, Theme Color, Background Image, integration form labels, KOSyncResolver device labels, and the OPDS My Catalogs / Popular Catalogs sections. CJK / Arabic / Hebrew / Indic / Thai / Tibetan locales bump to `1em` since `uppercase` is a no-op on those scripts. Redesign the OPDS My Catalogs cards: whole card becomes the browse trigger (role='button'), edit/delete collapse into a 3-dot dropdown menu, and the sync-status moves to a sub-line under Auto-download so the card height stays constant whether the toggle is on/off or sync data has arrived. Plumb a `from=settings-integrations` URL marker through the OPDS browser so both manual close and auto-close-on-failure (preserved as `router.back()` for transient failures, paired with a new `stashOPDSReturnTarget` helper) return the user to Settings -> Integrations -> OPDS Catalogs sub-page rather than the dialog's top level. Backed by new `requestedSubPage` deep-link store field. Skip the OPDS catalog passphrase prompt when credentials sync is disabled -- `replicaPublish` already drops encrypted fields at the wire, so prompting was both pointless and confusing. Fix `SettingsDialog` calling `setRequestedPanel(null)` inside a `useState` lazy initializer (zustand setter during render -> React warning); move the clear into a one-shot `useEffect`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ui/ux: opt Settings into OverlayScrollbars + caseless typography polish Add an opt-in `useOverlayScroll` prop to `<Dialog>` that swaps the body's native `overflow-y-auto` for `<OverlayScrollbarsComponent>` (autohide, click-scroll, no native overlaid bars). SettingsDialog flips it on so the long Layout / Color panels keep a visible, theme-aware scroll track on Android / iOS webviews where native scrollbars auto-hide entirely. Other short-modal callers stay on the native scrollbar. Drop the `uppercase tracking-wider` SectionTitle styling for caseless scripts and pair it with body-weight `font-medium` instead — those typographic effects are no-ops on Han / Hangul / Devanagari / Thai etc., so a plain medium-weight body-size title reads more correctly than a shrunken pseudo-uppercase one. SettingsRow / NavigationRow primary labels follow the same rule (drop `font-medium` in caseless locales since the inherited body weight already carries; CJK fonts bold poorly at body size). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ui/ux: SettingLabel primitive + KOSyncForm select polish + Tips alignment Add `<SettingLabel>` primitive — caseless-aware row/field label that pairs with `<SectionTitle>` (groups) for per-item labels. Cased scripts get `font-medium`; caseless scripts (CJK / Arabic / Hebrew / Indic / Thai / Tibetan) drop the weight since Han / Hangul / Devanagari etc. bold poorly at body size. No font-size class so it inherits the `.settings-content` 14/16 cascade. Routed through `SettingsRow`, `NavigationRow`, and the ~12 ad-hoc inline `text-sm font-medium` callsites in AIPanel / FontPanel / ColorPanel / IntegrationsPanel / KOSync / Readwise / Hardcover forms. Refactor KOSyncForm's Sync Strategy + Checksum Method rows onto the shared `<SettingsSelect>` primitive — the inline 17-line div/select/ MdArrowDropDown chassis becomes a single SettingsSelect call with an options array. Drops the unused MdArrowDropDown import and ~25 lines. Fix Tips list-item alignment: callers traditionally pass `<li>` elements (semantic) but the primitive was double-wrapping into `<li><span><li>...</li></span></li>` — invalid HTML, and the inner `<li>`'s `display: list-item` broke line-wrap alignment on multi-line items. Unwrap caller `<li>` to its content; add `flex-1` on the text span so wrapped lines align under the first line instead of falling back to the bullet column. Bullet container switches to `h-[1.4em]` so it tracks the text line-height and pins to the first line's optical center via `items-center` regardless of how much the content wraps. DESIGN.md §5 typography updated to point primary-label callers at `<SettingLabel>`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
295a588988 |
feat(share): route annotation exports through the system share sheet (#4107)
Adds a `share` flag and `sharePosition` to `saveFile` across the app
services. On iOS/Android/macOS/Windows the annotation export now calls
the sharekit `shareFile` (writing the markdown/txt to `$TEMP` first when
no `filePath` is provided), so users get the system "Share via…" sheet
that drops the export into Mail, Notes, Messages, etc. Linux desktop
keeps the existing save dialog, since sharekit has no Linux backend.
On the web, `saveFile` now prefers `navigator.share({ files })` when the
browser advertises support via `canShare`. AbortError (user dismissed)
is treated as a deliberate "don't share" choice; any other rejection
(e.g., Chrome desktop's `NotAllowedError` despite a positive `canShare`)
falls through to the `<a download>` fallback so a save still happens.
Also fixes the macOS share popover anchoring: `preferredEdge: 'top'`
maps to `NSMaxYEdge`, which is the rect's bottom edge in WKWebView's
flipped coords, so the picker rendered below the trigger button. The
annotations export only got away with it because its dialog has no room
below — macOS auto-flipped above. Switching to `preferredEdge: 'bottom'`
(`NSMinYEdge` → top edge in flipped coords) anchors the popover above
the button consistently. Adds `$TEMP/**/*` to the Tauri fs capabilities
so the writable temp share file is permitted.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
||
|
|
30dee7b909 |
feat(dict): improve MDict rendering and dictionary management (#4072)
* fix(reader): play sound:// links in MDict definitions via MDD lookup MDX entries reference audio resources with `<a href="sound://name.ext">`. Until now those anchors fell through to the browser, which tried to navigate to an invalid scheme and did nothing useful. Wire each `sound://` anchor inside the rendered MDX body to: - preventDefault + stopPropagation (so the parent card's tap-to-expand doesn't fire), - look up the path in every companion `.mdd` until one returns bytes (js-mdict's `MDD.locateBytes` auto-normalizes the leading separator), - wrap the bytes in a Blob and play via `new Audio(URL.createObjectURL)`, - cache the resolved URL on the anchor so subsequent clicks reuse it, with the URL tracked for revocation in `dispose()`. Note: many MW-style dictionaries use `.spx` (Speex) which Chromium and Safari don't natively decode — the lookup will succeed but playback may fail silently. Other formats (mp3, wav, ogg vorbis) play fine. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(dict): improve MDict rendering and dictionary management Builds on the sound:// fix to round out MDict rendering and tighten the dictionary settings panel. MDict provider: - Follow MDict-specific URL schemes inside the rendered HTML: `sound://path` plays via Audio (with a deprecation toast for `.spx` whose codec no major browser decodes), and `entry://word` / `bword://word` forward to ctx.onNavigate so the popup re-looks-up the target. Cycle-bounded (5 hops) `@@@LINK=<word>` content-level redirects are followed transparently, so entries that are pure redirect strings (e.g. "questions" → "question") render the canonical entry instead of the literal redirect text. - Render the body inside a shadow root so each dict's CSS stays scoped — `<link rel="stylesheet">` references are resolved against the companion .mdd, loose .css files imported alongside the bundle are read at init, and `url(...)` refs inside both are rewritten to blob URLs sourced from the MDD (covers sound icons, background images, @font-face sources). The body is tagged `data-dict-kind="mdict"` for downstream targeting. - A baseline app-level stylesheet (`getDictStyles`) is injected into every shadow root with theme-adaptive `mix-blend-mode` for `<a>` background icons / `<a> img` (multiply on light, screen on dark); isDarkMode is forwarded via the lookup context. - `<img src="/path">` is now treated as MDD-relative (the tightened IMG_SRC_PROTOCOL_RX skips schemes / protocol-relative only); a fallback retry strips the leading slash for bundles that store the resource without it. - The auto-prepended light-DOM headword `<h1>` is hidden when the dict body either leads with a same-text element (any tag — covers `<h3 class="entry_name">`, etc.) or contains an `<h1>` with the same trimmed text anywhere (covers wrapper-div-then-h1 layouts). Dictionary management: - Importing a dict whose name matches an existing one now replaces it in place, preserving the slot in providerOrder and inheriting the previous enabled flag. The .css extension is added to the file picker, and loose .css files imported alongside .mdx/.mdd are bundled with the dictionary regardless of stem-match. - The settings panel gains an Edit mode (parity with Delete mode): trailing pencil button on imported dicts and custom web searches opens a rename modal. Edit and Delete are mutually exclusive. Below 400px, the Edit/Delete labels collapse to icons only. Card UX: - The card's tap-to-expand handler now walks `composedPath()` so clicks on anchors / buttons / images inside the shadow root no longer fold the card. i18n: - Translations added for new strings across 33 locales. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
a272ba892a |
feat(reader): replace dictionary tabs with stacked result cards (#4071)
Redesign the dictionary lookup UI as a single scrolling list of expandable cards — one per provider that has a result — instead of a tab strip with one active provider at a time. Behavior: - All enabled dictionaries are queried in parallel; cards render in user-defined order. Cards whose provider returns no result, an unsupported format, or an error are removed entirely. - Cards default to expanded when 3 or fewer providers have results, collapsed (4-line preview) otherwise. Manual taps are sticky across re-renders; the auto-decision is reset only when a new word is looked up. - Web-search providers (Google, Urban, Merriam-Webster, custom templates) appear in a separate "Search the web" section as tappable rows. On the web build they use native target="_blank" anchors; on Tauri the click is routed through openUrl since target="_blank" doesn't open externally there. - The header carries a back arrow (when in-content link navigation has pushed onto the history stack), the looked-up word, and a gear that deep-links to Settings → Language → Dictionaries. Mobile / narrow viewports (<sm) get the same UX as a bottom sheet (Dialog with snapHeight 0.75); sm+ viewports keep the anchored popup with triangle pointer. Both share useDictionaryResults + DictionaryResultsHeader/Body. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
c27245e980 |
feat(reader): support deeplink and web link in annotation export (#4067)
Expose `annotation.appLink` (readest://) and `annotation.webLink` (https://web.readest.com) as template variables for custom export templates. The shipped default template now emits the readest:// app deeplink for the page link so exported notes open the native app. The non-template export mode keeps the universal https link. Preview links also gain target="_blank" so they open in a new tab instead of replacing the dialog. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
d66fedcab7 | feat(reader): manage rules shortcut in proofread popup (#4062) | ||
|
|
7bb1133706 |
feat(dictionaries): add DICT/Slob formats and Web Search providers (#4048)
Extends the dictionary system beyond StarDict/MDict with two more open
formats and a pluggable Web Search tier so users can fall back to online
sources when their offline bundles miss a word.
Formats:
- DICT (dictd, RFC 2229): .index + .dict.dz bundles. Shared DictZip
parsing with StarDict via new dictZip.ts helper.
- Slob (Aard 2): self-contained .slob containers, zlib-compressed
utf-8 entries; non-zlib/non-utf-8 bundles flagged unsupported at
import.
Web Search:
- Built-in templates for Google, Urban Dictionary, Merriam-Webster
(seeded into providerOrder, disabled by default).
- Custom URL templates via %WORD% placeholder, URL-encoded at
substitution; entries persist in settings.webSearches.
- V1 renders an "Open in {{name}}" external link (iframe embedding is
blocked by every major target site's X-Frame-Options).
UI:
- CustomDictionaries panel: flat outline-primary buttons for Import /
Add Web Search, end-aligned type badges for a uniform column,
hover states, compact tips block.
- Dictionary popup: bottom-right Manage icon (tooltip-only) deep-links
into Settings → Language → Dictionaries; rounded-corner clipping fix
on the tab strip.
File picker accepts .index and .slob; importer recognizes DICT and
Slob bundles and reads bundle metadata for friendly names.
Tests cover DICT/Slob readers and providers with real freedict-eng-nld
fixtures, web search substitution + provider rendering, and the new
store CRUD for web searches.
Closes #4038
|
||
|
|
486659a1ca |
feat(annotations): deep links for highlight exports (#4018)
* feat(annotations): deep links for highlight exports Embed an HTTPS deep link in markdown export so clicking a highlight in Obsidian / Notion / Mail launches Readest at the exact CFI position. Mobile App Links / Universal Links open the native app silently when installed; desktop attempts the readest:// scheme automatically with a manual fallback. - Markdown export wraps the page-number text in a per-annotation link: https://web.readest.com/o/book/{hash}/annotation/{id}?cfi=... - New /o/... smart landing page handles platform routing (intent:// on Android Chrome, scheme + visibility-cancel on other Android, auto scheme + 1 s fallback on desktop, manual button on iOS). - Reader honors a ?cfi= query param on initial load (overrides the saved last-position for the primary book only). - New useOpenAnnotationLink hook handles incoming readest:// and https://web.readest.com/o/... URLs, including cold-start (getCurrent) and library-load deferral; supports the legacy flat shape readest://annotation/{hash}/{id} from previous Readwise syncs. - ReadwiseClient now emits the HTTPS deep link instead of the legacy custom scheme. - AASA extended with /o/* matcher; Android intent-filter for the host has no pathPrefix so it already covers it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * i18n: translate annotation deep-link strings across all locales Translates the 13 new keys introduced for the annotation deep-link feature into all 31 supported locales. Replaces all 403 __STRING_NOT_TRANSLATED__ placeholders. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
94ede10f6e |
fix(annotator): defer Android quick action until touchend, closes #3935 (#3979)
On Android, long-press selects text via selectionchange while the finger is still on the screen. The quick action handler was gated by androidTouchEndRef and silently returned, so no popup ever opened. After the user lifted, nothing re-ran the gated action. Track the gated action in a small DeferredActionState ref and flush it from the native touchend handler, so instant copy/dictionary/wikipedia/ search/translate/tts now fire on the first long-press release. |
||
|
|
3e292af990 | refactor(nav): refactor book nav service with TOC enrichment (#3874) | ||
|
|
41b5e92563 | feat(annotator): support instant copy operation for selected text, closes #3828 (#3854) | ||
|
|
017a9338b3 |
fix(dictionary): add Chinese dictionary lookup with pinyin support (#3784)
The Wiktionary REST API does not support Chinese entries — simplified characters return 404, traditional characters omit Chinese results, and no pronunciation data is ever included. Switch Chinese lookups to the Wiktionary Action API which returns full wikitext with pinyin and definitions. Also add encodeURIComponent to the REST API URL for all other languages. Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
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> |
||
|
|
298d4872a0 |
fix(translate): disable yandex provider while upstream relay is down (#3765)
The translate.toil.cc relay that backs the yandex provider is currently unavailable. Introduce a `disabled` flag on TranslationProvider and filter disabled providers out of both `getTranslators()` and `getTranslator()` so the settings panel, translator popup, and the fallback logic in `useTranslator` all agree the provider doesn't exist — no per-callsite changes needed. Flip the flag back (or delete the line) on `yandexProvider` to re-enable once the upstream is healthy. Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
d682fcbb44 |
feat: add named highlight colors with sync and picker ux fixes (#3741)
* fix: add highlight color label fields
* fix: add default highlight label sync fields
* fix: add highlight prefs sync helpers
* fix: add highlight color name inputs
* fix: persist and sync highlight color names
* fix: add highlight color label helpers
* fix: add long press highlight label preview
* fix: pull highlight color prefs during library sync
* test: cover highlight color label helpers
* fix: widen highlight color name inputs
* fix: show highlight names on hover and touch hold
* fix: prevent highlight name input overlap
* fix: improve highlight name input responsiveness
* fix: support drag scrolling for highlight colors
* fix: batch custom color and label updates
* fix: serialize highlight prefs saves
* fix: align color strip drag and restore color clicks
* fix: translate default highlight color labels
* refactor: remove highlight preference sync wiring
* fix: align highlight option i18n with existing pattern
* fix: remove redundant english highlight keys
* fix: support raw and normalized highlight label keys
* fix: use underscore translator in highlight options
* fix: translate custom highlight color labels in editor
* refactor: simplify highlight settings persistence
* fix: maintianer review
* refactor: simplify highlight prefs save and clean up editor
- Drop the skipUserColors/skipLabels options from handleHighlightPrefsChange
and always persist both arrays; the flags only masked a no-op caller.
- Type handleHighlightColorsChange with Record<HighlightColor, string>
instead of typeof so the signature reads clearly.
- Remove the always-true `|| true` guard around the custom colors section.
- Stop wrapping user-typed custom color labels with _(), matching the
built-in color inputs and keeping the input value equal to what the
user typed.
* refactor: couple highlight labels to their colors
Replace the parallel `highlightColorLabels: Record<string, string>` map
with label storage that lives next to each color. This removes the hex
key normalization layer and its whole class of orphan/case-drift bugs.
Data model:
userHighlightColors: string[] -> UserHighlightColor[]
({ hex, label? })
highlightColorLabels: Record<string, string> -> (removed)
defaultHighlightLabels:
Partial<Record<DefaultHighlightColor, string>>
A `migrateHighlightColorPrefs` helper runs during `loadSettings` and
handles both the shipped `string[]` layout and the draft-build
`highlightColorLabels` layout: hex-keyed labels attach to matching user
colors, name-keyed labels move into `defaultHighlightLabels`. Malformed
entries are dropped.
Editor:
- Label inputs commit on blur (Enter also commits), so typing a long
label no longer fires `setSettings`/`saveSettings` on every
keystroke. A small `LabelInput` component owns the draft state and
syncs if the prop changes externally.
- Three explicit callbacks (`onCustomHighlightColorsChange`,
`onUserHighlightColorsChange`, `onDefaultHighlightLabelsChange`)
replace the previous single callback with opaque skip flags.
Picker:
- Extracted a reusable `useDragScroll` hook (mouse only, 6px
threshold, 120ms click suppression) from the inline state machine
in `HighlightOptions`. The picker drops to ~280 lines.
- Long-press preview stays touch/pen only. It now reads labels via
`getHighlightColorLabel` (which returns `undefined` when no user
label is set), letting the component layer decide whether to fall
back to a translated default name.
i18n:
- Default color names ('red' | 'yellow' | 'green' | 'blue' | 'violet')
are registered once at module scope via `stubTranslation` and
translated at the picker layer through `useTranslation`. User-typed
labels are never run through `_()`, so what the user types is what
the editor shows.
Tests:
- `annotator-util.test.ts`: rewritten around the new helper contract
(user label -> undefined fallback) and case-insensitive hex matching.
- `settings-highlight-migration.test.ts`: new, covers legacy
`string[]`, already-migrated entries, malformed hex filtering, and
the two draft-label fold paths.
---------
Co-authored-by: Huang Xin <chrox.huang@gmail.com>
|
||
|
|
80cab8e56d |
feat: Hardcover.app Sync (#3724)
* feat(hardcover): add one-way Hardcover sync integration - Add HardcoverClient with ISBN/title-based book lookup, progress push, and note sync - Add HardcoverSyncMapStore for persistent local mapping of note IDs to Hardcover journal IDs - Add GraphQL queries/mutations for Hardcover API (insert/update/recreate journal entries) - Add DB migration for hardcover_note_mappings table (schema: hardcover-sync) - Add HardcoverSettings dialog component (connect/disconnect/enable toggle) - Add useHardcoverSync hook wired in Annotator for push-notes and push-progress events - Add Hardcover Sync section in BookMenu with per-book toggle (persisted in BookConfig) - Add HardcoverSettings type and DEFAULT_HARDCOVER_SETTINGS to system settings - Add hardcoverSyncEnabled per-book flag to BookConfig - Mount HardcoverSettingsWindow in Reader alongside KOSync and Readwise windows Sync is one-way (Readest → Hardcover), manual-action only, per-book opt-in. Supports idempotent note sync: insert new, skip unchanged, update changed, recreate on stale remote ID. * fix(hardcover): proxy API calls server-side to fix CORS, normalize Bearer token - Add /api/hardcover/graphql Next.js route that proxies POST requests server-side, bypassing CORS restrictions (Hardcover API has no Access-Control-Allow-Origin header) - On Tauri (desktop), calls Hardcover directly; on web, routes through the proxy - Normalize token in HardcoverClient: accept raw JWT or 'Bearer <jwt>' format - Update helper text to point to hardcover.app → Settings → API * fix(hardcover): surface note-sync no-ops and harden book resolution - Show toast feedback when book data is still loading, Hardcover is not configured, or the current book has no annotations/excerpts to sync - Show an explicit info toast when note sync finds no new changes - Parse Hardcover search results in the current hits/document response shape - Resolve note sync through ensureBookInLibrary for parity with progress sync - Add console logging for note/progress sync failures * debug(hardcover): add runtime instrumentation for note sync * feat(metadata): keep identifier stable and store ISBN separately - Add dedicated metadata.isbn field - Expose ISBN as its own editable field in book metadata - Preserve identifier semantics for existing source IDs and hashes - Route metadata auto-retrieval ISBN handling through the new field - Prefer metadata.isbn for Hardcover matching * fix(hardcover): avoid wasm sqlite for note mappings on web - Store Hardcover note sync mappings in localStorage on web - Keep sqlite-backed mappings for desktop/native environments - Remove the web-only database dependency from manual note sync * fix(hardcover): dedupe notes by payload hash across unstable note IDs - Add payload-hash lookup in HardcoverSyncMapStore - Reuse existing journal mapping when payload already synced - Prevent duplicate insertions when note IDs change or duplicate locally * fix(hardcover): avoid duplicate quote export when annotation note exists - Detect excerpt+annotation pairs for the same highlight - Skip standalone excerpt export when annotation has note text - Keep annotation export as the single source of truth * docs(hardcover): add consolidated change summary for review * fix(hardcover): suppress excerpt export by CFI when annotation note exists * fix(hardcover): suppress empty-note annotation duplicates when note exists * fix(hardcover): deduplicate notes by text and cfi base node * refactor(hardcover): optimize sync performance, add rate limiting and clean up debug tools * chore: remove dev-only change log from main * test(hardcover): add unit tests for sync mapping and client logic * chore: custom deployment and UI fixes * fix(hardcover): use timestamptz for accurate annotation time * fix(hardcover): use date scalar and RFC 3339 formatting for journal entries * Revert "chore: custom deployment and UI fixes" This reverts commit 0329aba7129d1e1ebf2c663804b8fba9a9f87b91. * Fix hardcover progress dates and surface imported ISBNs * Fix Hardcover currently reading sync * fix(hardcover): avoid promoting note sync status * style(hardcover): apply prettier formatting * test(hardcover): fix strict TypeScript assertions * test(hardcover): harden sync regression coverage * test(hardcover): fix lint and formatting regressions * fix(hardcover): narrow note dedupe range matching * refactor(hardcover): extract note dedupe helpers * refactor(isbn): extract metadata normalization helpers * feat(hardcover): improve synced quote formatting |
||
|
|
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> |
||
|
|
45e5f0fa61 | fix(eink): disable range editor loupe for annotations on Eink devices, closes #3655 (#3656) | ||
|
|
c7e82825f5 |
fix(koplugin): avoid resurrecting deleted annotations on pull (#3639)
Closes #3621. |
||
|
|
2905506017 | fix(layout): fixed total scrollable width in vertical scrolled mode, closes #3583 (#3586) | ||
|
|
1936136596 |
fix: resolve various tracked exceptions in ph (#3584)
* fix: handle synced bookmarks without cfi * chore: clean up some exceptions in ph |
||
|
|
76b239f382 |
feat(koplugin): add support for annotation sync (#3579)
Add bidirectional annotation/highlight sync between KOReader and Readest: - Add xpointer0/xpointer1 fields to BookNote and DBBookNote types for KOReader XPointer positions alongside Readest's CFI format - Extend transform layer to pass through xpointer fields to/from DB - Convert CFI→XPointer on push and XPointer→CFI on pull in useNotesSync, discarding notes that fail conversion - Support KOReader's text()[K].N indexed text node format in xcfi.ts for paragraphs with inline elements (e.g. <a> page anchors) - Generate KOReader-compatible XPointers: text().N for single text nodes, text()[K].N only when multiple direct text nodes exist - Skip cfi-inert elements (injected by Readest at runtime) in XPointer path building and resolution - Map highlight colors between KOReader and Readest color systems - Implement KOReader plugin annotation push/pull with deterministic IDs, auto-sync on document open/close, and UIManager refresh on pull - Refactor koplugin into focused modules: syncauth, syncconfig, syncannotations, selfupdate Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> |
||
|
|
91bc4ddec7 | feat(library): backup to and restore from a zip file (#3571) | ||
|
|
1f4f5f8a6d | feat(annotator): responsive layout for the loupe (#3568) | ||
|
|
eab83c6d3f | feat(annotator): show the loupe when selecting text in instant annotation, closes #3539 (#3565) |