yx_app_upgrade_flutter/lib/app_upgrade_simple.dart

1153 lines
39 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:io';
import 'package:app_upgrade_plugin/widgets/market_selection_dialog.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'app_upgrade_plugin.dart';
import 'core/upgrade_utils.dart';
/// 简化版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({AppUpgradePlugin? plugin}) : _plugin = plugin ?? AppUpgradePlugin();
AppUpgradeSimple._() : _plugin = AppUpgradePlugin();
final AppUpgradePlugin _plugin;
/// 一键检查更新(最简单的使用方式)
///
/// 示例:
/// ```dart
/// AppUpgradeSimple.instance.checkUpdate(
/// context: context,
/// url: 'https://api.example.com/check-update',
/// );
/// ```
///
/// [context] 需要是一个能够访问到 `Navigator` 和 `MaterialLocalizations` 的有效上下文。
/// 如果您不确定,建议在 `init` 方法中提供 `navigatorKey`,这样插件可以获取一个可靠的上下文。
Future<void> checkUpdate({
required BuildContext context,
required String url,
Map<String, dynamic>? params,
bool showNoUpdateToast = true,
bool autoDownload = false,
bool autoInstall = false,
VoidCallback? onComplete,
}) async {
try {
assert(_canShowMaterialDialog(context), '请在 MaterialApp 环境内调用');
// 检查更新
final info = await _plugin.checkUpdate(url, params: params);
if (info == null || !info.hasUpdate) {
if (showNoUpdateToast && context.mounted) _showToast('已是最新版本');
onComplete?.call();
return;
}
await _showUpgradeDialog(
context: context,
info: info,
autoDownload: autoDownload,
autoInstall: autoInstall,
onComplete: onComplete,
);
} catch (e) {
debugPrint('检查更新失败: $e');
String errorMessage = '检查更新失败';
if (e.toString().contains('无网络连接')) {
errorMessage = '无网络连接,请检查网络设置';
} else if (e.toString().contains('Failed host lookup')) {
errorMessage = '无法连接到服务器,请检查网络或稍后重试';
} else if (e.toString().contains('Connection refused')) {
errorMessage = '服务器拒绝连接,请稍后重试';
} else if (e.toString().contains('timeout')) {
errorMessage = '连接超时,请检查网络';
} else {
errorMessage = '检查更新失败: ${e.toString().split(':').first}';
}
if (context.mounted) {
_showToast(errorMessage);
// 如果是网络问题,显示网络诊断建议
if (e.toString().contains('Failed host lookup') || e.toString().contains('无网络连接')) {
debugPrint('建议: 请检查网络连接或尝试使用网络诊断功能');
}
}
onComplete?.call();
}
}
/// 在无法显示对话框时的后备逻辑显示Toast并尝试后台下载/安装
Future<void> _showToastAndDownloadInBackground({
required BuildContext context,
required UpgradeInfo info,
required bool autoDownload,
required bool autoInstall,
}) async {
if (info.isForceUpdate) {
_showToast('有重要更新,但无法显示对话框。请在 MaterialApp 环境内重试。');
} else {
_showToast('发现新版本: ${info.versionName}');
if (autoDownload && Platform.isAndroid && info.downloadUrl != null) {
final filePath = await _plugin.downloadApk(info.downloadUrl!, onProgress: (_) {});
if (filePath != null && autoInstall) {
await _installApkHeadless(context, filePath);
}
}
}
}
/// 静默检查更新(不显示无更新提示)
Future<UpgradeInfo?> checkUpdateSilent({
required String url,
Map<String, dynamic>? params,
}) async {
try {
return await _plugin.checkUpdate(url, params: params);
} catch (e) {
debugPrint('静默检查更新失败: $e');
return null;
}
}
/// 显示升级对话框
Future<void> _showUpgradeDialog({
required BuildContext context,
required UpgradeInfo info,
required bool autoDownload,
required bool autoInstall,
VoidCallback? onComplete,
}) {
return showDialog(
context: context,
barrierDismissible: !info.isForceUpdate,
builder: (context) {
if (info.isForceUpdate) {
return _ForceUpgradeDialog(info: info);
} else {
return _SimpleUpgradeDialog(
info: info,
autoDownload: autoDownload,
autoInstall: autoInstall,
onComplete: onComplete,
showToast: (message) => _showToast(message),
);
}
},
);
}
/// 显示备用升级对话框当MaterialApp环境不可用时
void _showFallbackUpgradeDialog({
required BuildContext context,
required UpgradeInfo info,
required bool autoDownload,
required bool autoInstall,
VoidCallback? onComplete,
}) {
showGeneralDialog(
context: context,
barrierDismissible: !info.isForceUpdate,
barrierLabel: 'Dismiss', // Provide a non-localized label
pageBuilder: (buildContext, animation, secondaryAnimation) {
// 使用一个包装器来提供基本的文本样式和方向这对于独立于Material的对话框是必需的
return Directionality(
textDirection: TextDirection.ltr,
child: _FallbackUpgradeDialog(
info: info,
autoDownload: autoDownload,
autoInstall: autoInstall,
onComplete: onComplete,
showToast: (message) => _showToast(message),
),
);
},
).then((_) {
// The dialog has been dismissed.
onComplete?.call();
});
}
/// 显示Toast提示
void _showToast(String message) {
Fluttertoast.showToast(msg: message);
}
/// 检查是否存在可用的 Material 环境(仅用于对话框)
bool _canShowMaterialDialog(BuildContext context) {
if (!context.mounted) return false;
try {
// Localizations.of will throw if not found.
return Localizations.of<MaterialLocalizations>(context, MaterialLocalizations) != null;
} catch (_) {
// If it throws, it means we're not in a Material scope.
return false;
}
}
/// 共享的安装逻辑 (无UI)
Future<void> _installApkHeadless(BuildContext context, String filePath) async {
if (!Platform.isAndroid) return;
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context);
if (!hasPermission) {
_showToast('未授予安装权限,无法完成更新');
return;
}
final success = await _plugin.installApk(filePath);
if (!success) {
_showToast('安装失败,请手动安装');
}
}
}
/// 共享的升级对话框内容构建器
class _UpgradeDialogContent extends StatelessWidget {
final UpgradeInfo info;
final bool isDownloading;
final double downloadProgress;
final String statusText;
const _UpgradeDialogContent({
required this.info,
required this.isDownloading,
required this.downloadProgress,
required this.statusText,
});
@override
Widget build(BuildContext context) {
final List<String> changeItems =
info.updateContent.split(RegExp(r'\r?\n')).map((e) => e.trim()).where((e) => e.isNotEmpty).toList();
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'版本:${info.versionName}',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
if (info.apkSize != null) ...[
const SizedBox(height: 4),
Text('大小:${formatBytes(info.apkSize!)}'),
],
const SizedBox(height: 12),
const Text('更新内容:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Container(
constraints: const BoxConstraints(maxHeight: 220),
child: SingleChildScrollView(
child: changeItems.isEmpty
? Text(info.updateContent)
: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: changeItems
.map(
(line) => Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(''),
Expanded(child: Text(line)),
],
),
),
)
.toList(),
),
),
),
if (isDownloading) ...[
const SizedBox(height: 16),
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: LinearProgressIndicator(
value: downloadProgress.clamp(0.0, 1.0),
minHeight: 6,
),
),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(child: Text(statusText, overflow: TextOverflow.ellipsis)),
Text('${(downloadProgress * 100).toStringAsFixed(1)}%'),
],
),
],
],
);
}
}
/// 共享的升级操作逻辑
mixin _UpgradeDialogLogic<T extends StatefulWidget> on State<T> {
final _plugin = AppUpgradePlugin();
bool _isDownloading = false;
double _downloadProgress = 0;
String _statusText = '';
UpgradeInfo get info;
void Function(String) get showToast;
VoidCallback? get onComplete;
bool get autoDownload;
bool get autoInstall;
@override
void initState() {
super.initState();
if (autoDownload && Platform.isAndroid) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_startDownloadAndInstall();
}
});
}
}
Future<void> _startDownloadAndInstall() async {
if (!Platform.isAndroid || info.downloadUrl == null) return;
if (!mounted) return;
final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(context: context);
if (!hasStorage) {
showToast('缺少存储权限,无法下载');
return;
}
await PermissionHelper.checkAndRequestNotificationPermission(context: context);
setState(() {
_isDownloading = true;
_statusText = '下载中...';
_downloadProgress = 0;
});
final filePath = await _plugin.downloadApk(
info.downloadUrl!,
onProgress: (p) {
if (!mounted) return;
setState(() {
_downloadProgress = p.progress;
_statusText = '下载中 ${p.percentage}%';
});
},
);
if (!mounted) return;
if (filePath == null) {
setState(() {
_isDownloading = false;
_statusText = '下载失败';
});
return;
}
setState(() {
_statusText = '下载完成';
_downloadProgress = 1.0;
});
if (autoInstall) {
await _installApk(filePath);
}
}
Future<void> _installApk(String filePath) async {
if (!mounted) return;
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context);
if (!hasPermission) {
if (mounted) {
showToast('未授予安装权限,无法完成更新');
}
return;
}
final success = await _plugin.installApk(filePath);
if (!success && mounted) {
showToast('安装失败,请手动安装');
}
}
Future<void> _showInstallMethodChooser() async {
final bool hasMarkets = info.appMarkets != null && info.appMarkets!.isNotEmpty;
final bool hasDownload = info.downloadUrl != null;
if (!mounted) return;
// 始终提供“应用市场”入口;无具体列表则用通用 market:// 链接
final choice = await showModalBottomSheet<String>(
context: context,
isScrollControlled: false,
useRootNavigator: true,
// UI Beautification: Rounded corners
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Draggable handle
Container(
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
// Title and Close Button
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Stack(
alignment: Alignment.center,
children: [
const Text('选择更新方式', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Positioned(
right: -12,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(ctx).pop(),
),
),
],
),
),
// Option 1: App Market
ListTile(
leading: const Icon(Icons.storefront_outlined),
title: const Text('前往应用市场更新'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop('market'),
),
// Option 2: Direct Download
if (hasDownload)
ListTile(
leading: const Icon(Icons.download_for_offline_outlined),
title: const Text('直接下载安装包'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop('download'),
),
const Divider(height: 24),
// Cancel Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: Colors.white,
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
elevation: 2,
shadowColor: Colors.grey.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.shade300),
),
),
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
),
),
],
),
),
);
},
);
if (choice == 'market') {
if (hasMarkets) {
await MarketSelectionDialog.show(
context,
markets: info.appMarkets!,
onSelected: (market) async {
if (market.url != null && market.url!.isNotEmpty) {
_plugin.goToAppStore(market.url!);
} else {
final appInfo = await _plugin.getAppInfo();
final pkg = appInfo['packageName'] ?? '';
if (pkg.isNotEmpty) {
_plugin.goToAppStore('market://details?id=$pkg');
}
}
},
);
} else {
final appInfo = await _plugin.getAppInfo();
final pkg = appInfo['packageName'] ?? '';
if (pkg.isNotEmpty) {
_plugin.goToAppStore('market://details?id=$pkg');
}
}
if (!mounted) return;
Navigator.of(context).pop();
onComplete?.call();
return;
}
if (choice == 'download' && hasDownload && !_isDownloading) {
await _startDownloadAndInstall();
}
}
void _handleAction() {
if (Platform.isAndroid) {
_handleAndroidAction();
} else if (Platform.isIOS) {
_handleIosAction();
} else {
showToast('Unsupported platform');
}
}
/// Handles the upgrade action for iOS.
void _handleIosAction() {
if (info.appStoreUrl != null) {
_plugin.goToAppStore(info.appStoreUrl!);
// Pop the upgrade dialog.
if (Navigator.canPop(context)) {
Navigator.of(context).pop();
}
onComplete?.call();
} else {
showToast('App Store URL is not available.');
}
}
/// Handles the upgrade action for Android.
Future<void> _handleAndroidAction() async {
// On Android, we always assume a market option is available,
// because we can fall back to a generic market:// intent.
const bool hasMarketOption = true;
final bool hasDownloadOption = info.downloadUrl != null;
// This case is unlikely on Android, but kept for robustness.
if (!hasMarketOption && !hasDownloadOption) {
showToast('No update method available.');
return;
}
// If a download option is not available, going to the market is the only choice.
if (!hasDownloadOption) {
_handleMarketAction();
return;
}
// If a download option exists, always give the user a choice,
// as the market option is also implicitly available.
await _showDownloadChoiceSheet();
}
/// Opens the app store or shows a market selection dialog.
Future<void> _performMarketAction() async {
final hasMarkets = info.appMarkets?.isNotEmpty ?? false;
if (hasMarkets) {
await MarketSelectionDialog.show(
context,
markets: info.appMarkets!,
onSelected: (market) {
_plugin.goToAppStore(market.url ?? market.packageName ?? '');
},
);
} else {
// No specific markets, try a generic market link.
final appInfo = await _plugin.getAppInfo();
final pkg = appInfo['packageName'] ?? '';
if (pkg.isNotEmpty) {
_plugin.goToAppStore('market://details?id=$pkg');
// _plugin.goToAppStore('market://details?id=$pkg');
} else {
showToast('Could not determine app package name.');
}
}
}
/// Pops the current dialog and then performs the market action.
Future<void> _handleMarketAction() async {
// Pop the upgrade dialog before proceeding.
if (Navigator.canPop(context)) {
Navigator.of(context).pop();
}
if (!mounted) return;
await _performMarketAction();
onComplete?.call();
}
Future<void> _showDownloadChoiceSheet() async {
final bool hasDownload = info.downloadUrl != null;
if (!mounted) return;
final choice = await showModalBottomSheet<String>(
context: context,
isScrollControlled: false,
useRootNavigator: true,
// UI Beautification: Rounded corners
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Draggable handle
Container(
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
// Title and Close Button
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Stack(
alignment: Alignment.center,
children: [
const Text('选择更新方式', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Positioned(
right: -12,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(ctx).pop(),
),
),
],
),
),
// Option 1: App Market
ListTile(
leading: const Icon(Icons.storefront_outlined),
title: const Text('前往应用市场更新'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop('market'),
),
// Option 2: Direct Download
if (hasDownload)
ListTile(
leading: const Icon(Icons.download_for_offline_outlined),
title: const Text('直接下载安装包'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop('download'),
),
const Divider(height: 24),
// Cancel Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: Colors.white,
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
elevation: 2,
shadowColor: Colors.grey.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.shade300),
),
),
onPressed: () => Navigator.of(ctx).pop(),
child: const Text('取消', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
),
),
],
),
),
);
},
);
if (choice == 'market') {
await _performMarketAction();
onComplete?.call();
return;
}
if (choice == 'download' && hasDownload && !_isDownloading) {
await _startDownloadAndInstall();
}
}
}
/// 简化版升级对话框(非强制更新)
class _SimpleUpgradeDialog extends StatefulWidget {
final UpgradeInfo info;
final bool autoDownload;
final bool autoInstall;
final VoidCallback? onComplete;
final void Function(String) showToast;
const _SimpleUpgradeDialog({
required this.info,
required this.autoDownload,
required this.autoInstall,
this.onComplete,
required this.showToast,
});
@override
State<_SimpleUpgradeDialog> createState() => _SimpleUpgradeDialogState();
}
class _SimpleUpgradeDialogState extends State<_SimpleUpgradeDialog> with _UpgradeDialogLogic {
@override
UpgradeInfo get info => widget.info;
@override
void Function(String) get showToast => widget.showToast;
@override
VoidCallback? get onComplete => widget.onComplete;
@override
bool get autoDownload => widget.autoDownload;
@override
bool get autoInstall => widget.autoInstall;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
contentPadding: const EdgeInsets.fromLTRB(20, 8, 20, 16),
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
title: Row(
children: [
Icon(
Icons.system_update,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
const Text('发现新版本'),
],
),
content: _UpgradeDialogContent(
info: widget.info,
isDownloading: _isDownloading,
downloadProgress: _downloadProgress,
statusText: _statusText,
),
actionsAlignment: MainAxisAlignment.end,
actions: _isDownloading
? []
: [
if (!widget.info.isForceUpdate)
TextButton(
onPressed: () {
Navigator.of(context).pop();
widget.onComplete?.call();
},
child: const Text('稍后更新'),
),
ElevatedButton(
onPressed: _handleAction,
child: Text(Platform.isAndroid ? '立即更新' : '前往更新'),
),
],
);
}
}
/// 强制更新对话框
class _ForceUpgradeDialog extends StatefulWidget {
final UpgradeInfo info;
const _ForceUpgradeDialog({required this.info});
@override
State<_ForceUpgradeDialog> createState() => _ForceUpgradeDialogState();
}
class _ForceUpgradeDialogState extends State<_ForceUpgradeDialog> {
final _plugin = AppUpgradePlugin();
bool _isDownloading = false;
double _downloadProgress = 0;
String _statusText = '';
@override
void initState() {
super.initState();
// 强制更新也需用户交互后再开始下载
}
Future<void> _startDownload() async {
// 下载前申请权限 (Just-in-Time)
if (Platform.isAndroid) {
final hasStorage = await PermissionHelper.checkAndRequestStoragePermission(context: context);
if (!hasStorage) {
setState(() {
_statusText = '缺少存储权限,无法下载';
});
return;
}
// 通知权限仅为辅助
await PermissionHelper.checkAndRequestNotificationPermission(context: context);
}
if (widget.info.downloadUrl == null) {
setState(() {
_statusText = '下载地址无效';
});
return;
}
setState(() {
_isDownloading = true;
_statusText = '准备下载...';
});
final filePath = await _plugin.downloadApk(
widget.info.downloadUrl!,
onProgress: (p) {
if (!mounted) return;
setState(() {
_downloadProgress = p.progress;
_statusText = '下载中 ${p.percentage}%';
});
},
);
if (filePath != null) {
await _installApk(context, filePath);
}
}
// 进度已在 downloadApk 的回调中更新
Future<void> _installApk(BuildContext context, String filePath) async {
final hasPermission = await PermissionHelper.checkAndRequestInstallPermission(context: context);
if (!hasPermission) {
if (context.mounted) {
setState(() {
_statusText = '未授予安装权限,请手动授权后重试';
});
}
return;
}
await _plugin.installApk(filePath);
}
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return WillPopScope(
onWillPop: () async => false, // 强制更新,不允许返回
child: AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
contentPadding: const EdgeInsets.fromLTRB(20, 8, 20, 16),
actionsPadding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
title: Row(
children: [
Icon(
Icons.system_update,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
const Text('发现新版本 (强制)'),
],
),
content: _UpgradeDialogContent(
info: widget.info,
isDownloading: _isDownloading,
downloadProgress: _downloadProgress,
statusText: _statusText,
),
actionsAlignment: MainAxisAlignment.end,
actions: _isDownloading
? []
: [
ElevatedButton(
onPressed: () async {
// 强制更新下也先让用户选择安装方式
final hasMarkets = widget.info.appMarkets != null && widget.info.appMarkets!.isNotEmpty;
final hasDownload = widget.info.downloadUrl != null;
if (hasMarkets && hasDownload) {
final choice = await showModalBottomSheet<String>(
context: context,
isScrollControlled: false,
useRootNavigator: true,
// UI Beautification: Rounded corners
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (ctx) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Draggable handle
Container(
width: 40,
height: 4,
margin: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: Colors.grey.shade300,
borderRadius: BorderRadius.circular(2),
),
),
// Title and Close Button
Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Stack(
alignment: Alignment.center,
children: [
const Text('选择更新方式',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Positioned(
right: -12,
child: IconButton(
icon: const Icon(Icons.close),
onPressed: () => Navigator.of(ctx).pop(),
),
),
],
),
),
// Option 1: App Market
ListTile(
leading: const Icon(Icons.storefront_outlined),
title: const Text('前往应用市场更新'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop('market'),
),
// Option 2: Direct Download
if (hasDownload)
ListTile(
leading: const Icon(Icons.download_for_offline_outlined),
title: const Text('直接下载安装包'),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onTap: () => Navigator.of(ctx).pop('download'),
),
const Divider(height: 24),
// Cancel Button
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: Colors.white,
foregroundColor: Theme.of(context).textTheme.bodyLarge?.color,
elevation: 2,
shadowColor: Colors.grey.withOpacity(0.5),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.grey.shade300),
),
),
onPressed: () => Navigator.of(ctx).pop(),
child:
const Text('取消', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w500)),
),
),
],
),
),
);
},
);
if (choice == 'market') {
Navigator.of(context).pop();
MarketSelectionDialog.show(
context,
markets: widget.info.appMarkets!,
onSelected: (market) {
_plugin.goToAppStore(market.url ?? market.packageName ?? '');
},
);
return;
}
if (choice == 'download') {
await _startDownload();
return;
}
return;
}
if (hasMarkets) {
Navigator.of(context).pop();
MarketSelectionDialog.show(
context,
markets: widget.info.appMarkets!,
onSelected: (market) {
_plugin.goToAppStore(market.url ?? market.packageName ?? '');
},
);
return;
}
if (hasDownload) {
await _startDownload();
return;
}
},
child: const Text('立即更新'),
),
],
),
);
}
}
/// 当 [MaterialApp] 不可用时,提供一个基础的回退升级对话框
class _FallbackUpgradeDialog extends StatefulWidget {
final UpgradeInfo info;
final bool autoDownload;
final bool autoInstall;
final VoidCallback? onComplete;
final void Function(String) showToast;
const _FallbackUpgradeDialog({
required this.info,
required this.autoDownload,
required this.autoInstall,
this.onComplete,
required this.showToast,
});
@override
State<_FallbackUpgradeDialog> createState() => _FallbackUpgradeDialogState();
}
class _FallbackUpgradeDialogState extends State<_FallbackUpgradeDialog> with _UpgradeDialogLogic {
@override
UpgradeInfo get info => widget.info;
@override
void Function(String) get showToast => widget.showToast;
@override
VoidCallback? get onComplete => widget.onComplete;
@override
bool get autoDownload => widget.autoDownload;
@override
bool get autoInstall => widget.autoInstall;
@override
Widget build(BuildContext context) {
final title = '发现新版本${widget.info.isForceUpdate ? " (强制)" : ""}';
final body = Center(
child: Material(
type: MaterialType.transparency,
child: Container(
margin: const EdgeInsets.all(24.0),
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.0),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
title,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
DefaultTextStyle(
style: const TextStyle(color: Colors.black87, fontSize: 14),
child: _UpgradeDialogContent(
info: widget.info,
isDownloading: _isDownloading,
downloadProgress: _downloadProgress,
statusText: _statusText,
),
),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
if (!widget.info.isForceUpdate && !_isDownloading) ...[
GestureDetector(
onTap: () {
Navigator.of(context).pop();
widget.onComplete?.call();
},
child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: Text('稍后更新', style: TextStyle(color: Colors.blue)),
),
),
const SizedBox(width: 8),
],
GestureDetector(
onTap: _isDownloading ? null : _handleAction,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: _isDownloading ? Colors.grey : Colors.blue,
borderRadius: BorderRadius.circular(8),
),
child: Text(
_isDownloading
? '下载中...'
: Platform.isAndroid
? '立即更新'
: '前往更新',
style: const TextStyle(color: Colors.white),
),
),
),
],
),
],
),
),
),
);
return widget.info.isForceUpdate ? WillPopScope(onWillPop: () async => false, child: body) : body;
}
}