Files
record-tool/lib/shared/widgets/app_webview.dart
T

309 lines
9.5 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:recording_tool/app/router/app_navigator.dart';
import 'package:recording_tool/core/cache/app_storage.dart';
import 'package:recording_tool/core/cache/storage_keys.dart';
import 'package:recording_tool/features/events/model/model_event_info.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart';
class WebviewPage extends StatefulWidget {
final String? url;
final String? title;
final String? assetPath;
final bool? canPop;
final EventRegistrationItem? eventRegistrationItem;
final String? playerId;
const WebviewPage({
super.key,
this.url,
this.title,
this.assetPath,
this.canPop,
this.eventRegistrationItem,
this.playerId,
});
@override
State<WebviewPage> createState() => WebviewPageState();
}
class WebviewPageState extends State<WebviewPage> {
static const String _javaScriptChannelName = 'AppBridge';
WebViewController? _controller;
bool _isLoading = true;
String? _errorMessage;
@override
void initState() {
super.initState();
_initController();
}
@override
void dispose() {
final controller = _controller;
_controller = null;
if (controller != null) {
unawaited(_disposeWebViewController(controller));
}
super.dispose();
}
Future<void> _initController() async {
if ((widget.url?.isEmpty ?? true) && widget.assetPath == null) {
return;
}
final params = _createPlatformParams();
late final WebViewController controller;
controller = WebViewController.fromPlatformCreationParams(params)
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(Colors.transparent)
..setNavigationDelegate(
NavigationDelegate(
onProgress: (int progress) {
debugPrint('webview - progress: $progress');
},
onPageStarted: (String url) {
debugPrint('webview - page started: $url');
if (!mounted) return;
setState(() {
_isLoading = true;
_errorMessage = null;
});
},
onPageFinished: (String url) async {
debugPrint('webview - page finished: $url');
// await _injectPostMessageBridge(controller);
await _injectH5NeedData(controller);
if (!mounted) return;
setState(() => _isLoading = false);
},
onHttpError: (HttpResponseError error) {
debugPrint(
'webview - http error: ${error.response?.uri}, code: ${error.response?.statusCode}',
);
},
onWebResourceError: (WebResourceError error) {
debugPrint(
'webview error: ${error.description}, code: ${error.errorCode}, url: ${error.url}',
);
if (!mounted || error.isForMainFrame != true) return;
setState(() {
_isLoading = false;
_errorMessage = error.description;
});
},
onNavigationRequest: (NavigationRequest request) {
return NavigationDecision.navigate;
},
),
);
controller.addJavaScriptChannel(
_javaScriptChannelName,
onMessageReceived: (JavaScriptMessage message) {
_onMessageReceived(message);
},
);
if (controller.platform is AndroidWebViewController) {
AndroidWebViewController.enableDebugging(kDebugMode);
await (controller.platform as AndroidWebViewController)
.setMediaPlaybackRequiresUserGesture(false);
}
try {
if (widget.assetPath != null) {
await controller.loadFlutterAsset(widget.assetPath!);
} else {
final targetUrl = widget.url ?? '';
await controller.loadRequest(Uri.parse(targetUrl));
}
} catch (e) {
debugPrint('webview - load failed: $e');
if (!mounted) return;
setState(() {
_isLoading = false;
_errorMessage = e.toString();
});
return;
}
if (!mounted) {
unawaited(_disposeWebViewController(controller));
return;
}
setState(() => _controller = controller);
}
Future<void> _disposeWebViewController(WebViewController controller) async {
await _runWebViewCleanupStep(
'reset navigation delegate',
() => controller.setNavigationDelegate(NavigationDelegate()),
);
await _runWebViewCleanupStep(
'remove javascript channel',
() => controller.removeJavaScriptChannel(_javaScriptChannelName),
);
await _runWebViewCleanupStep(
'load blank page',
() => controller.loadRequest(Uri.parse('about:blank')),
);
await _runWebViewCleanupStep('clear cache', controller.clearCache);
await _runWebViewCleanupStep(
'clear local storage',
controller.clearLocalStorage,
);
await _runWebViewCleanupStep(
'clear cookies',
() => WebViewCookieManager().clearCookies(),
);
}
Future<void> _runWebViewCleanupStep(
String step,
Future<void> Function() cleanup,
) async {
try {
debugPrint('webview - dispose cleanup at $step ');
await cleanup();
} catch (e) {
debugPrint('webview - dispose cleanup failed at $step: $e');
}
}
void _onMessageReceived(JavaScriptMessage message) {
debugPrint('收到来自 WebView 的消息: ${message.message}');
final jsonMap = json.decode(message.message);
if (jsonMap['type'] != null && jsonMap['type'] == 'navigator_pop') {
Navigator.of(context).maybePop();
}
}
/// 脚本注入传输给h5 数据
Future<void> _injectH5NeedData(WebViewController controller) async {
final token = AppStorage.getString(StorageKeys.authToken);
try {
await controller.runJavaScript('''
(function () {
if (window.__appInstallData) {
return;
}
window.__appInstallData = ${true};
window.AppBridge.token = '$token';
window.AppBridge.scoreEntryContext = {
eventId: '${widget.eventRegistrationItem?.eventId}',
itemId: '${widget.eventRegistrationItem?.itemId}',
scheduleId: '${widget.eventRegistrationItem?.scheduleId}',
userId: '${widget.playerId}',
opponentId: '${widget.eventRegistrationItem?.opponentId}',
};
// console.log('window.AppBridge?.scoreEntryContext.eventId', window.AppBridge?.scoreEntryContext.eventId);
// console.log('window.AppBridge?.scoreEntryContext.itemId', window.AppBridge?.scoreEntryContext.itemId);
// console.log('window.AppBridge?.scoreEntryContext.scheduleId', window.AppBridge?.scoreEntryContext.scheduleId);
// console.log('window.AppBridge?.scoreEntryContext.userId', window.AppBridge?.scoreEntryContext.userId);
// console.log('window.AppBridge?.scoreEntryContext.opponentId', window.AppBridge?.scoreEntryContext.opponentId);
// confirm('已经注入完成');
})();
''');
} catch (e) {
debugPrint('webview - inject postMessage bridge failed: $e');
}
}
PlatformWebViewControllerCreationParams _createPlatformParams() {
if (WebViewPlatform.instance is WebKitWebViewPlatform) {
return WebKitWebViewControllerCreationParams(
allowsInlineMediaPlayback: true,
mediaTypesRequiringUserAction: const <PlaybackMediaTypes>{},
);
}
return const PlatformWebViewControllerCreationParams();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.transparent,
extendBodyBehindAppBar: true,
// appBar: myAppBar(context: context),
body: _buildBody(),
);
}
Widget _buildBody() {
if (_errorMessage != null) {
return Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Text(
'页面加载失败\n$_errorMessage',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white, fontSize: 14),
),
),
);
}
final controller = _controller;
if (controller == null) {
return const Center(child: CircularProgressIndicator());
}
return PopScope(
canPop: widget.canPop ?? true,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
final ok = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('提示'),
content: const Text('是否返回上一页?'),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('取消'),
),
TextButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('确定'),
),
],
),
);
if (ok == true) {
AppNavigator.pop();
}
},
child: Scaffold(
body: Column(
children: [
SizedBox(height: 48),
Expanded(
child: Stack(
children: [
Positioned.fill(child: WebViewWidget(controller: controller)),
if (_isLoading)
const Center(child: CircularProgressIndicator()),
],
),
),
],
),
),
);
}
}