完善功能

This commit is contained in:
zhx 2026-05-16 06:48:47 +08:00
parent 8be5c4c334
commit 8bf3d679a7
26 changed files with 958 additions and 186 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": "الانتقال للرفع",
@ -665,8 +665,10 @@
"mountains": "مركبات", "mountains": "مركبات",
"buy": "شراء", "buy": "شراء",
"visitorList": "قائمة الزوار", "visitorList": "قائمة الزوار",
"spendCoinsToGainExperiencePoints": "اصرف العملات لكسب نقاط الخبرة", "spendCoinsToGainExperiencePoints": "اصرف العملات لكسب نقاط الخبرة",
"howToUpgrade": "كيف يمكن الترقية؟", "richLevelUpgradeAnswer": "دع ثروتك تغزو كل شيء",
"charmLevelUpgradeAnswer": "اجعل الآخرين يخضعون لسحرك",
"howToUpgrade": "كيف يمكن الترقية؟",
"higherLevelFancierAvatarFrame": "مستوى أعلى، شارات/إطار صور أفخم.", "higherLevelFancierAvatarFrame": "مستوى أعلى، شارات/إطار صور أفخم.",
"use": "استخدم", "use": "استخدم",
"expired": "منتهي", "expired": "منتهي",

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",

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",
@ -566,10 +566,12 @@
"obtain": "Al", "obtain": "Al",
"win2": "{1} Kazan", "win2": "{1} Kazan",
"backTheRoom": "Odaya Dön", "backTheRoom": "Odaya Dön",
"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",
"higherLevelFancierAvatarFrame": "Daha yüksek seviye, daha şık rozetler/profil çerçevesi", "richLevelUpgradeAnswer": "Servetin her şeyi fethetsin",
"charmLevelUpgradeAnswer": "Başkalarını cazibene boyun eğdir",
"higherLevelFancierAvatarFrame": "Daha yüksek seviye, daha şık rozetler/profil çerçevesi",
"all": "Tümü", "all": "Tümü",
"gift": "Hediye", "gift": "Hediye",
"chat": "Sohbet", "chat": "Sohbet",

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

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

View File

