Merge branch 'feature' into test

This commit is contained in:
zhx 2026-05-18 11:29:29 +08:00
commit d5653afa4f
46 changed files with 2692 additions and 722 deletions

View File

@ -280,8 +280,8 @@
"bDLeaderInviteYouToBecomeBDLeader": "ندعوك لتصبح قائد تطوير الأعمال", "bDLeaderInviteYouToBecomeBDLeader": "ندعوك لتصبح قائد تطوير الأعمال",
"win2": "فوز {1}", "win2": "فوز {1}",
"level": "مستوى", "level": "مستوى",
"wealthLevel": "مستوى الثروة", "wealthLevel": "مستوى الثراء",
"userLevel": "مستوى المستخدم", "userLevel": "مستوى الجاذبية",
"themeGoToUploadTips": "1. ستتم مراجعة التحميل خلال 24 ساعة بعد نجاحه.\n2. ستُعاد جميع العملات إذا فشلت المراجعة.", "themeGoToUploadTips": "1. ستتم مراجعة التحميل خلال 24 ساعة بعد نجاحه.\n2. ستُعاد جميع العملات إذا فشلت المراجعة.",
"pleaseUploadAccordingToExample": "يرجى الرفع وفقًا للمثال المعروض.", "pleaseUploadAccordingToExample": "يرجى الرفع وفقًا للمثال المعروض.",
"goToUpload": "الانتقال للرفع", "goToUpload": "الانتقال للرفع",
@ -666,6 +666,8 @@
"buy": "شراء", "buy": "شراء",
"visitorList": "قائمة الزوار", "visitorList": "قائمة الزوار",
"spendCoinsToGainExperiencePoints": "اصرف العملات لكسب نقاط الخبرة", "spendCoinsToGainExperiencePoints": "اصرف العملات لكسب نقاط الخبرة",
"richLevelUpgradeAnswer": "دع ثروتك تغزو كل شيء",
"charmLevelUpgradeAnswer": "اجعل الآخرين يخضعون لسحرك",
"howToUpgrade": "كيف يمكن الترقية؟", "howToUpgrade": "كيف يمكن الترقية؟",
"higherLevelFancierAvatarFrame": "مستوى أعلى، شارات/إطار صور أفخم.", "higherLevelFancierAvatarFrame": "مستوى أعلى، شارات/إطار صور أفخم.",
"use": "استخدم", "use": "استخدم",

View File

@ -267,7 +267,7 @@
"aboutUs": "আমাদের সম্পর্কে", "aboutUs": "আমাদের সম্পর্কে",
"theme": "থিম", "theme": "থিম",
"wealthLevel": "সম্পদ স্তর", "wealthLevel": "সম্পদ স্তর",
"userLevel": "ব্যবহারকারী স্তর", "userLevel": "আকর্ষণ স্তর",
"goToUpload": "আপলোড করতে যান", "goToUpload": "আপলোড করতে যান",
"logout": "লগআউট", "logout": "লগআউট",
"luck": "ভাগ্য", "luck": "ভাগ্য",
@ -600,6 +600,8 @@
"toConsume": "খরচ করার জন্য", "toConsume": "খরচ করার জন্য",
"howToUpgrade": "কীভাবে উন্নত করব?", "howToUpgrade": "কীভাবে উন্নত করব?",
"spendCoinsToGainExperiencePoints": "অভিজ্ঞতা পয়েন্ট পাওয়ার জন্য কয়েন খরচ করুন", "spendCoinsToGainExperiencePoints": "অভিজ্ঞতা পয়েন্ট পাওয়ার জন্য কয়েন খরচ করুন",
"richLevelUpgradeAnswer": "তোমার সম্পদ সবকিছু জয় করুক",
"charmLevelUpgradeAnswer": "অন্যদের তোমার আকর্ষণের কাছে নত করো",
"higherLevelFancierAvatarFrame": "উচ্চতর স্তর, আরও সুন্দর ব্যাজ/প্রোফাইল ফ্রেম", "higherLevelFancierAvatarFrame": "উচ্চতর স্তর, আরও সুন্দর ব্যাজ/প্রোফাইল ফ্রেম",
"all": "সব", "all": "সব",
"gift": "গিফট", "gift": "গিফট",

View File

@ -236,8 +236,8 @@
"about": "About", "about": "About",
"aboutUs": "About Us", "aboutUs": "About Us",
"theme": "Theme", "theme": "Theme",
"wealthLevel": "Wealth Level", "wealthLevel": "Rich Level",
"userLevel": "User Level", "userLevel": "Charm Level",
"goToUpload": "Go to upload", "goToUpload": "Go to upload",
"logout": "Logout", "logout": "Logout",
"luck": "Luck", "luck": "Luck",
@ -569,6 +569,8 @@
"toConsume": "To consume", "toConsume": "To consume",
"howToUpgrade": "How to upgrade?", "howToUpgrade": "How to upgrade?",
"spendCoinsToGainExperiencePoints": "Spend Coins to gain experience points", "spendCoinsToGainExperiencePoints": "Spend Coins to gain experience points",
"richLevelUpgradeAnswer": "Let your wealth conquer everything",
"charmLevelUpgradeAnswer": "Make others submit to your charm",
"higherLevelFancierAvatarFrame": "Higher level, fancier badges/avatar frame", "higherLevelFancierAvatarFrame": "Higher level, fancier badges/avatar frame",
"all": "All", "all": "All",
"gift": "Gift", "gift": "Gift",
@ -724,7 +726,7 @@
"roomRocketNoteTitle": "Note", "roomRocketNoteTitle": "Note",
"roomRocketNoteSectionTitle": "Each time a VIP", "roomRocketNoteSectionTitle": "Each time a VIP",
"roomRocketNoteSectionBody": "1. Each time a VIP level is purchased, it is valid for 30 days, and can be manually renewed after expiration.", "roomRocketNoteSectionBody": "1. Each time a VIP level is purchased, it is valid for 30 days, and can be manually renewed after expiration.",
"roomRocketRewardEmptyText": "No rewards earned. Better luck next time.", "roomRocketRewardEmptyText": "No reward",
"roomRocketRecordEmptyText": "No reward records", "roomRocketRecordEmptyText": "No reward records",
"launchRocket": "Launch Rocket", "launchRocket": "Launch Rocket",
"roomRocketFuelFullNotice": "Rocket fuel is full! Launching in {1}s!" "roomRocketFuelFullNotice": "Rocket fuel is full! Launching in {1}s!"

View File

@ -237,7 +237,7 @@
"aboutUs": "À propos de nous", "aboutUs": "À propos de nous",
"theme": "Thème", "theme": "Thème",
"wealthLevel": "Niveau de richesse", "wealthLevel": "Niveau de richesse",
"userLevel": "Niveau utilisateur", "userLevel": "Niveau de charme",
"goToUpload": "Aller au téléchargement", "goToUpload": "Aller au téléchargement",
"logout": "Déconnexion", "logout": "Déconnexion",
"luck": "Chance", "luck": "Chance",
@ -569,6 +569,8 @@
"toConsume": "Consommer", "toConsume": "Consommer",
"howToUpgrade": "Comment mettre à niveau ?", "howToUpgrade": "Comment mettre à niveau ?",
"spendCoinsToGainExperiencePoints": "Dépensez des pièces pour gagner des points d'expérience", "spendCoinsToGainExperiencePoints": "Dépensez des pièces pour gagner des points d'expérience",
"richLevelUpgradeAnswer": "Que ta richesse conquière tout",
"charmLevelUpgradeAnswer": "Fais s'incliner les autres devant ton charme",
"higherLevelFancierAvatarFrame": "Cadre de badges/avatar de niveau supérieur et plus sophistiqué", "higherLevelFancierAvatarFrame": "Cadre de badges/avatar de niveau supérieur et plus sophistiqué",
"all": "Tout", "all": "Tout",
"gift": "Cadeau", "gift": "Cadeau",

View File

@ -237,7 +237,7 @@
"aboutUs": "Chi siamo", "aboutUs": "Chi siamo",
"theme": "Tema", "theme": "Tema",
"wealthLevel": "Livello di ricchezza", "wealthLevel": "Livello di ricchezza",
"userLevel": "Livello utente", "userLevel": "Livello di fascino",
"goToUpload": "Vai al caricamento", "goToUpload": "Vai al caricamento",
"logout": "Esci", "logout": "Esci",
"luck": "Fortuna", "luck": "Fortuna",
@ -569,6 +569,8 @@
"toConsume": "Consumare", "toConsume": "Consumare",
"howToUpgrade": "Come aggiornare?", "howToUpgrade": "Come aggiornare?",
"spendCoinsToGainExperiencePoints": "Spendi monete per guadagnare punti esperienza", "spendCoinsToGainExperiencePoints": "Spendi monete per guadagnare punti esperienza",
"richLevelUpgradeAnswer": "Lascia che la tua ricchezza conquisti tutto",
"charmLevelUpgradeAnswer": "Fa' che gli altri si arrendano al tuo fascino",
"higherLevelFancierAvatarFrame": "Distintivi/cornice avatar di livello superiore e più elaborati", "higherLevelFancierAvatarFrame": "Distintivi/cornice avatar di livello superiore e più elaborati",
"all": "Tutto", "all": "Tutto",
"gift": "Regalo", "gift": "Regalo",

View File

@ -237,7 +237,7 @@
"aboutUs": "Sobre nós", "aboutUs": "Sobre nós",
"theme": "Tema", "theme": "Tema",
"wealthLevel": "Nível de riqueza", "wealthLevel": "Nível de riqueza",
"userLevel": "Nível de usuário", "userLevel": "Nível de charme",
"goToUpload": "Ir para carregar", "goToUpload": "Ir para carregar",
"logout": "Sair", "logout": "Sair",
"luck": "Sorte", "luck": "Sorte",
@ -569,6 +569,8 @@
"toConsume": "Para consumir", "toConsume": "Para consumir",
"howToUpgrade": "Como atualizar?", "howToUpgrade": "Como atualizar?",
"spendCoinsToGainExperiencePoints": "Gaste moedas para ganhar pontos de experiência", "spendCoinsToGainExperiencePoints": "Gaste moedas para ganhar pontos de experiência",
"richLevelUpgradeAnswer": "Deixe sua riqueza conquistar tudo",
"charmLevelUpgradeAnswer": "Faça os outros se renderem ao seu charme",
"higherLevelFancierAvatarFrame": "Emblemas/moldura de avatar de nível superior e mais sofisticados", "higherLevelFancierAvatarFrame": "Emblemas/moldura de avatar de nível superior e mais sofisticados",
"all": "Todos", "all": "Todos",
"gift": "Presente", "gift": "Presente",

View File

@ -225,8 +225,8 @@
"about": "Hakkında", "about": "Hakkında",
"aboutUs": "Hakkımızda", "aboutUs": "Hakkımızda",
"theme": "Tema", "theme": "Tema",
"wealthLevel": "Servet Seviyesi", "wealthLevel": "Zenginlik Seviyesi",
"userLevel": "Kullanıcı Seviyesi", "userLevel": "Çekicilik Seviyesi",
"goToUpload": "Yüklemeye Git", "goToUpload": "Yüklemeye Git",
"logout": ıkış Yap", "logout": ıkış Yap",
"luck": "Şans", "luck": "Şans",
@ -569,6 +569,8 @@
"toConsume": "Tüketmek İçin", "toConsume": "Tüketmek İçin",
"howToUpgrade": "Nasıl yükseltirim?", "howToUpgrade": "Nasıl yükseltirim?",
"spendCoinsToGainExperiencePoints": "Deneyim puanı kazanmak için jetton harcayın", "spendCoinsToGainExperiencePoints": "Deneyim puanı kazanmak için jetton harcayın",
"richLevelUpgradeAnswer": "Servetin her şeyi fethetsin",
"charmLevelUpgradeAnswer": "Başkalarını cazibene boyun eğdir",
"higherLevelFancierAvatarFrame": "Daha yüksek seviye, daha şık rozetler/profil çerçevesi", "higherLevelFancierAvatarFrame": "Daha yüksek seviye, daha şık rozetler/profil çerçevesi",
"all": "Tümü", "all": "Tümü",
"gift": "Hediye", "gift": "Hediye",

View File

@ -79,6 +79,10 @@ class SCRoomMsgType {
/// ///
static const String rocketRewardUser = "ROCKET_REWARD_USER"; static const String rocketRewardUser = "ROCKET_REWARD_USER";
///
static const String voiceRoomRocketRewardUser =
"VOICE_ROOM_ROCKET_REWARD_USER";
/// ///
static const String rocketLaunchNotice = "ROCKET_LAUNCH_NOTICE"; static const String rocketLaunchNotice = "ROCKET_LAUNCH_NOTICE";
@ -129,4 +133,8 @@ class SCRoomMsgType {
static const String gameBurstCrystalSprint = "GAME_BURST_CRYSTAL_SPRINT"; static const String gameBurstCrystalSprint = "GAME_BURST_CRYSTAL_SPRINT";
static const String gameBurstCrystalBox = "GAME_BURST_CRYSTAL_BOX"; static const String gameBurstCrystalBox = "GAME_BURST_CRYSTAL_BOX";
static const String refreshOnlineUser = "REFRESH_ONLINE_USER"; static const String refreshOnlineUser = "REFRESH_ONLINE_USER";
static bool isRocketRewardUserType(String? type) {
return type == rocketRewardUser || type == voiceRoomRocketRewardUser;
}
} }

View File

@ -1045,6 +1045,10 @@ class SCAppLocalizations {
String get spendCoinsToGainExperiencePoints => String get spendCoinsToGainExperiencePoints =>
translate('spendCoinsToGainExperiencePoints'); translate('spendCoinsToGainExperiencePoints');
String get richLevelUpgradeAnswer => translate('richLevelUpgradeAnswer');
String get charmLevelUpgradeAnswer => translate('charmLevelUpgradeAnswer');
String get enterRoomName => translate('enterRoomName'); String get enterRoomName => translate('enterRoomName');
String get selectYourCountry => translate('selectYourCountry'); String get selectYourCountry => translate('selectYourCountry');

View File

@ -560,7 +560,6 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
/// ///
_floatingGiftListener(Msg msg) { _floatingGiftListener(Msg msg) {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false); final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
rtcProvider.scheduleRoomRocketStatusRefreshForGift();
if (!rtcProvider.shouldShowRoomVisualEffects) { if (!rtcProvider.shouldShowRoomVisualEffects) {
return; return;
} }

View File

@ -88,13 +88,7 @@ class _StoreChatboxPageState
height: 45.w, height: 45.w,
fit: BoxFit.contain, fit: BoxFit.contain,
), ),
SizedBox(height: 3.w), SizedBox(height: 10.w),
buildStoreBagItemTitle(
res.res.propsResources?.name ?? "",
textColor: Colors.white,
fontSize: 11.sp,
),
SizedBox(height: 3.w),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View File

@ -87,13 +87,7 @@ class StoreDataCardPageState
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
), ),
SizedBox(height: 8.w), SizedBox(height: 12.w),
buildStoreBagItemTitle(
res.res.propsResources?.name ?? "",
textColor: Colors.white,
fontSize: 11.sp,
),
SizedBox(height: 3.w),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View File

@ -87,13 +87,7 @@ class _StoreHeaddressPageState
width: 55.w, width: 55.w,
height: 55.w, height: 55.w,
), ),
SizedBox(height: 3.w), SizedBox(height: 10.w),
buildStoreBagItemTitle(
res.res.propsResources?.name ?? "",
textColor: Colors.white,
fontSize: 11.sp,
),
SizedBox(height: 3.w),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View File

@ -91,13 +91,7 @@ class _StoreMountainsPageState
width: 55.w, width: 55.w,
height: 55.w, height: 55.w,
), ),
SizedBox(height: 3.w), SizedBox(height: 10.w),
buildStoreBagItemTitle(
res.res.propsResources?.name ?? "",
textColor: Colors.white,
fontSize: 11.sp,
),
SizedBox(height: 3.w),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View File

