修复一些问题

This commit is contained in:
NIGGER SLAYER 2026-04-20 22:40:22 +08:00
parent 5b0b5b862e
commit 743f761dd1
28 changed files with 3128 additions and 2043 deletions

View File

@ -303,8 +303,9 @@
"goToRecharge": "اذهب لإعادة الشحن", "goToRecharge": "اذهب لإعادة الشحن",
"warning": "تحذير", "warning": "تحذير",
"ownerSendTheRedEnvelope": "أرسل مالك الغرفة عملات المكافأة.", "ownerSendTheRedEnvelope": "أرسل مالك الغرفة عملات المكافأة.",
"rewardCoins": "عملات المكافأة:{1} عملة", "rewardCoins": "عملات المكافأة:{1} عملة",
"lastWeekProgress": "تقدم الأسبوع الماضي", "signInRewardReceived": "تم تسجيل الدخول بنجاح. المكافأة: {1}",
"lastWeekProgress": "تقدم الأسبوع الماضي",
"currentProgress": "التقدم الحالي", "currentProgress": "التقدم الحالي",
"coins2": "{1} عملات", "coins2": "{1} عملات",
"roomReward2": "مكافأة الغرفة:{1}", "roomReward2": "مكافأة الغرفة:{1}",

View File

@ -159,6 +159,7 @@
"roomReward": "রুম পুরস্কার", "roomReward": "রুম পুরস্কার",
"ownerSendTheRedEnvelope": "মালিক পুরস্কার কয়েন পাঠিয়েছেন।", "ownerSendTheRedEnvelope": "মালিক পুরস্কার কয়েন পাঠিয়েছেন।",
"rewardCoins": "পুরস্কার কয়েন:{1} কয়েন", "rewardCoins": "পুরস্কার কয়েন:{1} কয়েন",
"signInRewardReceived": "সাইন ইন সফল হয়েছে। পুরস্কার: {1}",
"lastWeekProgress": "গত সপ্তাহের অগ্রগতি", "lastWeekProgress": "গত সপ্তাহের অগ্রগতি",
"redEnvelopeTips2": "*লাল খাম সময়সীমার মধ্যে দাবি না করলে, বাকি কয়েন প্রেরক ব্যবহারকারীকে ফেরত দেওয়া হবে।", "redEnvelopeTips2": "*লাল খাম সময়সীমার মধ্যে দাবি না করলে, বাকি কয়েন প্রেরক ব্যবহারকারীকে ফেরত দেওয়া হবে।",
"goToRecharge": "রিচার্জ করতে যান", "goToRecharge": "রিচার্জ করতে যান",

View File

@ -131,6 +131,7 @@
"expirationTime": "Expiration time", "expirationTime": "Expiration time",
"ownerSendTheRedEnvelope": "The owner sent reward coins.", "ownerSendTheRedEnvelope": "The owner sent reward coins.",
"rewardCoins": "Reward coins:{1} coins", "rewardCoins": "Reward coins:{1} coins",
"signInRewardReceived": "Signed in successfully. Reward: {1}",
"lastWeekProgress": "Last week's progress", "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.", "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", "goToRecharge": "Go to recharge",

View File

@ -117,10 +117,11 @@
"currentProgress": "Mevcut İlerleme", "currentProgress": "Mevcut İlerleme",
"currentStage": "Mevcut Aşama:{1}", "currentStage": "Mevcut Aşama:{1}",
"roomReward2": "Oda Ödülü:{1}", "roomReward2": "Oda Ödülü:{1}",
"roomReward": "Oda Ödülü", "roomReward": "Oda Ödülü",
"ownerSendTheRedEnvelope": "Sahip ödül jettonlarını gönderdi.", "ownerSendTheRedEnvelope": "Sahip ödül jettonlarını gönderdi.",
"rewardCoins": "Ödül jettonları:{1} jetton", "rewardCoins": "Ödül jettonları:{1} jetton",
"lastWeekProgress": "Geçen Haftanın İlerlemesi", "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.", "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", "goToRecharge": "Yüklemeye Git",
"deleteAccount2": " Hesabı Sil ({1} sn)", "deleteAccount2": " Hesabı Sil ({1} sn)",

View File

@ -11,22 +11,22 @@ class SCVariant1Config implements AppConfig {
String get appName => 'yumi'; // String get appName => 'yumi'; //
@override @override
String get packageName => 'com.org.yumiparty'; String get packageName => 'com.org.yumiparty';
@override @override
String get apiHost => const String.fromEnvironment( String get apiHost => const String.fromEnvironment(
'API_HOST', 'API_HOST',
defaultValue: 'https://jvapi.haiyihy.com/', defaultValue: 'http://192.168.110.43:1100/',
); // 线 --dart-define=API_HOST ); // 线 --dart-define=API_HOST
@override @override
String get imgHost => 'https://img.atuchat.com/'; // String get imgHost => 'https://img.atuchat.com/'; //
@override @override
String get privacyAgreementUrl => 'https://h5.haiyihy.com/privacy.html'; // String get privacyAgreementUrl => 'https://h5.haiyihy.com/privacy.html'; //
@override @override
String get userAgreementUrl => 'https://h5.haiyihy.com/service.html'; // String get userAgreementUrl => 'https://h5.haiyihy.com/service.html'; //
@override @override
String get appDownloadUrlGoogle => 'https://play.google.com/store/apps/details?id=$packageName'; 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 String get appDownloadUrlApple => 'https://apps.apple.com/us/app/atuchat/id1234567890'; // App Store ID
@override @override
String get anchorAgentUrl => 'https://h5.haiyihy.com/apply/index.html'; // H5 String get anchorAgentUrl => 'https://h5.haiyihy.com/apply/index.html'; // H5
@override @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 @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 @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 @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 @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 @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 @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 @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 @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 @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 @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 @override
int get primaryColor => 0xffFF5722; // int get primaryColor => 0xffFF5722; //
@override @override
String get tencentImAppid => '20036101'; String get tencentImAppid => '20036101';
@override @override
String get agoraRtcAppid => '4b5e5cea3b86476caf7f7a57d05b82d1'; String get agoraRtcAppid => '4b5e5cea3b86476caf7f7a57d05b82d1';
@override @override
num get gameAppid => 9999999999; // App ID num get gameAppid => 9999999999; // App ID
@ -86,10 +86,10 @@ class SCVariant1Config implements AppConfig {
String get gameAppChannel => 'yumi'; String get gameAppChannel => 'yumi';
@override @override
String get bigBroadcastGroup => '@TGS#2RUK4PK5C2'; String get bigBroadcastGroup => '@TGS#2RUK4PK5C2';
@override @override
String get imAdmin => 'c2c_yumiadmin'; String get imAdmin => 'c2c_yumiadmin';
@override @override
bool get isReview => true; bool get isReview => true;
@ -170,4 +170,4 @@ class SCVariant1Config implements AppConfig {
debugPrint('应用配置验证通过'); debugPrint('应用配置验证通过');
} }
} }
} }

View File

