diff --git a/lib/http/user.dart b/lib/http/user.dart index e3a20da0..3476eef1 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -484,23 +484,24 @@ class UserHttp { } } - static Future favSeasonList({ + static Future> favSeasonList({ required int id, required int pn, required int ps, }) async { - var res = await Request().get(Api.favSeasonList, queryParameters: { - 'season_id': id, - 'ps': ps, - 'pn': pn, - }); + var res = await Request().get( + Api.favSeasonList, + queryParameters: { + 'season_id': id, + 'ps': ps, + 'pn': pn, + }, + ); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': SubDetailModelData.fromJson(res.data['data']) - }; + return LoadingState.success( + SubDetailModelData.fromJson(res.data['data'])); } else { - return {'status': false, 'msg': res.data['message']}; + return LoadingState.error(res.data['message']); } } @@ -582,7 +583,7 @@ class UserHttp { } } - static Future favResourceList({ + static Future> favResourceList({ required int id, required int pn, required int ps, @@ -593,12 +594,10 @@ class UserHttp { 'pn': pn, }); if (res.data['code'] == 0) { - return { - 'status': true, - 'data': SubDetailModelData.fromJson(res.data['data']) - }; + return LoadingState.success( + SubDetailModelData.fromJson(res.data['data'])); } else { - return {'status': false, 'msg': res.data['message']}; + return LoadingState.error(res.data['message']); } } diff --git a/lib/pages/subscription_detail/controller.dart b/lib/pages/subscription_detail/controller.dart index 610678bb..4cafbce2 100644 --- a/lib/pages/subscription_detail/controller.dart +++ b/lib/pages/subscription_detail/controller.dart @@ -1,76 +1,70 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/http/user.dart'; import '../../models/user/sub_detail.dart'; import '../../models/user/sub_folder.dart'; -class SubDetailController extends GetxController { +class SubDetailController + extends CommonListController { late SubFolderItemData item; late int id; late String heroTag; - int currentPage = 1; - bool isLoadingMore = false; - Rx subInfo = DetailInfo().obs; - RxList subList = [].obs; - RxString loadingText = '加载中...'.obs; - int mediaCount = 0; + + RxInt mediaCount = 0.obs; RxInt playCount = 0.obs; @override void onInit() { + super.onInit(); item = Get.arguments; - if (playCount.value == 0) playCount.value = item.viewCount!; + playCount.value = item.viewCount!; if (Get.parameters.keys.isNotEmpty) { id = int.parse(Get.parameters['id']!); heroTag = Get.parameters['heroTag']!; } - super.onInit(); + queryData(); } - Future queryUserSubFolderDetail({type = 'init'}) async { - if (type == 'onLoad' && subList.length >= mediaCount) { - loadingText.value = '没有更多了'; - return; + @override + List? getDataList(SubDetailModelData response) { + return response.list; + } + + @override + void checkIsEnd(int length) { + if (length >= mediaCount.value) { + isEnd = true; } - isLoadingMore = true; - late Map res; + } + + @override + bool customHandleResponse( + bool isRefresh, Success response) { + mediaCount.value = response.response.info!.mediaCount!; + if (item.type == 11) { + playCount.value = response.response.info!.cntInfo!['play']; + } + return false; + } + + @override + Future> customGetData() { if (item.type! == 11) { - res = await UserHttp.favResourceList( + return UserHttp.favResourceList( id: id, ps: 20, pn: currentPage, ); } else { - res = await UserHttp.favSeasonList( + return UserHttp.favSeasonList( // item.type! == 21 id: id, ps: 20, pn: currentPage, ); } - if (res['status']) { - SubDetailModelData data = res['data']; - subInfo.value = data.info!; - if (currentPage == 1 && type == 'init') { - subList.value = data.list!; - mediaCount = data.info!.mediaCount!; - if (item.type == 11) { - playCount.value = data.info!.cntInfo!['play']; - } - } else if (type == 'onLoad') { - subList.addAll(data.list!); - } - if (subList.length >= mediaCount) { - loadingText.value = '没有更多了'; - } - currentPage += 1; - } - isLoadingMore = false; - return res; - } - - onLoad() { - queryUserSubFolderDetail(type: 'onLoad'); } } diff --git a/lib/pages/subscription_detail/view.dart b/lib/pages/subscription_detail/view.dart index ee5f9c34..359da919 100644 --- a/lib/pages/subscription_detail/view.dart +++ b/lib/pages/subscription_detail/view.dart @@ -1,8 +1,6 @@ -import 'dart:async'; - +import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/user/sub_detail.dart'; import 'package:PiliPlus/utils/grid.dart'; -import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:PiliPlus/common/skeleton/video_card_h.dart'; @@ -22,246 +20,206 @@ class SubDetailPage extends StatefulWidget { } class _SubDetailPageState extends State { - late final ScrollController _controller = ScrollController(); late final SubDetailController _subDetailController = Get.put( - SubDetailController(), - tag: Utils.makeHeroTag(Get.parameters['id'])); + SubDetailController(), + tag: Utils.makeHeroTag(Get.parameters['id']), + ); final RxBool showTitle = false.obs; - late Future _futureBuilderFuture; @override void initState() { super.initState(); - _futureBuilderFuture = _subDetailController.queryUserSubFolderDetail(); - _controller.addListener(listener); + _subDetailController.scrollController.addListener(listener); } void listener() { - showTitle.value = _controller.offset > 160; - - if (_controller.position.pixels >= - _controller.position.maxScrollExtent - 200) { - EasyThrottle.throttle('subDetail', const Duration(seconds: 1), () { - _subDetailController.onLoad(); - }); - } + showTitle.value = _subDetailController.scrollController.offset > 160; } @override void dispose() { - _controller.removeListener(listener); - _controller.dispose(); + _subDetailController.scrollController.removeListener(listener); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( - body: CustomScrollView( - controller: _controller, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverAppBar( - expandedHeight: 215 - MediaQuery.of(context).padding.top, - pinned: true, - title: Obx( - () { - return AnimatedOpacity( - opacity: showTitle.value ? 1 : 0, - curve: Curves.easeOut, - duration: const Duration(milliseconds: 500), - child: Row( - children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _subDetailController.item.title!, - style: Theme.of(context).textTheme.titleMedium, - ), - Text( - '共${_subDetailController.item.mediaCount!}条视频', - style: Theme.of(context).textTheme.labelMedium, - ) - ], - ) - ], - ), - ); - }, - ), - flexibleSpace: FlexibleSpaceBar( - background: Container( - decoration: BoxDecoration( - border: Border( - bottom: BorderSide( - color: Theme.of(context).dividerColor.withOpacity(0.2), - ), - ), - ), - padding: EdgeInsets.only( - top: kTextTabBarHeight + - MediaQuery.of(context).padding.top + - 15, - left: 12, - right: 12, - ), - child: SizedBox( - height: 200, - child: Row( - // mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Hero( - tag: _subDetailController.heroTag, - child: NetworkImgLayer( - width: 180, - height: 110, - src: _subDetailController.item.cover, - ), - ), - const SizedBox(width: 14), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const SizedBox(height: 4), - Text( - _subDetailController.item.title!, - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .titleMedium! - .fontSize, - fontWeight: FontWeight.bold), - ), - const SizedBox(height: 4), - GestureDetector( - onTap: () { - SubFolderItemData item = - _subDetailController.item; - Get.toNamed( - '/member?mid=${item.upper!.mid}', - arguments: { - 'face': item.upper!.face, - }, - ); - }, - child: Text( - _subDetailController.item.upper!.name!, - style: TextStyle( - color: - Theme.of(context).colorScheme.primary), - ), - ), - const SizedBox(height: 4), - Obx( - () => Text( - '${Utils.numFormat(_subDetailController.playCount.value)}次播放', - style: TextStyle( - fontSize: Theme.of(context) - .textTheme - .labelSmall! - .fontSize, - color: - Theme.of(context).colorScheme.outline), - ), - ), - ], - ), - ), - ], - ), - ), - ), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), - child: Obx( - () => Text( - '共${_subDetailController.subList.length}条视频', - style: TextStyle( - fontSize: - Theme.of(context).textTheme.labelMedium!.fontSize, - color: Theme.of(context).colorScheme.outline, - letterSpacing: 1), - ), - ), - ), - ), - FutureBuilder( - future: _futureBuilderFuture, - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - // TODO: refactor - if (snapshot.data is! Map) { - return HttpError( - callback: () => setState(() { - _futureBuilderFuture = - _subDetailController.queryUserSubFolderDetail(); - }), - ); - } - Map data = snapshot.data; - if (data['status']) { - if (_subDetailController.item.mediaCount == 0) { - return HttpError( - callback: () => setState(() { - _futureBuilderFuture = - _subDetailController.queryUserSubFolderDetail(); - }), - ); - } else { - List subList = - _subDetailController.subList; - return Obx( - () => subList.isEmpty - ? const SliverToBoxAdapter(child: SizedBox()) - : SliverPadding( - padding: EdgeInsets.only( - bottom: - MediaQuery.paddingOf(context).bottom + 80, - ), - sliver: SliverGrid( - gridDelegate: Grid.videoCardHDelegate(context), - delegate: SliverChildBuilderDelegate( - childCount: subList.length, - (BuildContext context, int index) { - return SubVideoCardH( - videoItem: subList[index], - ); - }, - ), - ), - ), - ); - } - } else { - return HttpError( - errMsg: data['msg'], - callback: () => setState(() { - _futureBuilderFuture = - _subDetailController.queryUserSubFolderDetail(); - }), - ); - } - } else { - // 骨架屏 - return SliverGrid( - gridDelegate: Grid.videoCardHDelegate(context), - delegate: SliverChildBuilderDelegate( - (context, index) => const VideoCardHSkeleton(), - childCount: 10, - ), - ); - } - }, - ), - ], + body: SafeArea( + top: false, + bottom: false, + child: CustomScrollView( + controller: _subDetailController.scrollController, + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + _buildAppBar, + _buildCount, + Obx(() => _buildBody(_subDetailController.loadingState.value)), + ], + ), ), ); } + + Widget _buildBody(LoadingState?> loadingState) { + return switch (loadingState) { + Loading() => SliverGrid( + gridDelegate: Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + (context, index) => const VideoCardHSkeleton(), + childCount: 10, + ), + ), + Success() => loadingState.response?.isNotEmpty == true + ? SliverPadding( + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom + 80, + ), + sliver: SliverGrid( + gridDelegate: Grid.videoCardHDelegate(context), + delegate: SliverChildBuilderDelegate( + childCount: loadingState.response!.length, + (context, index) { + if (index == loadingState.response!.length - 1) { + _subDetailController.onLoadMore(); + } + return SubVideoCardH( + videoItem: loadingState.response![index], + ); + }, + ), + ), + ) + : HttpError( + callback: _subDetailController.onReload, + ), + Error() => HttpError( + errMsg: loadingState.errMsg, + callback: _subDetailController.onReload, + ), + _ => throw UnimplementedError(), + }; + } + + Widget get _buildCount => SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only(top: 15, bottom: 8, left: 14), + child: Obx( + () => Text( + '共${_subDetailController.mediaCount}条视频', + style: TextStyle( + fontSize: Theme.of(context).textTheme.labelMedium!.fontSize, + color: Theme.of(context).colorScheme.outline, + letterSpacing: 1, + ), + ), + ), + ), + ); + + Widget get _buildAppBar => SliverAppBar( + expandedHeight: 215 - MediaQuery.paddingOf(context).bottom, + pinned: true, + title: Obx( + () { + return AnimatedOpacity( + opacity: showTitle.value ? 1 : 0, + curve: Curves.easeOut, + duration: const Duration(milliseconds: 500), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + _subDetailController.item.title!, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + '共${_subDetailController.mediaCount.value}条视频', + style: Theme.of(context).textTheme.labelMedium, + ) + ], + ), + ); + }, + ), + flexibleSpace: FlexibleSpaceBar( + background: Container( + height: 180, + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.withOpacity(0.2), + ), + ), + ), + padding: EdgeInsets.only( + top: kTextTabBarHeight + MediaQuery.of(context).padding.top + 15, + left: 12, + right: 12, + bottom: 20, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Hero( + tag: _subDetailController.heroTag, + child: NetworkImgLayer( + width: 180, + height: 110, + src: _subDetailController.item.cover, + ), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 4), + Text( + _subDetailController.item.title!, + style: TextStyle( + fontSize: Theme.of(context) + .textTheme + .titleMedium! + .fontSize, + fontWeight: FontWeight.bold), + ), + const SizedBox(height: 4), + GestureDetector( + onTap: () { + SubFolderItemData item = _subDetailController.item; + Get.toNamed( + '/member?mid=${item.upper!.mid}', + arguments: { + 'face': item.upper!.face, + }, + ); + }, + child: Text( + _subDetailController.item.upper!.name!, + style: TextStyle( + color: Theme.of(context).colorScheme.primary), + ), + ), + const SizedBox(height: 4), + Obx( + () => Text( + '${Utils.numFormat(_subDetailController.playCount.value)}次播放', + style: TextStyle( + fontSize: 12, + color: Theme.of(context).colorScheme.outline, + ), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); }