rwadurian/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart

483 lines
15 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:video_player/video_player.dart';
import '../../../../routes/route_paths.dart';
import '../../../../routes/app_router.dart';
import '../../../../bootstrap.dart';
import '../providers/auth_provider.dart';
/// 开屏页面 - 应用启动时显示的第一个页面
/// 播放开屏视频,视频结束后检查用户认证状态并跳转
class SplashPage extends ConsumerStatefulWidget {
const SplashPage({super.key});
@override
ConsumerState<SplashPage> createState() => _SplashPageState();
}
class _SplashPageState extends ConsumerState<SplashPage>
with SingleTickerProviderStateMixin {
/// 视频播放控制器
VideoPlayerController? _videoController;
/// 视频是否已初始化
bool _isVideoInitialized = false;
/// 是否显示跳过按钮
bool _showSkipButton = false;
/// 是否已经开始跳转(防止重复跳转)
bool _isNavigating = false;
/// 视频加载是否失败(使用 fallback 动画)
bool _videoFailed = false;
/// Fallback 动画控制器
AnimationController? _fallbackAnimationController;
/// Logo 缩放动画
Animation<double>? _logoScaleAnimation;
/// Logo 透明度动画
Animation<double>? _logoOpacityAnimation;
/// 文字透明度动画
Animation<double>? _textOpacityAnimation;
@override
void initState() {
super.initState();
_initializeVideo();
}
@override
void dispose() {
_videoController?.removeListener(_onVideoStateChanged);
_videoController?.dispose();
_fallbackAnimationController?.dispose();
super.dispose();
}
/// 初始化视频播放器
Future<void> _initializeVideo() async {
debugPrint('[SplashPage] ========== 开始初始化视频 ==========');
debugPrint('[SplashPage] 视频路径: assets/videos/splash.mp4');
try {
// 从 assets 加载视频
debugPrint('[SplashPage] 创建 VideoPlayerController...');
_videoController = VideoPlayerController.asset('assets/videos/splash.mp4');
debugPrint('[SplashPage] 开始 initialize()...');
final stopwatch = Stopwatch()..start();
await _videoController!.initialize();
stopwatch.stop();
debugPrint('[SplashPage] initialize() 完成,耗时: ${stopwatch.elapsedMilliseconds}ms');
// 打印视频信息
final value = _videoController!.value;
debugPrint('[SplashPage] 视频信息:');
debugPrint('[SplashPage] - 尺寸: ${value.size.width} x ${value.size.height}');
debugPrint('[SplashPage] - 时长: ${value.duration.inMilliseconds}ms');
debugPrint('[SplashPage] - 是否已初始化: ${value.isInitialized}');
debugPrint('[SplashPage] - 是否有错误: ${value.hasError}');
if (value.hasError) {
debugPrint('[SplashPage] - 错误信息: ${value.errorDescription}');
}
if (mounted) {
setState(() {
_isVideoInitialized = true;
});
// 设置视频播放完成监听
_videoController!.addListener(_onVideoStateChanged);
// 开始播放视频
debugPrint('[SplashPage] 开始播放视频...');
await _videoController!.play();
debugPrint('[SplashPage] play() 调用完成isPlaying: ${_videoController!.value.isPlaying}');
// 1秒后显示跳过按钮
Future.delayed(const Duration(seconds: 1), () {
if (mounted) {
setState(() {
_showSkipButton = true;
});
}
});
}
} catch (e, stackTrace) {
debugPrint('[SplashPage] ========== 视频初始化失败 ==========');
debugPrint('[SplashPage] 错误类型: ${e.runtimeType}');
debugPrint('[SplashPage] 错误信息: $e');
debugPrint('[SplashPage] 堆栈跟踪:\n$stackTrace');
// 检查 controller 状态
if (_videoController != null) {
final value = _videoController!.value;
debugPrint('[SplashPage] Controller 状态:');
debugPrint('[SplashPage] - 是否已初始化: ${value.isInitialized}');
debugPrint('[SplashPage] - 是否有错误: ${value.hasError}');
if (value.hasError) {
debugPrint('[SplashPage] - 错误描述: ${value.errorDescription}');
}
}
// 如果视频加载失败,启动 fallback 动画
_startFallbackAnimation();
}
}
/// 启动 fallback 动画(视频加载失败时使用)
void _startFallbackAnimation() {
debugPrint('[SplashPage] 启动 fallback 动画');
setState(() {
_videoFailed = true;
});
// 创建动画控制器,总时长 3 秒
_fallbackAnimationController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 3000),
);
// Logo 缩放动画0.5 -> 1.0 -> 1.05 -> 1.0(弹性效果)
_logoScaleAnimation = TweenSequence<double>([
TweenSequenceItem(
tween: Tween<double>(begin: 0.5, end: 1.05)
.chain(CurveTween(curve: Curves.easeOutBack)),
weight: 40,
),
TweenSequenceItem(
tween: Tween<double>(begin: 1.05, end: 1.0)
.chain(CurveTween(curve: Curves.easeInOut)),
weight: 10,
),
TweenSequenceItem(
tween: ConstantTween<double>(1.0),
weight: 50,
),
]).animate(_fallbackAnimationController!);
// Logo 透明度动画0 -> 1
_logoOpacityAnimation = TweenSequence<double>([
TweenSequenceItem(
tween: Tween<double>(begin: 0.0, end: 1.0)
.chain(CurveTween(curve: Curves.easeOut)),
weight: 30,
),
TweenSequenceItem(
tween: ConstantTween<double>(1.0),
weight: 70,
),
]).animate(_fallbackAnimationController!);
// 文字透明度动画:延迟出现
_textOpacityAnimation = TweenSequence<double>([
TweenSequenceItem(
tween: ConstantTween<double>(0.0),
weight: 30,
),
TweenSequenceItem(
tween: Tween<double>(begin: 0.0, end: 1.0)
.chain(CurveTween(curve: Curves.easeOut)),
weight: 20,
),
TweenSequenceItem(
tween: ConstantTween<double>(1.0),
weight: 50,
),
]).animate(_fallbackAnimationController!);
// 动画完成后跳转
_fallbackAnimationController!.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_navigateToNextPage();
}
});
// 开始动画
_fallbackAnimationController!.forward();
// 1秒后显示跳过按钮
Future.delayed(const Duration(seconds: 1), () {
if (mounted) {
setState(() {
_showSkipButton = true;
});
}
});
}
/// 跳过 fallback 动画
void _skipFallbackAnimation() {
_fallbackAnimationController?.stop();
_navigateToNextPage();
}
/// 视频状态变化监听
void _onVideoStateChanged() {
if (_videoController == null) return;
// 视频播放完成
if (_videoController!.value.position >= _videoController!.value.duration &&
_videoController!.value.duration > Duration.zero) {
_navigateToNextPage();
}
}
/// 跳过视频
void _skipVideo() {
_videoController?.pause();
_navigateToNextPage();
}
/// 导航到下一个页面
Future<void> _navigateToNextPage() async {
// 防止重复跳转
if (_isNavigating) return;
_isNavigating = true;
// 初始化遥测服务(需要 BuildContext
await initializeTelemetry(context);
// 检查认证状态
await ref.read(authProvider.notifier).checkAuthStatus();
if (!mounted) return;
final authState = ref.read(authProvider);
// 根据认证状态决定跳转目标
// 优先级:
// 1. 钱包已创建且已备份 → 主页面
// 2. 账号已创建但钱包未完成 → 备份助记词页面
// 3. 首次打开或未看过向导 → 向导页
// 4. 其他情况 → 创建账户页面
if (authState.isWalletCreated) {
// 已创建钱包且已备份,进入主页面(龙虎榜)
debugPrint('[SplashPage] 钱包已创建且已备份 → 跳转到龙虎榜');
context.go(RoutePaths.ranking);
} else if (authState.isAccountCreated && authState.userSerialNum != null) {
// 账号已创建但钱包未完成(可能正在生成或未备份),直接进入备份助记词页面
debugPrint('[SplashPage] 账号已创建但钱包未完成 → 跳转到备份助记词页面');
debugPrint('[SplashPage] userSerialNum: ${authState.userSerialNum}, isWalletReady: ${authState.isWalletReady}');
context.go(
RoutePaths.backupMnemonic,
extra: BackupMnemonicParams(
userSerialNum: authState.userSerialNum!,
referralCode: authState.referralCode,
),
);
} else if (authState.isFirstLaunch || !authState.hasSeenGuide) {
// 首次打开或未看过向导,进入向导页
debugPrint('[SplashPage] 首次打开或未看过向导 → 跳转到向导页');
context.go(RoutePaths.guide);
} else {
// 已看过向导但未创建钱包,直接进入创建账户页面
debugPrint('[SplashPage] 已看过向导但未创建钱包 → 跳转到创建账户页面');
context.go(RoutePaths.onboarding);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
fit: StackFit.expand,
children: [
// 视频播放器(全屏覆盖)
if (_isVideoInitialized && _videoController != null)
SizedBox.expand(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: _videoController!.value.size.width,
height: _videoController!.value.size.height,
child: VideoPlayer(_videoController!),
),
),
)
else if (_videoFailed)
// 视频加载失败,显示 fallback 动画
_buildFallbackAnimationView()
else
// 视频加载中显示 Logo
_buildLoadingView(),
// 跳过按钮
if (_showSkipButton)
Positioned(
top: MediaQuery.of(context).padding.top + 16,
right: 16,
child: _buildSkipButton(),
),
],
),
);
}
/// 构建加载中视图(视频未加载完成时显示)
Widget _buildLoadingView() {
return Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFFFF5E6),
Color(0xFFFFE4B5),
],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo
Image.asset(
'assets/images/logo/app_icon.png',
width: 128,
height: 128,
),
const SizedBox(height: 24),
const Text(
'榴莲皇后',
style: TextStyle(
fontSize: 32,
fontFamily: 'Noto Sans SC',
fontWeight: FontWeight.w700,
height: 1.25,
letterSpacing: 1.6,
color: Color(0xFF4A3F35),
),
),
const SizedBox(height: 24),
// 加载指示器
const SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
),
),
],
),
);
}
/// 构建 fallback 动画视图(视频加载失败时使用)
Widget _buildFallbackAnimationView() {
return AnimatedBuilder(
animation: _fallbackAnimationController!,
builder: (context, child) {
return Container(
width: double.infinity,
height: double.infinity,
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFFFF5E6),
Color(0xFFFFE4B5),
],
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Logo 带缩放和透明度动画
Opacity(
opacity: _logoOpacityAnimation?.value ?? 1.0,
child: Transform.scale(
scale: _logoScaleAnimation?.value ?? 1.0,
child: Image.asset(
'assets/images/logo/app_icon.png',
width: 128,
height: 128,
),
),
),
const SizedBox(height: 24),
// 文字带透明度动画
Opacity(
opacity: _textOpacityAnimation?.value ?? 1.0,
child: const Text(
'榴莲皇后',
style: TextStyle(
fontSize: 32,
fontFamily: 'Noto Sans SC',
fontWeight: FontWeight.w700,
height: 1.25,
letterSpacing: 1.6,
color: Color(0xFF4A3F35),
),
),
),
const SizedBox(height: 16),
// 副标题(延迟显示)
Opacity(
opacity: _textOpacityAnimation?.value ?? 1.0,
child: const Text(
'认种榴莲 · 共享收益',
style: TextStyle(
fontSize: 16,
fontFamily: 'Noto Sans SC',
fontWeight: FontWeight.w400,
color: Color(0xFF8B7355),
),
),
),
],
),
);
},
);
}
/// 构建跳过按钮
Widget _buildSkipButton() {
return GestureDetector(
onTap: _videoFailed ? _skipFallbackAnimation : _skipVideo,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.5),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: Colors.white.withValues(alpha: 0.3),
width: 1,
),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'跳过',
style: TextStyle(
fontSize: 14,
fontFamily: 'Inter',
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
SizedBox(width: 4),
Icon(
Icons.skip_next,
size: 18,
color: Colors.white,
),
],
),
),
);
}
}