feat(web): web platform TS structural refactor#145
Draft
ddfreiling wants to merge 106 commits into
Draft
Conversation
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
upstream PR has been opened
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
Conventional source directory name; disambiguates from the separate assets/_helper_scripts/ webview-helper bundle. Update path references in package.json (4 script paths), webpack.config.js comment, .github/instructions/typescript.instructions.md, CLAUDE.md, CONTRIBUTING.md, docs/architecture.md, docs/parity/*, and bin/typecheck. No code changes — build, typecheck, and 151 Jest tests all pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
… utils/ and model/ Move: - logger.ts → utils/ReadiumPluginLogger.ts - peripherals.ts → utils/Peripherals.ts - extensions/ReadiumPublication.ts → utils/ReadiumExtensions.ts - enums.ts → model/ReadiumReaderStatus.ts Re-export shims at old paths keep all existing imports unchanged. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Extract from helpers.ts into canonical locations: - utils/manifest.ts: fetchManifest - utils/colors.ts: dartColorToCss - utils/iframeInjection.ts: injectFlutterReadiumHelperScripts + asset cache - decorations/decorationOverrides.ts: UNDERLINE_GROUP_SUFFIX, sendDecorate, navIframeWindows, registerPendingDecorationGroup, injectDecorationOverrides, highlightSelection Add mediaTypes + findLinkByHref to utils/ReadiumExtensions.ts. helpers.ts is now a re-export barrel (keeping preferences helpers in-place until Phase A3). All existing import paths continue to work. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Move: - Epub/epubPreferences.ts → preferences/FlutterEpubPreferences.ts (+ convertVerticalScroll/textAlignFromJson/normalizeTypes inlined from helpers) - WebPub/webPubPrefences.ts → preferences/FlutterWebPubPreferences.ts (typo fixed) - TTS/ttsPreferences.ts → preferences/FlutterTTSPreferences.ts Update FlutterWebPubPreferences imports to come from FlutterEpubPreferences. helpers.ts barrel re-exports convertVerticalScroll/textAlignFromJson/normalizeTypes from their new home in FlutterEpubPreferences. Shims at all old paths keep existing imports unchanged. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…module Extract enrichWithTotalProgression, enrichWithTocHref, flattenToc into navigators/locatorEnrich.ts (shared with WebPub in step 5). Create navigators/FlutterEpubNavigator.ts: class wrapping EpubNavigator with static create() factory (logic moved verbatim from free function). Epub/epubNavigator.ts becomes a shim re-exporting FlutterEpubNavigator, enrichWithTotalProgression, and a backwards-compatible initializeEpubNavigatorAndPeripherals wrapper. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create navigators/FlutterWebPubNavigator.ts: class wrapping WebPubNavigator with static create() factory. Imports locatorEnrich from the shared module (removes the cross-Epub/epubNavigator import). WebPub/webpubNavigator.ts becomes a shim re-exporting the class and a backwards-compatible initializeWebPubNavigatorAndPeripherals wrapper. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create navigators/FlutterAudioNavigator.ts: class wrapping AudioNavigator with static create() factory. All logic moved verbatim from audioNavigator.ts; module-level _emissionsEnabled + setAudioEmissionsEnabled preserved. Exports buildStatePayload, seekAudioAndResume, SeekableAudioNavigator, AudioLocatorMapper, and __testing__ (makeAudioTotalProgressionFn, withTocHref). Audio/audioNavigator.ts becomes a shim re-exporting everything from the canonical location plus a backwards-compatible initializeAudioNavigator wrapper. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create navigators/FlutterTTSNavigator.ts: same class as WebTTSEngine but renamed FlutterTTSNavigator, with imports updated to canonical paths (utils/ReadiumExtensions, utils/ReadiumPluginLogger, preferences/FlutterTTSPreferences). TTS/ttsNavigator.ts becomes a shim re-exporting FlutterTTSNavigator under both the new name and the backwards-compatible WebTTSEngine alias. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create navigators/FlutterMediaOverlayNavigator.ts with logic from Audio/mediaOverlayNavigator.ts. Imports updated to canonical paths. Calls FlutterAudioNavigator.create() directly instead of the initializeAudioNavigator free function. Audio/mediaOverlayNavigator.ts becomes a shim re-exporting all public symbols and __testing__ from the canonical location. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
git mv flutter.d.ts → bridge/window.d.ts; update import to use model/ReadiumReaderStatus canonical path. Create bridge/ReadiumBridge.ts: the single module allowed to call window.* emit callbacks. Typed methods: emitReaderStatus, emitTextLocator, emitTimebasedState, emitTextSelected, emitError. Navigators will be wired to use the bridge in Phase D. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create publication/PublicationManager.ts: encapsulates the static _publications cache and manifest-fetch glue from the god class. Methods: fetchAndCache (used by getPublication), getOrFetch (used by openPublication cache-or-fetch pattern), evict/evictAll. Phase D will wire _ReadiumReader to delegate to this collaborator. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create decorations/DecorationController.ts: extracts applyDecorations, setDecorationStyle, _subgroupFor, _decorationsByGroup, and style state from _ReadiumReader into a focused collaborator class. Phase D will wire the god class to delegate to this instance. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create preferences/FlutterAudioPreferences.ts: audioPreferencesFromJson (extracted from FlutterAudioNavigator) and applyAudioPreferences (extracted from god class setAudioPreferences). Normalizes Dart preference keys to IAudioPreferences before submitting. Update FlutterAudioNavigator.ts to import audioPreferencesFromJson from the new module; remove the now-redundant private preferencesFromString function. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Wire _ReadiumReader to use all Phase C collaborators: - ReadiumBridge: all window.* calls replaced with bridge.emit* methods - PublicationManager: static _publications + fetch logic delegated - DecorationController: applyDecorations/setDecorationStyle delegated - applyAudioPreferences from FlutterAudioPreferences Update imports to canonical paths (navigators/, preferences/, utils/, model/, bridge/, publication/, decorations/). FlutterTTSNavigator replaces WebTTSEngine. FlutterAudioNavigator.create()/FlutterEpubNavigator.create()/ FlutterWebPubNavigator.create() used directly instead of free-function shims. Public method names/signatures unchanged (Dart↔JS contract intact). Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Create web/src/index.ts as the new webpack entry point (re-exports from ReadiumReader.ts). Update webpack.config.js entry from ReadiumReader.ts to index.ts. Output path lib/helpers/readiumReader.js is unchanged. Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Remove 13 re-export shims at legacy paths:
logger.ts, peripherals.ts, enums.ts, helpers.ts,
extensions/ReadiumPublication.ts,
Epub/{epubNavigator,epubPreferences}.ts,
WebPub/{webpubNavigator,webPubPrefences}.ts,
TTS/{ttsNavigator,ttsPreferences}.ts,
Audio/{audioNavigator,mediaOverlayNavigator}.ts
Update test imports to canonical paths:
__tests__/audioNavigator.test.ts → navigators/FlutterAudioNavigator
__tests__/epubNavigator.test.ts → navigators/locatorEnrich
__tests__/helpers.test.ts → utils/colors
__tests__/mediaOverlayNavigator.test.ts → navigators/FlutterMediaOverlayNavigator
__tests__/guidedNavigation.test.ts, mediaOverlayNavigator.test.ts → utils/ReadiumExtensions
Update Audio/guidedNavigation.ts + Audio/syncNarration.ts to import from
canonical utils/ paths (logger → ReadiumPluginLogger, extensions → ReadiumExtensions).
Fix implicit-any on Link callback parameter in guidedNavigation.ts.
Typecheck clean; 151 Jest tests pass.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
audioNavigator.test.ts → FlutterAudioNavigator.test.ts epubNavigator.test.ts → locatorEnrich.test.ts helpers.test.ts → colors.test.ts (helpers.ts deleted; only tests dartColorToCss) mediaOverlayNavigator.test.ts → FlutterMediaOverlayNavigator.test.ts closePublication.test.ts → ReadiumReader.test.ts (imports ReadiumReader.__testing__) guidedNavigation.test.ts and syncNarration.test.ts kept — their source modules (Audio/guidedNavigation.ts, Audio/syncNarration.ts) are unchanged. Also remove stale untracked shim files left on disk after Phase D15 git rm (enums.ts, logger.ts, peripherals.ts, Epub/, TTS/, WebPub/, extensions/). 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Audio/syncNarration.ts → mediaoverlay/syncNarration.ts Audio/guidedNavigation.ts → mediaoverlay/guidedNavigation.ts These are media-overlay helpers, not generic audio utilities; grouping them with FlutterMediaOverlayNavigator (navigators/) better reflects ownership. Update all import paths in: ReadiumReader.ts navigators/FlutterMediaOverlayNavigator.ts __tests__/syncNarration.test.ts __tests__/FlutterMediaOverlayNavigator.test.ts __tests__/guidedNavigation.test.ts Typecheck clean; 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- ReadiumReader.ts: merge two split imports from @readium/shared into one - FlutterWebPubNavigator.ts: add missing load error log + remove no-op try/catch - FlutterMediaOverlayNavigator.ts: replace Manifest.deserialize()! + null check with the correct null-guard pattern (non-null assertion is redundant when the runtime check follows immediately) - DecorationController.ts: remove stale 'Phase D' forward-reference from JSDoc - PublicationManager.ts: remove 'god class' / 'Extracts from' impl-note language No logic changes. 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
EpubNavigator and WebPubNavigator had three identical blocks: - iframe up/down scroll (17 lines × 2) - handleLocator external-URL check (12 lines × 2) - textSelected payload builder (9 lines × 2) Extract to navigators/navigatorUtils.ts: scrollVisibleIframes(direction) — iframe content scroll handleExternalLocator(href) — confirm-open / warn buildTextSelectionPayload(locator, selection) — selection JSON Both navigator create() functions now import and call these helpers, removing ~38 lines of duplicated code from each. 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
_emitState takes 8 parameters; each listener repeated the same 5 captured context arguments (locatorMapper, alsoText, computeTotalProgression, onTextLocatorChanged, getTocHref) verbatim. Introduce a local emit(state, locator, alsoText) closure inside create() that closes over the 5 context args. Listeners now only supply the 3 values they actually vary per event, making each callback self-explanatory. No logic change. 151 Jest tests pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- FlutterEpubNavigator/FlutterWebPubNavigator: remove the 7-method delegation façade (goRight/Left/Forward/Backward, goLink, go, destroy, currentLocator). ReadiumReader never stores the returned instance; it receives the raw upstream navigator via the setNav callback, so all wrapper methods were dead code. - FlutterWebPubNavigator.create() return type changed Promise<Wrapper>→ Promise<void> to match actual usage (return value was always discarded). - Fixed copy-paste log message: 'EpubNavigator loaded' → 'WebPubNavigator loaded' in FlutterWebPubNavigator. - decorationOverrides.ts: remove highlightSelection() and its exclusive imports (BasicTextSelection, Width, Layout, Locator, LocatorText, ReadiumPublication). The function was exported but never called anywhere in the codebase. - syncNarration.ts: replace per-item debug log loop with a single summary line; the old loop emitted one log entry per cue which is hundreds of lines on a typical audiobook, drowning useful output. - ReadiumExtensions.ts: merge duplicate @readium/shared imports; rewrite mediaTypes() as a single chained expression with const instead of let. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
FlutterAudioNavigator.create() return value is always discarded by both ReadiumReader and FlutterMediaOverlayNavigator — the actual navigator is delivered via the setNav callback. Remove the vestigial class constructor and underlying field; change Promise<FlutterAudioNavigator> → Promise<void> to match actual usage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- Merge duplicate FlutterAudioNavigator import (class + functions were on separate import lines; now collapsed into one). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Pre-PR formatting pass via bin/format. All changes are line-wrapping only — no logic changes. These files had accumulated formatting drift since the last dart format run. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
subosito/flutter-action v2.23.0 treats flutter-version-file as a semver constraint and resolves it to the latest matching stable (3.41.9 → 3.44.1), ignoring the intended pin. Fix: read the file in a dedicated step and pass the exact string via flutter-version:. Remove channel: stable (inferred from the exact version). Document the pitfall in ci.instructions.md. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
analysis_options.base.yaml sets formatter.page_width: 120 via flutter_lints. bin/format was not running pub get first, so the lints package could not be resolved and dart format fell back to 80-char width. CI runs pub get before dart format and correctly uses 120-char width, causing 69+ files to appear changed in CI despite looking correct locally. Fix bin/format to run flutter pub get --directory before each dart format call so local and CI formatting always agree. Apply the correct 120-char formatting to flutter_readium_platform_interface (69 files), flutter_readium (27 files), and flutter_readium/example (20 files). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ℹ️ This PR is a test of CoPilot's Autopilot to drive a refactor plan devised by Claude Code Opus 4.8 from start to finished implementation and opened PR.
What this PR does
Follows #136 with a full TypeScript structural refactor that mirrors the OOP architecture of the native sides.
Part 2 — TypeScript structural refactor
The web implementation was a 991-line god class (
_ReadiumReader) in a flat directory. This PR restructures it to mirror the native iOS/Android OOP architecture, following the migration plan indocs/web-ts-refactor-plan.md.Directory layout (before → after)
Key changes:
helpers.tsgod-file dissolved into focused sub-modulesFlutter*Navigatorwrappers extracted from free init functions (matchFlutter*-prefix convention from native)ReadiumBridge.tsis the only module allowed to touchwindow.*callbacks — injected by dependency, not reached globallyPublicationManagerowns publication lifecycle stateDecorationControllerowns decoration style/group statewebPubPrefences.ts→FlutterWebPubPreferences.tsWebTTSEnginerenamed →FlutterTTSNavigatorhighlightSelection()export,underlyingfieldsInvariant preserved
The Dart↔JS contract is byte-identical:
globalThis.ReadiumReadermethod names/signatures andwindow.*callback names/JSON shapes are unchanged. No changes to Dartjs_interop.Verification
npm test)bin/typecheckclean (tsc --noEmit)bin/formatclean (Dart)bin/analyzeexits 0bin/update_web_example)