diff --git a/assets/l10n/intl_ar.json b/assets/l10n/intl_ar.json index 7ea7a87..13c202c 100644 --- a/assets/l10n/intl_ar.json +++ b/assets/l10n/intl_ar.json @@ -303,8 +303,9 @@ "goToRecharge": "اذهب لإعادة الشحن", "warning": "تحذير", "ownerSendTheRedEnvelope": "أرسل مالك الغرفة عملات المكافأة.", - "rewardCoins": "عملات المكافأة:{1} عملة", - "lastWeekProgress": "تقدم الأسبوع الماضي", + "rewardCoins": "عملات المكافأة:{1} عملة", + "signInRewardReceived": "تم تسجيل الدخول بنجاح. المكافأة: {1}", + "lastWeekProgress": "تقدم الأسبوع الماضي", "currentProgress": "التقدم الحالي", "coins2": "{1} عملات", "roomReward2": "مكافأة الغرفة:{1}", diff --git a/assets/l10n/intl_bn.json b/assets/l10n/intl_bn.json index 6c2a75d..6906807 100644 --- a/assets/l10n/intl_bn.json +++ b/assets/l10n/intl_bn.json @@ -159,6 +159,7 @@ "roomReward": "রুম পুরস্কার", "ownerSendTheRedEnvelope": "মালিক পুরস্কার কয়েন পাঠিয়েছেন।", "rewardCoins": "পুরস্কার কয়েন:{1} কয়েন", + "signInRewardReceived": "সাইন ইন সফল হয়েছে। পুরস্কার: {1}", "lastWeekProgress": "গত সপ্তাহের অগ্রগতি", "redEnvelopeTips2": "*লাল খাম সময়সীমার মধ্যে দাবি না করলে, বাকি কয়েন প্রেরক ব্যবহারকারীকে ফেরত দেওয়া হবে।", "goToRecharge": "রিচার্জ করতে যান", diff --git a/assets/l10n/intl_en.json b/assets/l10n/intl_en.json index c7ddff8..ad40369 100644 --- a/assets/l10n/intl_en.json +++ b/assets/l10n/intl_en.json @@ -131,6 +131,7 @@ "expirationTime": "Expiration time", "ownerSendTheRedEnvelope": "The owner sent reward coins.", "rewardCoins": "Reward coins:{1} coins", + "signInRewardReceived": "Signed in successfully. Reward: {1}", "lastWeekProgress": "Last week's progress", "redEnvelopeTips2": "*If the red envelope is not claimed within the time limit, the remaining coins will be returned to the user who sent the red envelope.", "goToRecharge": "Go to recharge", diff --git a/assets/l10n/intl_tr.json b/assets/l10n/intl_tr.json index 0ba1a08..2bec44f 100644 --- a/assets/l10n/intl_tr.json +++ b/assets/l10n/intl_tr.json @@ -117,10 +117,11 @@ "currentProgress": "Mevcut İlerleme", "currentStage": "Mevcut Aşama:{1}", "roomReward2": "Oda Ödülü:{1}", - "roomReward": "Oda Ödülü", - "ownerSendTheRedEnvelope": "Sahip ödül jettonlarını gönderdi.", - "rewardCoins": "Ödül jettonları:{1} jetton", - "lastWeekProgress": "Geçen Haftanın İlerlemesi", + "roomReward": "Oda Ödülü", + "ownerSendTheRedEnvelope": "Sahip ödül jettonlarını gönderdi.", + "rewardCoins": "Ödül jettonları:{1} jetton", + "signInRewardReceived": "Giriş başarılı. Ödül: {1}", + "lastWeekProgress": "Geçen Haftanın İlerlemesi", "redEnvelopeTips2": "*Kırmızı zarf zaman sınırı içinde talep edilmezse, kalan jettonlar gönderen kullanıcıya iade edilecektir.", "goToRecharge": "Yüklemeye Git", "deleteAccount2": " Hesabı Sil ({1} sn)", diff --git a/lib/app/config/configs/sc_variant1_config.dart b/lib/app/config/configs/sc_variant1_config.dart index 288ffcf..5a1b468 100644 --- a/lib/app/config/configs/sc_variant1_config.dart +++ b/lib/app/config/configs/sc_variant1_config.dart @@ -11,22 +11,22 @@ class SCVariant1Config implements AppConfig { String get appName => 'yumi'; // 马甲包应用名称 @override - String get packageName => 'com.org.yumiparty'; + String get packageName => 'com.org.yumiparty'; - @override - String get apiHost => const String.fromEnvironment( - 'API_HOST', - defaultValue: 'https://jvapi.haiyihy.com/', - ); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖 + @override + String get apiHost => const String.fromEnvironment( + 'API_HOST', + defaultValue: 'http://192.168.110.43:1100/', + ); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖 @override String get imgHost => 'https://img.atuchat.com/'; // 测试图片服务器,上架前需替换为正式域名 @override - String get privacyAgreementUrl => 'https://h5.haiyihy.com/privacy.html'; // 正式隐私政策页面 + String get privacyAgreementUrl => 'https://h5.haiyihy.com/privacy.html'; // 正式隐私政策页面 @override - String get userAgreementUrl => 'https://h5.haiyihy.com/service.html'; // 正式用户协议页面 + String get userAgreementUrl => 'https://h5.haiyihy.com/service.html'; // 正式用户协议页面 @override String get appDownloadUrlGoogle => 'https://play.google.com/store/apps/details?id=$packageName'; @@ -35,49 +35,49 @@ class SCVariant1Config implements AppConfig { String get appDownloadUrlApple => 'https://apps.apple.com/us/app/atuchat/id1234567890'; // 需要更新为真实App Store ID @override - String get anchorAgentUrl => 'https://h5.haiyihy.com/apply/index.html'; // 正式 H5 页面 + String get anchorAgentUrl => 'https://h5.haiyihy.com/apply/index.html'; // 正式 H5 页面 @override - String get hostCenterUrl => 'https://h5.haiyihy.com/host-center/index.html'; // 正式 H5 页面 + String get hostCenterUrl => 'https://h5.haiyihy.com/host-center/index.html'; // 正式 H5 页面 @override - String get bdCenterUrl => 'https://h5.haiyihy.com/bd-center/index.html'; // 正式 H5 页面 + String get bdCenterUrl => 'https://h5.haiyihy.com/bd-center/index.html'; // 正式 H5 页面 @override - String get bdLeaderUrl => 'https://h5.haiyihy.com/bd-leader-center/index.html'; // 正式 H5 页面 + String get bdLeaderUrl => 'https://h5.haiyihy.com/bd-leader-center/index.html'; // 正式 H5 页面 @override - String get coinSellerUrl => 'https://h5.haiyihy.com/coin-seller/index.html'; // 正式 H5 页面 + String get coinSellerUrl => 'https://h5.haiyihy.com/coin-seller/index.html'; // 正式 H5 页面 @override - String get adminUrl => 'https://h5.haiyihy.com/admin-center/index.html'; // 正式 H5 页面 + String get adminUrl => 'https://h5.haiyihy.com/admin-center/index.html'; // 正式 H5 页面 @override - String get agencyCenterUrl => 'https://h5.haiyihy.com/agency-center/index.html'; // 正式 H5 页面 + String get agencyCenterUrl => 'https://h5.haiyihy.com/agency-center/index.html'; // 正式 H5 页面 @override - String get gamesKingUrl => 'https://h5.haiyihy.com/games-king/index.html'; // 正式 H5 页面 + String get gamesKingUrl => 'https://h5.haiyihy.com/games-king/index.html'; // 正式 H5 页面 @override - String get wealthRankUrl => 'https://h5.haiyihy.com/ranking/index.html?first=Wealth'; // 正式 H5 页面 + String get wealthRankUrl => 'https://h5.haiyihy.com/ranking/index.html?first=Wealth'; // 正式 H5 页面 @override - String get charmRankUrl => 'https://h5.haiyihy.com/ranking/index.html?first=Charm'; // 正式 H5 页面 + String get charmRankUrl => 'https://h5.haiyihy.com/ranking/index.html?first=Charm'; // 正式 H5 页面 @override - String get roomRankUrl => 'https://h5.haiyihy.com/ranking/index.html?first=Room'; // 正式 H5 页面 + String get roomRankUrl => 'https://h5.haiyihy.com/ranking/index.html?first=Room'; // 正式 H5 页面 @override - String get inviteNewUserUrl => 'https://h5.haiyihy.com/invitation/invite-new-user/index.html'; // 正式 H5 页面 + String get inviteNewUserUrl => 'https://h5.haiyihy.com/invitation/invite-new-user/index.html'; // 正式 H5 页面 @override int get primaryColor => 0xffFF5722; // 不同主色(橙色) @override - String get tencentImAppid => '20036101'; + String get tencentImAppid => '20036101'; @override - String get agoraRtcAppid => '4b5e5cea3b86476caf7f7a57d05b82d1'; + String get agoraRtcAppid => '4b5e5cea3b86476caf7f7a57d05b82d1'; @override num get gameAppid => 9999999999; // 需要注册新的游戏服务账户并获取独立App ID @@ -86,10 +86,10 @@ class SCVariant1Config implements AppConfig { String get gameAppChannel => 'yumi'; @override - String get bigBroadcastGroup => '@TGS#2RUK4PK5C2'; + String get bigBroadcastGroup => '@TGS#2RUK4PK5C2'; @override - String get imAdmin => 'c2c_yumiadmin'; + String get imAdmin => 'c2c_yumiadmin'; @override bool get isReview => true; @@ -170,4 +170,4 @@ class SCVariant1Config implements AppConfig { debugPrint('应用配置验证通过'); } } -} +} diff --git a/lib/app_localizations.dart b/lib/app_localizations.dart index f038439..d554edb 100644 --- a/lib/app_localizations.dart +++ b/lib/app_localizations.dart @@ -1570,6 +1570,9 @@ class SCAppLocalizations { String rewardCoins(String name) => translate('rewardCoins').replaceAll('{1}', name); + String signInRewardReceived(String name) => + translate('signInRewardReceived').replaceAll('{1}', name); + String deleteAccount2(String name) => translate('deleteAccount2').replaceAll('{1}', name); diff --git a/lib/modules/gift/gift_page.dart b/lib/modules/gift/gift_page.dart index 7cafe62..e5e420b 100644 --- a/lib/modules/gift/gift_page.dart +++ b/lib/modules/gift/gift_page.dart @@ -186,7 +186,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { if (gift != null && (gift.giftSourceUrl ?? "").isNotEmpty && scGiftHasFullScreenEffect(gift.special)) { - SCGiftVapSvgaManager().preload(gift.giftSourceUrl!, highPriority: true); + SCGiftVapSvgaManager().preload(gift.giftSourceUrl!); _giftFxLog( 'preload selected gift ' 'giftId=${gift.id} ' @@ -1443,10 +1443,7 @@ class _GiftPageState extends State with TickerProviderStateMixin { debouncer.debounce( duration: Duration(milliseconds: 350), onDebounce: () { - Provider.of( - navigatorKey.currentState!.context, - listen: false, - ).retrieveMicrophoneList(); + rtcProvider?.requestGiftTriggeredMicRefresh(); }, ); } diff --git a/lib/modules/room/seat/sc_seat_item.dart b/lib/modules/room/seat/sc_seat_item.dart index c10f24a..0ee35b1 100644 --- a/lib/modules/room/seat/sc_seat_item.dart +++ b/lib/modules/room/seat/sc_seat_item.dart @@ -9,7 +9,6 @@ import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; -import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/shared/data_sources/models/enum/sc_room_special_mike_type.dart'; @@ -31,10 +30,7 @@ class _SCSeatItemState extends State with TickerProviderStateMixin { "sc_images/room/sc_icon_room_seat_heartbeat_value.png"; RtcProvider? provider; - JoinRoomRes? room; - MicRes? roomSeat; - JoinRoomRes? _cachedRoom; - MicRes? _cachedRoomSeat; + _SeatRenderSnapshot? _cachedSnapshot; final GlobalKey _targetKey = GlobalKey(); @override @@ -46,181 +42,314 @@ class _SCSeatItemState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - final liveRoomSeat = provider?.roomWheatMap[widget.index]; - final liveRoom = provider?.currenRoom; - if (provider?.isExitingCurrentVoiceRoomSession ?? false) { - roomSeat = liveRoomSeat ?? _cachedRoomSeat; - room = liveRoom ?? _cachedRoom; - } else { - roomSeat = liveRoomSeat; - room = liveRoom; - if (roomSeat != null) { - _cachedRoomSeat = roomSeat; - } - if (room != null) { - _cachedRoom = room; - } - } + return Selector( + selector: + (context, provider) => + _SeatRenderSnapshot.fromProvider(provider, widget.index), + builder: (context, liveSnapshot, child) { + final seatSnapshot = _resolveSeatSnapshot(liveSnapshot); + final resolvedHeaddress = + SCPathUtils.getFileExtension( + seatSnapshot.headdressSourceUrl, + ).toLowerCase() == + ".mp4" && + window.locale.languageCode == "ar" + ? "" + : seatSnapshot.headdressSourceUrl; - return GestureDetector( - behavior: HitTestBehavior.opaque, - child: Column( - children: [ - SizedBox( - key: _targetKey, - width: widget.isGameModel ? 40.w : 55.w, - height: widget.isGameModel ? 40.w : 55.w, - child: Stack( - alignment: Alignment.center, - children: [ - Sonic( - index: widget.index, - room: room, - isGameModel: widget.isGameModel, - ), - roomSeat?.user != null - ? head( - url: roomSeat?.user?.userAvatar ?? "", - width: widget.isGameModel ? 40.w : 55.w, - headdress: - SCPathUtils.getFileExtension( - roomSeat?.user - ?.getHeaddress() - ?.sourceUrl ?? - "", - ).toLowerCase() == - ".mp4" && - window.locale.languageCode == "ar" - ? "" - : roomSeat?.user?.getHeaddress()?.sourceUrl, - ) - : ((roomSeat?.micLock ?? false) - ? Image.asset( - "sc_images/room/sc_icon_seat_lock.png", - width: widget.isGameModel ? 38.w : 52.w, - height: widget.isGameModel ? 38.w : 52.w, + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + context.read().clickSite(widget.index); + }, + child: Column( + children: [ + SizedBox( + key: _targetKey, + width: widget.isGameModel ? 40.w : 55.w, + height: widget.isGameModel ? 40.w : 55.w, + child: Stack( + alignment: Alignment.center, + children: [ + Sonic( + index: widget.index, + specialMikeType: seatSnapshot.specialMikeType, + isGameModel: widget.isGameModel, + ), + seatSnapshot.hasUser + ? head( + url: seatSnapshot.userAvatar, + width: widget.isGameModel ? 40.w : 55.w, + headdress: resolvedHeaddress, ) - : Image.asset( - "sc_images/room/sc_icon_seat_open.png", - width: widget.isGameModel ? 38.w : 52.w, - height: widget.isGameModel ? 38.w : 52.w, - )), - Positioned( - bottom: widget.isGameModel ? 2.w : 5.w, - right: widget.isGameModel ? 2.w : 5.w, - child: - (roomSeat?.micMute ?? false) - ? Image.asset( - "sc_images/room/sc_icon_room_seat_mic_mute.png", - width: 14.w, - height: 14.w, - ) - : Container(), + : (seatSnapshot.micLock + ? Image.asset( + "sc_images/room/sc_icon_seat_lock.png", + width: widget.isGameModel ? 38.w : 52.w, + height: widget.isGameModel ? 38.w : 52.w, + ) + : Image.asset( + "sc_images/room/sc_icon_seat_open.png", + width: widget.isGameModel ? 38.w : 52.w, + height: widget.isGameModel ? 38.w : 52.w, + )), + Positioned( + bottom: widget.isGameModel ? 2.w : 5.w, + right: widget.isGameModel ? 2.w : 5.w, + child: + seatSnapshot.micMute + ? Image.asset( + "sc_images/room/sc_icon_room_seat_mic_mute.png", + width: 14.w, + height: 14.w, + ) + : Container(), + ), + IgnorePointer( + child: Emoticons( + index: widget.index, + isGameModel: widget.isGameModel, + ), + ), + ], ), - IgnorePointer( - child: Emoticons( - index: widget.index, - isGameModel: widget.isGameModel, - ), - ), - ], - ), - ), - widget.isGameModel - ? Container() - : (roomSeat?.user != null - ? SizedBox( - width: 64.w, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( + ), + widget.isGameModel + ? Container() + : (seatSnapshot.hasUser + ? SizedBox( + width: 64.w, + child: Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, children: [ - msgRoleTag( - roomSeat?.user?.roles ?? "", - width: 15.w, - height: 15.w, + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + msgRoleTag( + seatSnapshot.userRoles, + width: 15.w, + height: 15.w, + ), + Flexible( + child: socialchatNickNameText( + fontWeight: FontWeight.w600, + seatSnapshot.userNickname, + fontSize: 10.sp, + type: seatSnapshot.userVipName, + needScroll: + seatSnapshot + .userNickname + .characters + .length > + 8, + ), + ), + ], ), - Flexible( - child: socialchatNickNameText( - fontWeight: FontWeight.w600, - roomSeat?.user?.userNickname ?? "", - fontSize: 10.sp, - type: roomSeat?.user?.getVIP()?.name ?? "", - needScroll: - (roomSeat - ?.user - ?.userNickname - ?.characters - .length ?? - 0) > - 8, + SizedBox(height: 2.w), + SizedBox( + height: 10.w, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Image.asset( + _seatHeartbeatValueIconAsset, + width: 8.w, + height: 8.w, + fit: BoxFit.contain, + ), + SizedBox(width: 2.w), + text( + _heartbeatVaFormat( + seatSnapshot.userHeartbeatValue, + ), + fontWeight: FontWeight.w600, + fontSize: 8.sp, + lineHeight: 1, + ), + ], ), ), ], ), - SizedBox(height: 2.w), - SizedBox( - height: 10.w, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: [ - Image.asset( - _seatHeartbeatValueIconAsset, - width: 8.w, - height: 8.w, - fit: BoxFit.contain, - ), - SizedBox(width: 2.w), - text( - _heartbeatVaFormat(), - fontWeight: FontWeight.w600, - fontSize: 8.sp, - lineHeight: 1, - ), - ], + ) + : Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox(height: 16.w), + text( + "NO.${widget.index}", + fontSize: 10.sp, + fontWeight: FontWeight.w600, ), - ), - ], - ), - ) - : Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox(height: 16.w), - text( - "NO.${widget.index}", - fontSize: 10.sp, - fontWeight: FontWeight.w600, - ), - ], - )), - ], - ), - onTap: () { - Provider.of( - context, - listen: false, - ).clickSite(widget.index); + ], + )), + ], + ), + ); }, ); } - String _heartbeatVaFormat() { - int value = (roomSeat?.user?.heartbeatVal ?? 0).toInt(); - if (value >= 1000000) { - return "${(value / 1000000).toStringAsFixed(1)}M"; + _SeatRenderSnapshot _resolveSeatSnapshot(_SeatRenderSnapshot liveSnapshot) { + if (liveSnapshot.isExitingCurrentVoiceRoomSession) { + return _cachedSnapshot ?? liveSnapshot; } - if (value >= 10000) { - return "${(value / 1000).toStringAsFixed(0)}k"; - } - return "$value"; + _cachedSnapshot = liveSnapshot; + return liveSnapshot; } + + String _heartbeatVaFormat(num value) { + final resolvedValue = value.toInt(); + if (resolvedValue >= 1000000) { + return "${(resolvedValue / 1000000).toStringAsFixed(1)}M"; + } + if (resolvedValue >= 10000) { + return "${(resolvedValue / 1000).toStringAsFixed(0)}k"; + } + return "$resolvedValue"; + } +} + +class _SeatRenderSnapshot { + const _SeatRenderSnapshot({ + required this.isExitingCurrentVoiceRoomSession, + required this.specialMikeType, + required this.userId, + required this.userAvatar, + required this.headdressSourceUrl, + required this.userNickname, + required this.userRoles, + required this.userVipName, + required this.userHeartbeatValue, + required this.micLock, + required this.micMute, + }); + + factory _SeatRenderSnapshot.fromProvider(RtcProvider provider, num index) { + final roomSeat = provider.roomWheatMap[index]; + final user = roomSeat?.user; + return _SeatRenderSnapshot( + isExitingCurrentVoiceRoomSession: + provider.isExitingCurrentVoiceRoomSession, + specialMikeType: + provider.currenRoom?.roomProfile?.roomSetting?.roomSpecialMikeType ?? + "", + userId: user?.id ?? "", + userAvatar: user?.userAvatar ?? "", + headdressSourceUrl: user?.getHeaddress()?.sourceUrl ?? "", + userNickname: user?.userNickname ?? "", + userRoles: user?.roles ?? "", + userVipName: user?.getVIP()?.name ?? "", + userHeartbeatValue: user?.heartbeatVal ?? 0, + micLock: roomSeat?.micLock ?? false, + micMute: roomSeat?.micMute ?? false, + ); + } + + final bool isExitingCurrentVoiceRoomSession; + final String specialMikeType; + final String userId; + final String userAvatar; + final String headdressSourceUrl; + final String userNickname; + final String userRoles; + final String userVipName; + final num userHeartbeatValue; + final bool micLock; + final bool micMute; + + bool get hasUser => userId.isNotEmpty; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is _SeatRenderSnapshot && + other.isExitingCurrentVoiceRoomSession == + isExitingCurrentVoiceRoomSession && + other.specialMikeType == specialMikeType && + other.userId == userId && + other.userAvatar == userAvatar && + other.headdressSourceUrl == headdressSourceUrl && + other.userNickname == userNickname && + other.userRoles == userRoles && + other.userVipName == userVipName && + other.userHeartbeatValue == userHeartbeatValue && + other.micLock == micLock && + other.micMute == micMute; + } + + @override + int get hashCode => Object.hash( + isExitingCurrentVoiceRoomSession, + specialMikeType, + userId, + userAvatar, + headdressSourceUrl, + userNickname, + userRoles, + userVipName, + userHeartbeatValue, + micLock, + micMute, + ); +} + +class _SeatEmojiSnapshot { + const _SeatEmojiSnapshot({ + required this.userId, + required this.emojiPath, + required this.type, + required this.number, + }); + + factory _SeatEmojiSnapshot.fromMic(MicRes? mic) { + final emojiPath = mic?.emojiPath ?? ""; + return _SeatEmojiSnapshot( + userId: mic?.user?.id ?? "", + emojiPath: emojiPath.isEmpty ? null : emojiPath, + type: mic?.type ?? "", + number: mic?.number ?? "", + ); + } + + final String userId; + final String? emojiPath; + final String type; + final String number; + + bool get hasUser => userId.isNotEmpty; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is _SeatEmojiSnapshot && + other.userId == userId && + other.emojiPath == emojiPath && + other.type == type && + other.number == number; + } + + @override + int get hashCode => Object.hash(userId, emojiPath, type, number); +} + +class _SeatEmojiEvent { + const _SeatEmojiEvent({required this.type, required this.number}); + + final String type; + final String number; + + @override + String toString() => "type=$type number=$number"; } class Emoticons extends StatefulWidget { @@ -239,9 +368,9 @@ class _EmoticonsState extends State with TickerProviderStateMixin { late CurvedAnimation curvedAnimation; bool showIn = false; - MicRes? playingRes; + _SeatEmojiEvent? playingEvent; - List pathList = []; + List<_SeatEmojiEvent> pathList = []; String? giftPath; List giftList = []; bool showResult = false; @@ -267,23 +396,31 @@ class _EmoticonsState extends State with TickerProviderStateMixin { @override Widget build(BuildContext context) { - return Consumer( - builder: (BuildContext context, RtcProvider provider, Widget? child) { - MicRes? micRes = provider.roomWheatMap[widget.index]; - if (micRes?.user == null) { + return Selector( + selector: + (context, provider) => + _SeatEmojiSnapshot.fromMic(provider.roomWheatMap[widget.index]), + builder: ( + BuildContext context, + _SeatEmojiSnapshot snapshot, + Widget? child, + ) { + if (!snapshot.hasUser) { return Container(); } - String? emojiPath = provider.roomWheatMap[widget.index]?.emojiPath; - if (emojiPath != null) { - pathList.add(micRes); - micRes?.setEmojiPath = null; + if (snapshot.emojiPath != null) { + pathList.add( + _SeatEmojiEvent(type: snapshot.type, number: snapshot.number), + ); + context.read().roomWheatMap[widget.index]?.setEmojiPath = + null; _checkStart(); } - if (playingRes != null) { + if (playingEvent != null) { return FadeTransition( opacity: curvedAnimation, child: - playingRes?.type == SCRoomMsgType.roomDice + playingEvent?.type == SCRoomMsgType.roomDice ? FutureBuilder( future: Future.delayed(Duration(milliseconds: 2000)), // 传入Future @@ -294,7 +431,7 @@ class _EmoticonsState extends State with TickerProviderStateMixin { // 根据snapshot的状态来构建UI if (snapshot.connectionState == ConnectionState.done) { return Image.asset( - "sc_images/room/sc_icon_dice_${micRes?.number}.png", + "sc_images/room/sc_icon_dice_${playingEvent?.number}.png", height: widget.isGameModel ? 38.w : 45.w, ); } else { @@ -305,7 +442,7 @@ class _EmoticonsState extends State with TickerProviderStateMixin { } }, ) - : (playingRes?.type == SCRoomMsgType.roomRPS + : (playingEvent?.type == SCRoomMsgType.roomRPS ? FutureBuilder( future: Future.delayed(Duration(milliseconds: 2000)), // 传入Future @@ -317,7 +454,7 @@ class _EmoticonsState extends State with TickerProviderStateMixin { if (snapshot.connectionState == ConnectionState.done) { return Image.asset( - "sc_images/room/sc_icon_rps_${micRes?.number}.png", + "sc_images/room/sc_icon_rps_${playingEvent?.number}.png", height: widget.isGameModel ? 38.w : 45.w, ); } else { @@ -329,7 +466,7 @@ class _EmoticonsState extends State with TickerProviderStateMixin { }, ) : netImage( - url: playingRes?.number ?? "", + url: playingEvent?.number ?? "", width: widget.isGameModel ? 65.w : 75.w, )), ); @@ -349,13 +486,10 @@ class _EmoticonsState extends State with TickerProviderStateMixin { } void _checkStart() { - print('emoticonsAniCtr.status:${emoticonsAniCtr.status}'); - print('emoticonsAniCtr.path:${playingRes}'); if (!showIn) { if (pathList.isNotEmpty) { - playingRes = pathList.first; + playingEvent = pathList.first; pathList.removeAt(0); - print('播放表情:$playingRes'); emoticonsAniCtr.forward(); Future.delayed(Duration(milliseconds: 5)).then((value) { setState(() { @@ -367,13 +501,12 @@ class _EmoticonsState extends State with TickerProviderStateMixin { Future.delayed(Duration(seconds: 3)).then((value) { emoticonsAniCtr.reverse(); showIn = false; - playingRes = null; + playingEvent = null; _checkStart(); }); } else if (giftList.isNotEmpty) { giftPath = giftList.first; giftList.removeAt(0); - print('播放礼物:$playingRes'); emoticonsAniCtr.forward(); Future.delayed(Duration(milliseconds: 10)).then((value) { setState(() { @@ -396,12 +529,12 @@ class _EmoticonsState extends State with TickerProviderStateMixin { class Sonic extends StatefulWidget { final num index; final bool isGameModel; - final JoinRoomRes? room; + final String specialMikeType; const Sonic({ Key? key, required this.index, - required this.room, + required this.specialMikeType, this.isGameModel = false, }) : super(key: key); @@ -450,23 +583,33 @@ class _SonicState extends State with TickerProviderStateMixin { return Stack( clipBehavior: Clip.none, children: [ - SonicItem(ctrList[0], widget.room, isGameModel: widget.isGameModel), - SonicItem(ctrList[1], widget.room, isGameModel: widget.isGameModel), - SonicItem(ctrList[2], widget.room, isGameModel: widget.isGameModel), - SonicItem(ctrList[3], widget.room, isGameModel: widget.isGameModel), + SonicItem( + ctrList[0], + widget.specialMikeType, + isGameModel: widget.isGameModel, + ), + SonicItem( + ctrList[1], + widget.specialMikeType, + isGameModel: widget.isGameModel, + ), + SonicItem( + ctrList[2], + widget.specialMikeType, + isGameModel: widget.isGameModel, + ), + SonicItem( + ctrList[3], + widget.specialMikeType, + isGameModel: widget.isGameModel, + ), ], ); } void _checkSoundAni(int volume) async { if (volume <= 20) return; - // await Future.delayed(Duration(milliseconds: 10)); - // var dateTime = DateTime.now(); - // for(int i =0 ; i < 5000000;i++){ - // lastIndex = 1; - // } - // print('执行耗时:${DateTime.now().millisecondsSinceEpoch - dateTime.millisecondsSinceEpoch}'); - ctrList.forEach((element) { + for (final element in ctrList) { if (element.value == 0) { if (lastIndex == 0) { element.forward(); @@ -481,11 +624,10 @@ class _SonicState extends State with TickerProviderStateMixin { } } } - }); + } } _onVoiceChange(num index, int volum) { - print('说话声音:$volum'); if (widget.index == index) { _checkSoundAni(volum); } @@ -494,11 +636,15 @@ class _SonicState extends State with TickerProviderStateMixin { class SonicItem extends AnimatedWidget { final Animation animation; - final JoinRoomRes? room; + final String specialMikeType; final bool isGameModel; - SonicItem(this.animation, this.room, {super.key, this.isGameModel = false}) - : super(listenable: animation); + SonicItem( + this.animation, + this.specialMikeType, { + super.key, + this.isGameModel = false, + }) : super(listenable: animation); @override Widget build(BuildContext context) { @@ -510,15 +656,14 @@ class SonicItem extends AnimatedWidget { height: width(isGameModel ? 34 : 52), width: width(isGameModel ? 34 : 52), decoration: - (room?.roomProfile?.roomSetting?.roomSpecialMikeType ?? "") - .isEmpty || - (room?.roomProfile?.roomSetting?.roomSpecialMikeType != + specialMikeType.isEmpty || + (specialMikeType != SCRoomSpecialMikeType.TYPE_VIP3.name && - room?.roomProfile?.roomSetting?.roomSpecialMikeType != + specialMikeType != SCRoomSpecialMikeType.TYPE_VIP4.name && - room?.roomProfile?.roomSetting?.roomSpecialMikeType != + specialMikeType != SCRoomSpecialMikeType.TYPE_VIP5.name && - room?.roomProfile?.roomSetting?.roomSpecialMikeType != + specialMikeType != SCRoomSpecialMikeType.TYPE_VIP6.name) ? BoxDecoration( shape: BoxShape.circle, @@ -528,25 +673,19 @@ class SonicItem extends AnimatedWidget { ) : BoxDecoration(), child: - room?.roomProfile?.roomSetting?.roomSpecialMikeType == - SCRoomSpecialMikeType.TYPE_VIP3.name + specialMikeType == SCRoomSpecialMikeType.TYPE_VIP3.name ? Image.asset( "sc_images/room/sc_icon_room_vip3_sonic_anim.webp", ) - : (room?.roomProfile?.roomSetting?.roomSpecialMikeType == - SCRoomSpecialMikeType.TYPE_VIP4.name + : (specialMikeType == SCRoomSpecialMikeType.TYPE_VIP4.name ? Image.asset( "sc_images/room/sc_icon_room_vip4_sonic_anim.webp", ) - : (room?.roomProfile?.roomSetting?.roomSpecialMikeType == - SCRoomSpecialMikeType.TYPE_VIP5.name + : (specialMikeType == SCRoomSpecialMikeType.TYPE_VIP5.name ? Image.asset( "sc_images/room/sc_icon_room_vip5_sonic_anim.webp", ) - : (room - ?.roomProfile - ?.roomSetting - ?.roomSpecialMikeType == + : (specialMikeType == SCRoomSpecialMikeType.TYPE_VIP6.name ? Image.asset( "sc_images/room/sc_icon_room_vip6_sonic_anim.webp", diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index 3d98a86..dbb4dc6 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -141,17 +141,16 @@ class _VoiceRoomPageState extends State SCGiftVapSvgaManager().stopPlayback(); } - bool roomThemeBackActi(JoinRoomRes? room) { - if (room?.roomProps?.roomTheme != null) { - if (room?.roomProps?.roomTheme?.themeBack != null && - room!.roomProps!.roomTheme!.themeBack!.isNotEmpty) { - if ((room.roomProps?.roomTheme?.expireTime ?? 0) > - DateTime.now().millisecondsSinceEpoch) { - return true; - } - } + String? _resolveRoomThemeBackground(JoinRoomRes? room) { + final roomTheme = room?.roomProps?.roomTheme; + final themeBack = roomTheme?.themeBack ?? ""; + if (themeBack.isEmpty) { + return null; } - return false; + if ((roomTheme?.expireTime ?? 0) <= DateTime.now().millisecondsSinceEpoch) { + return null; + } + return themeBack; } @override @@ -173,13 +172,14 @@ class _VoiceRoomPageState extends State top: false, child: Stack( children: [ - Consumer( - builder: (context, ref, child) { - return roomThemeBackActi(ref.currenRoom) + Selector( + selector: + (context, provider) => + _resolveRoomThemeBackground(provider.currenRoom), + builder: (context, roomThemeBackground, child) { + return roomThemeBackground != null ? netImage( - url: - ref.currenRoom?.roomProps?.roomTheme?.themeBack ?? - "", + url: roomThemeBackground, width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, noDefaultImg: true, @@ -395,6 +395,7 @@ class _VoiceRoomPageState extends State giftModel.sendUserPic = msg.user?.userAvatar ?? ""; giftModel.giftPic = msg.gift?.giftPhoto ?? ""; giftModel.giftCount = 0; + giftModel.rewardAmount = awardAmount; giftModel.showLuckyRewardFrame = true; giftModel.rewardAmountText = _formatLuckyRewardAmount(awardAmount); Provider.of( diff --git a/lib/modules/room_game/bridge/baishun_js_bridge.dart b/lib/modules/room_game/bridge/baishun_js_bridge.dart index 12fa2a7..ad15a0f 100644 --- a/lib/modules/room_game/bridge/baishun_js_bridge.dart +++ b/lib/modules/room_game/bridge/baishun_js_bridge.dart @@ -133,6 +133,16 @@ class BaishunJsBridge { } })); } + function maskText(value) { + const text = (value == null ? '' : String(value)).trim(); + if (!text) { + return ''; + } + if (text.length <= 10) { + return text; + } + return text.slice(0, 6) + '***' + text.slice(-4); + } function ensureChannelAlias(name, handler) { if (!window[name] || typeof window[name].postMessage !== 'function') { window[name] = { postMessage: handler }; @@ -146,19 +156,27 @@ class BaishunJsBridge { } } window.NativeBridge.getConfigSync = function() { + postDebug('bridge.getConfigSync', { + hasConfig: !!window.__baishunLastConfig, + hasNativeBridgeConfig: !!(window.NativeBridge && window.NativeBridge.config) + }); return window.__baishunLastConfig || null; }; window.NativeBridge.getConfig = function(params) { + postDebug('bridge.getConfig.call', toPayload(params)); postAction('${BaishunBridgeActions.getConfig}', params); return window.__baishunLastConfig || null; }; window.NativeBridge.destroy = function(payload) { + postDebug('bridge.destroy.call', toPayload(payload)); postAction('${BaishunBridgeActions.destroy}', payload); }; window.NativeBridge.gameRecharge = function(payload) { + postDebug('bridge.gameRecharge.call', toPayload(payload)); postAction('${BaishunBridgeActions.gameRecharge}', payload); }; window.NativeBridge.gameLoaded = function(payload) { + postDebug('bridge.gameLoaded.call', toPayload(payload)); postAction('${BaishunBridgeActions.gameLoaded}', payload); }; window.__baishunDebugLog = postDebug; @@ -359,6 +377,10 @@ class BaishunJsBridge { if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') { window.dispatchEvent(new CustomEvent('baishunBridgeReady')); } + postDebug('bridge.ready', { + href: window.location && window.location.href, + ua: navigator && navigator.userAgent + }); })(); '''; } @@ -373,6 +395,16 @@ class BaishunJsBridge { (function() { const config = $payload; const explicitCallbackPath = $encodedCallbackPath; + function maskText(value) { + const text = (value == null ? '' : String(value)).trim(); + if (!text) { + return ''; + } + if (text.length <= 10) { + return text; + } + return text.slice(0, 6) + '***' + text.slice(-4); + } function resolvePath(path) { if (!path || typeof path !== 'string') { return null; @@ -407,15 +439,39 @@ class BaishunJsBridge { window.baishunBridgeConfig = config; window.NativeBridge = window.NativeBridge || {}; window.NativeBridge.config = config; + const callbackPath = explicitCallbackPath || window.__baishunLastJsCallback || ''; + let callbackInvoked = false; if (explicitCallbackPath) { window.__baishunLastJsCallback = explicitCallbackPath; } + if (typeof window.__baishunDebugLog === 'function') { + window.__baishunDebugLog('config.deliver', { + appId: config.appId, + appChannel: config.appChannel, + userId: config.userId, + roomId: config.roomId, + gameMode: config.gameMode, + language: config.language, + gsp: config.gsp, + code: maskText(config.code), + codeLength: config.code ? String(config.code).length : 0, + callbackPath: callbackPath + }); + } if (typeof window.__baishunGetConfigCallback === 'function') { window.__baishunGetConfigCallback(config); window.__baishunGetConfigCallback = null; } if (window.__baishunLastJsCallback) { - invokeCallbackPath(window.__baishunLastJsCallback); + callbackInvoked = invokeCallbackPath(window.__baishunLastJsCallback) || callbackInvoked; + } + if (typeof window.__baishunDebugLog === 'function') { + window.__baishunDebugLog('config.callback', { + callbackPath: callbackPath, + callbackInvoked: callbackInvoked, + hasLegacyCallback: typeof window.__baishunGetConfigCallback === 'function', + hasOnBaishunConfig: typeof window.onBaishunConfig === 'function' + }); } if (typeof window.onBaishunConfig === 'function') { window.onBaishunConfig(config); @@ -438,6 +494,9 @@ class BaishunJsBridge { return ''' (function() { const payload = $safePayload; + if (typeof window.__baishunDebugLog === 'function') { + window.__baishunDebugLog('wallet.update', payload); + } if (window.baishunChannel && typeof window.baishunChannel.walletUpdate === 'function') { window.baishunChannel.walletUpdate(payload); } diff --git a/lib/modules/room_game/views/baishun_game_page.dart b/lib/modules/room_game/views/baishun_game_page.dart index 4d1c06c..9defbcc 100644 --- a/lib/modules/room_game/views/baishun_game_page.dart +++ b/lib/modules/room_game/views/baishun_game_page.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; @@ -31,6 +32,8 @@ class BaishunGamePage extends StatefulWidget { } class _BaishunGamePageState extends State { + static const String _logPrefix = '[BaishunGame]'; + final RoomGameRepository _repository = RoomGameRepository(); final Set> _webGestureRecognizers = >{ @@ -45,6 +48,7 @@ class _BaishunGamePageState extends State { bool _didReceiveBridgeMessage = false; bool _didFinishPageLoad = false; bool _hasDeliveredLaunchConfig = false; + int _bridgeInjectCount = 0; String? _errorMessage; @override @@ -92,15 +96,21 @@ class _BaishunGamePageState extends State { ) ..setNavigationDelegate( NavigationDelegate( - onPageStarted: (String _) { + onPageStarted: (String url) { + _log('page_started url=${_clip(url, 240)}'); _prepareForPageLoad(); }, - onPageFinished: (String _) async { + onPageFinished: (String url) async { _didFinishPageLoad = true; + _log('page_finished url=${_clip(url, 240)}'); await _injectBridge(reason: 'page_finished'); }, onWebResourceError: (WebResourceError error) { - _stopBridgeBootstrap(); + _log( + 'web_resource_error code=${error.errorCode} ' + 'type=${error.errorType} desc=${_clip(error.description, 300)}', + ); + _stopBridgeBootstrap(reason: 'web_resource_error'); if (!mounted) { return; } @@ -111,36 +121,56 @@ class _BaishunGamePageState extends State { }, ), ); + _log('init launch=${_stringifyForLog(_buildLaunchSummary())}'); unawaited(_loadGameEntry()); } @override void dispose() { - _stopBridgeBootstrap(); + _log('dispose isClosing=$_isClosing'); + _stopBridgeBootstrap(reason: 'dispose'); super.dispose(); } void _prepareForPageLoad() { + _log( + 'prepare_page_load session=${widget.launchModel.gameSessionId} ' + 'entry=${_clip(widget.launchModel.entry.entryUrl, 240)}', + ); _didReceiveBridgeMessage = false; _didFinishPageLoad = false; _hasDeliveredLaunchConfig = false; - _stopBridgeBootstrap(); + _bridgeInjectCount = 0; + _stopBridgeBootstrap(reason: 'prepare_page_load'); _bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), ( Timer timer, ) { if (!mounted || _didReceiveBridgeMessage || _errorMessage != null) { + _log( + 'bootstrap_timer_stop tick=${timer.tick} ' + 'didReceiveBridge=$_didReceiveBridgeMessage ' + 'hasError=${_errorMessage != null}', + ); timer.cancel(); return; } unawaited(_injectBridge(reason: 'bootstrap')); if (_didFinishPageLoad && timer.tick >= 12) { + _log( + 'bootstrap_timer_stop tick=${timer.tick} reason=page_finished_guard', + ); timer.cancel(); } }); _loadingFallbackTimer = Timer(const Duration(seconds: 6), () { if (!mounted || _didReceiveBridgeMessage || _errorMessage != null) { + _log( + 'loading_fallback_skip didReceiveBridge=$_didReceiveBridgeMessage ' + 'hasError=${_errorMessage != null}', + ); return; } + _log('loading_fallback_fire isLoading=$_isLoading'); setState(() { _isLoading = false; }); @@ -154,7 +184,10 @@ class _BaishunGamePageState extends State { }); } - void _stopBridgeBootstrap() { + void _stopBridgeBootstrap({String reason = 'manual'}) { + if (_bridgeBootstrapTimer != null || _loadingFallbackTimer != null) { + _log('stop_bootstrap reason=$reason'); + } _bridgeBootstrapTimer?.cancel(); _bridgeBootstrapTimer = null; _loadingFallbackTimer?.cancel(); @@ -165,10 +198,15 @@ class _BaishunGamePageState extends State { try { _prepareForPageLoad(); final entryUrl = widget.launchModel.entry.entryUrl.trim(); + _log( + 'load_entry launchMode=${widget.launchModel.entry.launchMode} ' + 'entryUrl=${_clip(entryUrl, 240)}', + ); if (entryUrl.isEmpty || entryUrl.startsWith('mock://')) { final html = await rootBundle.loadString( 'assets/debug/baishun_mock/index.html', ); + _log('load_mock_html baseUrl=https://baishun.mock.local/'); await _controller.loadHtmlString( html, baseUrl: 'https://baishun.mock.local/', @@ -180,8 +218,10 @@ class _BaishunGamePageState extends State { if (uri == null) { throw Exception('Invalid game entry url: $entryUrl'); } + _log('load_request uri=${_clip(uri.toString(), 240)}'); await _controller.loadRequest(uri); } catch (error) { + _log('load_entry_error error=${_clip(error.toString(), 400)}'); if (!mounted) { return; } @@ -193,9 +233,19 @@ class _BaishunGamePageState extends State { } Future _injectBridge({String reason = 'manual'}) async { + _bridgeInjectCount += 1; + if (reason != 'bootstrap' || + _bridgeInjectCount <= 3 || + _bridgeInjectCount % 5 == 0) { + _log('inject_bridge reason=$reason count=$_bridgeInjectCount'); + } try { await _controller.runJavaScript(BaishunJsBridge.bootstrapScript()); - } catch (_) {} + } catch (error) { + _log( + 'inject_bridge_error reason=$reason error=${_clip(error.toString(), 300)}', + ); + } } Future _sendConfigToGame({ @@ -203,11 +253,16 @@ class _BaishunGamePageState extends State { bool force = false, }) async { if (_hasDeliveredLaunchConfig && !force) { + _log('skip_send_config reason=already_delivered'); return; } _hasDeliveredLaunchConfig = true; final rawConfig = widget.launchModel.bridgeConfig; final config = _buildEffectiveBridgeConfig(rawConfig); + _log( + 'send_config jsCallback=${jsCallbackPath ?? ''} ' + 'force=$force config=${_stringifyForLog(_buildConfigSummary(config, rawConfig: rawConfig))}', + ); try { await _controller.runJavaScript( BaishunJsBridge.buildConfigScript( @@ -215,10 +270,13 @@ class _BaishunGamePageState extends State { jsCallbackPath: jsCallbackPath, ), ); - } catch (_) {} + } catch (error) { + _log('send_config_error error=${_clip(error.toString(), 300)}'); + } } Future _handleBridgeMessage(JavaScriptMessage message) async { + _log('channel_message raw=${_clip(message.message, 600)}'); final bridgeMessage = BaishunBridgeMessage.parse(message.message); await _dispatchBridgeMessage(bridgeMessage); } @@ -231,6 +289,7 @@ class _BaishunGamePageState extends State { action, message.message, ); + _log('named_channel action=$action raw=${_clip(message.message, 600)}'); await _dispatchBridgeMessage(bridgeMessage); } @@ -238,12 +297,20 @@ class _BaishunGamePageState extends State { BaishunBridgeMessage bridgeMessage, ) async { if (bridgeMessage.action == BaishunBridgeActions.debugLog) { + final tag = bridgeMessage.payload['tag']?.toString().trim(); + final message = bridgeMessage.payload['message']?.toString() ?? ''; + _log('h5_debug tag=${tag ?? 'unknown'} message=${_clip(message, 800)}'); return; } final callbackPath = _extractCallbackPath(bridgeMessage.payload); + _log( + 'bridge_action action=${bridgeMessage.action} ' + 'callback=${callbackPath.isEmpty ? '-' : callbackPath} ' + 'payload=${_stringifyForLog(_sanitizeForLog(bridgeMessage.payload))}', + ); if (bridgeMessage.action.isNotEmpty) { _didReceiveBridgeMessage = true; - _stopBridgeBootstrap(); + _stopBridgeBootstrap(reason: 'bridge_message_${bridgeMessage.action}'); } switch (bridgeMessage.action) { case BaishunBridgeActions.getConfig: @@ -255,35 +322,43 @@ class _BaishunGamePageState extends State { if (!mounted) { return; } + _log('game_loaded received'); setState(() { _isLoading = false; _errorMessage = null; }); break; case BaishunBridgeActions.gameRecharge: + _log('game_recharge open_wallet'); await SCNavigatorUtils.push(context, WalletRoute.recharge); await _notifyWalletUpdate(); break; case BaishunBridgeActions.destroy: + _log('destroy requested by h5'); await _closeAndExit(reason: 'h5_destroy'); break; default: + _log('bridge_action_unhandled action=${bridgeMessage.action}'); break; } } Future _notifyWalletUpdate() async { + _log('notify_wallet_update'); try { await _controller.runJavaScript( BaishunJsBridge.buildWalletUpdateScript(), ); - } catch (_) {} + } catch (error) { + _log('notify_wallet_update_error error=${_clip(error.toString(), 300)}'); + } } Future _reload() async { if (!mounted) { return; } + _log('reload'); setState(() { _errorMessage = null; _isLoading = true; @@ -293,19 +368,27 @@ class _BaishunGamePageState extends State { Future _closeAndExit({String reason = 'page_back'}) async { if (_isClosing) { + _log('close_skip reason=already_closing source=$reason'); return; } _isClosing = true; + _log('close_start reason=$reason'); try { if (!widget.launchModel.gameSessionId.startsWith('bs_mock_')) { - await _repository.closeBaishunGame( + final result = await _repository.closeBaishunGame( roomId: widget.roomId, gameSessionId: widget.launchModel.gameSessionId, reason: reason, ); + _log( + 'close_success result=${_stringifyForLog({'roomId': result.roomId, 'state': result.state, 'gameSessionId': result.gameSessionId, 'currentGameId': result.currentGameId, 'hostUserId': result.hostUserId})}', + ); } - } catch (_) {} + } catch (error) { + _log('close_error error=${_clip(error.toString(), 300)}'); + } if (mounted) { + _log('close_pop'); Navigator.of(context).pop(); } } @@ -324,6 +407,110 @@ class _BaishunGamePageState extends State { return ''; } + void _log(String message) { + debugPrint('$_logPrefix $message'); + } + + String _clip(String value, [int limit = 600]) { + final trimmed = value.trim(); + if (trimmed.length <= limit) { + return trimmed; + } + return '${trimmed.substring(0, limit)}...'; + } + + String _maskValue(String value, {int keepStart = 6, int keepEnd = 4}) { + final trimmed = value.trim(); + if (trimmed.isEmpty) { + return ''; + } + if (trimmed.length <= keepStart + keepEnd) { + return trimmed; + } + return '${trimmed.substring(0, keepStart)}***${trimmed.substring(trimmed.length - keepEnd)}'; + } + + Object? _sanitizeForLog(Object? value, {String key = ''}) { + final lowerKey = key.toLowerCase(); + final shouldMask = + lowerKey.contains('code') || + lowerKey.contains('token') || + lowerKey.contains('authorization'); + if (value is Map) { + final result = {}; + for (final MapEntry entry in value.entries) { + result[entry.key.toString()] = _sanitizeForLog( + entry.value, + key: entry.key.toString(), + ); + } + return result; + } + if (value is List) { + return value.map((Object? item) => _sanitizeForLog(item)).toList(); + } + if (value is String) { + final normalized = shouldMask ? _maskValue(value) : value; + return _clip(normalized, 600); + } + return value; + } + + String _stringifyForLog(Object? value) { + if (value == null) { + return ''; + } + try { + return _clip(jsonEncode(value), 800); + } catch (_) { + return _clip(value.toString(), 800); + } + } + + Map _buildLaunchSummary() { + final entry = widget.launchModel.entry; + final roomState = widget.launchModel.roomState; + return { + 'roomId': widget.roomId, + 'gameId': widget.game.gameId, + 'gameName': widget.game.name, + 'vendorType': widget.launchModel.vendorType, + 'vendorGameId': widget.launchModel.vendorGameId, + 'gameSessionId': widget.launchModel.gameSessionId, + 'launchMode': entry.launchMode, + 'entryUrl': _clip(entry.entryUrl, 240), + 'previewUrl': _clip(entry.previewUrl, 240), + 'packageVersion': entry.packageVersion, + 'orientation': entry.orientation, + 'safeHeight': entry.safeHeight, + 'roomState': roomState.state, + 'currentGameId': roomState.currentGameId, + 'hostUserId': roomState.hostUserId, + 'bridgeConfig': _buildConfigSummary(widget.launchModel.bridgeConfig), + }; + } + + Map _buildConfigSummary( + BaishunBridgeConfigModel config, { + BaishunBridgeConfigModel? rawConfig, + }) { + return { + 'appName': config.appName, + 'appChannel': config.appChannel, + 'appId': config.appId, + 'userId': config.userId, + 'roomId': config.roomId, + 'gameMode': config.gameMode, + 'language': config.language, + 'rawLanguage': rawConfig?.language ?? config.language, + 'gsp': config.gsp, + 'code': _maskValue(config.code), + 'codeLength': config.code.length, + 'sceneMode': config.gameConfig.sceneMode, + 'currencyIcon': config.gameConfig.currencyIcon, + }; + } + BaishunBridgeConfigModel _buildEffectiveBridgeConfig( BaishunBridgeConfigModel rawConfig, ) { diff --git a/lib/modules/room_game/views/room_game_list_sheet.dart b/lib/modules/room_game/views/room_game_list_sheet.dart index 91d87ac..621186c 100644 --- a/lib/modules/room_game/views/room_game_list_sheet.dart +++ b/lib/modules/room_game/views/room_game_list_sheet.dart @@ -22,6 +22,7 @@ class RoomGameListSheet extends StatefulWidget { } class _RoomGameListSheetState extends State { + static const String _logPrefix = '[BaishunLaunch]'; static const int _itemsPerRow = 4; static const String _sheetFrameAsset = 'sc_images/room/sc_room_game_sheet_frame.png'; @@ -78,11 +79,31 @@ class _RoomGameListSheetState extends State { }); try { + _log( + 'launch_request roomId=$_roomId gameId=${game.gameId} ' + 'vendorGameId=${game.vendorGameId} gameMode=${game.gameMode} ' + 'origin=${Platform.isAndroid ? 'ANDROID' : 'IOS'}', + ); final launchModel = await _repository.launchBaishunGame( roomId: _roomId, gameId: game.gameId, clientOrigin: Platform.isAndroid ? 'ANDROID' : 'IOS', ); + _log( + 'launch_success payload=${_stringifyForLog({ + 'gameSessionId': launchModel.gameSessionId, + 'vendorType': launchModel.vendorType, + 'gameId': launchModel.gameId, + 'vendorGameId': launchModel.vendorGameId, + 'entryUrl': _clip(launchModel.entry.entryUrl, 240), + 'launchMode': launchModel.entry.launchMode, + 'packageVersion': launchModel.entry.packageVersion, + 'orientation': launchModel.entry.orientation, + 'safeHeight': launchModel.entry.safeHeight, + 'roomState': launchModel.roomState.state, + 'bridgeConfig': {'userId': launchModel.bridgeConfig.userId, 'roomId': launchModel.bridgeConfig.roomId, 'gameMode': launchModel.bridgeConfig.gameMode, 'language': launchModel.bridgeConfig.language, 'gsp': launchModel.bridgeConfig.gsp, 'code': _maskValue(launchModel.bridgeConfig.code), 'codeLength': launchModel.bridgeConfig.code.length}, + })}', + ); if (!mounted) { return; } @@ -99,6 +120,7 @@ class _RoomGameListSheetState extends State { barrierDismissible: true, ); } catch (error) { + _log('launch_error error=${_clip(error.toString(), 400)}'); SCTts.show('Launch failed'); } finally { if (mounted) { @@ -109,6 +131,36 @@ class _RoomGameListSheetState extends State { } } + void _log(String message) { + debugPrint('$_logPrefix $message'); + } + + String _clip(String value, [int limit = 600]) { + final trimmed = value.trim(); + if (trimmed.length <= limit) { + return trimmed; + } + return '${trimmed.substring(0, limit)}...'; + } + + String _maskValue(String value, {int keepStart = 6, int keepEnd = 4}) { + final trimmed = value.trim(); + if (trimmed.isEmpty) { + return ''; + } + if (trimmed.length <= keepStart + keepEnd) { + return trimmed; + } + return '${trimmed.substring(0, keepStart)}***${trimmed.substring(trimmed.length - keepEnd)}'; + } + + String _stringifyForLog(Object? value) { + if (value == null) { + return ''; + } + return _clip(value.toString(), 800); + } + @override Widget build(BuildContext context) { return SafeArea( diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index fcd2e93..0b7683d 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -3,51 +3,55 @@ import 'dart:async'; import 'package:agora_rtc_engine/agora_rtc_engine.dart'; import 'package:fluro/fluro.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/app/constants/sc_room_msg_type.dart'; -import 'package:yumi/shared/tools/sc_permission_utils.dart'; -import 'package:yumi/shared/tools/sc_room_utils.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/services/room/rc_room_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/room/room_msg_item.dart'; -import 'package:provider/provider.dart'; -import 'package:yumi/ui_kit/components/sc_tts.dart'; -import 'package:yumi/app/constants/sc_global_config.dart'; -import 'package:yumi/app/routes/sc_routes.dart'; -import 'package:yumi/app/routes/sc_fluro_navigator.dart'; -import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; -import 'package:yumi/shared/tools/sc_loading_manager.dart'; -import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; -import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; -import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; -import 'package:yumi/shared/business_logic/models/res/login_res.dart'; -import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; -import 'package:yumi/shared/business_logic/models/res/room_res.dart'; -import 'package:yumi/modules/room/voice_room_route.dart'; -import 'package:yumi/ui_kit/widgets/room/empty_mai_select.dart'; -import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart'; -import '../../shared/tools/sc_heartbeat_utils.dart'; -import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart'; -import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart'; -import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; -import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart'; -import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart'; -import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart'; -import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart'; -import '../../shared/tools/sc_room_profile_cache.dart'; -import '../../ui_kit/components/sc_float_ichart.dart'; - -typedef OnSoundVoiceChange = Function(num index, int volum); -typedef RtcProvider = RealTimeCommunicationManager; - +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/app/constants/sc_room_msg_type.dart'; +import 'package:yumi/shared/tools/sc_permission_utils.dart'; +import 'package:yumi/shared/tools/sc_room_utils.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/services/room/rc_room_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/room/room_msg_item.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/constants/sc_global_config.dart'; +import 'package:yumi/app/routes/sc_routes.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; +import 'package:yumi/shared/business_logic/models/res/join_room_res.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/shared/business_logic/models/res/mic_res.dart'; +import 'package:yumi/shared/business_logic/models/res/room_res.dart'; +import 'package:yumi/modules/room/voice_room_route.dart'; +import 'package:yumi/ui_kit/widgets/room/empty_mai_select.dart'; +import 'package:yumi/ui_kit/widgets/room/room_user_info_card.dart'; +import '../../shared/tools/sc_heartbeat_utils.dart'; +import '../../shared/data_sources/models/enum/sc_heartbeat_status.dart'; +import '../../shared/data_sources/models/enum/sc_room_info_event_type.dart'; +import '../../shared/data_sources/models/enum/sc_room_roles_type.dart'; +import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart'; +import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart'; +import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart'; +import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart'; +import '../../shared/tools/sc_room_profile_cache.dart'; +import '../../ui_kit/components/sc_float_ichart.dart'; + +typedef OnSoundVoiceChange = Function(num index, int volum); +typedef RtcProvider = RealTimeCommunicationManager; + class RealTimeCommunicationManager extends ChangeNotifier { static const Duration _micListPollingInterval = Duration(seconds: 2); static const Duration _onlineUsersPollingInterval = Duration(seconds: 3); + static const Duration _selfMicSwitchGracePeriod = Duration(seconds: 4); + static const Duration _giftTriggeredMicRefreshMinInterval = Duration( + milliseconds: 900, + ); bool needUpDataUserInfo = false; bool _roomVisualEffectsEnabled = false; @@ -56,70 +60,81 @@ class RealTimeCommunicationManager extends ChangeNotifier { Timer? _onlineUsersPollingTimer; bool _isRefreshingMicList = false; bool _isRefreshingOnlineUsers = false; - - ///当前所在房间 - JoinRoomRes? currenRoom; - - /// 声音音量变化监听 - final List _onSoundVoiceChangeList = []; - - ///麦位 - Map roomWheatMap = {}; - - RtcEngine? engine; - BuildContext? context; - RtmProvider? rtmProvider; - - SCIsFollowRoomRes? isFollowRoomRes; - SCRoomRocketStatusRes? roomRocketStatus; - - ///房间是否静音 - bool roomIsMute = false; - - bool closeFullGame = false; - - ///禁音开关 默认关闭 - bool isMic = true; - - ///在线用户列表 - List onlineUsers = []; - - ///房间管理员 - List managerUsers = []; - - ///音乐是否正在播放 - bool isMusicPlaying = false; - - ///房间红包列表 - List redPacketList = []; - - num roomTaskClaimableCount = 0; - - initializeRealTimeCommunicationManager(BuildContext context) { - this.context = context; - } - - bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled; - - bool get isExitingCurrentVoiceRoomSession => - _isExitingCurrentVoiceRoomSession; - - bool get shouldShowRoomVisualEffects => - currenRoom != null && _roomVisualEffectsEnabled; - - void setRoomVisualEffectsEnabled(bool enabled) { - if (_roomVisualEffectsEnabled == enabled) { - return; - } - _roomVisualEffectsEnabled = enabled; - notifyListeners(); - } - + bool _pendingMicListRefresh = false; + Timer? _deferredMicListRefreshTimer; + int _lastMicListRefreshStartedAtMs = 0; + num? _preferredSelfMicIndex; + num? _pendingSelfMicSourceIndex; + int? _pendingSelfMicSwitchGuardUntilMs; + ClientRoleType? _lastAppliedClientRole; + bool? _lastAppliedLocalAudioMuted; + bool? _lastScheduledVoiceLiveOnMic; + String? _lastScheduledVoiceLiveRoomId; + String? _lastScheduledAnchorRoomId; + + ///当前所在房间 + JoinRoomRes? currenRoom; + + /// 声音音量变化监听 + final List _onSoundVoiceChangeList = []; + + ///麦位 + Map roomWheatMap = {}; + + RtcEngine? engine; + BuildContext? context; + RtmProvider? rtmProvider; + + SCIsFollowRoomRes? isFollowRoomRes; + SCRoomRocketStatusRes? roomRocketStatus; + + ///房间是否静音 + bool roomIsMute = false; + + bool closeFullGame = false; + + ///禁音开关 默认关闭 + bool isMic = true; + + ///在线用户列表 + List onlineUsers = []; + + ///房间管理员 + List managerUsers = []; + + ///音乐是否正在播放 + bool isMusicPlaying = false; + + ///房间红包列表 + List redPacketList = []; + + num roomTaskClaimableCount = 0; + + initializeRealTimeCommunicationManager(BuildContext context) { + this.context = context; + } + + bool get roomVisualEffectsEnabled => _roomVisualEffectsEnabled; + + bool get isExitingCurrentVoiceRoomSession => + _isExitingCurrentVoiceRoomSession; + + bool get shouldShowRoomVisualEffects => + currenRoom != null && _roomVisualEffectsEnabled; + + void setRoomVisualEffectsEnabled(bool enabled) { + if (_roomVisualEffectsEnabled == enabled) { + return; + } + _roomVisualEffectsEnabled = enabled; + notifyListeners(); + } + void _setExitingCurrentVoiceRoomSession(bool enabled, {bool notify = true}) { if (_isExitingCurrentVoiceRoomSession == enabled) { return; - } - _isExitingCurrentVoiceRoomSession = enabled; + } + _isExitingCurrentVoiceRoomSession = enabled; if (notify) { notifyListeners(); } @@ -141,8 +156,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { void _stopRoomStatePolling() { _micListPollingTimer?.cancel(); _onlineUsersPollingTimer?.cancel(); + _deferredMicListRefreshTimer?.cancel(); _micListPollingTimer = null; _onlineUsersPollingTimer = null; + _deferredMicListRefreshTimer = null; _isRefreshingMicList = false; _isRefreshingOnlineUsers = false; } @@ -209,14 +226,13 @@ class RealTimeCommunicationManager extends ChangeNotifier { void _refreshManagerUsers(List users) { managerUsers ..clear() - ..addAll( - users.where((user) => user.roles == SCRoomRolesType.ADMIN.name), - ); + ..addAll(users.where((user) => user.roles == SCRoomRolesType.ADMIN.name)); } Map _buildMicMap(List roomWheatList) { final previousMap = roomWheatMap; final nextMap = {}; + final userSeatMap = {}; for (final roomWheat in roomWheatList) { final micIndex = roomWheat.micIndex; if (micIndex == null) { @@ -226,7 +242,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { final shouldPreserveTransientState = previousMic?.user?.id == roomWheat.user?.id && previousMic?.user?.id != null; - nextMap[micIndex] = + final mergedMic = shouldPreserveTransientState ? roomWheat.copyWith( emojiPath: @@ -243,8 +259,165 @@ class RealTimeCommunicationManager extends ChangeNotifier { : previousMic?.number, ) : roomWheat; + final normalizedUserId = (mergedMic.user?.id ?? "").trim(); + if (normalizedUserId.isNotEmpty) { + final existingIndex = userSeatMap[normalizedUserId]; + if (existingIndex != null) { + final shouldReplaceExistingSeat = _shouldPreferDuplicateMicSeat( + userId: normalizedUserId, + existingIndex: existingIndex, + candidateIndex: micIndex, + previousMap: previousMap, + ); + if (!shouldReplaceExistingSeat) { + continue; + } + nextMap.remove(existingIndex); + } + userSeatMap[normalizedUserId] = micIndex; + } + nextMap[micIndex] = mergedMic; } - return nextMap; + return _stabilizeSelfMicSnapshot(nextMap, previousMap: previousMap); + } + + void _startSelfMicSwitchGuard({ + required num sourceIndex, + required num targetIndex, + }) { + _preferredSelfMicIndex = targetIndex; + _pendingSelfMicSourceIndex = sourceIndex; + _pendingSelfMicSwitchGuardUntilMs = + DateTime.now().add(_selfMicSwitchGracePeriod).millisecondsSinceEpoch; + } + + void _clearSelfMicSwitchGuard({bool clearPreferredIndex = false}) { + _pendingSelfMicSourceIndex = null; + _pendingSelfMicSwitchGuardUntilMs = null; + if (clearPreferredIndex) { + _preferredSelfMicIndex = null; + } + } + + Map _stabilizeSelfMicSnapshot( + Map nextMap, { + required Map previousMap, + }) { + final currentUserId = + (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); + if (currentUserId.isEmpty) { + return nextMap; + } + + final targetIndex = _preferredSelfMicIndex; + final sourceIndex = _pendingSelfMicSourceIndex; + final guardUntilMs = _pendingSelfMicSwitchGuardUntilMs ?? 0; + final hasActiveGuard = + targetIndex != null && + sourceIndex != null && + guardUntilMs > DateTime.now().millisecondsSinceEpoch; + final selfSeatIndices = + nextMap.entries + .where((entry) => entry.value.user?.id == currentUserId) + .map((entry) => entry.key) + .toList(); + + if (targetIndex != null && + nextMap[targetIndex]?.user?.id == currentUserId) { + _clearSelfMicSwitchGuard(); + return nextMap; + } + + if (!hasActiveGuard) { + _clearSelfMicSwitchGuard(); + return nextMap; + } + final resolvedTargetIndex = targetIndex; + final resolvedSourceIndex = sourceIndex; + + final selfOnlyOnSource = + selfSeatIndices.isEmpty || + selfSeatIndices.every((seatIndex) => seatIndex == resolvedSourceIndex); + if (!selfOnlyOnSource) { + return nextMap; + } + + final optimisticTargetSeat = previousMap[resolvedTargetIndex]; + if (optimisticTargetSeat == null || + optimisticTargetSeat.user?.id != currentUserId) { + return nextMap; + } + + final incomingTargetSeat = nextMap[resolvedTargetIndex]; + if (incomingTargetSeat?.user != null && + incomingTargetSeat?.user?.id != currentUserId) { + return nextMap; + } + + final stabilizedMap = Map.from(nextMap); + for (final seatIndex in selfSeatIndices) { + if (seatIndex == resolvedTargetIndex) { + continue; + } + final seat = stabilizedMap[seatIndex]; + if (seat == null) { + continue; + } + stabilizedMap[seatIndex] = seat.copyWith(clearUser: true); + } + + final baseSeat = incomingTargetSeat ?? optimisticTargetSeat; + stabilizedMap[resolvedTargetIndex] = baseSeat.copyWith( + user: optimisticTargetSeat.user, + micMute: incomingTargetSeat?.micMute ?? optimisticTargetSeat.micMute, + micLock: incomingTargetSeat?.micLock ?? optimisticTargetSeat.micLock, + roomToken: + incomingTargetSeat?.roomToken ?? optimisticTargetSeat.roomToken, + emojiPath: + (incomingTargetSeat?.emojiPath ?? "").isNotEmpty + ? incomingTargetSeat?.emojiPath + : optimisticTargetSeat.emojiPath, + type: + (incomingTargetSeat?.type ?? "").isNotEmpty + ? incomingTargetSeat?.type + : optimisticTargetSeat.type, + number: + (incomingTargetSeat?.number ?? "").isNotEmpty + ? incomingTargetSeat?.number + : optimisticTargetSeat.number, + ); + + return stabilizedMap; + } + + bool _shouldPreferDuplicateMicSeat({ + required String userId, + required num existingIndex, + required num candidateIndex, + required Map previousMap, + }) { + if (existingIndex == candidateIndex) { + return true; + } + final currentUserId = + (AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim(); + if (userId == currentUserId && _preferredSelfMicIndex != null) { + if (candidateIndex == _preferredSelfMicIndex) { + return true; + } + if (existingIndex == _preferredSelfMicIndex) { + return false; + } + } + final candidateWasPreviouslyOccupiedByUser = + previousMap[candidateIndex]?.user?.id == userId; + final existingWasPreviouslyOccupiedByUser = + previousMap[existingIndex]?.user?.id == userId; + if (candidateWasPreviouslyOccupiedByUser != + existingWasPreviouslyOccupiedByUser) { + return candidateWasPreviouslyOccupiedByUser; + } + return false; } void _syncSelfMicRuntimeState() { @@ -252,6 +425,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { if ((currentUserId ?? "").isEmpty) { return; } + final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; MicRes? currentUserMic; for (final mic in roomWheatMap.values) { @@ -262,25 +436,110 @@ class RealTimeCommunicationManager extends ChangeNotifier { } if (currentUserMic == null) { - SCHeartbeatUtils.cancelAnchorTimer(); - engine?.setClientRole(role: ClientRoleType.clientRoleAudience); - engine?.muteLocalAudioStream(true); + _clearSelfMicSwitchGuard(clearPreferredIndex: true); + _syncRoomHeartbeatState( + roomId: roomId, + isOnMic: false, + shouldRunAnchorHeartbeat: false, + ); + _applyLocalAudioRuntimeState( + clientRole: ClientRoleType.clientRoleAudience, + muted: true, + ); return; } - SCHeartbeatUtils.scheduleAnchorHeartbeat( - currenRoom?.roomProfile?.roomProfile?.id ?? "", + _preferredSelfMicIndex = currentUserMic.micIndex; + _syncRoomHeartbeatState( + roomId: roomId, + isOnMic: true, + shouldRunAnchorHeartbeat: true, ); if ((currentUserMic.micMute ?? false) || isMic) { - engine?.setClientRole(role: ClientRoleType.clientRoleAudience); - engine?.muteLocalAudioStream(true); + _applyLocalAudioRuntimeState( + clientRole: ClientRoleType.clientRoleAudience, + muted: true, + ); return; } - adjustRecordingSignalVolume(100); - engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); - engine?.muteLocalAudioStream(false); + _applyLocalAudioRuntimeState( + clientRole: ClientRoleType.clientRoleBroadcaster, + muted: false, + ); + } + + void _syncRoomHeartbeatState({ + required String roomId, + required bool isOnMic, + required bool shouldRunAnchorHeartbeat, + }) { + final normalizedRoomId = roomId.trim(); + if (normalizedRoomId.isEmpty) { + _resetHeartbeatTracking(); + return; + } + + final voiceLiveChanged = + _lastScheduledVoiceLiveRoomId != normalizedRoomId || + _lastScheduledVoiceLiveOnMic != isOnMic; + if (voiceLiveChanged) { + SCHeartbeatUtils.scheduleHeartbeat( + SCHeartbeatStatus.VOICE_LIVE.name, + isOnMic, + roomId: normalizedRoomId, + ); + _lastScheduledVoiceLiveRoomId = normalizedRoomId; + _lastScheduledVoiceLiveOnMic = isOnMic; + } + + if (!shouldRunAnchorHeartbeat) { + if (_lastScheduledAnchorRoomId != null) { + SCHeartbeatUtils.cancelAnchorTimer(); + _lastScheduledAnchorRoomId = null; + } + return; + } + + if (voiceLiveChanged || _lastScheduledAnchorRoomId != normalizedRoomId) { + SCHeartbeatUtils.scheduleAnchorHeartbeat(normalizedRoomId); + _lastScheduledAnchorRoomId = normalizedRoomId; + } + } + + void _applyLocalAudioRuntimeState({ + required ClientRoleType clientRole, + required bool muted, + }) { + if (engine == null) { + _lastAppliedClientRole = null; + _lastAppliedLocalAudioMuted = null; + return; + } + + if (_lastAppliedClientRole != clientRole) { + engine?.setClientRole(role: clientRole); + _lastAppliedClientRole = clientRole; + } + if (!muted && _lastAppliedLocalAudioMuted != false) { + adjustRecordingSignalVolume(100); + } + if (_lastAppliedLocalAudioMuted != muted) { + engine?.muteLocalAudioStream(muted); + _lastAppliedLocalAudioMuted = muted; + } + } + + void _resetHeartbeatTracking() { + _lastScheduledVoiceLiveOnMic = null; + _lastScheduledVoiceLiveRoomId = null; + _lastScheduledAnchorRoomId = null; + } + + void _resetLocalAudioRuntimeTracking() { + _lastAppliedClientRole = null; + _lastAppliedLocalAudioMuted = null; } void _applyMicSnapshot( @@ -294,243 +553,332 @@ class RealTimeCommunicationManager extends ChangeNotifier { notifyListeners(); } } - - Future joinAgoraVoiceChannel() async { - try { - engine = await _initAgoraRtcEngine(); - engine?.setAudioProfile( - profile: AudioProfileType.audioProfileSpeechStandard, - scenario: AudioScenarioType.audioScenarioGameStreaming, - ); - engine?.enableAudioVolumeIndication( - interval: 500, - smooth: 3, - reportVad: true, - ); - await engine?.disableVideo(); - await engine?.setLocalPublishFallbackOption( - StreamFallbackOptions.streamFallbackOptionAudioOnly, - ); - engine?.registerEventHandler( - RtcEngineEventHandler( - onError: (ErrorCodeType err, String msg) { - print('rtc错误${err}'); - }, - onAudioMixingStateChanged: ( - AudioMixingStateType state, - AudioMixingReasonType reason, - ) { - switch (state) { - case AudioMixingStateType.audioMixingStatePlaying: - isMusicPlaying = true; - break; - case AudioMixingStateType.audioMixingStateStopped: - case AudioMixingStateType.audioMixingStateFailed: - isMusicPlaying = false; - break; - default: - break; - } - }, - onJoinChannelSuccess: (RtcConnection connection, int elapsed) { - print('rtc 自己加入 ${connection.channelId} ${connection.localUid}'); - }, - onUserJoined: (connection, remoteUid, elapsed) { - print('rtc用户 $remoteUid 加入了频道'); - }, - // 监听远端用户离开 - onUserOffline: (connection, remoteUid, reason) { - print('rtc用户 $remoteUid 离开了频道 (原因: ${reason})'); - }, - onTokenPrivilegeWillExpire: ( - RtcConnection connection, - String token, - ) async { - var rtcToken = await SCAccountRepository().getRtcToken( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - AccountStorage().getCurrentUser()?.userProfile?.id ?? "", - isPublisher: isOnMai(), - ); - engine?.renewToken(rtcToken.rtcToken ?? ""); - }, - onAudioVolumeIndication: initializeAudioVolumeIndicationCallback, - ), - ); - var rtcToken = await SCAccountRepository().getRtcToken( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - AccountStorage().getCurrentUser()?.userProfile?.id ?? "", - isPublisher: false, - ); - await engine?.joinChannel( - token: rtcToken.rtcToken ?? "", - channelId: currenRoom?.roomProfile?.roomProfile?.id ?? "", - uid: int.parse( - AccountStorage().getCurrentUser()?.userProfile?.account ?? "0", - ), - options: ChannelMediaOptions( - // 自动订阅所有视频流 - autoSubscribeVideo: false, - // 自动订阅所有音频流 - autoSubscribeAudio: true, - // 发布摄像头采集的视频 - publishCameraTrack: false, - // 发布麦克风采集的音频 - publishMicrophoneTrack: true, - // 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众) - clientRoleType: ClientRoleType.clientRoleAudience, - channelProfile: ChannelProfileType.channelProfileLiveBroadcasting, - ), - ); - engine?.muteAllRemoteAudioStreams(roomIsMute); - } catch (e) { - SCTts.show("Join room fail"); - exitCurrentVoiceRoomSession(false); - print('加入失败:${e.runtimeType},${e.toString()}'); - } - } - - Future _initAgoraRtcEngine() async { - RtcEngine? engine; - while (engine == null) { - engine = createAgoraRtcEngine(); - await engine.initialize( - RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid), - ); - } - return engine; - } - - void initializeAudioVolumeIndicationCallback( - RtcConnection connection, - List speakers, - int speakerNumber, - int totalVolume, - ) { - // if (user == null) { - // throw "本地User尚未初始化"; - // } - // print('初始化声音监听器:${user.toJson()}'); - - ///声音全部清零 - roomWheatMap.forEach((k, v) { - roomWheatMap[k]!.setVolume = 0; - }); - for (var info in speakers) { - num? uid = info.uid; - int voloum = info.volume!; - print('${info.uid}在说话---${voloum}'); - - if (voloum > 0) { - ///是本地声音 - if (uid == 0) { - uid = num.parse( - AccountStorage().getCurrentUser()!.userProfile!.account!, - ); - } - - ///在哪个麦 - roomWheatMap.forEach((k, v) { - if (v.user != null) { - if (num.parse(v.user!.account!) == uid) { - roomWheatMap[k]!.setVolume = voloum; - for (var element in _onSoundVoiceChangeList) { - element(k, voloum); - } - } - } - }); - } - } - } - - ///关注/取消房间 - void followCurrentVoiceRoom() async { - var result = await SCAccountRepository().followRoom( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - ); - if (isFollowRoomRes != null) { - isFollowRoomRes = isFollowRoomRes!.copyWith( - followRoom: !isFollowRoomRes!.followRoom!, - ); - } else { - isFollowRoomRes = SCIsFollowRoomRes(followRoom: result); - } - SmartDialog.dismiss(tag: "unFollowDialog"); - notifyListeners(); - } - - int startTime = 0; - - String? _preferNonEmpty(String? primary, String? fallback) { - if ((primary ?? "").trim().isNotEmpty) { - return primary; - } - if ((fallback ?? "").trim().isNotEmpty) { - return fallback; - } - return primary ?? fallback; - } - - void updateCurrentRoomBasicInfo({ - String? roomCover, - String? roomName, - String? roomDesc, - }) { - final currentRoomProfile = currenRoom?.roomProfile?.roomProfile; - if (currenRoom == null || currentRoomProfile == null) { - return; - } - currenRoom = currenRoom?.copyWith( - roomProfile: currenRoom?.roomProfile?.copyWith( - roomProfile: currentRoomProfile.copyWith( - roomCover: _preferNonEmpty(roomCover, currentRoomProfile.roomCover), - roomName: _preferNonEmpty(roomName, currentRoomProfile.roomName), - roomDesc: _preferNonEmpty(roomDesc, currentRoomProfile.roomDesc), - ), - ), - ); - SCRoomProfileCache.saveRoomProfile( - roomId: currentRoomProfile.id ?? "", - roomCover: roomCover, - roomName: roomName, - roomDesc: roomDesc, - ); - notifyListeners(); - } - - void joinVoiceRoomSession( - BuildContext context, - String roomId, { - String? pwd, - bool clearRoomData = false, - bool needOpenRedenvelope = false, - String redPackId = "", - }) async { - int nextTime = DateTime.now().millisecondsSinceEpoch; - if (nextTime - startTime < 1000) { - //频繁点击 - return; - } - startTime = nextTime; - if (clearRoomData) { - _clearData(); - } - bool hasPermission = await SCPermissionUtils.checkMicrophonePermission(); - if (!hasPermission) { - SCTts.show('Microphone permission is denied.'); - throw ArgumentError('Microphone permission is denied.'); - } + + int? _tryParseAgoraUid(String? value) { + final normalizedValue = (value ?? '').trim(); + if (normalizedValue.isEmpty) { + return null; + } + return int.tryParse(normalizedValue); + } + + int _stableAgoraUidFromUser(SocialChatUserProfile? user) { + final source = + '${user?.account ?? ""}|${user?.id ?? ""}|${user?.userNickname ?? ""}'; + var hash = 0x811C9DC5; + for (final codeUnit in source.codeUnits) { + hash ^= codeUnit; + hash = (hash * 0x01000193) & 0x7fffffff; + } + return hash == 0 ? 1 : hash; + } + + int _resolveAgoraUidForUser(SocialChatUserProfile? user) { + final parsedAccountUid = _tryParseAgoraUid(user?.account); + if (parsedAccountUid != null && parsedAccountUid > 0) { + return parsedAccountUid; + } + final parsedUserIdUid = _tryParseAgoraUid(user?.id); + if (parsedUserIdUid != null && parsedUserIdUid > 0) { + return parsedUserIdUid; + } + return _stableAgoraUidFromUser(user); + } + + int _resolveAgoraUidForCurrentUser() { + return _resolveAgoraUidForUser( + AccountStorage().getCurrentUser()?.userProfile, + ); + } + + void requestGiftTriggeredMicRefresh() { + requestMicrophoneListRefresh( + notifyIfUnchanged: false, + minInterval: _giftTriggeredMicRefreshMinInterval, + ); + } + + void requestMicrophoneListRefresh({ + bool notifyIfUnchanged = false, + Duration minInterval = Duration.zero, + }) { + final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; + if (roomId.isEmpty) { + return; + } + + if (minInterval <= Duration.zero) { + _deferredMicListRefreshTimer?.cancel(); + _deferredMicListRefreshTimer = null; + retrieveMicrophoneList( + notifyIfUnchanged: notifyIfUnchanged, + ).catchError((_) {}); + return; + } + + final nowMs = DateTime.now().millisecondsSinceEpoch; + final elapsedMs = nowMs - _lastMicListRefreshStartedAtMs; + if (!_isRefreshingMicList && elapsedMs >= minInterval.inMilliseconds) { + _deferredMicListRefreshTimer?.cancel(); + _deferredMicListRefreshTimer = null; + retrieveMicrophoneList( + notifyIfUnchanged: notifyIfUnchanged, + ).catchError((_) {}); + return; + } + + final remainingMs = minInterval.inMilliseconds - elapsedMs; + final delayMs = remainingMs > 80 ? remainingMs : 80; + _deferredMicListRefreshTimer?.cancel(); + _deferredMicListRefreshTimer = Timer(Duration(milliseconds: delayMs), () { + _deferredMicListRefreshTimer = null; + retrieveMicrophoneList( + notifyIfUnchanged: notifyIfUnchanged, + ).catchError((_) {}); + }); + } + + Future joinAgoraVoiceChannel() async { + try { + engine = await _initAgoraRtcEngine(); + engine?.setAudioProfile( + profile: AudioProfileType.audioProfileSpeechStandard, + scenario: AudioScenarioType.audioScenarioGameStreaming, + ); + engine?.enableAudioVolumeIndication( + interval: 500, + smooth: 3, + reportVad: true, + ); + await engine?.disableVideo(); + await engine?.setLocalPublishFallbackOption( + StreamFallbackOptions.streamFallbackOptionAudioOnly, + ); + engine?.registerEventHandler( + RtcEngineEventHandler( + onError: (ErrorCodeType err, String msg) { + print('rtc错误${err}'); + }, + onAudioMixingStateChanged: ( + AudioMixingStateType state, + AudioMixingReasonType reason, + ) { + switch (state) { + case AudioMixingStateType.audioMixingStatePlaying: + isMusicPlaying = true; + break; + case AudioMixingStateType.audioMixingStateStopped: + case AudioMixingStateType.audioMixingStateFailed: + isMusicPlaying = false; + break; + default: + break; + } + }, + onJoinChannelSuccess: (RtcConnection connection, int elapsed) { + print('rtc 自己加入 ${connection.channelId} ${connection.localUid}'); + }, + onUserJoined: (connection, remoteUid, elapsed) { + print('rtc用户 $remoteUid 加入了频道'); + }, + // 监听远端用户离开 + onUserOffline: (connection, remoteUid, reason) { + print('rtc用户 $remoteUid 离开了频道 (原因: ${reason})'); + }, + onTokenPrivilegeWillExpire: ( + RtcConnection connection, + String token, + ) async { + var rtcToken = await SCAccountRepository().getRtcToken( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + isPublisher: isOnMai(), + ); + engine?.renewToken(rtcToken.rtcToken ?? ""); + }, + onAudioVolumeIndication: initializeAudioVolumeIndicationCallback, + ), + ); + var rtcToken = await SCAccountRepository().getRtcToken( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + isPublisher: false, + ); + await engine?.joinChannel( + token: rtcToken.rtcToken ?? "", + channelId: currenRoom?.roomProfile?.roomProfile?.id ?? "", + uid: _resolveAgoraUidForCurrentUser(), + options: ChannelMediaOptions( + // 自动订阅所有视频流 + autoSubscribeVideo: false, + // 自动订阅所有音频流 + autoSubscribeAudio: true, + // 发布摄像头采集的视频 + publishCameraTrack: false, + // 发布麦克风采集的音频 + publishMicrophoneTrack: true, + // 设置用户角色为 clientRoleBroadcaster(主播)或 clientRoleAudience(观众) + clientRoleType: ClientRoleType.clientRoleAudience, + channelProfile: ChannelProfileType.channelProfileLiveBroadcasting, + ), + ); + _syncSelfMicRuntimeState(); + engine?.muteAllRemoteAudioStreams(roomIsMute); + } catch (e) { + SCTts.show("Join room fail"); + exitCurrentVoiceRoomSession(false); + print('加入失败:${e.runtimeType},${e.toString()}'); + } + } + + Future _initAgoraRtcEngine() async { + RtcEngine? engine; + while (engine == null) { + engine = createAgoraRtcEngine(); + await engine.initialize( + RtcEngineContext(appId: SCGlobalConfig.agoraRtcAppid), + ); + } + return engine; + } + + void initializeAudioVolumeIndicationCallback( + RtcConnection connection, + List speakers, + int speakerNumber, + int totalVolume, + ) { + // if (user == null) { + // throw "本地User尚未初始化"; + // } + // print('初始化声音监听器:${user.toJson()}'); + + if (roomWheatMap.isEmpty || speakers.isEmpty) { + return; + } + + for (final seat in roomWheatMap.values) { + seat.setVolume = 0; + } + + final seatIndexByAgoraUid = {}; + roomWheatMap.forEach((seatIndex, mic) { + final user = mic.user; + if (user == null) { + return; + } + seatIndexByAgoraUid[_resolveAgoraUidForUser(user)] = seatIndex; + }); + + final currentUserAgoraUid = _resolveAgoraUidForCurrentUser(); + final listeners = List.from(_onSoundVoiceChangeList); + for (final info in speakers) { + final volume = info.volume ?? 0; + if (volume <= 0) { + continue; + } + + final resolvedAgoraUid = info.uid == 0 ? currentUserAgoraUid : info.uid; + final seatIndex = seatIndexByAgoraUid[resolvedAgoraUid]; + if (seatIndex == null) { + continue; + } + + roomWheatMap[seatIndex]?.setVolume = volume; + for (final listener in listeners) { + listener(seatIndex, volume); + } + } + } + + ///关注/取消房间 + void followCurrentVoiceRoom() async { + var result = await SCAccountRepository().followRoom( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + ); + if (isFollowRoomRes != null) { + isFollowRoomRes = isFollowRoomRes!.copyWith( + followRoom: !isFollowRoomRes!.followRoom!, + ); + } else { + isFollowRoomRes = SCIsFollowRoomRes(followRoom: result); + } + SmartDialog.dismiss(tag: "unFollowDialog"); + notifyListeners(); + } + + int startTime = 0; + + String? _preferNonEmpty(String? primary, String? fallback) { + if ((primary ?? "").trim().isNotEmpty) { + return primary; + } + if ((fallback ?? "").trim().isNotEmpty) { + return fallback; + } + return primary ?? fallback; + } + + void updateCurrentRoomBasicInfo({ + String? roomCover, + String? roomName, + String? roomDesc, + }) { + final currentRoomProfile = currenRoom?.roomProfile?.roomProfile; + if (currenRoom == null || currentRoomProfile == null) { + return; + } + currenRoom = currenRoom?.copyWith( + roomProfile: currenRoom?.roomProfile?.copyWith( + roomProfile: currentRoomProfile.copyWith( + roomCover: _preferNonEmpty(roomCover, currentRoomProfile.roomCover), + roomName: _preferNonEmpty(roomName, currentRoomProfile.roomName), + roomDesc: _preferNonEmpty(roomDesc, currentRoomProfile.roomDesc), + ), + ), + ); + SCRoomProfileCache.saveRoomProfile( + roomId: currentRoomProfile.id ?? "", + roomCover: roomCover, + roomName: roomName, + roomDesc: roomDesc, + ); + notifyListeners(); + } + + void joinVoiceRoomSession( + BuildContext context, + String roomId, { + String? pwd, + bool clearRoomData = false, + bool needOpenRedenvelope = false, + String redPackId = "", + }) async { + int nextTime = DateTime.now().millisecondsSinceEpoch; + if (nextTime - startTime < 1000) { + //频繁点击 + return; + } + startTime = nextTime; + if (clearRoomData) { + _clearData(); + } + bool hasPermission = await SCPermissionUtils.checkMicrophonePermission(); + if (!hasPermission) { + SCTts.show('Microphone permission is denied.'); + throw ArgumentError('Microphone permission is denied.'); + } if (roomId == currenRoom?.roomProfile?.roomProfile?.id) { - ///最小化进入房间,或者进入的是同一个房间 - final loaded = await loadRoomInfo( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - ); - if (!loaded) { - return; - } - if (!context.mounted) { - return; - } + ///最小化进入房间,或者进入的是同一个房间 + final loaded = await loadRoomInfo( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + ); + if (!loaded) { + return; + } + if (!context.mounted) { + return; + } Provider.of( context, listen: false, @@ -540,111 +888,111 @@ class RealTimeCommunicationManager extends ChangeNotifier { _startRoomStatePolling(); setRoomVisualEffectsEnabled(true); SCFloatIchart().remove(); - SCNavigatorUtils.push( - context, - '${VoiceRoomRoute.voiceRoom}?id=$roomId', - transition: TransitionType.custom, - transitionDuration: kOpenRoomTransitionDuration, - transitionBuilder: ( - BuildContext context, - Animation animation, - Animation secondaryAnimation, - Widget child, - ) { - return kOpenRoomTransitionBuilder( - context, - animation, - secondaryAnimation, - child, - ); - }, - ); - } else { - SCFloatIchart().remove(); - if (currenRoom != null) { - await exitCurrentVoiceRoomSession(false); - } - if (!context.mounted) { - return; - } - rtmProvider = Provider.of(context, listen: false); - SCLoadingManager.show(context: context); - try { - currenRoom = await SCAccountRepository().entryRoom( - roomId, - pwd: pwd, - redPackId: redPackId, - needOpenRedenvelope: needOpenRedenvelope, - ); - await initializeRoomSession( - needOpenRedenvelope: needOpenRedenvelope, - redPackId: redPackId, - ); - notifyListeners(); - } catch (e) { - await resetLocalRoomState(fallbackRtmProvider: rtmProvider); - rethrow; - } finally { - SCLoadingManager.hide(); - } - } - } - - ///初始化房间相关 - Future initializeRoomSession({ - bool needOpenRedenvelope = false, - String? redPackId, - }) async { - if ((currenRoom?.roomProfile?.roomProfile?.event == - SCRoomInfoEventType.WAITING_CONFIRMED.name || - currenRoom?.roomProfile?.roomProfile?.event == - SCRoomInfoEventType.ID_CHANGE.name) && - currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name) { - ///需要去修改房间信息,创建群聊 - SCNavigatorUtils.push( - context!, - "${VoiceRoomRoute.roomEdit}?need=true", - replace: false, - ); - return Future; - } - SCRoomUtils.roomSCGlobalConfig( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - ); - if (currenRoom?.roomProfile?.roomProfile?.id != - AccountStorage().getCurrentUser()?.userProfile?.id) { - SCChatRoomRepository() - .isFollowRoom(currenRoom?.roomProfile?.roomProfile?.id ?? "") - .then((value) { - isFollowRoomRes = value; - notifyListeners(); - }); - } - setRoomVisualEffectsEnabled(true); - SCNavigatorUtils.push(context!, VoiceRoomRoute.voiceRoom, replace: false); - var joinResult = await rtmProvider?.joinRoomGroup( - currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", - "", - ); - SCChatRoomRepository() - .rocketClaim(currenRoom?.roomProfile?.roomProfile?.id ?? "") - .catchError((e) { - return true; // 错误已处理 - }); - rtmProvider?.addMsg( - Msg( - groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", - msg: SCAppLocalizations.of(context!)!.systemRoomTips, - type: SCRoomMsgType.systemTips, - ), - ); - rtmProvider?.addMsg( - Msg( - groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", - msg: currenRoom?.roomProfile?.roomProfile?.roomDesc, - type: SCRoomMsgType.systemTips, - ), - ); + SCNavigatorUtils.push( + context, + '${VoiceRoomRoute.voiceRoom}?id=$roomId', + transition: TransitionType.custom, + transitionDuration: kOpenRoomTransitionDuration, + transitionBuilder: ( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { + return kOpenRoomTransitionBuilder( + context, + animation, + secondaryAnimation, + child, + ); + }, + ); + } else { + SCFloatIchart().remove(); + if (currenRoom != null) { + await exitCurrentVoiceRoomSession(false); + } + if (!context.mounted) { + return; + } + rtmProvider = Provider.of(context, listen: false); + SCLoadingManager.show(context: context); + try { + currenRoom = await SCAccountRepository().entryRoom( + roomId, + pwd: pwd, + redPackId: redPackId, + needOpenRedenvelope: needOpenRedenvelope, + ); + await initializeRoomSession( + needOpenRedenvelope: needOpenRedenvelope, + redPackId: redPackId, + ); + notifyListeners(); + } catch (e) { + await resetLocalRoomState(fallbackRtmProvider: rtmProvider); + rethrow; + } finally { + SCLoadingManager.hide(); + } + } + } + + ///初始化房间相关 + Future initializeRoomSession({ + bool needOpenRedenvelope = false, + String? redPackId, + }) async { + if ((currenRoom?.roomProfile?.roomProfile?.event == + SCRoomInfoEventType.WAITING_CONFIRMED.name || + currenRoom?.roomProfile?.roomProfile?.event == + SCRoomInfoEventType.ID_CHANGE.name) && + currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name) { + ///需要去修改房间信息,创建群聊 + SCNavigatorUtils.push( + context!, + "${VoiceRoomRoute.roomEdit}?need=true", + replace: false, + ); + return Future; + } + SCRoomUtils.roomSCGlobalConfig( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + ); + if (currenRoom?.roomProfile?.roomProfile?.id != + AccountStorage().getCurrentUser()?.userProfile?.id) { + SCChatRoomRepository() + .isFollowRoom(currenRoom?.roomProfile?.roomProfile?.id ?? "") + .then((value) { + isFollowRoomRes = value; + notifyListeners(); + }); + } + setRoomVisualEffectsEnabled(true); + SCNavigatorUtils.push(context!, VoiceRoomRoute.voiceRoom, replace: false); + var joinResult = await rtmProvider?.joinRoomGroup( + currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", + "", + ); + SCChatRoomRepository() + .rocketClaim(currenRoom?.roomProfile?.roomProfile?.id ?? "") + .catchError((e) { + return true; // 错误已处理 + }); + rtmProvider?.addMsg( + Msg( + groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", + msg: SCAppLocalizations.of(context!)!.systemRoomTips, + type: SCRoomMsgType.systemTips, + ), + ); + rtmProvider?.addMsg( + Msg( + groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", + msg: currenRoom?.roomProfile?.roomProfile?.roomDesc, + type: SCRoomMsgType.systemTips, + ), + ); ///获取麦位 retrieveMicrophoneList(); @@ -653,59 +1001,59 @@ class RealTimeCommunicationManager extends ChangeNotifier { Provider.of( context!, listen: false, - ).fetchContributionLevelData( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - ); - await joinAgoraVoiceChannel(); - if (joinResult?.code == 0) { - Msg joinMsg = Msg( - groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", - user: AccountStorage().getCurrentUser()?.userProfile, - msg: "", - role: currenRoom?.entrants?.roles ?? "", - type: SCRoomMsgType.joinRoom, - ); - rtmProvider?.dispatchMessage(joinMsg, addLocal: true); - if (SCGlobalConfig.isEntryVehicleAnimation) { - if (AccountStorage().getCurrentUser()?.userProfile?.getMountains() != - null && - shouldShowRoomVisualEffects) { - Future.delayed(Duration(milliseconds: 550), () { - SCGiftVapSvgaManager().play( - AccountStorage() - .getCurrentUser() - ?.userProfile - ?.getMountains() - ?.sourceUrl ?? - "", - priority: 100, - type: 1, - ); - }); - } - } - if (rtmProvider?.msgUserJoinListener != null) { - rtmProvider?.msgUserJoinListener!(joinMsg); - } - } - SCChatRoomRepository() - .rocketStatus(currenRoom?.roomProfile?.roomProfile?.id ?? "") - .then((res) { - roomRocketStatus = res; - notifyListeners(); - }) - .catchError((e) {}); - loadRoomRedPacketList(1); - - fetchRoomTaskClaimableCount(); - } - - ///更新房间火箭信息 - void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) { - roomRocketStatus = res; - notifyListeners(); - } - + ).fetchContributionLevelData( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + ); + await joinAgoraVoiceChannel(); + if (joinResult?.code == 0) { + Msg joinMsg = Msg( + groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount ?? "", + user: AccountStorage().getCurrentUser()?.userProfile, + msg: "", + role: currenRoom?.entrants?.roles ?? "", + type: SCRoomMsgType.joinRoom, + ); + rtmProvider?.dispatchMessage(joinMsg, addLocal: true); + if (SCGlobalConfig.isEntryVehicleAnimation) { + if (AccountStorage().getCurrentUser()?.userProfile?.getMountains() != + null && + shouldShowRoomVisualEffects) { + Future.delayed(Duration(milliseconds: 550), () { + SCGiftVapSvgaManager().play( + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.getMountains() + ?.sourceUrl ?? + "", + priority: 100, + type: 1, + ); + }); + } + } + if (rtmProvider?.msgUserJoinListener != null) { + rtmProvider?.msgUserJoinListener!(joinMsg); + } + } + SCChatRoomRepository() + .rocketStatus(currenRoom?.roomProfile?.roomProfile?.id ?? "") + .then((res) { + roomRocketStatus = res; + notifyListeners(); + }) + .catchError((e) {}); + loadRoomRedPacketList(1); + + fetchRoomTaskClaimableCount(); + } + + ///更新房间火箭信息 + void updateRoomRocketConfigurationStatus(SCRoomRocketStatusRes res) { + roomRocketStatus = res; + notifyListeners(); + } + ///获取在线用户 Future fetchOnlineUsersList({bool notifyIfUnchanged = true}) async { final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; @@ -731,232 +1079,235 @@ class RealTimeCommunicationManager extends ChangeNotifier { Future retrieveMicrophoneList({bool notifyIfUnchanged = true}) async { final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? ""; - if (roomId.isEmpty || _isRefreshingMicList) { + if (roomId.isEmpty) { + return; + } + if (_isRefreshingMicList) { + _pendingMicListRefresh = true; return; } _isRefreshingMicList = true; - bool isOnMic = false; + _lastMicListRefreshStartedAtMs = DateTime.now().millisecondsSinceEpoch; try { final roomWheatList = await SCChatRoomRepository().micList(roomId); if (roomId != currenRoom?.roomProfile?.roomProfile?.id) { return; } final nextMap = _buildMicMap(roomWheatList); - for (final roomWheat in nextMap.values) { - if (roomWheat.user != null && - roomWheat.user!.id == - AccountStorage().getCurrentUser()?.userProfile?.id) { - isOnMic = true; - break; - } - } _applyMicSnapshot(nextMap, notifyIfUnchanged: notifyIfUnchanged); - SCHeartbeatUtils.scheduleHeartbeat( - SCHeartbeatStatus.VOICE_LIVE.name, - isOnMic, - roomId: currenRoom?.roomProfile?.roomProfile?.id, - ); } finally { _isRefreshingMicList = false; + if (_pendingMicListRefresh) { + _pendingMicListRefresh = false; + unawaited( + Future.microtask( + () => retrieveMicrophoneList(notifyIfUnchanged: notifyIfUnchanged), + ), + ); + } } } - - void fetchRoomTaskClaimableCount() { - SCChatRoomRepository() - .roomTaskClaimableCount() - .then((res) { - roomTaskClaimableCount = res.claimableCount ?? 0; - notifyListeners(); - }) - .catchError((e) {}); - } - - Future> loadRoomRedPacketList( - int current, - ) async { - var result = await SCChatRoomRepository().roomRedPacketList( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - current, - ); - if (current == 1) { - redPacketList = result; - notifyListeners(); - } - return result; - } - + + void fetchRoomTaskClaimableCount() { + SCChatRoomRepository() + .roomTaskClaimableCount() + .then((res) { + roomTaskClaimableCount = res.claimableCount ?? 0; + notifyListeners(); + }) + .catchError((e) {}); + } + + Future> loadRoomRedPacketList( + int current, + ) async { + var result = await SCChatRoomRepository().roomRedPacketList( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + current, + ); + if (current == 1) { + redPacketList = result; + notifyListeners(); + } + return result; + } + Future exitCurrentVoiceRoomSession(bool isLogout) async { _setExitingCurrentVoiceRoomSession(true); _stopRoomStatePolling(); try { rtmProvider?.msgAllListener = null; - rtmProvider?.msgChatListener = null; - rtmProvider?.msgGiftListener = null; - SCLoadingManager.show(context: context); - SCGiftVapSvgaManager().stopPlayback(); - setRoomVisualEffectsEnabled(false); - SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false); - SCHeartbeatUtils.cancelAnchorTimer(); - rtmProvider?.cleanRoomData(); - await engine?.leaveChannel(); - await Future.delayed(Duration(milliseconds: 100)); - await engine?.release(); - await rtmProvider?.quitGroup( - currenRoom!.roomProfile?.roomProfile?.roomAccount ?? "", - ); - await SCAccountRepository().quitRoom( - currenRoom!.roomProfile?.roomProfile?.id ?? "", - ); - _clearData(); - SCLoadingManager.hide(); - if (!isLogout) { - SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home)); - } - } catch (e) { - print('rtc退出房间出错: $e'); - _clearData(); - SCLoadingManager.hide(); - if (!isLogout) { - SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home)); - } - } - } - + rtmProvider?.msgChatListener = null; + rtmProvider?.msgGiftListener = null; + SCLoadingManager.show(context: context); + SCGiftVapSvgaManager().stopPlayback(); + setRoomVisualEffectsEnabled(false); + SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false); + SCHeartbeatUtils.cancelAnchorTimer(); + rtmProvider?.cleanRoomData(); + await engine?.leaveChannel(); + await Future.delayed(Duration(milliseconds: 100)); + await engine?.release(); + await rtmProvider?.quitGroup( + currenRoom!.roomProfile?.roomProfile?.roomAccount ?? "", + ); + await SCAccountRepository().quitRoom( + currenRoom!.roomProfile?.roomProfile?.id ?? "", + ); + _clearData(); + SCLoadingManager.hide(); + if (!isLogout) { + SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home)); + } + } catch (e) { + print('rtc退出房间出错: $e'); + _clearData(); + SCLoadingManager.hide(); + if (!isLogout) { + SCNavigatorUtils.popUntil(context!, ModalRoute.withName(SCRoutes.home)); + } + } + } + Future resetLocalRoomState({RtmProvider? fallbackRtmProvider}) async { rtmProvider ??= fallbackRtmProvider; _stopRoomStatePolling(); try { SCHeartbeatUtils.cancelTimer(); - SCHeartbeatUtils.cancelAnchorTimer(); - final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; - if (groupId.isNotEmpty) { - await rtmProvider?.quitGroup(groupId); - } - await engine?.leaveChannel(); - await Future.delayed(const Duration(milliseconds: 100)); - await engine?.release(); - } catch (e) { - print('rtc清理本地房间状态出错: $e'); - } finally { - engine = null; - rtmProvider?.cleanRoomData(); - _clearData(); - notifyListeners(); - } - } - - ///清空列表数据 + SCHeartbeatUtils.cancelAnchorTimer(); + final groupId = currenRoom?.roomProfile?.roomProfile?.roomAccount ?? ""; + if (groupId.isNotEmpty) { + await rtmProvider?.quitGroup(groupId); + } + await engine?.leaveChannel(); + await Future.delayed(const Duration(milliseconds: 100)); + await engine?.release(); + } catch (e) { + print('rtc清理本地房间状态出错: $e'); + } finally { + engine = null; + rtmProvider?.cleanRoomData(); + _clearData(); + notifyListeners(); + } + } + + ///清空列表数据 void _clearData() { _stopRoomStatePolling(); + engine = null; + _resetHeartbeatTracking(); + _resetLocalAudioRuntimeTracking(); roomRocketStatus = null; - rtmProvider - ?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] = - null; + rtmProvider + ?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] = + null; roomWheatMap.clear(); onlineUsers.clear(); managerUsers.clear(); needUpDataUserInfo = false; - SCRoomUtils.roomUsersMap.clear(); - roomIsMute = false; - rtmProvider?.roomAllMsgList.clear(); - rtmProvider?.roomChatMsgList.clear(); - redPacketList.clear(); - currenRoom = null; - _roomVisualEffectsEnabled = false; - _isExitingCurrentVoiceRoomSession = false; - isMic = true; - isMusicPlaying = false; - DataPersistence.setLastTimeRoomId(""); - SCGiftVapSvgaManager().stopPlayback(); - SCRoomUtils.closeAllDialogs(); - SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults(); - } - - void toggleRemoteAudioMuteForAllUsers() { - roomIsMute = !roomIsMute; - engine?.muteAllRemoteAudioStreams(roomIsMute); - notifyListeners(); - } - - ///点击的位置 - void clickSite(num index, {SocialChatUserProfile? clickUser}) { - if (_handleDirectSeatInteraction(index, clickUser: clickUser)) { - return; - } - - if (index == -1) { - if (clickUser != null) { - if (clickUser.id == - AccountStorage().getCurrentUser()?.userProfile?.id) { - ///是自己,直接打开资料卡 - showBottomInCenterDialog( - context!, - RoomUserInfoCard(userId: clickUser.id), - ); - } else { - showBottomInBottomDialog( - context!, - EmptyMaiSelect(index: index, clickUser: clickUser), - ); - } - } - } else { - SocialChatUserProfile? roomWheatUser = roomWheatMap[index]?.user; - showBottomInBottomDialog( - context!, - EmptyMaiSelect(index: index, clickUser: roomWheatUser), - ); - // if (roomWheatUser != null) { - // ///麦上有人 - // if (roomWheatUser.id == - // AccountStorage().getCurrentUser()?.userProfile?.id) { - // ///是自己 - // showBottomInBottomDialog( - // context!, - // EmptyMaiSelect(index: index, clickUser: roomWheatUser), - // ); - // } else { - // showBottomInBottomDialog( - // context!, - // RoomUserInfoCard(userId: roomWheatUser.id), - // ); - // } - // } else { - // showBottomInBottomDialog( - // context!, - // EmptyMaiSelect(index: index, clickUser: roomWheatUser), - // ); - // } - } - } - + SCRoomUtils.roomUsersMap.clear(); + _clearSelfMicSwitchGuard(clearPreferredIndex: true); + roomIsMute = false; + rtmProvider?.roomAllMsgList.clear(); + rtmProvider?.roomChatMsgList.clear(); + redPacketList.clear(); + currenRoom = null; + _roomVisualEffectsEnabled = false; + _isExitingCurrentVoiceRoomSession = false; + isMic = true; + isMusicPlaying = false; + DataPersistence.setLastTimeRoomId(""); + SCGiftVapSvgaManager().stopPlayback(); + SCRoomUtils.closeAllDialogs(); + SCGlobalConfig.resetVisualEffectSwitchesToRecommendedDefaults(); + } + + void toggleRemoteAudioMuteForAllUsers() { + roomIsMute = !roomIsMute; + engine?.muteAllRemoteAudioStreams(roomIsMute); + notifyListeners(); + } + + ///点击的位置 + void clickSite(num index, {SocialChatUserProfile? clickUser}) { + if (_handleDirectSeatInteraction(index, clickUser: clickUser)) { + return; + } + + if (index == -1) { + if (clickUser != null) { + if (clickUser.id == + AccountStorage().getCurrentUser()?.userProfile?.id) { + ///是自己,直接打开资料卡 + showBottomInCenterDialog( + context!, + RoomUserInfoCard(userId: clickUser.id), + ); + } else { + showBottomInBottomDialog( + context!, + EmptyMaiSelect(index: index, clickUser: clickUser), + ); + } + } + } else { + SocialChatUserProfile? roomWheatUser = roomWheatMap[index]?.user; + showBottomInBottomDialog( + context!, + EmptyMaiSelect(index: index, clickUser: roomWheatUser), + ); + // if (roomWheatUser != null) { + // ///麦上有人 + // if (roomWheatUser.id == + // AccountStorage().getCurrentUser()?.userProfile?.id) { + // ///是自己 + // showBottomInBottomDialog( + // context!, + // EmptyMaiSelect(index: index, clickUser: roomWheatUser), + // ); + // } else { + // showBottomInBottomDialog( + // context!, + // RoomUserInfoCard(userId: roomWheatUser.id), + // ); + // } + // } else { + // showBottomInBottomDialog( + // context!, + // EmptyMaiSelect(index: index, clickUser: roomWheatUser), + // ); + // } + } + } + bool _handleDirectSeatInteraction( num index, { SocialChatUserProfile? clickUser, }) { - final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id; - final isRoomAdmin = isFz() || isGL(); - - if (index == -1) { - if (clickUser == null || (clickUser.id ?? '').isEmpty) { - return false; - } - - if (clickUser.id == currentUserId || !isRoomAdmin) { - _openRoomUserInfoCard(clickUser.id); - return true; - } - return false; - } - - final seat = roomWheatMap[index]; - final seatUser = seat?.user; - if (seatUser == null) { - if (!(seat?.micLock ?? false) && !isRoomAdmin) { - shangMai(index); - return true; - } - return false; + final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id; + final isRoomAdmin = isFz() || isGL(); + + if (index == -1) { + if (clickUser == null || (clickUser.id ?? '').isEmpty) { + return false; + } + + if (clickUser.id == currentUserId || !isRoomAdmin) { + _openRoomUserInfoCard(clickUser.id); + return true; + } + return false; + } + + final seat = roomWheatMap[index]; + final seatUser = seat?.user; + if (seatUser == null) { + if (!(seat?.micLock ?? false) && !isRoomAdmin) { + shangMai(index); + return true; + } + return false; } if (seatUser.id == currentUserId) { @@ -966,87 +1317,126 @@ class RealTimeCommunicationManager extends ChangeNotifier { _openRoomUserInfoCard(seatUser.id); return true; } - - if (!isRoomAdmin && (seatUser.id ?? '').isNotEmpty) { - _openRoomUserInfoCard(seatUser.id); - return true; - } - + + if (!isRoomAdmin && (seatUser.id ?? '').isNotEmpty) { + _openRoomUserInfoCard(seatUser.id); + return true; + } + return false; } void _refreshMicListSilently() { retrieveMicrophoneList(notifyIfUnchanged: false).catchError((_) {}); } - - void _openRoomUserInfoCard(String? userId) { - final normalizedUserId = (userId ?? '').trim(); - if (normalizedUserId.isEmpty) { - return; - } - showBottomInCenterDialog( - context!, - RoomUserInfoCard(userId: normalizedUserId), - ); - } - - addSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) { - _onSoundVoiceChangeList.add(onSoundVoiceChange); - print('添加监听:${_onSoundVoiceChangeList.length}'); - } - - removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) { - _onSoundVoiceChangeList.remove(onSoundVoiceChange); - print('删除监听:${_onSoundVoiceChangeList.length}'); - } - + + void _clearUserFromSeats(String? userId, {num? exceptIndex}) { + final normalizedUserId = (userId ?? "").trim(); + if (normalizedUserId.isEmpty) { + return; + } + roomWheatMap.forEach((seatIndex, seat) { + if (seat.user?.id != normalizedUserId || seatIndex == exceptIndex) { + return; + } + roomWheatMap[seatIndex] = seat.copyWith(clearUser: true); + }); + } + + void _openRoomUserInfoCard(String? userId) { + final normalizedUserId = (userId ?? '').trim(); + if (normalizedUserId.isEmpty) { + return; + } + showBottomInCenterDialog( + context!, + RoomUserInfoCard(userId: normalizedUserId), + ); + } + + addSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) { + _onSoundVoiceChangeList.add(onSoundVoiceChange); + print('添加监听:${_onSoundVoiceChangeList.length}'); + } + + removeSoundVoiceChangeListener(OnSoundVoiceChange onSoundVoiceChange) { + _onSoundVoiceChangeList.remove(onSoundVoiceChange); + print('删除监听:${_onSoundVoiceChangeList.length}'); + } + void shangMai(num index, {String? eventType, String? inviterId}) async { var myUser = AccountStorage().getCurrentUser()?.userProfile; - if (currenRoom?.roomProfile?.roomSetting?.touristMike ?? false) { - } else { - if (isTourists()) { - SCTts.show( - SCAppLocalizations.of(context!)!.touristsAreNotAllowedToGoOnTheMic, - ); - return; - } - } - - try { - var micGoUpRes = await SCChatRoomRepository().micGoUp( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - index, - eventType: eventType, - inviterId: inviterId, - ); - - /// 设置成主播角色 - engine?.renewToken(micGoUpRes.roomToken ?? ""); + if (currenRoom?.roomProfile?.roomSetting?.touristMike ?? false) { + } else { + if (isTourists()) { + SCTts.show( + SCAppLocalizations.of(context!)!.touristsAreNotAllowedToGoOnTheMic, + ); + return; + } + } + + try { + var micGoUpRes = await SCChatRoomRepository().micGoUp( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + index, + eventType: eventType, + inviterId: inviterId, + ); + + /// 设置成主播角色 + engine?.renewToken(micGoUpRes.roomToken ?? ""); if (!micGoUpRes.micMute!) { if (!isMic) { engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); adjustRecordingSignalVolume(100); await engine?.muteLocalAudioStream(false); - } - } else { - engine?.setClientRole(role: ClientRoleType.clientRoleAudience); - await engine?.muteLocalAudioStream(true); - } + } + } else { + engine?.setClientRole(role: ClientRoleType.clientRoleAudience); + await engine?.muteLocalAudioStream(true); + } SCHeartbeatUtils.scheduleAnchorHeartbeat( currenRoom?.roomProfile?.roomProfile?.id ?? "", ); - final currentSeat = roomWheatMap[index]; - if (currentSeat != null && myUser != null) { - roomWheatMap[index] = currentSeat.copyWith( - user: myUser.copyWith(roles: currenRoom?.entrants?.roles), - micMute: micGoUpRes.micMute, - roomToken: micGoUpRes.roomToken, - ); + final targetIndex = micGoUpRes.micIndex ?? index; + final previousSelfSeatIndex = + myUser != null ? userOnMaiInIndex(myUser.id ?? "") : -1; + final isSeatSwitching = + previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex; + if (myUser != null) { + if (isSeatSwitching) { + _startSelfMicSwitchGuard( + sourceIndex: previousSelfSeatIndex, + targetIndex: targetIndex, + ); + } else { + _preferredSelfMicIndex = targetIndex; + _clearSelfMicSwitchGuard(); + } + _clearUserFromSeats(myUser.id, exceptIndex: targetIndex); + final currentSeat = roomWheatMap[targetIndex]; + final currentUser = myUser.copyWith(roles: currenRoom?.entrants?.roles); + roomWheatMap[targetIndex] = + currentSeat?.copyWith( + user: currentUser, + micMute: micGoUpRes.micMute, + micLock: micGoUpRes.micLock, + roomToken: micGoUpRes.roomToken, + ) ?? + MicRes( + roomId: currenRoom?.roomProfile?.roomProfile?.id, + micIndex: targetIndex, + micLock: micGoUpRes.micLock, + micMute: micGoUpRes.micMute, + user: currentUser, + roomToken: micGoUpRes.roomToken, + ); } - if (roomWheatMap[index]?.micMute ?? false) { + if (roomWheatMap[targetIndex]?.micMute ?? false) { ///房主上麦自动解禁麦位 if (isFz()) { - await jieJinMai(index); + await jieJinMai(targetIndex); } } @@ -1069,192 +1459,182 @@ class RealTimeCommunicationManager extends ChangeNotifier { isMic = true; engine?.muteLocalAudioStream(true); } + _clearSelfMicSwitchGuard(clearPreferredIndex: true); SCHeartbeatUtils.cancelAnchorTimer(); /// 设置成主持人角色 engine?.renewToken(""); engine?.setClientRole(role: ClientRoleType.clientRoleAudience); - final currentSeat = roomWheatMap[index]; - if (currentSeat != null) { - roomWheatMap[index] = currentSeat.copyWith(user: null); - } + _clearUserFromSeats(AccountStorage().getCurrentUser()?.userProfile?.id); notifyListeners(); _refreshMicListSilently(); } catch (ex) { SCTts.show('Failed to leave the microphone, $ex'); } } - - ///踢人下麦 - killXiaMai(String userId) { - try { - roomWheatMap.forEach((k, v) async { - if (v.user?.id == userId) { - var canKill = await SCChatRoomRepository().kickOffMicrophone( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - userId, - ); - if (canKill) { - await SCChatRoomRepository().micKill( - currenRoom?.roomProfile?.roomProfile?.id ?? "", - k, - ); - Provider.of(context!, listen: false).dispatchMessage( - Msg( - groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount, - msg: userId, - type: SCRoomMsgType.killXiaMai, - ), - addLocal: false, - ); - roomWheatMap[k]?.setUser = null; - notifyListeners(); - } else { - SCTts.show("cant kill the user"); - } - } - }); - } catch (e) { - SCTts.show("kill the user fail"); - print('踢人下麦失败'); - } - } - - ///自己是否在麦上 - bool isOnMai() { - return roomWheatMap.values - .map((userWheat) => userWheat.user?.id) - .toList() - .contains(AccountStorage().getCurrentUser()?.userProfile?.id); - } - - ///自己是否在指定的麦上 - bool isOnMaiInIndex(num index) { - return roomWheatMap[index]?.user?.id == - AccountStorage().getCurrentUser()?.userProfile?.id; - } - - ///点击的用户在哪个麦上 - num userOnMaiInIndex(String userId) { - num index = -1; - roomWheatMap.forEach((k, value) { - if (value.user?.id == userId) { - index = k; - } - }); - return index; - } - - ///座位上的用户是否开麦 - bool userOnMaiInIndexOfMute(String userId) { - bool micMute = false; - roomWheatMap.forEach((k, value) { - if (value.user?.id == userId) { - value.micMute; - } - }); - return micMute; - } - - ///是否是房主 - bool isFz() { - return currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name; - } - - ///是否是管理 - bool isGL() { - return currenRoom?.entrants?.roles == SCRoomRolesType.ADMIN.name; - } - - ///是否是会员 - bool isHY() { - return currenRoom?.entrants?.roles == SCRoomRolesType.MEMBER.name; - } - - ///是否是游客 - bool isTourists() { - return currenRoom?.entrants?.roles == SCRoomRolesType.TOURIST.name; - } - - ///麦位变动 + + ///踢人下麦 + killXiaMai(String userId) { + try { + roomWheatMap.forEach((k, v) async { + if (v.user?.id == userId) { + var canKill = await SCChatRoomRepository().kickOffMicrophone( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + userId, + ); + if (canKill) { + await SCChatRoomRepository().micKill( + currenRoom?.roomProfile?.roomProfile?.id ?? "", + k, + ); + Provider.of(context!, listen: false).dispatchMessage( + Msg( + groupId: currenRoom?.roomProfile?.roomProfile?.roomAccount, + msg: userId, + type: SCRoomMsgType.killXiaMai, + ), + addLocal: false, + ); + roomWheatMap[k]?.setUser = null; + notifyListeners(); + } else { + SCTts.show("cant kill the user"); + } + } + }); + } catch (e) { + SCTts.show("kill the user fail"); + print('踢人下麦失败'); + } + } + + ///自己是否在麦上 + bool isOnMai() { + return roomWheatMap.values + .map((userWheat) => userWheat.user?.id) + .toList() + .contains(AccountStorage().getCurrentUser()?.userProfile?.id); + } + + ///自己是否在指定的麦上 + bool isOnMaiInIndex(num index) { + return roomWheatMap[index]?.user?.id == + AccountStorage().getCurrentUser()?.userProfile?.id; + } + + ///点击的用户在哪个麦上 + num userOnMaiInIndex(String userId) { + num index = -1; + roomWheatMap.forEach((k, value) { + if (value.user?.id == userId) { + index = k; + } + }); + return index; + } + + ///座位上的用户是否开麦 + bool userOnMaiInIndexOfMute(String userId) { + bool micMute = false; + roomWheatMap.forEach((k, value) { + if (value.user?.id == userId) { + value.micMute; + } + }); + return micMute; + } + + ///是否是房主 + bool isFz() { + return currenRoom?.entrants?.roles == SCRoomRolesType.HOMEOWNER.name; + } + + ///是否是管理 + bool isGL() { + return currenRoom?.entrants?.roles == SCRoomRolesType.ADMIN.name; + } + + ///是否是会员 + bool isHY() { + return currenRoom?.entrants?.roles == SCRoomRolesType.MEMBER.name; + } + + ///是否是游客 + bool isTourists() { + return currenRoom?.entrants?.roles == SCRoomRolesType.TOURIST.name; + } + + ///麦位变动 void micChange(List? mics) { if (mics == null || mics.isEmpty) { return; } _applyMicSnapshot(_buildMicMap(mics)); - final isOnMic = - userOnMaiInIndex(AccountStorage().getCurrentUser()?.userProfile?.id ?? "") > - -1; - SCHeartbeatUtils.scheduleHeartbeat( - SCHeartbeatStatus.VOICE_LIVE.name, - isOnMic, - roomId: currenRoom?.roomProfile?.roomProfile?.id, - ); } - - ///找一个空麦位 -1表示没有空位 - num findWheat() { - for (var entry in roomWheatMap.entries) { - if (entry.value.user == null && !entry.value.micLock!) { - return entry.key; - } - } - return -1; - } - - Future loadRoomInfo(String roomId) async { - try { - debugPrint("[Room Cover Sync] loadRoomInfo start roomId=$roomId"); - final value = await SCChatRoomRepository().specific(roomId); - final currentRoomProfile = currenRoom?.roomProfile?.roomProfile; - debugPrint( - "[Room Cover Sync] loadRoomInfo fetched roomId=${value.id ?? roomId} roomCover=${value.roomCover ?? ""} roomName=${value.roomName ?? ""}", - ); - currenRoom = currenRoom?.copyWith( - roomProfile: currenRoom?.roomProfile?.copyWith( - roomCounter: value.counter ?? currenRoom?.roomProfile?.roomCounter, - roomSetting: value.setting ?? currenRoom?.roomProfile?.roomSetting, - roomProfile: currentRoomProfile?.copyWith( - roomCover: _preferNonEmpty( - value.roomCover, - currentRoomProfile.roomCover, - ), - roomName: _preferNonEmpty( - value.roomName, - currentRoomProfile.roomName, - ), - roomDesc: _preferNonEmpty( - value.roomDesc, - currentRoomProfile.roomDesc, - ), - ), - ), - ); - debugPrint( - "[Room Cover Sync] loadRoomInfo applied roomId=$roomId roomCover=${currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""}", - ); - - ///如果是游客禁止上麦,已经在麦上的游客需要下麦 - bool touristMike = - currenRoom?.roomProfile?.roomSetting?.touristMike ?? false; - if (!touristMike && isTourists()) { - num index = userOnMaiInIndex( - AccountStorage().getCurrentUser()?.userProfile?.id ?? "", - ); - if (index > -1) { - xiaMai(index); - } - } - notifyListeners(); - return true; - } catch (e) { - await resetLocalRoomState(fallbackRtmProvider: rtmProvider); - SCTts.show("Room not exists"); - return false; - } - } - - ///解锁麦位 + + ///找一个空麦位 -1表示没有空位 + num findWheat() { + for (var entry in roomWheatMap.entries) { + if (entry.value.user == null && !entry.value.micLock!) { + return entry.key; + } + } + return -1; + } + + Future loadRoomInfo(String roomId) async { + try { + debugPrint("[Room Cover Sync] loadRoomInfo start roomId=$roomId"); + final value = await SCChatRoomRepository().specific(roomId); + final currentRoomProfile = currenRoom?.roomProfile?.roomProfile; + debugPrint( + "[Room Cover Sync] loadRoomInfo fetched roomId=${value.id ?? roomId} roomCover=${value.roomCover ?? ""} roomName=${value.roomName ?? ""}", + ); + currenRoom = currenRoom?.copyWith( + roomProfile: currenRoom?.roomProfile?.copyWith( + roomCounter: value.counter ?? currenRoom?.roomProfile?.roomCounter, + roomSetting: value.setting ?? currenRoom?.roomProfile?.roomSetting, + roomProfile: currentRoomProfile?.copyWith( + roomCover: _preferNonEmpty( + value.roomCover, + currentRoomProfile.roomCover, + ), + roomName: _preferNonEmpty( + value.roomName, + currentRoomProfile.roomName, + ), + roomDesc: _preferNonEmpty( + value.roomDesc, + currentRoomProfile.roomDesc, + ), + ), + ), + ); + debugPrint( + "[Room Cover Sync] loadRoomInfo applied roomId=$roomId roomCover=${currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""}", + ); + + ///如果是游客禁止上麦,已经在麦上的游客需要下麦 + bool touristMike = + currenRoom?.roomProfile?.roomSetting?.touristMike ?? false; + if (!touristMike && isTourists()) { + num index = userOnMaiInIndex( + AccountStorage().getCurrentUser()?.userProfile?.id ?? "", + ); + if (index > -1) { + xiaMai(index); + } + } + notifyListeners(); + return true; + } catch (e) { + await resetLocalRoomState(fallbackRtmProvider: rtmProvider); + SCTts.show("Room not exists"); + return false; + } + } + + ///解锁麦位 Future jieFeng(num index) async { await SCChatRoomRepository().micLock( currenRoom?.roomProfile?.roomProfile?.id ?? "", @@ -1315,60 +1695,60 @@ class RealTimeCommunicationManager extends ChangeNotifier { currenRoom?.roomProfile?.roomProfile?.id ?? "", index, false, - ); - if (isOnMaiInIndex(index)) { - if (!Provider.of(context!, listen: false).isMic) { - if (roomWheatMap[index]?.user != null) { - adjustRecordingSignalVolume(100); - Provider.of( - context!, - listen: false, - ).engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); - Provider.of( - context!, - listen: false, - ).engine?.muteLocalAudioStream(false); - } - } - } - - var mic = roomWheatMap[index]; + ); + if (isOnMaiInIndex(index)) { + if (!Provider.of(context!, listen: false).isMic) { + if (roomWheatMap[index]?.user != null) { + adjustRecordingSignalVolume(100); + Provider.of( + context!, + listen: false, + ).engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster); + Provider.of( + context!, + listen: false, + ).engine?.muteLocalAudioStream(false); + } + } + } + + var mic = roomWheatMap[index]; if (mic != null) { roomWheatMap[index] = mic.copyWith(micMute: false); notifyListeners(); } _refreshMicListSilently(); } - - void addOnlineUser(String groupId, SocialChatUserProfile user) { - if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) { - return; - } - bool isExtOnlineList = false; - for (var us in onlineUsers) { - if (us.id == user.id) { - isExtOnlineList = true; - break; - } - } + + void addOnlineUser(String groupId, SocialChatUserProfile user) { + if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) { + return; + } + bool isExtOnlineList = false; + for (var us in onlineUsers) { + if (us.id == user.id) { + isExtOnlineList = true; + break; + } + } if (!isExtOnlineList) { Provider.of(context!, listen: false).onlineUsers.add(user); _refreshManagerUsers(onlineUsers); notifyListeners(); } } - - void removOnlineUser(String groupId, String userId) { - if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) { - return; - } - SocialChatUserProfile? isExtOnlineUser; - for (var us in onlineUsers) { - if (us.id == userId) { - isExtOnlineUser = us; - break; - } - } + + void removOnlineUser(String groupId, String userId) { + if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) { + return; + } + SocialChatUserProfile? isExtOnlineUser; + for (var us in onlineUsers) { + if (us.id == userId) { + isExtOnlineUser = us; + break; + } + } if (isExtOnlineUser != null) { Provider.of( context!, @@ -1378,66 +1758,66 @@ class RealTimeCommunicationManager extends ChangeNotifier { notifyListeners(); } } - - void starPlayEmoji(Msg msg) { - if (msg.number! > -1) { - var mic = roomWheatMap[msg.number]; - if (mic != null && mic.user != null) { - roomWheatMap[msg.number!] = mic.copyWith( - emojiPath: msg.msg, - type: msg.type, - number: msg.msg, - ); - notifyListeners(); - } - } - } - - ///更新房间背景 - void updateRoomBG(SCRoomThemeListRes res) { - var roomTheme = RoomTheme( - expireTime: res.expireTime, - id: res.id, - themeStatus: res.themeStatus, - themeBack: res.themeBack, - ); - currenRoom?.roomProps?.setRoomTheme(roomTheme); - notifyListeners(); - } - - void updateRoomSetting(RoomSetting roomSetting) { - currenRoom?.roomProfile?.setRoomSetting(roomSetting); - notifyListeners(); - } - - Map>> seatGlobalKeyMap = {}; - - void bindTargetKey(num index, GlobalKey> targetKey) { - seatGlobalKeyMap[index] = targetKey; - } - - GlobalKey>? getSeatGlobalKeyByIndex(String userId) { - ///需要有人的座位 - num index = userOnMaiInIndex(userId); - if (index > -1) { - return seatGlobalKeyMap[index]; - } - return null; - } - - ///播放音乐 - void startAudioMixing(String localPath, int cycle) { - if (localPath.isEmpty) { - return; - } - engine?.startAudioMixing( - filePath: localPath, - loopback: false, - cycle: cycle, - ); - } - - void adjustRecordingSignalVolume(int volume) { - engine?.adjustRecordingSignalVolume(volume); - } -} + + void starPlayEmoji(Msg msg) { + if (msg.number! > -1) { + var mic = roomWheatMap[msg.number]; + if (mic != null && mic.user != null) { + roomWheatMap[msg.number!] = mic.copyWith( + emojiPath: msg.msg, + type: msg.type, + number: msg.msg, + ); + notifyListeners(); + } + } + } + + ///更新房间背景 + void updateRoomBG(SCRoomThemeListRes res) { + var roomTheme = RoomTheme( + expireTime: res.expireTime, + id: res.id, + themeStatus: res.themeStatus, + themeBack: res.themeBack, + ); + currenRoom?.roomProps?.setRoomTheme(roomTheme); + notifyListeners(); + } + + void updateRoomSetting(RoomSetting roomSetting) { + currenRoom?.roomProfile?.setRoomSetting(roomSetting); + notifyListeners(); + } + + Map>> seatGlobalKeyMap = {}; + + void bindTargetKey(num index, GlobalKey> targetKey) { + seatGlobalKeyMap[index] = targetKey; + } + + GlobalKey>? getSeatGlobalKeyByIndex(String userId) { + ///需要有人的座位 + num index = userOnMaiInIndex(userId); + if (index > -1) { + return seatGlobalKeyMap[index]; + } + return null; + } + + ///播放音乐 + void startAudioMixing(String localPath, int cycle) { + if (localPath.isEmpty) { + return; + } + engine?.startAudioMixing( + filePath: localPath, + loopback: false, + cycle: cycle, + ); + } + + void adjustRecordingSignalVolume(int volume) { + engine?.adjustRecordingSignalVolume(volume); + } +} diff --git a/lib/services/audio/rtm_manager.dart b/lib/services/audio/rtm_manager.dart index ad792a0..fdcece2 100644 --- a/lib/services/audio/rtm_manager.dart +++ b/lib/services/audio/rtm_manager.dart @@ -48,7 +48,8 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_config_reposito import 'package:yumi/shared/data_sources/models/message/big_broadcast_group_message.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart'; -import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart'; +import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart' + hide Data; import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart'; @@ -72,6 +73,9 @@ typedef RtmProvider = RealTimeMessagingManager; class RealTimeMessagingManager extends ChangeNotifier { static const int _giftComboMergeWindowMs = 3000; static const int _maxLuckGiftPushQueueLength = 12; + static const int _luckyGiftFloatMinMultiple = 5; + static const int _luckyGiftBurstMinMultiple = 10; + static const int _luckyGiftBurstMinAwardAmount = 5000; BuildContext? context; @@ -119,6 +123,7 @@ class RealTimeMessagingManager extends ChangeNotifier { int notifcationUnReadCount = 0; SCBroadCastLuckGiftPush? currentPlayingLuckGift; final Queue _luckGiftPushQueue = Queue(); + String? _currentLuckGiftPushKey; Debouncer debouncer = Debouncer(); List conversationList = []; @@ -534,6 +539,7 @@ class RealTimeMessagingManager extends ChangeNotifier { Future joinRoomGroup(String groupID, String message) async { _luckGiftPushQueue.clear(); currentPlayingLuckGift = null; + _currentLuckGiftPushKey = null; var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( groupID: groupID, message: message, @@ -1025,7 +1031,14 @@ class RealTimeMessagingManager extends ChangeNotifier { required String source, }) { final rewardData = broadCastRes.data; - if (rewardData == null || !rewardData.shouldShowGlobalNews) { + if (rewardData == null) { + return; + } + if (_isLuckyGiftInCurrentRoom(broadCastRes)) { + addluckGiftPushQueue(broadCastRes); + } + if (!rewardData.shouldShowGlobalNews || + (rewardData.multiple ?? 0) < _luckyGiftFloatMinMultiple) { return; } if (source == 'broadcast' && _isLuckyGiftInCurrentRoom(broadCastRes)) { @@ -1428,93 +1441,103 @@ class RealTimeMessagingManager extends ChangeNotifier { } } else if (msg.type == SCRoomMsgType.gift) { final gift = msg.gift; - 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( - 'recv gift msg ' - 'fromUserId=${msg.user?.id} ' - 'fromUserName=${msg.user?.userNickname} ' - 'toUserId=${msg.toUser?.id} ' - 'toUserName=${msg.toUser?.userNickname} ' - 'giftId=${gift?.id} ' - 'giftName=${gift?.giftName} ' - 'giftSourceUrl=$giftSourceUrl ' - 'special=$special ' - 'hasSource=$hasSource ' - 'hasAnimation=$hasAnimation ' - 'hasGlobalGift=$hasGlobalGift ' - 'hasFullScreenEffect=$hasFullScreenEffect ' - 'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}', - ); - if (msg.gift!.giftSourceUrl != null && msg.gift!.special != null) { - if (scGiftHasFullScreenEffect(msg.gift!.special)) { - if (SCGlobalConfig.isGiftSpecialEffects && - Provider.of( - context!, - listen: false, - ).shouldShowRoomVisualEffects) { - _giftFxLog( - 'trigger player play path=${msg.gift!.giftSourceUrl} ' - 'giftId=${msg.gift?.id} giftName=${msg.gift?.giftName}', - ); - SCGiftVapSvgaManager().play(msg.gift!.giftSourceUrl!); + if (gift == null) { + _giftFxLog( + 'recv gift msg skipped reason=no_gift ' + 'fromUserId=${msg.user?.id} ' + 'toUserId=${msg.toUser?.id} ' + 'quantity=${msg.number}', + ); + } else { + final rtcProvider = Provider.of( + context!, + listen: false, + ); + 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( + 'recv gift msg ' + 'fromUserId=${msg.user?.id} ' + 'fromUserName=${msg.user?.userNickname} ' + 'toUserId=${msg.toUser?.id} ' + 'toUserName=${msg.toUser?.userNickname} ' + 'giftId=${gift.id} ' + 'giftName=${gift.giftName} ' + 'giftSourceUrl=$giftSourceUrl ' + 'special=$special ' + 'hasSource=$hasSource ' + 'hasAnimation=$hasAnimation ' + 'hasGlobalGift=$hasGlobalGift ' + 'hasFullScreenEffect=$hasFullScreenEffect ' + 'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}', + ); + if (giftSourceUrl.isNotEmpty && special.isNotEmpty) { + if (scGiftHasFullScreenEffect(special)) { + if (SCGlobalConfig.isGiftSpecialEffects && + rtcProvider.shouldShowRoomVisualEffects) { + _giftFxLog( + 'trigger player play path=$giftSourceUrl ' + 'giftId=${gift.id} giftName=${gift.giftName}', + ); + SCGiftVapSvgaManager().play(giftSourceUrl); + } else { + _giftFxLog( + 'skip player play because visual effects disabled ' + 'giftId=${gift.id} ' + 'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} ' + 'roomVisible=${rtcProvider.shouldShowRoomVisualEffects}', + ); + } } else { _giftFxLog( - 'skip player play because visual effects disabled ' - 'giftId=${msg.gift?.id} ' - 'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} ' - 'roomVisible=${Provider.of(context!, listen: false).shouldShowRoomVisualEffects}', + 'skip player play because special does not include ' + '${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} ' + 'giftId=${gift.id} special=${gift.special}', ); } } else { _giftFxLog( - 'skip player play because special does not include ' - '${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} ' - 'giftId=${msg.gift?.id} special=${msg.gift?.special}', + 'skip player play because giftSourceUrl or special is empty ' + 'giftId=${gift.id} ' + 'giftSourceUrl=${gift.giftSourceUrl} ' + 'special=${gift.special}', + ); + } + if (rtcProvider + .currenRoom + ?.roomProfile + ?.roomSetting + ?.showHeartbeat ?? + false) { + debouncer.debounce( + duration: Duration(milliseconds: 350), + onDebounce: () { + rtcProvider.requestGiftTriggeredMicRefresh(); + }, + ); + } + final coins = (msg.number ?? 0) * (gift.giftCandy ?? 0); + if (coins > 9999) { + OverlayManager().addMessage( + SCFloatingMessage( + type: 1, + userAvatarUrl: msg.user?.userAvatar ?? "", + userName: msg.user?.userNickname ?? "", + toUserName: msg.toUser?.userNickname ?? "", + toUserAvatarUrl: msg.toUser?.userAvatar ?? "", + giftUrl: gift.giftPhoto, + number: msg.number, + coins: coins, + roomId: msg.msg, + ), ); } - } else { - _giftFxLog( - 'skip player play because giftSourceUrl or special is null ' - 'giftId=${msg.gift?.id} ' - 'giftSourceUrl=${msg.gift?.giftSourceUrl} ' - 'special=${msg.gift?.special}', - ); - } - if (Provider.of( - context!, - listen: false, - ).currenRoom?.roomProfile?.roomSetting?.showHeartbeat ?? - false) { - debouncer.debounce( - duration: Duration(milliseconds: 350), - onDebounce: () { - Provider.of( - context!, - listen: false, - ).retrieveMicrophoneList(); - }, - ); - } - num coins = msg.number! * msg.gift!.giftCandy!; - if (coins > 9999) { - OverlayManager().addMessage( - SCFloatingMessage( - type: 1, - userAvatarUrl: msg.user?.userAvatar ?? "", - userName: msg.user?.userNickname ?? "", - toUserName: msg.toUser?.userNickname ?? "", - toUserAvatarUrl: msg.toUser?.userAvatar ?? "", - giftUrl: msg.gift!.giftPhoto, - number: msg.number, - coins: coins, - roomId: msg.msg, - ), - ); } } else if (msg.type == SCRoomMsgType.luckGiftAnimOther) { final hideLGiftAnimal = @@ -1686,6 +1709,7 @@ class RealTimeMessagingManager extends ChangeNotifier { roomChatMsgList.clear(); _luckGiftPushQueue.clear(); currentPlayingLuckGift = null; + _currentLuckGiftPushKey = null; onNewMessageListenerGroupMap.forEach((k, v) { v = null; }); @@ -1694,6 +1718,20 @@ class RealTimeMessagingManager extends ChangeNotifier { void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) { if (SCGlobalConfig.isLuckGiftSpecialEffects) { + if (!shouldPlayLuckyGiftBurst(broadCastRes.data)) { + return; + } + final eventKey = _luckyGiftPushEventKey(broadCastRes); + if (eventKey.isNotEmpty) { + if (_currentLuckGiftPushKey == eventKey) { + return; + } + for (final queued in _luckGiftPushQueue) { + if (queued != null && _luckyGiftPushEventKey(queued) == eventKey) { + return; + } + } + } while (_luckGiftPushQueue.length >= _maxLuckGiftPushQueueLength) { _luckGiftPushQueue.removeFirst(); } @@ -1702,8 +1740,19 @@ class RealTimeMessagingManager extends ChangeNotifier { } } + static bool shouldPlayLuckyGiftBurst(Data? rewardData) { + if (rewardData == null) { + return false; + } + final awardAmount = rewardData.awardAmount ?? 0; + final multiple = rewardData.multiple ?? 0; + return awardAmount > _luckyGiftBurstMinAwardAmount || + multiple >= _luckyGiftBurstMinMultiple; + } + void cleanLuckGiftBackCoins() { _luckGiftPushQueue.clear(); + _currentLuckGiftPushKey = null; } void playLuckGiftBackCoins() { @@ -1711,14 +1760,34 @@ class RealTimeMessagingManager extends ChangeNotifier { return; } currentPlayingLuckGift = _luckGiftPushQueue.removeFirst(); + _currentLuckGiftPushKey = + currentPlayingLuckGift == null + ? null + : _luckyGiftPushEventKey(currentPlayingLuckGift!); notifyListeners(); Future.delayed(Duration(milliseconds: 3000), () { currentPlayingLuckGift = null; + _currentLuckGiftPushKey = null; notifyListeners(); playLuckGiftBackCoins(); }); } + String _luckyGiftPushEventKey(SCBroadCastLuckGiftPush broadCastRes) { + final rewardData = broadCastRes.data; + if (rewardData == null) { + return ''; + } + return '${rewardData.roomId ?? ""}|' + '${rewardData.giftId ?? ""}|' + '${rewardData.sendUserId ?? ""}|' + '${rewardData.acceptUserId ?? ""}|' + '${rewardData.giftQuantity ?? 0}|' + '${rewardData.awardAmount ?? 0}|' + '${rewardData.multiple ?? 0}|' + '${rewardData.normalizedMultipleType}'; + } + void updateNotificationCount(int count) { notifcationUnReadCount = 0; allUnReadCount = diff --git a/lib/services/general/sc_app_general_manager.dart b/lib/services/general/sc_app_general_manager.dart index 0a3c38f..1b3ee6c 100644 --- a/lib/services/general/sc_app_general_manager.dart +++ b/lib/services/general/sc_app_general_manager.dart @@ -17,6 +17,8 @@ import '../../shared/data_sources/models/enum/sc_banner_type.dart'; import '../../shared/data_sources/models/enum/sc_gift_type.dart'; class SCAppGeneralManager extends ChangeNotifier { + static const int _maxInitialFullScreenGiftPreloads = 4; + List countryModeList = []; final Map> _countryMap = {}; final Map _countryByNameMap = {}; @@ -276,6 +278,8 @@ class SCAppGeneralManager extends ChangeNotifier { } void downLoad(List giftResList) { + final scheduledPaths = {}; + var scheduledCount = 0; for (var gift in giftResList) { final giftSourceUrl = gift.giftSourceUrl ?? ""; if (giftSourceUrl.isEmpty) { @@ -284,7 +288,14 @@ class SCAppGeneralManager extends ChangeNotifier { if (!scGiftHasFullScreenEffect(gift.special)) { continue; } + if (!scheduledPaths.add(giftSourceUrl)) { + continue; + } SCGiftVapSvgaManager().preload(giftSourceUrl); + scheduledCount += 1; + if (scheduledCount >= _maxInitialFullScreenGiftPreloads) { + break; + } } } diff --git a/lib/services/gift/gift_animation_manager.dart b/lib/services/gift/gift_animation_manager.dart index 01810cf..8b93f74 100644 --- a/lib/services/gift/gift_animation_manager.dart +++ b/lib/services/gift/gift_animation_manager.dart @@ -81,6 +81,7 @@ class GiftAnimationManager extends ChangeNotifier { if (target.giftName.isEmpty) { target.giftName = incoming.giftName; } + target.rewardAmount = target.rewardAmount + incoming.rewardAmount; if (incoming.rewardAmountText.isNotEmpty) { target.rewardAmountText = incoming.rewardAmountText; } diff --git a/lib/shared/business_logic/models/res/mic_res.dart b/lib/shared/business_logic/models/res/mic_res.dart index a65331a..7b68ab9 100644 --- a/lib/shared/business_logic/models/res/mic_res.dart +++ b/lib/shared/business_logic/models/res/mic_res.dart @@ -17,7 +17,7 @@ class MicRes { SocialChatUserProfile? user, String? type, String? roomToken, - String?number + String? number, }) { _roomId = roomId; _micIndex = micIndex; @@ -35,7 +35,10 @@ class MicRes { _micIndex = json['micIndex']; _micLock = json['micLock']; _micMute = json['micMute']; - _user = json['user'] != null ? SocialChatUserProfile.fromJson(json['user']) : null; + _user = + json['user'] != null + ? SocialChatUserProfile.fromJson(json['user']) + : null; _roomToken = json['roomToken']; _type = json['type']; _number = json['number']; @@ -59,6 +62,7 @@ class MicRes { bool? micMute, String? type, SocialChatUserProfile? user, + bool clearUser = false, String? roomToken, String? emojiPath, String? number, @@ -67,7 +71,7 @@ class MicRes { micIndex: micIndex ?? _micIndex, micLock: micLock ?? _micLock, micMute: micMute ?? _micMute, - user: user ?? _user, + user: clearUser ? null : user ?? _user, roomToken: roomToken ?? _roomToken, emojiPath: emojiPath ?? _emojiPath, type: type ?? _type, diff --git a/lib/shared/data_sources/models/message/sc_floating_message.dart b/lib/shared/data_sources/models/message/sc_floating_message.dart index d964c27..fa89e5d 100644 --- a/lib/shared/data_sources/models/message/sc_floating_message.dart +++ b/lib/shared/data_sources/models/message/sc_floating_message.dart @@ -13,7 +13,6 @@ class SCFloatingMessage { num? number; num? multiple; int priority = 10; //排序权重 - int aggregateVersion = 0; SCFloatingMessage({ this.type = 0, @@ -30,7 +29,6 @@ class SCFloatingMessage { this.coins = 0, this.priority = 10, this.multiple = 10, - this.aggregateVersion = 0, }); SCFloatingMessage.fromJson(dynamic json) { @@ -48,7 +46,6 @@ class SCFloatingMessage { number = json['number']; priority = json['priority']; multiple = json['multiple']; - aggregateVersion = json['aggregateVersion'] ?? 0; } Map toJson() { @@ -67,7 +64,6 @@ class SCFloatingMessage { map['number'] = number; map['priority'] = priority; map['multiple'] = multiple; - map['aggregateVersion'] = aggregateVersion; return map; } } diff --git a/lib/shared/data_sources/sources/local/floating_screen_manager.dart b/lib/shared/data_sources/sources/local/floating_screen_manager.dart index 59f4b62..5bfd0a0 100644 --- a/lib/shared/data_sources/sources/local/floating_screen_manager.dart +++ b/lib/shared/data_sources/sources/local/floating_screen_manager.dart @@ -20,7 +20,6 @@ class OverlayManager { ); bool _isPlaying = false; OverlayEntry? _currentOverlayEntry; - SCFloatingMessage? _currentMessage; bool _isProcessing = false; bool _isDisposed = false; @@ -34,9 +33,6 @@ class OverlayManager { void addMessage(SCFloatingMessage message) { if (_isDisposed) return; if (SCGlobalConfig.isFloatingAnimationInGlobal) { - if (_tryAggregateMessage(message)) { - return; - } _messageQueue.add(message); _safeScheduleNext(); } else { @@ -47,74 +43,6 @@ class OverlayManager { } } - bool _tryAggregateMessage(SCFloatingMessage incoming) { - if (!_supportsAggregation(incoming)) { - return false; - } - if (_currentMessage != null && - _isSameAggregatedFloatingMessage(_currentMessage!, incoming)) { - _mergeFloatingMessage(_currentMessage!, incoming); - _currentOverlayEntry?.markNeedsBuild(); - return true; - } - for (final queuedMessage in _messageQueue.unorderedElements) { - if (_isSameAggregatedFloatingMessage(queuedMessage, incoming)) { - _mergeFloatingMessage(queuedMessage, incoming); - return true; - } - } - return false; - } - - bool _supportsAggregation(SCFloatingMessage message) { - return message.type == 0; - } - - bool _isSameAggregatedFloatingMessage( - SCFloatingMessage existing, - SCFloatingMessage incoming, - ) { - return existing.type == incoming.type && - existing.roomId == incoming.roomId && - existing.userId == incoming.userId && - existing.toUserId == incoming.toUserId && - existing.giftUrl == incoming.giftUrl; - } - - void _mergeFloatingMessage( - SCFloatingMessage target, - SCFloatingMessage incoming, - ) { - target.coins = (target.coins ?? 0) + (incoming.coins ?? 0); - target.number = (target.number ?? 0) + (incoming.number ?? 0); - final currentMultiple = target.multiple ?? 0; - final incomingMultiple = incoming.multiple ?? 0; - target.multiple = - currentMultiple >= incomingMultiple - ? currentMultiple - : incomingMultiple; - if ((target.userAvatarUrl ?? '').isEmpty) { - target.userAvatarUrl = incoming.userAvatarUrl; - } - if ((target.userName ?? '').isEmpty) { - target.userName = incoming.userName; - } - if ((target.toUserName ?? '').isEmpty) { - target.toUserName = incoming.toUserName; - } - if ((target.giftUrl ?? '').isEmpty) { - target.giftUrl = incoming.giftUrl; - } - if ((target.roomId ?? '').isEmpty) { - target.roomId = incoming.roomId; - } - target.priority = - target.priority >= incoming.priority - ? target.priority - : incoming.priority; - target.aggregateVersion += 1; - } - void _safeScheduleNext() { if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return; _isProcessing = true; @@ -162,11 +90,9 @@ class OverlayManager { void _playMessage(SCFloatingMessage message) { _isPlaying = true; - _currentMessage = message; final context = navigatorKey.currentState?.context; if (context == null || !context.mounted) { _isPlaying = false; - _currentMessage = null; _safeScheduleNext(); return; } @@ -195,12 +121,10 @@ class OverlayManager { try { _currentOverlayEntry?.remove(); _currentOverlayEntry = null; - _currentMessage = null; _isPlaying = false; _safeScheduleNext(); } catch (e) { debugPrint('清理悬浮消息出错: $e'); - _currentMessage = null; _isPlaying = false; _safeScheduleNext(); } @@ -209,9 +133,6 @@ class OverlayManager { switch (message.type) { case 0: return FloatingLuckGiftScreenWidget( - key: ValueKey( - 'luck_${message.userId}_${message.toUserId}_${message.giftUrl}_${message.aggregateVersion}', - ), message: message, onAnimationCompleted: onComplete, ); @@ -253,7 +174,6 @@ class OverlayManager { _isDisposed = true; _currentOverlayEntry?.remove(); _currentOverlayEntry = null; - _currentMessage = null; _messageQueue.clear(); _isPlaying = false; _isProcessing = false; diff --git a/lib/ui_kit/widgets/daily_sign_in/daily_sign_in_dialog.dart b/lib/ui_kit/widgets/daily_sign_in/daily_sign_in_dialog.dart index 9ac33c7..8d660a4 100644 --- a/lib/ui_kit/widgets/daily_sign_in/daily_sign_in_dialog.dart +++ b/lib/ui_kit/widgets/daily_sign_in/daily_sign_in_dialog.dart @@ -324,9 +324,12 @@ class _DailySignInDialogState extends State { 'dayIndex=${checkInResult.dayIndex} ' 'checkedToday=${latestData.checkedToday}', ); - SCTts.show( - checkInResult.alreadySigned ? l10n.signedin : l10n.receiveSucc, - ); + final toastMessage = + checkInResult.alreadySigned + ? l10n.signedin + : _rewardToastMessage(l10n, checkInResult.rewardItems); + debugPrint('[SignInReward][Dialog] reward toast=$toastMessage'); + SCTts.show(toastMessage); SmartDialog.dismiss(tag: DailySignInDialog.dialogTag); } catch (error) { SCLoadingManager.hide(); @@ -365,6 +368,66 @@ class _DailySignInDialogState extends State { ); } + String _rewardToastMessage( + SCAppLocalizations l10n, + List rewardItems, + ) { + final rewardSummary = _rewardSummary(l10n, rewardItems); + if (rewardSummary.isEmpty) { + return l10n.receiveSucc; + } + return l10n.signInRewardReceived(rewardSummary); + } + + String _rewardSummary( + SCAppLocalizations l10n, + List rewardItems, + ) { + final rewards = + rewardItems + .map((item) => _rewardItemLabel(l10n, item)) + .where((item) => item.isNotEmpty) + .toList(); + if (rewards.isEmpty) { + return ''; + } + return rewards.join(', '); + } + + String _rewardItemLabel(SCAppLocalizations l10n, SCSignInRewardItem item) { + final type = item.type.trim().toUpperCase(); + final quantity = + item.quantity > 0 ? item.quantity.toString() : item.content.trim(); + + if (type == 'GOLD') { + return l10n.coins2(quantity.isEmpty ? '0' : quantity); + } + + final name = _pickFirstNonEmpty([ + item.name, + item.remark, + item.content, + item.type, + ]); + if (name.isEmpty) { + return ''; + } + if (item.quantity > 1) { + return '$name x${item.quantity}'; + } + return name; + } + + String _pickFirstNonEmpty(List values) { + for (final value in values) { + final trimmed = value?.trim() ?? ''; + if (trimmed.isNotEmpty) { + return trimmed; + } + } + return ''; + } + String _debugItemsSummary(List items) { return items .map( diff --git a/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart b/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart index 981bdf1..87609c6 100644 --- a/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart +++ b/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart @@ -1,438 +1,447 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/ui_kit/components/sc_compontent.dart'; -import 'package:yumi/main.dart'; -import 'package:provider/provider.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; -import 'package:yumi/services/gift/gift_animation_manager.dart'; -import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; - -///房间礼物滚屏动画 -class LGiftAnimalPage extends StatefulWidget { - const LGiftAnimalPage({super.key}); - - @override - State createState() { - return _GiftAnimalPageState(); - } -} - -class _GiftAnimalPageState extends State - with TickerProviderStateMixin { - static const String _luckyGiftRewardFrameAssetPath = - "sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga"; - - @override - Widget build(BuildContext context) { - return SizedBox( - height: 332.w, - child: Consumer( - builder: (context, ref, child) { - return Stack( - clipBehavior: Clip.none, - children: List.generate( - 4, - (index) => _buildGiftTickerItem(context, ref, index), - ), - ); - }, - ), - ); - } - - Widget _buildGiftTickerItem( - BuildContext context, - GiftAnimationManager ref, - int index, - ) { - final gift = ref.giftMap[index]; - if (gift == null || ref.animationControllerList.length <= index) { - return const SizedBox.shrink(); - } - final bean = ref.animationControllerList[index]; - return AnimatedBuilder( - animation: bean.controller, - builder: (context, child) { - final showLuckyRewardFrame = gift.showLuckyRewardFrame; - return Container( - margin: bean.verticalAnimation.value, - width: - ScreenUtil().screenWidth * (showLuckyRewardFrame ? 0.96 : 0.78), - child: _buildGiftTickerCard(context, gift, bean.sizeAnimation.value), - ); - }, - ); - } - - Widget _buildGiftTickerCard( - BuildContext context, - LGiftModel gift, - double animatedSize, - ) { - final showLuckyRewardFrame = gift.showLuckyRewardFrame; - return SizedBox( - height: 52.w, - child: Stack( - alignment: AlignmentDirectional.centerStart, - clipBehavior: Clip.none, - children: [ - Container( - width: - ScreenUtil().screenWidth * (showLuckyRewardFrame ? 0.69 : 0.67), - height: 42.w, - padding: EdgeInsetsDirectional.only( - start: 3.w, - end: showLuckyRewardFrame ? 174.w : 76.w, - top: 3.w, - bottom: 3.w, - ), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage(getBg("")), - fit: BoxFit.fill, - ), - ), - child: Row( - children: [ - netImage( - url: gift.sendUserPic, - shape: BoxShape.circle, - width: 26.w, - height: 26.w, - ), - SizedBox(width: 4.w), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - socialchatNickNameText( - maxWidth: 88.w, - gift.sendUserName, - fontSize: 12.sp, - fontWeight: FontWeight.w500, - type: "", - needScroll: gift.sendUserName.characters.length > 8, - ), - SizedBox(height: 1.w), - Row( - children: [ - Text( - "${SCAppLocalizations.of(context)!.sendTo} ", - style: TextStyle( - fontSize: 10.sp, - color: Colors.white, - height: 1.0, - ), - ), - Flexible( - child: Text( - gift.sendToUserName, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 10.sp, - color: const Color(0xFFFFD400), - height: 1.0, - ), - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - PositionedDirectional( - end: 0, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: - ScreenUtil().screenWidth * - (showLuckyRewardFrame ? 0.48 : 0.30), - minHeight: 40.w, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - netImage( - url: gift.giftPic, - fit: BoxFit.cover, - borderRadius: BorderRadius.circular(4.w), - width: 34.w, - height: 34.w, - ), - if (gift.giftCount > 0) ...[ - SizedBox(width: 8.w), - Flexible( - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.centerLeft, - child: _buildGiftCountLabel(gift, animatedSize), - ), - ), - ], - if (showLuckyRewardFrame) ...[ - SizedBox(width: 6.w), - _buildLuckyGiftRewardFrame(gift), - ], - ], - ), - ), - ), - ], - ), - ); - } - - Widget _buildGiftCountLabel(LGiftModel gift, double animatedSize) { - final xFontSize = animatedSize; - final countFontSize = animatedSize; - return Directionality( - textDirection: TextDirection.ltr, - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - "x", - style: TextStyle( - fontSize: xFontSize, - fontStyle: FontStyle.italic, - color: const Color(0xFFFFD400), - fontWeight: FontWeight.bold, - height: 1, - ), - ), - SizedBox(width: 2.w), - Text( - _giftCountText(gift.giftCount), - style: TextStyle( - fontSize: countFontSize, - fontStyle: FontStyle.italic, - color: const Color(0xFFFFD400), - fontWeight: FontWeight.bold, - height: 1, - ), - ), - ], - ), - ); - } - - Widget _buildLuckyGiftRewardFrame(LGiftModel gift) { - return SizedBox( - width: 108.w, - height: 52.w, - child: Stack( - alignment: Alignment.center, - children: [ - SCSvgaAssetWidget( - assetPath: _luckyGiftRewardFrameAssetPath, - width: 108.w, - height: 52.w, - fit: BoxFit.cover, - loop: true, - allowDrawingOverflow: true, - fallback: _buildLuckyGiftRewardFrameFallback(), - ), - Padding( - padding: EdgeInsetsDirectional.only( - start: 12.w, - end: 12.w, - top: 1.w, - ), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "+${gift.rewardAmountText}", - maxLines: 1, - style: TextStyle( - fontSize: 16.sp, - color: const Color(0xFFFFF2AE), - fontWeight: FontWeight.w800, - fontStyle: FontStyle.italic, - height: 1, - shadows: const [ - Shadow( - color: Color(0xCC6A3700), - blurRadius: 10, - offset: Offset(0, 2), - ), - ], - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildLuckyGiftRewardFrameFallback() { - return Container( - width: 108.w, - height: 52.w, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.w), - gradient: const LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [Color(0xCC7C4300), Color(0xE6D99A36)], - ), - border: Border.all(color: const Color(0xFFF7D87B), width: 1), - boxShadow: const [ - BoxShadow( - color: Color(0x663F1E00), - blurRadius: 8, - offset: Offset(0, 2), - ), - ], - ), - ); - } - - String _giftCountText(num count) { - return count % 1 == 0 ? count.toInt().toString() : count.toString(); - } - - @override - void dispose() { - Provider.of( - navigatorKey.currentState!.context, - listen: false, - ).cleanupAnimationResources(); - super.dispose(); - } - - @override - void initState() { - super.initState(); - initAnimal(); - } - - void initAnimal() { - List beans = []; - double top = 60; - for (int i = 0; i < 4; i++) { - var bean = LGiftScrollingScreenAnimsBean(); - var controller = AnimationController( - value: 0, - duration: const Duration(milliseconds: 5000), - vsync: this, - ); - bean.controller = controller; - // bean.transverseAnimation = Tween( - // begin: Offset(ScreenUtil().screenWidth, 0), - // end: Offset(0, 0), - // ).animate( - // CurvedAnimation( - // parent: controller, - // curve: Interval(0.0, 0.45, curve: Curves.ease), - // ), - // ); - bean.verticalAnimation = EdgeInsetsTween( - begin: EdgeInsets.only(top: top), - end: EdgeInsets.only(top: 0), - ).animate( - CurvedAnimation( - parent: controller, - curve: Interval(0.2, 1, curve: Curves.easeOut), - ), - ); - bean.sizeAnimation = Tween(begin: 0, end: 22).animate( - CurvedAnimation( - parent: controller, - curve: Interval(0.45, 0.55, curve: Curves.ease), - ), - ); - beans.add(bean); - top = top + 70; - } - beans[0].controller.addStatusListener((state) { - if (state == AnimationStatus.completed) { - //动画完成监听 - Provider.of( - context, - listen: false, - ).markAnimationAsFinished(0); - } - }); - beans[1].controller.addStatusListener((state) { - if (state == AnimationStatus.completed) { - //动画完成监听 - Provider.of( - context, - listen: false, - ).markAnimationAsFinished(1); - } - }); - beans[2].controller.addStatusListener((state) { - if (state == AnimationStatus.completed) { - //动画完成监听 - Provider.of( - context, - listen: false, - ).markAnimationAsFinished(2); - } - }); - beans[3].controller.addStatusListener((state) { - if (state == AnimationStatus.completed) { - //动画完成监听 - Provider.of( - context, - listen: false, - ).markAnimationAsFinished(3); - } - }); - Provider.of( - context, - listen: false, - ).attachAnimationControllers(beans); - } - - String getBg(String type) { - return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png"; - } -} - -class LGiftScrollingScreenAnimsBean { - // late Animation transverseAnimation; - late Animation verticalAnimation; - late AnimationController controller; - late Animation sizeAnimation; -} - -class LGiftModel { - //发送者的名字 - String sendUserName = ""; - - //发送给谁 - String sendToUserName = ""; - - //发送者的头像 - String sendUserPic = ""; - - //礼物的图片 - String giftPic = ""; - - //礼物的名字 - String giftName = ""; - - //一次发送礼物的数量 - num giftCount = 0; - - //幸运礼物奖励条右侧的金币数 - String rewardAmountText = ""; - - //是否显示幸运礼物奖励框 - bool showLuckyRewardFrame = false; - - //id - String labelId = ""; -} +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:provider/provider.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/services/gift/gift_animation_manager.dart'; +import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; + +///房间礼物滚屏动画 +class LGiftAnimalPage extends StatefulWidget { + const LGiftAnimalPage({super.key}); + + @override + State createState() { + return _GiftAnimalPageState(); + } +} + +class _GiftAnimalPageState extends State + with TickerProviderStateMixin { + static const String _luckyGiftRewardFrameAssetPath = + "sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga"; + late final GiftAnimationManager _giftAnimationManager; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 332.w, + child: Consumer( + builder: (context, ref, child) { + return Stack( + clipBehavior: Clip.none, + children: List.generate( + 4, + (index) => _buildGiftTickerItem(context, ref, index), + ), + ); + }, + ), + ); + } + + Widget _buildGiftTickerItem( + BuildContext context, + GiftAnimationManager ref, + int index, + ) { + final gift = ref.giftMap[index]; + if (gift == null || ref.animationControllerList.length <= index) { + return const SizedBox.shrink(); + } + final bean = ref.animationControllerList[index]; + return AnimatedBuilder( + animation: bean.controller, + builder: (context, child) { + final showLuckyRewardFrame = gift.showLuckyRewardFrame; + final tickerMargin = + showLuckyRewardFrame + ? bean.luckyGiftPinnedMargin + : bean.verticalAnimation.value; + return Container( + margin: tickerMargin, + width: + ScreenUtil().screenWidth * (showLuckyRewardFrame ? 0.96 : 0.78), + child: _buildGiftTickerCard(context, gift, bean.sizeAnimation.value), + ); + }, + ); + } + + Widget _buildGiftTickerCard( + BuildContext context, + LGiftModel gift, + double animatedSize, + ) { + final showLuckyRewardFrame = gift.showLuckyRewardFrame; + return SizedBox( + height: 52.w, + child: Stack( + alignment: AlignmentDirectional.centerStart, + clipBehavior: Clip.none, + children: [ + Container( + width: + ScreenUtil().screenWidth * (showLuckyRewardFrame ? 0.69 : 0.67), + height: 42.w, + padding: EdgeInsetsDirectional.only( + start: 3.w, + end: showLuckyRewardFrame ? 174.w : 76.w, + top: 3.w, + bottom: 3.w, + ), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage(getBg("")), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + netImage( + url: gift.sendUserPic, + shape: BoxShape.circle, + width: 26.w, + height: 26.w, + ), + SizedBox(width: 4.w), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + socialchatNickNameText( + maxWidth: 88.w, + gift.sendUserName, + fontSize: 12.sp, + fontWeight: FontWeight.w500, + type: "", + needScroll: gift.sendUserName.characters.length > 8, + ), + SizedBox(height: 1.w), + Row( + children: [ + Text( + "${SCAppLocalizations.of(context)!.sendTo} ", + style: TextStyle( + fontSize: 10.sp, + color: Colors.white, + height: 1.0, + ), + ), + Flexible( + child: Text( + gift.sendToUserName, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 10.sp, + color: const Color(0xFFFFD400), + height: 1.0, + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + PositionedDirectional( + end: 0, + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: + ScreenUtil().screenWidth * + (showLuckyRewardFrame ? 0.48 : 0.30), + minHeight: 40.w, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + netImage( + url: gift.giftPic, + fit: BoxFit.cover, + borderRadius: BorderRadius.circular(4.w), + width: 34.w, + height: 34.w, + ), + if (gift.giftCount > 0) ...[ + SizedBox(width: 8.w), + Flexible( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.centerLeft, + child: _buildGiftCountLabel(gift, animatedSize), + ), + ), + ], + if (showLuckyRewardFrame) ...[ + SizedBox(width: 6.w), + _buildLuckyGiftRewardFrame(gift), + ], + ], + ), + ), + ), + ], + ), + ); + } + + Widget _buildGiftCountLabel(LGiftModel gift, double animatedSize) { + final xFontSize = animatedSize; + final countFontSize = animatedSize; + return Directionality( + textDirection: TextDirection.ltr, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + "x", + style: TextStyle( + fontSize: xFontSize, + fontStyle: FontStyle.italic, + color: const Color(0xFFFFD400), + fontWeight: FontWeight.bold, + height: 1, + ), + ), + SizedBox(width: 2.w), + Text( + _giftCountText(gift.giftCount), + style: TextStyle( + fontSize: countFontSize, + fontStyle: FontStyle.italic, + color: const Color(0xFFFFD400), + fontWeight: FontWeight.bold, + height: 1, + ), + ), + ], + ), + ); + } + + Widget _buildLuckyGiftRewardFrame(LGiftModel gift) { + return SizedBox( + width: 108.w, + height: 52.w, + child: Stack( + alignment: Alignment.center, + children: [ + SCSvgaAssetWidget( + assetPath: _luckyGiftRewardFrameAssetPath, + width: 108.w, + height: 52.w, + fit: BoxFit.cover, + loop: true, + allowDrawingOverflow: true, + fallback: _buildLuckyGiftRewardFrameFallback(), + ), + Padding( + padding: EdgeInsetsDirectional.only( + start: 12.w, + end: 12.w, + top: 1.w, + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + "+${_giftRewardAmountText(gift)}", + maxLines: 1, + style: TextStyle( + fontSize: 16.sp, + color: const Color(0xFFFFF2AE), + fontWeight: FontWeight.w800, + fontStyle: FontStyle.italic, + height: 1, + shadows: const [ + Shadow( + color: Color(0xCC6A3700), + blurRadius: 10, + offset: Offset(0, 2), + ), + ], + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildLuckyGiftRewardFrameFallback() { + return Container( + width: 108.w, + height: 52.w, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.w), + gradient: const LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [Color(0xCC7C4300), Color(0xE6D99A36)], + ), + border: Border.all(color: const Color(0xFFF7D87B), width: 1), + boxShadow: const [ + BoxShadow( + color: Color(0x663F1E00), + blurRadius: 8, + offset: Offset(0, 2), + ), + ], + ), + ); + } + + String _giftCountText(num count) { + return count % 1 == 0 ? count.toInt().toString() : count.toString(); + } + + String _giftRewardAmountText(LGiftModel gift) { + final rewardAmount = gift.rewardAmount; + if (rewardAmount > 0) { + if (rewardAmount > 9999) { + return "${(rewardAmount / 1000).toStringAsFixed(0)}k"; + } + if (rewardAmount % 1 == 0) { + return rewardAmount.toInt().toString(); + } + return rewardAmount.toString(); + } + return gift.rewardAmountText; + } + + @override + void dispose() { + _giftAnimationManager.cleanupAnimationResources(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + _giftAnimationManager = Provider.of( + context, + listen: false, + ); + initAnimal(); + } + + void initAnimal() { + List beans = []; + double top = 60; + for (int i = 0; i < 4; i++) { + var bean = LGiftScrollingScreenAnimsBean(); + var controller = AnimationController( + value: 0, + duration: const Duration(milliseconds: 5000), + vsync: this, + ); + bean.controller = controller; + bean.luckyGiftPinnedMargin = EdgeInsets.only(top: top); + // bean.transverseAnimation = Tween( + // begin: Offset(ScreenUtil().screenWidth, 0), + // end: Offset(0, 0), + // ).animate( + // CurvedAnimation( + // parent: controller, + // curve: Interval(0.0, 0.45, curve: Curves.ease), + // ), + // ); + bean.verticalAnimation = EdgeInsetsTween( + begin: EdgeInsets.only(top: top), + end: EdgeInsets.only(top: 0), + ).animate( + CurvedAnimation( + parent: controller, + curve: Interval(0.2, 1, curve: Curves.easeOut), + ), + ); + bean.sizeAnimation = Tween(begin: 0, end: 22).animate( + CurvedAnimation( + parent: controller, + curve: Interval(0.45, 0.55, curve: Curves.ease), + ), + ); + beans.add(bean); + top = top + 70; + } + beans[0].controller.addStatusListener((state) { + if (state == AnimationStatus.completed) { + //动画完成监听 + _giftAnimationManager.markAnimationAsFinished(0); + } + }); + beans[1].controller.addStatusListener((state) { + if (state == AnimationStatus.completed) { + //动画完成监听 + _giftAnimationManager.markAnimationAsFinished(1); + } + }); + beans[2].controller.addStatusListener((state) { + if (state == AnimationStatus.completed) { + //动画完成监听 + _giftAnimationManager.markAnimationAsFinished(2); + } + }); + beans[3].controller.addStatusListener((state) { + if (state == AnimationStatus.completed) { + //动画完成监听 + _giftAnimationManager.markAnimationAsFinished(3); + } + }); + _giftAnimationManager.attachAnimationControllers(beans); + } + + String getBg(String type) { + return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png"; + } +} + +class LGiftScrollingScreenAnimsBean { + // late Animation transverseAnimation; + late Animation verticalAnimation; + late EdgeInsets luckyGiftPinnedMargin; + late AnimationController controller; + late Animation sizeAnimation; +} + +class LGiftModel { + //发送者的名字 + String sendUserName = ""; + + //发送给谁 + String sendToUserName = ""; + + //发送者的头像 + String sendUserPic = ""; + + //礼物的图片 + String giftPic = ""; + + //礼物的名字 + String giftName = ""; + + //一次发送礼物的数量 + num giftCount = 0; + + //幸运礼物奖励条右侧的金币数 + String rewardAmountText = ""; + + //幸运礼物奖励条右侧的金币累计值 + num rewardAmount = 0; + + //是否显示幸运礼物奖励框 + bool showLuckyRewardFrame = false; + + //id + String labelId = ""; +} diff --git a/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart b/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart index b8ca03e..d1a311e 100644 --- a/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart +++ b/lib/ui_kit/widgets/room/effect/luck_gift_nomor_anim_widget.dart @@ -6,8 +6,6 @@ import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; class LuckGiftNomorAnimWidget extends StatefulWidget { - static const num _rewardBurstMinAwardAmount = 5000; - static const num _rewardBurstMinMultiple = 10; static const String _rewardBurstAssetPath = "sc_images/room/anim/luck_gift/luck_gift_reward_burst.svga"; @@ -19,11 +17,11 @@ class LuckGiftNomorAnimWidget extends StatefulWidget { } class _LuckGiftNomorAnimWidgetState extends State { + String? _currentBurstEventId; + bool _isRewardAmountVisible = false; + bool _shouldPlayRewardBurst(Data rewardData) { - final awardAmount = rewardData.awardAmount ?? 0; - final multiple = rewardData.multiple ?? 0; - return awardAmount > LuckGiftNomorAnimWidget._rewardBurstMinAwardAmount || - multiple > LuckGiftNomorAnimWidget._rewardBurstMinMultiple; + return RealTimeMessagingManager.shouldPlayLuckyGiftBurst(rewardData); } String _formatAwardAmount(num awardAmount) { @@ -43,6 +41,8 @@ class _LuckGiftNomorAnimWidgetState extends State { builder: (context, provider, child) { final rewardData = provider.currentPlayingLuckGift?.data; if (rewardData == null || !_shouldPlayRewardBurst(rewardData)) { + _currentBurstEventId = null; + _isRewardAmountVisible = false; return const SizedBox.shrink(); } final rewardAnimationKey = ValueKey( @@ -52,6 +52,10 @@ class _LuckGiftNomorAnimWidgetState extends State { '|${rewardData.multiple ?? 0}' '|${rewardData.normalizedMultipleType}', ); + if (_currentBurstEventId != rewardAnimationKey.value) { + _currentBurstEventId = rewardAnimationKey.value; + _isRewardAmountVisible = false; + } return SizedBox( height: 380.w, child: Stack( @@ -66,37 +70,67 @@ class _LuckGiftNomorAnimWidgetState extends State { height: 380.w, fit: BoxFit.fitWidth, allowDrawingOverflow: true, + clearsAfterStop: true, + onPlaybackStarted: () { + if (!mounted) { + return; + } + if (!_isRewardAmountVisible) { + setState(() { + _isRewardAmountVisible = true; + }); + } + }, + onPlaybackCompleted: () { + if (!mounted) { + return; + } + if (_isRewardAmountVisible) { + setState(() { + _isRewardAmountVisible = false; + }); + } + }, ), ), - Positioned( - top: 154.w, - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: ScreenUtil().screenWidth * 0.56, - ), - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - "+${_formatAwardAmount(rewardData.awardAmount ?? 0)}", - maxLines: 1, - style: TextStyle( - fontSize: 28.sp, - color: const Color(0xFFFFF3B6), - fontWeight: FontWeight.w900, - fontStyle: FontStyle.italic, - height: 1, - shadows: const [ - Shadow( - color: Color(0xCC7A3E00), - blurRadius: 12, - offset: Offset(0, 3), + if (_isRewardAmountVisible) + Positioned.fill( + child: Align( + alignment: const Alignment(0, 0.12), + child: Transform.translate( + offset: Offset(0, 0), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: ScreenUtil().screenWidth * 0.56, + ), + child: FittedBox( + fit: BoxFit.scaleDown, + child: Padding( + padding: EdgeInsets.only(left: 6.w, right: 2.w), + child: Text( + _formatAwardAmount(rewardData.awardAmount ?? 0), + maxLines: 1, + style: TextStyle( + fontSize: 28.sp, + color: const Color(0xFFFFF3B6), + fontWeight: FontWeight.w900, + fontStyle: FontStyle.italic, + height: 1, + shadows: const [ + Shadow( + color: Color(0xCC7A3E00), + blurRadius: 12, + offset: Offset(0, 3), + ), + ], + ), + ), ), - ], + ), ), ), ), ), - ), ], ), ); diff --git a/lib/ui_kit/widgets/room/room_bottom_widget.dart b/lib/ui_kit/widgets/room/room_bottom_widget.dart index 008a362..48a83ef 100644 --- a/lib/ui_kit/widgets/room/room_bottom_widget.dart +++ b/lib/ui_kit/widgets/room/room_bottom_widget.dart @@ -50,17 +50,20 @@ class _RoomBottomWidgetState extends State { Widget build(BuildContext context) { return SizedBox( height: _floatingButtonHostHeight.w, - child: Consumer( - builder: (context, rtcProvider, child) { - final showMic = _shouldShowMic(rtcProvider); - + child: Selector( + selector: + (context, provider) => _RoomBottomSnapshot( + showMic: _shouldShowMic(provider), + isMic: provider.isMic, + ), + builder: (context, bottomSnapshot, child) { return LayoutBuilder( builder: (context, constraints) { final inputWidth = constraints.maxWidth / 3; final giftCenterX = _resolveGiftCenterX( maxWidth: constraints.maxWidth, inputWidth: inputWidth, - showMic: showMic, + showMic: bottomSnapshot.showMic, ); return Stack( @@ -78,9 +81,9 @@ class _RoomBottomWidgetState extends State { child: SizedBox( height: _bottomBarHeight.w, child: _buildBottomBar( - rtcProvider: rtcProvider, inputWidth: inputWidth, - showMic: showMic, + showMic: bottomSnapshot.showMic, + isMic: bottomSnapshot.isMic, ), ), ), @@ -105,9 +108,9 @@ class _RoomBottomWidgetState extends State { } Widget _buildBottomBar({ - required RtcProvider rtcProvider, required double inputWidth, required bool showMic, + required bool isMic, }) { final giftAction = _buildGiftAction(); final messageAction = _buildMessageAction(); @@ -126,7 +129,7 @@ class _RoomBottomWidgetState extends State { children: [ _buildChatEntry(inputWidth), giftAction, - _buildMicAction(rtcProvider), + _buildMicAction(isMic: isMic), menuAction, messageAction, ], @@ -265,10 +268,11 @@ class _RoomBottomWidgetState extends State { ); } - Widget _buildMicAction(RtcProvider provider) { + Widget _buildMicAction({required bool isMic}) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { + final provider = context.read(); setState(() { provider.isMic = !provider.isMic; @@ -298,7 +302,7 @@ class _RoomBottomWidgetState extends State { }, child: RoomBottomCircleAction( child: Image.asset( - "sc_images/room/${provider.isMic ? 'sc_icon_botton_mic_close' : 'sc_icon_botton_mic_open'}.png", + "sc_images/room/${isMic ? 'sc_icon_botton_mic_close' : 'sc_icon_botton_mic_open'}.png", width: 30.w, height: 30.w, fit: BoxFit.contain, @@ -320,3 +324,23 @@ class _RoomBottomWidgetState extends State { return show; } } + +class _RoomBottomSnapshot { + const _RoomBottomSnapshot({required this.showMic, required this.isMic}); + + final bool showMic; + final bool isMic; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is _RoomBottomSnapshot && + other.showMic == showMic && + other.isMic == isMic; + } + + @override + int get hashCode => Object.hash(showMic, isMic); +} diff --git a/lib/ui_kit/widgets/room/room_head_widget.dart b/lib/ui_kit/widgets/room/room_head_widget.dart index cb7d35d..07f964d 100644 --- a/lib/ui_kit/widgets/room/room_head_widget.dart +++ b/lib/ui_kit/widgets/room/room_head_widget.dart @@ -23,8 +23,22 @@ class RoomHeadWidget extends StatefulWidget { class _RoomHeadWidgetState extends State { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, provider, child) { + return Selector( + selector: (context, provider) { + final room = provider.currenRoom; + final roomOwnerUserId = room?.roomProfile?.roomProfile?.userId ?? ""; + final currentUserId = + AccountStorage().getCurrentUser()?.userProfile?.id ?? ""; + return _RoomHeadSnapshot( + roomProfileId: room?.roomProfile?.roomProfile?.id, + roomCover: room?.roomProfile?.roomProfile?.roomCover, + roomName: room?.roomProfile?.roomProfile?.roomName ?? "", + roomDisplayId: room?.roomProfile?.userProfile?.getID() ?? "", + roomOwnerUserId: roomOwnerUserId, + isRoomOwner: roomOwnerUserId == currentUserId, + ); + }, + builder: (context, roomSnapshot, child) { return Row( children: [ Row( @@ -32,8 +46,8 @@ class _RoomHeadWidgetState extends State { GestureDetector( onTap: () { showBottomInBottomDialog( - context!, - RoomDetailPage(provider.isFz()), + context, + RoomDetailPage(context.read().isFz()), ); }, child: Container( @@ -48,16 +62,8 @@ class _RoomHeadWidgetState extends State { children: [ netImage( url: resolveRoomCoverUrl( - provider - .currenRoom - ?.roomProfile - ?.roomProfile - ?.id, - provider - .currenRoom - ?.roomProfile - ?.roomProfile - ?.roomCover, + roomSnapshot.roomProfileId, + roomSnapshot.roomCover, ), defaultImg: kRoomCoverDefaultImg, width: 28.w, @@ -99,22 +105,9 @@ class _RoomHeadWidgetState extends State { maxHeight: 17.w, ), child: - (provider - .currenRoom - ?.roomProfile - ?.roomProfile - ?.roomName - ?.length ?? - 0) > - 10 + roomSnapshot.roomName.length > 10 ? Marquee( - text: - provider - .currenRoom - ?.roomProfile - ?.roomProfile - ?.roomName ?? - "", + text: roomSnapshot.roomName, style: TextStyle( fontSize: 14.sp, color: Color(0xffffffff), @@ -137,12 +130,7 @@ class _RoomHeadWidgetState extends State { decelerationCurve: Curves.easeOut, ) : Text( - provider - .currenRoom - ?.roomProfile - ?.roomProfile - ?.roomName ?? - '', + roomSnapshot.roomName, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( @@ -155,7 +143,7 @@ class _RoomHeadWidgetState extends State { ), text( - "ID:${provider.currenRoom?.roomProfile?.userProfile?.getID()}", + "ID:${roomSnapshot.roomDisplayId}", fontSize: 13.sp, fontWeight: FontWeight.w600, textColor: Colors.white70, @@ -163,11 +151,7 @@ class _RoomHeadWidgetState extends State { ], ), SizedBox(width: 6.w), - provider.currenRoom?.roomProfile?.roomProfile?.userId != - AccountStorage() - .getCurrentUser() - ?.userProfile - ?.id + !roomSnapshot.isRoomOwner ? Selector( selector: (c, p) => @@ -182,7 +166,9 @@ class _RoomHeadWidgetState extends State { height: 22.w, ), onTap: () { - provider.followCurrentVoiceRoom(); + context + .read() + .followCurrentVoiceRoom(); }, ) : Container(); @@ -203,7 +189,7 @@ class _RoomHeadWidgetState extends State { height: 32.w, ), onTap: () { - if (provider.isFz()) { + if (context.read().isFz()) { SCNavigatorUtils.push( context, "${VoiceRoomRoute.roomEdit}?need=false", @@ -234,8 +220,8 @@ class _RoomHeadWidgetState extends State { showCenterDialog( context, ExitMinRoomPage( - provider.isFz(), - provider.currenRoom?.roomProfile?.roomProfile?.id ?? "", + context.read().isFz(), + roomSnapshot.roomProfileId ?? "", ), barrierColor: Colors.black54, ); @@ -249,3 +235,45 @@ class _RoomHeadWidgetState extends State { ); } } + +class _RoomHeadSnapshot { + const _RoomHeadSnapshot({ + required this.roomProfileId, + required this.roomCover, + required this.roomName, + required this.roomDisplayId, + required this.roomOwnerUserId, + required this.isRoomOwner, + }); + + final String? roomProfileId; + final String? roomCover; + final String roomName; + final String roomDisplayId; + final String roomOwnerUserId; + final bool isRoomOwner; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is _RoomHeadSnapshot && + other.roomProfileId == roomProfileId && + other.roomCover == roomCover && + other.roomName == roomName && + other.roomDisplayId == roomDisplayId && + other.roomOwnerUserId == roomOwnerUserId && + other.isRoomOwner == isRoomOwner; + } + + @override + int get hashCode => Object.hash( + roomProfileId, + roomCover, + roomName, + roomDisplayId, + roomOwnerUserId, + isRoomOwner, + ); +} diff --git a/lib/ui_kit/widgets/room/room_online_user_widget.dart b/lib/ui_kit/widgets/room/room_online_user_widget.dart index 3bb34c8..7c84a00 100644 --- a/lib/ui_kit/widgets/room/room_online_user_widget.dart +++ b/lib/ui_kit/widgets/room/room_online_user_widget.dart @@ -6,6 +6,7 @@ import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/tools/sc_lk_dialog_util.dart'; import 'package:yumi/services/room/rc_room_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; @@ -29,17 +30,32 @@ class _RoomOnlineUserWidgetState extends State { double get _onlineUsersShellWidth => _onlineUsersAvatarsWidth + _onlineUsersCounterWidth; - void _openRoomOnlinePage(RtcProvider ref) { + void _openRoomOnlinePage() { showBottomInBottomDialog( context, - RoomOnlinePage(roomId: ref.currenRoom?.roomProfile?.roomProfile?.id), + RoomOnlinePage( + roomId: + context + .read() + .currenRoom + ?.roomProfile + ?.roomProfile + ?.id, + ), ); } @override Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, child) { + return Selector( + selector: + (context, provider) => _RoomOnlineUsersSnapshot( + onlineUsers: List.unmodifiable( + provider.onlineUsers, + ), + ), + builder: (context, onlineSnapshot, child) { + final onlineUsers = onlineSnapshot.onlineUsers; return Row( children: [ _buildExperience(), @@ -48,8 +64,8 @@ class _RoomOnlineUserWidgetState extends State { child: Align( alignment: Alignment.centerRight, child: - ref.onlineUsers.isNotEmpty - ? _buildOnlineUsers(ref) + onlineUsers.isNotEmpty + ? _buildOnlineUsers(onlineUsers) : _buildOnlineUsersPlaceholder(), ), ), @@ -59,12 +75,12 @@ class _RoomOnlineUserWidgetState extends State { ); } - Widget _buildOnlineUsers(RtcProvider ref) { + Widget _buildOnlineUsers(List onlineUsers) { return Padding( padding: EdgeInsets.only(right: 5.w), child: GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => _openRoomOnlinePage(ref), + onTap: _openRoomOnlinePage, child: SizedBox( width: _onlineUsersShellWidth, height: _onlineUsersShellHeight, @@ -80,13 +96,13 @@ class _RoomOnlineUserWidgetState extends State { scrollDirection: Axis.horizontal, child: Row( mainAxisSize: MainAxisSize.min, - children: List.generate(ref.onlineUsers.length, (index) { + children: List.generate(onlineUsers.length, (index) { return Transform.translate( offset: Offset(-3.w * index, 0), child: Padding( padding: EdgeInsets.only(right: 0.w), child: netImage( - url: ref.onlineUsers[index].userAvatar ?? "", + url: onlineUsers[index].userAvatar ?? "", width: 23.w, height: 23.w, defaultImg: @@ -117,11 +133,7 @@ class _RoomOnlineUserWidgetState extends State { width: 12.w, height: 12.sp, ), - text( - "${ref.onlineUsers.length}", - fontSize: 9, - lineHeight: 1, - ), + text("${onlineUsers.length}", fontSize: 9, lineHeight: 1), ], ), ), @@ -204,3 +216,37 @@ class _RoomOnlineUserWidgetState extends State { ); } } + +class _RoomOnlineUsersSnapshot { + const _RoomOnlineUsersSnapshot({required this.onlineUsers}); + + final List onlineUsers; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other is! _RoomOnlineUsersSnapshot || + other.onlineUsers.length != onlineUsers.length) { + return false; + } + for (int index = 0; index < onlineUsers.length; index++) { + final currentUser = onlineUsers[index]; + final otherUser = other.onlineUsers[index]; + if (currentUser.id != otherUser.id || + currentUser.userAvatar != otherUser.userAvatar || + currentUser.userNickname != otherUser.userNickname || + currentUser.roles != otherUser.roles || + currentUser.heartbeatVal != otherUser.heartbeatVal) { + return false; + } + } + return true; + } + + @override + int get hashCode => Object.hashAll( + onlineUsers.map((user) => Object.hash(user.id, user.userAvatar)), + ); +} diff --git a/lib/ui_kit/widgets/room/seat/room_seat_widget.dart b/lib/ui_kit/widgets/room/seat/room_seat_widget.dart index acfede8..17efcaa 100644 --- a/lib/ui_kit/widgets/room/seat/room_seat_widget.dart +++ b/lib/ui_kit/widgets/room/seat/room_seat_widget.dart @@ -13,12 +13,12 @@ class RoomSeatWidget extends StatefulWidget { class _RoomSeatWidgetState extends State { int _lastSeatCount = 0; - int _resolvedSeatCount(RtcProvider ref) { - final int seatCount = ref.roomWheatMap.length; - if (!ref.isExitingCurrentVoiceRoomSession && seatCount > 0) { + int _resolvedSeatCount(_RoomSeatLayoutSnapshot snapshot) { + final int seatCount = snapshot.seatCount; + if (!snapshot.isExitingCurrentVoiceRoomSession && seatCount > 0) { _lastSeatCount = seatCount; } - if (ref.isExitingCurrentVoiceRoomSession && _lastSeatCount > 0) { + if (snapshot.isExitingCurrentVoiceRoomSession && _lastSeatCount > 0) { return _lastSeatCount; } return seatCount; @@ -26,9 +26,15 @@ class _RoomSeatWidgetState extends State { @override Widget build(BuildContext context) { - return Consumer( - builder: (context, ref, child) { - final int seatCount = _resolvedSeatCount(ref); + return Selector( + selector: + (context, provider) => _RoomSeatLayoutSnapshot( + seatCount: provider.roomWheatMap.length, + isExitingCurrentVoiceRoomSession: + provider.isExitingCurrentVoiceRoomSession, + ), + builder: (context, snapshot, child) { + final int seatCount = _resolvedSeatCount(snapshot); return seatCount == 5 ? _buildSeat5() : (seatCount == 10 @@ -177,3 +183,27 @@ class _RoomSeatWidgetState extends State { ); } } + +class _RoomSeatLayoutSnapshot { + const _RoomSeatLayoutSnapshot({ + required this.seatCount, + required this.isExitingCurrentVoiceRoomSession, + }); + + final int seatCount; + final bool isExitingCurrentVoiceRoomSession; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + return other is _RoomSeatLayoutSnapshot && + other.seatCount == seatCount && + other.isExitingCurrentVoiceRoomSession == + isExitingCurrentVoiceRoomSession; + } + + @override + int get hashCode => Object.hash(seatCount, isExitingCurrentVoiceRoomSession); +} diff --git a/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart b/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart index 53cdfff..94ffd0f 100644 --- a/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart +++ b/lib/ui_kit/widgets/svga/sc_svga_asset_widget.dart @@ -14,6 +14,9 @@ class SCSvgaAssetWidget extends StatefulWidget { this.filterQuality = FilterQuality.low, this.allowDrawingOverflow = false, this.fallback, + this.clearsAfterStop = false, + this.onPlaybackStarted, + this.onPlaybackCompleted, }); final String assetPath; @@ -25,6 +28,9 @@ class SCSvgaAssetWidget extends StatefulWidget { final FilterQuality filterQuality; final bool allowDrawingOverflow; final Widget? fallback; + final bool clearsAfterStop; + final VoidCallback? onPlaybackStarted; + final VoidCallback? onPlaybackCompleted; @override State createState() => _SCSvgaAssetWidgetState(); @@ -44,9 +50,20 @@ class _SCSvgaAssetWidgetState extends State void initState() { super.initState(); _controller = SVGAAnimationController(vsync: this); + _controller.addStatusListener(_handleAnimationStatusChanged); _loadAsset(); } + void _handleAnimationStatusChanged(AnimationStatus status) { + if (status == AnimationStatus.forward) { + widget.onPlaybackStarted?.call(); + return; + } + if (status == AnimationStatus.completed) { + widget.onPlaybackCompleted?.call(); + } + } + @override void didUpdateWidget(covariant SCSvgaAssetWidget oldWidget) { super.didUpdateWidget(oldWidget); @@ -129,6 +146,7 @@ class _SCSvgaAssetWidgetState extends State if (!widget.active) { _controller.stop(); _controller.reset(); + widget.onPlaybackCompleted?.call(); return; } @@ -148,6 +166,7 @@ class _SCSvgaAssetWidgetState extends State @override void dispose() { + _controller.removeStatusListener(_handleAnimationStatusChanged); _controller.dispose(); super.dispose(); } @@ -170,7 +189,7 @@ class _SCSvgaAssetWidgetState extends State child: SVGAImage( _controller, fit: widget.fit, - clearsAfterStop: false, + clearsAfterStop: widget.clearsAfterStop, filterQuality: widget.filterQuality, allowDrawingOverflow: widget.allowDrawingOverflow, ), diff --git a/需求进度.md b/需求进度.md index 03fee58..feedc42 100644 --- a/需求进度.md +++ b/需求进度.md @@ -21,17 +21,26 @@ ## 已完成模块 - 已按 2026-04-20 最新首页视觉需求,为 Party 房间列表前 3 个房卡接入新的本地排名边框 SVGA:桌面“房间排序前三的框”中的 3 份素材已导入工程并挂到首页房卡最上层,仅作用于当前列表前 3 项,其余房卡保持原样;后续又继续为前三房卡底部信息区补了横向与底部安全区,避免外扩边框直接压住国旗、房名和在线人数。同时 `Me` 板块里的 `Recent / Followed` tab 已继续对齐上方首页 tab 的斜体字样式,并移除了点击时的波浪反馈。 -- 已按 2026-04-20 最新房间联调继续修正房主在自己房间里的麦位操作回归:当前房主/管理员点击自己所在麦位时,不再被“直接打开资料卡”逻辑短路,会重新回到底部麦位菜单,从而正常看到自己可用的上下麦/禁麦操作;同时上麦、下麦、禁麦、解禁、锁麦、解锁这些动作在接口成功后会立即回写本地麦位状态,并主动补拉一次最新麦位列表,减少房主端自己操作后 UI 状态延迟或短暂错乱。 +- 已按 2026-04-20 最新房间联调继续修正房主在自己房间里的麦位操作回归:当前房主/管理员点击自己所在麦位时,不再被“直接打开资料卡”逻辑短路,会重新回到底部麦位菜单,从而正常看到自己可用的上下麦/禁麦操作;同时上麦、下麦、禁麦、解禁、锁麦、解锁这些动作在接口成功后会立即回写本地麦位状态,并主动补拉一次最新麦位列表,减少房主端自己操作后 UI 状态延迟或短暂错乱。最新已继续收紧“换麦”场景:当前会先本地顺滑切到目标麦位,再在短保护窗内拦住把自己打回旧麦位的旧轮询/旧广播快照;同时补修 `MicRes.copyWith(user: null)` 实际不会清空用户的问题,避免切麦时出现双占位或 `1 -> 4 -> 1 -> 4` 这类来回闪动。 - 已按 2026-04-20 最新调整撤掉 `Wallet -> Recharge` 中新增的 MiFaPay 第三方支付 UI 与页面逻辑:当前充值页仅保留原生 `Google Pay / Apple Pay` 入口与商品列表,`Recharge methods` 区域也已收敛为单一原生支付卡片;此前接入的 MiFaPay 方法选择、底部弹窗、H5 收银台页以及对应页面级状态管理已从现有充值链路移除,避免继续对当前版本产生影响。 - 已按 2026-04-18 联调要求继续收口幸运礼物链路:项目默认 API host 已临时切到 `http://192.168.110.43:1100/` 方便直连测试环境;`/gift/give/lucky-gift` 请求体也已补齐为最新结构,除原有 `giftId/quantity/roomId/acceptUserIds/checkCombo` 外,会稳定携带 `gameId: null`、`accepts: []`、`dynamicContentId: null`、`songId: null` 这几组字段,便于和当前后端参数定义保持一致。 - 已继续补齐 `GAME_LUCKY_GIFT` 的 socket 播报与中奖动效:房间群消息收到该类型后,现在会统一落聊天室中奖消息、奖励弹层队列与发送者余额回写,不再只在 `3x+` 时才触发顶部奖励动画;同时全局飘窗已改为按服务端 `globalNews` 字段决定是否展示,避免再用前端本地倍率硬编码去猜。 -- 已按最新 UI 口径重排幸运礼物中奖展示:顶部 `LuckGiftNomorAnimWidget` 不再叠加大头像、倍率和奖励框,只在“单个礼物倍率 `> 10x`”或“单次中奖金币 `> 5000`”时负责播全屏 `luck_gift_reward_burst.svga`;原本的 `luck_gift_reward_frame.svga` 已改挂到房间礼物播报条最右侧,并在框内显示 `+formattedAwardAmount`,金额文案直接复用此前大头像下方那一份中奖金币额。 +- 已按最新 UI 口径重排幸运礼物中奖展示:顶部 `LuckGiftNomorAnimWidget` 不再叠加大头像、倍率和奖励框,只在“单个礼物倍率 `>= 10x`”或“单次中奖金币 `> 5000`”时负责播全屏 `luck_gift_reward_burst.svga`;原本的 `luck_gift_reward_frame.svga` 已改挂到房间礼物播报条最右侧,并在框内显示 `+formattedAwardAmount`,金额文案直接复用此前大头像下方那一份中奖金币额。 - 已按最新确认撤掉播报条里的 lucky combo 特殊样式:房间礼物播报条右侧的 `xN` 数字现已恢复普通礼物样式,不再在这里做幸运礼物 10/20/50/100... 的特殊放大/描边展示;这些 `lucky_gift_combo_count_xxx.svga` 资源仍保留在幸运礼物倍率/数量命中时走全屏特效链,不再和播报条 UI 混在一起。 - 已继续修正幸运礼物中奖播报条右侧奖励框不显示的问题:根因是 `pubspec.yaml` 之前只显式收录了 `sc_images/room/anim/gift/`,但没有把新的 `sc_images/room/anim/luck_gift/` 子目录单独打进 Flutter 资源清单,导致 `luck_gift_reward_frame.svga` 在运行时可引用但未被实际打包;当前已补齐资源目录声明,并为播报条奖励框增加本地 fallback,避免资源异常时再次只剩金额裸字。 - 已按最新文案口径继续收紧幸运礼物中奖消息:聊天室里高倍率幸运礼物提示不再显示冗长的 `Coins / a lucky(magic) gift` 文字,当前已改为直接展示“中奖金额 + 金币图标 + 对应礼物图”,和房间礼物播报条的视觉表达保持一致。 - 已继续微调幸运礼物中奖视觉:房间礼物播报条右侧的 `luck_gift_reward_frame.svga` 已进一步放大,并给右侧区域和主条正文预留了更宽的排版空间,避免资源已经正常显示但因为画布比例和透明边距看起来过小;同时聊天室高亮中奖消息与房间顶部幸运礼物横幅现已统一改成“中奖金额 + 金币图标 + from + 礼物图”的表达,去掉原先 `Coins / a lucky(magic) gift` 这类冗长英文文案;另外全屏 `luck_gift_reward_burst.svga` 也已补上中部金额文案,避免播发时只见特效不见本次实际中奖金币数。 - 已按 2026-04-20 最新联调继续收口幸运礼物播报噪音:顶部幸运礼物飘屏现在会对同一房间、同一送礼人、同一接收人、同一礼物的连续中奖做队列内聚合,不再每来一条都重新追加一个新飘屏;当前展示中的那一条也会直接叠加金币额并刷新,避免“刷礼过快时飘屏一条接一条排队刷过去”。同时飘屏中奖文案已强制收为单行显示,防止金额、`from` 和礼物图被挤成上下两行。 - 已继续修复房间礼物播报条偶发出现两条一模一样内容的问题:此前 `GiftAnimationManager` 只会和“正在播放”的同 `labelId` 项做合并,等待队列里的同类播报不会提前去重,因此高频情况下仍可能排出两条完全一样的条目;当前已改为在入队时同时检查“正在播”和“待播”两侧,命中相同播报键时直接原地合并,不再把重复项继续塞进队列。 +- 已按 2026-04-20 最新确认继续调整幸运礼物展示逻辑:房间播报条最右侧的中奖金币现已按同一条播报累计值展示,不再每次中奖只用最后一笔金额覆盖前一笔;幸运礼物顶部飘屏则撤回此前的聚合策略,恢复为每条都单独展示,但仅当服务端倍率达到 `5x` 及以上时才触发飘屏,避免低倍率也持续刷屏;同时 `luck_gift_reward_burst.svga` 的触发链路已补上“当前房间广播兜底 + 事件去重”,即便只收到广播消息也能进队列播放,而同一事件不会因为广播/群消息双到达而重复播发。 +- 已继续修正幸运礼物 `burst` 的倍率边界判断:`LuckGiftNomorAnimWidget` 内部此前仍使用旧的“倍率 `> 10x`”严格大于判断,导致刚好命中 `10x` 的中奖虽然会走飘屏链路,但不会实际触发 `luck_gift_reward_burst.svga`;当前已统一改为“倍率 `>= 10x` 或单次中奖金币 `> 5000`”。 +- 已继续修正幸运礼物 `burst` 的实际播放链路:此前 `currentPlayingLuckGift` 队列会把所有幸运礼物中奖事件都排进去,哪怕只是低倍率、根本不需要播 `luck_gift_reward_burst.svga` 的那类消息,也会先占掉每次 3 秒的播放时段;这样在刷得比较密时,真正命中 `10x+` 或 `>5000` 金币的事件只是被压在队列后面,视觉上就会像“明明达标了却没触发”。当前已把队列入口改成只接收真正命中 `burst` 条件的事件,并把命中判断收口到同一处,避免顶部飘屏和 `burst` 各自走不同口径。 +- 已继续调整幸运礼物播报条的连刷表现:带右侧中奖奖励框的幸运礼物播报现在会固定在各自的坑位上显示,不再跟随普通礼物滚屏那套纵向位移动画反复“刷新整条”;用户连续送同一条幸运礼物时,前端只会在原位更新右侧 `xN` 数量和累计金币,直到连刷停止后再按既有时长自然消失。 +- 已继续微调幸运礼物 `burst` 中央金额文案的定位:此前这层 `+金币` 文字仍使用固定 `top` 绝对定位,实际在不同画布比例下会整体偏上,容易跑出 `luck_gift_reward_burst.svga` 中央的金额承载区;当前已改成基于整层居中后再轻微下移的相对锚点,确保金额文案稳定落在特效中间那块 `+****` 的视觉区域内。 +- 已继续细调幸运礼物 `burst` 中央金额文案的纵向位置:上一版相对锚点虽然已经回到特效主体内部,但仍略偏下;当前已把对齐点和下移量一起往中间收回一档,让 `+金币` 更接近 `luck_gift_reward_burst.svga` 中央的视觉中线。 +- 已继续修正幸运礼物 `burst` 中央金额文案的细节对位:当前已再上移 `2` 个单位,并给金额文本左侧补出额外留白,避免斜体 `+` 号因为字形外扩而贴边或被裁掉,确保 `+金币` 能完整落在特效中部。 +- 已继续按最新联调口径修正幸运礼物 `burst` 中央金额文案:当前已去掉文本里的 `+` 字符,仅保留金币数本身显示;同时维持上一版向上微调后的纵向位置,让金额落点保持在特效中部偏上的稳定区域。 +- 已继续收口幸运礼物 `burst` 金额文案与 `svga` 本体的时机同步:此前中央金币文本直接跟外层中奖数据显隐,而 `svga` 自身还存在资源加载和单次播放完成的生命周期,所以两者在出现/消失时会有肉眼可见的前后差;当前已给 `SCSvgaAssetWidget` 补上播放开始/结束回调,并让 `burst` 中央金额只在 `svga` 真正开始播时显示、在它播完清帧时一并隐藏。 - 已优化语言房麦位/头像的二次确认交互:普通用户点击可上麦的空麦位时,当前会直接执行上麦,不再先弹出只有 `Take the mic / Cancel` 的确认层;普通用户点击房间头像或已占麦位上的用户头像时,也会直接打开个人卡片,不再额外弹出仅含 `Open user profile card / Cancel` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。 - 已继续收窄语言房个人卡片前的“确认意义”弹层:当前用户在麦位上点击自己的头像时,也会直接打开自己的个人卡片,不再先弹出仅包含 `Leave the mic / Open user profile card / Cancel` 的底部菜单;同时个人卡片内的“离开麦位”入口已替换为新的 `leave` 视觉素材,和最新房间交互稿保持一致。 - 已继续微调语言房个人卡片与送礼 UI:个人卡片底部动作文案现已支持两行居中展示,避免 `Leave the mic` 这类英文按钮被硬截断;房间底部礼物入口也已切换为新的本地 `SVGA` 资源 `room_bottom_gift_button.svga`,保持房间底栏视觉和最新动效稿一致。