feat(android): bundle GeckoView and bump version to 1.0.2
This commit is contained in:
+49
-164
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user