diff --git a/assets/l10n/intl_ar.json b/assets/l10n/intl_ar.json index a240659..7ea7a87 100644 --- a/assets/l10n/intl_ar.json +++ b/assets/l10n/intl_ar.json @@ -82,9 +82,10 @@ "kingQuuen": "الملك والملكة", "ramadan": "رمضان", "like": "إعجاب", - "updateNow": "تحديث الآن", - "skip2": "تخطي", - "importantReminder": "تذكير مهم", + "updateNow": "تحديث الآن", + "skip2": "تخطي", + "skipCountdown": "تخطي {1}ث", + "importantReminder": "تذكير مهم", "discard": "تجاهل", "deleteCommentTips": "هل أنت متأكد أنك تريد حذف هذا التعليق؟", "deleteSuccessful": "تم الحذف بنجاح!", diff --git a/assets/l10n/intl_bn.json b/assets/l10n/intl_bn.json index 5360f8d..6c2a75d 100644 --- a/assets/l10n/intl_bn.json +++ b/assets/l10n/intl_bn.json @@ -96,6 +96,7 @@ "ownerIncomeCoins": "মালিকের আয়:{1} কয়েন", "game": "গেম", "skip2": "স্কিপ করুন", + "skipCountdown": "{1}সে স্কিপ", "coins4": "কয়েন", "claim": "গ্রহণ", "complete": "সম্পূর্ণ", diff --git a/assets/l10n/intl_en.json b/assets/l10n/intl_en.json index 9b59dc8..c7ddff8 100644 --- a/assets/l10n/intl_en.json +++ b/assets/l10n/intl_en.json @@ -97,6 +97,7 @@ "ownerIncomeCoins": "Owner's income: {1} coins", "game": "Game", "skip2": "Skip", + "skipCountdown": "Skip {1}s", "coins4": "Coins", "weekStart": "Week-Start", "forMoreRewardsPleaseCheckTheTaskCenter": "For more rewards, please check the task center", diff --git a/assets/l10n/intl_tr.json b/assets/l10n/intl_tr.json index 71bfccd..0ba1a08 100644 --- a/assets/l10n/intl_tr.json +++ b/assets/l10n/intl_tr.json @@ -85,9 +85,10 @@ "applicationRecord": "Başvuru Kaydı", "appUpdateTip": "Uygulamanın yeni bir sürümü ({1}) var, şimdi indirmek ister misiniz?", "ownerIncomeCoins": "Sahibin Geliri:{1} jetton", - "game": "Oyun", - "skip2": "Atla", - "coins4": "Jetton", + "game": "Oyun", + "skip2": "Atla", + "skipCountdown": "{1} sn geç", + "coins4": "Jetton", "weekStart": "Hafta Başlangıcı", "forMoreRewardsPleaseCheckTheTaskCenter": "Daha fazla ödül için lütfen görev merkezini kontrol edin", "kingQuuen": "Kral-Kraliçe", diff --git a/lib/app_localizations.dart b/lib/app_localizations.dart index 1747257..f038439 100644 --- a/lib/app_localizations.dart +++ b/lib/app_localizations.dart @@ -13,7 +13,7 @@ class SCAppLocalizations { } static const LocalizationsDelegate delegate = - _SCAppLocalizationsDelegate(); + _SCAppLocalizationsDelegate(); Map? _localizedStrings; @@ -68,7 +68,8 @@ class SCAppLocalizations { String get expirationTime => translate('expirationTime'); - String get inviteNewUsersToEarnCoins => translate('inviteNewUsersToEarnCoins'); + String get inviteNewUsersToEarnCoins => + translate('inviteNewUsersToEarnCoins'); String get avoidBeingKicked => translate('avoidBeingKicked'); @@ -90,7 +91,8 @@ class SCAppLocalizations { String get numberOfMic => translate('numberOfMic'); - String get noHistoricalRecordsAvailable => translate('noHistoricalRecordsAvailable'); + String get noHistoricalRecordsAvailable => + translate('noHistoricalRecordsAvailable'); String get myRoom => translate('myRoom'); @@ -106,7 +108,8 @@ class SCAppLocalizations { String get crateMyRoom => translate('crateMyRoom'); - String get exclusiveEmojiWillBeReleasedAfterBecoming => translate('exclusiveEmojiWillBeReleasedAfterBecoming'); + String get exclusiveEmojiWillBeReleasedAfterBecoming => + translate('exclusiveEmojiWillBeReleasedAfterBecoming'); String get socialPrivilege => translate('socialPrivilege'); @@ -126,7 +129,8 @@ class SCAppLocalizations { String get vipBirthdayGift => translate('vipBirthdayGift'); - String get pleaseUpgradeYourVipLevel => translate('pleaseUpgradeYourVipLevel'); + String get pleaseUpgradeYourVipLevel => + translate('pleaseUpgradeYourVipLevel'); String get membershipFreeChatSpeak => translate('membershipFreeChatSpeak'); @@ -148,25 +152,34 @@ class SCAppLocalizations { String get enterTheRoom => translate('enterTheRoom'); - String get taskNamePersonalGameConsume => translate('taskNamePersonalGameConsume'); + String get taskNamePersonalGameConsume => + translate('taskNamePersonalGameConsume'); - String get taskNamePersonalMicInRoom => translate('taskNamePersonalMicInRoom'); + String get taskNamePersonalMicInRoom => + translate('taskNamePersonalMicInRoom'); - String get taskNamePersonalActiveInRoom => translate('taskNamePersonalActiveInRoom'); + String get taskNamePersonalActiveInRoom => + translate('taskNamePersonalActiveInRoom'); String get taskNameRoomOwnerMicTime => translate('taskNameRoomOwnerMicTime'); - String get taskNamePersonalLuckyGiftGold => translate('taskNamePersonalLuckyGiftGold'); + String get taskNamePersonalLuckyGiftGold => + translate('taskNamePersonalLuckyGiftGold'); - String get taskNamePersonalMagicGiftGold => translate('taskNamePersonalMagicGiftGold'); + String get taskNamePersonalMagicGiftGold => + translate('taskNamePersonalMagicGiftGold'); - String get taskNameRoomOwnerSendGiftGold => translate('taskNameRoomOwnerSendGiftGold'); + String get taskNameRoomOwnerSendGiftGold => + translate('taskNameRoomOwnerSendGiftGold'); - String get taskNameRoomUserSendGiftGold => translate('taskNameRoomUserSendGiftGold'); + String get taskNameRoomUserSendGiftGold => + translate('taskNameRoomUserSendGiftGold'); - String get taskNameRoomOwnerInviteMic => translate('taskNameRoomOwnerInviteMic'); + String get taskNameRoomOwnerInviteMic => + translate('taskNameRoomOwnerInviteMic'); - String get taskNameRoomOnlineUserCount => translate('taskNameRoomOnlineUserCount'); + String get taskNameRoomOnlineUserCount => + translate('taskNameRoomOnlineUserCount'); String get taskNamePersonalSendGift => translate('taskNamePersonalSendGift'); @@ -174,11 +187,14 @@ class SCAppLocalizations { String get taskNameRoomMicUser60Min => translate('taskNameRoomMicUser60Min'); - String get taskNameRoomMicUser120Min => translate('taskNameRoomMicUser120Min'); + String get taskNameRoomMicUser120Min => + translate('taskNameRoomMicUser120Min'); - String get taskNameRoomOwnerSendGiftUser => translate('taskNameRoomOwnerSendGiftUser'); + String get taskNameRoomOwnerSendGiftUser => + translate('taskNameRoomOwnerSendGiftUser'); - String get taskNameRoomOwnerSendRedPacket => translate('taskNameRoomOwnerSendRedPacket'); + String get taskNameRoomOwnerSendRedPacket => + translate('taskNameRoomOwnerSendRedPacket'); String get taskNameRoomNewMember => translate('taskNameRoomNewMember'); @@ -202,7 +218,8 @@ class SCAppLocalizations { String get dailyCoinBonanzaRules => translate('dailyCoinBonanzaRules'); - String get dailyCoinBonanzaRulesDetail => translate('dailyCoinBonanzaRulesDetail'); + String get dailyCoinBonanzaRulesDetail => + translate('dailyCoinBonanzaRulesDetail'); String get personalTasks => translate('personalTasks'); @@ -218,7 +235,8 @@ class SCAppLocalizations { String get operationFail => translate('operationFail'); - String get forMoreRewardsPleaseCheckTheTaskCenter => translate('forMoreRewardsPleaseCheckTheTaskCenter'); + String get forMoreRewardsPleaseCheckTheTaskCenter => + translate('forMoreRewardsPleaseCheckTheTaskCenter'); String get likedYourComment => translate('likedYourComment'); @@ -250,7 +268,8 @@ class SCAppLocalizations { String get basicFeatures => translate('basicFeatures'); - String get areYouSureYouWantToDeleteYourAccount => translate('areYouSureYouWantToDeleteYourAccount'); + String get areYouSureYouWantToDeleteYourAccount => + translate('areYouSureYouWantToDeleteYourAccount'); String get game => translate('game'); @@ -258,7 +277,8 @@ class SCAppLocalizations { String get deleteAccountTips => translate('deleteAccountTips'); - String get thisUserHasBeenBlacklisted => translate('thisUserHasBeenBlacklisted'); + String get thisUserHasBeenBlacklisted => + translate('thisUserHasBeenBlacklisted'); String get comment => translate('comment'); @@ -266,7 +286,8 @@ class SCAppLocalizations { String get customizedGiftRules => translate('customizedGiftRules'); - String get customizedGiftRulesContent => translate('customizedGiftRulesContent'); + String get customizedGiftRulesContent => + translate('customizedGiftRulesContent'); String get clearCache => translate('clearCache'); @@ -286,9 +307,11 @@ class SCAppLocalizations { String get enterThisVoiceChatRoom => translate('enterThisVoiceChatRoom'); - String get swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt => translate('swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt'); + String get swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt => + translate('swipeLeftOnTheFloatingScreenAreaToQuicklyCloseIt'); - String get areYouSureYouWantToClearLocalCache => translate('areYouSureYouWantToClearLocalCache'); + String get areYouSureYouWantToClearLocalCache => + translate('areYouSureYouWantToClearLocalCache'); String get multiple => translate('multiple'); @@ -300,13 +323,17 @@ class SCAppLocalizations { String get clearCacheSuccessfully => translate('clearCacheSuccessfully'); - String get successfullyAddedToTheBlacklist => translate('successfullyAddedToTheBlacklist'); + String get successfullyAddedToTheBlacklist => + translate('successfullyAddedToTheBlacklist'); - String get successfullyRemovedFromTheBlacklist => translate('successfullyRemovedFromTheBlacklist'); + String get successfullyRemovedFromTheBlacklist => + translate('successfullyRemovedFromTheBlacklist'); - String get successfullyRemovedFromTheDynamicBlacklist => translate('successfullyRemovedFromTheDynamicBlacklist'); + String get successfullyRemovedFromTheDynamicBlacklist => + translate('successfullyRemovedFromTheDynamicBlacklist'); - String get successfullyAddedToTheDynamicBlacklist => translate('successfullyAddedToTheDynamicBlacklist'); + String get successfullyAddedToTheDynamicBlacklist => + translate('successfullyAddedToTheDynamicBlacklist'); String get areYouSureToCancelBlacklist => translate('areYouSureToCancelBlacklist'); @@ -327,7 +354,8 @@ class SCAppLocalizations { String get userBlacklist => translate('userBlacklist'); - String get areYouSureToCancelDynamicBlacklist => translate('areYouSureToCancelDynamicBlacklist'); + String get areYouSureToCancelDynamicBlacklist => + translate('areYouSureToCancelDynamicBlacklist'); String get thisFeatureIsCurrentlyUnavailable => translate('thisFeatureIsCurrentlyUnavailable'); @@ -995,7 +1023,6 @@ class SCAppLocalizations { String get charmGameRulesTips => translate('charmGameRulesTips'); - String get enterYourNewPassword => translate('enterYourNewPassword'); String get confirmSwitchMicThemeTips => @@ -1063,13 +1090,11 @@ class SCAppLocalizations { String vipEmoticon(String name) => translate('vipEmoticon').replaceAll('{1}', name); - String family3(String name) => - translate('family3').replaceAll('{1}', name); + String family3(String name) => translate('family3').replaceAll('{1}', name); - String onlineUsers(String name1, String name2) => - translate( - 'onlineUsers', - ).replaceAll('{1}', name1).replaceAll('{2}', name2); + String onlineUsers(String name1, String name2) => translate( + 'onlineUsers', + ).replaceAll('{1}', name1).replaceAll('{2}', name2); String timeSpentTogether(String name) => translate('timeSpentTogether').replaceAll('{1}', name); @@ -1086,10 +1111,9 @@ class SCAppLocalizations { String win2(String name) => translate('win2').replaceAll('{1}', name); - String numberOfMyCPs(String name1, String name2) => - translate( - 'numberOfMyCPs', - ).replaceAll('{1}', name1).replaceAll('{2}', name2); + String numberOfMyCPs(String name1, String name2) => translate( + 'numberOfMyCPs', + ).replaceAll('{1}', name1).replaceAll('{2}', name2); String sendUserId(String name) => translate('sendUserId').replaceAll('{1}', name); @@ -1466,6 +1490,9 @@ class SCAppLocalizations { String get skip2 => translate('skip2'); + String skipCountdown(String seconds) => + translate('skipCountdown').replaceAll('{1}', seconds); + String get updateNow => translate('updateNow'); String get clearMessage => translate('clearMessage'); @@ -1505,13 +1532,14 @@ class SCAppLocalizations { String xxfamily(String name) => translate('xxfamily').replaceAll('{1}', name); - String collectionTimeTips(String time, - String remainCount, - String totalCount,) => - translate('collectionTimeTips') - .replaceAll('{1}', time) - .replaceAll("{2}", remainCount) - .replaceAll("{3}", totalCount); + String collectionTimeTips( + String time, + String remainCount, + String totalCount, + ) => translate('collectionTimeTips') + .replaceAll('{1}', time) + .replaceAll("{2}", remainCount) + .replaceAll("{3}", totalCount); String yourVipWillExpire(String name) => translate('yourVipWillExpire').replaceAll('{1}', name); @@ -1522,10 +1550,9 @@ class SCAppLocalizations { String privileges(String name) => translate('privileges').replaceAll('{1}', name); - String remainingNumberTips(String name1, String name2) => - translate( - 'remainingNumberTips', - ).replaceAll('{1}', name1).replaceAll("{2}", name2); + String remainingNumberTips(String name1, String name2) => translate( + 'remainingNumberTips', + ).replaceAll('{1}', name1).replaceAll("{2}", name2); String coins2(String name) => translate('coins2').replaceAll('{1}', name); @@ -1548,18 +1575,16 @@ class SCAppLocalizations { String skip(String name) => translate('skip').replaceAll('{1}', name); - String familyMember2(String name1, String name2) => - translate( - 'familyMember2', - ).replaceAll('{1}', name1).replaceAll('{2}', name2); + String familyMember2(String name1, String name2) => translate( + 'familyMember2', + ).replaceAll('{1}', name1).replaceAll('{2}', name2); String familyMember3(String name1) => translate('familyMember3').replaceAll('{1}', name1); - String familyAdmin2(String name1, String name2) => - translate( - 'familyAdmin2', - ).replaceAll('{1}', name1).replaceAll('{2}', name2); + String familyAdmin2(String name1, String name2) => translate( + 'familyAdmin2', + ).replaceAll('{1}', name1).replaceAll('{2}', name2); String follow2(String name) => translate('follow2').replaceAll('{1}', name); @@ -1586,7 +1611,7 @@ class _SCAppLocalizationsDelegate @override bool isSupported(Locale locale) => - ['en', 'zh', 'ar','bn','tr'].contains(locale.languageCode); + ['en', 'zh', 'ar', 'bn', 'tr'].contains(locale.languageCode); @override Future load(Locale locale) async { diff --git a/lib/modules/splash/last_weekly_cp_splash_cache.dart b/lib/modules/splash/last_weekly_cp_splash_cache.dart new file mode 100644 index 0000000..4c7e1a1 --- /dev/null +++ b/lib/modules/splash/last_weekly_cp_splash_cache.dart @@ -0,0 +1,105 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; + +class LastWeeklyCPSplashEntry { + const LastWeeklyCPSplashEntry({ + required this.rank, + required this.leftAvatarUrl, + required this.rightAvatarUrl, + required this.leftNickname, + required this.rightNickname, + }); + + final int rank; + final String leftAvatarUrl; + final String rightAvatarUrl; + final String leftNickname; + final String rightNickname; + + factory LastWeeklyCPSplashEntry.fromJson(Map json) { + return LastWeeklyCPSplashEntry( + rank: (json['rank'] as num?)?.toInt() ?? 0, + leftAvatarUrl: (json['leftAvatarUrl'] as String?) ?? '', + rightAvatarUrl: (json['rightAvatarUrl'] as String?) ?? '', + leftNickname: (json['leftNickname'] as String?) ?? '', + rightNickname: (json['rightNickname'] as String?) ?? '', + ); + } + + Map toJson() { + return { + 'rank': rank, + 'leftAvatarUrl': leftAvatarUrl, + 'rightAvatarUrl': rightAvatarUrl, + 'leftNickname': leftNickname, + 'rightNickname': rightNickname, + }; + } +} + +class LastWeeklyCPSplashCache { + static const String _cacheKey = 'last_weekly_cp_splash_cache_v1'; + + /// 搜索 `api-ready-launch-splash` 可以快速找到后续要恢复接入的位置。 + /// + /// 当前 CP 榜接口还没 ready,因此先彻底关闭这套自定义启动页。 + /// 正式恢复时保持“本地有缓存才展示、无缓存不展示”的逻辑,不要再写占位缓存。 + static const bool _allowCacheDrivenSplash = false; + + static bool get shouldShow { + if (!_allowCacheDrivenSplash) { + return false; + } + return loadCachedEntries().isNotEmpty; + } + + static List loadDisplayEntries() { + if (!_allowCacheDrivenSplash) { + return const []; + } + return loadCachedEntries(); + } + + static List loadCachedEntries() { + final raw = DataPersistence.getString(_cacheKey); + if (raw.isEmpty) { + return const []; + } + + try { + final decoded = jsonDecode(raw); + if (decoded is! List) { + return const []; + } + return decoded + .whereType() + .map( + (item) => LastWeeklyCPSplashEntry.fromJson( + Map.from(item), + ), + ) + .toList() + ..sort((a, b) => a.rank.compareTo(b.rank)); + } catch (error, stackTrace) { + debugPrint('LastWeeklyCPSplashCache parse failed: $error\n$stackTrace'); + return const []; + } + } + + static Future refreshCacheInBackground() async { + // TODO(api-ready-launch-splash): CP 榜接口 ready 后,把 `_allowCacheDrivenSplash` + // 改为 true,并在这里补上“接口取数 -> 映射 LastWeeklyCPSplashEntry -> + // 写入 `_cacheKey`”的流程。保留“有缓存才展示、无缓存不展示”的正式逻辑。 + if (!_allowCacheDrivenSplash) { + return; + } + + try { + // 当前占位逻辑已下线,等待正式 CP 榜接口接入。 + } catch (error, stackTrace) { + debugPrint('LastWeeklyCPSplashCache refresh failed: $error\n$stackTrace'); + } + } +} diff --git a/lib/modules/splash/splash_page.dart b/lib/modules/splash/splash_page.dart index 4e94464..ee553e4 100644 --- a/lib/modules/splash/splash_page.dart +++ b/lib/modules/splash/splash_page.dart @@ -1,13 +1,67 @@ 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}); @@ -17,15 +71,28 @@ class SplashPage extends StatefulWidget { } class _SplashPageState extends State { - static const Duration _minimumSplashDuration = Duration(milliseconds: 900); + 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(); } @@ -38,36 +105,341 @@ class _SplashPageState extends State { /// 启动页最短展示时间 void _startNavigationTimer() { - _timer = Timer(_minimumSplashDuration, () { - if (mounted) { - _goMainPage(); + _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( - alignment: Alignment.center, + fit: StackFit.expand, 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, - ), + splashContent, + if (showLaunchOverlay) _buildSkipCountdownButton(context), ], ), ); } - void _goMainPage() async { + _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) { @@ -92,5 +464,83 @@ class _SplashPageState extends State { 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), + ), + ), + ], + ); } } diff --git a/lib/modules/splash/weekly_star_splash_cache.dart b/lib/modules/splash/weekly_star_splash_cache.dart new file mode 100644 index 0000000..46999b6 --- /dev/null +++ b/lib/modules/splash/weekly_star_splash_cache.dart @@ -0,0 +1,148 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_top_four_with_reward_res.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_config_repository_imp.dart'; + +class WeeklyStarSplashEntry { + const WeeklyStarSplashEntry({ + required this.rank, + required this.avatarUrl, + required this.nickname, + }); + + final int rank; + final String avatarUrl; + final String nickname; + + factory WeeklyStarSplashEntry.fromJson(Map json) { + return WeeklyStarSplashEntry( + rank: (json['rank'] as num?)?.toInt() ?? 0, + avatarUrl: (json['avatarUrl'] as String?) ?? '', + nickname: (json['nickname'] as String?) ?? '', + ); + } + + Map toJson() { + return {'rank': rank, 'avatarUrl': avatarUrl, 'nickname': nickname}; + } +} + +class WeeklyStarSplashCache { + static const String _cacheKey = 'weekly_star_splash_cache_v1'; + + /// 搜索 `api-ready-launch-splash` 可以快速找到后续要恢复接入的位置。 + /// + /// 当前周榜接口链路还未 ready,因此先关闭这套自定义启动页。 + /// 正式恢复时保持“本地有缓存才展示、无缓存不展示”的逻辑,不要再回退到预览数据。 + static const bool _allowCacheDrivenSplash = false; + + static bool get shouldShow { + if (!_allowCacheDrivenSplash) { + return false; + } + return loadCachedEntries().isNotEmpty; + } + + static List loadDisplayEntries() { + if (!_allowCacheDrivenSplash) { + return const []; + } + return loadCachedEntries(); + } + + static List loadCachedEntries() { + final raw = DataPersistence.getString(_cacheKey); + if (raw.isEmpty) { + return const []; + } + + try { + final decoded = jsonDecode(raw); + if (decoded is! List) { + return const []; + } + return decoded + .whereType() + .map( + (item) => + WeeklyStarSplashEntry.fromJson(Map.from(item)), + ) + .toList() + ..sort((a, b) => a.rank.compareTo(b.rank)); + } catch (error, stackTrace) { + debugPrint('WeeklyStarSplashCache parse failed: $error\n$stackTrace'); + return const []; + } + } + + static Future refreshCacheInBackground() async { + // TODO(api-ready-launch-splash): 周榜接口 ready 后,把 `_allowCacheDrivenSplash` + // 改为 true,并继续只在这里写入真实接口结果到 `_cacheKey`。 + // 这样就能保持“有缓存才展示、无缓存不展示”的正式逻辑。 + if (!_allowCacheDrivenSplash) { + return; + } + + try { + final result = await SCConfigRepositoryImp().topFourWithReward(); + final entries = _mapTopThree(result); + if (entries.isEmpty) { + return; + } + await DataPersistence.setString( + _cacheKey, + jsonEncode(entries.map((entry) => entry.toJson()).toList()), + ); + } catch (error, stackTrace) { + debugPrint('WeeklyStarSplashCache refresh failed: $error\n$stackTrace'); + } + } + + static List _mapTopThree( + List result, + ) { + final topThree = + result + .where( + (item) => + (item.avatar ?? '').trim().isNotEmpty || + (item.nickname ?? '').trim().isNotEmpty, + ) + .map( + (item) => WeeklyStarSplashEntry( + rank: (item.rank ?? 0).toInt(), + avatarUrl: (item.avatar ?? '').trim(), + nickname: (item.nickname ?? '').trim(), + ), + ) + .toList() + ..sort((a, b) => a.rank.compareTo(b.rank)); + + return _ensureThreeEntries(topThree.take(3).toList()); + } + + static List _ensureThreeEntries( + List entries, + ) { + final normalized = List.from(entries) + ..sort((a, b) => a.rank.compareTo(b.rank)); + + for (var rank = 1; rank <= 3; rank++) { + final hasRank = normalized.any((entry) => entry.rank == rank); + if (!hasRank) { + normalized.add( + WeeklyStarSplashEntry( + rank: rank, + avatarUrl: '', + nickname: 'Namename', + ), + ); + } + } + + normalized.sort((a, b) => a.rank.compareTo(b.rank)); + return normalized.take(3).toList(); + } +} diff --git a/sc_images/splash/sc_icon_last_weekly_cp_rank_1.png b/sc_images/splash/sc_icon_last_weekly_cp_rank_1.png new file mode 100644 index 0000000..d52e420 Binary files /dev/null and b/sc_images/splash/sc_icon_last_weekly_cp_rank_1.png differ diff --git a/sc_images/splash/sc_icon_last_weekly_cp_rank_2.png b/sc_images/splash/sc_icon_last_weekly_cp_rank_2.png new file mode 100644 index 0000000..0d62caf Binary files /dev/null and b/sc_images/splash/sc_icon_last_weekly_cp_rank_2.png differ diff --git a/sc_images/splash/sc_icon_last_weekly_cp_rank_3.png b/sc_images/splash/sc_icon_last_weekly_cp_rank_3.png new file mode 100644 index 0000000..45de390 Binary files /dev/null and b/sc_images/splash/sc_icon_last_weekly_cp_rank_3.png differ diff --git a/sc_images/splash/sc_icon_weekly_star_rank_1.png b/sc_images/splash/sc_icon_weekly_star_rank_1.png new file mode 100644 index 0000000..34177cb Binary files /dev/null and b/sc_images/splash/sc_icon_weekly_star_rank_1.png differ diff --git a/sc_images/splash/sc_icon_weekly_star_rank_2.png b/sc_images/splash/sc_icon_weekly_star_rank_2.png new file mode 100644 index 0000000..9ca830c Binary files /dev/null and b/sc_images/splash/sc_icon_weekly_star_rank_2.png differ diff --git a/sc_images/splash/sc_icon_weekly_star_rank_3.png b/sc_images/splash/sc_icon_weekly_star_rank_3.png new file mode 100644 index 0000000..3fcb704 Binary files /dev/null and b/sc_images/splash/sc_icon_weekly_star_rank_3.png differ diff --git a/sc_images/splash/sc_last_weekly_cp_bg.png b/sc_images/splash/sc_last_weekly_cp_bg.png new file mode 100644 index 0000000..79df897 Binary files /dev/null and b/sc_images/splash/sc_last_weekly_cp_bg.png differ diff --git a/sc_images/splash/sc_weekly_star_bg.png b/sc_images/splash/sc_weekly_star_bg.png new file mode 100644 index 0000000..68432fd Binary files /dev/null and b/sc_images/splash/sc_weekly_star_bg.png differ diff --git a/需求进度.md b/需求进度.md index 26390e1..8709186 100644 --- a/需求进度.md +++ b/需求进度.md @@ -4,7 +4,14 @@ - 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。 ## 本轮启动优化(非网络) -- 已移除启动页固定 `6` 秒倒计时,改为最短约 `900ms` 展示后继续路由判断,避免人为等待直接拉长入 app 时间。 +- 已补回启动页正式展示逻辑:当前 `Weekly Star / Last Weekly CP` 两套自定义启动页都改为“本地有缓存才展示、无缓存不展示”;由于现阶段周榜与 CP 榜接口链路都还未 ready,缓存刷新逻辑已先关闭,所以当前启动阶段会直接回退到默认 splash,不再展示这两套定制视觉稿。相关恢复入口已在缓存类里用 `api-ready-launch-splash` 注释标记,后续接口 ready 后可直接搜索接回。 +- 已将启动页展示时间收敛为 `3` 秒,并在右上角新增通用 `skip 倒计时` 按钮:当前按钮会按秒级动态展示剩余时间,点击可立即跳过;文案已补齐 `en/ar/tr/bn` 多语言翻译,并按 locale 输出倒计时文本,便于后续继续做 RTL 语言验收。 +- 已按 2026-04-20 最新截图反馈重新校准 `Last Weekly CP` 启动页的头像与昵称区域,并在 `SplashPage` 中单独抽出 `_LastWeeklyCpLayout` 手动微调区;后续如果还要继续挪位置,直接改榜一/榜二/榜三的 `frame/avatar/label Rect` 即可,不用再翻布局层级。 +- 已按 2026-04-20 最新确认把 `Last Weekly CP` 启动页临时切到“通用头像占位”模式:由于当前 CP 榜正式接口还未就绪,启动页不再读取本地 `cpList` 或当前用户头像,现统一使用默认头像占位与通用昵称,专门用于先校对双人头像位和昵称区域的 UI 位置;后续接口 ready 后再切回真实数据源。 +- 已继续扩展启动页方案:桌面 `启动页_cp榜` 的背景图与 CP 榜一/榜二/榜三素材已导入工程,并新增 `Last Weekly CP` 启动页;当前 `SplashPage` 会在 `Weekly Star` 与 `Last Weekly CP` 两套视觉稿之间随机选一张展示,便于一起校对两套 UI 样式。 +- 已为 `Last Weekly CP` 启动页补齐本地缓存骨架:新增独立的 CP 榜缓存结构,当前会先把默认占位头像与通用昵称写入本地缓存,后续若改成正式的“有相关缓存才启用”或切到独立榜单来源,只需要替换数据入口,不用重做随机展示和布局层。 +- 已按 2026-04-20 新需求在 Flutter 启动页接入 `Weekly Star` 周榜视觉稿:桌面 `启动页_周榜` 的背景图与榜一/榜二/榜三翅膀框素材已导入工程,并替换了原先仅展示静态 logo 的 `SplashPage`,当前会按参考图在启动阶段叠加 3 个头像位和用户名描边文案,便于先校对 UI 样式。 +- 已为 `Weekly Star` 启动页补齐本地缓存读写骨架:新增独立缓存辅助类,支持把 `/ranking/top-four-with-reward` 返回的榜单前三头像/昵称写入本地,并在启动时优先读取缓存数据渲染;后续只需要把“当前固定启用”切回“缓存存在时才启用”即可,不用重做布局层。 - 已将 Firebase / Crashlytics 从 `runApp` 前阻塞初始化改为首帧后后台预热,并补充“未就绪时仅本地打印”的异常兜底,减少首屏前平台初始化阻塞。 - 已将 `SocialChatAuthenticationManager`、`SocialChatUserProfileManager` 改为按需创建,避免 splash 阶段就提前触发 Google Sign-In / Firebase 会话检查。 - 已将 deep link、设备信息、缓存目录创建等非首帧必需动作延后到首帧后执行,降低首屏竞争。 @@ -162,6 +169,9 @@ - `lib/modules/user/edit/edit_user_info_page2.dart` - `lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart` - `lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart` +- `lib/modules/splash/splash_page.dart` +- `lib/modules/splash/last_weekly_cp_splash_cache.dart` +- `lib/modules/splash/weekly_star_splash_cache.dart` - `lib/shared/tools/sc_room_profile_cache.dart` - `lib/shared/business_logic/models/res/room_res.dart` - `lib/shared/business_logic/models/res/follow_room_res.dart` @@ -193,6 +203,14 @@ - `lib/modules/user/my_items/theme/bags_theme_page.dart` - `lib/ui_kit/widgets/store/store_bag_page_helpers.dart` - `sc_images/general/sc_no_data.png` +- `sc_images/splash/sc_weekly_star_bg.png` +- `sc_images/splash/sc_icon_weekly_star_rank_1.png` +- `sc_images/splash/sc_icon_weekly_star_rank_2.png` +- `sc_images/splash/sc_icon_weekly_star_rank_3.png` +- `sc_images/splash/sc_last_weekly_cp_bg.png` +- `sc_images/splash/sc_icon_last_weekly_cp_rank_1.png` +- `sc_images/splash/sc_icon_last_weekly_cp_rank_2.png` +- `sc_images/splash/sc_icon_last_weekly_cp_rank_3.png` - `.gitignore` - `android/key.properties` - `pubspec.yaml`