import 'dart:convert'; import 'dart:ui'; import 'package:carousel_slider/carousel_slider.dart'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.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/tools/sc_user_utils.dart'; import 'package:yumi/modules/index/main_route.dart'; import 'package:provider/provider.dart'; import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/app/routes/sc_routes.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/shared/tools/sc_cp_relation_notice_utils.dart'; import 'package:yumi/shared/tools/sc_cp_relation_level_utils.dart'; import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/shared/tools/sc_lk_event_bus.dart'; import 'package:yumi/shared/tools/sc_pick_utils.dart'; import 'package:yumi/shared/tools/sc_string_utils.dart'; import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/models/enum/sc_props_type.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_gift_repository_imp.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/business_logic/models/res/my_room_res.dart'; import 'package:yumi/shared/business_logic/models/res/room_res.dart' hide WearBadge, UseProps, PropsResources; import 'package:yumi/shared/business_logic/models/res/sc_cp_rights_config_res.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.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 '../../../app/constants/sc_screen.dart'; import '../../../shared/business_logic/models/res/sc_user_counter_res.dart'; import '../../../shared/business_logic/models/res/sc_user_identity_res.dart'; import '../../../ui_kit/widgets/id/sc_special_id_badge.dart'; import '../../chat/chat_route.dart'; class PersonDetailPage extends StatefulWidget { final String isMe; final String tageId; const PersonDetailPage({Key? key, required this.isMe, required this.tageId}) : super(key: key); @override _PersonDetailPageState createState() => _PersonDetailPageState(); } class _PersonDetailPageState extends State { static const Color _profileBg = Color(0xff072121); static const Color _cardBg = Color(0xff08251E); static const Color _profileBorder = Color(0xffB2FBCC); static const String _cpProfileAssetBase = "sc_images/room/cp_profile"; static const int _wallPreviewItemCount = 9; SCUserIdentityRes? userIdentity; bool isFollow = false; bool isLoading = true; bool isBlacklistLoading = true; bool isBlacklist = false; bool _isBackgroundUpdating = false; bool _isCreatedRoomLoading = false; String? _createdRoomLookupKey; _ProfileCreatedRoom? _createdRoom; Map counterMap = {}; List giftWallList = []; List honorWallBadges = []; // 添加滚动控制器 final ScrollController _scrollController = ScrollController(); double _opacity = 0.0; @override void initState() { super.initState(); // 监听滚动 _scrollController.addListener(() { if (_scrollController.hasClients) { final offset = _scrollController.offset; // 当滚动到一定位置时,头像和昵称应该完全显示在AppBar上 // 这里计算透明度,可以根据需要调整 final newOpacity = (offset / 320).clamp(0.0, 1.0); if (newOpacity != _opacity) { setState(() { _opacity = newOpacity; }); } } }); Provider.of(context, listen: false) .userProfile = null; Provider.of( context, listen: false, ).getUserInfoById(widget.tageId); SCAccountRepository().userIdentity(userId: widget.tageId).then((v) { userIdentity = v; setState(() {}); }); if (widget.isMe != "true") { SCAccountRepository() .followCheck(widget.tageId) .then((v) { isFollow = v; isLoading = false; setState(() {}); }) .catchError((e) { setState(() { isLoading = false; }); }); SCAccountRepository() .blacklistCheck(widget.tageId) .then((v) { isBlacklist = v; isBlacklistLoading = false; setState(() {}); if (isBlacklist) { SCTts.show( SCAppLocalizations.of(context)!.thisUserHasBeenBlacklisted, ); } }) .catchError((e) { setState(() { isBlacklistLoading = false; }); }); } else { isBlacklist = false; isBlacklistLoading = false; } userCounter(widget.tageId); _loadHonorWall(); _loadGiftWall(); } void userCounter(String userId) async { try { var userCounterList = await SCAccountRepository().userCounter(userId); if (!mounted) return; counterMap = Map.fromEntries( userCounterList.map( (counter) => MapEntry(counter.counterType ?? "", counter), ), ); setState(() {}); } catch (_) {} } void _applyCounterDelta(String type, int delta) { final currentCounter = counterMap[type]; final currentQuantity = int.tryParse(currentCounter?.quantity ?? "0") ?? 0; final nextQuantity = (currentQuantity + delta).clamp(0, 1 << 31); counterMap = { ...counterMap, type: (currentCounter ?? SCUserCounterRes(counterType: type)).copyWith( quantity: nextQuantity.toString(), ), }; } void _loadGiftWall() { SCGiftRepositoryImp() .giftWall(widget.tageId) .then((result) { if (!mounted) return; giftWallList = result.take(_wallPreviewItemCount).toList(); setState(() {}); }) .catchError((e) {}); } void _loadHonorWall() { final userId = widget.tageId.trim(); if (userId.isEmpty) { return; } final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ""; final future = currentUserId.isNotEmpty && currentUserId == userId ? SCAccountRepository().currentUserBadges() : SCAccountRepository().otherUserBadges(userId); future .then((result) { if (!mounted) return; honorWallBadges = result.honorWallBadges; setState(() {}); }) .catchError((e) {}); } void _ensureCreatedRoomLoaded(SocialChatUserProfile? profile) { final userId = (profile?.id ?? widget.tageId).trim(); final lookupKey = [ userId, profile?.account?.trim() ?? "", profile?.getID().trim() ?? "", profile?.inRoomId?.trim() ?? "", ].join("|"); if (userId.isEmpty || _createdRoomLookupKey == lookupKey) { return; } _createdRoomLookupKey = lookupKey; _createdRoom = null; _isCreatedRoomLoading = true; _loadCreatedRoom(profile: profile, userId: userId) .then((room) { if (!mounted || _createdRoomLookupKey != lookupKey) { return; } setState(() { _createdRoom = room; _isCreatedRoomLoading = false; }); }) .catchError((e) { if (!mounted || _createdRoomLookupKey != lookupKey) { return; } setState(() { _createdRoom = null; _isCreatedRoomLoading = false; }); }); } Future<_ProfileCreatedRoom?> _loadCreatedRoom({ required SocialChatUserProfile? profile, required String userId, }) async { final repository = SCChatRoomRepository(); final searchKeys = { profile?.account?.trim() ?? "", profile?.getID().trim() ?? "", userId, }..removeWhere((value) => value.isEmpty); for (final key in searchKeys) { try { final rooms = await repository.searchRoom(key); final ownedRoom = _ownedRoomFromSearchResults(rooms, profile, userId); if (ownedRoom != null) { return _ProfileCreatedRoom.fromSocialChatRoom( ownedRoom, ownerProfile: profile, ); } } catch (_) { continue; } } final currentRoomId = profile?.inRoomId?.trim() ?? ""; if (currentRoomId.isNotEmpty) { try { final room = await repository.specific(currentRoomId); if ((room.userId ?? "").trim() == userId) { return _ProfileCreatedRoom.fromMyRoom(room, ownerProfile: profile); } } catch (_) { return null; } } return null; } SocialChatRoomRes? _ownedRoomFromSearchResults( List rooms, SocialChatUserProfile? profile, String userId, ) { final account = profile?.account?.trim() ?? ""; final displayId = profile?.getID().trim() ?? ""; for (final room in rooms) { if ((room.id ?? "").trim().isEmpty) { continue; } final roomUserId = (room.userId ?? room.userProfile?.id ?? "").trim(); if (roomUserId == userId) { return room; } if (roomUserId.isEmpty) { final roomAccount = (room.roomAccount ?? "").trim(); if (roomAccount.isNotEmpty && (roomAccount == account || roomAccount == displayId)) { return room; } } } return null; } @override void dispose() { _scrollController.dispose(); super.dispose(); } Widget _buildHeaderAvatar(SocialChatUserProfileManager ref) { return Center( child: GestureDetector( child: head( url: ref.userProfile?.userAvatar ?? "", width: 88.w, headdress: ref.userProfile?.getHeaddress()?.sourceUrl, headdressCover: ref.userProfile?.getHeaddress()?.cover, ), onTap: () { String encodedUrls = Uri.encodeComponent( jsonEncode([ref.userProfile?.userAvatar]), ); SCNavigatorUtils.push( context, "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", ); }, ), ); } Widget _buildHeaderName(SocialChatUserProfileManager ref) { final nickname = ref.userProfile?.userNickname ?? ""; return Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ socialchatNickNameText( nickname, maxWidth: 240.w, fontSize: 18.sp, textColor: Colors.white, fontWeight: FontWeight.w600, type: ref.userProfile?.getVIP()?.name ?? "", needScroll: nickname.characters.length > 18, ), SizedBox(width: 3.w), getVIPBadge(ref.userProfile?.getVIP()?.name, width: 45.w, height: 25.w), ], ); } Widget _buildHeaderMeta(SocialChatUserProfileManager ref) { final idText = ref.userProfile?.getID() ?? ""; return Directionality( textDirection: TextDirection.ltr, child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { Clipboard.setData(ClipboardData(text: idText)); SCTts.show(SCAppLocalizations.of(context)!.copiedToClipboard); }, child: Row( mainAxisSize: MainAxisSize.min, children: [ SCSpecialIdBadge( idText: idText, showAnimated: ref.userProfile?.hasSpecialId() ?? false, assetPath: SCSpecialIdAssets.userIdLarge, animationWidth: 80.w, animationHeight: 32.w, showTextBesideAnimated: true, animatedTextSpacing: 0, showAnimatedGradientText: ref.userProfile?.shouldShowColoredSpecialIdText() ?? false, animationTextStyle: TextStyle( color: Colors.white, fontSize: 16.sp, fontWeight: FontWeight.w700, ), normalTextStyle: TextStyle( color: Colors.white.withValues(alpha: 0.72), fontSize: 16.sp, fontWeight: FontWeight.w400, ), animationFit: BoxFit.contain, ), SizedBox(width: 6.w), Image.asset( "sc_images/room/sc_icon_user_card_copy_id.png", width: 15.w, height: 15.w, ), ], ), ), SizedBox(width: 9.w), _buildHeaderGenderAge(ref), SizedBox(width: 8.w), _buildHeaderCountryFlag(ref), ], ), ); } Widget _buildHeaderGenderAge(SocialChatUserProfileManager ref) { return Container( width: (ref.userProfile?.age ?? 0) > 999 ? 58.w : 48.w, height: 24.w, decoration: BoxDecoration( image: DecorationImage( image: AssetImage( ref.userProfile?.userSex == 0 ? "sc_images/login/sc_icon_sex_woman_bg.png" : "sc_images/login/sc_icon_sex_man_bg.png", ), ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, children: [ xb(ref.userProfile?.userSex), text( "${ref.userProfile?.age ?? 0}", textColor: Colors.white, fontSize: 14.sp, fontWeight: FontWeight.w600, ), SizedBox(width: 3.w), ], ), ); } Widget _buildHeaderCountryFlag(SocialChatUserProfileManager ref) { final countryName = ref.userProfile?.countryName ?? ""; String flag = ""; try { flag = Provider.of( context, listen: false, ).findCountryByName(countryName)?.nationalFlag ?? ""; } catch (_) { flag = ""; } if (flag.isEmpty) return const SizedBox.shrink(); return ClipRRect( borderRadius: BorderRadius.circular(2.w), child: netImage( url: flag, width: 24.w, height: 16.w, fit: BoxFit.cover, noDefaultImg: true, ), ); } Widget _buildHeaderBadgeStrip(SocialChatUserProfileManager ref) { return SCUserBadgeStrip( userId: ref.userProfile?.id ?? widget.tageId, fallbackBadges: ref.userProfile?.wearBadge ?.where((item) => item.use ?? false) .toList() ?? const [], height: 26.w, badgeHeight: 24.w, longBadgeWidth: 58.w, spacing: 6.w, maxBadges: 8, reserveSpace: true, ); } Widget _buildHeaderLongBadgeStrip(SocialChatUserProfileManager ref) { return SizedBox( width: ScreenUtil().screenWidth - 40.w, child: SCUserBadgeStrip( userId: ref.userProfile?.id ?? widget.tageId, fallbackBadges: ref.userProfile?.wearBadge ?.where((item) => item.use ?? false) .toList() ?? const [], displayScope: SCUserBadgeDisplayScope.long, height: 45.w, badgeHeight: 20.w, longBadgeWidth: 62.w, spacing: 4.w, wrap: true, maxWrapRows: 2, scrollLastWrapRow: true, reserveSpace: false, ), ); } Widget _buildHeaderIntroduction(SocialChatUserProfileManager ref) { final intro = (ref.userProfile?.autograph ?? "").trim(); final hobby = (ref.userProfile?.hobby ?? "").trim(); final localizations = SCAppLocalizations.of(context)!; return Padding( padding: EdgeInsets.symmetric(horizontal: 10.w), child: Column( children: [ _buildHeaderInfoRow(title: localizations.introduction, value: intro), SizedBox(height: 4.w), _buildHeaderInfoRow(title: localizations.hobby, value: hobby), ], ), ); } Widget _buildHeaderInfoRow({required String title, required String value}) { final displayValue = value.isEmpty ? "--" : value; final isRtl = Directionality.of(context) == TextDirection.rtl; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { _showProfileTextDetailDialog(title: title, value: value); }, child: SizedBox( height: 25.w, child: Row( children: [ Expanded( child: Text.rich( TextSpan( children: [ TextSpan(text: "$title: "), TextSpan(text: displayValue), ], ), maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.start, style: TextStyle( color: Colors.white, fontSize: 15.sp, fontWeight: FontWeight.w400, height: 1.2, ), ), ), SizedBox(width: 4.w), Icon( isRtl ? Icons.keyboard_arrow_left : Icons.keyboard_arrow_right, size: 18.w, color: Colors.white.withValues(alpha: 0.72), ), ], ), ), ); } void _showProfileTextDetailDialog({ required String title, required String value, }) { SmartDialog.dismiss(tag: "showProfileTextDetailDialog"); final displayValue = value.trim().isEmpty ? "--" : value.trim(); SmartDialog.show( tag: "showProfileTextDetailDialog", alignment: Alignment.center, debounce: true, animationType: SmartAnimationType.fade, maskColor: Colors.black.withValues(alpha: 0.45), builder: (_) { return ConstrainedBox( constraints: BoxConstraints(maxHeight: 260.w), child: Container( width: 275.w, constraints: BoxConstraints(minHeight: 112.w), padding: EdgeInsets.fromLTRB(12.w, 8.w, 12.w, 10.w), decoration: BoxDecoration( color: const Color(0xff0A342D), border: Border.all(color: const Color(0xff18F2B1), width: 1.w), borderRadius: BorderRadius.circular(10.w), ), child: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Text.rich( TextSpan( children: [ TextSpan(text: "$title: "), TextSpan(text: displayValue), ], ), textAlign: TextAlign.justify, style: TextStyle( color: Colors.white, fontSize: 12.sp, fontWeight: FontWeight.w400, height: 16 / 12, ), ), ), ), ); }, ); } Widget _buildHeaderStats() { return _buildGradientBorder( width: 355.w, height: 56.w, radius: 8.w, backgroundColor: _cardBg, endAlpha: 0.42, child: Row( children: [ _buildHeaderStatItem( value: "${counterMap["INTERVIEW"]?.quantity ?? 0}", label: SCAppLocalizations.of(context)!.vistors, onTap: () { if (widget.isMe == "true") { SCNavigatorUtils.push(context, SCMainRoute.vistors); } }, ), _buildHeaderStatsDivider(), _buildHeaderStatItem( value: "${counterMap["SUBSCRIPTION"]?.quantity ?? 0}", label: SCAppLocalizations.of(context)!.follow, onTap: () { if (widget.isMe == "true") { SCNavigatorUtils.push(context, SCMainRoute.follow); } }, ), _buildHeaderStatsDivider(), _buildHeaderStatItem( value: "${counterMap["FANS"]?.quantity ?? 0}", label: SCAppLocalizations.of(context)!.fans, onTap: () { if (widget.isMe == "true") { SCNavigatorUtils.push(context, SCMainRoute.fans); } }, ), ], ), ); } Widget _buildHeaderStatItem({ required String value, required String label, required VoidCallback onTap, }) { return Expanded( child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Column( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ text( value, fontSize: 17.sp, textColor: Colors.white, fontWeight: FontWeight.bold, lineHeight: 1, ), SizedBox(height: 4.w), text( label, fontSize: 14.sp, textColor: const Color(0xffB1B1B1), lineHeight: 1, ), ], ), ), ); } Widget _buildHeaderStatsDivider() { return Container( height: 28.w, width: 1.w, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color.fromRGBO(255, 255, 255, 0), Colors.white, Color.fromRGBO(255, 255, 255, 0), ], stops: [0, 0.4862, 1], ), ), ); } Widget _buildGradientBorder({ required double width, required double height, required double radius, required Widget child, Color backgroundColor = _cardBg, double endAlpha = 0, }) { final borderRadius = BorderRadius.circular(radius); return Container( width: width, height: height, decoration: BoxDecoration( borderRadius: borderRadius, gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [_profileBorder, _profileBorder.withValues(alpha: endAlpha)], ), ), padding: EdgeInsets.all(1.w), child: Container( decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular((radius - 1.w).clamp(0, radius)), ), child: child, ), ); } Widget _buildProfileDataCardBackground(PropsResources? _) { return const ColoredBox(color: _profileBg); } PropsResources? _activeDataCard(SocialChatUserProfile? profile) { final now = DateTime.now().millisecondsSinceEpoch; for (final item in profile?.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 _buildProfileHeader( SocialChatUserProfileManager ref, List backgroundPhotos, PropsResources? dataCard, ) { return SizedBox( height: 590.w, child: Stack( children: [ SizedBox( height: 300.w, width: ScreenUtil().screenWidth, child: backgroundPhotos.isNotEmpty ? CarouselSlider( options: CarouselOptions( height: 300.w, autoPlay: backgroundPhotos.length > 1, enlargeCenterPage: false, aspectRatio: 1 / 1, enableInfiniteScroll: true, autoPlayAnimationDuration: Duration(milliseconds: 800), viewportFraction: 1, onPageChanged: (index, reason) { setState(() {}); }, ), items: backgroundPhotos.map((item) { return _buildProfileBackgroundTapTarget( ref, netImage( url: item.url ?? "", width: ScreenUtil().screenWidth, height: 300.w, fit: BoxFit.cover, ), ); }).toList(), ) : _buildProfileBackgroundTapTarget( ref, Image.asset( 'sc_images/person/sc_icon_profile_card_default_bg.png', width: ScreenUtil().screenWidth, height: 300.w, fit: BoxFit.cover, ), ), ), Positioned( top: 250.w, left: 0, right: 0, bottom: 0, child: _buildProfileDataCardBackground(dataCard), ), Positioned( top: 210.w, left: 0, right: 0, child: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ _buildHeaderAvatar(ref), SizedBox(height: 6.w), _buildHeaderName(ref), SizedBox(height: 5.w), _buildHeaderMeta(ref), SizedBox(height: 4.w), _buildHeaderLongBadgeStrip(ref), SizedBox(height: 4.w), _buildHeaderBadgeStrip(ref), SizedBox(height: 6.w), _buildHeaderIntroduction(ref), SizedBox(height: 12.w), _buildHeaderStats(), SizedBox(height: 10.w), ], ), ), ], ), ); } Widget _buildProfileColumnContent(SocialChatUserProfile? profile) { final cpRelation = _firstDisplayProfileRelation( profile?.cpList, "CP", profile, ); final closeFriendRelations = _profileCloseFriendRelations(profile); final hasCloseFriendContent = cpRelation != null || closeFriendRelations.isNotEmpty; return Container( width: double.infinity, color: _profileBg, padding: EdgeInsets.fromLTRB(10.w, 4.w, 10.w, 220.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildProfileGuildPlaceholder(), if (hasCloseFriendContent) ...[ SizedBox(height: 12.w), _buildProfileCloseFriendTitle(), SizedBox(height: 12.w), if (cpRelation != null) ...[ _buildProfileCpRelationCard(profile, cpRelation), SizedBox(height: 8.w), ], if (closeFriendRelations.isNotEmpty) ...[ _buildProfileCloseFriendGrid(closeFriendRelations, profile), SizedBox(height: 18.w), ], ] else SizedBox(height: 18.w), _buildProfileSectionTitle( "Wall Of Honors", onTap: () { SCNavigatorUtils.push( context, "${SCMainRoute.wallOfHonors}?tageId=${widget.tageId}", ); }, ), SizedBox(height: 12.w), _buildWallGrid( itemCount: honorWallBadges.isEmpty ? _wallPreviewItemCount : honorWallBadges.length .clamp(0, _wallPreviewItemCount) .toInt(), itemBuilder: (index) { final badge = index < honorWallBadges.length ? honorWallBadges[index] : null; return _buildHonorWallItem(badge); }, ), SizedBox(height: 18.w), _buildProfileSectionTitle( "Gift Wall", onTap: () { SCNavigatorUtils.push( context, "${SCMainRoute.giftWall}?tageId=${widget.tageId}", ); }, ), SizedBox(height: 12.w), _buildWallGrid( itemCount: giftWallList.isEmpty ? _wallPreviewItemCount : giftWallList.length, itemBuilder: (index) { final gift = index < giftWallList.length ? giftWallList[index] : null; return _buildGiftWallItem(gift); }, ), ], ), ); } Widget _buildProfileGuildPlaceholder() { return ClipRRect( borderRadius: BorderRadius.circular(8.w), child: Image.asset( "$_cpProfileAssetBase/sc_cp_profile_guild_placeholder.png", width: double.infinity, height: 64.w, fit: BoxFit.fill, ), ); } Widget _buildProfileCloseFriendTitle() { return Image.asset( "$_cpProfileAssetBase/sc_cp_profile_close_friend_title.png", width: 85.w, height: 16.w, fit: BoxFit.contain, ); } Widget _buildProfileCpRelationCard( SocialChatUserProfile? profile, CPRes relation, ) { final owner = _profileRelationOwner(profile, relation); final partner = _profileRelationPartner(profile, relation); final card = SizedBox( height: 114.w, width: double.infinity, child: Stack( clipBehavior: Clip.none, children: [ Positioned.fill( child: Image.asset( _profileCpLevelAsset(relation), fit: BoxFit.fill, ), ), Positioned(right: 10.w, top: 8.w, child: const SCCpCornerLabel()), Positioned( left: 39.w, top: 26.w, child: _buildProfileRelationAvatar( _firstNonBlankProfileValue([ owner.avatarUrl, profile?.userAvatar, ]), 60.w, ), ), Positioned( right: 38.w, top: 26.w, child: _buildProfileRelationAvatar(partner.avatarUrl ?? "", 60.w), ), Positioned( left: 142.w, right: 142.w, top: 25.w, height: 22.w, child: _buildProfileCpCardText( _profileRelationDays(relation), color: const Color(0xffFF0979), fontSize: 20, fontWeight: FontWeight.w700, ), ), Positioned( left: 142.w, right: 142.w, top: 48.w, height: 14.w, child: _buildProfileCpCardText( "Days", color: const Color(0xffFF0979), fontSize: 12, fontWeight: FontWeight.w400, ), ), Positioned( left: 110.w, right: 110.w, top: 78.w, height: 16.w, child: _buildProfileCpCardText( _profileRelationLevelText(relation), color: const Color(0xffFEFEFF), fontSize: 11, fontWeight: FontWeight.w400, ), ), ], ), ); return _buildProfileRelationProgressTap( profile: profile, relation: relation, child: card, ); } Widget _buildProfileCpCardText( 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 _buildProfileCloseFriendGrid( List relations, SocialChatUserProfile? profile, ) { final spacing = ((ScreenUtil().screenWidth - 20.w - 113.w * 3) / 2) .clamp(6.w, 12.w) .toDouble(); return Wrap( spacing: spacing, runSpacing: 8.w, children: relations .map( (relation) => _buildProfileCloseFriendCard(relation, profile), ) .toList(), ); } Widget _buildProfileCloseFriendCard( CPRes relation, SocialChatUserProfile? profile, ) { final type = _normalizeProfileRelationType(relation.relationType); final friend = _profileRelationPartner(profile, relation); final isSister = type == "SISTERS"; final card = SizedBox( width: 113.w, height: 100.w, child: Stack( alignment: Alignment.topCenter, children: [ Positioned.fill( child: Image.asset( isSister ? "$_cpProfileAssetBase/sc_cp_profile_sister_card_bg.png" : "$_cpProfileAssetBase/sc_cp_profile_brother_card_bg.png", fit: BoxFit.fill, ), ), Positioned( top: 2.w, left: 0, right: 0, child: Text( isSister ? "Sisters" : "Brother", textAlign: TextAlign.center, textScaler: TextScaler.noScaling, style: TextStyle( color: isSister ? const Color(0xff7F49C8) : const Color(0xff3474B8), fontSize: 10.sp, height: 1, fontWeight: FontWeight.w500, decoration: TextDecoration.none, ), ), ), Positioned( top: 20.w, child: _buildProfileRelationAvatar(friend.avatarUrl ?? "", 51.w), ), Positioned( top: 66.w, child: Container( width: 27.w, height: 12.w, alignment: Alignment.center, decoration: BoxDecoration( color: Colors.white.withValues(alpha: 0.72), borderRadius: BorderRadius.circular(6.w), ), child: Text( _profileRelationLevelShortText(relation), textAlign: TextAlign.center, textScaler: TextScaler.noScaling, style: TextStyle( color: isSister ? const Color(0xff7F49C8) : const Color(0xff3474B8), fontSize: 8.sp, height: 1, fontWeight: FontWeight.w600, decoration: TextDecoration.none, ), ), ), ), Positioned( bottom: 9.w, left: 0, right: 0, child: Text( "${_profileRelationDays(relation)} Days", maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: TextAlign.center, textScaler: TextScaler.noScaling, style: TextStyle( color: const Color(0xff4C5C66), fontSize: 12.sp, height: 1, fontWeight: FontWeight.w500, decoration: TextDecoration.none, ), ), ), ], ), ); return _buildProfileRelationProgressTap( profile: profile, relation: relation, child: card, ); } Widget _buildProfileRelationProgressTap({ required SocialChatUserProfile? profile, required CPRes relation, required Widget child, }) { if (!_isOwnProfile(profile) || !_profileRelationHasPartner(profile, relation)) { return child; } return SCDebounceWidget( onTap: () => _openProfileRelationProgress(profile, relation), child: child, ); } Widget _buildProfileRelationAvatar(String avatarUrl, double size) { final trimmedUrl = avatarUrl.trim(); return Container( width: size, height: size, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.white.withValues(alpha: 0.85)), ), clipBehavior: Clip.antiAlias, child: trimmedUrl.isEmpty ? const SizedBox.shrink() : netImage( url: trimmedUrl, width: size, height: size, fit: BoxFit.cover, noDefaultImg: true, loadingWidget: const SizedBox.shrink(), errorWidget: const SizedBox.shrink(), ), ); } Widget _buildProfileSectionTitle(String title, {VoidCallback? onTap}) { final titleAsset = title == "Wall Of Honors" ? "sc_images/person/sc_icon_profile_wall_of_honors_title.png" : "sc_images/person/sc_icon_profile_gift_wall_title.png"; final titleHeight = 32.h; final titleWidth = title == "Wall Of Honors" ? 98.w : 61.w; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: onTap, child: Row( children: [ Expanded( child: Align( alignment: AlignmentDirectional.centerStart, child: Image.asset( titleAsset, height: titleHeight, width: titleWidth, fit: BoxFit.contain, errorBuilder: (_, __, ___) => Text( title, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: _profileBorder, fontSize: 19.sp, fontStyle: FontStyle.italic, fontWeight: FontWeight.w800, ), ), ), ), ), Icon(Icons.chevron_right_rounded, color: Colors.white, size: 28.w), ], ), ); } Widget _buildWallGrid({ required int itemCount, required Widget Function(int index) itemBuilder, }) { final spacing = ((ScreenUtil().screenWidth - 20.w - 76.w * 4) / 3) .clamp(8.w, 26.w) .toDouble(); return Wrap( spacing: spacing, runSpacing: 17.w, children: List.generate(itemCount, itemBuilder), ); } Widget _buildHonorWallItem(WearBadge? badge) { return _buildWallItemFrame( child: badge == null ? Opacity( opacity: 0.45, child: Image.asset( "sc_images/room/sc_icon_room_music_empty.png", width: 38.w, height: 38.w, fit: BoxFit.contain, ), ) : Padding( padding: EdgeInsets.all(8.w), child: netImage( url: _badgeUrl(badge), width: double.infinity, height: double.infinity, fit: BoxFit.contain, noDefaultImg: true, ), ), ); } String _badgeUrl(WearBadge badge) => (badge.selectUrl ?? "").trim(); Widget _buildGiftWallItem(SocialChatGiftRes? gift) { return _buildWallItemFrame( child: Stack( children: [ Positioned.fill( child: Padding( padding: EdgeInsets.all(8.w), child: gift == null ? Opacity( opacity: 0.28, child: Image.asset( "sc_images/room/sc_icon_room_music_empty.png", fit: BoxFit.contain, ), ) : netImage( url: gift.giftPhoto ?? "", width: double.infinity, height: double.infinity, fit: BoxFit.contain, noDefaultImg: true, ), ), ), if (gift != null) Positioned( right: 5.w, bottom: 4.w, child: Container( padding: EdgeInsets.symmetric(horizontal: 5.w, vertical: 1.w), decoration: BoxDecoration( color: Colors.black.withValues(alpha: 0.35), borderRadius: BorderRadius.circular(8.w), ), child: text( "x${gift.quantity ?? 0}", fontSize: 10.sp, textColor: Colors.white, ), ), ), ], ), ); } Widget _buildWallItemFrame({required Widget child}) { return _buildGradientBorder( width: 76.w, height: 76.w, radius: 8.w, backgroundColor: _cardBg, endAlpha: 0.62, child: child, ); } CPRes? _firstDisplayProfileRelation( List? relations, String relationType, SocialChatUserProfile? profile, ) { for (final relation in relations ?? const []) { if (_normalizeProfileRelationType(relation.relationType) != relationType) { continue; } if (_profileRelationHasPartner(profile, relation)) { return relation; } } return null; } List _profileCloseFriendRelations(SocialChatUserProfile? profile) { return (profile?.closeFriendList ?? const []) .where((relation) { final type = _normalizeProfileRelationType(relation.relationType); return (type == "BROTHER" || type == "SISTERS") && _profileRelationHasPartner(profile, relation); }) .take(6) .toList(); } bool _profileRelationHasPartner( SocialChatUserProfile? profile, CPRes relation, ) { final partner = _profileRelationPartner(profile, relation); return _firstNonBlankProfileValue([ partner.userId, partner.account, partner.nickname, partner.avatarUrl, ]).isNotEmpty; } _ProfileRelationUser _profileRelationOwner( SocialChatUserProfile? profile, CPRes relation, ) { final profileId = profile?.id?.trim() ?? ""; if (profileId.isNotEmpty && profileId == (relation.cpUserId ?? "")) { return _ProfileRelationUser( userId: relation.cpUserId, account: relation.cpAccount, nickname: relation.cpUserNickname, avatarUrl: relation.cpUserAvatar, ); } return _ProfileRelationUser( userId: relation.meUserId ?? profile?.id, account: relation.meAccount ?? profile?.account, nickname: relation.meUserNickname ?? profile?.userNickname, avatarUrl: relation.meUserAvatar ?? profile?.userAvatar, ); } _ProfileRelationUser _profileRelationPartner( SocialChatUserProfile? profile, CPRes relation, ) { final profileId = profile?.id?.trim() ?? ""; if (profileId.isNotEmpty && profileId == (relation.cpUserId ?? "")) { return _ProfileRelationUser( userId: relation.meUserId, account: relation.meAccount, nickname: relation.meUserNickname, avatarUrl: relation.meUserAvatar, ); } return _ProfileRelationUser( userId: relation.cpUserId, account: relation.cpAccount, nickname: relation.cpUserNickname, avatarUrl: relation.cpUserAvatar, ); } String _normalizeProfileRelationType(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"; } } bool _isOwnProfile(SocialChatUserProfile? profile) { if (widget.isMe == "true") { return true; } final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? ""; final profileId = (profile?.id ?? widget.tageId).trim(); return currentUserId.isNotEmpty && currentUserId == profileId; } _ProfileRelationProgressPayload _profileRelationProgressPayload( SocialChatUserProfile? profile, CPRes relation, ) { final currentProfile = AccountStorage().getCurrentUser()?.userProfile; final owner = _profileRelationOwner(profile, relation); final partner = _profileRelationPartner(profile, relation); return _ProfileRelationProgressPayload( leftAvatarUrl: _firstNonBlankProfileValue([ owner.avatarUrl, profile?.userAvatar, currentProfile?.userAvatar, ]), rightAvatarUrl: partner.avatarUrl?.trim() ?? "", leftName: _firstNonBlankProfileValue([ owner.nickname, profile?.userNickname, currentProfile?.userNickname, ]), rightName: _firstNonBlankProfileValue([ partner.nickname, partner.account, ]), leftUserId: _firstNonBlankProfileValue([ owner.userId, profile?.id, currentProfile?.id, ]), rightUserId: _firstNonBlankProfileValue([partner.userId]), days: _profileRelationDays(relation), levelText: _profileRelationLevelText(relation), relationType: _normalizeProfileRelationType(relation.relationType), cpValue: relation.cpValue ?? 0, dismissCost: relation.dismissCost, ); } Future _openProfileRelationProgress( SocialChatUserProfile? profile, CPRes relation, ) async { final payload = _profileRelationProgressPayload(profile, relation); if (payload.rightUserId.trim().isEmpty) { return; } final cancelText = SCAppLocalizations.of(context)!.cancel; SCLoadingManager.show(context: context); late final _ProfileRelationProgressData progress; try { progress = await _loadProfileRelationProgressData(payload); } finally { SCLoadingManager.hide(); } if (!mounted) { return; } RoomCpProgressDialog.show( context, leftUser: RoomCpProgressDialogUser( nickname: payload.leftName, userId: payload.leftUserId, avatarUrl: payload.leftAvatarUrl, ), rightUser: RoomCpProgressDialogUser( nickname: payload.rightName, userId: payload.rightUserId, avatarUrl: payload.rightAvatarUrl, ), daysValue: payload.days, levelText: progress.levelText, currentValue: progress.currentValue, targetValue: progress.targetValue, expAwayText: progress.expAwayText, nextLevelText: progress.nextLevelText, cancelText: cancelText, onCancel: () => _showProfileRelationDismissConfirm( payload, dismissCost: progress.dismissCost, ), ); } Future<_ProfileRelationProgressData> _loadProfileRelationProgressData( _ProfileRelationProgressPayload payload, ) async { final partnerUserId = payload.rightUserId.trim(); final fallback = _ProfileRelationProgressData.fallback(payload); if (partnerUserId.isEmpty) { return fallback; } var latestFallback = fallback; try { final config = await SCAccountRepository().cpRightsConfig( relationType: payload.relationType, cpUserId: partnerUserId, ); final progress = _ProfileRelationProgressData.fromRightsConfig( config, payload, ); latestFallback = progress; if (config.levels.isNotEmpty) { return progress; } } catch (_) { // Fall back to the cabin endpoint below. } try { final cabin = await SCAccountRepository().cpCabin( payload.leftUserId, relationType: payload.relationType, ); return _ProfileRelationProgressData.fromCabin( cabin, payload, fallbackDismissCost: latestFallback.dismissCost, ); } catch (_) { return latestFallback; } } Future _showProfileRelationDismissConfirm( _ProfileRelationProgressPayload payload, { int? dismissCost, }) async { final partnerUserId = payload.rightUserId.trim(); if (partnerUserId.isEmpty) { return; } final l10n = SCAppLocalizations.of(context)!; await showRoomCpDismissConfirmDialog( context, message: l10n.cpDismissConfirmMessage( dismissCost == null ? "--" : "$dismissCost", ), cancelText: l10n.cancel, confirmText: l10n.confirm, onConfirm: () async { SCLoadingManager.show(context: context); try { await SCAccountRepository().cpRelationshipDismissApply( partnerUserId, relationType: payload.relationType, ); if (!mounted) { return; } await Provider.of( context, listen: false, ).refreshLoadedCpProfiles(); await SmartDialog.dismiss(tag: RoomCpProgressDialog.dialogTag); SCTts.show(l10n.operationSuccessful); } catch (_) { // Repository/network layer surfaces backend errors. } finally { SCLoadingManager.hide(); } }, ); } String _profileCpLevelAsset(CPRes relation) { return "$_cpProfileAssetBase/sc_cp_profile_cp_level_${_profileRelationLevel(relation)}.png"; } int _profileRelationLevel(CPRes relation) { return scCpRelationLevelIndex(relation.levelText); } String _profileRelationLevelText(CPRes relation) { return _profileRelationLevelDisplayText( _firstNonBlankProfileValue([relation.levelText, "Lv.1 Simple Love"]), relation, ); } String _profileRelationLevelShortText(CPRes relation) { return "Lv.${_profileRelationLevel(relation)}"; } String _profileRelationDays(CPRes relation) { return scCpRelationDisplayDays( _firstNonBlankProfileValue([relation.days, "0"]), ); } String _profileRelationLevelDisplayText(String rawText, CPRes relation) { return scCpRelationLevelDisplayText(rawText); } String _firstNonBlankProfileValue(List values) { for (final value in values) { final trimmed = value?.trim() ?? ""; if (trimmed.isNotEmpty) { return trimmed; } } return ""; } Widget _buildBottomActions(SocialChatUserProfileManager ref) { _ensureCreatedRoomLoaded(ref.userProfile); final createdRoom = _createdRoom; final hasRoom = createdRoom != null && createdRoom.roomId.trim().isNotEmpty && !_isCreatedRoomLoading; final safeBottom = MediaQuery.of(context).padding.bottom; final bottomPadding = safeBottom + 32.w; final actionRowBottom = bottomPadding; final enterBottom = actionRowBottom + 36.w + 43.w; final backgroundHeight = 93.w + safeBottom + 24.w; final height = hasRoom ? enterBottom + 38.w : backgroundHeight; return SizedBox( height: height, child: Stack( clipBehavior: Clip.none, children: [ Positioned( left: 0, right: 0, bottom: 0, child: SizedBox(height: backgroundHeight), ), if (hasRoom) Positioned( left: 0, right: 0, bottom: enterBottom, child: _buildEnterRoomPill(ref, createdRoom), ), Positioned( left: 0, right: 0, bottom: actionRowBottom, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ SCDebounceWidget( debounceTime: const Duration(milliseconds: 800), onTap: () { final nextIsFollow = !isFollow; SCAccountRepository().followUser(widget.tageId).then((v) { if (!mounted) return; if (v != true) return; _applyCounterDelta("FANS", nextIsFollow ? 1 : -1); isFollow = nextIsFollow; setState(() {}); eventBus.fire( SCUserRelationChangedEvent( actorUserId: AccountStorage() .getCurrentUser() ?.userProfile ?.id ?? "", targetUserId: widget.tageId, isFollowing: nextIsFollow, ), ); userCounter(widget.tageId); }); }, child: _buildBottomButton( width: 154.w, icon: isFollow ? "sc_images/person/sc_icon_profile_following.png" : "sc_images/person/sc_icon_person_follow.png", label: isFollow ? SCAppLocalizations.of(context)!.following : SCAppLocalizations.of(context)!.follow, ), ), SizedBox(width: 20.w), SCDebounceWidget( debounceTime: const Duration(milliseconds: 800), onTap: _openChat, child: _buildBottomButton( width: 165.w, icon: "sc_images/person/sc_icon_person_tochat.png", label: SCAppLocalizations.of(context)!.chat, ), ), ], ), ), ], ), ); } Widget _buildBottomButton({ required double width, required String icon, required String label, }) { return Container( width: width, height: 36.w, decoration: BoxDecoration( color: const Color(0xff0A342D), borderRadius: BorderRadius.circular(22.w), border: Border.all(color: const Color(0xff18F2B1), width: 1.w), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset(icon, width: 28.w, height: 28.w, fit: BoxFit.contain), SizedBox(width: 6.w), Flexible( child: text( label, fontSize: 16.sp, textColor: Colors.white, fontWeight: FontWeight.w400, maxLines: 1, ), ), ], ), ); } Widget _buildEnterRoomPill( SocialChatUserProfileManager ref, _ProfileCreatedRoom room, ) { final displayName = room.displayName(ref.userProfile); return Center( child: SCDebounceWidget( debounceTime: const Duration(milliseconds: 800), onTap: () => _enterCreatedRoom(room), child: Container( width: 343.w, height: 38.w, decoration: BoxDecoration( color: const Color(0xff0A342D), borderRadius: BorderRadius.circular(22.w), border: Border.all(color: const Color(0xff18F2B1), width: 1.w), ), child: Row( children: [ SizedBox(width: 5.w), netImage( url: resolveRoomCoverUrl(room.roomId, room.roomCover), defaultImg: kRoomCoverDefaultImg, width: 30.w, height: 30.w, borderRadius: BorderRadius.circular(15.w), ), SizedBox(width: 8.w), Expanded( child: socialchatNickNameText( displayName, maxWidth: 190.w, fontSize: 16.sp, textColor: Colors.white, fontWeight: FontWeight.w400, type: ref.userProfile?.getVIP()?.name ?? "", needScroll: displayName.characters.length > 16, ), ), Container( width: 69.w, height: 27.w, alignment: Alignment.center, decoration: BoxDecoration( color: const Color(0xff18F2B1), borderRadius: BorderRadius.circular(22.w), ), child: text( SCAppLocalizations.of(context)!.enter, fontSize: 16.sp, textColor: Colors.black, fontWeight: FontWeight.w500, ), ), SizedBox(width: 8.w), ], ), ), ), ); } void _enterCreatedRoom(_ProfileCreatedRoom room) { final roomId = room.roomId.trim(); if (roomId.isEmpty) { return; } Provider.of( context, listen: false, ).joinVoiceRoomSession(context, roomId, previewData: room.previewData); } Widget _buildProfileBackgroundTapTarget( SocialChatUserProfileManager ref, Widget child, ) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: widget.isMe == "true" ? () => _pickAndUpdateProfileBackground(ref) : null, child: child, ); } void _pickAndUpdateProfileBackground(SocialChatUserProfileManager ref) { if (_isBackgroundUpdating) { return; } SCPickUtils.pickImage( context, (bool success, String url) { final imageUrl = url.trim(); if (!success || imageUrl.isEmpty || !mounted) { return; } _updateProfileBackground(ref, imageUrl); }, aspectRatio: ScreenUtil().screenWidth / 300.w, backOriginalFile: false, ); } Future _updateProfileBackground( SocialChatUserProfileManager ref, String imageUrl, ) async { if (_isBackgroundUpdating) { return; } _isBackgroundUpdating = true; SCLoadingManager.show(context: context); try { final updatedProfile = await SCAccountRepository().updateUserInfo( backgroundPhotos: [imageUrl], ); final returnedPhotos = updatedProfile.backgroundPhotos ?? []; final hasUploadedPhoto = returnedPhotos.any( (photo) => (photo.url ?? "").trim() == imageUrl, ); final backgroundPhotos = hasUploadedPhoto ? returnedPhotos : [PersonPhoto(url: imageUrl, status: 1)]; final mergedProfile = updatedProfile.copyWith( backgroundPhotos: backgroundPhotos, ); ref.userProfile = mergedProfile; ref.syncCurrentUserProfile(mergedProfile); if (!mounted) { return; } setState(() {}); SCTts.show(SCAppLocalizations.of(context)!.operationSuccessful); } catch (e) { if (mounted) { SCTts.show("update fail $e"); } } finally { _isBackgroundUpdating = false; SCLoadingManager.hide(); } } Future _openChat() async { final conversation = V2TimConversation( type: ConversationType.V2TIM_C2C, userID: widget.tageId, conversationID: '', ); final started = await Provider.of( context, listen: false, ).startConversation(conversation); if (!started || !mounted) return; final json = jsonEncode(conversation.toJson()); SCNavigatorUtils.push( context, "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", ); } Future _openEditProfile() async { await SCNavigatorUtils.push(context, SCMainRoute.edit, replace: false); if (!mounted) { return; } final ref = Provider.of( context, listen: false, ); final currentProfile = AccountStorage().getCurrentUser()?.userProfile; if (currentProfile?.id == widget.tageId) { ref.syncCurrentUserProfile(currentProfile); } ref.getUserInfoById(widget.tageId); } @override Widget build(BuildContext context) { return Consumer( builder: (context, ref, child) { final bool isProfileLoading = ref.userProfile == null; List backgroundPhotos = []; backgroundPhotos = (isProfileLoading ? [] : (ref.userProfile?.backgroundPhotos ?? [])) .where((t) => t.status == 1) .toList(); final dataCard = isProfileLoading ? null : _activeDataCard(ref.userProfile); return Directionality( textDirection: window.locale.languageCode == "ar" ? TextDirection.rtl : TextDirection.ltr, child: SafeArea( top: false, child: Stack( children: [ const Positioned.fill(child: ColoredBox(color: _profileBg)), Scaffold( extendBodyBehindAppBar: true, resizeToAvoidBottomInset: false, backgroundColor: Colors.transparent, appBar: SocialChatStandardAppBar( backButtonColor: Colors.white, title: "", leading: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { Navigator.pop(context); }, child: Container( width: width(50), height: height(30), alignment: AlignmentDirectional.centerStart, padding: EdgeInsetsDirectional.only(start: width(15)), child: Icon( window.locale.languageCode == "ar" ? Icons.keyboard_arrow_right : Icons.keyboard_arrow_left, size: 28.w, color: Colors.white, ), ), ), actions: [ // 在AppBar中显示头像和昵称 if (_opacity > 0.5 && !isProfileLoading) ...[ SizedBox(width: 45.w), head( url: ref.userProfile?.userAvatar ?? "", width: 50.w, height: 50.w, ), SizedBox(width: 8.w), socialchatNickNameText( maxWidth: 135.w, textColor: Colors.white, ref.userProfile?.userNickname ?? "", fontSize: 16.sp, fontWeight: FontWeight.w600, type: ref.userProfile?.getVIP()?.name ?? "", needScroll: (ref .userProfile ?.userNickname ?.characters .length ?? 0) > 13, ), SizedBox(width: 8.w), ], Spacer(), Builder( builder: (ct) { return IconButton( icon: widget.isMe == "true" ? Image.asset( "sc_images/person/sc_icon_edit_user_info2.png", width: 22.w, color: Colors.white, ) : Icon( Icons.more_horiz, size: 22.w, color: Colors.white, ), onPressed: () async { if (widget.isMe == "true") { await _openEditProfile(); return; } SmartDialog.showAttach( tag: "showUserInfoOptMenu", targetContext: ct, alignment: Alignment.bottomCenter, maskColor: Colors.transparent, animationType: SmartAnimationType.fade, scalePointBuilder: (selfSize) => Offset(selfSize.width, 10), builder: (_) { return GestureDetector( child: Transform.translate( offset: Offset(0, -10), child: Container( width: 163.w, margin: EdgeInsetsDirectional.only( end: 8.w, ), decoration: BoxDecoration( color: Colors.white30, borderRadius: BorderRadius.circular( 4, ), ), padding: EdgeInsets.symmetric( horizontal: 8.w, vertical: 5.w, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( child: text( SCAppLocalizations.of( context, )!.report, textColor: Colors.white, fontSize: 12.sp, ), onTap: () { SmartDialog.dismiss( tag: "showUserInfoOptMenu", ); SCNavigatorUtils.push( context, "${SCMainRoute.report}?type=user&tageId=${ref.userProfile?.id}", replace: false, ); }, ), ((!(userIdentity?.admin ?? false) && !(userIdentity ?.superAdmin ?? false)) && ((Provider.of< SocialChatUserProfileManager >( context, listen: false, ).userIdentity?.admin ?? false))) ? GestureDetector( child: Container( margin: EdgeInsets.only( top: 5.w, ), child: text( SCAppLocalizations.of( context, )!.userEditing, letterSpacing: 0.1, textColor: Colors.white, fontSize: 12.sp, ), ), onTap: () { SmartDialog.dismiss( tag: "showUserInfoOptMenu", ); SCNavigatorUtils.push( context, "${SCMainRoute.editingUserRoomAdmin}?type=User&profile=${Uri.encodeComponent(jsonEncode(ref.userProfile?.toJson()))}", ); }, ) : Container(), ((ref.userIdentity?.admin ?? false) || (ref .userIdentity ?.superFreightAgent ?? false)) && !((userIdentity?.admin ?? false) || (userIdentity ?.superFreightAgent ?? false)) ? SizedBox(height: 5.w) : Container(), ((ref.userIdentity?.admin ?? false) || (ref .userIdentity ?.superFreightAgent ?? false)) && !((userIdentity?.admin ?? false) || (userIdentity ?.superFreightAgent ?? false)) ? GestureDetector( child: text( letterSpacing: 0.1, SCAppLocalizations.of( context, )!.becomeAgent, textColor: Colors.white, fontSize: 12.sp, ), onTap: () { SmartDialog.dismiss( tag: "showUserInfoOptMenu", ); SCAccountRepository() .teamCreate( ref.userProfile ?.getID() ?? "", ); }, ) : Container(), (ref.userProfile?.isCpRelation ?? false) ? GestureDetector( behavior: HitTestBehavior.opaque, child: Container( padding: EdgeInsets.only( top: 3.w, ), child: text( letterSpacing: 0.1, SCAppLocalizations.of( context, )!.partWays, textColor: Colors.white, fontSize: 12.sp, ), ), onTap: () { SmartDialog.dismiss( tag: "showUserInfoOptMenu", ); _showPartWaysDialog(ref); }, ) : Container(), isBlacklistLoading ? Container() : GestureDetector( behavior: HitTestBehavior.opaque, child: Container( padding: EdgeInsets.only( top: 3.w, ), child: text( letterSpacing: 0.1, isBlacklist ? SCAppLocalizations.of( context, )!.removeFromBlacklist : SCAppLocalizations.of( context, )!.moveToBlacklist, textColor: Colors.white, fontSize: 12.sp, ), ), onTap: () { if ((ref .userProfile ?.isCpRelation ?? false)) { SmartDialog.show( tag: "showConfirmDialog", alignment: Alignment.center, debounce: true, animationType: SmartAnimationType .fade, builder: (_) { return MsgDialog( title: SCAppLocalizations.of( context, )!.tips, msg: SCAppLocalizations.of( context, )!.youAreCurrentlyCPRelationshipPleaseDissolve, btnText: SCAppLocalizations.of( context, )!.confirm, onEnsure: () {}, ); }, ); return; } SCAccountHelper.optBlacklist( widget.tageId, isBlacklist, context, (isOptBlacklist) { isBlacklist = isOptBlacklist; setState(() {}); }, ); }, ), ], ), ), ), onTap: () { SmartDialog.dismiss( tag: "showUserInfoOptMenu", ); }, ); }, ); }, ); }, ), ], ), body: isBlacklist ? Container() : (isProfileLoading || isBlacklistLoading ? const SizedBox.shrink() : SingleChildScrollView( controller: _scrollController, physics: const BouncingScrollPhysics(), child: Column( children: [ _buildProfileHeader( ref, backgroundPhotos, dataCard, ), _buildProfileColumnContent(ref.userProfile), ], ), )), ), // 底部按钮区域 isBlacklistLoading || isBlacklist || isProfileLoading ? Container() : Positioned( bottom: 0, left: 0, right: 0, child: widget.isMe == "true" ? Container() : (isLoading ? Container() : _buildBottomActions(ref)), ), ], ), ), ); }, ); } void _showPartWaysDialog(SocialChatUserProfileManager ref) { SmartDialog.show( tag: "showPartWaysDialog", alignment: Alignment.center, debounce: true, animationType: SmartAnimationType.fade, builder: (_) { return Container( height: 530.w, width: ScreenUtil().screenWidth * 0.9, decoration: BoxDecoration( image: DecorationImage( image: AssetImage( "sc_images/person/sc_icon_send_cp_requst_dialog_bg.png", ), fit: BoxFit.fill, ), ), child: Column( children: [ SizedBox(height: 58.w), Container( child: text( SCAppLocalizations.of(context)!.partWays, fontSize: 22.sp, textColor: Color(0xffDB5872), fontWeight: FontWeight.bold, ), ), Transform.translate( offset: Offset(0, -25), child: Stack( alignment: AlignmentDirectional.center, children: [ PositionedDirectional( top: 28.w, child: Row( mainAxisSize: MainAxisSize.min, children: [ head( url: AccountStorage() .getCurrentUser() ?.userProfile ?.userAvatar ?? "", width: 90.w, ), SizedBox(width: 15.w), head( url: ref.userProfile?.userAvatar ?? "", width: 90.w, ), ], ), ), Image.asset( "sc_images/person/sc_icon_send_cp_requst_dialog_head2.png", height: 120.w, fit: BoxFit.fill, ), ], ), ), Transform.translate( offset: Offset(0, -23), child: Container( height: 25.w, width: ScreenUtil().screenWidth * 0.6, decoration: BoxDecoration( image: DecorationImage( image: AssetImage( "sc_images/person/sc_icon_send_cp_requst_username_bg.png", ), fit: BoxFit.fill, ), ), child: Row( children: [ socialchatNickNameText( maxWidth: 100.w, AccountStorage() .getCurrentUser() ?.userProfile ?.userNickname ?? "", fontSize: 10.sp, fontWeight: FontWeight.w600, type: AccountStorage() .getCurrentUser() ?.userProfile ?.getVIP() ?.name ?? "", needScroll: (AccountStorage() .getCurrentUser() ?.userProfile ?.userNickname ?.characters .length ?? 0) > 8, ), SizedBox(width: 20.w), socialchatNickNameText( maxWidth: 100.w, ref.userProfile?.userNickname ?? "", fontSize: 10.sp, fontWeight: FontWeight.w600, type: ref.userProfile?.getVIP()?.name ?? "", needScroll: (ref.userProfile?.userNickname?.characters.length ?? 0) > 8, ), ], ), ), ), Container( padding: EdgeInsets.only(bottom: 12.w), margin: EdgeInsets.symmetric(horizontal: 18.w), decoration: BoxDecoration( image: DecorationImage( image: AssetImage( "sc_images/person/sc_icon_send_cp_requst_dialog_content.png", ), fit: BoxFit.fill, ), ), child: Column( children: [ Container( margin: EdgeInsets.symmetric( horizontal: 35.w, ).copyWith(top: 45.w), child: text( SCAppLocalizations.of( context, )!.areYouSureYouWantToPartWaysWithYourCP, fontWeight: FontWeight.w500, textColor: Color(0xffFF79A1), maxLines: 3, fontSize: 14.sp, ), ), Container( margin: EdgeInsets.symmetric(horizontal: 20.w), alignment: AlignmentDirectional.center, child: text( SCAppLocalizations.of(context)!.partWaysTips, fontSize: 10.w, textColor: Color(0xffFE91B0), maxLines: 6, fontWeight: FontWeight.w500, ), ), ], ), ), SizedBox(height: 10.w), Row( mainAxisSize: MainAxisSize.min, children: [ GestureDetector( child: Container( padding: EdgeInsets.only(top: 7.w), alignment: AlignmentDirectional.center, width: 130.w, height: 48.w, decoration: BoxDecoration( image: DecorationImage( image: AssetImage( "sc_images/person/sc_icon_send_cp_requst_ok_bg.png", ), fit: BoxFit.fill, ), ), child: text( SCAppLocalizations.of(context)!.cancel, fontSize: 18.sp, fontWeight: FontWeight.bold, textColor: Color(0xffDB5872), ), ), onTap: () { SmartDialog.dismiss(tag: "showPartWaysDialog"); }, ), SizedBox(width: 25.w), GestureDetector( child: Container( padding: EdgeInsets.only(top: 7.w), alignment: AlignmentDirectional.center, width: 130.w, height: 48.w, decoration: BoxDecoration( image: DecorationImage( image: AssetImage( "sc_images/person/sc_icon_send_cp_requst_cancel_bg.png", ), fit: BoxFit.fill, ), ), child: text( SCAppLocalizations.of(context)!.confirm, fontSize: 18.sp, fontWeight: FontWeight.bold, textColor: Colors.white, ), ), onTap: () { final cpRelation = ref.userProfile?.cpList?.isNotEmpty == true ? ref.userProfile?.cpList?.first : null; SCLoadingManager.show(); SCAccountRepository() .cpRelationshipDismissApply( ref.userProfile?.id ?? "", relationType: cpRelation?.relationType ?? "CP", ) .then((result) { ref.refreshLoadedCpProfiles(); SmartDialog.dismiss(tag: "showPartWaysDialog"); SCTts.show( SCAppLocalizations.of( context, )!.operationSuccessful, ); SCLoadingManager.hide(); SCNavigatorUtils.popUntil( context, ModalRoute.withName(SCRoutes.home), ); }) .catchError((e) { SCLoadingManager.hide(); }); }, ), ], ), ], ), ); }, ); } } class _ProfileRelationProgressData { const _ProfileRelationProgressData({ required this.levelText, required this.currentValue, required this.targetValue, required this.expAwayText, required this.nextLevelText, this.dismissCost, }); factory _ProfileRelationProgressData.fallback( _ProfileRelationProgressPayload payload, ) { final currentValue = payload.cpValue; return _ProfileRelationProgressData( levelText: payload.levelText.trim().isEmpty ? "Lv.1 Simple Love" : payload.levelText, currentValue: currentValue, targetValue: currentValue > 0 ? currentValue : 1, expAwayText: SCStringUtils.formatCompactNumber(0), nextLevelText: _levelShortText( _nextLevelIndex(_levelIndex(payload.levelText)), ), dismissCost: payload.dismissCost, ); } factory _ProfileRelationProgressData.fromRightsConfig( SCCpRightsConfigRes config, _ProfileRelationProgressPayload payload, ) { final currentValue = (config.intimacyValue ?? payload.cpValue).toDouble(); final levels = config.levels .map(_ProfileRelationProgressLevel.fromRightsLevel) .toList(); return _ProfileRelationProgressData.fromLevels( currentValue: currentValue, levels: levels, payload: payload, dismissCost: config.dismissConsumeGold ?? payload.dismissCost, ); } factory _ProfileRelationProgressData.fromCabin( SCCpCabinRes cabin, _ProfileRelationProgressPayload payload, { int? fallbackDismissCost, }) { final currentValue = cabin.cpVal?.toDouble() ?? cabin.cpPairUserProfileCO?.cpValue ?? payload.cpValue; final levels = (cabin.cpCabinConfigCO ?? const []) .map(_ProfileRelationProgressLevel.fromDynamic) .toList(); return _ProfileRelationProgressData.fromLevels( currentValue: currentValue, levels: levels, payload: payload, dismissCost: cabin.dismissCost ?? fallbackDismissCost ?? payload.dismissCost, ); } factory _ProfileRelationProgressData.fromLevels({ required double currentValue, required List<_ProfileRelationProgressLevel> levels, required _ProfileRelationProgressPayload payload, int? dismissCost, }) { final sortedLevels = levels.where((level) => level.requiredValue != null).toList()..sort( (left, right) => left.requiredValue!.compareTo(right.requiredValue!), ); _ProfileRelationProgressLevel? 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 _ProfileRelationProgressData( levelText: _levelDisplayText(currentLevel, payload.levelText), currentValue: currentValue, targetValue: targetValue > 0 ? targetValue : 1, expAwayText: SCStringUtils.formatCompactNumber(expAway), nextLevelText: nextLevel == null ? _levelShortText( _nextLevelIndex( currentLevel?.level ?? _levelIndex(payload.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 _ProfileRelationProgressLevel? _currentLevelFromConfig( List<_ProfileRelationProgressLevel> levels, double currentValue, ) { _ProfileRelationProgressLevel? currentLevel; for (final level in levels) { if ((level.requiredValue ?? 0) <= currentValue) { currentLevel = level; } } return currentLevel; } static String _levelDisplayText( _ProfileRelationProgressLevel? 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 _ProfileRelationProgressLevel { const _ProfileRelationProgressLevel({ this.level, this.levelName, this.requiredValue, }); factory _ProfileRelationProgressLevel.fromRightsLevel( SCCpRightsLevelRes level, ) { return _ProfileRelationProgressLevel( level: level.level, levelName: level.levelName, requiredValue: level.requiredIntimacyValue, ); } factory _ProfileRelationProgressLevel.fromDynamic(dynamic value) { final source = value is Map ? value : const {}; return _ProfileRelationProgressLevel( 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; } dynamic _firstValue(Map source, List keys) { for (final key in keys) { if (source.containsKey(key) && source[key] != null) { return source[key]; } } return null; } String? _asString(dynamic value) { final text = value?.toString().trim(); return text == null || text.isEmpty ? null : text; } int? _asInt(dynamic value) { if (value is int) { return value; } if (value is num) { return value.toInt(); } return int.tryParse(value?.toString() ?? ""); } class _ProfileRelationProgressPayload { const _ProfileRelationProgressPayload({ 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 _ProfileRelationUser { const _ProfileRelationUser({ this.userId, this.account, this.nickname, this.avatarUrl, }); final String? userId; final String? account; final String? nickname; final String? avatarUrl; } class _ProfileCreatedRoom { const _ProfileCreatedRoom({ required this.roomId, required this.roomCover, required this.roomName, this.previewData, }); factory _ProfileCreatedRoom.fromSocialChatRoom( SocialChatRoomRes room, { SocialChatUserProfile? ownerProfile, }) { return _ProfileCreatedRoom( roomId: room.id ?? "", roomCover: room.roomCover ?? "", roomName: room.roomName ?? "", previewData: RoomEntryPreviewData.fromSocialChatRoom( ownerProfile == null ? room : room.copyWith(userProfile: ownerProfile), ), ); } factory _ProfileCreatedRoom.fromMyRoom( MyRoomRes room, { SocialChatUserProfile? ownerProfile, }) { return _ProfileCreatedRoom( roomId: room.id ?? "", roomCover: room.roomCover ?? "", roomName: room.roomName ?? "", previewData: RoomEntryPreviewData.fromMyRoom( room, ownerProfile: ownerProfile, ), ); } final String roomId; final String roomCover; final String roomName; final RoomEntryPreviewData? previewData; String displayName(SocialChatUserProfile? fallbackProfile) { final nickname = fallbackProfile?.userNickname?.trim() ?? ""; if (nickname.isNotEmpty) { return nickname; } final name = roomName.trim(); if (name.isNotEmpty) { return name; } return ""; } }