chatapp3-flutter/lib/modules/gift/gift_page.dart
2026-04-16 19:23:54 +08:00

1525 lines
53 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: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/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/app/routes/sc_fluro_navigator.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/countdown_timer.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
import 'package:yumi/modules/wallet/wallet_route.dart';
import 'package:yumi/modules/gift/gift_tab_page.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 {
SocialChatUserProfile? toUser;
GiftPage({super.key, this.toUser});
@override
_GiftPageState createState() => _GiftPageState();
}
class _GiftPageState extends State<GiftPage>
with SingleTickerProviderStateMixin {
static const List<String> _preferredGiftTabOrder = <String>[
"ALL",
"ACTIVITY",
"LUCKY_GIFT",
"CP",
"MAGIC",
"CUSTOMIZED",
"NSCIONAL_FLAG",
];
TabController? _tabController;
List<String> _tabTypes = <String>[];
/// 业务逻辑策略访问器
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();
void _giftFxLog(String message) {
debugPrint('[GiftFX][Send] $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 _showLocalLuckyGiftFeedback(
List<MicRes> acceptUsers, {
required SocialChatGiftRes gift,
required int quantity,
}) {
final rtmProvider = Provider.of<RtmProvider>(
navigatorKey.currentState!.context,
listen: false,
);
final currentUser = AccountStorage().getCurrentUser()?.userProfile;
if (currentUser == null) {
_giftFxLog(
'local lucky feedback skipped reason=no_current_user giftId=${gift.id}',
);
return;
}
for (final acceptUser in acceptUsers) {
final targetUser = acceptUser.user;
if (targetUser == null) {
_giftFxLog(
'local lucky feedback skipped reason=no_target_user giftId=${gift.id}',
);
continue;
}
_giftFxLog(
'local lucky feedback trigger '
'giftId=${gift.id} '
'giftName=${gift.giftName} '
'toUserId=${targetUser.id} '
'toUserName=${targetUser.userNickname} '
'quantity=$quantity',
);
rtmProvider.msgFloatingGiftListener?.call(
Msg(
groupId:
rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount,
msg: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "",
type: SCRoomMsgType.luckGiftAnimOther,
gift: gift,
user: currentUser,
toUser: targetUser,
number: quantity,
),
);
}
}
void _applyGiftSelection(SocialChatGiftRes? gift, {bool notify = true}) {
checkedGift = gift;
if (gift != null &&
(gift.giftSourceUrl ?? "").isNotEmpty &&
scGiftHasFullScreenEffect(gift.special)) {
SCGiftVapSvgaManager().preload(gift.giftSourceUrl!, highPriority: true);
_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 (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();
Provider.of<SCAppGeneralManager>(context, listen: false).giftActivityList();
// Provider.of<GeneralProvider>(context, listen: false).giftBackpack();
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
rtcProvider?.roomWheatMap.forEach((k, v) {
if (v.user != null) {
if (v.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) {
listMai.add(HeadSelect(true, v));
} else {
listMai.add(HeadSelect(false, v));
}
}
isAll = true;
for (var mai in listMai) {
if (!mai.isSelect) {
isAll = false;
}
}
});
}
@override
void dispose() {
_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]);
}
List<_GiftPageTabItem> _buildGiftTabs(
BuildContext context,
SCAppGeneralManager ref,
) {
final localizations = SCAppLocalizations.of(context)!;
final availableTypes =
ref.giftByTab.entries
.where((entry) => entry.value.isNotEmpty)
.map((entry) => entry.key)
.toList();
final orderedTypes = <String>[];
for (final type in _preferredGiftTabOrder) {
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)) {
return;
}
final currentType =
(_tabController != null &&
_tabTypes.isNotEmpty &&
_tabController!.index < _tabTypes.length)
? _tabTypes[_tabController!.index]
: null;
final nextIndex = currentType != null ? nextTypes.indexOf(currentType) : 0;
_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 _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(gifts)) {
return;
}
final gift = _resolveSelectedGift(tabType, gifts);
_selectedGiftByTab[tabType] = gift;
_applyGiftSelection(gift, notify: false);
}
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 (gift.id == selectedGift!.id) {
return gift;
}
}
}
if (tabType == "CUSTOMIZED") {
return gifts.length > 1 ? gifts[1] : null;
}
return gifts.first;
}
bool _matchesCurrentGift(List<SocialChatGiftRes> gifts) {
final currentGiftId = checkedGift?.id;
if (currentGiftId == null) {
return gifts.isEmpty;
}
return gifts.any((gift) => gift.id == currentGiftId);
}
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;
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 _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<String, dynamic>) {
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.';
}
@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();
}
_ensureCurrentTabSelection(ref, giftTabs[tabController.index].type);
return SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
SCGlobalConfig.isReview
? Container()
: ((AccountStorage()
.getCurrentUser()
?.userProfile
?.firstRecharge ??
false)
? GestureDetector(
child: Stack(
alignment: AlignmentDirectional.bottomEnd,
children: [
Transform.flip(
flipX:
SCGlobalConfig.lang == "ar"
? true
: false, // 水平翻转
flipY: false, // 垂直翻转设为 false
child: Image.asset(
_strategy
.getGiftPageFirstRechargeRoomTagIcon(),
height: 75.w,
),
),
SCGlobalConfig.lang == "ar"
? PositionedDirectional(
end: 22.w,
bottom: 38.w,
child: Image.asset(
_strategy
.getGiftPageFirstRechargeTextIcon(
'ar',
),
height: 13.w,
),
)
: PositionedDirectional(
end: 16.w,
bottom: 38.w,
child: Image.asset(
_strategy
.getGiftPageFirstRechargeTextIcon(
'en',
),
height: 13.w,
),
),
PositionedDirectional(
end: 34.w,
bottom: 13.w,
child: CountdownTimer(
expiryDate:
DateTime.fromMillisecondsSinceEpoch(
AccountStorage()
.getCurrentUser()
?.userProfile
?.firstRechargeEndTime ??
0,
),
color: Colors.white,
fontSize: 12.w,
),
),
],
),
onTap: () {
SCDialogUtils.showFirstRechargeDialog(
navigatorKey.currentState!.context,
);
},
)
: Container()),
],
),
_buildGiftHead(),
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(
children: [
SizedBox(height: 12.w),
Row(
children: [
SizedBox(width: 8.w),
widget.toUser == null
? Builder(
builder: (ct) {
return GestureDetector(
child: Image.asset(
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,
),
onTap: () {
isAll = !isAll;
for (var mai in listMai) {
mai.isSelect = isAll;
}
setState(() {});
// showGiveTypeDialog(ct);
},
);
},
)
: 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),
],
),
Expanded(
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: () {
SmartDialog.dismiss(tag: "showGiftControl");
SCNavigatorUtils.push(
navigatorKey.currentState!.context,
WalletRoute.recharge,
replace: false,
);
},
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),
GestureDetector(
onTap: () {
giveGifts();
},
child: Container(
padding: EdgeInsets.symmetric(
vertical: 8.w,
horizontal: 20.w,
),
decoration: BoxDecoration(
color:
SocialChatTheme.primaryLight,
borderRadius:
BorderRadius.circular(5),
),
child: text(
SCAppLocalizations.of(
context,
)!.send,
fontSize: 14.sp,
),
),
),
],
),
),
);
},
),
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: () {
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() {
for (var mai in listMai) {
mai.isSelect = true;
}
}
///赠送礼物
void giveGifts() async {
List<String> acceptUserIds = [];
List<MicRes> acceptUsers = [];
if (widget.toUser != null) {
acceptUserIds.add(widget.toUser?.id ?? "");
acceptUsers.add(MicRes(user: widget.toUser));
} 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;
}
final selectedGift = checkedGift;
if (selectedGift == null) {
_giftFxLog(
'tap send aborted reason=no_selected_gift '
'acceptUserIds=${acceptUserIds.join(",")} '
'giveType=$giveType',
);
return;
}
final selectedNumber = number;
final roomId = rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "";
final roomAccount =
rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "";
final isLuckyGiftRequest = _usesLuckyGiftEndpoint(selectedGift);
final requestName = isLuckyGiftRequest ? 'giveLuckyGift' : 'giveGift';
final senderId = AccountStorage().getCurrentUser()?.userProfile?.id ?? "";
final senderName =
AccountStorage().getCurrentUser()?.userProfile?.userNickname ?? "";
final stopwatch = Stopwatch()..start();
_giftFxLog(
'tap send start request=$requestName '
'senderId=$senderId '
'senderName=$senderName '
'giftId=${selectedGift.id} '
'giftName=${selectedGift.giftName} '
'giftTab=${selectedGift.giftTab} '
'special=${selectedGift.special} '
'standardId=${selectedGift.standardId} '
'giftCandy=${selectedGift.giftCandy} '
'number=$selectedNumber '
'acceptCount=${acceptUserIds.length} '
'acceptUserIds=${acceptUserIds.join(",")} '
'roomId=$roomId '
'roomAccount=$roomAccount '
'giveType=$giveType '
'giftType=$giftType',
);
if (isLuckyGiftRequest && !_hasValidLuckyGiftStandardId(selectedGift)) {
const configError =
'Gift configuration unavailable, please try another gift.';
_giftFxLog(
'$requestName skipped giftId=${selectedGift.id} '
'giftName=${selectedGift.giftName} '
'giftTab=${selectedGift.giftTab} '
'standardId=${selectedGift.standardId} '
'reason=invalid_standard_id',
);
SCTts.show(configError);
return;
}
try {
final repository = SCChatRoomRepository();
_giftFxLog(
'calling repository.$requestName '
'giftId=${selectedGift.id} '
'roomId=$roomId '
'acceptUserIds=${acceptUserIds.join(",")} '
'quantity=$selectedNumber',
);
final result =
isLuckyGiftRequest
? await repository.giveLuckyGift(
acceptUserIds,
selectedGift.id ?? "",
selectedNumber,
false,
roomId: roomId,
)
: await repository.giveGift(
acceptUserIds,
selectedGift.id ?? "",
selectedNumber,
false,
roomId: roomId,
);
_giftFxLog(
'$requestName success giftId=${selectedGift.id} '
'giftName=${selectedGift.giftName} '
'giftSourceUrl=${selectedGift.giftSourceUrl} '
'special=${selectedGift.special} '
'giftTab=${selectedGift.giftTab} '
'standardId=${selectedGift.standardId} '
'number=$selectedNumber '
'acceptUserIds=${acceptUserIds.join(",")} '
'roomId=$roomId '
'balance=$result '
'elapsedMs=${stopwatch.elapsedMilliseconds}',
);
// SCTts.show(SCAppLocalizations.of(context)!.giftGivingSuccessful);
if (isLuckyGiftRequest) {
_showLocalLuckyGiftFeedback(
acceptUsers,
gift: selectedGift,
quantity: selectedNumber,
);
await sendLuckGiftAnimOtherMsg(
acceptUsers,
gift: selectedGift,
quantity: selectedNumber,
);
} else {
sendGiftMsg(acceptUsers, gift: selectedGift, quantity: selectedNumber);
}
if (mounted) {
Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
).updateBalance(result);
}
} catch (e) {
final errorMessage = _resolveGiftSendErrorMessage(e);
final errorDetails = _describeGiftSendError(e);
_giftFxLog(
'$requestName failed giftId=${selectedGift.id} '
'giftName=${selectedGift.giftName} '
'giftTab=${selectedGift.giftTab} '
'standardId=${selectedGift.standardId} '
'error=$e '
'resolvedError=$errorMessage '
'details={$errorDetails} '
'elapsedMs=${stopwatch.elapsedMilliseconds}',
);
if (mounted) {
SCTts.show(errorMessage);
}
}
}
void sendGiftMsg(
List<MicRes> acceptUsers, {
required SocialChatGiftRes gift,
required int quantity,
}) {
///发送一条IM消息
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}',
);
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,
number: quantity,
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: () {
Provider.of<RtcProvider>(
navigatorKey.currentState!.context,
listen: false,
).retrieveMicrophoneList();
},
);
}
num coins = (gift.giftCandy ?? 0) * quantity;
if (coins > 9999) {
var fMsg = SCFloatingMessage(
type: 1,
userAvatarUrl:
AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? "",
userName:
AccountStorage().getCurrentUser()?.userProfile?.userNickname ??
"",
toUserName: u.user?.userNickname ?? "",
giftUrl: gift.giftPhoto ?? "",
roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "",
coins: coins,
number: quantity,
);
OverlayManager().addMessage(fMsg);
}
if (gift.giftSourceUrl != null && gift.special != null) {
if (scGiftHasFullScreenEffect(gift.special)) {
if (SCGlobalConfig.isGiftSpecialEffects) {
_giftFxLog(
'local trigger player play '
'path=${gift.giftSourceUrl} '
'giftId=${gift.id} '
'giftName=${gift.giftName}',
);
SCGiftVapSvgaManager().play(gift.giftSourceUrl!);
} else {
_giftFxLog(
'skip local play because isGiftSpecialEffects=false '
'giftId=${gift.id}',
);
}
} 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}',
);
}
}
}
/// 将数字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 == 1 || giftType == 2 || giftType == 3 || 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,
}) async {
final targetUserIds =
acceptUsers
.map((u) => u.user?.id ?? "")
.where((id) => id.isNotEmpty)
.toList();
final firstTargetUser =
acceptUsers.isNotEmpty ? acceptUsers.first.user : null;
_giftFxLog(
'dispatch lucky anim msg '
'giftId=${gift.id} '
'giftName=${gift.giftName} '
'giftPhoto=${gift.giftPhoto} '
'quantity=$quantity '
'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,
number: quantity,
type: SCRoomMsgType.luckGiftAnimOther,
msg: jsonEncode(acceptUsers.map((u) => u.user?.id).toList()),
),
addLocal: true,
);
_giftFxLog(
'dispatch lucky anim msg finished '
'giftId=${gift.id} '
'targetUserIds=${targetUserIds.join(",")}',
);
}
}
class CheckNumber extends StatelessWidget {
final Function(int) onNumberChanged;
late BuildContext context;
CheckNumber({super.key, required this.onNumberChanged});
@override
Widget build(BuildContext context) {
this.context = 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 HeadSelect {
bool isSelect = false;
MicRes? mic;
HeadSelect(this.isSelect, this.mic);
}