@ufjs/webview 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/components/WebViewWeb.vue +134 -0
- package/flutter/.dart_tool/flutter_build/dart_plugin_registrant.dart +132 -0
- package/flutter/.dart_tool/package_config.json +371 -0
- package/flutter/.dart_tool/package_config_subset +241 -0
- package/flutter/.dart_tool/version +1 -0
- package/flutter/.flutter-plugins +13 -0
- package/flutter/.flutter-plugins-dependencies +1 -0
- package/flutter/build/native_assets/macos/native_assets.yaml +5 -0
- package/flutter/build/test_cache/build/cache.dill.track.dill +4 -0
- package/flutter/build/unit_test_assets/AssetManifest.bin +0 -0
- package/flutter/build/unit_test_assets/AssetManifest.json +1 -0
- package/flutter/build/unit_test_assets/FontManifest.json +1 -0
- package/flutter/build/unit_test_assets/NOTICES.Z +0 -0
- package/flutter/build/unit_test_assets/shaders/ink_sparkle.frag +0 -0
- package/flutter/lib/fjs_webview.dart +472 -0
- package/flutter/pubspec.lock +468 -0
- package/flutter/pubspec.yaml +30 -0
- package/flutter/test/web_view_test.dart +369 -0
- package/index.ts +210 -0
- package/package.json +69 -0
- package/prepare.mjs +40 -0
- package/public/demo.html +79 -0
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
// Flutter side of the fjs module "webview": the WebView behind <web-view />.
|
|
2
|
+
//
|
|
3
|
+
// fjs autolinks this — the generated host depends on this package and calls
|
|
4
|
+
// FjsWebview.register(engine) before runApp, because the module's
|
|
5
|
+
// package.json says so in its "fjs.flutter" field.
|
|
6
|
+
//
|
|
7
|
+
// Three decisions worth writing down (specs/013-web-view):
|
|
8
|
+
//
|
|
9
|
+
// * `webview_flutter`, not `flutter_inappwebview`. The latter can intercept
|
|
10
|
+
// requests, inject scripts and manage cookies — all of which this spec
|
|
11
|
+
// puts out of scope. The contract here (src, three events) is small
|
|
12
|
+
// enough that swapping the implementation later would not reach pages.
|
|
13
|
+
// * `@error` only reports MAIN-DOCUMENT failures. A page whose tracking
|
|
14
|
+
// pixel 404s has not failed to load, and reporting it would make the
|
|
15
|
+
// event useless. The browser cannot report even that much (docs/web.md).
|
|
16
|
+
// * `asset://` has three resolutions, not two: the dev server while
|
|
17
|
+
// `fjs dev` is connected, a Flutter asset in a release build, and the
|
|
18
|
+
// app's own static root on the web. The rules live once in the module's
|
|
19
|
+
// index.ts; this file mirrors the two that are Dart's.
|
|
20
|
+
import 'dart:async';
|
|
21
|
+
|
|
22
|
+
import 'package:flutter/foundation.dart';
|
|
23
|
+
import 'package:flutter/gestures.dart';
|
|
24
|
+
import 'package:flutter/widgets.dart';
|
|
25
|
+
import 'package:flutter_fjs/flutter_fjs.dart';
|
|
26
|
+
import 'package:webview_flutter/webview_flutter.dart';
|
|
27
|
+
|
|
28
|
+
/// The module's short name: what `/modules/<name>/…` and the asset path use.
|
|
29
|
+
/// Mirrors WEB_VIEW_MODULE in ../../index.ts.
|
|
30
|
+
const String fjsWebViewModule = 'webview';
|
|
31
|
+
|
|
32
|
+
/// Stable failure text — mirrors WEB_VIEW_ERROR in ../../index.ts. The
|
|
33
|
+
/// platform's own wording stays in the platform's log: an error string that
|
|
34
|
+
/// changes per platform is not a contract.
|
|
35
|
+
const String fjsWebViewErrorMessage = 'web-view load failed';
|
|
36
|
+
|
|
37
|
+
/// The platform view must win the pointer sequence that starts inside its
|
|
38
|
+
/// rectangle. With the plugin's default empty set, an enclosing
|
|
39
|
+
/// SingleChildScrollView can claim the vertical drag before the WebView gets
|
|
40
|
+
/// it, leaving the page unable to scroll when <web-view> is nested in
|
|
41
|
+
/// <scroll-view>.
|
|
42
|
+
final Set<Factory<OneSequenceGestureRecognizer>> _fjsWebViewGestures = {
|
|
43
|
+
Factory<EagerGestureRecognizer>(EagerGestureRecognizer.new),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/// `@load` / `@error` / `@message` payloads. Field order is part of the
|
|
47
|
+
/// contract (../../index.ts writes the same three).
|
|
48
|
+
String fjsWebViewLoadPayload(String src) => '{"src":${_json(src)}}';
|
|
49
|
+
|
|
50
|
+
String fjsWebViewErrorPayload(String src) =>
|
|
51
|
+
'{"src":${_json(src)},"errMsg":${_json(fjsWebViewErrorMessage)}}';
|
|
52
|
+
|
|
53
|
+
String fjsWebViewMessagePayload(String data) => '{"data":${_json(data)}}';
|
|
54
|
+
|
|
55
|
+
String _json(String value) {
|
|
56
|
+
final out = StringBuffer('"');
|
|
57
|
+
for (final rune in value.runes) {
|
|
58
|
+
switch (rune) {
|
|
59
|
+
case 0x22:
|
|
60
|
+
out.write(r'\"');
|
|
61
|
+
case 0x5C:
|
|
62
|
+
out.write(r'\\');
|
|
63
|
+
case 0x0A:
|
|
64
|
+
out.write(r'\n');
|
|
65
|
+
case 0x0D:
|
|
66
|
+
out.write(r'\r');
|
|
67
|
+
case 0x09:
|
|
68
|
+
out.write(r'\t');
|
|
69
|
+
default:
|
|
70
|
+
if (rune < 0x20) {
|
|
71
|
+
out.write('\\u${rune.toRadixString(16).padLeft(4, '0')}');
|
|
72
|
+
} else {
|
|
73
|
+
out.writeCharCode(rune);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
out.write('"');
|
|
78
|
+
return out.toString();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
enum FjsWebViewSrcKind { empty, http, asset, local, unsupported }
|
|
82
|
+
|
|
83
|
+
/// Where the app's own local files land in a release build — the one
|
|
84
|
+
/// directory the CLI syncs `public/` and `html/` into. Mirrors
|
|
85
|
+
/// APP_ASSET_BASE in ../../index.ts (specs/017-local-image-assets).
|
|
86
|
+
const String fjsAppAssetBase = 'assets/fjs/public';
|
|
87
|
+
|
|
88
|
+
/// What a `src` is, before anyone tries to load it. Mirrors classifySrc in
|
|
89
|
+
/// ../../index.ts.
|
|
90
|
+
FjsWebViewSrcKind fjsClassifyWebViewSrc(Object? raw) {
|
|
91
|
+
final src = (raw?.toString() ?? '').trim();
|
|
92
|
+
if (src.isEmpty) return FjsWebViewSrcKind.empty;
|
|
93
|
+
if (src.startsWith('http://') || src.startsWith('https://')) {
|
|
94
|
+
return FjsWebViewSrcKind.http;
|
|
95
|
+
}
|
|
96
|
+
if (src.startsWith('asset://')) return FjsWebViewSrcKind.asset;
|
|
97
|
+
// A root path is the app's own page, from the project's html/ directory
|
|
98
|
+
// (specs/018-src-hints-and-html-dir). `asset://` stays what it always was:
|
|
99
|
+
// a file THIS MODULE ships, resolved under modules/webview/.
|
|
100
|
+
if (src.startsWith('/')) return FjsWebViewSrcKind.local;
|
|
101
|
+
return FjsWebViewSrcKind.unsupported;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// The path part of a root-absolute src, or null when it escapes the app's
|
|
105
|
+
/// own files. Mirrors localPath in ../../index.ts.
|
|
106
|
+
String? fjsWebViewLocalPath(String raw) {
|
|
107
|
+
var path = raw;
|
|
108
|
+
while (path.startsWith('/')) {
|
|
109
|
+
path = path.substring(1);
|
|
110
|
+
}
|
|
111
|
+
if (path.isEmpty || path.contains('..')) return null;
|
|
112
|
+
return path;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/// The path inside the module's own directory, or null when it escapes it.
|
|
116
|
+
String? fjsWebViewAssetPath(String raw) {
|
|
117
|
+
var path = raw.substring('asset://'.length);
|
|
118
|
+
while (path.startsWith('/')) {
|
|
119
|
+
path = path.substring(1);
|
|
120
|
+
}
|
|
121
|
+
if (path.isEmpty || path.contains('..')) return null;
|
|
122
|
+
return path;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Where a resolved `src` lives. A Flutter asset is a separate shape because
|
|
126
|
+
/// it is loaded with `loadFlutterAsset`, not as a URL.
|
|
127
|
+
class FjsWebViewTarget {
|
|
128
|
+
const FjsWebViewTarget.url(this.url)
|
|
129
|
+
: asset = null,
|
|
130
|
+
suffix = '';
|
|
131
|
+
const FjsWebViewTarget.asset(this.asset, {this.suffix = ''}) : url = null;
|
|
132
|
+
const FjsWebViewTarget.none()
|
|
133
|
+
: url = null,
|
|
134
|
+
asset = null,
|
|
135
|
+
suffix = '';
|
|
136
|
+
|
|
137
|
+
final String? url;
|
|
138
|
+
final String? asset;
|
|
139
|
+
|
|
140
|
+
/// Query and fragment to append after the platform resolves the asset key
|
|
141
|
+
/// to its real local file URL. The key and document URL are separate:
|
|
142
|
+
/// only the former is used by the Flutter asset manifest.
|
|
143
|
+
final String suffix;
|
|
144
|
+
|
|
145
|
+
bool get isNothing => url == null && asset == null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/// Everything before `?` / `#`. A Flutter asset key is looked up in the
|
|
149
|
+
/// bundle manifest, so it cannot carry either — passing one through makes
|
|
150
|
+
/// loadFlutterAsset throw FWFURLParsingError.
|
|
151
|
+
String fjsWebViewStripQuery(String path) {
|
|
152
|
+
final cut = path.indexOf(RegExp(r'[?#]'));
|
|
153
|
+
return cut < 0 ? path : path.substring(0, cut);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/// The part that belongs to the document URL rather than the asset key.
|
|
157
|
+
String fjsWebViewAssetSuffix(String path) {
|
|
158
|
+
final cut = path.indexOf(RegExp(r'[?#]'));
|
|
159
|
+
return cut < 0 ? '' : path.substring(cut);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Mirrors resolveSrc in ../../index.ts for the two app cases: with a dev
|
|
163
|
+
/// connection an `asset://` is served by `fjs dev`, otherwise it is a
|
|
164
|
+
/// Flutter asset the build copied in.
|
|
165
|
+
FjsWebViewTarget fjsResolveWebViewSrc(Object? raw, {Uri? devUri}) {
|
|
166
|
+
final src = (raw?.toString() ?? '').trim();
|
|
167
|
+
switch (fjsClassifyWebViewSrc(src)) {
|
|
168
|
+
case FjsWebViewSrcKind.http:
|
|
169
|
+
return FjsWebViewTarget.url(src);
|
|
170
|
+
case FjsWebViewSrcKind.local:
|
|
171
|
+
final path = fjsWebViewLocalPath(src);
|
|
172
|
+
if (path == null) return const FjsWebViewTarget.none();
|
|
173
|
+
if (devUri != null) {
|
|
174
|
+
final base = devUri.toString().replaceAll(RegExp(r'/+$'), '');
|
|
175
|
+
return FjsWebViewTarget.url('$base/$path');
|
|
176
|
+
}
|
|
177
|
+
return FjsWebViewTarget.asset(
|
|
178
|
+
'$fjsAppAssetBase/${fjsWebViewStripQuery(path)}',
|
|
179
|
+
suffix: fjsWebViewAssetSuffix(path),
|
|
180
|
+
);
|
|
181
|
+
case FjsWebViewSrcKind.asset:
|
|
182
|
+
final path = fjsWebViewAssetPath(src);
|
|
183
|
+
if (path == null) return const FjsWebViewTarget.none();
|
|
184
|
+
if (devUri != null) {
|
|
185
|
+
final base = devUri.toString().replaceAll(RegExp(r'/+$'), '');
|
|
186
|
+
return FjsWebViewTarget.url('$base/modules/$fjsWebViewModule/$path');
|
|
187
|
+
}
|
|
188
|
+
final key = fjsWebViewStripQuery(path);
|
|
189
|
+
return FjsWebViewTarget.asset(
|
|
190
|
+
'assets/fjs/modules/$fjsWebViewModule/$key',
|
|
191
|
+
suffix: fjsWebViewAssetSuffix(path),
|
|
192
|
+
);
|
|
193
|
+
case FjsWebViewSrcKind.empty:
|
|
194
|
+
case FjsWebViewSrcKind.unsupported:
|
|
195
|
+
return const FjsWebViewTarget.none();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// One terminal event per load, and never the previous page's — the Dart
|
|
200
|
+
/// half of LoadCycle in ../../index.ts.
|
|
201
|
+
///
|
|
202
|
+
/// It is a class of its own so it can be tested without a WebViewController:
|
|
203
|
+
/// building one needs a platform implementation, which a widget test does
|
|
204
|
+
/// not have. What a test here cannot prove is that the NavigationDelegate is
|
|
205
|
+
/// wired to it; that is what the simulator pass is for.
|
|
206
|
+
class FjsWebViewLoadCycle {
|
|
207
|
+
int _generation = 0;
|
|
208
|
+
bool _settled = false;
|
|
209
|
+
|
|
210
|
+
int get current => _generation;
|
|
211
|
+
|
|
212
|
+
int begin() {
|
|
213
|
+
_generation += 1;
|
|
214
|
+
_settled = false;
|
|
215
|
+
return _generation;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
bool finish(int generation) {
|
|
219
|
+
if (generation != _generation || _settled) return false;
|
|
220
|
+
_settled = true;
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
bool accepts(int generation) => generation == _generation;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/// Reattaches an asset src's query and fragment to the local URL that
|
|
228
|
+
/// `loadFlutterAsset` resolved. The redirect must happen before the document
|
|
229
|
+
/// executes, otherwise its first script can observe the wrong location.
|
|
230
|
+
class FjsWebViewAssetNavigation {
|
|
231
|
+
FjsWebViewAssetNavigation(this.suffix);
|
|
232
|
+
|
|
233
|
+
final String suffix;
|
|
234
|
+
bool _redirected = false;
|
|
235
|
+
bool _finished = false;
|
|
236
|
+
|
|
237
|
+
String? redirect(String platformUrl) {
|
|
238
|
+
if (_redirected || suffix.isEmpty) return null;
|
|
239
|
+
if (platformUrl.contains(RegExp(r'[?#]'))) return null;
|
|
240
|
+
_redirected = true;
|
|
241
|
+
return '$platformUrl$suffix';
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
bool accepts(String platformUrl) =>
|
|
245
|
+
suffix.isEmpty || platformUrl.endsWith(suffix);
|
|
246
|
+
|
|
247
|
+
bool shouldPreventBaseNavigation(String platformUrl) =>
|
|
248
|
+
!_finished &&
|
|
249
|
+
_redirected &&
|
|
250
|
+
suffix.isNotEmpty &&
|
|
251
|
+
!accepts(platformUrl) &&
|
|
252
|
+
!platformUrl.contains(RegExp(r'[?#]'));
|
|
253
|
+
|
|
254
|
+
void markFinished(String platformUrl) {
|
|
255
|
+
if (accepts(platformUrl)) _finished = true;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// Whether a web-view can fill the box it was given.
|
|
260
|
+
///
|
|
261
|
+
/// A web page has no intrinsic height, so an unbounded main axis has no
|
|
262
|
+
/// answer — and a guessed one would give every page a number nobody asked
|
|
263
|
+
/// for. Pure so the rule can be tested; the caller warns (constitution V).
|
|
264
|
+
bool fjsWebViewFitsBox(BoxConstraints constraints) =>
|
|
265
|
+
constraints.hasBoundedHeight;
|
|
266
|
+
|
|
267
|
+
final Set<String> _warned = <String>{};
|
|
268
|
+
|
|
269
|
+
/// The core's fjsWarnOnce is not exported, so the module keeps its own —
|
|
270
|
+
/// same channel and same prefix, so a page author cannot tell them apart.
|
|
271
|
+
void fjsWebViewWarnOnce(String key, String message) {
|
|
272
|
+
if (!_warned.add(key)) return;
|
|
273
|
+
debugPrint('[fjs] $message');
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
@visibleForTesting
|
|
277
|
+
void resetFjsWebViewWarnings() => _warned.clear();
|
|
278
|
+
|
|
279
|
+
class FjsWebview {
|
|
280
|
+
static FjsEngine? _engine;
|
|
281
|
+
|
|
282
|
+
/// Registers <web-view /> on the engine. `registry/component.dart` calls
|
|
283
|
+
/// itself the extension point for platform views, and this is one.
|
|
284
|
+
static void register(FjsEngine engine) {
|
|
285
|
+
_engine = engine;
|
|
286
|
+
engine.components.register('web-view', _build);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
static final ComponentBuilder _build =
|
|
290
|
+
(context, node, children, dispatch) => FjsWebViewWidget(
|
|
291
|
+
key: ValueKey<int>(node.id),
|
|
292
|
+
node: node,
|
|
293
|
+
dispatch: dispatch,
|
|
294
|
+
devUri: _engine?.devUri,
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
class FjsWebViewWidget extends StatefulWidget {
|
|
299
|
+
const FjsWebViewWidget({
|
|
300
|
+
super.key,
|
|
301
|
+
required this.node,
|
|
302
|
+
required this.dispatch,
|
|
303
|
+
this.devUri,
|
|
304
|
+
@visibleForTesting this.controllerOverride,
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
final MirrorNode node;
|
|
308
|
+
final void Function(int nodeId, int eventType, {String? text}) dispatch;
|
|
309
|
+
final Uri? devUri;
|
|
310
|
+
final WebViewController? controllerOverride;
|
|
311
|
+
|
|
312
|
+
@override
|
|
313
|
+
State<FjsWebViewWidget> createState() => _FjsWebViewWidgetState();
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
class _FjsWebViewWidgetState extends State<FjsWebViewWidget> {
|
|
317
|
+
WebViewController? _controller;
|
|
318
|
+
|
|
319
|
+
/// Which load the results arriving now belong to. A `src` change bumps it,
|
|
320
|
+
/// so the previous page's onPageFinished, error and messages are dropped
|
|
321
|
+
/// instead of being reported against the new URL — the same generation
|
|
322
|
+
/// trick widgets/image.dart uses.
|
|
323
|
+
final FjsWebViewLoadCycle _cycle = FjsWebViewLoadCycle();
|
|
324
|
+
String _src = '';
|
|
325
|
+
String _loaded = '';
|
|
326
|
+
|
|
327
|
+
@override
|
|
328
|
+
void initState() {
|
|
329
|
+
super.initState();
|
|
330
|
+
_src = widget.node.props['src']?.toString() ?? '';
|
|
331
|
+
_configure();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
@override
|
|
335
|
+
void didUpdateWidget(covariant FjsWebViewWidget oldWidget) {
|
|
336
|
+
super.didUpdateWidget(oldWidget);
|
|
337
|
+
final next = widget.node.props['src']?.toString() ?? '';
|
|
338
|
+
if (next == _src) return;
|
|
339
|
+
_src = next;
|
|
340
|
+
_configure();
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
void _configure() {
|
|
344
|
+
final kind = fjsClassifyWebViewSrc(_src);
|
|
345
|
+
if (kind == FjsWebViewSrcKind.unsupported) {
|
|
346
|
+
fjsWebViewWarnOnce(
|
|
347
|
+
'web-view-src:$_src',
|
|
348
|
+
'<web-view> will not load "$_src": only http(s):// and asset:// '
|
|
349
|
+
'(a file this module ships) are supported. Other schemes behave '
|
|
350
|
+
'too differently between WKWebView and the browser to promise.',
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
final target = fjsResolveWebViewSrc(_src, devUri: widget.devUri);
|
|
354
|
+
if (target.isNothing) {
|
|
355
|
+
// Nothing to show: no controller, no request, no events.
|
|
356
|
+
setState(() {
|
|
357
|
+
_controller = null;
|
|
358
|
+
_loaded = '';
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
final generation = _cycle.begin();
|
|
363
|
+
final assetNavigation =
|
|
364
|
+
target.asset == null ? null : FjsWebViewAssetNavigation(target.suffix);
|
|
365
|
+
_loaded = target.url ?? 'asset://${_src.substring('asset://'.length)}';
|
|
366
|
+
final controller = widget.controllerOverride ?? WebViewController();
|
|
367
|
+
controller
|
|
368
|
+
..setJavaScriptMode(JavaScriptMode.unrestricted)
|
|
369
|
+
..setNavigationDelegate(
|
|
370
|
+
NavigationDelegate(
|
|
371
|
+
onNavigationRequest: (request) {
|
|
372
|
+
final navigation = assetNavigation;
|
|
373
|
+
if (navigation == null) return NavigationDecision.navigate;
|
|
374
|
+
if (navigation.shouldPreventBaseNavigation(request.url)) {
|
|
375
|
+
return NavigationDecision.prevent;
|
|
376
|
+
}
|
|
377
|
+
final redirect = navigation.redirect(request.url);
|
|
378
|
+
if (redirect == null) return NavigationDecision.navigate;
|
|
379
|
+
// loadFlutterAsset gives us the platform's real local file URL.
|
|
380
|
+
// Reusing it preserves relative resources on both platforms,
|
|
381
|
+
// while this second navigation supplies the page's parameters
|
|
382
|
+
// before its own scripts run.
|
|
383
|
+
unawaited(controller.loadRequest(Uri.parse(redirect)));
|
|
384
|
+
return NavigationDecision.prevent;
|
|
385
|
+
},
|
|
386
|
+
onPageStarted: (url) {
|
|
387
|
+
// Some platform implementations do not ask for a navigation
|
|
388
|
+
// decision for the initial loadFlutterAsset request. This early
|
|
389
|
+
// callback is the fallback; it still runs before page scripts.
|
|
390
|
+
final navigation = assetNavigation;
|
|
391
|
+
final redirect = navigation?.redirect(url);
|
|
392
|
+
if (redirect != null) {
|
|
393
|
+
unawaited(controller.loadRequest(Uri.parse(redirect)));
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
onPageFinished: (url) {
|
|
397
|
+
final navigation = assetNavigation;
|
|
398
|
+
if (navigation != null && !navigation.accepts(url)) return;
|
|
399
|
+
navigation?.markFinished(url);
|
|
400
|
+
_settle(generation, error: false);
|
|
401
|
+
},
|
|
402
|
+
// Only the main document. A page whose favicon 404s has loaded.
|
|
403
|
+
onWebResourceError: (error) {
|
|
404
|
+
if (error.isForMainFrame == false) return;
|
|
405
|
+
_settle(generation, error: true);
|
|
406
|
+
},
|
|
407
|
+
),
|
|
408
|
+
)
|
|
409
|
+
..addJavaScriptChannel(
|
|
410
|
+
// The name the loaded page calls: fjs.postMessage('…'). The web
|
|
411
|
+
// stand-in cannot inject this, so a page brings a shim; see
|
|
412
|
+
// public/demo.html.
|
|
413
|
+
'fjs',
|
|
414
|
+
onMessageReceived: (message) {
|
|
415
|
+
if (!_cycle.accepts(generation) || !mounted) return;
|
|
416
|
+
widget.dispatch(
|
|
417
|
+
widget.node.id,
|
|
418
|
+
FjsEvent.message,
|
|
419
|
+
text: fjsWebViewMessagePayload(message.message),
|
|
420
|
+
);
|
|
421
|
+
},
|
|
422
|
+
);
|
|
423
|
+
if (target.asset != null) {
|
|
424
|
+
unawaited(controller.loadFlutterAsset(target.asset!));
|
|
425
|
+
} else {
|
|
426
|
+
unawaited(controller.loadRequest(Uri.parse(target.url!)));
|
|
427
|
+
}
|
|
428
|
+
setState(() => _controller = controller);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
void _settle(int generation, {required bool error}) {
|
|
432
|
+
if (!mounted || !_cycle.finish(generation)) return;
|
|
433
|
+
widget.dispatch(
|
|
434
|
+
widget.node.id,
|
|
435
|
+
error ? FjsEvent.error : FjsEvent.load,
|
|
436
|
+
text: error
|
|
437
|
+
? fjsWebViewErrorPayload(_loaded)
|
|
438
|
+
: fjsWebViewLoadPayload(_loaded),
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
@override
|
|
443
|
+
Widget build(BuildContext context) {
|
|
444
|
+
final controller = _controller;
|
|
445
|
+
if (controller == null) return const SizedBox.shrink();
|
|
446
|
+
return LayoutBuilder(
|
|
447
|
+
builder: (context, constraints) {
|
|
448
|
+
// A WebView has no intrinsic height, so an unbounded main axis has
|
|
449
|
+
// no answer — guessing one would give every page a number nobody
|
|
450
|
+
// asked for. Say so and render nothing (constitution V).
|
|
451
|
+
if (!fjsWebViewFitsBox(constraints)) {
|
|
452
|
+
fjsWebViewWarnOnce(
|
|
453
|
+
'web-view-unbounded:${widget.node.id}',
|
|
454
|
+
'<web-view> node ${widget.node.id} has no height to fill: give '
|
|
455
|
+
'it a height, or a flex-grow, or put it in a box that has '
|
|
456
|
+
'one. A web page has no natural height to fall back on.',
|
|
457
|
+
);
|
|
458
|
+
return const SizedBox.shrink();
|
|
459
|
+
}
|
|
460
|
+
// This only claims pointers hit inside the platform view. A drag that
|
|
461
|
+
// starts on a sibling remains available to the enclosing scroll-view.
|
|
462
|
+
// We intentionally do not hand a drag to the parent when the page
|
|
463
|
+
// reaches its own edge; that requires platform-specific nested-scroll
|
|
464
|
+
// callbacks and is outside this module's cross-platform contract.
|
|
465
|
+
return WebViewWidget(
|
|
466
|
+
controller: controller,
|
|
467
|
+
gestureRecognizers: _fjsWebViewGestures,
|
|
468
|
+
);
|
|
469
|
+
},
|
|
470
|
+
);
|
|
471
|
+
}
|
|
472
|
+
}
|