// update_provider.dart — 应用更新检查(启动自动检查 + 定时轮询)。 // // 对照 jiu client/lib/providers/update_provider.dart:用 AsyncNotifier 的惰性 // build() 做「启动后延迟首查 + 每小时轮询」;shell 里 watch 一次即拉起整条流程。 // `_dismissed` 是本次进程内存标志(用户点「稍后」置真,防反复弹),每轮轮询重置。 // 强制更新(force_update)不受 dismiss 影响,由 UI 走不可关闭弹窗。 // // 拿到更新后,下载安装走 core/update/app_updater.dart(App 内下载,不再开浏览器)。 import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; import 'package:package_info_plus/package_info_plus.dart'; import '../services/api_config.dart'; /// 启动后首次检查的延迟(避开登录/首屏竞争)。 const _kInitialDelay = Duration(seconds: 3); /// 轮询间隔。 const _kPollInterval = Duration(hours: 1); /// 单次检查网络超时。 const _kCheckTimeout = Duration(seconds: 8); /// 一次更新检查的结果。 class AppUpdateInfo { const AppUpdateInfo({ required this.latestVersion, required this.buildNumber, required this.forceUpdate, required this.releaseNotes, required this.downloadUrls, required this.hasUpdate, }); final String latestVersion; final int buildNumber; final bool forceUpdate; final String releaseNotes; final Map downloadUrls; final bool hasUpdate; } /// 更新检查 Notifier。build() 惰性触发:延迟首查 + 每小时轮询。 class UpdateNotifier extends AsyncNotifier { Timer? _timer; Timer? _initialTimer; @override Future build() async { // 两个 timer 都在 onDispose 取消。初始延迟用可取消的 Timer(而非 // Future.delayed:其内部 timer 无法取消,provider 在延迟期间被 dispose // 时会悬挂 → widget 测试报 pending timer、生产留资源)。 ref.onDispose(() { _initialTimer?.cancel(); _timer?.cancel(); }); final ready = Completer(); _initialTimer = Timer(_kInitialDelay, ready.complete); await ready.future; // 若延迟期间被 dispose,_initialTimer 取消 → 永不 complete,build 中止 final first = await _check(); _timer = Timer.periodic(_kPollInterval, (_) async { state = AsyncValue.data(await _check()); }); return first; } /// 设置页「检查更新」手动触发:立即查一次并回结果。 Future forceCheck() async { final info = await _check(); state = AsyncValue.data(info); return info; } /// 拉取 `$kApiBaseUrl/version` 并与本地版本比较。网络/解析失败返回 null(静默)。 Future _check() async { try { final resp = await http .get(Uri.parse('$kApiBaseUrl/version')) .timeout(_kCheckTimeout); if (resp.statusCode != 200) return null; final data = jsonDecode(resp.body) as Map; final latestVersion = data['version'] as String? ?? '0.0.0'; final buildNumber = (data['build_number'] as num?)?.toInt() ?? 0; final forceUpdate = data['force_update'] as bool? ?? false; final releaseNotes = data['release_notes'] as String? ?? ''; final rawUrls = data['download_urls'] as Map? ?? const {}; final downloadUrls = rawUrls.map((k, v) => MapEntry(k, v?.toString() ?? '')); final pkg = await PackageInfo.fromPlatform(); final hasUpdate = _isNewer(latestVersion, pkg.version); return AppUpdateInfo( latestVersion: latestVersion, buildNumber: buildNumber, forceUpdate: forceUpdate, releaseNotes: releaseNotes, downloadUrls: downloadUrls, hasUpdate: hasUpdate, ); } catch (_) { return null; } } /// 语义化版本比较:latest > current → true。容忍 1.1.4-dev / 1.1.4+7 等后缀。 bool _isNewer(String latest, String current) { final l = _parse(latest); final c = _parse(current); for (var i = 0; i < 3; i++) { if (l[i] > c[i]) return true; if (l[i] < c[i]) return false; } return false; } List _parse(String v) { final parts = v.split('.').map((s) { final m = RegExp(r'^\d+').firstMatch(s); return m == null ? 0 : int.parse(m.group(0)!); }).toList(); while (parts.length < 3) { parts.add(0); } return parts; } } final updateProvider = AsyncNotifierProvider(UpdateNotifier.new); /// 用户在更新 banner 点「稍后再说」时忽略的版本号。忽略后 banner 隐藏;出现号 /// 不同的更新版本会重新显示;设置页手动「检查更新」会清空以重新提示。响应式, /// 供 shell 顶部 banner 的显隐判断。 final dismissedUpdateVersionProvider = StateProvider((ref) => null); /// 按当前平台从服务端 download_urls 里取对应下载直链。 String? platformDownloadUrl(Map downloadUrls) { if (Platform.isMacOS) return downloadUrls['macos']; if (Platform.isWindows) return downloadUrls['windows']; if (Platform.isIOS) return downloadUrls['ios']; if (Platform.isAndroid) return downloadUrls['android']; return downloadUrls['web']; }