yumi-flutter/lib/modules/user/profile/person_detail_page.dart
2026-05-23 19:12:58 +08:00

3039 lines
104 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'dart:async';
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:yumi/main.dart' show routeObserver;
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<PersonDetailPage> with RouteAware {
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<String, SCUserCounterRes> counterMap = {};
List<SocialChatGiftRes> giftWallList = [];
List<WearBadge> honorWallBadges = [];
// 添加滚动控制器
final ScrollController _scrollController = ScrollController();
double _opacity = 0.0;
PageRoute<dynamic>? _routeObserverRoute;
@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<SocialChatUserProfileManager>(context, listen: false)
.userProfile = null;
Provider.of<SocialChatUserProfileManager>(
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();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
final route = ModalRoute.of(context);
if (route is PageRoute<dynamic> && _routeObserverRoute != route) {
if (_routeObserverRoute != null) {
routeObserver.unsubscribe(this);
}
_routeObserverRoute = route;
routeObserver.subscribe(this, route);
}
}
@override
void didPopNext() {
_refreshProfileRelationsAfterResume();
}
void _refreshProfileRelationsAfterResume() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) {
return;
}
final ref = Provider.of<SocialChatUserProfileManager>(
context,
listen: false,
);
final profile = ref.userProfile;
final loadedUserId = profile?.id?.trim() ?? "";
final targetUserId = widget.tageId.trim();
if (targetUserId.isEmpty) {
return;
}
final hasRelationCards =
(profile?.cpList?.isNotEmpty ?? false) ||
(profile?.closeFriendList?.isNotEmpty ?? false);
if (loadedUserId.isEmpty ||
loadedUserId != targetUserId ||
!hasRelationCards) {
unawaited(ref.reloadUserInfoById(targetUserId));
return;
}
unawaited(ref.refreshLoadedCpProfiles());
});
}
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 = <String>{
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<SocialChatRoomRes> 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() {
routeObserver.unsubscribe(this);
_routeObserverRoute = null;
_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<SCAppGeneralManager>(
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 <WearBadge>[],
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 <WearBadge>[],
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 <UseProps>[]) {
final resource = item.propsResources;
if (resource?.type != SCPropsType.DATA_CARD.name) {
continue;
}
final expireText = item.expireTime?.trim() ?? "";
final expireTime = int.tryParse(expireText);
if (expireText.isNotEmpty && (expireTime ?? 0) <= now) {
continue;
}
final source = resource?.sourceUrl?.trim() ?? "";
final cover = resource?.cover?.trim() ?? "";
if (source.isNotEmpty || cover.isNotEmpty) {
return resource;
}
}
return null;
}
Widget _buildProfileHeader(
SocialChatUserProfileManager ref,
List<PersonPhoto> 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: [
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 _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<CPRes> 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<CPRes>? relations,
String relationType,
SocialChatUserProfile? profile,
) {
for (final relation in relations ?? const <CPRes>[]) {
if (_normalizeProfileRelationType(relation.relationType) !=
relationType) {
continue;
}
if (_profileRelationHasPartner(profile, relation)) {
return relation;
}
}
return null;
}
List<CPRes> _profileCloseFriendRelations(SocialChatUserProfile? profile) {
final brothers = <CPRes>[];
final sisters = <CPRes>[];
for (final relation in profile?.closeFriendList ?? const <CPRes>[]) {
if (!_profileRelationHasPartner(profile, relation)) {
continue;
}
final type = _normalizeProfileRelationType(relation.relationType);
if (type == "BROTHER" && brothers.length < 3) {
brothers.add(relation);
} else if (type == "SISTERS" && sisters.length < 3) {
sisters.add(relation);
}
}
return <CPRes>[...brothers, ...sisters];
}
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) {
return scNormalizeCpRelationType(relationType);
}
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<void> _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<void> _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;
}
Provider.of<RtcProvider?>(
context,
listen: false,
)?.invalidateRoomMicRelationBetween(
userId: payload.leftUserId,
peerUserId: partnerUserId,
relationType: payload.relationType,
);
await Provider.of<SocialChatUserProfileManager>(
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_${_profileRelationAssetLevel(relation)}.png";
}
int _profileRelationAssetLevel(CPRes relation) {
final level = _profileRelationLevel(relation);
return level <= 0 ? 1 : level;
}
int _profileRelationLevel(CPRes relation) {
return scCpRelationLevelIndex(relation.levelText);
}
String _profileRelationLevelText(CPRes relation) {
return _profileRelationLevelDisplayText(
_firstNonBlankProfileValue([relation.levelText, "Lv.0"]),
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<String?> 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<RtcProvider>(
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<void> _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<void> _openChat() async {
final conversation = V2TimConversation(
type: ConversationType.V2TIM_C2C,
userID: widget.tageId,
conversationID: '',
);
final started = await Provider.of<RtmProvider>(
context,
listen: false,
).startConversation(conversation);
if (!started || !mounted) return;
final json = jsonEncode(conversation.toJson());
SCNavigatorUtils.push(
context,
"${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}",
);
}
Future<void> _openEditProfile() async {
await SCNavigatorUtils.push(context, SCMainRoute.edit, replace: false);
if (!mounted) {
return;
}
final ref = Provider.of<SocialChatUserProfileManager>(
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<SocialChatUserProfileManager>(
builder: (context, ref, child) {
final bool isProfileLoading = ref.userProfile == null;
List<PersonPhoto> backgroundPhotos = [];
backgroundPhotos =
(isProfileLoading
? <PersonPhoto>[]
: (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) {
if (mounted) {
Provider.of<RtcProvider?>(
context,
listen: false,
)?.invalidateRoomMicRelationBetween(
userId:
AccountStorage()
.getCurrentUser()
?.userProfile
?.id,
peerUserId: ref.userProfile?.id,
relationType: cpRelation?.relationType ?? "CP",
);
}
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: scCpRelationLevelDisplayText(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 <dynamic>[])
.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 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 _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 <String, dynamic>{};
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<String> 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 "";
}
}