Skip to content

feat: Decoration styles#142

Draft
ddfreiling wants to merge 61 commits into
Notalib:mainfrom
ddfreiling:feat/decoration-styles
Draft

feat: Decoration styles#142
ddfreiling wants to merge 61 commits into
Notalib:mainfrom
ddfreiling:feat/decoration-styles

Conversation

@ddfreiling
Copy link
Copy Markdown
Member

New experimental decoration styles.

Extracted from #136 as they are still draft quality.

ddfreiling and others added 30 commits May 23, 2026 16:58
Bumps the three ts-toolkit packages to latest:
  @readium/navigator             2.2.4 → 2.5.5
  @readium/navigator-html-injectables  2.2.1 → 2.4.2
  @readium/shared                2.1.1 → 2.2.0

Adapter changes required by the new API surface:
- EpubNavigator/WebPubNavigator listeners: add stub handlers for
  contentProtection, peripheral, and contextMenu (now required fields).
- EpubPreferences: add scrollPaddingLeft / scrollPaddingRight fields
  introduced in 2.5.x.
- helpers.ts: note that highlightSelection() is experimental and will
  be superseded once ts-toolkit PR #209 (Decorator API) merges.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Brings the web platform to parity with iOS and Android for timebased
playback features.  All three share the same Dart API and event streams
(onTimebasedPlayerStateChanged, onTextLocatorChanged) already wired up
for audiobooks on native.

AudioNavigator (audiobooks)
- New Audio/audioNavigator.ts wraps @readium/navigator AudioNavigator.
- Introduces AudioLocatorMapper — an optional hook applied by every
  state-emitting listener (play, pause, positionChanged, trackEnded,
  error, stalled) so all state transitions carry correctly-mapped
  locators, not just periodic poll events.
- openPublication now initialises AudioNavigator for audiobook profiles
  (was a TODO stub).

TTS (Audio/ttsNavigator.ts + Audio/ttsPreferences.ts)
- WebTTSEngine walks EPUB text via PublicationContentIterator +
  HTMLResourceContentIterator and speaks each TextElement via the
  browser SpeechSynthesis API.
- Sub-utterance onboundary granularity (word/sentence) with 100 ms
  throttle; degrades silently when unavailable (Firefox, some mobile).
- Voice list: { identifier, name, language, networkRequired } —
  gender/quality enriched by ReaderTTSVoiceUtils in the platform
  interface via readium/speech voice data.
