1.2.7
This commit is contained in:
parent
36ba0f3573
commit
6a95763436
@ -502,7 +502,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 8;
|
CURRENT_PROJECT_VERSION = 9;
|
||||||
DEVELOPMENT_TEAM = S9X2AJ2US9;
|
DEVELOPMENT_TEAM = S9X2AJ2US9;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -511,7 +511,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.6;
|
MARKETING_VERSION = 1.2.7;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -693,7 +693,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 8;
|
CURRENT_PROJECT_VERSION = 9;
|
||||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -702,7 +702,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.6;
|
MARKETING_VERSION = 1.2.7;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
@ -722,7 +722,7 @@
|
|||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 8;
|
CURRENT_PROJECT_VERSION = 9;
|
||||||
DEVELOPMENT_TEAM = F33K8VUZ62;
|
DEVELOPMENT_TEAM = F33K8VUZ62;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@ -731,7 +731,7 @@
|
|||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.2.6;
|
MARKETING_VERSION = 1.2.7;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:extended_image/extended_image.dart'
|
import 'package:extended_image/extended_image.dart'
|
||||||
show ExtendedImage, ExtendedRawImage, ExtendedImageState, LoadState;
|
show ExtendedImage, ExtendedRawImage, ExtendedImageState, LoadState;
|
||||||
import 'package:extended_text/extended_text.dart';
|
import 'package:extended_text/extended_text.dart';
|
||||||
@ -54,6 +56,8 @@ import 'package:yumi/shared/tools/sc_path_utils.dart';
|
|||||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||||
import 'package:yumi/modules/index/main_route.dart';
|
import 'package:yumi/modules/index/main_route.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
|
||||||
|
|
||||||
import '../../shared/business_logic/models/res/sc_user_red_packet_send_res.dart';
|
import '../../shared/business_logic/models/res/sc_user_red_packet_send_res.dart';
|
||||||
|
|
||||||
@ -1245,31 +1249,7 @@ class _MessageItem extends StatelessWidget {
|
|||||||
return Builder(
|
return Builder(
|
||||||
builder: (ct) {
|
builder: (ct) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
child: Container(
|
child: _buildTextBubble(content),
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Color(0xff18F2B1).withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topLeft:
|
|
||||||
message.isSelf! ? Radius.circular(8) : Radius.circular(0),
|
|
||||||
topRight: Radius.circular(8.w),
|
|
||||||
bottomLeft: Radius.circular(8.w),
|
|
||||||
bottomRight:
|
|
||||||
message.isSelf! ? Radius.circular(0) : Radius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.all(8.0.w),
|
|
||||||
child: Container(
|
|
||||||
constraints: BoxConstraints(maxWidth: width(220)),
|
|
||||||
child: ExtendedText(
|
|
||||||
content,
|
|
||||||
// specialTextSpanBuilder: MySpecialTextSpanBuilder(),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: sp(14),
|
|
||||||
color: message.isSelf! ? Colors.white : Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
_showMsgItemMenu(ct, content);
|
_showMsgItemMenu(ct, content);
|
||||||
},
|
},
|
||||||
@ -1278,6 +1258,286 @@ class _MessageItem extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTextBubble(String content) {
|
||||||
|
final chatBubble = _senderProfile()?.getChatBox();
|
||||||
|
final chatBubbleUrl = _resolveChatBubbleImageUrl(chatBubble);
|
||||||
|
if (chatBubbleUrl.isNotEmpty) {
|
||||||
|
return _buildVipTextBubble(content: content, imageUrl: chatBubbleUrl);
|
||||||
|
}
|
||||||
|
return _buildDefaultTextBubble(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
SocialChatUserProfile? _senderProfile() {
|
||||||
|
if (message.isSelf ?? false) {
|
||||||
|
return AccountStorage().getCurrentUser()?.userProfile;
|
||||||
|
}
|
||||||
|
return friend;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDefaultTextBubble(String content) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color(0xff18F2B1).withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topLeft: message.isSelf! ? Radius.circular(8) : Radius.circular(0),
|
||||||
|
topRight: Radius.circular(8.w),
|
||||||
|
bottomLeft: Radius.circular(8.w),
|
||||||
|
bottomRight:
|
||||||
|
message.isSelf! ? Radius.circular(0) : Radius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(8.0.w),
|
||||||
|
child: Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: width(220)),
|
||||||
|
child: ExtendedText(
|
||||||
|
content,
|
||||||
|
style: TextStyle(fontSize: sp(14), color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVipTextBubble({
|
||||||
|
required String content,
|
||||||
|
required String imageUrl,
|
||||||
|
}) {
|
||||||
|
final textStyle = TextStyle(fontSize: sp(14), color: Colors.white);
|
||||||
|
final multilineMaxBubbleWidth = math.min(
|
||||||
|
ScreenUtil().screenWidth * 0.64,
|
||||||
|
260.w,
|
||||||
|
);
|
||||||
|
final multilineMaxTextWidth = math.max(
|
||||||
|
90.w,
|
||||||
|
multilineMaxBubbleWidth - 58.w,
|
||||||
|
);
|
||||||
|
final multilineTextPainter = TextPainter(
|
||||||
|
text: TextSpan(text: content, style: textStyle),
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
)..layout(maxWidth: multilineMaxTextWidth);
|
||||||
|
if (multilineTextPainter.computeLineMetrics().length > 1) {
|
||||||
|
return _buildMultilineVipTextBubble(
|
||||||
|
content: content,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
textStyle: textStyle,
|
||||||
|
textSize: multilineTextPainter.size,
|
||||||
|
maxBubbleWidth: multilineMaxBubbleWidth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildSingleLineVipTextBubble(
|
||||||
|
content: content,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
textStyle: textStyle,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMultilineVipTextBubble({
|
||||||
|
required String content,
|
||||||
|
required String imageUrl,
|
||||||
|
required TextStyle textStyle,
|
||||||
|
required Size textSize,
|
||||||
|
required double maxBubbleWidth,
|
||||||
|
}) {
|
||||||
|
final bubbleWidth = math
|
||||||
|
.max(126.w, textSize.width + 58.w)
|
||||||
|
.clamp(126.w, maxBubbleWidth);
|
||||||
|
final textMaxWidth = math.max(90.w, bubbleWidth - 58.w);
|
||||||
|
final bubbleHeight = math.max(58.w, textSize.height + 26.w);
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: bubbleWidth,
|
||||||
|
height: bubbleHeight,
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
_buildVipBubbleBackground(
|
||||||
|
url: imageUrl,
|
||||||
|
width: bubbleWidth,
|
||||||
|
height: bubbleHeight,
|
||||||
|
fallback: _buildVipBubbleFallback(),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsetsDirectional.fromSTEB(29.w, 11.w, 29.w, 11.w),
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: SizedBox(
|
||||||
|
width: textMaxWidth,
|
||||||
|
child: ExtendedText(content, style: textStyle),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSingleLineVipTextBubble({
|
||||||
|
required String content,
|
||||||
|
required String imageUrl,
|
||||||
|
required TextStyle textStyle,
|
||||||
|
}) {
|
||||||
|
final horizontalPadding = 18.w;
|
||||||
|
final verticalPadding = 8.w;
|
||||||
|
final minSkinWidth = 132.w;
|
||||||
|
final minSkinHeight = 44.w;
|
||||||
|
final maxTextWidth = width(220);
|
||||||
|
final maxBubbleWidth = maxTextWidth + horizontalPadding * 2;
|
||||||
|
final textSize = _measureTextSize(
|
||||||
|
text: content,
|
||||||
|
style: textStyle,
|
||||||
|
maxWidth: maxTextWidth,
|
||||||
|
);
|
||||||
|
final bubbleWidth = math.min(
|
||||||
|
maxBubbleWidth,
|
||||||
|
math.max(horizontalPadding * 2, textSize.width + horizontalPadding * 2),
|
||||||
|
);
|
||||||
|
final textMaxWidth = math.max(1.0, bubbleWidth - horizontalPadding * 2);
|
||||||
|
final bubbleHeight = math.max(
|
||||||
|
verticalPadding * 2,
|
||||||
|
textSize.height + verticalPadding * 2,
|
||||||
|
);
|
||||||
|
final skinAlignment =
|
||||||
|
message.isSelf!
|
||||||
|
? AlignmentDirectional.centerEnd
|
||||||
|
: AlignmentDirectional.centerStart;
|
||||||
|
|
||||||
|
return SizedBox(
|
||||||
|
width: bubbleWidth,
|
||||||
|
height: bubbleHeight,
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: _buildVipBubbleFallback()),
|
||||||
|
Positioned.fill(
|
||||||
|
child: _buildVipBubbleSkin(
|
||||||
|
url: imageUrl,
|
||||||
|
width: math.max(bubbleWidth, minSkinWidth),
|
||||||
|
height: math.max(bubbleHeight, minSkinHeight),
|
||||||
|
alignment: skinAlignment,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsetsDirectional.symmetric(
|
||||||
|
horizontal: horizontalPadding,
|
||||||
|
vertical: verticalPadding,
|
||||||
|
),
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: SizedBox(
|
||||||
|
width: textMaxWidth,
|
||||||
|
child: ExtendedText(content, style: textStyle),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVipBubbleSkin({
|
||||||
|
required String url,
|
||||||
|
required double width,
|
||||||
|
required double height,
|
||||||
|
required AlignmentGeometry alignment,
|
||||||
|
}) {
|
||||||
|
return OverflowBox(
|
||||||
|
alignment: alignment,
|
||||||
|
minWidth: width,
|
||||||
|
maxWidth: width,
|
||||||
|
minHeight: height,
|
||||||
|
maxHeight: height,
|
||||||
|
child:
|
||||||
|
SCNetworkSvgaWidget.isSvga(url)
|
||||||
|
? SCNetworkSvgaWidget(
|
||||||
|
resource: url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
fallback: SizedBox(width: width, height: height),
|
||||||
|
)
|
||||||
|
: SCNinePatchImage(
|
||||||
|
url: url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
centerSliceRatio: const EdgeInsets.fromLTRB(
|
||||||
|
0.34,
|
||||||
|
0.34,
|
||||||
|
0.34,
|
||||||
|
0.34,
|
||||||
|
),
|
||||||
|
fallback: SizedBox(width: width, height: height),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVipBubbleBackground({
|
||||||
|
required String url,
|
||||||
|
required double width,
|
||||||
|
required double height,
|
||||||
|
required Widget fallback,
|
||||||
|
}) {
|
||||||
|
if (SCNetworkSvgaWidget.isSvga(url)) {
|
||||||
|
return SCNetworkSvgaWidget(
|
||||||
|
resource: url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
fallback: fallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SCNinePatchImage(
|
||||||
|
url: url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
centerSliceRatio: const EdgeInsets.fromLTRB(0.34, 0.34, 0.34, 0.34),
|
||||||
|
fallback: fallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Size _measureTextSize({
|
||||||
|
required String text,
|
||||||
|
required TextStyle style,
|
||||||
|
required double maxWidth,
|
||||||
|
}) {
|
||||||
|
final textPainter = TextPainter(
|
||||||
|
text: TextSpan(text: text, style: style),
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
)..layout(maxWidth: maxWidth);
|
||||||
|
return textPainter.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVipBubbleFallback() {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Color(0xff18F2B1).withValues(alpha: 0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8.w),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _resolveChatBubbleImageUrl(PropsResources? resource) {
|
||||||
|
return _firstNonBlank([
|
||||||
|
resource?.imSendCoverUrl,
|
||||||
|
resource?.imOpenedUrl,
|
||||||
|
resource?.imNotOpenedUrl,
|
||||||
|
resource?.roomSendCoverUrl,
|
||||||
|
resource?.roomOpenedUrl,
|
||||||
|
resource?.roomNotOpenedUrl,
|
||||||
|
resource?.cover,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _firstNonBlank(Iterable<String?> values) {
|
||||||
|
for (final value in values) {
|
||||||
|
final text = value?.trim();
|
||||||
|
if (text != null && text.isNotEmpty) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
void _showMsgItemMenu(BuildContext ct, String content) {
|
void _showMsgItemMenu(BuildContext ct, String content) {
|
||||||
SmartDialog.showAttach(
|
SmartDialog.showAttach(
|
||||||
tag: "showMsgItemMenu",
|
tag: "showMsgItemMenu",
|
||||||
|
|||||||
@ -136,6 +136,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
required int quantity,
|
required int quantity,
|
||||||
int animationCount = 1,
|
int animationCount = 1,
|
||||||
}) {
|
}) {
|
||||||
|
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
|
||||||
final rtmProvider = Provider.of<RtmProvider>(
|
final rtmProvider = Provider.of<RtmProvider>(
|
||||||
navigatorKey.currentState!.context,
|
navigatorKey.currentState!.context,
|
||||||
listen: false,
|
listen: false,
|
||||||
@ -173,6 +174,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
gift: gift,
|
gift: gift,
|
||||||
user: currentUser,
|
user: currentUser,
|
||||||
toUser: targetUser,
|
toUser: targetUser,
|
||||||
|
targetUserIds: targetUserIds,
|
||||||
number: quantity,
|
number: quantity,
|
||||||
customAnimationCount: animationCount,
|
customAnimationCount: animationCount,
|
||||||
),
|
),
|
||||||
@ -220,19 +222,13 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
Provider.of<SocialChatUserProfileManager>(context, listen: false).balance();
|
||||||
rtcProvider?.roomWheatMap.forEach((k, v) {
|
rtcProvider?.roomWheatMap.forEach((k, v) {
|
||||||
if (v.user != null) {
|
if (v.user != null) {
|
||||||
if (v.user?.id == AccountStorage().getCurrentUser()?.userProfile?.id) {
|
listMai.add(HeadSelect(widget.toUser == null, v));
|
||||||
listMai.add(HeadSelect(true, v));
|
|
||||||
} else {
|
|
||||||
listMai.add(HeadSelect(false, v));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isAll = true;
|
|
||||||
for (var mai in listMai) {
|
|
||||||
if (!mai.isSelect) {
|
|
||||||
isAll = false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
isAll =
|
||||||
|
widget.toUser == null &&
|
||||||
|
listMai.isNotEmpty &&
|
||||||
|
listMai.every((mai) => mai.isSelect);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -1397,6 +1393,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
int animationCount = 1,
|
int animationCount = 1,
|
||||||
}) {
|
}) {
|
||||||
///发送一条IM消息
|
///发送一条IM消息
|
||||||
|
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
|
||||||
for (var u in acceptUsers) {
|
for (var u in acceptUsers) {
|
||||||
final special = gift.special ?? "";
|
final special = gift.special ?? "";
|
||||||
final giftSourceUrl = gift.giftSourceUrl ?? "";
|
final giftSourceUrl = gift.giftSourceUrl ?? "";
|
||||||
@ -1428,6 +1425,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
gift: gift,
|
gift: gift,
|
||||||
user: AccountStorage().getCurrentUser()?.userProfile,
|
user: AccountStorage().getCurrentUser()?.userProfile,
|
||||||
toUser: u.user,
|
toUser: u.user,
|
||||||
|
targetUserIds: targetUserIds,
|
||||||
number: quantity,
|
number: quantity,
|
||||||
customAnimationCount: animationCount,
|
customAnimationCount: animationCount,
|
||||||
type: SCRoomMsgType.gift,
|
type: SCRoomMsgType.gift,
|
||||||
@ -1603,11 +1601,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
required int quantity,
|
required int quantity,
|
||||||
int animationCount = 1,
|
int animationCount = 1,
|
||||||
}) async {
|
}) async {
|
||||||
final targetUserIds =
|
final targetUserIds = _resolveAcceptUserIds(acceptUsers);
|
||||||
acceptUsers
|
|
||||||
.map((u) => u.user?.id ?? "")
|
|
||||||
.where((id) => id.isNotEmpty)
|
|
||||||
.toList();
|
|
||||||
final firstTargetUser =
|
final firstTargetUser =
|
||||||
acceptUsers.isNotEmpty ? acceptUsers.first.user : null;
|
acceptUsers.isNotEmpty ? acceptUsers.first.user : null;
|
||||||
_giftFxLog(
|
_giftFxLog(
|
||||||
@ -1631,6 +1625,7 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
gift: gift,
|
gift: gift,
|
||||||
user: AccountStorage().getCurrentUser()?.userProfile,
|
user: AccountStorage().getCurrentUser()?.userProfile,
|
||||||
toUser: firstTargetUser,
|
toUser: firstTargetUser,
|
||||||
|
targetUserIds: targetUserIds,
|
||||||
number: quantity,
|
number: quantity,
|
||||||
customAnimationCount: animationCount,
|
customAnimationCount: animationCount,
|
||||||
type: SCRoomMsgType.luckGiftAnimOther,
|
type: SCRoomMsgType.luckGiftAnimOther,
|
||||||
@ -1644,6 +1639,18 @@ class _GiftPageState extends State<GiftPage> with TickerProviderStateMixin {
|
|||||||
'targetUserIds=${targetUserIds.join(",")}',
|
'targetUserIds=${targetUserIds.join(",")}',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> _resolveAcceptUserIds(List<MicRes> acceptUsers) {
|
||||||
|
final targetUserIds = <String>[];
|
||||||
|
for (final acceptUser in acceptUsers) {
|
||||||
|
final userId = (acceptUser.user?.id ?? "").trim();
|
||||||
|
if (userId.isEmpty || targetUserIds.contains(userId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
targetUserIds.add(userId);
|
||||||
|
}
|
||||||
|
return targetUserIds;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CheckNumber extends StatelessWidget {
|
class CheckNumber extends StatelessWidget {
|
||||||
|
|||||||
@ -9,9 +9,10 @@ import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository
|
|||||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||||
import 'package:yumi/app/constants/sc_screen.dart';
|
import 'package:yumi/app/constants/sc_screen.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
|
import 'package:yumi/shared/business_logic/models/res/room_member_res.dart';
|
||||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||||
|
|
||||||
@ -93,9 +94,10 @@ class _RoomMemberPageState
|
|||||||
list.isNotEmpty ? buildTag(list.first.roles) : Container(),
|
list.isNotEmpty ? buildTag(list.first.roles) : Container(),
|
||||||
SizedBox(height: 3.w),
|
SizedBox(height: 3.w),
|
||||||
Column(
|
Column(
|
||||||
children: List.generate(list.length, (cIndex) {
|
children: List.generate(list.length, (cIndex) {
|
||||||
SocialChatRoomMemberRes userInfo = list[cIndex];
|
SocialChatRoomMemberRes userInfo = list[cIndex];
|
||||||
return GestureDetector(
|
final userProfile = _mergeCurrentUserVip(userInfo.userProfile);
|
||||||
|
return GestureDetector(
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
|
margin: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 8.w),
|
||||||
@ -106,10 +108,9 @@ class _RoomMemberPageState
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
head(
|
head(
|
||||||
url: userInfo.userProfile?.userAvatar ?? "",
|
url: userProfile?.userAvatar ?? "",
|
||||||
width: 52.w,
|
width: 52.w,
|
||||||
headdress:
|
headdress: userProfile?.getHeaddress()?.sourceUrl,
|
||||||
userInfo.userProfile?.getHeaddress()?.sourceUrl,
|
|
||||||
),
|
),
|
||||||
SizedBox(width: 3.w),
|
SizedBox(width: 3.w),
|
||||||
Column(
|
Column(
|
||||||
@ -129,57 +130,47 @@ class _RoomMemberPageState
|
|||||||
SizedBox(width: 3.w),
|
SizedBox(width: 3.w),
|
||||||
socialchatNickNameText(
|
socialchatNickNameText(
|
||||||
maxWidth: 160.w,
|
maxWidth: 160.w,
|
||||||
userInfo.userProfile?.userNickname ?? "",
|
userProfile?.userNickname ?? "",
|
||||||
fontSize: 14.sp,
|
fontSize: 14.sp,
|
||||||
textColor: Colors.black,
|
textColor: Colors.black,
|
||||||
type:
|
type:
|
||||||
userInfo.userProfile?.getVIP()?.name ??
|
userProfile?.getVIP()?.name ?? "",
|
||||||
"",
|
needScroll:
|
||||||
needScroll:
|
(userProfile
|
||||||
(userInfo
|
?.userNickname
|
||||||
.userProfile
|
?.characters
|
||||||
?.userNickname
|
.length ??
|
||||||
?.characters
|
0) >
|
||||||
.length ??
|
|
||||||
0) >
|
|
||||||
14,
|
14,
|
||||||
),
|
),
|
||||||
SizedBox(width: 3.w),
|
SizedBox(width: 3.w),
|
||||||
userInfo.userProfile?.getVIP() != null
|
userProfile?.getVIP() != null
|
||||||
? netImage(
|
? netImage(
|
||||||
url:
|
url: userProfile?.getVIP()?.cover ?? "",
|
||||||
userInfo.userProfile
|
|
||||||
?.getVIP()
|
|
||||||
?.cover ??
|
|
||||||
"",
|
|
||||||
width: 25.w,
|
width: 25.w,
|
||||||
height: 25.w,
|
height: 25.w,
|
||||||
)
|
)
|
||||||
: Container(),
|
: Container(),
|
||||||
SizedBox(width: 5.w),
|
SizedBox(width: 5.w),
|
||||||
userInfo.userProfile?.wearBadge?.isNotEmpty ??
|
userProfile?.wearBadge?.isNotEmpty ??
|
||||||
false
|
false
|
||||||
? SizedBox(
|
? SizedBox(
|
||||||
height: 25.w,
|
height: 25.w,
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount:
|
itemCount:
|
||||||
userInfo
|
userProfile?.wearBadge?.length ??
|
||||||
.userProfile
|
0,
|
||||||
?.wearBadge
|
|
||||||
?.length ??
|
|
||||||
0,
|
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return netImage(
|
return netImage(
|
||||||
width: 25.w,
|
width: 25.w,
|
||||||
height: 25.w,
|
height: 25.w,
|
||||||
url:
|
url:
|
||||||
userInfo
|
userProfile
|
||||||
.userProfile
|
?.wearBadge?[index]
|
||||||
?.wearBadge?[index]
|
.selectUrl ??
|
||||||
.selectUrl ??
|
"",
|
||||||
"",
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
separatorBuilder: (
|
separatorBuilder: (
|
||||||
@ -197,18 +188,17 @@ class _RoomMemberPageState
|
|||||||
),
|
),
|
||||||
SizedBox(height: 3.w),
|
SizedBox(height: 3.w),
|
||||||
SCSpecialIdBadge(
|
SCSpecialIdBadge(
|
||||||
idText: userInfo.userProfile?.getID() ?? "",
|
idText: userProfile?.getID() ?? "",
|
||||||
showAnimated:
|
showAnimated: userProfile?.hasSpecialId() ?? false,
|
||||||
userInfo.userProfile?.hasSpecialId() ?? false,
|
|
||||||
assetPath: SCSpecialIdAssets.userId,
|
assetPath: SCSpecialIdAssets.userId,
|
||||||
animationWidth: 62.w,
|
animationWidth: 62.w,
|
||||||
animationHeight: 24.w,
|
animationHeight: 24.w,
|
||||||
showTextBesideAnimated: true,
|
showTextBesideAnimated: true,
|
||||||
animatedTextSpacing: 0,
|
animatedTextSpacing: 0,
|
||||||
showAnimatedGradientText:
|
showAnimatedGradientText:
|
||||||
userInfo.userProfile
|
userProfile
|
||||||
?.shouldShowColoredSpecialIdText() ??
|
?.shouldShowColoredSpecialIdText() ??
|
||||||
false,
|
false,
|
||||||
animationTextStyle: TextStyle(
|
animationTextStyle: TextStyle(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
fontSize: 12.sp,
|
fontSize: 12.sp,
|
||||||
@ -362,10 +352,29 @@ class _RoomMemberPageState
|
|||||||
if (onErr != null) {
|
if (onErr != null) {
|
||||||
onErr();
|
onErr();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget buildTag(String? roles) {
|
SocialChatUserProfile? _mergeCurrentUserVip(
|
||||||
|
SocialChatUserProfile? profile,
|
||||||
|
) {
|
||||||
|
if (profile == null || profile.vipLevelForColoredId > 0) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||||
|
if (currentProfile == null ||
|
||||||
|
currentProfile.vipLevelForColoredId <= 0 ||
|
||||||
|
currentProfile.id != profile.id) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
return profile.copyWith(
|
||||||
|
vipLevel:
|
||||||
|
currentProfile.vipLevel ??
|
||||||
|
currentProfile.vipLevelForColoredId.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget buildTag(String? roles) {
|
||||||
if (roles == SCRoomRolesType.HOMEOWNER.name) {
|
if (roles == SCRoomRolesType.HOMEOWNER.name) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
import 'package:flutter/cupertino.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||||
import 'package:yumi/main.dart';
|
import 'package:yumi/main.dart';
|
||||||
|
|
||||||
import 'package:yumi/app_localizations.dart';
|
import 'package:yumi/app_localizations.dart';
|
||||||
@ -47,9 +49,10 @@ class _RoomGiftRankTabPageState
|
|||||||
return buildList(context);
|
return buildList(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget buildItemOne(RoomGiftRankRes userInfo, int index) {
|
Widget buildItemOne(RoomGiftRankRes userInfo, int index) {
|
||||||
return GestureDetector(
|
final userProfile = _mergeCurrentUserVip(userInfo.userProfile);
|
||||||
|
return GestureDetector(
|
||||||
child: Container(
|
child: Container(
|
||||||
margin: EdgeInsets.symmetric(vertical: 3.w),
|
margin: EdgeInsets.symmetric(vertical: 3.w),
|
||||||
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
padding: EdgeInsets.symmetric(horizontal: 16.w),
|
||||||
@ -90,14 +93,14 @@ class _RoomGiftRankTabPageState
|
|||||||
SizedBox(width: 10.w),
|
SizedBox(width: 10.w),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
child: head(
|
child: head(
|
||||||
url: userInfo.userProfile?.userAvatar ?? "",
|
url: userProfile?.userAvatar ?? "",
|
||||||
width: 55.w,
|
width: 55.w,
|
||||||
// headdress: userInfo.userProfile?.getHeaddress()?.sourceUrl,
|
// headdress: userInfo.userProfile?.getHeaddress()?.sourceUrl,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
showBottomInCenterDialog(
|
showBottomInCenterDialog(
|
||||||
navigatorKey.currentState!.context,
|
navigatorKey.currentState!.context,
|
||||||
RoomUserInfoCard(userId: userInfo.userProfile?.id),
|
RoomUserInfoCard(userId: userProfile?.id),
|
||||||
);
|
);
|
||||||
SmartDialog.dismiss(tag: "showRoomGiftRankPage");
|
SmartDialog.dismiss(tag: "showRoomGiftRankPage");
|
||||||
},
|
},
|
||||||
@ -110,16 +113,12 @@ class _RoomGiftRankTabPageState
|
|||||||
children: [
|
children: [
|
||||||
socialchatNickNameText(
|
socialchatNickNameText(
|
||||||
maxWidth: 170.w,
|
maxWidth: 170.w,
|
||||||
userInfo.userProfile?.userNickname ?? "",
|
userProfile?.userNickname ?? "",
|
||||||
fontSize: 14.sp,
|
fontSize: 14.sp,
|
||||||
type: userInfo.userProfile?.getVIP()?.name ?? "",
|
type: userProfile?.getVIP()?.name ?? "",
|
||||||
needScroll:
|
needScroll:
|
||||||
(userInfo
|
(userProfile?.userNickname?.characters.length ??
|
||||||
.userProfile
|
0) >
|
||||||
?.userNickname
|
|
||||||
?.characters
|
|
||||||
.length ??
|
|
||||||
0) >
|
|
||||||
14,
|
14,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -131,18 +130,17 @@ class _RoomGiftRankTabPageState
|
|||||||
textDirection: TextDirection.ltr,
|
textDirection: TextDirection.ltr,
|
||||||
children: [
|
children: [
|
||||||
SCSpecialIdBadge(
|
SCSpecialIdBadge(
|
||||||
idText: userInfo.userProfile?.getID() ?? "",
|
idText: userProfile?.getID() ?? "",
|
||||||
showAnimated:
|
showAnimated: userProfile?.hasSpecialId() ?? false,
|
||||||
userInfo.userProfile?.hasSpecialId() ?? false,
|
|
||||||
assetPath: SCSpecialIdAssets.userId,
|
assetPath: SCSpecialIdAssets.userId,
|
||||||
animationWidth: 62.w,
|
animationWidth: 62.w,
|
||||||
animationHeight: 24.w,
|
animationHeight: 24.w,
|
||||||
showTextBesideAnimated: true,
|
showTextBesideAnimated: true,
|
||||||
animatedTextSpacing: 0,
|
animatedTextSpacing: 0,
|
||||||
showAnimatedGradientText:
|
showAnimatedGradientText:
|
||||||
userInfo.userProfile
|
userProfile
|
||||||
?.shouldShowColoredSpecialIdText() ??
|
?.shouldShowColoredSpecialIdText() ??
|
||||||
false,
|
false,
|
||||||
animationTextStyle: TextStyle(
|
animationTextStyle: TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 12.sp,
|
fontSize: 12.sp,
|
||||||
@ -165,7 +163,7 @@ class _RoomGiftRankTabPageState
|
|||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
ClipboardData(text: userInfo.userProfile?.getID() ?? ""),
|
ClipboardData(text: userProfile?.getID() ?? ""),
|
||||||
);
|
);
|
||||||
SCTts.show(
|
SCTts.show(
|
||||||
SCAppLocalizations.of(context)!.copiedToClipboard,
|
SCAppLocalizations.of(context)!.copiedToClipboard,
|
||||||
@ -191,11 +189,30 @@ class _RoomGiftRankTabPageState
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
onTap: () {},
|
onTap: () {},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
SocialChatUserProfile? _mergeCurrentUserVip(
|
||||||
empty() {
|
SocialChatUserProfile? profile,
|
||||||
|
) {
|
||||||
|
if (profile == null || profile.vipLevelForColoredId > 0) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||||
|
if (currentProfile == null ||
|
||||||
|
currentProfile.vipLevelForColoredId <= 0 ||
|
||||||
|
currentProfile.id != profile.id) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
return profile.copyWith(
|
||||||
|
vipLevel:
|
||||||
|
currentProfile.vipLevel ??
|
||||||
|
currentProfile.vipLevelForColoredId.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
empty() {
|
||||||
return mainEmpty(
|
return mainEmpty(
|
||||||
image: Image.asset(
|
image: Image.asset(
|
||||||
'sc_images/general/sc_icon_loading.png',
|
'sc_images/general/sc_icon_loading.png',
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
import 'package:yumi/app/constants/sc_room_msg_type.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
||||||
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
||||||
@ -56,6 +58,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
static const Duration _giftAnimationQueueDrainWindow =
|
static const Duration _giftAnimationQueueDrainWindow =
|
||||||
_luckyGiftQueueDrainWindow;
|
_luckyGiftQueueDrainWindow;
|
||||||
static const int _maxTrackedGiftAnimations = _maxLuckyGiftTrackedAnimations;
|
static const int _maxTrackedGiftAnimations = _maxLuckyGiftTrackedAnimations;
|
||||||
|
static const Duration _giftFlightBatchDedupeWindow = Duration(
|
||||||
|
milliseconds: 650,
|
||||||
|
);
|
||||||
|
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()];
|
final List<Widget> _pages = [AllChatPage(), ChatPage(), GiftChatPage()];
|
||||||
@ -64,6 +69,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
RoomGiftSeatFlightController();
|
RoomGiftSeatFlightController();
|
||||||
final Map<String, _LuckyGiftComboSession> _luckyGiftComboSessions =
|
final Map<String, _LuckyGiftComboSession> _luckyGiftComboSessions =
|
||||||
<String, _LuckyGiftComboSession>{};
|
<String, _LuckyGiftComboSession>{};
|
||||||
|
final Map<String, Timer> _giftFlightBatchDedupeTimers = <String, Timer>{};
|
||||||
int _shownRoomStartupFailureToken = 0;
|
int _shownRoomStartupFailureToken = 0;
|
||||||
RtcProvider? _rtcProvider;
|
RtcProvider? _rtcProvider;
|
||||||
bool _roomProviderRebuildScheduled = false;
|
bool _roomProviderRebuildScheduled = false;
|
||||||
@ -84,6 +90,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
listen: false,
|
listen: false,
|
||||||
).toggleGiftAnimationVisibility(false);
|
).toggleGiftAnimationVisibility(false);
|
||||||
_clearLuckyGiftComboSessions();
|
_clearLuckyGiftComboSessions();
|
||||||
|
_clearGiftFlightBatchDedupeTimers();
|
||||||
_giftSeatFlightController.clear();
|
_giftSeatFlightController.clear();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -169,6 +176,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
|
|
||||||
RoomEntranceHelper.clearQueue();
|
RoomEntranceHelper.clearQueue();
|
||||||
_clearLuckyGiftComboSessions();
|
_clearLuckyGiftComboSessions();
|
||||||
|
_clearGiftFlightBatchDedupeTimers();
|
||||||
_giftSeatFlightController.clear();
|
_giftSeatFlightController.clear();
|
||||||
OverlayManager().removeRoom();
|
OverlayManager().removeRoom();
|
||||||
SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend');
|
SCRoomEffectScheduler().clearDeferredTasks(reason: 'voice_room_suspend');
|
||||||
@ -314,7 +322,10 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [_buildChatView(), RoomBottomWidget()],
|
children: [
|
||||||
|
_buildChatView(),
|
||||||
|
const RoomBottomWidget(showGiftComboButton: false),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
LGiftAnimalPage(),
|
LGiftAnimalPage(),
|
||||||
Transform.translate(
|
Transform.translate(
|
||||||
@ -336,6 +347,7 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
// _buildPlayViews(),
|
// _buildPlayViews(),
|
||||||
///幸运礼物中奖动画
|
///幸运礼物中奖动画
|
||||||
LuckGiftNomorAnimWidget(),
|
LuckGiftNomorAnimWidget(),
|
||||||
|
const RoomGiftComboFloatingLayer(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -491,16 +503,27 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
).enqueueGiftAnimation(giftModel);
|
).enqueueGiftAnimation(giftModel);
|
||||||
|
|
||||||
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
|
final giftPhoto = (msg.gift?.giftPhoto ?? "").trim();
|
||||||
final targetUserId = _resolveGiftTargetUserId(msg);
|
final targetUserIds = _resolveGiftTargetUserIds(msg);
|
||||||
|
final targetGroupKey = _buildGiftTargetGroupKey(targetUserIds);
|
||||||
if (_supportsComboMilestoneEffects(msg)) {
|
if (_supportsComboMilestoneEffects(msg)) {
|
||||||
_handleComboMilestoneVisuals(msg, targetUserId);
|
_handleComboMilestoneVisuals(msg, targetGroupKey);
|
||||||
}
|
}
|
||||||
final isLuckyGift = _isLuckyGiftMessage(msg);
|
final isLuckyGift = _isLuckyGiftMessage(msg);
|
||||||
if (isLuckyGift) {
|
if (isLuckyGift) {
|
||||||
_handleLuckyGiftComboVisuals(msg, giftPhoto, targetUserId);
|
_handleLuckyGiftComboVisuals(
|
||||||
|
msg,
|
||||||
|
giftPhoto,
|
||||||
|
targetGroupKey,
|
||||||
|
targetUserIds,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_handleStandardGiftComboVisuals(msg, giftPhoto, targetUserId);
|
_handleStandardGiftComboVisuals(
|
||||||
|
msg,
|
||||||
|
giftPhoto,
|
||||||
|
targetGroupKey,
|
||||||
|
targetUserIds,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _luckyGiftRewardTickerListener(Msg msg) {
|
void _luckyGiftRewardTickerListener(Msg msg) {
|
||||||
@ -612,17 +635,24 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
void _handleStandardGiftComboVisuals(
|
void _handleStandardGiftComboVisuals(
|
||||||
Msg msg,
|
Msg msg,
|
||||||
String giftPhoto,
|
String giftPhoto,
|
||||||
String? targetUserId,
|
String? targetGroupKey,
|
||||||
|
List<String> targetUserIds,
|
||||||
) {
|
) {
|
||||||
if ((msg.number ?? 0) <= 0) {
|
if ((msg.number ?? 0) <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!_shouldPlaySeatFlightGiftAnimation(msg) ||
|
if (!_shouldPlaySeatFlightGiftAnimation(msg) ||
|
||||||
targetUserId == null ||
|
targetGroupKey == null ||
|
||||||
|
targetUserIds.isEmpty ||
|
||||||
giftPhoto.isEmpty) {
|
giftPhoto.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId);
|
final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds);
|
||||||
|
if (targetUserIds.length > 1 &&
|
||||||
|
!_markGiftFlightBatchForPlayback(flightBatchKey)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey);
|
||||||
final session = _luckyGiftComboSessions.putIfAbsent(
|
final session = _luckyGiftComboSessions.putIfAbsent(
|
||||||
sessionKey,
|
sessionKey,
|
||||||
() => _LuckyGiftComboSession(),
|
() => _LuckyGiftComboSession(),
|
||||||
@ -633,8 +663,9 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
_enqueueTrackedSeatFlightAnimations(
|
_enqueueTrackedSeatFlightAnimations(
|
||||||
sessionKey: sessionKey,
|
sessionKey: sessionKey,
|
||||||
giftPhoto: giftPhoto,
|
giftPhoto: giftPhoto,
|
||||||
targetUserId: targetUserId,
|
targetUserIds: targetUserIds,
|
||||||
animationCount: _resolveTrackedAnimationCount(msg),
|
animationCount: _resolveTrackedAnimationCount(msg),
|
||||||
|
batchKey: flightBatchKey,
|
||||||
);
|
);
|
||||||
|
|
||||||
_scheduleGiftAnimationSessionEnd(sessionKey);
|
_scheduleGiftAnimationSessionEnd(sessionKey);
|
||||||
@ -643,12 +674,16 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
void _handleLuckyGiftComboVisuals(
|
void _handleLuckyGiftComboVisuals(
|
||||||
Msg msg,
|
Msg msg,
|
||||||
String giftPhoto,
|
String giftPhoto,
|
||||||
String? targetUserId,
|
String? targetGroupKey,
|
||||||
|
List<String> targetUserIds,
|
||||||
) {
|
) {
|
||||||
if ((msg.number ?? 0) <= 0) {
|
if ((msg.number ?? 0) <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetUserId);
|
if (targetGroupKey == null || targetUserIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final sessionKey = _buildLuckyGiftComboSessionKey(msg, targetGroupKey);
|
||||||
final session = _luckyGiftComboSessions[sessionKey];
|
final session = _luckyGiftComboSessions[sessionKey];
|
||||||
if (session == null) {
|
if (session == null) {
|
||||||
return;
|
return;
|
||||||
@ -656,14 +691,19 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
session.endTimer?.cancel();
|
session.endTimer?.cancel();
|
||||||
session.clearQueueTimer?.cancel();
|
session.clearQueueTimer?.cancel();
|
||||||
|
|
||||||
if (_shouldPlaySeatFlightGiftAnimation(msg) &&
|
if (_shouldPlaySeatFlightGiftAnimation(msg) && giftPhoto.isNotEmpty) {
|
||||||
targetUserId != null &&
|
final flightBatchKey = _buildGiftFlightBatchKey(msg, targetUserIds);
|
||||||
giftPhoto.isNotEmpty) {
|
if (targetUserIds.length > 1 &&
|
||||||
|
!_markGiftFlightBatchForPlayback(flightBatchKey)) {
|
||||||
|
_scheduleGiftAnimationSessionEnd(sessionKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
_enqueueTrackedSeatFlightAnimations(
|
_enqueueTrackedSeatFlightAnimations(
|
||||||
sessionKey: sessionKey,
|
sessionKey: sessionKey,
|
||||||
giftPhoto: giftPhoto,
|
giftPhoto: giftPhoto,
|
||||||
targetUserId: targetUserId,
|
targetUserIds: targetUserIds,
|
||||||
animationCount: _resolveTrackedAnimationCount(msg),
|
animationCount: _resolveTrackedAnimationCount(msg),
|
||||||
|
batchKey: flightBatchKey,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_scheduleGiftAnimationSessionEnd(sessionKey);
|
_scheduleGiftAnimationSessionEnd(sessionKey);
|
||||||
@ -689,28 +729,42 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
void _enqueueTrackedSeatFlightAnimations({
|
void _enqueueTrackedSeatFlightAnimations({
|
||||||
required String sessionKey,
|
required String sessionKey,
|
||||||
required String giftPhoto,
|
required String giftPhoto,
|
||||||
required String targetUserId,
|
required List<String> targetUserIds,
|
||||||
required int animationCount,
|
required int animationCount,
|
||||||
|
required String batchKey,
|
||||||
}) {
|
}) {
|
||||||
unawaited(
|
unawaited(
|
||||||
warmImageResource(giftPhoto, logicalWidth: 96.w, logicalHeight: 96.w),
|
warmImageResource(giftPhoto, logicalWidth: 96.w, logicalHeight: 96.w),
|
||||||
);
|
);
|
||||||
|
final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds);
|
||||||
|
if (normalizedTargetUserIds.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
final normalizedAnimationCount = math.max(animationCount, 1);
|
final normalizedAnimationCount = math.max(animationCount, 1);
|
||||||
final cappedAnimationCount = math.min(
|
final cappedAnimationCount = math.min(
|
||||||
normalizedAnimationCount,
|
normalizedAnimationCount,
|
||||||
_maxTrackedGiftAnimations,
|
_maxTrackedGiftAnimations,
|
||||||
);
|
);
|
||||||
|
final maxTrackedRequests = math.max(
|
||||||
|
_maxTrackedGiftAnimations * normalizedTargetUserIds.length,
|
||||||
|
_maxTrackedGiftAnimations,
|
||||||
|
);
|
||||||
for (var index = 0; index < cappedAnimationCount; index += 1) {
|
for (var index = 0; index < cappedAnimationCount; index += 1) {
|
||||||
_giftSeatFlightController.enqueueLimited(
|
final roundBatchTag =
|
||||||
RoomGiftSeatFlightRequest(
|
normalizedTargetUserIds.length > 1 ? '$batchKey|round:$index' : null;
|
||||||
imagePath: giftPhoto,
|
for (final targetUserId in normalizedTargetUserIds) {
|
||||||
targetUserId: targetUserId,
|
_giftSeatFlightController.enqueueLimited(
|
||||||
beginSize: 96.w,
|
RoomGiftSeatFlightRequest(
|
||||||
endSize: 28.w,
|
imagePath: giftPhoto,
|
||||||
queueTag: sessionKey,
|
targetUserId: targetUserId,
|
||||||
),
|
beginSize: 96.w,
|
||||||
maxTrackedRequests: _maxTrackedGiftAnimations,
|
endSize: 28.w,
|
||||||
);
|
queueTag: sessionKey,
|
||||||
|
batchTag: roundBatchTag,
|
||||||
|
),
|
||||||
|
maxTrackedRequests: maxTrackedRequests,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -742,6 +796,96 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String> _resolveGiftTargetUserIds(Msg msg) {
|
||||||
|
final targetUserIds = <String>[];
|
||||||
|
void addTargetUserId(String? userId) {
|
||||||
|
final normalizedUserId = (userId ?? "").trim();
|
||||||
|
if (normalizedUserId.isEmpty ||
|
||||||
|
targetUserIds.contains(normalizedUserId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
targetUserIds.add(normalizedUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final userId in msg.targetUserIds ?? const <String>[]) {
|
||||||
|
addTargetUserId(userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type == SCRoomMsgType.luckGiftAnimOther) {
|
||||||
|
final rawTargetUserIds = (msg.msg ?? "").trim();
|
||||||
|
if (rawTargetUserIds.startsWith("[")) {
|
||||||
|
try {
|
||||||
|
final decoded = jsonDecode(rawTargetUserIds);
|
||||||
|
if (decoded is List) {
|
||||||
|
for (final userId in decoded) {
|
||||||
|
addTargetUserId(userId?.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addTargetUserId(_resolveGiftTargetUserId(msg));
|
||||||
|
return targetUserIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> _normalizeGiftTargetUserIds(List<String> targetUserIds) {
|
||||||
|
final normalizedTargetUserIds = <String>[];
|
||||||
|
for (final targetUserId in targetUserIds) {
|
||||||
|
final normalizedTargetUserId = targetUserId.trim();
|
||||||
|
if (normalizedTargetUserId.isEmpty ||
|
||||||
|
normalizedTargetUserIds.contains(normalizedTargetUserId)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
normalizedTargetUserIds.add(normalizedTargetUserId);
|
||||||
|
}
|
||||||
|
return normalizedTargetUserIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _buildGiftTargetGroupKey(List<String> targetUserIds) {
|
||||||
|
final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds);
|
||||||
|
if (normalizedTargetUserIds.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
normalizedTargetUserIds.sort();
|
||||||
|
return normalizedTargetUserIds.join(",");
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildGiftFlightBatchKey(Msg msg, List<String> targetUserIds) {
|
||||||
|
final normalizedTargetUserIds = _normalizeGiftTargetUserIds(targetUserIds);
|
||||||
|
normalizedTargetUserIds.sort();
|
||||||
|
final timeBucket =
|
||||||
|
((msg.time ?? DateTime.now().millisecondsSinceEpoch) / 650).floor();
|
||||||
|
return [
|
||||||
|
msg.type ?? "",
|
||||||
|
msg.gift?.id ?? "",
|
||||||
|
msg.user?.id ?? "",
|
||||||
|
msg.number?.toString() ?? "",
|
||||||
|
msg.customAnimationCount?.toString() ?? "",
|
||||||
|
normalizedTargetUserIds.join(","),
|
||||||
|
timeBucket.toString(),
|
||||||
|
].join("|");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _markGiftFlightBatchForPlayback(String batchKey) {
|
||||||
|
final normalizedBatchKey = batchKey.trim();
|
||||||
|
if (normalizedBatchKey.isEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (_giftFlightBatchDedupeTimers.containsKey(normalizedBatchKey)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
late final Timer timer;
|
||||||
|
timer = Timer(_giftFlightBatchDedupeWindow, () {
|
||||||
|
if (identical(_giftFlightBatchDedupeTimers[normalizedBatchKey], timer)) {
|
||||||
|
_giftFlightBatchDedupeTimers.remove(normalizedBatchKey);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
_giftFlightBatchDedupeTimers[normalizedBatchKey] = timer;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
String _buildLuckyGiftComboSessionKey(Msg msg, String? targetUserId) {
|
String _buildLuckyGiftComboSessionKey(Msg msg, String? targetUserId) {
|
||||||
final senderId = (msg.user?.id ?? '').trim();
|
final senderId = (msg.user?.id ?? '').trim();
|
||||||
final giftId = (msg.gift?.id ?? '').trim();
|
final giftId = (msg.gift?.id ?? '').trim();
|
||||||
@ -755,6 +899,13 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
|
|||||||
_luckyGiftComboSessions.clear();
|
_luckyGiftComboSessions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _clearGiftFlightBatchDedupeTimers() {
|
||||||
|
for (final timer in _giftFlightBatchDedupeTimers.values) {
|
||||||
|
timer.cancel();
|
||||||
|
}
|
||||||
|
_giftFlightBatchDedupeTimers.clear();
|
||||||
|
}
|
||||||
|
|
||||||
bool _shouldPlaySeatFlightGiftAnimation(Msg msg) {
|
bool _shouldPlaySeatFlightGiftAnimation(Msg msg) {
|
||||||
final gift = msg.gift;
|
final gift = msg.gift;
|
||||||
if (gift == null) {
|
if (gift == null) {
|
||||||
|
|||||||
@ -67,10 +67,6 @@ double resolveRoomGameCompatibilityBottomHeight(BuildContext context) {
|
|||||||
if (mediaQuery == null) {
|
if (mediaQuery == null) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
final isClassicScreen = mediaQuery.padding.bottom <= 0.5;
|
|
||||||
if (!isClassicScreen) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (mediaQuery.size.width * 0.19).clamp(56.0, 88.0).toDouble();
|
return (mediaQuery.size.width * 0.19).clamp(56.0, 88.0).toDouble();
|
||||||
}
|
}
|
||||||
@ -97,12 +93,7 @@ class RoomGameCompatibilityBottomFrame extends StatelessWidget {
|
|||||||
class _RoomGameBottomFramePainter extends CustomPainter {
|
class _RoomGameBottomFramePainter extends CustomPainter {
|
||||||
@override
|
@override
|
||||||
void paint(Canvas canvas, Size size) {
|
void paint(Canvas canvas, Size size) {
|
||||||
final radius = Radius.circular(size.height * 0.18);
|
final frameRect = Offset.zero & size;
|
||||||
final frameRect = RRect.fromRectAndCorners(
|
|
||||||
Offset.zero & size,
|
|
||||||
topLeft: radius,
|
|
||||||
topRight: radius,
|
|
||||||
);
|
|
||||||
|
|
||||||
final backgroundPaint =
|
final backgroundPaint =
|
||||||
Paint()
|
Paint()
|
||||||
@ -110,11 +101,11 @@ class _RoomGameBottomFramePainter extends CustomPainter {
|
|||||||
begin: Alignment.topCenter,
|
begin: Alignment.topCenter,
|
||||||
end: Alignment.bottomCenter,
|
end: Alignment.bottomCenter,
|
||||||
colors: <Color>[Color(0xFF0E392E), Color(0xFF082A23)],
|
colors: <Color>[Color(0xFF0E392E), Color(0xFF082A23)],
|
||||||
).createShader(Offset.zero & size);
|
).createShader(frameRect);
|
||||||
canvas.drawRRect(frameRect, backgroundPaint);
|
canvas.drawRect(frameRect, backgroundPaint);
|
||||||
|
|
||||||
canvas.save();
|
canvas.save();
|
||||||
canvas.clipRRect(frameRect);
|
canvas.clipRect(frameRect);
|
||||||
final patternPaint =
|
final patternPaint =
|
||||||
Paint()
|
Paint()
|
||||||
..color = const Color(0xFF2C5B4A).withValues(alpha: 0.20)
|
..color = const Color(0xFF2C5B4A).withValues(alpha: 0.20)
|
||||||
@ -156,8 +147,8 @@ class _RoomGameBottomFramePainter extends CustomPainter {
|
|||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
..strokeWidth = 1;
|
..strokeWidth = 1;
|
||||||
|
|
||||||
canvas.drawRRect(frameRect.deflate(1.5), borderPaint);
|
canvas.drawRect(frameRect.deflate(1.5), borderPaint);
|
||||||
canvas.drawRRect(frameRect.deflate(5), innerBorderPaint);
|
canvas.drawRect(frameRect.deflate(5), innerBorderPaint);
|
||||||
|
|
||||||
final highlightPaint =
|
final highlightPaint =
|
||||||
Paint()
|
Paint()
|
||||||
|
|||||||
@ -86,6 +86,7 @@ class _MePage2State extends State<MePage2> {
|
|||||||
'badge=${_vipEntryBadgeLogValue(status.badge)}',
|
'badge=${_vipEntryBadgeLogValue(status.badge)}',
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
_syncCurrentProfileVipStatus(status);
|
||||||
setState(() {
|
setState(() {
|
||||||
_vipStatus = status;
|
_vipStatus = status;
|
||||||
_isVipStatusLoading = false;
|
_isVipStatusLoading = false;
|
||||||
@ -113,6 +114,25 @@ class _MePage2State extends State<MePage2> {
|
|||||||
return repository.vipStatus();
|
return repository.vipStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _syncCurrentProfileVipStatus(SCVipStatusRes status) {
|
||||||
|
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
);
|
||||||
|
final profile = profileManager.currentUserProfile;
|
||||||
|
if (profile == null) return;
|
||||||
|
|
||||||
|
final nextVipLevel =
|
||||||
|
status.active == false || status.levelInt <= 0
|
||||||
|
? '0'
|
||||||
|
: status.levelInt.toString();
|
||||||
|
if (profile.vipLevel == nextVipLevel) return;
|
||||||
|
|
||||||
|
profileManager.syncCurrentUserProfile(
|
||||||
|
profile.copyWith(vipLevel: nextVipLevel),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void _refreshAfterVipReturn(SocialChatUserProfileManager profileManager) {
|
void _refreshAfterVipReturn(SocialChatUserProfileManager profileManager) {
|
||||||
debugPrint('[VIP][MeEntry] refresh user data after vip page return');
|
debugPrint('[VIP][MeEntry] refresh user data after vip page return');
|
||||||
profileManager.fetchUserProfileData(loadGuardCount: false);
|
profileManager.fetchUserProfileData(loadGuardCount: false);
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import 'package:yumi/shared/tools/sc_permission_utils.dart';
|
|||||||
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
import 'package:yumi/shared/tools/sc_room_utils.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||||
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
|
||||||
import 'package:yumi/services/room/rc_room_manager.dart';
|
import 'package:yumi/services/room/rc_room_manager.dart';
|
||||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||||
import 'package:yumi/services/auth/user_profile_manager.dart';
|
import 'package:yumi/services/auth/user_profile_manager.dart';
|
||||||
@ -43,6 +44,7 @@ import '../../shared/business_logic/models/res/sc_is_follow_room_res.dart';
|
|||||||
import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
|
import '../../shared/business_logic/models/res/sc_room_red_packet_list_res.dart';
|
||||||
import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
import '../../shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
||||||
import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart';
|
import '../../shared/business_logic/models/res/sc_room_theme_list_res.dart';
|
||||||
|
import '../../shared/business_logic/models/res/sc_vip_res.dart';
|
||||||
import '../../shared/tools/sc_room_profile_cache.dart';
|
import '../../shared/tools/sc_room_profile_cache.dart';
|
||||||
import '../../ui_kit/components/sc_float_ichart.dart';
|
import '../../ui_kit/components/sc_float_ichart.dart';
|
||||||
|
|
||||||
@ -247,6 +249,7 @@ class RoomEntryPreviewData {
|
|||||||
),
|
),
|
||||||
roomSetting: roomSetting,
|
roomSetting: roomSetting,
|
||||||
userProfile: ownerProfile,
|
userProfile: ownerProfile,
|
||||||
|
userSVipLevel: ownerProfile?.vipLevel,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -684,6 +687,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
previous.userNickname == next.userNickname &&
|
previous.userNickname == next.userNickname &&
|
||||||
previous.userSex == next.userSex &&
|
previous.userSex == next.userSex &&
|
||||||
previous.roles == next.roles &&
|
previous.roles == next.roles &&
|
||||||
|
previous.vipLevel == next.vipLevel &&
|
||||||
previous.heartbeatVal == next.heartbeatVal;
|
previous.heartbeatVal == next.heartbeatVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -748,7 +752,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
: previousMic?.number,
|
: previousMic?.number,
|
||||||
)
|
)
|
||||||
: roomWheat;
|
: roomWheat;
|
||||||
final normalizedUserId = (mergedMic.user?.id ?? "").trim();
|
final hydratedMic = _mergeKnownVipIntoMic(mergedMic);
|
||||||
|
final normalizedUserId = (hydratedMic.user?.id ?? "").trim();
|
||||||
if (normalizedUserId.isNotEmpty) {
|
if (normalizedUserId.isNotEmpty) {
|
||||||
final existingIndex = userSeatMap[normalizedUserId];
|
final existingIndex = userSeatMap[normalizedUserId];
|
||||||
if (existingIndex != null) {
|
if (existingIndex != null) {
|
||||||
@ -765,7 +770,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
userSeatMap[normalizedUserId] = micIndex;
|
userSeatMap[normalizedUserId] = micIndex;
|
||||||
}
|
}
|
||||||
nextMap[micIndex] = mergedMic;
|
nextMap[micIndex] = hydratedMic;
|
||||||
}
|
}
|
||||||
return _stabilizeSelfMicSnapshot(nextMap, previousMap: previousMap);
|
return _stabilizeSelfMicSnapshot(nextMap, previousMap: previousMap);
|
||||||
}
|
}
|
||||||
@ -2093,7 +2098,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
unawaited(_cleanupStaleEnteredRoom(enteredRoom));
|
unawaited(_cleanupStaleEnteredRoom(enteredRoom));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
currenRoom = enteredRoom;
|
currenRoom = _mergeVoiceRoomVipHints(enteredRoom, currenRoom);
|
||||||
|
_syncCurrentUserVipHintFromRoom(currenRoom);
|
||||||
|
unawaited(_refreshCurrentUserVipHintForRoom(entryRequestSerial));
|
||||||
_currentRoomIsEntryPreview = false;
|
_currentRoomIsEntryPreview = false;
|
||||||
_previewRoomSeatCount = null;
|
_previewRoomSeatCount = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@ -2112,6 +2119,322 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JoinRoomRes _mergeVoiceRoomVipHints(
|
||||||
|
JoinRoomRes enteredRoom,
|
||||||
|
JoinRoomRes? previewRoom,
|
||||||
|
) {
|
||||||
|
final roomProfile = enteredRoom.roomProfile;
|
||||||
|
if (roomProfile == null) {
|
||||||
|
return enteredRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
final enteredOwner = roomProfile.userProfile;
|
||||||
|
if (_profileHasPositiveVipLevelHint(enteredOwner)) {
|
||||||
|
return enteredRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
final roomId = _firstNonBlankText([roomProfile.roomProfile?.id]);
|
||||||
|
final ownerId = _firstNonBlankText([
|
||||||
|
roomProfile.roomProfile?.userId,
|
||||||
|
enteredOwner?.id,
|
||||||
|
]);
|
||||||
|
final previewRoomProfile = previewRoom?.roomProfile;
|
||||||
|
final previewOwner = previewRoomProfile?.userProfile;
|
||||||
|
final previewRoomId = _firstNonBlankText([
|
||||||
|
previewRoomProfile?.roomProfile?.id,
|
||||||
|
]);
|
||||||
|
final previewOwnerId = _firstNonBlankText([
|
||||||
|
previewRoomProfile?.roomProfile?.userId,
|
||||||
|
previewOwner?.id,
|
||||||
|
]);
|
||||||
|
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||||
|
|
||||||
|
SocialChatUserProfile? resolvedOwner = enteredOwner;
|
||||||
|
String? vipLevel;
|
||||||
|
|
||||||
|
final canUsePreviewOwner =
|
||||||
|
previewOwner != null &&
|
||||||
|
_sameNonBlankId(roomId, previewRoomId) &&
|
||||||
|
(ownerId == null || _sameNonBlankId(ownerId, previewOwnerId));
|
||||||
|
if (canUsePreviewOwner) {
|
||||||
|
vipLevel = _profileVipLevelHint(previewOwner);
|
||||||
|
resolvedOwner ??= previewOwner;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vipLevel == null && _sameNonBlankId(ownerId, currentProfile?.id)) {
|
||||||
|
vipLevel =
|
||||||
|
_profileVipLevelHint(currentProfile) ??
|
||||||
|
_roomVipLevelHintFromEntrants(enteredRoom.entrants);
|
||||||
|
resolvedOwner ??= currentProfile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vipLevel == null || resolvedOwner == null) {
|
||||||
|
return enteredRoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
return enteredRoom.copyWith(
|
||||||
|
roomProfile: roomProfile.copyWith(
|
||||||
|
userProfile: resolvedOwner.copyWith(vipLevel: vipLevel),
|
||||||
|
userSVipLevel: vipLevel,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _syncCurrentUserVipHintFromRoom(JoinRoomRes? room) {
|
||||||
|
final currentUser = AccountStorage().getCurrentUser();
|
||||||
|
final currentProfile = currentUser?.userProfile;
|
||||||
|
if (currentUser == null ||
|
||||||
|
currentProfile == null ||
|
||||||
|
_profileHasPositiveVipLevelHint(currentProfile)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ownerProfile = room?.roomProfile?.userProfile;
|
||||||
|
final ownerId = _firstNonBlankText([
|
||||||
|
room?.roomProfile?.roomProfile?.userId,
|
||||||
|
ownerProfile?.id,
|
||||||
|
]);
|
||||||
|
String? vipLevel;
|
||||||
|
if (_sameNonBlankId(ownerId, currentProfile.id)) {
|
||||||
|
vipLevel = _profileVipLevelHint(ownerProfile);
|
||||||
|
}
|
||||||
|
vipLevel ??= _roomVipLevelHintFromEntrants(room?.entrants);
|
||||||
|
if (vipLevel == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccountStorage().setCurrentUser(
|
||||||
|
currentUser.copyWith(
|
||||||
|
userProfile: currentProfile.copyWith(vipLevel: vipLevel),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _refreshCurrentUserVipHintForRoom(int entryRequestSerial) async {
|
||||||
|
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
|
||||||
|
if (roomId.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentUser = AccountStorage().getCurrentUser();
|
||||||
|
final currentProfile = currentUser?.userProfile;
|
||||||
|
if (currentUser == null || currentProfile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final existingVipLevel = _positiveProfileVipLevelHint(currentProfile);
|
||||||
|
final vipLevel =
|
||||||
|
existingVipLevel ?? await _loadCurrentVipLevelForColoredId();
|
||||||
|
if (vipLevel == null || vipLevel.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final updatedProfile = currentProfile.copyWith(vipLevel: vipLevel);
|
||||||
|
if (!_profileHasPositiveVipLevelHint(currentProfile)) {
|
||||||
|
AccountStorage().setCurrentUser(
|
||||||
|
currentUser.copyWith(userProfile: updatedProfile),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var changed = false;
|
||||||
|
final mergedRoom = _mergeKnownVipIntoCurrentRoom(
|
||||||
|
currenRoom,
|
||||||
|
updatedProfile,
|
||||||
|
);
|
||||||
|
if (!identical(mergedRoom, currenRoom)) {
|
||||||
|
currenRoom = mergedRoom;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final mergedOnlineUsers =
|
||||||
|
onlineUsers
|
||||||
|
.map((user) => _mergeKnownVipIntoProfile(user) ?? user)
|
||||||
|
.toList();
|
||||||
|
if (!_sameOnlineUsers(onlineUsers, mergedOnlineUsers)) {
|
||||||
|
onlineUsers = mergedOnlineUsers;
|
||||||
|
_refreshManagerUsers(onlineUsers);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final mergedMicMap = roomWheatMap.map(
|
||||||
|
(index, mic) => MapEntry(index, _mergeKnownVipIntoMic(mic)),
|
||||||
|
);
|
||||||
|
if (!_sameMicMaps(roomWheatMap, mergedMicMap)) {
|
||||||
|
roomWheatMap = mergedMicMap;
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changed) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _loadCurrentVipLevelForColoredId() async {
|
||||||
|
final repository = SCVipRepositoryImp();
|
||||||
|
try {
|
||||||
|
final home = await repository.vipHome();
|
||||||
|
final homeVipLevel = _vipLevelFromStatus(home.state);
|
||||||
|
if (homeVipLevel != null) {
|
||||||
|
return homeVipLevel;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return _vipLevelFromStatus(await repository.vipStatus());
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _vipLevelFromStatus(SCVipStatusRes? status) {
|
||||||
|
if (status == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (status.active == false) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
final level = status.levelInt;
|
||||||
|
return level > 0 ? level.toString() : '0';
|
||||||
|
}
|
||||||
|
|
||||||
|
JoinRoomRes? _mergeKnownVipIntoCurrentRoom(
|
||||||
|
JoinRoomRes? room,
|
||||||
|
SocialChatUserProfile currentProfile,
|
||||||
|
) {
|
||||||
|
final roomProfile = room?.roomProfile;
|
||||||
|
final ownerProfile = roomProfile?.userProfile;
|
||||||
|
if (room == null || roomProfile == null || ownerProfile == null) {
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ownerId = _firstNonBlankText([
|
||||||
|
roomProfile.roomProfile?.userId,
|
||||||
|
ownerProfile.id,
|
||||||
|
]);
|
||||||
|
if (!_sameNonBlankId(ownerId, currentProfile.id) ||
|
||||||
|
_profileHasPositiveVipLevelHint(ownerProfile)) {
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
final vipLevel = _positiveProfileVipLevelHint(currentProfile);
|
||||||
|
if (vipLevel == null) {
|
||||||
|
return room;
|
||||||
|
}
|
||||||
|
|
||||||
|
return room.copyWith(
|
||||||
|
roomProfile: roomProfile.copyWith(
|
||||||
|
userProfile: ownerProfile.copyWith(vipLevel: vipLevel),
|
||||||
|
userSVipLevel: vipLevel,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
MicRes _mergeKnownVipIntoMic(MicRes mic) {
|
||||||
|
final mergedUser = _mergeKnownVipIntoProfile(mic.user);
|
||||||
|
if (identical(mergedUser, mic.user)) {
|
||||||
|
return mic;
|
||||||
|
}
|
||||||
|
return mic.copyWith(user: mergedUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
SocialChatUserProfile? _mergeKnownVipIntoProfile(
|
||||||
|
SocialChatUserProfile? profile,
|
||||||
|
) {
|
||||||
|
if (profile == null || _profileHasPositiveVipLevelHint(profile)) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
|
||||||
|
final currentVipLevel = _positiveProfileVipLevelHint(currentProfile);
|
||||||
|
if (_sameNonBlankId(profile.id, currentProfile?.id) &&
|
||||||
|
currentVipLevel != null) {
|
||||||
|
return profile.copyWith(vipLevel: currentVipLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ownerProfile = currenRoom?.roomProfile?.userProfile;
|
||||||
|
final ownerVipLevel = _positiveProfileVipLevelHint(ownerProfile);
|
||||||
|
final ownerId = _firstNonBlankText([
|
||||||
|
currenRoom?.roomProfile?.roomProfile?.userId,
|
||||||
|
ownerProfile?.id,
|
||||||
|
]);
|
||||||
|
if (_sameNonBlankId(profile.id, ownerId) && ownerVipLevel != null) {
|
||||||
|
return profile.copyWith(vipLevel: ownerVipLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _profileHasPositiveVipLevelHint(SocialChatUserProfile? profile) {
|
||||||
|
return _positiveProfileVipLevelHint(profile) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _profileVipLevelHint(SocialChatUserProfile? profile) {
|
||||||
|
return _firstNonBlankText([profile?.vipLevel]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _positiveProfileVipLevelHint(SocialChatUserProfile? profile) {
|
||||||
|
final vipLevel = _profileVipLevelHint(profile);
|
||||||
|
if (profile == null || profile.vipLevelForColoredId <= 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return vipLevel ?? profile.vipLevelForColoredId.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _roomVipLevelHintFromEntrants(Entrants? entrants) {
|
||||||
|
final abilities = entrants?.nobleVipAbility ?? const <String>[];
|
||||||
|
var hasAbility = false;
|
||||||
|
for (final ability in abilities) {
|
||||||
|
final text = ability.trim();
|
||||||
|
final normalized = text.toLowerCase();
|
||||||
|
if (text.isEmpty || normalized == '0' || normalized == 'false') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
hasAbility = true;
|
||||||
|
if (_looksLikeVipLevelText(text)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasAbility ? '1' : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _looksLikeVipLevelText(String text) {
|
||||||
|
final plainLevel = int.tryParse(text);
|
||||||
|
if ((plainLevel ?? 0) > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (RegExp(
|
||||||
|
r'(?:S?VIP|NOBLE[_\s-]*VIP)[^\d]*[1-9]\d*',
|
||||||
|
caseSensitive: false,
|
||||||
|
).hasMatch(text)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return RegExp(r'[1-9]\d*$').hasMatch(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _firstNonBlankText(Iterable<dynamic> values) {
|
||||||
|
for (final value in values) {
|
||||||
|
final text = value?.toString().trim();
|
||||||
|
if (text != null && text.isNotEmpty) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _sameNonBlankId(dynamic left, dynamic right) {
|
||||||
|
final leftText = left?.toString().trim();
|
||||||
|
final rightText = right?.toString().trim();
|
||||||
|
return leftText != null &&
|
||||||
|
leftText.isNotEmpty &&
|
||||||
|
rightText != null &&
|
||||||
|
rightText.isNotEmpty &&
|
||||||
|
leftText == rightText;
|
||||||
|
}
|
||||||
|
|
||||||
bool _isActiveRoomEntryRequest(int entryRequestSerial) {
|
bool _isActiveRoomEntryRequest(int entryRequestSerial) {
|
||||||
return entryRequestSerial == _roomEntryRequestSerial &&
|
return entryRequestSerial == _roomEntryRequestSerial &&
|
||||||
_roomStartupStatus == RoomStartupStatus.loading;
|
_roomStartupStatus == RoomStartupStatus.loading;
|
||||||
@ -2355,7 +2678,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
|
!_isCurrentRoomEntrySession(entryRequestSerial, roomId)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
SCGiftVapSvgaManager().play(sourceUrl, priority: 100, type: 1);
|
SCGiftVapSvgaManager().play(
|
||||||
|
sourceUrl,
|
||||||
|
priority: SCGiftVapSvgaManager.entryEffectPriority,
|
||||||
|
type: SCGiftVapSvgaManager.entryEffectType,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2405,8 +2732,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
if (roomId != currenRoom?.roomProfile?.roomProfile?.id) {
|
if (roomId != currenRoom?.roomProfile?.roomProfile?.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final changed = !_sameOnlineUsers(onlineUsers, fetchedUsers);
|
final mergedUsers =
|
||||||
onlineUsers = fetchedUsers;
|
fetchedUsers.map((user) => _mergeKnownVipIntoProfile(user)!).toList();
|
||||||
|
final changed = !_sameOnlineUsers(onlineUsers, mergedUsers);
|
||||||
|
onlineUsers = mergedUsers;
|
||||||
_refreshManagerUsers(onlineUsers);
|
_refreshManagerUsers(onlineUsers);
|
||||||
if (changed || notifyIfUnchanged) {
|
if (changed || notifyIfUnchanged) {
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@ -3399,15 +3728,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
|
|||||||
if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) {
|
if (groupId != currenRoom?.roomProfile?.roomProfile?.roomAccount) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final mergedUser = _mergeKnownVipIntoProfile(user) ?? user;
|
||||||
bool isExtOnlineList = false;
|
bool isExtOnlineList = false;
|
||||||
for (var us in onlineUsers) {
|
for (var us in onlineUsers) {
|
||||||
if (us.id == user.id) {
|
if (us.id == mergedUser.id) {
|
||||||
isExtOnlineList = true;
|
isExtOnlineList = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!isExtOnlineList) {
|
if (!isExtOnlineList) {
|
||||||
Provider.of<RtcProvider>(context!, listen: false).onlineUsers.add(user);
|
Provider.of<RtcProvider>(
|
||||||
|
context!,
|
||||||
|
listen: false,
|
||||||
|
).onlineUsers.add(mergedUser);
|
||||||
_refreshManagerUsers(onlineUsers);
|
_refreshManagerUsers(onlineUsers);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1517,8 +1517,8 @@ class RealTimeMessagingManager extends ChangeNotifier {
|
|||||||
shouldShowRoomVisualEffects) {
|
shouldShowRoomVisualEffects) {
|
||||||
SCGiftVapSvgaManager().play(
|
SCGiftVapSvgaManager().play(
|
||||||
msg.user?.getMountains()?.sourceUrl ?? "",
|
msg.user?.getMountains()?.sourceUrl ?? "",
|
||||||
priority: 100,
|
priority: SCGiftVapSvgaManager.entryEffectPriority,
|
||||||
type: 1,
|
type: SCGiftVapSvgaManager.entryEffectType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,11 +4,13 @@ import 'package:yumi/ui_kit/components/sc_tts.dart';
|
|||||||
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart';
|
||||||
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
|
||||||
|
import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/req/sc_user_profile_cmd.dart';
|
import 'package:yumi/shared/business_logic/models/req/sc_user_profile_cmd.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/country_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/country_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart';
|
||||||
import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart';
|
import 'package:yumi/shared/business_logic/models/res/sc_user_identity_res.dart';
|
||||||
|
import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart';
|
||||||
|
|
||||||
class SocialChatUserProfileManager extends ChangeNotifier {
|
class SocialChatUserProfileManager extends ChangeNotifier {
|
||||||
SCUserProfileCmd? editUser;
|
SCUserProfileCmd? editUser;
|
||||||
@ -86,6 +88,7 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var userInfo = await SCAccountRepository().loadUserInfo(userId);
|
var userInfo = await SCAccountRepository().loadUserInfo(userId);
|
||||||
|
userInfo = await _withCurrentVipLevel(userInfo, userId);
|
||||||
syncCurrentUserProfile(userInfo);
|
syncCurrentUserProfile(userInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +104,8 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
|||||||
|
|
||||||
void getUserInfoById(String userId) async {
|
void getUserInfoById(String userId) async {
|
||||||
userProfile = null;
|
userProfile = null;
|
||||||
userProfile = await SCAccountRepository().loadUserInfo(userId);
|
final loadedProfile = await SCAccountRepository().loadUserInfo(userId);
|
||||||
|
userProfile = await _withCurrentVipLevel(loadedProfile, userId);
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@ -114,9 +118,85 @@ class SocialChatUserProfileManager extends ChangeNotifier {
|
|||||||
void roomUserCard(String roomId, String userId) async {
|
void roomUserCard(String roomId, String userId) async {
|
||||||
userCardInfo = null;
|
userCardInfo = null;
|
||||||
userCardInfo = await SCChatRoomRepository().roomUserCard(roomId, userId);
|
userCardInfo = await SCChatRoomRepository().roomUserCard(roomId, userId);
|
||||||
|
userCardInfo = await _withCurrentVipLevelForCard(userCardInfo, userId);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<RoomUserCardRes?> _withCurrentVipLevelForCard(
|
||||||
|
RoomUserCardRes? card,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
|
final profile = card?.userProfile;
|
||||||
|
if (card == null || profile == null) {
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
final mergedProfile = await _withCurrentVipLevel(profile, userId);
|
||||||
|
if (mergedProfile == profile) {
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
return card.copyWith(userProfile: mergedProfile);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<SocialChatUserProfile> _withCurrentVipLevel(
|
||||||
|
SocialChatUserProfile profile,
|
||||||
|
String userId,
|
||||||
|
) async {
|
||||||
|
if (!_isCurrentUserId(userId)) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
final profileVipLevel = profile.vipLevel?.trim();
|
||||||
|
if (profileVipLevel != null && profileVipLevel.isNotEmpty) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
final currentVipLevel = currentUserProfile?.vipLevel?.trim();
|
||||||
|
final resolvedVipLevel =
|
||||||
|
currentVipLevel != null && currentVipLevel.isNotEmpty
|
||||||
|
? currentVipLevel
|
||||||
|
: await _loadCurrentVipLevel();
|
||||||
|
if (resolvedVipLevel == null || resolvedVipLevel.isEmpty) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return profile.copyWith(vipLevel: resolvedVipLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isCurrentUserId(String userId) {
|
||||||
|
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
|
||||||
|
return currentUserId != null &&
|
||||||
|
currentUserId.isNotEmpty &&
|
||||||
|
currentUserId == userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String?> _loadCurrentVipLevel() async {
|
||||||
|
final repository = SCVipRepositoryImp();
|
||||||
|
try {
|
||||||
|
final home = await repository.vipHome();
|
||||||
|
final homeVipLevel = _vipLevelFromStatus(home.state);
|
||||||
|
if (homeVipLevel != null) {
|
||||||
|
return homeVipLevel;
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return _vipLevelFromStatus(await repository.vipStatus());
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _vipLevelFromStatus(SCVipStatusRes? status) {
|
||||||
|
if (status == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (status.active == false) {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
final level = status.levelInt;
|
||||||
|
return level > 0 ? level.toString() : '0';
|
||||||
|
}
|
||||||
|
|
||||||
void followUser(String userId) async {
|
void followUser(String userId) async {
|
||||||
var result = await SCAccountRepository().followUser(userId);
|
var result = await SCAccountRepository().followUser(userId);
|
||||||
if (result) {
|
if (result) {
|
||||||
|
|||||||
@ -53,8 +53,8 @@ class FollowRoomRes {
|
|||||||
/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"s","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}
|
/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"s","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}
|
||||||
/// userSVipLevel : ""
|
/// userSVipLevel : ""
|
||||||
|
|
||||||
class RoomProfile {
|
class RoomProfile {
|
||||||
RoomProfile({
|
RoomProfile({
|
||||||
String? countryCode,
|
String? countryCode,
|
||||||
String? countryName,
|
String? countryName,
|
||||||
String? event,
|
String? event,
|
||||||
@ -69,12 +69,12 @@ class RoomProfile {
|
|||||||
String? roomGameIcon,
|
String? roomGameIcon,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
String? sysOrigin,
|
String? sysOrigin,
|
||||||
String? userId,
|
String? userId,
|
||||||
SocialChatUserProfile? userProfile,
|
SocialChatUserProfile? userProfile,
|
||||||
String? userSVipLevel,
|
String? userSVipLevel,
|
||||||
RoomMemberCounter? roomCounter,
|
RoomMemberCounter? roomCounter,
|
||||||
ExtValues? extValues,
|
ExtValues? extValues,
|
||||||
}) {
|
}) {
|
||||||
_countryCode = countryCode;
|
_countryCode = countryCode;
|
||||||
_countryName = countryName;
|
_countryName = countryName;
|
||||||
_event = event;
|
_event = event;
|
||||||
@ -89,12 +89,16 @@ class RoomProfile {
|
|||||||
_roomGameIcon = roomGameIcon;
|
_roomGameIcon = roomGameIcon;
|
||||||
_roomName = roomName;
|
_roomName = roomName;
|
||||||
_sysOrigin = sysOrigin;
|
_sysOrigin = sysOrigin;
|
||||||
_userId = userId;
|
_userId = userId;
|
||||||
_userProfile = userProfile;
|
_userSVipLevel = userSVipLevel;
|
||||||
_userSVipLevel = userSVipLevel;
|
final normalizedVipLevel = userSVipLevel?.trim();
|
||||||
_roomCounter = roomCounter;
|
_userProfile =
|
||||||
_extValues = extValues;
|
normalizedVipLevel == null || normalizedVipLevel.isEmpty
|
||||||
}
|
? userProfile
|
||||||
|
: userProfile?.copyWith(vipLevel: normalizedVipLevel);
|
||||||
|
_roomCounter = roomCounter;
|
||||||
|
_extValues = extValues;
|
||||||
|
}
|
||||||
|
|
||||||
RoomProfile.fromJson(dynamic json) {
|
RoomProfile.fromJson(dynamic json) {
|
||||||
_countryCode = json['countryCode'];
|
_countryCode = json['countryCode'];
|
||||||
@ -115,18 +119,22 @@ class RoomProfile {
|
|||||||
_roomName = json['roomName'];
|
_roomName = json['roomName'];
|
||||||
_sysOrigin = json['sysOrigin'];
|
_sysOrigin = json['sysOrigin'];
|
||||||
_userId = json['userId'];
|
_userId = json['userId'];
|
||||||
_userProfile =
|
_userProfile =
|
||||||
json['userProfile'] != null
|
json['userProfile'] != null
|
||||||
? SocialChatUserProfile.fromJson(json['userProfile'])
|
? SocialChatUserProfile.fromJson(json['userProfile'])
|
||||||
: null;
|
: null;
|
||||||
_userSVipLevel = json['userSVipLevel'];
|
_userSVipLevel = socialChatVipLevelHintFromJson(json);
|
||||||
final counterJson = json['roomCounter'] ?? json['counter'];
|
final normalizedVipLevel = _userSVipLevel?.trim();
|
||||||
_roomCounter =
|
if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) {
|
||||||
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
|
_userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel);
|
||||||
_extValues =
|
}
|
||||||
json['extValues'] != null
|
final counterJson = json['roomCounter'] ?? json['counter'];
|
||||||
? ExtValues.fromJson(json['extValues'])
|
_roomCounter =
|
||||||
: null;
|
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
|
||||||
|
_extValues =
|
||||||
|
json['extValues'] != null
|
||||||
|
? ExtValues.fromJson(json['extValues'])
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
String? _countryCode;
|
String? _countryCode;
|
||||||
String? _countryName;
|
String? _countryName;
|
||||||
@ -142,11 +150,11 @@ class RoomProfile {
|
|||||||
String? _roomGameIcon;
|
String? _roomGameIcon;
|
||||||
String? _roomName;
|
String? _roomName;
|
||||||
String? _sysOrigin;
|
String? _sysOrigin;
|
||||||
String? _userId;
|
String? _userId;
|
||||||
SocialChatUserProfile? _userProfile;
|
SocialChatUserProfile? _userProfile;
|
||||||
String? _userSVipLevel;
|
String? _userSVipLevel;
|
||||||
RoomMemberCounter? _roomCounter;
|
RoomMemberCounter? _roomCounter;
|
||||||
ExtValues? _extValues;
|
ExtValues? _extValues;
|
||||||
RoomProfile copyWith({
|
RoomProfile copyWith({
|
||||||
String? countryCode,
|
String? countryCode,
|
||||||
String? countryName,
|
String? countryName,
|
||||||
@ -162,12 +170,12 @@ class RoomProfile {
|
|||||||
String? roomGameIcon,
|
String? roomGameIcon,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
String? sysOrigin,
|
String? sysOrigin,
|
||||||
String? userId,
|
String? userId,
|
||||||
SocialChatUserProfile? userProfile,
|
SocialChatUserProfile? userProfile,
|
||||||
String? userSVipLevel,
|
String? userSVipLevel,
|
||||||
RoomMemberCounter? roomCounter,
|
RoomMemberCounter? roomCounter,
|
||||||
ExtValues? extValues,
|
ExtValues? extValues,
|
||||||
}) => RoomProfile(
|
}) => RoomProfile(
|
||||||
countryCode: countryCode ?? _countryCode,
|
countryCode: countryCode ?? _countryCode,
|
||||||
countryName: countryName ?? _countryName,
|
countryName: countryName ?? _countryName,
|
||||||
event: event ?? _event,
|
event: event ?? _event,
|
||||||
@ -182,12 +190,12 @@ class RoomProfile {
|
|||||||
roomGameIcon: roomGameIcon ?? _roomGameIcon,
|
roomGameIcon: roomGameIcon ?? _roomGameIcon,
|
||||||
roomName: roomName ?? _roomName,
|
roomName: roomName ?? _roomName,
|
||||||
sysOrigin: sysOrigin ?? _sysOrigin,
|
sysOrigin: sysOrigin ?? _sysOrigin,
|
||||||
userId: userId ?? _userId,
|
userId: userId ?? _userId,
|
||||||
userProfile: userProfile ?? _userProfile,
|
userProfile: userProfile ?? _userProfile,
|
||||||
userSVipLevel: userSVipLevel ?? _userSVipLevel,
|
userSVipLevel: userSVipLevel ?? _userSVipLevel,
|
||||||
roomCounter: roomCounter ?? _roomCounter,
|
roomCounter: roomCounter ?? _roomCounter,
|
||||||
extValues: extValues ?? _extValues,
|
extValues: extValues ?? _extValues,
|
||||||
);
|
);
|
||||||
String? get countryCode => _countryCode;
|
String? get countryCode => _countryCode;
|
||||||
String? get countryName => _countryName;
|
String? get countryName => _countryName;
|
||||||
String? get event => _event;
|
String? get event => _event;
|
||||||
@ -203,25 +211,25 @@ class RoomProfile {
|
|||||||
String? get roomName => _roomName;
|
String? get roomName => _roomName;
|
||||||
String? get sysOrigin => _sysOrigin;
|
String? get sysOrigin => _sysOrigin;
|
||||||
String? get userId => _userId;
|
String? get userId => _userId;
|
||||||
SocialChatUserProfile? get userProfile => _userProfile;
|
SocialChatUserProfile? get userProfile => _userProfile;
|
||||||
String? get userSVipLevel => _userSVipLevel;
|
String? get userSVipLevel => _userSVipLevel;
|
||||||
RoomMemberCounter? get roomCounter => _roomCounter;
|
RoomMemberCounter? get roomCounter => _roomCounter;
|
||||||
ExtValues? get extValues => _extValues;
|
ExtValues? get extValues => _extValues;
|
||||||
|
|
||||||
String get displayMemberCount {
|
String get displayMemberCount {
|
||||||
final memberCount = roomCounter?.memberCount;
|
final memberCount = roomCounter?.memberCount;
|
||||||
if (memberCount != null) {
|
if (memberCount != null) {
|
||||||
if (memberCount == memberCount.roundToDouble()) {
|
if (memberCount == memberCount.roundToDouble()) {
|
||||||
return memberCount.toInt().toString();
|
return memberCount.toInt().toString();
|
||||||
}
|
}
|
||||||
return memberCount.toString();
|
return memberCount.toString();
|
||||||
}
|
}
|
||||||
final memberQuantity = extValues?.memberQuantity;
|
final memberQuantity = extValues?.memberQuantity;
|
||||||
if ((memberQuantity ?? "").trim().isNotEmpty) {
|
if ((memberQuantity ?? "").trim().isNotEmpty) {
|
||||||
return memberQuantity!;
|
return memberQuantity!;
|
||||||
}
|
}
|
||||||
return "0";
|
return "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final map = <String, dynamic>{};
|
final map = <String, dynamic>{};
|
||||||
@ -240,16 +248,16 @@ class RoomProfile {
|
|||||||
map['roomName'] = _roomName;
|
map['roomName'] = _roomName;
|
||||||
map['sysOrigin'] = _sysOrigin;
|
map['sysOrigin'] = _sysOrigin;
|
||||||
map['userId'] = _userId;
|
map['userId'] = _userId;
|
||||||
if (_userProfile != null) {
|
if (_userProfile != null) {
|
||||||
map['userProfile'] = _userProfile?.toJson();
|
map['userProfile'] = _userProfile?.toJson();
|
||||||
}
|
}
|
||||||
map['userSVipLevel'] = _userSVipLevel;
|
map['userSVipLevel'] = _userSVipLevel;
|
||||||
if (_roomCounter != null) {
|
if (_roomCounter != null) {
|
||||||
map['roomCounter'] = _roomCounter?.toJson();
|
map['roomCounter'] = _roomCounter?.toJson();
|
||||||
}
|
}
|
||||||
if (_extValues != null) {
|
if (_extValues != null) {
|
||||||
map['extValues'] = _extValues?.toJson();
|
map['extValues'] = _extValues?.toJson();
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,17 +17,31 @@ class JoinRoomRes {
|
|||||||
_roomProps = roomProps;
|
_roomProps = roomProps;
|
||||||
}
|
}
|
||||||
|
|
||||||
JoinRoomRes.fromJson(dynamic json) {
|
JoinRoomRes.fromJson(dynamic json) {
|
||||||
_entrants =
|
_entrants =
|
||||||
json['entrants'] != null ? Entrants.fromJson(json['entrants']) : null;
|
json['entrants'] != null ? Entrants.fromJson(json['entrants']) : null;
|
||||||
_roomProfile =
|
_roomProfile =
|
||||||
json['roomProfile'] != null
|
json['roomProfile'] != null
|
||||||
? RoomProfile.fromJson(json['roomProfile'])
|
? RoomProfile.fromJson(json['roomProfile'])
|
||||||
: null;
|
: null;
|
||||||
_roomProps =
|
final roomVipLevel = _firstNonBlankRoomVipLevel([
|
||||||
json['roomProps'] != null
|
json['userSVipLevel'],
|
||||||
? RoomProps.fromJson(json['roomProps'])
|
json['supperVip'],
|
||||||
: null;
|
json['superVip'],
|
||||||
|
json['vipLevel'],
|
||||||
|
json['svipLevel'],
|
||||||
|
json['sVipLevel'],
|
||||||
|
json['nobleVipLevel'],
|
||||||
|
json['vipLevelCode'],
|
||||||
|
json['levelCode'],
|
||||||
|
]);
|
||||||
|
if (roomVipLevel != null) {
|
||||||
|
_roomProfile = _roomProfile?.copyWith(userSVipLevel: roomVipLevel);
|
||||||
|
}
|
||||||
|
_roomProps =
|
||||||
|
json['roomProps'] != null
|
||||||
|
? RoomProps.fromJson(json['roomProps'])
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Entrants? _entrants;
|
Entrants? _entrants;
|
||||||
@ -62,13 +76,23 @@ class JoinRoomRes {
|
|||||||
map['roomProps'] = _roomProps?.toJson();
|
map['roomProps'] = _roomProps?.toJson();
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// layoutCode : ""
|
String? _firstNonBlankRoomVipLevel(Iterable<dynamic> values) {
|
||||||
/// roomTheme : {"expireTime":0,"id":0,"themeBack":"","themeStatus":"","useTheme":false}
|
for (final value in values) {
|
||||||
|
final text = value?.toString().trim();
|
||||||
class RoomProps {
|
if (text != null && text.isNotEmpty) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// layoutCode : ""
|
||||||
|
/// roomTheme : {"expireTime":0,"id":0,"themeBack":"","themeStatus":"","useTheme":false}
|
||||||
|
|
||||||
|
class RoomProps {
|
||||||
RoomProps({String? layoutCode, RoomTheme? roomTheme}) {
|
RoomProps({String? layoutCode, RoomTheme? roomTheme}) {
|
||||||
_layoutCode = layoutCode;
|
_layoutCode = layoutCode;
|
||||||
_roomTheme = roomTheme;
|
_roomTheme = roomTheme;
|
||||||
@ -181,26 +205,33 @@ class RoomTheme {
|
|||||||
/// regionCode : ""
|
/// regionCode : ""
|
||||||
/// roomCounter : {"adminCount":0,"memberCount":0}
|
/// roomCounter : {"adminCount":0,"memberCount":0}
|
||||||
/// roomProfile : {"activeTime":0,"countryCode":"","countryName":"","createTime":0,"del":false,"event":"","id":"0","langCode":"","nationalFlag":"","roomAccount":"","roomCover":"","roomDesc":"","roomName":"","sysOrigin":"","updateTime":0,"userId":"0"}
|
/// roomProfile : {"activeTime":0,"countryCode":"","countryName":"","createTime":0,"del":false,"event":"","id":"0","langCode":"","nationalFlag":"","roomAccount":"","roomCover":"","roomDesc":"","roomName":"","sysOrigin":"","updateTime":0,"userId":"0"}
|
||||||
/// roomSetting : {"adminLockSeat":false,"allowMusic":false,"joinGolds":0,"lockLocation":"","maxAdmin":0,"maxMember":0,"mikeSize":0,"openKtvMode":false,"password":"","roomId":0,"roomSpecialMikeType":"","showHeartbeat":false,"takeMicRole":"","touristMike":false,"touristMsg":false,"userId":"0"}
|
/// roomSetting : {"adminLockSeat":false,"allowMusic":false,"joinGolds":0,"lockLocation":"","maxAdmin":0,"maxMember":0,"mikeSize":0,"openKtvMode":false,"password":"","roomId":0,"roomSpecialMikeType":"","showHeartbeat":false,"takeMicRole":"","touristMike":false,"touristMsg":false,"userId":"0"}
|
||||||
/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}
|
/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}
|
||||||
|
/// userSVipLevel : ""
|
||||||
class RoomProfile {
|
|
||||||
RoomProfile({
|
class RoomProfile {
|
||||||
String? regionCode,
|
RoomProfile({
|
||||||
RoomCounter? roomCounter,
|
String? regionCode,
|
||||||
RoomProfile2? roomProfile,
|
RoomCounter? roomCounter,
|
||||||
RoomSetting? roomSetting,
|
RoomProfile2? roomProfile,
|
||||||
SocialChatUserProfile? userProfile,
|
RoomSetting? roomSetting,
|
||||||
}) {
|
SocialChatUserProfile? userProfile,
|
||||||
_regionCode = regionCode;
|
String? userSVipLevel,
|
||||||
_roomCounter = roomCounter;
|
}) {
|
||||||
_roomProfile = roomProfile;
|
_regionCode = regionCode;
|
||||||
_roomSetting = roomSetting;
|
_roomCounter = roomCounter;
|
||||||
_userProfile = userProfile;
|
_roomProfile = roomProfile;
|
||||||
}
|
_roomSetting = roomSetting;
|
||||||
|
_userSVipLevel = userSVipLevel;
|
||||||
RoomProfile.fromJson(dynamic json) {
|
final normalizedVipLevel = userSVipLevel?.trim();
|
||||||
_regionCode = json['regionCode'];
|
_userProfile =
|
||||||
|
normalizedVipLevel == null || normalizedVipLevel.isEmpty
|
||||||
|
? userProfile
|
||||||
|
: userProfile?.copyWith(vipLevel: normalizedVipLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
RoomProfile.fromJson(dynamic json) {
|
||||||
|
_regionCode = json['regionCode'];
|
||||||
_roomCounter =
|
_roomCounter =
|
||||||
json['roomCounter'] != null
|
json['roomCounter'] != null
|
||||||
? RoomCounter.fromJson(json['roomCounter'])
|
? RoomCounter.fromJson(json['roomCounter'])
|
||||||
@ -213,31 +244,52 @@ class RoomProfile {
|
|||||||
json['roomSetting'] != null
|
json['roomSetting'] != null
|
||||||
? RoomSetting.fromJson(json['roomSetting'])
|
? RoomSetting.fromJson(json['roomSetting'])
|
||||||
: null;
|
: null;
|
||||||
_userProfile =
|
_userProfile =
|
||||||
json['userProfile'] != null
|
json['userProfile'] != null
|
||||||
? SocialChatUserProfile.fromJson(json['userProfile'])
|
? SocialChatUserProfile.fromJson(json['userProfile'])
|
||||||
: null;
|
: null;
|
||||||
}
|
_userSVipLevel = _firstNonBlankRoomVipLevel([
|
||||||
|
json['userSVipLevel'],
|
||||||
String? _regionCode;
|
json['supperVip'],
|
||||||
RoomCounter? _roomCounter;
|
json['superVip'],
|
||||||
RoomProfile2? _roomProfile;
|
json['vipLevel'],
|
||||||
RoomSetting? _roomSetting;
|
json['svipLevel'],
|
||||||
SocialChatUserProfile? _userProfile;
|
json['sVipLevel'],
|
||||||
|
json['nobleVipLevel'],
|
||||||
RoomProfile copyWith({
|
json['vipLevelCode'],
|
||||||
String? regionCode,
|
json['levelCode'],
|
||||||
RoomCounter? roomCounter,
|
json['roomProfile'] is Map ? json['roomProfile']['userSVipLevel'] : null,
|
||||||
RoomProfile2? roomProfile,
|
json['roomProfile'] is Map ? json['roomProfile']['supperVip'] : null,
|
||||||
RoomSetting? roomSetting,
|
json['roomProfile'] is Map ? json['roomProfile']['vipLevel'] : null,
|
||||||
SocialChatUserProfile? userProfile,
|
]);
|
||||||
}) => RoomProfile(
|
final normalizedVipLevel = _userSVipLevel?.trim();
|
||||||
regionCode: regionCode ?? _regionCode,
|
if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) {
|
||||||
roomCounter: roomCounter ?? _roomCounter,
|
_userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel);
|
||||||
roomProfile: roomProfile ?? _roomProfile,
|
}
|
||||||
roomSetting: roomSetting ?? _roomSetting,
|
}
|
||||||
userProfile: userProfile ?? _userProfile,
|
|
||||||
);
|
String? _regionCode;
|
||||||
|
RoomCounter? _roomCounter;
|
||||||
|
RoomProfile2? _roomProfile;
|
||||||
|
RoomSetting? _roomSetting;
|
||||||
|
SocialChatUserProfile? _userProfile;
|
||||||
|
String? _userSVipLevel;
|
||||||
|
|
||||||
|
RoomProfile copyWith({
|
||||||
|
String? regionCode,
|
||||||
|
RoomCounter? roomCounter,
|
||||||
|
RoomProfile2? roomProfile,
|
||||||
|
RoomSetting? roomSetting,
|
||||||
|
SocialChatUserProfile? userProfile,
|
||||||
|
String? userSVipLevel,
|
||||||
|
}) => RoomProfile(
|
||||||
|
regionCode: regionCode ?? _regionCode,
|
||||||
|
roomCounter: roomCounter ?? _roomCounter,
|
||||||
|
roomProfile: roomProfile ?? _roomProfile,
|
||||||
|
roomSetting: roomSetting ?? _roomSetting,
|
||||||
|
userProfile: userProfile ?? _userProfile,
|
||||||
|
userSVipLevel: userSVipLevel ?? _userSVipLevel,
|
||||||
|
);
|
||||||
|
|
||||||
String? get regionCode => _regionCode;
|
String? get regionCode => _regionCode;
|
||||||
|
|
||||||
@ -247,11 +299,13 @@ class RoomProfile {
|
|||||||
|
|
||||||
RoomSetting? get roomSetting => _roomSetting;
|
RoomSetting? get roomSetting => _roomSetting;
|
||||||
|
|
||||||
SocialChatUserProfile? get userProfile => _userProfile;
|
SocialChatUserProfile? get userProfile => _userProfile;
|
||||||
|
|
||||||
Map<String?, dynamic> toJson() {
|
String? get userSVipLevel => _userSVipLevel;
|
||||||
final map = <String?, dynamic>{};
|
|
||||||
map['regionCode'] = _regionCode;
|
Map<String?, dynamic> toJson() {
|
||||||
|
final map = <String?, dynamic>{};
|
||||||
|
map['regionCode'] = _regionCode;
|
||||||
if (_roomCounter != null) {
|
if (_roomCounter != null) {
|
||||||
map['roomCounter'] = _roomCounter?.toJson();
|
map['roomCounter'] = _roomCounter?.toJson();
|
||||||
}
|
}
|
||||||
@ -261,11 +315,12 @@ class RoomProfile {
|
|||||||
if (_roomSetting != null) {
|
if (_roomSetting != null) {
|
||||||
map['roomSetting'] = _roomSetting?.toJson();
|
map['roomSetting'] = _roomSetting?.toJson();
|
||||||
}
|
}
|
||||||
if (_userProfile != null) {
|
if (_userProfile != null) {
|
||||||
map['userProfile'] = _userProfile?.toJson();
|
map['userProfile'] = _userProfile?.toJson();
|
||||||
}
|
}
|
||||||
return map;
|
map['userSVipLevel'] = _userSVipLevel;
|
||||||
}
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
void setRoomSetting(RoomSetting roomSetting) {
|
void setRoomSetting(RoomSetting roomSetting) {
|
||||||
_roomSetting = roomSetting;
|
_roomSetting = roomSetting;
|
||||||
|
|||||||
@ -121,6 +121,7 @@ class SocialChatUserProfile {
|
|||||||
num? heartbeatVal,
|
num? heartbeatVal,
|
||||||
String? originSys,
|
String? originSys,
|
||||||
OwnSpecialId? ownSpecialId,
|
OwnSpecialId? ownSpecialId,
|
||||||
|
String? vipLevel,
|
||||||
bool? sameRegion,
|
bool? sameRegion,
|
||||||
int? firstRechargeEndTime,
|
int? firstRechargeEndTime,
|
||||||
String? sysOriginChild,
|
String? sysOriginChild,
|
||||||
@ -160,6 +161,7 @@ class SocialChatUserProfile {
|
|||||||
_wealthLevel = wealthLevel;
|
_wealthLevel = wealthLevel;
|
||||||
_heartbeatVal = heartbeatVal;
|
_heartbeatVal = heartbeatVal;
|
||||||
_ownSpecialId = ownSpecialId;
|
_ownSpecialId = ownSpecialId;
|
||||||
|
_vipLevel = vipLevel;
|
||||||
_sameRegion = sameRegion;
|
_sameRegion = sameRegion;
|
||||||
_isCpRelation = isCpRelation;
|
_isCpRelation = isCpRelation;
|
||||||
_firstRechargeEndTime = firstRechargeEndTime;
|
_firstRechargeEndTime = firstRechargeEndTime;
|
||||||
@ -178,10 +180,14 @@ class SocialChatUserProfile {
|
|||||||
_backgroundPhotos = backgroundPhotos;
|
_backgroundPhotos = backgroundPhotos;
|
||||||
_personalPhotos = personalPhotos;
|
_personalPhotos = personalPhotos;
|
||||||
_cpList = cpList;
|
_cpList = cpList;
|
||||||
}
|
}
|
||||||
|
|
||||||
SocialChatUserProfile.fromJson(dynamic json) {
|
SocialChatUserProfile.fromJson(dynamic json) {
|
||||||
_account = json['account'];
|
final sourceJson = json;
|
||||||
|
if (sourceJson is Map && sourceJson['userProfile'] is Map) {
|
||||||
|
json = sourceJson['userProfile'];
|
||||||
|
}
|
||||||
|
_account = json['account'];
|
||||||
_accountStatus = json['accountStatus'];
|
_accountStatus = json['accountStatus'];
|
||||||
_age = json['age'];
|
_age = json['age'];
|
||||||
_bornDay = json['bornDay'];
|
_bornDay = json['bornDay'];
|
||||||
@ -204,6 +210,10 @@ class SocialChatUserProfile {
|
|||||||
json['ownSpecialId'] != null
|
json['ownSpecialId'] != null
|
||||||
? OwnSpecialId.fromJson(json['ownSpecialId'])
|
? OwnSpecialId.fromJson(json['ownSpecialId'])
|
||||||
: null;
|
: null;
|
||||||
|
_vipLevel = _firstNonBlankString([
|
||||||
|
..._vipLevelHintValues(json),
|
||||||
|
..._vipLevelHintValues(sourceJson),
|
||||||
|
]);
|
||||||
_sameRegion = json['sameRegion'];
|
_sameRegion = json['sameRegion'];
|
||||||
_isCpRelation = json['isCpRelation'];
|
_isCpRelation = json['isCpRelation'];
|
||||||
_firstRechargeEndTime = json['firstRechargeEndTime'];
|
_firstRechargeEndTime = json['firstRechargeEndTime'];
|
||||||
@ -274,6 +284,7 @@ class SocialChatUserProfile {
|
|||||||
num? _wealthLevel;
|
num? _wealthLevel;
|
||||||
num? _heartbeatVal;
|
num? _heartbeatVal;
|
||||||
OwnSpecialId? _ownSpecialId;
|
OwnSpecialId? _ownSpecialId;
|
||||||
|
String? _vipLevel;
|
||||||
bool? _sameRegion;
|
bool? _sameRegion;
|
||||||
bool? _isCpRelation;
|
bool? _isCpRelation;
|
||||||
int? _firstRechargeEndTime;
|
int? _firstRechargeEndTime;
|
||||||
@ -314,6 +325,7 @@ class SocialChatUserProfile {
|
|||||||
num? wealthLevel,
|
num? wealthLevel,
|
||||||
num? heartbeatVal,
|
num? heartbeatVal,
|
||||||
OwnSpecialId? ownSpecialId,
|
OwnSpecialId? ownSpecialId,
|
||||||
|
String? vipLevel,
|
||||||
bool? sameRegion,
|
bool? sameRegion,
|
||||||
bool? isCpRelation,
|
bool? isCpRelation,
|
||||||
int? firstRechargeEndTime,
|
int? firstRechargeEndTime,
|
||||||
@ -353,6 +365,7 @@ class SocialChatUserProfile {
|
|||||||
wealthLevel: wealthLevel ?? _wealthLevel,
|
wealthLevel: wealthLevel ?? _wealthLevel,
|
||||||
heartbeatVal: heartbeatVal ?? _heartbeatVal,
|
heartbeatVal: heartbeatVal ?? _heartbeatVal,
|
||||||
ownSpecialId: ownSpecialId ?? _ownSpecialId,
|
ownSpecialId: ownSpecialId ?? _ownSpecialId,
|
||||||
|
vipLevel: vipLevel ?? _vipLevel,
|
||||||
sameRegion: sameRegion ?? _sameRegion,
|
sameRegion: sameRegion ?? _sameRegion,
|
||||||
isCpRelation: isCpRelation ?? _isCpRelation,
|
isCpRelation: isCpRelation ?? _isCpRelation,
|
||||||
firstRechargeEndTime: firstRechargeEndTime ?? _firstRechargeEndTime,
|
firstRechargeEndTime: firstRechargeEndTime ?? _firstRechargeEndTime,
|
||||||
@ -413,6 +426,8 @@ class SocialChatUserProfile {
|
|||||||
|
|
||||||
OwnSpecialId? get ownSpecialId => _ownSpecialId;
|
OwnSpecialId? get ownSpecialId => _ownSpecialId;
|
||||||
|
|
||||||
|
String? get vipLevel => _vipLevel;
|
||||||
|
|
||||||
bool? get sameRegion => _sameRegion;
|
bool? get sameRegion => _sameRegion;
|
||||||
|
|
||||||
bool? get isCpRelation => _isCpRelation;
|
bool? get isCpRelation => _isCpRelation;
|
||||||
@ -494,6 +509,26 @@ class SocialChatUserProfile {
|
|||||||
return pr;
|
return pr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PropsResources? getFloatPicture() {
|
||||||
|
PropsResources? pr;
|
||||||
|
_useProps?.forEach((value) {
|
||||||
|
if (value.propsResources?.type == SCPropsType.FLOAT_PICTURE.name) {
|
||||||
|
pr = value.propsResources;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return pr;
|
||||||
|
}
|
||||||
|
|
||||||
|
PropsResources? getVipBadge() {
|
||||||
|
PropsResources? pr;
|
||||||
|
_useProps?.forEach((value) {
|
||||||
|
if (value.propsResources?.type == SCPropsType.BADGE.name) {
|
||||||
|
pr = value.propsResources;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return pr;
|
||||||
|
}
|
||||||
|
|
||||||
PropsResources? getMountains() {
|
PropsResources? getMountains() {
|
||||||
PropsResources? pr;
|
PropsResources? pr;
|
||||||
_useProps?.forEach((value) {
|
_useProps?.forEach((value) {
|
||||||
@ -547,6 +582,9 @@ class SocialChatUserProfile {
|
|||||||
if (_ownSpecialId != null) {
|
if (_ownSpecialId != null) {
|
||||||
map['ownSpecialId'] = _ownSpecialId?.toJson();
|
map['ownSpecialId'] = _ownSpecialId?.toJson();
|
||||||
}
|
}
|
||||||
|
if (_vipLevel != null) {
|
||||||
|
map['vipLevel'] = _vipLevel;
|
||||||
|
}
|
||||||
map['sameRegion'] = _sameRegion;
|
map['sameRegion'] = _sameRegion;
|
||||||
map['isCpRelation'] = _isCpRelation;
|
map['isCpRelation'] = _isCpRelation;
|
||||||
map['firstRecharge'] = _firstRecharge;
|
map['firstRecharge'] = _firstRecharge;
|
||||||
@ -591,22 +629,212 @@ class SocialChatUserProfile {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ID 数字渐变字的 VIP 条件预留。
|
int get vipLevelForColoredId {
|
||||||
/// 当前先复用 useProps 里的 NOBLE_VIP,道具接口稳定后可切到独立字段。
|
final hasExplicitVipLevel = _vipLevel?.trim().isNotEmpty ?? false;
|
||||||
bool hasVipPrivilegeForColoredId() {
|
final explicitLevel = _parseVipLevelForColoredId(_vipLevel) ?? 0;
|
||||||
return _useProps?.any(
|
if (hasExplicitVipLevel && explicitLevel <= 0) {
|
||||||
(value) => value.propsResources?.type == SCPropsType.NOBLE_VIP.name,
|
return 0;
|
||||||
) ??
|
}
|
||||||
false;
|
if (explicitLevel > 0) {
|
||||||
}
|
return explicitLevel;
|
||||||
|
}
|
||||||
|
|
||||||
bool shouldShowColoredSpecialIdText() {
|
final vipResource = getVIP();
|
||||||
return hasVipPrivilegeForColoredId();
|
final resourceLevel =
|
||||||
}
|
_parseVipLevelForColoredId(
|
||||||
|
vipResource?.name,
|
||||||
|
allowPlainNumber: false,
|
||||||
|
) ??
|
||||||
|
_parseVipLevelForColoredId(
|
||||||
|
vipResource?.code,
|
||||||
|
allowPlainNumber: false,
|
||||||
|
allowTrailingNumber: false,
|
||||||
|
);
|
||||||
|
if ((resourceLevel ?? 0) > 0) {
|
||||||
|
return resourceLevel!;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vipResource == null ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int get vipVisualResourceLevelForColoredId {
|
||||||
|
for (final resource in [
|
||||||
|
getVipBadge(),
|
||||||
|
getHeaddress(),
|
||||||
|
getChatBox(),
|
||||||
|
getFloatPicture(),
|
||||||
|
getDataCard(),
|
||||||
|
getMountains(),
|
||||||
|
]) {
|
||||||
|
final level = _vipLevelFromVisualResource(resource);
|
||||||
|
if ((level ?? 0) > 0) {
|
||||||
|
return level!;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// ID 数字渐变字的 VIP 条件。
|
||||||
|
/// 新 VIP 接口优先读等级字段;旧数据继续兼容 useProps 里的 NOBLE_VIP。
|
||||||
|
bool hasVipPrivilegeForColoredId() {
|
||||||
|
return vipLevelForColoredId > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool hasVipVisualResourceForColoredId() {
|
||||||
|
return vipVisualResourceLevelForColoredId > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool shouldShowColoredSpecialIdText() {
|
||||||
|
return hasVipPrivilegeForColoredId() || hasVipVisualResourceForColoredId();
|
||||||
|
}
|
||||||
|
|
||||||
void setHeartbeatVal(num? heartbeatVal) {
|
void setHeartbeatVal(num? heartbeatVal) {
|
||||||
_heartbeatVal = (_heartbeatVal ?? 0) + (heartbeatVal ?? 0);
|
_heartbeatVal = (_heartbeatVal ?? 0) + (heartbeatVal ?? 0);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? socialChatVipLevelHintFromJson(dynamic json) {
|
||||||
|
return _firstNonBlankString(_vipLevelHintValues(json));
|
||||||
|
}
|
||||||
|
|
||||||
|
SocialChatUserProfile? socialChatProfileWithVipLevelHint(
|
||||||
|
SocialChatUserProfile? profile,
|
||||||
|
dynamic json,
|
||||||
|
) {
|
||||||
|
final vipLevel = socialChatVipLevelHintFromJson(json);
|
||||||
|
if (profile == null || vipLevel == null) {
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
return profile.copyWith(vipLevel: vipLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
SocialChatUserProfile? socialChatUserProfileFromJsonWithVipHint(
|
||||||
|
dynamic json, {
|
||||||
|
String userProfileKey = 'userProfile',
|
||||||
|
}) {
|
||||||
|
if (json == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (json is Map && json[userProfileKey] != null) {
|
||||||
|
return socialChatProfileWithVipLevelHint(
|
||||||
|
SocialChatUserProfile.fromJson(json[userProfileKey]),
|
||||||
|
json,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SocialChatUserProfile.fromJson(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<dynamic> _vipLevelHintValues(dynamic json) sync* {
|
||||||
|
if (json is! Map) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
yield json['vipLevel'];
|
||||||
|
yield json['svipLevel'];
|
||||||
|
yield json['sVipLevel'];
|
||||||
|
yield json['userSVipLevel'];
|
||||||
|
yield json['supperVip'];
|
||||||
|
yield json['superVip'];
|
||||||
|
yield json['nobleVipLevel'];
|
||||||
|
yield json['vipLevelCode'];
|
||||||
|
yield json['levelCode'];
|
||||||
|
yield _nestedVipLevel(json['vipStatus']);
|
||||||
|
yield _nestedVipLevel(json['vipState']);
|
||||||
|
yield _nestedVipLevel(json['vip']);
|
||||||
|
final nestedProfile = json['userProfile'];
|
||||||
|
if (nestedProfile is Map) {
|
||||||
|
yield* _vipLevelHintValues(nestedProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _firstNonBlankString(Iterable<dynamic> values) {
|
||||||
|
for (final value in values) {
|
||||||
|
final text = value?.toString().trim();
|
||||||
|
if (text != null && text.isNotEmpty) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic _nestedVipLevel(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final active = value['active'];
|
||||||
|
if (active == false ||
|
||||||
|
active?.toString().trim().toLowerCase() == 'false') {
|
||||||
|
return '0';
|
||||||
|
}
|
||||||
|
return value['level'] ??
|
||||||
|
value['vipLevel'] ??
|
||||||
|
value['userSVipLevel'] ??
|
||||||
|
value['supperVip'] ??
|
||||||
|
value['levelCode'];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _vipLevelFromVisualResource(PropsResources? resource) {
|
||||||
|
if (resource == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return _parseVipLevelForColoredId(
|
||||||
|
resource.name,
|
||||||
|
allowPlainNumber: false,
|
||||||
|
) ??
|
||||||
|
_parseVipLevelForColoredId(
|
||||||
|
resource.code,
|
||||||
|
allowPlainNumber: false,
|
||||||
|
) ??
|
||||||
|
_parseVipLevelForColoredId(
|
||||||
|
resource.cover,
|
||||||
|
allowPlainNumber: false,
|
||||||
|
) ??
|
||||||
|
_parseVipLevelForColoredId(
|
||||||
|
resource.sourceUrl,
|
||||||
|
allowPlainNumber: false,
|
||||||
|
) ??
|
||||||
|
_parseVipLevelForColoredId(
|
||||||
|
resource.roomOpenedUrl,
|
||||||
|
allowPlainNumber: false,
|
||||||
|
) ??
|
||||||
|
_parseVipLevelForColoredId(
|
||||||
|
resource.roomSendCoverUrl,
|
||||||
|
allowPlainNumber: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _parseVipLevelForColoredId(
|
||||||
|
dynamic value, {
|
||||||
|
bool allowPlainNumber = true,
|
||||||
|
bool allowTrailingNumber = true,
|
||||||
|
}) {
|
||||||
|
final text = value?.toString().trim();
|
||||||
|
if (text == null || text.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowPlainNumber) {
|
||||||
|
final plainLevel = int.tryParse(text);
|
||||||
|
if ((plainLevel ?? 0) > 0) {
|
||||||
|
return plainLevel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final vipLevelMatch = RegExp(
|
||||||
|
r'(?:S?VIP|NOBLE[_\s-]*VIP)[^\d]*([1-9]\d*)',
|
||||||
|
caseSensitive: false,
|
||||||
|
).firstMatch(text);
|
||||||
|
if (vipLevelMatch != null) {
|
||||||
|
return int.tryParse(vipLevelMatch.group(1) ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowTrailingNumber) {
|
||||||
|
final trailingLevelMatch = RegExp(r'([1-9]\d*)$').firstMatch(text);
|
||||||
|
if (trailingLevelMatch != null) {
|
||||||
|
return int.tryParse(trailingLevelMatch.group(1) ?? '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// animationUrl : ""
|
/// animationUrl : ""
|
||||||
@ -992,11 +1220,11 @@ class PropsResources {
|
|||||||
PropsResources.fromJson(dynamic json) {
|
PropsResources.fromJson(dynamic json) {
|
||||||
_amount = json['amount'];
|
_amount = json['amount'];
|
||||||
_code = json['code'];
|
_code = json['code'];
|
||||||
_cover = json['cover'];
|
_cover = json['cover'] ?? json['coverUrl'];
|
||||||
_createTime = json['createTime'];
|
_createTime = json['createTime'];
|
||||||
_del = json['del'];
|
_del = json['del'];
|
||||||
_expand = json['expand'];
|
_expand = json['expand'];
|
||||||
_id = json['id'];
|
_id = json['id'] ?? json['resourceId'];
|
||||||
_imNotOpenedUrl = json['imNotOpenedUrl'];
|
_imNotOpenedUrl = json['imNotOpenedUrl'];
|
||||||
_imOpenedUrl = json['imOpenedUrl'];
|
_imOpenedUrl = json['imOpenedUrl'];
|
||||||
_imOpenedUrlTwo = json['imOpenedUrlTwo'];
|
_imOpenedUrlTwo = json['imOpenedUrlTwo'];
|
||||||
@ -1005,7 +1233,7 @@ class PropsResources {
|
|||||||
_roomNotOpenedUrl = json['roomNotOpenedUrl'];
|
_roomNotOpenedUrl = json['roomNotOpenedUrl'];
|
||||||
_roomOpenedUrl = json['roomOpenedUrl'];
|
_roomOpenedUrl = json['roomOpenedUrl'];
|
||||||
_roomSendCoverUrl = json['roomSendCoverUrl'];
|
_roomSendCoverUrl = json['roomSendCoverUrl'];
|
||||||
_sourceUrl = json['sourceUrl'];
|
_sourceUrl = json['sourceUrl'] ?? json['resourceUrl'] ?? json['url'];
|
||||||
_sysOrigin = json['sysOrigin'];
|
_sysOrigin = json['sysOrigin'];
|
||||||
_type = json['type'];
|
_type = json['type'];
|
||||||
_updateTime = json['updateTime'];
|
_updateTime = json['updateTime'];
|
||||||
|
|||||||
@ -34,11 +34,14 @@ class MicRes {
|
|||||||
_roomId = json['roomId'];
|
_roomId = json['roomId'];
|
||||||
_micIndex = json['micIndex'];
|
_micIndex = json['micIndex'];
|
||||||
_micLock = json['micLock'];
|
_micLock = json['micLock'];
|
||||||
_micMute = json['micMute'];
|
_micMute = json['micMute'];
|
||||||
_user =
|
_user =
|
||||||
json['user'] != null
|
json['user'] != null
|
||||||
? SocialChatUserProfile.fromJson(json['user'])
|
? socialChatProfileWithVipLevelHint(
|
||||||
: null;
|
SocialChatUserProfile.fromJson(json['user']),
|
||||||
|
json,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
_roomToken = json['roomToken'];
|
_roomToken = json['roomToken'];
|
||||||
_type = json['type'];
|
_type = json['type'];
|
||||||
_number = json['number'];
|
_number = json['number'];
|
||||||
@ -84,13 +87,15 @@ class MicRes {
|
|||||||
|
|
||||||
bool? get micLock => _micLock;
|
bool? get micLock => _micLock;
|
||||||
|
|
||||||
bool? get micMute => _micMute;
|
bool? get micMute => _micMute;
|
||||||
|
|
||||||
SocialChatUserProfile? get user => _user;
|
SocialChatUserProfile? get user => _user;
|
||||||
|
|
||||||
String? get roomToken => _roomToken;
|
String? get roomToken => _roomToken;
|
||||||
|
|
||||||
String? get type => _type;
|
int? get volume => _volume;
|
||||||
|
|
||||||
|
String? get type => _type;
|
||||||
|
|
||||||
String? get emojiPath => _emojiPath;
|
String? get emojiPath => _emojiPath;
|
||||||
|
|
||||||
|
|||||||
@ -4,21 +4,24 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
|||||||
/// totalFormat : ""
|
/// totalFormat : ""
|
||||||
/// total : 0
|
/// total : 0
|
||||||
|
|
||||||
class RoomGiftRankRes {
|
class RoomGiftRankRes {
|
||||||
RoomGiftRankRes({
|
RoomGiftRankRes({
|
||||||
SocialChatUserProfile? userProfile,
|
SocialChatUserProfile? userProfile,
|
||||||
String? totalFormat,
|
String? totalFormat,
|
||||||
String? total,}){
|
String? total,}){
|
||||||
_userProfile = userProfile;
|
_userProfile = userProfile;
|
||||||
_totalFormat = totalFormat;
|
_totalFormat = totalFormat;
|
||||||
_total = total;
|
_total = total;
|
||||||
}
|
}
|
||||||
|
|
||||||
RoomGiftRankRes.fromJson(dynamic json) {
|
RoomGiftRankRes.fromJson(dynamic json) {
|
||||||
_userProfile = json['userProfile'] != null ? SocialChatUserProfile.fromJson(json['userProfile']) : null;
|
_userProfile =
|
||||||
_totalFormat = json['totalFormat'];
|
json['userProfile'] != null
|
||||||
_total = json['total'];
|
? socialChatUserProfileFromJsonWithVipHint(json)
|
||||||
}
|
: null;
|
||||||
|
_totalFormat = json['totalFormat'];
|
||||||
|
_total = json['total'];
|
||||||
|
}
|
||||||
SocialChatUserProfile? _userProfile;
|
SocialChatUserProfile? _userProfile;
|
||||||
String? _totalFormat;
|
String? _totalFormat;
|
||||||
String? _total;
|
String? _total;
|
||||||
|
|||||||
@ -11,14 +11,14 @@ class SocialChatRoomMemberRes {
|
|||||||
_roles = roles;
|
_roles = roles;
|
||||||
}
|
}
|
||||||
|
|
||||||
SocialChatRoomMemberRes.fromJson(dynamic json) {
|
SocialChatRoomMemberRes.fromJson(dynamic json) {
|
||||||
_id = json['id'];
|
_id = json['id'];
|
||||||
_userProfile =
|
_userProfile =
|
||||||
json['userProfile'] != null
|
json['userProfile'] != null
|
||||||
? SocialChatUserProfile.fromJson(json['userProfile'])
|
? socialChatUserProfileFromJsonWithVipHint(json)
|
||||||
: null;
|
: null;
|
||||||
_roles = json['roles'];
|
_roles = json['roles'];
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _id;
|
String? _id;
|
||||||
SocialChatUserProfile? _userProfile;
|
SocialChatUserProfile? _userProfile;
|
||||||
|
|||||||
@ -18,8 +18,8 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
|||||||
/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":0,"originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}
|
/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":0,"originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}
|
||||||
/// userSVipLevel : ""
|
/// userSVipLevel : ""
|
||||||
|
|
||||||
class SocialChatRoomRes {
|
class SocialChatRoomRes {
|
||||||
SocialChatRoomRes({
|
SocialChatRoomRes({
|
||||||
String? countryCode,
|
String? countryCode,
|
||||||
String? countryName,
|
String? countryName,
|
||||||
String? event,
|
String? event,
|
||||||
@ -34,12 +34,12 @@ class SocialChatRoomRes {
|
|||||||
String? roomGameIcon,
|
String? roomGameIcon,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
String? sysOrigin,
|
String? sysOrigin,
|
||||||
String? userId,
|
String? userId,
|
||||||
SocialChatUserProfile? userProfile,
|
SocialChatUserProfile? userProfile,
|
||||||
String? userSVipLevel,
|
String? userSVipLevel,
|
||||||
RoomMemberCounter? roomCounter,
|
RoomMemberCounter? roomCounter,
|
||||||
ExtValues? extValues,
|
ExtValues? extValues,
|
||||||
}) {
|
}) {
|
||||||
_countryCode = countryCode;
|
_countryCode = countryCode;
|
||||||
_countryName = countryName;
|
_countryName = countryName;
|
||||||
_event = event;
|
_event = event;
|
||||||
@ -54,12 +54,16 @@ class SocialChatRoomRes {
|
|||||||
_roomGameIcon = roomGameIcon;
|
_roomGameIcon = roomGameIcon;
|
||||||
_roomName = roomName;
|
_roomName = roomName;
|
||||||
_sysOrigin = sysOrigin;
|
_sysOrigin = sysOrigin;
|
||||||
_userId = userId;
|
_userId = userId;
|
||||||
_userProfile = userProfile;
|
_userSVipLevel = userSVipLevel;
|
||||||
_userSVipLevel = userSVipLevel;
|
final normalizedVipLevel = userSVipLevel?.trim();
|
||||||
_roomCounter = roomCounter;
|
_userProfile =
|
||||||
_extValues = extValues;
|
normalizedVipLevel == null || normalizedVipLevel.isEmpty
|
||||||
}
|
? userProfile
|
||||||
|
: userProfile?.copyWith(vipLevel: normalizedVipLevel);
|
||||||
|
_roomCounter = roomCounter;
|
||||||
|
_extValues = extValues;
|
||||||
|
}
|
||||||
|
|
||||||
SocialChatRoomRes.fromJson(dynamic json) {
|
SocialChatRoomRes.fromJson(dynamic json) {
|
||||||
_countryCode = json['countryCode'];
|
_countryCode = json['countryCode'];
|
||||||
@ -82,18 +86,22 @@ class SocialChatRoomRes {
|
|||||||
_roomName = json['roomName'];
|
_roomName = json['roomName'];
|
||||||
_sysOrigin = json['sysOrigin'];
|
_sysOrigin = json['sysOrigin'];
|
||||||
_userId = json['userId'];
|
_userId = json['userId'];
|
||||||
_userProfile =
|
_userProfile =
|
||||||
json['userProfile'] != null
|
json['userProfile'] != null
|
||||||
? SocialChatUserProfile.fromJson(json['userProfile'])
|
? SocialChatUserProfile.fromJson(json['userProfile'])
|
||||||
: null;
|
: null;
|
||||||
_userSVipLevel = json['userSVipLevel'];
|
_userSVipLevel = socialChatVipLevelHintFromJson(json);
|
||||||
final counterJson = json['roomCounter'] ?? json['counter'];
|
final normalizedVipLevel = _userSVipLevel?.trim();
|
||||||
_roomCounter =
|
if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) {
|
||||||
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
|
_userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel);
|
||||||
_extValues =
|
}
|
||||||
json['extValues'] != null
|
final counterJson = json['roomCounter'] ?? json['counter'];
|
||||||
? ExtValues.fromJson(json['extValues'])
|
_roomCounter =
|
||||||
: null;
|
counterJson != null ? RoomMemberCounter.fromJson(counterJson) : null;
|
||||||
|
_extValues =
|
||||||
|
json['extValues'] != null
|
||||||
|
? ExtValues.fromJson(json['extValues'])
|
||||||
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
String? _countryCode;
|
String? _countryCode;
|
||||||
@ -110,11 +118,11 @@ class SocialChatRoomRes {
|
|||||||
String? _roomGameIcon;
|
String? _roomGameIcon;
|
||||||
String? _roomName;
|
String? _roomName;
|
||||||
String? _sysOrigin;
|
String? _sysOrigin;
|
||||||
String? _userId;
|
String? _userId;
|
||||||
SocialChatUserProfile? _userProfile;
|
SocialChatUserProfile? _userProfile;
|
||||||
String? _userSVipLevel;
|
String? _userSVipLevel;
|
||||||
RoomMemberCounter? _roomCounter;
|
RoomMemberCounter? _roomCounter;
|
||||||
ExtValues? _extValues;
|
ExtValues? _extValues;
|
||||||
|
|
||||||
SocialChatRoomRes copyWith({
|
SocialChatRoomRes copyWith({
|
||||||
String? countryCode,
|
String? countryCode,
|
||||||
@ -131,12 +139,12 @@ class SocialChatRoomRes {
|
|||||||
String? roomGameIcon,
|
String? roomGameIcon,
|
||||||
String? roomName,
|
String? roomName,
|
||||||
String? sysOrigin,
|
String? sysOrigin,
|
||||||
String? userId,
|
String? userId,
|
||||||
SocialChatUserProfile? userProfile,
|
SocialChatUserProfile? userProfile,
|
||||||
String? userSVipLevel,
|
String? userSVipLevel,
|
||||||
RoomMemberCounter? roomCounter,
|
RoomMemberCounter? roomCounter,
|
||||||
ExtValues? extValues,
|
ExtValues? extValues,
|
||||||
}) => SocialChatRoomRes(
|
}) => SocialChatRoomRes(
|
||||||
countryCode: countryCode ?? _countryCode,
|
countryCode: countryCode ?? _countryCode,
|
||||||
countryName: countryName ?? _countryName,
|
countryName: countryName ?? _countryName,
|
||||||
event: event ?? _event,
|
event: event ?? _event,
|
||||||
@ -151,12 +159,12 @@ class SocialChatRoomRes {
|
|||||||
roomGameIcon: roomGameIcon ?? _roomGameIcon,
|
roomGameIcon: roomGameIcon ?? _roomGameIcon,
|
||||||
roomName: roomName ?? _roomName,
|
roomName: roomName ?? _roomName,
|
||||||
sysOrigin: sysOrigin ?? _sysOrigin,
|
sysOrigin: sysOrigin ?? _sysOrigin,
|
||||||
userId: userId ?? _userId,
|
userId: userId ?? _userId,
|
||||||
userProfile: userProfile ?? _userProfile,
|
userProfile: userProfile ?? _userProfile,
|
||||||
userSVipLevel: userSVipLevel ?? _userSVipLevel,
|
userSVipLevel: userSVipLevel ?? _userSVipLevel,
|
||||||
roomCounter: roomCounter ?? _roomCounter,
|
roomCounter: roomCounter ?? _roomCounter,
|
||||||
extValues: extValues ?? _extValues,
|
extValues: extValues ?? _extValues,
|
||||||
);
|
);
|
||||||
|
|
||||||
String? get countryCode => _countryCode;
|
String? get countryCode => _countryCode;
|
||||||
|
|
||||||
@ -188,25 +196,25 @@ class SocialChatRoomRes {
|
|||||||
|
|
||||||
String? get userId => _userId;
|
String? get userId => _userId;
|
||||||
|
|
||||||
SocialChatUserProfile? get userProfile => _userProfile;
|
SocialChatUserProfile? get userProfile => _userProfile;
|
||||||
|
|
||||||
String? get userSVipLevel => _userSVipLevel;
|
String? get userSVipLevel => _userSVipLevel;
|
||||||
|
|
||||||
RoomMemberCounter? get roomCounter => _roomCounter;
|
RoomMemberCounter? get roomCounter => _roomCounter;
|
||||||
|
|
||||||
ExtValues? get extValues => _extValues;
|
ExtValues? get extValues => _extValues;
|
||||||
|
|
||||||
String get displayMemberCount {
|
String get displayMemberCount {
|
||||||
final memberCount = roomCounter?.memberCount;
|
final memberCount = roomCounter?.memberCount;
|
||||||
if (memberCount != null) {
|
if (memberCount != null) {
|
||||||
return _formatRoomMemberCount(memberCount);
|
return _formatRoomMemberCount(memberCount);
|
||||||
}
|
}
|
||||||
final memberQuantity = extValues?.memberQuantity;
|
final memberQuantity = extValues?.memberQuantity;
|
||||||
if ((memberQuantity ?? "").trim().isNotEmpty) {
|
if ((memberQuantity ?? "").trim().isNotEmpty) {
|
||||||
return memberQuantity!;
|
return memberQuantity!;
|
||||||
}
|
}
|
||||||
return "0";
|
return "0";
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final map = <String, dynamic>{};
|
final map = <String, dynamic>{};
|
||||||
@ -225,58 +233,58 @@ class SocialChatRoomRes {
|
|||||||
map['roomName'] = _roomName;
|
map['roomName'] = _roomName;
|
||||||
map['sysOrigin'] = _sysOrigin;
|
map['sysOrigin'] = _sysOrigin;
|
||||||
map['userId'] = _userId;
|
map['userId'] = _userId;
|
||||||
if (_userProfile != null) {
|
if (_userProfile != null) {
|
||||||
map['userProfile'] = _userProfile?.toJson();
|
map['userProfile'] = _userProfile?.toJson();
|
||||||
}
|
}
|
||||||
map['userSVipLevel'] = _userSVipLevel;
|
map['userSVipLevel'] = _userSVipLevel;
|
||||||
if (_roomCounter != null) {
|
if (_roomCounter != null) {
|
||||||
map['roomCounter'] = _roomCounter?.toJson();
|
map['roomCounter'] = _roomCounter?.toJson();
|
||||||
}
|
}
|
||||||
if (_extValues != null) {
|
if (_extValues != null) {
|
||||||
map['extValues'] = _extValues?.toJson();
|
map['extValues'] = _extValues?.toJson();
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatRoomMemberCount(num value) {
|
String _formatRoomMemberCount(num value) {
|
||||||
if (value == value.roundToDouble()) {
|
if (value == value.roundToDouble()) {
|
||||||
return value.toInt().toString();
|
return value.toInt().toString();
|
||||||
}
|
}
|
||||||
return value.toString();
|
return value.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
class RoomMemberCounter {
|
class RoomMemberCounter {
|
||||||
RoomMemberCounter({num? adminCount, num? memberCount}) {
|
RoomMemberCounter({num? adminCount, num? memberCount}) {
|
||||||
_adminCount = adminCount;
|
_adminCount = adminCount;
|
||||||
_memberCount = memberCount;
|
_memberCount = memberCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
RoomMemberCounter.fromJson(dynamic json) {
|
RoomMemberCounter.fromJson(dynamic json) {
|
||||||
_adminCount = json['adminCount'];
|
_adminCount = json['adminCount'];
|
||||||
_memberCount = json['memberCount'];
|
_memberCount = json['memberCount'];
|
||||||
}
|
}
|
||||||
|
|
||||||
num? _adminCount;
|
num? _adminCount;
|
||||||
num? _memberCount;
|
num? _memberCount;
|
||||||
|
|
||||||
RoomMemberCounter copyWith({num? adminCount, num? memberCount}) =>
|
RoomMemberCounter copyWith({num? adminCount, num? memberCount}) =>
|
||||||
RoomMemberCounter(
|
RoomMemberCounter(
|
||||||
adminCount: adminCount ?? _adminCount,
|
adminCount: adminCount ?? _adminCount,
|
||||||
memberCount: memberCount ?? _memberCount,
|
memberCount: memberCount ?? _memberCount,
|
||||||
);
|
);
|
||||||
|
|
||||||
num? get adminCount => _adminCount;
|
num? get adminCount => _adminCount;
|
||||||
|
|
||||||
num? get memberCount => _memberCount;
|
num? get memberCount => _memberCount;
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final map = <String, dynamic>{};
|
final map = <String, dynamic>{};
|
||||||
map['adminCount'] = _adminCount;
|
map['adminCount'] = _adminCount;
|
||||||
map['memberCount'] = _memberCount;
|
map['memberCount'] = _memberCount;
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// account : ""
|
/// account : ""
|
||||||
/// accountStatus : ""
|
/// accountStatus : ""
|
||||||
|
|||||||
@ -12,19 +12,19 @@ import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
|||||||
/// userLevel : {"charmLevel":0,"extValues":{"":{}},"wealthLevel":0}
|
/// userLevel : {"charmLevel":0,"extValues":{"":{}},"wealthLevel":0}
|
||||||
/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}
|
/// userProfile : {"account":"","accountStatus":"","age":0,"bornDay":0,"bornMonth":0,"bornYear":0,"countryCode":"","countryId":0,"countryName":"","createTime":0,"del":false,"freezingTime":0,"id":"0","originSys":"","ownSpecialId":{"account":"","expiredTime":0},"sameRegion":false,"sysOriginChild":"","useProps":[{"expireTime":0,"propsResources":{"amount":0.0,"code":"","cover":"","expand":"","id":0,"name":"","sourceUrl":"","type":""},"userId":0}],"userAvatar":"","userNickname":"","userSex":0,"wearBadge":[{"animationUrl":"","badgeKey":"","badgeLevel":0,"badgeName":"","expireTime":0,"id":0,"milestone":0,"notSelectUrl":"","selectUrl":"","type":"","userId":0}]}
|
||||||
|
|
||||||
class RoomUserCardRes {
|
class RoomUserCardRes {
|
||||||
RoomUserCardRes({
|
RoomUserCardRes({
|
||||||
bool? agent,
|
bool? agent,
|
||||||
bool? anchor,
|
bool? anchor,
|
||||||
bool? follow,
|
bool? follow,
|
||||||
bool? friend,
|
bool? friend,
|
||||||
String? ktvIntegral,
|
String? ktvIntegral,
|
||||||
String? roomRole,
|
String? roomRole,
|
||||||
String? supperVip,
|
String? supperVip,
|
||||||
List<UseBadge>? useBadge,
|
List<UseBadge>? useBadge,
|
||||||
List<UseProps>? useProps,
|
List<UseProps>? useProps,
|
||||||
UserLevel? userLevel,
|
UserLevel? userLevel,
|
||||||
SocialChatUserProfile? userProfile,}){
|
SocialChatUserProfile? userProfile,}){
|
||||||
_agent = agent;
|
_agent = agent;
|
||||||
_anchor = anchor;
|
_anchor = anchor;
|
||||||
_follow = follow;
|
_follow = follow;
|
||||||
@ -35,8 +35,12 @@ class RoomUserCardRes {
|
|||||||
_useBadge = useBadge;
|
_useBadge = useBadge;
|
||||||
_useProps = useProps;
|
_useProps = useProps;
|
||||||
_userLevel = userLevel;
|
_userLevel = userLevel;
|
||||||
_userProfile = userProfile;
|
final normalizedVipLevel = supperVip?.trim();
|
||||||
}
|
_userProfile =
|
||||||
|
normalizedVipLevel == null || normalizedVipLevel.isEmpty
|
||||||
|
? userProfile
|
||||||
|
: userProfile?.copyWith(vipLevel: normalizedVipLevel);
|
||||||
|
}
|
||||||
|
|
||||||
RoomUserCardRes.fromJson(dynamic json) {
|
RoomUserCardRes.fromJson(dynamic json) {
|
||||||
_agent = json['agent'];
|
_agent = json['agent'];
|
||||||
@ -45,7 +49,7 @@ class RoomUserCardRes {
|
|||||||
_friend = json['friend'];
|
_friend = json['friend'];
|
||||||
_ktvIntegral = json['ktvIntegral'];
|
_ktvIntegral = json['ktvIntegral'];
|
||||||
_roomRole = json['roomRole'];
|
_roomRole = json['roomRole'];
|
||||||
_supperVip = json['supperVip'];
|
_supperVip = socialChatVipLevelHintFromJson(json);
|
||||||
if (json['useBadge'] != null) {
|
if (json['useBadge'] != null) {
|
||||||
_useBadge = [];
|
_useBadge = [];
|
||||||
json['useBadge'].forEach((v) {
|
json['useBadge'].forEach((v) {
|
||||||
@ -58,8 +62,15 @@ class RoomUserCardRes {
|
|||||||
_useProps?.add(UseProps.fromJson(v));
|
_useProps?.add(UseProps.fromJson(v));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
_userLevel = json['userLevel'] != null ? UserLevel.fromJson(json['userLevel']) : null;
|
_userLevel = json['userLevel'] != null ? UserLevel.fromJson(json['userLevel']) : null;
|
||||||
_userProfile = json['userProfile'] != null ? SocialChatUserProfile.fromJson(json['userProfile']) : null;
|
_userProfile =
|
||||||
|
json['userProfile'] != null
|
||||||
|
? socialChatUserProfileFromJsonWithVipHint(json)
|
||||||
|
: null;
|
||||||
|
final normalizedVipLevel = _supperVip?.trim();
|
||||||
|
if (normalizedVipLevel != null && normalizedVipLevel.isNotEmpty) {
|
||||||
|
_userProfile = _userProfile?.copyWith(vipLevel: normalizedVipLevel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
bool? _agent;
|
bool? _agent;
|
||||||
bool? _anchor;
|
bool? _anchor;
|
||||||
@ -72,29 +83,29 @@ class RoomUserCardRes {
|
|||||||
List<UseProps>? _useProps;
|
List<UseProps>? _useProps;
|
||||||
UserLevel? _userLevel;
|
UserLevel? _userLevel;
|
||||||
SocialChatUserProfile? _userProfile;
|
SocialChatUserProfile? _userProfile;
|
||||||
RoomUserCardRes copyWith({ bool? agent,
|
RoomUserCardRes copyWith({ bool? agent,
|
||||||
bool? anchor,
|
bool? anchor,
|
||||||
bool? follow,
|
bool? follow,
|
||||||
bool? friend,
|
bool? friend,
|
||||||
String? ktvIntegral,
|
String? ktvIntegral,
|
||||||
String? roomRole,
|
String? roomRole,
|
||||||
String? supperVip,
|
String? supperVip,
|
||||||
List<UseBadge>? useBadge,
|
List<UseBadge>? useBadge,
|
||||||
List<UseProps>? useProps,
|
List<UseProps>? useProps,
|
||||||
UserLevel? userLevel,
|
UserLevel? userLevel,
|
||||||
SocialChatUserProfile? userProfile,
|
SocialChatUserProfile? userProfile,
|
||||||
}) => RoomUserCardRes( agent: agent ?? _agent,
|
}) => RoomUserCardRes( agent: agent ?? _agent,
|
||||||
anchor: anchor ?? _anchor,
|
anchor: anchor ?? _anchor,
|
||||||
follow: follow ?? _follow,
|
follow: follow ?? _follow,
|
||||||
friend: friend ?? _friend,
|
friend: friend ?? _friend,
|
||||||
ktvIntegral: ktvIntegral ?? _ktvIntegral,
|
ktvIntegral: ktvIntegral ?? _ktvIntegral,
|
||||||
roomRole: roomRole ?? _roomRole,
|
roomRole: roomRole ?? _roomRole,
|
||||||
supperVip: supperVip ?? _supperVip,
|
supperVip: supperVip ?? _supperVip,
|
||||||
useBadge: useBadge ?? _useBadge,
|
useBadge: useBadge ?? _useBadge,
|
||||||
useProps: useProps ?? _useProps,
|
useProps: useProps ?? _useProps,
|
||||||
userLevel: userLevel ?? _userLevel,
|
userLevel: userLevel ?? _userLevel,
|
||||||
userProfile: userProfile ?? _userProfile,
|
userProfile: userProfile ?? _userProfile,
|
||||||
);
|
);
|
||||||
bool? get agent => _agent;
|
bool? get agent => _agent;
|
||||||
bool? get anchor => _anchor;
|
bool? get anchor => _anchor;
|
||||||
bool? get follow => _follow;
|
bool? get follow => _follow;
|
||||||
@ -129,9 +140,9 @@ RoomUserCardRes copyWith({ bool? agent,
|
|||||||
map['userProfile'] = _userProfile?.toJson();
|
map['userProfile'] = _userProfile?.toJson();
|
||||||
}
|
}
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// account : ""
|
/// account : ""
|
||||||
/// accountStatus : ""
|
/// accountStatus : ""
|
||||||
@ -168,19 +179,19 @@ RoomUserCardRes copyWith({ bool? agent,
|
|||||||
/// type : ""
|
/// type : ""
|
||||||
/// userId : 0
|
/// userId : 0
|
||||||
|
|
||||||
class WearBadge {
|
class WearBadge {
|
||||||
WearBadge({
|
WearBadge({
|
||||||
String? animationUrl,
|
String? animationUrl,
|
||||||
String? badgeKey,
|
String? badgeKey,
|
||||||
num? badgeLevel,
|
num? badgeLevel,
|
||||||
String? badgeName,
|
String? badgeName,
|
||||||
num? expireTime,
|
num? expireTime,
|
||||||
num? id,
|
num? id,
|
||||||
num? milestone,
|
num? milestone,
|
||||||
String? notSelectUrl,
|
String? notSelectUrl,
|
||||||
String? selectUrl,
|
String? selectUrl,
|
||||||
String? type,
|
String? type,
|
||||||
num? userId,}){
|
num? userId,}){
|
||||||
_animationUrl = animationUrl;
|
_animationUrl = animationUrl;
|
||||||
_badgeKey = badgeKey;
|
_badgeKey = badgeKey;
|
||||||
_badgeLevel = badgeLevel;
|
_badgeLevel = badgeLevel;
|
||||||
@ -189,10 +200,10 @@ class WearBadge {
|
|||||||
_id = id;
|
_id = id;
|
||||||
_milestone = milestone;
|
_milestone = milestone;
|
||||||
_notSelectUrl = notSelectUrl;
|
_notSelectUrl = notSelectUrl;
|
||||||
_selectUrl = selectUrl;
|
_selectUrl = selectUrl;
|
||||||
_type = type;
|
_type = type;
|
||||||
_userId = userId;
|
_userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
WearBadge.fromJson(dynamic json) {
|
WearBadge.fromJson(dynamic json) {
|
||||||
_animationUrl = json['animationUrl'];
|
_animationUrl = json['animationUrl'];
|
||||||
@ -218,29 +229,29 @@ class WearBadge {
|
|||||||
String? _selectUrl;
|
String? _selectUrl;
|
||||||
String? _type;
|
String? _type;
|
||||||
num? _userId;
|
num? _userId;
|
||||||
WearBadge copyWith({ String? animationUrl,
|
WearBadge copyWith({ String? animationUrl,
|
||||||
String? badgeKey,
|
String? badgeKey,
|
||||||
num? badgeLevel,
|
num? badgeLevel,
|
||||||
String? badgeName,
|
String? badgeName,
|
||||||
num? expireTime,
|
num? expireTime,
|
||||||
num? id,
|
num? id,
|
||||||
num? milestone,
|
num? milestone,
|
||||||
String? notSelectUrl,
|
String? notSelectUrl,
|
||||||
String? selectUrl,
|
String? selectUrl,
|
||||||
String? type,
|
String? type,
|
||||||
num? userId,
|
num? userId,
|
||||||
}) => WearBadge( animationUrl: animationUrl ?? _animationUrl,
|
}) => WearBadge( animationUrl: animationUrl ?? _animationUrl,
|
||||||
badgeKey: badgeKey ?? _badgeKey,
|
badgeKey: badgeKey ?? _badgeKey,
|
||||||
badgeLevel: badgeLevel ?? _badgeLevel,
|
badgeLevel: badgeLevel ?? _badgeLevel,
|
||||||
badgeName: badgeName ?? _badgeName,
|
badgeName: badgeName ?? _badgeName,
|
||||||
expireTime: expireTime ?? _expireTime,
|
expireTime: expireTime ?? _expireTime,
|
||||||
id: id ?? _id,
|
id: id ?? _id,
|
||||||
milestone: milestone ?? _milestone,
|
milestone: milestone ?? _milestone,
|
||||||
notSelectUrl: notSelectUrl ?? _notSelectUrl,
|
notSelectUrl: notSelectUrl ?? _notSelectUrl,
|
||||||
selectUrl: selectUrl ?? _selectUrl,
|
selectUrl: selectUrl ?? _selectUrl,
|
||||||
type: type ?? _type,
|
type: type ?? _type,
|
||||||
userId: userId ?? _userId,
|
userId: userId ?? _userId,
|
||||||
);
|
);
|
||||||
String? get animationUrl => _animationUrl;
|
String? get animationUrl => _animationUrl;
|
||||||
String? get badgeKey => _badgeKey;
|
String? get badgeKey => _badgeKey;
|
||||||
num? get badgeLevel => _badgeLevel;
|
num? get badgeLevel => _badgeLevel;
|
||||||
@ -267,17 +278,17 @@ WearBadge copyWith({ String? animationUrl,
|
|||||||
map['type'] = _type;
|
map['type'] = _type;
|
||||||
map['userId'] = _userId;
|
map['userId'] = _userId;
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UserLevel {
|
class UserLevel {
|
||||||
UserLevel({
|
UserLevel({
|
||||||
num? charmLevel,
|
num? charmLevel,
|
||||||
num? wealthLevel,}){
|
num? wealthLevel,}){
|
||||||
_charmLevel = charmLevel;
|
_charmLevel = charmLevel;
|
||||||
_wealthLevel = wealthLevel;
|
_wealthLevel = wealthLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
UserLevel.fromJson(dynamic json) {
|
UserLevel.fromJson(dynamic json) {
|
||||||
_charmLevel = json['charmLevel'];
|
_charmLevel = json['charmLevel'];
|
||||||
@ -285,11 +296,11 @@ class UserLevel {
|
|||||||
}
|
}
|
||||||
num? _charmLevel;
|
num? _charmLevel;
|
||||||
num? _wealthLevel;
|
num? _wealthLevel;
|
||||||
UserLevel copyWith({ num? charmLevel,
|
UserLevel copyWith({ num? charmLevel,
|
||||||
num? wealthLevel,
|
num? wealthLevel,
|
||||||
}) => UserLevel( charmLevel: charmLevel ?? _charmLevel,
|
}) => UserLevel( charmLevel: charmLevel ?? _charmLevel,
|
||||||
wealthLevel: wealthLevel ?? _wealthLevel,
|
wealthLevel: wealthLevel ?? _wealthLevel,
|
||||||
);
|
);
|
||||||
num? get charmLevel => _charmLevel;
|
num? get charmLevel => _charmLevel;
|
||||||
num? get wealthLevel => _wealthLevel;
|
num? get wealthLevel => _wealthLevel;
|
||||||
|
|
||||||
@ -298,23 +309,23 @@ UserLevel copyWith({ num? charmLevel,
|
|||||||
map['charmLevel'] = _charmLevel;
|
map['charmLevel'] = _charmLevel;
|
||||||
map['wealthLevel'] = _wealthLevel;
|
map['wealthLevel'] = _wealthLevel;
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UseBadge {
|
class UseBadge {
|
||||||
UseBadge({
|
UseBadge({
|
||||||
String? animationUrl,
|
String? animationUrl,
|
||||||
String? badgeKey,
|
String? badgeKey,
|
||||||
num? badgeLevel,
|
num? badgeLevel,
|
||||||
String? badgeName,
|
String? badgeName,
|
||||||
num? expireTime,
|
num? expireTime,
|
||||||
String? id,
|
String? id,
|
||||||
String? milestone,
|
String? milestone,
|
||||||
String? notSelectUrl,
|
String? notSelectUrl,
|
||||||
String? selectUrl,
|
String? selectUrl,
|
||||||
String? type,
|
String? type,
|
||||||
String? userId,}){
|
String? userId,}){
|
||||||
_animationUrl = animationUrl;
|
_animationUrl = animationUrl;
|
||||||
_badgeKey = badgeKey;
|
_badgeKey = badgeKey;
|
||||||
_badgeLevel = badgeLevel;
|
_badgeLevel = badgeLevel;
|
||||||
@ -323,10 +334,10 @@ class UseBadge {
|
|||||||
_id = id;
|
_id = id;
|
||||||
_milestone = milestone;
|
_milestone = milestone;
|
||||||
_notSelectUrl = notSelectUrl;
|
_notSelectUrl = notSelectUrl;
|
||||||
_selectUrl = selectUrl;
|
_selectUrl = selectUrl;
|
||||||
_type = type;
|
_type = type;
|
||||||
_userId = userId;
|
_userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
UseBadge.fromJson(dynamic json) {
|
UseBadge.fromJson(dynamic json) {
|
||||||
_animationUrl = json['animationUrl'];
|
_animationUrl = json['animationUrl'];
|
||||||
@ -352,29 +363,29 @@ class UseBadge {
|
|||||||
String? _selectUrl;
|
String? _selectUrl;
|
||||||
String? _type;
|
String? _type;
|
||||||
String? _userId;
|
String? _userId;
|
||||||
UseBadge copyWith({ String? animationUrl,
|
UseBadge copyWith({ String? animationUrl,
|
||||||
String? badgeKey,
|
String? badgeKey,
|
||||||
num? badgeLevel,
|
num? badgeLevel,
|
||||||
String? badgeName,
|
String? badgeName,
|
||||||
num? expireTime,
|
num? expireTime,
|
||||||
String? id,
|
String? id,
|
||||||
String? milestone,
|
String? milestone,
|
||||||
String? notSelectUrl,
|
String? notSelectUrl,
|
||||||
String? selectUrl,
|
String? selectUrl,
|
||||||
String? type,
|
String? type,
|
||||||
String? userId,
|
String? userId,
|
||||||
}) => UseBadge( animationUrl: animationUrl ?? _animationUrl,
|
}) => UseBadge( animationUrl: animationUrl ?? _animationUrl,
|
||||||
badgeKey: badgeKey ?? _badgeKey,
|
badgeKey: badgeKey ?? _badgeKey,
|
||||||
badgeLevel: badgeLevel ?? _badgeLevel,
|
badgeLevel: badgeLevel ?? _badgeLevel,
|
||||||
badgeName: badgeName ?? _badgeName,
|
badgeName: badgeName ?? _badgeName,
|
||||||
expireTime: expireTime ?? _expireTime,
|
expireTime: expireTime ?? _expireTime,
|
||||||
id: id ?? _id,
|
id: id ?? _id,
|
||||||
milestone: milestone ?? _milestone,
|
milestone: milestone ?? _milestone,
|
||||||
notSelectUrl: notSelectUrl ?? _notSelectUrl,
|
notSelectUrl: notSelectUrl ?? _notSelectUrl,
|
||||||
selectUrl: selectUrl ?? _selectUrl,
|
selectUrl: selectUrl ?? _selectUrl,
|
||||||
type: type ?? _type,
|
type: type ?? _type,
|
||||||
userId: userId ?? _userId,
|
userId: userId ?? _userId,
|
||||||
);
|
);
|
||||||
String? get animationUrl => _animationUrl;
|
String? get animationUrl => _animationUrl;
|
||||||
String? get badgeKey => _badgeKey;
|
String? get badgeKey => _badgeKey;
|
||||||
num? get badgeLevel => _badgeLevel;
|
num? get badgeLevel => _badgeLevel;
|
||||||
@ -401,6 +412,6 @@ UseBadge copyWith({ String? animationUrl,
|
|||||||
map['type'] = _type;
|
map['type'] = _type;
|
||||||
map['userId'] = _userId;
|
map['userId'] = _userId;
|
||||||
return map;
|
return map;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ class SCFloatingMessage {
|
|||||||
String? userName; // 用户昵称
|
String? userName; // 用户昵称
|
||||||
String? toUserName; // 用户昵称
|
String? toUserName; // 用户昵称
|
||||||
String? giftUrl; // 礼物图标
|
String? giftUrl; // 礼物图标
|
||||||
|
String? fallbackUrl; // 兜底资源
|
||||||
String? giftId; // 礼物id
|
String? giftId; // 礼物id
|
||||||
int? type;
|
int? type;
|
||||||
int? rocketLevel;
|
int? rocketLevel;
|
||||||
@ -26,6 +27,7 @@ class SCFloatingMessage {
|
|||||||
this.userName = '',
|
this.userName = '',
|
||||||
this.toUserName = '',
|
this.toUserName = '',
|
||||||
this.giftUrl = '',
|
this.giftUrl = '',
|
||||||
|
this.fallbackUrl = '',
|
||||||
this.giftId = '',
|
this.giftId = '',
|
||||||
this.number = 0,
|
this.number = 0,
|
||||||
this.coins = 0,
|
this.coins = 0,
|
||||||
@ -44,6 +46,7 @@ class SCFloatingMessage {
|
|||||||
userName = json['userName'];
|
userName = json['userName'];
|
||||||
toUserName = json['toUserName'];
|
toUserName = json['toUserName'];
|
||||||
giftUrl = json['giftUrl'];
|
giftUrl = json['giftUrl'];
|
||||||
|
fallbackUrl = json['fallbackUrl'];
|
||||||
giftId = json['giftId'];
|
giftId = json['giftId'];
|
||||||
coins = json['coins'];
|
coins = json['coins'];
|
||||||
number = json['number'];
|
number = json['number'];
|
||||||
@ -63,6 +66,7 @@ class SCFloatingMessage {
|
|||||||
map['userName'] = userName;
|
map['userName'] = userName;
|
||||||
map['toUserName'] = toUserName;
|
map['toUserName'] = toUserName;
|
||||||
map['giftUrl'] = giftUrl;
|
map['giftUrl'] = giftUrl;
|
||||||
|
map['fallbackUrl'] = fallbackUrl;
|
||||||
map['giftId'] = giftId;
|
map['giftId'] = giftId;
|
||||||
map['coins'] = coins;
|
map['coins'] = coins;
|
||||||
map['number'] = number;
|
map['number'] = number;
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import 'package:yumi/ui_kit/widgets/room/floating/floating_gift_screen_widget.da
|
|||||||
import 'package:yumi/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart';
|
import 'package:yumi/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart';
|
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_redenvelope_screen_widget.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart';
|
import 'package:yumi/ui_kit/widgets/room/floating/floating_room_rocket_screen_widget.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/room/floating/floating_vip_entry_screen_widget.dart';
|
||||||
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart';
|
||||||
|
|
||||||
@ -76,8 +77,8 @@ class OverlayManager {
|
|||||||
unawaited(
|
unawaited(
|
||||||
warmImageResource(
|
warmImageResource(
|
||||||
message.giftUrl ?? "",
|
message.giftUrl ?? "",
|
||||||
logicalWidth: 96,
|
logicalWidth: message.type == 5 ? 350 : 96,
|
||||||
logicalHeight: 96,
|
logicalHeight: message.type == 5 ? 84 : 96,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
if (message.type == 0) {
|
if (message.type == 0) {
|
||||||
@ -209,6 +210,12 @@ class OverlayManager {
|
|||||||
message: message,
|
message: message,
|
||||||
onAnimationCompleted: onComplete,
|
onAnimationCompleted: onComplete,
|
||||||
);
|
);
|
||||||
|
case 5:
|
||||||
|
//VIP进房飘窗
|
||||||
|
return FloatingVipEntryScreenWidget(
|
||||||
|
message: message,
|
||||||
|
onAnimationCompleted: onComplete,
|
||||||
|
);
|
||||||
default:
|
default:
|
||||||
onComplete();
|
onComplete();
|
||||||
return Container();
|
return Container();
|
||||||
@ -233,6 +240,7 @@ class OverlayManager {
|
|||||||
_removeActiveRoomMessage();
|
_removeActiveRoomMessage();
|
||||||
_removeMessagesByType(1);
|
_removeMessagesByType(1);
|
||||||
_removeMessagesByType(0);
|
_removeMessagesByType(0);
|
||||||
|
_removeMessagesByType(5);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 辅助方法:移除特定类型的消息
|
// 辅助方法:移除特定类型的消息
|
||||||
@ -266,7 +274,7 @@ class OverlayManager {
|
|||||||
if (message == null) {
|
if (message == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (message.type != 0 && message.type != 1) {
|
if (message.type != 0 && message.type != 1 && message.type != 5) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -289,7 +297,9 @@ class OverlayManager {
|
|||||||
void _removeActiveRoomMessage() {
|
void _removeActiveRoomMessage() {
|
||||||
final activeMessage = _currentMessage;
|
final activeMessage = _currentMessage;
|
||||||
if (activeMessage == null ||
|
if (activeMessage == null ||
|
||||||
(activeMessage.type != 0 && activeMessage.type != 1)) {
|
(activeMessage.type != 0 &&
|
||||||
|
activeMessage.type != 1 &&
|
||||||
|
activeMessage.type != 5)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_currentOverlayEntry?.remove();
|
_currentOverlayEntry?.remove();
|
||||||
|
|||||||
@ -196,8 +196,8 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
num mickIndex, {
|
num mickIndex, {
|
||||||
String? eventType,
|
String? eventType,
|
||||||
String? inviterId,
|
String? inviterId,
|
||||||
}) async {
|
}) async {
|
||||||
Map<String, dynamic> params = {};
|
Map<String, dynamic> params = {};
|
||||||
params["roomId"] = roomId;
|
params["roomId"] = roomId;
|
||||||
params["mickIndex"] = mickIndex;
|
params["mickIndex"] = mickIndex;
|
||||||
if (eventType != null) {
|
if (eventType != null) {
|
||||||
@ -262,12 +262,12 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
silentErrorToast
|
silentErrorToast
|
||||||
? const {BaseNetworkClient.silentErrorToastKey: true}
|
? const {BaseNetworkClient.silentErrorToastKey: true}
|
||||||
: null,
|
: null,
|
||||||
fromJson:
|
fromJson:
|
||||||
(json) =>
|
(json) =>
|
||||||
(json as List)
|
(json as List)
|
||||||
.map((e) => SocialChatUserProfile.fromJson(e))
|
.map((e) => socialChatUserProfileFromJsonWithVipHint(e)!)
|
||||||
.toList(),
|
.toList(),
|
||||||
);
|
);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -316,8 +316,8 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
String? roomId,
|
String? roomId,
|
||||||
SCGiveAwayGiftRoomAcceptsCmd? accepts,
|
SCGiveAwayGiftRoomAcceptsCmd? accepts,
|
||||||
String? dynamicContentId,
|
String? dynamicContentId,
|
||||||
}) async {
|
}) async {
|
||||||
Map<String, dynamic> params = {};
|
Map<String, dynamic> params = {};
|
||||||
if (roomId != null) {
|
if (roomId != null) {
|
||||||
params["roomId"] = roomId;
|
params["roomId"] = roomId;
|
||||||
}
|
}
|
||||||
@ -512,9 +512,10 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
bool? adminLockSeat,
|
bool? adminLockSeat,
|
||||||
bool? showHeartbeat,
|
bool? showHeartbeat,
|
||||||
bool? openKtvMode,
|
bool? openKtvMode,
|
||||||
String? roomSpecialMikeType,
|
String? roomSpecialMikeType,
|
||||||
}) async {
|
}) async {
|
||||||
Map<String, dynamic> params = {};
|
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
|
||||||
|
Map<String, dynamic> params = {};
|
||||||
params["roomId"] = roomId;
|
params["roomId"] = roomId;
|
||||||
if (touristMike != null) {
|
if (touristMike != null) {
|
||||||
params["touristMike"] = touristMike;
|
params["touristMike"] = touristMike;
|
||||||
@ -552,7 +553,7 @@ class SCChatRoomRepository implements SocialChatRoomRepository {
|
|||||||
fromJson: (json) => json as bool,
|
fromJson: (json) => json as bool,
|
||||||
);
|
);
|
||||||
|
|
||||||
Provider.of<RtmProvider>(context, listen: false).dispatchMessage(
|
rtmProvider.dispatchMessage(
|
||||||
Msg(
|
Msg(
|
||||||
groupId: roomAcount,
|
groupId: roomAcount,
|
||||||
msg: roomId,
|
msg: roomId,
|
||||||
|
|||||||
@ -14,7 +14,15 @@ import 'package:tancent_vap/widgets/vap_view.dart';
|
|||||||
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
|
||||||
|
|
||||||
class SCGiftVapSvgaManager {
|
class SCGiftVapSvgaManager {
|
||||||
static const int _maxPendingTaskCount = 18;
|
static const int giftEffectType = 0;
|
||||||
|
static const int entryEffectType = 1;
|
||||||
|
static const int giftEffectPriority = 100;
|
||||||
|
static const int entryEffectPriority = 1000;
|
||||||
|
static const int _maxPendingGiftTaskCount = 24;
|
||||||
|
static const int _maxPendingEntryTaskCount = 5;
|
||||||
|
static const int _maxConsecutiveEntryTasksBeforeGift = 2;
|
||||||
|
static const Duration _taskWatchdogTimeout = Duration(seconds: 20);
|
||||||
|
static const Duration _entryTaskTtl = Duration(seconds: 6);
|
||||||
Map<String, MovieEntity> videoItemCache = {};
|
Map<String, MovieEntity> videoItemCache = {};
|
||||||
static SCGiftVapSvgaManager? _inst;
|
static SCGiftVapSvgaManager? _inst;
|
||||||
static const int _maxPreloadConcurrency = 1;
|
static const int _maxPreloadConcurrency = 1;
|
||||||
@ -26,7 +34,10 @@ class SCGiftVapSvgaManager {
|
|||||||
|
|
||||||
factory SCGiftVapSvgaManager() => _inst ??= SCGiftVapSvgaManager._internal();
|
factory SCGiftVapSvgaManager() => _inst ??= SCGiftVapSvgaManager._internal();
|
||||||
|
|
||||||
final SCPriorityQueue<SCVapTask> _tq = SCPriorityQueue<SCVapTask>(
|
final SCPriorityQueue<SCVapTask> _giftQueue = SCPriorityQueue<SCVapTask>(
|
||||||
|
(a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法
|
||||||
|
);
|
||||||
|
final SCPriorityQueue<SCVapTask> _entryQueue = SCPriorityQueue<SCVapTask>(
|
||||||
(a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法
|
(a, b) => a.compareTo(b), // 调用 SCVapTask 的 compareTo 方法
|
||||||
);
|
);
|
||||||
VapController? _rgc;
|
VapController? _rgc;
|
||||||
@ -34,12 +45,14 @@ class SCGiftVapSvgaManager {
|
|||||||
bool _play = false;
|
bool _play = false;
|
||||||
bool _dis = false;
|
bool _dis = false;
|
||||||
SCVapTask? _currentTask;
|
SCVapTask? _currentTask;
|
||||||
|
Timer? _currentTaskWatchdogTimer;
|
||||||
final Queue<String> _preloadQueue = Queue<String>();
|
final Queue<String> _preloadQueue = Queue<String>();
|
||||||
final Set<String> _queuedPreloadPaths = <String>{};
|
final Set<String> _queuedPreloadPaths = <String>{};
|
||||||
final Map<String, Future<MovieEntity>> _svgaLoadTasks = {};
|
final Map<String, Future<MovieEntity>> _svgaLoadTasks = {};
|
||||||
final Map<String, Future<String>> _playablePathTasks = {};
|
final Map<String, Future<String>> _playablePathTasks = {};
|
||||||
final Map<String, String> _playablePathCache = {};
|
final Map<String, String> _playablePathCache = {};
|
||||||
int _activePreloadCount = 0;
|
int _activePreloadCount = 0;
|
||||||
|
int _consecutiveEntryTaskCount = 0;
|
||||||
|
|
||||||
bool _pause = false;
|
bool _pause = false;
|
||||||
|
|
||||||
@ -63,6 +76,26 @@ class SCGiftVapSvgaManager {
|
|||||||
return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga";
|
return SCPathUtils.getFileExtension(path).toLowerCase() == ".svga";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static int resolveEffectPriority({
|
||||||
|
int priority = 0,
|
||||||
|
int type = giftEffectType,
|
||||||
|
}) {
|
||||||
|
if (type == entryEffectType) {
|
||||||
|
return priority > entryEffectPriority ? priority : entryEffectPriority;
|
||||||
|
}
|
||||||
|
if (priority != 0) {
|
||||||
|
return priority;
|
||||||
|
}
|
||||||
|
return giftEffectPriority;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isEntryTask(SCVapTask task) => task.type == entryEffectType;
|
||||||
|
|
||||||
|
int get _pendingTaskCount => _giftQueue.length + _entryQueue.length;
|
||||||
|
|
||||||
|
String get _queueSummary =>
|
||||||
|
'entry=${_entryQueue.length} gift=${_giftQueue.length}';
|
||||||
|
|
||||||
bool _isControllerReady(SCVapTask task) {
|
bool _isControllerReady(SCVapTask task) {
|
||||||
if (_needsSvgaController(task.path)) {
|
if (_needsSvgaController(task.path)) {
|
||||||
return _rsc != null;
|
return _rsc != null;
|
||||||
@ -253,6 +286,96 @@ class SCGiftVapSvgaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _enqueueTask(SCVapTask task) {
|
||||||
|
if (_isEntryTask(task)) {
|
||||||
|
_dropExpiredEntryTasks();
|
||||||
|
_entryQueue.add(task);
|
||||||
|
_trimEntryQueue();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_giftQueue.add(task);
|
||||||
|
_trimGiftQueue();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _trimEntryQueue() {
|
||||||
|
while (_entryQueue.length > _maxPendingEntryTaskCount) {
|
||||||
|
final removedTask = _removeEntryOverflowTask();
|
||||||
|
_completeDroppedQueuedTask(removedTask, 'trim entry queue');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SCVapTask _removeEntryOverflowTask() {
|
||||||
|
final tasks = _entryQueue.unorderedElements.cast<SCVapTask>();
|
||||||
|
var candidate = tasks.first;
|
||||||
|
for (final task in tasks.skip(1)) {
|
||||||
|
if (task.priority < candidate.priority ||
|
||||||
|
(task.priority == candidate.priority &&
|
||||||
|
task.createdAt.isBefore(candidate.createdAt))) {
|
||||||
|
candidate = task;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_entryQueue.remove(candidate);
|
||||||
|
return candidate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _trimGiftQueue() {
|
||||||
|
while (_giftQueue.length > _maxPendingGiftTaskCount) {
|
||||||
|
final removedTask = _giftQueue.removeLast();
|
||||||
|
_completeDroppedQueuedTask(removedTask, 'trim gift queue');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _dropExpiredEntryTasks() {
|
||||||
|
if (_entryQueue.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final now = DateTime.now();
|
||||||
|
final expiredTasks =
|
||||||
|
_entryQueue.unorderedElements
|
||||||
|
.cast<SCVapTask>()
|
||||||
|
.where((task) => now.difference(task.createdAt) > _entryTaskTtl)
|
||||||
|
.toList();
|
||||||
|
for (final task in expiredTasks) {
|
||||||
|
_entryQueue.remove(task);
|
||||||
|
_completeDroppedQueuedTask(
|
||||||
|
task,
|
||||||
|
'drop expired entry task ageMs=${now.difference(task.createdAt).inMilliseconds}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _completeDroppedQueuedTask(SCVapTask task, String reason) {
|
||||||
|
SCRoomEffectScheduler().completeHighCostTask(debugLabel: task.path);
|
||||||
|
_log(
|
||||||
|
'$reason path=${task.path} priority=${task.priority} '
|
||||||
|
'type=${task.type} queue=$_queueSummary',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SCVapTask? _peekNextQueuedTask() {
|
||||||
|
_dropExpiredEntryTasks();
|
||||||
|
if (_entryQueue.isNotEmpty) {
|
||||||
|
final shouldYieldToGift =
|
||||||
|
_giftQueue.isNotEmpty &&
|
||||||
|
_consecutiveEntryTaskCount >= _maxConsecutiveEntryTasksBeforeGift;
|
||||||
|
if (!shouldYieldToGift) {
|
||||||
|
return _entryQueue.first;
|
||||||
|
}
|
||||||
|
return _giftQueue.first;
|
||||||
|
}
|
||||||
|
return _giftQueue.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeQueuedTask(SCVapTask task) {
|
||||||
|
if (_isEntryTask(task)) {
|
||||||
|
_entryQueue.remove(task);
|
||||||
|
_consecutiveEntryTaskCount++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_giftQueue.remove(task);
|
||||||
|
_consecutiveEntryTaskCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
void _scheduleNextTask({Duration delay = Duration.zero}) {
|
void _scheduleNextTask({Duration delay = Duration.zero}) {
|
||||||
if (_dis) {
|
if (_dis) {
|
||||||
return;
|
return;
|
||||||
@ -268,8 +391,14 @@ class SCGiftVapSvgaManager {
|
|||||||
if (_dis) {
|
if (_dis) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final finishedTask = _currentTask;
|
||||||
|
if (!_play && finishedTask == null) {
|
||||||
|
_scheduleNextTask(delay: delay);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_cancelCurrentTaskWatchdog();
|
||||||
SCRoomEffectScheduler().completeHighCostTask(
|
SCRoomEffectScheduler().completeHighCostTask(
|
||||||
debugLabel: _currentTask?.path,
|
debugLabel: finishedTask?.path,
|
||||||
);
|
);
|
||||||
_play = false;
|
_play = false;
|
||||||
_currentTask = null;
|
_currentTask = null;
|
||||||
@ -288,7 +417,7 @@ class SCGiftVapSvgaManager {
|
|||||||
_rgc = vapController;
|
_rgc = vapController;
|
||||||
_log(
|
_log(
|
||||||
'bindVapCtrl hasVapCtrl=${_rgc != null} '
|
'bindVapCtrl hasVapCtrl=${_rgc != null} '
|
||||||
'hasSvgaCtrl=${_rsc != null} queue=${_tq.length} mute=$_mute',
|
'hasSvgaCtrl=${_rsc != null} queue=$_queueSummary mute=$_mute',
|
||||||
);
|
);
|
||||||
_rgc?.setAnimListener(
|
_rgc?.setAnimListener(
|
||||||
onVideoStart: () {
|
onVideoStart: () {
|
||||||
@ -314,18 +443,13 @@ class SCGiftVapSvgaManager {
|
|||||||
_rsc = svgaController;
|
_rsc = svgaController;
|
||||||
_log(
|
_log(
|
||||||
'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} '
|
'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} '
|
||||||
'hasVapCtrl=${_rgc != null} queue=${_tq.length}',
|
'hasVapCtrl=${_rgc != null} queue=$_queueSummary',
|
||||||
);
|
);
|
||||||
_rsc?.addStatusListener((AnimationStatus status) {
|
_rsc?.addStatusListener((AnimationStatus status) {
|
||||||
if (status.isCompleted) {
|
if (status.isCompleted) {
|
||||||
_log('svga completed path=${_currentTask?.path}');
|
_log('svga completed path=${_currentTask?.path}');
|
||||||
_rsc?.reset();
|
_rsc?.reset();
|
||||||
SCRoomEffectScheduler().completeHighCostTask(
|
_finishCurrentTask();
|
||||||
debugLabel: _currentTask?.path,
|
|
||||||
);
|
|
||||||
_play = false;
|
|
||||||
_currentTask = null;
|
|
||||||
_pn();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
_scheduleNextTask();
|
_scheduleNextTask();
|
||||||
@ -337,18 +461,22 @@ class SCGiftVapSvgaManager {
|
|||||||
String path, {
|
String path, {
|
||||||
int priority = 0,
|
int priority = 0,
|
||||||
Map<String, VAPContent>? customResources,
|
Map<String, VAPContent>? customResources,
|
||||||
int type = 0,
|
int type = giftEffectType,
|
||||||
}) {
|
}) {
|
||||||
if (path.isEmpty) {
|
if (path.isEmpty) {
|
||||||
_log('play ignored because path is empty');
|
_log('play ignored because path is empty');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final resolvedPriority = resolveEffectPriority(
|
||||||
|
priority: priority,
|
||||||
|
type: type,
|
||||||
|
);
|
||||||
_log(
|
_log(
|
||||||
'play request path=$path ext=${SCPathUtils.getFileExtension(path)} '
|
'play request path=$path ext=${SCPathUtils.getFileExtension(path)} '
|
||||||
'priority=$priority type=$type '
|
'priority=$priority resolvedPriority=$resolvedPriority type=$type '
|
||||||
'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim} '
|
'sdkInt=${SCGlobalConfig.sdkInt} maxSdkNoAnim=${SCGlobalConfig.maxSdkNoAnim} '
|
||||||
'lowPerformance=${SCGlobalConfig.isLowPerformanceDevice} '
|
'lowPerformance=${SCGlobalConfig.isLowPerformanceDevice} '
|
||||||
'disposed=$_dis playing=$_play queueBefore=${_tq.length} '
|
'disposed=$_dis playing=$_play queueBefore=$_queueSummary '
|
||||||
'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null}',
|
'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null}',
|
||||||
);
|
);
|
||||||
if (SCGlobalConfig.allowsHighCostAnimations) {
|
if (SCGlobalConfig.allowsHighCostAnimations) {
|
||||||
@ -358,31 +486,15 @@ class SCGiftVapSvgaManager {
|
|||||||
}
|
}
|
||||||
final task = SCVapTask(
|
final task = SCVapTask(
|
||||||
path: path,
|
path: path,
|
||||||
priority: priority,
|
type: type,
|
||||||
|
priority: resolvedPriority,
|
||||||
customResources: customResources,
|
customResources: customResources,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (_tq.length >= _maxPendingTaskCount && priority <= 0) {
|
|
||||||
_log(
|
|
||||||
'drop play request because queue is full path=$path '
|
|
||||||
'priority=$priority queue=${_tq.length}',
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_tq.add(task);
|
|
||||||
SCRoomEffectScheduler().registerHighCostTaskQueued(debugLabel: path);
|
SCRoomEffectScheduler().registerHighCostTaskQueued(debugLabel: path);
|
||||||
|
_enqueueTask(task);
|
||||||
unawaited(preload(path));
|
unawaited(preload(path));
|
||||||
while (_tq.length > _maxPendingTaskCount) {
|
_log('task enqueued path=$path queueAfter=$_queueSummary');
|
||||||
final removedTask = _tq.removeLast();
|
|
||||||
SCRoomEffectScheduler().completeHighCostTask(
|
|
||||||
debugLabel: removedTask.path,
|
|
||||||
);
|
|
||||||
_log(
|
|
||||||
'trim queued task path=${removedTask.path} '
|
|
||||||
'priority=${removedTask.priority} queue=${_tq.length}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_log('task enqueued path=$path queueAfter=${_tq.length}');
|
|
||||||
if (!_play) {
|
if (!_play) {
|
||||||
_pn();
|
_pn();
|
||||||
}
|
}
|
||||||
@ -398,32 +510,34 @@ class SCGiftVapSvgaManager {
|
|||||||
// 播放下一个任务
|
// 播放下一个任务
|
||||||
Future<void> _pn() async {
|
Future<void> _pn() async {
|
||||||
if (_pause) {
|
if (_pause) {
|
||||||
_log('skip _pn because paused queue=${_tq.length}');
|
_log('skip _pn because paused queue=$_queueSummary');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_dis || _tq.isEmpty || _play) return;
|
if (_dis || _pendingTaskCount == 0 || _play) return;
|
||||||
|
|
||||||
final task = _tq.first;
|
final task = _peekNextQueuedTask();
|
||||||
if (task == null || !_isControllerReady(task)) {
|
if (task == null || !_isControllerReady(task)) {
|
||||||
_log(
|
_log(
|
||||||
'controller not ready for path=${task?.path} '
|
'controller not ready for path=${task?.path} '
|
||||||
'needSvga=${task != null ? _needsSvgaController(task.path) : "unknown"} '
|
'needSvga=${task != null ? _needsSvgaController(task.path) : "unknown"} '
|
||||||
'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null} '
|
'hasSvgaCtrl=${_rsc != null} hasVapCtrl=${_rgc != null} '
|
||||||
'queue=${_tq.length}',
|
'queue=$_queueSummary',
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_tq.removeFirst();
|
_removeQueuedTask(task);
|
||||||
_play = true;
|
_play = true;
|
||||||
_currentTask = task;
|
_currentTask = task;
|
||||||
|
_armCurrentTaskWatchdog(task);
|
||||||
try {
|
try {
|
||||||
final pathType = SCPathUtils.getPathType(task.path);
|
final pathType = SCPathUtils.getPathType(task.path);
|
||||||
_log(
|
_log(
|
||||||
'start task path=${task.path} '
|
'start task path=${task.path} '
|
||||||
'pathType=$pathType '
|
'pathType=$pathType '
|
||||||
'queueRemaining=${_tq.length} '
|
'queueRemaining=$_queueSummary '
|
||||||
'needSvga=${_needsSvgaController(task.path)}',
|
'needSvga=${_needsSvgaController(task.path)} '
|
||||||
|
'type=${task.type} consecutiveEntry=$_consecutiveEntryTaskCount',
|
||||||
);
|
);
|
||||||
if (pathType == PathType.asset) {
|
if (pathType == PathType.asset) {
|
||||||
await _pa(task);
|
await _pa(task);
|
||||||
@ -447,6 +561,9 @@ class SCGiftVapSvgaManager {
|
|||||||
if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
|
if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
|
||||||
_log('play asset svga path=${task.path}');
|
_log('play asset svga path=${task.path}');
|
||||||
final entity = await _loadSvgaEntity(task.path);
|
final entity = await _loadSvgaEntity(task.path);
|
||||||
|
if (!_isCurrentTask(task)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_log('use prepared asset svga path=${task.path}');
|
_log('use prepared asset svga path=${task.path}');
|
||||||
_rsc?.videoItem = entity;
|
_rsc?.videoItem = entity;
|
||||||
_rsc?.reset();
|
_rsc?.reset();
|
||||||
@ -470,6 +587,9 @@ class SCGiftVapSvgaManager {
|
|||||||
}
|
}
|
||||||
if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
|
if (SCPathUtils.getFileExtension(task.path).toLowerCase() == ".svga") {
|
||||||
final entity = await _loadSvgaEntity(task.path);
|
final entity = await _loadSvgaEntity(task.path);
|
||||||
|
if (!_isCurrentTask(task)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_log('play local svga file path=${task.path}');
|
_log('play local svga file path=${task.path}');
|
||||||
_rsc?.videoItem = entity;
|
_rsc?.videoItem = entity;
|
||||||
_rsc?.reset();
|
_rsc?.reset();
|
||||||
@ -483,11 +603,17 @@ class SCGiftVapSvgaManager {
|
|||||||
}
|
}
|
||||||
_log('play local vap/mp4 file path=${task.path}');
|
_log('play local vap/mp4 file path=${task.path}');
|
||||||
await _rgc?.setMute(_mute);
|
await _rgc?.setMute(_mute);
|
||||||
|
if (!_isCurrentTask(task)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (task.customResources != null) {
|
if (task.customResources != null) {
|
||||||
task.customResources?.forEach((k, v) async {
|
task.customResources?.forEach((k, v) async {
|
||||||
await _rgc?.setVapTagContent(k, v);
|
await _rgc?.setVapTagContent(k, v);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (!_isCurrentTask(task)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
await _rgc!.playFile(task.path);
|
await _rgc!.playFile(task.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -504,6 +630,9 @@ class SCGiftVapSvgaManager {
|
|||||||
_finishCurrentTask();
|
_finishCurrentTask();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!_isCurrentTask(task)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
_log('use prepared network svga path=${task.path}');
|
_log('use prepared network svga path=${task.path}');
|
||||||
_rsc?.videoItem = entity;
|
_rsc?.videoItem = entity;
|
||||||
_rsc?.reset();
|
_rsc?.reset();
|
||||||
@ -512,11 +641,15 @@ class SCGiftVapSvgaManager {
|
|||||||
} else {
|
} else {
|
||||||
_log('download network vap/mp4 path=${task.path}');
|
_log('download network vap/mp4 path=${task.path}');
|
||||||
final playablePath = await _ensurePlayableFilePath(task.path);
|
final playablePath = await _ensurePlayableFilePath(task.path);
|
||||||
|
if (!_isCurrentTask(task)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (!_dis) {
|
if (!_dis) {
|
||||||
_log('use prepared network vap/mp4 local path=$playablePath');
|
_log('use prepared network vap/mp4 local path=$playablePath');
|
||||||
await _pf(
|
await _pf(
|
||||||
SCVapTask(
|
SCVapTask(
|
||||||
path: playablePath,
|
path: playablePath,
|
||||||
|
type: task.type,
|
||||||
priority: task.priority,
|
priority: task.priority,
|
||||||
customResources: task.customResources,
|
customResources: task.customResources,
|
||||||
),
|
),
|
||||||
@ -533,25 +666,52 @@ class SCGiftVapSvgaManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isCurrentTask(SCVapTask task) {
|
||||||
|
return !_dis && _play && identical(_currentTask, task);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _armCurrentTaskWatchdog(SCVapTask task) {
|
||||||
|
_cancelCurrentTaskWatchdog();
|
||||||
|
_currentTaskWatchdogTimer = Timer(_taskWatchdogTimeout, () {
|
||||||
|
if (!_isCurrentTask(task)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_log('task watchdog timeout path=${task.path}');
|
||||||
|
_rgc?.stop();
|
||||||
|
_rsc?.stop();
|
||||||
|
_rsc?.reset();
|
||||||
|
_rsc?.videoItem = null;
|
||||||
|
_finishCurrentTask(delay: const Duration(milliseconds: 50));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelCurrentTaskWatchdog() {
|
||||||
|
_currentTaskWatchdogTimer?.cancel();
|
||||||
|
_currentTaskWatchdogTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
//暂停动画播放
|
//暂停动画播放
|
||||||
void pauseAnim() {
|
void pauseAnim() {
|
||||||
_pause = true;
|
_pause = true;
|
||||||
_log('pauseAnim queue=${_tq.length}');
|
_log('pauseAnim queue=$_queueSummary');
|
||||||
}
|
}
|
||||||
|
|
||||||
//恢复动画播放
|
//恢复动画播放
|
||||||
void resumeAnim() {
|
void resumeAnim() {
|
||||||
_pause = false;
|
_pause = false;
|
||||||
_log('resumeAnim queue=${_tq.length}');
|
_log('resumeAnim queue=$_queueSummary');
|
||||||
_pn();
|
_pn();
|
||||||
}
|
}
|
||||||
|
|
||||||
void stopPlayback() {
|
void stopPlayback() {
|
||||||
_log('stopPlayback queue=${_tq.length} currentPath=${_currentTask?.path}');
|
_log('stopPlayback queue=$_queueSummary currentPath=${_currentTask?.path}');
|
||||||
_play = false;
|
_play = false;
|
||||||
_currentTask = null;
|
_currentTask = null;
|
||||||
_pause = false;
|
_pause = false;
|
||||||
_tq.clear();
|
_consecutiveEntryTaskCount = 0;
|
||||||
|
_cancelCurrentTaskWatchdog();
|
||||||
|
_giftQueue.clear();
|
||||||
|
_entryQueue.clear();
|
||||||
_preloadQueue.clear();
|
_preloadQueue.clear();
|
||||||
_queuedPreloadPaths.clear();
|
_queuedPreloadPaths.clear();
|
||||||
_activePreloadCount = 0;
|
_activePreloadCount = 0;
|
||||||
@ -564,7 +724,7 @@ class SCGiftVapSvgaManager {
|
|||||||
|
|
||||||
// 释放资源
|
// 释放资源
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_log('dispose queue=${_tq.length} currentPath=${_currentTask?.path}');
|
_log('dispose queue=$_queueSummary currentPath=${_currentTask?.path}');
|
||||||
_dis = true;
|
_dis = true;
|
||||||
stopPlayback();
|
stopPlayback();
|
||||||
_svgaLoadTasks.clear();
|
_svgaLoadTasks.clear();
|
||||||
@ -580,7 +740,7 @@ class SCGiftVapSvgaManager {
|
|||||||
// 清除所有任务
|
// 清除所有任务
|
||||||
void clearTasks() {
|
void clearTasks() {
|
||||||
_log(
|
_log(
|
||||||
'clearTasks queueBefore=${_tq.length} currentPath=${_currentTask?.path}',
|
'clearTasks queueBefore=$_queueSummary currentPath=${_currentTask?.path}',
|
||||||
);
|
);
|
||||||
stopPlayback();
|
stopPlayback();
|
||||||
}
|
}
|
||||||
@ -594,6 +754,7 @@ class SCVapTask implements Comparable<SCVapTask> {
|
|||||||
final int type;
|
final int type;
|
||||||
final int priority;
|
final int priority;
|
||||||
final Map<String, VAPContent>? customResources;
|
final Map<String, VAPContent>? customResources;
|
||||||
|
final DateTime createdAt;
|
||||||
final int _seq;
|
final int _seq;
|
||||||
|
|
||||||
static int _nextSeq = 0;
|
static int _nextSeq = 0;
|
||||||
@ -603,7 +764,9 @@ class SCVapTask implements Comparable<SCVapTask> {
|
|||||||
this.priority = 0,
|
this.priority = 0,
|
||||||
this.customResources,
|
this.customResources,
|
||||||
this.type = 0,
|
this.type = 0,
|
||||||
}) : _seq = _nextSeq++;
|
DateTime? createdAt,
|
||||||
|
}) : createdAt = createdAt ?? DateTime.now(),
|
||||||
|
_seq = _nextSeq++;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int compareTo(SCVapTask other) {
|
int compareTo(SCVapTask other) {
|
||||||
@ -618,7 +781,7 @@ class SCVapTask implements Comparable<SCVapTask> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'SCVapTask{path: $path, priority: $priority, seq: $_seq}';
|
return 'SCVapTask{path: $path, type: $type, priority: $priority, seq: $_seq}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -649,6 +812,8 @@ class SCPriorityQueue<E> {
|
|||||||
|
|
||||||
void clear() => _els.clear();
|
void clear() => _els.clear();
|
||||||
|
|
||||||
|
bool remove(E element) => _els.remove(element);
|
||||||
|
|
||||||
E removeLast() {
|
E removeLast() {
|
||||||
if (isEmpty) throw StateError("No elements");
|
if (isEmpty) throw StateError("No elements");
|
||||||
return _els.removeLast();
|
return _els.removeLast();
|
||||||
|
|||||||
@ -90,7 +90,21 @@ class SCPathUtils {
|
|||||||
|
|
||||||
/// 获取路径的文件扩展名
|
/// 获取路径的文件扩展名
|
||||||
static String getFileExtension(String filePath) {
|
static String getFileExtension(String filePath) {
|
||||||
return path.extension(filePath).toLowerCase();
|
final value = filePath.trim();
|
||||||
|
if (value.isEmpty) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
final uri = Uri.tryParse(value);
|
||||||
|
if (uri != null && (uri.scheme.isNotEmpty || uri.host.isNotEmpty)) {
|
||||||
|
final uriPathExtension = path.extension(uri.path).toLowerCase();
|
||||||
|
if (uriPathExtension.isNotEmpty) {
|
||||||
|
return uriPathExtension;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalizedValue = value.split('?').first.split('#').first;
|
||||||
|
return path.extension(normalizedValue).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 从网络地址中提取文件名
|
/// 从网络地址中提取文件名
|
||||||
|
|||||||
@ -9,6 +9,9 @@ import 'package:yumi/ui_kit/components/text/sc_text.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:yumi/services/audio/rtc_manager.dart';
|
import 'package:yumi/services/audio/rtc_manager.dart';
|
||||||
import 'package:yumi/services/audio/rtm_manager.dart';
|
import 'package:yumi/services/audio/rtm_manager.dart';
|
||||||
|
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
|
||||||
|
import 'package:yumi/shared/data_sources/models/message/sc_floating_message.dart';
|
||||||
|
import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart';
|
||||||
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
import 'package:yumi/shared/tools/sc_network_image_utils.dart';
|
||||||
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
import 'package:yumi/ui_kit/widgets/room/room_msg_item.dart';
|
||||||
@ -40,6 +43,7 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
|||||||
if (!_effectsEnabled) {
|
if (!_effectsEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
_enqueueVipEntryFloatingMessage(msg);
|
||||||
unawaited(
|
unawaited(
|
||||||
warmImageResource(
|
warmImageResource(
|
||||||
msg.user?.userAvatar ?? "",
|
msg.user?.userAvatar ?? "",
|
||||||
@ -59,6 +63,72 @@ class _RoomAnimationQueueScreenState extends State<RoomAnimationQueueScreen> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _enqueueVipEntryFloatingMessage(Msg msg) {
|
||||||
|
final floatPicture = msg.user?.getFloatPicture();
|
||||||
|
final floatPictureUrl = _resolveVipFloatPictureUrl(floatPicture);
|
||||||
|
final floatPictureFallbackUrl = _resolveVipFloatPictureFallbackUrl(
|
||||||
|
floatPicture,
|
||||||
|
);
|
||||||
|
if (floatPictureUrl.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final roomId =
|
||||||
|
Provider.of<RtcProvider>(
|
||||||
|
context,
|
||||||
|
listen: false,
|
||||||
|
).currenRoom?.roomProfile?.roomProfile?.id?.trim() ??
|
||||||
|
"";
|
||||||
|
if (roomId.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugPrint(
|
||||||
|
'[VIP][RoomFloat] enqueue entry float '
|
||||||
|
'userId=${msg.user?.id} roomId=$roomId resourceId=${floatPicture?.id} '
|
||||||
|
'url=$floatPictureUrl',
|
||||||
|
);
|
||||||
|
OverlayManager().addMessage(
|
||||||
|
SCFloatingMessage(
|
||||||
|
type: 5,
|
||||||
|
userId: msg.user?.id,
|
||||||
|
roomId: roomId,
|
||||||
|
userAvatarUrl: msg.user?.userAvatar,
|
||||||
|
userName: msg.user?.userNickname,
|
||||||
|
giftUrl: floatPictureUrl,
|
||||||
|
fallbackUrl: floatPictureFallbackUrl,
|
||||||
|
priority: 900,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _resolveVipFloatPictureUrl(PropsResources? resource) {
|
||||||
|
return _firstNonBlank([
|
||||||
|
resource?.sourceUrl,
|
||||||
|
resource?.roomSendCoverUrl,
|
||||||
|
resource?.roomOpenedUrl,
|
||||||
|
resource?.cover,
|
||||||
|
resource?.roomNotOpenedUrl,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _resolveVipFloatPictureFallbackUrl(PropsResources? resource) {
|
||||||
|
return _firstNonBlank([
|
||||||
|
resource?.roomSendCoverUrl,
|
||||||
|
resource?.roomOpenedUrl,
|
||||||
|
resource?.cover,
|
||||||
|
resource?.roomNotOpenedUrl,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _firstNonBlank(Iterable<String?> values) {
|
||||||
|
for (final value in values) {
|
||||||
|
final text = value?.trim();
|
||||||
|
if (text != null && text.isNotEmpty) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
void _addToQueue(Msg msg) {
|
void _addToQueue(Msg msg) {
|
||||||
if (!_effectsEnabled || !mounted) {
|
if (!_effectsEnabled || !mounted) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
|
import 'dart:async';
|
||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -16,6 +17,7 @@ class RoomGiftSeatFlightRequest {
|
|||||||
this.beginSize = 96,
|
this.beginSize = 96,
|
||||||
this.endSize = 34,
|
this.endSize = 34,
|
||||||
this.queueTag,
|
this.queueTag,
|
||||||
|
this.batchTag,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String imagePath;
|
final String imagePath;
|
||||||
@ -25,6 +27,7 @@ class RoomGiftSeatFlightRequest {
|
|||||||
final double beginSize;
|
final double beginSize;
|
||||||
final double endSize;
|
final double endSize;
|
||||||
final String? queueTag;
|
final String? queueTag;
|
||||||
|
final String? batchTag;
|
||||||
}
|
}
|
||||||
|
|
||||||
class RoomGiftSeatFlightController {
|
class RoomGiftSeatFlightController {
|
||||||
@ -121,6 +124,7 @@ class RoomGiftSeatFlightController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final queueTag = request.queueTag?.trim();
|
final queueTag = request.queueTag?.trim();
|
||||||
|
final batchTag = request.batchTag?.trim();
|
||||||
return RoomGiftSeatFlightRequest(
|
return RoomGiftSeatFlightRequest(
|
||||||
imagePath: imagePath,
|
imagePath: imagePath,
|
||||||
targetUserId: targetUserId,
|
targetUserId: targetUserId,
|
||||||
@ -129,6 +133,7 @@ class RoomGiftSeatFlightController {
|
|||||||
beginSize: request.beginSize,
|
beginSize: request.beginSize,
|
||||||
endSize: request.endSize,
|
endSize: request.endSize,
|
||||||
queueTag: queueTag == null || queueTag.isEmpty ? null : queueTag,
|
queueTag: queueTag == null || queueTag.isEmpty ? null : queueTag,
|
||||||
|
batchTag: batchTag == null || batchTag.isEmpty ? null : batchTag,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -216,12 +221,12 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
final GlobalKey _overlayKey = GlobalKey();
|
final GlobalKey _overlayKey = GlobalKey();
|
||||||
|
|
||||||
late final AnimationController _controller;
|
late final AnimationController _controller;
|
||||||
|
Timer? _activeFlightWatchdogTimer;
|
||||||
|
|
||||||
RoomGiftSeatFlightRequest? _centerRequest;
|
RoomGiftSeatFlightRequest? _centerRequest;
|
||||||
ImageProvider<Object>? _centerImageProvider;
|
ImageProvider<Object>? _centerImageProvider;
|
||||||
RoomGiftSeatFlightRequest? _activeRequest;
|
List<_ActiveRoomGiftSeatFlight> _activeFlights =
|
||||||
ImageProvider<Object>? _activeImageProvider;
|
const <_ActiveRoomGiftSeatFlight>[];
|
||||||
Offset? _activeTargetOffset;
|
|
||||||
bool _isPlaying = false;
|
bool _isPlaying = false;
|
||||||
int _sessionToken = 0;
|
int _sessionToken = 0;
|
||||||
|
|
||||||
@ -248,6 +253,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
widget.controller._detach(this);
|
widget.controller._detach(this);
|
||||||
|
_cancelActiveFlightWatchdog();
|
||||||
_controller.dispose();
|
_controller.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@ -283,14 +289,13 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
void _clear() {
|
void _clear() {
|
||||||
_sessionToken += 1;
|
_sessionToken += 1;
|
||||||
_queue.clear();
|
_queue.clear();
|
||||||
|
_cancelActiveFlightWatchdog();
|
||||||
_controller.stop();
|
_controller.stop();
|
||||||
_controller.reset();
|
_controller.reset();
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
_centerRequest = null;
|
_centerRequest = null;
|
||||||
_centerImageProvider = null;
|
_centerImageProvider = null;
|
||||||
_activeRequest = null;
|
_activeFlights = const <_ActiveRoomGiftSeatFlight>[];
|
||||||
_activeImageProvider = null;
|
|
||||||
_activeTargetOffset = null;
|
|
||||||
_imageProviderCache.clear();
|
_imageProviderCache.clear();
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
return;
|
return;
|
||||||
@ -298,17 +303,17 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
setState(() {
|
setState(() {
|
||||||
_centerRequest = null;
|
_centerRequest = null;
|
||||||
_centerImageProvider = null;
|
_centerImageProvider = null;
|
||||||
_activeRequest = null;
|
_activeFlights = const <_ActiveRoomGiftSeatFlight>[];
|
||||||
_activeImageProvider = null;
|
|
||||||
_activeTargetOffset = null;
|
|
||||||
_imageProviderCache.clear();
|
_imageProviderCache.clear();
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _hasTrackedRequests(String queueTag) {
|
bool _hasTrackedRequests(String queueTag) {
|
||||||
if ((_activeRequest?.queueTag ?? '').trim() == queueTag) {
|
for (final activeFlight in _activeFlights) {
|
||||||
return true;
|
if ((activeFlight.request.queueTag ?? '').trim() == queueTag) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (final queuedRequest in _queue) {
|
for (final queuedRequest in _queue) {
|
||||||
if ((queuedRequest.request.queueTag ?? '').trim() == queueTag) {
|
if ((queuedRequest.request.queueTag ?? '').trim() == queueTag) {
|
||||||
@ -334,8 +339,10 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
|
|
||||||
int _countTrackedRequests(String queueTag) {
|
int _countTrackedRequests(String queueTag) {
|
||||||
var count = 0;
|
var count = 0;
|
||||||
if ((_activeRequest?.queueTag ?? '').trim() == queueTag) {
|
for (final activeFlight in _activeFlights) {
|
||||||
count += 1;
|
if ((activeFlight.request.queueTag ?? '').trim() == queueTag) {
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (final queuedRequest in _queue) {
|
for (final queuedRequest in _queue) {
|
||||||
if ((queuedRequest.request.queueTag ?? '').trim() == queueTag) {
|
if ((queuedRequest.request.queueTag ?? '').trim() == queueTag) {
|
||||||
@ -361,6 +368,33 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<_QueuedRoomGiftSeatFlightRequest> _removeNextQueuedBatch() {
|
||||||
|
if (_queue.isEmpty) {
|
||||||
|
return const <_QueuedRoomGiftSeatFlightRequest>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final firstRequest = _queue.removeFirst();
|
||||||
|
final batchTag = (firstRequest.request.batchTag ?? '').trim();
|
||||||
|
if (batchTag.isEmpty) {
|
||||||
|
return <_QueuedRoomGiftSeatFlightRequest>[firstRequest];
|
||||||
|
}
|
||||||
|
|
||||||
|
final batch = <_QueuedRoomGiftSeatFlightRequest>[firstRequest];
|
||||||
|
while (_queue.isNotEmpty &&
|
||||||
|
(_queue.first.request.batchTag ?? '').trim() == batchTag) {
|
||||||
|
batch.add(_queue.removeFirst());
|
||||||
|
}
|
||||||
|
return batch;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _requeueBatchAtFront(
|
||||||
|
List<_QueuedRoomGiftSeatFlightRequest> queuedBatch,
|
||||||
|
) {
|
||||||
|
for (final queuedRequest in queuedBatch.reversed) {
|
||||||
|
_queue.addFirst(queuedRequest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _scheduleNextAnimation() {
|
void _scheduleNextAnimation() {
|
||||||
if (_isPlaying || _queue.isEmpty || !mounted) {
|
if (_isPlaying || _queue.isEmpty || !mounted) {
|
||||||
return;
|
return;
|
||||||
@ -381,78 +415,131 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
}
|
}
|
||||||
|
|
||||||
final activeSessionToken = _sessionToken;
|
final activeSessionToken = _sessionToken;
|
||||||
final queuedRequest = _queue.removeFirst();
|
final queuedBatch = _removeNextQueuedBatch();
|
||||||
final targetOffset = _resolveTargetOffset(
|
final activeFlights = <_ActiveRoomGiftSeatFlight>[];
|
||||||
queuedRequest.request.targetUserId,
|
final unresolvedBatch = <_QueuedRoomGiftSeatFlightRequest>[];
|
||||||
|
|
||||||
|
for (final queuedRequest in queuedBatch) {
|
||||||
|
final targetOffset = _resolveTargetOffset(
|
||||||
|
queuedRequest.request.targetUserId,
|
||||||
|
);
|
||||||
|
if (targetOffset == null) {
|
||||||
|
unresolvedBatch.add(queuedRequest);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final imageProvider = _resolveImageProvider(
|
||||||
|
queuedRequest.request.imagePath,
|
||||||
|
request: queuedRequest.request,
|
||||||
|
);
|
||||||
|
activeFlights.add(
|
||||||
|
_ActiveRoomGiftSeatFlight(
|
||||||
|
request: queuedRequest.request,
|
||||||
|
imageProvider: imageProvider,
|
||||||
|
targetOffset: targetOffset,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final shouldRetryBatch = unresolvedBatch.any(
|
||||||
|
(queuedRequest) => queuedRequest.retryCount < 6,
|
||||||
);
|
);
|
||||||
if (targetOffset == null) {
|
if (shouldRetryBatch) {
|
||||||
if (queuedRequest.retryCount < 6) {
|
final retryBatch =
|
||||||
|
queuedBatch
|
||||||
|
.map(
|
||||||
|
(queuedRequest) => queuedRequest.copyWith(
|
||||||
|
retryCount: queuedRequest.retryCount + 1,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
if (retryBatch.isNotEmpty) {
|
||||||
Future.delayed(const Duration(milliseconds: 120), () {
|
Future.delayed(const Duration(milliseconds: 120), () {
|
||||||
if (!mounted || activeSessionToken != _sessionToken) {
|
if (!mounted || activeSessionToken != _sessionToken) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_queue.addFirst(
|
_requeueBatchAtFront(retryBatch);
|
||||||
queuedRequest.copyWith(retryCount: queuedRequest.retryCount + 1),
|
|
||||||
);
|
|
||||||
_scheduleNextAnimation();
|
_scheduleNextAnimation();
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
_syncIdleCenterVisual();
|
|
||||||
_scheduleNextAnimation();
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final imageProvider = _resolveImageProvider(
|
if (activeFlights.isEmpty) {
|
||||||
queuedRequest.request.imagePath,
|
_syncIdleCenterVisual();
|
||||||
);
|
_scheduleNextAnimation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_isPlaying = true;
|
_isPlaying = true;
|
||||||
_controller.duration = queuedRequest.request.flightDuration;
|
final centerFlight = activeFlights.first;
|
||||||
|
_controller.duration = centerFlight.request.flightDuration;
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_centerRequest = queuedRequest.request;
|
_centerRequest = centerFlight.request;
|
||||||
_centerImageProvider = imageProvider;
|
_centerImageProvider = centerFlight.imageProvider;
|
||||||
_activeRequest = queuedRequest.request;
|
_activeFlights = activeFlights;
|
||||||
_activeTargetOffset = targetOffset;
|
|
||||||
_activeImageProvider = imageProvider;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await precacheImage(
|
await Future.wait(
|
||||||
imageProvider,
|
activeFlights.map(
|
||||||
context,
|
(flight) => precacheImage(
|
||||||
).timeout(const Duration(milliseconds: 250));
|
flight.imageProvider,
|
||||||
|
context,
|
||||||
|
).timeout(const Duration(milliseconds: 250)),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (_) {}
|
} catch (_) {}
|
||||||
|
|
||||||
if (!mounted ||
|
if (!mounted ||
|
||||||
activeSessionToken != _sessionToken ||
|
activeSessionToken != _sessionToken ||
|
||||||
_activeRequest != queuedRequest.request) {
|
!identical(_activeFlights, activeFlights)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_controller.reset();
|
_controller.reset();
|
||||||
|
_armActiveFlightWatchdog(centerFlight.request.flightDuration);
|
||||||
_controller.forward();
|
_controller.forward();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _finishCurrentAnimation() {
|
void _finishCurrentAnimation() {
|
||||||
|
_cancelActiveFlightWatchdog();
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
_activeRequest = null;
|
_activeFlights = const <_ActiveRoomGiftSeatFlight>[];
|
||||||
_activeImageProvider = null;
|
|
||||||
_activeTargetOffset = null;
|
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
_syncIdleCenterVisual();
|
_syncIdleCenterVisual();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setState(() {
|
setState(() {
|
||||||
_activeRequest = null;
|
_activeFlights = const <_ActiveRoomGiftSeatFlight>[];
|
||||||
_activeImageProvider = null;
|
|
||||||
_activeTargetOffset = null;
|
|
||||||
_isPlaying = false;
|
_isPlaying = false;
|
||||||
});
|
});
|
||||||
_syncIdleCenterVisual();
|
_syncIdleCenterVisual();
|
||||||
_scheduleNextAnimation();
|
_scheduleNextAnimation();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _armActiveFlightWatchdog(Duration flightDuration) {
|
||||||
|
_cancelActiveFlightWatchdog();
|
||||||
|
final timeout = flightDuration + const Duration(milliseconds: 900);
|
||||||
|
_activeFlightWatchdogTimer = Timer(timeout, () {
|
||||||
|
_activeFlightWatchdogTimer = null;
|
||||||
|
if (!_isPlaying || _activeFlights.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugPrint(
|
||||||
|
'[GiftSeatFlight] watchdog timeout active=${_activeFlights.length}',
|
||||||
|
);
|
||||||
|
_controller.stop();
|
||||||
|
_finishCurrentAnimation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _cancelActiveFlightWatchdog() {
|
||||||
|
_activeFlightWatchdogTimer?.cancel();
|
||||||
|
_activeFlightWatchdogTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
void _ensureCenterVisual(RoomGiftSeatFlightRequest request) {
|
void _ensureCenterVisual(RoomGiftSeatFlightRequest request) {
|
||||||
if (_centerRequest != null && _centerImageProvider != null) {
|
if (_centerRequest != null && _centerImageProvider != null) {
|
||||||
return;
|
return;
|
||||||
@ -473,7 +560,7 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _syncIdleCenterVisual() {
|
void _syncIdleCenterVisual() {
|
||||||
if (_isPlaying || _activeRequest != null) {
|
if (_isPlaying || _activeFlights.isNotEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -532,7 +619,10 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
String imagePath, {
|
String imagePath, {
|
||||||
RoomGiftSeatFlightRequest? request,
|
RoomGiftSeatFlightRequest? request,
|
||||||
}) {
|
}) {
|
||||||
final currentRequest = request ?? _activeRequest ?? _centerRequest;
|
final currentRequest =
|
||||||
|
request ??
|
||||||
|
(_activeFlights.isNotEmpty ? _activeFlights.first.request : null) ??
|
||||||
|
_centerRequest;
|
||||||
final logicalSize =
|
final logicalSize =
|
||||||
currentRequest == null
|
currentRequest == null
|
||||||
? null
|
? null
|
||||||
@ -561,16 +651,17 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
key: _overlayKey,
|
key: _overlayKey,
|
||||||
child:
|
child:
|
||||||
(_centerRequest == null || _centerImageProvider == null) &&
|
(_centerRequest == null || _centerImageProvider == null) &&
|
||||||
(_activeRequest == null ||
|
_activeFlights.isEmpty
|
||||||
_activeImageProvider == null ||
|
|
||||||
_activeTargetOffset == null)
|
|
||||||
? const SizedBox.shrink()
|
? const SizedBox.shrink()
|
||||||
: AnimatedBuilder(
|
: AnimatedBuilder(
|
||||||
animation: _controller,
|
animation: _controller,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
final center = overlaySize.center(Offset.zero);
|
final center = overlaySize.center(Offset.zero);
|
||||||
final currentCenterRequest =
|
final currentCenterRequest =
|
||||||
_centerRequest ?? _activeRequest;
|
_centerRequest ??
|
||||||
|
(_activeFlights.isNotEmpty
|
||||||
|
? _activeFlights.first.request
|
||||||
|
: null);
|
||||||
final children = <Widget>[];
|
final children = <Widget>[];
|
||||||
|
|
||||||
if (currentCenterRequest != null &&
|
if (currentCenterRequest != null &&
|
||||||
@ -585,40 +676,38 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final activeRequest = _activeRequest;
|
if (_activeFlights.isNotEmpty) {
|
||||||
final activeImageProvider = _activeImageProvider;
|
|
||||||
final activeTargetOffset = _activeTargetOffset;
|
|
||||||
if (activeRequest != null &&
|
|
||||||
activeImageProvider != null &&
|
|
||||||
activeTargetOffset != null) {
|
|
||||||
final travelProgress = Curves.easeInOutCubic.transform(
|
final travelProgress = Curves.easeInOutCubic.transform(
|
||||||
_controller.value.clamp(0.0, 1.0),
|
_controller.value.clamp(0.0, 1.0),
|
||||||
);
|
);
|
||||||
final nodeCenter =
|
|
||||||
Offset.lerp(
|
|
||||||
center,
|
|
||||||
activeTargetOffset,
|
|
||||||
travelProgress,
|
|
||||||
)!;
|
|
||||||
final nodeSize =
|
|
||||||
lerpDouble(
|
|
||||||
activeRequest.beginSize,
|
|
||||||
activeRequest.endSize,
|
|
||||||
travelProgress,
|
|
||||||
)!;
|
|
||||||
final fadeOut =
|
final fadeOut =
|
||||||
1 -
|
1 -
|
||||||
Curves.easeOut.transform(
|
Curves.easeOut.transform(
|
||||||
((travelProgress - 0.82) / 0.18).clamp(0.0, 1.0),
|
((travelProgress - 0.82) / 0.18).clamp(0.0, 1.0),
|
||||||
);
|
);
|
||||||
children.add(
|
for (final activeFlight in _activeFlights) {
|
||||||
_buildGiftNode(
|
final activeRequest = activeFlight.request;
|
||||||
imageProvider: activeImageProvider,
|
final nodeCenter =
|
||||||
center: nodeCenter,
|
Offset.lerp(
|
||||||
size: nodeSize,
|
center,
|
||||||
opacity: fadeOut,
|
activeFlight.targetOffset,
|
||||||
),
|
travelProgress,
|
||||||
);
|
)!;
|
||||||
|
final nodeSize =
|
||||||
|
lerpDouble(
|
||||||
|
activeRequest.beginSize,
|
||||||
|
activeRequest.endSize,
|
||||||
|
travelProgress,
|
||||||
|
)!;
|
||||||
|
children.add(
|
||||||
|
_buildGiftNode(
|
||||||
|
imageProvider: activeFlight.imageProvider,
|
||||||
|
center: nodeCenter,
|
||||||
|
size: nodeSize,
|
||||||
|
opacity: fadeOut,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return RepaintBoundary(
|
return RepaintBoundary(
|
||||||
@ -677,6 +766,18 @@ class _RoomGiftSeatFlightOverlayState extends State<RoomGiftSeatFlightOverlay>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ActiveRoomGiftSeatFlight {
|
||||||
|
const _ActiveRoomGiftSeatFlight({
|
||||||
|
required this.request,
|
||||||
|
required this.imageProvider,
|
||||||
|
required this.targetOffset,
|
||||||
|
});
|
||||||
|
|
||||||
|
final RoomGiftSeatFlightRequest request;
|
||||||
|
final ImageProvider<Object> imageProvider;
|
||||||
|
final Offset targetOffset;
|
||||||
|
}
|
||||||
|
|
||||||
class _QueuedRoomGiftSeatFlightRequest {
|
class _QueuedRoomGiftSeatFlightRequest {
|
||||||
const _QueuedRoomGiftSeatFlightRequest({
|
const _QueuedRoomGiftSeatFlightRequest({
|
||||||
required this.request,
|
required this.request,
|
||||||
|
|||||||
@ -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';
|
import '../../components/sc_debounce_widget.dart';
|
||||||
|
|
||||||
class RoomBottomWidget extends StatefulWidget {
|
class RoomBottomWidget extends StatefulWidget {
|
||||||
const RoomBottomWidget({super.key});
|
const RoomBottomWidget({super.key, this.showGiftComboButton = true});
|
||||||
|
|
||||||
|
final bool showGiftComboButton;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<RoomBottomWidget> createState() => _RoomBottomWidgetState();
|
State<RoomBottomWidget> createState() => _RoomBottomWidgetState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class RoomGiftComboFloatingLayer extends StatelessWidget {
|
||||||
|
const RoomGiftComboFloatingLayer({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Selector<RtcProvider, bool>(
|
||||||
|
selector: (context, provider) => _shouldShowRoomBottomMic(provider),
|
||||||
|
builder: (context, showMic, child) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final inputWidth = _resolveRoomBottomInputWidth(
|
||||||
|
maxWidth: constraints.maxWidth,
|
||||||
|
showMic: showMic,
|
||||||
|
);
|
||||||
|
final giftCenterX = _resolveRoomGiftCenterX(
|
||||||
|
maxWidth: constraints.maxWidth,
|
||||||
|
inputWidth: inputWidth,
|
||||||
|
showMic: showMic,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
children: [
|
||||||
|
Positioned(
|
||||||
|
left: giftCenterX - (_floatingButtonWidth.w / 2),
|
||||||
|
bottom: _floatingButtonBottomOffset.w,
|
||||||
|
child: const RoomGiftComboFloatingButton(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
||||||
int roomMenuStime1 = 0;
|
int roomMenuStime1 = 0;
|
||||||
static const double _bottomBarHeight = 72;
|
|
||||||
static const double _floatingButtonHostHeight = 122;
|
|
||||||
static const double _floatingButtonWidth =
|
|
||||||
RoomGiftComboFloatingButton.hostSize;
|
|
||||||
static const double _floatingButtonBottomOffset = 54;
|
|
||||||
static const double _giftActionWidth = 52;
|
|
||||||
static const double _circleActionWidth = 46;
|
|
||||||
static const double _emojiActionWidth = 38;
|
|
||||||
static const double _chatEmojiGap = 8;
|
|
||||||
static const double _compactGap = 14;
|
|
||||||
static const double _horizontalPadding = 16;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@ -55,17 +83,17 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
|||||||
child: Selector<RtcProvider, _RoomBottomSnapshot>(
|
child: Selector<RtcProvider, _RoomBottomSnapshot>(
|
||||||
selector:
|
selector:
|
||||||
(context, provider) => _RoomBottomSnapshot(
|
(context, provider) => _RoomBottomSnapshot(
|
||||||
showMic: _shouldShowMic(provider),
|
showMic: _shouldShowRoomBottomMic(provider),
|
||||||
isMic: provider.isMic,
|
isMic: provider.isMic,
|
||||||
),
|
),
|
||||||
builder: (context, bottomSnapshot, child) {
|
builder: (context, bottomSnapshot, child) {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final inputWidth = _resolveInputWidth(
|
final inputWidth = _resolveRoomBottomInputWidth(
|
||||||
maxWidth: constraints.maxWidth,
|
maxWidth: constraints.maxWidth,
|
||||||
showMic: bottomSnapshot.showMic,
|
showMic: bottomSnapshot.showMic,
|
||||||
);
|
);
|
||||||
final giftCenterX = _resolveGiftCenterX(
|
final giftCenterX = _resolveRoomGiftCenterX(
|
||||||
maxWidth: constraints.maxWidth,
|
maxWidth: constraints.maxWidth,
|
||||||
inputWidth: inputWidth,
|
inputWidth: inputWidth,
|
||||||
showMic: bottomSnapshot.showMic,
|
showMic: bottomSnapshot.showMic,
|
||||||
@ -74,11 +102,12 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
children: [
|
children: [
|
||||||
Positioned(
|
if (widget.showGiftComboButton)
|
||||||
left: giftCenterX - (_floatingButtonWidth.w / 2),
|
Positioned(
|
||||||
bottom: _floatingButtonBottomOffset.w,
|
left: giftCenterX - (_floatingButtonWidth.w / 2),
|
||||||
child: const RoomGiftComboFloatingButton(),
|
bottom: _floatingButtonBottomOffset.w,
|
||||||
),
|
child: const RoomGiftComboFloatingButton(),
|
||||||
|
),
|
||||||
Positioned(
|
Positioned(
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
@ -199,58 +228,6 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
double _resolveInputWidth({required double maxWidth, required bool showMic}) {
|
|
||||||
final contentWidth = maxWidth - (_horizontalPadding.w * 2);
|
|
||||||
final actionWidth =
|
|
||||||
_emojiActionWidth.w +
|
|
||||||
_chatEmojiGap.w +
|
|
||||||
_giftActionWidth.w +
|
|
||||||
_circleActionWidth.w * (showMic ? 3 : 2) +
|
|
||||||
(showMic ? 16.w : _compactGap.w * 2);
|
|
||||||
final availableWidth = contentWidth - actionWidth;
|
|
||||||
return availableWidth.clamp(82.w, 112.w).toDouble();
|
|
||||||
}
|
|
||||||
|
|
||||||
double _resolveGiftCenterX({
|
|
||||||
required double maxWidth,
|
|
||||||
required double inputWidth,
|
|
||||||
required bool showMic,
|
|
||||||
}) {
|
|
||||||
final contentWidth = maxWidth - (_horizontalPadding.w * 2);
|
|
||||||
if (showMic) {
|
|
||||||
final occupiedWidth =
|
|
||||||
inputWidth +
|
|
||||||
_chatEmojiGap.w +
|
|
||||||
_emojiActionWidth.w +
|
|
||||||
_giftActionWidth.w +
|
|
||||||
_circleActionWidth.w * 3;
|
|
||||||
final gapCount = 4;
|
|
||||||
final gap = ((contentWidth - occupiedWidth) / gapCount).clamp(
|
|
||||||
0.0,
|
|
||||||
double.infinity,
|
|
||||||
);
|
|
||||||
return _horizontalPadding.w +
|
|
||||||
inputWidth +
|
|
||||||
_chatEmojiGap.w +
|
|
||||||
_emojiActionWidth.w +
|
|
||||||
gap +
|
|
||||||
(_giftActionWidth.w / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
final fixedWidth =
|
|
||||||
inputWidth +
|
|
||||||
_chatEmojiGap.w +
|
|
||||||
_emojiActionWidth.w +
|
|
||||||
_giftActionWidth.w +
|
|
||||||
_circleActionWidth.w * 2 +
|
|
||||||
_compactGap.w * 2;
|
|
||||||
final spacerWidth = (contentWidth - fixedWidth).clamp(0.0, double.infinity);
|
|
||||||
return _horizontalPadding.w +
|
|
||||||
inputWidth +
|
|
||||||
spacerWidth +
|
|
||||||
(_giftActionWidth.w / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showGiftPanel() {
|
void _showGiftPanel() {
|
||||||
SmartDialog.show(
|
SmartDialog.show(
|
||||||
tag: "showGiftControl",
|
tag: "showGiftControl",
|
||||||
@ -354,10 +331,76 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool _shouldShowMic(RtcProvider provider) {
|
const double _bottomBarHeight = 72;
|
||||||
return provider.isOnMai();
|
const double _floatingButtonHostHeight = 122;
|
||||||
|
const double _floatingButtonWidth = RoomGiftComboFloatingButton.hostSize;
|
||||||
|
const double _floatingButtonBottomOffset = 54;
|
||||||
|
const double _giftActionWidth = 52;
|
||||||
|
const double _circleActionWidth = 46;
|
||||||
|
const double _emojiActionWidth = 38;
|
||||||
|
const double _chatEmojiGap = 8;
|
||||||
|
const double _compactGap = 14;
|
||||||
|
const double _horizontalPadding = 16;
|
||||||
|
|
||||||
|
double _resolveRoomBottomInputWidth({
|
||||||
|
required double maxWidth,
|
||||||
|
required bool showMic,
|
||||||
|
}) {
|
||||||
|
final contentWidth = maxWidth - (_horizontalPadding.w * 2);
|
||||||
|
final actionWidth =
|
||||||
|
_emojiActionWidth.w +
|
||||||
|
_chatEmojiGap.w +
|
||||||
|
_giftActionWidth.w +
|
||||||
|
_circleActionWidth.w * (showMic ? 3 : 2) +
|
||||||
|
(showMic ? 16.w : _compactGap.w * 2);
|
||||||
|
final availableWidth = contentWidth - actionWidth;
|
||||||
|
return availableWidth.clamp(82.w, 112.w).toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
double _resolveRoomGiftCenterX({
|
||||||
|
required double maxWidth,
|
||||||
|
required double inputWidth,
|
||||||
|
required bool showMic,
|
||||||
|
}) {
|
||||||
|
final contentWidth = maxWidth - (_horizontalPadding.w * 2);
|
||||||
|
if (showMic) {
|
||||||
|
final occupiedWidth =
|
||||||
|
inputWidth +
|
||||||
|
_chatEmojiGap.w +
|
||||||
|
_emojiActionWidth.w +
|
||||||
|
_giftActionWidth.w +
|
||||||
|
_circleActionWidth.w * 3;
|
||||||
|
final gapCount = 4;
|
||||||
|
final gap = ((contentWidth - occupiedWidth) / gapCount).clamp(
|
||||||
|
0.0,
|
||||||
|
double.infinity,
|
||||||
|
);
|
||||||
|
return _horizontalPadding.w +
|
||||||
|
inputWidth +
|
||||||
|
_chatEmojiGap.w +
|
||||||
|
_emojiActionWidth.w +
|
||||||
|
gap +
|
||||||
|
(_giftActionWidth.w / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final fixedWidth =
|
||||||
|
inputWidth +
|
||||||
|
_chatEmojiGap.w +
|
||||||
|
_emojiActionWidth.w +
|
||||||
|
_giftActionWidth.w +
|
||||||
|
_circleActionWidth.w * 2 +
|
||||||
|
_compactGap.w * 2;
|
||||||
|
final spacerWidth = (contentWidth - fixedWidth).clamp(0.0, double.infinity);
|
||||||
|
return _horizontalPadding.w +
|
||||||
|
inputWidth +
|
||||||
|
spacerWidth +
|
||||||
|
(_giftActionWidth.w / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _shouldShowRoomBottomMic(RtcProvider provider) {
|
||||||
|
return provider.isOnMai();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RoomBottomSnapshot {
|
class _RoomBottomSnapshot {
|
||||||
|
|||||||
@ -31,32 +31,45 @@ class RoomMsgInput extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _RoomMsgInputState extends State<RoomMsgInput> {
|
class _RoomMsgInputState extends State<RoomMsgInput> {
|
||||||
static const List<String> _roomEmojiAssets = [
|
static const List<_RoomEmojiCategory> _roomEmojiCategories = [
|
||||||
"sc_images/room/emoji/fluent_emoji_01.webp",
|
_RoomEmojiCategory(
|
||||||
"sc_images/room/emoji/fluent_emoji_02.webp",
|
id: "fluent",
|
||||||
"sc_images/room/emoji/fluent_emoji_03.webp",
|
iconAsset: "sc_images/room/emoji/fluent_emoji_01.webp",
|
||||||
"sc_images/room/emoji/fluent_emoji_04.webp",
|
assets: [
|
||||||
"sc_images/room/emoji/fluent_emoji_05.webp",
|
"sc_images/room/emoji/fluent_emoji_01.webp",
|
||||||
"sc_images/room/emoji/fluent_emoji_06.webp",
|
"sc_images/room/emoji/fluent_emoji_02.webp",
|
||||||
"sc_images/room/emoji/fluent_emoji_07.webp",
|
"sc_images/room/emoji/fluent_emoji_03.webp",
|
||||||
"sc_images/room/emoji/fluent_emoji_08.webp",
|
"sc_images/room/emoji/fluent_emoji_04.webp",
|
||||||
"sc_images/room/emoji/fluent_emoji_09.webp",
|
"sc_images/room/emoji/fluent_emoji_05.webp",
|
||||||
"sc_images/room/emoji/fluent_emoji_10.webp",
|
"sc_images/room/emoji/fluent_emoji_06.webp",
|
||||||
"sc_images/room/emoji/monkey_1.gif",
|
"sc_images/room/emoji/fluent_emoji_07.webp",
|
||||||
"sc_images/room/emoji/monkey_2.gif",
|
"sc_images/room/emoji/fluent_emoji_08.webp",
|
||||||
"sc_images/room/emoji/monkey_3.gif",
|
"sc_images/room/emoji/fluent_emoji_09.webp",
|
||||||
"sc_images/room/emoji/monkey_4.gif",
|
"sc_images/room/emoji/fluent_emoji_10.webp",
|
||||||
"sc_images/room/emoji/monkey_5.gif",
|
],
|
||||||
"sc_images/room/emoji/monkey_6.gif",
|
),
|
||||||
"sc_images/room/emoji/monkey_7.gif",
|
_RoomEmojiCategory(
|
||||||
"sc_images/room/emoji/monkey_8.gif",
|
id: "monkey",
|
||||||
"sc_images/room/emoji/monkey_9.gif",
|
iconAsset: "sc_images/room/emoji/monkey_1.gif",
|
||||||
"sc_images/room/emoji/monkey_10.gif",
|
assets: [
|
||||||
|
"sc_images/room/emoji/monkey_1.gif",
|
||||||
|
"sc_images/room/emoji/monkey_2.gif",
|
||||||
|
"sc_images/room/emoji/monkey_3.gif",
|
||||||
|
"sc_images/room/emoji/monkey_4.gif",
|
||||||
|
"sc_images/room/emoji/monkey_5.gif",
|
||||||
|
"sc_images/room/emoji/monkey_6.gif",
|
||||||
|
"sc_images/room/emoji/monkey_7.gif",
|
||||||
|
"sc_images/room/emoji/monkey_8.gif",
|
||||||
|
"sc_images/room/emoji/monkey_9.gif",
|
||||||
|
"sc_images/room/emoji/monkey_10.gif",
|
||||||
|
],
|
||||||
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
bool showSend = false;
|
bool showSend = false;
|
||||||
bool showEmoji = false;
|
bool showEmoji = false;
|
||||||
bool _emojiAssetsPrecached = false;
|
bool _emojiAssetsPrecached = false;
|
||||||
|
int _selectedEmojiCategoryIndex = 0;
|
||||||
|
|
||||||
final FocusNode msgNode = FocusNode();
|
final FocusNode msgNode = FocusNode();
|
||||||
final TextEditingController controller = TextEditingController();
|
final TextEditingController controller = TextEditingController();
|
||||||
@ -234,33 +247,103 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildEmojiPanel() {
|
Widget _buildEmojiPanel() {
|
||||||
|
final selectedCategory = _roomEmojiCategories[_selectedEmojiCategoryIndex];
|
||||||
return Container(
|
return Container(
|
||||||
height: 150.w,
|
height: 220.w,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: _panelBackgroundColor,
|
color: _panelBackgroundColor,
|
||||||
borderRadius: BorderRadius.circular(height(5)),
|
borderRadius: BorderRadius.circular(height(5)),
|
||||||
),
|
),
|
||||||
child: GridView.builder(
|
child: Column(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 15.w, vertical: 10.w),
|
children: [
|
||||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
Expanded(
|
||||||
crossAxisCount: 8,
|
child: GridView.builder(
|
||||||
crossAxisSpacing: 8.w,
|
key: ValueKey(selectedCategory.id),
|
||||||
mainAxisSpacing: 8.w,
|
padding: EdgeInsets.fromLTRB(18.w, 14.w, 18.w, 10.w),
|
||||||
|
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 4,
|
||||||
|
crossAxisSpacing: 18.w,
|
||||||
|
mainAxisSpacing: 12.w,
|
||||||
|
),
|
||||||
|
itemCount: selectedCategory.assets.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final emojiAsset = selectedCategory.assets[index];
|
||||||
|
return GestureDetector(
|
||||||
|
behavior: HitTestBehavior.opaque,
|
||||||
|
onTap: () {
|
||||||
|
_sendRoomEmoji(emojiAsset);
|
||||||
|
},
|
||||||
|
child: Center(
|
||||||
|
child: RoomEmojiAssetImage(
|
||||||
|
key: ValueKey(emojiAsset),
|
||||||
|
asset: emojiAsset,
|
||||||
|
width: 42.w,
|
||||||
|
height: 42.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildEmojiCategoryBar(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmojiCategoryBar() {
|
||||||
|
return Container(
|
||||||
|
height: 50.w,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 12.w, vertical: 7.w),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withValues(alpha: 0.08),
|
||||||
|
border: Border(
|
||||||
|
top: BorderSide(color: Colors.white.withValues(alpha: 0.08)),
|
||||||
),
|
),
|
||||||
itemCount: _roomEmojiAssets.length,
|
),
|
||||||
|
child: ListView.separated(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
itemCount: _roomEmojiCategories.length,
|
||||||
|
separatorBuilder: (_, __) => SizedBox(width: 10.w),
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final emojiAsset = _roomEmojiAssets[index];
|
final category = _roomEmojiCategories[index];
|
||||||
|
final selected = index == _selectedEmojiCategoryIndex;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
behavior: HitTestBehavior.opaque,
|
behavior: HitTestBehavior.opaque,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_sendRoomEmoji(emojiAsset);
|
if (selected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setState(() {
|
||||||
|
_selectedEmojiCategoryIndex = index;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
child: Center(
|
child: AnimatedContainer(
|
||||||
child: RoomEmojiAssetImage(
|
duration: const Duration(milliseconds: 160),
|
||||||
key: ValueKey(emojiAsset),
|
curve: Curves.easeOut,
|
||||||
asset: emojiAsset,
|
width: 64.w,
|
||||||
width: 30.w,
|
height: 36.w,
|
||||||
height: 30.w,
|
decoration: BoxDecoration(
|
||||||
|
color:
|
||||||
|
selected
|
||||||
|
? Colors.white.withValues(alpha: 0.22)
|
||||||
|
: Colors.white.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(18.w),
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
selected
|
||||||
|
? Colors.white.withValues(alpha: 0.30)
|
||||||
|
: Colors.transparent,
|
||||||
|
width: 1.w,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: RoomEmojiAssetImage(
|
||||||
|
key: ValueKey("emoji_category_${category.id}"),
|
||||||
|
asset: category.iconAsset,
|
||||||
|
width: 26.w,
|
||||||
|
height: 26.w,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -290,8 +373,11 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_emojiAssetsPrecached = true;
|
_emojiAssetsPrecached = true;
|
||||||
for (final asset in _roomEmojiAssets) {
|
for (final category in _roomEmojiCategories) {
|
||||||
precacheImage(AssetImage(asset), context);
|
precacheImage(AssetImage(category.iconAsset), context);
|
||||||
|
for (final asset in category.assets) {
|
||||||
|
precacheImage(AssetImage(asset), context);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -346,6 +432,18 @@ class _RoomMsgInputState extends State<RoomMsgInput> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _RoomEmojiCategory {
|
||||||
|
const _RoomEmojiCategory({
|
||||||
|
required this.id,
|
||||||
|
required this.iconAsset,
|
||||||
|
required this.assets,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String id;
|
||||||
|
final String iconAsset;
|
||||||
|
final List<String> assets;
|
||||||
|
}
|
||||||
|
|
||||||
class PopRoute extends PopupRoute {
|
class PopRoute extends PopupRoute {
|
||||||
final Duration _duration = Duration(milliseconds: 350);
|
final Duration _duration = Duration(milliseconds: 350);
|
||||||
Widget child;
|
Widget child;
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:extended_text/extended_text.dart';
|
import 'package:extended_text/extended_text.dart';
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@ -18,6 +20,8 @@ import 'package:yumi/shared/business_logic/models/res/mic_res.dart';
|
|||||||
import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
|
import 'package:yumi/shared/business_logic/usecases/sc_case.dart';
|
||||||
import 'package:yumi/modules/index/main_route.dart';
|
import 'package:yumi/modules/index/main_route.dart';
|
||||||
import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart';
|
import 'package:yumi/ui_kit/widgets/room/room_emoji_asset_image.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/sc_nine_patch_image.dart';
|
||||||
|
import 'package:yumi/ui_kit/widgets/svga/sc_network_svga_widget.dart';
|
||||||
|
|
||||||
import '../../../shared/data_sources/models/enum/sc_activity_reward_props_type.dart';
|
import '../../../shared/data_sources/models/enum/sc_activity_reward_props_type.dart';
|
||||||
import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart';
|
import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart';
|
||||||
@ -28,13 +32,12 @@ import '../../components/text/sc_text.dart';
|
|||||||
///消息item
|
///消息item
|
||||||
class MsgItem extends StatefulWidget {
|
class MsgItem extends StatefulWidget {
|
||||||
final Function(SocialChatUserProfile? user) onClick;
|
final Function(SocialChatUserProfile? user) onClick;
|
||||||
Msg msg;
|
final Msg msg;
|
||||||
|
|
||||||
MsgItem({Key? key, required this.msg, required this.onClick})
|
const MsgItem({super.key, required this.msg, required this.onClick});
|
||||||
: super(key: key);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
_MsgItemState createState() => _MsgItemState();
|
State<MsgItem> createState() => _MsgItemState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _MsgItemState extends State<MsgItem> {
|
class _MsgItemState extends State<MsgItem> {
|
||||||
@ -130,42 +133,11 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
Flexible(
|
Flexible(
|
||||||
// 使用Flexible替代Expanded
|
// 使用Flexible替代Expanded
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
child: Container(
|
child: _buildTextMessageBubble(),
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxWidth: ScreenUtil().screenWidth * 0.7, // 最大宽度约束
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.black38,
|
|
||||||
borderRadius: BorderRadius.only(
|
|
||||||
topRight: Radius.circular(15.w),
|
|
||||||
bottomLeft: Radius.circular(15.w),
|
|
||||||
bottomRight: Radius.circular(15.w),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: 10.w,
|
|
||||||
vertical: 4.w,
|
|
||||||
),
|
|
||||||
margin: EdgeInsetsDirectional.only(
|
|
||||||
start: 54.w,
|
|
||||||
).copyWith(bottom: 8.w),
|
|
||||||
child: ExtendedText(
|
|
||||||
widget.msg?.msg ?? '',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: sp(13),
|
|
||||||
color: Color(_msgColor(widget.msg.type ?? "")),
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
softWrap: true, // 自动换行
|
|
||||||
specialTextSpanBuilder: AtTextSpanBuilder(
|
|
||||||
onTapCall: (userId) {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
onLongPress: () {
|
onLongPress: () {
|
||||||
if (!(widget.msg?.msg ?? "").startsWith(AtText.flag)) {
|
if (!(widget.msg.msg ?? "").startsWith(AtText.flag)) {
|
||||||
Clipboard.setData(
|
Clipboard.setData(
|
||||||
ClipboardData(text: widget.msg?.msg ?? ''),
|
ClipboardData(text: widget.msg.msg ?? ''),
|
||||||
);
|
);
|
||||||
SCTts.show("Copied");
|
SCTts.show("Copied");
|
||||||
}
|
}
|
||||||
@ -194,7 +166,7 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
borderRadius: BorderRadius.all(Radius.circular(15.w)),
|
borderRadius: BorderRadius.all(Radius.circular(15.w)),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.msg?.msg ?? '',
|
widget.msg.msg ?? '',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: sp(13),
|
fontSize: sp(13),
|
||||||
color: Color(_msgColor(widget.msg.type ?? "")),
|
color: Color(_msgColor(widget.msg.type ?? "")),
|
||||||
@ -207,6 +179,295 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildTextMessageBubble() {
|
||||||
|
final chatBubble = widget.msg.user?.getChatBox();
|
||||||
|
final hasChatBubble = _resolveRoomChatBubbleImageUrl(chatBubble).isNotEmpty;
|
||||||
|
if (hasChatBubble) {
|
||||||
|
return _buildVipTextMessageBubble(chatBubble);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
constraints: BoxConstraints(maxWidth: ScreenUtil().screenWidth * 0.7),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black38,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topRight: Radius.circular(15.w),
|
||||||
|
bottomLeft: Radius.circular(15.w),
|
||||||
|
bottomRight: Radius.circular(15.w),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 4.w),
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 54.w).copyWith(bottom: 8.w),
|
||||||
|
child: ExtendedText(
|
||||||
|
widget.msg.msg ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: sp(13),
|
||||||
|
color: Color(_msgColor(widget.msg.type ?? "")),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
softWrap: true,
|
||||||
|
specialTextSpanBuilder: AtTextSpanBuilder(onTapCall: (userId) {}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVipTextMessageBubble(PropsResources? resource) {
|
||||||
|
final messageText = widget.msg.msg ?? "";
|
||||||
|
final textStyle = TextStyle(
|
||||||
|
fontSize: sp(13),
|
||||||
|
color: Color(_msgColor(widget.msg.type ?? "")),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
);
|
||||||
|
final imageUrl = _resolveRoomChatBubbleImageUrl(resource);
|
||||||
|
final multilineMaxBubbleWidth = math.min(
|
||||||
|
ScreenUtil().screenWidth * 0.72,
|
||||||
|
300.w,
|
||||||
|
);
|
||||||
|
final multilineMaxTextWidth = math.max(
|
||||||
|
90.w,
|
||||||
|
multilineMaxBubbleWidth - 58.w,
|
||||||
|
);
|
||||||
|
final multilineTextPainter = TextPainter(
|
||||||
|
text: TextSpan(text: messageText, style: textStyle),
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
)..layout(maxWidth: multilineMaxTextWidth);
|
||||||
|
if (multilineTextPainter.computeLineMetrics().length > 1) {
|
||||||
|
return _buildMultilineVipTextMessageBubble(
|
||||||
|
messageText: messageText,
|
||||||
|
textStyle: textStyle,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
textSize: multilineTextPainter.size,
|
||||||
|
maxBubbleWidth: multilineMaxBubbleWidth,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildSingleLineVipTextMessageBubble(
|
||||||
|
messageText: messageText,
|
||||||
|
textStyle: textStyle,
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildMultilineVipTextMessageBubble({
|
||||||
|
required String messageText,
|
||||||
|
required TextStyle textStyle,
|
||||||
|
required String imageUrl,
|
||||||
|
required Size textSize,
|
||||||
|
required double maxBubbleWidth,
|
||||||
|
}) {
|
||||||
|
final bubbleWidth = math
|
||||||
|
.max(126.w, textSize.width + 58.w)
|
||||||
|
.clamp(126.w, maxBubbleWidth);
|
||||||
|
final textMaxWidth = math.max(90.w, bubbleWidth - 58.w);
|
||||||
|
final bubbleHeight = math.max(58.w, textSize.height + 26.w);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: bubbleWidth,
|
||||||
|
height: bubbleHeight,
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 54.w).copyWith(bottom: 8.w),
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
_buildVipBubbleBackground(
|
||||||
|
url: imageUrl,
|
||||||
|
width: bubbleWidth,
|
||||||
|
height: bubbleHeight,
|
||||||
|
fallback: _buildVipBubbleFallbackBackground(),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsetsDirectional.fromSTEB(29.w, 11.w, 29.w, 11.w),
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: SizedBox(
|
||||||
|
width: textMaxWidth,
|
||||||
|
child: ExtendedText(
|
||||||
|
messageText,
|
||||||
|
style: textStyle,
|
||||||
|
softWrap: true,
|
||||||
|
specialTextSpanBuilder: AtTextSpanBuilder(
|
||||||
|
onTapCall: (userId) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSingleLineVipTextMessageBubble({
|
||||||
|
required String messageText,
|
||||||
|
required TextStyle textStyle,
|
||||||
|
required String imageUrl,
|
||||||
|
}) {
|
||||||
|
final horizontalPadding = 18.w;
|
||||||
|
final verticalPadding = 4.w;
|
||||||
|
final minSkinWidth = 132.w;
|
||||||
|
final minSkinHeight = 42.w;
|
||||||
|
final maxBubbleWidth = ScreenUtil().screenWidth * 0.7;
|
||||||
|
final maxTextWidth = math.max(1.0, maxBubbleWidth - horizontalPadding * 2);
|
||||||
|
final textSize = _measureMessageTextSize(
|
||||||
|
text: messageText,
|
||||||
|
style: textStyle,
|
||||||
|
maxWidth: maxTextWidth,
|
||||||
|
);
|
||||||
|
final bubbleWidth = math.min(
|
||||||
|
maxBubbleWidth,
|
||||||
|
math.max(horizontalPadding * 2, textSize.width + horizontalPadding * 2),
|
||||||
|
);
|
||||||
|
final textMaxWidth = math.max(1.0, bubbleWidth - horizontalPadding * 2);
|
||||||
|
final bubbleHeight = math.max(
|
||||||
|
verticalPadding * 2,
|
||||||
|
textSize.height + verticalPadding * 2,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
width: bubbleWidth,
|
||||||
|
height: bubbleHeight,
|
||||||
|
margin: EdgeInsetsDirectional.only(start: 54.w).copyWith(bottom: 8.w),
|
||||||
|
child: Stack(
|
||||||
|
clipBehavior: Clip.none,
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
Positioned.fill(child: _buildVipBubbleFallbackBackground()),
|
||||||
|
Positioned.fill(
|
||||||
|
child: _buildVipBubbleSkin(
|
||||||
|
url: imageUrl,
|
||||||
|
width: math.max(bubbleWidth, minSkinWidth),
|
||||||
|
height: math.max(bubbleHeight, minSkinHeight),
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: EdgeInsetsDirectional.symmetric(
|
||||||
|
horizontal: horizontalPadding,
|
||||||
|
vertical: verticalPadding,
|
||||||
|
),
|
||||||
|
child: Align(
|
||||||
|
alignment: AlignmentDirectional.centerStart,
|
||||||
|
child: SizedBox(
|
||||||
|
width: textMaxWidth,
|
||||||
|
child: ExtendedText(
|
||||||
|
messageText,
|
||||||
|
style: textStyle,
|
||||||
|
softWrap: true,
|
||||||
|
specialTextSpanBuilder: AtTextSpanBuilder(
|
||||||
|
onTapCall: (userId) {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVipBubbleSkin({
|
||||||
|
required String url,
|
||||||
|
required double width,
|
||||||
|
required double height,
|
||||||
|
required AlignmentGeometry alignment,
|
||||||
|
}) {
|
||||||
|
return OverflowBox(
|
||||||
|
alignment: alignment,
|
||||||
|
minWidth: width,
|
||||||
|
maxWidth: width,
|
||||||
|
minHeight: height,
|
||||||
|
maxHeight: height,
|
||||||
|
child:
|
||||||
|
SCNetworkSvgaWidget.isSvga(url)
|
||||||
|
? SCNetworkSvgaWidget(
|
||||||
|
resource: url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
fallback: SizedBox(width: width, height: height),
|
||||||
|
)
|
||||||
|
: SCNinePatchImage(
|
||||||
|
url: url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
centerSliceRatio: const EdgeInsets.fromLTRB(
|
||||||
|
0.34,
|
||||||
|
0.34,
|
||||||
|
0.34,
|
||||||
|
0.34,
|
||||||
|
),
|
||||||
|
fallback: SizedBox(width: width, height: height),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVipBubbleBackground({
|
||||||
|
required String url,
|
||||||
|
required double width,
|
||||||
|
required double height,
|
||||||
|
required Widget fallback,
|
||||||
|
}) {
|
||||||
|
if (SCNetworkSvgaWidget.isSvga(url)) {
|
||||||
|
return SCNetworkSvgaWidget(
|
||||||
|
resource: url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
fallback: fallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return SCNinePatchImage(
|
||||||
|
url: url,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
centerSliceRatio: const EdgeInsets.fromLTRB(0.34, 0.34, 0.34, 0.34),
|
||||||
|
fallback: fallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Size _measureMessageTextSize({
|
||||||
|
required String text,
|
||||||
|
required TextStyle style,
|
||||||
|
required double maxWidth,
|
||||||
|
}) {
|
||||||
|
final textPainter = TextPainter(
|
||||||
|
text: TextSpan(text: text, style: style),
|
||||||
|
textDirection: Directionality.of(context),
|
||||||
|
)..layout(maxWidth: maxWidth);
|
||||||
|
return textPainter.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildVipBubbleFallbackBackground() {
|
||||||
|
return DecoratedBox(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.black.withValues(alpha: 0.38),
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
topRight: Radius.circular(18.w),
|
||||||
|
bottomLeft: Radius.circular(18.w),
|
||||||
|
bottomRight: Radius.circular(18.w),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _resolveRoomChatBubbleImageUrl(PropsResources? resource) {
|
||||||
|
return _firstNonBlank([
|
||||||
|
resource?.roomSendCoverUrl,
|
||||||
|
resource?.roomOpenedUrl,
|
||||||
|
resource?.roomNotOpenedUrl,
|
||||||
|
resource?.cover,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _firstNonBlank(Iterable<String?> values) {
|
||||||
|
for (final value in values) {
|
||||||
|
final text = value?.trim();
|
||||||
|
if (text != null && text.isNotEmpty) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
///系统提示
|
///系统提示
|
||||||
_systemTipsRoomItem(BuildContext context) {
|
_systemTipsRoomItem(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -224,7 +485,7 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
borderRadius: BorderRadius.all(Radius.circular(15.w)),
|
borderRadius: BorderRadius.all(Radius.circular(15.w)),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
widget.msg?.msg ?? '',
|
widget.msg.msg ?? '',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: sp(13),
|
fontSize: sp(13),
|
||||||
height: 1.5.w,
|
height: 1.5.w,
|
||||||
@ -658,7 +919,7 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
///进入房间
|
///进入房间
|
||||||
_enterRoomItem(BuildContext context) {
|
_enterRoomItem(BuildContext context) {
|
||||||
List<InlineSpan> spans = [];
|
List<InlineSpan> spans = [];
|
||||||
if (widget.msg.role != null && widget.msg.role != 0) {
|
if (widget.msg.role != null && widget.msg.role != "0") {
|
||||||
spans.add(
|
spans.add(
|
||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
alignment: PlaceholderAlignment.middle,
|
alignment: PlaceholderAlignment.middle,
|
||||||
@ -671,7 +932,7 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
}
|
}
|
||||||
spans.add(
|
spans.add(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: "${widget.msg?.user?.userNickname ?? ''} ",
|
text: "${widget.msg.user?.userNickname ?? ''} ",
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: sp(13),
|
fontSize: sp(13),
|
||||||
color: _roleNameColor(widget.msg.role ?? ""),
|
color: _roleNameColor(widget.msg.role ?? ""),
|
||||||
@ -718,7 +979,7 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
scrollDirection: Axis.horizontal,
|
scrollDirection: Axis.horizontal,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
itemCount: wearBadge.length ?? 0,
|
itemCount: wearBadge.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
return netImage(
|
return netImage(
|
||||||
width: 25.w,
|
width: 25.w,
|
||||||
@ -749,7 +1010,6 @@ class _MsgItemState extends State<MsgItem> {
|
|||||||
case SCRoomMsgType.qcfj:
|
case SCRoomMsgType.qcfj:
|
||||||
case SCRoomMsgType.killXiaMai:
|
case SCRoomMsgType.killXiaMai:
|
||||||
return 0xFFFFFFFF;
|
return 0xFFFFFFFF;
|
||||||
break;
|
|
||||||
// case type.text:
|
// case type.text:
|
||||||
// return _mapTextColor();
|
// return _mapTextColor();
|
||||||
// break;
|
// break;
|
||||||
@ -1403,6 +1663,7 @@ class Msg {
|
|||||||
|
|
||||||
// 送礼给谁
|
// 送礼给谁
|
||||||
SocialChatUserProfile? toUser;
|
SocialChatUserProfile? toUser;
|
||||||
|
List<String>? targetUserIds;
|
||||||
int? time = 0;
|
int? time = 0;
|
||||||
|
|
||||||
//动画是否已经播放(骰子、石头剪刀布、随机数字)
|
//动画是否已经播放(骰子、石头剪刀布、随机数字)
|
||||||
@ -1422,6 +1683,7 @@ class Msg {
|
|||||||
this.number,
|
this.number,
|
||||||
this.customAnimationCount,
|
this.customAnimationCount,
|
||||||
this.awardAmount,
|
this.awardAmount,
|
||||||
|
this.targetUserIds,
|
||||||
this.hasPlayedAnimation,
|
this.hasPlayedAnimation,
|
||||||
this.needUpDataUserInfo,
|
this.needUpDataUserInfo,
|
||||||
}) {
|
}) {
|
||||||
@ -1437,6 +1699,14 @@ class Msg {
|
|||||||
number = json['number'];
|
number = json['number'];
|
||||||
customAnimationCount = json['customAnimationCount'];
|
customAnimationCount = json['customAnimationCount'];
|
||||||
awardAmount = json['awardAmount'];
|
awardAmount = json['awardAmount'];
|
||||||
|
final dynamic rawTargetUserIds = json['targetUserIds'];
|
||||||
|
if (rawTargetUserIds is List) {
|
||||||
|
targetUserIds =
|
||||||
|
rawTargetUserIds
|
||||||
|
.map((item) => item?.toString().trim() ?? "")
|
||||||
|
.where((id) => id.isNotEmpty)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
hasPlayedAnimation = json['hasPlayedAnimation'];
|
hasPlayedAnimation = json['hasPlayedAnimation'];
|
||||||
needUpDataUserInfo = json['needUpDataUserInfo'];
|
needUpDataUserInfo = json['needUpDataUserInfo'];
|
||||||
userWheat =
|
userWheat =
|
||||||
@ -1466,6 +1736,9 @@ class Msg {
|
|||||||
map['customAnimationCount'] = customAnimationCount;
|
map['customAnimationCount'] = customAnimationCount;
|
||||||
map['hasPlayedAnimation'] = hasPlayedAnimation;
|
map['hasPlayedAnimation'] = hasPlayedAnimation;
|
||||||
map['needUpDataUserInfo'] = needUpDataUserInfo;
|
map['needUpDataUserInfo'] = needUpDataUserInfo;
|
||||||
|
if (targetUserIds != null) {
|
||||||
|
map['targetUserIds'] = targetUserIds;
|
||||||
|
}
|
||||||
if (userWheat != null) {
|
if (userWheat != null) {
|
||||||
map['userWheat'] = userWheat?.toJson();
|
map['userWheat'] = userWheat?.toJson();
|
||||||
}
|
}
|
||||||
|
|||||||
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
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.2.6+8
|
version: 1.2.7+9
|
||||||
|
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
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