import 'dart:math' as math; import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; 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/audio/rtm_manager.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; import 'package:yumi/shared/tools/sc_path_utils.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/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/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 { 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; late TabController _tabController; final List _pages = [AllChatPage(), ChatPage(), GiftChatPage()]; final List _tabs = []; late StreamSubscription _subscription; final RoomGiftSeatFlightController _giftSeatFlightController = RoomGiftSeatFlightController(); final Map _luckyGiftComboSessions = {}; @override void initState() { super.initState(); _tabController = TabController(length: _pages.length, vsync: this); _enableRoomVisualEffects(); _tabController.addListener(() {}); // 监听切换 _subscription = eventBus.on().listen(( event, ) { if (mounted) { Provider.of(context, listen: false).clearAllGiftData(); Provider.of( context, listen: false, ).toggleGiftAnimationVisibility(false); _clearLuckyGiftComboSessions(); _giftSeatFlightController.clear(); } }); } @override void didChangeDependencies() { super.didChangeDependencies(); _ensureRoomVisualEffectsEnabled(); } @override void dispose() { _suspendRoomVisualEffects(); _tabController.dispose(); // 释放资源 _subscription.cancel(); super.dispose(); } void _enableRoomVisualEffects() { Provider.of( context, listen: false, ).setRoomVisualEffectsEnabled(true); Provider.of(context, listen: false).msgFloatingGiftListener = _floatingGiftListener; } void _ensureRoomVisualEffectsEnabled() { final rtcProvider = Provider.of(context, listen: false); if (rtcProvider.currenRoom == null || rtcProvider.roomVisualEffectsEnabled) { return; } _enableRoomVisualEffects(); } void _suspendRoomVisualEffects() { final rtcProvider = Provider.of(context, listen: false); rtcProvider.setRoomVisualEffectsEnabled(false); final rtmProvider = Provider.of(context, listen: false); if (rtmProvider.msgFloatingGiftListener == _floatingGiftListener) { rtmProvider.msgFloatingGiftListener = null; } RoomEntranceHelper.clearQueue(); _clearLuckyGiftComboSessions(); _giftSeatFlightController.clear(); SCGiftVapSvgaManager().stopPlayback(); } bool roomThemeBackActi(JoinRoomRes? room) { if (room?.roomProps?.roomTheme != null) { if (room?.roomProps?.roomTheme?.themeBack != null && room!.roomProps!.roomTheme!.themeBack!.isNotEmpty) { if ((room.roomProps?.roomTheme?.expireTime ?? 0) > DateTime.now().millisecondsSinceEpoch) { return true; } } } return false; } @override Widget build(BuildContext context) { _tabs.clear(); _tabs.add(Tab(text: SCAppLocalizations.of(context)!.all)); _tabs.add(Tab(text: SCAppLocalizations.of(context)!.chat)); _tabs.add(Tab(text: SCAppLocalizations.of(context)!.gift)); return PopScope( canPop: false, onPopInvokedWithResult: (bool didPop, Object? result) { if (!didPop) { _suspendRoomVisualEffects(); SCFloatIchart().show(); SCNavigatorUtils.goBack(context); } }, child: Scaffold( backgroundColor: SCGlobalConfig.businessLogicStrategy.getVoiceRoomBackgroundColor(), resizeToAvoidBottomInset: false, body: SafeArea( top: false, child: Stack( children: [ Consumer( builder: (context, ref, child) { return roomThemeBackActi(ref.currenRoom) ? netImage( url: ref.currenRoom?.roomProps?.roomTheme?.themeBack ?? "", 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(), RoomBottomWidget()], ), LGiftAnimalPage(), Transform.translate( offset: Offset(0, -20), child: RoomAnimationQueueScreen(), ), ], ), ), ], ), // _buildPlayViews(), ///幸运礼物中奖动画 LuckGiftNomorAnimWidget(), ], ), ), ), ); } ///消息 Widget _buildChatView() { return Expanded( child: Stack( alignment: AlignmentDirectional.bottomEnd, children: [ Column( children: [ TabBar( tabAlignment: TabAlignment.start, labelPadding: SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabLabelPadding() .copyWith( left: SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabLabelPadding() .left * ScreenUtil().setWidth(1), right: SCGlobalConfig.businessLogicStrategy .getVoiceRoomTabLabelPadding() .right * ScreenUtil().setWidth(1), ), 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: _tabs, ), 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: _pages, ), ), ), ), ], ), RoomPlayWidget(), ], ), ); } ///礼物上飘动画 _floatingGiftListener(Msg msg) { if (!Provider.of( context, listen: false, ).shouldShowRoomVisualEffects) { return; } if (Provider.of(context, listen: false).hideLGiftAnimal) { return; } var giftModel = LGiftModel(); final giftTab = (msg.gift?.giftTab ?? '').trim(); 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 = msg.number ?? 0; giftModel.isLuckyGift = giftTab == "LUCK" || giftTab == SCGiftType.LUCKY_GIFT.name; Provider.of( context, listen: false, ).enqueueGiftAnimation(giftModel); final giftPhoto = (msg.gift?.giftPhoto ?? "").trim(); final targetUserId = _resolveGiftTargetUserId(msg); if (_supportsComboMilestoneEffects(msg)) { _handleComboMilestoneVisuals(msg, targetUserId); } final isLuckyGift = _isLuckyGiftMessage(msg); if (isLuckyGift) { _handleLuckyGiftComboVisuals(msg, giftPhoto, targetUserId); return; } _handleStandardGiftComboVisuals(msg, giftPhoto, targetUserId); } 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(); session.totalCount += quantity; final highestMilestone = SocialChatGiftSystemManager.resolveHighestReachedComboMilestone( 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? targetUserId, ) { if ((msg.number ?? 0) <= 0) { return; } if (!_shouldPlaySeatFlightGiftAnimation(msg) || targetUserId == null || giftPhoto.isEmpty) { return; } final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId); final session = _luckyGiftComboSessions.putIfAbsent( sessionKey, () => _LuckyGiftComboSession(), ); session.endTimer?.cancel(); session.clearQueueTimer?.cancel(); _enqueueTrackedSeatFlightAnimations( sessionKey: sessionKey, giftPhoto: giftPhoto, targetUserId: targetUserId, animationCount: _resolveTrackedAnimationCount(msg), ); _scheduleGiftAnimationSessionEnd(sessionKey); } void _handleLuckyGiftComboVisuals( Msg msg, String giftPhoto, String? targetUserId, ) { if ((msg.number ?? 0) <= 0) { return; } final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId); final session = _luckyGiftComboSessions[sessionKey]; if (session == null) { return; } session.endTimer?.cancel(); session.clearQueueTimer?.cancel(); if (_shouldPlaySeatFlightGiftAnimation(msg) && targetUserId != null && giftPhoto.isNotEmpty) { _enqueueTrackedSeatFlightAnimations( sessionKey: sessionKey, giftPhoto: giftPhoto, targetUserId: targetUserId, animationCount: _resolveTrackedAnimationCount(msg), ); } _scheduleGiftAnimationSessionEnd(sessionKey); } int _resolveTrackedAnimationCount(Msg msg) { return math.max(msg.customAnimationCount ?? 1, 1); } void _enqueueTrackedSeatFlightAnimations({ required String sessionKey, required String giftPhoto, required String targetUserId, required int animationCount, }) { final normalizedAnimationCount = math.max(animationCount, 1); final cappedAnimationCount = math.min( normalizedAnimationCount, _maxTrackedGiftAnimations, ); for (var index = 0; index < cappedAnimationCount; index += 1) { _giftSeatFlightController.enqueueLimited( RoomGiftSeatFlightRequest( imagePath: giftPhoto, targetUserId: targetUserId, beginSize: 96.w, endSize: 28.w, queueTag: sessionKey, ), maxTrackedRequests: _maxTrackedGiftAnimations, ); } } 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(); }); }); } 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(); } 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(); } }