From 7bf88cd6a84589b345f234da41b59570073b6255 Mon Sep 17 00:00:00 2001 From: hailin Date: Thu, 11 Dec 2025 04:23:04 -0800 Subject: [PATCH] feat(mobile): replace splash screen with video player MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- frontend/mobile-app/assets/videos/.gitkeep | 0 .../auth/presentation/pages/splash_page.dart | 290 +++++++++++++----- .../Flutter/GeneratedPluginRegistrant.swift | 2 + frontend/mobile-app/pubspec.lock | 56 ++++ frontend/mobile-app/pubspec.yaml | 2 + 5 files changed, 272 insertions(+), 78 deletions(-) create mode 100644 frontend/mobile-app/assets/videos/.gitkeep diff --git a/frontend/mobile-app/assets/videos/.gitkeep b/frontend/mobile-app/assets/videos/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart b/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart index ae406378..d2b906f6 100644 --- a/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart +++ b/frontend/mobile-app/lib/features/auth/presentation/pages/splash_page.dart @@ -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 { + /// 视频播放控制器 + VideoPlayerController? _videoController; + + /// 视频是否已初始化 + bool _isVideoInitialized = false; + + /// 是否显示跳过按钮 + bool _showSkipButton = false; + + /// 是否已经开始跳转(防止重复跳转) + bool _isNavigating = false; + @override void initState() { super.initState(); - _initializeApp(); + _initializeVideo(); } - /// 初始化应用并检查认证状态 - Future _initializeApp() async { + @override + void dispose() { + _videoController?.removeListener(_onVideoStateChanged); + _videoController?.dispose(); + super.dispose(); + } + + /// 初始化视频播放器 + Future _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 _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 { 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(0xFFD4AF37)), ), ), ], @@ -158,17 +269,40 @@ class _SplashPageState extends ConsumerState { ); } - /// 构建应用标题 - 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, + ), + ], + ), ), ); } diff --git a/frontend/mobile-app/macos/Flutter/GeneratedPluginRegistrant.swift b/frontend/mobile-app/macos/Flutter/GeneratedPluginRegistrant.swift index 87cdbc2b..3b887e63 100644 --- a/frontend/mobile-app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/frontend/mobile-app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -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")) } diff --git a/frontend/mobile-app/pubspec.lock b/frontend/mobile-app/pubspec.lock index a2173ef5..d1c2f51e 100644 --- a/frontend/mobile-app/pubspec.lock +++ b/frontend/mobile-app/pubspec.lock @@ -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: diff --git a/frontend/mobile-app/pubspec.yaml b/frontend/mobile-app/pubspec.yaml index 5ad14c34..01e7fd5f 100644 --- a/frontend/mobile-app/pubspec.yaml +++ b/frontend/mobile-app/pubspec.yaml @@ -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/