show medal wall

show user follow time

show top image title

Signed-off-by: dom <githubaccount56556@proton.me>
This commit is contained in:
dom
2026-03-20 22:12:43 +08:00
parent 662ccfcf0a
commit ae59d257c3
21 changed files with 927 additions and 222 deletions

View File

@@ -16,6 +16,7 @@ import 'package:PiliPlus/pages/fan/view.dart';
import 'package:PiliPlus/pages/follow/view.dart';
import 'package:PiliPlus/pages/follow_type/followed/view.dart';
import 'package:PiliPlus/pages/member/widget/header_layout_widget.dart';
import 'package:PiliPlus/pages/member/widget/medal_widget.dart';
import 'package:PiliPlus/utils/accounts.dart';
import 'package:PiliPlus/utils/app_scheme.dart';
import 'package:PiliPlus/utils/extension/context_ext.dart';
@@ -25,9 +26,12 @@ import 'package:PiliPlus/utils/extension/theme_ext.dart';
import 'package:PiliPlus/utils/image_utils.dart';
import 'package:PiliPlus/utils/num_utils.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/platform_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/foundation.dart' show kDebugMode;
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
class UserInfoCard extends StatelessWidget {
@@ -41,6 +45,7 @@ class UserInfoCard extends StatelessWidget {
this.live,
this.silence,
required this.headerControllerBuilder,
required this.showLiveMedalWall,
});
final bool isOwner;
@@ -51,6 +56,7 @@ class UserInfoCard extends StatelessWidget {
final Live? live;
final int? silence;
final ValueGetter<PageController> headerControllerBuilder;
final VoidCallback showLiveMedalWall;
@override
Widget build(BuildContext context) {
@@ -88,9 +94,15 @@ class UserInfoCard extends StatelessWidget {
case UserInfoType.like:
count = card.likes?.likeNum;
}
void onShowCount() => SmartDialog.showToast(
'${type.title}: $count',
alignment: const Alignment(0.0, -0.8),
);
return GestureDetector(
behavior: .opaque,
onTap: onTap,
onLongPress: PlatformUtils.isMobile ? onShowCount : null,
onSecondaryTap: PlatformUtils.isDesktop ? onShowCount : null,
child: Align(
alignment: type.alignment,
widthFactor: 1.0,
@@ -119,206 +131,241 @@ class UserInfoCard extends StatelessWidget {
BuildContext context,
ColorScheme colorScheme,
bool isLight,
) => [
Padding(
padding: const EdgeInsets.only(left: 20, right: 20),
child: Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
GestureDetector(
onTap: () => Utils.copyText(card.name!),
child: Text(
card.name!,
strutStyle: const StrutStyle(
height: 1,
leading: 0,
fontSize: 17,
fontWeight: FontWeight.bold,
),
style: TextStyle(
height: 1,
fontSize: 17,
fontWeight: FontWeight.bold,
color: (card.vip?.status ?? -1) > 0 && card.vip?.type == 2
? colorScheme.vipColor
: null,
),
),
) {
Widget? liveMedal;
if (card.liveFansWearing?.detailV2 case final detailV2?) {
Color? nameColor;
Color? levelColor;
Color? backgroundColor;
try {
nameColor = Utils.parseColor(detailV2.medalColorName!);
levelColor = Utils.parseColor(detailV2.medalColorLevel!);
backgroundColor = Utils.parseColor(detailV2.medalColor!);
} catch (e, s) {
if (kDebugMode) {
Utils.reportError(e, s);
}
}
try {
liveMedal = GestureDetector(
onTap: showLiveMedalWall,
child: MedalWidget(
medalName: detailV2.medalName!,
level: detailV2.level!,
backgroundColor: backgroundColor ?? colorScheme.secondaryContainer,
nameColor: nameColor ?? colorScheme.onSecondaryContainer,
levelColor: levelColor ?? colorScheme.onSecondaryContainer,
),
Image.asset(
Utils.levelName(
card.levelInfo!.currentLevel!,
isSeniorMember: card.levelInfo?.identity == 2,
),
height: 11,
cacheHeight: 11.cacheSize(context),
semanticLabel: '等级${card.levelInfo?.currentLevel}',
),
if (card.vip?.status == 1)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
borderRadius: StyleString.mdRadius,
color: colorScheme.vipColor,
),
);
} catch (e, s) {
if (kDebugMode) {
Utils.reportError(e, s);
}
}
}
return [
Padding(
padding: const EdgeInsets.only(left: 20, right: 20),
child: Wrap(
spacing: 8,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
GestureDetector(
onTap: () => Utils.copyText(card.name!),
child: Text(
card.vip?.label?.text ?? '大会员',
card.name!,
strutStyle: const StrutStyle(
height: 1,
fontSize: 10,
leading: 0,
fontSize: 17,
fontWeight: FontWeight.bold,
),
style: const TextStyle(
style: TextStyle(
height: 1,
fontSize: 17,
fontWeight: FontWeight.bold,
fontSize: 10,
color: Colors.white,
color: (card.vip?.status ?? -1) > 0 && card.vip?.type == 2
? colorScheme.vipColor
: null,
),
),
),
// if (card.nameplate?.imageSmall?.isNotEmpty == true)
// CachedNetworkImage(
// imageUrl: ImageUtils.thumbnailUrl(card.nameplate!.imageSmall!),
// height: 20,
// placeholder: (context, url) {
// return const SizedBox.shrink();
// },
// ),
],
),
),
if (card.officialVerify?.desc?.isNotEmpty == true)
Container(
margin: const EdgeInsets.only(left: 20, top: 8, right: 20),
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
color: colorScheme.onInverseSurface,
),
child: Text.rich(
TextSpan(
children: [
if (card.officialVerify?.icon?.isNotEmpty == true) ...[
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.surface,
),
child: Icon(
Icons.offline_bolt,
color: card.officialVerify?.type == 0
? const Color(0xFFFFCC00)
: Colors.lightBlueAccent,
size: 18,
),
Image.asset(
Utils.levelName(
card.levelInfo!.currentLevel!,
isSeniorMember: card.levelInfo?.identity == 2,
),
height: 11,
cacheHeight: 11.cacheSize(context),
semanticLabel: '等级${card.levelInfo?.currentLevel}',
),
if (card.vip?.status == 1)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
borderRadius: StyleString.mdRadius,
color: colorScheme.vipColor,
),
child: Text(
card.vip?.label?.text ?? '大会员',
strutStyle: const StrutStyle(
height: 1,
leading: 0,
fontSize: 10,
fontWeight: FontWeight.bold,
),
style: const TextStyle(
height: 1,
fontSize: 10,
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
const TextSpan(
text: ' ',
),
// if (card.nameplate?.imageSmall?.isNotEmpty == true)
// CachedNetworkImage(
// imageUrl: ImageUtils.thumbnailUrl(card.nameplate!.imageSmall!),
// height: 20,
// placeholder: (context, url) {
// return const SizedBox.shrink();
// },
// ),
?liveMedal,
],
),
),
if (card.officialVerify?.desc?.isNotEmpty == true)
Container(
margin: const EdgeInsets.only(left: 20, top: 8, right: 20),
padding: const EdgeInsets.symmetric(horizontal: 5, vertical: 2),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
color: colorScheme.onInverseSurface,
),
child: Text.rich(
TextSpan(
children: [
if (card.officialVerify?.icon?.isNotEmpty == true) ...[
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: DecoratedBox(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.surface,
),
child: Icon(
Icons.offline_bolt,
color: card.officialVerify?.type == 0
? const Color(0xFFFFCC00)
: Colors.lightBlueAccent,
size: 18,
),
),
),
const TextSpan(
text: ' ',
),
],
TextSpan(
text: card.officialVerify!.spliceTitle!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
TextSpan(
text: card.officialVerify!.spliceTitle!,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface.withValues(alpha: 0.7),
),
),
],
),
),
),
if (card.sign?.isNotEmpty == true)
Padding(
padding: const EdgeInsets.only(left: 20, top: 6, right: 20),
child: SelectableText(
card.sign!.trim().replaceAll(RegExp(r'\n{2,}'), '\n'),
style: const TextStyle(fontSize: 14),
),
),
if (card.followingsFollowedUpper?.items?.isNotEmpty == true) ...[
const SizedBox(height: 6),
_buildFollowedUp(colorScheme, card.followingsFollowedUpper!),
],
Padding(
padding: const EdgeInsets.only(left: 20, top: 6, right: 20),
child: Wrap(
spacing: 10,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
GestureDetector(
onTap: () => Utils.copyText(card.mid.toString()),
child: Text(
'UID: ${card.mid}',
style: TextStyle(
fontSize: 12,
color: colorScheme.outline,
),
),
),
...?card.spaceTag?.map(
(item) {
final hasUri = item.uri?.isNotEmpty == true;
final child = Text(
item.title ?? '',
),
if (card.sign?.isNotEmpty == true)
Padding(
padding: const EdgeInsets.only(left: 20, top: 6, right: 20),
child: SelectableText(
card.sign!.trim().replaceAll(RegExp(r'\n{2,}'), '\n'),
style: const TextStyle(fontSize: 14),
),
),
if (card.followingsFollowedUpper?.items?.isNotEmpty == true) ...[
const SizedBox(height: 6),
_buildFollowedUp(colorScheme, card.followingsFollowedUpper!),
],
Padding(
padding: const EdgeInsets.only(left: 20, top: 6, right: 20),
child: Wrap(
spacing: 10,
runSpacing: 8,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
GestureDetector(
onTap: () => Utils.copyText(card.mid.toString()),
child: Text(
'UID: ${card.mid}',
style: TextStyle(
fontSize: 12,
color: hasUri ? colorScheme.secondary : colorScheme.outline,
color: colorScheme.outline,
),
);
if (hasUri) {
return GestureDetector(
onTap: () => PiliScheme.routePushFromUrl(item.uri!),
child: child,
),
),
...?card.spaceTag?.map(
(item) {
final hasUri = item.uri?.isNotEmpty == true;
final child = Text(
item.title ?? '',
style: TextStyle(
fontSize: 12,
color: hasUri ? colorScheme.secondary : colorScheme.outline,
),
);
}
return child;
},
),
],
),
),
if (silence == 1)
Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(6)),
color: isLight ? colorScheme.errorContainer : colorScheme.error,
),
margin: const EdgeInsets.only(left: 20, top: 8, right: 20),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text.rich(
TextSpan(
children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.info,
size: 17,
color: isLight
? colorScheme.onErrorContainer
: colorScheme.onError,
),
),
TextSpan(
text: ' 该账号封禁中',
style: TextStyle(
color: isLight
? colorScheme.onErrorContainer
: colorScheme.onError,
),
),
],
),
if (hasUri) {
return GestureDetector(
onTap: () => PiliScheme.routePushFromUrl(item.uri!),
child: child,
);
}
return child;
},
),
],
),
),
];
if (silence == 1)
Container(
width: double.infinity,
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(6)),
color: isLight ? colorScheme.errorContainer : colorScheme.error,
),
margin: const EdgeInsets.only(left: 20, top: 8, right: 20),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
child: Text.rich(
TextSpan(
children: [
WidgetSpan(
alignment: PlaceholderAlignment.middle,
child: Icon(
Icons.info,
size: 17,
color: isLight
? colorScheme.onErrorContainer
: colorScheme.onError,
),
),
TextSpan(
text: ' 该账号封禁中',
style: TextStyle(
color: isLight
? colorScheme.onErrorContainer
: colorScheme.onError,
),
),
],
),
),
),
];
}
Column _buildRight(ColorScheme colorScheme) => Column(
mainAxisSize: MainAxisSize.min,
@@ -541,6 +588,34 @@ class UserInfoCard extends StatelessWidget {
},
),
),
Positioned(
right: 0,
bottom: 3.5,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 125),
child: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: .centerLeft,
end: .centerRight,
colors: [
Colors.transparent,
Colors.black12,
Colors.black38,
Colors.black45,
],
),
),
child: Padding(
padding: const .only(left: 15, right: 5, bottom: 2),
child: HeaderTitle(
images: imgUrls,
pageController: controller,
),
),
),
),
),
Positioned(
left: 0,
right: 0,
@@ -788,3 +863,79 @@ class _HeaderIndicatorState extends State<HeaderIndicator> {
);
}
}
class HeaderTitle extends StatefulWidget {
const HeaderTitle({
super.key,
required this.images,
required this.pageController,
});
final List<TopImage> images;
final PageController pageController;
@override
State<HeaderTitle> createState() => _HeaderTitleState();
}
class _HeaderTitleState extends State<HeaderTitle> {
late int _index;
@override
void initState() {
super.initState();
_updateIndex();
widget.pageController.addListener(_listener);
}
void _listener() {
_updateIndex();
setState(() {});
}
void _updateIndex() {
_index = widget.pageController.page?.round() ?? 0;
}
@override
void dispose() {
widget.pageController.removeListener(_listener);
super.dispose();
}
@override
Widget build(BuildContext context) {
final title = widget.images[_index].title;
if (title == null) return const SizedBox.shrink();
try {
return Column(
crossAxisAlignment: .end,
children: [
Text(
title.title!,
maxLines: 1,
overflow: .ellipsis,
style: const TextStyle(fontSize: 12, color: Colors.white),
),
Text(
title.subTitle!,
style: TextStyle(
fontSize: 12,
fontFamily: 'digital_id_num',
color: title.subTitleColorFormat?.colors?.isNotEmpty == true
? Utils.parseMedalColor(
title.subTitleColorFormat!.colors!.last,
)
: Colors.white,
),
),
],
);
} catch (e, s) {
if (kDebugMode) {
Utils.reportError(e, s);
}
return const SizedBox.shrink();
}
}
}