3151 lines
107 KiB
Dart
3151 lines
107 KiB
Dart
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;
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
_loadInitialProfileData();
|
||
});
|
||
}
|
||
|
||
void _loadInitialProfileData() {
|
||
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||
context,
|
||
listen: false,
|
||
);
|
||
profileManager.userProfile = null;
|
||
profileManager.getUserInfoById(widget.tageId);
|
||
SCAccountRepository().userIdentity(userId: widget.tageId).then((v) {
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
userIdentity = v;
|
||
setState(() {});
|
||
});
|
||
if (widget.isMe != "true") {
|
||
SCAccountRepository()
|
||
.followCheck(widget.tageId)
|
||
.then((v) {
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
isFollow = v;
|
||
isLoading = false;
|
||
setState(() {});
|
||
})
|
||
.catchError((e) {
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
setState(() {
|
||
isLoading = false;
|
||
});
|
||
});
|
||
SCAccountRepository()
|
||
.blacklistCheck(widget.tageId)
|
||
.then((v) {
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
isBlacklist = v;
|
||
isBlacklistLoading = false;
|
||
setState(() {});
|
||
if (isBlacklist) {
|
||
SCTts.show(
|
||
SCAppLocalizations.of(context)!.thisUserHasBeenBlacklisted,
|
||
);
|
||
}
|
||
})
|
||
.catchError((e) {
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
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,
|
||
gifFit: BoxFit.cover,
|
||
gifAlignment: Alignment.topCenter,
|
||
),
|
||
);
|
||
}).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;
|
||
}
|
||
const cancelText = 'unbind';
|
||
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 = _counterpartUserIdForCurrentUser(
|
||
leftUserId: payload.leftUserId,
|
||
rightUserId: payload.rightUserId,
|
||
);
|
||
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;
|
||
}
|
||
}
|
||
|
||
String _counterpartUserIdForCurrentUser({
|
||
required String leftUserId,
|
||
required String rightUserId,
|
||
}) {
|
||
final currentUserId =
|
||
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
|
||
final leftId = leftUserId.trim();
|
||
final rightId = rightUserId.trim();
|
||
if (currentUserId.isEmpty) {
|
||
return rightId;
|
||
}
|
||
if (currentUserId == leftId) {
|
||
return rightId;
|
||
}
|
||
if (currentUserId == rightId) {
|
||
return leftId;
|
||
}
|
||
return rightId;
|
||
}
|
||
|
||
Future<void> _showProfileRelationDismissConfirm(
|
||
_ProfileRelationProgressPayload payload, {
|
||
int? dismissCost,
|
||
}) async {
|
||
final partnerUserId = _counterpartUserIdForCurrentUser(
|
||
leftUserId: payload.leftUserId,
|
||
rightUserId: payload.rightUserId,
|
||
);
|
||
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;
|
||
}
|
||
final profileManager = Provider.of<SocialChatUserProfileManager>(
|
||
context,
|
||
listen: false,
|
||
);
|
||
profileManager.removeLoadedCpRelation(
|
||
userId: payload.leftUserId,
|
||
peerUserId: partnerUserId,
|
||
relationType: payload.relationType,
|
||
);
|
||
Provider.of<RtcProvider?>(
|
||
context,
|
||
listen: false,
|
||
)?.invalidateRoomMicRelationBetween(
|
||
userId: payload.leftUserId,
|
||
peerUserId: partnerUserId,
|
||
relationType: payload.relationType,
|
||
);
|
||
await profileManager.refreshLoadedCpProfiles();
|
||
profileManager.removeLoadedCpRelation(
|
||
userId: payload.leftUserId,
|
||
peerUserId: partnerUserId,
|
||
relationType: payload.relationType,
|
||
);
|
||
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,
|
||
allowGif: true,
|
||
);
|
||
}
|
||
|
||
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],
|
||
);
|
||
ref.userProfile = updatedProfile;
|
||
ref.syncCurrentUserProfile(updatedProfile);
|
||
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;
|
||
final bool profileLoadFailed =
|
||
ref.userProfileLoadError != null &&
|
||
!ref.isUserProfileLoading &&
|
||
ref.userProfile == null;
|
||
List<PersonPhoto> backgroundPhotos = [];
|
||
backgroundPhotos =
|
||
(isProfileLoading || profileLoadFailed
|
||
? <PersonPhoto>[]
|
||
: (ref.userProfile?.backgroundPhotos ?? []))
|
||
.where((t) => t.status == 1)
|
||
.toList();
|
||
final dataCard =
|
||
isProfileLoading || profileLoadFailed
|
||
? 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()
|
||
: (profileLoadFailed
|
||
? _buildProfileLoadFailed(ref)
|
||
: (isProfileLoading || isBlacklistLoading
|
||
? const SizedBox.shrink()
|
||
: SingleChildScrollView(
|
||
controller: _scrollController,
|
||
physics: const BouncingScrollPhysics(),
|
||
child: Column(
|
||
children: [
|
||
_buildProfileHeader(
|
||
ref,
|
||
backgroundPhotos,
|
||
dataCard,
|
||
),
|
||
_buildProfileColumnContent(
|
||
ref.userProfile,
|
||
),
|
||
],
|
||
),
|
||
))),
|
||
),
|
||
// 底部按钮区域
|
||
isBlacklistLoading ||
|
||
isBlacklist ||
|
||
isProfileLoading ||
|
||
profileLoadFailed
|
||
? Container()
|
||
: Positioned(
|
||
bottom: 0,
|
||
left: 0,
|
||
right: 0,
|
||
child:
|
||
widget.isMe == "true"
|
||
? Container()
|
||
: (isLoading
|
||
? Container()
|
||
: _buildBottomActions(ref)),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildProfileLoadFailed(SocialChatUserProfileManager ref) {
|
||
final localizations = SCAppLocalizations.of(context);
|
||
return Center(
|
||
child: SCDebounceWidget(
|
||
onTap: () => ref.getUserInfoById(widget.tageId),
|
||
child: Padding(
|
||
padding: EdgeInsets.symmetric(horizontal: 32.w),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(Icons.refresh, color: Colors.white70, size: 32.w),
|
||
SizedBox(height: 12.w),
|
||
text(
|
||
localizations?.loadingFailedClickToRetry ??
|
||
'Loading failed, click to retry',
|
||
textAlign: TextAlign.center,
|
||
textColor: Colors.white70,
|
||
fontSize: 14.sp,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
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) async {
|
||
final currentUserId =
|
||
AccountStorage()
|
||
.getCurrentUser()
|
||
?.userProfile
|
||
?.id
|
||
?.trim() ??
|
||
"";
|
||
final profileUserId =
|
||
ref.userProfile?.id?.trim() ?? "";
|
||
ref.removeLoadedCpRelation(
|
||
userId: currentUserId,
|
||
peerUserId: profileUserId,
|
||
relationType: cpRelation?.relationType ?? "CP",
|
||
);
|
||
if (mounted) {
|
||
Provider.of<RtcProvider?>(
|
||
context,
|
||
listen: false,
|
||
)?.invalidateRoomMicRelationBetween(
|
||
userId:
|
||
AccountStorage()
|
||
.getCurrentUser()
|
||
?.userProfile
|
||
?.id,
|
||
peerUserId: ref.userProfile?.id,
|
||
relationType: cpRelation?.relationType ?? "CP",
|
||
);
|
||
}
|
||
await ref.refreshLoadedCpProfiles();
|
||
ref.removeLoadedCpRelation(
|
||
userId: currentUserId,
|
||
peerUserId: profileUserId,
|
||
relationType: cpRelation?.relationType ?? "CP",
|
||
);
|
||
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 "";
|
||
}
|
||
}
|