From 73f1cf11998aa6539592e7dd9d5e43e82b66a869 Mon Sep 17 00:00:00 2001 From: NIGGER SLAYER Date: Wed, 15 Apr 2026 21:22:05 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A4=BC=E7=89=A9gift=E4=BB=A5=E5=8F=8A?= =?UTF-8?q?=E8=B0=B7=E6=AD=8Cservice?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- android/app/google-services.json | 42 +- .../base_business_logic_strategy.dart | 95 +- .../variant1_business_logic_strategy.dart | 31 +- lib/modules/gift/gift_page.dart | 2292 +++++++++-------- lib/modules/gift/gift_tab_page.dart | 12 +- lib/services/audio/rtc_manager.dart | 20 +- .../widgets/room/room_bottom_widget.dart | 447 ++-- 需求进度.md | 5 + 8 files changed, 1534 insertions(+), 1410 deletions(-) diff --git a/android/app/google-services.json b/android/app/google-services.json index ce18278..6f6bddd 100644 --- a/android/app/google-services.json +++ b/android/app/google-services.json @@ -1,36 +1,20 @@ { "project_info": { - "project_number": "980005024266", - "project_id": "yumi-c3b30", - "storage_bucket": "yumi-c3b30.firebasestorage.app" + "project_number": "991697445884", + "project_id": "yumi-chat-party", + "storage_bucket": "yumi-chat-party.firebasestorage.app" }, "client": [ { "client_info": { - "mobilesdk_app_id": "1:980005024266:android:581aa38059aa318d9c65f3", + "mobilesdk_app_id": "1:991697445884:android:1d6eff055324e2ba128879", "android_client_info": { "package_name": "com.org.yumiparty" } }, "oauth_client": [ { - "client_id": "980005024266-o9pjdmdbqqt1julbh1q1ovafcvmr1mv1.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "com.org.yumiparty", - "certificate_hash": "3fe178dcc3294f7420df2754cfaf4646d6a19478" - } - }, - { - "client_id": "980005024266-sl5h466pe90jsjmoi4jcd7bqhmckieec.apps.googleusercontent.com", - "client_type": 1, - "android_info": { - "package_name": "com.org.yumiparty", - "certificate_hash": "9ab21924bfbe2c56555860ebabf23c4099fd7412" - } - }, - { - "client_id": "980005024266-vrrp2us89svbqgc93oe7q4ad7uoh8fl6.apps.googleusercontent.com", + "client_id": "991697445884-6b6losu2td9u0era7qui4aeavm16lj57.apps.googleusercontent.com", "client_type": 1, "android_info": { "package_name": "com.org.yumiparty", @@ -38,20 +22,28 @@ } }, { - "client_id": "980005024266-dgtthe3q98k8tk873rfdrsnu5ot61p09.apps.googleusercontent.com", + "client_id": "991697445884-jc3r56gntpgdeicjj8j8qhkght67f7og.apps.googleusercontent.com", + "client_type": 1, + "android_info": { + "package_name": "com.org.yumiparty", + "certificate_hash": "02a381c217fa1bd986cb00aefb8476efc5bea8e3" + } + }, + { + "client_id": "991697445884-3ac3fk104691brjgkcejulijnivgr7o2.apps.googleusercontent.com", "client_type": 3 } ], "api_key": [ { - "current_key": "AIzaSyAbxU9QpbnC5PrrhUjDSK1camiotDlF3qE" + "current_key": "AIzaSyDLY8LRCaKYapwXt2BvxUoHYD8kdC1fL70" } ], "services": { "appinvite_service": { "other_platform_oauth_client": [ { - "client_id": "980005024266-dgtthe3q98k8tk873rfdrsnu5ot61p09.apps.googleusercontent.com", + "client_id": "991697445884-3ac3fk104691brjgkcejulijnivgr7o2.apps.googleusercontent.com", "client_type": 3 } ] @@ -60,4 +52,4 @@ } ], "configuration_version": "1" -} +} \ No newline at end of file diff --git a/lib/app/config/strategies/base_business_logic_strategy.dart b/lib/app/config/strategies/base_business_logic_strategy.dart index 9e64c71..56c2438 100644 --- a/lib/app/config/strategies/base_business_logic_strategy.dart +++ b/lib/app/config/strategies/base_business_logic_strategy.dart @@ -10,7 +10,6 @@ import 'package:provider/provider.dart'; /// 基础业务逻辑策略实现 /// 提供原始应用的默认业务逻辑 class BaseBusinessLogicStrategy implements BusinessLogicStrategy { - @override List getHomeTabPages(BuildContext context) { final List pages = []; @@ -25,7 +24,6 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { return tabs; } - @override int getHomeInitialTabIndex() { return 0; @@ -33,7 +31,10 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { @override void onAvatarTap(BuildContext context) { - Provider.of(context, listen: false).openDrawer(context); + Provider.of( + context, + listen: false, + ).openDrawer(context); } @override @@ -125,11 +126,6 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { /// === 家族页面差异化方法实现 === - - - - - @override bool shouldShowPasswordRoomIcon() { // 原始应用:显示密码房间图标 @@ -276,10 +272,7 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { const Color(0xFF3F0810), ]; } else { - return [ - const Color(0xFF666666), - const Color(0xFF000000), - ]; + return [const Color(0xFF666666), const Color(0xFF000000)]; } } @@ -287,15 +280,9 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { List getMePageTabIndicatorGradient(bool isFemale) { // 原始应用Tab指示器渐变 if (isFemale) { - return [ - const Color(0xffC62548), - const Color(0xff9C0322), - ]; + return [const Color(0xffC62548), const Color(0xff9C0322)]; } else { - return [ - const Color(0xff141414), - const Color(0xffE4E4E4), - ]; + return [const Color(0xff141414), const Color(0xffE4E4E4)]; } } @@ -446,10 +433,7 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { @override List getVipPageButtonGradient() { // 原始应用:VIP按钮渐变颜色 - return [ - const Color(0xffFF5722), - const Color(0xffFEB219), - ]; + return [const Color(0xffFF5722), const Color(0xffFEB219)]; } @override @@ -519,7 +503,11 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { } @override - String getVipPagePreviewImage(int vipLevel, String previewType, String featureName) { + String getVipPagePreviewImage( + int vipLevel, + String previewType, + String featureName, + ) { // 原始应用:VIP页面预览图像路径模式 if (featureName == 'profile_frame') { return "sc_images/vip/sc_icon_vip${vipLevel}_profile_rev.png"; @@ -666,10 +654,7 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { @override List getAdminEditingButtonGradient(String buttonType) { // 原始应用:编辑页面按钮渐变颜色,两个按钮使用相同的渐变 - return [ - const Color(0xffC670FF), - const Color(0xff7726FF), - ]; + return [const Color(0xffC670FF), const Color(0xff7726FF)]; } @override @@ -699,10 +684,7 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { return BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), - border: Border.all( - color: const Color(0xffE6E6E6), - width: 1.0, - ), + border: Border.all(color: const Color(0xffE6E6E6), width: 1.0), ); } @@ -722,10 +704,7 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { Map getAdminEditingViolationTypeMapping(String targetType) { // 原始应用:违规类型ID映射 if (targetType == 'User') { - return { - 'Pornography': 1, - 'Illegal information': 2, - }; + return {'Pornography': 1, 'Illegal information': 2}; } else if (targetType == 'Room') { return { 'Pornography': 3, @@ -748,10 +727,7 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { @override List getAdminSearchButtonGradient(String pageType) { // 原始应用:搜索按钮渐变颜色 - 透明渐变 - return [ - Colors.transparent, - Colors.transparent, - ]; + return [Colors.transparent, Colors.transparent]; } @override @@ -888,10 +864,7 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { @override List getEditProfileGenderButtonGradient(bool isMale, bool isSelected) { // 原始应用默认性别按钮渐变颜色 - return [ - const Color(0xffFF9326), - const Color(0xffFEB219), - ]; + return [const Color(0xffFF9326), const Color(0xffFEB219)]; } @override @@ -1203,11 +1176,14 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { case 'ACTIVITY': return "sc_images/room/sc_icon_activity_gift_head_bg.png"; case 'LUCK': - return "sc_images/room/sc_icon_luck_gift_head_bg.png"; + // Fallback to the activity header until dedicated localized assets land. + return "sc_images/room/sc_icon_activity_gift_head_bg.png"; case 'CP': - return "sc_images/room/sc_icon_cp_gift_head_bg.png"; + // Fallback to the activity header until dedicated localized assets land. + return "sc_images/room/sc_icon_activity_gift_head_bg.png"; case 'MAGIC': - return "sc_images/room/sc_icon_magic_gift_head_bg.png"; + // Fallback to the activity header until dedicated localized assets land. + return "sc_images/room/sc_icon_activity_gift_head_bg.png"; default: return "sc_images/room/sc_icon_activity_gift_head_bg.png"; } @@ -1520,10 +1496,7 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { // 原始应用:语音房间页面Tab标签选中文本样式 // 字体大小15,字体族"MyCustomFont" // 注意:页面需要将字体大小乘以ScreenUtil().sp - return const TextStyle( - fontSize: 15, - fontFamily: "MyCustomFont", - ); + return const TextStyle(fontSize: 15, fontFamily: "MyCustomFont"); } @override @@ -1531,10 +1504,7 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { // 原始应用:语音房间页面Tab标签未选中文本样式 // 字体大小13,字体族"MyCustomFont" // 注意:页面需要将字体大小乘以ScreenUtil().sp - return const TextStyle( - fontSize: 13, - fontFamily: "MyCustomFont", - ); + return const TextStyle(fontSize: 13, fontFamily: "MyCustomFont"); } @override @@ -1955,20 +1925,14 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { TextStyle getDynamicPageTabLabelStyle() { // 原始应用:动态页面Tab标签选中文本样式 - TextStyle(fontWeight: FontWeight.bold, fontSize: 16.sp) // 注意:这里无法使用.sp单位,需要在调用处使用ScreenUtil() - return const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ); + return const TextStyle(fontWeight: FontWeight.bold, fontSize: 16); } @override TextStyle getDynamicPageTabUnselectedLabelStyle() { // 原始应用:动态页面Tab标签未选中文本样式 - TextStyle(fontWeight: FontWeight.normal, fontSize: 15.sp) // 注意:这里无法使用.sp单位,需要在调用处使用ScreenUtil() - return const TextStyle( - fontWeight: FontWeight.normal, - fontSize: 15, - ); + return const TextStyle(fontWeight: FontWeight.normal, fontSize: 15); } /// === 启动页面差异化方法实现 === @@ -2568,9 +2532,10 @@ class BaseBusinessLogicStrategy implements BusinessLogicStrategy { // 原始应用:账户页面Scaffold背景颜色 - Colors.transparent return Colors.transparent; } + @override Color getGoldRecordPageListBackgroundColor() { // 马甲包策略:金币记录页面列表背景颜色 - 深灰色 return const Color(0xff1a1a1a); // #1a1a1a } -} \ No newline at end of file +} diff --git a/lib/app/config/strategies/variant1_business_logic_strategy.dart b/lib/app/config/strategies/variant1_business_logic_strategy.dart index c26eab7..c226652 100644 --- a/lib/app/config/strategies/variant1_business_logic_strategy.dart +++ b/lib/app/config/strategies/variant1_business_logic_strategy.dart @@ -11,7 +11,6 @@ import '../../../modules/home/popular/party/sc_home_party_page.dart'; /// 马甲包业务逻辑策略实现 /// 提供与原始应用不同的业务逻辑 class Variant1BusinessLogicStrategy extends BaseBusinessLogicStrategy { - /// 马甲包版本:调整Tab顺序,家族放在最后 @override List getHomeTabPages(BuildContext context) { @@ -134,11 +133,6 @@ class Variant1BusinessLogicStrategy extends BaseBusinessLogicStrategy { /// === 家族页面差异化方法实现(马甲包专属) === - - - - - @override bool shouldShowPasswordRoomIcon() { // 马甲包策略:不显示密码房间图标,简化UI @@ -1183,11 +1177,14 @@ class Variant1BusinessLogicStrategy extends BaseBusinessLogicStrategy { case 'ACTIVITY': return "sc_images/room/sc_icon_activity_gift_head_bg.png"; case 'LUCK': - return "sc_images/room/sc_icon_luck_gift_head_bg.png"; + // Fallback to the activity header until dedicated localized assets land. + return "sc_images/room/sc_icon_activity_gift_head_bg.png"; case 'CP': - return "sc_images/room/sc_icon_cp_gift_head_bg.png"; + // Fallback to the activity header until dedicated localized assets land. + return "sc_images/room/sc_icon_activity_gift_head_bg.png"; case 'MAGIC': - return "sc_images/room/sc_icon_magic_gift_head_bg.png"; + // Fallback to the activity header until dedicated localized assets land. + return "sc_images/room/sc_icon_activity_gift_head_bg.png"; default: return "sc_images/room/sc_icon_activity_gift_head_bg.png"; } @@ -1531,7 +1528,7 @@ class Variant1BusinessLogicStrategy extends BaseBusinessLogicStrategy { @override Color getRechargePageScaffoldBackgroundColor() { // 马甲包策略:充值页面Scaffold背景颜色 - 深灰色 - return Colors.transparent; // #1a1a1a + return Colors.transparent; // #1a1a1a } @override @@ -1668,7 +1665,7 @@ class Variant1BusinessLogicStrategy extends BaseBusinessLogicStrategy { @override Color getGoldRecordPageScaffoldBackgroundColor() { // 马甲包策略:金币记录页面Scaffold背景颜色 - 深灰色 - return Colors.transparent; // #1a1a1a + return Colors.transparent; // #1a1a1a } @override @@ -1811,13 +1808,13 @@ class Variant1BusinessLogicStrategy extends BaseBusinessLogicStrategy { @override Color getStoreItemUnselectedBorderColor() { // 马甲包策略:Store商品项未选中边框颜色 - 调整为浅灰色 - return Colors.transparent; // 浅灰色,替代Colors.black12 + return Colors.transparent; // 浅灰色,替代Colors.black12 } @override Color getStoreItemSelectedBorderColor() { // 马甲包策略:Store商品项选中边框颜色 - 使用马甲包主题色 - return SocialChatTheme.primaryLight; // 马甲包主题橙色,替代Color(0xffFF9500) + return SocialChatTheme.primaryLight; // 马甲包主题橙色,替代Color(0xffFF9500) } @override @@ -1829,7 +1826,7 @@ class Variant1BusinessLogicStrategy extends BaseBusinessLogicStrategy { @override Color getStoreItemPriceTextColor() { // 马甲包策略:Store商品项价格文本颜色 - 使用马甲包主要文本颜色 - return SocialChatTheme.primaryLight; // 马甲包主要文本颜色,替代Colors.black + return SocialChatTheme.primaryLight; // 马甲包主要文本颜色,替代Colors.black } @override @@ -2061,7 +2058,7 @@ class Variant1BusinessLogicStrategy extends BaseBusinessLogicStrategy { @override Color getSearchPageScaffoldBackgroundColor() { // 马甲包策略:搜索页面Scaffold背景颜色 - 使用浅灰色背景 - return Colors.transparent; // 马甲包浅灰色背景颜色,替代Colors.white + return Colors.transparent; // 马甲包浅灰色背景颜色,替代Colors.white } @override @@ -2436,7 +2433,7 @@ class Variant1BusinessLogicStrategy extends BaseBusinessLogicStrategy { @override Color getSettingsPageContainerBorderColor() { // 马甲包策略:设置页面容器边框颜色 - 使用浅灰色 - return Colors.transparent; + return Colors.transparent; } @override @@ -2560,6 +2557,6 @@ class Variant1BusinessLogicStrategy extends BaseBusinessLogicStrategy { @override Color getGoldRecordPageListBackgroundColor() { // 马甲包策略:金币记录页面列表背景颜色 - 深灰色 - return Colors.transparent; // #1a1a1a + return Colors.transparent; // #1a1a1a } } diff --git a/lib/modules/gift/gift_page.dart b/lib/modules/gift/gift_page.dart index 1b895d3..2f6fdbd 100644 --- a/lib/modules/gift/gift_page.dart +++ b/lib/modules/gift/gift_page.dart @@ -1,1045 +1,1247 @@ -import 'dart:convert'; -import 'dart:ui' as ui; - -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/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 GiftPage extends StatefulWidget { - SocialChatUserProfile? toUser; - - GiftPage({super.key, this.toUser}); - - @override - _GiftPageState createState() => _GiftPageState(); -} - -class _GiftPageState extends State - with SingleTickerProviderStateMixin { - late TabController _tabController; - - /// 业务逻辑策略访问器 - BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; - - // int checkedIndex = 0; - SocialChatGiftRes? checkedGift; - final List _pages = []; - final List _tabs = []; - RtcProvider? rtcProvider; - - bool isAll = false; - List listMai = []; - - bool noShowNumber = false; - - ///选中人员 - Set set = {}; - - ///数量的箭头是否朝上 - bool isNumberUp = true; - - ///数量 - int number = 1; - - int giveType = 1; - - int giftType = 0; - Debouncer debouncer = Debouncer(); - - void _giftFxLog(String message) { - debugPrint('[GiftFX][Send] $message'); - } - - void _handleGiftSelected( - SocialChatGiftRes? gift, { - required int nextGiftType, - }) { - 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; - setState(() { - giftType = nextGiftType; - }); - } - - @override - void initState() { - super.initState(); - rtcProvider = Provider.of(context, listen: false); - Provider.of(context, listen: false).giftList(); - Provider.of(context, listen: false).giftActivityList(); - // Provider.of(context, listen: false).giftBackpack(); - Provider.of(context, listen: false).balance(); - _pages.add( - GiftTabPage("ALL", (int checkedI) { - var all = - Provider.of( - context, - listen: false, - ).giftByTab["ALL"]; - _handleGiftSelected( - all != null ? all[checkedI] : null, - nextGiftType: 0, - ); - }), - ); - - _tabController = TabController(length: _pages.length, vsync: this); - _tabController.addListener(() {}); // 监听切换 - 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 - Widget build(BuildContext context) { - _tabs.clear(); - _tabs.add(Tab(text: SCAppLocalizations.of(context)!.gift)); - return Consumer( - builder: (context, ref, child) { - 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: Color(0xff09372E).withOpacity(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: Container( - 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: _tabs, - ), - ), - ), - SizedBox(width: 5.w), - ], - ), - Expanded( - child: TabBarView( - physics: NeverScrollableScrollPhysics(), - controller: _tabController, - children: _pages, - ), - ), - 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( - 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: [ - 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() { - List acceptUserIds = []; - List 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(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) { - SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectTheRecipient); - return; - } - if (checkedGift == null) { - return; - } - SCChatRoomRepository() - .giveGift( - acceptUserIds, - checkedGift!.id ?? "", - number, - false, - roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", - ) - .then((result) { - _giftFxLog( - 'giveGift success giftId=${checkedGift?.id} ' - 'giftName=${checkedGift?.giftName} ' - 'giftSourceUrl=${checkedGift?.giftSourceUrl} ' - 'special=${checkedGift?.special} ' - 'giftTab=${checkedGift?.giftTab} ' - 'number=$number ' - 'acceptUserIds=${acceptUserIds.join(",")} ' - 'roomId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id}', - ); - // SCTts.show(SCAppLocalizations.of(context)!.giftGivingSuccessful); - sendGiftMsg(acceptUsers); - Provider.of( - context, - listen: false, - ).updateBalance(result); - }) - .catchError((e) { - _giftFxLog( - 'giveGift failed giftId=${checkedGift?.id} ' - 'giftName=${checkedGift?.giftName} ' - 'error=$e', - ); - }); - } - - void sendGiftMsg(List acceptUsers) { - ///发送一条IM消息 - for (var u in acceptUsers) { - final special = checkedGift?.special ?? ""; - final giftSourceUrl = checkedGift?.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=${checkedGift?.id} ' - 'giftName=${checkedGift?.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( - navigatorKey.currentState!.context, - listen: false, - ).dispatchMessage( - Msg( - groupId: - rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount, - gift: checkedGift, - user: AccountStorage().getCurrentUser()?.userProfile, - toUser: u.user, - number: number, - type: SCRoomMsgType.gift, - role: - Provider.of( - 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( - navigatorKey.currentState!.context, - listen: false, - ).retrieveMicrophoneList(); - }, - ); - } - num coins = checkedGift!.giftCandy! * number; - if (coins > 9999) { - var fMsg = SCFloatingMessage( - type: 1, - userAvatarUrl: - AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? "", - userName: - AccountStorage().getCurrentUser()?.userProfile?.userNickname ?? - "", - toUserName: u.user?.userNickname ?? "", - giftUrl: checkedGift?.giftPhoto ?? "", - roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", - coins: coins, - number: number, - ); - OverlayManager().addMessage(fMsg); - } - if (checkedGift!.giftSourceUrl != null && checkedGift!.special != null) { - if (scGiftHasFullScreenEffect(checkedGift!.special)) { - if (SCGlobalConfig.isGiftSpecialEffects) { - _giftFxLog( - 'local trigger player play ' - 'path=${checkedGift!.giftSourceUrl} ' - 'giftId=${checkedGift?.id} ' - 'giftName=${checkedGift?.giftName}', - ); - SCGiftVapSvgaManager().play(checkedGift!.giftSourceUrl!); - } else { - _giftFxLog( - 'skip local play because isGiftSpecialEffects=false ' - 'giftId=${checkedGift?.id}', - ); - } - } else { - _giftFxLog( - 'skip local play because special does not include ' - '${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} ' - 'giftId=${checkedGift?.id} special=${checkedGift?.special}', - ); - } - } else { - _giftFxLog( - 'skip local play because giftSourceUrl or special is null ' - 'giftId=${checkedGift?.id} ' - 'giftSourceUrl=${checkedGift?.giftSourceUrl} ' - 'special=${checkedGift?.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(); - } - - ///发送一条消息,幸运礼物,房间里所有人都能看到礼物飘向麦位的动画 - void sendLuckGiftAnimOtherMsg(List acceptUsers) { - Provider.of( - navigatorKey.currentState!.context, - listen: false, - ).dispatchMessage( - Msg( - groupId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount, - gift: checkedGift, - type: SCRoomMsgType.luckGiftAnimOther, - msg: jsonEncode(acceptUsers.map((u) => u.user?.id).toList()), - ), - addLocal: false, - ); - } -} - -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: [ - 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: [ - 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); -} +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/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 + with SingleTickerProviderStateMixin { + static const List _preferredGiftTabOrder = [ + "ALL", + "ACTIVITY", + "LUCKY_GIFT", + "CP", + "MAGIC", + "CUSTOMIZED", + "NSCIONAL_FLAG", + ]; + + TabController? _tabController; + List _tabTypes = []; + + /// 业务逻辑策略访问器 + BusinessLogicStrategy get _strategy => SCGlobalConfig.businessLogicStrategy; + + // int checkedIndex = 0; + SocialChatGiftRes? checkedGift; + final Map _selectedGiftByTab = + {}; + RtcProvider? rtcProvider; + + bool isAll = false; + List listMai = []; + + bool noShowNumber = false; + + ///选中人员 + Set set = {}; + + ///数量的箭头是否朝上 + bool isNumberUp = true; + + ///数量 + int number = 1; + + int giveType = 1; + + int giftType = 0; + Debouncer debouncer = Debouncer(); + + void _giftFxLog(String message) { + debugPrint('[GiftFX][Send] $message'); + } + + 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(context, listen: false); + Provider.of(context, listen: false).giftList(); + Provider.of(context, listen: false).giftActivityList(); + // Provider.of(context, listen: false).giftBackpack(); + Provider.of(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(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 = []; + 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 []; + final gift = _resolveSelectedGift(tabType, gifts); + _selectedGiftByTab[tabType] = gift; + _applyGiftSelection(gift, notify: notify); + } + + void _ensureCurrentTabSelection(SCAppGeneralManager ref, String tabType) { + final gifts = ref.giftByTab[tabType] ?? const []; + if (_matchesCurrentGift(gifts)) { + return; + } + final gift = _resolveSelectedGift(tabType, gifts); + _selectedGiftByTab[tabType] = gift; + _applyGiftSelection(gift, notify: false); + } + + SocialChatGiftRes? _resolveSelectedGift( + String tabType, + List 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 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; + } + } + + @override + Widget build(BuildContext context) { + return Consumer( + 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( + 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: [ + 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() { + List acceptUserIds = []; + List 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(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) { + SCTts.show(SCAppLocalizations.of(context)!.pleaseSelectTheRecipient); + return; + } + if (checkedGift == null) { + return; + } + SCChatRoomRepository() + .giveGift( + acceptUserIds, + checkedGift!.id ?? "", + number, + false, + roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", + ) + .then((result) { + _giftFxLog( + 'giveGift success giftId=${checkedGift?.id} ' + 'giftName=${checkedGift?.giftName} ' + 'giftSourceUrl=${checkedGift?.giftSourceUrl} ' + 'special=${checkedGift?.special} ' + 'giftTab=${checkedGift?.giftTab} ' + 'number=$number ' + 'acceptUserIds=${acceptUserIds.join(",")} ' + 'roomId=${rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id}', + ); + // SCTts.show(SCAppLocalizations.of(context)!.giftGivingSuccessful); + sendGiftMsg(acceptUsers); + Provider.of( + context, + listen: false, + ).updateBalance(result); + }) + .catchError((e) { + _giftFxLog( + 'giveGift failed giftId=${checkedGift?.id} ' + 'giftName=${checkedGift?.giftName} ' + 'error=$e', + ); + }); + } + + void sendGiftMsg(List acceptUsers) { + ///发送一条IM消息 + for (var u in acceptUsers) { + final special = checkedGift?.special ?? ""; + final giftSourceUrl = checkedGift?.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=${checkedGift?.id} ' + 'giftName=${checkedGift?.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( + navigatorKey.currentState!.context, + listen: false, + ).dispatchMessage( + Msg( + groupId: + rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount, + gift: checkedGift, + user: AccountStorage().getCurrentUser()?.userProfile, + toUser: u.user, + number: number, + type: SCRoomMsgType.gift, + role: + Provider.of( + 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( + navigatorKey.currentState!.context, + listen: false, + ).retrieveMicrophoneList(); + }, + ); + } + num coins = checkedGift!.giftCandy! * number; + if (coins > 9999) { + var fMsg = SCFloatingMessage( + type: 1, + userAvatarUrl: + AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? "", + userName: + AccountStorage().getCurrentUser()?.userProfile?.userNickname ?? + "", + toUserName: u.user?.userNickname ?? "", + giftUrl: checkedGift?.giftPhoto ?? "", + roomId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.id ?? "", + coins: coins, + number: number, + ); + OverlayManager().addMessage(fMsg); + } + if (checkedGift!.giftSourceUrl != null && checkedGift!.special != null) { + if (scGiftHasFullScreenEffect(checkedGift!.special)) { + if (SCGlobalConfig.isGiftSpecialEffects) { + _giftFxLog( + 'local trigger player play ' + 'path=${checkedGift!.giftSourceUrl} ' + 'giftId=${checkedGift?.id} ' + 'giftName=${checkedGift?.giftName}', + ); + SCGiftVapSvgaManager().play(checkedGift!.giftSourceUrl!); + } else { + _giftFxLog( + 'skip local play because isGiftSpecialEffects=false ' + 'giftId=${checkedGift?.id}', + ); + } + } else { + _giftFxLog( + 'skip local play because special does not include ' + '${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} ' + 'giftId=${checkedGift?.id} special=${checkedGift?.special}', + ); + } + } else { + _giftFxLog( + 'skip local play because giftSourceUrl or special is null ' + 'giftId=${checkedGift?.id} ' + 'giftSourceUrl=${checkedGift?.giftSourceUrl} ' + 'special=${checkedGift?.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(); + } + + ///发送一条消息,幸运礼物,房间里所有人都能看到礼物飘向麦位的动画 + void sendLuckGiftAnimOtherMsg(List acceptUsers) { + Provider.of( + navigatorKey.currentState!.context, + listen: false, + ).dispatchMessage( + Msg( + groupId: rtcProvider?.currenRoom?.roomProfile?.roomProfile?.roomAccount, + gift: checkedGift, + type: SCRoomMsgType.luckGiftAnimOther, + msg: jsonEncode(acceptUsers.map((u) => u.user?.id).toList()), + ), + addLocal: false, + ); + } +} + +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: [ + 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: [ + 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); +} diff --git a/lib/modules/gift/gift_tab_page.dart b/lib/modules/gift/gift_tab_page.dart index f015cf2..6536a10 100644 --- a/lib/modules/gift/gift_tab_page.dart +++ b/lib/modules/gift/gift_tab_page.dart @@ -24,7 +24,7 @@ const Duration _kGiftPageSkeletonMaxDuration = Duration(milliseconds: 900); class GiftTabPage extends StatefulWidget { final String type; final bool isDark; - final Function(int checkedIndex) checkedCall; + final ValueChanged checkedCall; const GiftTabPage( this.type, @@ -53,13 +53,7 @@ class _GiftTabPageState extends State @override void initState() { super.initState(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (widget.type == "CUSTOMIZED") { - widget.checkedCall(1); - } else { - widget.checkedCall(0); - } - }); + checkedIndex = widget.type == "CUSTOMIZED" ? 1 : 0; } @override @@ -522,7 +516,7 @@ class _GiftTabPageState extends State onTap: () { setState(() { checkedIndex = ref.giftByTab[widget.type]!.indexOf(gift); - widget.checkedCall(checkedIndex); + widget.checkedCall(gift); }); }, ); diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index a02f496..aea0e7f 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -109,13 +109,6 @@ class RealTimeCommunicationManager extends ChangeNotifier { onError: (ErrorCodeType err, String msg) { print('rtc错误${err}'); }, - onLocalAudioStateChanged: - ( - RtcConnection connection, - LocalAudioStreamState state, - LocalAudioStreamReason reason, - ) {}, - onAudioRoutingChanged: (routing) {}, onAudioMixingStateChanged: ( AudioMixingStateType state, AudioMixingReasonType reason, @@ -132,15 +125,6 @@ class RealTimeCommunicationManager extends ChangeNotifier { break; } }, - onRemoteAudioStateChanged: ( - RtcConnection connection, - int remoteUid, - RemoteAudioState state, - RemoteAudioStateReason reason, - int elapsed, - ) { - // print('用户 $remoteUid 音频状态: $state, 原因: $reason'); - }, onJoinChannelSuccess: (RtcConnection connection, int elapsed) { print('rtc 自己加入 ${connection.channelId} ${connection.localUid}'); }, @@ -1037,6 +1021,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { context!, listen: false, ).engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); + Provider.of( + context!, + listen: false, + ).engine?.muteLocalAudioStream(false); } } } diff --git a/lib/ui_kit/widgets/room/room_bottom_widget.dart b/lib/ui_kit/widgets/room/room_bottom_widget.dart index 0566066..5069a39 100644 --- a/lib/ui_kit/widgets/room/room_bottom_widget.dart +++ b/lib/ui_kit/widgets/room/room_bottom_widget.dart @@ -1,233 +1,214 @@ -import 'package:agora_rtc_engine/agora_rtc_engine.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:yumi/ui_kit/widgets/room/room_menu_dialog.dart'; -import 'package:yumi/ui_kit/widgets/room/room_msg_input.dart'; -import 'package:provider/provider.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; -import 'package:yumi/shared/tools/sc_room_utils.dart'; -import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; -import 'package:yumi/services/audio/rtc_manager.dart'; -import 'package:yumi/modules/gift/gift_page.dart'; - -import '../../../app/routes/sc_fluro_navigator.dart'; -import '../../../modules/index/main_route.dart'; -import '../../../services/audio/rtm_manager.dart'; -import '../../components/sc_debounce_widget.dart'; - -class RoomBottomWidget extends StatefulWidget { - @override - _RoomBottomWidgetState createState() => _RoomBottomWidgetState(); -} - -class _RoomBottomWidgetState extends State { - int roomMenuStime1 = 0; - - @override - Widget build(BuildContext context) { - return Stack( - alignment: Alignment.center, - children: [ - Container( - height: 55.w, - margin: EdgeInsetsDirectional.only(start: 25.w), - child: Row( - children: [ - SCDebounceWidget( - onTap: () { - if (SCRoomUtils.touristCanMsg(context)) { - Navigator.push( - context, - PopRoute(child: RoomMsgInput()), - ); - } - }, child: Image.asset( - "sc_images/room/icon_room_input_t.png", - width: 30.w, - height: 30.w, - ), - ), - Spacer(), - Consumer( - builder: (context, ref, child) { - return _mic1(ref); - }, - ), - SizedBox(width: 10.w), - SCDebounceWidget( - child: Image.asset( - "sc_images/room/sc_icon_botton_menu.png", - width: 30.w, - height: 30.w, - fit: BoxFit.contain, - ), - onTap: () { - SmartDialog.show( - tag: "showRoomMenuDialog", - alignment: Alignment.bottomCenter, - debounce: true, - animationType: SmartAnimationType.fade, - maskColor: Colors.transparent, - clickMaskDismiss: true, - builder: (_) { - return RoomMenuDialog(roomMenuStime1, (eTime) { - roomMenuStime1 = eTime; - }); - }, - ); - }, - ), - SizedBox(width: 10.w), - SCDebounceWidget( - child: Selector( - selector: (c, p) => p.allUnReadCount, - shouldRebuild: (prev, next) => prev != next, - builder: (_, allUnReadCount, __) { - return allUnReadCount > 0 - ? Badge( - backgroundColor: Colors.red, - label: text( - "${allUnReadCount > 99 ? "99+" : allUnReadCount}", - fontSize: 9.sp, - textColor: Colors.white, - fontWeight: FontWeight.w600, - ), - alignment: AlignmentDirectional.topEnd, - child: Image.asset( - "sc_images/room/sc_icon_botton_message.png", - width: 30.w, - height: 30.w, - fit: BoxFit.contain, - ), - ) - : Image.asset( - "sc_images/room/sc_icon_botton_message.png", - width: 30.w, - height: 30.w, - fit: BoxFit.contain, - ); - }, - ), - onTap: () { - SCNavigatorUtils.push( - context, - "${SCMainRoute.message}?isFromRoom=true", - ); - }, - ), - SizedBox(width: 15.w), - ], - ), - ), - PositionedDirectional( - bottom: 8.w, - child: SCDebounceWidget( - onTap: () { - SmartDialog.show( - tag: "showGiftControl", - alignment: Alignment.bottomCenter, - maskColor: Colors.transparent, - animationType: SmartAnimationType.fade, - clickMaskDismiss: true, - builder: (_) { - return GiftPage(); - }, - ); - }, - child: Image.asset( - "sc_images/room/sc_icon_botton_gift.png", - width: 45.w, - height: 45.w, - fit: BoxFit.contain, - ), - ), - ), - ], - ); - } - - _mic1(RtcProvider provider) { - ///默认不显示 - bool show = false; - - ///在麦上 - provider.roomWheatMap.forEach((k, v) { - if (v.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { - show = true; - } - }); - - return Visibility( - visible: show, - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - setState(() { - provider.isMic = !provider.isMic; - - ///没被禁麦才显示 - provider.roomWheatMap.forEach((k, v) { - if (v.user?.id == - AccountStorage().getCurrentUser()?.userProfile?.id && - !provider.roomWheatMap[k]!.micMute!) { - if (!provider.isMic) { - Provider.of( - context, - listen: false, - ).engine?.adjustRecordingSignalVolume(100); - Provider.of( - context, - listen: false, - ).engine?.setClientRole( - role: ClientRoleType.clientRoleBroadcaster, - ); - } else { - if (Provider.of( - context, - listen: false, - ).isMusicPlaying) { - Provider.of( - context, - listen: false, - ).engine?.adjustRecordingSignalVolume(0); - } else { - Provider.of( - context, - listen: false, - ).engine?.setClientRole( - role: ClientRoleType.clientRoleAudience, - ); - Provider.of( - context, - listen: false, - ).engine?.muteLocalAudioStream(provider.isMic); - } - } - } - }); - }); - }, - child: Padding( - padding: EdgeInsets.only(right: 12.w), - child: Container( - width: 30.w, - height: 30.w, - alignment: Alignment.center, - // decoration: BoxDecoration( - // color: Colors.white.withOpacity(0.2), shape: BoxShape.circle - // ), - child: Image.asset( - "sc_images/room/${provider.isMic ? 'sc_icon_botton_mic_close' : 'sc_icon_botton_mic_open'}.png", - width: 30.w, - fit: BoxFit.fill, - gaplessPlayback: true, - ), - ), - ), - ), - ); - } -} +import 'package:agora_rtc_engine/agora_rtc_engine.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/ui_kit/widgets/room/room_menu_dialog.dart'; +import 'package:yumi/ui_kit/widgets/room/room_msg_input.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/modules/gift/gift_page.dart'; + +import '../../../app/routes/sc_fluro_navigator.dart'; +import '../../../modules/index/main_route.dart'; +import '../../../services/audio/rtm_manager.dart'; +import '../../components/sc_debounce_widget.dart'; + +class RoomBottomWidget extends StatefulWidget { + const RoomBottomWidget({super.key}); + + @override + _RoomBottomWidgetState createState() => _RoomBottomWidgetState(); +} + +class _RoomBottomWidgetState extends State { + int roomMenuStime1 = 0; + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + Container( + height: 55.w, + margin: EdgeInsetsDirectional.only(start: 25.w), + child: Row( + children: [ + SCDebounceWidget( + onTap: () { + if (SCRoomUtils.touristCanMsg(context)) { + Navigator.push(context, PopRoute(child: RoomMsgInput())); + } + }, + child: Image.asset( + "sc_images/room/icon_room_input_t.png", + width: 30.w, + height: 30.w, + ), + ), + Spacer(), + Consumer( + builder: (context, ref, child) { + return _mic1(ref); + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + child: Image.asset( + "sc_images/room/sc_icon_botton_menu.png", + width: 30.w, + height: 30.w, + fit: BoxFit.contain, + ), + onTap: () { + SmartDialog.show( + tag: "showRoomMenuDialog", + alignment: Alignment.bottomCenter, + debounce: true, + animationType: SmartAnimationType.fade, + maskColor: Colors.transparent, + clickMaskDismiss: true, + builder: (_) { + return RoomMenuDialog(roomMenuStime1, (eTime) { + roomMenuStime1 = eTime; + }); + }, + ); + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + child: Selector( + selector: (c, p) => p.allUnReadCount, + shouldRebuild: (prev, next) => prev != next, + builder: (_, allUnReadCount, __) { + return allUnReadCount > 0 + ? Badge( + backgroundColor: Colors.red, + label: text( + "${allUnReadCount > 99 ? "99+" : allUnReadCount}", + fontSize: 9.sp, + textColor: Colors.white, + fontWeight: FontWeight.w600, + ), + alignment: AlignmentDirectional.topEnd, + child: Image.asset( + "sc_images/room/sc_icon_botton_message.png", + width: 30.w, + height: 30.w, + fit: BoxFit.contain, + ), + ) + : Image.asset( + "sc_images/room/sc_icon_botton_message.png", + width: 30.w, + height: 30.w, + fit: BoxFit.contain, + ); + }, + ), + onTap: () { + SCNavigatorUtils.push( + context, + "${SCMainRoute.message}?isFromRoom=true", + ); + }, + ), + SizedBox(width: 15.w), + ], + ), + ), + PositionedDirectional( + bottom: 8.w, + child: SCDebounceWidget( + onTap: () { + SmartDialog.show( + tag: "showGiftControl", + alignment: Alignment.bottomCenter, + maskColor: Colors.transparent, + animationType: SmartAnimationType.fade, + clickMaskDismiss: true, + builder: (_) { + return GiftPage(); + }, + ); + }, + child: Image.asset( + "sc_images/room/sc_icon_botton_gift.png", + width: 45.w, + height: 45.w, + fit: BoxFit.contain, + ), + ), + ), + ], + ); + } + + _mic1(RtcProvider provider) { + ///默认不显示 + bool show = false; + + ///在麦上 + provider.roomWheatMap.forEach((k, v) { + if (v.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { + show = true; + } + }); + + return Visibility( + visible: show, + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + setState(() { + provider.isMic = !provider.isMic; + + ///没被禁麦才显示 + provider.roomWheatMap.forEach((k, v) { + if (v.user?.id == + AccountStorage().getCurrentUser()?.userProfile?.id && + !provider.roomWheatMap[k]!.micMute!) { + if (!provider.isMic) { + provider.engine?.adjustRecordingSignalVolume(100); + provider.engine?.setClientRole( + role: ClientRoleType.clientRoleBroadcaster, + ); + provider.engine?.muteLocalAudioStream(false); + } else { + if (provider.isMusicPlaying) { + provider.engine?.adjustRecordingSignalVolume(0); + } else { + provider.engine?.setClientRole( + role: ClientRoleType.clientRoleAudience, + ); + provider.engine?.muteLocalAudioStream(provider.isMic); + } + } + } + }); + }); + }, + child: Padding( + padding: EdgeInsets.only(right: 12.w), + child: Container( + width: 30.w, + height: 30.w, + alignment: Alignment.center, + // decoration: BoxDecoration( + // color: Colors.white.withOpacity(0.2), shape: BoxShape.circle + // ), + child: Image.asset( + "sc_images/room/${provider.isMic ? 'sc_icon_botton_mic_close' : 'sc_icon_botton_mic_open'}.png", + width: 30.w, + fit: BoxFit.fill, + gaplessPlayback: true, + ), + ), + ), + ), + ); + } +} diff --git a/需求进度.md b/需求进度.md index da9ae13..53683d8 100644 --- a/需求进度.md +++ b/需求进度.md @@ -13,6 +13,11 @@ - 本轮按需求暂未处理网络链路上的启动等待,例如审核态检查或远端启动页配置请求。 ## 已完成模块 +- 已排查语言房“双设备、不同账号进入同一房间,上麦后无声”的直接原因:当前 RTC 进房时统一以 Agora `Audience` 身份加入频道,见 `lib/services/audio/rtc_manager.dart` 中 `joinChannel()` 的 `clientRoleType: clientRoleAudience`;而本地麦克风开关 `isMic` 默认值为 `true`(当前语义实际是“闭麦”),上麦 `shangMai()` 后只有在 `!isMic` 时才会切到 `Broadcaster` 并取消本地静音,所以“只上麦、不点底部麦克风按钮”时不会发声。 +- 已补充语言房当前真实交互结论:现有实现里“上麦”和“开麦”是两个动作。用户点空麦位后只是占麦;还需要再点击底部麦克风按钮,触发 `lib/ui_kit/widgets/room/room_bottom_widget.dart` 中的角色切换,才能真正开始向房间发送音频。 +- 已同步补充该需求的后续决策点:如果产品预期是“上麦即能说话”,则需要单独改需求并调整实现为“上麦成功后自动切 `Broadcaster` 且自动开麦”;如果继续保留当前双步骤交互,则至少要补一条明确提示文案/引导,并把“双设备不同账号同房,上麦后需手动开麦才能互听”加入验收用例,避免测试误判为 RTC 故障。 +- 已重新收敛语言房无声修复方案:撤回上一轮会影响进房的较大改动后,当前改为只做最小行为修补,不触碰进房和 token/join 链路;已在“底部麦克风点开”和“麦位解除静音且本人处于开麦态”两条路径上补齐 `muteLocalAudioStream(false)`,修正 UI 显示已开麦但 Agora 本地音频流仍保持静音的问题。 +- 已按测试收尾要求移除语言房 RTC 诊断面板:该组件仅用于当前无声问题的真机排查,现已从房间页和相关临时诊断代码中整体删除,不作为正式功能保留。 - 已为语言房页面补充右下方 `game` 悬浮入口:入口位于聊天区右下侧、距右侧约 `15.w`、位于底部操作栏上方约 `100.w`,当前先使用代码绘制的 `🎮` 占位图标,并已在实现处标注后续替换为正式 UI 图片资源。 - 已新增语言房游戏底部弹窗:点击 `game` 入口后会从底部弹出约三分之一屏高的面板,采用房间页现有半透明磨砂风格,便于后续继续沿用当前视觉体系。 - 已补齐语言房游戏列表占位结构:弹窗内部先用静态游戏数据驱动,并按“`ListView` 可上下滑动 + 每行 `5` 个图标位”的方式实现,后续只需替换入口图、列表图标和真实接口数据即可继续开发。