This commit is contained in:
roxy 2026-04-30 19:54:24 +08:00
parent 36ba0f3573
commit 6a95763436
36 changed files with 3958 additions and 937 deletions

View File

@ -502,7 +502,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = S9X2AJ2US9; DEVELOPMENT_TEAM = S9X2AJ2US9;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -511,7 +511,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.6; MARKETING_VERSION = 1.2.7;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -693,7 +693,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = F33K8VUZ62; DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -702,7 +702,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.6; MARKETING_VERSION = 1.2.7;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
@ -722,7 +722,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 8; CURRENT_PROJECT_VERSION = 9;
DEVELOPMENT_TEAM = F33K8VUZ62; DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@ -731,7 +731,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.2.6; MARKETING_VERSION = 1.2.7;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty; PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -1,5 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math' as math;
import 'package:extended_image/extended_image.dart' import 'package:extended_image/extended_image.dart'
show ExtendedImage, ExtendedRawImage, ExtendedImageState, LoadState; show ExtendedImage, ExtendedRawImage, ExtendedImageState, LoadState;
import 'package:extended_text/extended_text.dart'; import 'package:extended_text/extended_text.dart';
@ -54,6 +56,8 @@ import 'package:yumi/shared/tools/sc_path_utils.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/modules/index/main_route.dart'; import 'package:yumi/modules/index/main_route.dart';
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
import '../../shared/business_logic/models/res/sc_user_red_packet_send_res.dart'; import '../../shared/business_logic/models/res/sc_user_red_packet_send_res.dart';
@ -1245,31 +1249,7 @@ class _MessageItem extends StatelessWidget {
return Builder( return Builder(
builder: (ct) { builder: (ct) {
return GestureDetector( return GestureDetector(
child: Container( child: _buildTextBubble(content),
decoration: BoxDecoration(
color: Color(0xff18F2B1).withOpacity(0.1),
borderRadius: BorderRadius.only(
topLeft:
message.isSelf! ? Radius.circular(8) : Radius.circular(0),
topRight: Radius.circular(8.w),
bottomLeft: Radius.circular(8.w),
bottomRight:
message.isSelf! ? Radius.circular(0) : Radius.circular(8),
),
),
padding: EdgeInsets.all(8.0.w),
child: Container(
constraints: BoxConstraints(maxWidth: width(220)),
child: ExtendedText(
content,
// specialTextSpanBuilder: MySpecialTextSpanBuilder(),
style: TextStyle(
fontSize: sp(14),
color: message.isSelf! ? Colors.white : Colors.white,
),
),
),
),
onLongPress: () { onLongPress: () {
_showMsgItemMenu(ct, content); _showMsgItemMenu(ct, content);
}, },
@ -1278,6 +1258,286 @@ class _MessageItem extends StatelessWidget {
); );
} }
Widget _buildTextBubble(String content) {
final chatBubble = _senderProfile()?.getChatBox();
final chatBubbleUrl = _resolveChatBubbleImageUrl(chatBubble);
if (chatBubbleUrl.isNotEmpty) {
return _buildVipTextBubble(content: content, imageUrl: chatBubbleUrl);
}
return _buildDefaultTextBubble(content);
}
SocialChatUserProfile? _senderProfile() {
if (message.isSelf ?? false) {
return AccountStorage().getCurrentUser()?.userProfile;
}
return friend;
}
Widget _buildDefaultTextBubble(String content) {
return Container(
decoration: BoxDecoration(
color: Color(0xff18F2B1).withValues(alpha: 0.1),
borderRadius: BorderRadius.only(
topLeft: message.isSelf! ? Radius.circular(8) : Radius.circular(0),
topRight: Radius.circular(8.w),
bottomLeft: Radius.circular(8.w),
bottomRight:
message.isSelf! ? Radius.circular(0) : Radius.circular(8),
),
),
padding: EdgeInsets.all(8.0.w),
child: Container(
constraints: BoxConstraints(maxWidth: width(220)),
child: ExtendedText(
content,
style: TextStyle(fontSize: sp(14), color: Colors.white),
),
),
);
}
Widget _buildVipTextBubble({
required String content,
required String imageUrl,
}) {
final textStyle = TextStyle(fontSize: sp(14), color: Colors.white);
final multilineMaxBubbleWidth = math.min(
ScreenUtil().screenWidth * 0.64,
260.w,
);
final multilineMaxTextWidth = math.max(
90.w,
multilineMaxBubbleWidth - 58.w,
);
final multilineTextPainter = TextPainter(
text: TextSpan(text: content, style: textStyle),
textDirection: Directionality.of(context),
)..layout(maxWidth: multilineMaxTextWidth);
if (multilineTextPainter.computeLineMetrics().length > 1) {
return _buildMultilineVipTextBubble(
content: content,
imageUrl: imageUrl,
textStyle: textStyle,
textSize: multilineTextPainter.size,
maxBubbleWidth: multilineMaxBubbleWidth,
);
}
return _buildSingleLineVipTextBubble(
content: content,
imageUrl: imageUrl,
textStyle: textStyle,
);
}
Widget _buildMultilineVipTextBubble({
required String content,
required String imageUrl,
required TextStyle textStyle,
required Size textSize,
required double maxBubbleWidth,
}) {
final bubbleWidth = math
.max(126.w, textSize.width + 58.w)
.clamp(126.w, maxBubbleWidth);
final textMaxWidth = math.max(90.w, bubbleWidth - 58.w);
final bubbleHeight = math.max(58.w, textSize.height + 26.w);
return SizedBox(
width: bubbleWidth,
height: bubbleHeight,
child: Stack(
fit: StackFit.expand,
children: [
_buildVipBubbleBackground(
url: imageUrl,
width: bubbleWidth,
height: bubbleHeight,
fallback: _buildVipBubbleFallback(),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(29.w, 11.w, 29.w, 11.w),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: SizedBox(
width: textMaxWidth,
child: ExtendedText(content, style: textStyle),
),
),
),
],
),
);
}
Widget _buildSingleLineVipTextBubble({
required String content,
required String imageUrl,
required TextStyle textStyle,
}) {
final horizontalPadding = 18.w;
final verticalPadding = 8.w;
final minSkinWidth = 132.w;
final minSkinHeight = 44.w;
final maxTextWidth = width(220);
final maxBubbleWidth = maxTextWidth + horizontalPadding * 2;
final textSize = _measureTextSize(
text: content,
style: textStyle,
maxWidth: maxTextWidth,
);
final bubbleWidth = math.min(
maxBubbleWidth,
math.max(horizontalPadding * 2, textSize.width + horizontalPadding * 2),
);
final textMaxWidth = math.max(1.0, bubbleWidth - horizontalPadding * 2);
final bubbleHeight = math.max(
verticalPadding * 2,
textSize.height + verticalPadding * 2,
);
final skinAlignment =
message.isSelf!
? AlignmentDirectional.centerEnd
: AlignmentDirectional.centerStart;
return SizedBox(
width: bubbleWidth,
height: bubbleHeight,
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
Positioned.fill(child: _buildVipBubbleFallback()),
Positioned.fill(
child: _buildVipBubbleSkin(
url: imageUrl,
width: math.max(bubbleWidth, minSkinWidth),
height: math.max(bubbleHeight, minSkinHeight),
alignment: skinAlignment,
),
),
Padding(
padding: EdgeInsetsDirectional.symmetric(
horizontal: horizontalPadding,
vertical: verticalPadding,
),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: SizedBox(
width: textMaxWidth,
child: ExtendedText(content, style: textStyle),
),
),
),
],
),
);
}
Widget _buildVipBubbleSkin({
required String url,
required double width,
required double height,
required AlignmentGeometry alignment,
}) {
return OverflowBox(
alignment: alignment,
minWidth: width,
maxWidth: width,
minHeight: height,
maxHeight: height,
child:
SCNetworkSvgaWidget.isSvga(url)
? SCNetworkSvgaWidget(
resource: url,
width: width,
height: height,
fit: BoxFit.fill,
fallback: SizedBox(width: width, height: height),
)
: SCNinePatchImage(
url: url,
width: width,
height: height,
centerSliceRatio: const EdgeInsets.fromLTRB(
0.34,
0.34,
0.34,
0.34,
),
fallback: SizedBox(width: width, height: height),
),
);
}
Widget _buildVipBubbleBackground({
required String url,
required double width,
required double height,
required Widget fallback,
}) {
if (SCNetworkSvgaWidget.isSvga(url)) {
return SCNetworkSvgaWidget(
resource: url,
width: width,
height: height,
fit: BoxFit.fill,
fallback: fallback,
);
}
return SCNinePatchImage(
url: url,
width: width,
height: height,
centerSliceRatio: const EdgeInsets.fromLTRB(0.34, 0.34, 0.34, 0.34),
fallback: fallback,
);
}
Size _measureTextSize({
required String text,
required TextStyle style,
required double maxWidth,
}) {
final textPainter = TextPainter(
text: TextSpan(text: text, style: style),
textDirection: Directionality.of(context),
)..layout(maxWidth: maxWidth);
return textPainter.size;
}
Widget _buildVipBubbleFallback() {
return DecoratedBox(
decoration: BoxDecoration(
color: Color(0xff18F2B1).withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8.w),
),
);
}
String _resolveChatBubbleImageUrl(PropsResources? resource) {
return _firstNonBlank([
resource?.imSendCoverUrl,
resource?.imOpenedUrl,
resource?.imNotOpenedUrl,
resource?.roomSendCoverUrl,
resource?.roomOpenedUrl,
resource?.roomNotOpenedUrl,
resource?.cover,
]);
}
String _firstNonBlank(Iterable<String?> values) {
for (final value in values) {
final text = value?.trim();
if (text != null && text.isNotEmpty) {
return text;
}
}
return "";
}
void _showMsgItemMenu(BuildContext ct, String content) { void _showMsgItemMenu(BuildContext ct, String content) {
SmartDialog.showAttach( SmartDialog.showAttach(
tag: "showMsgItemMenu", tag: "showMsgItemMenu",

View File

@ -136,6 +136,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
required int quantity, required int quantity,
int animationCount = 1, int animationCount = 1,
}) { }) {
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
final rtmProvider = Provider.of<RtmProvider>( final rtmProvider = Provider.of<RtmProvider>(
navigatorKey.currentState!.context, navigatorKey.currentState!.context,
listen: false, listen: false,
@ -173,6 +174,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
gift: gift, gift: gift,
user: currentUser, user: currentUser,
toUser: targetUser, toUser: targetUser,
targetUserIds: targetUserIds,
number: quantity, number: quantity,
customAnimationCount: animationCount, customAnimationCount: animationCount,
), ),
@ -220,19 +222,13 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance(); Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
rtcProvider?.roomWheatMap.forEach((k, v) { rtcProvider?.roomWheatMap.forEach((k, v) {
if (v.user != null) { if (v.user != null) {
if (v.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) { listMai.add(HeadSelect(widget.toUser == null, v));
listMai.add(HeadSelect(true, v));
} else {
listMai.add(HeadSelect(false, v));
}
}
isAll = true;
for (var mai in listMai) {
if (!mai.isSelect) {
isAll = false;
}
} }
}); });
isAll =
widget.toUser == null &&
listMai.isNotEmpty &&
listMai.every((mai) => mai.isSelect);
} }
@override @override
@ -1397,6 +1393,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
int animationCount = 1, int animationCount = 1,
}) { }) {
///IM消息 ///IM消息
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
for (var u in acceptUsers) { for (var u in acceptUsers) {
final special = gift.special ?? ""; final special = gift.special ?? "";
final giftSourceUrl = gift.giftSourceUrl ?? ""; final giftSourceUrl = gift.giftSourceUrl ?? "";
@ -1428,6 +1425,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
gift: gift, gift: gift,
user: AccountStorage().getCurrentUser()?.userProfile, user: AccountStorage().getCurrentUser()?.userProfile,
toUser: u.user, toUser: u.user,
targetUserIds: targetUserIds,
number: quantity, number: quantity,
customAnimationCount: animationCount, customAnimationCount: animationCount,
type: SCRoomMsgType.gift, type: SCRoomMsgType.gift,
@ -1603,11 +1601,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
required int quantity, required int quantity,
int animationCount = 1, int animationCount = 1,
}) async { }) async {
final targetUserIds = final targetUserIds = _resolveAcceptUserIds(acceptUsers);
acceptUsers
.map((u) => u.user?.id ?? "")
.where((id) => id.isNotEmpty)
.toList();
final firstTargetUser = final firstTargetUser =
acceptUsers.isNotEmpty ? acceptUsers.first.user : null; acceptUsers.isNotEmpty ? acceptUsers.first.user : null;
_giftFxLog( _giftFxLog(
@ -1631,6 +1625,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
gift: gift, gift: gift,
user: AccountStorage().getCurrentUser()?.userProfile, user: AccountStorage().getCurrentUser()?.userProfile,
toUser: firstTargetUser, toUser: firstTargetUser,
targetUserIds: targetUserIds,
number: quantity, number: quantity,
customAnimationCount: animationCount, customAnimationCount: animationCount,
type: SCRoomMsgType.luckGiftAnimOther, type: SCRoomMsgType.luckGiftAnimOther,
@ -1644,6 +1639,18 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
'targetUserIds=${targetUserIds.join(",")}', 'targetUserIds=${targetUserIds.join(",")}',
); );
} }
List<String> _resolveAcceptUserIds(List<MicRes> acceptUsers) {
final targetUserIds = <String>[];
for (final acceptUser in acceptUsers) {
final userId = (acceptUser.user?.id ?? "").trim();
if (userId.isEmpty || targetUserIds.contains(userId)) {
continue;
}
targetUserIds.add(userId);
}
return targetUserIds;
}
} }
class CheckNumber extends StatelessWidget { class CheckNumber extends StatelessWidget {

View File

@ -9,9 +9,10 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:yumi/app/constants/sc_room_msg_type.dart'; import 'package:yumi/app/constants/sc_room_msg_type.dart';
import 'package:yumi/app/constants/sc_screen.dart'; import 'package:yumi/app/constants/sc_screen.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart';
import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
@ -93,9 +94,10 @@ class _RoomMemberPageState
list.isNotEmpty ? buildTag(list.first.roles) : Container(), list.isNotEmpty ? buildTag(list.first.roles) : Container(),
SizedBox(height: 3.w), SizedBox(height: 3.w),
Column( Column(
children: List.generate(list.length, (cIndex) { children: List.generate(list.length, (cIndex) {
SocialChatRoomMemberRes userInfo = list[cIndex]; SocialChatRoomMemberRes userInfo = list[cIndex];
return GestureDetector( final userProfile = _mergeCurrentUserVip(userInfo.userProfile);
return GestureDetector(
child: Container( child: Container(
margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w), padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
@ -106,10 +108,9 @@ class _RoomMemberPageState
child: Row( child: Row(
children: [ children: [
head( head(
url: userInfo.userProfile?.userAvatar ?? "", url: userProfile?.userAvatar ?? "",
width: 52.w, width: 52.w,
headdress: headdress: userProfile?.getHeaddress()?.sourceUrl,
userInfo.userProfile?.getHeaddress()?.sourceUrl,
), ),
SizedBox(width: 3.w), SizedBox(width: 3.w),
Column( Column(
@ -129,57 +130,47 @@ class _RoomMemberPageState
SizedBox(width: 3.w), SizedBox(width: 3.w),
socialchatNickNameText( socialchatNickNameText(
maxWidth: 160.w, maxWidth: 160.w,
userInfo.userProfile?.userNickname ?? "", userProfile?.userNickname ?? "",
fontSize: 14.sp, fontSize: 14.sp,
textColor: Colors.black, textColor: Colors.black,
type: type:
userInfo.userProfile?.getVIP()?.name ?? userProfile?.getVIP()?.name ?? "",
"", needScroll:
needScroll: (userProfile
(userInfo ?.userNickname
.userProfile ?.characters
?.userNickname .length ??
?.characters 0) >
.length ??
0) >
14, 14,
), ),
SizedBox(width: 3.w), SizedBox(width: 3.w),
userInfo.userProfile?.getVIP() != null userProfile?.getVIP() != null
? netImage( ? netImage(
url: url: userProfile?.getVIP()?.cover ?? "",
userInfo.userProfile
?.getVIP()
?.cover ??
"",
width: 25.w, width: 25.w,
height: 25.w, height: 25.w,
) )
: Container(), : Container(),
SizedBox(width: 5.w), SizedBox(width: 5.w),
userInfo.userProfile?.wearBadge?.isNotEmpty ?? userProfile?.wearBadge?.isNotEmpty ??
false false
? SizedBox( ? SizedBox(
height: 25.w, height: 25.w,
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
shrinkWrap: true, shrinkWrap: true,
itemCount: itemCount:
userInfo userProfile?.wearBadge?.length ??
.userProfile 0,
?.wearBadge
?.length ??
0,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return netImage( return netImage(
width: 25.w, width: 25.w,
height: 25.w, height: 25.w,
url: url:
userInfo userProfile
.userProfile ?.wearBadge?[index]
?.wearBadge?[index] .selectUrl ??
.selectUrl ?? "",
"",
); );
}, },
separatorBuilder: ( separatorBuilder: (
@ -197,18 +188,17 @@ class _RoomMemberPageState
), ),
SizedBox(height: 3.w), SizedBox(height: 3.w),
SCSpecialIdBadge( SCSpecialIdBadge(
idText: userInfo.userProfile?.getID() ?? "", idText: userProfile?.getID() ?? "",
showAnimated: showAnimated: userProfile?.hasSpecialId() ?? false,
userInfo.userProfile?.hasSpecialId() ?? false,
assetPath: SCSpecialIdAssets.userId, assetPath: SCSpecialIdAssets.userId,
animationWidth: 62.w, animationWidth: 62.w,
animationHeight: 24.w, animationHeight: 24.w,
showTextBesideAnimated: true, showTextBesideAnimated: true,
animatedTextSpacing: 0, animatedTextSpacing: 0,
showAnimatedGradientText: showAnimatedGradientText:
userInfo.userProfile userProfile
?.shouldShowColoredSpecialIdText() ?? ?.shouldShowColoredSpecialIdText() ??
false, false,
animationTextStyle: TextStyle( animationTextStyle: TextStyle(
color: Colors.black, color: Colors.black,
fontSize: 12.sp, fontSize: 12.sp,
@ -362,10 +352,29 @@ class _RoomMemberPageState
if (onErr != null) { if (onErr != null) {
onErr(); onErr();
} }
} }
} }
Widget buildTag(String? roles) { SocialChatUserProfile? _mergeCurrentUserVip(
SocialChatUserProfile? profile,
) {
if (profile == null || profile.vipLevelForColoredId > 0) {
return profile;
}
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
if (currentProfile == null ||
currentProfile.vipLevelForColoredId <= 0 ||
currentProfile.id != profile.id) {
return profile;
}
return profile.copyWith(
vipLevel:
currentProfile.vipLevel ??
currentProfile.vipLevelForColoredId.toString(),
);
}
Widget buildTag(String? roles) {
if (roles == SCRoomRolesType.HOMEOWNER.name) { if (roles == SCRoomRolesType.HOMEOWNER.name) {
return Row( return Row(
children: [ children: [

View File

@ -1,9 +1,11 @@
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
import 'package:yumi/main.dart'; import 'package:yumi/main.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
@ -47,9 +49,10 @@ class _RoomGiftRankTabPageState
return buildList(context); return buildList(context);
} }
@override @override
Widget buildItemOne(RoomGiftRankRes userInfo, int index) { Widget buildItemOne(RoomGiftRankRes userInfo, int index) {
return GestureDetector( final userProfile = _mergeCurrentUserVip(userInfo.userProfile);
return GestureDetector(
child: Container( child: Container(
margin: EdgeInsets.symmetric(vertical: 3.w), margin: EdgeInsets.symmetric(vertical: 3.w),
padding: EdgeInsets.symmetric(horizontal: 16.w), padding: EdgeInsets.symmetric(horizontal: 16.w),
@ -90,14 +93,14 @@ class _RoomGiftRankTabPageState
SizedBox(width: 10.w), SizedBox(width: 10.w),
GestureDetector( GestureDetector(
child: head( child: head(
url: userInfo.userProfile?.userAvatar ?? "", url: userProfile?.userAvatar ?? "",
width: 55.w, width: 55.w,
// headdress: userInfo.userProfile?.getHeaddress()?.sourceUrl, // headdress: userInfo.userProfile?.getHeaddress()?.sourceUrl,
), ),
onTap: () { onTap: () {
showBottomInCenterDialog( showBottomInCenterDialog(
navigatorKey.currentState!.context, navigatorKey.currentState!.context,
RoomUserInfoCard(userId: userInfo.userProfile?.id), RoomUserInfoCard(userId: userProfile?.id),
); );
SmartDialog.dismiss(tag: "showRoomGiftRankPage"); SmartDialog.dismiss(tag: "showRoomGiftRankPage");
}, },
@ -110,16 +113,12 @@ class _RoomGiftRankTabPageState
children: [ children: [
socialchatNickNameText( socialchatNickNameText(
maxWidth: 170.w, maxWidth: 170.w,
userInfo.userProfile?.userNickname ?? "", userProfile?.userNickname ?? "",
fontSize: 14.sp, fontSize: 14.sp,
type: userInfo.userProfile?.getVIP()?.name ?? "", type: userProfile?.getVIP()?.name ?? "",
needScroll: needScroll:
(userInfo (userProfile?.userNickname?.characters.length ??
.userProfile 0) >
?.userNickname
?.characters
.length ??
0) >
14, 14,
), ),
], ],
@ -131,18 +130,17 @@ class _RoomGiftRankTabPageState
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
children: [ children: [
SCSpecialIdBadge( SCSpecialIdBadge(
idText: userInfo.userProfile?.getID() ?? "", idText: userProfile?.getID() ?? "",
showAnimated: showAnimated: userProfile?.hasSpecialId() ?? false,
userInfo.userProfile?.hasSpecialId() ?? false,
assetPath: SCSpecialIdAssets.userId, assetPath: SCSpecialIdAssets.userId,
animationWidth: 62.w, animationWidth: 62.w,
animationHeight: 24.w, animationHeight: 24.w,
showTextBesideAnimated: true, showTextBesideAnimated: true,
animatedTextSpacing: 0, animatedTextSpacing: 0,
showAnimatedGradientText: showAnimatedGradientText:
userInfo.userProfile userProfile
?.shouldShowColoredSpecialIdText() ?? ?.shouldShowColoredSpecialIdText() ??
false, false,
animationTextStyle: TextStyle( animationTextStyle: TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 12.sp, fontSize: 12.sp,
@ -165,7 +163,7 @@ class _RoomGiftRankTabPageState
), ),
onTap: () { onTap: () {
Clipboard.setData( Clipboard.setData(
ClipboardData(text: userInfo.userProfile?.getID() ?? ""), ClipboardData(text: userProfile?.getID() ?? ""),
); );
SCTts.show( SCTts.show(
SCAppLocalizations.of(context)!.copiedToClipboard, SCAppLocalizations.of(context)!.copiedToClipboard,
@ -191,11 +189,30 @@ class _RoomGiftRankTabPageState
), ),
), ),
onTap: () {}, onTap: () {},
); );
} }
@override SocialChatUserProfile? _mergeCurrentUserVip(
empty() { SocialChatUserProfile? profile,
) {
if (profile == null || profile.vipLevelForColoredId > 0) {
return profile;
}
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
if (currentProfile == null ||
currentProfile.vipLevelForColoredId <= 0 ||
currentProfile.id != profile.id) {
return profile;
}
return profile.copyWith(
vipLevel:
currentProfile.vipLevel ??
currentProfile.vipLevelForColoredId.toString(),
);
}
@override
empty() {
return mainEmpty( return mainEmpty(
image: Image.asset( image: Image.asset(
'sc_images/general/sc_icon_loading.png', 'sc_images/general/sc_icon_loading.png',

View File

@ -1,5 +1,7 @@
import 'dart:math' as math;
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:math' as math;
import 'package:yumi/app/constants/sc_room_msg_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -56,6 +58,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
static const Duration _giftAnimationQueueDrainWindow = static const Duration _giftAnimationQueueDrainWindow =
_luckyGiftQueueDrainWindow; _luckyGiftQueueDrainWindow;
static const int _maxTrackedGiftAnimations = _maxLuckyGiftTrackedAnimations; static const int _maxTrackedGiftAnimations = _maxLuckyGiftTrackedAnimations;
static const Duration _giftFlightBatchDedupeWindow = Duration(
milliseconds: 650,
);
late TabController _tabController; late TabController _tabController;
final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()]; final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()];
@ -64,6 +69,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
RoomGiftSeatFlightController(); RoomGiftSeatFlightController();
final Map<String, _LuckyGiftComboSession> _luckyGiftComboSessions = final Map<String, _LuckyGiftComboSession> _luckyGiftComboSessions =
<String, _LuckyGiftComboSession>{}; <String, _LuckyGiftComboSession>{};
final Map<String, Timer> _giftFlightBatchDedupeTimers = <String, Timer>{};
int _shownRoomStartupFailureToken = 0; int _shownRoomStartupFailureToken = 0;
RtcProvider? _rtcProvider; RtcProvider? _rtcProvider;
bool _roomProviderRebuildScheduled = false; bool _roomProviderRebuildScheduled = false;
@ -84,6 +90,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
listen: false, listen: false,
).toggleGiftAnimationVisibility(false); ).toggleGiftAnimationVisibility(false);
_clearLuckyGiftComboSessions(); _clearLuckyGiftComboSessions();
_clearGiftFlightBatchDedupeTimers();
_giftSeatFlightController.clear(); _giftSeatFlightController.clear();
} }
}); });
@ -169,6 +176,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
RoomEntranceHelper.clearQueue(); RoomEntranceHelper.clearQueue();
_clearLuckyGiftComboSessions(); _clearLuckyGiftComboSessions();
_clearGiftFlightBatchDedupeTimers();
_giftSeatFlightController.clear(); _giftSeatFlightController.clear();
OverlayManager().removeRoom(); OverlayManager().removeRoom();
SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend'); SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend');
@ -314,7 +322,10 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
child: Stack( child: Stack(
children: [ children: [
Column( Column(
children: [_buildChatView(), RoomBottomWidget()], children: [
_buildChatView(),
const RoomBottomWidget(showGiftComboButton: false),
],
), ),
LGiftAnimalPage(), LGiftAnimalPage(),
Transform.translate( Transform.translate(
@ -336,6 +347,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
// _buildPlayViews(), // _buildPlayViews(),
/// ///
LuckGiftNomorAnimWidget(), LuckGiftNomorAnimWidget(),
const RoomGiftComboFloatingLayer(),
], ],
), ),
), ),
@ -491,16 +503,27 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
).enqueueGiftAnimation(giftModel); ).enqueueGiftAnimation(giftModel);
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim(); final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
final targetUserId = _resolveGiftTargetUserId(msg); final targetUserIds = _resolveGiftTargetUserIds(msg);
final targetGroupKey = _buildGiftTargetGroupKey(targetUserIds);
if (_supportsComboMilestoneEffects(msg)) { if (_supportsComboMilestoneEffects(msg)) {
_handleComboMilestoneVisuals(msg, targetUserId); _handleComboMilestoneVisuals(msg, targetGroupKey);
} }
final isLuckyGift = _isLuckyGiftMessage(msg); final isLuckyGift = _isLuckyGiftMessage(msg);
if (isLuckyGift) { if (isLuckyGift) {
_handleLuckyGiftComboVisuals(msg, giftPhoto, targetUserId); _handleLuckyGiftComboVisuals(
msg,
giftPhoto,
targetGroupKey,
targetUserIds,
);
return; return;
} }
_handleStandardGiftComboVisuals(msg, giftPhoto, targetUserId); _handleStandardGiftComboVisuals(
msg,
giftPhoto,
targetGroupKey,
targetUserIds,
);
} }
void _luckyGiftRewardTickerListener(Msg msg) { void _luckyGiftRewardTickerListener(Msg msg) {
@ -612,17 +635,24 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
void _handleStandardGiftComboVisuals( void _handleStandardGiftComboVisuals(
Msg msg, Msg msg,
String giftPhoto, String giftPhoto,
String? targetUserId, String? targetGroupKey,
List<String> targetUserIds,
) { ) {
if ((msg.number ?? 0) <= 0) { if ((msg.number ?? 0) <= 0) {
return; return;
} }
if (!_shouldPlaySeatFlightGiftAnimation(msg) || if (!_shouldPlaySeatFlightGiftAnimation(msg) ||
targetUserId == null || targetGroupKey == null ||
targetUserIds.isEmpty ||
giftPhoto.isEmpty) { giftPhoto.isEmpty) {
return; return;
} }
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId); final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds);
if (targetUserIds.length > 1 &&
!_markGiftFlightBatchForPlayback(flightBatchKey)) {
return;
}
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey);
final session = _luckyGiftComboSessions.putIfAbsent( final session = _luckyGiftComboSessions.putIfAbsent(
sessionKey, sessionKey,
() => _LuckyGiftComboSession(), () => _LuckyGiftComboSession(),
@ -633,8 +663,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
_enqueueTrackedSeatFlightAnimations( _enqueueTrackedSeatFlightAnimations(
sessionKey: sessionKey, sessionKey: sessionKey,
giftPhoto: giftPhoto, giftPhoto: giftPhoto,
targetUserId: targetUserId, targetUserIds: targetUserIds,
animationCount: _resolveTrackedAnimationCount(msg), animationCount: _resolveTrackedAnimationCount(msg),
batchKey: flightBatchKey,
); );
_scheduleGiftAnimationSessionEnd(sessionKey); _scheduleGiftAnimationSessionEnd(sessionKey);
@ -643,12 +674,16 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
void _handleLuckyGiftComboVisuals( void _handleLuckyGiftComboVisuals(
Msg msg, Msg msg,
String giftPhoto, String giftPhoto,
String? targetUserId, String? targetGroupKey,
List<String> targetUserIds,
) { ) {
if ((msg.number ?? 0) <= 0) { if ((msg.number ?? 0) <= 0) {
return; return;
} }
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId); if (targetGroupKey == null || targetUserIds.isEmpty) {
return;
}
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey);
final session = _luckyGiftComboSessions[sessionKey]; final session = _luckyGiftComboSessions[sessionKey];
if (session == null) { if (session == null) {
return; return;
@ -656,14 +691,19 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
session.endTimer?.cancel(); session.endTimer?.cancel();
session.clearQueueTimer?.cancel(); session.clearQueueTimer?.cancel();
if (_shouldPlaySeatFlightGiftAnimation(msg) && if (_shouldPlaySeatFlightGiftAnimation(msg) && giftPhoto.isNotEmpty) {
targetUserId != null && final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds);
giftPhoto.isNotEmpty) { if (targetUserIds.length > 1 &&
!_markGiftFlightBatchForPlayback(flightBatchKey)) {
_scheduleGiftAnimationSessionEnd(sessionKey);
return;
}
_enqueueTrackedSeatFlightAnimations( _enqueueTrackedSeatFlightAnimations(
sessionKey: sessionKey, sessionKey: sessionKey,
giftPhoto: giftPhoto, giftPhoto: giftPhoto,
targetUserId: targetUserId, targetUserIds: targetUserIds,
animationCount: _resolveTrackedAnimationCount(msg), animationCount: _resolveTrackedAnimationCount(msg),
batchKey: flightBatchKey,
); );
} }
_scheduleGiftAnimationSessionEnd(sessionKey); _scheduleGiftAnimationSessionEnd(sessionKey);
@ -689,28 +729,42 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
void _enqueueTrackedSeatFlightAnimations({ void _enqueueTrackedSeatFlightAnimations({
required String sessionKey, required String sessionKey,
required String giftPhoto, required String giftPhoto,
required String targetUserId, required List<String> targetUserIds,
required int animationCount, required int animationCount,
required String batchKey,
}) { }) {
unawaited( unawaited(
warmImageResource(giftPhoto, logicalWidth: 96.w, logicalHeight: 96.w), warmImageResource(giftPhoto, logicalWidth: 96.w, logicalHeight: 96.w),
); );
final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds);
if (normalizedTargetUserIds.isEmpty) {
return;
}
final normalizedAnimationCount = math.max(animationCount, 1); final normalizedAnimationCount = math.max(animationCount, 1);
final cappedAnimationCount = math.min( final cappedAnimationCount = math.min(
normalizedAnimationCount, normalizedAnimationCount,
_maxTrackedGiftAnimations, _maxTrackedGiftAnimations,
); );
final maxTrackedRequests = math.max(
_maxTrackedGiftAnimations * normalizedTargetUserIds.length,
_maxTrackedGiftAnimations,
);
for (var index = 0; index < cappedAnimationCount; index += 1) { for (var index = 0; index < cappedAnimationCount; index += 1) {
_giftSeatFlightController.enqueueLimited( final roundBatchTag =
RoomGiftSeatFlightRequest( normalizedTargetUserIds.length > 1 ? '$batchKey|round:$index' : null;
imagePath: giftPhoto, for (final targetUserId in normalizedTargetUserIds) {
targetUserId: targetUserId, _giftSeatFlightController.enqueueLimited(
beginSize: 96.w, RoomGiftSeatFlightRequest(
endSize: 28.w, imagePath: giftPhoto,
queueTag: sessionKey, targetUserId: targetUserId,
), beginSize: 96.w,
maxTrackedRequests: _maxTrackedGiftAnimations, endSize: 28.w,
); queueTag: sessionKey,
batchTag: roundBatchTag,
),
maxTrackedRequests: maxTrackedRequests,
);
}
} }
} }
@ -742,6 +796,96 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
}); });
} }
List<String> _resolveGiftTargetUserIds(Msg msg) {
final targetUserIds = <String>[];
void addTargetUserId(String? userId) {
final normalizedUserId = (userId ?? "").trim();
if (normalizedUserId.isEmpty ||
targetUserIds.contains(normalizedUserId)) {
return;
}
targetUserIds.add(normalizedUserId);
}
for (final userId in msg.targetUserIds ?? const <String>[]) {
addTargetUserId(userId);
}
if (msg.type == SCRoomMsgType.luckGiftAnimOther) {
final rawTargetUserIds = (msg.msg ?? "").trim();
if (rawTargetUserIds.startsWith("[")) {
try {
final decoded = jsonDecode(rawTargetUserIds);
if (decoded is List) {
for (final userId in decoded) {
addTargetUserId(userId?.toString());
}
}
} catch (_) {}
}
}
addTargetUserId(_resolveGiftTargetUserId(msg));
return targetUserIds;
}
List<String> _normalizeGiftTargetUserIds(List<String> targetUserIds) {
final normalizedTargetUserIds = <String>[];
for (final targetUserId in targetUserIds) {
final normalizedTargetUserId = targetUserId.trim();
if (normalizedTargetUserId.isEmpty ||
normalizedTargetUserIds.contains(normalizedTargetUserId)) {
continue;
}
normalizedTargetUserIds.add(normalizedTargetUserId);
}
return normalizedTargetUserIds;
}
String? _buildGiftTargetGroupKey(List<String> targetUserIds) {
final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds);
if (normalizedTargetUserIds.isEmpty) {
return null;
}
normalizedTargetUserIds.sort();
return normalizedTargetUserIds.join(",");
}
String _buildGiftFlightBatchKey(Msg msg, List<String> targetUserIds) {
final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds);
normalizedTargetUserIds.sort();
final timeBucket =
((msg.time ?? DateTime.now().millisecondsSinceEpoch) / 650).floor();
return [
msg.type ?? "",
msg.gift?.id ?? "",
msg.user?.id ?? "",
msg.number?.toString() ?? "",
msg.customAnimationCount?.toString() ?? "",
normalizedTargetUserIds.join(","),
timeBucket.toString(),
].join("|");
}
bool _markGiftFlightBatchForPlayback(String batchKey) {
final normalizedBatchKey = batchKey.trim();
if (normalizedBatchKey.isEmpty) {
return true;
}
if (_giftFlightBatchDedupeTimers.containsKey(normalizedBatchKey)) {
return false;
}
late final Timer timer;
timer = Timer(_giftFlightBatchDedupeWindow, () {
if (identical(_giftFlightBatchDedupeTimers[normalizedBatchKey], timer)) {
_giftFlightBatchDedupeTimers.remove(normalizedBatchKey);
}
});
_giftFlightBatchDedupeTimers[normalizedBatchKey] = timer;
return true;
}
String _buildLuckyGiftComboSessionKey(Msg msg, String? targetUserId) { String _buildLuckyGiftComboSessionKey(Msg msg, String? targetUserId) {
final senderId = (msg.user?.id ?? '').trim(); final senderId = (msg.user?.id ?? '').trim();
final giftId = (msg.gift?.id ?? '').trim(); final giftId = (msg.gift?.id ?? '').trim();
@ -755,6 +899,13 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
_luckyGiftComboSessions.clear(); _luckyGiftComboSessions.clear();
} }
void _clearGiftFlightBatchDedupeTimers() {
for (final timer in _giftFlightBatchDedupeTimers.values) {
timer.cancel();
}
_giftFlightBatchDedupeTimers.clear();
}
bool _shouldPlaySeatFlightGiftAnimation(Msg msg) { bool _shouldPlaySeatFlightGiftAnimation(Msg msg) {
final gift = msg.gift; final gift = msg.gift;
if (gift == null) { if (gift == null) {

View File

@ -67,10 +67,6 @@ double resolveRoomGameCompatibilityBottomHeight(BuildContext context) {
if (mediaQuery == null) { if (mediaQuery == null) {
return 0; return 0;
} }
final isClassicScreen = mediaQuery.padding.bottom <= 0.5;
if (!isClassicScreen) {
return 0;
}
return (mediaQuery.size.width * 0.19).clamp(56.0, 88.0).toDouble(); return (mediaQuery.size.width * 0.19).clamp(56.0, 88.0).toDouble();
} }
@ -97,12 +93,7 @@ class RoomGameCompatibilityBottomFrame extends StatelessWidget {
class _RoomGameBottomFramePainter extends CustomPainter { class _RoomGameBottomFramePainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
final radius = Radius.circular(size.height * 0.18); final frameRect = Offset.zero & size;
final frameRect = RRect.fromRectAndCorners(
Offset.zero & size,
topLeft: radius,
topRight: radius,
);
final backgroundPaint = final backgroundPaint =
Paint() Paint()
@ -110,11 +101,11 @@ class _RoomGameBottomFramePainter extends CustomPainter {
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: <Color>[Color(0xFF0E392E), Color(0xFF082A23)], colors: <Color>[Color(0xFF0E392E), Color(0xFF082A23)],
).createShader(Offset.zero & size); ).createShader(frameRect);
canvas.drawRRect(frameRect, backgroundPaint); canvas.drawRect(frameRect, backgroundPaint);
canvas.save(); canvas.save();
canvas.clipRRect(frameRect); canvas.clipRect(frameRect);
final patternPaint = final patternPaint =
Paint() Paint()
..color = const Color(0xFF2C5B4A).withValues(alpha: 0.20) ..color = const Color(0xFF2C5B4A).withValues(alpha: 0.20)
@ -156,8 +147,8 @@ class _RoomGameBottomFramePainter extends CustomPainter {
..style = PaintingStyle.stroke ..style = PaintingStyle.stroke
..strokeWidth = 1; ..strokeWidth = 1;
canvas.drawRRect(frameRect.deflate(1.5), borderPaint); canvas.drawRect(frameRect.deflate(1.5), borderPaint);
canvas.drawRRect(frameRect.deflate(5), innerBorderPaint); canvas.drawRect(frameRect.deflate(5), innerBorderPaint);
final highlightPaint = final highlightPaint =
Paint() Paint()

View File

@ -86,6 +86,7 @@ class _MePage2State extends State<MePage2> {
'badge=${_vipEntryBadgeLogValue(status.badge)}', 'badge=${_vipEntryBadgeLogValue(status.badge)}',
); );
if (!mounted) return; if (!mounted) return;
_syncCurrentProfileVipStatus(status);
setState(() { setState(() {
_vipStatus = status; _vipStatus = status;
_isVipStatusLoading = false; _isVipStatusLoading = false;
@ -113,6 +114,25 @@ class _MePage2State extends State<MePage2> {
return repository.vipStatus(); return repository.vipStatus();
} }
void _syncCurrentProfileVipStatus(SCVipStatusRes status) {
final profileManager = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
);
final profile = profileManager.currentUserProfile;
if (profile == null) return;
final nextVipLevel =
status.active == false || status.levelInt <= 0
? '0'
: status.levelInt.toString();
if (profile.vipLevel == nextVipLevel) return;
profileManager.syncCurrentUserProfile(
profile.copyWith(vipLevel: nextVipLevel),
);
}
void _refreshAfterVipReturn(SocialChatUserProfileManager profileManager) { void _refreshAfterVipReturn(SocialChatUserProfileManager profileManager) {
debugPrint('[VIP][MeEntry] refresh user data after vip page return'); debugPrint('[VIP][MeEntry] refresh user data after vip page return');
profileManager.fetchUserProfileData(loadGuardCount: false); profileManager.fetchUserProfileData(loadGuardCount: false);

View File

@ -10,6 +10,7 @@ import 'package:yumi/shared/tools/sc_permission_utils.dart';
import 'package:yumi/shared/tools/sc_room_utils.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
import 'package:yumi/services/room/rc_room_manager.dart'; import 'package:yumi/services/room/rc_room_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart';
@ -43,6 +44,7 @@ import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart';
import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart'; import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart'; import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart';
import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart'; import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart';
import '../../shared/business_logic/models/res/sc_vip_res.dart';
import '../../shared/tools/sc_room_profile_cache.dart'; import '../../shared/tools/sc_room_profile_cache.dart';
import '../../ui_kit/components/sc_float_ichart.dart'; import '../../ui_kit/components/sc_float_ichart.dart';
@ -247,6 +249,7 @@ class RoomEntryPreviewData {
), ),
roomSetting: roomSetting, roomSetting: roomSetting,
userProfile: ownerProfile, userProfile: ownerProfile,
userSVipLevel: ownerProfile?.vipLevel,
), ),
); );
} }
@ -684,6 +687,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
previous.userNickname == next.userNickname && previous.userNickname == next.userNickname &&
previous.userSex == next.userSex && previous.userSex == next.userSex &&
previous.roles == next.roles && previous.roles == next.roles &&
previous.vipLevel == next.vipLevel &&
previous.heartbeatVal == next.heartbeatVal; previous.heartbeatVal == next.heartbeatVal;
} }
@ -748,7 +752,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
: previousMic?.number, : previousMic?.number,
) )
: roomWheat; : roomWheat;
final normalizedUserId = (mergedMic.user?.id ?? "").trim(); final hydratedMic = _mergeKnownVipIntoMic(mergedMic);
final normalizedUserId = (hydratedMic.user?.id ?? "").trim();
if (normalizedUserId.isNotEmpty) { if (normalizedUserId.isNotEmpty) {
final existingIndex = userSeatMap[normalizedUserId]; final existingIndex = userSeatMap[normalizedUserId];
if (existingIndex != null) { if (existingIndex != null) {
@ -765,7 +770,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
userSeatMap[normalizedUserId] = micIndex; userSeatMap[normalizedUserId] = micIndex;
} }
nextMap[micIndex] = mergedMic; nextMap[micIndex] = hydratedMic;
} }
return _stabilizeSelfMicSnapshot(nextMap, previousMap: previousMap); return _stabilizeSelfMicSnapshot(nextMap, previousMap: previousMap);
} }
@ -2093,7 +2098,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
unawaited(_cleanupStaleEnteredRoom(enteredRoom)); unawaited(_cleanupStaleEnteredRoom(enteredRoom));
return; return;
} }
currenRoom = enteredRoom; currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom);
_syncCurrentUserVipHintFromRoom(currenRoom);
unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial));
_currentRoomIsEntryPreview = false; _currentRoomIsEntryPreview = false;
_previewRoomSeatCount = null; _previewRoomSeatCount = null;
notifyListeners(); notifyListeners();
@ -2112,6 +2119,322 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
} }
JoinRoomRes _mergeVoiceRoomVipHints(
JoinRoomRes enteredRoom,
JoinRoomRes? previewRoom,
) {
final roomProfile = enteredRoom.roomProfile;
if (roomProfile == null) {
return enteredRoom;
}
final enteredOwner = roomProfile.userProfile;
if (_profileHasPositiveVipLevelHint(enteredOwner)) {
return enteredRoom;
}
final roomId = _firstNonBlankText([roomProfile.roomProfile?.id]);
final ownerId = _firstNonBlankText([
roomProfile.roomProfile?.userId,
enteredOwner?.id,
]);
final previewRoomProfile = previewRoom?.roomProfile;
final previewOwner = previewRoomProfile?.userProfile;
final previewRoomId = _firstNonBlankText([
previewRoomProfile?.roomProfile?.id,
]);
final previewOwnerId = _firstNonBlankText([
previewRoomProfile?.roomProfile?.userId,
previewOwner?.id,
]);
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
SocialChatUserProfile? resolvedOwner = enteredOwner;
String? vipLevel;
final canUsePreviewOwner =
previewOwner != null &&
_sameNonBlankId(roomId, previewRoomId) &&
(ownerId == null || _sameNonBlankId(ownerId, previewOwnerId));
if (canUsePreviewOwner) {
vipLevel = _profileVipLevelHint(previewOwner);
resolvedOwner ??= previewOwner;
}
if (vipLevel == null && _sameNonBlankId(ownerId, currentProfile?.id)) {
vipLevel =
_profileVipLevelHint(currentProfile) ??
_roomVipLevelHintFromEntrants(enteredRoom.entrants);
resolvedOwner ??= currentProfile;
}
if (vipLevel == null || resolvedOwner == null) {
return enteredRoom;
}
return enteredRoom.copyWith(
roomProfile: roomProfile.copyWith(
userProfile: resolvedOwner.copyWith(vipLevel: vipLevel),
userSVipLevel: vipLevel,
),
);
}
void _syncCurrentUserVipHintFromRoom(JoinRoomRes? room) {
final currentUser = AccountStorage().getCurrentUser();
final currentProfile = currentUser?.userProfile;
if (currentUser == null ||
currentProfile == null ||
_profileHasPositiveVipLevelHint(currentProfile)) {
return;
}
final ownerProfile = room?.roomProfile?.userProfile;
final ownerId = _firstNonBlankText([
room?.roomProfile?.roomProfile?.userId,
ownerProfile?.id,
]);
String? vipLevel;
if (_sameNonBlankId(ownerId, currentProfile.id)) {
vipLevel = _profileVipLevelHint(ownerProfile);
}
vipLevel ??= _roomVipLevelHintFromEntrants(room?.entrants);
if (vipLevel == null) {
return;
}
AccountStorage().setCurrentUser(
currentUser.copyWith(
userProfile: currentProfile.copyWith(vipLevel: vipLevel),
),
);
}
Future<void> _refreshCurrentUserVipHintForRoom(int entryRequestSerial) async {
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
if (roomId.isEmpty) {
return;
}
final currentUser = AccountStorage().getCurrentUser();
final currentProfile = currentUser?.userProfile;
if (currentUser == null || currentProfile == null) {
return;
}
final existingVipLevel = _positiveProfileVipLevelHint(currentProfile);
final vipLevel =
existingVipLevel ?? await _loadCurrentVipLevelForColoredId();
if (vipLevel == null || vipLevel.isEmpty) {
return;
}
final updatedProfile = currentProfile.copyWith(vipLevel: vipLevel);
if (!_profileHasPositiveVipLevelHint(currentProfile)) {
AccountStorage().setCurrentUser(
currentUser.copyWith(userProfile: updatedProfile),
);
}
if (!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
return;
}
var changed = false;
final mergedRoom = _mergeKnownVipIntoCurrentRoom(
currenRoom,
updatedProfile,
);
if (!identical(mergedRoom, currenRoom)) {
currenRoom = mergedRoom;
changed = true;
}
final mergedOnlineUsers =
onlineUsers
.map((user) => _mergeKnownVipIntoProfile(user) ?? user)
.toList();
if (!_sameOnlineUsers(onlineUsers, mergedOnlineUsers)) {
onlineUsers = mergedOnlineUsers;
_refreshManagerUsers(onlineUsers);
changed = true;
}
final mergedMicMap = roomWheatMap.map(
(index, mic) => MapEntry(index, _mergeKnownVipIntoMic(mic)),
);
if (!_sameMicMaps(roomWheatMap, mergedMicMap)) {
roomWheatMap = mergedMicMap;
changed = true;
}
if (changed) {
notifyListeners();
}
}
Future<String?> _loadCurrentVipLevelForColoredId() async {
final repository = SCVipRepositoryImp();
try {
final home = await repository.vipHome();
final homeVipLevel = _vipLevelFromStatus(home.state);
if (homeVipLevel != null) {
return homeVipLevel;
}
} catch (_) {}
try {
return _vipLevelFromStatus(await repository.vipStatus());
} catch (_) {
return null;
}
}
String? _vipLevelFromStatus(SCVipStatusRes? status) {
if (status == null) {
return null;
}
if (status.active == false) {
return '0';
}
final level = status.levelInt;
return level > 0 ? level.toString() : '0';
}
JoinRoomRes? _mergeKnownVipIntoCurrentRoom(
JoinRoomRes? room,
SocialChatUserProfile currentProfile,
) {
final roomProfile = room?.roomProfile;
final ownerProfile = roomProfile?.userProfile;
if (room == null || roomProfile == null || ownerProfile == null) {
return room;
}
final ownerId = _firstNonBlankText([
roomProfile.roomProfile?.userId,
ownerProfile.id,
]);
if (!_sameNonBlankId(ownerId, currentProfile.id) ||
_profileHasPositiveVipLevelHint(ownerProfile)) {
return room;
}
final vipLevel = _positiveProfileVipLevelHint(currentProfile);
if (vipLevel == null) {
return room;
}
return room.copyWith(
roomProfile: roomProfile.copyWith(
userProfile: ownerProfile.copyWith(vipLevel: vipLevel),
userSVipLevel: vipLevel,
),
);
}
MicRes _mergeKnownVipIntoMic(MicRes mic) {
final mergedUser = _mergeKnownVipIntoProfile(mic.user);
if (identical(mergedUser, mic.user)) {
return mic;
}
return mic.copyWith(user: mergedUser);
}
SocialChatUserProfile? _mergeKnownVipIntoProfile(
SocialChatUserProfile? profile,
) {
if (profile == null || _profileHasPositiveVipLevelHint(profile)) {
return profile;
}
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
final currentVipLevel = _positiveProfileVipLevelHint(currentProfile);
if (_sameNonBlankId(profile.id, currentProfile?.id) &&
currentVipLevel != null) {
return profile.copyWith(vipLevel: currentVipLevel);
}
final ownerProfile = currenRoom?.roomProfile?.userProfile;
final ownerVipLevel = _positiveProfileVipLevelHint(ownerProfile);
final ownerId = _firstNonBlankText([
currenRoom?.roomProfile?.roomProfile?.userId,
ownerProfile?.id,
]);
if (_sameNonBlankId(profile.id, ownerId) && ownerVipLevel != null) {
return profile.copyWith(vipLevel: ownerVipLevel);
}
return profile;
}
bool _profileHasPositiveVipLevelHint(SocialChatUserProfile? profile) {
return _positiveProfileVipLevelHint(profile) != null;
}
String? _profileVipLevelHint(SocialChatUserProfile? profile) {
return _firstNonBlankText([profile?.vipLevel]);
}
String? _positiveProfileVipLevelHint(SocialChatUserProfile? profile) {
final vipLevel = _profileVipLevelHint(profile);
if (profile == null || profile.vipLevelForColoredId <= 0) {
return null;
}
return vipLevel ?? profile.vipLevelForColoredId.toString();
}
String? _roomVipLevelHintFromEntrants(Entrants? entrants) {
final abilities = entrants?.nobleVipAbility ?? const <String>[];
var hasAbility = false;
for (final ability in abilities) {
final text = ability.trim();
final normalized = text.toLowerCase();
if (text.isEmpty || normalized == '0' || normalized == 'false') {
continue;
}
hasAbility = true;
if (_looksLikeVipLevelText(text)) {
return text;
}
}
return hasAbility ? '1' : null;
}
bool _looksLikeVipLevelText(String text) {
final plainLevel = int.tryParse(text);
if ((plainLevel ?? 0) > 0) {
return true;
}
if (RegExp(
r'(?:S?VIP|NOBLE[_\s-]*VIP)[^\d]*[1-9]\d*',
caseSensitive: false,
).hasMatch(text)) {
return true;
}
return RegExp(r'[1-9]\d*$').hasMatch(text);
}
String? _firstNonBlankText(Iterable<dynamic> values) {
for (final value in values) {
final text = value?.toString().trim();
if (text != null && text.isNotEmpty) {
return text;
}
}
return null;
}
bool _sameNonBlankId(dynamic left, dynamic right) {
final leftText = left?.toString().trim();
final rightText = right?.toString().trim();
return leftText != null &&
leftText.isNotEmpty &&
rightText != null &&
rightText.isNotEmpty &&
leftText == rightText;
}
bool _isActiveRoomEntryRequest(int entryRequestSerial) { bool _isActiveRoomEntryRequest(int entryRequestSerial) {
return entryRequestSerial == _roomEntryRequestSerial && return entryRequestSerial == _roomEntryRequestSerial &&
_roomStartupStatus == RoomStartupStatus.loading; _roomStartupStatus == RoomStartupStatus.loading;
@ -2355,7 +2678,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) { !_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
return; return;
} }
SCGiftVapSvgaManager().play(sourceUrl, priority: 100, type: 1); SCGiftVapSvgaManager().play(
sourceUrl,
priority: SCGiftVapSvgaManager.entryEffectPriority,
type: SCGiftVapSvgaManager.entryEffectType,
);
}); });
} }
} }
@ -2405,8 +2732,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (roomId != currenRoom?.roomProfile?.roomProfile?.id) { if (roomId != currenRoom?.roomProfile?.roomProfile?.id) {
return; return;
} }
final changed = !_sameOnlineUsers(onlineUsers, fetchedUsers); final mergedUsers =
onlineUsers = fetchedUsers; fetchedUsers.map((user) => _mergeKnownVipIntoProfile(user)!).toList();
final changed = !_sameOnlineUsers(onlineUsers, mergedUsers);
onlineUsers = mergedUsers;
_refreshManagerUsers(onlineUsers); _refreshManagerUsers(onlineUsers);
if (changed || notifyIfUnchanged) { if (changed || notifyIfUnchanged) {
notifyListeners(); notifyListeners();
@ -3399,15 +3728,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) { if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) {
return; return;
} }
final mergedUser = _mergeKnownVipIntoProfile(user) ?? user;
bool isExtOnlineList = false; bool isExtOnlineList = false;
for (var us in onlineUsers) { for (var us in onlineUsers) {
if (us.id == user.id) { if (us.id == mergedUser.id) {
isExtOnlineList = true; isExtOnlineList = true;
break; break;
} }
} }
if (!isExtOnlineList) { if (!isExtOnlineList) {
Provider.of<RtcProvider>(context!, listen: false).onlineUsers.add(user); Provider.of<RtcProvider>(
context!,
listen: false,
).onlineUsers.add(mergedUser);
_refreshManagerUsers(onlineUsers); _refreshManagerUsers(onlineUsers);
notifyListeners(); notifyListeners();
} }

