This commit is contained in:
roxy 2026-05-15 17:43:49 +08:00
parent bd6d35ee1b
commit 77d1fd13b1
53 changed files with 7021 additions and 2530 deletions

View File

@ -723,5 +723,8 @@
"duration2": "المدة:{1}", "duration2": "المدة:{1}",
"roomRocketNoteTitle": "ملاحظة", "roomRocketNoteTitle": "ملاحظة",
"roomRocketNoteSectionTitle": "في كل مرة يتم فيها شراء مستوى VIP", "roomRocketNoteSectionTitle": "في كل مرة يتم فيها شراء مستوى VIP",
"roomRocketNoteSectionBody": "1. في كل مرة يتم فيها شراء مستوى VIP، تكون صلاحيته 30 يومًا، ويمكن تجديده يدويًا بعد انتهاء الصلاحية." "roomRocketNoteSectionBody": "1. في كل مرة يتم فيها شراء مستوى VIP، تكون صلاحيته 30 يومًا، ويمكن تجديده يدويًا بعد انتهاء الصلاحية.",
"roomRocketRewardEmptyText": "لم تحصل على مكافآت. حظًا أوفر في المرة القادمة.",
"roomRocketRecordEmptyText": "لا توجد سجلات مكافآت",
"launchRocket": "إطلاق الصاروخ"
} }

View File

@ -723,5 +723,8 @@
"duration2": "সময়:{1}", "duration2": "সময়:{1}",
"roomRocketNoteTitle": "নোট", "roomRocketNoteTitle": "নোট",
"roomRocketNoteSectionTitle": "প্রতিবার VIP লেভেল কেনার সময়", "roomRocketNoteSectionTitle": "প্রতিবার VIP লেভেল কেনার সময়",
"roomRocketNoteSectionBody": "1. প্রতিবার VIP লেভেল কেনার পর এটি ৩০ দিনের জন্য বৈধ থাকে এবং মেয়াদ শেষ হলে ম্যানুয়ালি নবায়ন করা যায়।" "roomRocketNoteSectionBody": "1. প্রতিবার VIP লেভেল কেনার পর এটি ৩০ দিনের জন্য বৈধ থাকে এবং মেয়াদ শেষ হলে ম্যানুয়ালি নবায়ন করা যায়।",
"roomRocketRewardEmptyText": "কোনো পুরস্কার পাওয়া যায়নি। পরের বার শুভকামনা।",
"roomRocketRecordEmptyText": "কোনো পুরস্কারের রেকর্ড নেই",
"launchRocket": "রকেট চালু করুন"
} }

View File

@ -723,5 +723,8 @@
"duration2": "Duration:{1}", "duration2": "Duration:{1}",
"roomRocketNoteTitle": "Note", "roomRocketNoteTitle": "Note",
"roomRocketNoteSectionTitle": "Each time a VIP", "roomRocketNoteSectionTitle": "Each time a VIP",
"roomRocketNoteSectionBody": "1. Each time a VIP level is purchased, it is valid for 30 days, and can be manually renewed after expiration." "roomRocketNoteSectionBody": "1. Each time a VIP level is purchased, it is valid for 30 days, and can be manually renewed after expiration.",
"roomRocketRewardEmptyText": "No rewards earned. Better luck next time.",
"roomRocketRecordEmptyText": "No reward records",
"launchRocket": "Launch Rocket"
} }

View File

@ -723,5 +723,8 @@
"duration2": "Süre:{1}", "duration2": "Süre:{1}",
"roomRocketNoteTitle": "Not", "roomRocketNoteTitle": "Not",
"roomRocketNoteSectionTitle": "Her VIP seviyesi satın alındığında", "roomRocketNoteSectionTitle": "Her VIP seviyesi satın alındığında",
"roomRocketNoteSectionBody": "1. Her VIP seviyesi satın alındığında 30 gün geçerlidir ve süresi dolduktan sonra manuel olarak yenilenebilir." "roomRocketNoteSectionBody": "1. Her VIP seviyesi satın alındığında 30 gün geçerlidir ve süresi dolduktan sonra manuel olarak yenilenebilir.",
"roomRocketRewardEmptyText": "Ödül kazanmadın. Bir dahaki sefere bol şans.",
"roomRocketRecordEmptyText": "Ödül kaydı yok",
"launchRocket": "Roketi Fırlat"
} }

View File

