2042 lines
67 KiB
Dart
2042 lines
67 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:url_launcher/url_launcher.dart';
|
||
|
||
import 'app_upgrade_plugin_platform_interface.dart';
|
||
import 'core/permission_helper.dart';
|
||
import 'core/upgrade_utils.dart';
|
||
import 'models/app_upgrade_method.dart';
|
||
import 'models/app_upgrade_version.dart';
|
||
import 'models/upgrade_info.dart';
|
||
import 'widgets/market_selection_dialog.dart';
|
||
|
||
/// 简化的插件接口,避免循环导入
|
||
class _SimpleAppUpgradePlugin {
|
||
static final _SimpleAppUpgradePlugin _instance = _SimpleAppUpgradePlugin._();
|
||
|
||
_SimpleAppUpgradePlugin._();
|
||
|
||
static _SimpleAppUpgradePlugin get instance => _instance;
|
||
|
||
Future<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, {required BuildContext context}) {
|
||
return AppUpgradePluginPlatform.instance.goToAppStore(url, context: context);
|
||
}
|
||
|
||
Future<Map<String, String>> getAppInfo() {
|
||
return AppUpgradePluginPlatform.instance.getAppInfo();
|
||
}
|
||
|
||
/// 获取下载路径
|
||
Future<String?> getDownloadPath() {
|
||
return AppUpgradePluginPlatform.instance.getDownloadPath();
|
||
}
|
||
}
|
||
|
||
/// 升级配置选项
|
||
class UpgradeConfig {
|
||
/// 是否显示无更新提示
|
||
final bool showNoUpdateToast;
|
||
|
||
/// 是否自动下载
|
||
final bool autoDownload;
|
||
|
||
/// 是否自动安装
|
||
final bool autoInstall;
|
||
|
||
/// 连接超时时间(秒)
|
||
final int connectionTimeout;
|
||
|
||
/// 下载超时时间(秒)
|
||
final int downloadTimeout;
|
||
|
||
/// 安装检测超时时间(秒)
|
||
final int installTimeout;
|
||
|
||
/// 是否启用调试日志
|
||
final bool enableDebugLog;
|
||
|
||
/// 自定义Toast显示函数
|
||
final void Function(String message)? customToast;
|
||
|
||
/// 是否需要获取安装未知应用权限(默认false,直接安装)
|
||
final bool requireInstallPermission;
|
||
|
||
const UpgradeConfig({
|
||
this.showNoUpdateToast = true,
|
||
this.autoDownload = false,
|
||
this.autoInstall = false,
|
||
this.connectionTimeout = 30,
|
||
this.downloadTimeout = 300,
|
||
this.installTimeout = 45,
|
||
this.enableDebugLog = true,
|
||
this.customToast,
|
||
this.requireInstallPermission = false, // 默认不需要权限
|
||
});
|
||
|
||
/// 快速配置:自动更新(不需要权限)
|
||
static const UpgradeConfig auto = UpgradeConfig(
|
||
autoDownload: true,
|
||
autoInstall: true,
|
||
requireInstallPermission: false,
|
||
);
|
||
|
||
/// 快速配置:静默检查
|
||
static const UpgradeConfig silent = UpgradeConfig(
|
||
showNoUpdateToast: false,
|
||
enableDebugLog: false,
|
||
requireInstallPermission: false,
|
||
);
|
||
|
||
/// 快速配置:开发模式(详细日志 + 较短超时)
|
||
static const UpgradeConfig development = UpgradeConfig(
|
||
enableDebugLog: true,
|
||
installTimeout: 30,
|
||
connectionTimeout: 10,
|
||
requireInstallPermission: false,
|
||
);
|
||
|
||
/// 快速配置:生产模式(静默 + 较长超时)
|
||
static const UpgradeConfig production = UpgradeConfig(
|
||
showNoUpdateToast: false,
|
||
enableDebugLog: false,
|
||
installTimeout: 60,
|
||
connectionTimeout: 30,
|
||
requireInstallPermission: false,
|
||
);
|
||
|
||
/// 快速配置:需要权限模式(传统方式)
|
||
static const UpgradeConfig withPermission = UpgradeConfig(
|
||
requireInstallPermission: true,
|
||
);
|
||
}
|
||
|
||
/// 简化版App升级管理器
|
||
/// 提供最简单的API,一行代码即可实现App升级功能
|
||
class AppUpgradeSimple {
|
||
static AppUpgradeSimple? _instance;
|
||
|
||
/// 获取单例实例
|
||
static AppUpgradeSimple get instance {
|
||
_instance ??= AppUpgradeSimple._();
|
||
return _instance!;
|
||
}
|
||
|
||
@visibleForTesting
|
||
static set instance(AppUpgradeSimple value) {
|
||
_instance = value;
|
||
}
|
||
|
||
@visibleForTesting
|
||
AppUpgradeSimple.private({_SimpleAppUpgradePlugin? plugin}) : _plugin = plugin ?? _SimpleAppUpgradePlugin.instance;
|
||
|
||
AppUpgradeSimple._() : _plugin = _SimpleAppUpgradePlugin.instance;
|
||
|
||
final _SimpleAppUpgradePlugin _plugin;
|
||
UpgradeConfig _config = const UpgradeConfig();
|
||
|
||
/// 配置升级参数
|
||
void configure(UpgradeConfig config) {
|
||
_config = config;
|
||
}
|
||
|
||
/// 快速配置:一键设置自动更新
|
||
void enableAutoUpdate() {
|
||
_config = UpgradeConfig.auto;
|
||
}
|
||
|
||
/// 快速配置:一键设置静默检查
|
||
void enableSilentCheck() {
|
||
_config = UpgradeConfig.silent;
|
||
}
|
||
|
||
/// 检查网络连接状态
|
||
Future<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');
|
||
}
|
||
}
|
||
|
||
/// 一键检查更新(支持配置)
|
||
///
|
||
/// 参数说明:
|
||
/// - [context] (必需) BuildContext,用于显示升级对话框,必须在 MaterialApp 环境内调用
|
||
/// - [future] (必需) 异步函数,用于获取服务器版本信息,返回 [AppUpgradeVersion] 对象或 null
|
||
/// - 返回 null 时视为无更新
|
||
/// - 返回 [AppUpgradeVersion] 时,会与当前版本比较,决定是否显示升级对话框
|
||
/// - [showNoUpdateToast] (可选) 是否显示"已是最新版本"的提示
|
||
/// - true: 无更新时显示提示(默认)
|
||
/// - false: 无更新时不显示提示
|
||
/// - null: 使用 [config] 或全局配置的 [UpgradeConfig.showNoUpdateToast]
|
||
/// - [autoDownload] (可选) 是否自动下载APK
|
||
/// - true: 检测到新版本后自动开始下载
|
||
/// - false: 需要用户点击"立即更新"按钮才开始下载(默认)
|
||
/// - null: 使用 [config] 或全局配置的 [UpgradeConfig.autoDownload]
|
||
/// - [autoInstall] (可选) 是否自动安装APK
|
||
/// - true: 下载完成后自动触发安装流程
|
||
/// - false: 下载完成后需要用户手动触发安装(默认)
|
||
/// - null: 使用 [config] 或全局配置的 [UpgradeConfig.autoInstall]
|
||
/// - 注意:仅在 [autoDownload] 为 true 时生效
|
||
/// - [onComplete] (可选) 完成回调函数
|
||
/// - 在以下情况会被调用:
|
||
/// - 检查完成(无论是否有更新)
|
||
/// - 检查失败
|
||
/// - 用户关闭升级对话框
|
||
/// - [config] (可选) 升级配置对象 [UpgradeConfig]
|
||
/// - 如果提供,会覆盖全局配置和单个参数设置
|
||
/// - 如果为 null,使用全局配置 [UpgradeConfig]
|
||
/// - 配置项包括:超时时间、调试日志、权限要求等
|
||
///
|
||
/// 使用示例:
|
||
/// ```dart
|
||
/// await AppUpgradeSimple.instance.checkUpdate(
|
||
/// context: context,
|
||
/// future: () async {
|
||
/// // 调用您的API获取版本信息
|
||
/// final response = await http.get('https://api.example.com/version');
|
||
/// return AppUpgradeVersion.fromJson(json.decode(response.body));
|
||
/// },
|
||
/// showNoUpdateToast: true,
|
||
/// autoDownload: false,
|
||
/// autoInstall: false,
|
||
/// onComplete: () {
|
||
/// print('检查更新完成');
|
||
/// },
|
||
/// );
|
||
/// ```
|
||
///
|
||
/// 参数优先级:
|
||
/// 1. 方法参数(如 [showNoUpdateToast], [autoDownload], [autoInstall])
|
||
/// 2. [config] 参数中的配置
|
||
/// 3. 全局配置(通过 [configure] 方法设置)
|
||
/// 4. 默认配置
|
||
Future<void> checkUpdate({
|
||
required BuildContext context,
|
||
required Future<AppUpgradeVersion?> Function() future,
|
||
bool? showNoUpdateToast,
|
||
bool? autoDownload,
|
||
bool? autoInstall,
|
||
VoidCallback? onComplete,
|
||
UpgradeConfig? config,
|
||
}) async {
|
||
// 使用传入的配置或默认配置
|
||
final effectiveConfig = config ?? _config;
|
||
final finalShowNoUpdateToast = showNoUpdateToast ?? effectiveConfig.showNoUpdateToast;
|
||
final finalAutoDownload = autoDownload ?? effectiveConfig.autoDownload;
|
||
final finalAutoInstall = autoInstall ?? effectiveConfig.autoInstall;
|
||
try {
|
||
assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用');
|
||
|
||
// 1. 获取服务器版本信息
|
||
final serverInfo = await future();
|
||
if (serverInfo == null) {
|
||
// 获取失败或无数据,视作无更新
|
||
if (effectiveConfig.enableDebugLog) {
|
||
debugPrint('🔍 检查更新结果: 未返回版本信息');
|
||
}
|
||
onComplete?.call();
|
||
return;
|
||
}
|
||
|
||
if (effectiveConfig.enableDebugLog) {
|
||
debugPrint('🔍 获取到服务器版本: $serverInfo');
|
||
}
|
||
|
||
// 2. 获取当前App信息
|
||
final appInfo = await _plugin.getAppInfo();
|
||
final currentVersionName = appInfo['version'] ?? '';
|
||
final currentBuildNumber = int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0;
|
||
|
||
if (effectiveConfig.enableDebugLog) {
|
||
debugPrint('📱 当前版本: $currentVersionName (Build: $currentBuildNumber)');
|
||
}
|
||
|
||
// 3. 比较版本并构建 UpgradeInfo
|
||
bool hasUpdate = false;
|
||
if (serverInfo.versionBuildNumber > 0) {
|
||
// 优先比较 buildNumber
|
||
if (serverInfo.versionBuildNumber > currentBuildNumber) {
|
||
hasUpdate = true;
|
||
} else if (serverInfo.versionBuildNumber == currentBuildNumber) {
|
||
// buildNumber 相同,比较版本名
|
||
if (_compareVersionStrings(serverInfo.versionName, currentVersionName) > 0) {
|
||
hasUpdate = true;
|
||
}
|
||
}
|
||
} else {
|
||
// 只比较版本名
|
||
if (_compareVersionStrings(serverInfo.versionName, currentVersionName) > 0) {
|
||
hasUpdate = true;
|
||
}
|
||
}
|
||
|
||
if (effectiveConfig.enableDebugLog) {
|
||
debugPrint('📊 版本比较结果: ${hasUpdate ? "有新版本" : "已是最新"}');
|
||
}
|
||
|
||
// 构建内部使用的 UpgradeInfo
|
||
final info = UpgradeInfo(
|
||
hasUpdate: hasUpdate,
|
||
isForceUpdate: serverInfo.isForce,
|
||
versionName: serverInfo.versionName,
|
||
versionBuildNumber: serverInfo.versionBuildNumber,
|
||
currentVersionName: currentVersionName,
|
||
currentBuildNumber: currentBuildNumber,
|
||
updateContent: serverInfo.updateContent,
|
||
downloadUrl: serverInfo.downloadUrl,
|
||
appStoreUrl: serverInfo.appStoreUrl,
|
||
apkSize: serverInfo.apkSize,
|
||
apkMd5: serverInfo.apkMd5,
|
||
appMarkets: serverInfo.appMarkets,
|
||
supportedMethods: serverInfo.supportedMethods ??
|
||
const [AppUpgradeMethod.market, AppUpgradeMethod.browser, AppUpgradeMethod.inApp],
|
||
);
|
||
|
||
if (!info.hasUpdate) {
|
||
if (finalShowNoUpdateToast && context.mounted) {
|
||
_showToast('已是最新版本', context, effectiveConfig);
|
||
}
|
||
onComplete?.call();
|
||
return;
|
||
}
|
||
|
||
await _showUpgradeDialog(
|
||
context: context,
|
||
info: info,
|
||
autoDownload: finalAutoDownload,
|
||
autoInstall: finalAutoInstall,
|
||
onComplete: onComplete,
|
||
config: effectiveConfig,
|
||
);
|
||
} catch (e) {
|
||
debugPrint('检查更新失败: $e');
|
||
|
||
if (context.mounted) {
|
||
_showToast('检查更新遇到问题', context, effectiveConfig);
|
||
}
|
||
onComplete?.call();
|
||
}
|
||
}
|
||
|
||
/// 预下载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<Map<String, String>> getCurrentAppInfo() async {
|
||
try {
|
||
return await _plugin.getAppInfo();
|
||
} catch (e) {
|
||
debugPrint('获取应用信息失败: $e');
|
||
return {};
|
||
}
|
||
}
|
||
|
||
/// 精确检测应用是否已安装(通过包名)
|
||
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;
|
||
}
|
||
}
|
||
|
||
/// 比较版本号是否已更新
|
||
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) {
|
||
if (currentBuildNumber < targetBuildNumber) {
|
||
return false;
|
||
} else if (currentBuildNumber > targetBuildNumber) {
|
||
return true;
|
||
}
|
||
return _compareVersionStrings(currentVersion, targetVersion) >= 0;
|
||
}
|
||
|
||
return _compareVersionStrings(currentVersion, targetVersion) >= 0;
|
||
} catch (e) {
|
||
debugPrint('版本对比失败: $e');
|
||
return false;
|
||
}
|
||
}
|
||
|
||
int _compareVersionStrings(String v1, String v2) {
|
||
try {
|
||
final v1Parts = v1.split('.').map((e) => int.tryParse(e) ?? 0).toList();
|
||
final v2Parts = v2.split('.').map((e) => int.tryParse(e) ?? 0).toList();
|
||
|
||
final maxLength = v1Parts.length > v2Parts.length ? v1Parts.length : v2Parts.length;
|
||
|
||
for (int i = 0; i < maxLength; i++) {
|
||
final part1 = i < v1Parts.length ? v1Parts[i] : 0;
|
||
final part2 = i < v2Parts.length ? v2Parts[i] : 0;
|
||
|
||
if (part1 < part2) {
|
||
return -1;
|
||
} else if (part1 > part2) {
|
||
return 1;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
} catch (e) {
|
||
return v1.compareTo(v2);
|
||
}
|
||
}
|
||
|
||
/// 显示升级对话框
|
||
Future<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, context, effectiveConfig),
|
||
);
|
||
}
|
||
},
|
||
);
|
||
}
|
||
|
||
/// 显示Toast提示
|
||
void _showToast(String message, BuildContext context, [UpgradeConfig? config]) {
|
||
final effectiveConfig = config ?? _config;
|
||
if (effectiveConfig.customToast != null) {
|
||
effectiveConfig.customToast!(message);
|
||
} else {
|
||
// 检查 context 是否有效且有 Scaffold
|
||
if (!context.mounted) {
|
||
debugPrint('Toast消息(context已卸载): $message');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 尝试获取 ScaffoldMessenger,如果失败则使用 debugPrint
|
||
final scaffoldMessenger = ScaffoldMessenger.maybeOf(context);
|
||
if (scaffoldMessenger != null) {
|
||
scaffoldMessenger.showSnackBar(
|
||
SnackBar(
|
||
content: Text(message),
|
||
duration: Duration(seconds: 2),
|
||
),
|
||
);
|
||
} else {
|
||
// 如果没有 Scaffold,使用 debugPrint 作为后备方案
|
||
debugPrint('Toast消息(无Scaffold): $message');
|
||
}
|
||
} catch (e) {
|
||
// 如果出现任何错误,使用 debugPrint 作为后备方案
|
||
debugPrint('显示Toast失败: $e');
|
||
debugPrint('Toast消息: $message');
|
||
}
|
||
}
|
||
}
|
||
|
||
bool _canShowMaterialDialog(BuildContext context) {
|
||
if (!context.mounted) return false;
|
||
try {
|
||
return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations) != null;
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 共享的升级操作逻辑
|
||
mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
|
||
final _plugin = _SimpleAppUpgradePlugin.instance;
|
||
bool _isDownloading = false;
|
||
double _downloadProgress = 0;
|
||
String _statusText = '';
|
||
String? _downloadedFilePath;
|
||
bool _isInstalling = false;
|
||
bool _isWaitingForInstallation = false;
|
||
Timer? _installCheckTimer;
|
||
|
||
UpgradeInfo get info;
|
||
void Function(String) get showToast;
|
||
VoidCallback? get onComplete;
|
||
bool get autoDownload;
|
||
bool get autoInstall;
|
||
UpgradeConfig get config;
|
||
|
||
void initUpgradeLogic() {
|
||
if (autoDownload && Platform.isAndroid) {
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (mounted) {
|
||
_startDownloadAndInstall();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
void disposeUpgradeLogic() {
|
||
_installCheckTimer?.cancel();
|
||
}
|
||
|
||
void onAppLifecycleStateChanged(AppLifecycleState state) {
|
||
debugPrint('🔄 应用生命周期状态变化: $state, _isWaitingForInstallation=$_isWaitingForInstallation');
|
||
|
||
if (_isWaitingForInstallation && state == AppLifecycleState.resumed) {
|
||
debugPrint('⚡ 应用回到前台,检查安装状态');
|
||
Future.delayed(const Duration(milliseconds: 1500), () {
|
||
if (mounted && _isWaitingForInstallation) {
|
||
_checkInstallationResult();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
/// 前往浏览器
|
||
void _goToBrowser() async {
|
||
final downloadApkUrl = info.downloadUrl;
|
||
if (!Platform.isAndroid || downloadApkUrl == null) {
|
||
showToast('下载地址为空');
|
||
return;
|
||
}
|
||
if (!mounted) return;
|
||
|
||
try {
|
||
final uri = Uri.parse(downloadApkUrl);
|
||
try {
|
||
final launched = await launchUrl(
|
||
uri,
|
||
mode: LaunchMode.externalApplication,
|
||
);
|
||
if (!launched) {
|
||
await launchUrl(
|
||
uri,
|
||
mode: LaunchMode.platformDefault,
|
||
);
|
||
}
|
||
// 不再关闭弹窗,即使用户跳转到浏览器
|
||
// 原代码:
|
||
// if (mounted && Navigator.canPop(context)) { ... }
|
||
} catch (launchError) {
|
||
debugPrint('launchUrl 失败: $launchError');
|
||
final canLaunch = await canLaunchUrl(uri);
|
||
if (canLaunch) {
|
||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||
// 不再关闭弹窗
|
||
} else {
|
||
showToast('无法打开下载链接,请检查是否安装了浏览器');
|
||
}
|
||
}
|
||
} catch (e) {
|
||
debugPrint('打开浏览器失败: $e');
|
||
showToast('打开浏览器失败: ${e.toString()}');
|
||
}
|
||
}
|
||
|
||
Future<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;
|
||
});
|
||
|
||
try {
|
||
final filePath = await _plugin.downloadApk(
|
||
info.downloadUrl!,
|
||
onProgress: (p) {
|
||
if (!mounted) return;
|
||
setState(() {
|
||
_downloadProgress = p.progress;
|
||
_statusText = '下载中 ${p.percentage}%';
|
||
});
|
||
},
|
||
);
|
||
|
||
if (!mounted) return;
|
||
|
||
if (filePath == null) {
|
||
setState(() {
|
||
_isDownloading = false;
|
||
_statusText = '下载失败';
|
||
});
|
||
showToast('下载失败,请稍后重试');
|
||
return;
|
||
}
|
||
|
||
setState(() {
|
||
_statusText = '下载完成';
|
||
_downloadProgress = 1.0;
|
||
_downloadedFilePath = filePath;
|
||
});
|
||
|
||
if (autoInstall) {
|
||
await _installApk(filePath);
|
||
}
|
||
} catch (e) {
|
||
if (!mounted) return;
|
||
debugPrint('下载APK异常: $e');
|
||
setState(() {
|
||
_isDownloading = false;
|
||
_statusText = '下载失败';
|
||
});
|
||
// 显示用户友好的错误提示
|
||
final errorMessage = e.toString().replaceFirst('Exception: ', '');
|
||
showToast(errorMessage.isNotEmpty ? errorMessage : '下载失败,请稍后重试');
|
||
}
|
||
}
|
||
|
||
Future<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.getAppInfo();
|
||
final currentVersion = appInfo['version'] ?? '';
|
||
final currentBuildNumber = int.tryParse(appInfo['buildNumber'] ?? '0') ?? 0;
|
||
|
||
debugPrint('📱 当前版本: $currentVersion, 构建号: $currentBuildNumber');
|
||
debugPrint('🎯 目标版本: ${info.versionName}, 构建号: ${info.versionBuildNumber}');
|
||
|
||
bool isUpdated = false;
|
||
if (info.versionBuildNumber > 0) {
|
||
isUpdated = currentBuildNumber >= info.versionBuildNumber;
|
||
debugPrint('📊 构建号比较: $currentBuildNumber >= ${info.versionBuildNumber} = $isUpdated');
|
||
} else {
|
||
isUpdated = currentVersion == info.versionName;
|
||
debugPrint('📊 版本号比较: $currentVersion == ${info.versionName} = $isUpdated');
|
||
}
|
||
|
||
if (isUpdated) {
|
||
debugPrint('✅ 检测结果: 安装成功');
|
||
_handleInstallationSuccess();
|
||
} else {
|
||
debugPrint('❌ 检测结果: 安装被取消(版本未更新)');
|
||
_handleInstallationCancelled();
|
||
}
|
||
} catch (e) {
|
||
debugPrint('❌ 检测失败: $e');
|
||
setState(() {
|
||
_isWaitingForInstallation = false;
|
||
_statusText = '检测失败';
|
||
});
|
||
showToast('无法检测安装状态,请手动确认');
|
||
}
|
||
}
|
||
|
||
void _handleInstallationSuccess() {
|
||
_installCheckTimer?.cancel();
|
||
setState(() {
|
||
_isWaitingForInstallation = false;
|
||
_statusText = '安装成功';
|
||
});
|
||
showToast('应用更新成功!');
|
||
|
||
Future.delayed(const Duration(seconds: 1), () {
|
||
if (mounted && Navigator.canPop(context)) {
|
||
Navigator.of(context).pop();
|
||
}
|
||
onComplete?.call();
|
||
});
|
||
}
|
||
|
||
void _handleInstallationCancelled() {
|
||
setState(() {
|
||
_isWaitingForInstallation = false;
|
||
_statusText = '安装被取消';
|
||
});
|
||
showToast('安装被取消,可以点击重试按钮重新安装');
|
||
}
|
||
|
||
Future<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: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(
|
||
Icons.rocket_launch,
|
||
size: 14,
|
||
color: colorScheme.onPrimary,
|
||
),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
'v${info.versionName} +${info.versionBuildNumber}',
|
||
style: TextStyle(
|
||
color: colorScheme.onPrimary,
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (info.isForceUpdate) ...[
|
||
const SizedBox(width: 8),
|
||
Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||
decoration: BoxDecoration(
|
||
color: colorScheme.error,
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
child: Text(
|
||
'强制更新',
|
||
style: TextStyle(
|
||
color: colorScheme.onError,
|
||
fontSize: 10,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
|
||
// 版本对比和大小信息
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: _buildInfoChip(
|
||
context,
|
||
icon: Icons.update,
|
||
label: '当前版本',
|
||
// 显示版本号
|
||
value: '${info.currentVersionName} +${info.currentBuildNumber}',
|
||
colorScheme: colorScheme,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
if (info.apkSize != null) ...[
|
||
const SizedBox(height: 8),
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: _buildInfoChip(
|
||
context,
|
||
icon: Icons.file_download,
|
||
label: '大小',
|
||
value: formatBytes(info.apkSize!),
|
||
colorScheme: colorScheme,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildInfoChip(
|
||
BuildContext context, {
|
||
required IconData icon,
|
||
required String label,
|
||
required String value,
|
||
required ColorScheme colorScheme,
|
||
}) {
|
||
return Container(
|
||
padding: const EdgeInsets.all(8),
|
||
decoration: BoxDecoration(
|
||
color: colorScheme.surface,
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(
|
||
color: colorScheme.outline.withOpacity(0.2),
|
||
width: 0.5,
|
||
),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(
|
||
icon,
|
||
size: 12,
|
||
color: colorScheme.onSurface.withOpacity(0.6),
|
||
),
|
||
const SizedBox(width: 4),
|
||
Text(
|
||
label,
|
||
style: TextStyle(
|
||
fontSize: 10,
|
||
color: colorScheme.onSurface.withOpacity(0.6),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
value,
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: colorScheme.onSurface,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildUpdateContent(BuildContext context) {
|
||
final colorScheme = Theme.of(context).colorScheme;
|
||
final changeItems =
|
||
info.updateContent.split(RegExp(r'\r?\n')).map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Icon(
|
||
Icons.new_releases_outlined,
|
||
size: 16,
|
||
color: colorScheme.primary,
|
||
),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
'更新内容',
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.bold,
|
||
color: colorScheme.onSurface,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 12),
|
||
Container(
|
||
width: double.infinity,
|
||
constraints: const BoxConstraints(maxHeight: 200),
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: colorScheme.surfaceContainerHighest.withOpacity(0.3),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(
|
||
color: colorScheme.outline.withOpacity(0.2),
|
||
width: 0.5,
|
||
),
|
||
),
|
||
child: SingleChildScrollView(
|
||
child: changeItems.isEmpty
|
||
? _buildRichText(
|
||
info.updateContent,
|
||
colorScheme,
|
||
)
|
||
: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: changeItems.asMap().entries.map((entry) {
|
||
final index = entry.key;
|
||
final line = entry.value;
|
||
return Container(
|
||
width: double.infinity,
|
||
margin: EdgeInsets.only(
|
||
bottom: index < changeItems.length - 1 ? 8 : 0,
|
||
),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Container(
|
||
margin: const EdgeInsets.only(top: 6),
|
||
width: 4,
|
||
height: 4,
|
||
decoration: BoxDecoration(
|
||
color: colorScheme.primary,
|
||
shape: BoxShape.circle,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: _buildRichText(
|
||
line,
|
||
colorScheme,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildRichText(String content, ColorScheme colorScheme) {
|
||
final spans = <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,
|
||
);
|
||
}
|
||
|
||
Widget _buildEnhancedDownloadProgress(BuildContext context, ColorScheme colorScheme) {
|
||
final bool showRetryButton = _downloadedFilePath != null &&
|
||
!_isDownloading &&
|
||
!_isInstalling &&
|
||
!_isWaitingForInstallation &&
|
||
_downloadProgress >= 1.0 &&
|
||
_statusText != '安装成功';
|
||
|
||
debugPrint('重试按钮显示条件检查:');
|
||
debugPrint(' _downloadedFilePath != null: ${_downloadedFilePath != null}');
|
||
debugPrint(' !_isDownloading: ${!_isDownloading}');
|
||
debugPrint(' !_isInstalling: ${!_isInstalling}');
|
||
debugPrint(' !_isWaitingForInstallation: ${!_isWaitingForInstallation}');
|
||
debugPrint(' _downloadProgress >= 1.0: ${_downloadProgress >= 1.0}');
|
||
debugPrint(' _statusText != "安装成功": ${_statusText != '安装成功'}');
|
||
debugPrint(' showRetryButton: $showRetryButton');
|
||
debugPrint(' 当前状态: $_statusText');
|
||
|
||
return Column(
|
||
children: [
|
||
const SizedBox(height: 20),
|
||
GestureDetector(
|
||
onTap: _shouldShowRetryOptions() ? _retryInstall : null,
|
||
child: Container(
|
||
padding: const EdgeInsets.all(16),
|
||
decoration: BoxDecoration(
|
||
color: colorScheme.primaryContainer.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(
|
||
color: colorScheme.primary.withOpacity(0.2),
|
||
width: 1,
|
||
),
|
||
boxShadow: _shouldShowRetryOptions()
|
||
? [
|
||
BoxShadow(
|
||
color: colorScheme.primary.withOpacity(0.1),
|
||
blurRadius: 4,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
]
|
||
: null,
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Row(
|
||
children: [
|
||
AnimatedContainer(
|
||
duration: const Duration(milliseconds: 300),
|
||
padding: const EdgeInsets.all(8),
|
||
decoration: BoxDecoration(
|
||
color: _getStatusColor(colorScheme).withOpacity(0.1),
|
||
shape: BoxShape.circle,
|
||
),
|
||
child: AnimatedSwitcher(
|
||
duration: const Duration(milliseconds: 300),
|
||
child: Icon(
|
||
_getStatusIcon(),
|
||
key: ValueKey(_statusText),
|
||
color: _getStatusColor(colorScheme),
|
||
size: 20,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(width: 12),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
AnimatedSwitcher(
|
||
duration: const Duration(milliseconds: 300),
|
||
child: Text(
|
||
_statusText,
|
||
key: ValueKey(_statusText),
|
||
style: TextStyle(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w500,
|
||
color: colorScheme.onSurface,
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 2),
|
||
if (_isDownloading || _downloadProgress < 1.0)
|
||
Text(
|
||
'${(_downloadProgress * 100).toStringAsFixed(1)}%',
|
||
style: TextStyle(
|
||
fontSize: 12,
|
||
color: colorScheme.primary,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
if (_shouldShowRetryOptions())
|
||
Text(
|
||
_getClickHintText(),
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
color: colorScheme.onSurface.withOpacity(0.6),
|
||
),
|
||
),
|
||
if (_statusText == '请完成安装')
|
||
Padding(
|
||
padding: const EdgeInsets.only(top: 12),
|
||
child: Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: colorScheme.primaryContainer.withOpacity(0.2),
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(
|
||
color: colorScheme.primary.withOpacity(0.3),
|
||
width: 1,
|
||
),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
Icon(
|
||
Icons.touch_app,
|
||
color: colorScheme.primary,
|
||
size: 24,
|
||
),
|
||
const SizedBox(height: 8),
|
||
Text(
|
||
'请在系统安装界面完成操作',
|
||
style: TextStyle(
|
||
fontSize: 13,
|
||
color: colorScheme.onSurface,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
const SizedBox(height: 4),
|
||
Text(
|
||
'系统将自动检测安装结果',
|
||
style: TextStyle(
|
||
fontSize: 11,
|
||
color: colorScheme.onSurface.withOpacity(0.7),
|
||
),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (_shouldShowRetryOptions())
|
||
Container(
|
||
margin: const EdgeInsets.only(left: 8),
|
||
child: ElevatedButton.icon(
|
||
onPressed: _retryInstall,
|
||
icon: Icon(_getRetryButtonIcon(), size: 16),
|
||
label: Text(_getRetryButtonText(), style: const TextStyle(fontSize: 12)),
|
||
style: ElevatedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
minimumSize: Size.zero,
|
||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
backgroundColor: _getRetryButtonColor(colorScheme),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
if (_isDownloading || (_downloadProgress > 0 && _downloadProgress < 1.0)) ...[
|
||
const SizedBox(height: 16),
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.circular(4),
|
||
child: Stack(
|
||
children: [
|
||
Container(
|
||
height: 8,
|
||
width: double.infinity,
|
||
decoration: BoxDecoration(
|
||
color: colorScheme.outline.withOpacity(0.1),
|
||
borderRadius: BorderRadius.circular(4),
|
||
),
|
||
),
|
||
AnimatedContainer(
|
||
duration: const Duration(milliseconds: 300),
|
||
curve: Curves.easeOut,
|
||
height: 8,
|
||
width: double.infinity,
|
||
alignment: Alignment.centerLeft,
|
||
child: FractionallySizedBox(
|
||
widthFactor: _downloadProgress.clamp(0.0, 1.0),
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors: [
|
||
colorScheme.primary,
|
||
colorScheme.primary.withOpacity(0.8),
|
||
],
|
||
begin: Alignment.centerLeft,
|
||
end: Alignment.centerRight,
|
||
),
|
||
borderRadius: BorderRadius.circular(4),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: colorScheme.primary.withOpacity(0.3),
|
||
blurRadius: 4,
|
||
offset: const Offset(0, 2),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
IconData _getStatusIcon() {
|
||
if (_isInstalling) {
|
||
return Icons.install_mobile;
|
||
} else if (_isDownloading) {
|
||
return Icons.download_for_offline;
|
||
} else if (_statusText == '安装成功') {
|
||
return Icons.check_circle;
|
||
} else if (_statusText == '请完成安装') {
|
||
return Icons.touch_app;
|
||
} else if (_statusText == '等待安装中') {
|
||
return Icons.hourglass_bottom;
|
||
} else if (_statusText == '等待确认中') {
|
||
return Icons.help_outline;
|
||
} else if (_statusText == '等待超时') {
|
||
return Icons.timer_off;
|
||
} else if (_statusText == '安装被取消') {
|
||
return Icons.cancel_outlined;
|
||
} else if (_statusText == '安装超时' || _statusText == '检测失败') {
|
||
return Icons.schedule;
|
||
} else if (_statusText == '安装失败' || _statusText == '安装异常' || _statusText == '权限被拒绝') {
|
||
return Icons.error_outline;
|
||
} else if (_downloadProgress >= 1.0) {
|
||
return Icons.check_circle_outline;
|
||
} else {
|
||
return Icons.download_for_offline;
|
||
}
|
||
}
|
||
|
||
Color _getStatusColor(ColorScheme colorScheme) {
|
||
if (_isInstalling) {
|
||
return colorScheme.secondary;
|
||
} else if (_statusText == '安装成功') {
|
||
return colorScheme.tertiary;
|
||
} else if (_statusText == '请完成安装') {
|
||
return colorScheme.secondary;
|
||
} else if (_statusText == '等待安装中') {
|
||
return colorScheme.secondary.withOpacity(0.9);
|
||
} else if (_statusText == '等待确认中') {
|
||
return Colors.orange;
|
||
} else if (_statusText == '等待超时') {
|
||
return colorScheme.error.withOpacity(0.7);
|
||
} else if (_statusText == '安装被取消') {
|
||
return colorScheme.secondary.withOpacity(0.8);
|
||
} else if (_statusText == '安装超时' || _statusText == '检测失败') {
|
||
return colorScheme.secondary.withOpacity(0.7);
|
||
} else if (_statusText == '安装失败' || _statusText == '安装异常' || _statusText == '权限被拒绝') {
|
||
return colorScheme.error;
|
||
} else if (_downloadProgress >= 1.0) {
|
||
return colorScheme.tertiary;
|
||
} else {
|
||
return colorScheme.primary;
|
||
}
|
||
}
|
||
|
||
IconData _getRetryButtonIcon() {
|
||
if (_statusText == '权限被拒绝' && config.requireInstallPermission) {
|
||
return Icons.settings;
|
||
} else if (_statusText == '安装失败' ||
|
||
_statusText == '安装异常' ||
|
||
_statusText == '安装超时' ||
|
||
_statusText == '安装被取消' ||
|
||
_statusText == '检测失败' ||
|
||
_statusText == '等待安装中' ||
|
||
_statusText == '等待确认中' ||
|
||
_statusText == '等待超时') {
|
||
return Icons.refresh;
|
||
} else if (_statusText == '请完成安装') {
|
||
return Icons.launch;
|
||
} else {
|
||
return Icons.install_mobile;
|
||
}
|
||
}
|
||
|
||
String _getRetryButtonText() {
|
||
if (_statusText == '权限被拒绝' && config.requireInstallPermission) {
|
||
return '设置';
|
||
} else if (_statusText == '安装失败' ||
|
||
_statusText == '安装异常' ||
|
||
_statusText == '安装超时' ||
|
||
_statusText == '安装被取消' ||
|
||
_statusText == '检测失败' ||
|
||
_statusText == '等待超时') {
|
||
return '重试';
|
||
} else if (_statusText == '等待安装中' || _statusText == '等待确认中') {
|
||
return '重新安装';
|
||
} else if (_statusText == '请完成安装') {
|
||
return '重新安装';
|
||
} else {
|
||
return '安装';
|
||
}
|
||
}
|
||
|
||
Color? _getRetryButtonColor(ColorScheme colorScheme) {
|
||
if (_statusText == '权限被拒绝') {
|
||
return colorScheme.secondary;
|
||
} else if (_statusText == '安装失败' || _statusText == '安装异常') {
|
||
return colorScheme.error;
|
||
} else if (_statusText == '安装超时' || _statusText == '安装被取消' || _statusText == '检测失败' || _statusText == '等待安装中') {
|
||
return colorScheme.secondary.withOpacity(0.8);
|
||
} else if (_statusText == '请完成安装') {
|
||
return colorScheme.secondary;
|
||
} else {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
bool _shouldShowRetryOptions() {
|
||
return _statusText == '安装被取消' ||
|
||
_statusText == '安装失败' ||
|
||
_statusText == '安装异常' ||
|
||
(_statusText == '权限被拒绝' && config.requireInstallPermission) ||
|
||
_statusText == '安装超时' ||
|
||
_statusText == '检测失败' ||
|
||
_statusText == '等待安装中' ||
|
||
_statusText == '等待确认中' ||
|
||
_statusText == '等待超时' ||
|
||
(_downloadedFilePath != null &&
|
||
!_isDownloading &&
|
||
!_isInstalling &&
|
||
!_isWaitingForInstallation &&
|
||
_downloadProgress >= 1.0 &&
|
||
_statusText != '安装成功');
|
||
}
|
||
|
||
String _getClickHintText() {
|
||
switch (_statusText) {
|
||
case '请完成安装':
|
||
return '完成后请点击确认按钮';
|
||
case '等待安装中':
|
||
return '点击重新安装';
|
||
case '等待确认中':
|
||
return '请点击确认按钮';
|
||
case '等待超时':
|
||
return '点击重新安装';
|
||
case '安装被取消':
|
||
return '点击重新安装';
|
||
case '权限被拒绝':
|
||
return config.requireInstallPermission ? '点击打开设置' : '点击重试';
|
||
case '安装超时':
|
||
return '点击重新尝试';
|
||
default:
|
||
return '点击区域重试';
|
||
}
|
||
}
|
||
|
||
void _handleAction() {
|
||
if (Platform.isAndroid) {
|
||
_handleAndroidAction();
|
||
} else if (Platform.isIOS) {
|
||
_handleIosAction(context);
|
||
} else {
|
||
showToast('Unsupported platform');
|
||
}
|
||
}
|
||
|
||
void _handleIosAction(BuildContext context) {
|
||
if (info.appStoreUrl != null) {
|
||
_plugin.goToAppStore(info.appStoreUrl!, context: context);
|
||
// 移除关闭弹窗代码,始终不关闭
|
||
onComplete?.call();
|
||
} else {
|
||
showToast('App Store URL is not available.');
|
||
}
|
||
}
|
||
|
||
Future<void> _handleAndroidAction() async {
|
||
final List<AppUpgradeMethod> availableMethods = [];
|
||
final supported = info.supportedMethods;
|
||
|
||
if (supported.contains(AppUpgradeMethod.market)) {
|
||
availableMethods.add(AppUpgradeMethod.market);
|
||
}
|
||
|
||
if (info.downloadUrl != null) {
|
||
if (supported.contains(AppUpgradeMethod.browser)) {
|
||
availableMethods.add(AppUpgradeMethod.browser);
|
||
}
|
||
if (supported.contains(AppUpgradeMethod.inApp)) {
|
||
availableMethods.add(AppUpgradeMethod.inApp);
|
||
}
|
||
}
|
||
|
||
debugPrint('可用更新方式: $availableMethods');
|
||
|
||
if (availableMethods.isEmpty) {
|
||
showToast('未找到可用的更新方式');
|
||
return;
|
||
}
|
||
|
||
if (availableMethods.length == 1) {
|
||
final method = availableMethods.first;
|
||
switch (method) {
|
||
case AppUpgradeMethod.market:
|
||
_handleMarketAction();
|
||
break;
|
||
case AppUpgradeMethod.browser:
|
||
_goToBrowser();
|
||
break;
|
||
case AppUpgradeMethod.inApp:
|
||
await _startDownloadAndInstall();
|
||
break;
|
||
}
|
||
return;
|
||
}
|
||
|
||
await _showDownloadChoiceSheet(availableMethods);
|
||
}
|
||
|
||
Future<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 ?? '', context: context);
|
||
},
|
||
);
|
||
} else {
|
||
final appInfo = await _plugin.getAppInfo();
|
||
final pkg = appInfo['packageName'] ?? '';
|
||
if (pkg.isNotEmpty) {
|
||
_plugin.goToAppStore('market://details?id=$pkg', context: context);
|
||
// _plugin.goToAppStore('market://details?id=com.yuanxuan.learningOfficerOa');
|
||
} else {
|
||
showToast('Could not determine app package name.');
|
||
}
|
||
}
|
||
}
|
||
|
||
Future<void> _handleMarketAction() async {
|
||
if (!mounted) return;
|
||
await _performMarketAction();
|
||
onComplete?.call();
|
||
// 移除关闭弹窗代码
|
||
}
|
||
|
||
Future<void> _showDownloadChoiceSheet(List<AppUpgradeMethod> availableMethods) async {
|
||
if (!mounted) return;
|
||
|
||
final choice = await showModalBottomSheet<AppUpgradeMethod>(
|
||
context: context,
|
||
isScrollControlled: false,
|
||
useRootNavigator: true,
|
||
isDismissible: !info.isForceUpdate,
|
||
enableDrag: !info.isForceUpdate,
|
||
shape: const RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||
),
|
||
builder: (ctx) {
|
||
return SafeArea(
|
||
child: Padding(
|
||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
width: 40,
|
||
height: 4,
|
||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: Colors.grey.shade300,
|
||
borderRadius: BorderRadius.circular(2),
|
||
),
|
||
),
|
||
Padding(
|
||
padding: const EdgeInsets.only(bottom: 8.0),
|
||
child: Stack(
|
||
alignment: Alignment.center,
|
||
children: [
|
||
const Text('选择更新方式', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||
if (!info.isForceUpdate)
|
||
Positioned(
|
||
right: -12,
|
||
child: IconButton(
|
||
icon: const Icon(Icons.close),
|
||
onPressed: () => Navigator.of(ctx).pop(),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
if (availableMethods.contains(AppUpgradeMethod.market))
|
||
ListTile(
|
||
leading: const Icon(Icons.storefront_outlined),
|
||
title: const Text('前往应用市场更新'),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.market),
|
||
),
|
||
if (availableMethods.contains(AppUpgradeMethod.inApp))
|
||
ListTile(
|
||
leading: const Icon(Icons.system_update),
|
||
title: const Text('APP内更新'),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.inApp),
|
||
),
|
||
if (availableMethods.contains(AppUpgradeMethod.browser))
|
||
ListTile(
|
||
leading: const Icon(Icons.download_for_offline_outlined),
|
||
title: const Text('前往浏览器下载安装包'),
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
onTap: () => Navigator.of(ctx).pop(AppUpgradeMethod.browser),
|
||
),
|
||
const Divider(height: 24),
|
||
if (!info.isForceUpdate)
|
||
SizedBox(
|
||
width: double.infinity,
|
||
child: ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||
backgroundColor: Colors.white,
|
||
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
|
||
elevation: 2,
|
||
shadowColor: Colors.grey.withOpacity(0.5),
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(12),
|
||
side: BorderSide(color: Colors.grey.shade300),
|
||
),
|
||
),
|
||
onPressed: () => Navigator.of(ctx).pop(),
|
||
child: const Text('取消', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
|
||
// 不再关闭弹窗,即使用户选择了更新方式
|
||
if (choice == AppUpgradeMethod.market) {
|
||
await _performMarketAction();
|
||
onComplete?.call();
|
||
return;
|
||
}
|
||
|
||
if (choice == AppUpgradeMethod.inApp && !_isDownloading) {
|
||
await _startDownloadAndInstall();
|
||
return;
|
||
}
|
||
|
||
if (choice == AppUpgradeMethod.browser && !_isDownloading) {
|
||
_goToBrowser();
|
||
}
|
||
}
|
||
}
|
||
|
||
class _SimpleUpgradeDialog extends StatefulWidget {
|
||
final UpgradeInfo info;
|
||
final bool autoDownload;
|
||
final bool autoInstall;
|
||
final VoidCallback? onComplete;
|
||
final void Function(String) showToast;
|
||
final UpgradeConfig config;
|
||
|
||
const _SimpleUpgradeDialog({
|
||
required this.info,
|
||
required this.autoDownload,
|
||
required this.autoInstall,
|
||
this.onComplete,
|
||
required this.showToast,
|
||
required this.config,
|
||
});
|
||
|
||
@override
|
||
State<_SimpleUpgradeDialog> createState() => _SimpleUpgradeDialogState();
|
||
}
|
||
|
||
class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _UpgradeDialogLogic, WidgetsBindingObserver {
|
||
@override
|
||
UpgradeInfo get info => widget.info;
|
||
@override
|
||
void Function(String) get showToast => widget.showToast;
|
||
@override
|
||
VoidCallback? get onComplete => widget.onComplete;
|
||
@override
|
||
bool get autoDownload => widget.autoDownload;
|
||
@override
|
||
bool get autoInstall => widget.autoInstall;
|
||
@override
|
||
UpgradeConfig get config => widget.config;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
WidgetsBinding.instance.addObserver(this);
|
||
initUpgradeLogic();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
WidgetsBinding.instance.removeObserver(this);
|
||
disposeUpgradeLogic();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
onAppLifecycleStateChanged(state);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Dialog(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
insetPadding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24),
|
||
child: Container(
|
||
width: 320,
|
||
constraints: const BoxConstraints(
|
||
maxHeight: 600,
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||
child: Row(
|
||
children: [
|
||
Icon(
|
||
Icons.system_update,
|
||
color: Theme.of(context).colorScheme.primary,
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Expanded(
|
||
child: Text(
|
||
'发现新版本',
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Flexible(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const SizedBox(height: 10),
|
||
_buildVersionInfoCard(context, Theme.of(context).colorScheme),
|
||
const SizedBox(height: 16),
|
||
_buildUpdateContent(context),
|
||
if (_isDownloading || _downloadedFilePath != null)
|
||
_buildEnhancedDownloadProgress(context, Theme.of(context).colorScheme),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
if (!_isDownloading)
|
||
Container(
|
||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.end,
|
||
children: [
|
||
TextButton(
|
||
onPressed: () {
|
||
Navigator.of(context).pop();
|
||
widget.onComplete?.call();
|
||
},
|
||
child: const Text('稍后更新'),
|
||
),
|
||
const SizedBox(width: 8),
|
||
ElevatedButton(
|
||
onPressed: _handleAction,
|
||
child: Text(Platform.isAndroid ? '立即更新' : '前往更新'),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _ForceUpgradeDialog extends StatefulWidget {
|
||
final UpgradeInfo info;
|
||
final UpgradeConfig config;
|
||
|
||
const _ForceUpgradeDialog({
|
||
required this.info,
|
||
required this.config,
|
||
});
|
||
|
||
@override
|
||
State<_ForceUpgradeDialog> createState() => _ForceUpgradeDialogState();
|
||
}
|
||
|
||
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> with _UpgradeDialogLogic, WidgetsBindingObserver {
|
||
@override
|
||
UpgradeInfo get info => widget.info;
|
||
@override
|
||
void Function(String) get showToast =>
|
||
(message) => AppUpgradeSimple.instance._showToast(message, context, widget.config);
|
||
@override
|
||
VoidCallback? get onComplete => null;
|
||
@override
|
||
bool get autoDownload => false;
|
||
@override
|
||
bool get autoInstall => true;
|
||
@override
|
||
UpgradeConfig get config => widget.config;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
WidgetsBinding.instance.addObserver(this);
|
||
initUpgradeLogic();
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
WidgetsBinding.instance.removeObserver(this);
|
||
disposeUpgradeLogic();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||
onAppLifecycleStateChanged(state);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return WillPopScope(
|
||
onWillPop: () async => false,
|
||
child: Dialog(
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(16),
|
||
),
|
||
insetPadding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24),
|
||
child: Container(
|
||
width: 320,
|
||
constraints: const BoxConstraints(
|
||
maxHeight: 600,
|
||
),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: [
|
||
Icon(
|
||
Icons.warning_amber_rounded,
|
||
color: Theme.of(context).colorScheme.error,
|
||
),
|
||
const SizedBox(width: 8),
|
||
const Expanded(
|
||
child: Text(
|
||
'发现新版本 (强制)',
|
||
style: TextStyle(
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
textAlign: TextAlign.center,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Flexible(
|
||
child: SingleChildScrollView(
|
||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 16),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
const SizedBox(height: 10),
|
||
_buildVersionInfoCard(context, Theme.of(context).colorScheme),
|
||
const SizedBox(height: 16),
|
||
_buildUpdateContent(context),
|
||
if (_isDownloading || _downloadedFilePath != null)
|
||
_buildEnhancedDownloadProgress(context, Theme.of(context).colorScheme),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
if (!_isDownloading)
|
||
Container(
|
||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||
child: SizedBox(
|
||
width: double.infinity,
|
||
child: ElevatedButton(
|
||
onPressed: _handleAction,
|
||
child: Text(Platform.isAndroid ? '立即更新' : '前往更新'),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|