yumi-flutter/lib/ui_kit/widgets/room/room_user_info_card.dart
2026-05-22 19:32:01 +08:00

2076 lines
63 KiB
Dart

import 'dart:convert';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/app_localizations.dart';
import 'package:yumi/main.dart';
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
import 'package:yumi/ui_kit/components/text/sc_text.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/modules/index/main_route.dart';
import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_input.dart';
import 'package:yumi/ui_kit/widgets/room/room_user_card_setting.dart';
import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart';
import 'package:marquee/marquee.dart';
import 'package:provider/provider.dart';
import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart';
import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/shared/tools/sc_lk_dialog_util.dart';
import 'package:yumi/shared/tools/sc_loading_manager.dart';
import 'package:yumi/shared/tools/sc_room_utils.dart';
import 'package:yumi/shared/tools/sc_room_top_layer_guard.dart';
import 'package:yumi/shared/tools/sc_string_utils.dart';
import 'package:yumi/shared/tools/sc_cp_relation_level_utils.dart';
import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart';
import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_cp_rights_config_res.dart';
import 'package:yumi/shared/business_logic/models/res/room_user_card_res.dart'
as room_card;
import 'package:yumi/services/general/sc_app_general_manager.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/modules/gift/gift_page.dart';
import 'package:yumi/ui_kit/components/sc_tts.dart';
import 'package:yumi/ui_kit/widgets/id/sc_special_id_badge.dart';
import 'package:yumi/ui_kit/widgets/props/sc_data_card_resource_view.dart';
import 'package:yumi/ui_kit/widgets/room/cp/sc_cp_corner_label.dart';
import 'package:yumi/ui_kit/widgets/room/cp/room_cp_progress_dialog.dart';
import '../../../shared/data_sources/models/enum/sc_room_roles_type.dart';
import '../../../shared/business_logic/models/res/sc_user_identity_res.dart';
import '../../../modules/chat/chat_route.dart';
class RoomUserInfoCard extends StatefulWidget {
final String? userId;
const RoomUserInfoCard({super.key, this.userId});
@override
State<RoomUserInfoCard> createState() => _RoomUserInfoCardState();
}
class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
static const String _leaveMicActionIconAsset =
"sc_images/room/sc_icon_room_user_card_leave_mic.png";
static const String _defaultProfileCardBackgroundAsset =
"sc_images/room/sc_bg_room_user_profile_card_default.png";
static const String _cpProfileAssetBase = "sc_images/room/cp_profile";
static const double _profileSheetHeightFactor = 0.54;
static const double _dataCardBackgroundTranslateFactor = 0.8;
SocialChatUserProfileManager? userProvider;
String roomId = "";
SCUserIdentityRes? userIdentity;
@override
void initState() {
super.initState();
userProvider = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
);
roomId =
Provider.of<RtcProvider>(
context,
listen: false,
).currenRoom?.roomProfile?.roomProfile?.id ??
"";
userProvider?.roomUserCard(roomId, widget.userId ?? "");
SCAccountRepository().userIdentity(userId: widget.userId).then((v) {
if (!mounted) {
return;
}
userIdentity = v;
setState(() {});
});
}
@override
void dispose() {
super.dispose();
userProvider?.userCardInfo = null;
}
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
final defaultSheetHeight = screenHeight * _profileSheetHeightFactor;
return Consumer<SocialChatUserProfileManager>(
builder: (context, ref, child) {
final currentUserId =
AccountStorage().getCurrentUser()?.userProfile?.id;
final isSelf = widget.userId == currentUserId;
final rtcProvider = Provider.of<RtcProvider>(context, listen: false);
final currentMicIndex =
isSelf && currentUserId != null
? rtcProvider.userOnMaiInIndex(currentUserId)
: -1;
final canLeaveMic = currentMicIndex > -1;
if (ref.userCardInfo == null) {
return _buildLoadingSheet(defaultSheetHeight);
}
final profile = ref.userCardInfo?.userProfile;
final dataCard = _activeDataCard(ref.userCardInfo);
final sheetHeight = _resolvedProfileSheetHeight(screenHeight, profile);
return _buildProfileSheet(
context: context,
ref: ref,
rtcProvider: rtcProvider,
sheetHeight: sheetHeight,
dataCardBackgroundHeight: screenHeight,
dataCard: dataCard,
isSelf: isSelf,
currentMicIndex: currentMicIndex,
canLeaveMic: canLeaveMic,
currentUserId: currentUserId,
);
},
);
}
Widget _buildLoadingSheet(double sheetHeight) {
return SizedBox(
width: double.infinity,
height: sheetHeight,
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
child: Stack(
children: [
Positioned.fill(child: _buildDefaultSheetBackground()),
Center(
child: Container(
width: 55.w,
height: 55.w,
decoration: BoxDecoration(
color: Colors.black26,
borderRadius: BorderRadius.circular(8.w),
),
child: CupertinoActivityIndicator(color: Colors.white24),
),
),
],
),
),
);
}
double _resolvedProfileSheetHeight(
double screenHeight,
SocialChatUserProfile? profile,
) {
final defaultSheetHeight = screenHeight * _profileSheetHeightFactor;
final hasCpRelation = _hasCpRelation(profile);
final hasCloseFriends = _closeFriendProfileData(profile).isNotEmpty;
var resolvedHeight = defaultSheetHeight;
if (!hasCpRelation) {
resolvedHeight -= 126.w;
}
if (!hasCloseFriends) {
resolvedHeight -= (hasCpRelation ? 52.w : 53.w);
}
final minHeight = screenHeight * 0.36;
return resolvedHeight < minHeight ? minHeight : resolvedHeight;
}
Widget _buildProfileSheet({
required BuildContext context,
required SocialChatUserProfileManager ref,
required RtcProvider rtcProvider,
required double sheetHeight,
required double dataCardBackgroundHeight,
required PropsResources? dataCard,
required bool isSelf,
required num currentMicIndex,
required bool canLeaveMic,
required String? currentUserId,
}) {
final bottomPadding = MediaQuery.of(context).padding.bottom;
final profile = ref.userCardInfo?.userProfile;
final canShowReport = currentUserId != widget.userId;
final canShowSetting =
canShowReport &&
ref.userCardInfo?.roomRole != SCRoomRolesType.HOMEOWNER.name &&
(rtcProvider.isFz() || rtcProvider.isGL());
final profileContent = _buildProfileContentLayer(
context: context,
ref: ref,
rtcProvider: rtcProvider,
profile: profile,
bottomPadding: bottomPadding,
isSelf: isSelf,
canLeaveMic: canLeaveMic,
currentMicIndex: currentMicIndex,
canShowReport: canShowReport,
canShowSetting: canShowSetting,
);
if (dataCard == null) {
return SizedBox(
width: double.infinity,
height: sheetHeight,
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
child: Stack(
children: [
Positioned.fill(child: _buildDefaultSheetBackground()),
Positioned.fill(child: profileContent),
],
),
),
);
}
return _buildDataCardFrame(
sheetHeight: sheetHeight,
backgroundHeight: dataCardBackgroundHeight,
dataCard: dataCard,
child: profileContent,
);
}
Widget _buildDataCardFrame({
required double sheetHeight,
required double backgroundHeight,
required PropsResources dataCard,
required Widget child,
}) {
return SizedBox(
width: double.infinity,
height: sheetHeight,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
child: DecoratedBox(decoration: _sheetDecoration()),
),
),
Positioned(
left: 0,
right: 0,
top:
-(backgroundHeight - sheetHeight) +
sheetHeight * _dataCardBackgroundTranslateFactor,
height: backgroundHeight,
child: IgnorePointer(child: _buildSheetBackground(dataCard)),
),
Positioned.fill(
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
child: child,
),
),
],
),
);
}
Widget _buildProfileContentLayer({
required BuildContext context,
required SocialChatUserProfileManager ref,
required RtcProvider rtcProvider,
required SocialChatUserProfile? profile,
required double bottomPadding,
required bool isSelf,
required bool canLeaveMic,
required num currentMicIndex,
required bool canShowReport,
required bool canShowSetting,
}) {
final hasCpRelation = _hasCpRelation(profile);
final closeFriends = _closeFriendProfileData(profile).take(5).toList();
return Stack(
children: [
Positioned.fill(
child: LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: Alignment.topCenter,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.topCenter,
child: SizedBox(
width: constraints.maxWidth,
child: Padding(
padding: EdgeInsets.fromLTRB(
10.w,
16.w,
10.w,
bottomPadding > 6.w ? bottomPadding : 6.w,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildCpProfileHeader(context, profile, ref),
SizedBox(height: 6.w),
_buildCompactBadgeStrip(
profile?.id ?? widget.userId,
ref,
profile,
),
if (hasCpRelation) ...[
SizedBox(height: 12.w),
_buildCpRelationCard(
context: context,
profile: profile,
isSelf: isSelf,
),
],
if (closeFriends.isNotEmpty) ...[
SizedBox(height: hasCpRelation ? 11.w : 12.w),
_buildCloseFriendStrip(closeFriends),
],
SizedBox(
height:
hasCpRelation || closeFriends.isNotEmpty
? 14.w
: 12.w,
),
_buildActions(
context: context,
ref: ref,
rtcProvider: rtcProvider,
isSelf: isSelf,
canLeaveMic: canLeaveMic,
currentMicIndex: currentMicIndex,
),
],
),
),
),
),
);
},
),
),
if (canShowReport)
Positioned(
top: 14.w,
right: 16.w,
child: _buildHeaderIconButton(
iconAsset: "sc_images/room/sc_icon_user_card_report.png",
onTap: () {
final route =
"${SCMainRoute.report}?type=user&tageId=${widget.userId}";
Navigator.of(context).pop();
SCNavigatorUtils.push(
navigatorKey.currentState?.context ?? context,
route,
replace: false,
);
},
),
),
if (canShowSetting)
Positioned(
top: 14.w,
right: 52.w,
child: _buildHeaderIconButton(
iconAsset: "sc_images/room/sc_icon_room_user_card_setting.png",
onTap: () {
if (userProvider?.userCardInfo == null) {
return;
}
Navigator.of(context).pop();
showBottomInBottomDialog(
navigatorKey.currentState?.context ?? context,
RoomUserCardSetting(
roomId: roomId,
userCardInfo: userProvider?.userCardInfo?.copyWith(),
),
);
},
),
),
],
);
}
BoxDecoration _sheetDecoration() {
return const BoxDecoration(color: Color(0xff061F1E));
}
Widget _buildDefaultSheetBackground() {
return DecoratedBox(
decoration: _sheetDecoration(),
child: SizedBox.expand(
child: Image.asset(
_defaultProfileCardBackgroundAsset,
fit: BoxFit.cover,
),
),
);
}
Widget _buildSheetBackground(PropsResources? dataCard) {
if (dataCard == null) {
return _buildDefaultSheetBackground();
}
return SCDataCardResourceView(
resource: dataCard,
fit: BoxFit.cover,
showCoverFallback: false,
allowCoverAsResource: false,
);
}
PropsResources? _activeDataCard(room_card.RoomUserCardRes? cardInfo) {
return _activeDataCardFromUseProps(cardInfo?.userProfile?.useProps) ??
_activeDataCardFromUseProps(cardInfo?.useProps);
}
PropsResources? _activeDataCardFromUseProps(List<UseProps>? useProps) {
final now = DateTime.now().millisecondsSinceEpoch;
for (final item in useProps ?? const <UseProps>[]) {
final resource = item.propsResources;
if (resource?.type != SCPropsType.DATA_CARD.name) {
continue;
}
final expireText = item.expireTime?.trim() ?? "";
final expireTime = int.tryParse(expireText);
if (expireText.isNotEmpty && (expireTime ?? 0) <= now) {
continue;
}
final source = resource?.sourceUrl?.trim() ?? "";
final cover = resource?.cover?.trim() ?? "";
if (source.isNotEmpty || cover.isNotEmpty) {
return resource;
}
}
return null;
}
Widget _buildCpProfileHeader(
BuildContext context,
SocialChatUserProfile? profile,
SocialChatUserProfileManager ref,
) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
GestureDetector(
onTap: () => _openProfile(context),
child: head(
url: _avatarFallback(profile?.userAvatar),
width: 72.w,
border: Border.all(color: Colors.white.withValues(alpha: 0.85)),
headdress: profile?.getHeaddress()?.sourceUrl,
headdressCover: profile?.getHeaddress()?.cover,
),
),
SizedBox(height: 8.w),
Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4.w,
runSpacing: 4.w,
children: [
msgRoleTag(
ref.userCardInfo?.roomRole ?? "",
width: 16.w,
height: 16.w,
),
socialchatNickNameText(
maxWidth: 174.w,
profile?.userNickname ?? "",
fontSize: 18.sp,
textColor: Colors.white,
fontWeight: FontWeight.bold,
type: "",
needScroll: (profile?.userNickname?.characters.length ?? 0) > 12,
),
getWealthLevel(
profile?.wealthLevel ??
ref.userCardInfo?.userLevel?.wealthLevel ??
0,
width: 42.w,
height: 20.w,
fontSize: 9.sp,
textOffsetX: 3,
),
getUserLevel(
profile?.charmLevel ??
ref.userCardInfo?.userLevel?.charmLevel ??
0,
width: 42.w,
height: 20.w,
fontSize: 9.sp,
textOffsetX: 3,
),
],
),
SizedBox(height: 4.w),
_buildSpecialIdMetaRow(context, profile),
],
);
}
Widget _buildCompactBadgeStrip(
String? userId,
SocialChatUserProfileManager ref,
SocialChatUserProfile? profile,
) {
return SizedBox(
width: double.infinity,
height: 16.w,
child: Center(
child: SCUserBadgeStrip(
userId: userId,
fallbackBadges: _roomCardFallbackBadges(ref, profile),
displayScope: SCUserBadgeDisplayScope.short,
height: 16.w,
badgeHeight: 16.w,
longBadgeWidth: 32.w,
spacing: 4.w,
maxBadges: 3,
wrap: false,
reserveSpace: false,
),
),
);
}
Widget _buildCpRelationCard({
required BuildContext context,
required SocialChatUserProfile? profile,
required bool isSelf,
}) {
final cp = _cpProfileData(profile);
final card = SizedBox(
height: 114.w,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: Image.asset(
_cpLevelAsset(cp.levelText),
fit: BoxFit.fill,
errorBuilder:
(_, __, ___) => Image.asset(
"$_cpProfileAssetBase/sc_cp_profile_relation_card_bg.png",
fit: BoxFit.fill,
),
),
),
Positioned(right: 10.w, top: 8.w, child: const SCCpCornerLabel()),
Positioned(
left: 39.w,
top: 26.w,
child: _buildCpAvatar(cp.leftAvatarUrl),
),
Positioned(
right: 38.w,
top: 26.w,
child: _buildCpAvatar(cp.rightAvatarUrl),
),
Positioned(
left: 142.w,
right: 142.w,
top: 25.w,
height: 22.w,
child: _buildCpCardText(
cp.days,
color: const Color(0xffFF0979),
fontSize: 20,
fontWeight: FontWeight.w700,
),
),
Positioned(
left: 142.w,
right: 142.w,
top: 48.w,
height: 14.w,
child: _buildCpCardText(
"Days",
color: const Color(0xffFF0979),
fontSize: 12,
fontWeight: FontWeight.w400,
),
),
Positioned(
left: 110.w,
right: 110.w,
top: 78.w,
height: 16.w,
child: _buildCpCardText(
cp.levelText,
color: const Color(0xffFEFEFF),
fontSize: 11,
fontWeight: FontWeight.w400,
),
),
],
),
);
if (!isSelf) {
return card;
}
return SCDebounceWidget(
onTap: () => _openCpProgress(context, profile),
child: card,
);
}
Widget _buildCpCardText(
String value, {
required Color color,
required double fontSize,
required FontWeight fontWeight,
}) {
return FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.center,
child: Text(
value,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.visible,
textAlign: TextAlign.center,
textScaler: TextScaler.noScaling,
style: TextStyle(
color: color,
fontSize: fontSize.sp,
height: 1,
fontWeight: fontWeight,
decoration: TextDecoration.none,
),
),
);
}
Widget _buildCpAvatar(String avatarUrl) {
final trimmedUrl = avatarUrl.trim();
return Container(
width: 60.w,
height: 60.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white.withValues(alpha: 0.86)),
),
clipBehavior: Clip.antiAlias,
child:
trimmedUrl.isEmpty
? const SizedBox.shrink()
: netImage(
url: trimmedUrl,
width: 60.w,
height: 60.w,
fit: BoxFit.cover,
noDefaultImg: true,
loadingWidget: const SizedBox.shrink(),
errorWidget: const SizedBox.shrink(),
),
);
}
Widget _buildCloseFriendStrip(List<String> friends) {
return SizedBox(
height: 41.w,
width: double.infinity,
child: Stack(
clipBehavior: Clip.none,
children: [
Positioned.fill(
child: Image.asset(
"$_cpProfileAssetBase/sc_cp_profile_close_friend_bg.png",
fit: BoxFit.fill,
),
),
PositionedDirectional(
top: 7.w,
end: 11.w,
width: 118.w,
height: 27.w,
child: DecoratedBox(
decoration: BoxDecoration(
color: const Color(0xffebe2ff).withValues(alpha: 0.92),
borderRadius: BorderRadius.circular(14.w),
),
),
),
PositionedDirectional(
top: 8.w,
end: 13.w,
child: SizedBox(
height: 25.w,
width: 112.w,
child: Stack(
alignment: AlignmentDirectional.centerEnd,
children: [
for (var index = 0; index < friends.length; index++)
PositionedDirectional(
end: (friends.length - 1 - index) * 17.w,
child: _buildCloseFriendAvatar(friends[index]),
),
],
),
),
),
],
),
);
}
_RoomProfileCpData _cpProfileData(SocialChatUserProfile? profile) {
final currentProfile = AccountStorage().getCurrentUser()?.userProfile;
final cp = _firstRelation(
profile?.cpList,
"CP",
profile: profile,
requirePartner: true,
);
final owner = _relationOwner(profile, cp);
final partner = _relationPartner(profile, cp);
return _RoomProfileCpData(
leftAvatarUrl: _avatarFallback(
owner.avatarUrl ?? profile?.userAvatar ?? currentProfile?.userAvatar,
),
rightAvatarUrl: partner.avatarUrl?.trim() ?? "",
leftName: _firstNonBlank([
owner.nickname,
profile?.userNickname,
currentProfile?.userNickname,
]),
rightName: _firstNonBlank([partner.nickname, partner.account]),
leftUserId: _firstNonBlank([
owner.userId,
profile?.id,
currentProfile?.id,
]),
rightUserId: _firstNonBlank([partner.userId]),
days: scCpRelationDisplayDays(_firstNonBlank([cp?.days, "0"])),
relationType: _normalizeCpRelationType(cp?.relationType),
dismissCost: cp?.dismissCost,
cpValue: cp?.cpValue ?? 0,
levelText:
cp == null
? ""
: _cpLevelDisplayText(
_firstNonBlank([cp.levelText, "Lv.1 Simple Love"]),
),
);
}
CPRes? _firstRelation(
List<CPRes>? relations,
String relationType, {
SocialChatUserProfile? profile,
bool requirePartner = false,
}) {
for (final relation in relations ?? const <CPRes>[]) {
if (_normalizeCpRelationType(relation.relationType) == relationType &&
(!requirePartner || _relationHasPartner(profile, relation))) {
return relation;
}
}
return null;
}
bool _hasCpRelation(SocialChatUserProfile? profile) {
return _firstRelation(
profile?.cpList,
"CP",
profile: profile,
requirePartner: true,
) !=
null;
}
List<String> _closeFriendProfileData(SocialChatUserProfile? profile) {
final avatars = <String>[];
for (final relation in profile?.closeFriendList ?? const <CPRes>[]) {
final type = _normalizeCpRelationType(relation.relationType);
if (type != "BROTHER" && type != "SISTERS") {
continue;
}
if (!_relationHasPartner(profile, relation)) {
continue;
}
final friend = _relationPartner(profile, relation);
final avatar = friend.avatarUrl?.trim() ?? "";
if (avatar.isNotEmpty) {
avatars.add(avatar);
}
}
return avatars;
}
Widget _buildCloseFriendAvatar(String avatarUrl) {
return Container(
width: 25.w,
height: 25.w,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(
color: Colors.white.withValues(alpha: 0.85),
width: 1.w,
),
),
clipBehavior: Clip.antiAlias,
child: netImage(
url: avatarUrl,
width: 25.w,
height: 25.w,
fit: BoxFit.cover,
noDefaultImg: true,
loadingWidget: const SizedBox.shrink(),
errorWidget: const SizedBox.shrink(),
),
);
}
String _cpLevelAsset(String levelText) {
return "$_cpProfileAssetBase/sc_cp_profile_cp_level_${_cpLevelIndex(levelText)}.png";
}
int _cpLevelIndex(String levelText) {
return scCpRelationLevelIndex(levelText);
}
String _cpLevelDisplayText(String rawText) {
return scCpRelationLevelDisplayText(rawText);
}
bool _relationHasPartner(SocialChatUserProfile? profile, CPRes relation) {
final partner = _relationPartner(profile, relation);
return _firstNonBlank([
partner.userId,
partner.account,
partner.nickname,
partner.avatarUrl,
]).isNotEmpty;
}
_RoomRelationUser _relationOwner(
SocialChatUserProfile? profile,
CPRes? relation,
) {
if (relation == null) {
return _RoomRelationUser(
userId: profile?.id,
account: profile?.account,
nickname: profile?.userNickname,
avatarUrl: profile?.userAvatar,
);
}
final profileId = profile?.id?.trim() ?? "";
if (profileId.isNotEmpty && profileId == (relation.cpUserId ?? "")) {
return _RoomRelationUser(
userId: relation.cpUserId,
account: relation.cpAccount,
nickname: relation.cpUserNickname,
avatarUrl: relation.cpUserAvatar,
);
}
return _RoomRelationUser(
userId: relation.meUserId,
account: relation.meAccount,
nickname: relation.meUserNickname,
avatarUrl: relation.meUserAvatar,
);
}
_RoomRelationUser _relationPartner(
SocialChatUserProfile? profile,
CPRes? relation,
) {
if (relation == null) {
return const _RoomRelationUser();
}
final profileId = profile?.id?.trim() ?? "";
if (profileId.isNotEmpty && profileId == (relation.cpUserId ?? "")) {
return _RoomRelationUser(
userId: relation.meUserId,
account: relation.meAccount,
nickname: relation.meUserNickname,
avatarUrl: relation.meUserAvatar,
);
}
return _RoomRelationUser(
userId: relation.cpUserId,
account: relation.cpAccount,
nickname: relation.cpUserNickname,
avatarUrl: relation.cpUserAvatar,
);
}
String _normalizeCpRelationType(String? relationType) {
final value = relationType?.trim().toUpperCase() ?? "";
switch (value) {
case "BROTHER":
case "BROTHERS":
case "SWORN_BROTHER":
case "SWORN_BROTHERS":
return "BROTHER";
case "SISTER":
case "SISTERS":
return "SISTERS";
default:
return "CP";
}
}
String _avatarFallback(String? avatarUrl) {
final value = avatarUrl?.trim() ?? "";
if (value.isNotEmpty) {
return value;
}
return AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? "";
}
String _firstNonBlank(List<String?> values) {
for (final value in values) {
final trimmed = value?.trim() ?? "";
if (trimmed.isNotEmpty) {
return trimmed;
}
}
return "";
}
// ignore: unused_element
Widget _buildProfileHeader(
BuildContext context,
SocialChatUserProfile? profile,
SocialChatUserProfileManager ref,
) {
return Column(
children: [
GestureDetector(
onTap: () => _openProfile(context),
child: head(
url: profile?.userAvatar ?? "",
width: 105.w,
border: Border.all(color: Colors.white, width: 3),
headdress: profile?.getHeaddress()?.sourceUrl,
headdressCover: profile?.getHeaddress()?.cover,
),
),
SizedBox(height: 5.w),
Wrap(
alignment: WrapAlignment.center,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 4.w,
runSpacing: 5.w,
children: [
msgRoleTag(
ref.userCardInfo?.roomRole ?? "",
width: 16.w,
height: 16.w,
),
socialchatNickNameText(
maxWidth: 116.w,
profile?.userNickname ?? "",
fontSize: 18.sp,
textColor: Colors.white,
fontWeight: FontWeight.bold,
type: "",
needScroll: (profile?.userNickname?.characters.length ?? 0) > 12,
),
getVIPBadge(
profile?.vipLevel ?? profile?.getVIP()?.name,
width: 45.w,
height: 25.w,
),
getWealthLevel(
profile?.wealthLevel ??
ref.userCardInfo?.userLevel?.wealthLevel ??
0,
width: 42.w,
height: 20.w,
fontSize: 9.sp,
textOffsetX: 3,
),
getUserLevel(
profile?.charmLevel ??
ref.userCardInfo?.userLevel?.charmLevel ??
0,
width: 42.w,
height: 20.w,
fontSize: 9.sp,
textOffsetX: 3,
),
],
),
SizedBox(height: 3.w),
_buildHeaderLongBadgeStrip(profile?.id ?? widget.userId, ref, profile),
SizedBox(height: 2.w),
_buildSpecialIdMetaRow(context, profile),
],
);
}
Widget _buildSpecialIdMetaRow(
BuildContext context,
SocialChatUserProfile? profile,
) {
final flagUrl = _countryFlagUrl(context, profile);
final hasSex = profile?.userSex != null;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
SCSpecialIdBadge(
idText: profile?.getID() ?? "",
showAnimated: profile?.hasSpecialId() ?? false,
assetPath: SCSpecialIdAssets.userId,
animationWidth: 62.w,
animationHeight: 24.w,
showTextBesideAnimated: true,
animatedTextSpacing: 0,
showAnimatedGradientText:
profile?.shouldShowColoredSpecialIdText() ?? false,
animationTextStyle: TextStyle(
color: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
normalTextStyle: TextStyle(
color: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
animationFit: BoxFit.contain,
),
if (flagUrl.isNotEmpty) ...[
SizedBox(width: 8.w),
_buildCountryFlag(flagUrl),
],
if (hasSex) ...[SizedBox(width: 7.w), _buildSexIcon(profile)],
],
);
}
String _countryFlagUrl(BuildContext context, SocialChatUserProfile? profile) {
final countryName = profile?.countryName ?? "";
try {
return Provider.of<SCAppGeneralManager>(
context,
listen: false,
).findCountryByName(countryName)?.nationalFlag ??
"";
} catch (_) {
return "";
}
}
Widget _buildCountryFlag(String flagUrl) {
final trimmedUrl = flagUrl.trim();
if (trimmedUrl.isEmpty) {
return const SizedBox.shrink();
}
return ClipRRect(
borderRadius: BorderRadius.circular(2.w),
child: netImage(
url: trimmedUrl,
width: 22.w,
height: 15.w,
fit: BoxFit.cover,
noDefaultImg: true,
),
);
}
Widget _buildSexIcon(SocialChatUserProfile? profile) {
if (profile?.userSex == null) {
return const SizedBox.shrink();
}
return xb(profile?.userSex, height: 15.w, color: null);
}
Widget _buildHeaderLongBadgeStrip(
String? userId,
SocialChatUserProfileManager ref,
SocialChatUserProfile? profile,
) {
return SizedBox(
width: double.infinity,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 10.w),
child: SCUserBadgeStrip(
userId: userId,
fallbackBadges: _roomCardFallbackBadges(ref, profile),
displayScope: SCUserBadgeDisplayScope.long,
height: 62.w,
badgeHeight: 20.w,
longBadgeWidth: 62.w,
spacing: 4.w,
runSpacing: 4.w,
wrap: true,
maxWrapRows: 2,
scrollLastWrapRow: true,
reserveSpace: false,
),
),
);
}
// ignore: unused_element
Widget _buildBadgeStrip(
String? userId,
SocialChatUserProfileManager ref,
SocialChatUserProfile? profile,
) {
return SizedBox(
width: double.infinity,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 10.w),
child: SCUserBadgeStrip(
userId: userId,
fallbackBadges: _roomCardFallbackBadges(ref, profile),
displayScope: SCUserBadgeDisplayScope.short,
height: 62.w,
badgeHeight: 57.5.w,
longBadgeWidth: 145.w,
spacing: 12.w,
maxBadges: 12,
wrap: false,
reserveSpace: false,
),
),
);
}
List<WearBadge> _roomCardFallbackBadges(
SocialChatUserProfileManager ref,
SocialChatUserProfile? profile,
) {
final wearBadges =
profile?.wearBadge
?.where((item) => item.use != false && _hasBadgeUrl(item))
.toList() ??
const <WearBadge>[];
if (wearBadges.isNotEmpty) {
return wearBadges;
}
return ref.userCardInfo?.useBadge
?.map<WearBadge>(_roomUseBadgeToWearBadge)
.where(_hasBadgeUrl)
.toList() ??
const <WearBadge>[];
}
WearBadge _roomUseBadgeToWearBadge(room_card.UseBadge badge) {
return WearBadge(
animationUrl: badge.animationUrl,
badgeKey: badge.badgeKey,
badgeLevel: badge.badgeLevel,
badgeName: badge.badgeName,
expireTime: badge.expireTime?.toInt(),
id: badge.id,
milestone: badge.milestone,
notSelectUrl: badge.notSelectUrl,
selectUrl: badge.selectUrl,
type: badge.type,
userId: badge.userId,
use: true,
);
}
bool _hasBadgeUrl(WearBadge badge) {
return (badge.selectUrl ?? "").trim().isNotEmpty;
}
Widget _buildActions({
required BuildContext context,
required SocialChatUserProfileManager ref,
required RtcProvider rtcProvider,
required bool isSelf,
required bool canLeaveMic,
required num currentMicIndex,
}) {
if (isSelf) {
if (!canLeaveMic) {
return SizedBox(height: 55.w);
}
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
width: 92.w,
child: _buildSheetAction(
iconAsset: _leaveMicActionIconAsset,
label: SCAppLocalizations.of(context)!.leavelTheMic,
onTap: () {
Navigator.of(context).pop();
rtcProvider.xiaMai(currentMicIndex);
},
),
),
],
);
}
return Row(
children: [
Expanded(
child: _buildSheetAction(
iconAsset: "sc_images/room/sc_icon_send_user_message.png",
label: SCAppLocalizations.of(context)!.sayHi2,
onTap: () => _openChat(context),
),
),
Expanded(child: _buildFollowAction(context)),
Expanded(
child: _buildSheetAction(
iconAsset: "sc_images/room/sc_icon_at_tag_user.png",
label: SCAppLocalizations.of(context)!.atTag,
onTap: () => _openAtInput(context, ref),
),
),
Expanded(
child: _buildSheetAction(
iconAsset: "sc_images/room/sc_icon_send_user_gift.png",
label: SCAppLocalizations.of(context)!.send,
onTap: () => _openGift(context, ref),
),
),
],
);
}
Widget _buildSheetAction({
required String iconAsset,
required String label,
required VoidCallback onTap,
}) {
return SCDebounceWidget(
onTap: onTap,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 42.w,
height: 42.w,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.58),
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xff18F2B1).withValues(alpha: 0.18),
),
),
alignment: Alignment.center,
child: Image.asset(iconAsset, width: 28.w, height: 28.w),
),
SizedBox(height: 5.w),
SizedBox(
height: 18.w,
child: text(
label,
maxLines: 1,
textAlign: TextAlign.center,
textColor: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 12.sp,
lineHeight: 1,
),
),
],
),
);
}
Widget _buildFollowAction(BuildContext context) {
return SCDebounceWidget(
onTap: () {
final provider = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
);
provider.followUser(widget.userId ?? "");
},
child: Selector<SocialChatUserProfileManager, bool>(
selector: (context, provider) => provider.userCardInfo?.follow ?? false,
shouldRebuild: (prev, next) => prev != next,
builder: (context, follow, _) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 42.w,
height: 42.w,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.58),
shape: BoxShape.circle,
border: Border.all(
color: const Color(0xff18F2B1).withValues(alpha: 0.18),
),
),
alignment: Alignment.center,
child: Image.asset(
follow
? "sc_images/room/sc_icon_user_un_follow.png"
: "sc_images/room/sc_icon_user_follow.png",
width: 28.w,
height: 28.w,
),
),
SizedBox(height: 5.w),
SizedBox(
height: 18.w,
child: text(
follow
? SCAppLocalizations.of(context)!.followed
: SCAppLocalizations.of(context)!.follow,
maxLines: 1,
textAlign: TextAlign.center,
textColor: Colors.white,
fontWeight: FontWeight.w600,
fontSize: 12.sp,
lineHeight: 1,
),
),
],
);
},
),
);
}
Widget _buildHeaderIconButton({
required String iconAsset,
required VoidCallback onTap,
}) {
return SCDebounceWidget(
onTap: onTap,
child: Container(
width: 28.w,
height: 28.w,
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.45),
shape: BoxShape.circle,
),
alignment: Alignment.center,
child: Image.asset(
iconAsset,
width: 18.w,
height: 18.w,
color: Colors.black,
),
),
);
}
void _openProfile(BuildContext context) {
final route =
"${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == widget.userId}&tageId=${widget.userId}";
Navigator.of(context).pop();
SCNavigatorUtils.push(navigatorKey.currentState?.context ?? context, route);
}
Future<void> _openChat(BuildContext context) async {
final rtmProvider = Provider.of<RtmProvider>(context, listen: false);
Navigator.of(context).pop();
var conversation = V2TimConversation(
type: ConversationType.V2TIM_C2C,
userID: widget.userId,
conversationID: '',
);
final didStart = await rtmProvider.startConversation(conversation);
if (!didStart || !mounted) return;
var json = jsonEncode(conversation.toJson());
SCNavigatorUtils.push(
navigatorKey.currentState!.context,
"${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}",
);
}
Future<void> _openAtInput(
BuildContext context,
SocialChatUserProfileManager ref,
) async {
if (!SCRoomUtils.touristCanMsg(context)) {
return;
}
final nickname = ref.userCardInfo?.userProfile?.userNickname ?? "";
if (nickname.isEmpty) {
return;
}
Navigator.of(context).pop();
Navigator.push(
navigatorKey.currentState?.context ?? context,
PopRoute(
child: RoomMsgInput(
atTextContent: "<user id=\"${widget.userId}\">$nickname</user>",
),
),
);
}
void _openGift(BuildContext context, SocialChatUserProfileManager ref) {
Navigator.of(context).pop();
SmartDialog.show(
tag: "showGiftControl",
alignment: Alignment.bottomCenter,
maskColor: Colors.transparent,
animationType: SmartAnimationType.fade,
clickMaskDismiss: true,
builder: (_) {
return SCRoomTopLayerSuppressor(
reason: 'room_user_gift_panel',
child: GiftPage(toUser: ref.userCardInfo?.userProfile),
);
},
);
}
Future<void> _openCpProgress(
BuildContext context,
SocialChatUserProfile? profile,
) async {
final cp = _cpProfileData(profile);
final cancelText = SCAppLocalizations.of(context)!.cancel;
SCLoadingManager.show();
late final _RoomCpProgressData progress;
try {
progress = await _loadCpProgressData(cp);
} finally {
SCLoadingManager.hide();
}
if (!mounted || !context.mounted) {
return;
}
RoomCpProgressDialog.show(
context,
leftUser: RoomCpProgressDialogUser(
nickname: cp.leftName,
userId: cp.leftUserId,
avatarUrl: cp.leftAvatarUrl,
),
rightUser: RoomCpProgressDialogUser(
nickname: cp.rightName,
userId: cp.rightUserId,
avatarUrl: cp.rightAvatarUrl,
),
daysValue: cp.days,
levelText: progress.levelText,
currentValue: progress.currentValue,
targetValue: progress.targetValue,
expAwayText: progress.expAwayText,
nextLevelText: progress.nextLevelText,
cancelText: cancelText,
onCancel:
() => _showCpDismissConfirm(
context,
cp,
dismissCost: progress.dismissCost,
),
);
}
Future<_RoomCpProgressData> _loadCpProgressData(_RoomProfileCpData cp) async {
final partnerUserId = cp.rightUserId.trim();
final fallback = _RoomCpProgressData.fallback(cp);
if (partnerUserId.isEmpty) {
return fallback;
}
var latestFallback = fallback;
try {
final config = await SCAccountRepository().cpRightsConfig(
relationType: cp.relationType,
cpUserId: partnerUserId,
);
final progress = _RoomCpProgressData.fromRightsConfig(config, cp);
latestFallback = progress;
if (config.levels.isNotEmpty) {
return progress;
}
} catch (_) {
// Fall back to the Java-current CP cabin endpoint below.
}
try {
final cabin = await SCAccountRepository().cpCabin(
cp.leftUserId,
relationType: cp.relationType,
);
return _RoomCpProgressData.fromCabin(
cabin,
cp,
fallbackDismissCost: latestFallback.dismissCost,
);
} catch (_) {
return latestFallback;
}
}
Future<void> _showCpDismissConfirm(
BuildContext context,
_RoomProfileCpData cp, {
int? dismissCost,
}) async {
final l10n = SCAppLocalizations.of(context)!;
final partnerUserId = cp.rightUserId.trim();
if (partnerUserId.isEmpty) {
return;
}
await showRoomCpDismissConfirmDialog(
context,
message: l10n.cpDismissConfirmMessage(
dismissCost == null ? "--" : "$dismissCost",
),
cancelText: l10n.cancel,
confirmText: l10n.confirm,
onConfirm: () async {
SCLoadingManager.show();
try {
await SCAccountRepository().cpRelationshipDismissApply(
partnerUserId,
relationType: cp.relationType,
);
await userProvider?.refreshLoadedCpProfiles();
await SmartDialog.dismiss(tag: RoomCpProgressDialog.dialogTag);
SCTts.show(l10n.operationSuccessful);
} catch (_) {
// The repository/network layer already surfaces the backend error.
} finally {
SCLoadingManager.hide();
}
},
);
}
String roleName(String role) {
if (role == SCRoomRolesType.HOMEOWNER.name) {
return SCAppLocalizations.of(context)!.owner;
} else if (role == SCRoomRolesType.ADMIN.name) {
return SCAppLocalizations.of(context)!.admin;
} else if (role == SCRoomRolesType.MEMBER.name) {
return SCAppLocalizations.of(context)!.member;
}
return SCAppLocalizations.of(context)!.guest;
}
// ignore: unused_element
_buildEmptyCpItem(bool canAdd) {
return Container(
width: ScreenUtil().screenWidth,
margin: EdgeInsets.symmetric(horizontal: 12.w),
height: 105.w,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(
canAdd
? "sc_images/person/sc_icon_no_cp_item_bg.png"
: "sc_images/person/sc_icon_no_cp_item_bg4.png",
),
fit: BoxFit.fill,
),
),
child: Container(
margin: EdgeInsets.only(bottom: 13.w),
alignment: AlignmentDirectional.bottomCenter,
child: text(
SCAppLocalizations.of(context)!.noMatchedCP,
fontWeight: FontWeight.w600,
fontSize: 12.sp,
textColor: canAdd ? Color(0xFFFF79A1) : Colors.white,
),
),
);
}
// ignore: unused_element
Widget _buildCpItem(CPRes item) {
return Stack(
alignment: AlignmentDirectional.center,
children: [
Container(
width: ScreenUtil().screenWidth,
margin: EdgeInsets.symmetric(horizontal: 15.w),
height: 135.w,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage("sc_images/person/sc_icon_no_cp_item_bg2.png"),
fit: BoxFit.fill,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(height: 68.w),
Container(
alignment: AlignmentDirectional.bottomCenter,
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Image.asset(
"sc_images/person/sc_icon_cp_value_tag.png",
height: 18.w,
),
SizedBox(width: 5.w),
text(
_cpVaFormat(item.cpValue ?? 0),
fontWeight: FontWeight.w600,
fontSize: 12.sp,
textColor: Color(0xFFFF79A1),
),
],
),
),
SizedBox(height: 8.w),
Container(
alignment: AlignmentDirectional.bottomCenter,
child: text(
SCAppLocalizations.of(
context,
)!.timeSpentTogether(scCpRelationDisplayDays(item.days)),
fontWeight: FontWeight.w600,
fontSize: 12.sp,
textColor: Color(0xFFFF79A1),
),
),
Container(
alignment: AlignmentDirectional.bottomCenter,
child: text(
SCAppLocalizations.of(context)!.firstDay(item.firstDay ?? ""),
fontWeight: FontWeight.w600,
fontSize: 11.sp,
textColor: Colors.white,
),
),
],
),
),
PositionedDirectional(
top: 30.w,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Column(
children: [
GestureDetector(
child: SizedBox(
height: 50.w,
child: Stack(
alignment: AlignmentDirectional.center,
children: [
netImage(
url: item.meUserAvatar ?? "",
width: 48.w,
shape: BoxShape.circle,
defaultImg:
"sc_images/general/sc_icon_avar_defalt.png",
),
Image.asset(
"sc_images/person/sc_icon_cp_head_ring.png",
),
],
),
),
onTap: () {
SCNavigatorUtils.push(
context,
replace: false,
"${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == item.meUserId}&tageId=${item.meUserId}",
);
},
),
Container(
alignment: Alignment.center,
width: 80.w,
height: 15.w,
child:
(item.meUserNickname?.length ?? 0) > 8
? Marquee(
text: item.meUserNickname ?? "",
style: TextStyle(
fontSize: 10.sp,
color: Colors.white,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
scrollAxis: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.start,
blankSpace: 20.0,
velocity: 40.0,
pauseAfterRound: Duration(seconds: 1),
accelerationDuration: Duration(seconds: 1),
accelerationCurve: Curves.easeOut,
decelerationDuration: Duration(milliseconds: 550),
decelerationCurve: Curves.easeOut,
)
: Text(
item.meUserNickname ?? "",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 10.sp,
color: Colors.white,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
),
],
),
SizedBox(width: 120.w),
Column(
children: [
GestureDetector(
child: SizedBox(
height: 50.w,
child: Stack(
alignment: AlignmentDirectional.center,
children: [
netImage(
url: item.cpUserAvatar ?? "",
width: 48.w,
shape: BoxShape.circle,
defaultImg:
"sc_images/general/sc_icon_avar_defalt.png",
),
Image.asset(
"sc_images/person/sc_icon_cp_head_ring.png",
),
],
),
),
onTap: () {
SCNavigatorUtils.push(
context,
replace: false,
"${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == item.cpUserId}&tageId=${item.cpUserId}",
);
},
),
Container(
alignment: Alignment.center,
width: 80.w,
height: 15.w,
child:
(item.cpUserNickname?.length ?? 0) > 8
? Marquee(
text: item.cpUserNickname ?? "",
style: TextStyle(
fontSize: 10.sp,
color: Colors.white,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
scrollAxis: Axis.horizontal,
crossAxisAlignment: CrossAxisAlignment.start,
blankSpace: 20.0,
velocity: 40.0,
pauseAfterRound: Duration(seconds: 1),
accelerationDuration: Duration(seconds: 1),
accelerationCurve: Curves.easeOut,
decelerationDuration: Duration(milliseconds: 550),
decelerationCurve: Curves.easeOut,
)
: Text(
item.cpUserNickname ?? "",
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 10.sp,
color: Colors.white,
fontWeight: FontWeight.w600,
decoration: TextDecoration.none,
),
),
),
],
),
],
),
),
],
);
}
String getUserCardBg() {
return "";
}
String _cpVaFormat(double cpValue) {
return SCStringUtils.formatCompactNumber(cpValue);
}
}
class _RoomCpProgressData {
const _RoomCpProgressData({
required this.levelText,
required this.currentValue,
required this.targetValue,
required this.expAwayText,
required this.nextLevelText,
this.dismissCost,
});
factory _RoomCpProgressData.fallback(_RoomProfileCpData cp) {
final currentValue = cp.cpValue;
return _RoomCpProgressData(
levelText:
cp.levelText.trim().isEmpty ? "Lv.1 Simple Love" : cp.levelText,
currentValue: currentValue,
targetValue: currentValue > 0 ? currentValue : 1,
expAwayText: SCStringUtils.formatCompactNumber(0),
nextLevelText: _levelShortText(
_nextLevelIndex(_levelIndex(cp.levelText)),
),
dismissCost: cp.dismissCost,
);
}
factory _RoomCpProgressData.fromRightsConfig(
SCCpRightsConfigRes config,
_RoomProfileCpData cp,
) {
final currentValue = (config.intimacyValue ?? cp.cpValue).toDouble();
final levels = config.levels.map(_CpProgressLevel.fromRightsLevel).toList();
return _RoomCpProgressData.fromLevels(
currentValue: currentValue,
levels: levels,
cp: cp,
dismissCost: config.dismissConsumeGold ?? cp.dismissCost,
);
}
factory _RoomCpProgressData.fromCabin(
SCCpCabinRes cabin,
_RoomProfileCpData cp, {
int? fallbackDismissCost,
}) {
final currentValue =
(cabin.cpVal?.toDouble() ??
cabin.cpPairUserProfileCO?.cpValue ??
cp.cpValue);
final levels =
(cabin.cpCabinConfigCO ?? const <dynamic>[])
.map(_CpProgressLevel.fromDynamic)
.toList();
return _RoomCpProgressData.fromLevels(
currentValue: currentValue,
levels: levels,
cp: cp,
dismissCost: cabin.dismissCost ?? fallbackDismissCost ?? cp.dismissCost,
);
}
factory _RoomCpProgressData.fromLevels({
required double currentValue,
required List<_CpProgressLevel> levels,
required _RoomProfileCpData cp,
int? dismissCost,
}) {
final sortedLevels =
levels.where((level) => level.requiredValue != null).toList()..sort(
(left, right) => left.requiredValue!.compareTo(right.requiredValue!),
);
_CpProgressLevel? nextLevel;
for (final level in sortedLevels) {
if ((level.requiredValue ?? 0) > currentValue) {
nextLevel = level;
break;
}
}
final targetValue =
nextLevel?.requiredValue?.toDouble() ??
(currentValue > 0 ? currentValue : 1);
final expAway =
nextLevel == null
? 0
: (targetValue - currentValue).ceil().clamp(0, 1 << 62).toInt();
final currentLevel = _currentLevelFromConfig(sortedLevels, currentValue);
return _RoomCpProgressData(
levelText: _levelDisplayText(currentLevel, cp.levelText),
currentValue: currentValue,
targetValue: targetValue > 0 ? targetValue : 1,
expAwayText: SCStringUtils.formatCompactNumber(expAway),
nextLevelText:
nextLevel == null
? _levelShortText(
_nextLevelIndex(
currentLevel?.level ?? _levelIndex(cp.levelText),
),
)
: _levelShortText(nextLevel.level, fallback: nextLevel.levelName),
dismissCost: dismissCost,
);
}
final String levelText;
final double currentValue;
final double targetValue;
final String expAwayText;
final String nextLevelText;
final int? dismissCost;
static _CpProgressLevel? _currentLevelFromConfig(
List<_CpProgressLevel> levels,
double currentValue,
) {
_CpProgressLevel? currentLevel;
for (final level in levels) {
if ((level.requiredValue ?? 0) <= currentValue) {
currentLevel = level;
}
}
return currentLevel;
}
static String _levelDisplayText(_CpProgressLevel? level, String fallback) {
final fallbackText = fallback.trim();
if (level == null) {
return fallbackText.isEmpty ? "Lv.1 Simple Love" : fallbackText;
}
final levelName = level.levelName?.trim();
if (levelName != null && levelName.isNotEmpty) {
return levelName;
}
if (level.level != null) {
return "Lv.${level.level}";
}
return fallbackText.isEmpty ? "Lv.1 Simple Love" : fallbackText;
}
static String _levelShortText(int? level, {String? fallback}) {
if (level != null) {
return "Lv. $level";
}
final fallbackText = fallback?.trim() ?? "";
if (fallbackText.isNotEmpty) {
return fallbackText;
}
return "Lv. 1";
}
static int _levelIndex(String levelText) {
return scCpRelationLevelIndex(levelText);
}
static int _nextLevelIndex(int level) {
return (level + 1).clamp(1, 5).toInt();
}
}
class _CpProgressLevel {
const _CpProgressLevel({this.level, this.levelName, this.requiredValue});
factory _CpProgressLevel.fromRightsLevel(SCCpRightsLevelRes level) {
return _CpProgressLevel(
level: level.level,
levelName: level.levelName,
requiredValue: level.requiredIntimacyValue,
);
}
factory _CpProgressLevel.fromDynamic(dynamic value) {
final source = value is Map ? value : const <String, dynamic>{};
return _CpProgressLevel(
level: _asInt(
_firstValue(source, const ["level", "cpLevel", "cabinLevel", "lv"]),
),
levelName: _asString(
_firstValue(source, const [
"levelName",
"name",
"title",
"cabinName",
"cpName",
]),
),
requiredValue: _asInt(
_firstValue(source, const [
"requiredIntimacyValue",
"requiredCpValue",
"requiredCpVal",
"cpValue",
"cpVal",
"intimacyValue",
]),
),
);
}
final int? level;
final String? levelName;
final int? requiredValue;
static dynamic _firstValue(Map source, List<String> keys) {
for (final key in keys) {
if (source.containsKey(key) && source[key] != null) {
return source[key];
}
}
return null;
}
static String? _asString(dynamic value) {
final text = value?.toString().trim();
return text == null || text.isEmpty ? null : text;
}
static int? _asInt(dynamic value) {
if (value is int) {
return value;
}
if (value is num) {
return value.toInt();
}
return int.tryParse(value?.toString() ?? "");
}
}
class _RoomProfileCpData {
const _RoomProfileCpData({
required this.leftAvatarUrl,
required this.rightAvatarUrl,
required this.leftName,
required this.rightName,
required this.leftUserId,
required this.rightUserId,
required this.days,
required this.levelText,
required this.relationType,
required this.cpValue,
this.dismissCost,
});
final String leftAvatarUrl;
final String rightAvatarUrl;
final String leftName;
final String rightName;
final String leftUserId;
final String rightUserId;
final String days;
final String levelText;
final String relationType;
final double cpValue;
final int? dismissCost;
}
class _RoomRelationUser {
const _RoomRelationUser({
this.userId,
this.account,
this.nickname,
this.avatarUrl,
});
final String? userId;
final String? account;
final String? nickname;
final String? avatarUrl;
}