import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:uuid/uuid.dart'; import '../../core/config/app_config.dart'; import '../../core/network/api_client.dart'; import '../../core/storage/local_store.dart'; import '../../core/theme/app_theme.dart'; import '../../providers/auth_provider.dart'; import '../../services/invite_service.dart'; import '../../services/version_service.dart'; /// 启动页:依次执行 ①设备指纹 ②域名发现+ApiClient 初始化 ③登录态恢复 ④版本/公告检查 class SplashScreen extends ConsumerStatefulWidget { const SplashScreen({super.key}); @override ConsumerState createState() => _SplashScreenState(); } class _SplashScreenState extends ConsumerState { String _step = '正在初始化…'; String? _notice; @override void initState() { super.initState(); _bootstrap(); } Future _bootstrap() async { try { // ① 设备指纹(UUID v4,持久化) _setStep('初始化设备…'); LocalStore.I.ensureDeviceId(() => const Uuid().v4()); // ② 域名发现(TXT → 兜底)+ 网络客户端初始化 _setStep('连接服务器…'); await ApiClient.init(); // ②.5 推广归因:剪贴板口令(主)→ 无口令则首启模糊匹配上报(辅) try { await _attributeInvite(); } catch (_) { // 归因失败不阻塞启动 } // ③ 登录态恢复(安全存储 Token → /users/me) _setStep('恢复登录态…'); await ref.read(authProvider.notifier).restore(); // ④ 版本检查与维护公告(失败不阻塞进入首页) try { final info = await VersionService.check(); if (!mounted) return; // 强制更新(当前 version_code 低于服务端最低要求):阻断使用 if (info.forceUpdate) { await _showForceUpdateDialog(info); return; } // 非强制新版本:可跳过的提示弹窗 if (info.latestVersionCode > AppConfig.appVersionCode) { await _showUpdatePrompt(info); if (!mounted) return; } setState(() => _notice = info.notice.isNotEmpty ? info.notice : null); if (_notice == null) { _goHome(); return; } // 有公告:展示 2 秒后进入 Future.delayed(const Duration(seconds: 2), _goHome); } catch (_) { _goHome(); } } catch (e) { _setStep('网络异常,请检查后重试'); // 域名发现彻底失败:提供重试入口 } } /// 推广归因: /// - 剪贴板命中分享口令 → 存 pending 邀请码(注册页预填,准确率 100%) /// - 无口令且未上报过 → 模糊匹配上报(每安装仅一次,命中结果服务端缓存) Future _attributeInvite() async { final clip = await Clipboard.getData(Clipboard.kTextPlain); final code = InviteService.parseInviteCommand(clip?.text); if (code != null) { await LocalStore.I.savePendingInviteCode(code); return; } if (LocalStore.I.inviteMatchReported) return; final matched = await InviteService.reportMatch(LocalStore.I.deviceId); await LocalStore.I.markInviteMatchReported(); if (matched != null && matched.isNotEmpty) { await LocalStore.I.savePendingInviteCode(matched); } } void _setStep(String s) { if (mounted) setState(() => _step = s); } void _goHome() { if (!mounted) return; // 游客可直接浏览(计费接口按 device_id 走),登录页由路由守卫按需唤起 context.go('/home'); } /// 跳转下载页(商店/外部浏览器) Future _openUpdateUrl(String url) async { if (url.isEmpty) return; final uri = Uri.tryParse(url); if (uri == null) return; try { await launchUrl(uri, mode: LaunchMode.externalApplication); } catch (_) { // 调起失败不阻塞(强制更新场景弹窗仍在,可重试) } } /// 强制更新弹窗:不可关闭、不可返回,仅可跳下载页(《01-开发文档》5.7) Future _showForceUpdateDialog(VersionInfo info) { return showDialog( context: context, barrierDismissible: false, builder: (ctx) => PopScope( canPop: false, child: AlertDialog( backgroundColor: AppColors.surface, title: const Text('版本过低', style: TextStyle(fontSize: 16)), content: Text( '当前版本已停止服务,请更新到 v${info.latestVersion} 后继续使用。', ), actions: [ ElevatedButton( onPressed: () => _openUpdateUrl(info.updateUrl), child: const Text('立即更新'), ), ], ), ), ); } /// 非强制更新弹窗:可跳过;点更新跳下载页后继续进入 App Future _showUpdatePrompt(VersionInfo info) async { final update = await showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: AppColors.surface, title: Text('发现新版本 v${info.latestVersion}', style: const TextStyle(fontSize: 16)), content: const Text('建议更新到最新版本以获得更好的体验。'), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('跳过'), ), ElevatedButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('立即更新'), ), ], ), ); if (update == true) await _openUpdateUrl(info.updateUrl); } @override Widget build(BuildContext context) { return Scaffold( body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( width: 88, height: 88, decoration: BoxDecoration( gradient: const LinearGradient(colors: [AppColors.primary, AppColors.secondary]), borderRadius: BorderRadius.circular(20), ), child: const Icon(Icons.play_arrow_rounded, size: 56, color: Colors.white), ), const SizedBox(height: 24), const Text('短视频', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700)), const SizedBox(height: 40), if (_notice != null) Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text(_notice!, textAlign: TextAlign.center, style: const TextStyle(color: AppColors.gold)), ) else Text(_step, style: const TextStyle(color: AppColors.textSecondary)), if (_step.contains('异常')) ...[ const SizedBox(height: 16), TextButton(onPressed: _bootstrap, child: const Text('点击重试')), ], ], ), ), ); } }