chatapp3-flutter/lib/services/gift/room_gift_combo_send_controller.dart
2026-04-18 19:00:19 +08:00

317 lines
8.1 KiB
Dart

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<void> 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<String> acceptUserIds;
final List<MicRes> acceptUsers;
final SocialChatGiftRes gift;
final int quantity;
final int clickCount;
final String roomId;
final String roomAccount;
final bool isLuckyGiftRequest;
RoomGiftComboSendRequest copyWith({
List<String>? acceptUserIds,
List<MicRes>? acceptUsers,
SocialChatGiftRes? gift,
int? quantity,
int? clickCount,
String? roomId,
String? roomAccount,
bool? isLuckyGiftRequest,
}) {
return RoomGiftComboSendRequest(
acceptUserIds: List<String>.from(acceptUserIds ?? this.acceptUserIds),
acceptUsers: List<MicRes>.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<String>.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<void> 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<void> _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<String>.from(request.acceptUserIds),
acceptUsers: List<MicRes>.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<String> acceptUserIds;
final List<MicRes> 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<String>.from(acceptUserIds),
acceptUsers: List<MicRes>.from(acceptUsers),
gift: gift,
quantity: quantity,
clickCount: clickCount,
roomId: roomId,
roomAccount: roomAccount,
isLuckyGiftRequest: isLuckyGiftRequest,
);
}
}