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 get currentPackageInfo async => _cachedPackageInfo ??= await PackageInfo.fromPlatform(); /// 检测更新并在有新版时弹窗提示。 /// /// - [silentIfNoUpdate]=true(如启动时):无更新或检测失败均静默; /// - [silentIfNoUpdate]=false(如手动「检查更新」):无更新/失败给出明确反馈。 /// - [orgId] 当前登录用户所属组织 ID;传空时与旧行为一致。 Future 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( 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, ), ), ], ); } }