@ -1570,6 +1570,9 @@ class SCAppLocalizations {
String rewardCoins(String name) => String rewardCoins(String name) =>
translate('rewardCoins').replaceAll('{1}', name); translate('rewardCoins').replaceAll('{1}', name);
String signInRewardReceived(String name) =>
translate('signInRewardReceived').replaceAll('{1}', name);
String deleteAccount2(String name) => String deleteAccount2(String name) =>
translate('deleteAccount2').replaceAll('{1}', name); translate('deleteAccount2').replaceAll('{1}', name);

View File

@ -186,7 +186,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
if (gift != null && if (gift != null &&
(gift.giftSourceUrl ?? "").isNotEmpty && (gift.giftSourceUrl ?? "").isNotEmpty &&
scGiftHasFullScreenEffect(gift.special)) { scGiftHasFullScreenEffect(gift.special)) {
SCGiftVapSvgaManager().preload(gift.giftSourceUrl!, highPriority: true); SCGiftVapSvgaManager().preload(gift.giftSourceUrl!);
_giftFxLog( _giftFxLog(
'preload selected gift ' 'preload selected gift '
'giftId=${gift.id} ' 'giftId=${gift.id} '
@ -1443,10 +1443,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
debouncer.debounce( debouncer.debounce(
duration: Duration(milliseconds: 350), duration: Duration(milliseconds: 350),
onDebounce: () { onDebounce: () {
Provider.of<RtcProvider>( rtcProvider?.requestGiftTriggeredMicRefresh();
navigatorKey.currentState!.context,
listen: false,
).retrieveMicrophoneList();
}, },
); );
} }

View File

@ -9,7 +9,6 @@ import 'package:yumi/app/constants/sc_room_msg_type.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/app/constants/sc_screen.dart';
import 'package:yumi/shared/tools/sc_path_utils.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/shared/business_logic/models/res/mic_res.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/shared/data_sources/models/enum/sc_room_special_mike_type.dart'; import 'package:yumi/shared/data_sources/models/enum/sc_room_special_mike_type.dart';
@ -31,10 +30,7 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
"sc_images/room/sc_icon_room_seat_heartbeat_value.png"; "sc_images/room/sc_icon_room_seat_heartbeat_value.png";
RtcProvider? provider; RtcProvider? provider;
JoinRoomRes? room; _SeatRenderSnapshot? _cachedSnapshot;
MicRes? roomSeat;
JoinRoomRes? _cachedRoom;
MicRes? _cachedRoomSeat;
final GlobalKey _targetKey = GlobalKey(); final GlobalKey _targetKey = GlobalKey();
@override @override
@ -46,181 +42,314 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final liveRoomSeat = provider?.roomWheatMap[widget.index]; return Selector<RtcProvider, _SeatRenderSnapshot>(
final liveRoom = provider?.currenRoom; selector:
if (provider?.isExitingCurrentVoiceRoomSession ?? false) { (context, provider) =>
roomSeat = liveRoomSeat ?? _cachedRoomSeat; _SeatRenderSnapshot.fromProvider(provider, widget.index),
room = liveRoom ?? _cachedRoom; builder: (context, liveSnapshot, child) {
} else { final seatSnapshot = _resolveSeatSnapshot(liveSnapshot);
roomSeat = liveRoomSeat; final resolvedHeaddress =
room = liveRoom; SCPathUtils.getFileExtension(
if (roomSeat != null) { seatSnapshot.headdressSourceUrl,
_cachedRoomSeat = roomSeat; ).toLowerCase() ==
} ".mp4" &&
if (room != null) { window.locale.languageCode == "ar"
_cachedRoom = room; ? ""
} : seatSnapshot.headdressSourceUrl;
}
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: Column( onTap: () {
children: [ context.read<RtcProvider>().clickSite(widget.index);
SizedBox( },
key: _targetKey, child: Column(
width: widget.isGameModel ? 40.w : 55.w, children: [
height: widget.isGameModel ? 40.w : 55.w, SizedBox(
child: Stack( key: _targetKey,
alignment: Alignment.center, width: widget.isGameModel ? 40.w : 55.w,
children: [ height: widget.isGameModel ? 40.w : 55.w,
Sonic( child: Stack(
index: widget.index, alignment: Alignment.center,
room: room, children: [
isGameModel: widget.isGameModel, Sonic(
), index: widget.index,
roomSeat?.user != null specialMikeType: seatSnapshot.specialMikeType,
? head( isGameModel: widget.isGameModel,
url: roomSeat?.user?.userAvatar ?? "", ),
width: widget.isGameModel ? 40.w : 55.w, seatSnapshot.hasUser
headdress: ? head(
SCPathUtils.getFileExtension( url: seatSnapshot.userAvatar,
roomSeat?.user width: widget.isGameModel ? 40.w : 55.w,
?.getHeaddress() headdress: resolvedHeaddress,
?.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,
) )
: Image.asset( : (seatSnapshot.micLock
"sc_images/room/sc_icon_seat_open.png", ? Image.asset(
width: widget.isGameModel ? 38.w : 52.w, "sc_images/room/sc_icon_seat_lock.png",
height: widget.isGameModel ? 38.w : 52.w, width: widget.isGameModel ? 38.w : 52.w,
)), height: widget.isGameModel ? 38.w : 52.w,
Positioned( )
bottom: widget.isGameModel ? 2.w : 5.w, : Image.asset(
right: widget.isGameModel ? 2.w : 5.w, "sc_images/room/sc_icon_seat_open.png",
child: width: widget.isGameModel ? 38.w : 52.w,
(roomSeat?.micMute ?? false) height: widget.isGameModel ? 38.w : 52.w,
? Image.asset( )),
"sc_images/room/sc_icon_room_seat_mic_mute.png", Positioned(
width: 14.w, bottom: widget.isGameModel ? 2.w : 5.w,
height: 14.w, right: widget.isGameModel ? 2.w : 5.w,
) child:
: Container(), 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( widget.isGameModel
index: widget.index, ? Container()
isGameModel: widget.isGameModel, : (seatSnapshot.hasUser
), ? SizedBox(
), width: 64.w,
], child: Column(
),
),
widget.isGameModel
? Container()
: (roomSeat?.user != null
? SizedBox(
width: 64.w,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
msgRoleTag( Row(
roomSeat?.user?.roles ?? "", mainAxisSize: MainAxisSize.min,
width: 15.w, crossAxisAlignment: CrossAxisAlignment.center,
height: 15.w, 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( SizedBox(height: 2.w),
child: socialchatNickNameText( SizedBox(
fontWeight: FontWeight.w600, height: 10.w,
roomSeat?.user?.userNickname ?? "", child: Row(
fontSize: 10.sp, mainAxisAlignment: MainAxisAlignment.center,
type: roomSeat?.user?.getVIP()?.name ?? "", mainAxisSize: MainAxisSize.min,
needScroll: children: [
(roomSeat Image.asset(
?.user _seatHeartbeatValueIconAsset,
?.userNickname width: 8.w,
?.characters height: 8.w,
.length ?? fit: BoxFit.contain,
0) > ),
8, SizedBox(width: 2.w),
text(
_heartbeatVaFormat(
seatSnapshot.userHeartbeatValue,
),
fontWeight: FontWeight.w600,
fontSize: 8.sp,
lineHeight: 1,
),
],
), ),
), ),
], ],
), ),
SizedBox(height: 2.w), )
SizedBox( : Row(
height: 10.w, mainAxisSize: MainAxisSize.min,
child: Row( crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, children: [
mainAxisSize: MainAxisSize.min, SizedBox(height: 16.w),
children: [ text(
Image.asset( "NO.${widget.index}",
_seatHeartbeatValueIconAsset, fontSize: 10.sp,
width: 8.w, fontWeight: FontWeight.w600,
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,
),
],
)),
],
),
onTap: () {
Provider.of<RtcProvider>(
context,
listen: false,
).clickSite(widget.index);
}, },
); );
} }
String _heartbeatVaFormat() { _SeatRenderSnapshot _resolveSeatSnapshot(_SeatRenderSnapshot liveSnapshot) {
int value = (roomSeat?.user?.heartbeatVal ?? 0).toInt(); if (liveSnapshot.isExitingCurrentVoiceRoomSession) {
if (value >= 1000000) { return _cachedSnapshot ?? liveSnapshot;
return "${(value / 1000000).toStringAsFixed(1)}M";
} }
if (value >= 10000) { _cachedSnapshot = liveSnapshot;
return "${(value / 1000).toStringAsFixed(0)}k"; return liveSnapshot;
}
return "$value";
} }
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 { class Emoticons extends StatefulWidget {
@ -239,9 +368,9 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
late CurvedAnimation curvedAnimation; late CurvedAnimation curvedAnimation;
bool showIn = false; bool showIn = false;
MicRes? playingRes; _SeatEmojiEvent? playingEvent;
List<MicRes?> pathList = []; List<_SeatEmojiEvent> pathList = [];
String? giftPath; String? giftPath;
List<String> giftList = []; List<String> giftList = [];
bool showResult = false; bool showResult = false;
@ -267,23 +396,31 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<RtcProvider>( return Selector<RtcProvider, _SeatEmojiSnapshot>(
builder: (BuildContext context, RtcProvider provider, Widget? child) { selector:
MicRes? micRes = provider.roomWheatMap[widget.index]; (context, provider) =>
if (micRes?.user == null) { _SeatEmojiSnapshot.fromMic(provider.roomWheatMap[widget.index]),
builder: (
BuildContext context,
_SeatEmojiSnapshot snapshot,
Widget? child,
) {
if (!snapshot.hasUser) {
return Container(); return Container();
} }
String? emojiPath = provider.roomWheatMap[widget.index]?.emojiPath; if (snapshot.emojiPath != null) {
if (emojiPath != null) { pathList.add(
pathList.add(micRes); _SeatEmojiEvent(type: snapshot.type, number: snapshot.number),
micRes?.setEmojiPath = null; );
context.read<RtcProvider>().roomWheatMap[widget.index]?.setEmojiPath =
null;
_checkStart(); _checkStart();
} }
if (playingRes != null) { if (playingEvent != null) {
return FadeTransition( return FadeTransition(
opacity: curvedAnimation, opacity: curvedAnimation,
child: child:
playingRes?.type == SCRoomMsgType.roomDice playingEvent?.type == SCRoomMsgType.roomDice
? FutureBuilder<void>( ? FutureBuilder<void>(
future: Future.delayed(Duration(milliseconds: 2000)), future: Future.delayed(Duration(milliseconds: 2000)),
// Future // Future
@ -294,7 +431,7 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
// snapshot的状态来构建UI // snapshot的状态来构建UI
if (snapshot.connectionState == ConnectionState.done) { if (snapshot.connectionState == ConnectionState.done) {
return Image.asset( 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, height: widget.isGameModel ? 38.w : 45.w,
); );
} else { } else {
@ -305,7 +442,7 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
} }
}, },
) )
: (playingRes?.type == SCRoomMsgType.roomRPS : (playingEvent?.type == SCRoomMsgType.roomRPS
? FutureBuilder<void>( ? FutureBuilder<void>(
future: Future.delayed(Duration(milliseconds: 2000)), future: Future.delayed(Duration(milliseconds: 2000)),
// Future // Future
@ -317,7 +454,7 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
if (snapshot.connectionState == if (snapshot.connectionState ==
ConnectionState.done) { ConnectionState.done) {
return Image.asset( 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, height: widget.isGameModel ? 38.w : 45.w,
); );
} else { } else {
@ -329,7 +466,7 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
}, },
) )
: netImage( : netImage(
url: playingRes?.number ?? "", url: playingEvent?.number ?? "",
width: widget.isGameModel ? 65.w : 75.w, width: widget.isGameModel ? 65.w : 75.w,
)), )),
); );
@ -349,13 +486,10 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
} }
void _checkStart() { void _checkStart() {
print('emoticonsAniCtr.status:${emoticonsAniCtr.status}');
print('emoticonsAniCtr.path:${playingRes}');
if (!showIn) { if (!showIn) {
if (pathList.isNotEmpty) { if (pathList.isNotEmpty) {
playingRes = pathList.first; playingEvent = pathList.first;
pathList.removeAt(0); pathList.removeAt(0);
print('播放表情:$playingRes');
emoticonsAniCtr.forward(); emoticonsAniCtr.forward();
Future.delayed(Duration(milliseconds: 5)).then((value) { Future.delayed(Duration(milliseconds: 5)).then((value) {
setState(() { setState(() {
@ -367,13 +501,12 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
Future.delayed(Duration(seconds: 3)).then((value) { Future.delayed(Duration(seconds: 3)).then((value) {
emoticonsAniCtr.reverse(); emoticonsAniCtr.reverse();
showIn = false; showIn = false;
playingRes = null; playingEvent = null;
_checkStart(); _checkStart();
}); });
} else if (giftList.isNotEmpty) { } else if (giftList.isNotEmpty) {
giftPath = giftList.first; giftPath = giftList.first;
giftList.removeAt(0); giftList.removeAt(0);
print('播放礼物:$playingRes');
emoticonsAniCtr.forward(); emoticonsAniCtr.forward();
Future.delayed(Duration(milliseconds: 10)).then((value) { Future.delayed(Duration(milliseconds: 10)).then((value) {
setState(() { setState(() {
@ -396,12 +529,12 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
class Sonic extends StatefulWidget { class Sonic extends StatefulWidget {
final num index; final num index;
final bool isGameModel; final bool isGameModel;
final JoinRoomRes? room; final String specialMikeType;
const Sonic({ const Sonic({
Key? key, Key? key,
required this.index, required this.index,
required this.room, required this.specialMikeType,
this.isGameModel = false, this.isGameModel = false,
}) : super(key: key); }) : super(key: key);
@ -450,23 +583,33 @@ class _SonicState extends State<Sonic> with TickerProviderStateMixin {
return Stack( return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
SonicItem(ctrList[0], widget.room, isGameModel: widget.isGameModel), SonicItem(
SonicItem(ctrList[1], widget.room, isGameModel: widget.isGameModel), ctrList[0],
SonicItem(ctrList[2], widget.room, isGameModel: widget.isGameModel), widget.specialMikeType,
SonicItem(ctrList[3], widget.room, isGameModel: widget.isGameModel), 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 { void _checkSoundAni(int volume) async {
if (volume <= 20) return; if (volume <= 20) return;
// await Future.delayed(Duration(milliseconds: 10)); for (final element in ctrList) {
// var dateTime = DateTime.now();
// for(int i =0 ; i < 5000000;i++){
// lastIndex = 1;
// }
// print('执行耗时:${DateTime.now().millisecondsSinceEpoch - dateTime.millisecondsSinceEpoch}');
ctrList.forEach((element) {
if (element.value == 0) { if (element.value == 0) {
if (lastIndex == 0) { if (lastIndex == 0) {
element.forward(); element.forward();
@ -481,11 +624,10 @@ class _SonicState extends State<Sonic> with TickerProviderStateMixin {
} }
} }
} }
}); }
} }
_onVoiceChange(num index, int volum) { _onVoiceChange(num index, int volum) {
print('说话声音:$volum');
if (widget.index == index) { if (widget.index == index) {
_checkSoundAni(volum); _checkSoundAni(volum);
} }
@ -494,11 +636,15 @@ class _SonicState extends State<Sonic> with TickerProviderStateMixin {
class SonicItem extends AnimatedWidget { class SonicItem extends AnimatedWidget {
final Animation<double> animation; final Animation<double> animation;
final JoinRoomRes? room; final String specialMikeType;
final bool isGameModel; final bool isGameModel;
SonicItem(this.animation, this.room, {super.key, this.isGameModel = false}) SonicItem(
: super(listenable: animation); this.animation,
this.specialMikeType, {
super.key,
this.isGameModel = false,
}) : super(listenable: animation);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -510,15 +656,14 @@ class SonicItem extends AnimatedWidget {
height: width(isGameModel ? 34 : 52), height: width(isGameModel ? 34 : 52),
width: width(isGameModel ? 34 : 52), width: width(isGameModel ? 34 : 52),
decoration: decoration:
(room?.roomProfile?.roomSetting?.roomSpecialMikeType ?? "") specialMikeType.isEmpty ||
.isEmpty || (specialMikeType !=
(room?.roomProfile?.roomSetting?.roomSpecialMikeType !=
SCRoomSpecialMikeType.TYPE_VIP3.name && SCRoomSpecialMikeType.TYPE_VIP3.name &&
room?.roomProfile?.roomSetting?.roomSpecialMikeType != specialMikeType !=
SCRoomSpecialMikeType.TYPE_VIP4.name && SCRoomSpecialMikeType.TYPE_VIP4.name &&
room?.roomProfile?.roomSetting?.roomSpecialMikeType != specialMikeType !=
SCRoomSpecialMikeType.TYPE_VIP5.name && SCRoomSpecialMikeType.TYPE_VIP5.name &&
room?.roomProfile?.roomSetting?.roomSpecialMikeType != specialMikeType !=
SCRoomSpecialMikeType.TYPE_VIP6.name) SCRoomSpecialMikeType.TYPE_VIP6.name)
? BoxDecoration( ? BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
@ -528,25 +673,19 @@ class SonicItem extends AnimatedWidget {
) )
: BoxDecoration(), : BoxDecoration(),
child: child:
room?.roomProfile?.roomSetting?.roomSpecialMikeType == specialMikeType == SCRoomSpecialMikeType.TYPE_VIP3.name
SCRoomSpecialMikeType.TYPE_VIP3.name
? Image.asset( ? Image.asset(
"sc_images/room/sc_icon_room_vip3_sonic_anim.webp", "sc_images/room/sc_icon_room_vip3_sonic_anim.webp",
) )
: (room?.roomProfile?.roomSetting?.roomSpecialMikeType == : (specialMikeType == SCRoomSpecialMikeType.TYPE_VIP4.name
SCRoomSpecialMikeType.TYPE_VIP4.name
? Image.asset( ? Image.asset(
"sc_images/room/sc_icon_room_vip4_sonic_anim.webp", "sc_images/room/sc_icon_room_vip4_sonic_anim.webp",
) )
: (room?.roomProfile?.roomSetting?.roomSpecialMikeType == : (specialMikeType == SCRoomSpecialMikeType.TYPE_VIP5.name
SCRoomSpecialMikeType.TYPE_VIP5.name
? Image.asset( ? Image.asset(
"sc_images/room/sc_icon_room_vip5_sonic_anim.webp", "sc_images/room/sc_icon_room_vip5_sonic_anim.webp",
) )
: (room : (specialMikeType ==
?.roomProfile
?.roomSetting
?.roomSpecialMikeType ==
SCRoomSpecialMikeType.TYPE_VIP6.name SCRoomSpecialMikeType.TYPE_VIP6.name
? Image.asset( ? Image.asset(
"sc_images/room/sc_icon_room_vip6_sonic_anim.webp", "sc_images/room/sc_icon_room_vip6_sonic_anim.webp",

View File

@ -141,17 +141,16 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
SCGiftVapSvgaManager().stopPlayback(); SCGiftVapSvgaManager().stopPlayback();
} }
bool roomThemeBackActi(JoinRoomRes? room) { String? _resolveRoomThemeBackground(JoinRoomRes? room) {
if (room?.roomProps?.roomTheme != null) { final roomTheme = room?.roomProps?.roomTheme;
if (room?.roomProps?.roomTheme?.themeBack != null && final themeBack = roomTheme?.themeBack ?? "";
room!.roomProps!.roomTheme!.themeBack!.isNotEmpty) { if (themeBack.isEmpty) {
if ((room.roomProps?.roomTheme?.expireTime ?? 0) > return null;
DateTime.now().millisecondsSinceEpoch) {
return true;
}
}
} }
return false; if ((roomTheme?.expireTime ?? 0) <= DateTime.now().millisecondsSinceEpoch) {
return null;
}
return themeBack;
} }
@override @override
@ -173,13 +172,14 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
top: false, top: false,
child: Stack( child: Stack(
children: [ children: [
Consumer<RtcProvider>( Selector<RtcProvider, String?>(
builder: (context, ref, child) { selector:
return roomThemeBackActi(ref.currenRoom) (context, provider) =>
_resolveRoomThemeBackground(provider.currenRoom),
builder: (context, roomThemeBackground, child) {
return roomThemeBackground != null
? netImage( ? netImage(
url: url: roomThemeBackground,
ref.currenRoom?.roomProps?.roomTheme?.themeBack ??
"",
width: ScreenUtil().screenWidth, width: ScreenUtil().screenWidth,
height: ScreenUtil().screenHeight, height: ScreenUtil().screenHeight,
noDefaultImg: true, noDefaultImg: true,
@ -395,6 +395,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
giftModel.sendUserPic = msg.user?.userAvatar ?? ""; giftModel.sendUserPic = msg.user?.userAvatar ?? "";
giftModel.giftPic = msg.gift?.giftPhoto ?? ""; giftModel.giftPic = msg.gift?.giftPhoto ?? "";
giftModel.giftCount = 0; giftModel.giftCount = 0;
giftModel.rewardAmount = awardAmount;
giftModel.showLuckyRewardFrame = true; giftModel.showLuckyRewardFrame = true;
giftModel.rewardAmountText = _formatLuckyRewardAmount(awardAmount); giftModel.rewardAmountText = _formatLuckyRewardAmount(awardAmount);
Provider.of<GiftAnimationManager>( Provider.of<GiftAnimationManager>(

View File

@ -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) { function ensureChannelAlias(name, handler) {
if (!window[name] || typeof window[name].postMessage !== 'function') { if (!window[name] || typeof window[name].postMessage !== 'function') {
window[name] = { postMessage: handler }; window[name] = { postMessage: handler };
@ -146,19 +156,27 @@ class BaishunJsBridge {
} }
} }
window.NativeBridge.getConfigSync = function() { window.NativeBridge.getConfigSync = function() {
postDebug('bridge.getConfigSync', {
hasConfig: !!window.__baishunLastConfig,
hasNativeBridgeConfig: !!(window.NativeBridge && window.NativeBridge.config)
});
return window.__baishunLastConfig || null; return window.__baishunLastConfig || null;
}; };
window.NativeBridge.getConfig = function(params) { window.NativeBridge.getConfig = function(params) {
postDebug('bridge.getConfig.call', toPayload(params));
postAction('${BaishunBridgeActions.getConfig}', params); postAction('${BaishunBridgeActions.getConfig}', params);
return window.__baishunLastConfig || null; return window.__baishunLastConfig || null;
}; };
window.NativeBridge.destroy = function(payload) { window.NativeBridge.destroy = function(payload) {
postDebug('bridge.destroy.call', toPayload(payload));
postAction('${BaishunBridgeActions.destroy}', payload); postAction('${BaishunBridgeActions.destroy}', payload);
}; };
window.NativeBridge.gameRecharge = function(payload) { window.NativeBridge.gameRecharge = function(payload) {
postDebug('bridge.gameRecharge.call', toPayload(payload));
postAction('${BaishunBridgeActions.gameRecharge}', payload); postAction('${BaishunBridgeActions.gameRecharge}', payload);
}; };
window.NativeBridge.gameLoaded = function(payload) { window.NativeBridge.gameLoaded = function(payload) {
postDebug('bridge.gameLoaded.call', toPayload(payload));
postAction('${BaishunBridgeActions.gameLoaded}', payload); postAction('${BaishunBridgeActions.gameLoaded}', payload);
}; };
window.__baishunDebugLog = postDebug; window.__baishunDebugLog = postDebug;
@ -359,6 +377,10 @@ class BaishunJsBridge {
if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') { if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
window.dispatchEvent(new CustomEvent('baishunBridgeReady')); 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() { (function() {
const config = $payload; const config = $payload;
const explicitCallbackPath = $encodedCallbackPath; 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) { function resolvePath(path) {
if (!path || typeof path !== 'string') { if (!path || typeof path !== 'string') {
return null; return null;
@ -407,15 +439,39 @@ class BaishunJsBridge {
window.baishunBridgeConfig = config; window.baishunBridgeConfig = config;
window.NativeBridge = window.NativeBridge || {}; window.NativeBridge = window.NativeBridge || {};
window.NativeBridge.config = config; window.NativeBridge.config = config;
const callbackPath = explicitCallbackPath || window.__baishunLastJsCallback || '';
let callbackInvoked = false;
if (explicitCallbackPath) { if (explicitCallbackPath) {
window.__baishunLastJsCallback = 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') { if (typeof window.__baishunGetConfigCallback === 'function') {
window.__baishunGetConfigCallback(config); window.__baishunGetConfigCallback(config);
window.__baishunGetConfigCallback = null; window.__baishunGetConfigCallback = null;
} }
if (window.__baishunLastJsCallback) { 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') { if (typeof window.onBaishunConfig === 'function') {
window.onBaishunConfig(config); window.onBaishunConfig(config);
@ -438,6 +494,9 @@ class BaishunJsBridge {
return ''' return '''
(function() { (function() {
const payload = $safePayload; const payload = $safePayload;
if (typeof window.__baishunDebugLog === 'function') {
window.__baishunDebugLog('wallet.update', payload);
}
if (window.baishunChannel && typeof window.baishunChannel.walletUpdate === 'function') { if (window.baishunChannel && typeof window.baishunChannel.walletUpdate === 'function') {
window.baishunChannel.walletUpdate(payload); window.baishunChannel.walletUpdate(payload);
} }

View File

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
@ -31,6 +32,8 @@ class BaishunGamePage extends StatefulWidget {
} }
class _BaishunGamePageState extends State<BaishunGamePage> { class _BaishunGamePageState extends State<BaishunGamePage> {
static const String _logPrefix = '[BaishunGame]';
final RoomGameRepository _repository = RoomGameRepository(); final RoomGameRepository _repository = RoomGameRepository();
final Set<Factory<OneSequenceGestureRecognizer>> _webGestureRecognizers = final Set<Factory<OneSequenceGestureRecognizer>> _webGestureRecognizers =
<Factory<OneSequenceGestureRecognizer>>{ <Factory<OneSequenceGestureRecognizer>>{
@ -45,6 +48,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
bool _didReceiveBridgeMessage = false; bool _didReceiveBridgeMessage = false;
bool _didFinishPageLoad = false; bool _didFinishPageLoad = false;
bool _hasDeliveredLaunchConfig = false; bool _hasDeliveredLaunchConfig = false;
int _bridgeInjectCount = 0;
String? _errorMessage; String? _errorMessage;
@override @override
@ -92,15 +96,21 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
) )
..setNavigationDelegate( ..setNavigationDelegate(
NavigationDelegate( NavigationDelegate(
onPageStarted: (String _) { onPageStarted: (String url) {
_log('page_started url=${_clip(url, 240)}');
_prepareForPageLoad(); _prepareForPageLoad();
}, },
onPageFinished: (String _) async { onPageFinished: (String url) async {
_didFinishPageLoad = true; _didFinishPageLoad = true;
_log('page_finished url=${_clip(url, 240)}');
await _injectBridge(reason: 'page_finished'); await _injectBridge(reason: 'page_finished');
}, },
onWebResourceError: (WebResourceError error) { 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) { if (!mounted) {
return; return;
} }
@ -111,36 +121,56 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
}, },
), ),
); );
_log('init launch=${_stringifyForLog(_buildLaunchSummary())}');
unawaited(_loadGameEntry()); unawaited(_loadGameEntry());
} }
@override @override
void dispose() { void dispose() {
_stopBridgeBootstrap(); _log('dispose isClosing=$_isClosing');
_stopBridgeBootstrap(reason: 'dispose');
super.dispose(); super.dispose();
} }
void _prepareForPageLoad() { void _prepareForPageLoad() {
_log(
'prepare_page_load session=${widget.launchModel.gameSessionId} '
'entry=${_clip(widget.launchModel.entry.entryUrl, 240)}',
);
_didReceiveBridgeMessage = false; _didReceiveBridgeMessage = false;
_didFinishPageLoad = false; _didFinishPageLoad = false;
_hasDeliveredLaunchConfig = false; _hasDeliveredLaunchConfig = false;
_stopBridgeBootstrap(); _bridgeInjectCount = 0;
_stopBridgeBootstrap(reason: 'prepare_page_load');
_bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), ( _bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), (
Timer timer, Timer timer,
) { ) {
if (!mounted || _didReceiveBridgeMessage || _errorMessage != null) { if (!mounted || _didReceiveBridgeMessage || _errorMessage != null) {
_log(
'bootstrap_timer_stop tick=${timer.tick} '
'didReceiveBridge=$_didReceiveBridgeMessage '
'hasError=${_errorMessage != null}',
);
timer.cancel(); timer.cancel();
return; return;
} }
unawaited(_injectBridge(reason: 'bootstrap')); unawaited(_injectBridge(reason: 'bootstrap'));
if (_didFinishPageLoad && timer.tick >= 12) { if (_didFinishPageLoad && timer.tick >= 12) {
_log(
'bootstrap_timer_stop tick=${timer.tick} reason=page_finished_guard',
);
timer.cancel(); timer.cancel();
} }
}); });
_loadingFallbackTimer = Timer(const Duration(seconds: 6), () { _loadingFallbackTimer = Timer(const Duration(seconds: 6), () {
if (!mounted || _didReceiveBridgeMessage || _errorMessage != null) { if (!mounted || _didReceiveBridgeMessage || _errorMessage != null) {
_log(
'loading_fallback_skip didReceiveBridge=$_didReceiveBridgeMessage '
'hasError=${_errorMessage != null}',
);
return; return;
} }
_log('loading_fallback_fire isLoading=$_isLoading');
setState(() { setState(() {
_isLoading = false; _isLoading = false;
}); });
@ -154,7 +184,10 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
}); });
} }
void _stopBridgeBootstrap() { void _stopBridgeBootstrap({String reason = 'manual'}) {
if (_bridgeBootstrapTimer != null || _loadingFallbackTimer != null) {
_log('stop_bootstrap reason=$reason');
}
_bridgeBootstrapTimer?.cancel(); _bridgeBootstrapTimer?.cancel();
_bridgeBootstrapTimer = null; _bridgeBootstrapTimer = null;
_loadingFallbackTimer?.cancel(); _loadingFallbackTimer?.cancel();
@ -165,10 +198,15 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
try { try {
_prepareForPageLoad(); _prepareForPageLoad();
final entryUrl = widget.launchModel.entry.entryUrl.trim(); 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://')) { if (entryUrl.isEmpty || entryUrl.startsWith('mock://')) {
final html = await rootBundle.loadString( final html = await rootBundle.loadString(
'assets/debug/baishun_mock/index.html', 'assets/debug/baishun_mock/index.html',
); );
_log('load_mock_html baseUrl=https://baishun.mock.local/');
await _controller.loadHtmlString( await _controller.loadHtmlString(
html, html,
baseUrl: 'https://baishun.mock.local/', baseUrl: 'https://baishun.mock.local/',
@ -180,8 +218,10 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
if (uri == null) { if (uri == null) {
throw Exception('Invalid game entry url: $entryUrl'); throw Exception('Invalid game entry url: $entryUrl');
} }
_log('load_request uri=${_clip(uri.toString(), 240)}');
await _controller.loadRequest(uri); await _controller.loadRequest(uri);
} catch (error) { } catch (error) {
_log('load_entry_error error=${_clip(error.toString(), 400)}');
if (!mounted) { if (!mounted) {
return; return;
} }
@ -193,9 +233,19 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
} }
Future<void> _injectBridge({String reason = 'manual'}) async { Future<void> _injectBridge({String reason = 'manual'}) async {
_bridgeInjectCount += 1;
if (reason != 'bootstrap' ||
_bridgeInjectCount <= 3 ||
_bridgeInjectCount % 5 == 0) {
_log('inject_bridge reason=$reason count=$_bridgeInjectCount');
}
try { try {
await _controller.runJavaScript(BaishunJsBridge.bootstrapScript()); await _controller.runJavaScript(BaishunJsBridge.bootstrapScript());
} catch (_) {} } catch (error) {
_log(
'inject_bridge_error reason=$reason error=${_clip(error.toString(), 300)}',
);
}
} }
Future<void> _sendConfigToGame({ Future<void> _sendConfigToGame({
@ -203,11 +253,16 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
bool force = false, bool force = false,
}) async { }) async {
if (_hasDeliveredLaunchConfig && !force) { if (_hasDeliveredLaunchConfig && !force) {
_log('skip_send_config reason=already_delivered');
return; return;
} }
_hasDeliveredLaunchConfig = true; _hasDeliveredLaunchConfig = true;
final rawConfig = widget.launchModel.bridgeConfig; final rawConfig = widget.launchModel.bridgeConfig;
final config = _buildEffectiveBridgeConfig(rawConfig); final config = _buildEffectiveBridgeConfig(rawConfig);
_log(
'send_config jsCallback=${jsCallbackPath ?? ''} '
'force=$force config=${_stringifyForLog(_buildConfigSummary(config, rawConfig: rawConfig))}',
);
try { try {
await _controller.runJavaScript( await _controller.runJavaScript(
BaishunJsBridge.buildConfigScript( BaishunJsBridge.buildConfigScript(
@ -215,10 +270,13 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
jsCallbackPath: jsCallbackPath, jsCallbackPath: jsCallbackPath,
), ),
); );
} catch (_) {} } catch (error) {
_log('send_config_error error=${_clip(error.toString(), 300)}');
}
} }
Future<void> _handleBridgeMessage(JavaScriptMessage message) async { Future<void> _handleBridgeMessage(JavaScriptMessage message) async {
_log('channel_message raw=${_clip(message.message, 600)}');
final bridgeMessage = BaishunBridgeMessage.parse(message.message); final bridgeMessage = BaishunBridgeMessage.parse(message.message);
await _dispatchBridgeMessage(bridgeMessage); await _dispatchBridgeMessage(bridgeMessage);
} }
@ -231,6 +289,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
action, action,
message.message, message.message,
); );
_log('named_channel action=$action raw=${_clip(message.message, 600)}');
await _dispatchBridgeMessage(bridgeMessage); await _dispatchBridgeMessage(bridgeMessage);
} }
@ -238,12 +297,20 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
BaishunBridgeMessage bridgeMessage, BaishunBridgeMessage bridgeMessage,
) async { ) async {
if (bridgeMessage.action == BaishunBridgeActions.debugLog) { 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; return;
} }
final callbackPath = _extractCallbackPath(bridgeMessage.payload); final callbackPath = _extractCallbackPath(bridgeMessage.payload);
_log(
'bridge_action action=${bridgeMessage.action} '
'callback=${callbackPath.isEmpty ? '-' : callbackPath} '
'payload=${_stringifyForLog(_sanitizeForLog(bridgeMessage.payload))}',
);
if (bridgeMessage.action.isNotEmpty) { if (bridgeMessage.action.isNotEmpty) {
_didReceiveBridgeMessage = true; _didReceiveBridgeMessage = true;
_stopBridgeBootstrap(); _stopBridgeBootstrap(reason: 'bridge_message_${bridgeMessage.action}');
} }
switch (bridgeMessage.action) { switch (bridgeMessage.action) {
case BaishunBridgeActions.getConfig: case BaishunBridgeActions.getConfig:
@ -255,35 +322,43 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
if (!mounted) { if (!mounted) {
return; return;
} }
_log('game_loaded received');
setState(() { setState(() {
_isLoading = false; _isLoading = false;
_errorMessage = null; _errorMessage = null;
}); });
break; break;
case BaishunBridgeActions.gameRecharge: case BaishunBridgeActions.gameRecharge:
_log('game_recharge open_wallet');
await SCNavigatorUtils.push(context, WalletRoute.recharge); await SCNavigatorUtils.push(context, WalletRoute.recharge);
await _notifyWalletUpdate(); await _notifyWalletUpdate();
break; break;
case BaishunBridgeActions.destroy: case BaishunBridgeActions.destroy:
_log('destroy requested by h5');
await _closeAndExit(reason: 'h5_destroy'); await _closeAndExit(reason: 'h5_destroy');
break; break;
default: default:
_log('bridge_action_unhandled action=${bridgeMessage.action}');
break; break;
} }
} }
Future<void> _notifyWalletUpdate() async { Future<void> _notifyWalletUpdate() async {
_log('notify_wallet_update');
try { try {
await _controller.runJavaScript( await _controller.runJavaScript(
BaishunJsBridge.buildWalletUpdateScript(), BaishunJsBridge.buildWalletUpdateScript(),
); );
} catch (_) {} } catch (error) {
_log('notify_wallet_update_error error=${_clip(error.toString(), 300)}');
}
} }
Future<void> _reload() async { Future<void> _reload() async {
if (!mounted) { if (!mounted) {
return; return;
} }
_log('reload');
setState(() { setState(() {
_errorMessage = null; _errorMessage = null;
_isLoading = true; _isLoading = true;
@ -293,19 +368,27 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
Future<void> _closeAndExit({String reason = 'page_back'}) async { Future<void> _closeAndExit({String reason = 'page_back'}) async {
if (_isClosing) { if (_isClosing) {
_log('close_skip reason=already_closing source=$reason');
return; return;
} }
_isClosing = true; _isClosing = true;
_log('close_start reason=$reason');
try { try {
if (!widget.launchModel.gameSessionId.startsWith('bs_mock_')) { if (!widget.launchModel.gameSessionId.startsWith('bs_mock_')) {
await _repository.closeBaishunGame( final result = await _repository.closeBaishunGame(
roomId: widget.roomId, roomId: widget.roomId,
gameSessionId: widget.launchModel.gameSessionId, gameSessionId: widget.launchModel.gameSessionId,
reason: reason, reason: reason,
); );
_log(
'close_success result=${_stringifyForLog(<String, dynamic>{'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) { if (mounted) {
_log('close_pop');
Navigator.of(context).pop(); Navigator.of(context).pop();
} }
} }
@ -324,6 +407,110 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
return ''; 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 = <String, dynamic>{};
for (final MapEntry<dynamic, dynamic> 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<String, dynamic> _buildLaunchSummary() {
final entry = widget.launchModel.entry;
final roomState = widget.launchModel.roomState;
return <String, dynamic>{
'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<String, dynamic> _buildConfigSummary(
BaishunBridgeConfigModel config, {
BaishunBridgeConfigModel? rawConfig,
}) {
return <String, dynamic>{
'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 _buildEffectiveBridgeConfig(
BaishunBridgeConfigModel rawConfig, BaishunBridgeConfigModel rawConfig,
) { ) {

View File

@ -22,6 +22,7 @@ class RoomGameListSheet extends StatefulWidget {
} }
class _RoomGameListSheetState extends State<RoomGameListSheet> { class _RoomGameListSheetState extends State<RoomGameListSheet> {
static const String _logPrefix = '[BaishunLaunch]';
static const int _itemsPerRow = 4; static const int _itemsPerRow = 4;
static const String _sheetFrameAsset = static const String _sheetFrameAsset =
'sc_images/room/sc_room_game_sheet_frame.png'; 'sc_images/room/sc_room_game_sheet_frame.png';
@ -78,11 +79,31 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
}); });
try { 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( final launchModel = await _repository.launchBaishunGame(
roomId: _roomId, roomId: _roomId,
gameId: game.gameId, gameId: game.gameId,
clientOrigin: Platform.isAndroid ? 'ANDROID' : 'IOS', clientOrigin: Platform.isAndroid ? 'ANDROID' : 'IOS',
); );
_log(
'launch_success payload=${_stringifyForLog(<String, dynamic>{
'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': <String, dynamic>{'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) { if (!mounted) {
return; return;
} }
@ -99,6 +120,7 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
barrierDismissible: true, barrierDismissible: true,
); );
} catch (error) { } catch (error) {
_log('launch_error error=${_clip(error.toString(), 400)}');
SCTts.show('Launch failed'); SCTts.show('Launch failed');
} finally { } finally {
if (mounted) { if (mounted) {
@ -109,6 +131,36 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
} }
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return SafeArea(

File diff suppressed because it is too large Load Diff

View File

@ -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/big_broadcast_group_message.dart';
import 'package:yumi/shared/data_sources/models/message/sc_floating_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/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/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_public_message_page_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_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 { class RealTimeMessagingManager extends ChangeNotifier {
static const int _giftComboMergeWindowMs = 3000; static const int _giftComboMergeWindowMs = 3000;
static const int _maxLuckGiftPushQueueLength = 12; static const int _maxLuckGiftPushQueueLength = 12;
static const int _luckyGiftFloatMinMultiple = 5;
static const int _luckyGiftBurstMinMultiple = 10;
static const int _luckyGiftBurstMinAwardAmount = 5000;
BuildContext? context; BuildContext? context;
@ -119,6 +123,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
int notifcationUnReadCount = 0; int notifcationUnReadCount = 0;
SCBroadCastLuckGiftPush? currentPlayingLuckGift; SCBroadCastLuckGiftPush? currentPlayingLuckGift;
final Queue<SCBroadCastLuckGiftPush?> _luckGiftPushQueue = Queue(); final Queue<SCBroadCastLuckGiftPush?> _luckGiftPushQueue = Queue();
String? _currentLuckGiftPushKey;
Debouncer debouncer = Debouncer(); Debouncer debouncer = Debouncer();
List<V2TimConversation> conversationList = []; List<V2TimConversation> conversationList = [];
@ -534,6 +539,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
Future<V2TimCallback> joinRoomGroup(String groupID, String message) async { Future<V2TimCallback> joinRoomGroup(String groupID, String message) async {
_luckGiftPushQueue.clear(); _luckGiftPushQueue.clear();
currentPlayingLuckGift = null; currentPlayingLuckGift = null;
_currentLuckGiftPushKey = null;
var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup( var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup(
groupID: groupID, groupID: groupID,
message: message, message: message,
@ -1025,7 +1031,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
required String source, required String source,
}) { }) {
final rewardData = broadCastRes.data; 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; return;
} }
if (source == 'broadcast' && _isLuckyGiftInCurrentRoom(broadCastRes)) { if (source == 'broadcast' && _isLuckyGiftInCurrentRoom(broadCastRes)) {
@ -1428,93 +1441,103 @@ class RealTimeMessagingManager extends ChangeNotifier {
} }
} else if (msg.type == SCRoomMsgType.gift) { } else if (msg.type == SCRoomMsgType.gift) {
final gift = msg.gift; final gift = msg.gift;
final special = gift?.special ?? ""; if (gift == null) {
final giftSourceUrl = gift?.giftSourceUrl ?? ""; _giftFxLog(
final hasSource = giftSourceUrl.isNotEmpty; 'recv gift msg skipped reason=no_gift '
final hasAnimation = scGiftHasAnimationSpecial(special); 'fromUserId=${msg.user?.id} '
final hasGlobalGift = special.contains(SCGiftType.GLOBAL_GIFT.name); 'toUserId=${msg.toUser?.id} '
final hasFullScreenEffect = scGiftHasFullScreenEffect(special); 'quantity=${msg.number}',
_giftFxLog( );
'recv gift msg ' } else {
'fromUserId=${msg.user?.id} ' final rtcProvider = Provider.of<RealTimeCommunicationManager>(
'fromUserName=${msg.user?.userNickname} ' context!,
'toUserId=${msg.toUser?.id} ' listen: false,
'toUserName=${msg.toUser?.userNickname} ' );
'giftId=${gift?.id} ' final special = gift.special ?? "";
'giftName=${gift?.giftName} ' final giftSourceUrl = gift.giftSourceUrl ?? "";
'giftSourceUrl=$giftSourceUrl ' final hasSource = giftSourceUrl.isNotEmpty;
'special=$special ' final hasAnimation = scGiftHasAnimationSpecial(special);
'hasSource=$hasSource ' final hasGlobalGift = special.contains(
'hasAnimation=$hasAnimation ' SCGiftType.GLOBAL_GIFT.name,
'hasGlobalGift=$hasGlobalGift ' );
'hasFullScreenEffect=$hasFullScreenEffect ' final hasFullScreenEffect = scGiftHasFullScreenEffect(special);
'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}', _giftFxLog(
); 'recv gift msg '
if (msg.gift!.giftSourceUrl != null && msg.gift!.special != null) { 'fromUserId=${msg.user?.id} '
if (scGiftHasFullScreenEffect(msg.gift!.special)) { 'fromUserName=${msg.user?.userNickname} '
if (SCGlobalConfig.isGiftSpecialEffects && 'toUserId=${msg.toUser?.id} '
Provider.of<RealTimeCommunicationManager>( 'toUserName=${msg.toUser?.userNickname} '
context!, 'giftId=${gift.id} '
listen: false, 'giftName=${gift.giftName} '
).shouldShowRoomVisualEffects) { 'giftSourceUrl=$giftSourceUrl '
_giftFxLog( 'special=$special '
'trigger player play path=${msg.gift!.giftSourceUrl} ' 'hasSource=$hasSource '
'giftId=${msg.gift?.id} giftName=${msg.gift?.giftName}', 'hasAnimation=$hasAnimation '
); 'hasGlobalGift=$hasGlobalGift '
SCGiftVapSvgaManager().play(msg.gift!.giftSourceUrl!); '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 { } else {
_giftFxLog( _giftFxLog(
'skip player play because visual effects disabled ' 'skip player play because special does not include '
'giftId=${msg.gift?.id} ' '${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} '
'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} ' 'giftId=${gift.id} special=${gift.special}',
'roomVisible=${Provider.of<RealTimeCommunicationManager>(context!, listen: false).shouldShowRoomVisualEffects}',
); );
} }
} else { } else {
_giftFxLog( _giftFxLog(
'skip player play because special does not include ' 'skip player play because giftSourceUrl or special is empty '
'${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} ' 'giftId=${gift.id} '
'giftId=${msg.gift?.id} special=${msg.gift?.special}', '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<RealTimeCommunicationManager>(
context!,
listen: false,
).currenRoom?.roomProfile?.roomSetting?.showHeartbeat ??
false) {
debouncer.debounce(
duration: Duration(milliseconds: 350),
onDebounce: () {
Provider.of<RealTimeCommunicationManager>(
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) { } else if (msg.type == SCRoomMsgType.luckGiftAnimOther) {
final hideLGiftAnimal = final hideLGiftAnimal =
@ -1686,6 +1709,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
roomChatMsgList.clear(); roomChatMsgList.clear();
_luckGiftPushQueue.clear(); _luckGiftPushQueue.clear();
currentPlayingLuckGift = null; currentPlayingLuckGift = null;
_currentLuckGiftPushKey = null;
onNewMessageListenerGroupMap.forEach((k, v) { onNewMessageListenerGroupMap.forEach((k, v) {
v = null; v = null;
}); });
@ -1694,6 +1718,20 @@ class RealTimeMessagingManager extends ChangeNotifier {
void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) { void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) {
if (SCGlobalConfig.isLuckGiftSpecialEffects) { 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) { while (_luckGiftPushQueue.length >= _maxLuckGiftPushQueueLength) {
_luckGiftPushQueue.removeFirst(); _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() { void cleanLuckGiftBackCoins() {
_luckGiftPushQueue.clear(); _luckGiftPushQueue.clear();
_currentLuckGiftPushKey = null;
} }
void playLuckGiftBackCoins() { void playLuckGiftBackCoins() {
@ -1711,14 +1760,34 @@ class RealTimeMessagingManager extends ChangeNotifier {
return; return;
} }
currentPlayingLuckGift = _luckGiftPushQueue.removeFirst(); currentPlayingLuckGift = _luckGiftPushQueue.removeFirst();
_currentLuckGiftPushKey =
currentPlayingLuckGift == null
? null
: _luckyGiftPushEventKey(currentPlayingLuckGift!);
notifyListeners(); notifyListeners();
Future.delayed(Duration(milliseconds: 3000), () { Future.delayed(Duration(milliseconds: 3000), () {
currentPlayingLuckGift = null; currentPlayingLuckGift = null;
_currentLuckGiftPushKey = null;
notifyListeners(); notifyListeners();
playLuckGiftBackCoins(); 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) { void updateNotificationCount(int count) {
notifcationUnReadCount = 0; notifcationUnReadCount = 0;
allUnReadCount = allUnReadCount =

View File

@ -17,6 +17,8 @@ import '../../shared/data_sources/models/enum/sc_banner_type.dart';
import '../../shared/data_sources/models/enum/sc_gift_type.dart'; import '../../shared/data_sources/models/enum/sc_gift_type.dart';
class SCAppGeneralManager extends ChangeNotifier { class SCAppGeneralManager extends ChangeNotifier {
static const int _maxInitialFullScreenGiftPreloads = 4;
List<CountryMode> countryModeList = []; List<CountryMode> countryModeList = [];
final Map<String, List<Country>> _countryMap = {}; final Map<String, List<Country>> _countryMap = {};
final Map<String, Country> _countryByNameMap = {}; final Map<String, Country> _countryByNameMap = {};
@ -276,6 +278,8 @@ class SCAppGeneralManager extends ChangeNotifier {
} }
void downLoad(List<SocialChatGiftRes> giftResList) { void downLoad(List<SocialChatGiftRes> giftResList) {
final scheduledPaths = <String>{};
var scheduledCount = 0;
for (var gift in giftResList) { for (var gift in giftResList) {
final giftSourceUrl = gift.giftSourceUrl ?? ""; final giftSourceUrl = gift.giftSourceUrl ?? "";
if (giftSourceUrl.isEmpty) { if (giftSourceUrl.isEmpty) {
@ -284,7 +288,14 @@ class SCAppGeneralManager extends ChangeNotifier {
if (!scGiftHasFullScreenEffect(gift.special)) { if (!scGiftHasFullScreenEffect(gift.special)) {
continue; continue;
} }
if (!scheduledPaths.add(giftSourceUrl)) {
continue;
}
SCGiftVapSvgaManager().preload(giftSourceUrl); SCGiftVapSvgaManager().preload(giftSourceUrl);
scheduledCount += 1;
if (scheduledCount >= _maxInitialFullScreenGiftPreloads) {
break;
}
} }
} }

View File

@ -81,6 +81,7 @@ class GiftAnimationManager extends ChangeNotifier {
if (target.giftName.isEmpty) { if (target.giftName.isEmpty) {
target.giftName = incoming.giftName; target.giftName = incoming.giftName;
} }
target.rewardAmount = target.rewardAmount + incoming.rewardAmount;
if (incoming.rewardAmountText.isNotEmpty) { if (incoming.rewardAmountText.isNotEmpty) {
target.rewardAmountText = incoming.rewardAmountText; target.rewardAmountText = incoming.rewardAmountText;
} }

View File

@ -17,7 +17,7 @@ class MicRes {
SocialChatUserProfile? user, SocialChatUserProfile? user,
String? type, String? type,
String? roomToken, String? roomToken,
String?number String? number,
}) { }) {
_roomId = roomId; _roomId = roomId;
_micIndex = micIndex; _micIndex = micIndex;
@ -35,7 +35,10 @@ class MicRes {
_micIndex = json['micIndex']; _micIndex = json['micIndex'];
_micLock = json['micLock']; _micLock = json['micLock'];
_micMute = json['micMute']; _micMute = json['micMute'];
_user = json['user'] != null ? SocialChatUserProfile.fromJson(json['user']) : null; _user =
json['user'] != null
? SocialChatUserProfile.fromJson(json['user'])
: null;
_roomToken = json['roomToken']; _roomToken = json['roomToken'];
_type = json['type']; _type = json['type'];
_number = json['number']; _number = json['number'];
@ -59,6 +62,7 @@ class MicRes {
bool? micMute, bool? micMute,
String? type, String? type,
SocialChatUserProfile? user, SocialChatUserProfile? user,
bool clearUser = false,
String? roomToken, String? roomToken,
String? emojiPath, String? emojiPath,
String? number, String? number,
@ -67,7 +71,7 @@ class MicRes {
micIndex: micIndex ?? _micIndex, micIndex: micIndex ?? _micIndex,
micLock: micLock ?? _micLock, micLock: micLock ?? _micLock,
micMute: micMute ?? _micMute, micMute: micMute ?? _micMute,
user: user ?? _user, user: clearUser ? null : user ?? _user,
roomToken: roomToken ?? _roomToken, roomToken: roomToken ?? _roomToken,
emojiPath: emojiPath ?? _emojiPath, emojiPath: emojiPath ?? _emojiPath,
type: type ?? _type, type: type ?? _type,

View File

@ -13,7 +13,6 @@ class SCFloatingMessage {
num? number; num? number;
num? multiple; num? multiple;
int priority = 10; // int priority = 10; //
int aggregateVersion = 0;
SCFloatingMessage({ SCFloatingMessage({
this.type = 0, this.type = 0,
@ -30,7 +29,6 @@ class SCFloatingMessage {
this.coins = 0, this.coins = 0,
this.priority = 10, this.priority = 10,
this.multiple = 10, this.multiple = 10,
this.aggregateVersion = 0,
}); });
SCFloatingMessage.fromJson(dynamic json) { SCFloatingMessage.fromJson(dynamic json) {
@ -48,7 +46,6 @@ class SCFloatingMessage {
number = json['number']; number = json['number'];
priority = json['priority']; priority = json['priority'];
multiple = json['multiple']; multiple = json['multiple'];
aggregateVersion = json['aggregateVersion'] ?? 0;
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
@ -67,7 +64,6 @@ class SCFloatingMessage {
map['number'] = number; map['number'] = number;
map['priority'] = priority; map['priority'] = priority;
map['multiple'] = multiple; map['multiple'] = multiple;
map['aggregateVersion'] = aggregateVersion;
return map; return map;
} }
} }

View File

@ -20,7 +20,6 @@ class OverlayManager {
); );
bool _isPlaying = false; bool _isPlaying = false;
OverlayEntry? _currentOverlayEntry; OverlayEntry? _currentOverlayEntry;
SCFloatingMessage? _currentMessage;
bool _isProcessing = false; bool _isProcessing = false;
bool _isDisposed = false; bool _isDisposed = false;
@ -34,9 +33,6 @@ class OverlayManager {
void addMessage(SCFloatingMessage message) { void addMessage(SCFloatingMessage message) {
if (_isDisposed) return; if (_isDisposed) return;
if (SCGlobalConfig.isFloatingAnimationInGlobal) { if (SCGlobalConfig.isFloatingAnimationInGlobal) {
if (_tryAggregateMessage(message)) {
return;
}
_messageQueue.add(message); _messageQueue.add(message);
_safeScheduleNext(); _safeScheduleNext();
} else { } 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() { void _safeScheduleNext() {
if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return; if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return;
_isProcessing = true; _isProcessing = true;
@ -162,11 +90,9 @@ class OverlayManager {
void _playMessage(SCFloatingMessage message) { void _playMessage(SCFloatingMessage message) {
_isPlaying = true; _isPlaying = true;
_currentMessage = message;
final context = navigatorKey.currentState?.context; final context = navigatorKey.currentState?.context;
if (context == null || !context.mounted) { if (context == null || !context.mounted) {
_isPlaying = false; _isPlaying = false;
_currentMessage = null;
_safeScheduleNext(); _safeScheduleNext();
return; return;
} }
@ -195,12 +121,10 @@ class OverlayManager {
try { try {
_currentOverlayEntry?.remove(); _currentOverlayEntry?.remove();
_currentOverlayEntry = null; _currentOverlayEntry = null;
_currentMessage = null;
_isPlaying = false; _isPlaying = false;
_safeScheduleNext(); _safeScheduleNext();
} catch (e) { } catch (e) {
debugPrint('清理悬浮消息出错: $e'); debugPrint('清理悬浮消息出错: $e');
_currentMessage = null;
_isPlaying = false; _isPlaying = false;
_safeScheduleNext(); _safeScheduleNext();
} }
@ -209,9 +133,6 @@ class OverlayManager {
switch (message.type) { switch (message.type) {
case 0: case 0:
return FloatingLuckGiftScreenWidget( return FloatingLuckGiftScreenWidget(
key: ValueKey<String>(
'luck_${message.userId}_${message.toUserId}_${message.giftUrl}_${message.aggregateVersion}',
),
message: message, message: message,
onAnimationCompleted: onComplete, onAnimationCompleted: onComplete,
); );
@ -253,7 +174,6 @@ class OverlayManager {
_isDisposed = true; _isDisposed = true;
_currentOverlayEntry?.remove(); _currentOverlayEntry?.remove();
_currentOverlayEntry = null; _currentOverlayEntry = null;
_currentMessage = null;
_messageQueue.clear(); _messageQueue.clear();
_isPlaying = false; _isPlaying = false;
_isProcessing = false; _isProcessing = false;

View File

@ -324,9 +324,12 @@ class _DailySignInDialogState extends State<DailySignInDialog> {
'dayIndex=${checkInResult.dayIndex} ' 'dayIndex=${checkInResult.dayIndex} '
'checkedToday=${latestData.checkedToday}', 'checkedToday=${latestData.checkedToday}',
); );
SCTts.show( final toastMessage =
checkInResult.alreadySigned ? l10n.signedin : l10n.receiveSucc, checkInResult.alreadySigned
); ? l10n.signedin
: _rewardToastMessage(l10n, checkInResult.rewardItems);
debugPrint('[SignInReward][Dialog] reward toast=$toastMessage');
SCTts.show(toastMessage);
SmartDialog.dismiss(tag: DailySignInDialog.dialogTag); SmartDialog.dismiss(tag: DailySignInDialog.dialogTag);
} catch (error) { } catch (error) {
SCLoadingManager.hide(); SCLoadingManager.hide();
@ -365,6 +368,66 @@ class _DailySignInDialogState extends State<DailySignInDialog> {
); );
} }
String _rewardToastMessage(
SCAppLocalizations l10n,
List<SCSignInRewardItem> rewardItems,
) {
final rewardSummary = _rewardSummary(l10n, rewardItems);
if (rewardSummary.isEmpty) {
return l10n.receiveSucc;
}
return l10n.signInRewardReceived(rewardSummary);
}
String _rewardSummary(
SCAppLocalizations l10n,
List<SCSignInRewardItem> 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<String?> values) {
for (final value in values) {
final trimmed = value?.trim() ?? '';
if (trimmed.isNotEmpty) {
return trimmed;
}
}
return '';
}
String _debugItemsSummary(List<DailySignInDialogItem> items) { String _debugItemsSummary(List<DailySignInDialogItem> items) {
return items return items
.map( .map(

View File

@ -1,438 +1,447 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/main.dart'; import 'package:provider/provider.dart';
import 'package:provider/provider.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart';
import 'package:yumi/ui_kit/components/text/sc_text.dart'; import 'package:yumi/services/gift/gift_animation_manager.dart';
import 'package:yumi/services/gift/gift_animation_manager.dart'; import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
///
/// class LGiftAnimalPage extends StatefulWidget {
class LGiftAnimalPage extends StatefulWidget { const LGiftAnimalPage({super.key});
const LGiftAnimalPage({super.key});
@override
@override State<StatefulWidget> createState() {
State<StatefulWidget> createState() { return _GiftAnimalPageState();
return _GiftAnimalPageState(); }
} }
}
class _GiftAnimalPageState extends State<LGiftAnimalPage>
class _GiftAnimalPageState extends State<LGiftAnimalPage> with TickerProviderStateMixin {
with TickerProviderStateMixin { static const String _luckyGiftRewardFrameAssetPath =
static const String _luckyGiftRewardFrameAssetPath = "sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga";
"sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga"; late final GiftAnimationManager _giftAnimationManager;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: 332.w, height: 332.w,
child: Consumer<GiftAnimationManager>( child: Consumer<GiftAnimationManager>(
builder: (context, ref, child) { builder: (context, ref, child) {
return Stack( return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: List.generate( children: List.generate(
4, 4,
(index) => _buildGiftTickerItem(context, ref, index), (index) => _buildGiftTickerItem(context, ref, index),
), ),
); );
}, },
), ),
); );
} }
Widget _buildGiftTickerItem( Widget _buildGiftTickerItem(
BuildContext context, BuildContext context,
GiftAnimationManager ref, GiftAnimationManager ref,
int index, int index,
) { ) {
final gift = ref.giftMap[index]; final gift = ref.giftMap[index];
if (gift == null || ref.animationControllerList.length <= index) { if (gift == null || ref.animationControllerList.length <= index) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final bean = ref.animationControllerList[index]; final bean = ref.animationControllerList[index];
return AnimatedBuilder( return AnimatedBuilder(
animation: bean.controller, animation: bean.controller,
builder: (context, child) { builder: (context, child) {
final showLuckyRewardFrame = gift.showLuckyRewardFrame; final showLuckyRewardFrame = gift.showLuckyRewardFrame;
return Container( final tickerMargin =
margin: bean.verticalAnimation.value, showLuckyRewardFrame
width: ? bean.luckyGiftPinnedMargin
ScreenUtil().screenWidth * (showLuckyRewardFrame ? 0.96 : 0.78), : bean.verticalAnimation.value;
child: _buildGiftTickerCard(context, gift, bean.sizeAnimation.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,
) { Widget _buildGiftTickerCard(
final showLuckyRewardFrame = gift.showLuckyRewardFrame; BuildContext context,
return SizedBox( LGiftModel gift,
height: 52.w, double animatedSize,
child: Stack( ) {
alignment: AlignmentDirectional.centerStart, final showLuckyRewardFrame = gift.showLuckyRewardFrame;
clipBehavior: Clip.none, return SizedBox(
children: [ height: 52.w,
Container( child: Stack(
width: alignment: AlignmentDirectional.centerStart,
ScreenUtil().screenWidth * (showLuckyRewardFrame ? 0.69 : 0.67), clipBehavior: Clip.none,
height: 42.w, children: [
padding: EdgeInsetsDirectional.only( Container(
start: 3.w, width:
end: showLuckyRewardFrame ? 174.w : 76.w, ScreenUtil().screenWidth * (showLuckyRewardFrame ? 0.69 : 0.67),
top: 3.w, height: 42.w,
bottom: 3.w, padding: EdgeInsetsDirectional.only(
), start: 3.w,
decoration: BoxDecoration( end: showLuckyRewardFrame ? 174.w : 76.w,
image: DecorationImage( top: 3.w,
image: AssetImage(getBg("")), bottom: 3.w,
fit: BoxFit.fill, ),
), decoration: BoxDecoration(
), image: DecorationImage(
child: Row( image: AssetImage(getBg("")),
children: <Widget>[ fit: BoxFit.fill,
netImage( ),
url: gift.sendUserPic, ),
shape: BoxShape.circle, child: Row(
width: 26.w, children: <Widget>[
height: 26.w, netImage(
), url: gift.sendUserPic,
SizedBox(width: 4.w), shape: BoxShape.circle,
Expanded( width: 26.w,
child: Column( height: 26.w,
mainAxisSize: MainAxisSize.min, ),
mainAxisAlignment: MainAxisAlignment.center, SizedBox(width: 4.w),
crossAxisAlignment: CrossAxisAlignment.start, Expanded(
children: [ child: Column(
socialchatNickNameText( mainAxisSize: MainAxisSize.min,
maxWidth: 88.w, mainAxisAlignment: MainAxisAlignment.center,
gift.sendUserName, crossAxisAlignment: CrossAxisAlignment.start,
fontSize: 12.sp, children: [
fontWeight: FontWeight.w500, socialchatNickNameText(
type: "", maxWidth: 88.w,
needScroll: gift.sendUserName.characters.length > 8, gift.sendUserName,
), fontSize: 12.sp,
SizedBox(height: 1.w), fontWeight: FontWeight.w500,
Row( type: "",
children: [ needScroll: gift.sendUserName.characters.length > 8,
Text( ),
"${SCAppLocalizations.of(context)!.sendTo} ", SizedBox(height: 1.w),
style: TextStyle( Row(
fontSize: 10.sp, children: [
color: Colors.white, Text(
height: 1.0, "${SCAppLocalizations.of(context)!.sendTo} ",
), style: TextStyle(
), fontSize: 10.sp,
Flexible( color: Colors.white,
child: Text( height: 1.0,
gift.sendToUserName, ),
maxLines: 1, ),
overflow: TextOverflow.ellipsis, Flexible(
style: TextStyle( child: Text(
fontSize: 10.sp, gift.sendToUserName,
color: const Color(0xFFFFD400), maxLines: 1,
height: 1.0, overflow: TextOverflow.ellipsis,
), style: TextStyle(
), fontSize: 10.sp,
), color: const Color(0xFFFFD400),
], height: 1.0,
), ),
], ),
), ),
), ],
], ),
), ],
), ),
PositionedDirectional( ),
end: 0, ],
child: ConstrainedBox( ),
constraints: BoxConstraints( ),
maxWidth: PositionedDirectional(
ScreenUtil().screenWidth * end: 0,
(showLuckyRewardFrame ? 0.48 : 0.30), child: ConstrainedBox(
minHeight: 40.w, constraints: BoxConstraints(
), maxWidth:
child: Row( ScreenUtil().screenWidth *
mainAxisSize: MainAxisSize.min, (showLuckyRewardFrame ? 0.48 : 0.30),
crossAxisAlignment: CrossAxisAlignment.center, minHeight: 40.w,
children: <Widget>[ ),
netImage( child: Row(
url: gift.giftPic, mainAxisSize: MainAxisSize.min,
fit: BoxFit.cover, crossAxisAlignment: CrossAxisAlignment.center,
borderRadius: BorderRadius.circular(4.w), children: <Widget>[
width: 34.w, netImage(
height: 34.w, url: gift.giftPic,
), fit: BoxFit.cover,
if (gift.giftCount > 0) ...[ borderRadius: BorderRadius.circular(4.w),
SizedBox(width: 8.w), width: 34.w,
Flexible( height: 34.w,
child: FittedBox( ),
fit: BoxFit.scaleDown, if (gift.giftCount > 0) ...[
alignment: Alignment.centerLeft, SizedBox(width: 8.w),
child: _buildGiftCountLabel(gift, animatedSize), Flexible(
), child: FittedBox(
), fit: BoxFit.scaleDown,
], alignment: Alignment.centerLeft,
if (showLuckyRewardFrame) ...[ child: _buildGiftCountLabel(gift, animatedSize),
SizedBox(width: 6.w), ),
_buildLuckyGiftRewardFrame(gift), ),
], ],
], 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, Widget _buildGiftCountLabel(LGiftModel gift, double animatedSize) {
child: Row( final xFontSize = animatedSize;
mainAxisSize: MainAxisSize.min, final countFontSize = animatedSize;
crossAxisAlignment: CrossAxisAlignment.end, return Directionality(
children: [ textDirection: TextDirection.ltr,
Text( child: Row(
"x", mainAxisSize: MainAxisSize.min,
style: TextStyle( crossAxisAlignment: CrossAxisAlignment.end,
fontSize: xFontSize, children: [
fontStyle: FontStyle.italic, Text(
color: const Color(0xFFFFD400), "x",
fontWeight: FontWeight.bold, style: TextStyle(
height: 1, fontSize: xFontSize,
), fontStyle: FontStyle.italic,
), color: const Color(0xFFFFD400),
SizedBox(width: 2.w), fontWeight: FontWeight.bold,
Text( height: 1,
_giftCountText(gift.giftCount), ),
style: TextStyle( ),
fontSize: countFontSize, SizedBox(width: 2.w),
fontStyle: FontStyle.italic, Text(
color: const Color(0xFFFFD400), _giftCountText(gift.giftCount),
fontWeight: FontWeight.bold, style: TextStyle(
height: 1, 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( Widget _buildLuckyGiftRewardFrame(LGiftModel gift) {
alignment: Alignment.center, return SizedBox(
children: [ width: 108.w,
SCSvgaAssetWidget( height: 52.w,
assetPath: _luckyGiftRewardFrameAssetPath, child: Stack(
width: 108.w, alignment: Alignment.center,
height: 52.w, children: [
fit: BoxFit.cover, SCSvgaAssetWidget(
loop: true, assetPath: _luckyGiftRewardFrameAssetPath,
allowDrawingOverflow: true, width: 108.w,
fallback: _buildLuckyGiftRewardFrameFallback(), height: 52.w,
), fit: BoxFit.cover,
Padding( loop: true,
padding: EdgeInsetsDirectional.only( allowDrawingOverflow: true,
start: 12.w, fallback: _buildLuckyGiftRewardFrameFallback(),
end: 12.w, ),
top: 1.w, Padding(
), padding: EdgeInsetsDirectional.only(
child: FittedBox( start: 12.w,
fit: BoxFit.scaleDown, end: 12.w,
child: Text( top: 1.w,
"+${gift.rewardAmountText}", ),
maxLines: 1, child: FittedBox(
style: TextStyle( fit: BoxFit.scaleDown,
fontSize: 16.sp, child: Text(
color: const Color(0xFFFFF2AE), "+${_giftRewardAmountText(gift)}",
fontWeight: FontWeight.w800, maxLines: 1,
fontStyle: FontStyle.italic, style: TextStyle(
height: 1, fontSize: 16.sp,
shadows: const [ color: const Color(0xFFFFF2AE),
Shadow( fontWeight: FontWeight.w800,
color: Color(0xCC6A3700), fontStyle: FontStyle.italic,
blurRadius: 10, height: 1,
offset: Offset(0, 2), shadows: const [
), Shadow(
], color: Color(0xCC6A3700),
), blurRadius: 10,
), offset: Offset(0, 2),
), ),
), ],
], ),
), ),
); ),
} ),
],
Widget _buildLuckyGiftRewardFrameFallback() { ),
return Container( );
width: 108.w, }
height: 52.w,
decoration: BoxDecoration( Widget _buildLuckyGiftRewardFrameFallback() {
borderRadius: BorderRadius.circular(8.w), return Container(
gradient: const LinearGradient( width: 108.w,
begin: Alignment.centerLeft, height: 52.w,
end: Alignment.centerRight, decoration: BoxDecoration(
colors: <Color>[Color(0xCC7C4300), Color(0xE6D99A36)], borderRadius: BorderRadius.circular(8.w),
), gradient: const LinearGradient(
border: Border.all(color: const Color(0xFFF7D87B), width: 1), begin: Alignment.centerLeft,
boxShadow: const [ end: Alignment.centerRight,
BoxShadow( colors: <Color>[Color(0xCC7C4300), Color(0xE6D99A36)],
color: Color(0x663F1E00), ),
blurRadius: 8, border: Border.all(color: const Color(0xFFF7D87B), width: 1),
offset: Offset(0, 2), 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 String _giftCountText(num count) {
void dispose() { return count % 1 == 0 ? count.toInt().toString() : count.toString();
Provider.of<GiftAnimationManager>( }
navigatorKey.currentState!.context,
listen: false, String _giftRewardAmountText(LGiftModel gift) {
).cleanupAnimationResources(); final rewardAmount = gift.rewardAmount;
super.dispose(); if (rewardAmount > 0) {
} if (rewardAmount > 9999) {
return "${(rewardAmount / 1000).toStringAsFixed(0)}k";
@override }
void initState() { if (rewardAmount % 1 == 0) {
super.initState(); return rewardAmount.toInt().toString();
initAnimal(); }
} return rewardAmount.toString();
}
void initAnimal() { return gift.rewardAmountText;
List<LGiftScrollingScreenAnimsBean> beans = []; }
double top = 60;
for (int i = 0; i < 4; i++) { @override
var bean = LGiftScrollingScreenAnimsBean(); void dispose() {
var controller = AnimationController( _giftAnimationManager.cleanupAnimationResources();
value: 0, super.dispose();
duration: const Duration(milliseconds: 5000), }
vsync: this,
); @override
bean.controller = controller; void initState() {
// bean.transverseAnimation = Tween<Offset>( super.initState();
// begin: Offset(ScreenUtil().screenWidth, 0), _giftAnimationManager = Provider.of<GiftAnimationManager>(
// end: Offset(0, 0), context,
// ).animate( listen: false,
// CurvedAnimation( );
// parent: controller, initAnimal();
// curve: Interval(0.0, 0.45, curve: Curves.ease), }
// ),
// ); void initAnimal() {
bean.verticalAnimation = EdgeInsetsTween( List<LGiftScrollingScreenAnimsBean> beans = [];
begin: EdgeInsets.only(top: top), double top = 60;
end: EdgeInsets.only(top: 0), for (int i = 0; i < 4; i++) {
).animate( var bean = LGiftScrollingScreenAnimsBean();
CurvedAnimation( var controller = AnimationController(
parent: controller, value: 0,
curve: Interval(0.2, 1, curve: Curves.easeOut), duration: const Duration(milliseconds: 5000),
), vsync: this,
); );
bean.sizeAnimation = Tween<double>(begin: 0, end: 22).animate( bean.controller = controller;
CurvedAnimation( bean.luckyGiftPinnedMargin = EdgeInsets.only(top: top);
parent: controller, // bean.transverseAnimation = Tween<Offset>(
curve: Interval(0.45, 0.55, curve: Curves.ease), // begin: Offset(ScreenUtil().screenWidth, 0),
), // end: Offset(0, 0),
); // ).animate(
beans.add(bean); // CurvedAnimation(
top = top + 70; // parent: controller,
} // curve: Interval(0.0, 0.45, curve: Curves.ease),
beans[0].controller.addStatusListener((state) { // ),
if (state == AnimationStatus.completed) { // );
// bean.verticalAnimation = EdgeInsetsTween(
Provider.of<GiftAnimationManager>( begin: EdgeInsets.only(top: top),
context, end: EdgeInsets.only(top: 0),
listen: false, ).animate(
).markAnimationAsFinished(0); CurvedAnimation(
} parent: controller,
}); curve: Interval(0.2, 1, curve: Curves.easeOut),
beans[1].controller.addStatusListener((state) { ),
if (state == AnimationStatus.completed) { );
// bean.sizeAnimation = Tween<double>(begin: 0, end: 22).animate(
Provider.of<GiftAnimationManager>( CurvedAnimation(
context, parent: controller,
listen: false, curve: Interval(0.45, 0.55, curve: Curves.ease),
).markAnimationAsFinished(1); ),
} );
}); beans.add(bean);
beans[2].controller.addStatusListener((state) { top = top + 70;
if (state == AnimationStatus.completed) { }
// beans[0].controller.addStatusListener((state) {
Provider.of<GiftAnimationManager>( if (state == AnimationStatus.completed) {
context, //
listen: false, _giftAnimationManager.markAnimationAsFinished(0);
).markAnimationAsFinished(2); }
} });
}); beans[1].controller.addStatusListener((state) {
beans[3].controller.addStatusListener((state) { if (state == AnimationStatus.completed) {
if (state == AnimationStatus.completed) { //
// _giftAnimationManager.markAnimationAsFinished(1);
Provider.of<GiftAnimationManager>( }
context, });
listen: false, beans[2].controller.addStatusListener((state) {
).markAnimationAsFinished(3); if (state == AnimationStatus.completed) {
} //
}); _giftAnimationManager.markAnimationAsFinished(2);
Provider.of<GiftAnimationManager>( }
context, });
listen: false, beans[3].controller.addStatusListener((state) {
).attachAnimationControllers(beans); if (state == AnimationStatus.completed) {
} //
_giftAnimationManager.markAnimationAsFinished(3);
String getBg(String type) { }
return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png"; });
} _giftAnimationManager.attachAnimationControllers(beans);
} }
class LGiftScrollingScreenAnimsBean { String getBg(String type) {
// late Animation<Offset> transverseAnimation; return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png";
late Animation<EdgeInsets> verticalAnimation; }
late AnimationController controller; }
late Animation<double> sizeAnimation;
} class LGiftScrollingScreenAnimsBean {
// late Animation<Offset> transverseAnimation;
class LGiftModel { late Animation<EdgeInsets> verticalAnimation;
// late EdgeInsets luckyGiftPinnedMargin;
String sendUserName = ""; late AnimationController controller;
late Animation<double> sizeAnimation;
// }
String sendToUserName = "";
class LGiftModel {
// //
String sendUserPic = ""; String sendUserName = "";
// //
String giftPic = ""; String sendToUserName = "";
// //
String giftName = ""; String sendUserPic = "";
// //
num giftCount = 0; String giftPic = "";
// //
String rewardAmountText = ""; String giftName = "";
// //
bool showLuckyRewardFrame = false; num giftCount = 0;
//id //
String labelId = ""; String rewardAmountText = "";
}
//
num rewardAmount = 0;
//
bool showLuckyRewardFrame = false;
//id
String labelId = "";
}

View File

@ -6,8 +6,6 @@ import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
class LuckGiftNomorAnimWidget extends StatefulWidget { class LuckGiftNomorAnimWidget extends StatefulWidget {
static const num _rewardBurstMinAwardAmount = 5000;
static const num _rewardBurstMinMultiple = 10;
static const String _rewardBurstAssetPath = static const String _rewardBurstAssetPath =
"sc_images/room/anim/luck_gift/luck_gift_reward_burst.svga"; "sc_images/room/anim/luck_gift/luck_gift_reward_burst.svga";
@ -19,11 +17,11 @@ class LuckGiftNomorAnimWidget extends StatefulWidget {
} }
class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> { class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
String? _currentBurstEventId;
bool _isRewardAmountVisible = false;
bool _shouldPlayRewardBurst(Data rewardData) { bool _shouldPlayRewardBurst(Data rewardData) {
final awardAmount = rewardData.awardAmount ?? 0; return RealTimeMessagingManager.shouldPlayLuckyGiftBurst(rewardData);
final multiple = rewardData.multiple ?? 0;
return awardAmount > LuckGiftNomorAnimWidget._rewardBurstMinAwardAmount ||
multiple > LuckGiftNomorAnimWidget._rewardBurstMinMultiple;
} }
String _formatAwardAmount(num awardAmount) { String _formatAwardAmount(num awardAmount) {
@ -43,6 +41,8 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
builder: (context, provider, child) { builder: (context, provider, child) {
final rewardData = provider.currentPlayingLuckGift?.data; final rewardData = provider.currentPlayingLuckGift?.data;
if (rewardData == null || !_shouldPlayRewardBurst(rewardData)) { if (rewardData == null || !_shouldPlayRewardBurst(rewardData)) {
_currentBurstEventId = null;
_isRewardAmountVisible = false;
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final rewardAnimationKey = ValueKey<String>( final rewardAnimationKey = ValueKey<String>(
@ -52,6 +52,10 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
'|${rewardData.multiple ?? 0}' '|${rewardData.multiple ?? 0}'
'|${rewardData.normalizedMultipleType}', '|${rewardData.normalizedMultipleType}',
); );
if (_currentBurstEventId != rewardAnimationKey.value) {
_currentBurstEventId = rewardAnimationKey.value;
_isRewardAmountVisible = false;
}
return SizedBox( return SizedBox(
height: 380.w, height: 380.w,
child: Stack( child: Stack(
@ -66,37 +70,67 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
height: 380.w, height: 380.w,
fit: BoxFit.fitWidth, fit: BoxFit.fitWidth,
allowDrawingOverflow: true, allowDrawingOverflow: true,
clearsAfterStop: true,
onPlaybackStarted: () {
if (!mounted) {
return;
}
if (!_isRewardAmountVisible) {
setState(() {
_isRewardAmountVisible = true;
});
}
},
onPlaybackCompleted: () {
if (!mounted) {
return;
}
if (_isRewardAmountVisible) {
setState(() {
_isRewardAmountVisible = false;
});
}
},
), ),
), ),
Positioned( if (_isRewardAmountVisible)
top: 154.w, Positioned.fill(
child: ConstrainedBox( child: Align(
constraints: BoxConstraints( alignment: const Alignment(0, 0.12),
maxWidth: ScreenUtil().screenWidth * 0.56, child: Transform.translate(
), offset: Offset(0, 0),
child: FittedBox( child: ConstrainedBox(
fit: BoxFit.scaleDown, constraints: BoxConstraints(
child: Text( maxWidth: ScreenUtil().screenWidth * 0.56,
"+${_formatAwardAmount(rewardData.awardAmount ?? 0)}", ),
maxLines: 1, child: FittedBox(
style: TextStyle( fit: BoxFit.scaleDown,
fontSize: 28.sp, child: Padding(
color: const Color(0xFFFFF3B6), padding: EdgeInsets.only(left: 6.w, right: 2.w),
fontWeight: FontWeight.w900, child: Text(
fontStyle: FontStyle.italic, _formatAwardAmount(rewardData.awardAmount ?? 0),
height: 1, maxLines: 1,
shadows: const [ style: TextStyle(
Shadow( fontSize: 28.sp,
color: Color(0xCC7A3E00), color: const Color(0xFFFFF3B6),
blurRadius: 12, fontWeight: FontWeight.w900,
offset: Offset(0, 3), fontStyle: FontStyle.italic,
height: 1,
shadows: const [
Shadow(
color: Color(0xCC7A3E00),
blurRadius: 12,
offset: Offset(0, 3),
),
],
),
),
), ),
], ),
), ),
), ),
), ),
), ),
),
], ],
), ),
); );

View File

@ -50,17 +50,20 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SizedBox( return SizedBox(
height: _floatingButtonHostHeight.w, height: _floatingButtonHostHeight.w,
child: Consumer<RtcProvider>( child: Selector<RtcProvider, _RoomBottomSnapshot>(
builder: (context, rtcProvider, child) { selector:
final showMic = _shouldShowMic(rtcProvider); (context, provider) => _RoomBottomSnapshot(
showMic: _shouldShowMic(provider),
isMic: provider.isMic,
),
builder: (context, bottomSnapshot, child) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final inputWidth = constraints.maxWidth / 3; final inputWidth = constraints.maxWidth / 3;
final giftCenterX = _resolveGiftCenterX( final giftCenterX = _resolveGiftCenterX(
maxWidth: constraints.maxWidth, maxWidth: constraints.maxWidth,
inputWidth: inputWidth, inputWidth: inputWidth,
showMic: showMic, showMic: bottomSnapshot.showMic,
); );
return Stack( return Stack(
@ -78,9 +81,9 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
child: SizedBox( child: SizedBox(
height: _bottomBarHeight.w, height: _bottomBarHeight.w,
child: _buildBottomBar( child: _buildBottomBar(
rtcProvider: rtcProvider,
inputWidth: inputWidth, inputWidth: inputWidth,
showMic: showMic, showMic: bottomSnapshot.showMic,
isMic: bottomSnapshot.isMic,
), ),
), ),
), ),
@ -105,9 +108,9 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
} }
Widget _buildBottomBar({ Widget _buildBottomBar({
required RtcProvider rtcProvider,
required double inputWidth, required double inputWidth,
required bool showMic, required bool showMic,
required bool isMic,
}) { }) {
final giftAction = _buildGiftAction(); final giftAction = _buildGiftAction();
final messageAction = _buildMessageAction(); final messageAction = _buildMessageAction();
@ -126,7 +129,7 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
children: [ children: [
_buildChatEntry(inputWidth), _buildChatEntry(inputWidth),
giftAction, giftAction,
_buildMicAction(rtcProvider), _buildMicAction(isMic: isMic),
menuAction, menuAction,
messageAction, messageAction,
], ],
@ -265,10 +268,11 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
); );
} }
Widget _buildMicAction(RtcProvider provider) { Widget _buildMicAction({required bool isMic}) {
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
final provider = context.read<RtcProvider>();
setState(() { setState(() {
provider.isMic = !provider.isMic; provider.isMic = !provider.isMic;
@ -298,7 +302,7 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
}, },
child: RoomBottomCircleAction( child: RoomBottomCircleAction(
child: Image.asset( 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, width: 30.w,
height: 30.w, height: 30.w,
fit: BoxFit.contain, fit: BoxFit.contain,
@ -320,3 +324,23 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
return show; 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);
}

View File

@ -23,8 +23,22 @@ class RoomHeadWidget extends StatefulWidget {
class _RoomHeadWidgetState extends State<RoomHeadWidget> { class _RoomHeadWidgetState extends State<RoomHeadWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<RtcProvider>( return Selector<RtcProvider, _RoomHeadSnapshot>(
builder: (context, provider, child) { 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( return Row(
children: [ children: [
Row( Row(
@ -32,8 +46,8 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
GestureDetector( GestureDetector(
onTap: () { onTap: () {
showBottomInBottomDialog( showBottomInBottomDialog(
context!, context,
RoomDetailPage(provider.isFz()), RoomDetailPage(context.read<RtcProvider>().isFz()),
); );
}, },
child: Container( child: Container(
@ -48,16 +62,8 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
children: [ children: [
netImage( netImage(
url: resolveRoomCoverUrl( url: resolveRoomCoverUrl(
provider roomSnapshot.roomProfileId,
.currenRoom roomSnapshot.roomCover,
?.roomProfile
?.roomProfile
?.id,
provider
.currenRoom
?.roomProfile
?.roomProfile
?.roomCover,
), ),
defaultImg: kRoomCoverDefaultImg, defaultImg: kRoomCoverDefaultImg,
width: 28.w, width: 28.w,
@ -99,22 +105,9 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
maxHeight: 17.w, maxHeight: 17.w,
), ),
child: child:
(provider roomSnapshot.roomName.length > 10
.currenRoom
?.roomProfile
?.roomProfile
?.roomName
?.length ??
0) >
10
? Marquee( ? Marquee(
text: text: roomSnapshot.roomName,
provider
.currenRoom
?.roomProfile
?.roomProfile
?.roomName ??
"",
style: TextStyle( style: TextStyle(
fontSize: 14.sp, fontSize: 14.sp,
color: Color(0xffffffff), color: Color(0xffffffff),
@ -137,12 +130,7 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
decelerationCurve: Curves.easeOut, decelerationCurve: Curves.easeOut,
) )
: Text( : Text(
provider roomSnapshot.roomName,
.currenRoom
?.roomProfile
?.roomProfile
?.roomName ??
'',
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
@ -155,7 +143,7 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
), ),
text( text(
"ID:${provider.currenRoom?.roomProfile?.userProfile?.getID()}", "ID:${roomSnapshot.roomDisplayId}",
fontSize: 13.sp, fontSize: 13.sp,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
textColor: Colors.white70, textColor: Colors.white70,
@ -163,11 +151,7 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
], ],
), ),
SizedBox(width: 6.w), SizedBox(width: 6.w),
provider.currenRoom?.roomProfile?.roomProfile?.userId != !roomSnapshot.isRoomOwner
AccountStorage()
.getCurrentUser()
?.userProfile
?.id
? Selector<RtcProvider, bool>( ? Selector<RtcProvider, bool>(
selector: selector:
(c, p) => (c, p) =>
@ -182,7 +166,9 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
height: 22.w, height: 22.w,
), ),
onTap: () { onTap: () {
provider.followCurrentVoiceRoom(); context
.read<RtcProvider>()
.followCurrentVoiceRoom();
}, },
) )
: Container(); : Container();
@ -203,7 +189,7 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
height: 32.w, height: 32.w,
), ),
onTap: () { onTap: () {
if (provider.isFz()) { if (context.read<RtcProvider>().isFz()) {
SCNavigatorUtils.push( SCNavigatorUtils.push(
context, context,
"${VoiceRoomRoute.roomEdit}?need=false", "${VoiceRoomRoute.roomEdit}?need=false",
@ -234,8 +220,8 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
showCenterDialog( showCenterDialog(
context, context,
ExitMinRoomPage( ExitMinRoomPage(
provider.isFz(), context.read<RtcProvider>().isFz(),
provider.currenRoom?.roomProfile?.roomProfile?.id ?? "", roomSnapshot.roomProfileId ?? "",
), ),
barrierColor: Colors.black54, barrierColor: Colors.black54,
); );
@ -249,3 +235,45 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
); );
} }
} }
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,
);
}

View File

@ -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/sc_compontent.dart';
import 'package:yumi/ui_kit/components/text/sc_text.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/shared/tools/sc_lk_dialog_util.dart';
import 'package:yumi/services/room/rc_room_manager.dart'; import 'package:yumi/services/room/rc_room_manager.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
@ -29,17 +30,32 @@ class _RoomOnlineUserWidgetState extends State<RoomOnlineUserWidget> {
double get _onlineUsersShellWidth => double get _onlineUsersShellWidth =>
_onlineUsersAvatarsWidth + _onlineUsersCounterWidth; _onlineUsersAvatarsWidth + _onlineUsersCounterWidth;
void _openRoomOnlinePage(RtcProvider ref) { void _openRoomOnlinePage() {
showBottomInBottomDialog( showBottomInBottomDialog(
context, context,
RoomOnlinePage(roomId: ref.currenRoom?.roomProfile?.roomProfile?.id), RoomOnlinePage(
roomId:
context
.read<RtcProvider>()
.currenRoom
?.roomProfile
?.roomProfile
?.id,
),
); );
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<RtcProvider>( return Selector<RtcProvider, _RoomOnlineUsersSnapshot>(
builder: (context, ref, child) { selector:
(context, provider) => _RoomOnlineUsersSnapshot(
onlineUsers: List<SocialChatUserProfile>.unmodifiable(
provider.onlineUsers,
),
),
builder: (context, onlineSnapshot, child) {
final onlineUsers = onlineSnapshot.onlineUsers;
return Row( return Row(
children: [ children: [
_buildExperience(), _buildExperience(),
@ -48,8 +64,8 @@ class _RoomOnlineUserWidgetState extends State<RoomOnlineUserWidget> {
child: Align( child: Align(
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: child:
ref.onlineUsers.isNotEmpty onlineUsers.isNotEmpty
? _buildOnlineUsers(ref) ? _buildOnlineUsers(onlineUsers)
: _buildOnlineUsersPlaceholder(), : _buildOnlineUsersPlaceholder(),
), ),
), ),
@ -59,12 +75,12 @@ class _RoomOnlineUserWidgetState extends State<RoomOnlineUserWidget> {
); );
} }
Widget _buildOnlineUsers(RtcProvider ref) { Widget _buildOnlineUsers(List<SocialChatUserProfile> onlineUsers) {
return Padding( return Padding(
padding: EdgeInsets.only(right: 5.w), padding: EdgeInsets.only(right: 5.w),
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () => _openRoomOnlinePage(ref), onTap: _openRoomOnlinePage,
child: SizedBox( child: SizedBox(
width: _onlineUsersShellWidth, width: _onlineUsersShellWidth,
height: _onlineUsersShellHeight, height: _onlineUsersShellHeight,
@ -80,13 +96,13 @@ class _RoomOnlineUserWidgetState extends State<RoomOnlineUserWidget> {
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: List.generate(ref.onlineUsers.length, (index) { children: List.generate(onlineUsers.length, (index) {
return Transform.translate( return Transform.translate(
offset: Offset(-3.w * index, 0), offset: Offset(-3.w * index, 0),
child: Padding( child: Padding(
padding: EdgeInsets.only(right: 0.w), padding: EdgeInsets.only(right: 0.w),
child: netImage( child: netImage(
url: ref.onlineUsers[index].userAvatar ?? "", url: onlineUsers[index].userAvatar ?? "",
width: 23.w, width: 23.w,
height: 23.w, height: 23.w,
defaultImg: defaultImg:
@ -117,11 +133,7 @@ class _RoomOnlineUserWidgetState extends State<RoomOnlineUserWidget> {
width: 12.w, width: 12.w,
height: 12.sp, height: 12.sp,
), ),
text( text("${onlineUsers.length}", fontSize: 9, lineHeight: 1),
"${ref.onlineUsers.length}",
fontSize: 9,
lineHeight: 1,
),
], ],
), ),
), ),
@ -204,3 +216,37 @@ class _RoomOnlineUserWidgetState extends State<RoomOnlineUserWidget> {
); );
} }
} }
class _RoomOnlineUsersSnapshot {
const _RoomOnlineUsersSnapshot({required this.onlineUsers});
final List<SocialChatUserProfile> 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)),
);
}

View File

@ -13,12 +13,12 @@ class RoomSeatWidget extends StatefulWidget {
class _RoomSeatWidgetState extends State<RoomSeatWidget> { class _RoomSeatWidgetState extends State<RoomSeatWidget> {
int _lastSeatCount = 0; int _lastSeatCount = 0;
int _resolvedSeatCount(RtcProvider ref) { int _resolvedSeatCount(_RoomSeatLayoutSnapshot snapshot) {
final int seatCount = ref.roomWheatMap.length; final int seatCount = snapshot.seatCount;
if (!ref.isExitingCurrentVoiceRoomSession && seatCount > 0) { if (!snapshot.isExitingCurrentVoiceRoomSession && seatCount > 0) {
_lastSeatCount = seatCount; _lastSeatCount = seatCount;
} }
if (ref.isExitingCurrentVoiceRoomSession && _lastSeatCount > 0) { if (snapshot.isExitingCurrentVoiceRoomSession && _lastSeatCount > 0) {
return _lastSeatCount; return _lastSeatCount;
} }
return seatCount; return seatCount;
@ -26,9 +26,15 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Consumer<RtcProvider>( return Selector<RtcProvider, _RoomSeatLayoutSnapshot>(
builder: (context, ref, child) { selector:
final int seatCount = _resolvedSeatCount(ref); (context, provider) => _RoomSeatLayoutSnapshot(
seatCount: provider.roomWheatMap.length,
isExitingCurrentVoiceRoomSession:
provider.isExitingCurrentVoiceRoomSession,
),
builder: (context, snapshot, child) {
final int seatCount = _resolvedSeatCount(snapshot);
return seatCount == 5 return seatCount == 5
? _buildSeat5() ? _buildSeat5()
: (seatCount == 10 : (seatCount == 10
@ -177,3 +183,27 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
); );
} }
} }
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);
}

View File

@ -14,6 +14,9 @@ class SCSvgaAssetWidget extends StatefulWidget {
this.filterQuality = FilterQuality.low, this.filterQuality = FilterQuality.low,
this.allowDrawingOverflow = false, this.allowDrawingOverflow = false,
this.fallback, this.fallback,
this.clearsAfterStop = false,
this.onPlaybackStarted,
this.onPlaybackCompleted,
}); });
final String assetPath; final String assetPath;
@ -25,6 +28,9 @@ class SCSvgaAssetWidget extends StatefulWidget {
final FilterQuality filterQuality; final FilterQuality filterQuality;
final bool allowDrawingOverflow; final bool allowDrawingOverflow;
final Widget? fallback; final Widget? fallback;
final bool clearsAfterStop;
final VoidCallback? onPlaybackStarted;
final VoidCallback? onPlaybackCompleted;
@override @override
State<SCSvgaAssetWidget> createState() => _SCSvgaAssetWidgetState(); State<SCSvgaAssetWidget> createState() => _SCSvgaAssetWidgetState();
@ -44,9 +50,20 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
void initState() { void initState() {
super.initState(); super.initState();
_controller = SVGAAnimationController(vsync: this); _controller = SVGAAnimationController(vsync: this);
_controller.addStatusListener(_handleAnimationStatusChanged);
_loadAsset(); _loadAsset();
} }
void _handleAnimationStatusChanged(AnimationStatus status) {
if (status == AnimationStatus.forward) {
widget.onPlaybackStarted?.call();
return;
}
if (status == AnimationStatus.completed) {
widget.onPlaybackCompleted?.call();
}
}
@override @override
void didUpdateWidget(covariant SCSvgaAssetWidget oldWidget) { void didUpdateWidget(covariant SCSvgaAssetWidget oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
@ -129,6 +146,7 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
if (!widget.active) { if (!widget.active) {
_controller.stop(); _controller.stop();
_controller.reset(); _controller.reset();
widget.onPlaybackCompleted?.call();
return; return;
} }
@ -148,6 +166,7 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
@override @override
void dispose() { void dispose() {
_controller.removeStatusListener(_handleAnimationStatusChanged);
_controller.dispose(); _controller.dispose();
super.dispose(); super.dispose();
} }
@ -170,7 +189,7 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
child: SVGAImage( child: SVGAImage(
_controller, _controller,
fit: widget.fit, fit: widget.fit,
clearsAfterStop: false, clearsAfterStop: widget.clearsAfterStop,
filterQuality: widget.filterQuality, filterQuality: widget.filterQuality,
allowDrawingOverflow: widget.allowDrawingOverflow, allowDrawingOverflow: widget.allowDrawingOverflow,
), ),

View File

@ -21,17 +21,26 @@
## 已完成模块 ## 已完成模块
- 已按 2026-04-20 最新首页视觉需求,为 Party 房间列表前 3 个房卡接入新的本地排名边框 SVGA桌面“房间排序前三的框”中的 3 份素材已导入工程并挂到首页房卡最上层,仅作用于当前列表前 3 项,其余房卡保持原样;后续又继续为前三房卡底部信息区补了横向与底部安全区,避免外扩边框直接压住国旗、房名和在线人数。同时 `Me` 板块里的 `Recent / Followed` tab 已继续对齐上方首页 tab 的斜体字样式,并移除了点击时的波浪反馈。 - 已按 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-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` 这几组字段,便于和当前后端参数定义保持一致。 - 已按 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` 字段决定是否展示,避免再用前端本地倍率硬编码去猜。 - 已继续补齐 `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 混在一起。 - 已按最新确认撤掉播报条里的 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避免资源异常时再次只剩金额裸字。 - 已继续修正幸运礼物中奖播报条右侧奖励框不显示的问题:根因是 `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` 文字,当前已改为直接展示“中奖金额 + 金币图标 + 对应礼物图”,和房间礼物播报条的视觉表达保持一致。 - 已按最新文案口径继续收紧幸运礼物中奖消息:聊天室里高倍率幸运礼物提示不再显示冗长的 `Coins / a lucky(magic) gift` 文字,当前已改为直接展示“中奖金额 + 金币图标 + 对应礼物图”,和房间礼物播报条的视觉表达保持一致。
- 已继续微调幸运礼物中奖视觉:房间礼物播报条右侧的 `luck_gift_reward_frame.svga` 已进一步放大,并给右侧区域和主条正文预留了更宽的排版空间,避免资源已经正常显示但因为画布比例和透明边距看起来过小;同时聊天室高亮中奖消息与房间顶部幸运礼物横幅现已统一改成“中奖金额 + 金币图标 + from + 礼物图”的表达,去掉原先 `Coins / a lucky(magic) gift` 这类冗长英文文案;另外全屏 `luck_gift_reward_burst.svga` 也已补上中部金额文案,避免播发时只见特效不见本次实际中奖金币数。 - 已继续微调幸运礼物中奖视觉:房间礼物播报条右侧的 `luck_gift_reward_frame.svga` 已进一步放大,并给右侧区域和主条正文预留了更宽的排版空间,避免资源已经正常显示但因为画布比例和透明边距看起来过小;同时聊天室高亮中奖消息与房间顶部幸运礼物横幅现已统一改成“中奖金额 + 金币图标 + from + 礼物图”的表达,去掉原先 `Coins / a lucky(magic) gift` 这类冗长英文文案;另外全屏 `luck_gift_reward_burst.svga` 也已补上中部金额文案,避免播发时只见特效不见本次实际中奖金币数。
- 已按 2026-04-20 最新联调继续收口幸运礼物播报噪音:顶部幸运礼物飘屏现在会对同一房间、同一送礼人、同一接收人、同一礼物的连续中奖做队列内聚合,不再每来一条都重新追加一个新飘屏;当前展示中的那一条也会直接叠加金币额并刷新,避免“刷礼过快时飘屏一条接一条排队刷过去”。同时飘屏中奖文案已强制收为单行显示,防止金额、`from` 和礼物图被挤成上下两行。 - 已按 2026-04-20 最新联调继续收口幸运礼物播报噪音:顶部幸运礼物飘屏现在会对同一房间、同一送礼人、同一接收人、同一礼物的连续中奖做队列内聚合,不再每来一条都重新追加一个新飘屏;当前展示中的那一条也会直接叠加金币额并刷新,避免“刷礼过快时飘屏一条接一条排队刷过去”。同时飘屏中奖文案已强制收为单行显示,防止金额、`from` 和礼物图被挤成上下两行。
- 已继续修复房间礼物播报条偶发出现两条一模一样内容的问题:此前 `GiftAnimationManager` 只会和“正在播放”的同 `labelId` 项做合并,等待队列里的同类播报不会提前去重,因此高频情况下仍可能排出两条完全一样的条目;当前已改为在入队时同时检查“正在播”和“待播”两侧,命中相同播报键时直接原地合并,不再把重复项继续塞进队列。 - 已继续修复房间礼物播报条偶发出现两条一模一样内容的问题:此前 `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` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。 - 已优化语言房麦位/头像的二次确认交互:普通用户点击可上麦的空麦位时,当前会直接执行上麦,不再先弹出只有 `Take the mic / Cancel` 的确认层;普通用户点击房间头像或已占麦位上的用户头像时,也会直接打开个人卡片,不再额外弹出仅含 `Open user profile card / Cancel` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。
- 已继续收窄语言房个人卡片前的“确认意义”弹层:当前用户在麦位上点击自己的头像时,也会直接打开自己的个人卡片,不再先弹出仅包含 `Leave the mic / Open user profile card / Cancel` 的底部菜单;同时个人卡片内的“离开麦位”入口已替换为新的 `leave` 视觉素材,和最新房间交互稿保持一致。 - 已继续收窄语言房个人卡片前的“确认意义”弹层:当前用户在麦位上点击自己的头像时,也会直接打开自己的个人卡片,不再先弹出仅包含 `Leave the mic / Open user profile card / Cancel` 的底部菜单;同时个人卡片内的“离开麦位”入口已替换为新的 `leave` 视觉素材,和最新房间交互稿保持一致。
- 已继续微调语言房个人卡片与送礼 UI个人卡片底部动作文案现已支持两行居中展示避免 `Leave the mic` 这类英文按钮被硬截断;房间底部礼物入口也已切换为新的本地 `SVGA` 资源 `room_bottom_gift_button.svga`,保持房间底栏视觉和最新动效稿一致。 - 已继续微调语言房个人卡片与送礼 UI个人卡片底部动作文案现已支持两行居中展示避免 `Leave the mic` 这类英文按钮被硬截断;房间底部礼物入口也已切换为新的本地 `SVGA` 资源 `room_bottom_gift_button.svga`,保持房间底栏视觉和最新动效稿一致。