yx_app_upgrade_flutter/lib/app_upgrade_simple.dart

2228 lines
73 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:io';
import 'package:app_upgrade_plugin/core/upgrade_utils.dart';
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 '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 环境内调用');
// 1. 获取服务器版本信息
final serverInfo = await future();
if (serverInfo == null) {
// 获取失败或无数据,视作无更新(已是最新)
if (effectiveConfig.enableDebugLog) {
debugPrint('🔍 检查更新结果: 未返回版本信息');
}
onComplete?.call(true);
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(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);
}
}
/// 预下载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);
}