diff --git a/lib/common/widgets/avatars.dart b/lib/common/widgets/avatars.dart index 933060b27..c49db91af 100644 --- a/lib/common/widgets/avatars.dart +++ b/lib/common/widgets/avatars.dart @@ -5,11 +5,11 @@ import 'package:flutter/material.dart'; Widget avatars({ required ColorScheme colorScheme, required Iterable users, + double gap = 6.0, }) { - const gap = 6.0; const size = 22.0; const padding = 0.8; - const offset = size - gap; + final offset = size - gap; const imgSize = size - 2 * padding; if (users.length == 1) { return NetworkImgLayer( diff --git a/lib/http/api.dart b/lib/http/api.dart index 903e4e2be..fdba2b4ce 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -997,4 +997,7 @@ abstract final class Api { static const String liveMedalWall = '${HttpString.liveBaseUrl}/xlive/web-ucenter/user/MedalWall'; + + static const String memberGuard = + '${HttpString.liveBaseUrl}/xlive/app-ucenter/v1/guard/MainGuardCardAll'; } diff --git a/lib/http/member.dart b/lib/http/member.dart index df8ae83ce..6cb090333 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -21,6 +21,7 @@ import 'package:PiliPlus/models_new/member/coin_like_arc/data.dart'; import 'package:PiliPlus/models_new/member/search_archive/data.dart'; import 'package:PiliPlus/models_new/member/season_web/data.dart'; import 'package:PiliPlus/models_new/member_card_info/data.dart'; +import 'package:PiliPlus/models_new/member_guard/data.dart'; import 'package:PiliPlus/models_new/space/space/data.dart'; import 'package:PiliPlus/models_new/space/space_archive/data.dart'; import 'package:PiliPlus/models_new/space/space_article/data.dart'; @@ -830,4 +831,23 @@ abstract final class MemberHttp { return Error(res.data['message']); } } + + static Future> memberGuard({ + required Object ruid, + required int page, + }) async { + final res = await Request().get( + Api.memberGuard, + queryParameters: { + 'page': page, + 'page_size': 20, + 'ruid': ruid, + }, + ); + if (res.data['code'] == 0) { + return Success(MemberGuardData.fromJson(res.data['data'])); + } else { + return Error(res.data['message']); + } + } } diff --git a/lib/models_new/member_guard/data.dart b/lib/models_new/member_guard/data.dart new file mode 100644 index 000000000..2267874fa --- /dev/null +++ b/lib/models_new/member_guard/data.dart @@ -0,0 +1,19 @@ +import 'package:PiliPlus/models_new/member_guard/guard_top_list.dart'; + +class MemberGuardData { + List guardTopList; + int? hasMore; + + MemberGuardData({ + required this.guardTopList, + this.hasMore, + }); + + factory MemberGuardData.fromJson(Map json) => + MemberGuardData( + guardTopList: (json['guard_top_list'] as List) + .map((e) => GuardItem.fromJson(e as Map)) + .toList(), + hasMore: json['has_more'] as int?, + ); +} diff --git a/lib/models_new/member_guard/guard_top_list.dart b/lib/models_new/member_guard/guard_top_list.dart new file mode 100644 index 000000000..f2dbe50bb --- /dev/null +++ b/lib/models_new/member_guard/guard_top_list.dart @@ -0,0 +1,20 @@ +class GuardItem { + int uid; + String username; + String face; + int guardLevel; + + GuardItem({ + required this.uid, + required this.username, + required this.face, + required this.guardLevel, + }); + + factory GuardItem.fromJson(Map json) => GuardItem( + uid: json['uid'], + username: json['username'], + face: json['face'], + guardLevel: json['guard_level'], + ); +} diff --git a/lib/models_new/space/space/elec.dart b/lib/models_new/space/space/elec.dart index 1bcaa61ab..3e2a9518b 100644 --- a/lib/models_new/space/space/elec.dart +++ b/lib/models_new/space/space/elec.dart @@ -1,3 +1,5 @@ +import 'package:PiliPlus/models/model_owner.dart'; + class Elec { int? total; List? list; @@ -15,9 +17,11 @@ class Elec { ); } -class ElecItem { +class ElecItem extends Owner { String? uname; String? avatar; + @override + String? get face => avatar; ElecItem({ this.uname, diff --git a/lib/models_new/space/space/guard.dart b/lib/models_new/space/space/guard.dart index 2fe4cd197..bbf4f8a45 100644 --- a/lib/models_new/space/space/guard.dart +++ b/lib/models_new/space/space/guard.dart @@ -2,20 +2,17 @@ import 'package:PiliPlus/models/model_owner.dart'; class Guard { String? uri; - String? desc; + Object? count; List? item; - Guard({ - this.uri, - this.desc, - this.item, - }); - - factory Guard.fromJson(Map json) => Guard( - uri: json['uri'] as String?, - desc: json['desc'] as String?, - item: (json['item'] as List?) + Guard.fromJson(Map json) { + uri = json['uri'] as String?; + item = (json['item'] as List?) ?.map((e) => Owner.fromJson(e as Map)) - .toList(), - ); + .toList(); + final String? desc = json['desc']; + if (desc != null) { + count = RegExp(r'^(\d+)').firstMatch(desc)?.group(1); + } + } } diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index 8b64e936f..9dd5d62f3 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -5,7 +5,9 @@ import 'package:PiliPlus/http/member.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/models/common/member/tab_type.dart'; +import 'package:PiliPlus/models/model_owner.dart'; import 'package:PiliPlus/models_new/space/space/data.dart'; +import 'package:PiliPlus/models_new/space/space/elec.dart'; import 'package:PiliPlus/models_new/space/space/live.dart'; import 'package:PiliPlus/models_new/space/space/setting.dart'; import 'package:PiliPlus/models_new/space/space/tab2.dart'; @@ -43,7 +45,13 @@ class MemberController extends CommonDataController bool? hasSeasonOrSeries; - late bool hasCharge = false; + List? charges; + int? chargeCount; + bool get hasCharge => chargeCount != null && chargeCount! > 0; + + List? guards; + Object? guardCount; + bool get hasGuard => guards?.isNotEmpty ?? false; final fromViewAid = Get.parameters['from_view_aid']; @@ -58,21 +66,30 @@ class MemberController extends CommonDataController @override bool customHandleResponse(bool isRefresh, Success response) { final data = response.response; - username = data.card?.name ?? ''; - isFollowed = data.card?.relation?.isFollowed; - hasCharge = (data.elec?.total ?? 0) > 0; + final card = data.card; + username = card?.name ?? ''; + isFollowed = card?.relation?.isFollowed; + // charge + final elec = data.elec; + charges = elec?.list; + chargeCount = elec?.total; + // guard + final guard = data.guard; + guards = guard?.item; + guardCount = guard?.count; + if (data.relation == -1) { relation.value = 128; } else { - relation.value = data.card?.relation?.isFollow == 1 + relation.value = card?.relation?.isFollow == 1 ? data.relSpecial == 1 ? -10 - : data.card?.relation?.status ?? 2 + : card?.relation?.status ?? 2 : 0; } tab2 = data.tab2; live = data.live; - silence = data.card?.silence; + silence = card?.silence; if ((data.ugcSeason?.count != null && data.ugcSeason?.count != 0) || data.series?.item?.isNotEmpty == true) { hasSeasonOrSeries = true; diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index ab90f70cd..0604e55ca 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -103,6 +103,10 @@ class _MemberPageState extends State { silence: _userController.silence, headerControllerBuilder: getHeaderController, showLiveMedalWall: _showLiveMedalWall, + charges: _userController.charges, + chargeCount: _userController.chargeCount, + guards: _userController.guards, + guardCount: _userController.guardCount, ), ), ), @@ -218,23 +222,38 @@ class _MemberPageState extends State { ], ), ), - if (_userController.hasCharge) - PopupMenuItem( - onTap: () => Get.toNamed( - '/upowerRank', - parameters: { - 'mid': _userController.mid.toString(), - }, - ), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.electric_bolt, size: 19), - SizedBox(width: 10), - Text('充电排行榜'), - ], - ), - ), + // if (_userController.hasCharge) + // PopupMenuItem( + // onTap: () => UpowerRankPage.toUpowerRank( + // mid: _userController.mid, + // name: _userController.username ?? '', + // count: _userController.chargeCount, + // ), + // child: const Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Icon(Icons.electric_bolt, size: 19), + // SizedBox(width: 10), + // Text('充电排行榜'), + // ], + // ), + // ), + // if (_userController.hasGuard) + // PopupMenuItem( + // onTap: () => MemberGuard.toMemberGuard( + // mid: _userController.mid, + // name: _userController.username ?? '', + // count: _userController.guardCount, + // ), + // child: const Row( + // mainAxisSize: MainAxisSize.min, + // children: [ + // Icon(Icons.anchor, size: 19), + // SizedBox(width: 10), + // Text('大航海舰队'), + // ], + // ), + // ), if (Get.isRegistered(tag: _heroTag)) PopupMenuItem( onTap: _toWebArchive, diff --git a/lib/pages/member/widget/user_info_card.dart b/lib/pages/member/widget/user_info_card.dart index 8a201d5f0..7ab6cfab8 100644 --- a/lib/pages/member/widget/user_info_card.dart +++ b/lib/pages/member/widget/user_info_card.dart @@ -7,7 +7,9 @@ import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/common/member/user_info_type.dart'; +import 'package:PiliPlus/models/model_owner.dart'; import 'package:PiliPlus/models_new/space/space/card.dart'; +import 'package:PiliPlus/models_new/space/space/elec.dart'; import 'package:PiliPlus/models_new/space/space/followings_followed_upper.dart'; import 'package:PiliPlus/models_new/space/space/images.dart'; import 'package:PiliPlus/models_new/space/space/live.dart'; @@ -18,6 +20,8 @@ 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/pages/member_guard/view.dart'; +import 'package:PiliPlus/pages/member_upower_rank/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; @@ -47,6 +51,10 @@ class UserInfoCard extends StatelessWidget { this.silence, required this.headerControllerBuilder, required this.showLiveMedalWall, + required this.charges, + required this.chargeCount, + required this.guards, + required this.guardCount, }); final bool isOwner; @@ -58,6 +66,10 @@ class UserInfoCard extends StatelessWidget { final int? silence; final ValueGetter headerControllerBuilder; final VoidCallback showLiveMedalWall; + final List? charges; + final Object? chargeCount; + final List? guards; + final Object? guardCount; @override Widget build(BuildContext context) { @@ -132,7 +144,22 @@ class UserInfoCard extends StatelessWidget { BuildContext context, ColorScheme colorScheme, bool isLight, + bool isPortrait, ) { + return [ + _buildName(context, colorScheme), + if (card.officialVerify?.desc?.isNotEmpty ?? false) + _buildVerify(colorScheme), + if (card.sign?.isNotEmpty ?? false) _buildSign(), + ?_buildChargeAndGuard(colorScheme, isPortrait), + if (card.followingsFollowedUpper?.items?.isNotEmpty ?? false) + _buildFollowedUp(colorScheme, card.followingsFollowedUpper!), + _buildExtraInfo(colorScheme), + if (silence == 1) _buildBanWidget(colorScheme, isLight), + ]; + } + + Widget _buildName(BuildContext context, ColorScheme colorScheme) { Widget? liveMedal; if (card.liveFansWearing?.detailV2 case final detailV2?) { Color? nameColor; @@ -161,212 +188,210 @@ class UserInfoCard extends StatelessWidget { } } } - 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!), + return Padding( + padding: const .only(left: 20, right: 20), + child: Wrap( + spacing: 8, + runSpacing: 8, + crossAxisAlignment: .center, + children: [ + GestureDetector( + onTap: () => Utils.copyText(card.name!), + child: Text( + card.name!, + strutStyle: const StrutStyle( + height: 1, + leading: 0, + fontSize: 17, + fontWeight: .bold, + ), + style: TextStyle( + height: 1, + fontSize: 17, + fontWeight: .bold, + color: (card.vip?.status ?? -1) > 0 && card.vip?.type == 2 + ? colorScheme.vipColor + : null, + ), + ), + ), + 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 .symmetric(horizontal: 8, vertical: 3), + decoration: BoxDecoration( + borderRadius: Style.mdRadius, + color: colorScheme.vipColor, + ), child: Text( - card.name!, + card.vip?.label?.text ?? '大会员', strutStyle: const StrutStyle( height: 1, leading: 0, - fontSize: 17, - fontWeight: FontWeight.bold, + fontSize: 10, + fontWeight: .bold, ), - style: TextStyle( + style: const TextStyle( height: 1, - fontSize: 17, - fontWeight: FontWeight.bold, - color: (card.vip?.status ?? -1) > 0 && card.vip?.type == 2 - ? colorScheme.vipColor - : null, + fontSize: 10, + color: Colors.white, + fontWeight: .bold, ), ), ), - Image.asset( - Utils.levelName( - card.levelInfo!.currentLevel!, - isSeniorMember: card.levelInfo?.identity == 2, + // if (card.nameplate?.imageSmall?.isNotEmpty ?? false) + // CachedNetworkImage( + // imageUrl: ImageUtils.thumbnailUrl(card.nameplate!.imageSmall!), + // height: 20, + // placeholder: (context, url) { + // return const SizedBox.shrink(); + // }, + // ), + ?liveMedal, + ], + ), + ); + } + + Widget _buildVerify(ColorScheme colorScheme) { + return Container( + margin: const .only(left: 20, top: 8, right: 20), + padding: const .symmetric(horizontal: 5, vertical: 2), + decoration: BoxDecoration( + borderRadius: const .all(.circular(12)), + color: colorScheme.onInverseSurface, + ), + child: Text.rich( + TextSpan( + children: [ + if (card.officialVerify?.spliceTitle?.isNotEmpty ?? false) ...[ + WidgetSpan( + alignment: .middle, + child: DecoratedBox( + decoration: BoxDecoration( + shape: .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: .bold, + color: colorScheme.onSurface.withValues(alpha: 0.7), ), - height: 11, - cacheHeight: 11.cacheSize(context), - semanticLabel: '等级${card.levelInfo?.currentLevel}', ), - if (card.vip?.status == 1) - Container( - padding: const .symmetric(horizontal: 8, vertical: 3), - decoration: BoxDecoration( - borderRadius: Style.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, - ), - ), - ), - // 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?.spliceTitle?.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), - ), - ), - ], + ); + } + + Widget _buildSign() { + return Padding( + padding: const .only(left: 20, top: 6, right: 20), + child: SelectableText( + card.sign!.trim().replaceAll(RegExp(r'\n{2,}'), '\n'), + style: const TextStyle(fontSize: 14), + ), + ); + } + + Widget _buildExtraInfo(ColorScheme colorScheme) { + return Padding( + padding: const .only(left: 20, top: 6, right: 20), + child: Wrap( + spacing: 10, + runSpacing: 8, + crossAxisAlignment: .center, + children: [ + GestureDetector( + onTap: () => Utils.copyText(card.mid.toString()), + child: Text( + 'UID: ${card.mid}', + style: TextStyle(fontSize: 12, color: colorScheme.outline), ), ), - ), - 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}', + ...?card.spaceTag?.map( + (item) { + final hasUri = item.uri?.isNotEmpty ?? false; + final child = Text( + item.title ?? '', style: TextStyle( fontSize: 12, - color: colorScheme.outline, + color: hasUri ? colorScheme.secondary : colorScheme.outline, ), + ); + if (hasUri) { + return GestureDetector( + onTap: () => PiliScheme.routePushFromUrl(item.uri!), + child: child, + ); + } + return child; + }, + ), + ], + ), + ); + } + + Widget _buildBanWidget(ColorScheme colorScheme, bool isLight) { + return Container( + width: .infinity, + decoration: BoxDecoration( + borderRadius: const .all(.circular(6)), + color: isLight ? colorScheme.errorContainer : colorScheme.error, + ), + margin: const .only(left: 20, top: 8, right: 20), + padding: const .symmetric(horizontal: 8, vertical: 4), + child: Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: .middle, + child: Icon( + Icons.info, + size: 17, + color: isLight + ? colorScheme.onErrorContainer + : colorScheme.onError, ), ), - ...?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, - ), - ); - if (hasUri) { - return GestureDetector( - onTap: () => PiliScheme.routePushFromUrl(item.uri!), - child: child, - ); - } - return child; - }, + TextSpan( + text: ' 该账号封禁中', + style: TextStyle( + color: isLight + ? colorScheme.onErrorContainer + : colorScheme.onError, + ), ), ], ), ), - 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, + mainAxisSize: .min, children: [ Row( children: UserInfoType.values @@ -392,7 +417,7 @@ class UserInfoCard extends StatelessWidget { const SizedBox(height: 5), Row( spacing: 10, - mainAxisSize: MainAxisSize.min, + mainAxisSize: .min, children: [ if (!isOwner) IconButton.outlined( @@ -417,9 +442,9 @@ class UserInfoCard extends StatelessWidget { width: 1.0, color: colorScheme.outline.withValues(alpha: 0.3), ), + padding: .zero, tapTargetSize: .padded, - padding: EdgeInsets.zero, - visualDensity: VisualDensity.compact, + visualDensity: .compact, ), ), Expanded( @@ -440,7 +465,7 @@ class UserInfoCard extends StatelessWidget { children: [ if (relation != 0 && relation != 128) ...[ WidgetSpan( - alignment: PlaceholderAlignment.middle, + alignment: .middle, child: Icon( Icons.sort, size: 16, @@ -512,8 +537,8 @@ class UserInfoCard extends StatelessWidget { ) { final imgUrls = images.collectionTopSimple?.top?.imgUrls; return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: .min, + crossAxisAlignment: .start, children: [ HeaderLayoutWidget( header: imgUrls != null && imgUrls.isNotEmpty @@ -533,8 +558,8 @@ class UserInfoCard extends StatelessWidget { actions: _buildRight(scheme), ), const SizedBox(height: 5), - ..._buildLeft(context, scheme, isLight), - if (card.prInfo?.content?.isNotEmpty == true) + ..._buildLeft(context, scheme, isLight, true), + if (card.prInfo?.content?.isNotEmpty ?? false) buildPrInfo(context, scheme, isLight, card.prInfo!), const SizedBox(height: 5), ], @@ -673,8 +698,8 @@ class UserInfoCard extends StatelessWidget { : null, colorBlendMode: filter ? isLight - ? BlendMode.lighten - : BlendMode.darken + ? .lighten + : .darken : null, fadeInDuration: const Duration(milliseconds: 120), fadeOutDuration: const Duration(milliseconds: 120), @@ -699,8 +724,8 @@ class UserInfoCard extends StatelessWidget { : null; Widget child = Container( - margin: const EdgeInsets.only(top: 8), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + margin: const .only(top: 8), + padding: const .symmetric(horizontal: 16, vertical: 10), color: Utils.parseColor(isLight ? prInfo.bgColor : prInfo.bgColorNight), child: Row( children: [ @@ -721,7 +746,7 @@ class UserInfoCard extends StatelessWidget { style: TextStyle(fontSize: 13, color: textColor), ), ), - if (prInfo.url?.isNotEmpty == true) ...[ + if (prInfo.url?.isNotEmpty ?? false) ...[ const SizedBox(width: 10), Icon( Icons.keyboard_arrow_right, @@ -731,7 +756,7 @@ class UserInfoCard extends StatelessWidget { ], ), ); - if (prInfo.url?.isNotEmpty == true) { + if (prInfo.url?.isNotEmpty ?? false) { return GestureDetector( onTap: () => PageUtils.handleWebview(prInfo.url!), child: child, @@ -742,8 +767,8 @@ class UserInfoCard extends StatelessWidget { Column _buildH(BuildContext context, ColorScheme scheme, bool isLight) => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: .min, + crossAxisAlignment: .start, children: [ // _buildHeader(context), const SizedBox(height: kToolbarHeight), @@ -751,7 +776,7 @@ class UserInfoCard extends StatelessWidget { children: [ const SizedBox(width: 20), Padding( - padding: EdgeInsets.only( + padding: .only( top: 10, bottom: card.prInfo?.content?.isNotEmpty == true ? 0 : 10, ), @@ -761,27 +786,110 @@ class UserInfoCard extends StatelessWidget { Expanded( flex: 5, child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: .min, + crossAxisAlignment: .start, children: [ const SizedBox(height: 10), - ..._buildLeft(context, scheme, isLight), + ..._buildLeft(context, scheme, isLight, false), const SizedBox(height: 5), ], ), ), - Expanded( - flex: 3, - child: _buildRight(scheme), - ), + Expanded(flex: 3, child: _buildRight(scheme)), const SizedBox(width: 20), ], ), - if (card.prInfo?.content?.isNotEmpty == true) + if (card.prInfo?.content?.isNotEmpty ?? false) buildPrInfo(context, scheme, isLight, card.prInfo!), ], ); + Widget _buildChargeItem( + ColorScheme colorScheme, + List? list, + Object? count, + String desc, + VoidCallback onTap, + ) { + return GestureDetector( + onTap: onTap, + child: Row( + mainAxisSize: .min, + children: [ + avatars( + gap: 10, + colorScheme: colorScheme, + users: list!.take(3), + ), + const SizedBox(width: 4), + Text.rich( + TextSpan( + children: [ + TextSpan( + text: NumUtils.numFormat(count), + style: TextStyle( + fontSize: 12, + color: colorScheme.onSurfaceVariant, + ), + ), + TextSpan( + text: desc, + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + ], + ), + ), + Icon( + Icons.keyboard_arrow_right, + size: 20, + color: colorScheme.outline, + ), + ], + ), + ); + } + + Widget? _buildChargeAndGuard(ColorScheme colorScheme, bool isPortrait) { + final children = [ + if (charges?.isNotEmpty ?? false) + _buildChargeItem( + colorScheme, + charges, + chargeCount, + '人为TA充电', + () => UpowerRankPage.toUpowerRank( + mid: card.mid!, + name: card.name!, + count: chargeCount, + ), + ), + if (guards?.isNotEmpty ?? false) + _buildChargeItem( + colorScheme, + guards, + guardCount, + '人加入大航海', + () => MemberGuard.toMemberGuard( + mid: card.mid!, + name: card.name!, + count: guardCount, + ), + ), + ]; + if (children.isNotEmpty) { + return Padding( + padding: const .only(left: 20, right: 20, top: 6), + child: isPortrait + ? Row(mainAxisAlignment: .spaceBetween, children: children) + : Wrap(spacing: 10, runSpacing: 6, children: children), + ); + } + return null; + } + Widget _buildFollowedUp( ColorScheme colorScheme, FollowingsFollowedUpper item, @@ -789,34 +897,39 @@ class UserInfoCard extends StatelessWidget { var list = item.items!; final flag = list.length > 3; if (flag) list = list.sublist(0, 3); - Widget child = Row( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(width: 20), - avatars(colorScheme: colorScheme, users: list), - const SizedBox(width: 4), - Flexible( - child: Text( - list.map((e) => e.name).join('、'), - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 13, - color: colorScheme.onSurfaceVariant, + Widget child = Padding( + padding: const .only(left: 20, top: 6, right: 20), + child: Row( + mainAxisSize: .min, + children: [ + avatars( + gap: 10, + colorScheme: colorScheme, + users: list, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + list.map((e) => e.name).join('、'), + maxLines: 1, + overflow: .ellipsis, + style: TextStyle( + fontSize: 13, + color: colorScheme.onSurfaceVariant, + ), ), ), - ), - Text( - '${flag ? '等${item.items!.length}人' : ''}也关注了TA ', - style: TextStyle(fontSize: 13, color: colorScheme.outline), - ), - Icon( - Icons.keyboard_arrow_right, - size: 20, - color: colorScheme.outline, - ), - const SizedBox(width: 10), - ], + Text( + '${flag ? '等${item.items!.length}人' : ''}也关注了TA', + style: TextStyle(fontSize: 13, color: colorScheme.outline), + ), + Icon( + Icons.keyboard_arrow_right, + size: 20, + color: colorScheme.outline, + ), + ], + ), ); return GestureDetector( onTap: () => FollowedPage.toFollowedPage(mid: card.mid, name: card.name), diff --git a/lib/pages/member_guard/controller.dart b/lib/pages/member_guard/controller.dart new file mode 100644 index 000000000..1abd90e72 --- /dev/null +++ b/lib/pages/member_guard/controller.dart @@ -0,0 +1,44 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/member.dart'; +import 'package:PiliPlus/models_new/member_guard/data.dart'; +import 'package:PiliPlus/models_new/member_guard/guard_top_list.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:get/get_core/src/get_main.dart'; +import 'package:get/get_navigation/get_navigation.dart'; + +class MemberGuardController + extends CommonListController { + @override + void onInit() { + super.onInit(); + queryData(); + } + + final Object ruid = Get.arguments['ruid']; + + late List tops; + + @override + List? getDataList(MemberGuardData response) { + return response.guardTopList; + } + + @override + bool customHandleResponse(bool isRefresh, Success response) { + if (response.response.hasMore != 1) { + isEnd = true; + } + if (isRefresh) { + final list = response.response.guardTopList; + tops = list.take(3).toList(); + if (list.length > 3) { + list.removeRange(0, 3); + } + } + return false; + } + + @override + Future> customGetData() => + MemberHttp.memberGuard(ruid: ruid, page: page); +} diff --git a/lib/pages/member_guard/view.dart b/lib/pages/member_guard/view.dart new file mode 100644 index 000000000..36855c8b2 --- /dev/null +++ b/lib/pages/member_guard/view.dart @@ -0,0 +1,206 @@ +import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; +import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models_new/member_guard/guard_top_list.dart'; +import 'package:PiliPlus/pages/member_guard/controller.dart'; +import 'package:PiliPlus/utils/extension/widget_ext.dart'; +import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:flutter/material.dart' hide ListTile; +import 'package:get/get.dart'; + +class MemberGuard extends StatefulWidget { + const MemberGuard({super.key}); + + @override + State createState() => _MemberGuardState(); + + static Future? toMemberGuard({ + required Object mid, + required String name, + required Object? count, + }) { + return Get.toNamed( + '/memberGuard', + arguments: { + 'ruid': mid, + 'name': name, + 'count': count, + }, + ); + } +} + +class _MemberGuardState extends State { + late final String _userName; + late final Object? _count; + late final MemberGuardController _controller; + + @override + void initState() { + super.initState(); + final args = Get.arguments; + _userName = args['name']; + _count = args['count']; + _controller = Get.put( + MemberGuardController(), + tag: args['ruid'].toString(), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('$_userName的舰队${_count == null ? '' : '($_count)'}'), + ), + body: refreshIndicator( + onRefresh: _controller.onRefresh, + child: CustomScrollView( + slivers: [ + ViewSliverSafeArea( + sliver: Obx(() => _buildBody(_controller.loadingState.value)), + ), + ], + ), + ).constraintWidth(), + ); + } + + Widget _buildBody(LoadingState?> state) { + switch (state) { + case Loading(): + return linearLoading; + case Success?>(:final response): + return SliverMainAxisGroup( + slivers: [ + _buildTopItems(), + if (response!.isNotEmpty) + SliverPadding( + padding: const .only(top: 10), + sliver: SliverList.separated( + itemCount: response.length, + itemBuilder: (context, index) { + if (index == response.length - 1) { + _controller.onLoadMore(); + } + + final item = response[index]; + return ListTile( + safeArea: false, + visualDensity: .comfortable, + onTap: () => Get.toNamed('/member?mid=${item.uid}'), + leading: _avatar(item.face, 32, item.guardLevel), + title: Text( + item.username, + style: const TextStyle(fontSize: 14), + ), + ); + }, + separatorBuilder: (_, _) => const SizedBox(height: 4), + ), + ), + ], + ); + case Error(:final errMsg): + return HttpError(errMsg: errMsg, onReload: _controller.onReload); + } + } + + Widget _buildTopItem(GuardItem item, double size) { + final child = GestureDetector( + behavior: .opaque, + onTap: () => Get.toNamed('/member?mid=${item.uid}'), + child: Padding( + padding: const .symmetric(vertical: 10.0), + child: Column( + spacing: 5, + mainAxisSize: .min, + children: [ + SizedBox( + height: 67.5, // 50 * 1.35 + child: Align( + alignment: .bottomCenter, + child: _avatar(item.face, size, item.guardLevel), + ), + ), + Text(item.username, maxLines: 1, overflow: .ellipsis), + ], + ), + ), + ); + if (PlatformUtils.isDesktop) { + return MouseRegion( + cursor: SystemMouseCursors.click, + child: child, + ); + } + return child; + } + + Widget _buildTopItems() { + final Widget first; + final Widget second; + final Widget third; + if (_controller.tops.firstOrNull case final item?) { + first = _buildTopItem(item, 50); + } else { + first = const SizedBox.shrink(); + } + if (_controller.tops.elementAtOrNull(1) case final item?) { + second = _buildTopItem(item, 42); + } else { + second = const SizedBox.shrink(); + } + if (_controller.tops.elementAtOrNull(2) case final item?) { + third = _buildTopItem(item, 42); + } else { + third = const SizedBox.shrink(); + } + return SliverToBoxAdapter( + child: Row( + children: [ + Expanded(child: second), + Expanded(child: first), + Expanded(child: third), + ], + ), + ); + } + + static String? _pendantUrl(int guardLevel) => switch (guardLevel) { + 1 => + 'https://i0.hdslb.com/bfs/live/a454275dea465ac15a03f121f0d7edaf96e30bcf.png', + 2 => + 'https://i0.hdslb.com/bfs/live/3b46129e796df42ec7356fcba77c8a79d47db682.png', + 3 => + 'https://i0.hdslb.com/bfs/live/80f732943cc3367029df65e267960d56736a82ee.png', + _ => null, + }; + + static Widget _avatar(String url, double size, int guardLevel) { + final pendentSize = 1.35 * size; + return Stack( + clipBehavior: .none, + alignment: .center, + children: [ + NetworkImgLayer( + src: url, + width: size, + height: size, + type: .avatar, + ), + NetworkImgLayer( + type: .emote, + width: pendentSize, + height: pendentSize, + src: _pendantUrl(guardLevel), + getPlaceHolder: () => const SizedBox.shrink(), + ), + ], + ); + } +} diff --git a/lib/pages/member_upower_rank/controller.dart b/lib/pages/member_upower_rank/controller.dart index 8e8ce1028..7aa6f9ac6 100644 --- a/lib/pages/member_upower_rank/controller.dart +++ b/lib/pages/member_upower_rank/controller.dart @@ -8,13 +8,15 @@ import 'package:get/get.dart'; class UpowerRankController extends CommonListController { - UpowerRankController({this.privilegeType, required this.upMid}); - int? privilegeType; + UpowerRankController({ + this.privilegeType, + required this.upMid, + }); final String upMid; - final Rx name = Rx(null); - final Rx?> tabs = Rx?>(null); - int? memberTotal; + final int? privilegeType; + + late final Rx?> tabs = Rx?>(null); @override void onInit() { @@ -25,11 +27,11 @@ class UpowerRankController @override List? getDataList(UpowerRankData response) { isEnd = true; - memberTotal = response.memberTotal ?? 0; - if (response.levelInfo != null && response.levelInfo!.length > 1) { + if (privilegeType == null && + response.levelInfo != null && + response.levelInfo!.length > 1) { tabs.value = response.levelInfo; } - name.value = response.upInfo!.nickname; return response.rankInfo; } diff --git a/lib/pages/member_upower_rank/view.dart b/lib/pages/member_upower_rank/view.dart index 191b5feec..a10f2e2fd 100644 --- a/lib/pages/member_upower_rank/view.dart +++ b/lib/pages/member_upower_rank/view.dart @@ -10,39 +10,58 @@ import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/upower_rank/rank_info.dart'; import 'package:PiliPlus/pages/member_upower_rank/controller.dart'; import 'package:PiliPlus/utils/extension/widget_ext.dart'; -import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart' hide ListTile; import 'package:get/get.dart'; class UpowerRankPage extends StatefulWidget { - const UpowerRankPage({super.key, this.upMid, this.tag, this.privilegeType}); + const UpowerRankPage({ + super.key, + this.privilegeType, + }); - final String? upMid; - final String? tag; final int? privilegeType; @override State createState() => _UpowerRankPageState(); + + static Future? toUpowerRank({ + required Object mid, + required String name, + required Object? count, + }) { + return Get.toNamed( + '/upowerRank', + arguments: { + 'mid': mid, + 'name': name, + 'count': count, + }, + ); + } } class _UpowerRankPageState extends State with AutomaticKeepAliveClientMixin { - late final _upMid = Get.parameters['mid']!; - late final String _tag; + String? _name; + Object? _count; + late final String _upMid; late final UpowerRankController _controller; @override void initState() { super.initState(); - _tag = widget.privilegeType == null - ? Utils.generateRandomString(8) - : '${widget.tag}${widget.privilegeType}'; + final params = Get.arguments; + _upMid = params['mid']!.toString(); + if (widget.privilegeType == null) { + _name = params['name']; + _count = params['count']; + } _controller = Get.put( UpowerRankController( privilegeType: widget.privilegeType, - upMid: widget.upMid ?? _upMid, + upMid: _upMid, ), - tag: _tag, + tag: '$_upMid${widget.privilegeType}', ); } @@ -70,14 +89,7 @@ class _UpowerRankPageState extends State return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( - title: Obx(() { - final name = _controller.name.value; - return name == null - ? const SizedBox.shrink() - : Text( - '$name 充电排行榜${_controller.memberTotal == 0 ? '' : '(${_controller.memberTotal})'}', - ); - }), + title: Text('$_name的充电排行榜${_count == null ? '' : '($_count)'}'), actions: [ TextButton( onPressed: () => Get.toNamed( @@ -126,7 +138,7 @@ class _UpowerRankPageState extends State } else { Get.find( tag: - '$_tag${tabs[index].privilegeType}', + '$_upMid${tabs[index].privilegeType}', ).animateToTop(); } } catch (_) {} @@ -141,8 +153,6 @@ class _UpowerRankPageState extends State .skip(1) .map( (e) => UpowerRankPage( - upMid: _upMid, - tag: _tag, privilegeType: e.privilegeType, ), ), @@ -170,7 +180,7 @@ class _UpowerRankPageState extends State ) { late final width = MediaQuery.textScalerOf(context).scale(32); return switch (loadingState) { - Loading() => linearLoading, + Loading() => const SliverFillRemaining(child: m3eLoading), Success?>(:final response) => response != null && response.isNotEmpty ? SliverList.builder( diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index 64df48521..9d3662d21 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -34,6 +34,7 @@ import 'package:PiliPlus/pages/main_reply/view.dart'; import 'package:PiliPlus/pages/match_info/view.dart'; import 'package:PiliPlus/pages/member/view.dart'; import 'package:PiliPlus/pages/member_dynamics/view.dart'; +import 'package:PiliPlus/pages/member_guard/view.dart'; import 'package:PiliPlus/pages/member_profile/view.dart'; import 'package:PiliPlus/pages/member_search/view.dart'; import 'package:PiliPlus/pages/member_upower_rank/view.dart'; @@ -196,5 +197,6 @@ class Routes { GetPage(name: '/myReply', page: () => const MyReply()), GetPage(name: '/videoWeb', page: () => const MemberVideoWeb()), GetPage(name: '/ssWeb', page: () => const MemberSSWeb()), + GetPage(name: '/memberGuard', page: () => const MemberGuard()), ]; }