1.2.7
This commit is contained in:
parent
36ba0f3573
commit
6a95763436
@ -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 = "";
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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: [
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 : ""
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
/// 从网络地址中提取文件名
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
209
lib/ui_kit/widgets/sc_nine_patch_image.dart
Normal file
209
lib/ui_kit/widgets/sc_nine_patch_image.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
179
lib/ui_kit/widgets/svga/sc_network_svga_widget.dart
Normal file
179
lib/ui_kit/widgets/svga/sc_network_svga_widget.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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:
|
||||
|
||||
53
test/gift_vap_svga_manager_priority_test.dart
Normal file
53
test/gift_vap_svga_manager_priority_test.dart
Normal 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');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
29
test/sc_path_utils_test.dart
Normal file
29
test/sc_path_utils_test.dart
Normal 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');
|
||||
});
|
||||
});
|
||||
}
|
||||
185
test/user_profile_vip_colored_id_test.dart
Normal file
185
test/user_profile_vip_colored_id_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user