import 'dart:async'; import 'package:flutter/cupertino.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/main.dart'; import 'package:provider/provider.dart'; import 'package:yumi/shared/tools/sc_network_image_utils.dart'; import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; import 'package:yumi/shared/tools/sc_entrance_vap_svga_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/ui_kit/widgets/room/floating/floating_game_screen_widget.dart'; import 'package:yumi/ui_kit/widgets/room/floating/floating_gift_screen_widget.dart'; import 'package:yumi/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart'; import 'package:yumi/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart'; import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart'; import 'package:yumi/ui_kit/widgets/room/floating/floating_vip_entry_screen_widget.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; typedef FloatingScreenManager = OverlayManager; class OverlayManager { static const int _maxLowPerformanceCompensationQueueLength = 8; static const int _maxDeferredRegionRedPacketCount = 12; static const Duration _compensationDedupWindow = Duration(seconds: 3); static const Duration _redPacketFloatingDedupWindow = Duration(minutes: 30); final SCPriorityQueue _messageQueue = SCPriorityQueue( (a, b) => b.priority.compareTo(a.priority), ); final List _deferredRegionRedPacketMessages = []; final Map _recentCompensationKeys = {}; final Map _recentFloatingKeys = {}; bool _isPlaying = false; OverlayEntry? _currentOverlayEntry; SCFloatingMessage? _currentMessage; bool _homeRootTabsVisible = false; int _suppressedCount = 0; bool _isProcessing = false; bool _isDisposed = false; static final OverlayManager _instance = OverlayManager._internal(); factory OverlayManager() => _instance; OverlayManager._internal(); void addMessage(SCFloatingMessage message) { if (_isDisposed || _isSuppressed) return; if (!SCGlobalConfig.isFloatingBroadcastClosed) { if (_isRecentFloatingDuplicate(message)) { return; } if (message.type == 4) {} _rememberFloatingMessage(message); _enqueueMessage( message, debugLabel: 'floating_message_type_${message.type ?? -1}', ); } else { _removeActiveMessage(scheduleNext: false); _messageQueue.clear(); _deferredRegionRedPacketMessages.clear(); _isPlaying = false; _isProcessing = false; _isDisposed = false; } } void addLowPerformanceCompensationMessage( SCFloatingMessage message, { String? dedupKey, }) { if (_isDisposed || _isSuppressed || !SCGlobalConfig.isLowPerformanceDevice || SCGlobalConfig.isFloatingBroadcastClosed) { return; } final key = dedupKey ?? _compensationKeyFor(message); if (_isRecentCompensationDuplicate(key)) { return; } _rememberCompensationKey(key); _trimLowPerformanceCompensationQueue(); _enqueueMessage( message, debugLabel: 'low_performance_compensation_${message.type ?? -1}', ); } void _enqueueMessage( SCFloatingMessage message, { required String debugLabel, }) { unawaited(_warmMessageImages(message)); SCRoomEffectScheduler().scheduleDeferredEffect( debugLabel: debugLabel, action: () { if (_isDisposed || _isSuppressed) { return; } _messageQueue.add(message); _safeScheduleNext(); }, ); } bool _isRecentCompensationDuplicate(String key) { _pruneRecentCompensationKeys(); final recentTime = _recentCompensationKeys[key]; if (recentTime == null) { return false; } return DateTime.now().difference(recentTime) < _compensationDedupWindow; } void _rememberCompensationKey(String key) { _recentCompensationKeys[key] = DateTime.now(); _pruneRecentCompensationKeys(); } void _pruneRecentCompensationKeys() { final now = DateTime.now(); _recentCompensationKeys.removeWhere( (_, time) => now.difference(time) > _compensationDedupWindow, ); } bool _isRecentFloatingDuplicate(SCFloatingMessage message) { _pruneRecentFloatingKeys(); final key = _floatingDedupKeyFor(message); if (key == null) { return false; } final recentTime = _recentFloatingKeys[key]; if (recentTime == null) { return false; } return DateTime.now().difference(recentTime) < _redPacketFloatingDedupWindow; } void _rememberFloatingMessage(SCFloatingMessage message) { final key = _floatingDedupKeyFor(message); if (key == null) { return; } _recentFloatingKeys[key] = DateTime.now(); _pruneRecentFloatingKeys(); } void _pruneRecentFloatingKeys() { final now = DateTime.now(); _recentFloatingKeys.removeWhere( (_, time) => now.difference(time) > _redPacketFloatingDedupWindow, ); } String? _floatingDedupKeyFor(SCFloatingMessage message) { if (message.type == 3) { final launchNo = message.giftId?.trim() ?? ''; if (launchNo.isEmpty) { return null; } return 'room_rocket_launch|$launchNo'; } if (message.type == 4) { final packetId = message.toUserId?.trim() ?? ''; if (packetId.isEmpty) { return null; } return 'room_red_packet|$packetId'; } return null; } String _compensationKeyFor(SCFloatingMessage message) { return [ message.type, message.roomId, message.userId, message.toUserId, message.giftId, message.number, message.giftUrl, ].join('|'); } void _trimLowPerformanceCompensationQueue() { if (_messageQueue.length < _maxLowPerformanceCompensationQueueLength) { return; } final retained = _messageQueue.unorderedElements.cast().toList() ..sort((a, b) => b.priority.compareTo(a.priority)); _messageQueue.clear(); for (final message in retained.take( _maxLowPerformanceCompensationQueueLength - 1, )) { _messageQueue.add(message); } } Future _warmMessageImages(SCFloatingMessage message) async { unawaited( warmImageResource( message.userAvatarUrl ?? "", logicalWidth: 80, logicalHeight: 80, ), ); unawaited( warmImageResource( message.toUserAvatarUrl ?? "", logicalWidth: 80, logicalHeight: 80, ), ); unawaited( warmImageResource( message.giftUrl ?? "", logicalWidth: message.type == 5 ? 350 : 96, logicalHeight: message.type == 5 ? 84 : 96, ), ); if (message.type == 0) { unawaited(_warmLuckyGiftBannerSvga()); } } Future _warmLuckyGiftBannerSvga() async { try { await SCSvgaAssetWidget.preloadAsset( FloatingLuckGiftScreenWidget.backgroundSvgaAssetPath, ); } catch (error) {} } void _safeScheduleNext() { if (_isSuppressed) return; if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return; _isProcessing = true; try { _scheduleNext(); } finally { _isProcessing = false; } } void _scheduleNext() { if (_isPlaying || _messageQueue.isEmpty || _isDisposed || _isSuppressed) { return; } try { final context = navigatorKey.currentState?.context; if (context == null || !context.mounted) return; // 安全地获取第一个消息 if (_messageQueue.isEmpty) return; final messageToProcess = _messageQueue.first; if (messageToProcess == null) { _messageQueue.removeFirst(); _safeScheduleNext(); return; } if (!_shouldDisplayMessage(context, messageToProcess)) { _messageQueue.removeFirst(); if (_shouldDeferRegionRedPacketMessage(context, messageToProcess)) { _deferRegionRedPacketMessage(context, messageToProcess); _safeScheduleNext(); return; } _logRegionRedPacketDrop(context, messageToProcess); _safeScheduleNext(); return; } // 安全地移除并播放消息 final messageToPlay = _messageQueue.removeFirst(); _playMessage(messageToPlay); } catch (e) { _isPlaying = false; _safeScheduleNext(); } } void _playMessage(SCFloatingMessage message) { _isPlaying = true; _currentMessage = message; final context = navigatorKey.currentState?.context; if (context == null || !context.mounted) { _isPlaying = false; _currentMessage = null; _safeScheduleNext(); return; } _currentOverlayEntry = OverlayEntry( builder: (_) => Align( alignment: AlignmentDirectional.topStart, child: Transform.translate( offset: Offset(0, 70.w), child: RepaintBoundary(child: _buildScreenWidget(message)), ), ), ); Overlay.of(context).insert(_currentOverlayEntry!); } Widget _buildScreenWidget(SCFloatingMessage message) { bool completed = false; void onComplete() { if (completed) return; completed = true; try { _currentOverlayEntry?.remove(); _currentOverlayEntry = null; _isPlaying = false; _currentMessage = null; _safeScheduleNext(); } catch (e) { _isPlaying = false; _currentMessage = null; _safeScheduleNext(); } } switch (message.type) { case 0: return FloatingLuckGiftScreenWidget( message: message, onAnimationCompleted: onComplete, ); case 1: //房间礼物 return FloatingGiftScreenWidget( message: message, onAnimationCompleted: onComplete, ); case 2: //游戏中奖 return FloatingGameScreenWidget( message: message, onAnimationCompleted: onComplete, ); case 3: //火箭 return FloatingRoomRocketScreenWidget( message: message, onAnimationCompleted: onComplete, ); case 4: //红包 return FloatingRoomRedenvelopeScreenWidget( message: message, onAnimationCompleted: onComplete, ); case 5: //VIP进房飘窗 return FloatingVipEntryScreenWidget( message: message, onAnimationCompleted: onComplete, ); default: onComplete(); return Container(); } } void activate() { _isDisposed = false; _loadSavedDisplayPreference(); } void _loadSavedDisplayPreference() { final account = AccountStorage().getCurrentUser()?.userProfile?.account; final savedFloatingBroadcastScope = DataPersistence.getString( SCGlobalConfig.floatingBroadcastScopeStorageKey(account), ); if (savedFloatingBroadcastScope.isNotEmpty) { final scope = SCGlobalConfig.clampFloatingBroadcastScope( savedFloatingBroadcastScope, ); SCGlobalConfig.floatingBroadcastScope = scope; return; } final defaultEffectsEnabled = !SCGlobalConfig.isLowPerformanceDevice; final isLegacyFloatingAnimationInGlobal = DataPersistence.getBool( SCGlobalConfig.legacyFloatingAnimationInGlobalStorageKey(account), defaultValue: defaultEffectsEnabled, ); SCGlobalConfig.floatingBroadcastScope = SCGlobalConfig.clampVisualEffectPreference( isLegacyFloatingAnimationInGlobal, ) ? SCGlobalConfig.floatingBroadcastScopeAll : SCGlobalConfig.floatingBroadcastScopeOff; } void setHomeRootTabsVisible(bool visible) { if (_homeRootTabsVisible == visible) { return; } _homeRootTabsVisible = visible; if (!visible) { _removeActiveRegionBroadcastMessage(); _removeRegionBroadcastMessages(); return; } _restoreDeferredRegionRedPacketMessages(); _safeScheduleNext(); } void beginSuppressFloatingScreens({String reason = 'unknown'}) { _suppressedCount += 1; _removeActiveMessage(scheduleNext: false); _messageQueue.clear(); _isPlaying = false; _isProcessing = false; } void endSuppressFloatingScreens({String reason = 'unknown'}) { if (_suppressedCount > 0) { _suppressedCount -= 1; } if (!_isSuppressed) { _safeScheduleNext(); } } void refreshDisplayPreference() { if (_isDisposed) { return; } if (SCGlobalConfig.isFloatingBroadcastClosed) { _removeActiveMessage(scheduleNext: false); _messageQueue.clear(); _deferredRegionRedPacketMessages.clear(); return; } _restoreDeferredRegionRedPacketMessages(); final context = navigatorKey.currentState?.context; final activeMessage = _currentMessage; if (context != null && context.mounted && activeMessage != null && !_shouldDisplayMessage(context, activeMessage)) { _removeActiveMessage(); return; } _safeScheduleNext(); } void dispose() { _isDisposed = true; _homeRootTabsVisible = false; _suppressedCount = 0; _currentOverlayEntry?.remove(); _currentOverlayEntry = null; _currentMessage = null; _messageQueue.clear(); _deferredRegionRedPacketMessages.clear(); _recentCompensationKeys.clear(); _isPlaying = false; _isProcessing = false; } void removeRoom({String? roomId}) { final normalizedRoomId = roomId?.trim() ?? ''; _removeActiveRoomMessage( roomId: normalizedRoomId, includeRedPacket: normalizedRoomId.isNotEmpty, ); _removeMessagesByType(1); _removeMessagesByType(0); _removeMessagesByType(5); if (normalizedRoomId.isNotEmpty) { _removeMessagesByType(4, roomId: normalizedRoomId); } } // 辅助方法:移除特定类型的消息 void _removeMessagesByType(int type, {String? roomId}) { final normalizedRoomId = roomId?.trim() ?? ''; // 由于 PriorityQueue 没有直接的方法来移除特定元素, // 我们需要重建队列,排除指定类型的消息 final newQueue = SCPriorityQueue( (a, b) => b.priority.compareTo(a.priority), ); // 遍历原始队列,只添加非指定类型的消息 while (_messageQueue.isNotEmpty) { final message = _messageQueue.removeFirst(); final matchesType = message.type == type; final matchesRoom = normalizedRoomId.isEmpty || (message.roomId ?? "").trim() == normalizedRoomId; if (!matchesType || !matchesRoom) { newQueue.add(message); } } // 将新队列赋值给原队列 while (newQueue.isNotEmpty) { _messageQueue.add(newQueue.removeFirst()); } } // 辅助方法:获取队列信息 int get queueLength => _messageQueue.length; bool get isPlaying => _isPlaying; bool get _isSuppressed => _suppressedCount > 0; bool _isRegionRedPacketMessage(SCFloatingMessage message) { return message.type == 4 && message.isRegionBroadcast; } bool _shouldDeferRegionRedPacketMessage( BuildContext context, SCFloatingMessage message, ) { if (!_isRegionRedPacketMessage(message) || SCGlobalConfig.isFloatingBroadcastClosed) { return false; } return !_shouldDisplayRegionBroadcastMessage(context, message); } void _deferRegionRedPacketMessage( BuildContext context, SCFloatingMessage message, ) { final key = _floatingDedupKeyFor(message); if (key != null && _deferredRegionRedPacketMessages.any( (item) => _floatingDedupKeyFor(item) == key, )) { return; } while (_deferredRegionRedPacketMessages.length >= _maxDeferredRegionRedPacketCount) { _deferredRegionRedPacketMessages.removeAt(0); } _deferredRegionRedPacketMessages.add(message); } void _restoreDeferredRegionRedPacketMessages() { if (_deferredRegionRedPacketMessages.isEmpty) { return; } final messages = List.from( _deferredRegionRedPacketMessages, ); _deferredRegionRedPacketMessages.clear(); for (final message in messages) { _messageQueue.add(message); } } void _logRegionRedPacketDrop( BuildContext context, SCFloatingMessage message, ) { if (message.type != 4) { return; } } String _displayStateForLog(BuildContext context) { final rtcProvider = Provider.of( context, listen: false, ); final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; return 'homeVisible=$_homeRootTabsVisible ' 'roomVisible=${rtcProvider.shouldShowRoomVisualEffects} ' 'currentRoomId=$currentRoomId ' 'scope=${SCGlobalConfig.floatingBroadcastScope}'; } bool _shouldDisplayMessage(BuildContext context, SCFloatingMessage? message) { if (message == null) { return false; } if (SCGlobalConfig.isFloatingBroadcastClosed) { return false; } if (message.isRegionBroadcast) { return _shouldDisplayRegionBroadcastMessage(context, message); } if (message.type == 4) { return _isCurrentRoomFloatingMessage(context, message); } if (SCGlobalConfig.isFloatingBroadcastRoomOnly) { return _isCurrentRoomFloatingMessage(context, message); } if (message.type != 0 && message.type != 1 && message.type != 5) { return true; } return _isCurrentRoomFloatingMessage(context, message); } bool _isCurrentRoomFloatingMessage( BuildContext context, SCFloatingMessage message, ) { final rtcProvider = Provider.of( context, listen: false, ); if (!rtcProvider.shouldShowRoomVisualEffects || !rtcProvider.isVoiceRoomRouteVisible) { return false; } final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; final messageRoomId = (message.roomId ?? "").trim(); if (currentRoomId.isEmpty || messageRoomId.isEmpty) { return false; } return currentRoomId == messageRoomId; } bool _shouldDisplayRegionBroadcastMessage( BuildContext context, SCFloatingMessage message, ) { if (SCGlobalConfig.isFloatingBroadcastClosed) { return false; } final rtcProvider = Provider.of( context, listen: false, ); final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ""; final messageRoomId = (message.roomId ?? "").trim(); final isCurrentVisibleRoom = rtcProvider.shouldShowRoomVisualEffects && rtcProvider.isVoiceRoomRouteVisible && currentRoomId.isNotEmpty && messageRoomId.isNotEmpty && currentRoomId == messageRoomId; if (isCurrentVisibleRoom) { // 区域红包在当前房间仍然需要展示;区域礼物当前房间由 RTM 层跳过,避免和房间 IM 礼物飘屏重复。 return message.type == 4; } final isCurrentRoomMessage = currentRoomId.isNotEmpty && messageRoomId.isNotEmpty && currentRoomId == messageRoomId; if (message.type == 4 && isCurrentRoomMessage) { return false; } if (message.type == 4 && rtcProvider.shouldShowRoomVisualEffects) { return true; } if (message.type == 4) { return _homeRootTabsVisible; } if (SCGlobalConfig.isFloatingBroadcastRoomOnly) { return false; } if (rtcProvider.shouldShowRoomVisualEffects) { return false; } return SCGlobalConfig.isFloatingBroadcastAll && _homeRootTabsVisible; } void _removeRegionBroadcastMessages() { final newQueue = SCPriorityQueue( (a, b) => b.priority.compareTo(a.priority), ); while (_messageQueue.isNotEmpty) { final message = _messageQueue.removeFirst(); if (!message.isRegionBroadcast || _isRegionRedPacketMessage(message)) { newQueue.add(message); } } while (newQueue.isNotEmpty) { _messageQueue.add(newQueue.removeFirst()); } } void _removeActiveRegionBroadcastMessage() { final activeMessage = _currentMessage; if (activeMessage == null || !activeMessage.isRegionBroadcast) { return; } _currentOverlayEntry?.remove(); _currentOverlayEntry = null; _currentMessage = null; _isPlaying = false; _safeScheduleNext(); } void _removeActiveRoomMessage({ String? roomId, bool includeRedPacket = false, }) { final activeMessage = _currentMessage; final normalizedRoomId = roomId?.trim() ?? ''; if (activeMessage == null || (activeMessage.type != 0 && activeMessage.type != 1 && activeMessage.type != 5 && !(includeRedPacket && activeMessage.type == 4))) { return; } if (normalizedRoomId.isNotEmpty && (activeMessage.roomId ?? "").trim() != normalizedRoomId) { return; } _removeActiveMessage(); } void _removeActiveMessage({bool scheduleNext = true}) { _currentOverlayEntry?.remove(); _currentOverlayEntry = null; _currentMessage = null; _isPlaying = false; if (scheduleNext) { _safeScheduleNext(); } } }