210 lines
7.1 KiB
Dart
210 lines
7.1 KiB
Dart
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<SplashScreen> createState() => _SplashScreenState();
|
||
}
|
||
|
||
class _SplashScreenState extends ConsumerState<SplashScreen> {
|
||
String _step = '正在初始化…';
|
||
String? _notice;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_bootstrap();
|
||
}
|
||
|
||
Future<void> _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<void> _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<void> _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<void> _showForceUpdateDialog(VersionInfo info) {
|
||
return showDialog<void>(
|
||
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<void> _showUpdatePrompt(VersionInfo info) async {
|
||
final update = await showDialog<bool>(
|
||
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('点击重试')),
|
||
],
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|