2108 lines
70 KiB
Dart
2108 lines
70 KiB
Dart
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/upgrade_info.dart';
|
||
import 'widgets/market_selection_dialog.dart';
|
||
|
||
/// 简化的插件接口,避免循环导入
|
||
class _SimpleAppUpgradePlugin {
|
||
static final _SimpleAppUpgradePlugin _instance = _SimpleAppUpgradePlugin._();
|
||
|
||
_SimpleAppUpgradePlugin._();
|
||
|
||
static _SimpleAppUpgradePlugin get instance => _instance;
|
||
|
||
Future<UpgradeInfo?> checkUpdate(String url, {Map<String, dynamic>? params}) {
|
||
return AppUpgradePluginPlatform.instance.checkUpdate(url, params: params);
|
||
}
|
||
|
||
Future<String?> downloadApk(String url, {Function(DownloadProgress)? onProgress}) {
|
||
return AppUpgradePluginPlatform.instance.downloadApk(url, onProgress: onProgress);
|
||
}
|
||
|
||
Future<bool> installApk(String filePath) {
|
||
return AppUpgradePluginPlatform.instance.installApk(filePath);
|
||
}
|
||
|
||
Future<bool> goToAppStore(String url) {
|
||
return AppUpgradePluginPlatform.instance.goToAppStore(url);
|
||
}
|
||
|
||
Future<Map<String, String>> getAppInfo() {
|
||
return AppUpgradePluginPlatform.instance.getAppInfo();
|
||
}
|
||
|
||
/// 检查当前应用的版本信息(用于安装状态检测)
|
||
Future<Map<String, String>> getCurrentAppInfo() async {
|
||
try {
|
||
return await AppUpgradePluginPlatform.instance.getAppInfo();
|
||
} catch (e) {
|
||
debugPrint('获取应用信息失败: $e');
|
||
return {};
|
||
}
|
||
}
|
||
|
||
/// 获取下载路径
|
||
Future<String?> getDownloadPath() {
|
||
return AppUpgradePluginPlatform.instance.getDownloadPath();
|
||
}
|
||
|
||
/// 精确检测应用是否已安装(通过包名)
|
||
Future<bool> 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;
|
||
}
|
||
}
|
||
|
||
/// 比较版本号是否已更新
|
||
///
|
||
/// [targetVersion] 目标版本名称(如 "1.0.0")
|
||
/// [targetBuildNumber] 目标构建号
|
||
///
|
||
/// 返回 true 表示当前版本已更新到目标版本或更高版本
|
||
/// 返回 false 表示当前版本低于目标版本,需要升级
|
||
///
|
||
/// 判断逻辑:
|
||
/// 1. 优先比较 buildNumber(构建号):
|
||
/// - 当前 buildNumber < 目标 buildNumber → 未更新,需要升级
|
||
/// - 当前 buildNumber > 目标 buildNumber → 已更新
|
||
/// - 当前 buildNumber == 目标 buildNumber → 继续比较版本名称
|
||
/// 2. buildNumber 相同时,比较 versionName:
|
||
/// - 当前版本 >= 目标版本 → 已更新
|
||
/// - 当前版本 < 目标版本 → 未更新,需要升级
|
||
Future<bool> 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) {
|
||
// 1. 首先比较 buildCode(构建号)
|
||
if (currentBuildNumber < targetBuildNumber) {
|
||
// 当前 buildCode < 目标版本,未更新
|
||
return false;
|
||
} else if (currentBuildNumber > targetBuildNumber) {
|
||
// 当前 buildCode > 目标版本,已更新
|
||
return true;
|
||
}
|
||
|
||
// 2. buildCode 相同时,比较 versionName 字符串版本号
|
||
// 当前版本 >= 目标版本时,表示已更新
|
||
return _compareVersionStrings(currentVersion, targetVersion) >= 0;
|
||
}
|
||
|
||
// 备用方案:只比较版本号字符串
|
||
return _compareVersionStrings(currentVersion, targetVersion) >= 0;
|
||
} catch (e) {
|
||
debugPrint('版本对比失败: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// 比较版本号字符串
|
||
///
|
||
/// [v1] 第一个版本号字符串(如 "1.2.3")
|
||
/// [v2] 第二个版本号字符串(如 "1.3.0")
|
||
///
|
||
/// 返回值:
|
||
/// - -1 表示 v1 < v2(第一个版本低于第二个版本)
|
||
/// - 0 表示 v1 == v2(版本相同)
|
||
/// - 1 表示 v1 > v2(第一个版本高于第二个版本)
|
||
///
|
||
/// 示例:
|
||
/// - _compareVersionStrings("1.2.3", "1.3.0") 返回 -1
|
||
/// - _compareVersionStrings("2.0.0", "1.9.9") 返回 1
|
||
/// - _compareVersionStrings("1.0.0", "1.0.0") 返回 0
|
||
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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 升级配置选项
|
||
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升级功能
|
||
///
|
||
/// 版本: 2.0.0
|
||
/// 更新日期: 2025-09-18
|
||
///
|
||
/// 主要特性:
|
||
/// - 🎨 现代化UI设计,支持Material Design 3
|
||
/// - 🔄 智能安装状态检测,支持多种安装场景
|
||
/// - 📱 完整的生命周期管理和错误处理
|
||
/// - ⚙️ 灵活的配置系统,支持多种使用模式
|
||
/// - 🎯 富文本支持,更好的内容展示
|
||
/// - 🚀 高性能,低内存占用
|
||
/// - 🛡️ 完善的权限处理和安全检查
|
||
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<bool> checkNetworkStatus() async {
|
||
try {
|
||
final result = await InternetAddress.lookup('baidu.com');
|
||
return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
|
||
} catch (e) {
|
||
debugPrint('网络检查失败: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// 清理下载的临时文件
|
||
Future<void> 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');
|
||
}
|
||
}
|
||
|
||
/// 一键检查更新(最简单的使用方式)
|
||
///
|
||
/// 基础用法:
|
||
/// ```dart
|
||
/// AppUpgradeSimple.instance.checkUpdate(
|
||
/// context: context,
|
||
/// url: 'https://api.example.com/check-update',
|
||
/// );
|
||
/// ```
|
||
///
|
||
/// 高级用法:
|
||
/// ```dart
|
||
/// // 配置自动更新
|
||
/// AppUpgradeSimple.instance.configure(UpgradeConfig.auto);
|
||
///
|
||
/// // 或者使用自定义配置
|
||
/// AppUpgradeSimple.instance.configure(UpgradeConfig(
|
||
/// autoDownload: true,
|
||
/// autoInstall: false,
|
||
/// installTimeout: 60,
|
||
/// customToast: (message) => ScaffoldMessenger.of(context).showSnackBar(
|
||
/// SnackBar(content: Text(message)),
|
||
/// ),
|
||
/// ));
|
||
///
|
||
/// // 检查更新
|
||
/// await AppUpgradeSimple.instance.checkUpdate(
|
||
/// context: context,
|
||
/// url: 'https://api.example.com/check-update',
|
||
/// params: {'userId': '123', 'channel': 'official'},
|
||
/// onComplete: () => print('更新检查完成'),
|
||
/// );
|
||
/// ```
|
||
///
|
||
/// [context] 需要是一个能够访问到 `Navigator` 和 `MaterialLocalizations` 的有效上下文。
|
||
/// [url] 检查更新的API接口地址
|
||
/// [params] 可选的请求参数
|
||
/// [config] 可选的配置覆盖
|
||
/// [onComplete] 更新流程完成后的回调
|
||
/// 一键检查更新(支持配置)
|
||
Future<void> checkUpdate({
|
||
required BuildContext context,
|
||
required String url,
|
||
Map<String, dynamic>? params,
|
||
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 环境内调用');
|
||
|
||
// 检查更新
|
||
final info = await _plugin.checkUpdate(url, params: params);
|
||
if (effectiveConfig.enableDebugLog) {
|
||
debugPrint('🔍 检查更新结果: $info');
|
||
}
|
||
|
||
if (info == null || !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');
|
||
|
||
final errorString = e.toString();
|
||
final String errorMessage;
|
||
|
||
if (errorString.contains('无网络连接')) {
|
||
errorMessage = '无网络连接,请检查网络设置';
|
||
} else if (errorString.contains('Failed host lookup')) {
|
||
errorMessage = '无法连接到服务器,请检查网络或稍后重试';
|
||
} else if (errorString.contains('Connection refused')) {
|
||
errorMessage = '服务器拒绝连接,请稍后重试';
|
||
} else if (errorString.contains('timeout')) {
|
||
errorMessage = '连接超时,请检查网络';
|
||
} else {
|
||
errorMessage = '检查更新失败: ${errorString.split(':').first}';
|
||
}
|
||
|
||
if (context.mounted) {
|
||
_showToast(errorMessage, effectiveConfig);
|
||
|
||
// 如果是网络问题,显示网络诊断建议
|
||
if (errorString.contains('Failed host lookup') || errorString.contains('无网络连接')) {
|
||
if (effectiveConfig.enableDebugLog) {
|
||
debugPrint('💡 建议: 请检查网络连接或尝试使用网络诊断功能');
|
||
}
|
||
}
|
||
}
|
||
onComplete?.call();
|
||
}
|
||
}
|
||
|
||
/// 静默检查更新(不显示无更新提示)
|
||
Future<UpgradeInfo?> checkUpdateSilent({
|
||
required String url,
|
||
Map<String, dynamic>? params,
|
||
}) async {
|
||
try {
|
||
return await _plugin.checkUpdate(url, params: params);
|
||
} catch (e) {
|
||
if (_config.enableDebugLog) {
|
||
debugPrint('静默检查更新失败: $e');
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/// 预下载APK(不显示UI,后台下载)
|
||
Future<String?> 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<String?> 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<Map<String, String>> getAppInfo() async {
|
||
return await _plugin.getAppInfo();
|
||
}
|
||
|
||
/// 显示升级对话框
|
||
Future<void> _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,
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 检查是否存在可用的 Material 环境(仅用于对话框)
|
||
bool _canShowMaterialDialog(BuildContext context) {
|
||
if (!context.mounted) return false;
|
||
try {
|
||
// Localizations.of will throw if not found.
|
||
return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations) != null;
|
||
} catch (_) {
|
||
// If it throws, it means we're not in a Material scope.
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 共享的升级操作逻辑
|
||
mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||
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;
|
||
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);
|
||
|
||
// 对于 APK 下载链接,直接尝试打开,不先检查 canLaunchUrl
|
||
// 因为 canLaunchUrl 可能无法正确识别 APK 下载链接
|
||
try {
|
||
final launched = await launchUrl(
|
||
uri,
|
||
mode: LaunchMode.externalApplication,
|
||
);
|
||
if (!launched) {
|
||
// 如果 launchUrl 返回 false,尝试使用 platformDefault 模式
|
||
await launchUrl(
|
||
uri,
|
||
mode: LaunchMode.platformDefault,
|
||
);
|
||
}
|
||
// 关闭对话框
|
||
if (mounted && Navigator.canPop(context)) {
|
||
Navigator.of(context).pop();
|
||
}
|
||
// 不需要关闭弹窗
|
||
Future.delayed(const Duration(seconds: 1), () {
|
||
onComplete?.call();
|
||
});
|
||
} catch (launchError) {
|
||
debugPrint('launchUrl 失败: $launchError');
|
||
// 如果 launchUrl 失败,尝试检查是否可以启动
|
||
final canLaunch = await canLaunchUrl(uri);
|
||
if (canLaunch) {
|
||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||
if (mounted && Navigator.canPop(context)) {
|
||
Navigator.of(context).pop();
|
||
}
|
||
// 不需要关闭弹窗
|
||
Future.delayed(const Duration(seconds: 1), () {
|
||
onComplete?.call();
|
||
});
|
||
} else {
|
||
showToast('无法打开下载链接,请检查是否安装了浏览器');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
debugPrint('打开浏览器失败: $e');
|
||
showToast('打开浏览器失败: ${e.toString()}');
|
||
}
|
||
}
|
||
|
||
Future<void> _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;
|
||
});
|
||
|
||
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 = '下载失败';
|
||
});
|
||
return;
|
||
}
|
||
|
||
setState(() {
|
||
_statusText = '下载完成';
|
||
_downloadProgress = 1.0;
|
||
_downloadedFilePath = filePath; // 保存文件路径
|
||
});
|
||
|
||
if (autoInstall) {
|
||
await _installApk(filePath);
|
||
}
|
||
}
|
||
|
||
Future<void> _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<void> _checkInstallationResult() async {
|
||
if (!mounted || !_isWaitingForInstallation) {
|
||
debugPrint('跳过安装结果检查: mounted=$mounted, _isWaitingForInstallation=$_isWaitingForInstallation');
|
||
return;
|
||
}
|
||
|
||
debugPrint('🔍 开始简化安装检测...');
|
||
|
||
try {
|
||
// 获取当前应用信息
|
||
final appInfo = await _plugin.getCurrentAppInfo();
|
||
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('安装被取消,可以点击重试按钮重新安装');
|
||
}
|
||
|
||
/// 重新安装APK
|
||
Future<void> _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: 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 = <TextSpan>[];
|
||
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: [
|
||
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');
|
||
}
|
||
}
|
||
|
||
/// Handles the upgrade action for iOS.
|
||
void _handleIosAction() {
|
||
if (info.appStoreUrl != null) {
|
||
_plugin.goToAppStore(info.appStoreUrl!);
|
||
// Pop the upgrade dialog.
|
||
if (Navigator.canPop(context)) {
|
||
Navigator.of(context).pop();
|
||
}
|
||
onComplete?.call();
|
||
} else {
|
||
showToast('App Store URL is not available.');
|
||
}
|
||
}
|
||
|
||
/// Handles the upgrade action for Android.
|
||
Future<void> _handleAndroidAction() async {
|
||
// On Android, we always assume a market option is available,
|
||
// because we can fall back to a generic market:// intent.
|
||
final bool hasDownloadOption = info.downloadUrl != null;
|
||
|
||
// 在Android上,我们总是假设市场选项可用,因为可以回退到通用的 market:// intent
|
||
// 因此这里不需要检查无更新方法的情况
|
||
|
||
// If a download option is not available, going to the market is the only choice.
|
||
if (!hasDownloadOption) {
|
||
_handleMarketAction();
|
||
return;
|
||
}
|
||
|
||
// If a download option exists, always give the user a choice,
|
||
// as the market option is also implicitly available.
|
||
await _showDownloadChoiceSheet();
|
||
}
|
||
|
||
/// Opens the app store or shows a market selection dialog.
|
||
Future<void> _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 {
|
||
// No specific markets, try a generic market link.
|
||
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.');
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Pops the current dialog and then performs the market action.
|
||
Future<void> _handleMarketAction() async {
|
||
// 对于强制更新,不关闭对话框
|
||
if (!info.isForceUpdate && Navigator.canPop(context)) {
|
||
Navigator.of(context).pop();
|
||
}
|
||
if (!mounted) return;
|
||
await _performMarketAction();
|
||
onComplete?.call();
|
||
}
|
||
|
||
Future<void> _showDownloadChoiceSheet() async {
|
||
if (!mounted) return;
|
||
|
||
final choice = await showModalBottomSheet<String>(
|
||
context: context,
|
||
isScrollControlled: false,
|
||
useRootNavigator: true,
|
||
isDismissible: !info.isForceUpdate, // 强制更新时不允许通过手势关闭
|
||
enableDrag: !info.isForceUpdate, // 强制更新时不允许拖拽关闭
|
||
// UI Beautification: Rounded corners
|
||
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: [
|
||
// Draggable handle
|
||
Container(
|
||
width: 40,
|
||
height: 4,
|
||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: Colors.grey.shade300,
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
// Title and Close Button - 强制更新时不显示关闭按钮
|
||
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(),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// Option 1: App Market
|
||
ListTile(
|
||
leading: const Icon(Icons.storefront_outlined),
|
||
title: const Text('前往应用市场更新'),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
onTap: () => Navigator.of(ctx).pop('market'),
|
||
),
|
||
|
||
// Option 2: Direct Download
|
||
ListTile(
|
||
leading: const Icon(Icons.system_update),
|
||
title: const Text('APP内更新'),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
onTap: () => Navigator.of(ctx).pop('update_within_APP'),
|
||
),
|
||
ListTile(
|
||
leading: const Icon(Icons.download_for_offline_outlined),
|
||
title: const Text('前往浏览器下载安装包'),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
onTap: () => Navigator.of(ctx).pop('go_to_browser'),
|
||
),
|
||
|
||
const Divider(height: 24),
|
||
|
||
// Cancel Button - 强制更新时不显示取消按钮
|
||
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 == 'market') {
|
||
// 对于强制更新,不调用 onComplete,保持对话框打开状态
|
||
if (!info.isForceUpdate) {
|
||
await _performMarketAction();
|
||
onComplete?.call();
|
||
} else {
|
||
await _performMarketAction();
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (choice == 'update_within_APP' && !_isDownloading) {
|
||
await _startDownloadAndInstall();
|
||
return;
|
||
}
|
||
|
||
/// 前往浏览器更新
|
||
if (choice == 'go_to_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),
|
||
|
||
// 下载进度 - 使用增强版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 ? '立即更新' : '前往更新'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 强制更新对话框
|
||
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),
|
||
|
||
// 下载进度 - 使用增强版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 ? '立即更新' : '前往更新'),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|