Skip to content

Feat: web platform feature parity#136

Draft
ddfreiling wants to merge 78 commits into
Notalib:mainfrom
ddfreiling:feat/web-feature-parity
Draft

Feat: web platform feature parity#136
ddfreiling wants to merge 78 commits into
Notalib:mainfrom
ddfreiling:feat/web-feature-parity

Conversation

@ddfreiling
Copy link
Copy Markdown
Member

@ddfreiling ddfreiling commented May 23, 2026

Attempting to bring web platform up-to-speed.
Initial skeleton impl. done with help by AI, but tested and corrected from there.

It needs testing and further work:

  • Use new AudioNavigator by readium.
  • Implement TTS Navigator using WebSpeech API and mapping via the readium/speech repo.
  • Implement MediaOverlay Navigator similarly to how we did on native platforms.
  • Fix TTS, it does not work in practice (traced to bug in @readium/shared, made upstream PR)
  • Implement goToProgression in all modals.
  • Add Guided Navigation to support to above navigator
  • Validate that Comic book (Notalib specific) media-overlay books render correctly
  • Decoration API
  • Synchronized of audio/text
  • Bug: totalProgression is null
  • WebPublicationPreferences. Missing many settings, should we try to open with epubNavigator instead?
  • Validate that tocHref enricher and ToC skip/goTo works
  • Validate that disableSynchronization works for TTS and Overlays publications

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 9 commits June 1, 2026 18:14
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>
ddfreiling and others added 10 commits June 2, 2026 15:53
exploded and minimal version of the one used in native example app
_emitState passed the mapped stateLocator (text href, no time fragment) into computeTotalProgression, which keys on the audio reading order — so it always returned undefined and totalProgression was silently null on the emitted media-overlay currentLocator. Pass the raw audio locator instead, matching the intent already documented in combinedLocatorForItem.

Also adds a test-only `__testing__` export (makeAudioTotalProgressionFn) consumed by the accompanying test(web) commit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
upstream AudioNavigator stalled position polling and got into bad states - we need to do some upstream PRs and then revise these fixes
Comment thread flutter_readium/lib/src/flutter_readium_web.dart
Comment thread flutter_readium/web/_scripts/WebPub/webPubPrefences.ts Outdated
* origin:
  ci: consolidate CI workflows & badges
  chore: simplify pana_check
  chore: comment out dependency_overrides now that platform_interface is published
  chore: improve pana score by fixing bracket comments referencing outdated or out-of-scope types
  chore: make pana_check support FVM and optional verbose flag
  chore: update CHANGELOGs before publish
  chore: explicit that currentReaderWidget and defaultPreferences should not be set directly by clients
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