Compare commits

..

No commits in common. "d5653afa4f14bff8a261199f83940275590779ad" and "660f712ae6817adb6f7d94cca11c6c5a4e2b957b" have entirely different histories.

46 changed files with 722 additions and 2692 deletions

View File

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

View File

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

View File

@ -236,8 +236,8 @@
"about": "About",
"aboutUs": "About Us",
"theme": "Theme",
"wealthLevel": "Rich Level",
"userLevel": "Charm Level",
"wealthLevel": "Wealth Level",
"userLevel": "User Level",
"goToUpload": "Go to upload",
"logout": "Logout",
"luck": "Luck",
@ -569,8 +569,6 @@
"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",
@ -726,7 +724,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 reward",
"roomRocketRewardEmptyText": "No rewards earned. Better luck next time.",
"roomRocketRecordEmptyText": "No reward records",
"launchRocket": "Launch Rocket",
"roomRocketFuelFullNotice": "Rocket fuel is full! Launching in {1}s!"

View File

@ -237,7 +237,7 @@
"aboutUs": "À propos de nous",
"theme": "Thème",
"wealthLevel": "Niveau de richesse",
"userLevel": "Niveau de charme",
"userLevel": "Niveau utilisateur",
"goToUpload": "Aller au téléchargement",
"logout": "Déconnexion",
"luck": "Chance",
@ -569,8 +569,6 @@
"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",

View File

@ -237,7 +237,7 @@
"aboutUs": "Chi siamo",
"theme": "Tema",
"wealthLevel": "Livello di ricchezza",
"userLevel": "Livello di fascino",
"userLevel": "Livello utente",
"goToUpload": "Vai al caricamento",
"logout": "Esci",
"luck": "Fortuna",
@ -569,8 +569,6 @@
"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",

View File

@ -237,7 +237,7 @@
"aboutUs": "Sobre nós",
"theme": "Tema",
"wealthLevel": "Nível de riqueza",
"userLevel": "Nível de charme",
"userLevel": "Nível de usuário",
"goToUpload": "Ir para carregar",
"logout": "Sair",
"luck": "Sorte",
@ -569,8 +569,6 @@
"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",

View File

@ -225,8 +225,8 @@
"about": "Hakkında",
"aboutUs": "Hakkımızda",
"theme": "Tema",
"wealthLevel": "Zenginlik Seviyesi",
"userLevel": "Çekicilik Seviyesi",
"wealthLevel": "Servet Seviyesi",
"userLevel": "Kullanıcı Seviyesi",
"goToUpload": "Yüklemeye Git",
"logout": ıkış Yap",
"luck": "Şans",
@ -566,12 +566,10 @@
"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",
"richLevelUpgradeAnswer": "Servetin her şeyi fethetsin",
"charmLevelUpgradeAnswer": "Başkalarını cazibene boyun eğdir",
"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",
"higherLevelFancierAvatarFrame": "Daha yüksek seviye, daha şık rozetler/profil çerçevesi",
"all": "Tümü",
"gift": "Hediye",
"chat": "Sohbet",

View File

@ -79,10 +79,6 @@ 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";
@ -133,8 +129,4 @@ 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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -32,6 +32,7 @@ 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';
@ -579,8 +580,30 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
);
}
Widget _buildProfileDataCardBackground(PropsResources? _) {
return const ColoredBox(color: _profileBg);
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],
),
),
),
],
);
}
PropsResources? _activeDataCard(SocialChatUserProfile? profile) {

View File

@ -151,7 +151,7 @@ class _ProfileWallOfHonorsPageState extends State<ProfileWallOfHonorsPage> {
),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisExtent: 96.w,
mainAxisExtent: 132.w,
mainAxisSpacing: 14.w,
crossAxisSpacing: 12.w,
),
@ -161,26 +161,39 @@ class _ProfileWallOfHonorsPageState extends State<ProfileWallOfHonorsPage> {
Widget _buildBadgeItem(WearBadge badge) {
final url = _badgeUrl(badge);
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),
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,
),
),
child: netImage(
url: url,
width: 68.w,
height: 68.w,
fit: BoxFit.contain,
noDefaultImg: true,
SizedBox(height: 8.w),
text(
name.isEmpty ? "--" : name,
fontSize: 12,
textColor: Colors.white,
fontWeight: FontWeight.w400,
maxLines: 2,
textAlign: TextAlign.center,
),
),
],
);
}

View File

