import 'dart:async'; import 'dart:collection'; import 'dart:convert'; import 'dart:ui' as ui; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_debouncer/flutter_debouncer.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:yumi/app_localizations.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/ui_kit/theme/socialchat_theme.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/config/business_logic_strategy.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/remote/net/api.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart'; import 'package:yumi/main.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/shared/tools/sc_dialog_utils.dart'; import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/ui_kit/widgets/gift/sc_gift_combo_send_button.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import 'package:yumi/modules/gift/gift_tab_page.dart'; import 'package:yumi/modules/gift/cp_rights/cp_rights_guide_page.dart'; import 'package:yumi/services/gift/room_gift_combo_send_controller.dart'; import 'package:yumi/ui_kit/widgets/room/room_recharge_bottom_sheet.dart'; import '../../shared/data_sources/models/enum/sc_gift_type.dart'; import '../../shared/data_sources/models/message/sc_floating_message.dart'; import '../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; class _GiftPageTabItem { const _GiftPageTabItem({required this.type, required this.label}); final String type; final String label; } class GiftPage extends StatefulWidget { final SocialChatUserProfile? toUser; final String? initialTabType; const GiftPage({super.key, this.toUser, this.initialTabType}); @override State createState() => _GiftPageState(); } class _GiftPageState extends State with TickerProviderStateMixin { static const String _cpProfileAssetBase = "sc_images/room/cp_profile"; static const String _firstRechargeGiftEntryCard = "sc_images/first_recharge/room_gift_entry_card.png"; static const String _backpackGiftTab = SCAppGeneralManager.backpackGiftTab; static const Duration _comboSendBatchWindow = Duration(milliseconds: 200); static const List _preferredGiftTabOrder = [ "ALL", _backpackGiftTab, "ACTIVITY", "LUCKY_GIFT", "CP", "MAGIC", "NSCIONAL_FLAG", ]; TabController? _tabController; List _tabTypes = []; bool _didApplyInitialTab = false; /// 业务逻辑策略访问器 BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; // int checkedIndex = 0; SocialChatGiftRes? checkedGift; final Map _selectedGiftByTab = {}; RtcProvider? rtcProvider; bool isAll = false; List listMai = []; bool noShowNumber = false; ///选中人员 Set set = {}; ///数量的箭头是否朝上 bool isNumberUp = true; ///数量 int number = 1; int giveType = 1; int giftType = 0; Debouncer debouncer = Debouncer(); final ListQueue<_PendingGiftSendBatch> _comboSendBatchQueue = ListQueue<_PendingGiftSendBatch>(); Timer? _comboSendBatchTimer; bool _isComboSendBatchInFlight = false; void _giftFxLog(String message) { debugPrint('[GiftFx][gift] $message'); } String _describeGiftSendError(Object error) { if (error is DioException) { final requestPath = error.requestOptions.path; final statusCode = error.response?.statusCode; final responseData = error.response?.data; return 'dioType=${error.type} ' 'statusCode=$statusCode ' 'path=$requestPath ' 'message=${error.message} ' 'error=${error.error} ' 'response=$responseData'; } if (error is NotSuccessException) { return 'notSuccess message=${error.message}'; } return 'type=${error.runtimeType} error=$error'; } void _applyGiftSelection(SocialChatGiftRes? gift, {bool notify = true}) { checkedGift = gift; if (gift != null && (gift.giftSourceUrl ?? "").isNotEmpty && scGiftHasFullScreenEffect(gift.special)) { SCGiftVapSvgaManager().preload(gift.giftSourceUrl!); _giftFxLog( 'preload selected gift ' 'giftId=${gift.id} ' 'giftName=${gift.giftName} ' 'giftSourceUrl=${gift.giftSourceUrl} ' 'special=${gift.special}', ); } number = 1; noShowNumber = false; giftType = _giftTypeFromTab(gift?.giftTab); if (_isCpGift(gift)) { _normalizeCpRecipientSelection(); } if (notify) { setState(() {}); } } void _handleGiftSelected(String tabType, SocialChatGiftRes? gift) { _selectedGiftByTab[tabType] = gift; _applyGiftSelection(gift); } @override void initState() { super.initState(); rtcProvider = Provider.of(context, listen: false); Provider.of( context, listen: false, ).giftList(includeCustomized: true); Provider.of(context, listen: false).giftActivityList(); unawaited( Provider.of( context, listen: false, ).giftBackpack(forceRefresh: true), ); Provider.of(context, listen: false).balance(); rtcProvider?.roomWheatMap.forEach((k, v) { if (v.user != null) { listMai.add(HeadSelect(widget.toUser == null, v)); } }); isAll = widget.toUser == null && listMai.isNotEmpty && listMai.every((mai) => mai.isSelect); } @override void dispose() { _comboSendBatchTimer?.cancel(); if (_comboSendBatchQueue.isNotEmpty) { unawaited(_flushAllPendingComboGiftSends()); } _tabController?.removeListener(_handleTabChanged); _tabController?.dispose(); super.dispose(); } void _handleTabChanged() { final controller = _tabController; if (!mounted || controller == null || controller.index >= _tabTypes.length) { return; } final ref = Provider.of(context, listen: false); _syncSelectedGiftForTab(ref, _tabTypes[controller.index]); _normalizeCpRecipientSelection(notify: true); } bool get _isCurrentCpGiftTab { final controller = _tabController; if (controller == null || controller.index >= _tabTypes.length) { return false; } return _isCpGiftTab(_tabTypes[controller.index]); } bool _isCpGiftTab(String tabType) => scIsCpGiftTab(tabType); bool _isCpGift(SocialChatGiftRes? gift) => scIsCpGift(gift); bool get _isCpRecipientSingleMode => _isCurrentCpGiftTab || _isCpGift(checkedGift); void _normalizeCpRecipientSelection({bool notify = false}) { if (widget.toUser != null || (!_isCurrentCpGiftTab && !_isCpGift(checkedGift))) { return; } var changed = false; var keptSelected = false; for (final mai in listMai) { if (!mai.isSelect) { continue; } if (!keptSelected) { keptSelected = true; continue; } mai.isSelect = false; changed = true; } if (isAll) { isAll = false; changed = true; } if (giveType == 2) { giveType = 1; changed = true; } if (notify && changed && mounted) { setState(() {}); } } List<_GiftPageTabItem> _buildGiftTabs( BuildContext context, SCAppGeneralManager ref, ) { final localizations = SCAppLocalizations.of(context)!; final availableTypes = ref.giftByTab.entries .where((entry) => entry.value.isNotEmpty) .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); } } orderedTypes.addAll(availableTypes); if (orderedTypes.isEmpty) { orderedTypes.add("ALL"); } return orderedTypes .map( (type) => _GiftPageTabItem( type: type, label: _giftTabLabel(localizations, type), ), ) .toList(); } void _ensureTabController( SCAppGeneralManager ref, List<_GiftPageTabItem> tabs, ) { final nextTypes = tabs.map((tab) => tab.type).toList(); if (_tabController != null && listEquals(_tabTypes, nextTypes)) { _applyInitialTabIfNeeded(nextTypes); return; } final currentType = (_tabController != null && _tabTypes.isNotEmpty && _tabController!.index < _tabTypes.length) ? _tabTypes[_tabController!.index] : null; final requestedType = widget.initialTabType?.trim(); final requestedIndex = !_didApplyInitialTab && (requestedType?.isNotEmpty ?? false) ? nextTypes.indexOf(requestedType!) : -1; final nextIndex = requestedIndex >= 0 ? requestedIndex : currentType != null ? nextTypes.indexOf(currentType) : 0; if (requestedIndex >= 0) { _didApplyInitialTab = true; } _tabController?.removeListener(_handleTabChanged); _tabController?.dispose(); _tabTypes = nextTypes; _tabController = TabController( length: tabs.length, vsync: this, initialIndex: nextIndex >= 0 ? nextIndex : 0, )..addListener(_handleTabChanged); _syncSelectedGiftForTab( ref, _tabTypes[_tabController!.index], notify: false, ); } void _applyInitialTabIfNeeded(List tabTypes) { final requestedType = widget.initialTabType?.trim(); if (_didApplyInitialTab || requestedType == null || requestedType.isEmpty) { return; } final requestedIndex = tabTypes.indexOf(requestedType); final controller = _tabController; if (requestedIndex < 0 || controller == null) { return; } _didApplyInitialTab = true; if (controller.index != requestedIndex) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted || _tabController != controller) { return; } controller.animateTo(requestedIndex); }); } } void _syncSelectedGiftForTab( SCAppGeneralManager ref, String tabType, { bool notify = true, }) { final gifts = ref.giftByTab[tabType] ?? const []; final gift = _resolveSelectedGift(tabType, gifts); _selectedGiftByTab[tabType] = gift; _applyGiftSelection(gift, notify: notify); } void _ensureCurrentTabSelection(SCAppGeneralManager ref, String tabType) { final gifts = ref.giftByTab[tabType] ?? const []; if (_matchesCurrentGift(tabType, gifts)) { if (_isCpGiftTab(tabType)) { _normalizeCpRecipientSelection(); } return; } final gift = _resolveSelectedGift(tabType, gifts); _selectedGiftByTab[tabType] = gift; _applyGiftSelection(gift, notify: false); if (_isCpGiftTab(tabType)) { _normalizeCpRecipientSelection(); } } SocialChatGiftRes? _resolveSelectedGift( String tabType, List gifts, ) { if (gifts.isEmpty) { return null; } 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; } } } if (tabType == "CUSTOMIZED") { return gifts.length > 1 ? gifts[1] : null; } return gifts.first; } bool _matchesCurrentGift(String tabType, List gifts) { final currentGiftId = checkedGift?.id; if (currentGiftId == null) { return gifts.isEmpty; } 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) { switch (tabType) { case "ALL": return localizations.all; case "ACTIVITY": return localizations.activity; case "LUCK": case "LUCKY_GIFT": return localizations.luck; case "CP": return "CP"; case "MAGIC": return localizations.magic; case "CUSTOMIZED": return localizations.customized; case "NSCIONAL_FLAG": return localizations.country; case _backpackGiftTab: return localizations.bag; default: return tabType .toLowerCase() .split("_") .map( (word) => word.isEmpty ? word : "${word[0].toUpperCase()}${word.substring(1)}", ) .join(" "); } } int _giftTypeFromTab(String? tabType) { switch (tabType) { case "ACTIVITY": return 1; case "LUCK": case "LUCKY_GIFT": return 2; case "CP": return 3; case "MAGIC": return 5; default: return 0; } } bool _usesLuckyGiftEndpoint(SocialChatGiftRes? gift) { final giftTab = (gift?.giftTab ?? '').trim(); return giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name || 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'; } String _resolveGiftSendErrorMessage(Object error) { String sanitize(String message) { var value = message.trim(); const prefixes = [ 'Exception: ', 'DioException: ', 'DioException [unknown]: ', ]; for (final prefix in prefixes) { if (value.startsWith(prefix)) { value = value.substring(prefix.length).trim(); } } return value; } if (error is DioException) { final responseData = error.response?.data; if (responseData is Map) { final responseMessage = sanitize( responseData['errorMsg']?.toString() ?? '', ); if (responseMessage.isNotEmpty) { return responseMessage; } } if (error.error is NotSuccessException) { final responseMessage = sanitize( (error.error as NotSuccessException).message, ); if (responseMessage.isNotEmpty) { return responseMessage; } } final dioMessage = sanitize(error.message ?? ''); if (dioMessage.isNotEmpty) { return dioMessage; } final nestedMessage = sanitize(error.error?.toString() ?? ''); if (nestedMessage.isNotEmpty) { return nestedMessage; } } if (error is NotSuccessException) { final responseMessage = sanitize(error.message); if (responseMessage.isNotEmpty) { return responseMessage; } } final fallbackMessage = sanitize(error.toString()); if (fallbackMessage.isNotEmpty) { return fallbackMessage; } 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'); } bool _isInsufficientMoneyError(Object error, String resolvedMessage) { final errorText = '${_readBackendErrorText(error)} $resolvedMessage'.toLowerCase(); return errorText.contains('no enough money') || errorText.contains('not enough money') || errorText.contains('insufficient money') || errorText.contains('insufficient balance') || errorText.contains('insufficient_balance') || errorText.contains('balance not enough') || errorText.contains('not enough gold') || errorText.contains('insufficient gold'); } 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( builder: (context, ref, child) { final giftTabs = _buildGiftTabs(context, ref); _ensureTabController(ref, giftTabs); final tabController = _tabController; if (tabController == null) { return const SizedBox.shrink(); } final currentTabType = giftTabs[tabController.index].type; final isCpTab = currentTabType == "CP"; final showFirstRechargeEntry = !SCGlobalConfig.isReview && !isCpTab && ref.shouldShowFirstRechargeReward; _ensureCurrentTabSelection(ref, giftTabs[tabController.index].type); final isCpRecipientSingleMode = isCpTab || _isCpGift(checkedGift); return SafeArea( top: false, child: Column( mainAxisSize: MainAxisSize.min, children: [ _buildGiftHead(), if (showFirstRechargeEntry) ...[ Padding( padding: EdgeInsets.symmetric(horizontal: 10.w), child: _buildFirstRechargeGiftEntry(), ), SizedBox(height: 8.w), ], if (isCpTab) ...[ Padding( padding: EdgeInsets.symmetric(horizontal: 10.w), child: _buildCpGiftCloseFriendEntry(), ), SizedBox(height: 8.w), ], ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(12.0), topRight: Radius.circular(12.0), ), child: BackdropFilter( filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15), child: Container( color: const Color(0xff09372E).withValues(alpha: 0.5), constraints: BoxConstraints(maxHeight: 430.w), child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 12.w), Row( children: [ SizedBox(width: 8.w), widget.toUser == null ? Builder( builder: (ct) { return Opacity( opacity: isCpRecipientSingleMode ? 0.45 : 1, child: GestureDetector( onTap: isCpRecipientSingleMode ? null : () { isAll = !isAll; for (var mai in listMai) { mai.isSelect = isAll; } setState(() {}); // showGiveTypeDialog(ct); }, child: Image.asset( !isCpRecipientSingleMode && isAll ? "sc_images/room/sc_icon_gift_all_en.png" : "sc_images/room/sc_icon_gift_all_no.png", width: 28.w, height: 28.w, ), ), ); }, ) : Container(), SizedBox(width: 8.w), widget.toUser == null ? Expanded( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: listMai .map((e) => _maiHead(e)) .toList(), ), ), ) : Stack( alignment: AlignmentDirectional.center, children: [ Container( padding: EdgeInsets.all(2.w), child: netImage( url: widget.toUser?.userAvatar ?? "", width: 26.w, defaultImg: _strategy .getMePageDefaultAvatarImage(), shape: BoxShape.circle, ), ), PositionedDirectional( bottom: 0, end: 0, child: Image.asset( "sc_images/login/sc_icon_login_ser_select.png", width: 10.w, height: 10.w, ), ), ], ), SizedBox(width: 12.w), ], ), Row( children: [ SizedBox(width: 5.w), Expanded( child: SizedBox( height: 28.w, child: TabBar( tabAlignment: TabAlignment.start, labelPadding: EdgeInsets.symmetric( horizontal: 8.w, ), labelColor: SocialChatTheme.primaryLight, indicatorWeight: 0, isScrollable: true, indicator: SCFixedWidthTabIndicator( width: 15.w, color: SocialChatTheme.primaryLight, ), unselectedLabelColor: Colors.white54, labelStyle: TextStyle(fontSize: 14.sp), unselectedLabelStyle: TextStyle( fontSize: 12.sp, ), indicatorColor: Colors.transparent, dividerColor: Colors.transparent, controller: tabController, tabs: giftTabs .map((tab) => Tab(text: tab.label)) .toList(), ), ), ), SizedBox(width: 5.w), ], ), SizedBox( height: kGiftTabPageHeight.w, child: TabBarView( physics: NeverScrollableScrollPhysics(), controller: tabController, children: giftTabs .map( (tab) => GiftTabPage( key: ValueKey(tab.type), tab.type, (gift) => _handleGiftSelected(tab.type, gift), ), ) .toList(), ), ), Row( children: [ SizedBox(width: 15.w), GestureDetector( onTap: _openRoomRechargeBottomSheet, child: Container( padding: EdgeInsets.symmetric( vertical: 8.w, horizontal: 8.w, ), width: 120.w, decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.circular(5), ), child: Row( children: [ Image.asset( _strategy.getGiftPageGoldCoinIcon(), width: 14.w, height: 14.w, ), SizedBox(width: 5.w), Consumer( builder: (context, ref, child) { return Expanded( child: text( "${ref.myBalance}", fontSize: 12.sp, ), ); }, ), SizedBox(width: 5.w), Icon( Icons.arrow_forward_ios, color: Colors.white, size: 14.w, ), ], ), ), ), Spacer(), Builder( builder: (ct) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { if (noShowNumber) { return; } isNumberUp = false; _showNumber(ct); setState(() {}); }, child: Container( decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.circular(5), ), child: Row( children: [ SizedBox(width: 10.w), text("$number", fontSize: 12.sp), Icon( isNumberUp ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, color: Colors.white, size: 20.w, ), SizedBox(width: 5.w), _buildSendButton(), ], ), ), ); }, ), SizedBox(width: 15.w), ], ), SizedBox(height: 15.w), ], ), ), ), ), ], ), ); }, ); } void showGiveTypeDialog(BuildContext ct) { SmartDialog.showAttach( tag: "showGiveType", targetContext: ct, alignment: Alignment.bottomCenter, animationType: SmartAnimationType.fade, scalePointBuilder: (selfSize) => Offset(selfSize.width, 10), builder: (_) { return Container( height: 135.w, width: 200.w, decoration: BoxDecoration( image: DecorationImage( image: AssetImage(_strategy.getGiftPageGiveTypeBackground()), fit: BoxFit.fill, ), ), padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), child: Column( children: [ SizedBox(height: 5.w), Expanded( child: GestureDetector( child: Row( children: [ Image.asset( _strategy.getGiftPageAllOnMicrophoneIcon(), height: 18.w, width: 18.w, ), SizedBox(width: 8.w), Expanded( child: text( SCAppLocalizations.of(context)!.allOnMicrophone, fontSize: 14.sp, textColor: Colors.white54, ), ), SizedBox(width: 8.w), Image.asset( giveType == 0 ? _strategy.getCommonSelectIcon() : _strategy.getCommonUnselectIcon(), width: 15.w, height: 15.w, ), ], ), onTap: () { giveType = 0; SmartDialog.dismiss(tag: "showGiveType"); selecteAllMicUsers(); setState(() {}); }, ), ), Expanded( child: GestureDetector( child: Row( children: [ Image.asset( _strategy.getGiftPageUsersOnMicrophoneIcon(), height: 18.w, width: 18.w, ), SizedBox(width: 8.w), Expanded( child: text( SCAppLocalizations.of(context)!.usersOnMicrophone, fontSize: 14.sp, textColor: Colors.white54, ), ), SizedBox(width: 8.w), Image.asset( giveType == 1 ? _strategy.getCommonSelectIcon() : _strategy.getCommonUnselectIcon(), width: 15.w, height: 15.w, ), ], ), onTap: () { giveType = 1; SmartDialog.dismiss(tag: "showGiveType"); setState(() {}); }, ), ), Expanded( child: GestureDetector( onTap: () { giveType = 2; SmartDialog.dismiss(tag: "showGiveType"); setState(() {}); }, behavior: HitTestBehavior.opaque, child: Row( children: [ Image.asset( _strategy.getGiftPageAllInTheRoomIcon(), height: 18.w, width: 18.w, ), SizedBox(width: 8.w), Expanded( child: text( SCAppLocalizations.of(context)!.allInTheRoom, fontSize: 14.sp, textColor: Colors.white54, ), ), SizedBox(width: 8.w), Image.asset( giveType == 2 ? _strategy.getCommonSelectIcon() : _strategy.getCommonUnselectIcon(), width: 15.w, height: 15.w, ), ], ), ), ), ], ), ); }, ); } Widget _maiHead(HeadSelect shead) { // 是否选中 return GestureDetector( onTap: () { if (_isCpRecipientSingleMode) { final shouldSelect = !shead.isSelect; for (final mai in listMai) { mai.isSelect = false; } shead.isSelect = shouldSelect; isAll = false; giveType = 1; setState(() {}); return; } isAll = true; for (var mai in listMai) { if (shead.mic?.user?.id == mai.mic?.user?.id) { mai.isSelect = !mai.isSelect; } if (!mai.isSelect) { isAll = false; } } setState(() {}); }, child: Stack( alignment: Alignment.bottomCenter, clipBehavior: Clip.none, children: [ Stack( alignment: AlignmentDirectional.center, children: [ Container( padding: EdgeInsets.all(2.w), child: netImage( url: shead.mic?.user?.userAvatar ?? "", width: 26.w, defaultImg: _strategy.getMePageDefaultAvatarImage(), shape: BoxShape.circle, ), ), PositionedDirectional( bottom: 0, end: 0, child: Container( alignment: AlignmentDirectional.center, width: 12.w, height: 12.w, decoration: BoxDecoration( color: Colors.black38, shape: BoxShape.circle, border: shead.isSelect ? Border.all( color: SocialChatTheme.primaryLight, width: 1.w, ) : null, ), child: text( "${shead.mic?.micIndex}", textColor: Colors.white, fontSize: 8.sp, ), ), ), shead.isSelect ? PositionedDirectional( bottom: 0, end: 0, child: Image.asset( "sc_images/login/sc_icon_login_ser_select.png", width: 12.w, height: 12.w, ), ) : Container(), ], ), ], ), ); } ///数量选项 void _showNumber(BuildContext ct) { SmartDialog.showAttach( tag: "showNumber", targetContext: ct, maskColor: Colors.transparent, alignment: Alignment.topLeft, animationType: SmartAnimationType.fade, scalePointBuilder: (selfSize) => Offset(selfSize.width, 10), onDismiss: () { isNumberUp = true; setState(() {}); }, builder: (_) { return Transform.translate( offset: Offset(SCGlobalConfig.lang == "ar" ? 20 : -20, -5), child: CheckNumber( onNumberChanged: (number) { this.number = number; isNumberUp = true; setState(() {}); }, ), ); }, ); } ///选中所有在座位上的用户 void selecteAllMicUsers() { if (_isCpRecipientSingleMode) { return; } for (var mai in listMai) { mai.isSelect = true; } } _GiftSendRequest? _buildGiftSendRequest() { final selectedGift = checkedGift; if (selectedGift == null) { _giftFxLog( 'tap send aborted reason=no_selected_gift ' 'giveType=$giveType', ); return null; } final isCpGiftRequest = _isCpGift(selectedGift); List acceptUserIds = []; List acceptUsers = []; if (widget.toUser != null) { acceptUserIds.add(widget.toUser?.id ?? ""); acceptUsers.add(MicRes(user: widget.toUser)); } else if (isCpGiftRequest) { isAll = false; giveType = 1; for (final mu in listMai) { if (mu.isSelect) { acceptUsers.add(mu.mic!); acceptUserIds.add(mu.mic!.user!.id ?? ""); break; } } } else { ///所有在线 if (giveType == 2) { for (var value in Provider.of(context, listen: false).onlineUsers) { acceptUsers.add(MicRes(user: value)); acceptUserIds.add(value.id ?? ""); } } else { for (var mu in listMai) { if (mu.isSelect) { acceptUsers.add(mu.mic!); acceptUserIds.add(mu.mic!.user!.id ?? ""); } } } } if (acceptUserIds.isEmpty) { _giftFxLog( 'tap send aborted reason=no_accept_users ' 'giftId=${checkedGift?.id} ' 'giftName=${checkedGift?.giftName} ' 'giftTab=${checkedGift?.giftTab} ' 'giveType=$giveType', ); SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectTheRecipient); return null; } final selectedNumber = number; final roomId = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? ""; final roomAccount = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; 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 = 'Gift configuration unavailable, please try another gift.'; final requestName = isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift'; _giftFxLog( '$requestName skipped giftId=${selectedGift.id} ' 'giftName=${selectedGift.giftName} ' 'giftTab=${selectedGift.giftTab} ' 'standardId=${selectedGift.standardId} ' 'reason=invalid_standard_id', ); SCTts.show(configError); return null; } return _GiftSendRequest( acceptUserIds: acceptUserIds, acceptUsers: acceptUsers, gift: selectedGift, quantity: selectedNumber, clickCount: 1, roomId: roomId, roomAccount: roomAccount, isLuckyGiftRequest: isLuckyGiftRequest, isBackpackGiftRequest: isBackpackGiftRequest, ); } ///赠送礼物 void giveGifts() async { final request = _buildGiftSendRequest(); if (request == null) { return; } 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); } else { unawaited(_performGiftSend(request, trigger: 'direct')); } SmartDialog.dismiss(tag: "showGiftControl"); } bool _supportsComboRequestBatching(SocialChatGiftRes gift) { return !_usesBackpackGiftEndpoint(gift) && _supportsComboFeedback(gift); } void _enqueueComboGiftSendRequest(_GiftSendRequest request) { final now = DateTime.now(); _PendingGiftSendBatch? existingBatch; for (final batch in _comboSendBatchQueue) { if (batch.batchKey == request.batchKey) { existingBatch = batch; break; } } if (existingBatch != null) { existingBatch.quantity += request.quantity; existingBatch.clickCount += request.clickCount; existingBatch.readyAt = now.add(_comboSendBatchWindow); _giftFxLog( 'aggregate combo send update ' 'batchKey=${existingBatch.batchKey} ' 'giftId=${request.gift.id} ' 'quantity=${existingBatch.quantity} ' 'clickCount=${existingBatch.clickCount} ' 'acceptUserIds=${request.acceptUserIds.join(",")}', ); } else { final batch = _PendingGiftSendBatch.fromRequest( request, readyAt: now.add(_comboSendBatchWindow), ); _comboSendBatchQueue.add(batch); _giftFxLog( 'aggregate combo send enqueue ' 'batchKey=${batch.batchKey} ' 'giftId=${request.gift.id} ' 'quantity=${batch.quantity} ' 'clickCount=${batch.clickCount} ' 'acceptUserIds=${request.acceptUserIds.join(",")}', ); } _scheduleNextComboGiftSendFlush(); } void _scheduleNextComboGiftSendFlush() { _comboSendBatchTimer?.cancel(); _comboSendBatchTimer = null; if (_isComboSendBatchInFlight || _comboSendBatchQueue.isEmpty) { return; } final headBatch = _comboSendBatchQueue.first; final delay = headBatch.readyAt.difference(DateTime.now()); if (delay <= Duration.zero) { unawaited(_flushNextComboGiftSendBatch()); return; } _comboSendBatchTimer = Timer(delay, () { unawaited(_flushNextComboGiftSendBatch()); }); } Future _flushNextComboGiftSendBatch() async { _comboSendBatchTimer?.cancel(); _comboSendBatchTimer = null; if (_isComboSendBatchInFlight || _comboSendBatchQueue.isEmpty) { return; } final headBatch = _comboSendBatchQueue.first; if (headBatch.readyAt.isAfter(DateTime.now())) { _scheduleNextComboGiftSendFlush(); return; } _comboSendBatchQueue.removeFirst(); _isComboSendBatchInFlight = true; try { await _performGiftSend(headBatch.toRequest(), trigger: 'batched'); } finally { _isComboSendBatchInFlight = false; _scheduleNextComboGiftSendFlush(); } } Future _flushAllPendingComboGiftSends() async { _comboSendBatchTimer?.cancel(); _comboSendBatchTimer = null; while (_comboSendBatchQueue.isNotEmpty) { if (_isComboSendBatchInFlight) { await Future.delayed(const Duration(milliseconds: 50)); continue; } _comboSendBatchQueue.first.readyAt = DateTime.now(); await _flushNextComboGiftSendBatch(); } } Future _performGiftSend( _GiftSendRequest request, { required String trigger, }) async { final requestName = request.requestName; final profileManager = navigatorKey.currentState == null ? null : Provider.of( navigatorKey.currentState!.context, listen: false, ); final senderId = AccountStorage().getCurrentUser()?.userProfile?.id ?? ""; final senderName = AccountStorage().getCurrentUser()?.userProfile?.userNickname ?? ""; final stopwatch = Stopwatch()..start(); _giftFxLog( 'send start trigger=$trigger request=$requestName ' 'senderId=$senderId ' 'senderName=$senderName ' 'giftId=${request.gift.id} ' 'giftName=${request.gift.giftName} ' 'giftTab=${request.gift.giftTab} ' 'special=${request.gift.special} ' 'standardId=${request.gift.standardId} ' 'giftCandy=${request.gift.giftCandy} ' 'number=${request.quantity} ' 'clickCount=${request.clickCount} ' 'acceptCount=${request.acceptUserIds.length} ' 'acceptUserIds=${request.acceptUserIds.join(",")} ' 'roomId=${request.roomId} ' 'roomAccount=${request.roomAccount} ' 'giveType=$giveType ' 'giftType=$giftType', ); try { final repository = SCChatRoomRepository(); _giftFxLog( 'calling repository.$requestName ' 'trigger=$trigger ' 'giftId=${request.gift.id} ' 'roomId=${request.roomId} ' 'acceptUserIds=${request.acceptUserIds.join(",")} ' '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}', ); _showLocalCpInviteWaitingAfterGift(request, sentUsers); if (sentUsers.isNotEmpty) { final giftBatchId = _createGiftBatchId(request); sendGiftMsg( sentUsers, gift: request.gift, quantity: request.quantity, giftBatchId: giftBatchId, animationCount: request.clickCount, ); } await _refreshGiftBackpack(); _refreshCpStateAfterGift(request, profileManager); return; } final result = request.isLuckyGiftRequest ? await repository.giveLuckyGift( request.acceptUserIds, request.gift.id ?? "", request.quantity, false, roomId: request.roomId, relationType: request.cpRelationType, ) : await repository.giveGift( request.acceptUserIds, request.gift.id ?? "", request.quantity, false, roomId: request.roomId, relationType: request.cpRelationType, ); _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} ' 'standardId=${request.gift.standardId} ' 'number=${request.quantity} ' 'clickCount=${request.clickCount} ' 'acceptUserIds=${request.acceptUserIds.join(",")} ' 'roomId=${request.roomId} ' 'balance=$result ' 'elapsedMs=${stopwatch.elapsedMilliseconds}', ); _showLocalCpInviteWaitingAfterGift(request, request.acceptUsers); if (request.isLuckyGiftRequest) { final giftBatchId = _createGiftBatchId(request); await sendLuckGiftAnimOtherMsg( request.acceptUsers, gift: request.gift, quantity: request.quantity, giftBatchId: giftBatchId, animationCount: request.clickCount, ); } else { final giftBatchId = _createGiftBatchId(request); sendGiftMsg( request.acceptUsers, gift: request.gift, quantity: request.quantity, giftBatchId: giftBatchId, animationCount: request.clickCount, ); } profileManager?.updateBalance(result); _refreshCpStateAfterGift(request, profileManager); } catch (e) { final errorMessage = request.isBackpackGiftRequest && _isBackpackQuantityError(e) ? _backpackQuantityInsufficientMessage() : _resolveGiftSendErrorMessage(e); final errorDetails = _describeGiftSendError(e); _giftFxLog( '$requestName failed trigger=$trigger ' 'giftId=${request.gift.id} ' 'giftName=${request.gift.giftName} ' 'giftTab=${request.gift.giftTab} ' 'standardId=${request.gift.standardId} ' 'error=$e ' 'resolvedError=$errorMessage ' 'details={$errorDetails} ' 'elapsedMs=${stopwatch.elapsedMilliseconds}', ); if (request.isBackpackGiftRequest) { unawaited(_refreshGiftBackpack()); } if (!request.isBackpackGiftRequest && _isInsufficientMoneyError(e, errorMessage)) { RoomGiftComboSendController().hide(); RoomRechargeBottomSheet.show( navigatorKey.currentState?.context ?? context, ); return; } SCTts.show(errorMessage); } } void _refreshCpStateAfterGift( _GiftSendRequest request, SocialChatUserProfileManager? profileManager, ) { if (!_isCpGift(request.gift)) { return; } unawaited(profileManager?.refreshLoadedCpProfiles()); } void _showLocalCpInviteWaitingAfterGift( _GiftSendRequest request, List sentUsers, ) { if (!_isCpGift(request.gift)) { return; } final relationType = request.cpRelationType; if (relationType == null || relationType.trim().isEmpty) { _giftFxLog( 'local cp waiting skipped reason=no_relation_type ' 'giftId=${request.gift.id} acceptUserIds=${request.acceptUserIds}', ); return; } if (sentUsers.isEmpty) { _giftFxLog( 'local cp waiting skipped reason=no_sent_users ' 'giftId=${request.gift.id} relationType=$relationType', ); return; } final receiverProfile = sentUsers.first.user; if (receiverProfile == null) { _giftFxLog( 'local cp waiting skipped reason=no_receiver_profile ' 'giftId=${request.gift.id} relationType=$relationType', ); return; } final context = navigatorKey.currentState?.context; if (context == null) { _giftFxLog( 'local cp waiting skipped reason=no_context ' 'giftId=${request.gift.id} relationType=$relationType', ); return; } _giftFxLog( 'local cp waiting trigger ' 'giftId=${request.gift.id} ' 'relationType=$relationType ' 'receiverId=${receiverProfile.id}', ); unawaited( Provider.of( context, listen: false, ).showLocalCpGiftInviteWaiting( receiverProfile: receiverProfile, relationType: relationType, ), ); } 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), relationType: request.cpRelationType, ); 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, required int quantity, required String giftBatchId, int animationCount = 1, }) { ///发送一条IM消息 final targetUserIds = _resolveAcceptUserIds(acceptUsers); for (var u in acceptUsers) { final special = gift.special ?? ""; final giftSourceUrl = gift.giftSourceUrl ?? ""; final hasSource = giftSourceUrl.isNotEmpty; final hasAnimation = scGiftHasAnimationSpecial(special); final hasGlobalGift = special.contains(SCGiftType.GLOBAL_GIFT.name); final hasFullScreenEffect = scGiftHasFullScreenEffect(special); _giftFxLog( 'dispatch gift msg ' 'giftId=${gift.id} ' 'giftName=${gift.giftName} ' 'toUserId=${u.user?.id} ' 'toUserName=${u.user?.userNickname} ' 'giftSourceUrl=$giftSourceUrl ' 'special=$special ' 'hasSource=$hasSource ' 'hasAnimation=$hasAnimation ' 'hasGlobalGift=$hasGlobalGift ' 'hasFullScreenEffect=$hasFullScreenEffect ' 'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}', ); if (gift.giftSourceUrl != null && gift.special != null) { if (scGiftHasFullScreenEffect(gift.special)) { if (SCGlobalConfig.allowsHighCostAnimations && SCGlobalConfig.isGiftSpecialEffects && (rtcProvider?.shouldShowRoomVisualEffects ?? false)) { _giftFxLog( 'local trigger player play ' 'path=${gift.giftSourceUrl} ' 'giftId=${gift.id} ' 'giftName=${gift.giftName} ' 'animationCount=$animationCount', ); SCGiftVapSvgaManager().play(gift.giftSourceUrl!); } else { _giftFxLog( 'skip local play because visual effects disabled ' 'giftId=${gift.id} ' 'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} ' 'roomVisible=${rtcProvider?.shouldShowRoomVisualEffects ?? false}', ); } } else { _giftFxLog( 'skip local play because special does not include ' '${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} ' 'giftId=${gift.id} special=${gift.special}', ); } } else { _giftFxLog( 'skip local play because giftSourceUrl or special is null ' 'giftId=${gift.id} ' 'giftSourceUrl=${gift.giftSourceUrl} ' 'special=${gift.special}', ); } Provider.of( navigatorKey.currentState!.context, listen: false, ).dispatchMessage( Msg( groupId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount, gift: gift, user: AccountStorage().getCurrentUser()?.userProfile, toUser: u.user, targetUserIds: targetUserIds, giftBatchId: giftBatchId, number: quantity, customAnimationCount: animationCount, type: SCRoomMsgType.gift, role: Provider.of( navigatorKey.currentState!.context, listen: false, ).currenRoom?.entrants?.roles ?? "", msg: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", ), addLocal: true, ); if (rtcProvider?.currenRoom?.roomProfile?.roomSetting?.showHeartbeat ?? false) { debouncer.debounce( duration: Duration(milliseconds: 350), onDebounce: () { rtcProvider?.requestGiftTriggeredMicRefresh(); }, ); } num coins = (gift.giftCandy ?? 0) * quantity; if (coins > 9999 && !_isCpGift(gift)) { var fMsg = SCFloatingMessage( type: 1, userId: AccountStorage().getCurrentUser()?.userProfile?.id ?? "", toUserId: u.user?.id ?? "", userAvatarUrl: AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? "", userName: AccountStorage().getCurrentUser()?.userProfile?.userNickname ?? "", toUserName: u.user?.userNickname ?? "", toUserAvatarUrl: u.user?.userAvatar ?? "", giftUrl: gift.giftPhoto ?? "", giftId: gift.id, giftTab: _floatingGiftTabFor(gift), roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", coins: coins, number: quantity, ); _enqueueGiftFloatingMessage( fMsg, dedupKey: _giftFloatingDedupKey( roomId: fMsg.roomId, senderId: fMsg.userId, receiverId: fMsg.toUserId, giftId: gift.id, quantity: quantity, ), ); } } } void _enqueueGiftFloatingMessage( SCFloatingMessage message, { String? dedupKey, }) { OverlayManager().addMessage(message); } String _giftFloatingDedupKey({ required String? roomId, required String? senderId, required String? receiverId, required String? giftId, required int quantity, }) { return [ "gift", roomId ?? "", senderId ?? "", receiverId ?? "", giftId ?? "", quantity, ].join("|"); } String _floatingGiftTabFor(SocialChatGiftRes gift) { if (_isCpGift(gift)) { return scCpRelationTypeForGift(gift); } return gift.giftTab ?? ""; } bool _supportsComboFeedback(SocialChatGiftRes gift) { final giftTab = (gift.giftTab ?? '').trim(); return giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name || giftTab == SCGiftType.CP.name || giftTab == SCGiftType.MAGIC.name; } Widget _buildCpGiftCloseFriendEntry() { return Semantics( button: true, label: "Gifts for Close Friends", child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: _openCpRightsGuide, child: SizedBox( width: 355.w, height: 46.w, child: Image.asset( "$_cpProfileAssetBase/sc_cp_profile_gift_entry_card.png", fit: BoxFit.fill, ), ), ), ); } Widget _buildFirstRechargeGiftEntry() { return Semantics( button: true, label: "First recharge offer", child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: _openFirstRechargeDialog, child: SizedBox( width: 355.w, height: 46.w, child: Image.asset(_firstRechargeGiftEntryCard, fit: BoxFit.fill), ), ), ); } void _openFirstRechargeDialog() { SmartDialog.dismiss(tag: "showGiftControl"); Future.delayed(const Duration(milliseconds: 80), () { final navigator = navigatorKey.currentState; if (navigator == null || !navigator.mounted) { return; } SCDialogUtils.showFirstRechargeDialog(navigator.context); }); } void _openRoomRechargeBottomSheet() { SmartDialog.dismiss(tag: "showGiftControl"); Future.delayed(const Duration(milliseconds: 80), () { final navigator = navigatorKey.currentState; if (navigator == null || !navigator.mounted) { return; } RoomRechargeBottomSheet.show(navigator.context); }); } void _openCpRightsGuide() { SmartDialog.dismiss(tag: "showGiftControl"); Future.delayed(const Duration(milliseconds: 80), () { final navigator = navigatorKey.currentState; if (navigator == null) { return; } navigator.push( MaterialPageRoute(builder: (_) => const CpRightsGuidePage()), ); }); } Widget _buildSendButton() { return SCGiftComboSendButton( label: SCAppLocalizations.of(context)!.send, onPressed: giveGifts, showCountdown: false, width: 96.w, ); } Future _performFloatingComboSend( RoomGiftComboSendRequest request, { required String trigger, }) async { await _performGiftSend( _GiftSendRequest( 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, isBackpackGiftRequest: _usesBackpackGiftEndpoint(request.gift), ), trigger: trigger, ); } /// 将数字giftType转换为字符串类型,用于活动礼物头部背景 String _giftTypeToString(int giftType) { switch (giftType) { case 1: // ACTIVITY return 'ACTIVITY'; case 2: // LUCKY_GIFT -> LUCK return 'LUCK'; case 3: // CP return 'CP'; case 5: // MAGIC return 'MAGIC'; default: return 'ACTIVITY'; } } _buildGiftHead() { if (giftType == 2 || giftType == 3) { return Container(); } if (giftType == 1 || giftType == 5) { // 获取基础路径 String basePath = _strategy.getGiftPageActivityGiftHeadBackground( _giftTypeToString(giftType), ); // 添加语言后缀 String imagePath; if (SCGlobalConfig.lang == "ar") { // 移除扩展名,添加 _ar 后缀,然后重新添加扩展名 if (basePath.endsWith('.png')) { imagePath = '${basePath.substring(0, basePath.length - 4)}_ar.png'; } else { imagePath = '${basePath}_ar'; } } else { if (basePath.endsWith('.png')) { imagePath = '${basePath.substring(0, basePath.length - 4)}_en.png'; } else { imagePath = '${basePath}_en'; } } // 确定高度 double height = giftType == 5 ? 80.w : 65.w; return Container( margin: EdgeInsets.symmetric(horizontal: 10.w), child: Image.asset(imagePath, height: height, fit: BoxFit.fill), ); } return Container(); } ///发送一条消息,幸运礼物,房间里所有人都能看到礼物飘向麦位的动画 Future sendLuckGiftAnimOtherMsg( List acceptUsers, { required SocialChatGiftRes gift, required int quantity, required String giftBatchId, int animationCount = 1, }) async { final targetUserIds = _resolveAcceptUserIdentityKeys(acceptUsers); final firstTargetUser = acceptUsers.isNotEmpty ? acceptUsers.first.user : null; _giftFxLog( 'dispatch lucky anim msg ' 'giftId=${gift.id} ' 'giftName=${gift.giftName} ' 'giftPhoto=${gift.giftPhoto} ' 'quantity=$quantity ' 'animationCount=$animationCount ' 'firstTargetUserId=${firstTargetUser?.id} ' 'groupId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount} ' 'roomId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id} ' 'targetUserIds=${targetUserIds.join(",")}', ); await Provider.of( navigatorKey.currentState!.context, listen: false, ).dispatchMessage( Msg( groupId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount, gift: gift, user: AccountStorage().getCurrentUser()?.userProfile, toUser: firstTargetUser, targetUserIds: targetUserIds, giftBatchId: giftBatchId, number: quantity, customAnimationCount: animationCount, type: SCRoomMsgType.luckGiftAnimOther, msg: jsonEncode(targetUserIds), ), addLocal: true, ); _giftFxLog( 'dispatch lucky anim msg finished ' 'giftId=${gift.id} ' 'targetUserIds=${targetUserIds.join(",")}', ); } List _resolveAcceptUserIds(List acceptUsers) { final targetUserIds = []; for (final acceptUser in acceptUsers) { final userId = (acceptUser.user?.id ?? "").trim(); if (userId.isEmpty || targetUserIds.contains(userId)) { continue; } targetUserIds.add(userId); } return targetUserIds; } List _resolveAcceptUserIdentityKeys(List acceptUsers) { final targetUserIds = []; void addTargetUserId(String? userId) { final normalizedUserId = (userId ?? "").trim(); if (normalizedUserId.isEmpty || targetUserIds.contains(normalizedUserId)) { return; } targetUserIds.add(normalizedUserId); } for (final acceptUser in acceptUsers) { final user = acceptUser.user; addTargetUserId(user?.id); addTargetUserId(user?.account); addTargetUserId(user?.getID()); } return targetUserIds; } String _createGiftBatchId(_GiftSendRequest request) { final sortedAcceptUserIds = List.from(request.acceptUserIds) ..sort(); return [ request.isBackpackGiftRequest ? 'backpack' : request.isLuckyGiftRequest ? 'lucky' : 'gift', request.roomId, request.gift.id ?? '', request.quantity, request.clickCount, sortedAcceptUserIds.join(','), DateTime.now().microsecondsSinceEpoch, ].join('|'); } } class CheckNumber extends StatelessWidget { final ValueChanged onNumberChanged; const CheckNumber({super.key, required this.onNumberChanged}); @override Widget build(BuildContext context) { return Container( alignment: AlignmentDirectional.topEnd, margin: EdgeInsets.only(right: width(22)), child: ClipRRect( borderRadius: BorderRadius.all(Radius.circular(12.w)), child: BackdropFilter( filter: ui.ImageFilter.blur(sigmaX: 25, sigmaY: 25), child: Container( decoration: BoxDecoration( color: Colors.white10, borderRadius: BorderRadius.all(Radius.circular(height(6))), ), width: width(75), child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: height(10)), _item(1), _item(7), _item(17), _item(77), _item(777), _item(7777), SizedBox(height: height(10)), ], ), ), ), ), ); } _item(int number) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { SmartDialog.dismiss(tag: "showNumber"); onNumberChanged(number); }, child: SizedBox( height: height(24), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SizedBox(width: width(10)), Text( "$number", style: TextStyle( fontSize: sp(14), color: Colors.white, fontWeight: FontWeight.w400, decoration: TextDecoration.none, ), ), SizedBox(width: width(10)), ], ), ), ); } } class _GiftSendRequest { const _GiftSendRequest({ required this.acceptUserIds, required this.acceptUsers, required this.gift, required this.quantity, required this.clickCount, required this.roomId, required this.roomAccount, required this.isLuckyGiftRequest, required this.isBackpackGiftRequest, }); final List acceptUserIds; final List acceptUsers; final SocialChatGiftRes gift; final int quantity; final int clickCount; final String roomId; final String roomAccount; final bool isLuckyGiftRequest; final bool isBackpackGiftRequest; String get requestName { if (isBackpackGiftRequest) { return 'giveBackpackGift'; } return isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift'; } String? get cpRelationType { if (!scIsCpGift(gift)) { return null; } if (isSelfRecipientRequest) { return null; } return scCpRelationTypeForGift(gift); } bool get isSelfRecipientRequest { final currentProfile = AccountStorage().getCurrentUser()?.userProfile; final currentKeys = _profileIdentityKeys(currentProfile); if (currentKeys.isEmpty) { return false; } for (final userId in acceptUserIds) { if (currentKeys.contains(userId.trim())) { return true; } } for (final acceptUser in acceptUsers) { final acceptKeys = _profileIdentityKeys(acceptUser.user); if (acceptKeys.any(currentKeys.contains)) { return true; } } return false; } Set _profileIdentityKeys(SocialChatUserProfile? profile) { return { profile?.id?.trim() ?? "", profile?.account?.trim() ?? "", profile?.getID().trim() ?? "", }..remove(""); } String get batchKey { final sortedAcceptUserIds = List.from(acceptUserIds)..sort(); final type = isBackpackGiftRequest ? "backpack" : isLuckyGiftRequest ? "lucky" : "gift"; return [ type, gift.id ?? "", gift.backpackId ?? "", roomId, sortedAcceptUserIds.join(","), ].join("|"); } } class _PendingGiftSendBatch { _PendingGiftSendBatch({ required this.batchKey, required this.acceptUserIds, required this.acceptUsers, required this.gift, required this.quantity, required this.clickCount, required this.roomId, required this.roomAccount, required this.isLuckyGiftRequest, required this.isBackpackGiftRequest, required this.readyAt, }); factory _PendingGiftSendBatch.fromRequest( _GiftSendRequest request, { required DateTime readyAt, }) { return _PendingGiftSendBatch( batchKey: request.batchKey, 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, isBackpackGiftRequest: request.isBackpackGiftRequest, readyAt: readyAt, ); } final String batchKey; final List acceptUserIds; final List acceptUsers; final SocialChatGiftRes gift; int quantity; int clickCount; final String roomId; final String roomAccount; final bool isLuckyGiftRequest; final bool isBackpackGiftRequest; DateTime readyAt; _GiftSendRequest toRequest() { return _GiftSendRequest( acceptUserIds: List.from(acceptUserIds), acceptUsers: List.from(acceptUsers), gift: gift, quantity: quantity, clickCount: clickCount, roomId: roomId, roomAccount: roomAccount, isLuckyGiftRequest: isLuckyGiftRequest, isBackpackGiftRequest: isBackpackGiftRequest, ); } } class HeadSelect { bool isSelect = false; MicRes? mic; HeadSelect(this.isSelect, this.mic); }