diff --git a/lib/http/follow.dart b/lib/http/follow.dart index 2394ef478..49b9adb35 100644 --- a/lib/http/follow.dart +++ b/lib/http/follow.dart @@ -1,3 +1,5 @@ +import 'package:PiliPlus/http/loading_state.dart'; + import '../models/follow/result.dart'; import 'index.dart'; @@ -20,4 +22,22 @@ class FollowHttp { return {'status': false, 'msg': res.data['message']}; } } + + static Future?>> followingsNew( + {int? vmid, int? pn, int? ps, String? orderType}) async { + var res = await Request().get(Api.followings, queryParameters: { + 'vmid': vmid, + 'pn': pn, + 'ps': ps, + 'order': 'desc', + 'order_type': orderType, + }); + + if (res.data['code'] == 0) { + return LoadingState.success( + FollowDataModel.fromJson(res.data['data']).list); + } else { + return LoadingState.error(res.data['message']); + } + } } diff --git a/lib/http/member.dart b/lib/http/member.dart index 55c727a86..cfcaff559 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -470,8 +470,8 @@ class MemberHttp { if (res.data['code'] == 0) { return { 'status': true, - 'data': res.data['data'] - .map((e) => MemberTagItemModel.fromJson(e)) + 'data': (res.data['data'] as List?) + ?.map((e) => MemberTagItemModel.fromJson(e)) .toList() }; } else { @@ -525,28 +525,27 @@ class MemberHttp { } // 获取某分组下的up - static Future followUpGroup( + static Future?>> followUpGroup( int? mid, int? tagid, int? pn, int? ps, ) async { - var res = await Request().get(Api.followUpGroup, queryParameters: { - 'mid': mid, - 'tagid': tagid, - 'pn': pn, - 'ps': ps, - }); + var res = await Request().get( + Api.followUpGroup, + queryParameters: { + 'mid': mid, + 'tagid': tagid, + 'pn': pn, + 'ps': ps, + }, + ); if (res.data['code'] == 0) { - // FollowItemModel - return { - 'status': true, - 'data': res.data['data'] - .map((e) => FollowItemModel.fromJson(e)) - .toList() - }; + return LoadingState.success((res.data['data'] as List?) + ?.map((e) => FollowItemModel.fromJson(e)) + .toList()); } else { - return {'status': false, 'msg': res.data['message']}; + return LoadingState.error(res.data['message']); } } diff --git a/lib/pages/follow/child_controller.dart b/lib/pages/follow/child_controller.dart new file mode 100644 index 000000000..4c9aea72d --- /dev/null +++ b/lib/pages/follow/child_controller.dart @@ -0,0 +1,28 @@ +import 'package:PiliPlus/http/follow.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/member.dart'; +import 'package:PiliPlus/models/follow/result.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; + +class FollowChildController + extends CommonListController?, FollowItemModel> { + FollowChildController(this.mid, this.tagid); + final int? tagid; + final int mid; + + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + Future?>> customGetData() { + if (tagid != null) { + return MemberHttp.followUpGroup(mid, tagid, currentPage, 20); + } + + return FollowHttp.followingsNew( + vmid: mid, pn: currentPage, ps: 20, orderType: 'attention'); + } +} diff --git a/lib/pages/follow/child_view.dart b/lib/pages/follow/child_view.dart new file mode 100644 index 000000000..56e91976e --- /dev/null +++ b/lib/pages/follow/child_view.dart @@ -0,0 +1,90 @@ +import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; +import 'package:PiliPlus/common/widgets/http_error.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/follow/result.dart'; +import 'package:PiliPlus/pages/follow/child_controller.dart'; +import 'package:PiliPlus/pages/follow/widgets/follow_item.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class FollowChildPage extends StatefulWidget { + const FollowChildPage({super.key, required this.mid, this.tagid}); + + final int mid; + final int? tagid; + + @override + State createState() => _FollowChildPageState(); +} + +class _FollowChildPageState extends State + with AutomaticKeepAliveClientMixin { + late final _followController = Get.put( + FollowChildController(widget.mid, widget.tagid), + tag: Utils.generateRandomString(8)); + late final _isOwner = widget.tagid != null; + + @override + Widget build(BuildContext context) { + super.build(context); + return refreshIndicator( + onRefresh: () async { + await _followController.onRefresh(); + }, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom + 80), + sliver: Obx(() => _buildBody(_followController.loadingState.value)), + ), + ], + ), + ); + } + + Widget _buildBody(LoadingState?> loadingState) { + return switch (loadingState) { + Loading() => SliverList.builder( + itemCount: 12, + itemBuilder: (context, index) { + return MsgFeedTopSkeleton(); + }, + ), + Success() => loadingState.response?.isNotEmpty == true + ? SliverList.builder( + itemCount: loadingState.response!.length, + itemBuilder: (context, index) { + if (index == loadingState.response!.length - 1) { + _followController.onLoadMore(); + } + return FollowItem( + item: loadingState.response![index], + isOwner: _isOwner, + callback: (attr) { + List list = + (_followController.loadingState.value as Success) + .response; + list[index].attribute = attr == 0 ? -1 : 0; + _followController.loadingState.refresh(); + }, + ); + }, + ) + : HttpError( + callback: _followController.onReload, + ), + Error() => HttpError( + errMsg: loadingState.errMsg, + callback: _followController.onReload, + ), + _ => throw UnimplementedError(), + }; + } + + @override + bool get wantKeepAlive => widget.tagid != null; +} diff --git a/lib/pages/follow/controller.dart b/lib/pages/follow/controller.dart index ab090aebb..6f21875f4 100644 --- a/lib/pages/follow/controller.dart +++ b/lib/pages/follow/controller.dart @@ -1,83 +1,52 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:get/get.dart'; -import 'package:PiliPlus/http/follow.dart'; +import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/member.dart'; -import 'package:PiliPlus/models/follow/result.dart'; import 'package:PiliPlus/models/member/tags.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; import 'package:PiliPlus/utils/storage.dart'; -/// 查看自己的关注时,可以查看分类 -/// 查看其他人的关注时,只可以看全部 class FollowController extends GetxController with GetSingleTickerProviderStateMixin { - int pn = 1; - int ps = 20; - int total = 0; - RxList followList = [].obs; - late int? mid; - late String? name; - dynamic userInfo; - RxString loadingText = '加载中...'.obs; - RxBool isOwner = false.obs; - late List followTags; - late TabController tabController; + late int mid; + String? name; + late bool isOwner; + + late final Rx?>> followState = + LoadingState?>.loading().obs; + TabController? tabController; @override void onInit() { super.onInit(); - userInfo = GStorage.userInfo.get('userInfoCache'); + int ownerMid = Accounts.main.mid; mid = Get.parameters['mid'] != null ? int.parse(Get.parameters['mid']!) - : userInfo?.mid; - isOwner.value = mid == userInfo?.mid; - name = Get.parameters['name'] ?? userInfo?.uname; + : ownerMid; + isOwner = ownerMid == mid; + name = + Get.parameters['name'] ?? GStorage.userInfo.get('userInfoCache')?.uname; + if (isOwner) { + queryFollowUpTags(); + } } - Future queryFollowings(type) async { - if (type == 'init') { - pn = 1; - loadingText.value == '加载中...'; - } - if (loadingText.value == '没有更多了') { - return; - } - var res = await FollowHttp.followings( - vmid: mid, - pn: pn, - ps: ps, - orderType: 'attention', - ); + Future queryFollowUpTags() async { + var res = await MemberHttp.followUpTags(); if (res['status']) { - if (type == 'init') { - followList.value = res['data'].list; - total = res['data'].total; - } else if (type == 'onLoad') { - followList.addAll(res['data'].list); - } - if ((pn == 1 && total < ps) || res['data'].list.isEmpty) { - loadingText.value = '没有更多了'; - } - pn += 1; + tabController = TabController( + initialIndex: 0, + length: res['data'].length, + vsync: this, + ); + followState.value = LoadingState.success(res['data']); } else { - SmartDialog.showToast(res['msg']); + followState.value = LoadingState.error(res['msg']); } - return res; } - // 当查看当前用户的关注时,请求关注分组 - Future followUpTags() async { - if (userInfo != null && mid == userInfo.mid) { - var res = await MemberHttp.followUpTags(); - if (res['status']) { - followTags = res['data']; - tabController = TabController( - initialIndex: 0, - length: res['data'].length, - vsync: this, - ); - } - return res; - } + @override + void onClose() { + tabController?.dispose(); + super.onClose(); } } diff --git a/lib/pages/follow/view.dart b/lib/pages/follow/view.dart index b7d544d56..641211631 100644 --- a/lib/pages/follow/view.dart +++ b/lib/pages/follow/view.dart @@ -1,12 +1,13 @@ +import 'package:PiliPlus/common/widgets/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/member/tags.dart'; +import 'package:PiliPlus/pages/follow/child_view.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'controller.dart'; -import 'widgets/follow_list.dart'; -import 'widgets/owner_follow_list.dart'; -import 'package:PiliPlus/common/widgets/scroll_physics.dart'; -// TODO: refactor class FollowPage extends StatefulWidget { const FollowPage({super.key}); @@ -15,112 +16,94 @@ class FollowPage extends StatefulWidget { } class _FollowPageState extends State { - late String mid; - late FollowController _followController; - - @override - void initState() { - super.initState(); - mid = Get.parameters['mid']!; - _followController = - Get.put(FollowController(), tag: Utils.makeHeroTag(mid)); - } + final FollowController _followController = + Get.put(FollowController(), tag: Utils.generateRandomString(8)); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( - _followController.isOwner.value - ? '我的关注' - : '${_followController.name}的关注', + _followController.isOwner ? '我的关注' : '${_followController.name}的关注', ), - actions: [ - IconButton( - onPressed: () => Get.toNamed( - '/followSearch', - arguments: { - 'mid': int.parse(mid), - }, - ), - icon: const Icon(Icons.search_outlined), - tooltip: '搜索', - ), - PopupMenuButton( - icon: const Icon(Icons.more_vert), - itemBuilder: (BuildContext context) => [ - PopupMenuItem( - onTap: () => Get.toNamed('/blackListPage'), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.block, size: 19), - SizedBox(width: 10), - Text('黑名单管理'), + actions: _followController.isOwner + ? [ + IconButton( + onPressed: () => Get.toNamed( + '/followSearch', + arguments: { + 'mid': _followController.mid, + }, + ), + icon: const Icon(Icons.search_outlined), + tooltip: '搜索', + ), + PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (context) => [ + PopupMenuItem( + onTap: () => Get.toNamed('/blackListPage'), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.block, size: 19), + SizedBox(width: 10), + Text('黑名单管理'), + ], + ), + ) ], ), - ) - ], - ), - const SizedBox(width: 6), - ], - ), - body: Obx( - () => !_followController.isOwner.value - ? FollowList(ctr: _followController) - : FutureBuilder( - future: _followController.followUpTags(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - var data = snapshot.data; - if (data['status']) { - return Column( - children: [ - SafeArea( - top: false, - bottom: false, - child: TabBar( - controller: _followController.tabController, - isScrollable: true, - tabAlignment: TabAlignment.start, - tabs: [ - for (var i in data['data']) ...[ - Tab(text: i.name), - ] - ], - ), - ), - Expanded( - child: Material( - color: Colors.transparent, - child: tabBarView( - controller: _followController.tabController, - children: [ - for (var i = 0; - i < - _followController - .tabController.length; - i++) ...[ - OwnerFollowList( - ctr: _followController, - tagItem: _followController.followTags[i], - ) - ] - ], - ), - ), - ), - ], - ); - } else { - return const SizedBox(); - } - } else { - return const SizedBox(); - } - }, - ), + const SizedBox(width: 6), + ] + : null, ), + body: _followController.isOwner + ? Obx(() => _buildBody(_followController.followState.value)) + : FollowChildPage(mid: _followController.mid), ); } + + Widget _buildBody(LoadingState?> loadingState) { + return switch (loadingState) { + Loading() => loadingWidget, + Success() => loadingState.response?.isNotEmpty == true + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SafeArea( + top: false, + bottom: false, + child: TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + controller: _followController.tabController, + tabs: loadingState.response! + .map((item) => Tab(text: item.name)) + .toList(), + ), + ), + Expanded( + child: Material( + color: Colors.transparent, + child: tabBarView( + controller: _followController.tabController, + children: loadingState.response! + .map( + (item) => FollowChildPage( + mid: _followController.mid, + tagid: item.tagid, + ), + ) + .toList(), + ), + ), + ), + ], + ) + : FollowChildPage(mid: _followController.mid), + Error() => FollowChildPage(mid: _followController.mid), + _ => throw UnimplementedError(), + }; + } } diff --git a/lib/pages/follow/widgets/follow_item.dart b/lib/pages/follow/widgets/follow_item.dart index 43f2b7d02..b29fab492 100644 --- a/lib/pages/follow/widgets/follow_item.dart +++ b/lib/pages/follow/widgets/follow_item.dart @@ -3,20 +3,19 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/common/widgets/network_img_layer.dart'; import 'package:PiliPlus/models/follow/result.dart'; -import 'package:PiliPlus/pages/follow/index.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/utils.dart'; class FollowItem extends StatelessWidget { final FollowItemModel item; - final FollowController? ctr; + final bool? isOwner; final ValueChanged? callback; const FollowItem({ super.key, required this.item, this.callback, - this.ctr, + this.isOwner, }); @override @@ -73,7 +72,7 @@ class FollowItem extends StatelessWidget { overflow: TextOverflow.ellipsis, ), dense: true, - trailing: ctr?.isOwner.value == true + trailing: isOwner == true ? SizedBox( height: 34, child: FilledButton.tonal( diff --git a/lib/pages/follow/widgets/follow_list.dart b/lib/pages/follow/widgets/follow_list.dart deleted file mode 100644 index 23ac9c37f..000000000 --- a/lib/pages/follow/widgets/follow_list.dart +++ /dev/null @@ -1,113 +0,0 @@ -import 'package:PiliPlus/common/widgets/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; -import 'package:easy_debounce/easy_throttle.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:PiliPlus/models/follow/result.dart'; -import 'package:PiliPlus/pages/follow/index.dart'; - -import 'follow_item.dart'; - -// TODO: refactor -class FollowList extends StatefulWidget { - final FollowController ctr; - const FollowList({ - super.key, - required this.ctr, - }); - - @override - State createState() => _FollowListState(); -} - -class _FollowListState extends State { - late Future _futureBuilderFuture; - final ScrollController scrollController = ScrollController(); - - @override - void initState() { - super.initState(); - _futureBuilderFuture = widget.ctr.queryFollowings('init'); - scrollController.addListener(listener); - } - - void listener() { - if (scrollController.position.pixels >= - scrollController.position.maxScrollExtent - 200) { - EasyThrottle.throttle('follow', const Duration(seconds: 1), () { - widget.ctr.queryFollowings('onLoad'); - }); - } - } - - @override - void dispose() { - scrollController.removeListener(listener); - scrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return refreshIndicator( - onRefresh: () async => await widget.ctr.queryFollowings('init'), - child: FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - var data = snapshot.data; - if (data['status']) { - List list = widget.ctr.followList; - return Obx( - () => list.isNotEmpty - ? ListView.builder( - controller: scrollController, - itemCount: list.length + 1, - itemBuilder: (BuildContext context, int index) { - if (index == list.length) { - return Container( - height: - MediaQuery.of(context).padding.bottom + 80, - padding: EdgeInsets.only( - bottom: - MediaQuery.of(context).padding.bottom), - child: Center( - child: Obx( - () => Text( - widget.ctr.loadingText.value, - style: TextStyle( - color: Theme.of(context) - .colorScheme - .outline, - fontSize: 13), - ), - ), - ), - ); - } else { - return FollowItem( - item: list[index], - ctr: widget.ctr, - ); - } - }, - ) - : scrollErrorWidget( - callback: () => widget.ctr.queryFollowings('init'), - ), - ); - } else { - return scrollErrorWidget( - errMsg: data['msg'], - callback: () => widget.ctr.queryFollowings('init'), - ); - } - } else { - // 骨架屏 - return const SizedBox(); - } - }, - ), - ); - } -} diff --git a/lib/pages/follow/widgets/owner_follow_list.dart b/lib/pages/follow/widgets/owner_follow_list.dart deleted file mode 100644 index a6a2cd948..000000000 --- a/lib/pages/follow/widgets/owner_follow_list.dart +++ /dev/null @@ -1,128 +0,0 @@ -import 'package:PiliPlus/common/widgets/loading_widget.dart'; -import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; -import 'package:easy_debounce/easy_throttle.dart'; -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:PiliPlus/http/member.dart'; -import 'package:PiliPlus/models/follow/result.dart'; -import 'package:PiliPlus/models/member/tags.dart'; -import 'package:PiliPlus/pages/follow/index.dart'; -import 'follow_item.dart'; - -// TODO: refactor -class OwnerFollowList extends StatefulWidget { - final FollowController ctr; - final MemberTagItemModel? tagItem; - const OwnerFollowList({super.key, required this.ctr, this.tagItem}); - - @override - State createState() => _OwnerFollowListState(); -} - -class _OwnerFollowListState extends State - with AutomaticKeepAliveClientMixin { - late int? mid; - late Future _futureBuilderFuture; - final ScrollController scrollController = ScrollController(); - int pn = 1; - int ps = 20; - late MemberTagItemModel tagItem; - RxList followList = [].obs; - - @override - bool get wantKeepAlive => true; - - @override - void initState() { - super.initState(); - mid = widget.ctr.mid; - tagItem = widget.tagItem!; - _futureBuilderFuture = followUpGroup('init'); - scrollController.addListener(listener); - } - - void listener() { - if (scrollController.position.pixels >= - scrollController.position.maxScrollExtent - 200) { - EasyThrottle.throttle('follow', const Duration(seconds: 1), () { - followUpGroup('onLoad'); - }); - } - } - - // 获取分组下up - Future followUpGroup(type) async { - if (type == 'init') { - pn = 1; - } - var res = await MemberHttp.followUpGroup(mid, tagItem.tagid, pn, ps); - if (res['status']) { - if (res['data'].isNotEmpty) { - if (type == 'init') { - followList.value = res['data']; - } else { - followList.addAll(res['data']); - } - pn += 1; - } - } - return res; - } - - @override - void dispose() { - scrollController.removeListener(listener); - scrollController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return refreshIndicator( - onRefresh: () async => await followUpGroup('init'), - child: FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - var data = snapshot.data; - if (data['status']) { - return Obx( - () => followList.isNotEmpty - ? ListView.builder( - physics: const AlwaysScrollableScrollPhysics(), - controller: scrollController, - itemCount: followList.length, - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom + 80, - ), - itemBuilder: (BuildContext context, int index) { - return FollowItem( - item: followList[index], - ctr: widget.ctr, - callback: (attr) { - followList[index].attribute = attr == 0 ? -1 : 0; - followList.refresh(); - }, - ); - }, - ) - : scrollErrorWidget( - callback: () => widget.ctr.queryFollowings('init'), - ), - ); - } else { - return scrollErrorWidget( - errMsg: data['msg'], - callback: () => widget.ctr.queryFollowings('init'), - ); - } - } else { - // 骨架屏 - return const SizedBox(); - } - }, - ), - ); - } -}