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,
|
|||
|
|
),
|
|||
|
|
),
|
|||
|
|
],
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
}
|