appbridgenewplus 1.0.0-beta.1
appbridgenewplus: ^1.0.0-beta.1 copied to clipboard
A versatile Flutter plugin for integrating various platform-specific functionalities and bridging communication with web views. It provides modules for UI, navigation, live activities, events, downloa [...]
example/lib/main.dart
import 'dart:io';
import 'dart:ui';
import 'package:url_launcher/url_launcher.dart';
import 'package:appbridgenewplus/app_bridge_webview.dart';
import 'package:http/http.dart' as http;
import 'package:appbridgenewplus/webrtc_live/webrtc_config.dart';
import 'package:appbridgenewplus/appbridgenew.dart';
import 'package:appbridgenewplus/appbridgenew_platform_interface.dart';
import 'package:appbridgenewplus/src/ui/video_player_page.dart';
import 'package:appbridgenewplus/src/ui/novel_reader_page.dart';
import 'package:appbridgenewplus/src/ui/comics_reader_page.dart';
import 'package:appbridgenewplus/src/ui/post_reader_page.dart';
import 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'dart:isolate';
import 'downloader_io.dart';
import 'package:appbridgenewplus/event_service.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:appbridgenewplus/src/models/bridge_response.dart';
// 用于与下载器隔离区通信的接收端口
final ReceivePort port = ReceivePort();
// 应用程序入口
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// 强制锁定竖屏
await SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
]);
await FlutterDownloader.initialize(
debug: true,
);
FlutterDownloader.registerCallback(downloadCallback);
runApp(const MyApp());
}
// MyApp 是整个 Flutter 应用程序的根 widget。
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
// _MyAppState 类管理应用程序的状态和生命周期。
class _MyAppState extends State<MyApp> with WidgetsBindingObserver {
String _platformVersion = 'Unknown';
final _appbridgenewPlugin = Appbridgenew();
String? _demoHtmlContent;
String? _initialUrl;
DateTime? _lastPopTime;
String? _currentDownloadTaskId;
final Map<String, int> _retryAttempts = {};
String? _pendingInstallApkPath;
ThemeMode _themeMode = ThemeMode.system;
String _currentAppBarTitle = 'AppBridgeH5 SDK 演示';
String? _currentAppBarSubtitle;
bool _isAppBarVisible = true;
late Color _currentAppBarBackgroundColor;
late Color _currentAppBarForegroundColor;
late Brightness _currentAppBarStyle;
static const platform =
MethodChannel('com.example.appbridgenew_example/shortcut_channel');
// 根据主题模式获取AppBar的背景颜色。
Color _getAppBarBackgroundColor(ThemeMode mode) {
return mode == ThemeMode.dark ? Colors.grey[900]! : Colors.blue.shade700;
}
// 根据主题模式获取 AppBar 的前景颜色。
Color _getAppBarForegroundColor(ThemeMode mode) {
return mode == ThemeMode.dark ? Colors.white : Colors.white;
}
// 根据主题模式获取 AppBar 的亮度样式。
Brightness _getAppBarStyle(ThemeMode mode) {
final Color bgColor = _getAppBarBackgroundColor(mode);
return bgColor.computeLuminance() > 0.5
? Brightness.dark
: Brightness.light;
}
// 获取弹窗标题的文本样式。
TextStyle _getDialogTitleTextStyle(BuildContext context) {
final TextStyle? headlineSmall = Theme.of(context).textTheme.headlineSmall;
return headlineSmall?.copyWith(
fontSize: (headlineSmall.fontSize ?? 24.0) - 3.0) ??
const TextStyle(fontSize: 21.0, fontWeight: FontWeight.bold);
}
@override
// 初始化状态,注册生命周期观察者和平台方法调用处理器。
void initState() {
super.initState();
_preWarmSignalingServer(); // 启动即唤醒信令服务器
WidgetsBinding.instance.addObserver(this);
_currentAppBarBackgroundColor = _getAppBarBackgroundColor(_themeMode);
_currentAppBarForegroundColor = _getAppBarForegroundColor(_themeMode);
_currentAppBarStyle = _getAppBarStyle(_themeMode);
_requestPermissions();
WidgetsBinding.instance.addPostFrameCallback((_) {
_loadDemoHtmlAndInitPlatformState();
});
platform.setMethodCallHandler((call) async {
if (call.method == "receiveShortcutUrl") {
final String? url = call.arguments;
if (url != null && url.isNotEmpty) {
setState(() {
_initialUrl = url;
});
}
}
});
IsolateNameServer.registerPortWithName(
port.sendPort, 'downloader_send_port');
port.listen((dynamic data) async {
final Map<String, dynamic> downloadData = data as Map<String, dynamic>;
final String id = downloadData['id'] as String;
final int statusInt = downloadData['status'] as int;
int progress = downloadData['progress'] as int;
String? filePath;
String? errorMessage;
String formattedSpeed = "0.00KB/s";
int currentTotalBytes = 0;
int currentDownloadedBytes = 0;
final tasks = await FlutterDownloader.loadTasksWithRawQuery(
query: 'SELECT * FROM task WHERE task_id = "$id"');
if (tasks != null && tasks.isNotEmpty) {
final task = tasks.first;
filePath = '${task.savedDir}/${task.filename}';
} else {
errorMessage = 'Download task not found for id: $id';
}
if (_appbridgenewPlugin.downloadTaskTotalBytes[id] != null &&
_appbridgenewPlugin.downloadTaskTotalBytes[id]! > 0) {
currentTotalBytes = _appbridgenewPlugin.downloadTaskTotalBytes[id]!;
currentDownloadedBytes = (progress / 100 * currentTotalBytes).round();
final int currentTime = DateTime.now().millisecondsSinceEpoch;
final int currentBytes = currentDownloadedBytes;
final int? lastTime = _appbridgenewPlugin.lastProgressTime[id];
final int? lastBytes = _appbridgenewPlugin.lastProgressBytes[id];
if (lastTime != null && lastBytes != null) {
final timeDelta = currentTime - lastTime;
final bytesDelta = currentBytes - lastBytes;
if (timeDelta > 500 && bytesDelta >= 0) {
final double speedInBps = (bytesDelta * 1000) / timeDelta;
if (speedInBps >= 1024 * 1024) {
formattedSpeed =
'${(speedInBps / (1024 * 1024)).toStringAsFixed(2)} MB/s';
} else if (speedInBps >= 1024) {
formattedSpeed = '${(speedInBps / 1024).toStringAsFixed(2)} KB/s';
} else {
formattedSpeed = '${speedInBps.toStringAsFixed(2)} B/s';
}
_appbridgenewPlugin.lastProgressTime[id] = currentTime;
_appbridgenewPlugin.lastProgressBytes[id] = currentBytes;
}
} else {
_appbridgenewPlugin.lastProgressTime[id] = currentTime;
_appbridgenewPlugin.lastProgressBytes[id] = currentBytes;
}
}
final DownloadTaskStatus downloadStatus =
DownloadTaskStatus.values[statusInt];
String statusString = downloadStatus.toString().split('.').last;
if (downloadStatus == DownloadTaskStatus.failed && progress == -1) {
final int attempts = _retryAttempts[id] ?? 0;
if (attempts < 1) {
_retryAttempts[id] = attempts + 1;
if (filePath != null) {
final file = File(filePath);
if (await file.exists()) {
try {
await file.delete();
} catch (e) {}
}
}
if (tasks != null && tasks.isNotEmpty) {
final task = tasks.first;
final newTaskId = await FlutterDownloader.enqueue(
url: task.url,
savedDir: task.savedDir,
fileName: task.filename,
showNotification: true,
openFileFromNotification: true,
);
if (id == _currentDownloadTaskId) {
setState(() {
_currentDownloadTaskId = newTaskId;
});
}
}
return;
} else {}
}
if (downloadStatus == DownloadTaskStatus.failed &&
progress == -1 &&
filePath != null) {
final file = File(filePath);
if (await file.exists()) {
statusString = DownloadTaskStatus.complete.toString().split('.').last;
errorMessage = null;
progress = 100;
} else {
errorMessage =
'Download failed or cancelled for task: $id. Path: $filePath';
}
} else if (downloadStatus == DownloadTaskStatus.failed ||
downloadStatus == DownloadTaskStatus.canceled) {
errorMessage =
'Download failed or cancelled for task: $id. Path: $filePath';
}
final eventPayload = {
'id': id,
'status': statusString,
'progress': progress,
'path': filePath,
'error': errorMessage,
'speed': formattedSpeed,
'totalBytes': currentTotalBytes,
};
if (statusString ==
DownloadTaskStatus.complete.toString().split('.').last) {
_appbridgenewPlugin.emitEvent('download.completed', eventPayload);
} else if (statusString ==
DownloadTaskStatus.failed.toString().split('.').last) {
_appbridgenewPlugin.emitEvent('download.failed', eventPayload);
} else if (statusString ==
DownloadTaskStatus.canceled.toString().split('.').last) {
_appbridgenewPlugin.emitEvent('download.canceled', eventPayload);
} else {
_appbridgenewPlugin.emitEvent('download.progress', eventPayload);
}
});
}
Future<void> _requestPermissions() async {
print("DEBUG: _requestPermissions method started.");
// 请求相机权限
print("DEBUG: Requesting Camera permission. Current status: ${await Permission.camera.status}");
PermissionStatus cameraStatus = await Permission.camera.request();
if (cameraStatus.isGranted) {
print("DEBUG: Camera permission granted");
} else if (cameraStatus.isDenied) {
print("DEBUG: Camera permission denied (first time)");
// 可选:显示对话框解释为何需要权限
} else if (cameraStatus.isPermanentlyDenied) {
print("DEBUG: Camera permission permanently denied");
openAppSettings(); // 打开应用程序设置供用户手动启用
}
print("DEBUG: Camera permission final status: $cameraStatus");
// 请求麦克风权限
print("DEBUG: Requesting Microphone permission. Current status: ${await Permission.microphone.status}");
PermissionStatus microphoneStatus = await Permission.microphone.request();
if (microphoneStatus.isGranted) {
print("DEBUG: Microphone microphoneStatus granted");
} else if (microphoneStatus.isDenied) {
print("DEBUG: Microphone microphoneStatus denied (first time)");
} else if (microphoneStatus.isPermanentlyDenied) {
print("DEBUG: Microphone microphoneStatus permanently denied");
openAppSettings(); // 打开应用程序设置供用户手动启用
}
print("DEBUG: Microphone permission final status: $microphoneStatus");
// 请求蓝牙连接权限(用于直播模块)
print("DEBUG: Requesting BluetoothConnect permission. Current status: ${await Permission.bluetoothConnect.status}");
PermissionStatus bluetoothStatus = await Permission.bluetoothConnect.request();
if (bluetoothStatus.isGranted) {
print("DEBUG: BluetoothConnect permission granted");
} else if (bluetoothStatus.isDenied) {
print("DEBUG: BluetoothConnect permission denied");
} else if (bluetoothStatus.isPermanentlyDenied) {
print("DEBUG: BluetoothConnect permission permanently denied");
}
// --- 诊断:打印 Info.plist 条目 ---
try {
final PackageInfo packageInfo = await PackageInfo.fromPlatform();
print("APP_VERSION:${packageInfo.version}+${packageInfo.buildNumber}");
} catch (e) {
print("Error getting PackageInfo or Info.plist entries: $e");
}
print("DEBUG: _requestPermissions method finished.");
}
@override
// 销毁时移除生命周期观察者和端口映射。
void dispose() {
WidgetsBinding.instance.removeObserver(this);
IsolateNameServer.removePortNameMapping('downloader_send_port');
super.dispose();
}
// 加载 demo HTML 内容并初始化平台状态。
Future<void> _loadDemoHtmlAndInitPlatformState() async {
if (_initialUrl == null) {
try {
_demoHtmlContent = await rootBundle
.loadString('packages/appbridgenewplus/assets/demo.html');
} catch (e) {
_demoHtmlContent = '<h1>Error loading demo.html</h1><p>$e</p>';
}
}
String platformVersion;
try {
await _appbridgenewPlugin.initialize(
onNavOpen: (url, {title, animated, modal, inExternal}) async {
if (!mounted) return;
if (inExternal == true) {
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url),
mode: LaunchMode.externalApplication);
} else {}
} else {
Navigator.of(_appbridgenewPlugin.mainContext!).push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(title: Text(title ?? '新页面')),
body: AppBridgeWebView(
initialUrl: url,
initialHtmlContent: null,
onWebViewCreated: (controller) {
_appbridgenewPlugin.emitEvent('theme.change', {
'theme':
_themeMode == ThemeMode.dark ? 'dark' : 'light'
});
}),
),
fullscreenDialog: modal ?? false,
),
);
}
},
onNavClose: () {
EventService().addLog('EVENT: nav.close');
},
onNavReplace: (url, title) {
EventService().addLog('EVENT: nav.replace, URL: $url, Title: $title');
},
onAddShortcut: (title, url) async {
if (!mounted) return BridgeResponse.error(-1, 'Widget not mounted');
final result = await _appbridgenewPlugin.addShortcuts(title, url);
return BridgeResponse.success(result);
},
onAppIcon: (styleId) async {
if (!mounted) return BridgeResponse.error(-1, 'Widget not mounted');
final result = await _appbridgenewPlugin.appIcon(styleId: styleId);
return BridgeResponse.success(result);
},
onAppUpdateCheck: () async {
if (!mounted) return BridgeResponse.error(-1, 'Widget not mounted');
final currentContext = _appbridgenewPlugin.mainContext;
if (currentContext == null || !currentContext.mounted) {
return BridgeResponse.error(-1, 'No valid context.');
}
try {
const hasUpdate = true;
const version = '1.1.1';
const releaseNotes = '更新内容:\n修复了一些已知问题;\n优化了用户体验。';
const downloadUrl =
'https://apk.loapk.com/51dongman_android/yjdm_01_2.28.apk';
final userAction = await showDialog<String>(
context: currentContext,
barrierDismissible: false,
builder: (dialogContext) {
return AlertDialog(
title: const Text('发现新版本'),
content: const Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('版本: $version'),
SizedBox(height: 10),
Text(releaseNotes),
],
),
actions: [
TextButton(
onPressed: () =>
Navigator.of(dialogContext).pop('cancel'),
child: const Text('稍后再说'),
),
ElevatedButton(
onPressed: () =>
Navigator.of(dialogContext).pop('update'),
child: const Text('立即更新'),
),
],
);
},
);
if (userAction == 'update') {
return BridgeResponse.success({
'hasUpdate': hasUpdate,
'version': version,
'releaseNotes': releaseNotes,
'downloadUrl': downloadUrl,
'shouldUpdate': true,
});
}
return BridgeResponse.success(
{'hasUpdate': hasUpdate, 'shouldUpdate': false});
} catch (e) {
return BridgeResponse.error(-1, e.toString());
}
},
);
platformVersion = await _appbridgenewPlugin.getPlatformVersion() ??
'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
_currentAppBarTitle = 'AppBridgeH5 SDK 演示 ($_platformVersion)';
});
try {
final response = await _appbridgenewPlugin.callModuleMethod(
'core.has', {'path': 'core.getVersion'}); // 测试 core.getVersion 方法是否存在
final response2 = await _appbridgenewPlugin.callModuleMethod(
'core.has', {'path': 'non.existent.method'}); // 测试 一个不存在的方法
} catch (e) {}
_appbridgenewPlugin.initialize(
onNavSetTitle: (title, subtitle) {
if (mounted) {
setState(() {
_currentAppBarTitle = title;
_currentAppBarSubtitle = subtitle;
});
}
},
// 处理导航栏设置事件,控制导航栏的显示、颜色和样式。
onNavSetBars: (hidden, color, style) {
if (mounted) {
setState(() {
_isAppBarVisible = !(hidden ?? false);
if (color != null) {
try {
_currentAppBarBackgroundColor =
Color(int.parse(color.replaceFirst('#', '0xff')));
} catch (e) {
_currentAppBarBackgroundColor =
_getAppBarBackgroundColor(_themeMode);
}
} else {
_currentAppBarBackgroundColor =
_getAppBarBackgroundColor(_themeMode);
}
_currentAppBarForegroundColor =
_currentAppBarBackgroundColor.computeLuminance() > 0.5
? Colors.black
: Colors.white;
if (style != null) {
_currentAppBarStyle =
style == 'dark' ? Brightness.dark : Brightness.light;
} else {
_currentAppBarStyle =
_currentAppBarBackgroundColor.computeLuminance() > 0.5
? Brightness.dark
: Brightness.light;
}
});
}
},
// 处理导航打开事件,根据 URL 跳转到不同的页面或功能。
onNavOpen: (url) {
if (url.isNotEmpty &&
_appbridgenewPlugin.mainContext != null &&
_appbridgenewPlugin.mainContext!.mounted) {
// 处理视频播放请求
if (url.startsWith('appbridge://video_open')) {
final Uri uri = Uri.parse(url);
final String? videoUrl = uri.queryParameters['videoUrl'];
final String? pageTitle = uri.queryParameters['title'];
if (videoUrl != null) {
Navigator.of(_appbridgenewPlugin.mainContext!).push(
MaterialPageRoute(
builder: (context) => VideoPlayerPage(
videoId: 'video_123',
videoUrl:
'https://cdn.theoplayer.com/video/big_buck_bunny/big_buck_bunny.m3u8',
title: pageTitle,
vttUrl:
'https://cdn.theoplayer.com/video/big_buck_bunny/thumbnails.vtt',
appBridge: _appbridgenewPlugin,
showCachingInfo: true),
),
);
} else {}
// 处理小说阅读请求
} else if (url.startsWith('appbridge://novel_open')) {
final Uri uri = Uri.parse(url);
final String? novelId = uri.queryParameters['novelId'];
final String? novelUrl = uri.queryParameters['novelUrl'];
final String? pageTitle = uri.queryParameters['title'];
_appbridgenewPlugin.setNovelReaderActive(true);
Navigator.of(_appbridgenewPlugin.mainContext!)
.push(
MaterialPageRoute(
builder: (context) => NovelReaderPage(
novelId: novelId,
novelUrl: novelUrl,
novelTitle: pageTitle,
appBridge: _appbridgenewPlugin,
),
),
)
.then((_) {
_appbridgenewPlugin.notifyNovelReaderDismissed();
});
// 处理漫画阅读请求
} else if (url.startsWith('appbridge://comics_open')) {
final Uri uri = Uri.parse(url);
final String? comicId = uri.queryParameters['comicId'];
final String? comicUrl = uri.queryParameters['comicUrl'];
final String? pageTitle = uri.queryParameters['title'];
if (comicId != null && pageTitle != null) {
Navigator.of(_appbridgenewPlugin.mainContext!).push(
MaterialPageRoute(
builder: (context) => ComicsReaderPage(
comicId: comicId,
comicUrl: comicUrl ?? '',
comicTitle: pageTitle,
),
),
);
} else {}
// 处理帖子阅读请求
} else if (url.startsWith('appbridge://post_open')) {
final Uri uri = Uri.parse(url);
final String? postId = uri.queryParameters['postId'];
final String? postUrl = uri.queryParameters['postUrl'];
final String? pageTitle = uri.queryParameters['title'];
if (postId != null && pageTitle != null) {
Navigator.of(_appbridgenewPlugin.mainContext!).push(
MaterialPageRoute(
builder: (context) => PostReaderPage(
postId: postId,
postUrl: postUrl ?? '',
postTitle: pageTitle,
),
),
);
} else {}
}
// 处理事件测试页面
else if (url.contains('events_test.html')) {
Navigator.of(_appbridgenewPlugin.mainContext!).push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(
title: const Text('事件测试'),
centerTitle: true,
actions: [
IconButton(
icon: const Icon(Icons.repeat),
tooltip: '模拟原生事件 (on)',
onPressed: () {
_appbridgenewPlugin.emitEvent(
'native-to-h5',
{
'message': '来自 AppBar on 按钮',
'timestamp': DateTime.now().millisecondsSinceEpoch
},
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已发送 native-to-h5 事件 (on)')),
);
},
),
IconButton(
icon: const Icon(Icons.looks_one),
tooltip: '模拟原生事件 (once)',
onPressed: () {
_appbridgenewPlugin.emitEvent(
'native-to-h5',
{
'message': '来自 AppBar once 按钮',
'timestamp': DateTime.now().millisecondsSinceEpoch
},
);
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('已发送 native-to-h5 事件 (once)')),
);
},
),
],
),
body: AppBridgeWebView(
initialUrl:
'packages/appbridgenewplus/assets/events_test.html',
initialHtmlContent: null,
onWebViewCreated: (controller) {
_appbridgenewPlugin.emitEvent('theme.change', {
'theme':
_themeMode == ThemeMode.dark ? 'dark' : 'light'
});
}),
),
),
);
} else {
Navigator.of(_appbridgenewPlugin.mainContext!).push(
MaterialPageRoute(
builder: (context) => Scaffold(
appBar: AppBar(title: const Text('新页面')),
body: AppBridgeWebView(
initialUrl: url,
initialHtmlContent: null,
onWebViewCreated: (controller) {
_appbridgenewPlugin.emitEvent('theme.change', {
'theme':
_themeMode == ThemeMode.dark ? 'dark' : 'light'
});
}),
),
),
);
}
} else {}
},
onNavClose: () {
EventService().addLog('EVENT: nav.close');
},
onNavReplace: (url, title) {
EventService().addLog('EVENT: nav.replace, URL: $url, Title: $title');
},
// 处理添加快捷方式事件。
onAddShortcut: (title, url) async {
if (!mounted) return BridgeResponse.error(-1, 'Widget not mounted');
final currentContext = _appbridgenewPlugin.mainContext;
if (currentContext != null && currentContext.mounted) {
ScaffoldMessenger.of(currentContext).showSnackBar(
SnackBar(content: Text('Added shortcut: $title, url: $url')),
);
} else {}
final result = await _appbridgenewPlugin.addShortcuts(title, url);
return BridgeResponse.success(result);
},
// 处理应用图标更改事件。
onAppIcon: (styleId) async {
if (!mounted) return BridgeResponse.error(-1, 'Widget not mounted');
final currentContext = _appbridgenewPlugin.mainContext;
if (currentContext != null && currentContext.mounted) {
ScaffoldMessenger.of(currentContext).showSnackBar(SnackBar(
content: Text('Requested app icon change for style: $styleId')));
} else {}
final result = await _appbridgenewPlugin.appIcon(styleId: styleId);
return BridgeResponse.success(result);
},
// 处理应用更新检查事件。
onAppUpdateCheck: () async {
if (!mounted) return BridgeResponse.error(-1, 'Widget not mounted');
final currentContext = _appbridgenewPlugin.mainContext;
if (currentContext == null || !currentContext.mounted) {
return BridgeResponse.error(-1, 'No valid context.');
}
try {
String version = '1.1.1';
String releaseNotes = '更新内容:\n修复了一些已知问题;\n优化了用户体验。';
String downloadUrl = 'https://apk.loapk.com/51dongman_android/yjdm_01_2.28.apk';
bool hasUpdate = true;
// 针对 iOS 的处理逻辑
if (Platform.isIOS) {
final iosInfo = await _appbridgenewPlugin.checkUpdate();
// 生产环境下这里应该从服务器获取最新版本并与 iosInfo['currentVersion'] 比较
version = '1.1.5'; // 假设服务器最新版本
// 提示:正式发布时请替换为你的真实 App ID
downloadUrl = 'https://apps.apple.com/app/id414478124'; // 示例:微信的 AppStore 链接
}
final userAction = await showDialog<String>(
context: currentContext,
barrierDismissible: false,
builder: (dialogContext) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0)),
elevation: 10.0,
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.system_update,
size: 48,
color: Theme.of(dialogContext).primaryColor),
const SizedBox(height: 16),
Text(
hasUpdate ? '发现新版本 V$version' : '当前已是最新版本',
style: _getDialogTitleTextStyle(dialogContext)
.copyWith(fontSize: 20.0),
textAlign: TextAlign.center,
),
const SizedBox(height: 10),
Text(
hasUpdate ? releaseNotes : '您当前使用的是最新版本,无需更新。',
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
TextButton(
onPressed: () =>
Navigator.of(dialogContext).pop('later'),
child: const Text('以后再说'),
),
if (hasUpdate)
ElevatedButton(
onPressed: () =>
Navigator.of(dialogContext).pop('update'),
child: const Text('立即更新'),
),
],
),
],
),
),
);
},
);
if (userAction == 'update') {
await _appbridgenewPlugin.triggerAppUpdateApply(downloadUrl);
return BridgeResponse.success({'userAction': 'update'});
} else {
return BridgeResponse.success({'userAction': 'later'});
}
} catch (e) {
return BridgeResponse.error(-1, 'Update check failed: $e');
}
},
// 处理应用更新应用事件,包括下载和安装 APK。
onAppUpdateApply: (url) async {
if (url == null || url.isEmpty) {
_appbridgenewPlugin.emitEvent('apk.install.failed',
{'reason': 'Download URL is null or empty.'});
return BridgeResponse.error(
-1, 'Download URL cannot be null or empty.');
}
if (!mounted) {
return BridgeResponse.error(-1, 'Widget not mounted');
}
final currentContext = _appbridgenewPlugin.mainContext;
if (currentContext == null || !currentContext.mounted) {
return BridgeResponse.error(-1, 'No valid context.');
}
// 针对 iOS 的特殊处理:直接跳转,不显示下载进度条
if (Platform.isIOS) {
try {
await _appbridgenewPlugin.applyUpdate(url);
ScaffoldMessenger.of(currentContext).showSnackBar(
const SnackBar(content: Text('正在为您跳转至 App Store...')));
return BridgeResponse.success(true);
} catch (e) {
ScaffoldMessenger.of(currentContext).showSnackBar(
SnackBar(content: Text('跳转失败: ${e.toString()}')));
return BridgeResponse.error(-1, 'Failed to open App Store URL: $e');
}
}
final String localGeneratedDownloadId =
'update_apk_${DateTime.now().millisecondsSinceEpoch}';
final Completer<Map<String, dynamic>> downloadCompleter =
Completer<Map<String, dynamic>>();
StreamSubscription? progressSubscription;
double currentProgress = 0.0;
final String? returnedTaskId = await _appbridgenewPlugin
.applyUpdate(url, downloadId: localGeneratedDownloadId);
if (mounted) {
setState(() {
_currentDownloadTaskId = returnedTaskId;
});
}
final bool initiationSuccess = _currentDownloadTaskId != null;
bool isDialogDismissedByCancel = false;
if (currentContext.mounted) {
showDialog(
context: currentContext,
barrierDismissible: false,
builder: (dialogContext) {
return Dialog(
// 使用 Dialog 代替 AlertDialog
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16.0)), // 圆角
elevation: 10.0, // 阴影
child: Padding(
padding: const EdgeInsets.all(20.0),
child: StatefulBuilder(
builder: (context, setStateInDialog) {
String totalSize = '未知';
if (progressSubscription == null) {
progressSubscription = _appbridgenewPlugin
.on('download.progress', (eventData) {
final Map<String, dynamic> data =
eventData['payload'] as Map<String, dynamic>;
if (data['id'] ==
(_currentDownloadTaskId ??
localGeneratedDownloadId)) {
setStateInDialog(() {
currentProgress =
(data['progress'] as int? ?? 0) / 100.0;
final int? totalBytesFromEvent =
data['totalBytes'] as int?;
if (totalBytesFromEvent != null &&
totalBytesFromEvent > 0) {
totalSize =
'${(totalBytesFromEvent / (1024 * 1024)).toStringAsFixed(2)} MB';
} else {
totalSize = '未知';
}
});
}
});
_appbridgenewPlugin.on('download.completed',
(eventData) {
final Map<String, dynamic> data =
eventData['payload'] as Map<String, dynamic>;
if (data['id'] ==
(_currentDownloadTaskId ??
localGeneratedDownloadId)) {
progressSubscription?.cancel();
if (!downloadCompleter.isCompleted) {
downloadCompleter.complete(
{'success': true, 'filePath': data['path']});
}
}
});
_appbridgenewPlugin.on('download.failed', (eventData) {
final Map<String, dynamic> data =
eventData['payload'] as Map<String, dynamic>;
if (data['id'] ==
(_currentDownloadTaskId ??
localGeneratedDownloadId)) {
progressSubscription?.cancel();
if (!downloadCompleter.isCompleted) {
downloadCompleter.complete(
{'success': false, 'error': data['error']});
}
}
});
_appbridgenewPlugin.on('download.canceled',
(eventData) {
final Map<String, dynamic> data =
eventData['payload'] as Map<String, dynamic>;
if (data['id'] ==
(_currentDownloadTaskId ??
localGeneratedDownloadId)) {
progressSubscription?.cancel();
if (!downloadCompleter.isCompleted) {
downloadCompleter.complete({
'success': false,
'error': 'Download cancelled by system or user.'
});
}
}
});
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.download,
size: 48,
color:
Theme.of(dialogContext).primaryColor), // 添加图标
const SizedBox(height: 16),
Text('下载更新',
style: _getDialogTitleTextStyle(dialogContext)
.copyWith(fontSize: 20.0)), // 调整标题字号
const SizedBox(height: 10),
Text(
'新版本下载中: ${totalSize == '未知' ? '' : '($totalSize)'}'),
const SizedBox(height: 20),
LinearProgressIndicator(value: currentProgress),
const SizedBox(height: 10),
Text('进度: ${(currentProgress * 100).toInt()}%'),
const SizedBox(height: 20),
TextButton(
onPressed: () async {
final idToCancel = _currentDownloadTaskId ??
localGeneratedDownloadId;
await _appbridgenewPlugin.download
?.handleMethod('cancel', {'id': idToCancel});
progressSubscription?.cancel();
if (!downloadCompleter.isCompleted) {
downloadCompleter.complete(
{'success': false, 'error': '取消了下载'});
}
isDialogDismissedByCancel = true;
Navigator.of(dialogContext).pop();
},
child: const Text('取消'),
),
],
);
},
),
),
);
},
);
}
if (currentContext.mounted) {
final Map<String, dynamic> downloadResult =
await downloadCompleter.future;
final bool downloadSuccess =
downloadResult['success'] as bool? ?? false;
final String? downloadedFilePath =
downloadResult['filePath'] as String?;
final String? downloadError = downloadResult['error'] as String?;
if (!isDialogDismissedByCancel) {
Navigator.of(currentContext).pop();
}
if (downloadSuccess && (initiationSuccess ?? false)) {
ScaffoldMessenger.of(currentContext)
.showSnackBar(const SnackBar(content: Text('下载成功,正在准备安装...')));
if (downloadedFilePath != null && downloadedFilePath.isNotEmpty) {
_pendingInstallApkPath = downloadedFilePath; // 存储路径,以备在应用恢复时继续安装
final bool? installInitiated =
await _appbridgenewPlugin.apkInstall(downloadedFilePath);
if (installInitiated != true) {
ScaffoldMessenger.of(currentContext).showSnackBar(
const SnackBar(content: Text('APK安装失败,请尝试从通知栏或文件管理器安装。')));
_appbridgenewPlugin.emitEvent('apk.install.failed', {
'path': downloadedFilePath,
'reason': 'Failed to initiate install intent.'
});
}
_pendingInstallApkPath = null; // 尝试安装后清除路径
} else {
ScaffoldMessenger.of(currentContext).showSnackBar(
const SnackBar(content: Text('更新失败:无法获取文件路径。')));
_appbridgenewPlugin.emitEvent('apk.install.failed', {
'path': null,
'reason': 'Downloaded file path is null or empty.'
});
}
} else {
ScaffoldMessenger.of(currentContext).showSnackBar(
SnackBar(content: Text('更新失败: ${downloadError ?? '未知错误'}')));
_appbridgenewPlugin.emitEvent('download.failed', {
'url': url,
'error': downloadError ?? 'Unknown download error.'
});
}
}
return BridgeResponse.success(initiationSuccess);
},
);
// _runAllApiTests();
}
@override
// 监听应用生命周期变化,用于处理 APK 安装中断后的恢复。
void didChangeAppLifecycleState(AppLifecycleState state) async {
super.didChangeAppLifecycleState(state);
if (state == AppLifecycleState.resumed) {
if (_pendingInstallApkPath != null) {
final currentContext = _appbridgenewPlugin.mainContext;
if (currentContext != null && currentContext.mounted) {
ScaffoldMessenger.of(currentContext).showSnackBar(
const SnackBar(content: Text('检测到有未完成的APK安装,尝试继续安装...')));
final bool? installInitiated =
await _appbridgenewPlugin.apkInstall(_pendingInstallApkPath!);
if (installInitiated == true) {
} else {
ScaffoldMessenger.of(currentContext).showSnackBar(
const SnackBar(content: Text('APK安装失败,请尝试从通知栏或文件管理器安装。')));
_appbridgenewPlugin.emitEvent('apk.install.failed', {
'path': _pendingInstallApkPath,
'reason': 'Failed to initiate install intent on resume.'
});
}
_pendingInstallApkPath = null; // 尝试安装后清除路径
}
}
}
}
@override
// 构建应用程序的用户界面。
Widget build(BuildContext context) {
// 根据 AppBarTheme 的 textStyle 优先确定基础样式
final TextStyle baseStyle = Theme.of(context).appBarTheme.titleTextStyle ??
Theme.of(context).textTheme.titleLarge ??
const TextStyle(fontSize: 16.0, color: Colors.white); // 默认回退样式
// 创建最终样式,降低字号并设置正确的颜色
final TextStyle finalTitleStyle = baseStyle.copyWith(
fontSize: (baseStyle.fontSize ?? 16.0) - 4.0,
color: _currentAppBarForegroundColor,
);
return MaterialApp(
navigatorKey: AppbridgenewPlatform.instance.navigatorKey,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.blue.shade700,
brightness: Brightness.light,
),
useMaterial3: true,
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.grey.shade900,
brightness: Brightness.dark,
),
useMaterial3: true,
),
themeMode: _themeMode,
// 使用状态变量控制主题模式
home: Scaffold(
appBar: _isAppBarVisible
? AppBar(
title: Text(
_currentAppBarTitle,
style: finalTitleStyle,
),
backgroundColor: _currentAppBarBackgroundColor,
foregroundColor: _currentAppBarForegroundColor,
// 使用状态变量控制前景颜色
centerTitle: true,
systemOverlayStyle: _currentAppBarStyle == Brightness.dark
? SystemUiOverlayStyle.dark
: SystemUiOverlayStyle.light,
actions: [
IconButton(
icon: Icon(
_themeMode == ThemeMode.dark
? Icons.dark_mode
: Icons.light_mode,
color: _currentAppBarForegroundColor, // 使用状态变量控制图标颜色
),
onPressed: () {
setState(() {
_themeMode = _themeMode == ThemeMode.dark
? ThemeMode.light
: ThemeMode.dark;
_currentAppBarBackgroundColor =
_getAppBarBackgroundColor(_themeMode);
_currentAppBarForegroundColor =
_getAppBarForegroundColor(_themeMode);
_currentAppBarStyle = _getAppBarStyle(_themeMode);
_appbridgenewPlugin.emitEvent('theme.change', {
'theme':
_themeMode == ThemeMode.dark ? 'dark' : 'light'
});
});
},
),
],
)
: null,
body: (_initialUrl != null || _demoHtmlContent != null)
? AppBridgeWebView(
initialUrl: _initialUrl,
initialHtmlContent:
_initialUrl == null ? _demoHtmlContent : null)
: Container(
color: Colors.white,
child: const Center(child: CircularProgressIndicator()))),
);
}
/*
* 预热信令服务器 (唤醒 Render 冷启动)
*/
Future<void> _preWarmSignalingServer() async {
try {
const String signalingServerUrl = WebRTCConfig.signalingServerUrl;
final String httpUrl = '${signalingServerUrl
.replaceFirst('wss://', 'https://')
.replaceFirst('ws://', 'http://')}/ping';
debugPrint('🔥 程序启动:正在预热信令服务器以唤醒冷启动: $httpUrl');
// 发起一个简单的 GET 请求,无需等待结果
http
.get(Uri.parse(httpUrl))
.timeout(const Duration(seconds: 10))
.then((_) {
debugPrint('✅ 信令服务器预热请求已发出');
}).catchError((e) {
debugPrint('ℹ️ 信令服务器预热请求已发出或失败 (预期中): $e');
});
} catch (e) {
debugPrint('❌ 预热信令服务器逻辑错误: $e');
}
}
}