From 72fa9c51f0b172a6a8f43320c163d540f60724f8 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Sat, 29 Mar 2025 13:28:07 +0800 Subject: [PATCH] feat: fav pgc page Signed-off-by: bggRGjQaUbCoE --- lib/common/widgets/dialog.dart | 67 +++++ lib/http/api.dart | 5 +- lib/http/bangumi.dart | 11 +- lib/http/video.dart | 6 +- lib/models/bangumi/list.dart | 7 +- lib/pages/bangumi/controller.dart | 2 +- .../bangumi/introduction/controller.dart | 2 +- lib/pages/bangumi/introduction/view.dart | 74 ++---- lib/pages/fav/note/child_view.dart | 3 + lib/pages/fav/note/view.dart | 51 ++-- lib/pages/fav/note/widget/item.dart | 237 +++++++++--------- lib/pages/fav/pgc/child_view.dart | 217 ++++++++++++++++ lib/pages/fav/pgc/controller.dart | 134 ++++++++++ lib/pages/fav/pgc/view.dart | 93 +++++++ lib/pages/fav/pgc/widget/item.dart | 186 ++++++++++++++ lib/pages/fav/view.dart | 5 +- 16 files changed, 884 insertions(+), 216 deletions(-) create mode 100644 lib/pages/fav/pgc/child_view.dart create mode 100644 lib/pages/fav/pgc/controller.dart create mode 100644 lib/pages/fav/pgc/view.dart create mode 100644 lib/pages/fav/pgc/widget/item.dart diff --git a/lib/common/widgets/dialog.dart b/lib/common/widgets/dialog.dart index 90d4479f..10a0565a 100644 --- a/lib/common/widgets/dialog.dart +++ b/lib/common/widgets/dialog.dart @@ -33,3 +33,70 @@ void showConfirmDialog({ }, ); } + +void showPgcFollowDialog({ + required BuildContext context, + required String type, + required int followStatus, + required ValueChanged onUpdateStatus, +}) { + Widget statusItem({ + required bool enabled, + required String text, + required VoidCallback onTap, + }) { + return ListTile( + dense: true, + enabled: enabled, + title: Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + '标记为 $text', + style: const TextStyle(fontSize: 14), + ), + ), + trailing: !enabled ? const Icon(size: 22, Icons.check) : null, + onTap: onTap, + ); + } + + showDialog( + context: context, + builder: (context) => AlertDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.symmetric(vertical: 12), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ...[ + {'followStatus': 3, 'title': '看过'}, + {'followStatus': 2, 'title': '在看'}, + {'followStatus': 1, 'title': '想看'}, + ].map( + (Map item) => statusItem( + enabled: followStatus != item['followStatus'], + text: item['title'], + onTap: () { + Get.back(); + onUpdateStatus(item['followStatus']); + }, + ), + ), + ListTile( + dense: true, + title: Padding( + padding: EdgeInsets.only(left: 10), + child: Text( + '取消$type', + style: TextStyle(fontSize: 14), + ), + ), + onTap: () { + Get.back(); + onUpdateStatus(-1); + }, + ) + ], + ), + )); +} diff --git a/lib/http/api.dart b/lib/http/api.dart index fcd1a48e..20b3684c 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -411,7 +411,7 @@ class Api { '/pgc/season/index/result?st=1&order=3&season_version=-1&spoken_language_type=-1&area=-1&is_finish=-1©right=-1&season_status=-1&season_month=-1&year=-1&style_id=-1&sort=0&season_type=1&pagesize=20&type=1'; // 我的追番/追剧 ?type=1&pn=1&ps=15 - static const String bangumiFollow = '/x/space/bangumi/follow/list'; + static const String bangumiFollowList = '/x/space/bangumi/follow/list'; // 黑名单 static const String blackLst = '/x/relation/blacks'; @@ -643,9 +643,6 @@ class Api { /// 最近点赞的视频 static const getRecentLikeVideoApi = '/x/space/like/video'; - /// 最近追番 - static const getRecentBangumiApi = '/x/space/bangumi/follow/list'; - /// 用户专栏 static const getMemberSeasonsApi = '/x/polymer/web-space/home/seasons_series'; diff --git a/lib/http/bangumi.dart b/lib/http/bangumi.dart index e58e2a2e..b753d10b 100644 --- a/lib/http/bangumi.dart +++ b/lib/http/bangumi.dart @@ -67,19 +67,24 @@ class BangumiHttp { } } - static Future bangumiFollow({ - dynamic mid, + static Future bangumiFollowList({ + required dynamic mid, required int type, required int pn, + int? followStatus, }) async { - var res = await Request().get(Api.bangumiFollow, queryParameters: { + var res = await Request().get(Api.bangumiFollowList, queryParameters: { 'vmid': mid, 'type': type, + if (followStatus != null) 'follow_status': followStatus, 'pn': pn, }); if (res.data['code'] == 0) { BangumiListDataModel data = BangumiListDataModel.fromJson(res.data['data']); + if (followStatus != null) { + return LoadingState.success(data.list); + } return LoadingState.success(data); } else { return LoadingState.error(res.data['message']); diff --git a/lib/http/video.dart b/lib/http/video.dart index b92e32b0..33f849c7 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -920,13 +920,13 @@ class VideoHttp { } static Future bangumiUpdate({ - dynamic seasonId, - dynamic status, + required List seasonId, + required dynamic status, }) async { var res = await Request().post( Api.bangumiUpdate, data: { - 'season_id': seasonId, + 'season_id': seasonId.join(','), 'status': status, 'csrf': Accounts.main.csrf, }, diff --git a/lib/models/bangumi/list.dart b/lib/models/bangumi/list.dart index 0e37a145..f2896728 100644 --- a/lib/models/bangumi/list.dart +++ b/lib/models/bangumi/list.dart @@ -44,6 +44,7 @@ class BangumiListItemModel { this.title, this.titleIcon, this.progress, + this.newEp, }); String? badge; @@ -62,9 +63,11 @@ class BangumiListItemModel { String? subTitle; String? title; String? titleIcon; - + Map? newEp; String? progress; + bool? checked; + BangumiListItemModel.fromJson(Map json) { badge = json['badge'] == '' ? null : json['badge']; badgeType = json['badge_type']; @@ -82,7 +85,7 @@ class BangumiListItemModel { subTitle = json['sub_title']; title = json['title']; titleIcon = json['title_icon']; - + newEp = json['new_ep']; progress = json['progress']; } } diff --git a/lib/pages/bangumi/controller.dart b/lib/pages/bangumi/controller.dart index eab64020..056fd330 100644 --- a/lib/pages/bangumi/controller.dart +++ b/lib/pages/bangumi/controller.dart @@ -51,7 +51,7 @@ class BangumiController extends CommonController { return; } followLoading = true; - dynamic res = await BangumiHttp.bangumiFollow( + dynamic res = await BangumiHttp.bangumiFollowList( mid: mid, type: tabType == TabType.bangumi ? 1 : 2, pn: followPage, diff --git a/lib/pages/bangumi/introduction/controller.dart b/lib/pages/bangumi/introduction/controller.dart index 57f73ce8..b55edef6 100644 --- a/lib/pages/bangumi/introduction/controller.dart +++ b/lib/pages/bangumi/introduction/controller.dart @@ -521,7 +521,7 @@ class BangumiIntroController extends CommonController { Future bangumiUpdate(status) async { var result = await VideoHttp.bangumiUpdate( - seasonId: (loadingState.value as Success).response.seasonId, + seasonId: [(loadingState.value as Success).response.seasonId], status: status, ); if (result['status']) { diff --git a/lib/pages/bangumi/introduction/view.dart b/lib/pages/bangumi/introduction/view.dart index 2301d35c..865a3372 100644 --- a/lib/pages/bangumi/introduction/view.dart +++ b/lib/pages/bangumi/introduction/view.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:PiliPlus/common/widgets/dialog.dart'; import 'package:PiliPlus/common/widgets/http_error.dart'; import 'package:PiliPlus/common/widgets/interactiveviewer_gallery/interactiveviewer_gallery.dart' show SourceModel; @@ -299,10 +300,24 @@ class _BangumiInfoState extends State : () { if (bangumiIntroController .isFollowed.value) { - showDialog( + showPgcFollowDialog( context: context, - builder: (context) => - _followDialog(), + type: bangumiIntroController + .type, + followStatus: + bangumiIntroController + .followStatus.value, + onUpdateStatus: + (followStatus) { + if (followStatus == -1) { + bangumiIntroController + .bangumiDel(); + } else { + bangumiIntroController + .bangumiUpdate( + followStatus); + } + }, ); } else { bangumiIntroController @@ -601,59 +616,6 @@ class _BangumiInfoState extends State text: '转发'), ]); } - - Widget _followDialog() { - return AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _followDialogItem(3, '看过'), - _followDialogItem(2, '在看'), - _followDialogItem(1, '想看'), - ListTile( - dense: true, - title: Padding( - padding: EdgeInsets.only(left: 10), - child: Text( - '取消${bangumiIntroController.type}', - style: TextStyle(fontSize: 14), - ), - ), - onTap: () { - Get.back(); - bangumiIntroController.bangumiDel(); - }, - ) - ], - ), - ); - } - - Widget _followDialogItem( - int followStatus, - String text, - ) { - return ListTile( - dense: true, - enabled: bangumiIntroController.followStatus.value != followStatus, - title: Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - '标记为 $text', - style: const TextStyle(fontSize: 14), - ), - ), - trailing: bangumiIntroController.followStatus.value == followStatus - ? const Icon(size: 22, Icons.check) - : null, - onTap: () { - Get.back(); - bangumiIntroController.bangumiUpdate(followStatus); - }, - ); - } } class AreasAndPubTime extends StatelessWidget { diff --git a/lib/pages/fav/note/child_view.dart b/lib/pages/fav/note/child_view.dart index 406ede79..0dd0c050 100644 --- a/lib/pages/fav/note/child_view.dart +++ b/lib/pages/fav/note/child_view.dart @@ -149,6 +149,9 @@ class _FavNoteChildPageState extends State ), delegate: SliverChildBuilderDelegate( (context, index) { + if (index == loadingState.response.length - 1) { + _favNoteController.onLoadMore(); + } return FavNoteItem( item: loadingState.response[index], ctr: _favNoteController, diff --git a/lib/pages/fav/note/view.dart b/lib/pages/fav/note/view.dart index 72058fd9..3886635d 100644 --- a/lib/pages/fav/note/view.dart +++ b/lib/pages/fav/note/view.dart @@ -1,8 +1,5 @@ -import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/pages/fav/note/child_view.dart'; -import 'package:PiliPlus/pages/fav/note/controller.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; class FavNotePage extends StatefulWidget { const FavNotePage({super.key}); @@ -62,30 +59,30 @@ class _FavNotePageState extends State ], ), ), - TextButton( - style: TextButton.styleFrom( - foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant, - visualDensity: VisualDensity(horizontal: -2, vertical: -2), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - onPressed: () async { - final favNoteController = Get.find( - tag: _tabController.index == 0 ? 'false' : 'true'); - if (favNoteController.enableMultiSelect.value) { - favNoteController.onDisable(); - } else { - if (favNoteController.loadingState.value is Success && - ((favNoteController.loadingState.value as Success) - .response as List?) - ?.isNotEmpty == - true) { - favNoteController.enableMultiSelect.value = true; - } - } - }, - child: const Text('管理'), - ), - const SizedBox(width: 12), + // TextButton( + // style: TextButton.styleFrom( + // foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant, + // visualDensity: VisualDensity(horizontal: -2, vertical: -2), + // tapTargetSize: MaterialTapTargetSize.shrinkWrap, + // ), + // onPressed: () async { + // final favNoteController = Get.find( + // tag: _tabController.index == 0 ? 'false' : 'true'); + // if (favNoteController.enableMultiSelect.value) { + // favNoteController.onDisable(); + // } else { + // if (favNoteController.loadingState.value is Success && + // ((favNoteController.loadingState.value as Success) + // .response as List?) + // ?.isNotEmpty == + // true) { + // favNoteController.enableMultiSelect.value = true; + // } + // } + // }, + // child: const Text('管理'), + // ), + // const SizedBox(width: 12), ], ), Expanded( diff --git a/lib/pages/fav/note/widget/item.dart b/lib/pages/fav/note/widget/item.dart index d6cadbc4..3a6a62a0 100644 --- a/lib/pages/fav/note/widget/item.dart +++ b/lib/pages/fav/note/widget/item.dart @@ -18,123 +18,126 @@ class FavNoteItem extends StatelessWidget { @override Widget build(BuildContext context) { - return InkWell( - onTap: () { - if (ctr.enableMultiSelect.value) { - onSelect(); - return; - } - Utils.handleWebview(item['web_url'], inApp: true); - }, - onLongPress: () { - if (!ctr.enableMultiSelect.value) { - ctr.enableMultiSelect.value = true; - onSelect(); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: StyleString.safeSpace, - vertical: 5, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Text( - item['title'], - maxLines: 2, - overflow: TextOverflow.ellipsis, - style: TextStyle( - height: 1.4, - fontSize: 14, - fontWeight: FontWeight.bold, + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + if (ctr.enableMultiSelect.value) { + onSelect(); + return; + } + Utils.handleWebview(item['web_url'], inApp: true); + }, + onLongPress: () { + if (!ctr.enableMultiSelect.value) { + ctr.enableMultiSelect.value = true; + onSelect(); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: StyleString.safeSpace, + vertical: 5, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + item['title'], + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: TextStyle( + height: 1.4, + fontSize: 14, + fontWeight: FontWeight.bold, + ), ), ), - ), - Text( - item['summary'], - maxLines: 1, - style: TextStyle( - color: Theme.of(context).colorScheme.outline, + Text( + item['summary'], + maxLines: 1, + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), ), - ), - Text( - item['message'], - maxLines: 1, - style: TextStyle( - color: Theme.of(context).colorScheme.outline, + Text( + item['message'], + maxLines: 1, + style: TextStyle( + color: Theme.of(context).colorScheme.outline, + ), ), - ), - ], + ], + ), ), - ), - if (item['arc']?['pic'] != null) ...[ - const SizedBox(width: 10), - AspectRatio( - aspectRatio: StyleString.aspectRatio, - child: LayoutBuilder( - builder: - (BuildContext context, BoxConstraints boxConstraints) { - return Stack( - clipBehavior: Clip.none, - children: [ - NetworkImgLayer( - src: item['arc']?['pic'], - width: boxConstraints.maxWidth, - height: boxConstraints.maxHeight, - ), - Positioned.fill( - child: IgnorePointer( - child: LayoutBuilder( - builder: (context, constraints) => - AnimatedOpacity( - opacity: item['checked'] == true ? 1 : 0, - duration: const Duration(milliseconds: 200), - child: Container( - alignment: Alignment.center, - height: constraints.maxHeight, - width: constraints.maxHeight * - StyleString.aspectRatio, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - color: Colors.black.withOpacity(0.6), - ), - child: SizedBox( - width: 34, - height: 34, - child: AnimatedScale( - scale: item['checked'] == true ? 1 : 0, - duration: - const Duration(milliseconds: 250), - curve: Curves.easeInOut, - child: IconButton( - tooltip: '取消选择', - style: ButtonStyle( - padding: WidgetStateProperty.all( - EdgeInsets.zero), - backgroundColor: - WidgetStateProperty.resolveWith( - (states) { - return Theme.of(context) - .colorScheme - .surface - .withOpacity(0.8); - }, + if (item['arc']?['pic'] != null) ...[ + const SizedBox(width: 10), + AspectRatio( + aspectRatio: StyleString.aspectRatio, + child: LayoutBuilder( + builder: + (BuildContext context, BoxConstraints boxConstraints) { + return Stack( + clipBehavior: Clip.none, + children: [ + NetworkImgLayer( + src: item['arc']?['pic'], + width: boxConstraints.maxWidth, + height: boxConstraints.maxHeight, + ), + Positioned.fill( + child: IgnorePointer( + child: LayoutBuilder( + builder: (context, constraints) => + AnimatedOpacity( + opacity: item['checked'] == true ? 1 : 0, + duration: const Duration(milliseconds: 200), + child: Container( + alignment: Alignment.center, + height: constraints.maxHeight, + width: constraints.maxHeight * + StyleString.aspectRatio, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Colors.black.withOpacity(0.6), + ), + child: SizedBox( + width: 34, + height: 34, + child: AnimatedScale( + scale: item['checked'] == true ? 1 : 0, + duration: + const Duration(milliseconds: 250), + curve: Curves.easeInOut, + child: IconButton( + tooltip: '取消选择', + style: ButtonStyle( + padding: WidgetStateProperty.all( + EdgeInsets.zero), + backgroundColor: + WidgetStateProperty.resolveWith( + (states) { + return Theme.of(context) + .colorScheme + .surface + .withOpacity(0.8); + }, + ), + ), + onPressed: null, + icon: Icon( + Icons.done_all_outlined, + color: Theme.of(context) + .colorScheme + .primary, ), - ), - onPressed: null, - icon: Icon( - Icons.done_all_outlined, - color: Theme.of(context) - .colorScheme - .primary, ), ), ), @@ -143,14 +146,14 @@ class FavNoteItem extends StatelessWidget { ), ), ), - ), - ], - ); - }, + ], + ); + }, + ), ), - ), + ], ], - ], + ), ), ), ); diff --git a/lib/pages/fav/pgc/child_view.dart b/lib/pages/fav/pgc/child_view.dart new file mode 100644 index 00000000..50a92985 --- /dev/null +++ b/lib/pages/fav/pgc/child_view.dart @@ -0,0 +1,217 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/skeleton/video_card_h.dart'; +import 'package:PiliPlus/common/widgets/dialog.dart'; +import 'package:PiliPlus/common/widgets/http_error.dart'; +import 'package:PiliPlus/common/widgets/icon_button.dart'; +import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/pages/fav/pgc/controller.dart'; +import 'package:PiliPlus/pages/fav/pgc/widget/item.dart'; +import 'package:PiliPlus/utils/grid.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +class FavPgcChildPage extends StatefulWidget { + const FavPgcChildPage({ + super.key, + required this.type, + required this.followStatus, + }); + + final int type; + final int followStatus; + + @override + State createState() => _FavPgcChildPageState(); +} + +class _FavPgcChildPageState extends State + with AutomaticKeepAliveClientMixin { + late final FavPgcController _favPgcController = Get.put( + FavPgcController(widget.type, widget.followStatus), + tag: '${widget.type}${widget.followStatus}', + ); + + @override + Widget build(BuildContext context) { + super.build(context); + return LayoutBuilder( + builder: (context, constraints) => Stack( + clipBehavior: Clip.none, + children: [ + refreshIndicator( + onRefresh: () async { + await _favPgcController.onRefresh(); + }, + child: CustomScrollView( + slivers: [ + Obx(() => _buildBody(_favPgcController.loadingState.value)), + ], + ), + ), + Positioned( + top: constraints.maxHeight, + left: 0, + right: 0, + child: Obx( + () => AnimatedSlide( + offset: _favPgcController.enableMultiSelect.value + ? Offset(0, -1) + : Offset.zero, + duration: const Duration(milliseconds: 150), + child: Container( + padding: MediaQuery.paddingOf(context), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onInverseSurface, + border: Border( + top: BorderSide( + width: 0.5, + color: Theme.of(context) + .colorScheme + .outline + .withOpacity(0.5), + ), + ), + ), + width: double.infinity, + child: Row( + children: [ + const SizedBox(width: 16), + iconButton( + size: 32, + tooltip: '取消', + context: context, + icon: Icons.clear, + onPressed: _favPgcController.onDisable, + ), + const SizedBox(width: 12), + Obx( + () => Checkbox( + value: _favPgcController.allSelected.value, + onChanged: (value) { + _favPgcController.handleSelect( + !_favPgcController.allSelected.value); + }, + ), + ), + GestureDetector( + onTap: () { + _favPgcController.handleSelect( + !_favPgcController.allSelected.value); + }, + child: const Text('全选'), + ), + const Spacer(), + ...[ + {'followStatus': 1, 'title': '想看'}, + {'followStatus': 2, 'title': '在看'}, + {'followStatus': 3, 'title': '看过'}, + ] + .where((item) => + item['followStatus'] != widget.followStatus) + .map( + (Map item) => Padding( + padding: const EdgeInsets.only(left: 25), + child: GestureDetector( + onTap: () { + _favPgcController + .onUpdate(item['followStatus']); + }, + child: Text( + '标记为${item['title']}', + style: TextStyle( + color: Theme.of(context) + .colorScheme + .onSurfaceVariant, + ), + ), + ), + ), + ), + const SizedBox(width: 20), + ], + ), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildBody(LoadingState loadingState) { + return switch (loadingState) { + Loading() => SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + mainAxisSpacing: 2, + maxCrossAxisExtent: Grid.mediumCardWidth * 2, + childAspectRatio: StyleString.aspectRatio * 2.2, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + return const VideoCardHSkeleton(); + }, + childCount: 10, + ), + ), + Success() => (loadingState.response as List?)?.isNotEmpty == true + ? SliverPadding( + padding: EdgeInsets.only( + bottom: MediaQuery.paddingOf(context).bottom + 80), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( + mainAxisSpacing: 2, + maxCrossAxisExtent: Grid.mediumCardWidth * 2, + childAspectRatio: StyleString.aspectRatio * 2.6, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == loadingState.response.length - 1) { + _favPgcController.onLoadMore(); + } + return FavPgcItem( + item: loadingState.response[index], + ctr: _favPgcController, + onSelect: () { + _favPgcController.onSelect(index); + }, + onUpdateStatus: () { + showPgcFollowDialog( + context: context, + type: widget.type == 0 ? '追番' : '追剧', + followStatus: widget.followStatus, + onUpdateStatus: (followStatus) { + if (followStatus == -1) { + _favPgcController.bangumiDel( + index, + loadingState.response[index].seasonId, + ); + } else { + _favPgcController.bangumiUpdate( + index, + followStatus, + loadingState.response[index].seasonId, + ); + } + }, + ); + }, + ); + }, + childCount: loadingState.response.length, + ), + ), + ) + : HttpError(callback: _favPgcController.onReload), + Error() => HttpError( + errMsg: loadingState.errMsg, + callback: _favPgcController.onReload, + ), + LoadingState() => throw UnimplementedError(), + }; + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/pages/fav/pgc/controller.dart b/lib/pages/fav/pgc/controller.dart new file mode 100644 index 00000000..e0a1ba5d --- /dev/null +++ b/lib/pages/fav/pgc/controller.dart @@ -0,0 +1,134 @@ +import 'package:PiliPlus/http/bangumi.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/video.dart'; +import 'package:PiliPlus/models/bangumi/list.dart'; +import 'package:PiliPlus/pages/common/multi_select_controller.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class FavPgcController extends MultiSelectController { + final int type; + final int followStatus; + + FavPgcController(this.type, this.followStatus); + + late final allSelected = false.obs; + + @override + void onInit() { + super.onInit(); + queryData(); + } + + @override + onSelect(int index) { + List list = (loadingState.value as Success).response; + list[index].checked = !(list[index].checked ?? false); + checkedCount.value = list.where((item) => item.checked == true).length; + loadingState.value = LoadingState.success(list); + allSelected.value = checkedCount.value == list.length; + if (checkedCount.value == 0) { + enableMultiSelect.value = false; + } + } + + @override + void handleSelect([bool checked = false]) { + allSelected.value = checked; + if (loadingState.value is Success) { + List list = + (loadingState.value as Success).response; + if (list.isNotEmpty) { + loadingState.value = LoadingState.success(list + .map((item) => item..checked = checked) + .toList()); + checkedCount.value = checked ? list.length : 0; + } + } + } + + @override + Future customGetData() => BangumiHttp.bangumiFollowList( + mid: Accounts.main.mid, + type: type, + followStatus: followStatus, + pn: currentPage, + ); + + void onDisable() { + if (checkedCount.value != 0) { + handleSelect(); + } + enableMultiSelect.value = false; + } + + // 取消追番 + Future bangumiDel(index, seasonId) async { + var result = await VideoHttp.bangumiDel(seasonId: seasonId); + if (result['status']) { + List list = + (loadingState.value as Success).response; + list.removeAt(index); + loadingState.value = LoadingState.success(list); + } + SmartDialog.showToast(result['msg']); + } + + Future onUpdate(followStatus) async { + List dataList = + (loadingState.value as Success).response as List; + Set updateList = + dataList.where((item) => item.checked == true).toSet(); + final res = await VideoHttp.bangumiUpdate( + seasonId: updateList.map((item) => item.seasonId).toList(), + status: followStatus, + ); + if (res['status']) { + List remainList = + dataList.toSet().difference(updateList).toList(); + loadingState.value = LoadingState.success(remainList); + enableMultiSelect.value = false; + try { + final ctr = Get.find(tag: '$type$followStatus'); + if (ctr.loadingState.value is Success) { + List list = + (ctr.loadingState.value as Success).response; + list.insertAll(0, updateList.map((item) => item..checked = null)); + ctr.loadingState.value = LoadingState.success(list); + ctr.allSelected.value = false; + } + } catch (e) { + debugPrint('fav pgc onUpdate: $e'); + } + } + SmartDialog.showToast(res['msg']); + } + + Future bangumiUpdate(index, followStatus, seasonId) async { + var result = await VideoHttp.bangumiUpdate( + seasonId: [seasonId], + status: followStatus, + ); + if (result['status']) { + List list = + (loadingState.value as Success).response; + final item = list.removeAt(index); + loadingState.value = LoadingState.success(list); + try { + final ctr = Get.find(tag: '$type$followStatus'); + if (ctr.loadingState.value is Success) { + List list = + (ctr.loadingState.value as Success).response; + list.insert(0, item); + ctr.loadingState.value = LoadingState.success(list); + ctr.allSelected.value = false; + } + } catch (e) { + debugPrint('fav pgc bangumiUpdate: $e'); + } + } + SmartDialog.showToast(result['msg']); + } +} diff --git a/lib/pages/fav/pgc/view.dart b/lib/pages/fav/pgc/view.dart new file mode 100644 index 00000000..c6feb6d5 --- /dev/null +++ b/lib/pages/fav/pgc/view.dart @@ -0,0 +1,93 @@ +import 'package:PiliPlus/pages/fav/pgc/child_view.dart'; +import 'package:flutter/material.dart'; + +class FavPgcPage extends StatefulWidget { + const FavPgcPage({super.key, required this.type}); + + final int type; + + @override + State createState() => _FavPgcPageState(); +} + +class _FavPgcPageState extends State + with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin { + late final TabController _tabController = + TabController(length: 3, vsync: this, initialIndex: 1); + + @override + bool get wantKeepAlive => true; + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: TabBar( + overlayColor: WidgetStateProperty.all(Colors.transparent), + splashFactory: NoSplash.splashFactory, + isScrollable: true, + tabAlignment: TabAlignment.start, + controller: _tabController, + padding: const EdgeInsets.symmetric(horizontal: 8), + dividerHeight: 0, + indicatorWeight: 0, + indicatorPadding: + const EdgeInsets.symmetric(horizontal: 3, vertical: 8), + indicator: BoxDecoration( + color: Theme.of(context).colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(20), + ), + indicatorSize: TabBarIndicatorSize.tab, + labelStyle: TabBarTheme.of(context) + .labelStyle + ?.copyWith(fontSize: 14) ?? + const TextStyle(fontSize: 14), + labelColor: Theme.of(context).colorScheme.onSecondaryContainer, + unselectedLabelColor: Theme.of(context).colorScheme.outline, + tabs: const [ + Tab(text: '想看'), + Tab(text: '在看'), + Tab(text: '看过'), + ], + ), + ), + // TextButton( + // style: TextButton.styleFrom( + // foregroundColor: Theme.of(context).colorScheme.onSurfaceVariant, + // visualDensity: VisualDensity(horizontal: -2, vertical: -2), + // tapTargetSize: MaterialTapTargetSize.shrinkWrap, + // ), + // onPressed: () {}, + // child: const Text('管理'), + // ), + // const SizedBox(width: 12), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + physics: const NeverScrollableScrollPhysics(), + children: List.generate( + 3, + (index) => FavPgcChildPage( + type: widget.type, + followStatus: index + 1, + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/fav/pgc/widget/item.dart b/lib/pages/fav/pgc/widget/item.dart new file mode 100644 index 00000000..935156d9 --- /dev/null +++ b/lib/pages/fav/pgc/widget/item.dart @@ -0,0 +1,186 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/badge.dart'; +import 'package:PiliPlus/common/widgets/icon_button.dart'; +import 'package:PiliPlus/common/widgets/network_img_layer.dart'; +import 'package:PiliPlus/models/bangumi/list.dart'; +import 'package:PiliPlus/pages/common/multi_select_controller.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/material.dart'; + +class FavPgcItem extends StatelessWidget { + const FavPgcItem({ + super.key, + required this.item, + required this.ctr, + required this.onSelect, + required this.onUpdateStatus, + }); + + final BangumiListItemModel item; + final MultiSelectController ctr; + final VoidCallback onSelect; + final VoidCallback onUpdateStatus; + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: Stack( + clipBehavior: Clip.none, + children: [ + InkWell( + onTap: () { + if (ctr.enableMultiSelect.value) { + onSelect(); + return; + } + Utils.viewBangumi(seasonId: item.seasonId); + }, + onLongPress: () { + if (!ctr.enableMultiSelect.value) { + ctr.enableMultiSelect.value = true; + onSelect(); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: StyleString.safeSpace, + vertical: 5, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AspectRatio( + aspectRatio: 3 / 4, + child: LayoutBuilder( + builder: (BuildContext context, + BoxConstraints boxConstraints) { + return Stack( + clipBehavior: Clip.none, + children: [ + NetworkImgLayer( + radius: 4, + src: item.cover, + width: boxConstraints.maxWidth, + height: boxConstraints.maxHeight, + ), + if (item.badge?.isNotEmpty == true) + PBadge( + right: 4, + top: 4, + text: item.badge, + ), + Positioned.fill( + child: IgnorePointer( + child: LayoutBuilder( + builder: (context, constraints) => + AnimatedOpacity( + opacity: item.checked == true ? 1 : 0, + duration: const Duration(milliseconds: 200), + child: Container( + alignment: Alignment.center, + height: constraints.maxHeight, + width: constraints.maxHeight * + StyleString.aspectRatio, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Colors.black.withOpacity(0.6), + ), + child: SizedBox( + width: 34, + height: 34, + child: AnimatedScale( + scale: item.checked == true ? 1 : 0, + duration: + const Duration(milliseconds: 250), + curve: Curves.easeInOut, + child: IconButton( + tooltip: '取消选择', + style: ButtonStyle( + padding: WidgetStateProperty.all( + EdgeInsets.zero), + backgroundColor: + WidgetStateProperty + .resolveWith( + (states) { + return Theme.of(context) + .colorScheme + .surface + .withOpacity(0.8); + }, + ), + ), + onPressed: null, + icon: Icon( + Icons.done_all_outlined, + color: Theme.of(context) + .colorScheme + .primary, + ), + ), + ), + ), + ), + ), + ), + ), + ), + ], + ); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(item.title!), + if (item.newEp?['index_show'] != null) ...[ + const SizedBox(height: 6), + Text( + '${item.newEp?['index_show']}', + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.outline, + ), + ), + ], + if (item.progress != null) ...[ + SizedBox( + height: + item.newEp?['index_show'] != null ? 2 : 6), + Text( + item.progress!, + style: TextStyle( + fontSize: 13, + color: Theme.of(context).colorScheme.outline, + ), + ), + ], + ], + ), + ), + ], + ), + ), + ), + Positioned( + right: 12, + bottom: 0, + child: iconButton( + iconSize: 22, + context: context, + onPressed: onUpdateStatus, + icon: Icons.more_vert, + iconColor: Theme.of(context).colorScheme.onSurfaceVariant, + bgColor: Colors.transparent, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart index af3b3e73..5c32cb01 100644 --- a/lib/pages/fav/view.dart +++ b/lib/pages/fav/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/pages/fav/note/view.dart'; +import 'package:PiliPlus/pages/fav/pgc/view.dart'; import 'package:PiliPlus/pages/fav/video/index.dart'; import 'package:PiliPlus/pages/fav_search/view.dart'; import 'package:flutter/material.dart'; @@ -93,8 +94,8 @@ class _FavPageState extends State with SingleTickerProviderStateMixin { .map( (item) => switch (item) { _FavType.video => const FavVideoPage(), - _FavType.bangumi => Center(child: Text(item.title)), - _FavType.cinema => Center(child: Text(item.title)), + _FavType.bangumi => const FavPgcPage(type: 1), + _FavType.cinema => const FavPgcPage(type: 2), _FavType.article => Center(child: Text(item.title)), _FavType.note => const FavNotePage(), },