From b9bbc259ae033a5991f2c2613cd25be4d4cc2ea2 Mon Sep 17 00:00:00 2001 From: Daniel Freiling Date: Wed, 3 Jun 2026 18:23:26 +0200 Subject: [PATCH 01/29] =?UTF-8?q?refactor(web):=20rename=20web/=5Fscripts?= =?UTF-8?q?=20=E2=86=92=20web/src?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- .../instructions/typescript.instructions.md | 2 +- .vscode/settings.json | 9 +- CLAUDE.md | 2 +- CONTRIBUTING.md | 2 +- bin/typecheck | 4 +- docs/architecture.md | 2 +- docs/parity/README.md | 4 +- docs/parity/web-comic-support-plan.md | 8 +- docs/parity/web-decorations.md | 4 +- docs/web-ts-refactor-plan.md | 170 ++++++++++++++++++ flutter_readium/package.json | 8 +- .../{_scripts => src}/Audio/audioNavigator.ts | 0 .../Audio/guidedNavigation.ts | 0 .../Audio/mediaOverlayNavigator.ts | 0 .../{_scripts => src}/Audio/syncNarration.ts | 0 .../{_scripts => src}/Epub/epubNavigator.ts | 0 .../{_scripts => src}/Epub/epubPreferences.ts | 0 .../web/{_scripts => src}/ReadiumReader.ts | 0 .../web/{_scripts => src}/TTS/ttsNavigator.ts | 0 .../{_scripts => src}/TTS/ttsPreferences.ts | 0 .../WebPub/webPubPrefences.ts | 0 .../WebPub/webpubNavigator.ts | 0 .../__tests__/audioNavigator.test.ts | 0 .../__tests__/closePublication.test.ts | 0 .../__tests__/epubNavigator.test.ts | 0 .../__tests__/guidedNavigation.test.ts | 0 .../__tests__/helpers.test.ts | 0 .../__tests__/mediaOverlayNavigator.test.ts | 0 .../{_scripts => src}/__tests__/style.stub.js | 0 .../__tests__/syncNarration.test.ts | 0 .../web/{_scripts => src}/enums.ts | 0 .../extensions/ReadiumPublication.ts | 0 .../web/{_scripts => src}/flutter.d.ts | 0 .../web/{_scripts => src}/helpers.ts | 0 .../web/{_scripts => src}/jest.config.js | 0 .../web/{_scripts => src}/logger.ts | 0 .../web/{_scripts => src}/peripherals.ts | 0 .../web/{_scripts => src}/style.css | 0 .../web/{_scripts => src}/tsconfig.json | 0 .../web/{_scripts => src}/tsconfig.test.json | 0 .../web/{_scripts => src}/webpack.config.js | 2 +- 41 files changed, 194 insertions(+), 23 deletions(-) create mode 100644 docs/web-ts-refactor-plan.md rename flutter_readium/web/{_scripts => src}/Audio/audioNavigator.ts (100%) rename flutter_readium/web/{_scripts => src}/Audio/guidedNavigation.ts (100%) rename flutter_readium/web/{_scripts => src}/Audio/mediaOverlayNavigator.ts (100%) rename flutter_readium/web/{_scripts => src}/Audio/syncNarration.ts (100%) rename flutter_readium/web/{_scripts => src}/Epub/epubNavigator.ts (100%) rename flutter_readium/web/{_scripts => src}/Epub/epubPreferences.ts (100%) rename flutter_readium/web/{_scripts => src}/ReadiumReader.ts (100%) rename flutter_readium/web/{_scripts => src}/TTS/ttsNavigator.ts (100%) rename flutter_readium/web/{_scripts => src}/TTS/ttsPreferences.ts (100%) rename flutter_readium/web/{_scripts => src}/WebPub/webPubPrefences.ts (100%) rename flutter_readium/web/{_scripts => src}/WebPub/webpubNavigator.ts (100%) rename flutter_readium/web/{_scripts => src}/__tests__/audioNavigator.test.ts (100%) rename flutter_readium/web/{_scripts => src}/__tests__/closePublication.test.ts (100%) rename flutter_readium/web/{_scripts => src}/__tests__/epubNavigator.test.ts (100%) rename flutter_readium/web/{_scripts => src}/__tests__/guidedNavigation.test.ts (100%) rename flutter_readium/web/{_scripts => src}/__tests__/helpers.test.ts (100%) rename flutter_readium/web/{_scripts => src}/__tests__/mediaOverlayNavigator.test.ts (100%) rename flutter_readium/web/{_scripts => src}/__tests__/style.stub.js (100%) rename flutter_readium/web/{_scripts => src}/__tests__/syncNarration.test.ts (100%) rename flutter_readium/web/{_scripts => src}/enums.ts (100%) rename flutter_readium/web/{_scripts => src}/extensions/ReadiumPublication.ts (100%) rename flutter_readium/web/{_scripts => src}/flutter.d.ts (100%) rename flutter_readium/web/{_scripts => src}/helpers.ts (100%) rename flutter_readium/web/{_scripts => src}/jest.config.js (100%) rename flutter_readium/web/{_scripts => src}/logger.ts (100%) rename flutter_readium/web/{_scripts => src}/peripherals.ts (100%) rename flutter_readium/web/{_scripts => src}/style.css (100%) rename flutter_readium/web/{_scripts => src}/tsconfig.json (100%) rename flutter_readium/web/{_scripts => src}/tsconfig.test.json (100%) rename flutter_readium/web/{_scripts => src}/webpack.config.js (98%) diff --git a/.github/instructions/typescript.instructions.md b/.github/instructions/typescript.instructions.md index 9e63bd11..6e8cb75a 100644 --- a/.github/instructions/typescript.instructions.md +++ b/.github/instructions/typescript.instructions.md @@ -16,7 +16,7 @@ applyTo: 'flutter_readium/web/**/*.ts' ## Built JS -Do **not** hand-edit the compiled JS in `example/web/`. Edit TS sources in `flutter_readium/web/_scripts/`, then run `bin/update_web_example` from the repo root to rebuild and copy the bundle. +Do **not** hand-edit the compiled JS in `example/web/`. Edit TS sources in `flutter_readium/web/src/`, then run `bin/update_web_example` from the repo root to rebuild and copy the bundle. ## Linting diff --git a/.vscode/settings.json b/.vscode/settings.json index 287f90b5..63ddaac0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,10 +27,10 @@ "[dart]": { "editor.rulers": [ 120 - ], + ] }, "cSpell.enabledFileTypes": { - "dart": true, + "dart": true }, "cSpell.ignorePaths": [ "**/.pub-cache/**", @@ -94,5 +94,6 @@ "videoref" ], "java.configuration.updateBuildConfiguration": "interactive", - "dart.mcpServer": true -} + "dart.mcpServer": true, + "dart.flutterSdkPath": ".fvm/versions/3.41.9" +} \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index 68e51746..40dd1e53 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -37,7 +37,7 @@ Key scripts: - `bin/install` — bootstrap everything: `pub get` in both packages, `pod update && pod install` for the example, build helper scripts, build web JS, copy JS into example. Run after a fresh clone or when dependencies change. - `bin/format` — check Dart formatting across all three packages (platform interface, plugin, example). Fails if any file needs reformatting. - `bin/analyze` — run `dart analyze --fatal-infos --fatal-warnings` across all three packages. -- `bin/typecheck` — type-check the web TypeScript (sources + Jest tests) via `tsc --noEmit` against `web/_scripts/tsconfig.json`. Run after editing any TS in `flutter_readium/web/`. Exits non-zero on a type error. +- `bin/typecheck` — type-check the web TypeScript (sources + Jest tests) via `tsc --noEmit` against `web/src/tsconfig.json`. Run after editing any TS in `flutter_readium/web/`. Exits non-zero on a type error. - `bin/build_js` — build the web bundle (currently `build_dev`; production build is commented out). - `bin/update_web_example` — `build_js` + copy the bundle into `flutter_readium/example/web/`. Run after editing TS in `flutter_readium/web/`. - `bin/update_readium_voice_data` — refresh `flutter_readium/assets/voice_data/voices.json` from the upstream `readium/speech` repo (requires `jq`). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0b63d60d..42d8847e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,7 +96,7 @@ The same suite runs in CI on every push/PR via [.github/workflows/integration-te ## Building the web bundle -After editing TypeScript files in `flutter_readium/web/_scripts/` or `flutter_readium/assets/_helper_scripts/src/`: +After editing TypeScript files in `flutter_readium/web/src/` or `flutter_readium/assets/_helper_scripts/src/`: ```bash bin/update_web_example # build + copy into example/web/ diff --git a/bin/typecheck b/bin/typecheck index 370722c2..2b7af89a 100755 --- a/bin/typecheck +++ b/bin/typecheck @@ -2,8 +2,8 @@ source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/_common.sh" # Type-check the flutter_readium web TypeScript (sources + Jest tests) without -# emitting any output. Uses web/_scripts/tsconfig.test.json so test files are -# included too. Run after editing any TS under flutter_readium/web/_scripts. +# emitting any output. Uses web/src/tsconfig.test.json so test files are +# included too. Run after editing any TS under flutter_readium/web/src. echo "Type-checking flutter_readium web TypeScript" cd "$REPO_ROOT/flutter_readium" diff --git a/docs/architecture.md b/docs/architecture.md index 44ac925a..55c55aa7 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -40,7 +40,7 @@ All models in `flutter_readium_platform_interface` define hand-written `toJson` ## Web implementation -The web plugin is a TypeScript/webpack bundle (`flutter_readium/web/_scripts/`) compiled to `flutter_readium/lib/helpers/readiumReader.js`. It is loaded inside a webview and communicates with Dart via `postMessage` / JS interop. +The web plugin is a TypeScript/webpack bundle (`flutter_readium/web/src/`) compiled to `flutter_readium/lib/helpers/readiumReader.js`. It is loaded inside a webview and communicates with Dart via `postMessage` / JS interop. After any TypeScript change run `bin/update_web_example` to rebuild and deploy to the example app. diff --git a/docs/parity/README.md b/docs/parity/README.md index 07e362ff..05e8431d 100644 --- a/docs/parity/README.md +++ b/docs/parity/README.md @@ -32,9 +32,9 @@ These plans have been implemented on the `feat/web-feature-parity` branch and ar ## Considered and deferred -- **Web TTS audit** — The original audit produced a `web-tts.md` plan claiming web TTS was entirely unimplemented. That premise is incorrect: `WebTTSEngine` lives at [flutter_readium/web/_scripts/TTS/ttsNavigator.ts](../../flutter_readium/web/_scripts/TTS/ttsNavigator.ts) and the full `ttsEnable` / `play` / `pause` / `stop` / `next` / `previous` / voice-selection surface is wired through `ReadiumReader.ts`. A genuine TTS parity audit (word-level highlighting, decoration sync, missing-voice fallback) is still worthwhile but needs a fresh investigation against the current code. +- **Web TTS audit** — The original audit produced a `web-tts.md` plan claiming web TTS was entirely unimplemented. That premise is incorrect: `WebTTSEngine` lives at [flutter_readium/web/src/TTS/ttsNavigator.ts](../../flutter_readium/web/src/TTS/ttsNavigator.ts) and the full `ttsEnable` / `play` / `pause` / `stop` / `next` / `previous` / voice-selection surface is wired through `ReadiumReader.ts`. A genuine TTS parity audit (word-level highlighting, decoration sync, missing-voice fallback) is still worthwhile but needs a fresh investigation against the current code. -- **Media overlay on web audit** — Same staleness: the original `media-overlay-missing-on-web.md` plan was written against an outdated snapshot. Media Overlay (`application/vnd.readium.narration+json`) and Guided Navigation (`application/guided-navigation+json`) both play through `audioEnable` on web today via [Audio/mediaOverlayNavigator.ts](../../flutter_readium/web/_scripts/Audio/mediaOverlayNavigator.ts) and [Audio/guidedNavigation.ts](../../flutter_readium/web/_scripts/Audio/guidedNavigation.ts). Any follow-up parity work in this area should start from a fresh audit. +- **Media overlay on web audit** — Same staleness: the original `media-overlay-missing-on-web.md` plan was written against an outdated snapshot. Media Overlay (`application/vnd.readium.narration+json`) and Guided Navigation (`application/guided-navigation+json`) both play through `audioEnable` on web today via [Audio/mediaOverlayNavigator.ts](../../flutter_readium/web/src/Audio/mediaOverlayNavigator.ts) and [Audio/guidedNavigation.ts](../../flutter_readium/web/src/Audio/guidedNavigation.ts). Any follow-up parity work in this area should start from a fresh audit. - **Positions list API** — Both upstream toolkits compute a `positionsByReadingOrder` list internally but do not expose it as a public API on their navigators. The Dart `PositionsList` model exists, but surfacing it would require either polling the publication-level service or adding a dedicated method-channel call. Consumer demand is low compared to the maintenance cost; deferred. diff --git a/docs/parity/web-comic-support-plan.md b/docs/parity/web-comic-support-plan.md index 6248b56b..12d71569 100644 --- a/docs/parity/web-comic-support-plan.md +++ b/docs/parity/web-comic-support-plan.md @@ -87,10 +87,10 @@ In `ReadiumReader._syncVisualToMediaOverlayLocator`: - Add a `## Unreleased` `fix(web):` entry to `CHANGELOG.md`. ## Key files -- `flutter_readium/web/_scripts/Epub/epubNavigator.ts` — inject helper in `frameLoaded`. -- `flutter_readium/web/_scripts/helpers.ts` — `injectComicBookHelper` util. -- `flutter_readium/web/_scripts/ReadiumReader.ts` — comic special-case + duration plumbing. -- `flutter_readium/web/_scripts/Audio/mediaOverlayNavigator.ts` — pass cue duration to callback (if needed). +- `flutter_readium/web/src/Epub/epubNavigator.ts` — inject helper in `frameLoaded`. +- `flutter_readium/web/src/helpers.ts` — `injectComicBookHelper` util. +- `flutter_readium/web/src/ReadiumReader.ts` — comic special-case + duration plumbing. +- `flutter_readium/web/src/Audio/mediaOverlayNavigator.ts` — pass cue duration to callback (if needed). - `CHANGELOG.md`. ## Decision (confirmed) diff --git a/docs/parity/web-decorations.md b/docs/parity/web-decorations.md index 1fe43d81..2a3c6b3e 100644 --- a/docs/parity/web-decorations.md +++ b/docs/parity/web-decorations.md @@ -10,7 +10,7 @@ `applyDecorations` is the primary way consumers persist user highlights and annotations across a session. On iOS and Android, calling `applyDecorations(id, decorations)` renders coloured highlight / underline overlays on the text and fires `onDecorationInteraction` when the user taps one. On Web, the method is a no-op — decorations are silently dropped. A consumer writing a cross-platform app cannot restore saved highlights on the web reader without this, which is a user-visible regression whenever the web target is used. -The ts-toolkit EpubNavigator internally uses a `decorate` message via its frame-comms protocol (visible in `flutter_readium/web/_scripts/helpers.ts` at `highlightSelection()`) — so the underlying machinery exists in the npm package; it just isn't wired to the Dart `applyDecorations` call. +The ts-toolkit EpubNavigator internally uses a `decorate` message via its frame-comms protocol (visible in `flutter_readium/web/src/helpers.ts` at `highlightSelection()`) — so the underlying machinery exists in the npm package; it just isn't wired to the Dart `applyDecorations` call. ## Current state @@ -21,7 +21,7 @@ The ts-toolkit EpubNavigator internally uses a `decorate` message via its frame- ## Proposed approach -1. **JS side** (`flutter_readium/web/_scripts/ReadiumReader.ts`): Add a public `applyDecorations(groupId: string, decorationsJson: string): void` method that deserialises the array and calls the navigator's frame-comms `decorate` message (following the pattern already used in `helpers.ts`'s `highlightSelection()`). Add a matching method to the `ReadiumReader` JS interop extension type in `js_publication_channel.dart`. +1. **JS side** (`flutter_readium/web/src/ReadiumReader.ts`): Add a public `applyDecorations(groupId: string, decorationsJson: string): void` method that deserialises the array and calls the navigator's frame-comms `decorate` message (following the pattern already used in `helpers.ts`'s `highlightSelection()`). Add a matching method to the `ReadiumReader` JS interop extension type in `js_publication_channel.dart`. 2. **Decoration interactions**: Wire the navigator's `decorationActivated` (or equivalent listener) to call back into Dart via `onDecorationInteractionCallback`. The `onDecorationInteractionCallback` setter and `onDecorationInteractionHandler` are already plumbed in `readium_webview.dart`. 3. **Dart web plugin** (`flutter_readium_web.dart`): Replace the no-op with a real call to the new `JsPublicationChannel.applyDecorations(id, encodedList)`. 4. **Widget** (`reader_widget_web.dart`): Replace the no-op similarly. diff --git a/docs/web-ts-refactor-plan.md b/docs/web-ts-refactor-plan.md new file mode 100644 index 00000000..aed26d92 --- /dev/null +++ b/docs/web-ts-refactor-plan.md @@ -0,0 +1,170 @@ +# Web Platform (TypeScript) Structural Refactor + +## Context + +The web implementation lives in `flutter_readium/flutter_readium/web/_scripts/` and is built by webpack into `lib/helpers/readiumReader.js`. Today it centers on **`ReadiumReader.ts` — a 991-line god class** (`_ReadiumReader`, exported to `globalThis.ReadiumReader`) with ~30 public methods that mix publication lifecycle, visual navigation, EPUB preferences, decorations, audio playback, TTS, and media-overlay sync, and which holds *all* state. Navigators are created by free `initialize*()` functions returning bare upstream `@readium/navigator` objects, and `helpers.ts` (502 lines) is a grab-bag of manifest fetching, color conversion, decoration frame-comms, and iframe injection. Naming is inconsistent (`webPubPrefences.ts` typo; `Epub` vs `EPUB`). + +By contrast the **native iOS/Android sides** use clean OO abstractions: a `ReadiumReaderView` protocol (visual nav), a `FlutterTimebasedNavigator` protocol with TTS/Audio/MediaOverlay strategy implementations, `Flutter*`-prefixed wrapper classes (prefix dodges upstream-Readium name collisions), dedicated `model/`/`preferences/`/`events/` dirs, and factory dispatch on publication type. + +**Goal:** restructure the web TS to mirror the native architecture — better directory organization, consistent naming, and OO classes/interfaces — so the three platforms read alike and the web layer is maintainable. This is a structural refactor: logic moves largely verbatim; deep per-class cleanup is a later pass. + +**Decisions (confirmed with user):** +- Source dir: rename **`web/_scripts/` → `web/src/`** (conventional name; also disambiguates from the separate `assets/_helper_scripts/` webview-helper bundle). +- Wrapper class naming: **`Flutter*` prefix** (mirrors native; renames `WebTTSEngine` → `FlutterTTSNavigator`). +- Scope: **full restructure** — including collapsing `_ReadiumReader` into a thin facade. +- Delivery: **single PR, composed of small atomic commits** (one per migration step below), build + tests green at each commit. + +**Hard invariant:** the Dart↔JS contract must not change. The `globalThis.ReadiumReader` method names/signatures (`openPublication`, `goTo`, `setEPUBPreferences`, `ttsEnable`, …) and the `window.*` callback names/JSON shapes (`updateTextLocator`, `updateReaderStatus`, `updateTimebasedPlayerState`, `onTextSelectedCallback`, `onErrorCallback`) stay byte-identical. No changes to Dart `js_interop` (`lib/src/js_publication_channel.dart`) are required. + +## Target directory tree (`web/src/`) + +``` +index.ts # NEW webpack entry: import style.css, build facade, assign globalThis.ReadiumReader, install bridge +ReadiumReader.ts # SHRUNKEN facade/dispatcher (~150-200 lines) +style.css # unchanged + +bridge/ + ReadiumBridge.ts # ONLY code allowed to touch window.* emit callbacks + window.d.ts # was flutter.d.ts + +publication/ + PublicationManager.ts # publication lifecycle + static _publications cache + fetchManifest glue + +navigators/ + VisualNavigator.ts # INTERFACE (mirrors ReadiumReaderView) + TimebasedNavigator.ts # INTERFACE (mirrors FlutterTimebasedNavigator) + locatorEnrich.ts # shared enrichWithTotalProgression/tocHref/flattenToc (EPUB+WebPub both use) + FlutterEpubNavigator.ts # was Epub/epubNavigator.ts (free fn -> class impl VisualNavigator) + FlutterWebPubNavigator.ts # was WebPub/webpubNavigator.ts + FlutterAudioNavigator.ts # was Audio/audioNavigator.ts (impl TimebasedNavigator) + FlutterTTSNavigator.ts # was TTS/ttsNavigator.ts (WebTTSEngine renamed, impl TimebasedNavigator) + FlutterMediaOverlayNavigator.ts # was Audio/mediaOverlayNavigator.ts (owns sync + comic cue logic) + +mediaoverlay/ + syncNarration.ts # was Audio/syncNarration.ts + guidedNavigation.ts # was Audio/guidedNavigation.ts + +decorations/ + DecorationController.ts # applyDecorations/setDecorationStyle/_subgroupFor + style/group state (from god class) + decorationOverrides.ts # from helpers.ts: sendDecorate, navIframeWindows, registerPendingDecorationGroup, + # injectDecorationOverrides, UNDERLINE_GROUP_SUFFIX, highlightSelection + +preferences/ + FlutterEpubPreferences.ts # was Epub/epubPreferences.ts (+ convertVerticalScroll/textAlignFromJson/normalizeTypes) + FlutterWebPubPreferences.ts # was WebPub/webPubPrefences.ts <- TYPO FIXED + FlutterAudioPreferences.ts # NEW: extracted setAudioPreferences mapping (from god class) + preferencesFromString (from audioNav) + FlutterTTSPreferences.ts # was TTS/ttsPreferences.ts + +model/ + ReadiumReaderStatus.ts # was enums.ts (room for typed ReadiumTimebasedState later) + +utils/ + ReadiumPluginLogger.ts # was logger.ts + ReadiumExtensions.ts # was extensions/ReadiumPublication.ts + helpers' mediaTypes + findLinkByHref + colors.ts # was helpers' dartColorToCss + manifest.ts # was helpers' fetchManifest + iframeInjection.ts # was helpers' injectFlutterReadiumHelperScripts + asset cache + Peripherals.ts # was peripherals.ts + +__tests__/ # import paths updated to final locations (re-export shims bridge the transition) +``` + +`helpers.ts` is fully dissolved across the above. + +## Interfaces to introduce + +**`VisualNavigator`** (mirrors iOS `ReadiumReaderView`) — implemented by `FlutterEpubNavigator`, `FlutterWebPubNavigator`: +- `underlying` (escape hatch for decoration frame-comms), `currentLocator`, `positions` (EPUB has them; WebPub `[]`) +- `goRight/goLeft/goForward/goBackward`, `goToLink(link, animated)`, `goToLocator(locator, animated)`, `goToProgression(p)`, `destroy()` +- The `initialize*AndPeripherals` free functions become static `create(...)` factories returning the wrapper (the `setNav`/`setPositions` callbacks disappear). + +**`TimebasedNavigator`** (mirrors iOS `FlutterTimebasedNavigator`) — implemented by `FlutterTTSNavigator`, `FlutterAudioNavigator`, `FlutterMediaOverlayNavigator` (strategy pattern): +- `play(locator?)`, `pause()`, `resume()`, `stop()`, `next()`, `previous()`, `seekBy(s)`, `goToProgression(p)`, `goToLocator(locator)`, `destroy()` +- Collapses the facade's repeated `if (this._ttsEngine) … else if (this._audioNav) …` branching into "dispatch to the single active `timebasedNav`." + +## Facade shape after collapse + +``` +class _ReadiumReader { + private bridge: ReadiumBridge; + private pubManager: PublicationManager; + private visualNav?: VisualNavigator; + private timebasedNav?: TimebasedNavigator; // TTS | Audio | MediaOverlay (the active one) + private decorations: DecorationController; +} +``` +Public method names/signatures unchanged. Method → new home (representative; full map in commits): +- `getPublication` → `pubManager`; `openPublication`/`closePublication` → facade orchestrates collaborators +- `goRight/Left/Forward/Backward`, `goToProgression(visual)` → `visualNav` +- `goTo` → facade keeps the route decision (TTS vs MediaOverlay vs audiobook vs visual), delegates each branch; text↔audio mapping moves into `FlutterMediaOverlayNavigator.goToLocator` +- `applyDecorations/setDecorationStyle/_subgroupFor` → `DecorationController` +- `play/pause/resume/stop/next/previous/seekBy`, `audioEnable`, `ttsEnable`, `ttsSetVoice/ttsSetPreferences` → `timebasedNav` +- `setAudioPreferences`/`setEPUBPreferences` → `preferences/*` modules applied to the relevant nav +- `_syncVisualToMediaOverlayLocator`/`_callGotoComicFrame` + `_isComicBook`/`_lastMediaOverlayLocatorKey` → `FlutterMediaOverlayNavigator` +- `disableSynchronization` stays facade-side (cross-cutting plugin state, passed at enable-time) + +## Bridge (centralized `window.*`) + +`bridge/ReadiumBridge.ts` is the only module touching `window.*`. Typed methods wrap each callback with **identical names/JSON shapes**: `emitReaderStatus`, `emitTextLocator` (uses `JSON.stringify(locator.serialize())` per project convention), `emitTimebasedState`, `emitTextSelected`, `emitError`. Navigators receive the bridge (or the specific emit callbacks) by injection rather than reaching `window` directly. The 200ms EPUB text-locator debounce is preserved (stays in `FlutterEpubNavigator` calling `bridge.emitTextLocator`). + +## Migration ordering (each = one atomic commit; build + jest + tsc green at every step) + +Principle: **move/rename first behind re-export shims, refactor internals last.** + +**Phase 0 — rename source dir (`git mv`, no code change)** +0. `git mv web/_scripts web/src`. Update path references: `flutter_readium/package.json` (4 script paths: `build_dev`, `build`, `typecheck`, `test`), `web/src/webpack.config.js` (entry comment — output is `__dirname`-relative so unchanged), `.vscode/{launch,settings,tasks}.json`, `.github/instructions/typescript.instructions.md`, `CLAUDE.md`, `CONTRIBUTING.md`, `docs/architecture.md`, `docs/parity/*`, and the `bin/typecheck` comment. `bin/` scripts invoke `npm run`, so no functional change there. Verify with `bin/typecheck` + `npx jest` + `bin/build_js` (bundle still emits to `lib/helpers/readiumReader.js`). All subsequent steps operate under `web/src/`. + +**Phase A — non-behavioral moves** +1. Create `utils/`, `model/`; move logger→`ReadiumPluginLogger.ts`, peripherals→`Peripherals.ts`, `extensions/ReadiumPublication.ts`→`ReadiumExtensions.ts`, enums→`model/ReadiumReaderStatus.ts`. Re-export shims at old paths. +2. Split `helpers.ts` → `decorations/decorationOverrides.ts`, `utils/colors.ts`, `utils/manifest.ts`, `utils/iframeInjection.ts`, fold `mediaTypes`/`findLinkByHref` into `ReadiumExtensions.ts`. Keep `helpers.ts` as a re-export barrel. +3. Fix typo/casing: preferences files → `preferences/FlutterEpubPreferences.ts`, `FlutterWebPubPreferences.ts`, `FlutterTTSPreferences.ts`. Shims at old paths. + +**Phase B — navigators to classes (verbatim logic)** +4. `FlutterEpubNavigator` + shared `navigators/locatorEnrich.ts` (move `enrichWithTotalProgression`). Old free fn becomes thin wrapper. +5. `FlutterWebPubNavigator` (imports enrich from shared module — removes the cross-`Epub` import). +6. `FlutterAudioNavigator` (preserve `export const __testing__`; re-export for tests). +7. `FlutterTTSNavigator` (rename `WebTTSEngine`). +8. `FlutterMediaOverlayNavigator` (preserve `__testing__`; move comic + cue-sync logic in from god class). + +**Phase C — extract facade collaborators** +9. `bridge/ReadiumBridge.ts` + `window.d.ts`; route all `window.*` through it. +10. `publication/PublicationManager.ts`. +11. `decorations/DecorationController.ts`. +12. `preferences/FlutterAudioPreferences.ts`. + +**Phase D — collapse + re-point entry** +13. Rewrite `_ReadiumReader` as the thin dispatcher delegating to collaborators + active navigators. Diff its public-method list against the original to confirm the contract is intact. +14. Add `index.ts` entry; update `webpack.config.js` `entry: ReadiumReader.ts → index.ts`. Output path `../../lib/helpers/readiumReader.js` unchanged. +15. Delete all re-export shims; update remaining `__tests__` imports to final paths; remove `helpers.ts` barrel. + +**Imports/paths that must be touched (only these):** dir-rename path references in step 0 (package.json, .vscode, docs, instructions); `__tests__/*.test.ts` import paths (deferred via shims until each phase; preserve `__testing__` and named exports `enrichWithTotalProgression`/`dartColorToCss`); `webpack.config.js` entry (step 14). **Dart `js_interop`: no changes.** + +## Critical files (paths shown post-rename, i.e. under `web/src/`) + +- `web/src/ReadiumReader.ts` — the god class to dissolve +- `web/src/helpers.ts` — the grab-bag to split +- `web/src/Audio/{audioNavigator,mediaOverlayNavigator,syncNarration,guidedNavigation}.ts` +- `web/src/{Epub,WebPub,TTS}/*` — navigators + preferences to reclass/rename +- `web/src/webpack.config.js` — entry path (step 14 only) +- `web/src/__tests__/*.test.ts` — import-path updates +- `flutter_readium/package.json`, `.vscode/*` — dir-rename path references (step 0) + +## Verification (per commit + final) + +Run from repo root unless noted: +- `bin/typecheck` — `tsc --noEmit` over web TS (run after any TS edit). +- **Unit tests (Jest):** `npm test` — the **7** existing suites (`audioNavigator`, `epubNavigator`, `guidedNavigation`, `mediaOverlayNavigator`, `syncNarration`, `closePublication`, `helpers`) must stay green at **every** commit. As files move, update each test's import paths (e.g. `helpers.test.ts` imports `dartColorToCss` from `../helpers`; `closePublication.test.ts` and the navigator tests import the modules/`__testing__` exports directly). Re-export shims keep them green until their owning step; final step re-points them to canonical paths. **Do not weaken or skip a test to make it pass** — a failing unit test signals a real regression. +- **Integration tests (Dart):** `example/integration_test/` (`plugin_integration_test.dart` with `test_fixtures_web.dart`) exercise the web platform end-to-end and are the strongest contract-regression gate. Because the Dart↔JS contract is preserved, **these must pass with their source unchanged** — if a fixture or assertion needs editing to pass, treat it as evidence the contract drifted and fix the TS, not the test. Run them on the web target (`flutter test integration_test` / driver) after the facade collapse (step 13) and again at the end (step 15). +- `bin/build_js` then `bin/update_web_example` — confirm `lib/helpers/readiumReader.js` regenerates and is copied into `example/web/`. +- `bin/format` && `bin/analyze` — Dart side unchanged but required pre-PR (checks all packages). +- **End-to-end smoke (final, mandatory for this behavioral refactor):** run the example app on web/Chrome, then exercise each affected path — open an EPUB (paginated + scroll), navigate (arrows/goTo/progression), apply a highlight/underline decoration, enable TTS and play/pause/next, play a media-overlay/sync-narration title and confirm visual sync + comic-frame handling, open a plain audiobook and seek. Confirm via the reader UI and logs that `updateTextLocator`/`updateTimebasedPlayerState`/`updateReaderStatus` still fire with unchanged shapes. (Per project memory: do not delegate Flutter-web in-browser verification to a sub-agent — verify manually.) +- **Contract check:** diff the final facade's public method names against the original `_ReadiumReader` and the `window.*` callback names to prove the Dart↔JS contract is byte-identical. + +## Execution + +- **Plan persisted in repo** at `docs/web-ts-refactor-plan.md` (committed as the first commit on the feature branch) so it travels with the PR and reviewers can follow the step map. +- Implementation proceeds **sequentially**, one atomic commit per numbered step (0 → 15); each step runs only after the previous step's verification gate passes. Steps are dependent — re-export shims/barrels are removed only in step 15, so steps must not be reordered or parallelized. + +## CHANGELOG + +This is an internal restructure with no consumer-visible behavior change, so no CHANGELOG entry is warranted (Keep-a-Changelog test fails). Note the rationale in the PR description instead. diff --git a/flutter_readium/package.json b/flutter_readium/package.json index 45be7b6d..1cf45157 100644 --- a/flutter_readium/package.json +++ b/flutter_readium/package.json @@ -19,9 +19,9 @@ }, "scripts": { "postinstall": "patch-package", - "build_dev": "webpack --config ./web/_scripts/webpack.config.js --mode development", - "build": "webpack --config ./web/_scripts/webpack.config.js --mode production", - "typecheck": "tsc --noEmit -p web/_scripts/tsconfig.json", - "test": "jest --config web/_scripts/jest.config.js" + "build_dev": "webpack --config ./web/src/webpack.config.js --mode development", + "build": "webpack --config ./web/src/webpack.config.js --mode production", + "typecheck": "tsc --noEmit -p web/src/tsconfig.json", + "test": "jest --config web/src/jest.config.js" } } diff --git a/flutter_readium/web/_scripts/Audio/audioNavigator.ts b/flutter_readium/web/src/Audio/audioNavigator.ts similarity index 100% rename from flutter_readium/web/_scripts/Audio/audioNavigator.ts rename to flutter_readium/web/src/Audio/audioNavigator.ts diff --git a/flutter_readium/web/_scripts/Audio/guidedNavigation.ts b/flutter_readium/web/src/Audio/guidedNavigation.ts similarity index 100% rename from flutter_readium/web/_scripts/Audio/guidedNavigation.ts rename to flutter_readium/web/src/Audio/guidedNavigation.ts diff --git a/flutter_readium/web/_scripts/Audio/mediaOverlayNavigator.ts b/flutter_readium/web/src/Audio/mediaOverlayNavigator.ts similarity index 100% rename from flutter_readium/web/_scripts/Audio/mediaOverlayNavigator.ts rename to flutter_readium/web/src/Audio/mediaOverlayNavigator.ts diff --git a/flutter_readium/web/_scripts/Audio/syncNarration.ts b/flutter_readium/web/src/Audio/syncNarration.ts similarity index 100% rename from flutter_readium/web/_scripts/Audio/syncNarration.ts rename to flutter_readium/web/src/Audio/syncNarration.ts diff --git a/flutter_readium/web/_scripts/Epub/epubNavigator.ts b/flutter_readium/web/src/Epub/epubNavigator.ts similarity index 100% rename from flutter_readium/web/_scripts/Epub/epubNavigator.ts rename to flutter_readium/web/src/Epub/epubNavigator.ts diff --git a/flutter_readium/web/_scripts/Epub/epubPreferences.ts b/flutter_readium/web/src/Epub/epubPreferences.ts similarity index 100% rename from flutter_readium/web/_scripts/Epub/epubPreferences.ts rename to flutter_readium/web/src/Epub/epubPreferences.ts diff --git a/flutter_readium/web/_scripts/ReadiumReader.ts b/flutter_readium/web/src/ReadiumReader.ts similarity index 100% rename from flutter_readium/web/_scripts/ReadiumReader.ts rename to flutter_readium/web/src/ReadiumReader.ts diff --git a/flutter_readium/web/_scripts/TTS/ttsNavigator.ts b/flutter_readium/web/src/TTS/ttsNavigator.ts similarity index 100% rename from flutter_readium/web/_scripts/TTS/ttsNavigator.ts rename to flutter_readium/web/src/TTS/ttsNavigator.ts diff --git a/flutter_readium/web/_scripts/TTS/ttsPreferences.ts b/flutter_readium/web/src/TTS/ttsPreferences.ts similarity index 100% rename from flutter_readium/web/_scripts/TTS/ttsPreferences.ts rename to flutter_readium/web/src/TTS/ttsPreferences.ts diff --git a/flutter_readium/web/_scripts/WebPub/webPubPrefences.ts b/flutter_readium/web/src/WebPub/webPubPrefences.ts similarity index 100% rename from flutter_readium/web/_scripts/WebPub/webPubPrefences.ts rename to flutter_readium/web/src/WebPub/webPubPrefences.ts diff --git a/flutter_readium/web/_scripts/WebPub/webpubNavigator.ts b/flutter_readium/web/src/WebPub/webpubNavigator.ts similarity index 100% rename from flutter_readium/web/_scripts/WebPub/webpubNavigator.ts rename to flutter_readium/web/src/WebPub/webpubNavigator.ts diff --git a/flutter_readium/web/_scripts/__tests__/audioNavigator.test.ts b/flutter_readium/web/src/__tests__/audioNavigator.test.ts similarity index 100% rename from flutter_readium/web/_scripts/__tests__/audioNavigator.test.ts rename to flutter_readium/web/src/__tests__/audioNavigator.test.ts diff --git a/flutter_readium/web/_scripts/__tests__/closePublication.test.ts b/flutter_readium/web/src/__tests__/closePublication.test.ts similarity index 100% rename from flutter_readium/web/_scripts/__tests__/closePublication.test.ts rename to flutter_readium/web/src/__tests__/closePublication.test.ts diff --git a/flutter_readium/web/_scripts/__tests__/epubNavigator.test.ts b/flutter_readium/web/src/__tests__/epubNavigator.test.ts similarity index 100% rename from flutter_readium/web/_scripts/__tests__/epubNavigator.test.ts rename to flutter_readium/web/src/__tests__/epubNavigator.test.ts diff --git a/flutter_readium/web/_scripts/__tests__/guidedNavigation.test.ts b/flutter_readium/web/src/__tests__/guidedNavigation.test.ts similarity index 100% rename from flutter_readium/web/_scripts/__tests__/guidedNavigation.test.ts rename to flutter_readium/web/src/__tests__/guidedNavigation.test.ts diff --git a/flutter_readium/web/_scripts/__tests__/helpers.test.ts b/flutter_readium/web/src/__tests__/helpers.test.ts similarity index 100% rename from flutter_readium/web/_scripts/__tests__/helpers.test.ts rename to flutter_readium/web/src/__tests__/helpers.test.ts diff --git a/flutter_readium/web/_scripts/__tests__/mediaOverlayNavigator.test.ts b/flutter_readium/web/src/__tests__/mediaOverlayNavigator.test.ts similarity index 100% rename from flutter_readium/web/_scripts/__tests__/mediaOverlayNavigator.test.ts rename to flutter_readium/web/src/__tests__/mediaOverlayNavigator.test.ts diff --git a/flutter_readium/web/_scripts/__tests__/style.stub.js b/flutter_readium/web/src/__tests__/style.stub.js similarity index 100% rename from flutter_readium/web/_scripts/__tests__/style.stub.js rename to flutter_readium/web/src/__tests__/style.stub.js diff --git a/flutter_readium/web/_scripts/__tests__/syncNarration.test.ts b/flutter_readium/web/src/__tests__/syncNarration.test.ts similarity index 100% rename from flutter_readium/web/_scripts/__tests__/syncNarration.test.ts rename to flutter_readium/web/src/__tests__/syncNarration.test.ts diff --git a/flutter_readium/web/_scripts/enums.ts b/flutter_readium/web/src/enums.ts similarity index 100% rename from flutter_readium/web/_scripts/enums.ts rename to flutter_readium/web/src/enums.ts diff --git a/flutter_readium/web/_scripts/extensions/ReadiumPublication.ts b/flutter_readium/web/src/extensions/ReadiumPublication.ts similarity index 100% rename from flutter_readium/web/_scripts/extensions/ReadiumPublication.ts rename to flutter_readium/web/src/extensions/ReadiumPublication.ts diff --git a/flutter_readium/web/_scripts/flutter.d.ts b/flutter_readium/web/src/flutter.d.ts similarity index 100% rename from flutter_readium/web/_scripts/flutter.d.ts rename to flutter_readium/web/src/flutter.d.ts diff --git a/flutter_readium/web/_scripts/helpers.ts b/flutter_readium/web/src/helpers.ts similarity index 100% rename from flutter_readium/web/_scripts/helpers.ts rename to flutter_readium/web/src/helpers.ts diff --git a/flutter_readium/web/_scripts/jest.config.js b/flutter_readium/web/src/jest.config.js similarity index 100% rename from flutter_readium/web/_scripts/jest.config.js rename to flutter_readium/web/src/jest.config.js diff --git a/flutter_readium/web/_scripts/logger.ts b/flutter_readium/web/src/logger.ts similarity index 100% rename from flutter_readium/web/_scripts/logger.ts rename to flutter_readium/web/src/logger.ts diff --git a/flutter_readium/web/_scripts/peripherals.ts b/flutter_readium/web/src/peripherals.ts similarity index 100% rename from flutter_readium/web/_scripts/peripherals.ts rename to flutter_readium/web/src/peripherals.ts diff --git a/flutter_readium/web/_scripts/style.css b/flutter_readium/web/src/style.css similarity index 100% rename from flutter_readium/web/_scripts/style.css rename to flutter_readium/web/src/style.css diff --git a/flutter_readium/web/_scripts/tsconfig.json b/flutter_readium/web/src/tsconfig.json similarity index 100% rename from flutter_readium/web/_scripts/tsconfig.json rename to flutter_readium/web/src/tsconfig.json diff --git a/flutter_readium/web/_scripts/tsconfig.test.json b/flutter_readium/web/src/tsconfig.test.json similarity index 100% rename from flutter_readium/web/_scripts/tsconfig.test.json rename to flutter_readium/web/src/tsconfig.test.json diff --git a/flutter_readium/web/_scripts/webpack.config.js b/flutter_readium/web/src/webpack.config.js similarity index 98% rename from flutter_readium/web/_scripts/webpack.config.js rename to flutter_readium/web/src/webpack.config.js index 340eb82b..e360da6d 100644 --- a/flutter_readium/web/_scripts/webpack.config.js +++ b/flutter_readium/web/src/webpack.config.js @@ -5,7 +5,7 @@ module.exports = (argv) => { const isDev = argv.mode === "development"; return { mode: argv.mode || "production", - entry: path.resolve(__dirname, "ReadiumReader.ts"), // Entry point relative to '_scripts' + entry: path.resolve(__dirname, "ReadiumReader.ts"), // Entry point relative to 'src' output: { // TODO: differentiate dev and prod output filenames when ready filename: "readiumReader.js", // Name of the output file From d9b9a82135594ea52b70fdb8bd510a70cbd7499d Mon Sep 17 00:00:00 2001 From: Daniel Freiling Date: Wed, 3 Jun 2026 18:24:39 +0200 Subject: [PATCH 02/29] =?UTF-8?q?refactor(web):=20Phase=20A1=20=E2=80=94?= =?UTF-8?q?=20move=20logger/peripherals/extensions/enums=20to=20utils/=20a?= =?UTF-8?q?nd=20model/?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- flutter_readium/web/src/enums.ts | 9 +- .../web/src/extensions/ReadiumPublication.ts | 27 +---- flutter_readium/web/src/logger.ts | 113 +----------------- .../web/src/model/ReadiumReaderStatus.ts | 7 ++ flutter_readium/web/src/peripherals.ts | 76 +----------- flutter_readium/web/src/utils/Peripherals.ts | 73 +++++++++++ .../web/src/utils/ReadiumExtensions.ts | 25 ++++ .../web/src/utils/ReadiumPluginLogger.ts | 110 +++++++++++++++++ 8 files changed, 225 insertions(+), 215 deletions(-) create mode 100644 flutter_readium/web/src/model/ReadiumReaderStatus.ts create mode 100644 flutter_readium/web/src/utils/Peripherals.ts create mode 100644 flutter_readium/web/src/utils/ReadiumExtensions.ts create mode 100644 flutter_readium/web/src/utils/ReadiumPluginLogger.ts diff --git a/flutter_readium/web/src/enums.ts b/flutter_readium/web/src/enums.ts index 6ffd82b5..60a746da 100644 --- a/flutter_readium/web/src/enums.ts +++ b/flutter_readium/web/src/enums.ts @@ -1,7 +1,2 @@ -export enum ReadiumReaderStatus { - loading = "loading", - ready = "ready", - closed = "closed", - reachedEndOfPublication = "reachedEndOfPublication", - error = "error", -} +// Re-export shim — canonical location is model/ReadiumReaderStatus.ts +export { ReadiumReaderStatus } from "./model/ReadiumReaderStatus"; diff --git a/flutter_readium/web/src/extensions/ReadiumPublication.ts b/flutter_readium/web/src/extensions/ReadiumPublication.ts index 176c78b5..749063c2 100644 --- a/flutter_readium/web/src/extensions/ReadiumPublication.ts +++ b/flutter_readium/web/src/extensions/ReadiumPublication.ts @@ -1,25 +1,2 @@ -import { Profile, Publication } from "@readium/shared"; - -export class ReadiumPublication extends Publication { - private conformsToArray = this.manifest.metadata.conformsTo; - - public conformsToEpub: boolean = - this.conformsToArray?.some((profile) => { - return profile == Profile.EPUB; - }) ?? false; - - public conformsToAudiobook: boolean = - this.conformsToArray?.some((profile) => { - return profile == Profile.AUDIOBOOK; - }) ?? false; - - public conformsToDivina: boolean = - this.conformsToArray?.some((profile) => { - return profile == Profile.DIVINA; - }) ?? false; - - public conformsToPDF: boolean = - this.conformsToArray?.some((profile) => { - return profile == Profile.PDF; - }) ?? false; -} +// Re-export shim — canonical location is utils/ReadiumExtensions.ts +export { ReadiumPublication } from "../utils/ReadiumExtensions"; diff --git a/flutter_readium/web/src/logger.ts b/flutter_readium/web/src/logger.ts index 2c62c4d5..87e45e99 100644 --- a/flutter_readium/web/src/logger.ts +++ b/flutter_readium/web/src/logger.ts @@ -1,110 +1,3 @@ -/** - * Lightweight tagged logger for the Readium web bundle. - * - * All messages are prefixed with the padded level and `[Readium/]` so - * they align vertically and can be easily filtered in the browser DevTools - * console: - * - * DEBUG [Readium/Reader] goForward - * INFO [Readium/WebPlugin] publication opened - * WARN [Readium/TTS] voice not found, using default - * ERROR [Readium/Reader] failed to open publication - * - * The active level can be changed at runtime via `setLogLevel()` which is - * exposed on the ReadiumReader class so the Dart side can control verbosity - * through the plugin's `setLogLevel` interface method. - */ - -/** - * Log verbosity levels. - * - * Values are intentionally identical to the indices of Dart's `LogLevel` enum: - * none=0, error=1, warn=2, info=3, debug=4 - * - * This means the Dart bridge can pass `level.index` directly as a number and - * this enum interprets it correctly — no translation layer needed. - * Do NOT reorder or insert values. - */ -export enum LogLevel { - none = 0, - error = 1, - warn = 2, - info = 3, - debug = 4, -} - -let _currentLevel: LogLevel = LogLevel.info; - -/** Sets the minimum log level. Messages below this level are suppressed. */ -export function setLogLevel(level: LogLevel): void { - _currentLevel = level; -} - -/** Returns the current active log level. */ -export function getLogLevel(): LogLevel { - return _currentLevel; -} - -export interface Logger { - debug(...args: unknown[]): void; - info(...args: unknown[]): void; - warn(...args: unknown[]): void; - error(...args: unknown[]): void; -} - -/** - * Renders the log args into a single human-readable string. `Error`s collapse to - * `name: message`; other objects are JSON-stringified (with a `String()` fallback - * for circular structures). - */ -function format(args: unknown[]): string { - return args - .map((a) => { - if (typeof a === "string") return a; - if (a instanceof Error) return `${a.name}: ${a.message}`; - try { - return JSON.stringify(a); - } catch { - return String(a); - } - }) - .join(" "); -} - -/** The object/Error args worth inspecting interactively in DevTools. */ -function inspectables(args: unknown[]): unknown[] { - return args.filter((a) => a !== null && typeof a === "object"); -} - -/** - * Each log call passes the fully-formatted message as the FIRST argument, then - * re-appends any object/Error args. - * - * Why both: browser DevTools renders the original variadic objects as expandable - * trees (with `Error` stacks), but `flutter drive`'s console bridge forwards only - * the FIRST argument to the terminal — so the old `console.info(level, prefix, - * ...args)` form collapsed to just "INFO" under web integration tests. Putting the - * full string first keeps the terminal output complete, while the trailing object - * args (ignored by flutter-drive) preserve DevTools' interactive inspection. - */ -export function createLogger(tag: string): Logger { - const prefix = `[Readium/${tag}]`; - return { - debug: (...args) => { - if (_currentLevel >= LogLevel.debug) - console.debug(`DEBUG ${prefix} ${format(args)}`, ...inspectables(args)); - }, - info: (...args) => { - if (_currentLevel >= LogLevel.info) - console.info(`INFO ${prefix} ${format(args)}`, ...inspectables(args)); - }, - warn: (...args) => { - if (_currentLevel >= LogLevel.warn) - console.warn(`WARN ${prefix} ${format(args)}`, ...inspectables(args)); - }, - error: (...args) => { - if (_currentLevel >= LogLevel.error) - console.error(`ERROR ${prefix} ${format(args)}`, ...inspectables(args)); - }, - }; -} +// Re-export shim — canonical location is utils/ReadiumPluginLogger.ts +export { createLogger, setLogLevel, getLogLevel, LogLevel } from "./utils/ReadiumPluginLogger"; +export type { Logger } from "./utils/ReadiumPluginLogger"; diff --git a/flutter_readium/web/src/model/ReadiumReaderStatus.ts b/flutter_readium/web/src/model/ReadiumReaderStatus.ts new file mode 100644 index 00000000..6ffd82b5 --- /dev/null +++ b/flutter_readium/web/src/model/ReadiumReaderStatus.ts @@ -0,0 +1,7 @@ +export enum ReadiumReaderStatus { + loading = "loading", + ready = "ready", + closed = "closed", + reachedEndOfPublication = "reachedEndOfPublication", + error = "error", +} diff --git a/flutter_readium/web/src/peripherals.ts b/flutter_readium/web/src/peripherals.ts index d9c19bc2..5cc4ee07 100644 --- a/flutter_readium/web/src/peripherals.ts +++ b/flutter_readium/web/src/peripherals.ts @@ -1,73 +1,3 @@ -// Peripherals based on XBReader -// copied from readium ts-toolkit test app vanilla - -export interface PCallbacks { - moveTo: (direction: 'left' | 'right' | 'up' | 'down') => void; - menu: (show?: boolean) => void; - goProgression: (shiftKey?: boolean) => void; -} - -export default class Peripherals { - private readonly observers = ['keyup', 'keydown']; - private targets: EventTarget[] = []; - private readonly callbacks: PCallbacks; - - constructor(callbacks: PCallbacks) { - this.observers.forEach((method) => { - (this as any)['on' + method] = (this as any)['on' + method].bind(this); - }); - this.callbacks = callbacks; - } - - destroy() { - this.targets.forEach((t) => this.unobserve(t)); - } - - unobserve(item: EventTarget) { - if (!item) return; - this.observers.forEach((EventName) => { - item.removeEventListener( - EventName, - (this as any)['on' + EventName], - false - ); - }); - this.targets = this.targets.filter((t) => t !== item); - } - - observe(item: EventTarget) { - if (!item) return; - if (this.targets.includes(item)) return; - this.observers.forEach((EventName) => { - item.addEventListener(EventName, (this as any)['on' + EventName], false); - }); - this.targets.push(item); - } - - onkeyup(e: KeyboardEvent) { - if (e.code === 'Space') this.callbacks.goProgression(e.shiftKey); - if (e.code === 'Enter') this.callbacks.menu(true); - } - - onkeydown(e: KeyboardEvent) { - // TODO: look into focus check so that we don't handle keys when the user is typing in an input field or textarea. - switch (e.code) { - case 'ArrowRight': - this.callbacks.moveTo('right'); - e.preventDefault(); - break; - case 'ArrowLeft': - this.callbacks.moveTo('left'); - e.preventDefault(); - break; - case 'ArrowUp': - this.callbacks.moveTo('up'); - e.preventDefault(); - break; - case 'ArrowDown': - this.callbacks.moveTo('down'); - e.preventDefault(); - break; - } - } -} +// Re-export shim — canonical location is utils/Peripherals.ts +export { default } from "./utils/Peripherals"; +export type { PCallbacks } from "./utils/Peripherals"; diff --git a/flutter_readium/web/src/utils/Peripherals.ts b/flutter_readium/web/src/utils/Peripherals.ts new file mode 100644 index 00000000..d9c19bc2 --- /dev/null +++ b/flutter_readium/web/src/utils/Peripherals.ts @@ -0,0 +1,73 @@ +// Peripherals based on XBReader +// copied from readium ts-toolkit test app vanilla + +export interface PCallbacks { + moveTo: (direction: 'left' | 'right' | 'up' | 'down') => void; + menu: (show?: boolean) => void; + goProgression: (shiftKey?: boolean) => void; +} + +export default class Peripherals { + private readonly observers = ['keyup', 'keydown']; + private targets: EventTarget[] = []; + private readonly callbacks: PCallbacks; + + constructor(callbacks: PCallbacks) { + this.observers.forEach((method) => { + (this as any)['on' + method] = (this as any)['on' + method].bind(this); + }); + this.callbacks = callbacks; + } + + destroy() { + this.targets.forEach((t) => this.unobserve(t)); + } + + unobserve(item: EventTarget) { + if (!item) return; + this.observers.forEach((EventName) => { + item.removeEventListener( + EventName, + (this as any)['on' + EventName], + false + ); + }); + this.targets = this.targets.filter((t) => t !== item); + } + + observe(item: EventTarget) { + if (!item) return; + if (this.targets.includes(item)) return; + this.observers.forEach((EventName) => { + item.addEventListener(EventName, (this as any)['on' + EventName], false); + }); + this.targets.push(item); + } + + onkeyup(e: KeyboardEvent) { + if (e.code === 'Space') this.callbacks.goProgression(e.shiftKey); + if (e.code === 'Enter') this.callbacks.menu(true); + } + + onkeydown(e: KeyboardEvent) { + // TODO: look into focus check so that we don't handle keys when the user is typing in an input field or textarea. + switch (e.code) { + case 'ArrowRight': + this.callbacks.moveTo('right'); + e.preventDefault(); + break; + case 'ArrowLeft': + this.callbacks.moveTo('left'); + e.preventDefault(); + break; + case 'ArrowUp': + this.callbacks.moveTo('up'); + e.preventDefault(); + break; + case 'ArrowDown': + this.callbacks.moveTo('down'); + e.preventDefault(); + break; + } + } +} diff --git a/flutter_readium/web/src/utils/ReadiumExtensions.ts b/flutter_readium/web/src/utils/ReadiumExtensions.ts new file mode 100644 index 00000000..176c78b5 --- /dev/null +++ b/flutter_readium/web/src/utils/ReadiumExtensions.ts @@ -0,0 +1,25 @@ +import { Profile, Publication } from "@readium/shared"; + +export class ReadiumPublication extends Publication { + private conformsToArray = this.manifest.metadata.conformsTo; + + public conformsToEpub: boolean = + this.conformsToArray?.some((profile) => { + return profile == Profile.EPUB; + }) ?? false; + + public conformsToAudiobook: boolean = + this.conformsToArray?.some((profile) => { + return profile == Profile.AUDIOBOOK; + }) ?? false; + + public conformsToDivina: boolean = + this.conformsToArray?.some((profile) => { + return profile == Profile.DIVINA; + }) ?? false; + + public conformsToPDF: boolean = + this.conformsToArray?.some((profile) => { + return profile == Profile.PDF; + }) ?? false; +} diff --git a/flutter_readium/web/src/utils/ReadiumPluginLogger.ts b/flutter_readium/web/src/utils/ReadiumPluginLogger.ts new file mode 100644 index 00000000..2c62c4d5 --- /dev/null +++ b/flutter_readium/web/src/utils/ReadiumPluginLogger.ts @@ -0,0 +1,110 @@ +/** + * Lightweight tagged logger for the Readium web bundle. + * + * All messages are prefixed with the padded level and `[Readium/]` so + * they align vertically and can be easily filtered in the browser DevTools + * console: + * + * DEBUG [Readium/Reader] goForward + * INFO [Readium/WebPlugin] publication opened + * WARN [Readium/TTS] voice not found, using default + * ERROR [Readium/Reader] failed to open publication + * + * The active level can be changed at runtime via `setLogLevel()` which is + * exposed on the ReadiumReader class so the Dart side can control verbosity + * through the plugin's `setLogLevel` interface method. + */ + +/** + * Log verbosity levels. + * + * Values are intentionally identical to the indices of Dart's `LogLevel` enum: + * none=0, error=1, warn=2, info=3, debug=4 + * + * This means the Dart bridge can pass `level.index` directly as a number and + * this enum interprets it correctly — no translation layer needed. + * Do NOT reorder or insert values. + */ +export enum LogLevel { + none = 0, + error = 1, + warn = 2, + info = 3, + debug = 4, +} + +let _currentLevel: LogLevel = LogLevel.info; + +/** Sets the minimum log level. Messages below this level are suppressed. */ +export function setLogLevel(level: LogLevel): void { + _currentLevel = level; +} + +/** Returns the current active log level. */ +export function getLogLevel(): LogLevel { + return _currentLevel; +} + +export interface Logger { + debug(...args: unknown[]): void; + info(...args: unknown[]): void; + warn(...args: unknown[]): void; + error(...args: unknown[]): void; +} + +/** + * Renders the log args into a single human-readable string. `Error`s collapse to + * `name: message`; other objects are JSON-stringified (with a `String()` fallback + * for circular structures). + */ +function format(args: unknown[]): string { + return args + .map((a) => { + if (typeof a === "string") return a; + if (a instanceof Error) return `${a.name}: ${a.message}`; + try { + return JSON.stringify(a); + } catch { + return String(a); + } + }) + .join(" "); +} + +/** The object/Error args worth inspecting interactively in DevTools. */ +function inspectables(args: unknown[]): unknown[] { + return args.filter((a) => a !== null && typeof a === "object"); +} + +/** + * Each log call passes the fully-formatted message as the FIRST argument, then + * re-appends any object/Error args. + * + * Why both: browser DevTools renders the original variadic objects as expandable + * trees (with `Error` stacks), but `flutter drive`'s console bridge forwards only + * the FIRST argument to the terminal — so the old `console.info(level, prefix, + * ...args)` form collapsed to just "INFO" under web integration tests. Putting the + * full string first keeps the terminal output complete, while the trailing object + * args (ignored by flutter-drive) preserve DevTools' interactive inspection. + */ +export function createLogger(tag: string): Logger { + const prefix = `[Readium/${tag}]`; + return { + debug: (...args) => { + if (_currentLevel >= LogLevel.debug) + console.debug(`DEBUG ${prefix} ${format(args)}`, ...inspectables(args)); + }, + info: (...args) => { + if (_currentLevel >= LogLevel.info) + console.info(`INFO ${prefix} ${format(args)}`, ...inspectables(args)); + }, + warn: (...args) => { + if (_currentLevel >= LogLevel.warn) + console.warn(`WARN ${prefix} ${format(args)}`, ...inspectables(args)); + }, + error: (...args) => { + if (_currentLevel >= LogLevel.error) + console.error(`ERROR ${prefix} ${format(args)}`, ...inspectables(args)); + }, + }; +} From 0091b75aa388f7b8255d027806ba455403bd249b Mon Sep 17 00:00:00 2001 From: Daniel Freiling Date: Wed, 3 Jun 2026 18:29:38 +0200 Subject: [PATCH 03/29] =?UTF-8?q?refactor(web):=20Phase=20A2=20=E2=80=94?= =?UTF-8?q?=20split=20helpers.ts=20into=20sub-modules?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- .../src/decorations/decorationOverrides.ts | 263 +++++++++++ flutter_readium/web/src/helpers.ts | 447 +----------------- .../web/src/utils/ReadiumExtensions.ts | 38 +- flutter_readium/web/src/utils/colors.ts | 11 + .../web/src/utils/iframeInjection.ts | 110 +++++ flutter_readium/web/src/utils/manifest.ts | 15 + 6 files changed, 454 insertions(+), 430 deletions(-) create mode 100644 flutter_readium/web/src/decorations/decorationOverrides.ts create mode 100644 flutter_readium/web/src/utils/colors.ts create mode 100644 flutter_readium/web/src/utils/iframeInjection.ts create mode 100644 flutter_readium/web/src/utils/manifest.ts diff --git a/flutter_readium/web/src/decorations/decorationOverrides.ts b/flutter_readium/web/src/decorations/decorationOverrides.ts new file mode 100644 index 00000000..8ab7fc2e --- /dev/null +++ b/flutter_readium/web/src/decorations/decorationOverrides.ts @@ -0,0 +1,263 @@ +// NOTE: decoration support here is experimental and will be replaced once +// https://github.com/readium/ts-toolkit/pull/209 (Decorator API) merges. + +import { EpubNavigator, WebPubNavigator } from "@readium/navigator"; +import { + BasicTextSelection, + Width, + Layout, + Decoration, +} from "@readium/navigator-html-injectables"; +import { Locator, LocatorText } from "@readium/shared"; +import { ReadiumPublication } from "../utils/ReadiumExtensions"; + +/** + * Group-name suffix used to mark decorations whose Dart style is "underline". + * Underline-style decorations are sent to a separate upstream group so the + * in-iframe override stylesheet can target them with `[data-group$="__underline"]` + * (DOM-fallback path) or with a sibling `