@ -91,13 +91,7 @@ class _StoreThemePagePageState
height: 55.w, height: 55.w,
borderRadius: BorderRadius.all(Radius.circular(8.w)), borderRadius: BorderRadius.all(Radius.circular(8.w)),
), ),
SizedBox(height: 3.w), SizedBox(height: 10.w),
buildStoreBagItemTitle(
res.res.propsResources?.name ?? "",
textColor: Colors.white,
fontSize: 11.sp,
),
SizedBox(height: 3.w),
Row( Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View File

@ -120,6 +120,14 @@ class _UserLevelPageState extends State<UserLevelPage> {
color: Colors.white24, color: Colors.white24,
), ),
), ),
Container(
width: getLevelExperience(185.w),
height: 6.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: SocialChatTheme.primaryLight,
),
),
], ],
), ),
SizedBox(height: 5.w), SizedBox(height: 5.w),
@ -150,18 +158,6 @@ class _UserLevelPageState extends State<UserLevelPage> {
], ],
), ),
), ),
text(
SCAppLocalizations.of(context)!.medalAndAvatarFrameRewards,
fontSize: 20.sp,
fontWeight: FontWeight.bold,
textColor: SocialChatTheme.primaryLight,
),
text(
SCAppLocalizations.of(context)!.higherLevelFancierAvatarFrame,
fontSize: 12.sp,
fontWeight: FontWeight.w600,
textColor: Colors.white,
),
Image.asset("sc_images/level/sc_icon_user_level_center_bg_1.png"), Image.asset("sc_images/level/sc_icon_user_level_center_bg_1.png"),
text( text(
SCAppLocalizations.of(context)!.howToUpgrade, SCAppLocalizations.of(context)!.howToUpgrade,
@ -171,12 +167,13 @@ class _UserLevelPageState extends State<UserLevelPage> {
), ),
SizedBox(height: 3), SizedBox(height: 3),
text( text(
SCAppLocalizations.of( SCAppLocalizations.of(context)!.charmLevelUpgradeAnswer,
context,
)!.spendCoinsToGainExperiencePoints,
fontSize: 12.sp, fontSize: 12.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
textColor: Colors.white, textColor: Colors.white,
maxLines: 2,
maxWidth: 320.w,
textAlign: TextAlign.center,
), ),
SizedBox(height: 25.w), SizedBox(height: 25.w),
GestureDetector( GestureDetector(
@ -209,6 +206,9 @@ class _UserLevelPageState extends State<UserLevelPage> {
int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0"); int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0");
int nexExp = SCStringUtils.convertToInteger(res?.nextExperience ?? "0"); int nexExp = SCStringUtils.convertToInteger(res?.nextExperience ?? "0");
int levelExp = thatExp + nexExp; int levelExp = thatExp + nexExp;
if (levelExp <= 0) {
return 0;
}
return (thatExp / levelExp) * width; return (thatExp / levelExp) * width;
} }

View File

@ -120,6 +120,14 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
color: Colors.white24, color: Colors.white24,
), ),
), ),
Container(
width: getLevelExperience(185.w),
height: 6.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: SocialChatTheme.primaryLight,
),
),
], ],
), ),
SizedBox(height: 5.w), SizedBox(height: 5.w),
@ -150,18 +158,6 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
], ],
), ),
), ),
text(
SCAppLocalizations.of(context)!.medalAndAvatarFrameRewards,
fontSize: 20.sp,
fontWeight: FontWeight.bold,
textColor: SocialChatTheme.primaryLight,
),
text(
SCAppLocalizations.of(context)!.higherLevelFancierAvatarFrame,
fontSize: 12.sp,
fontWeight: FontWeight.w600,
textColor: Colors.white,
),
Image.asset("sc_images/level/sc_icon_user_wealth_center_bg_1.png"), Image.asset("sc_images/level/sc_icon_user_wealth_center_bg_1.png"),
text( text(
SCAppLocalizations.of(context)!.howToUpgrade, SCAppLocalizations.of(context)!.howToUpgrade,
@ -171,12 +167,13 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
), ),
SizedBox(height: 3), SizedBox(height: 3),
text( text(
SCAppLocalizations.of( SCAppLocalizations.of(context)!.richLevelUpgradeAnswer,
context,
)!.spendCoinsToGainExperiencePoints,
fontSize: 12.sp, fontSize: 12.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
textColor: Colors.white, textColor: Colors.white,
maxLines: 2,
maxWidth: 320.w,
textAlign: TextAlign.center,
), ),
SizedBox(height: 25.w), SizedBox(height: 25.w),
GestureDetector( GestureDetector(
@ -209,6 +206,9 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0"); int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0");
int nexExp = SCStringUtils.convertToInteger(res?.nextExperience ?? "0"); int nexExp = SCStringUtils.convertToInteger(res?.nextExperience ?? "0");
int levelExp = thatExp + nexExp; int levelExp = thatExp + nexExp;
if (levelExp <= 0) {
return 0;
}
return (thatExp / levelExp) * width; return (thatExp / levelExp) * width;
} }

View File

