From 77d1fd13b1d016af161523e54dc154ec8a9d205a Mon Sep 17 00:00:00 2001 From: roxy Date: Fri, 15 May 2026 17:43:49 +0800 Subject: [PATCH] 1.4 --- assets/l10n/intl_ar.json | 5 +- assets/l10n/intl_bn.json | 5 +- assets/l10n/intl_en.json | 5 +- assets/l10n/intl_tr.json | 5 +- ios/Podfile.lock | 10 + ios/Runner.xcodeproj/project.pbxproj | 12 +- lib/app_localizations.dart | 2 + lib/debug/room_rocket_floating_preview.dart | 66 + lib/modules/gift/gift_page.dart | 318 +++- lib/modules/gift/gift_tab_page.dart | 55 +- .../popular/follow/sc_room_follow_page.dart | 28 +- .../popular/history/sc_room_history_page.dart | 28 +- .../popular/party/sc_home_party_page.dart | 1461 ++++++++--------- lib/modules/room/edit/room_edit_page.dart | 1009 +++++++----- lib/modules/room/online/room_online_page.dart | 2 +- .../room/rank/room_gift_rank_tab_page.dart | 2 +- .../room/rocket/room_rocket_rank_page.dart | 117 +- lib/modules/room/voice_room_page.dart | 1 + lib/modules/search/sc_search_page.dart | 6 +- .../user/profile/person_detail_page.dart | 24 +- lib/services/audio/rtc_manager.dart | 311 +++- lib/services/audio/rtm_manager.dart | 342 +++- .../general/sc_app_general_manager.dart | 84 +- .../business_logic/models/res/gift_res.dart | 20 + .../business_logic/models/res/login_res.dart | 85 +- .../models/res/sc_room_rocket_api_res.dart | 348 +++- .../models/res/sc_room_rocket_status_res.dart | 87 + .../models/res/sc_user_badge_res.dart | 76 + .../repositories/room_repository.dart | 19 +- .../repositories/user_repository.dart | 15 +- .../room_rocket_launch_broadcast_message.dart | 83 + .../local/floating_screen_manager.dart | 362 +++- .../repositories/sc_room_repository_imp.dart | 256 ++- .../repositories/sc_user_repository_impl.dart | 32 + lib/shared/tools/sc_path_utils.dart | 2 + lib/shared/tools/sc_room_utils.dart | 18 + .../widgets/badge/sc_user_badge_strip.dart | 183 +++ lib/ui_kit/widgets/room/empty_mai_select.dart | 2 +- .../floating_room_rocket_screen_widget.dart | 292 +++- .../room/rocket/room_rocket_api_mapper.dart | 31 +- .../room/rocket/room_rocket_dialog.dart | 671 +++++++- .../rocket/room_rocket_floating_entry.dart | 475 ++++-- .../room_rocket_pag_effect_overlay.dart | 243 +++ .../rocket/room_rocket_record_dialog.dart | 181 +- .../rocket/room_rocket_reward_dialog.dart | 359 +++- .../room_rocket_reward_dialog_loader.dart | 206 +++ lib/ui_kit/widgets/room/room_msg_item.dart | 65 +- .../widgets/room/room_password_dialog.dart | 267 +++ lib/ui_kit/widgets/room/room_play_widget.dart | 13 +- .../widgets/room/room_user_info_card.dart | 1218 +++++++------- pubspec.lock | 40 + pubspec.yaml | 4 +- .../room/rocket/rocket_floating_banner_bg.png | Bin 0 -> 155590 bytes 53 files changed, 7021 insertions(+), 2530 deletions(-) create mode 100644 lib/debug/room_rocket_floating_preview.dart create mode 100644 lib/shared/business_logic/models/res/sc_user_badge_res.dart create mode 100644 lib/ui_kit/widgets/badge/sc_user_badge_strip.dart create mode 100644 lib/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart create mode 100644 lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart create mode 100644 lib/ui_kit/widgets/room/room_password_dialog.dart create mode 100644 sc_images/room/rocket/rocket_floating_banner_bg.png diff --git a/assets/l10n/intl_ar.json b/assets/l10n/intl_ar.json index bf375c9..48ac919 100644 --- a/assets/l10n/intl_ar.json +++ b/assets/l10n/intl_ar.json @@ -723,5 +723,8 @@ "duration2": "المدة:{1}", "roomRocketNoteTitle": "ملاحظة", "roomRocketNoteSectionTitle": "في كل مرة يتم فيها شراء مستوى VIP", - "roomRocketNoteSectionBody": "1. في كل مرة يتم فيها شراء مستوى VIP، تكون صلاحيته 30 يومًا، ويمكن تجديده يدويًا بعد انتهاء الصلاحية." + "roomRocketNoteSectionBody": "1. في كل مرة يتم فيها شراء مستوى VIP، تكون صلاحيته 30 يومًا، ويمكن تجديده يدويًا بعد انتهاء الصلاحية.", + "roomRocketRewardEmptyText": "لم تحصل على مكافآت. حظًا أوفر في المرة القادمة.", + "roomRocketRecordEmptyText": "لا توجد سجلات مكافآت", + "launchRocket": "إطلاق الصاروخ" } diff --git a/assets/l10n/intl_bn.json b/assets/l10n/intl_bn.json index cfb2ca5..a7cbbf2 100644 --- a/assets/l10n/intl_bn.json +++ b/assets/l10n/intl_bn.json @@ -723,5 +723,8 @@ "duration2": "সময়:{1}", "roomRocketNoteTitle": "নোট", "roomRocketNoteSectionTitle": "প্রতিবার VIP লেভেল কেনার সময়", - "roomRocketNoteSectionBody": "1. প্রতিবার VIP লেভেল কেনার পর এটি ৩০ দিনের জন্য বৈধ থাকে এবং মেয়াদ শেষ হলে ম্যানুয়ালি নবায়ন করা যায়।" + "roomRocketNoteSectionBody": "1. প্রতিবার VIP লেভেল কেনার পর এটি ৩০ দিনের জন্য বৈধ থাকে এবং মেয়াদ শেষ হলে ম্যানুয়ালি নবায়ন করা যায়।", + "roomRocketRewardEmptyText": "কোনো পুরস্কার পাওয়া যায়নি। পরের বার শুভকামনা।", + "roomRocketRecordEmptyText": "কোনো পুরস্কারের রেকর্ড নেই", + "launchRocket": "রকেট চালু করুন" } diff --git a/assets/l10n/intl_en.json b/assets/l10n/intl_en.json index c0bdebc..8913d5f 100644 --- a/assets/l10n/intl_en.json +++ b/assets/l10n/intl_en.json @@ -723,5 +723,8 @@ "duration2": "Duration:{1}", "roomRocketNoteTitle": "Note", "roomRocketNoteSectionTitle": "Each time a VIP", - "roomRocketNoteSectionBody": "1. Each time a VIP level is purchased, it is valid for 30 days, and can be manually renewed after expiration." + "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" } diff --git a/assets/l10n/intl_tr.json b/assets/l10n/intl_tr.json index 5d5cd39..7715660 100644 --- a/assets/l10n/intl_tr.json +++ b/assets/l10n/intl_tr.json @@ -723,5 +723,8 @@ "duration2": "Süre:{1}", "roomRocketNoteTitle": "Not", "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" } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index cdaa4c8..f371098 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -189,6 +189,7 @@ PODS: - FlutterMacOS - iris_method_channel (0.0.1): - Flutter + - libpag (4.5.52) - libwebp (1.5.0): - libwebp/demux (= 1.5.0) - libwebp/mux (= 1.5.0) @@ -214,6 +215,9 @@ PODS: - SwiftyBeaver - package_info_plus (0.4.5): - Flutter + - pag (0.0.1): + - Flutter + - libpag - permission_handler_apple (9.3.0): - Flutter - PromisesObjC (2.4.0) @@ -275,6 +279,7 @@ DEPENDENCIES: - iris_method_channel (from `.symlinks/plugins/iris_method_channel/ios`) - on_audio_query_ios (from `.symlinks/plugins/on_audio_query_ios/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`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sign_in_with_apple (from `.symlinks/plugins/sign_in_with_apple/ios`) @@ -311,6 +316,7 @@ SPEC REPOS: - GTMAppAuth - GTMSessionFetcher - HydraAsync + - libpag - libwebp - Mantle - nanopb @@ -358,6 +364,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/on_audio_query_ios/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" + pag: + :path: ".symlinks/plugins/pag/ios" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" shared_preferences_foundation: @@ -419,11 +427,13 @@ SPEC CHECKSUMS: image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 in_app_purchase_storekit: 22cca7d08eebca9babdf4d07d0baccb73325d3c8 iris_method_channel: 7d661cf3259b3009ae423508470dbeb9374446ee + libpag: 98742fad4b3ac2a2ee31e383d2483495fb9a5fb4 libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8 Mantle: c5aa8794a29a022dfbbfc9799af95f477a69b62d nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 on_audio_query_ios: 28a780e2d0d85d92d500ba6e12c6c8167022b2fa package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499 + pag: f01aa9017ab0e04a83ba4a20d6070a50f0ac9da8 permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 49d0ef2..adc7601 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -502,7 +502,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = S9X2AJ2US9; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -511,7 +511,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.4.0; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -693,7 +693,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = F33K8VUZ62; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -702,7 +702,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.4.0; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -722,7 +722,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 131; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = F33K8VUZ62; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -731,7 +731,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.3.1; + MARKETING_VERSION = 1.4.0; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/lib/app_localizations.dart b/lib/app_localizations.dart index ad694ee..8f6f70b 100644 --- a/lib/app_localizations.dart +++ b/lib/app_localizations.dart @@ -799,6 +799,8 @@ class SCAppLocalizations { String get launchedARocket => translate('launchedARocket'); + String get launchRocket => translate('launchRocket'); + String get useCoupontips => translate('useCoupontips'); String get joinMemberTips2 => translate('joinMemberTips2'); diff --git a/lib/debug/room_rocket_floating_preview.dart b/lib/debug/room_rocket_floating_preview.dart new file mode 100644 index 0000000..3380d46 --- /dev/null +++ b/lib/debug/room_rocket_floating_preview.dart @@ -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: () {}, + ), + ), + ], + ), + ); + } +} diff --git a/lib/modules/gift/gift_page.dart b/lib/modules/gift/gift_page.dart index b57ad25..49dab82 100644 --- a/lib/modules/gift/gift_page.dart +++ b/lib/modules/gift/gift_page.dart @@ -60,10 +60,12 @@ class GiftPage extends StatefulWidget { } class _GiftPageState extends State with TickerProviderStateMixin { + static const String _backpackGiftTab = SCAppGeneralManager.backpackGiftTab; static const Duration _comboSendBatchWindow = Duration(milliseconds: 200); static const List _preferredGiftTabOrder = [ "ALL", + _backpackGiftTab, "ACTIVITY", "LUCKY_GIFT", "CP", @@ -218,7 +220,12 @@ class _GiftPageState extends State with TickerProviderStateMixin { listen: false, ).giftList(includeCustomized: false); Provider.of(context, listen: false).giftActivityList(); - // Provider.of(context, listen: false).giftBackpack(); + unawaited( + Provider.of( + context, + listen: false, + ).giftBackpack(forceRefresh: true), + ); Provider.of(context, listen: false).balance(); rtcProvider?.roomWheatMap.forEach((k, v) { if (v.user != null) { @@ -262,10 +269,23 @@ class _GiftPageState extends State with TickerProviderStateMixin { ref.giftByTab.entries .where((entry) => entry.value.isNotEmpty) .where((entry) => entry.key != "CUSTOMIZED") + .where((entry) => entry.key != _backpackGiftTab) .map((entry) => entry.key) .toList(); final orderedTypes = []; + if (availableTypes.remove("ALL") || + ref.isGiftListLoading || + ref.giftResList.isEmpty) { + orderedTypes.add("ALL"); + } for (final type in _preferredGiftTabOrder) { + if (type == "ALL") { + continue; + } + if (type == _backpackGiftTab) { + orderedTypes.add(type); + continue; + } if (availableTypes.remove(type)) { orderedTypes.add(type); } @@ -328,7 +348,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { void _ensureCurrentTabSelection(SCAppGeneralManager ref, String tabType) { final gifts = ref.giftByTab[tabType] ?? const []; - if (_matchesCurrentGift(gifts)) { + if (_matchesCurrentGift(tabType, gifts)) { return; } final gift = _resolveSelectedGift(tabType, gifts); @@ -347,6 +367,13 @@ class _GiftPageState extends State with TickerProviderStateMixin { final selectedGift = _selectedGiftByTab[tabType]; if (selectedGift?.id != null) { 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) { return gift; } @@ -359,12 +386,22 @@ class _GiftPageState extends State with TickerProviderStateMixin { return gifts.first; } - bool _matchesCurrentGift(List gifts) { + bool _matchesCurrentGift(String tabType, List gifts) { final currentGiftId = checkedGift?.id; if (currentGiftId == null) { 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) { @@ -384,6 +421,8 @@ class _GiftPageState extends State with TickerProviderStateMixin { return localizations.customized; case "NSCIONAL_FLAG": return localizations.country; + case _backpackGiftTab: + return localizations.bag; default: return tabType .toLowerCase() @@ -421,6 +460,10 @@ class _GiftPageState extends State with TickerProviderStateMixin { giftTab == SCGiftType.MAGIC.name; } + bool _usesBackpackGiftEndpoint(SocialChatGiftRes? gift) { + return (gift?.giftTab ?? '').trim().toLowerCase() == _backpackGiftTab; + } + bool _hasValidLuckyGiftStandardId(SocialChatGiftRes gift) { final standardId = (gift.standardId ?? '').trim(); return standardId.isNotEmpty && standardId != '0'; @@ -444,7 +487,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { if (error is DioException) { final responseData = error.response?.data; - if (responseData is Map) { + if (responseData is Map) { final responseMessage = sanitize( responseData['errorMsg']?.toString() ?? '', ); @@ -488,6 +531,47 @@ class _GiftPageState extends State with TickerProviderStateMixin { 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 = []; + 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 Widget build(BuildContext context) { return Consumer( @@ -1108,7 +1192,51 @@ class _GiftPageState extends State with TickerProviderStateMixin { final roomId = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? ""; final 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( + context, + listen: false, + ).giftBackpack(forceRefresh: true), + ); + return null; + } + } if (isLuckyGiftRequest && !_hasValidLuckyGiftStandardId(selectedGift)) { const configError = @@ -1134,6 +1262,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { roomId: roomId, roomAccount: roomAccount, isLuckyGiftRequest: isLuckyGiftRequest, + isBackpackGiftRequest: isBackpackGiftRequest, ); } @@ -1144,19 +1273,23 @@ class _GiftPageState extends State with TickerProviderStateMixin { return; } - RoomGiftComboSendController().show( - request: RoomGiftComboSendRequest( - acceptUserIds: List.from(request.acceptUserIds), - acceptUsers: List.from(request.acceptUsers), - gift: request.gift, - quantity: request.quantity, - clickCount: request.clickCount, - roomId: request.roomId, - roomAccount: request.roomAccount, - isLuckyGiftRequest: request.isLuckyGiftRequest, - ), - executor: _performFloatingComboSend, - ); + if (request.isBackpackGiftRequest) { + RoomGiftComboSendController().hide(); + } else { + RoomGiftComboSendController().show( + request: RoomGiftComboSendRequest( + acceptUserIds: List.from(request.acceptUserIds), + acceptUsers: List.from(request.acceptUsers), + gift: request.gift, + quantity: request.quantity, + clickCount: request.clickCount, + roomId: request.roomId, + roomAccount: request.roomAccount, + isLuckyGiftRequest: request.isLuckyGiftRequest, + ), + executor: _performFloatingComboSend, + ); + } if (_supportsComboRequestBatching(request.gift)) { _enqueueComboGiftSendRequest(request); @@ -1168,7 +1301,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { } bool _supportsComboRequestBatching(SocialChatGiftRes gift) { - return _supportsComboFeedback(gift); + return !_usesBackpackGiftEndpoint(gift) && _supportsComboFeedback(gift); } void _enqueueComboGiftSendRequest(_GiftSendRequest request) { @@ -1315,6 +1448,40 @@ class _GiftPageState extends State with TickerProviderStateMixin { 'quantity=${request.quantity} ' '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 = request.isLuckyGiftRequest ? await repository.giveLuckyGift( @@ -1374,7 +1541,10 @@ class _GiftPageState extends State with TickerProviderStateMixin { } profileManager?.updateBalance(result); } catch (e) { - final errorMessage = _resolveGiftSendErrorMessage(e); + final errorMessage = + request.isBackpackGiftRequest && _isBackpackQuantityError(e) + ? _backpackQuantityInsufficientMessage() + : _resolveGiftSendErrorMessage(e); final errorDetails = _describeGiftSendError(e); _giftFxLog( '$requestName failed trigger=$trigger ' @@ -1387,10 +1557,79 @@ class _GiftPageState extends State with TickerProviderStateMixin { 'details={$errorDetails} ' 'elapsedMs=${stopwatch.elapsedMilliseconds}', ); + if (request.isBackpackGiftRequest) { + unawaited(_refreshGiftBackpack()); + } SCTts.show(errorMessage); } } + Future> _giveBackpackGiftToRecipients( + SCChatRoomRepository repository, + _GiftSendRequest request, + ) async { + final sentUsers = []; + 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 []) { + if ((mic.user?.id ?? "").trim() == normalizedUserId) { + return mic.micIndex; + } + } + return null; + } + + Future _refreshGiftBackpack() async { + final context = navigatorKey.currentState?.context; + if (context == null) { + return; + } + await Provider.of( + context, + listen: false, + ).giftBackpack(forceRefresh: true); + } + void sendGiftMsg( List acceptUsers, { required SocialChatGiftRes gift, @@ -1628,6 +1867,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { roomId: request.roomId, roomAccount: request.roomAccount, isLuckyGiftRequest: request.isLuckyGiftRequest, + isBackpackGiftRequest: _usesBackpackGiftEndpoint(request.gift), ), trigger: trigger, ); @@ -1752,7 +1992,11 @@ class _GiftPageState extends State with TickerProviderStateMixin { final sortedAcceptUserIds = List.from(request.acceptUserIds) ..sort(); return [ - request.isLuckyGiftRequest ? 'lucky' : 'gift', + request.isBackpackGiftRequest + ? 'backpack' + : request.isLuckyGiftRequest + ? 'lucky' + : 'gift', request.roomId, request.gift.id ?? '', request.quantity, @@ -1842,6 +2086,7 @@ class _GiftSendRequest { required this.roomId, required this.roomAccount, required this.isLuckyGiftRequest, + required this.isBackpackGiftRequest, }); final List acceptUserIds; @@ -1852,15 +2097,30 @@ class _GiftSendRequest { final String roomId; final String roomAccount; 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 { final sortedAcceptUserIds = List.from(acceptUserIds)..sort(); - return '${isLuckyGiftRequest ? "lucky" : "gift"}' - '|${gift.id ?? ""}' - '|$roomId' - '|${sortedAcceptUserIds.join(",")}'; + final type = + isBackpackGiftRequest + ? "backpack" + : isLuckyGiftRequest + ? "lucky" + : "gift"; + return [ + type, + gift.id ?? "", + gift.backpackId ?? "", + roomId, + sortedAcceptUserIds.join(","), + ].join("|"); } } @@ -1875,6 +2135,7 @@ class _PendingGiftSendBatch { required this.roomId, required this.roomAccount, required this.isLuckyGiftRequest, + required this.isBackpackGiftRequest, required this.readyAt, }); @@ -1892,6 +2153,7 @@ class _PendingGiftSendBatch { roomId: request.roomId, roomAccount: request.roomAccount, isLuckyGiftRequest: request.isLuckyGiftRequest, + isBackpackGiftRequest: request.isBackpackGiftRequest, readyAt: readyAt, ); } @@ -1905,6 +2167,7 @@ class _PendingGiftSendBatch { final String roomId; final String roomAccount; final bool isLuckyGiftRequest; + final bool isBackpackGiftRequest; DateTime readyAt; _GiftSendRequest toRequest() { @@ -1917,6 +2180,7 @@ class _PendingGiftSendBatch { roomId: roomId, roomAccount: roomAccount, isLuckyGiftRequest: isLuckyGiftRequest, + isBackpackGiftRequest: isBackpackGiftRequest, ); } } diff --git a/lib/modules/gift/gift_tab_page.dart b/lib/modules/gift/gift_tab_page.dart index 574c267..7dff45e 100644 --- a/lib/modules/gift/gift_tab_page.dart +++ b/lib/modules/gift/gift_tab_page.dart @@ -64,7 +64,7 @@ class _GiftTabPageState extends State _schedulePageWarmup(ref, gifts, _index); } return gifts.isEmpty - ? (ref.isGiftListLoading + ? (ref.isGiftTabLoading(widget.type) ? _buildGiftPageSkeleton() : mainEmpty( textColor: Colors.white54, @@ -444,19 +444,33 @@ class _GiftTabPageState extends State ), Row( mainAxisAlignment: MainAxisAlignment.center, - children: [ - 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, - ), - ], + children: + _isBackpackTab + ? [ + text( + "x${_formatBackpackQuantity(gift)}", + fontSize: 10.sp, + fontWeight: FontWeight.w600, + textColor: + widget.isDark + ? SocialChatTheme.primaryLight + : 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 ); } + 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) { var size = (ref.giftByTab[widget.type]!.length) / 8; var list = []; diff --git a/lib/modules/home/popular/follow/sc_room_follow_page.dart b/lib/modules/home/popular/follow/sc_room_follow_page.dart index 5962815..8f4e4c1 100644 --- a/lib/modules/home/popular/follow/sc_room_follow_page.dart +++ b/lib/modules/home/popular/follow/sc_room_follow_page.dart @@ -203,35 +203,17 @@ class _RoomFollowPageState ), ), SizedBox(width: 5.w), - (roomRes - .roomProfile - ?.extValues - ?.roomSetting - ?.password - ?.isEmpty ?? - false) + roomRes.roomProfile?.extValues?.existsPassword != true ? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w) : Image.asset( "sc_images/index/sc_icon_room_suo.png", width: 20.w, height: 20.w, ), - (roomRes - .roomProfile - ?.extValues - ?.roomSetting - ?.password - ?.isEmpty ?? - false) + roomRes.roomProfile?.extValues?.existsPassword != true ? SizedBox(width: 3.w) : Container(height: 10.w), - (roomRes - .roomProfile - ?.extValues - ?.roomSetting - ?.password - ?.isEmpty ?? - false) + roomRes.roomProfile?.extValues?.existsPassword != true ? text( roomRes.roomProfile?.displayMemberCount ?? "0", fontSize: 10.sp, @@ -363,8 +345,8 @@ class _RoomFollowPageState previous.roomProfile?.roomGameIcon == next.roomProfile?.roomGameIcon && previous.roomProfile?.displayMemberCount == next.roomProfile?.displayMemberCount && - previous.roomProfile?.extValues?.roomSetting?.password == - next.roomProfile?.extValues?.roomSetting?.password; + previous.roomProfile?.extValues?.existsPassword == + next.roomProfile?.extValues?.existsPassword; } bool _sameRoomLists(List previous, List next) { diff --git a/lib/modules/home/popular/history/sc_room_history_page.dart b/lib/modules/home/popular/history/sc_room_history_page.dart index 9842276..cebdea6 100644 --- a/lib/modules/home/popular/history/sc_room_history_page.dart +++ b/lib/modules/home/popular/history/sc_room_history_page.dart @@ -204,35 +204,17 @@ class _SCRoomHistoryPageState ), ), SizedBox(width: 5.w), - (roomRes - .roomProfile - ?.extValues - ?.roomSetting - ?.password - ?.isEmpty ?? - false) + roomRes.roomProfile?.extValues?.existsPassword != true ? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w) : Image.asset( "sc_images/index/sc_icon_room_suo.png", width: 20.w, height: 20.w, ), - (roomRes - .roomProfile - ?.extValues - ?.roomSetting - ?.password - ?.isEmpty ?? - false) + roomRes.roomProfile?.extValues?.existsPassword != true ? SizedBox(width: 3.w) : Container(height: 10.w), - (roomRes - .roomProfile - ?.extValues - ?.roomSetting - ?.password - ?.isEmpty ?? - false) + roomRes.roomProfile?.extValues?.existsPassword != true ? text( roomRes.roomProfile?.displayMemberCount ?? "0", fontSize: 10.sp, @@ -363,8 +345,8 @@ class _SCRoomHistoryPageState previous.roomProfile?.roomGameIcon == next.roomProfile?.roomGameIcon && previous.roomProfile?.displayMemberCount == next.roomProfile?.displayMemberCount && - previous.roomProfile?.extValues?.roomSetting?.password == - next.roomProfile?.extValues?.roomSetting?.password; + previous.roomProfile?.extValues?.existsPassword == + next.roomProfile?.extValues?.existsPassword; } bool _sameRoomLists(List previous, List next) { diff --git a/lib/modules/home/popular/party/sc_home_party_page.dart b/lib/modules/home/popular/party/sc_home_party_page.dart index 92b28fb..69be8f6 100644 --- a/lib/modules/home/popular/party/sc_home_party_page.dart +++ b/lib/modules/home/popular/party/sc_home_party_page.dart @@ -3,16 +3,16 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; -import 'package:provider/provider.dart'; -import 'package:pull_to_refresh/pull_to_refresh.dart'; -import '../../../../app_localizations.dart'; -import '../../../../app/constants/sc_global_config.dart'; -import '../../../../app/routes/sc_fluro_navigator.dart'; -import '../../../../shared/business_logic/models/res/sc_index_banner_res.dart'; -import '../../../../shared/tools/sc_banner_utils.dart'; -import '../../../../shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; -import '../../../../shared/business_logic/models/res/follow_room_res.dart'; -import '../../../../shared/business_logic/models/res/room_res.dart'; +import 'package:provider/provider.dart'; +import 'package:pull_to_refresh/pull_to_refresh.dart'; +import '../../../../app_localizations.dart'; +import '../../../../app/constants/sc_global_config.dart'; +import '../../../../app/routes/sc_fluro_navigator.dart'; +import '../../../../shared/business_logic/models/res/sc_index_banner_res.dart'; +import '../../../../shared/tools/sc_banner_utils.dart'; +import '../../../../shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; +import '../../../../shared/business_logic/models/res/follow_room_res.dart'; +import '../../../../shared/business_logic/models/res/room_res.dart'; import '../../../../services/audio/rtc_manager.dart'; import '../../../../services/general/sc_app_general_manager.dart'; import '../../../../services/home/home_room_preload_manager.dart'; @@ -21,10 +21,10 @@ import '../../../../ui_kit/components/text/sc_text.dart'; import '../../../../ui_kit/widgets/room/room_live_audio_indicator.dart'; import '../../../../ui_kit/widgets/svga/sc_svga_asset_widget.dart'; import '../../../index/main_route.dart'; - -const Duration _kPartySkeletonAnimationDuration = Duration(milliseconds: 1450); -const Color _kPartySkeletonShell = Color(0xFF0F3730); -const Color _kPartySkeletonShellStrong = Color(0xFF1A4A41); + +const Duration _kPartySkeletonAnimationDuration = Duration(milliseconds: 1450); +const Color _kPartySkeletonShell = Color(0xFF0F3730); +const Color _kPartySkeletonShellStrong = Color(0xFF1A4A41); const Color _kPartySkeletonBoneBase = Color(0xFF2B5C53); const Color _kPartySkeletonBoneHighlight = Color(0xFF5F8177); const Color _kPartySkeletonBorder = Color(0x66D8B57B); @@ -42,12 +42,12 @@ class _RoomCountryFilterOption { } class SCHomePartyPage extends StatefulWidget { - const SCHomePartyPage({super.key}); - - @override - State createState() => _HomePartyPageState(); -} - + const SCHomePartyPage({super.key}); + + @override + State createState() => _HomePartyPageState(); +} + class _HomePartyPageState extends State with SingleTickerProviderStateMixin, @@ -67,8 +67,8 @@ class _HomePartyPageState extends State bool isLoading = false; bool _isBannerLoading = false; bool _isLeaderboardLoading = false; - final RefreshController _refreshController = RefreshController( - initialRefresh: false, + final RefreshController _refreshController = RefreshController( + initialRefresh: false, ); late final AnimationController _skeletonController; List rooms = []; @@ -121,63 +121,63 @@ class _HomePartyPageState extends State } _wasTickerModeEnabled = isTickerModeEnabled; } - + @override Widget build(BuildContext context) { super.build(context); return Scaffold( - resizeToAvoidBottomInset: false, - backgroundColor: Colors.transparent, - body: Stack( - alignment: AlignmentDirectional.bottomCenter, - children: [ - SmartRefresher( - enablePullDown: true, - enablePullUp: false, - controller: _refreshController, - onRefresh: () { + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + body: Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + SmartRefresher( + enablePullDown: true, + enablePullUp: false, + controller: _refreshController, + onRefresh: () { loadData(forceRefresh: true); - }, - onLoading: () {}, - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Consumer( - builder: (context, ref, child) { - if (!_shouldShowBannerSection(ref)) { - return const SizedBox.shrink(); - } - return Padding( - padding: EdgeInsets.only(bottom: 12.w), - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - if (_shouldShowBannerSkeleton(ref)) - _buildBannerSkeleton() - else - _banner(ref), - ], - ), - ); - }, - ), - Consumer( - builder: (context, ref, child) { - if (_shouldShowLeaderboardSkeleton(ref)) { - return _buildLeaderboardSkeleton(); - } - return _banner2(ref); - }, - ), - _buildRoomsSection(), - ], - ), - ), - ), - ], - ), - ); + }, + onLoading: () {}, + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Consumer( + builder: (context, ref, child) { + if (!_shouldShowBannerSection(ref)) { + return const SizedBox.shrink(); + } + return Padding( + padding: EdgeInsets.only(bottom: 12.w), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + if (_shouldShowBannerSkeleton(ref)) + _buildBannerSkeleton() + else + _banner(ref), + ], + ), + ); + }, + ), + Consumer( + builder: (context, ref, child) { + if (_shouldShowLeaderboardSkeleton(ref)) { + return _buildLeaderboardSkeleton(); + } + return _banner2(ref); + }, + ), + _buildRoomsSection(), + ], + ), + ), + ), + ], + ), + ); } void _startRoomListAutoRefresh() { @@ -218,8 +218,7 @@ class _HomePartyPageState extends State previous.roomCover == next.roomCover && previous.roomGameIcon == next.roomGameIcon && previous.displayMemberCount == next.displayMemberCount && - previous.extValues?.roomSetting?.password == - next.extValues?.roomSetting?.password; + previous.extValues?.existsPassword == next.extValues?.existsPassword; } List _mergeLatestRoomsWithCurrentCounters( @@ -414,299 +413,299 @@ class _HomePartyPageState extends State _isSilentRefreshingRooms = false; } } - - _banner(SCAppGeneralManager ref) { - final banners = _partyBanners(ref); - final hasMultipleBanners = banners.length > 1; - if (banners.isEmpty) { - return Container(); - } - return Stack( - alignment: AlignmentDirectional.bottomCenter, - children: [ - CarouselSlider( - options: CarouselOptions( - height: 95.w, - autoPlay: hasMultipleBanners, - // 启用自动播放 - enlargeCenterPage: false, - // 居中放大当前页面 - enableInfiniteScroll: hasMultipleBanners, - // 启用无限循环 - autoPlayAnimationDuration: Duration(milliseconds: 800), - // 自动播放动画时长 - viewportFraction: 1, - scrollPhysics: - hasMultipleBanners - ? const BouncingScrollPhysics() - : const NeverScrollableScrollPhysics(), - // 视口分数 - onPageChanged: (index, reason) { - _currentIndex = index; - setState(() {}); - }, - ), - items: - banners.map((item) { - return GestureDetector( - child: netImage( - url: item.cover ?? "", - height: 95.w, - width: ScreenUtil().screenWidth * 0.9, - fit: BoxFit.fill, - borderRadius: BorderRadius.circular(12.w), - ), - onTap: () { - SCBannerUtils.openBanner(item, context); - }, - ); - }).toList(), - ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: - banners.asMap().entries.map((entry) { - return Container( - width: 6.0, - height: 6.0, - margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: - _currentIndex == entry.key ? Colors.blue : Colors.white, - ), - ); - }).toList(), - ), - ], - ); - } - - List _partyBanners(SCAppGeneralManager ref) { - if (ref.exploreBanners.isNotEmpty) { - return ref.exploreBanners; - } - return ref.homeBanners; - } - - _banner2(SCAppGeneralManager ref) { - String roomGiftOneAvatar = ""; - String roomGiftTwoAvatar = ""; - String roomGiftThreeAvatar = ""; - if (ref.appLeaderResult?.roomGiftsLeaderboard != null && - ref.appLeaderResult?.roomGiftsLeaderboard?.weekly != null) { - if ((ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?.isNotEmpty ?? - false) && - ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?.first != null) { - roomGiftOneAvatar = - ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?.first.avatar ?? - ""; - } - - if (ref.appLeaderResult!.roomGiftsLeaderboard!.weekly!.length > 1 && - ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?[1] != null) { - roomGiftTwoAvatar = - ref.appLeaderResult?.roomGiftsLeaderboard?.weekly![1].avatar ?? ""; - } - if (ref.appLeaderResult!.roomGiftsLeaderboard!.weekly!.length > 2 && - ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?[2] != null) { - roomGiftThreeAvatar = - ref.appLeaderResult?.roomGiftsLeaderboard?.weekly![2].avatar ?? ""; - } - } - - String wealthOneAvatar = ""; - String wealthTwoAvatar = ""; - String wealthThreeAvatar = ""; - - if (ref.appLeaderResult?.giftsSendLeaderboard != null && - ref.appLeaderResult?.giftsSendLeaderboard?.weekly != null) { - if ((ref.appLeaderResult?.giftsSendLeaderboard?.weekly?.isNotEmpty ?? - false) && - ref.appLeaderResult?.giftsSendLeaderboard?.weekly?.first != null) { - wealthOneAvatar = - ref.appLeaderResult?.giftsSendLeaderboard?.weekly?.first.avatar ?? - ""; - } - - if (ref.appLeaderResult!.giftsSendLeaderboard!.weekly!.length > 1 && - ref.appLeaderResult?.giftsSendLeaderboard?.weekly?[1] != null) { - wealthTwoAvatar = - ref.appLeaderResult?.giftsSendLeaderboard?.weekly![1].avatar ?? ""; - } - if (ref.appLeaderResult!.giftsSendLeaderboard!.weekly!.length > 2 && - ref.appLeaderResult?.giftsSendLeaderboard?.weekly?[2] != null) { - wealthThreeAvatar = - ref.appLeaderResult?.giftsSendLeaderboard?.weekly![2].avatar ?? ""; - } - } - - String charmOneAvatar = ""; - String charmTwoAvatar = ""; - String charmThreeAvatar = ""; - - if (ref.appLeaderResult?.giftsReceivedLeaderboard != null && - ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly != null) { - if ((ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?.isNotEmpty ?? - false) && - ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?.first != - null) { - charmOneAvatar = - ref - .appLeaderResult - ?.giftsReceivedLeaderboard - ?.weekly - ?.first - .avatar ?? - ""; - } - - if (ref.appLeaderResult!.giftsReceivedLeaderboard!.weekly!.length > 1 && - ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?[1] != null) { - charmTwoAvatar = - ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly![1].avatar ?? - ""; - } - if (ref.appLeaderResult!.giftsReceivedLeaderboard!.weekly!.length > 2 && - ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?[2] != null) { - charmThreeAvatar = - ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly![2].avatar ?? - ""; - } - } - - if (roomGiftOneAvatar.isEmpty && - wealthOneAvatar.isEmpty && - charmOneAvatar.isEmpty) { - return Container(); - } - return SizedBox( - height: 130.w, - child: Row( - children: [ - Expanded( - child: GestureDetector( - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - PositionedDirectional( - start: 26.w, - top: 65.w, - child: head(url: wealthTwoAvatar, width: 30.w), - ), - Positioned( - top: 35.w, - child: head(url: wealthOneAvatar, width: 36.w), - ), - PositionedDirectional( - end: 24.w, - top: 65.w, - child: head(url: wealthThreeAvatar, width: 30.w), - ), - Image.asset( - SCGlobalConfig.businessLogicStrategy - .getPopularLeaderboardBackgroundImage('wealth'), - fit: BoxFit.fill, - ), - ], - ), - onTap: () { - SCNavigatorUtils.push( - context, - "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.wealthRankUrl)}&showTitle=false", - replace: false, - ); - }, - ), - ), - Expanded( - child: GestureDetector( - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - PositionedDirectional( - start: 24.w, - top: 68.w, - child: head(url: roomGiftTwoAvatar, width: 30.w), - ), - Positioned( - top: 35.w, - child: head(url: roomGiftOneAvatar, width: 38.w), - ), - PositionedDirectional( - end: 25.w, - top: 69.w, - child: head(url: roomGiftThreeAvatar, width: 30.w), - ), - Image.asset( - SCGlobalConfig.businessLogicStrategy - .getPopularLeaderboardBackgroundImage('room'), - fit: BoxFit.fill, - ), - ], - ), - onTap: () { - SCNavigatorUtils.push( - context, - "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.roomRankUrl)}&showTitle=false", - replace: false, - ); - }, - ), - ), - Expanded( - child: GestureDetector( - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - PositionedDirectional( - start: 26.w, - top: 66.w, - child: head(url: charmTwoAvatar, width: 30.w), - ), - Positioned( - top: 38.w, - child: head(url: charmOneAvatar, width: 37.w), - ), - PositionedDirectional( - end: 25.w, - top: 66.w, - child: head(url: charmThreeAvatar, width: 30.w), - ), - Image.asset( - SCGlobalConfig.businessLogicStrategy - .getPopularLeaderboardBackgroundImage('charm'), - fit: BoxFit.fill, - ), - ], - ), - onTap: () { - SCNavigatorUtils.push( - context, - "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.charmRankUrl)}&showTitle=false", - replace: false, - ); - }, - ), - ), - ], - ), - ); - } - + + _banner(SCAppGeneralManager ref) { + final banners = _partyBanners(ref); + final hasMultipleBanners = banners.length > 1; + if (banners.isEmpty) { + return Container(); + } + return Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + CarouselSlider( + options: CarouselOptions( + height: 95.w, + autoPlay: hasMultipleBanners, + // 启用自动播放 + enlargeCenterPage: false, + // 居中放大当前页面 + enableInfiniteScroll: hasMultipleBanners, + // 启用无限循环 + autoPlayAnimationDuration: Duration(milliseconds: 800), + // 自动播放动画时长 + viewportFraction: 1, + scrollPhysics: + hasMultipleBanners + ? const BouncingScrollPhysics() + : const NeverScrollableScrollPhysics(), + // 视口分数 + onPageChanged: (index, reason) { + _currentIndex = index; + setState(() {}); + }, + ), + items: + banners.map((item) { + return GestureDetector( + child: netImage( + url: item.cover ?? "", + height: 95.w, + width: ScreenUtil().screenWidth * 0.9, + fit: BoxFit.fill, + borderRadius: BorderRadius.circular(12.w), + ), + onTap: () { + SCBannerUtils.openBanner(item, context); + }, + ); + }).toList(), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: + banners.asMap().entries.map((entry) { + return Container( + width: 6.0, + height: 6.0, + margin: EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + _currentIndex == entry.key ? Colors.blue : Colors.white, + ), + ); + }).toList(), + ), + ], + ); + } + + List _partyBanners(SCAppGeneralManager ref) { + if (ref.exploreBanners.isNotEmpty) { + return ref.exploreBanners; + } + return ref.homeBanners; + } + + _banner2(SCAppGeneralManager ref) { + String roomGiftOneAvatar = ""; + String roomGiftTwoAvatar = ""; + String roomGiftThreeAvatar = ""; + if (ref.appLeaderResult?.roomGiftsLeaderboard != null && + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly != null) { + if ((ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?.isNotEmpty ?? + false) && + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?.first != null) { + roomGiftOneAvatar = + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?.first.avatar ?? + ""; + } + + if (ref.appLeaderResult!.roomGiftsLeaderboard!.weekly!.length > 1 && + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?[1] != null) { + roomGiftTwoAvatar = + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly![1].avatar ?? ""; + } + if (ref.appLeaderResult!.roomGiftsLeaderboard!.weekly!.length > 2 && + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly?[2] != null) { + roomGiftThreeAvatar = + ref.appLeaderResult?.roomGiftsLeaderboard?.weekly![2].avatar ?? ""; + } + } + + String wealthOneAvatar = ""; + String wealthTwoAvatar = ""; + String wealthThreeAvatar = ""; + + if (ref.appLeaderResult?.giftsSendLeaderboard != null && + ref.appLeaderResult?.giftsSendLeaderboard?.weekly != null) { + if ((ref.appLeaderResult?.giftsSendLeaderboard?.weekly?.isNotEmpty ?? + false) && + ref.appLeaderResult?.giftsSendLeaderboard?.weekly?.first != null) { + wealthOneAvatar = + ref.appLeaderResult?.giftsSendLeaderboard?.weekly?.first.avatar ?? + ""; + } + + if (ref.appLeaderResult!.giftsSendLeaderboard!.weekly!.length > 1 && + ref.appLeaderResult?.giftsSendLeaderboard?.weekly?[1] != null) { + wealthTwoAvatar = + ref.appLeaderResult?.giftsSendLeaderboard?.weekly![1].avatar ?? ""; + } + if (ref.appLeaderResult!.giftsSendLeaderboard!.weekly!.length > 2 && + ref.appLeaderResult?.giftsSendLeaderboard?.weekly?[2] != null) { + wealthThreeAvatar = + ref.appLeaderResult?.giftsSendLeaderboard?.weekly![2].avatar ?? ""; + } + } + + String charmOneAvatar = ""; + String charmTwoAvatar = ""; + String charmThreeAvatar = ""; + + if (ref.appLeaderResult?.giftsReceivedLeaderboard != null && + ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly != null) { + if ((ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?.isNotEmpty ?? + false) && + ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?.first != + null) { + charmOneAvatar = + ref + .appLeaderResult + ?.giftsReceivedLeaderboard + ?.weekly + ?.first + .avatar ?? + ""; + } + + if (ref.appLeaderResult!.giftsReceivedLeaderboard!.weekly!.length > 1 && + ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?[1] != null) { + charmTwoAvatar = + ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly![1].avatar ?? + ""; + } + if (ref.appLeaderResult!.giftsReceivedLeaderboard!.weekly!.length > 2 && + ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly?[2] != null) { + charmThreeAvatar = + ref.appLeaderResult?.giftsReceivedLeaderboard?.weekly![2].avatar ?? + ""; + } + } + + if (roomGiftOneAvatar.isEmpty && + wealthOneAvatar.isEmpty && + charmOneAvatar.isEmpty) { + return Container(); + } + return SizedBox( + height: 130.w, + child: Row( + children: [ + Expanded( + child: GestureDetector( + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + PositionedDirectional( + start: 26.w, + top: 65.w, + child: head(url: wealthTwoAvatar, width: 30.w), + ), + Positioned( + top: 35.w, + child: head(url: wealthOneAvatar, width: 36.w), + ), + PositionedDirectional( + end: 24.w, + top: 65.w, + child: head(url: wealthThreeAvatar, width: 30.w), + ), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getPopularLeaderboardBackgroundImage('wealth'), + fit: BoxFit.fill, + ), + ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.wealthRankUrl)}&showTitle=false", + replace: false, + ); + }, + ), + ), + Expanded( + child: GestureDetector( + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + PositionedDirectional( + start: 24.w, + top: 68.w, + child: head(url: roomGiftTwoAvatar, width: 30.w), + ), + Positioned( + top: 35.w, + child: head(url: roomGiftOneAvatar, width: 38.w), + ), + PositionedDirectional( + end: 25.w, + top: 69.w, + child: head(url: roomGiftThreeAvatar, width: 30.w), + ), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getPopularLeaderboardBackgroundImage('room'), + fit: BoxFit.fill, + ), + ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.roomRankUrl)}&showTitle=false", + replace: false, + ); + }, + ), + ), + Expanded( + child: GestureDetector( + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + PositionedDirectional( + start: 26.w, + top: 66.w, + child: head(url: charmTwoAvatar, width: 30.w), + ), + Positioned( + top: 38.w, + child: head(url: charmOneAvatar, width: 37.w), + ), + PositionedDirectional( + end: 25.w, + top: 66.w, + child: head(url: charmThreeAvatar, width: 30.w), + ), + Image.asset( + SCGlobalConfig.businessLogicStrategy + .getPopularLeaderboardBackgroundImage('charm'), + fit: BoxFit.fill, + ), + ], + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.webViewPage}?url=${Uri.encodeComponent(SCGlobalConfig.charmRankUrl)}&showTitle=false", + replace: false, + ); + }, + ), + ), + ], + ), + ); + } + Widget _buildRoomsSection() { if (!_hasCompletedInitialRoomLoad && isLoading && rooms.isEmpty) { return _buildRoomGridSkeleton(); } - if (rooms.isEmpty) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - loadData(); - }, - child: mainEmpty(msg: SCAppLocalizations.of(context)!.noData), - ); - } + if (rooms.isEmpty) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + loadData(); + }, + child: mainEmpty(msg: SCAppLocalizations.of(context)!.noData), + ); + } final countryOptions = _roomCountryFilterOptions(); final visibleRooms = _filteredRooms(); final countrySelector = _buildCountryFilterSelector(countryOptions); @@ -888,334 +887,334 @@ class _HomePartyPageState extends State } bool _shouldShowLeaderboardSkeleton(SCAppGeneralManager ref) { - return _isLeaderboardLoading && !_hasLeaderboardData(ref); - } - - bool _shouldShowBannerSection(SCAppGeneralManager ref) { - return _shouldShowBannerSkeleton(ref) || _partyBanners(ref).isNotEmpty; - } - - bool _shouldShowBannerSkeleton(SCAppGeneralManager ref) { - return _isBannerLoading && _partyBanners(ref).isEmpty; - } - - bool _hasLeaderboardData(SCAppGeneralManager ref) { - final appLeaderResult = ref.appLeaderResult; - return (appLeaderResult?.giftsSendLeaderboard?.weekly?.isNotEmpty ?? - false) || - (appLeaderResult?.roomGiftsLeaderboard?.weekly?.isNotEmpty ?? false) || - (appLeaderResult?.giftsReceivedLeaderboard?.weekly?.isNotEmpty ?? - false); - } - - Widget _buildBannerSkeleton() { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 18.w), - child: SizedBox( - height: 95.w, - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - Positioned.fill( - child: DecoratedBox( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(12.w), - border: Border.all(color: _kPartySkeletonBorder, width: 1.w), - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [_kPartySkeletonShellStrong, _kPartySkeletonShell], - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.14), - blurRadius: 18.w, - offset: Offset(0, 8.w), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(12.w), - child: Stack( - children: [ - Positioned.fill( - child: Opacity( - opacity: 0.45, - child: _buildSkeletonPanel(), - ), - ), - ], - ), - ), - ), - ), - Positioned( - bottom: 10.w, - child: Row( - children: List.generate(3, (index) { - return Padding( - padding: EdgeInsets.symmetric(horizontal: 4.w), - child: _buildSkeletonBone( - width: 6.w, - height: 6.w, - shape: BoxShape.circle, - ), - ); - }), - ), - ), - ], - ), - ), - ); - } - - Widget _buildLeaderboardSkeleton() { - return SizedBox( - height: 130.w, - child: Row( - children: List.generate(3, (index) { - final bool isCenterCard = index == 1; - return Expanded( - child: Padding( - padding: EdgeInsets.fromLTRB( - 4.w, - isCenterCard ? 0 : 6.w, - 4.w, - isCenterCard ? 0 : 6.w, - ), - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(18.w), - border: Border.all(color: _kPartySkeletonBorder, width: 1.w), - gradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [_kPartySkeletonShellStrong, _kPartySkeletonShell], - ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.18), - blurRadius: 14.w, - offset: Offset(0, 8.w), - ), - ], - ), - child: Stack( - alignment: Alignment.center, - children: [ - Positioned.fill( - child: Padding( - padding: EdgeInsets.all(1.w), - child: ClipRRect( - borderRadius: BorderRadius.circular(17.w), - child: Opacity( - opacity: 0.34, - child: _buildSkeletonPanel(), - ), - ), - ), - ), - Positioned( - top: 21.w, - child: _buildSkeletonBone( - width: isCenterCard ? 42.w : 38.w, - height: isCenterCard ? 42.w : 38.w, - shape: BoxShape.circle, - ), - ), - PositionedDirectional( - start: 16.w, - top: 56.w, - child: _buildSkeletonBone( - width: 28.w, - height: 28.w, - shape: BoxShape.circle, - ), - ), - PositionedDirectional( - end: 16.w, - top: 56.w, - child: _buildSkeletonBone( - width: 28.w, - height: 28.w, - shape: BoxShape.circle, - ), - ), - Positioned( - bottom: 16.w, - child: _buildSkeletonBone( - width: isCenterCard ? 56.w : 48.w, - height: 10.w, - borderRadius: BorderRadius.circular(999.w), - ), - ), - ], - ), - ), - ), - ); - }), - ), - ); - } - - Widget _buildRoomGridSkeleton() { - return GridView.builder( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - childAspectRatio: 1, - mainAxisSpacing: 10, - crossAxisSpacing: 10, - ), - padding: EdgeInsets.all(10.w), - itemCount: 6, - itemBuilder: (context, index) => _buildRoomSkeletonCard(), - ); - } - - Widget _buildRoomSkeletonCard() { - return Container( - margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(14.w), - border: Border.all(color: _kPartySkeletonBorder, width: 1.w), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.16), - blurRadius: 16.w, - offset: Offset(0, 8.w), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(14.w), - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - Positioned.fill( - child: DecoratedBox( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [_kPartySkeletonShellStrong, _kPartySkeletonShell], - ), - ), - ), - ), - Positioned.fill( - child: Padding( - padding: EdgeInsets.only(bottom: 34.w), - child: _buildSkeletonPanel( - borderRadius: BorderRadius.vertical( - top: Radius.circular(14.w), - ), - ), - ), - ), - Positioned( - top: 12.w, - right: 12.w, - child: _buildSkeletonBone( - width: 30.w, - height: 12.w, - borderRadius: BorderRadius.circular(999.w), - ), - ), - Positioned( - left: 0, - right: 0, - bottom: 0, - child: Container( - height: 34.w, - padding: EdgeInsets.symmetric(horizontal: 10.w), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.24), - border: Border( - top: BorderSide( - color: Colors.white.withValues(alpha: 0.05), - width: 1.w, - ), - ), - ), - child: Row( - children: [ - _buildSkeletonBone( - width: 20.w, - height: 13.w, - borderRadius: BorderRadius.circular(3.w), - ), - SizedBox(width: 6.w), - Expanded( - child: _buildSkeletonBone( - height: 10.w, - borderRadius: BorderRadius.circular(999.w), - ), - ), - SizedBox(width: 8.w), - _buildSkeletonBone( - width: 18.w, - height: 10.w, - borderRadius: BorderRadius.circular(999.w), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildSkeletonPanel({ - BorderRadius? borderRadius, - BoxShape shape = BoxShape.rectangle, - }) { - return AnimatedBuilder( - animation: _skeletonController, - builder: (context, child) { - final double slideOffset = (_skeletonController.value * 2) - 1; - return DecoratedBox( - decoration: BoxDecoration( - shape: shape, - borderRadius: - shape == BoxShape.circle - ? null - : (borderRadius ?? BorderRadius.circular(12.w)), - gradient: LinearGradient( - begin: Alignment(-1.4 + slideOffset, -0.2), - end: Alignment(1.4 + slideOffset, 0.2), - colors: const [ - _kPartySkeletonBoneBase, - _kPartySkeletonBoneBase, - _kPartySkeletonBoneHighlight, - _kPartySkeletonBoneBase, - ], - stops: const [0.0, 0.36, 0.54, 1.0], - ), - ), - ); - }, - ); - } - - Widget _buildSkeletonBone({ - double? width, - double? height, - BorderRadius? borderRadius, - BoxShape shape = BoxShape.rectangle, - }) { - return SizedBox( - width: width, - height: height, - child: _buildSkeletonPanel(borderRadius: borderRadius, shape: shape), - ); - } - + return _isLeaderboardLoading && !_hasLeaderboardData(ref); + } + + bool _shouldShowBannerSection(SCAppGeneralManager ref) { + return _shouldShowBannerSkeleton(ref) || _partyBanners(ref).isNotEmpty; + } + + bool _shouldShowBannerSkeleton(SCAppGeneralManager ref) { + return _isBannerLoading && _partyBanners(ref).isEmpty; + } + + bool _hasLeaderboardData(SCAppGeneralManager ref) { + final appLeaderResult = ref.appLeaderResult; + return (appLeaderResult?.giftsSendLeaderboard?.weekly?.isNotEmpty ?? + false) || + (appLeaderResult?.roomGiftsLeaderboard?.weekly?.isNotEmpty ?? false) || + (appLeaderResult?.giftsReceivedLeaderboard?.weekly?.isNotEmpty ?? + false); + } + + Widget _buildBannerSkeleton() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 18.w), + child: SizedBox( + height: 95.w, + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12.w), + border: Border.all(color: _kPartySkeletonBorder, width: 1.w), + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [_kPartySkeletonShellStrong, _kPartySkeletonShell], + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.14), + blurRadius: 18.w, + offset: Offset(0, 8.w), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(12.w), + child: Stack( + children: [ + Positioned.fill( + child: Opacity( + opacity: 0.45, + child: _buildSkeletonPanel(), + ), + ), + ], + ), + ), + ), + ), + Positioned( + bottom: 10.w, + child: Row( + children: List.generate(3, (index) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 4.w), + child: _buildSkeletonBone( + width: 6.w, + height: 6.w, + shape: BoxShape.circle, + ), + ); + }), + ), + ), + ], + ), + ), + ); + } + + Widget _buildLeaderboardSkeleton() { + return SizedBox( + height: 130.w, + child: Row( + children: List.generate(3, (index) { + final bool isCenterCard = index == 1; + return Expanded( + child: Padding( + padding: EdgeInsets.fromLTRB( + 4.w, + isCenterCard ? 0 : 6.w, + 4.w, + isCenterCard ? 0 : 6.w, + ), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(18.w), + border: Border.all(color: _kPartySkeletonBorder, width: 1.w), + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [_kPartySkeletonShellStrong, _kPartySkeletonShell], + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.18), + blurRadius: 14.w, + offset: Offset(0, 8.w), + ), + ], + ), + child: Stack( + alignment: Alignment.center, + children: [ + Positioned.fill( + child: Padding( + padding: EdgeInsets.all(1.w), + child: ClipRRect( + borderRadius: BorderRadius.circular(17.w), + child: Opacity( + opacity: 0.34, + child: _buildSkeletonPanel(), + ), + ), + ), + ), + Positioned( + top: 21.w, + child: _buildSkeletonBone( + width: isCenterCard ? 42.w : 38.w, + height: isCenterCard ? 42.w : 38.w, + shape: BoxShape.circle, + ), + ), + PositionedDirectional( + start: 16.w, + top: 56.w, + child: _buildSkeletonBone( + width: 28.w, + height: 28.w, + shape: BoxShape.circle, + ), + ), + PositionedDirectional( + end: 16.w, + top: 56.w, + child: _buildSkeletonBone( + width: 28.w, + height: 28.w, + shape: BoxShape.circle, + ), + ), + Positioned( + bottom: 16.w, + child: _buildSkeletonBone( + width: isCenterCard ? 56.w : 48.w, + height: 10.w, + borderRadius: BorderRadius.circular(999.w), + ), + ), + ], + ), + ), + ), + ); + }), + ), + ); + } + + Widget _buildRoomGridSkeleton() { + return GridView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + childAspectRatio: 1, + mainAxisSpacing: 10, + crossAxisSpacing: 10, + ), + padding: EdgeInsets.all(10.w), + itemCount: 6, + itemBuilder: (context, index) => _buildRoomSkeletonCard(), + ); + } + + Widget _buildRoomSkeletonCard() { + return Container( + margin: EdgeInsets.symmetric(horizontal: 5.w, vertical: 5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14.w), + border: Border.all(color: _kPartySkeletonBorder, width: 1.w), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.16), + blurRadius: 16.w, + offset: Offset(0, 8.w), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(14.w), + child: Stack( + alignment: Alignment.bottomCenter, + children: [ + Positioned.fill( + child: DecoratedBox( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [_kPartySkeletonShellStrong, _kPartySkeletonShell], + ), + ), + ), + ), + Positioned.fill( + child: Padding( + padding: EdgeInsets.only(bottom: 34.w), + child: _buildSkeletonPanel( + borderRadius: BorderRadius.vertical( + top: Radius.circular(14.w), + ), + ), + ), + ), + Positioned( + top: 12.w, + right: 12.w, + child: _buildSkeletonBone( + width: 30.w, + height: 12.w, + borderRadius: BorderRadius.circular(999.w), + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + height: 34.w, + padding: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.24), + border: Border( + top: BorderSide( + color: Colors.white.withValues(alpha: 0.05), + width: 1.w, + ), + ), + ), + child: Row( + children: [ + _buildSkeletonBone( + width: 20.w, + height: 13.w, + borderRadius: BorderRadius.circular(3.w), + ), + SizedBox(width: 6.w), + Expanded( + child: _buildSkeletonBone( + height: 10.w, + borderRadius: BorderRadius.circular(999.w), + ), + ), + SizedBox(width: 8.w), + _buildSkeletonBone( + width: 18.w, + height: 10.w, + borderRadius: BorderRadius.circular(999.w), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + Widget _buildSkeletonPanel({ + BorderRadius? borderRadius, + BoxShape shape = BoxShape.rectangle, + }) { + return AnimatedBuilder( + animation: _skeletonController, + builder: (context, child) { + final double slideOffset = (_skeletonController.value * 2) - 1; + return DecoratedBox( + decoration: BoxDecoration( + shape: shape, + borderRadius: + shape == BoxShape.circle + ? null + : (borderRadius ?? BorderRadius.circular(12.w)), + gradient: LinearGradient( + begin: Alignment(-1.4 + slideOffset, -0.2), + end: Alignment(1.4 + slideOffset, 0.2), + colors: const [ + _kPartySkeletonBoneBase, + _kPartySkeletonBoneBase, + _kPartySkeletonBoneHighlight, + _kPartySkeletonBoneBase, + ], + stops: const [0.0, 0.36, 0.54, 1.0], + ), + ), + ); + }, + ); + } + + Widget _buildSkeletonBone({ + double? width, + double? height, + BorderRadius? borderRadius, + BoxShape shape = BoxShape.rectangle, + }) { + return SizedBox( + width: width, + height: height, + child: _buildSkeletonPanel(borderRadius: borderRadius, shape: shape), + ); + } + loadData({bool forceRefresh = false}) { final generalManager = Provider.of( context, @@ -1249,24 +1248,24 @@ class _HomePartyPageState extends State _hasCompletedInitialRoomLoad = true; if (mounted) setState(() {}); }); - generalManager.loadMainBanner().whenComplete(() { - if (!mounted) { - return; - } - setState(() { - _isBannerLoading = false; - }); - }); - generalManager.appLeaderboard().whenComplete(() { - if (!mounted) { - return; - } - setState(() { - _isLeaderboardLoading = false; - }); - }); - } - + generalManager.loadMainBanner().whenComplete(() { + if (!mounted) { + return; + } + setState(() { + _isBannerLoading = false; + }); + }); + generalManager.appLeaderboard().whenComplete(() { + if (!mounted) { + return; + } + setState(() { + _isLeaderboardLoading = false; + }); + }); + } + _buildItem(SocialChatRoomRes res, int index) { final rankBorderAsset = index < _topRankBorderAssets.length @@ -1286,22 +1285,22 @@ class _HomePartyPageState extends State clipBehavior: Clip.none, alignment: Alignment.bottomCenter, children: [ - Container( - padding: EdgeInsets.all(3.w), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage("sc_images/index/sc_icon_room_bord.png"), - fit: BoxFit.fill, - ), - ), - child: netImage( - url: resolveRoomCoverUrl(res.id, res.roomCover), - defaultImg: kRoomCoverDefaultImg, - borderRadius: BorderRadius.circular(12.w), - width: 200.w, - height: 200.w, - ), - ), + Container( + padding: EdgeInsets.all(3.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage("sc_images/index/sc_icon_room_bord.png"), + fit: BoxFit.fill, + ), + ), + child: netImage( + url: resolveRoomCoverUrl(res.id, res.roomCover), + defaultImg: kRoomCoverDefaultImg, + borderRadius: BorderRadius.circular(12.w), + width: 200.w, + height: 200.w, + ), + ), Padding( padding: EdgeInsets.only( left: rankInfoHorizontalInset, @@ -1385,17 +1384,17 @@ class _HomePartyPageState extends State ), ), SizedBox(width: 5.w), - (res.extValues?.roomSetting?.password?.isEmpty ?? false) + res.extValues?.existsPassword != true ? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w) : Image.asset( "sc_images/index/sc_icon_room_suo.png", width: 20.w, height: 20.w, ), - (res.extValues?.roomSetting?.password?.isEmpty ?? false) + res.extValues?.existsPassword != true ? SizedBox(width: 3.w) : Container(height: 10.w), - (res.extValues?.roomSetting?.password?.isEmpty ?? false) + res.extValues?.existsPassword != true ? text( res.displayMemberCount, fontSize: 10.sp, @@ -1433,4 +1432,4 @@ class _HomePartyPageState extends State }, ); } -} +} diff --git a/lib/modules/room/edit/room_edit_page.dart b/lib/modules/room/edit/room_edit_page.dart index 27e3e5c..7966b26 100644 --- a/lib/modules/room/edit/room_edit_page.dart +++ b/lib/modules/room/edit/room_edit_page.dart @@ -1,23 +1,23 @@ 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/components/sc_compontent.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app_localizations.dart'; -import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; -import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/shared/tools/sc_pick_utils.dart'; import 'package:yumi/services/room/rc_room_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import '../../../shared/tools/sc_lk_dialog_util.dart'; import '../../../shared/data_sources/models/enum/sc_room_info_event_type.dart'; @@ -38,17 +38,25 @@ class RoomEditPage extends StatefulWidget { class _RoomEditPageState extends State { static const Set _roomGroupReadyCodes = {0, 10021, 10025}; + static const Color _backgroundColor = Color(0xff071E1D); + static const Color _fieldColor = Color(0xff08251E); + static const Color _fieldBorderColor = Color(0xffB2FBCC); + static const Color _dangerColor = Color(0xffff0000); RtcProvider? rtcProvider; bool isEdit = false; final TextEditingController _roomNameController = TextEditingController(); final TextEditingController _roomAnnouncementController = TextEditingController(); + final TextEditingController _roomLockPasswordController = + TextEditingController(); final int _roomNameMaxLength = 24; int _roomNameCurrentLength = 0; String roomCover = ""; - final int _roomAnnouncementMaxLength = 100; + final int _roomAnnouncementMaxLength = 24; int _roomAnnouncementCurrentLength = 0; + bool _initialRoomLocked = false; + bool _isRoomPrivate = false; @override void initState() { @@ -78,425 +86,602 @@ class _RoomEditPageState extends State { ); roomCover = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""; + final roomPassword = + rtcProvider?.currenRoom?.roomProfile?.roomSetting?.password?.trim() ?? + ""; + _initialRoomLocked = roomPassword.isNotEmpty; + _isRoomPrivate = _initialRoomLocked; } @override void dispose() { _roomNameController.dispose(); _roomAnnouncementController.dispose(); + _roomLockPasswordController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { - return WillPopScope( - onWillPop: () { - if (!isEdit) { - SCNavigatorUtils.goBack(context); - return Future.value(false); + return PopScope( + canPop: false, + onPopInvokedWithResult: (didPop, result) { + if (didPop) { + return; } - SmartDialog.show( - tag: "showConfirmDialog", - alignment: Alignment.center, - debounce: true, - animationType: SmartAnimationType.fade, - builder: (_) { - return MsgDialog( - title: SCAppLocalizations.of(context)!.tips, - msg: SCAppLocalizations.of(context)!.theModificationsMade, - onEnsure: () { - SCNavigatorUtils.goBack(context); - }, - ); - }, - ); - return Future.value(false); + _handleSystemBack(); }, - child: Stack( - children: [ - Image.asset( - "sc_images/person/sc_icon_edit_userinfo_bg.png", - width: ScreenUtil().screenWidth, - height: ScreenUtil().screenHeight, - fit: BoxFit.fill, + child: AnnotatedRegion( + value: SystemUiOverlayStyle.light, + child: Scaffold( + resizeToAvoidBottomInset: false, + backgroundColor: _backgroundColor, + body: _buildBody(context), + ), + ), + ); + } + + Widget _buildBody(BuildContext context) { + final mediaQuery = MediaQuery.of(context); + final bottomPadding = + mediaQuery.viewPadding.bottom > 34.w + ? 34.w + : mediaQuery.viewPadding.bottom; + return Column( + children: [ + SizedBox(height: mediaQuery.viewPadding.top), + _buildNavigationBar(context), + Expanded( + child: Stack( + children: [ + SingleChildScrollView( + keyboardDismissBehavior: + ScrollViewKeyboardDismissBehavior.onDrag, + padding: EdgeInsets.only(bottom: bottomPadding + 116.w), + child: _buildForm(context), + ), + Positioned( + left: 24.w, + right: 24.w, + bottom: bottomPadding + 14.w, + child: _buildSaveButton(context), + ), + ], ), - Scaffold( - resizeToAvoidBottomInset: false, - backgroundColor: Colors.transparent, - appBar: SocialChatStandardAppBar( - title: SCAppLocalizations.of(context)!.roomSetting, - onTag: () { - if (widget.needRestCurrentRoomInfo == "true") { - Provider.of(context, listen: false).currenRoom = - null; - } - Navigator.pop(context); - }, - actions: [ - GestureDetector( - behavior: HitTestBehavior.opaque, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 5.w), - margin: EdgeInsetsDirectional.only(end: 8.w), - child: text( - SCAppLocalizations.of(context)!.save, - textColor: Colors.white, - fontSize: 15.sp, + ), + ], + ); + } + + Widget _buildNavigationBar(BuildContext context) { + return SizedBox( + width: double.infinity, + height: 44.w, + child: Stack( + alignment: Alignment.center, + children: [ + Positioned( + left: 0, + top: 0, + bottom: 0, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: _handleBack, + child: SizedBox( + width: 56.w, + height: 44.w, + child: Padding( + padding: EdgeInsets.only(left: 16.w), + child: Align( + alignment: Alignment.centerLeft, + child: Icon( + Icons.keyboard_arrow_left, + size: 24.w, + color: Colors.white, ), ), - onTap: () { - submit(context); - }, ), - ], - ), - body: SingleChildScrollView( - child: Column( - children: [ - SizedBox(height: 35.w), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox(width: 20.w), - GestureDetector( - child: Stack( - alignment: Alignment.bottomRight, - children: [ - netImage( - url: roomCover, - defaultImg: kRoomCoverDefaultImg, - borderRadius: BorderRadius.all( - Radius.circular(12), - ), - width: 90.w, - height: 90.w, - ), - Icon( - Icons.camera_alt, - size: 25.w, - color: Colors.white, - ), - ], - ), - onTap: () { - SCPickUtils.pickImage(context, ( - bool success, - String url, - ) { - if (success) { - setState(() { - roomCover = url; - isEdit = true; - }); - } - }); - }, - ), - ], - ), - SizedBox(height: 25.w), - Row( - children: [ - SizedBox(width: 25.w), - text( - SCAppLocalizations.of(context)!.roomName, - textColor: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 15.sp, - ), - ], - ), - SizedBox(height: 10.w), - Row( - children: [ - Expanded( - child: Container( - margin: EdgeInsets.symmetric(horizontal: 25.w), - padding: EdgeInsets.only( - left: width(12), - right: width(12), - ), - alignment: AlignmentDirectional.centerStart, - height: 45.w, - width: ScreenUtil().screenWidth, - decoration: BoxDecoration( - border: Border.all( - color: Color(0xff5FFFB7), - width: 0.5, - ), - gradient: LinearGradient( - colors: [ - Color(0xff5FFFB7).withOpacity(0.1), - Color(0xff5FFFB7).withOpacity(0.6), - ], - ), - borderRadius: BorderRadius.all( - Radius.circular(height(8)), - ), - ), - child: TextField( - controller: _roomNameController, - onChanged: (text) { - isEdit = true; - }, - inputFormatters: [ - SCAccurateLengthLimitingTextInputFormatter( - _roomNameMaxLength, - ), - ], - decoration: InputDecoration( - hintText: - SCAppLocalizations.of(context)!.enterRoomName, - hintStyle: TextStyle( - color: Colors.white54, - fontSize: 14.sp, - ), - contentPadding: EdgeInsets.only(top: 0.w), - counterText: '', - isDense: true, - filled: false, - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - // enabledBorder: UnderlineInputBorder( - // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), - // focusedBorder: UnderlineInputBorder( - // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), - // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), - fillColor: Colors.white54, - suffix: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 3.w), - text( - '$_roomNameCurrentLength/$_roomNameMaxLength', - textColor: Colors.white54, - ), - ], - ), - ), - style: TextStyle( - fontSize: sp(15), - color: Colors.white, - textBaseline: TextBaseline.alphabetic, - ), - ), - ), - ), - ], - ), - SizedBox(height: 15.w), - Row( - children: [ - SizedBox(width: 25.w), - text( - SCAppLocalizations.of(context)!.roomAnnouncement, - textColor: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 15.sp, - ), - ], - ), - SizedBox(height: 10.w), - Row( - children: [ - Expanded( - child: Container( - margin: EdgeInsets.symmetric(horizontal: 25.w), - padding: EdgeInsets.only( - top: 5.w, - left: width(12), - right: width(12), - ), - alignment: AlignmentDirectional.centerStart, - height: 110.w, - width: ScreenUtil().screenWidth, - decoration: BoxDecoration( - border: Border.all( - color: Color(0xff5FFFB7), - width: 0.5, - ), - gradient: LinearGradient( - colors: [ - Color(0xff5FFFB7).withOpacity(0.1), - Color(0xff5FFFB7).withOpacity(0.6), - ], - ), - borderRadius: BorderRadius.all( - Radius.circular(height(8)), - ), - ), - child: TextField( - controller: _roomAnnouncementController, - onChanged: (text) { - isEdit = true; - }, - inputFormatters: [ - SCAccurateLengthLimitingTextInputFormatter( - _roomAnnouncementMaxLength, - ), - ], - maxLines: 5, - decoration: InputDecoration( - hintText: "", - hintStyle: TextStyle( - color: Colors.white54, - fontSize: 14.sp, - ), - contentPadding: EdgeInsets.only(top: 0.w), - counterText: '', - isDense: true, - filled: false, - focusColor: Colors.transparent, - hoverColor: Colors.transparent, - border: InputBorder.none, - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - disabledBorder: InputBorder.none, - errorBorder: InputBorder.none, - focusedErrorBorder: InputBorder.none, - // enabledBorder: UnderlineInputBorder( - // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), - // focusedBorder: UnderlineInputBorder( - // borderSide: BorderSide(width: 0.5,color: Colors.white,style: BorderStyle.solid),), - // prefixIcon: Padding(padding: EdgeInsets.all(8.w), child: Image.asset("images/login/sc_icon_phone.png",width: 20.w, height: 20.w,fit: BoxFit.fill,),), - fillColor: Colors.white54, - suffix: Row( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox(width: 3.w), - text( - '$_roomAnnouncementCurrentLength/$_roomAnnouncementMaxLength', - textColor: Colors.white54, - ), - ], - ), - ), - style: TextStyle( - fontSize: sp(15), - color: Colors.white, - textBaseline: TextBaseline.alphabetic, - ), - ), - ), - ), - ], - ), - widget.needRestCurrentRoomInfo != "true" - ? Column( - children: [ - SizedBox(height: 12.w), - SCDebounceWidget( - child: Row( - children: [ - SizedBox(width: 25.w), - text( - SCAppLocalizations.of(context)!.roomTheme2, - textColor: Colors.white, - fontWeight: FontWeight.w500, - fontSize: 15.sp, - ), - Spacer(), - Icon( - Icons.keyboard_arrow_right, - color: Colors.white, - size: 20.w, - ), - SizedBox(width: 25.w), - ], - ), - onTap: () { - VoiceRoomRoute.openRoomTheme(context); - }, - ), - SizedBox(height: 12.w), - SCDebounceWidget( - child: Row( - children: [ - SizedBox(width: 25.w), - text( - SCAppLocalizations.of(context)!.micManagement, - textColor: Colors.white, - fontWeight: FontWeight.w500, - fontSize: 15.sp, - ), - Spacer(), - Icon( - Icons.keyboard_arrow_right, - color: Colors.white, - size: 20.w, - ), - SizedBox(width: 25.w), - ], - ), - onTap: () { - SmartDialog.show( - tag: "showRoomMicSwitch", - alignment: Alignment.bottomCenter, - animationType: SmartAnimationType.fade, - debounce: true, - builder: (_) { - return RoomMicSwitchPage(); - }, - ); - }, - ), - SizedBox(height: 12.w), - SCDebounceWidget( - child: Row( - children: [ - SizedBox(width: 25.w), - text( - SCAppLocalizations.of(context)!.blockedList2, - textColor: Colors.white, - fontWeight: FontWeight.w500, - fontSize: 15.sp, - ), - Spacer(), - Icon( - Icons.keyboard_arrow_right, - color: Colors.white, - size: 20.w, - ), - SizedBox(width: 25.w), - ], - ), - onTap: () { - showBottomInBottomDialog( - context, - BlockedListPage( - roomId: - Provider.of( - context, - listen: false, - ) - .currenRoom - ?.roomProfile - ?.roomProfile - ?.id ?? - "", - ), - ); - }, - ), - SizedBox(height: 15.w), - ], - ) - : Container(), - ], ), ), ), + Text( + "Room Setup", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 18.sp, + fontWeight: FontWeight.w500, + height: 1, + ), + ), ], ), ); } + Widget _buildForm(BuildContext context) { + return Column( + children: [ + SizedBox(height: 16.w), + _buildRoomAvatar(), + SizedBox(height: 20.w), + _buildTextInput( + controller: _roomNameController, + height: 40.w, + hintText: "Please enter a name", + counterText: '$_roomNameCurrentLength/$_roomNameMaxLength', + inputFormatters: [ + SCAccurateLengthLimitingTextInputFormatter(_roomNameMaxLength), + ], + onChanged: (_) { + isEdit = true; + }, + ), + SizedBox(height: 16.w), + _buildSectionHeader( + title: "Notice", + counterText: + '$_roomAnnouncementCurrentLength/$_roomAnnouncementMaxLength', + ), + SizedBox(height: 12.w), + _buildTextInput( + controller: _roomAnnouncementController, + height: 135.w, + hintText: "Please enter a name", + maxLines: null, + expands: true, + inputFormatters: [ + SCAccurateLengthLimitingTextInputFormatter( + _roomAnnouncementMaxLength, + ), + ], + onChanged: (_) { + isEdit = true; + }, + ), + SizedBox(height: 16.w), + _buildLockHeader(), + SizedBox(height: 8.w), + _buildTextInput( + controller: _roomLockPasswordController, + height: 40.w, + hintText: "Please enter password", + keyboardType: TextInputType.visiblePassword, + obscureText: true, + onChanged: (_) { + isEdit = true; + }, + ), + SizedBox(height: 8.w), + _buildLockTips(), + if (widget.needRestCurrentRoomInfo != "true") ...[ + SizedBox(height: 16.w), + _buildSettingRow( + icon: Icons.dashboard_rounded, + title: SCAppLocalizations.of(context)!.roomTheme2, + onTap: () { + VoiceRoomRoute.openRoomTheme(context); + }, + ), + SizedBox(height: 8.w), + _buildSettingRow( + icon: Icons.mic_none_rounded, + title: "Microphone Settings", + onTap: () { + SmartDialog.show( + tag: "showRoomMicSwitch", + alignment: Alignment.bottomCenter, + animationType: SmartAnimationType.fade, + debounce: true, + builder: (_) { + return RoomMicSwitchPage(); + }, + ); + }, + ), + SizedBox(height: 8.w), + _buildSettingRow( + icon: Icons.person_off_rounded, + title: "Block List", + onTap: () { + showBottomInBottomDialog( + context, + BlockedListPage( + roomId: + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id ?? + "", + ), + ); + }, + ), + ], + ], + ); + } + + Widget _buildRoomAvatar() { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + SCPickUtils.pickImage(context, (bool success, String url) { + if (success) { + setState(() { + roomCover = url; + isEdit = true; + }); + } + }); + }, + child: ClipOval( + child: netImage( + url: roomCover, + defaultImg: kRoomCoverDefaultImg, + width: 90.w, + height: 90.w, + fit: BoxFit.cover, + shape: BoxShape.circle, + ), + ), + ); + } + + Widget _buildTextInput({ + required TextEditingController controller, + required double height, + required String hintText, + String? counterText, + int? maxLines = 1, + bool expands = false, + bool obscureText = false, + TextInputType? keyboardType, + List? inputFormatters, + ValueChanged? onChanged, + }) { + final isMultiline = maxLines == null || expands; + return Container( + height: height, + margin: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + color: _fieldColor, + borderRadius: BorderRadius.circular(8.w), + border: Border.all(color: _fieldBorderColor, width: 1), + ), + child: Stack( + alignment: isMultiline ? Alignment.topLeft : Alignment.centerLeft, + children: [ + Padding( + padding: EdgeInsets.only( + left: 12.w, + right: counterText == null ? 12.w : 56.w, + top: isMultiline ? 10.w : 0, + bottom: isMultiline ? 10.w : 0, + ), + child: TextField( + controller: controller, + cursorColor: SocialChatTheme.primaryColor, + keyboardType: keyboardType, + obscureText: obscureText, + inputFormatters: inputFormatters, + maxLines: maxLines, + expands: expands, + onChanged: onChanged, + textAlignVertical: + isMultiline + ? TextAlignVertical.top + : TextAlignVertical.center, + decoration: InputDecoration( + hintText: hintText, + hintStyle: TextStyle( + color: Colors.white, + 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, + counterText: '', + isDense: true, + isCollapsed: !isMultiline, + contentPadding: EdgeInsets.zero, + ), + style: TextStyle( + color: Colors.white, + backgroundColor: Colors.transparent, + fontSize: 14.sp, + fontWeight: FontWeight.w400, + height: 16 / 14, + ), + ), + ), + if (counterText != null) + PositionedDirectional( + end: 12.w, + top: 0, + bottom: 0, + child: Center( + child: Text( + counterText, + textAlign: TextAlign.right, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.6), + fontSize: 14.sp, + fontWeight: FontWeight.w400, + height: 16 / 14, + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildSectionHeader({required String title, String? counterText}) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: SizedBox( + height: 16.w, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + title, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w500, + height: 1, + ), + ), + const Spacer(), + if (counterText != null) + Text( + counterText, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.6), + fontSize: 12.sp, + fontWeight: FontWeight.w400, + height: 1, + ), + ), + ], + ), + ), + ); + } + + Widget _buildLockHeader() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: SizedBox( + height: 20.w, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "Room lock", + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + fontWeight: FontWeight.w500, + height: 1, + ), + ), + const Spacer(), + _buildLockOption( + selected: !_isRoomPrivate, + label: "Public", + onTap: () { + setState(() { + if (_isRoomPrivate) { + isEdit = true; + } + _isRoomPrivate = false; + }); + }, + ), + SizedBox(width: 8.w), + _buildLockOption( + selected: _isRoomPrivate, + label: "Private", + onTap: () { + setState(() { + if (!_isRoomPrivate) { + isEdit = true; + } + _isRoomPrivate = true; + }); + }, + ), + ], + ), + ), + ); + } + + Widget _buildLockOption({ + required bool selected, + required String label, + required VoidCallback onTap, + }) { + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: SizedBox( + height: 20.w, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + width: 20.w, + height: 20.w, + child: Center( + child: Container( + width: 14.w, + height: 14.w, + decoration: BoxDecoration( + color: + selected + ? SocialChatTheme.primaryColor + : Colors.transparent, + borderRadius: BorderRadius.circular(2.w), + border: Border.all( + color: SocialChatTheme.primaryColor, + width: 1.w, + ), + ), + child: + selected + ? Icon(Icons.check, color: Colors.white, size: 11.w) + : null, + ), + ), + ), + SizedBox(width: 4.w), + Text( + label, + style: TextStyle( + color: + selected + ? Colors.white + : Colors.white.withValues(alpha: 0.6), + fontSize: 12.sp, + fontWeight: FontWeight.w400, + height: 1, + ), + ), + ], + ), + ), + ); + } + + Widget _buildLockTips() { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: SizedBox( + width: double.infinity, + child: Text( + "After locking the door, enter the correct password to enter the room.", + maxLines: 2, + style: TextStyle( + color: _dangerColor, + fontSize: 14.sp, + fontWeight: FontWeight.w400, + height: 16 / 14, + ), + ), + ), + ); + } + + Widget _buildSettingRow({ + required IconData icon, + required String title, + required VoidCallback onTap, + }) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 10.w), + child: SCDebounceWidget( + onTap: onTap, + child: Container( + height: 40.w, + decoration: BoxDecoration( + color: _fieldColor, + borderRadius: BorderRadius.circular(8.w), + border: Border.all(color: _fieldBorderColor, width: 1), + ), + child: Row( + children: [ + SizedBox(width: 8.w), + Icon(icon, color: Colors.white, size: 24.w), + SizedBox(width: 4.w), + Expanded( + child: Text( + title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: Colors.white, + fontSize: 14.sp, + fontWeight: FontWeight.w400, + height: 16 / 14, + ), + ), + ), + Icon(Icons.keyboard_arrow_right, color: Colors.white, size: 24.w), + SizedBox(width: 8.w), + ], + ), + ), + ), + ); + } + + Widget _buildSaveButton(BuildContext context) { + return SCDebounceWidget( + onTap: () { + submit(context); + }, + child: Container( + height: 44.w, + alignment: Alignment.center, + decoration: BoxDecoration( + color: SocialChatTheme.primaryColor, + borderRadius: BorderRadius.circular(22.w), + ), + child: Text( + SCAppLocalizations.of(context)!.save, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.black, + fontSize: 20.sp, + fontWeight: FontWeight.w500, + height: 1, + ), + ), + ), + ); + } + + void _handleBack() { + if (widget.needRestCurrentRoomInfo == "true") { + Provider.of(context, listen: false).currenRoom = null; + } + Navigator.pop(context); + } + + void _handleSystemBack() { + if (!isEdit) { + SCNavigatorUtils.goBack(context); + return; + } + SmartDialog.show( + tag: "showConfirmDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return MsgDialog( + title: SCAppLocalizations.of(context)!.tips, + msg: SCAppLocalizations.of(context)!.theModificationsMade, + onEnsure: () { + SCNavigatorUtils.goBack(context); + }, + ); + }, + ); + } + int _getActualCharacterCount(String text) { return text.characters.length; } @@ -518,6 +703,8 @@ class _RoomEditPageState extends State { final submittedRoomName = _roomNameController.text.trim(); final submittedRoomDesc = _roomAnnouncementController.text.trim(); final submittedRoomCover = roomCover.trim(); + final submittedRoomPassword = _roomLockPasswordController.text.trim(); + final roomId = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? ""; if (submittedRoomName.isEmpty) { SCTts.show("Room name not empty!"); return; @@ -526,14 +713,21 @@ class _RoomEditPageState extends State { SCTts.show("Room announcement not empty!"); return; } + if (_isRoomPrivate && + (!_initialRoomLocked || submittedRoomPassword.isNotEmpty) && + submittedRoomPassword.isEmpty) { + SCTts.show("Please enter password"); + return; + } SCLoadingManager.show(context: context); var roomInfo = await SCAccountRepository().editRoomInfo( - rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", + roomId, submittedRoomCover, submittedRoomName, submittedRoomDesc, SCRoomInfoEventType.AVAILABLE.name, ); + await _submitRoomLockSetting(roomId, submittedRoomPassword); if (!context.mounted) { SCLoadingManager.hide(); return; @@ -594,6 +788,7 @@ class _RoomEditPageState extends State { roomName: mergedRoomInfo.roomName ?? submittedRoomName, roomDesc: mergedRoomInfo.roomDesc ?? submittedRoomDesc, roomSetting: rtcProvider?.currenRoom?.roomProfile?.roomSetting, + existsPassword: _isRoomPrivate, ownerProfile: rtcProvider?.currenRoom?.roomProfile?.userProfile, roomAdminCount: rtcProvider?.currenRoom?.roomProfile?.roomCounter?.adminCount, @@ -616,4 +811,20 @@ class _RoomEditPageState extends State { SCNavigatorUtils.goBack(context); } } + + Future _submitRoomLockSetting(String roomId, String password) async { + final normalizedRoomId = roomId.trim(); + if (normalizedRoomId.isEmpty) { + return; + } + if (_isRoomPrivate) { + if (!_initialRoomLocked || password.isNotEmpty) { + await SCChatRoomRepository().roomLocked(normalizedRoomId, password); + } + } else if (_initialRoomLocked) { + await SCChatRoomRepository().roomUnlocked(normalizedRoomId); + } + _initialRoomLocked = _isRoomPrivate; + _roomLockPasswordController.clear(); + } } diff --git a/lib/modules/room/online/room_online_page.dart b/lib/modules/room/online/room_online_page.dart index 1b14cc4..0c51636 100644 --- a/lib/modules/room/online/room_online_page.dart +++ b/lib/modules/room/online/room_online_page.dart @@ -43,7 +43,7 @@ class _RoomOnlinePageState if (dialogContext == null) { return; } - showBottomInCenterDialog(dialogContext, RoomUserInfoCard(userId: userId)); + showBottomInBottomDialog(dialogContext, RoomUserInfoCard(userId: userId)); }); } diff --git a/lib/modules/room/rank/room_gift_rank_tab_page.dart b/lib/modules/room/rank/room_gift_rank_tab_page.dart index b52901f..36e9f2a 100644 --- a/lib/modules/room/rank/room_gift_rank_tab_page.dart +++ b/lib/modules/room/rank/room_gift_rank_tab_page.dart @@ -98,7 +98,7 @@ class _RoomGiftRankTabPageState // headdress: userInfo.userProfile?.getHeaddress()?.sourceUrl, ), onTap: () { - showBottomInCenterDialog( + showBottomInBottomDialog( navigatorKey.currentState!.context, RoomUserInfoCard(userId: userProfile?.id), ); diff --git a/lib/modules/room/rocket/room_rocket_rank_page.dart b/lib/modules/room/rocket/room_rocket_rank_page.dart index 0ffc19f..b220fef 100644 --- a/lib/modules/room/rocket/room_rocket_rank_page.dart +++ b/lib/modules/room/rocket/room_rocket_rank_page.dart @@ -6,8 +6,13 @@ import 'package:flutter/services.dart'; import 'package:provider/provider.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/local/floating_screen_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/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 { const RoomRocketRankPage({ @@ -66,18 +71,25 @@ class RoomRocketRankEntry { } class _RoomRocketRankPageState extends State { + static const String _effectSuppressReason = 'room_rocket_rank'; + static const int _levelCount = 6; + late int _selectedLevel; final Map> _loadedRanks = {}; final Map _loadedRoundNos = {}; final Map _loadedCurrentUsers = {}; final Set _loadingLevels = {}; - - static const int _levelCount = 6; + RtcProvider? _suppressedRtcProvider; + String _suppressedRoomId = ''; + bool _roomEffectsSuppressed = false; + bool _restoreRoomVisualEffects = false; + bool _restoreVoiceRoomRouteVisible = false; @override void initState() { super.initState(); _selectedLevel = widget.initialLevel.clamp(0, _levelCount - 1); + _suppressRoomEffects(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _loadLevel(_selectedLevel); @@ -85,6 +97,12 @@ class _RoomRocketRankPageState extends State { }); } + @override + void dispose() { + _releaseRoomEffects(); + super.dispose(); + } + @override Widget build(BuildContext context) { return AnnotatedRegion( @@ -110,6 +128,9 @@ class _RoomRocketRankPageState extends State { selectedLevel: _selectedLevel, showRound2Today: widget.showRound2Today, roundNo: _loadedRoundNos[_selectedLevel] ?? 0, + isLoading: + widget.levelRanks == null && + _loadingLevels.contains(_selectedLevel), ranksByLevel: _normalizedRanks, currentUser: widget.currentUser ?? @@ -128,6 +149,61 @@ class _RoomRocketRankPageState extends State { ); } + void _suppressRoomEffects() { + if (_roomEffectsSuppressed) { + return; + } + _roomEffectsSuppressed = true; + try { + final rtcProvider = Provider.of(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> get _normalizedRanks { final providedRanks = widget.levelRanks; if (providedRanks == null) { @@ -178,7 +254,9 @@ class _RoomRocketRankPageState extends State { _rocketRankDebug('skip king request because roomId is empty'); return; } - _loadingLevels.add(index); + setState(() { + _loadingLevels.add(index); + }); _rocketRankDebug('request king roomId=$roomId level=${index + 1}'); SCChatRoomRepository() .roomRocketKing(roomId, level: index + 1) @@ -205,7 +283,13 @@ class _RoomRocketRankPageState extends State { }); }) .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.showRound2Today, required this.roundNo, + required this.isLoading, required this.ranksByLevel, required this.currentUser, required this.onLevelChanged, @@ -323,6 +408,7 @@ class _RocketRankCanvas extends StatelessWidget { final int selectedLevel; final bool showRound2Today; final int roundNo; + final bool isLoading; final List> ranksByLevel; final RoomRocketRankEntry currentUser; final ValueChanged onLevelChanged; @@ -360,7 +446,9 @@ class _RocketRankCanvas extends StatelessWidget { onChanged: onLevelChanged, ), ), - if (entries.isEmpty) + if (isLoading) + const _RocketRankLoadingState() + else if (entries.isEmpty) const _RocketRankEmptyState() else _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 { const _RocketRankEmptyState(); @@ -1093,5 +1198,5 @@ class _RocketRankAssets { 'sc_images/room/rocket/rocket_rank_frame_2.png'; static const String rankThreeFrame = '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'; } diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index d018ffa..942920a 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -209,6 +209,7 @@ class _VoiceRoomPageState extends State void _ensureRoomVisualEffectsEnabled() { final rtcProvider = Provider.of(context, listen: false); if (rtcProvider.currenRoom == null || + !_roomRouteVisible || rtcProvider.roomVisualEffectsEnabled) { return; } diff --git a/lib/modules/search/sc_search_page.dart b/lib/modules/search/sc_search_page.dart index 8cbb05a..fd6c350 100644 --- a/lib/modules/search/sc_search_page.dart +++ b/lib/modules/search/sc_search_page.dart @@ -535,17 +535,17 @@ class _SearchRoomListState extends State { ), ), SizedBox(width: 5.w), - (e.extValues?.roomSetting?.password?.isEmpty ?? false) + e.extValues?.existsPassword != true ? SCRoomLiveAudioIndicator(width: 14.w, height: 14.w) : Image.asset( "sc_images/index/sc_icon_room_suo.png", width: 20.w, height: 20.w, ), - (e.extValues?.roomSetting?.password?.isEmpty ?? false) + e.extValues?.existsPassword != true ? SizedBox(width: 3.w) : Container(height: 10.w), - (e.extValues?.roomSetting?.password?.isEmpty ?? false) + e.extValues?.existsPassword != true ? text( e.displayMemberCount, fontSize: 10.sp, diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index 081879d..2e7b39c 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -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/auth/user_profile_manager.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart'; import '../../../app/constants/sc_screen.dart'; import '../../../shared/business_logic/models/res/sc_user_counter_res.dart'; @@ -393,13 +394,20 @@ class _PersonDetailPageState extends State { ); } - Widget _buildHeaderBadgePlaceholder() { - return SizedBox( - height: 24.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: const [], - ), + Widget _buildHeaderBadgeStrip(SocialChatUserProfileManager ref) { + return SCUserBadgeStrip( + userId: ref.userProfile?.id ?? widget.tageId, + fallbackBadges: + ref.userProfile?.wearBadge + ?.where((item) => item.use ?? false) + .toList() ?? + const [], + height: 26.w, + badgeHeight: 24.w, + longBadgeWidth: 58.w, + spacing: 6.w, + maxBadges: 8, + reserveSpace: true, ); } @@ -612,7 +620,7 @@ class _PersonDetailPageState extends State { SizedBox(height: 5.w), _buildHeaderMeta(ref), SizedBox(height: 4.w), - _buildHeaderBadgePlaceholder(), + _buildHeaderBadgeStrip(ref), SizedBox(height: 6.w), _buildHeaderIntroduction(ref), SizedBox(height: 12.w), diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 88598fa..91a4a6c 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -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/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_password_dialog.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_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/tools/sc_room_profile_cache.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_reward_dialog.dart'; +import '../../ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart'; +import '../../ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart'; typedef OnSoundVoiceChange = Function(num index, int volum); typedef RtcProvider = RealTimeCommunicationManager; @@ -120,6 +121,7 @@ class RoomEntryPreviewData { this.updateTime, this.activeTime, this.roomSetting, + this.existsPassword, this.ownerProfile, this.roomAdminCount, this.roomMemberCount, @@ -141,6 +143,7 @@ class RoomEntryPreviewData { countryName: room.countryName, nationalFlag: room.nationalFlag, roomSetting: room.extValues?.roomSetting, + existsPassword: room.extValues?.existsPassword == true, ownerProfile: room.userProfile, roomAdminCount: room.roomCounter?.adminCount, roomMemberCount: room.roomCounter?.memberCount, @@ -163,6 +166,7 @@ class RoomEntryPreviewData { countryName: roomProfile?.countryName, nationalFlag: roomProfile?.nationalFlag, roomSetting: roomProfile?.extValues?.roomSetting, + existsPassword: roomProfile?.extValues?.existsPassword == true, ownerProfile: roomProfile?.userProfile, roomAdminCount: roomProfile?.roomCounter?.adminCount, roomMemberCount: roomProfile?.roomCounter?.memberCount, @@ -192,6 +196,7 @@ class RoomEntryPreviewData { updateTime: room.updateTime, activeTime: room.activeTime, roomSetting: room.setting, + existsPassword: (room.setting?.password ?? "").trim().isNotEmpty, ownerProfile: ownerProfile, roomAdminCount: room.counter?.adminCount, roomMemberCount: room.counter?.memberCount, @@ -217,6 +222,7 @@ class RoomEntryPreviewData { final num? updateTime; final num? activeTime; final RoomSetting? roomSetting; + final bool? existsPassword; final SocialChatUserProfile? ownerProfile; final num? roomAdminCount; final num? roomMemberCount; @@ -292,6 +298,9 @@ class _PendingRoomStartupSeatAction { class RealTimeCommunicationManager extends ChangeNotifier { 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 _onlineUsersPollingInterval = Duration(seconds: 3); @@ -2122,6 +2131,9 @@ class RealTimeCommunicationManager extends ChangeNotifier { SCTts.show('Microphone permission is denied.'); throw ArgumentError('Microphone permission is denied.'); } + if (!context.mounted) { + return; + } if (roomId == currenRoom?.roomProfile?.roomProfile?.id) { if (_currentRoomIsEntryPreview || _roomStartupStatus == RoomStartupStatus.loading) { @@ -2164,6 +2176,17 @@ class RealTimeCommunicationManager extends ChangeNotifier { _startVoiceRoomForegroundService(); VoiceRoomRoute.openVoiceRoom(context); } 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) { _switchAwayFromCurrentRoomForNewEntry(); } @@ -2186,15 +2209,26 @@ class RealTimeCommunicationManager extends ChangeNotifier { VoiceRoomRoute.openVoiceRoom(context); notifyListeners(); final entryRequestSerial = ++_roomEntryRequestSerial; - unawaited( - _enterAndInitializeVoiceRoomSession( - roomId, - entryRequestSerial: entryRequestSerial, - pwd: pwd, - needOpenRedenvelope: needOpenRedenvelope, - redPackId: redPackId, - ), - ); + if (preEnteredRoom != null) { + unawaited( + _completeEnteredVoiceRoomSession( + preEnteredRoom, + entryRequestSerial: entryRequestSerial, + needOpenRedenvelope: needOpenRedenvelope, + redPackId: redPackId, + ), + ); + } else { + unawaited( + _enterAndInitializeVoiceRoomSession( + roomId, + entryRequestSerial: entryRequestSerial, + pwd: pwd, + needOpenRedenvelope: needOpenRedenvelope, + redPackId: redPackId, + ), + ); + } } } @@ -2212,32 +2246,167 @@ class RealTimeCommunicationManager extends ChangeNotifier { redPackId: redPackId, needOpenRedenvelope: needOpenRedenvelope, ); - if (!_isActiveRoomEntryRequest(entryRequestSerial)) { - unawaited(_cleanupStaleEnteredRoom(enteredRoom)); - return; - } - currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom); - _syncCurrentUserVipHintFromRoom(currenRoom); - _startVoiceRoomForegroundService(); - unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial)); - _currentRoomIsEntryPreview = false; - _previewRoomSeatCount = null; - notifyListeners(); - await initializeRoomSession( + await _completeEnteredVoiceRoomSession( + enteredRoom, entryRequestSerial: entryRequestSerial, needOpenRedenvelope: needOpenRedenvelope, redPackId: redPackId, - shouldOpenRoomPage: false, ); - } catch (e, stackTrace) { + } catch (e, _) { if (!_isActiveRoomEntryRequest(entryRequestSerial)) { 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(); _setRoomStartupFailed(RoomStartupFailureType.entry); } } + Future _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( + context, + listen: false, + ).userIdentity; + if (userIdentity?.superAdmin == true || + userIdentity?.admin == true || + userIdentity?.yumiManager == true) { + return false; + } + return true; + } + + Future _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 enteredRoom, JoinRoomRes? previewRoom, @@ -2914,13 +3083,9 @@ class RealTimeCommunicationManager extends ChangeNotifier { maskColor: Colors.transparent, clickMaskDismiss: false, builder: - (_) => RoomRocketRewardDialog( - rewards: RoomRocketApiMapper.rewardItemsFromRecords( - res.records, - ), - winners: RoomRocketApiMapper.winnersFromRecords(res.records), - loadingText: '', - useMockFallback: false, + (_) => RoomRocketRewardDialogLoader( + roomId: roomId, + initialPopupRecords: res.records, onClose: () { SmartDialog.dismiss(tag: _roomRocketRewardDialogTag); if (recordIds.isNotEmpty) { @@ -2971,11 +3136,21 @@ class RealTimeCommunicationManager extends ChangeNotifier { _roomRocketLaunchAnimationTimer?.cancel(); if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible) { - // TODO(rocket-launch): 发射动画素材还没有,资源到位后在这里接入 - // SVGA/VAP/PNG 序列播放。这里先保留完整触发入口和时长控制, - // 避免后续接接口时再改 RTM 分发逻辑。 + final animationUrl = + _firstNonBlankText([ + launch.rocketAnimationUrl, + _rocketLaunchAnimationUrlForLevel(launch.safeLevel), + ]) ?? + ""; + if (animationUrl.isNotEmpty) { + _playRoomRocketLaunchAnimation( + animationUrl: animationUrl, + launch: launch, + ); + } _roomRocketLaunchAnimationTimer = Timer(launch.displayDuration, () { _roomRocketLaunchAnimationTimer = null; + SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); }); return; @@ -2984,6 +3159,68 @@ class RealTimeCommunicationManager extends ChangeNotifier { 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 fetchOnlineUsersList({bool notifyIfUnchanged = true}) async { final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; @@ -3539,7 +3776,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (clickUser.id == AccountStorage().getCurrentUser()?.userProfile?.id) { ///是自己,直接打开资料卡 - showBottomInCenterDialog( + showBottomInBottomDialog( context!, RoomUserInfoCard(userId: clickUser.id), ); @@ -3649,7 +3886,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { if (normalizedUserId.isEmpty) { return; } - showBottomInCenterDialog( + showBottomInBottomDialog( context!, RoomUserInfoCard(userId: normalizedUserId), ); diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index 2cc9ef8..6770751 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -54,14 +54,14 @@ import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push hide Data; 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_room_rocket_api_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/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_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/rocket/room_rocket_api_mapper.dart'; -import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart'; +import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart'; 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/services/gift/gift_system_manager.dart'; @@ -119,6 +119,7 @@ class RealTimeMessagingManager extends ChangeNotifier { static const int _luckyGiftBurstPlaybackWatchdogMs = 8000; BuildContext? context; + final Set _sentRoomRocketRewardMessageKeys = {}; void _giftFxLog(String message) {} @@ -1103,6 +1104,8 @@ class RealTimeMessagingManager extends ChangeNotifier { return true; case SCRoomMsgType.roomRedPacketClaim: return true; + case SCRoomMsgType.rocketRewardUser: + return true; case SCRoomMsgType.emoticons: return (msg.number ?? -1) < 0; default: @@ -1132,6 +1135,10 @@ class RealTimeMessagingManager extends ChangeNotifier { _hasRoomRedPacketClaimMessage(msg)) { return; } + if (msg.type == SCRoomMsgType.rocketRewardUser && + _hasRoomRocketRewardMessage(msg)) { + return; + } roomAllMsgList.insert(0, msg); if (roomAllMsgList.length > 250) { @@ -1164,6 +1171,12 @@ class RealTimeMessagingManager extends ChangeNotifier { roomChatMsgList.removeAt(roomChatMsgList.length - 1); } 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 || msg.type == SCRoomMsgType.luckGiftAnimOther) { roomGiftMsgList.insert(0, msg); @@ -1271,6 +1284,32 @@ class RealTimeMessagingManager extends ChangeNotifier { 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) { if (incoming.type != SCRoomMsgType.gift && incoming.type != SCRoomMsgType.luckGiftAnimOther) { @@ -1825,7 +1864,6 @@ class RealTimeMessagingManager extends ChangeNotifier { (rtcProvider.currenRoom?.roomProfile?.roomProfile?.id ?? '').trim(); if (currentRoomId == launch.roomId) { rtcProvider.handleRoomRocketLaunchBroadcast(launch); - return; } } @@ -1835,19 +1873,259 @@ class RealTimeMessagingManager extends ChangeNotifier { void _handleVoiceRoomRocketRewardPopup(dynamic payload) { final data = _broadcastPayloadMap(payload); 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 currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ''; if (currentUserId.isEmpty || !userIds.contains(currentUserId)) { return; } - final currentRoomId = _currentVoiceRoomId(); - if (roomId.isNotEmpty && - currentRoomId.isNotEmpty && - roomId != currentRoomId) { + unawaited(_showRoomRocketRewardPopupDialog(resolvedRoomId)); + } + + Future _sendRoomRocketRewardPublicMessages( + String roomId, + Map payload, + ) async { + final groupId = _currentVoiceRoomGroupId(); + if (groupId.isEmpty) { 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 _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> _loadRocketRewardPublicRecords( + String roomId, + String launchNo, + ) async { + if (roomId.trim().isEmpty || launchNo.trim().isEmpty) { + return const []; + } + 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 []; + } + } + + List _rocketRewardRecordsFromPayload( + Map 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 []; + } + + 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 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 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 _rocketRewardUserPayload( + Map 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 {'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 _showRoomRocketRewardPopupDialog(String roomId) async { @@ -1856,6 +2134,11 @@ class RealTimeMessagingManager extends ChangeNotifier { if (res.records.isEmpty) { return; } + unawaited( + _sendRoomRocketRewardPublicMessages(roomId, { + 'records': res.records.map((item) => item.toJson()).toList(), + }), + ); final recordIds = res.records .map((item) => item.id) .where((item) => item.isNotEmpty) @@ -1869,11 +2152,9 @@ class RealTimeMessagingManager extends ChangeNotifier { maskColor: Colors.transparent, clickMaskDismiss: false, builder: - (_) => RoomRocketRewardDialog( - rewards: RoomRocketApiMapper.rewardItemsFromRecords(res.records), - winners: RoomRocketApiMapper.winnersFromRecords(res.records), - loadingText: '', - useMockFallback: false, + (_) => RoomRocketRewardDialogLoader( + roomId: roomId, + initialPopupRecords: res.records, onClose: () { SmartDialog.dismiss(tag: _roomRocketRewardDialogTag); if (recordIds.isNotEmpty) { @@ -1894,13 +2175,18 @@ class RealTimeMessagingManager extends ChangeNotifier { type: 3, roomId: launch.roomId, rocketLevel: launch.safeLevel, - userAvatarUrl: launch.triggerUser.avatar, - userName: launch.triggerUser.nickname, + userAvatarUrl: _firstNonBlank([ + launch.roomCoverUrl, + launch.triggerUser.avatar, + ]), + userName: _firstNonBlank([launch.roomName, launch.roomAccount]), userId: _firstNonBlank([ launch.triggerUser.account, launch.triggerUser.userId, launch.triggerUserId, ]), + giftUrl: launch.rocketIconUrl, + fallbackUrl: launch.rocketIconUrl, giftId: launch.launchNo, durationSeconds: launch.durationSeconds, priority: 1000, @@ -2074,13 +2360,35 @@ class RealTimeMessagingManager extends ChangeNotifier { } else if (type == "ROCKET_ENERGY_LAUNCH") { ///火箭触发飘屏 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( type: 3, roomId: fdata["roomId"], rocketLevel: fdata["fromLevel"], - userAvatarUrl: fdata["userAvatar"], - userName: fdata["nickname"], + userAvatarUrl: _firstNonBlank([ + _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"], + giftUrl: rocketIconUrl, + fallbackUrl: rocketIconUrl, priority: 1000, ); OverlayManager().addMessage(msg); diff --git a/lib/services/general/sc_app_general_manager.dart b/lib/services/general/sc_app_general_manager.dart index e229272..bd69f00 100644 --- a/lib/services/general/sc_app_general_manager.dart +++ b/lib/services/general/sc_app_general_manager.dart @@ -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/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/gift_backpack_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_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'; class SCAppGeneralManager extends ChangeNotifier { + static const String backpackGiftTab = 'backpack'; static const int _maxInitialFullScreenGiftPreloads = 4; List countryModeList = []; @@ -32,9 +34,12 @@ class SCAppGeneralManager extends ChangeNotifier { final Set _warmingGiftCoverUrls = {}; bool _isFetchingGiftList = false; bool _hasGiftListLoaded = false; + bool _isFetchingGiftBackpack = false; + bool _hasGiftBackpackLoaded = false; Map> giftByTab = {}; Map> activityGiftByTab = {}; + List giftBackpackResList = []; ///选中的国家 Country? selectCountryInfo; @@ -51,6 +56,13 @@ class SCAppGeneralManager extends ChangeNotifier { bool get isGiftListLoading => _isFetchingGiftList && !_hasGiftListLoaded; + bool get isGiftBackpackLoading => + _isFetchingGiftBackpack && !_hasGiftBackpackLoaded; + + bool isGiftTabLoading(String type) { + return type == backpackGiftTab ? isGiftBackpackLoading : isGiftListLoading; + } + ///加载国家列表 Future fetchCountryList() async { if (_isFetchingCountryList) { @@ -203,6 +215,7 @@ class SCAppGeneralManager extends ChangeNotifier { } else { giftByTab.remove("CUSTOMIZED"); } + _mergeBackpackGiftTab(); } Future warmGiftPageCovers( @@ -270,12 +283,75 @@ class SCAppGeneralManager extends ChangeNotifier { } ///礼物背包 - void giftBackpack() async { - try { - await SCChatRoomRepository().giftBackpack(); - } catch (_) { + Future giftBackpack({bool forceRefresh = false}) async { + if (_isFetchingGiftBackpack) { 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 = []; + 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获取礼物 diff --git a/lib/shared/business_logic/models/res/gift_res.dart b/lib/shared/business_logic/models/res/gift_res.dart index 2cef234..ae63b1d 100644 --- a/lib/shared/business_logic/models/res/gift_res.dart +++ b/lib/shared/business_logic/models/res/gift_res.dart @@ -34,6 +34,8 @@ class SocialChatGiftRes { String? userId, String? giftName, String? quantity, + String? backpackId, + num? backpackQuantity, num? expiredTime, }) { _id = id; @@ -54,6 +56,8 @@ class SocialChatGiftRes { _userId = userId; _giftName = giftName; _quantity = quantity; + _backpackId = backpackId; + _backpackQuantity = backpackQuantity; _expiredTime = expiredTime; } @@ -123,6 +127,12 @@ class SocialChatGiftRes { _quantity = _giftResString( json['quantity'] ?? json['giftQuantity'] ?? json['count'] ?? json['num'], ); + _backpackId = _giftResString( + json['backpackId'] ?? json['backId'] ?? json['backpackRecordId'], + ); + _backpackQuantity = _giftResNum( + json['backpackQuantity'] ?? json['backpackCount'], + ); _expiredTime = _giftResNum( json['expiredTime'] ?? giftConfig?['expiredTime'], ); @@ -145,6 +155,8 @@ class SocialChatGiftRes { String? _userId; String? _giftName; String? _quantity; + String? _backpackId; + num? _backpackQuantity; num? _expiredTime; SocialChatGiftRes copyWith({ String? id, @@ -165,6 +177,8 @@ class SocialChatGiftRes { String? userId, String? giftName, String? quantity, + String? backpackId, + num? backpackQuantity, num? expiredTime, }) => SocialChatGiftRes( id: id ?? _id, @@ -185,6 +199,8 @@ class SocialChatGiftRes { userId: userId ?? _userId, giftName: giftName ?? _giftName, quantity: quantity ?? _quantity, + backpackId: backpackId ?? _backpackId, + backpackQuantity: backpackQuantity ?? _backpackQuantity, expiredTime: expiredTime ?? _expiredTime, ); String? get id => _id; @@ -205,6 +221,8 @@ class SocialChatGiftRes { String? get userId => _userId; String? get giftName => _giftName; String? get quantity => _quantity; + String? get backpackId => _backpackId; + num? get backpackQuantity => _backpackQuantity; num? get expiredTime => _expiredTime; Map toJson() { @@ -227,6 +245,8 @@ class SocialChatGiftRes { map['userId'] = _userId; map['giftName'] = _giftName; map['quantity'] = _quantity; + map['backpackId'] = _backpackId; + map['backpackQuantity'] = _backpackQuantity; map['expiredTime'] = _expiredTime; return map; } diff --git a/lib/shared/business_logic/models/res/login_res.dart b/lib/shared/business_logic/models/res/login_res.dart index 3718284..9109c50 100644 --- a/lib/shared/business_logic/models/res/login_res.dart +++ b/lib/shared/business_logic/models/res/login_res.dart @@ -878,20 +878,30 @@ class WearBadge { _userId = userId; } - WearBadge.fromJson(dynamic json) { - _animationUrl = json['animationUrl']; - _badgeKey = json['badgeKey']; - _badgeLevel = json['badgeLevel']; - _badgeName = json['badgeName']; - _expireTime = json['expireTime']; - _id = json['id']; - _use = json['use']; - _milestone = json['milestone']; - _notSelectUrl = json['notSelectUrl']; - _selectUrl = json['selectUrl']; - _type = json['type']; - _userId = json['userId']; - } + WearBadge.fromJson(dynamic json) { + if (json is! Map) { + return; + } + _animationUrl = _stringValue(json['animationUrl']); + _badgeKey = _stringValue(json['badgeKey']); + _badgeLevel = _numValue(json['badgeLevel'] ?? json['level']); + _badgeName = _stringValue( + json['badgeName'] ?? json['name'] ?? json['displayName'], + ); + _expireTime = _intValue(json['expireTime'] ?? json['expireAt']); + _id = _stringValue(json['id'] ?? json['badgeId']); + _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? _badgeKey; @@ -958,7 +968,7 @@ class WearBadge { String? get userId => _userId; - Map toJson() { + Map toJson() { final map = {}; map['animationUrl'] = _animationUrl; map['badgeKey'] = _badgeKey; @@ -972,9 +982,48 @@ class WearBadge { map['selectUrl'] = _selectUrl; map['type'] = _type; 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 { PersonPhoto({String? url, num? status}) { diff --git a/lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart b/lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart index c0a0fa7..5c70817 100644 --- a/lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart +++ b/lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart @@ -4,21 +4,192 @@ class SCRoomRocketLevelRes { required this.level, required this.needEnergy, required this.rocketIconUrl, + required this.rocketPreviewUrl, required this.rocketAnimationUrl, required this.progressBarUrl, + required this.shakeThresholdPercent, required this.enabled, required this.sort, }); factory SCRoomRocketLevelRes.fromJson(dynamic 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( id: _stringValue(data['id']), level: _intValue(data['level']), needEnergy: _intValue(data['needEnergy']), - rocketIconUrl: _stringValue(data['rocketIconUrl']), - rocketAnimationUrl: _stringValue(data['rocketAnimationUrl']), - progressBarUrl: _stringValue(data['progressBarUrl']), + rocketIconUrl: _firstAssetString([ + data['rocket_icon_url'], + 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), sort: _intValue(data['sort']), ); @@ -28,10 +199,27 @@ class SCRoomRocketLevelRes { final int level; final int needEnergy; final String rocketIconUrl; + final String rocketPreviewUrl; final String rocketAnimationUrl; final String progressBarUrl; + final num shakeThresholdPercent; final bool enabled; final int sort; + + Map toJson() { + return { + 'id': id, + 'level': level, + 'needEnergy': needEnergy, + 'rocketIconUrl': rocketIconUrl, + 'rocketPreviewUrl': rocketPreviewUrl, + 'rocketAnimationUrl': rocketAnimationUrl, + 'progressBarUrl': progressBarUrl, + 'shakeThresholdPercent': shakeThresholdPercent, + 'enabled': enabled, + 'sort': sort, + }; + } } class SCRoomRocketRewardRes { @@ -230,16 +418,36 @@ class SCRoomRocketKingRes { } class SCRoomRocketRewardPopupRes { - SCRoomRocketRewardPopupRes({required this.records}); + SCRoomRocketRewardPopupRes({ + required this.records, + required this.cursor, + required this.limit, + required this.emptyReason, + }); factory SCRoomRocketRewardPopupRes.fromJson(dynamic json) { final data = _asMap(json); return SCRoomRocketRewardPopupRes( records: SCRoomRocketRewardRecordRes.listFromJson(data['records']), + cursor: _intValue(data['cursor'], fallback: 1), + limit: _intValue(data['limit'], fallback: 10), + emptyReason: _stringValue(data['emptyReason']), ); } final List records; + final int cursor; + final int limit; + final String emptyReason; + + Map toJson() { + return { + "records": records.map((item) => item.toJson()).toList(growable: false), + "cursor": cursor, + "limit": limit, + "emptyReason": emptyReason, + }; + } } class SCRoomRocketRewardRecordsRes { @@ -255,7 +463,7 @@ class SCRoomRocketRewardRecordsRes { return SCRoomRocketRewardRecordsRes( records: SCRoomRocketRewardRecordRes.listFromJson(data['records']), cursor: _intValue(data['cursor'], fallback: 1), - limit: _intValue(data['limit'], fallback: 20), + limit: _intValue(data['limit'], fallback: 10), emptyReason: _stringValue(data['emptyReason']), ); } @@ -264,6 +472,15 @@ class SCRoomRocketRewardRecordsRes { final int cursor; final int limit; final String emptyReason; + + Map toJson() { + return { + "records": records.map((item) => item.toJson()).toList(growable: false), + "cursor": cursor, + "limit": limit, + "emptyReason": emptyReason, + }; + } } class SCRoomRocketRewardRecordRes { @@ -275,6 +492,9 @@ class SCRoomRocketRewardRecordRes { required this.roundNo, required this.level, required this.userId, + required this.userAvatar, + required this.userNickname, + required this.account, required this.rewardScene, required this.rewardType, required this.rewardItemId, @@ -299,6 +519,25 @@ class SCRoomRocketRewardRecordRes { roundNo: _intValue(data['roundNo']), level: _intValue(data['level']), 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']), rewardType: _stringValue(data['rewardType']), rewardItemId: _stringValue(data['rewardItemId']), @@ -321,6 +560,9 @@ class SCRoomRocketRewardRecordRes { final int roundNo; final int level; final String userId; + final String userAvatar; + final String userNickname; + final String account; final String rewardScene; final String rewardType; final String rewardItemId; @@ -334,6 +576,19 @@ class SCRoomRocketRewardRecordRes { final String createTime; 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 { if (rewardAmount <= 0) { return '1'; @@ -344,6 +599,33 @@ class SCRoomRocketRewardRecordRes { return rewardAmount.toString(); } + Map 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 listFromJson(dynamic value) { if (value is! Iterable) { return const []; @@ -371,6 +653,62 @@ String _stringValue(dynamic value) { return value.toString().trim(); } +String _firstAssetString(Iterable 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}) { if (value is int) { return value; diff --git a/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart b/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart index 3ecc795..5292e17 100644 --- a/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart +++ b/lib/shared/business_logic/models/res/sc_room_rocket_status_res.dart @@ -46,6 +46,10 @@ class SCRoomRocketStatusRes { List? levels, SCRoomRocketRewardPreviewRes? rewardPreview, List? rocketKings, + String? timezone, + int? resetRemainingSeconds, + Object? resetTime, + Object? serverTime, }) { _roomId = roomId; _date = date; @@ -72,6 +76,10 @@ class SCRoomRocketStatusRes { _levels = levels; _rewardPreview = rewardPreview; _rocketKings = rocketKings; + _timezone = timezone; + _resetRemainingSeconds = resetRemainingSeconds; + _resetTime = resetTime; + _serverTime = serverTime; } SCRoomRocketStatusRes.fromJson(dynamic json) { @@ -104,6 +112,48 @@ class SCRoomRocketStatusRes { json['rewardPreview'] == null ? null : 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; @@ -131,6 +181,10 @@ class SCRoomRocketStatusRes { List? _levels; SCRoomRocketRewardPreviewRes? _rewardPreview; List? _rocketKings; + String? _timezone; + int? _resetRemainingSeconds; + Object? _resetTime; + Object? _serverTime; String? get roomId => _roomId; @@ -182,6 +236,14 @@ class SCRoomRocketStatusRes { List? get rocketKings => _rocketKings; + String? get timezone => _timezone; + + int? get resetRemainingSeconds => _resetRemainingSeconds; + + Object? get resetTime => _resetTime; + + Object? get serverTime => _serverTime; + Map toJson() { final map = {}; map['roomId'] = _roomId; @@ -206,6 +268,10 @@ class SCRoomRocketStatusRes { map['needEnergy'] = _needEnergy; map['displayPercent'] = _displayPercent; map['shake'] = _shake; + map['timezone'] = _timezone; + map['resetRemainingSeconds'] = _resetRemainingSeconds; + map['resetTime'] = _resetTime; + map['serverTime'] = _serverTime; return map; } @@ -240,6 +306,27 @@ class SCRoomRocketStatusRes { 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 values) { + for (final value in values) { + if (value == null) { + continue; + } + if (value is String && value.trim().isEmpty) { + continue; + } + return value; + } + return null; + } + static List? _levelList(dynamic value) { if (value is! Iterable) { return null; diff --git a/lib/shared/business_logic/models/res/sc_user_badge_res.dart b/lib/shared/business_logic/models/res/sc_user_badge_res.dart new file mode 100644 index 0000000..e7b6ec9 --- /dev/null +++ b/lib/shared/business_logic/models/res/sc_user_badge_res.dart @@ -0,0 +1,76 @@ +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; + +class SCUserBadgeRes { + SCUserBadgeRes({ + this.userId, + List? badges, + List? longBadges, + List? shortBadges, + }) : badges = badges ?? const [], + longBadges = longBadges ?? const [], + shortBadges = shortBadges ?? const []; + + 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 badges; + final List longBadges; + final List shortBadges; + + List get displayBadges { + if (longBadges.isNotEmpty || shortBadges.isNotEmpty) { + return [...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 _badgeList(dynamic value) { + if (value is! List) { + return const []; + } + return value + .whereType() + .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; + } +} diff --git a/lib/shared/business_logic/repositories/room_repository.dart b/lib/shared/business_logic/repositories/room_repository.dart index 0b7961d..f3c09e3 100644 --- a/lib/shared/business_logic/repositories/room_repository.dart +++ b/lib/shared/business_logic/repositories/room_repository.dart @@ -190,6 +190,17 @@ abstract class SocialChatRoomRepository { ///礼物背包 Future> giftBackpack(); + ///赠送背包礼物 + Future giveBackpackGift({ + required String id, + required String acceptUserId, + required String giftId, + required num quantity, + String? roomId, + num? seatIndex, + String? songId, + }); + ///查询房间火箭状态 Future rocketStatus(String roomId); @@ -203,7 +214,11 @@ abstract class SocialChatRoomRepository { }); ///查询中奖弹窗 - Future roomRocketRewardPopups(String roomId); + Future roomRocketRewardPopups( + String roomId, { + int cursor = 1, + int limit = 10, + }); ///确认中奖弹窗已读 Future ackRoomRocketRewardPopups(List rewardRecordIds); @@ -212,7 +227,7 @@ abstract class SocialChatRoomRepository { Future roomRocketRewardRecords( String roomId, { int cursor = 1, - int limit = 20, + int limit = 10, }); ///查询火箭规则说明 diff --git a/lib/shared/business_logic/repositories/user_repository.dart b/lib/shared/business_logic/repositories/user_repository.dart index 352d021..b983a3a 100644 --- a/lib/shared/business_logic/repositories/user_repository.dart +++ b/lib/shared/business_logic/repositories/user_repository.dart @@ -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_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_identity_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_level_exp_res.dart'; @@ -63,7 +64,13 @@ abstract class SocialChatUserRepository { Future createRoom(); ///加入房间 - Future entryRoom(String roomId, {String? pwd}); + Future entryRoom( + String roomId, { + String? pwd, + String redPackId = "", + bool needOpenRedenvelope = false, + bool silentErrorToast = false, + }); ///发送心跳 Future heartbeat(String status, bool upMick, {String? roomId}); @@ -248,6 +255,12 @@ abstract class SocialChatUserRepository { ///查询已拥有徽章 Future> badgeOwnList(String type); + ///查询当前用户正在展示的徽章 + Future currentUserBadges(); + + ///查询其他用户正在展示的徽章 + Future otherUserBadges(String userId); + ///切换佩戴徽章 Future badgeToggle(String badgeId); diff --git a/lib/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart b/lib/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart index 1fa75ae..7d5046d 100644 --- a/lib/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart +++ b/lib/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart @@ -3,8 +3,11 @@ class RoomRocketLaunchBroadcastMessage { required this.roomId, required this.roomAccount, required this.roomName, + required this.roomCoverUrl, required this.roundNo, required this.level, + required this.rocketIconUrl, + required this.rocketAnimationUrl, required this.durationSeconds, required this.triggerUser, required this.triggerUserId, @@ -17,8 +20,11 @@ class RoomRocketLaunchBroadcastMessage { roomId: _stringValue(data['roomId']), roomAccount: _stringValue(data['roomAccount']), roomName: _stringValue(data['roomName']), + roomCoverUrl: _roomCoverUrl(data), roundNo: _intValue(data['roundNo']), level: _intValue(data['level'], fallback: 1), + rocketIconUrl: _rocketIconUrl(data), + rocketAnimationUrl: _rocketAnimationUrl(data), durationSeconds: _intValue(data['durationSeconds'], fallback: 7), triggerUser: RoomRocketLaunchTriggerUser.fromJson(data['triggerUser']), triggerUserId: _stringValue(data['triggerUserId']), @@ -29,8 +35,11 @@ class RoomRocketLaunchBroadcastMessage { final String roomId; final String roomAccount; final String roomName; + final String roomCoverUrl; final int roundNo; final int level; + final String rocketIconUrl; + final String rocketAnimationUrl; final int durationSeconds; final RoomRocketLaunchTriggerUser triggerUser; final String triggerUserId; @@ -103,6 +112,80 @@ String _stringValue(dynamic value) { return value.toString().trim(); } +String _roomCoverUrl(Map 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 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 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 values) { + for (final value in values) { + final text = _stringValue(value); + if (text.isNotEmpty) { + return text; + } + } + return ''; +} + int _intValue(dynamic value, {int fallback = 0}) { if (value is int) { return value; diff --git a/lib/shared/data_sources/sources/local/floating_screen_manager.dart b/lib/shared/data_sources/sources/local/floating_screen_manager.dart index fe0896f..8d374cf 100644 --- a/lib/shared/data_sources/sources/local/floating_screen_manager.dart +++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart @@ -27,6 +27,9 @@ class OverlayManager { static const int _maxDeferredRegionRedPacketCount = 12; static const Duration _compensationDedupWindow = Duration(seconds: 3); static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30); + static const List _stackedFloatingOrder = [5, 1, 4, 3]; + static const double _stackedFloatingBaseTop = 70; + static const double _stackedFloatingGap = 8; final SCPriorityQueue _messageQueue = SCPriorityQueue( (a, b) => b.priority.compareTo(a.priority), @@ -34,6 +37,8 @@ class OverlayManager { final List _deferredRegionRedPacketMessages = []; final Map _recentCompensationKeys = {}; final Map _recentFloatingKeys = {}; + final Map _stackedOverlayEntries = {}; + final Map _stackedMessages = {}; bool _isPlaying = false; OverlayEntry? _currentOverlayEntry; SCFloatingMessage? _currentMessage; @@ -55,7 +60,6 @@ class OverlayManager { if (_isRecentFloatingDuplicate(message)) { return; } - if (message.type == 4) {} _rememberFloatingMessage(message); _enqueueMessage( message, @@ -63,6 +67,7 @@ class OverlayManager { ); } else { _removeActiveMessage(scheduleNext: false); + _removeAllStackedMessages(); _messageQueue.clear(); _deferredRegionRedPacketMessages.clear(); _isPlaying = false; @@ -238,12 +243,14 @@ class OverlayManager { await SCSvgaAssetWidget.preloadAsset( FloatingLuckGiftScreenWidget.backgroundSvgaAssetPath, ); - } catch (error) {} + } catch (error) { + // Preload failure is non-fatal; the banner will retry during rendering. + } } void _safeScheduleNext() { if (_isSuppressed) return; - if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return; + if (_isProcessing || _messageQueue.isEmpty) return; _isProcessing = true; try { @@ -254,7 +261,7 @@ class OverlayManager { } void _scheduleNext() { - if (_isPlaying || _messageQueue.isEmpty || _isDisposed || _isSuppressed) { + if (_messageQueue.isEmpty || _isDisposed || _isSuppressed) { return; } @@ -262,37 +269,64 @@ class OverlayManager { final context = navigatorKey.currentState?.context; if (context == null || !context.mounted) return; - // 安全地获取第一个消息 - if (_messageQueue.isEmpty) return; - final messageToProcess = _messageQueue.first; - if (messageToProcess == null) { - _messageQueue.removeFirst(); - _safeScheduleNext(); - return; - } - - if (!_shouldDisplayMessage(context, messageToProcess)) { - _messageQueue.removeFirst(); - if (_shouldDeferRegionRedPacketMessage(context, messageToProcess)) { - _deferRegionRedPacketMessage(context, messageToProcess); - _safeScheduleNext(); + while (_messageQueue.isNotEmpty) { + final messageToPlay = _removeNextPlayableMessage(context); + if (messageToPlay == null) { + return; + } + _playMessage(messageToPlay); + if (!_isStackedFloatingMessage(messageToPlay)) { return; } - _logRegionRedPacketDrop(context, messageToProcess); - _safeScheduleNext(); - return; } - - // 安全地移除并播放消息 - final messageToPlay = _messageQueue.removeFirst(); - _playMessage(messageToPlay); } catch (e) { _isPlaying = false; _safeScheduleNext(); } } + SCFloatingMessage? _removeNextPlayableMessage(BuildContext context) { + final skippedMessages = []; + 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) { + if (_isStackedFloatingMessage(message)) { + _playStackedMessage(message); + return; + } + _isPlaying = true; _currentMessage = message; final context = navigatorKey.currentState?.context; @@ -303,21 +337,6 @@ class OverlayManager { 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; 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) { case 0: 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.from(_stackedOverlayEntries.keys); + for (final slotKey in slotKeys) { + _removeStackedMessage(slotKey); + } + } + void activate() { _isDisposed = false; _loadSavedDisplayPreference(); @@ -426,6 +593,7 @@ class OverlayManager { void beginSuppressFloatingScreens({String reason = 'unknown'}) { _suppressedCount += 1; _removeActiveMessage(scheduleNext: false); + _removeAllStackedMessages(); _messageQueue.clear(); _isPlaying = false; _isProcessing = false; @@ -446,6 +614,7 @@ class OverlayManager { } if (SCGlobalConfig.isFloatingBroadcastClosed) { _removeActiveMessage(scheduleNext: false); + _removeAllStackedMessages(); _messageQueue.clear(); _deferredRegionRedPacketMessages.clear(); return; @@ -457,8 +626,10 @@ class OverlayManager { context.mounted && activeMessage != null && !_shouldDisplayMessage(context, activeMessage)) { - _removeActiveMessage(); - return; + _removeActiveMessage(scheduleNext: false); + } + if (context != null && context.mounted) { + _removeHiddenStackedMessages(context); } _safeScheduleNext(); } @@ -470,6 +641,7 @@ class OverlayManager { _currentOverlayEntry?.remove(); _currentOverlayEntry = null; _currentMessage = null; + _removeAllStackedMessages(); _messageQueue.clear(); _deferredRegionRedPacketMessages.clear(); _recentCompensationKeys.clear(); @@ -486,6 +658,9 @@ class OverlayManager { _removeMessagesByType(1); _removeMessagesByType(0); _removeMessagesByType(5); + if (normalizedRoomId.isNotEmpty) { + _removeMessagesByType(3, roomId: normalizedRoomId); + } if (normalizedRoomId.isNotEmpty) { _removeMessagesByType(4, roomId: normalizedRoomId); } @@ -516,12 +691,20 @@ class OverlayManager { while (newQueue.isNotEmpty) { _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; - bool get isPlaying => _isPlaying; + bool get isPlaying => _isPlaying || _stackedOverlayEntries.isNotEmpty; bool get _isSuppressed => _suppressedCount > 0; @@ -580,19 +763,6 @@ class OverlayManager { } } - String _displayStateForLog(BuildContext context) { - final rtcProvider = Provider.of( - 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) { if (message == null) { return false; @@ -702,13 +872,13 @@ class OverlayManager { void _removeActiveRegionBroadcastMessage() { final activeMessage = _currentMessage; - if (activeMessage == null || !activeMessage.isRegionBroadcast) { - return; + if (activeMessage != null && activeMessage.isRegionBroadcast) { + _currentOverlayEntry?.remove(); + _currentOverlayEntry = null; + _currentMessage = null; + _isPlaying = false; } - _currentOverlayEntry?.remove(); - _currentOverlayEntry = null; - _currentMessage = null; - _isPlaying = false; + _removeStackedMessagesWhere((message) => message.isRegionBroadcast); _safeScheduleNext(); } @@ -718,18 +888,21 @@ class OverlayManager { }) { final activeMessage = _currentMessage; final normalizedRoomId = roomId?.trim() ?? ''; - if (activeMessage == null || - (activeMessage.type != 0 && - activeMessage.type != 1 && - activeMessage.type != 5 && - !(includeRedPacket && activeMessage.type == 4))) { - return; + if (activeMessage != null && + (activeMessage.type == 0 || + activeMessage.type == 1 || + activeMessage.type == 3 || + activeMessage.type == 5 || + (includeRedPacket && activeMessage.type == 4)) && + (normalizedRoomId.isEmpty || + (activeMessage.roomId ?? "").trim() == normalizedRoomId)) { + _removeActiveMessage(scheduleNext: false); } - if (normalizedRoomId.isNotEmpty && - (activeMessage.roomId ?? "").trim() != normalizedRoomId) { - return; - } - _removeActiveMessage(); + _removeStackedRoomMessages( + roomId: normalizedRoomId, + includeRedPacket: includeRedPacket, + ); + _safeScheduleNext(); } void _removeActiveMessage({bool scheduleNext = true}) { @@ -741,4 +914,47 @@ class OverlayManager { _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 = []; + _stackedMessages.forEach((slotKey, message) { + if (test(message)) { + matchedSlotKeys.add(slotKey); + } + }); + if (matchedSlotKeys.isEmpty) { + return; + } + for (final slotKey in matchedSlotKeys) { + _removeStackedMessage(slotKey); + } + _markStackedEntriesNeedsBuild(); + } } diff --git a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart index 81e2e03..fe5c4c1 100644 --- a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart +++ b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart @@ -122,6 +122,193 @@ class SCChatRoomRepository implements SocialChatRoomRepository { 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 []; + 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 {}; + 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 _debugMap(dynamic value) { + if (value is Map) { + return value; + } + if (value is Map) { + return value.map((key, item) => MapEntry(key.toString(), item)); + } + return const {}; + } + + static const List _debugIconKeys = [ + 'rocket_icon_url', + 'rocketIconUrl', + 'rocketIcon', + 'iconUrl', + 'icon', + ]; + + static const List _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 _debugAnimationKeys = [ + 'rocket_animation_url', + 'rocketAnimationUrl', + 'rocketAnimation', + 'rocketSvgaUrl', + 'animationUrl', + 'animation', + 'svgaUrl', + 'rocketLaunchSvgaUrl', + 'launchAnimationUrl', + 'launchAnimation', + 'launchSvgaUrl', + 'sourceUrl', + ]; + + String _debugAssetValue(Map data, List 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) { final normalized = value.trim(); return int.tryParse(normalized) ?? normalized; @@ -552,23 +739,25 @@ class SCChatRoomRepository implements SocialChatRoomRepository { ///room/setting/locked @override Future roomLocked(String roomId, String password) async { - final result = await http.put( + await http.put( "e9531da4dd5c83bd5386366519ac6d0b727c997eb7a1adb8323f850b6c541f3b", data: {"roomId": roomId, "password": password}, - fromJson: (json) => json as bool, + allowNullBody: true, + fromJson: (json) => json, ); - return result; + return true; } ///room/setting/unlocked @override Future roomUnlocked(String roomId) async { - final result = await http.post( + await http.post( "d8e78b8fb4cf9f0d41716d9978235334d9382f6a419ce06c2dab0b362c318a5d", data: {"roomId": roomId}, - fromJson: (json) => json as bool, + allowNullBody: true, + fromJson: (json) => json, ); - return result; + return true; } ///room/setting @@ -768,11 +957,48 @@ class SCChatRoomRepository implements SocialChatRoomRepository { (json) => (json as List) .map((e) => SocialChatGiftBackpackRes.fromJson(e)) + .where((item) => item.giftConfig != null) .toList(), ); return result; } + ///gift/backpack/give-away + @override + Future giveBackpackGift({ + required String id, + required String acceptUserId, + required String giftId, + required num quantity, + String? roomId, + num? seatIndex, + String? songId, + }) async { + final params = { + "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( + "/gift/backpack/give-away", + data: params, + allowNullBody: true, + extra: const {BaseNetworkClient.silentErrorToastKey: true}, + fromJson: (json) => json, + ); + return result; + } + ///go/app/voice-room/rocket/status @override Future rocketStatus(String roomId) async { @@ -786,7 +1012,9 @@ class SCChatRoomRepository implements SocialChatRoomRepository { extra: {BaseNetworkClient.silentErrorToastKey: true}, fromJson: (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); @@ -827,9 +1055,15 @@ class SCChatRoomRepository implements SocialChatRoomRepository { ///go/app/voice-room/rocket/reward-popups @override Future roomRocketRewardPopups( - String roomId, - ) async { - final queryParams = {if (roomId.trim().isNotEmpty) "roomId": roomId}; + String roomId, { + int cursor = 1, + 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"; _rocketApiDebug('request GET $path', query: queryParams); final result = await http.get( @@ -875,7 +1109,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository { Future roomRocketRewardRecords( String roomId, { int cursor = 1, - int limit = 20, + int limit = 10, }) async { final queryParams = { if (roomId.trim().isNotEmpty) "roomId": roomId, diff --git a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart index 2f97b17..2d527ca 100644 --- a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart +++ b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart @@ -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_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_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_level_exp_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 redPackId = "", bool needOpenRedenvelope = false, + bool silentErrorToast = false, }) async { var param = {}; param["roomId"] = roomId; @@ -195,6 +197,10 @@ class SCAccountRepository implements SocialChatUserRepository { final result = await http.post( "363603111e51beac2d183014dd29b81c219577507bcc9c6abfaf327548fed0e0", data: param, + extra: + silentErrorToast + ? const {BaseNetworkClient.silentErrorToastKey: true} + : null, fromJson: (json) => JoinRoomRes.fromJson(json), ); return SCRoomProfileCache.applyToJoinRoomRes(result); @@ -1181,6 +1187,32 @@ class SCAccountRepository implements SocialChatUserRepository { return result; } + ///app/user/badge/current + @override + Future currentUserBadges() async { + const path = "/app/user/badge/current"; + final result = await http.get( + path, + extra: const {BaseNetworkClient.silentErrorToastKey: true}, + fromJson: (json) => SCUserBadgeRes.fromJson(json), + ); + return result; + } + + ///app/user/badge/other + @override + Future otherUserBadges(String userId) async { + final params = {"userId": userId}; + const path = "/app/user/badge/other"; + final result = await http.get( + path, + queryParams: params, + extra: const {BaseNetworkClient.silentErrorToastKey: true}, + fromJson: (json) => SCUserBadgeRes.fromJson(json), + ); + return result; + } + ///user/badge/toggle @override Future badgeToggle(String badgeId) async { diff --git a/lib/shared/tools/sc_path_utils.dart b/lib/shared/tools/sc_path_utils.dart index 1d5b8e3..da4dbbb 100644 --- a/lib/shared/tools/sc_path_utils.dart +++ b/lib/shared/tools/sc_path_utils.dart @@ -53,6 +53,7 @@ class SCPathUtils { '.mp4', '.vap', '.svga', + '.pag', ].contains(ext); return isDirectAsset && isSupportedAsset; @@ -76,6 +77,7 @@ class SCPathUtils { '.mov', '.avi', '.svga', + '.pag', '.png', '.jpg', '.txt', diff --git a/lib/shared/tools/sc_room_utils.dart b/lib/shared/tools/sc_room_utils.dart index 23b2222..d495f75 100644 --- a/lib/shared/tools/sc_room_utils.dart +++ b/lib/shared/tools/sc_room_utils.dart @@ -23,6 +23,24 @@ typedef SCRoomUtils = SCChatRoomHelper; class SCChatRoomHelper { static Map roomUsersMap = {}; + static bool isCurrentRoom(String roomId, BuildContext context) { + final normalizedRoomId = roomId.trim(); + if (normalizedRoomId.isEmpty) { + return false; + } + try { + final currentRoomId = + Provider.of( + context, + listen: false, + ).currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? + ''; + return currentRoomId.isNotEmpty && currentRoomId == normalizedRoomId; + } catch (_) { + return false; + } + } + static void goRoom( String roomId, BuildContext context, { diff --git a/lib/ui_kit/widgets/badge/sc_user_badge_strip.dart b/lib/ui_kit/widgets/badge/sc_user_badge_strip.dart new file mode 100644 index 0000000..0ee187f --- /dev/null +++ b/lib/ui_kit/widgets/badge/sc_user_badge_strip.dart @@ -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 [], + 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 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 createState() => _SCUserBadgeStripState(); +} + +class _SCUserBadgeStripState extends State { + static final Map> _badgeFutures = {}; + + Future? _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? _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( + future: future, + builder: (context, snapshot) { + final remoteBadges = + snapshot.data?.displayBadges ?? const []; + final badges = remoteBadges.isNotEmpty ? remoteBadges : _fallbackBadges; + return _buildBadges(badges, stripHeight); + }, + ); + } + + List get _fallbackBadges { + return widget.fallbackBadges + .where((badge) => _badgeUrl(badge).isNotEmpty) + .toList(); + } + + Widget _buildBadges(List 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 _withSpacing(List children, double spacing) { + if (children.length < 2) { + return children; + } + final spaced = []; + for (var i = 0; i < children.length; i++) { + if (i > 0) { + spaced.add(SizedBox(width: spacing)); + } + spaced.add(children[i]); + } + return spaced; + } +} diff --git a/lib/ui_kit/widgets/room/empty_mai_select.dart b/lib/ui_kit/widgets/room/empty_mai_select.dart index 662640a..73ec623 100644 --- a/lib/ui_kit/widgets/room/empty_mai_select.dart +++ b/lib/ui_kit/widgets/room/empty_mai_select.dart @@ -247,7 +247,7 @@ class EmptyMaiSelect extends StatelessWidget { context, () { Navigator.of(context).pop(); - showBottomInCenterDialog( + showBottomInBottomDialog( context, RoomUserInfoCard(userId: clickUser?.id), ); diff --git a/lib/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart index 78b6562..ca4550b 100644 --- a/lib/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart +++ b/lib/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart @@ -2,10 +2,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_debouncer/flutter_debouncer.dart'; import 'package:flutter_screenutil/flutter_screenutil.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/main.dart'; -import 'package:marquee/marquee.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; @@ -15,13 +13,13 @@ class FloatingRoomRocketScreenWidget extends StatefulWidget { final VoidCallback onAnimationCompleted; // 动画完成回调 const FloatingRoomRocketScreenWidget({ - Key? key, + super.key, required this.message, required this.onAnimationCompleted, - }) : super(key: key); + }); @override - _FloatingRoomRocketScreenWidgetState createState() => + State createState() => _FloatingRoomRocketScreenWidgetState(); } @@ -108,10 +106,7 @@ class _FloatingRoomRocketScreenWidgetState } Duration get _holdDuration { - final totalSeconds = widget.message.durationSeconds; - final totalMs = totalSeconds == null ? 6000 : (totalSeconds * 1000).round(); - final holdMs = totalMs - 3000; - return Duration(milliseconds: holdMs.clamp(0, 10000).toInt()); + return const Duration(seconds: 7); } @override @@ -139,19 +134,32 @@ class _FloatingRoomRocketScreenWidgetState } 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像素) return GestureDetector( onTap: () { debouncer.debounce( duration: Duration(milliseconds: 350), onDebounce: () { - if (widget.message.roomId != null && - widget.message.roomId!.isNotEmpty) { - SCRoomUtils.goRoom( - widget.message.roomId!, - navigatorKey.currentState!.context, - fromFloting: true, - ); + final roomId = widget.message.roomId?.trim() ?? ''; + final currentContext = navigatorKey.currentState?.context; + if (roomId.isEmpty || + currentContext == null || + !currentContext.mounted || + SCRoomUtils.isCurrentRoom(roomId, currentContext)) { + return; + } + if (roomId.isNotEmpty) { + SCRoomUtils.goRoom(roomId, currentContext, fromFloting: true); } }, ); @@ -177,91 +185,78 @@ class _FloatingRoomRocketScreenWidgetState child: Directionality( textDirection: TextDirection.ltr, child: Container( - alignment: Alignment.topCenter, - height: 128.w, - width: 290.w, - margin: EdgeInsets.only(top: 10.w), - padding: EdgeInsetsDirectional.only(start: 3.w, end: 25.w), - decoration: BoxDecoration( + width: 307.w, + height: 68.w, + decoration: const BoxDecoration( image: DecorationImage( - image: AssetImage( - "sc_images/index/sc_icon_rocket_broad_bg_lv${widget.message.rocketLevel}.webp", - ), + image: AssetImage(_RocketFloatingAssets.bannerBg), fit: BoxFit.fill, ), ), - child: Row( - mainAxisSize: MainAxisSize.min, // 宽度由内容决定 + child: Stack( + clipBehavior: Clip.none, children: [ - SizedBox(width: 5.w), - Container( + Positioned( + left: 21.w, + top: 8.w, + width: 38.w, + height: 38.w, child: head( - url: widget.message.userAvatarUrl ?? "", - width: 66.w, + url: widget.message.userAvatarUrl ?? '', + width: 38.w, ), ), - SizedBox(width: 4.w), - Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox(height: 6.w), - Row( - children: [ - SizedBox( - width: 60.w, - 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: 9.w, + width: 126.w, + child: Text( + roomName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + textScaler: TextScaler.noScaling, + style: TextStyle( + color: Colors.white, + fontSize: 16.sp, + height: 1, + fontWeight: FontWeight.w400, + decoration: TextDecoration.none, + ), ), ), + 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.forward(); } + + String _firstNonBlank(Iterable 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'; } diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart index 107f4b4..456c929 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart @@ -24,7 +24,17 @@ class RoomRocketApiMapper { .map( (item) => RoomRocketLevelItem( label: 'Lv.${item.level}', + level: item.level, 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, ), ) @@ -41,12 +51,28 @@ class RoomRocketApiMapper { 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( SCRoomRocketRewardRes reward, ) { return RoomRocketRewardItem( imageUrl: reward.rewardCover.isEmpty ? null : reward.rewardCover, amount: reward.displayAmount, + rewardType: reward.rewardType, + expireDays: reward.expireDays, ); } @@ -85,7 +111,8 @@ class RoomRocketApiMapper { .map((entry) { final first = entry.value.first; return RoomRocketRewardWinner( - nickname: first.userId.isNotEmpty ? 'ID:${first.userId}' : 'You', + nickname: first.displayName, + avatarUrl: first.userAvatar.isEmpty ? null : first.userAvatar, rewards: rewardItemsFromRecords(entry.value), ); }) @@ -133,6 +160,8 @@ class RoomRocketApiMapper { return RoomRocketRewardItem( imageUrl: record.rewardCover.isEmpty ? null : record.rewardCover, amount: record.displayAmount, + rewardType: record.rewardType, + expireDays: record.expireDays, ); } diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart index f204958..aff1d6d 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_dialog.dart @@ -1,8 +1,14 @@ +import 'dart:async'; import 'dart:math' as math; +import 'package:flutter/foundation.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/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 { const RoomRocketDialog({ @@ -14,6 +20,9 @@ class RoomRocketDialog extends StatelessWidget { this.selectedLevel = 0, this.onLevelSelected, this.rewards, + this.rewardGroups, + this.selectedRewardTab = RoomRocketRewardTab.top1, + this.onRewardTabSelected, this.crew, this.onClose, this.onOpenRank, @@ -29,6 +38,9 @@ class RoomRocketDialog extends StatelessWidget { final int selectedLevel; final ValueChanged? onLevelSelected; final List? rewards; + final RoomRocketRewardGroups? rewardGroups; + final RoomRocketRewardTab selectedRewardTab; + final ValueChanged? onRewardTabSelected; final List? crew; final VoidCallback? onClose; final VoidCallback? onOpenRank; @@ -39,9 +51,15 @@ class RoomRocketDialog extends StatelessWidget { @override Widget build(BuildContext context) { final resolvedLevels = _normalizeLevels(levels); - final resolvedRewards = _normalizeRewards(rewards); + final resolvedRewards = _normalizeRewards( + rewardGroups?.rewardsFor(selectedRewardTab) ?? rewards, + useFallback: rewardGroups == null, + ); 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 = progressStage ?? RoomRocketProgressStage.fromPercent(percent); final resolvedBottomStage = @@ -81,11 +99,15 @@ class RoomRocketDialog extends StatelessWidget { percent: percent, progressStage: resolvedProgressStage, bottomStage: resolvedBottomStage, + status: status, round: round, levels: resolvedLevels, - selectedLevel: selectedLevel, + selectedLevel: resolvedSelectedLevel, + selectedLevelItem: selectedLevelItem, onLevelSelected: onLevelSelected, rewards: resolvedRewards, + selectedRewardTab: selectedRewardTab, + onRewardTabSelected: onRewardTabSelected, crew: resolvedCrew, onClose: onClose, onOpenRank: onOpenRank, @@ -117,20 +139,25 @@ class RoomRocketDialog extends StatelessWidget { } return List.generate( 6, - (index) => RoomRocketLevelItem(label: 'Lv.${index + 1}'), + (index) => + RoomRocketLevelItem(label: 'Lv.${index + 1}', level: index + 1), growable: false, ); } static List _normalizeRewards( - List? rewards, - ) { + List? rewards, { + required bool useFallback, + }) { final source = rewards?.where((item) => item.amount.trim().isNotEmpty).toList() ?? const []; if (source.isNotEmpty) { return source; } + if (!useFallback) { + return const []; + } return const [ 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; 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 { - 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? imageUrl; 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 [], + this.setOff = const [], + this.inRoom = const [], + }); + + final List top1; + final List setOff; + final List inRoom; + + List rewardsFor(RoomRocketRewardTab tab) { + switch (tab) { + case RoomRocketRewardTab.top1: + return top1; + case RoomRocketRewardTab.setOff: + return setOff; + case RoomRocketRewardTab.inRoom: + return inRoom; + } + } } class RoomRocketLevelItem { const RoomRocketLevelItem({ required this.label, + this.level = 1, this.asset, this.imageUrl, + this.previewImageUrl, + this.animationUrl, + this.progressImageUrl, + this.shakeThresholdPercent = 95, this.enabled = true, }); final String label; + final int level; final String? asset; final String? imageUrl; + final String? previewImageUrl; + final String? animationUrl; + final String? progressImageUrl; + final num shakeThresholdPercent; final bool enabled; } @@ -220,11 +321,15 @@ class _RocketCanvas extends StatelessWidget { required this.percent, required this.progressStage, required this.bottomStage, + required this.status, required this.round, required this.levels, required this.selectedLevel, + required this.selectedLevelItem, this.onLevelSelected, required this.rewards, + required this.selectedRewardTab, + this.onRewardTabSelected, required this.crew, this.onClose, this.onOpenRank, @@ -237,11 +342,15 @@ class _RocketCanvas extends StatelessWidget { final double percent; final RoomRocketProgressStage progressStage; final RoomRocketBottomStage bottomStage; + final SCRoomRocketStatusRes? status; final int round; final List levels; final int selectedLevel; + final RoomRocketLevelItem selectedLevelItem; final ValueChanged? onLevelSelected; final List rewards; + final RoomRocketRewardTab selectedRewardTab; + final ValueChanged? onRewardTabSelected; final List crew; final VoidCallback? onClose; final VoidCallback? onOpenRank; @@ -257,7 +366,7 @@ class _RocketCanvas extends StatelessWidget { clipBehavior: Clip.hardEdge, children: [ _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, 0, 202, 66, 207), _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( String asset, double left, @@ -347,9 +474,20 @@ class _RocketCanvas extends StatelessWidget { RoomRocketProgressStage.charging => _RocketAssets.progressPartial, RoomRocketProgressStage.full => _RocketAssets.progressFull, }; + final progressImageUrl = selectedLevelItem.progressImageUrl?.trim() ?? ''; return Stack( 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( left: s(316), top: s(373), @@ -438,30 +576,12 @@ class _RocketCanvas extends StatelessWidget { color: Colors.white, ), ), - for (final item in const [ - _TimerToken('23', 193), - _TimerToken('23', 223), - _TimerToken('23', 253), - ]) - 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), - ), - ), + Positioned( + left: s(187), + top: s(468), + width: s(88), + child: _ResetCountdownText(status: status, scale: scale), + ), ], ); } @@ -500,10 +620,25 @@ class _RocketCanvas extends StatelessWidget { left: 31, top: 552, 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( left: s(28), top: s(578), @@ -514,7 +649,9 @@ class _RocketCanvas extends StatelessWidget { behavior: const _NoGlowScrollBehavior(), child: SingleChildScrollView( scrollDirection: Axis.horizontal, - physics: const BouncingScrollPhysics(), + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), child: Row( children: [ for (var index = 0; index < rewards.length; index++) ...[ @@ -524,7 +661,11 @@ class _RocketCanvas extends StatelessWidget { child: _RewardCard( scale: scale, 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)), @@ -564,7 +705,7 @@ class _RocketCanvas extends StatelessWidget { if (!activeBottom) Positioned( left: s(108), - top: s(750), + top: s(760), width: s(169), height: s(36), child: FittedBox( @@ -651,28 +792,24 @@ class _RewardCard extends StatelessWidget { top: 24 * scale, width: 50 * scale, height: 50 * scale, - child: _RocketImage( - asset: item.asset, - imageUrl: item.imageUrl, - fit: BoxFit.cover, - ), + child: + item.isGoldReward + ? Image.asset(_RocketAssets.coin, fit: BoxFit.contain) + : _RocketImage( + asset: item.asset, + imageUrl: item.imageUrl, + fit: BoxFit.contain, + ), ), Positioned( - left: 21 * scale, - top: 106 * scale, - width: 14 * scale, - height: 14 * scale, - child: Image.asset(_RocketAssets.coin, fit: BoxFit.contain), - ), - Positioned( - left: 37 * scale, + left: 16 * scale, top: 105 * scale, - width: 46 * scale, + width: 65 * scale, child: _RocketText( - item.amount, + item.displayText, fontSize: 14 * scale, color: Colors.white, - textAlign: TextAlign.left, + textAlign: TextAlign.center, ), ), ], @@ -792,18 +929,139 @@ class _CrewAvatar 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? imageUrl; final BoxFit fit; + final bool preferSvg; + final String? debugName; @override Widget build(BuildContext context) { 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) { + 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(' 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 _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 { const _GradientText( 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 { const _RocketAssets._(); diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart index 5df2daa..e5f089c 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart @@ -1,20 +1,25 @@ import 'dart:async'; +import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.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:provider/provider.dart'; import 'package:yumi/modules/room/voice_room_route.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/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/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_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_reward_dialog.dart'; class RoomRocketFloatingEntry extends StatelessWidget { const RoomRocketFloatingEntry({super.key}); @@ -29,55 +34,53 @@ class RoomRocketFloatingEntry extends StatelessWidget { return Selector( selector: (_, provider) => provider.roomRocketStatus, builder: (context, status, child) { + final level = _currentEntryLevel(status); + final percent = _entryPercent(status); + final shouldShake = + (status?.shake ?? false) || + (level != null && percent > level.shakeThresholdPercent); return SCDebounceWidget( onTap: () => _showRocketDialog(context, status), - child: SizedBox( - width: 44.w, - height: 44.w, - child: DecoratedBox( - decoration: BoxDecoration( - 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, - ), - ), - ], - ), - ), - ), + child: _RoomRocketEntryButton( + iconUrl: level?.rocketIconUrl ?? '', + progressUrl: level?.progressBarUrl ?? '', + shake: shouldShake, + loading: status == null, ), ); }, ); } + SCRoomRocketLevelRes? _currentEntryLevel(SCRoomRocketStatusRes? status) { + final levels = status?.levels ?? const []; + 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) { final rtcProvider = Provider.of(context, listen: false); 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(' { SCRoomRocketStatusRes? _status; List? _noteSections; + int _selectedLevel = 0; + RoomRocketRewardTab _selectedRewardTab = RoomRocketRewardTab.top1; + bool _levelTouched = false; @override void initState() { super.initState(); _status = widget.initialStatus; + _selectedLevel = _levelIndexFromStatus(widget.initialStatus); unawaited(_load()); unawaited(_loadRule()); } @@ -145,7 +329,13 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { if (!mounted) { return; } - setState(() => _status = res); + _debugStatusAssets(res); + setState(() { + _status = res; + if (!_levelTouched) { + _selectedLevel = _levelIndexFromStatus(res); + } + }); try { Provider.of( context, @@ -175,22 +365,63 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { @override Widget build(BuildContext context) { final status = _status; - final selectedLevel = - ((status?.currentLevel ?? status?.level ?? 1) - 1).clamp(0, 5).toInt(); + final selectedLevel = _selectedLevel.clamp(0, 5).toInt(); return RoomRocketDialog( status: status, levels: RoomRocketApiMapper.levelsFromStatus(status), selectedLevel: selectedLevel, + onLevelSelected: _onLevelSelected, rewards: RoomRocketApiMapper.rewardsFromStatus(status), + rewardGroups: RoomRocketApiMapper.rewardGroupsFromStatus(status), + selectedRewardTab: _selectedRewardTab, + onRewardTabSelected: _onRewardTabSelected, crew: RoomRocketApiMapper.crewFromStatus(status), onClose: widget.onClose, onOpenRank: () => _openRank(selectedLevel), - onRewardSelected: _openReward, onOpenRecord: _openRecord, 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 []; + _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) { _rocketUiDebug( '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() { _rocketUiDebug('tap record roomId=${widget.roomId}'); 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? _rewards; - List? _winners; - bool _loading = true; - - @override - void initState() { - super.initState(); - unawaited(_load()); - } - - Future _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 { const _RoomRocketRecordDialogLoader({required this.roomId, this.onClose}); @@ -355,32 +500,74 @@ class _RoomRocketRecordDialogLoader extends StatefulWidget { class _RoomRocketRecordDialogLoaderState extends State<_RoomRocketRecordDialogLoader> { + final List _rawRecords = []; List? _records; bool _loading = true; + bool _loadingMore = false; + bool _hasMore = true; + int _cursor = 1; @override void initState() { super.initState(); - unawaited(_load()); + unawaited(_load(reset: true)); } - Future _load() async { + Future _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 { - _rocketUiDebug('request reward-records roomId=${widget.roomId}'); + _rocketUiDebug( + 'request reward-records roomId=${widget.roomId} ' + 'cursor=$requestCursor limit=$_rocketPageSize', + ); final res = await SCChatRoomRepository().roomRocketRewardRecords( widget.roomId, + cursor: requestCursor, + limit: _rocketPageSize, ); if (!mounted) { return; } + final nextRecords = res.records; setState(() { - _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; + _loadingMore = false; }); + _rocketUiDebug( + 'reward-records loaded cursor=$requestCursor ' + 'count=${nextRecords.length} total=${_rawRecords.length} ' + 'hasMore=$_hasMore nextCursor=$_cursor', + ); } catch (error) { _rocketUiDebug('reward-records request failed: $error'); if (mounted) { - setState(() => _loading = false); + setState(() { + _loading = false; + _loadingMore = false; + _hasMore = false; + }); } } } @@ -391,6 +578,9 @@ class _RoomRocketRecordDialogLoaderState records: _records, useMockFallback: false, loadingText: _loading ? 'Loading data......' : '', + isLoadingMore: _loadingMore, + hasMore: _hasMore, + onLoadMore: () => unawaited(_load()), limitText: 'Show only records from the last 35 days', onClose: widget.onClose, ); @@ -448,3 +638,14 @@ void _rocketUiDebug(String 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)}...'; +} diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart new file mode 100644 index 0000000..34c31eb --- /dev/null +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart @@ -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 createState() => + _RoomRocketPagEffectOverlayState(); +} + +class _RoomRocketPagEffectOverlayState + extends State { + 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 _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( + 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 _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://'); + } +} diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_record_dialog.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_record_dialog.dart index c5b2229..79cc022 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_record_dialog.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_record_dialog.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; 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/widgets/room/rocket/room_rocket_dialog.dart'; @@ -10,6 +11,9 @@ class RoomRocketRecordDialog extends StatelessWidget { this.records, this.loadingText = 'Loading data......', this.limitText = 'Show only records from the last 35 days', + this.isLoadingMore = false, + this.hasMore = false, + this.onLoadMore, this.useMockFallback = true, this.onClose, }); @@ -17,11 +21,21 @@ class RoomRocketRecordDialog extends StatelessWidget { final List? records; final String loadingText; final String limitText; + final bool isLoadingMore; + final bool hasMore; + final VoidCallback? onLoadMore; final bool useMockFallback; final VoidCallback? onClose; @override 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( records, useMockFallback: useMockFallback, @@ -57,6 +71,11 @@ class RoomRocketRecordDialog extends StatelessWidget { records: resolvedRecords, loadingText: loadingText, limitText: limitText, + isLoadingMore: isLoadingMore, + hasMore: hasMore, + onLoadMore: onLoadMore, + emptyText: emptyText, + contentDirection: contentDirection, onClose: onClose, ), ), @@ -100,6 +119,18 @@ class RoomRocketRecordDialog extends StatelessWidget { 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 { @@ -122,6 +153,11 @@ class _RecordDialogCanvas extends StatelessWidget { required this.records, required this.loadingText, required this.limitText, + required this.isLoadingMore, + required this.hasMore, + this.onLoadMore, + required this.emptyText, + required this.contentDirection, this.onClose, }); @@ -129,19 +165,26 @@ class _RecordDialogCanvas extends StatelessWidget { final List records; final String loadingText; final String limitText; + final bool isLoadingMore; + final bool hasMore; + final VoidCallback? onLoadMore; + final String emptyText; + final TextDirection contentDirection; final VoidCallback? onClose; double s(double value) => value * scale; @override Widget build(BuildContext context) { + final isLoading = loadingText.trim().isNotEmpty; + final isEmpty = !isLoading && records.isEmpty; return Stack( clipBehavior: Clip.hardEdge, children: [ _buildPanelBackground(), _buildHeader(), _buildColumnTitles(), - _buildRecordList(), + if (isEmpty) _buildEmptyState() else _buildRecordList(), _buildFooter(), _buildCloseButton(), ], @@ -227,21 +270,81 @@ class _RecordDialogCanvas extends StatelessWidget { child: ClipRect( child: ScrollConfiguration( behavior: const _NoGlowScrollBehavior(), - child: ListView.builder( - padding: EdgeInsets.zero, - primary: false, - physics: const BouncingScrollPhysics(), - itemExtent: s(61), - itemCount: records.length, - itemBuilder: (context, index) { - return _RecordRow(scale: scale, item: records[index]); - }, + child: NotificationListener( + onNotification: _handleScrollNotification, + child: ListView.builder( + padding: EdgeInsets.zero, + primary: false, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + 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() { return Stack( children: [ @@ -391,7 +494,9 @@ class _RecordRow extends StatelessWidget { child: ClipRect( child: SingleChildScrollView( scrollDirection: Axis.horizontal, - physics: const BouncingScrollPhysics(), + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), child: Row( children: [ for (var index = 0; index < rewards.length; index++) ...[ @@ -429,28 +534,25 @@ class _SmallReward extends StatelessWidget { top: 0, width: 28 * scale, height: 28 * scale, - child: _RocketImage( - asset: item.asset ?? _RecordDialogAssets.rewardThumb, - imageUrl: item.imageUrl, - fit: BoxFit.cover, - ), + child: + item.isGoldReward + ? Image.asset(_RecordDialogAssets.coin, fit: BoxFit.contain) + : _RocketImage( + asset: item.asset ?? _RecordDialogAssets.rewardThumb, + imageUrl: item.imageUrl, + fit: BoxFit.contain, + ), ), Positioned( - left: 0, + left: -3 * scale, top: 30 * scale, - width: 8 * scale, - height: 8 * scale, - child: Image.asset(_RecordDialogAssets.coin, fit: BoxFit.contain), - ), - Positioned( - left: 8 * scale, - top: 30 * scale, - width: 26 * scale, + width: 34 * scale, child: Text( - item.amount, + item.displayText, textScaler: TextScaler.noScaling, maxLines: 1, - overflow: TextOverflow.visible, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, style: TextStyle( color: Colors.white, 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 { const _RecordDialogAssets._(); @@ -681,6 +807,7 @@ class _RecordDialogAssets { 'sc_images/room/rocket/reward_dialog_bottom.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 empty = 'sc_images/room/sc_icon_room_music_empty.png'; } const double _designWidth = 375; diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart index ab6d8f4..b9db84e 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog.dart @@ -1,6 +1,7 @@ import 'dart:math' as math; 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/widgets/room/rocket/room_rocket_dialog.dart'; @@ -11,6 +12,9 @@ class RoomRocketRewardDialog extends StatelessWidget { this.rewards, this.winners, this.loadingText = 'Loading data......', + this.isLoadingMore = false, + this.hasMore = false, + this.onLoadMore, this.useMockFallback = true, this.onClose, }); @@ -19,11 +23,21 @@ class RoomRocketRewardDialog extends StatelessWidget { final List? rewards; final List? winners; final String loadingText; + final bool isLoadingMore; + final bool hasMore; + final VoidCallback? onLoadMore; final bool useMockFallback; final VoidCallback? onClose; @override 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( selectedReward, rewards, @@ -65,6 +79,11 @@ class RoomRocketRewardDialog extends StatelessWidget { rewards: resolvedRewards, winners: resolvedWinners, loadingText: loadingText, + isLoadingMore: isLoadingMore, + hasMore: hasMore, + onLoadMore: onLoadMore, + emptyText: emptyText, + contentDirection: contentDirection, onClose: onClose, ), ), @@ -89,7 +108,7 @@ class RoomRocketRewardDialog extends StatelessWidget { if (source.isNotEmpty) { return source; } - if (!useMockFallback && selectedReward == null) { + if (!useMockFallback) { return const []; } final fallback = @@ -153,6 +172,18 @@ class RoomRocketRewardDialog extends StatelessWidget { ); }, 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 { @@ -187,6 +218,11 @@ class _RewardDialogCanvas extends StatelessWidget { required this.rewards, required this.winners, required this.loadingText, + required this.isLoadingMore, + required this.hasMore, + this.onLoadMore, + required this.emptyText, + required this.contentDirection, this.onClose, }); @@ -194,19 +230,30 @@ class _RewardDialogCanvas extends StatelessWidget { final List rewards; final List winners; final String loadingText; + final bool isLoadingMore; + final bool hasMore; + final VoidCallback? onLoadMore; + final String emptyText; + final TextDirection contentDirection; final VoidCallback? onClose; double s(double value) => value * scale; @override Widget build(BuildContext context) { + final isLoading = loadingText.trim().isNotEmpty; + final isEmpty = !isLoading && rewards.isEmpty && winners.isEmpty; return Stack( clipBehavior: Clip.hardEdge, children: [ _buildPanelBackground(), - _buildHeader(), - _buildRewardGrid(), - _buildWinnerList(), + _buildHeader(showSections: !isEmpty), + if (isEmpty) + _buildEmptyState() + else ...[ + _buildRewardGrid(), + _buildWinnerList(), + ], _buildLoadingText(), _buildCloseButton(), ], @@ -233,7 +280,7 @@ class _RewardDialogCanvas extends StatelessWidget { ); } - Widget _buildHeader() { + Widget _buildHeader({required bool showSections}) { return Stack( children: [ Positioned( @@ -258,48 +305,112 @@ class _RewardDialogCanvas extends StatelessWidget { ), ), ), - Positioned( - left: s(0), - top: s(230), - width: s(_designWidth), - child: _SectionTitle( - text: 'Congratulations on winning', - scale: scale, + if (showSections) ...[ + Positioned( + left: s(0), + top: s(230), + width: s(_designWidth), + child: _SectionTitle( + text: 'Congratulations on winning', + scale: scale, + ), ), - ), - Positioned( - left: s(0), - top: s(482), - width: s(_designWidth), - child: _SectionTitle(text: '12 users received rewards', scale: scale), - ), + Positioned( + left: s(0), + top: s(482), + width: s(_designWidth), + child: _SectionTitle( + 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() { if (rewards.isEmpty) { return const SizedBox.shrink(); } - final visibleRewards = - rewards.length >= 6 - ? rewards.take(6).toList(growable: false) - : List.generate( - 6, - (index) => rewards[index % rewards.length], - growable: false, - ); - return Stack( - children: [ - for (var index = 0; index < visibleRewards.length; index++) - Positioned( - left: s(28 + (index % 3) * 112), - top: s(index < 3 ? 256 : 365), - width: s(97), - height: s(101), - child: _RewardDetailCard(scale: scale, item: visibleRewards[index]), + final columns = math.max(3, (rewards.length / 2).ceil()); + return Positioned( + left: s(28), + top: s(256), + width: s(336), + height: s(210), + child: ClipRect( + child: ScrollConfiguration( + behavior: const _NoGlowScrollBehavior(), + child: NotificationListener( + onNotification: _handleHorizontalScrollNotification, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + 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: ScrollConfiguration( behavior: const _NoGlowScrollBehavior(), - child: ListView.builder( - padding: EdgeInsets.zero, - primary: false, - physics: const BouncingScrollPhysics(), - itemExtent: s(51), - itemCount: winners.length, - itemBuilder: (context, index) { - return _WinnerRow( - scale: scale, - winner: winners[index], - showRocketBadge: index == 0, - ); - }, + child: NotificationListener( + onNotification: _handleScrollNotification, + child: ListView.builder( + padding: EdgeInsets.zero, + primary: false, + physics: const BouncingScrollPhysics( + parent: AlwaysScrollableScrollPhysics(), + ), + itemExtent: s(51), + itemCount: winners.length + (isLoadingMore ? 1 : 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() { return Positioned( left: 0, @@ -421,28 +566,25 @@ class _RewardDetailCard extends StatelessWidget { top: 11 * scale, width: 50 * scale, height: 50 * scale, - child: _RocketImage( - asset: item.asset ?? _RewardDialogAssets.rewardThumb, - imageUrl: item.imageUrl, - fit: BoxFit.cover, - ), + child: + item.isGoldReward + ? Image.asset(_RewardDialogAssets.coin, fit: BoxFit.contain) + : _RocketImage( + asset: item.asset ?? _RewardDialogAssets.rewardThumb, + imageUrl: item.imageUrl, + fit: BoxFit.contain, + ), ), Positioned( - left: 25 * scale, - top: 74 * scale, - width: 12 * scale, - height: 12 * scale, - child: Image.asset(_RewardDialogAssets.coin, fit: BoxFit.contain), - ), - Positioned( - left: 39 * scale, + left: 21 * scale, top: 73 * scale, - width: 44 * scale, + width: 56 * scale, child: Text( - item.amount, + item.displayText, textScaler: TextScaler.noScaling, maxLines: 1, overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 12 * scale, @@ -469,7 +611,7 @@ class _WinnerRow extends StatelessWidget { @override Widget build(BuildContext context) { - final rewards = winner.rewards.take(4).toList(growable: false); + final rewards = winner.rewards; return SizedBox( height: 51 * scale, child: Stack( @@ -522,14 +664,33 @@ class _WinnerRow extends StatelessWidget { ), ), ), - for (var index = 0; index < rewards.length; index++) - Positioned( - left: (202 + index * 36) * scale, - top: 2 * scale, - width: 27 * scale, - height: 32 * scale, - child: _SmallReward(scale: scale, item: rewards[index]), + Positioned( + left: 202 * scale, + top: 2 * scale, + width: 134 * scale, + height: 36 * scale, + child: ClipRect( + 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, width: 22 * scale, height: 22 * scale, - child: _RocketImage( - asset: item.asset ?? _RewardDialogAssets.rewardThumb, - imageUrl: item.imageUrl, - fit: BoxFit.cover, - ), + child: + item.isGoldReward + ? Image.asset(_RewardDialogAssets.coin, fit: BoxFit.contain) + : _RocketImage( + asset: item.asset ?? _RewardDialogAssets.rewardThumb, + imageUrl: item.imageUrl, + fit: BoxFit.contain, + ), ), Positioned( - left: 0, - top: 25 * scale, - width: 6 * scale, - height: 6 * scale, - child: Image.asset(_RewardDialogAssets.coin, fit: BoxFit.contain), - ), - Positioned( - left: 7 * scale, + left: -1 * scale, top: 24 * scale, - width: 22 * scale, + width: 29 * scale, child: Text( - _compactAmount(item.amount), + _compactAmount(item.displayText), textScaler: TextScaler.noScaling, maxLines: 1, overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, style: TextStyle( color: Colors.white, 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 { @override void paint(Canvas canvas, Size size) { @@ -917,6 +1099,7 @@ class _RewardDialogAssets { 'sc_images/room/rocket/reward_dialog_bottom.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 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 avatar2 = 'sc_images/room/rocket/rocket_rank_frame_2.png'; static const String avatar3 = 'sc_images/room/rocket/rocket_rank_frame_3.png'; diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart new file mode 100644 index 0000000..be89a38 --- /dev/null +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_reward_dialog_loader.dart @@ -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 [], + this.onClose, + }); + + final String roomId; + final RoomRocketRewardItem? selectedReward; + final List initialPopupRecords; + final VoidCallback? onClose; + + @override + State createState() => + _RoomRocketRewardDialogLoaderState(); +} + +class _RoomRocketRewardDialogLoaderState + extends State { + final List _popupRecords = []; + final List _roomRecords = []; + List? _rewards; + List? _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 _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> _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> _loadRoomRewardRecords( + int cursor, + ) async { + if (widget.roomId.trim().isEmpty) { + return const []; + } + 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 []; + } + } + + List _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'); +} diff --git a/lib/ui_kit/widgets/room/room_msg_item.dart b/lib/ui_kit/widgets/room/room_msg_item.dart index 3eb6936..321ed06 100644 --- a/lib/ui_kit/widgets/room/room_msg_item.dart +++ b/lib/ui_kit/widgets/room/room_msg_item.dart @@ -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/modules/index/main_route.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/red_packet/room_red_packet_chat_message.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart'; @@ -1167,31 +1168,25 @@ class _MsgItemState extends State { ); } - Widget _buildMedals(List wearBadge) { - return wearBadge.isNotEmpty - ? Expanded( - child: SingleChildScrollView( - child: SizedBox( - height: 25.w, - child: ListView.separated( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - itemCount: wearBadge.length, - itemBuilder: (context, index) { - return netImage( - width: 25.w, - height: 25.w, - url: wearBadge[index].selectUrl ?? "", - ); - }, - separatorBuilder: (BuildContext context, int index) { - return SizedBox(width: 5.w); - }, - ), - ), - ), - ) - : Container(); + Widget _buildMedals({ + required String? userId, + List fallbackBadges = const [], + }) { + final hasUserId = (userId ?? "").trim().isNotEmpty; + if (!hasUserId && fallbackBadges.isEmpty) { + return Container(); + } + return Expanded( + child: SCUserBadgeStrip( + userId: userId, + fallbackBadges: fallbackBadges, + height: 25.w, + badgeHeight: 25.w, + longBadgeWidth: 55.w, + spacing: 5.w, + maxBadges: 8, + ), + ); } int _msgColor(String type) { @@ -1773,10 +1768,12 @@ class _MsgItemState extends State { ) : Container(), _buildMedals( - (widget.msg.user?.wearBadge?.where((item) { - return item.use ?? false; - }).toList() ?? - []), + userId: widget.msg.user?.id, + fallbackBadges: + widget.msg.user?.wearBadge + ?.where((item) => item.use ?? false) + .toList() ?? + const [], ), ], ), @@ -1853,10 +1850,12 @@ class _MsgItemState extends State { ) : Container(), _buildMedals( - user.wearBadge?.where((item) { - return item.use ?? false; - }).toList() ?? - [], + userId: user.id, + fallbackBadges: + user.wearBadge + ?.where((item) => item.use ?? false) + .toList() ?? + const [], ), ], ), diff --git a/lib/ui_kit/widgets/room/room_password_dialog.dart b/lib/ui_kit/widgets/room/room_password_dialog.dart new file mode 100644 index 0000000..c6d62be --- /dev/null +++ b/lib/ui_kit/widgets/room/room_password_dialog.dart @@ -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 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 onSuccess; + + static Future show({required RoomPasswordSubmit onSubmit}) { + final completer = Completer(); + 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 createState() => _RoomPasswordDialogState(); +} + +class _RoomPasswordDialogState extends State { + final TextEditingController _passwordController = TextEditingController(); + String _errorText = ""; + bool _isSubmitting = false; + + @override + void dispose() { + _passwordController.dispose(); + super.dispose(); + } + + Future _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), + ); + } +} diff --git a/lib/ui_kit/widgets/room/room_play_widget.dart b/lib/ui_kit/widgets/room/room_play_widget.dart index f41879e..9947d30 100644 --- a/lib/ui_kit/widgets/room/room_play_widget.dart +++ b/lib/ui_kit/widgets/room/room_play_widget.dart @@ -6,6 +6,7 @@ import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/services/audio/rtc_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/widgets/room/music/room_music_floating_entry.dart'; import 'package:yumi/ui_kit/widgets/room/red_packet/room_red_packet_models.dart'; @@ -35,9 +36,15 @@ class _RoomPlayWidgetState extends State { bottom: (150 - _rightActionDownShift).w, child: const RoomMusicFloatingEntry(), ), - PositionedDirectional( - end: 65.w, - bottom: (150 - _rightActionDownShift).w, + Selector( + selector: (_, manager) => manager.current != null, + builder: (context, hasCurrentMusic, child) { + return PositionedDirectional( + end: (hasCurrentMusic ? 65 : 15).w, + bottom: (150 - _rightActionDownShift).w, + child: child!, + ); + }, child: const RoomRocketFloatingEntry(), ), PositionedDirectional( diff --git a/lib/ui_kit/widgets/room/room_user_info_card.dart b/lib/ui_kit/widgets/room/room_user_info_card.dart index 8cb75e7..ec5cac4 100644 --- a/lib/ui_kit/widgets/room/room_user_info_card.dart +++ b/lib/ui_kit/widgets/room/room_user_info_card.dart @@ -12,6 +12,7 @@ import 'package:yumi/modules/index/main_route.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_input.dart'; import 'package:yumi/ui_kit/widgets/room/room_user_card_setting.dart'; +import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart'; import 'package:marquee/marquee.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; @@ -49,35 +50,6 @@ class _RoomUserInfoCardState extends State { String roomId = ""; SCUserIdentityRes? userIdentity; - Widget _buildCardAction({ - required String iconAsset, - required String label, - required VoidCallback onTap, - }) { - return SCDebounceWidget( - onTap: onTap, - child: Column( - children: [ - Image.asset(iconAsset, width: 32.w, height: 32.w), - SizedBox(height: 5.w), - Container( - alignment: Alignment.center, - width: 70.w, - child: text( - label, - maxLines: 2, - textAlign: TextAlign.center, - textColor: Color(0xff808080), - fontWeight: FontWeight.w600, - fontSize: 14.sp, - lineHeight: 1.1, - ), - ), - ], - ), - ); - } - @override void initState() { super.initState(); @@ -93,6 +65,9 @@ class _RoomUserInfoCardState extends State { ""; userProvider?.roomUserCard(roomId, widget.userId ?? ""); SCAccountRepository().userIdentity(userId: widget.userId).then((v) { + if (!mounted) { + return; + } userIdentity = v; setState(() {}); }); @@ -106,635 +81,586 @@ class _RoomUserInfoCardState extends State { @override Widget build(BuildContext context) { - return SafeArea( - top: false, - child: Consumer( - builder: (context, ref, child) { - final currentUserId = - AccountStorage().getCurrentUser()?.userProfile?.id; - final isSelf = widget.userId == currentUserId; - final rtcProvider = Provider.of(context, listen: false); - final currentMicIndex = - isSelf && currentUserId != null - ? rtcProvider.userOnMaiInIndex(currentUserId) - : -1; - final canLeaveMic = currentMicIndex > -1; - if (ref.userCardInfo == null) { - return SizedBox( - height: ScreenUtil().screenHeight * 0.7, - child: Center( - child: Container( - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.circular(8), - ), - width: 55.w, - height: 55.w, - child: CupertinoActivityIndicator(color: Colors.white24), - ), - ), - ); - } - return Container( - constraints: BoxConstraints( - maxHeight: ScreenUtil().screenHeight * 0.7, - ), - margin: EdgeInsets.symmetric(horizontal: 15.w), - child: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Stack( - alignment: Alignment.center, - children: [ - Container( - margin: EdgeInsets.only( - top: - ref.userCardInfo?.userProfile - ?.getDataCard() - ?.sourceUrl != - null - ? 45.w - : 55.w, - ), - child: Stack( - children: [ - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: - ref.userCardInfo?.userProfile - ?.getDataCard() - ?.sourceUrl != - null - ? [ - Colors.transparent, - Colors.transparent, - ] - : [ - Color(0xff18F2B1), - Color(0xffE4FFF6), - Color(0xffE4FFF6), - Color(0xffE4FFF6), - Color(0xffE4FFF6), - Color(0xffE4FFF6), - Color(0xffE4FFF6), - Color(0xffFFFFFF), - ], - ), - borderRadius: BorderRadius.all( - Radius.circular(12.w), - ), - ), - child: Stack( - children: [ - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - SizedBox(height: 25.w), - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: - CrossAxisAlignment.center, - children: [ - SizedBox(width: 20.w), - GestureDetector( - child: head( - url: - ref - .userCardInfo - ?.userProfile - ?.userAvatar ?? - "", - width: 55.w, - border: Border.all( - color: Colors.white, - width: 2, - ), - headdress: - ref.userCardInfo?.userProfile - ?.getHeaddress() - ?.sourceUrl, - headdressCover: - ref.userCardInfo?.userProfile - ?.getHeaddress() - ?.cover, - ), - onTap: () { - SCNavigatorUtils.push( - context, - "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == widget.userId}&tageId=${widget.userId}", - ); - }, - ), - SizedBox(width: 3.w), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Row( - children: [ - msgRoleTag( - ref - .userCardInfo - ?.roomRole ?? - "", - width: 16.w, - height: 16.w, - ), - SizedBox(width: 2.w), - socialchatNickNameText( - maxWidth: 115.w, - ref - .userCardInfo - ?.userProfile - ?.userNickname ?? - "", - fontSize: 14.sp, - textColor: Colors.black, - fontWeight: FontWeight.bold, - type: "", - needScroll: - (ref - .userCardInfo - ?.userProfile - ?.userNickname - ?.characters - .length ?? - 0) > - 10, - ), - ], - ), - SizedBox(height: 2.w), - SCSpecialIdBadge( - idText: - ref - .userCardInfo - ?.userProfile - ?.getID() ?? - "", - showAnimated: - ref - .userCardInfo - ?.userProfile - ?.hasSpecialId() ?? - false, - assetPath: - SCSpecialIdAssets.userId, - animationWidth: 62.w, - animationHeight: 24.w, - showTextBesideAnimated: true, - animatedTextSpacing: 0, - showAnimatedGradientText: - ref - .userCardInfo - ?.userProfile - ?.shouldShowColoredSpecialIdText() ?? - false, - animationTextStyle: TextStyle( - color: Colors.black, - fontSize: 12.sp, - fontWeight: FontWeight.bold, - ), - normalTextStyle: TextStyle( - color: Colors.black, - fontSize: 12.sp, - fontWeight: FontWeight.bold, - ), - animationFit: BoxFit.contain, - ), - ], - ), - ], - ), - Row( - mainAxisAlignment: - MainAxisAlignment.start, - children: [ - Container( - decoration: BoxDecoration( - borderRadius: - BorderRadius.circular(15.w), - color: Colors.transparent, - ), - padding: EdgeInsets.symmetric( - horizontal: 10.w, - vertical: 5.w, - ), - child: Row( - children: [ - SizedBox(width: 20.w), - netImage( - url: - Provider.of< - SCAppGeneralManager - >( - context, - listen: false, - ) - .findCountryByName( - ref - .userCardInfo - ?.userProfile - ?.countryName ?? - "", - ) - ?.nationalFlag ?? - "", - borderRadius: - BorderRadius.all( - Radius.circular(3.w), - ), - width: 19.w, - height: 14.w, - ), - SizedBox(width: 5.w), - Container( - constraints: BoxConstraints( - maxWidth: 110.w, - ), - child: text( - ref - .userCardInfo - ?.userProfile - ?.countryName ?? - "", - textColor: Colors.black, - fontSize: 12.sp, - fontWeight: FontWeight.bold, - ), - ), - ], - ), - ), - SizedBox(width: 5.w), - Container( - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - ref - .userCardInfo - ?.userProfile - ?.userSex == - 0 - ? "sc_images/login/sc_icon_sex_woman_bg.png" - : "sc_images/login/sc_icon_sex_man_bg.png", - ), - ), - ), - padding: EdgeInsets.symmetric( - horizontal: 10.w, - vertical: 5.w, - ), - child: Row( - children: [ - xb( - ref - .userCardInfo - ?.userProfile - ?.userSex, - ), - SizedBox(width: 3.w), - text( - "${ref.userCardInfo?.userProfile?.age ?? 0}", - textColor: Colors.white, - fontSize: 12.sp, - fontWeight: FontWeight.bold, - ), - ], - ), - ), - ], - ), + final sheetHeight = MediaQuery.of(context).size.height * 0.4; - SizedBox(height: 15.w), - Container( - margin: EdgeInsets.symmetric( - horizontal: 25.w, - ), - child: - isSelf - ? (canLeaveMic - ? Row( - mainAxisAlignment: - MainAxisAlignment - .center, - children: [ - _buildCardAction( - iconAsset: - _leaveMicActionIconAsset, - label: - SCAppLocalizations.of( - context, - )!.leavelTheMic, - onTap: () { - Navigator.of( - context, - ).pop(); - rtcProvider.xiaMai( - currentMicIndex, - ); - }, - ), - ], - ) - : const SizedBox.shrink()) - : Row( - mainAxisAlignment: - MainAxisAlignment - .spaceBetween, - children: [ - _buildCardAction( - iconAsset: - "sc_images/room/sc_icon_send_user_message.png", - label: - SCAppLocalizations.of( - context, - )!.sayHi2, - onTap: () async { - Navigator.of( - context, - ).pop(); - var conversation = - V2TimConversation( - type: - ConversationType - .V2TIM_C2C, - userID: - widget.userId, - conversationID: - '', - ); - var bool = - await Provider.of< - RtmProvider - >( - context, - listen: false, - ).startConversation( - conversation, - ); - if (!bool) return; - var json = jsonEncode( - conversation.toJson(), - ); - SCNavigatorUtils.push( - navigatorKey - .currentState! - .context, - "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", - ); - }, - ), - SCDebounceWidget( - child: Selector< - SocialChatUserProfileManager, - bool - >( - selector: - ( - context, - provider, - ) => - provider - .userCardInfo - ?.follow ?? - false, - shouldRebuild: - (prev, next) => - prev != next, - builder: ( - context, - follow, - _, - ) { - return Column( - children: [ - Image.asset( - follow - ? "sc_images/room/sc_icon_user_un_follow.png" - : "sc_images/room/sc_icon_user_follow.png", - width: 32.w, - height: 32.w, - ), - SizedBox( - height: 5.w, - ), - Container( - alignment: - Alignment - .center, - width: 70.w, - child: text( - follow - ? SCAppLocalizations.of( - context, - )!.followed - : SCAppLocalizations.of( - context, - )!.follow, - fontSize: - 14.sp, - textColor: Color( - 0xff808080, - ), - fontWeight: - FontWeight - .bold, - ), - ), - ], - ); - }, - ), - onTap: () { - final provider = - Provider.of< - SocialChatUserProfileManager - >( - context, - listen: false, - ); - provider.followUser( - widget.userId ?? "", - ); - }, - ), - _buildCardAction( - iconAsset: - "sc_images/room/sc_icon_at_tag_user.png", - label: - SCAppLocalizations.of( - context, - )!.atTag, - onTap: () async { - if (SCRoomUtils.touristCanMsg( - context, - )) { - if ((ref - .userCardInfo - ?.userProfile - ?.userNickname ?? - "") - .isEmpty) { - return; - } - Navigator.of( - context, - ).pop(); - Navigator.push( - context, - PopRoute( - child: RoomMsgInput( - atTextContent: - "${ref.userCardInfo?.userProfile?.userNickname}", - ), - ), - ); - } - }, - ), - _buildCardAction( - iconAsset: - "sc_images/room/sc_icon_send_user_gift.png", - label: - SCAppLocalizations.of( - context, - )!.send, - onTap: () { - Navigator.of( - context, - ).pop(); - SmartDialog.show( - tag: - "showGiftControl", - alignment: - Alignment - .bottomCenter, - maskColor: - Colors - .transparent, - animationType: - SmartAnimationType - .fade, - clickMaskDismiss: - true, - builder: (_) { - return GiftPage( - toUser: - ref - .userCardInfo - ?.userProfile, - ); - }, - ); - }, - ), - ], - ), - ), - SizedBox( - height: - isSelf - ? (canLeaveMic ? 35.w : 15.w) - : 35.w, - ), - ], - ), - ], - ), - ), - AccountStorage() - .getCurrentUser() - ?.userProfile - ?.id != - widget.userId - ? Positioned( - top: 28.w, - right: 15.w, - child: GestureDetector( - child: Image.asset( - "sc_images/room/sc_icon_user_card_report.png", - width: 24.w, - height: 24.w, - color: Colors.black, - ), - onTap: () { - SCNavigatorUtils.push( - context, - "${SCMainRoute.report}?type=user&tageId=${widget.userId}", - replace: false, - ); - }, - ), - ) - : Container(), - ref.userCardInfo?.roomRole == - SCRoomRolesType.HOMEOWNER.name - ? Container() - : (AccountStorage() - .getCurrentUser() - ?.userProfile - ?.id != - widget.userId && - Provider.of( - context, - listen: false, - ).isFz() || - AccountStorage() - .getCurrentUser() - ?.userProfile - ?.id != - widget.userId && - Provider.of( - context, - listen: false, - ).isGL()) - ? Positioned( - top: 28.w, - right: 45.w, - child: GestureDetector( - child: Image.asset( - "sc_images/room/sc_icon_room_user_card_setting.png", - width: 24.w, - height: 24.w, - color: Colors.black, - ), - onTap: () { - if (userProvider?.userCardInfo != null) { - Navigator.of(context).pop(); - showBottomInBottomDialog( - context!, - RoomUserCardSetting( - roomId: roomId, - userCardInfo: - userProvider?.userCardInfo - ?.copyWith(), - ), - ); - } - }, - ), - ) - : Container(), - ], - ), + return Consumer( + builder: (context, ref, child) { + final currentUserId = + AccountStorage().getCurrentUser()?.userProfile?.id; + final isSelf = widget.userId == currentUserId; + final rtcProvider = Provider.of(context, listen: false); + final currentMicIndex = + isSelf && currentUserId != null + ? rtcProvider.userOnMaiInIndex(currentUserId) + : -1; + final canLeaveMic = currentMicIndex > -1; + + if (ref.userCardInfo == null) { + return _buildLoadingSheet(sheetHeight); + } + + return _buildProfileSheet( + context: context, + ref: ref, + rtcProvider: rtcProvider, + sheetHeight: sheetHeight, + isSelf: isSelf, + currentMicIndex: currentMicIndex, + canLeaveMic: canLeaveMic, + currentUserId: currentUserId, + ); + }, + ); + } + + Widget _buildLoadingSheet(double sheetHeight) { + return SizedBox( + width: double.infinity, + height: sheetHeight, + child: ClipRRect( + borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), + child: DecoratedBox( + decoration: _sheetDecoration(), + child: Center( + child: Container( + width: 55.w, + height: 55.w, + decoration: BoxDecoration( + color: Colors.black26, + borderRadius: BorderRadius.circular(8.w), + ), + child: CupertinoActivityIndicator(color: Colors.white24), + ), + ), + ), + ), + ); + } + + Widget _buildProfileSheet({ + required BuildContext context, + required SocialChatUserProfileManager ref, + required RtcProvider rtcProvider, + required double sheetHeight, + required bool isSelf, + required num currentMicIndex, + required bool canLeaveMic, + required String? currentUserId, + }) { + final bottomPadding = MediaQuery.of(context).padding.bottom; + final profile = ref.userCardInfo?.userProfile; + final canShowReport = currentUserId != widget.userId; + final canShowSetting = + canShowReport && + ref.userCardInfo?.roomRole != SCRoomRolesType.HOMEOWNER.name && + (rtcProvider.isFz() || rtcProvider.isGL()); + + return SizedBox( + width: double.infinity, + height: sheetHeight, + child: ClipRRect( + borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), + child: DecoratedBox( + decoration: _sheetDecoration(), + child: Stack( + children: [ + Positioned.fill( + child: SingleChildScrollView( + physics: const ClampingScrollPhysics(), + padding: EdgeInsets.fromLTRB( + 18.w, + 16.w, + 18.w, + bottomPadding + 10.w, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildProfileHeader(context, profile, ref), + SizedBox(height: 6.w), + _buildInfoChips(context, profile), + SizedBox(height: 9.w), + _buildBadgeStrip(profile?.id ?? widget.userId), + SizedBox(height: 10.w), + _buildActions( + context: context, + ref: ref, + rtcProvider: rtcProvider, + isSelf: isSelf, + canLeaveMic: canLeaveMic, + currentMicIndex: currentMicIndex, ), ], ), - ], + ), + ), + if (canShowReport) + Positioned( + top: 14.w, + right: 16.w, + child: _buildHeaderIconButton( + iconAsset: "sc_images/room/sc_icon_user_card_report.png", + onTap: () { + final route = + "${SCMainRoute.report}?type=user&tageId=${widget.userId}"; + Navigator.of(context).pop(); + SCNavigatorUtils.push( + navigatorKey.currentState?.context ?? context, + route, + replace: false, + ); + }, + ), + ), + if (canShowSetting) + Positioned( + top: 14.w, + right: 52.w, + child: _buildHeaderIconButton( + iconAsset: + "sc_images/room/sc_icon_room_user_card_setting.png", + onTap: () { + if (userProvider?.userCardInfo == null) { + return; + } + Navigator.of(context).pop(); + showBottomInBottomDialog( + navigatorKey.currentState?.context ?? context, + RoomUserCardSetting( + roomId: roomId, + userCardInfo: userProvider?.userCardInfo?.copyWith(), + ), + ); + }, + ), + ), + ], + ), + ), + ), + ); + } + + BoxDecoration _sheetDecoration() { + return const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xff18F2B1), + Color(0xffE4FFF6), + Color(0xffF7FFFC), + Color(0xffFFFFFF), + ], + ), + ); + } + + Widget _buildProfileHeader( + BuildContext context, + SocialChatUserProfile? profile, + SocialChatUserProfileManager ref, + ) { + return Column( + children: [ + GestureDetector( + onTap: () => _openProfile(context), + child: head( + url: profile?.userAvatar ?? "", + width: 70.w, + border: Border.all(color: Colors.white, width: 2), + headdress: profile?.getHeaddress()?.sourceUrl, + headdressCover: profile?.getHeaddress()?.cover, + ), + ), + SizedBox(height: 5.w), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + msgRoleTag( + ref.userCardInfo?.roomRole ?? "", + width: 16.w, + height: 16.w, + ), + SizedBox(width: 4.w), + socialchatNickNameText( + maxWidth: 190.w, + profile?.userNickname ?? "", + fontSize: 18.sp, + textColor: Colors.black, + fontWeight: FontWeight.bold, + type: "", + needScroll: (profile?.userNickname?.characters.length ?? 0) > 12, + ), + ], + ), + SizedBox(height: 2.w), + SCSpecialIdBadge( + idText: profile?.getID() ?? "", + showAnimated: profile?.hasSpecialId() ?? false, + assetPath: SCSpecialIdAssets.userId, + animationWidth: 62.w, + animationHeight: 24.w, + showTextBesideAnimated: true, + animatedTextSpacing: 0, + showAnimatedGradientText: + profile?.shouldShowColoredSpecialIdText() ?? false, + animationTextStyle: TextStyle( + color: Colors.black, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + normalTextStyle: TextStyle( + color: Colors.black, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + animationFit: BoxFit.contain, + ), + ], + ); + } + + Widget _buildInfoChips(BuildContext context, SocialChatUserProfile? profile) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildCountryChip(context, profile), + SizedBox(width: 7.w), + _buildSexAgeChip(profile), + ], + ); + } + + Widget _buildCountryChip( + BuildContext context, + SocialChatUserProfile? profile, + ) { + final countryName = profile?.countryName ?? ""; + final flagUrl = + Provider.of( + context, + listen: false, + ).findCountryByName(countryName)?.nationalFlag ?? + ""; + + return Container( + constraints: BoxConstraints(maxWidth: 145.w), + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.55), + borderRadius: BorderRadius.circular(16.w), + border: Border.all( + color: const Color(0xff18F2B1).withValues(alpha: 0.18), + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + netImage( + url: flagUrl, + borderRadius: BorderRadius.all(Radius.circular(3.w)), + width: 19.w, + height: 14.w, + ), + SizedBox(width: 5.w), + Flexible( + child: text( + countryName, + maxLines: 1, + textColor: Colors.black, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ); + } + + Widget _buildSexAgeChip(SocialChatUserProfile? profile) { + return Container( + height: 26.w, + padding: EdgeInsets.symmetric(horizontal: 10.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + profile?.userSex == 0 + ? "sc_images/login/sc_icon_sex_woman_bg.png" + : "sc_images/login/sc_icon_sex_man_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + xb(profile?.userSex), + SizedBox(width: 3.w), + text( + "${profile?.age ?? 0}", + textColor: Colors.white, + fontSize: 12.sp, + fontWeight: FontWeight.bold, + ), + ], + ), + ); + } + + Widget _buildBadgeStrip(String? userId) { + return SCUserBadgeStrip( + userId: userId, + height: 55.w, + badgeHeight: 23.w, + longBadgeWidth: 58.w, + spacing: 8.w, + maxBadges: 12, + wrap: true, + reserveSpace: true, + ); + } + + Widget _buildActions({ + required BuildContext context, + required SocialChatUserProfileManager ref, + required RtcProvider rtcProvider, + required bool isSelf, + required bool canLeaveMic, + required num currentMicIndex, + }) { + if (isSelf) { + if (!canLeaveMic) { + return SizedBox(height: 55.w); + } + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SizedBox( + width: 92.w, + child: _buildSheetAction( + iconAsset: _leaveMicActionIconAsset, + label: SCAppLocalizations.of(context)!.leavelTheMic, + onTap: () { + Navigator.of(context).pop(); + rtcProvider.xiaMai(currentMicIndex); + }, + ), + ), + ], + ); + } + + return Row( + children: [ + Expanded( + child: _buildSheetAction( + iconAsset: "sc_images/room/sc_icon_send_user_message.png", + label: SCAppLocalizations.of(context)!.sayHi2, + onTap: () => _openChat(context), + ), + ), + Expanded(child: _buildFollowAction(context)), + Expanded( + child: _buildSheetAction( + iconAsset: "sc_images/room/sc_icon_at_tag_user.png", + label: SCAppLocalizations.of(context)!.atTag, + onTap: () => _openAtInput(context, ref), + ), + ), + Expanded( + child: _buildSheetAction( + iconAsset: "sc_images/room/sc_icon_send_user_gift.png", + label: SCAppLocalizations.of(context)!.send, + onTap: () => _openGift(context, ref), + ), + ), + ], + ); + } + + Widget _buildSheetAction({ + required String iconAsset, + required String label, + required VoidCallback onTap, + }) { + return SCDebounceWidget( + onTap: onTap, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 42.w, + height: 42.w, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.58), + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xff18F2B1).withValues(alpha: 0.18), ), ), + alignment: Alignment.center, + child: Image.asset(iconAsset, width: 28.w, height: 28.w), + ), + SizedBox(height: 5.w), + SizedBox( + height: 31.w, + child: text( + label, + maxLines: 2, + textAlign: TextAlign.center, + textColor: const Color(0xff666666), + fontWeight: FontWeight.w600, + fontSize: 12.sp, + lineHeight: 1.1, + ), + ), + ], + ), + ); + } + + Widget _buildFollowAction(BuildContext context) { + return SCDebounceWidget( + onTap: () { + final provider = Provider.of( + context, + listen: false, + ); + provider.followUser(widget.userId ?? ""); + }, + child: Selector( + selector: (context, provider) => provider.userCardInfo?.follow ?? false, + shouldRebuild: (prev, next) => prev != next, + builder: (context, follow, _) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 42.w, + height: 42.w, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.58), + shape: BoxShape.circle, + border: Border.all( + color: const Color(0xff18F2B1).withValues(alpha: 0.18), + ), + ), + alignment: Alignment.center, + child: Image.asset( + follow + ? "sc_images/room/sc_icon_user_un_follow.png" + : "sc_images/room/sc_icon_user_follow.png", + width: 28.w, + height: 28.w, + ), + ), + SizedBox(height: 5.w), + SizedBox( + height: 31.w, + child: text( + follow + ? SCAppLocalizations.of(context)!.followed + : SCAppLocalizations.of(context)!.follow, + maxLines: 2, + textAlign: TextAlign.center, + textColor: const Color(0xff666666), + fontWeight: FontWeight.w600, + fontSize: 12.sp, + lineHeight: 1.1, + ), + ), + ], ); }, ), ); } + Widget _buildHeaderIconButton({ + required String iconAsset, + required VoidCallback onTap, + }) { + return SCDebounceWidget( + onTap: onTap, + child: Container( + width: 28.w, + height: 28.w, + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.45), + shape: BoxShape.circle, + ), + alignment: Alignment.center, + child: Image.asset( + iconAsset, + width: 18.w, + height: 18.w, + color: Colors.black, + ), + ), + ); + } + + void _openProfile(BuildContext context) { + final route = + "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == widget.userId}&tageId=${widget.userId}"; + Navigator.of(context).pop(); + SCNavigatorUtils.push(navigatorKey.currentState?.context ?? context, route); + } + + Future _openChat(BuildContext context) async { + final rtmProvider = Provider.of(context, listen: false); + Navigator.of(context).pop(); + var conversation = V2TimConversation( + type: ConversationType.V2TIM_C2C, + userID: widget.userId, + conversationID: '', + ); + final didStart = await rtmProvider.startConversation(conversation); + if (!didStart || !mounted) return; + var json = jsonEncode(conversation.toJson()); + SCNavigatorUtils.push( + navigatorKey.currentState!.context, + "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", + ); + } + + Future _openAtInput( + BuildContext context, + SocialChatUserProfileManager ref, + ) async { + if (!SCRoomUtils.touristCanMsg(context)) { + return; + } + final nickname = ref.userCardInfo?.userProfile?.userNickname ?? ""; + if (nickname.isEmpty) { + return; + } + Navigator.of(context).pop(); + Navigator.push( + navigatorKey.currentState?.context ?? context, + PopRoute( + child: RoomMsgInput( + atTextContent: "$nickname", + ), + ), + ); + } + + void _openGift(BuildContext context, SocialChatUserProfileManager ref) { + Navigator.of(context).pop(); + SmartDialog.show( + tag: "showGiftControl", + alignment: Alignment.bottomCenter, + maskColor: Colors.transparent, + animationType: SmartAnimationType.fade, + clickMaskDismiss: true, + builder: (_) { + return GiftPage(toUser: ref.userCardInfo?.userProfile); + }, + ); + } + String roleName(String role) { if (role == SCRoomRolesType.HOMEOWNER.name) { return SCAppLocalizations.of(context)!.owner; diff --git a/pubspec.lock b/pubspec.lock index 97547cc..c1e1601 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -635,6 +635,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted 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: dependency: "direct main" description: @@ -1137,6 +1145,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted 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: dependency: "direct main" description: @@ -1645,6 +1661,30 @@ packages: url: "https://pub.flutter-io.cn" source: hosted 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: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index d57aa39..2c4a4ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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 # 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. -version: 1.3.1+131 +version: 1.4.0+140 environment: @@ -133,6 +133,8 @@ dependencies: readmore: ^3.0.0 #外链跳转 app_links: ^6.4.1 + flutter_svg: ^2.3.0 + pag: ^1.0.7 dev_dependencies: flutter_launcher_icons: ^0.14.4 diff --git a/sc_images/room/rocket/rocket_floating_banner_bg.png b/sc_images/room/rocket/rocket_floating_banner_bg.png new file mode 100644 index 0000000000000000000000000000000000000000..71ae137722bf8024c056ef58a0198f298017aacd GIT binary patch literal 155590 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�f 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�Jv^8PyrUDr&h25@wp^o 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 JPDHLkV1hV%bs7Kw literal 0 HcmV?d00001