Files
CloudHome-Ops/lib/utils/app_update.dart

162 lines
4.9 KiB
Dart
Raw Permalink 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 'package:flutter/material.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import '../services/api_client.dart';
import '../services/update_service.dart';
import '../utils/i10n.dart';
/// 全局「已弹过更新提示」标记,避免会话内重复弹窗。
bool _promptedThisSession = false;
/// 全局当前 [PackageInfo] 缓存(首次读取后复用)。
PackageInfo? _cachedPackageInfo;
/// 取得当前 App 包信息(带缓存)。
Future<PackageInfo> get currentPackageInfo async =>
_cachedPackageInfo ??= await PackageInfo.fromPlatform();
/// 检测更新并在有新版时弹窗提示。
///
/// - [silentIfNoUpdate]=true(如启动时):无更新或检测失败均静默;
/// - [silentIfNoUpdate]=false(如手动「检查更新」):无更新/失败给出明确反馈。
/// - [orgId] 当前登录用户所属组织 ID;传空时与旧行为一致。
Future<void> checkAndPromptUpdate(
BuildContext context, {
bool silentIfNoUpdate = true,
String? orgId,
}) async {
final t = AppLocalizations.of(context);
try {
final pkg = await currentPackageInfo;
final service = UpdateService(packageName: pkg.packageName);
final currentCode = int.tryParse(pkg.buildNumber) ?? 0;
final result = await service.checkForUpdate(currentCode, orgId: orgId);
if (!context.mounted) return;
if (result.hasUpdate && result.latest != null) {
if (!_promptedThisSession) {
_promptedThisSession = true;
showUpdateDialog(context, result, pkg);
}
return;
}
if (!silentIfNoUpdate && context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.updateNoNew)),
);
}
} catch (e) {
// 任何异常均被捕获,不抛给调用方;仅非静默模式下给用户一句友好提示。
if (!silentIfNoUpdate && context.mounted) {
final detail = formatApiError(e, fallback: '网络异常,请稍后重试');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('检测更新失败:$detail')),
);
}
}
}
/// 版本更新提示对话框:展示当前 / 最新版本,并提供「立即更新」跳转下载。
void showUpdateDialog(
BuildContext context,
UpdateCheckResult result,
PackageInfo pkg,
) {
final t = AppLocalizations.of(context);
final latest = result.latest!;
showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: Row(
children: [
const Icon(Icons.system_update_alt, color: Colors.green),
const SizedBox(width: 8),
Text(t.updateAvailable),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_InfoRow(label: t.currentVersion, value: '${pkg.version} (${pkg.buildNumber})'),
const SizedBox(height: 6),
_InfoRow(
label: t.latestVersion,
value: '${latest.versionName} (${latest.versionCode})',
),
if (latest.apkUrl.isNotEmpty) ...[
const SizedBox(height: 6),
_InfoRow(label: t.downloadUrl, value: latest.apkUrl),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(t.updateLater),
),
FilledButton(
onPressed: () async {
Navigator.pop(ctx);
final url = latest.apkUrl;
if (url.isEmpty) return;
final uri = Uri.parse(url);
try {
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);
} else if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.updateOpenFailed)),
);
}
} catch (_) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(t.updateOpenFailed)),
);
}
}
},
child: Text(t.updateNow),
),
],
),
);
}
/// 对话框内的「标签:值」单行。
class _InfoRow extends StatelessWidget {
const _InfoRow({required this.label, required this.value});
final String label;
final String value;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 84,
child: Text(
label,
style: theme.textTheme.bodySmall?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
Expanded(
child: SelectableText(
value,
style: theme.textTheme.bodyMedium,
),
),
],
);
}
}