@ -32,7 +32,6 @@ import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart';
import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart'; import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart';
import 'package:yumi/ui_kit/widgets/props/sc_data_card_resource_view.dart';
import '../../../app/constants/sc_screen.dart'; import '../../../app/constants/sc_screen.dart';
import '../../../shared/business_logic/models/res/sc_user_counter_res.dart'; import '../../../shared/business_logic/models/res/sc_user_counter_res.dart';
@ -580,30 +579,8 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
); );
} }
Widget _buildProfileDataCardBackground(PropsResources? dataCard) { Widget _buildProfileDataCardBackground(PropsResources? _) {
if (dataCard == null) { return const ColoredBox(color: _profileBg);
return const ColoredBox(color: _profileBg);
}
return Stack(
fit: StackFit.expand,
children: [
SCDataCardResourceView(resource: dataCard, fit: BoxFit.cover),
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.26),
_profileBg.withValues(alpha: 0.54),
_profileBg.withValues(alpha: 0.78),
],
stops: const [0, 0.45, 1],
),
),
),
],
);
} }
PropsResources? _activeDataCard(SocialChatUserProfile? profile) { PropsResources? _activeDataCard(SocialChatUserProfile? profile) {

View File

@ -151,7 +151,7 @@ class _ProfileWallOfHonorsPageState extends State<ProfileWallOfHonorsPage> {
), ),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, crossAxisCount: 3,
mainAxisExtent: 132.w, mainAxisExtent: 96.w,
mainAxisSpacing: 14.w, mainAxisSpacing: 14.w,
crossAxisSpacing: 12.w, crossAxisSpacing: 12.w,
), ),
@ -161,39 +161,26 @@ class _ProfileWallOfHonorsPageState extends State<ProfileWallOfHonorsPage> {
Widget _buildBadgeItem(WearBadge badge) { Widget _buildBadgeItem(WearBadge badge) {
final url = _badgeUrl(badge); final url = _badgeUrl(badge);
final name = (badge.badgeName ?? "").trim(); return Center(
return Column( child: Container(
mainAxisSize: MainAxisSize.min, width: 86.w,
children: [ height: 86.w,
Container( alignment: Alignment.center,
width: 86.w, decoration: BoxDecoration(
height: 86.w, color: const Color(0xff0E332D),
alignment: Alignment.center, borderRadius: BorderRadius.circular(8.w),
decoration: BoxDecoration( border: Border.all(
color: const Color(0xff0E332D), color: const Color(0xff18F2B1).withValues(alpha: 0.24),
borderRadius: BorderRadius.circular(8.w),
border: Border.all(
color: const Color(0xff18F2B1).withValues(alpha: 0.24),
),
),
child: netImage(
url: url,
width: 68.w,
height: 68.w,
fit: BoxFit.contain,
noDefaultImg: true,
), ),
), ),
SizedBox(height: 8.w), child: netImage(
text( url: url,
name.isEmpty ? "--" : name, width: 68.w,
fontSize: 12, height: 68.w,
textColor: Colors.white, fit: BoxFit.contain,
fontWeight: FontWeight.w400, noDefaultImg: true,
maxLines: 2,
textAlign: TextAlign.center,
), ),
], ),
); );
} }

View File

@ -13,6 +13,7 @@ import 'package:yumi/shared/tools/sc_loading_manager.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart';
import 'package:yumi/ui_kit/widgets/props/sc_data_card_resource_view.dart';
class VipDetailPage extends StatefulWidget { class VipDetailPage extends StatefulWidget {
const VipDetailPage({super.key}); const VipDetailPage({super.key});
@ -58,7 +59,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
final status = results[1] as SCVipStatusRes; final status = results[1] as SCVipStatusRes;
final resolvedStatus = _resolveStatus(status, home.state); final resolvedStatus = _resolveStatus(status, home.state);
final selectedLevel = _resolveInitialLevel(home, resolvedStatus); final selectedLevel = _resolveInitialLevel(home, resolvedStatus);
_logLoadedVipData(home, resolvedStatus, selectedLevel);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
@ -141,11 +141,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
_selectedLevel = level; _selectedLevel = level;
_preview = null; _preview = null;
}); });
_logSelectedLevelResources(
level,
config: _levelConfigFor(level),
status: _status,
);
_loadPreviewForLevel(level); _loadPreviewForLevel(level);
} }
@ -201,6 +196,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
await _loadVipData(showLoading: false); await _loadVipData(showLoading: false);
} }
} catch (error) { } catch (error) {
debugPrint('VIP purchase failed: $error');
} finally { } finally {
SCLoadingManager.hide(); SCLoadingManager.hide();
if (mounted) { if (mounted) {
@ -214,56 +210,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
return 'vip-buy-$userId-${DateTime.now().millisecondsSinceEpoch}'; return 'vip-buy-$userId-${DateTime.now().millisecondsSinceEpoch}';
} }
void _logLoadedVipData(
SCVipHomeRes home,
SCVipStatusRes? status,
int selectedLevel,
) {
if (home.configured != true) {}
if (home.levels.isEmpty) {}
_logSelectedLevelResources(
selectedLevel,
config: _levelConfigFromHome(home, selectedLevel),
status: status,
);
}
void _logSelectedLevelResources(
int level, {
required SCVipLevelConfigRes? config,
required SCVipStatusRes? status,
}) {
final state = status?.levelInt == level ? status : null;
if (config == null && state == null) {
return;
}
final badge = _preferResource(config?.badge, state?.badge);
final avatarFrame = _preferResource(
config?.avatarFrame,
state?.avatarFrame,
);
final entryEffect = _preferResource(
config?.entryEffect,
state?.entryEffect,
);
final chatBubble = _preferResource(config?.chatBubble, state?.chatBubble);
final floatPicture = _preferResource(
config?.floatPicture,
state?.floatPicture,
);
}
String _resourceLogValue(SCVipResourceRes? resource) {
if (!_hasResource(resource)) {
return 'null';
}
return '{id:${resource?.resourceId},name:${resource?.name},'
'hasPreviewUrl:${resource?.hasPreviewUrl},'
'hasSourceUrl:${resource?.hasSourceUrl},'
'previewUrl:${resource?.previewUrl},'
'sourceUrl:${resource?.sourceResourceUrl}}';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -524,6 +470,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
Widget _buildLevelCard() { Widget _buildLevelCard() {
final assetLevel = _assetLevel; final assetLevel = _assetLevel;
final backgroundCard = _selectedBackgroundCard;
final effectImage = _selectedEffectImage ?? _selectedLongBadge;
return Center( return Center(
child: SizedBox( child: SizedBox(
width: 351.w, width: 351.w,
@ -532,18 +480,23 @@ class _VipDetailPageState extends State<VipDetailPage> {
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned.fill( Positioned.fill(
child: Container( child: ClipRRect(
decoration: BoxDecoration( borderRadius: BorderRadius.circular(8.w),
borderRadius: BorderRadius.circular(8.w), child: _buildVipCardBackground(
border: Border.all( resource: backgroundCard,
color: const Color(0xFFEBCB76), assetLevel: assetLevel,
width: 1.w, ),
), ),
image: DecorationImage( ),
image: AssetImage( Positioned.fill(
'sc_images/vip/sc_vip_card_bg_$assetLevel.png', child: IgnorePointer(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.w),
border: Border.all(
color: const Color(0xFFEBCB76),
width: 1.w,
), ),
fit: BoxFit.fill,
), ),
), ),
), ),
@ -575,8 +528,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
PositionedDirectional( PositionedDirectional(
end: 24.w, end: 24.w,
top: -36.w, top: -36.w,
child: _buildResourceImage( child: _buildDynamicResourceVisual(
resource: _selectedBadge, resource: effectImage,
width: 135.w, width: 135.w,
height: 135.w, height: 135.w,
fallbackIcon: Icons.workspace_premium_outlined, fallbackIcon: Icons.workspace_premium_outlined,
@ -588,6 +541,27 @@ class _VipDetailPageState extends State<VipDetailPage> {
); );
} }
Widget _buildVipCardBackground({
required SCVipResourceRes? resource,
required int assetLevel,
}) {
final fallback = Image.asset(
'sc_images/vip/sc_vip_card_bg_$assetLevel.png',
fit: BoxFit.fill,
width: double.infinity,
height: double.infinity,
);
if (!_hasResourceUrl(resource)) {
return fallback;
}
return SCDataCardResourceView(
sourceUrl: resource?.sourceResourceUrl,
coverUrl: resource?.previewUrl,
fit: BoxFit.fill,
fallback: fallback,
);
}
Widget _buildLevelDivider() { Widget _buildLevelDivider() {
final ornamentLayout = _ornamentLayoutForLevel(_assetLevel); final ornamentLayout = _ornamentLayoutForLevel(_assetLevel);
return SizedBox( return SizedBox(
@ -713,9 +687,14 @@ class _VipDetailPageState extends State<VipDetailPage> {
); );
} }
SCVipResourceRes? get _selectedBadge => _selectedResource( SCVipResourceRes? get _selectedLongBadge => _selectedResource(
fromConfig: (config) => config.badge, fromConfig: (config) => config.longBadge,
fromState: (status) => status.badge, fromState: (status) => status.longBadge,
);
SCVipResourceRes? get _selectedShortBadge => _selectedResource(
fromConfig: (config) => config.shortBadge,
fromState: (status) => status.shortBadge,
); );
SCVipResourceRes? get _selectedAvatarFrame => _selectedResource( SCVipResourceRes? get _selectedAvatarFrame => _selectedResource(
@ -738,6 +717,16 @@ class _VipDetailPageState extends State<VipDetailPage> {
fromState: (status) => status.floatPicture, fromState: (status) => status.floatPicture,
); );
SCVipResourceRes? get _selectedBackgroundCard => _selectedResource(
fromConfig: (config) => config.backgroundCard,
fromState: (status) => status.backgroundCard,
);
SCVipResourceRes? get _selectedEffectImage => _selectedResource(
fromConfig: (config) => config.effectImage,
fromState: (status) => status.effectImage,
);
SCVipResourceRes? _selectedResource({ SCVipResourceRes? _selectedResource({
required SCVipResourceRes? Function(SCVipLevelConfigRes config) fromConfig, required SCVipResourceRes? Function(SCVipLevelConfigRes config) fromConfig,
required SCVipResourceRes? Function(SCVipStatusRes status) fromState, required SCVipResourceRes? Function(SCVipStatusRes status) fromState,
@ -768,8 +757,14 @@ class _VipDetailPageState extends State<VipDetailPage> {
final items = <_VipFeatureData>[]; final items = <_VipFeatureData>[];
_addResourceItem( _addResourceItem(
items, items,
resource: _selectedBadge, resource: _selectedLongBadge,
fallbackTitle: 'VIP Badge', fallbackTitle: 'Long Badge',
fallbackIcon: Icons.workspace_premium_outlined,
);
_addResourceItem(
items,
resource: _selectedShortBadge,
fallbackTitle: 'Short Badge',
fallbackIcon: Icons.workspace_premium_outlined, fallbackIcon: Icons.workspace_premium_outlined,
); );
_addResourceItem( _addResourceItem(
@ -797,12 +792,19 @@ class _VipDetailPageState extends State<VipDetailPage> {
fallbackIcon: Icons.airline_seat_recline_extra, fallbackIcon: Icons.airline_seat_recline_extra,
wide: true, wide: true,
); );
_addResourceItem(
items,
resource: _selectedBackgroundCard,
fallbackTitle: 'Profile Card',
fallbackIcon: Icons.badge_outlined,
wide: true,
);
return items; return items;
} }
List<_VipFeatureData> get _vipPrivilegeItems { List<_VipFeatureData> get _vipPrivilegeItems {
// The VIP home API currently returns resource perks only: // The VIP home API currently returns resource perks only:
// badge/avatarFrame/entryEffect/chatBubble/floatPicture. // badges/avatarFrame/entryEffect/chatBubble/floatPicture/backgroundCard.
// Do not render demo-only functional privileges without backend data. // Do not render demo-only functional privileges without backend data.
return const <_VipFeatureData>[]; return const <_VipFeatureData>[];
} }
@ -844,6 +846,40 @@ class _VipDetailPageState extends State<VipDetailPage> {
return fallbackTitle; return fallbackTitle;
} }
Widget _buildDynamicResourceVisual({
required SCVipResourceRes? resource,
required double width,
required double height,
required IconData fallbackIcon,
}) {
final sourceUrl = resource?.sourceResourceUrl?.trim();
final previewUrl = resource?.previewUrl?.trim();
final hasSource = sourceUrl != null && sourceUrl.isNotEmpty;
final hasPreview = previewUrl != null && previewUrl.isNotEmpty;
if (hasSource || hasPreview) {
return SCDataCardResourceView(
sourceUrl: sourceUrl,
coverUrl: previewUrl,
width: width,
height: height,
fit: BoxFit.contain,
fallback: _buildResourceImage(
resource: resource,
width: width,
height: height,
fallbackIcon: fallbackIcon,
),
);
}
return _buildResourceImage(
resource: resource,
width: width,
height: height,
fallbackIcon: fallbackIcon,
);
}
Widget _buildResourceImage({ Widget _buildResourceImage({
required SCVipResourceRes? resource, required SCVipResourceRes? resource,
required double width, required double width,

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
@ -291,6 +292,20 @@ class _PendingRoomStartupSeatAction {
final String? inviterId; final String? inviterId;
} }
class _PersistedVoiceRoomCleanupRequest {
const _PersistedVoiceRoomCleanupRequest({
required this.staleRoomId,
required this.targetRoomId,
required this.restoreOnlineHeartbeatWhenIdle,
this.clearMarkerOnly = false,
});
final String staleRoomId;
final String targetRoomId;
final bool restoreOnlineHeartbeatWhenIdle;
final bool clearMarkerOnly;
}
class RealTimeCommunicationManager extends ChangeNotifier { class RealTimeCommunicationManager extends ChangeNotifier {
static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog'; static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog';
static const String _roomRocketPagLaunchDialogTag = static const String _roomRocketPagLaunchDialogTag =
@ -304,6 +319,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Duration(seconds: 5), Duration(seconds: 5),
Duration(seconds: 10), Duration(seconds: 10),
]; ];
static const List<Duration> _roomRocketEntryReadyFallbackDelays = [
Duration(milliseconds: 300),
Duration(milliseconds: 900),
Duration(milliseconds: 1800),
Duration(milliseconds: 3200),
];
static const int _roomPasswordNotTrueErrorCode = 9005; static const int _roomPasswordNotTrueErrorCode = 9005;
static const Duration _micListPollingInterval = Duration(seconds: 2); static const Duration _micListPollingInterval = Duration(seconds: 2);
@ -343,8 +364,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Timer? _onlineUsersPollingTimer; Timer? _onlineUsersPollingTimer;
Timer? _roomEntryEffectTimer; Timer? _roomEntryEffectTimer;
Timer? _roomRocketLaunchAnimationTimer; Timer? _roomRocketLaunchAnimationTimer;
ValueNotifier<String?>? _roomRocketLaunchTopAvatarNotifier;
Timer? _roomRocketGiftRefreshTimer; Timer? _roomRocketGiftRefreshTimer;
final List<Timer> _roomRocketPostLaunchRefreshTimers = []; final List<Timer> _roomRocketPostLaunchRefreshTimers = [];
final List<Timer> _roomRocketEntryReadyFallbackTimers = [];
Timer? _roomRedPacketPresenceTimer; Timer? _roomRedPacketPresenceTimer;
Future<void>? _rtcEnginePrewarmTask; Future<void>? _rtcEnginePrewarmTask;
Future<void>? _pendingRoomSwitchRtcLeaveTask; Future<void>? _pendingRoomSwitchRtcLeaveTask;
@ -490,8 +513,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomEntryEffectTimer = null; _roomEntryEffectTimer = null;
_roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null; _roomRocketLaunchAnimationTimer = null;
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
_releaseRoomRocketLaunchTopAvatarNotifier();
_roomRocketGiftRefreshTimer?.cancel(); _roomRocketGiftRefreshTimer?.cancel();
_roomRocketGiftRefreshTimer = null; _roomRocketGiftRefreshTimer = null;
_cancelRoomRocketEntryReadyFallbacks();
_cancelRoomRocketPostLaunchStatusRefresh(); _cancelRoomRocketPostLaunchStatusRefresh();
} }
_roomVisualEffectsEnabled = enabled; _roomVisualEffectsEnabled = enabled;
@ -500,6 +526,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
void setVoiceRoomRouteVisible(bool visible) { void setVoiceRoomRouteVisible(bool visible) {
_voiceRoomRouteVisible = visible; _voiceRoomRouteVisible = visible;
if (visible) {
final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
if (roomId.isNotEmpty) {
_scheduleRoomRocketEntryReadyFallback(roomId);
}
}
} }
void _setRoomStartupLoading() { void _setRoomStartupLoading() {
@ -1780,10 +1812,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (!context.mounted) { if (!context.mounted) {
return; return;
} }
await cleanupPersistedVoiceRoomSessionBeforeEntry(targetRoomId: roomId); final persistedRoomCleanupRequest =
if (!context.mounted) { _preparePersistedVoiceRoomCleanupBeforeEntry(
return; targetRoomId: roomId,
} restoreOnlineHeartbeatWhenIdle: false,
);
if (roomId == currenRoom?.roomProfile?.roomProfile?.id) { if (roomId == currenRoom?.roomProfile?.roomProfile?.id) {
if (_currentRoomIsEntryPreview || if (_currentRoomIsEntryPreview ||
_roomStartupStatus == RoomStartupStatus.loading) { _roomStartupStatus == RoomStartupStatus.loading) {
@ -1792,6 +1825,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
_startVoiceRoomForegroundService(); _startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context); VoiceRoomRoute.openVoiceRoom(context);
_startPersistedVoiceRoomCleanupInBackground(
persistedRoomCleanupRequest,
);
return; return;
} }
@ -1830,6 +1866,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_refreshRoomRedPacketListAfterEntry(roomId: roomId); _refreshRoomRedPacketListAfterEntry(roomId: roomId);
_startVoiceRoomForegroundService(); _startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context); VoiceRoomRoute.openVoiceRoom(context);
_startPersistedVoiceRoomCleanupInBackground(persistedRoomCleanupRequest);
if (shouldRejoinRoomRtc) { if (shouldRejoinRoomRtc) {
unawaited(_rejoinCurrentRoomRtcForSameRoomReuse(roomId)); unawaited(_rejoinCurrentRoomRtcForSameRoomReuse(roomId));
} }
@ -1865,6 +1902,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
); );
_startVoiceRoomForegroundService(); _startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context); VoiceRoomRoute.openVoiceRoom(context);
_startPersistedVoiceRoomCleanupInBackground(persistedRoomCleanupRequest);
notifyListeners(); notifyListeners();
final entryRequestSerial = ++_roomEntryRequestSerial; final entryRequestSerial = ++_roomEntryRequestSerial;
if (preEnteredRoom != null) { if (preEnteredRoom != null) {
@ -2487,14 +2525,70 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Future<void> cleanupPersistedVoiceRoomSessionBeforeEntry({ Future<void> cleanupPersistedVoiceRoomSessionBeforeEntry({
String? targetRoomId, String? targetRoomId,
bool restoreOnlineHeartbeatWhenIdle = true,
}) { }) {
final cleanupRequest = _preparePersistedVoiceRoomCleanupBeforeEntry(
targetRoomId: targetRoomId,
restoreOnlineHeartbeatWhenIdle: restoreOnlineHeartbeatWhenIdle,
);
return _runPersistedVoiceRoomCleanup(cleanupRequest);
}
_PersistedVoiceRoomCleanupRequest?
_preparePersistedVoiceRoomCleanupBeforeEntry({
String? targetRoomId,
bool restoreOnlineHeartbeatWhenIdle = true,
}) {
final staleRoomId = DataPersistence.getLastTimeRoomId().trim();
if (staleRoomId.isEmpty) {
return null;
}
final currentRoomId =
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
if (currentRoomId.isNotEmpty) {
return null;
}
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
if (currentUserId.isEmpty || AccountStorage().getToken().trim().isEmpty) {
return null;
}
final normalizedTargetRoomId = (targetRoomId ?? "").trim();
if (normalizedTargetRoomId.isNotEmpty &&
normalizedTargetRoomId == staleRoomId) {
return null;
}
final staleRoomUserId = DataPersistence.getLastTimeRoomUserId().trim();
return _PersistedVoiceRoomCleanupRequest(
staleRoomId: staleRoomId,
targetRoomId: normalizedTargetRoomId,
restoreOnlineHeartbeatWhenIdle: restoreOnlineHeartbeatWhenIdle,
clearMarkerOnly:
staleRoomUserId.isNotEmpty && staleRoomUserId != currentUserId,
);
}
void _startPersistedVoiceRoomCleanupInBackground(
_PersistedVoiceRoomCleanupRequest? cleanupRequest,
) {
unawaited(_runPersistedVoiceRoomCleanup(cleanupRequest));
}
Future<void> _runPersistedVoiceRoomCleanup(
_PersistedVoiceRoomCleanupRequest? cleanupRequest,
) {
if (cleanupRequest == null) {
return Future<void>.value();
}
final runningTask = _persistedRoomCleanupTask; final runningTask = _persistedRoomCleanupTask;
if (runningTask != null) { if (runningTask != null) {
return runningTask; return runningTask;
} }
final task = _cleanupPersistedVoiceRoomSessionBeforeEntry( final task = _cleanupPersistedVoiceRoomSessionBeforeEntry(cleanupRequest);
targetRoomId: targetRoomId,
);
_persistedRoomCleanupTask = task; _persistedRoomCleanupTask = task;
return task.whenComplete(() { return task.whenComplete(() {
if (identical(_persistedRoomCleanupTask, task)) { if (identical(_persistedRoomCleanupTask, task)) {
@ -2503,28 +2597,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}); });
} }
Future<void> _cleanupPersistedVoiceRoomSessionBeforeEntry({ Future<void> _cleanupPersistedVoiceRoomSessionBeforeEntry(
String? targetRoomId, _PersistedVoiceRoomCleanupRequest cleanupRequest,
}) async { ) async {
final staleRoomId = DataPersistence.getLastTimeRoomId().trim(); final staleRoomId = cleanupRequest.staleRoomId;
if (staleRoomId.isEmpty) { if (cleanupRequest.clearMarkerOnly) {
return;
}
final currentRoomId =
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
if (currentRoomId.isNotEmpty) {
return;
}
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
if (currentUserId.isEmpty || AccountStorage().getToken().trim().isEmpty) {
return;
}
final staleRoomUserId = DataPersistence.getLastTimeRoomUserId().trim();
if (staleRoomUserId.isNotEmpty && staleRoomUserId != currentUserId) {
await _clearPersistedRoomMarkerIfMatches(staleRoomId); await _clearPersistedRoomMarkerIfMatches(staleRoomId);
return; return;
} }
@ -2545,7 +2622,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
); );
if (didClean) { if (didClean) {
await _clearPersistedRoomMarkerIfMatches(staleRoomId); await _clearPersistedRoomMarkerIfMatches(staleRoomId);
if ((targetRoomId ?? "").trim() != staleRoomId) { final currentRoomId =
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
if (cleanupRequest.restoreOnlineHeartbeatWhenIdle &&
cleanupRequest.targetRoomId != staleRoomId &&
currentRoomId.isEmpty) {
await SCHeartbeatUtils.scheduleHeartbeat( await SCHeartbeatUtils.scheduleHeartbeat(
SCHeartbeatStatus.ONLINE.name, SCHeartbeatStatus.ONLINE.name,
false, false,
@ -2594,6 +2675,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
setRoomVisualEffectsEnabled(true); setRoomVisualEffectsEnabled(true);
if (shouldOpenRoomPage) { if (shouldOpenRoomPage) {
VoiceRoomRoute.openVoiceRoom(context!); VoiceRoomRoute.openVoiceRoom(context!);
setVoiceRoomRouteVisible(true);
} }
unawaited( unawaited(
_bootstrapEnteredVoiceRoomSession( _bootstrapEnteredVoiceRoomSession(
@ -2694,6 +2776,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
roomId: roomId, roomId: roomId,
); );
_loadRoomSecondaryData(); _loadRoomSecondaryData();
_scheduleRoomRocketEntryReadyFallback(roomId);
} catch (error, stackTrace) { } catch (error, stackTrace) {
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) { if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
return; return;
@ -2806,18 +2889,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
void _loadRoomSecondaryData() { void _loadRoomSecondaryData() {
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
SCChatRoomRepository().rocketClaim(roomId).catchError((e) {
return true; //
});
SCChatRoomRepository() SCChatRoomRepository()
.rocketStatus(roomId) .rocketStatus(roomId)
.then((res) { .then((res) {
roomRocketStatus = res; roomRocketStatus = res;
notifyListeners(); notifyListeners();
if (!res.isEnabled) {
return;
}
_scheduleRoomRocketAssetPreload(roomId, res, afterRoomEntry: true); _scheduleRoomRocketAssetPreload(roomId, res, afterRoomEntry: true);
_scheduleRoomRocketEntryReadyFallback(roomId);
_loadRoomRocketRewardPopups(roomId);
}) })
.catchError((e) {}); .catchError((e) {});
_loadRoomRocketRewardPopups(roomId);
_refreshRoomRedPacketListAfterEntry(); _refreshRoomRedPacketListAfterEntry();
fetchRoomTaskClaimableCount(); fetchRoomTaskClaimableCount();
@ -2873,6 +2957,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) { void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) {
roomRocketStatus = res; roomRocketStatus = res;
notifyListeners(); notifyListeners();
if (!res.isEnabled) {
return;
}
_scheduleRoomRocketAssetPreload( _scheduleRoomRocketAssetPreload(
(res.roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(), (res.roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(),
res, res,
@ -2893,6 +2980,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
roomRocketStatus = res; roomRocketStatus = res;
notifyListeners(); notifyListeners();
if (!res.isEnabled) {
return;
}
_scheduleRoomRocketAssetPreload(resolvedRoomId, res); _scheduleRoomRocketAssetPreload(resolvedRoomId, res);
} }
@ -2905,6 +2995,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (resolvedRoomId.isEmpty) { if (resolvedRoomId.isEmpty) {
return; return;
} }
if (!status.isEnabled) {
return;
}
if (afterRoomEntry) { if (afterRoomEntry) {
RoomRocketAssetPreloader.preloadAfterRoomEntry( RoomRocketAssetPreloader.preloadAfterRoomEntry(
roomId: resolvedRoomId, roomId: resolvedRoomId,
@ -2937,6 +3030,42 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}); });
} }
void _scheduleRoomRocketEntryReadyFallback(String roomId) {
final normalizedRoomId = roomId.trim();
if (normalizedRoomId.isEmpty) {
return;
}
_notifyRoomRocketEntryReadyIfCurrent(normalizedRoomId);
for (final delay in _roomRocketEntryReadyFallbackDelays) {
final timer = Timer(delay, () {
_roomRocketEntryReadyFallbackTimers.removeWhere(
(item) => !item.isActive,
);
_notifyRoomRocketEntryReadyIfCurrent(normalizedRoomId);
});
_roomRocketEntryReadyFallbackTimers.add(timer);
}
}
void _notifyRoomRocketEntryReadyIfCurrent(String roomId) {
final normalizedRoomId = roomId.trim();
final currentRoomId =
currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
if (normalizedRoomId.isEmpty ||
currentRoomId != normalizedRoomId ||
!_voiceRoomRouteVisible) {
return;
}
rtmProvider?.handleVoiceRoomReadyForRocketRewards(normalizedRoomId);
}
void _cancelRoomRocketEntryReadyFallbacks() {
for (final timer in _roomRocketEntryReadyFallbackTimers) {
timer.cancel();
}
_roomRocketEntryReadyFallbackTimers.clear();
}
void scheduleRoomRocketPostLaunchStatusRefresh({String? roomId}) { void scheduleRoomRocketPostLaunchStatusRefresh({String? roomId}) {
final resolvedRoomId = final resolvedRoomId =
(roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(); (roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
@ -2973,6 +3102,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
_roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer?.cancel();
_releaseRoomRocketLaunchTopAvatarNotifier();
if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible) { if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible) {
final animationUrl = final animationUrl =
@ -2990,6 +3120,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomRocketLaunchAnimationTimer = Timer(launch.displayDuration, () { _roomRocketLaunchAnimationTimer = Timer(launch.displayDuration, () {
_roomRocketLaunchAnimationTimer = null; _roomRocketLaunchAnimationTimer = null;
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
_releaseRoomRocketLaunchTopAvatarNotifier();
unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
scheduleRoomRocketPostLaunchStatusRefresh(roomId: launch.roomId); scheduleRoomRocketPostLaunchStatusRefresh(roomId: launch.roomId);
}); });
@ -3015,6 +3146,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
required RoomRocketLaunchBroadcastMessage launch, required RoomRocketLaunchBroadcastMessage launch,
}) { }) {
if (RoomRocketPagEffectOverlay.isPag(animationUrl)) { if (RoomRocketPagEffectOverlay.isPag(animationUrl)) {
final topAvatarNotifier = ValueNotifier<String?>(
_roomRocketTop1AvatarUrl(launch.safeLevel),
);
_roomRocketLaunchTopAvatarNotifier = topAvatarNotifier;
unawaited(_loadRoomRocketTop1Avatar(launch, topAvatarNotifier));
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
SmartDialog.show( SmartDialog.show(
tag: _roomRocketPagLaunchDialogTag, tag: _roomRocketPagLaunchDialogTag,
@ -3026,11 +3162,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
builder: builder:
(_) => RoomRocketPagEffectOverlay( (_) => RoomRocketPagEffectOverlay(
resource: animationUrl, resource: animationUrl,
topAvatarUrl: _roomRocketTop1AvatarUrl(), topAvatarUrlListenable: topAvatarNotifier,
onCompleted: () { onCompleted: () {
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
_roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null; _roomRocketLaunchAnimationTimer = null;
_releaseRoomRocketLaunchTopAvatarNotifier(topAvatarNotifier);
unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
scheduleRoomRocketPostLaunchStatusRefresh( scheduleRoomRocketPostLaunchStatusRefresh(
roomId: launch.roomId, roomId: launch.roomId,
@ -3047,8 +3184,21 @@ class RealTimeCommunicationManager extends ChangeNotifier {
); );
} }
String? _roomRocketTop1AvatarUrl() { String? _roomRocketTop1AvatarUrl(int level) {
final kings = roomRocketStatus?.rocketKings ?? const []; final kings =
roomRocketStatus?.rocketKingsForLevel(level) ??
const <SCRoomRocketKingUserRes>[];
return _roomRocketTop1AvatarUrlFromUsers(kings);
}
String? _roomRocketTop1AvatarUrlFromKing(SCRoomRocketKingRes king) {
final source = king.podium.isNotEmpty ? king.podium : king.displayRecords;
return _roomRocketTop1AvatarUrlFromUsers(source);
}
String? _roomRocketTop1AvatarUrlFromUsers(
List<SCRoomRocketKingUserRes> kings,
) {
if (kings.isEmpty) { if (kings.isEmpty) {
return null; return null;
} }
@ -3065,6 +3215,90 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return null; return null;
} }
Future<void> _loadRoomRocketTop1Avatar(
RoomRocketLaunchBroadcastMessage launch,
ValueNotifier<String?> notifier,
) async {
final roomId = launch.roomId.trim();
if (roomId.isEmpty) {
return;
}
try {
_roomRocketDebug(
'request launch top1 avatar roomId=$roomId '
'level=${launch.safeLevel} roundNo=${launch.roundNo}',
);
var king = await SCChatRoomRepository().roomRocketKing(
roomId,
level: launch.safeLevel,
roundNo: launch.roundNo,
cursor: 1,
limit: 3,
);
var avatar = _roomRocketTop1AvatarUrlFromKing(king);
if (avatar == null && launch.roundNo > 0) {
_roomRocketDebug(
'launch top1 avatar retry without roundNo '
'level=${launch.safeLevel} roundNo=${launch.roundNo}',
);
king = await SCChatRoomRepository().roomRocketKing(
roomId,
level: launch.safeLevel,
cursor: 1,
limit: 3,
);
avatar = _roomRocketTop1AvatarUrlFromKing(king);
}
if (_roomRocketLaunchTopAvatarNotifier != notifier) {
return;
}
_roomRocketDebug(
'launch top1 avatar loaded level=${launch.safeLevel} '
'roundNo=${launch.roundNo} avatar=${_shortRoomRocketDebugUrl(avatar)} '
'podium=${king.podium.length} records=${king.records.length}',
);
if (avatar != null && avatar != notifier.value) {
notifier.value = avatar;
}
} catch (error) {
_roomRocketDebug(
'launch top1 avatar load failed level=${launch.safeLevel} '
'roundNo=${launch.roundNo}: $error',
);
}
}
void _releaseRoomRocketLaunchTopAvatarNotifier([
ValueNotifier<String?>? expected,
]) {
final notifier = _roomRocketLaunchTopAvatarNotifier;
if (notifier == null || (expected != null && notifier != expected)) {
return;
}
_roomRocketLaunchTopAvatarNotifier = null;
WidgetsBinding.instance.addPostFrameCallback((_) {
notifier.dispose();
});
}
void _roomRocketDebug(String message) {
if (!kDebugMode) {
return;
}
debugPrint('[RoomRocket] $message');
}
String _shortRoomRocketDebugUrl(String? url) {
final value = url?.trim() ?? '';
if (value.isEmpty) {
return '-';
}
if (value.length <= 96) {
return value;
}
return '${value.substring(0, 48)}...${value.substring(value.length - 24)}';
}
RoomRocketRewardUserProfile? roomRocketRewardUserProfileForRecord( RoomRocketRewardUserProfile? roomRocketRewardUserProfileForRecord(
SCRoomRocketRewardRecordRes record, SCRoomRocketRewardRecordRes record,
) { ) {
@ -3367,9 +3601,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
final exitRtmProvider = rtmProvider; final exitRtmProvider = rtmProvider;
final navigationContext = context; final navigationContext = context;
try { try {
exitRtmProvider?.msgAllListener = null;
exitRtmProvider?.msgChatListener = null;
exitRtmProvider?.msgGiftListener = null;
SCGiftVapSvgaManager().stopPlayback(); SCGiftVapSvgaManager().stopPlayback();
setRoomVisualEffectsEnabled(false); setRoomVisualEffectsEnabled(false);
SCHeartbeatUtils.cancelTimer(); SCHeartbeatUtils.cancelTimer();
@ -3405,9 +3636,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
final isPreviewOnlyExit = _currentRoomIsEntryPreview; final isPreviewOnlyExit = _currentRoomIsEntryPreview;
final exitRtmProvider = rtmProvider; final exitRtmProvider = rtmProvider;
try { try {
exitRtmProvider?.msgAllListener = null;
exitRtmProvider?.msgChatListener = null;
exitRtmProvider?.msgGiftListener = null;
SCGiftVapSvgaManager().stopPlayback(); SCGiftVapSvgaManager().stopPlayback();
setRoomVisualEffectsEnabled(false); setRoomVisualEffectsEnabled(false);
SCHeartbeatUtils.cancelTimer(); SCHeartbeatUtils.cancelTimer();
@ -3613,6 +3841,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomEntryEffectTimer = null; _roomEntryEffectTimer = null;
_roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null; _roomRocketLaunchAnimationTimer = null;
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
_releaseRoomRocketLaunchTopAvatarNotifier();
_roomRocketGiftRefreshTimer?.cancel(); _roomRocketGiftRefreshTimer?.cancel();
_roomRocketGiftRefreshTimer = null; _roomRocketGiftRefreshTimer = null;
_cancelRoomRocketPostLaunchStatusRefresh(); _cancelRoomRocketPostLaunchStatusRefresh();

File diff suppressed because it is too large Load Diff

View File

@ -37,6 +37,8 @@ class SCRoomRocketStatusRes {
bool? hasClaimed, bool? hasClaimed,
num? myContribution, num? myContribution,
num? myContributionRate, num? myContributionRate,
bool? configured,
bool? enabled,
int? roundNo, int? roundNo,
bool? displayRound, bool? displayRound,
int? currentLevel, int? currentLevel,
@ -69,6 +71,8 @@ class SCRoomRocketStatusRes {
_hasClaimed = hasClaimed; _hasClaimed = hasClaimed;
_myContribution = myContribution; _myContribution = myContribution;
_myContributionRate = myContributionRate; _myContributionRate = myContributionRate;
_configured = configured;
_enabled = enabled;
_roundNo = roundNo; _roundNo = roundNo;
_displayRound = displayRound; _displayRound = displayRound;
_currentLevel = currentLevel; _currentLevel = currentLevel;
@ -87,6 +91,15 @@ class SCRoomRocketStatusRes {
} }
SCRoomRocketStatusRes.fromJson(dynamic json) { SCRoomRocketStatusRes.fromJson(dynamic json) {
_configured = _boolValue(json['configured']);
_enabled =
_boolValue(
json['enabled'] ??
json['enable'] ??
json['rocketEnabled'] ??
json['rocket_enabled'],
) ??
true;
_roomId = json['roomId'] ?? json['room_id']; _roomId = json['roomId'] ?? json['room_id'];
_date = json['date'] ?? json['dayKey'] ?? json['day_key']; _date = json['date'] ?? json['dayKey'] ?? json['day_key'];
_roundNo = _intValue(json['roundNo'] ?? json['round_no']); _roundNo = _intValue(json['roundNo'] ?? json['round_no']);
@ -219,6 +232,8 @@ class SCRoomRocketStatusRes {
bool? _hasClaimed; bool? _hasClaimed;
num? _myContribution; num? _myContribution;
num? _myContributionRate; num? _myContributionRate;
bool? _configured;
bool? _enabled;
int? _roundNo; int? _roundNo;
bool? _displayRound; bool? _displayRound;
int? _currentLevel; int? _currentLevel;
@ -267,6 +282,17 @@ class SCRoomRocketStatusRes {
num? get myContributionRate => _myContributionRate; num? get myContributionRate => _myContributionRate;
bool? get configured => _configured;
bool? get enabled => _enabled;
bool get isEnabled {
final normalizedStatus = (_status ?? '').trim().toUpperCase();
return _configured != false &&
_enabled != false &&
normalizedStatus != 'DISABLED';
}
int? get roundNo => _roundNo; int? get roundNo => _roundNo;
bool? get displayRound => _displayRound; bool? get displayRound => _displayRound;
@ -371,6 +397,8 @@ class SCRoomRocketStatusRes {
map['hasClaimed'] = _hasClaimed; map['hasClaimed'] = _hasClaimed;
map['myContribution'] = _myContribution; map['myContribution'] = _myContribution;
map['myContributionRate'] = _myContributionRate; map['myContributionRate'] = _myContributionRate;
map['configured'] = _configured;
map['enabled'] = _enabled;
map['roundNo'] = _roundNo; map['roundNo'] = _roundNo;
map['displayRound'] = _displayRound; map['displayRound'] = _displayRound;
map['currentLevel'] = _currentLevel; map['currentLevel'] = _currentLevel;

View File

@ -110,10 +110,14 @@ class SCVipStatusRes {
String? startAt, String? startAt,
String? expireAt, String? expireAt,
SCVipResourceRes? badge, SCVipResourceRes? badge,
SCVipResourceRes? longBadge,
SCVipResourceRes? shortBadge,
SCVipResourceRes? avatarFrame, SCVipResourceRes? avatarFrame,
SCVipResourceRes? entryEffect, SCVipResourceRes? entryEffect,
SCVipResourceRes? chatBubble, SCVipResourceRes? chatBubble,
SCVipResourceRes? floatPicture, SCVipResourceRes? floatPicture,
SCVipResourceRes? backgroundCard,
SCVipResourceRes? effectImage,
}) { }) {
_userId = userId; _userId = userId;
_sysOrigin = sysOrigin; _sysOrigin = sysOrigin;
@ -125,10 +129,14 @@ class SCVipStatusRes {
_startAt = startAt; _startAt = startAt;
_expireAt = expireAt; _expireAt = expireAt;
_badge = badge; _badge = badge;
_longBadge = longBadge;
_shortBadge = shortBadge;
_avatarFrame = avatarFrame; _avatarFrame = avatarFrame;
_entryEffect = entryEffect; _entryEffect = entryEffect;
_chatBubble = chatBubble; _chatBubble = chatBubble;
_floatPicture = floatPicture; _floatPicture = floatPicture;
_backgroundCard = backgroundCard;
_effectImage = effectImage;
} }
SCVipStatusRes.fromJson(dynamic json) { SCVipStatusRes.fromJson(dynamic json) {
@ -144,10 +152,14 @@ class SCVipStatusRes {
_startAt = _stringValue(map['startAt']); _startAt = _stringValue(map['startAt']);
_expireAt = _stringValue(map['expireAt']); _expireAt = _stringValue(map['expireAt']);
_badge = _resourceValue(map['badge']); _badge = _resourceValue(map['badge']);
_longBadge = _resourceValue(map['longBadge']) ?? _badge;
_shortBadge = _resourceValue(map['shortBadge']);
_avatarFrame = _resourceValue(map['avatarFrame']); _avatarFrame = _resourceValue(map['avatarFrame']);
_entryEffect = _resourceValue(map['entryEffect']); _entryEffect = _resourceValue(map['entryEffect']);
_chatBubble = _resourceValue(map['chatBubble']); _chatBubble = _resourceValue(map['chatBubble']);
_floatPicture = _resourceValue(map['floatPicture']); _floatPicture = _resourceValue(map['floatPicture']);
_backgroundCard = _resourceValue(map['backgroundCard']);
_effectImage = _resourceValue(map['effectImage']);
} }
String? _userId; String? _userId;
@ -160,10 +172,14 @@ class SCVipStatusRes {
String? _startAt; String? _startAt;
String? _expireAt; String? _expireAt;
SCVipResourceRes? _badge; SCVipResourceRes? _badge;
SCVipResourceRes? _longBadge;
SCVipResourceRes? _shortBadge;
SCVipResourceRes? _avatarFrame; SCVipResourceRes? _avatarFrame;
SCVipResourceRes? _entryEffect; SCVipResourceRes? _entryEffect;
SCVipResourceRes? _chatBubble; SCVipResourceRes? _chatBubble;
SCVipResourceRes? _floatPicture; SCVipResourceRes? _floatPicture;
SCVipResourceRes? _backgroundCard;
SCVipResourceRes? _effectImage;
String? get userId => _userId; String? get userId => _userId;
@ -183,7 +199,11 @@ class SCVipStatusRes {
String? get expireAt => _expireAt; String? get expireAt => _expireAt;
SCVipResourceRes? get badge => _badge; SCVipResourceRes? get badge => longBadge;
SCVipResourceRes? get longBadge => _longBadge ?? _badge;
SCVipResourceRes? get shortBadge => _shortBadge;
SCVipResourceRes? get avatarFrame => _avatarFrame; SCVipResourceRes? get avatarFrame => _avatarFrame;
@ -193,6 +213,10 @@ class SCVipStatusRes {
SCVipResourceRes? get floatPicture => _floatPicture; SCVipResourceRes? get floatPicture => _floatPicture;
SCVipResourceRes? get backgroundCard => _backgroundCard;
SCVipResourceRes? get effectImage => _effectImage;
bool get isActive => _active == true && (_level ?? 0) > 0; bool get isActive => _active == true && (_level ?? 0) > 0;
int get levelInt => (_level ?? 0).toInt(); int get levelInt => (_level ?? 0).toInt();
@ -216,11 +240,17 @@ class SCVipStatusRes {
map['displayName'] = _displayName; map['displayName'] = _displayName;
map['startAt'] = _startAt; map['startAt'] = _startAt;
map['expireAt'] = _expireAt; map['expireAt'] = _expireAt;
if (_badge != null) map['badge'] = _badge?.toJson(); if (badge != null) map['badge'] = badge?.toJson();
if (longBadge != null) map['longBadge'] = longBadge?.toJson();
if (_shortBadge != null) map['shortBadge'] = _shortBadge?.toJson();
if (_avatarFrame != null) map['avatarFrame'] = _avatarFrame?.toJson(); if (_avatarFrame != null) map['avatarFrame'] = _avatarFrame?.toJson();
if (_entryEffect != null) map['entryEffect'] = _entryEffect?.toJson(); if (_entryEffect != null) map['entryEffect'] = _entryEffect?.toJson();
if (_chatBubble != null) map['chatBubble'] = _chatBubble?.toJson(); if (_chatBubble != null) map['chatBubble'] = _chatBubble?.toJson();
if (_floatPicture != null) map['floatPicture'] = _floatPicture?.toJson(); if (_floatPicture != null) map['floatPicture'] = _floatPicture?.toJson();
if (_backgroundCard != null) {
map['backgroundCard'] = _backgroundCard?.toJson();
}
if (_effectImage != null) map['effectImage'] = _effectImage?.toJson();
return map; return map;
} }
} }
@ -234,10 +264,14 @@ class SCVipLevelConfigRes {
num? durationDays, num? durationDays,
num? priceGold, num? priceGold,
SCVipResourceRes? badge, SCVipResourceRes? badge,
SCVipResourceRes? longBadge,
SCVipResourceRes? shortBadge,
SCVipResourceRes? avatarFrame, SCVipResourceRes? avatarFrame,
SCVipResourceRes? entryEffect, SCVipResourceRes? entryEffect,
SCVipResourceRes? chatBubble, SCVipResourceRes? chatBubble,
SCVipResourceRes? floatPicture, SCVipResourceRes? floatPicture,
SCVipResourceRes? backgroundCard,
SCVipResourceRes? effectImage,
bool? canPurchase, bool? canPurchase,
num? payableGold, num? payableGold,
}) { }) {
@ -248,10 +282,14 @@ class SCVipLevelConfigRes {
_durationDays = durationDays; _durationDays = durationDays;
_priceGold = priceGold; _priceGold = priceGold;
_badge = badge; _badge = badge;
_longBadge = longBadge;
_shortBadge = shortBadge;
_avatarFrame = avatarFrame; _avatarFrame = avatarFrame;
_entryEffect = entryEffect; _entryEffect = entryEffect;
_chatBubble = chatBubble; _chatBubble = chatBubble;
_floatPicture = floatPicture; _floatPicture = floatPicture;
_backgroundCard = backgroundCard;
_effectImage = effectImage;
_canPurchase = canPurchase; _canPurchase = canPurchase;
_payableGold = payableGold; _payableGold = payableGold;
} }
@ -266,10 +304,14 @@ class SCVipLevelConfigRes {
_durationDays = _numValue(map['durationDays']); _durationDays = _numValue(map['durationDays']);
_priceGold = _numValue(map['priceGold']); _priceGold = _numValue(map['priceGold']);
_badge = _resourceValue(map['badge']); _badge = _resourceValue(map['badge']);
_longBadge = _resourceValue(map['longBadge']) ?? _badge;
_shortBadge = _resourceValue(map['shortBadge']);
_avatarFrame = _resourceValue(map['avatarFrame']); _avatarFrame = _resourceValue(map['avatarFrame']);
_entryEffect = _resourceValue(map['entryEffect']); _entryEffect = _resourceValue(map['entryEffect']);
_chatBubble = _resourceValue(map['chatBubble']); _chatBubble = _resourceValue(map['chatBubble']);
_floatPicture = _resourceValue(map['floatPicture']); _floatPicture = _resourceValue(map['floatPicture']);
_backgroundCard = _resourceValue(map['backgroundCard']);
_effectImage = _resourceValue(map['effectImage']);
_canPurchase = _boolValue(map['canPurchase']); _canPurchase = _boolValue(map['canPurchase']);
_payableGold = _numValue(map['payableGold']); _payableGold = _numValue(map['payableGold']);
} }
@ -281,10 +323,14 @@ class SCVipLevelConfigRes {
num? _durationDays; num? _durationDays;
num? _priceGold; num? _priceGold;
SCVipResourceRes? _badge; SCVipResourceRes? _badge;
SCVipResourceRes? _longBadge;
SCVipResourceRes? _shortBadge;
SCVipResourceRes? _avatarFrame; SCVipResourceRes? _avatarFrame;
SCVipResourceRes? _entryEffect; SCVipResourceRes? _entryEffect;
SCVipResourceRes? _chatBubble; SCVipResourceRes? _chatBubble;
SCVipResourceRes? _floatPicture; SCVipResourceRes? _floatPicture;
SCVipResourceRes? _backgroundCard;
SCVipResourceRes? _effectImage;
bool? _canPurchase; bool? _canPurchase;
num? _payableGold; num? _payableGold;
@ -300,7 +346,11 @@ class SCVipLevelConfigRes {
num? get priceGold => _priceGold; num? get priceGold => _priceGold;
SCVipResourceRes? get badge => _badge; SCVipResourceRes? get badge => longBadge;
SCVipResourceRes? get longBadge => _longBadge ?? _badge;
SCVipResourceRes? get shortBadge => _shortBadge;
SCVipResourceRes? get avatarFrame => _avatarFrame; SCVipResourceRes? get avatarFrame => _avatarFrame;
@ -310,6 +360,10 @@ class SCVipLevelConfigRes {
SCVipResourceRes? get floatPicture => _floatPicture; SCVipResourceRes? get floatPicture => _floatPicture;
SCVipResourceRes? get backgroundCard => _backgroundCard;
SCVipResourceRes? get effectImage => _effectImage;
bool? get canPurchase => _canPurchase; bool? get canPurchase => _canPurchase;
num? get payableGold => _payableGold; num? get payableGold => _payableGold;
@ -332,11 +386,17 @@ class SCVipLevelConfigRes {
map['enabled'] = _enabled; map['enabled'] = _enabled;
map['durationDays'] = _durationDays; map['durationDays'] = _durationDays;
map['priceGold'] = _priceGold; map['priceGold'] = _priceGold;
if (_badge != null) map['badge'] = _badge?.toJson(); if (badge != null) map['badge'] = badge?.toJson();
if (longBadge != null) map['longBadge'] = longBadge?.toJson();
if (_shortBadge != null) map['shortBadge'] = _shortBadge?.toJson();
if (_avatarFrame != null) map['avatarFrame'] = _avatarFrame?.toJson(); if (_avatarFrame != null) map['avatarFrame'] = _avatarFrame?.toJson();
if (_entryEffect != null) map['entryEffect'] = _entryEffect?.toJson(); if (_entryEffect != null) map['entryEffect'] = _entryEffect?.toJson();
if (_chatBubble != null) map['chatBubble'] = _chatBubble?.toJson(); if (_chatBubble != null) map['chatBubble'] = _chatBubble?.toJson();
if (_floatPicture != null) map['floatPicture'] = _floatPicture?.toJson(); if (_floatPicture != null) map['floatPicture'] = _floatPicture?.toJson();
if (_backgroundCard != null) {
map['backgroundCard'] = _backgroundCard?.toJson();
}
if (_effectImage != null) map['effectImage'] = _effectImage?.toJson();
map['canPurchase'] = _canPurchase; map['canPurchase'] = _canPurchase;
map['payableGold'] = _payableGold; map['payableGold'] = _payableGold;
return map; return map;

View File

@ -1,3 +1,5 @@
// ignore_for_file: constant_identifier_names
enum SCPropsType { enum SCPropsType {
AVATAR_FRAME, AVATAR_FRAME,
FLOAT_PICTURE, FLOAT_PICTURE,
@ -8,6 +10,7 @@ enum SCPropsType {
THEME, THEME,
BADGE, BADGE,
NOBLE_VIP, NOBLE_VIP,
VIP_EFFECT_IMAGE,
FRAGMENTS, FRAGMENTS,
RED_PACKET, RED_PACKET,
} }

View File

@ -57,15 +57,18 @@ Widget head({
final String headdressCoverUrl = headdressCover?.trim() ?? ""; final String headdressCoverUrl = headdressCover?.trim() ?? "";
final String headdressExtension = final String headdressExtension =
SCPathUtils.getFileExtension(headdressUrl).toLowerCase(); SCPathUtils.getFileExtension(headdressUrl).toLowerCase();
final bool hasLocalVirtualHeaddress = _isLocalAssetsVirtualUrl(headdressUrl);
final bool hasPictureHeaddress = final bool hasPictureHeaddress =
headdressUrl.isNotEmpty && SCPathUtils.fileTypeIsPic2(headdressUrl); headdressUrl.isNotEmpty && SCPathUtils.fileTypeIsPic2(headdressUrl);
final bool hasAnimatedHeaddress = final bool hasAnimatedHeaddress =
headdressExtension == ".svga" || headdressExtension == ".mp4"; headdressExtension == ".svga" || headdressExtension == ".mp4";
final bool shouldShowAnimatedHeaddress = final bool shouldShowAnimatedHeaddress =
hasAnimatedHeaddress && SCGlobalConfig.allowsHighCostAnimations; hasAnimatedHeaddress &&
SCGlobalConfig.allowsHighCostAnimations &&
!hasLocalVirtualHeaddress;
final bool shouldShowStaticHeaddressFallback = final bool shouldShowStaticHeaddressFallback =
hasAnimatedHeaddress && hasAnimatedHeaddress &&
!SCGlobalConfig.allowsHighCostAnimations && (!SCGlobalConfig.allowsHighCostAnimations || hasLocalVirtualHeaddress) &&
headdressCoverUrl.isNotEmpty; headdressCoverUrl.isNotEmpty;
final bool hasHeaddress = final bool hasHeaddress =
headdressUrl.isNotEmpty || shouldShowStaticHeaddressFallback; headdressUrl.isNotEmpty || shouldShowStaticHeaddressFallback;
@ -205,6 +208,11 @@ Widget head({
); );
} }
bool _isLocalAssetsVirtualUrl(String value) {
final uri = Uri.tryParse(value.trim());
return uri?.host.toLowerCase() == "local.assets";
}
Widget netImage({ Widget netImage({
required String url, required String url,
String? defaultImg, String? defaultImg,

View File

@ -23,6 +23,8 @@ class SCDataCardResourceView extends StatelessWidget {
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.loop = true, this.loop = true,
this.fallback, this.fallback,
this.showCoverFallback = true,
this.allowCoverAsResource = true,
}); });
final PropsResources? resource; final PropsResources? resource;
@ -33,6 +35,8 @@ class SCDataCardResourceView extends StatelessWidget {
final BoxFit fit; final BoxFit fit;
final bool loop; final bool loop;
final Widget? fallback; final Widget? fallback;
final bool showCoverFallback;
final bool allowCoverAsResource;
static bool isSupported(String? url) { static bool isSupported(String? url) {
return _isSvga(url) || return _isSvga(url) ||
@ -50,15 +54,22 @@ class SCDataCardResourceView extends StatelessWidget {
final resolvedHeight = _resolvedSize(height, constraints.maxHeight); final resolvedHeight = _resolvedSize(height, constraints.maxHeight);
final cover = _firstNonBlank([coverUrl, resource?.cover]); final cover = _firstNonBlank([coverUrl, resource?.cover]);
final source = _firstNonBlank([sourceUrl, resource?.sourceUrl]); final source = _firstNonBlank([sourceUrl, resource?.sourceUrl]);
final resourceUrl = source.isNotEmpty ? source : cover; final resourceUrl =
source.isNotEmpty || !allowCoverAsResource ? source : cover;
final emptyFallback = SizedBox(
width: resolvedWidth,
height: resolvedHeight,
);
final fallbackView = final fallbackView =
fallback ?? fallback ??
_buildImage( (showCoverFallback
cover, ? _buildImage(
width: resolvedWidth, cover,
height: resolvedHeight, width: resolvedWidth,
fit: fit, height: resolvedHeight,
); fit: fit,
)
: emptyFallback);
if (resourceUrl.isEmpty) { if (resourceUrl.isEmpty) {
return fallbackView; return fallbackView;
@ -330,10 +341,12 @@ class _LoopingDataCardVapVideo extends StatefulWidget {
class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> { class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
static const int _legacyRgbLeftAlphaRightMode = 3; static const int _legacyRgbLeftAlphaRightMode = 3;
static const int _longRunningLoopCount = 999999;
VapController? _controller; VapController? _controller;
int _playToken = 0; int _playToken = 0;
bool _failed = false; bool _failed = false;
bool _started = false;
@override @override
void didUpdateWidget(covariant _LoopingDataCardVapVideo oldWidget) { void didUpdateWidget(covariant _LoopingDataCardVapVideo oldWidget) {
@ -343,8 +356,11 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
oldWidget.fit != widget.fit) { oldWidget.fit != widget.fit) {
_playToken++; _playToken++;
_stop(); _stop();
if (_failed) { if (_failed || _started) {
setState(() => _failed = false); setState(() {
_failed = false;
_started = false;
});
} }
_play(); _play();
} }
@ -362,15 +378,32 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
_controller = controller; _controller = controller;
controller.setAnimListener( controller.setAnimListener(
onFailed: (_, __, ___) => _markFailed(), onFailed: (_, __, ___) => _markFailed(),
onVideoComplete: _handleVideoComplete,
onVideoStart: () { onVideoStart: () {
if (mounted && _failed) { if (mounted && (_failed || !_started)) {
setState(() => _failed = false); setState(() {
_failed = false;
_started = true;
});
} }
}, },
); );
_play(); _play();
} }
void _handleVideoComplete() {
if (!mounted || !widget.loop || _failed) {
return;
}
unawaited(
Future<void>.delayed(Duration.zero, () {
if (mounted && widget.loop && !_failed) {
return _play();
}
}),
);
}
Future<void> _play() async { Future<void> _play() async {
final controller = _controller; final controller = _controller;
final url = widget.url.trim(); final url = widget.url.trim();
@ -390,7 +423,7 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
await controller.setMute(true); await controller.setMute(true);
await controller.enableVersion1(true); await controller.enableVersion1(true);
await controller.setVideoMode(_legacyRgbLeftAlphaRightMode); await controller.setVideoMode(_legacyRgbLeftAlphaRightMode);
await controller.setLoop(widget.loop ? -1 : 0); await controller.setLoop(widget.loop ? _longRunningLoopCount : 0);
await controller.setScaleType(_scaleTypeForFit(widget.fit).key); await controller.setScaleType(_scaleTypeForFit(widget.fit).key);
if (!_isCurrent(token, controller)) { if (!_isCurrent(token, controller)) {
return; return;
@ -451,7 +484,10 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
if (!mounted || _failed) { if (!mounted || _failed) {
return; return;
} }
setState(() => _failed = true); setState(() {
_failed = true;
_started = false;
});
} }
ScaleType _scaleTypeForFit(BoxFit fit) { ScaleType _scaleTypeForFit(BoxFit fit) {
@ -482,7 +518,7 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
if (!_failed) if (!_failed)
VapView( VapView(
scaleType: _scaleTypeForFit(widget.fit), scaleType: _scaleTypeForFit(widget.fit),
repeat: widget.loop ? -1 : 0, repeat: widget.loop ? _longRunningLoopCount : 0,
mute: true, mute: true,
onViewCreated: _handleViewCreated, onViewCreated: _handleViewCreated,
), ),

View File

@ -209,6 +209,20 @@ class RoomRocketDialog extends StatelessWidget {
} }
} }
@visibleForTesting
String debugRoomRocketTitleText({required int level, required int round}) {
return _roomRocketTitleText(level: level, round: round);
}
String _roomRocketTitleText({required int level, required int round}) {
final normalizedLevel = level.clamp(1, 99).toInt();
final normalizedRound = round.clamp(1, 99).toInt();
if (normalizedRound <= 1) {
return 'Lv.$normalizedLevel Today';
}
return 'Lv.$normalizedLevel · Round $normalizedRound Today';
}
class RoomRocketRewardItem { class RoomRocketRewardItem {
const RoomRocketRewardItem({ const RoomRocketRewardItem({
this.asset, this.asset,
@ -449,7 +463,7 @@ class _RocketCanvas extends StatelessWidget {
top: s(120), top: s(120),
width: s(_designWidth), width: s(_designWidth),
child: _GradientText( child: _GradientText(
'Round $round Today', _roomRocketTitleText(level: selectedLevelItem.level, round: round),
fontSize: s(16), fontSize: s(16),
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
textAlign: TextAlign.center, textAlign: TextAlign.center,
@ -463,31 +477,11 @@ class _RocketCanvas extends StatelessWidget {
top: s(212), top: s(212),
width: s(40), width: s(40),
height: s(190), height: s(190),
child: ScrollConfiguration( child: _LevelRail(
behavior: const _NoGlowScrollBehavior(), scale: scale,
child: ListView.separated( levels: levels,
padding: EdgeInsets.zero, selectedLevel: selectedLevel,
primary: false, onLevelSelected: onLevelSelected,
physics: const BouncingScrollPhysics(),
itemCount: levels.length,
separatorBuilder: (_, __) => SizedBox(height: s(4)),
itemBuilder: (context, index) {
final item = levels[index];
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: item.enabled ? () => onLevelSelected?.call(index) : null,
child: SizedBox(
width: s(40),
height: s(52),
child: _LevelBadge(
scale: scale,
item: item,
selected: index == selectedLevel,
),
),
);
},
),
), ),
); );
} }
@ -1031,9 +1025,12 @@ class _LevelBadge extends StatelessWidget {
item.label, item.label,
fontSize: 10 * scale, fontSize: 10 * scale,
color: color:
item.enabled selected
? const Color(0xFFA5FFE3)
: item.enabled
? Colors.white ? Colors.white
: Colors.white.withValues(alpha: 0.52), : Colors.white.withValues(alpha: 0.52),
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
), ),
), ),
], ],
@ -1042,6 +1039,118 @@ class _LevelBadge extends StatelessWidget {
} }
} }
class _LevelRail extends StatefulWidget {
const _LevelRail({
required this.scale,
required this.levels,
required this.selectedLevel,
this.onLevelSelected,
});
final double scale;
final List<RoomRocketLevelItem> levels;
final int selectedLevel;
final ValueChanged<int>? onLevelSelected;
@override
State<_LevelRail> createState() => _LevelRailState();
}
class _LevelRailState extends State<_LevelRail> {
late final ScrollController _controller;
double get _itemHeight => 52 * widget.scale;
double get _separatorHeight => 4 * widget.scale;
double get _viewportHeight => 190 * widget.scale;
@override
void initState() {
super.initState();
_controller = ScrollController(
initialScrollOffset: _targetOffset(widget.selectedLevel),
);
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToSelected());
}
@override
void didUpdateWidget(covariant _LevelRail oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.selectedLevel != widget.selectedLevel ||
oldWidget.levels.length != widget.levels.length ||
oldWidget.scale != widget.scale) {
WidgetsBinding.instance.addPostFrameCallback((_) => _scrollToSelected());
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
double _targetOffset(int selectedLevel) {
if (widget.levels.isEmpty) {
return 0;
}
final index = selectedLevel.clamp(0, widget.levels.length - 1).toInt();
final totalHeight =
widget.levels.length * _itemHeight +
math.max(0, widget.levels.length - 1) * _separatorHeight;
final maxOffset = math.max(0, totalHeight - _viewportHeight).toDouble();
final centered =
index * (_itemHeight + _separatorHeight) -
(_viewportHeight - _itemHeight) / 2;
return centered.clamp(0, maxOffset).toDouble();
}
void _scrollToSelected() {
if (!mounted || !_controller.hasClients) {
return;
}
final target = _targetOffset(widget.selectedLevel);
if ((_controller.offset - target).abs() < 0.5) {
return;
}
_controller.animateTo(
target,
duration: const Duration(milliseconds: 220),
curve: Curves.easeOutCubic,
);
}
@override
Widget build(BuildContext context) {
return ScrollConfiguration(
behavior: const _NoGlowScrollBehavior(),
child: ListView.separated(
controller: _controller,
padding: EdgeInsets.zero,
primary: false,
physics: const BouncingScrollPhysics(),
itemCount: widget.levels.length,
separatorBuilder: (_, __) => SizedBox(height: _separatorHeight),
itemBuilder: (context, index) {
final item = widget.levels[index];
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap:
item.enabled ? () => widget.onLevelSelected?.call(index) : null,
child: SizedBox(
width: 40 * widget.scale,
height: _itemHeight,
child: _LevelBadge(
scale: widget.scale,
item: item,
selected: index == widget.selectedLevel,
),
),
);
},
),
);
}
}
class _CrewAvatar extends StatelessWidget { class _CrewAvatar extends StatelessWidget {
const _CrewAvatar({ const _CrewAvatar({
required this.scale, required this.scale,
@ -1399,28 +1508,45 @@ class _LevelBadgePainter extends CustomPainter {
size, size,
const Size(40, 52), const Size(40, 52),
); );
if (selected) {
canvas.drawPath(
path,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = size.width / 7
..maskFilter = MaskFilter.blur(BlurStyle.normal, size.width / 12)
..color = const Color(0xFF56FFD0).withValues(alpha: 0.38),
);
}
canvas.drawPath( canvas.drawPath(
path, path,
Paint() Paint()
..shader = const LinearGradient( ..shader = LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [Color(0xFF077C60), Color(0xFF08251E)], colors:
selected
? const [Color(0xFF10DFA8), Color(0xFF07392E)]
: const [Color(0xFF077C60), Color(0xFF08251E)],
).createShader(Offset.zero & size), ).createShader(Offset.zero & size),
); );
canvas.drawPath( canvas.drawPath(
path, path,
Paint() Paint()
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = selected ? size.width / 22 : size.width / 40 ..strokeWidth = selected ? size.width / 14 : size.width / 40
..shader = LinearGradient( ..shader = LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: [
enabled selected
? Colors.white
: enabled
? const Color(0xFFB2FBCC) ? const Color(0xFFB2FBCC)
: const Color(0xFFB2FBCC).withValues(alpha: 0.42), : const Color(0xFFB2FBCC).withValues(alpha: 0.42),
const Color(0xFFB2FBCC).withValues(alpha: 0), selected
? const Color(0xFF56FFD0)
: const Color(0xFFB2FBCC).withValues(alpha: 0),
], ],
).createShader(Offset.zero & size), ).createShader(Offset.zero & size),
); );
@ -1554,12 +1680,14 @@ class _RocketText extends StatelessWidget {
required this.fontSize, required this.fontSize,
required this.color, required this.color,
this.textAlign = TextAlign.center, this.textAlign = TextAlign.center,
this.fontWeight = FontWeight.w400,
}); });
final String text; final String text;
final double fontSize; final double fontSize;
final Color color; final Color color;
final TextAlign textAlign; final TextAlign textAlign;
final FontWeight fontWeight;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -1572,6 +1700,7 @@ class _RocketText extends StatelessWidget {
style: TextStyle( style: TextStyle(
color: color, color: color,
fontSize: fontSize, fontSize: fontSize,
fontWeight: fontWeight,
height: 1, height: 1,
decoration: TextDecoration.none, decoration: TextDecoration.none,
), ),

View File

@ -34,19 +34,24 @@ class RoomRocketFloatingEntry extends StatelessWidget {
return Selector<RtcProvider, SCRoomRocketStatusRes?>( return Selector<RtcProvider, SCRoomRocketStatusRes?>(
selector: (_, provider) => provider.roomRocketStatus, selector: (_, provider) => provider.roomRocketStatus,
builder: (context, status, child) { builder: (context, status, child) {
if (status == null || !status.isEnabled) {
return const SizedBox.shrink();
}
final level = _currentEntryLevel(status); final level = _currentEntryLevel(status);
if (level == null || level.rocketIconUrl.trim().isEmpty) {
return const SizedBox.shrink();
}
final percent = _entryPercent(status); final percent = _entryPercent(status);
final shouldShake = final shouldShake =
(status?.shake ?? false) || (status.shake ?? false) || percent > level.shakeThresholdPercent;
(level != null && percent > level.shakeThresholdPercent);
return SCDebounceWidget( return SCDebounceWidget(
onTap: () => _showRocketDialog(context, status), onTap: () => _showRocketDialog(context, status),
child: _RoomRocketEntryButton( child: _RoomRocketEntryButton(
iconUrl: level?.rocketIconUrl ?? '', iconUrl: level.rocketIconUrl,
progressUrl: level?.progressBarUrl ?? '', progressUrl: level.progressBarUrl,
progressPercent: percent, progressPercent: percent,
shake: shouldShake, shake: shouldShake,
loading: status == null, loading: false,
), ),
); );
}, },
@ -108,6 +113,55 @@ class RoomRocketFloatingEntry extends StatelessWidget {
} }
} }
@visibleForTesting
int debugResolveRoomRocketSelectedLevelIndex({
required List<RoomRocketLevelItem>? levelItems,
required SCRoomRocketStatusRes? status,
required int selectedLevel,
required bool levelTouched,
}) {
return _resolveRoomRocketSelectedLevelIndex(
levelItems: levelItems,
status: status,
selectedLevel: selectedLevel,
levelTouched: levelTouched,
);
}
int _resolveRoomRocketSelectedLevelIndex({
required List<RoomRocketLevelItem>? levelItems,
required SCRoomRocketStatusRes? status,
required int selectedLevel,
required bool levelTouched,
}) {
final items = levelItems ?? const <RoomRocketLevelItem>[];
final maxIndex = math.max(0, (items.isEmpty ? 6 : items.length) - 1);
if (levelTouched) {
return selectedLevel.clamp(0, maxIndex).toInt();
}
final currentLevel = _roomRocketLevelNumberFromStatus(status);
final matchedIndex = items.indexWhere((item) => item.level == currentLevel);
if (matchedIndex >= 0) {
return matchedIndex;
}
return (currentLevel - 1).clamp(0, maxIndex).toInt();
}
int _roomRocketLevelNumberFromStatus(SCRoomRocketStatusRes? status) {
return (status?.currentLevel ?? status?.level ?? 1).clamp(1, 99).toInt();
}
int _roomRocketLevelNumberForIndex(
List<RoomRocketLevelItem>? levelItems,
int index,
) {
final items = levelItems ?? const <RoomRocketLevelItem>[];
if (index >= 0 && index < items.length) {
return items[index].level.clamp(1, 99).toInt();
}
return (index + 1).clamp(1, 99).toInt();
}
class _RoomRocketEntryButton extends StatefulWidget { class _RoomRocketEntryButton extends StatefulWidget {
const _RoomRocketEntryButton({ const _RoomRocketEntryButton({
required this.iconUrl, required this.iconUrl,
@ -369,9 +423,16 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
super.initState(); super.initState();
_status = widget.initialStatus; _status = widget.initialStatus;
_selectedLevel = _levelIndexFromStatus(widget.initialStatus); _selectedLevel = _levelIndexFromStatus(widget.initialStatus);
_cacheRewardGroupsFromStatus(widget.initialStatus, _selectedLevel + 1); final initialLevelItems = RoomRocketApiMapper.levelsFromStatus(
widget.initialStatus,
);
final initialLevelNumber = _levelNumberForIndex(
initialLevelItems,
_selectedLevel,
);
_cacheRewardGroupsFromStatus(widget.initialStatus, initialLevelNumber);
unawaited(_load()); unawaited(_load());
unawaited(_loadRewardForLevel(_selectedLevel + 1)); unawaited(_loadRewardForLevel(initialLevelNumber));
unawaited(_loadConfigs()); unawaited(_loadConfigs());
unawaited(_loadRule()); unawaited(_loadRule());
} }
@ -387,16 +448,29 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
if (!mounted) { if (!mounted) {
return; return;
} }
if (!res.isEnabled) {
try {
Provider.of<RtcProvider>(
context,
listen: false,
).updateRoomRocketConfigurationStatus(res);
} catch (_) {}
widget.onClose?.call();
return;
}
_debugStatusAssets(res); _debugStatusAssets(res);
final levelItems = RoomRocketApiMapper.levelsFromStatus(res);
late final int selectedLevelNumber;
setState(() { setState(() {
_status = res; _status = res;
if (!_levelTouched) { if (!_levelTouched) {
_selectedLevel = _levelIndexFromStatus(res); _selectedLevel = _levelIndexFromStatus(res);
} }
_cacheRewardGroupsFromStatus(res, _selectedLevel + 1); selectedLevelNumber = _levelNumberForIndex(levelItems, _selectedLevel);
_cacheRewardGroupsFromStatus(res, selectedLevelNumber);
}); });
unawaited(_loadCrewForLevel(_selectedLevel + 1)); unawaited(_loadCrewForLevel(selectedLevelNumber));
unawaited(_loadRewardForLevel(_selectedLevel + 1)); unawaited(_loadRewardForLevel(selectedLevelNumber));
try { try {
Provider.of<RtcProvider>( Provider.of<RtcProvider>(
context, context,
@ -430,13 +504,29 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
if (!mounted) { if (!mounted) {
return; return;
} }
if (!contents.enabled) {
setState(() {
_levelContents = contents;
_configRewardGroupsByLevel = {};
});
_rocketUiDebug('rocket level contents disabled');
return;
}
final groups = RoomRocketApiMapper.rewardGroupsByLevelFromLevelContents( final groups = RoomRocketApiMapper.rewardGroupsByLevelFromLevelContents(
contents, contents,
); );
final levelItems = RoomRocketApiMapper.levelsFromLevelContents(contents);
late final int selectedLevelNumber;
setState(() { setState(() {
_levelContents = contents; _levelContents = contents;
_configRewardGroupsByLevel = groups; _configRewardGroupsByLevel = groups;
if (!_levelTouched) {
_selectedLevel = _resolvedSelectedLevelIndex(levelItems, _status);
}
selectedLevelNumber = _levelNumberForIndex(levelItems, _selectedLevel);
}); });
unawaited(_loadCrewForLevel(selectedLevelNumber));
unawaited(_loadRewardForLevel(selectedLevelNumber));
_rocketUiDebug( _rocketUiDebug(
'rocket level contents loaded levels=${contents.levels.length} ' 'rocket level contents loaded levels=${contents.levels.length} '
'rewardLevels=${groups.keys.toList()}', 'rewardLevels=${groups.keys.toList()}',
@ -471,8 +561,11 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final status = _status; final status = _status;
final selectedLevel = _selectedLevel.clamp(0, 5).toInt(); final levelItems =
final selectedLevelNumber = selectedLevel + 1; RoomRocketApiMapper.levelsFromLevelContents(_levelContents) ??
RoomRocketApiMapper.levelsFromStatus(status);
final selectedLevel = _resolvedSelectedLevelIndex(levelItems, status);
final selectedLevelNumber = _levelNumberForIndex(levelItems, selectedLevel);
final currentLevelNumber = _levelNumberFromStatus(status); final currentLevelNumber = _levelNumberFromStatus(status);
final statusRewardGroups = RoomRocketApiMapper.rewardGroupsFromStatus( final statusRewardGroups = RoomRocketApiMapper.rewardGroupsFromStatus(
status, status,
@ -487,9 +580,6 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
: rewardGroupsFromStatus ?? : rewardGroupsFromStatus ??
configRewardGroups ?? configRewardGroups ??
const RoomRocketRewardGroups(); const RoomRocketRewardGroups();
final levelItems =
RoomRocketApiMapper.levelsFromLevelContents(_levelContents) ??
RoomRocketApiMapper.levelsFromStatus(status);
return RoomRocketDialog( return RoomRocketDialog(
status: status, status: status,
levels: levelItems, levels: levelItems,
@ -536,13 +626,32 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
} }
int _levelIndexFromStatus(SCRoomRocketStatusRes? status) { int _levelIndexFromStatus(SCRoomRocketStatusRes? status) {
return ((status?.currentLevel ?? status?.level ?? 1) - 1) return _resolveRoomRocketSelectedLevelIndex(
.clamp(0, 5) levelItems: RoomRocketApiMapper.levelsFromStatus(status),
.toInt(); status: status,
selectedLevel: _selectedLevel,
levelTouched: false,
);
} }
int _levelNumberFromStatus(SCRoomRocketStatusRes? status) { int _levelNumberFromStatus(SCRoomRocketStatusRes? status) {
return (status?.currentLevel ?? status?.level ?? 1).clamp(1, 99).toInt(); return _roomRocketLevelNumberFromStatus(status);
}
int _resolvedSelectedLevelIndex(
List<RoomRocketLevelItem>? levelItems,
SCRoomRocketStatusRes? status,
) {
return _resolveRoomRocketSelectedLevelIndex(
levelItems: levelItems,
status: status,
selectedLevel: _selectedLevel,
levelTouched: _levelTouched,
);
}
int _levelNumberForIndex(List<RoomRocketLevelItem>? levelItems, int index) {
return _roomRocketLevelNumberForIndex(levelItems, index);
} }
void _cacheRewardGroupsFromStatus(SCRoomRocketStatusRes? status, int level) { void _cacheRewardGroupsFromStatus(SCRoomRocketStatusRes? status, int level) {
@ -576,13 +685,17 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
void _onLevelSelected(int index) { void _onLevelSelected(int index) {
final selectedIndex = index.clamp(0, 5).toInt(); final selectedIndex = index.clamp(0, 5).toInt();
_rocketUiDebug('select rocket level=${selectedIndex + 1}'); final levelItems =
RoomRocketApiMapper.levelsFromLevelContents(_levelContents) ??
RoomRocketApiMapper.levelsFromStatus(_status);
final selectedLevelNumber = _levelNumberForIndex(levelItems, selectedIndex);
_rocketUiDebug('select rocket level=$selectedLevelNumber');
setState(() { setState(() {
_levelTouched = true; _levelTouched = true;
_selectedLevel = selectedIndex; _selectedLevel = selectedIndex;
}); });
unawaited(_loadRewardForLevel(selectedIndex + 1)); unawaited(_loadRewardForLevel(selectedLevelNumber));
unawaited(_loadCrewForLevel(selectedIndex + 1)); unawaited(_loadCrewForLevel(selectedLevelNumber));
} }
Future<void> _loadRewardForLevel(int level) async { Future<void> _loadRewardForLevel(int level) async {
@ -801,60 +914,75 @@ class _RoomRocketRecordDialogLoaderState
@override @override
void initState() { void initState() {
super.initState(); super.initState();
unawaited(_load(reset: true)); unawaited(_loadAllRecords());
} }
Future<void> _load({bool reset = false}) async { Future<void> _loadAllRecords() async {
if (_loadingMore || (!reset && (!_hasMore || _loading))) { if (_loadingMore) {
return; return;
} }
final requestCursor = reset ? 1 : _cursor;
setState(() { setState(() {
if (reset) { _loading = true;
_loading = true; _loadingMore = false;
} else { _hasMore = true;
_loadingMore = true; _cursor = 1;
} _rawRecords.clear();
_records = null;
}); });
var requestCursor = 1;
final allRecords = <SCRoomRocketRewardRecordRes>[];
try { try {
_rocketUiDebug( while (mounted) {
'request reward-records roomId=${widget.roomId} ' _rocketUiDebug(
'cursor=$requestCursor limit=$_rocketPageSize', 'request reward-records roomId=${widget.roomId} '
); 'cursor=$requestCursor limit=$_rocketPageSize',
final res = await SCChatRoomRepository().roomRocketRewardRecords( );
widget.roomId, final res = await SCChatRoomRepository().roomRocketRewardRecords(
cursor: requestCursor, widget.roomId,
limit: _rocketPageSize, cursor: requestCursor,
); limit: _rocketPageSize,
);
if (!mounted) {
return;
}
final nextRecords = res.records;
allRecords.addAll(nextRecords);
_rocketUiDebug(
'reward-records loaded cursor=$requestCursor '
'count=${nextRecords.length} total=${allRecords.length}',
);
if (nextRecords.length < _rocketPageSize) {
break;
}
requestCursor += 1;
}
if (!mounted) { if (!mounted) {
return; return;
} }
final nextRecords = res.records;
setState(() { setState(() {
if (reset) { _rawRecords
_rawRecords ..clear()
..clear() ..addAll(allRecords);
..addAll(nextRecords);
} else {
_rawRecords.addAll(nextRecords);
}
_records = RoomRocketApiMapper.recordItemsFromRecords(_rawRecords); _records = RoomRocketApiMapper.recordItemsFromRecords(_rawRecords);
_hasMore = nextRecords.length >= _rocketPageSize; _hasMore = false;
if (_hasMore) { _cursor = requestCursor;
_cursor = requestCursor + 1;
}
_loading = false; _loading = false;
_loadingMore = false; _loadingMore = false;
}); });
_rocketUiDebug( _rocketUiDebug(
'reward-records loaded cursor=$requestCursor ' 'reward-records all loaded total=${_rawRecords.length} '
'count=${nextRecords.length} total=${_rawRecords.length} ' 'lastCursor=$_cursor',
'hasMore=$_hasMore nextCursor=$_cursor',
); );
} catch (error) { } catch (error) {
_rocketUiDebug('reward-records request failed: $error'); _rocketUiDebug('reward-records request failed: $error');
if (mounted) { if (mounted) {
setState(() { setState(() {
if (allRecords.isNotEmpty) {
_rawRecords
..clear()
..addAll(allRecords);
_records = RoomRocketApiMapper.recordItemsFromRecords(_rawRecords);
}
_loading = false; _loading = false;
_loadingMore = false; _loadingMore = false;
_hasMore = false; _hasMore = false;
@ -871,7 +999,7 @@ class _RoomRocketRecordDialogLoaderState
loadingText: _loading ? 'Loading data......' : '', loadingText: _loading ? 'Loading data......' : '',
isLoadingMore: _loadingMore, isLoadingMore: _loadingMore,
hasMore: _hasMore, hasMore: _hasMore,
onLoadMore: () => unawaited(_load()), onLoadMore: null,
limitText: 'Show only records from the last 35 days', limitText: 'Show only records from the last 35 days',
onClose: widget.onClose, onClose: widget.onClose,
); );

View File

@ -11,17 +11,21 @@ import 'package:yumi/ui_kit/components/custom_cached_image.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart'; import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
const Duration _pagMemoryTtl = Duration(minutes: 5); const Duration _pagMemoryTtl = Duration(minutes: 5);
const Size _roomRocketFallbackPagRawSize = Size(750, 1624);
const Rect _roomRocketTopAvatarRawRect = Rect.fromLTWH(291, 164, 168, 168);
class RoomRocketPagEffectOverlay extends StatefulWidget { class RoomRocketPagEffectOverlay extends StatefulWidget {
const RoomRocketPagEffectOverlay({ const RoomRocketPagEffectOverlay({
super.key, super.key,
required this.resource, required this.resource,
this.topAvatarUrl, this.topAvatarUrl,
this.topAvatarUrlListenable,
this.onCompleted, this.onCompleted,
}); });
final String resource; final String resource;
final String? topAvatarUrl; final String? topAvatarUrl;
final ValueListenable<String?>? topAvatarUrlListenable;
final VoidCallback? onCompleted; final VoidCallback? onCompleted;
static bool isPag(String? resource) { static bool isPag(String? resource) {
@ -50,6 +54,7 @@ class RoomRocketPagEffectOverlay extends StatefulWidget {
class _RoomRocketPagEffectOverlayState class _RoomRocketPagEffectOverlayState
extends State<RoomRocketPagEffectOverlay> { extends State<RoomRocketPagEffectOverlay> {
bool _completed = false; bool _completed = false;
Size? _pagRawSize;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -65,10 +70,7 @@ class _RoomRocketPagEffectOverlayState
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
_RoomRocketPagTopAvatar( _buildTopAvatar(size),
avatarUrl: widget.topAvatarUrl,
canvasSize: size,
),
Positioned.fill( Positioned.fill(
child: _RoomRocketPagView( child: _RoomRocketPagView(
resource: resource, resource: resource,
@ -79,6 +81,7 @@ class _RoomRocketPagEffectOverlayState
displayScale: 1, displayScale: 1,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
memoryTtl: _pagMemoryTtl, memoryTtl: _pagMemoryTtl,
onReady: _updatePagRawSize,
onCompleted: _complete, onCompleted: _complete,
defaultBuilder: defaultBuilder:
(_) => const Center(child: SCRotatingDotsLoading()), (_) => const Center(child: SCRotatingDotsLoading()),
@ -99,16 +102,48 @@ class _RoomRocketPagEffectOverlayState
_completed = true; _completed = true;
widget.onCompleted?.call(); widget.onCompleted?.call();
} }
void _updatePagRawSize(Size rawSize) {
if (_pagRawSize == rawSize) {
return;
}
setState(() => _pagRawSize = rawSize);
}
Widget _buildTopAvatar(Size canvasSize) {
final listenable = widget.topAvatarUrlListenable;
if (listenable == null) {
return _RoomRocketPagTopAvatar(
avatarUrl: widget.topAvatarUrl,
canvasSize: canvasSize,
pagRawSize: _pagRawSize,
);
}
return ValueListenableBuilder<String?>(
valueListenable: listenable,
builder:
(context, avatarUrl, _) => _RoomRocketPagTopAvatar(
avatarUrl: _firstNonBlankPagAvatar([
avatarUrl,
widget.topAvatarUrl,
]),
canvasSize: canvasSize,
pagRawSize: _pagRawSize,
),
);
}
} }
class _RoomRocketPagTopAvatar extends StatelessWidget { class _RoomRocketPagTopAvatar extends StatelessWidget {
const _RoomRocketPagTopAvatar({ const _RoomRocketPagTopAvatar({
required this.avatarUrl, required this.avatarUrl,
required this.canvasSize, required this.canvasSize,
required this.pagRawSize,
}); });
final String? avatarUrl; final String? avatarUrl;
final Size canvasSize; final Size canvasSize;
final Size? pagRawSize;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -116,12 +151,13 @@ class _RoomRocketPagTopAvatar extends StatelessWidget {
if (url.isEmpty) { if (url.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final size = (canvasSize.width * 0.15).clamp(46.0, 66.0).toDouble(); final rect = _mapRoomRocketPagRawRectToCoverCanvas(
return Positioned( rawRect: _roomRocketTopAvatarRawRect,
left: (canvasSize.width - size) / 2, rawSize: pagRawSize ?? _roomRocketFallbackPagRawSize,
top: canvasSize.height * 0.098, canvasSize: canvasSize,
width: size, );
height: size, return Positioned.fromRect(
rect: rect,
child: ClipOval( child: ClipOval(
child: CustomCachedImage( child: CustomCachedImage(
imageUrl: url, imageUrl: url,
@ -200,6 +236,7 @@ class _RoomRocketPagView extends StatefulWidget {
required this.clipBehavior, required this.clipBehavior,
required this.memoryTtl, required this.memoryTtl,
this.underlayBuilder, this.underlayBuilder,
this.onReady,
this.onCompleted, this.onCompleted,
this.defaultBuilder, this.defaultBuilder,
}); });
@ -213,6 +250,7 @@ class _RoomRocketPagView extends StatefulWidget {
final Clip clipBehavior; final Clip clipBehavior;
final Duration memoryTtl; final Duration memoryTtl;
final WidgetBuilder? underlayBuilder; final WidgetBuilder? underlayBuilder;
final ValueChanged<Size>? onReady;
final VoidCallback? onCompleted; final VoidCallback? onCompleted;
final WidgetBuilder? defaultBuilder; final WidgetBuilder? defaultBuilder;
@ -414,6 +452,7 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
'viewSize=${widget.width?.toStringAsFixed(1)}x' 'viewSize=${widget.width?.toStringAsFixed(1)}x'
'${widget.height?.toStringAsFixed(1)} fit=${widget.fit.name}', '${widget.height?.toStringAsFixed(1)} fit=${widget.fit.name}',
); );
widget.onReady?.call(Size(rawWidth, rawHeight));
unawaited(_debugLayers(state, rawWidth, rawHeight)); unawaited(_debugLayers(state, rawWidth, rawHeight));
if (!_initialized) { if (!_initialized) {
setState(() => _initialized = true); setState(() => _initialized = true);
@ -587,6 +626,55 @@ Timer? _schedulePagMemoryReleaseTimer(
return Timer(memoryTtl, onRelease); return Timer(memoryTtl, onRelease);
} }
@visibleForTesting
Rect debugMapRoomRocketPagRawRectToCoverCanvas({
required Rect rawRect,
required Size rawSize,
required Size canvasSize,
}) {
return _mapRoomRocketPagRawRectToCoverCanvas(
rawRect: rawRect,
rawSize: rawSize,
canvasSize: canvasSize,
);
}
Rect _mapRoomRocketPagRawRectToCoverCanvas({
required Rect rawRect,
required Size rawSize,
required Size canvasSize,
}) {
if (rawSize.width <= 0 ||
rawSize.height <= 0 ||
canvasSize.width <= 0 ||
canvasSize.height <= 0) {
return Rect.zero;
}
final scaleX = canvasSize.width / rawSize.width;
final scaleY = canvasSize.height / rawSize.height;
final scale = scaleX > scaleY ? scaleX : scaleY;
final fittedWidth = rawSize.width * scale;
final fittedHeight = rawSize.height * scale;
final offsetX = (canvasSize.width - fittedWidth) / 2;
final offsetY = (canvasSize.height - fittedHeight) / 2;
return Rect.fromLTWH(
offsetX + rawRect.left * scale,
offsetY + rawRect.top * scale,
rawRect.width * scale,
rawRect.height * scale,
);
}
String? _firstNonBlankPagAvatar(Iterable<String?> values) {
for (final value in values) {
final text = value?.trim() ?? '';
if (text.isNotEmpty) {
return text;
}
}
return null;
}
@visibleForTesting @visibleForTesting
String debugNormalizeRoomRocketPagResource(String resource) { String debugNormalizeRoomRocketPagResource(String resource) {
return _normalizePagResource(resource); return _normalizePagResource(resource);

View File

@ -35,7 +35,7 @@ class RoomRocketRewardDialog extends StatelessWidget {
final emptyText = _translate( final emptyText = _translate(
localizations, localizations,
'roomRocketRewardEmptyText', 'roomRocketRewardEmptyText',
'No rewards earned. Better luck next time.', 'No reward',
); );
final contentDirection = Directionality.of(context); final contentDirection = Directionality.of(context);
final resolvedRewards = _normalizeRewards( final resolvedRewards = _normalizeRewards(
@ -257,7 +257,7 @@ class _RewardDialogCanvas extends StatelessWidget {
if (isEmpty) if (isEmpty)
_buildEmptyState() _buildEmptyState()
else ...[ else ...[
_buildRewardGrid(), rewards.isEmpty ? _buildRewardEmptyState() : _buildRewardGrid(),
_buildWinnerList(), _buildWinnerList(),
], ],
_buildLoadingText(), _buildLoadingText(),
@ -380,9 +380,6 @@ class _RewardDialogCanvas extends StatelessWidget {
} }
Widget _buildRewardGrid() { Widget _buildRewardGrid() {
if (rewards.isEmpty) {
return const SizedBox.shrink();
}
final columns = math.max(3, (rewards.length / 2).ceil()); final columns = math.max(3, (rewards.length / 2).ceil());
return Positioned( return Positioned(
left: s(28), left: s(28),
@ -425,6 +422,45 @@ class _RewardDialogCanvas extends StatelessWidget {
); );
} }
Widget _buildRewardEmptyState() {
return Positioned(
left: s(0),
top: s(268),
width: s(_designWidth),
height: s(178),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
_RewardDialogAssets.empty,
width: s(72),
height: s(72),
fit: BoxFit.contain,
),
SizedBox(height: s(10)),
SizedBox(
width: s(250),
child: Text(
emptyText,
textDirection: contentDirection,
textScaler: TextScaler.noScaling,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.72),
fontSize: s(13),
height: 1.25,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
),
],
),
);
}
Widget _buildWinnerList() { Widget _buildWinnerList() {
return Positioned( return Positioned(
left: s(12), left: s(12),

View File

@ -81,7 +81,7 @@ class _MsgItemState extends State<MsgItem> {
} }
/// ///
if (widget.msg.type == SCRoomMsgType.rocketRewardUser) { if (SCRoomMsgType.isRocketRewardUserType(widget.msg.type)) {
return _buildRocketRewardMsg(context); return _buildRocketRewardMsg(context);
} }
@ -692,10 +692,17 @@ class _MsgItemState extends State<MsgItem> {
/// ///
_buildRocketRewardMsg(BuildContext context) { _buildRocketRewardMsg(BuildContext context) {
List<InlineSpan> spans = []; List<InlineSpan> spans = [];
final nickname = Msg._firstNonBlankString([
widget.msg.user?.userNickname,
widget.msg.user?.account,
widget.msg.user?.id,
SCAppLocalizations.of(context)?.member,
'Member',
]);
spans.add( spans.add(
TextSpan( TextSpan(
text: "${widget.msg.user?.userNickname} ", text: "$nickname ",
style: TextStyle( style: TextStyle(
fontSize: sp(12), fontSize: sp(12),
color: SocialChatTheme.primaryColor, color: SocialChatTheme.primaryColor,
@ -1995,6 +2002,118 @@ class Msg {
gift = gift =
json['gift'] != null ? SocialChatGiftRes.fromJson(json['gift']) : null; json['gift'] != null ? SocialChatGiftRes.fromJson(json['gift']) : null;
_hydrateRocketRewardPayload(json);
}
void _hydrateRocketRewardPayload(dynamic json) {
if (!SCRoomMsgType.isRocketRewardUserType(type)) {
return;
}
final root = _stringKeyMap(json);
final payload = _stringKeyMap(root['data']);
final source = payload.isEmpty ? root : payload;
groupId = _firstNonBlankString([
groupId,
source['groupId'],
source['roomAccount'],
source['room_account'],
source['roomId'],
source['room_id'],
]);
role = _firstNonBlankString([
role,
source['rewardType'],
source['reward_type'],
]);
rocketIconUrl = _firstNonBlankString([
rocketIconUrl,
source['rocketIconUrl'],
source['rocket_icon_url'],
source['rocketIcon'],
source['rewardCover'],
source['reward_cover'],
]);
msg = _firstNonBlankString([
msg,
source['rewardCover'],
source['reward_cover'],
source['rewardIconUrl'],
source['reward_icon_url'],
source['cover'],
rocketIconUrl,
]);
user ??= _rocketRewardUserProfile(source);
}
static SocialChatUserProfile? _rocketRewardUserProfile(
Map<String, dynamic> source,
) {
final userId = _firstNonBlankString([
source['userId'],
source['user_id'],
source['rewardUserId'],
source['reward_user_id'],
source['receiverUserId'],
source['receiver_user_id'],
source['targetUserId'],
source['target_user_id'],
]);
final nickname = _firstNonBlankString([
source['userNickname'],
source['user_nickname'],
source['nickname'],
source['receiverNickname'],
source['receiver_nickname'],
source['account'],
userId,
]);
final account = _firstNonBlankString([
source['account'],
source['userAccount'],
source['user_account'],
source['actualAccount'],
source['actual_account'],
userId,
]);
final avatar = _firstNonBlankString([
source['userAvatar'],
source['user_avatar'],
source['avatar'],
source['receiverAvatar'],
source['receiver_avatar'],
]);
if (userId.isEmpty &&
nickname.isEmpty &&
account.isEmpty &&
avatar.isEmpty) {
return null;
}
return SocialChatUserProfile(
id: userId,
account: account,
userNickname: nickname,
userAvatar: avatar,
);
}
static Map<String, dynamic> _stringKeyMap(dynamic value) {
if (value is Map<String, dynamic>) {
return value;
}
if (value is Map) {
return value.map((key, item) => MapEntry(key.toString(), item));
}
return const <String, dynamic>{};
}
static String _firstNonBlankString(Iterable<dynamic> values) {
for (final value in values) {
final text = value?.toString().trim() ?? '';
if (text.isNotEmpty && text.toLowerCase() != 'null') {
return text;
}
}
return '';
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {

View File

@ -49,6 +49,10 @@ class RoomUserInfoCard extends StatefulWidget {
class _RoomUserInfoCardState extends State<RoomUserInfoCard> { class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
static const String _leaveMicActionIconAsset = static const String _leaveMicActionIconAsset =
"sc_images/room/sc_icon_room_user_card_leave_mic.png"; "sc_images/room/sc_icon_room_user_card_leave_mic.png";
static const String _defaultProfileCardBackgroundAsset =
"sc_images/room/sc_bg_room_user_profile_card_default.png";
static const double _profileSheetHeightFactor = 0.4;
static const double _dataCardBackgroundTranslateFactor = 0.8;
SocialChatUserProfileManager? userProvider; SocialChatUserProfileManager? userProvider;
String roomId = ""; String roomId = "";
@ -85,7 +89,8 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sheetHeight = MediaQuery.of(context).size.height * 0.6; final screenHeight = MediaQuery.of(context).size.height;
final defaultSheetHeight = screenHeight * _profileSheetHeightFactor;
return Consumer<SocialChatUserProfileManager>( return Consumer<SocialChatUserProfileManager>(
builder: (context, ref, child) { builder: (context, ref, child) {
@ -100,14 +105,18 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
final canLeaveMic = currentMicIndex > -1; final canLeaveMic = currentMicIndex > -1;
if (ref.userCardInfo == null) { if (ref.userCardInfo == null) {
return _buildLoadingSheet(sheetHeight); return _buildLoadingSheet(defaultSheetHeight);
} }
final dataCard = _activeDataCard(ref.userCardInfo);
return _buildProfileSheet( return _buildProfileSheet(
context: context, context: context,
ref: ref, ref: ref,
rtcProvider: rtcProvider, rtcProvider: rtcProvider,
sheetHeight: sheetHeight, sheetHeight: defaultSheetHeight,
dataCardBackgroundHeight: screenHeight,
dataCard: dataCard,
isSelf: isSelf, isSelf: isSelf,
currentMicIndex: currentMicIndex, currentMicIndex: currentMicIndex,
canLeaveMic: canLeaveMic, canLeaveMic: canLeaveMic,
@ -123,19 +132,21 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
height: sheetHeight, height: sheetHeight,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
child: DecoratedBox( child: Stack(
decoration: _sheetDecoration(), children: [
child: Center( Positioned.fill(child: _buildDefaultSheetBackground()),
child: Container( Center(
width: 55.w, child: Container(
height: 55.w, width: 55.w,
decoration: BoxDecoration( height: 55.w,
color: Colors.black26, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.w), color: Colors.black26,
borderRadius: BorderRadius.circular(8.w),
),
child: CupertinoActivityIndicator(color: Colors.white24),
), ),
child: CupertinoActivityIndicator(color: Colors.white24),
), ),
), ],
), ),
), ),
); );
@ -146,6 +157,8 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
required SocialChatUserProfileManager ref, required SocialChatUserProfileManager ref,
required RtcProvider rtcProvider, required RtcProvider rtcProvider,
required double sheetHeight, required double sheetHeight,
required double dataCardBackgroundHeight,
required PropsResources? dataCard,
required bool isSelf, required bool isSelf,
required num currentMicIndex, required num currentMicIndex,
required bool canLeaveMic, required bool canLeaveMic,
@ -153,138 +166,215 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
}) { }) {
final bottomPadding = MediaQuery.of(context).padding.bottom; final bottomPadding = MediaQuery.of(context).padding.bottom;
final profile = ref.userCardInfo?.userProfile; final profile = ref.userCardInfo?.userProfile;
final dataCard = _activeDataCard(ref.userCardInfo);
final canShowReport = currentUserId != widget.userId; final canShowReport = currentUserId != widget.userId;
final canShowSetting = final canShowSetting =
canShowReport && canShowReport &&
ref.userCardInfo?.roomRole != SCRoomRolesType.HOMEOWNER.name && ref.userCardInfo?.roomRole != SCRoomRolesType.HOMEOWNER.name &&
(rtcProvider.isFz() || rtcProvider.isGL()); (rtcProvider.isFz() || rtcProvider.isGL());
final profileContent = _buildProfileContentLayer(
context: context,
ref: ref,
rtcProvider: rtcProvider,
profile: profile,
bottomPadding: bottomPadding,
isSelf: isSelf,
canLeaveMic: canLeaveMic,
currentMicIndex: currentMicIndex,
canShowReport: canShowReport,
canShowSetting: canShowSetting,
);
if (dataCard == null) {
return SizedBox(
width: double.infinity,
height: sheetHeight,
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
child: Stack(
children: [
Positioned.fill(child: _buildDefaultSheetBackground()),
Positioned.fill(child: profileContent),
],
),
),
);
}
return _buildDataCardFrame(
sheetHeight: sheetHeight,
backgroundHeight: dataCardBackgroundHeight,
dataCard: dataCard,
child: profileContent,
);
}
Widget _buildDataCardFrame({
required double sheetHeight,
required double backgroundHeight,
required PropsResources dataCard,
required Widget child,
}) {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
height: sheetHeight, height: sheetHeight,
child: ClipRRect( child: Stack(
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), clipBehavior: Clip.none,
child: Stack( children: [
children: [ Positioned.fill(
Positioned.fill(child: _buildSheetBackground(dataCard)), child: ClipRRect(
Positioned.fill( borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
child: SingleChildScrollView( child: DecoratedBox(decoration: _sheetDecoration()),
physics: const ClampingScrollPhysics(),
padding: EdgeInsets.fromLTRB(
18.w,
16.w,
18.w,
bottomPadding + 10.w,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildProfileHeader(context, profile, ref),
SizedBox(height: 6.w),
_buildInfoChips(context, profile),
SizedBox(height: 9.w),
_buildBadgeStrip(
profile?.id ?? widget.userId,
ref,
profile,
),
SizedBox(height: 10.w),
_buildActions(
context: context,
ref: ref,
rtcProvider: rtcProvider,
isSelf: isSelf,
canLeaveMic: canLeaveMic,
currentMicIndex: currentMicIndex,
),
],
),
),
), ),
if (canShowReport) ),
Positioned( Positioned(
top: 14.w, left: 0,
right: 16.w, right: 0,
child: _buildHeaderIconButton( top:
iconAsset: "sc_images/room/sc_icon_user_card_report.png", -(backgroundHeight - sheetHeight) +
onTap: () { sheetHeight * _dataCardBackgroundTranslateFactor,
final route = height: backgroundHeight,
"${SCMainRoute.report}?type=user&tageId=${widget.userId}"; child: IgnorePointer(child: _buildSheetBackground(dataCard)),
Navigator.of(context).pop(); ),
SCNavigatorUtils.push( Positioned.fill(
navigatorKey.currentState?.context ?? context, child: ClipRRect(
route, borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
replace: false, child: child,
); ),
}, ),
), ],
),
if (canShowSetting)
Positioned(
top: 14.w,
right: 52.w,
child: _buildHeaderIconButton(
iconAsset:
"sc_images/room/sc_icon_room_user_card_setting.png",
onTap: () {
if (userProvider?.userCardInfo == null) {
return;
}
Navigator.of(context).pop();
showBottomInBottomDialog(
navigatorKey.currentState?.context ?? context,
RoomUserCardSetting(
roomId: roomId,
userCardInfo: userProvider?.userCardInfo?.copyWith(),
),
);
},
),
),
],
),
), ),
); );
} }
Widget _buildProfileContentLayer({
required BuildContext context,
required SocialChatUserProfileManager ref,
required RtcProvider rtcProvider,
required SocialChatUserProfile? profile,
required double bottomPadding,
required bool isSelf,
required bool canLeaveMic,
required num currentMicIndex,
required bool canShowReport,
required bool canShowSetting,
}) {
return Stack(
children: [
Positioned.fill(
child: LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: Alignment.topCenter,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.topCenter,
child: SizedBox(
width: constraints.maxWidth,
child: Padding(
padding: EdgeInsets.fromLTRB(
18.w,
10.w,
18.w,
bottomPadding > 6.w ? bottomPadding : 6.w,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildProfileHeader(context, profile, ref),
SizedBox(height: 6.w),
_buildBadgeStrip(
profile?.id ?? widget.userId,
ref,
profile,
),
SizedBox(height: 8.w),
_buildActions(
context: context,
ref: ref,
rtcProvider: rtcProvider,
isSelf: isSelf,
canLeaveMic: canLeaveMic,
currentMicIndex: currentMicIndex,
),
],
),
),
),
),
);
},
),
),
if (canShowReport)
Positioned(
top: 14.w,
right: 16.w,
child: _buildHeaderIconButton(
iconAsset: "sc_images/room/sc_icon_user_card_report.png",
onTap: () {
final route =
"${SCMainRoute.report}?type=user&tageId=${widget.userId}";
Navigator.of(context).pop();
SCNavigatorUtils.push(
navigatorKey.currentState?.context ?? context,
route,
replace: false,
);
},
),
),
if (canShowSetting)
Positioned(
top: 14.w,
right: 52.w,
child: _buildHeaderIconButton(
iconAsset: "sc_images/room/sc_icon_room_user_card_setting.png",
onTap: () {
if (userProvider?.userCardInfo == null) {
return;
}
Navigator.of(context).pop();
showBottomInBottomDialog(
navigatorKey.currentState?.context ?? context,
RoomUserCardSetting(
roomId: roomId,
userCardInfo: userProvider?.userCardInfo?.copyWith(),
),
);
},
),
),
],
);
}
BoxDecoration _sheetDecoration() { BoxDecoration _sheetDecoration() {
return const BoxDecoration( return const BoxDecoration(color: Color(0xff061F1E));
gradient: LinearGradient( }
begin: Alignment.topCenter,
end: Alignment.bottomCenter, Widget _buildDefaultSheetBackground() {
colors: [ return DecoratedBox(
Color(0xff18F2B1), decoration: _sheetDecoration(),
Color(0xffE4FFF6), child: SizedBox.expand(
Color(0xffF7FFFC), child: Image.asset(
Color(0xffFFFFFF), _defaultProfileCardBackgroundAsset,
], fit: BoxFit.cover,
),
), ),
); );
} }
Widget _buildSheetBackground(PropsResources? dataCard) { Widget _buildSheetBackground(PropsResources? dataCard) {
if (dataCard == null) { if (dataCard == null) {
return DecoratedBox(decoration: _sheetDecoration()); return _buildDefaultSheetBackground();
} }
return Stack(
fit: StackFit.expand, return SCDataCardResourceView(
children: [ resource: dataCard,
SCDataCardResourceView(resource: dataCard, fit: BoxFit.cover), fit: BoxFit.cover,
DecoratedBox( showCoverFallback: false,
decoration: BoxDecoration( allowCoverAsResource: false,
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withValues(alpha: 0.08),
Colors.white.withValues(alpha: 0.22),
Colors.white.withValues(alpha: 0.58),
],
),
),
),
],
); );
} }
@ -325,8 +415,8 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
onTap: () => _openProfile(context), onTap: () => _openProfile(context),
child: head( child: head(
url: profile?.userAvatar ?? "", url: profile?.userAvatar ?? "",
width: 70.w, width: 105.w,
border: Border.all(color: Colors.white, width: 2), border: Border.all(color: Colors.white, width: 3),
headdress: profile?.getHeaddress()?.sourceUrl, headdress: profile?.getHeaddress()?.sourceUrl,
headdressCover: profile?.getHeaddress()?.cover, headdressCover: profile?.getHeaddress()?.cover,
), ),
@ -347,7 +437,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
maxWidth: 116.w, maxWidth: 116.w,
profile?.userNickname ?? "", profile?.userNickname ?? "",
fontSize: 18.sp, fontSize: 18.sp,
textColor: Colors.black, textColor: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
type: "", type: "",
needScroll: (profile?.userNickname?.characters.length ?? 0) > 12, needScroll: (profile?.userNickname?.characters.length ?? 0) > 12,
@ -381,6 +471,22 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
], ],
), ),
SizedBox(height: 2.w), SizedBox(height: 2.w),
_buildSpecialIdMetaRow(context, profile),
],
);
}
Widget _buildSpecialIdMetaRow(
BuildContext context,
SocialChatUserProfile? profile,
) {
final flagUrl = _countryFlagUrl(context, profile);
final hasSex = profile?.userSex != null;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
SCSpecialIdBadge( SCSpecialIdBadge(
idText: profile?.getID() ?? "", idText: profile?.getID() ?? "",
showAnimated: profile?.hasSpecialId() ?? false, showAnimated: profile?.hasSpecialId() ?? false,
@ -392,106 +498,62 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
showAnimatedGradientText: showAnimatedGradientText:
profile?.shouldShowColoredSpecialIdText() ?? false, profile?.shouldShowColoredSpecialIdText() ?? false,
animationTextStyle: TextStyle( animationTextStyle: TextStyle(
color: Colors.black, color: Colors.white,
fontSize: 12.sp, fontSize: 12.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
normalTextStyle: TextStyle( normalTextStyle: TextStyle(
color: Colors.black, color: Colors.white,
fontSize: 12.sp, fontSize: 12.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
animationFit: BoxFit.contain, animationFit: BoxFit.contain,
), ),
if (flagUrl.isNotEmpty) ...[
SizedBox(width: 8.w),
_buildCountryFlag(flagUrl),
],
if (hasSex) ...[SizedBox(width: 7.w), _buildSexIcon(profile)],
], ],
); );
} }
Widget _buildInfoChips(BuildContext context, SocialChatUserProfile? profile) { String _countryFlagUrl(BuildContext context, SocialChatUserProfile? profile) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildCountryChip(context, profile),
SizedBox(width: 7.w),
_buildSexAgeChip(profile),
],
);
}
Widget _buildCountryChip(
BuildContext context,
SocialChatUserProfile? profile,
) {
final countryName = profile?.countryName ?? ""; final countryName = profile?.countryName ?? "";
final flagUrl = try {
Provider.of<SCAppGeneralManager>( return Provider.of<SCAppGeneralManager>(
context, context,
listen: false, listen: false,
).findCountryByName(countryName)?.nationalFlag ?? ).findCountryByName(countryName)?.nationalFlag ??
""; "";
} catch (_) {
return "";
}
}
return Container( Widget _buildCountryFlag(String flagUrl) {
constraints: BoxConstraints(maxWidth: 145.w), final trimmedUrl = flagUrl.trim();
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), if (trimmedUrl.isEmpty) {
decoration: BoxDecoration( return const SizedBox.shrink();
color: Colors.white.withValues(alpha: 0.55), }
borderRadius: BorderRadius.circular(16.w),
border: Border.all( return ClipRRect(
color: const Color(0xff18F2B1).withValues(alpha: 0.18), borderRadius: BorderRadius.circular(2.w),
), child: netImage(
), url: trimmedUrl,
child: Row( width: 22.w,
mainAxisSize: MainAxisSize.min, height: 15.w,
children: [ fit: BoxFit.cover,
netImage( noDefaultImg: true,
url: flagUrl,
borderRadius: BorderRadius.all(Radius.circular(3.w)),
width: 19.w,
height: 14.w,
),
SizedBox(width: 5.w),
Flexible(
child: text(
countryName,
maxLines: 1,
textColor: Colors.black,
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
],
), ),
); );
} }
Widget _buildSexAgeChip(SocialChatUserProfile? profile) { Widget _buildSexIcon(SocialChatUserProfile? profile) {
return Container( if (profile?.userSex == null) {
height: 26.w, return const SizedBox.shrink();
padding: EdgeInsets.symmetric(horizontal: 10.w), }
decoration: BoxDecoration( return xb(profile?.userSex, height: 15.w, color: null);
image: DecorationImage(
image: AssetImage(
profile?.userSex == 0
? "sc_images/login/sc_icon_sex_woman_bg.png"
: "sc_images/login/sc_icon_sex_man_bg.png",
),
fit: BoxFit.fill,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
xb(profile?.userSex),
SizedBox(width: 3.w),
text(
"${profile?.age ?? 0}",
textColor: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
],
),
);
} }
Widget _buildHeaderLongBadgeStrip( Widget _buildHeaderLongBadgeStrip(
@ -547,7 +609,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
return wearBadges; return wearBadges;
} }
return ref.userCardInfo?.useBadge return ref.userCardInfo?.useBadge
?.map(_roomUseBadgeToWearBadge) ?.map<WearBadge>(_roomUseBadgeToWearBadge)
.where(_hasBadgeUrl) .where(_hasBadgeUrl)
.toList() ?? .toList() ??
const <WearBadge>[]; const <WearBadge>[];
@ -662,7 +724,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
label, label,
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
textColor: const Color(0xff666666), textColor: Colors.white,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 12.sp, fontSize: 12.sp,
lineHeight: 1.1, lineHeight: 1.1,
@ -717,7 +779,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
: SCAppLocalizations.of(context)!.follow, : SCAppLocalizations.of(context)!.follow,
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
textColor: const Color(0xff666666), textColor: Colors.white,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 12.sp, fontSize: 12.sp,
lineHeight: 1.1, lineHeight: 1.1,

View File

@ -1,5 +1,3 @@
import 'package:extended_image/extended_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -70,11 +68,6 @@ class _PropsStoreChatboxDetailDialogState
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox(width: 15.w), SizedBox(width: 15.w),
text(
widget.res.propsResources?.name ?? "",
textColor: Colors.white,
fontSize: 12.sp,
),
Spacer(), Spacer(),
Image.asset( Image.asset(
"sc_images/general/sc_icon_jb.png", "sc_images/general/sc_icon_jb.png",

View File

@ -74,14 +74,7 @@ class _PropsStoreDataCardDetailDialogState
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox(width: 15.w), SizedBox(width: 15.w),
Expanded( const Spacer(),
child: text(
widget.res.propsResources?.name ?? "",
textColor: Colors.white,
fontSize: 12.sp,
maxLines: 1,
),
),
Image.asset( Image.asset(
"sc_images/general/sc_icon_jb.png", "sc_images/general/sc_icon_jb.png",
width: 22.w, width: 22.w,

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -77,11 +76,6 @@ class _PropsStoreHeaddressDetailDialogState
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox(width: 15.w), SizedBox(width: 15.w),
text(
widget.res.propsResources?.name ?? "",
textColor: Colors.white,
fontSize: 12.sp,
),
Spacer(), Spacer(),
Image.asset( Image.asset(
"sc_images/general/sc_icon_jb.png", "sc_images/general/sc_icon_jb.png",

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -16,7 +15,6 @@ import '../../../app/constants/sc_global_config.dart';
import '../../../app/routes/sc_fluro_navigator.dart'; import '../../../app/routes/sc_fluro_navigator.dart';
import '../../../shared/data_sources/models/enum/sc_currency_type.dart'; import '../../../shared/data_sources/models/enum/sc_currency_type.dart';
import '../../../shared/data_sources/models/enum/sc_props_type.dart'; import '../../../shared/data_sources/models/enum/sc_props_type.dart';
import '../../../shared/data_sources/sources/local/user_manager.dart';
import '../../../modules/wallet/wallet_route.dart'; import '../../../modules/wallet/wallet_route.dart';
import '../../../main.dart'; import '../../../main.dart';
import '../../components/sc_debounce_widget.dart'; import '../../components/sc_debounce_widget.dart';
@ -76,11 +74,6 @@ class _PropsStoreMountainsDetailDialogState
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox(width: 15.w), SizedBox(width: 15.w),
text(
widget.res.propsResources?.name ?? "",
textColor: Colors.white,
fontSize: 12.sp,
),
Spacer(), Spacer(),
Image.asset( Image.asset(
"sc_images/general/sc_icon_jb.png", "sc_images/general/sc_icon_jb.png",

View File

@ -1,4 +1,3 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -76,11 +75,6 @@ class _PropsStoreThemeDetailDialogState
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
SizedBox(width: 15.w), SizedBox(width: 15.w),
text(
widget.res.propsResources?.name ?? "",
textColor: Colors.white,
fontSize: 12.sp,
),
Spacer(), Spacer(),
Image.asset( Image.asset(
"sc_images/general/sc_icon_jb.png", "sc_images/general/sc_icon_jb.png",

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 95 KiB

View File

@ -0,0 +1,45 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart';
void main() {
test('rocket dialog title displays current level before daily round', () {
expect(debugRoomRocketTitleText(level: 3, round: 1), 'Lv.3 Today');
expect(
debugRoomRocketTitleText(level: 2, round: 4),
'Lv.2 · Round 4 Today',
);
});
test(
'rocket level rail follows current level unless user selected manually',
() {
final status = SCRoomRocketStatusRes.fromJson({'currentLevel': 4});
const levels = [
RoomRocketLevelItem(label: 'Lv.2', level: 2),
RoomRocketLevelItem(label: 'Lv.4', level: 4),
RoomRocketLevelItem(label: 'Lv.5', level: 5),
];
expect(
debugResolveRoomRocketSelectedLevelIndex(
levelItems: levels,
status: status,
selectedLevel: 0,
levelTouched: false,
),
1,
);
expect(
debugResolveRoomRocketSelectedLevelIndex(
levelItems: levels,
status: status,
selectedLevel: 2,
levelTouched: true,
),
2,
);
},
);
}

View File

@ -0,0 +1,43 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yumi/app/constants/sc_room_msg_type.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
void main() {
test('hydrates rocket reward user from nested im payload', () {
final msg = Msg.fromJson({
'type': SCRoomMsgType.voiceRoomRocketRewardUser,
'data': {
'roomId': '9001',
'userId': '1001',
'userNickname': 'Winner',
'account': '223456',
'userAvatar': 'https://cdn.example.com/avatar.png',
'rewardType': 'GOLD',
'rewardCover': 'https://cdn.example.com/reward.png',
'rocketIconUrl': 'https://cdn.example.com/rocket.png',
},
});
expect(msg.type, SCRoomMsgType.voiceRoomRocketRewardUser);
expect(msg.groupId, '9001');
expect(msg.user?.id, '1001');
expect(msg.user?.userNickname, 'Winner');
expect(msg.user?.account, '223456');
expect(msg.user?.userAvatar, 'https://cdn.example.com/avatar.png');
expect(msg.role, 'GOLD');
expect(msg.msg, 'https://cdn.example.com/reward.png');
expect(msg.rocketIconUrl, 'https://cdn.example.com/rocket.png');
});
test('rocket reward user nickname falls back to account', () {
final msg = Msg.fromJson({
'type': SCRoomMsgType.voiceRoomRocketRewardUser,
'userId': '1002',
'account': '334455',
'rewardType': 'BADGE',
});
expect(msg.user?.userNickname, '334455');
expect(msg.user?.id, '1002');
});
}

View File

@ -1,4 +1,5 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:yumi/app/config/app_config.dart'; import 'package:yumi/app/config/app_config.dart';
import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart'; import 'package:yumi/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart';
@ -60,6 +61,19 @@ void main() {
timer?.cancel(); timer?.cancel();
}); });
test('maps top avatar frame from pag raw canvas using cover fit', () {
final rect = debugMapRoomRocketPagRawRectToCoverCanvas(
rawRect: const Rect.fromLTWH(291, 164, 168, 168),
rawSize: const Size(750, 1624),
canvasSize: const Size(392.7272727, 829.090909),
);
expect(rect.left, closeTo(152.38, 0.01));
expect(rect.top, closeTo(75.23, 0.01));
expect(rect.width, closeTo(87.97, 0.01));
expect(rect.height, closeTo(87.97, 0.01));
});
test('extracts launch pag urls from nested resource payloads', () { test('extracts launch pag urls from nested resource payloads', () {
final launch = RoomRocketLaunchBroadcastMessage.fromJson({ final launch = RoomRocketLaunchBroadcastMessage.fromJson({
'rocketAnimationResource': { 'rocketAnimationResource': {

View File

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart';
void main() { void main() {
@ -27,6 +30,30 @@ void main() {
expect(records.map((item) => item.rewardAmount), [5000, 5200]); expect(records.map((item) => item.rewardAmount), [5000, 5200]);
}); });
}); });
group('room rocket reward dialog', () {
testWidgets('shows reward empty state when room winners exist', (
tester,
) async {
await tester.pumpWidget(
const MaterialApp(
home: Scaffold(
body: RoomRocketRewardDialog(
rewards: <RoomRocketRewardItem>[],
winners: <RoomRocketRewardWinner>[
RoomRocketRewardWinner(nickname: 'test man'),
],
loadingText: '',
useMockFallback: false,
),
),
),
);
expect(find.text('No reward'), findsOneWidget);
expect(find.text('test man'), findsOneWidget);
});
});
} }
SCRoomRocketRewardRecordRes _rewardRecord({ SCRoomRocketRewardRecordRes _rewardRecord({

View File

@ -122,6 +122,37 @@ void main() {
expect(levels?.last.progressImageUrl, 'progress-2'); expect(levels?.last.progressImageUrl, 'progress-2');
}); });
test('parses disabled rocket status from go api', () {
final status = SCRoomRocketStatusRes.fromJson({
'configured': true,
'enabled': false,
'status': 'DISABLED',
'roomId': 'room-1',
'levels': [],
});
expect(status.configured, isTrue);
expect(status.enabled, isFalse);
expect(status.isEnabled, isFalse);
expect(status.status, 'DISABLED');
expect(RoomRocketApiMapper.levelsFromStatus(status), isNull);
});
test('treats unconfigured or disabled-status rocket as unavailable', () {
final unconfigured = SCRoomRocketStatusRes.fromJson({
'configured': false,
'enabled': true,
'status': 'ENABLED',
});
final disabledStatus = SCRoomRocketStatusRes.fromJson({
'configured': true,
'status': 'DISABLED',
});
expect(unconfigured.isEnabled, isFalse);
expect(disabledStatus.isEnabled, isFalse);
});
test( test(
'uses top-level reward preview for the response level instead of current level', 'uses top-level reward preview for the response level instead of current level',
() { () {