feat(mobile): replace splash screen with video player
- Add video_player dependency for video playback - Create assets/videos directory for splash video - Implement video splash screen with: - Full-screen video playback (splash.mp4) - Skip button appears after 1 second - Fallback to logo view if video fails to load - Auto-navigate when video completes - Video file should be placed at: assets/videos/splash.mp4 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
3644c04521
commit
7bf88cd6a8
|
|
@ -1,14 +1,14 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/constants/app_constants.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';
|
||||
|
||||
/// 开屏页面 - 应用启动时显示的第一个页面
|
||||
/// 显示 Logo 和应用名称,同时检查用户认证状态
|
||||
/// 播放开屏视频,视频结束后检查用户认证状态并跳转
|
||||
class SplashPage extends ConsumerStatefulWidget {
|
||||
const SplashPage({super.key});
|
||||
|
||||
|
|
@ -17,20 +17,92 @@ class SplashPage extends ConsumerStatefulWidget {
|
|||
}
|
||||
|
||||
class _SplashPageState extends ConsumerState<SplashPage> {
|
||||
/// 视频播放控制器
|
||||
VideoPlayerController? _videoController;
|
||||
|
||||
/// 视频是否已初始化
|
||||
bool _isVideoInitialized = false;
|
||||
|
||||
/// 是否显示跳过按钮
|
||||
bool _showSkipButton = false;
|
||||
|
||||
/// 是否已经开始跳转(防止重复跳转)
|
||||
bool _isNavigating = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeApp();
|
||||
_initializeVideo();
|
||||
}
|
||||
|
||||
/// 初始化应用并检查认证状态
|
||||
Future<void> _initializeApp() async {
|
||||
@override
|
||||
void dispose() {
|
||||
_videoController?.removeListener(_onVideoStateChanged);
|
||||
_videoController?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 初始化视频播放器
|
||||
Future<void> _initializeVideo() async {
|
||||
try {
|
||||
// 从 assets 加载视频
|
||||
_videoController = VideoPlayerController.asset('assets/videos/splash.mp4');
|
||||
|
||||
await _videoController!.initialize();
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isVideoInitialized = true;
|
||||
});
|
||||
|
||||
// 设置视频播放完成监听
|
||||
_videoController!.addListener(_onVideoStateChanged);
|
||||
|
||||
// 开始播放视频
|
||||
await _videoController!.play();
|
||||
|
||||
// 1秒后显示跳过按钮
|
||||
Future.delayed(const Duration(seconds: 1), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_showSkipButton = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[SplashPage] 视频初始化失败: $e');
|
||||
// 如果视频加载失败,直接进行跳转
|
||||
_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 Future.delayed(AppConstants.splashDuration);
|
||||
|
||||
// 检查认证状态
|
||||
await ref.read(authProvider.notifier).checkAuthStatus();
|
||||
|
||||
|
|
@ -68,89 +140,128 @@ class _SplashPageState extends ConsumerState<SplashPage> {
|
|||
debugPrint('[SplashPage] 已看过向导但未创建钱包 → 跳转到创建账户页面');
|
||||
context.go(RoutePaths.onboarding);
|
||||
}
|
||||
|
||||
// 注意:更新检查已移至 HomeShellPage,在主页面加载后执行
|
||||
// 这里不再调用 checkForAppUpdate,因为 context.go() 后 context 已失效
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
body: 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 图片容器
|
||||
_buildLogo(),
|
||||
const SizedBox(height: 24),
|
||||
// 应用名称
|
||||
_buildAppTitle(),
|
||||
],
|
||||
),
|
||||
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
|
||||
// 视频加载中显示 Logo
|
||||
_buildLoadingView(),
|
||||
|
||||
// 跳过按钮
|
||||
if (_showSkipButton)
|
||||
Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 16,
|
||||
right: 16,
|
||||
child: _buildSkipButton(),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 构建 Logo 组件
|
||||
/// 深色背景上的皇冠图标
|
||||
Widget _buildLogo() {
|
||||
/// 构建加载中视图(视频未加载完成时显示)
|
||||
Widget _buildLoadingView() {
|
||||
return Container(
|
||||
width: 128,
|
||||
height: 152,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2D2A26), // 深棕色背景
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
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
|
||||
Container(
|
||||
width: 64,
|
||||
height: 48,
|
||||
width: 128,
|
||||
height: 152,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFD4A84B), // 金色
|
||||
color: const Color(0xFF2D2A26),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.workspace_premium,
|
||||
size: 32,
|
||||
color: Color(0xFF2D2A26),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
width: 64,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFD4A84B),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.workspace_premium,
|
||||
size: 32,
|
||||
color: Color(0xFF2D2A26),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'DURIAN',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 2,
|
||||
color: Color(0xFFD4A84B),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
const Text(
|
||||
'QUEEN',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 1.5,
|
||||
color: Color(0xFFD4A84B),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// DURIAN 文字
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
'DURIAN',
|
||||
'榴莲女皇',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
letterSpacing: 2,
|
||||
color: Color(0xFFD4A84B),
|
||||
fontSize: 32,
|
||||
fontFamily: 'Noto Sans SC',
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.25,
|
||||
letterSpacing: 1.6,
|
||||
color: Color(0xFF4A3F35),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// QUEEN 文字
|
||||
const Text(
|
||||
'QUEEN',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w400,
|
||||
letterSpacing: 1.5,
|
||||
color: Color(0xFFD4A84B),
|
||||
const SizedBox(height: 24),
|
||||
// 加载指示器
|
||||
const SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
|
@ -158,17 +269,40 @@ class _SplashPageState extends ConsumerState<SplashPage> {
|
|||
);
|
||||
}
|
||||
|
||||
/// 构建应用标题
|
||||
Widget _buildAppTitle() {
|
||||
return const Text(
|
||||
'榴莲女皇',
|
||||
style: TextStyle(
|
||||
fontSize: 32,
|
||||
fontFamily: 'Noto Sans SC',
|
||||
fontWeight: FontWeight.w700,
|
||||
height: 1.25,
|
||||
letterSpacing: 1.6,
|
||||
color: Color(0xFF4A3F35), // 深棕色文字
|
||||
/// 构建跳过按钮
|
||||
Widget _buildSkipButton() {
|
||||
return GestureDetector(
|
||||
onTap: _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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import share_plus
|
|||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
import video_player_avfoundation
|
||||
|
||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
|
||||
|
|
@ -31,4 +32,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -249,6 +249,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
csslib:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: csslib
|
||||
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.2"
|
||||
cupertino_icons:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -621,6 +629,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: html
|
||||
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.15.6"
|
||||
http:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1570,6 +1586,46 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
video_player:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: video_player
|
||||
sha256: "096bc28ce10d131be80dfb00c223024eb0fba301315a406728ab43dd99c45bdf"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.1"
|
||||
video_player_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_android
|
||||
sha256: d74b66f283afff135d5be0ceccca2ca74dff7df1e9b1eaca6bd4699875d3ae60
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.22"
|
||||
video_player_avfoundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_avfoundation
|
||||
sha256: e4d33b79a064498c6eb3a6a492b6a5012573d4943c28d566caf1a6c0840fe78d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.8"
|
||||
video_player_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_platform_interface
|
||||
sha256: "57c5d73173f76d801129d0531c2774052c5a7c11ccb962f1830630decd9f24ec"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.0"
|
||||
video_player_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: video_player_web
|
||||
sha256: "9f3c00be2ef9b76a95d94ac5119fb843dca6f2c69e6c9968f6f2b6c9e7afbdeb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.0"
|
||||
vm_service:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ dependencies:
|
|||
mobile_scanner: ^5.1.1
|
||||
flutter_screenutil: ^5.9.0
|
||||
city_pickers: ^1.3.0
|
||||
video_player: ^2.8.6
|
||||
|
||||
# 工具
|
||||
intl: ^0.20.2
|
||||
|
|
@ -111,3 +112,4 @@ flutter:
|
|||
- assets/icons/tokens/
|
||||
- assets/icons/actions/
|
||||
- assets/lottie/
|
||||
- assets/videos/
|
||||
|
|
|
|||
Loading…
Reference in New Issue