forked from akai/readest
bfb85c2f68e45fbc3e3d825d1bb7fe8e7f77167d
3 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> |
||
|
|
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> |
||
|
|
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> |