import 'dart:async'; import 'dart:convert'; import 'dart:math' as math; import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:flutter/material.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/app/constants/sc_global_config.dart'; import 'package:yumi/main.dart' show routeObserver; import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/ui_kit/widgets/room/room_bottom_widget.dart'; import 'package:provider/provider.dart'; import 'package:yumi/shared/tools/sc_lk_event_bus.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; import 'package:yumi/services/gift/gift_animation_manager.dart'; import 'package:yumi/services/gift/gift_system_manager.dart'; import 'package:yumi/services/music/room_music_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; import 'package:yumi/shared/tools/sc_lucky_gift_win_sound_player.dart'; import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart'; import 'package:yumi/shared/tools/sc_network_image_utils.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart'; import 'package:yumi/ui_kit/widgets/room/anim/room_gift_seat_flight_overlay.dart'; import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_screen.dart'; import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_widget.dart'; import 'package:yumi/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart'; import 'package:yumi/ui_kit/widgets/room/effect/vapp_svga_layer_widget.dart'; import 'package:yumi/ui_kit/widgets/room/room_head_widget.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import 'package:yumi/ui_kit/widgets/room/room_online_user_widget.dart'; import 'package:yumi/ui_kit/widgets/room/room_play_widget.dart'; import 'package:yumi/ui_kit/widgets/room/music/room_music_room_player.dart'; import 'package:yumi/shared/data_sources/models/enum/sc_gift_type.dart'; import '../../ui_kit/components/sc_float_ichart.dart'; import '../../ui_kit/widgets/room/seat/room_seat_widget.dart'; import 'chat/all/all_chat_page.dart'; import 'chat/chat/chat_page.dart'; import 'chat/gift/gift_chat_page.dart'; ///语聊房 class VoiceRoomPage extends StatefulWidget { const VoiceRoomPage({super.key}); @override State createState() => _VoiceRoomPageState(); } class _VoiceRoomPageState extends State with SingleTickerProviderStateMixin, RouteAware { static const int _chatTabCount = 3; static const Duration _luckyGiftComboWindow = Duration(seconds: 3); static const Duration _luckyGiftQueueDrainWindow = Duration(seconds: 3); static const int _maxLuckyGiftTrackedAnimations = 5; static const Duration _giftAnimationSessionWindow = _luckyGiftComboWindow; static const Duration _giftAnimationQueueDrainWindow = _luckyGiftQueueDrainWindow; static const int _maxTrackedGiftAnimations = _maxLuckyGiftTrackedAnimations; static const Duration _giftFlightBatchDedupeWindow = Duration( milliseconds: 650, ); static const Duration _giftVisualBatchDedupeWindow = Duration(seconds: 4); late TabController _tabController; late StreamSubscription _subscription; final RoomGiftSeatFlightController _giftSeatFlightController = RoomGiftSeatFlightController(); final Map _luckyGiftComboSessions = {}; final Map _giftFlightBatchDedupeTimers = {}; final Map _giftVisualBatchDedupeTimers = {}; int _shownRoomStartupFailureToken = 0; RtcProvider? _rtcProvider; bool _roomProviderRebuildScheduled = false; bool _roomRouteVisibleScheduled = false; bool _roomVisualEffectsEnsureScheduled = false; PageRoute? _routeObserverRoute; SCRoomTopLayerGuardLease? _coveredRouteGuardLease; bool _roomRouteVisible = false; @override void initState() { super.initState(); _tabController = TabController(length: _chatTabCount, vsync: this); _tabController.addListener(_handleTabChange); _subscription = eventBus.on().listen(( event, ) { if (mounted) { Provider.of(context, listen: false).clearAllGiftData(); Provider.of( context, listen: false, ).toggleGiftAnimationVisibility(false); _clearLuckyGiftComboSessions(); _clearGiftFlightBatchDedupeTimers(); _clearGiftVisualBatchDedupeTimers(); _giftSeatFlightController.clear(); } }); } @override void didChangeDependencies() { super.didChangeDependencies(); final rtcProvider = context.read(); if (!identical(_rtcProvider, rtcProvider)) { _rtcProvider?.removeListener(_handleRoomProviderChanged); _rtcProvider = rtcProvider; rtcProvider.addListener(_handleRoomProviderChanged); context.read().attachRtcProvider(rtcProvider); } final route = ModalRoute.of(context); if (route is PageRoute && _routeObserverRoute != route) { if (_routeObserverRoute != null) { routeObserver.unsubscribe(this); } _routeObserverRoute = route; routeObserver.subscribe(this, route); if (route.isCurrent) { _scheduleRoomRouteVisible(); } else { _handleRoomRouteCovered(); } } _scheduleEnsureRoomVisualEffectsEnabled(); } @override void dispose() { routeObserver.unsubscribe(this); _routeObserverRoute = null; _releaseCoveredRouteGuard(); _setRoomRouteVisible(false); _rtcProvider?.removeListener(_handleRoomProviderChanged); _rtcProvider = null; _roomProviderRebuildScheduled = false; _suspendRoomVisualEffects(); _tabController.removeListener(_handleTabChange); _tabController.dispose(); // 释放资源 _subscription.cancel(); super.dispose(); } @override void didPush() { _handleRoomRouteVisible(); } @override void didPopNext() { _handleRoomRouteVisible(); } @override void didPushNext() { _handleRoomRouteCovered(); } @override void didPop() { _handleRoomRouteCovered(); _releaseCoveredRouteGuard(); } void _handleRoomRouteVisible() { _releaseCoveredRouteGuard(); _setRoomRouteVisible(true); } void _scheduleRoomRouteVisible() { if (_roomRouteVisibleScheduled) { return; } _roomRouteVisibleScheduled = true; WidgetsBinding.instance.addPostFrameCallback((_) { _roomRouteVisibleScheduled = false; if (!mounted || !(ModalRoute.of(context)?.isCurrent ?? false)) { return; } _handleRoomRouteVisible(); _ensureRoomVisualEffectsEnabled(); }); } void _handleRoomRouteCovered() { _setRoomRouteVisible(false); _acquireCoveredRouteGuard(); _clearTransientRoomVisualEffects(reason: 'voice_room_route_covered'); } void _acquireCoveredRouteGuard() { _coveredRouteGuardLease ??= SCRoomTopLayerGuard().acquire( reason: 'voice_room_route_covered', ); } void _releaseCoveredRouteGuard() { _coveredRouteGuardLease?.release(); _coveredRouteGuardLease = null; } void _setRoomRouteVisible(bool visible) { if (_roomRouteVisible == visible) { return; } _roomRouteVisible = visible; final rtcProvider = _rtcProvider ?? Provider.of(context, listen: false); rtcProvider.setVoiceRoomRouteVisible(visible); } void _handleTabChange() { if (!mounted) { return; } setState(() {}); } void _handleRoomProviderChanged() { if (!mounted || _roomProviderRebuildScheduled) { return; } _roomProviderRebuildScheduled = true; WidgetsBinding.instance.addPostFrameCallback((_) { _roomProviderRebuildScheduled = false; if (!mounted) { return; } setState(() {}); }); } void _enableRoomVisualEffects() { Provider.of( context, listen: false, ).setRoomVisualEffectsEnabled(true); final rtmProvider = Provider.of(context, listen: false); rtmProvider.msgFloatingGiftListener = _floatingGiftListener; rtmProvider.msgLuckyGiftRewardTickerListener = _luckyGiftRewardTickerListener; } void _ensureRoomVisualEffectsEnabled() { final rtcProvider = Provider.of(context, listen: false); if (rtcProvider.currenRoom == null || !_roomRouteVisible || rtcProvider.roomVisualEffectsEnabled) { return; } _enableRoomVisualEffects(); } void _scheduleEnsureRoomVisualEffectsEnabled() { if (_roomVisualEffectsEnsureScheduled) { return; } _roomVisualEffectsEnsureScheduled = true; WidgetsBinding.instance.addPostFrameCallback((_) { _roomVisualEffectsEnsureScheduled = false; if (!mounted || !(ModalRoute.of(context)?.isCurrent ?? false)) { return; } _ensureRoomVisualEffectsEnabled(); }); } void _clearTransientRoomVisualEffects({required String reason}) { if (!mounted) { return; } final rtcProvider = Provider.of(context, listen: false); final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; RoomEntranceHelper.clearQueue(); Provider.of(context, listen: false).clearAllGiftData(); Provider.of( context, listen: false, ).clearActiveAnimations(); _clearLuckyGiftComboSessions(); _clearGiftFlightBatchDedupeTimers(); _clearGiftVisualBatchDedupeTimers(); _giftSeatFlightController.clear(); OverlayManager().removeRoom(roomId: currentRoomId); SCRoomEffectScheduler().clearDeferredTasks(reason: reason); SCGiftVapSvgaManager().stopPlayback(); } void _suspendRoomVisualEffects() { final rtcProvider = Provider.of(context, listen: false); final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; rtcProvider.setVoiceRoomRouteVisible(false); rtcProvider.setRoomVisualEffectsEnabled(false); final rtmProvider = Provider.of(context, listen: false); if (rtmProvider.msgFloatingGiftListener == _floatingGiftListener) { rtmProvider.msgFloatingGiftListener = null; } if (rtmProvider.msgLuckyGiftRewardTickerListener == _luckyGiftRewardTickerListener) { rtmProvider.msgLuckyGiftRewardTickerListener = null; } RoomEntranceHelper.clearQueue(); _clearLuckyGiftComboSessions(); _clearGiftFlightBatchDedupeTimers(); _clearGiftVisualBatchDedupeTimers(); _giftSeatFlightController.clear(); OverlayManager().removeRoom(roomId: currentRoomId); SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend'); SCGiftVapSvgaManager().stopPlayback(); if (!SCFloatIchart().isShow()) { unawaited( Provider.of(context, listen: false).stopForRoomExit(), ); } } Widget _buildRoomStartupLayer() { return Selector( selector: (context, provider) => _RoomStartupSnapshot( status: provider.roomStartupStatus, failureType: provider.roomStartupFailureType, failureToken: provider.roomStartupFailureToken, ), builder: (context, snapshot, child) { if (snapshot.status == RoomStartupStatus.failed) { _showRoomStartupFailureDialog(snapshot); return const SizedBox.shrink(); } if (snapshot.status != RoomStartupStatus.loading) { return const SizedBox.shrink(); } return const SizedBox.shrink(); }, ); } void _showRoomStartupFailureDialog(_RoomStartupSnapshot snapshot) { if (_shownRoomStartupFailureToken == snapshot.failureToken) { return; } _shownRoomStartupFailureToken = snapshot.failureToken; WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) { return; } SmartDialog.dismiss(tag: "roomStartupFailureDialog"); SmartDialog.show( tag: "roomStartupFailureDialog", alignment: Alignment.center, animationType: SmartAnimationType.fade, backType: SmartBackType.block, clickMaskDismiss: false, builder: (_) { return _RoomStartupFailureDialog( title: SCAppLocalizations.of(context)!.tips, message: _roomStartupFailureMessage(snapshot.failureType), btnText: SCAppLocalizations.of(context)!.confirm, onEnsure: () { SmartDialog.dismiss(tag: "roomStartupFailureDialog"); context.read().confirmRoomStartupFailureAndLeave( navigationContext: context, ); }, ); }, ); }); } String _roomStartupFailureMessage(RoomStartupFailureType failureType) { switch (failureType) { case RoomStartupFailureType.im: return SCAppLocalizations.of(context)!.enterRoomFailedRetry; case RoomStartupFailureType.rtc: return SCAppLocalizations.of(context)!.voiceConnectionFailedRetry; case RoomStartupFailureType.kicked: return SCAppLocalizations.of(context)!.roomKickedEntryBlockedTips; case RoomStartupFailureType.entry: case RoomStartupFailureType.none: return SCAppLocalizations.of(context)!.operationFail; } } String? _resolveRoomBackground(JoinRoomRes? room) { final roomBackground = room?.roomProfile?.roomProfile?.roomBackground?.trim() ?? ""; if (roomBackground.isNotEmpty) { return roomBackground; } final roomTheme = room?.roomProps?.roomTheme; final themeBack = roomTheme?.themeBack ?? ""; if (themeBack.isEmpty) { return null; } if ((roomTheme?.expireTime ?? 0) <= DateTime.now().millisecondsSinceEpoch) { return null; } return themeBack; } @override Widget build(BuildContext context) { final chatRoomKey = _chatRoomKey(context); return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, Object? result) { if (!didPop) { final rtcProvider = context.read(); if (rtcProvider.roomStartupStatus == RoomStartupStatus.loading || rtcProvider.roomStartupStatus == RoomStartupStatus.failed) { return; } _suspendRoomVisualEffects(); SCFloatIchart().show(); SCNavigatorUtils.goBack(context); } }, child: Scaffold( backgroundColor: SCGlobalConfig.businessLogicStrategy.getVoiceRoomBackgroundColor(), resizeToAvoidBottomInset: false, body: SafeArea( top: false, child: Stack( children: [ Selector( selector: (context, provider) => _resolveRoomBackground(provider.currenRoom), builder: (context, roomBackground, child) { return roomBackground != null ? netImage( url: roomBackground, width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, noDefaultImg: true, fit: BoxFit.cover, ) : Image.asset( SCGlobalConfig.businessLogicStrategy .getVoiceRoomDefaultBackgroundImage(), width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, fit: BoxFit.cover, ); }, ), Column( children: [ SizedBox(height: ScreenUtil().setWidth(42)), RoomHeadWidget(), SizedBox(height: ScreenUtil().setWidth(5)), RoomOnlineUserWidget(), RoomSeatWidget(), SizedBox(height: 2.w), Expanded( child: Stack( children: [ Column( children: [ _buildChatView(chatRoomKey), const RoomBottomWidget(showGiftComboButton: false), ], ), PositionedDirectional( start: 0, end: 0, top: 0, bottom: RoomBottomWidget.floatingButtonHostHeight.w, child: const IgnorePointer(child: LGiftAnimalPage()), ), Transform.translate( offset: Offset(0, -20), child: RoomAnimationQueueScreen(), ), ], ), ), ], ), _buildRoomStartupLayer(), const PositionedDirectional( start: 0, end: 0, bottom: 0, child: RoomMusicRoomPlayer(), ), // _buildPlayViews(), ///幸运礼物中奖动画 LuckGiftNomorAnimWidget(), _buildRoomGiftSvgaLayer(), _buildRoomGiftSeatFlightLayer(), const RoomGiftComboFloatingLayer(), ], ), ), ), ); } Widget _buildRoomGiftSvgaLayer() { if (!SCGlobalConfig.allowsHighCostAnimations) { return const SizedBox.shrink(); } return Positioned.fill( child: Selector( selector: (context, provider) => provider.shouldShowRoomVisualEffects, builder: (context, shouldShowRoomVisualEffects, child) { if (!shouldShowRoomVisualEffects) { return const SizedBox.shrink(); } return child!; }, child: const VapPlusSvgaPlayer(tag: "room_gift"), ), ); } Widget _buildRoomGiftSeatFlightLayer() { return Positioned.fill( child: Selector( selector: (context, provider) => provider.shouldShowRoomVisualEffects, builder: (context, shouldShowRoomVisualEffects, child) { if (!shouldShowRoomVisualEffects) { return const SizedBox.shrink(); } return RoomGiftSeatFlightOverlay( controller: _giftSeatFlightController, resolveTargetKey: (userId) => context.read().getSeatGlobalKeyByIndex(userId), ); }, ), ); } ///消息 Widget _buildChatView(String chatRoomKey) { return Expanded( child: Stack( alignment: AlignmentDirectional.bottomEnd, children: [ Column( children: [ TabBar( tabAlignment: TabAlignment.start, splashFactory: NoSplash.splashFactory, overlayColor: WidgetStateProperty.all(Colors.transparent), labelPadding: EdgeInsets.symmetric(horizontal: 8.w), labelColor: SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabLabelColor(), isScrollable: true, indicator: BoxDecoration(), unselectedLabelColor: SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabUnselectedLabelColor(), labelStyle: SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabLabelStyle() .copyWith( fontSize: SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabLabelStyle() .fontSize! * ScreenUtil().setSp(1), ), unselectedLabelStyle: SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabUnselectedLabelStyle() .copyWith( fontSize: SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabUnselectedLabelStyle() .fontSize! * ScreenUtil().setSp(1), ), indicatorColor: SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabIndicatorColor(), dividerColor: SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabDividerColor(), controller: _tabController, tabs: List.generate(_chatTabCount, _buildImageTab), ), Expanded( child: Container( margin: SCGlobalConfig.businessLogicStrategy .getVoiceRoomChatContainerMargin() .copyWith( end: SCGlobalConfig.businessLogicStrategy .getVoiceRoomChatContainerMargin() .end * ScreenUtil().setWidth(1), ), child: MediaQuery.removePadding( context: context, removeTop: true, child: TabBarView( controller: _tabController, children: _buildChatPages(chatRoomKey), ), ), ), ), ], ), RoomPlayWidget(), ], ), ); } String _chatRoomKey(BuildContext context) { final roomProfile = context.read().currenRoom?.roomProfile?.roomProfile; final roomId = roomProfile?.id?.trim() ?? ""; final groupId = roomProfile?.roomAccount?.trim() ?? ""; return "$roomId|$groupId"; } List _buildChatPages(String chatRoomKey) { return [ KeyedSubtree( key: ValueKey("all_chat_$chatRoomKey"), child: const AllChatPage(), ), KeyedSubtree(key: ValueKey("chat_$chatRoomKey"), child: ChatPage()), KeyedSubtree( key: ValueKey("gift_chat_$chatRoomKey"), child: const GiftChatPage(), ), ]; } Widget _buildImageTab(int index) { return Tab( height: 32.w, child: IgnorePointer( child: Image.asset( _roomChatTabAsset(index), height: 26.w, fit: BoxFit.contain, ), ), ); } String _roomChatTabAsset(int index) { final bool selected = _tabController.index == index; switch (index) { case 0: return selected ? "sc_images/room/sc_icon_room_chat_tab_all_selected.png" : "sc_images/room/sc_icon_room_chat_tab_all_unselected.png"; case 1: return selected ? "sc_images/room/sc_icon_room_chat_tab_chat_selected.png" : "sc_images/room/sc_icon_room_chat_tab_chat_unselected.png"; case 2: default: return selected ? "sc_images/room/sc_icon_room_chat_tab_gift_selected.png" : "sc_images/room/sc_icon_room_chat_tab_gift_unselected.png"; } } ///礼物上飘动画 _floatingGiftListener(Msg msg) { final rtcProvider = Provider.of(context, listen: false); if (!rtcProvider.shouldShowRoomVisualEffects) { return; } if (Provider.of(context, listen: false).hideLGiftAnimal) { return; } final targetUserIds = _resolveGiftTargetUserIds(msg); final targetGroupKey = _buildGiftTargetGroupKey(targetUserIds); final shouldPlayBatchVisuals = targetUserIds.length <= 1 || targetGroupKey == null || _markGiftVisualBatchForPlayback(msg, targetGroupKey); if (shouldPlayBatchVisuals) { var giftModel = LGiftModel(); giftModel.labelId = _buildGiftTickerLabelId(msg, targetGroupKey); giftModel.sendUserName = msg.user?.userNickname ?? ""; giftModel.sendToUserName = _resolveGiftTickerTargetName( msg, targetUserIds, ); giftModel.sendUserPic = msg.user?.userAvatar ?? ""; giftModel.giftPic = msg.gift?.giftPhoto ?? ""; giftModel.giftCount = msg.number ?? 0; giftModel.giftCountStepUnit = _resolveGiftCountStepUnit(msg); unawaited( warmImageResource( giftModel.sendUserPic, logicalWidth: 26.w, logicalHeight: 26.w, ), ); unawaited( warmImageResource( giftModel.giftPic, logicalWidth: 34.w, logicalHeight: 34.w, ), ); Provider.of( context, listen: false, ).enqueueGiftAnimation(giftModel); } else { return; } final giftPhoto = (msg.gift?.giftPhoto ?? "").trim(); _enqueueCpGiftFloatingIfNeeded(msg); if (_supportsComboMilestoneEffects(msg)) { _handleComboMilestoneVisuals(msg, targetGroupKey); } final isLuckyGift = _isLuckyGiftMessage(msg); if (isLuckyGift) { _handleLuckyGiftComboVisuals( msg, giftPhoto, targetGroupKey, targetUserIds, ); return; } _handleStandardGiftComboVisuals( msg, giftPhoto, targetGroupKey, targetUserIds, ); } void _enqueueCpGiftFloatingIfNeeded(Msg msg) { final gift = msg.gift; if (!scIsCpGift(gift) || gift == null) { return; } final quantity = msg.number ?? 0; if (quantity <= 0) { return; } final roomId = _resolveGiftFloatingRoomId(msg); OverlayManager().addMessage( SCFloatingMessage( type: 1, priority: 900, userId: msg.user?.id ?? "", toUserId: msg.toUser?.id ?? "", userAvatarUrl: msg.user?.userAvatar ?? "", toUserAvatarUrl: msg.toUser?.userAvatar ?? "", userName: msg.user?.userNickname ?? "", toUserName: msg.toUser?.userNickname ?? "", giftUrl: gift.giftPhoto ?? "", giftId: gift.id, giftTab: scCpRelationTypeForGift(gift), number: quantity, coins: quantity * (gift.giftCandy ?? 0), roomId: roomId, ), ); } String _resolveGiftFloatingRoomId(Msg msg) { final messageRoomId = (msg.msg ?? "").trim(); if (messageRoomId.isNotEmpty) { return messageRoomId; } return Provider.of( context, listen: false, ).currenRoom?.roomProfile?.roomProfile?.id ?? ""; } void _luckyGiftRewardTickerListener(Msg msg) { if (!Provider.of( context, listen: false, ).shouldShowRoomVisualEffects) { return; } if (Provider.of(context, listen: false).hideLGiftAnimal) { return; } final awardAmount = msg.awardAmount ?? 0; if (awardAmount <= 0) { return; } final giftModel = LGiftModel(); giftModel.labelId = "${msg.gift?.id}${msg.user?.id}${msg.toUser?.id}"; giftModel.sendUserName = msg.user?.userNickname ?? ""; giftModel.sendToUserName = msg.toUser?.userNickname ?? ""; giftModel.sendUserPic = msg.user?.userAvatar ?? ""; giftModel.giftPic = msg.gift?.giftPhoto ?? ""; giftModel.giftCount = 0; giftModel.rewardAmount = awardAmount; giftModel.showLuckyRewardFrame = true; giftModel.rewardAmountText = _formatLuckyRewardAmount(awardAmount); unawaited( warmImageResource( giftModel.sendUserPic, logicalWidth: 26.w, logicalHeight: 26.w, ), ); unawaited( warmImageResource( giftModel.giftPic, logicalWidth: 34.w, logicalHeight: 34.w, ), ); Provider.of( context, listen: false, ).enqueueGiftAnimation(giftModel); unawaited( SCLuckyGiftWinSoundPlayer.play( eventKey: SCLuckyGiftWinSoundPlayer.buildEventKey( giftId: msg.gift?.id, userId: msg.user?.id, toUserId: msg.toUser?.id, awardAmount: awardAmount, ), ), ); } String _formatLuckyRewardAmount(num awardAmount) { if (awardAmount > 9999) { return "${(awardAmount / 1000).toStringAsFixed(0)}k"; } if (awardAmount % 1 == 0) { return awardAmount.toInt().toString(); } return awardAmount.toString(); } bool _isLuckyGiftMessage(Msg msg) { final giftTab = (msg.gift?.giftTab ?? '').trim(); return giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name; } bool _supportsComboMilestoneEffects(Msg msg) { return SocialChatGiftSystemManager.supportsComboMilestoneEffects(msg.gift); } bool _shouldPlayComboMilestoneEffect(Msg msg) { if (!_supportsComboMilestoneEffects(msg)) { return false; } return SCGlobalConfig.isLuckGiftSpecialEffects; } void _handleComboMilestoneVisuals(Msg msg, String? targetUserId) { final quantity = (msg.number ?? 0).floor(); if (quantity <= 0) { return; } final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId); final session = _luckyGiftComboSessions.putIfAbsent( sessionKey, () => _LuckyGiftComboSession(), ); session.endTimer?.cancel(); session.clearQueueTimer?.cancel(); final previousTotal = session.totalCount; session.totalCount += quantity; final highestMilestone = SocialChatGiftSystemManager.resolveHighestCrossedComboEffectMilestone( previousCount: previousTotal, currentCount: session.totalCount, ); if (highestMilestone != null && highestMilestone > session.highestPlayedMilestone && _shouldPlayComboMilestoneEffect(msg)) { final effectPath = SocialChatGiftSystemManager.resolveComboMilestoneEffectPath( highestMilestone, ); if (effectPath != null && effectPath.isNotEmpty) { SCGiftVapSvgaManager().play(effectPath, priority: 200); session.highestPlayedMilestone = highestMilestone; } } } void _handleStandardGiftComboVisuals( Msg msg, String giftPhoto, String? targetGroupKey, List targetUserIds, ) { if ((msg.number ?? 0) <= 0) { return; } if (!_shouldPlaySeatFlightGiftAnimation(msg) || targetGroupKey == null || targetUserIds.isEmpty || giftPhoto.isEmpty) { return; } final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds); if (targetUserIds.length > 1 && !_markGiftFlightBatchForPlayback(flightBatchKey)) { return; } final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey); final session = _luckyGiftComboSessions.putIfAbsent( sessionKey, () => _LuckyGiftComboSession(), ); session.endTimer?.cancel(); session.clearQueueTimer?.cancel(); _enqueueTrackedSeatFlightAnimations( sessionKey: sessionKey, giftPhoto: giftPhoto, targetUserIds: targetUserIds, animationCount: _resolveTrackedAnimationCount(msg), batchKey: flightBatchKey, ); _scheduleGiftAnimationSessionEnd(sessionKey); } void _handleLuckyGiftComboVisuals( Msg msg, String giftPhoto, String? targetGroupKey, List targetUserIds, ) { if ((msg.number ?? 0) <= 0) { return; } if (targetGroupKey == null || targetUserIds.isEmpty) { return; } final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey); final session = _luckyGiftComboSessions[sessionKey]; if (session == null) { return; } session.endTimer?.cancel(); session.clearQueueTimer?.cancel(); if (_shouldPlaySeatFlightGiftAnimation(msg) && giftPhoto.isNotEmpty) { final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds); if (targetUserIds.length > 1 && !_markGiftFlightBatchForPlayback(flightBatchKey)) { _scheduleGiftAnimationSessionEnd(sessionKey); return; } _enqueueTrackedSeatFlightAnimations( sessionKey: sessionKey, giftPhoto: giftPhoto, targetUserIds: targetUserIds, animationCount: _resolveTrackedAnimationCount(msg), batchKey: flightBatchKey, ); } _scheduleGiftAnimationSessionEnd(sessionKey); } int _resolveTrackedAnimationCount(Msg msg) { return math.max(msg.customAnimationCount ?? 1, 1); } num _resolveGiftCountStepUnit(Msg msg) { final quantity = msg.number ?? 0; final clickCount = math.max(msg.customAnimationCount ?? 1, 1); if (quantity <= 0) { return 1; } final stepUnit = quantity / clickCount; if (stepUnit <= 0) { return quantity; } return stepUnit; } void _enqueueTrackedSeatFlightAnimations({ required String sessionKey, required String giftPhoto, required List targetUserIds, required int animationCount, required String batchKey, }) { unawaited( warmImageResource(giftPhoto, logicalWidth: 96.w, logicalHeight: 96.w), ); final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds); if (normalizedTargetUserIds.isEmpty) { return; } final normalizedAnimationCount = math.max(animationCount, 1); final cappedAnimationCount = math.min( normalizedAnimationCount, _maxTrackedGiftAnimations, ); final maxTrackedRequests = math.max( _maxTrackedGiftAnimations * normalizedTargetUserIds.length, _maxTrackedGiftAnimations, ); for (var index = 0; index < cappedAnimationCount; index += 1) { final roundBatchTag = normalizedTargetUserIds.length > 1 ? '$batchKey|round:$index' : null; for (final targetUserId in normalizedTargetUserIds) { _giftSeatFlightController.enqueueLimited( RoomGiftSeatFlightRequest( imagePath: giftPhoto, targetUserId: targetUserId, beginSize: 96.w, endSize: 28.w, queueTag: sessionKey, batchTag: roundBatchTag, ), maxTrackedRequests: maxTrackedRequests, ); } } } void _scheduleGiftAnimationSessionEnd(String sessionKey) { final session = _luckyGiftComboSessions[sessionKey]; if (session == null) { return; } session.endTimer?.cancel(); session.endTimer = Timer(_giftAnimationSessionWindow, () { if (!mounted) { return; } final activeSession = _luckyGiftComboSessions[sessionKey]; if (activeSession == null) { return; } if (!_giftSeatFlightController.hasTrackedRequests(sessionKey)) { activeSession.dispose(); _luckyGiftComboSessions.remove(sessionKey); return; } activeSession.clearQueueTimer?.cancel(); activeSession.clearQueueTimer = Timer(_giftAnimationQueueDrainWindow, () { _giftSeatFlightController.clearQueuedRequests(sessionKey); final expiredSession = _luckyGiftComboSessions.remove(sessionKey); expiredSession?.dispose(); }); }); } List _resolveGiftTargetUserIds(Msg msg) { final targetUserIds = []; void addTargetUserId(String? userId) { final normalizedUserId = (userId ?? "").trim(); if (normalizedUserId.isEmpty || targetUserIds.contains(normalizedUserId)) { return; } targetUserIds.add(normalizedUserId); } for (final userId in msg.targetUserIds ?? const []) { addTargetUserId(userId); } if (msg.type == SCRoomMsgType.luckGiftAnimOther) { final rawTargetUserIds = (msg.msg ?? "").trim(); if (rawTargetUserIds.startsWith("[")) { try { final decoded = jsonDecode(rawTargetUserIds); if (decoded is List) { for (final userId in decoded) { addTargetUserId(userId?.toString()); } } } catch (_) {} } } addTargetUserId(_resolveGiftTargetUserId(msg)); return targetUserIds; } String _buildGiftTickerLabelId(Msg msg, String? targetGroupKey) { final giftId = (msg.gift?.id ?? "").trim(); final senderId = (msg.user?.id ?? "").trim(); if ((targetGroupKey ?? "").trim().isNotEmpty) { return "$giftId|$senderId|$targetGroupKey"; } return "$giftId|$senderId|${msg.toUser?.id ?? ""}"; } String _resolveGiftTickerTargetName(Msg msg, List targetUserIds) { if (targetUserIds.length > 1) { return SCAppLocalizations.of(context)?.multiple ?? "Multiple"; } return msg.toUser?.userNickname ?? ""; } List _normalizeGiftTargetUserIds(List targetUserIds) { final normalizedTargetUserIds = []; for (final targetUserId in targetUserIds) { final normalizedTargetUserId = targetUserId.trim(); if (normalizedTargetUserId.isEmpty || normalizedTargetUserIds.contains(normalizedTargetUserId)) { continue; } normalizedTargetUserIds.add(normalizedTargetUserId); } return normalizedTargetUserIds; } String? _buildGiftTargetGroupKey(List targetUserIds) { final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds); if (normalizedTargetUserIds.isEmpty) { return null; } normalizedTargetUserIds.sort(); return normalizedTargetUserIds.join(","); } bool _markGiftVisualBatchForPlayback(Msg msg, String targetGroupKey) { final batchKey = _buildGiftVisualBatchKey(msg, targetGroupKey); if (batchKey.isEmpty) { return true; } if (_giftVisualBatchDedupeTimers.containsKey(batchKey)) { return false; } final hasStableBatchId = (msg.giftBatchId ?? "").trim().isNotEmpty; final dedupeWindow = hasStableBatchId ? _giftVisualBatchDedupeWindow : _giftFlightBatchDedupeWindow; late final Timer timer; timer = Timer(dedupeWindow, () { if (identical(_giftVisualBatchDedupeTimers[batchKey], timer)) { _giftVisualBatchDedupeTimers.remove(batchKey); } }); _giftVisualBatchDedupeTimers[batchKey] = timer; return true; } String _buildGiftVisualBatchKey(Msg msg, String targetGroupKey) { final stableBatchId = (msg.giftBatchId ?? "").trim(); if (stableBatchId.isNotEmpty) { return "batch|$stableBatchId"; } final timeBucket = ((msg.time ?? DateTime.now().millisecondsSinceEpoch) / 150).floor(); return [ "legacy", msg.type ?? "", msg.groupId ?? "", msg.gift?.id ?? "", msg.user?.id ?? "", targetGroupKey, msg.number?.toString() ?? "", msg.customAnimationCount?.toString() ?? "", timeBucket.toString(), ].join("|"); } String _buildGiftFlightBatchKey(Msg msg, List targetUserIds) { final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds); normalizedTargetUserIds.sort(); final timeBucket = ((msg.time ?? DateTime.now().millisecondsSinceEpoch) / 650).floor(); return [ msg.type ?? "", msg.gift?.id ?? "", msg.user?.id ?? "", msg.number?.toString() ?? "", msg.customAnimationCount?.toString() ?? "", normalizedTargetUserIds.join(","), timeBucket.toString(), ].join("|"); } bool _markGiftFlightBatchForPlayback(String batchKey) { final normalizedBatchKey = batchKey.trim(); if (normalizedBatchKey.isEmpty) { return true; } if (_giftFlightBatchDedupeTimers.containsKey(normalizedBatchKey)) { return false; } late final Timer timer; timer = Timer(_giftFlightBatchDedupeWindow, () { if (identical(_giftFlightBatchDedupeTimers[normalizedBatchKey], timer)) { _giftFlightBatchDedupeTimers.remove(normalizedBatchKey); } }); _giftFlightBatchDedupeTimers[normalizedBatchKey] = timer; return true; } String _buildLuckyGiftComboSessionKey(Msg msg, String? targetUserId) { final senderId = (msg.user?.id ?? '').trim(); final giftId = (msg.gift?.id ?? '').trim(); return '$giftId|$senderId|${targetUserId ?? ""}'; } void _clearLuckyGiftComboSessions() { for (final session in _luckyGiftComboSessions.values) { session.dispose(); } _luckyGiftComboSessions.clear(); } void _clearGiftFlightBatchDedupeTimers() { for (final timer in _giftFlightBatchDedupeTimers.values) { timer.cancel(); } _giftFlightBatchDedupeTimers.clear(); } void _clearGiftVisualBatchDedupeTimers() { for (final timer in _giftVisualBatchDedupeTimers.values) { timer.cancel(); } _giftVisualBatchDedupeTimers.clear(); } bool _shouldPlaySeatFlightGiftAnimation(Msg msg) { final gift = msg.gift; if (gift == null) { return false; } final giftPhoto = (gift.giftPhoto ?? "").trim(); if (giftPhoto.isEmpty) { return false; } final giftPhotoExt = _normalizedGiftResourceExtension(giftPhoto); if (_isAnimatedGiftResource(giftPhotoExt)) { return false; } final giftSourceUrl = (gift.giftSourceUrl ?? "").trim(); final sourceExt = _normalizedGiftResourceExtension(giftSourceUrl); return !_isAnimatedGiftResource(sourceExt); } bool _isAnimatedGiftResource(String extension) { return extension == ".svga" || extension == ".mp4" || extension == ".vap"; } String _normalizedGiftResourceExtension(String resource) { final value = resource.trim(); if (value.isEmpty) { return ""; } final uri = Uri.tryParse(value); if (uri != null && ((uri.scheme.isNotEmpty) || (uri.host.isNotEmpty))) { return SCPathUtils.getFileExtension(uri.path).toLowerCase(); } final normalizedValue = value.split("?").first.split("#").first; return SCPathUtils.getFileExtension(normalizedValue).toLowerCase(); } String? _resolveGiftTargetUserId(Msg msg) { final directUserId = (msg.toUser?.id ?? "").trim(); if (directUserId.isNotEmpty) { return directUserId; } final targetAccount = (msg.toUser?.account ?? "").trim(); if (targetAccount.isEmpty) { return null; } final rtcProvider = Provider.of(context, listen: false); for (final micRes in rtcProvider.roomWheatMap.values) { if ((micRes.user?.account ?? "").trim() == targetAccount) { final userId = (micRes.user?.id ?? "").trim(); if (userId.isNotEmpty) { return userId; } } } return null; } } class _LuckyGiftComboSession { Timer? endTimer; Timer? clearQueueTimer; int totalCount = 0; int highestPlayedMilestone = 0; void dispose() { endTimer?.cancel(); clearQueueTimer?.cancel(); } } class _RoomStartupSnapshot { const _RoomStartupSnapshot({ required this.status, required this.failureType, required this.failureToken, }); final RoomStartupStatus status; final RoomStartupFailureType failureType; final int failureToken; @override bool operator ==(Object other) { if (identical(this, other)) { return true; } return other is _RoomStartupSnapshot && other.status == status && other.failureType == failureType && other.failureToken == failureToken; } @override int get hashCode => Object.hash(status, failureType, failureToken); } class _RoomStartupFailureDialog extends StatelessWidget { const _RoomStartupFailureDialog({ required this.title, required this.message, required this.btnText, required this.onEnsure, }); final String title; final String message; final String btnText; final VoidCallback onEnsure; @override Widget build(BuildContext context) { return Material( type: MaterialType.transparency, child: Center( child: Container( constraints: BoxConstraints(maxWidth: 320.w), margin: EdgeInsets.symmetric(horizontal: 22.w), padding: EdgeInsets.fromLTRB(18.w, 18.w, 18.w, 30.w), decoration: BoxDecoration( color: const Color(0xff09372E), borderRadius: BorderRadius.circular(12.w), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( title, textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 16.sp, fontWeight: FontWeight.w700, decoration: TextDecoration.none, ), ), SizedBox(height: 14.w), Text( message, textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 14.sp, height: 1.25, decoration: TextDecoration.none, ), ), SizedBox(height: 18.w), GestureDetector( behavior: HitTestBehavior.opaque, onTap: onEnsure, child: Container( width: 160.w, height: 38.w, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xff18F2B1), borderRadius: BorderRadius.circular(12.w), ), child: Text( btnText, style: TextStyle( color: Colors.black, fontSize: 14.sp, fontWeight: FontWeight.w700, decoration: TextDecoration.none, ), ), ), ), ], ), ), ), ); } }