import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/shared/tools/sc_version_utils.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/app/routes/sc_routes.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/data_sources/sources/local/file_cache_manager.dart'; import 'package:yumi/modules/auth/login_route.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'last_weekly_cp_splash_cache.dart'; import 'weekly_star_splash_cache.dart'; enum _SplashPresentationVariant { weeklyStar, lastWeeklyCp, defaultSplash } /// CP 启动页手动微调区。 /// 如果后续还要继续对齐设计稿,优先只改这一组 Rect / Size, /// 不要去动 _buildCpRankCard 里的层级和结构。 class _LastWeeklyCpLayout { static const Rect rank1FrameRect = Rect.fromLTWH(40, 253, 297, 148); static const Rect rank2FrameRect = Rect.fromLTWH(0, 427, 196, 111); static const Rect rank3FrameRect = Rect.fromLTWH(179, 427, 196, 111); static final Rect rank1LeftAvatarRect = Rect.fromCenter( center: const Offset(151, 353), width: 58, height: 58, ); static final Rect rank1RightAvatarRect = Rect.fromCenter( center: const Offset(227, 352), width: 58, height: 58, ); static const Rect rank1LabelRect = Rect.fromLTWH(118, 383, 140, 22); static final Rect rank2LeftAvatarRect = Rect.fromCenter( center: const Offset(46, 508), width: 50, height: 50, ); static final Rect rank2RightAvatarRect = Rect.fromCenter( center: const Offset(123, 509), width: 50, height: 50, ); static const Rect rank2LabelRect = Rect.fromLTWH(10, 533, 145, 26); static final Rect rank3LeftAvatarRect = Rect.fromCenter( center: const Offset(252, 508), width: 50, height: 50, ); static final Rect rank3RightAvatarRect = Rect.fromCenter( center: const Offset(329, 509), width: 50, height: 50, ); static const Rect rank3LabelRect = Rect.fromLTWH(218, 533, 145, 26); } class SplashPage extends StatefulWidget { const SplashPage({super.key}); @override State createState() => _SplashPageState(); } class _SplashPageState extends State { static const Duration _minimumSplashDuration = Duration(seconds: 3); static const Size _weeklyStarCanvasSize = Size(375, 812); static const Size _lastWeeklyCpCanvasSize = Size(375, 812); Timer? _timer; Timer? _countdownTimer; late final List _weeklyStarEntries; late final List _lastWeeklyCpEntries; late final _SplashPresentationVariant _selectedVariant; int _remainingSeconds = _minimumSplashDuration.inSeconds; bool _isNavigating = false; @override void initState() { super.initState(); _weeklyStarEntries = WeeklyStarSplashCache.loadDisplayEntries(); _lastWeeklyCpEntries = LastWeeklyCPSplashCache.loadDisplayEntries(); _selectedVariant = _pickSplashVariant(); WidgetsBinding.instance.addPostFrameCallback((_) { unawaited(FileCacheManager.getInstance().getFilePath()); unawaited(WeeklyStarSplashCache.refreshCacheInBackground()); unawaited(LastWeeklyCPSplashCache.refreshCacheInBackground()); }); _startNavigationTimer(); } @override void dispose() { _cancelTimer(); super.dispose(); } /// 启动页最短展示时间 void _startNavigationTimer() { _remainingSeconds = _minimumSplashDuration.inSeconds; _timer = Timer(_minimumSplashDuration, _dismissSplash); _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { final nextSeconds = _minimumSplashDuration.inSeconds - timer.tick; if (nextSeconds <= 0) { timer.cancel(); return; } if (!mounted) { return; } setState(() { _remainingSeconds = nextSeconds; }); }); } @override Widget build(BuildContext context) { final splashContent = switch (_selectedVariant) { _SplashPresentationVariant.weeklyStar => _buildWeeklyStarSplash(), _SplashPresentationVariant.lastWeeklyCp => _buildLastWeeklyCpSplash(), _SplashPresentationVariant.defaultSplash => _buildDefaultSplash(), }; final showLaunchOverlay = _selectedVariant != _SplashPresentationVariant.defaultSplash; return Material( color: Colors.black, child: Stack( fit: StackFit.expand, children: [ splashContent, if (showLaunchOverlay) _buildSkipCountdownButton(context), ], ), ); } _SplashPresentationVariant _pickSplashVariant() { final variants = <_SplashPresentationVariant>[]; if (WeeklyStarSplashCache.shouldShow) { variants.add(_SplashPresentationVariant.weeklyStar); } if (LastWeeklyCPSplashCache.shouldShow) { variants.add(_SplashPresentationVariant.lastWeeklyCp); } if (variants.isEmpty) { return _SplashPresentationVariant.defaultSplash; } return variants[Random().nextInt(variants.length)]; } Widget _buildDefaultSplash() { return Stack( alignment: Alignment.center, children: [ Image.asset( SCGlobalConfig.businessLogicStrategy.getSplashPageBackgroundImage(), width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, fit: BoxFit.cover, ), Image.asset( SCGlobalConfig.businessLogicStrategy.getSplashPageIcon(), width: 107.w, height: 159.w, ), ], ); } Widget _buildSkipCountdownButton(BuildContext context) { final localizations = SCAppLocalizations.of(context); final label = localizations?.skipCountdown(_remainingSeconds.toString()) ?? 'Skip ${_remainingSeconds}s'; final borderRadius = BorderRadius.circular(999); return SafeArea( child: Align( alignment: AlignmentDirectional.topEnd, child: Padding( padding: EdgeInsetsDirectional.only(top: 12.h, end: 16.w), child: Material( color: Colors.transparent, child: Ink( decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0x33FFFFFF), Color(0x18FFFFFF)], begin: AlignmentDirectional.centerStart, end: AlignmentDirectional.centerEnd, ), borderRadius: borderRadius, border: Border.all(color: Colors.white.withValues(alpha: 0.18)), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.1), blurRadius: 10.w, offset: Offset(0, 3.w), ), ], ), child: InkWell( borderRadius: borderRadius, onTap: _dismissSplash, splashFactory: NoSplash.splashFactory, overlayColor: WidgetStateProperty.all(Colors.transparent), splashColor: Colors.transparent, highlightColor: Colors.transparent, hoverColor: Colors.transparent, child: Padding( padding: EdgeInsets.symmetric( horizontal: 12.w, vertical: 8.h, ), child: Text( label, maxLines: 1, softWrap: false, textAlign: TextAlign.center, style: TextStyle( color: Colors.white.withValues(alpha: 0.96), fontSize: 12.sp, height: 1, fontWeight: FontWeight.w700, ), ), ), ), ), ), ), ), ); } Widget _buildWeeklyStarSplash() { return SizedBox.expand( child: FittedBox( fit: BoxFit.cover, child: SizedBox( width: _weeklyStarCanvasSize.width, height: _weeklyStarCanvasSize.height, child: Stack( children: [ Positioned.fill( child: Image.asset( 'sc_images/splash/sc_weekly_star_bg.png', fit: BoxFit.cover, ), ), _buildRankCard( entry: _entryForRank(1), frameAsset: 'sc_images/splash/sc_icon_weekly_star_rank_1.png', frameRect: const Rect.fromLTWH(24, 110, 327, 248), avatarRect: Rect.fromCenter( center: Offset(188, 261), width: 74, height: 74, ), labelRect: const Rect.fromLTWH(154, 328, 67, 12), ), _buildRankCard( entry: _entryForRank(2), frameAsset: 'sc_images/splash/sc_icon_weekly_star_rank_2.png', frameRect: const Rect.fromLTWH(0, 376, 182, 115), avatarRect: Rect.fromCenter( center: Offset(77, 437), width: 62, height: 62, ), labelRect: const Rect.fromLTWH(40, 501, 74, 12), ), _buildRankCard( entry: _entryForRank(3), frameAsset: 'sc_images/splash/sc_icon_weekly_star_rank_3.png', frameRect: const Rect.fromLTWH(180, 376, 195, 117), avatarRect: Rect.fromCenter( center: Offset(299, 439), width: 66, height: 66, ), labelRect: const Rect.fromLTWH(260, 501, 78, 12), ), ], ), ), ), ); } Widget _buildLastWeeklyCpSplash() { return SizedBox.expand( child: FittedBox( fit: BoxFit.cover, child: SizedBox( width: _lastWeeklyCpCanvasSize.width, height: _lastWeeklyCpCanvasSize.height, child: Stack( children: [ Positioned.fill( child: Image.asset( 'sc_images/splash/sc_last_weekly_cp_bg.png', fit: BoxFit.cover, ), ), _buildCpRankCard( entry: _cpEntryForRank(1), frameAsset: 'sc_images/splash/sc_icon_last_weekly_cp_rank_1.png', frameRect: _LastWeeklyCpLayout.rank1FrameRect, leftAvatarRect: _LastWeeklyCpLayout.rank1LeftAvatarRect, rightAvatarRect: _LastWeeklyCpLayout.rank1RightAvatarRect, labelRect: _LastWeeklyCpLayout.rank1LabelRect, ), _buildCpRankCard( entry: _cpEntryForRank(2), frameAsset: 'sc_images/splash/sc_icon_last_weekly_cp_rank_2.png', frameRect: _LastWeeklyCpLayout.rank2FrameRect, leftAvatarRect: _LastWeeklyCpLayout.rank2LeftAvatarRect, rightAvatarRect: _LastWeeklyCpLayout.rank2RightAvatarRect, labelRect: _LastWeeklyCpLayout.rank2LabelRect, ), _buildCpRankCard( entry: _cpEntryForRank(3), frameAsset: 'sc_images/splash/sc_icon_last_weekly_cp_rank_3.png', frameRect: _LastWeeklyCpLayout.rank3FrameRect, leftAvatarRect: _LastWeeklyCpLayout.rank3LeftAvatarRect, rightAvatarRect: _LastWeeklyCpLayout.rank3RightAvatarRect, labelRect: _LastWeeklyCpLayout.rank3LabelRect, ), ], ), ), ), ); } WeeklyStarSplashEntry _entryForRank(int rank) { return _weeklyStarEntries.firstWhere( (entry) => entry.rank == rank, orElse: () => WeeklyStarSplashEntry( rank: rank, avatarUrl: '', nickname: 'Namename', ), ); } LastWeeklyCPSplashEntry _cpEntryForRank(int rank) { return _lastWeeklyCpEntries.firstWhere( (entry) => entry.rank == rank, orElse: () => LastWeeklyCPSplashEntry( rank: rank, leftAvatarUrl: '', rightAvatarUrl: '', leftNickname: 'Namename', rightNickname: 'Namename', ), ); } Widget _buildRankCard({ required WeeklyStarSplashEntry entry, required String frameAsset, required Rect frameRect, required Rect avatarRect, required Rect labelRect, }) { return Stack( children: [ Positioned.fromRect( rect: avatarRect, child: head(url: entry.avatarUrl, width: avatarRect.width), ), Positioned.fromRect( rect: frameRect, child: Image.asset(frameAsset, fit: BoxFit.fill), ), Positioned.fromRect( rect: labelRect, child: _SplashOutlinedText(text: entry.nickname), ), ], ); } Widget _buildCpRankCard({ required LastWeeklyCPSplashEntry entry, required String frameAsset, required Rect frameRect, required Rect leftAvatarRect, required Rect rightAvatarRect, required Rect labelRect, }) { return Stack( children: [ Positioned.fromRect( rect: frameRect, child: Image.asset(frameAsset, fit: BoxFit.fill), ), Positioned.fromRect( rect: leftAvatarRect, child: head(url: entry.leftAvatarUrl, width: leftAvatarRect.width), ), Positioned.fromRect( rect: rightAvatarRect, child: head(url: entry.rightAvatarUrl, width: rightAvatarRect.width), ), Positioned.fromRect( rect: labelRect, child: _LastWeeklyCpPairLabel( leftText: entry.leftNickname, rightText: entry.rightNickname, ), ), ], ); } void _dismissSplash() { if (_isNavigating) { return; } _isNavigating = true; _cancelTimer(); unawaited(_goMainPage()); } Future _goMainPage() async { try { await SCVersionUtils.checkReview(); if (!mounted) { return; } var user = AccountStorage().getCurrentUser(); var token = AccountStorage().getToken(); if (user != null && token.isNotEmpty) { SCNavigatorUtils.push(context, SCRoutes.home, replace: true); } else { SCNavigatorUtils.push(context, LoginRouter.login, replace: true); } } catch (e) { if (!mounted) { return; } SCNavigatorUtils.push(context, LoginRouter.login, replace: true); } } /// 取消倒计时的计时器。 void _cancelTimer() { // 计时器(`Timer`)组件的取消(`cancel`)方法,取消计时器。 _timer?.cancel(); _countdownTimer?.cancel(); } } class _SplashOutlinedText extends StatelessWidget { const _SplashOutlinedText({required this.text}); final String text; static const Color _fillColor = Color(0xFFA11726); static const Color _strokeColor = Color(0xFFFFEBBA); @override Widget build(BuildContext context) { final displayText = text.trim().isEmpty ? 'Namename' : text.trim(); return FittedBox( fit: BoxFit.scaleDown, child: Stack( alignment: Alignment.center, children: [ Text( displayText, maxLines: 1, style: TextStyle( fontSize: 12, height: 1, fontWeight: FontWeight.w500, foreground: Paint() ..style = PaintingStyle.stroke ..strokeWidth = 1 ..color = _strokeColor, ), ), Text( displayText, maxLines: 1, style: const TextStyle( fontSize: 12, height: 1, fontWeight: FontWeight.w500, color: _fillColor, ), ), ], ), ); } } class _LastWeeklyCpPairLabel extends StatelessWidget { const _LastWeeklyCpPairLabel({ required this.leftText, required this.rightText, }); final String leftText; final String rightText; @override Widget build(BuildContext context) { return Row( children: [ Expanded( child: Align( alignment: Alignment.centerRight, child: _SplashOutlinedText(text: leftText), ), ), const SizedBox(width: 8), Expanded( child: Align( alignment: Alignment.centerLeft, child: _SplashOutlinedText(text: rightText), ), ), ], ); } }