原生epub阅读

This commit is contained in:
DelLevin-Home
2026-06-27 14:06:44 +08:00
parent 5acbc3a602
commit d54e33e4cf
177 changed files with 6500 additions and 93933 deletions

View File

@@ -0,0 +1,87 @@
import 'dart:convert';
import 'webview_bridge.dart';
/// Typed Dart mirror of the TypeScript `ReaderApi` interface
/// (`web_assets/controller.js/api.ts`).
///
/// Every public method corresponds 1-to-1 with its TypeScript counterpart.
/// The token parameter is managed internally by [WebViewBridge] — callers
/// never touch raw token integers through this class.
///
/// Methods that return `Future<int>` fire the JS call and return a token the
/// caller can later pass to [WebViewBridge.waitForEvent] / [waitForEvents]
/// when it wants to batch-await multiple operations together.
///
/// Methods that return `Future<void>` fire the JS call and await its
/// completion before returning.
class ReaderApi {
final WebViewBridge _bridge;
ReaderApi(this._bridge);
// ─── Token-based (deferred await) ──────────────────────────────────
/// Loads [url] into the iframe identified by [slot].
/// [anchors] should be a JSON-encoded list: `'["id1","id2"]'`.
Future<int> loadFrame(
String slot,
String url,
String anchors,
String properties,
) => _bridge.call(
(t) => "window.api.loadFrame($t, '$slot', '$url', $anchors, $properties)",
);
/// Scrolls [slot]'s iframe to [pageIndex] without immediately awaiting.
Future<int> jumpToPageFor(String slot, int pageIndex) =>
_bridge.call((t) => "window.api.jumpToPageFor($t, '$slot', $pageIndex)");
/// Scrolls [slot]'s iframe to its last page without immediately awaiting.
Future<int> jumpToLastPageOfFrame(String slot) =>
_bridge.call((t) => "window.api.jumpToLastPageOfFrame($t, '$slot')");
/// Rotates the iframe triple in [direction] (`'next'` or `'prev'`).
Future<int> cycleFrames(String direction) =>
_bridge.call((t) => "window.api.cycleFrames($t, '$direction')");
// ─── Fire-and-await ────────────────────────────────────────────────
/// Scrolls the current iframe to [pageIndex] and awaits completion.
Future<void> jumpToPage(int pageIndex) =>
_bridge.callAndWait((t) => 'window.api.jumpToPage($t, $pageIndex)', 1000);
/// Restores the scroll position using a fractional [ratio] in [0,1].
Future<void> restoreScrollPosition(double ratio) => _bridge.callAndWait(
(t) => 'window.api.restoreScrollPosition($t, $ratio)',
1000,
);
/// Waits for the current frame to finish rendering.
Future<void> waitForRender() =>
_bridge.callAndWait((t) => 'window.api.waitForRender($t)', 1000);
/// Updates the reader theme/layout and awaits completion.
///
/// [theme] must be a JSON-serialisable map produced by `EpubTheme.toMap()`.
Future<void> updateTheme(
double viewWidth,
double viewHeight,
Map<String, dynamic> theme,
) {
final themeJson = jsonEncode(theme);
return _bridge.callAndWait(
(t) => 'window.api.updateTheme($t, $viewWidth, $viewHeight, $themeJson)',
);
}
// ─── Fire-and-forget ───────────────────────────────────────────────
/// Checks whether there is an interactive element (image, etc.) at (x, y).
Future<void> checkLongPressElementAt(double x, double y) =>
_bridge.evaluate('window.api.checkLongPressElementAt($x, $y)');
/// Checks whether the tap at (x, y) hits a link, footnote, or other element.
Future<void> checkTapElementAt(double x, double y) =>
_bridge.evaluate('window.api.checkTapElementAt($x, $y)');
}

View File

@@ -0,0 +1,119 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
/// Manages JS↔Dart communication over an [InAppWebViewController].
///
/// Provides token-based async call tracking so that callers can fire a JS
/// method that will eventually invoke `FlutterBridge.onEventFinished(token)`,
/// and await the result on the Dart side via [waitForEvent].
///
/// Typical usage:
/// ```dart
/// // Fire and forget the token; caller awaits separately.
/// final token = await _bridge.call((t) => "window.api.loadFrame($t, ...)");
/// await _bridge.waitForEvent(token);
///
/// // Fire and immediately await.
/// await _bridge.callAndWait((t) => "window.api.jumpToPage($t, $idx)");
/// ```
class WebViewBridge {
InAppWebViewController? _controller;
int _currentToken = 0;
final Map<int, Completer<void>> _completers = {};
// ─── Controller lifecycle ──────────────────────────────────────────
/// Attaches a live [InAppWebViewController]. Call this in `onWebViewCreated`.
void attach(InAppWebViewController controller) {
_controller = controller;
}
/// Detaches the controller and cancels all pending completers.
void detach() {
_controller = null;
for (final completer in _completers.values) {
if (!completer.isCompleted) {
completer.completeError(StateError('WebViewBridge detached'));
}
}
_completers.clear();
}
// ─── JS evaluation ─────────────────────────────────────────────────
/// Evaluates [source] in the WebView. No-ops if no controller is attached.
Future<void> evaluate(String source) async {
await _controller?.evaluateJavascript(source: source);
}
// ─── Token management ──────────────────────────────────────────────
/// Allocates a new token and registers a [Completer] for it.
///
/// Embed the returned token in the JS call so JS can resolve it via
/// `FlutterBridge.onEventFinished(token)`.
int issueToken() {
_currentToken++;
_completers[_currentToken] = Completer<void>();
return _currentToken;
}
/// Called by the `onEventFinished` JS handler to resolve a pending token.
///
/// A [token] of `-1` is a sentinel for fire-and-forget notifications that
/// do not need to be tracked.
void resolveToken(int token) {
if (token == -1) return;
final completer = _completers.remove(token);
if (completer != null && !completer.isCompleted) {
completer.complete();
}
}
// ─── Awaiting ──────────────────────────────────────────────────────
/// Waits for [token] to be resolved, or times out after [timeoutMs] ms.
Future<void> waitForEvent(int token, [int timeoutMs = 10000]) async {
final completer = _completers[token];
if (completer == null) {
debugPrint('WebViewBridge: no completer for token $token');
return;
}
return completer.future.timeout(
Duration(milliseconds: timeoutMs),
onTimeout: () {
_completers.remove(token);
debugPrint('WebViewBridge: timeout for token $token');
},
);
}
/// Waits for all [tokens] to be resolved concurrently.
Future<void> waitForEvents(List<int> tokens, [int timeoutMs = 10000]) async {
await Future.wait(tokens.map((t) => waitForEvent(t, timeoutMs)));
}
// ─── Convenience helpers ───────────────────────────────────────────
/// Issues a token, evaluates the JS returned by [source], and returns the
/// token so the caller can [waitForEvent] later.
Future<int> call(String Function(int token) source) async {
final token = issueToken();
await evaluate(source(token));
return token;
}
/// Issues a token, evaluates the JS returned by [source], and immediately
/// awaits [waitForEvent] before returning.
Future<void> callAndWait(
String Function(int token) source, [
int timeoutMs = 10000,
]) async {
final token = issueToken();
await evaluate(source(token));
await waitForEvent(token, timeoutMs);
}
}