1079 lines
31 KiB
Dart
1079 lines
31 KiB
Dart
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_cp_gift_relation_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/ui_kit/widgets/room/floating/floating_cp_relation_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);
|
|
static const Duration _rocketFloatingFallbackDedupWindow = Duration(
|
|
seconds: 30,
|
|
);
|
|
static const Duration _gameWinFloatingDedupWindow = Duration(seconds: 10);
|
|
static const int _maxStackedFloatingVisibleCount = 2;
|
|
static const List<int> _stackedFloatingOrder = [6, 5, 1, 4, 3];
|
|
static const double _stackedFloatingBaseTop = 70;
|
|
static const double _stackedFloatingGap = 8;
|
|
|
|
final SCPriorityQueue<SCFloatingMessage> _messageQueue = SCPriorityQueue(
|
|
(a, b) => b.priority.compareTo(a.priority),
|
|
);
|
|
final List<SCFloatingMessage> _deferredRegionRedPacketMessages = [];
|
|
final Map<String, DateTime> _recentCompensationKeys = {};
|
|
final Map<String, DateTime> _recentFloatingKeys = {};
|
|
final Map<int, OverlayEntry> _stackedOverlayEntries = {};
|
|
final Map<int, SCFloatingMessage> _stackedMessages = {};
|
|
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;
|
|
}
|
|
_rememberFloatingMessage(message);
|
|
_enqueueMessage(
|
|
message,
|
|
debugLabel: 'floating_message_type_${message.type ?? -1}',
|
|
);
|
|
} else {
|
|
_removeActiveMessage(scheduleNext: false);
|
|
_removeAllStackedMessages();
|
|
_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) <
|
|
_floatingDedupWindowFor(message);
|
|
}
|
|
|
|
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.isNotEmpty) {
|
|
return 'room_rocket_launch|$launchNo';
|
|
}
|
|
final roomId = message.roomId?.trim() ?? '';
|
|
final level = message.rocketLevel ?? 0;
|
|
if (roomId.isEmpty || level <= 0) {
|
|
return null;
|
|
}
|
|
return 'room_rocket_launch_fallback|$roomId|$level';
|
|
}
|
|
if (message.type == 4) {
|
|
final packetId = message.toUserId?.trim() ?? '';
|
|
if (packetId.isEmpty) {
|
|
return null;
|
|
}
|
|
return 'room_red_packet|$packetId';
|
|
}
|
|
if (message.type == 2) {
|
|
final roundId = message.giftId?.trim() ?? '';
|
|
final userId = message.userId?.trim() ?? '';
|
|
if (roundId.isNotEmpty) {
|
|
return 'game_win|$roundId|$userId';
|
|
}
|
|
final roomId = message.roomId?.trim() ?? '';
|
|
final gameId = message.gameId?.trim() ?? '';
|
|
final coins = message.coins ?? 0;
|
|
if (roomId.isEmpty && userId.isEmpty && gameId.isEmpty && coins == 0) {
|
|
return null;
|
|
}
|
|
return 'game_win_fallback|$roomId|$userId|$gameId|$coins';
|
|
}
|
|
if (message.type == 6) {
|
|
final roomId = message.roomId?.trim() ?? '';
|
|
final action = message.giftId?.trim() ?? '';
|
|
final relationType = message.giftTab?.trim() ?? '';
|
|
final userId = message.userId?.trim() ?? '';
|
|
final toUserId = message.toUserId?.trim() ?? '';
|
|
if (roomId.isEmpty &&
|
|
action.isEmpty &&
|
|
relationType.isEmpty &&
|
|
userId.isEmpty &&
|
|
toUserId.isEmpty) {
|
|
return null;
|
|
}
|
|
return 'cp_relation|$roomId|$action|$relationType|$userId|$toUserId';
|
|
}
|
|
return null;
|
|
}
|
|
|
|
Duration _floatingDedupWindowFor(SCFloatingMessage message) {
|
|
if (message.type == 2) {
|
|
return _gameWinFloatingDedupWindow;
|
|
}
|
|
if (message.type == 6) {
|
|
return _gameWinFloatingDedupWindow;
|
|
}
|
|
if (message.type == 3 && (message.giftId?.trim() ?? '').isEmpty) {
|
|
return _rocketFloatingFallbackDedupWindow;
|
|
}
|
|
return _redPacketFloatingDedupWindow;
|
|
}
|
|
|
|
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<SCFloatingMessage>().toList()
|
|
..sort((a, b) => b.priority.compareTo(a.priority));
|
|
_messageQueue.clear();
|
|
for (final message in retained.take(
|
|
_maxLowPerformanceCompensationQueueLength - 1,
|
|
)) {
|
|
_messageQueue.add(message);
|
|
}
|
|
}
|
|
|
|
Future<void> _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 || message.type == 6 ? 350 : 96,
|
|
logicalHeight: message.type == 5 || message.type == 6 ? 84 : 96,
|
|
),
|
|
);
|
|
if (message.type == 0) {
|
|
unawaited(_warmLuckyGiftBannerSvga());
|
|
}
|
|
if (message.type == 2) {
|
|
unawaited(_warmGameWinBannerSvga());
|
|
}
|
|
}
|
|
|
|
Future<void> _warmLuckyGiftBannerSvga() async {
|
|
try {
|
|
await SCSvgaAssetWidget.preloadAsset(
|
|
FloatingLuckGiftScreenWidget.backgroundSvgaAssetPath,
|
|
);
|
|
} catch (error) {
|
|
// Preload failure is non-fatal; the banner will retry during rendering.
|
|
}
|
|
}
|
|
|
|
Future<void> _warmGameWinBannerSvga() async {
|
|
try {
|
|
await SCSvgaAssetWidget.preloadAsset(
|
|
FloatingGameScreenWidget.backgroundSvgaAssetPath,
|
|
);
|
|
} catch (error) {
|
|
// Preload failure is non-fatal; the banner will retry during rendering.
|
|
}
|
|
}
|
|
|
|
void _safeScheduleNext() {
|
|
if (_isSuppressed) return;
|
|
if (_isProcessing || _messageQueue.isEmpty) return;
|
|
_isProcessing = true;
|
|
|
|
try {
|
|
_scheduleNext();
|
|
} finally {
|
|
_isProcessing = false;
|
|
}
|
|
}
|
|
|
|
void _scheduleNext() {
|
|
if (_messageQueue.isEmpty || _isDisposed || _isSuppressed) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
final context = navigatorKey.currentState?.context;
|
|
if (context == null || !context.mounted) return;
|
|
|
|
while (_messageQueue.isNotEmpty) {
|
|
final messageToPlay = _removeNextPlayableMessage(context);
|
|
if (messageToPlay == null) {
|
|
return;
|
|
}
|
|
_playMessage(messageToPlay);
|
|
if (!_isStackedFloatingMessage(messageToPlay)) {
|
|
return;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
_isPlaying = false;
|
|
_safeScheduleNext();
|
|
}
|
|
}
|
|
|
|
SCFloatingMessage? _removeNextPlayableMessage(BuildContext context) {
|
|
final skippedMessages = <SCFloatingMessage>[];
|
|
SCFloatingMessage? selectedMessage;
|
|
|
|
while (_messageQueue.isNotEmpty) {
|
|
final message = _messageQueue.removeFirst();
|
|
if (!_shouldDisplayMessage(context, message)) {
|
|
if (_shouldDeferRegionRedPacketMessage(context, message)) {
|
|
_deferRegionRedPacketMessage(context, message);
|
|
continue;
|
|
}
|
|
_logRegionRedPacketDrop(context, message);
|
|
continue;
|
|
}
|
|
if (!_canPlayMessage(message)) {
|
|
skippedMessages.add(message);
|
|
continue;
|
|
}
|
|
selectedMessage = message;
|
|
break;
|
|
}
|
|
|
|
for (final message in skippedMessages) {
|
|
_messageQueue.add(message);
|
|
}
|
|
return selectedMessage;
|
|
}
|
|
|
|
bool _canPlayMessage(SCFloatingMessage message) {
|
|
final slotKey = _stackSlotKeyFor(message);
|
|
if (slotKey != null) {
|
|
return !_isPlaying &&
|
|
!_stackedOverlayEntries.containsKey(slotKey) &&
|
|
_stackedOverlayEntries.length < _maxStackedFloatingVisibleCount;
|
|
}
|
|
return !_isPlaying && _stackedOverlayEntries.isEmpty;
|
|
}
|
|
|
|
void _playMessage(SCFloatingMessage message) {
|
|
if (_isStackedFloatingMessage(message)) {
|
|
_playStackedMessage(message);
|
|
return;
|
|
}
|
|
|
|
_isPlaying = true;
|
|
_currentMessage = message;
|
|
final context = navigatorKey.currentState?.context;
|
|
if (context == null || !context.mounted) {
|
|
_isPlaying = false;
|
|
_currentMessage = null;
|
|
_safeScheduleNext();
|
|
return;
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
_currentOverlayEntry = OverlayEntry(
|
|
builder:
|
|
(_) => Align(
|
|
alignment: AlignmentDirectional.topStart,
|
|
child: Transform.translate(
|
|
offset: Offset(0, 70.w),
|
|
child: RepaintBoundary(
|
|
child: _buildScreenWidget(message, onComplete),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
|
|
Overlay.of(context).insert(_currentOverlayEntry!);
|
|
}
|
|
|
|
void _playStackedMessage(SCFloatingMessage message) {
|
|
final slotKey = _stackSlotKeyFor(message);
|
|
final context = navigatorKey.currentState?.context;
|
|
if (slotKey == null || context == null || !context.mounted) {
|
|
return;
|
|
}
|
|
if (_stackedOverlayEntries.containsKey(slotKey)) {
|
|
_messageQueue.add(message);
|
|
return;
|
|
}
|
|
|
|
bool completed = false;
|
|
|
|
void onComplete() {
|
|
if (completed) return;
|
|
completed = true;
|
|
|
|
_removeStackedMessage(slotKey);
|
|
_markStackedEntriesNeedsBuild();
|
|
_safeScheduleNext();
|
|
}
|
|
|
|
_stackedMessages[slotKey] = message;
|
|
final entry = OverlayEntry(
|
|
builder:
|
|
(_) => Align(
|
|
alignment: AlignmentDirectional.topStart,
|
|
child: Transform.translate(
|
|
offset: Offset(0, _stackedWrapperTopFor(message).w),
|
|
child: RepaintBoundary(
|
|
child: _buildScreenWidget(message, onComplete),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
_stackedOverlayEntries[slotKey] = entry;
|
|
Overlay.of(context).insert(entry);
|
|
_markStackedEntriesNeedsBuild();
|
|
}
|
|
|
|
Widget _buildScreenWidget(
|
|
SCFloatingMessage message,
|
|
VoidCallback onComplete,
|
|
) {
|
|
switch (message.type) {
|
|
case 0:
|
|
return FloatingLuckGiftScreenWidget(
|
|
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,
|
|
);
|
|
case 6:
|
|
//CP / 挚友关系飘窗
|
|
return FloatingCpRelationScreenWidget(
|
|
message: message,
|
|
onAnimationCompleted: onComplete,
|
|
);
|
|
default:
|
|
onComplete();
|
|
return Container();
|
|
}
|
|
}
|
|
|
|
int? _stackSlotKeyFor(SCFloatingMessage message) {
|
|
switch (message.type) {
|
|
case 0:
|
|
case 1:
|
|
return 1;
|
|
case 3:
|
|
return 3;
|
|
case 4:
|
|
return 4;
|
|
case 5:
|
|
return 5;
|
|
case 6:
|
|
return 6;
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
bool _isStackedFloatingMessage(SCFloatingMessage message) {
|
|
return _stackSlotKeyFor(message) != null;
|
|
}
|
|
|
|
double _stackedWrapperTopFor(SCFloatingMessage message) {
|
|
final slotKey = _stackSlotKeyFor(message);
|
|
if (slotKey == null) {
|
|
return _stackedFloatingBaseTop;
|
|
}
|
|
var visualTop = _stackedFloatingBaseTop;
|
|
for (final key in _stackedFloatingOrder) {
|
|
if (key == slotKey) {
|
|
break;
|
|
}
|
|
final previousMessage = _stackedMessages[key];
|
|
if (previousMessage != null) {
|
|
visualTop +=
|
|
_stackedVisualHeightFor(previousMessage) + _stackedFloatingGap;
|
|
}
|
|
}
|
|
final wrapperTop = visualTop - _stackedInternalTopFor(message);
|
|
return wrapperTop < 0 ? 0 : wrapperTop;
|
|
}
|
|
|
|
double _stackedVisualHeightFor(SCFloatingMessage message) {
|
|
switch (message.type) {
|
|
case 0:
|
|
return 83;
|
|
case 1:
|
|
return scIsCpGiftTab(message.giftTab) ? 65 : 54;
|
|
case 3:
|
|
return 68;
|
|
case 4:
|
|
return 66;
|
|
case 5:
|
|
return 84;
|
|
case 6:
|
|
return FloatingCpRelationScreenWidget.bannerHeight;
|
|
default:
|
|
return 68;
|
|
}
|
|
}
|
|
|
|
double _stackedInternalTopFor(SCFloatingMessage message) {
|
|
switch (message.type) {
|
|
case 1:
|
|
return 20;
|
|
case 4:
|
|
return 42;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
void _markStackedEntriesNeedsBuild() {
|
|
for (final entry in _stackedOverlayEntries.values) {
|
|
entry.markNeedsBuild();
|
|
}
|
|
}
|
|
|
|
void _removeStackedMessage(int slotKey) {
|
|
try {
|
|
_stackedOverlayEntries.remove(slotKey)?.remove();
|
|
} catch (_) {}
|
|
_stackedMessages.remove(slotKey);
|
|
}
|
|
|
|
void _removeAllStackedMessages() {
|
|
final slotKeys = List<int>.from(_stackedOverlayEntries.keys);
|
|
for (final slotKey in slotKeys) {
|
|
_removeStackedMessage(slotKey);
|
|
}
|
|
}
|
|
|
|
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);
|
|
_removeAllStackedMessages();
|
|
_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);
|
|
_removeAllStackedMessages();
|
|
_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(scheduleNext: false);
|
|
}
|
|
if (context != null && context.mounted) {
|
|
_removeHiddenStackedMessages(context);
|
|
}
|
|
_safeScheduleNext();
|
|
}
|
|
|
|
void dispose() {
|
|
_isDisposed = true;
|
|
_homeRootTabsVisible = false;
|
|
_suppressedCount = 0;
|
|
_currentOverlayEntry?.remove();
|
|
_currentOverlayEntry = null;
|
|
_currentMessage = null;
|
|
_removeAllStackedMessages();
|
|
_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);
|
|
_removeMessagesByType(6);
|
|
if (normalizedRoomId.isNotEmpty) {
|
|
_removeMessagesByType(3, roomId: normalizedRoomId);
|
|
}
|
|
if (normalizedRoomId.isNotEmpty) {
|
|
_removeMessagesByType(2, roomId: normalizedRoomId);
|
|
}
|
|
if (normalizedRoomId.isNotEmpty) {
|
|
_removeMessagesByType(4, roomId: normalizedRoomId);
|
|
}
|
|
}
|
|
|
|
// 辅助方法:移除特定类型的消息
|
|
void _removeMessagesByType(int type, {String? roomId}) {
|
|
final normalizedRoomId = roomId?.trim() ?? '';
|
|
// 由于 PriorityQueue 没有直接的方法来移除特定元素,
|
|
// 我们需要重建队列,排除指定类型的消息
|
|
final newQueue = SCPriorityQueue<SCFloatingMessage>(
|
|
(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());
|
|
}
|
|
|
|
_removeStackedMessagesWhere((message) {
|
|
final matchesType = message.type == type;
|
|
final matchesRoom =
|
|
normalizedRoomId.isEmpty ||
|
|
(message.roomId ?? "").trim() == normalizedRoomId;
|
|
return matchesType && matchesRoom;
|
|
});
|
|
}
|
|
|
|
// 辅助方法:获取队列信息
|
|
int get queueLength => _messageQueue.length;
|
|
|
|
bool get isPlaying => _isPlaying || _stackedOverlayEntries.isNotEmpty;
|
|
|
|
bool get _isSuppressed => _suppressedCount > 0;
|
|
|
|
bool _isRegionRedPacketMessage(SCFloatingMessage message) {
|
|
return message.type == 4 && message.isRegionBroadcast;
|
|
}
|
|
|
|
bool _isRegionRoomRocketMessage(SCFloatingMessage message) {
|
|
return message.type == 3 && message.isRegionBroadcast;
|
|
}
|
|
|
|
bool _isRegionGameWinMessage(SCFloatingMessage message) {
|
|
return message.type == 2 && message.isRegionBroadcast;
|
|
}
|
|
|
|
bool _isCrossRoomRegionFloatingMessage(SCFloatingMessage message) {
|
|
return _isRegionRedPacketMessage(message) ||
|
|
_isRegionRoomRocketMessage(message) ||
|
|
_isRegionGameWinMessage(message);
|
|
}
|
|
|
|
bool _shouldDeferRegionRedPacketMessage(
|
|
BuildContext context,
|
|
SCFloatingMessage message,
|
|
) {
|
|
if (!_isCrossRoomRegionFloatingMessage(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<SCFloatingMessage>.from(
|
|
_deferredRegionRedPacketMessages,
|
|
);
|
|
_deferredRegionRedPacketMessages.clear();
|
|
for (final message in messages) {
|
|
_messageQueue.add(message);
|
|
}
|
|
}
|
|
|
|
void _logRegionRedPacketDrop(
|
|
BuildContext context,
|
|
SCFloatingMessage message,
|
|
) {
|
|
if (message.type != 4) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
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 (message.type == 6) {
|
|
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<RealTimeCommunicationManager>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
if (!rtcProvider.shouldShowRoomVisualEffects ||
|
|
!rtcProvider.isVoiceRoomRouteVisible) {
|
|
return false;
|
|
}
|
|
final currentRoomId =
|
|
rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? "";
|
|
final currentRoomGroupId =
|
|
rtcProvider.currenRoom?.roomProfile?.roomProfile?.roomAccount?.trim() ??
|
|
"";
|
|
final messageRoomId = (message.roomId ?? "").trim();
|
|
if (currentRoomId.isEmpty || messageRoomId.isEmpty) {
|
|
return false;
|
|
}
|
|
return currentRoomId == messageRoomId ||
|
|
(currentRoomGroupId.isNotEmpty && currentRoomGroupId == messageRoomId);
|
|
}
|
|
|
|
bool _shouldDisplayRegionBroadcastMessage(
|
|
BuildContext context,
|
|
SCFloatingMessage message,
|
|
) {
|
|
if (SCGlobalConfig.isFloatingBroadcastClosed) {
|
|
return false;
|
|
}
|
|
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
|
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 || message.type == 3 || message.type == 2;
|
|
}
|
|
final isCurrentRoomMessage =
|
|
currentRoomId.isNotEmpty &&
|
|
messageRoomId.isNotEmpty &&
|
|
currentRoomId == messageRoomId;
|
|
if ((message.type == 4 || message.type == 3 || message.type == 2) &&
|
|
isCurrentRoomMessage) {
|
|
return false;
|
|
}
|
|
if (message.type == 2) {
|
|
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
|
|
return false;
|
|
}
|
|
if (rtcProvider.shouldShowRoomVisualEffects) {
|
|
return true;
|
|
}
|
|
return SCGlobalConfig.isFloatingBroadcastAll && _homeRootTabsVisible;
|
|
}
|
|
if ((message.type == 4 || message.type == 3) &&
|
|
rtcProvider.shouldShowRoomVisualEffects) {
|
|
return true;
|
|
}
|
|
if (message.type == 4 || message.type == 3) {
|
|
return _homeRootTabsVisible;
|
|
}
|
|
if (SCGlobalConfig.isFloatingBroadcastRoomOnly) {
|
|
return false;
|
|
}
|
|
if (rtcProvider.shouldShowRoomVisualEffects) {
|
|
return false;
|
|
}
|
|
return SCGlobalConfig.isFloatingBroadcastAll && _homeRootTabsVisible;
|
|
}
|
|
|
|
void _removeRegionBroadcastMessages() {
|
|
final newQueue = SCPriorityQueue<SCFloatingMessage>(
|
|
(a, b) => b.priority.compareTo(a.priority),
|
|
);
|
|
|
|
while (_messageQueue.isNotEmpty) {
|
|
final message = _messageQueue.removeFirst();
|
|
if (!message.isRegionBroadcast ||
|
|
_isCrossRoomRegionFloatingMessage(message)) {
|
|
newQueue.add(message);
|
|
}
|
|
}
|
|
|
|
while (newQueue.isNotEmpty) {
|
|
_messageQueue.add(newQueue.removeFirst());
|
|
}
|
|
}
|
|
|
|
void _removeActiveRegionBroadcastMessage() {
|
|
final activeMessage = _currentMessage;
|
|
if (activeMessage != null && activeMessage.isRegionBroadcast) {
|
|
_currentOverlayEntry?.remove();
|
|
_currentOverlayEntry = null;
|
|
_currentMessage = null;
|
|
_isPlaying = false;
|
|
}
|
|
_removeStackedMessagesWhere((message) => message.isRegionBroadcast);
|
|
_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 == 2 ||
|
|
activeMessage.type == 3 ||
|
|
activeMessage.type == 5 ||
|
|
(includeRedPacket && activeMessage.type == 4)) &&
|
|
(normalizedRoomId.isEmpty ||
|
|
(activeMessage.roomId ?? "").trim() == normalizedRoomId)) {
|
|
_removeActiveMessage(scheduleNext: false);
|
|
}
|
|
_removeStackedRoomMessages(
|
|
roomId: normalizedRoomId,
|
|
includeRedPacket: includeRedPacket,
|
|
);
|
|
_safeScheduleNext();
|
|
}
|
|
|
|
void _removeActiveMessage({bool scheduleNext = true}) {
|
|
_currentOverlayEntry?.remove();
|
|
_currentOverlayEntry = null;
|
|
_currentMessage = null;
|
|
_isPlaying = false;
|
|
if (scheduleNext) {
|
|
_safeScheduleNext();
|
|
}
|
|
}
|
|
|
|
void _removeHiddenStackedMessages(BuildContext context) {
|
|
_removeStackedMessagesWhere(
|
|
(message) => !_shouldDisplayMessage(context, message),
|
|
);
|
|
}
|
|
|
|
void _removeStackedRoomMessages({
|
|
required String roomId,
|
|
required bool includeRedPacket,
|
|
}) {
|
|
_removeStackedMessagesWhere((message) {
|
|
final type = message.type;
|
|
final isRoomFloatingType =
|
|
type == 0 ||
|
|
type == 1 ||
|
|
type == 2 ||
|
|
type == 3 ||
|
|
type == 5 ||
|
|
(includeRedPacket && type == 4);
|
|
if (!isRoomFloatingType) {
|
|
return false;
|
|
}
|
|
return roomId.isEmpty || (message.roomId ?? "").trim() == roomId;
|
|
});
|
|
}
|
|
|
|
void _removeStackedMessagesWhere(
|
|
bool Function(SCFloatingMessage message) test,
|
|
) {
|
|
final matchedSlotKeys = <int>[];
|
|
_stackedMessages.forEach((slotKey, message) {
|
|
if (test(message)) {
|
|
matchedSlotKeys.add(slotKey);
|
|
}
|
|
});
|
|
if (matchedSlotKeys.isEmpty) {
|
|
return;
|
|
}
|
|
for (final slotKey in matchedSlotKeys) {
|
|
_removeStackedMessage(slotKey);
|
|
}
|
|
_markStackedEntriesNeedsBuild();
|
|
}
|
|
}
|