import 'dart:async'; import 'dart:collection'; import 'package:flutter/foundation.dart'; import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; typedef RoomGiftComboSendExecutor = Future Function( RoomGiftComboSendRequest request, { required String trigger, }); class RoomGiftComboSendRequest { const RoomGiftComboSendRequest({ required this.acceptUserIds, required this.acceptUsers, required this.gift, required this.quantity, required this.clickCount, required this.roomId, required this.roomAccount, required this.isLuckyGiftRequest, }); final List acceptUserIds; final List acceptUsers; final SocialChatGiftRes gift; final int quantity; final int clickCount; final String roomId; final String roomAccount; final bool isLuckyGiftRequest; RoomGiftComboSendRequest copyWith({ List? acceptUserIds, List? acceptUsers, SocialChatGiftRes? gift, int? quantity, int? clickCount, String? roomId, String? roomAccount, bool? isLuckyGiftRequest, }) { return RoomGiftComboSendRequest( acceptUserIds: List.from(acceptUserIds ?? this.acceptUserIds), acceptUsers: List.from(acceptUsers ?? this.acceptUsers), gift: gift ?? this.gift, quantity: quantity ?? this.quantity, clickCount: clickCount ?? this.clickCount, roomId: roomId ?? this.roomId, roomAccount: roomAccount ?? this.roomAccount, isLuckyGiftRequest: isLuckyGiftRequest ?? this.isLuckyGiftRequest, ); } String get batchKey { final sortedAcceptUserIds = List.from(acceptUserIds)..sort(); return '${isLuckyGiftRequest ? "lucky" : "gift"}' '|${gift.id ?? ""}' '|$roomId' '|${sortedAcceptUserIds.join(",")}'; } } class RoomGiftComboSendController extends ChangeNotifier { static final RoomGiftComboSendController _instance = RoomGiftComboSendController._internal(); factory RoomGiftComboSendController() => _instance; RoomGiftComboSendController._internal(); static const Duration comboFeedbackDuration = Duration(seconds: 3); static const Duration _comboSendBatchWindow = Duration(milliseconds: 200); static const Duration _countdownRefreshInterval = Duration(milliseconds: 50); final ListQueue<_PendingRoomGiftComboSendBatch> _queue = ListQueue<_PendingRoomGiftComboSendBatch>(); RoomGiftComboSendRequest? _activeRequest; RoomGiftComboSendExecutor? _executor; Timer? _countdownTimer; Timer? _batchTimer; DateTime? _countdownDeadline; bool _isBatchInFlight = false; bool _visible = false; bool get isVisible => _visible && _activeRequest != null && _executor != null; double get countdownProgress { if (!isVisible) { return 0; } final deadline = _countdownDeadline; if (deadline == null) { return 0; } final totalMs = comboFeedbackDuration.inMilliseconds; if (totalMs <= 0) { return 1; } final remainingMs = deadline.difference(DateTime.now()).inMilliseconds; if (remainingMs <= 0) { return 1; } return (1 - remainingMs / totalMs).clamp(0.0, 1.0).toDouble(); } void show({ required RoomGiftComboSendRequest request, required RoomGiftComboSendExecutor executor, }) { _activeRequest = request.copyWith(); _executor = executor; _visible = true; _restartCountdown(); notifyListeners(); } Future sendActiveRequest() async { final request = _activeRequest; final executor = _executor; if (request == null || executor == null) { return; } _visible = true; _restartCountdown(); _enqueueRequest(request.copyWith(clickCount: 1)); notifyListeners(); } void hide() { _countdownTimer?.cancel(); _countdownTimer = null; _countdownDeadline = null; if (_visible) { _visible = false; notifyListeners(); } } void clear() { _countdownTimer?.cancel(); _countdownTimer = null; _batchTimer?.cancel(); _batchTimer = null; _countdownDeadline = null; _queue.clear(); _activeRequest = null; _executor = null; _isBatchInFlight = false; _visible = false; notifyListeners(); } void _restartCountdown() { _countdownDeadline = DateTime.now().add(comboFeedbackDuration); _countdownTimer?.cancel(); _countdownTimer = Timer.periodic(_countdownRefreshInterval, (timer) { final deadline = _countdownDeadline; if (deadline == null) { timer.cancel(); return; } if (!DateTime.now().isBefore(deadline)) { timer.cancel(); _countdownDeadline = null; if (_visible) { _visible = false; notifyListeners(); } return; } notifyListeners(); }); } void _enqueueRequest(RoomGiftComboSendRequest request) { final now = DateTime.now(); _PendingRoomGiftComboSendBatch? existingBatch; for (final batch in _queue) { if (batch.batchKey == request.batchKey) { existingBatch = batch; break; } } if (existingBatch != null) { existingBatch.quantity += request.quantity; existingBatch.clickCount += request.clickCount; existingBatch.readyAt = now.add(_comboSendBatchWindow); } else { _queue.add( _PendingRoomGiftComboSendBatch.fromRequest( request, readyAt: now.add(_comboSendBatchWindow), ), ); } _scheduleNextFlush(); } void _scheduleNextFlush() { _batchTimer?.cancel(); _batchTimer = null; if (_isBatchInFlight || _queue.isEmpty) { return; } final nextBatch = _queue.first; final delay = nextBatch.readyAt.difference(DateTime.now()); if (delay <= Duration.zero) { unawaited(_flushNextBatch()); return; } _batchTimer = Timer(delay, () { unawaited(_flushNextBatch()); }); } Future _flushNextBatch() async { _batchTimer?.cancel(); _batchTimer = null; if (_isBatchInFlight || _queue.isEmpty) { return; } final executor = _executor; if (executor == null) { _queue.clear(); return; } final batch = _queue.first; if (batch.readyAt.isAfter(DateTime.now())) { _scheduleNextFlush(); return; } _queue.removeFirst(); _isBatchInFlight = true; try { await executor(batch.toRequest(), trigger: 'floating_batched'); } finally { _isBatchInFlight = false; _scheduleNextFlush(); } } } class _PendingRoomGiftComboSendBatch { _PendingRoomGiftComboSendBatch({ required this.batchKey, required this.acceptUserIds, required this.acceptUsers, required this.gift, required this.quantity, required this.clickCount, required this.roomId, required this.roomAccount, required this.isLuckyGiftRequest, required this.readyAt, }); factory _PendingRoomGiftComboSendBatch.fromRequest( RoomGiftComboSendRequest request, { required DateTime readyAt, }) { return _PendingRoomGiftComboSendBatch( batchKey: request.batchKey, acceptUserIds: List.from(request.acceptUserIds), acceptUsers: List.from(request.acceptUsers), gift: request.gift, quantity: request.quantity, clickCount: request.clickCount, roomId: request.roomId, roomAccount: request.roomAccount, isLuckyGiftRequest: request.isLuckyGiftRequest, readyAt: readyAt, ); } final String batchKey; final List acceptUserIds; final List acceptUsers; final SocialChatGiftRes gift; int quantity; int clickCount; final String roomId; final String roomAccount; final bool isLuckyGiftRequest; DateTime readyAt; RoomGiftComboSendRequest toRequest() { return RoomGiftComboSendRequest( acceptUserIds: List.from(acceptUserIds), acceptUsers: List.from(acceptUsers), gift: gift, quantity: quantity, clickCount: clickCount, roomId: roomId, roomAccount: roomAccount, isLuckyGiftRequest: isLuckyGiftRequest, ); } }