@ -110,18 +110,26 @@ class _UserLevelPageState extends State<UserLevelPage> {
], ],
), ),
SizedBox(height: 3.w), SizedBox(height: 3.w),
Stack( Stack(
children: [ children: [
Container( Container(
width: 185.w, width: 185.w,
height: 6.w, height: 6.w,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
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),
Row( Row(
children: [ children: [
@ -150,19 +158,7 @@ class _UserLevelPageState extends State<UserLevelPage> {
], ],
), ),
), ),
text( Image.asset("sc_images/level/sc_icon_user_level_center_bg_1.png"),
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"),
text( text(
SCAppLocalizations.of(context)!.howToUpgrade, SCAppLocalizations.of(context)!.howToUpgrade,
fontSize: 20.sp, fontSize: 20.sp,
@ -170,14 +166,15 @@ class _UserLevelPageState extends State<UserLevelPage> {
textColor: SocialChatTheme.primaryLight, textColor: SocialChatTheme.primaryLight,
), ),
SizedBox(height: 3), SizedBox(height: 3),
text( text(
SCAppLocalizations.of( SCAppLocalizations.of(context)!.charmLevelUpgradeAnswer,
context, fontSize: 12.sp,
)!.spendCoinsToGainExperiencePoints, fontWeight: FontWeight.w600,
fontSize: 12.sp, textColor: Colors.white,
fontWeight: FontWeight.w600, maxLines: 2,
textColor: Colors.white, maxWidth: 320.w,
), textAlign: TextAlign.center,
),
SizedBox(height: 25.w), SizedBox(height: 25.w),
GestureDetector( GestureDetector(
child: Container( child: Container(
@ -205,12 +202,15 @@ class _UserLevelPageState extends State<UserLevelPage> {
: Container(); : Container();
} }
double getLevelExperience(double width) { double getLevelExperience(double width) {
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;
return (thatExp / levelExp) * width; if (levelExp <= 0) {
} return 0;
}
return (thatExp / levelExp) * width;
}
num getLevelTotalExperience() { num getLevelTotalExperience() {
int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0"); int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0");

View File

@ -110,18 +110,26 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
], ],
), ),
SizedBox(height: 3.w), SizedBox(height: 3.w),
Stack( Stack(
children: [ children: [
Container( Container(
width: 185.w, width: 185.w,
height: 6.w, height: 6.w,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
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),
Row( Row(
children: [ children: [
@ -150,19 +158,7 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
], ],
), ),
), ),
text( Image.asset("sc_images/level/sc_icon_user_wealth_center_bg_1.png"),
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"),
text( text(
SCAppLocalizations.of(context)!.howToUpgrade, SCAppLocalizations.of(context)!.howToUpgrade,
fontSize: 20.sp, fontSize: 20.sp,
@ -170,14 +166,15 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
textColor: SocialChatTheme.primaryLight, textColor: SocialChatTheme.primaryLight,
), ),
SizedBox(height: 3), SizedBox(height: 3),
text( text(
SCAppLocalizations.of( SCAppLocalizations.of(context)!.richLevelUpgradeAnswer,
context, fontSize: 12.sp,
)!.spendCoinsToGainExperiencePoints, fontWeight: FontWeight.w600,
fontSize: 12.sp, textColor: Colors.white,
fontWeight: FontWeight.w600, maxLines: 2,
textColor: Colors.white, maxWidth: 320.w,
), textAlign: TextAlign.center,
),
SizedBox(height: 25.w), SizedBox(height: 25.w),
GestureDetector( GestureDetector(
child: Container( child: Container(
@ -205,12 +202,15 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
: Container(); : Container();
} }
double getLevelExperience(double width) { double getLevelExperience(double width) {
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;
return (thatExp / levelExp) * width; if (levelExp <= 0) {
} return 0;
}
return (thatExp / levelExp) * width;
}
num getLevelTotalExperience() { num getLevelTotalExperience() {
int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0"); int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0");

View File

@ -2826,19 +2826,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); _scheduleRoomRocketEntryReadyFallback(roomId);
_loadRoomRocketRewardPopups(roomId);
}) })
.catchError((e) {}); .catchError((e) {});
_loadRoomRocketRewardPopups(roomId);
_refreshRoomRedPacketListAfterEntry(); _refreshRoomRedPacketListAfterEntry();
fetchRoomTaskClaimableCount(); fetchRoomTaskClaimableCount();
@ -2894,6 +2894,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,
@ -2914,6 +2917,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
roomRocketStatus = res; roomRocketStatus = res;
notifyListeners(); notifyListeners();
if (!res.isEnabled) {
return;
}
_scheduleRoomRocketAssetPreload(resolvedRoomId, res); _scheduleRoomRocketAssetPreload(resolvedRoomId, res);
} }
@ -2926,6 +2932,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,

View File

@ -4,6 +4,7 @@ import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:extended_image/extended_image.dart'; import 'package:extended_image/extended_image.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_debouncer/flutter_debouncer.dart'; import 'package:flutter_debouncer/flutter_debouncer.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -143,6 +144,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
Duration(seconds: 2), Duration(seconds: 2),
Duration(seconds: 5), Duration(seconds: 5),
]; ];
static const Duration _roomRocketRewardAfterLaunchDelay = Duration(
milliseconds: 350,
);
static const List<Duration> _roomRocketRoomReadyRewardRetryDelays = [ static const List<Duration> _roomRocketRoomReadyRewardRetryDelays = [
Duration(milliseconds: 300), Duration(milliseconds: 300),
Duration(milliseconds: 900), Duration(milliseconds: 900),
@ -272,6 +276,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
final Map<String, Timer> _roomRocketLaunchEffectTimers = <String, Timer>{}; final Map<String, Timer> _roomRocketLaunchEffectTimers = <String, Timer>{};
final Map<String, int> _consumedRoomRocketLaunchEffectTimes = <String, int>{}; final Map<String, int> _consumedRoomRocketLaunchEffectTimes = <String, int>{};
final Set<String> _hiddenCurrentRoomRocketLaunchKeys = <String>{}; final Set<String> _hiddenCurrentRoomRocketLaunchKeys = <String>{};
final Map<String, int> _roomRocketRewardHoldUntilTimes = <String, int>{};
final Map<String, Timer> _roomRocketRewardHoldTimers = <String, Timer>{};
Timer? _roomRocketRewardPopupRetryTimer; Timer? _roomRocketRewardPopupRetryTimer;
Timer? _roomRocketRoomReadyRewardRetryTimer; Timer? _roomRocketRoomReadyRewardRetryTimer;
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken; int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
@ -1162,6 +1168,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
case SCRoomMsgType.roomRedPacketClaim: case SCRoomMsgType.roomRedPacketClaim:
return true; return true;
case SCRoomMsgType.rocketRewardUser: case SCRoomMsgType.rocketRewardUser:
case SCRoomMsgType.voiceRoomRocketRewardUser:
return true; return true;
case SCRoomMsgType.rocketLaunchNotice: case SCRoomMsgType.rocketLaunchNotice:
return true; return true;
@ -1195,7 +1202,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
_hasRoomRedPacketClaimMessage(msg)) { _hasRoomRedPacketClaimMessage(msg)) {
return; return;
} }
if (msg.type == SCRoomMsgType.rocketRewardUser && if (SCRoomMsgType.isRocketRewardUserType(msg.type) &&
_hasRoomRocketRewardMessage(msg)) { _hasRoomRocketRewardMessage(msg)) {
return; return;
} }
@ -1235,7 +1242,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
roomChatMsgList.removeAt(roomChatMsgList.length - 1); roomChatMsgList.removeAt(roomChatMsgList.length - 1);
} }
msgChatListener?.call(msg); msgChatListener?.call(msg);
} else if (msg.type == SCRoomMsgType.rocketRewardUser) { } else if (SCRoomMsgType.isRocketRewardUserType(msg.type)) {
roomChatMsgList.insert(0, msg); roomChatMsgList.insert(0, msg);
if (roomChatMsgList.length > 250) { if (roomChatMsgList.length > 250) {
roomChatMsgList.removeAt(roomChatMsgList.length - 1); roomChatMsgList.removeAt(roomChatMsgList.length - 1);
@ -1388,7 +1395,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
} }
return roomAllMsgList.any( return roomAllMsgList.any(
(item) => (item) =>
item.type == SCRoomMsgType.rocketRewardUser && SCRoomMsgType.isRocketRewardUserType(item.type) &&
_roomRocketRewardMessageKey(item) == key, _roomRocketRewardMessageKey(item) == key,
); );
} }
@ -2091,6 +2098,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
void _handleVoiceRoomRocketRewardPopup(dynamic payload) { void _handleVoiceRoomRocketRewardPopup(dynamic payload) {
final data = _broadcastPayloadMap(payload); final data = _broadcastPayloadMap(payload);
if (!_isCurrentUserInRoomRocketRewardPopup(data)) {
return;
}
final roomId = _payloadRoomId(data); final roomId = _payloadRoomId(data);
final currentRoomId = _currentVoiceRoomId(); final currentRoomId = _currentVoiceRoomId();
final resolvedRoomId = roomId.isEmpty ? currentRoomId : roomId; final resolvedRoomId = roomId.isEmpty ? currentRoomId : roomId;
@ -2119,6 +2129,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
); );
return; return;
} }
if (_isRoomRocketRewardPopupHeld(resolvedRoomId)) {
_scheduleRoomRocketRewardPopupAfterHold(
resolvedRoomId,
initialRoomRecords: initialRoomRecords,
);
return;
}
_pendingRoomRocketRewardPopups.remove(resolvedRoomId); _pendingRoomRocketRewardPopups.remove(resolvedRoomId);
_recentRoomRocketRewardProbeTimes.remove(resolvedRoomId); _recentRoomRocketRewardProbeTimes.remove(resolvedRoomId);
@ -2130,6 +2147,87 @@ class RealTimeMessagingManager extends ChangeNotifier {
); );
} }
bool _isCurrentUserInRoomRocketRewardPopup(Map<String, dynamic> payload) {
final userIds = _roomRocketRewardPopupUserIds(payload);
if (userIds == null) {
return true;
}
if (userIds.isEmpty) {
return false;
}
final profile = AccountStorage().getCurrentUser()?.userProfile;
final currentKeys = <String>{
profile?.id?.trim() ?? '',
profile?.account?.trim() ?? '',
}..removeWhere((item) => item.isEmpty);
if (currentKeys.isEmpty) {
return false;
}
return currentKeys.any(userIds.contains);
}
Set<String>? _roomRocketRewardPopupUserIds(Map<String, dynamic> payload) {
for (final key in const [
'userIds',
'user_ids',
'targetUserIds',
'target_user_ids',
'winnerUserIds',
'winner_user_ids',
'userId',
'user_id',
'winnerUserId',
'winner_user_id',
'rewardUserId',
'reward_user_id',
'receiverUserId',
'receiver_user_id',
'targetUserId',
'target_user_id',
'toUserId',
'to_user_id',
]) {
if (payload.containsKey(key)) {
return _payloadStringSet(payload[key]);
}
}
return null;
}
Set<String> _payloadStringSet(dynamic value) {
if (value == null) {
return <String>{};
}
if (value is Iterable && value is! String) {
return value
.map((item) => item?.toString().trim() ?? '')
.where((item) => item.isNotEmpty)
.toSet();
}
if (value is Map) {
return value.values
.map((item) => item?.toString().trim() ?? '')
.where((item) => item.isNotEmpty)
.toSet();
}
final raw = value.toString().trim();
if (raw.isEmpty) {
return <String>{};
}
if (raw.startsWith('[') || raw.startsWith('{')) {
try {
return _payloadStringSet(jsonDecode(raw));
} catch (_) {
return <String>{};
}
}
return raw
.split(',')
.map((item) => item.trim())
.where((item) => item.isNotEmpty)
.toSet();
}
void handleVoiceRoomReadyForRocketRewards(String roomId, {int attempt = 0}) { void handleVoiceRoomReadyForRocketRewards(String roomId, {int attempt = 0}) {
final normalizedRoomId = roomId.trim(); final normalizedRoomId = roomId.trim();
if (normalizedRoomId.isEmpty) { if (normalizedRoomId.isEmpty) {
@ -2166,6 +2264,15 @@ class RealTimeMessagingManager extends ChangeNotifier {
_recentRoomRocketRewardProbeTimes.remove(normalizedRoomId); _recentRoomRocketRewardProbeTimes.remove(normalizedRoomId);
_roomRocketRoomReadyRewardRetryTimer?.cancel(); _roomRocketRoomReadyRewardRetryTimer?.cancel();
_roomRocketRoomReadyRewardRetryTimer = null; _roomRocketRoomReadyRewardRetryTimer = null;
if (_isRoomRocketRewardPopupHeld(normalizedRoomId)) {
_scheduleRoomRocketRewardPopupAfterHold(
normalizedRoomId,
initialRoomRecords:
pending?.initialRoomRecords ??
const <SCRoomRocketRewardRecordRes>[],
);
return;
}
unawaited( unawaited(
_showRoomRocketRewardPopupDialog( _showRoomRocketRewardPopupDialog(
normalizedRoomId, normalizedRoomId,
@ -2225,6 +2332,113 @@ class RealTimeMessagingManager extends ChangeNotifier {
_recentRoomRocketRewardProbeTimes[normalizedRoomId] = now; _recentRoomRocketRewardProbeTimes[normalizedRoomId] = now;
} }
void _holdRoomRocketRewardPopupUntilLaunchEnds(
RoomRocketLaunchBroadcastMessage launch,
) {
if (!launch.isValid) {
return;
}
final normalizedRoomId = launch.roomId.trim();
if (normalizedRoomId.isEmpty) {
return;
}
final now = DateTime.now().millisecondsSinceEpoch;
final holdUntil =
now +
launch.displayDuration.inMilliseconds +
_roomRocketRewardAfterLaunchDelay.inMilliseconds;
final existing = _roomRocketRewardHoldUntilTimes[normalizedRoomId] ?? 0;
if (holdUntil > existing) {
_roomRocketRewardHoldUntilTimes[normalizedRoomId] = holdUntil;
}
}
bool _isRoomRocketRewardPopupHeld(String roomId) {
final normalizedRoomId = roomId.trim();
if (normalizedRoomId.isEmpty) {
return false;
}
final now = DateTime.now().millisecondsSinceEpoch;
final holdUntil = _roomRocketRewardHoldUntilTimes[normalizedRoomId] ?? 0;
if (holdUntil > now) {
return true;
}
if (holdUntil > 0) {
_roomRocketRewardHoldUntilTimes.remove(normalizedRoomId);
}
return _pendingRoomRocketCountdownLaunches.containsKey(normalizedRoomId);
}
int _roomRocketRewardHoldUntil(String roomId) {
final normalizedRoomId = roomId.trim();
if (normalizedRoomId.isEmpty) {
return 0;
}
final now = DateTime.now().millisecondsSinceEpoch;
var holdUntil = _roomRocketRewardHoldUntilTimes[normalizedRoomId] ?? 0;
final launch = _pendingRoomRocketCountdownLaunches[normalizedRoomId];
if (launch != null) {
final key = _roomRocketLaunchCountdownKeysByRoom[normalizedRoomId];
final launchDueAt =
key == null ? now : _roomRocketLaunchCountdownDueTimes[key] ?? now;
final countdownHoldUntil =
launchDueAt +
launch.displayDuration.inMilliseconds +
_roomRocketRewardAfterLaunchDelay.inMilliseconds;
if (countdownHoldUntil > holdUntil) {
holdUntil = countdownHoldUntil;
}
}
return holdUntil;
}
void _scheduleRoomRocketRewardPopupAfterHold(
String roomId, {
List<SCRoomRocketRewardRecordRes> initialRoomRecords =
const <SCRoomRocketRewardRecordRes>[],
}) {
final normalizedRoomId = roomId.trim();
if (normalizedRoomId.isEmpty) {
return;
}
_cachePendingRoomRocketRewardPopup(
normalizedRoomId,
initialRoomRecords: initialRoomRecords,
);
final now = DateTime.now().millisecondsSinceEpoch;
final holdUntil = _roomRocketRewardHoldUntil(normalizedRoomId);
final delayMs = holdUntil <= now ? 120 : holdUntil - now;
_roomRocketRewardHoldTimers[normalizedRoomId]?.cancel();
_roomRocketRewardHoldTimers[normalizedRoomId] = Timer(
Duration(milliseconds: delayMs),
() {
_roomRocketRewardHoldTimers.remove(normalizedRoomId);
if (_isRoomRocketRewardPopupHeld(normalizedRoomId)) {
_scheduleRoomRocketRewardPopupAfterHold(normalizedRoomId);
return;
}
if (!_isCurrentVisibleVoiceRoom(normalizedRoomId)) {
_scheduleRoomRocketReadyRewardRetry(normalizedRoomId, 0);
return;
}
final pending = _pendingRoomRocketRewardPopups.remove(normalizedRoomId);
_recentRoomRocketRewardProbeTimes.remove(normalizedRoomId);
unawaited(
_showRoomRocketRewardPopupDialog(
normalizedRoomId,
initialRoomRecords:
pending?.initialRoomRecords ??
const <SCRoomRocketRewardRecordRes>[],
),
);
},
);
_roomRocketRewardDebug(
'defer popup roomId=$normalizedRoomId delayMs=$delayMs '
'initialRoomRecords=${initialRoomRecords.length}',
);
}
void _pruneRoomRocketRewardFallbacks(int now) { void _pruneRoomRocketRewardFallbacks(int now) {
_pendingRoomRocketRewardPopups.removeWhere( _pendingRoomRocketRewardPopups.removeWhere(
(_, pending) => now - pending.receivedAt > _roomRocketRewardPendingTtlMs, (_, pending) => now - pending.receivedAt > _roomRocketRewardPendingTtlMs,
@ -2241,6 +2455,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
_hiddenCurrentRoomRocketLaunchKeys.clear(); _hiddenCurrentRoomRocketLaunchKeys.clear();
_roomRocketRoomReadyRewardRetryTimer?.cancel(); _roomRocketRoomReadyRewardRetryTimer?.cancel();
_roomRocketRoomReadyRewardRetryTimer = null; _roomRocketRoomReadyRewardRetryTimer = null;
for (final timer in _roomRocketRewardHoldTimers.values) {
timer.cancel();
}
_roomRocketRewardHoldTimers.clear();
_roomRocketRewardHoldUntilTimes.clear();
_clearRoomRocketLaunchCountdownState(); _clearRoomRocketLaunchCountdownState();
} }
@ -2322,15 +2541,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
final pending = _pendingRoomRocketRewardPopups.remove(launch.roomId); final pending = _pendingRoomRocketRewardPopups.remove(launch.roomId);
_recentRoomRocketRewardProbeTimes.remove(launch.roomId); _recentRoomRocketRewardProbeTimes.remove(launch.roomId);
_markRoomRocketLaunchEffectConsumed(launch); _markRoomRocketLaunchEffectConsumed(launch);
_holdRoomRocketRewardPopupUntilLaunchEnds(launch);
rtcProvider.handleRoomRocketLaunchBroadcast(launch); rtcProvider.handleRoomRocketLaunchBroadcast(launch);
if (shouldFetchReward) { if (shouldFetchReward) {
unawaited( _scheduleRoomRocketRewardPopupAfterHold(
_showRoomRocketRewardPopupDialog( launch.roomId,
launch.roomId, initialRoomRecords:
initialRoomRecords: pending?.initialRoomRecords ??
pending?.initialRoomRecords ?? const <SCRoomRocketRewardRecordRes>[],
const <SCRoomRocketRewardRecordRes>[],
),
); );
} }
} }
@ -2616,6 +2834,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
if (!_isCurrentVisibleVoiceRoom(roomId)) { if (!_isCurrentVisibleVoiceRoom(roomId)) {
return; return;
} }
if (_isRoomRocketRewardPopupHeld(roomId)) {
_scheduleRoomRocketRewardPopupAfterHold(
roomId,
initialRoomRecords: initialRoomRecords,
);
return;
}
try { try {
final res = await SCChatRoomRepository().roomRocketRewardPopups(roomId); final res = await SCChatRoomRepository().roomRocketRewardPopups(roomId);
if (!_isCurrentVisibleVoiceRoom(roomId)) { if (!_isCurrentVisibleVoiceRoom(roomId)) {
@ -2698,6 +2923,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
); );
} }
void _roomRocketRewardDebug(String message) {
if (!kDebugMode) {
return;
}
debugPrint('[RoomRocketRewardPopup] $message');
}
RoomRocketRewardUserProfileResolver? _roomRocketRewardUserProfileResolver() { RoomRocketRewardUserProfileResolver? _roomRocketRewardUserProfileResolver() {
final currentContext = context; final currentContext = context;
if (currentContext == null || !currentContext.mounted) { if (currentContext == null || !currentContext.mounted) {
@ -3278,7 +3510,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
} else if (type == SCRoomMsgType.voiceRoomRocketStatusUpdate) { } else if (type == SCRoomMsgType.voiceRoomRocketStatusUpdate) {
_handleVoiceRoomRocketStatusUpdate(data["data"]); _handleVoiceRoomRocketStatusUpdate(data["data"]);
} else if (type == SCRoomMsgType.voiceRoomRocketRewardPopup) { } else if (type == SCRoomMsgType.voiceRoomRocketRewardPopup) {
_handleVoiceRoomRocketRewardPopup(data["data"]); _handleVoiceRoomRocketRewardPopup(data["data"] ?? data);
} else if (SCRoomMsgType.isRocketRewardUserType(type?.toString())) {
_handleVoiceRoomRocketRewardPopup(data["data"] ?? data);
} else if (type == SCRoomMsgType.roomRedPacket) { } else if (type == SCRoomMsgType.roomRedPacket) {
/// ///
var fData = data["data"]; var fData = data["data"];
@ -3376,9 +3610,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
return; return;
} }
if (data["type"] == SCRoomMsgType.voiceRoomRocketRewardPopup) { if (data["type"] == SCRoomMsgType.voiceRoomRocketRewardPopup) {
_handleVoiceRoomRocketRewardPopup(data["data"]); _handleVoiceRoomRocketRewardPopup(data["data"] ?? data);
return; return;
} }
if (SCRoomMsgType.isRocketRewardUserType(data["type"]?.toString())) {
_handleVoiceRoomRocketRewardPopup(data["data"] ?? data);
}
if (data["type"] == SCRoomMsgType.roomRedPacket) { if (data["type"] == SCRoomMsgType.roomRedPacket) {
/// ///
var fData = data["data"]; var fData = data["data"];
@ -4088,6 +4325,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
_hiddenCurrentRoomRocketLaunchKeys.clear(); _hiddenCurrentRoomRocketLaunchKeys.clear();
_roomRocketRewardPopupRetryTimer?.cancel(); _roomRocketRewardPopupRetryTimer?.cancel();
_roomRocketRewardPopupRetryTimer = null; _roomRocketRewardPopupRetryTimer = null;
for (final timer in _roomRocketRewardHoldTimers.values) {
timer.cancel();
}
_roomRocketRewardHoldTimers.clear();
_roomRocketRewardHoldUntilTimes.clear();
onNewMessageListenerGroupMap.forEach((k, v) { onNewMessageListenerGroupMap.forEach((k, v) {
v = null; v = null;
}); });

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,12 @@ class SCRoomRocketStatusRes {
num? get myContributionRate => _myContributionRate; num? get myContributionRate => _myContributionRate;
bool? get configured => _configured;
bool? get enabled => _enabled;
bool get isEnabled => _enabled != false;
int? get roundNo => _roundNo; int? get roundNo => _roundNo;
bool? get displayRound => _displayRound; bool? get displayRound => _displayRound;
@ -371,6 +392,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

@ -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

@ -514,10 +514,7 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
if (_failed || !_started) widget.fallback,
widget.fallback
else
const ColoredBox(color: Colors.black),
if (!_failed) if (!_failed)
VapView( VapView(
scaleType: _scaleTypeForFit(widget.fit), scaleType: _scaleTypeForFit(widget.fit),

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,10 +34,13 @@ 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);
final percent = _entryPercent(status); final percent = _entryPercent(status);
final shouldShake = final shouldShake =
(status?.shake ?? false) || (status.shake ?? false) ||
(level != null && percent > level.shakeThresholdPercent); (level != null && percent > level.shakeThresholdPercent);
return SCDebounceWidget( return SCDebounceWidget(
onTap: () => _showRocketDialog(context, status), onTap: () => _showRocketDialog(context, status),
@ -46,7 +49,7 @@ class RoomRocketFloatingEntry extends StatelessWidget {
progressUrl: level?.progressBarUrl ?? '', progressUrl: level?.progressBarUrl ?? '',
progressPercent: percent, progressPercent: percent,
shake: shouldShake, shake: shouldShake,
loading: status == null, loading: false,
), ),
); );
}, },
@ -108,6 +111,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 +421,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 +446,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 +502,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 +559,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 +578,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 +624,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 +683,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 {

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

@ -52,8 +52,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
static const String _defaultProfileCardBackgroundAsset = static const String _defaultProfileCardBackgroundAsset =
"sc_images/room/sc_bg_room_user_profile_card_default.png"; "sc_images/room/sc_bg_room_user_profile_card_default.png";
static const double _profileSheetHeightFactor = 0.4; static const double _profileSheetHeightFactor = 0.4;
static const double _dataCardTranslateDownFactor = 0.4; static const double _dataCardBackgroundTranslateFactor = 0.8;
static const double _defaultBackgroundImageFactor = 0.68;
SocialChatUserProfileManager? userProvider; SocialChatUserProfileManager? userProvider;
String roomId = ""; String roomId = "";
@ -90,8 +89,8 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sheetHeight = final screenHeight = MediaQuery.of(context).size.height;
MediaQuery.of(context).size.height * _profileSheetHeightFactor; final defaultSheetHeight = screenHeight * _profileSheetHeightFactor;
return Consumer<SocialChatUserProfileManager>( return Consumer<SocialChatUserProfileManager>(
builder: (context, ref, child) { builder: (context, ref, child) {
@ -106,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,
@ -154,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,
@ -161,7 +166,6 @@ 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 &&
@ -199,6 +203,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
return _buildDataCardFrame( return _buildDataCardFrame(
sheetHeight: sheetHeight, sheetHeight: sheetHeight,
backgroundHeight: dataCardBackgroundHeight,
dataCard: dataCard, dataCard: dataCard,
child: profileContent, child: profileContent,
); );
@ -206,6 +211,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
Widget _buildDataCardFrame({ Widget _buildDataCardFrame({
required double sheetHeight, required double sheetHeight,
required double backgroundHeight,
required PropsResources dataCard, required PropsResources dataCard,
required Widget child, required Widget child,
}) { }) {
@ -224,8 +230,10 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
top: sheetHeight * _dataCardTranslateDownFactor, top:
height: sheetHeight, -(backgroundHeight - sheetHeight) +
sheetHeight * _dataCardBackgroundTranslateFactor,
height: backgroundHeight,
child: IgnorePointer(child: _buildSheetBackground(dataCard)), child: IgnorePointer(child: _buildSheetBackground(dataCard)),
), ),
Positioned.fill( Positioned.fill(
@ -348,14 +356,10 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
Widget _buildDefaultSheetBackground() { Widget _buildDefaultSheetBackground() {
return DecoratedBox( return DecoratedBox(
decoration: _sheetDecoration(), decoration: _sheetDecoration(),
child: Center( child: SizedBox.expand(
child: FractionallySizedBox( child: Image.asset(
widthFactor: _defaultBackgroundImageFactor, _defaultProfileCardBackgroundAsset,
heightFactor: _defaultBackgroundImageFactor, fit: BoxFit.cover,
child: Image.asset(
_defaultProfileCardBackgroundAsset,
fit: BoxFit.contain,
),
), ),
), ),
); );

Binary file not shown.

Before

Width:  |  Height:  |  Size: 311 KiB

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

@ -122,6 +122,22 @@ 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( 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',
() { () {