View File

@ -1517,8 +1517,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
shouldShowRoomVisualEffects) { shouldShowRoomVisualEffects) {
SCGiftVapSvgaManager().play( SCGiftVapSvgaManager().play(
msg.user?.getMountains()?.sourceUrl ?? "", msg.user?.getMountains()?.sourceUrl ?? "",
priority: 100, priority: SCGiftVapSvgaManager.entryEffectPriority,
type: 1, type: SCGiftVapSvgaManager.entryEffectType,
); );
} }
} }

View File

@ -4,11 +4,13 @@ import 'package:yumi/ui_kit/components/sc_tts.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
import 'package:yumi/shared/business_logic/models/req/sc_user_profile_cmd.dart'; import 'package:yumi/shared/business_logic/models/req/sc_user_profile_cmd.dart';
import 'package:yumi/shared/business_logic/models/res/country_res.dart'; import 'package:yumi/shared/business_logic/models/res/country_res.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
class SocialChatUserProfileManager extends ChangeNotifier { class SocialChatUserProfileManager extends ChangeNotifier {
SCUserProfileCmd? editUser; SCUserProfileCmd? editUser;
@ -86,6 +88,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
return; return;
} }
var userInfo = await SCAccountRepository().loadUserInfo(userId); var userInfo = await SCAccountRepository().loadUserInfo(userId);
userInfo = await _withCurrentVipLevel(userInfo, userId);
syncCurrentUserProfile(userInfo); syncCurrentUserProfile(userInfo);
} }
@ -101,7 +104,8 @@ class SocialChatUserProfileManager extends ChangeNotifier {
void getUserInfoById(String userId) async { void getUserInfoById(String userId) async {
userProfile = null; userProfile = null;
userProfile = await SCAccountRepository().loadUserInfo(userId); final loadedProfile = await SCAccountRepository().loadUserInfo(userId);
userProfile = await _withCurrentVipLevel(loadedProfile, userId);
notifyListeners(); notifyListeners();
} }
@ -114,9 +118,85 @@ class SocialChatUserProfileManager extends ChangeNotifier {
void roomUserCard(String roomId, String userId) async { void roomUserCard(String roomId, String userId) async {
userCardInfo = null; userCardInfo = null;
userCardInfo = await SCChatRoomRepository().roomUserCard(roomId, userId); userCardInfo = await SCChatRoomRepository().roomUserCard(roomId, userId);
userCardInfo = await _withCurrentVipLevelForCard(userCardInfo, userId);
notifyListeners(); notifyListeners();
} }
Future<RoomUserCardRes?> _withCurrentVipLevelForCard(
RoomUserCardRes? card,
String userId,
) async {
final profile = card?.userProfile;
if (card == null || profile == null) {
return card;
}
final mergedProfile = await _withCurrentVipLevel(profile, userId);
if (mergedProfile == profile) {
return card;
}
return card.copyWith(userProfile: mergedProfile);
}
Future<SocialChatUserProfile> _withCurrentVipLevel(
SocialChatUserProfile profile,
String userId,
) async {
if (!_isCurrentUserId(userId)) {
return profile;
}
final profileVipLevel = profile.vipLevel?.trim();
if (profileVipLevel != null && profileVipLevel.isNotEmpty) {
return profile;
}
final currentVipLevel = currentUserProfile?.vipLevel?.trim();
final resolvedVipLevel =
currentVipLevel != null && currentVipLevel.isNotEmpty
? currentVipLevel
: await _loadCurrentVipLevel();
if (resolvedVipLevel == null || resolvedVipLevel.isEmpty) {
return profile;
}
return profile.copyWith(vipLevel: resolvedVipLevel);
}
bool _isCurrentUserId(String userId) {
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
return currentUserId != null &&
currentUserId.isNotEmpty &&
currentUserId == userId;
}
Future<String?> _loadCurrentVipLevel() async {
final repository = SCVipRepositoryImp();
try {
final home = await repository.vipHome();
final homeVipLevel = _vipLevelFromStatus(home.state);
if (homeVipLevel != null) {
return homeVipLevel;
}
} catch (_) {}
try {
return _vipLevelFromStatus(await repository.vipStatus());
} catch (_) {
return null;
}
}
String? _vipLevelFromStatus(SCVipStatusRes? status) {
if (status == null) {
return null;
}
if (status.active == false) {
return '0';
}
final level = status.levelInt;
return level > 0 ? level.toString() : '0';
}
void followUser(String userId) async { void followUser(String userId) async {
var result = await SCAccountRepository().followUser(userId); var result = await SCAccountRepository().followUser(userId);
if (result) { if (result) {

View File

@ -53,8 +53,8 @@ class FollowRoomRes {
/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"s","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} /// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"s","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}
/// userSVipLevel : "" /// userSVipLevel : ""
class RoomProfile { class RoomProfile {
RoomProfile({ RoomProfile({
String? countryCode, String? countryCode,
String? countryName, String? countryName,
String? event, String? event,
@ -69,12 +69,12 @@ class RoomProfile {
String? roomGameIcon, String? roomGameIcon,
String? roomName, String? roomName,
String? sysOrigin, String? sysOrigin,
String? userId, String? userId,
SocialChatUserProfile? userProfile, SocialChatUserProfile? userProfile,
String? userSVipLevel, String? userSVipLevel,
RoomMemberCounter? roomCounter, RoomMemberCounter? roomCounter,
ExtValues? extValues, ExtValues? extValues,
}) { }) {
_countryCode = countryCode; _countryCode = countryCode;
_countryName = countryName; _countryName = countryName;
_event = event; _event = event;
@ -89,12 +89,16 @@ class RoomProfile {
_roomGameIcon = roomGameIcon; _roomGameIcon = roomGameIcon;
_roomName = roomName; _roomName = roomName;
_sysOrigin = sysOrigin; _sysOrigin = sysOrigin;
_userId = userId; _userId = userId;
_userProfile = userProfile; _userSVipLevel = userSVipLevel;
_userSVipLevel = userSVipLevel; final normalizedVipLevel = userSVipLevel?.trim();
_roomCounter = roomCounter; _userProfile =
_extValues = extValues; normalizedVipLevel == null || normalizedVipLevel.isEmpty
} ? userProfile
: userProfile?.copyWith(vipLevel: normalizedVipLevel);
_roomCounter = roomCounter;
_extValues = extValues;
}
RoomProfile.fromJson(dynamic json) { RoomProfile.fromJson(dynamic json) {
_countryCode = json['countryCode']; _countryCode = json['countryCode'];
@ -115,18 +119,22 @@ class RoomProfile {
_roomName = json['roomName']; _roomName = json['roomName'];
_sysOrigin = json['sysOrigin']; _sysOrigin = json['sysOrigin'];
_userId = json['userId']; _userId = json['userId'];
_userProfile = _userProfile =
json['userProfile'] != null json['userProfile'] != null
? SocialChatUserProfile.fromJson(json['userProfile']) ? SocialChatUserProfile.fromJson(json['userProfile'])
: null; : null;
_userSVipLevel = json['userSVipLevel']; _userSVipLevel = socialChatVipLevelHintFromJson(json);
final counterJson = json['roomCounter'] ?? json['counter']; final normalizedVipLevel = _userSVipLevel?.trim();
_roomCounter = if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) {
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null; _userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel);
_extValues = }
json['extValues'] != null final counterJson = json['roomCounter'] ?? json['counter'];
? ExtValues.fromJson(json['extValues']) _roomCounter =
: null; counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
_extValues =
json['extValues'] != null
? ExtValues.fromJson(json['extValues'])
: null;
} }
String? _countryCode; String? _countryCode;
String? _countryName; String? _countryName;
@ -142,11 +150,11 @@ class RoomProfile {
String? _roomGameIcon; String? _roomGameIcon;
String? _roomName; String? _roomName;
String? _sysOrigin; String? _sysOrigin;
String? _userId; String? _userId;
SocialChatUserProfile? _userProfile; SocialChatUserProfile? _userProfile;
String? _userSVipLevel; String? _userSVipLevel;
RoomMemberCounter? _roomCounter; RoomMemberCounter? _roomCounter;
ExtValues? _extValues; ExtValues? _extValues;
RoomProfile copyWith({ RoomProfile copyWith({
String? countryCode, String? countryCode,
String? countryName, String? countryName,
@ -162,12 +170,12 @@ class RoomProfile {
String? roomGameIcon, String? roomGameIcon,
String? roomName, String? roomName,
String? sysOrigin, String? sysOrigin,
String? userId, String? userId,
SocialChatUserProfile? userProfile, SocialChatUserProfile? userProfile,
String? userSVipLevel, String? userSVipLevel,
RoomMemberCounter? roomCounter, RoomMemberCounter? roomCounter,
ExtValues? extValues, ExtValues? extValues,
}) => RoomProfile( }) => RoomProfile(
countryCode: countryCode ?? _countryCode, countryCode: countryCode ?? _countryCode,
countryName: countryName ?? _countryName, countryName: countryName ?? _countryName,
event: event ?? _event, event: event ?? _event,
@ -182,12 +190,12 @@ class RoomProfile {
roomGameIcon: roomGameIcon ?? _roomGameIcon, roomGameIcon: roomGameIcon ?? _roomGameIcon,
roomName: roomName ?? _roomName, roomName: roomName ?? _roomName,
sysOrigin: sysOrigin ?? _sysOrigin, sysOrigin: sysOrigin ?? _sysOrigin,
userId: userId ?? _userId, userId: userId ?? _userId,
userProfile: userProfile ?? _userProfile, userProfile: userProfile ?? _userProfile,
userSVipLevel: userSVipLevel ?? _userSVipLevel, userSVipLevel: userSVipLevel ?? _userSVipLevel,
roomCounter: roomCounter ?? _roomCounter, roomCounter: roomCounter ?? _roomCounter,
extValues: extValues ?? _extValues, extValues: extValues ?? _extValues,
); );
String? get countryCode => _countryCode; String? get countryCode => _countryCode;
String? get countryName => _countryName; String? get countryName => _countryName;
String? get event => _event; String? get event => _event;
@ -203,25 +211,25 @@ class RoomProfile {
String? get roomName => _roomName; String? get roomName => _roomName;
String? get sysOrigin => _sysOrigin; String? get sysOrigin => _sysOrigin;
String? get userId => _userId; String? get userId => _userId;
SocialChatUserProfile? get userProfile => _userProfile; SocialChatUserProfile? get userProfile => _userProfile;
String? get userSVipLevel => _userSVipLevel; String? get userSVipLevel => _userSVipLevel;
RoomMemberCounter? get roomCounter => _roomCounter; RoomMemberCounter? get roomCounter => _roomCounter;
ExtValues? get extValues => _extValues; ExtValues? get extValues => _extValues;
String get displayMemberCount { String get displayMemberCount {
final memberCount = roomCounter?.memberCount; final memberCount = roomCounter?.memberCount;
if (memberCount != null) { if (memberCount != null) {
if (memberCount == memberCount.roundToDouble()) { if (memberCount == memberCount.roundToDouble()) {
return memberCount.toInt().toString(); return memberCount.toInt().toString();
} }
return memberCount.toString(); return memberCount.toString();
} }
final memberQuantity = extValues?.memberQuantity; final memberQuantity = extValues?.memberQuantity;
if ((memberQuantity ?? "").trim().isNotEmpty) { if ((memberQuantity ?? "").trim().isNotEmpty) {
return memberQuantity!; return memberQuantity!;
} }
return "0"; return "0";
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final map = <String, dynamic>{}; final map = <String, dynamic>{};
@ -240,16 +248,16 @@ class RoomProfile {
map['roomName'] = _roomName; map['roomName'] = _roomName;
map['sysOrigin'] = _sysOrigin; map['sysOrigin'] = _sysOrigin;
map['userId'] = _userId; map['userId'] = _userId;
if (_userProfile != null) { if (_userProfile != null) {
map['userProfile'] = _userProfile?.toJson(); map['userProfile'] = _userProfile?.toJson();
} }
map['userSVipLevel'] = _userSVipLevel; map['userSVipLevel'] = _userSVipLevel;
if (_roomCounter != null) { if (_roomCounter != null) {
map['roomCounter'] = _roomCounter?.toJson(); map['roomCounter'] = _roomCounter?.toJson();
} }
if (_extValues != null) { if (_extValues != null) {
map['extValues'] = _extValues?.toJson(); map['extValues'] = _extValues?.toJson();
} }
return map; return map;
} }
} }

View File

@ -17,17 +17,31 @@ class JoinRoomRes {
_roomProps = roomProps; _roomProps = roomProps;
} }
JoinRoomRes.fromJson(dynamic json) { JoinRoomRes.fromJson(dynamic json) {
_entrants = _entrants =
json['entrants'] != null ? Entrants.fromJson(json['entrants']) : null; json['entrants'] != null ? Entrants.fromJson(json['entrants']) : null;
_roomProfile = _roomProfile =
json['roomProfile'] != null json['roomProfile'] != null
? RoomProfile.fromJson(json['roomProfile']) ? RoomProfile.fromJson(json['roomProfile'])
: null; : null;
_roomProps = final roomVipLevel = _firstNonBlankRoomVipLevel([
json['roomProps'] != null json['userSVipLevel'],
? RoomProps.fromJson(json['roomProps']) json['supperVip'],
: null; json['superVip'],
json['vipLevel'],
json['svipLevel'],
json['sVipLevel'],
json['nobleVipLevel'],
json['vipLevelCode'],
json['levelCode'],
]);
if (roomVipLevel != null) {
_roomProfile = _roomProfile?.copyWith(userSVipLevel: roomVipLevel);
}
_roomProps =
json['roomProps'] != null
? RoomProps.fromJson(json['roomProps'])
: null;
} }
Entrants? _entrants; Entrants? _entrants;
@ -62,13 +76,23 @@ class JoinRoomRes {
map['roomProps'] = _roomProps?.toJson(); map['roomProps'] = _roomProps?.toJson();
} }
return map; return map;
} }
} }
/// layoutCode : "" String? _firstNonBlankRoomVipLevel(Iterable<dynamic> values) {
/// roomTheme : {"expireTime":0,"id":0,"themeBack":"","themeStatus":"","useTheme":false} for (final value in values) {
final text = value?.toString().trim();
class RoomProps { if (text != null && text.isNotEmpty) {
return text;
}
}
return null;
}
/// layoutCode : ""
/// roomTheme : {"expireTime":0,"id":0,"themeBack":"","themeStatus":"","useTheme":false}
class RoomProps {
RoomProps({String? layoutCode, RoomTheme? roomTheme}) { RoomProps({String? layoutCode, RoomTheme? roomTheme}) {
_layoutCode = layoutCode; _layoutCode = layoutCode;
_roomTheme = roomTheme; _roomTheme = roomTheme;
@ -181,26 +205,33 @@ class RoomTheme {
/// regionCode : "" /// regionCode : ""
/// roomCounter : {"adminCount":0,"memberCount":0} /// roomCounter : {"adminCount":0,"memberCount":0}
/// roomProfile : {"activeTime":0,"countryCode":"","countryName":"","createTime":0,"del":false,"event":"","id":"0","langCode":"","nationalFlag":"","roomAccount":"","roomCover":"","roomDesc":"","roomName":"","sysOrigin":"","updateTime":0,"userId":"0"} /// roomProfile : {"activeTime":0,"countryCode":"","countryName":"","createTime":0,"del":false,"event":"","id":"0","langCode":"","nationalFlag":"","roomAccount":"","roomCover":"","roomDesc":"","roomName":"","sysOrigin":"","updateTime":0,"userId":"0"}
/// roomSetting : {"adminLockSeat":false,"allowMusic":false,"joinGolds":0,"lockLocation":"","maxAdmin":0,"maxMember":0,"mikeSize":0,"openKtvMode":false,"password":"","roomId":0,"roomSpecialMikeType":"","showHeartbeat":false,"takeMicRole":"","touristMike":false,"touristMsg":false,"userId":"0"} /// roomSetting : {"adminLockSeat":false,"allowMusic":false,"joinGolds":0,"lockLocation":"","maxAdmin":0,"maxMember":0,"mikeSize":0,"openKtvMode":false,"password":"","roomId":0,"roomSpecialMikeType":"","showHeartbeat":false,"takeMicRole":"","touristMike":false,"touristMsg":false,"userId":"0"}
/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} /// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}
/// userSVipLevel : ""
class RoomProfile {
RoomProfile({ class RoomProfile {
String? regionCode, RoomProfile({
RoomCounter? roomCounter, String? regionCode,
RoomProfile2? roomProfile, RoomCounter? roomCounter,
RoomSetting? roomSetting, RoomProfile2? roomProfile,
SocialChatUserProfile? userProfile, RoomSetting? roomSetting,
}) { SocialChatUserProfile? userProfile,
_regionCode = regionCode; String? userSVipLevel,
_roomCounter = roomCounter; }) {
_roomProfile = roomProfile; _regionCode = regionCode;
_roomSetting = roomSetting; _roomCounter = roomCounter;
_userProfile = userProfile; _roomProfile = roomProfile;
} _roomSetting = roomSetting;
_userSVipLevel = userSVipLevel;
RoomProfile.fromJson(dynamic json) { final normalizedVipLevel = userSVipLevel?.trim();
_regionCode = json['regionCode']; _userProfile =
normalizedVipLevel == null || normalizedVipLevel.isEmpty
? userProfile
: userProfile?.copyWith(vipLevel: normalizedVipLevel);
}
RoomProfile.fromJson(dynamic json) {
_regionCode = json['regionCode'];
_roomCounter = _roomCounter =
json['roomCounter'] != null json['roomCounter'] != null
? RoomCounter.fromJson(json['roomCounter']) ? RoomCounter.fromJson(json['roomCounter'])
@ -213,31 +244,52 @@ class RoomProfile {
json['roomSetting'] != null json['roomSetting'] != null
? RoomSetting.fromJson(json['roomSetting']) ? RoomSetting.fromJson(json['roomSetting'])
: null; : null;
_userProfile = _userProfile =
json['userProfile'] != null json['userProfile'] != null
? SocialChatUserProfile.fromJson(json['userProfile']) ? SocialChatUserProfile.fromJson(json['userProfile'])
: null; : null;
} _userSVipLevel = _firstNonBlankRoomVipLevel([
json['userSVipLevel'],
String? _regionCode; json['supperVip'],
RoomCounter? _roomCounter; json['superVip'],
RoomProfile2? _roomProfile; json['vipLevel'],
RoomSetting? _roomSetting; json['svipLevel'],
SocialChatUserProfile? _userProfile; json['sVipLevel'],
json['nobleVipLevel'],
RoomProfile copyWith({ json['vipLevelCode'],
String? regionCode, json['levelCode'],
RoomCounter? roomCounter, json['roomProfile'] is Map ? json['roomProfile']['userSVipLevel'] : null,
RoomProfile2? roomProfile, json['roomProfile'] is Map ? json['roomProfile']['supperVip'] : null,
RoomSetting? roomSetting, json['roomProfile'] is Map ? json['roomProfile']['vipLevel'] : null,
SocialChatUserProfile? userProfile, ]);
}) => RoomProfile( final normalizedVipLevel = _userSVipLevel?.trim();
regionCode: regionCode ?? _regionCode, if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) {
roomCounter: roomCounter ?? _roomCounter, _userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel);
roomProfile: roomProfile ?? _roomProfile, }
roomSetting: roomSetting ?? _roomSetting, }
userProfile: userProfile ?? _userProfile,
); String? _regionCode;
RoomCounter? _roomCounter;
RoomProfile2? _roomProfile;
RoomSetting? _roomSetting;
SocialChatUserProfile? _userProfile;
String? _userSVipLevel;
RoomProfile copyWith({
String? regionCode,
RoomCounter? roomCounter,
RoomProfile2? roomProfile,
RoomSetting? roomSetting,
SocialChatUserProfile? userProfile,
String? userSVipLevel,
}) => RoomProfile(
regionCode: regionCode ?? _regionCode,
roomCounter: roomCounter ?? _roomCounter,
roomProfile: roomProfile ?? _roomProfile,
roomSetting: roomSetting ?? _roomSetting,
userProfile: userProfile ?? _userProfile,
userSVipLevel: userSVipLevel ?? _userSVipLevel,
);
String? get regionCode => _regionCode; String? get regionCode => _regionCode;
@ -247,11 +299,13 @@ class RoomProfile {
RoomSetting? get roomSetting => _roomSetting; RoomSetting? get roomSetting => _roomSetting;
SocialChatUserProfile? get userProfile => _userProfile; SocialChatUserProfile? get userProfile => _userProfile;
Map<String?, dynamic> toJson() { String? get userSVipLevel => _userSVipLevel;
final map = <String?, dynamic>{};
map['regionCode'] = _regionCode; Map<String?, dynamic> toJson() {
final map = <String?, dynamic>{};
map['regionCode'] = _regionCode;
if (_roomCounter != null) { if (_roomCounter != null) {
map['roomCounter'] = _roomCounter?.toJson(); map['roomCounter'] = _roomCounter?.toJson();
} }
@ -261,11 +315,12 @@ class RoomProfile {
if (_roomSetting != null) { if (_roomSetting != null) {
map['roomSetting'] = _roomSetting?.toJson(); map['roomSetting'] = _roomSetting?.toJson();
} }
if (_userProfile != null) { if (_userProfile != null) {
map['userProfile'] = _userProfile?.toJson(); map['userProfile'] = _userProfile?.toJson();
} }
return map; map['userSVipLevel'] = _userSVipLevel;
} return map;
}
void setRoomSetting(RoomSetting roomSetting) { void setRoomSetting(RoomSetting roomSetting) {
_roomSetting = roomSetting; _roomSetting = roomSetting;

View File

@ -121,6 +121,7 @@ class SocialChatUserProfile {
num? heartbeatVal, num? heartbeatVal,
String? originSys, String? originSys,
OwnSpecialId? ownSpecialId, OwnSpecialId? ownSpecialId,
String? vipLevel,
bool? sameRegion, bool? sameRegion,
int? firstRechargeEndTime, int? firstRechargeEndTime,
String? sysOriginChild, String? sysOriginChild,
@ -160,6 +161,7 @@ class SocialChatUserProfile {
_wealthLevel = wealthLevel; _wealthLevel = wealthLevel;
_heartbeatVal = heartbeatVal; _heartbeatVal = heartbeatVal;
_ownSpecialId = ownSpecialId; _ownSpecialId = ownSpecialId;
_vipLevel = vipLevel;
_sameRegion = sameRegion; _sameRegion = sameRegion;
_isCpRelation = isCpRelation; _isCpRelation = isCpRelation;
_firstRechargeEndTime = firstRechargeEndTime; _firstRechargeEndTime = firstRechargeEndTime;
@ -178,10 +180,14 @@ class SocialChatUserProfile {
_backgroundPhotos = backgroundPhotos; _backgroundPhotos = backgroundPhotos;
_personalPhotos = personalPhotos; _personalPhotos = personalPhotos;
_cpList = cpList; _cpList = cpList;
} }
SocialChatUserProfile.fromJson(dynamic json) { SocialChatUserProfile.fromJson(dynamic json) {
_account = json['account']; final sourceJson = json;
if (sourceJson is Map && sourceJson['userProfile'] is Map) {
json = sourceJson['userProfile'];
}
_account = json['account'];
_accountStatus = json['accountStatus']; _accountStatus = json['accountStatus'];
_age = json['age']; _age = json['age'];
_bornDay = json['bornDay']; _bornDay = json['bornDay'];
@ -204,6 +210,10 @@ class SocialChatUserProfile {
json['ownSpecialId'] != null json['ownSpecialId'] != null
? OwnSpecialId.fromJson(json['ownSpecialId']) ? OwnSpecialId.fromJson(json['ownSpecialId'])
: null; : null;
_vipLevel = _firstNonBlankString([
..._vipLevelHintValues(json),
..._vipLevelHintValues(sourceJson),
]);
_sameRegion = json['sameRegion']; _sameRegion = json['sameRegion'];
_isCpRelation = json['isCpRelation']; _isCpRelation = json['isCpRelation'];
_firstRechargeEndTime = json['firstRechargeEndTime']; _firstRechargeEndTime = json['firstRechargeEndTime'];
@ -274,6 +284,7 @@ class SocialChatUserProfile {
num? _wealthLevel; num? _wealthLevel;
num? _heartbeatVal; num? _heartbeatVal;
OwnSpecialId? _ownSpecialId; OwnSpecialId? _ownSpecialId;
String? _vipLevel;
bool? _sameRegion; bool? _sameRegion;
bool? _isCpRelation; bool? _isCpRelation;
int? _firstRechargeEndTime; int? _firstRechargeEndTime;
@ -314,6 +325,7 @@ class SocialChatUserProfile {
num? wealthLevel, num? wealthLevel,
num? heartbeatVal, num? heartbeatVal,
OwnSpecialId? ownSpecialId, OwnSpecialId? ownSpecialId,
String? vipLevel,
bool? sameRegion, bool? sameRegion,
bool? isCpRelation, bool? isCpRelation,
int? firstRechargeEndTime, int? firstRechargeEndTime,
@ -353,6 +365,7 @@ class SocialChatUserProfile {
wealthLevel: wealthLevel ?? _wealthLevel, wealthLevel: wealthLevel ?? _wealthLevel,
heartbeatVal: heartbeatVal ?? _heartbeatVal, heartbeatVal: heartbeatVal ?? _heartbeatVal,
ownSpecialId: ownSpecialId ?? _ownSpecialId, ownSpecialId: ownSpecialId ?? _ownSpecialId,
vipLevel: vipLevel ?? _vipLevel,
sameRegion: sameRegion ?? _sameRegion, sameRegion: sameRegion ?? _sameRegion,
isCpRelation: isCpRelation ?? _isCpRelation, isCpRelation: isCpRelation ?? _isCpRelation,
firstRechargeEndTime: firstRechargeEndTime ?? _firstRechargeEndTime, firstRechargeEndTime: firstRechargeEndTime ?? _firstRechargeEndTime,
@ -413,6 +426,8 @@ class SocialChatUserProfile {
OwnSpecialId? get ownSpecialId => _ownSpecialId; OwnSpecialId? get ownSpecialId => _ownSpecialId;
String? get vipLevel => _vipLevel;
bool? get sameRegion => _sameRegion; bool? get sameRegion => _sameRegion;
bool? get isCpRelation => _isCpRelation; bool? get isCpRelation => _isCpRelation;
@ -494,6 +509,26 @@ class SocialChatUserProfile {
return pr; return pr;
} }
PropsResources? getFloatPicture() {
PropsResources? pr;
_useProps?.forEach((value) {
if (value.propsResources?.type == SCPropsType.FLOAT_PICTURE.name) {
pr = value.propsResources;
}
});
return pr;
}
PropsResources? getVipBadge() {
PropsResources? pr;
_useProps?.forEach((value) {
if (value.propsResources?.type == SCPropsType.BADGE.name) {
pr = value.propsResources;
}
});
return pr;
}
PropsResources? getMountains() { PropsResources? getMountains() {
PropsResources? pr; PropsResources? pr;
_useProps?.forEach((value) { _useProps?.forEach((value) {
@ -547,6 +582,9 @@ class SocialChatUserProfile {
if (_ownSpecialId != null) { if (_ownSpecialId != null) {
map['ownSpecialId'] = _ownSpecialId?.toJson(); map['ownSpecialId'] = _ownSpecialId?.toJson();
} }
if (_vipLevel != null) {
map['vipLevel'] = _vipLevel;
}
map['sameRegion'] = _sameRegion; map['sameRegion'] = _sameRegion;
map['isCpRelation'] = _isCpRelation; map['isCpRelation'] = _isCpRelation;
map['firstRecharge'] = _firstRecharge; map['firstRecharge'] = _firstRecharge;
@ -591,22 +629,212 @@ class SocialChatUserProfile {
return false; return false;
} }
/// ID VIP int get vipLevelForColoredId {
/// useProps NOBLE_VIP final hasExplicitVipLevel = _vipLevel?.trim().isNotEmpty ?? false;
bool hasVipPrivilegeForColoredId() { final explicitLevel = _parseVipLevelForColoredId(_vipLevel) ?? 0;
return _useProps?.any( if (hasExplicitVipLevel && explicitLevel <= 0) {
(value) => value.propsResources?.type == SCPropsType.NOBLE_VIP.name, return 0;
) ?? }
false; if (explicitLevel > 0) {
} return explicitLevel;
}
bool shouldShowColoredSpecialIdText() { final vipResource = getVIP();
return hasVipPrivilegeForColoredId(); final resourceLevel =
} _parseVipLevelForColoredId(
vipResource?.name,
allowPlainNumber: false,
) ??
_parseVipLevelForColoredId(
vipResource?.code,
allowPlainNumber: false,
allowTrailingNumber: false,
);
if ((resourceLevel ?? 0) > 0) {
return resourceLevel!;
}
return vipResource == null ? 0 : 1;
}
int get vipVisualResourceLevelForColoredId {
for (final resource in [
getVipBadge(),
getHeaddress(),
getChatBox(),
getFloatPicture(),
getDataCard(),
getMountains(),
]) {
final level = _vipLevelFromVisualResource(resource);
if ((level ?? 0) > 0) {
return level!;
}
}
return 0;
}
/// ID VIP
/// VIP useProps NOBLE_VIP
bool hasVipPrivilegeForColoredId() {
return vipLevelForColoredId > 0;
}
bool hasVipVisualResourceForColoredId() {
return vipVisualResourceLevelForColoredId > 0;
}
bool shouldShowColoredSpecialIdText() {
return hasVipPrivilegeForColoredId() || hasVipVisualResourceForColoredId();
}
void setHeartbeatVal(num? heartbeatVal) { void setHeartbeatVal(num? heartbeatVal) {
_heartbeatVal = (_heartbeatVal ?? 0) + (heartbeatVal ?? 0); _heartbeatVal = (_heartbeatVal ?? 0) + (heartbeatVal ?? 0);
} }
}
String? socialChatVipLevelHintFromJson(dynamic json) {
return _firstNonBlankString(_vipLevelHintValues(json));
}
SocialChatUserProfile? socialChatProfileWithVipLevelHint(
SocialChatUserProfile? profile,
dynamic json,
) {
final vipLevel = socialChatVipLevelHintFromJson(json);
if (profile == null || vipLevel == null) {
return profile;
}
return profile.copyWith(vipLevel: vipLevel);
}
SocialChatUserProfile? socialChatUserProfileFromJsonWithVipHint(
dynamic json, {
String userProfileKey = 'userProfile',
}) {
if (json == null) {
return null;
}
if (json is Map && json[userProfileKey] != null) {
return socialChatProfileWithVipLevelHint(
SocialChatUserProfile.fromJson(json[userProfileKey]),
json,
);
}
return SocialChatUserProfile.fromJson(json);
}
Iterable<dynamic> _vipLevelHintValues(dynamic json) sync* {
if (json is! Map) {
return;
}
yield json['vipLevel'];
yield json['svipLevel'];
yield json['sVipLevel'];
yield json['userSVipLevel'];
yield json['supperVip'];
yield json['superVip'];
yield json['nobleVipLevel'];
yield json['vipLevelCode'];
yield json['levelCode'];
yield _nestedVipLevel(json['vipStatus']);
yield _nestedVipLevel(json['vipState']);
yield _nestedVipLevel(json['vip']);
final nestedProfile = json['userProfile'];
if (nestedProfile is Map) {
yield* _vipLevelHintValues(nestedProfile);
}
}
String? _firstNonBlankString(Iterable<dynamic> values) {
for (final value in values) {
final text = value?.toString().trim();
if (text != null && text.isNotEmpty) {
return text;
}
}
return null;
}
dynamic _nestedVipLevel(dynamic value) {
if (value is Map) {
final active = value['active'];
if (active == false ||
active?.toString().trim().toLowerCase() == 'false') {
return '0';
}
return value['level'] ??
value['vipLevel'] ??
value['userSVipLevel'] ??
value['supperVip'] ??
value['levelCode'];
}
return null;
}
int? _vipLevelFromVisualResource(PropsResources? resource) {
if (resource == null) {
return null;
}
return _parseVipLevelForColoredId(
resource.name,
allowPlainNumber: false,
) ??
_parseVipLevelForColoredId(
resource.code,
allowPlainNumber: false,
) ??
_parseVipLevelForColoredId(
resource.cover,
allowPlainNumber: false,
) ??
_parseVipLevelForColoredId(
resource.sourceUrl,
allowPlainNumber: false,
) ??
_parseVipLevelForColoredId(
resource.roomOpenedUrl,
allowPlainNumber: false,
) ??
_parseVipLevelForColoredId(
resource.roomSendCoverUrl,
allowPlainNumber: false,
);
}
int? _parseVipLevelForColoredId(
dynamic value, {
bool allowPlainNumber = true,
bool allowTrailingNumber = true,
}) {
final text = value?.toString().trim();
if (text == null || text.isEmpty) {
return null;
}
if (allowPlainNumber) {
final plainLevel = int.tryParse(text);
if ((plainLevel ?? 0) > 0) {
return plainLevel;
}
}
final vipLevelMatch = RegExp(
r'(?:S?VIP|NOBLE[_\s-]*VIP)[^\d]*([1-9]\d*)',
caseSensitive: false,
).firstMatch(text);
if (vipLevelMatch != null) {
return int.tryParse(vipLevelMatch.group(1) ?? '');
}
if (allowTrailingNumber) {
final trailingLevelMatch = RegExp(r'([1-9]\d*)$').firstMatch(text);
if (trailingLevelMatch != null) {
return int.tryParse(trailingLevelMatch.group(1) ?? '');
}
}
return null;
} }
/// animationUrl : "" /// animationUrl : ""
@ -992,11 +1220,11 @@ class PropsResources {
PropsResources.fromJson(dynamic json) { PropsResources.fromJson(dynamic json) {
_amount = json['amount']; _amount = json['amount'];
_code = json['code']; _code = json['code'];
_cover = json['cover']; _cover = json['cover'] ?? json['coverUrl'];
_createTime = json['createTime']; _createTime = json['createTime'];
_del = json['del']; _del = json['del'];
_expand = json['expand']; _expand = json['expand'];
_id = json['id']; _id = json['id'] ?? json['resourceId'];
_imNotOpenedUrl = json['imNotOpenedUrl']; _imNotOpenedUrl = json['imNotOpenedUrl'];
_imOpenedUrl = json['imOpenedUrl']; _imOpenedUrl = json['imOpenedUrl'];
_imOpenedUrlTwo = json['imOpenedUrlTwo']; _imOpenedUrlTwo = json['imOpenedUrlTwo'];
@ -1005,7 +1233,7 @@ class PropsResources {
_roomNotOpenedUrl = json['roomNotOpenedUrl']; _roomNotOpenedUrl = json['roomNotOpenedUrl'];
_roomOpenedUrl = json['roomOpenedUrl']; _roomOpenedUrl = json['roomOpenedUrl'];
_roomSendCoverUrl = json['roomSendCoverUrl']; _roomSendCoverUrl = json['roomSendCoverUrl'];
_sourceUrl = json['sourceUrl']; _sourceUrl = json['sourceUrl'] ?? json['resourceUrl'] ?? json['url'];
_sysOrigin = json['sysOrigin']; _sysOrigin = json['sysOrigin'];
_type = json['type']; _type = json['type'];
_updateTime = json['updateTime']; _updateTime = json['updateTime'];

View File

@ -34,11 +34,14 @@ class MicRes {
_roomId = json['roomId']; _roomId = json['roomId'];
_micIndex = json['micIndex']; _micIndex = json['micIndex'];
_micLock = json['micLock']; _micLock = json['micLock'];
_micMute = json['micMute']; _micMute = json['micMute'];
_user = _user =
json['user'] != null json['user'] != null
? SocialChatUserProfile.fromJson(json['user']) ? socialChatProfileWithVipLevelHint(
: null; SocialChatUserProfile.fromJson(json['user']),
json,
)
: null;
_roomToken = json['roomToken']; _roomToken = json['roomToken'];
_type = json['type']; _type = json['type'];
_number = json['number']; _number = json['number'];
@ -84,13 +87,15 @@ class MicRes {
bool? get micLock => _micLock; bool? get micLock => _micLock;
bool? get micMute => _micMute; bool? get micMute => _micMute;
SocialChatUserProfile? get user => _user; SocialChatUserProfile? get user => _user;
String? get roomToken => _roomToken; String? get roomToken => _roomToken;
String? get type => _type; int? get volume => _volume;
String? get type => _type;
String? get emojiPath => _emojiPath; String? get emojiPath => _emojiPath;

View File

@ -4,21 +4,24 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
/// totalFormat : "" /// totalFormat : ""
/// total : 0 /// total : 0
class RoomGiftRankRes { class RoomGiftRankRes {
RoomGiftRankRes({ RoomGiftRankRes({
SocialChatUserProfile? userProfile, SocialChatUserProfile? userProfile,
String? totalFormat, String? totalFormat,
String? total,}){ String? total,}){
_userProfile = userProfile; _userProfile = userProfile;
_totalFormat = totalFormat; _totalFormat = totalFormat;
_total = total; _total = total;
} }
RoomGiftRankRes.fromJson(dynamic json) { RoomGiftRankRes.fromJson(dynamic json) {
_userProfile = json['userProfile'] != null ? SocialChatUserProfile.fromJson(json['userProfile']) : null; _userProfile =
_totalFormat = json['totalFormat']; json['userProfile'] != null
_total = json['total']; ? socialChatUserProfileFromJsonWithVipHint(json)
} : null;
_totalFormat = json['totalFormat'];
_total = json['total'];
}
SocialChatUserProfile? _userProfile; SocialChatUserProfile? _userProfile;
String? _totalFormat; String? _totalFormat;
String? _total; String? _total;

View File

@ -11,14 +11,14 @@ class SocialChatRoomMemberRes {
_roles = roles; _roles = roles;
} }
SocialChatRoomMemberRes.fromJson(dynamic json) { SocialChatRoomMemberRes.fromJson(dynamic json) {
_id = json['id']; _id = json['id'];
_userProfile = _userProfile =
json['userProfile'] != null json['userProfile'] != null
? SocialChatUserProfile.fromJson(json['userProfile']) ? socialChatUserProfileFromJsonWithVipHint(json)
: null; : null;
_roles = json['roles']; _roles = json['roles'];
} }
String? _id; String? _id;
SocialChatUserProfile? _userProfile; SocialChatUserProfile? _userProfile;

View File

@ -18,8 +18,8 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":0,"originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} /// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":0,"originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}
/// userSVipLevel : "" /// userSVipLevel : ""
class SocialChatRoomRes { class SocialChatRoomRes {
SocialChatRoomRes({ SocialChatRoomRes({
String? countryCode, String? countryCode,
String? countryName, String? countryName,
String? event, String? event,
@ -34,12 +34,12 @@ class SocialChatRoomRes {
String? roomGameIcon, String? roomGameIcon,
String? roomName, String? roomName,
String? sysOrigin, String? sysOrigin,
String? userId, String? userId,
SocialChatUserProfile? userProfile, SocialChatUserProfile? userProfile,
String? userSVipLevel, String? userSVipLevel,
RoomMemberCounter? roomCounter, RoomMemberCounter? roomCounter,
ExtValues? extValues, ExtValues? extValues,
}) { }) {
_countryCode = countryCode; _countryCode = countryCode;
_countryName = countryName; _countryName = countryName;
_event = event; _event = event;
@ -54,12 +54,16 @@ class SocialChatRoomRes {
_roomGameIcon = roomGameIcon; _roomGameIcon = roomGameIcon;
_roomName = roomName; _roomName = roomName;
_sysOrigin = sysOrigin; _sysOrigin = sysOrigin;
_userId = userId; _userId = userId;
_userProfile = userProfile; _userSVipLevel = userSVipLevel;
_userSVipLevel = userSVipLevel; final normalizedVipLevel = userSVipLevel?.trim();
_roomCounter = roomCounter; _userProfile =
_extValues = extValues; normalizedVipLevel == null || normalizedVipLevel.isEmpty
} ? userProfile
: userProfile?.copyWith(vipLevel: normalizedVipLevel);
_roomCounter = roomCounter;
_extValues = extValues;
}
SocialChatRoomRes.fromJson(dynamic json) { SocialChatRoomRes.fromJson(dynamic json) {
_countryCode = json['countryCode']; _countryCode = json['countryCode'];
@ -82,18 +86,22 @@ class SocialChatRoomRes {
_roomName = json['roomName']; _roomName = json['roomName'];
_sysOrigin = json['sysOrigin']; _sysOrigin = json['sysOrigin'];
_userId = json['userId']; _userId = json['userId'];
_userProfile = _userProfile =
json['userProfile'] != null json['userProfile'] != null
? SocialChatUserProfile.fromJson(json['userProfile']) ? SocialChatUserProfile.fromJson(json['userProfile'])
: null; : null;
_userSVipLevel = json['userSVipLevel']; _userSVipLevel = socialChatVipLevelHintFromJson(json);
final counterJson = json['roomCounter'] ?? json['counter']; final normalizedVipLevel = _userSVipLevel?.trim();
_roomCounter = if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) {
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null; _userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel);
_extValues = }
json['extValues'] != null final counterJson = json['roomCounter'] ?? json['counter'];
? ExtValues.fromJson(json['extValues']) _roomCounter =
: null; counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
_extValues =
json['extValues'] != null
? ExtValues.fromJson(json['extValues'])
: null;
} }
String? _countryCode; String? _countryCode;
@ -110,11 +118,11 @@ class SocialChatRoomRes {
String? _roomGameIcon; String? _roomGameIcon;
String? _roomName; String? _roomName;
String? _sysOrigin; String? _sysOrigin;
String? _userId; String? _userId;
SocialChatUserProfile? _userProfile; SocialChatUserProfile? _userProfile;
String? _userSVipLevel; String? _userSVipLevel;
RoomMemberCounter? _roomCounter; RoomMemberCounter? _roomCounter;
ExtValues? _extValues; ExtValues? _extValues;
SocialChatRoomRes copyWith({ SocialChatRoomRes copyWith({
String? countryCode, String? countryCode,
@ -131,12 +139,12 @@ class SocialChatRoomRes {
String? roomGameIcon, String? roomGameIcon,
String? roomName, String? roomName,
String? sysOrigin, String? sysOrigin,
String? userId, String? userId,
SocialChatUserProfile? userProfile, SocialChatUserProfile? userProfile,
String? userSVipLevel, String? userSVipLevel,
RoomMemberCounter? roomCounter, RoomMemberCounter? roomCounter,
ExtValues? extValues, ExtValues? extValues,
}) => SocialChatRoomRes( }) => SocialChatRoomRes(
countryCode: countryCode ?? _countryCode, countryCode: countryCode ?? _countryCode,
countryName: countryName ?? _countryName, countryName: countryName ?? _countryName,
event: event ?? _event, event: event ?? _event,
@ -151,12 +159,12 @@ class SocialChatRoomRes {
roomGameIcon: roomGameIcon ?? _roomGameIcon, roomGameIcon: roomGameIcon ?? _roomGameIcon,
roomName: roomName ?? _roomName, roomName: roomName ?? _roomName,
sysOrigin: sysOrigin ?? _sysOrigin, sysOrigin: sysOrigin ?? _sysOrigin,
userId: userId ?? _userId, userId: userId ?? _userId,
userProfile: userProfile ?? _userProfile, userProfile: userProfile ?? _userProfile,
userSVipLevel: userSVipLevel ?? _userSVipLevel, userSVipLevel: userSVipLevel ?? _userSVipLevel,
roomCounter: roomCounter ?? _roomCounter, roomCounter: roomCounter ?? _roomCounter,
extValues: extValues ?? _extValues, extValues: extValues ?? _extValues,
); );
String? get countryCode => _countryCode; String? get countryCode => _countryCode;
@ -188,25 +196,25 @@ class SocialChatRoomRes {
String? get userId => _userId; String? get userId => _userId;
SocialChatUserProfile? get userProfile => _userProfile; SocialChatUserProfile? get userProfile => _userProfile;
String? get userSVipLevel => _userSVipLevel; String? get userSVipLevel => _userSVipLevel;
RoomMemberCounter? get roomCounter => _roomCounter; RoomMemberCounter? get roomCounter => _roomCounter;
ExtValues? get extValues => _extValues; ExtValues? get extValues => _extValues;
String get displayMemberCount { String get displayMemberCount {
final memberCount = roomCounter?.memberCount; final memberCount = roomCounter?.memberCount;
if (memberCount != null) { if (memberCount != null) {
return _formatRoomMemberCount(memberCount); return _formatRoomMemberCount(memberCount);
} }
final memberQuantity = extValues?.memberQuantity; final memberQuantity = extValues?.memberQuantity;
if ((memberQuantity ?? "").trim().isNotEmpty) { if ((memberQuantity ?? "").trim().isNotEmpty) {
return memberQuantity!; return memberQuantity!;
} }
return "0"; return "0";
} }
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final map = <String, dynamic>{}; final map = <String, dynamic>{};
@ -225,58 +233,58 @@ class SocialChatRoomRes {
map['roomName'] = _roomName; map['roomName'] = _roomName;
map['sysOrigin'] = _sysOrigin; map['sysOrigin'] = _sysOrigin;
map['userId'] = _userId; map['userId'] = _userId;
if (_userProfile != null) { if (_userProfile != null) {
map['userProfile'] = _userProfile?.toJson(); map['userProfile'] = _userProfile?.toJson();
} }
map['userSVipLevel'] = _userSVipLevel; map['userSVipLevel'] = _userSVipLevel;
if (_roomCounter != null) { if (_roomCounter != null) {
map['roomCounter'] = _roomCounter?.toJson(); map['roomCounter'] = _roomCounter?.toJson();
} }
if (_extValues != null) { if (_extValues != null) {
map['extValues'] = _extValues?.toJson(); map['extValues'] = _extValues?.toJson();
} }
return map; return map;
} }
} }
String _formatRoomMemberCount(num value) { String _formatRoomMemberCount(num value) {
if (value == value.roundToDouble()) { if (value == value.roundToDouble()) {
return value.toInt().toString(); return value.toInt().toString();
} }
return value.toString(); return value.toString();
} }
class RoomMemberCounter { class RoomMemberCounter {
RoomMemberCounter({num? adminCount, num? memberCount}) { RoomMemberCounter({num? adminCount, num? memberCount}) {
_adminCount = adminCount; _adminCount = adminCount;
_memberCount = memberCount; _memberCount = memberCount;
} }
RoomMemberCounter.fromJson(dynamic json) { RoomMemberCounter.fromJson(dynamic json) {
_adminCount = json['adminCount']; _adminCount = json['adminCount'];
_memberCount = json['memberCount']; _memberCount = json['memberCount'];
} }
num? _adminCount; num? _adminCount;
num? _memberCount; num? _memberCount;
RoomMemberCounter copyWith({num? adminCount, num? memberCount}) => RoomMemberCounter copyWith({num? adminCount, num? memberCount}) =>
RoomMemberCounter( RoomMemberCounter(
adminCount: adminCount ?? _adminCount, adminCount: adminCount ?? _adminCount,
memberCount: memberCount ?? _memberCount, memberCount: memberCount ?? _memberCount,
); );
num? get adminCount => _adminCount; num? get adminCount => _adminCount;
num? get memberCount => _memberCount; num? get memberCount => _memberCount;
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final map = <String, dynamic>{}; final map = <String, dynamic>{};
map['adminCount'] = _adminCount; map['adminCount'] = _adminCount;
map['memberCount'] = _memberCount; map['memberCount'] = _memberCount;
return map; return map;
} }
} }
/// account : "" /// account : ""
/// accountStatus : "" /// accountStatus : ""

View File

@ -12,19 +12,19 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
/// userLevel : {"charmLevel":0,"extValues":{"":{}},"wealthLevel":0} /// userLevel : {"charmLevel":0,"extValues":{"":{}},"wealthLevel":0}
/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]} /// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}
class RoomUserCardRes { class RoomUserCardRes {
RoomUserCardRes({ RoomUserCardRes({
bool? agent, bool? agent,
bool? anchor, bool? anchor,
bool? follow, bool? follow,
bool? friend, bool? friend,
String? ktvIntegral, String? ktvIntegral,
String? roomRole, String? roomRole,
String? supperVip, String? supperVip,
List<UseBadge>? useBadge, List<UseBadge>? useBadge,
List<UseProps>? useProps, List<UseProps>? useProps,
UserLevel? userLevel, UserLevel? userLevel,
SocialChatUserProfile? userProfile,}){ SocialChatUserProfile? userProfile,}){
_agent = agent; _agent = agent;
_anchor = anchor; _anchor = anchor;
_follow = follow; _follow = follow;
@ -35,8 +35,12 @@ class RoomUserCardRes {
_useBadge = useBadge; _useBadge = useBadge;
_useProps = useProps; _useProps = useProps;
_userLevel = userLevel; _userLevel = userLevel;
_userProfile = userProfile; final normalizedVipLevel = supperVip?.trim();
} _userProfile =
normalizedVipLevel == null || normalizedVipLevel.isEmpty
? userProfile
: userProfile?.copyWith(vipLevel: normalizedVipLevel);
}
RoomUserCardRes.fromJson(dynamic json) { RoomUserCardRes.fromJson(dynamic json) {
_agent = json['agent']; _agent = json['agent'];
@ -45,7 +49,7 @@ class RoomUserCardRes {
_friend = json['friend']; _friend = json['friend'];
_ktvIntegral = json['ktvIntegral']; _ktvIntegral = json['ktvIntegral'];
_roomRole = json['roomRole']; _roomRole = json['roomRole'];
_supperVip = json['supperVip']; _supperVip = socialChatVipLevelHintFromJson(json);
if (json['useBadge'] != null) { if (json['useBadge'] != null) {
_useBadge = []; _useBadge = [];
json['useBadge'].forEach((v) { json['useBadge'].forEach((v) {
@ -58,8 +62,15 @@ class RoomUserCardRes {
_useProps?.add(UseProps.fromJson(v)); _useProps?.add(UseProps.fromJson(v));
}); });
} }
_userLevel = json['userLevel'] != null ? UserLevel.fromJson(json['userLevel']) : null; _userLevel = json['userLevel'] != null ? UserLevel.fromJson(json['userLevel']) : null;
_userProfile = json['userProfile'] != null ? SocialChatUserProfile.fromJson(json['userProfile']) : null; _userProfile =
json['userProfile'] != null
? socialChatUserProfileFromJsonWithVipHint(json)
: null;
final normalizedVipLevel = _supperVip?.trim();
if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) {
_userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel);
}
} }
bool? _agent; bool? _agent;
bool? _anchor; bool? _anchor;
@ -72,29 +83,29 @@ class RoomUserCardRes {
List<UseProps>? _useProps; List<UseProps>? _useProps;
UserLevel? _userLevel; UserLevel? _userLevel;
SocialChatUserProfile? _userProfile; SocialChatUserProfile? _userProfile;
RoomUserCardRes copyWith({ bool? agent, RoomUserCardRes copyWith({ bool? agent,
bool? anchor, bool? anchor,
bool? follow, bool? follow,
bool? friend, bool? friend,
String? ktvIntegral, String? ktvIntegral,
String? roomRole, String? roomRole,
String? supperVip, String? supperVip,
List<UseBadge>? useBadge, List<UseBadge>? useBadge,
List<UseProps>? useProps, List<UseProps>? useProps,
UserLevel? userLevel, UserLevel? userLevel,
SocialChatUserProfile? userProfile, SocialChatUserProfile? userProfile,
}) => RoomUserCardRes( agent: agent ?? _agent, }) => RoomUserCardRes( agent: agent ?? _agent,
anchor: anchor ?? _anchor, anchor: anchor ?? _anchor,
follow: follow ?? _follow, follow: follow ?? _follow,
friend: friend ?? _friend, friend: friend ?? _friend,
ktvIntegral: ktvIntegral ?? _ktvIntegral, ktvIntegral: ktvIntegral ?? _ktvIntegral,
roomRole: roomRole ?? _roomRole, roomRole: roomRole ?? _roomRole,
supperVip: supperVip ?? _supperVip, supperVip: supperVip ?? _supperVip,
useBadge: useBadge ?? _useBadge, useBadge: useBadge ?? _useBadge,
useProps: useProps ?? _useProps, useProps: useProps ?? _useProps,
userLevel: userLevel ?? _userLevel, userLevel: userLevel ?? _userLevel,
userProfile: userProfile ?? _userProfile, userProfile: userProfile ?? _userProfile,
); );
bool? get agent => _agent; bool? get agent => _agent;
bool? get anchor => _anchor; bool? get anchor => _anchor;
bool? get follow => _follow; bool? get follow => _follow;
@ -129,9 +140,9 @@ RoomUserCardRes copyWith({ bool? agent,
map['userProfile'] = _userProfile?.toJson(); map['userProfile'] = _userProfile?.toJson();
} }
return map; return map;
} }
} }
/// account : "" /// account : ""
/// accountStatus : "" /// accountStatus : ""
@ -168,19 +179,19 @@ RoomUserCardRes copyWith({ bool? agent,
/// type : "" /// type : ""
/// userId : 0 /// userId : 0
class WearBadge { class WearBadge {
WearBadge({ WearBadge({
String? animationUrl, String? animationUrl,
String? badgeKey, String? badgeKey,
num? badgeLevel, num? badgeLevel,
String? badgeName, String? badgeName,
num? expireTime, num? expireTime,
num? id, num? id,
num? milestone, num? milestone,
String? notSelectUrl, String? notSelectUrl,
String? selectUrl, String? selectUrl,
String? type, String? type,
num? userId,}){ num? userId,}){
_animationUrl = animationUrl; _animationUrl = animationUrl;
_badgeKey = badgeKey; _badgeKey = badgeKey;
_badgeLevel = badgeLevel; _badgeLevel = badgeLevel;
@ -189,10 +200,10 @@ class WearBadge {
_id = id; _id = id;
_milestone = milestone; _milestone = milestone;
_notSelectUrl = notSelectUrl; _notSelectUrl = notSelectUrl;
_selectUrl = selectUrl; _selectUrl = selectUrl;
_type = type; _type = type;
_userId = userId; _userId = userId;
} }
WearBadge.fromJson(dynamic json) { WearBadge.fromJson(dynamic json) {
_animationUrl = json['animationUrl']; _animationUrl = json['animationUrl'];
@ -218,29 +229,29 @@ class WearBadge {
String? _selectUrl; String? _selectUrl;
String? _type; String? _type;
num? _userId; num? _userId;
WearBadge copyWith({ String? animationUrl, WearBadge copyWith({ String? animationUrl,
String? badgeKey, String? badgeKey,
num? badgeLevel, num? badgeLevel,
String? badgeName, String? badgeName,
num? expireTime, num? expireTime,
num? id, num? id,
num? milestone, num? milestone,
String? notSelectUrl, String? notSelectUrl,
String? selectUrl, String? selectUrl,
String? type, String? type,
num? userId, num? userId,
}) => WearBadge( animationUrl: animationUrl ?? _animationUrl, }) => WearBadge( animationUrl: animationUrl ?? _animationUrl,
badgeKey: badgeKey ?? _badgeKey, badgeKey: badgeKey ?? _badgeKey,
badgeLevel: badgeLevel ?? _badgeLevel, badgeLevel: badgeLevel ?? _badgeLevel,
badgeName: badgeName ?? _badgeName, badgeName: badgeName ?? _badgeName,
expireTime: expireTime ?? _expireTime, expireTime: expireTime ?? _expireTime,
id: id ?? _id, id: id ?? _id,
milestone: milestone ?? _milestone, milestone: milestone ?? _milestone,
notSelectUrl: notSelectUrl ?? _notSelectUrl, notSelectUrl: notSelectUrl ?? _notSelectUrl,
selectUrl: selectUrl ?? _selectUrl, selectUrl: selectUrl ?? _selectUrl,
type: type ?? _type, type: type ?? _type,
userId: userId ?? _userId, userId: userId ?? _userId,
); );
String? get animationUrl => _animationUrl; String? get animationUrl => _animationUrl;
String? get badgeKey => _badgeKey; String? get badgeKey => _badgeKey;
num? get badgeLevel => _badgeLevel; num? get badgeLevel => _badgeLevel;
@ -267,17 +278,17 @@ WearBadge copyWith({ String? animationUrl,
map['type'] = _type; map['type'] = _type;
map['userId'] = _userId; map['userId'] = _userId;
return map; return map;
} }
} }
class UserLevel { class UserLevel {
UserLevel({ UserLevel({
num? charmLevel, num? charmLevel,
num? wealthLevel,}){ num? wealthLevel,}){
_charmLevel = charmLevel; _charmLevel = charmLevel;
_wealthLevel = wealthLevel; _wealthLevel = wealthLevel;
} }
UserLevel.fromJson(dynamic json) { UserLevel.fromJson(dynamic json) {
_charmLevel = json['charmLevel']; _charmLevel = json['charmLevel'];
@ -285,11 +296,11 @@ class UserLevel {
} }
num? _charmLevel; num? _charmLevel;
num? _wealthLevel; num? _wealthLevel;
UserLevel copyWith({ num? charmLevel, UserLevel copyWith({ num? charmLevel,
num? wealthLevel, num? wealthLevel,
}) => UserLevel( charmLevel: charmLevel ?? _charmLevel, }) => UserLevel( charmLevel: charmLevel ?? _charmLevel,
wealthLevel: wealthLevel ?? _wealthLevel, wealthLevel: wealthLevel ?? _wealthLevel,
); );
num? get charmLevel => _charmLevel; num? get charmLevel => _charmLevel;
num? get wealthLevel => _wealthLevel; num? get wealthLevel => _wealthLevel;
@ -298,23 +309,23 @@ UserLevel copyWith({ num? charmLevel,
map['charmLevel'] = _charmLevel; map['charmLevel'] = _charmLevel;
map['wealthLevel'] = _wealthLevel; map['wealthLevel'] = _wealthLevel;
return map; return map;
} }
} }
class UseBadge { class UseBadge {
UseBadge({ UseBadge({
String? animationUrl, String? animationUrl,
String? badgeKey, String? badgeKey,
num? badgeLevel, num? badgeLevel,
String? badgeName, String? badgeName,
num? expireTime, num? expireTime,
String? id, String? id,
String? milestone, String? milestone,
String? notSelectUrl, String? notSelectUrl,
String? selectUrl, String? selectUrl,
String? type, String? type,
String? userId,}){ String? userId,}){
_animationUrl = animationUrl; _animationUrl = animationUrl;
_badgeKey = badgeKey; _badgeKey = badgeKey;
_badgeLevel = badgeLevel; _badgeLevel = badgeLevel;
@ -323,10 +334,10 @@ class UseBadge {
_id = id; _id = id;
_milestone = milestone; _milestone = milestone;
_notSelectUrl = notSelectUrl; _notSelectUrl = notSelectUrl;
_selectUrl = selectUrl; _selectUrl = selectUrl;
_type = type; _type = type;
_userId = userId; _userId = userId;
} }
UseBadge.fromJson(dynamic json) { UseBadge.fromJson(dynamic json) {
_animationUrl = json['animationUrl']; _animationUrl = json['animationUrl'];
@ -352,29 +363,29 @@ class UseBadge {
String? _selectUrl; String? _selectUrl;
String? _type; String? _type;
String? _userId; String? _userId;
UseBadge copyWith({ String? animationUrl, UseBadge copyWith({ String? animationUrl,
String? badgeKey, String? badgeKey,
num? badgeLevel, num? badgeLevel,
String? badgeName, String? badgeName,
num? expireTime, num? expireTime,
String? id, String? id,
String? milestone, String? milestone,
String? notSelectUrl, String? notSelectUrl,
String? selectUrl, String? selectUrl,
String? type, String? type,
String? userId, String? userId,
}) => UseBadge( animationUrl: animationUrl ?? _animationUrl, }) => UseBadge( animationUrl: animationUrl ?? _animationUrl,
badgeKey: badgeKey ?? _badgeKey, badgeKey: badgeKey ?? _badgeKey,
badgeLevel: badgeLevel ?? _badgeLevel, badgeLevel: badgeLevel ?? _badgeLevel,
badgeName: badgeName ?? _badgeName, badgeName: badgeName ?? _badgeName,
expireTime: expireTime ?? _expireTime, expireTime: expireTime ?? _expireTime,
id: id ?? _id, id: id ?? _id,
milestone: milestone ?? _milestone, milestone: milestone ?? _milestone,
notSelectUrl: notSelectUrl ?? _notSelectUrl, notSelectUrl: notSelectUrl ?? _notSelectUrl,
selectUrl: selectUrl ?? _selectUrl, selectUrl: selectUrl ?? _selectUrl,
type: type ?? _type, type: type ?? _type,
userId: userId ?? _userId, userId: userId ?? _userId,
); );
String? get animationUrl => _animationUrl; String? get animationUrl => _animationUrl;
String? get badgeKey => _badgeKey; String? get badgeKey => _badgeKey;
num? get badgeLevel => _badgeLevel; num? get badgeLevel => _badgeLevel;
@ -401,6 +412,6 @@ UseBadge copyWith({ String? animationUrl,
map['type'] = _type; map['type'] = _type;
map['userId'] = _userId; map['userId'] = _userId;
return map; return map;
} }
} }

View File

@ -7,6 +7,7 @@ class SCFloatingMessage {
String? userName; // String? userName; //
String? toUserName; // String? toUserName; //
String? giftUrl; // String? giftUrl; //
String? fallbackUrl; //
String? giftId; // id String? giftId; // id
int? type; int? type;
int? rocketLevel; int? rocketLevel;
@ -26,6 +27,7 @@ class SCFloatingMessage {
this.userName = '', this.userName = '',
this.toUserName = '', this.toUserName = '',
this.giftUrl = '', this.giftUrl = '',
this.fallbackUrl = '',
this.giftId = '', this.giftId = '',
this.number = 0, this.number = 0,
this.coins = 0, this.coins = 0,
@ -44,6 +46,7 @@ class SCFloatingMessage {
userName = json['userName']; userName = json['userName'];
toUserName = json['toUserName']; toUserName = json['toUserName'];
giftUrl = json['giftUrl']; giftUrl = json['giftUrl'];
fallbackUrl = json['fallbackUrl'];
giftId = json['giftId']; giftId = json['giftId'];
coins = json['coins']; coins = json['coins'];
number = json['number']; number = json['number'];
@ -63,6 +66,7 @@ class SCFloatingMessage {
map['userName'] = userName; map['userName'] = userName;
map['toUserName'] = toUserName; map['toUserName'] = toUserName;
map['giftUrl'] = giftUrl; map['giftUrl'] = giftUrl;
map['fallbackUrl'] = fallbackUrl;
map['giftId'] = giftId; map['giftId'] = giftId;
map['coins'] = coins; map['coins'] = coins;
map['number'] = number; map['number'] = number;

View File

@ -14,6 +14,7 @@ import 'package:yumi/ui_kit/widgets/room/floating/floating_gift_screen_widget.da
import 'package:yumi/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart'; import 'package:yumi/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart';
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart'; import 'package:yumi/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart';
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart'; import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart';
import 'package:yumi/ui_kit/widgets/room/floating/floating_vip_entry_screen_widget.dart';
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart'; import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
@ -76,8 +77,8 @@ class OverlayManager {
unawaited( unawaited(
warmImageResource( warmImageResource(
message.giftUrl ?? "", message.giftUrl ?? "",
logicalWidth: 96, logicalWidth: message.type == 5 ? 350 : 96,
logicalHeight: 96, logicalHeight: message.type == 5 ? 84 : 96,
), ),
); );
if (message.type == 0) { if (message.type == 0) {
@ -209,6 +210,12 @@ class OverlayManager {
message: message, message: message,
onAnimationCompleted: onComplete, onAnimationCompleted: onComplete,
); );
case 5:
//VIP进房飘窗
return FloatingVipEntryScreenWidget(
message: message,
onAnimationCompleted: onComplete,
);
default: default:
onComplete(); onComplete();
return Container(); return Container();
@ -233,6 +240,7 @@ class OverlayManager {
_removeActiveRoomMessage(); _removeActiveRoomMessage();
_removeMessagesByType(1); _removeMessagesByType(1);
_removeMessagesByType(0); _removeMessagesByType(0);
_removeMessagesByType(5);
} }
// //
@ -266,7 +274,7 @@ class OverlayManager {
if (message == null) { if (message == null) {
return false; return false;
} }
if (message.type != 0 && message.type != 1) { if (message.type != 0 && message.type != 1 && message.type != 5) {
return true; return true;
} }
@ -289,7 +297,9 @@ class OverlayManager {
void _removeActiveRoomMessage() { void _removeActiveRoomMessage() {
final activeMessage = _currentMessage; final activeMessage = _currentMessage;
if (activeMessage == null || if (activeMessage == null ||
(activeMessage.type != 0 && activeMessage.type != 1)) { (activeMessage.type != 0 &&
activeMessage.type != 1 &&
activeMessage.type != 5)) {
return; return;
} }
_currentOverlayEntry?.remove(); _currentOverlayEntry?.remove();

View File

@ -196,8 +196,8 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
num mickIndex, { num mickIndex, {
String? eventType, String? eventType,
String? inviterId, String? inviterId,
}) async { }) async {
Map<String, dynamic> params = {}; Map<String, dynamic> params = {};
params["roomId"] = roomId; params["roomId"] = roomId;
params["mickIndex"] = mickIndex; params["mickIndex"] = mickIndex;
if (eventType != null) { if (eventType != null) {
@ -262,12 +262,12 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
silentErrorToast silentErrorToast
? const {BaseNetworkClient.silentErrorToastKey: true} ? const {BaseNetworkClient.silentErrorToastKey: true}
: null, : null,
fromJson: fromJson:
(json) => (json) =>
(json as List) (json as List)
.map((e) => SocialChatUserProfile.fromJson(e)) .map((e) => socialChatUserProfileFromJsonWithVipHint(e)!)
.toList(), .toList(),
); );
return result; return result;
} }
@ -316,8 +316,8 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
String? roomId, String? roomId,
SCGiveAwayGiftRoomAcceptsCmd? accepts, SCGiveAwayGiftRoomAcceptsCmd? accepts,
String? dynamicContentId, String? dynamicContentId,
}) async { }) async {
Map<String, dynamic> params = {}; Map<String, dynamic> params = {};
if (roomId != null) { if (roomId != null) {
params["roomId"] = roomId; params["roomId"] = roomId;
} }
@ -512,9 +512,10 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
bool? adminLockSeat, bool? adminLockSeat,
bool? showHeartbeat, bool? showHeartbeat,
bool? openKtvMode, bool? openKtvMode,
String? roomSpecialMikeType, String? roomSpecialMikeType,
}) async { }) async {
Map<String, dynamic> params = {}; final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
Map<String, dynamic> params = {};
params["roomId"] = roomId; params["roomId"] = roomId;
if (touristMike != null) { if (touristMike != null) {
params["touristMike"] = touristMike; params["touristMike"] = touristMike;
@ -552,7 +553,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
fromJson: (json) => json as bool, fromJson: (json) => json as bool,
); );
Provider.of<RtmProvider>(context, listen: false).dispatchMessage( rtmProvider.dispatchMessage(
Msg( Msg(
groupId: roomAcount, groupId: roomAcount,
msg: roomId, msg: roomId,

View File

@ -14,7 +14,15 @@ import 'package:tancent_vap/widgets/vap_view.dart';
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart'; import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
class SCGiftVapSvgaManager { class SCGiftVapSvgaManager {
static const int _maxPendingTaskCount = 18; static const int giftEffectType = 0;
static const int entryEffectType = 1;
static const int giftEffectPriority = 100;
static const int entryEffectPriority = 1000;
static const int _maxPendingGiftTaskCount = 24;
static const int _maxPendingEntryTaskCount = 5;
static const int _maxConsecutiveEntryTasksBeforeGift = 2;
static const Duration _taskWatchdogTimeout = Duration(seconds: 20);
static const Duration _entryTaskTtl = Duration(seconds: 6);
Map<String, MovieEntity> videoItemCache = {}; Map<String, MovieEntity> videoItemCache = {};
static SCGiftVapSvgaManager? _inst; static SCGiftVapSvgaManager? _inst;
static const int _maxPreloadConcurrency = 1; static const int _maxPreloadConcurrency = 1;
@ -26,7 +34,10 @@ class SCGiftVapSvgaManager {
factory SCGiftVapSvgaManager() => _inst ??= SCGiftVapSvgaManager._internal(); factory SCGiftVapSvgaManager() => _inst ??= SCGiftVapSvgaManager._internal();
final SCPriorityQueue<SCVapTask> _tq = SCPriorityQueue<SCVapTask>( final SCPriorityQueue<SCVapTask> _giftQueue = SCPriorityQueue<SCVapTask>(
(a, b) => a.compareTo(b), // SCVapTask compareTo
);
final SCPriorityQueue<SCVapTask> _entryQueue = SCPriorityQueue<SCVapTask>(
(a, b) => a.compareTo(b), // SCVapTask compareTo (a, b) => a.compareTo(b), // SCVapTask compareTo
); );
VapController? _rgc; VapController? _rgc;
@ -34,12 +45,14 @@ class SCGiftVapSvgaManager {
bool _play = false; bool _play = false;
bool _dis = false; bool _dis = false;
SCVapTask? _currentTask; SCVapTask? _currentTask;
Timer? _currentTaskWatchdogTimer;
final Queue<String> _preloadQueue = Queue<String>(); final Queue<String> _preloadQueue = Queue<String>();
final Set<String> _queuedPreloadPaths = <String>{}; final Set<String> _queuedPreloadPaths = <String>{};
final Map<String, Future<MovieEntity>> _svgaLoadTasks = {}; final Map<String, Future<MovieEntity>> _svgaLoadTasks = {};
final Map<String, Future<String>> _playablePathTasks = {}; final Map<String, Future<String>> _playablePathTasks = {};
final Map<String, String> _playablePathCache = {}; final Map<String, String> _playablePathCache = {};
int _activePreloadCount = 0; int _activePreloadCount = 0;
int _consecutiveEntryTaskCount = 0;
bool _pause = false; bool _pause = false;
@ -63,6 +76,26 @@ class SCGiftVapSvgaManager {
return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga"; return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga";
} }
static int resolveEffectPriority({
int priority = 0,
int type = giftEffectType,
}) {
if (type == entryEffectType) {
return priority > entryEffectPriority ? priority : entryEffectPriority;
}
if (priority != 0) {
return priority;
}
return giftEffectPriority;
}
bool _isEntryTask(SCVapTask task) => task.type == entryEffectType;
int get _pendingTaskCount => _giftQueue.length + _entryQueue.length;
String get _queueSummary =>
'entry=${_entryQueue.length} gift=${_giftQueue.length}';
bool _isControllerReady(SCVapTask task) { bool _isControllerReady(SCVapTask task) {
if (_needsSvgaController(task.path)) { if (_needsSvgaController(task.path)) {
return _rsc != null; return _rsc != null;
@ -253,6 +286,96 @@ class SCGiftVapSvgaManager {
} }
} }
void _enqueueTask(SCVapTask task) {
if (_isEntryTask(task)) {
_dropExpiredEntryTasks();
_entryQueue.add(task);
_trimEntryQueue();
return;
}
_giftQueue.add(task);
_trimGiftQueue();
}
void _trimEntryQueue() {
while (_entryQueue.length > _maxPendingEntryTaskCount) {
final removedTask = _removeEntryOverflowTask();
_completeDroppedQueuedTask(removedTask, 'trim entry queue');
}
}
SCVapTask _removeEntryOverflowTask() {
final tasks = _entryQueue.unorderedElements.cast<SCVapTask>();
var candidate = tasks.first;
for (final task in tasks.skip(1)) {
if (task.priority < candidate.priority ||
(task.priority == candidate.priority &&
task.createdAt.isBefore(candidate.createdAt))) {
candidate = task;
}
}
_entryQueue.remove(candidate);
return candidate;
}
void _trimGiftQueue() {
while (_giftQueue.length > _maxPendingGiftTaskCount) {
final removedTask = _giftQueue.removeLast();
_completeDroppedQueuedTask(removedTask, 'trim gift queue');
}
}
void _dropExpiredEntryTasks() {
if (_entryQueue.isEmpty) {
return;
}
final now = DateTime.now();
final expiredTasks =
_entryQueue.unorderedElements
.cast<SCVapTask>()
.where((task) => now.difference(task.createdAt) > _entryTaskTtl)
.toList();
for (final task in expiredTasks) {
_entryQueue.remove(task);
_completeDroppedQueuedTask(
task,
'drop expired entry task ageMs=${now.difference(task.createdAt).inMilliseconds}',
);
}
}
void _completeDroppedQueuedTask(SCVapTask task, String reason) {
SCRoomEffectScheduler().completeHighCostTask(debugLabel: task.path);
_log(
'$reason path=${task.path} priority=${task.priority} '
'type=${task.type} queue=$_queueSummary',
);
}
SCVapTask? _peekNextQueuedTask() {
_dropExpiredEntryTasks();
if (_entryQueue.isNotEmpty) {
final shouldYieldToGift =
_giftQueue.isNotEmpty &&
_consecutiveEntryTaskCount >= _maxConsecutiveEntryTasksBeforeGift;
if (!shouldYieldToGift) {
return _entryQueue.first;
}
return _giftQueue.first;
}
return _giftQueue.first;
}
void _removeQueuedTask(SCVapTask task) {
if (_isEntryTask(task)) {
_entryQueue.remove(task);
_consecutiveEntryTaskCount++;
return;
}
_giftQueue.remove(task);
_consecutiveEntryTaskCount = 0;
}
void _scheduleNextTask({Duration delay = Duration.zero}) { void _scheduleNextTask({Duration delay = Duration.zero}) {
if (_dis) { if (_dis) {
return; return;
@ -268,8 +391,14 @@ class SCGiftVapSvgaManager {
if (_dis) { if (_dis) {
return; return;
} }
final finishedTask = _currentTask;
if (!_play && finishedTask == null) {
_scheduleNextTask(delay: delay);
return;
}
_cancelCurrentTaskWatchdog();
SCRoomEffectScheduler().completeHighCostTask( SCRoomEffectScheduler().completeHighCostTask(
debugLabel: _currentTask?.path, debugLabel: finishedTask?.path,
); );
_play = false; _play = false;
_currentTask = null; _currentTask = null;
@ -288,7 +417,7 @@ class SCGiftVapSvgaManager {
_rgc = vapController; _rgc = vapController;
_log( _log(
'bindVapCtrl hasVapCtrl=${_rgc != null} ' 'bindVapCtrl hasVapCtrl=${_rgc != null} '
'hasSvgaCtrl=${_rsc != null} queue=${_tq.length} mute=$_mute', 'hasSvgaCtrl=${_rsc != null} queue=$_queueSummary mute=$_mute',
); );
_rgc?.setAnimListener( _rgc?.setAnimListener(
onVideoStart: () { onVideoStart: () {
@ -314,18 +443,13 @@ class SCGiftVapSvgaManager {
_rsc = svgaController; _rsc = svgaController;
_log( _log(
'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} ' 'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} '
'hasVapCtrl=${_rgc != null} queue=${_tq.length}', 'hasVapCtrl=${_rgc != null} queue=$_queueSummary',
); );
_rsc?.addStatusListener((AnimationStatus status) { _rsc?.addStatusListener((AnimationStatus status) {
if (status.isCompleted) { if (status.isCompleted) {
_log('svga completed path=${_currentTask?.path}'); _log('svga completed path=${_currentTask?.path}');
_rsc?.reset(); _rsc?.reset();
SCRoomEffectScheduler().completeHighCostTask( _finishCurrentTask();
debugLabel: _currentTask?.path,
);
_play = false;
_currentTask = null;
_pn();
} }
}); });
_scheduleNextTask(); _scheduleNextTask();
@ -337,18 +461,22 @@ class SCGiftVapSvgaManager {
String path, { String path, {
int priority = 0, int priority = 0,
Map<String, VAPContent>? customResources, Map<String, VAPContent>? customResources,
int type = 0, int type = giftEffectType,
}) { }) {
if (path.isEmpty) { if (path.isEmpty) {
_log('play ignored because path is empty'); _log('play ignored because path is empty');
return; return;
} }
final resolvedPriority = resolveEffectPriority(
priority: priority,
type: type,
);
_log( _log(
'play request path=$path ext=${SCPathUtils.getFileExtension(path)} ' 'play request path=$path ext=${SCPathUtils.getFileExtension(path)} '
'priority=$priority type=$type ' 'priority=$priority resolvedPriority=$resolvedPriority type=$type '
'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim} ' 'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim} '
'lowPerformance=${SCGlobalConfig.isLowPerformanceDevice} ' 'lowPerformance=${SCGlobalConfig.isLowPerformanceDevice} '
'disposed=$_dis playing=$_play queueBefore=${_tq.length} ' 'disposed=$_dis playing=$_play queueBefore=$_queueSummary '
'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null}', 'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null}',
); );
if (SCGlobalConfig.allowsHighCostAnimations) { if (SCGlobalConfig.allowsHighCostAnimations) {
@ -358,31 +486,15 @@ class SCGiftVapSvgaManager {
} }
final task = SCVapTask( final task = SCVapTask(
path: path, path: path,
priority: priority, type: type,
priority: resolvedPriority,
customResources: customResources, customResources: customResources,
); );
if (_tq.length >= _maxPendingTaskCount && priority <= 0) {
_log(
'drop play request because queue is full path=$path '
'priority=$priority queue=${_tq.length}',
);
return;
}
_tq.add(task);
SCRoomEffectScheduler().registerHighCostTaskQueued(debugLabel: path); SCRoomEffectScheduler().registerHighCostTaskQueued(debugLabel: path);
_enqueueTask(task);
unawaited(preload(path)); unawaited(preload(path));
while (_tq.length > _maxPendingTaskCount) { _log('task enqueued path=$path queueAfter=$_queueSummary');
final removedTask = _tq.removeLast();
SCRoomEffectScheduler().completeHighCostTask(
debugLabel: removedTask.path,
);
_log(
'trim queued task path=${removedTask.path} '
'priority=${removedTask.priority} queue=${_tq.length}',
);
}
_log('task enqueued path=$path queueAfter=${_tq.length}');
if (!_play) { if (!_play) {
_pn(); _pn();
} }
@ -398,32 +510,34 @@ class SCGiftVapSvgaManager {
// //
Future<void> _pn() async { Future<void> _pn() async {
if (_pause) { if (_pause) {
_log('skip _pn because paused queue=${_tq.length}'); _log('skip _pn because paused queue=$_queueSummary');
return; return;
} }
if (_dis || _tq.isEmpty || _play) return; if (_dis || _pendingTaskCount == 0 || _play) return;
final task = _tq.first; final task = _peekNextQueuedTask();
if (task == null || !_isControllerReady(task)) { if (task == null || !_isControllerReady(task)) {
_log( _log(
'controller not ready for path=${task?.path} ' 'controller not ready for path=${task?.path} '
'needSvga=${task != null ? _needsSvgaController(task.path) : "unknown"} ' 'needSvga=${task != null ? _needsSvgaController(task.path) : "unknown"} '
'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null} ' 'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null} '
'queue=${_tq.length}', 'queue=$_queueSummary',
); );
return; return;
} }
_tq.removeFirst(); _removeQueuedTask(task);
_play = true; _play = true;
_currentTask = task; _currentTask = task;
_armCurrentTaskWatchdog(task);
try { try {
final pathType = SCPathUtils.getPathType(task.path); final pathType = SCPathUtils.getPathType(task.path);
_log( _log(
'start task path=${task.path} ' 'start task path=${task.path} '
'pathType=$pathType ' 'pathType=$pathType '
'queueRemaining=${_tq.length} ' 'queueRemaining=$_queueSummary '
'needSvga=${_needsSvgaController(task.path)}', 'needSvga=${_needsSvgaController(task.path)} '
'type=${task.type} consecutiveEntry=$_consecutiveEntryTaskCount',
); );
if (pathType == PathType.asset) { if (pathType == PathType.asset) {
await _pa(task); await _pa(task);
@ -447,6 +561,9 @@ class SCGiftVapSvgaManager {
if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
_log('play asset svga path=${task.path}'); _log('play asset svga path=${task.path}');
final entity = await _loadSvgaEntity(task.path); final entity = await _loadSvgaEntity(task.path);
if (!_isCurrentTask(task)) {
return;
}
_log('use prepared asset svga path=${task.path}'); _log('use prepared asset svga path=${task.path}');
_rsc?.videoItem = entity; _rsc?.videoItem = entity;
_rsc?.reset(); _rsc?.reset();
@ -470,6 +587,9 @@ class SCGiftVapSvgaManager {
} }
if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") { if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
final entity = await _loadSvgaEntity(task.path); final entity = await _loadSvgaEntity(task.path);
if (!_isCurrentTask(task)) {
return;
}
_log('play local svga file path=${task.path}'); _log('play local svga file path=${task.path}');
_rsc?.videoItem = entity; _rsc?.videoItem = entity;
_rsc?.reset(); _rsc?.reset();
@ -483,11 +603,17 @@ class SCGiftVapSvgaManager {
} }
_log('play local vap/mp4 file path=${task.path}'); _log('play local vap/mp4 file path=${task.path}');
await _rgc?.setMute(_mute); await _rgc?.setMute(_mute);
if (!_isCurrentTask(task)) {
return;
}
if (task.customResources != null) { if (task.customResources != null) {
task.customResources?.forEach((k, v) async { task.customResources?.forEach((k, v) async {
await _rgc?.setVapTagContent(k, v); await _rgc?.setVapTagContent(k, v);
}); });
} }
if (!_isCurrentTask(task)) {
return;
}
await _rgc!.playFile(task.path); await _rgc!.playFile(task.path);
} }
@ -504,6 +630,9 @@ class SCGiftVapSvgaManager {
_finishCurrentTask(); _finishCurrentTask();
return; return;
} }
if (!_isCurrentTask(task)) {
return;
}
_log('use prepared network svga path=${task.path}'); _log('use prepared network svga path=${task.path}');
_rsc?.videoItem = entity; _rsc?.videoItem = entity;
_rsc?.reset(); _rsc?.reset();
@ -512,11 +641,15 @@ class SCGiftVapSvgaManager {
} else { } else {
_log('download network vap/mp4 path=${task.path}'); _log('download network vap/mp4 path=${task.path}');
final playablePath = await _ensurePlayableFilePath(task.path); final playablePath = await _ensurePlayableFilePath(task.path);
if (!_isCurrentTask(task)) {
return;
}
if (!_dis) { if (!_dis) {
_log('use prepared network vap/mp4 local path=$playablePath'); _log('use prepared network vap/mp4 local path=$playablePath');
await _pf( await _pf(
SCVapTask( SCVapTask(
path: playablePath, path: playablePath,
type: task.type,
priority: task.priority, priority: task.priority,
customResources: task.customResources, customResources: task.customResources,
), ),
@ -533,25 +666,52 @@ class SCGiftVapSvgaManager {
} }
} }
bool _isCurrentTask(SCVapTask task) {
return !_dis && _play && identical(_currentTask, task);
}
void _armCurrentTaskWatchdog(SCVapTask task) {
_cancelCurrentTaskWatchdog();
_currentTaskWatchdogTimer = Timer(_taskWatchdogTimeout, () {
if (!_isCurrentTask(task)) {
return;
}
_log('task watchdog timeout path=${task.path}');
_rgc?.stop();
_rsc?.stop();
_rsc?.reset();
_rsc?.videoItem = null;
_finishCurrentTask(delay: const Duration(milliseconds: 50));
});
}
void _cancelCurrentTaskWatchdog() {
_currentTaskWatchdogTimer?.cancel();
_currentTaskWatchdogTimer = null;
}
// //
void pauseAnim() { void pauseAnim() {
_pause = true; _pause = true;
_log('pauseAnim queue=${_tq.length}'); _log('pauseAnim queue=$_queueSummary');
} }
// //
void resumeAnim() { void resumeAnim() {
_pause = false; _pause = false;
_log('resumeAnim queue=${_tq.length}'); _log('resumeAnim queue=$_queueSummary');
_pn(); _pn();
} }
void stopPlayback() { void stopPlayback() {
_log('stopPlayback queue=${_tq.length} currentPath=${_currentTask?.path}'); _log('stopPlayback queue=$_queueSummary currentPath=${_currentTask?.path}');
_play = false; _play = false;
_currentTask = null; _currentTask = null;
_pause = false; _pause = false;
_tq.clear(); _consecutiveEntryTaskCount = 0;
_cancelCurrentTaskWatchdog();
_giftQueue.clear();
_entryQueue.clear();
_preloadQueue.clear(); _preloadQueue.clear();
_queuedPreloadPaths.clear(); _queuedPreloadPaths.clear();
_activePreloadCount = 0; _activePreloadCount = 0;
@ -564,7 +724,7 @@ class SCGiftVapSvgaManager {
// //
void dispose() { void dispose() {
_log('dispose queue=${_tq.length} currentPath=${_currentTask?.path}'); _log('dispose queue=$_queueSummary currentPath=${_currentTask?.path}');
_dis = true; _dis = true;
stopPlayback(); stopPlayback();
_svgaLoadTasks.clear(); _svgaLoadTasks.clear();
@ -580,7 +740,7 @@ class SCGiftVapSvgaManager {
// //
void clearTasks() { void clearTasks() {
_log( _log(
'clearTasks queueBefore=${_tq.length} currentPath=${_currentTask?.path}', 'clearTasks queueBefore=$_queueSummary currentPath=${_currentTask?.path}',
); );
stopPlayback(); stopPlayback();
} }
@ -594,6 +754,7 @@ class SCVapTask implements Comparable<SCVapTask> {
final int type; final int type;
final int priority; final int priority;
final Map<String, VAPContent>? customResources; final Map<String, VAPContent>? customResources;
final DateTime createdAt;
final int _seq; final int _seq;
static int _nextSeq = 0; static int _nextSeq = 0;
@ -603,7 +764,9 @@ class SCVapTask implements Comparable<SCVapTask> {
this.priority = 0, this.priority = 0,
this.customResources, this.customResources,
this.type = 0, this.type = 0,
}) : _seq = _nextSeq++; DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now(),
_seq = _nextSeq++;
@override @override
int compareTo(SCVapTask other) { int compareTo(SCVapTask other) {
@ -618,7 +781,7 @@ class SCVapTask implements Comparable<SCVapTask> {
@override @override
String toString() { String toString() {
return 'SCVapTask{path: $path, priority: $priority, seq: $_seq}'; return 'SCVapTask{path: $path, type: $type, priority: $priority, seq: $_seq}';
} }
} }
@ -649,6 +812,8 @@ class SCPriorityQueue<E> {
void clear() => _els.clear(); void clear() => _els.clear();
bool remove(E element) => _els.remove(element);
E removeLast() { E removeLast() {
if (isEmpty) throw StateError("No elements"); if (isEmpty) throw StateError("No elements");
return _els.removeLast(); return _els.removeLast();

View File

@ -90,7 +90,21 @@ class SCPathUtils {
/// ///
static String getFileExtension(String filePath) { static String getFileExtension(String filePath) {
return path.extension(filePath).toLowerCase(); final value = filePath.trim();
if (value.isEmpty) {
return '';
}
final uri = Uri.tryParse(value);
if (uri != null && (uri.scheme.isNotEmpty || uri.host.isNotEmpty)) {
final uriPathExtension = path.extension(uri.path).toLowerCase();
if (uriPathExtension.isNotEmpty) {
return uriPathExtension;
}
}
final normalizedValue = value.split('?').first.split('#').first;
return path.extension(normalizedValue).toLowerCase();
} }
/// ///

View File

@ -9,6 +9,9 @@ import 'package:yumi/ui_kit/components/text/sc_text.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
import 'package:yumi/shared/tools/sc_network_image_utils.dart'; import 'package:yumi/shared/tools/sc_network_image_utils.dart';
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart'; import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
@ -40,6 +43,7 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
if (!_effectsEnabled) { if (!_effectsEnabled) {
return; return;
} }
_enqueueVipEntryFloatingMessage(msg);
unawaited( unawaited(
warmImageResource( warmImageResource(
msg.user?.userAvatar ?? "", msg.user?.userAvatar ?? "",
@ -59,6 +63,72 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
}); });
} }
void _enqueueVipEntryFloatingMessage(Msg msg) {
final floatPicture = msg.user?.getFloatPicture();
final floatPictureUrl = _resolveVipFloatPictureUrl(floatPicture);
final floatPictureFallbackUrl = _resolveVipFloatPictureFallbackUrl(
floatPicture,
);
if (floatPictureUrl.isEmpty) {
return;
}
final roomId =
Provider.of<RtcProvider>(
context,
listen: false,
).currenRoom?.roomProfile?.roomProfile?.id?.trim() ??
"";
if (roomId.isEmpty) {
return;
}
debugPrint(
'[VIP][RoomFloat] enqueue entry float '
'userId=${msg.user?.id} roomId=$roomId resourceId=${floatPicture?.id} '
'url=$floatPictureUrl',
);
OverlayManager().addMessage(
SCFloatingMessage(
type: 5,
userId: msg.user?.id,
roomId: roomId,
userAvatarUrl: msg.user?.userAvatar,
userName: msg.user?.userNickname,
giftUrl: floatPictureUrl,
fallbackUrl: floatPictureFallbackUrl,
priority: 900,
),
);
}
String _resolveVipFloatPictureUrl(PropsResources? resource) {
return _firstNonBlank([
resource?.sourceUrl,
resource?.roomSendCoverUrl,
resource?.roomOpenedUrl,
resource?.cover,
resource?.roomNotOpenedUrl,
]);
}
String _resolveVipFloatPictureFallbackUrl(PropsResources? resource) {
return _firstNonBlank([
resource?.roomSendCoverUrl,
resource?.roomOpenedUrl,
resource?.cover,
resource?.roomNotOpenedUrl,
]);
}
String _firstNonBlank(Iterable<String?> values) {
for (final value in values) {
final text = value?.trim();
if (text != null && text.isNotEmpty) {
return text;
}
}
return "";
}
void _addToQueue(Msg msg) { void _addToQueue(Msg msg) {
if (!_effectsEnabled || !mounted) { if (!_effectsEnabled || !mounted) {
return; return;

View File

@ -1,4 +1,5 @@
import 'dart:collection'; import 'dart:collection';
import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -16,6 +17,7 @@ class RoomGiftSeatFlightRequest {
this.beginSize = 96, this.beginSize = 96,
this.endSize = 34, this.endSize = 34,
this.queueTag, this.queueTag,
this.batchTag,
}); });
final String imagePath; final String imagePath;
@ -25,6 +27,7 @@ class RoomGiftSeatFlightRequest {
final double beginSize; final double beginSize;
final double endSize; final double endSize;
final String? queueTag; final String? queueTag;
final String? batchTag;
} }
class RoomGiftSeatFlightController { class RoomGiftSeatFlightController {
@ -121,6 +124,7 @@ class RoomGiftSeatFlightController {
} }
final queueTag = request.queueTag?.trim(); final queueTag = request.queueTag?.trim();
final batchTag = request.batchTag?.trim();
return RoomGiftSeatFlightRequest( return RoomGiftSeatFlightRequest(
imagePath: imagePath, imagePath: imagePath,
targetUserId: targetUserId, targetUserId: targetUserId,
@ -129,6 +133,7 @@ class RoomGiftSeatFlightController {
beginSize: request.beginSize, beginSize: request.beginSize,
endSize: request.endSize, endSize: request.endSize,
queueTag: queueTag == null || queueTag.isEmpty ? null : queueTag, queueTag: queueTag == null || queueTag.isEmpty ? null : queueTag,
batchTag: batchTag == null || batchTag.isEmpty ? null : batchTag,
); );
} }
@ -216,12 +221,12 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
final GlobalKey _overlayKey = GlobalKey(); final GlobalKey _overlayKey = GlobalKey();
late final AnimationController _controller; late final AnimationController _controller;
Timer? _activeFlightWatchdogTimer;
RoomGiftSeatFlightRequest? _centerRequest; RoomGiftSeatFlightRequest? _centerRequest;
ImageProvider<Object>? _centerImageProvider; ImageProvider<Object>? _centerImageProvider;
RoomGiftSeatFlightRequest? _activeRequest; List<_ActiveRoomGiftSeatFlight> _activeFlights =
ImageProvider<Object>? _activeImageProvider; const <_ActiveRoomGiftSeatFlight>[];
Offset? _activeTargetOffset;
bool _isPlaying = false; bool _isPlaying = false;
int _sessionToken = 0; int _sessionToken = 0;
@ -248,6 +253,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
@override @override
void dispose() { void dispose() {
widget.controller._detach(this); widget.controller._detach(this);
_cancelActiveFlightWatchdog();
_controller.dispose(); _controller.dispose();
super.dispose(); super.dispose();
} }
@ -283,14 +289,13 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
void _clear() { void _clear() {
_sessionToken += 1; _sessionToken += 1;
_queue.clear(); _queue.clear();
_cancelActiveFlightWatchdog();
_controller.stop(); _controller.stop();
_controller.reset(); _controller.reset();
if (!mounted) { if (!mounted) {
_centerRequest = null; _centerRequest = null;
_centerImageProvider = null; _centerImageProvider = null;
_activeRequest = null; _activeFlights = const <_ActiveRoomGiftSeatFlight>[];
_activeImageProvider = null;
_activeTargetOffset = null;
_imageProviderCache.clear(); _imageProviderCache.clear();
_isPlaying = false; _isPlaying = false;
return; return;
@ -298,17 +303,17 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
setState(() { setState(() {
_centerRequest = null; _centerRequest = null;
_centerImageProvider = null; _centerImageProvider = null;
_activeRequest = null; _activeFlights = const <_ActiveRoomGiftSeatFlight>[];
_activeImageProvider = null;
_activeTargetOffset = null;
_imageProviderCache.clear(); _imageProviderCache.clear();
_isPlaying = false; _isPlaying = false;
}); });
} }
bool _hasTrackedRequests(String queueTag) { bool _hasTrackedRequests(String queueTag) {
if ((_activeRequest?.queueTag ?? '').trim() == queueTag) { for (final activeFlight in _activeFlights) {
return true; if ((activeFlight.request.queueTag ?? '').trim() == queueTag) {
return true;
}
} }
for (final queuedRequest in _queue) { for (final queuedRequest in _queue) {
if ((queuedRequest.request.queueTag ?? '').trim() == queueTag) { if ((queuedRequest.request.queueTag ?? '').trim() == queueTag) {
@ -334,8 +339,10 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
int _countTrackedRequests(String queueTag) { int _countTrackedRequests(String queueTag) {
var count = 0; var count = 0;
if ((_activeRequest?.queueTag ?? '').trim() == queueTag) { for (final activeFlight in _activeFlights) {
count += 1; if ((activeFlight.request.queueTag ?? '').trim() == queueTag) {
count += 1;
}
} }
for (final queuedRequest in _queue) { for (final queuedRequest in _queue) {
if ((queuedRequest.request.queueTag ?? '').trim() == queueTag) { if ((queuedRequest.request.queueTag ?? '').trim() == queueTag) {
@ -361,6 +368,33 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
} }
} }
List<_QueuedRoomGiftSeatFlightRequest> _removeNextQueuedBatch() {
if (_queue.isEmpty) {
return const <_QueuedRoomGiftSeatFlightRequest>[];
}
final firstRequest = _queue.removeFirst();
final batchTag = (firstRequest.request.batchTag ?? '').trim();
if (batchTag.isEmpty) {
return <_QueuedRoomGiftSeatFlightRequest>[firstRequest];
}
final batch = <_QueuedRoomGiftSeatFlightRequest>[firstRequest];
while (_queue.isNotEmpty &&
(_queue.first.request.batchTag ?? '').trim() == batchTag) {
batch.add(_queue.removeFirst());
}
return batch;
}
void _requeueBatchAtFront(
List<_QueuedRoomGiftSeatFlightRequest> queuedBatch,
) {
for (final queuedRequest in queuedBatch.reversed) {
_queue.addFirst(queuedRequest);
}
}
void _scheduleNextAnimation() { void _scheduleNextAnimation() {
if (_isPlaying || _queue.isEmpty || !mounted) { if (_isPlaying || _queue.isEmpty || !mounted) {
return; return;
@ -381,78 +415,131 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
} }
final activeSessionToken = _sessionToken; final activeSessionToken = _sessionToken;
final queuedRequest = _queue.removeFirst(); final queuedBatch = _removeNextQueuedBatch();
final targetOffset = _resolveTargetOffset( final activeFlights = <_ActiveRoomGiftSeatFlight>[];
queuedRequest.request.targetUserId, final unresolvedBatch = <_QueuedRoomGiftSeatFlightRequest>[];
for (final queuedRequest in queuedBatch) {
final targetOffset = _resolveTargetOffset(
queuedRequest.request.targetUserId,
);
if (targetOffset == null) {
unresolvedBatch.add(queuedRequest);
continue;
}
final imageProvider = _resolveImageProvider(
queuedRequest.request.imagePath,
request: queuedRequest.request,
);
activeFlights.add(
_ActiveRoomGiftSeatFlight(
request: queuedRequest.request,
imageProvider: imageProvider,
targetOffset: targetOffset,
),
);
}
final shouldRetryBatch = unresolvedBatch.any(
(queuedRequest) => queuedRequest.retryCount < 6,
); );
if (targetOffset == null) { if (shouldRetryBatch) {
if (queuedRequest.retryCount < 6) { final retryBatch =
queuedBatch
.map(
(queuedRequest) => queuedRequest.copyWith(
retryCount: queuedRequest.retryCount + 1,
),
)
.toList();
if (retryBatch.isNotEmpty) {
Future.delayed(const Duration(milliseconds: 120), () { Future.delayed(const Duration(milliseconds: 120), () {
if (!mounted || activeSessionToken != _sessionToken) { if (!mounted || activeSessionToken != _sessionToken) {
return; return;
} }
_queue.addFirst( _requeueBatchAtFront(retryBatch);
queuedRequest.copyWith(retryCount: queuedRequest.retryCount + 1),
);
_scheduleNextAnimation(); _scheduleNextAnimation();
}); });
} else {
_syncIdleCenterVisual();
_scheduleNextAnimation();
} }
return; return;
} }
final imageProvider = _resolveImageProvider( if (activeFlights.isEmpty) {
queuedRequest.request.imagePath, _syncIdleCenterVisual();
); _scheduleNextAnimation();
return;
}
_isPlaying = true; _isPlaying = true;
_controller.duration = queuedRequest.request.flightDuration; final centerFlight = activeFlights.first;
_controller.duration = centerFlight.request.flightDuration;
setState(() { setState(() {
_centerRequest = queuedRequest.request; _centerRequest = centerFlight.request;
_centerImageProvider = imageProvider; _centerImageProvider = centerFlight.imageProvider;
_activeRequest = queuedRequest.request; _activeFlights = activeFlights;
_activeTargetOffset = targetOffset;
_activeImageProvider = imageProvider;
}); });
try { try {
await precacheImage( await Future.wait(
imageProvider, activeFlights.map(
context, (flight) => precacheImage(
).timeout(const Duration(milliseconds: 250)); flight.imageProvider,
context,
).timeout(const Duration(milliseconds: 250)),
),
);
} catch (_) {} } catch (_) {}
if (!mounted || if (!mounted ||
activeSessionToken != _sessionToken || activeSessionToken != _sessionToken ||
_activeRequest != queuedRequest.request) { !identical(_activeFlights, activeFlights)) {
return; return;
} }
_controller.reset(); _controller.reset();
_armActiveFlightWatchdog(centerFlight.request.flightDuration);
_controller.forward(); _controller.forward();
} }
void _finishCurrentAnimation() { void _finishCurrentAnimation() {
_cancelActiveFlightWatchdog();
if (!mounted) { if (!mounted) {
_activeRequest = null; _activeFlights = const <_ActiveRoomGiftSeatFlight>[];
_activeImageProvider = null;
_activeTargetOffset = null;
_isPlaying = false; _isPlaying = false;
_syncIdleCenterVisual(); _syncIdleCenterVisual();
return; return;
} }
setState(() { setState(() {
_activeRequest = null; _activeFlights = const <_ActiveRoomGiftSeatFlight>[];
_activeImageProvider = null;
_activeTargetOffset = null;
_isPlaying = false; _isPlaying = false;
}); });
_syncIdleCenterVisual(); _syncIdleCenterVisual();
_scheduleNextAnimation(); _scheduleNextAnimation();
} }
void _armActiveFlightWatchdog(Duration flightDuration) {
_cancelActiveFlightWatchdog();
final timeout = flightDuration + const Duration(milliseconds: 900);
_activeFlightWatchdogTimer = Timer(timeout, () {
_activeFlightWatchdogTimer = null;
if (!_isPlaying || _activeFlights.isEmpty) {
return;
}
debugPrint(
'[GiftSeatFlight] watchdog timeout active=${_activeFlights.length}',
);
_controller.stop();
_finishCurrentAnimation();
});
}
void _cancelActiveFlightWatchdog() {
_activeFlightWatchdogTimer?.cancel();
_activeFlightWatchdogTimer = null;
}
void _ensureCenterVisual(RoomGiftSeatFlightRequest request) { void _ensureCenterVisual(RoomGiftSeatFlightRequest request) {
if (_centerRequest != null && _centerImageProvider != null) { if (_centerRequest != null && _centerImageProvider != null) {
return; return;
@ -473,7 +560,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
} }
void _syncIdleCenterVisual() { void _syncIdleCenterVisual() {
if (_isPlaying || _activeRequest != null) { if (_isPlaying || _activeFlights.isNotEmpty) {
return; return;
} }
@ -532,7 +619,10 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
String imagePath, { String imagePath, {
RoomGiftSeatFlightRequest? request, RoomGiftSeatFlightRequest? request,
}) { }) {
final currentRequest = request ?? _activeRequest ?? _centerRequest; final currentRequest =
request ??
(_activeFlights.isNotEmpty ? _activeFlights.first.request : null) ??
_centerRequest;
final logicalSize = final logicalSize =
currentRequest == null currentRequest == null
? null ? null
@ -561,16 +651,17 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
key: _overlayKey, key: _overlayKey,
child: child:
(_centerRequest == null || _centerImageProvider == null) && (_centerRequest == null || _centerImageProvider == null) &&
(_activeRequest == null || _activeFlights.isEmpty
_activeImageProvider == null ||
_activeTargetOffset == null)
? const SizedBox.shrink() ? const SizedBox.shrink()
: AnimatedBuilder( : AnimatedBuilder(
animation: _controller, animation: _controller,
builder: (context, child) { builder: (context, child) {
final center = overlaySize.center(Offset.zero); final center = overlaySize.center(Offset.zero);
final currentCenterRequest = final currentCenterRequest =
_centerRequest ?? _activeRequest; _centerRequest ??
(_activeFlights.isNotEmpty
? _activeFlights.first.request
: null);
final children = <Widget>[]; final children = <Widget>[];
if (currentCenterRequest != null && if (currentCenterRequest != null &&
@ -585,40 +676,38 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
); );
} }
final activeRequest = _activeRequest; if (_activeFlights.isNotEmpty) {
final activeImageProvider = _activeImageProvider;
final activeTargetOffset = _activeTargetOffset;
if (activeRequest != null &&
activeImageProvider != null &&
activeTargetOffset != null) {
final travelProgress = Curves.easeInOutCubic.transform( final travelProgress = Curves.easeInOutCubic.transform(
_controller.value.clamp(0.0, 1.0), _controller.value.clamp(0.0, 1.0),
); );
final nodeCenter =
Offset.lerp(
center,
activeTargetOffset,
travelProgress,
)!;
final nodeSize =
lerpDouble(
activeRequest.beginSize,
activeRequest.endSize,
travelProgress,
)!;
final fadeOut = final fadeOut =
1 - 1 -
Curves.easeOut.transform( Curves.easeOut.transform(
((travelProgress - 0.82) / 0.18).clamp(0.0, 1.0), ((travelProgress - 0.82) / 0.18).clamp(0.0, 1.0),
); );
children.add( for (final activeFlight in _activeFlights) {
_buildGiftNode( final activeRequest = activeFlight.request;
imageProvider: activeImageProvider, final nodeCenter =
center: nodeCenter, Offset.lerp(
size: nodeSize, center,
opacity: fadeOut, activeFlight.targetOffset,
), travelProgress,
); )!;
final nodeSize =
lerpDouble(
activeRequest.beginSize,
activeRequest.endSize,
travelProgress,
)!;
children.add(
_buildGiftNode(
imageProvider: activeFlight.imageProvider,
center: nodeCenter,
size: nodeSize,
opacity: fadeOut,
),
);
}
} }
return RepaintBoundary( return RepaintBoundary(
@ -677,6 +766,18 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
} }
} }
class _ActiveRoomGiftSeatFlight {
const _ActiveRoomGiftSeatFlight({
required this.request,
required this.imageProvider,
required this.targetOffset,
});
final RoomGiftSeatFlightRequest request;
final ImageProvider<Object> imageProvider;
final Offset targetOffset;
}
class _QueuedRoomGiftSeatFlightRequest { class _QueuedRoomGiftSeatFlightRequest {
const _QueuedRoomGiftSeatFlightRequest({ const _QueuedRoomGiftSeatFlightRequest({
required this.request, required this.request,

View File

@ -0,0 +1,401 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:flutter_debouncer/flutter_debouncer.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_svga/flutter_svga.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/main.dart';
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
import 'package:yumi/shared/tools/sc_room_utils.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
class FloatingVipEntryScreenWidget extends StatefulWidget {
const FloatingVipEntryScreenWidget({
super.key,
required this.message,
required this.onAnimationCompleted,
});
final SCFloatingMessage message;
final VoidCallback onAnimationCompleted;
@override
State<FloatingVipEntryScreenWidget> createState() =>
_FloatingVipEntryScreenWidgetState();
}
class _FloatingVipEntryScreenWidgetState
extends State<FloatingVipEntryScreenWidget>
with TickerProviderStateMixin {
static const double _bannerWidth = 350;
static const double _bannerHeight = 84;
static const String _avatarDynamicKey = "avatar";
static const String _nicknameDynamicKey = "nickname";
late final AnimationController _controller;
late final Animation<Offset> _offsetAnimation;
late final AnimationController _swipeController;
late final Animation<Offset> _swipeAnimation;
final Debouncer _debouncer = Debouncer();
bool _isSwipeAnimating = false;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 5),
vsync: this,
);
_swipeController = AnimationController(
duration: const Duration(milliseconds: 550),
vsync: this,
);
_offsetAnimation = Tween<Offset>(
begin: const Offset(1, 0),
end: const Offset(-1, 0),
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeOut));
_swipeAnimation = Tween<Offset>(
begin: Offset.zero,
end: const Offset(-1.5, 0),
).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn));
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed && !_isSwipeAnimating) {
widget.onAnimationCompleted();
}
});
_swipeController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
widget.onAnimationCompleted();
}
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_controller.forward();
}
});
}
@override
void dispose() {
_controller.dispose();
_swipeController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: _goRoom,
onHorizontalDragEnd: _handleHorizontalDragEnd,
child: SlideTransition(
position: _isSwipeAnimating ? _swipeAnimation : _offsetAnimation,
child: SizedBox(
width: _bannerWidth.w,
height: _bannerHeight.w,
child: _buildBanner(),
),
),
);
}
Widget _buildBanner() {
final url = widget.message.giftUrl?.trim();
if (url != null && url.isNotEmpty) {
if (SCNetworkSvgaWidget.isSvga(url)) {
return SCNetworkSvgaWidget(
resource: url,
width: _bannerWidth.w,
height: _bannerHeight.w,
fit: BoxFit.cover,
dynamicIdentity: [
url,
widget.message.userAvatarUrl ?? "",
widget.message.userName ?? "",
].join("|"),
movieConfigurer: _configureVipEntrySvga,
fallback: _buildFallbackBanner(widget.message.fallbackUrl),
);
}
return _buildFallbackBanner(url);
}
return _buildFallbackBanner(widget.message.fallbackUrl);
}
Widget _buildFallbackBanner(String? imageUrl) {
return Stack(
fit: StackFit.expand,
children: [
_buildImageBackground(imageUrl),
Padding(
padding: EdgeInsetsDirectional.only(start: 72.w, end: 20.w),
child: Row(
textDirection: TextDirection.ltr,
children: [
head(
url: widget.message.userAvatarUrl ?? "",
width: 50.w,
showDefault: true,
),
SizedBox(width: 10.w),
Expanded(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.message.userName ?? "",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white,
fontSize: 14.sp,
fontWeight: FontWeight.w700,
decoration: TextDecoration.none,
height: 1.1,
),
),
SizedBox(height: 3.w),
Text(
SCAppLocalizations.of(context)!.enterTheRoom,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: Colors.white.withValues(alpha: 0.92),
fontSize: 12.sp,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
height: 1.1,
),
),
],
),
),
],
),
),
],
);
}
Widget _buildImageBackground(String? url) {
final imageUrl = url?.trim();
if (imageUrl == null || imageUrl.isEmpty) {
return _buildFallbackBackground();
}
return ClipRRect(
borderRadius: BorderRadius.circular(_bannerHeight.w / 2),
child: netImage(
url: imageUrl,
width: _bannerWidth.w,
height: _bannerHeight.w,
fit: BoxFit.cover,
noDefaultImg: true,
errorWidget: _buildFallbackBackground(),
),
);
}
Widget _buildFallbackBackground() {
return DecoratedBox(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(_bannerHeight.w / 2),
gradient: const LinearGradient(
colors: [Color(0xFF6573FF), Color(0xFF8D5CFF)],
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.18),
blurRadius: 10.w,
offset: Offset(0, 4.w),
),
],
),
);
}
Future<void> _configureVipEntrySvga(MovieEntity movieEntity) async {
_logMissingDynamicKeys(movieEntity);
final avatarUrl = (widget.message.userAvatarUrl ?? "").trim();
if (avatarUrl.isNotEmpty) {
try {
final avatarImage = await _loadCircularUiImage(
avatarUrl,
logicalSize: 96,
);
if (avatarImage != null) {
movieEntity.dynamicItem.setImage(avatarImage, _avatarDynamicKey);
}
} catch (error) {
debugPrint("[VIP][RoomFloat][SVGA] avatar sync failed: $error");
}
}
final nickname = (widget.message.userName ?? "").trim();
if (nickname.isNotEmpty) {
try {
movieEntity.dynamicItem.setImage(
await _buildTransparentImage(),
_nicknameDynamicKey,
);
movieEntity.dynamicItem.setText(
TextPainter(
textDirection: TextDirection.ltr,
maxLines: 1,
ellipsis: "",
text: TextSpan(
text: nickname,
style: const TextStyle(
color: Colors.white,
fontSize: 26,
fontWeight: FontWeight.w800,
height: 1,
decoration: TextDecoration.none,
shadows: [
Shadow(
color: Color(0x99000000),
blurRadius: 2,
offset: Offset(0, 1),
),
],
),
),
)..layout(maxWidth: 220),
_nicknameDynamicKey,
);
} catch (error) {
debugPrint("[VIP][RoomFloat][SVGA] nickname sync failed: $error");
}
}
}
void _logMissingDynamicKeys(MovieEntity movieEntity) {
final keys =
movieEntity.sprites
.map((sprite) => sprite.imageKey)
.where((key) => key.isNotEmpty)
.toSet();
final missing = <String>[
if (!keys.contains(_avatarDynamicKey)) _avatarDynamicKey,
if (!keys.contains(_nicknameDynamicKey)) _nicknameDynamicKey,
];
if (missing.isEmpty) {
return;
}
debugPrint(
"[VIP][RoomFloat][SVGA] dynamic keys missing=${missing.join(",")} "
"available=${keys.take(30).join(",")}",
);
}
Future<ui.Image?> _loadCircularUiImage(
String resource, {
required int logicalSize,
}) async {
final image = await _loadUiImage(
resource,
logicalWidth: logicalSize.toDouble(),
logicalHeight: logicalSize.toDouble(),
);
if (image == null) {
return null;
}
final size = logicalSize.toDouble();
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
final rect = Rect.fromLTWH(0, 0, size, size);
canvas.clipPath(Path()..addOval(rect));
paintImage(
canvas: canvas,
rect: rect,
image: image,
fit: BoxFit.cover,
filterQuality: FilterQuality.medium,
);
return recorder.endRecording().toImage(logicalSize, logicalSize);
}
Future<ui.Image?> _loadUiImage(
String resource, {
double? logicalWidth,
double? logicalHeight,
}) async {
final target = resource.trim();
if (target.isEmpty) {
return null;
}
final provider = buildCachedImageProvider(
target,
logicalWidth: logicalWidth,
logicalHeight: logicalHeight,
);
final stream = provider.resolve(ImageConfiguration.empty);
final completer = Completer<ui.Image?>();
late final ImageStreamListener listener;
listener = ImageStreamListener(
(imageInfo, synchronousCall) {
if (!completer.isCompleted) {
completer.complete(imageInfo.image);
}
stream.removeListener(listener);
},
onError: (Object error, StackTrace? stackTrace) {
if (!completer.isCompleted) {
completer.complete(null);
}
stream.removeListener(listener);
},
);
stream.addListener(listener);
return completer.future.timeout(
const Duration(seconds: 2),
onTimeout: () {
stream.removeListener(listener);
return null;
},
);
}
Future<ui.Image> _buildTransparentImage() {
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
canvas.drawColor(Colors.transparent, BlendMode.clear);
return recorder.endRecording().toImage(1, 1);
}
void _goRoom() {
_debouncer.debounce(
duration: const Duration(milliseconds: 350),
onDebounce: () {
final roomId = widget.message.roomId ?? "";
if (roomId.isEmpty) return;
SCRoomUtils.goRoom(
roomId,
navigatorKey.currentState!.context,
fromFloting: true,
);
},
);
}
void _handleHorizontalDragEnd(DragEndDetails details) {
final textDirection = Directionality.of(context);
final velocity =
textDirection == TextDirection.rtl
? -(details.primaryVelocity ?? 0)
: (details.primaryVelocity ?? 0);
if (velocity >= 0 || _isSwipeAnimating) return;
setState(() => _isSwipeAnimating = true);
_controller.stop();
_swipeController.reset();
_swipeController.forward();
}
}

View File

@ -22,25 +22,53 @@ import '../../../services/audio/rtm_manager.dart';
import '../../components/sc_debounce_widget.dart'; import '../../components/sc_debounce_widget.dart';
class RoomBottomWidget extends StatefulWidget { class RoomBottomWidget extends StatefulWidget {
const RoomBottomWidget({super.key}); const RoomBottomWidget({super.key, this.showGiftComboButton = true});
final bool showGiftComboButton;
@override @override
State<RoomBottomWidget> createState() => _RoomBottomWidgetState(); State<RoomBottomWidget> createState() => _RoomBottomWidgetState();
} }
class RoomGiftComboFloatingLayer extends StatelessWidget {
const RoomGiftComboFloatingLayer({super.key});
@override
Widget build(BuildContext context) {
return Selector<RtcProvider, bool>(
selector: (context, provider) => _shouldShowRoomBottomMic(provider),
builder: (context, showMic, child) {
return LayoutBuilder(
builder: (context, constraints) {
final inputWidth = _resolveRoomBottomInputWidth(
maxWidth: constraints.maxWidth,
showMic: showMic,
);
final giftCenterX = _resolveRoomGiftCenterX(
maxWidth: constraints.maxWidth,
inputWidth: inputWidth,
showMic: showMic,
);
return Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: giftCenterX - (_floatingButtonWidth.w / 2),
bottom: _floatingButtonBottomOffset.w,
child: const RoomGiftComboFloatingButton(),
),
],
);
},
);
},
);
}
}
class _RoomBottomWidgetState extends State<RoomBottomWidget> { class _RoomBottomWidgetState extends State<RoomBottomWidget> {
int roomMenuStime1 = 0; int roomMenuStime1 = 0;
static const double _bottomBarHeight = 72;
static const double _floatingButtonHostHeight = 122;
static const double _floatingButtonWidth =
RoomGiftComboFloatingButton.hostSize;
static const double _floatingButtonBottomOffset = 54;
static const double _giftActionWidth = 52;
static const double _circleActionWidth = 46;
static const double _emojiActionWidth = 38;
static const double _chatEmojiGap = 8;
static const double _compactGap = 14;
static const double _horizontalPadding = 16;
@override @override
void dispose() { void dispose() {
@ -55,17 +83,17 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
child: Selector<RtcProvider, _RoomBottomSnapshot>( child: Selector<RtcProvider, _RoomBottomSnapshot>(
selector: selector:
(context, provider) => _RoomBottomSnapshot( (context, provider) => _RoomBottomSnapshot(
showMic: _shouldShowMic(provider), showMic: _shouldShowRoomBottomMic(provider),
isMic: provider.isMic, isMic: provider.isMic,
), ),
builder: (context, bottomSnapshot, child) { builder: (context, bottomSnapshot, child) {
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final inputWidth = _resolveInputWidth( final inputWidth = _resolveRoomBottomInputWidth(
maxWidth: constraints.maxWidth, maxWidth: constraints.maxWidth,
showMic: bottomSnapshot.showMic, showMic: bottomSnapshot.showMic,
); );
final giftCenterX = _resolveGiftCenterX( final giftCenterX = _resolveRoomGiftCenterX(
maxWidth: constraints.maxWidth, maxWidth: constraints.maxWidth,
inputWidth: inputWidth, inputWidth: inputWidth,
showMic: bottomSnapshot.showMic, showMic: bottomSnapshot.showMic,
@ -74,11 +102,12 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
return Stack( return Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
Positioned( if (widget.showGiftComboButton)
left: giftCenterX - (_floatingButtonWidth.w / 2), Positioned(
bottom: _floatingButtonBottomOffset.w, left: giftCenterX - (_floatingButtonWidth.w / 2),
child: const RoomGiftComboFloatingButton(), bottom: _floatingButtonBottomOffset.w,
), child: const RoomGiftComboFloatingButton(),
),
Positioned( Positioned(
left: 0, left: 0,
right: 0, right: 0,
@ -199,58 +228,6 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
); );
} }
double _resolveInputWidth({required double maxWidth, required bool showMic}) {
final contentWidth = maxWidth - (_horizontalPadding.w * 2);
final actionWidth =
_emojiActionWidth.w +
_chatEmojiGap.w +
_giftActionWidth.w +
_circleActionWidth.w * (showMic ? 3 : 2) +
(showMic ? 16.w : _compactGap.w * 2);
final availableWidth = contentWidth - actionWidth;
return availableWidth.clamp(82.w, 112.w).toDouble();
}
double _resolveGiftCenterX({
required double maxWidth,
required double inputWidth,
required bool showMic,
}) {
final contentWidth = maxWidth - (_horizontalPadding.w * 2);
if (showMic) {
final occupiedWidth =
inputWidth +
_chatEmojiGap.w +
_emojiActionWidth.w +
_giftActionWidth.w +
_circleActionWidth.w * 3;
final gapCount = 4;
final gap = ((contentWidth - occupiedWidth) / gapCount).clamp(
0.0,
double.infinity,
);
return _horizontalPadding.w +
inputWidth +
_chatEmojiGap.w +
_emojiActionWidth.w +
gap +
(_giftActionWidth.w / 2);
}
final fixedWidth =
inputWidth +
_chatEmojiGap.w +
_emojiActionWidth.w +
_giftActionWidth.w +
_circleActionWidth.w * 2 +
_compactGap.w * 2;
final spacerWidth = (contentWidth - fixedWidth).clamp(0.0, double.infinity);
return _horizontalPadding.w +
inputWidth +
spacerWidth +
(_giftActionWidth.w / 2);
}
void _showGiftPanel() { void _showGiftPanel() {
SmartDialog.show( SmartDialog.show(
tag: "showGiftControl", tag: "showGiftControl",
@ -354,10 +331,76 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
), ),
); );
} }
}
bool _shouldShowMic(RtcProvider provider) { const double _bottomBarHeight = 72;
return provider.isOnMai(); const double _floatingButtonHostHeight = 122;
const double _floatingButtonWidth = RoomGiftComboFloatingButton.hostSize;
const double _floatingButtonBottomOffset = 54;
const double _giftActionWidth = 52;
const double _circleActionWidth = 46;
const double _emojiActionWidth = 38;
const double _chatEmojiGap = 8;
const double _compactGap = 14;
const double _horizontalPadding = 16;
double _resolveRoomBottomInputWidth({
required double maxWidth,
required bool showMic,
}) {
final contentWidth = maxWidth - (_horizontalPadding.w * 2);
final actionWidth =
_emojiActionWidth.w +
_chatEmojiGap.w +
_giftActionWidth.w +
_circleActionWidth.w * (showMic ? 3 : 2) +
(showMic ? 16.w : _compactGap.w * 2);
final availableWidth = contentWidth - actionWidth;
return availableWidth.clamp(82.w, 112.w).toDouble();
}
double _resolveRoomGiftCenterX({
required double maxWidth,
required double inputWidth,
required bool showMic,
}) {
final contentWidth = maxWidth - (_horizontalPadding.w * 2);
if (showMic) {
final occupiedWidth =
inputWidth +
_chatEmojiGap.w +
_emojiActionWidth.w +
_giftActionWidth.w +
_circleActionWidth.w * 3;
final gapCount = 4;
final gap = ((contentWidth - occupiedWidth) / gapCount).clamp(
0.0,
double.infinity,
);
return _horizontalPadding.w +
inputWidth +
_chatEmojiGap.w +
_emojiActionWidth.w +
gap +
(_giftActionWidth.w / 2);
} }
final fixedWidth =
inputWidth +
_chatEmojiGap.w +
_emojiActionWidth.w +
_giftActionWidth.w +
_circleActionWidth.w * 2 +
_compactGap.w * 2;
final spacerWidth = (contentWidth - fixedWidth).clamp(0.0, double.infinity);
return _horizontalPadding.w +
inputWidth +
spacerWidth +
(_giftActionWidth.w / 2);
}
bool _shouldShowRoomBottomMic(RtcProvider provider) {
return provider.isOnMai();
} }
class _RoomBottomSnapshot { class _RoomBottomSnapshot {

View File

@ -31,32 +31,45 @@ class RoomMsgInput extends StatefulWidget {
} }
class _RoomMsgInputState extends State<RoomMsgInput> { class _RoomMsgInputState extends State<RoomMsgInput> {
static const List<String> _roomEmojiAssets = [ static const List<_RoomEmojiCategory> _roomEmojiCategories = [
"sc_images/room/emoji/fluent_emoji_01.webp", _RoomEmojiCategory(
"sc_images/room/emoji/fluent_emoji_02.webp", id: "fluent",
"sc_images/room/emoji/fluent_emoji_03.webp", iconAsset: "sc_images/room/emoji/fluent_emoji_01.webp",
"sc_images/room/emoji/fluent_emoji_04.webp", assets: [
"sc_images/room/emoji/fluent_emoji_05.webp", "sc_images/room/emoji/fluent_emoji_01.webp",
"sc_images/room/emoji/fluent_emoji_06.webp", "sc_images/room/emoji/fluent_emoji_02.webp",
"sc_images/room/emoji/fluent_emoji_07.webp", "sc_images/room/emoji/fluent_emoji_03.webp",
"sc_images/room/emoji/fluent_emoji_08.webp", "sc_images/room/emoji/fluent_emoji_04.webp",
"sc_images/room/emoji/fluent_emoji_09.webp", "sc_images/room/emoji/fluent_emoji_05.webp",
"sc_images/room/emoji/fluent_emoji_10.webp", "sc_images/room/emoji/fluent_emoji_06.webp",
"sc_images/room/emoji/monkey_1.gif", "sc_images/room/emoji/fluent_emoji_07.webp",
"sc_images/room/emoji/monkey_2.gif", "sc_images/room/emoji/fluent_emoji_08.webp",
"sc_images/room/emoji/monkey_3.gif", "sc_images/room/emoji/fluent_emoji_09.webp",
"sc_images/room/emoji/monkey_4.gif", "sc_images/room/emoji/fluent_emoji_10.webp",
"sc_images/room/emoji/monkey_5.gif", ],
"sc_images/room/emoji/monkey_6.gif", ),
"sc_images/room/emoji/monkey_7.gif", _RoomEmojiCategory(
"sc_images/room/emoji/monkey_8.gif", id: "monkey",
"sc_images/room/emoji/monkey_9.gif", iconAsset: "sc_images/room/emoji/monkey_1.gif",
"sc_images/room/emoji/monkey_10.gif", assets: [
"sc_images/room/emoji/monkey_1.gif",
"sc_images/room/emoji/monkey_2.gif",
"sc_images/room/emoji/monkey_3.gif",
"sc_images/room/emoji/monkey_4.gif",
"sc_images/room/emoji/monkey_5.gif",
"sc_images/room/emoji/monkey_6.gif",
"sc_images/room/emoji/monkey_7.gif",
"sc_images/room/emoji/monkey_8.gif",
"sc_images/room/emoji/monkey_9.gif",
"sc_images/room/emoji/monkey_10.gif",
],
),
]; ];
bool showSend = false; bool showSend = false;
bool showEmoji = false; bool showEmoji = false;
bool _emojiAssetsPrecached = false; bool _emojiAssetsPrecached = false;
int _selectedEmojiCategoryIndex = 0;
final FocusNode msgNode = FocusNode(); final FocusNode msgNode = FocusNode();
final TextEditingController controller = TextEditingController(); final TextEditingController controller = TextEditingController();
@ -234,33 +247,103 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
} }
Widget _buildEmojiPanel() { Widget _buildEmojiPanel() {
final selectedCategory = _roomEmojiCategories[_selectedEmojiCategoryIndex];
return Container( return Container(
height: 150.w, height: 220.w,
decoration: BoxDecoration( decoration: BoxDecoration(
color: _panelBackgroundColor, color: _panelBackgroundColor,
borderRadius: BorderRadius.circular(height(5)), borderRadius: BorderRadius.circular(height(5)),
), ),
child: GridView.builder( child: Column(
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w), children: [
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( Expanded(
crossAxisCount: 8, child: GridView.builder(
crossAxisSpacing: 8.w, key: ValueKey(selectedCategory.id),
mainAxisSpacing: 8.w, padding: EdgeInsets.fromLTRB(18.w, 14.w, 18.w, 10.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 18.w,
mainAxisSpacing: 12.w,
),
itemCount: selectedCategory.assets.length,
itemBuilder: (context, index) {
final emojiAsset = selectedCategory.assets[index];
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
_sendRoomEmoji(emojiAsset);
},
child: Center(
child: RoomEmojiAssetImage(
key: ValueKey(emojiAsset),
asset: emojiAsset,
width: 42.w,
height: 42.w,
),
),
);
},
),
),
_buildEmojiCategoryBar(),
],
),
);
}
Widget _buildEmojiCategoryBar() {
return Container(
height: 50.w,
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 7.w),
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.08),
border: Border(
top: BorderSide(color: Colors.white.withValues(alpha: 0.08)),
), ),
itemCount: _roomEmojiAssets.length, ),
child: ListView.separated(
scrollDirection: Axis.horizontal,
itemCount: _roomEmojiCategories.length,
separatorBuilder: (_, __) => SizedBox(width: 10.w),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final emojiAsset = _roomEmojiAssets[index]; final category = _roomEmojiCategories[index];
final selected = index == _selectedEmojiCategoryIndex;
return GestureDetector( return GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onTap: () { onTap: () {
_sendRoomEmoji(emojiAsset); if (selected) {
return;
}
setState(() {
_selectedEmojiCategoryIndex = index;
});
}, },
child: Center( child: AnimatedContainer(
child: RoomEmojiAssetImage( duration: const Duration(milliseconds: 160),
key: ValueKey(emojiAsset), curve: Curves.easeOut,
asset: emojiAsset, width: 64.w,
width: 30.w, height: 36.w,
height: 30.w, decoration: BoxDecoration(
color:
selected
? Colors.white.withValues(alpha: 0.22)
: Colors.white.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(18.w),
border: Border.all(
color:
selected
? Colors.white.withValues(alpha: 0.30)
: Colors.transparent,
width: 1.w,
),
),
child: Center(
child: RoomEmojiAssetImage(
key: ValueKey("emoji_category_${category.id}"),
asset: category.iconAsset,
width: 26.w,
height: 26.w,
),
), ),
), ),
); );
@ -290,8 +373,11 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
return; return;
} }
_emojiAssetsPrecached = true; _emojiAssetsPrecached = true;
for (final asset in _roomEmojiAssets) { for (final category in _roomEmojiCategories) {
precacheImage(AssetImage(asset), context); precacheImage(AssetImage(category.iconAsset), context);
for (final asset in category.assets) {
precacheImage(AssetImage(asset), context);
}
} }
} }
@ -346,6 +432,18 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
} }
} }
class _RoomEmojiCategory {
const _RoomEmojiCategory({
required this.id,
required this.iconAsset,
required this.assets,
});
final String id;
final String iconAsset;
final List<String> assets;
}
class PopRoute extends PopupRoute { class PopRoute extends PopupRoute {
final Duration _duration = Duration(milliseconds: 350); final Duration _duration = Duration(milliseconds: 350);
Widget child; Widget child;

View File

@ -1,4 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math' as math;
import 'package:extended_text/extended_text.dart'; import 'package:extended_text/extended_text.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -18,6 +20,8 @@ import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
import 'package:yumi/shared/business_logic/usecases/sc_case.dart'; import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
import 'package:yumi/modules/index/main_route.dart'; import 'package:yumi/modules/index/main_route.dart';
import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart'; import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart';
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
import '../../../shared/data_sources/models/enum/sc_activity_reward_props_type.dart'; import '../../../shared/data_sources/models/enum/sc_activity_reward_props_type.dart';
import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart'; import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart';
@ -28,13 +32,12 @@ import '../../components/text/sc_text.dart';
///item ///item
class MsgItem extends StatefulWidget { class MsgItem extends StatefulWidget {
final Function(SocialChatUserProfile? user) onClick; final Function(SocialChatUserProfile? user) onClick;
Msg msg; final Msg msg;
MsgItem({Key? key, required this.msg, required this.onClick}) const MsgItem({super.key, required this.msg, required this.onClick});
: super(key: key);
@override @override
_MsgItemState createState() => _MsgItemState(); State<MsgItem> createState() => _MsgItemState();
} }
class _MsgItemState extends State<MsgItem> { class _MsgItemState extends State<MsgItem> {
@ -130,42 +133,11 @@ class _MsgItemState extends State<MsgItem> {
Flexible( Flexible(
// 使Flexible替代Expanded // 使Flexible替代Expanded
child: GestureDetector( child: GestureDetector(
child: Container( child: _buildTextMessageBubble(),
constraints: BoxConstraints(
maxWidth: ScreenUtil().screenWidth * 0.7, //
),
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.only(
topRight: Radius.circular(15.w),
bottomLeft: Radius.circular(15.w),
bottomRight: Radius.circular(15.w),
),
),
padding: EdgeInsets.symmetric(
horizontal: 10.w,
vertical: 4.w,
),
margin: EdgeInsetsDirectional.only(
start: 54.w,
).copyWith(bottom: 8.w),
child: ExtendedText(
widget.msg?.msg ?? '',
style: TextStyle(
fontSize: sp(13),
color: Color(_msgColor(widget.msg.type ?? "")),
fontWeight: FontWeight.w500,
),
softWrap: true, //
specialTextSpanBuilder: AtTextSpanBuilder(
onTapCall: (userId) {},
),
),
),
onLongPress: () { onLongPress: () {
if (!(widget.msg?.msg ?? "").startsWith(AtText.flag)) { if (!(widget.msg.msg ?? "").startsWith(AtText.flag)) {
Clipboard.setData( Clipboard.setData(
ClipboardData(text: widget.msg?.msg ?? ''), ClipboardData(text: widget.msg.msg ?? ''),
); );
SCTts.show("Copied"); SCTts.show("Copied");
} }
@ -194,7 +166,7 @@ class _MsgItemState extends State<MsgItem> {
borderRadius: BorderRadius.all(Radius.circular(15.w)), borderRadius: BorderRadius.all(Radius.circular(15.w)),
), ),
child: Text( child: Text(
widget.msg?.msg ?? '', widget.msg.msg ?? '',
style: TextStyle( style: TextStyle(
fontSize: sp(13), fontSize: sp(13),
color: Color(_msgColor(widget.msg.type ?? "")), color: Color(_msgColor(widget.msg.type ?? "")),
@ -207,6 +179,295 @@ class _MsgItemState extends State<MsgItem> {
} }
} }
Widget _buildTextMessageBubble() {
final chatBubble = widget.msg.user?.getChatBox();
final hasChatBubble = _resolveRoomChatBubbleImageUrl(chatBubble).isNotEmpty;
if (hasChatBubble) {
return _buildVipTextMessageBubble(chatBubble);
}
return Container(
constraints: BoxConstraints(maxWidth: ScreenUtil().screenWidth * 0.7),
decoration: BoxDecoration(
color: Colors.black38,
borderRadius: BorderRadius.only(
topRight: Radius.circular(15.w),
bottomLeft: Radius.circular(15.w),
bottomRight: Radius.circular(15.w),
),
),
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
margin: EdgeInsetsDirectional.only(start: 54.w).copyWith(bottom: 8.w),
child: ExtendedText(
widget.msg.msg ?? '',
style: TextStyle(
fontSize: sp(13),
color: Color(_msgColor(widget.msg.type ?? "")),
fontWeight: FontWeight.w500,
),
softWrap: true,
specialTextSpanBuilder: AtTextSpanBuilder(onTapCall: (userId) {}),
),
);
}
Widget _buildVipTextMessageBubble(PropsResources? resource) {
final messageText = widget.msg.msg ?? "";
final textStyle = TextStyle(
fontSize: sp(13),
color: Color(_msgColor(widget.msg.type ?? "")),
fontWeight: FontWeight.w500,
);
final imageUrl = _resolveRoomChatBubbleImageUrl(resource);
final multilineMaxBubbleWidth = math.min(
ScreenUtil().screenWidth * 0.72,
300.w,
);
final multilineMaxTextWidth = math.max(
90.w,
multilineMaxBubbleWidth - 58.w,
);
final multilineTextPainter = TextPainter(
text: TextSpan(text: messageText, style: textStyle),
textDirection: Directionality.of(context),
)..layout(maxWidth: multilineMaxTextWidth);
if (multilineTextPainter.computeLineMetrics().length > 1) {
return _buildMultilineVipTextMessageBubble(
messageText: messageText,
textStyle: textStyle,
imageUrl: imageUrl,
textSize: multilineTextPainter.size,
maxBubbleWidth: multilineMaxBubbleWidth,
);
}
return _buildSingleLineVipTextMessageBubble(
messageText: messageText,
textStyle: textStyle,
imageUrl: imageUrl,
);
}
Widget _buildMultilineVipTextMessageBubble({
required String messageText,
required TextStyle textStyle,
required String imageUrl,
required Size textSize,
required double maxBubbleWidth,
}) {
final bubbleWidth = math
.max(126.w, textSize.width + 58.w)
.clamp(126.w, maxBubbleWidth);
final textMaxWidth = math.max(90.w, bubbleWidth - 58.w);
final bubbleHeight = math.max(58.w, textSize.height + 26.w);
return Container(
width: bubbleWidth,
height: bubbleHeight,
margin: EdgeInsetsDirectional.only(start: 54.w).copyWith(bottom: 8.w),
child: Stack(
fit: StackFit.expand,
children: [
_buildVipBubbleBackground(
url: imageUrl,
width: bubbleWidth,
height: bubbleHeight,
fallback: _buildVipBubbleFallbackBackground(),
),
Padding(
padding: EdgeInsetsDirectional.fromSTEB(29.w, 11.w, 29.w, 11.w),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: SizedBox(
width: textMaxWidth,
child: ExtendedText(
messageText,
style: textStyle,
softWrap: true,
specialTextSpanBuilder: AtTextSpanBuilder(
onTapCall: (userId) {},
),
),
),
),
),
],
),
);
}
Widget _buildSingleLineVipTextMessageBubble({
required String messageText,
required TextStyle textStyle,
required String imageUrl,
}) {
final horizontalPadding = 18.w;
final verticalPadding = 4.w;
final minSkinWidth = 132.w;
final minSkinHeight = 42.w;
final maxBubbleWidth = ScreenUtil().screenWidth * 0.7;
final maxTextWidth = math.max(1.0, maxBubbleWidth - horizontalPadding * 2);
final textSize = _measureMessageTextSize(
text: messageText,
style: textStyle,
maxWidth: maxTextWidth,
);
final bubbleWidth = math.min(
maxBubbleWidth,
math.max(horizontalPadding * 2, textSize.width + horizontalPadding * 2),
);
final textMaxWidth = math.max(1.0, bubbleWidth - horizontalPadding * 2);
final bubbleHeight = math.max(
verticalPadding * 2,
textSize.height + verticalPadding * 2,
);
return Container(
width: bubbleWidth,
height: bubbleHeight,
margin: EdgeInsetsDirectional.only(start: 54.w).copyWith(bottom: 8.w),
child: Stack(
clipBehavior: Clip.none,
fit: StackFit.expand,
children: [
Positioned.fill(child: _buildVipBubbleFallbackBackground()),
Positioned.fill(
child: _buildVipBubbleSkin(
url: imageUrl,
width: math.max(bubbleWidth, minSkinWidth),
height: math.max(bubbleHeight, minSkinHeight),
alignment: AlignmentDirectional.centerStart,
),
),
Padding(
padding: EdgeInsetsDirectional.symmetric(
horizontal: horizontalPadding,
vertical: verticalPadding,
),
child: Align(
alignment: AlignmentDirectional.centerStart,
child: SizedBox(
width: textMaxWidth,
child: ExtendedText(
messageText,
style: textStyle,
softWrap: true,
specialTextSpanBuilder: AtTextSpanBuilder(
onTapCall: (userId) {},
),
),
),
),
),
],
),
);
}
Widget _buildVipBubbleSkin({
required String url,
required double width,
required double height,
required AlignmentGeometry alignment,
}) {
return OverflowBox(
alignment: alignment,
minWidth: width,
maxWidth: width,
minHeight: height,
maxHeight: height,
child:
SCNetworkSvgaWidget.isSvga(url)
? SCNetworkSvgaWidget(
resource: url,
width: width,
height: height,
fit: BoxFit.fill,
fallback: SizedBox(width: width, height: height),
)
: SCNinePatchImage(
url: url,
width: width,
height: height,
centerSliceRatio: const EdgeInsets.fromLTRB(
0.34,
0.34,
0.34,
0.34,
),
fallback: SizedBox(width: width, height: height),
),
);
}
Widget _buildVipBubbleBackground({
required String url,
required double width,
required double height,
required Widget fallback,
}) {
if (SCNetworkSvgaWidget.isSvga(url)) {
return SCNetworkSvgaWidget(
resource: url,
width: width,
height: height,
fit: BoxFit.fill,
fallback: fallback,
);
}
return SCNinePatchImage(
url: url,
width: width,
height: height,
centerSliceRatio: const EdgeInsets.fromLTRB(0.34, 0.34, 0.34, 0.34),
fallback: fallback,
);
}
Size _measureMessageTextSize({
required String text,
required TextStyle style,
required double maxWidth,
}) {
final textPainter = TextPainter(
text: TextSpan(text: text, style: style),
textDirection: Directionality.of(context),
)..layout(maxWidth: maxWidth);
return textPainter.size;
}
Widget _buildVipBubbleFallbackBackground() {
return DecoratedBox(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.38),
borderRadius: BorderRadius.only(
topRight: Radius.circular(18.w),
bottomLeft: Radius.circular(18.w),
bottomRight: Radius.circular(18.w),
),
),
);
}
String _resolveRoomChatBubbleImageUrl(PropsResources? resource) {
return _firstNonBlank([
resource?.roomSendCoverUrl,
resource?.roomOpenedUrl,
resource?.roomNotOpenedUrl,
resource?.cover,
]);
}
String _firstNonBlank(Iterable<String?> values) {
for (final value in values) {
final text = value?.trim();
if (text != null && text.isNotEmpty) {
return text;
}
}
return "";
}
/// ///
_systemTipsRoomItem(BuildContext context) { _systemTipsRoomItem(BuildContext context) {
return Container( return Container(
@ -224,7 +485,7 @@ class _MsgItemState extends State<MsgItem> {
borderRadius: BorderRadius.all(Radius.circular(15.w)), borderRadius: BorderRadius.all(Radius.circular(15.w)),
), ),
child: Text( child: Text(
widget.msg?.msg ?? '', widget.msg.msg ?? '',
style: TextStyle( style: TextStyle(
fontSize: sp(13), fontSize: sp(13),
height: 1.5.w, height: 1.5.w,
@ -658,7 +919,7 @@ class _MsgItemState extends State<MsgItem> {
/// ///
_enterRoomItem(BuildContext context) { _enterRoomItem(BuildContext context) {
List<InlineSpan> spans = []; List<InlineSpan> spans = [];
if (widget.msg.role != null && widget.msg.role != 0) { if (widget.msg.role != null && widget.msg.role != "0") {
spans.add( spans.add(
WidgetSpan( WidgetSpan(
alignment: PlaceholderAlignment.middle, alignment: PlaceholderAlignment.middle,
@ -671,7 +932,7 @@ class _MsgItemState extends State<MsgItem> {
} }
spans.add( spans.add(
TextSpan( TextSpan(
text: "${widget.msg?.user?.userNickname ?? ''} ", text: "${widget.msg.user?.userNickname ?? ''} ",
style: TextStyle( style: TextStyle(
fontSize: sp(13), fontSize: sp(13),
color: _roleNameColor(widget.msg.role ?? ""), color: _roleNameColor(widget.msg.role ?? ""),
@ -718,7 +979,7 @@ class _MsgItemState extends State<MsgItem> {
child: ListView.separated( child: ListView.separated(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
shrinkWrap: true, shrinkWrap: true,
itemCount: wearBadge.length ?? 0, itemCount: wearBadge.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return netImage( return netImage(
width: 25.w, width: 25.w,
@ -749,7 +1010,6 @@ class _MsgItemState extends State<MsgItem> {
case SCRoomMsgType.qcfj: case SCRoomMsgType.qcfj:
case SCRoomMsgType.killXiaMai: case SCRoomMsgType.killXiaMai:
return 0xFFFFFFFF; return 0xFFFFFFFF;
break;
// case type.text: // case type.text:
// return _mapTextColor(); // return _mapTextColor();
// break; // break;
@ -1403,6 +1663,7 @@ class Msg {
// //
SocialChatUserProfile? toUser; SocialChatUserProfile? toUser;
List<String>? targetUserIds;
int? time = 0; int? time = 0;
// //
@ -1422,6 +1683,7 @@ class Msg {
this.number, this.number,
this.customAnimationCount, this.customAnimationCount,
this.awardAmount, this.awardAmount,
this.targetUserIds,
this.hasPlayedAnimation, this.hasPlayedAnimation,
this.needUpDataUserInfo, this.needUpDataUserInfo,
}) { }) {
@ -1437,6 +1699,14 @@ class Msg {
number = json['number']; number = json['number'];
customAnimationCount = json['customAnimationCount']; customAnimationCount = json['customAnimationCount'];
awardAmount = json['awardAmount']; awardAmount = json['awardAmount'];
final dynamic rawTargetUserIds = json['targetUserIds'];
if (rawTargetUserIds is List) {
targetUserIds =
rawTargetUserIds
.map((item) => item?.toString().trim() ?? "")
.where((id) => id.isNotEmpty)
.toList();
}
hasPlayedAnimation = json['hasPlayedAnimation']; hasPlayedAnimation = json['hasPlayedAnimation'];
needUpDataUserInfo = json['needUpDataUserInfo']; needUpDataUserInfo = json['needUpDataUserInfo'];
userWheat = userWheat =
@ -1466,6 +1736,9 @@ class Msg {
map['customAnimationCount'] = customAnimationCount; map['customAnimationCount'] = customAnimationCount;
map['hasPlayedAnimation'] = hasPlayedAnimation; map['hasPlayedAnimation'] = hasPlayedAnimation;
map['needUpDataUserInfo'] = needUpDataUserInfo; map['needUpDataUserInfo'] = needUpDataUserInfo;
if (targetUserIds != null) {
map['targetUserIds'] = targetUserIds;
}
if (userWheat != null) { if (userWheat != null) {
map['userWheat'] = userWheat?.toJson(); map['userWheat'] = userWheat?.toJson();
} }

View File

@ -0,0 +1,209 @@
import 'dart:async';
import 'dart:ui' as ui;
import 'package:flutter/material.dart';
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
class SCNinePatchImage extends StatefulWidget {
const SCNinePatchImage({
super.key,
required this.url,
required this.width,
required this.height,
this.centerSliceRatio = const EdgeInsets.fromLTRB(0.34, 0.34, 0.34, 0.34),
this.sourceEdgeInset = const EdgeInsets.all(2),
this.filterQuality = FilterQuality.low,
this.fallback,
});
final String url;
final double width;
final double height;
final EdgeInsets centerSliceRatio;
final EdgeInsets sourceEdgeInset;
final FilterQuality filterQuality;
final Widget? fallback;
@override
State<SCNinePatchImage> createState() => _SCNinePatchImageState();
}
class _SCNinePatchImageState extends State<SCNinePatchImage> {
ImageStream? _imageStream;
ImageStreamListener? _imageListener;
ui.Image? _image;
bool _hasError = false;
int _resolveSerial = 0;
@override
void initState() {
super.initState();
_resolveImage();
}
@override
void didUpdateWidget(covariant SCNinePatchImage oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.url != widget.url ||
oldWidget.width != widget.width ||
oldWidget.height != widget.height ||
oldWidget.sourceEdgeInset != widget.sourceEdgeInset) {
_resolveImage();
}
}
@override
void dispose() {
_resolveSerial++;
_removeListener();
_image?.dispose();
super.dispose();
}
void _resolveImage() {
_removeListener();
_image?.dispose();
_image = null;
final serial = ++_resolveSerial;
final url = widget.url.trim();
if (url.isEmpty) {
setState(() {
_hasError = true;
});
return;
}
final provider = buildCachedImageProvider(
url,
logicalWidth: widget.width,
logicalHeight: widget.height,
);
final stream = provider.resolve(ImageConfiguration.empty);
late final ImageStreamListener listener;
listener = ImageStreamListener(
(imageInfo, synchronousCall) {
unawaited(_cropAndUseImage(imageInfo.image, serial));
},
onError: (error, stackTrace) {
if (!mounted || serial != _resolveSerial) {
return;
}
debugPrint("[SCNinePatchImage] load failed url=$url error=$error");
setState(() {
_image = null;
_hasError = true;
});
},
);
_imageStream = stream;
_imageListener = listener;
stream.addListener(listener);
}
Future<void> _cropAndUseImage(ui.Image sourceImage, int serial) async {
try {
final croppedImage = await _cropImage(sourceImage);
if (!mounted || serial != _resolveSerial) {
croppedImage.dispose();
return;
}
setState(() {
_image?.dispose();
_image = croppedImage;
_hasError = false;
});
} catch (error) {
debugPrint("[SCNinePatchImage] crop failed error=$error");
if (!mounted || serial != _resolveSerial) {
return;
}
setState(() {
_image = null;
_hasError = true;
});
}
}
Future<ui.Image> _cropImage(ui.Image sourceImage) async {
final imageWidth = sourceImage.width.toDouble();
final imageHeight = sourceImage.height.toDouble();
final left = widget.sourceEdgeInset.left.clamp(0.0, imageWidth / 4);
final top = widget.sourceEdgeInset.top.clamp(0.0, imageHeight / 4);
final right = widget.sourceEdgeInset.right.clamp(0.0, imageWidth / 4);
final bottom = widget.sourceEdgeInset.bottom.clamp(0.0, imageHeight / 4);
final sourceRect = Rect.fromLTRB(
left,
top,
imageWidth - right,
imageHeight - bottom,
);
final croppedWidth = sourceRect.width.round().clamp(1, sourceImage.width);
final croppedHeight = sourceRect.height.round().clamp(
1,
sourceImage.height,
);
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);
canvas.drawImageRect(
sourceImage,
sourceRect,
Rect.fromLTWH(0, 0, croppedWidth.toDouble(), croppedHeight.toDouble()),
Paint()..filterQuality = FilterQuality.low,
);
final picture = recorder.endRecording();
final croppedImage = await picture.toImage(croppedWidth, croppedHeight);
picture.dispose();
return croppedImage;
}
void _removeListener() {
final stream = _imageStream;
final listener = _imageListener;
if (stream != null && listener != null) {
stream.removeListener(listener);
}
_imageStream = null;
_imageListener = null;
}
@override
Widget build(BuildContext context) {
final image = _image;
if (_hasError || image == null) {
return widget.fallback ??
SizedBox(width: widget.width, height: widget.height);
}
return RawImage(
image: image,
width: widget.width,
height: widget.height,
fit: BoxFit.fill,
centerSlice: _buildCenterSlice(image),
filterQuality: widget.filterQuality,
);
}
Rect _buildCenterSlice(ui.Image image) {
final imageWidth = image.width.toDouble();
final imageHeight = image.height.toDouble();
final left = (imageWidth * widget.centerSliceRatio.left).clamp(
1.0,
imageWidth - 2.0,
);
final top = (imageHeight * widget.centerSliceRatio.top).clamp(
1.0,
imageHeight - 2.0,
);
final right = (imageWidth * widget.centerSliceRatio.right).clamp(
1.0,
imageWidth - left - 1.0,
);
final bottom = (imageHeight * widget.centerSliceRatio.bottom).clamp(
1.0,
imageHeight - top - 1.0,
);
return Rect.fromLTRB(left, top, imageWidth - right, imageHeight - bottom);
}
}

View File

@ -0,0 +1,179 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_svga/flutter_svga.dart';
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
typedef SCNetworkSvgaMovieConfigurer =
FutureOr<void> Function(MovieEntity movieEntity);
class SCNetworkSvgaWidget extends StatefulWidget {
const SCNetworkSvgaWidget({
super.key,
required this.resource,
this.width,
this.height,
this.active = true,
this.loop = false,
this.fit = BoxFit.contain,
this.filterQuality = FilterQuality.low,
this.allowDrawingOverflow = false,
this.fallback,
this.dynamicIdentity,
this.movieConfigurer,
});
final String resource;
final double? width;
final double? height;
final bool active;
final bool loop;
final BoxFit fit;
final FilterQuality filterQuality;
final bool allowDrawingOverflow;
final Widget? fallback;
final Object? dynamicIdentity;
final SCNetworkSvgaMovieConfigurer? movieConfigurer;
static bool isSvga(String? resource) {
final lower = resource?.trim().toLowerCase() ?? "";
return lower.endsWith(".svga") || lower.contains(".svga?");
}
@override
State<SCNetworkSvgaWidget> createState() => _SCNetworkSvgaWidgetState();
}
class _SCNetworkSvgaWidgetState extends State<SCNetworkSvgaWidget>
with SingleTickerProviderStateMixin {
late final SVGAAnimationController _controller;
String? _loadedResource;
bool _hasError = false;
@override
void initState() {
super.initState();
_controller = SVGAAnimationController(vsync: this);
_loadResource();
}
@override
void didUpdateWidget(covariant SCNetworkSvgaWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.resource != widget.resource ||
oldWidget.dynamicIdentity != widget.dynamicIdentity) {
_loadResource();
return;
}
if (oldWidget.active != widget.active || oldWidget.loop != widget.loop) {
_syncPlayback(restartIfActive: widget.active && !oldWidget.active);
}
}
Future<void> _loadResource() async {
final resource = widget.resource.trim();
if (resource.isEmpty || !SCNetworkSvgaWidget.isSvga(resource)) {
setState(() {
_hasError = true;
_loadedResource = null;
_controller.videoItem = null;
});
return;
}
setState(() {
_hasError = false;
});
try {
final cache = SCGiftVapSvgaManager().videoItemCache;
var movieEntity = cache[resource];
if (movieEntity == null) {
movieEntity =
resource.startsWith("http")
? await SVGAParser.shared.decodeFromURL(resource)
: await SVGAParser.shared.decodeFromAssets(resource);
movieEntity.autorelease = false;
cache[resource] = movieEntity;
}
if (widget.movieConfigurer != null) {
movieEntity.dynamicItem.reset();
await widget.movieConfigurer!(movieEntity);
}
if (!mounted || widget.resource.trim() != resource) {
return;
}
setState(() {
_loadedResource = resource;
_controller.videoItem = movieEntity;
});
_syncPlayback(restartIfActive: true);
} catch (error) {
debugPrint("[SCNetworkSVGA] load failed resource=$resource error=$error");
if (!mounted || widget.resource.trim() != resource) {
return;
}
setState(() {
_hasError = true;
_loadedResource = null;
_controller.videoItem = null;
});
}
}
void _syncPlayback({bool restartIfActive = false}) {
if (!widget.active || _controller.videoItem == null) {
_controller.stop();
return;
}
if (widget.loop) {
if (restartIfActive || !_controller.isAnimating) {
_controller.reset();
_controller.repeat();
}
return;
}
if (restartIfActive || !_controller.isAnimating) {
_controller.reset();
_controller.repeat(count: 1);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_hasError ||
_controller.videoItem == null ||
_loadedResource != widget.resource.trim()) {
return _buildFallback();
}
return SizedBox(
width: widget.width,
height: widget.height,
child: IgnorePointer(
child: SVGAImage(
_controller,
fit: widget.fit,
clearsAfterStop: false,
filterQuality: widget.filterQuality,
allowDrawingOverflow: widget.allowDrawingOverflow,
preferredSize:
widget.width != null && widget.height != null
? Size(widget.width!, widget.height!)
: null,
),
),
);
}
Widget _buildFallback() {
return widget.fallback ??
SizedBox(width: widget.width, height: widget.height);
}
}

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts # In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix. # of the product and file versions while build-number is used as the build suffix.
version: 1.2.6+8 version: 1.2.7+9
environment: environment:

View File

@ -0,0 +1,53 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart';
void main() {
group('SCGiftVapSvgaManager priorities', () {
test('entry effects keep the highest default priority', () {
final giftPriority = SCGiftVapSvgaManager.resolveEffectPriority();
final entryPriority = SCGiftVapSvgaManager.resolveEffectPriority(
type: SCGiftVapSvgaManager.entryEffectType,
);
expect(entryPriority, greaterThan(giftPriority));
expect(entryPriority, SCGiftVapSvgaManager.entryEffectPriority);
});
test('explicit gift priority is kept below default entry priority', () {
expect(SCGiftVapSvgaManager.resolveEffectPriority(priority: 200), 200);
expect(
SCGiftVapSvgaManager.resolveEffectPriority(
priority: 200,
type: SCGiftVapSvgaManager.entryEffectType,
),
SCGiftVapSvgaManager.entryEffectPriority,
);
});
test(
'priority queue still orders entry before gift in the same channel',
() {
final queue = SCPriorityQueue<SCVapTask>((a, b) => a.compareTo(b));
queue.add(
SCVapTask(
path: 'entry.svga',
type: SCGiftVapSvgaManager.entryEffectType,
priority: SCGiftVapSvgaManager.resolveEffectPriority(
type: SCGiftVapSvgaManager.entryEffectType,
),
),
);
queue.add(
SCVapTask(
path: 'gift.svga',
priority: SCGiftVapSvgaManager.resolveEffectPriority(),
),
);
expect(queue.removeFirst().path, 'entry.svga');
expect(queue.removeFirst().path, 'gift.svga');
},
);
});
}

View File

@ -0,0 +1,29 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yumi/shared/tools/sc_path_utils.dart';
void main() {
group('SCPathUtils.getFileExtension', () {
test('normalizes query and fragment from svga urls', () {
expect(
SCPathUtils.getFileExtension(
'https://example.test/gifts/effect.svga?Expires=1&Signature=abc',
),
'.svga',
);
expect(
SCPathUtils.getFileExtension(
'https://example.test/gifts/effect.SVGA#preview',
),
'.svga',
);
});
test('keeps local and asset paths unchanged', () {
expect(
SCPathUtils.getFileExtension('sc_images/room/effect.svga'),
'.svga',
);
expect(SCPathUtils.getFileExtension('/tmp/effect.mp4'), '.mp4');
});
});
}

View File

@ -0,0 +1,185 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:yumi/shared/business_logic/models/res/join_room_res.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
import 'package:yumi/shared/business_logic/models/res/room_gift_rank_res.dart';
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart';
import 'package:yumi/shared/business_logic/models/res/room_res.dart';
import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart';
void main() {
group('VIP colored ID', () {
test('uses explicit VIP level from user profile', () {
final profile = SocialChatUserProfile.fromJson({
'account': '10001',
'vipLevel': 3,
});
expect(profile.vipLevelForColoredId, 3);
expect(profile.shouldShowColoredSpecialIdText(), isTrue);
});
test('VIP level zero disables legacy prop fallback', () {
final profile = SocialChatUserProfile.fromJson({
'account': '10001',
'vipLevel': 0,
'useProps': [
{
'propsResources': {'type': 'NOBLE_VIP', 'code': '10026'},
},
],
});
expect(profile.vipLevelForColoredId, 0);
expect(profile.shouldShowColoredSpecialIdText(), isFalse);
});
test('keeps legacy NOBLE_VIP prop support', () {
final profile = SocialChatUserProfile.fromJson({
'account': '10001',
'useProps': [
{
'propsResources': {'type': 'NOBLE_VIP', 'code': '10026'},
},
],
});
expect(profile.vipLevelForColoredId, 1);
expect(profile.shouldShowColoredSpecialIdText(), isTrue);
});
test('uses VIP visual resources for colored ID on user cards', () {
final profile = SocialChatUserProfile.fromJson({
'account': '10001',
'useProps': [
{
'propsResources': {
'type': 'AVATAR_FRAME',
'name': 'VIP5 avatar frame',
'sourceUrl': 'https://example.test/vip_5_frame.svga',
},
},
],
});
expect(profile.vipVisualResourceLevelForColoredId, 5);
expect(profile.shouldShowColoredSpecialIdText(), isTrue);
});
test('does not color inactive nested VIP status', () {
final profile = SocialChatUserProfile.fromJson({
'account': '10001',
'vipStatus': {'active': false, 'level': 5},
});
expect(profile.vipLevelForColoredId, 0);
expect(profile.shouldShowColoredSpecialIdText(), isFalse);
});
test('pipes room user card supperVip into nested profile', () {
final card = RoomUserCardRes.fromJson({
'supperVip': 'VIP4',
'userProfile': {'account': '10001'},
});
expect(card.userProfile?.vipLevelForColoredId, 4);
expect(card.userProfile?.shouldShowColoredSpecialIdText(), isTrue);
});
test('pipes room owner userSVipLevel into nested profile', () {
final room = SocialChatRoomRes.fromJson({
'userSVipLevel': '5',
'userProfile': {'account': '10001'},
});
expect(room.userProfile?.vipLevelForColoredId, 5);
expect(room.userProfile?.shouldShowColoredSpecialIdText(), isTrue);
});
test('pipes joined voice room userSVipLevel into owner profile', () {
final room = JoinRoomRes.fromJson({
'roomProfile': {
'userSVipLevel': 'VIP5',
'userProfile': {'account': '10001'},
},
});
expect(room.roomProfile?.userProfile?.vipLevelForColoredId, 5);
expect(
room.roomProfile?.userProfile?.shouldShowColoredSpecialIdText(),
isTrue,
);
});
test('pipes joined voice room supperVip into owner profile', () {
final room = JoinRoomRes.fromJson({
'roomProfile': {
'supperVip': '4',
'userProfile': {'account': '10001'},
},
});
expect(room.roomProfile?.userProfile?.vipLevelForColoredId, 4);
expect(
room.roomProfile?.userProfile?.shouldShowColoredSpecialIdText(),
isTrue,
);
});
test('pipes top-level joined voice room VIP into owner profile', () {
final room = JoinRoomRes.fromJson({
'userSVipLevel': 'SVIP6',
'roomProfile': {
'userProfile': {'account': '10001'},
},
});
expect(room.roomProfile?.userProfile?.vipLevelForColoredId, 6);
expect(
room.roomProfile?.userProfile?.shouldShowColoredSpecialIdText(),
isTrue,
);
});
test('reads VIP level from wrapped user profile payloads', () {
final profile = SocialChatUserProfile.fromJson({
'userSVipLevel': 'VIP5',
'userProfile': {'account': '10001'},
});
expect(profile.account, '10001');
expect(profile.vipLevelForColoredId, 5);
expect(profile.shouldShowColoredSpecialIdText(), isTrue);
});
test('pipes room rank VIP into nested profile', () {
final rank = RoomGiftRankRes.fromJson({
'userSVipLevel': 'VIP5',
'userProfile': {'account': '10001'},
});
expect(rank.userProfile?.vipLevelForColoredId, 5);
expect(rank.userProfile?.shouldShowColoredSpecialIdText(), isTrue);
});
test('pipes room member VIP into nested profile', () {
final member = SocialChatRoomMemberRes.fromJson({
'supperVip': '5',
'userProfile': {'account': '10001'},
});
expect(member.userProfile?.vipLevelForColoredId, 5);
expect(member.userProfile?.shouldShowColoredSpecialIdText(), isTrue);
});
test('pipes microphone VIP into nested user', () {
final mic = MicRes.fromJson({
'userSVipLevel': 'VIP5',
'user': {'account': '10001'},
});
expect(mic.user?.vipLevelForColoredId, 5);
expect(mic.user?.shouldShowColoredSpecialIdText(), isTrue);
});
});
}