feat(android): bundle GeckoView and bump version to 1.0.2

This commit is contained in:
2026-10-18 19:19:58 +08:00
parent 867989e09b
commit 7f493e0f89
10 changed files with 532 additions and 408 deletions
+49 -164
View File
@@ -1,10 +1,7 @@
import 'dart:async';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'config.dart';
@@ -53,176 +50,53 @@ class HomePage extends StatefulWidget {
}
class _HomePageState extends State<HomePage> {
/// 原生下载通道(MainActivity 中用系统 DownloadManager 实现)
static const MethodChannel _downloadsChannel = MethodChannel(
'dsh.workbench/downloads',
);
static final Uri _siteUri = Uri.parse(kSiteUrl);
late final WebViewController _controller;
MethodChannel? _channel;
bool _loading = true;
String? _loadError;
@override
void initState() {
super.initState();
_controller = _buildController();
}
WebViewController _buildController() {
final controller = WebViewController()
..setJavaScriptMode(JavaScriptMode.unrestricted)
..setBackgroundColor(const Color(kThemeColorValue))
..setNavigationDelegate(
NavigationDelegate(
onPageStarted: (_) {
if (mounted) {
setState(() {
_loading = true;
_loadError = null;
});
}
},
onPageFinished: (_) {
if (mounted) setState(() => _loading = false);
},
onWebResourceError: (error) {
// 只把主文档加载失败视作页面错误;子资源错误(图片等)
// 以及被新导航打断(ERR_ABORTED)的情况交给页面自行处理
if (!(error.isForMainFrame ?? false)) return;
if (error.description.contains('ERR_ABORTED')) return;
if (mounted) {
setState(() {
_loading = false;
_loadError = error.description;
});
}
},
onNavigationRequest: _onNavigationRequest,
),
)
..loadRequest(Uri.parse(kSiteUrl));
// Android 专属能力:文件选择器(上传)、原生 JS 对话框(alert/confirm)
final platform = controller.platform;
if (platform is AndroidWebViewController) {
platform
..setOnShowFileSelector(_onShowFileSelector)
..setOnJavaScriptAlertDialog(_onJavaScriptAlert)
..setOnJavaScriptConfirmDialog(_onJavaScriptConfirm);
}
return controller;
}
/// 面板用 window.location.assign 触发下载(文件/目录 ZIP/备份包),
/// 这里拦下来交给原生 DownloadManager(携带登录 Cookie),WebView 停留原页。
NavigationDecision _onNavigationRequest(NavigationRequest request) {
final uri = Uri.tryParse(request.url);
if (uri != null && _isPanelDownload(uri)) {
_startDownload(uri);
return NavigationDecision.prevent;
}
return NavigationDecision.navigate;
}
bool _isPanelDownload(Uri uri) {
if (uri.host != _siteUri.host) return false;
// 工作区文件/目录 ZIP 下载
if (uri.path.endsWith('/api/ws/download')) return true;
// 配置备份包下载
if (uri.path.endsWith('/backups') &&
uri.queryParameters.containsKey('download')) {
return true;
}
return false;
}
Future<void> _startDownload(Uri uri) async {
try {
await _downloadsChannel.invokeMethod<bool>('download', {
'url': uri.toString(),
});
} on PlatformException catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('下载失败:${e.message ?? '未知错误'}')),
);
Future<void> _onViewCreated(int id) async {
final channel = MethodChannel('dsh.workbench/gecko/$id');
_channel = channel;
channel.setMethodCallHandler((call) async {
if (!mounted) return;
switch (call.method) {
case 'onPageStarted':
setState(() {
_loading = true;
_loadError = null;
});
case 'onPageFinished':
setState(() => _loading = false);
case 'onLoadError':
setState(() {
_loading = false;
_loadError = call.arguments as String? ?? '页面加载失败';
});
case 'onDownloadError':
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('下载失败:${call.arguments}')));
}
}
});
await _invoke('load');
}
/// WebView 里的 <input type="file"> 上传(工作区上传/备份导入)。
/// Android 的类型过滤一次只支持单一 MIME 或单一扩展名,
/// 多约束时退化为不限类型,避免选择器异常。
Future<List<String>> _onShowFileSelector(FileSelectorParams params) async {
final typeGroups = <XTypeGroup>[];
final mimes = params.acceptTypes.where((t) => t.contains('/')).toList();
if (mimes.length == 1) {
typeGroups.add(XTypeGroup(mimeTypes: mimes));
} else {
final exts = params.acceptTypes
.where((t) => t.startsWith('.'))
.map((t) => t.toLowerCase())
.toList();
if (exts.length == 1) typeGroups.add(XTypeGroup(extensions: exts));
Future<T?> _invoke<T>(String method) async {
try {
return await _channel?.invokeMethod<T>(method);
} on PlatformException catch (error) {
if (mounted) {
setState(() {
_loading = false;
_loadError = error.message ?? '浏览器启动失败';
});
}
return null;
}
if (params.mode == FileSelectorMode.openMultiple) {
final files = await openFiles(acceptedTypeGroups: typeGroups);
return files.map((f) => f.path).toList();
}
final file = await openFile(acceptedTypeGroups: typeGroups);
return file == null ? const <String>[] : [file.path];
}
/// 面板少量路径仍使用 window.alert / window.confirm,
/// 未注册回调时 WebView 会静默返回 false,必须在这里补上。
Future<void> _onJavaScriptAlert(JavaScriptAlertDialogRequest request) async {
if (!mounted) return;
await showDialog<void>(
context: context,
builder: (context) => AlertDialog(
content: Text(request.message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('确定'),
),
],
),
);
}
Future<bool> _onJavaScriptConfirm(
JavaScriptConfirmDialogRequest request,
) async {
if (!mounted) return false;
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
content: Text(request.message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('取消'),
),
FilledButton(
onPressed: () => Navigator.of(context).pop(true),
child: const Text('确定'),
),
],
),
);
return confirmed ?? false;
}
Future<void> _onPopInvoked(bool didPop, Object? result) async {
if (didPop) return;
// 先回退 WebView 历史(SPA 内部导航),没有历史再退出应用
if (await _controller.canGoBack()) {
await _controller.goBack();
} else {
if (await _invoke<bool>('goBack') == false && mounted) {
await SystemNavigator.pop();
}
}
@@ -232,7 +106,13 @@ class _HomePageState extends State<HomePage> {
_loadError = null;
_loading = true;
});
unawaited(_controller.reload());
unawaited(_invoke<void>('reload'));
}
@override
void dispose() {
_channel?.setMethodCallHandler(null);
super.dispose();
}
@override
@@ -245,7 +125,12 @@ class _HomePageState extends State<HomePage> {
body: SafeArea(
child: Stack(
children: [
WebViewWidget(controller: _controller),
AndroidView(
viewType: 'dsh.workbench/gecko',
creationParams: const {'url': kSiteUrl},
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated: _onViewCreated,
),
if (_loadError != null)
_ErrorView(message: _loadError!, onRetry: _retry)
else if (_loading)