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:
hailin 2025-12-11 04:23:04 -08:00
parent 3644c04521
commit 7bf88cd6a8
5 changed files with 272 additions and 78 deletions

View File

@ -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,
),
],
),
),
);
}

View File

@ -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"))
}

View File

@ -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:

View File

@ -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/