import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:url_launcher/url_launcher.dart'; import 'app_upgrade_plugin_platform_interface.dart'; import 'core/permission_helper.dart'; import 'core/upgrade_utils.dart'; import 'models/app_upgrade_method.dart'; import 'models/app_upgrade_version.dart'; import 'models/upgrade_info.dart'; import 'widgets/market_selection_dialog.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) { return AppUpgradePluginPlatform.instance.goToAppStore(url); } Future> getAppInfo() { return AppUpgradePluginPlatform.instance.getAppInfo(); } /// 获取下载路径 Future getDownloadPath() { return AppUpgradePluginPlatform.instance.getDownloadPath(); } } /// 升级配置选项 class UpgradeConfig { /// 是否显示无更新提示 final bool showNoUpdateToast; /// 是否自动下载 final bool autoDownload; /// 是否自动安装 final bool autoInstall; /// 连接超时时间(秒) final int connectionTimeout; /// 下载超时时间(秒) final int downloadTimeout; /// 安装检测超时时间(秒) final int installTimeout; /// 是否启用调试日志 final bool enableDebugLog; /// 自定义Toast显示函数 final void Function(String message)? customToast; /// 是否需要获取安装未知应用权限(默认false,直接安装) final bool requireInstallPermission; const UpgradeConfig({ this.showNoUpdateToast = true, this.autoDownload = false, this.autoInstall = false, this.connectionTimeout = 30, this.downloadTimeout = 300, this.installTimeout = 45, this.enableDebugLog = true, this.customToast, this.requireInstallPermission = false, // 默认不需要权限 }); /// 快速配置:自动更新(不需要权限) static const UpgradeConfig auto = UpgradeConfig( autoDownload: true, autoInstall: true, requireInstallPermission: false, ); /// 快速配置:静默检查 static const UpgradeConfig silent = UpgradeConfig( showNoUpdateToast: false, enableDebugLog: false, requireInstallPermission: false, ); /// 快速配置:开发模式(详细日志 + 较短超时) static const UpgradeConfig development = UpgradeConfig( enableDebugLog: true, installTimeout: 30, connectionTimeout: 10, requireInstallPermission: false, ); /// 快速配置:生产模式(静默 + 较长超时) static const UpgradeConfig production = UpgradeConfig( showNoUpdateToast: false, enableDebugLog: false, installTimeout: 60, connectionTimeout: 30, requireInstallPermission: false, ); /// 快速配置:需要权限模式(传统方式) static const UpgradeConfig withPermission = UpgradeConfig( requireInstallPermission: true, ); } /// 简化版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; } /// 快速配置:一键设置自动更新 void enableAutoUpdate() { _config = UpgradeConfig.auto; } /// 快速配置:一键设置静默检查 void enableSilentCheck() { _config = UpgradeConfig.silent; } /// 检查网络连接状态 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'); } } /// 一键检查更新(支持配置) Future checkUpdate({ required BuildContext context, required Future Function() future, bool? showNoUpdateToast, bool? autoDownload, bool? autoInstall, VoidCallback? onComplete, UpgradeConfig? config, }) async { // 使用传入的配置或默认配置 final effectiveConfig = config ?? _config; final finalShowNoUpdateToast = showNoUpdateToast ?? effectiveConfig.showNoUpdateToast; final finalAutoDownload = autoDownload ?? effectiveConfig.autoDownload; final finalAutoInstall = autoInstall ?? effectiveConfig.autoInstall; try { assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用'); // 1. 获取服务器版本信息 final serverInfo = await future(); if (serverInfo == null) { // 获取失败或无数据,视作无更新 if (effectiveConfig.enableDebugLog) { debugPrint('🔍 检查更新结果: 未返回版本信息'); } onComplete?.call(); return; } if (effectiveConfig.enableDebugLog) { debugPrint('🔍 获取到服务器版本: $serverInfo'); } // 2. 获取当前App信息 final appInfo = await _plugin.getAppInfo(); final currentVersionName = appInfo['version'] ?? ''; final currentBuildNumber = int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0; if (effectiveConfig.enableDebugLog) { debugPrint('📱 当前版本: $currentVersionName (Build: $currentBuildNumber)'); } // 3. 比较版本并构建 UpgradeInfo bool hasUpdate = false; if (serverInfo.versionBuildNumber > 0) { // 优先比较 buildNumber if (serverInfo.versionBuildNumber > currentBuildNumber) { hasUpdate = true; } else if (serverInfo.versionBuildNumber == currentBuildNumber) { // buildNumber 相同,比较版本名 if (_compareVersionStrings(serverInfo.versionName, currentVersionName) > 0) { hasUpdate = true; } } } else { // 只比较版本名 if (_compareVersionStrings(serverInfo.versionName, currentVersionName) > 0) { hasUpdate = true; } } if (effectiveConfig.enableDebugLog) { debugPrint('📊 版本比较结果: ${hasUpdate ? "有新版本" : "已是最新"}'); } // 构建内部使用的 UpgradeInfo final info = UpgradeInfo( hasUpdate: hasUpdate, isForceUpdate: serverInfo.isForce, versionName: serverInfo.versionName, versionBuildNumber: serverInfo.versionBuildNumber, 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], ); if (!info.hasUpdate) { if (finalShowNoUpdateToast && context.mounted) { _showToast('已是最新版本', effectiveConfig); } onComplete?.call(); return; } await _showUpgradeDialog( context: context, info: info, autoDownload: finalAutoDownload, autoInstall: finalAutoInstall, onComplete: onComplete, config: effectiveConfig, ); } catch (e) { debugPrint('检查更新失败: $e'); if (context.mounted) { _showToast('检查更新遇到问题', effectiveConfig); } onComplete?.call(); } } /// 预下载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(); } /// 检查当前应用的版本信息(用于安装状态检测) Future> getCurrentAppInfo() async { try { return await _plugin.getAppInfo(); } catch (e) { debugPrint('获取应用信息失败: $e'); return {}; } } /// 精确检测应用是否已安装(通过包名) Future isPackageInstalled(String packageName) async { try { final appInfo = await getAppInfo(); final currentPackage = appInfo['packageName'] ?? ''; debugPrint('检查包安装状态: 当前包名=$currentPackage, 目标包名=$packageName'); if (currentPackage == packageName) { return true; } return false; } catch (e) { debugPrint('检查包安装状态失败: $e'); return false; } } /// 比较版本号是否已更新 Future isVersionUpdated(String targetVersion, int? targetBuildNumber) async { try { final appInfo = await getCurrentAppInfo(); final currentVersion = appInfo['version'] ?? ''; final currentBuildNumber = int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0; debugPrint('版本对比: 当前版本=$currentVersion, 目标版本=$targetVersion'); debugPrint('构建号对比: 当前构建号=$currentBuildNumber, 目标构建号=$targetBuildNumber'); if (targetBuildNumber != null && targetBuildNumber > 0) { if (currentBuildNumber < targetBuildNumber) { return false; } else if (currentBuildNumber > targetBuildNumber) { return true; } return _compareVersionStrings(currentVersion, targetVersion) >= 0; } return _compareVersionStrings(currentVersion, targetVersion) >= 0; } catch (e) { debugPrint('版本对比失败: $e'); return false; } } 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 autoDownload, required bool autoInstall, VoidCallback? onComplete, UpgradeConfig? config, }) { final effectiveConfig = config ?? _config; return showDialog( context: context, barrierDismissible: !info.isForceUpdate, builder: (context) { if (info.isForceUpdate) { return _ForceUpgradeDialog( info: info, config: effectiveConfig, ); } else { return _SimpleUpgradeDialog( info: info, autoDownload: autoDownload, autoInstall: autoInstall, onComplete: onComplete, config: effectiveConfig, showToast: (message) => _showToast(message, effectiveConfig), ); } }, ); } /// 显示Toast提示 void _showToast(String message, [UpgradeConfig? config]) { final effectiveConfig = config ?? _config; if (effectiveConfig.customToast != null) { effectiveConfig.customToast!(message); } else { Fluttertoast.showToast( msg: message, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.CENTER, backgroundColor: Colors.black87, textColor: Colors.white, fontSize: 14.0, ); } } 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; VoidCallback? get onComplete; bool get autoDownload; bool get autoInstall; UpgradeConfig get config; void initUpgradeLogic() { if (autoDownload && Platform.isAndroid) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _startDownloadAndInstall(); } }); } } 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) { isUpdated = currentBuildNumber >= info.versionBuildNumber; debugPrint('📊 构建号比较: $currentBuildNumber >= ${info.versionBuildNumber} = $isUpdated'); } else { isUpdated = currentVersion == info.versionName; debugPrint('📊 版本号比较: $currentVersion == ${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(); } onComplete?.call(); }); } 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( 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: [ Expanded( child: _buildInfoChip( context, icon: Icons.update, label: '当前版本', // 显示版本号 value: '${info.currentVersionName} +${info.currentBuildNumber}', colorScheme: colorScheme, ), ), ], ), if (info.apkSize != null) ...[ const SizedBox(height: 8), Row( children: [ Expanded( child: _buildInfoChip( context, icon: Icons.file_download, label: '大小', value: formatBytes(info.apkSize!), colorScheme: colorScheme, ), ), ], ), ], ], ), ); } Widget _buildInfoChip( BuildContext context, { required IconData icon, required String label, required String value, required ColorScheme colorScheme, }) { return Container( padding: const EdgeInsets.all(8), 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: 2), Text( value, style: TextStyle( fontSize: 12, fontWeight: FontWeight.w500, color: colorScheme.onSurface, ), ), ], ), ); } Widget _buildUpdateContent(BuildContext context) { final colorScheme = Theme.of(context).colorScheme; final changeItems = info.updateContent.split(RegExp(r'\r?\n')).map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); 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: 12), Container( width: double.infinity, constraints: const BoxConstraints(maxHeight: 200), 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: changeItems.isEmpty ? _buildRichText( info.updateContent, colorScheme, ) : Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: changeItems.asMap().entries.map((entry) { final index = entry.key; final line = entry.value; return Container( width: double.infinity, margin: EdgeInsets.only( bottom: index < changeItems.length - 1 ? 8 : 0, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ 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( line, colorScheme, ), ), ], ), ); }).toList(), ), ), ), ], ); } Widget _buildRichText(String content, ColorScheme colorScheme) { final spans = []; final regex = RegExp(r'\*\*(.*?)\*\*|__(.*?)__|`(.*?)`|\[(.*?)\]'); int lastIndex = 0; for (final match in regex.allMatches(content)) { if (match.start > lastIndex) { spans.add(TextSpan( text: content.substring(lastIndex, match.start), style: TextStyle( fontSize: 13, color: colorScheme.onSurface.withOpacity(0.8), height: 1.4, ), )); } if (match.group(1) != null) { spans.add(TextSpan( text: match.group(1), style: TextStyle( fontSize: 13, color: colorScheme.onSurface.withOpacity(0.9), height: 1.4, fontWeight: FontWeight.bold, ), )); } else if (match.group(2) != null) { spans.add(TextSpan( text: match.group(2), style: TextStyle( fontSize: 13, color: colorScheme.onSurface.withOpacity(0.9), height: 1.4, fontStyle: FontStyle.italic, ), )); } else if (match.group(3) != null) { spans.add(TextSpan( text: match.group(3), style: TextStyle( fontSize: 12, color: colorScheme.primary, height: 1.4, fontFamily: 'monospace', backgroundColor: colorScheme.primaryContainer.withOpacity(0.2), ), )); } else if (match.group(4) != null) { spans.add(TextSpan( text: match.group(4), style: TextStyle( fontSize: 13, color: colorScheme.primary, height: 1.4, fontWeight: FontWeight.w600, ), )); } lastIndex = match.end; } if (lastIndex < content.length) { spans.add(TextSpan( text: content.substring(lastIndex), style: TextStyle( fontSize: 13, color: colorScheme.onSurface.withOpacity(0.8), height: 1.4, ), )); } if (spans.isEmpty) { return Text( content, style: TextStyle( fontSize: 13, color: colorScheme.onSurface.withOpacity(0.8), height: 1.4, ), softWrap: true, maxLines: null, ); } return RichText( text: TextSpan(children: spans), softWrap: true, maxLines: null, ); } 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 '点击区域重试'; } } void _handleAction() { if (Platform.isAndroid) { _handleAndroidAction(); } else if (Platform.isIOS) { _handleIosAction(); } else { showToast('Unsupported platform'); } } void _handleIosAction() { if (info.appStoreUrl != null) { _plugin.goToAppStore(info.appStoreUrl!); // 移除关闭弹窗代码,始终不关闭 onComplete?.call(); } 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 hasMarkets = info.appMarkets?.isNotEmpty ?? false; if (hasMarkets) { await MarketSelectionDialog.show( context, markets: info.appMarkets!, onSelected: (market) { _plugin.goToAppStore(market.url ?? market.packageName ?? ''); }, ); } else { final appInfo = await _plugin.getAppInfo(); final pkg = appInfo['packageName'] ?? ''; if (pkg.isNotEmpty) { _plugin.goToAppStore('market://details?id=$pkg'); } else { showToast('Could not determine app package name.'); } } } Future _handleMarketAction() async { if (!mounted) return; await _performMarketAction(); onComplete?.call(); // 移除关闭弹窗代码 } 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: [ const Text('选择更新方式', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), if (!info.isForceUpdate) 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('前往应用市场更新'), 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内更新'), 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('前往浏览器下载安装包'), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.browser), ), const Divider(height: 24), if (!info.isForceUpdate) 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(); onComplete?.call(); return; } if (choice == AppUpgradeMethod.inApp && !_isDownloading) { await _startDownloadAndInstall(); return; } if (choice == AppUpgradeMethod.browser && !_isDownloading) { _goToBrowser(); } } } class _SimpleUpgradeDialog extends StatefulWidget { final UpgradeInfo info; final bool autoDownload; final bool autoInstall; final VoidCallback? onComplete; final void Function(String) showToast; final UpgradeConfig config; const _SimpleUpgradeDialog({ required this.info, required this.autoDownload, required this.autoInstall, this.onComplete, 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; @override VoidCallback? get onComplete => widget.onComplete; @override bool get autoDownload => widget.autoDownload; @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(); widget.onComplete?.call(); }, child: const Text('稍后更新'), ), const SizedBox(width: 8), ElevatedButton( onPressed: _handleAction, child: Text(Platform.isAndroid ? '立即更新' : '前往更新'), ), ], ), ), ], ), ), ); } } class _ForceUpgradeDialog extends StatefulWidget { final UpgradeInfo info; final UpgradeConfig config; const _ForceUpgradeDialog({ required this.info, 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, widget.config); @override VoidCallback? get onComplete => null; @override bool get autoDownload => false; @override bool get autoInstall => true; @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: 600, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Container( padding: const EdgeInsets.fromLTRB(20, 16, 20, 8), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.warning_amber_rounded, color: Theme.of(context).colorScheme.error, ), const SizedBox(width: 8), 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 ? '立即更新' : '前往更新'), ), ), ), ], ), ), ), ); } }