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/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.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/route_paths.dart';
import '../../../../routes/app_router.dart'; import '../../../../routes/app_router.dart';
import '../../../../bootstrap.dart'; import '../../../../bootstrap.dart';
import '../providers/auth_provider.dart'; import '../providers/auth_provider.dart';
/// - /// -
/// Logo ///
class SplashPage extends ConsumerStatefulWidget { class SplashPage extends ConsumerStatefulWidget {
const SplashPage({super.key}); const SplashPage({super.key});
@ -17,20 +17,92 @@ class SplashPage extends ConsumerStatefulWidget {
} }
class _SplashPageState extends ConsumerState<SplashPage> { class _SplashPageState extends ConsumerState<SplashPage> {
///
VideoPlayerController? _videoController;
///
bool _isVideoInitialized = false;
///
bool _showSkipButton = false;
///
bool _isNavigating = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_initializeApp(); _initializeVideo();
} }
/// @override
Future<void> _initializeApp() async { 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 // BuildContext
await initializeTelemetry(context); await initializeTelemetry(context);
//
await Future.delayed(AppConstants.splashDuration);
// //
await ref.read(authProvider.notifier).checkAuthStatus(); await ref.read(authProvider.notifier).checkAuthStatus();
@ -68,89 +140,128 @@ class _SplashPageState extends ConsumerState<SplashPage> {
debugPrint('[SplashPage] 已看过向导但未创建钱包 → 跳转到创建账户页面'); debugPrint('[SplashPage] 已看过向导但未创建钱包 → 跳转到创建账户页面');
context.go(RoutePaths.onboarding); context.go(RoutePaths.onboarding);
} }
// HomeShellPage
// checkForAppUpdate context.go() context
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Container( backgroundColor: Colors.black,
width: double.infinity, body: Stack(
height: double.infinity, fit: StackFit.expand,
// - children: [
decoration: const BoxDecoration( //
gradient: LinearGradient( if (_isVideoInitialized && _videoController != null)
begin: Alignment.topCenter, SizedBox.expand(
end: Alignment.bottomCenter, child: FittedBox(
colors: [ fit: BoxFit.cover,
Color(0xFFFFF5E6), // child: SizedBox(
Color(0xFFFFE4B5), // width: _videoController!.value.size.width,
], height: _videoController!.value.size.height,
), child: VideoPlayer(_videoController!),
), ),
child: Column( ),
mainAxisAlignment: MainAxisAlignment.center, )
children: [ else
// Logo // Logo
_buildLogo(), _buildLoadingView(),
const SizedBox(height: 24),
// //
_buildAppTitle(), if (_showSkipButton)
], Positioned(
), top: MediaQuery.of(context).padding.top + 16,
right: 16,
child: _buildSkipButton(),
),
],
), ),
); );
} }
/// Logo ///
/// Widget _buildLoadingView() {
Widget _buildLogo() {
return Container( return Container(
width: 128, width: double.infinity,
height: 152, height: double.infinity,
decoration: BoxDecoration( decoration: const BoxDecoration(
color: const Color(0xFF2D2A26), // gradient: LinearGradient(
borderRadius: BorderRadius.circular(8), begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFFFFF5E6),
Color(0xFFFFE4B5),
],
),
), ),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
// // Logo
Container( Container(
width: 64, width: 128,
height: 48, height: 152,
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFD4A84B), // color: const Color(0xFF2D2A26),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const Icon( child: Column(
Icons.workspace_premium, mainAxisAlignment: MainAxisAlignment.center,
size: 32, children: [
color: Color(0xFF2D2A26), 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), const SizedBox(height: 24),
// DURIAN
const Text( const Text(
'DURIAN', '榴莲女皇',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 32,
fontWeight: FontWeight.w600, fontFamily: 'Noto Sans SC',
letterSpacing: 2, fontWeight: FontWeight.w700,
color: Color(0xFFD4A84B), height: 1.25,
letterSpacing: 1.6,
color: Color(0xFF4A3F35),
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 24),
// QUEEN //
const Text( const SizedBox(
'QUEEN', width: 24,
style: TextStyle( height: 24,
fontSize: 10, child: CircularProgressIndicator(
fontWeight: FontWeight.w400, strokeWidth: 2,
letterSpacing: 1.5, valueColor: AlwaysStoppedAnimation<Color>(Color(0xFFD4AF37)),
color: Color(0xFFD4A84B),
), ),
), ),
], ],
@ -158,17 +269,40 @@ class _SplashPageState extends ConsumerState<SplashPage> {
); );
} }
/// ///
Widget _buildAppTitle() { Widget _buildSkipButton() {
return const Text( return GestureDetector(
'榴莲女皇', onTap: _skipVideo,
style: TextStyle( child: Container(
fontSize: 32, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
fontFamily: 'Noto Sans SC', decoration: BoxDecoration(
fontWeight: FontWeight.w700, color: Colors.black.withValues(alpha: 0.5),
height: 1.25, borderRadius: BorderRadius.circular(20),
letterSpacing: 1.6, border: Border.all(
color: Color(0xFF4A3F35), // 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 shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
import url_launcher_macos import url_launcher_macos
import video_player_avfoundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin")) ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
@ -31,4 +32,5 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 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" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.7" version: "3.0.7"
csslib:
dependency: transitive
description:
name: csslib
sha256: "09bad715f418841f976c77db72d5398dc1253c21fb9c0c7f0b0b985860b2d58e"
url: "https://pub.dev"
source: hosted
version: "1.0.2"
cupertino_icons: cupertino_icons:
dependency: "direct main" dependency: "direct main"
description: description:
@ -621,6 +629,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
html:
dependency: transitive
description:
name: html
sha256: "6d1264f2dffa1b1101c25a91dff0dc2daee4c18e87cd8538729773c073dbf602"
url: "https://pub.dev"
source: hosted
version: "0.15.6"
http: http:
dependency: transitive dependency: transitive
description: description:
@ -1570,6 +1586,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.2.0" 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: vm_service:
dependency: transitive dependency: transitive
description: description:

View File

@ -44,6 +44,7 @@ dependencies:
mobile_scanner: ^5.1.1 mobile_scanner: ^5.1.1
flutter_screenutil: ^5.9.0 flutter_screenutil: ^5.9.0
city_pickers: ^1.3.0 city_pickers: ^1.3.0
video_player: ^2.8.6
# 工具 # 工具
intl: ^0.20.2 intl: ^0.20.2
@ -111,3 +112,4 @@ flutter:
- assets/icons/tokens/ - assets/icons/tokens/
- assets/icons/actions/ - assets/icons/actions/
- assets/lottie/ - assets/lottie/
- assets/videos/