@ -189,6 +189,7 @@ PODS:
- FlutterMacOS - FlutterMacOS
- iris_method_channel (0.0.1): - iris_method_channel (0.0.1):
- Flutter - Flutter
- libpag (4.5.52)
- libwebp (1.5.0): - libwebp (1.5.0):
- libwebp/demux (= 1.5.0) - libwebp/demux (= 1.5.0)
- libwebp/mux (= 1.5.0) - libwebp/mux (= 1.5.0)
@ -214,6 +215,9 @@ PODS:
- SwiftyBeaver - SwiftyBeaver
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
- Flutter - Flutter
- pag (0.0.1):
- Flutter
- libpag
- permission_handler_apple (9.3.0): - permission_handler_apple (9.3.0):
- Flutter - Flutter
- PromisesObjC (2.4.0) - PromisesObjC (2.4.0)
@ -275,6 +279,7 @@ DEPENDENCIES:
- iris_method_channel (from `.symlinks/plugins/iris_method_channel/ios`) - iris_method_channel (from `.symlinks/plugins/iris_method_channel/ios`)
- on_audio_query_ios (from `.symlinks/plugins/on_audio_query_ios/ios`) - on_audio_query_ios (from `.symlinks/plugins/on_audio_query_ios/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- pag (from `.symlinks/plugins/pag/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`)
@ -311,6 +316,7 @@ SPEC REPOS:
- GTMAppAuth - GTMAppAuth
- GTMSessionFetcher - GTMSessionFetcher
- HydraAsync - HydraAsync
- libpag
- libwebp - libwebp
- Mantle - Mantle
- nanopb - nanopb
@ -358,6 +364,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/on_audio_query_ios/ios" :path: ".symlinks/plugins/on_audio_query_ios/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
pag:
:path: ".symlinks/plugins/pag/ios"
permission_handler_apple: permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios" :path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_foundation: shared_preferences_foundation:
@ -419,11 +427,13 @@ SPEC CHECKSUMS:
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8 in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8
iris_method_channel: 7d661cf3259b3009ae423508470dbeb9374446ee iris_method_channel: 7d661cf3259b3009ae423508470dbeb9374446ee
libpag: 98742fad4b3ac2a2ee31e383d2483495fb9a5fb4
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
on_audio_query_ios: 28a780e2d0d85d92d500ba6e12c6c8167022b2fa on_audio_query_ios: 28a780e2d0d85d92d500ba6e12c6c8167022b2fa
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
pag: f01aa9017ab0e04a83ba4a20d6070a50f0ac9da8
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851

View File

@ -502,7 +502,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 131; CURRENT_PROJECT_VERSION = 140;
DEVELOPMENT_TEAM = S9X2AJ2US9; DEVELOPMENT_TEAM = S9X2AJ2US9;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -511,7 +511,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.3.1; MARKETING_VERSION = 1.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -693,7 +693,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 131; CURRENT_PROJECT_VERSION = 140;
DEVELOPMENT_TEAM = F33K8VUZ62; DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -702,7 +702,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.3.1; MARKETING_VERSION = 1.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -722,7 +722,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 131; CURRENT_PROJECT_VERSION = 140;
DEVELOPMENT_TEAM = F33K8VUZ62; DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -731,7 +731,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.3.1; MARKETING_VERSION = 1.4.0;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -799,6 +799,8 @@ class SCAppLocalizations {
String get launchedARocket => translate('launchedARocket'); String get launchedARocket => translate('launchedARocket');
String get launchRocket => translate('launchRocket');
String get useCoupontips => translate('useCoupontips'); String get useCoupontips => translate('useCoupontips');
String get joinMemberTips2 => translate('joinMemberTips2'); String get joinMemberTips2 => translate('joinMemberTips2');

View File

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart';
void main() {
runApp(const RoomRocketFloatingPreviewApp());
}
class RoomRocketFloatingPreviewApp extends StatelessWidget {
const RoomRocketFloatingPreviewApp({super.key});
@override
Widget build(BuildContext context) {
return ScreenUtilInit(
designSize: const Size(375, 812),
minTextAdapt: true,
builder: (_, child) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: const _RoomRocketFloatingPreviewPage(),
);
},
);
}
}
class _RoomRocketFloatingPreviewPage extends StatelessWidget {
const _RoomRocketFloatingPreviewPage();
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
Positioned.fill(
child: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFF20100B), Color(0xFF5A2A13)],
),
),
),
),
Transform.translate(
offset: Offset(0, 70.w),
child: FloatingRoomRocketScreenWidget(
message: SCFloatingMessage(
type: 3,
roomId: '123456',
userName: 'Namenamna...',
userAvatarUrl:
'https://images.unsplash.com/photo-1494790108377-be9c29b29330',
durationSeconds: 20,
),
onAnimationCompleted: () {},
),
),
],
),
);
}
}

View File

@ -60,10 +60,12 @@ class GiftPage extends StatefulWidget {
} }
class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin { class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
static const String _backpackGiftTab = SCAppGeneralManager.backpackGiftTab;
static const Duration _comboSendBatchWindow = Duration(milliseconds: 200); static const Duration _comboSendBatchWindow = Duration(milliseconds: 200);
static const List<String> _preferredGiftTabOrder = <String>[ static const List<String> _preferredGiftTabOrder = <String>[
"ALL", "ALL",
_backpackGiftTab,
"ACTIVITY", "ACTIVITY",
"LUCKY_GIFT", "LUCKY_GIFT",
"CP", "CP",
@ -218,7 +220,12 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
listen: false, listen: false,
).giftList(includeCustomized: false); ).giftList(includeCustomized: false);
Provider.of<SCAppGeneralManager>(context, listen: false).giftActivityList(); Provider.of<SCAppGeneralManager>(context, listen: false).giftActivityList();
// Provider.of<GeneralProvider>(context, listen: false).giftBackpack(); unawaited(
Provider.of<SCAppGeneralManager>(
context,
listen: false,
).giftBackpack(forceRefresh: true),
);
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance(); Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
rtcProvider?.roomWheatMap.forEach((k, v) { rtcProvider?.roomWheatMap.forEach((k, v) {
if (v.user != null) { if (v.user != null) {
@ -262,10 +269,23 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
ref.giftByTab.entries ref.giftByTab.entries
.where((entry) => entry.value.isNotEmpty) .where((entry) => entry.value.isNotEmpty)
.where((entry) => entry.key != "CUSTOMIZED") .where((entry) => entry.key != "CUSTOMIZED")
.where((entry) => entry.key != _backpackGiftTab)
.map((entry) => entry.key) .map((entry) => entry.key)
.toList(); .toList();
final orderedTypes = <String>[]; final orderedTypes = <String>[];
if (availableTypes.remove("ALL") ||
ref.isGiftListLoading ||
ref.giftResList.isEmpty) {
orderedTypes.add("ALL");
}
for (final type in _preferredGiftTabOrder) { for (final type in _preferredGiftTabOrder) {
if (type == "ALL") {
continue;
}
if (type == _backpackGiftTab) {
orderedTypes.add(type);
continue;
}
if (availableTypes.remove(type)) { if (availableTypes.remove(type)) {
orderedTypes.add(type); orderedTypes.add(type);
} }
@ -328,7 +348,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
void _ensureCurrentTabSelection(SCAppGeneralManager ref, String tabType) { void _ensureCurrentTabSelection(SCAppGeneralManager ref, String tabType) {
final gifts = ref.giftByTab[tabType] ?? const <SocialChatGiftRes>[]; final gifts = ref.giftByTab[tabType] ?? const <SocialChatGiftRes>[];
if (_matchesCurrentGift(gifts)) { if (_matchesCurrentGift(tabType, gifts)) {
return; return;
} }
final gift = _resolveSelectedGift(tabType, gifts); final gift = _resolveSelectedGift(tabType, gifts);
@ -347,6 +367,13 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
final selectedGift = _selectedGiftByTab[tabType]; final selectedGift = _selectedGiftByTab[tabType];
if (selectedGift?.id != null) { if (selectedGift?.id != null) {
for (final gift in gifts) { for (final gift in gifts) {
if (tabType == _backpackGiftTab) {
final selectedBackpackId = selectedGift!.backpackId;
if ((selectedBackpackId ?? "").isNotEmpty &&
gift.backpackId == selectedBackpackId) {
return gift;
}
}
if (gift.id == selectedGift!.id) { if (gift.id == selectedGift!.id) {
return gift; return gift;
} }
@ -359,12 +386,22 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
return gifts.first; return gifts.first;
} }
bool _matchesCurrentGift(List<SocialChatGiftRes> gifts) { bool _matchesCurrentGift(String tabType, List<SocialChatGiftRes> gifts) {
final currentGiftId = checkedGift?.id; final currentGiftId = checkedGift?.id;
if (currentGiftId == null) { if (currentGiftId == null) {
return gifts.isEmpty; return gifts.isEmpty;
} }
return gifts.any((gift) => gift.id == currentGiftId); for (final gift in gifts) {
if (gift.id != currentGiftId) {
continue;
}
if (tabType != _backpackGiftTab) {
return true;
}
return gift.backpackId == checkedGift?.backpackId &&
gift.backpackQuantity == checkedGift?.backpackQuantity;
}
return false;
} }
String _giftTabLabel(SCAppLocalizations localizations, String tabType) { String _giftTabLabel(SCAppLocalizations localizations, String tabType) {
@ -384,6 +421,8 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
return localizations.customized; return localizations.customized;
case "NSCIONAL_FLAG": case "NSCIONAL_FLAG":
return localizations.country; return localizations.country;
case _backpackGiftTab:
return localizations.bag;
default: default:
return tabType return tabType
.toLowerCase() .toLowerCase()
@ -421,6 +460,10 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
giftTab == SCGiftType.MAGIC.name; giftTab == SCGiftType.MAGIC.name;
} }
bool _usesBackpackGiftEndpoint(SocialChatGiftRes? gift) {
return (gift?.giftTab ?? '').trim().toLowerCase() == _backpackGiftTab;
}
bool _hasValidLuckyGiftStandardId(SocialChatGiftRes gift) { bool _hasValidLuckyGiftStandardId(SocialChatGiftRes gift) {
final standardId = (gift.standardId ?? '').trim(); final standardId = (gift.standardId ?? '').trim();
return standardId.isNotEmpty && standardId != '0'; return standardId.isNotEmpty && standardId != '0';
@ -444,7 +487,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
if (error is DioException) { if (error is DioException) {
final responseData = error.response?.data; final responseData = error.response?.data;
if (responseData is Map<String, dynamic>) { if (responseData is Map) {
final responseMessage = sanitize( final responseMessage = sanitize(
responseData['errorMsg']?.toString() ?? '', responseData['errorMsg']?.toString() ?? '',
); );
@ -488,6 +531,47 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
return 'Gift sending failed, please try again.'; return 'Gift sending failed, please try again.';
} }
String _backpackQuantityInsufficientMessage() {
return 'Gift quantity is insufficient or has been used up.';
}
bool _isBackpackQuantityError(Object error) {
final errorText = _readBackendErrorText(error).toUpperCase();
return errorText.contains('INSUFFICIENT_BALANCE');
}
String _readBackendErrorText(Object error) {
final values = <String>[];
void addValue(dynamic value) {
final text = value?.toString().trim() ?? '';
if (text.isNotEmpty) {
values.add(text);
}
}
if (error is DioException) {
final responseData = error.response?.data;
if (responseData is Map) {
addValue(responseData['errorCodeName']);
addValue(responseData['codeName']);
addValue(responseData['errorCode']);
addValue(responseData['code']);
addValue(responseData['errorMsg']);
addValue(responseData['message']);
} else {
addValue(responseData);
}
addValue(error.message);
addValue(error.error);
} else if (error is NotSuccessException) {
addValue(error.message);
} else {
addValue(error);
}
return values.join(' ');
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<SCAppGeneralManager>( return Consumer<SCAppGeneralManager>(
@ -1108,7 +1192,51 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
final roomId = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? ""; final roomId = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "";
final roomAccount = final roomAccount =
rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
final isLuckyGiftRequest = _usesLuckyGiftEndpoint(selectedGift); final isBackpackGiftRequest = _usesBackpackGiftEndpoint(selectedGift);
final isLuckyGiftRequest =
!isBackpackGiftRequest && _usesLuckyGiftEndpoint(selectedGift);
if (isBackpackGiftRequest) {
final backpackRecordId = (selectedGift.backpackId ?? "").trim();
final giftId = (selectedGift.id ?? "").trim();
if (backpackRecordId.isEmpty || giftId.isEmpty) {
const configError =
'Gift configuration unavailable, please try another gift.';
_giftFxLog(
'giveBackpackGift skipped giftId=${selectedGift.id} '
'giftName=${selectedGift.giftName} '
'backpackId=${selectedGift.backpackId} '
'reason=invalid_backpack_config',
);
SCTts.show(configError);
return null;
}
final availableQuantity =
selectedGift.backpackQuantity?.toInt() ??
int.tryParse(selectedGift.quantity ?? '') ??
0;
final requiredQuantity = selectedNumber * acceptUserIds.length;
if (requiredQuantity > availableQuantity) {
_giftFxLog(
'giveBackpackGift skipped giftId=${selectedGift.id} '
'giftName=${selectedGift.giftName} '
'backpackId=${selectedGift.backpackId} '
'selectedNumber=$selectedNumber '
'acceptCount=${acceptUserIds.length} '
'requiredQuantity=$requiredQuantity '
'availableQuantity=$availableQuantity '
'reason=insufficient_backpack_quantity',
);
SCTts.show(_backpackQuantityInsufficientMessage());
unawaited(
Provider.of<SCAppGeneralManager>(
context,
listen: false,
).giftBackpack(forceRefresh: true),
);
return null;
}
}
if (isLuckyGiftRequest && !_hasValidLuckyGiftStandardId(selectedGift)) { if (isLuckyGiftRequest && !_hasValidLuckyGiftStandardId(selectedGift)) {
const configError = const configError =
@ -1134,6 +1262,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
roomId: roomId, roomId: roomId,
roomAccount: roomAccount, roomAccount: roomAccount,
isLuckyGiftRequest: isLuckyGiftRequest, isLuckyGiftRequest: isLuckyGiftRequest,
isBackpackGiftRequest: isBackpackGiftRequest,
); );
} }
@ -1144,19 +1273,23 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
return; return;
} }
RoomGiftComboSendController().show( if (request.isBackpackGiftRequest) {
request: RoomGiftComboSendRequest( RoomGiftComboSendController().hide();
acceptUserIds: List<String>.from(request.acceptUserIds), } else {
acceptUsers: List<MicRes>.from(request.acceptUsers), RoomGiftComboSendController().show(
gift: request.gift, request: RoomGiftComboSendRequest(
quantity: request.quantity, acceptUserIds: List<String>.from(request.acceptUserIds),
clickCount: request.clickCount, acceptUsers: List<MicRes>.from(request.acceptUsers),
roomId: request.roomId, gift: request.gift,
roomAccount: request.roomAccount, quantity: request.quantity,
isLuckyGiftRequest: request.isLuckyGiftRequest, clickCount: request.clickCount,
), roomId: request.roomId,
executor: _performFloatingComboSend, roomAccount: request.roomAccount,
); isLuckyGiftRequest: request.isLuckyGiftRequest,
),
executor: _performFloatingComboSend,
);
}
if (_supportsComboRequestBatching(request.gift)) { if (_supportsComboRequestBatching(request.gift)) {
_enqueueComboGiftSendRequest(request); _enqueueComboGiftSendRequest(request);
@ -1168,7 +1301,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
} }
bool _supportsComboRequestBatching(SocialChatGiftRes gift) { bool _supportsComboRequestBatching(SocialChatGiftRes gift) {
return _supportsComboFeedback(gift); return !_usesBackpackGiftEndpoint(gift) && _supportsComboFeedback(gift);
} }
void _enqueueComboGiftSendRequest(_GiftSendRequest request) { void _enqueueComboGiftSendRequest(_GiftSendRequest request) {
@ -1315,6 +1448,40 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
'quantity=${request.quantity} ' 'quantity=${request.quantity} '
'clickCount=${request.clickCount}', 'clickCount=${request.clickCount}',
); );
if (request.isBackpackGiftRequest) {
final sentUsers = await _giveBackpackGiftToRecipients(
repository,
request,
);
_giftFxLog(
'$requestName success trigger=$trigger '
'giftId=${request.gift.id} '
'giftName=${request.gift.giftName} '
'giftSourceUrl=${request.gift.giftSourceUrl} '
'special=${request.gift.special} '
'giftTab=${request.gift.giftTab} '
'backpackId=${request.gift.backpackId} '
'backpackQuantity=${request.gift.backpackQuantity} '
'number=${request.quantity} '
'clickCount=${request.clickCount} '
'acceptUserIds=${request.acceptUserIds.join(",")} '
'roomId=${request.roomId} '
'elapsedMs=${stopwatch.elapsedMilliseconds}',
);
if (sentUsers.isNotEmpty) {
final giftBatchId = _createGiftBatchId(request);
sendGiftMsg(
sentUsers,
gift: request.gift,
quantity: request.quantity,
giftBatchId: giftBatchId,
animationCount: request.clickCount,
);
}
await _refreshGiftBackpack();
return;
}
final result = final result =
request.isLuckyGiftRequest request.isLuckyGiftRequest
? await repository.giveLuckyGift( ? await repository.giveLuckyGift(
@ -1374,7 +1541,10 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
} }
profileManager?.updateBalance(result); profileManager?.updateBalance(result);
} catch (e) { } catch (e) {
final errorMessage = _resolveGiftSendErrorMessage(e); final errorMessage =
request.isBackpackGiftRequest && _isBackpackQuantityError(e)
? _backpackQuantityInsufficientMessage()
: _resolveGiftSendErrorMessage(e);
final errorDetails = _describeGiftSendError(e); final errorDetails = _describeGiftSendError(e);
_giftFxLog( _giftFxLog(
'$requestName failed trigger=$trigger ' '$requestName failed trigger=$trigger '
@ -1387,10 +1557,79 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
'details={$errorDetails} ' 'details={$errorDetails} '
'elapsedMs=${stopwatch.elapsedMilliseconds}', 'elapsedMs=${stopwatch.elapsedMilliseconds}',
); );
if (request.isBackpackGiftRequest) {
unawaited(_refreshGiftBackpack());
}
SCTts.show(errorMessage); SCTts.show(errorMessage);
} }
} }
Future<List<MicRes>> _giveBackpackGiftToRecipients(
SCChatRoomRepository repository,
_GiftSendRequest request,
) async {
final sentUsers = <MicRes>[];
for (var i = 0; i < request.acceptUserIds.length; i++) {
final acceptUserId = request.acceptUserIds[i].trim();
if (acceptUserId.isEmpty) {
continue;
}
final acceptUser =
i < request.acceptUsers.length ? request.acceptUsers[i] : null;
try {
await repository.giveBackpackGift(
id: request.gift.backpackId ?? "",
acceptUserId: acceptUserId,
giftId: request.gift.id ?? "",
quantity: request.quantity,
roomId: request.roomId,
seatIndex:
acceptUser?.micIndex ?? _resolveSeatIndexForUserId(acceptUserId),
);
if (acceptUser != null) {
sentUsers.add(acceptUser);
}
} catch (_) {
if (sentUsers.isNotEmpty) {
final giftBatchId = _createGiftBatchId(request);
sendGiftMsg(
sentUsers,
gift: request.gift,
quantity: request.quantity,
giftBatchId: giftBatchId,
animationCount: request.clickCount,
);
}
rethrow;
}
}
return sentUsers;
}
num? _resolveSeatIndexForUserId(String userId) {
final normalizedUserId = userId.trim();
if (normalizedUserId.isEmpty) {
return null;
}
for (final mic in rtcProvider?.roomWheatMap.values ?? const <MicRes>[]) {
if ((mic.user?.id ?? "").trim() == normalizedUserId) {
return mic.micIndex;
}
}
return null;
}
Future<void> _refreshGiftBackpack() async {
final context = navigatorKey.currentState?.context;
if (context == null) {
return;
}
await Provider.of<SCAppGeneralManager>(
context,
listen: false,
).giftBackpack(forceRefresh: true);
}
void sendGiftMsg( void sendGiftMsg(
List<MicRes> acceptUsers, { List<MicRes> acceptUsers, {
required SocialChatGiftRes gift, required SocialChatGiftRes gift,
@ -1628,6 +1867,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
roomId: request.roomId, roomId: request.roomId,
roomAccount: request.roomAccount, roomAccount: request.roomAccount,
isLuckyGiftRequest: request.isLuckyGiftRequest, isLuckyGiftRequest: request.isLuckyGiftRequest,
isBackpackGiftRequest: _usesBackpackGiftEndpoint(request.gift),
), ),
trigger: trigger, trigger: trigger,
); );
@ -1752,7 +1992,11 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
final sortedAcceptUserIds = List<String>.from(request.acceptUserIds) final sortedAcceptUserIds = List<String>.from(request.acceptUserIds)
..sort(); ..sort();
return [ return [
request.isLuckyGiftRequest ? 'lucky' : 'gift', request.isBackpackGiftRequest
? 'backpack'
: request.isLuckyGiftRequest
? 'lucky'
: 'gift',
request.roomId, request.roomId,
request.gift.id ?? '', request.gift.id ?? '',
request.quantity, request.quantity,
@ -1842,6 +2086,7 @@ class _GiftSendRequest {
required this.roomId, required this.roomId,
required this.roomAccount, required this.roomAccount,
required this.isLuckyGiftRequest, required this.isLuckyGiftRequest,
required this.isBackpackGiftRequest,
}); });
final List<String> acceptUserIds; final List<String> acceptUserIds;
@ -1852,15 +2097,30 @@ class _GiftSendRequest {
final String roomId; final String roomId;
final String roomAccount; final String roomAccount;
final bool isLuckyGiftRequest; final bool isLuckyGiftRequest;
final bool isBackpackGiftRequest;
String get requestName => isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift'; String get requestName {
if (isBackpackGiftRequest) {
return 'giveBackpackGift';
}
return isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift';
}
String get batchKey { String get batchKey {
final sortedAcceptUserIds = List<String>.from(acceptUserIds)..sort(); final sortedAcceptUserIds = List<String>.from(acceptUserIds)..sort();
return '${isLuckyGiftRequest ? "lucky" : "gift"}' final type =
'|${gift.id ?? ""}' isBackpackGiftRequest
'|$roomId' ? "backpack"
'|${sortedAcceptUserIds.join(",")}'; : isLuckyGiftRequest
? "lucky"
: "gift";
return [
type,
gift.id ?? "",
gift.backpackId ?? "",
roomId,
sortedAcceptUserIds.join(","),
].join("|");
} }
} }
@ -1875,6 +2135,7 @@ class _PendingGiftSendBatch {
required this.roomId, required this.roomId,
required this.roomAccount, required this.roomAccount,
required this.isLuckyGiftRequest, required this.isLuckyGiftRequest,
required this.isBackpackGiftRequest,
required this.readyAt, required this.readyAt,
}); });
@ -1892,6 +2153,7 @@ class _PendingGiftSendBatch {
roomId: request.roomId, roomId: request.roomId,
roomAccount: request.roomAccount, roomAccount: request.roomAccount,
isLuckyGiftRequest: request.isLuckyGiftRequest, isLuckyGiftRequest: request.isLuckyGiftRequest,
isBackpackGiftRequest: request.isBackpackGiftRequest,
readyAt: readyAt, readyAt: readyAt,
); );
} }
@ -1905,6 +2167,7 @@ class _PendingGiftSendBatch {
final String roomId; final String roomId;
final String roomAccount; final String roomAccount;
final bool isLuckyGiftRequest; final bool isLuckyGiftRequest;
final bool isBackpackGiftRequest;
DateTime readyAt; DateTime readyAt;
_GiftSendRequest toRequest() { _GiftSendRequest toRequest() {
@ -1917,6 +2180,7 @@ class _PendingGiftSendBatch {
roomId: roomId, roomId: roomId,
roomAccount: roomAccount, roomAccount: roomAccount,
isLuckyGiftRequest: isLuckyGiftRequest, isLuckyGiftRequest: isLuckyGiftRequest,
isBackpackGiftRequest: isBackpackGiftRequest,
); );
} }
} }

View File

@ -64,7 +64,7 @@ class _GiftTabPageState extends State<GiftTabPage>
_schedulePageWarmup(ref, gifts, _index); _schedulePageWarmup(ref, gifts, _index);
} }
return gifts.isEmpty return gifts.isEmpty
? (ref.isGiftListLoading ? (ref.isGiftTabLoading(widget.type)
? _buildGiftPageSkeleton() ? _buildGiftPageSkeleton()
: mainEmpty( : mainEmpty(
textColor: Colors.white54, textColor: Colors.white54,
@ -444,19 +444,33 @@ class _GiftTabPageState extends State<GiftTabPage>
), ),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children:
Image.asset( _isBackpackTab
_strategy.getGiftPageGoldCoinIcon(), ? [
width: 14.w, text(
height: 14.w, "x${_formatBackpackQuantity(gift)}",
), fontSize: 10.sp,
SizedBox(width: 3.w), fontWeight: FontWeight.w600,
text( textColor:
"${gift.giftCandy}", widget.isDark
fontSize: 10.sp, ? SocialChatTheme.primaryLight
textColor: widget.isDark ? Colors.white : Colors.black, : SocialChatTheme.primaryColor,
), ),
], ]
: [
Image.asset(
_strategy.getGiftPageGoldCoinIcon(),
width: 14.w,
height: 14.w,
),
SizedBox(width: 3.w),
text(
"${gift.giftCandy}",
fontSize: 10.sp,
textColor:
widget.isDark ? Colors.white : Colors.black,
),
],
), ),
], ],
), ),
@ -470,6 +484,19 @@ class _GiftTabPageState extends State<GiftTabPage>
); );
} }
bool get _isBackpackTab => widget.type == SCAppGeneralManager.backpackGiftTab;
String _formatBackpackQuantity(SocialChatGiftRes gift) {
final quantity = gift.backpackQuantity;
if (quantity != null) {
if (quantity % 1 == 0) {
return quantity.toInt().toString();
}
return quantity.toString();
}
return gift.quantity ?? '0';
}
_indicator(SCAppGeneralManager ref) { _indicator(SCAppGeneralManager ref) {
var size = (ref.giftByTab[widget.type]!.length) / 8; var size = (ref.giftByTab[widget.type]!.length) / 8;
var list = <Widget>[]; var list = <Widget>[];

View File

@ -203,35 +203,17 @@ class _RoomFollowPageState
), ),
), ),
SizedBox(width: 5.w), SizedBox(width: 5.w),
(roomRes roomRes.roomProfile?.extValues?.existsPassword != true
.roomProfile
?.extValues
?.roomSetting
?.password
?.isEmpty ??
false)
? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w) ? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w)
: Image.asset( : Image.asset(
"sc_images/index/sc_icon_room_suo.png", "sc_images/index/sc_icon_room_suo.png",
width: 20.w, width: 20.w,
height: 20.w, height: 20.w,
), ),
(roomRes roomRes.roomProfile?.extValues?.existsPassword != true
.roomProfile
?.extValues
?.roomSetting
?.password
?.isEmpty ??
false)
? SizedBox(width: 3.w) ? SizedBox(width: 3.w)
: Container(height: 10.w), : Container(height: 10.w),
(roomRes roomRes.roomProfile?.extValues?.existsPassword != true
.roomProfile
?.extValues
?.roomSetting
?.password
?.isEmpty ??
false)
? text( ? text(
roomRes.roomProfile?.displayMemberCount ?? "0", roomRes.roomProfile?.displayMemberCount ?? "0",
fontSize: 10.sp, fontSize: 10.sp,
@ -363,8 +345,8 @@ class _RoomFollowPageState
previous.roomProfile?.roomGameIcon == next.roomProfile?.roomGameIcon && previous.roomProfile?.roomGameIcon == next.roomProfile?.roomGameIcon &&
previous.roomProfile?.displayMemberCount == previous.roomProfile?.displayMemberCount ==
next.roomProfile?.displayMemberCount && next.roomProfile?.displayMemberCount &&
previous.roomProfile?.extValues?.roomSetting?.password == previous.roomProfile?.extValues?.existsPassword ==
next.roomProfile?.extValues?.roomSetting?.password; next.roomProfile?.extValues?.existsPassword;
} }
bool _sameRoomLists(List<FollowRoomRes> previous, List<FollowRoomRes> next) { bool _sameRoomLists(List<FollowRoomRes> previous, List<FollowRoomRes> next) {

View File

@ -204,35 +204,17 @@ class _SCRoomHistoryPageState
), ),
), ),
SizedBox(width: 5.w), SizedBox(width: 5.w),
(roomRes roomRes.roomProfile?.extValues?.existsPassword != true
.roomProfile
?.extValues
?.roomSetting
?.password
?.isEmpty ??
false)
? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w) ? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w)
: Image.asset( : Image.asset(
"sc_images/index/sc_icon_room_suo.png", "sc_images/index/sc_icon_room_suo.png",
width: 20.w, width: 20.w,
height: 20.w, height: 20.w,
), ),
(roomRes roomRes.roomProfile?.extValues?.existsPassword != true
.roomProfile
?.extValues
?.roomSetting
?.password
?.isEmpty ??
false)
? SizedBox(width: 3.w) ? SizedBox(width: 3.w)
: Container(height: 10.w), : Container(height: 10.w),
(roomRes roomRes.roomProfile?.extValues?.existsPassword != true
.roomProfile
?.extValues
?.roomSetting
?.password
?.isEmpty ??
false)
? text( ? text(
roomRes.roomProfile?.displayMemberCount ?? "0", roomRes.roomProfile?.displayMemberCount ?? "0",
fontSize: 10.sp, fontSize: 10.sp,
@ -363,8 +345,8 @@ class _SCRoomHistoryPageState
previous.roomProfile?.roomGameIcon == next.roomProfile?.roomGameIcon && previous.roomProfile?.roomGameIcon == next.roomProfile?.roomGameIcon &&
previous.roomProfile?.displayMemberCount == previous.roomProfile?.displayMemberCount ==
next.roomProfile?.displayMemberCount && next.roomProfile?.displayMemberCount &&
previous.roomProfile?.extValues?.roomSetting?.password == previous.roomProfile?.extValues?.existsPassword ==
next.roomProfile?.extValues?.roomSetting?.password; next.roomProfile?.extValues?.existsPassword;
} }
bool _sameRoomLists(List<FollowRoomRes> previous, List<FollowRoomRes> next) { bool _sameRoomLists(List<FollowRoomRes> previous, List<FollowRoomRes> next) {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -43,7 +43,7 @@ class _RoomOnlinePageState
if (dialogContext == null) { if (dialogContext == null) {
return; return;
} }
showBottomInCenterDialog(dialogContext, RoomUserInfoCard(userId: userId)); showBottomInBottomDialog(dialogContext, RoomUserInfoCard(userId: userId));
}); });
} }

View File

@ -98,7 +98,7 @@ class _RoomGiftRankTabPageState
// headdress: userInfo.userProfile?.getHeaddress()?.sourceUrl, // headdress: userInfo.userProfile?.getHeaddress()?.sourceUrl,
), ),
onTap: () { onTap: () {
showBottomInCenterDialog( showBottomInBottomDialog(
navigatorKey.currentState!.context, navigatorKey.currentState!.context,
RoomUserInfoCard(userId: userProfile?.id), RoomUserInfoCard(userId: userProfile?.id),
); );

View File

@ -6,8 +6,13 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_widget.dart';
class RoomRocketRankPage extends StatefulWidget { class RoomRocketRankPage extends StatefulWidget {
const RoomRocketRankPage({ const RoomRocketRankPage({
@ -66,18 +71,25 @@ class RoomRocketRankEntry {
} }
class _RoomRocketRankPageState extends State<RoomRocketRankPage> { class _RoomRocketRankPageState extends State<RoomRocketRankPage> {
static const String _effectSuppressReason = 'room_rocket_rank';
static const int _levelCount = 6;
late int _selectedLevel; late int _selectedLevel;
final Map<int, List<RoomRocketRankEntry>> _loadedRanks = {}; final Map<int, List<RoomRocketRankEntry>> _loadedRanks = {};
final Map<int, int> _loadedRoundNos = {}; final Map<int, int> _loadedRoundNos = {};
final Map<int, RoomRocketRankEntry> _loadedCurrentUsers = {}; final Map<int, RoomRocketRankEntry> _loadedCurrentUsers = {};
final Set<int> _loadingLevels = {}; final Set<int> _loadingLevels = {};
RtcProvider? _suppressedRtcProvider;
static const int _levelCount = 6; String _suppressedRoomId = '';
bool _roomEffectsSuppressed = false;
bool _restoreRoomVisualEffects = false;
bool _restoreVoiceRoomRouteVisible = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_selectedLevel = widget.initialLevel.clamp(0, _levelCount - 1); _selectedLevel = widget.initialLevel.clamp(0, _levelCount - 1);
_suppressRoomEffects();
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) { if (mounted) {
_loadLevel(_selectedLevel); _loadLevel(_selectedLevel);
@ -85,6 +97,12 @@ class _RoomRocketRankPageState extends State<RoomRocketRankPage> {
}); });
} }
@override
void dispose() {
_releaseRoomEffects();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
@ -110,6 +128,9 @@ class _RoomRocketRankPageState extends State<RoomRocketRankPage> {
selectedLevel: _selectedLevel, selectedLevel: _selectedLevel,
showRound2Today: widget.showRound2Today, showRound2Today: widget.showRound2Today,
roundNo: _loadedRoundNos[_selectedLevel] ?? 0, roundNo: _loadedRoundNos[_selectedLevel] ?? 0,
isLoading:
widget.levelRanks == null &&
_loadingLevels.contains(_selectedLevel),
ranksByLevel: _normalizedRanks, ranksByLevel: _normalizedRanks,
currentUser: currentUser:
widget.currentUser ?? widget.currentUser ??
@ -128,6 +149,61 @@ class _RoomRocketRankPageState extends State<RoomRocketRankPage> {
); );
} }
void _suppressRoomEffects() {
if (_roomEffectsSuppressed) {
return;
}
_roomEffectsSuppressed = true;
try {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
_suppressedRtcProvider = rtcProvider;
_suppressedRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
_restoreRoomVisualEffects = rtcProvider.roomVisualEffectsEnabled;
_restoreVoiceRoomRouteVisible = rtcProvider.isVoiceRoomRouteVisible;
rtcProvider.setVoiceRoomRouteVisible(false);
rtcProvider.setRoomVisualEffectsEnabled(false);
} catch (_) {}
RoomEntranceHelper.clearQueue();
OverlayManager().beginSuppressFloatingScreens(
reason: _effectSuppressReason,
);
SCRoomEffectScheduler().clearDeferredTasks(reason: _effectSuppressReason);
SCGiftVapSvgaManager().stopPlayback();
SCGiftVapSvgaManager().pauseAnim();
}
void _releaseRoomEffects() {
if (!_roomEffectsSuppressed) {
return;
}
_roomEffectsSuppressed = false;
SCGiftVapSvgaManager().stopPlayback();
OverlayManager().endSuppressFloatingScreens(reason: _effectSuppressReason);
final rtcProvider = _suppressedRtcProvider;
if (rtcProvider != null && _isSameSuppressedRoom(rtcProvider)) {
if (_restoreVoiceRoomRouteVisible) {
rtcProvider.setVoiceRoomRouteVisible(true);
}
if (_restoreRoomVisualEffects) {
rtcProvider.setRoomVisualEffectsEnabled(true);
}
}
SCGiftVapSvgaManager().resumeAnim();
_suppressedRtcProvider = null;
}
bool _isSameSuppressedRoom(RtcProvider rtcProvider) {
final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? '';
if (currentRoomId.isEmpty) {
return false;
}
return _suppressedRoomId.isEmpty || currentRoomId == _suppressedRoomId;
}
List<List<RoomRocketRankEntry>> get _normalizedRanks { List<List<RoomRocketRankEntry>> get _normalizedRanks {
final providedRanks = widget.levelRanks; final providedRanks = widget.levelRanks;
if (providedRanks == null) { if (providedRanks == null) {
@ -178,7 +254,9 @@ class _RoomRocketRankPageState extends State<RoomRocketRankPage> {
_rocketRankDebug('skip king request because roomId is empty'); _rocketRankDebug('skip king request because roomId is empty');
return; return;
} }
_loadingLevels.add(index); setState(() {
_loadingLevels.add(index);
});
_rocketRankDebug('request king roomId=$roomId level=${index + 1}'); _rocketRankDebug('request king roomId=$roomId level=${index + 1}');
SCChatRoomRepository() SCChatRoomRepository()
.roomRocketKing(roomId, level: index + 1) .roomRocketKing(roomId, level: index + 1)
@ -205,7 +283,13 @@ class _RoomRocketRankPageState extends State<RoomRocketRankPage> {
}); });
}) })
.whenComplete(() { .whenComplete(() {
_loadingLevels.remove(index); if (!mounted) {
_loadingLevels.remove(index);
return;
}
setState(() {
_loadingLevels.remove(index);
});
}); });
} }
@ -315,6 +399,7 @@ class _RocketRankCanvas extends StatelessWidget {
required this.selectedLevel, required this.selectedLevel,
required this.showRound2Today, required this.showRound2Today,
required this.roundNo, required this.roundNo,
required this.isLoading,
required this.ranksByLevel, required this.ranksByLevel,
required this.currentUser, required this.currentUser,
required this.onLevelChanged, required this.onLevelChanged,
@ -323,6 +408,7 @@ class _RocketRankCanvas extends StatelessWidget {
final int selectedLevel; final int selectedLevel;
final bool showRound2Today; final bool showRound2Today;
final int roundNo; final int roundNo;
final bool isLoading;
final List<List<RoomRocketRankEntry>> ranksByLevel; final List<List<RoomRocketRankEntry>> ranksByLevel;
final RoomRocketRankEntry currentUser; final RoomRocketRankEntry currentUser;
final ValueChanged<int> onLevelChanged; final ValueChanged<int> onLevelChanged;
@ -360,7 +446,9 @@ class _RocketRankCanvas extends StatelessWidget {
onChanged: onLevelChanged, onChanged: onLevelChanged,
), ),
), ),
if (entries.isEmpty) if (isLoading)
const _RocketRankLoadingState()
else if (entries.isEmpty)
const _RocketRankEmptyState() const _RocketRankEmptyState()
else else
_RocketRankListContent(entries: entries), _RocketRankListContent(entries: entries),
@ -929,6 +1017,23 @@ class _RocketRankBottomBar extends StatelessWidget {
} }
} }
class _RocketRankLoadingState extends StatelessWidget {
const _RocketRankLoadingState();
@override
Widget build(BuildContext context) {
return const Positioned(
left: 28,
top: 208,
width: 319,
height: 430,
child: Center(
child: SCRotatingDotsLoading(size: 34, color: Color(0xFF75F3CB)),
),
);
}
}
class _RocketRankEmptyState extends StatelessWidget { class _RocketRankEmptyState extends StatelessWidget {
const _RocketRankEmptyState(); const _RocketRankEmptyState();
@ -1093,5 +1198,5 @@ class _RocketRankAssets {
'sc_images/room/rocket/rocket_rank_frame_2.png'; 'sc_images/room/rocket/rocket_rank_frame_2.png';
static const String rankThreeFrame = static const String rankThreeFrame =
'sc_images/room/rocket/rocket_rank_frame_3.png'; 'sc_images/room/rocket/rocket_rank_frame_3.png';
static const String noData = 'sc_images/general/sc_no_data.png'; static const String noData = 'sc_images/room/sc_icon_room_music_empty.png';
} }

View File

@ -209,6 +209,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
void _ensureRoomVisualEffectsEnabled() { void _ensureRoomVisualEffectsEnabled() {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false); final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
if (rtcProvider.currenRoom == null || if (rtcProvider.currenRoom == null ||
!_roomRouteVisible ||
rtcProvider.roomVisualEffectsEnabled) { rtcProvider.roomVisualEffectsEnabled) {
return; return;
} }

View File

@ -535,17 +535,17 @@ class _SearchRoomListState extends State<SearchRoomList> {
), ),
), ),
SizedBox(width: 5.w), SizedBox(width: 5.w),
(e.extValues?.roomSetting?.password?.isEmpty ?? false) e.extValues?.existsPassword != true
? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w) ? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w)
: Image.asset( : Image.asset(
"sc_images/index/sc_icon_room_suo.png", "sc_images/index/sc_icon_room_suo.png",
width: 20.w, width: 20.w,
height: 20.w, height: 20.w,
), ),
(e.extValues?.roomSetting?.password?.isEmpty ?? false) e.extValues?.existsPassword != true
? SizedBox(width: 3.w) ? SizedBox(width: 3.w)
: Container(height: 10.w), : Container(height: 10.w),
(e.extValues?.roomSetting?.password?.isEmpty ?? false) e.extValues?.existsPassword != true
? text( ? text(
e.displayMemberCount, e.displayMemberCount,
fontSize: 10.sp, fontSize: 10.sp,

View File

@ -29,6 +29,7 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart';
import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart';
import '../../../app/constants/sc_screen.dart'; import '../../../app/constants/sc_screen.dart';
import '../../../shared/business_logic/models/res/sc_user_counter_res.dart'; import '../../../shared/business_logic/models/res/sc_user_counter_res.dart';
@ -393,13 +394,20 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
); );
} }
Widget _buildHeaderBadgePlaceholder() { Widget _buildHeaderBadgeStrip(SocialChatUserProfileManager ref) {
return SizedBox( return SCUserBadgeStrip(
height: 24.w, userId: ref.userProfile?.id ?? widget.tageId,
child: Row( fallbackBadges:
mainAxisAlignment: MainAxisAlignment.center, ref.userProfile?.wearBadge
children: const [], ?.where((item) => item.use ?? false)
), .toList() ??
const <WearBadge>[],
height: 26.w,
badgeHeight: 24.w,
longBadgeWidth: 58.w,
spacing: 6.w,
maxBadges: 8,
reserveSpace: true,
); );
} }
@ -612,7 +620,7 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
SizedBox(height: 5.w), SizedBox(height: 5.w),
_buildHeaderMeta(ref), _buildHeaderMeta(ref),
SizedBox(height: 4.w), SizedBox(height: 4.w),
_buildHeaderBadgePlaceholder(), _buildHeaderBadgeStrip(ref),
SizedBox(height: 6.w), SizedBox(height: 6.w),
_buildHeaderIntroduction(ref), _buildHeaderIntroduction(ref),
SizedBox(height: 12.w), SizedBox(height: 12.w),

View File

@ -40,6 +40,7 @@ import 'package:yumi/shared/business_logic/models/res/room_res.dart'
import 'package:yumi/modules/room/voice_room_route.dart'; import 'package:yumi/modules/room/voice_room_route.dart';
import 'package:yumi/ui_kit/widgets/room/empty_mai_select.dart'; import 'package:yumi/ui_kit/widgets/room/empty_mai_select.dart';
import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart'; import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart';
import 'package:yumi/ui_kit/widgets/room/room_password_dialog.dart';
import '../../shared/tools/sc_heartbeat_utils.dart'; import '../../shared/tools/sc_heartbeat_utils.dart';
import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart'; import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart';
import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart'; import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart';
@ -52,8 +53,8 @@ import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart';
import '../../shared/business_logic/models/res/sc_vip_res.dart'; import '../../shared/business_logic/models/res/sc_vip_res.dart';
import '../../shared/tools/sc_room_profile_cache.dart'; import '../../shared/tools/sc_room_profile_cache.dart';
import '../../ui_kit/components/sc_float_ichart.dart'; import '../../ui_kit/components/sc_float_ichart.dart';
import '../../ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart'; import '../../ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart';
import '../../ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart'; import '../../ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart';
typedef OnSoundVoiceChange = Function(num index, int volum); typedef OnSoundVoiceChange = Function(num index, int volum);
typedef RtcProvider = RealTimeCommunicationManager; typedef RtcProvider = RealTimeCommunicationManager;
@ -120,6 +121,7 @@ class RoomEntryPreviewData {
this.updateTime, this.updateTime,
this.activeTime, this.activeTime,
this.roomSetting, this.roomSetting,
this.existsPassword,
this.ownerProfile, this.ownerProfile,
this.roomAdminCount, this.roomAdminCount,
this.roomMemberCount, this.roomMemberCount,
@ -141,6 +143,7 @@ class RoomEntryPreviewData {
countryName: room.countryName, countryName: room.countryName,
nationalFlag: room.nationalFlag, nationalFlag: room.nationalFlag,
roomSetting: room.extValues?.roomSetting, roomSetting: room.extValues?.roomSetting,
existsPassword: room.extValues?.existsPassword == true,
ownerProfile: room.userProfile, ownerProfile: room.userProfile,
roomAdminCount: room.roomCounter?.adminCount, roomAdminCount: room.roomCounter?.adminCount,
roomMemberCount: room.roomCounter?.memberCount, roomMemberCount: room.roomCounter?.memberCount,
@ -163,6 +166,7 @@ class RoomEntryPreviewData {
countryName: roomProfile?.countryName, countryName: roomProfile?.countryName,
nationalFlag: roomProfile?.nationalFlag, nationalFlag: roomProfile?.nationalFlag,
roomSetting: roomProfile?.extValues?.roomSetting, roomSetting: roomProfile?.extValues?.roomSetting,
existsPassword: roomProfile?.extValues?.existsPassword == true,
ownerProfile: roomProfile?.userProfile, ownerProfile: roomProfile?.userProfile,
roomAdminCount: roomProfile?.roomCounter?.adminCount, roomAdminCount: roomProfile?.roomCounter?.adminCount,
roomMemberCount: roomProfile?.roomCounter?.memberCount, roomMemberCount: roomProfile?.roomCounter?.memberCount,
@ -192,6 +196,7 @@ class RoomEntryPreviewData {
updateTime: room.updateTime, updateTime: room.updateTime,
activeTime: room.activeTime, activeTime: room.activeTime,
roomSetting: room.setting, roomSetting: room.setting,
existsPassword: (room.setting?.password ?? "").trim().isNotEmpty,
ownerProfile: ownerProfile, ownerProfile: ownerProfile,
roomAdminCount: room.counter?.adminCount, roomAdminCount: room.counter?.adminCount,
roomMemberCount: room.counter?.memberCount, roomMemberCount: room.counter?.memberCount,
@ -217,6 +222,7 @@ class RoomEntryPreviewData {
final num? updateTime; final num? updateTime;
final num? activeTime; final num? activeTime;
final RoomSetting? roomSetting; final RoomSetting? roomSetting;
final bool? existsPassword;
final SocialChatUserProfile? ownerProfile; final SocialChatUserProfile? ownerProfile;
final num? roomAdminCount; final num? roomAdminCount;
final num? roomMemberCount; final num? roomMemberCount;
@ -292,6 +298,9 @@ class _PendingRoomStartupSeatAction {
class RealTimeCommunicationManager extends ChangeNotifier { class RealTimeCommunicationManager extends ChangeNotifier {
static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog'; static const String _roomRocketRewardDialogTag = 'showRoomRocketRewardDialog';
static const String _roomRocketPagLaunchDialogTag =
'showRoomRocketPagLaunchDialog';
static const int _roomPasswordNotTrueErrorCode = 9005;
static const Duration _micListPollingInterval = Duration(seconds: 2); static const Duration _micListPollingInterval = Duration(seconds: 2);
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3); static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
@ -2122,6 +2131,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
SCTts.show('Microphone permission is denied.'); SCTts.show('Microphone permission is denied.');
throw ArgumentError('Microphone permission is denied.'); throw ArgumentError('Microphone permission is denied.');
} }
if (!context.mounted) {
return;
}
if (roomId == currenRoom?.roomProfile?.roomProfile?.id) { if (roomId == currenRoom?.roomProfile?.roomProfile?.id) {
if (_currentRoomIsEntryPreview || if (_currentRoomIsEntryPreview ||
_roomStartupStatus == RoomStartupStatus.loading) { _roomStartupStatus == RoomStartupStatus.loading) {
@ -2164,6 +2176,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_startVoiceRoomForegroundService(); _startVoiceRoomForegroundService();
VoiceRoomRoute.openVoiceRoom(context); VoiceRoomRoute.openVoiceRoom(context);
} else { } else {
JoinRoomRes? preEnteredRoom;
if (pwd == null && _previewRoomRequiresPassword(context, previewData)) {
preEnteredRoom = await _showLockedRoomEntryDialog(
roomId,
needOpenRedenvelope: needOpenRedenvelope,
redPackId: redPackId,
);
if (preEnteredRoom == null || !context.mounted) {
return;
}
}
if (currenRoom != null) { if (currenRoom != null) {
_switchAwayFromCurrentRoomForNewEntry(); _switchAwayFromCurrentRoomForNewEntry();
} }
@ -2186,15 +2209,26 @@ class RealTimeCommunicationManager extends ChangeNotifier {
VoiceRoomRoute.openVoiceRoom(context); VoiceRoomRoute.openVoiceRoom(context);
notifyListeners(); notifyListeners();
final entryRequestSerial = ++_roomEntryRequestSerial; final entryRequestSerial = ++_roomEntryRequestSerial;
unawaited( if (preEnteredRoom != null) {
_enterAndInitializeVoiceRoomSession( unawaited(
roomId, _completeEnteredVoiceRoomSession(
entryRequestSerial: entryRequestSerial, preEnteredRoom,
pwd: pwd, entryRequestSerial: entryRequestSerial,
needOpenRedenvelope: needOpenRedenvelope, needOpenRedenvelope: needOpenRedenvelope,
redPackId: redPackId, redPackId: redPackId,
), ),
); );
} else {
unawaited(
_enterAndInitializeVoiceRoomSession(
roomId,
entryRequestSerial: entryRequestSerial,
pwd: pwd,
needOpenRedenvelope: needOpenRedenvelope,
redPackId: redPackId,
),
);
}
} }
} }
@ -2212,32 +2246,167 @@ class RealTimeCommunicationManager extends ChangeNotifier {
redPackId: redPackId, redPackId: redPackId,
needOpenRedenvelope: needOpenRedenvelope, needOpenRedenvelope: needOpenRedenvelope,
); );
if (!_isActiveRoomEntryRequest(entryRequestSerial)) { await _completeEnteredVoiceRoomSession(
unawaited(_cleanupStaleEnteredRoom(enteredRoom)); enteredRoom,
return;
}
currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom);
_syncCurrentUserVipHintFromRoom(currenRoom);
_startVoiceRoomForegroundService();
unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial));
_currentRoomIsEntryPreview = false;
_previewRoomSeatCount = null;
notifyListeners();
await initializeRoomSession(
entryRequestSerial: entryRequestSerial, entryRequestSerial: entryRequestSerial,
needOpenRedenvelope: needOpenRedenvelope, needOpenRedenvelope: needOpenRedenvelope,
redPackId: redPackId, redPackId: redPackId,
shouldOpenRoomPage: false,
); );
} catch (e, stackTrace) { } catch (e, _) {
if (!_isActiveRoomEntryRequest(entryRequestSerial)) { if (!_isActiveRoomEntryRequest(entryRequestSerial)) {
return; return;
} }
if (pwd == null && _isRoomPasswordError(e)) {
final enteredRoom = await _showLockedRoomEntryDialog(
roomId,
needOpenRedenvelope: needOpenRedenvelope,
redPackId: redPackId,
);
if (enteredRoom != null &&
_isActiveRoomEntryRequest(entryRequestSerial)) {
await _completeEnteredVoiceRoomSession(
enteredRoom,
entryRequestSerial: entryRequestSerial,
needOpenRedenvelope: needOpenRedenvelope,
redPackId: redPackId,
);
return;
}
}
_stopVoiceRoomForegroundService(); _stopVoiceRoomForegroundService();
_setRoomStartupFailed(RoomStartupFailureType.entry); _setRoomStartupFailed(RoomStartupFailureType.entry);
} }
} }
Future<void> _completeEnteredVoiceRoomSession(
JoinRoomRes enteredRoom, {
required int entryRequestSerial,
bool needOpenRedenvelope = false,
String redPackId = "",
}) async {
if (!_isActiveRoomEntryRequest(entryRequestSerial)) {
unawaited(_cleanupStaleEnteredRoom(enteredRoom));
return;
}
currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom);
_syncCurrentUserVipHintFromRoom(currenRoom);
_startVoiceRoomForegroundService();
unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial));
_currentRoomIsEntryPreview = false;
_previewRoomSeatCount = null;
notifyListeners();
await initializeRoomSession(
entryRequestSerial: entryRequestSerial,
needOpenRedenvelope: needOpenRedenvelope,
redPackId: redPackId,
shouldOpenRoomPage: false,
);
}
bool _previewRoomRequiresPassword(
BuildContext context,
RoomEntryPreviewData? previewData,
) {
if (previewData?.existsPassword != true) {
return false;
}
final ownerId = (previewData?.userId ?? "").trim();
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
if (ownerId.isNotEmpty && ownerId == currentUserId) {
return false;
}
final userIdentity =
Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
).userIdentity;
if (userIdentity?.superAdmin == true ||
userIdentity?.admin == true ||
userIdentity?.yumiManager == true) {
return false;
}
return true;
}
Future<JoinRoomRes?> _showLockedRoomEntryDialog(
String roomId, {
bool needOpenRedenvelope = false,
String redPackId = "",
}) async {
JoinRoomRes? enteredRoom;
await RoomPasswordDialog.show(
onSubmit: (password) async {
try {
enteredRoom = await SCAccountRepository().entryRoom(
roomId,
pwd: password,
redPackId: redPackId,
needOpenRedenvelope: needOpenRedenvelope,
silentErrorToast: true,
);
return null;
} catch (error) {
if (_isRoomPasswordError(error)) {
return "Password is incorrect";
}
final message = _readNetworkErrorMessage(error);
return message.isNotEmpty ? message : "Failed to enter room";
}
},
);
return enteredRoom;
}
bool _isRoomPasswordError(Object error) {
return _readNetworkErrorCode(error) == _roomPasswordNotTrueErrorCode ||
_readNetworkErrorCodeName(error) == "PASSWORD_NOT_TRUE";
}
int? _readNetworkErrorCode(Object error) {
if (error is DioException) {
final responseData = error.response?.data;
if (responseData is Map) {
final value = responseData["errorCode"] ?? responseData["code"];
if (value is int) {
return value;
}
if (value is num) {
return value.toInt();
}
if (value is String) {
return int.tryParse(value);
}
}
}
return null;
}
String _readNetworkErrorCodeName(Object error) {
if (error is DioException) {
final responseData = error.response?.data;
if (responseData is Map) {
return (responseData["errorCodeName"] ?? responseData["code"] ?? "")
.toString()
.trim();
}
}
return "";
}
String _readNetworkErrorMessage(Object error) {
if (error is DioException) {
final responseData = error.response?.data;
if (responseData is Map) {
return (responseData["errorMsg"] ?? responseData["message"] ?? "")
.toString()
.trim();
}
return (error.error ?? error.message ?? "").toString().trim();
}
return error.toString().trim();
}
JoinRoomRes _mergeVoiceRoomVipHints( JoinRoomRes _mergeVoiceRoomVipHints(
JoinRoomRes enteredRoom, JoinRoomRes enteredRoom,
JoinRoomRes? previewRoom, JoinRoomRes? previewRoom,
@ -2914,13 +3083,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
maskColor: Colors.transparent, maskColor: Colors.transparent,
clickMaskDismiss: false, clickMaskDismiss: false,
builder: builder:
(_) => RoomRocketRewardDialog( (_) => RoomRocketRewardDialogLoader(
rewards: RoomRocketApiMapper.rewardItemsFromRecords( roomId: roomId,
res.records, initialPopupRecords: res.records,
),
winners: RoomRocketApiMapper.winnersFromRecords(res.records),
loadingText: '',
useMockFallback: false,
onClose: () { onClose: () {
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag); SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
if (recordIds.isNotEmpty) { if (recordIds.isNotEmpty) {
@ -2971,11 +3136,21 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer?.cancel();
if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible) { if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible) {
// TODO(rocket-launch): final animationUrl =
// SVGA/VAP/PNG _firstNonBlankText([
// RTM launch.rocketAnimationUrl,
_rocketLaunchAnimationUrlForLevel(launch.safeLevel),
]) ??
"";
if (animationUrl.isNotEmpty) {
_playRoomRocketLaunchAnimation(
animationUrl: animationUrl,
launch: launch,
);
}
_roomRocketLaunchAnimationTimer = Timer(launch.displayDuration, () { _roomRocketLaunchAnimationTimer = Timer(launch.displayDuration, () {
_roomRocketLaunchAnimationTimer = null; _roomRocketLaunchAnimationTimer = null;
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
}); });
return; return;
@ -2984,6 +3159,68 @@ class RealTimeCommunicationManager extends ChangeNotifier {
unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
} }
String? _rocketLaunchAnimationUrlForLevel(int level) {
final levels = roomRocketStatus?.levels ?? const [];
for (final item in levels) {
if (item.level == level && item.rocketAnimationUrl.trim().isNotEmpty) {
return item.rocketAnimationUrl;
}
}
return null;
}
void _playRoomRocketLaunchAnimation({
required String animationUrl,
required RoomRocketLaunchBroadcastMessage launch,
}) {
if (RoomRocketPagEffectOverlay.isPag(animationUrl)) {
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
SmartDialog.show(
tag: _roomRocketPagLaunchDialogTag,
alignment: Alignment.center,
debounce: true,
animationType: SmartAnimationType.fade,
maskColor: Colors.transparent,
clickMaskDismiss: false,
builder:
(_) => RoomRocketPagEffectOverlay(
resource: animationUrl,
topAvatarUrl: _roomRocketTop1AvatarUrl(),
onCompleted: () {
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
_roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null;
unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
},
),
);
return;
}
SCGiftVapSvgaManager().play(
animationUrl,
priority: SCGiftVapSvgaManager.entryEffectPriority + 100,
type: SCGiftVapSvgaManager.entryEffectType,
);
}
String? _roomRocketTop1AvatarUrl() {
final kings = roomRocketStatus?.rocketKings ?? const [];
if (kings.isEmpty) {
return null;
}
for (final item in kings) {
if (item.rank == 1 && item.userAvatar.trim().isNotEmpty) {
return item.userAvatar;
}
}
for (final item in kings) {
if (item.userAvatar.trim().isNotEmpty) {
return item.userAvatar;
}
}
return null;
}
///线 ///线
Future<void> fetchOnlineUsersList({bool notifyIfUnchanged = true}) async { Future<void> fetchOnlineUsersList({bool notifyIfUnchanged = true}) async {
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
@ -3539,7 +3776,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (clickUser.id == if (clickUser.id ==
AccountStorage().getCurrentUser()?.userProfile?.id) { AccountStorage().getCurrentUser()?.userProfile?.id) {
/// ///
showBottomInCenterDialog( showBottomInBottomDialog(
context!, context!,
RoomUserInfoCard(userId: clickUser.id), RoomUserInfoCard(userId: clickUser.id),
); );
@ -3649,7 +3886,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (normalizedUserId.isEmpty) { if (normalizedUserId.isEmpty) {
return; return;
} }
showBottomInCenterDialog( showBottomInBottomDialog(
context!, context!,
RoomUserInfoCard(userId: normalizedUserId), RoomUserInfoCard(userId: normalizedUserId),
); );

View File

@ -54,14 +54,14 @@ import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push
hide Data; hide Data;
import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart';
import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart';
import 'package:yumi/ui_kit/widgets/room/invite/invite_room_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/invite/invite_room_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_open_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_open_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_pending_cache.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_pending_cache.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/services/gift/gift_system_manager.dart'; import 'package:yumi/services/gift/gift_system_manager.dart';
@ -119,6 +119,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
static const int _luckyGiftBurstPlaybackWatchdogMs = 8000; static const int _luckyGiftBurstPlaybackWatchdogMs = 8000;
BuildContext? context; BuildContext? context;
final Set<String> _sentRoomRocketRewardMessageKeys = <String>{};
void _giftFxLog(String message) {} void _giftFxLog(String message) {}
@ -1103,6 +1104,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
return true; return true;
case SCRoomMsgType.roomRedPacketClaim: case SCRoomMsgType.roomRedPacketClaim:
return true; return true;
case SCRoomMsgType.rocketRewardUser:
return true;
case SCRoomMsgType.emoticons: case SCRoomMsgType.emoticons:
return (msg.number ?? -1) < 0; return (msg.number ?? -1) < 0;
default: default:
@ -1132,6 +1135,10 @@ class RealTimeMessagingManager extends ChangeNotifier {
_hasRoomRedPacketClaimMessage(msg)) { _hasRoomRedPacketClaimMessage(msg)) {
return; return;
} }
if (msg.type == SCRoomMsgType.rocketRewardUser &&
_hasRoomRocketRewardMessage(msg)) {
return;
}
roomAllMsgList.insert(0, msg); roomAllMsgList.insert(0, msg);
if (roomAllMsgList.length > 250) { if (roomAllMsgList.length > 250) {
@ -1164,6 +1171,12 @@ class RealTimeMessagingManager extends ChangeNotifier {
roomChatMsgList.removeAt(roomChatMsgList.length - 1); roomChatMsgList.removeAt(roomChatMsgList.length - 1);
} }
msgChatListener?.call(msg); msgChatListener?.call(msg);
} else if (msg.type == SCRoomMsgType.rocketRewardUser) {
roomChatMsgList.insert(0, msg);
if (roomChatMsgList.length > 250) {
roomChatMsgList.removeAt(roomChatMsgList.length - 1);
}
msgChatListener?.call(msg);
} else if (msg.type == SCRoomMsgType.gift || } else if (msg.type == SCRoomMsgType.gift ||
msg.type == SCRoomMsgType.luckGiftAnimOther) { msg.type == SCRoomMsgType.luckGiftAnimOther) {
roomGiftMsgList.insert(0, msg); roomGiftMsgList.insert(0, msg);
@ -1271,6 +1284,32 @@ class RealTimeMessagingManager extends ChangeNotifier {
return ['legacy', userId, msg.msg ?? '', amount, time].join('|'); return ['legacy', userId, msg.msg ?? '', amount, time].join('|');
} }
bool _hasRoomRocketRewardMessage(Msg msg) {
final key = _roomRocketRewardMessageKey(msg);
if (key.isEmpty) {
return false;
}
return roomAllMsgList.any(
(item) =>
item.type == SCRoomMsgType.rocketRewardUser &&
_roomRocketRewardMessageKey(item) == key,
);
}
String _roomRocketRewardMessageKey(Msg msg) {
final stableKey = (msg.giftBatchId ?? '').trim();
if (stableKey.isNotEmpty) {
return stableKey;
}
return [
msg.groupId ?? '',
msg.user?.id ?? '',
msg.number?.toString() ?? '',
msg.role ?? '',
msg.msg ?? '',
].join('|');
}
Msg? _mergeGiftMessageIfNeeded(Msg incoming) { Msg? _mergeGiftMessageIfNeeded(Msg incoming) {
if (incoming.type != SCRoomMsgType.gift && if (incoming.type != SCRoomMsgType.gift &&
incoming.type != SCRoomMsgType.luckGiftAnimOther) { incoming.type != SCRoomMsgType.luckGiftAnimOther) {
@ -1825,7 +1864,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
(rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? '').trim(); (rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? '').trim();
if (currentRoomId == launch.roomId) { if (currentRoomId == launch.roomId) {
rtcProvider.handleRoomRocketLaunchBroadcast(launch); rtcProvider.handleRoomRocketLaunchBroadcast(launch);
return;
} }
} }
@ -1835,19 +1873,259 @@ class RealTimeMessagingManager extends ChangeNotifier {
void _handleVoiceRoomRocketRewardPopup(dynamic payload) { void _handleVoiceRoomRocketRewardPopup(dynamic payload) {
final data = _broadcastPayloadMap(payload); final data = _broadcastPayloadMap(payload);
final roomId = _payloadText(data['roomId']); final roomId = _payloadText(data['roomId']);
final currentRoomId = _currentVoiceRoomId();
final resolvedRoomId = roomId.isEmpty ? currentRoomId : roomId;
if (resolvedRoomId.isEmpty ||
(currentRoomId.isNotEmpty && resolvedRoomId != currentRoomId)) {
return;
}
final userIds = _payloadStringList(data['userIds']); final userIds = _payloadStringList(data['userIds']);
final currentUserId = final currentUserId =
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ''; AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? '';
if (currentUserId.isEmpty || !userIds.contains(currentUserId)) { if (currentUserId.isEmpty || !userIds.contains(currentUserId)) {
return; return;
} }
final currentRoomId = _currentVoiceRoomId(); unawaited(_showRoomRocketRewardPopupDialog(resolvedRoomId));
if (roomId.isNotEmpty && }
currentRoomId.isNotEmpty &&
roomId != currentRoomId) { Future<void> _sendRoomRocketRewardPublicMessages(
String roomId,
Map<String, dynamic> payload,
) async {
final groupId = _currentVoiceRoomGroupId();
if (groupId.isEmpty) {
return; return;
} }
unawaited(_showRoomRocketRewardPopupDialog(roomId)); final launchNo = _payloadText(payload['launchNo']);
var records = _rocketRewardRecordsFromPayload(payload);
if (records.isEmpty) {
records = await _loadRocketRewardPublicRecords(roomId, launchNo);
}
if (records.isNotEmpty) {
for (final record in records) {
await _sendRoomRocketRewardMessage(
groupId,
_buildRocketRewardRecordMsg(groupId, record),
);
}
return;
}
final userIds = _payloadStringList(payload['userIds']);
for (final userId in userIds) {
await _sendRoomRocketRewardMessage(
groupId,
_buildRocketRewardFallbackMsg(groupId, payload, userId),
);
}
}
Future<void> _sendRoomRocketRewardMessage(String groupId, Msg msg) async {
final key = _roomRocketRewardMessageKey(msg);
if (key.isEmpty || !_sentRoomRocketRewardMessageKeys.add(key)) {
return;
}
try {
final textMsg = await TencentImSDKPlugin.v2TIMManager
.getMessageManager()
.createCustomMessage(data: jsonEncode(msg.toJson()));
if (textMsg.code != 0 || textMsg.data?.id == null) {
_sentRoomRocketRewardMessageKeys.remove(key);
return;
}
final sendResult = await TencentImSDKPlugin.v2TIMManager
.getMessageManager()
.sendMessage(id: textMsg.data!.id!, groupID: groupId, receiver: '');
if (sendResult.code == 0) {
addMsg(msg);
} else {
_sentRoomRocketRewardMessageKeys.remove(key);
}
} catch (_) {
_sentRoomRocketRewardMessageKeys.remove(key);
}
}
Future<List<SCRoomRocketRewardRecordRes>> _loadRocketRewardPublicRecords(
String roomId,
String launchNo,
) async {
if (roomId.trim().isEmpty || launchNo.trim().isEmpty) {
return const <SCRoomRocketRewardRecordRes>[];
}
try {
final res = await SCChatRoomRepository().roomRocketRewardRecords(
roomId,
cursor: 1,
limit: 100,
);
return res.records
.where((item) => item.launchNo.trim() == launchNo.trim())
.toList(growable: false);
} catch (_) {
return const <SCRoomRocketRewardRecordRes>[];
}
}
List<SCRoomRocketRewardRecordRes> _rocketRewardRecordsFromPayload(
Map<String, dynamic> payload,
) {
final candidates = [
payload['records'],
payload['rewardRecords'],
payload['rewards'],
_broadcastPayloadMap(payload['body'])['records'],
_broadcastPayloadMap(payload['data'])['records'],
];
for (final candidate in candidates) {
if (candidate is Iterable) {
final records = candidate
.map(SCRoomRocketRewardRecordRes.fromJson)
.toList(growable: false);
final usableRecords = records
.where((item) => item.userId.trim().isNotEmpty)
.toList(growable: false);
if (usableRecords.isNotEmpty) {
return usableRecords;
}
}
}
return const <SCRoomRocketRewardRecordRes>[];
}
Msg _buildRocketRewardRecordMsg(
String groupId,
SCRoomRocketRewardRecordRes record,
) {
return Msg(
groupId: groupId,
msg: record.rewardCover,
type: SCRoomMsgType.rocketRewardUser,
user: SocialChatUserProfile(
id: record.userId,
userNickname: record.displayName,
userAvatar: record.userAvatar,
account: record.account,
),
role: _rocketRewardRole(record.rewardType, record.rewardCover),
number: record.level <= 0 ? 1 : record.level,
giftBatchId: _rocketRewardRecordKey(record),
);
}
Msg _buildRocketRewardFallbackMsg(
String groupId,
Map<String, dynamic> payload,
String userId,
) {
final rewardCover = _firstNonBlank([
_payloadText(payload['rewardCover']),
_payloadText(payload['rewardIcon']),
_payloadText(payload['rewardUrl']),
_payloadText(payload['giftUrl']),
_payloadText(payload['giftIcon']),
]);
final rewardType = _firstNonBlank([
_payloadText(payload['rewardType']),
_payloadText(payload['type']),
rewardCover.isEmpty ? 'GOLD' : '',
]);
final level = (_payloadNum(payload['level']) ?? 1).toInt();
return Msg(
groupId: groupId,
msg: rewardCover,
type: SCRoomMsgType.rocketRewardUser,
user: _rocketRewardUserFromPayload(payload, userId),
role: _rocketRewardRole(rewardType, rewardCover),
number: level <= 0 ? 1 : level,
giftBatchId: [
'rocket_reward_fallback',
_payloadText(payload['launchNo']),
userId,
level,
rewardType,
].join('|'),
);
}
SocialChatUserProfile _rocketRewardUserFromPayload(
Map<String, dynamic> payload,
String userId,
) {
final userPayload = _rocketRewardUserPayload(payload, userId);
final nickname = _firstNonBlank([
_payloadText(userPayload['userNickname']),
_payloadText(userPayload['nickname']),
_payloadText(userPayload['nickName']),
_payloadText(userPayload['account']),
userId,
]);
return SocialChatUserProfile(
id: _firstNonBlank([_payloadText(userPayload['userId']), userId]),
userNickname: nickname,
userAvatar: _firstNonBlank([
_payloadText(userPayload['userAvatar']),
_payloadText(userPayload['avatar']),
_payloadText(userPayload['avatarUrl']),
]),
account: _payloadText(userPayload['account']),
);
}
Map<String, dynamic> _rocketRewardUserPayload(
Map<String, dynamic> payload,
String userId,
) {
final candidates = [
payload['users'],
payload['rewardUsers'],
payload['winnerUsers'],
payload['winners'],
];
for (final candidate in candidates) {
if (candidate is Map) {
final item = candidate[userId] ?? candidate[userId.toString()];
final map = _broadcastPayloadMap(item);
if (map.isNotEmpty) {
return map;
}
}
if (candidate is Iterable) {
for (final item in candidate) {
final map = _broadcastPayloadMap(item);
final id = _firstNonBlank([
_payloadText(map['userId']),
_payloadText(map['id']),
_payloadText(map['account']),
]);
if (id == userId) {
return map;
}
}
}
}
return <String, dynamic>{'userId': userId, 'userNickname': userId};
}
String _rocketRewardRole(String rewardType, String rewardCover) {
final normalized = rewardType.trim().toUpperCase();
if (normalized == 'GOLD' || normalized == 'COIN' || normalized == 'COINS') {
return 'GOLD';
}
return rewardCover.trim().isEmpty ? 'GOLD' : normalized;
}
String _rocketRewardRecordKey(SCRoomRocketRewardRecordRes record) {
return [
'rocket_reward',
record.id,
record.launchNo,
record.userId,
record.rewardScene,
record.rewardType,
record.rewardItemId,
].join('|');
} }
Future<void> _showRoomRocketRewardPopupDialog(String roomId) async { Future<void> _showRoomRocketRewardPopupDialog(String roomId) async {
@ -1856,6 +2134,11 @@ class RealTimeMessagingManager extends ChangeNotifier {
if (res.records.isEmpty) { if (res.records.isEmpty) {
return; return;
} }
unawaited(
_sendRoomRocketRewardPublicMessages(roomId, {
'records': res.records.map((item) => item.toJson()).toList(),
}),
);
final recordIds = res.records final recordIds = res.records
.map((item) => item.id) .map((item) => item.id)
.where((item) => item.isNotEmpty) .where((item) => item.isNotEmpty)
@ -1869,11 +2152,9 @@ class RealTimeMessagingManager extends ChangeNotifier {
maskColor: Colors.transparent, maskColor: Colors.transparent,
clickMaskDismiss: false, clickMaskDismiss: false,
builder: builder:
(_) => RoomRocketRewardDialog( (_) => RoomRocketRewardDialogLoader(
rewards: RoomRocketApiMapper.rewardItemsFromRecords(res.records), roomId: roomId,
winners: RoomRocketApiMapper.winnersFromRecords(res.records), initialPopupRecords: res.records,
loadingText: '',
useMockFallback: false,
onClose: () { onClose: () {
SmartDialog.dismiss(tag: _roomRocketRewardDialogTag); SmartDialog.dismiss(tag: _roomRocketRewardDialogTag);
if (recordIds.isNotEmpty) { if (recordIds.isNotEmpty) {
@ -1894,13 +2175,18 @@ class RealTimeMessagingManager extends ChangeNotifier {
type: 3, type: 3,
roomId: launch.roomId, roomId: launch.roomId,
rocketLevel: launch.safeLevel, rocketLevel: launch.safeLevel,
userAvatarUrl: launch.triggerUser.avatar, userAvatarUrl: _firstNonBlank([
userName: launch.triggerUser.nickname, launch.roomCoverUrl,
launch.triggerUser.avatar,
]),
userName: _firstNonBlank([launch.roomName, launch.roomAccount]),
userId: _firstNonBlank([ userId: _firstNonBlank([
launch.triggerUser.account, launch.triggerUser.account,
launch.triggerUser.userId, launch.triggerUser.userId,
launch.triggerUserId, launch.triggerUserId,
]), ]),
giftUrl: launch.rocketIconUrl,
fallbackUrl: launch.rocketIconUrl,
giftId: launch.launchNo, giftId: launch.launchNo,
durationSeconds: launch.durationSeconds, durationSeconds: launch.durationSeconds,
priority: 1000, priority: 1000,
@ -2074,13 +2360,35 @@ class RealTimeMessagingManager extends ChangeNotifier {
} else if (type == "ROCKET_ENERGY_LAUNCH") { } else if (type == "ROCKET_ENERGY_LAUNCH") {
/// ///
var fdata = data["data"]; var fdata = data["data"];
final rocketIconUrl = _firstNonBlank([
_payloadText(fdata["rocketIconUrl"]),
_payloadText(fdata["rocketIcon"]),
_payloadText(fdata["rocketUrl"]),
_payloadText(fdata["rocketImageUrl"]),
_payloadText(fdata["giftUrl"]),
_payloadText(fdata["giftIcon"]),
]);
SCFloatingMessage msg = SCFloatingMessage( SCFloatingMessage msg = SCFloatingMessage(
type: 3, type: 3,
roomId: fdata["roomId"], roomId: fdata["roomId"],
rocketLevel: fdata["fromLevel"], rocketLevel: fdata["fromLevel"],
userAvatarUrl: fdata["userAvatar"], userAvatarUrl: _firstNonBlank([
userName: fdata["nickname"], _payloadText(fdata["roomCoverUrl"]),
_payloadText(fdata["roomCover"]),
_payloadText(fdata["roomAvatar"]),
_payloadText(fdata["roomIcon"]),
_payloadText(fdata["coverUrl"]),
_payloadText(fdata["cover"]),
_payloadText(fdata["userAvatar"]),
]),
userName: _firstNonBlank([
_payloadText(fdata["roomName"]),
_payloadText(fdata["roomAccount"]),
_payloadText(fdata["nickname"]),
]),
userId: fdata["actualAccount"], userId: fdata["actualAccount"],
giftUrl: rocketIconUrl,
fallbackUrl: rocketIconUrl,
priority: 1000, priority: 1000,
); );
OverlayManager().addMessage(msg); OverlayManager().addMessage(msg);

View File

@ -9,6 +9,7 @@ import 'package:yumi/shared/tools/sc_network_image_utils.dart';
import 'package:yumi/shared/data_sources/models/country_mode.dart'; import 'package:yumi/shared/data_sources/models/country_mode.dart';
import 'package:yumi/shared/business_logic/models/res/sc_banner_leaderboard_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_banner_leaderboard_res.dart';
import 'package:yumi/shared/business_logic/models/res/country_res.dart'; import 'package:yumi/shared/business_logic/models/res/country_res.dart';
import 'package:yumi/shared/business_logic/models/res/gift_backpack_res.dart';
import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_index_banner_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_emoji_res.dart';
@ -17,6 +18,7 @@ import '../../shared/data_sources/models/enum/sc_banner_type.dart';
import '../../shared/data_sources/models/enum/sc_gift_type.dart'; import '../../shared/data_sources/models/enum/sc_gift_type.dart';
class SCAppGeneralManager extends ChangeNotifier { class SCAppGeneralManager extends ChangeNotifier {
static const String backpackGiftTab = 'backpack';
static const int _maxInitialFullScreenGiftPreloads = 4; static const int _maxInitialFullScreenGiftPreloads = 4;
List<CountryMode> countryModeList = []; List<CountryMode> countryModeList = [];
@ -32,9 +34,12 @@ class SCAppGeneralManager extends ChangeNotifier {
final Set<String> _warmingGiftCoverUrls = <String>{}; final Set<String> _warmingGiftCoverUrls = <String>{};
bool _isFetchingGiftList = false; bool _isFetchingGiftList = false;
bool _hasGiftListLoaded = false; bool _hasGiftListLoaded = false;
bool _isFetchingGiftBackpack = false;
bool _hasGiftBackpackLoaded = false;
Map<String, List<SocialChatGiftRes>> giftByTab = {}; Map<String, List<SocialChatGiftRes>> giftByTab = {};
Map<String, List<SocialChatGiftRes>> activityGiftByTab = {}; Map<String, List<SocialChatGiftRes>> activityGiftByTab = {};
List<SocialChatGiftBackpackRes> giftBackpackResList = [];
/// ///
Country? selectCountryInfo; Country? selectCountryInfo;
@ -51,6 +56,13 @@ class SCAppGeneralManager extends ChangeNotifier {
bool get isGiftListLoading => _isFetchingGiftList && !_hasGiftListLoaded; bool get isGiftListLoading => _isFetchingGiftList && !_hasGiftListLoaded;
bool get isGiftBackpackLoading =>
_isFetchingGiftBackpack && !_hasGiftBackpackLoaded;
bool isGiftTabLoading(String type) {
return type == backpackGiftTab ? isGiftBackpackLoading : isGiftListLoading;
}
/// ///
Future<void> fetchCountryList() async { Future<void> fetchCountryList() async {
if (_isFetchingCountryList) { if (_isFetchingCountryList) {
@ -203,6 +215,7 @@ class SCAppGeneralManager extends ChangeNotifier {
} else { } else {
giftByTab.remove("CUSTOMIZED"); giftByTab.remove("CUSTOMIZED");
} }
_mergeBackpackGiftTab();
} }
Future<void> warmGiftPageCovers( Future<void> warmGiftPageCovers(
@ -270,12 +283,75 @@ class SCAppGeneralManager extends ChangeNotifier {
} }
/// ///
void giftBackpack() async { Future<void> giftBackpack({bool forceRefresh = false}) async {
try { if (_isFetchingGiftBackpack) {
await SCChatRoomRepository().giftBackpack();
} catch (_) {
return; return;
} }
if (_hasGiftBackpackLoaded && !forceRefresh) {
_mergeBackpackGiftTab();
notifyListeners();
unawaited(
warmGiftPageCovers(
backpackGiftTab,
pageIndex: 0,
pageCount: 2,
pageSize: 8,
),
);
return;
}
_isFetchingGiftBackpack = true;
if (!_hasGiftBackpackLoaded) {
notifyListeners();
}
try {
giftBackpackResList = await SCChatRoomRepository().giftBackpack();
_hasGiftBackpackLoaded = true;
_mergeBackpackGiftTab();
notifyListeners();
unawaited(
warmGiftPageCovers(
backpackGiftTab,
pageIndex: 0,
pageCount: 2,
pageSize: 8,
),
);
} catch (_) {
return;
} finally {
_isFetchingGiftBackpack = false;
notifyListeners();
}
}
void _mergeBackpackGiftTab() {
final backpackGifts = <SocialChatGiftRes>[];
for (final item in giftBackpackResList) {
final giftConfig = item.giftConfig;
if (giftConfig == null) {
continue;
}
backpackGifts.add(
giftConfig.copyWith(
giftTab: backpackGiftTab,
quantity: _formatBackpackQuantity(item.quantity),
backpackId: item.id,
backpackQuantity: item.quantity,
),
);
}
giftByTab[backpackGiftTab] = backpackGifts;
}
String _formatBackpackQuantity(num? quantity) {
if (quantity == null) {
return '0';
}
if (quantity % 1 == 0) {
return quantity.toInt().toString();
}
return quantity.toString();
} }
///id获取礼物 ///id获取礼物

View File

@ -34,6 +34,8 @@ class SocialChatGiftRes {
String? userId, String? userId,
String? giftName, String? giftName,
String? quantity, String? quantity,
String? backpackId,
num? backpackQuantity,
num? expiredTime, num? expiredTime,
}) { }) {
_id = id; _id = id;
@ -54,6 +56,8 @@ class SocialChatGiftRes {
_userId = userId; _userId = userId;
_giftName = giftName; _giftName = giftName;
_quantity = quantity; _quantity = quantity;
_backpackId = backpackId;
_backpackQuantity = backpackQuantity;
_expiredTime = expiredTime; _expiredTime = expiredTime;
} }
@ -123,6 +127,12 @@ class SocialChatGiftRes {
_quantity = _giftResString( _quantity = _giftResString(
json['quantity'] ?? json['giftQuantity'] ?? json['count'] ?? json['num'], json['quantity'] ?? json['giftQuantity'] ?? json['count'] ?? json['num'],
); );
_backpackId = _giftResString(
json['backpackId'] ?? json['backId'] ?? json['backpackRecordId'],
);
_backpackQuantity = _giftResNum(
json['backpackQuantity'] ?? json['backpackCount'],
);
_expiredTime = _giftResNum( _expiredTime = _giftResNum(
json['expiredTime'] ?? giftConfig?['expiredTime'], json['expiredTime'] ?? giftConfig?['expiredTime'],
); );
@ -145,6 +155,8 @@ class SocialChatGiftRes {
String? _userId; String? _userId;
String? _giftName; String? _giftName;
String? _quantity; String? _quantity;
String? _backpackId;
num? _backpackQuantity;
num? _expiredTime; num? _expiredTime;
SocialChatGiftRes copyWith({ SocialChatGiftRes copyWith({
String? id, String? id,
@ -165,6 +177,8 @@ class SocialChatGiftRes {
String? userId, String? userId,
String? giftName, String? giftName,
String? quantity, String? quantity,
String? backpackId,
num? backpackQuantity,
num? expiredTime, num? expiredTime,
}) => SocialChatGiftRes( }) => SocialChatGiftRes(
id: id ?? _id, id: id ?? _id,
@ -185,6 +199,8 @@ class SocialChatGiftRes {
userId: userId ?? _userId, userId: userId ?? _userId,
giftName: giftName ?? _giftName, giftName: giftName ?? _giftName,
quantity: quantity ?? _quantity, quantity: quantity ?? _quantity,
backpackId: backpackId ?? _backpackId,
backpackQuantity: backpackQuantity ?? _backpackQuantity,
expiredTime: expiredTime ?? _expiredTime, expiredTime: expiredTime ?? _expiredTime,
); );
String? get id => _id; String? get id => _id;
@ -205,6 +221,8 @@ class SocialChatGiftRes {
String? get userId => _userId; String? get userId => _userId;
String? get giftName => _giftName; String? get giftName => _giftName;
String? get quantity => _quantity; String? get quantity => _quantity;
String? get backpackId => _backpackId;
num? get backpackQuantity => _backpackQuantity;
num? get expiredTime => _expiredTime; num? get expiredTime => _expiredTime;
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -227,6 +245,8 @@ class SocialChatGiftRes {
map['userId'] = _userId; map['userId'] = _userId;
map['giftName'] = _giftName; map['giftName'] = _giftName;
map['quantity'] = _quantity; map['quantity'] = _quantity;
map['backpackId'] = _backpackId;
map['backpackQuantity'] = _backpackQuantity;
map['expiredTime'] = _expiredTime; map['expiredTime'] = _expiredTime;
return map; return map;
} }

View File

@ -878,20 +878,30 @@ class WearBadge {
_userId = userId; _userId = userId;
} }
WearBadge.fromJson(dynamic json) { WearBadge.fromJson(dynamic json) {
_animationUrl = json['animationUrl']; if (json is! Map) {
_badgeKey = json['badgeKey']; return;
_badgeLevel = json['badgeLevel']; }
_badgeName = json['badgeName']; _animationUrl = _stringValue(json['animationUrl']);
_expireTime = json['expireTime']; _badgeKey = _stringValue(json['badgeKey']);
_id = json['id']; _badgeLevel = _numValue(json['badgeLevel'] ?? json['level']);
_use = json['use']; _badgeName = _stringValue(
_milestone = json['milestone']; json['badgeName'] ?? json['name'] ?? json['displayName'],
_notSelectUrl = json['notSelectUrl']; );
_selectUrl = json['selectUrl']; _expireTime = _intValue(json['expireTime'] ?? json['expireAt']);
_type = json['type']; _id = _stringValue(json['id'] ?? json['badgeId']);
_userId = json['userId']; _use = _boolValue(json['use']);
} _milestone = _stringValue(json['milestone']);
_notSelectUrl = _stringValue(json['notSelectUrl']);
_selectUrl = _stringValue(
json['url'] ??
json['selectUrl'] ??
json['notSelectUrl'] ??
json['animationUrl'],
);
_type = _stringValue(json['type']);
_userId = _stringValue(json['userId']);
}
String? _animationUrl; String? _animationUrl;
String? _badgeKey; String? _badgeKey;
@ -958,7 +968,7 @@ class WearBadge {
String? get userId => _userId; String? get userId => _userId;
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final map = <String, dynamic>{}; final map = <String, dynamic>{};
map['animationUrl'] = _animationUrl; map['animationUrl'] = _animationUrl;
map['badgeKey'] = _badgeKey; map['badgeKey'] = _badgeKey;
@ -972,9 +982,48 @@ class WearBadge {
map['selectUrl'] = _selectUrl; map['selectUrl'] = _selectUrl;
map['type'] = _type; map['type'] = _type;
map['userId'] = _userId; map['userId'] = _userId;
return map; return map;
} }
}
static String? _stringValue(dynamic value) {
final text = value?.toString().trim();
return text == null || text.isEmpty ? null : text;
}
static int? _intValue(dynamic value) {
if (value is int) {
return value;
}
if (value is num) {
return value.toInt();
}
return int.tryParse(value?.toString() ?? "");
}
static num? _numValue(dynamic value) {
if (value is num) {
return value;
}
return num.tryParse(value?.toString() ?? "");
}
static bool? _boolValue(dynamic value) {
if (value is bool) {
return value;
}
if (value is num) {
return value != 0;
}
final text = value?.toString().trim().toLowerCase();
if (text == "true" || text == "1") {
return true;
}
if (text == "false" || text == "0") {
return false;
}
return null;
}
}
class PersonPhoto { class PersonPhoto {
PersonPhoto({String? url, num? status}) { PersonPhoto({String? url, num? status}) {

View File

@ -4,21 +4,192 @@ class SCRoomRocketLevelRes {
required this.level, required this.level,
required this.needEnergy, required this.needEnergy,
required this.rocketIconUrl, required this.rocketIconUrl,
required this.rocketPreviewUrl,
required this.rocketAnimationUrl, required this.rocketAnimationUrl,
required this.progressBarUrl, required this.progressBarUrl,
required this.shakeThresholdPercent,
required this.enabled, required this.enabled,
required this.sort, required this.sort,
}); });
factory SCRoomRocketLevelRes.fromJson(dynamic json) { factory SCRoomRocketLevelRes.fromJson(dynamic json) {
final data = _asMap(json); final data = _asMap(json);
final rocket = _asMap(data['rocket']);
final levelInfo = _asMap(data['levelInfo']);
final config = _asMap(data['config']);
final resource = _asMap(
data['resource'] ?? data['propsResources'] ?? data['rocketResource'],
);
final preview = _asMap(
data['rocketPreview'] ??
data['preview'] ??
data['rocketPreviewResource'] ??
data['previewResource'],
);
final animation = _asMap(
data['rocketAnimation'] ??
data['animation'] ??
data['rocketAnimationResource'] ??
data['animationResource'] ??
data['launchAnimation'],
);
return SCRoomRocketLevelRes( return SCRoomRocketLevelRes(
id: _stringValue(data['id']), id: _stringValue(data['id']),
level: _intValue(data['level']), level: _intValue(data['level']),
needEnergy: _intValue(data['needEnergy']), needEnergy: _intValue(data['needEnergy']),
rocketIconUrl: _stringValue(data['rocketIconUrl']), rocketIconUrl: _firstAssetString([
rocketAnimationUrl: _stringValue(data['rocketAnimationUrl']), data['rocket_icon_url'],
progressBarUrl: _stringValue(data['progressBarUrl']), data['rocketIconUrl'],
data['iconUrl'],
data['icon'],
data['rocketIcon'],
rocket['rocket_icon_url'],
rocket['rocketIconUrl'],
rocket['iconUrl'],
rocket['icon'],
rocket['rocketIcon'],
levelInfo['rocket_icon_url'],
levelInfo['rocketIconUrl'],
levelInfo['iconUrl'],
levelInfo['rocketIcon'],
config['rocket_icon_url'],
config['rocketIconUrl'],
config['iconUrl'],
config['rocketIcon'],
resource['rocket_icon_url'],
resource['rocketIconUrl'],
resource['iconUrl'],
resource['cover'],
resource['sourceUrl'],
]),
rocketPreviewUrl: _firstAssetString([
data['preview_rocket_url'],
data['previewRocketUrl'],
data['rocketPreviewUrl'],
data['rocketPreviewSvgUrl'],
data['rocketPreviewSvg'],
data['rocketPreview'],
data['rocketSvgUrl'],
data['rocketSvg'],
data['previewSvgUrl'],
data['previewSvg'],
data['previewUrl'],
data['preview'],
data['svgUrl'],
data['svg'],
data['rocketDisplayUrl'],
data['rocketShowUrl'],
data['rocketImageUrl'],
data['previewImageUrl'],
data['sourceUrl'],
rocket['preview_rocket_url'],
rocket['previewRocketUrl'],
rocket['rocketPreviewUrl'],
rocket['rocketPreviewSvgUrl'],
rocket['rocketPreviewSvg'],
rocket['rocketPreview'],
rocket['rocketSvgUrl'],
rocket['rocketSvg'],
rocket['previewSvgUrl'],
rocket['previewSvg'],
rocket['previewUrl'],
rocket['preview'],
rocket['svgUrl'],
rocket['sourceUrl'],
levelInfo['preview_rocket_url'],
levelInfo['previewRocketUrl'],
levelInfo['rocketPreviewUrl'],
levelInfo['rocketPreviewSvgUrl'],
levelInfo['rocketPreviewSvg'],
levelInfo['rocketPreview'],
levelInfo['previewUrl'],
levelInfo['previewSvgUrl'],
levelInfo['previewSvg'],
levelInfo['preview'],
levelInfo['svgUrl'],
levelInfo['sourceUrl'],
config['preview_rocket_url'],
config['previewRocketUrl'],
config['rocketPreviewUrl'],
config['rocketPreviewSvgUrl'],
config['rocketPreviewSvg'],
config['rocketPreview'],
config['previewUrl'],
config['previewSvgUrl'],
config['previewSvg'],
config['preview'],
config['svgUrl'],
config['sourceUrl'],
preview,
resource['preview_rocket_url'],
resource['previewRocketUrl'],
resource['rocketPreviewUrl'],
resource['previewUrl'],
resource['sourceUrl'],
]),
rocketAnimationUrl: _firstAssetString([
data['rocket_animation_url'],
data['rocketAnimationUrl'],
data['rocketSvgaUrl'],
data['animationUrl'],
data['rocketAnimation'],
data['animation'],
data['svgaUrl'],
data['rocketLaunchSvgaUrl'],
data['launchAnimationUrl'],
data['launchAnimation'],
data['launchSvgaUrl'],
data['sourceUrl'],
rocket['rocket_animation_url'],
rocket['rocketAnimationUrl'],
rocket['rocketSvgaUrl'],
rocket['animationUrl'],
rocket['rocketAnimation'],
rocket['animation'],
rocket['svgaUrl'],
rocket['sourceUrl'],
levelInfo['rocket_animation_url'],
levelInfo['rocketAnimationUrl'],
levelInfo['rocketSvgaUrl'],
levelInfo['animationUrl'],
levelInfo['rocketAnimation'],
levelInfo['animation'],
levelInfo['svgaUrl'],
levelInfo['sourceUrl'],
config['rocket_animation_url'],
config['rocketAnimationUrl'],
config['rocketSvgaUrl'],
config['animationUrl'],
config['rocketAnimation'],
config['animation'],
config['svgaUrl'],
config['sourceUrl'],
animation,
]),
progressBarUrl: _firstAssetString([
data['progress_bar_url'],
data['progressBarUrl'],
data['progressUrl'],
data['progressBar'],
rocket['progress_bar_url'],
rocket['progressBarUrl'],
levelInfo['progress_bar_url'],
levelInfo['progressBarUrl'],
config['progress_bar_url'],
config['progressBarUrl'],
]),
shakeThresholdPercent:
_numValue(
data['shake_threshold_percent'] ??
data['shakeThresholdPercent'] ??
rocket['shake_threshold_percent'] ??
rocket['shakeThresholdPercent'] ??
levelInfo['shake_threshold_percent'] ??
levelInfo['shakeThresholdPercent'] ??
config['shake_threshold_percent'] ??
config['shakeThresholdPercent'],
) ??
95,
enabled: _boolValue(data['enabled'], fallback: true), enabled: _boolValue(data['enabled'], fallback: true),
sort: _intValue(data['sort']), sort: _intValue(data['sort']),
); );
@ -28,10 +199,27 @@ class SCRoomRocketLevelRes {
final int level; final int level;
final int needEnergy; final int needEnergy;
final String rocketIconUrl; final String rocketIconUrl;
final String rocketPreviewUrl;
final String rocketAnimationUrl; final String rocketAnimationUrl;
final String progressBarUrl; final String progressBarUrl;
final num shakeThresholdPercent;
final bool enabled; final bool enabled;
final int sort; final int sort;
Map<String, dynamic> toJson() {
return {
'id': id,
'level': level,
'needEnergy': needEnergy,
'rocketIconUrl': rocketIconUrl,
'rocketPreviewUrl': rocketPreviewUrl,
'rocketAnimationUrl': rocketAnimationUrl,
'progressBarUrl': progressBarUrl,
'shakeThresholdPercent': shakeThresholdPercent,
'enabled': enabled,
'sort': sort,
};
}
} }
class SCRoomRocketRewardRes { class SCRoomRocketRewardRes {
@ -230,16 +418,36 @@ class SCRoomRocketKingRes {
} }
class SCRoomRocketRewardPopupRes { class SCRoomRocketRewardPopupRes {
SCRoomRocketRewardPopupRes({required this.records}); SCRoomRocketRewardPopupRes({
required this.records,
required this.cursor,
required this.limit,
required this.emptyReason,
});
factory SCRoomRocketRewardPopupRes.fromJson(dynamic json) { factory SCRoomRocketRewardPopupRes.fromJson(dynamic json) {
final data = _asMap(json); final data = _asMap(json);
return SCRoomRocketRewardPopupRes( return SCRoomRocketRewardPopupRes(
records: SCRoomRocketRewardRecordRes.listFromJson(data['records']), records: SCRoomRocketRewardRecordRes.listFromJson(data['records']),
cursor: _intValue(data['cursor'], fallback: 1),
limit: _intValue(data['limit'], fallback: 10),
emptyReason: _stringValue(data['emptyReason']),
); );
} }
final List<SCRoomRocketRewardRecordRes> records; final List<SCRoomRocketRewardRecordRes> records;
final int cursor;
final int limit;
final String emptyReason;
Map<String, dynamic> toJson() {
return {
"records": records.map((item) => item.toJson()).toList(growable: false),
"cursor": cursor,
"limit": limit,
"emptyReason": emptyReason,
};
}
} }
class SCRoomRocketRewardRecordsRes { class SCRoomRocketRewardRecordsRes {
@ -255,7 +463,7 @@ class SCRoomRocketRewardRecordsRes {
return SCRoomRocketRewardRecordsRes( return SCRoomRocketRewardRecordsRes(
records: SCRoomRocketRewardRecordRes.listFromJson(data['records']), records: SCRoomRocketRewardRecordRes.listFromJson(data['records']),
cursor: _intValue(data['cursor'], fallback: 1), cursor: _intValue(data['cursor'], fallback: 1),
limit: _intValue(data['limit'], fallback: 20), limit: _intValue(data['limit'], fallback: 10),
emptyReason: _stringValue(data['emptyReason']), emptyReason: _stringValue(data['emptyReason']),
); );
} }
@ -264,6 +472,15 @@ class SCRoomRocketRewardRecordsRes {
final int cursor; final int cursor;
final int limit; final int limit;
final String emptyReason; final String emptyReason;
Map<String, dynamic> toJson() {
return {
"records": records.map((item) => item.toJson()).toList(growable: false),
"cursor": cursor,
"limit": limit,
"emptyReason": emptyReason,
};
}
} }
class SCRoomRocketRewardRecordRes { class SCRoomRocketRewardRecordRes {
@ -275,6 +492,9 @@ class SCRoomRocketRewardRecordRes {
required this.roundNo, required this.roundNo,
required this.level, required this.level,
required this.userId, required this.userId,
required this.userAvatar,
required this.userNickname,
required this.account,
required this.rewardScene, required this.rewardScene,
required this.rewardType, required this.rewardType,
required this.rewardItemId, required this.rewardItemId,
@ -299,6 +519,25 @@ class SCRoomRocketRewardRecordRes {
roundNo: _intValue(data['roundNo']), roundNo: _intValue(data['roundNo']),
level: _intValue(data['level']), level: _intValue(data['level']),
userId: _stringValue(data['userId']), userId: _stringValue(data['userId']),
userAvatar: _stringValue(
data['userAvatar'] ??
data['avatar'] ??
data['avatarUrl'] ??
_asMap(data['userProfile'] ?? data['user'])['userAvatar'] ??
_asMap(data['userProfile'] ?? data['user'])['avatar'],
),
userNickname: _stringValue(
data['userNickname'] ??
data['nickname'] ??
data['nickName'] ??
_asMap(data['userProfile'] ?? data['user'])['userNickname'] ??
_asMap(data['userProfile'] ?? data['user'])['nickname'] ??
_asMap(data['userProfile'] ?? data['user'])['nickName'],
),
account: _stringValue(
data['account'] ??
_asMap(data['userProfile'] ?? data['user'])['account'],
),
rewardScene: _stringValue(data['rewardScene']), rewardScene: _stringValue(data['rewardScene']),
rewardType: _stringValue(data['rewardType']), rewardType: _stringValue(data['rewardType']),
rewardItemId: _stringValue(data['rewardItemId']), rewardItemId: _stringValue(data['rewardItemId']),
@ -321,6 +560,9 @@ class SCRoomRocketRewardRecordRes {
final int roundNo; final int roundNo;
final int level; final int level;
final String userId; final String userId;
final String userAvatar;
final String userNickname;
final String account;
final String rewardScene; final String rewardScene;
final String rewardType; final String rewardType;
final String rewardItemId; final String rewardItemId;
@ -334,6 +576,19 @@ class SCRoomRocketRewardRecordRes {
final String createTime; final String createTime;
final String errorMessage; final String errorMessage;
String get displayName {
if (userNickname.trim().isNotEmpty) {
return userNickname;
}
if (account.trim().isNotEmpty) {
return account;
}
if (userId.trim().isNotEmpty) {
return 'ID:$userId';
}
return 'You';
}
String get displayAmount { String get displayAmount {
if (rewardAmount <= 0) { if (rewardAmount <= 0) {
return '1'; return '1';
@ -344,6 +599,33 @@ class SCRoomRocketRewardRecordRes {
return rewardAmount.toString(); return rewardAmount.toString();
} }
Map<String, dynamic> toJson() {
return {
"id": id,
"launchNo": launchNo,
"roomId": roomId,
"dayKey": dayKey,
"roundNo": roundNo,
"level": level,
"userId": userId,
"userAvatar": userAvatar,
"userNickname": userNickname,
"account": account,
"rewardScene": rewardScene,
"rewardType": rewardType,
"rewardItemId": rewardItemId,
"rewardName": rewardName,
"rewardCover": rewardCover,
"rewardAmount": rewardAmount,
"expireDays": expireDays,
"grantStatus": grantStatus,
"popupStatus": popupStatus,
"grantTime": grantTime,
"createTime": createTime,
"errorMessage": errorMessage,
};
}
static List<SCRoomRocketRewardRecordRes> listFromJson(dynamic value) { static List<SCRoomRocketRewardRecordRes> listFromJson(dynamic value) {
if (value is! Iterable) { if (value is! Iterable) {
return const <SCRoomRocketRewardRecordRes>[]; return const <SCRoomRocketRewardRecordRes>[];
@ -371,6 +653,62 @@ String _stringValue(dynamic value) {
return value.toString().trim(); return value.toString().trim();
} }
String _firstAssetString(Iterable<dynamic> values) {
for (final value in values) {
final text = _assetString(value);
if (text.isNotEmpty) {
return text;
}
}
return '';
}
String _assetString(dynamic value) {
if (value == null) {
return '';
}
if (value is Map) {
final map = _asMap(value);
return _firstAssetString([
map['sourceUrl'],
map['url'],
map['resourceUrl'],
map['fileUrl'],
map['src'],
map['path'],
map['cover'],
map['imageUrl'],
map['svgUrl'],
map['svg'],
map['value'],
]);
}
final map = _asMap(value);
if (map.isNotEmpty) {
return _firstAssetString([
map['sourceUrl'],
map['url'],
map['resourceUrl'],
map['fileUrl'],
map['src'],
map['path'],
map['cover'],
map['imageUrl'],
map['svgUrl'],
map['svg'],
map['value'],
]);
}
if (value is Iterable && value is! String) {
return _firstAssetString(value);
}
final text = _stringValue(value);
if (text == '{}' || text == '[]' || text.toLowerCase() == 'null') {
return '';
}
return text;
}
int _intValue(dynamic value, {int fallback = 0}) { int _intValue(dynamic value, {int fallback = 0}) {
if (value is int) { if (value is int) {
return value; return value;

View File

@ -46,6 +46,10 @@ class SCRoomRocketStatusRes {
List<SCRoomRocketLevelRes>? levels, List<SCRoomRocketLevelRes>? levels,
SCRoomRocketRewardPreviewRes? rewardPreview, SCRoomRocketRewardPreviewRes? rewardPreview,
List<SCRoomRocketKingUserRes>? rocketKings, List<SCRoomRocketKingUserRes>? rocketKings,
String? timezone,
int? resetRemainingSeconds,
Object? resetTime,
Object? serverTime,
}) { }) {
_roomId = roomId; _roomId = roomId;
_date = date; _date = date;
@ -72,6 +76,10 @@ class SCRoomRocketStatusRes {
_levels = levels; _levels = levels;
_rewardPreview = rewardPreview; _rewardPreview = rewardPreview;
_rocketKings = rocketKings; _rocketKings = rocketKings;
_timezone = timezone;
_resetRemainingSeconds = resetRemainingSeconds;
_resetTime = resetTime;
_serverTime = serverTime;
} }
SCRoomRocketStatusRes.fromJson(dynamic json) { SCRoomRocketStatusRes.fromJson(dynamic json) {
@ -104,6 +112,48 @@ class SCRoomRocketStatusRes {
json['rewardPreview'] == null json['rewardPreview'] == null
? null ? null
: SCRoomRocketRewardPreviewRes.fromJson(json['rewardPreview']); : SCRoomRocketRewardPreviewRes.fromJson(json['rewardPreview']);
_timezone = _stringValue(json['timezone'] ?? json['timeZone']);
final resetRemainingMillis = _intValue(
json['resetRemainingMillis'] ??
json['resetRemainMillis'] ??
json['remainingMillis'] ??
json['remainMillis'],
);
_resetRemainingSeconds =
_intValue(
json['resetRemainingSeconds'] ??
json['resetRemainSeconds'] ??
json['remainingSeconds'] ??
json['remainSeconds'] ??
json['countdownSeconds'] ??
json['resetCountdownSeconds'] ??
json['secondsToReset'] ??
json['resetSeconds'],
) ??
(resetRemainingMillis == null
? null
: (resetRemainingMillis / 1000).ceil());
_resetTime = _firstNonNull([
json['resetTime'],
json['resetAt'],
json['resetDateTime'],
json['resetTimestamp'],
json['resetTimeMillis'],
json['nextResetTime'],
json['nextResetAt'],
json['nextResetDateTime'],
json['nextResetTimestamp'],
json['nextResetTimeMillis'],
json['dayEndTime'],
json['nextDayTime'],
]);
_serverTime = _firstNonNull([
json['serverTime'],
json['currentTime'],
json['currentTimestamp'],
json['now'],
json['time'],
]);
} }
String? _roomId; String? _roomId;
@ -131,6 +181,10 @@ class SCRoomRocketStatusRes {
List<SCRoomRocketLevelRes>? _levels; List<SCRoomRocketLevelRes>? _levels;
SCRoomRocketRewardPreviewRes? _rewardPreview; SCRoomRocketRewardPreviewRes? _rewardPreview;
List<SCRoomRocketKingUserRes>? _rocketKings; List<SCRoomRocketKingUserRes>? _rocketKings;
String? _timezone;
int? _resetRemainingSeconds;
Object? _resetTime;
Object? _serverTime;
String? get roomId => _roomId; String? get roomId => _roomId;
@ -182,6 +236,14 @@ class SCRoomRocketStatusRes {
List<SCRoomRocketKingUserRes>? get rocketKings => _rocketKings; List<SCRoomRocketKingUserRes>? get rocketKings => _rocketKings;
String? get timezone => _timezone;
int? get resetRemainingSeconds => _resetRemainingSeconds;
Object? get resetTime => _resetTime;
Object? get serverTime => _serverTime;
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final map = <String, dynamic>{}; final map = <String, dynamic>{};
map['roomId'] = _roomId; map['roomId'] = _roomId;
@ -206,6 +268,10 @@ class SCRoomRocketStatusRes {
map['needEnergy'] = _needEnergy; map['needEnergy'] = _needEnergy;
map['displayPercent'] = _displayPercent; map['displayPercent'] = _displayPercent;
map['shake'] = _shake; map['shake'] = _shake;
map['timezone'] = _timezone;
map['resetRemainingSeconds'] = _resetRemainingSeconds;
map['resetTime'] = _resetTime;
map['serverTime'] = _serverTime;
return map; return map;
} }
@ -240,6 +306,27 @@ class SCRoomRocketStatusRes {
return null; return null;
} }
static String? _stringValue(dynamic value) {
final text = value?.toString().trim();
if (text == null || text.isEmpty) {
return null;
}
return text;
}
static Object? _firstNonNull(List<Object?> values) {
for (final value in values) {
if (value == null) {
continue;
}
if (value is String && value.trim().isEmpty) {
continue;
}
return value;
}
return null;
}
static List<SCRoomRocketLevelRes>? _levelList(dynamic value) { static List<SCRoomRocketLevelRes>? _levelList(dynamic value) {
if (value is! Iterable) { if (value is! Iterable) {
return null; return null;

View File

@ -0,0 +1,76 @@
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
class SCUserBadgeRes {
SCUserBadgeRes({
this.userId,
List<WearBadge>? badges,
List<WearBadge>? longBadges,
List<WearBadge>? shortBadges,
}) : badges = badges ?? const <WearBadge>[],
longBadges = longBadges ?? const <WearBadge>[],
shortBadges = shortBadges ?? const <WearBadge>[];
factory SCUserBadgeRes.empty({String? userId}) {
return SCUserBadgeRes(userId: userId);
}
factory SCUserBadgeRes.fromJson(dynamic json) {
final source = _bodyFrom(json);
if (source is! Map) {
return SCUserBadgeRes.empty();
}
return SCUserBadgeRes(
userId: _stringValue(source['userId']),
badges: _badgeList(source['badges']),
longBadges: _badgeList(source['longBadges']),
shortBadges: _badgeList(source['shortBadges']),
);
}
final String? userId;
final List<WearBadge> badges;
final List<WearBadge> longBadges;
final List<WearBadge> shortBadges;
List<WearBadge> get displayBadges {
if (longBadges.isNotEmpty || shortBadges.isNotEmpty) {
return <WearBadge>[...longBadges, ...shortBadges];
}
return badges;
}
static dynamic _bodyFrom(dynamic json) {
if (json is Map) {
final body = json['body'];
if (body is Map) {
return body;
}
final data = json['data'];
if (data is Map) {
return data;
}
}
return json;
}
static List<WearBadge> _badgeList(dynamic value) {
if (value is! List) {
return const <WearBadge>[];
}
return value
.whereType<Map>()
.map((item) => WearBadge.fromJson(item))
.where((badge) => _badgeUrl(badge).isNotEmpty)
.toList();
}
static String _badgeUrl(WearBadge badge) {
return (badge.selectUrl ?? badge.notSelectUrl ?? badge.animationUrl ?? "")
.trim();
}
static String? _stringValue(dynamic value) {
final text = value?.toString().trim();
return text == null || text.isEmpty ? null : text;
}
}

View File

@ -190,6 +190,17 @@ abstract class SocialChatRoomRepository {
/// ///
Future<List<SocialChatGiftBackpackRes>> giftBackpack(); Future<List<SocialChatGiftBackpackRes>> giftBackpack();
///
Future<Object?> giveBackpackGift({
required String id,
required String acceptUserId,
required String giftId,
required num quantity,
String? roomId,
num? seatIndex,
String? songId,
});
/// ///
Future<SCRoomRocketStatusRes> rocketStatus(String roomId); Future<SCRoomRocketStatusRes> rocketStatus(String roomId);
@ -203,7 +214,11 @@ abstract class SocialChatRoomRepository {
}); });
/// ///
Future<SCRoomRocketRewardPopupRes> roomRocketRewardPopups(String roomId); Future<SCRoomRocketRewardPopupRes> roomRocketRewardPopups(
String roomId, {
int cursor = 1,
int limit = 10,
});
/// ///
Future<bool> ackRoomRocketRewardPopups(List<String> rewardRecordIds); Future<bool> ackRoomRocketRewardPopups(List<String> rewardRecordIds);
@ -212,7 +227,7 @@ abstract class SocialChatRoomRepository {
Future<SCRoomRocketRewardRecordsRes> roomRocketRewardRecords( Future<SCRoomRocketRewardRecordsRes> roomRocketRewardRecords(
String roomId, { String roomId, {
int cursor = 1, int cursor = 1,
int limit = 20, int limit = 10,
}); });
/// ///

View File

@ -22,6 +22,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_rtc_token_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_sign_in_reward_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_sign_in_reward_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_badge_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_level_exp_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_level_exp_res.dart';
@ -63,7 +64,13 @@ abstract class SocialChatUserRepository {
Future<SocialChatRoomRes> createRoom(); Future<SocialChatRoomRes> createRoom();
/// ///
Future<JoinRoomRes> entryRoom(String roomId, {String? pwd}); Future<JoinRoomRes> entryRoom(
String roomId, {
String? pwd,
String redPackId = "",
bool needOpenRedenvelope = false,
bool silentErrorToast = false,
});
/// ///
Future heartbeat(String status, bool upMick, {String? roomId}); Future heartbeat(String status, bool upMick, {String? roomId});
@ -248,6 +255,12 @@ abstract class SocialChatUserRepository {
/// ///
Future<List<WearBadge>> badgeOwnList(String type); Future<List<WearBadge>> badgeOwnList(String type);
///
Future<SCUserBadgeRes> currentUserBadges();
///
Future<SCUserBadgeRes> otherUserBadges(String userId);
/// ///
Future<bool> badgeToggle(String badgeId); Future<bool> badgeToggle(String badgeId);

View File

@ -3,8 +3,11 @@ class RoomRocketLaunchBroadcastMessage {
required this.roomId, required this.roomId,
required this.roomAccount, required this.roomAccount,
required this.roomName, required this.roomName,
required this.roomCoverUrl,
required this.roundNo, required this.roundNo,
required this.level, required this.level,
required this.rocketIconUrl,
required this.rocketAnimationUrl,
required this.durationSeconds, required this.durationSeconds,
required this.triggerUser, required this.triggerUser,
required this.triggerUserId, required this.triggerUserId,
@ -17,8 +20,11 @@ class RoomRocketLaunchBroadcastMessage {
roomId: _stringValue(data['roomId']), roomId: _stringValue(data['roomId']),
roomAccount: _stringValue(data['roomAccount']), roomAccount: _stringValue(data['roomAccount']),
roomName: _stringValue(data['roomName']), roomName: _stringValue(data['roomName']),
roomCoverUrl: _roomCoverUrl(data),
roundNo: _intValue(data['roundNo']), roundNo: _intValue(data['roundNo']),
level: _intValue(data['level'], fallback: 1), level: _intValue(data['level'], fallback: 1),
rocketIconUrl: _rocketIconUrl(data),
rocketAnimationUrl: _rocketAnimationUrl(data),
durationSeconds: _intValue(data['durationSeconds'], fallback: 7), durationSeconds: _intValue(data['durationSeconds'], fallback: 7),
triggerUser: RoomRocketLaunchTriggerUser.fromJson(data['triggerUser']), triggerUser: RoomRocketLaunchTriggerUser.fromJson(data['triggerUser']),
triggerUserId: _stringValue(data['triggerUserId']), triggerUserId: _stringValue(data['triggerUserId']),
@ -29,8 +35,11 @@ class RoomRocketLaunchBroadcastMessage {
final String roomId; final String roomId;
final String roomAccount; final String roomAccount;
final String roomName; final String roomName;
final String roomCoverUrl;
final int roundNo; final int roundNo;
final int level; final int level;
final String rocketIconUrl;
final String rocketAnimationUrl;
final int durationSeconds; final int durationSeconds;
final RoomRocketLaunchTriggerUser triggerUser; final RoomRocketLaunchTriggerUser triggerUser;
final String triggerUserId; final String triggerUserId;
@ -103,6 +112,80 @@ String _stringValue(dynamic value) {
return value.toString().trim(); return value.toString().trim();
} }
String _roomCoverUrl(Map<String, dynamic> data) {
return _firstStringValue([
data['roomCoverUrl'],
data['roomCover'],
data['roomAvatar'],
data['roomIcon'],
data['coverUrl'],
data['cover'],
_asMap(data['room'])['coverUrl'],
_asMap(data['room'])['cover'],
_asMap(data['room'])['avatar'],
]);
}
String _rocketIconUrl(Map<String, dynamic> data) {
return _firstStringValue([
data['rocket_icon_url'],
data['rocketIconUrl'],
data['rocketIcon'],
data['rocketUrl'],
data['rocketImageUrl'],
data['rocketImage'],
data['iconUrl'],
_asMap(data['rocket'])['rocket_icon_url'],
_asMap(data['rocket'])['rocketIconUrl'],
_asMap(data['rocket'])['iconUrl'],
_asMap(data['rocket'])['icon'],
_asMap(data['levelInfo'])['rocket_icon_url'],
_asMap(data['levelInfo'])['rocketIconUrl'],
_asMap(data['rocketLevel'])['rocket_icon_url'],
_asMap(data['rocketLevel'])['rocketIconUrl'],
_asMap(data['config'])['rocket_icon_url'],
_asMap(data['config'])['rocketIconUrl'],
]);
}
String _rocketAnimationUrl(Map<String, dynamic> data) {
return _firstStringValue([
data['rocket_animation_url'],
data['rocketAnimationUrl'],
data['rocketSvgaUrl'],
data['animationUrl'],
data['svgaUrl'],
data['rocketLaunchSvgaUrl'],
data['launchAnimationUrl'],
data['launchSvgaUrl'],
data['sourceUrl'],
_asMap(data['rocket'])['rocket_animation_url'],
_asMap(data['rocket'])['rocketAnimationUrl'],
_asMap(data['rocket'])['rocketSvgaUrl'],
_asMap(data['rocket'])['animationUrl'],
_asMap(data['rocket'])['svgaUrl'],
_asMap(data['levelInfo'])['rocket_animation_url'],
_asMap(data['levelInfo'])['rocketAnimationUrl'],
_asMap(data['levelInfo'])['rocketSvgaUrl'],
_asMap(data['rocketLevel'])['rocket_animation_url'],
_asMap(data['rocketLevel'])['rocketAnimationUrl'],
_asMap(data['rocketLevel'])['rocketSvgaUrl'],
_asMap(data['config'])['rocket_animation_url'],
_asMap(data['config'])['rocketAnimationUrl'],
_asMap(data['config'])['rocketSvgaUrl'],
]);
}
String _firstStringValue(Iterable<dynamic> values) {
for (final value in values) {
final text = _stringValue(value);
if (text.isNotEmpty) {
return text;
}
}
return '';
}
int _intValue(dynamic value, {int fallback = 0}) { int _intValue(dynamic value, {int fallback = 0}) {
if (value is int) { if (value is int) {
return value; return value;

View File

@ -27,6 +27,9 @@ class OverlayManager {
static const int _maxDeferredRegionRedPacketCount = 12; static const int _maxDeferredRegionRedPacketCount = 12;
static const Duration _compensationDedupWindow = Duration(seconds: 3); static const Duration _compensationDedupWindow = Duration(seconds: 3);
static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30); static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30);
static const List<int> _stackedFloatingOrder = [5, 1, 4, 3];
static const double _stackedFloatingBaseTop = 70;
static const double _stackedFloatingGap = 8;
final SCPriorityQueue<SCFloatingMessage> _messageQueue = SCPriorityQueue( final SCPriorityQueue<SCFloatingMessage> _messageQueue = SCPriorityQueue(
(a, b) => b.priority.compareTo(a.priority), (a, b) => b.priority.compareTo(a.priority),
@ -34,6 +37,8 @@ class OverlayManager {
final List<SCFloatingMessage> _deferredRegionRedPacketMessages = []; final List<SCFloatingMessage> _deferredRegionRedPacketMessages = [];
final Map<String, DateTime> _recentCompensationKeys = {}; final Map<String, DateTime> _recentCompensationKeys = {};
final Map<String, DateTime> _recentFloatingKeys = {}; final Map<String, DateTime> _recentFloatingKeys = {};
final Map<int, OverlayEntry> _stackedOverlayEntries = {};
final Map<int, SCFloatingMessage> _stackedMessages = {};
bool _isPlaying = false; bool _isPlaying = false;
OverlayEntry? _currentOverlayEntry; OverlayEntry? _currentOverlayEntry;
SCFloatingMessage? _currentMessage; SCFloatingMessage? _currentMessage;
@ -55,7 +60,6 @@ class OverlayManager {
if (_isRecentFloatingDuplicate(message)) { if (_isRecentFloatingDuplicate(message)) {
return; return;
} }
if (message.type == 4) {}
_rememberFloatingMessage(message); _rememberFloatingMessage(message);
_enqueueMessage( _enqueueMessage(
message, message,
@ -63,6 +67,7 @@ class OverlayManager {
); );
} else { } else {
_removeActiveMessage(scheduleNext: false); _removeActiveMessage(scheduleNext: false);
_removeAllStackedMessages();
_messageQueue.clear(); _messageQueue.clear();
_deferredRegionRedPacketMessages.clear(); _deferredRegionRedPacketMessages.clear();
_isPlaying = false; _isPlaying = false;
@ -238,12 +243,14 @@ class OverlayManager {
await SCSvgaAssetWidget.preloadAsset( await SCSvgaAssetWidget.preloadAsset(
FloatingLuckGiftScreenWidget.backgroundSvgaAssetPath, FloatingLuckGiftScreenWidget.backgroundSvgaAssetPath,
); );
} catch (error) {} } catch (error) {
// Preload failure is non-fatal; the banner will retry during rendering.
}
} }
void _safeScheduleNext() { void _safeScheduleNext() {
if (_isSuppressed) return; if (_isSuppressed) return;
if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return; if (_isProcessing || _messageQueue.isEmpty) return;
_isProcessing = true; _isProcessing = true;
try { try {
@ -254,7 +261,7 @@ class OverlayManager {
} }
void _scheduleNext() { void _scheduleNext() {
if (_isPlaying || _messageQueue.isEmpty || _isDisposed || _isSuppressed) { if (_messageQueue.isEmpty || _isDisposed || _isSuppressed) {
return; return;
} }
@ -262,37 +269,64 @@ class OverlayManager {
final context = navigatorKey.currentState?.context; final context = navigatorKey.currentState?.context;
if (context == null || !context.mounted) return; if (context == null || !context.mounted) return;
// while (_messageQueue.isNotEmpty) {
if (_messageQueue.isEmpty) return; final messageToPlay = _removeNextPlayableMessage(context);
final messageToProcess = _messageQueue.first; if (messageToPlay == null) {
if (messageToProcess == null) { return;
_messageQueue.removeFirst(); }
_safeScheduleNext(); _playMessage(messageToPlay);
return; if (!_isStackedFloatingMessage(messageToPlay)) {
}
if (!_shouldDisplayMessage(context, messageToProcess)) {
_messageQueue.removeFirst();
if (_shouldDeferRegionRedPacketMessage(context, messageToProcess)) {
_deferRegionRedPacketMessage(context, messageToProcess);
_safeScheduleNext();
return; return;
} }
_logRegionRedPacketDrop(context, messageToProcess);
_safeScheduleNext();
return;
} }
//
final messageToPlay = _messageQueue.removeFirst();
_playMessage(messageToPlay);
} catch (e) { } catch (e) {
_isPlaying = false; _isPlaying = false;
_safeScheduleNext(); _safeScheduleNext();
} }
} }
SCFloatingMessage? _removeNextPlayableMessage(BuildContext context) {
final skippedMessages = <SCFloatingMessage>[];
SCFloatingMessage? selectedMessage;
while (_messageQueue.isNotEmpty) {
final message = _messageQueue.removeFirst();
if (!_shouldDisplayMessage(context, message)) {
if (_shouldDeferRegionRedPacketMessage(context, message)) {
_deferRegionRedPacketMessage(context, message);
continue;
}
_logRegionRedPacketDrop(context, message);
continue;
}
if (!_canPlayMessage(message)) {
skippedMessages.add(message);
continue;
}
selectedMessage = message;
break;
}
for (final message in skippedMessages) {
_messageQueue.add(message);
}
return selectedMessage;
}
bool _canPlayMessage(SCFloatingMessage message) {
final slotKey = _stackSlotKeyFor(message);
if (slotKey != null) {
return !_isPlaying && !_stackedOverlayEntries.containsKey(slotKey);
}
return !_isPlaying && _stackedOverlayEntries.isEmpty;
}
void _playMessage(SCFloatingMessage message) { void _playMessage(SCFloatingMessage message) {
if (_isStackedFloatingMessage(message)) {
_playStackedMessage(message);
return;
}
_isPlaying = true; _isPlaying = true;
_currentMessage = message; _currentMessage = message;
final context = navigatorKey.currentState?.context; final context = navigatorKey.currentState?.context;
@ -303,21 +337,6 @@ class OverlayManager {
return; return;
} }
_currentOverlayEntry = OverlayEntry(
builder:
(_) => Align(
alignment: AlignmentDirectional.topStart,
child: Transform.translate(
offset: Offset(0, 70.w),
child: RepaintBoundary(child: _buildScreenWidget(message)),
),
),
);
Overlay.of(context).insert(_currentOverlayEntry!);
}
Widget _buildScreenWidget(SCFloatingMessage message) {
bool completed = false; bool completed = false;
void onComplete() { void onComplete() {
@ -337,6 +356,66 @@ class OverlayManager {
} }
} }
_currentOverlayEntry = OverlayEntry(
builder:
(_) => Align(
alignment: AlignmentDirectional.topStart,
child: Transform.translate(
offset: Offset(0, 70.w),
child: RepaintBoundary(
child: _buildScreenWidget(message, onComplete),
),
),
),
);
Overlay.of(context).insert(_currentOverlayEntry!);
}
void _playStackedMessage(SCFloatingMessage message) {
final slotKey = _stackSlotKeyFor(message);
final context = navigatorKey.currentState?.context;
if (slotKey == null || context == null || !context.mounted) {
return;
}
if (_stackedOverlayEntries.containsKey(slotKey)) {
_messageQueue.add(message);
return;
}
bool completed = false;
void onComplete() {
if (completed) return;
completed = true;
_removeStackedMessage(slotKey);
_markStackedEntriesNeedsBuild();
_safeScheduleNext();
}
_stackedMessages[slotKey] = message;
final entry = OverlayEntry(
builder:
(_) => Align(
alignment: AlignmentDirectional.topStart,
child: Transform.translate(
offset: Offset(0, _stackedWrapperTopFor(message).w),
child: RepaintBoundary(
child: _buildScreenWidget(message, onComplete),
),
),
),
);
_stackedOverlayEntries[slotKey] = entry;
Overlay.of(context).insert(entry);
_markStackedEntriesNeedsBuild();
}
Widget _buildScreenWidget(
SCFloatingMessage message,
VoidCallback onComplete,
) {
switch (message.type) { switch (message.type) {
case 0: case 0:
return FloatingLuckGiftScreenWidget( return FloatingLuckGiftScreenWidget(
@ -379,6 +458,94 @@ class OverlayManager {
} }
} }
int? _stackSlotKeyFor(SCFloatingMessage message) {
switch (message.type) {
case 0:
case 1:
return 1;
case 3:
return 3;
case 4:
return 4;
case 5:
return 5;
default:
return null;
}
}
bool _isStackedFloatingMessage(SCFloatingMessage message) {
return _stackSlotKeyFor(message) != null;
}
double _stackedWrapperTopFor(SCFloatingMessage message) {
final slotKey = _stackSlotKeyFor(message);
if (slotKey == null) {
return _stackedFloatingBaseTop;
}
var visualTop = _stackedFloatingBaseTop;
for (final key in _stackedFloatingOrder) {
if (key == slotKey) {
break;
}
final previousMessage = _stackedMessages[key];
if (previousMessage != null) {
visualTop +=
_stackedVisualHeightFor(previousMessage) + _stackedFloatingGap;
}
}
final wrapperTop = visualTop - _stackedInternalTopFor(message);
return wrapperTop < 0 ? 0 : wrapperTop;
}
double _stackedVisualHeightFor(SCFloatingMessage message) {
switch (message.type) {
case 0:
return 83;
case 1:
return 54;
case 3:
return 68;
case 4:
return 66;
case 5:
return 84;
default:
return 68;
}
}
double _stackedInternalTopFor(SCFloatingMessage message) {
switch (message.type) {
case 1:
return 20;
case 4:
return 42;
default:
return 0;
}
}
void _markStackedEntriesNeedsBuild() {
for (final entry in _stackedOverlayEntries.values) {
entry.markNeedsBuild();
}
}
void _removeStackedMessage(int slotKey) {
try {
_stackedOverlayEntries.remove(slotKey)?.remove();
} catch (_) {}
_stackedMessages.remove(slotKey);
}
void _removeAllStackedMessages() {
final slotKeys = List<int>.from(_stackedOverlayEntries.keys);
for (final slotKey in slotKeys) {
_removeStackedMessage(slotKey);
}
}
void activate() { void activate() {
_isDisposed = false; _isDisposed = false;
_loadSavedDisplayPreference(); _loadSavedDisplayPreference();
@ -426,6 +593,7 @@ class OverlayManager {
void beginSuppressFloatingScreens({String reason = 'unknown'}) { void beginSuppressFloatingScreens({String reason = 'unknown'}) {
_suppressedCount += 1; _suppressedCount += 1;
_removeActiveMessage(scheduleNext: false); _removeActiveMessage(scheduleNext: false);
_removeAllStackedMessages();
_messageQueue.clear(); _messageQueue.clear();
_isPlaying = false; _isPlaying = false;
_isProcessing = false; _isProcessing = false;
@ -446,6 +614,7 @@ class OverlayManager {
} }
if (SCGlobalConfig.isFloatingBroadcastClosed) { if (SCGlobalConfig.isFloatingBroadcastClosed) {
_removeActiveMessage(scheduleNext: false); _removeActiveMessage(scheduleNext: false);
_removeAllStackedMessages();
_messageQueue.clear(); _messageQueue.clear();
_deferredRegionRedPacketMessages.clear(); _deferredRegionRedPacketMessages.clear();
return; return;
@ -457,8 +626,10 @@ class OverlayManager {
context.mounted && context.mounted &&
activeMessage != null && activeMessage != null &&
!_shouldDisplayMessage(context, activeMessage)) { !_shouldDisplayMessage(context, activeMessage)) {
_removeActiveMessage(); _removeActiveMessage(scheduleNext: false);
return; }
if (context != null && context.mounted) {
_removeHiddenStackedMessages(context);
} }
_safeScheduleNext(); _safeScheduleNext();
} }
@ -470,6 +641,7 @@ class OverlayManager {
_currentOverlayEntry?.remove(); _currentOverlayEntry?.remove();
_currentOverlayEntry = null; _currentOverlayEntry = null;
_currentMessage = null; _currentMessage = null;
_removeAllStackedMessages();
_messageQueue.clear(); _messageQueue.clear();
_deferredRegionRedPacketMessages.clear(); _deferredRegionRedPacketMessages.clear();
_recentCompensationKeys.clear(); _recentCompensationKeys.clear();
@ -486,6 +658,9 @@ class OverlayManager {
_removeMessagesByType(1); _removeMessagesByType(1);
_removeMessagesByType(0); _removeMessagesByType(0);
_removeMessagesByType(5); _removeMessagesByType(5);
if (normalizedRoomId.isNotEmpty) {
_removeMessagesByType(3, roomId: normalizedRoomId);
}
if (normalizedRoomId.isNotEmpty) { if (normalizedRoomId.isNotEmpty) {
_removeMessagesByType(4, roomId: normalizedRoomId); _removeMessagesByType(4, roomId: normalizedRoomId);
} }
@ -516,12 +691,20 @@ class OverlayManager {
while (newQueue.isNotEmpty) { while (newQueue.isNotEmpty) {
_messageQueue.add(newQueue.removeFirst()); _messageQueue.add(newQueue.removeFirst());
} }
_removeStackedMessagesWhere((message) {
final matchesType = message.type == type;
final matchesRoom =
normalizedRoomId.isEmpty ||
(message.roomId ?? "").trim() == normalizedRoomId;
return matchesType && matchesRoom;
});
} }
// //
int get queueLength => _messageQueue.length; int get queueLength => _messageQueue.length;
bool get isPlaying => _isPlaying; bool get isPlaying => _isPlaying || _stackedOverlayEntries.isNotEmpty;
bool get _isSuppressed => _suppressedCount > 0; bool get _isSuppressed => _suppressedCount > 0;
@ -580,19 +763,6 @@ class OverlayManager {
} }
} }
String _displayStateForLog(BuildContext context) {
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
context,
listen: false,
);
final currentRoomId =
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
return 'homeVisible=$_homeRootTabsVisible '
'roomVisible=${rtcProvider.shouldShowRoomVisualEffects} '
'currentRoomId=$currentRoomId '
'scope=${SCGlobalConfig.floatingBroadcastScope}';
}
bool _shouldDisplayMessage(BuildContext context, SCFloatingMessage? message) { bool _shouldDisplayMessage(BuildContext context, SCFloatingMessage? message) {
if (message == null) { if (message == null) {
return false; return false;
@ -702,13 +872,13 @@ class OverlayManager {
void _removeActiveRegionBroadcastMessage() { void _removeActiveRegionBroadcastMessage() {
final activeMessage = _currentMessage; final activeMessage = _currentMessage;
if (activeMessage == null || !activeMessage.isRegionBroadcast) { if (activeMessage != null && activeMessage.isRegionBroadcast) {
return; _currentOverlayEntry?.remove();
_currentOverlayEntry = null;
_currentMessage = null;
_isPlaying = false;
} }
_currentOverlayEntry?.remove(); _removeStackedMessagesWhere((message) => message.isRegionBroadcast);
_currentOverlayEntry = null;
_currentMessage = null;
_isPlaying = false;
_safeScheduleNext(); _safeScheduleNext();
} }
@ -718,18 +888,21 @@ class OverlayManager {
}) { }) {
final activeMessage = _currentMessage; final activeMessage = _currentMessage;
final normalizedRoomId = roomId?.trim() ?? ''; final normalizedRoomId = roomId?.trim() ?? '';
if (activeMessage == null || if (activeMessage != null &&
(activeMessage.type != 0 && (activeMessage.type == 0 ||
activeMessage.type != 1 && activeMessage.type == 1 ||
activeMessage.type != 5 && activeMessage.type == 3 ||
!(includeRedPacket && activeMessage.type == 4))) { activeMessage.type == 5 ||
return; (includeRedPacket && activeMessage.type == 4)) &&
(normalizedRoomId.isEmpty ||
(activeMessage.roomId ?? "").trim() == normalizedRoomId)) {
_removeActiveMessage(scheduleNext: false);
} }
if (normalizedRoomId.isNotEmpty && _removeStackedRoomMessages(
(activeMessage.roomId ?? "").trim() != normalizedRoomId) { roomId: normalizedRoomId,
return; includeRedPacket: includeRedPacket,
} );
_removeActiveMessage(); _safeScheduleNext();
} }
void _removeActiveMessage({bool scheduleNext = true}) { void _removeActiveMessage({bool scheduleNext = true}) {
@ -741,4 +914,47 @@ class OverlayManager {
_safeScheduleNext(); _safeScheduleNext();
} }
} }
void _removeHiddenStackedMessages(BuildContext context) {
_removeStackedMessagesWhere(
(message) => !_shouldDisplayMessage(context, message),
);
}
void _removeStackedRoomMessages({
required String roomId,
required bool includeRedPacket,
}) {
_removeStackedMessagesWhere((message) {
final type = message.type;
final isRoomFloatingType =
type == 0 ||
type == 1 ||
type == 3 ||
type == 5 ||
(includeRedPacket && type == 4);
if (!isRoomFloatingType) {
return false;
}
return roomId.isEmpty || (message.roomId ?? "").trim() == roomId;
});
}
void _removeStackedMessagesWhere(
bool Function(SCFloatingMessage message) test,
) {
final matchedSlotKeys = <int>[];
_stackedMessages.forEach((slotKey, message) {
if (test(message)) {
matchedSlotKeys.add(slotKey);
}
});
if (matchedSlotKeys.isEmpty) {
return;
}
for (final slotKey in matchedSlotKeys) {
_removeStackedMessage(slotKey);
}
_markStackedEntriesNeedsBuild();
}
} }

View File

@ -122,6 +122,193 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
debugPrint(parts.join(' ')); debugPrint(parts.join(' '));
} }
void _rocketStatusAssetDebug(dynamic raw, SCRoomRocketStatusRes result) {
if (!kDebugMode) {
return;
}
final rawLevels = _debugMap(raw)['levels'];
final rawList = rawLevels is Iterable ? rawLevels.toList() : const [];
final parsed = result.levels ?? const <SCRoomRocketLevelRes>[];
debugPrint(
'[RoomRocketApi] status asset summary levels=${parsed.length} '
'currentLevel=${result.currentLevel} displayPercent=${result.displayPercent}',
);
for (var index = 0; index < parsed.length; index += 1) {
final item = parsed[index];
final rawItem =
index < rawList.length
? _debugMap(rawList[index])
: const <String, dynamic>{};
debugPrint(
'[RoomRocketApi] status level asset '
'index=$index level=${item.level} rawKeys=${rawItem.keys.toList()} '
'rawIcon=${_shortDebugAsset(_debugAssetValue(rawItem, _debugIconKeys))} '
'rawPreview=${_shortDebugAsset(_debugAssetValue(rawItem, _debugPreviewKeys))} '
'rawAnimation=${_shortDebugAsset(_debugAssetValue(rawItem, _debugAnimationKeys))} '
'parsedIcon=${_shortDebugAsset(item.rocketIconUrl)} '
'centerPreview=${_shortDebugAsset(item.rocketPreviewUrl)} '
'parsedAnimation=${_shortDebugAsset(item.rocketAnimationUrl)} '
'progress=${_shortDebugAsset(item.progressBarUrl)} '
'shakeThreshold=${item.shakeThresholdPercent}',
);
}
}
Map<String, dynamic> _debugMap(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 const List<String> _debugIconKeys = [
'rocket_icon_url',
'rocketIconUrl',
'rocketIcon',
'iconUrl',
'icon',
];
static const List<String> _debugPreviewKeys = [
'preview_rocket_url',
'previewRocketUrl',
'rocketPreviewUrl',
'rocketPreviewSvgUrl',
'rocketPreviewSvg',
'rocketPreview',
'rocketSvgUrl',
'rocketSvg',
'previewSvgUrl',
'previewSvg',
'previewUrl',
'preview',
'svgUrl',
'svg',
'rocketDisplayUrl',
'rocketShowUrl',
'rocketImageUrl',
'previewImageUrl',
'sourceUrl',
];
static const List<String> _debugAnimationKeys = [
'rocket_animation_url',
'rocketAnimationUrl',
'rocketAnimation',
'rocketSvgaUrl',
'animationUrl',
'animation',
'svgaUrl',
'rocketLaunchSvgaUrl',
'launchAnimationUrl',
'launchAnimation',
'launchSvgaUrl',
'sourceUrl',
];
String _debugAssetValue(Map<String, dynamic> data, List<String> keys) {
for (final key in keys) {
final value = _debugNestedAssetValue(data[key]);
if (value.isNotEmpty) {
return value;
}
}
for (final nestedKey in const [
'rocket',
'levelInfo',
'config',
'resource',
'rocketResource',
'previewResource',
'animationResource',
]) {
final nested = _debugMap(data[nestedKey]);
if (nested.isEmpty) {
continue;
}
final value = _debugAssetValue(nested, keys);
if (value.isNotEmpty) {
return value;
}
}
return '';
}
String _debugNestedAssetValue(dynamic value) {
if (value is Map) {
final nested = _debugMap(value);
for (final key in const [
'sourceUrl',
'url',
'resourceUrl',
'fileUrl',
'src',
'path',
'cover',
'imageUrl',
'svgUrl',
'svg',
'value',
]) {
final text = _debugNestedAssetValue(nested[key]);
if (text.isNotEmpty) {
return text;
}
}
return '';
}
final nested = _debugMap(value);
if (nested.isNotEmpty) {
for (final key in const [
'sourceUrl',
'url',
'resourceUrl',
'fileUrl',
'src',
'path',
'cover',
'imageUrl',
'svgUrl',
'svg',
'value',
]) {
final text = _debugNestedAssetValue(nested[key]);
if (text.isNotEmpty) {
return text;
}
}
return '';
}
if (value is Iterable && value is! String) {
for (final item in value) {
final text = _debugNestedAssetValue(item);
if (text.isNotEmpty) {
return text;
}
}
return '';
}
final text = value?.toString().trim() ?? '';
if (text == '{}' || text == '[]' || text.toLowerCase() == 'null') {
return '';
}
return text;
}
String _shortDebugAsset(String value) {
final text = value.replaceAll(RegExp(r'\s+'), ' ').trim();
if (text.isEmpty) {
return '-';
}
if (text.length <= 140) {
return text;
}
return '${text.substring(0, 137)}...';
}
dynamic _jsonInt64(String value) { dynamic _jsonInt64(String value) {
final normalized = value.trim(); final normalized = value.trim();
return int.tryParse(normalized) ?? normalized; return int.tryParse(normalized) ?? normalized;
@ -552,23 +739,25 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
///room/setting/locked ///room/setting/locked
@override @override
Future<bool> roomLocked(String roomId, String password) async { Future<bool> roomLocked(String roomId, String password) async {
final result = await http.put<bool>( await http.put<Object?>(
"e9531da4dd5c83bd5386366519ac6d0b727c997eb7a1adb8323f850b6c541f3b", "e9531da4dd5c83bd5386366519ac6d0b727c997eb7a1adb8323f850b6c541f3b",
data: {"roomId": roomId, "password": password}, data: {"roomId": roomId, "password": password},
fromJson: (json) => json as bool, allowNullBody: true,
fromJson: (json) => json,
); );
return result; return true;
} }
///room/setting/unlocked ///room/setting/unlocked
@override @override
Future<bool> roomUnlocked(String roomId) async { Future<bool> roomUnlocked(String roomId) async {
final result = await http.post<bool>( await http.post<Object?>(
"d8e78b8fb4cf9f0d41716d9978235334d9382f6a419ce06c2dab0b362c318a5d", "d8e78b8fb4cf9f0d41716d9978235334d9382f6a419ce06c2dab0b362c318a5d",
data: {"roomId": roomId}, data: {"roomId": roomId},
fromJson: (json) => json as bool, allowNullBody: true,
fromJson: (json) => json,
); );
return result; return true;
} }
///room/setting ///room/setting
@ -768,11 +957,48 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
(json) => (json) =>
(json as List) (json as List)
.map((e) => SocialChatGiftBackpackRes.fromJson(e)) .map((e) => SocialChatGiftBackpackRes.fromJson(e))
.where((item) => item.giftConfig != null)
.toList(), .toList(),
); );
return result; return result;
} }
///gift/backpack/give-away
@override
Future<Object?> giveBackpackGift({
required String id,
required String acceptUserId,
required String giftId,
required num quantity,
String? roomId,
num? seatIndex,
String? songId,
}) async {
final params = <String, dynamic>{
"id": id,
"acceptUserId": acceptUserId,
"giftId": giftId,
"quantity": quantity,
};
if (roomId != null && roomId.trim().isNotEmpty) {
params["roomId"] = roomId;
}
if (seatIndex != null) {
params["seatIndex"] = seatIndex;
}
if (songId != null && songId.trim().isNotEmpty) {
params["songId"] = songId;
}
final result = await http.post<Object?>(
"/gift/backpack/give-away",
data: params,
allowNullBody: true,
extra: const {BaseNetworkClient.silentErrorToastKey: true},
fromJson: (json) => json,
);
return result;
}
///go/app/voice-room/rocket/status ///go/app/voice-room/rocket/status
@override @override
Future<SCRoomRocketStatusRes> rocketStatus(String roomId) async { Future<SCRoomRocketStatusRes> rocketStatus(String roomId) async {
@ -786,7 +1012,9 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
extra: {BaseNetworkClient.silentErrorToastKey: true}, extra: {BaseNetworkClient.silentErrorToastKey: true},
fromJson: (json) { fromJson: (json) {
_rocketApiDebug('GET $path', query: params, raw: json); _rocketApiDebug('GET $path', query: params, raw: json);
return SCRoomRocketStatusRes.fromJson(json); final status = SCRoomRocketStatusRes.fromJson(json);
_rocketStatusAssetDebug(json, status);
return status;
}, },
); );
_rocketApiDebug('parsed $path', query: params, result: result); _rocketApiDebug('parsed $path', query: params, result: result);
@ -827,9 +1055,15 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
///go/app/voice-room/rocket/reward-popups ///go/app/voice-room/rocket/reward-popups
@override @override
Future<SCRoomRocketRewardPopupRes> roomRocketRewardPopups( Future<SCRoomRocketRewardPopupRes> roomRocketRewardPopups(
String roomId, String roomId, {
) async { int cursor = 1,
final queryParams = {if (roomId.trim().isNotEmpty) "roomId": roomId}; int limit = 10,
}) async {
final queryParams = {
if (roomId.trim().isNotEmpty) "roomId": roomId,
"cursor": cursor,
"limit": limit,
};
const path = "/go/app/voice-room/rocket/reward-popups"; const path = "/go/app/voice-room/rocket/reward-popups";
_rocketApiDebug('request GET $path', query: queryParams); _rocketApiDebug('request GET $path', query: queryParams);
final result = await http.get<SCRoomRocketRewardPopupRes>( final result = await http.get<SCRoomRocketRewardPopupRes>(
@ -875,7 +1109,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
Future<SCRoomRocketRewardRecordsRes> roomRocketRewardRecords( Future<SCRoomRocketRewardRecordsRes> roomRocketRewardRecords(
String roomId, { String roomId, {
int cursor = 1, int cursor = 1,
int limit = 20, int limit = 10,
}) async { }) async {
final queryParams = { final queryParams = {
if (roomId.trim().isNotEmpty) "roomId": roomId, if (roomId.trim().isNotEmpty) "roomId": roomId,

View File

@ -12,6 +12,7 @@ import 'package:yumi/shared/business_logic/models/res/room_res.dart'
import 'package:yumi/shared/business_logic/models/res/sc_rtc_token_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_rtc_token_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_task_list_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_counter_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_badge_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_level_exp_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_level_exp_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_red_packet_grab_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_red_packet_grab_res.dart';
@ -183,6 +184,7 @@ class SCAccountRepository implements SocialChatUserRepository {
String? pwd, String? pwd,
String redPackId = "", String redPackId = "",
bool needOpenRedenvelope = false, bool needOpenRedenvelope = false,
bool silentErrorToast = false,
}) async { }) async {
var param = {}; var param = {};
param["roomId"] = roomId; param["roomId"] = roomId;
@ -195,6 +197,10 @@ class SCAccountRepository implements SocialChatUserRepository {
final result = await http.post( final result = await http.post(
"363603111e51beac2d183014dd29b81c219577507bcc9c6abfaf327548fed0e0", "363603111e51beac2d183014dd29b81c219577507bcc9c6abfaf327548fed0e0",
data: param, data: param,
extra:
silentErrorToast
? const {BaseNetworkClient.silentErrorToastKey: true}
: null,
fromJson: (json) => JoinRoomRes.fromJson(json), fromJson: (json) => JoinRoomRes.fromJson(json),
); );
return SCRoomProfileCache.applyToJoinRoomRes(result); return SCRoomProfileCache.applyToJoinRoomRes(result);
@ -1181,6 +1187,32 @@ class SCAccountRepository implements SocialChatUserRepository {
return result; return result;
} }
///app/user/badge/current
@override
Future<SCUserBadgeRes> currentUserBadges() async {
const path = "/app/user/badge/current";
final result = await http.get<SCUserBadgeRes>(
path,
extra: const {BaseNetworkClient.silentErrorToastKey: true},
fromJson: (json) => SCUserBadgeRes.fromJson(json),
);
return result;
}
///app/user/badge/other
@override
Future<SCUserBadgeRes> otherUserBadges(String userId) async {
final params = <String, dynamic>{"userId": userId};
const path = "/app/user/badge/other";
final result = await http.get<SCUserBadgeRes>(
path,
queryParams: params,
extra: const {BaseNetworkClient.silentErrorToastKey: true},
fromJson: (json) => SCUserBadgeRes.fromJson(json),
);
return result;
}
///user/badge/toggle ///user/badge/toggle
@override @override
Future<bool> badgeToggle(String badgeId) async { Future<bool> badgeToggle(String badgeId) async {

View File

@ -53,6 +53,7 @@ class SCPathUtils {
'.mp4', '.mp4',
'.vap', '.vap',
'.svga', '.svga',
'.pag',
].contains(ext); ].contains(ext);
return isDirectAsset && isSupportedAsset; return isDirectAsset && isSupportedAsset;
@ -76,6 +77,7 @@ class SCPathUtils {
'.mov', '.mov',
'.avi', '.avi',
'.svga', '.svga',
'.pag',
'.png', '.png',
'.jpg', '.jpg',
'.txt', '.txt',

View File

@ -23,6 +23,24 @@ typedef SCRoomUtils = SCChatRoomHelper;
class SCChatRoomHelper { class SCChatRoomHelper {
static Map<String, SocialChatUserProfile> roomUsersMap = {}; static Map<String, SocialChatUserProfile> roomUsersMap = {};
static bool isCurrentRoom(String roomId, BuildContext context) {
final normalizedRoomId = roomId.trim();
if (normalizedRoomId.isEmpty) {
return false;
}
try {
final currentRoomId =
Provider.of<RealTimeCommunicationManager>(
context,
listen: false,
).currenRoom?.roomProfile?.roomProfile?.id?.trim() ??
'';
return currentRoomId.isNotEmpty && currentRoomId == normalizedRoomId;
} catch (_) {
return false;
}
}
static void goRoom( static void goRoom(
String roomId, String roomId,
BuildContext context, { BuildContext context, {

View File

@ -0,0 +1,183 @@
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_badge_res.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
class SCUserBadgeStrip extends StatefulWidget {
const SCUserBadgeStrip({
super.key,
this.userId,
this.fallbackBadges = const <WearBadge>[],
this.height,
this.badgeHeight,
this.longBadgeWidth,
this.spacing,
this.maxBadges,
this.wrap = false,
this.reserveSpace = false,
this.alignment = WrapAlignment.center,
});
final String? userId;
final List<WearBadge> fallbackBadges;
final double? height;
final double? badgeHeight;
final double? longBadgeWidth;
final double? spacing;
final int? maxBadges;
final bool wrap;
final bool reserveSpace;
final WrapAlignment alignment;
@override
State<SCUserBadgeStrip> createState() => _SCUserBadgeStripState();
}
class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
static final Map<String, Future<SCUserBadgeRes>> _badgeFutures = {};
Future<SCUserBadgeRes>? _future;
@override
void initState() {
super.initState();
_future = _resolveFuture();
}
@override
void didUpdateWidget(covariant SCUserBadgeStrip oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.userId != widget.userId) {
_future = _resolveFuture();
}
}
Future<SCUserBadgeRes>? _resolveFuture() {
final userId = _normalizedUserId;
if (userId.isEmpty) {
return null;
}
final currentUserId =
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
final isCurrentUser = currentUserId.isNotEmpty && userId == currentUserId;
final cacheKey = isCurrentUser ? "current:$userId" : "other:$userId";
return _badgeFutures.putIfAbsent(cacheKey, () async {
try {
return isCurrentUser
? await SCAccountRepository().currentUserBadges()
: await SCAccountRepository().otherUserBadges(userId);
} catch (_) {
_badgeFutures.remove(cacheKey);
return SCUserBadgeRes.empty(userId: userId);
}
});
}
String get _normalizedUserId => widget.userId?.trim() ?? "";
@override
Widget build(BuildContext context) {
final stripHeight = widget.height ?? 25.w;
final future = _future;
if (future == null) {
return _buildBadges(_fallbackBadges, stripHeight);
}
return FutureBuilder<SCUserBadgeRes>(
future: future,
builder: (context, snapshot) {
final remoteBadges =
snapshot.data?.displayBadges ?? const <WearBadge>[];
final badges = remoteBadges.isNotEmpty ? remoteBadges : _fallbackBadges;
return _buildBadges(badges, stripHeight);
},
);
}
List<WearBadge> get _fallbackBadges {
return widget.fallbackBadges
.where((badge) => _badgeUrl(badge).isNotEmpty)
.toList();
}
Widget _buildBadges(List<WearBadge> rawBadges, double stripHeight) {
final badges =
widget.maxBadges == null
? rawBadges
: rawBadges.take(widget.maxBadges!).toList();
if (badges.isEmpty) {
return widget.reserveSpace
? SizedBox(height: stripHeight)
: const SizedBox.shrink();
}
final badgeHeight = widget.badgeHeight ?? stripHeight;
final spacing = widget.spacing ?? 5.w;
final children =
badges
.map((badge) => _buildBadge(badge, badgeHeight: badgeHeight))
.toList();
if (widget.wrap) {
return SizedBox(
height: stripHeight,
child: Center(
child: Wrap(
alignment: widget.alignment,
spacing: spacing,
runSpacing: 5.w,
children: children,
),
),
);
}
return SizedBox(
height: stripHeight,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
child: Row(children: _withSpacing(children, spacing)),
),
);
}
Widget _buildBadge(WearBadge badge, {required double badgeHeight}) {
final type = badge.type?.trim().toUpperCase() ?? "";
final isLongBadge = type == "LONG";
final width =
isLongBadge
? (widget.longBadgeWidth ?? badgeHeight * 2.2)
: badgeHeight;
return netImage(
url: _badgeUrl(badge),
width: width,
height: badgeHeight,
fit: BoxFit.contain,
noDefaultImg: true,
loadingWidget: const SizedBox.shrink(),
errorWidget: const SizedBox.shrink(),
);
}
String _badgeUrl(WearBadge badge) {
return (badge.selectUrl ?? badge.notSelectUrl ?? badge.animationUrl ?? "")
.trim();
}
List<Widget> _withSpacing(List<Widget> children, double spacing) {
if (children.length < 2) {
return children;
}
final spaced = <Widget>[];
for (var i = 0; i < children.length; i++) {
if (i > 0) {
spaced.add(SizedBox(width: spacing));
}
spaced.add(children[i]);
}
return spaced;
}
}

View File

@ -247,7 +247,7 @@ class EmptyMaiSelect extends StatelessWidget {
context, context,
() { () {
Navigator.of(context).pop(); Navigator.of(context).pop();
showBottomInCenterDialog( showBottomInBottomDialog(
context, context,
RoomUserInfoCard(userId: clickUser?.id), RoomUserInfoCard(userId: clickUser?.id),
); );

View File

@ -2,10 +2,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_debouncer/flutter_debouncer.dart'; import 'package:flutter_debouncer/flutter_debouncer.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/components/text/sc_text.dart';
import 'package:yumi/shared/tools/sc_room_utils.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart';
import 'package:yumi/main.dart'; import 'package:yumi/main.dart';
import 'package:marquee/marquee.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
@ -15,13 +13,13 @@ class FloatingRoomRocketScreenWidget extends StatefulWidget {
final VoidCallback onAnimationCompleted; // final VoidCallback onAnimationCompleted; //
const FloatingRoomRocketScreenWidget({ const FloatingRoomRocketScreenWidget({
Key? key, super.key,
required this.message, required this.message,
required this.onAnimationCompleted, required this.onAnimationCompleted,
}) : super(key: key); });
@override @override
_FloatingRoomRocketScreenWidgetState createState() => State<FloatingRoomRocketScreenWidget> createState() =>
_FloatingRoomRocketScreenWidgetState(); _FloatingRoomRocketScreenWidgetState();
} }
@ -108,10 +106,7 @@ class _FloatingRoomRocketScreenWidgetState
} }
Duration get _holdDuration { Duration get _holdDuration {
final totalSeconds = widget.message.durationSeconds; return const Duration(seconds: 7);
final totalMs = totalSeconds == null ? 6000 : (totalSeconds * 1000).round();
final holdMs = totalMs - 3000;
return Duration(milliseconds: holdMs.clamp(0, 10000).toInt());
} }
@override @override
@ -139,19 +134,32 @@ class _FloatingRoomRocketScreenWidgetState
} }
Widget _buildRocketAnimation() { Widget _buildRocketAnimation() {
final roomName = _firstNonBlank([
widget.message.userName,
widget.message.userId,
widget.message.roomId,
]);
final rocketIconUrl = _firstNonBlank([
widget.message.giftUrl,
widget.message.fallbackUrl,
]);
// 使Positioned.fill确保飘屏覆盖全屏宽度100 // 使Positioned.fill确保飘屏覆盖全屏宽度100
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
debouncer.debounce( debouncer.debounce(
duration: Duration(milliseconds: 350), duration: Duration(milliseconds: 350),
onDebounce: () { onDebounce: () {
if (widget.message.roomId != null && final roomId = widget.message.roomId?.trim() ?? '';
widget.message.roomId!.isNotEmpty) { final currentContext = navigatorKey.currentState?.context;
SCRoomUtils.goRoom( if (roomId.isEmpty ||
widget.message.roomId!, currentContext == null ||
navigatorKey.currentState!.context, !currentContext.mounted ||
fromFloting: true, SCRoomUtils.isCurrentRoom(roomId, currentContext)) {
); return;
}
if (roomId.isNotEmpty) {
SCRoomUtils.goRoom(roomId, currentContext, fromFloting: true);
} }
}, },
); );
@ -177,91 +185,78 @@ class _FloatingRoomRocketScreenWidgetState
child: Directionality( child: Directionality(
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
child: Container( child: Container(
alignment: Alignment.topCenter, width: 307.w,
height: 128.w, height: 68.w,
width: 290.w, decoration: const BoxDecoration(
margin: EdgeInsets.only(top: 10.w),
padding: EdgeInsetsDirectional.only(start: 3.w, end: 25.w),
decoration: BoxDecoration(
image: DecorationImage( image: DecorationImage(
image: AssetImage( image: AssetImage(_RocketFloatingAssets.bannerBg),
"sc_images/index/sc_icon_rocket_broad_bg_lv${widget.message.rocketLevel}.webp",
),
fit: BoxFit.fill, fit: BoxFit.fill,
), ),
), ),
child: Row( child: Stack(
mainAxisSize: MainAxisSize.min, // clipBehavior: Clip.none,
children: [ children: [
SizedBox(width: 5.w), Positioned(
Container( left: 21.w,
top: 8.w,
width: 38.w,
height: 38.w,
child: head( child: head(
url: widget.message.userAvatarUrl ?? "", url: widget.message.userAvatarUrl ?? '',
width: 66.w, width: 38.w,
), ),
), ),
SizedBox(width: 4.w), Positioned(
Expanded( left: 65.w,
child: Row( top: 9.w,
mainAxisAlignment: MainAxisAlignment.start, width: 126.w,
children: [ child: Text(
Column( roomName,
mainAxisSize: MainAxisSize.min, maxLines: 1,
crossAxisAlignment: CrossAxisAlignment.center, overflow: TextOverflow.ellipsis,
mainAxisAlignment: MainAxisAlignment.center, textScaler: TextScaler.noScaling,
children: [ style: TextStyle(
SizedBox(height: 6.w), color: Colors.white,
Row( fontSize: 16.sp,
children: [ height: 1,
SizedBox( fontWeight: FontWeight.w400,
width: 60.w, decoration: TextDecoration.none,
height: 22.w, ),
child: Marquee(
text: "ID:${widget.message.userId} ",
style: TextStyle(
fontSize: 15.sp,
color: Colors.yellow,
fontStyle: FontStyle.italic,
fontWeight: FontWeight.w900,
decoration: TextDecoration.none,
),
scrollAxis: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.start,
blankSpace: 20.0,
velocity: 40.0,
pauseAfterRound: Duration(seconds: 1),
accelerationDuration: Duration(seconds: 1),
accelerationCurve: Curves.easeOut,
decelerationDuration: Duration(
milliseconds: 500,
),
decelerationCurve: Curves.easeOut,
),
),
text(
SCAppLocalizations.of(context)!.launchedARocket,
textColor: Colors.white,
fontWeight: FontWeight.bold,
fontStyle: FontStyle.italic,
fontSize: 13.sp,
),
],
),
Transform.translate(
offset: Offset(0, -5),
child: text(
SCAppLocalizations.of(context)!.letGoToWatch,
textColor: Colors.white,
fontWeight: FontWeight.w900,
fontSize: 17.sp,
fontStyle: FontStyle.italic,
),
),
],
),
],
), ),
), ),
Positioned(
left: 65.w,
top: 31.w,
width: 126.w,
child: Text(
SCAppLocalizations.of(context)?.launchRocket ??
'Launch Rocket',
maxLines: 1,
overflow: TextOverflow.ellipsis,
textScaler: TextScaler.noScaling,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.61),
fontSize: 12.sp,
height: 1,
fontWeight: FontWeight.w400,
decoration: TextDecoration.none,
),
),
),
Positioned(
left: 201.w,
top: 8.w,
width: 40.w,
height: 40.w,
child: _RocketIcon(url: rocketIconUrl),
),
Positioned(
left: 249.w,
top: 14.w,
width: 42.w,
height: 28.w,
child: const _RocketGoButton(),
),
], ],
), ),
), ),
@ -285,4 +280,119 @@ class _FloatingRoomRocketScreenWidgetState
_swipeController.reset(); _swipeController.reset();
_swipeController.forward(); _swipeController.forward();
} }
String _firstNonBlank(Iterable<String?> values) {
for (final value in values) {
final text = value?.trim();
if (text != null && text.isNotEmpty) {
return text;
}
}
return '';
}
}
class _RocketIcon extends StatelessWidget {
const _RocketIcon({required this.url});
final String url;
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(4.w),
color: const Color(0xFF08261F),
border: Border.all(
color: const Color(0xFF54F8D2).withValues(alpha: 0.5),
width: 0.7.w,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF14FFC7).withValues(alpha: 0.16),
blurRadius: 7.w,
),
],
),
child: Padding(
padding: EdgeInsets.all(5.w),
child:
url.trim().isEmpty
? Image.asset(
_RocketFloatingAssets.fallbackRocket,
fit: BoxFit.contain,
)
: netImage(
url: url,
width: 30.w,
height: 30.w,
fit: BoxFit.contain,
noDefaultImg: true,
errorWidget: Image.asset(
_RocketFloatingAssets.fallbackRocket,
fit: BoxFit.contain,
),
),
),
);
}
}
class _RocketGoButton extends StatelessWidget {
const _RocketGoButton();
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(999.w),
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF0C745B), Color(0xFF013C31)],
),
border: Border.all(
color: const Color(0xFF8DFFE3).withValues(alpha: 0.72),
width: 1.w,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF21FFC3).withValues(alpha: 0.28),
blurRadius: 6.w,
spreadRadius: 0.5.w,
),
],
),
child: Center(
child: ShaderMask(
shaderCallback:
(bounds) => const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.white, Color(0xFFA4FFE3)],
).createShader(bounds),
blendMode: BlendMode.srcIn,
child: Text(
'GO',
textScaler: TextScaler.noScaling,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
height: 1,
fontWeight: FontWeight.w500,
decoration: TextDecoration.none,
),
),
),
),
);
}
}
class _RocketFloatingAssets {
const _RocketFloatingAssets._();
static const String bannerBg =
'sc_images/room/rocket/rocket_floating_banner_bg.png';
static const String fallbackRocket = 'sc_images/room/rocket/rocket_body.png';
} }

View File

@ -24,7 +24,17 @@ class RoomRocketApiMapper {
.map( .map(
(item) => RoomRocketLevelItem( (item) => RoomRocketLevelItem(
label: 'Lv.${item.level}', label: 'Lv.${item.level}',
level: item.level,
imageUrl: item.rocketIconUrl.isEmpty ? null : item.rocketIconUrl, imageUrl: item.rocketIconUrl.isEmpty ? null : item.rocketIconUrl,
previewImageUrl:
item.rocketPreviewUrl.isEmpty ? null : item.rocketPreviewUrl,
animationUrl:
item.rocketAnimationUrl.isEmpty
? null
: item.rocketAnimationUrl,
progressImageUrl:
item.progressBarUrl.isEmpty ? null : item.progressBarUrl,
shakeThresholdPercent: item.shakeThresholdPercent,
enabled: item.enabled, enabled: item.enabled,
), ),
) )
@ -41,12 +51,28 @@ class RoomRocketApiMapper {
return rewards.map(rewardItemFromPreview).toList(growable: false); return rewards.map(rewardItemFromPreview).toList(growable: false);
} }
static RoomRocketRewardGroups? rewardGroupsFromStatus(
SCRoomRocketStatusRes? status,
) {
final preview = status?.rewardPreview;
if (preview == null) {
return null;
}
return RoomRocketRewardGroups(
top1: preview.top1.map(rewardItemFromPreview).toList(growable: false),
setOff: preview.ignite.map(rewardItemFromPreview).toList(growable: false),
inRoom: preview.inRoom.map(rewardItemFromPreview).toList(growable: false),
);
}
static RoomRocketRewardItem rewardItemFromPreview( static RoomRocketRewardItem rewardItemFromPreview(
SCRoomRocketRewardRes reward, SCRoomRocketRewardRes reward,
) { ) {
return RoomRocketRewardItem( return RoomRocketRewardItem(
imageUrl: reward.rewardCover.isEmpty ? null : reward.rewardCover, imageUrl: reward.rewardCover.isEmpty ? null : reward.rewardCover,
amount: reward.displayAmount, amount: reward.displayAmount,
rewardType: reward.rewardType,
expireDays: reward.expireDays,
); );
} }
@ -85,7 +111,8 @@ class RoomRocketApiMapper {
.map((entry) { .map((entry) {
final first = entry.value.first; final first = entry.value.first;
return RoomRocketRewardWinner( return RoomRocketRewardWinner(
nickname: first.userId.isNotEmpty ? 'ID:${first.userId}' : 'You', nickname: first.displayName,
avatarUrl: first.userAvatar.isEmpty ? null : first.userAvatar,
rewards: rewardItemsFromRecords(entry.value), rewards: rewardItemsFromRecords(entry.value),
); );
}) })
@ -133,6 +160,8 @@ class RoomRocketApiMapper {
return RoomRocketRewardItem( return RoomRocketRewardItem(
imageUrl: record.rewardCover.isEmpty ? null : record.rewardCover, imageUrl: record.rewardCover.isEmpty ? null : record.rewardCover,
amount: record.displayAmount, amount: record.displayAmount,
rewardType: record.rewardType,
expireDays: record.expireDays,
); );
} }

View File

@ -1,8 +1,14 @@
import 'dart:async';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart';
import 'package:yumi/ui_kit/components/custom_cached_image.dart'; import 'package:yumi/ui_kit/components/custom_cached_image.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart';
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
class RoomRocketDialog extends StatelessWidget { class RoomRocketDialog extends StatelessWidget {
const RoomRocketDialog({ const RoomRocketDialog({
@ -14,6 +20,9 @@ class RoomRocketDialog extends StatelessWidget {
this.selectedLevel = 0, this.selectedLevel = 0,
this.onLevelSelected, this.onLevelSelected,
this.rewards, this.rewards,
this.rewardGroups,
this.selectedRewardTab = RoomRocketRewardTab.top1,
this.onRewardTabSelected,
this.crew, this.crew,
this.onClose, this.onClose,
this.onOpenRank, this.onOpenRank,
@ -29,6 +38,9 @@ class RoomRocketDialog extends StatelessWidget {
final int selectedLevel; final int selectedLevel;
final ValueChanged<int>? onLevelSelected; final ValueChanged<int>? onLevelSelected;
final List<RoomRocketRewardItem>? rewards; final List<RoomRocketRewardItem>? rewards;
final RoomRocketRewardGroups? rewardGroups;
final RoomRocketRewardTab selectedRewardTab;
final ValueChanged<RoomRocketRewardTab>? onRewardTabSelected;
final List<RoomRocketCrewMember>? crew; final List<RoomRocketCrewMember>? crew;
final VoidCallback? onClose; final VoidCallback? onClose;
final VoidCallback? onOpenRank; final VoidCallback? onOpenRank;
@ -39,9 +51,15 @@ class RoomRocketDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final resolvedLevels = _normalizeLevels(levels); final resolvedLevels = _normalizeLevels(levels);
final resolvedRewards = _normalizeRewards(rewards); final resolvedRewards = _normalizeRewards(
rewardGroups?.rewardsFor(selectedRewardTab) ?? rewards,
useFallback: rewardGroups == null,
);
final resolvedCrew = _normalizeCrew(crew, status); final resolvedCrew = _normalizeCrew(crew, status);
final percent = _normalizePercent(status?.energyPercent); final resolvedSelectedLevel =
selectedLevel.clamp(0, math.max(0, resolvedLevels.length - 1)).toInt();
final selectedLevelItem = resolvedLevels[resolvedSelectedLevel];
final percent = _percentForSelectedLevel(status, selectedLevelItem.level);
final resolvedProgressStage = final resolvedProgressStage =
progressStage ?? RoomRocketProgressStage.fromPercent(percent); progressStage ?? RoomRocketProgressStage.fromPercent(percent);
final resolvedBottomStage = final resolvedBottomStage =
@ -81,11 +99,15 @@ class RoomRocketDialog extends StatelessWidget {
percent: percent, percent: percent,
progressStage: resolvedProgressStage, progressStage: resolvedProgressStage,
bottomStage: resolvedBottomStage, bottomStage: resolvedBottomStage,
status: status,
round: round, round: round,
levels: resolvedLevels, levels: resolvedLevels,
selectedLevel: selectedLevel, selectedLevel: resolvedSelectedLevel,
selectedLevelItem: selectedLevelItem,
onLevelSelected: onLevelSelected, onLevelSelected: onLevelSelected,
rewards: resolvedRewards, rewards: resolvedRewards,
selectedRewardTab: selectedRewardTab,
onRewardTabSelected: onRewardTabSelected,
crew: resolvedCrew, crew: resolvedCrew,
onClose: onClose, onClose: onClose,
onOpenRank: onOpenRank, onOpenRank: onOpenRank,
@ -117,20 +139,25 @@ class RoomRocketDialog extends StatelessWidget {
} }
return List<RoomRocketLevelItem>.generate( return List<RoomRocketLevelItem>.generate(
6, 6,
(index) => RoomRocketLevelItem(label: 'Lv.${index + 1}'), (index) =>
RoomRocketLevelItem(label: 'Lv.${index + 1}', level: index + 1),
growable: false, growable: false,
); );
} }
static List<RoomRocketRewardItem> _normalizeRewards( static List<RoomRocketRewardItem> _normalizeRewards(
List<RoomRocketRewardItem>? rewards, List<RoomRocketRewardItem>? rewards, {
) { required bool useFallback,
}) {
final source = final source =
rewards?.where((item) => item.amount.trim().isNotEmpty).toList() ?? rewards?.where((item) => item.amount.trim().isNotEmpty).toList() ??
const <RoomRocketRewardItem>[]; const <RoomRocketRewardItem>[];
if (source.isNotEmpty) { if (source.isNotEmpty) {
return source; return source;
} }
if (!useFallback) {
return const <RoomRocketRewardItem>[];
}
return const [ return const [
RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'), RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'),
RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'), RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'),
@ -165,27 +192,101 @@ class RoomRocketDialog extends StatelessWidget {
final percent = raw > 0 && raw <= 1 ? raw * 100 : raw; final percent = raw > 0 && raw <= 1 ? raw * 100 : raw;
return percent.clamp(0, 100).toDouble(); return percent.clamp(0, 100).toDouble();
} }
static double _percentForSelectedLevel(
SCRoomRocketStatusRes? status,
int selectedLevel,
) {
final currentLevel =
(status?.currentLevel ?? status?.level?.toInt() ?? selectedLevel)
.clamp(1, 99)
.toInt();
if (selectedLevel < currentLevel) {
return 100;
}
if (selectedLevel > currentLevel) {
return 0;
}
return _normalizePercent(status?.displayPercent ?? status?.energyPercent);
}
} }
class RoomRocketRewardItem { class RoomRocketRewardItem {
const RoomRocketRewardItem({this.asset, this.imageUrl, required this.amount}); const RoomRocketRewardItem({
this.asset,
this.imageUrl,
required this.amount,
this.rewardType = 'GOLD',
this.expireDays = 0,
});
final String? asset; final String? asset;
final String? imageUrl; final String? imageUrl;
final String amount; final String amount;
final String rewardType;
final int expireDays;
bool get isGoldReward {
final normalizedType = rewardType.trim().toUpperCase();
return normalizedType == 'GOLD' ||
normalizedType == 'COIN' ||
normalizedType == 'COINS';
}
String get displayText {
if (isGoldReward) {
return amount;
}
return expireDays > 0 ? '${expireDays}D' : amount;
}
}
enum RoomRocketRewardTab { top1, setOff, inRoom }
class RoomRocketRewardGroups {
const RoomRocketRewardGroups({
this.top1 = const <RoomRocketRewardItem>[],
this.setOff = const <RoomRocketRewardItem>[],
this.inRoom = const <RoomRocketRewardItem>[],
});
final List<RoomRocketRewardItem> top1;
final List<RoomRocketRewardItem> setOff;
final List<RoomRocketRewardItem> inRoom;
List<RoomRocketRewardItem> rewardsFor(RoomRocketRewardTab tab) {
switch (tab) {
case RoomRocketRewardTab.top1:
return top1;
case RoomRocketRewardTab.setOff:
return setOff;
case RoomRocketRewardTab.inRoom:
return inRoom;
}
}
} }
class RoomRocketLevelItem { class RoomRocketLevelItem {
const RoomRocketLevelItem({ const RoomRocketLevelItem({
required this.label, required this.label,
this.level = 1,
this.asset, this.asset,
this.imageUrl, this.imageUrl,
this.previewImageUrl,
this.animationUrl,
this.progressImageUrl,
this.shakeThresholdPercent = 95,
this.enabled = true, this.enabled = true,
}); });
final String label; final String label;
final int level;
final String? asset; final String? asset;
final String? imageUrl; final String? imageUrl;
final String? previewImageUrl;
final String? animationUrl;
final String? progressImageUrl;
final num shakeThresholdPercent;
final bool enabled; final bool enabled;
} }
@ -220,11 +321,15 @@ class _RocketCanvas extends StatelessWidget {
required this.percent, required this.percent,
required this.progressStage, required this.progressStage,
required this.bottomStage, required this.bottomStage,
required this.status,
required this.round, required this.round,
required this.levels, required this.levels,
required this.selectedLevel, required this.selectedLevel,
required this.selectedLevelItem,
this.onLevelSelected, this.onLevelSelected,
required this.rewards, required this.rewards,
required this.selectedRewardTab,
this.onRewardTabSelected,
required this.crew, required this.crew,
this.onClose, this.onClose,
this.onOpenRank, this.onOpenRank,
@ -237,11 +342,15 @@ class _RocketCanvas extends StatelessWidget {
final double percent; final double percent;
final RoomRocketProgressStage progressStage; final RoomRocketProgressStage progressStage;
final RoomRocketBottomStage bottomStage; final RoomRocketBottomStage bottomStage;
final SCRoomRocketStatusRes? status;
final int round; final int round;
final List<RoomRocketLevelItem> levels; final List<RoomRocketLevelItem> levels;
final int selectedLevel; final int selectedLevel;
final RoomRocketLevelItem selectedLevelItem;
final ValueChanged<int>? onLevelSelected; final ValueChanged<int>? onLevelSelected;
final List<RoomRocketRewardItem> rewards; final List<RoomRocketRewardItem> rewards;
final RoomRocketRewardTab selectedRewardTab;
final ValueChanged<RoomRocketRewardTab>? onRewardTabSelected;
final List<RoomRocketCrewMember> crew; final List<RoomRocketCrewMember> crew;
final VoidCallback? onClose; final VoidCallback? onClose;
final VoidCallback? onOpenRank; final VoidCallback? onOpenRank;
@ -257,7 +366,7 @@ class _RocketCanvas extends StatelessWidget {
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
children: [ children: [
_image(_RocketAssets.rocketStage, 15, 361, 344, 97), _image(_RocketAssets.rocketStage, 15, 361, 344, 97),
_image(_RocketAssets.rocketBody, 88, 111, 200, 432), _buildRocketBody(),
_image(_RocketAssets.sidePanel, 309, 202, 66, 207), _image(_RocketAssets.sidePanel, 309, 202, 66, 207),
_image(_RocketAssets.sidePanel, 0, 202, 66, 207), _image(_RocketAssets.sidePanel, 0, 202, 66, 207),
_buildLevelRail(), _buildLevelRail(),
@ -271,6 +380,24 @@ class _RocketCanvas extends StatelessWidget {
); );
} }
Widget _buildRocketBody() {
return Positioned(
left: s(88),
top: s(111),
width: s(200),
height: s(432),
child: _RocketImage(
asset: selectedLevelItem.asset ?? _RocketAssets.rocketBody,
imageUrl: selectedLevelItem.previewImageUrl,
fit: BoxFit.contain,
preferSvg: true,
debugName:
'centerPreview level=${selectedLevelItem.level} '
'preview=${selectedLevelItem.previewImageUrl ?? '-'}',
),
);
}
Widget _image( Widget _image(
String asset, String asset,
double left, double left,
@ -347,9 +474,20 @@ class _RocketCanvas extends StatelessWidget {
RoomRocketProgressStage.charging => _RocketAssets.progressPartial, RoomRocketProgressStage.charging => _RocketAssets.progressPartial,
RoomRocketProgressStage.full => _RocketAssets.progressFull, RoomRocketProgressStage.full => _RocketAssets.progressFull,
}; };
final progressImageUrl = selectedLevelItem.progressImageUrl?.trim() ?? '';
return Stack( return Stack(
children: [ children: [
_image(asset, 320, 227, 44, 134), Positioned(
left: s(320),
top: s(227),
width: s(44),
height: s(134),
child: _RocketImage(
asset: asset,
imageUrl: progressImageUrl.isEmpty ? null : progressImageUrl,
fit: BoxFit.fill,
),
),
Positioned( Positioned(
left: s(316), left: s(316),
top: s(373), top: s(373),
@ -438,30 +576,12 @@ class _RocketCanvas extends StatelessWidget {
color: Colors.white, color: Colors.white,
), ),
), ),
for (final item in const [ Positioned(
_TimerToken('23', 193), left: s(187),
_TimerToken('23', 223), top: s(468),
_TimerToken('23', 253), width: s(88),
]) child: _ResetCountdownText(status: status, scale: scale),
Positioned( ),
left: s(item.left),
top: s(468),
child: _RocketText(
item.value,
fontSize: s(12),
color: Color(0xFF2DFFC1),
),
),
for (final left in const [209.0, 239.0])
Positioned(
left: s(left),
top: s(468),
child: _RocketText(
':',
fontSize: s(12),
color: const Color(0xFF2DFFC1),
),
),
], ],
); );
} }
@ -500,10 +620,25 @@ class _RocketCanvas extends StatelessWidget {
left: 31, left: 31,
top: 552, top: 552,
label: 'Top1', label: 'Top1',
active: true, active: selectedRewardTab == RoomRocketRewardTab.top1,
onTap: () => onRewardTabSelected?.call(RoomRocketRewardTab.top1),
),
_RewardTab(
scale: scale,
left: 147,
top: 552,
label: 'Set off',
active: selectedRewardTab == RoomRocketRewardTab.setOff,
onTap: () => onRewardTabSelected?.call(RoomRocketRewardTab.setOff),
),
_RewardTab(
scale: scale,
left: 252,
top: 552,
label: 'In the room',
active: selectedRewardTab == RoomRocketRewardTab.inRoom,
onTap: () => onRewardTabSelected?.call(RoomRocketRewardTab.inRoom),
), ),
_RewardTab(scale: scale, left: 147, top: 552, label: 'Set off'),
_RewardTab(scale: scale, left: 252, top: 552, label: 'In the room'),
Positioned( Positioned(
left: s(28), left: s(28),
top: s(578), top: s(578),
@ -514,7 +649,9 @@ class _RocketCanvas extends StatelessWidget {
behavior: const _NoGlowScrollBehavior(), behavior: const _NoGlowScrollBehavior(),
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
child: Row( child: Row(
children: [ children: [
for (var index = 0; index < rewards.length; index++) ...[ for (var index = 0; index < rewards.length; index++) ...[
@ -524,7 +661,11 @@ class _RocketCanvas extends StatelessWidget {
child: _RewardCard( child: _RewardCard(
scale: scale, scale: scale,
item: rewards[index], item: rewards[index],
onTap: () => onRewardSelected?.call(rewards[index]), onTap:
onRewardSelected == null
? null
: () =>
onRewardSelected?.call(rewards[index]),
), ),
), ),
if (index != rewards.length - 1) SizedBox(width: s(12)), if (index != rewards.length - 1) SizedBox(width: s(12)),
@ -564,7 +705,7 @@ class _RocketCanvas extends StatelessWidget {
if (!activeBottom) if (!activeBottom)
Positioned( Positioned(
left: s(108), left: s(108),
top: s(750), top: s(760),
width: s(169), width: s(169),
height: s(36), height: s(36),
child: FittedBox( child: FittedBox(
@ -651,28 +792,24 @@ class _RewardCard extends StatelessWidget {
top: 24 * scale, top: 24 * scale,
width: 50 * scale, width: 50 * scale,
height: 50 * scale, height: 50 * scale,
child: _RocketImage( child:
asset: item.asset, item.isGoldReward
imageUrl: item.imageUrl, ? Image.asset(_RocketAssets.coin, fit: BoxFit.contain)
fit: BoxFit.cover, : _RocketImage(
), asset: item.asset,
imageUrl: item.imageUrl,
fit: BoxFit.contain,
),
), ),
Positioned( Positioned(
left: 21 * scale, left: 16 * scale,
top: 106 * scale,
width: 14 * scale,
height: 14 * scale,
child: Image.asset(_RocketAssets.coin, fit: BoxFit.contain),
),
Positioned(
left: 37 * scale,
top: 105 * scale, top: 105 * scale,
width: 46 * scale, width: 65 * scale,
child: _RocketText( child: _RocketText(
item.amount, item.displayText,
fontSize: 14 * scale, fontSize: 14 * scale,
color: Colors.white, color: Colors.white,
textAlign: TextAlign.left, textAlign: TextAlign.center,
), ),
), ),
], ],
@ -792,18 +929,139 @@ class _CrewAvatar extends StatelessWidget {
} }
class _RocketImage extends StatelessWidget { class _RocketImage extends StatelessWidget {
const _RocketImage({this.asset, this.imageUrl, this.fit = BoxFit.cover}); const _RocketImage({
this.asset,
this.imageUrl,
this.fit = BoxFit.cover,
this.preferSvg = false,
this.debugName,
});
final String? asset; final String? asset;
final String? imageUrl; final String? imageUrl;
final BoxFit fit; final BoxFit fit;
final bool preferSvg;
final String? debugName;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (imageUrl != null && imageUrl!.trim().isNotEmpty) { if (imageUrl != null && imageUrl!.trim().isNotEmpty) {
return CustomCachedImage(imageUrl: imageUrl!, fit: fit); final source = imageUrl!.trim();
_debugImage('load ${_sourceKind(source)} ${_shortSource(source)}');
if (SCNetworkSvgaWidget.isSvga(source)) {
return SCNetworkSvgaWidget(
resource: source,
loop: true,
fit: fit,
fallback: _imageFallback(),
);
}
if (RoomRocketPagEffectOverlay.isPag(source)) {
return RoomRocketPagPreview(
resource: source,
loop: true,
defaultBuilder: (_) => const SCRotatingDotsLoading(),
);
}
if (_isInlineSvg(source)) {
return SvgPicture.string(
source,
fit: fit,
errorBuilder: (_, error, __) {
_debugImage('inline svg failed: $error');
return _imageFallback();
},
);
}
if (_isSvg(source) || preferSvg) {
return SvgPicture.network(
source,
fit: fit,
placeholderBuilder: (_) => const SCRotatingDotsLoading(),
errorBuilder: (_, error, __) {
_debugImage('network svg failed: $error');
return preferSvg && !_isSvg(source)
? CustomCachedImage(imageUrl: source, fit: fit)
: _imageFallback();
},
);
}
return CustomCachedImage(imageUrl: source, fit: fit);
} }
if (asset != null && asset!.trim().isNotEmpty) { if (asset != null && asset!.trim().isNotEmpty) {
final source = asset!.trim();
_debugImage('load asset ${_shortSource(source)}');
if (SCNetworkSvgaWidget.isSvga(source)) {
return SCNetworkSvgaWidget(
resource: source,
loop: true,
fit: fit,
fallback: _imageFallback(),
);
}
if (RoomRocketPagEffectOverlay.isPag(source)) {
return RoomRocketPagPreview(
resource: source,
loop: true,
defaultBuilder: (_) => _imageFallback(),
);
}
if (_isSvg(source)) {
return SvgPicture.asset(
source,
fit: fit,
errorBuilder: (_, error, __) {
_debugImage('asset svg failed: $error');
return const SizedBox.shrink();
},
);
}
return Image.asset(source, fit: fit, gaplessPlayback: true);
}
return const SizedBox.shrink();
}
static bool _isSvg(String value) {
final lower = value.trim().toLowerCase();
return lower.endsWith('.svg') || lower.contains('.svg?');
}
static bool _isInlineSvg(String value) {
final trimmed = value.trimLeft();
return trimmed.startsWith('<svg') ||
(trimmed.startsWith('<?xml') && trimmed.contains('<svg'));
}
static String _sourceKind(String source) {
if (_isInlineSvg(source)) {
return 'inline-svg';
}
if (SCNetworkSvgaWidget.isSvga(source)) {
return 'svga';
}
if (_isSvg(source)) {
return 'svg-url';
}
return 'url';
}
static String _shortSource(String value) {
final text = value.replaceAll(RegExp(r'\s+'), ' ').trim();
if (text.length <= 140) {
return text;
}
return '${text.substring(0, 137)}...';
}
void _debugImage(String message) {
if (!kDebugMode || debugName == null) {
return;
}
debugPrint('[RoomRocketImage] $debugName $message');
}
Widget _imageFallback() {
if (asset != null && asset!.trim().isNotEmpty && !_isSvg(asset!)) {
return Image.asset(asset!, fit: fit, gaplessPlayback: true); return Image.asset(asset!, fit: fit, gaplessPlayback: true);
} }
return const SizedBox.shrink(); return const SizedBox.shrink();
@ -817,6 +1075,7 @@ class _RewardTab extends StatelessWidget {
required this.top, required this.top,
required this.label, required this.label,
this.active = false, this.active = false,
this.onTap,
}); });
final double scale; final double scale;
@ -824,6 +1083,7 @@ class _RewardTab extends StatelessWidget {
final double top; final double top;
final String label; final String label;
final bool active; final bool active;
final VoidCallback? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -842,32 +1102,36 @@ class _RewardTab extends StatelessWidget {
top: top * scale, top: top * scale,
width: width * scale, width: width * scale,
height: 14 * scale, height: 14 * scale,
child: Row( child: GestureDetector(
mainAxisAlignment: MainAxisAlignment.center, behavior: HitTestBehavior.opaque,
children: [ onTap: onTap,
_TabSlash(color: color, scale: scale), child: Row(
SizedBox(width: 3 * scale), mainAxisAlignment: MainAxisAlignment.center,
Flexible( children: [
child: Text( _TabSlash(color: color, scale: scale),
label, SizedBox(width: 3 * scale),
textScaler: TextScaler.noScaling, Flexible(
maxLines: 1, child: Text(
overflow: TextOverflow.visible, label,
textAlign: TextAlign.center, textScaler: TextScaler.noScaling,
style: TextStyle( maxLines: 1,
color: color, overflow: TextOverflow.visible,
fontSize: 12 * scale, textAlign: TextAlign.center,
height: 1, style: TextStyle(
decoration: TextDecoration.none, color: color,
fontSize: 12 * scale,
height: 1,
decoration: TextDecoration.none,
),
), ),
), ),
), SizedBox(width: 3 * scale),
SizedBox(width: 3 * scale), Transform.rotate(
Transform.rotate( angle: math.pi,
angle: math.pi, child: _TabSlash(color: color, scale: scale),
child: _TabSlash(color: color, scale: scale), ),
), ],
], ),
), ),
); );
} }
@ -1110,6 +1374,244 @@ class _RocketText extends StatelessWidget {
} }
} }
class _ResetCountdownText extends StatefulWidget {
const _ResetCountdownText({required this.status, required this.scale});
final SCRoomRocketStatusRes? status;
final double scale;
@override
State<_ResetCountdownText> createState() => _ResetCountdownTextState();
}
class _ResetCountdownTextState extends State<_ResetCountdownText> {
Timer? _timer;
late DateTime _anchor;
late String _sourceKey;
int? _initialRemainingSeconds;
DateTime? _targetTime;
DateTime? _serverTimeAtAnchor;
@override
void initState() {
super.initState();
_resetSource();
_timer = Timer.periodic(
const Duration(seconds: 1),
(_) => mounted ? setState(() {}) : null,
);
}
@override
void didUpdateWidget(covariant _ResetCountdownText oldWidget) {
super.didUpdateWidget(oldWidget);
final nextKey = _countdownSourceKey(widget.status);
if (nextKey != _sourceKey) {
_resetSource();
}
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
void _resetSource() {
_anchor = DateTime.now();
_sourceKey = _countdownSourceKey(widget.status);
_initialRemainingSeconds = widget.status?.resetRemainingSeconds;
_targetTime = _targetTimeFromStatus(widget.status);
_serverTimeAtAnchor = _dateTimeFromValue(
widget.status?.serverTime,
_timezoneOffset(widget.status?.timezone),
);
}
@override
Widget build(BuildContext context) {
final parts = _formatDuration(_remainingSeconds());
return Row(
mainAxisSize: MainAxisSize.min,
children: [
_countdownPart(parts[0]),
_separator(),
_countdownPart(parts[1]),
_separator(),
_countdownPart(parts[2]),
],
);
}
Widget _countdownPart(String text) {
return SizedBox(
width: 23 * widget.scale,
child: _RocketText(
text,
fontSize: 12 * widget.scale,
color: const Color(0xFF2DFFC1),
),
);
}
Widget _separator() {
return SizedBox(
width: 7 * widget.scale,
child: _RocketText(
':',
fontSize: 12 * widget.scale,
color: const Color(0xFF2DFFC1),
),
);
}
int _remainingSeconds() {
final elapsed = DateTime.now().difference(_anchor).inSeconds;
final directRemaining = _initialRemainingSeconds;
if (directRemaining != null) {
return math.max(0, directRemaining - elapsed);
}
final target = _targetTime ?? _fallbackResetTarget(widget.status);
final serverTime = _serverTimeAtAnchor;
final now =
serverTime == null
? DateTime.now()
: serverTime.add(Duration(seconds: elapsed));
return math.max(0, target.difference(now).inSeconds);
}
List<String> _formatDuration(int totalSeconds) {
final hours = totalSeconds ~/ 3600;
final minutes = (totalSeconds % 3600) ~/ 60;
final seconds = totalSeconds % 60;
String two(int value) => value.toString().padLeft(2, '0');
return [two(hours), two(minutes), two(seconds)];
}
static String _countdownSourceKey(SCRoomRocketStatusRes? status) {
return [
status?.date,
status?.timezone,
status?.resetRemainingSeconds,
status?.resetTime,
status?.serverTime,
].join('|');
}
static DateTime? _targetTimeFromStatus(SCRoomRocketStatusRes? status) {
return _dateTimeFromValue(
status?.resetTime,
_timezoneOffset(status?.timezone),
);
}
static DateTime _fallbackResetTarget(SCRoomRocketStatusRes? status) {
final offset = _timezoneOffset(status?.timezone);
final dayKey = status?.date?.trim() ?? '';
if (RegExp(r'^\d{8}$').hasMatch(dayKey)) {
final year = int.parse(dayKey.substring(0, 4));
final month = int.parse(dayKey.substring(4, 6));
final day = int.parse(dayKey.substring(6, 8));
return DateTime.utc(year, month, day + 1).subtract(offset).toLocal();
}
final businessNow = DateTime.now().toUtc().add(offset);
return DateTime.utc(
businessNow.year,
businessNow.month,
businessNow.day + 1,
).subtract(offset).toLocal();
}
static DateTime? _dateTimeFromValue(Object? value, Duration offset) {
if (value == null) {
return null;
}
if (value is num) {
return _dateTimeFromTimestamp(value);
}
final text = value.toString().trim();
if (text.isEmpty) {
return null;
}
final numeric = num.tryParse(text);
if (numeric != null) {
return _dateTimeFromTimestamp(numeric);
}
return _dateTimeFromText(text, offset);
}
static DateTime _dateTimeFromTimestamp(num value) {
final milliseconds =
value.abs() < 1000000000000 ? (value * 1000).round() : value.round();
return DateTime.fromMillisecondsSinceEpoch(milliseconds).toLocal();
}
static DateTime? _dateTimeFromText(String text, Duration offset) {
final normalized = text.replaceFirst(' ', 'T');
final hasZone = RegExp(r'(Z|[+-]\d{2}:?\d{2})$').hasMatch(normalized);
if (hasZone) {
return DateTime.tryParse(normalized)?.toLocal();
}
final match = RegExp(
r'^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?',
).firstMatch(text);
if (match == null) {
return DateTime.tryParse(normalized)?.toLocal();
}
final year = int.parse(match.group(1)!);
final month = int.parse(match.group(2)!);
final day = int.parse(match.group(3)!);
final hour = int.parse(match.group(4)!);
final minute = int.parse(match.group(5)!);
final second = int.tryParse(match.group(6) ?? '0') ?? 0;
return DateTime.utc(
year,
month,
day,
hour,
minute,
second,
).subtract(offset).toLocal();
}
static Duration _timezoneOffset(String? timezone) {
final value = timezone?.trim();
if (value == null || value.isEmpty) {
return const Duration(hours: 3);
}
final normalized = value.toUpperCase();
final match = RegExp(
r'^(?:UTC|GMT)?([+-])(\d{1,2})(?::?(\d{2}))?$',
).firstMatch(normalized);
if (match != null) {
final sign = match.group(1) == '-' ? -1 : 1;
final hours = int.parse(match.group(2)!);
final minutes = int.tryParse(match.group(3) ?? '0') ?? 0;
return Duration(hours: sign * hours, minutes: sign * minutes);
}
switch (value) {
case 'Asia/Riyadh':
case 'Asia/Kuwait':
case 'Asia/Qatar':
case 'Asia/Bahrain':
case 'Asia/Baghdad':
case 'Europe/Moscow':
return const Duration(hours: 3);
case 'Asia/Dubai':
case 'Asia/Muscat':
return const Duration(hours: 4);
case 'Asia/Shanghai':
case 'Asia/Hong_Kong':
case 'Asia/Singapore':
return const Duration(hours: 8);
default:
return const Duration(hours: 3);
}
}
}
class _GradientText extends StatelessWidget { class _GradientText extends StatelessWidget {
const _GradientText( const _GradientText(
this.text, { this.text, {
@ -1163,13 +1665,6 @@ class _GradientText extends StatelessWidget {
} }
} }
class _TimerToken {
const _TimerToken(this.value, this.left);
final String value;
final double left;
}
class _RocketAssets { class _RocketAssets {
const _RocketAssets._(); const _RocketAssets._();

View File

@ -1,20 +1,25 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:yumi/modules/room/voice_room_route.dart'; import 'package:yumi/modules/room/voice_room_route.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
import 'package:yumi/ui_kit/components/custom_cached_image.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_note_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_record_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_record_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart';
class RoomRocketFloatingEntry extends StatelessWidget { class RoomRocketFloatingEntry extends StatelessWidget {
const RoomRocketFloatingEntry({super.key}); const RoomRocketFloatingEntry({super.key});
@ -29,55 +34,53 @@ class RoomRocketFloatingEntry extends StatelessWidget {
return Selector<RtcProvider, SCRoomRocketStatusRes?>( return Selector<RtcProvider, SCRoomRocketStatusRes?>(
selector: (_, provider) => provider.roomRocketStatus, selector: (_, provider) => provider.roomRocketStatus,
builder: (context, status, child) { builder: (context, status, child) {
final level = _currentEntryLevel(status);
final percent = _entryPercent(status);
final shouldShake =
(status?.shake ?? false) ||
(level != null && percent > level.shakeThresholdPercent);
return SCDebounceWidget( return SCDebounceWidget(
onTap: () => _showRocketDialog(context, status), onTap: () => _showRocketDialog(context, status),
child: SizedBox( child: _RoomRocketEntryButton(
width: 44.w, iconUrl: level?.rocketIconUrl ?? '',
height: 44.w, progressUrl: level?.progressBarUrl ?? '',
child: DecoratedBox( shake: shouldShake,
decoration: BoxDecoration( loading: status == null,
shape: BoxShape.circle,
gradient: const LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Color(0xFF093D33), Color(0xFF061A16)],
),
border: Border.all(
color: const Color(0xFF38F4BF).withValues(alpha: 0.78),
width: 1.w,
),
boxShadow: [
BoxShadow(
color: const Color(0xFF21FFC3).withValues(alpha: 0.24),
blurRadius: 10.w,
spreadRadius: 1.w,
),
],
),
child: ClipOval(
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
top: 4.w,
child: Image.asset(
'sc_images/room/rocket/rocket_body.png',
width: 31.w,
height: 38.w,
fit: BoxFit.contain,
gaplessPlayback: true,
),
),
],
),
),
),
), ),
); );
}, },
); );
} }
SCRoomRocketLevelRes? _currentEntryLevel(SCRoomRocketStatusRes? status) {
final levels = status?.levels ?? const <SCRoomRocketLevelRes>[];
if (levels.isEmpty) {
return null;
}
final currentLevel = (status?.currentLevel ?? status?.level?.toInt() ?? 1)
.clamp(1, 99);
for (final item in levels) {
if (item.level == currentLevel) {
return item;
}
}
final sorted = [...levels]..sort((a, b) {
final sortCompare = a.sort.compareTo(b.sort);
return sortCompare != 0 ? sortCompare : a.level.compareTo(b.level);
});
return sorted.first;
}
double _entryPercent(SCRoomRocketStatusRes? status) {
final value = status?.displayPercent ?? status?.energyPercent;
if (value == null) {
return 0;
}
final raw = value.toDouble();
final percent = raw > 0 && raw <= 1 ? raw * 100 : raw;
return percent.clamp(0, 100).toDouble();
}
void _showRocketDialog(BuildContext context, SCRoomRocketStatusRes? status) { void _showRocketDialog(BuildContext context, SCRoomRocketStatusRes? status) {
final rtcProvider = Provider.of<RtcProvider>(context, listen: false); final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
final roomId = final roomId =
@ -104,6 +107,183 @@ class RoomRocketFloatingEntry extends StatelessWidget {
} }
} }
class _RoomRocketEntryButton extends StatefulWidget {
const _RoomRocketEntryButton({
required this.iconUrl,
required this.progressUrl,
required this.shake,
required this.loading,
});
final String iconUrl;
final String progressUrl;
final bool shake;
final bool loading;
@override
State<_RoomRocketEntryButton> createState() => _RoomRocketEntryButtonState();
}
class _RoomRocketEntryButtonState extends State<_RoomRocketEntryButton>
with SingleTickerProviderStateMixin {
late final AnimationController _shakeController;
@override
void initState() {
super.initState();
_shakeController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 480),
);
_syncShake();
}
@override
void didUpdateWidget(covariant _RoomRocketEntryButton oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.shake != widget.shake) {
_syncShake();
}
}
@override
void dispose() {
_shakeController.dispose();
super.dispose();
}
void _syncShake() {
if (widget.shake) {
_shakeController.repeat();
return;
}
_shakeController
..stop()
..value = 0;
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _shakeController,
builder: (context, child) {
final wave = math.sin(_shakeController.value * math.pi * 2);
return Transform.translate(
offset: widget.shake ? Offset(wave * 1.4.w, 0) : Offset.zero,
child: Transform.rotate(
angle: widget.shake ? wave * 0.065 : 0,
child: child,
),
);
},
child: SizedBox(
width: 46.w,
height: 56.w,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: 44.w,
height: 44.w,
child: _RocketEntryImage(
source: widget.iconUrl,
loading: widget.loading,
fit: BoxFit.contain,
),
),
SizedBox(height: 3.w),
SizedBox(
width: 42.w,
height: 8.w,
child:
widget.progressUrl.trim().isEmpty
? const SizedBox.shrink()
: ClipRRect(
borderRadius: BorderRadius.circular(5.w),
child: _RocketEntryImage(
source: widget.progressUrl,
loading: false,
fit: BoxFit.fill,
),
),
),
],
),
),
);
}
}
class _RocketEntryImage extends StatelessWidget {
const _RocketEntryImage({
required this.source,
this.loading = false,
this.fit = BoxFit.contain,
});
final String source;
final bool loading;
final BoxFit fit;
@override
Widget build(BuildContext context) {
final value = source.trim();
if (value.isEmpty) {
return _placeholder();
}
if (RoomRocketPagEffectOverlay.isPag(value)) {
return RoomRocketPagPreview(
resource: value,
loop: true,
defaultBuilder: (_) => _placeholder(),
);
}
if (value.startsWith('sc_images/')) {
return Image.asset(value, fit: fit, gaplessPlayback: true);
}
if (_isInlineSvg(value)) {
return SvgPicture.string(
value,
fit: fit,
errorBuilder: (_, __, ___) => _placeholder(),
);
}
if (_isSvg(value)) {
return SvgPicture.network(
value,
fit: fit,
errorBuilder: (_, __, ___) => _placeholder(),
);
}
return CustomCachedImage(
imageUrl: value,
fit: fit,
placeholder: _placeholder(),
errorWidget: const SizedBox.shrink(),
);
}
static bool _isSvg(String value) {
final lower = value.toLowerCase();
return lower.endsWith('.svg') || lower.contains('.svg?');
}
static bool _isInlineSvg(String value) {
final trimmed = value.trimLeft();
return trimmed.startsWith('<svg') ||
(trimmed.startsWith('<?xml') && trimmed.contains('<svg'));
}
Widget _placeholder() {
if (!loading) {
return const SizedBox.shrink();
}
return const Center(child: SCRotatingDotsLoading());
}
}
const int _rocketPageSize = 10;
class _RoomRocketDialogLoader extends StatefulWidget { class _RoomRocketDialogLoader extends StatefulWidget {
const _RoomRocketDialogLoader({ const _RoomRocketDialogLoader({
required this.roomId, required this.roomId,
@ -125,11 +305,15 @@ class _RoomRocketDialogLoader extends StatefulWidget {
class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
SCRoomRocketStatusRes? _status; SCRoomRocketStatusRes? _status;
List<RoomRocketNoteSection>? _noteSections; List<RoomRocketNoteSection>? _noteSections;
int _selectedLevel = 0;
RoomRocketRewardTab _selectedRewardTab = RoomRocketRewardTab.top1;
bool _levelTouched = false;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_status = widget.initialStatus; _status = widget.initialStatus;
_selectedLevel = _levelIndexFromStatus(widget.initialStatus);
unawaited(_load()); unawaited(_load());
unawaited(_loadRule()); unawaited(_loadRule());
} }
@ -145,7 +329,13 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
if (!mounted) { if (!mounted) {
return; return;
} }
setState(() => _status = res); _debugStatusAssets(res);
setState(() {
_status = res;
if (!_levelTouched) {
_selectedLevel = _levelIndexFromStatus(res);
}
});
try { try {
Provider.of<RtcProvider>( Provider.of<RtcProvider>(
context, context,
@ -175,22 +365,63 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final status = _status; final status = _status;
final selectedLevel = final selectedLevel = _selectedLevel.clamp(0, 5).toInt();
((status?.currentLevel ?? status?.level ?? 1) - 1).clamp(0, 5).toInt();
return RoomRocketDialog( return RoomRocketDialog(
status: status, status: status,
levels: RoomRocketApiMapper.levelsFromStatus(status), levels: RoomRocketApiMapper.levelsFromStatus(status),
selectedLevel: selectedLevel, selectedLevel: selectedLevel,
onLevelSelected: _onLevelSelected,
rewards: RoomRocketApiMapper.rewardsFromStatus(status), rewards: RoomRocketApiMapper.rewardsFromStatus(status),
rewardGroups: RoomRocketApiMapper.rewardGroupsFromStatus(status),
selectedRewardTab: _selectedRewardTab,
onRewardTabSelected: _onRewardTabSelected,
crew: RoomRocketApiMapper.crewFromStatus(status), crew: RoomRocketApiMapper.crewFromStatus(status),
onClose: widget.onClose, onClose: widget.onClose,
onOpenRank: () => _openRank(selectedLevel), onOpenRank: () => _openRank(selectedLevel),
onRewardSelected: _openReward,
onOpenRecord: _openRecord, onOpenRecord: _openRecord,
onOpenNote: _openNote, onOpenNote: _openNote,
); );
} }
int _levelIndexFromStatus(SCRoomRocketStatusRes? status) {
return ((status?.currentLevel ?? status?.level ?? 1) - 1)
.clamp(0, 5)
.toInt();
}
void _debugStatusAssets(SCRoomRocketStatusRes status) {
final levels = status.levels ?? const <SCRoomRocketLevelRes>[];
_rocketUiDebug(
'status assets loaded roomId=${status.roomId} levels=${levels.length} '
'currentLevel=${status.currentLevel} selectedIndex=${_levelIndexFromStatus(status)}',
);
for (final item in levels) {
_rocketUiDebug(
'level=${item.level} icon=${_shortRocketDebug(item.rocketIconUrl)} '
'centerPreview=${_shortRocketDebug(item.rocketPreviewUrl)} '
'animation=${_shortRocketDebug(item.rocketAnimationUrl)} '
'progress=${_shortRocketDebug(item.progressBarUrl)} '
'shakeThreshold=${item.shakeThresholdPercent}',
);
}
}
void _onLevelSelected(int index) {
_rocketUiDebug('select rocket level=${index + 1}');
setState(() {
_levelTouched = true;
_selectedLevel = index.clamp(0, 5).toInt();
});
}
void _onRewardTabSelected(RoomRocketRewardTab tab) {
if (_selectedRewardTab == tab) {
return;
}
_rocketUiDebug('select reward tab=${tab.name}');
setState(() => _selectedRewardTab = tab);
}
void _openRank(int selectedLevel) { void _openRank(int selectedLevel) {
_rocketUiDebug( _rocketUiDebug(
'tap rocket king roomId=${widget.roomId} level=${selectedLevel + 1}', 'tap rocket king roomId=${widget.roomId} level=${selectedLevel + 1}',
@ -211,29 +442,6 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
}); });
} }
void _openReward(RoomRocketRewardItem reward) {
_rocketUiDebug('tap reward preview roomId=${widget.roomId}');
SmartDialog.dismiss(tag: RoomRocketFloatingEntry.dialogTag).then((_) {
SmartDialog.show(
tag: RoomRocketFloatingEntry.rewardDialogTag,
alignment: Alignment.center,
debounce: true,
animationType: SmartAnimationType.fade,
maskColor: Colors.transparent,
clickMaskDismiss: false,
builder:
(_) => _RoomRocketRewardDialogLoader(
roomId: widget.roomId,
selectedReward: reward,
onClose:
() => SmartDialog.dismiss(
tag: RoomRocketFloatingEntry.rewardDialogTag,
),
),
);
});
}
void _openRecord() { void _openRecord() {
_rocketUiDebug('tap record roomId=${widget.roomId}'); _rocketUiDebug('tap record roomId=${widget.roomId}');
SmartDialog.dismiss(tag: RoomRocketFloatingEntry.dialogTag).then((_) { SmartDialog.dismiss(tag: RoomRocketFloatingEntry.dialogTag).then((_) {
@ -279,69 +487,6 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> {
} }
} }
class _RoomRocketRewardDialogLoader extends StatefulWidget {
const _RoomRocketRewardDialogLoader({
required this.roomId,
required this.selectedReward,
this.onClose,
});
final String roomId;
final RoomRocketRewardItem selectedReward;
final VoidCallback? onClose;
@override
State<_RoomRocketRewardDialogLoader> createState() =>
_RoomRocketRewardDialogLoaderState();
}
class _RoomRocketRewardDialogLoaderState
extends State<_RoomRocketRewardDialogLoader> {
List<RoomRocketRewardItem>? _rewards;
List<RoomRocketRewardWinner>? _winners;
bool _loading = true;
@override
void initState() {
super.initState();
unawaited(_load());
}
Future<void> _load() async {
try {
_rocketUiDebug('request reward-popups roomId=${widget.roomId}');
final res = await SCChatRoomRepository().roomRocketRewardPopups(
widget.roomId,
);
if (!mounted) {
return;
}
setState(() {
_rewards = RoomRocketApiMapper.rewardItemsFromRecords(res.records);
_winners = RoomRocketApiMapper.winnersFromRecords(res.records);
_loading = false;
});
} catch (error) {
_rocketUiDebug('reward-popups request failed: $error');
if (mounted) {
setState(() => _loading = false);
}
}
}
@override
Widget build(BuildContext context) {
return RoomRocketRewardDialog(
selectedReward: widget.selectedReward,
rewards: _rewards,
winners: _winners,
loadingText: _loading ? 'Loading data......' : '',
useMockFallback: false,
onClose: widget.onClose,
);
}
}
class _RoomRocketRecordDialogLoader extends StatefulWidget { class _RoomRocketRecordDialogLoader extends StatefulWidget {
const _RoomRocketRecordDialogLoader({required this.roomId, this.onClose}); const _RoomRocketRecordDialogLoader({required this.roomId, this.onClose});
@ -355,32 +500,74 @@ class _RoomRocketRecordDialogLoader extends StatefulWidget {
class _RoomRocketRecordDialogLoaderState class _RoomRocketRecordDialogLoaderState
extends State<_RoomRocketRecordDialogLoader> { extends State<_RoomRocketRecordDialogLoader> {
final List<SCRoomRocketRewardRecordRes> _rawRecords = [];
List<RoomRocketRecordItem>? _records; List<RoomRocketRecordItem>? _records;
bool _loading = true; bool _loading = true;
bool _loadingMore = false;
bool _hasMore = true;
int _cursor = 1;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
unawaited(_load()); unawaited(_load(reset: true));
} }
Future<void> _load() async { Future<void> _load({bool reset = false}) async {
if (_loadingMore || (!reset && (!_hasMore || _loading))) {
return;
}
final requestCursor = reset ? 1 : _cursor;
setState(() {
if (reset) {
_loading = true;
} else {
_loadingMore = true;
}
});
try { try {
_rocketUiDebug('request reward-records roomId=${widget.roomId}'); _rocketUiDebug(
'request reward-records roomId=${widget.roomId} '
'cursor=$requestCursor limit=$_rocketPageSize',
);
final res = await SCChatRoomRepository().roomRocketRewardRecords( final res = await SCChatRoomRepository().roomRocketRewardRecords(
widget.roomId, widget.roomId,
cursor: requestCursor,
limit: _rocketPageSize,
); );
if (!mounted) { if (!mounted) {
return; return;
} }
final nextRecords = res.records;
setState(() { setState(() {
_records = RoomRocketApiMapper.recordItemsFromRecords(res.records); if (reset) {
_rawRecords
..clear()
..addAll(nextRecords);
} else {
_rawRecords.addAll(nextRecords);
}
_records = RoomRocketApiMapper.recordItemsFromRecords(_rawRecords);
_hasMore = nextRecords.length >= _rocketPageSize;
if (_hasMore) {
_cursor = requestCursor + 1;
}
_loading = false; _loading = false;
_loadingMore = false;
}); });
_rocketUiDebug(
'reward-records loaded cursor=$requestCursor '
'count=${nextRecords.length} total=${_rawRecords.length} '
'hasMore=$_hasMore nextCursor=$_cursor',
);
} catch (error) { } catch (error) {
_rocketUiDebug('reward-records request failed: $error'); _rocketUiDebug('reward-records request failed: $error');
if (mounted) { if (mounted) {
setState(() => _loading = false); setState(() {
_loading = false;
_loadingMore = false;
_hasMore = false;
});
} }
} }
} }
@ -391,6 +578,9 @@ class _RoomRocketRecordDialogLoaderState
records: _records, records: _records,
useMockFallback: false, useMockFallback: false,
loadingText: _loading ? 'Loading data......' : '', loadingText: _loading ? 'Loading data......' : '',
isLoadingMore: _loadingMore,
hasMore: _hasMore,
onLoadMore: () => unawaited(_load()),
limitText: 'Show only records from the last 35 days', limitText: 'Show only records from the last 35 days',
onClose: widget.onClose, onClose: widget.onClose,
); );
@ -448,3 +638,14 @@ void _rocketUiDebug(String message) {
debugPrint('[RoomRocketUi] $message'); debugPrint('[RoomRocketUi] $message');
} }
} }
String _shortRocketDebug(String value) {
final text = value.replaceAll(RegExp(r'\s+'), ' ').trim();
if (text.isEmpty) {
return '-';
}
if (text.length <= 140) {
return text;
}
return '${text.substring(0, 137)}...';
}

View File

@ -0,0 +1,243 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:pag/pag.dart';
import 'package:yumi/ui_kit/components/custom_cached_image.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
class RoomRocketPagEffectOverlay extends StatefulWidget {
const RoomRocketPagEffectOverlay({
super.key,
required this.resource,
this.topAvatarUrl,
this.onCompleted,
});
final String resource;
final String? topAvatarUrl;
final VoidCallback? onCompleted;
static bool isPag(String? resource) {
final lower = resource?.trim().toLowerCase() ?? '';
return lower.contains('.pag');
}
@override
State<RoomRocketPagEffectOverlay> createState() =>
_RoomRocketPagEffectOverlayState();
}
class _RoomRocketPagEffectOverlayState
extends State<RoomRocketPagEffectOverlay> {
bool _completed = false;
@override
Widget build(BuildContext context) {
final resource = widget.resource.trim();
final size = MediaQuery.sizeOf(context);
return IgnorePointer(
child: ColoredBox(
color: Colors.transparent,
child: Center(
child: SizedBox(
width: size.width,
height: size.height,
child: Stack(
clipBehavior: Clip.none,
children: [
_RoomRocketPagTopAvatar(
avatarUrl: widget.topAvatarUrl,
canvasSize: size,
),
Positioned.fill(
child: _RoomRocketPagView(
resource: resource,
width: size.width,
height: size.height,
loop: false,
onCompleted: _complete,
defaultBuilder:
(_) => const Center(child: SCRotatingDotsLoading()),
),
),
],
),
),
),
),
);
}
void _complete() {
if (_completed) {
return;
}
_completed = true;
widget.onCompleted?.call();
}
}
class _RoomRocketPagTopAvatar extends StatelessWidget {
const _RoomRocketPagTopAvatar({
required this.avatarUrl,
required this.canvasSize,
});
final String? avatarUrl;
final Size canvasSize;
@override
Widget build(BuildContext context) {
final url = avatarUrl?.trim() ?? '';
if (url.isEmpty) {
return const SizedBox.shrink();
}
final size = (canvasSize.width * 0.15).clamp(46.0, 66.0).toDouble();
return Positioned(
left: (canvasSize.width - size) / 2,
top: canvasSize.height * 0.098,
width: size,
height: size,
child: ClipOval(
child: CustomCachedImage(
imageUrl: url,
fit: BoxFit.cover,
placeholder: const SizedBox.shrink(),
errorWidget: const SizedBox.shrink(),
),
),
);
}
}
class RoomRocketPagPreview extends StatelessWidget {
const RoomRocketPagPreview({
super.key,
required this.resource,
this.width,
this.height,
this.loop = true,
this.defaultBuilder,
});
final String resource;
final double? width;
final double? height;
final bool loop;
final WidgetBuilder? defaultBuilder;
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (context, constraints) {
final resolvedWidth =
width ??
(constraints.maxWidth.isFinite ? constraints.maxWidth : null);
final resolvedHeight =
height ??
(constraints.maxHeight.isFinite ? constraints.maxHeight : null);
return _RoomRocketPagView(
resource: resource,
width: resolvedWidth,
height: resolvedHeight,
loop: loop,
defaultBuilder:
defaultBuilder ??
(_) => const Center(child: SCRotatingDotsLoading()),
);
},
);
}
}
class _RoomRocketPagView extends StatefulWidget {
const _RoomRocketPagView({
required this.resource,
this.width,
this.height,
required this.loop,
this.onCompleted,
this.defaultBuilder,
});
final String resource;
final double? width;
final double? height;
final bool loop;
final VoidCallback? onCompleted;
final WidgetBuilder? defaultBuilder;
@override
State<_RoomRocketPagView> createState() => _RoomRocketPagViewState();
}
class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
late Future<Uint8List?> _bytesFuture;
@override
void initState() {
super.initState();
_bytesFuture = _loadPagBytes(widget.resource.trim());
}
@override
void didUpdateWidget(covariant _RoomRocketPagView oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.resource.trim() != widget.resource.trim()) {
_bytesFuture = _loadPagBytes(widget.resource.trim());
}
}
@override
Widget build(BuildContext context) {
final path = widget.resource.trim();
if (path.isEmpty || !RoomRocketPagEffectOverlay.isPag(path)) {
return widget.defaultBuilder?.call(context) ?? const SizedBox.shrink();
}
final repeatCount =
widget.loop ? PAGView.REPEAT_COUNT_LOOP : PAGView.REPEAT_COUNT_DEFAULT;
final defaultChild =
widget.defaultBuilder?.call(context) ??
const Center(child: SCRotatingDotsLoading());
return FutureBuilder<Uint8List?>(
future: _bytesFuture,
builder: (context, snapshot) {
final bytes = snapshot.data;
if (snapshot.connectionState != ConnectionState.done ||
bytes == null ||
bytes.isEmpty) {
return defaultChild;
}
return PAGView.bytes(
bytes,
key: ValueKey('room-rocket-pag:$path:${widget.loop}'),
width: widget.width,
height: widget.height,
autoPlay: true,
repeatCount: repeatCount,
onAnimationEnd: widget.onCompleted,
defaultBuilder: (_) => defaultChild,
);
},
);
}
Future<Uint8List?> _loadPagBytes(String path) async {
try {
final data =
_isNetwork(path)
? await NetworkAssetBundle(Uri.parse(path)).load(path)
: await rootBundle.load(path);
return data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
} catch (error) {
if (kDebugMode) {
debugPrint('[RoomRocketPag] load failed path=$path error=$error');
}
return null;
}
}
static bool _isNetwork(String value) {
return value.startsWith('http://') || value.startsWith('https://');
}
}

View File

@ -1,6 +1,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/ui_kit/components/custom_cached_image.dart'; import 'package:yumi/ui_kit/components/custom_cached_image.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart';
@ -10,6 +11,9 @@ class RoomRocketRecordDialog extends StatelessWidget {
this.records, this.records,
this.loadingText = 'Loading data......', this.loadingText = 'Loading data......',
this.limitText = 'Show only records from the last 35 days', this.limitText = 'Show only records from the last 35 days',
this.isLoadingMore = false,
this.hasMore = false,
this.onLoadMore,
this.useMockFallback = true, this.useMockFallback = true,
this.onClose, this.onClose,
}); });
@ -17,11 +21,21 @@ class RoomRocketRecordDialog extends StatelessWidget {
final List<RoomRocketRecordItem>? records; final List<RoomRocketRecordItem>? records;
final String loadingText; final String loadingText;
final String limitText; final String limitText;
final bool isLoadingMore;
final bool hasMore;
final VoidCallback? onLoadMore;
final bool useMockFallback; final bool useMockFallback;
final VoidCallback? onClose; final VoidCallback? onClose;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localizations = SCAppLocalizations.of(context);
final emptyText = _translate(
localizations,
'roomRocketRecordEmptyText',
'No reward records',
);
final contentDirection = Directionality.of(context);
final resolvedRecords = _normalizeRecords( final resolvedRecords = _normalizeRecords(
records, records,
useMockFallback: useMockFallback, useMockFallback: useMockFallback,
@ -57,6 +71,11 @@ class RoomRocketRecordDialog extends StatelessWidget {
records: resolvedRecords, records: resolvedRecords,
loadingText: loadingText, loadingText: loadingText,
limitText: limitText, limitText: limitText,
isLoadingMore: isLoadingMore,
hasMore: hasMore,
onLoadMore: onLoadMore,
emptyText: emptyText,
contentDirection: contentDirection,
onClose: onClose, onClose: onClose,
), ),
), ),
@ -100,6 +119,18 @@ class RoomRocketRecordDialog extends StatelessWidget {
growable: false, growable: false,
); );
} }
static String _translate(
SCAppLocalizations? localizations,
String key,
String fallback,
) {
final value = localizations?.translate(key);
if (value == null || value == key) {
return fallback;
}
return value;
}
} }
class RoomRocketRecordItem { class RoomRocketRecordItem {
@ -122,6 +153,11 @@ class _RecordDialogCanvas extends StatelessWidget {
required this.records, required this.records,
required this.loadingText, required this.loadingText,
required this.limitText, required this.limitText,
required this.isLoadingMore,
required this.hasMore,
this.onLoadMore,
required this.emptyText,
required this.contentDirection,
this.onClose, this.onClose,
}); });
@ -129,19 +165,26 @@ class _RecordDialogCanvas extends StatelessWidget {
final List<RoomRocketRecordItem> records; final List<RoomRocketRecordItem> records;
final String loadingText; final String loadingText;
final String limitText; final String limitText;
final bool isLoadingMore;
final bool hasMore;
final VoidCallback? onLoadMore;
final String emptyText;
final TextDirection contentDirection;
final VoidCallback? onClose; final VoidCallback? onClose;
double s(double value) => value * scale; double s(double value) => value * scale;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isLoading = loadingText.trim().isNotEmpty;
final isEmpty = !isLoading && records.isEmpty;
return Stack( return Stack(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
children: [ children: [
_buildPanelBackground(), _buildPanelBackground(),
_buildHeader(), _buildHeader(),
_buildColumnTitles(), _buildColumnTitles(),
_buildRecordList(), if (isEmpty) _buildEmptyState() else _buildRecordList(),
_buildFooter(), _buildFooter(),
_buildCloseButton(), _buildCloseButton(),
], ],
@ -227,21 +270,81 @@ class _RecordDialogCanvas extends StatelessWidget {
child: ClipRect( child: ClipRect(
child: ScrollConfiguration( child: ScrollConfiguration(
behavior: const _NoGlowScrollBehavior(), behavior: const _NoGlowScrollBehavior(),
child: ListView.builder( child: NotificationListener<ScrollNotification>(
padding: EdgeInsets.zero, onNotification: _handleScrollNotification,
primary: false, child: ListView.builder(
physics: const BouncingScrollPhysics(), padding: EdgeInsets.zero,
itemExtent: s(61), primary: false,
itemCount: records.length, physics: const BouncingScrollPhysics(
itemBuilder: (context, index) { parent: AlwaysScrollableScrollPhysics(),
return _RecordRow(scale: scale, item: records[index]); ),
}, itemExtent: s(61),
itemCount: records.length + (isLoadingMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= records.length) {
return _LoadMoreRow(scale: scale);
}
return _RecordRow(scale: scale, item: records[index]);
},
),
), ),
), ),
), ),
); );
} }
bool _handleScrollNotification(ScrollNotification notification) {
if (notification.metrics.axis != Axis.vertical) {
return false;
}
if (!hasMore || isLoadingMore) {
return false;
}
if (notification.metrics.extentAfter <= s(96)) {
onLoadMore?.call();
}
return false;
}
Widget _buildEmptyState() {
return Positioned(
left: s(0),
top: s(340),
width: s(_designWidth),
height: s(190),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
_RecordDialogAssets.empty,
width: s(86),
height: s(86),
fit: BoxFit.contain,
),
SizedBox(height: s(12)),
SizedBox(
width: s(268),
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(14),
height: 1.25,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
),
],
),
);
}
Widget _buildFooter() { Widget _buildFooter() {
return Stack( return Stack(
children: [ children: [
@ -391,7 +494,9 @@ class _RecordRow extends StatelessWidget {
child: ClipRect( child: ClipRect(
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(), physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
child: Row( child: Row(
children: [ children: [
for (var index = 0; index < rewards.length; index++) ...[ for (var index = 0; index < rewards.length; index++) ...[
@ -429,28 +534,25 @@ class _SmallReward extends StatelessWidget {
top: 0, top: 0,
width: 28 * scale, width: 28 * scale,
height: 28 * scale, height: 28 * scale,
child: _RocketImage( child:
asset: item.asset ?? _RecordDialogAssets.rewardThumb, item.isGoldReward
imageUrl: item.imageUrl, ? Image.asset(_RecordDialogAssets.coin, fit: BoxFit.contain)
fit: BoxFit.cover, : _RocketImage(
), asset: item.asset ?? _RecordDialogAssets.rewardThumb,
imageUrl: item.imageUrl,
fit: BoxFit.contain,
),
), ),
Positioned( Positioned(
left: 0, left: -3 * scale,
top: 30 * scale, top: 30 * scale,
width: 8 * scale, width: 34 * scale,
height: 8 * scale,
child: Image.asset(_RecordDialogAssets.coin, fit: BoxFit.contain),
),
Positioned(
left: 8 * scale,
top: 30 * scale,
width: 26 * scale,
child: Text( child: Text(
item.amount, item.displayText,
textScaler: TextScaler.noScaling, textScaler: TextScaler.noScaling,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.visible, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 8 * scale, fontSize: 8 * scale,
@ -671,6 +773,30 @@ class _NoGlowScrollBehavior extends ScrollBehavior {
} }
} }
class _LoadMoreRow extends StatelessWidget {
const _LoadMoreRow({required this.scale});
final double scale;
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'Loading data......',
textScaler: TextScaler.noScaling,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: const Color(0xFF78FF78).withValues(alpha: 0.56),
fontSize: 10 * scale,
height: 1,
decoration: TextDecoration.none,
),
),
);
}
}
class _RecordDialogAssets { class _RecordDialogAssets {
const _RecordDialogAssets._(); const _RecordDialogAssets._();
@ -681,6 +807,7 @@ class _RecordDialogAssets {
'sc_images/room/rocket/reward_dialog_bottom.png'; 'sc_images/room/rocket/reward_dialog_bottom.png';
static const String rewardThumb = 'sc_images/room/rocket/reward_thumb.png'; static const String rewardThumb = 'sc_images/room/rocket/reward_thumb.png';
static const String coin = 'sc_images/register_reward/yumi_coin.png'; static const String coin = 'sc_images/register_reward/yumi_coin.png';
static const String empty = 'sc_images/room/sc_icon_room_music_empty.png';
} }
const double _designWidth = 375; const double _designWidth = 375;

View File

@ -1,6 +1,7 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/ui_kit/components/custom_cached_image.dart'; import 'package:yumi/ui_kit/components/custom_cached_image.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_dialog.dart';
@ -11,6 +12,9 @@ class RoomRocketRewardDialog extends StatelessWidget {
this.rewards, this.rewards,
this.winners, this.winners,
this.loadingText = 'Loading data......', this.loadingText = 'Loading data......',
this.isLoadingMore = false,
this.hasMore = false,
this.onLoadMore,
this.useMockFallback = true, this.useMockFallback = true,
this.onClose, this.onClose,
}); });
@ -19,11 +23,21 @@ class RoomRocketRewardDialog extends StatelessWidget {
final List<RoomRocketRewardItem>? rewards; final List<RoomRocketRewardItem>? rewards;
final List<RoomRocketRewardWinner>? winners; final List<RoomRocketRewardWinner>? winners;
final String loadingText; final String loadingText;
final bool isLoadingMore;
final bool hasMore;
final VoidCallback? onLoadMore;
final bool useMockFallback; final bool useMockFallback;
final VoidCallback? onClose; final VoidCallback? onClose;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final localizations = SCAppLocalizations.of(context);
final emptyText = _translate(
localizations,
'roomRocketRewardEmptyText',
'No rewards earned. Better luck next time.',
);
final contentDirection = Directionality.of(context);
final resolvedRewards = _normalizeRewards( final resolvedRewards = _normalizeRewards(
selectedReward, selectedReward,
rewards, rewards,
@ -65,6 +79,11 @@ class RoomRocketRewardDialog extends StatelessWidget {
rewards: resolvedRewards, rewards: resolvedRewards,
winners: resolvedWinners, winners: resolvedWinners,
loadingText: loadingText, loadingText: loadingText,
isLoadingMore: isLoadingMore,
hasMore: hasMore,
onLoadMore: onLoadMore,
emptyText: emptyText,
contentDirection: contentDirection,
onClose: onClose, onClose: onClose,
), ),
), ),
@ -89,7 +108,7 @@ class RoomRocketRewardDialog extends StatelessWidget {
if (source.isNotEmpty) { if (source.isNotEmpty) {
return source; return source;
} }
if (!useMockFallback && selectedReward == null) { if (!useMockFallback) {
return const <RoomRocketRewardItem>[]; return const <RoomRocketRewardItem>[];
} }
final fallback = final fallback =
@ -153,6 +172,18 @@ class RoomRocketRewardDialog extends StatelessWidget {
); );
}, growable: false); }, growable: false);
} }
static String _translate(
SCAppLocalizations? localizations,
String key,
String fallback,
) {
final value = localizations?.translate(key);
if (value == null || value == key) {
return fallback;
}
return value;
}
} }
class RoomRocketRewardWinner { class RoomRocketRewardWinner {
@ -187,6 +218,11 @@ class _RewardDialogCanvas extends StatelessWidget {
required this.rewards, required this.rewards,
required this.winners, required this.winners,
required this.loadingText, required this.loadingText,
required this.isLoadingMore,
required this.hasMore,
this.onLoadMore,
required this.emptyText,
required this.contentDirection,
this.onClose, this.onClose,
}); });
@ -194,19 +230,30 @@ class _RewardDialogCanvas extends StatelessWidget {
final List<RoomRocketRewardItem> rewards; final List<RoomRocketRewardItem> rewards;
final List<RoomRocketRewardWinner> winners; final List<RoomRocketRewardWinner> winners;
final String loadingText; final String loadingText;
final bool isLoadingMore;
final bool hasMore;
final VoidCallback? onLoadMore;
final String emptyText;
final TextDirection contentDirection;
final VoidCallback? onClose; final VoidCallback? onClose;
double s(double value) => value * scale; double s(double value) => value * scale;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isLoading = loadingText.trim().isNotEmpty;
final isEmpty = !isLoading && rewards.isEmpty && winners.isEmpty;
return Stack( return Stack(
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
children: [ children: [
_buildPanelBackground(), _buildPanelBackground(),
_buildHeader(), _buildHeader(showSections: !isEmpty),
_buildRewardGrid(), if (isEmpty)
_buildWinnerList(), _buildEmptyState()
else ...[
_buildRewardGrid(),
_buildWinnerList(),
],
_buildLoadingText(), _buildLoadingText(),
_buildCloseButton(), _buildCloseButton(),
], ],
@ -233,7 +280,7 @@ class _RewardDialogCanvas extends StatelessWidget {
); );
} }
Widget _buildHeader() { Widget _buildHeader({required bool showSections}) {
return Stack( return Stack(
children: [ children: [
Positioned( Positioned(
@ -258,48 +305,112 @@ class _RewardDialogCanvas extends StatelessWidget {
), ),
), ),
), ),
Positioned( if (showSections) ...[
left: s(0), Positioned(
top: s(230), left: s(0),
width: s(_designWidth), top: s(230),
child: _SectionTitle( width: s(_designWidth),
text: 'Congratulations on winning', child: _SectionTitle(
scale: scale, text: 'Congratulations on winning',
scale: scale,
),
), ),
), Positioned(
Positioned( left: s(0),
left: s(0), top: s(482),
top: s(482), width: s(_designWidth),
width: s(_designWidth), child: _SectionTitle(
child: _SectionTitle(text: '12 users received rewards', scale: scale), text: '12 users received rewards',
), scale: scale,
),
),
],
], ],
); );
} }
Widget _buildEmptyState() {
return Positioned(
left: s(0),
top: s(322),
width: s(_designWidth),
height: s(250),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Image.asset(
_RewardDialogAssets.empty,
width: s(86),
height: s(86),
fit: BoxFit.contain,
),
SizedBox(height: s(12)),
SizedBox(
width: s(268),
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(14),
height: 1.25,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
),
],
),
);
}
Widget _buildRewardGrid() { Widget _buildRewardGrid() {
if (rewards.isEmpty) { if (rewards.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final visibleRewards = final columns = math.max(3, (rewards.length / 2).ceil());
rewards.length >= 6 return Positioned(
? rewards.take(6).toList(growable: false) left: s(28),
: List<RoomRocketRewardItem>.generate( top: s(256),
6, width: s(336),
(index) => rewards[index % rewards.length], height: s(210),
growable: false, child: ClipRect(
); child: ScrollConfiguration(
return Stack( behavior: const _NoGlowScrollBehavior(),
children: [ child: NotificationListener<ScrollNotification>(
for (var index = 0; index < visibleRewards.length; index++) onNotification: _handleHorizontalScrollNotification,
Positioned( child: SingleChildScrollView(
left: s(28 + (index % 3) * 112), scrollDirection: Axis.horizontal,
top: s(index < 3 ? 256 : 365), physics: const BouncingScrollPhysics(
width: s(97), parent: AlwaysScrollableScrollPhysics(),
height: s(101), ),
child: _RewardDetailCard(scale: scale, item: visibleRewards[index]), child: SizedBox(
width: s(columns * 112),
height: s(210),
child: Stack(
children: [
for (var index = 0; index < rewards.length; index++)
Positioned(
left: s((index ~/ 2) * 112),
top: s(index.isEven ? 0 : 109),
width: s(97),
height: s(101),
child: _RewardDetailCard(
scale: scale,
item: rewards[index],
),
),
],
),
),
),
), ),
], ),
),
); );
} }
@ -312,25 +423,59 @@ class _RewardDialogCanvas extends StatelessWidget {
child: ClipRect( child: ClipRect(
child: ScrollConfiguration( child: ScrollConfiguration(
behavior: const _NoGlowScrollBehavior(), behavior: const _NoGlowScrollBehavior(),
child: ListView.builder( child: NotificationListener<ScrollNotification>(
padding: EdgeInsets.zero, onNotification: _handleScrollNotification,
primary: false, child: ListView.builder(
physics: const BouncingScrollPhysics(), padding: EdgeInsets.zero,
itemExtent: s(51), primary: false,
itemCount: winners.length, physics: const BouncingScrollPhysics(
itemBuilder: (context, index) { parent: AlwaysScrollableScrollPhysics(),
return _WinnerRow( ),
scale: scale, itemExtent: s(51),
winner: winners[index], itemCount: winners.length + (isLoadingMore ? 1 : 0),
showRocketBadge: index == 0, itemBuilder: (context, index) {
); if (index >= winners.length) {
}, return _LoadMoreRow(scale: scale);
}
return _WinnerRow(
scale: scale,
winner: winners[index],
showRocketBadge: index == 0,
);
},
),
), ),
), ),
), ),
); );
} }
bool _handleScrollNotification(ScrollNotification notification) {
if (notification.metrics.axis != Axis.vertical) {
return false;
}
if (!hasMore || isLoadingMore) {
return false;
}
if (notification.metrics.extentAfter <= s(80)) {
onLoadMore?.call();
}
return false;
}
bool _handleHorizontalScrollNotification(ScrollNotification notification) {
if (notification.metrics.axis != Axis.horizontal) {
return false;
}
if (!hasMore || isLoadingMore) {
return false;
}
if (notification.metrics.extentAfter <= s(96)) {
onLoadMore?.call();
}
return false;
}
Widget _buildLoadingText() { Widget _buildLoadingText() {
return Positioned( return Positioned(
left: 0, left: 0,
@ -421,28 +566,25 @@ class _RewardDetailCard extends StatelessWidget {
top: 11 * scale, top: 11 * scale,
width: 50 * scale, width: 50 * scale,
height: 50 * scale, height: 50 * scale,
child: _RocketImage( child:
asset: item.asset ?? _RewardDialogAssets.rewardThumb, item.isGoldReward
imageUrl: item.imageUrl, ? Image.asset(_RewardDialogAssets.coin, fit: BoxFit.contain)
fit: BoxFit.cover, : _RocketImage(
), asset: item.asset ?? _RewardDialogAssets.rewardThumb,
imageUrl: item.imageUrl,
fit: BoxFit.contain,
),
), ),
Positioned( Positioned(
left: 25 * scale, left: 21 * scale,
top: 74 * scale,
width: 12 * scale,
height: 12 * scale,
child: Image.asset(_RewardDialogAssets.coin, fit: BoxFit.contain),
),
Positioned(
left: 39 * scale,
top: 73 * scale, top: 73 * scale,
width: 44 * scale, width: 56 * scale,
child: Text( child: Text(
item.amount, item.displayText,
textScaler: TextScaler.noScaling, textScaler: TextScaler.noScaling,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 12 * scale, fontSize: 12 * scale,
@ -469,7 +611,7 @@ class _WinnerRow extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final rewards = winner.rewards.take(4).toList(growable: false); final rewards = winner.rewards;
return SizedBox( return SizedBox(
height: 51 * scale, height: 51 * scale,
child: Stack( child: Stack(
@ -522,14 +664,33 @@ class _WinnerRow extends StatelessWidget {
), ),
), ),
), ),
for (var index = 0; index < rewards.length; index++) Positioned(
Positioned( left: 202 * scale,
left: (202 + index * 36) * scale, top: 2 * scale,
top: 2 * scale, width: 134 * scale,
width: 27 * scale, height: 36 * scale,
height: 32 * scale, child: ClipRect(
child: _SmallReward(scale: scale, item: rewards[index]), child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics(),
),
child: Row(
children: [
for (var index = 0; index < rewards.length; index++) ...[
SizedBox(
width: 27 * scale,
height: 32 * scale,
child: _SmallReward(scale: scale, item: rewards[index]),
),
if (index != rewards.length - 1)
SizedBox(width: 9 * scale),
],
],
),
),
), ),
),
], ],
), ),
); );
@ -552,28 +713,25 @@ class _SmallReward extends StatelessWidget {
top: 0, top: 0,
width: 22 * scale, width: 22 * scale,
height: 22 * scale, height: 22 * scale,
child: _RocketImage( child:
asset: item.asset ?? _RewardDialogAssets.rewardThumb, item.isGoldReward
imageUrl: item.imageUrl, ? Image.asset(_RewardDialogAssets.coin, fit: BoxFit.contain)
fit: BoxFit.cover, : _RocketImage(
), asset: item.asset ?? _RewardDialogAssets.rewardThumb,
imageUrl: item.imageUrl,
fit: BoxFit.contain,
),
), ),
Positioned( Positioned(
left: 0, left: -1 * scale,
top: 25 * scale,
width: 6 * scale,
height: 6 * scale,
child: Image.asset(_RewardDialogAssets.coin, fit: BoxFit.contain),
),
Positioned(
left: 7 * scale,
top: 24 * scale, top: 24 * scale,
width: 22 * scale, width: 29 * scale,
child: Text( child: Text(
_compactAmount(item.amount), _compactAmount(item.displayText),
textScaler: TextScaler.noScaling, textScaler: TextScaler.noScaling,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 8 * scale, fontSize: 8 * scale,
@ -795,6 +953,30 @@ class _NoGlowScrollBehavior extends ScrollBehavior {
} }
} }
class _LoadMoreRow extends StatelessWidget {
const _LoadMoreRow({required this.scale});
final double scale;
@override
Widget build(BuildContext context) {
return Center(
child: Text(
'Loading data......',
textScaler: TextScaler.noScaling,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: const Color(0xFF78FF78).withValues(alpha: 0.56),
fontSize: 10 * scale,
height: 1,
decoration: TextDecoration.none,
),
),
);
}
}
class _RewardDetailCardPainter extends CustomPainter { class _RewardDetailCardPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
@ -917,6 +1099,7 @@ class _RewardDialogAssets {
'sc_images/room/rocket/reward_dialog_bottom.png'; 'sc_images/room/rocket/reward_dialog_bottom.png';
static const String rewardThumb = 'sc_images/room/rocket/reward_thumb.png'; static const String rewardThumb = 'sc_images/room/rocket/reward_thumb.png';
static const String coin = 'sc_images/register_reward/yumi_coin.png'; static const String coin = 'sc_images/register_reward/yumi_coin.png';
static const String empty = 'sc_images/room/sc_icon_room_music_empty.png';
static const String avatar1 = 'sc_images/room/rocket/rocket_rank_frame_1.png'; static const String avatar1 = 'sc_images/room/rocket/rocket_rank_frame_1.png';
static const String avatar2 = 'sc_images/room/rocket/rocket_rank_frame_2.png'; static const String avatar2 = 'sc_images/room/rocket/rocket_rank_frame_2.png';
static const String avatar3 = 'sc_images/room/rocket/rocket_rank_frame_3.png'; static const String avatar3 = 'sc_images/room/rocket/rocket_rank_frame_3.png';

View File

@ -0,0 +1,206 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_api_mapper.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';
const int _rewardDialogPageSize = 10;
class RoomRocketRewardDialogLoader extends StatefulWidget {
const RoomRocketRewardDialogLoader({
super.key,
required this.roomId,
this.selectedReward,
this.initialPopupRecords = const <SCRoomRocketRewardRecordRes>[],
this.onClose,
});
final String roomId;
final RoomRocketRewardItem? selectedReward;
final List<SCRoomRocketRewardRecordRes> initialPopupRecords;
final VoidCallback? onClose;
@override
State<RoomRocketRewardDialogLoader> createState() =>
_RoomRocketRewardDialogLoaderState();
}
class _RoomRocketRewardDialogLoaderState
extends State<RoomRocketRewardDialogLoader> {
final List<SCRoomRocketRewardRecordRes> _popupRecords = [];
final List<SCRoomRocketRewardRecordRes> _roomRecords = [];
List<RoomRocketRewardItem>? _rewards;
List<RoomRocketRewardWinner>? _winners;
bool _loading = true;
bool _loadingMore = false;
bool _hasMore = true;
bool _usedInitialPopupRecords = false;
int _cursor = 1;
@override
void initState() {
super.initState();
if (widget.initialPopupRecords.isNotEmpty) {
_popupRecords.addAll(widget.initialPopupRecords);
_rewards = RoomRocketApiMapper.rewardItemsFromRecords(_popupRecords);
_winners = RoomRocketApiMapper.winnersFromRecords(_popupRecords);
}
unawaited(_load(reset: true));
}
Future<void> _load({bool reset = false}) async {
if (_loadingMore || (!reset && (!_hasMore || _loading))) {
return;
}
final requestCursor = reset ? 1 : _cursor;
setState(() {
if (reset) {
_loading = true;
} else {
_loadingMore = true;
}
});
try {
final useInitialPopupRecords =
reset &&
widget.initialPopupRecords.isNotEmpty &&
!_usedInitialPopupRecords;
final nextPopupRecords =
useInitialPopupRecords
? widget.initialPopupRecords
: await _loadPopupRecords(requestCursor);
if (useInitialPopupRecords) {
_usedInitialPopupRecords = true;
}
final nextRoomRecords = await _loadRoomRewardRecords(requestCursor);
if (!mounted) {
return;
}
setState(() {
if (reset) {
_popupRecords
..clear()
..addAll(nextPopupRecords);
_roomRecords
..clear()
..addAll(nextRoomRecords);
} else {
_popupRecords.addAll(nextPopupRecords);
_roomRecords.addAll(nextRoomRecords);
}
_rewards = RoomRocketApiMapper.rewardItemsFromRecords(_popupRecords);
_winners = RoomRocketApiMapper.winnersFromRecords(
_visibleRoomRewardRecords(),
);
_hasMore =
nextPopupRecords.length >= _rewardDialogPageSize ||
nextRoomRecords.length >= _rewardDialogPageSize;
if (_hasMore) {
_cursor = requestCursor + 1;
}
_loading = false;
_loadingMore = false;
});
_rewardDialogDebug(
'loaded cursor=$requestCursor '
'popupCount=${nextPopupRecords.length} '
'popupTotal=${_popupRecords.length} '
'roomCount=${nextRoomRecords.length} '
'roomTotal=${_roomRecords.length} '
'visibleRoomTotal=${_visibleRoomRewardRecords().length} '
'hasMore=$_hasMore nextCursor=$_cursor',
);
} catch (error) {
_rewardDialogDebug('request failed: $error');
if (mounted) {
setState(() {
_loading = false;
_loadingMore = false;
_hasMore = false;
});
}
}
}
Future<List<SCRoomRocketRewardRecordRes>> _loadPopupRecords(
int cursor,
) async {
_rewardDialogDebug(
'request reward-popups roomId=${widget.roomId} '
'cursor=$cursor limit=$_rewardDialogPageSize',
);
final res = await SCChatRoomRepository().roomRocketRewardPopups(
widget.roomId,
cursor: cursor,
limit: _rewardDialogPageSize,
);
return res.records;
}
Future<List<SCRoomRocketRewardRecordRes>> _loadRoomRewardRecords(
int cursor,
) async {
if (widget.roomId.trim().isEmpty) {
return const <SCRoomRocketRewardRecordRes>[];
}
try {
_rewardDialogDebug(
'request reward-room-records roomId=${widget.roomId} '
'cursor=$cursor limit=$_rewardDialogPageSize',
);
final res = await SCChatRoomRepository().roomRocketRewardRecords(
widget.roomId,
cursor: cursor,
limit: _rewardDialogPageSize,
);
return res.records;
} catch (error) {
_rewardDialogDebug('reward-room-records request failed: $error');
return const <SCRoomRocketRewardRecordRes>[];
}
}
List<SCRoomRocketRewardRecordRes> _visibleRoomRewardRecords() {
if (_roomRecords.isEmpty) {
return _popupRecords;
}
final launchNos =
_popupRecords
.map((item) => item.launchNo.trim())
.where((item) => item.isNotEmpty)
.toSet();
if (launchNos.isEmpty) {
return _roomRecords;
}
final matched = _roomRecords
.where((item) => launchNos.contains(item.launchNo.trim()))
.toList(growable: false);
return matched.isEmpty ? _roomRecords : matched;
}
@override
Widget build(BuildContext context) {
return RoomRocketRewardDialog(
selectedReward: widget.selectedReward,
rewards: _rewards,
winners: _winners,
loadingText: _loading ? 'Loading data......' : '',
isLoadingMore: _loadingMore,
hasMore: _hasMore,
onLoadMore: () => unawaited(_load()),
useMockFallback: false,
onClose: widget.onClose,
);
}
}
void _rewardDialogDebug(String message) {
if (!kDebugMode) {
return;
}
debugPrint('[RoomRocketRewardDialog] $message');
}

View File

@ -20,6 +20,7 @@ import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
import 'package:yumi/shared/business_logic/usecases/sc_case.dart'; import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
import 'package:yumi/modules/index/main_route.dart'; import 'package:yumi/modules/index/main_route.dart';
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart'; import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart';
import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart'; import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart';
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_chat_message.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_chat_message.dart';
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
@ -1167,31 +1168,25 @@ class _MsgItemState extends State<MsgItem> {
); );
} }
Widget _buildMedals(List<WearBadge> wearBadge) { Widget _buildMedals({
return wearBadge.isNotEmpty required String? userId,
? Expanded( List<WearBadge> fallbackBadges = const <WearBadge>[],
child: SingleChildScrollView( }) {
child: SizedBox( final hasUserId = (userId ?? "").trim().isNotEmpty;
height: 25.w, if (!hasUserId && fallbackBadges.isEmpty) {
child: ListView.separated( return Container();
scrollDirection: Axis.horizontal, }
shrinkWrap: true, return Expanded(
itemCount: wearBadge.length, child: SCUserBadgeStrip(
itemBuilder: (context, index) { userId: userId,
return netImage( fallbackBadges: fallbackBadges,
width: 25.w, height: 25.w,
height: 25.w, badgeHeight: 25.w,
url: wearBadge[index].selectUrl ?? "", longBadgeWidth: 55.w,
); spacing: 5.w,
}, maxBadges: 8,
separatorBuilder: (BuildContext context, int index) { ),
return SizedBox(width: 5.w); );
},
),
),
),
)
: Container();
} }
int _msgColor(String type) { int _msgColor(String type) {
@ -1773,10 +1768,12 @@ class _MsgItemState extends State<MsgItem> {
) )
: Container(), : Container(),
_buildMedals( _buildMedals(
(widget.msg.user?.wearBadge?.where((item) { userId: widget.msg.user?.id,
return item.use ?? false; fallbackBadges:
}).toList() ?? widget.msg.user?.wearBadge
[]), ?.where((item) => item.use ?? false)
.toList() ??
const <WearBadge>[],
), ),
], ],
), ),
@ -1853,10 +1850,12 @@ class _MsgItemState extends State<MsgItem> {
) )
: Container(), : Container(),
_buildMedals( _buildMedals(
user.wearBadge?.where((item) { userId: user.id,
return item.use ?? false; fallbackBadges:
}).toList() ?? user.wearBadge
[], ?.where((item) => item.use ?? false)
.toList() ??
const <WearBadge>[],
), ),
], ],
), ),

View File

@ -0,0 +1,267 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
typedef RoomPasswordSubmit = Future<String?> Function(String password);
class RoomPasswordDialog extends StatefulWidget {
static const String dialogTag = 'showRoomPasswordDialog';
const RoomPasswordDialog({
super.key,
required this.onSubmit,
required this.onCancel,
required this.onSuccess,
});
final RoomPasswordSubmit onSubmit;
final VoidCallback onCancel;
final ValueChanged<String> onSuccess;
static Future<String?> show({required RoomPasswordSubmit onSubmit}) {
final completer = Completer<String?>();
var isCompleted = false;
void complete(String? password) {
if (isCompleted) {
return;
}
isCompleted = true;
completer.complete(password);
}
SmartDialog.dismiss(tag: dialogTag);
SmartDialog.show(
tag: dialogTag,
alignment: Alignment.center,
animationType: SmartAnimationType.fade,
backType: SmartBackType.block,
clickMaskDismiss: false,
maskColor: Colors.black.withValues(alpha: 0.58),
onDismiss: () {
complete(null);
},
builder: (_) {
return RoomPasswordDialog(
onSubmit: onSubmit,
onCancel: () {
complete(null);
SmartDialog.dismiss(tag: dialogTag);
},
onSuccess: (password) {
complete(password);
SmartDialog.dismiss(tag: dialogTag);
},
);
},
);
return completer.future;
}
@override
State<RoomPasswordDialog> createState() => _RoomPasswordDialogState();
}
class _RoomPasswordDialogState extends State<RoomPasswordDialog> {
final TextEditingController _passwordController = TextEditingController();
String _errorText = "";
bool _isSubmitting = false;
@override
void dispose() {
_passwordController.dispose();
super.dispose();
}
Future<void> _handleEnter() async {
if (_isSubmitting) {
return;
}
final password = _passwordController.text.trim();
if (password.isEmpty) {
setState(() {
_errorText = "Please enter password";
});
return;
}
setState(() {
_errorText = "";
_isSubmitting = true;
});
final errorText = await widget.onSubmit(password);
if (!mounted) {
return;
}
if ((errorText ?? "").trim().isNotEmpty) {
setState(() {
_errorText = errorText!.trim();
_isSubmitting = false;
});
return;
}
widget.onSuccess(password);
}
@override
Widget build(BuildContext context) {
return Material(
type: MaterialType.transparency,
child: Center(
child: Container(
width: 316.w,
padding: EdgeInsets.fromLTRB(18.w, 18.w, 18.w, 16.w),
decoration: BoxDecoration(
color: const Color(0xff09372E),
borderRadius: BorderRadius.circular(12.w),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Room lock",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 16.sp,
fontWeight: FontWeight.w600,
height: 1,
),
),
SizedBox(height: 18.w),
_buildPasswordInput(),
SizedBox(height: _errorText.isEmpty ? 14.w : 8.w),
if (_errorText.isNotEmpty) _buildErrorText(),
SizedBox(height: 12.w),
Row(
children: [
Expanded(
child: _buildButton(
label: "cancel",
filled: false,
onTap: _isSubmitting ? null : widget.onCancel,
),
),
SizedBox(width: 12.w),
Expanded(
child: _buildButton(
label: "enter",
filled: true,
onTap: _isSubmitting ? null : _handleEnter,
),
),
],
),
],
),
),
),
);
}
Widget _buildPasswordInput() {
return Container(
height: 44.w,
decoration: BoxDecoration(
color: const Color(0xff08251E),
borderRadius: BorderRadius.circular(8.w),
border: Border.all(color: const Color(0xffB2FBCC), width: 1),
),
child: TextField(
controller: _passwordController,
autofocus: true,
obscureText: true,
cursorColor: SocialChatTheme.primaryColor,
keyboardType: TextInputType.visiblePassword,
textInputAction: TextInputAction.done,
inputFormatters: [LengthLimitingTextInputFormatter(24)],
onSubmitted: (_) {
_handleEnter();
},
textAlignVertical: TextAlignVertical.center,
decoration: InputDecoration(
hintText: "Please enter password",
hintStyle: TextStyle(
color: Colors.white.withValues(alpha: 0.55),
fontSize: 14.sp,
height: 16 / 14,
),
filled: true,
fillColor: Colors.transparent,
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
disabledBorder: InputBorder.none,
errorBorder: InputBorder.none,
focusedErrorBorder: InputBorder.none,
isDense: true,
contentPadding: EdgeInsets.symmetric(horizontal: 12.w),
),
style: TextStyle(
color: Colors.white,
backgroundColor: Colors.transparent,
fontSize: 14.sp,
fontWeight: FontWeight.w400,
height: 16 / 14,
),
),
);
}
Widget _buildErrorText() {
return SizedBox(
width: double.infinity,
child: Text(
_errorText,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: const Color(0xffff4d4d),
fontSize: 12.sp,
fontWeight: FontWeight.w400,
height: 14 / 12,
),
),
);
}
Widget _buildButton({
required String label,
required bool filled,
required VoidCallback? onTap,
}) {
final child = Container(
height: 38.w,
alignment: Alignment.center,
decoration: BoxDecoration(
color: filled ? SocialChatTheme.primaryColor : Colors.transparent,
borderRadius: BorderRadius.circular(19.w),
border: Border.all(
color: filled ? SocialChatTheme.primaryColor : Colors.white,
width: 1.w,
),
),
child: Text(
label,
textAlign: TextAlign.center,
style: TextStyle(
color: filled ? Colors.black : Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w600,
height: 1,
),
),
);
return Opacity(
opacity: onTap == null ? 0.55 : 1,
child: SCDebounceWidget(onTap: onTap ?? () {}, child: child),
);
}
}

View File

@ -6,6 +6,7 @@ import 'package:provider/provider.dart';
import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/services/music/room_music_manager.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
import 'package:yumi/ui_kit/widgets/room/music/room_music_floating_entry.dart'; import 'package:yumi/ui_kit/widgets/room/music/room_music_floating_entry.dart';
import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart';
@ -35,9 +36,15 @@ class _RoomPlayWidgetState extends State<RoomPlayWidget> {
bottom: (150 - _rightActionDownShift).w, bottom: (150 - _rightActionDownShift).w,
child: const RoomMusicFloatingEntry(), child: const RoomMusicFloatingEntry(),
), ),
PositionedDirectional( Selector<RoomMusicManager, bool>(
end: 65.w, selector: (_, manager) => manager.current != null,
bottom: (150 - _rightActionDownShift).w, builder: (context, hasCurrentMusic, child) {
return PositionedDirectional(
end: (hasCurrentMusic ? 65 : 15).w,
bottom: (150 - _rightActionDownShift).w,
child: child!,
);
},
child: const RoomRocketFloatingEntry(), child: const RoomRocketFloatingEntry(),
), ),
PositionedDirectional( PositionedDirectional(

File diff suppressed because it is too large Load Diff

View File

@ -635,6 +635,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.7.0" version: "0.7.0"
flutter_svg:
dependency: "direct main"
description:
name: flutter_svg
sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.3.0"
flutter_svga: flutter_svga:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1137,6 +1145,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.2.1" version: "3.2.1"
pag:
dependency: "direct main"
description:
name: pag
sha256: "8ba3b9c18309b0ef71ad53d20c67ed13893ad3cc49fdb7cc48a8d4e78bfe6db7"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.7"
path: path:
dependency: "direct main" dependency: "direct main"
description: description:
@ -1645,6 +1661,30 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.5.3" version: "4.5.3"
vector_graphics:
dependency: transitive
description:
name: vector_graphics
sha256: "4d35a36400983c3457c289d4d553b5308f506ea84f7e51c7a564651b5525209a"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.1"
vector_graphics_codec:
dependency: transitive
description:
name: vector_graphics_codec
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.13"
vector_graphics_compiler:
dependency: transitive
description:
name: vector_graphics_compiler
sha256: "98e7e94de127b46a86ef46197fff84ff99f3d3b80a708390d717ad731efef598"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.2.2"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.3.1+131 version: 1.4.0+140
environment: environment:
@ -133,6 +133,8 @@ dependencies:
readmore: ^3.0.0 readmore: ^3.0.0
#外链跳转 #外链跳转
app_links: ^6.4.1 app_links: ^6.4.1
flutter_svg: ^2.3.0
pag: ^1.0.7
dev_dependencies: dev_dependencies:
flutter_launcher_icons: ^0.14.4 flutter_launcher_icons: ^0.14.4

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 KiB