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 createState() => _RoomUserInfoCardState(); } class _RoomUserInfoCardState extends State { 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; int? _userCardSessionSerial; @override void initState() { super.initState(); userProvider = Provider.of( context, listen: false, ); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) { return; } _loadRoomUserCard(); }); SCAccountRepository().userIdentity(userId: widget.userId).then((v) { if (!mounted) { return; } userIdentity = v; setState(() {}); }); } void _loadRoomUserCard() { userProvider ??= Provider.of( context, listen: false, ); roomId = Provider.of( context, listen: false, ).currenRoom?.roomProfile?.roomProfile?.id ?? ""; final sessionSerial = userProvider?.beginRoomUserCardSession(); _userCardSessionSerial = sessionSerial; userProvider?.roomUserCard( roomId, widget.userId ?? "", sessionSerial: sessionSerial, ); } @override void dispose() { super.dispose(); userProvider?.clearRoomUserCard(sessionSerial: _userCardSessionSerial); } @override Widget build(BuildContext context) { final screenHeight = MediaQuery.of(context).size.height; final defaultSheetHeight = screenHeight * _profileSheetHeightFactor; return Consumer( builder: (context, ref, child) { final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id; final isSelf = widget.userId == currentUserId; final rtcProvider = Provider.of(context, listen: false); final currentMicIndex = isSelf && currentUserId != null ? rtcProvider.userOnMaiInIndex(currentUserId) : -1; final canLeaveMic = currentMicIndex > -1; if (ref.userCardInfo == null && ref.userCardLoadError != null) { return _buildErrorSheet(defaultSheetHeight); } if (ref.userCardInfo == null) { return _buildLoadingSheet(defaultSheetHeight); } final profile = ref.userCardInfo?.userProfile; final dataCard = _activeDataCard(ref.userCardInfo); final sheetHeight = _resolvedProfileSheetHeight( screenHeight, profile, relationLoading: ref.isUserCardRelationLoading, ); return _buildProfileSheet( context: context, ref: ref, rtcProvider: rtcProvider, sheetHeight: sheetHeight, dataCardBackgroundHeight: screenHeight, dataCard: dataCard, isSelf: isSelf, currentMicIndex: currentMicIndex, canLeaveMic: canLeaveMic, currentUserId: currentUserId, ); }, ); } Widget _buildErrorSheet(double sheetHeight) { final localizations = SCAppLocalizations.of(context); 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: SCDebounceWidget( onTap: _loadRoomUserCard, child: Container( padding: EdgeInsets.symmetric( horizontal: 18.w, vertical: 14.w, ), decoration: BoxDecoration( color: Colors.black26, borderRadius: BorderRadius.circular(10.w), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.refresh, color: Colors.white70, size: 28.w), SizedBox(height: 8.w), text( localizations?.loadingFailedClickToRetry ?? 'Loading failed, click to retry', textAlign: TextAlign.center, textColor: Colors.white70, fontWeight: FontWeight.w600, fontSize: 12.sp, ), ], ), ), ), ), ], ), ), ); } 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, { required bool relationLoading, }) { final defaultSheetHeight = screenHeight * _profileSheetHeightFactor; if (relationLoading) { return defaultSheetHeight; } 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); final isRelationLoading = ref.isUserCardRelationLoading; 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 (isRelationLoading) ...[ SizedBox(height: 12.w), _buildCpRelationSkeletonCard(), SizedBox(height: 11.w), _buildCloseFriendSkeletonStrip(), ] else 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: isRelationLoading || 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) { final now = DateTime.now().millisecondsSinceEpoch; for (final item in useProps ?? const []) { 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: 20.w, child: Center( child: SCUserBadgeStrip( userId: userId, fallbackBadges: _roomCardFallbackBadges(ref, profile), displayScope: SCUserBadgeDisplayScope.short, height: 20.w, badgeHeight: 20.w, longBadgeWidth: 44.w, spacing: 5.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 _buildCpRelationSkeletonCard() { return SizedBox( height: 114.w, child: DecoratedBox( decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(12.w), border: Border.all(color: Colors.white.withValues(alpha: 0.12)), ), child: Stack( children: [ Positioned( left: 39.w, top: 26.w, child: _buildSkeletonCircle(60.w), ), Positioned( right: 38.w, top: 26.w, child: _buildSkeletonCircle(60.w), ), Positioned( left: 142.w, right: 142.w, top: 28.w, child: _buildSkeletonBlock(height: 18.w, radius: 9.w), ), Positioned( left: 156.w, right: 156.w, top: 54.w, child: _buildSkeletonBlock(height: 10.w, radius: 5.w), ), Positioned( left: 118.w, right: 118.w, top: 80.w, child: _buildSkeletonBlock(height: 14.w, radius: 7.w), ), ], ), ), ); } 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 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]), ), ], ), ), ), ], ), ); } Widget _buildCloseFriendSkeletonStrip() { return SizedBox( height: 41.w, width: double.infinity, child: DecoratedBox( decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.10), borderRadius: BorderRadius.circular(12.w), border: Border.all(color: Colors.white.withValues(alpha: 0.12)), ), child: Stack( children: [ PositionedDirectional( top: 13.w, start: 16.w, width: 118.w, child: _buildSkeletonBlock(height: 14.w, radius: 7.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 < 4; index++) PositionedDirectional( end: (3 - index) * 17.w, child: _buildSkeletonCircle(25.w), ), ], ), ), ), ], ), ), ); } Widget _buildSkeletonBlock({required double height, required double radius}) { return Container( height: height, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.22), borderRadius: BorderRadius.circular(radius), ), ); } Widget _buildSkeletonCircle(double size) { return Container( width: size, height: size, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withValues(alpha: 0.22), ), ); } _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.0"])), ); } CPRes? _firstRelation( List? relations, String relationType, { SocialChatUserProfile? profile, bool requirePartner = false, }) { for (final relation in relations ?? const []) { 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 _closeFriendProfileData(SocialChatUserProfile? profile) { final brotherAvatars = []; final sisterAvatars = []; for (final relation in profile?.closeFriendList ?? const []) { 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) { if (type == "BROTHER" && brotherAvatars.length < 3) { brotherAvatars.add(avatar); } else if (type == "SISTERS" && sisterAvatars.length < 3) { sisterAvatars.add(avatar); } } } return [...brotherAvatars, ...sisterAvatars]; } 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_${_cpLevelAssetIndex(levelText)}.png"; } int _cpLevelAssetIndex(String levelText) { final level = _cpLevelIndex(levelText); return level <= 0 ? 1 : level; } 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) { return scNormalizeCpRelationType(relationType); } String _avatarFallback(String? avatarUrl) { final value = avatarUrl?.trim() ?? ""; if (value.isNotEmpty) { return value; } return AccountStorage().getCurrentUser()?.userProfile?.userAvatar ?? ""; } String _firstNonBlank(List 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( 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 _roomCardFallbackBadges( SocialChatUserProfileManager ref, SocialChatUserProfile? profile, ) { final wearBadges = profile?.wearBadge ?.where((item) => item.use != false && _hasBadgeUrl(item)) .toList() ?? const []; if (wearBadges.isNotEmpty) { return wearBadges; } return ref.userCardInfo?.useBadge ?.map(_roomUseBadgeToWearBadge) .where(_hasBadgeUrl) .toList() ?? const []; } 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( context, listen: false, ); provider.followUser(widget.userId ?? ""); }, child: Selector( 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}"; final rootContext = navigatorKey.currentState?.context; Navigator.of(context).pop(); WidgetsBinding.instance.addPostFrameCallback((_) { final targetContext = navigatorKey.currentState?.context ?? rootContext; if (targetContext == null) { return; } SCNavigatorUtils.push(targetContext, route); }); } Future _openChat(BuildContext context) async { final rtmProvider = Provider.of(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 _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: "$nickname", ), ), ); } 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 _openCpProgress( BuildContext context, SocialChatUserProfile? profile, ) async { final cp = _cpProfileData(profile); const cancelText = 'unbind'; 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 = _counterpartUserIdForCurrentUser( leftUserId: cp.leftUserId, rightUserId: cp.rightUserId, ); 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; } } String _counterpartUserIdForCurrentUser({ required String leftUserId, required String rightUserId, }) { final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ""; final leftId = leftUserId.trim(); final rightId = rightUserId.trim(); if (currentUserId.isEmpty) { return rightId; } if (currentUserId == leftId) { return rightId; } if (currentUserId == rightId) { return leftId; } return rightId; } Future _showCpDismissConfirm( BuildContext context, _RoomProfileCpData cp, { int? dismissCost, }) async { final l10n = SCAppLocalizations.of(context)!; final partnerUserId = _counterpartUserIdForCurrentUser( leftUserId: cp.leftUserId, rightUserId: cp.rightUserId, ); 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, ); if (context.mounted) { Provider.of( context, listen: false, )?.invalidateRoomMicRelationBetween( userId: cp.leftUserId, peerUserId: 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: scCpRelationLevelDisplayText(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 []) .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 scCpRelationLevelDisplayText(fallbackText); } final levelName = level.levelName?.trim(); if (level.level != null && levelName != null && levelName.isNotEmpty) { return scCpRelationLevelDisplayText("Lv.${level.level} $levelName"); } if (levelName != null && levelName.isNotEmpty) { return scCpRelationLevelDisplayText(levelName); } if (level.level != null) { return "Lv.${level.level}"; } return scCpRelationLevelDisplayText(fallbackText); } static String _levelShortText(int? level, {String? fallback}) { if (level != null) { return "Lv. $level"; } final fallbackText = fallback?.trim() ?? ""; if (fallbackText.isNotEmpty) { return scCpRelationLevelDisplayText(fallbackText); } return "Lv. 0"; } static int _levelIndex(String levelText) { return scCpRelationLevelIndex(levelText); } static int _nextLevelIndex(int level) { return (level + 1).clamp(0, 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 {}; 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 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; }