yumi-flutter/lib/modules/gift/gift_page.dart
2026-06-17 19:28:08 +08:00

2381 lines
76 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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);
}