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