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

View File

@ -1,5 +1,7 @@
import 'dart:convert';
import 'dart:io';
import 'dart:math' as math;
import 'package:extended_image/extended_image.dart'
show ExtendedImage, ExtendedRawImage, ExtendedImageState, LoadState;
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/services/auth/user_profile_manager.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';
@ -1245,31 +1249,7 @@ class _MessageItem extends StatelessWidget {
return Builder(
builder: (ct) {
return GestureDetector(
child: Container(
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,
),
),
),
),
child: _buildTextBubble(content),
onLongPress: () {
_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) {
SmartDialog.showAttach(
tag: "showMsgItemMenu",

View File

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

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

View File

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

View File

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

View File

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

View File

@ -86,6 +86,7 @@ class _MePage2State extends State<MePage2> {
'badge=${_vipEntryBadgeLogValue(status.badge)}',
);
if (!mounted) return;
_syncCurrentProfileVipStatus(status);
setState(() {
_vipStatus = status;
_isVipStatusLoading = false;
@ -113,6 +114,25 @@ class _MePage2State extends State<MePage2> {
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) {
debugPrint('[VIP][MeEntry] refresh user data after vip page return');
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/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_vip_repository_imp.dart';
import 'package:yumi/services/room/rc_room_manager.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
@ -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_rocket_status_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 '../../ui_kit/components/sc_float_ichart.dart';
@ -247,6 +249,7 @@ class RoomEntryPreviewData {
),
roomSetting: roomSetting,
userProfile: ownerProfile,
userSVipLevel: ownerProfile?.vipLevel,
),
);
}
@ -684,6 +687,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
previous.userNickname == next.userNickname &&
previous.userSex == next.userSex &&
previous.roles == next.roles &&
previous.vipLevel == next.vipLevel &&
previous.heartbeatVal == next.heartbeatVal;
}
@ -748,7 +752,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
: previousMic?.number,
)
: roomWheat;
final normalizedUserId = (mergedMic.user?.id ?? "").trim();
final hydratedMic = _mergeKnownVipIntoMic(mergedMic);
final normalizedUserId = (hydratedMic.user?.id ?? "").trim();
if (normalizedUserId.isNotEmpty) {
final existingIndex = userSeatMap[normalizedUserId];
if (existingIndex != null) {
@ -765,7 +770,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
userSeatMap[normalizedUserId] = micIndex;
}
nextMap[micIndex] = mergedMic;
nextMap[micIndex] = hydratedMic;
}
return _stabilizeSelfMicSnapshot(nextMap, previousMap: previousMap);
}
@ -2093,7 +2098,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
unawaited(_cleanupStaleEnteredRoom(enteredRoom));
return;
}
currenRoom = enteredRoom;
currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom);
_syncCurrentUserVipHintFromRoom(currenRoom);
unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial));
_currentRoomIsEntryPreview = false;
_previewRoomSeatCount = null;
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) {
return entryRequestSerial == _roomEntryRequestSerial &&
_roomStartupStatus == RoomStartupStatus.loading;
@ -2355,7 +2678,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
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) {
return;
}
final changed = !_sameOnlineUsers(onlineUsers, fetchedUsers);
onlineUsers = fetchedUsers;
final mergedUsers =
fetchedUsers.map((user) => _mergeKnownVipIntoProfile(user)!).toList();
final changed = !_sameOnlineUsers(onlineUsers, mergedUsers);
onlineUsers = mergedUsers;
_refreshManagerUsers(onlineUsers);
if (changed || notifyIfUnchanged) {
notifyListeners();
@ -3399,15 +3728,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) {
return;
}
final mergedUser = _mergeKnownVipIntoProfile(user) ?? user;
bool isExtOnlineList = false;
for (var us in onlineUsers) {
if (us.id == user.id) {
if (us.id == mergedUser.id) {
isExtOnlineList = true;
break;
}
}
if (!isExtOnlineList) {
Provider.of<RtcProvider>(context!, listen: false).onlineUsers.add(user);
Provider.of<RtcProvider>(
context!,
listen: false,
).onlineUsers.add(mergedUser);
_refreshManagerUsers(onlineUsers);
notifyListeners();
}

View File

@ -1517,8 +1517,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
shouldShowRoomVisualEffects) {
SCGiftVapSvgaManager().play(
msg.user?.getMountains()?.sourceUrl ?? "",
priority: 100,
type: 1,
priority: SCGiftVapSvgaManager.entryEffectPriority,
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/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_vip_repository_imp.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/login_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_vip_res.dart';
class SocialChatUserProfileManager extends ChangeNotifier {
SCUserProfileCmd? editUser;
@ -86,6 +88,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
return;
}
var userInfo = await SCAccountRepository().loadUserInfo(userId);
userInfo = await _withCurrentVipLevel(userInfo, userId);
syncCurrentUserProfile(userInfo);
}
@ -101,7 +104,8 @@ class SocialChatUserProfileManager extends ChangeNotifier {
void getUserInfoById(String userId) async {
userProfile = null;
userProfile = await SCAccountRepository().loadUserInfo(userId);
final loadedProfile = await SCAccountRepository().loadUserInfo(userId);
userProfile = await _withCurrentVipLevel(loadedProfile, userId);
notifyListeners();
}
@ -114,9 +118,85 @@ class SocialChatUserProfileManager extends ChangeNotifier {
void roomUserCard(String roomId, String userId) async {
userCardInfo = null;
userCardInfo = await SCChatRoomRepository().roomUserCard(roomId, userId);
userCardInfo = await _withCurrentVipLevelForCard(userCardInfo, userId);
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 {
var result = await SCAccountRepository().followUser(userId);
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}]}
/// userSVipLevel : ""
class RoomProfile {
RoomProfile({
class RoomProfile {
RoomProfile({
String? countryCode,
String? countryName,
String? event,
@ -69,12 +69,12 @@ class RoomProfile {
String? roomGameIcon,
String? roomName,
String? sysOrigin,
String? userId,
SocialChatUserProfile? userProfile,
String? userSVipLevel,
RoomMemberCounter? roomCounter,
ExtValues? extValues,
}) {
String? userId,
SocialChatUserProfile? userProfile,
String? userSVipLevel,
RoomMemberCounter? roomCounter,
ExtValues? extValues,
}) {
_countryCode = countryCode;
_countryName = countryName;
_event = event;
@ -89,12 +89,16 @@ class RoomProfile {
_roomGameIcon = roomGameIcon;
_roomName = roomName;
_sysOrigin = sysOrigin;
_userId = userId;
_userProfile = userProfile;
_userSVipLevel = userSVipLevel;
_roomCounter = roomCounter;
_extValues = extValues;
}
_userId = userId;
_userSVipLevel = userSVipLevel;
final normalizedVipLevel = userSVipLevel?.trim();
_userProfile =
normalizedVipLevel == null || normalizedVipLevel.isEmpty
? userProfile
: userProfile?.copyWith(vipLevel: normalizedVipLevel);
_roomCounter = roomCounter;
_extValues = extValues;
}
RoomProfile.fromJson(dynamic json) {
_countryCode = json['countryCode'];
@ -115,18 +119,22 @@ class RoomProfile {
_roomName = json['roomName'];
_sysOrigin = json['sysOrigin'];
_userId = json['userId'];
_userProfile =
json['userProfile'] != null
? SocialChatUserProfile.fromJson(json['userProfile'])
: null;
_userSVipLevel = json['userSVipLevel'];
final counterJson = json['roomCounter'] ?? json['counter'];
_roomCounter =
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
_extValues =
json['extValues'] != null
? ExtValues.fromJson(json['extValues'])
: null;
_userProfile =
json['userProfile'] != null
? SocialChatUserProfile.fromJson(json['userProfile'])
: null;
_userSVipLevel = socialChatVipLevelHintFromJson(json);
final normalizedVipLevel = _userSVipLevel?.trim();
if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) {
_userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel);
}
final counterJson = json['roomCounter'] ?? json['counter'];
_roomCounter =
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
_extValues =
json['extValues'] != null
? ExtValues.fromJson(json['extValues'])
: null;
}
String? _countryCode;
String? _countryName;
@ -142,11 +150,11 @@ class RoomProfile {
String? _roomGameIcon;
String? _roomName;
String? _sysOrigin;
String? _userId;
SocialChatUserProfile? _userProfile;
String? _userSVipLevel;
RoomMemberCounter? _roomCounter;
ExtValues? _extValues;
String? _userId;
SocialChatUserProfile? _userProfile;
String? _userSVipLevel;
RoomMemberCounter? _roomCounter;
ExtValues? _extValues;
RoomProfile copyWith({
String? countryCode,
String? countryName,
@ -162,12 +170,12 @@ class RoomProfile {
String? roomGameIcon,
String? roomName,
String? sysOrigin,
String? userId,
SocialChatUserProfile? userProfile,
String? userSVipLevel,
RoomMemberCounter? roomCounter,
ExtValues? extValues,
}) => RoomProfile(
String? userId,
SocialChatUserProfile? userProfile,
String? userSVipLevel,
RoomMemberCounter? roomCounter,
ExtValues? extValues,
}) => RoomProfile(
countryCode: countryCode ?? _countryCode,
countryName: countryName ?? _countryName,
event: event ?? _event,
@ -182,12 +190,12 @@ class RoomProfile {
roomGameIcon: roomGameIcon ?? _roomGameIcon,
roomName: roomName ?? _roomName,
sysOrigin: sysOrigin ?? _sysOrigin,
userId: userId ?? _userId,
userProfile: userProfile ?? _userProfile,
userSVipLevel: userSVipLevel ?? _userSVipLevel,
roomCounter: roomCounter ?? _roomCounter,
extValues: extValues ?? _extValues,
);
userId: userId ?? _userId,
userProfile: userProfile ?? _userProfile,
userSVipLevel: userSVipLevel ?? _userSVipLevel,
roomCounter: roomCounter ?? _roomCounter,
extValues: extValues ?? _extValues,
);
String? get countryCode => _countryCode;
String? get countryName => _countryName;
String? get event => _event;
@ -203,25 +211,25 @@ class RoomProfile {
String? get roomName => _roomName;
String? get sysOrigin => _sysOrigin;
String? get userId => _userId;
SocialChatUserProfile? get userProfile => _userProfile;
String? get userSVipLevel => _userSVipLevel;
RoomMemberCounter? get roomCounter => _roomCounter;
ExtValues? get extValues => _extValues;
String get displayMemberCount {
final memberCount = roomCounter?.memberCount;
if (memberCount != null) {
if (memberCount == memberCount.roundToDouble()) {
return memberCount.toInt().toString();
}
return memberCount.toString();
}
final memberQuantity = extValues?.memberQuantity;
if ((memberQuantity ?? "").trim().isNotEmpty) {
return memberQuantity!;
}
return "0";
}
SocialChatUserProfile? get userProfile => _userProfile;
String? get userSVipLevel => _userSVipLevel;
RoomMemberCounter? get roomCounter => _roomCounter;
ExtValues? get extValues => _extValues;
String get displayMemberCount {
final memberCount = roomCounter?.memberCount;
if (memberCount != null) {
if (memberCount == memberCount.roundToDouble()) {
return memberCount.toInt().toString();
}
return memberCount.toString();
}
final memberQuantity = extValues?.memberQuantity;
if ((memberQuantity ?? "").trim().isNotEmpty) {
return memberQuantity!;
}
return "0";
}
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
@ -240,16 +248,16 @@ class RoomProfile {
map['roomName'] = _roomName;
map['sysOrigin'] = _sysOrigin;
map['userId'] = _userId;
if (_userProfile != null) {
map['userProfile'] = _userProfile?.toJson();
}
map['userSVipLevel'] = _userSVipLevel;
if (_roomCounter != null) {
map['roomCounter'] = _roomCounter?.toJson();
}
if (_extValues != null) {
map['extValues'] = _extValues?.toJson();
}
if (_userProfile != null) {
map['userProfile'] = _userProfile?.toJson();
}
map['userSVipLevel'] = _userSVipLevel;
if (_roomCounter != null) {
map['roomCounter'] = _roomCounter?.toJson();
}
if (_extValues != null) {
map['extValues'] = _extValues?.toJson();
}
return map;
}
}

View File

@ -17,17 +17,31 @@ class JoinRoomRes {
_roomProps = roomProps;
}
JoinRoomRes.fromJson(dynamic json) {
_entrants =
json['entrants'] != null ? Entrants.fromJson(json['entrants']) : null;
_roomProfile =
json['roomProfile'] != null
? RoomProfile.fromJson(json['roomProfile'])
: null;
_roomProps =
json['roomProps'] != null
? RoomProps.fromJson(json['roomProps'])
: null;
JoinRoomRes.fromJson(dynamic json) {
_entrants =
json['entrants'] != null ? Entrants.fromJson(json['entrants']) : null;
_roomProfile =
json['roomProfile'] != null
? RoomProfile.fromJson(json['roomProfile'])
: null;
final roomVipLevel = _firstNonBlankRoomVipLevel([
json['userSVipLevel'],
json['supperVip'],
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;
@ -62,13 +76,23 @@ class JoinRoomRes {
map['roomProps'] = _roomProps?.toJson();
}
return map;
}
}
/// layoutCode : ""
/// roomTheme : {"expireTime":0,"id":0,"themeBack":"","themeStatus":"","useTheme":false}
class RoomProps {
}
}
String? _firstNonBlankRoomVipLevel(Iterable<dynamic> values) {
for (final value in values) {
final text = value?.toString().trim();
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}) {
_layoutCode = layoutCode;
_roomTheme = roomTheme;
@ -181,26 +205,33 @@ class RoomTheme {
/// regionCode : ""
/// 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"}
/// 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}]}
class RoomProfile {
RoomProfile({
String? regionCode,
RoomCounter? roomCounter,
RoomProfile2? roomProfile,
RoomSetting? roomSetting,
SocialChatUserProfile? userProfile,
}) {
_regionCode = regionCode;
_roomCounter = roomCounter;
_roomProfile = roomProfile;
_roomSetting = roomSetting;
_userProfile = userProfile;
}
RoomProfile.fromJson(dynamic json) {
_regionCode = json['regionCode'];
/// 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}]}
/// userSVipLevel : ""
class RoomProfile {
RoomProfile({
String? regionCode,
RoomCounter? roomCounter,
RoomProfile2? roomProfile,
RoomSetting? roomSetting,
SocialChatUserProfile? userProfile,
String? userSVipLevel,
}) {
_regionCode = regionCode;
_roomCounter = roomCounter;
_roomProfile = roomProfile;
_roomSetting = roomSetting;
_userSVipLevel = userSVipLevel;
final normalizedVipLevel = userSVipLevel?.trim();
_userProfile =
normalizedVipLevel == null || normalizedVipLevel.isEmpty
? userProfile
: userProfile?.copyWith(vipLevel: normalizedVipLevel);
}
RoomProfile.fromJson(dynamic json) {
_regionCode = json['regionCode'];
_roomCounter =
json['roomCounter'] != null
? RoomCounter.fromJson(json['roomCounter'])
@ -213,31 +244,52 @@ class RoomProfile {
json['roomSetting'] != null
? RoomSetting.fromJson(json['roomSetting'])
: null;
_userProfile =
json['userProfile'] != null
? SocialChatUserProfile.fromJson(json['userProfile'])
: null;
}
String? _regionCode;
RoomCounter? _roomCounter;
RoomProfile2? _roomProfile;
RoomSetting? _roomSetting;
SocialChatUserProfile? _userProfile;
RoomProfile copyWith({
String? regionCode,
RoomCounter? roomCounter,
RoomProfile2? roomProfile,
RoomSetting? roomSetting,
SocialChatUserProfile? userProfile,
}) => RoomProfile(
regionCode: regionCode ?? _regionCode,
roomCounter: roomCounter ?? _roomCounter,
roomProfile: roomProfile ?? _roomProfile,
roomSetting: roomSetting ?? _roomSetting,
userProfile: userProfile ?? _userProfile,
);
_userProfile =
json['userProfile'] != null
? SocialChatUserProfile.fromJson(json['userProfile'])
: null;
_userSVipLevel = _firstNonBlankRoomVipLevel([
json['userSVipLevel'],
json['supperVip'],
json['superVip'],
json['vipLevel'],
json['svipLevel'],
json['sVipLevel'],
json['nobleVipLevel'],
json['vipLevelCode'],
json['levelCode'],
json['roomProfile'] is Map ? json['roomProfile']['userSVipLevel'] : null,
json['roomProfile'] is Map ? json['roomProfile']['supperVip'] : null,
json['roomProfile'] is Map ? json['roomProfile']['vipLevel'] : null,
]);
final normalizedVipLevel = _userSVipLevel?.trim();
if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) {
_userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel);
}
}
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;
@ -247,11 +299,13 @@ class RoomProfile {
RoomSetting? get roomSetting => _roomSetting;
SocialChatUserProfile? get userProfile => _userProfile;
Map<String?, dynamic> toJson() {
final map = <String?, dynamic>{};
map['regionCode'] = _regionCode;
SocialChatUserProfile? get userProfile => _userProfile;
String? get userSVipLevel => _userSVipLevel;
Map<String?, dynamic> toJson() {
final map = <String?, dynamic>{};
map['regionCode'] = _regionCode;
if (_roomCounter != null) {
map['roomCounter'] = _roomCounter?.toJson();
}
@ -261,11 +315,12 @@ class RoomProfile {
if (_roomSetting != null) {
map['roomSetting'] = _roomSetting?.toJson();
}
if (_userProfile != null) {
map['userProfile'] = _userProfile?.toJson();
}
return map;
}
if (_userProfile != null) {
map['userProfile'] = _userProfile?.toJson();
}
map['userSVipLevel'] = _userSVipLevel;
return map;
}
void setRoomSetting(RoomSetting roomSetting) {
_roomSetting = roomSetting;

View File

@ -121,6 +121,7 @@ class SocialChatUserProfile {
num? heartbeatVal,
String? originSys,
OwnSpecialId? ownSpecialId,
String? vipLevel,
bool? sameRegion,
int? firstRechargeEndTime,
String? sysOriginChild,
@ -160,6 +161,7 @@ class SocialChatUserProfile {
_wealthLevel = wealthLevel;
_heartbeatVal = heartbeatVal;
_ownSpecialId = ownSpecialId;
_vipLevel = vipLevel;
_sameRegion = sameRegion;
_isCpRelation = isCpRelation;
_firstRechargeEndTime = firstRechargeEndTime;
@ -178,10 +180,14 @@ class SocialChatUserProfile {
_backgroundPhotos = backgroundPhotos;
_personalPhotos = personalPhotos;
_cpList = cpList;
}
SocialChatUserProfile.fromJson(dynamic json) {
_account = json['account'];
}
SocialChatUserProfile.fromJson(dynamic json) {
final sourceJson = json;
if (sourceJson is Map && sourceJson['userProfile'] is Map) {
json = sourceJson['userProfile'];
}
_account = json['account'];
_accountStatus = json['accountStatus'];
_age = json['age'];
_bornDay = json['bornDay'];
@ -204,6 +210,10 @@ class SocialChatUserProfile {
json['ownSpecialId'] != null
? OwnSpecialId.fromJson(json['ownSpecialId'])
: null;
_vipLevel = _firstNonBlankString([
..._vipLevelHintValues(json),
..._vipLevelHintValues(sourceJson),
]);
_sameRegion = json['sameRegion'];
_isCpRelation = json['isCpRelation'];
_firstRechargeEndTime = json['firstRechargeEndTime'];
@ -274,6 +284,7 @@ class SocialChatUserProfile {
num? _wealthLevel;
num? _heartbeatVal;
OwnSpecialId? _ownSpecialId;
String? _vipLevel;
bool? _sameRegion;
bool? _isCpRelation;
int? _firstRechargeEndTime;
@ -314,6 +325,7 @@ class SocialChatUserProfile {
num? wealthLevel,
num? heartbeatVal,
OwnSpecialId? ownSpecialId,
String? vipLevel,
bool? sameRegion,
bool? isCpRelation,
int? firstRechargeEndTime,
@ -353,6 +365,7 @@ class SocialChatUserProfile {
wealthLevel: wealthLevel ?? _wealthLevel,
heartbeatVal: heartbeatVal ?? _heartbeatVal,
ownSpecialId: ownSpecialId ?? _ownSpecialId,
vipLevel: vipLevel ?? _vipLevel,
sameRegion: sameRegion ?? _sameRegion,
isCpRelation: isCpRelation ?? _isCpRelation,
firstRechargeEndTime: firstRechargeEndTime ?? _firstRechargeEndTime,
@ -413,6 +426,8 @@ class SocialChatUserProfile {
OwnSpecialId? get ownSpecialId => _ownSpecialId;
String? get vipLevel => _vipLevel;
bool? get sameRegion => _sameRegion;
bool? get isCpRelation => _isCpRelation;
@ -494,6 +509,26 @@ class SocialChatUserProfile {
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? pr;
_useProps?.forEach((value) {
@ -547,6 +582,9 @@ class SocialChatUserProfile {
if (_ownSpecialId != null) {
map['ownSpecialId'] = _ownSpecialId?.toJson();
}
if (_vipLevel != null) {
map['vipLevel'] = _vipLevel;
}
map['sameRegion'] = _sameRegion;
map['isCpRelation'] = _isCpRelation;
map['firstRecharge'] = _firstRecharge;
@ -591,22 +629,212 @@ class SocialChatUserProfile {
return false;
}
/// ID VIP
/// useProps NOBLE_VIP
bool hasVipPrivilegeForColoredId() {
return _useProps?.any(
(value) => value.propsResources?.type == SCPropsType.NOBLE_VIP.name,
) ??
false;
}
int get vipLevelForColoredId {
final hasExplicitVipLevel = _vipLevel?.trim().isNotEmpty ?? false;
final explicitLevel = _parseVipLevelForColoredId(_vipLevel) ?? 0;
if (hasExplicitVipLevel && explicitLevel <= 0) {
return 0;
}
if (explicitLevel > 0) {
return explicitLevel;
}
bool shouldShowColoredSpecialIdText() {
return hasVipPrivilegeForColoredId();
}
final vipResource = getVIP();
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) {
_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 : ""
@ -992,11 +1220,11 @@ class PropsResources {
PropsResources.fromJson(dynamic json) {
_amount = json['amount'];
_code = json['code'];
_cover = json['cover'];
_cover = json['cover'] ?? json['coverUrl'];
_createTime = json['createTime'];
_del = json['del'];
_expand = json['expand'];
_id = json['id'];
_id = json['id'] ?? json['resourceId'];
_imNotOpenedUrl = json['imNotOpenedUrl'];
_imOpenedUrl = json['imOpenedUrl'];
_imOpenedUrlTwo = json['imOpenedUrlTwo'];
@ -1005,7 +1233,7 @@ class PropsResources {
_roomNotOpenedUrl = json['roomNotOpenedUrl'];
_roomOpenedUrl = json['roomOpenedUrl'];
_roomSendCoverUrl = json['roomSendCoverUrl'];
_sourceUrl = json['sourceUrl'];
_sourceUrl = json['sourceUrl'] ?? json['resourceUrl'] ?? json['url'];
_sysOrigin = json['sysOrigin'];
_type = json['type'];
_updateTime = json['updateTime'];

View File

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

View File

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

View File

@ -11,14 +11,14 @@ class SocialChatRoomMemberRes {
_roles = roles;
}
SocialChatRoomMemberRes.fromJson(dynamic json) {
_id = json['id'];
_userProfile =
json['userProfile'] != null
? SocialChatUserProfile.fromJson(json['userProfile'])
: null;
_roles = json['roles'];
}
SocialChatRoomMemberRes.fromJson(dynamic json) {
_id = json['id'];
_userProfile =
json['userProfile'] != null
? socialChatUserProfileFromJsonWithVipHint(json)
: null;
_roles = json['roles'];
}
String? _id;
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}]}
/// userSVipLevel : ""
class SocialChatRoomRes {
SocialChatRoomRes({
class SocialChatRoomRes {
SocialChatRoomRes({
String? countryCode,
String? countryName,
String? event,
@ -34,12 +34,12 @@ class SocialChatRoomRes {
String? roomGameIcon,
String? roomName,
String? sysOrigin,
String? userId,
SocialChatUserProfile? userProfile,
String? userSVipLevel,
RoomMemberCounter? roomCounter,
ExtValues? extValues,
}) {
String? userId,
SocialChatUserProfile? userProfile,
String? userSVipLevel,
RoomMemberCounter? roomCounter,
ExtValues? extValues,
}) {
_countryCode = countryCode;
_countryName = countryName;
_event = event;
@ -54,12 +54,16 @@ class SocialChatRoomRes {
_roomGameIcon = roomGameIcon;
_roomName = roomName;
_sysOrigin = sysOrigin;
_userId = userId;
_userProfile = userProfile;
_userSVipLevel = userSVipLevel;
_roomCounter = roomCounter;
_extValues = extValues;
}
_userId = userId;
_userSVipLevel = userSVipLevel;
final normalizedVipLevel = userSVipLevel?.trim();
_userProfile =
normalizedVipLevel == null || normalizedVipLevel.isEmpty
? userProfile
: userProfile?.copyWith(vipLevel: normalizedVipLevel);
_roomCounter = roomCounter;
_extValues = extValues;
}
SocialChatRoomRes.fromJson(dynamic json) {
_countryCode = json['countryCode'];
@ -82,18 +86,22 @@ class SocialChatRoomRes {
_roomName = json['roomName'];
_sysOrigin = json['sysOrigin'];
_userId = json['userId'];
_userProfile =
json['userProfile'] != null
? SocialChatUserProfile.fromJson(json['userProfile'])
: null;
_userSVipLevel = json['userSVipLevel'];
final counterJson = json['roomCounter'] ?? json['counter'];
_roomCounter =
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
_extValues =
json['extValues'] != null
? ExtValues.fromJson(json['extValues'])
: null;
_userProfile =
json['userProfile'] != null
? SocialChatUserProfile.fromJson(json['userProfile'])
: null;
_userSVipLevel = socialChatVipLevelHintFromJson(json);
final normalizedVipLevel = _userSVipLevel?.trim();
if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) {
_userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel);
}
final counterJson = json['roomCounter'] ?? json['counter'];
_roomCounter =
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
_extValues =
json['extValues'] != null
? ExtValues.fromJson(json['extValues'])
: null;
}
String? _countryCode;
@ -110,11 +118,11 @@ class SocialChatRoomRes {
String? _roomGameIcon;
String? _roomName;
String? _sysOrigin;
String? _userId;
SocialChatUserProfile? _userProfile;
String? _userSVipLevel;
RoomMemberCounter? _roomCounter;
ExtValues? _extValues;
String? _userId;
SocialChatUserProfile? _userProfile;
String? _userSVipLevel;
RoomMemberCounter? _roomCounter;
ExtValues? _extValues;
SocialChatRoomRes copyWith({
String? countryCode,
@ -131,12 +139,12 @@ class SocialChatRoomRes {
String? roomGameIcon,
String? roomName,
String? sysOrigin,
String? userId,
SocialChatUserProfile? userProfile,
String? userSVipLevel,
RoomMemberCounter? roomCounter,
ExtValues? extValues,
}) => SocialChatRoomRes(
String? userId,
SocialChatUserProfile? userProfile,
String? userSVipLevel,
RoomMemberCounter? roomCounter,
ExtValues? extValues,
}) => SocialChatRoomRes(
countryCode: countryCode ?? _countryCode,
countryName: countryName ?? _countryName,
event: event ?? _event,
@ -151,12 +159,12 @@ class SocialChatRoomRes {
roomGameIcon: roomGameIcon ?? _roomGameIcon,
roomName: roomName ?? _roomName,
sysOrigin: sysOrigin ?? _sysOrigin,
userId: userId ?? _userId,
userProfile: userProfile ?? _userProfile,
userSVipLevel: userSVipLevel ?? _userSVipLevel,
roomCounter: roomCounter ?? _roomCounter,
extValues: extValues ?? _extValues,
);
userId: userId ?? _userId,
userProfile: userProfile ?? _userProfile,
userSVipLevel: userSVipLevel ?? _userSVipLevel,
roomCounter: roomCounter ?? _roomCounter,
extValues: extValues ?? _extValues,
);
String? get countryCode => _countryCode;
@ -188,25 +196,25 @@ class SocialChatRoomRes {
String? get userId => _userId;
SocialChatUserProfile? get userProfile => _userProfile;
String? get userSVipLevel => _userSVipLevel;
RoomMemberCounter? get roomCounter => _roomCounter;
ExtValues? get extValues => _extValues;
String get displayMemberCount {
final memberCount = roomCounter?.memberCount;
if (memberCount != null) {
return _formatRoomMemberCount(memberCount);
}
final memberQuantity = extValues?.memberQuantity;
if ((memberQuantity ?? "").trim().isNotEmpty) {
return memberQuantity!;
}
return "0";
}
SocialChatUserProfile? get userProfile => _userProfile;
String? get userSVipLevel => _userSVipLevel;
RoomMemberCounter? get roomCounter => _roomCounter;
ExtValues? get extValues => _extValues;
String get displayMemberCount {
final memberCount = roomCounter?.memberCount;
if (memberCount != null) {
return _formatRoomMemberCount(memberCount);
}
final memberQuantity = extValues?.memberQuantity;
if ((memberQuantity ?? "").trim().isNotEmpty) {
return memberQuantity!;
}
return "0";
}
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
@ -225,58 +233,58 @@ class SocialChatRoomRes {
map['roomName'] = _roomName;
map['sysOrigin'] = _sysOrigin;
map['userId'] = _userId;
if (_userProfile != null) {
map['userProfile'] = _userProfile?.toJson();
}
map['userSVipLevel'] = _userSVipLevel;
if (_roomCounter != null) {
map['roomCounter'] = _roomCounter?.toJson();
}
if (_extValues != null) {
map['extValues'] = _extValues?.toJson();
}
return map;
}
}
String _formatRoomMemberCount(num value) {
if (value == value.roundToDouble()) {
return value.toInt().toString();
}
return value.toString();
}
class RoomMemberCounter {
RoomMemberCounter({num? adminCount, num? memberCount}) {
_adminCount = adminCount;
_memberCount = memberCount;
}
RoomMemberCounter.fromJson(dynamic json) {
_adminCount = json['adminCount'];
_memberCount = json['memberCount'];
}
num? _adminCount;
num? _memberCount;
RoomMemberCounter copyWith({num? adminCount, num? memberCount}) =>
RoomMemberCounter(
adminCount: adminCount ?? _adminCount,
memberCount: memberCount ?? _memberCount,
);
num? get adminCount => _adminCount;
num? get memberCount => _memberCount;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['adminCount'] = _adminCount;
map['memberCount'] = _memberCount;
return map;
}
}
if (_userProfile != null) {
map['userProfile'] = _userProfile?.toJson();
}
map['userSVipLevel'] = _userSVipLevel;
if (_roomCounter != null) {
map['roomCounter'] = _roomCounter?.toJson();
}
if (_extValues != null) {
map['extValues'] = _extValues?.toJson();
}
return map;
}
}
String _formatRoomMemberCount(num value) {
if (value == value.roundToDouble()) {
return value.toInt().toString();
}
return value.toString();
}
class RoomMemberCounter {
RoomMemberCounter({num? adminCount, num? memberCount}) {
_adminCount = adminCount;
_memberCount = memberCount;
}
RoomMemberCounter.fromJson(dynamic json) {
_adminCount = json['adminCount'];
_memberCount = json['memberCount'];
}
num? _adminCount;
num? _memberCount;
RoomMemberCounter copyWith({num? adminCount, num? memberCount}) =>
RoomMemberCounter(
adminCount: adminCount ?? _adminCount,
memberCount: memberCount ?? _memberCount,
);
num? get adminCount => _adminCount;
num? get memberCount => _memberCount;
Map<String, dynamic> toJson() {
final map = <String, dynamic>{};
map['adminCount'] = _adminCount;
map['memberCount'] = _memberCount;
return map;
}
}
/// account : ""
/// accountStatus : ""

View File

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

View File

@ -7,6 +7,7 @@ class SCFloatingMessage {
String? userName; //
String? toUserName; //
String? giftUrl; //
String? fallbackUrl; //
String? giftId; // id
int? type;
int? rocketLevel;
@ -26,6 +27,7 @@ class SCFloatingMessage {
this.userName = '',
this.toUserName = '',
this.giftUrl = '',
this.fallbackUrl = '',
this.giftId = '',
this.number = 0,
this.coins = 0,
@ -44,6 +46,7 @@ class SCFloatingMessage {
userName = json['userName'];
toUserName = json['toUserName'];
giftUrl = json['giftUrl'];
fallbackUrl = json['fallbackUrl'];
giftId = json['giftId'];
coins = json['coins'];
number = json['number'];
@ -63,6 +66,7 @@ class SCFloatingMessage {
map['userName'] = userName;
map['toUserName'] = toUserName;
map['giftUrl'] = giftUrl;
map['fallbackUrl'] = fallbackUrl;
map['giftId'] = giftId;
map['coins'] = coins;
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_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_vip_entry_screen_widget.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';
@ -76,8 +77,8 @@ class OverlayManager {
unawaited(
warmImageResource(
message.giftUrl ?? "",
logicalWidth: 96,
logicalHeight: 96,
logicalWidth: message.type == 5 ? 350 : 96,
logicalHeight: message.type == 5 ? 84 : 96,
),
);
if (message.type == 0) {
@ -209,6 +210,12 @@ class OverlayManager {
message: message,
onAnimationCompleted: onComplete,
);
case 5:
//VIP进房飘窗
return FloatingVipEntryScreenWidget(
message: message,
onAnimationCompleted: onComplete,
);
default:
onComplete();
return Container();
@ -233,6 +240,7 @@ class OverlayManager {
_removeActiveRoomMessage();
_removeMessagesByType(1);
_removeMessagesByType(0);
_removeMessagesByType(5);
}
//
@ -266,7 +274,7 @@ class OverlayManager {
if (message == null) {
return false;
}
if (message.type != 0 && message.type != 1) {
if (message.type != 0 && message.type != 1 && message.type != 5) {
return true;
}
@ -289,7 +297,9 @@ class OverlayManager {
void _removeActiveRoomMessage() {
final activeMessage = _currentMessage;
if (activeMessage == null ||
(activeMessage.type != 0 && activeMessage.type != 1)) {
(activeMessage.type != 0 &&
activeMessage.type != 1 &&
activeMessage.type != 5)) {
return;
}
_currentOverlayEntry?.remove();

View File

@ -196,8 +196,8 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
num mickIndex, {
String? eventType,
String? inviterId,
}) async {
Map<String, dynamic> params = {};
}) async {
Map<String, dynamic> params = {};
params["roomId"] = roomId;
params["mickIndex"] = mickIndex;
if (eventType != null) {
@ -262,12 +262,12 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
silentErrorToast
? const {BaseNetworkClient.silentErrorToastKey: true}
: null,
fromJson:
(json) =>
(json as List)
.map((e) => SocialChatUserProfile.fromJson(e))
.toList(),
);
fromJson:
(json) =>
(json as List)
.map((e) => socialChatUserProfileFromJsonWithVipHint(e)!)
.toList(),
);
return result;
}
@ -316,8 +316,8 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
String? roomId,
SCGiveAwayGiftRoomAcceptsCmd? accepts,
String? dynamicContentId,
}) async {
Map<String, dynamic> params = {};
}) async {
Map<String, dynamic> params = {};
if (roomId != null) {
params["roomId"] = roomId;
}
@ -512,9 +512,10 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
bool? adminLockSeat,
bool? showHeartbeat,
bool? openKtvMode,
String? roomSpecialMikeType,
}) async {
Map<String, dynamic> params = {};
String? roomSpecialMikeType,
}) async {
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
Map<String, dynamic> params = {};
params["roomId"] = roomId;
if (touristMike != null) {
params["touristMike"] = touristMike;
@ -552,7 +553,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
fromJson: (json) => json as bool,
);
Provider.of<RtmProvider>(context, listen: false).dispatchMessage(
rtmProvider.dispatchMessage(
Msg(
groupId: roomAcount,
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';
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 = {};
static SCGiftVapSvgaManager? _inst;
static const int _maxPreloadConcurrency = 1;
@ -26,7 +34,10 @@ class SCGiftVapSvgaManager {
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
);
VapController? _rgc;
@ -34,12 +45,14 @@ class SCGiftVapSvgaManager {
bool _play = false;
bool _dis = false;
SCVapTask? _currentTask;
Timer? _currentTaskWatchdogTimer;
final Queue<String> _preloadQueue = Queue<String>();
final Set<String> _queuedPreloadPaths = <String>{};
final Map<String, Future<MovieEntity>> _svgaLoadTasks = {};
final Map<String, Future<String>> _playablePathTasks = {};
final Map<String, String> _playablePathCache = {};
int _activePreloadCount = 0;
int _consecutiveEntryTaskCount = 0;
bool _pause = false;
@ -63,6 +76,26 @@ class SCGiftVapSvgaManager {
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) {
if (_needsSvgaController(task.path)) {
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}) {
if (_dis) {
return;
@ -268,8 +391,14 @@ class SCGiftVapSvgaManager {
if (_dis) {
return;
}
final finishedTask = _currentTask;
if (!_play && finishedTask == null) {
_scheduleNextTask(delay: delay);
return;
}
_cancelCurrentTaskWatchdog();
SCRoomEffectScheduler().completeHighCostTask(
debugLabel: _currentTask?.path,
debugLabel: finishedTask?.path,
);
_play = false;
_currentTask = null;
@ -288,7 +417,7 @@ class SCGiftVapSvgaManager {
_rgc = vapController;
_log(
'bindVapCtrl hasVapCtrl=${_rgc != null} '
'hasSvgaCtrl=${_rsc != null} queue=${_tq.length} mute=$_mute',
'hasSvgaCtrl=${_rsc != null} queue=$_queueSummary mute=$_mute',
);
_rgc?.setAnimListener(
onVideoStart: () {
@ -314,18 +443,13 @@ class SCGiftVapSvgaManager {
_rsc = svgaController;
_log(
'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} '
'hasVapCtrl=${_rgc != null} queue=${_tq.length}',
'hasVapCtrl=${_rgc != null} queue=$_queueSummary',
);
_rsc?.addStatusListener((AnimationStatus status) {
if (status.isCompleted) {
_log('svga completed path=${_currentTask?.path}');
_rsc?.reset();
SCRoomEffectScheduler().completeHighCostTask(
debugLabel: _currentTask?.path,
);
_play = false;
_currentTask = null;
_pn();
_finishCurrentTask();
}
});
_scheduleNextTask();
@ -337,18 +461,22 @@ class SCGiftVapSvgaManager {
String path, {
int priority = 0,
Map<String, VAPContent>? customResources,
int type = 0,
int type = giftEffectType,
}) {
if (path.isEmpty) {
_log('play ignored because path is empty');
return;
}
final resolvedPriority = resolveEffectPriority(
priority: priority,
type: type,
);
_log(
'play request path=$path ext=${SCPathUtils.getFileExtension(path)} '
'priority=$priority type=$type '
'priority=$priority resolvedPriority=$resolvedPriority type=$type '
'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim} '
'lowPerformance=${SCGlobalConfig.isLowPerformanceDevice} '
'disposed=$_dis playing=$_play queueBefore=${_tq.length} '
'disposed=$_dis playing=$_play queueBefore=$_queueSummary '
'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null}',
);
if (SCGlobalConfig.allowsHighCostAnimations) {
@ -358,31 +486,15 @@ class SCGiftVapSvgaManager {
}
final task = SCVapTask(
path: path,
priority: priority,
type: type,
priority: resolvedPriority,
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);
_enqueueTask(task);
unawaited(preload(path));
while (_tq.length > _maxPendingTaskCount) {
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}');
_log('task enqueued path=$path queueAfter=$_queueSummary');
if (!_play) {
_pn();
}
@ -398,32 +510,34 @@ class SCGiftVapSvgaManager {
//
Future<void> _pn() async {
if (_pause) {
_log('skip _pn because paused queue=${_tq.length}');
_log('skip _pn because paused queue=$_queueSummary');
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)) {
_log(
'controller not ready for path=${task?.path} '
'needSvga=${task != null ? _needsSvgaController(task.path) : "unknown"} '
'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null} '
'queue=${_tq.length}',
'queue=$_queueSummary',
);
return;
}
_tq.removeFirst();
_removeQueuedTask(task);
_play = true;
_currentTask = task;
_armCurrentTaskWatchdog(task);
try {
final pathType = SCPathUtils.getPathType(task.path);
_log(
'start task path=${task.path} '
'pathType=$pathType '
'queueRemaining=${_tq.length} '
'needSvga=${_needsSvgaController(task.path)}',
'queueRemaining=$_queueSummary '
'needSvga=${_needsSvgaController(task.path)} '
'type=${task.type} consecutiveEntry=$_consecutiveEntryTaskCount',
);
if (pathType == PathType.asset) {
await _pa(task);
@ -447,6 +561,9 @@ class SCGiftVapSvgaManager {
if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
_log('play asset svga path=${task.path}');
final entity = await _loadSvgaEntity(task.path);
if (!_isCurrentTask(task)) {
return;
}
_log('use prepared asset svga path=${task.path}');
_rsc?.videoItem = entity;
_rsc?.reset();
@ -470,6 +587,9 @@ class SCGiftVapSvgaManager {
}
if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
final entity = await _loadSvgaEntity(task.path);
if (!_isCurrentTask(task)) {
return;
}
_log('play local svga file path=${task.path}');
_rsc?.videoItem = entity;
_rsc?.reset();
@ -483,11 +603,17 @@ class SCGiftVapSvgaManager {
}
_log('play local vap/mp4 file path=${task.path}');
await _rgc?.setMute(_mute);
if (!_isCurrentTask(task)) {
return;
}
if (task.customResources != null) {
task.customResources?.forEach((k, v) async {
await _rgc?.setVapTagContent(k, v);
});
}
if (!_isCurrentTask(task)) {
return;
}
await _rgc!.playFile(task.path);
}
@ -504,6 +630,9 @@ class SCGiftVapSvgaManager {
_finishCurrentTask();
return;
}
if (!_isCurrentTask(task)) {
return;
}
_log('use prepared network svga path=${task.path}');
_rsc?.videoItem = entity;
_rsc?.reset();
@ -512,11 +641,15 @@ class SCGiftVapSvgaManager {
} else {
_log('download network vap/mp4 path=${task.path}');
final playablePath = await _ensurePlayableFilePath(task.path);
if (!_isCurrentTask(task)) {
return;
}
if (!_dis) {
_log('use prepared network vap/mp4 local path=$playablePath');
await _pf(
SCVapTask(
path: playablePath,
type: task.type,
priority: task.priority,
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() {
_pause = true;
_log('pauseAnim queue=${_tq.length}');
_log('pauseAnim queue=$_queueSummary');
}
//
void resumeAnim() {
_pause = false;
_log('resumeAnim queue=${_tq.length}');
_log('resumeAnim queue=$_queueSummary');
_pn();
}
void stopPlayback() {
_log('stopPlayback queue=${_tq.length} currentPath=${_currentTask?.path}');
_log('stopPlayback queue=$_queueSummary currentPath=${_currentTask?.path}');
_play = false;
_currentTask = null;
_pause = false;
_tq.clear();
_consecutiveEntryTaskCount = 0;
_cancelCurrentTaskWatchdog();
_giftQueue.clear();
_entryQueue.clear();
_preloadQueue.clear();
_queuedPreloadPaths.clear();
_activePreloadCount = 0;
@ -564,7 +724,7 @@ class SCGiftVapSvgaManager {
//
void dispose() {
_log('dispose queue=${_tq.length} currentPath=${_currentTask?.path}');
_log('dispose queue=$_queueSummary currentPath=${_currentTask?.path}');
_dis = true;
stopPlayback();
_svgaLoadTasks.clear();
@ -580,7 +740,7 @@ class SCGiftVapSvgaManager {
//
void clearTasks() {
_log(
'clearTasks queueBefore=${_tq.length} currentPath=${_currentTask?.path}',
'clearTasks queueBefore=$_queueSummary currentPath=${_currentTask?.path}',
);
stopPlayback();
}
@ -594,6 +754,7 @@ class SCVapTask implements Comparable<SCVapTask> {
final int type;
final int priority;
final Map<String, VAPContent>? customResources;
final DateTime createdAt;
final int _seq;
static int _nextSeq = 0;
@ -603,7 +764,9 @@ class SCVapTask implements Comparable<SCVapTask> {
this.priority = 0,
this.customResources,
this.type = 0,
}) : _seq = _nextSeq++;
DateTime? createdAt,
}) : createdAt = createdAt ?? DateTime.now(),
_seq = _nextSeq++;
@override
int compareTo(SCVapTask other) {
@ -618,7 +781,7 @@ class SCVapTask implements Comparable<SCVapTask> {
@override
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();
bool remove(E element) => _els.remove(element);
E removeLast() {
if (isEmpty) throw StateError("No elements");
return _els.removeLast();

View File

@ -90,7 +90,21 @@ class SCPathUtils {
///
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:yumi/services/audio/rtc_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_room_effect_scheduler.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
@ -40,6 +43,7 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
if (!_effectsEnabled) {
return;
}
_enqueueVipEntryFloatingMessage(msg);
unawaited(
warmImageResource(
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) {
if (!_effectsEnabled || !mounted) {
return;

View File

@ -1,4 +1,5 @@
import 'dart:collection';
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
@ -16,6 +17,7 @@ class RoomGiftSeatFlightRequest {
this.beginSize = 96,
this.endSize = 34,
this.queueTag,
this.batchTag,
});
final String imagePath;
@ -25,6 +27,7 @@ class RoomGiftSeatFlightRequest {
final double beginSize;
final double endSize;
final String? queueTag;
final String? batchTag;
}
class RoomGiftSeatFlightController {
@ -121,6 +124,7 @@ class RoomGiftSeatFlightController {
}
final queueTag = request.queueTag?.trim();
final batchTag = request.batchTag?.trim();
return RoomGiftSeatFlightRequest(
imagePath: imagePath,
targetUserId: targetUserId,
@ -129,6 +133,7 @@ class RoomGiftSeatFlightController {
beginSize: request.beginSize,
endSize: request.endSize,
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();
late final AnimationController _controller;
Timer? _activeFlightWatchdogTimer;
RoomGiftSeatFlightRequest? _centerRequest;
ImageProvider<Object>? _centerImageProvider;
RoomGiftSeatFlightRequest? _activeRequest;
ImageProvider<Object>? _activeImageProvider;
Offset? _activeTargetOffset;
List<_ActiveRoomGiftSeatFlight> _activeFlights =
const <_ActiveRoomGiftSeatFlight>[];
bool _isPlaying = false;
int _sessionToken = 0;
@ -248,6 +253,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
@override
void dispose() {
widget.controller._detach(this);
_cancelActiveFlightWatchdog();
_controller.dispose();
super.dispose();
}
@ -283,14 +289,13 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
void _clear() {
_sessionToken += 1;
_queue.clear();
_cancelActiveFlightWatchdog();
_controller.stop();
_controller.reset();
if (!mounted) {
_centerRequest = null;
_centerImageProvider = null;
_activeRequest = null;
_activeImageProvider = null;
_activeTargetOffset = null;
_activeFlights = const <_ActiveRoomGiftSeatFlight>[];
_imageProviderCache.clear();
_isPlaying = false;
return;
@ -298,17 +303,17 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
setState(() {
_centerRequest = null;
_centerImageProvider = null;
_activeRequest = null;
_activeImageProvider = null;
_activeTargetOffset = null;
_activeFlights = const <_ActiveRoomGiftSeatFlight>[];
_imageProviderCache.clear();
_isPlaying = false;
});
}
bool _hasTrackedRequests(String queueTag) {
if ((_activeRequest?.queueTag ?? '').trim() == queueTag) {
return true;
for (final activeFlight in _activeFlights) {
if ((activeFlight.request.queueTag ?? '').trim() == queueTag) {
return true;
}
}
for (final queuedRequest in _queue) {
if ((queuedRequest.request.queueTag ?? '').trim() == queueTag) {
@ -334,8 +339,10 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
int _countTrackedRequests(String queueTag) {
var count = 0;
if ((_activeRequest?.queueTag ?? '').trim() == queueTag) {
count += 1;
for (final activeFlight in _activeFlights) {
if ((activeFlight.request.queueTag ?? '').trim() == queueTag) {
count += 1;
}
}
for (final queuedRequest in _queue) {
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() {
if (_isPlaying || _queue.isEmpty || !mounted) {
return;
@ -381,78 +415,131 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
}
final activeSessionToken = _sessionToken;
final queuedRequest = _queue.removeFirst();
final targetOffset = _resolveTargetOffset(
queuedRequest.request.targetUserId,
final queuedBatch = _removeNextQueuedBatch();
final activeFlights = <_ActiveRoomGiftSeatFlight>[];
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 (queuedRequest.retryCount < 6) {
if (shouldRetryBatch) {
final retryBatch =
queuedBatch
.map(
(queuedRequest) => queuedRequest.copyWith(
retryCount: queuedRequest.retryCount + 1,
),
)
.toList();
if (retryBatch.isNotEmpty) {
Future.delayed(const Duration(milliseconds: 120), () {
if (!mounted || activeSessionToken != _sessionToken) {
return;
}
_queue.addFirst(
queuedRequest.copyWith(retryCount: queuedRequest.retryCount + 1),
);
_requeueBatchAtFront(retryBatch);
_scheduleNextAnimation();
});
} else {
_syncIdleCenterVisual();
_scheduleNextAnimation();
}
return;
}
final imageProvider = _resolveImageProvider(
queuedRequest.request.imagePath,
);
if (activeFlights.isEmpty) {
_syncIdleCenterVisual();
_scheduleNextAnimation();
return;
}
_isPlaying = true;
_controller.duration = queuedRequest.request.flightDuration;
final centerFlight = activeFlights.first;
_controller.duration = centerFlight.request.flightDuration;
setState(() {
_centerRequest = queuedRequest.request;
_centerImageProvider = imageProvider;
_activeRequest = queuedRequest.request;
_activeTargetOffset = targetOffset;
_activeImageProvider = imageProvider;
_centerRequest = centerFlight.request;
_centerImageProvider = centerFlight.imageProvider;
_activeFlights = activeFlights;
});
try {
await precacheImage(
imageProvider,
context,
).timeout(const Duration(milliseconds: 250));
await Future.wait(
activeFlights.map(
(flight) => precacheImage(
flight.imageProvider,
context,
).timeout(const Duration(milliseconds: 250)),
),
);
} catch (_) {}
if (!mounted ||
activeSessionToken != _sessionToken ||
_activeRequest != queuedRequest.request) {
!identical(_activeFlights, activeFlights)) {
return;
}
_controller.reset();
_armActiveFlightWatchdog(centerFlight.request.flightDuration);
_controller.forward();
}
void _finishCurrentAnimation() {
_cancelActiveFlightWatchdog();
if (!mounted) {
_activeRequest = null;
_activeImageProvider = null;
_activeTargetOffset = null;
_activeFlights = const <_ActiveRoomGiftSeatFlight>[];
_isPlaying = false;
_syncIdleCenterVisual();
return;
}
setState(() {
_activeRequest = null;
_activeImageProvider = null;
_activeTargetOffset = null;
_activeFlights = const <_ActiveRoomGiftSeatFlight>[];
_isPlaying = false;
});
_syncIdleCenterVisual();
_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) {
if (_centerRequest != null && _centerImageProvider != null) {
return;
@ -473,7 +560,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
}
void _syncIdleCenterVisual() {
if (_isPlaying || _activeRequest != null) {
if (_isPlaying || _activeFlights.isNotEmpty) {
return;
}
@ -532,7 +619,10 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
String imagePath, {
RoomGiftSeatFlightRequest? request,
}) {
final currentRequest = request ?? _activeRequest ?? _centerRequest;
final currentRequest =
request ??
(_activeFlights.isNotEmpty ? _activeFlights.first.request : null) ??
_centerRequest;
final logicalSize =
currentRequest == null
? null
@ -561,16 +651,17 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
key: _overlayKey,
child:
(_centerRequest == null || _centerImageProvider == null) &&
(_activeRequest == null ||
_activeImageProvider == null ||
_activeTargetOffset == null)
_activeFlights.isEmpty
? const SizedBox.shrink()
: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final center = overlaySize.center(Offset.zero);
final currentCenterRequest =
_centerRequest ?? _activeRequest;
_centerRequest ??
(_activeFlights.isNotEmpty
? _activeFlights.first.request
: null);
final children = <Widget>[];
if (currentCenterRequest != null &&
@ -585,40 +676,38 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
);
}
final activeRequest = _activeRequest;
final activeImageProvider = _activeImageProvider;
final activeTargetOffset = _activeTargetOffset;
if (activeRequest != null &&
activeImageProvider != null &&
activeTargetOffset != null) {
if (_activeFlights.isNotEmpty) {
final travelProgress = Curves.easeInOutCubic.transform(
_controller.value.clamp(0.0, 1.0),
);
final nodeCenter =
Offset.lerp(
center,
activeTargetOffset,
travelProgress,
)!;
final nodeSize =
lerpDouble(
activeRequest.beginSize,
activeRequest.endSize,
travelProgress,
)!;
final fadeOut =
1 -
Curves.easeOut.transform(
((travelProgress - 0.82) / 0.18).clamp(0.0, 1.0),
);
children.add(
_buildGiftNode(
imageProvider: activeImageProvider,
center: nodeCenter,
size: nodeSize,
opacity: fadeOut,
),
);
for (final activeFlight in _activeFlights) {
final activeRequest = activeFlight.request;
final nodeCenter =
Offset.lerp(
center,
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(
@ -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 {
const _QueuedRoomGiftSeatFlightRequest({
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';
class RoomBottomWidget extends StatefulWidget {
const RoomBottomWidget({super.key});
const RoomBottomWidget({super.key, this.showGiftComboButton = true});
final bool showGiftComboButton;
@override
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> {
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
void dispose() {
@ -55,17 +83,17 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
child: Selector<RtcProvider, _RoomBottomSnapshot>(
selector:
(context, provider) => _RoomBottomSnapshot(
showMic: _shouldShowMic(provider),
showMic: _shouldShowRoomBottomMic(provider),
isMic: provider.isMic,
),
builder: (context, bottomSnapshot, child) {
return LayoutBuilder(
builder: (context, constraints) {
final inputWidth = _resolveInputWidth(
final inputWidth = _resolveRoomBottomInputWidth(
maxWidth: constraints.maxWidth,
showMic: bottomSnapshot.showMic,
);
final giftCenterX = _resolveGiftCenterX(
final giftCenterX = _resolveRoomGiftCenterX(
maxWidth: constraints.maxWidth,
inputWidth: inputWidth,
showMic: bottomSnapshot.showMic,
@ -74,11 +102,12 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
return Stack(
clipBehavior: Clip.none,
children: [
Positioned(
left: giftCenterX - (_floatingButtonWidth.w / 2),
bottom: _floatingButtonBottomOffset.w,
child: const RoomGiftComboFloatingButton(),
),
if (widget.showGiftComboButton)
Positioned(
left: giftCenterX - (_floatingButtonWidth.w / 2),
bottom: _floatingButtonBottomOffset.w,
child: const RoomGiftComboFloatingButton(),
),
Positioned(
left: 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() {
SmartDialog.show(
tag: "showGiftControl",
@ -354,10 +331,76 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
),
);
}
}
bool _shouldShowMic(RtcProvider provider) {
return provider.isOnMai();
const double _bottomBarHeight = 72;
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 {

View File

@ -31,32 +31,45 @@ class RoomMsgInput extends StatefulWidget {
}
class _RoomMsgInputState extends State<RoomMsgInput> {
static const List<String> _roomEmojiAssets = [
"sc_images/room/emoji/fluent_emoji_01.webp",
"sc_images/room/emoji/fluent_emoji_02.webp",
"sc_images/room/emoji/fluent_emoji_03.webp",
"sc_images/room/emoji/fluent_emoji_04.webp",
"sc_images/room/emoji/fluent_emoji_05.webp",
"sc_images/room/emoji/fluent_emoji_06.webp",
"sc_images/room/emoji/fluent_emoji_07.webp",
"sc_images/room/emoji/fluent_emoji_08.webp",
"sc_images/room/emoji/fluent_emoji_09.webp",
"sc_images/room/emoji/fluent_emoji_10.webp",
"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",
static const List<_RoomEmojiCategory> _roomEmojiCategories = [
_RoomEmojiCategory(
id: "fluent",
iconAsset: "sc_images/room/emoji/fluent_emoji_01.webp",
assets: [
"sc_images/room/emoji/fluent_emoji_01.webp",
"sc_images/room/emoji/fluent_emoji_02.webp",
"sc_images/room/emoji/fluent_emoji_03.webp",
"sc_images/room/emoji/fluent_emoji_04.webp",
"sc_images/room/emoji/fluent_emoji_05.webp",
"sc_images/room/emoji/fluent_emoji_06.webp",
"sc_images/room/emoji/fluent_emoji_07.webp",
"sc_images/room/emoji/fluent_emoji_08.webp",
"sc_images/room/emoji/fluent_emoji_09.webp",
"sc_images/room/emoji/fluent_emoji_10.webp",
],
),
_RoomEmojiCategory(
id: "monkey",
iconAsset: "sc_images/room/emoji/monkey_1.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 showEmoji = false;
bool _emojiAssetsPrecached = false;
int _selectedEmojiCategoryIndex = 0;
final FocusNode msgNode = FocusNode();
final TextEditingController controller = TextEditingController();
@ -234,33 +247,103 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
}
Widget _buildEmojiPanel() {
final selectedCategory = _roomEmojiCategories[_selectedEmojiCategoryIndex];
return Container(
height: 150.w,
height: 220.w,
decoration: BoxDecoration(
color: _panelBackgroundColor,
borderRadius: BorderRadius.circular(height(5)),
),
child: GridView.builder(
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 8,
crossAxisSpacing: 8.w,
mainAxisSpacing: 8.w,
child: Column(
children: [
Expanded(
child: GridView.builder(
key: ValueKey(selectedCategory.id),
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) {
final emojiAsset = _roomEmojiAssets[index];
final category = _roomEmojiCategories[index];
final selected = index == _selectedEmojiCategoryIndex;
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
_sendRoomEmoji(emojiAsset);
if (selected) {
return;
}
setState(() {
_selectedEmojiCategoryIndex = index;
});
},
child: Center(
child: RoomEmojiAssetImage(
key: ValueKey(emojiAsset),
asset: emojiAsset,
width: 30.w,
height: 30.w,
child: AnimatedContainer(
duration: const Duration(milliseconds: 160),
curve: Curves.easeOut,
width: 64.w,
height: 36.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;
}
_emojiAssetsPrecached = true;
for (final asset in _roomEmojiAssets) {
precacheImage(AssetImage(asset), context);
for (final category in _roomEmojiCategories) {
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 {
final Duration _duration = Duration(milliseconds: 350);
Widget child;

View File

@ -1,4 +1,6 @@
import 'dart:convert';
import 'dart:math' as math;
import 'package:extended_text/extended_text.dart';
import 'package:flutter/gestures.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/modules/index/main_route.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_room_roles_type.dart';
@ -28,13 +32,12 @@ import '../../components/text/sc_text.dart';
///item
class MsgItem extends StatefulWidget {
final Function(SocialChatUserProfile? user) onClick;
Msg msg;
final Msg msg;
MsgItem({Key? key, required this.msg, required this.onClick})
: super(key: key);
const MsgItem({super.key, required this.msg, required this.onClick});
@override
_MsgItemState createState() => _MsgItemState();
State<MsgItem> createState() => _MsgItemState();
}
class _MsgItemState extends State<MsgItem> {
@ -130,42 +133,11 @@ class _MsgItemState extends State<MsgItem> {
Flexible(
// 使Flexible替代Expanded
child: GestureDetector(
child: 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) {},
),
),
),
child: _buildTextMessageBubble(),
onLongPress: () {
if (!(widget.msg?.msg ?? "").startsWith(AtText.flag)) {
if (!(widget.msg.msg ?? "").startsWith(AtText.flag)) {
Clipboard.setData(
ClipboardData(text: widget.msg?.msg ?? ''),
ClipboardData(text: widget.msg.msg ?? ''),
);
SCTts.show("Copied");
}
@ -194,7 +166,7 @@ class _MsgItemState extends State<MsgItem> {
borderRadius: BorderRadius.all(Radius.circular(15.w)),
),
child: Text(
widget.msg?.msg ?? '',
widget.msg.msg ?? '',
style: TextStyle(
fontSize: sp(13),
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) {
return Container(
@ -224,7 +485,7 @@ class _MsgItemState extends State<MsgItem> {
borderRadius: BorderRadius.all(Radius.circular(15.w)),
),
child: Text(
widget.msg?.msg ?? '',
widget.msg.msg ?? '',
style: TextStyle(
fontSize: sp(13),
height: 1.5.w,
@ -658,7 +919,7 @@ class _MsgItemState extends State<MsgItem> {
///
_enterRoomItem(BuildContext context) {
List<InlineSpan> spans = [];
if (widget.msg.role != null && widget.msg.role != 0) {
if (widget.msg.role != null && widget.msg.role != "0") {
spans.add(
WidgetSpan(
alignment: PlaceholderAlignment.middle,
@ -671,7 +932,7 @@ class _MsgItemState extends State<MsgItem> {
}
spans.add(
TextSpan(
text: "${widget.msg?.user?.userNickname ?? ''} ",
text: "${widget.msg.user?.userNickname ?? ''} ",
style: TextStyle(
fontSize: sp(13),
color: _roleNameColor(widget.msg.role ?? ""),
@ -718,7 +979,7 @@ class _MsgItemState extends State<MsgItem> {
child: ListView.separated(
scrollDirection: Axis.horizontal,
shrinkWrap: true,
itemCount: wearBadge.length ?? 0,
itemCount: wearBadge.length,
itemBuilder: (context, index) {
return netImage(
width: 25.w,
@ -749,7 +1010,6 @@ class _MsgItemState extends State<MsgItem> {
case SCRoomMsgType.qcfj:
case SCRoomMsgType.killXiaMai:
return 0xFFFFFFFF;
break;
// case type.text:
// return _mapTextColor();
// break;
@ -1403,6 +1663,7 @@ class Msg {
//
SocialChatUserProfile? toUser;
List<String>? targetUserIds;
int? time = 0;
//
@ -1422,6 +1683,7 @@ class Msg {
this.number,
this.customAnimationCount,
this.awardAmount,
this.targetUserIds,
this.hasPlayedAnimation,
this.needUpDataUserInfo,
}) {
@ -1437,6 +1699,14 @@ class Msg {
number = json['number'];
customAnimationCount = json['customAnimationCount'];
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'];
needUpDataUserInfo = json['needUpDataUserInfo'];
userWheat =
@ -1466,6 +1736,9 @@ class Msg {
map['customAnimationCount'] = customAnimationCount;
map['hasPlayedAnimation'] = hasPlayedAnimation;
map['needUpDataUserInfo'] = needUpDataUserInfo;
if (targetUserIds != null) {
map['targetUserIds'] = targetUserIds;
}
if (userWheat != null) {
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
# 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.
version: 1.2.6+8
version: 1.2.7+9
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);
});
});
}