Files
pangolin/client/lib/state/update_provider.dart
T
wangjia 0b86138b55
Deploy Client / build-windows (push) Successful in 1m47s
Deploy Client / build-android (push) Has been cancelled
Deploy Client / build-macos (push) Has been cancelled
Deploy Client / build-ios (push) Has been cancelled
Deploy Client / release-deploy (push) Has been cancelled
feat(client/update): 更新提示改 jiu 式顶部横条(非强制不再弹窗)
按需求去掉居中弹窗,改成不打断的顶部 banner(对照 jiu 黄条):
- 非强制更新 → home_shell 顶部 UpdateBanner「发现新版本 vX + 立即更新 + ×」;
  「立即更新」App 内下载安装,「×」忽略此版本(banner 隐藏,更新版本会重现)
- 强制更新(force_update)→ 仍走不可关闭弹窗 showUpdateDialog
- 忽略状态改 dismissedUpdateVersionProvider(按版本号,响应式);去掉 notifier
  的 _dismissed
- 设置页「检查更新」有更新时:清忽略版本→banner 显示 + toast,不再弹窗
- 新增 6 语「立即更新」文案。flutter analyze 仅 2 个既有 withOpacity info

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
2026-07-07 19:19:05 +08:00

140 lines
4.8 KiB
Dart

// 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<String, String> downloadUrls;
final bool hasUpdate;
}
/// 更新检查 Notifier。build() 惰性触发:延迟首查 + 每小时轮询。
class UpdateNotifier extends AsyncNotifier<AppUpdateInfo?> {
Timer? _timer;
@override
Future<AppUpdateInfo?> build() async {
ref.onDispose(() => _timer?.cancel());
await Future<void>.delayed(_kInitialDelay);
final first = await _check();
_timer = Timer.periodic(_kPollInterval, (_) async {
state = AsyncValue.data(await _check());
});
return first;
}
/// 设置页「检查更新」手动触发:立即查一次并回结果。
Future<AppUpdateInfo?> forceCheck() async {
final info = await _check();
state = AsyncValue.data(info);
return info;
}
/// 拉取 `$kApiBaseUrl/version` 并与本地版本比较。网络/解析失败返回 null(静默)。
Future<AppUpdateInfo?> _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<String, dynamic>;
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<String, dynamic>? ?? 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<int> _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, AppUpdateInfo?>(UpdateNotifier.new);
/// 用户在更新 banner 点「稍后再说」时忽略的版本号。忽略后 banner 隐藏;出现号
/// 不同的更新版本会重新显示;设置页手动「检查更新」会清空以重新提示。响应式,
/// 供 shell 顶部 banner 的显隐判断。
final dismissedUpdateVersionProvider = StateProvider<String?>((ref) => null);
/// 按当前平台从服务端 download_urls 里取对应下载直链。
String? platformDownloadUrl(Map<String, String> 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'];
}