2381 lines
76 KiB
Dart
2381 lines
76 KiB
Dart
import 'dart:async';
|
||
import 'dart:collection';
|
||
import 'dart:convert';
|
||
import 'dart:ui' as ui;
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_debouncer/flutter_debouncer.dart';
|
||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||
import 'package:yumi/app_localizations.dart';
|
||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
||
import 'package:yumi/ui_kit/theme/socialchat_theme.dart';
|
||
import 'package:yumi/app/constants/sc_global_config.dart';
|
||
import 'package:yumi/app/config/business_logic_strategy.dart';
|
||
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
|
||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||
import 'package:yumi/shared/data_sources/sources/remote/net/api.dart';
|
||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||
import 'package:yumi/shared/tools/sc_cp_gift_relation_utils.dart';
|
||
import 'package:yumi/main.dart';
|
||
import 'package:provider/provider.dart';
|
||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||
import 'package:yumi/app/constants/sc_screen.dart';
|
||
import 'package:yumi/shared/tools/sc_dialog_utils.dart';
|
||
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||
import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
|
||
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||
import 'package:yumi/services/audio/rtm_manager.dart';
|
||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||
import 'package:yumi/ui_kit/widgets/gift/sc_gift_combo_send_button.dart';
|
||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||
import 'package:yumi/modules/gift/gift_tab_page.dart';
|
||
import 'package:yumi/modules/gift/cp_rights/cp_rights_guide_page.dart';
|
||
import 'package:yumi/services/gift/room_gift_combo_send_controller.dart';
|
||
import 'package:yumi/ui_kit/widgets/room/room_recharge_bottom_sheet.dart';
|
||
import '../../shared/data_sources/models/enum/sc_gift_type.dart';
|
||
import '../../shared/data_sources/models/message/sc_floating_message.dart';
|
||
import '../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart';
|
||
|
||
class _GiftPageTabItem {
|
||
const _GiftPageTabItem({required this.type, required this.label});
|
||
|
||
final String type;
|
||
final String label;
|
||
}
|
||
|
||
class GiftPage extends StatefulWidget {
|
||
final SocialChatUserProfile? toUser;
|
||
final String? initialTabType;
|
||
|
||
const GiftPage({super.key, this.toUser, this.initialTabType});
|
||
|
||
@override
|
||
State<GiftPage> createState() => _GiftPageState();
|
||
}
|
||
|
||
class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||
static const String _cpProfileAssetBase = "sc_images/room/cp_profile";
|
||
static const String _firstRechargeGiftEntryCard =
|
||
"sc_images/first_recharge/room_gift_entry_card.png";
|
||
static const String _backpackGiftTab = SCAppGeneralManager.backpackGiftTab;
|
||
static const Duration _comboSendBatchWindow = Duration(milliseconds: 200);
|
||
|
||
static const List<String> _preferredGiftTabOrder = <String>[
|
||
"ALL",
|
||
_backpackGiftTab,
|
||
"ACTIVITY",
|
||
"LUCKY_GIFT",
|
||
"CP",
|
||
"MAGIC",
|
||
"NSCIONAL_FLAG",
|
||
];
|
||
|
||
TabController? _tabController;
|
||
List<String> _tabTypes = <String>[];
|
||
bool _didApplyInitialTab = false;
|
||
|
||
/// 业务逻辑策略访问器
|
||
BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy;
|
||
|
||
// int checkedIndex = 0;
|
||
SocialChatGiftRes? checkedGift;
|
||
final Map<String, SocialChatGiftRes?> _selectedGiftByTab =
|
||
<String, SocialChatGiftRes?>{};
|
||
RtcProvider? rtcProvider;
|
||
|
||
bool isAll = false;
|
||
List<HeadSelect> listMai = [];
|
||
|
||
bool noShowNumber = false;
|
||
|
||
///选中人员
|
||
Set<String> set = {};
|
||
|
||
///数量的箭头是否朝上
|
||
bool isNumberUp = true;
|
||
|
||
///数量
|
||
int number = 1;
|
||
|
||
int giveType = 1;
|
||
|
||
int giftType = 0;
|
||
Debouncer debouncer = Debouncer();
|
||
final ListQueue<_PendingGiftSendBatch> _comboSendBatchQueue =
|
||
ListQueue<_PendingGiftSendBatch>();
|
||
Timer? _comboSendBatchTimer;
|
||
bool _isComboSendBatchInFlight = false;
|
||
|
||
void _giftFxLog(String message) {
|
||
debugPrint('[GiftFx][gift] $message');
|
||
}
|
||
|
||
String _describeGiftSendError(Object error) {
|
||
if (error is DioException) {
|
||
final requestPath = error.requestOptions.path;
|
||
final statusCode = error.response?.statusCode;
|
||
final responseData = error.response?.data;
|
||
return 'dioType=${error.type} '
|
||
'statusCode=$statusCode '
|
||
'path=$requestPath '
|
||
'message=${error.message} '
|
||
'error=${error.error} '
|
||
'response=$responseData';
|
||
}
|
||
|
||
if (error is NotSuccessException) {
|
||
return 'notSuccess message=${error.message}';
|
||
}
|
||
|
||
return 'type=${error.runtimeType} error=$error';
|
||
}
|
||
|
||
void _applyGiftSelection(SocialChatGiftRes? gift, {bool notify = true}) {
|
||
checkedGift = gift;
|
||
if (gift != null &&
|
||
(gift.giftSourceUrl ?? "").isNotEmpty &&
|
||
scGiftHasFullScreenEffect(gift.special)) {
|
||
SCGiftVapSvgaManager().preload(gift.giftSourceUrl!);
|
||
_giftFxLog(
|
||
'preload selected gift '
|
||
'giftId=${gift.id} '
|
||
'giftName=${gift.giftName} '
|
||
'giftSourceUrl=${gift.giftSourceUrl} '
|
||
'special=${gift.special}',
|
||
);
|
||
}
|
||
number = 1;
|
||
noShowNumber = false;
|
||
giftType = _giftTypeFromTab(gift?.giftTab);
|
||
if (_isCpGift(gift)) {
|
||
_normalizeCpRecipientSelection();
|
||
}
|
||
if (notify) {
|
||
setState(() {});
|
||
}
|
||
}
|
||
|
||
void _handleGiftSelected(String tabType, SocialChatGiftRes? gift) {
|
||
_selectedGiftByTab[tabType] = gift;
|
||
_applyGiftSelection(gift);
|
||
}
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
rtcProvider = Provider.of<RtcProvider>(context, listen: false);
|
||
Provider.of<SCAppGeneralManager>(
|
||
context,
|
||
listen: false,
|
||
).giftList(includeCustomized: true);
|
||
Provider.of<SCAppGeneralManager>(context, listen: false).giftActivityList();
|
||
unawaited(
|
||
Provider.of<SCAppGeneralManager>(
|
||
context,
|
||
listen: false,
|
||
).giftBackpack(forceRefresh: true),
|
||
);
|
||
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
||
rtcProvider?.roomWheatMap.forEach((k, v) {
|
||
if (v.user != null) {
|
||
listMai.add(HeadSelect(widget.toUser == null, v));
|
||
}
|
||
});
|
||
isAll =
|
||
widget.toUser == null &&
|
||
listMai.isNotEmpty &&
|
||
listMai.every((mai) => mai.isSelect);
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_comboSendBatchTimer?.cancel();
|
||
if (_comboSendBatchQueue.isNotEmpty) {
|
||
unawaited(_flushAllPendingComboGiftSends());
|
||
}
|
||
_tabController?.removeListener(_handleTabChanged);
|
||
_tabController?.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
void _handleTabChanged() {
|
||
final controller = _tabController;
|
||
if (!mounted ||
|
||
controller == null ||
|
||
controller.index >= _tabTypes.length) {
|
||
return;
|
||
}
|
||
final ref = Provider.of<SCAppGeneralManager>(context, listen: false);
|
||
_syncSelectedGiftForTab(ref, _tabTypes[controller.index]);
|
||
_normalizeCpRecipientSelection(notify: true);
|
||
}
|
||
|
||
bool get _isCurrentCpGiftTab {
|
||
final controller = _tabController;
|
||
if (controller == null || controller.index >= _tabTypes.length) {
|
||
return false;
|
||
}
|
||
return _isCpGiftTab(_tabTypes[controller.index]);
|
||
}
|
||
|
||
bool _isCpGiftTab(String tabType) => scIsCpGiftTab(tabType);
|
||
|
||
bool _isCpGift(SocialChatGiftRes? gift) => scIsCpGift(gift);
|
||
|
||
bool get _isCpRecipientSingleMode =>
|
||
_isCurrentCpGiftTab || _isCpGift(checkedGift);
|
||
|
||
void _normalizeCpRecipientSelection({bool notify = false}) {
|
||
if (widget.toUser != null ||
|
||
(!_isCurrentCpGiftTab && !_isCpGift(checkedGift))) {
|
||
return;
|
||
}
|
||
var changed = false;
|
||
var keptSelected = false;
|
||
for (final mai in listMai) {
|
||
if (!mai.isSelect) {
|
||
continue;
|
||
}
|
||
if (!keptSelected) {
|
||
keptSelected = true;
|
||
continue;
|
||
}
|
||
mai.isSelect = false;
|
||
changed = true;
|
||
}
|
||
if (isAll) {
|
||
isAll = false;
|
||
changed = true;
|
||
}
|
||
if (giveType == 2) {
|
||
giveType = 1;
|
||
changed = true;
|
||
}
|
||
if (notify && changed && mounted) {
|
||
setState(() {});
|
||
}
|
||
}
|
||
|
||
List<_GiftPageTabItem> _buildGiftTabs(
|
||
BuildContext context,
|
||
SCAppGeneralManager ref,
|
||
) {
|
||
final localizations = SCAppLocalizations.of(context)!;
|
||
final availableTypes =
|
||
ref.giftByTab.entries
|
||
.where((entry) => entry.value.isNotEmpty)
|
||
.where((entry) => entry.key != _backpackGiftTab)
|
||
.map((entry) => entry.key)
|
||
.toList();
|
||
final orderedTypes = <String>[];
|
||
if (availableTypes.remove("ALL") ||
|
||
ref.isGiftListLoading ||
|
||
ref.giftResList.isEmpty) {
|
||
orderedTypes.add("ALL");
|
||
}
|
||
for (final type in _preferredGiftTabOrder) {
|
||
if (type == "ALL") {
|
||
continue;
|
||
}
|
||
if (type == _backpackGiftTab) {
|
||
orderedTypes.add(type);
|
||
continue;
|
||
}
|
||
if (availableTypes.remove(type)) {
|
||
orderedTypes.add(type);
|
||
}
|
||
}
|
||
orderedTypes.addAll(availableTypes);
|
||
if (orderedTypes.isEmpty) {
|
||
orderedTypes.add("ALL");
|
||
}
|
||
return orderedTypes
|
||
.map(
|
||
(type) => _GiftPageTabItem(
|
||
type: type,
|
||
label: _giftTabLabel(localizations, type),
|
||
),
|
||
)
|
||
.toList();
|
||
}
|
||
|
||
void _ensureTabController(
|
||
SCAppGeneralManager ref,
|
||
List<_GiftPageTabItem> tabs,
|
||
) {
|
||
final nextTypes = tabs.map((tab) => tab.type).toList();
|
||
if (_tabController != null && listEquals(_tabTypes, nextTypes)) {
|
||
_applyInitialTabIfNeeded(nextTypes);
|
||
return;
|
||
}
|
||
|
||
final currentType =
|
||
(_tabController != null &&
|
||
_tabTypes.isNotEmpty &&
|
||
_tabController!.index < _tabTypes.length)
|
||
? _tabTypes[_tabController!.index]
|
||
: null;
|
||
final requestedType = widget.initialTabType?.trim();
|
||
final requestedIndex =
|
||
!_didApplyInitialTab && (requestedType?.isNotEmpty ?? false)
|
||
? nextTypes.indexOf(requestedType!)
|
||
: -1;
|
||
final nextIndex =
|
||
requestedIndex >= 0
|
||
? requestedIndex
|
||
: currentType != null
|
||
? nextTypes.indexOf(currentType)
|
||
: 0;
|
||
if (requestedIndex >= 0) {
|
||
_didApplyInitialTab = true;
|
||
}
|
||
_tabController?.removeListener(_handleTabChanged);
|
||
_tabController?.dispose();
|
||
_tabTypes = nextTypes;
|
||
_tabController = TabController(
|
||
length: tabs.length,
|
||
vsync: this,
|
||
initialIndex: nextIndex >= 0 ? nextIndex : 0,
|
||
)..addListener(_handleTabChanged);
|
||
_syncSelectedGiftForTab(
|
||
ref,
|
||
_tabTypes[_tabController!.index],
|
||
notify: false,
|
||
);
|
||
}
|
||
|
||
void _applyInitialTabIfNeeded(List<String> tabTypes) {
|
||
final requestedType = widget.initialTabType?.trim();
|
||
if (_didApplyInitialTab || requestedType == null || requestedType.isEmpty) {
|
||
return;
|
||
}
|
||
final requestedIndex = tabTypes.indexOf(requestedType);
|
||
final controller = _tabController;
|
||
if (requestedIndex < 0 || controller == null) {
|
||
return;
|
||
}
|
||
_didApplyInitialTab = true;
|
||
if (controller.index != requestedIndex) {
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (!mounted || _tabController != controller) {
|
||
return;
|
||
}
|
||
controller.animateTo(requestedIndex);
|
||
});
|
||
}
|
||
}
|
||
|
||
void _syncSelectedGiftForTab(
|
||
SCAppGeneralManager ref,
|
||
String tabType, {
|
||
bool notify = true,
|
||
}) {
|
||
final gifts = ref.giftByTab[tabType] ?? const <SocialChatGiftRes>[];
|
||
final gift = _resolveSelectedGift(tabType, gifts);
|
||
_selectedGiftByTab[tabType] = gift;
|
||
_applyGiftSelection(gift, notify: notify);
|
||
}
|
||
|
||
void _ensureCurrentTabSelection(SCAppGeneralManager ref, String tabType) {
|
||
final gifts = ref.giftByTab[tabType] ?? const <SocialChatGiftRes>[];
|
||
if (_matchesCurrentGift(tabType, gifts)) {
|
||
if (_isCpGiftTab(tabType)) {
|
||
_normalizeCpRecipientSelection();
|
||
}
|
||
return;
|
||
}
|
||
final gift = _resolveSelectedGift(tabType, gifts);
|
||
_selectedGiftByTab[tabType] = gift;
|
||
_applyGiftSelection(gift, notify: false);
|
||
if (_isCpGiftTab(tabType)) {
|
||
_normalizeCpRecipientSelection();
|
||
}
|
||
}
|
||
|
||
SocialChatGiftRes? _resolveSelectedGift(
|
||
String tabType,
|
||
List<SocialChatGiftRes> gifts,
|
||
) {
|
||
if (gifts.isEmpty) {
|
||
return null;
|
||
}
|
||
|
||
final selectedGift = _selectedGiftByTab[tabType];
|
||
if (selectedGift?.id != null) {
|
||
for (final gift in gifts) {
|
||
if (tabType == _backpackGiftTab) {
|
||
final selectedBackpackId = selectedGift!.backpackId;
|
||
if ((selectedBackpackId ?? "").isNotEmpty &&
|
||
gift.backpackId == selectedBackpackId) {
|
||
return gift;
|
||
}
|
||
}
|
||
if (gift.id == selectedGift!.id) {
|
||
return gift;
|
||
}
|
||
}
|
||
}
|
||
|
||
if (tabType == "CUSTOMIZED") {
|
||
return gifts.length > 1 ? gifts[1] : null;
|
||
}
|
||
return gifts.first;
|
||
}
|
||
|
||
bool _matchesCurrentGift(String tabType, List<SocialChatGiftRes> gifts) {
|
||
final currentGiftId = checkedGift?.id;
|
||
if (currentGiftId == null) {
|
||
return gifts.isEmpty;
|
||
}
|
||
for (final gift in gifts) {
|
||
if (gift.id != currentGiftId) {
|
||
continue;
|
||
}
|
||
if (tabType != _backpackGiftTab) {
|
||
return true;
|
||
}
|
||
return gift.backpackId == checkedGift?.backpackId &&
|
||
gift.backpackQuantity == checkedGift?.backpackQuantity;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
String _giftTabLabel(SCAppLocalizations localizations, String tabType) {
|
||
switch (tabType) {
|
||
case "ALL":
|
||
return localizations.all;
|
||
case "ACTIVITY":
|
||
return localizations.activity;
|
||
case "LUCK":
|
||
case "LUCKY_GIFT":
|
||
return localizations.luck;
|
||
case "CP":
|
||
return "CP";
|
||
case "MAGIC":
|
||
return localizations.magic;
|
||
case "CUSTOMIZED":
|
||
return localizations.customized;
|
||
case "NSCIONAL_FLAG":
|
||
return localizations.country;
|
||
case _backpackGiftTab:
|
||
return localizations.bag;
|
||
default:
|
||
return tabType
|
||
.toLowerCase()
|
||
.split("_")
|
||
.map(
|
||
(word) =>
|
||
word.isEmpty
|
||
? word
|
||
: "${word[0].toUpperCase()}${word.substring(1)}",
|
||
)
|
||
.join(" ");
|
||
}
|
||
}
|
||
|
||
int _giftTypeFromTab(String? tabType) {
|
||
switch (tabType) {
|
||
case "ACTIVITY":
|
||
return 1;
|
||
case "LUCK":
|
||
case "LUCKY_GIFT":
|
||
return 2;
|
||
case "CP":
|
||
return 3;
|
||
case "MAGIC":
|
||
return 5;
|
||
default:
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
bool _usesLuckyGiftEndpoint(SocialChatGiftRes? gift) {
|
||
final giftTab = (gift?.giftTab ?? '').trim();
|
||
return giftTab == "LUCK" ||
|
||
giftTab == SCGiftType.LUCKY_GIFT.name ||
|
||
giftTab == SCGiftType.MAGIC.name;
|
||
}
|
||
|
||
bool _usesBackpackGiftEndpoint(SocialChatGiftRes? gift) {
|
||
return (gift?.giftTab ?? '').trim().toLowerCase() == _backpackGiftTab;
|
||
}
|
||
|
||
bool _hasValidLuckyGiftStandardId(SocialChatGiftRes gift) {
|
||
final standardId = (gift.standardId ?? '').trim();
|
||
return standardId.isNotEmpty && standardId != '0';
|
||
}
|
||
|
||
String _resolveGiftSendErrorMessage(Object error) {
|
||
String sanitize(String message) {
|
||
var value = message.trim();
|
||
const prefixes = <String>[
|
||
'Exception: ',
|
||
'DioException: ',
|
||
'DioException [unknown]: ',
|
||
];
|
||
for (final prefix in prefixes) {
|
||
if (value.startsWith(prefix)) {
|
||
value = value.substring(prefix.length).trim();
|
||
}
|
||
}
|
||
return value;
|
||
}
|
||
|
||
if (error is DioException) {
|
||
final responseData = error.response?.data;
|
||
if (responseData is Map) {
|
||
final responseMessage = sanitize(
|
||
responseData['errorMsg']?.toString() ?? '',
|
||
);
|
||
if (responseMessage.isNotEmpty) {
|
||
return responseMessage;
|
||
}
|
||
}
|
||
|
||
if (error.error is NotSuccessException) {
|
||
final responseMessage = sanitize(
|
||
(error.error as NotSuccessException).message,
|
||
);
|
||
if (responseMessage.isNotEmpty) {
|
||
return responseMessage;
|
||
}
|
||
}
|
||
|
||
final dioMessage = sanitize(error.message ?? '');
|
||
if (dioMessage.isNotEmpty) {
|
||
return dioMessage;
|
||
}
|
||
|
||
final nestedMessage = sanitize(error.error?.toString() ?? '');
|
||
if (nestedMessage.isNotEmpty) {
|
||
return nestedMessage;
|
||
}
|
||
}
|
||
|
||
if (error is NotSuccessException) {
|
||
final responseMessage = sanitize(error.message);
|
||
if (responseMessage.isNotEmpty) {
|
||
return responseMessage;
|
||
}
|
||
}
|
||
|
||
final fallbackMessage = sanitize(error.toString());
|
||
if (fallbackMessage.isNotEmpty) {
|
||
return fallbackMessage;
|
||
}
|
||
|
||
return 'Gift sending failed, please try again.';
|
||
}
|
||
|
||
String _backpackQuantityInsufficientMessage() {
|
||
return 'Gift quantity is insufficient or has been used up.';
|
||
}
|
||
|
||
bool _isBackpackQuantityError(Object error) {
|
||
final errorText = _readBackendErrorText(error).toUpperCase();
|
||
return errorText.contains('INSUFFICIENT_BALANCE');
|
||
}
|
||
|
||
bool _isInsufficientMoneyError(Object error, String resolvedMessage) {
|
||
final errorText =
|
||
'${_readBackendErrorText(error)} $resolvedMessage'.toLowerCase();
|
||
return errorText.contains('no enough money') ||
|
||
errorText.contains('not enough money') ||
|
||
errorText.contains('insufficient money') ||
|
||
errorText.contains('insufficient balance') ||
|
||
errorText.contains('insufficient_balance') ||
|
||
errorText.contains('balance not enough') ||
|
||
errorText.contains('not enough gold') ||
|
||
errorText.contains('insufficient gold');
|
||
}
|
||
|
||
String _readBackendErrorText(Object error) {
|
||
final values = <String>[];
|
||
void addValue(dynamic value) {
|
||
final text = value?.toString().trim() ?? '';
|
||
if (text.isNotEmpty) {
|
||
values.add(text);
|
||
}
|
||
}
|
||
|
||
if (error is DioException) {
|
||
final responseData = error.response?.data;
|
||
if (responseData is Map) {
|
||
addValue(responseData['errorCodeName']);
|
||
addValue(responseData['codeName']);
|
||
addValue(responseData['errorCode']);
|
||
addValue(responseData['code']);
|
||
addValue(responseData['errorMsg']);
|
||
addValue(responseData['message']);
|
||
} else {
|
||
addValue(responseData);
|
||
}
|
||
addValue(error.message);
|
||
addValue(error.error);
|
||
} else if (error is NotSuccessException) {
|
||
addValue(error.message);
|
||
} else {
|
||
addValue(error);
|
||
}
|
||
|
||
return values.join(' ');
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Consumer<SCAppGeneralManager>(
|
||
builder: (context, ref, child) {
|
||
final giftTabs = _buildGiftTabs(context, ref);
|
||
_ensureTabController(ref, giftTabs);
|
||
final tabController = _tabController;
|
||
if (tabController == null) {
|
||
return const SizedBox.shrink();
|
||
}
|
||
final currentTabType = giftTabs[tabController.index].type;
|
||
final isCpTab = currentTabType == "CP";
|
||
final showFirstRechargeEntry =
|
||
!SCGlobalConfig.isReview &&
|
||
!isCpTab &&
|
||
ref.shouldShowFirstRechargeReward;
|
||
_ensureCurrentTabSelection(ref, giftTabs[tabController.index].type);
|
||
final isCpRecipientSingleMode = isCpTab || _isCpGift(checkedGift);
|
||
return SafeArea(
|
||
top: false,
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
_buildGiftHead(),
|
||
if (showFirstRechargeEntry) ...[
|
||
Padding(
|
||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||
child: _buildFirstRechargeGiftEntry(),
|
||
),
|
||
SizedBox(height: 8.w),
|
||
],
|
||
if (isCpTab) ...[
|
||
Padding(
|
||
padding: EdgeInsets.symmetric(horizontal: 10.w),
|
||
child: _buildCpGiftCloseFriendEntry(),
|
||
),
|
||
SizedBox(height: 8.w),
|
||
],
|
||
ClipRRect(
|
||
borderRadius: BorderRadius.only(
|
||
topLeft: Radius.circular(12.0),
|
||
topRight: Radius.circular(12.0),
|
||
),
|
||
child: BackdropFilter(
|
||
filter: ui.ImageFilter.blur(sigmaX: 15, sigmaY: 15),
|
||
child: Container(
|
||
color: const Color(0xff09372E).withValues(alpha: 0.5),
|
||
constraints: BoxConstraints(maxHeight: 430.w),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
SizedBox(height: 12.w),
|
||
Row(
|
||
children: [
|
||
SizedBox(width: 8.w),
|
||
widget.toUser == null
|
||
? Builder(
|
||
builder: (ct) {
|
||
return Opacity(
|
||
opacity:
|
||
isCpRecipientSingleMode ? 0.45 : 1,
|
||
child: GestureDetector(
|
||
onTap:
|
||
isCpRecipientSingleMode
|
||
? null
|
||
: () {
|
||
isAll = !isAll;
|
||
for (var mai in listMai) {
|
||
mai.isSelect = isAll;
|
||
}
|
||
setState(() {});
|
||
// showGiveTypeDialog(ct);
|
||
},
|
||
child: Image.asset(
|
||
!isCpRecipientSingleMode && isAll
|
||
? "sc_images/room/sc_icon_gift_all_en.png"
|
||
: "sc_images/room/sc_icon_gift_all_no.png",
|
||
width: 28.w,
|
||
height: 28.w,
|
||
),
|
||
),
|
||
);
|
||
},
|
||
)
|
||
: Container(),
|
||
SizedBox(width: 8.w),
|
||
widget.toUser == null
|
||
? Expanded(
|
||
child: SingleChildScrollView(
|
||
scrollDirection: Axis.horizontal,
|
||
child: Row(
|
||
children:
|
||
listMai
|
||
.map((e) => _maiHead(e))
|
||
.toList(),
|
||
),
|
||
),
|
||
)
|
||
: Stack(
|
||
alignment: AlignmentDirectional.center,
|
||
children: [
|
||
Container(
|
||
padding: EdgeInsets.all(2.w),
|
||
child: netImage(
|
||
url: widget.toUser?.userAvatar ?? "",
|
||
width: 26.w,
|
||
defaultImg:
|
||
_strategy
|
||
.getMePageDefaultAvatarImage(),
|
||
shape: BoxShape.circle,
|
||
),
|
||
),
|
||
PositionedDirectional(
|
||
bottom: 0,
|
||
end: 0,
|
||
child: Image.asset(
|
||
"sc_images/login/sc_icon_login_ser_select.png",
|
||
width: 10.w,
|
||
height: 10.w,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
SizedBox(width: 12.w),
|
||
],
|
||
),
|
||
Row(
|
||
children: [
|
||
SizedBox(width: 5.w),
|
||
Expanded(
|
||
child: SizedBox(
|
||
height: 28.w,
|
||
child: TabBar(
|
||
tabAlignment: TabAlignment.start,
|
||
labelPadding: EdgeInsets.symmetric(
|
||
horizontal: 8.w,
|
||
),
|
||
labelColor: SocialChatTheme.primaryLight,
|
||
|
||
indicatorWeight: 0,
|
||
isScrollable: true,
|
||
indicator: SCFixedWidthTabIndicator(
|
||
width: 15.w,
|
||
color: SocialChatTheme.primaryLight,
|
||
),
|
||
unselectedLabelColor: Colors.white54,
|
||
labelStyle: TextStyle(fontSize: 14.sp),
|
||
unselectedLabelStyle: TextStyle(
|
||
fontSize: 12.sp,
|
||
),
|
||
indicatorColor: Colors.transparent,
|
||
dividerColor: Colors.transparent,
|
||
controller: tabController,
|
||
tabs:
|
||
giftTabs
|
||
.map((tab) => Tab(text: tab.label))
|
||
.toList(),
|
||
),
|
||
),
|
||
),
|
||
SizedBox(width: 5.w),
|
||
],
|
||
),
|
||
SizedBox(
|
||
height: kGiftTabPageHeight.w,
|
||
child: TabBarView(
|
||
physics: NeverScrollableScrollPhysics(),
|
||
controller: tabController,
|
||
children:
|
||
giftTabs
|
||
.map(
|
||
(tab) => GiftTabPage(
|
||
key: ValueKey(tab.type),
|
||
tab.type,
|
||
(gift) =>
|
||
_handleGiftSelected(tab.type, gift),
|
||
),
|
||
)
|
||
.toList(),
|
||
),
|
||
),
|
||
Row(
|
||
children: [
|
||
SizedBox(width: 15.w),
|
||
GestureDetector(
|
||
onTap: _openRoomRechargeBottomSheet,
|
||
child: Container(
|
||
padding: EdgeInsets.symmetric(
|
||
vertical: 8.w,
|
||
horizontal: 8.w,
|
||
),
|
||
width: 120.w,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white10,
|
||
borderRadius: BorderRadius.circular(5),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Image.asset(
|
||
_strategy.getGiftPageGoldCoinIcon(),
|
||
width: 14.w,
|
||
height: 14.w,
|
||
),
|
||
SizedBox(width: 5.w),
|
||
Consumer<SocialChatUserProfileManager>(
|
||
builder: (context, ref, child) {
|
||
return Expanded(
|
||
child: text(
|
||
"${ref.myBalance}",
|
||
fontSize: 12.sp,
|
||
),
|
||
);
|
||
},
|
||
),
|
||
SizedBox(width: 5.w),
|
||
Icon(
|
||
Icons.arrow_forward_ios,
|
||
color: Colors.white,
|
||
size: 14.w,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
Spacer(),
|
||
Builder(
|
||
builder: (ct) {
|
||
return GestureDetector(
|
||
behavior: HitTestBehavior.opaque,
|
||
onTap: () {
|
||
if (noShowNumber) {
|
||
return;
|
||
}
|
||
isNumberUp = false;
|
||
_showNumber(ct);
|
||
setState(() {});
|
||
},
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white10,
|
||
borderRadius: BorderRadius.circular(5),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
SizedBox(width: 10.w),
|
||
text("$number", fontSize: 12.sp),
|
||
Icon(
|
||
isNumberUp
|
||
? Icons.keyboard_arrow_up
|
||
: Icons.keyboard_arrow_down,
|
||
color: Colors.white,
|
||
size: 20.w,
|
||
),
|
||
SizedBox(width: 5.w),
|
||
_buildSendButton(),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
SizedBox(width: 15.w),
|
||
],
|
||
),
|
||
SizedBox(height: 15.w),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
void showGiveTypeDialog(BuildContext ct) {
|
||
SmartDialog.showAttach(
|
||
tag: "showGiveType",
|
||
targetContext: ct,
|
||
alignment: Alignment.bottomCenter,
|
||
animationType: SmartAnimationType.fade,
|
||
scalePointBuilder: (selfSize) => Offset(selfSize.width, 10),
|
||
builder: (_) {
|
||
return Container(
|
||
height: 135.w,
|
||
width: 200.w,
|
||
decoration: BoxDecoration(
|
||
image: DecorationImage(
|
||
image: AssetImage(_strategy.getGiftPageGiveTypeBackground()),
|
||
fit: BoxFit.fill,
|
||
),
|
||
),
|
||
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
|
||
child: Column(
|
||
children: [
|
||
SizedBox(height: 5.w),
|
||
Expanded(
|
||
child: GestureDetector(
|
||
child: Row(
|
||
children: [
|
||
Image.asset(
|
||
_strategy.getGiftPageAllOnMicrophoneIcon(),
|
||
height: 18.w,
|
||
width: 18.w,
|
||
),
|
||
SizedBox(width: 8.w),
|
||
Expanded(
|
||
child: text(
|
||
SCAppLocalizations.of(context)!.allOnMicrophone,
|
||
fontSize: 14.sp,
|
||
textColor: Colors.white54,
|
||
),
|
||
),
|
||
SizedBox(width: 8.w),
|
||
Image.asset(
|
||
giveType == 0
|
||
? _strategy.getCommonSelectIcon()
|
||
: _strategy.getCommonUnselectIcon(),
|
||
width: 15.w,
|
||
height: 15.w,
|
||
),
|
||
],
|
||
),
|
||
onTap: () {
|
||
giveType = 0;
|
||
SmartDialog.dismiss(tag: "showGiveType");
|
||
selecteAllMicUsers();
|
||
setState(() {});
|
||
},
|
||
),
|
||
),
|
||
Expanded(
|
||
child: GestureDetector(
|
||
child: Row(
|
||
children: [
|
||
Image.asset(
|
||
_strategy.getGiftPageUsersOnMicrophoneIcon(),
|
||
height: 18.w,
|
||
width: 18.w,
|
||
),
|
||
SizedBox(width: 8.w),
|
||
Expanded(
|
||
child: text(
|
||
SCAppLocalizations.of(context)!.usersOnMicrophone,
|
||
fontSize: 14.sp,
|
||
textColor: Colors.white54,
|
||
),
|
||
),
|
||
SizedBox(width: 8.w),
|
||
Image.asset(
|
||
giveType == 1
|
||
? _strategy.getCommonSelectIcon()
|
||
: _strategy.getCommonUnselectIcon(),
|
||
width: 15.w,
|
||
height: 15.w,
|
||
),
|
||
],
|
||
),
|
||
onTap: () {
|
||
giveType = 1;
|
||
SmartDialog.dismiss(tag: "showGiveType");
|
||
setState(() {});
|
||
},
|
||
),
|
||
),
|
||
Expanded(
|
||
child: GestureDetector(
|
||
onTap: () {
|
||
giveType = 2;
|
||
SmartDialog.dismiss(tag: "showGiveType");
|
||
setState(() {});
|
||
},
|
||
behavior: HitTestBehavior.opaque,
|
||
child: Row(
|
||
children: [
|
||
Image.asset(
|
||
_strategy.getGiftPageAllInTheRoomIcon(),
|
||
height: 18.w,
|
||
width: 18.w,
|
||
),
|
||
SizedBox(width: 8.w),
|
||
Expanded(
|
||
child: text(
|
||
SCAppLocalizations.of(context)!.allInTheRoom,
|
||
fontSize: 14.sp,
|
||
textColor: Colors.white54,
|
||
),
|
||
),
|
||
SizedBox(width: 8.w),
|
||
Image.asset(
|
||
giveType == 2
|
||
? _strategy.getCommonSelectIcon()
|
||
: _strategy.getCommonUnselectIcon(),
|
||
width: 15.w,
|
||
height: 15.w,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _maiHead(HeadSelect shead) {
|
||
// 是否选中
|
||
return GestureDetector(
|
||
onTap: () {
|
||
if (_isCpRecipientSingleMode) {
|
||
final shouldSelect = !shead.isSelect;
|
||
for (final mai in listMai) {
|
||
mai.isSelect = false;
|
||
}
|
||
shead.isSelect = shouldSelect;
|
||
isAll = false;
|
||
giveType = 1;
|
||
setState(() {});
|
||
return;
|
||
}
|
||
isAll = true;
|
||
for (var mai in listMai) {
|
||
if (shead.mic?.user?.id == mai.mic?.user?.id) {
|
||
mai.isSelect = !mai.isSelect;
|
||
}
|
||
if (!mai.isSelect) {
|
||
isAll = false;
|
||
}
|
||
}
|
||
|
||
setState(() {});
|
||
},
|
||
child: Stack(
|
||
alignment: Alignment.bottomCenter,
|
||
clipBehavior: Clip.none,
|
||
children: <Widget>[
|
||
Stack(
|
||
alignment: AlignmentDirectional.center,
|
||
children: [
|
||
Container(
|
||
padding: EdgeInsets.all(2.w),
|
||
child: netImage(
|
||
url: shead.mic?.user?.userAvatar ?? "",
|
||
width: 26.w,
|
||
defaultImg: _strategy.getMePageDefaultAvatarImage(),
|
||
shape: BoxShape.circle,
|
||
),
|
||
),
|
||
PositionedDirectional(
|
||
bottom: 0,
|
||
end: 0,
|
||
child: Container(
|
||
alignment: AlignmentDirectional.center,
|
||
width: 12.w,
|
||
height: 12.w,
|
||
decoration: BoxDecoration(
|
||
color: Colors.black38,
|
||
shape: BoxShape.circle,
|
||
border:
|
||
shead.isSelect
|
||
? Border.all(
|
||
color: SocialChatTheme.primaryLight,
|
||
width: 1.w,
|
||
)
|
||
: null,
|
||
),
|
||
child: text(
|
||
"${shead.mic?.micIndex}",
|
||
textColor: Colors.white,
|
||
fontSize: 8.sp,
|
||
),
|
||
),
|
||
),
|
||
shead.isSelect
|
||
? PositionedDirectional(
|
||
bottom: 0,
|
||
end: 0,
|
||
child: Image.asset(
|
||
"sc_images/login/sc_icon_login_ser_select.png",
|
||
width: 12.w,
|
||
height: 12.w,
|
||
),
|
||
)
|
||
: Container(),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
///数量选项
|
||
void _showNumber(BuildContext ct) {
|
||
SmartDialog.showAttach(
|
||
tag: "showNumber",
|
||
targetContext: ct,
|
||
maskColor: Colors.transparent,
|
||
alignment: Alignment.topLeft,
|
||
animationType: SmartAnimationType.fade,
|
||
scalePointBuilder: (selfSize) => Offset(selfSize.width, 10),
|
||
onDismiss: () {
|
||
isNumberUp = true;
|
||
setState(() {});
|
||
},
|
||
builder: (_) {
|
||
return Transform.translate(
|
||
offset: Offset(SCGlobalConfig.lang == "ar" ? 20 : -20, -5),
|
||
child: CheckNumber(
|
||
onNumberChanged: (number) {
|
||
this.number = number;
|
||
isNumberUp = true;
|
||
setState(() {});
|
||
},
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
///选中所有在座位上的用户
|
||
void selecteAllMicUsers() {
|
||
if (_isCpRecipientSingleMode) {
|
||
return;
|
||
}
|
||
for (var mai in listMai) {
|
||
mai.isSelect = true;
|
||
}
|
||
}
|
||
|
||
_GiftSendRequest? _buildGiftSendRequest() {
|
||
final selectedGift = checkedGift;
|
||
if (selectedGift == null) {
|
||
_giftFxLog(
|
||
'tap send aborted reason=no_selected_gift '
|
||
'giveType=$giveType',
|
||
);
|
||
return null;
|
||
}
|
||
final isCpGiftRequest = _isCpGift(selectedGift);
|
||
List<String> acceptUserIds = [];
|
||
List<MicRes> acceptUsers = [];
|
||
|
||
if (widget.toUser != null) {
|
||
acceptUserIds.add(widget.toUser?.id ?? "");
|
||
acceptUsers.add(MicRes(user: widget.toUser));
|
||
} else if (isCpGiftRequest) {
|
||
isAll = false;
|
||
giveType = 1;
|
||
for (final mu in listMai) {
|
||
if (mu.isSelect) {
|
||
acceptUsers.add(mu.mic!);
|
||
acceptUserIds.add(mu.mic!.user!.id ?? "");
|
||
break;
|
||
}
|
||
}
|
||
} else {
|
||
///所有在线
|
||
if (giveType == 2) {
|
||
for (var value
|
||
in Provider.of<RtcProvider>(context, listen: false).onlineUsers) {
|
||
acceptUsers.add(MicRes(user: value));
|
||
acceptUserIds.add(value.id ?? "");
|
||
}
|
||
} else {
|
||
for (var mu in listMai) {
|
||
if (mu.isSelect) {
|
||
acceptUsers.add(mu.mic!);
|
||
acceptUserIds.add(mu.mic!.user!.id ?? "");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (acceptUserIds.isEmpty) {
|
||
_giftFxLog(
|
||
'tap send aborted reason=no_accept_users '
|
||
'giftId=${checkedGift?.id} '
|
||
'giftName=${checkedGift?.giftName} '
|
||
'giftTab=${checkedGift?.giftTab} '
|
||
'giveType=$giveType',
|
||
);
|
||
SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectTheRecipient);
|
||
return null;
|
||
}
|
||
final selectedNumber = number;
|
||
final roomId = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||
final roomAccount =
|
||
rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
|
||
final isBackpackGiftRequest = _usesBackpackGiftEndpoint(selectedGift);
|
||
final isLuckyGiftRequest =
|
||
!isBackpackGiftRequest && _usesLuckyGiftEndpoint(selectedGift);
|
||
|
||
if (isBackpackGiftRequest) {
|
||
final backpackRecordId = (selectedGift.backpackId ?? "").trim();
|
||
final giftId = (selectedGift.id ?? "").trim();
|
||
if (backpackRecordId.isEmpty || giftId.isEmpty) {
|
||
const configError =
|
||
'Gift configuration unavailable, please try another gift.';
|
||
_giftFxLog(
|
||
'giveBackpackGift skipped giftId=${selectedGift.id} '
|
||
'giftName=${selectedGift.giftName} '
|
||
'backpackId=${selectedGift.backpackId} '
|
||
'reason=invalid_backpack_config',
|
||
);
|
||
SCTts.show(configError);
|
||
return null;
|
||
}
|
||
final availableQuantity =
|
||
selectedGift.backpackQuantity?.toInt() ??
|
||
int.tryParse(selectedGift.quantity ?? '') ??
|
||
0;
|
||
final requiredQuantity = selectedNumber * acceptUserIds.length;
|
||
if (requiredQuantity > availableQuantity) {
|
||
_giftFxLog(
|
||
'giveBackpackGift skipped giftId=${selectedGift.id} '
|
||
'giftName=${selectedGift.giftName} '
|
||
'backpackId=${selectedGift.backpackId} '
|
||
'selectedNumber=$selectedNumber '
|
||
'acceptCount=${acceptUserIds.length} '
|
||
'requiredQuantity=$requiredQuantity '
|
||
'availableQuantity=$availableQuantity '
|
||
'reason=insufficient_backpack_quantity',
|
||
);
|
||
SCTts.show(_backpackQuantityInsufficientMessage());
|
||
unawaited(
|
||
Provider.of<SCAppGeneralManager>(
|
||
context,
|
||
listen: false,
|
||
).giftBackpack(forceRefresh: true),
|
||
);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
if (isLuckyGiftRequest && !_hasValidLuckyGiftStandardId(selectedGift)) {
|
||
const configError =
|
||
'Gift configuration unavailable, please try another gift.';
|
||
final requestName = isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift';
|
||
_giftFxLog(
|
||
'$requestName skipped giftId=${selectedGift.id} '
|
||
'giftName=${selectedGift.giftName} '
|
||
'giftTab=${selectedGift.giftTab} '
|
||
'standardId=${selectedGift.standardId} '
|
||
'reason=invalid_standard_id',
|
||
);
|
||
SCTts.show(configError);
|
||
return null;
|
||
}
|
||
|
||
return _GiftSendRequest(
|
||
acceptUserIds: acceptUserIds,
|
||
acceptUsers: acceptUsers,
|
||
gift: selectedGift,
|
||
quantity: selectedNumber,
|
||
clickCount: 1,
|
||
roomId: roomId,
|
||
roomAccount: roomAccount,
|
||
isLuckyGiftRequest: isLuckyGiftRequest,
|
||
isBackpackGiftRequest: isBackpackGiftRequest,
|
||
);
|
||
}
|
||
|
||
///赠送礼物
|
||
void giveGifts() async {
|
||
final request = _buildGiftSendRequest();
|
||
if (request == null) {
|
||
return;
|
||
}
|
||
|
||
if (request.isBackpackGiftRequest) {
|
||
RoomGiftComboSendController().hide();
|
||
} else {
|
||
RoomGiftComboSendController().show(
|
||
request: RoomGiftComboSendRequest(
|
||
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,
|
||
),
|
||
executor: _performFloatingComboSend,
|
||
);
|
||
}
|
||
|
||
if (_supportsComboRequestBatching(request.gift)) {
|
||
_enqueueComboGiftSendRequest(request);
|
||
} else {
|
||
unawaited(_performGiftSend(request, trigger: 'direct'));
|
||
}
|
||
|
||
SmartDialog.dismiss(tag: "showGiftControl");
|
||
}
|
||
|
||
bool _supportsComboRequestBatching(SocialChatGiftRes gift) {
|
||
return !_usesBackpackGiftEndpoint(gift) && _supportsComboFeedback(gift);
|
||
}
|
||
|
||
void _enqueueComboGiftSendRequest(_GiftSendRequest request) {
|
||
final now = DateTime.now();
|
||
_PendingGiftSendBatch? existingBatch;
|
||
for (final batch in _comboSendBatchQueue) {
|
||
if (batch.batchKey == request.batchKey) {
|
||
existingBatch = batch;
|
||
break;
|
||
}
|
||
}
|
||
|
||
if (existingBatch != null) {
|
||
existingBatch.quantity += request.quantity;
|
||
existingBatch.clickCount += request.clickCount;
|
||
existingBatch.readyAt = now.add(_comboSendBatchWindow);
|
||
_giftFxLog(
|
||
'aggregate combo send update '
|
||
'batchKey=${existingBatch.batchKey} '
|
||
'giftId=${request.gift.id} '
|
||
'quantity=${existingBatch.quantity} '
|
||
'clickCount=${existingBatch.clickCount} '
|
||
'acceptUserIds=${request.acceptUserIds.join(",")}',
|
||
);
|
||
} else {
|
||
final batch = _PendingGiftSendBatch.fromRequest(
|
||
request,
|
||
readyAt: now.add(_comboSendBatchWindow),
|
||
);
|
||
_comboSendBatchQueue.add(batch);
|
||
_giftFxLog(
|
||
'aggregate combo send enqueue '
|
||
'batchKey=${batch.batchKey} '
|
||
'giftId=${request.gift.id} '
|
||
'quantity=${batch.quantity} '
|
||
'clickCount=${batch.clickCount} '
|
||
'acceptUserIds=${request.acceptUserIds.join(",")}',
|
||
);
|
||
}
|
||
|
||
_scheduleNextComboGiftSendFlush();
|
||
}
|
||
|
||
void _scheduleNextComboGiftSendFlush() {
|
||
_comboSendBatchTimer?.cancel();
|
||
_comboSendBatchTimer = null;
|
||
if (_isComboSendBatchInFlight || _comboSendBatchQueue.isEmpty) {
|
||
return;
|
||
}
|
||
|
||
final headBatch = _comboSendBatchQueue.first;
|
||
final delay = headBatch.readyAt.difference(DateTime.now());
|
||
if (delay <= Duration.zero) {
|
||
unawaited(_flushNextComboGiftSendBatch());
|
||
return;
|
||
}
|
||
|
||
_comboSendBatchTimer = Timer(delay, () {
|
||
unawaited(_flushNextComboGiftSendBatch());
|
||
});
|
||
}
|
||
|
||
Future<void> _flushNextComboGiftSendBatch() async {
|
||
_comboSendBatchTimer?.cancel();
|
||
_comboSendBatchTimer = null;
|
||
if (_isComboSendBatchInFlight || _comboSendBatchQueue.isEmpty) {
|
||
return;
|
||
}
|
||
|
||
final headBatch = _comboSendBatchQueue.first;
|
||
if (headBatch.readyAt.isAfter(DateTime.now())) {
|
||
_scheduleNextComboGiftSendFlush();
|
||
return;
|
||
}
|
||
|
||
_comboSendBatchQueue.removeFirst();
|
||
_isComboSendBatchInFlight = true;
|
||
try {
|
||
await _performGiftSend(headBatch.toRequest(), trigger: 'batched');
|
||
} finally {
|
||
_isComboSendBatchInFlight = false;
|
||
_scheduleNextComboGiftSendFlush();
|
||
}
|
||
}
|
||
|
||
Future<void> _flushAllPendingComboGiftSends() async {
|
||
_comboSendBatchTimer?.cancel();
|
||
_comboSendBatchTimer = null;
|
||
while (_comboSendBatchQueue.isNotEmpty) {
|
||
if (_isComboSendBatchInFlight) {
|
||
await Future<void>.delayed(const Duration(milliseconds: 50));
|
||
continue;
|
||
}
|
||
_comboSendBatchQueue.first.readyAt = DateTime.now();
|
||
await _flushNextComboGiftSendBatch();
|
||
}
|
||
}
|
||
|
||
Future<void> _performGiftSend(
|
||
_GiftSendRequest request, {
|
||
required String trigger,
|
||
}) async {
|
||
final requestName = request.requestName;
|
||
final profileManager =
|
||
navigatorKey.currentState == null
|
||
? null
|
||
: Provider.of<SocialChatUserProfileManager>(
|
||
navigatorKey.currentState!.context,
|
||
listen: false,
|
||
);
|
||
final senderId = AccountStorage().getCurrentUser()?.userProfile?.id ?? "";
|
||
final senderName =
|
||
AccountStorage().getCurrentUser()?.userProfile?.userNickname ?? "";
|
||
final stopwatch = Stopwatch()..start();
|
||
|
||
_giftFxLog(
|
||
'send start trigger=$trigger request=$requestName '
|
||
'senderId=$senderId '
|
||
'senderName=$senderName '
|
||
'giftId=${request.gift.id} '
|
||
'giftName=${request.gift.giftName} '
|
||
'giftTab=${request.gift.giftTab} '
|
||
'special=${request.gift.special} '
|
||
'standardId=${request.gift.standardId} '
|
||
'giftCandy=${request.gift.giftCandy} '
|
||
'number=${request.quantity} '
|
||
'clickCount=${request.clickCount} '
|
||
'acceptCount=${request.acceptUserIds.length} '
|
||
'acceptUserIds=${request.acceptUserIds.join(",")} '
|
||
'roomId=${request.roomId} '
|
||
'roomAccount=${request.roomAccount} '
|
||
'giveType=$giveType '
|
||
'giftType=$giftType',
|
||
);
|
||
|
||
try {
|
||
final repository = SCChatRoomRepository();
|
||
_giftFxLog(
|
||
'calling repository.$requestName '
|
||
'trigger=$trigger '
|
||
'giftId=${request.gift.id} '
|
||
'roomId=${request.roomId} '
|
||
'acceptUserIds=${request.acceptUserIds.join(",")} '
|
||
'quantity=${request.quantity} '
|
||
'clickCount=${request.clickCount}',
|
||
);
|
||
if (request.isBackpackGiftRequest) {
|
||
final sentUsers = await _giveBackpackGiftToRecipients(
|
||
repository,
|
||
request,
|
||
);
|
||
_giftFxLog(
|
||
'$requestName success trigger=$trigger '
|
||
'giftId=${request.gift.id} '
|
||
'giftName=${request.gift.giftName} '
|
||
'giftSourceUrl=${request.gift.giftSourceUrl} '
|
||
'special=${request.gift.special} '
|
||
'giftTab=${request.gift.giftTab} '
|
||
'backpackId=${request.gift.backpackId} '
|
||
'backpackQuantity=${request.gift.backpackQuantity} '
|
||
'number=${request.quantity} '
|
||
'clickCount=${request.clickCount} '
|
||
'acceptUserIds=${request.acceptUserIds.join(",")} '
|
||
'roomId=${request.roomId} '
|
||
'elapsedMs=${stopwatch.elapsedMilliseconds}',
|
||
);
|
||
_showLocalCpInviteWaitingAfterGift(request, sentUsers);
|
||
if (sentUsers.isNotEmpty) {
|
||
final giftBatchId = _createGiftBatchId(request);
|
||
sendGiftMsg(
|
||
sentUsers,
|
||
gift: request.gift,
|
||
quantity: request.quantity,
|
||
giftBatchId: giftBatchId,
|
||
animationCount: request.clickCount,
|
||
);
|
||
}
|
||
await _refreshGiftBackpack();
|
||
_refreshCpStateAfterGift(request, profileManager);
|
||
return;
|
||
}
|
||
|
||
final result =
|
||
request.isLuckyGiftRequest
|
||
? await repository.giveLuckyGift(
|
||
request.acceptUserIds,
|
||
request.gift.id ?? "",
|
||
request.quantity,
|
||
false,
|
||
roomId: request.roomId,
|
||
relationType: request.cpRelationType,
|
||
)
|
||
: await repository.giveGift(
|
||
request.acceptUserIds,
|
||
request.gift.id ?? "",
|
||
request.quantity,
|
||
false,
|
||
roomId: request.roomId,
|
||
relationType: request.cpRelationType,
|
||
);
|
||
_giftFxLog(
|
||
'$requestName success trigger=$trigger '
|
||
'giftId=${request.gift.id} '
|
||
'giftName=${request.gift.giftName} '
|
||
'giftSourceUrl=${request.gift.giftSourceUrl} '
|
||
'special=${request.gift.special} '
|
||
'giftTab=${request.gift.giftTab} '
|
||
'standardId=${request.gift.standardId} '
|
||
'number=${request.quantity} '
|
||
'clickCount=${request.clickCount} '
|
||
'acceptUserIds=${request.acceptUserIds.join(",")} '
|
||
'roomId=${request.roomId} '
|
||
'balance=$result '
|
||
'elapsedMs=${stopwatch.elapsedMilliseconds}',
|
||
);
|
||
_showLocalCpInviteWaitingAfterGift(request, request.acceptUsers);
|
||
if (request.isLuckyGiftRequest) {
|
||
final giftBatchId = _createGiftBatchId(request);
|
||
await sendLuckGiftAnimOtherMsg(
|
||
request.acceptUsers,
|
||
gift: request.gift,
|
||
quantity: request.quantity,
|
||
giftBatchId: giftBatchId,
|
||
animationCount: request.clickCount,
|
||
);
|
||
} else {
|
||
final giftBatchId = _createGiftBatchId(request);
|
||
sendGiftMsg(
|
||
request.acceptUsers,
|
||
gift: request.gift,
|
||
quantity: request.quantity,
|
||
giftBatchId: giftBatchId,
|
||
animationCount: request.clickCount,
|
||
);
|
||
}
|
||
profileManager?.updateBalance(result);
|
||
_refreshCpStateAfterGift(request, profileManager);
|
||
} catch (e) {
|
||
final errorMessage =
|
||
request.isBackpackGiftRequest && _isBackpackQuantityError(e)
|
||
? _backpackQuantityInsufficientMessage()
|
||
: _resolveGiftSendErrorMessage(e);
|
||
final errorDetails = _describeGiftSendError(e);
|
||
_giftFxLog(
|
||
'$requestName failed trigger=$trigger '
|
||
'giftId=${request.gift.id} '
|
||
'giftName=${request.gift.giftName} '
|
||
'giftTab=${request.gift.giftTab} '
|
||
'standardId=${request.gift.standardId} '
|
||
'error=$e '
|
||
'resolvedError=$errorMessage '
|
||
'details={$errorDetails} '
|
||
'elapsedMs=${stopwatch.elapsedMilliseconds}',
|
||
);
|
||
if (request.isBackpackGiftRequest) {
|
||
unawaited(_refreshGiftBackpack());
|
||
}
|
||
if (!request.isBackpackGiftRequest &&
|
||
_isInsufficientMoneyError(e, errorMessage)) {
|
||
RoomGiftComboSendController().hide();
|
||
RoomRechargeBottomSheet.show(
|
||
navigatorKey.currentState?.context ?? context,
|
||
);
|
||
return;
|
||
}
|
||
SCTts.show(errorMessage);
|
||
}
|
||
}
|
||
|
||
void _refreshCpStateAfterGift(
|
||
_GiftSendRequest request,
|
||
SocialChatUserProfileManager? profileManager,
|
||
) {
|
||
if (!_isCpGift(request.gift)) {
|
||
return;
|
||
}
|
||
unawaited(profileManager?.refreshLoadedCpProfiles());
|
||
}
|
||
|
||
void _showLocalCpInviteWaitingAfterGift(
|
||
_GiftSendRequest request,
|
||
List<MicRes> sentUsers,
|
||
) {
|
||
if (!_isCpGift(request.gift)) {
|
||
return;
|
||
}
|
||
final relationType = request.cpRelationType;
|
||
if (relationType == null || relationType.trim().isEmpty) {
|
||
_giftFxLog(
|
||
'local cp waiting skipped reason=no_relation_type '
|
||
'giftId=${request.gift.id} acceptUserIds=${request.acceptUserIds}',
|
||
);
|
||
return;
|
||
}
|
||
if (sentUsers.isEmpty) {
|
||
_giftFxLog(
|
||
'local cp waiting skipped reason=no_sent_users '
|
||
'giftId=${request.gift.id} relationType=$relationType',
|
||
);
|
||
return;
|
||
}
|
||
final receiverProfile = sentUsers.first.user;
|
||
if (receiverProfile == null) {
|
||
_giftFxLog(
|
||
'local cp waiting skipped reason=no_receiver_profile '
|
||
'giftId=${request.gift.id} relationType=$relationType',
|
||
);
|
||
return;
|
||
}
|
||
final context = navigatorKey.currentState?.context;
|
||
if (context == null) {
|
||
_giftFxLog(
|
||
'local cp waiting skipped reason=no_context '
|
||
'giftId=${request.gift.id} relationType=$relationType',
|
||
);
|
||
return;
|
||
}
|
||
_giftFxLog(
|
||
'local cp waiting trigger '
|
||
'giftId=${request.gift.id} '
|
||
'relationType=$relationType '
|
||
'receiverId=${receiverProfile.id}',
|
||
);
|
||
unawaited(
|
||
Provider.of<RtmProvider>(
|
||
context,
|
||
listen: false,
|
||
).showLocalCpGiftInviteWaiting(
|
||
receiverProfile: receiverProfile,
|
||
relationType: relationType,
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<List<MicRes>> _giveBackpackGiftToRecipients(
|
||
SCChatRoomRepository repository,
|
||
_GiftSendRequest request,
|
||
) async {
|
||
final sentUsers = <MicRes>[];
|
||
for (var i = 0; i < request.acceptUserIds.length; i++) {
|
||
final acceptUserId = request.acceptUserIds[i].trim();
|
||
if (acceptUserId.isEmpty) {
|
||
continue;
|
||
}
|
||
final acceptUser =
|
||
i < request.acceptUsers.length ? request.acceptUsers[i] : null;
|
||
try {
|
||
await repository.giveBackpackGift(
|
||
id: request.gift.backpackId ?? "",
|
||
acceptUserId: acceptUserId,
|
||
giftId: request.gift.id ?? "",
|
||
quantity: request.quantity,
|
||
roomId: request.roomId,
|
||
seatIndex:
|
||
acceptUser?.micIndex ?? _resolveSeatIndexForUserId(acceptUserId),
|
||
relationType: request.cpRelationType,
|
||
);
|
||
if (acceptUser != null) {
|
||
sentUsers.add(acceptUser);
|
||
}
|
||
} catch (_) {
|
||
if (sentUsers.isNotEmpty) {
|
||
final giftBatchId = _createGiftBatchId(request);
|
||
sendGiftMsg(
|
||
sentUsers,
|
||
gift: request.gift,
|
||
quantity: request.quantity,
|
||
giftBatchId: giftBatchId,
|
||
animationCount: request.clickCount,
|
||
);
|
||
}
|
||
rethrow;
|
||
}
|
||
}
|
||
return sentUsers;
|
||
}
|
||
|
||
num? _resolveSeatIndexForUserId(String userId) {
|
||
final normalizedUserId = userId.trim();
|
||
if (normalizedUserId.isEmpty) {
|
||
return null;
|
||
}
|
||
for (final mic in rtcProvider?.roomWheatMap.values ?? const <MicRes>[]) {
|
||
if ((mic.user?.id ?? "").trim() == normalizedUserId) {
|
||
return mic.micIndex;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
Future<void> _refreshGiftBackpack() async {
|
||
final context = navigatorKey.currentState?.context;
|
||
if (context == null) {
|
||
return;
|
||
}
|
||
await Provider.of<SCAppGeneralManager>(
|
||
context,
|
||
listen: false,
|
||
).giftBackpack(forceRefresh: true);
|
||
}
|
||
|
||
void sendGiftMsg(
|
||
List<MicRes> acceptUsers, {
|
||
required SocialChatGiftRes gift,
|
||
required int quantity,
|
||
required String giftBatchId,
|
||
int animationCount = 1,
|
||
}) {
|
||
///发送一条IM消息
|
||
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
|
||
for (var u in acceptUsers) {
|
||
final special = gift.special ?? "";
|
||
final giftSourceUrl = gift.giftSourceUrl ?? "";
|
||
final hasSource = giftSourceUrl.isNotEmpty;
|
||
final hasAnimation = scGiftHasAnimationSpecial(special);
|
||
final hasGlobalGift = special.contains(SCGiftType.GLOBAL_GIFT.name);
|
||
final hasFullScreenEffect = scGiftHasFullScreenEffect(special);
|
||
_giftFxLog(
|
||
'dispatch gift msg '
|
||
'giftId=${gift.id} '
|
||
'giftName=${gift.giftName} '
|
||
'toUserId=${u.user?.id} '
|
||
'toUserName=${u.user?.userNickname} '
|
||
'giftSourceUrl=$giftSourceUrl '
|
||
'special=$special '
|
||
'hasSource=$hasSource '
|
||
'hasAnimation=$hasAnimation '
|
||
'hasGlobalGift=$hasGlobalGift '
|
||
'hasFullScreenEffect=$hasFullScreenEffect '
|
||
'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}',
|
||
);
|
||
if (gift.giftSourceUrl != null && gift.special != null) {
|
||
if (scGiftHasFullScreenEffect(gift.special)) {
|
||
if (SCGlobalConfig.allowsHighCostAnimations &&
|
||
SCGlobalConfig.isGiftSpecialEffects &&
|
||
(rtcProvider?.shouldShowRoomVisualEffects ?? false)) {
|
||
_giftFxLog(
|
||
'local trigger player play '
|
||
'path=${gift.giftSourceUrl} '
|
||
'giftId=${gift.id} '
|
||
'giftName=${gift.giftName} '
|
||
'animationCount=$animationCount',
|
||
);
|
||
SCGiftVapSvgaManager().play(gift.giftSourceUrl!);
|
||
} else {
|
||
_giftFxLog(
|
||
'skip local play because visual effects disabled '
|
||
'giftId=${gift.id} '
|
||
'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} '
|
||
'roomVisible=${rtcProvider?.shouldShowRoomVisualEffects ?? false}',
|
||
);
|
||
}
|
||
} else {
|
||
_giftFxLog(
|
||
'skip local play because special does not include '
|
||
'${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} '
|
||
'giftId=${gift.id} special=${gift.special}',
|
||
);
|
||
}
|
||
} else {
|
||
_giftFxLog(
|
||
'skip local play because giftSourceUrl or special is null '
|
||
'giftId=${gift.id} '
|
||
'giftSourceUrl=${gift.giftSourceUrl} '
|
||
'special=${gift.special}',
|
||
);
|
||
}
|
||
Provider.of<RtmProvider>(
|
||
navigatorKey.currentState!.context,
|
||
listen: false,
|
||
).dispatchMessage(
|
||
Msg(
|
||
groupId:
|
||
rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount,
|
||
gift: gift,
|
||
user: AccountStorage().getCurrentUser()?.userProfile,
|
||
toUser: u.user,
|
||
targetUserIds: targetUserIds,
|
||
giftBatchId: giftBatchId,
|
||
number: quantity,
|
||
customAnimationCount: animationCount,
|
||
type: SCRoomMsgType.gift,
|
||
role:
|
||
Provider.of<RtcProvider>(
|
||
navigatorKey.currentState!.context,
|
||
listen: false,
|
||
).currenRoom?.entrants?.roles ??
|
||
"",
|
||
msg: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
),
|
||
addLocal: true,
|
||
);
|
||
if (rtcProvider?.currenRoom?.roomProfile?.roomSetting?.showHeartbeat ??
|
||
false) {
|
||
debouncer.debounce(
|
||
duration: Duration(milliseconds: 350),
|
||
onDebounce: () {
|
||
rtcProvider?.requestGiftTriggeredMicRefresh();
|
||
},
|
||
);
|
||
}
|
||
num coins = (gift.giftCandy ?? 0) * quantity;
|
||
if (coins > 9999 && !_isCpGift(gift)) {
|
||
var fMsg = SCFloatingMessage(
|
||
type: 1,
|
||
userId: AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
|
||
toUserId: u.user?.id ?? "",
|
||
userAvatarUrl:
|
||
AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? "",
|
||
userName:
|
||
AccountStorage().getCurrentUser()?.userProfile?.userNickname ??
|
||
"",
|
||
toUserName: u.user?.userNickname ?? "",
|
||
toUserAvatarUrl: u.user?.userAvatar ?? "",
|
||
giftUrl: gift.giftPhoto ?? "",
|
||
giftId: gift.id,
|
||
giftTab: _floatingGiftTabFor(gift),
|
||
roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||
coins: coins,
|
||
number: quantity,
|
||
);
|
||
_enqueueGiftFloatingMessage(
|
||
fMsg,
|
||
dedupKey: _giftFloatingDedupKey(
|
||
roomId: fMsg.roomId,
|
||
senderId: fMsg.userId,
|
||
receiverId: fMsg.toUserId,
|
||
giftId: gift.id,
|
||
quantity: quantity,
|
||
),
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
void _enqueueGiftFloatingMessage(
|
||
SCFloatingMessage message, {
|
||
String? dedupKey,
|
||
}) {
|
||
OverlayManager().addMessage(message);
|
||
}
|
||
|
||
String _giftFloatingDedupKey({
|
||
required String? roomId,
|
||
required String? senderId,
|
||
required String? receiverId,
|
||
required String? giftId,
|
||
required int quantity,
|
||
}) {
|
||
return [
|
||
"gift",
|
||
roomId ?? "",
|
||
senderId ?? "",
|
||
receiverId ?? "",
|
||
giftId ?? "",
|
||
quantity,
|
||
].join("|");
|
||
}
|
||
|
||
String _floatingGiftTabFor(SocialChatGiftRes gift) {
|
||
if (_isCpGift(gift)) {
|
||
return scCpRelationTypeForGift(gift);
|
||
}
|
||
return gift.giftTab ?? "";
|
||
}
|
||
|
||
bool _supportsComboFeedback(SocialChatGiftRes gift) {
|
||
final giftTab = (gift.giftTab ?? '').trim();
|
||
return giftTab == "LUCK" ||
|
||
giftTab == SCGiftType.LUCKY_GIFT.name ||
|
||
giftTab == SCGiftType.CP.name ||
|
||
giftTab == SCGiftType.MAGIC.name;
|
||
}
|
||
|
||
Widget _buildCpGiftCloseFriendEntry() {
|
||
return Semantics(
|
||
button: true,
|
||
label: "Gifts for Close Friends",
|
||
child: GestureDetector(
|
||
behavior: HitTestBehavior.opaque,
|
||
onTap: _openCpRightsGuide,
|
||
child: SizedBox(
|
||
width: 355.w,
|
||
height: 46.w,
|
||
child: Image.asset(
|
||
"$_cpProfileAssetBase/sc_cp_profile_gift_entry_card.png",
|
||
fit: BoxFit.fill,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildFirstRechargeGiftEntry() {
|
||
return Semantics(
|
||
button: true,
|
||
label: "First recharge offer",
|
||
child: GestureDetector(
|
||
behavior: HitTestBehavior.opaque,
|
||
onTap: _openFirstRechargeDialog,
|
||
child: SizedBox(
|
||
width: 355.w,
|
||
height: 46.w,
|
||
child: Image.asset(_firstRechargeGiftEntryCard, fit: BoxFit.fill),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _openFirstRechargeDialog() {
|
||
SmartDialog.dismiss(tag: "showGiftControl");
|
||
Future<void>.delayed(const Duration(milliseconds: 80), () {
|
||
final navigator = navigatorKey.currentState;
|
||
if (navigator == null || !navigator.mounted) {
|
||
return;
|
||
}
|
||
SCDialogUtils.showFirstRechargeDialog(navigator.context);
|
||
});
|
||
}
|
||
|
||
void _openRoomRechargeBottomSheet() {
|
||
SmartDialog.dismiss(tag: "showGiftControl");
|
||
Future<void>.delayed(const Duration(milliseconds: 80), () {
|
||
final navigator = navigatorKey.currentState;
|
||
if (navigator == null || !navigator.mounted) {
|
||
return;
|
||
}
|
||
RoomRechargeBottomSheet.show(navigator.context);
|
||
});
|
||
}
|
||
|
||
void _openCpRightsGuide() {
|
||
SmartDialog.dismiss(tag: "showGiftControl");
|
||
Future<void>.delayed(const Duration(milliseconds: 80), () {
|
||
final navigator = navigatorKey.currentState;
|
||
if (navigator == null) {
|
||
return;
|
||
}
|
||
navigator.push(
|
||
MaterialPageRoute<void>(builder: (_) => const CpRightsGuidePage()),
|
||
);
|
||
});
|
||
}
|
||
|
||
Widget _buildSendButton() {
|
||
return SCGiftComboSendButton(
|
||
label: SCAppLocalizations.of(context)!.send,
|
||
onPressed: giveGifts,
|
||
showCountdown: false,
|
||
width: 96.w,
|
||
);
|
||
}
|
||
|
||
Future<void> _performFloatingComboSend(
|
||
RoomGiftComboSendRequest request, {
|
||
required String trigger,
|
||
}) async {
|
||
await _performGiftSend(
|
||
_GiftSendRequest(
|
||
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,
|
||
isBackpackGiftRequest: _usesBackpackGiftEndpoint(request.gift),
|
||
),
|
||
trigger: trigger,
|
||
);
|
||
}
|
||
|
||
/// 将数字giftType转换为字符串类型,用于活动礼物头部背景
|
||
String _giftTypeToString(int giftType) {
|
||
switch (giftType) {
|
||
case 1: // ACTIVITY
|
||
return 'ACTIVITY';
|
||
case 2: // LUCKY_GIFT -> LUCK
|
||
return 'LUCK';
|
||
case 3: // CP
|
||
return 'CP';
|
||
case 5: // MAGIC
|
||
return 'MAGIC';
|
||
default:
|
||
return 'ACTIVITY';
|
||
}
|
||
}
|
||
|
||
_buildGiftHead() {
|
||
if (giftType == 2 || giftType == 3) {
|
||
return Container();
|
||
}
|
||
|
||
if (giftType == 1 || giftType == 5) {
|
||
// 获取基础路径
|
||
String basePath = _strategy.getGiftPageActivityGiftHeadBackground(
|
||
_giftTypeToString(giftType),
|
||
);
|
||
|
||
// 添加语言后缀
|
||
String imagePath;
|
||
if (SCGlobalConfig.lang == "ar") {
|
||
// 移除扩展名,添加 _ar 后缀,然后重新添加扩展名
|
||
if (basePath.endsWith('.png')) {
|
||
imagePath = '${basePath.substring(0, basePath.length - 4)}_ar.png';
|
||
} else {
|
||
imagePath = '${basePath}_ar';
|
||
}
|
||
} else {
|
||
if (basePath.endsWith('.png')) {
|
||
imagePath = '${basePath.substring(0, basePath.length - 4)}_en.png';
|
||
} else {
|
||
imagePath = '${basePath}_en';
|
||
}
|
||
}
|
||
|
||
// 确定高度
|
||
double height = giftType == 5 ? 80.w : 65.w;
|
||
|
||
return Container(
|
||
margin: EdgeInsets.symmetric(horizontal: 10.w),
|
||
child: Image.asset(imagePath, height: height, fit: BoxFit.fill),
|
||
);
|
||
}
|
||
return Container();
|
||
}
|
||
|
||
///发送一条消息,幸运礼物,房间里所有人都能看到礼物飘向麦位的动画
|
||
Future<void> sendLuckGiftAnimOtherMsg(
|
||
List<MicRes> acceptUsers, {
|
||
required SocialChatGiftRes gift,
|
||
required int quantity,
|
||
required String giftBatchId,
|
||
int animationCount = 1,
|
||
}) async {
|
||
final targetUserIds = _resolveAcceptUserIdentityKeys(acceptUsers);
|
||
final firstTargetUser =
|
||
acceptUsers.isNotEmpty ? acceptUsers.first.user : null;
|
||
_giftFxLog(
|
||
'dispatch lucky anim msg '
|
||
'giftId=${gift.id} '
|
||
'giftName=${gift.giftName} '
|
||
'giftPhoto=${gift.giftPhoto} '
|
||
'quantity=$quantity '
|
||
'animationCount=$animationCount '
|
||
'firstTargetUserId=${firstTargetUser?.id} '
|
||
'groupId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount} '
|
||
'roomId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id} '
|
||
'targetUserIds=${targetUserIds.join(",")}',
|
||
);
|
||
await Provider.of<RtmProvider>(
|
||
navigatorKey.currentState!.context,
|
||
listen: false,
|
||
).dispatchMessage(
|
||
Msg(
|
||
groupId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount,
|
||
gift: gift,
|
||
user: AccountStorage().getCurrentUser()?.userProfile,
|
||
toUser: firstTargetUser,
|
||
targetUserIds: targetUserIds,
|
||
giftBatchId: giftBatchId,
|
||
number: quantity,
|
||
customAnimationCount: animationCount,
|
||
type: SCRoomMsgType.luckGiftAnimOther,
|
||
msg: jsonEncode(targetUserIds),
|
||
),
|
||
addLocal: true,
|
||
);
|
||
_giftFxLog(
|
||
'dispatch lucky anim msg finished '
|
||
'giftId=${gift.id} '
|
||
'targetUserIds=${targetUserIds.join(",")}',
|
||
);
|
||
}
|
||
|
||
List<String> _resolveAcceptUserIds(List<MicRes> acceptUsers) {
|
||
final targetUserIds = <String>[];
|
||
for (final acceptUser in acceptUsers) {
|
||
final userId = (acceptUser.user?.id ?? "").trim();
|
||
if (userId.isEmpty || targetUserIds.contains(userId)) {
|
||
continue;
|
||
}
|
||
targetUserIds.add(userId);
|
||
}
|
||
return targetUserIds;
|
||
}
|
||
|
||
List<String> _resolveAcceptUserIdentityKeys(List<MicRes> acceptUsers) {
|
||
final targetUserIds = <String>[];
|
||
void addTargetUserId(String? userId) {
|
||
final normalizedUserId = (userId ?? "").trim();
|
||
if (normalizedUserId.isEmpty ||
|
||
targetUserIds.contains(normalizedUserId)) {
|
||
return;
|
||
}
|
||
targetUserIds.add(normalizedUserId);
|
||
}
|
||
|
||
for (final acceptUser in acceptUsers) {
|
||
final user = acceptUser.user;
|
||
addTargetUserId(user?.id);
|
||
addTargetUserId(user?.account);
|
||
addTargetUserId(user?.getID());
|
||
}
|
||
return targetUserIds;
|
||
}
|
||
|
||
String _createGiftBatchId(_GiftSendRequest request) {
|
||
final sortedAcceptUserIds = List<String>.from(request.acceptUserIds)
|
||
..sort();
|
||
return [
|
||
request.isBackpackGiftRequest
|
||
? 'backpack'
|
||
: request.isLuckyGiftRequest
|
||
? 'lucky'
|
||
: 'gift',
|
||
request.roomId,
|
||
request.gift.id ?? '',
|
||
request.quantity,
|
||
request.clickCount,
|
||
sortedAcceptUserIds.join(','),
|
||
DateTime.now().microsecondsSinceEpoch,
|
||
].join('|');
|
||
}
|
||
}
|
||
|
||
class CheckNumber extends StatelessWidget {
|
||
final ValueChanged<int> onNumberChanged;
|
||
|
||
const CheckNumber({super.key, required this.onNumberChanged});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
alignment: AlignmentDirectional.topEnd,
|
||
margin: EdgeInsets.only(right: width(22)),
|
||
child: ClipRRect(
|
||
borderRadius: BorderRadius.all(Radius.circular(12.w)),
|
||
child: BackdropFilter(
|
||
filter: ui.ImageFilter.blur(sigmaX: 25, sigmaY: 25),
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white10,
|
||
borderRadius: BorderRadius.all(Radius.circular(height(6))),
|
||
),
|
||
width: width(75),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: <Widget>[
|
||
SizedBox(height: height(10)),
|
||
_item(1),
|
||
_item(7),
|
||
_item(17),
|
||
_item(77),
|
||
_item(777),
|
||
_item(7777),
|
||
SizedBox(height: height(10)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
_item(int number) {
|
||
return GestureDetector(
|
||
behavior: HitTestBehavior.opaque,
|
||
onTap: () {
|
||
SmartDialog.dismiss(tag: "showNumber");
|
||
onNumberChanged(number);
|
||
},
|
||
child: SizedBox(
|
||
height: height(24),
|
||
child: Row(
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: <Widget>[
|
||
SizedBox(width: width(10)),
|
||
Text(
|
||
"$number",
|
||
style: TextStyle(
|
||
fontSize: sp(14),
|
||
color: Colors.white,
|
||
fontWeight: FontWeight.w400,
|
||
decoration: TextDecoration.none,
|
||
),
|
||
),
|
||
SizedBox(width: width(10)),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _GiftSendRequest {
|
||
const _GiftSendRequest({
|
||
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.isBackpackGiftRequest,
|
||
});
|
||
|
||
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;
|
||
final bool isBackpackGiftRequest;
|
||
|
||
String get requestName {
|
||
if (isBackpackGiftRequest) {
|
||
return 'giveBackpackGift';
|
||
}
|
||
return isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift';
|
||
}
|
||
|
||
String? get cpRelationType {
|
||
if (!scIsCpGift(gift)) {
|
||
return null;
|
||
}
|
||
if (isSelfRecipientRequest) {
|
||
return null;
|
||
}
|
||
return scCpRelationTypeForGift(gift);
|
||
}
|
||
|
||
bool get isSelfRecipientRequest {
|
||
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||
final currentKeys = _profileIdentityKeys(currentProfile);
|
||
if (currentKeys.isEmpty) {
|
||
return false;
|
||
}
|
||
for (final userId in acceptUserIds) {
|
||
if (currentKeys.contains(userId.trim())) {
|
||
return true;
|
||
}
|
||
}
|
||
for (final acceptUser in acceptUsers) {
|
||
final acceptKeys = _profileIdentityKeys(acceptUser.user);
|
||
if (acceptKeys.any(currentKeys.contains)) {
|
||
return true;
|
||
}
|
||
}
|
||
return false;
|
||
}
|
||
|
||
Set<String> _profileIdentityKeys(SocialChatUserProfile? profile) {
|
||
return <String>{
|
||
profile?.id?.trim() ?? "",
|
||
profile?.account?.trim() ?? "",
|
||
profile?.getID().trim() ?? "",
|
||
}..remove("");
|
||
}
|
||
|
||
String get batchKey {
|
||
final sortedAcceptUserIds = List<String>.from(acceptUserIds)..sort();
|
||
final type =
|
||
isBackpackGiftRequest
|
||
? "backpack"
|
||
: isLuckyGiftRequest
|
||
? "lucky"
|
||
: "gift";
|
||
return [
|
||
type,
|
||
gift.id ?? "",
|
||
gift.backpackId ?? "",
|
||
roomId,
|
||
sortedAcceptUserIds.join(","),
|
||
].join("|");
|
||
}
|
||
}
|
||
|
||
class _PendingGiftSendBatch {
|
||
_PendingGiftSendBatch({
|
||
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.isBackpackGiftRequest,
|
||
required this.readyAt,
|
||
});
|
||
|
||
factory _PendingGiftSendBatch.fromRequest(
|
||
_GiftSendRequest request, {
|
||
required DateTime readyAt,
|
||
}) {
|
||
return _PendingGiftSendBatch(
|
||
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,
|
||
isBackpackGiftRequest: request.isBackpackGiftRequest,
|
||
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;
|
||
final bool isBackpackGiftRequest;
|
||
DateTime readyAt;
|
||
|
||
_GiftSendRequest toRequest() {
|
||
return _GiftSendRequest(
|
||
acceptUserIds: List<String>.from(acceptUserIds),
|
||
acceptUsers: List<MicRes>.from(acceptUsers),
|
||
gift: gift,
|
||
quantity: quantity,
|
||
clickCount: clickCount,
|
||
roomId: roomId,
|
||
roomAccount: roomAccount,
|
||
isLuckyGiftRequest: isLuckyGiftRequest,
|
||
isBackpackGiftRequest: isBackpackGiftRequest,
|
||
);
|
||
}
|
||
}
|
||
|
||
class HeadSelect {
|
||
bool isSelect = false;
|
||
MicRes? mic;
|
||
|
||
HeadSelect(this.isSelect, this.mic);
|
||
}
|