完善功能
This commit is contained in:
parent
8be5c4c334
commit
8bf3d679a7
@ -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",
|
||||
|
||||
@ -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');
|
||||
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -2826,19 +2826,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();
|
||||
@ -2894,6 +2894,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,
|
||||
@ -2914,6 +2917,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
roomRocketStatus = res;
|
||||
notifyListeners();
|
||||
if (!res.isEnabled) {
|
||||
return;
|
||||
}
|
||||
_scheduleRoomRocketAssetPreload(resolvedRoomId, res);
|
||||
}
|
||||
|
||||
@ -2926,6 +2932,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
if (resolvedRoomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (!status.isEnabled) {
|
||||
return;
|
||||
}
|
||||
if (afterRoomEntry) {
|
||||
RoomRocketAssetPreloader.preloadAfterRoomEntry(
|
||||
roomId: resolvedRoomId,
|
||||
|
||||
@ -4,6 +4,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:extended_image/extended_image.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_debouncer/flutter_debouncer.dart';
|
||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||
@ -143,6 +144,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
Duration(seconds: 2),
|
||||
Duration(seconds: 5),
|
||||
];
|
||||
static const Duration _roomRocketRewardAfterLaunchDelay = Duration(
|
||||
milliseconds: 350,
|
||||
);
|
||||
static const List<Duration> _roomRocketRoomReadyRewardRetryDelays = [
|
||||
Duration(milliseconds: 300),
|
||||
Duration(milliseconds: 900),
|
||||
@ -272,6 +276,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
final Map<String, Timer> _roomRocketLaunchEffectTimers = <String, Timer>{};
|
||||
final Map<String, int> _consumedRoomRocketLaunchEffectTimes = <String, int>{};
|
||||
final Set<String> _hiddenCurrentRoomRocketLaunchKeys = <String>{};
|
||||
final Map<String, int> _roomRocketRewardHoldUntilTimes = <String, int>{};
|
||||
final Map<String, Timer> _roomRocketRewardHoldTimers = <String, Timer>{};
|
||||
Timer? _roomRocketRewardPopupRetryTimer;
|
||||
Timer? _roomRocketRoomReadyRewardRetryTimer;
|
||||
int get currentLuckGiftBurstPlaybackToken => _luckGiftPushPlaybackToken;
|
||||
@ -1162,6 +1168,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
case SCRoomMsgType.roomRedPacketClaim:
|
||||
return true;
|
||||
case SCRoomMsgType.rocketRewardUser:
|
||||
case SCRoomMsgType.voiceRoomRocketRewardUser:
|
||||
return true;
|
||||
case SCRoomMsgType.rocketLaunchNotice:
|
||||
return true;
|
||||
@ -1195,7 +1202,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
_hasRoomRedPacketClaimMessage(msg)) {
|
||||
return;
|
||||
}
|
||||
if (msg.type == SCRoomMsgType.rocketRewardUser &&
|
||||
if (SCRoomMsgType.isRocketRewardUserType(msg.type) &&
|
||||
_hasRoomRocketRewardMessage(msg)) {
|
||||
return;
|
||||
}
|
||||
@ -1235,7 +1242,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
roomChatMsgList.removeAt(roomChatMsgList.length - 1);
|
||||
}
|
||||
msgChatListener?.call(msg);
|
||||
} else if (msg.type == SCRoomMsgType.rocketRewardUser) {
|
||||
} else if (SCRoomMsgType.isRocketRewardUserType(msg.type)) {
|
||||
roomChatMsgList.insert(0, msg);
|
||||
if (roomChatMsgList.length > 250) {
|
||||
roomChatMsgList.removeAt(roomChatMsgList.length - 1);
|
||||
@ -1388,7 +1395,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
}
|
||||
return roomAllMsgList.any(
|
||||
(item) =>
|
||||
item.type == SCRoomMsgType.rocketRewardUser &&
|
||||
SCRoomMsgType.isRocketRewardUserType(item.type) &&
|
||||
_roomRocketRewardMessageKey(item) == key,
|
||||
);
|
||||
}
|
||||
@ -2091,6 +2098,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
|
||||
void _handleVoiceRoomRocketRewardPopup(dynamic payload) {
|
||||
final data = _broadcastPayloadMap(payload);
|
||||
if (!_isCurrentUserInRoomRocketRewardPopup(data)) {
|
||||
return;
|
||||
}
|
||||
final roomId = _payloadRoomId(data);
|
||||
final currentRoomId = _currentVoiceRoomId();
|
||||
final resolvedRoomId = roomId.isEmpty ? currentRoomId : roomId;
|
||||
@ -2119,6 +2129,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (_isRoomRocketRewardPopupHeld(resolvedRoomId)) {
|
||||
_scheduleRoomRocketRewardPopupAfterHold(
|
||||
resolvedRoomId,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_pendingRoomRocketRewardPopups.remove(resolvedRoomId);
|
||||
_recentRoomRocketRewardProbeTimes.remove(resolvedRoomId);
|
||||
@ -2130,6 +2147,87 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
bool _isCurrentUserInRoomRocketRewardPopup(Map<String, dynamic> payload) {
|
||||
final userIds = _roomRocketRewardPopupUserIds(payload);
|
||||
if (userIds == null) {
|
||||
return true;
|
||||
}
|
||||
if (userIds.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final profile = AccountStorage().getCurrentUser()?.userProfile;
|
||||
final currentKeys = <String>{
|
||||
profile?.id?.trim() ?? '',
|
||||
profile?.account?.trim() ?? '',
|
||||
}..removeWhere((item) => item.isEmpty);
|
||||
if (currentKeys.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return currentKeys.any(userIds.contains);
|
||||
}
|
||||
|
||||
Set<String>? _roomRocketRewardPopupUserIds(Map<String, dynamic> payload) {
|
||||
for (final key in const [
|
||||
'userIds',
|
||||
'user_ids',
|
||||
'targetUserIds',
|
||||
'target_user_ids',
|
||||
'winnerUserIds',
|
||||
'winner_user_ids',
|
||||
'userId',
|
||||
'user_id',
|
||||
'winnerUserId',
|
||||
'winner_user_id',
|
||||
'rewardUserId',
|
||||
'reward_user_id',
|
||||
'receiverUserId',
|
||||
'receiver_user_id',
|
||||
'targetUserId',
|
||||
'target_user_id',
|
||||
'toUserId',
|
||||
'to_user_id',
|
||||
]) {
|
||||
if (payload.containsKey(key)) {
|
||||
return _payloadStringSet(payload[key]);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Set<String> _payloadStringSet(dynamic value) {
|
||||
if (value == null) {
|
||||
return <String>{};
|
||||
}
|
||||
if (value is Iterable && value is! String) {
|
||||
return value
|
||||
.map((item) => item?.toString().trim() ?? '')
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toSet();
|
||||
}
|
||||
if (value is Map) {
|
||||
return value.values
|
||||
.map((item) => item?.toString().trim() ?? '')
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toSet();
|
||||
}
|
||||
final raw = value.toString().trim();
|
||||
if (raw.isEmpty) {
|
||||
return <String>{};
|
||||
}
|
||||
if (raw.startsWith('[') || raw.startsWith('{')) {
|
||||
try {
|
||||
return _payloadStringSet(jsonDecode(raw));
|
||||
} catch (_) {
|
||||
return <String>{};
|
||||
}
|
||||
}
|
||||
return raw
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.where((item) => item.isNotEmpty)
|
||||
.toSet();
|
||||
}
|
||||
|
||||
void handleVoiceRoomReadyForRocketRewards(String roomId, {int attempt = 0}) {
|
||||
final normalizedRoomId = roomId.trim();
|
||||
if (normalizedRoomId.isEmpty) {
|
||||
@ -2166,6 +2264,15 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
_recentRoomRocketRewardProbeTimes.remove(normalizedRoomId);
|
||||
_roomRocketRoomReadyRewardRetryTimer?.cancel();
|
||||
_roomRocketRoomReadyRewardRetryTimer = null;
|
||||
if (_isRoomRocketRewardPopupHeld(normalizedRoomId)) {
|
||||
_scheduleRoomRocketRewardPopupAfterHold(
|
||||
normalizedRoomId,
|
||||
initialRoomRecords:
|
||||
pending?.initialRoomRecords ??
|
||||
const <SCRoomRocketRewardRecordRes>[],
|
||||
);
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
_showRoomRocketRewardPopupDialog(
|
||||
normalizedRoomId,
|
||||
@ -2225,6 +2332,113 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
_recentRoomRocketRewardProbeTimes[normalizedRoomId] = now;
|
||||
}
|
||||
|
||||
void _holdRoomRocketRewardPopupUntilLaunchEnds(
|
||||
RoomRocketLaunchBroadcastMessage launch,
|
||||
) {
|
||||
if (!launch.isValid) {
|
||||
return;
|
||||
}
|
||||
final normalizedRoomId = launch.roomId.trim();
|
||||
if (normalizedRoomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final holdUntil =
|
||||
now +
|
||||
launch.displayDuration.inMilliseconds +
|
||||
_roomRocketRewardAfterLaunchDelay.inMilliseconds;
|
||||
final existing = _roomRocketRewardHoldUntilTimes[normalizedRoomId] ?? 0;
|
||||
if (holdUntil > existing) {
|
||||
_roomRocketRewardHoldUntilTimes[normalizedRoomId] = holdUntil;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isRoomRocketRewardPopupHeld(String roomId) {
|
||||
final normalizedRoomId = roomId.trim();
|
||||
if (normalizedRoomId.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final holdUntil = _roomRocketRewardHoldUntilTimes[normalizedRoomId] ?? 0;
|
||||
if (holdUntil > now) {
|
||||
return true;
|
||||
}
|
||||
if (holdUntil > 0) {
|
||||
_roomRocketRewardHoldUntilTimes.remove(normalizedRoomId);
|
||||
}
|
||||
return _pendingRoomRocketCountdownLaunches.containsKey(normalizedRoomId);
|
||||
}
|
||||
|
||||
int _roomRocketRewardHoldUntil(String roomId) {
|
||||
final normalizedRoomId = roomId.trim();
|
||||
if (normalizedRoomId.isEmpty) {
|
||||
return 0;
|
||||
}
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
var holdUntil = _roomRocketRewardHoldUntilTimes[normalizedRoomId] ?? 0;
|
||||
final launch = _pendingRoomRocketCountdownLaunches[normalizedRoomId];
|
||||
if (launch != null) {
|
||||
final key = _roomRocketLaunchCountdownKeysByRoom[normalizedRoomId];
|
||||
final launchDueAt =
|
||||
key == null ? now : _roomRocketLaunchCountdownDueTimes[key] ?? now;
|
||||
final countdownHoldUntil =
|
||||
launchDueAt +
|
||||
launch.displayDuration.inMilliseconds +
|
||||
_roomRocketRewardAfterLaunchDelay.inMilliseconds;
|
||||
if (countdownHoldUntil > holdUntil) {
|
||||
holdUntil = countdownHoldUntil;
|
||||
}
|
||||
}
|
||||
return holdUntil;
|
||||
}
|
||||
|
||||
void _scheduleRoomRocketRewardPopupAfterHold(
|
||||
String roomId, {
|
||||
List<SCRoomRocketRewardRecordRes> initialRoomRecords =
|
||||
const <SCRoomRocketRewardRecordRes>[],
|
||||
}) {
|
||||
final normalizedRoomId = roomId.trim();
|
||||
if (normalizedRoomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_cachePendingRoomRocketRewardPopup(
|
||||
normalizedRoomId,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
);
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final holdUntil = _roomRocketRewardHoldUntil(normalizedRoomId);
|
||||
final delayMs = holdUntil <= now ? 120 : holdUntil - now;
|
||||
_roomRocketRewardHoldTimers[normalizedRoomId]?.cancel();
|
||||
_roomRocketRewardHoldTimers[normalizedRoomId] = Timer(
|
||||
Duration(milliseconds: delayMs),
|
||||
() {
|
||||
_roomRocketRewardHoldTimers.remove(normalizedRoomId);
|
||||
if (_isRoomRocketRewardPopupHeld(normalizedRoomId)) {
|
||||
_scheduleRoomRocketRewardPopupAfterHold(normalizedRoomId);
|
||||
return;
|
||||
}
|
||||
if (!_isCurrentVisibleVoiceRoom(normalizedRoomId)) {
|
||||
_scheduleRoomRocketReadyRewardRetry(normalizedRoomId, 0);
|
||||
return;
|
||||
}
|
||||
final pending = _pendingRoomRocketRewardPopups.remove(normalizedRoomId);
|
||||
_recentRoomRocketRewardProbeTimes.remove(normalizedRoomId);
|
||||
unawaited(
|
||||
_showRoomRocketRewardPopupDialog(
|
||||
normalizedRoomId,
|
||||
initialRoomRecords:
|
||||
pending?.initialRoomRecords ??
|
||||
const <SCRoomRocketRewardRecordRes>[],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
_roomRocketRewardDebug(
|
||||
'defer popup roomId=$normalizedRoomId delayMs=$delayMs '
|
||||
'initialRoomRecords=${initialRoomRecords.length}',
|
||||
);
|
||||
}
|
||||
|
||||
void _pruneRoomRocketRewardFallbacks(int now) {
|
||||
_pendingRoomRocketRewardPopups.removeWhere(
|
||||
(_, pending) => now - pending.receivedAt > _roomRocketRewardPendingTtlMs,
|
||||
@ -2241,6 +2455,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
_hiddenCurrentRoomRocketLaunchKeys.clear();
|
||||
_roomRocketRoomReadyRewardRetryTimer?.cancel();
|
||||
_roomRocketRoomReadyRewardRetryTimer = null;
|
||||
for (final timer in _roomRocketRewardHoldTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_roomRocketRewardHoldTimers.clear();
|
||||
_roomRocketRewardHoldUntilTimes.clear();
|
||||
_clearRoomRocketLaunchCountdownState();
|
||||
}
|
||||
|
||||
@ -2322,15 +2541,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
final pending = _pendingRoomRocketRewardPopups.remove(launch.roomId);
|
||||
_recentRoomRocketRewardProbeTimes.remove(launch.roomId);
|
||||
_markRoomRocketLaunchEffectConsumed(launch);
|
||||
_holdRoomRocketRewardPopupUntilLaunchEnds(launch);
|
||||
rtcProvider.handleRoomRocketLaunchBroadcast(launch);
|
||||
if (shouldFetchReward) {
|
||||
unawaited(
|
||||
_showRoomRocketRewardPopupDialog(
|
||||
launch.roomId,
|
||||
initialRoomRecords:
|
||||
pending?.initialRoomRecords ??
|
||||
const <SCRoomRocketRewardRecordRes>[],
|
||||
),
|
||||
_scheduleRoomRocketRewardPopupAfterHold(
|
||||
launch.roomId,
|
||||
initialRoomRecords:
|
||||
pending?.initialRoomRecords ??
|
||||
const <SCRoomRocketRewardRecordRes>[],
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2616,6 +2834,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
if (!_isCurrentVisibleVoiceRoom(roomId)) {
|
||||
return;
|
||||
}
|
||||
if (_isRoomRocketRewardPopupHeld(roomId)) {
|
||||
_scheduleRoomRocketRewardPopupAfterHold(
|
||||
roomId,
|
||||
initialRoomRecords: initialRoomRecords,
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final res = await SCChatRoomRepository().roomRocketRewardPopups(roomId);
|
||||
if (!_isCurrentVisibleVoiceRoom(roomId)) {
|
||||
@ -2698,6 +2923,13 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
void _roomRocketRewardDebug(String message) {
|
||||
if (!kDebugMode) {
|
||||
return;
|
||||
}
|
||||
debugPrint('[RoomRocketRewardPopup] $message');
|
||||
}
|
||||
|
||||
RoomRocketRewardUserProfileResolver? _roomRocketRewardUserProfileResolver() {
|
||||
final currentContext = context;
|
||||
if (currentContext == null || !currentContext.mounted) {
|
||||
@ -3278,7 +3510,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
} else if (type == SCRoomMsgType.voiceRoomRocketStatusUpdate) {
|
||||
_handleVoiceRoomRocketStatusUpdate(data["data"]);
|
||||
} else if (type == SCRoomMsgType.voiceRoomRocketRewardPopup) {
|
||||
_handleVoiceRoomRocketRewardPopup(data["data"]);
|
||||
_handleVoiceRoomRocketRewardPopup(data["data"] ?? data);
|
||||
} else if (SCRoomMsgType.isRocketRewardUserType(type?.toString())) {
|
||||
_handleVoiceRoomRocketRewardPopup(data["data"] ?? data);
|
||||
} else if (type == SCRoomMsgType.roomRedPacket) {
|
||||
///红包触发飘屏
|
||||
var fData = data["data"];
|
||||
@ -3376,9 +3610,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
if (data["type"] == SCRoomMsgType.voiceRoomRocketRewardPopup) {
|
||||
_handleVoiceRoomRocketRewardPopup(data["data"]);
|
||||
_handleVoiceRoomRocketRewardPopup(data["data"] ?? data);
|
||||
return;
|
||||
}
|
||||
if (SCRoomMsgType.isRocketRewardUserType(data["type"]?.toString())) {
|
||||
_handleVoiceRoomRocketRewardPopup(data["data"] ?? data);
|
||||
}
|
||||
if (data["type"] == SCRoomMsgType.roomRedPacket) {
|
||||
///房间红包
|
||||
var fData = data["data"];
|
||||
@ -4088,6 +4325,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
_hiddenCurrentRoomRocketLaunchKeys.clear();
|
||||
_roomRocketRewardPopupRetryTimer?.cancel();
|
||||
_roomRocketRewardPopupRetryTimer = null;
|
||||
for (final timer in _roomRocketRewardHoldTimers.values) {
|
||||
timer.cancel();
|
||||
}
|
||||
_roomRocketRewardHoldTimers.clear();
|
||||
_roomRocketRewardHoldUntilTimes.clear();
|
||||
onNewMessageListenerGroupMap.forEach((k, v) {
|
||||
v = null;
|
||||
});
|
||||
|
||||
@ -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,12 @@ class SCRoomRocketStatusRes {
|
||||
|
||||
num? get myContributionRate => _myContributionRate;
|
||||
|
||||
bool? get configured => _configured;
|
||||
|
||||
bool? get enabled => _enabled;
|
||||
|
||||
bool get isEnabled => _enabled != false;
|
||||
|
||||
int? get roundNo => _roundNo;
|
||||
|
||||
bool? get displayRound => _displayRound;
|
||||
@ -371,6 +392,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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -514,10 +514,7 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
if (_failed || !_started)
|
||||
widget.fallback
|
||||
else
|
||||
const ColoredBox(color: Colors.black),
|
||||
widget.fallback,
|
||||
if (!_failed)
|
||||
VapView(
|
||||
scaleType: _scaleTypeForFit(widget.fit),
|
||||
|
||||
@ -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,10 +34,13 @@ 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);
|
||||
final percent = _entryPercent(status);
|
||||
final shouldShake =
|
||||
(status?.shake ?? false) ||
|
||||
(status.shake ?? false) ||
|
||||
(level != null && percent > level.shakeThresholdPercent);
|
||||
return SCDebounceWidget(
|
||||
onTap: () => _showRocketDialog(context, status),
|
||||
@ -46,7 +49,7 @@ class RoomRocketFloatingEntry extends StatelessWidget {
|
||||
progressUrl: level?.progressBarUrl ?? '',
|
||||
progressPercent: percent,
|
||||
shake: shouldShake,
|
||||
loading: status == null,
|
||||
loading: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
@ -108,6 +111,55 @@ class RoomRocketFloatingEntry extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
int debugResolveRoomRocketSelectedLevelIndex({
|
||||
required List<RoomRocketLevelItem>? levelItems,
|
||||
required SCRoomRocketStatusRes? status,
|
||||
required int selectedLevel,
|
||||
required bool levelTouched,
|
||||
}) {
|
||||
return _resolveRoomRocketSelectedLevelIndex(
|
||||
levelItems: levelItems,
|
||||
status: status,
|
||||
selectedLevel: selectedLevel,
|
||||
levelTouched: levelTouched,
|
||||
);
|
||||
}
|
||||
|
||||
int _resolveRoomRocketSelectedLevelIndex({
|
||||
required List<RoomRocketLevelItem>? levelItems,
|
||||
required SCRoomRocketStatusRes? status,
|
||||
required int selectedLevel,
|
||||
required bool levelTouched,
|
||||
}) {
|
||||
final items = levelItems ?? const <RoomRocketLevelItem>[];
|
||||
final maxIndex = math.max(0, (items.isEmpty ? 6 : items.length) - 1);
|
||||
if (levelTouched) {
|
||||
return selectedLevel.clamp(0, maxIndex).toInt();
|
||||
}
|
||||
final currentLevel = _roomRocketLevelNumberFromStatus(status);
|
||||
final matchedIndex = items.indexWhere((item) => item.level == currentLevel);
|
||||
if (matchedIndex >= 0) {
|
||||
return matchedIndex;
|
||||
}
|
||||
return (currentLevel - 1).clamp(0, maxIndex).toInt();
|
||||
}
|
||||
|
||||
int _roomRocketLevelNumberFromStatus(SCRoomRocketStatusRes? status) {
|
||||
return (status?.currentLevel ?? status?.level ?? 1).clamp(1, 99).toInt();
|
||||
}
|
||||
|
||||
int _roomRocketLevelNumberForIndex(
|
||||
List<RoomRocketLevelItem>? levelItems,
|
||||
int index,
|
||||
) {
|
||||
final items = levelItems ?? const <RoomRocketLevelItem>[];
|
||||
if (index >= 0 && index < items.length) {
|
||||
return items[index].level.clamp(1, 99).toInt();
|
||||
}
|
||||
return (index + 1).clamp(1, 99).toInt();
|
||||
}
|
||||
|
||||
class _RoomRocketEntryButton extends StatefulWidget {
|
||||
const _RoomRocketEntryButton({
|
||||
required this.iconUrl,
|
||||
@ -369,9 +421,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 +446,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 +502,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 +559,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 +578,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 +624,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 +683,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 {
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -52,8 +52,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
static const String _defaultProfileCardBackgroundAsset =
|
||||
"sc_images/room/sc_bg_room_user_profile_card_default.png";
|
||||
static const double _profileSheetHeightFactor = 0.4;
|
||||
static const double _dataCardTranslateDownFactor = 0.4;
|
||||
static const double _defaultBackgroundImageFactor = 0.68;
|
||||
static const double _dataCardBackgroundTranslateFactor = 0.8;
|
||||
|
||||
SocialChatUserProfileManager? userProvider;
|
||||
String roomId = "";
|
||||
@ -90,8 +89,8 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final sheetHeight =
|
||||
MediaQuery.of(context).size.height * _profileSheetHeightFactor;
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
final defaultSheetHeight = screenHeight * _profileSheetHeightFactor;
|
||||
|
||||
return Consumer<SocialChatUserProfileManager>(
|
||||
builder: (context, ref, child) {
|
||||
@ -106,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,
|
||||
@ -154,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,
|
||||
@ -161,7 +166,6 @@ 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 &&
|
||||
@ -199,6 +203,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
|
||||
return _buildDataCardFrame(
|
||||
sheetHeight: sheetHeight,
|
||||
backgroundHeight: dataCardBackgroundHeight,
|
||||
dataCard: dataCard,
|
||||
child: profileContent,
|
||||
);
|
||||
@ -206,6 +211,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
|
||||
Widget _buildDataCardFrame({
|
||||
required double sheetHeight,
|
||||
required double backgroundHeight,
|
||||
required PropsResources dataCard,
|
||||
required Widget child,
|
||||
}) {
|
||||
@ -224,8 +230,10 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
Positioned(
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: sheetHeight * _dataCardTranslateDownFactor,
|
||||
height: sheetHeight,
|
||||
top:
|
||||
-(backgroundHeight - sheetHeight) +
|
||||
sheetHeight * _dataCardBackgroundTranslateFactor,
|
||||
height: backgroundHeight,
|
||||
child: IgnorePointer(child: _buildSheetBackground(dataCard)),
|
||||
),
|
||||
Positioned.fill(
|
||||
@ -348,14 +356,10 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
|
||||
Widget _buildDefaultSheetBackground() {
|
||||
return DecoratedBox(
|
||||
decoration: _sheetDecoration(),
|
||||
child: Center(
|
||||
child: FractionallySizedBox(
|
||||
widthFactor: _defaultBackgroundImageFactor,
|
||||
heightFactor: _defaultBackgroundImageFactor,
|
||||
child: Image.asset(
|
||||
_defaultProfileCardBackgroundAsset,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
child: SizedBox.expand(
|
||||
child: Image.asset(
|
||||
_defaultProfileCardBackgroundAsset,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 311 KiB After Width: | Height: | Size: 70 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 7.1 KiB After Width: | Height: | Size: 95 KiB |
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');
|
||||
});
|
||||
}
|
||||
@ -122,6 +122,22 @@ 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(
|
||||
'uses top-level reward preview for the response level instead of current level',
|
||||
() {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user