Merge branch 'feature' into test
This commit is contained in:
commit
d5653afa4f
@ -280,8 +280,8 @@
|
||||
"bDLeaderInviteYouToBecomeBDLeader": "ندعوك لتصبح قائد تطوير الأعمال",
|
||||
"win2": "فوز {1}",
|
||||
"level": "مستوى",
|
||||
"wealthLevel": "مستوى الثروة",
|
||||
"userLevel": "مستوى المستخدم",
|
||||
"wealthLevel": "مستوى الثراء",
|
||||
"userLevel": "مستوى الجاذبية",
|
||||
"themeGoToUploadTips": "1. ستتم مراجعة التحميل خلال 24 ساعة بعد نجاحه.\n2. ستُعاد جميع العملات إذا فشلت المراجعة.",
|
||||
"pleaseUploadAccordingToExample": "يرجى الرفع وفقًا للمثال المعروض.",
|
||||
"goToUpload": "الانتقال للرفع",
|
||||
@ -665,8 +665,10 @@
|
||||
"mountains": "مركبات",
|
||||
"buy": "شراء",
|
||||
"visitorList": "قائمة الزوار",
|
||||
"spendCoinsToGainExperiencePoints": "اصرف العملات لكسب نقاط الخبرة",
|
||||
"howToUpgrade": "كيف يمكن الترقية؟",
|
||||
"spendCoinsToGainExperiencePoints": "اصرف العملات لكسب نقاط الخبرة",
|
||||
"richLevelUpgradeAnswer": "دع ثروتك تغزو كل شيء",
|
||||
"charmLevelUpgradeAnswer": "اجعل الآخرين يخضعون لسحرك",
|
||||
"howToUpgrade": "كيف يمكن الترقية؟",
|
||||
"higherLevelFancierAvatarFrame": "مستوى أعلى، شارات/إطار صور أفخم.",
|
||||
"use": "استخدم",
|
||||
"expired": "منتهي",
|
||||
|
||||
@ -267,7 +267,7 @@
|
||||
"aboutUs": "আমাদের সম্পর্কে",
|
||||
"theme": "থিম",
|
||||
"wealthLevel": "সম্পদ স্তর",
|
||||
"userLevel": "ব্যবহারকারী স্তর",
|
||||
"userLevel": "আকর্ষণ স্তর",
|
||||
"goToUpload": "আপলোড করতে যান",
|
||||
"logout": "লগআউট",
|
||||
"luck": "ভাগ্য",
|
||||
@ -600,6 +600,8 @@
|
||||
"toConsume": "খরচ করার জন্য",
|
||||
"howToUpgrade": "কীভাবে উন্নত করব?",
|
||||
"spendCoinsToGainExperiencePoints": "অভিজ্ঞতা পয়েন্ট পাওয়ার জন্য কয়েন খরচ করুন",
|
||||
"richLevelUpgradeAnswer": "তোমার সম্পদ সবকিছু জয় করুক",
|
||||
"charmLevelUpgradeAnswer": "অন্যদের তোমার আকর্ষণের কাছে নত করো",
|
||||
"higherLevelFancierAvatarFrame": "উচ্চতর স্তর, আরও সুন্দর ব্যাজ/প্রোফাইল ফ্রেম",
|
||||
"all": "সব",
|
||||
"gift": "গিফট",
|
||||
|
||||
@ -236,8 +236,8 @@
|
||||
"about": "About",
|
||||
"aboutUs": "About Us",
|
||||
"theme": "Theme",
|
||||
"wealthLevel": "Wealth Level",
|
||||
"userLevel": "User Level",
|
||||
"wealthLevel": "Rich Level",
|
||||
"userLevel": "Charm Level",
|
||||
"goToUpload": "Go to upload",
|
||||
"logout": "Logout",
|
||||
"luck": "Luck",
|
||||
@ -569,6 +569,8 @@
|
||||
"toConsume": "To consume",
|
||||
"howToUpgrade": "How to upgrade?",
|
||||
"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",
|
||||
"all": "All",
|
||||
"gift": "Gift",
|
||||
@ -724,7 +726,7 @@
|
||||
"roomRocketNoteTitle": "Note",
|
||||
"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.",
|
||||
"roomRocketRewardEmptyText": "No rewards earned. Better luck next time.",
|
||||
"roomRocketRewardEmptyText": "No reward",
|
||||
"roomRocketRecordEmptyText": "No reward records",
|
||||
"launchRocket": "Launch Rocket",
|
||||
"roomRocketFuelFullNotice": "Rocket fuel is full! Launching in {1}s!"
|
||||
|
||||
@ -237,7 +237,7 @@
|
||||
"aboutUs": "À propos de nous",
|
||||
"theme": "Thème",
|
||||
"wealthLevel": "Niveau de richesse",
|
||||
"userLevel": "Niveau utilisateur",
|
||||
"userLevel": "Niveau de charme",
|
||||
"goToUpload": "Aller au téléchargement",
|
||||
"logout": "Déconnexion",
|
||||
"luck": "Chance",
|
||||
@ -569,6 +569,8 @@
|
||||
"toConsume": "Consommer",
|
||||
"howToUpgrade": "Comment mettre à niveau ?",
|
||||
"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é",
|
||||
"all": "Tout",
|
||||
"gift": "Cadeau",
|
||||
|
||||
@ -237,7 +237,7 @@
|
||||
"aboutUs": "Chi siamo",
|
||||
"theme": "Tema",
|
||||
"wealthLevel": "Livello di ricchezza",
|
||||
"userLevel": "Livello utente",
|
||||
"userLevel": "Livello di fascino",
|
||||
"goToUpload": "Vai al caricamento",
|
||||
"logout": "Esci",
|
||||
"luck": "Fortuna",
|
||||
@ -569,6 +569,8 @@
|
||||
"toConsume": "Consumare",
|
||||
"howToUpgrade": "Come aggiornare?",
|
||||
"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",
|
||||
"all": "Tutto",
|
||||
"gift": "Regalo",
|
||||
|
||||
@ -237,7 +237,7 @@
|
||||
"aboutUs": "Sobre nós",
|
||||
"theme": "Tema",
|
||||
"wealthLevel": "Nível de riqueza",
|
||||
"userLevel": "Nível de usuário",
|
||||
"userLevel": "Nível de charme",
|
||||
"goToUpload": "Ir para carregar",
|
||||
"logout": "Sair",
|
||||
"luck": "Sorte",
|
||||
@ -569,6 +569,8 @@
|
||||
"toConsume": "Para consumir",
|
||||
"howToUpgrade": "Como atualizar?",
|
||||
"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",
|
||||
"all": "Todos",
|
||||
"gift": "Presente",
|
||||
|
||||
@ -225,8 +225,8 @@
|
||||
"about": "Hakkında",
|
||||
"aboutUs": "Hakkımızda",
|
||||
"theme": "Tema",
|
||||
"wealthLevel": "Servet Seviyesi",
|
||||
"userLevel": "Kullanıcı Seviyesi",
|
||||
"wealthLevel": "Zenginlik Seviyesi",
|
||||
"userLevel": "Çekicilik Seviyesi",
|
||||
"goToUpload": "Yüklemeye Git",
|
||||
"logout": "Çıkış Yap",
|
||||
"luck": "Şans",
|
||||
@ -566,10 +566,12 @@
|
||||
"obtain": "Al",
|
||||
"win2": "{1} Kazan",
|
||||
"backTheRoom": "Odaya Dön",
|
||||
"toConsume": "Tüketmek İçin",
|
||||
"howToUpgrade": "Nasıl yükseltirim?",
|
||||
"spendCoinsToGainExperiencePoints": "Deneyim puanı kazanmak için jetton harcayın",
|
||||
"higherLevelFancierAvatarFrame": "Daha yüksek seviye, daha şık rozetler/profil çerçevesi",
|
||||
"toConsume": "Tüketmek İçin",
|
||||
"howToUpgrade": "Nasıl yükseltirim?",
|
||||
"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",
|
||||
"all": "Tümü",
|
||||
"gift": "Hediye",
|
||||
"chat": "Sohbet",
|
||||
|
||||
@ -79,6 +79,10 @@ class SCRoomMsgType {
|
||||
///用户火箭中奖
|
||||
static const String rocketRewardUser = "ROCKET_REWARD_USER";
|
||||
|
||||
///语音房火箭用户中奖
|
||||
static const String voiceRoomRocketRewardUser =
|
||||
"VOICE_ROOM_ROCKET_REWARD_USER";
|
||||
|
||||
///火箭燃料满倒计时发射提示
|
||||
static const String rocketLaunchNotice = "ROCKET_LAUNCH_NOTICE";
|
||||
|
||||
@ -129,4 +133,8 @@ class SCRoomMsgType {
|
||||
static const String gameBurstCrystalSprint = "GAME_BURST_CRYSTAL_SPRINT";
|
||||
static const String gameBurstCrystalBox = "GAME_BURST_CRYSTAL_BOX";
|
||||
static const String refreshOnlineUser = "REFRESH_ONLINE_USER";
|
||||
|
||||
static bool isRocketRewardUserType(String? type) {
|
||||
return type == rocketRewardUser || type == voiceRoomRocketRewardUser;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1042,10 +1042,14 @@ class SCAppLocalizations {
|
||||
|
||||
String get backTheRoom => translate('backTheRoom');
|
||||
|
||||
String get spendCoinsToGainExperiencePoints =>
|
||||
translate('spendCoinsToGainExperiencePoints');
|
||||
|
||||
String get enterRoomName => translate('enterRoomName');
|
||||
String get spendCoinsToGainExperiencePoints =>
|
||||
translate('spendCoinsToGainExperiencePoints');
|
||||
|
||||
String get richLevelUpgradeAnswer => translate('richLevelUpgradeAnswer');
|
||||
|
||||
String get charmLevelUpgradeAnswer => translate('charmLevelUpgradeAnswer');
|
||||
|
||||
String get enterRoomName => translate('enterRoomName');
|
||||
|
||||
String get selectYourCountry => translate('selectYourCountry');
|
||||
|
||||
|
||||
@ -560,7 +560,6 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
///礼物上飘动画
|
||||
_floatingGiftListener(Msg msg) {
|
||||
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||||
rtcProvider.scheduleRoomRocketStatusRefreshForGift();
|
||||
if (!rtcProvider.shouldShowRoomVisualEffects) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -88,13 +88,7 @@ class _StoreChatboxPageState
|
||||
height: 45.w,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
buildStoreBagItemTitle(
|
||||
res.res.propsResources?.name ?? "",
|
||||
textColor: Colors.white,
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
SizedBox(height: 10.w),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@ -87,13 +87,7 @@ class StoreDataCardPageState
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
buildStoreBagItemTitle(
|
||||
res.res.propsResources?.name ?? "",
|
||||
textColor: Colors.white,
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
SizedBox(height: 12.w),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@ -87,13 +87,7 @@ class _StoreHeaddressPageState
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
buildStoreBagItemTitle(
|
||||
res.res.propsResources?.name ?? "",
|
||||
textColor: Colors.white,
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
SizedBox(height: 10.w),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@ -91,13 +91,7 @@ class _StoreMountainsPageState
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
buildStoreBagItemTitle(
|
||||
res.res.propsResources?.name ?? "",
|
||||
textColor: Colors.white,
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
SizedBox(height: 10.w),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@ -91,13 +91,7 @@ class _StoreThemePagePageState
|
||||
height: 55.w,
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.w)),
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
buildStoreBagItemTitle(
|
||||
res.res.propsResources?.name ?? "",
|
||||
textColor: Colors.white,
|
||||
fontSize: 11.sp,
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
SizedBox(height: 10.w),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
|
||||
@ -110,18 +110,26 @@ class _UserLevelPageState extends State<UserLevelPage> {
|
||||
],
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 185.w,
|
||||
height: 6.w,
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 185.w,
|
||||
height: 6.w,
|
||||
decoration: BoxDecoration(
|
||||
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),
|
||||
Row(
|
||||
children: [
|
||||
@ -150,19 +158,7 @@ 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(
|
||||
SCAppLocalizations.of(context)!.howToUpgrade,
|
||||
fontSize: 20.sp,
|
||||
@ -170,14 +166,15 @@ class _UserLevelPageState extends State<UserLevelPage> {
|
||||
textColor: SocialChatTheme.primaryLight,
|
||||
),
|
||||
SizedBox(height: 3),
|
||||
text(
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)!.spendCoinsToGainExperiencePoints,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.charmLevelUpgradeAnswer,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
textColor: Colors.white,
|
||||
maxLines: 2,
|
||||
maxWidth: 320.w,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 25.w),
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
@ -205,12 +202,15 @@ class _UserLevelPageState extends State<UserLevelPage> {
|
||||
: Container();
|
||||
}
|
||||
|
||||
double getLevelExperience(double width) {
|
||||
int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0");
|
||||
int nexExp = SCStringUtils.convertToInteger(res?.nextExperience ?? "0");
|
||||
int levelExp = thatExp + nexExp;
|
||||
return (thatExp / levelExp) * width;
|
||||
}
|
||||
double getLevelExperience(double width) {
|
||||
int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0");
|
||||
int nexExp = SCStringUtils.convertToInteger(res?.nextExperience ?? "0");
|
||||
int levelExp = thatExp + nexExp;
|
||||
if (levelExp <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (thatExp / levelExp) * width;
|
||||
}
|
||||
|
||||
num getLevelTotalExperience() {
|
||||
int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0");
|
||||
|
||||
@ -110,18 +110,26 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
|
||||
],
|
||||
),
|
||||
SizedBox(height: 3.w),
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 185.w,
|
||||
height: 6.w,
|
||||
Stack(
|
||||
children: [
|
||||
Container(
|
||||
width: 185.w,
|
||||
height: 6.w,
|
||||
decoration: BoxDecoration(
|
||||
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),
|
||||
Row(
|
||||
children: [
|
||||
@ -150,19 +158,7 @@ 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(
|
||||
SCAppLocalizations.of(context)!.howToUpgrade,
|
||||
fontSize: 20.sp,
|
||||
@ -170,14 +166,15 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
|
||||
textColor: SocialChatTheme.primaryLight,
|
||||
),
|
||||
SizedBox(height: 3),
|
||||
text(
|
||||
SCAppLocalizations.of(
|
||||
context,
|
||||
)!.spendCoinsToGainExperiencePoints,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
textColor: Colors.white,
|
||||
),
|
||||
text(
|
||||
SCAppLocalizations.of(context)!.richLevelUpgradeAnswer,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
textColor: Colors.white,
|
||||
maxLines: 2,
|
||||
maxWidth: 320.w,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 25.w),
|
||||
GestureDetector(
|
||||
child: Container(
|
||||
@ -205,12 +202,15 @@ class _WealthLevelPageState extends State<WealthLevelPage> {
|
||||
: Container();
|
||||
}
|
||||
|
||||
double getLevelExperience(double width) {
|
||||
int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0");
|
||||
int nexExp = SCStringUtils.convertToInteger(res?.nextExperience ?? "0");
|
||||
int levelExp = thatExp + nexExp;
|
||||
return (thatExp / levelExp) * width;
|
||||
}
|
||||
double getLevelExperience(double width) {
|
||||
int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0");
|
||||
int nexExp = SCStringUtils.convertToInteger(res?.nextExperience ?? "0");
|
||||
int levelExp = thatExp + nexExp;
|
||||
if (levelExp <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return (thatExp / levelExp) * width;
|
||||
}
|
||||
|
||||
num getLevelTotalExperience() {
|
||||
int thatExp = SCStringUtils.convertToInteger(res?.thatExperience ?? "0");
|
||||
|
||||
@ -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/general/sc_app_general_manager.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 '../../../shared/business_logic/models/res/sc_user_counter_res.dart';
|
||||
@ -580,30 +579,8 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileDataCardBackground(PropsResources? dataCard) {
|
||||
if (dataCard == null) {
|
||||
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],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
Widget _buildProfileDataCardBackground(PropsResources? _) {
|
||||
return const ColoredBox(color: _profileBg);
|
||||
}
|
||||
|
||||
PropsResources? _activeDataCard(SocialChatUserProfile? profile) {
|
||||
|
||||
@ -151,7 +151,7 @@ class _ProfileWallOfHonorsPageState extends State<ProfileWallOfHonorsPage> {
|
||||
),
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 3,
|
||||
mainAxisExtent: 132.w,
|
||||
mainAxisExtent: 96.w,
|
||||
mainAxisSpacing: 14.w,
|
||||
crossAxisSpacing: 12.w,
|
||||
),
|
||||
@ -161,39 +161,26 @@ class _ProfileWallOfHonorsPageState extends State<ProfileWallOfHonorsPage> {
|
||||
|
||||
Widget _buildBadgeItem(WearBadge badge) {
|
||||
final url = _badgeUrl(badge);
|
||||
final name = (badge.badgeName ?? "").trim();
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 86.w,
|
||||
height: 86.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff0E332D),
|
||||
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,
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 86.w,
|
||||
height: 86.w,
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xff0E332D),
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
border: Border.all(
|
||||
color: const Color(0xff18F2B1).withValues(alpha: 0.24),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8.w),
|
||||
text(
|
||||
name.isEmpty ? "--" : name,
|
||||
fontSize: 12,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w400,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
child: netImage(
|
||||
url: url,
|
||||
width: 68.w,
|
||||
height: 68.w,
|
||||
fit: BoxFit.contain,
|
||||
noDefaultImg: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -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_debounce_widget.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 {
|
||||
const VipDetailPage({super.key});
|
||||
@ -58,7 +59,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
final status = results[1] as SCVipStatusRes;
|
||||
final resolvedStatus = _resolveStatus(status, home.state);
|
||||
final selectedLevel = _resolveInitialLevel(home, resolvedStatus);
|
||||
_logLoadedVipData(home, resolvedStatus, selectedLevel);
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
@ -141,11 +141,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
_selectedLevel = level;
|
||||
_preview = null;
|
||||
});
|
||||
_logSelectedLevelResources(
|
||||
level,
|
||||
config: _levelConfigFor(level),
|
||||
status: _status,
|
||||
);
|
||||
_loadPreviewForLevel(level);
|
||||
}
|
||||
|
||||
@ -201,6 +196,7 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
await _loadVipData(showLoading: false);
|
||||
}
|
||||
} catch (error) {
|
||||
debugPrint('VIP purchase failed: $error');
|
||||
} finally {
|
||||
SCLoadingManager.hide();
|
||||
if (mounted) {
|
||||
@ -214,56 +210,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@ -524,6 +470,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
|
||||
Widget _buildLevelCard() {
|
||||
final assetLevel = _assetLevel;
|
||||
final backgroundCard = _selectedBackgroundCard;
|
||||
final effectImage = _selectedEffectImage ?? _selectedLongBadge;
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
width: 351.w,
|
||||
@ -532,18 +480,23 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
border: Border.all(
|
||||
color: const Color(0xFFEBCB76),
|
||||
width: 1.w,
|
||||
),
|
||||
image: DecorationImage(
|
||||
image: AssetImage(
|
||||
'sc_images/vip/sc_vip_card_bg_$assetLevel.png',
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
child: _buildVipCardBackground(
|
||||
resource: backgroundCard,
|
||||
assetLevel: assetLevel,
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
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(
|
||||
end: 24.w,
|
||||
top: -36.w,
|
||||
child: _buildResourceImage(
|
||||
resource: _selectedBadge,
|
||||
child: _buildDynamicResourceVisual(
|
||||
resource: effectImage,
|
||||
width: 135.w,
|
||||
height: 135.w,
|
||||
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() {
|
||||
final ornamentLayout = _ornamentLayoutForLevel(_assetLevel);
|
||||
return SizedBox(
|
||||
@ -713,9 +687,14 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
);
|
||||
}
|
||||
|
||||
SCVipResourceRes? get _selectedBadge => _selectedResource(
|
||||
fromConfig: (config) => config.badge,
|
||||
fromState: (status) => status.badge,
|
||||
SCVipResourceRes? get _selectedLongBadge => _selectedResource(
|
||||
fromConfig: (config) => config.longBadge,
|
||||
fromState: (status) => status.longBadge,
|
||||
);
|
||||
|
||||
SCVipResourceRes? get _selectedShortBadge => _selectedResource(
|
||||
fromConfig: (config) => config.shortBadge,
|
||||
fromState: (status) => status.shortBadge,
|
||||
);
|
||||
|
||||
SCVipResourceRes? get _selectedAvatarFrame => _selectedResource(
|
||||
@ -738,6 +717,16 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
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({
|
||||
required SCVipResourceRes? Function(SCVipLevelConfigRes config) fromConfig,
|
||||
required SCVipResourceRes? Function(SCVipStatusRes status) fromState,
|
||||
@ -768,8 +757,14 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
final items = <_VipFeatureData>[];
|
||||
_addResourceItem(
|
||||
items,
|
||||
resource: _selectedBadge,
|
||||
fallbackTitle: 'VIP Badge',
|
||||
resource: _selectedLongBadge,
|
||||
fallbackTitle: 'Long Badge',
|
||||
fallbackIcon: Icons.workspace_premium_outlined,
|
||||
);
|
||||
_addResourceItem(
|
||||
items,
|
||||
resource: _selectedShortBadge,
|
||||
fallbackTitle: 'Short Badge',
|
||||
fallbackIcon: Icons.workspace_premium_outlined,
|
||||
);
|
||||
_addResourceItem(
|
||||
@ -797,12 +792,19 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
fallbackIcon: Icons.airline_seat_recline_extra,
|
||||
wide: true,
|
||||
);
|
||||
_addResourceItem(
|
||||
items,
|
||||
resource: _selectedBackgroundCard,
|
||||
fallbackTitle: 'Profile Card',
|
||||
fallbackIcon: Icons.badge_outlined,
|
||||
wide: true,
|
||||
);
|
||||
return items;
|
||||
}
|
||||
|
||||
List<_VipFeatureData> get _vipPrivilegeItems {
|
||||
// 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.
|
||||
return const <_VipFeatureData>[];
|
||||
}
|
||||
@ -844,6 +846,40 @@ class _VipDetailPageState extends State<VipDetailPage> {
|
||||
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({
|
||||
required SCVipResourceRes? resource,
|
||||
required double width,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:dio/dio.dart';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
@ -291,6 +292,20 @@ class _PendingRoomStartupSeatAction {
|
||||
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 {
|
||||
static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog';
|
||||
static const String _roomRocketPagLaunchDialogTag =
|
||||
@ -304,6 +319,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
Duration(seconds: 5),
|
||||
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 Duration _micListPollingInterval = Duration(seconds: 2);
|
||||
@ -343,8 +364,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
Timer? _onlineUsersPollingTimer;
|
||||
Timer? _roomEntryEffectTimer;
|
||||
Timer? _roomRocketLaunchAnimationTimer;
|
||||
ValueNotifier<String?>? _roomRocketLaunchTopAvatarNotifier;
|
||||
Timer? _roomRocketGiftRefreshTimer;
|
||||
final List<Timer> _roomRocketPostLaunchRefreshTimers = [];
|
||||
final List<Timer> _roomRocketEntryReadyFallbackTimers = [];
|
||||
Timer? _roomRedPacketPresenceTimer;
|
||||
Future<void>? _rtcEnginePrewarmTask;
|
||||
Future<void>? _pendingRoomSwitchRtcLeaveTask;
|
||||
@ -490,8 +513,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_roomEntryEffectTimer = null;
|
||||
_roomRocketLaunchAnimationTimer?.cancel();
|
||||
_roomRocketLaunchAnimationTimer = null;
|
||||
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
|
||||
_releaseRoomRocketLaunchTopAvatarNotifier();
|
||||
_roomRocketGiftRefreshTimer?.cancel();
|
||||
_roomRocketGiftRefreshTimer = null;
|
||||
_cancelRoomRocketEntryReadyFallbacks();
|
||||
_cancelRoomRocketPostLaunchStatusRefresh();
|
||||
}
|
||||
_roomVisualEffectsEnabled = enabled;
|
||||
@ -500,6 +526,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
void setVoiceRoomRouteVisible(bool visible) {
|
||||
_voiceRoomRouteVisible = visible;
|
||||
if (visible) {
|
||||
final roomId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||
if (roomId.isNotEmpty) {
|
||||
_scheduleRoomRocketEntryReadyFallback(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _setRoomStartupLoading() {
|
||||
@ -1780,10 +1812,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
await cleanupPersistedVoiceRoomSessionBeforeEntry(targetRoomId: roomId);
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
final persistedRoomCleanupRequest =
|
||||
_preparePersistedVoiceRoomCleanupBeforeEntry(
|
||||
targetRoomId: roomId,
|
||||
restoreOnlineHeartbeatWhenIdle: false,
|
||||
);
|
||||
if (roomId == currenRoom?.roomProfile?.roomProfile?.id) {
|
||||
if (_currentRoomIsEntryPreview ||
|
||||
_roomStartupStatus == RoomStartupStatus.loading) {
|
||||
@ -1792,6 +1825,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
_startVoiceRoomForegroundService();
|
||||
VoiceRoomRoute.openVoiceRoom(context);
|
||||
_startPersistedVoiceRoomCleanupInBackground(
|
||||
persistedRoomCleanupRequest,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1830,6 +1866,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_refreshRoomRedPacketListAfterEntry(roomId: roomId);
|
||||
_startVoiceRoomForegroundService();
|
||||
VoiceRoomRoute.openVoiceRoom(context);
|
||||
_startPersistedVoiceRoomCleanupInBackground(persistedRoomCleanupRequest);
|
||||
if (shouldRejoinRoomRtc) {
|
||||
unawaited(_rejoinCurrentRoomRtcForSameRoomReuse(roomId));
|
||||
}
|
||||
@ -1865,6 +1902,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
);
|
||||
_startVoiceRoomForegroundService();
|
||||
VoiceRoomRoute.openVoiceRoom(context);
|
||||
_startPersistedVoiceRoomCleanupInBackground(persistedRoomCleanupRequest);
|
||||
notifyListeners();
|
||||
final entryRequestSerial = ++_roomEntryRequestSerial;
|
||||
if (preEnteredRoom != null) {
|
||||
@ -2487,14 +2525,70 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
Future<void> cleanupPersistedVoiceRoomSessionBeforeEntry({
|
||||
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;
|
||||
if (runningTask != null) {
|
||||
return runningTask;
|
||||
}
|
||||
final task = _cleanupPersistedVoiceRoomSessionBeforeEntry(
|
||||
targetRoomId: targetRoomId,
|
||||
);
|
||||
final task = _cleanupPersistedVoiceRoomSessionBeforeEntry(cleanupRequest);
|
||||
_persistedRoomCleanupTask = task;
|
||||
return task.whenComplete(() {
|
||||
if (identical(_persistedRoomCleanupTask, task)) {
|
||||
@ -2503,28 +2597,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _cleanupPersistedVoiceRoomSessionBeforeEntry({
|
||||
String? targetRoomId,
|
||||
}) async {
|
||||
final staleRoomId = DataPersistence.getLastTimeRoomId().trim();
|
||||
if (staleRoomId.isEmpty) {
|
||||
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) {
|
||||
Future<void> _cleanupPersistedVoiceRoomSessionBeforeEntry(
|
||||
_PersistedVoiceRoomCleanupRequest cleanupRequest,
|
||||
) async {
|
||||
final staleRoomId = cleanupRequest.staleRoomId;
|
||||
if (cleanupRequest.clearMarkerOnly) {
|
||||
await _clearPersistedRoomMarkerIfMatches(staleRoomId);
|
||||
return;
|
||||
}
|
||||
@ -2545,7 +2622,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
);
|
||||
if (didClean) {
|
||||
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(
|
||||
SCHeartbeatStatus.ONLINE.name,
|
||||
false,
|
||||
@ -2594,6 +2675,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
setRoomVisualEffectsEnabled(true);
|
||||
if (shouldOpenRoomPage) {
|
||||
VoiceRoomRoute.openVoiceRoom(context!);
|
||||
setVoiceRoomRouteVisible(true);
|
||||
}
|
||||
unawaited(
|
||||
_bootstrapEnteredVoiceRoomSession(
|
||||
@ -2694,6 +2776,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
roomId: roomId,
|
||||
);
|
||||
_loadRoomSecondaryData();
|
||||
_scheduleRoomRocketEntryReadyFallback(roomId);
|
||||
} catch (error, stackTrace) {
|
||||
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
|
||||
return;
|
||||
@ -2806,18 +2889,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
void _loadRoomSecondaryData() {
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
SCChatRoomRepository().rocketClaim(roomId).catchError((e) {
|
||||
return true; // 错误已处理
|
||||
});
|
||||
SCChatRoomRepository()
|
||||
.rocketStatus(roomId)
|
||||
.then((res) {
|
||||
roomRocketStatus = res;
|
||||
notifyListeners();
|
||||
if (!res.isEnabled) {
|
||||
return;
|
||||
}
|
||||
_scheduleRoomRocketAssetPreload(roomId, res, afterRoomEntry: true);
|
||||
_scheduleRoomRocketEntryReadyFallback(roomId);
|
||||
_loadRoomRocketRewardPopups(roomId);
|
||||
})
|
||||
.catchError((e) {});
|
||||
_loadRoomRocketRewardPopups(roomId);
|
||||
_refreshRoomRedPacketListAfterEntry();
|
||||
|
||||
fetchRoomTaskClaimableCount();
|
||||
@ -2873,6 +2957,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) {
|
||||
roomRocketStatus = res;
|
||||
notifyListeners();
|
||||
if (!res.isEnabled) {
|
||||
return;
|
||||
}
|
||||
_scheduleRoomRocketAssetPreload(
|
||||
(res.roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(),
|
||||
res,
|
||||
@ -2893,6 +2980,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
roomRocketStatus = res;
|
||||
notifyListeners();
|
||||
if (!res.isEnabled) {
|
||||
return;
|
||||
}
|
||||
_scheduleRoomRocketAssetPreload(resolvedRoomId, res);
|
||||
}
|
||||
|
||||
@ -2905,6 +2995,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (resolvedRoomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (!status.isEnabled) {
|
||||
return;
|
||||
}
|
||||
if (afterRoomEntry) {
|
||||
RoomRocketAssetPreloader.preloadAfterRoomEntry(
|
||||
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}) {
|
||||
final resolvedRoomId =
|
||||
(roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
|
||||
@ -2973,6 +3102,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
_roomRocketLaunchAnimationTimer?.cancel();
|
||||
_releaseRoomRocketLaunchTopAvatarNotifier();
|
||||
|
||||
if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible) {
|
||||
final animationUrl =
|
||||
@ -2990,6 +3120,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_roomRocketLaunchAnimationTimer = Timer(launch.displayDuration, () {
|
||||
_roomRocketLaunchAnimationTimer = null;
|
||||
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
|
||||
_releaseRoomRocketLaunchTopAvatarNotifier();
|
||||
unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
|
||||
scheduleRoomRocketPostLaunchStatusRefresh(roomId: launch.roomId);
|
||||
});
|
||||
@ -3015,6 +3146,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
required RoomRocketLaunchBroadcastMessage launch,
|
||||
}) {
|
||||
if (RoomRocketPagEffectOverlay.isPag(animationUrl)) {
|
||||
final topAvatarNotifier = ValueNotifier<String?>(
|
||||
_roomRocketTop1AvatarUrl(launch.safeLevel),
|
||||
);
|
||||
_roomRocketLaunchTopAvatarNotifier = topAvatarNotifier;
|
||||
unawaited(_loadRoomRocketTop1Avatar(launch, topAvatarNotifier));
|
||||
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
|
||||
SmartDialog.show(
|
||||
tag: _roomRocketPagLaunchDialogTag,
|
||||
@ -3026,11 +3162,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
builder:
|
||||
(_) => RoomRocketPagEffectOverlay(
|
||||
resource: animationUrl,
|
||||
topAvatarUrl: _roomRocketTop1AvatarUrl(),
|
||||
topAvatarUrlListenable: topAvatarNotifier,
|
||||
onCompleted: () {
|
||||
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
|
||||
_roomRocketLaunchAnimationTimer?.cancel();
|
||||
_roomRocketLaunchAnimationTimer = null;
|
||||
_releaseRoomRocketLaunchTopAvatarNotifier(topAvatarNotifier);
|
||||
unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
|
||||
scheduleRoomRocketPostLaunchStatusRefresh(
|
||||
roomId: launch.roomId,
|
||||
@ -3047,8 +3184,21 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
String? _roomRocketTop1AvatarUrl() {
|
||||
final kings = roomRocketStatus?.rocketKings ?? const [];
|
||||
String? _roomRocketTop1AvatarUrl(int level) {
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
@ -3065,6 +3215,90 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
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(
|
||||
SCRoomRocketRewardRecordRes record,
|
||||
) {
|
||||
@ -3367,9 +3601,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
final exitRtmProvider = rtmProvider;
|
||||
final navigationContext = context;
|
||||
try {
|
||||
exitRtmProvider?.msgAllListener = null;
|
||||
exitRtmProvider?.msgChatListener = null;
|
||||
exitRtmProvider?.msgGiftListener = null;
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
setRoomVisualEffectsEnabled(false);
|
||||
SCHeartbeatUtils.cancelTimer();
|
||||
@ -3405,9 +3636,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
final isPreviewOnlyExit = _currentRoomIsEntryPreview;
|
||||
final exitRtmProvider = rtmProvider;
|
||||
try {
|
||||
exitRtmProvider?.msgAllListener = null;
|
||||
exitRtmProvider?.msgChatListener = null;
|
||||
exitRtmProvider?.msgGiftListener = null;
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
setRoomVisualEffectsEnabled(false);
|
||||
SCHeartbeatUtils.cancelTimer();
|
||||
@ -3613,6 +3841,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
_roomEntryEffectTimer = null;
|
||||
_roomRocketLaunchAnimationTimer?.cancel();
|
||||
_roomRocketLaunchAnimationTimer = null;
|
||||
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
|
||||
_releaseRoomRocketLaunchTopAvatarNotifier();
|
||||
_roomRocketGiftRefreshTimer?.cancel();
|
||||
_roomRocketGiftRefreshTimer = null;
|
||||
_cancelRoomRocketPostLaunchStatusRefresh();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -37,6 +37,8 @@ class SCRoomRocketStatusRes {
|
||||
bool? hasClaimed,
|
||||
num? myContribution,
|
||||
num? myContributionRate,
|
||||
bool? configured,
|
||||
bool? enabled,
|
||||
int? roundNo,
|
||||
bool? displayRound,
|
||||
int? currentLevel,
|
||||
@ -69,6 +71,8 @@ class SCRoomRocketStatusRes {
|
||||
_hasClaimed = hasClaimed;
|
||||
_myContribution = myContribution;
|
||||
_myContributionRate = myContributionRate;
|
||||
_configured = configured;
|
||||
_enabled = enabled;
|
||||
_roundNo = roundNo;
|
||||
_displayRound = displayRound;
|
||||
_currentLevel = currentLevel;
|
||||
@ -87,6 +91,15 @@ class SCRoomRocketStatusRes {
|
||||
}
|
||||
|
||||
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'];
|
||||
_date = json['date'] ?? json['dayKey'] ?? json['day_key'];
|
||||
_roundNo = _intValue(json['roundNo'] ?? json['round_no']);
|
||||
@ -219,6 +232,8 @@ class SCRoomRocketStatusRes {
|
||||
bool? _hasClaimed;
|
||||
num? _myContribution;
|
||||
num? _myContributionRate;
|
||||
bool? _configured;
|
||||
bool? _enabled;
|
||||
int? _roundNo;
|
||||
bool? _displayRound;
|
||||
int? _currentLevel;
|
||||
@ -267,6 +282,17 @@ class SCRoomRocketStatusRes {
|
||||
|
||||
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;
|
||||
|
||||
bool? get displayRound => _displayRound;
|
||||
@ -371,6 +397,8 @@ class SCRoomRocketStatusRes {
|
||||
map['hasClaimed'] = _hasClaimed;
|
||||
map['myContribution'] = _myContribution;
|
||||
map['myContributionRate'] = _myContributionRate;
|
||||
map['configured'] = _configured;
|
||||
map['enabled'] = _enabled;
|
||||
map['roundNo'] = _roundNo;
|
||||
map['displayRound'] = _displayRound;
|
||||
map['currentLevel'] = _currentLevel;
|
||||
|
||||
@ -110,10 +110,14 @@ class SCVipStatusRes {
|
||||
String? startAt,
|
||||
String? expireAt,
|
||||
SCVipResourceRes? badge,
|
||||
SCVipResourceRes? longBadge,
|
||||
SCVipResourceRes? shortBadge,
|
||||
SCVipResourceRes? avatarFrame,
|
||||
SCVipResourceRes? entryEffect,
|
||||
SCVipResourceRes? chatBubble,
|
||||
SCVipResourceRes? floatPicture,
|
||||
SCVipResourceRes? backgroundCard,
|
||||
SCVipResourceRes? effectImage,
|
||||
}) {
|
||||
_userId = userId;
|
||||
_sysOrigin = sysOrigin;
|
||||
@ -125,10 +129,14 @@ class SCVipStatusRes {
|
||||
_startAt = startAt;
|
||||
_expireAt = expireAt;
|
||||
_badge = badge;
|
||||
_longBadge = longBadge;
|
||||
_shortBadge = shortBadge;
|
||||
_avatarFrame = avatarFrame;
|
||||
_entryEffect = entryEffect;
|
||||
_chatBubble = chatBubble;
|
||||
_floatPicture = floatPicture;
|
||||
_backgroundCard = backgroundCard;
|
||||
_effectImage = effectImage;
|
||||
}
|
||||
|
||||
SCVipStatusRes.fromJson(dynamic json) {
|
||||
@ -144,10 +152,14 @@ class SCVipStatusRes {
|
||||
_startAt = _stringValue(map['startAt']);
|
||||
_expireAt = _stringValue(map['expireAt']);
|
||||
_badge = _resourceValue(map['badge']);
|
||||
_longBadge = _resourceValue(map['longBadge']) ?? _badge;
|
||||
_shortBadge = _resourceValue(map['shortBadge']);
|
||||
_avatarFrame = _resourceValue(map['avatarFrame']);
|
||||
_entryEffect = _resourceValue(map['entryEffect']);
|
||||
_chatBubble = _resourceValue(map['chatBubble']);
|
||||
_floatPicture = _resourceValue(map['floatPicture']);
|
||||
_backgroundCard = _resourceValue(map['backgroundCard']);
|
||||
_effectImage = _resourceValue(map['effectImage']);
|
||||
}
|
||||
|
||||
String? _userId;
|
||||
@ -160,10 +172,14 @@ class SCVipStatusRes {
|
||||
String? _startAt;
|
||||
String? _expireAt;
|
||||
SCVipResourceRes? _badge;
|
||||
SCVipResourceRes? _longBadge;
|
||||
SCVipResourceRes? _shortBadge;
|
||||
SCVipResourceRes? _avatarFrame;
|
||||
SCVipResourceRes? _entryEffect;
|
||||
SCVipResourceRes? _chatBubble;
|
||||
SCVipResourceRes? _floatPicture;
|
||||
SCVipResourceRes? _backgroundCard;
|
||||
SCVipResourceRes? _effectImage;
|
||||
|
||||
String? get userId => _userId;
|
||||
|
||||
@ -183,7 +199,11 @@ class SCVipStatusRes {
|
||||
|
||||
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;
|
||||
|
||||
@ -193,6 +213,10 @@ class SCVipStatusRes {
|
||||
|
||||
SCVipResourceRes? get floatPicture => _floatPicture;
|
||||
|
||||
SCVipResourceRes? get backgroundCard => _backgroundCard;
|
||||
|
||||
SCVipResourceRes? get effectImage => _effectImage;
|
||||
|
||||
bool get isActive => _active == true && (_level ?? 0) > 0;
|
||||
|
||||
int get levelInt => (_level ?? 0).toInt();
|
||||
@ -216,11 +240,17 @@ class SCVipStatusRes {
|
||||
map['displayName'] = _displayName;
|
||||
map['startAt'] = _startAt;
|
||||
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 (_entryEffect != null) map['entryEffect'] = _entryEffect?.toJson();
|
||||
if (_chatBubble != null) map['chatBubble'] = _chatBubble?.toJson();
|
||||
if (_floatPicture != null) map['floatPicture'] = _floatPicture?.toJson();
|
||||
if (_backgroundCard != null) {
|
||||
map['backgroundCard'] = _backgroundCard?.toJson();
|
||||
}
|
||||
if (_effectImage != null) map['effectImage'] = _effectImage?.toJson();
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@ -234,10 +264,14 @@ class SCVipLevelConfigRes {
|
||||
num? durationDays,
|
||||
num? priceGold,
|
||||
SCVipResourceRes? badge,
|
||||
SCVipResourceRes? longBadge,
|
||||
SCVipResourceRes? shortBadge,
|
||||
SCVipResourceRes? avatarFrame,
|
||||
SCVipResourceRes? entryEffect,
|
||||
SCVipResourceRes? chatBubble,
|
||||
SCVipResourceRes? floatPicture,
|
||||
SCVipResourceRes? backgroundCard,
|
||||
SCVipResourceRes? effectImage,
|
||||
bool? canPurchase,
|
||||
num? payableGold,
|
||||
}) {
|
||||
@ -248,10 +282,14 @@ class SCVipLevelConfigRes {
|
||||
_durationDays = durationDays;
|
||||
_priceGold = priceGold;
|
||||
_badge = badge;
|
||||
_longBadge = longBadge;
|
||||
_shortBadge = shortBadge;
|
||||
_avatarFrame = avatarFrame;
|
||||
_entryEffect = entryEffect;
|
||||
_chatBubble = chatBubble;
|
||||
_floatPicture = floatPicture;
|
||||
_backgroundCard = backgroundCard;
|
||||
_effectImage = effectImage;
|
||||
_canPurchase = canPurchase;
|
||||
_payableGold = payableGold;
|
||||
}
|
||||
@ -266,10 +304,14 @@ class SCVipLevelConfigRes {
|
||||
_durationDays = _numValue(map['durationDays']);
|
||||
_priceGold = _numValue(map['priceGold']);
|
||||
_badge = _resourceValue(map['badge']);
|
||||
_longBadge = _resourceValue(map['longBadge']) ?? _badge;
|
||||
_shortBadge = _resourceValue(map['shortBadge']);
|
||||
_avatarFrame = _resourceValue(map['avatarFrame']);
|
||||
_entryEffect = _resourceValue(map['entryEffect']);
|
||||
_chatBubble = _resourceValue(map['chatBubble']);
|
||||
_floatPicture = _resourceValue(map['floatPicture']);
|
||||
_backgroundCard = _resourceValue(map['backgroundCard']);
|
||||
_effectImage = _resourceValue(map['effectImage']);
|
||||
_canPurchase = _boolValue(map['canPurchase']);
|
||||
_payableGold = _numValue(map['payableGold']);
|
||||
}
|
||||
@ -281,10 +323,14 @@ class SCVipLevelConfigRes {
|
||||
num? _durationDays;
|
||||
num? _priceGold;
|
||||
SCVipResourceRes? _badge;
|
||||
SCVipResourceRes? _longBadge;
|
||||
SCVipResourceRes? _shortBadge;
|
||||
SCVipResourceRes? _avatarFrame;
|
||||
SCVipResourceRes? _entryEffect;
|
||||
SCVipResourceRes? _chatBubble;
|
||||
SCVipResourceRes? _floatPicture;
|
||||
SCVipResourceRes? _backgroundCard;
|
||||
SCVipResourceRes? _effectImage;
|
||||
bool? _canPurchase;
|
||||
num? _payableGold;
|
||||
|
||||
@ -300,7 +346,11 @@ class SCVipLevelConfigRes {
|
||||
|
||||
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;
|
||||
|
||||
@ -310,6 +360,10 @@ class SCVipLevelConfigRes {
|
||||
|
||||
SCVipResourceRes? get floatPicture => _floatPicture;
|
||||
|
||||
SCVipResourceRes? get backgroundCard => _backgroundCard;
|
||||
|
||||
SCVipResourceRes? get effectImage => _effectImage;
|
||||
|
||||
bool? get canPurchase => _canPurchase;
|
||||
|
||||
num? get payableGold => _payableGold;
|
||||
@ -332,11 +386,17 @@ class SCVipLevelConfigRes {
|
||||
map['enabled'] = _enabled;
|
||||
map['durationDays'] = _durationDays;
|
||||
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 (_entryEffect != null) map['entryEffect'] = _entryEffect?.toJson();
|
||||
if (_chatBubble != null) map['chatBubble'] = _chatBubble?.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['payableGold'] = _payableGold;
|
||||
return map;
|
||||
|
||||
@ -1,13 +1,16 @@
|
||||
enum SCPropsType {
|
||||
AVATAR_FRAME,
|
||||
FLOAT_PICTURE,
|
||||
DATA_CARD,
|
||||
CHAT_BUBBLE,
|
||||
RIDE,
|
||||
LAYOUT,
|
||||
THEME,
|
||||
BADGE,
|
||||
NOBLE_VIP,
|
||||
FRAGMENTS,
|
||||
RED_PACKET,
|
||||
}
|
||||
// ignore_for_file: constant_identifier_names
|
||||
|
||||
enum SCPropsType {
|
||||
AVATAR_FRAME,
|
||||
FLOAT_PICTURE,
|
||||
DATA_CARD,
|
||||
CHAT_BUBBLE,
|
||||
RIDE,
|
||||
LAYOUT,
|
||||
THEME,
|
||||
BADGE,
|
||||
NOBLE_VIP,
|
||||
VIP_EFFECT_IMAGE,
|
||||
FRAGMENTS,
|
||||
RED_PACKET,
|
||||
}
|
||||
|
||||
@ -57,15 +57,18 @@ Widget head({
|
||||
final String headdressCoverUrl = headdressCover?.trim() ?? "";
|
||||
final String headdressExtension =
|
||||
SCPathUtils.getFileExtension(headdressUrl).toLowerCase();
|
||||
final bool hasLocalVirtualHeaddress = _isLocalAssetsVirtualUrl(headdressUrl);
|
||||
final bool hasPictureHeaddress =
|
||||
headdressUrl.isNotEmpty && SCPathUtils.fileTypeIsPic2(headdressUrl);
|
||||
final bool hasAnimatedHeaddress =
|
||||
headdressExtension == ".svga" || headdressExtension == ".mp4";
|
||||
final bool shouldShowAnimatedHeaddress =
|
||||
hasAnimatedHeaddress && SCGlobalConfig.allowsHighCostAnimations;
|
||||
hasAnimatedHeaddress &&
|
||||
SCGlobalConfig.allowsHighCostAnimations &&
|
||||
!hasLocalVirtualHeaddress;
|
||||
final bool shouldShowStaticHeaddressFallback =
|
||||
hasAnimatedHeaddress &&
|
||||
!SCGlobalConfig.allowsHighCostAnimations &&
|
||||
(!SCGlobalConfig.allowsHighCostAnimations || hasLocalVirtualHeaddress) &&
|
||||
headdressCoverUrl.isNotEmpty;
|
||||
final bool hasHeaddress =
|
||||
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({
|
||||
required String url,
|
||||
String? defaultImg,
|
||||
|
||||
@ -23,6 +23,8 @@ class SCDataCardResourceView extends StatelessWidget {
|
||||
this.fit = BoxFit.cover,
|
||||
this.loop = true,
|
||||
this.fallback,
|
||||
this.showCoverFallback = true,
|
||||
this.allowCoverAsResource = true,
|
||||
});
|
||||
|
||||
final PropsResources? resource;
|
||||
@ -33,6 +35,8 @@ class SCDataCardResourceView extends StatelessWidget {
|
||||
final BoxFit fit;
|
||||
final bool loop;
|
||||
final Widget? fallback;
|
||||
final bool showCoverFallback;
|
||||
final bool allowCoverAsResource;
|
||||
|
||||
static bool isSupported(String? url) {
|
||||
return _isSvga(url) ||
|
||||
@ -50,15 +54,22 @@ class SCDataCardResourceView extends StatelessWidget {
|
||||
final resolvedHeight = _resolvedSize(height, constraints.maxHeight);
|
||||
final cover = _firstNonBlank([coverUrl, resource?.cover]);
|
||||
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 =
|
||||
fallback ??
|
||||
_buildImage(
|
||||
cover,
|
||||
width: resolvedWidth,
|
||||
height: resolvedHeight,
|
||||
fit: fit,
|
||||
);
|
||||
(showCoverFallback
|
||||
? _buildImage(
|
||||
cover,
|
||||
width: resolvedWidth,
|
||||
height: resolvedHeight,
|
||||
fit: fit,
|
||||
)
|
||||
: emptyFallback);
|
||||
|
||||
if (resourceUrl.isEmpty) {
|
||||
return fallbackView;
|
||||
@ -330,10 +341,12 @@ class _LoopingDataCardVapVideo extends StatefulWidget {
|
||||
|
||||
class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
|
||||
static const int _legacyRgbLeftAlphaRightMode = 3;
|
||||
static const int _longRunningLoopCount = 999999;
|
||||
|
||||
VapController? _controller;
|
||||
int _playToken = 0;
|
||||
bool _failed = false;
|
||||
bool _started = false;
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _LoopingDataCardVapVideo oldWidget) {
|
||||
@ -343,8 +356,11 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
|
||||
oldWidget.fit != widget.fit) {
|
||||
_playToken++;
|
||||
_stop();
|
||||
if (_failed) {
|
||||
setState(() => _failed = false);
|
||||
if (_failed || _started) {
|
||||
setState(() {
|
||||
_failed = false;
|
||||
_started = false;
|
||||
});
|
||||
}
|
||||
_play();
|
||||
}
|
||||
@ -362,15 +378,32 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
|
||||
_controller = controller;
|
||||
controller.setAnimListener(
|
||||
onFailed: (_, __, ___) => _markFailed(),
|
||||
onVideoComplete: _handleVideoComplete,
|
||||
onVideoStart: () {
|
||||
if (mounted && _failed) {
|
||||
setState(() => _failed = false);
|
||||
if (mounted && (_failed || !_started)) {
|
||||
setState(() {
|
||||
_failed = false;
|
||||
_started = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
_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 {
|
||||
final controller = _controller;
|
||||
final url = widget.url.trim();
|
||||
@ -390,7 +423,7 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
|
||||
await controller.setMute(true);
|
||||
await controller.enableVersion1(true);
|
||||
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);
|
||||
if (!_isCurrent(token, controller)) {
|
||||
return;
|
||||
@ -451,7 +484,10 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
|
||||
if (!mounted || _failed) {
|
||||
return;
|
||||
}
|
||||
setState(() => _failed = true);
|
||||
setState(() {
|
||||
_failed = true;
|
||||
_started = false;
|
||||
});
|
||||
}
|
||||
|
||||
ScaleType _scaleTypeForFit(BoxFit fit) {
|
||||
@ -482,7 +518,7 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
|
||||
if (!_failed)
|
||||
VapView(
|
||||
scaleType: _scaleTypeForFit(widget.fit),
|
||||
repeat: widget.loop ? -1 : 0,
|
||||
repeat: widget.loop ? _longRunningLoopCount : 0,
|
||||
mute: true,
|
||||
onViewCreated: _handleViewCreated,
|
||||
),
|
||||
|
||||
@ -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 {
|
||||
const RoomRocketRewardItem({
|
||||
this.asset,
|
||||
@ -449,7 +463,7 @@ class _RocketCanvas extends StatelessWidget {
|
||||
top: s(120),
|
||||
width: s(_designWidth),
|
||||
child: _GradientText(
|
||||
'Round $round Today',
|
||||
_roomRocketTitleText(level: selectedLevelItem.level, round: round),
|
||||
fontSize: s(16),
|
||||
fontWeight: FontWeight.w600,
|
||||
textAlign: TextAlign.center,
|
||||
@ -463,31 +477,11 @@ class _RocketCanvas extends StatelessWidget {
|
||||
top: s(212),
|
||||
width: s(40),
|
||||
height: s(190),
|
||||
child: ScrollConfiguration(
|
||||
behavior: const _NoGlowScrollBehavior(),
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
primary: false,
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
child: _LevelRail(
|
||||
scale: scale,
|
||||
levels: levels,
|
||||
selectedLevel: selectedLevel,
|
||||
onLevelSelected: onLevelSelected,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -1031,9 +1025,12 @@ class _LevelBadge extends StatelessWidget {
|
||||
item.label,
|
||||
fontSize: 10 * scale,
|
||||
color:
|
||||
item.enabled
|
||||
selected
|
||||
? const Color(0xFFA5FFE3)
|
||||
: item.enabled
|
||||
? Colors.white
|
||||
: 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 {
|
||||
const _CrewAvatar({
|
||||
required this.scale,
|
||||
@ -1399,28 +1508,45 @@ class _LevelBadgePainter extends CustomPainter {
|
||||
size,
|
||||
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(
|
||||
path,
|
||||
Paint()
|
||||
..shader = const LinearGradient(
|
||||
..shader = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [Color(0xFF077C60), Color(0xFF08251E)],
|
||||
colors:
|
||||
selected
|
||||
? const [Color(0xFF10DFA8), Color(0xFF07392E)]
|
||||
: const [Color(0xFF077C60), Color(0xFF08251E)],
|
||||
).createShader(Offset.zero & size),
|
||||
);
|
||||
canvas.drawPath(
|
||||
path,
|
||||
Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
..strokeWidth = selected ? size.width / 22 : size.width / 40
|
||||
..strokeWidth = selected ? size.width / 14 : size.width / 40
|
||||
..shader = LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
enabled
|
||||
selected
|
||||
? Colors.white
|
||||
: enabled
|
||||
? const Color(0xFFB2FBCC)
|
||||
: 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),
|
||||
);
|
||||
@ -1554,12 +1680,14 @@ class _RocketText extends StatelessWidget {
|
||||
required this.fontSize,
|
||||
required this.color,
|
||||
this.textAlign = TextAlign.center,
|
||||
this.fontWeight = FontWeight.w400,
|
||||
});
|
||||
|
||||
final String text;
|
||||
final double fontSize;
|
||||
final Color color;
|
||||
final TextAlign textAlign;
|
||||
final FontWeight fontWeight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -1572,6 +1700,7 @@ class _RocketText extends StatelessWidget {
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: fontSize,
|
||||
fontWeight: fontWeight,
|
||||
height: 1,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
|
||||
@ -34,19 +34,24 @@ class RoomRocketFloatingEntry extends StatelessWidget {
|
||||
return Selector<RtcProvider, SCRoomRocketStatusRes?>(
|
||||
selector: (_, provider) => provider.roomRocketStatus,
|
||||
builder: (context, status, child) {
|
||||
if (status == null || !status.isEnabled) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final level = _currentEntryLevel(status);
|
||||
if (level == null || level.rocketIconUrl.trim().isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final percent = _entryPercent(status);
|
||||
final shouldShake =
|
||||
(status?.shake ?? false) ||
|
||||
(level != null && percent > level.shakeThresholdPercent);
|
||||
(status.shake ?? false) || percent > level.shakeThresholdPercent;
|
||||
return SCDebounceWidget(
|
||||
onTap: () => _showRocketDialog(context, status),
|
||||
child: _RoomRocketEntryButton(
|
||||
iconUrl: level?.rocketIconUrl ?? '',
|
||||
progressUrl: level?.progressBarUrl ?? '',
|
||||
iconUrl: level.rocketIconUrl,
|
||||
progressUrl: level.progressBarUrl,
|
||||
progressPercent: percent,
|
||||
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 {
|
||||
const _RoomRocketEntryButton({
|
||||
required this.iconUrl,
|
||||
@ -369,9 +423,16 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
|
||||
super.initState();
|
||||
_status = 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(_loadRewardForLevel(_selectedLevel + 1));
|
||||
unawaited(_loadRewardForLevel(initialLevelNumber));
|
||||
unawaited(_loadConfigs());
|
||||
unawaited(_loadRule());
|
||||
}
|
||||
@ -387,16 +448,29 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (!res.isEnabled) {
|
||||
try {
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).updateRoomRocketConfigurationStatus(res);
|
||||
} catch (_) {}
|
||||
widget.onClose?.call();
|
||||
return;
|
||||
}
|
||||
_debugStatusAssets(res);
|
||||
final levelItems = RoomRocketApiMapper.levelsFromStatus(res);
|
||||
late final int selectedLevelNumber;
|
||||
setState(() {
|
||||
_status = res;
|
||||
if (!_levelTouched) {
|
||||
_selectedLevel = _levelIndexFromStatus(res);
|
||||
}
|
||||
_cacheRewardGroupsFromStatus(res, _selectedLevel + 1);
|
||||
selectedLevelNumber = _levelNumberForIndex(levelItems, _selectedLevel);
|
||||
_cacheRewardGroupsFromStatus(res, selectedLevelNumber);
|
||||
});
|
||||
unawaited(_loadCrewForLevel(_selectedLevel + 1));
|
||||
unawaited(_loadRewardForLevel(_selectedLevel + 1));
|
||||
unawaited(_loadCrewForLevel(selectedLevelNumber));
|
||||
unawaited(_loadRewardForLevel(selectedLevelNumber));
|
||||
try {
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
@ -430,13 +504,29 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (!contents.enabled) {
|
||||
setState(() {
|
||||
_levelContents = contents;
|
||||
_configRewardGroupsByLevel = {};
|
||||
});
|
||||
_rocketUiDebug('rocket level contents disabled');
|
||||
return;
|
||||
}
|
||||
final groups = RoomRocketApiMapper.rewardGroupsByLevelFromLevelContents(
|
||||
contents,
|
||||
);
|
||||
final levelItems = RoomRocketApiMapper.levelsFromLevelContents(contents);
|
||||
late final int selectedLevelNumber;
|
||||
setState(() {
|
||||
_levelContents = contents;
|
||||
_configRewardGroupsByLevel = groups;
|
||||
if (!_levelTouched) {
|
||||
_selectedLevel = _resolvedSelectedLevelIndex(levelItems, _status);
|
||||
}
|
||||
selectedLevelNumber = _levelNumberForIndex(levelItems, _selectedLevel);
|
||||
});
|
||||
unawaited(_loadCrewForLevel(selectedLevelNumber));
|
||||
unawaited(_loadRewardForLevel(selectedLevelNumber));
|
||||
_rocketUiDebug(
|
||||
'rocket level contents loaded levels=${contents.levels.length} '
|
||||
'rewardLevels=${groups.keys.toList()}',
|
||||
@ -471,8 +561,11 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final status = _status;
|
||||
final selectedLevel = _selectedLevel.clamp(0, 5).toInt();
|
||||
final selectedLevelNumber = selectedLevel + 1;
|
||||
final levelItems =
|
||||
RoomRocketApiMapper.levelsFromLevelContents(_levelContents) ??
|
||||
RoomRocketApiMapper.levelsFromStatus(status);
|
||||
final selectedLevel = _resolvedSelectedLevelIndex(levelItems, status);
|
||||
final selectedLevelNumber = _levelNumberForIndex(levelItems, selectedLevel);
|
||||
final currentLevelNumber = _levelNumberFromStatus(status);
|
||||
final statusRewardGroups = RoomRocketApiMapper.rewardGroupsFromStatus(
|
||||
status,
|
||||
@ -487,9 +580,6 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
|
||||
: rewardGroupsFromStatus ??
|
||||
configRewardGroups ??
|
||||
const RoomRocketRewardGroups();
|
||||
final levelItems =
|
||||
RoomRocketApiMapper.levelsFromLevelContents(_levelContents) ??
|
||||
RoomRocketApiMapper.levelsFromStatus(status);
|
||||
return RoomRocketDialog(
|
||||
status: status,
|
||||
levels: levelItems,
|
||||
@ -536,13 +626,32 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
|
||||
}
|
||||
|
||||
int _levelIndexFromStatus(SCRoomRocketStatusRes? status) {
|
||||
return ((status?.currentLevel ?? status?.level ?? 1) - 1)
|
||||
.clamp(0, 5)
|
||||
.toInt();
|
||||
return _resolveRoomRocketSelectedLevelIndex(
|
||||
levelItems: RoomRocketApiMapper.levelsFromStatus(status),
|
||||
status: status,
|
||||
selectedLevel: _selectedLevel,
|
||||
levelTouched: false,
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -576,13 +685,17 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
|
||||
|
||||
void _onLevelSelected(int index) {
|
||||
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(() {
|
||||
_levelTouched = true;
|
||||
_selectedLevel = selectedIndex;
|
||||
});
|
||||
unawaited(_loadRewardForLevel(selectedIndex + 1));
|
||||
unawaited(_loadCrewForLevel(selectedIndex + 1));
|
||||
unawaited(_loadRewardForLevel(selectedLevelNumber));
|
||||
unawaited(_loadCrewForLevel(selectedLevelNumber));
|
||||
}
|
||||
|
||||
Future<void> _loadRewardForLevel(int level) async {
|
||||
@ -801,60 +914,75 @@ class _RoomRocketRecordDialogLoaderState
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(_load(reset: true));
|
||||
unawaited(_loadAllRecords());
|
||||
}
|
||||
|
||||
Future<void> _load({bool reset = false}) async {
|
||||
if (_loadingMore || (!reset && (!_hasMore || _loading))) {
|
||||
Future<void> _loadAllRecords() async {
|
||||
if (_loadingMore) {
|
||||
return;
|
||||
}
|
||||
final requestCursor = reset ? 1 : _cursor;
|
||||
setState(() {
|
||||
if (reset) {
|
||||
_loading = true;
|
||||
} else {
|
||||
_loadingMore = true;
|
||||
}
|
||||
_loading = true;
|
||||
_loadingMore = false;
|
||||
_hasMore = true;
|
||||
_cursor = 1;
|
||||
_rawRecords.clear();
|
||||
_records = null;
|
||||
});
|
||||
var requestCursor = 1;
|
||||
final allRecords = <SCRoomRocketRewardRecordRes>[];
|
||||
try {
|
||||
_rocketUiDebug(
|
||||
'request reward-records roomId=${widget.roomId} '
|
||||
'cursor=$requestCursor limit=$_rocketPageSize',
|
||||
);
|
||||
final res = await SCChatRoomRepository().roomRocketRewardRecords(
|
||||
widget.roomId,
|
||||
cursor: requestCursor,
|
||||
limit: _rocketPageSize,
|
||||
);
|
||||
while (mounted) {
|
||||
_rocketUiDebug(
|
||||
'request reward-records roomId=${widget.roomId} '
|
||||
'cursor=$requestCursor limit=$_rocketPageSize',
|
||||
);
|
||||
final res = await SCChatRoomRepository().roomRocketRewardRecords(
|
||||
widget.roomId,
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
final nextRecords = res.records;
|
||||
setState(() {
|
||||
if (reset) {
|
||||
_rawRecords
|
||||
..clear()
|
||||
..addAll(nextRecords);
|
||||
} else {
|
||||
_rawRecords.addAll(nextRecords);
|
||||
}
|
||||
_rawRecords
|
||||
..clear()
|
||||
..addAll(allRecords);
|
||||
_records = RoomRocketApiMapper.recordItemsFromRecords(_rawRecords);
|
||||
_hasMore = nextRecords.length >= _rocketPageSize;
|
||||
if (_hasMore) {
|
||||
_cursor = requestCursor + 1;
|
||||
}
|
||||
_hasMore = false;
|
||||
_cursor = requestCursor;
|
||||
_loading = false;
|
||||
_loadingMore = false;
|
||||
});
|
||||
_rocketUiDebug(
|
||||
'reward-records loaded cursor=$requestCursor '
|
||||
'count=${nextRecords.length} total=${_rawRecords.length} '
|
||||
'hasMore=$_hasMore nextCursor=$_cursor',
|
||||
'reward-records all loaded total=${_rawRecords.length} '
|
||||
'lastCursor=$_cursor',
|
||||
);
|
||||
} catch (error) {
|
||||
_rocketUiDebug('reward-records request failed: $error');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
if (allRecords.isNotEmpty) {
|
||||
_rawRecords
|
||||
..clear()
|
||||
..addAll(allRecords);
|
||||
_records = RoomRocketApiMapper.recordItemsFromRecords(_rawRecords);
|
||||
}
|
||||
_loading = false;
|
||||
_loadingMore = false;
|
||||
_hasMore = false;
|
||||
@ -871,7 +999,7 @@ class _RoomRocketRecordDialogLoaderState
|
||||
loadingText: _loading ? 'Loading data......' : '',
|
||||
isLoadingMore: _loadingMore,
|
||||
hasMore: _hasMore,
|
||||
onLoadMore: () => unawaited(_load()),
|
||||
onLoadMore: null,
|
||||
limitText: 'Show only records from the last 35 days',
|
||||
onClose: widget.onClose,
|
||||
);
|
||||
|
||||
@ -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';
|
||||
|
||||
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 {
|
||||
const RoomRocketPagEffectOverlay({
|
||||
super.key,
|
||||
required this.resource,
|
||||
this.topAvatarUrl,
|
||||
this.topAvatarUrlListenable,
|
||||
this.onCompleted,
|
||||
});
|
||||
|
||||
final String resource;
|
||||
final String? topAvatarUrl;
|
||||
final ValueListenable<String?>? topAvatarUrlListenable;
|
||||
final VoidCallback? onCompleted;
|
||||
|
||||
static bool isPag(String? resource) {
|
||||
@ -50,6 +54,7 @@ class RoomRocketPagEffectOverlay extends StatefulWidget {
|
||||
class _RoomRocketPagEffectOverlayState
|
||||
extends State<RoomRocketPagEffectOverlay> {
|
||||
bool _completed = false;
|
||||
Size? _pagRawSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -65,10 +70,7 @@ class _RoomRocketPagEffectOverlayState
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
_RoomRocketPagTopAvatar(
|
||||
avatarUrl: widget.topAvatarUrl,
|
||||
canvasSize: size,
|
||||
),
|
||||
_buildTopAvatar(size),
|
||||
Positioned.fill(
|
||||
child: _RoomRocketPagView(
|
||||
resource: resource,
|
||||
@ -79,6 +81,7 @@ class _RoomRocketPagEffectOverlayState
|
||||
displayScale: 1,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
memoryTtl: _pagMemoryTtl,
|
||||
onReady: _updatePagRawSize,
|
||||
onCompleted: _complete,
|
||||
defaultBuilder:
|
||||
(_) => const Center(child: SCRotatingDotsLoading()),
|
||||
@ -99,16 +102,48 @@ class _RoomRocketPagEffectOverlayState
|
||||
_completed = true;
|
||||
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 {
|
||||
const _RoomRocketPagTopAvatar({
|
||||
required this.avatarUrl,
|
||||
required this.canvasSize,
|
||||
required this.pagRawSize,
|
||||
});
|
||||
|
||||
final String? avatarUrl;
|
||||
final Size canvasSize;
|
||||
final Size? pagRawSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -116,12 +151,13 @@ class _RoomRocketPagTopAvatar extends StatelessWidget {
|
||||
if (url.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final size = (canvasSize.width * 0.15).clamp(46.0, 66.0).toDouble();
|
||||
return Positioned(
|
||||
left: (canvasSize.width - size) / 2,
|
||||
top: canvasSize.height * 0.098,
|
||||
width: size,
|
||||
height: size,
|
||||
final rect = _mapRoomRocketPagRawRectToCoverCanvas(
|
||||
rawRect: _roomRocketTopAvatarRawRect,
|
||||
rawSize: pagRawSize ?? _roomRocketFallbackPagRawSize,
|
||||
canvasSize: canvasSize,
|
||||
);
|
||||
return Positioned.fromRect(
|
||||
rect: rect,
|
||||
child: ClipOval(
|
||||
child: CustomCachedImage(
|
||||
imageUrl: url,
|
||||
@ -200,6 +236,7 @@ class _RoomRocketPagView extends StatefulWidget {
|
||||
required this.clipBehavior,
|
||||
required this.memoryTtl,
|
||||
this.underlayBuilder,
|
||||
this.onReady,
|
||||
this.onCompleted,
|
||||
this.defaultBuilder,
|
||||
});
|
||||
@ -213,6 +250,7 @@ class _RoomRocketPagView extends StatefulWidget {
|
||||
final Clip clipBehavior;
|
||||
final Duration memoryTtl;
|
||||
final WidgetBuilder? underlayBuilder;
|
||||
final ValueChanged<Size>? onReady;
|
||||
final VoidCallback? onCompleted;
|
||||
final WidgetBuilder? defaultBuilder;
|
||||
|
||||
@ -414,6 +452,7 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
|
||||
'viewSize=${widget.width?.toStringAsFixed(1)}x'
|
||||
'${widget.height?.toStringAsFixed(1)} fit=${widget.fit.name}',
|
||||
);
|
||||
widget.onReady?.call(Size(rawWidth, rawHeight));
|
||||
unawaited(_debugLayers(state, rawWidth, rawHeight));
|
||||
if (!_initialized) {
|
||||
setState(() => _initialized = true);
|
||||
@ -587,6 +626,55 @@ Timer? _schedulePagMemoryReleaseTimer(
|
||||
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
|
||||
String debugNormalizeRoomRocketPagResource(String resource) {
|
||||
return _normalizePagResource(resource);
|
||||
|
||||
@ -35,7 +35,7 @@ class RoomRocketRewardDialog extends StatelessWidget {
|
||||
final emptyText = _translate(
|
||||
localizations,
|
||||
'roomRocketRewardEmptyText',
|
||||
'No rewards earned. Better luck next time.',
|
||||
'No reward',
|
||||
);
|
||||
final contentDirection = Directionality.of(context);
|
||||
final resolvedRewards = _normalizeRewards(
|
||||
@ -257,7 +257,7 @@ class _RewardDialogCanvas extends StatelessWidget {
|
||||
if (isEmpty)
|
||||
_buildEmptyState()
|
||||
else ...[
|
||||
_buildRewardGrid(),
|
||||
rewards.isEmpty ? _buildRewardEmptyState() : _buildRewardGrid(),
|
||||
_buildWinnerList(),
|
||||
],
|
||||
_buildLoadingText(),
|
||||
@ -380,9 +380,6 @@ class _RewardDialogCanvas extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildRewardGrid() {
|
||||
if (rewards.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final columns = math.max(3, (rewards.length / 2).ceil());
|
||||
return Positioned(
|
||||
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() {
|
||||
return Positioned(
|
||||
left: s(12),
|
||||
|
||||
@ -81,7 +81,7 @@ class _MsgItemState extends State<MsgItem> {
|
||||
}
|
||||
|
||||
///火箭用户中奖
|
||||
if (widget.msg.type == SCRoomMsgType.rocketRewardUser) {
|
||||
if (SCRoomMsgType.isRocketRewardUserType(widget.msg.type)) {
|
||||
return _buildRocketRewardMsg(context);
|
||||
}
|
||||
|
||||
@ -692,10 +692,17 @@ class _MsgItemState extends State<MsgItem> {
|
||||
///火箭用户中奖
|
||||
_buildRocketRewardMsg(BuildContext context) {
|
||||
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(
|
||||
TextSpan(
|
||||
text: "${widget.msg.user?.userNickname} ",
|
||||
text: "$nickname ",
|
||||
style: TextStyle(
|
||||
fontSize: sp(12),
|
||||
color: SocialChatTheme.primaryColor,
|
||||
@ -1995,6 +2002,118 @@ class Msg {
|
||||
|
||||
gift =
|
||||
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() {
|
||||
|
||||
@ -49,6 +49,10 @@ class RoomUserInfoCard extends StatefulWidget {
|
||||
class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
static const String _leaveMicActionIconAsset =
|
||||
"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;
|
||||
String roomId = "";
|
||||
@ -85,7 +89,8 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
|
||||
@override
|
||||
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>(
|
||||
builder: (context, ref, child) {
|
||||
@ -100,14 +105,18 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
final canLeaveMic = currentMicIndex > -1;
|
||||
|
||||
if (ref.userCardInfo == null) {
|
||||
return _buildLoadingSheet(sheetHeight);
|
||||
return _buildLoadingSheet(defaultSheetHeight);
|
||||
}
|
||||
|
||||
final dataCard = _activeDataCard(ref.userCardInfo);
|
||||
|
||||
return _buildProfileSheet(
|
||||
context: context,
|
||||
ref: ref,
|
||||
rtcProvider: rtcProvider,
|
||||
sheetHeight: sheetHeight,
|
||||
sheetHeight: defaultSheetHeight,
|
||||
dataCardBackgroundHeight: screenHeight,
|
||||
dataCard: dataCard,
|
||||
isSelf: isSelf,
|
||||
currentMicIndex: currentMicIndex,
|
||||
canLeaveMic: canLeaveMic,
|
||||
@ -123,19 +132,21 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
height: sheetHeight,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
|
||||
child: DecoratedBox(
|
||||
decoration: _sheetDecoration(),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black26,
|
||||
borderRadius: BorderRadius.circular(8.w),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: _buildDefaultSheetBackground()),
|
||||
Center(
|
||||
child: Container(
|
||||
width: 55.w,
|
||||
height: 55.w,
|
||||
decoration: BoxDecoration(
|
||||
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 RtcProvider rtcProvider,
|
||||
required double sheetHeight,
|
||||
required double dataCardBackgroundHeight,
|
||||
required PropsResources? dataCard,
|
||||
required bool isSelf,
|
||||
required num currentMicIndex,
|
||||
required bool canLeaveMic,
|
||||
@ -153,138 +166,215 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
}) {
|
||||
final bottomPadding = MediaQuery.of(context).padding.bottom;
|
||||
final profile = ref.userCardInfo?.userProfile;
|
||||
final dataCard = _activeDataCard(ref.userCardInfo);
|
||||
final canShowReport = currentUserId != widget.userId;
|
||||
final canShowSetting =
|
||||
canShowReport &&
|
||||
ref.userCardInfo?.roomRole != SCRoomRolesType.HOMEOWNER.name &&
|
||||
(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(
|
||||
width: double.infinity,
|
||||
height: sheetHeight,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned.fill(child: _buildSheetBackground(dataCard)),
|
||||
Positioned.fill(
|
||||
child: SingleChildScrollView(
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
Positioned.fill(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
|
||||
child: DecoratedBox(decoration: _sheetDecoration()),
|
||||
),
|
||||
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(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top:
|
||||
-(backgroundHeight - sheetHeight) +
|
||||
sheetHeight * _dataCardBackgroundTranslateFactor,
|
||||
height: backgroundHeight,
|
||||
child: IgnorePointer(child: _buildSheetBackground(dataCard)),
|
||||
),
|
||||
Positioned.fill(
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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() {
|
||||
return const BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Color(0xff18F2B1),
|
||||
Color(0xffE4FFF6),
|
||||
Color(0xffF7FFFC),
|
||||
Color(0xffFFFFFF),
|
||||
],
|
||||
return const BoxDecoration(color: Color(0xff061F1E));
|
||||
}
|
||||
|
||||
Widget _buildDefaultSheetBackground() {
|
||||
return DecoratedBox(
|
||||
decoration: _sheetDecoration(),
|
||||
child: SizedBox.expand(
|
||||
child: Image.asset(
|
||||
_defaultProfileCardBackgroundAsset,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSheetBackground(PropsResources? dataCard) {
|
||||
if (dataCard == null) {
|
||||
return DecoratedBox(decoration: _sheetDecoration());
|
||||
return _buildDefaultSheetBackground();
|
||||
}
|
||||
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.white.withValues(alpha: 0.08),
|
||||
Colors.white.withValues(alpha: 0.22),
|
||||
Colors.white.withValues(alpha: 0.58),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
return SCDataCardResourceView(
|
||||
resource: dataCard,
|
||||
fit: BoxFit.cover,
|
||||
showCoverFallback: false,
|
||||
allowCoverAsResource: false,
|
||||
);
|
||||
}
|
||||
|
||||
@ -325,8 +415,8 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
onTap: () => _openProfile(context),
|
||||
child: head(
|
||||
url: profile?.userAvatar ?? "",
|
||||
width: 70.w,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
width: 105.w,
|
||||
border: Border.all(color: Colors.white, width: 3),
|
||||
headdress: profile?.getHeaddress()?.sourceUrl,
|
||||
headdressCover: profile?.getHeaddress()?.cover,
|
||||
),
|
||||
@ -347,7 +437,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
maxWidth: 116.w,
|
||||
profile?.userNickname ?? "",
|
||||
fontSize: 18.sp,
|
||||
textColor: Colors.black,
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
type: "",
|
||||
needScroll: (profile?.userNickname?.characters.length ?? 0) > 12,
|
||||
@ -381,6 +471,22 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
],
|
||||
),
|
||||
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(
|
||||
idText: profile?.getID() ?? "",
|
||||
showAnimated: profile?.hasSpecialId() ?? false,
|
||||
@ -392,106 +498,62 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
showAnimatedGradientText:
|
||||
profile?.shouldShowColoredSpecialIdText() ?? false,
|
||||
animationTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
normalTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
color: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
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) {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildCountryChip(context, profile),
|
||||
SizedBox(width: 7.w),
|
||||
_buildSexAgeChip(profile),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCountryChip(
|
||||
BuildContext context,
|
||||
SocialChatUserProfile? profile,
|
||||
) {
|
||||
String _countryFlagUrl(BuildContext context, SocialChatUserProfile? profile) {
|
||||
final countryName = profile?.countryName ?? "";
|
||||
final flagUrl =
|
||||
Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).findCountryByName(countryName)?.nationalFlag ??
|
||||
"";
|
||||
try {
|
||||
return Provider.of<SCAppGeneralManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).findCountryByName(countryName)?.nationalFlag ??
|
||||
"";
|
||||
} catch (_) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
return Container(
|
||||
constraints: BoxConstraints(maxWidth: 145.w),
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white.withValues(alpha: 0.55),
|
||||
borderRadius: BorderRadius.circular(16.w),
|
||||
border: Border.all(
|
||||
color: const Color(0xff18F2B1).withValues(alpha: 0.18),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
netImage(
|
||||
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 _buildCountryFlag(String flagUrl) {
|
||||
final trimmedUrl = flagUrl.trim();
|
||||
if (trimmedUrl.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.circular(2.w),
|
||||
child: netImage(
|
||||
url: trimmedUrl,
|
||||
width: 22.w,
|
||||
height: 15.w,
|
||||
fit: BoxFit.cover,
|
||||
noDefaultImg: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSexAgeChip(SocialChatUserProfile? profile) {
|
||||
return Container(
|
||||
height: 26.w,
|
||||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||||
decoration: BoxDecoration(
|
||||
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 _buildSexIcon(SocialChatUserProfile? profile) {
|
||||
if (profile?.userSex == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return xb(profile?.userSex, height: 15.w, color: null);
|
||||
}
|
||||
|
||||
Widget _buildHeaderLongBadgeStrip(
|
||||
@ -547,7 +609,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
return wearBadges;
|
||||
}
|
||||
return ref.userCardInfo?.useBadge
|
||||
?.map(_roomUseBadgeToWearBadge)
|
||||
?.map<WearBadge>(_roomUseBadgeToWearBadge)
|
||||
.where(_hasBadgeUrl)
|
||||
.toList() ??
|
||||
const <WearBadge>[];
|
||||
@ -662,7 +724,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
label,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
textColor: const Color(0xff666666),
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12.sp,
|
||||
lineHeight: 1.1,
|
||||
@ -717,7 +779,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
: SCAppLocalizations.of(context)!.follow,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
textColor: const Color(0xff666666),
|
||||
textColor: Colors.white,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 12.sp,
|
||||
lineHeight: 1.1,
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@ -70,11 +68,6 @@ class _PropsStoreChatboxDetailDialogState
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(width: 15.w),
|
||||
text(
|
||||
widget.res.propsResources?.name ?? "",
|
||||
textColor: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
Spacer(),
|
||||
Image.asset(
|
||||
"sc_images/general/sc_icon_jb.png",
|
||||
|
||||
@ -74,14 +74,7 @@ class _PropsStoreDataCardDetailDialogState
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(width: 15.w),
|
||||
Expanded(
|
||||
child: text(
|
||||
widget.res.propsResources?.name ?? "",
|
||||
textColor: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
maxLines: 1,
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Image.asset(
|
||||
"sc_images/general/sc_icon_jb.png",
|
||||
width: 22.w,
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@ -77,11 +76,6 @@ class _PropsStoreHeaddressDetailDialogState
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(width: 15.w),
|
||||
text(
|
||||
widget.res.propsResources?.name ?? "",
|
||||
textColor: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
Spacer(),
|
||||
Image.asset(
|
||||
"sc_images/general/sc_icon_jb.png",
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.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 '../../../shared/data_sources/models/enum/sc_currency_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 '../../../main.dart';
|
||||
import '../../components/sc_debounce_widget.dart';
|
||||
@ -76,11 +74,6 @@ class _PropsStoreMountainsDetailDialogState
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(width: 15.w),
|
||||
text(
|
||||
widget.res.propsResources?.name ?? "",
|
||||
textColor: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
Spacer(),
|
||||
Image.asset(
|
||||
"sc_images/general/sc_icon_jb.png",
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@ -76,11 +75,6 @@ class _PropsStoreThemeDetailDialogState
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(width: 15.w),
|
||||
text(
|
||||
widget.res.propsResources?.name ?? "",
|
||||
textColor: Colors.white,
|
||||
fontSize: 12.sp,
|
||||
),
|
||||
Spacer(),
|
||||
Image.asset(
|
||||
"sc_images/general/sc_icon_jb.png",
|
||||
|
||||
BIN
sc_images/room/sc_bg_room_user_profile_card_default.png
Normal file
BIN
sc_images/room/sc_bg_room_user_profile_card_default.png
Normal file
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 |
45
test/room_rocket_dialog_test.dart
Normal file
45
test/room_rocket_dialog_test.dart
Normal 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
43
test/room_rocket_message_test.dart
Normal file
43
test/room_rocket_message_test.dart
Normal 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');
|
||||
});
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:yumi/app/config/app_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';
|
||||
@ -60,6 +61,19 @@ void main() {
|
||||
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', () {
|
||||
final launch = RoomRocketLaunchBroadcastMessage.fromJson({
|
||||
'rocketAnimationResource': {
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import 'package:flutter/material.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/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';
|
||||
|
||||
void main() {
|
||||
@ -27,6 +30,30 @@ void main() {
|
||||
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({
|
||||
|
||||
@ -122,6 +122,37 @@ void main() {
|
||||
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(
|
||||
'uses top-level reward preview for the response level instead of current level',
|
||||
() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user