162 lines
4.9 KiB
Dart
162 lines
4.9 KiB
Dart
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,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|