@ -13,7 +13,6 @@ 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});
@ -59,6 +58,7 @@ 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,6 +141,11 @@ class _VipDetailPageState extends State<VipDetailPage> {
_selectedLevel = level;
_preview = null;
});
_logSelectedLevelResources(
level,
config: _levelConfigFor(level),
status: _status,
);
_loadPreviewForLevel(level);
}
@ -196,7 +201,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
await _loadVipData(showLoading: false);
}
} catch (error) {
debugPrint('VIP purchase failed: $error');
} finally {
SCLoadingManager.hide();
if (mounted) {
@ -210,6 +214,56 @@ 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(
@ -470,8 +524,6 @@ class _VipDetailPageState extends State<VipDetailPage> {
Widget _buildLevelCard() {
final assetLevel = _assetLevel;
final backgroundCard = _selectedBackgroundCard;
final effectImage = _selectedEffectImage ?? _selectedLongBadge;
return Center(
child: SizedBox(
width: 351.w,
@ -480,23 +532,18 @@ class _VipDetailPageState extends State<VipDetailPage> {
clipBehavior: Clip.none,
children: [
Positioned.fill(
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,
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',
),
fit: BoxFit.fill,
),
),
),
@ -528,8 +575,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
PositionedDirectional(
end: 24.w,
top: -36.w,
child: _buildDynamicResourceVisual(
resource: effectImage,
child: _buildResourceImage(
resource: _selectedBadge,
width: 135.w,
height: 135.w,
fallbackIcon: Icons.workspace_premium_outlined,
@ -541,27 +588,6 @@ 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(
@ -687,14 +713,9 @@ class _VipDetailPageState extends State<VipDetailPage> {
);
}
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 _selectedBadge => _selectedResource(
fromConfig: (config) => config.badge,
fromState: (status) => status.badge,
);
SCVipResourceRes? get _selectedAvatarFrame => _selectedResource(
@ -717,16 +738,6 @@ 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,
@ -757,14 +768,8 @@ class _VipDetailPageState extends State<VipDetailPage> {
final items = <_VipFeatureData>[];
_addResourceItem(
items,
resource: _selectedLongBadge,
fallbackTitle: 'Long Badge',
fallbackIcon: Icons.workspace_premium_outlined,
);
_addResourceItem(
items,
resource: _selectedShortBadge,
fallbackTitle: 'Short Badge',
resource: _selectedBadge,
fallbackTitle: 'VIP Badge',
fallbackIcon: Icons.workspace_premium_outlined,
);
_addResourceItem(
@ -792,19 +797,12 @@ 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:
// badges/avatarFrame/entryEffect/chatBubble/floatPicture/backgroundCard.
// badge/avatarFrame/entryEffect/chatBubble/floatPicture.
// Do not render demo-only functional privileges without backend data.
return const <_VipFeatureData>[];
}
@ -846,40 +844,6 @@ 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,

View File

@ -1,7 +1,6 @@
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';
@ -292,20 +291,6 @@ 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 =
@ -319,12 +304,6 @@ 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);
@ -364,10 +343,8 @@ 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;
@ -513,11 +490,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomEntryEffectTimer = null;
_roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null;
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
_releaseRoomRocketLaunchTopAvatarNotifier();
_roomRocketGiftRefreshTimer?.cancel();
_roomRocketGiftRefreshTimer = null;
_cancelRoomRocketEntryReadyFallbacks();
_cancelRoomRocketPostLaunchStatusRefresh();
}
_roomVisualEffectsEnabled = enabled;
@ -526,12 +500,6 @@ 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() {
@ -1812,11 +1780,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (!context.mounted) {
return;
}
final persistedRoomCleanupRequest =
_preparePersistedVoiceRoomCleanupBeforeEntry(
targetRoomId: roomId,
restoreOnlineHeartbeatWhenIdle: false,
);
await cleanupPersistedVoiceRoomSessionBeforeEntry(targetRoomId: roomId);
if (!context.mounted) {
return;
}
if (roomId == currenRoom?.roomProfile?.roomProfile?.id) {
if (_currentRoomIsEntryPreview ||
_roomStartupStatus == RoomStartupStatus.loading) {
@ -1825,9 +1792,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
_startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context);
_startPersistedVoiceRoomCleanupInBackground(
persistedRoomCleanupRequest,
);
return;
}
@ -1866,7 +1830,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_refreshRoomRedPacketListAfterEntry(roomId: roomId);
_startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context);
_startPersistedVoiceRoomCleanupInBackground(persistedRoomCleanupRequest);
if (shouldRejoinRoomRtc) {
unawaited(_rejoinCurrentRoomRtcForSameRoomReuse(roomId));
}
@ -1902,7 +1865,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
);
_startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context);
_startPersistedVoiceRoomCleanupInBackground(persistedRoomCleanupRequest);
notifyListeners();
final entryRequestSerial = ++_roomEntryRequestSerial;
if (preEnteredRoom != null) {
@ -2525,70 +2487,14 @@ 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(cleanupRequest);
final task = _cleanupPersistedVoiceRoomSessionBeforeEntry(
targetRoomId: targetRoomId,
);
_persistedRoomCleanupTask = task;
return task.whenComplete(() {
if (identical(_persistedRoomCleanupTask, task)) {
@ -2597,11 +2503,28 @@ class RealTimeCommunicationManager extends ChangeNotifier {
});
}
Future<void> _cleanupPersistedVoiceRoomSessionBeforeEntry(
_PersistedVoiceRoomCleanupRequest cleanupRequest,
) async {
final staleRoomId = cleanupRequest.staleRoomId;
if (cleanupRequest.clearMarkerOnly) {
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) {
await _clearPersistedRoomMarkerIfMatches(staleRoomId);
return;
}
@ -2622,11 +2545,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
);
if (didClean) {
await _clearPersistedRoomMarkerIfMatches(staleRoomId);
final currentRoomId =
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
if (cleanupRequest.restoreOnlineHeartbeatWhenIdle &&
cleanupRequest.targetRoomId != staleRoomId &&
currentRoomId.isEmpty) {
if ((targetRoomId ?? "").trim() != staleRoomId) {
await SCHeartbeatUtils.scheduleHeartbeat(
SCHeartbeatStatus.ONLINE.name,
false,
@ -2675,7 +2594,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
setRoomVisualEffectsEnabled(true);
if (shouldOpenRoomPage) {
VoiceRoomRoute.openVoiceRoom(context!);
setVoiceRoomRouteVisible(true);
}
unawaited(
_bootstrapEnteredVoiceRoomSession(
@ -2776,7 +2694,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
roomId: roomId,
);
_loadRoomSecondaryData();
_scheduleRoomRocketEntryReadyFallback(roomId);
} catch (error, stackTrace) {
if (!_isActiveRoomStartup(entryRequestSerial, roomId)) {
return;
@ -2889,19 +2806,18 @@ 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();
@ -2957,9 +2873,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) {
roomRocketStatus = res;
notifyListeners();
if (!res.isEnabled) {
return;
}
_scheduleRoomRocketAssetPreload(
(res.roomId ?? currenRoom?.roomProfile?.roomProfile?.id ?? "").trim(),
res,
@ -2980,9 +2893,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
roomRocketStatus = res;
notifyListeners();
if (!res.isEnabled) {
return;
}
_scheduleRoomRocketAssetPreload(resolvedRoomId, res);
}
@ -2995,9 +2905,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (resolvedRoomId.isEmpty) {
return;
}
if (!status.isEnabled) {
return;
}
if (afterRoomEntry) {
RoomRocketAssetPreloader.preloadAfterRoomEntry(
roomId: resolvedRoomId,
@ -3030,42 +2937,6 @@ 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();
@ -3102,7 +2973,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
_roomRocketLaunchAnimationTimer?.cancel();
_releaseRoomRocketLaunchTopAvatarNotifier();
if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible) {
final animationUrl =
@ -3120,7 +2990,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomRocketLaunchAnimationTimer = Timer(launch.displayDuration, () {
_roomRocketLaunchAnimationTimer = null;
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
_releaseRoomRocketLaunchTopAvatarNotifier();
unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
scheduleRoomRocketPostLaunchStatusRefresh(roomId: launch.roomId);
});
@ -3146,11 +3015,6 @@ 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,
@ -3162,12 +3026,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
builder:
(_) => RoomRocketPagEffectOverlay(
resource: animationUrl,
topAvatarUrlListenable: topAvatarNotifier,
topAvatarUrl: _roomRocketTop1AvatarUrl(),
onCompleted: () {
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
_roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null;
_releaseRoomRocketLaunchTopAvatarNotifier(topAvatarNotifier);
unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
scheduleRoomRocketPostLaunchStatusRefresh(
roomId: launch.roomId,
@ -3184,21 +3047,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
);
}
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,
) {
String? _roomRocketTop1AvatarUrl() {
final kings = roomRocketStatus?.rocketKings ?? const [];
if (kings.isEmpty) {
return null;
}
@ -3215,90 +3065,6 @@ 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,
) {
@ -3601,6 +3367,9 @@ 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();
@ -3636,6 +3405,9 @@ 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();
@ -3841,8 +3613,6 @@ 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

View File

@ -37,8 +37,6 @@ class SCRoomRocketStatusRes {
bool? hasClaimed,
num? myContribution,
num? myContributionRate,
bool? configured,
bool? enabled,
int? roundNo,
bool? displayRound,
int? currentLevel,
@ -71,8 +69,6 @@ class SCRoomRocketStatusRes {
_hasClaimed = hasClaimed;
_myContribution = myContribution;
_myContributionRate = myContributionRate;
_configured = configured;
_enabled = enabled;
_roundNo = roundNo;
_displayRound = displayRound;
_currentLevel = currentLevel;
@ -91,15 +87,6 @@ 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']);
@ -232,8 +219,6 @@ class SCRoomRocketStatusRes {
bool? _hasClaimed;
num? _myContribution;
num? _myContributionRate;
bool? _configured;
bool? _enabled;
int? _roundNo;
bool? _displayRound;
int? _currentLevel;
@ -282,17 +267,6 @@ 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;
@ -397,8 +371,6 @@ 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;

View File

@ -110,14 +110,10 @@ 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;
@ -129,14 +125,10 @@ 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) {
@ -152,14 +144,10 @@ 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;
@ -172,14 +160,10 @@ 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;
@ -199,11 +183,7 @@ class SCVipStatusRes {
String? get expireAt => _expireAt;
SCVipResourceRes? get badge => longBadge;
SCVipResourceRes? get longBadge => _longBadge ?? _badge;
SCVipResourceRes? get shortBadge => _shortBadge;
SCVipResourceRes? get badge => _badge;
SCVipResourceRes? get avatarFrame => _avatarFrame;
@ -213,10 +193,6 @@ 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();
@ -240,17 +216,11 @@ class SCVipStatusRes {
map['displayName'] = _displayName;
map['startAt'] = _startAt;
map['expireAt'] = _expireAt;
if (badge != null) map['badge'] = badge?.toJson();
if (longBadge != null) map['longBadge'] = longBadge?.toJson();
if (_shortBadge != null) map['shortBadge'] = _shortBadge?.toJson();
if (_badge != null) map['badge'] = _badge?.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;
}
}
@ -264,14 +234,10 @@ 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,
}) {
@ -282,14 +248,10 @@ 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;
}
@ -304,14 +266,10 @@ 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']);
}
@ -323,14 +281,10 @@ 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;
@ -346,11 +300,7 @@ class SCVipLevelConfigRes {
num? get priceGold => _priceGold;
SCVipResourceRes? get badge => longBadge;
SCVipResourceRes? get longBadge => _longBadge ?? _badge;
SCVipResourceRes? get shortBadge => _shortBadge;
SCVipResourceRes? get badge => _badge;
SCVipResourceRes? get avatarFrame => _avatarFrame;
@ -360,10 +310,6 @@ class SCVipLevelConfigRes {
SCVipResourceRes? get floatPicture => _floatPicture;
SCVipResourceRes? get backgroundCard => _backgroundCard;
SCVipResourceRes? get effectImage => _effectImage;
bool? get canPurchase => _canPurchase;
num? get payableGold => _payableGold;
@ -386,17 +332,11 @@ class SCVipLevelConfigRes {
map['enabled'] = _enabled;
map['durationDays'] = _durationDays;
map['priceGold'] = _priceGold;
if (badge != null) map['badge'] = badge?.toJson();
if (longBadge != null) map['longBadge'] = longBadge?.toJson();
if (_shortBadge != null) map['shortBadge'] = _shortBadge?.toJson();
if (_badge != null) map['badge'] = _badge?.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;

View File

@ -1,16 +1,13 @@
// 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,
}
enum SCPropsType {
AVATAR_FRAME,
FLOAT_PICTURE,
DATA_CARD,
CHAT_BUBBLE,
RIDE,
LAYOUT,
THEME,
BADGE,
NOBLE_VIP,
FRAGMENTS,
RED_PACKET,
}

View File

@ -57,18 +57,15 @@ 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 &&
!hasLocalVirtualHeaddress;
hasAnimatedHeaddress && SCGlobalConfig.allowsHighCostAnimations;
final bool shouldShowStaticHeaddressFallback =
hasAnimatedHeaddress &&
(!SCGlobalConfig.allowsHighCostAnimations || hasLocalVirtualHeaddress) &&
!SCGlobalConfig.allowsHighCostAnimations &&
headdressCoverUrl.isNotEmpty;
final bool hasHeaddress =
headdressUrl.isNotEmpty || shouldShowStaticHeaddressFallback;
@ -208,11 +205,6 @@ 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,

View File

@ -23,8 +23,6 @@ class SCDataCardResourceView extends StatelessWidget {
this.fit = BoxFit.cover,
this.loop = true,
this.fallback,
this.showCoverFallback = true,
this.allowCoverAsResource = true,
});
final PropsResources? resource;
@ -35,8 +33,6 @@ 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) ||
@ -54,22 +50,15 @@ 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 || !allowCoverAsResource ? source : cover;
final emptyFallback = SizedBox(
width: resolvedWidth,
height: resolvedHeight,
);
final resourceUrl = source.isNotEmpty ? source : cover;
final fallbackView =
fallback ??
(showCoverFallback
? _buildImage(
cover,
width: resolvedWidth,
height: resolvedHeight,
fit: fit,
)
: emptyFallback);
_buildImage(
cover,
width: resolvedWidth,
height: resolvedHeight,
fit: fit,
);
if (resourceUrl.isEmpty) {
return fallbackView;
@ -341,12 +330,10 @@ 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) {
@ -356,11 +343,8 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
oldWidget.fit != widget.fit) {
_playToken++;
_stop();
if (_failed || _started) {
setState(() {
_failed = false;
_started = false;
});
if (_failed) {
setState(() => _failed = false);
}
_play();
}
@ -378,32 +362,15 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
_controller = controller;
controller.setAnimListener(
onFailed: (_, __, ___) => _markFailed(),
onVideoComplete: _handleVideoComplete,
onVideoStart: () {
if (mounted && (_failed || !_started)) {
setState(() {
_failed = false;
_started = true;
});
if (mounted && _failed) {
setState(() => _failed = false);
}
},
);
_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();
@ -423,7 +390,7 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
await controller.setMute(true);
await controller.enableVersion1(true);
await controller.setVideoMode(_legacyRgbLeftAlphaRightMode);
await controller.setLoop(widget.loop ? _longRunningLoopCount : 0);
await controller.setLoop(widget.loop ? -1 : 0);
await controller.setScaleType(_scaleTypeForFit(widget.fit).key);
if (!_isCurrent(token, controller)) {
return;
@ -484,10 +451,7 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
if (!mounted || _failed) {
return;
}
setState(() {
_failed = true;
_started = false;
});
setState(() => _failed = true);
}
ScaleType _scaleTypeForFit(BoxFit fit) {
@ -518,7 +482,7 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
if (!_failed)
VapView(
scaleType: _scaleTypeForFit(widget.fit),
repeat: widget.loop ? _longRunningLoopCount : 0,
repeat: widget.loop ? -1 : 0,
mute: true,
onViewCreated: _handleViewCreated,
),

View File

@ -209,20 +209,6 @@ 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,
@ -463,7 +449,7 @@ class _RocketCanvas extends StatelessWidget {
top: s(120),
width: s(_designWidth),
child: _GradientText(
_roomRocketTitleText(level: selectedLevelItem.level, round: round),
'Round $round Today',
fontSize: s(16),
fontWeight: FontWeight.w600,
textAlign: TextAlign.center,
@ -477,11 +463,31 @@ class _RocketCanvas extends StatelessWidget {
top: s(212),
width: s(40),
height: s(190),
child: _LevelRail(
scale: scale,
levels: levels,
selectedLevel: selectedLevel,
onLevelSelected: onLevelSelected,
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,
),
),
);
},
),
),
);
}
@ -1025,12 +1031,9 @@ class _LevelBadge extends StatelessWidget {
item.label,
fontSize: 10 * scale,
color:
selected
? const Color(0xFFA5FFE3)
: item.enabled
item.enabled
? Colors.white
: Colors.white.withValues(alpha: 0.52),
fontWeight: selected ? FontWeight.w700 : FontWeight.w500,
),
),
],
@ -1039,118 +1042,6 @@ 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,
@ -1508,45 +1399,28 @@ 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 = LinearGradient(
..shader = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors:
selected
? const [Color(0xFF10DFA8), Color(0xFF07392E)]
: const [Color(0xFF077C60), Color(0xFF08251E)],
colors: [Color(0xFF077C60), Color(0xFF08251E)],
).createShader(Offset.zero & size),
);
canvas.drawPath(
path,
Paint()
..style = PaintingStyle.stroke
..strokeWidth = selected ? size.width / 14 : size.width / 40
..strokeWidth = selected ? size.width / 22 : size.width / 40
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
selected
? Colors.white
: enabled
enabled
? const Color(0xFFB2FBCC)
: const Color(0xFFB2FBCC).withValues(alpha: 0.42),
selected
? const Color(0xFF56FFD0)
: const Color(0xFFB2FBCC).withValues(alpha: 0),
const Color(0xFFB2FBCC).withValues(alpha: 0),
],
).createShader(Offset.zero & size),
);
@ -1680,14 +1554,12 @@ 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) {
@ -1700,7 +1572,6 @@ class _RocketText extends StatelessWidget {
style: TextStyle(
color: color,
fontSize: fontSize,
fontWeight: fontWeight,
height: 1,
decoration: TextDecoration.none,
),

View File

@ -34,24 +34,19 @@ 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) || percent > level.shakeThresholdPercent;
(status?.shake ?? false) ||
(level != null && 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: false,
loading: status == null,
),
);
},
@ -113,55 +108,6 @@ 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,
@ -423,16 +369,9 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
super.initState();
_status = widget.initialStatus;
_selectedLevel = _levelIndexFromStatus(widget.initialStatus);
final initialLevelItems = RoomRocketApiMapper.levelsFromStatus(
widget.initialStatus,
);
final initialLevelNumber = _levelNumberForIndex(
initialLevelItems,
_selectedLevel,
);
_cacheRewardGroupsFromStatus(widget.initialStatus, initialLevelNumber);
_cacheRewardGroupsFromStatus(widget.initialStatus, _selectedLevel + 1);
unawaited(_load());
unawaited(_loadRewardForLevel(initialLevelNumber));
unawaited(_loadRewardForLevel(_selectedLevel + 1));
unawaited(_loadConfigs());
unawaited(_loadRule());
}
@ -448,29 +387,16 @@ 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);
}
selectedLevelNumber = _levelNumberForIndex(levelItems, _selectedLevel);
_cacheRewardGroupsFromStatus(res, selectedLevelNumber);
_cacheRewardGroupsFromStatus(res, _selectedLevel + 1);
});
unawaited(_loadCrewForLevel(selectedLevelNumber));
unawaited(_loadRewardForLevel(selectedLevelNumber));
unawaited(_loadCrewForLevel(_selectedLevel + 1));
unawaited(_loadRewardForLevel(_selectedLevel + 1));
try {
Provider.of<RtcProvider>(
context,
@ -504,29 +430,13 @@ 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()}',
@ -561,11 +471,8 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
@override
Widget build(BuildContext context) {
final status = _status;
final levelItems =
RoomRocketApiMapper.levelsFromLevelContents(_levelContents) ??
RoomRocketApiMapper.levelsFromStatus(status);
final selectedLevel = _resolvedSelectedLevelIndex(levelItems, status);
final selectedLevelNumber = _levelNumberForIndex(levelItems, selectedLevel);
final selectedLevel = _selectedLevel.clamp(0, 5).toInt();
final selectedLevelNumber = selectedLevel + 1;
final currentLevelNumber = _levelNumberFromStatus(status);
final statusRewardGroups = RoomRocketApiMapper.rewardGroupsFromStatus(
status,
@ -580,6 +487,9 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
: rewardGroupsFromStatus ??
configRewardGroups ??
const RoomRocketRewardGroups();
final levelItems =
RoomRocketApiMapper.levelsFromLevelContents(_levelContents) ??
RoomRocketApiMapper.levelsFromStatus(status);
return RoomRocketDialog(
status: status,
levels: levelItems,
@ -626,32 +536,13 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
}
int _levelIndexFromStatus(SCRoomRocketStatusRes? status) {
return _resolveRoomRocketSelectedLevelIndex(
levelItems: RoomRocketApiMapper.levelsFromStatus(status),
status: status,
selectedLevel: _selectedLevel,
levelTouched: false,
);
return ((status?.currentLevel ?? status?.level ?? 1) - 1)
.clamp(0, 5)
.toInt();
}
int _levelNumberFromStatus(SCRoomRocketStatusRes? status) {
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);
return (status?.currentLevel ?? status?.level ?? 1).clamp(1, 99).toInt();
}
void _cacheRewardGroupsFromStatus(SCRoomRocketStatusRes? status, int level) {
@ -685,17 +576,13 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
void _onLevelSelected(int index) {
final selectedIndex = index.clamp(0, 5).toInt();
final levelItems =
RoomRocketApiMapper.levelsFromLevelContents(_levelContents) ??
RoomRocketApiMapper.levelsFromStatus(_status);
final selectedLevelNumber = _levelNumberForIndex(levelItems, selectedIndex);
_rocketUiDebug('select rocket level=$selectedLevelNumber');
_rocketUiDebug('select rocket level=${selectedIndex + 1}');
setState(() {
_levelTouched = true;
_selectedLevel = selectedIndex;
});
unawaited(_loadRewardForLevel(selectedLevelNumber));
unawaited(_loadCrewForLevel(selectedLevelNumber));
unawaited(_loadRewardForLevel(selectedIndex + 1));
unawaited(_loadCrewForLevel(selectedIndex + 1));
}
Future<void> _loadRewardForLevel(int level) async {
@ -914,75 +801,60 @@ class _RoomRocketRecordDialogLoaderState
@override
void initState() {
super.initState();
unawaited(_loadAllRecords());
unawaited(_load(reset: true));
}
Future<void> _loadAllRecords() async {
if (_loadingMore) {
Future<void> _load({bool reset = false}) async {
if (_loadingMore || (!reset && (!_hasMore || _loading))) {
return;
}
final requestCursor = reset ? 1 : _cursor;
setState(() {
_loading = true;
_loadingMore = false;
_hasMore = true;
_cursor = 1;
_rawRecords.clear();
_records = null;
});
var requestCursor = 1;
final allRecords = <SCRoomRocketRewardRecordRes>[];
try {
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 (reset) {
_loading = true;
} else {
_loadingMore = true;
}
});
try {
_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;
setState(() {
_rawRecords
..clear()
..addAll(allRecords);
if (reset) {
_rawRecords
..clear()
..addAll(nextRecords);
} else {
_rawRecords.addAll(nextRecords);
}
_records = RoomRocketApiMapper.recordItemsFromRecords(_rawRecords);
_hasMore = false;
_cursor = requestCursor;
_hasMore = nextRecords.length >= _rocketPageSize;
if (_hasMore) {
_cursor = requestCursor + 1;
}
_loading = false;
_loadingMore = false;
});
_rocketUiDebug(
'reward-records all loaded total=${_rawRecords.length} '
'lastCursor=$_cursor',
'reward-records loaded cursor=$requestCursor '
'count=${nextRecords.length} total=${_rawRecords.length} '
'hasMore=$_hasMore nextCursor=$_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;
@ -999,7 +871,7 @@ class _RoomRocketRecordDialogLoaderState
loadingText: _loading ? 'Loading data......' : '',
isLoadingMore: _loadingMore,
hasMore: _hasMore,
onLoadMore: null,
onLoadMore: () => unawaited(_load()),
limitText: 'Show only records from the last 35 days',
onClose: widget.onClose,
);

View File

@ -11,21 +11,17 @@ 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) {
@ -54,7 +50,6 @@ class RoomRocketPagEffectOverlay extends StatefulWidget {
class _RoomRocketPagEffectOverlayState
extends State<RoomRocketPagEffectOverlay> {
bool _completed = false;
Size? _pagRawSize;
@override
Widget build(BuildContext context) {
@ -70,7 +65,10 @@ class _RoomRocketPagEffectOverlayState
child: Stack(
clipBehavior: Clip.none,
children: [
_buildTopAvatar(size),
_RoomRocketPagTopAvatar(
avatarUrl: widget.topAvatarUrl,
canvasSize: size,
),
Positioned.fill(
child: _RoomRocketPagView(
resource: resource,
@ -81,7 +79,6 @@ class _RoomRocketPagEffectOverlayState
displayScale: 1,
clipBehavior: Clip.hardEdge,
memoryTtl: _pagMemoryTtl,
onReady: _updatePagRawSize,
onCompleted: _complete,
defaultBuilder:
(_) => const Center(child: SCRotatingDotsLoading()),
@ -102,48 +99,16 @@ 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) {
@ -151,13 +116,12 @@ class _RoomRocketPagTopAvatar extends StatelessWidget {
if (url.isEmpty) {
return const SizedBox.shrink();
}
final rect = _mapRoomRocketPagRawRectToCoverCanvas(
rawRect: _roomRocketTopAvatarRawRect,
rawSize: pagRawSize ?? _roomRocketFallbackPagRawSize,
canvasSize: canvasSize,
);
return Positioned.fromRect(
rect: rect,
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,
child: ClipOval(
child: CustomCachedImage(
imageUrl: url,
@ -236,7 +200,6 @@ class _RoomRocketPagView extends StatefulWidget {
required this.clipBehavior,
required this.memoryTtl,
this.underlayBuilder,
this.onReady,
this.onCompleted,
this.defaultBuilder,
});
@ -250,7 +213,6 @@ class _RoomRocketPagView extends StatefulWidget {
final Clip clipBehavior;
final Duration memoryTtl;
final WidgetBuilder? underlayBuilder;
final ValueChanged<Size>? onReady;
final VoidCallback? onCompleted;
final WidgetBuilder? defaultBuilder;
@ -452,7 +414,6 @@ 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);
@ -626,55 +587,6 @@ 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);

View File

@ -35,7 +35,7 @@ class RoomRocketRewardDialog extends StatelessWidget {
final emptyText = _translate(
localizations,
'roomRocketRewardEmptyText',
'No reward',
'No rewards earned. Better luck next time.',
);
final contentDirection = Directionality.of(context);
final resolvedRewards = _normalizeRewards(
@ -257,7 +257,7 @@ class _RewardDialogCanvas extends StatelessWidget {
if (isEmpty)
_buildEmptyState()
else ...[
rewards.isEmpty ? _buildRewardEmptyState() : _buildRewardGrid(),
_buildRewardGrid(),
_buildWinnerList(),
],
_buildLoadingText(),
@ -380,6 +380,9 @@ 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),
@ -422,45 +425,6 @@ 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),

View File

@ -81,7 +81,7 @@ class _MsgItemState extends State<MsgItem> {
}
///
if (SCRoomMsgType.isRocketRewardUserType(widget.msg.type)) {
if (widget.msg.type == SCRoomMsgType.rocketRewardUser) {
return _buildRocketRewardMsg(context);
}
@ -692,17 +692,10 @@ 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: "$nickname ",
text: "${widget.msg.user?.userNickname} ",
style: TextStyle(
fontSize: sp(12),
color: SocialChatTheme.primaryColor,
@ -2002,118 +1995,6 @@ 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() {

View File

@ -49,10 +49,6 @@ 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 = "";
@ -89,8 +85,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
final defaultSheetHeight = screenHeight * _profileSheetHeightFactor;
final sheetHeight = MediaQuery.of(context).size.height * 0.6;
return Consumer<SocialChatUserProfileManager>(
builder: (context, ref, child) {
@ -105,18 +100,14 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
final canLeaveMic = currentMicIndex > -1;
if (ref.userCardInfo == null) {
return _buildLoadingSheet(defaultSheetHeight);
return _buildLoadingSheet(sheetHeight);
}
final dataCard = _activeDataCard(ref.userCardInfo);
return _buildProfileSheet(
context: context,
ref: ref,
rtcProvider: rtcProvider,
sheetHeight: defaultSheetHeight,
dataCardBackgroundHeight: screenHeight,
dataCard: dataCard,
sheetHeight: sheetHeight,
isSelf: isSelf,
currentMicIndex: currentMicIndex,
canLeaveMic: canLeaveMic,
@ -132,21 +123,19 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
height: sheetHeight,
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(18.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: DecoratedBox(
decoration: _sheetDecoration(),
child: Center(
child: Container(
width: 55.w,
height: 55.w,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(8.w),
),
child: CupertinoActivityIndicator(color: Colors.white24),
),
],
),
),
),
);
@ -157,8 +146,6 @@ 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,
@ -166,215 +153,138 @@ 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: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
child: DecoratedBox(decoration: _sheetDecoration()),
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,
),
],
),
),
),
),
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,
),
),
],
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(),
),
);
},
),
),
],
),
),
);
}
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(color: Color(0xff061F1E));
}
Widget _buildDefaultSheetBackground() {
return DecoratedBox(
decoration: _sheetDecoration(),
child: SizedBox.expand(
child: Image.asset(
_defaultProfileCardBackgroundAsset,
fit: BoxFit.cover,
),
return const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xff18F2B1),
Color(0xffE4FFF6),
Color(0xffF7FFFC),
Color(0xffFFFFFF),
],
),
);
}
Widget _buildSheetBackground(PropsResources? dataCard) {
if (dataCard == null) {
return _buildDefaultSheetBackground();
return DecoratedBox(decoration: _sheetDecoration());
}
return SCDataCardResourceView(
resource: dataCard,
fit: BoxFit.cover,
showCoverFallback: false,
allowCoverAsResource: false,
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),
],
),
),
),
],
);
}
@ -415,8 +325,8 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
onTap: () => _openProfile(context),
child: head(
url: profile?.userAvatar ?? "",
width: 105.w,
border: Border.all(color: Colors.white, width: 3),
width: 70.w,
border: Border.all(color: Colors.white, width: 2),
headdress: profile?.getHeaddress()?.sourceUrl,
headdressCover: profile?.getHeaddress()?.cover,
),
@ -437,7 +347,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
maxWidth: 116.w,
profile?.userNickname ?? "",
fontSize: 18.sp,
textColor: Colors.white,
textColor: Colors.black,
fontWeight: FontWeight.bold,
type: "",
needScroll: (profile?.userNickname?.characters.length ?? 0) > 12,
@ -471,22 +381,6 @@ 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,
@ -498,62 +392,106 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
showAnimatedGradientText:
profile?.shouldShowColoredSpecialIdText() ?? false,
animationTextStyle: TextStyle(
color: Colors.white,
color: Colors.black,
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
normalTextStyle: TextStyle(
color: Colors.white,
color: Colors.black,
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)],
],
);
}
String _countryFlagUrl(BuildContext context, SocialChatUserProfile? profile) {
final countryName = profile?.countryName ?? "";
try {
return Provider.of<SCAppGeneralManager>(
context,
listen: false,
).findCountryByName(countryName)?.nationalFlag ??
"";
} catch (_) {
return "";
}
Widget _buildInfoChips(BuildContext context, SocialChatUserProfile? profile) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildCountryChip(context, profile),
SizedBox(width: 7.w),
_buildSexAgeChip(profile),
],
);
}
Widget _buildCountryFlag(String flagUrl) {
final trimmedUrl = flagUrl.trim();
if (trimmedUrl.isEmpty) {
return const SizedBox.shrink();
}
Widget _buildCountryChip(
BuildContext context,
SocialChatUserProfile? profile,
) {
final countryName = profile?.countryName ?? "";
final flagUrl =
Provider.of<SCAppGeneralManager>(
context,
listen: false,
).findCountryByName(countryName)?.nationalFlag ??
"";
return ClipRRect(
borderRadius: BorderRadius.circular(2.w),
child: netImage(
url: trimmedUrl,
width: 22.w,
height: 15.w,
fit: BoxFit.cover,
noDefaultImg: true,
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 _buildSexIcon(SocialChatUserProfile? profile) {
if (profile?.userSex == null) {
return const SizedBox.shrink();
}
return xb(profile?.userSex, height: 15.w, color: null);
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 _buildHeaderLongBadgeStrip(
@ -609,7 +547,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
return wearBadges;
}
return ref.userCardInfo?.useBadge
?.map<WearBadge>(_roomUseBadgeToWearBadge)
?.map(_roomUseBadgeToWearBadge)
.where(_hasBadgeUrl)
.toList() ??
const <WearBadge>[];
@ -724,7 +662,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
label,
maxLines: 2,
textAlign: TextAlign.center,
textColor: Colors.white,
textColor: const Color(0xff666666),
fontWeight: FontWeight.w600,
fontSize: 12.sp,
lineHeight: 1.1,
@ -779,7 +717,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
: SCAppLocalizations.of(context)!.follow,
maxLines: 2,
textAlign: TextAlign.center,
textColor: Colors.white,
textColor: const Color(0xff666666),
fontWeight: FontWeight.w600,
fontSize: 12.sp,
lineHeight: 1.1,

View File

@ -1,3 +1,5 @@
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';
@ -68,6 +70,11 @@ 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",

View File

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

View File

@ -1,3 +1,4 @@
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,6 +77,11 @@ 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",

View File

@ -1,3 +1,4 @@
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';
@ -15,6 +16,7 @@ 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';
@ -74,6 +76,11 @@ 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",

View File

@ -1,3 +1,4 @@
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';
@ -75,6 +76,11 @@ 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",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

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

View File

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

View File

@ -1,5 +1,4 @@
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';
@ -61,19 +60,6 @@ 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': {

View File

@ -1,8 +1,5 @@
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() {
@ -30,30 +27,6 @@ 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({

View File

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