- Per-language voice map and global voice override.
- TODO(#209): visual word/sentence highlight deferred until ts-toolkit
  Decorator API (PR #209) merges.

Media Overlay / Sync Narration
  (Audio/syncNarration.ts + Audio/mediaOverlayNavigator.ts)
- Parses Readium Sync Narration JSON alternates
  (application/vnd.readium.narration+json) into SyncNarrationItem[].
- Builds a synthetic audiobook reading order (one Link per unique audio
  file) and reuses AudioNavigator via AudioLocatorMapper to emit
  text-based locators on every state event — matching iOS/Android.
- audioSeekBy wired through AudioNavigator.jump(seconds).

Dart wiring
- js_publication_channel.dart: JS interop + static wrappers for the
  full timebased playback API (play, pause, resume, stop, next,
  previous, seekBy, setAudioPreferences, ttsEnable,
  ttsGetAvailableVoices, ttsSetVoice, ttsSetPreferences, audioEnable).
- flutter_readium_web.dart: replaces UnimplementedError stubs for all
  playback methods with real JsPublicationChannel calls.
- readium_webview.dart: registers updateTimebasedPlayerState JS export
  so AudioNavigator/TTS state events reach the Dart stream.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Webpack 5's default splitChunks optimisation, combined with the
pre-split @readium/* packages (locale and ReadiumCSS variant files),
was generating 30+ chunk files alongside the main bundle.  These had
to be committed and served as separate assets.

Fix: add splitChunks: false and output.asyncChunks: false to
webpack.config.js.  asyncChunks: false (Webpack 5.84+) inlines all
dynamic imports — including the ReadiumCSS injection files that
@readium/navigator loads lazily — into the single bundle.  The
trade-off is a slightly larger readiumReader.js (~1.2 MiB dev build)
with no practical penalty since the webview loads everything up front.

Deleted all stale chunk files from lib/helpers/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Link.fromJsonArray: guard against non-Map elements (e.g. JSON-LD
  @context strings embedded in link arrays) instead of hard-casting,
  preventing a TypeError on manifests that follow the JSON-LD convention.
- MediaType: add readiumNarration constant for
  application/vnd.readium.narration+json (Readium Sync Narration format).
- Publication.containsMediaOverlays: extend to recognise both
  vnd.syncnarr+json and vnd.readium.narration+json alternates, so
  isAudioBook returns true for Sync Narration EPUBs and the example app's
  play-button dispatch routes them to audioEnable correctly.
- FlutterReadiumWebPlugin: override setLogLevel (was throwing
  UnimplementedError; the web layer needs only ReadiumLog.setLevel).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PlayerControlsBloc: add SeekRelative event dispatching audioSeekBy so
  ±10 s relative seeks can be triggered from the UI.
- PlayerControlsWidget: show replay-10 / forward-10 buttons when audio
  (audiobook or Media Overlay) is active; buttons carry ValueKeys for
  marionette automation.
- webManifestList.json: replace placeholder URLs with a Nota EPUB and
  audio-only webpub for smoke-testing TTS and Media Overlay on web.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add a 'build_web' task that runs bin/update_web_example (builds the TS
bundle and copies readiumReader.js into example/web/) and an
'example (web)' launch configuration that targets Chrome, running the
build task as a pre-launch step.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
browser scrolling in publication emits updates with 60hz
timing between calls during init was all wrong before, now uses a ready promise for when first track is loaded. Should avoid observed race-conditions
ddfreiling and others added 30 commits May 24, 2026 03:19
…spotlight, ruler)

Extends `DecorationStyle` with `spotlight` and `ruler` modes and wires
them up on iOS, Android, and Web:

- highlight: existing filled rectangle behind text (unchanged)
- underline: existing border-bottom (unchanged)
- spotlight: tinted box + large box-shadow dims surrounding viewport,
  drawing the reader's eye to the active range
- ruler: full-viewport-width tinted stripe per text line
  (Layout.boxes / Layout.BOXES + Width.viewport / Width.VIEWPORT)

Web: all four styles are routed to separate upstream FrameComms subgroups
(`<group>`, `<group>__underline`, `<group>__spotlight`, `<group>__ruler`).
Spotlight CSS (body dim + color restore via ::highlight()) is activated
automatically from decoration presence — no separate API call needed.
Removes the now-unnecessary `setSpotlightGroup` Dart/TS/JS API entirely.

iOS: custom HTMLDecorationTemplates registered for Decoration.Style.Id
"spotlight" and "ruler" alongside the upstream defaults.

Android: SpotlightStyle / RulerStyle data classes (Decoration.Style.Tinted
+ @parcelize), registered in HtmlDecorationTemplates at navigator creation.
decorationStyleFromMap() updated to parse the new style strings.

Also adds docs/highlight-modes-plan.md covering the design rationale and
the Phase D TTS wiring pointer for the web-TTS branch.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* main:
  chore(iOS): add further diagnostics to the HTTP client used by swift-toolkit
  fix: possible crash when mapping from PlatformException
  fix(iOS): skip the FormatSniffer when fetching known formats
  chore(iOS): also remove the viewPort data from PDF reader
  chore(ios): improve HTTPError handling
  chore(ios): fix warnings
  fix: Fix PR comments
  fix: Fix small mistake
  fix(iOS): Add contiuous fast forwarding
  fix: Add continuous seeking setting in API
  fix(iOS): Add continuous seeking setting
…er_readium into feat/web-feature-parity

* 'feat/web-feature-parity' of github.com:ddfreiling/flutter_readium:
  chore(example): adjust font weight handling for web platform
…or non-narration anchors

- `audioEnable` falls back to the visual EPUB navigator's current locator
  when Dart passes no `fromLocator`, mirroring Android's
  `initialLocator ?: epubNavigator?.currentLocator?.value`. Toggling audio
  mid-book now starts at the user's reading position instead of the start
  of the publication.
- `textLocatorToAudioLocator` falls back to the first Sync Narration item
  in the matching resource when a fragment / cssSelector doesn't match any
  item's textId. ToC entries pointing at headings without sync data (e.g.
  `chap1.xhtml#title`) now seek the audio to chapter start instead of
  leaving playback where it was.
- Example app's Play button uses `PlayerControlsBloc.currentLocator` (latest
  from both visual and timebased streams) instead of the stale opening
  locator from `PublicationBloc`.
- Promotes the goTo MediaOverlay decision logs from debug to info so the
  seek path is visible without flipping log levels.

Follow-up: Notalib#139 (mirror the textId fallback on
iOS/Android).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Promotes one-shot lifecycle events to `log.info` so the audio init and
seek path is visible in the browser console without flipping log levels.

- `mediaOverlayNavigator.initializeMediaOverlayNavigator`: logs the
  initial-locator href, parsed-item count + distinct audio files,
  synthetic reading order summary (count + first/last href), and the
  result of mapping the initial text locator → audio locator.
- `audioNavigator.initializeAudioNavigator`: logs track count, initial
  position (or that there is none), and whether a Media Overlay locator
  mapper is attached.
- `audioNavigator` listeners: `play` and `pause` events now log the href
  and fragment so seek+resume flows are traceable.

High-frequency events (`positionChanged`, `timeupdate`, the per-call
"Available SyncNarrationItems" dump) remain at `debug` to keep INFO
scannable during playback.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The theme field referenced in api-reference and guides/preferences.md was
never implemented in the Dart model or any native platform. Replace the
section with direct backgroundColor/textColor examples. The feature will
not be added — upstream toolkits handle themes via color pairs directly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…(iOS)

All three properties exist in swift-toolkit's PDFNavigatorViewController.Preferences
but were not surfaced through the Dart API. Android PdfiumPreferences does not expose
these fields; they are documented as iOS-only and silently ignored on Android/web.

- Add PDFSpread enum (auto/never/always) to platform interface
- Extend PDFPreferences with offsetFirstPage, spread, visibleScrollbar fields
- Wire all three in FlutterPDFPreferences.swift via Spread(rawValue:)
- Update docs/api-reference/preferences.md table with platform annotations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Maps to Decoration.Style.HighlightConfig.isActive on iOS and
Decoration.Style.Highlight/Underline.isActive on Android, allowing
consumers to render one decoration in a visually distinct "active" state
(e.g. current search result in a result browser).

Also fixes two pre-existing iOS parsing bugs discovered while wiring the
new field:
- Decoration.init(fromMap:) now reads the locator as a nested JSON object
  (the Dart model has always serialised it this way) rather than expecting
  a JSON string, and reads the style/tint from the nested style sub-object.
- setDecorationStyle now casts the method-channel map to [String: Any]
  instead of [String: String], which is what the Flutter runtime delivers.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Previously onErrorEvent returned const Stream.empty() on web, meaning
subscribers would receive no events. Now it is backed by a broadcast
StreamController<ReadiumError> that mirrors the iOS EventChannel pattern.

Wire-up:
- js_publication_channel.dart: adds @js external set onErrorCallback
- readium_webview.dart: registers onErrorHandler JSExport in registerJSExports
- flutter_readium_web.dart: adds _errorEventController + addErrorEvent;
  pure audiobook path registers the callback via _AudiobookCallbacks
- ReadiumReader.ts: openPublication catch block calls window.onErrorCallback
  with a JSON error payload; flutter.d.ts declares the window property type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove the `spotlight` and `ruler` DecorationStyle modes and the
`ReaderDecorationStyle.isActive` flag from all platforms, keeping only the
baseline `highlight` and `underline` styles. This keeps the web-feature-parity
branch focused on web parity; the extracted feature lands in its own stacked PR
(feat/decoration-styles).

- Shared Dart: drop spotlight/ruler enum members + isActive field/serialisation.
- iOS: remove spotlight/ruler HTMLDecorationTemplates and isActive threading.
  Keep the nested-locator + [String: Any] parsing fixes (needed by highlight/
  underline on iOS).
- Android: delete FlutterDecorationStyles.kt (Spotlight/Ruler styles), drop their
  template registrations and the isActive arguments.
- Web (TS): remove spotlight/ruler routing, the spotlight body-dim CSS, and the
  ruler-fill MutationObserver from helpers.ts / ReadiumReader.ts. Keep the
  highlight/underline decoration plumbing. Rebuild the JS bundle.
- Example: drop the Spot/Ruler style selector segments.
- Docs/changelog: remove docs/highlight-modes-plan.md and the spotlight/ruler/
  isActive changelog entries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add the `spotlight` and `ruler` DecorationStyle modes and the
`ReaderDecorationStyle.isActive` flag across iOS, Android, and Web, on top of
the baseline highlight/underline support. Stacked on feat/web-feature-parity.

- spotlight: tinted box + viewport-dimming box-shadow (iOS/Android) / body-dim
  CSS (Web), focusing the reader on the active range.
- ruler: full-viewport-width tinted stripe per text line (a reading-ruler aid).
- isActive: renders a decoration in a visually distinct "active" state; maps to
  upstream HighlightConfig.isActive (iOS) and Highlight/Underline.isActive
  (Android). Includes the iOS nested-locator / [String: Any] parsing handling.
- Web: routes spotlight/ruler to dedicated FrameComms subgroups; ruler-fill
  MutationObserver + spotlight body-dim CSS. Rebuilt JS bundle.
- Example: Spot/Ruler style selector segments.
- Docs: docs/highlight-modes-plan.md design rationale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants