diff --git a/lib/common/widgets/button/more_btn.dart b/lib/common/widgets/button/more_btn.dart new file mode 100644 index 00000000..2ec3a8c7 --- /dev/null +++ b/lib/common/widgets/button/more_btn.dart @@ -0,0 +1,34 @@ +import 'package:flutter/material.dart'; + +Widget moreTextButton({ + String text = '查看更多', + required VoidCallback onTap, + EdgeInsets? padding, + Color? color, +}) { + Widget child = Text.rich( + style: TextStyle(color: color, height: 1), + strutStyle: const StrutStyle(leading: 0, height: 1), + TextSpan( + children: [ + TextSpan(text: text), + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + size: 22, + color: color, + Icons.keyboard_arrow_right, + ), + ), + ], + ), + ); + if (padding != null) { + child = Padding(padding: padding, child: child); + } + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: onTap, + child: child, + ); +} diff --git a/lib/http/api.dart b/lib/http/api.dart index c501bf74..ac82d1ae 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -966,4 +966,8 @@ class Api { static const String danmakuRecall = '/x/dm/recall'; static const String danmakuEditState = '/x/v2/dm/edit/state'; + + static const String followedUp = '/x/relation/followings/followed_upper'; + + static const String sameFollowing = '/x/relation/same/followings'; } diff --git a/lib/http/fan.dart b/lib/http/fan.dart index 3df1a5d8..149661e2 100644 --- a/lib/http/fan.dart +++ b/lib/http/fan.dart @@ -1,10 +1,10 @@ import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/models_new/fans/data.dart'; +import 'package:PiliPlus/models_new/follow/data.dart'; class FanHttp { - static Future> fans({ + static Future> fans({ int? vmid, int? pn, int ps = 20, @@ -21,7 +21,7 @@ class FanHttp { }, ); if (res.data['code'] == 0) { - return Success(FansData.fromJson(res.data['data'])); + return Success(FollowData.fromJson(res.data['data'])); } else { return Error(res.data['message']); } diff --git a/lib/http/member.dart b/lib/http/member.dart index ec89c653..808770de 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -328,7 +328,9 @@ class MemberHttp { } } - static Future memberCardInfo({int? mid}) async { + static Future> memberCardInfo({ + int? mid, + }) async { var res = await Request().get( Api.memberCardInfo, queryParameters: { @@ -337,12 +339,9 @@ class MemberHttp { }, ); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': MemberCardInfoData.fromJson(res.data['data']), - }; + return Success(MemberCardInfoData.fromJson(res.data['data'])); } else { - return {'status': false, 'msg': res.data['message']}; + return Error(res.data['message']); } } diff --git a/lib/http/user.dart b/lib/http/user.dart index a30df31c..096f8889 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -5,6 +5,7 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/user/info.dart'; import 'package:PiliPlus/models/user/stat.dart'; import 'package:PiliPlus/models_new/coin_log/data.dart'; +import 'package:PiliPlus/models_new/follow/data.dart'; import 'package:PiliPlus/models_new/history/data.dart'; import 'package:PiliPlus/models_new/later/data.dart'; import 'package:PiliPlus/models_new/login_log/data.dart'; @@ -496,4 +497,48 @@ class UserHttp { return Error(res.data['message']); } } + + static Future> followedUp({ + required Object mid, + required int pn, + }) async { + final res = await Request().get( + Api.followedUp, + queryParameters: { + 'csrf': Accounts.main.csrf, + 'pn': pn, + 'vmid': mid, + 'web_location': 333.789, + 'x-bili-device-req-json': + '{"platform":"web","device":"pc","spmid":"333.789"}', + }, + ); + if (res.data['code'] == 0) { + return Success(FollowData.fromJson(res.data['data'])); + } else { + return Error(res.data['message']); + } + } + + static Future> sameFollowing({ + required Object mid, + int? pn, + }) async { + final res = await Request().get( + Api.sameFollowing, + queryParameters: { + 'csrf': Accounts.main.csrf, + 'pn': ?pn, + 'vmid': mid, + 'web_location': 333.789, + 'x-bili-device-req-json': + '{"platform":"web","device":"pc","spmid":"333.789"}', + }, + ); + if (res.data['code'] == 0) { + return Success(FollowData.fromJson(res.data['data'])); + } else { + return Error(res.data['message']); + } + } } diff --git a/lib/models_new/fans/data.dart b/lib/models_new/fans/data.dart deleted file mode 100644 index f4ab2a01..00000000 --- a/lib/models_new/fans/data.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:PiliPlus/models_new/fans/list.dart'; - -class FansData { - List? list; - String? offset; - int? reVersion; - int? total; - - FansData({this.list, this.offset, this.reVersion, this.total}); - - factory FansData.fromJson(Map json) => FansData( - list: (json['list'] as List?) - ?.map((e) => FansItemModel.fromJson(e as Map)) - .toList(), - offset: json['offset'] as String?, - reVersion: json['re_version'] as int?, - total: json['total'] as int?, - ); -} diff --git a/lib/models_new/fans/list.dart b/lib/models_new/fans/list.dart deleted file mode 100644 index 47e7fd05..00000000 --- a/lib/models_new/fans/list.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:PiliPlus/models/model_avatar.dart'; - -class FansItemModel { - int? mid; - int? attribute; - int? mtime; - dynamic tag; - int? special; - String? uname; - String? face; - String? sign; - int? faceNft; - BaseOfficialVerify? officialVerify; - Vip? vip; - String? nftIcon; - String? recReason; - String? trackId; - String? followTime; - - FansItemModel({ - this.mid, - this.attribute, - this.mtime, - this.tag, - this.special, - this.uname, - this.face, - this.sign, - this.faceNft, - this.officialVerify, - this.vip, - this.nftIcon, - this.recReason, - this.trackId, - this.followTime, - }); - - factory FansItemModel.fromJson(Map json) => FansItemModel( - mid: json['mid'] as int?, - attribute: json['attribute'] as int?, - mtime: json['mtime'] as int?, - tag: json['tag'] as dynamic, - special: json['special'] as int?, - uname: json['uname'] as String?, - face: json['face'] as String?, - sign: json['sign'] as String?, - faceNft: json['face_nft'] as int?, - officialVerify: json['official_verify'] == null - ? null - : BaseOfficialVerify.fromJson( - json['official_verify'] as Map, - ), - vip: json['vip'] == null - ? null - : Vip.fromJson(json['vip'] as Map), - nftIcon: json['nft_icon'] as String?, - recReason: json['rec_reason'] as String?, - trackId: json['track_id'] as String?, - followTime: json['follow_time'] as String?, - ); -} diff --git a/lib/models_new/follow/data.dart b/lib/models_new/follow/data.dart index 1a24c24b..7efe4ff4 100644 --- a/lib/models_new/follow/data.dart +++ b/lib/models_new/follow/data.dart @@ -2,10 +2,9 @@ import 'package:PiliPlus/models_new/follow/list.dart'; class FollowData { late List list; - int? reVersion; int? total; - FollowData({required this.list, this.reVersion, this.total}); + FollowData({required this.list, this.total}); factory FollowData.fromJson(Map json) => FollowData( list: @@ -13,7 +12,6 @@ class FollowData { ?.map((e) => FollowItemModel.fromJson(e as Map)) .toList() ?? [], - reVersion: json['re_version'] as int?, total: json['total'] as int?, ); } diff --git a/lib/models_new/follow/list.dart b/lib/models_new/follow/list.dart index 21011408..f9a53e13 100644 --- a/lib/models_new/follow/list.dart +++ b/lib/models_new/follow/list.dart @@ -7,12 +7,8 @@ class FollowItemModel extends UpItem { dynamic tag; int? special; String? sign; - int? faceNft; BaseOfficialVerify? officialVerify; Vip? vip; - String? nftIcon; - String? recReason; - String? trackId; String? followTime; FollowItemModel({ @@ -24,12 +20,8 @@ class FollowItemModel extends UpItem { super.uname, super.face, this.sign, - this.faceNft, this.officialVerify, this.vip, - this.nftIcon, - this.recReason, - this.trackId, this.followTime, }); @@ -43,7 +35,6 @@ class FollowItemModel extends UpItem { uname: json['uname'] as String?, face: json['face'] as String?, sign: json['sign'] as String?, - faceNft: json['face_nft'] as int?, officialVerify: json['official_verify'] == null ? null : BaseOfficialVerify.fromJson( @@ -52,9 +43,6 @@ class FollowItemModel extends UpItem { vip: json['vip'] == null ? null : Vip.fromJson(json['vip'] as Map), - nftIcon: json['nft_icon'] as String?, - recReason: json['rec_reason'] as String?, - trackId: json['track_id'] as String?, followTime: json['follow_time'] as String?, ); } diff --git a/lib/models_new/user_real_name/reject_page.dart b/lib/models_new/user_real_name/reject_page.dart index ed9a9efb..574171cf 100644 --- a/lib/models_new/user_real_name/reject_page.dart +++ b/lib/models_new/user_real_name/reject_page.dart @@ -1,19 +1,13 @@ class RejectPage { - String? title; - String? text; - String? img; + String? title; + String? text; + String? img; - RejectPage({this.title, this.text, this.img}); + RejectPage({this.title, this.text, this.img}); - factory RejectPage.fromJson(Map json) => RejectPage( - title: json['title'] as String?, - text: json['text'] as String?, - img: json['img'] as String?, - ); - - Map toJson() => { - 'title': title, - 'text': text, - 'img': img, - }; + factory RejectPage.fromJson(Map json) => RejectPage( + title: json['title'] as String?, + text: json['text'] as String?, + img: json['img'] as String?, + ); } diff --git a/lib/pages/contact/view.dart b/lib/pages/contact/view.dart index ff603819..11d630b8 100644 --- a/lib/pages/contact/view.dart +++ b/lib/pages/contact/view.dart @@ -72,7 +72,7 @@ class _ContactPageState extends State onSelect: widget.isFromSelect ? onSelect : null, ), FansPage( - mid: accountService.mid, + showName: false, onSelect: widget.isFromSelect ? onSelect : null, ), ], diff --git a/lib/pages/fan/controller.dart b/lib/pages/fan/controller.dart index ca997078..8c6d1a31 100644 --- a/lib/pages/fan/controller.dart +++ b/lib/pages/fan/controller.dart @@ -1,29 +1,35 @@ import 'package:PiliPlus/http/fan.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/video.dart'; -import 'package:PiliPlus/models_new/fans/data.dart'; -import 'package:PiliPlus/models_new/fans/list.dart'; -import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:PiliPlus/models_new/follow/data.dart'; +import 'package:PiliPlus/pages/follow_type/controller.dart'; +import 'package:PiliPlus/utils/accounts.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; -class FansController extends CommonListController { - FansController(this.mid); - int total = 0; - int mid; +class FansController extends FollowTypeController { + FansController(this.showName); + final bool showName; + late final bool isOwner; @override - void onInit() { - super.onInit(); + void init() { + final ownerMid = Accounts.main.mid; + final mid = Get.parameters['mid']; + this.mid = mid != null ? int.parse(mid) : ownerMid; + isOwner = ownerMid == this.mid; + if (showName && !isOwner) { + final name = Get.parameters['name']; + this.name = RxnString(name); + if (name == null) { + queryUserName(); + } + } queryData(); } @override - List? getDataList(FansData response) { - return response.list; - } - - @override - Future> customGetData() => FanHttp.fans( + Future> customGetData() => FanHttp.fans( vmid: mid, pn: page, orderType: 'attention', diff --git a/lib/pages/fan/view.dart b/lib/pages/fan/view.dart index eb387957..2a598ff0 100644 --- a/lib/pages/fan/view.dart +++ b/lib/pages/fan/view.dart @@ -1,16 +1,9 @@ -import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.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/refresh_indicator.dart'; -import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; -import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/models/common/image_type.dart'; -import 'package:PiliPlus/models_new/fans/list.dart'; +import 'package:PiliPlus/models_new/follow/list.dart'; import 'package:PiliPlus/pages/fan/controller.dart'; +import 'package:PiliPlus/pages/follow_type/view.dart'; +import 'package:PiliPlus/pages/follow_type/widgets/item.dart'; import 'package:PiliPlus/pages/share/view.dart' show UserModel; -import 'package:PiliPlus/services/account_service.dart'; -import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -18,160 +11,63 @@ import 'package:get/get.dart'; class FansPage extends StatefulWidget { const FansPage({ super.key, - this.mid, + this.showName, this.onSelect, }); - final int? mid; + final bool? showName; final ValueChanged? onSelect; @override State createState() => _FansPageState(); } -class _FansPageState extends State { - late int mid; - String? name; - late bool isOwner; - late FansController _fansController; - +class _FansPageState extends FollowTypePageState { @override - void initState() { - super.initState(); - AccountService accountService = Get.find(); - late final mid = Get.parameters['mid']; - this.mid = - widget.mid ?? (mid != null ? int.parse(mid) : accountService.mid); - isOwner = this.mid == accountService.mid; - name = Get.parameters['name'] ?? accountService.name.value; - _fansController = Get.put( - FansController(this.mid), - tag: Utils.makeHeroTag(this.mid), - ); - } - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context).colorScheme; - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: widget.mid != null - ? null - : AppBar(title: Text(isOwner ? '我的粉丝' : '$name的粉丝')), - body: refreshIndicator( - onRefresh: _fansController.onRefresh, - child: CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - controller: _fansController.scrollController, - slivers: [ - ViewSliverSafeArea( - sliver: Obx( - () => _buildBody(theme, _fansController.loadingState.value), - ), - ), - ], - ), - ), - ); - } - - late final gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: Grid.smallCardWidth * 2, - mainAxisExtent: 66, + late final FansController controller = Get.put( + FansController(widget.showName ?? true), + tag: Utils.generateRandomString(8), ); + late final flag = widget.onSelect == null && controller.isOwner; - Widget _buildBody( - ColorScheme theme, - LoadingState?> loadingState, - ) { - return switch (loadingState) { - Loading() => SliverGrid.builder( - gridDelegate: gridDelegate, - itemBuilder: (context, index) => const MsgFeedTopSkeleton(), - itemCount: 16, - ), - Success(:var response) => - response?.isNotEmpty == true - ? SliverGrid.builder( - gridDelegate: gridDelegate, - itemBuilder: (context, index) { - if (index == response.length - 1) { - _fansController.onLoadMore(); - } - return _buildItem(theme, index, response[index]); - }, - itemCount: response!.length, - ) - : HttpError(onReload: _fansController.onReload), - Error(:var errMsg) => HttpError( - errMsg: errMsg, - onReload: _fansController.onReload, - ), - }; - } + @override + PreferredSizeWidget? get appBar => widget.showName == false + ? null + : AppBar( + title: controller.isOwner + ? const Text('我的粉丝') + : Obx(() { + final name = controller.name.value; + if (name != null) return Text('$name的粉丝'); + return const SizedBox.shrink(); + }), + ); - Widget _buildItem(ColorScheme theme, int index, FansItemModel item) { - final isSelect = widget.onSelect != null; + @override + Widget buildItem(int index, FollowItemModel item) { void onRemove() => showConfirmDialog( context: context, title: '确定移除 ${item.uname} ?', - onConfirm: () => _fansController.onRemoveFan(index, item.mid!), + onConfirm: () => controller.onRemoveFan(index, item.mid), ); - final flag = !isSelect && isOwner; - return SizedBox( - height: 66, - child: InkWell( - onTap: () { - if (widget.onSelect != null) { - widget.onSelect!( - UserModel( - mid: item.mid!, - name: item.uname!, - avatar: item.face!, - ), - ); - return; - } - Get.toNamed('/member?mid=${item.mid}'); - }, - onLongPress: flag ? onRemove : null, - onSecondaryTap: flag && !Utils.isMobile ? onRemove : null, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), - child: Row( - spacing: 10, - children: [ - NetworkImgLayer( - width: 45, - height: 45, - type: ImageType.avatar, - src: item.face, - ), - Column( - spacing: 3, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - item.uname!, - style: const TextStyle(fontSize: 14), - ), - if (item.sign != null) - Text( - item.sign!, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle(fontSize: 13, color: theme.outline), - ), - ], - ), - ], - ), - ), - ), + return FollowTypeItem( + item: item, + onTap: () { + if (widget.onSelect != null) { + widget.onSelect!( + UserModel( + mid: item.mid, + name: item.uname!, + avatar: item.face!, + ), + ); + return; + } + Get.toNamed('/member?mid=${item.mid}'); + }, + onLongPress: flag ? onRemove : null, + onSecondaryTap: flag && !Utils.isMobile ? onRemove : null, ); } } diff --git a/lib/pages/follow/child/child_controller.dart b/lib/pages/follow/child/child_controller.dart index a07328c6..6afc7f4d 100644 --- a/lib/pages/follow/child/child_controller.dart +++ b/lib/pages/follow/child/child_controller.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/http/follow.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/member.dart'; +import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/models/common/follow_order_type.dart'; import 'package:PiliPlus/models_new/follow/data.dart'; import 'package:PiliPlus/models_new/follow/list.dart'; @@ -14,6 +15,11 @@ class FollowChildController final FollowController? controller; final int? tagid; final int mid; + int? total; + + late final loadSameFollow = controller?.isOwner == false; + late final Rx?>> sameState = + LoadingState?>.loading().obs; late final Rx orderType = FollowOrderType.def.obs; @@ -21,13 +27,24 @@ class FollowChildController void onInit() { super.onInit(); queryData(); + if (loadSameFollow) { + _loadSameFollow(); + } } @override List? getDataList(FollowData response) { + total = response.total; return response.list; } + @override + void checkIsEnd(int length) { + if (total != null && length >= total!) { + isEnd = true; + } + } + @override bool customHandleResponse(bool isRefresh, Success response) { if (controller != null) { @@ -57,4 +74,11 @@ class FollowChildController orderType: orderType.value.type, ); } + + Future _loadSameFollow() async { + final res = await UserHttp.sameFollowing(mid: mid); + if (res.isSuccess) { + sameState.value = Success(res.data.list); + } + } } diff --git a/lib/pages/follow/child/child_view.dart b/lib/pages/follow/child/child_view.dart index af54172d..3008f63f 100644 --- a/lib/pages/follow/child/child_view.dart +++ b/lib/pages/follow/child/child_view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; +import 'package:PiliPlus/common/widgets/button/more_btn.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -42,7 +43,24 @@ class _FollowChildPageState extends State @override Widget build(BuildContext context) { super.build(context); + final colorScheme = ColorScheme.of(context); final padding = MediaQuery.viewPaddingOf(context); + Widget sliver = Obx( + () => _buildBody(_followController.loadingState.value), + ); + if (_followController.loadSameFollow) { + sliver = SliverMainAxisGroup( + slivers: [ + Obx( + () => _buildSameFollowing( + colorScheme, + _followController.sameState.value, + ), + ), + sliver, + ], + ); + } Widget child = refreshIndicator( onRefresh: _followController.onRefresh, child: CustomScrollView( @@ -55,9 +73,7 @@ class _FollowChildPageState extends State right: padding.right, bottom: padding.bottom + 100, ), - sliver: Obx( - () => _buildBody(_followController.loadingState.value), - ), + sliver: sliver, ), ], ), @@ -122,6 +138,68 @@ class _FollowChildPageState extends State }; } + Widget _buildSameFollowing( + ColorScheme colorScheme, + LoadingState?> state, + ) { + return switch (state) { + Success(:var response) => + response?.isNotEmpty == true + ? SliverMainAxisGroup( + slivers: [ + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only( + left: 16, + right: 16, + bottom: 6, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '我们的共同关注', + style: TextStyle( + color: colorScheme.onSurfaceVariant, + ), + ), + moreTextButton( + onTap: () => Get.toNamed( + '/sameFollowing?mid=${_followController.mid}&name=${widget.controller?.name.value}', + ), + color: colorScheme.outline, + ), + ], + ), + ), + ), + SliverList.builder( + itemCount: response!.length, + itemBuilder: (_, index) => + FollowItem(item: response[index]), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only( + left: 16, + top: 16, + bottom: 6, + ), + child: Text( + '全部关注', + style: TextStyle( + color: colorScheme.onSurfaceVariant, + ), + ), + ), + ), + ], + ) + : const SliverToBoxAdapter(), + _ => const SliverToBoxAdapter(), + }; + } + @override bool get wantKeepAlive => widget.onSelect != null || widget.controller?.tabController != null; diff --git a/lib/pages/follow/controller.dart b/lib/pages/follow/controller.dart index fc310db8..504d7b54 100644 --- a/lib/pages/follow/controller.dart +++ b/lib/pages/follow/controller.dart @@ -1,16 +1,15 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/member.dart'; import 'package:PiliPlus/models/member/tags.dart'; -import 'package:PiliPlus/services/account_service.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; class FollowController extends GetxController with GetTickerProviderStateMixin { - late int mid; - String? name; - late bool isOwner; + late final int mid; + late final RxnString name; + late final bool isOwner; late final Rx followState = LoadingState.loading().obs; late final RxList tabs = [].obs; @@ -19,16 +18,26 @@ class FollowController extends GetxController with GetTickerProviderStateMixin { @override void onInit() { super.onInit(); - int ownerMid = Accounts.main.mid; + final ownerMid = Accounts.main.mid; final mid = Get.parameters['mid']; this.mid = mid != null ? int.parse(mid) : ownerMid; isOwner = ownerMid == this.mid; - name = Get.parameters['name'] ?? Get.find().name.value; if (isOwner) { queryFollowUpTags(); + } else { + final name = Get.parameters['name']; + this.name = RxnString(name); + if (name == null) { + _queryUserName(); + } } } + Future _queryUserName() async { + final res = await MemberHttp.memberCardInfo(mid: mid); + name.value = res.dataOrNull?.card?.name; + } + Future queryFollowUpTags() async { var res = await MemberHttp.followUpTags(); if (res.isSuccess) { diff --git a/lib/pages/follow/view.dart b/lib/pages/follow/view.dart index b1a2271e..4e606142 100644 --- a/lib/pages/follow/view.dart +++ b/lib/pages/follow/view.dart @@ -31,9 +31,13 @@ class _FollowPageState extends State { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( - title: Text( - _followController.isOwner ? '我的关注' : '${_followController.name}的关注', - ), + title: _followController.isOwner + ? const Text('我的关注') + : Obx(() { + final name = _followController.name.value; + if (name != null) return Text('$name的关注'); + return const SizedBox.shrink(); + }), actions: _followController.isOwner ? [ IconButton( diff --git a/lib/pages/follow_type/controller.dart b/lib/pages/follow_type/controller.dart new file mode 100644 index 00000000..cc781144 --- /dev/null +++ b/lib/pages/follow_type/controller.dart @@ -0,0 +1,50 @@ +import 'package:PiliPlus/http/member.dart'; +import 'package:PiliPlus/models_new/follow/data.dart'; +import 'package:PiliPlus/models_new/follow/list.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:PiliPlus/utils/accounts.dart'; +import 'package:get/get.dart'; + +abstract class FollowTypeController + extends CommonListController { + late final int mid; + late final RxnString name; + + RxInt total = 0.obs; + + @override + void onInit() { + super.onInit(); + init(); + } + + void init() { + final ownerMid = Accounts.main.mid; + final mid = Get.parameters['mid']; + this.mid = mid != null ? int.parse(mid) : ownerMid; + final name = Get.parameters['name']; + this.name = RxnString(name); + if (name == null) { + queryUserName(); + } + queryData(); + } + + Future queryUserName() async { + final res = await MemberHttp.memberCardInfo(mid: mid); + name.value = res.dataOrNull?.card?.name; + } + + @override + List? getDataList(FollowData response) { + total.value = response.total ?? 0; + return response.list; + } + + @override + void checkIsEnd(int length) { + if (length >= total.value) { + isEnd = true; + } + } +} diff --git a/lib/pages/follow_type/follow_same/controller.dart b/lib/pages/follow_type/follow_same/controller.dart new file mode 100644 index 00000000..cb326f06 --- /dev/null +++ b/lib/pages/follow_type/follow_same/controller.dart @@ -0,0 +1,10 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/user.dart'; +import 'package:PiliPlus/models_new/follow/data.dart'; +import 'package:PiliPlus/pages/follow_type/controller.dart'; + +class FollowSameController extends FollowTypeController { + @override + Future> customGetData() => + UserHttp.sameFollowing(mid: mid, pn: page); +} diff --git a/lib/pages/follow_type/follow_same/view.dart b/lib/pages/follow_type/follow_same/view.dart new file mode 100644 index 00000000..4a296509 --- /dev/null +++ b/lib/pages/follow_type/follow_same/view.dart @@ -0,0 +1,30 @@ +import 'package:PiliPlus/pages/follow_type/follow_same/controller.dart'; +import 'package:PiliPlus/pages/follow_type/view.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class FollowSamePage extends StatefulWidget { + const FollowSamePage({super.key}); + + @override + State createState() => _FollowSamePageState(); +} + +class _FollowSamePageState extends FollowTypePageState { + @override + final controller = Get.put( + FollowSameController(), + tag: Utils.generateRandomString(8), + ); + + @override + PreferredSizeWidget get appBar => AppBar( + title: Obx( + () { + final name = controller.name.value; + return Text('${name == null ? '' : '我与$name的'}共同关注'); + }, + ), + ); +} diff --git a/lib/pages/follow_type/followed/controller.dart b/lib/pages/follow_type/followed/controller.dart new file mode 100644 index 00000000..412375a7 --- /dev/null +++ b/lib/pages/follow_type/followed/controller.dart @@ -0,0 +1,10 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/user.dart'; +import 'package:PiliPlus/models_new/follow/data.dart'; +import 'package:PiliPlus/pages/follow_type/controller.dart'; + +class FollowedController extends FollowTypeController { + @override + Future> customGetData() => + UserHttp.followedUp(mid: mid, pn: page); +} diff --git a/lib/pages/follow_type/followed/view.dart b/lib/pages/follow_type/followed/view.dart new file mode 100644 index 00000000..88853ace --- /dev/null +++ b/lib/pages/follow_type/followed/view.dart @@ -0,0 +1,29 @@ +import 'package:PiliPlus/pages/follow_type/followed/controller.dart'; +import 'package:PiliPlus/pages/follow_type/view.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class FollowedPage extends StatefulWidget { + const FollowedPage({super.key}); + + @override + State createState() => _FollowedPageState(); +} + +class _FollowedPageState extends FollowTypePageState { + @override + final controller = Get.put( + FollowedController(), + tag: Utils.generateRandomString(8), + ); + + @override + PreferredSizeWidget get appBar => AppBar( + title: Obx( + () => Text( + '我关注的${controller.total.value}人也关注了${controller.name.value ?? 'TA'}', + ), + ), + ); +} diff --git a/lib/pages/follow_type/view.dart b/lib/pages/follow_type/view.dart new file mode 100644 index 00000000..f42f9592 --- /dev/null +++ b/lib/pages/follow_type/view.dart @@ -0,0 +1,77 @@ +import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/view_sliver_safe_area.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models_new/follow/list.dart'; +import 'package:PiliPlus/pages/follow/widgets/follow_item.dart'; +import 'package:PiliPlus/pages/follow_type/controller.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +abstract class FollowTypePageState extends State { + FollowTypeController get controller; + + PreferredSizeWidget? get appBar; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context).colorScheme; + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: appBar, + body: refreshIndicator( + onRefresh: controller.onRefresh, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + controller: controller.scrollController, + slivers: [ + ViewSliverSafeArea( + sliver: Obx( + () => _buildBody(theme, controller.loadingState.value), + ), + ), + ], + ), + ), + ); + } + + late final gridDelegate = SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: Grid.smallCardWidth * 2, + mainAxisExtent: 66, + ); + + Widget _buildBody( + ColorScheme theme, + LoadingState?> loadingState, + ) { + return switch (loadingState) { + Loading() => SliverGrid.builder( + gridDelegate: gridDelegate, + itemBuilder: (context, index) => const MsgFeedTopSkeleton(), + itemCount: 16, + ), + Success(:var response) => + response?.isNotEmpty == true + ? SliverGrid.builder( + gridDelegate: gridDelegate, + itemBuilder: (context, index) { + if (index == response.length - 1) { + controller.onLoadMore(); + } + return buildItem(index, response[index]); + }, + itemCount: response!.length, + ) + : HttpError(onReload: controller.onReload), + Error(:var errMsg) => HttpError( + errMsg: errMsg, + onReload: controller.onReload, + ), + }; + } + + Widget buildItem(int index, FollowItemModel item) => FollowItem(item: item); +} diff --git a/lib/pages/follow_type/widgets/item.dart b/lib/pages/follow_type/widgets/item.dart new file mode 100644 index 00000000..8e081584 --- /dev/null +++ b/lib/pages/follow_type/widgets/item.dart @@ -0,0 +1,71 @@ +import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/models/common/image_type.dart'; +import 'package:PiliPlus/models_new/follow/list.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class FollowTypeItem extends StatelessWidget { + const FollowTypeItem({ + super.key, + required this.item, + this.onTap, + this.onLongPress, + this.onSecondaryTap, + }); + + final FollowItemModel item; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + final VoidCallback? onSecondaryTap; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 66, + child: InkWell( + onTap: onTap ?? () => Get.toNamed('/member?mid=${item.mid}'), + onLongPress: onLongPress, + onSecondaryTap: onSecondaryTap, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + child: Row( + spacing: 10, + children: [ + NetworkImgLayer( + width: 45, + height: 45, + type: ImageType.avatar, + src: item.face, + ), + Expanded( + child: Column( + spacing: 3, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + item.uname!, + style: const TextStyle(fontSize: 14), + ), + if (item.sign case final sign?) + Text( + sign, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: 13, + color: ColorScheme.of(context).outline, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index b21e20ce..79eac62c 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/video_card_v.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; +import 'package:PiliPlus/common/widgets/button/more_btn.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/pair.dart'; @@ -251,25 +252,9 @@ class _LivePageState extends CommonPageState ), ), const Spacer(), - GestureDetector( - behavior: HitTestBehavior.opaque, + moreTextButton( onTap: () => Get.to(const LiveFollowPage()), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '查看更多', - style: TextStyle( - color: theme.colorScheme.outline, - ), - ), - Icon( - size: 20, - Icons.keyboard_arrow_right_outlined, - color: theme.colorScheme.outline, - ), - ], - ), + color: theme.colorScheme.outline, ), ], ), diff --git a/lib/pages/member/widget/user_info_card.dart b/lib/pages/member/widget/user_info_card.dart index b4f5454b..adb2324f 100644 --- a/lib/pages/member/widget/user_info_card.dart +++ b/lib/pages/member/widget/user_info_card.dart @@ -652,17 +652,9 @@ class UserInfoCard extends StatelessWidget { const SizedBox(width: 10), ], ); - if (item.jumpUrl?.isNotEmpty == true) { - return GestureDetector( - onTap: () { - final isDark = Get.isDarkMode; - PageUtils.handleWebview( - '${item.jumpUrl}&native.theme=${isDark ? 2 : 1}&night=${isDark ? 1 : 0}', - ); - }, - child: child, - ); - } - return child; + return GestureDetector( + onTap: () => Get.toNamed('/followed?mid=${card.mid}&name=${card.name}'), + child: child, + ); } } diff --git a/lib/pages/member_home/view.dart b/lib/pages/member_home/view.dart index 2e188f60..402addfc 100644 --- a/lib/pages/member_home/view.dart +++ b/lib/pages/member_home/view.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/button/more_btn.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space/data.dart'; @@ -301,7 +302,7 @@ class _MemberHomeState extends State ], ), ), - GestureDetector( + moreTextButton( onTap: () { int index = _ctr.tab2!.indexWhere( (item) => item.param == param, @@ -361,25 +362,7 @@ class _MemberHomeState extends State SmartDialog.showToast('view $param'); } }, - child: Text.rich( - TextSpan( - children: [ - TextSpan( - text: '查看更多', - style: TextStyle(color: color), - ), - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Icon( - Icons.arrow_forward_ios, - size: 14, - color: color, - ), - style: TextStyle(fontSize: 13, color: color), - ), - ], - ), - ), + color: color, ), ], ), diff --git a/lib/pages/pgc/view.dart b/lib/pages/pgc/view.dart index 4f8946eb..0aa321af 100644 --- a/lib/pages/pgc/view.dart +++ b/lib/pages/pgc/view.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/button/more_btn.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/refresh_indicator.dart'; @@ -231,8 +232,8 @@ class _PgcPageState extends CommonPageState '推荐', style: theme.textTheme.titleMedium, ), - GestureDetector( - behavior: HitTestBehavior.opaque, + moreTextButton( + padding: const EdgeInsets.symmetric(vertical: 2), onTap: () { if (widget.tabType == HomeTabType.bangumi) { Get.to(const PgcIndexPage()); @@ -291,26 +292,7 @@ class _PgcPageState extends CommonPageState ); } }, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '查看更多', - strutStyle: const StrutStyle(leading: 0, height: 1), - style: TextStyle( - height: 1, - color: theme.colorScheme.secondary, - ), - ), - Icon( - Icons.chevron_right, - color: theme.colorScheme.secondary, - ), - ], - ), - ), + color: theme.colorScheme.secondary, ), ], ), @@ -394,34 +376,16 @@ class _PgcPageState extends CommonPageState () => controller.accountService.isLogin.value ? Padding( padding: const EdgeInsets.symmetric(horizontal: 10), - child: GestureDetector( - behavior: HitTestBehavior.opaque, + child: moreTextButton( + text: '查看全部', onTap: () => Get.toNamed( '/fav', arguments: widget.tabType == HomeTabType.bangumi ? FavTabType.bangumi.index : FavTabType.cinema.index, ), - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - '查看全部', - strutStyle: const StrutStyle(leading: 0, height: 1), - style: TextStyle( - height: 1, - color: theme.colorScheme.secondary, - ), - ), - Icon( - Icons.chevron_right, - color: theme.colorScheme.secondary, - ), - ], - ), - ), + padding: const EdgeInsets.symmetric(vertical: 8), + color: theme.colorScheme.secondary, ), ) : const SizedBox.shrink(), diff --git a/lib/pages/video/introduction/ugc/controller.dart b/lib/pages/video/introduction/ugc/controller.dart index 2325d41a..95e9b614 100644 --- a/lib/pages/video/introduction/ugc/controller.dart +++ b/lib/pages/video/introduction/ugc/controller.dart @@ -157,8 +157,8 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { return; } var result = await MemberHttp.memberCardInfo(mid: mid); - if (result['status']) { - userStat.value = result['data']; + if (result.isSuccess) { + userStat.value = result.data; } } } diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index fc1920d4..a9451a5c 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -16,6 +16,8 @@ import 'package:PiliPlus/pages/fav_detail/view.dart'; import 'package:PiliPlus/pages/fav_search/view.dart'; import 'package:PiliPlus/pages/follow/view.dart'; import 'package:PiliPlus/pages/follow_search/view.dart'; +import 'package:PiliPlus/pages/follow_type/follow_same/view.dart'; +import 'package:PiliPlus/pages/follow_type/followed/view.dart'; import 'package:PiliPlus/pages/history/view.dart'; import 'package:PiliPlus/pages/history_search/view.dart'; import 'package:PiliPlus/pages/home/view.dart'; @@ -223,6 +225,8 @@ class Routes { ), CustomGetPage(name: '/audio', page: () => const AudioPage()), CustomGetPage(name: '/mainReply', page: () => const MainReplyPage()), + CustomGetPage(name: '/followed', page: () => const FollowedPage()), + CustomGetPage(name: '/sameFollowing', page: () => const FollowSamePage()), ]; } diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index 3f51f5a9..01a53189 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -633,16 +633,53 @@ abstract class PiliScheme { launchURL(); return false; } else if (host.contains('space.bilibili.com')) { - String? sid = - uri.queryParameters['sid'] ?? + void toType({ + required String mid, + required String? type, + }) { + switch (type) { + case 'follow': + Get.toNamed('/follow?mid=$mid'); + break; + case 'fans': + Get.toNamed('/fan?mid=$mid'); + break; + case 'followed': + Get.toNamed('/followed?mid=$mid'); + break; + default: + PageUtils.toDupNamed('/member?mid=$mid', off: off); + } + } + + late final queryParameters = uri.queryParameters; + + // space.bilibili.com/h5/follow?mid={{mid}}&type={{type}} + if (path.startsWith('/h5/follow')) { + final mid = queryParameters['mid']; + final type = queryParameters['type']; + if (mid != null) { + toType(mid: mid, type: type); + return true; + } + } + + // space.bilibili.com/{{uid}}/lists/{{season_id}} + // space.bilibili.com/{{uid}}/lists?sid={{season_id}} + // space.bilibili.com/{{uid}}/channel/collectiondetail?sid={{season_id}} + final sid = + queryParameters['sid'] ?? RegExp(r'lists/(\d+)').firstMatch(path)?.group(1); if (sid != null) { SubDetailPage.toSubDetailPage(int.parse(sid)); return true; } - String? mid = uriDigitRegExp.firstMatch(path)?.group(1); + + // space.bilibili.com/{{mid}}/relation/{{type}} + final mid = uriDigitRegExp.firstMatch(path)?.group(1); + final type = RegExp(r'relation/([a-z]+)').firstMatch(path)?.group(1); if (mid != null) { - PageUtils.toDupNamed('/member?mid=$mid', off: off); + toType(mid: mid, type: type); return true; } launchURL();