import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:yx_app_upgrade_flutter/core/upgrade_utils.dart'; import 'app_upgrade_plugin_platform_interface.dart'; import 'core/permission_helper.dart'; import 'models/app_upgrade_method.dart'; import 'models/app_upgrade_version.dart'; import 'models/upgrade_info.dart'; /// 简化的插件接口,避免循环导入 class _SimpleAppUpgradePlugin { static final _SimpleAppUpgradePlugin _instance = _SimpleAppUpgradePlugin._(); _SimpleAppUpgradePlugin._(); static _SimpleAppUpgradePlugin get instance => _instance; Future checkUpdate(String url, {Map? params}) { return AppUpgradePluginPlatform.instance.checkUpdate(url, params: params); } Future downloadApk(String url, {Function(DownloadProgress)? onProgress}) { return AppUpgradePluginPlatform.instance .downloadApk(url, onProgress: onProgress); } Future installApk(String filePath) { return AppUpgradePluginPlatform.instance.installApk(filePath); } Future goToAppStore(String url, {required BuildContext context}) { return AppUpgradePluginPlatform.instance .goToAppStore(url, context: context); } Future> getAppInfo() { return AppUpgradePluginPlatform.instance.getAppInfo(); } /// 获取已安装的应用市场列表 Future> getInstalledMarkets() { return AppUpgradePluginPlatform.instance.getInstalledMarkets(); } /// 获取下载路径 Future getDownloadPath() { return AppUpgradePluginPlatform.instance.getDownloadPath(); } } /// 升级配置选项 class UpgradeConfig { /// 是否显示无更新提示 final bool showNoUpdateToast; /// 是否自动安装 final bool autoInstall; /// 安装检测超时时间(秒) final int installTimeout; /// 是否启用调试日志 final bool enableDebugLog; /// 自定义Toast显示函数 final void Function(String message)? customToast; /// 是否需要获取安装未知应用权限(默认false,直接安装) final bool requireInstallPermission; const UpgradeConfig({ this.showNoUpdateToast = true, this.autoInstall = false, this.installTimeout = 45, this.enableDebugLog = true, this.customToast, this.requireInstallPermission = false, // 默认不需要权限 }); /// 快速配置:开发模式 /// /// 适用于:开发和测试阶段 /// - 显示所有提示信息(便于调试) /// - 启用详细调试日志(便于排查问题) /// - 安装检测超时时间较短(30秒,快速反馈) /// - 手动安装(更安全,便于测试) static const UpgradeConfig development = UpgradeConfig( showNoUpdateToast: true, autoInstall: true, installTimeout: 45, enableDebugLog: true, requireInstallPermission: false, ); /// 快速配置:生产模式 /// /// 适用于:正式发布环境 /// - 静默检查(无更新时不显示提示,减少打扰) /// - 手动安装(更安全,用户可控) /// - 关闭调试日志(提升性能,减少日志输出) /// - 安装检测超时时间较长(60秒,给用户充足时间完成安装) static const UpgradeConfig production = UpgradeConfig( showNoUpdateToast: false, autoInstall: true, installTimeout: 45, enableDebugLog: false, requireInstallPermission: false, ); } class ParsedUpgradeContent { const ParsedUpgradeContent({ this.header, this.hasExplicitHeaderBlock = false, this.bodyItems = const [], }); final String? header; final bool hasExplicitHeaderBlock; final List bodyItems; bool get hasHeader => header?.isNotEmpty ?? false; bool get hasBodyItems => bodyItems.isNotEmpty; } class ParsedUpgradeContentItem { const ParsedUpgradeContentItem({ required this.text, this.hasLeadingMarker = false, }); final String text; final bool hasLeadingMarker; } /// 简化版App升级管理器 /// 提供最简单的API,一行代码即可实现App升级功能 class AppUpgradeSimple { static AppUpgradeSimple? _instance; /// 获取单例实例 static AppUpgradeSimple get instance { _instance ??= AppUpgradeSimple._(); return _instance!; } @visibleForTesting static set instance(AppUpgradeSimple value) { _instance = value; } @visibleForTesting AppUpgradeSimple.private({_SimpleAppUpgradePlugin? plugin}) : _plugin = plugin ?? _SimpleAppUpgradePlugin.instance; AppUpgradeSimple._() : _plugin = _SimpleAppUpgradePlugin.instance; final _SimpleAppUpgradePlugin _plugin; UpgradeConfig _config = const UpgradeConfig(); /// 配置升级参数 void configure(UpgradeConfig config) { _config = config; } /// 检查网络连接状态 Future checkNetworkStatus() async { try { final result = await InternetAddress.lookup('baidu.com'); return result.isNotEmpty && result[0].rawAddress.isNotEmpty; } catch (e) { debugPrint('网络检查失败: $e'); return false; } } /// 清理下载的临时文件 Future clearDownloadCache() async { try { final downloadPath = await _plugin.getDownloadPath(); if (downloadPath != null) { final dir = Directory(downloadPath); if (await dir.exists()) { final files = await dir .list() .where((file) => file.path.endsWith('.apk')) .toList(); for (final file in files) { try { await file.delete(); debugPrint('已删除缓存文件: ${file.path}'); } catch (e) { debugPrint('删除缓存文件失败: ${file.path}, 错误: $e'); } } } } } catch (e) { debugPrint('清理下载缓存失败: $e'); } } /// 一键检查更新(支持配置) /// /// 参数说明: /// - [context] (必需) BuildContext,用于显示升级对话框,必须在 MaterialApp 环境内调用 /// - [future] (必需) 异步函数,用于获取服务器版本信息,返回 [AppUpgradeVersion] 对象或 null /// - 返回 null 时视为无更新 /// - 返回 [AppUpgradeVersion] 时,会与当前版本比较,决定是否显示升级对话框 /// - [showNoUpdateToast] (可选) 是否显示"已是最新版本"的提示 /// - true: 无更新时显示提示(默认) /// - false: 无更新时不显示提示 /// - null: 使用 [config] 或全局配置的 [UpgradeConfig.showNoUpdateToast] /// - [autoInstall] (可选) 是否自动安装APK /// - true: 下载完成后自动触发安装流程 /// - false: 下载完成后需要用户手动触发安装(默认) /// - null: 使用 [config] 或全局配置的 [UpgradeConfig.autoInstall] /// - [onComplete] (可选) 完成回调函数,接收一个 bool 参数表示是否更新成功 /// - true: 更新成功或已是最新版本 /// - false: 用户取消更新或更新失败 /// - 在以下情况会被调用: /// - 检查完成(无论是否有更新) /// - 用户完成更新或关闭升级对话框 /// - [config] (可选) 升级配置对象 [UpgradeConfig] /// - 如果提供,会覆盖全局配置和单个参数设置 /// - 如果为 null,使用全局配置 [UpgradeConfig] /// - 配置项包括:超时时间、调试日志、权限要求等 /// /// 使用示例: /// ```dart /// await AppUpgradeSimple.instance.checkUpdate( /// context: context, /// future: () async { /// // 调用您的API获取版本信息 /// final response = await http.get('https://api.example.com/version'); /// return AppUpgradeVersion.fromJson(json.decode(response.body)); /// }, /// showNoUpdateToast: true, /// autoInstall: false, /// onComplete: (success) { /// print('检查更新完成,结果: $success'); /// }, /// ); /// ``` /// /// 参数优先级: /// 1. 方法参数(如 [showNoUpdateToast], [autoInstall]) /// 2. [config] 参数中的配置 /// 3. 全局配置(通过 [configure] 方法设置) /// 4. 默认配置 Future checkUpdate({ required BuildContext context, required Future Function() future, bool? showNoUpdateToast, bool? autoInstall, VoidCallback? onComplete, VoidCallback? onUpdateLater, UpgradeConfig? config, }) async { // 使用传入的配置或默认配置 final effectiveConfig = config ?? _config; final finalShowNoUpdateToast = showNoUpdateToast ?? effectiveConfig.showNoUpdateToast; final finalAutoInstall = autoInstall ?? effectiveConfig.autoInstall; try { assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用'); final info = await _prepareUpgradeInfo(future: future, config: effectiveConfig); if (info == null) { return; } if (!info.hasUpdate) { if (finalShowNoUpdateToast && context.mounted) { _showToast('已是最新版本', context, effectiveConfig); } return; } await _showUpgradeDialog( context: context, info: info, autoInstall: finalAutoInstall, onUpdateLater: onUpdateLater, config: effectiveConfig, ); } catch (e) { debugPrint('检查更新失败: $e'); if (context.mounted) { _showToast('检查更新遇到问题', context, effectiveConfig); } } finally { onComplete?.call(); } } /// 静默检查更新(不弹出任何 UI) /// /// 返回 [UpgradeInfo],其中包含服务端版本信息以及 hasUpdate 标记。 /// 场景示例: /// - App 冷启动时后台检查更新,但不打扰用户 /// - 进入「设置-检查更新」页面前,先决定是否展示弹窗 /// /// 搭配 [showPreparedUpgrade] 可避免重复请求服务端。 Future silentCheckUpdate({ required Future Function() future, UpgradeConfig? config, }) async { final effectiveConfig = config ?? _config; try { final info = await _prepareUpgradeInfo(future: future, config: effectiveConfig); if (effectiveConfig.enableDebugLog) { if (info == null) { debugPrint('🔕 静默检查结果: 未返回版本信息'); } else { debugPrint('🔕 静默检查完成: hasUpdate=${info.hasUpdate}'); } } return info; } catch (e) { if (effectiveConfig.enableDebugLog) { debugPrint('静默检查更新失败: $e'); } return null; } } /// 使用已知的 [UpgradeInfo] 展示升级弹窗 /// /// 通常与 [silentCheckUpdate] 搭配:先静默检查并缓存结果,用户点击 /// 「检查更新」按钮时再调用此方法展示 UI,无需再次访问服务端。 Future showPreparedUpgrade({ required BuildContext context, required UpgradeInfo info, bool? autoInstall, VoidCallback? onUpdateLater, UpgradeConfig? config, }) async { final effectiveConfig = config ?? _config; final finalAutoInstall = autoInstall ?? effectiveConfig.autoInstall; assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用'); if (!info.hasUpdate) { if (effectiveConfig.enableDebugLog) { debugPrint('🔔 showPreparedUpgrade: 无新版本,跳过弹窗'); } if (effectiveConfig.showNoUpdateToast && context.mounted) { _showToast('已是最新版本', context, effectiveConfig); } return; } await _showUpgradeDialog( context: context, info: info, autoInstall: finalAutoInstall, onUpdateLater: onUpdateLater, config: effectiveConfig, ); } /// 内部方法:获取版本信息并构建 UpgradeInfo Future _prepareUpgradeInfo({ required Future Function() future, required UpgradeConfig config, }) async { // 1. 获取服务器版本信息 final serverInfo = await future(); if (serverInfo == null) { if (config.enableDebugLog) { debugPrint('🔍 检查更新结果: 未返回版本信息'); } return null; } if (config.enableDebugLog) { debugPrint('🔍 获取到服务器版本: $serverInfo'); } // 2. 获取当前App信息 final appInfo = await _plugin.getAppInfo(); final currentVersionName = appInfo['version'] ?? ''; final currentBuildNumber = int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0; if (config.enableDebugLog) { debugPrint('📱 当前版本: $currentVersionName (Build: $currentBuildNumber)'); } // 3. 比较版本并构建 UpgradeInfo bool hasUpdate = false; final versionName = serverInfo.versionName; final versionBuildNumber = serverInfo.versionBuildNumber; /// 断言 判断 serverInfo 的版本名称和版本号不可都为null assert( !(versionBuildNumber == null && versionName == null), 'versionBuildNumber和versionName不可同时为NULL', ); if (versionBuildNumber != null && versionBuildNumber > 0) { // 优先比较 buildNumber if (versionBuildNumber > currentBuildNumber) { hasUpdate = true; } else { if (versionName != null && compareVersionStrings(versionName, currentVersionName) > 0) { hasUpdate = true; } } } else { // 只比较版本名 if (versionName != null && compareVersionStrings(versionName, currentVersionName) > 0) { hasUpdate = true; } } // if (versionBuildNumber > 0) { // // 优先比较 buildNumber // if (versionBuildNumber > currentBuildNumber) { // hasUpdate = true; // } else if (versionBuildNumber == currentBuildNumber) { // // buildNumber 相同,比较版本名 // if (_compareVersionStrings(versionName, currentVersionName) > 0) { // hasUpdate = true; // } // } // } else { // // 只比较版本名 // if (_compareVersionStrings(versionName, currentVersionName) > 0) { // hasUpdate = true; // } // } if (config.enableDebugLog) { debugPrint('📊 版本比较结果: ${hasUpdate ? "有新版本" : "已是最新"}'); } // 构建 UpgradeInfo // 兜底处理,避免 serverInfo 里的可空字段传入非空参数导致崩溃 final safeVersionName = serverInfo.versionName ?? currentVersionName; final safeVersionBuildNumber = serverInfo.versionBuildNumber ?? currentBuildNumber; return UpgradeInfo( hasUpdate: hasUpdate, isForceUpdate: serverInfo.isForce, versionName: safeVersionName, versionBuildNumber: safeVersionBuildNumber, currentVersionName: currentVersionName, currentBuildNumber: currentBuildNumber, updateContent: serverInfo.updateContent, downloadUrl: serverInfo.downloadUrl, appStoreUrl: serverInfo.appStoreUrl, apkSize: serverInfo.apkSize, apkMd5: serverInfo.apkMd5, appMarkets: serverInfo.appMarkets, supportedMethods: serverInfo.supportedMethods ?? const [ AppUpgradeMethod.market, AppUpgradeMethod.browser, AppUpgradeMethod.inApp ], ); } /// 预下载APK(不显示UI,后台下载) Future preDownloadApk({ required String url, Function(DownloadProgress)? onProgress, }) async { try { return await _plugin.downloadApk(url, onProgress: onProgress); } catch (e) { if (_config.enableDebugLog) { debugPrint('预下载APK失败: $e'); } return null; } } /// 检查是否有已下载的APK文件 Future findDownloadedApk(String version) async { try { final downloadPath = await _plugin.getDownloadPath(); if (downloadPath != null) { final dir = Directory(downloadPath); if (await dir.exists()) { final files = await dir.list().toList(); for (final file in files) { if (file.path.contains(version) && file.path.endsWith('.apk')) { final fileEntity = File(file.path); if (await fileEntity.exists()) { return file.path; } } } } } return null; } catch (e) { if (_config.enableDebugLog) { debugPrint('查找已下载APK失败: $e'); } return null; } } /// 获取当前应用版本信息 Future> getAppInfo() async { return await _plugin.getAppInfo(); } static ParsedUpgradeContent parseUpdateContent(String content) { final normalizedContent = content.replaceAll('\r\n', '\n').replaceAll('\r', '\n').trim(); if (normalizedContent.isEmpty) { return const ParsedUpgradeContent(); } final extraction = _extractUpgradeContentHeader( normalizedContent.split('\n'), ); final bodyItems = extraction.bodyLines .map((line) => line.trim()) .where((line) => line.isNotEmpty) .map( (line) => ParsedUpgradeContentItem( text: line, hasLeadingMarker: _contentLineMarkerRegExp.hasMatch(line), ), ) .toList(growable: false); return ParsedUpgradeContent( header: extraction.header, hasExplicitHeaderBlock: extraction.hasExplicitHeaderBlock, bodyItems: bodyItems, ); } static int compareVersionStrings(String v1, String v2) { try { final v1Parts = v1.split('.').map((e) => int.tryParse(e) ?? 0).toList(); final v2Parts = v2.split('.').map((e) => int.tryParse(e) ?? 0).toList(); final maxLength = v1Parts.length > v2Parts.length ? v1Parts.length : v2Parts.length; for (int i = 0; i < maxLength; i++) { final part1 = i < v1Parts.length ? v1Parts[i] : 0; final part2 = i < v2Parts.length ? v2Parts[i] : 0; if (part1 < part2) { return -1; } else if (part1 > part2) { return 1; } } return 0; } catch (e) { return v1.compareTo(v2); } } /// 显示升级对话框 Future _showUpgradeDialog({ required BuildContext context, required UpgradeInfo info, required bool autoInstall, VoidCallback? onUpdateLater, UpgradeConfig? config, }) { final effectiveConfig = config ?? _config; return showDialog( context: context, barrierDismissible: !info.isForceUpdate, builder: (context) { if (info.isForceUpdate) { return _ForceUpgradeDialog( info: info, autoInstall: autoInstall, config: effectiveConfig, ); } else { return _SimpleUpgradeDialog( info: info, autoInstall: autoInstall, onUpdateLater: onUpdateLater, config: effectiveConfig, showToast: (message) => _showToast(message, context, effectiveConfig), ); } }, ); } /// 使用 Overlay 显示 Toast(不依赖 Scaffold) void _showOverlayToast( BuildContext context, String message, UpgradeConfig config) { if (!context.mounted) { debugPrint('Toast消息(context已卸载): $message'); return; } try { final overlay = Overlay.of(context); // 创建 OverlayEntry final overlayEntry = OverlayEntry( builder: (context) => _ToastWidget(message: message), ); // 插入到 Overlay overlay.insert(overlayEntry); // 2秒后自动移除 Future.delayed(const Duration(seconds: 2), () { try { overlayEntry.remove(); } catch (e) { // 忽略移除错误 if (config.enableDebugLog) { debugPrint('移除Toast失败: $e'); } } }); } catch (e) { debugPrint('显示Toast失败: $e'); debugPrint('Toast消息: $message'); } } /// 尝试使用 ScaffoldMessenger 显示 SnackBar(如果可用) void _tryShowSnackBar( BuildContext context, String message, UpgradeConfig config) { if (!context.mounted) { _showOverlayToast(context, message, config); return; } // 尝试获取 ScaffoldMessenger final scaffoldMessenger = ScaffoldMessenger.maybeOf(context); if (scaffoldMessenger == null) { // 如果没有 ScaffoldMessenger,使用 Overlay Toast _showOverlayToast(context, message, config); return; } // 尝试使用根 Navigator 的 context(更安全) BuildContext? rootContext; try { final navigator = Navigator.maybeOf(context, rootNavigator: true); if (navigator != null) { rootContext = navigator.context; } } catch (e) { // 忽略错误,继续使用原 context } // 优先使用根 context 的 ScaffoldMessenger final messenger = rootContext != null && rootContext.mounted ? ScaffoldMessenger.maybeOf(rootContext) : scaffoldMessenger; if (messenger == null) { _showOverlayToast(context, message, config); return; } // 尝试显示 SnackBar,如果失败则使用 Overlay Toast try { messenger.showSnackBar( SnackBar( content: Text(message), duration: Duration(seconds: 2), ), ); } catch (e) { // 如果 SnackBar 失败,使用 Overlay Toast if (config.enableDebugLog) { debugPrint('SnackBar显示失败,使用Overlay Toast: $e'); } _showOverlayToast(context, message, config); } } /// 显示Toast提示 void _showToast(String message, BuildContext context, [UpgradeConfig? config]) { final effectiveConfig = config ?? _config; if (effectiveConfig.customToast != null) { effectiveConfig.customToast!(message); } else { // 优先尝试使用 SnackBar,如果失败则使用 Overlay Toast _tryShowSnackBar(context, message, effectiveConfig); } } bool _canShowMaterialDialog(BuildContext context) { if (!context.mounted) return false; try { return Localizations.of( context, MaterialLocalizations) != null; } catch (_) { return false; } } } /// 共享的升级操作逻辑 mixin _UpgradeDialogLogic on State { final _plugin = _SimpleAppUpgradePlugin.instance; bool _isDownloading = false; double _downloadProgress = 0; String _statusText = ''; String? _downloadedFilePath; bool _isInstalling = false; bool _isWaitingForInstallation = false; Timer? _installCheckTimer; UpgradeInfo get info; void Function(String) get showToast; bool get autoInstall; UpgradeConfig get config; void initUpgradeLogic() { // 初始化逻辑(如果需要) } void disposeUpgradeLogic() { _installCheckTimer?.cancel(); } void onAppLifecycleStateChanged(AppLifecycleState state) { debugPrint( '🔄 应用生命周期状态变化: $state, _isWaitingForInstallation=$_isWaitingForInstallation'); if (_isWaitingForInstallation && state == AppLifecycleState.resumed) { debugPrint('⚡ 应用回到前台,检查安装状态'); Future.delayed(const Duration(milliseconds: 1500), () { if (mounted && _isWaitingForInstallation) { _checkInstallationResult(); } }); } } /// 前往浏览器 void _goToBrowser() async { final downloadApkUrl = info.downloadUrl; if (!Platform.isAndroid || downloadApkUrl == null) { showToast('下载地址为空'); return; } if (!mounted) return; try { final uri = Uri.parse(downloadApkUrl); try { final launched = await launchUrl( uri, mode: LaunchMode.externalApplication, ); if (!launched) { await launchUrl( uri, mode: LaunchMode.platformDefault, ); } // 不再关闭弹窗,即使用户跳转到浏览器 // 原代码: // if (mounted && Navigator.canPop(context)) { ... } } catch (launchError) { debugPrint('launchUrl 失败: $launchError'); final canLaunch = await canLaunchUrl(uri); if (canLaunch) { await launchUrl(uri, mode: LaunchMode.externalApplication); // 不再关闭弹窗 } else { showToast('无法打开下载链接,请检查是否安装了浏览器'); } } } catch (e) { debugPrint('打开浏览器失败: $e'); showToast('打开浏览器失败: ${e.toString()}'); } } Future _startDownloadAndInstall() async { if (!Platform.isAndroid || info.downloadUrl == null) return; if (!mounted) return; final hasStorage = await PermissionHelper.checkAndRequestStoragePermission( context: context); if (!hasStorage) { showToast('缺少存储权限,无法下载'); return; } await PermissionHelper.checkAndRequestNotificationPermission( context: context); setState(() { _isDownloading = true; _statusText = '下载中...'; _downloadProgress = 0; }); try { final filePath = await _plugin.downloadApk( info.downloadUrl!, onProgress: (p) { if (!mounted) return; setState(() { _downloadProgress = p.progress; _statusText = '下载中 ${p.percentage}%'; }); }, ); if (!mounted) return; if (filePath == null) { setState(() { _isDownloading = false; _statusText = '下载失败'; }); showToast('下载失败,请稍后重试'); return; } setState(() { _statusText = '下载完成'; _downloadProgress = 1.0; _downloadedFilePath = filePath; }); if (autoInstall) { await _installApk(filePath); } } catch (e) { if (!mounted) return; debugPrint('下载APK异常: $e'); setState(() { _isDownloading = false; _statusText = '下载失败'; }); // 显示用户友好的错误提示 final errorMessage = e.toString().replaceFirst('Exception: ', ''); showToast(errorMessage.isNotEmpty ? errorMessage : '下载失败,请稍后重试'); } } Future _installApk(String filePath) async { if (!mounted) return; setState(() { _isInstalling = true; _statusText = '准备安装...'; }); if (config.requireInstallPermission) { debugPrint('🔐 检查安装权限(配置要求)'); final hasPermission = await PermissionHelper.checkAndRequestInstallPermission( context: context); if (!hasPermission) { if (mounted) { setState(() { _isInstalling = false; _statusText = '权限被拒绝'; }); showToast('未授予安装权限,无法完成更新'); } return; } } else { debugPrint('🚀 跳过权限检查,直接安装(配置默认)'); } if (mounted) { setState(() { _statusText = '正在安装...'; }); } try { final success = await _plugin.installApk(filePath); if (mounted) { if (success) { setState(() { _isInstalling = false; _isWaitingForInstallation = true; _statusText = '请完成安装'; }); showToast('请在系统弹窗中完成安装'); _startInstallationTimeoutCheck(); } else { setState(() { _isInstalling = false; _statusText = '安装失败'; }); showToast('无法启动安装程序,请重试'); } } } catch (e) { debugPrint('安装APK异常: $e'); if (mounted) { setState(() { _isInstalling = false; _statusText = '安装异常'; }); showToast('安装出现异常,请重试'); } } } void _startInstallationTimeoutCheck() { debugPrint('🚀 启动简化安装检测系统'); _installCheckTimer = Timer(Duration(seconds: config.installTimeout), () { if (mounted && _isWaitingForInstallation) { debugPrint('⏰ 安装检测超时'); setState(() { _isWaitingForInstallation = false; _statusText = '安装超时'; }); showToast('安装超时,请使用重试按钮重新安装'); } }); } Future _checkInstallationResult() async { if (!mounted || !_isWaitingForInstallation) { debugPrint( '跳过安装结果检查: mounted=$mounted, _isWaitingForInstallation=$_isWaitingForInstallation'); return; } debugPrint('🔍 开始简化安装检测...'); try { final appInfo = await _plugin.getAppInfo(); final currentVersion = appInfo['version'] ?? ''; final currentBuildNumber = int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0; debugPrint('📱 当前版本: $currentVersion, 构建号: $currentBuildNumber'); debugPrint( '🎯 目标版本: ${info.versionName}, 构建号: ${info.versionBuildNumber}'); bool isUpdated = false; if (info.versionBuildNumber > 0) { if (currentBuildNumber > info.versionBuildNumber) { isUpdated = true; } else { isUpdated = AppUpgradeSimple.compareVersionStrings( currentVersion, info.versionName) > 0; } debugPrint( '📊 构建号比较: $currentBuildNumber vs ${info.versionBuildNumber}, 版本比较(如需): ${info.versionName} -> $isUpdated'); } else { isUpdated = AppUpgradeSimple.compareVersionStrings( currentVersion, info.versionName) > 0; debugPrint( '📊 版本号比较: $currentVersion vs ${info.versionName} = $isUpdated'); } if (isUpdated) { debugPrint('✅ 检测结果: 安装成功'); _handleInstallationSuccess(); } else { debugPrint('❌ 检测结果: 安装被取消(版本未更新)'); _handleInstallationCancelled(); } } catch (e) { debugPrint('❌ 检测失败: $e'); setState(() { _isWaitingForInstallation = false; _statusText = '检测失败'; }); showToast('无法检测安装状态,请手动确认'); } } void _handleInstallationSuccess() { _installCheckTimer?.cancel(); setState(() { _isWaitingForInstallation = false; _statusText = '安装成功'; }); showToast('应用更新成功!'); Future.delayed(const Duration(seconds: 1), () { if (mounted && Navigator.canPop(context)) { Navigator.of(context).pop(); } }); } void _handleInstallationCancelled() { setState(() { _isWaitingForInstallation = false; _statusText = '安装被取消'; }); showToast('安装被取消,可以点击重试按钮重新安装'); } Future _retryInstall() async { if (_downloadedFilePath != null) { if (_statusText == '请完成安装') { await _checkInstallationResult(); if (_statusText == '请完成安装') { await _installApk(_downloadedFilePath!); } return; } if (_statusText == '权限被拒绝' && config.requireInstallPermission) { final hasPermission = await PermissionHelper.checkAndRequestInstallPermission( context: context); if (!hasPermission) { showToast('仍未获得安装权限,请在设置中手动开启'); return; } if (mounted) { setState(() { _statusText = '下载完成'; }); } } await _installApk(_downloadedFilePath!); } } Widget _buildVersionInfoCard(BuildContext context, ColorScheme colorScheme) { return Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( colors: [ colorScheme.primaryContainer.withOpacity(0.3), colorScheme.primaryContainer.withOpacity(0.1), ], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(12), border: Border.all( color: colorScheme.outline.withOpacity(0.2), width: 1, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( color: colorScheme.primary, borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.rocket_launch, size: 14, color: colorScheme.onPrimary, ), const SizedBox(width: 4), Text( 'v${info.versionName} +${info.versionBuildNumber}', style: TextStyle( color: colorScheme.onPrimary, fontSize: 14, fontWeight: FontWeight.bold, ), ), ], ), ), if (info.isForceUpdate) ...[ const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: colorScheme.error, borderRadius: BorderRadius.circular(4), ), child: Text( '强制更新', style: TextStyle( color: colorScheme.onError, fontSize: 10, fontWeight: FontWeight.w500, ), ), ), ], ], ), const SizedBox(height: 8), // 版本对比和大小信息 Row( children: [ if (info.apkSize != null) ...[ Expanded( child: _buildInfoChip( context, icon: Icons.file_download, label: '新版体积', /// 格式化文件大小 value: UpgradeUtils.formatBytes(info.apkSize!), colorScheme: colorScheme, ), ), const SizedBox(width: 8), ], Expanded( child: _buildInfoChip( context, icon: Icons.update, label: '已安装版本', // 显示版本号 value: '${info.currentVersionName} +${info.currentBuildNumber}', colorScheme: colorScheme, ), ), ], ), ], ), ); } Widget _buildInfoChip( BuildContext context, { required IconData icon, required String label, required String value, required ColorScheme colorScheme, }) { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: colorScheme.surface, borderRadius: BorderRadius.circular(8), border: Border.all( color: colorScheme.outline.withOpacity(0.2), width: 0.5, ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( icon, size: 12, color: colorScheme.onSurface.withOpacity(0.6), ), const SizedBox(width: 4), Text( label, style: TextStyle( fontSize: 10, color: colorScheme.onSurface.withOpacity(0.6), ), ), ], ), const SizedBox(height: 4), Text( value, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: colorScheme.onSurface, ), ), ], ), ); } Widget _buildUpdateContent(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final parsedContent = AppUpgradeSimple.parseUpdateContent(info.updateContent); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.new_releases_outlined, size: 16, color: colorScheme.primary, ), const SizedBox(width: 6), Text( '更新内容', style: TextStyle( fontSize: 14, fontWeight: FontWeight.bold, color: colorScheme.onSurface, ), ), ], ), const SizedBox(height: 6), Container( width: double.infinity, constraints: const BoxConstraints(maxHeight: 300), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colorScheme.surfaceContainerHighest.withOpacity(0.3), borderRadius: BorderRadius.circular(8), border: Border.all( color: colorScheme.outline.withOpacity(0.2), width: 0.5, ), ), child: SingleChildScrollView( child: !parsedContent.hasBodyItems && !parsedContent.hasHeader ? _buildRichText( info.updateContent, colorScheme, ) : Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ if (parsedContent.hasHeader) ...[ _buildRichText( parsedContent.header!, colorScheme, ), if (parsedContent.hasBodyItems) const SizedBox(height: 10), ], ...parsedContent.bodyItems.asMap().entries.map((entry) { final index = entry.key; final item = entry.value; return Container( width: double.infinity, margin: EdgeInsets.only( bottom: index < parsedContent.bodyItems.length - 1 ? 8 : 0, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ if (!item.hasLeadingMarker) ...[ Container( margin: const EdgeInsets.only(top: 6), width: 4, height: 4, decoration: BoxDecoration( color: colorScheme.primary, shape: BoxShape.circle, ), ), const SizedBox(width: 8), ], Expanded( child: _buildRichText(item.text, colorScheme), ), ], ), ); }), ], ), ), ), ], ); } Widget _buildRichText(String content, ColorScheme colorScheme) { // 递归解析文本,支持嵌套格式 final spans = _parseRichText(content, colorScheme); if (spans.isEmpty || spans.length == 1 && spans[0].text == content) { return Text( content, style: TextStyle( fontSize: 13, color: colorScheme.onSurface.withOpacity(0.8), height: 1.4, ), softWrap: true, maxLines: null, ); } return RichText( text: TextSpan( style: TextStyle( fontSize: 13, color: colorScheme.onSurface.withOpacity(0.8), height: 1.4, ), children: spans, ), softWrap: true, maxLines: null, ); } /// 递归解析富文本,支持嵌套格式 /// /// 支持的格式: /// - `**粗体**` /// - `__斜体__` /// - `` `代码` `` /// - `[高亮]` /// /// 采用自定义解析器,逐字符扫描并使用递归解析子内容,从而支持任意嵌套。 List _parseRichText(String content, ColorScheme colorScheme) { final styles = _RichTextStyles(colorScheme); final result = _parseRichTextInternal(content, styles, 0, null); return result.spans; } _RichTextParseResult _parseRichTextInternal( String text, _RichTextStyles styles, int startIndex, String? endToken, ) { final spans = []; final buffer = StringBuffer(); var index = startIndex; void flushBuffer() { if (buffer.isEmpty) return; spans.add(TextSpan( text: buffer.toString(), )); buffer.clear(); } while (index < text.length) { // 如果遇到结束标记,返回到上层 if (endToken != null && text.startsWith(endToken, index)) { flushBuffer(); return _RichTextParseResult(spans, index + endToken.length, true); } final currentChar = text[index]; var handled = false; // [高亮] if (currentChar == '[') { final innerResult = _parseRichTextInternal(text, styles, index + 1, ']'); if (innerResult.closed) { flushBuffer(); final innerText = text.substring(index + 1, innerResult.nextIndex - 1); spans.addAll(_applyStyleToSpans( innerResult.spans, styles.highlightStyle, innerText, )); index = innerResult.nextIndex; handled = true; } else { // 无法找到匹配的 ],按普通文本处理 buffer.write(currentChar); index++; handled = true; } } // `代码` else if (currentChar == '`') { final closingIndex = text.indexOf('`', index + 1); if (closingIndex != -1) { flushBuffer(); final codeText = text.substring(index + 1, closingIndex); spans.add(TextSpan( text: codeText, style: styles.codeStyle, )); index = closingIndex + 1; handled = true; } else { // 没有找到闭合的 `,按普通文本处理 buffer.write(currentChar); index++; handled = true; } } // **粗体** else if (text.startsWith('**', index)) { final innerResult = _parseRichTextInternal(text, styles, index + 2, '**'); if (innerResult.closed) { flushBuffer(); final innerText = text.substring(index + 2, innerResult.nextIndex - 2); spans.addAll(_applyStyleToSpans( innerResult.spans, styles.boldStyle, innerText, )); index = innerResult.nextIndex; handled = true; } else { // 没有匹配的 ** buffer.write('**'); index += 2; handled = true; } } // __斜体__ else if (text.startsWith('__', index)) { final innerResult = _parseRichTextInternal(text, styles, index + 2, '__'); if (innerResult.closed) { flushBuffer(); final innerText = text.substring(index + 2, innerResult.nextIndex - 2); spans.addAll(_applyStyleToSpans( innerResult.spans, styles.italicStyle, innerText, )); index = innerResult.nextIndex; handled = true; } else { buffer.write('__'); index += 2; handled = true; } } if (!handled) { buffer.write(currentChar); index++; } } flushBuffer(); return _RichTextParseResult(spans, index, false); } List _applyStyleToSpans( List spans, TextStyle style, String fallbackText) { if (spans.isEmpty) { return [ TextSpan( text: fallbackText, style: style, ), ]; } return spans.map((span) => _mergeTextSpanStyle(span, style)).toList(); } TextSpan _mergeTextSpanStyle(TextSpan span, TextStyle style) { final mergedChildren = span.children ?.map((child) => child is TextSpan ? _mergeTextSpanStyle(child, style) : child) .toList(); final mergedStyle = span.style != null ? style.merge(span.style) : style; return TextSpan( text: span.text, style: mergedStyle, children: mergedChildren, ); } Widget _buildEnhancedDownloadProgress( BuildContext context, ColorScheme colorScheme) { final bool showRetryButton = _downloadedFilePath != null && !_isDownloading && !_isInstalling && !_isWaitingForInstallation && _downloadProgress >= 1.0 && _statusText != '安装成功'; debugPrint('重试按钮显示条件检查:'); debugPrint(' _downloadedFilePath != null: ${_downloadedFilePath != null}'); debugPrint(' !_isDownloading: ${!_isDownloading}'); debugPrint(' !_isInstalling: ${!_isInstalling}'); debugPrint(' !_isWaitingForInstallation: ${!_isWaitingForInstallation}'); debugPrint(' _downloadProgress >= 1.0: ${_downloadProgress >= 1.0}'); debugPrint(' _statusText != "安装成功": ${_statusText != '安装成功'}'); debugPrint(' showRetryButton: $showRetryButton'); debugPrint(' 当前状态: $_statusText'); return Column( children: [ const SizedBox(height: 20), GestureDetector( onTap: _shouldShowRetryOptions() ? _retryInstall : null, child: Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: colorScheme.primaryContainer.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all( color: colorScheme.primary.withOpacity(0.2), width: 1, ), boxShadow: _shouldShowRetryOptions() ? [ BoxShadow( color: colorScheme.primary.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2), ), ] : null, ), child: Column( children: [ Row( children: [ AnimatedContainer( duration: const Duration(milliseconds: 300), padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: _getStatusColor(colorScheme).withOpacity(0.1), shape: BoxShape.circle, ), child: AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: Icon( _getStatusIcon(), key: ValueKey(_statusText), color: _getStatusColor(colorScheme), size: 20, ), ), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AnimatedSwitcher( duration: const Duration(milliseconds: 300), child: Text( _statusText, key: ValueKey(_statusText), style: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, color: colorScheme.onSurface, ), ), ), const SizedBox(height: 2), if (_isDownloading || _downloadProgress < 1.0) Text( '${(_downloadProgress * 100).toStringAsFixed(1)}%', style: TextStyle( fontSize: 12, color: colorScheme.primary, fontWeight: FontWeight.bold, ), ), if (_shouldShowRetryOptions()) Text( _getClickHintText(), style: TextStyle( fontSize: 11, color: colorScheme.onSurface.withOpacity(0.6), ), ), if (_statusText == '请完成安装') Padding( padding: const EdgeInsets.only(top: 12), child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colorScheme.primaryContainer .withOpacity(0.2), borderRadius: BorderRadius.circular(8), border: Border.all( color: colorScheme.primary.withOpacity(0.3), width: 1, ), ), child: Column( children: [ Icon( Icons.touch_app, color: colorScheme.primary, size: 24, ), const SizedBox(height: 8), Text( '请在系统安装界面完成操作', style: TextStyle( fontSize: 13, color: colorScheme.onSurface, fontWeight: FontWeight.w500, ), textAlign: TextAlign.center, ), const SizedBox(height: 4), Text( '系统将自动检测安装结果', style: TextStyle( fontSize: 11, color: colorScheme.onSurface .withOpacity(0.7), ), textAlign: TextAlign.center, ), ], ), ), ), ], ), ), if (_shouldShowRetryOptions()) Container( margin: const EdgeInsets.only(left: 8), child: ElevatedButton.icon( onPressed: _retryInstall, icon: Icon(_getRetryButtonIcon(), size: 16), label: Text(_getRetryButtonText(), style: const TextStyle(fontSize: 12)), style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 8), minimumSize: Size.zero, tapTargetSize: MaterialTapTargetSize.shrinkWrap, backgroundColor: _getRetryButtonColor(colorScheme), ), ), ), ], ), if (_isDownloading || (_downloadProgress > 0 && _downloadProgress < 1.0)) ...[ const SizedBox(height: 16), ClipRRect( borderRadius: BorderRadius.circular(4), child: Stack( children: [ Container( height: 8, width: double.infinity, decoration: BoxDecoration( color: colorScheme.outline.withOpacity(0.1), borderRadius: BorderRadius.circular(4), ), ), AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeOut, height: 8, width: double.infinity, alignment: Alignment.centerLeft, child: FractionallySizedBox( widthFactor: _downloadProgress.clamp(0.0, 1.0), child: Container( decoration: BoxDecoration( gradient: LinearGradient( colors: [ colorScheme.primary, colorScheme.primary.withOpacity(0.8), ], begin: Alignment.centerLeft, end: Alignment.centerRight, ), borderRadius: BorderRadius.circular(4), boxShadow: [ BoxShadow( color: colorScheme.primary.withOpacity(0.3), blurRadius: 4, offset: const Offset(0, 2), ), ], ), ), ), ), ], ), ), ], ], ), ), ), ], ); } IconData _getStatusIcon() { if (_isInstalling) { return Icons.install_mobile; } else if (_isDownloading) { return Icons.download_for_offline; } else if (_statusText == '安装成功') { return Icons.check_circle; } else if (_statusText == '请完成安装') { return Icons.touch_app; } else if (_statusText == '等待安装中') { return Icons.hourglass_bottom; } else if (_statusText == '等待确认中') { return Icons.help_outline; } else if (_statusText == '等待超时') { return Icons.timer_off; } else if (_statusText == '安装被取消') { return Icons.cancel_outlined; } else if (_statusText == '安装超时' || _statusText == '检测失败') { return Icons.schedule; } else if (_statusText == '安装失败' || _statusText == '安装异常' || _statusText == '权限被拒绝') { return Icons.error_outline; } else if (_downloadProgress >= 1.0) { return Icons.check_circle_outline; } else { return Icons.download_for_offline; } } Color _getStatusColor(ColorScheme colorScheme) { if (_isInstalling) { return colorScheme.secondary; } else if (_statusText == '安装成功') { return colorScheme.tertiary; } else if (_statusText == '请完成安装') { return colorScheme.secondary; } else if (_statusText == '等待安装中') { return colorScheme.secondary.withOpacity(0.9); } else if (_statusText == '等待确认中') { return Colors.orange; } else if (_statusText == '等待超时') { return colorScheme.error.withOpacity(0.7); } else if (_statusText == '安装被取消') { return colorScheme.secondary.withOpacity(0.8); } else if (_statusText == '安装超时' || _statusText == '检测失败') { return colorScheme.secondary.withOpacity(0.7); } else if (_statusText == '安装失败' || _statusText == '安装异常' || _statusText == '权限被拒绝') { return colorScheme.error; } else if (_downloadProgress >= 1.0) { return colorScheme.tertiary; } else { return colorScheme.primary; } } IconData _getRetryButtonIcon() { if (_statusText == '权限被拒绝' && config.requireInstallPermission) { return Icons.settings; } else if (_statusText == '安装失败' || _statusText == '安装异常' || _statusText == '安装超时' || _statusText == '安装被取消' || _statusText == '检测失败' || _statusText == '等待安装中' || _statusText == '等待确认中' || _statusText == '等待超时') { return Icons.refresh; } else if (_statusText == '请完成安装') { return Icons.launch; } else { return Icons.install_mobile; } } String _getRetryButtonText() { if (_statusText == '权限被拒绝' && config.requireInstallPermission) { return '设置'; } else if (_statusText == '安装失败' || _statusText == '安装异常' || _statusText == '安装超时' || _statusText == '安装被取消' || _statusText == '检测失败' || _statusText == '等待超时') { return '重试'; } else if (_statusText == '等待安装中' || _statusText == '等待确认中') { return '重新安装'; } else if (_statusText == '请完成安装') { return '重新安装'; } else { return '安装'; } } Color? _getRetryButtonColor(ColorScheme colorScheme) { if (_statusText == '权限被拒绝') { return colorScheme.secondary; } else if (_statusText == '安装失败' || _statusText == '安装异常') { return colorScheme.error; } else if (_statusText == '安装超时' || _statusText == '安装被取消' || _statusText == '检测失败' || _statusText == '等待安装中') { return colorScheme.secondary.withOpacity(0.8); } else if (_statusText == '请完成安装') { return colorScheme.secondary; } else { return null; } } bool _shouldShowRetryOptions() { return _statusText == '安装被取消' || _statusText == '安装失败' || _statusText == '安装异常' || (_statusText == '权限被拒绝' && config.requireInstallPermission) || _statusText == '安装超时' || _statusText == '检测失败' || _statusText == '等待安装中' || _statusText == '等待确认中' || _statusText == '等待超时' || (_downloadedFilePath != null && !_isDownloading && !_isInstalling && !_isWaitingForInstallation && _downloadProgress >= 1.0 && _statusText != '安装成功'); } String _getClickHintText() { switch (_statusText) { case '请完成安装': return '完成后请点击确认按钮'; case '等待安装中': return '点击重新安装'; case '等待确认中': return '请点击确认按钮'; case '等待超时': return '点击重新安装'; case '安装被取消': return '点击重新安装'; case '权限被拒绝': return config.requireInstallPermission ? '点击打开设置' : '点击重试'; case '安装超时': return '点击重新尝试'; default: return '点击区域重试'; } } Future _handleAction() async { if (Platform.isAndroid) { await _handleAndroidAction(); } else if (Platform.isIOS) { await _handleIosAction(context); } else { showToast('Unsupported platform'); } } Future _handleIosAction(BuildContext context) async { if (info.appStoreUrl != null) { final success = await _plugin.goToAppStore(info.appStoreUrl!, context: context); if (!success) { showToast('无法打开App Store,请稍后重试'); } // 移除关闭弹窗代码,始终不关闭 } else { showToast('App Store URL is not available.'); } } Future _handleAndroidAction() async { final List availableMethods = []; final supported = info.supportedMethods; if (supported.contains(AppUpgradeMethod.market)) { availableMethods.add(AppUpgradeMethod.market); } if (info.downloadUrl != null) { if (supported.contains(AppUpgradeMethod.browser)) { availableMethods.add(AppUpgradeMethod.browser); } if (supported.contains(AppUpgradeMethod.inApp)) { availableMethods.add(AppUpgradeMethod.inApp); } } debugPrint('可用更新方式: $availableMethods'); if (availableMethods.isEmpty) { showToast('未找到可用的更新方式'); return; } if (availableMethods.length == 1) { final method = availableMethods.first; switch (method) { case AppUpgradeMethod.market: _handleMarketAction(); break; case AppUpgradeMethod.browser: _goToBrowser(); break; case AppUpgradeMethod.inApp: await _startDownloadAndInstall(); break; } return; } await _showDownloadChoiceSheet(availableMethods); } Future _performMarketAction() async { // 始终检查设备已安装的应用市场 final installedMarkets = await _plugin.getInstalledMarkets(); debugPrint('设备已安装的应用市场: $installedMarkets'); final hasWhitelist = info.appMarkets?.isNotEmpty ?? false; if (hasWhitelist) { debugPrint('配置的应用市场白名单: ${info.appMarkets}'); // 筛选出设备上已安装且在白名单中的应用市场 final availableMarkets = info.appMarkets! .where((market) => installedMarkets.contains(market.name)) .toList(); debugPrint('可用的应用市场: $availableMarkets'); if (availableMarkets.isEmpty) { // 没有匹配的应用市场,仅提示用户 showToast('当前设备的应用市场不在支持列表中,请选择其他方式更新'); return; } } else { // 未配置白名单,但也要检查设备是否有应用市场 if (installedMarkets.isEmpty) { showToast('当前设备未安装应用市场'); return; } } // 跳转到应用市场(使用设备默认的 market:// 协议) final appInfo = await _plugin.getAppInfo(); final pkg = appInfo['packageName'] ?? ''; if (pkg.isNotEmpty) { final success = await _plugin.goToAppStore('market://details?id=$pkg', context: context); if (!success) { showToast('当前APP没有上架当前设备对应的应用市场,请选择其他方式更新'); } } else { showToast('无法获取应用包名'); } } Future _handleMarketAction() async { if (!mounted) return; await _performMarketAction(); // 移除关闭弹窗代码 } Future _showDownloadChoiceSheet( List availableMethods) async { if (!mounted) return; final choice = await showModalBottomSheet( context: context, isScrollControlled: false, useRootNavigator: true, isDismissible: !info.isForceUpdate, enableDrag: !info.isForceUpdate, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(20)), ), builder: (ctx) { return SafeArea( child: Padding( padding: const EdgeInsets.fromLTRB(16, 8, 16, 16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( width: 40, height: 4, margin: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( color: Colors.grey.shade300, borderRadius: BorderRadius.circular(2), ), ), Padding( padding: const EdgeInsets.only(bottom: 8.0), child: Stack( alignment: Alignment.center, children: [ Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( child: Text('选择更新方式', textAlign: TextAlign.center, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold))), ], ), Positioned( right: -12, child: IconButton( icon: const Icon(Icons.close), onPressed: () => Navigator.of(ctx).pop(), ), ), ], ), ), if (availableMethods.contains(AppUpgradeMethod.market)) ListTile( leading: const Icon(Icons.storefront_outlined), title: const Text('前往应用市场更新', style: TextStyle(fontSize: 16)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.market), ), if (availableMethods.contains(AppUpgradeMethod.inApp)) ListTile( leading: const Icon(Icons.system_update), title: const Text('APP内更新', style: TextStyle(fontSize: 16)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.inApp), ), if (availableMethods.contains(AppUpgradeMethod.browser)) ListTile( leading: const Icon(Icons.download_for_offline_outlined), title: const Text('前往浏览器下载安装包', textAlign: TextAlign.left, style: TextStyle(fontSize: 16)), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12)), onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.browser), ), const Divider(height: 24), SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 12), backgroundColor: Colors.white, foregroundColor: Theme.of(context).textTheme.bodyLarge?.color, elevation: 2, shadowColor: Colors.grey.withOpacity(0.5), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300), ), ), onPressed: () => Navigator.of(ctx).pop(), child: const Text('取消', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500)), ), ), ], ), ), ); }, ); // 不再关闭弹窗,即使用户选择了更新方式 if (choice == AppUpgradeMethod.market) { await _performMarketAction(); return; } if (choice == AppUpgradeMethod.inApp && !_isDownloading) { await _startDownloadAndInstall(); return; } if (choice == AppUpgradeMethod.browser && !_isDownloading) { _goToBrowser(); } } } class _SimpleUpgradeDialog extends StatefulWidget { final UpgradeInfo info; final bool autoInstall; final VoidCallback? onUpdateLater; final void Function(String) showToast; final UpgradeConfig config; const _SimpleUpgradeDialog({ required this.info, required this.autoInstall, this.onUpdateLater, required this.showToast, required this.config, }); @override State<_SimpleUpgradeDialog> createState() => _SimpleUpgradeDialogState(); } class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _UpgradeDialogLogic, WidgetsBindingObserver { @override UpgradeInfo get info => widget.info; @override void Function(String) get showToast => widget.showToast; VoidCallback? get onUpdateLater => widget.onUpdateLater; @override bool get autoInstall => widget.autoInstall; @override UpgradeConfig get config => widget.config; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); initUpgradeLogic(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); disposeUpgradeLogic(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { onAppLifecycleStateChanged(state); } @override Widget build(BuildContext context) { return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), insetPadding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24), child: Container( width: 320, constraints: const BoxConstraints( maxHeight: 600, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), child: Row( children: [ Icon( Icons.system_update, color: Theme.of(context).colorScheme.primary, ), const SizedBox(width: 8), const Expanded( child: Text( '发现新版本', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, ), ), ), ], ), ), Flexible( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 0, 20, 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), _buildVersionInfoCard( context, Theme.of(context).colorScheme), const SizedBox(height: 16), _buildUpdateContent(context), if (_isDownloading || _downloadedFilePath != null) _buildEnhancedDownloadProgress( context, Theme.of(context).colorScheme), ], ), ), ), if (!_isDownloading) Container( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( onPressed: () { Navigator.of(context).pop(); onUpdateLater?.call(); }, child: const Text('稍后更新'), ), const SizedBox(width: 8), ElevatedButton( onPressed: _handleAction, child: Text(Platform.isAndroid ? '立即更新' : '前往更新'), ), ], ), ), ], ), ), ); } } class _ForceUpgradeDialog extends StatefulWidget { final UpgradeInfo info; final bool autoInstall; final UpgradeConfig config; const _ForceUpgradeDialog({ required this.info, required this.autoInstall, required this.config, }); @override State<_ForceUpgradeDialog> createState() => _ForceUpgradeDialogState(); } class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeDialogLogic, WidgetsBindingObserver { @override UpgradeInfo get info => widget.info; @override void Function(String) get showToast => (message) => AppUpgradeSimple.instance._showToast(message, context, widget.config); @override bool get autoInstall => widget.autoInstall; @override UpgradeConfig get config => widget.config; @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); initUpgradeLogic(); } @override void dispose() { WidgetsBinding.instance.removeObserver(this); disposeUpgradeLogic(); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { onAppLifecycleStateChanged(state); } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async => false, child: Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), insetPadding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24), child: Container( width: 320, constraints: const BoxConstraints( maxHeight: 700, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Expanded( child: Text( '发现新版本', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600), textAlign: TextAlign.center, ), ), ], ), ), Flexible( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 0, 20, 16), child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 10), _buildVersionInfoCard( context, Theme.of(context).colorScheme), const SizedBox(height: 16), _buildUpdateContent(context), if (_isDownloading || _downloadedFilePath != null) _buildEnhancedDownloadProgress( context, Theme.of(context).colorScheme), ], ), ), ), if (!_isDownloading) Container( padding: const EdgeInsets.fromLTRB(16, 0, 16, 12), child: SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _handleAction, child: Text(Platform.isAndroid ? '立即更新' : '前往更新'), ), ), ), ], ), ), ), ); } } /// 使用 Overlay 显示的 Toast Widget(不依赖 Scaffold) class _ToastWidget extends StatelessWidget { final String message; const _ToastWidget({required this.message}); @override Widget build(BuildContext context) { return Positioned( top: MediaQuery.of(context).padding.top + 16, left: 16, right: 16, child: Material( color: Colors.transparent, child: SafeArea( child: IgnorePointer( child: TweenAnimationBuilder( duration: const Duration(milliseconds: 300), tween: Tween(begin: 0.0, end: 1.0), builder: (context, value, child) { return Opacity( opacity: value, child: Transform.translate( offset: Offset(0, -20 * (1 - value)), child: child, ), ); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), decoration: BoxDecoration( color: Colors.black87, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.2), blurRadius: 8, offset: const Offset(0, 4), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Text( message, style: const TextStyle( color: Colors.white, fontSize: 14, ), textAlign: TextAlign.center, ), ), ], ), ), ), ), ), ), ); } } class _RichTextStyles { _RichTextStyles(ColorScheme colorScheme) : boldStyle = TextStyle( fontSize: 13, color: colorScheme.onSurface.withOpacity(0.9), height: 1.4, fontWeight: FontWeight.bold, ), italicStyle = TextStyle( fontSize: 13, color: colorScheme.onSurface.withOpacity(0.9), height: 1.4, fontStyle: FontStyle.italic, ), codeStyle = TextStyle( fontSize: 12, color: colorScheme.primary, height: 1.4, fontFamily: 'monospace', backgroundColor: colorScheme.primaryContainer.withOpacity(0.2), ), highlightStyle = TextStyle( fontSize: 13, color: colorScheme.primary, height: 1.4, fontWeight: FontWeight.w600, ); final TextStyle boldStyle; final TextStyle italicStyle; final TextStyle codeStyle; final TextStyle highlightStyle; } class _RichTextParseResult { final List spans; final int nextIndex; final bool closed; const _RichTextParseResult(this.spans, this.nextIndex, this.closed); } final RegExp _contentLineMarkerRegExp = RegExp( r'^(?:(?:\d+|[一二三四五六七八九十]+)[\.\、\.\)](?=\s|[^\d]|$)\s*|(?:[-*+•●○▪▫■□◆◇])(?=\s|$)\s*)', ); class _UpgradeContentHeaderExtraction { const _UpgradeContentHeaderExtraction({ required this.bodyLines, this.header, this.hasExplicitHeaderBlock = false, }); final List bodyLines; final String? header; final bool hasExplicitHeaderBlock; } _UpgradeContentHeaderExtraction _extractUpgradeContentHeader( List rawLines, ) { const startMarker = '/{'; const endMarker = '/}'; if (rawLines.isEmpty) { return const _UpgradeContentHeaderExtraction(bodyLines: []); } final firstLine = rawLines.first.trim(); if (!firstLine.startsWith(startMarker)) { return _UpgradeContentHeaderExtraction( bodyLines: List.from(rawLines), ); } final headerLines = []; final bodyLines = []; var currentLine = firstLine.substring(startMarker.length).trimLeft(); var endFound = false; var endLineIndex = 0; var pendingBlankLine = false; void addHeaderLine(String value) { final normalizedLine = value.trim(); if (normalizedLine.isEmpty) { pendingBlankLine = headerLines.isNotEmpty; return; } if (pendingBlankLine && headerLines.isNotEmpty && headerLines.last.isNotEmpty) { headerLines.add(''); } headerLines.add(normalizedLine); pendingBlankLine = false; } bool closeHeader(String value) { final endIndex = value.indexOf(endMarker); if (endIndex < 0) { return false; } addHeaderLine(value.substring(0, endIndex)); final trailingBody = value.substring(endIndex + endMarker.length).trim(); if (trailingBody.isNotEmpty) { bodyLines.add(trailingBody); } endFound = true; return true; } if (!closeHeader(currentLine) && currentLine.isNotEmpty) { addHeaderLine(currentLine); } for (var index = 1; index < rawLines.length && !endFound; index++) { currentLine = rawLines[index].trim(); endLineIndex = index; if (closeHeader(currentLine)) { break; } addHeaderLine(currentLine); } if (!endFound) { return _UpgradeContentHeaderExtraction( bodyLines: List.from(rawLines), ); } if (endLineIndex + 1 < rawLines.length) { bodyLines.addAll(rawLines.sublist(endLineIndex + 1)); } return _UpgradeContentHeaderExtraction( bodyLines: bodyLines, header: headerLines.isEmpty ? null : headerLines.join('\n'), hasExplicitHeaderBlock: true, ); }