diff --git a/README.md b/README.md index d647b34..ced77ea 100644 --- a/README.md +++ b/README.md @@ -175,7 +175,7 @@ dependencies { { "hasUpdate": true, "isForceUpdate": false, - "versionCode": "101", + "versionBuildNumber": "101", "versionName": "1.0.1", "updateContent": "1. 修复了登录问题\n2. 优化了界面显示\n3. 提升了性能", "downloadUrl": "https://your-cdn.com/app-release.apk", diff --git a/lib/app_upgrade_plugin_method_channel.dart b/lib/app_upgrade_plugin_method_channel.dart index 0b20169..3ec9836 100644 --- a/lib/app_upgrade_plugin_method_channel.dart +++ b/lib/app_upgrade_plugin_method_channel.dart @@ -110,19 +110,19 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform { Future checkUpdate(String url, {Map? params}) async { try { final packageInfo = await PackageInfo.fromPlatform(); - final currentVersion = packageInfo.version; - final currentBuildNumber = packageInfo.buildNumber; + final currentVersionName = packageInfo.version; + final currentBuildNumber = int.parse(packageInfo.buildNumber); // 准备请求参数 final requestParams = { - 'version': currentVersion, + 'version': currentVersionName, 'buildNumber': currentBuildNumber, 'platform': Platform.isAndroid ? 'android' : 'ios', ...?params, }; debugPrint('==== 检查更新 ===='); - debugPrint('当前版本: $currentVersion (Build: $currentBuildNumber)'); + debugPrint('当前版本: $currentVersionName (Build: $currentBuildNumber)'); debugPrint('请求URL: $url'); Response? response; @@ -168,10 +168,39 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform { debugPrint('响应数据: $responseData'); // 解析更新信息 - final upgradeInfo = UpgradeInfo.fromJson(responseData); + // final upgradeInfo = UpgradeInfo.fromJson(responseData); + final upgradeInfo = UpgradeInfo.fromJson( + currentBuildNumber: currentBuildNumber, + currentVersionName: currentVersionName, + { + "isForceUpdate": true, + "versionBuildNumber": 101, + "versionName": "1.0.1", + "updateContent": "1. 修复了登修复了登录问题修复了登录问题修复了登录问题录问题\n2. 优化了界面显示优化了界面显示\n3. 提升了性能", + "downloadUrl": + "https://dpc-job-oss.23544.com/infra-app/making_school_asignment_app/1.0.5/1/app-release.apk", + "appStoreUrl": "https://apps.apple.com/app/id123456789", + // "apkSize": 25165824, + // "apkMd5": "d41d8cd98f00b204e9800998ecf8427e", + // "appMarkets": [ + // { + // "customName": "华为应用市场", + // "market": "huawei", + // "packageName": "com.huawei.appmarket", + // "url": "appmarket://details?id=com.yourapp.package" + // }, + // { + // "customName": "小米应用商店", + // "market": "xiaomi", + // "packageName": "com.xiaomi.market", + // "url": "mimarket://details?id=com.yourapp.package", + // } + // ] + }, + ); // 比较版本 - if (_compareVersion(upgradeInfo.versionCode, currentBuildNumber) > 0) { + if (upgradeInfo.hasUpdate) { debugPrint('✅ 发现新版本: ${upgradeInfo.versionName}'); return upgradeInfo; } else { @@ -552,11 +581,8 @@ class MethodChannelAppUpgradePlugin extends AppUpgradePluginPlatform { /// 比较版本号 /// 返回值:1表示v1大于v2,0表示相等,-1表示v1小于v2 - int _compareVersion(String v1, String v2) { + int _compareVersion(int version1, int version2) { try { - final version1 = int.tryParse(v1) ?? 0; - final version2 = int.tryParse(v2) ?? 0; - if (version1 > version2) { return 1; } else if (version1 < version2) { diff --git a/lib/app_upgrade_simple.dart b/lib/app_upgrade_simple.dart index 29cc22d..01d9189 100644 --- a/lib/app_upgrade_simple.dart +++ b/lib/app_upgrade_simple.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; @@ -87,7 +88,7 @@ class AppUpgradeSimple { // 检查更新 final info = await _plugin.checkUpdate(url, params: params); - + print('info: $info'); if (info == null || !info.hasUpdate) { if (showNoUpdateToast && context.mounted) _showToast('已是最新版本'); onComplete?.call(); @@ -189,93 +190,16 @@ class AppUpgradeSimple { } } -/// 共享的升级对话框内容构建器 -class _UpgradeDialogContent extends StatelessWidget { - final UpgradeInfo info; - final bool isDownloading; - final double downloadProgress; - final String statusText; - - const _UpgradeDialogContent({ - required this.info, - required this.isDownloading, - required this.downloadProgress, - required this.statusText, - }); - - @override - Widget build(BuildContext context) { - final changeItems = - info.updateContent.split(RegExp(r'\r?\n')).map((e) => e.trim()).where((e) => e.isNotEmpty).toList(); - - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '版本:${info.versionName}', - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), - ), - if (info.apkSize != null) ...[ - const SizedBox(height: 4), - Text('大小:${formatBytes(info.apkSize!)}'), - ], - const SizedBox(height: 12), - const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)), - const SizedBox(height: 8), - Container( - constraints: const BoxConstraints(maxHeight: 220), - child: SingleChildScrollView( - child: changeItems.isEmpty - ? Text(info.updateContent) - : Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: changeItems - .map( - (line) => Padding( - padding: const EdgeInsets.only(bottom: 6), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text('• '), - Expanded(child: Text(line)), - ], - ), - ), - ) - .toList(), - ), - ), - ), - if (isDownloading) ...[ - const SizedBox(height: 16), - ClipRRect( - borderRadius: const BorderRadius.all(Radius.circular(4)), - child: LinearProgressIndicator( - value: downloadProgress.clamp(0.0, 1.0), - minHeight: 6, - ), - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded(child: Text(statusText, overflow: TextOverflow.ellipsis)), - Text('${(downloadProgress * 100).toStringAsFixed(1)}%'), - ], - ), - ], - ], - ); - } -} - /// 共享的升级操作逻辑 mixin _UpgradeDialogLogic on State { final _plugin = _SimpleAppUpgradePlugin.instance; bool _isDownloading = false; double _downloadProgress = 0; String _statusText = ''; + String? _downloadedFilePath; // 保存下载完成的APK文件路径 + bool _isInstalling = false; // 是否正在安装 + bool _isWaitingForInstallation = false; // 是否在等待用户完成安装 + Timer? _installCheckTimer; // 安装检测定时器 UpgradeInfo get info; void Function(String) get showToast; @@ -283,9 +207,7 @@ mixin _UpgradeDialogLogic on State { bool get autoDownload; bool get autoInstall; - @override - void initState() { - super.initState(); + void initUpgradeLogic() { if (autoDownload && Platform.isAndroid) { WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { @@ -295,6 +217,25 @@ mixin _UpgradeDialogLogic on State { } } + void disposeUpgradeLogic() { + _installCheckTimer?.cancel(); + } + + void onAppLifecycleStateChanged(AppLifecycleState state) { + debugPrint('应用生命周期状态变化: $state, _isWaitingForInstallation=$_isWaitingForInstallation'); + + // 当应用从后台回到前台时,检查是否是从安装界面返回 + if (state == AppLifecycleState.resumed && _isWaitingForInstallation) { + debugPrint('应用回到前台,准备检查安装状态'); + // 延迟一点时间再检查,确保系统状态稳定 + Future.delayed(const Duration(milliseconds: 500), () { + if (mounted && _isWaitingForInstallation) { + _checkInstallationResult(); + } + }); + } + } + Future _startDownloadAndInstall() async { if (!Platform.isAndroid || info.downloadUrl == null) return; if (!mounted) return; @@ -336,6 +277,7 @@ mixin _UpgradeDialogLogic on State { setState(() { _statusText = '下载完成'; _downloadProgress = 1.0; + _downloadedFilePath = filePath; // 保存文件路径 }); if (autoInstall) { @@ -345,18 +287,862 @@ mixin _UpgradeDialogLogic on State { Future _installApk(String filePath) async { if (!mounted) return; + + setState(() { + _isInstalling = true; + _statusText = '准备安装...'; + }); + final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context); if (!hasPermission) { if (mounted) { + setState(() { + _isInstalling = false; + _statusText = '权限被拒绝'; + }); showToast('未授予安装权限,无法完成更新'); } return; } - final success = await _plugin.installApk(filePath); - if (!success && mounted) { - showToast('安装失败,请手动安装'); + 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() { + // 5秒后自动检测一次(快速检测) + Timer(const Duration(seconds: 5), () { + if (mounted && _isWaitingForInstallation && _statusText == '请完成安装') { + debugPrint('5秒后自动检测安装状态'); + _checkInstallationResult(); + } + }); + + // 30秒后如果还在等待安装,则提示超时 + _installCheckTimer = Timer(const Duration(seconds: 30), () { + if (mounted && _isWaitingForInstallation && _statusText == '请完成安装') { + 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['versionName'] ?? ''; + + debugPrint('检查安装结果: 当前版本=$currentVersion, 目标版本=${info.versionName}'); + + // 如果版本号已更新,说明安装成功 + if (currentVersion == info.versionName) { + debugPrint('检测到版本已更新,安装成功'); + _installCheckTimer?.cancel(); + setState(() { + _isWaitingForInstallation = false; + _statusText = '安装成功'; + }); + showToast('应用更新成功!'); + + // 延迟关闭对话框 + Future.delayed(const Duration(seconds: 1), () { + if (mounted && Navigator.canPop(context)) { + Navigator.of(context).pop(); + } + onComplete?.call(); + }); + } else { + // 版本号未更新,用户可能取消了安装 + debugPrint('版本号未更新,用户可能取消了安装'); + setState(() { + _isWaitingForInstallation = false; + _statusText = '安装被取消'; + }); + showToast('安装被取消,可以重新尝试安装'); + } + } catch (e) { + debugPrint('检测应用状态失败: $e'); + setState(() { + _isWaitingForInstallation = false; + _statusText = '检测失败'; + }); + showToast('无法检测安装状态,请手动检查'); + } + } + + /// 重新安装APK + Future _retryInstall() async { + if (_downloadedFilePath != null) { + // 如果是"请完成安装"状态,先检查安装结果 + if (_statusText == '请完成安装') { + await _checkInstallationResult(); + // 如果检查后状态仍然是"请完成安装",则重新启动安装 + if (_statusText == '请完成安装') { + await _installApk(_downloadedFilePath!); + } + return; + } + + // 如果是权限被拒绝,先尝试重新请求权限 + if (_statusText == '权限被拒绝') { + 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: Text( + 'v${info.versionName}', + style: TextStyle( + color: colorScheme.onPrimary, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 8), + if (info.isForceUpdate) + 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, + colorScheme: colorScheme, + ), + ), + if (info.apkSize != null) ...[ + const SizedBox(width: 12), + 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, + ); + } + + /// 构建增强的下载进度UI + Widget _buildEnhancedDownloadProgress(BuildContext context, ColorScheme colorScheme) { + // 判断是否显示重新安装按钮 - 在以下情况显示: + // 1. 有下载完成的文件路径 + // 2. 不在下载状态 + // 3. 不在安装状态(或者不在等待安装状态) + // 4. 下载进度已完成 + // 5. 状态不是"安装成功" + 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: [ + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: _getStatusColor(colorScheme).withOpacity(0.1), + shape: BoxShape.circle, + ), + child: Icon( + _getStatusIcon(), + color: _getStatusColor(colorScheme), + size: 20, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _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 (showRetryButton && + (_statusText == '安装失败' || + _statusText == '安装异常' || + _statusText == '权限被拒绝' || + _statusText == '请完成安装' || + _statusText == '安装超时' || + _statusText == '安装被取消' || + _statusText == '检测失败')) + Text( + _statusText == '请完成安装' ? '可重新启动安装' : '点击区域重试', + style: TextStyle( + fontSize: 11, + color: colorScheme.onSurface.withOpacity(0.6), + ), + ), + // 为"请完成安装"状态添加操作按钮 + if (_statusText == '请完成安装') + Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: _checkInstallationResult, + icon: const Icon(Icons.refresh, size: 14), + label: const Text('检查状态', style: TextStyle(fontSize: 11)), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + backgroundColor: colorScheme.secondary.withOpacity(0.8), + ), + ), + ), + const SizedBox(width: 4), + Expanded( + child: ElevatedButton.icon( + onPressed: () async { + // 直接重新安装,不检查状态 + if (_downloadedFilePath != null) { + await _installApk(_downloadedFilePath!); + } + }, + icon: const Icon(Icons.install_mobile, size: 14), + label: const Text('重新安装', style: TextStyle(fontSize: 11)), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + backgroundColor: colorScheme.primary, + ), + ), + ), + const SizedBox(width: 4), + // 测试按钮:模拟取消安装 + Expanded( + child: ElevatedButton.icon( + onPressed: () { + setState(() { + _isWaitingForInstallation = false; + _statusText = '安装被取消'; + }); + showToast('已模拟安装被取消,现在应该显示重试按钮'); + }, + icon: const Icon(Icons.cancel, size: 14), + label: const Text('模拟取消', style: TextStyle(fontSize: 10)), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + backgroundColor: colorScheme.error.withOpacity(0.8), + ), + ), + ), + ], + ), + ), + ], + ), + ), + // 重新安装按钮 - 强制在特定状态下显示 + 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.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.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 == '权限被拒绝') { + return Icons.settings; + } else if (_statusText == '安装失败' || + _statusText == '安装异常' || + _statusText == '安装超时' || + _statusText == '安装被取消' || + _statusText == '检测失败') { + return Icons.refresh; + } else if (_statusText == '请完成安装') { + return Icons.launch; + } else { + return Icons.install_mobile; + } + } + + /// 获取重试按钮文本 + String _getRetryButtonText() { + if (_statusText == '权限被拒绝') { + return '设置'; + } else if (_statusText == '安装失败' || + _statusText == '安装异常' || + _statusText == '安装超时' || + _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 == '检测失败') { + return colorScheme.secondary.withOpacity(0.8); + } else if (_statusText == '请完成安装') { + return colorScheme.secondary; + } else { + return null; // 使用默认颜色 + } + } + + /// 判断是否应该显示重试选项 + bool _shouldShowRetryOptions() { + return _statusText == '安装被取消' || + _statusText == '安装失败' || + _statusText == '安装异常' || + _statusText == '权限被拒绝' || + _statusText == '安装超时' || + _statusText == '检测失败' || + (_downloadedFilePath != null && + !_isDownloading && + !_isInstalling && + !_isWaitingForInstallation && + _downloadProgress >= 1.0 && + _statusText != '安装成功'); } void _handleAction() { @@ -568,7 +1354,7 @@ class _SimpleUpgradeDialog extends StatefulWidget { State<_SimpleUpgradeDialog> createState() => _SimpleUpgradeDialogState(); } -class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _UpgradeDialogLogic { +class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _UpgradeDialogLogic, WidgetsBindingObserver { @override UpgradeInfo get info => widget.info; @override @@ -580,46 +1366,110 @@ class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _Upgrad @override bool get autoInstall => widget.autoInstall; + @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 AlertDialog( + return Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - contentPadding: const EdgeInsets.fromLTRB(20, 8, 20, 16), - actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 12), - title: Row( - children: [ - Icon( - Icons.system_update, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - const Text('发现新版本'), - ], - ), - content: _UpgradeDialogContent( - info: widget.info, - isDownloading: _isDownloading, - downloadProgress: _downloadProgress, - statusText: _statusText, - ), - actionsAlignment: MainAxisAlignment.end, - actions: _isDownloading - ? [] - : [ - TextButton( - onPressed: () { - Navigator.of(context).pop(); - widget.onComplete?.call(); - }, - child: const Text('稍后更新'), + 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, + ), + ), + ), + ], ), - ElevatedButton( - onPressed: _handleAction, - child: Text(Platform.isAndroid ? '立即更新' : '前往更新'), + ), + // 内容区域 + 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), + + // 下载进度 - 使用增强版UI + 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 ? '立即更新' : '前往更新'), + ), + ], + ), + ), + ], + ), + ), ); } } @@ -634,7 +1484,7 @@ class _ForceUpgradeDialog extends StatefulWidget { State<_ForceUpgradeDialog> createState() => _ForceUpgradeDialogState(); } -class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeDialogLogic { +class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeDialogLogic, WidgetsBindingObserver { @override UpgradeInfo get info => widget.info; @override @@ -646,41 +1496,104 @@ class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeD @override bool get autoInstall => true; + @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: AlertDialog( + child: Dialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - contentPadding: const EdgeInsets.fromLTRB(20, 8, 20, 16), - actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 12), - title: Row( - children: [ - Icon( - Icons.system_update, - color: Theme.of(context).colorScheme.primary, - ), - const SizedBox(width: 8), - const Text('发现新版本 (强制)'), - ], - ), - content: _UpgradeDialogContent( - info: widget.info, - isDownloading: _isDownloading, - downloadProgress: _downloadProgress, - statusText: _statusText, - ), - actionsAlignment: MainAxisAlignment.end, - actions: _isDownloading - ? [] - : [ - ElevatedButton( - onPressed: _handleAction, - child: Text(Platform.isAndroid ? '立即更新' : '前往更新'), + 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), + + // 下载进度 - 使用增强版UI + 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 ? '立即更新' : '前往更新'), + ), + ), + ), + ], + ), + ), ), ); } diff --git a/lib/models/app_market.dart b/lib/models/app_market.dart index c896eb5..1099b70 100644 --- a/lib/models/app_market.dart +++ b/lib/models/app_market.dart @@ -1,5 +1,5 @@ /// 应用商店信息 -class AppMarketInfo { +class AppMarketInfo extends Object { /// 应用商店 final AppMarket market; diff --git a/lib/models/upgrade_info.dart b/lib/models/upgrade_info.dart index 4e8db20..ab56f54 100644 --- a/lib/models/upgrade_info.dart +++ b/lib/models/upgrade_info.dart @@ -9,11 +9,17 @@ class UpgradeInfo { final bool isForceUpdate; /// 版本号 - final String versionCode; + final int versionBuildNumber; /// 版本名称 final String versionName; + /// 当前版本号 + final int currentBuildNumber; + + /// 当前版本名称 + final String currentVersionName; + /// 更新说明 final String updateContent; @@ -35,9 +41,11 @@ class UpgradeInfo { UpgradeInfo({ this.hasUpdate = false, required this.isForceUpdate, - required this.versionCode, + required this.versionBuildNumber, required this.versionName, required this.updateContent, + required this.currentBuildNumber, + required this.currentVersionName, this.downloadUrl, this.appStoreUrl, this.apkSize, @@ -45,12 +53,23 @@ class UpgradeInfo { this.appMarkets, }); - factory UpgradeInfo.fromJson(Map json) { + /// 从JSON创建 + /// [currentBuildNumber] 当前版本号 + /// [currentVersion] 当前版本名称 + factory UpgradeInfo.fromJson( + Map json, { + required int currentBuildNumber, + required String currentVersionName, + }) { + final versionBuildNumber = json['versionBuildNumber']; + final versionName = json['versionName']; return UpgradeInfo( - hasUpdate: json['hasUpdate'] as bool? ?? false, + hasUpdate: versionBuildNumber != currentBuildNumber || versionName != currentVersionName, isForceUpdate: json['isForceUpdate'] ?? false, - versionCode: json['versionCode'] ?? '', - versionName: json['versionName'] ?? '', + versionBuildNumber: versionBuildNumber, + versionName: versionName, + currentBuildNumber: currentBuildNumber, + currentVersionName: currentVersionName, updateContent: json['updateContent'] ?? '', downloadUrl: json['downloadUrl'] as String?, appStoreUrl: json['appStoreUrl'] as String?, @@ -66,9 +85,11 @@ class UpgradeInfo { return { 'hasUpdate': hasUpdate, 'isForceUpdate': isForceUpdate, - 'versionCode': versionCode, + 'versionBuildNumber': versionBuildNumber, 'versionName': versionName, 'updateContent': updateContent, + 'currentBuildNumber': currentBuildNumber, + 'currentVersionName': currentVersionName, 'downloadUrl': downloadUrl, 'appStoreUrl': appStoreUrl, 'apkSize': apkSize, diff --git a/lib/widgets/market_selection_dialog.dart b/lib/widgets/market_selection_dialog.dart index 2c802c2..7232f4f 100644 --- a/lib/widgets/market_selection_dialog.dart +++ b/lib/widgets/market_selection_dialog.dart @@ -21,10 +21,7 @@ class MarketSelectionDialog extends StatelessWidget { await showDialog( context: context, useRootNavigator: true, - builder: (context) => MarketSelectionDialog( - markets: markets, - onSelected: onSelected, - ), + builder: (context) => MarketSelectionDialog(markets: markets, onSelected: onSelected), ); }