add
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
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('点击重试')),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user