修复一些问题
This commit is contained in:
parent
5b0b5b862e
commit
743f761dd1
@ -304,6 +304,7 @@
|
||||
"warning": "تحذير",
|
||||
"ownerSendTheRedEnvelope": "أرسل مالك الغرفة عملات المكافأة.",
|
||||
"rewardCoins": "عملات المكافأة:{1} عملة",
|
||||
"signInRewardReceived": "تم تسجيل الدخول بنجاح. المكافأة: {1}",
|
||||
"lastWeekProgress": "تقدم الأسبوع الماضي",
|
||||
"currentProgress": "التقدم الحالي",
|
||||
"coins2": "{1} عملات",
|
||||
|
||||
@ -159,6 +159,7 @@
|
||||
"roomReward": "রুম পুরস্কার",
|
||||
"ownerSendTheRedEnvelope": "মালিক পুরস্কার কয়েন পাঠিয়েছেন।",
|
||||
"rewardCoins": "পুরস্কার কয়েন:{1} কয়েন",
|
||||
"signInRewardReceived": "সাইন ইন সফল হয়েছে। পুরস্কার: {1}",
|
||||
"lastWeekProgress": "গত সপ্তাহের অগ্রগতি",
|
||||
"redEnvelopeTips2": "*লাল খাম সময়সীমার মধ্যে দাবি না করলে, বাকি কয়েন প্রেরক ব্যবহারকারীকে ফেরত দেওয়া হবে।",
|
||||
"goToRecharge": "রিচার্জ করতে যান",
|
||||
|
||||
@ -131,6 +131,7 @@
|
||||
"expirationTime": "Expiration time",
|
||||
"ownerSendTheRedEnvelope": "The owner sent reward coins.",
|
||||
"rewardCoins": "Reward coins:{1} coins",
|
||||
"signInRewardReceived": "Signed in successfully. Reward: {1}",
|
||||
"lastWeekProgress": "Last week's progress",
|
||||
"redEnvelopeTips2": "*If the red envelope is not claimed within the time limit, the remaining coins will be returned to the user who sent the red envelope.",
|
||||
"goToRecharge": "Go to recharge",
|
||||
|
||||
@ -120,6 +120,7 @@
|
||||
"roomReward": "Oda Ödülü",
|
||||
"ownerSendTheRedEnvelope": "Sahip ödül jettonlarını gönderdi.",
|
||||
"rewardCoins": "Ödül jettonları:{1} jetton",
|
||||
"signInRewardReceived": "Giriş başarılı. Ödül: {1}",
|
||||
"lastWeekProgress": "Geçen Haftanın İlerlemesi",
|
||||
"redEnvelopeTips2": "*Kırmızı zarf zaman sınırı içinde talep edilmezse, kalan jettonlar gönderen kullanıcıya iade edilecektir.",
|
||||
"goToRecharge": "Yüklemeye Git",
|
||||
|
||||
@ -16,7 +16,7 @@ class SCVariant1Config implements AppConfig {
|
||||
@override
|
||||
String get apiHost => const String.fromEnvironment(
|
||||
'API_HOST',
|
||||
defaultValue: 'https://jvapi.haiyihy.com/',
|
||||
defaultValue: 'http://192.168.110.43:1100/',
|
||||
); // 默认连线上环境,本地调试可通过 --dart-define=API_HOST 覆盖
|
||||
|
||||
@override
|
||||
|
||||
@ -1570,6 +1570,9 @@ class SCAppLocalizations {
|
||||
String rewardCoins(String name) =>
|
||||
translate('rewardCoins').replaceAll('{1}', name);
|
||||
|
||||
String signInRewardReceived(String name) =>
|
||||
translate('signInRewardReceived').replaceAll('{1}', name);
|
||||
|
||||
String deleteAccount2(String name) =>
|
||||
translate('deleteAccount2').replaceAll('{1}', name);
|
||||
|
||||
|
||||
@ -186,7 +186,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
if (gift != null &&
|
||||
(gift.giftSourceUrl ?? "").isNotEmpty &&
|
||||
scGiftHasFullScreenEffect(gift.special)) {
|
||||
SCGiftVapSvgaManager().preload(gift.giftSourceUrl!, highPriority: true);
|
||||
SCGiftVapSvgaManager().preload(gift.giftSourceUrl!);
|
||||
_giftFxLog(
|
||||
'preload selected gift '
|
||||
'giftId=${gift.id} '
|
||||
@ -1443,10 +1443,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
||||
debouncer.debounce(
|
||||
duration: Duration(milliseconds: 350),
|
||||
onDebounce: () {
|
||||
Provider.of<RtcProvider>(
|
||||
navigatorKey.currentState!.context,
|
||||
listen: false,
|
||||
).retrieveMicrophoneList();
|
||||
rtcProvider?.requestGiftTriggeredMicRefresh();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@ -9,7 +9,6 @@ import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/app/constants/sc_screen.dart';
|
||||
import 'package:yumi/shared/tools/sc_path_utils.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/join_room_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
import 'package:yumi/shared/data_sources/models/enum/sc_room_special_mike_type.dart';
|
||||
@ -31,10 +30,7 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
"sc_images/room/sc_icon_room_seat_heartbeat_value.png";
|
||||
|
||||
RtcProvider? provider;
|
||||
JoinRoomRes? room;
|
||||
MicRes? roomSeat;
|
||||
JoinRoomRes? _cachedRoom;
|
||||
MicRes? _cachedRoomSeat;
|
||||
_SeatRenderSnapshot? _cachedSnapshot;
|
||||
final GlobalKey _targetKey = GlobalKey();
|
||||
|
||||
@override
|
||||
@ -46,24 +42,26 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final liveRoomSeat = provider?.roomWheatMap[widget.index];
|
||||
final liveRoom = provider?.currenRoom;
|
||||
if (provider?.isExitingCurrentVoiceRoomSession ?? false) {
|
||||
roomSeat = liveRoomSeat ?? _cachedRoomSeat;
|
||||
room = liveRoom ?? _cachedRoom;
|
||||
} else {
|
||||
roomSeat = liveRoomSeat;
|
||||
room = liveRoom;
|
||||
if (roomSeat != null) {
|
||||
_cachedRoomSeat = roomSeat;
|
||||
}
|
||||
if (room != null) {
|
||||
_cachedRoom = room;
|
||||
}
|
||||
}
|
||||
return Selector<RtcProvider, _SeatRenderSnapshot>(
|
||||
selector:
|
||||
(context, provider) =>
|
||||
_SeatRenderSnapshot.fromProvider(provider, widget.index),
|
||||
builder: (context, liveSnapshot, child) {
|
||||
final seatSnapshot = _resolveSeatSnapshot(liveSnapshot);
|
||||
final resolvedHeaddress =
|
||||
SCPathUtils.getFileExtension(
|
||||
seatSnapshot.headdressSourceUrl,
|
||||
).toLowerCase() ==
|
||||
".mp4" &&
|
||||
window.locale.languageCode == "ar"
|
||||
? ""
|
||||
: seatSnapshot.headdressSourceUrl;
|
||||
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
context.read<RtcProvider>().clickSite(widget.index);
|
||||
},
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
@ -75,26 +73,16 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
children: [
|
||||
Sonic(
|
||||
index: widget.index,
|
||||
room: room,
|
||||
specialMikeType: seatSnapshot.specialMikeType,
|
||||
isGameModel: widget.isGameModel,
|
||||
),
|
||||
roomSeat?.user != null
|
||||
seatSnapshot.hasUser
|
||||
? head(
|
||||
url: roomSeat?.user?.userAvatar ?? "",
|
||||
url: seatSnapshot.userAvatar,
|
||||
width: widget.isGameModel ? 40.w : 55.w,
|
||||
headdress:
|
||||
SCPathUtils.getFileExtension(
|
||||
roomSeat?.user
|
||||
?.getHeaddress()
|
||||
?.sourceUrl ??
|
||||
"",
|
||||
).toLowerCase() ==
|
||||
".mp4" &&
|
||||
window.locale.languageCode == "ar"
|
||||
? ""
|
||||
: roomSeat?.user?.getHeaddress()?.sourceUrl,
|
||||
headdress: resolvedHeaddress,
|
||||
)
|
||||
: ((roomSeat?.micLock ?? false)
|
||||
: (seatSnapshot.micLock
|
||||
? Image.asset(
|
||||
"sc_images/room/sc_icon_seat_lock.png",
|
||||
width: widget.isGameModel ? 38.w : 52.w,
|
||||
@ -109,7 +97,7 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
bottom: widget.isGameModel ? 2.w : 5.w,
|
||||
right: widget.isGameModel ? 2.w : 5.w,
|
||||
child:
|
||||
(roomSeat?.micMute ?? false)
|
||||
seatSnapshot.micMute
|
||||
? Image.asset(
|
||||
"sc_images/room/sc_icon_room_seat_mic_mute.png",
|
||||
width: 14.w,
|
||||
@ -128,7 +116,7 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
),
|
||||
widget.isGameModel
|
||||
? Container()
|
||||
: (roomSeat?.user != null
|
||||
: (seatSnapshot.hasUser
|
||||
? SizedBox(
|
||||
width: 64.w,
|
||||
child: Column(
|
||||
@ -140,23 +128,21 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
msgRoleTag(
|
||||
roomSeat?.user?.roles ?? "",
|
||||
seatSnapshot.userRoles,
|
||||
width: 15.w,
|
||||
height: 15.w,
|
||||
),
|
||||
Flexible(
|
||||
child: socialchatNickNameText(
|
||||
fontWeight: FontWeight.w600,
|
||||
roomSeat?.user?.userNickname ?? "",
|
||||
seatSnapshot.userNickname,
|
||||
fontSize: 10.sp,
|
||||
type: roomSeat?.user?.getVIP()?.name ?? "",
|
||||
type: seatSnapshot.userVipName,
|
||||
needScroll:
|
||||
(roomSeat
|
||||
?.user
|
||||
?.userNickname
|
||||
?.characters
|
||||
.length ??
|
||||
0) >
|
||||
seatSnapshot
|
||||
.userNickname
|
||||
.characters
|
||||
.length >
|
||||
8,
|
||||
),
|
||||
),
|
||||
@ -177,7 +163,9 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
),
|
||||
SizedBox(width: 2.w),
|
||||
text(
|
||||
_heartbeatVaFormat(),
|
||||
_heartbeatVaFormat(
|
||||
seatSnapshot.userHeartbeatValue,
|
||||
),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 8.sp,
|
||||
lineHeight: 1,
|
||||
@ -202,25 +190,166 @@ class _SCSeatItemState extends State<SCSeatItem> with TickerProviderStateMixin {
|
||||
)),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
Provider.of<RtcProvider>(
|
||||
context,
|
||||
listen: false,
|
||||
).clickSite(widget.index);
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _heartbeatVaFormat() {
|
||||
int value = (roomSeat?.user?.heartbeatVal ?? 0).toInt();
|
||||
if (value >= 1000000) {
|
||||
return "${(value / 1000000).toStringAsFixed(1)}M";
|
||||
_SeatRenderSnapshot _resolveSeatSnapshot(_SeatRenderSnapshot liveSnapshot) {
|
||||
if (liveSnapshot.isExitingCurrentVoiceRoomSession) {
|
||||
return _cachedSnapshot ?? liveSnapshot;
|
||||
}
|
||||
if (value >= 10000) {
|
||||
return "${(value / 1000).toStringAsFixed(0)}k";
|
||||
_cachedSnapshot = liveSnapshot;
|
||||
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 {
|
||||
@ -239,9 +368,9 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
late CurvedAnimation curvedAnimation;
|
||||
|
||||
bool showIn = false;
|
||||
MicRes? playingRes;
|
||||
_SeatEmojiEvent? playingEvent;
|
||||
|
||||
List<MicRes?> pathList = [];
|
||||
List<_SeatEmojiEvent> pathList = [];
|
||||
String? giftPath;
|
||||
List<String> giftList = [];
|
||||
bool showResult = false;
|
||||
@ -267,23 +396,31 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<RtcProvider>(
|
||||
builder: (BuildContext context, RtcProvider provider, Widget? child) {
|
||||
MicRes? micRes = provider.roomWheatMap[widget.index];
|
||||
if (micRes?.user == null) {
|
||||
return Selector<RtcProvider, _SeatEmojiSnapshot>(
|
||||
selector:
|
||||
(context, provider) =>
|
||||
_SeatEmojiSnapshot.fromMic(provider.roomWheatMap[widget.index]),
|
||||
builder: (
|
||||
BuildContext context,
|
||||
_SeatEmojiSnapshot snapshot,
|
||||
Widget? child,
|
||||
) {
|
||||
if (!snapshot.hasUser) {
|
||||
return Container();
|
||||
}
|
||||
String? emojiPath = provider.roomWheatMap[widget.index]?.emojiPath;
|
||||
if (emojiPath != null) {
|
||||
pathList.add(micRes);
|
||||
micRes?.setEmojiPath = null;
|
||||
if (snapshot.emojiPath != null) {
|
||||
pathList.add(
|
||||
_SeatEmojiEvent(type: snapshot.type, number: snapshot.number),
|
||||
);
|
||||
context.read<RtcProvider>().roomWheatMap[widget.index]?.setEmojiPath =
|
||||
null;
|
||||
_checkStart();
|
||||
}
|
||||
if (playingRes != null) {
|
||||
if (playingEvent != null) {
|
||||
return FadeTransition(
|
||||
opacity: curvedAnimation,
|
||||
child:
|
||||
playingRes?.type == SCRoomMsgType.roomDice
|
||||
playingEvent?.type == SCRoomMsgType.roomDice
|
||||
? FutureBuilder<void>(
|
||||
future: Future.delayed(Duration(milliseconds: 2000)),
|
||||
// 传入Future
|
||||
@ -294,7 +431,7 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
// 根据snapshot的状态来构建UI
|
||||
if (snapshot.connectionState == ConnectionState.done) {
|
||||
return Image.asset(
|
||||
"sc_images/room/sc_icon_dice_${micRes?.number}.png",
|
||||
"sc_images/room/sc_icon_dice_${playingEvent?.number}.png",
|
||||
height: widget.isGameModel ? 38.w : 45.w,
|
||||
);
|
||||
} else {
|
||||
@ -305,7 +442,7 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
}
|
||||
},
|
||||
)
|
||||
: (playingRes?.type == SCRoomMsgType.roomRPS
|
||||
: (playingEvent?.type == SCRoomMsgType.roomRPS
|
||||
? FutureBuilder<void>(
|
||||
future: Future.delayed(Duration(milliseconds: 2000)),
|
||||
// 传入Future
|
||||
@ -317,7 +454,7 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
if (snapshot.connectionState ==
|
||||
ConnectionState.done) {
|
||||
return Image.asset(
|
||||
"sc_images/room/sc_icon_rps_${micRes?.number}.png",
|
||||
"sc_images/room/sc_icon_rps_${playingEvent?.number}.png",
|
||||
height: widget.isGameModel ? 38.w : 45.w,
|
||||
);
|
||||
} else {
|
||||
@ -329,7 +466,7 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
},
|
||||
)
|
||||
: netImage(
|
||||
url: playingRes?.number ?? "",
|
||||
url: playingEvent?.number ?? "",
|
||||
width: widget.isGameModel ? 65.w : 75.w,
|
||||
)),
|
||||
);
|
||||
@ -349,13 +486,10 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
}
|
||||
|
||||
void _checkStart() {
|
||||
print('emoticonsAniCtr.status:${emoticonsAniCtr.status}');
|
||||
print('emoticonsAniCtr.path:${playingRes}');
|
||||
if (!showIn) {
|
||||
if (pathList.isNotEmpty) {
|
||||
playingRes = pathList.first;
|
||||
playingEvent = pathList.first;
|
||||
pathList.removeAt(0);
|
||||
print('播放表情:$playingRes');
|
||||
emoticonsAniCtr.forward();
|
||||
Future.delayed(Duration(milliseconds: 5)).then((value) {
|
||||
setState(() {
|
||||
@ -367,13 +501,12 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
Future.delayed(Duration(seconds: 3)).then((value) {
|
||||
emoticonsAniCtr.reverse();
|
||||
showIn = false;
|
||||
playingRes = null;
|
||||
playingEvent = null;
|
||||
_checkStart();
|
||||
});
|
||||
} else if (giftList.isNotEmpty) {
|
||||
giftPath = giftList.first;
|
||||
giftList.removeAt(0);
|
||||
print('播放礼物:$playingRes');
|
||||
emoticonsAniCtr.forward();
|
||||
Future.delayed(Duration(milliseconds: 10)).then((value) {
|
||||
setState(() {
|
||||
@ -396,12 +529,12 @@ class _EmoticonsState extends State<Emoticons> with TickerProviderStateMixin {
|
||||
class Sonic extends StatefulWidget {
|
||||
final num index;
|
||||
final bool isGameModel;
|
||||
final JoinRoomRes? room;
|
||||
final String specialMikeType;
|
||||
|
||||
const Sonic({
|
||||
Key? key,
|
||||
required this.index,
|
||||
required this.room,
|
||||
required this.specialMikeType,
|
||||
this.isGameModel = false,
|
||||
}) : super(key: key);
|
||||
|
||||
@ -450,23 +583,33 @@ class _SonicState extends State<Sonic> with TickerProviderStateMixin {
|
||||
return Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
SonicItem(ctrList[0], widget.room, isGameModel: widget.isGameModel),
|
||||
SonicItem(ctrList[1], widget.room, isGameModel: widget.isGameModel),
|
||||
SonicItem(ctrList[2], widget.room, isGameModel: widget.isGameModel),
|
||||
SonicItem(ctrList[3], widget.room, isGameModel: widget.isGameModel),
|
||||
SonicItem(
|
||||
ctrList[0],
|
||||
widget.specialMikeType,
|
||||
isGameModel: widget.isGameModel,
|
||||
),
|
||||
SonicItem(
|
||||
ctrList[1],
|
||||
widget.specialMikeType,
|
||||
isGameModel: widget.isGameModel,
|
||||
),
|
||||
SonicItem(
|
||||
ctrList[2],
|
||||
widget.specialMikeType,
|
||||
isGameModel: widget.isGameModel,
|
||||
),
|
||||
SonicItem(
|
||||
ctrList[3],
|
||||
widget.specialMikeType,
|
||||
isGameModel: widget.isGameModel,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _checkSoundAni(int volume) async {
|
||||
if (volume <= 20) return;
|
||||
// await Future.delayed(Duration(milliseconds: 10));
|
||||
// var dateTime = DateTime.now();
|
||||
// for(int i =0 ; i < 5000000;i++){
|
||||
// lastIndex = 1;
|
||||
// }
|
||||
// print('执行耗时:${DateTime.now().millisecondsSinceEpoch - dateTime.millisecondsSinceEpoch}');
|
||||
ctrList.forEach((element) {
|
||||
for (final element in ctrList) {
|
||||
if (element.value == 0) {
|
||||
if (lastIndex == 0) {
|
||||
element.forward();
|
||||
@ -481,11 +624,10 @@ class _SonicState extends State<Sonic> with TickerProviderStateMixin {
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_onVoiceChange(num index, int volum) {
|
||||
print('说话声音:$volum');
|
||||
if (widget.index == index) {
|
||||
_checkSoundAni(volum);
|
||||
}
|
||||
@ -494,11 +636,15 @@ class _SonicState extends State<Sonic> with TickerProviderStateMixin {
|
||||
|
||||
class SonicItem extends AnimatedWidget {
|
||||
final Animation<double> animation;
|
||||
final JoinRoomRes? room;
|
||||
final String specialMikeType;
|
||||
final bool isGameModel;
|
||||
|
||||
SonicItem(this.animation, this.room, {super.key, this.isGameModel = false})
|
||||
: super(listenable: animation);
|
||||
SonicItem(
|
||||
this.animation,
|
||||
this.specialMikeType, {
|
||||
super.key,
|
||||
this.isGameModel = false,
|
||||
}) : super(listenable: animation);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -510,15 +656,14 @@ class SonicItem extends AnimatedWidget {
|
||||
height: width(isGameModel ? 34 : 52),
|
||||
width: width(isGameModel ? 34 : 52),
|
||||
decoration:
|
||||
(room?.roomProfile?.roomSetting?.roomSpecialMikeType ?? "")
|
||||
.isEmpty ||
|
||||
(room?.roomProfile?.roomSetting?.roomSpecialMikeType !=
|
||||
specialMikeType.isEmpty ||
|
||||
(specialMikeType !=
|
||||
SCRoomSpecialMikeType.TYPE_VIP3.name &&
|
||||
room?.roomProfile?.roomSetting?.roomSpecialMikeType !=
|
||||
specialMikeType !=
|
||||
SCRoomSpecialMikeType.TYPE_VIP4.name &&
|
||||
room?.roomProfile?.roomSetting?.roomSpecialMikeType !=
|
||||
specialMikeType !=
|
||||
SCRoomSpecialMikeType.TYPE_VIP5.name &&
|
||||
room?.roomProfile?.roomSetting?.roomSpecialMikeType !=
|
||||
specialMikeType !=
|
||||
SCRoomSpecialMikeType.TYPE_VIP6.name)
|
||||
? BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
@ -528,25 +673,19 @@ class SonicItem extends AnimatedWidget {
|
||||
)
|
||||
: BoxDecoration(),
|
||||
child:
|
||||
room?.roomProfile?.roomSetting?.roomSpecialMikeType ==
|
||||
SCRoomSpecialMikeType.TYPE_VIP3.name
|
||||
specialMikeType == SCRoomSpecialMikeType.TYPE_VIP3.name
|
||||
? Image.asset(
|
||||
"sc_images/room/sc_icon_room_vip3_sonic_anim.webp",
|
||||
)
|
||||
: (room?.roomProfile?.roomSetting?.roomSpecialMikeType ==
|
||||
SCRoomSpecialMikeType.TYPE_VIP4.name
|
||||
: (specialMikeType == SCRoomSpecialMikeType.TYPE_VIP4.name
|
||||
? Image.asset(
|
||||
"sc_images/room/sc_icon_room_vip4_sonic_anim.webp",
|
||||
)
|
||||
: (room?.roomProfile?.roomSetting?.roomSpecialMikeType ==
|
||||
SCRoomSpecialMikeType.TYPE_VIP5.name
|
||||
: (specialMikeType == SCRoomSpecialMikeType.TYPE_VIP5.name
|
||||
? Image.asset(
|
||||
"sc_images/room/sc_icon_room_vip5_sonic_anim.webp",
|
||||
)
|
||||
: (room
|
||||
?.roomProfile
|
||||
?.roomSetting
|
||||
?.roomSpecialMikeType ==
|
||||
: (specialMikeType ==
|
||||
SCRoomSpecialMikeType.TYPE_VIP6.name
|
||||
? Image.asset(
|
||||
"sc_images/room/sc_icon_room_vip6_sonic_anim.webp",
|
||||
|
||||
@ -141,17 +141,16 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
SCGiftVapSvgaManager().stopPlayback();
|
||||
}
|
||||
|
||||
bool roomThemeBackActi(JoinRoomRes? room) {
|
||||
if (room?.roomProps?.roomTheme != null) {
|
||||
if (room?.roomProps?.roomTheme?.themeBack != null &&
|
||||
room!.roomProps!.roomTheme!.themeBack!.isNotEmpty) {
|
||||
if ((room.roomProps?.roomTheme?.expireTime ?? 0) >
|
||||
DateTime.now().millisecondsSinceEpoch) {
|
||||
return true;
|
||||
String? _resolveRoomThemeBackground(JoinRoomRes? room) {
|
||||
final roomTheme = room?.roomProps?.roomTheme;
|
||||
final themeBack = roomTheme?.themeBack ?? "";
|
||||
if (themeBack.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
if ((roomTheme?.expireTime ?? 0) <= DateTime.now().millisecondsSinceEpoch) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return themeBack;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -173,13 +172,14 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
top: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
Consumer<RtcProvider>(
|
||||
builder: (context, ref, child) {
|
||||
return roomThemeBackActi(ref.currenRoom)
|
||||
Selector<RtcProvider, String?>(
|
||||
selector:
|
||||
(context, provider) =>
|
||||
_resolveRoomThemeBackground(provider.currenRoom),
|
||||
builder: (context, roomThemeBackground, child) {
|
||||
return roomThemeBackground != null
|
||||
? netImage(
|
||||
url:
|
||||
ref.currenRoom?.roomProps?.roomTheme?.themeBack ??
|
||||
"",
|
||||
url: roomThemeBackground,
|
||||
width: ScreenUtil().screenWidth,
|
||||
height: ScreenUtil().screenHeight,
|
||||
noDefaultImg: true,
|
||||
@ -395,6 +395,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
||||
giftModel.sendUserPic = msg.user?.userAvatar ?? "";
|
||||
giftModel.giftPic = msg.gift?.giftPhoto ?? "";
|
||||
giftModel.giftCount = 0;
|
||||
giftModel.rewardAmount = awardAmount;
|
||||
giftModel.showLuckyRewardFrame = true;
|
||||
giftModel.rewardAmountText = _formatLuckyRewardAmount(awardAmount);
|
||||
Provider.of<GiftAnimationManager>(
|
||||
|
||||
@ -133,6 +133,16 @@ class BaishunJsBridge {
|
||||
}
|
||||
}));
|
||||
}
|
||||
function maskText(value) {
|
||||
const text = (value == null ? '' : String(value)).trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
if (text.length <= 10) {
|
||||
return text;
|
||||
}
|
||||
return text.slice(0, 6) + '***' + text.slice(-4);
|
||||
}
|
||||
function ensureChannelAlias(name, handler) {
|
||||
if (!window[name] || typeof window[name].postMessage !== 'function') {
|
||||
window[name] = { postMessage: handler };
|
||||
@ -146,19 +156,27 @@ class BaishunJsBridge {
|
||||
}
|
||||
}
|
||||
window.NativeBridge.getConfigSync = function() {
|
||||
postDebug('bridge.getConfigSync', {
|
||||
hasConfig: !!window.__baishunLastConfig,
|
||||
hasNativeBridgeConfig: !!(window.NativeBridge && window.NativeBridge.config)
|
||||
});
|
||||
return window.__baishunLastConfig || null;
|
||||
};
|
||||
window.NativeBridge.getConfig = function(params) {
|
||||
postDebug('bridge.getConfig.call', toPayload(params));
|
||||
postAction('${BaishunBridgeActions.getConfig}', params);
|
||||
return window.__baishunLastConfig || null;
|
||||
};
|
||||
window.NativeBridge.destroy = function(payload) {
|
||||
postDebug('bridge.destroy.call', toPayload(payload));
|
||||
postAction('${BaishunBridgeActions.destroy}', payload);
|
||||
};
|
||||
window.NativeBridge.gameRecharge = function(payload) {
|
||||
postDebug('bridge.gameRecharge.call', toPayload(payload));
|
||||
postAction('${BaishunBridgeActions.gameRecharge}', payload);
|
||||
};
|
||||
window.NativeBridge.gameLoaded = function(payload) {
|
||||
postDebug('bridge.gameLoaded.call', toPayload(payload));
|
||||
postAction('${BaishunBridgeActions.gameLoaded}', payload);
|
||||
};
|
||||
window.__baishunDebugLog = postDebug;
|
||||
@ -359,6 +377,10 @@ class BaishunJsBridge {
|
||||
if (typeof window.dispatchEvent === 'function' && typeof CustomEvent === 'function') {
|
||||
window.dispatchEvent(new CustomEvent('baishunBridgeReady'));
|
||||
}
|
||||
postDebug('bridge.ready', {
|
||||
href: window.location && window.location.href,
|
||||
ua: navigator && navigator.userAgent
|
||||
});
|
||||
})();
|
||||
''';
|
||||
}
|
||||
@ -373,6 +395,16 @@ class BaishunJsBridge {
|
||||
(function() {
|
||||
const config = $payload;
|
||||
const explicitCallbackPath = $encodedCallbackPath;
|
||||
function maskText(value) {
|
||||
const text = (value == null ? '' : String(value)).trim();
|
||||
if (!text) {
|
||||
return '';
|
||||
}
|
||||
if (text.length <= 10) {
|
||||
return text;
|
||||
}
|
||||
return text.slice(0, 6) + '***' + text.slice(-4);
|
||||
}
|
||||
function resolvePath(path) {
|
||||
if (!path || typeof path !== 'string') {
|
||||
return null;
|
||||
@ -407,15 +439,39 @@ class BaishunJsBridge {
|
||||
window.baishunBridgeConfig = config;
|
||||
window.NativeBridge = window.NativeBridge || {};
|
||||
window.NativeBridge.config = config;
|
||||
const callbackPath = explicitCallbackPath || window.__baishunLastJsCallback || '';
|
||||
let callbackInvoked = false;
|
||||
if (explicitCallbackPath) {
|
||||
window.__baishunLastJsCallback = explicitCallbackPath;
|
||||
}
|
||||
if (typeof window.__baishunDebugLog === 'function') {
|
||||
window.__baishunDebugLog('config.deliver', {
|
||||
appId: config.appId,
|
||||
appChannel: config.appChannel,
|
||||
userId: config.userId,
|
||||
roomId: config.roomId,
|
||||
gameMode: config.gameMode,
|
||||
language: config.language,
|
||||
gsp: config.gsp,
|
||||
code: maskText(config.code),
|
||||
codeLength: config.code ? String(config.code).length : 0,
|
||||
callbackPath: callbackPath
|
||||
});
|
||||
}
|
||||
if (typeof window.__baishunGetConfigCallback === 'function') {
|
||||
window.__baishunGetConfigCallback(config);
|
||||
window.__baishunGetConfigCallback = null;
|
||||
}
|
||||
if (window.__baishunLastJsCallback) {
|
||||
invokeCallbackPath(window.__baishunLastJsCallback);
|
||||
callbackInvoked = invokeCallbackPath(window.__baishunLastJsCallback) || callbackInvoked;
|
||||
}
|
||||
if (typeof window.__baishunDebugLog === 'function') {
|
||||
window.__baishunDebugLog('config.callback', {
|
||||
callbackPath: callbackPath,
|
||||
callbackInvoked: callbackInvoked,
|
||||
hasLegacyCallback: typeof window.__baishunGetConfigCallback === 'function',
|
||||
hasOnBaishunConfig: typeof window.onBaishunConfig === 'function'
|
||||
});
|
||||
}
|
||||
if (typeof window.onBaishunConfig === 'function') {
|
||||
window.onBaishunConfig(config);
|
||||
@ -438,6 +494,9 @@ class BaishunJsBridge {
|
||||
return '''
|
||||
(function() {
|
||||
const payload = $safePayload;
|
||||
if (typeof window.__baishunDebugLog === 'function') {
|
||||
window.__baishunDebugLog('wallet.update', payload);
|
||||
}
|
||||
if (window.baishunChannel && typeof window.baishunChannel.walletUpdate === 'function') {
|
||||
window.baishunChannel.walletUpdate(payload);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
@ -31,6 +32,8 @@ class BaishunGamePage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
static const String _logPrefix = '[BaishunGame]';
|
||||
|
||||
final RoomGameRepository _repository = RoomGameRepository();
|
||||
final Set<Factory<OneSequenceGestureRecognizer>> _webGestureRecognizers =
|
||||
<Factory<OneSequenceGestureRecognizer>>{
|
||||
@ -45,6 +48,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
bool _didReceiveBridgeMessage = false;
|
||||
bool _didFinishPageLoad = false;
|
||||
bool _hasDeliveredLaunchConfig = false;
|
||||
int _bridgeInjectCount = 0;
|
||||
String? _errorMessage;
|
||||
|
||||
@override
|
||||
@ -92,15 +96,21 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
)
|
||||
..setNavigationDelegate(
|
||||
NavigationDelegate(
|
||||
onPageStarted: (String _) {
|
||||
onPageStarted: (String url) {
|
||||
_log('page_started url=${_clip(url, 240)}');
|
||||
_prepareForPageLoad();
|
||||
},
|
||||
onPageFinished: (String _) async {
|
||||
onPageFinished: (String url) async {
|
||||
_didFinishPageLoad = true;
|
||||
_log('page_finished url=${_clip(url, 240)}');
|
||||
await _injectBridge(reason: 'page_finished');
|
||||
},
|
||||
onWebResourceError: (WebResourceError error) {
|
||||
_stopBridgeBootstrap();
|
||||
_log(
|
||||
'web_resource_error code=${error.errorCode} '
|
||||
'type=${error.errorType} desc=${_clip(error.description, 300)}',
|
||||
);
|
||||
_stopBridgeBootstrap(reason: 'web_resource_error');
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
@ -111,36 +121,56 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
},
|
||||
),
|
||||
);
|
||||
_log('init launch=${_stringifyForLog(_buildLaunchSummary())}');
|
||||
unawaited(_loadGameEntry());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopBridgeBootstrap();
|
||||
_log('dispose isClosing=$_isClosing');
|
||||
_stopBridgeBootstrap(reason: 'dispose');
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _prepareForPageLoad() {
|
||||
_log(
|
||||
'prepare_page_load session=${widget.launchModel.gameSessionId} '
|
||||
'entry=${_clip(widget.launchModel.entry.entryUrl, 240)}',
|
||||
);
|
||||
_didReceiveBridgeMessage = false;
|
||||
_didFinishPageLoad = false;
|
||||
_hasDeliveredLaunchConfig = false;
|
||||
_stopBridgeBootstrap();
|
||||
_bridgeInjectCount = 0;
|
||||
_stopBridgeBootstrap(reason: 'prepare_page_load');
|
||||
_bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), (
|
||||
Timer timer,
|
||||
) {
|
||||
if (!mounted || _didReceiveBridgeMessage || _errorMessage != null) {
|
||||
_log(
|
||||
'bootstrap_timer_stop tick=${timer.tick} '
|
||||
'didReceiveBridge=$_didReceiveBridgeMessage '
|
||||
'hasError=${_errorMessage != null}',
|
||||
);
|
||||
timer.cancel();
|
||||
return;
|
||||
}
|
||||
unawaited(_injectBridge(reason: 'bootstrap'));
|
||||
if (_didFinishPageLoad && timer.tick >= 12) {
|
||||
_log(
|
||||
'bootstrap_timer_stop tick=${timer.tick} reason=page_finished_guard',
|
||||
);
|
||||
timer.cancel();
|
||||
}
|
||||
});
|
||||
_loadingFallbackTimer = Timer(const Duration(seconds: 6), () {
|
||||
if (!mounted || _didReceiveBridgeMessage || _errorMessage != null) {
|
||||
_log(
|
||||
'loading_fallback_skip didReceiveBridge=$_didReceiveBridgeMessage '
|
||||
'hasError=${_errorMessage != null}',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_log('loading_fallback_fire isLoading=$_isLoading');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
});
|
||||
@ -154,7 +184,10 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
});
|
||||
}
|
||||
|
||||
void _stopBridgeBootstrap() {
|
||||
void _stopBridgeBootstrap({String reason = 'manual'}) {
|
||||
if (_bridgeBootstrapTimer != null || _loadingFallbackTimer != null) {
|
||||
_log('stop_bootstrap reason=$reason');
|
||||
}
|
||||
_bridgeBootstrapTimer?.cancel();
|
||||
_bridgeBootstrapTimer = null;
|
||||
_loadingFallbackTimer?.cancel();
|
||||
@ -165,10 +198,15 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
try {
|
||||
_prepareForPageLoad();
|
||||
final entryUrl = widget.launchModel.entry.entryUrl.trim();
|
||||
_log(
|
||||
'load_entry launchMode=${widget.launchModel.entry.launchMode} '
|
||||
'entryUrl=${_clip(entryUrl, 240)}',
|
||||
);
|
||||
if (entryUrl.isEmpty || entryUrl.startsWith('mock://')) {
|
||||
final html = await rootBundle.loadString(
|
||||
'assets/debug/baishun_mock/index.html',
|
||||
);
|
||||
_log('load_mock_html baseUrl=https://baishun.mock.local/');
|
||||
await _controller.loadHtmlString(
|
||||
html,
|
||||
baseUrl: 'https://baishun.mock.local/',
|
||||
@ -180,8 +218,10 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
if (uri == null) {
|
||||
throw Exception('Invalid game entry url: $entryUrl');
|
||||
}
|
||||
_log('load_request uri=${_clip(uri.toString(), 240)}');
|
||||
await _controller.loadRequest(uri);
|
||||
} catch (error) {
|
||||
_log('load_entry_error error=${_clip(error.toString(), 400)}');
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
@ -193,9 +233,19 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
}
|
||||
|
||||
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 {
|
||||
await _controller.runJavaScript(BaishunJsBridge.bootstrapScript());
|
||||
} catch (_) {}
|
||||
} catch (error) {
|
||||
_log(
|
||||
'inject_bridge_error reason=$reason error=${_clip(error.toString(), 300)}',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _sendConfigToGame({
|
||||
@ -203,11 +253,16 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
bool force = false,
|
||||
}) async {
|
||||
if (_hasDeliveredLaunchConfig && !force) {
|
||||
_log('skip_send_config reason=already_delivered');
|
||||
return;
|
||||
}
|
||||
_hasDeliveredLaunchConfig = true;
|
||||
final rawConfig = widget.launchModel.bridgeConfig;
|
||||
final config = _buildEffectiveBridgeConfig(rawConfig);
|
||||
_log(
|
||||
'send_config jsCallback=${jsCallbackPath ?? ''} '
|
||||
'force=$force config=${_stringifyForLog(_buildConfigSummary(config, rawConfig: rawConfig))}',
|
||||
);
|
||||
try {
|
||||
await _controller.runJavaScript(
|
||||
BaishunJsBridge.buildConfigScript(
|
||||
@ -215,10 +270,13 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
jsCallbackPath: jsCallbackPath,
|
||||
),
|
||||
);
|
||||
} catch (_) {}
|
||||
} catch (error) {
|
||||
_log('send_config_error error=${_clip(error.toString(), 300)}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleBridgeMessage(JavaScriptMessage message) async {
|
||||
_log('channel_message raw=${_clip(message.message, 600)}');
|
||||
final bridgeMessage = BaishunBridgeMessage.parse(message.message);
|
||||
await _dispatchBridgeMessage(bridgeMessage);
|
||||
}
|
||||
@ -231,6 +289,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
action,
|
||||
message.message,
|
||||
);
|
||||
_log('named_channel action=$action raw=${_clip(message.message, 600)}');
|
||||
await _dispatchBridgeMessage(bridgeMessage);
|
||||
}
|
||||
|
||||
@ -238,12 +297,20 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
BaishunBridgeMessage bridgeMessage,
|
||||
) async {
|
||||
if (bridgeMessage.action == BaishunBridgeActions.debugLog) {
|
||||
final tag = bridgeMessage.payload['tag']?.toString().trim();
|
||||
final message = bridgeMessage.payload['message']?.toString() ?? '';
|
||||
_log('h5_debug tag=${tag ?? 'unknown'} message=${_clip(message, 800)}');
|
||||
return;
|
||||
}
|
||||
final callbackPath = _extractCallbackPath(bridgeMessage.payload);
|
||||
_log(
|
||||
'bridge_action action=${bridgeMessage.action} '
|
||||
'callback=${callbackPath.isEmpty ? '-' : callbackPath} '
|
||||
'payload=${_stringifyForLog(_sanitizeForLog(bridgeMessage.payload))}',
|
||||
);
|
||||
if (bridgeMessage.action.isNotEmpty) {
|
||||
_didReceiveBridgeMessage = true;
|
||||
_stopBridgeBootstrap();
|
||||
_stopBridgeBootstrap(reason: 'bridge_message_${bridgeMessage.action}');
|
||||
}
|
||||
switch (bridgeMessage.action) {
|
||||
case BaishunBridgeActions.getConfig:
|
||||
@ -255,35 +322,43 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_log('game_loaded received');
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_errorMessage = null;
|
||||
});
|
||||
break;
|
||||
case BaishunBridgeActions.gameRecharge:
|
||||
_log('game_recharge open_wallet');
|
||||
await SCNavigatorUtils.push(context, WalletRoute.recharge);
|
||||
await _notifyWalletUpdate();
|
||||
break;
|
||||
case BaishunBridgeActions.destroy:
|
||||
_log('destroy requested by h5');
|
||||
await _closeAndExit(reason: 'h5_destroy');
|
||||
break;
|
||||
default:
|
||||
_log('bridge_action_unhandled action=${bridgeMessage.action}');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _notifyWalletUpdate() async {
|
||||
_log('notify_wallet_update');
|
||||
try {
|
||||
await _controller.runJavaScript(
|
||||
BaishunJsBridge.buildWalletUpdateScript(),
|
||||
);
|
||||
} catch (_) {}
|
||||
} catch (error) {
|
||||
_log('notify_wallet_update_error error=${_clip(error.toString(), 300)}');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _reload() async {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
_log('reload');
|
||||
setState(() {
|
||||
_errorMessage = null;
|
||||
_isLoading = true;
|
||||
@ -293,19 +368,27 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
|
||||
Future<void> _closeAndExit({String reason = 'page_back'}) async {
|
||||
if (_isClosing) {
|
||||
_log('close_skip reason=already_closing source=$reason');
|
||||
return;
|
||||
}
|
||||
_isClosing = true;
|
||||
_log('close_start reason=$reason');
|
||||
try {
|
||||
if (!widget.launchModel.gameSessionId.startsWith('bs_mock_')) {
|
||||
await _repository.closeBaishunGame(
|
||||
final result = await _repository.closeBaishunGame(
|
||||
roomId: widget.roomId,
|
||||
gameSessionId: widget.launchModel.gameSessionId,
|
||||
reason: reason,
|
||||
);
|
||||
_log(
|
||||
'close_success result=${_stringifyForLog(<String, dynamic>{'roomId': result.roomId, 'state': result.state, 'gameSessionId': result.gameSessionId, 'currentGameId': result.currentGameId, 'hostUserId': result.hostUserId})}',
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
_log('close_error error=${_clip(error.toString(), 300)}');
|
||||
}
|
||||
} catch (_) {}
|
||||
if (mounted) {
|
||||
_log('close_pop');
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
@ -324,6 +407,110 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
|
||||
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 rawConfig,
|
||||
) {
|
||||
|
||||
@ -22,6 +22,7 @@ class RoomGameListSheet extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
static const String _logPrefix = '[BaishunLaunch]';
|
||||
static const int _itemsPerRow = 4;
|
||||
static const String _sheetFrameAsset =
|
||||
'sc_images/room/sc_room_game_sheet_frame.png';
|
||||
@ -78,11 +79,31 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
});
|
||||
|
||||
try {
|
||||
_log(
|
||||
'launch_request roomId=$_roomId gameId=${game.gameId} '
|
||||
'vendorGameId=${game.vendorGameId} gameMode=${game.gameMode} '
|
||||
'origin=${Platform.isAndroid ? 'ANDROID' : 'IOS'}',
|
||||
);
|
||||
final launchModel = await _repository.launchBaishunGame(
|
||||
roomId: _roomId,
|
||||
gameId: game.gameId,
|
||||
clientOrigin: Platform.isAndroid ? 'ANDROID' : 'IOS',
|
||||
);
|
||||
_log(
|
||||
'launch_success payload=${_stringifyForLog(<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) {
|
||||
return;
|
||||
}
|
||||
@ -99,6 +120,7 @@ class _RoomGameListSheetState extends State<RoomGameListSheet> {
|
||||
barrierDismissible: true,
|
||||
);
|
||||
} catch (error) {
|
||||
_log('launch_error error=${_clip(error.toString(), 400)}');
|
||||
SCTts.show('Launch failed');
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@ -109,6 +131,36 @@ class _RoomGameListSheetState extends State<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
|
||||
Widget build(BuildContext context) {
|
||||
return SafeArea(
|
||||
|
||||
@ -48,6 +48,10 @@ typedef RtcProvider = RealTimeCommunicationManager;
|
||||
class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
static const Duration _micListPollingInterval = Duration(seconds: 2);
|
||||
static const Duration _onlineUsersPollingInterval = Duration(seconds: 3);
|
||||
static const Duration _selfMicSwitchGracePeriod = Duration(seconds: 4);
|
||||
static const Duration _giftTriggeredMicRefreshMinInterval = Duration(
|
||||
milliseconds: 900,
|
||||
);
|
||||
|
||||
bool needUpDataUserInfo = false;
|
||||
bool _roomVisualEffectsEnabled = false;
|
||||
@ -56,6 +60,17 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
Timer? _onlineUsersPollingTimer;
|
||||
bool _isRefreshingMicList = false;
|
||||
bool _isRefreshingOnlineUsers = false;
|
||||
bool _pendingMicListRefresh = false;
|
||||
Timer? _deferredMicListRefreshTimer;
|
||||
int _lastMicListRefreshStartedAtMs = 0;
|
||||
num? _preferredSelfMicIndex;
|
||||
num? _pendingSelfMicSourceIndex;
|
||||
int? _pendingSelfMicSwitchGuardUntilMs;
|
||||
ClientRoleType? _lastAppliedClientRole;
|
||||
bool? _lastAppliedLocalAudioMuted;
|
||||
bool? _lastScheduledVoiceLiveOnMic;
|
||||
String? _lastScheduledVoiceLiveRoomId;
|
||||
String? _lastScheduledAnchorRoomId;
|
||||
|
||||
///当前所在房间
|
||||
JoinRoomRes? currenRoom;
|
||||
@ -141,8 +156,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
void _stopRoomStatePolling() {
|
||||
_micListPollingTimer?.cancel();
|
||||
_onlineUsersPollingTimer?.cancel();
|
||||
_deferredMicListRefreshTimer?.cancel();
|
||||
_micListPollingTimer = null;
|
||||
_onlineUsersPollingTimer = null;
|
||||
_deferredMicListRefreshTimer = null;
|
||||
_isRefreshingMicList = false;
|
||||
_isRefreshingOnlineUsers = false;
|
||||
}
|
||||
@ -209,14 +226,13 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
void _refreshManagerUsers(List<SocialChatUserProfile> users) {
|
||||
managerUsers
|
||||
..clear()
|
||||
..addAll(
|
||||
users.where((user) => user.roles == SCRoomRolesType.ADMIN.name),
|
||||
);
|
||||
..addAll(users.where((user) => user.roles == SCRoomRolesType.ADMIN.name));
|
||||
}
|
||||
|
||||
Map<num, MicRes> _buildMicMap(List<MicRes> roomWheatList) {
|
||||
final previousMap = roomWheatMap;
|
||||
final nextMap = <num, MicRes>{};
|
||||
final userSeatMap = <String, num>{};
|
||||
for (final roomWheat in roomWheatList) {
|
||||
final micIndex = roomWheat.micIndex;
|
||||
if (micIndex == null) {
|
||||
@ -226,7 +242,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
final shouldPreserveTransientState =
|
||||
previousMic?.user?.id == roomWheat.user?.id &&
|
||||
previousMic?.user?.id != null;
|
||||
nextMap[micIndex] =
|
||||
final mergedMic =
|
||||
shouldPreserveTransientState
|
||||
? roomWheat.copyWith(
|
||||
emojiPath:
|
||||
@ -243,15 +259,173 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
: previousMic?.number,
|
||||
)
|
||||
: roomWheat;
|
||||
final normalizedUserId = (mergedMic.user?.id ?? "").trim();
|
||||
if (normalizedUserId.isNotEmpty) {
|
||||
final existingIndex = userSeatMap[normalizedUserId];
|
||||
if (existingIndex != null) {
|
||||
final shouldReplaceExistingSeat = _shouldPreferDuplicateMicSeat(
|
||||
userId: normalizedUserId,
|
||||
existingIndex: existingIndex,
|
||||
candidateIndex: micIndex,
|
||||
previousMap: previousMap,
|
||||
);
|
||||
if (!shouldReplaceExistingSeat) {
|
||||
continue;
|
||||
}
|
||||
nextMap.remove(existingIndex);
|
||||
}
|
||||
userSeatMap[normalizedUserId] = micIndex;
|
||||
}
|
||||
nextMap[micIndex] = mergedMic;
|
||||
}
|
||||
return _stabilizeSelfMicSnapshot(nextMap, previousMap: previousMap);
|
||||
}
|
||||
|
||||
void _startSelfMicSwitchGuard({
|
||||
required num sourceIndex,
|
||||
required num targetIndex,
|
||||
}) {
|
||||
_preferredSelfMicIndex = targetIndex;
|
||||
_pendingSelfMicSourceIndex = sourceIndex;
|
||||
_pendingSelfMicSwitchGuardUntilMs =
|
||||
DateTime.now().add(_selfMicSwitchGracePeriod).millisecondsSinceEpoch;
|
||||
}
|
||||
|
||||
void _clearSelfMicSwitchGuard({bool clearPreferredIndex = false}) {
|
||||
_pendingSelfMicSourceIndex = null;
|
||||
_pendingSelfMicSwitchGuardUntilMs = null;
|
||||
if (clearPreferredIndex) {
|
||||
_preferredSelfMicIndex = null;
|
||||
}
|
||||
}
|
||||
|
||||
Map<num, MicRes> _stabilizeSelfMicSnapshot(
|
||||
Map<num, MicRes> nextMap, {
|
||||
required Map<num, MicRes> previousMap,
|
||||
}) {
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
if (currentUserId.isEmpty) {
|
||||
return nextMap;
|
||||
}
|
||||
|
||||
final targetIndex = _preferredSelfMicIndex;
|
||||
final sourceIndex = _pendingSelfMicSourceIndex;
|
||||
final guardUntilMs = _pendingSelfMicSwitchGuardUntilMs ?? 0;
|
||||
final hasActiveGuard =
|
||||
targetIndex != null &&
|
||||
sourceIndex != null &&
|
||||
guardUntilMs > DateTime.now().millisecondsSinceEpoch;
|
||||
final selfSeatIndices =
|
||||
nextMap.entries
|
||||
.where((entry) => entry.value.user?.id == currentUserId)
|
||||
.map((entry) => entry.key)
|
||||
.toList();
|
||||
|
||||
if (targetIndex != null &&
|
||||
nextMap[targetIndex]?.user?.id == currentUserId) {
|
||||
_clearSelfMicSwitchGuard();
|
||||
return nextMap;
|
||||
}
|
||||
|
||||
if (!hasActiveGuard) {
|
||||
_clearSelfMicSwitchGuard();
|
||||
return nextMap;
|
||||
}
|
||||
final resolvedTargetIndex = targetIndex;
|
||||
final resolvedSourceIndex = sourceIndex;
|
||||
|
||||
final selfOnlyOnSource =
|
||||
selfSeatIndices.isEmpty ||
|
||||
selfSeatIndices.every((seatIndex) => seatIndex == resolvedSourceIndex);
|
||||
if (!selfOnlyOnSource) {
|
||||
return nextMap;
|
||||
}
|
||||
|
||||
final optimisticTargetSeat = previousMap[resolvedTargetIndex];
|
||||
if (optimisticTargetSeat == null ||
|
||||
optimisticTargetSeat.user?.id != currentUserId) {
|
||||
return nextMap;
|
||||
}
|
||||
|
||||
final incomingTargetSeat = nextMap[resolvedTargetIndex];
|
||||
if (incomingTargetSeat?.user != null &&
|
||||
incomingTargetSeat?.user?.id != currentUserId) {
|
||||
return nextMap;
|
||||
}
|
||||
|
||||
final stabilizedMap = Map<num, MicRes>.from(nextMap);
|
||||
for (final seatIndex in selfSeatIndices) {
|
||||
if (seatIndex == resolvedTargetIndex) {
|
||||
continue;
|
||||
}
|
||||
final seat = stabilizedMap[seatIndex];
|
||||
if (seat == null) {
|
||||
continue;
|
||||
}
|
||||
stabilizedMap[seatIndex] = seat.copyWith(clearUser: true);
|
||||
}
|
||||
|
||||
final baseSeat = incomingTargetSeat ?? optimisticTargetSeat;
|
||||
stabilizedMap[resolvedTargetIndex] = baseSeat.copyWith(
|
||||
user: optimisticTargetSeat.user,
|
||||
micMute: incomingTargetSeat?.micMute ?? optimisticTargetSeat.micMute,
|
||||
micLock: incomingTargetSeat?.micLock ?? optimisticTargetSeat.micLock,
|
||||
roomToken:
|
||||
incomingTargetSeat?.roomToken ?? optimisticTargetSeat.roomToken,
|
||||
emojiPath:
|
||||
(incomingTargetSeat?.emojiPath ?? "").isNotEmpty
|
||||
? incomingTargetSeat?.emojiPath
|
||||
: optimisticTargetSeat.emojiPath,
|
||||
type:
|
||||
(incomingTargetSeat?.type ?? "").isNotEmpty
|
||||
? incomingTargetSeat?.type
|
||||
: optimisticTargetSeat.type,
|
||||
number:
|
||||
(incomingTargetSeat?.number ?? "").isNotEmpty
|
||||
? incomingTargetSeat?.number
|
||||
: optimisticTargetSeat.number,
|
||||
);
|
||||
|
||||
return stabilizedMap;
|
||||
}
|
||||
|
||||
bool _shouldPreferDuplicateMicSeat({
|
||||
required String userId,
|
||||
required num existingIndex,
|
||||
required num candidateIndex,
|
||||
required Map<num, MicRes> previousMap,
|
||||
}) {
|
||||
if (existingIndex == candidateIndex) {
|
||||
return true;
|
||||
}
|
||||
final currentUserId =
|
||||
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
|
||||
if (userId == currentUserId && _preferredSelfMicIndex != null) {
|
||||
if (candidateIndex == _preferredSelfMicIndex) {
|
||||
return true;
|
||||
}
|
||||
if (existingIndex == _preferredSelfMicIndex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
final candidateWasPreviouslyOccupiedByUser =
|
||||
previousMap[candidateIndex]?.user?.id == userId;
|
||||
final existingWasPreviouslyOccupiedByUser =
|
||||
previousMap[existingIndex]?.user?.id == userId;
|
||||
if (candidateWasPreviouslyOccupiedByUser !=
|
||||
existingWasPreviouslyOccupiedByUser) {
|
||||
return candidateWasPreviouslyOccupiedByUser;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void _syncSelfMicRuntimeState() {
|
||||
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
||||
if ((currentUserId ?? "").isEmpty) {
|
||||
return;
|
||||
}
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
|
||||
MicRes? currentUserMic;
|
||||
for (final mic in roomWheatMap.values) {
|
||||
@ -262,25 +436,110 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
if (currentUserMic == null) {
|
||||
SCHeartbeatUtils.cancelAnchorTimer();
|
||||
engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||||
engine?.muteLocalAudioStream(true);
|
||||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||||
_syncRoomHeartbeatState(
|
||||
roomId: roomId,
|
||||
isOnMic: false,
|
||||
shouldRunAnchorHeartbeat: false,
|
||||
);
|
||||
_applyLocalAudioRuntimeState(
|
||||
clientRole: ClientRoleType.clientRoleAudience,
|
||||
muted: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
SCHeartbeatUtils.scheduleAnchorHeartbeat(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
_preferredSelfMicIndex = currentUserMic.micIndex;
|
||||
_syncRoomHeartbeatState(
|
||||
roomId: roomId,
|
||||
isOnMic: true,
|
||||
shouldRunAnchorHeartbeat: true,
|
||||
);
|
||||
|
||||
if ((currentUserMic.micMute ?? false) || isMic) {
|
||||
engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||||
engine?.muteLocalAudioStream(true);
|
||||
_applyLocalAudioRuntimeState(
|
||||
clientRole: ClientRoleType.clientRoleAudience,
|
||||
muted: true,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_applyLocalAudioRuntimeState(
|
||||
clientRole: ClientRoleType.clientRoleBroadcaster,
|
||||
muted: false,
|
||||
);
|
||||
}
|
||||
|
||||
void _syncRoomHeartbeatState({
|
||||
required String roomId,
|
||||
required bool isOnMic,
|
||||
required bool shouldRunAnchorHeartbeat,
|
||||
}) {
|
||||
final normalizedRoomId = roomId.trim();
|
||||
if (normalizedRoomId.isEmpty) {
|
||||
_resetHeartbeatTracking();
|
||||
return;
|
||||
}
|
||||
|
||||
final voiceLiveChanged =
|
||||
_lastScheduledVoiceLiveRoomId != normalizedRoomId ||
|
||||
_lastScheduledVoiceLiveOnMic != isOnMic;
|
||||
if (voiceLiveChanged) {
|
||||
SCHeartbeatUtils.scheduleHeartbeat(
|
||||
SCHeartbeatStatus.VOICE_LIVE.name,
|
||||
isOnMic,
|
||||
roomId: normalizedRoomId,
|
||||
);
|
||||
_lastScheduledVoiceLiveRoomId = normalizedRoomId;
|
||||
_lastScheduledVoiceLiveOnMic = isOnMic;
|
||||
}
|
||||
|
||||
if (!shouldRunAnchorHeartbeat) {
|
||||
if (_lastScheduledAnchorRoomId != null) {
|
||||
SCHeartbeatUtils.cancelAnchorTimer();
|
||||
_lastScheduledAnchorRoomId = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (voiceLiveChanged || _lastScheduledAnchorRoomId != normalizedRoomId) {
|
||||
SCHeartbeatUtils.scheduleAnchorHeartbeat(normalizedRoomId);
|
||||
_lastScheduledAnchorRoomId = normalizedRoomId;
|
||||
}
|
||||
}
|
||||
|
||||
void _applyLocalAudioRuntimeState({
|
||||
required ClientRoleType clientRole,
|
||||
required bool muted,
|
||||
}) {
|
||||
if (engine == null) {
|
||||
_lastAppliedClientRole = null;
|
||||
_lastAppliedLocalAudioMuted = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (_lastAppliedClientRole != clientRole) {
|
||||
engine?.setClientRole(role: clientRole);
|
||||
_lastAppliedClientRole = clientRole;
|
||||
}
|
||||
if (!muted && _lastAppliedLocalAudioMuted != false) {
|
||||
adjustRecordingSignalVolume(100);
|
||||
engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
|
||||
engine?.muteLocalAudioStream(false);
|
||||
}
|
||||
if (_lastAppliedLocalAudioMuted != muted) {
|
||||
engine?.muteLocalAudioStream(muted);
|
||||
_lastAppliedLocalAudioMuted = muted;
|
||||
}
|
||||
}
|
||||
|
||||
void _resetHeartbeatTracking() {
|
||||
_lastScheduledVoiceLiveOnMic = null;
|
||||
_lastScheduledVoiceLiveRoomId = null;
|
||||
_lastScheduledAnchorRoomId = null;
|
||||
}
|
||||
|
||||
void _resetLocalAudioRuntimeTracking() {
|
||||
_lastAppliedClientRole = null;
|
||||
_lastAppliedLocalAudioMuted = null;
|
||||
}
|
||||
|
||||
void _applyMicSnapshot(
|
||||
@ -295,6 +554,90 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
int? _tryParseAgoraUid(String? value) {
|
||||
final normalizedValue = (value ?? '').trim();
|
||||
if (normalizedValue.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return int.tryParse(normalizedValue);
|
||||
}
|
||||
|
||||
int _stableAgoraUidFromUser(SocialChatUserProfile? user) {
|
||||
final source =
|
||||
'${user?.account ?? ""}|${user?.id ?? ""}|${user?.userNickname ?? ""}';
|
||||
var hash = 0x811C9DC5;
|
||||
for (final codeUnit in source.codeUnits) {
|
||||
hash ^= codeUnit;
|
||||
hash = (hash * 0x01000193) & 0x7fffffff;
|
||||
}
|
||||
return hash == 0 ? 1 : hash;
|
||||
}
|
||||
|
||||
int _resolveAgoraUidForUser(SocialChatUserProfile? user) {
|
||||
final parsedAccountUid = _tryParseAgoraUid(user?.account);
|
||||
if (parsedAccountUid != null && parsedAccountUid > 0) {
|
||||
return parsedAccountUid;
|
||||
}
|
||||
final parsedUserIdUid = _tryParseAgoraUid(user?.id);
|
||||
if (parsedUserIdUid != null && parsedUserIdUid > 0) {
|
||||
return parsedUserIdUid;
|
||||
}
|
||||
return _stableAgoraUidFromUser(user);
|
||||
}
|
||||
|
||||
int _resolveAgoraUidForCurrentUser() {
|
||||
return _resolveAgoraUidForUser(
|
||||
AccountStorage().getCurrentUser()?.userProfile,
|
||||
);
|
||||
}
|
||||
|
||||
void requestGiftTriggeredMicRefresh() {
|
||||
requestMicrophoneListRefresh(
|
||||
notifyIfUnchanged: false,
|
||||
minInterval: _giftTriggeredMicRefreshMinInterval,
|
||||
);
|
||||
}
|
||||
|
||||
void requestMicrophoneListRefresh({
|
||||
bool notifyIfUnchanged = false,
|
||||
Duration minInterval = Duration.zero,
|
||||
}) {
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
if (roomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (minInterval <= Duration.zero) {
|
||||
_deferredMicListRefreshTimer?.cancel();
|
||||
_deferredMicListRefreshTimer = null;
|
||||
retrieveMicrophoneList(
|
||||
notifyIfUnchanged: notifyIfUnchanged,
|
||||
).catchError((_) {});
|
||||
return;
|
||||
}
|
||||
|
||||
final nowMs = DateTime.now().millisecondsSinceEpoch;
|
||||
final elapsedMs = nowMs - _lastMicListRefreshStartedAtMs;
|
||||
if (!_isRefreshingMicList && elapsedMs >= minInterval.inMilliseconds) {
|
||||
_deferredMicListRefreshTimer?.cancel();
|
||||
_deferredMicListRefreshTimer = null;
|
||||
retrieveMicrophoneList(
|
||||
notifyIfUnchanged: notifyIfUnchanged,
|
||||
).catchError((_) {});
|
||||
return;
|
||||
}
|
||||
|
||||
final remainingMs = minInterval.inMilliseconds - elapsedMs;
|
||||
final delayMs = remainingMs > 80 ? remainingMs : 80;
|
||||
_deferredMicListRefreshTimer?.cancel();
|
||||
_deferredMicListRefreshTimer = Timer(Duration(milliseconds: delayMs), () {
|
||||
_deferredMicListRefreshTimer = null;
|
||||
retrieveMicrophoneList(
|
||||
notifyIfUnchanged: notifyIfUnchanged,
|
||||
).catchError((_) {});
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> joinAgoraVoiceChannel() async {
|
||||
try {
|
||||
engine = await _initAgoraRtcEngine();
|
||||
@ -364,9 +707,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
await engine?.joinChannel(
|
||||
token: rtcToken.rtcToken ?? "",
|
||||
channelId: currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
uid: int.parse(
|
||||
AccountStorage().getCurrentUser()?.userProfile?.account ?? "0",
|
||||
),
|
||||
uid: _resolveAgoraUidForCurrentUser(),
|
||||
options: ChannelMediaOptions(
|
||||
// 自动订阅所有视频流
|
||||
autoSubscribeVideo: false,
|
||||
@ -381,6 +722,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
|
||||
),
|
||||
);
|
||||
_syncSelfMicRuntimeState();
|
||||
engine?.muteAllRemoteAudioStreams(roomIsMute);
|
||||
} catch (e) {
|
||||
SCTts.show("Join room fail");
|
||||
@ -411,34 +753,40 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
// }
|
||||
// print('初始化声音监听器:${user.toJson()}');
|
||||
|
||||
///声音全部清零
|
||||
roomWheatMap.forEach((k, v) {
|
||||
roomWheatMap[k]!.setVolume = 0;
|
||||
});
|
||||
for (var info in speakers) {
|
||||
num? uid = info.uid;
|
||||
int voloum = info.volume!;
|
||||
print('${info.uid}在说话---${voloum}');
|
||||
|
||||
if (voloum > 0) {
|
||||
///是本地声音
|
||||
if (uid == 0) {
|
||||
uid = num.parse(
|
||||
AccountStorage().getCurrentUser()!.userProfile!.account!,
|
||||
);
|
||||
if (roomWheatMap.isEmpty || speakers.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
///在哪个麦
|
||||
roomWheatMap.forEach((k, v) {
|
||||
if (v.user != null) {
|
||||
if (num.parse(v.user!.account!) == uid) {
|
||||
roomWheatMap[k]!.setVolume = voloum;
|
||||
for (var element in _onSoundVoiceChangeList) {
|
||||
element(k, voloum);
|
||||
}
|
||||
for (final seat in roomWheatMap.values) {
|
||||
seat.setVolume = 0;
|
||||
}
|
||||
|
||||
final seatIndexByAgoraUid = <int, num>{};
|
||||
roomWheatMap.forEach((seatIndex, mic) {
|
||||
final user = mic.user;
|
||||
if (user == null) {
|
||||
return;
|
||||
}
|
||||
seatIndexByAgoraUid[_resolveAgoraUidForUser(user)] = seatIndex;
|
||||
});
|
||||
|
||||
final currentUserAgoraUid = _resolveAgoraUidForCurrentUser();
|
||||
final listeners = List<OnSoundVoiceChange>.from(_onSoundVoiceChangeList);
|
||||
for (final info in speakers) {
|
||||
final volume = info.volume ?? 0;
|
||||
if (volume <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final resolvedAgoraUid = info.uid == 0 ? currentUserAgoraUid : info.uid;
|
||||
final seatIndex = seatIndexByAgoraUid[resolvedAgoraUid];
|
||||
if (seatIndex == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
roomWheatMap[seatIndex]?.setVolume = volume;
|
||||
for (final listener in listeners) {
|
||||
listener(seatIndex, volume);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -731,33 +1079,32 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
|
||||
Future<void> retrieveMicrophoneList({bool notifyIfUnchanged = true}) async {
|
||||
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||
if (roomId.isEmpty || _isRefreshingMicList) {
|
||||
if (roomId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (_isRefreshingMicList) {
|
||||
_pendingMicListRefresh = true;
|
||||
return;
|
||||
}
|
||||
_isRefreshingMicList = true;
|
||||
bool isOnMic = false;
|
||||
_lastMicListRefreshStartedAtMs = DateTime.now().millisecondsSinceEpoch;
|
||||
try {
|
||||
final roomWheatList = await SCChatRoomRepository().micList(roomId);
|
||||
if (roomId != currenRoom?.roomProfile?.roomProfile?.id) {
|
||||
return;
|
||||
}
|
||||
final nextMap = _buildMicMap(roomWheatList);
|
||||
for (final roomWheat in nextMap.values) {
|
||||
if (roomWheat.user != null &&
|
||||
roomWheat.user!.id ==
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id) {
|
||||
isOnMic = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
_applyMicSnapshot(nextMap, notifyIfUnchanged: notifyIfUnchanged);
|
||||
SCHeartbeatUtils.scheduleHeartbeat(
|
||||
SCHeartbeatStatus.VOICE_LIVE.name,
|
||||
isOnMic,
|
||||
roomId: currenRoom?.roomProfile?.roomProfile?.id,
|
||||
);
|
||||
} finally {
|
||||
_isRefreshingMicList = false;
|
||||
if (_pendingMicListRefresh) {
|
||||
_pendingMicListRefresh = false;
|
||||
unawaited(
|
||||
Future<void>.microtask(
|
||||
() => retrieveMicrophoneList(notifyIfUnchanged: notifyIfUnchanged),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -848,6 +1195,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
///清空列表数据
|
||||
void _clearData() {
|
||||
_stopRoomStatePolling();
|
||||
engine = null;
|
||||
_resetHeartbeatTracking();
|
||||
_resetLocalAudioRuntimeTracking();
|
||||
roomRocketStatus = null;
|
||||
rtmProvider
|
||||
?.onNewMessageListenerGroupMap["${currenRoom?.roomProfile?.roomProfile?.roomAccount}"] =
|
||||
@ -857,6 +1207,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
managerUsers.clear();
|
||||
needUpDataUserInfo = false;
|
||||
SCRoomUtils.roomUsersMap.clear();
|
||||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||||
roomIsMute = false;
|
||||
rtmProvider?.roomAllMsgList.clear();
|
||||
rtmProvider?.roomChatMsgList.clear();
|
||||
@ -979,6 +1330,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
retrieveMicrophoneList(notifyIfUnchanged: false).catchError((_) {});
|
||||
}
|
||||
|
||||
void _clearUserFromSeats(String? userId, {num? exceptIndex}) {
|
||||
final normalizedUserId = (userId ?? "").trim();
|
||||
if (normalizedUserId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
roomWheatMap.forEach((seatIndex, seat) {
|
||||
if (seat.user?.id != normalizedUserId || seatIndex == exceptIndex) {
|
||||
return;
|
||||
}
|
||||
roomWheatMap[seatIndex] = seat.copyWith(clearUser: true);
|
||||
});
|
||||
}
|
||||
|
||||
void _openRoomUserInfoCard(String? userId) {
|
||||
final normalizedUserId = (userId ?? '').trim();
|
||||
if (normalizedUserId.isEmpty) {
|
||||
@ -1035,18 +1399,44 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
SCHeartbeatUtils.scheduleAnchorHeartbeat(
|
||||
currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
);
|
||||
final currentSeat = roomWheatMap[index];
|
||||
if (currentSeat != null && myUser != null) {
|
||||
roomWheatMap[index] = currentSeat.copyWith(
|
||||
user: myUser.copyWith(roles: currenRoom?.entrants?.roles),
|
||||
final targetIndex = micGoUpRes.micIndex ?? index;
|
||||
final previousSelfSeatIndex =
|
||||
myUser != null ? userOnMaiInIndex(myUser.id ?? "") : -1;
|
||||
final isSeatSwitching =
|
||||
previousSelfSeatIndex > -1 && previousSelfSeatIndex != targetIndex;
|
||||
if (myUser != null) {
|
||||
if (isSeatSwitching) {
|
||||
_startSelfMicSwitchGuard(
|
||||
sourceIndex: previousSelfSeatIndex,
|
||||
targetIndex: targetIndex,
|
||||
);
|
||||
} else {
|
||||
_preferredSelfMicIndex = targetIndex;
|
||||
_clearSelfMicSwitchGuard();
|
||||
}
|
||||
_clearUserFromSeats(myUser.id, exceptIndex: targetIndex);
|
||||
final currentSeat = roomWheatMap[targetIndex];
|
||||
final currentUser = myUser.copyWith(roles: currenRoom?.entrants?.roles);
|
||||
roomWheatMap[targetIndex] =
|
||||
currentSeat?.copyWith(
|
||||
user: currentUser,
|
||||
micMute: micGoUpRes.micMute,
|
||||
micLock: micGoUpRes.micLock,
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
) ??
|
||||
MicRes(
|
||||
roomId: currenRoom?.roomProfile?.roomProfile?.id,
|
||||
micIndex: targetIndex,
|
||||
micLock: micGoUpRes.micLock,
|
||||
micMute: micGoUpRes.micMute,
|
||||
user: currentUser,
|
||||
roomToken: micGoUpRes.roomToken,
|
||||
);
|
||||
}
|
||||
if (roomWheatMap[index]?.micMute ?? false) {
|
||||
if (roomWheatMap[targetIndex]?.micMute ?? false) {
|
||||
///房主上麦自动解禁麦位
|
||||
if (isFz()) {
|
||||
await jieJinMai(index);
|
||||
await jieJinMai(targetIndex);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1069,15 +1459,13 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
isMic = true;
|
||||
engine?.muteLocalAudioStream(true);
|
||||
}
|
||||
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
|
||||
SCHeartbeatUtils.cancelAnchorTimer();
|
||||
|
||||
/// 设置成主持人角色
|
||||
engine?.renewToken("");
|
||||
engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
|
||||
final currentSeat = roomWheatMap[index];
|
||||
if (currentSeat != null) {
|
||||
roomWheatMap[index] = currentSeat.copyWith(user: null);
|
||||
}
|
||||
_clearUserFromSeats(AccountStorage().getCurrentUser()?.userProfile?.id);
|
||||
notifyListeners();
|
||||
_refreshMicListSilently();
|
||||
} catch (ex) {
|
||||
@ -1182,14 +1570,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
_applyMicSnapshot(_buildMicMap(mics));
|
||||
final isOnMic =
|
||||
userOnMaiInIndex(AccountStorage().getCurrentUser()?.userProfile?.id ?? "") >
|
||||
-1;
|
||||
SCHeartbeatUtils.scheduleHeartbeat(
|
||||
SCHeartbeatStatus.VOICE_LIVE.name,
|
||||
isOnMic,
|
||||
roomId: currenRoom?.roomProfile?.roomProfile?.id,
|
||||
);
|
||||
}
|
||||
|
||||
///找一个空麦位 -1表示没有空位
|
||||
|
||||
@ -48,7 +48,8 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_config_reposito
|
||||
import 'package:yumi/shared/data_sources/models/message/big_broadcast_group_message.dart';
|
||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_broad_cast_luck_gift_push.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/broad_cast_mic_change_push.dart'
|
||||
hide Data;
|
||||
import 'package:yumi/shared/business_logic/models/res/gift_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_public_message_page_res.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/sc_room_theme_list_res.dart';
|
||||
@ -72,6 +73,9 @@ typedef RtmProvider = RealTimeMessagingManager;
|
||||
class RealTimeMessagingManager extends ChangeNotifier {
|
||||
static const int _giftComboMergeWindowMs = 3000;
|
||||
static const int _maxLuckGiftPushQueueLength = 12;
|
||||
static const int _luckyGiftFloatMinMultiple = 5;
|
||||
static const int _luckyGiftBurstMinMultiple = 10;
|
||||
static const int _luckyGiftBurstMinAwardAmount = 5000;
|
||||
|
||||
BuildContext? context;
|
||||
|
||||
@ -119,6 +123,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
int notifcationUnReadCount = 0;
|
||||
SCBroadCastLuckGiftPush? currentPlayingLuckGift;
|
||||
final Queue<SCBroadCastLuckGiftPush?> _luckGiftPushQueue = Queue();
|
||||
String? _currentLuckGiftPushKey;
|
||||
Debouncer debouncer = Debouncer();
|
||||
List<V2TimConversation> conversationList = [];
|
||||
|
||||
@ -534,6 +539,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
Future<V2TimCallback> joinRoomGroup(String groupID, String message) async {
|
||||
_luckGiftPushQueue.clear();
|
||||
currentPlayingLuckGift = null;
|
||||
_currentLuckGiftPushKey = null;
|
||||
var joinResult = await TencentImSDKPlugin.v2TIMManager.joinGroup(
|
||||
groupID: groupID,
|
||||
message: message,
|
||||
@ -1025,7 +1031,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
required String source,
|
||||
}) {
|
||||
final rewardData = broadCastRes.data;
|
||||
if (rewardData == null || !rewardData.shouldShowGlobalNews) {
|
||||
if (rewardData == null) {
|
||||
return;
|
||||
}
|
||||
if (_isLuckyGiftInCurrentRoom(broadCastRes)) {
|
||||
addluckGiftPushQueue(broadCastRes);
|
||||
}
|
||||
if (!rewardData.shouldShowGlobalNews ||
|
||||
(rewardData.multiple ?? 0) < _luckyGiftFloatMinMultiple) {
|
||||
return;
|
||||
}
|
||||
if (source == 'broadcast' && _isLuckyGiftInCurrentRoom(broadCastRes)) {
|
||||
@ -1428,11 +1441,25 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
}
|
||||
} else if (msg.type == SCRoomMsgType.gift) {
|
||||
final gift = msg.gift;
|
||||
final special = gift?.special ?? "";
|
||||
final giftSourceUrl = gift?.giftSourceUrl ?? "";
|
||||
if (gift == null) {
|
||||
_giftFxLog(
|
||||
'recv gift msg skipped reason=no_gift '
|
||||
'fromUserId=${msg.user?.id} '
|
||||
'toUserId=${msg.toUser?.id} '
|
||||
'quantity=${msg.number}',
|
||||
);
|
||||
} else {
|
||||
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
);
|
||||
final special = gift.special ?? "";
|
||||
final giftSourceUrl = gift.giftSourceUrl ?? "";
|
||||
final hasSource = giftSourceUrl.isNotEmpty;
|
||||
final hasAnimation = scGiftHasAnimationSpecial(special);
|
||||
final hasGlobalGift = special.contains(SCGiftType.GLOBAL_GIFT.name);
|
||||
final hasGlobalGift = special.contains(
|
||||
SCGiftType.GLOBAL_GIFT.name,
|
||||
);
|
||||
final hasFullScreenEffect = scGiftHasFullScreenEffect(special);
|
||||
_giftFxLog(
|
||||
'recv gift msg '
|
||||
@ -1440,8 +1467,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
'fromUserName=${msg.user?.userNickname} '
|
||||
'toUserId=${msg.toUser?.id} '
|
||||
'toUserName=${msg.toUser?.userNickname} '
|
||||
'giftId=${gift?.id} '
|
||||
'giftName=${gift?.giftName} '
|
||||
'giftId=${gift.id} '
|
||||
'giftName=${gift.giftName} '
|
||||
'giftSourceUrl=$giftSourceUrl '
|
||||
'special=$special '
|
||||
'hasSource=$hasSource '
|
||||
@ -1450,57 +1477,52 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
'hasFullScreenEffect=$hasFullScreenEffect '
|
||||
'effectsEnabled=${SCGlobalConfig.isGiftSpecialEffects}',
|
||||
);
|
||||
if (msg.gift!.giftSourceUrl != null && msg.gift!.special != null) {
|
||||
if (scGiftHasFullScreenEffect(msg.gift!.special)) {
|
||||
if (giftSourceUrl.isNotEmpty && special.isNotEmpty) {
|
||||
if (scGiftHasFullScreenEffect(special)) {
|
||||
if (SCGlobalConfig.isGiftSpecialEffects &&
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).shouldShowRoomVisualEffects) {
|
||||
rtcProvider.shouldShowRoomVisualEffects) {
|
||||
_giftFxLog(
|
||||
'trigger player play path=${msg.gift!.giftSourceUrl} '
|
||||
'giftId=${msg.gift?.id} giftName=${msg.gift?.giftName}',
|
||||
'trigger player play path=$giftSourceUrl '
|
||||
'giftId=${gift.id} giftName=${gift.giftName}',
|
||||
);
|
||||
SCGiftVapSvgaManager().play(msg.gift!.giftSourceUrl!);
|
||||
SCGiftVapSvgaManager().play(giftSourceUrl);
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip player play because visual effects disabled '
|
||||
'giftId=${msg.gift?.id} '
|
||||
'giftId=${gift.id} '
|
||||
'isGiftSpecialEffects=${SCGlobalConfig.isGiftSpecialEffects} '
|
||||
'roomVisible=${Provider.of<RealTimeCommunicationManager>(context!, listen: false).shouldShowRoomVisualEffects}',
|
||||
'roomVisible=${rtcProvider.shouldShowRoomVisualEffects}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip player play because special does not include '
|
||||
'${SCGiftType.ANIMSCION.name}/$kSCGiftAnimationSpecialAlias/${SCGiftType.GLOBAL_GIFT.name} '
|
||||
'giftId=${msg.gift?.id} special=${msg.gift?.special}',
|
||||
'giftId=${gift.id} special=${gift.special}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
_giftFxLog(
|
||||
'skip player play because giftSourceUrl or special is null '
|
||||
'giftId=${msg.gift?.id} '
|
||||
'giftSourceUrl=${msg.gift?.giftSourceUrl} '
|
||||
'special=${msg.gift?.special}',
|
||||
'skip player play because giftSourceUrl or special is empty '
|
||||
'giftId=${gift.id} '
|
||||
'giftSourceUrl=${gift.giftSourceUrl} '
|
||||
'special=${gift.special}',
|
||||
);
|
||||
}
|
||||
if (Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).currenRoom?.roomProfile?.roomSetting?.showHeartbeat ??
|
||||
if (rtcProvider
|
||||
.currenRoom
|
||||
?.roomProfile
|
||||
?.roomSetting
|
||||
?.showHeartbeat ??
|
||||
false) {
|
||||
debouncer.debounce(
|
||||
duration: Duration(milliseconds: 350),
|
||||
onDebounce: () {
|
||||
Provider.of<RealTimeCommunicationManager>(
|
||||
context!,
|
||||
listen: false,
|
||||
).retrieveMicrophoneList();
|
||||
rtcProvider.requestGiftTriggeredMicRefresh();
|
||||
},
|
||||
);
|
||||
}
|
||||
num coins = msg.number! * msg.gift!.giftCandy!;
|
||||
final coins = (msg.number ?? 0) * (gift.giftCandy ?? 0);
|
||||
if (coins > 9999) {
|
||||
OverlayManager().addMessage(
|
||||
SCFloatingMessage(
|
||||
@ -1509,13 +1531,14 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
userName: msg.user?.userNickname ?? "",
|
||||
toUserName: msg.toUser?.userNickname ?? "",
|
||||
toUserAvatarUrl: msg.toUser?.userAvatar ?? "",
|
||||
giftUrl: msg.gift!.giftPhoto,
|
||||
giftUrl: gift.giftPhoto,
|
||||
number: msg.number,
|
||||
coins: coins,
|
||||
roomId: msg.msg,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (msg.type == SCRoomMsgType.luckGiftAnimOther) {
|
||||
final hideLGiftAnimal =
|
||||
Provider.of<GiftProvider>(
|
||||
@ -1686,6 +1709,7 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
roomChatMsgList.clear();
|
||||
_luckGiftPushQueue.clear();
|
||||
currentPlayingLuckGift = null;
|
||||
_currentLuckGiftPushKey = null;
|
||||
onNewMessageListenerGroupMap.forEach((k, v) {
|
||||
v = null;
|
||||
});
|
||||
@ -1694,6 +1718,20 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
|
||||
void addluckGiftPushQueue(SCBroadCastLuckGiftPush broadCastRes) {
|
||||
if (SCGlobalConfig.isLuckGiftSpecialEffects) {
|
||||
if (!shouldPlayLuckyGiftBurst(broadCastRes.data)) {
|
||||
return;
|
||||
}
|
||||
final eventKey = _luckyGiftPushEventKey(broadCastRes);
|
||||
if (eventKey.isNotEmpty) {
|
||||
if (_currentLuckGiftPushKey == eventKey) {
|
||||
return;
|
||||
}
|
||||
for (final queued in _luckGiftPushQueue) {
|
||||
if (queued != null && _luckyGiftPushEventKey(queued) == eventKey) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
while (_luckGiftPushQueue.length >= _maxLuckGiftPushQueueLength) {
|
||||
_luckGiftPushQueue.removeFirst();
|
||||
}
|
||||
@ -1702,8 +1740,19 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
static bool shouldPlayLuckyGiftBurst(Data? rewardData) {
|
||||
if (rewardData == null) {
|
||||
return false;
|
||||
}
|
||||
final awardAmount = rewardData.awardAmount ?? 0;
|
||||
final multiple = rewardData.multiple ?? 0;
|
||||
return awardAmount > _luckyGiftBurstMinAwardAmount ||
|
||||
multiple >= _luckyGiftBurstMinMultiple;
|
||||
}
|
||||
|
||||
void cleanLuckGiftBackCoins() {
|
||||
_luckGiftPushQueue.clear();
|
||||
_currentLuckGiftPushKey = null;
|
||||
}
|
||||
|
||||
void playLuckGiftBackCoins() {
|
||||
@ -1711,14 +1760,34 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
||||
return;
|
||||
}
|
||||
currentPlayingLuckGift = _luckGiftPushQueue.removeFirst();
|
||||
_currentLuckGiftPushKey =
|
||||
currentPlayingLuckGift == null
|
||||
? null
|
||||
: _luckyGiftPushEventKey(currentPlayingLuckGift!);
|
||||
notifyListeners();
|
||||
Future.delayed(Duration(milliseconds: 3000), () {
|
||||
currentPlayingLuckGift = null;
|
||||
_currentLuckGiftPushKey = null;
|
||||
notifyListeners();
|
||||
playLuckGiftBackCoins();
|
||||
});
|
||||
}
|
||||
|
||||
String _luckyGiftPushEventKey(SCBroadCastLuckGiftPush broadCastRes) {
|
||||
final rewardData = broadCastRes.data;
|
||||
if (rewardData == null) {
|
||||
return '';
|
||||
}
|
||||
return '${rewardData.roomId ?? ""}|'
|
||||
'${rewardData.giftId ?? ""}|'
|
||||
'${rewardData.sendUserId ?? ""}|'
|
||||
'${rewardData.acceptUserId ?? ""}|'
|
||||
'${rewardData.giftQuantity ?? 0}|'
|
||||
'${rewardData.awardAmount ?? 0}|'
|
||||
'${rewardData.multiple ?? 0}|'
|
||||
'${rewardData.normalizedMultipleType}';
|
||||
}
|
||||
|
||||
void updateNotificationCount(int count) {
|
||||
notifcationUnReadCount = 0;
|
||||
allUnReadCount =
|
||||
|
||||
@ -17,6 +17,8 @@ import '../../shared/data_sources/models/enum/sc_banner_type.dart';
|
||||
import '../../shared/data_sources/models/enum/sc_gift_type.dart';
|
||||
|
||||
class SCAppGeneralManager extends ChangeNotifier {
|
||||
static const int _maxInitialFullScreenGiftPreloads = 4;
|
||||
|
||||
List<CountryMode> countryModeList = [];
|
||||
final Map<String, List<Country>> _countryMap = {};
|
||||
final Map<String, Country> _countryByNameMap = {};
|
||||
@ -276,6 +278,8 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
}
|
||||
|
||||
void downLoad(List<SocialChatGiftRes> giftResList) {
|
||||
final scheduledPaths = <String>{};
|
||||
var scheduledCount = 0;
|
||||
for (var gift in giftResList) {
|
||||
final giftSourceUrl = gift.giftSourceUrl ?? "";
|
||||
if (giftSourceUrl.isEmpty) {
|
||||
@ -284,7 +288,14 @@ class SCAppGeneralManager extends ChangeNotifier {
|
||||
if (!scGiftHasFullScreenEffect(gift.special)) {
|
||||
continue;
|
||||
}
|
||||
if (!scheduledPaths.add(giftSourceUrl)) {
|
||||
continue;
|
||||
}
|
||||
SCGiftVapSvgaManager().preload(giftSourceUrl);
|
||||
scheduledCount += 1;
|
||||
if (scheduledCount >= _maxInitialFullScreenGiftPreloads) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -81,6 +81,7 @@ class GiftAnimationManager extends ChangeNotifier {
|
||||
if (target.giftName.isEmpty) {
|
||||
target.giftName = incoming.giftName;
|
||||
}
|
||||
target.rewardAmount = target.rewardAmount + incoming.rewardAmount;
|
||||
if (incoming.rewardAmountText.isNotEmpty) {
|
||||
target.rewardAmountText = incoming.rewardAmountText;
|
||||
}
|
||||
|
||||
@ -17,7 +17,7 @@ class MicRes {
|
||||
SocialChatUserProfile? user,
|
||||
String? type,
|
||||
String? roomToken,
|
||||
String?number
|
||||
String? number,
|
||||
}) {
|
||||
_roomId = roomId;
|
||||
_micIndex = micIndex;
|
||||
@ -35,7 +35,10 @@ class MicRes {
|
||||
_micIndex = json['micIndex'];
|
||||
_micLock = json['micLock'];
|
||||
_micMute = json['micMute'];
|
||||
_user = json['user'] != null ? SocialChatUserProfile.fromJson(json['user']) : null;
|
||||
_user =
|
||||
json['user'] != null
|
||||
? SocialChatUserProfile.fromJson(json['user'])
|
||||
: null;
|
||||
_roomToken = json['roomToken'];
|
||||
_type = json['type'];
|
||||
_number = json['number'];
|
||||
@ -59,6 +62,7 @@ class MicRes {
|
||||
bool? micMute,
|
||||
String? type,
|
||||
SocialChatUserProfile? user,
|
||||
bool clearUser = false,
|
||||
String? roomToken,
|
||||
String? emojiPath,
|
||||
String? number,
|
||||
@ -67,7 +71,7 @@ class MicRes {
|
||||
micIndex: micIndex ?? _micIndex,
|
||||
micLock: micLock ?? _micLock,
|
||||
micMute: micMute ?? _micMute,
|
||||
user: user ?? _user,
|
||||
user: clearUser ? null : user ?? _user,
|
||||
roomToken: roomToken ?? _roomToken,
|
||||
emojiPath: emojiPath ?? _emojiPath,
|
||||
type: type ?? _type,
|
||||
|
||||
@ -13,7 +13,6 @@ class SCFloatingMessage {
|
||||
num? number;
|
||||
num? multiple;
|
||||
int priority = 10; //排序权重
|
||||
int aggregateVersion = 0;
|
||||
|
||||
SCFloatingMessage({
|
||||
this.type = 0,
|
||||
@ -30,7 +29,6 @@ class SCFloatingMessage {
|
||||
this.coins = 0,
|
||||
this.priority = 10,
|
||||
this.multiple = 10,
|
||||
this.aggregateVersion = 0,
|
||||
});
|
||||
|
||||
SCFloatingMessage.fromJson(dynamic json) {
|
||||
@ -48,7 +46,6 @@ class SCFloatingMessage {
|
||||
number = json['number'];
|
||||
priority = json['priority'];
|
||||
multiple = json['multiple'];
|
||||
aggregateVersion = json['aggregateVersion'] ?? 0;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
@ -67,7 +64,6 @@ class SCFloatingMessage {
|
||||
map['number'] = number;
|
||||
map['priority'] = priority;
|
||||
map['multiple'] = multiple;
|
||||
map['aggregateVersion'] = aggregateVersion;
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
@ -20,7 +20,6 @@ class OverlayManager {
|
||||
);
|
||||
bool _isPlaying = false;
|
||||
OverlayEntry? _currentOverlayEntry;
|
||||
SCFloatingMessage? _currentMessage;
|
||||
|
||||
bool _isProcessing = false;
|
||||
bool _isDisposed = false;
|
||||
@ -34,9 +33,6 @@ class OverlayManager {
|
||||
void addMessage(SCFloatingMessage message) {
|
||||
if (_isDisposed) return;
|
||||
if (SCGlobalConfig.isFloatingAnimationInGlobal) {
|
||||
if (_tryAggregateMessage(message)) {
|
||||
return;
|
||||
}
|
||||
_messageQueue.add(message);
|
||||
_safeScheduleNext();
|
||||
} else {
|
||||
@ -47,74 +43,6 @@ class OverlayManager {
|
||||
}
|
||||
}
|
||||
|
||||
bool _tryAggregateMessage(SCFloatingMessage incoming) {
|
||||
if (!_supportsAggregation(incoming)) {
|
||||
return false;
|
||||
}
|
||||
if (_currentMessage != null &&
|
||||
_isSameAggregatedFloatingMessage(_currentMessage!, incoming)) {
|
||||
_mergeFloatingMessage(_currentMessage!, incoming);
|
||||
_currentOverlayEntry?.markNeedsBuild();
|
||||
return true;
|
||||
}
|
||||
for (final queuedMessage in _messageQueue.unorderedElements) {
|
||||
if (_isSameAggregatedFloatingMessage(queuedMessage, incoming)) {
|
||||
_mergeFloatingMessage(queuedMessage, incoming);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool _supportsAggregation(SCFloatingMessage message) {
|
||||
return message.type == 0;
|
||||
}
|
||||
|
||||
bool _isSameAggregatedFloatingMessage(
|
||||
SCFloatingMessage existing,
|
||||
SCFloatingMessage incoming,
|
||||
) {
|
||||
return existing.type == incoming.type &&
|
||||
existing.roomId == incoming.roomId &&
|
||||
existing.userId == incoming.userId &&
|
||||
existing.toUserId == incoming.toUserId &&
|
||||
existing.giftUrl == incoming.giftUrl;
|
||||
}
|
||||
|
||||
void _mergeFloatingMessage(
|
||||
SCFloatingMessage target,
|
||||
SCFloatingMessage incoming,
|
||||
) {
|
||||
target.coins = (target.coins ?? 0) + (incoming.coins ?? 0);
|
||||
target.number = (target.number ?? 0) + (incoming.number ?? 0);
|
||||
final currentMultiple = target.multiple ?? 0;
|
||||
final incomingMultiple = incoming.multiple ?? 0;
|
||||
target.multiple =
|
||||
currentMultiple >= incomingMultiple
|
||||
? currentMultiple
|
||||
: incomingMultiple;
|
||||
if ((target.userAvatarUrl ?? '').isEmpty) {
|
||||
target.userAvatarUrl = incoming.userAvatarUrl;
|
||||
}
|
||||
if ((target.userName ?? '').isEmpty) {
|
||||
target.userName = incoming.userName;
|
||||
}
|
||||
if ((target.toUserName ?? '').isEmpty) {
|
||||
target.toUserName = incoming.toUserName;
|
||||
}
|
||||
if ((target.giftUrl ?? '').isEmpty) {
|
||||
target.giftUrl = incoming.giftUrl;
|
||||
}
|
||||
if ((target.roomId ?? '').isEmpty) {
|
||||
target.roomId = incoming.roomId;
|
||||
}
|
||||
target.priority =
|
||||
target.priority >= incoming.priority
|
||||
? target.priority
|
||||
: incoming.priority;
|
||||
target.aggregateVersion += 1;
|
||||
}
|
||||
|
||||
void _safeScheduleNext() {
|
||||
if (_isProcessing || _isPlaying || _messageQueue.isEmpty) return;
|
||||
_isProcessing = true;
|
||||
@ -162,11 +90,9 @@ class OverlayManager {
|
||||
|
||||
void _playMessage(SCFloatingMessage message) {
|
||||
_isPlaying = true;
|
||||
_currentMessage = message;
|
||||
final context = navigatorKey.currentState?.context;
|
||||
if (context == null || !context.mounted) {
|
||||
_isPlaying = false;
|
||||
_currentMessage = null;
|
||||
_safeScheduleNext();
|
||||
return;
|
||||
}
|
||||
@ -195,12 +121,10 @@ class OverlayManager {
|
||||
try {
|
||||
_currentOverlayEntry?.remove();
|
||||
_currentOverlayEntry = null;
|
||||
_currentMessage = null;
|
||||
_isPlaying = false;
|
||||
_safeScheduleNext();
|
||||
} catch (e) {
|
||||
debugPrint('清理悬浮消息出错: $e');
|
||||
_currentMessage = null;
|
||||
_isPlaying = false;
|
||||
_safeScheduleNext();
|
||||
}
|
||||
@ -209,9 +133,6 @@ class OverlayManager {
|
||||
switch (message.type) {
|
||||
case 0:
|
||||
return FloatingLuckGiftScreenWidget(
|
||||
key: ValueKey<String>(
|
||||
'luck_${message.userId}_${message.toUserId}_${message.giftUrl}_${message.aggregateVersion}',
|
||||
),
|
||||
message: message,
|
||||
onAnimationCompleted: onComplete,
|
||||
);
|
||||
@ -253,7 +174,6 @@ class OverlayManager {
|
||||
_isDisposed = true;
|
||||
_currentOverlayEntry?.remove();
|
||||
_currentOverlayEntry = null;
|
||||
_currentMessage = null;
|
||||
_messageQueue.clear();
|
||||
_isPlaying = false;
|
||||
_isProcessing = false;
|
||||
|
||||
@ -324,9 +324,12 @@ class _DailySignInDialogState extends State<DailySignInDialog> {
|
||||
'dayIndex=${checkInResult.dayIndex} '
|
||||
'checkedToday=${latestData.checkedToday}',
|
||||
);
|
||||
SCTts.show(
|
||||
checkInResult.alreadySigned ? l10n.signedin : l10n.receiveSucc,
|
||||
);
|
||||
final toastMessage =
|
||||
checkInResult.alreadySigned
|
||||
? l10n.signedin
|
||||
: _rewardToastMessage(l10n, checkInResult.rewardItems);
|
||||
debugPrint('[SignInReward][Dialog] reward toast=$toastMessage');
|
||||
SCTts.show(toastMessage);
|
||||
SmartDialog.dismiss(tag: DailySignInDialog.dialogTag);
|
||||
} catch (error) {
|
||||
SCLoadingManager.hide();
|
||||
@ -365,6 +368,66 @@ class _DailySignInDialogState extends State<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) {
|
||||
return items
|
||||
.map(
|
||||
|
||||
@ -2,7 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||
import 'package:yumi/app_localizations.dart';
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/main.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:yumi/services/gift/gift_animation_manager.dart';
|
||||
@ -22,6 +21,7 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
with TickerProviderStateMixin {
|
||||
static const String _luckyGiftRewardFrameAssetPath =
|
||||
"sc_images/room/anim/luck_gift/luck_gift_reward_frame.svga";
|
||||
late final GiftAnimationManager _giftAnimationManager;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -55,8 +55,12 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
animation: bean.controller,
|
||||
builder: (context, child) {
|
||||
final showLuckyRewardFrame = gift.showLuckyRewardFrame;
|
||||
final tickerMargin =
|
||||
showLuckyRewardFrame
|
||||
? bean.luckyGiftPinnedMargin
|
||||
: bean.verticalAnimation.value;
|
||||
return Container(
|
||||
margin: bean.verticalAnimation.value,
|
||||
margin: tickerMargin,
|
||||
width:
|
||||
ScreenUtil().screenWidth * (showLuckyRewardFrame ? 0.96 : 0.78),
|
||||
child: _buildGiftTickerCard(context, gift, bean.sizeAnimation.value),
|
||||
@ -250,7 +254,7 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
"+${gift.rewardAmountText}",
|
||||
"+${_giftRewardAmountText(gift)}",
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 16.sp,
|
||||
@ -301,18 +305,33 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
return count % 1 == 0 ? count.toInt().toString() : count.toString();
|
||||
}
|
||||
|
||||
String _giftRewardAmountText(LGiftModel gift) {
|
||||
final rewardAmount = gift.rewardAmount;
|
||||
if (rewardAmount > 0) {
|
||||
if (rewardAmount > 9999) {
|
||||
return "${(rewardAmount / 1000).toStringAsFixed(0)}k";
|
||||
}
|
||||
if (rewardAmount % 1 == 0) {
|
||||
return rewardAmount.toInt().toString();
|
||||
}
|
||||
return rewardAmount.toString();
|
||||
}
|
||||
return gift.rewardAmountText;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
Provider.of<GiftAnimationManager>(
|
||||
navigatorKey.currentState!.context,
|
||||
listen: false,
|
||||
).cleanupAnimationResources();
|
||||
_giftAnimationManager.cleanupAnimationResources();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_giftAnimationManager = Provider.of<GiftAnimationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
initAnimal();
|
||||
}
|
||||
|
||||
@ -327,6 +346,7 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
vsync: this,
|
||||
);
|
||||
bean.controller = controller;
|
||||
bean.luckyGiftPinnedMargin = EdgeInsets.only(top: top);
|
||||
// bean.transverseAnimation = Tween<Offset>(
|
||||
// begin: Offset(ScreenUtil().screenWidth, 0),
|
||||
// end: Offset(0, 0),
|
||||
@ -357,43 +377,28 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
beans[0].controller.addStatusListener((state) {
|
||||
if (state == AnimationStatus.completed) {
|
||||
//动画完成监听
|
||||
Provider.of<GiftAnimationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).markAnimationAsFinished(0);
|
||||
_giftAnimationManager.markAnimationAsFinished(0);
|
||||
}
|
||||
});
|
||||
beans[1].controller.addStatusListener((state) {
|
||||
if (state == AnimationStatus.completed) {
|
||||
//动画完成监听
|
||||
Provider.of<GiftAnimationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).markAnimationAsFinished(1);
|
||||
_giftAnimationManager.markAnimationAsFinished(1);
|
||||
}
|
||||
});
|
||||
beans[2].controller.addStatusListener((state) {
|
||||
if (state == AnimationStatus.completed) {
|
||||
//动画完成监听
|
||||
Provider.of<GiftAnimationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).markAnimationAsFinished(2);
|
||||
_giftAnimationManager.markAnimationAsFinished(2);
|
||||
}
|
||||
});
|
||||
beans[3].controller.addStatusListener((state) {
|
||||
if (state == AnimationStatus.completed) {
|
||||
//动画完成监听
|
||||
Provider.of<GiftAnimationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).markAnimationAsFinished(3);
|
||||
_giftAnimationManager.markAnimationAsFinished(3);
|
||||
}
|
||||
});
|
||||
Provider.of<GiftAnimationManager>(
|
||||
context,
|
||||
listen: false,
|
||||
).attachAnimationControllers(beans);
|
||||
_giftAnimationManager.attachAnimationControllers(beans);
|
||||
}
|
||||
|
||||
String getBg(String type) {
|
||||
@ -404,6 +409,7 @@ class _GiftAnimalPageState extends State<LGiftAnimalPage>
|
||||
class LGiftScrollingScreenAnimsBean {
|
||||
// late Animation<Offset> transverseAnimation;
|
||||
late Animation<EdgeInsets> verticalAnimation;
|
||||
late EdgeInsets luckyGiftPinnedMargin;
|
||||
late AnimationController controller;
|
||||
late Animation<double> sizeAnimation;
|
||||
}
|
||||
@ -430,6 +436,9 @@ class LGiftModel {
|
||||
//幸运礼物奖励条右侧的金币数
|
||||
String rewardAmountText = "";
|
||||
|
||||
//幸运礼物奖励条右侧的金币累计值
|
||||
num rewardAmount = 0;
|
||||
|
||||
//是否显示幸运礼物奖励框
|
||||
bool showLuckyRewardFrame = false;
|
||||
|
||||
|
||||
@ -6,8 +6,6 @@ import 'package:yumi/services/audio/rtm_manager.dart';
|
||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||
|
||||
class LuckGiftNomorAnimWidget extends StatefulWidget {
|
||||
static const num _rewardBurstMinAwardAmount = 5000;
|
||||
static const num _rewardBurstMinMultiple = 10;
|
||||
static const String _rewardBurstAssetPath =
|
||||
"sc_images/room/anim/luck_gift/luck_gift_reward_burst.svga";
|
||||
|
||||
@ -19,11 +17,11 @@ class LuckGiftNomorAnimWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
||||
String? _currentBurstEventId;
|
||||
bool _isRewardAmountVisible = false;
|
||||
|
||||
bool _shouldPlayRewardBurst(Data rewardData) {
|
||||
final awardAmount = rewardData.awardAmount ?? 0;
|
||||
final multiple = rewardData.multiple ?? 0;
|
||||
return awardAmount > LuckGiftNomorAnimWidget._rewardBurstMinAwardAmount ||
|
||||
multiple > LuckGiftNomorAnimWidget._rewardBurstMinMultiple;
|
||||
return RealTimeMessagingManager.shouldPlayLuckyGiftBurst(rewardData);
|
||||
}
|
||||
|
||||
String _formatAwardAmount(num awardAmount) {
|
||||
@ -43,6 +41,8 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
||||
builder: (context, provider, child) {
|
||||
final rewardData = provider.currentPlayingLuckGift?.data;
|
||||
if (rewardData == null || !_shouldPlayRewardBurst(rewardData)) {
|
||||
_currentBurstEventId = null;
|
||||
_isRewardAmountVisible = false;
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final rewardAnimationKey = ValueKey<String>(
|
||||
@ -52,6 +52,10 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
||||
'|${rewardData.multiple ?? 0}'
|
||||
'|${rewardData.normalizedMultipleType}',
|
||||
);
|
||||
if (_currentBurstEventId != rewardAnimationKey.value) {
|
||||
_currentBurstEventId = rewardAnimationKey.value;
|
||||
_isRewardAmountVisible = false;
|
||||
}
|
||||
return SizedBox(
|
||||
height: 380.w,
|
||||
child: Stack(
|
||||
@ -66,18 +70,45 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
||||
height: 380.w,
|
||||
fit: BoxFit.fitWidth,
|
||||
allowDrawingOverflow: true,
|
||||
clearsAfterStop: true,
|
||||
onPlaybackStarted: () {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (!_isRewardAmountVisible) {
|
||||
setState(() {
|
||||
_isRewardAmountVisible = true;
|
||||
});
|
||||
}
|
||||
},
|
||||
onPlaybackCompleted: () {
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
if (_isRewardAmountVisible) {
|
||||
setState(() {
|
||||
_isRewardAmountVisible = false;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 154.w,
|
||||
if (_isRewardAmountVisible)
|
||||
Positioned.fill(
|
||||
child: Align(
|
||||
alignment: const Alignment(0, 0.12),
|
||||
child: Transform.translate(
|
||||
offset: Offset(0, 0),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: ScreenUtil().screenWidth * 0.56,
|
||||
),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 6.w, right: 2.w),
|
||||
child: Text(
|
||||
"+${_formatAwardAmount(rewardData.awardAmount ?? 0)}",
|
||||
_formatAwardAmount(rewardData.awardAmount ?? 0),
|
||||
maxLines: 1,
|
||||
style: TextStyle(
|
||||
fontSize: 28.sp,
|
||||
@ -97,6 +128,9 @@ class _LuckGiftNomorAnimWidgetState extends State<LuckGiftNomorAnimWidget> {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@ -50,17 +50,20 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: _floatingButtonHostHeight.w,
|
||||
child: Consumer<RtcProvider>(
|
||||
builder: (context, rtcProvider, child) {
|
||||
final showMic = _shouldShowMic(rtcProvider);
|
||||
|
||||
child: Selector<RtcProvider, _RoomBottomSnapshot>(
|
||||
selector:
|
||||
(context, provider) => _RoomBottomSnapshot(
|
||||
showMic: _shouldShowMic(provider),
|
||||
isMic: provider.isMic,
|
||||
),
|
||||
builder: (context, bottomSnapshot, child) {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final inputWidth = constraints.maxWidth / 3;
|
||||
final giftCenterX = _resolveGiftCenterX(
|
||||
maxWidth: constraints.maxWidth,
|
||||
inputWidth: inputWidth,
|
||||
showMic: showMic,
|
||||
showMic: bottomSnapshot.showMic,
|
||||
);
|
||||
|
||||
return Stack(
|
||||
@ -78,9 +81,9 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
child: SizedBox(
|
||||
height: _bottomBarHeight.w,
|
||||
child: _buildBottomBar(
|
||||
rtcProvider: rtcProvider,
|
||||
inputWidth: inputWidth,
|
||||
showMic: showMic,
|
||||
showMic: bottomSnapshot.showMic,
|
||||
isMic: bottomSnapshot.isMic,
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -105,9 +108,9 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
}
|
||||
|
||||
Widget _buildBottomBar({
|
||||
required RtcProvider rtcProvider,
|
||||
required double inputWidth,
|
||||
required bool showMic,
|
||||
required bool isMic,
|
||||
}) {
|
||||
final giftAction = _buildGiftAction();
|
||||
final messageAction = _buildMessageAction();
|
||||
@ -126,7 +129,7 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
children: [
|
||||
_buildChatEntry(inputWidth),
|
||||
giftAction,
|
||||
_buildMicAction(rtcProvider),
|
||||
_buildMicAction(isMic: isMic),
|
||||
menuAction,
|
||||
messageAction,
|
||||
],
|
||||
@ -265,10 +268,11 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMicAction(RtcProvider provider) {
|
||||
Widget _buildMicAction({required bool isMic}) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
final provider = context.read<RtcProvider>();
|
||||
setState(() {
|
||||
provider.isMic = !provider.isMic;
|
||||
|
||||
@ -298,7 +302,7 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
},
|
||||
child: RoomBottomCircleAction(
|
||||
child: Image.asset(
|
||||
"sc_images/room/${provider.isMic ? 'sc_icon_botton_mic_close' : 'sc_icon_botton_mic_open'}.png",
|
||||
"sc_images/room/${isMic ? 'sc_icon_botton_mic_close' : 'sc_icon_botton_mic_open'}.png",
|
||||
width: 30.w,
|
||||
height: 30.w,
|
||||
fit: BoxFit.contain,
|
||||
@ -320,3 +324,23 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -23,8 +23,22 @@ class RoomHeadWidget extends StatefulWidget {
|
||||
class _RoomHeadWidgetState extends State<RoomHeadWidget> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<RtcProvider>(
|
||||
builder: (context, provider, child) {
|
||||
return Selector<RtcProvider, _RoomHeadSnapshot>(
|
||||
selector: (context, provider) {
|
||||
final room = provider.currenRoom;
|
||||
final roomOwnerUserId = room?.roomProfile?.roomProfile?.userId ?? "";
|
||||
final currentUserId =
|
||||
AccountStorage().getCurrentUser()?.userProfile?.id ?? "";
|
||||
return _RoomHeadSnapshot(
|
||||
roomProfileId: room?.roomProfile?.roomProfile?.id,
|
||||
roomCover: room?.roomProfile?.roomProfile?.roomCover,
|
||||
roomName: room?.roomProfile?.roomProfile?.roomName ?? "",
|
||||
roomDisplayId: room?.roomProfile?.userProfile?.getID() ?? "",
|
||||
roomOwnerUserId: roomOwnerUserId,
|
||||
isRoomOwner: roomOwnerUserId == currentUserId,
|
||||
);
|
||||
},
|
||||
builder: (context, roomSnapshot, child) {
|
||||
return Row(
|
||||
children: [
|
||||
Row(
|
||||
@ -32,8 +46,8 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
showBottomInBottomDialog(
|
||||
context!,
|
||||
RoomDetailPage(provider.isFz()),
|
||||
context,
|
||||
RoomDetailPage(context.read<RtcProvider>().isFz()),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
@ -48,16 +62,8 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
|
||||
children: [
|
||||
netImage(
|
||||
url: resolveRoomCoverUrl(
|
||||
provider
|
||||
.currenRoom
|
||||
?.roomProfile
|
||||
?.roomProfile
|
||||
?.id,
|
||||
provider
|
||||
.currenRoom
|
||||
?.roomProfile
|
||||
?.roomProfile
|
||||
?.roomCover,
|
||||
roomSnapshot.roomProfileId,
|
||||
roomSnapshot.roomCover,
|
||||
),
|
||||
defaultImg: kRoomCoverDefaultImg,
|
||||
width: 28.w,
|
||||
@ -99,22 +105,9 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
|
||||
maxHeight: 17.w,
|
||||
),
|
||||
child:
|
||||
(provider
|
||||
.currenRoom
|
||||
?.roomProfile
|
||||
?.roomProfile
|
||||
?.roomName
|
||||
?.length ??
|
||||
0) >
|
||||
10
|
||||
roomSnapshot.roomName.length > 10
|
||||
? Marquee(
|
||||
text:
|
||||
provider
|
||||
.currenRoom
|
||||
?.roomProfile
|
||||
?.roomProfile
|
||||
?.roomName ??
|
||||
"",
|
||||
text: roomSnapshot.roomName,
|
||||
style: TextStyle(
|
||||
fontSize: 14.sp,
|
||||
color: Color(0xffffffff),
|
||||
@ -137,12 +130,7 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
|
||||
decelerationCurve: Curves.easeOut,
|
||||
)
|
||||
: Text(
|
||||
provider
|
||||
.currenRoom
|
||||
?.roomProfile
|
||||
?.roomProfile
|
||||
?.roomName ??
|
||||
'',
|
||||
roomSnapshot.roomName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
@ -155,7 +143,7 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
|
||||
),
|
||||
|
||||
text(
|
||||
"ID:${provider.currenRoom?.roomProfile?.userProfile?.getID()}",
|
||||
"ID:${roomSnapshot.roomDisplayId}",
|
||||
fontSize: 13.sp,
|
||||
fontWeight: FontWeight.w600,
|
||||
textColor: Colors.white70,
|
||||
@ -163,11 +151,7 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
|
||||
],
|
||||
),
|
||||
SizedBox(width: 6.w),
|
||||
provider.currenRoom?.roomProfile?.roomProfile?.userId !=
|
||||
AccountStorage()
|
||||
.getCurrentUser()
|
||||
?.userProfile
|
||||
?.id
|
||||
!roomSnapshot.isRoomOwner
|
||||
? Selector<RtcProvider, bool>(
|
||||
selector:
|
||||
(c, p) =>
|
||||
@ -182,7 +166,9 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
|
||||
height: 22.w,
|
||||
),
|
||||
onTap: () {
|
||||
provider.followCurrentVoiceRoom();
|
||||
context
|
||||
.read<RtcProvider>()
|
||||
.followCurrentVoiceRoom();
|
||||
},
|
||||
)
|
||||
: Container();
|
||||
@ -203,7 +189,7 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
|
||||
height: 32.w,
|
||||
),
|
||||
onTap: () {
|
||||
if (provider.isFz()) {
|
||||
if (context.read<RtcProvider>().isFz()) {
|
||||
SCNavigatorUtils.push(
|
||||
context,
|
||||
"${VoiceRoomRoute.roomEdit}?need=false",
|
||||
@ -234,8 +220,8 @@ class _RoomHeadWidgetState extends State<RoomHeadWidget> {
|
||||
showCenterDialog(
|
||||
context,
|
||||
ExitMinRoomPage(
|
||||
provider.isFz(),
|
||||
provider.currenRoom?.roomProfile?.roomProfile?.id ?? "",
|
||||
context.read<RtcProvider>().isFz(),
|
||||
roomSnapshot.roomProfileId ?? "",
|
||||
),
|
||||
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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import 'package:yumi/app/constants/sc_global_config.dart';
|
||||
|
||||
import 'package:yumi/ui_kit/components/sc_compontent.dart';
|
||||
import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
|
||||
import 'package:yumi/services/room/rc_room_manager.dart';
|
||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||
@ -29,17 +30,32 @@ class _RoomOnlineUserWidgetState extends State<RoomOnlineUserWidget> {
|
||||
double get _onlineUsersShellWidth =>
|
||||
_onlineUsersAvatarsWidth + _onlineUsersCounterWidth;
|
||||
|
||||
void _openRoomOnlinePage(RtcProvider ref) {
|
||||
void _openRoomOnlinePage() {
|
||||
showBottomInBottomDialog(
|
||||
context,
|
||||
RoomOnlinePage(roomId: ref.currenRoom?.roomProfile?.roomProfile?.id),
|
||||
RoomOnlinePage(
|
||||
roomId:
|
||||
context
|
||||
.read<RtcProvider>()
|
||||
.currenRoom
|
||||
?.roomProfile
|
||||
?.roomProfile
|
||||
?.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<RtcProvider>(
|
||||
builder: (context, ref, child) {
|
||||
return Selector<RtcProvider, _RoomOnlineUsersSnapshot>(
|
||||
selector:
|
||||
(context, provider) => _RoomOnlineUsersSnapshot(
|
||||
onlineUsers: List<SocialChatUserProfile>.unmodifiable(
|
||||
provider.onlineUsers,
|
||||
),
|
||||
),
|
||||
builder: (context, onlineSnapshot, child) {
|
||||
final onlineUsers = onlineSnapshot.onlineUsers;
|
||||
return Row(
|
||||
children: [
|
||||
_buildExperience(),
|
||||
@ -48,8 +64,8 @@ class _RoomOnlineUserWidgetState extends State<RoomOnlineUserWidget> {
|
||||
child: Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child:
|
||||
ref.onlineUsers.isNotEmpty
|
||||
? _buildOnlineUsers(ref)
|
||||
onlineUsers.isNotEmpty
|
||||
? _buildOnlineUsers(onlineUsers)
|
||||
: _buildOnlineUsersPlaceholder(),
|
||||
),
|
||||
),
|
||||
@ -59,12 +75,12 @@ class _RoomOnlineUserWidgetState extends State<RoomOnlineUserWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOnlineUsers(RtcProvider ref) {
|
||||
Widget _buildOnlineUsers(List<SocialChatUserProfile> onlineUsers) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(right: 5.w),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => _openRoomOnlinePage(ref),
|
||||
onTap: _openRoomOnlinePage,
|
||||
child: SizedBox(
|
||||
width: _onlineUsersShellWidth,
|
||||
height: _onlineUsersShellHeight,
|
||||
@ -80,13 +96,13 @@ class _RoomOnlineUserWidgetState extends State<RoomOnlineUserWidget> {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: List.generate(ref.onlineUsers.length, (index) {
|
||||
children: List.generate(onlineUsers.length, (index) {
|
||||
return Transform.translate(
|
||||
offset: Offset(-3.w * index, 0),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(right: 0.w),
|
||||
child: netImage(
|
||||
url: ref.onlineUsers[index].userAvatar ?? "",
|
||||
url: onlineUsers[index].userAvatar ?? "",
|
||||
width: 23.w,
|
||||
height: 23.w,
|
||||
defaultImg:
|
||||
@ -117,11 +133,7 @@ class _RoomOnlineUserWidgetState extends State<RoomOnlineUserWidget> {
|
||||
width: 12.w,
|
||||
height: 12.sp,
|
||||
),
|
||||
text(
|
||||
"${ref.onlineUsers.length}",
|
||||
fontSize: 9,
|
||||
lineHeight: 1,
|
||||
),
|
||||
text("${onlineUsers.length}", fontSize: 9, lineHeight: 1),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -204,3 +216,37 @@ class _RoomOnlineUserWidgetState extends State<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)),
|
||||
);
|
||||
}
|
||||
|
||||
@ -13,12 +13,12 @@ class RoomSeatWidget extends StatefulWidget {
|
||||
class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
||||
int _lastSeatCount = 0;
|
||||
|
||||
int _resolvedSeatCount(RtcProvider ref) {
|
||||
final int seatCount = ref.roomWheatMap.length;
|
||||
if (!ref.isExitingCurrentVoiceRoomSession && seatCount > 0) {
|
||||
int _resolvedSeatCount(_RoomSeatLayoutSnapshot snapshot) {
|
||||
final int seatCount = snapshot.seatCount;
|
||||
if (!snapshot.isExitingCurrentVoiceRoomSession && seatCount > 0) {
|
||||
_lastSeatCount = seatCount;
|
||||
}
|
||||
if (ref.isExitingCurrentVoiceRoomSession && _lastSeatCount > 0) {
|
||||
if (snapshot.isExitingCurrentVoiceRoomSession && _lastSeatCount > 0) {
|
||||
return _lastSeatCount;
|
||||
}
|
||||
return seatCount;
|
||||
@ -26,9 +26,15 @@ class _RoomSeatWidgetState extends State<RoomSeatWidget> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<RtcProvider>(
|
||||
builder: (context, ref, child) {
|
||||
final int seatCount = _resolvedSeatCount(ref);
|
||||
return Selector<RtcProvider, _RoomSeatLayoutSnapshot>(
|
||||
selector:
|
||||
(context, provider) => _RoomSeatLayoutSnapshot(
|
||||
seatCount: provider.roomWheatMap.length,
|
||||
isExitingCurrentVoiceRoomSession:
|
||||
provider.isExitingCurrentVoiceRoomSession,
|
||||
),
|
||||
builder: (context, snapshot, child) {
|
||||
final int seatCount = _resolvedSeatCount(snapshot);
|
||||
return seatCount == 5
|
||||
? _buildSeat5()
|
||||
: (seatCount == 10
|
||||
@ -177,3 +183,27 @@ class _RoomSeatWidgetState extends State<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);
|
||||
}
|
||||
|
||||
@ -14,6 +14,9 @@ class SCSvgaAssetWidget extends StatefulWidget {
|
||||
this.filterQuality = FilterQuality.low,
|
||||
this.allowDrawingOverflow = false,
|
||||
this.fallback,
|
||||
this.clearsAfterStop = false,
|
||||
this.onPlaybackStarted,
|
||||
this.onPlaybackCompleted,
|
||||
});
|
||||
|
||||
final String assetPath;
|
||||
@ -25,6 +28,9 @@ class SCSvgaAssetWidget extends StatefulWidget {
|
||||
final FilterQuality filterQuality;
|
||||
final bool allowDrawingOverflow;
|
||||
final Widget? fallback;
|
||||
final bool clearsAfterStop;
|
||||
final VoidCallback? onPlaybackStarted;
|
||||
final VoidCallback? onPlaybackCompleted;
|
||||
|
||||
@override
|
||||
State<SCSvgaAssetWidget> createState() => _SCSvgaAssetWidgetState();
|
||||
@ -44,9 +50,20 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = SVGAAnimationController(vsync: this);
|
||||
_controller.addStatusListener(_handleAnimationStatusChanged);
|
||||
_loadAsset();
|
||||
}
|
||||
|
||||
void _handleAnimationStatusChanged(AnimationStatus status) {
|
||||
if (status == AnimationStatus.forward) {
|
||||
widget.onPlaybackStarted?.call();
|
||||
return;
|
||||
}
|
||||
if (status == AnimationStatus.completed) {
|
||||
widget.onPlaybackCompleted?.call();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant SCSvgaAssetWidget oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
@ -129,6 +146,7 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
|
||||
if (!widget.active) {
|
||||
_controller.stop();
|
||||
_controller.reset();
|
||||
widget.onPlaybackCompleted?.call();
|
||||
return;
|
||||
}
|
||||
|
||||
@ -148,6 +166,7 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.removeStatusListener(_handleAnimationStatusChanged);
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@ -170,7 +189,7 @@ class _SCSvgaAssetWidgetState extends State<SCSvgaAssetWidget>
|
||||
child: SVGAImage(
|
||||
_controller,
|
||||
fit: widget.fit,
|
||||
clearsAfterStop: false,
|
||||
clearsAfterStop: widget.clearsAfterStop,
|
||||
filterQuality: widget.filterQuality,
|
||||
allowDrawingOverflow: widget.allowDrawingOverflow,
|
||||
),
|
||||
|
||||
13
需求进度.md
13
需求进度.md
@ -21,17 +21,26 @@
|
||||
|
||||
## 已完成模块
|
||||
- 已按 2026-04-20 最新首页视觉需求,为 Party 房间列表前 3 个房卡接入新的本地排名边框 SVGA:桌面“房间排序前三的框”中的 3 份素材已导入工程并挂到首页房卡最上层,仅作用于当前列表前 3 项,其余房卡保持原样;后续又继续为前三房卡底部信息区补了横向与底部安全区,避免外扩边框直接压住国旗、房名和在线人数。同时 `Me` 板块里的 `Recent / Followed` tab 已继续对齐上方首页 tab 的斜体字样式,并移除了点击时的波浪反馈。
|
||||
- 已按 2026-04-20 最新房间联调继续修正房主在自己房间里的麦位操作回归:当前房主/管理员点击自己所在麦位时,不再被“直接打开资料卡”逻辑短路,会重新回到底部麦位菜单,从而正常看到自己可用的上下麦/禁麦操作;同时上麦、下麦、禁麦、解禁、锁麦、解锁这些动作在接口成功后会立即回写本地麦位状态,并主动补拉一次最新麦位列表,减少房主端自己操作后 UI 状态延迟或短暂错乱。
|
||||
- 已按 2026-04-20 最新房间联调继续修正房主在自己房间里的麦位操作回归:当前房主/管理员点击自己所在麦位时,不再被“直接打开资料卡”逻辑短路,会重新回到底部麦位菜单,从而正常看到自己可用的上下麦/禁麦操作;同时上麦、下麦、禁麦、解禁、锁麦、解锁这些动作在接口成功后会立即回写本地麦位状态,并主动补拉一次最新麦位列表,减少房主端自己操作后 UI 状态延迟或短暂错乱。最新已继续收紧“换麦”场景:当前会先本地顺滑切到目标麦位,再在短保护窗内拦住把自己打回旧麦位的旧轮询/旧广播快照;同时补修 `MicRes.copyWith(user: null)` 实际不会清空用户的问题,避免切麦时出现双占位或 `1 -> 4 -> 1 -> 4` 这类来回闪动。
|
||||
- 已按 2026-04-20 最新调整撤掉 `Wallet -> Recharge` 中新增的 MiFaPay 第三方支付 UI 与页面逻辑:当前充值页仅保留原生 `Google Pay / Apple Pay` 入口与商品列表,`Recharge methods` 区域也已收敛为单一原生支付卡片;此前接入的 MiFaPay 方法选择、底部弹窗、H5 收银台页以及对应页面级状态管理已从现有充值链路移除,避免继续对当前版本产生影响。
|
||||
- 已按 2026-04-18 联调要求继续收口幸运礼物链路:项目默认 API host 已临时切到 `http://192.168.110.43:1100/` 方便直连测试环境;`/gift/give/lucky-gift` 请求体也已补齐为最新结构,除原有 `giftId/quantity/roomId/acceptUserIds/checkCombo` 外,会稳定携带 `gameId: null`、`accepts: []`、`dynamicContentId: null`、`songId: null` 这几组字段,便于和当前后端参数定义保持一致。
|
||||
- 已继续补齐 `GAME_LUCKY_GIFT` 的 socket 播报与中奖动效:房间群消息收到该类型后,现在会统一落聊天室中奖消息、奖励弹层队列与发送者余额回写,不再只在 `3x+` 时才触发顶部奖励动画;同时全局飘窗已改为按服务端 `globalNews` 字段决定是否展示,避免再用前端本地倍率硬编码去猜。
|
||||
- 已按最新 UI 口径重排幸运礼物中奖展示:顶部 `LuckGiftNomorAnimWidget` 不再叠加大头像、倍率和奖励框,只在“单个礼物倍率 `> 10x`”或“单次中奖金币 `> 5000`”时负责播全屏 `luck_gift_reward_burst.svga`;原本的 `luck_gift_reward_frame.svga` 已改挂到房间礼物播报条最右侧,并在框内显示 `+formattedAwardAmount`,金额文案直接复用此前大头像下方那一份中奖金币额。
|
||||
- 已按最新 UI 口径重排幸运礼物中奖展示:顶部 `LuckGiftNomorAnimWidget` 不再叠加大头像、倍率和奖励框,只在“单个礼物倍率 `>= 10x`”或“单次中奖金币 `> 5000`”时负责播全屏 `luck_gift_reward_burst.svga`;原本的 `luck_gift_reward_frame.svga` 已改挂到房间礼物播报条最右侧,并在框内显示 `+formattedAwardAmount`,金额文案直接复用此前大头像下方那一份中奖金币额。
|
||||
- 已按最新确认撤掉播报条里的 lucky combo 特殊样式:房间礼物播报条右侧的 `xN` 数字现已恢复普通礼物样式,不再在这里做幸运礼物 10/20/50/100... 的特殊放大/描边展示;这些 `lucky_gift_combo_count_xxx.svga` 资源仍保留在幸运礼物倍率/数量命中时走全屏特效链,不再和播报条 UI 混在一起。
|
||||
- 已继续修正幸运礼物中奖播报条右侧奖励框不显示的问题:根因是 `pubspec.yaml` 之前只显式收录了 `sc_images/room/anim/gift/`,但没有把新的 `sc_images/room/anim/luck_gift/` 子目录单独打进 Flutter 资源清单,导致 `luck_gift_reward_frame.svga` 在运行时可引用但未被实际打包;当前已补齐资源目录声明,并为播报条奖励框增加本地 fallback,避免资源异常时再次只剩金额裸字。
|
||||
- 已按最新文案口径继续收紧幸运礼物中奖消息:聊天室里高倍率幸运礼物提示不再显示冗长的 `Coins / a lucky(magic) gift` 文字,当前已改为直接展示“中奖金额 + 金币图标 + 对应礼物图”,和房间礼物播报条的视觉表达保持一致。
|
||||
- 已继续微调幸运礼物中奖视觉:房间礼物播报条右侧的 `luck_gift_reward_frame.svga` 已进一步放大,并给右侧区域和主条正文预留了更宽的排版空间,避免资源已经正常显示但因为画布比例和透明边距看起来过小;同时聊天室高亮中奖消息与房间顶部幸运礼物横幅现已统一改成“中奖金额 + 金币图标 + from + 礼物图”的表达,去掉原先 `Coins / a lucky(magic) gift` 这类冗长英文文案;另外全屏 `luck_gift_reward_burst.svga` 也已补上中部金额文案,避免播发时只见特效不见本次实际中奖金币数。
|
||||
- 已按 2026-04-20 最新联调继续收口幸运礼物播报噪音:顶部幸运礼物飘屏现在会对同一房间、同一送礼人、同一接收人、同一礼物的连续中奖做队列内聚合,不再每来一条都重新追加一个新飘屏;当前展示中的那一条也会直接叠加金币额并刷新,避免“刷礼过快时飘屏一条接一条排队刷过去”。同时飘屏中奖文案已强制收为单行显示,防止金额、`from` 和礼物图被挤成上下两行。
|
||||
- 已继续修复房间礼物播报条偶发出现两条一模一样内容的问题:此前 `GiftAnimationManager` 只会和“正在播放”的同 `labelId` 项做合并,等待队列里的同类播报不会提前去重,因此高频情况下仍可能排出两条完全一样的条目;当前已改为在入队时同时检查“正在播”和“待播”两侧,命中相同播报键时直接原地合并,不再把重复项继续塞进队列。
|
||||
- 已按 2026-04-20 最新确认继续调整幸运礼物展示逻辑:房间播报条最右侧的中奖金币现已按同一条播报累计值展示,不再每次中奖只用最后一笔金额覆盖前一笔;幸运礼物顶部飘屏则撤回此前的聚合策略,恢复为每条都单独展示,但仅当服务端倍率达到 `5x` 及以上时才触发飘屏,避免低倍率也持续刷屏;同时 `luck_gift_reward_burst.svga` 的触发链路已补上“当前房间广播兜底 + 事件去重”,即便只收到广播消息也能进队列播放,而同一事件不会因为广播/群消息双到达而重复播发。
|
||||
- 已继续修正幸运礼物 `burst` 的倍率边界判断:`LuckGiftNomorAnimWidget` 内部此前仍使用旧的“倍率 `> 10x`”严格大于判断,导致刚好命中 `10x` 的中奖虽然会走飘屏链路,但不会实际触发 `luck_gift_reward_burst.svga`;当前已统一改为“倍率 `>= 10x` 或单次中奖金币 `> 5000`”。
|
||||
- 已继续修正幸运礼物 `burst` 的实际播放链路:此前 `currentPlayingLuckGift` 队列会把所有幸运礼物中奖事件都排进去,哪怕只是低倍率、根本不需要播 `luck_gift_reward_burst.svga` 的那类消息,也会先占掉每次 3 秒的播放时段;这样在刷得比较密时,真正命中 `10x+` 或 `>5000` 金币的事件只是被压在队列后面,视觉上就会像“明明达标了却没触发”。当前已把队列入口改成只接收真正命中 `burst` 条件的事件,并把命中判断收口到同一处,避免顶部飘屏和 `burst` 各自走不同口径。
|
||||
- 已继续调整幸运礼物播报条的连刷表现:带右侧中奖奖励框的幸运礼物播报现在会固定在各自的坑位上显示,不再跟随普通礼物滚屏那套纵向位移动画反复“刷新整条”;用户连续送同一条幸运礼物时,前端只会在原位更新右侧 `xN` 数量和累计金币,直到连刷停止后再按既有时长自然消失。
|
||||
- 已继续微调幸运礼物 `burst` 中央金额文案的定位:此前这层 `+金币` 文字仍使用固定 `top` 绝对定位,实际在不同画布比例下会整体偏上,容易跑出 `luck_gift_reward_burst.svga` 中央的金额承载区;当前已改成基于整层居中后再轻微下移的相对锚点,确保金额文案稳定落在特效中间那块 `+****` 的视觉区域内。
|
||||
- 已继续细调幸运礼物 `burst` 中央金额文案的纵向位置:上一版相对锚点虽然已经回到特效主体内部,但仍略偏下;当前已把对齐点和下移量一起往中间收回一档,让 `+金币` 更接近 `luck_gift_reward_burst.svga` 中央的视觉中线。
|
||||
- 已继续修正幸运礼物 `burst` 中央金额文案的细节对位:当前已再上移 `2` 个单位,并给金额文本左侧补出额外留白,避免斜体 `+` 号因为字形外扩而贴边或被裁掉,确保 `+金币` 能完整落在特效中部。
|
||||
- 已继续按最新联调口径修正幸运礼物 `burst` 中央金额文案:当前已去掉文本里的 `+` 字符,仅保留金币数本身显示;同时维持上一版向上微调后的纵向位置,让金额落点保持在特效中部偏上的稳定区域。
|
||||
- 已继续收口幸运礼物 `burst` 金额文案与 `svga` 本体的时机同步:此前中央金币文本直接跟外层中奖数据显隐,而 `svga` 自身还存在资源加载和单次播放完成的生命周期,所以两者在出现/消失时会有肉眼可见的前后差;当前已给 `SCSvgaAssetWidget` 补上播放开始/结束回调,并让 `burst` 中央金额只在 `svga` 真正开始播时显示、在它播完清帧时一并隐藏。
|
||||
- 已优化语言房麦位/头像的二次确认交互:普通用户点击可上麦的空麦位时,当前会直接执行上麦,不再先弹出只有 `Take the mic / Cancel` 的确认层;普通用户点击房间头像或已占麦位上的用户头像时,也会直接打开个人卡片,不再额外弹出仅含 `Open user profile card / Cancel` 的底部确认。房主/管理员仍保留原有带禁麦、锁麦、邀请上麦等管理动作的底部菜单,避免误删管理能力。
|
||||
- 已继续收窄语言房个人卡片前的“确认意义”弹层:当前用户在麦位上点击自己的头像时,也会直接打开自己的个人卡片,不再先弹出仅包含 `Leave the mic / Open user profile card / Cancel` 的底部菜单;同时个人卡片内的“离开麦位”入口已替换为新的 `leave` 视觉素材,和最新房间交互稿保持一致。
|
||||
- 已继续微调语言房个人卡片与送礼 UI:个人卡片底部动作文案现已支持两行居中展示,避免 `Leave the mic` 这类英文按钮被硬截断;房间底部礼物入口也已切换为新的本地 `SVGA` 资源 `room_bottom_gift_button.svga`,保持房间底栏视觉和最新动效稿一致。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user