generated from dellevin/template
原生epub阅读
This commit is contained in:
87
lib/utils/epub/web/reader_api.dart
Normal file
87
lib/utils/epub/web/reader_api.dart
Normal 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)');
|
||||
}
|
||||
119
lib/utils/epub/web/webview_bridge.dart
Normal file
119
lib/utils/epub/web/webview_bridge.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user