From becb566ca8d80013e7164de325cfe647f3c44d01 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Wed, 26 Mar 2025 16:20:40 +0800 Subject: [PATCH] feat: sort fav Closes #530 Signed-off-by: bggRGjQaUbCoE --- lib/http/api.dart | 41 +++-- lib/http/user.dart | 27 +++- lib/pages/fav_detail/fav_sort_page.dart | 145 ++++++++++++++++++ lib/pages/fav_detail/view.dart | 28 ++++ .../fav_detail/widget/fav_video_card.dart | 102 ++++++------ 5 files changed, 273 insertions(+), 70 deletions(-) create mode 100644 lib/pages/fav_detail/fav_sort_page.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index eeba5cfb..716c7cf9 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -81,6 +81,18 @@ class Api { /// https://api.bilibili.com/x/web-interface/archive/coins static const String hasCoinVideo = '/x/web-interface/archive/coins'; + /// 收藏夹 详情 + /// media_id 当前收藏夹id 搜索全部时为默认收藏夹id + /// pn int 当前页 + /// ps int pageSize + /// keyword String 搜索词 + /// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿 + /// tid int 分区id + /// platform web + /// type 0 当前收藏夹 1 全部收藏夹 + // https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0 + static const String favResourceList = '/x/v3/fav/resource/list'; + // 收藏视频(双端)POST // access_key str APP登录Token APP方式必要 /// rid num 稿件avid 必要 @@ -96,6 +108,14 @@ class Api { static const String delFav = '/x/v3/fav/resource/batch-del'; + static const String copyFav = '/x/v3/fav/resource/copy'; + + static const String moveFav = '/x/v3/fav/resource/move'; + + static const String cleanFav = '/x/v3/fav/resource/clean'; + + static const String sortFav = '/x/v3/fav/resource/sort'; + // 判断视频是否被收藏(双端)GET /// aid // https://api.bilibili.com/x/v2/fav/video/favoured @@ -123,10 +143,6 @@ class Api { // rid num 目标 视频稿件avid static const String favFolder = '/x/v3/fav/folder/created/list-all'; - static const String copyFav = '/x/v3/fav/resource/copy'; - - static const String moveFav = '/x/v3/fav/resource/move'; - static const String copyToview = '/x/v2/history/toview/copy'; static const String moveToview = '/x/v2/history/toview/move'; @@ -192,20 +208,6 @@ class Api { static const String deleteFolder = '/x/v3/fav/folder/del'; - static const String cleanFav = '/x/v3/fav/resource/clean'; - - /// 收藏夹 详情 - /// media_id 当前收藏夹id 搜索全部时为默认收藏夹id - /// pn int 当前页 - /// ps int pageSize - /// keyword String 搜索词 - /// order String 排序方式 view 最多播放 mtime 最近收藏 pubtime 最近投稿 - /// tid int 分区id - /// platform web - /// type 0 当前收藏夹 1 全部收藏夹 - // https://api.bilibili.com/x/v3/fav/resource/list?media_id=76614671&pn=1&ps=20&keyword=&order=mtime&type=0&tid=0 - static const String userFavFolderDetail = '/x/v3/fav/resource/list'; - // 正在直播的up & 关注的up // https://api.bilibili.com/x/polymer/web-dynamic/v1/portal static const String followUp = '/x/polymer/web-dynamic/v1/portal'; @@ -674,9 +676,6 @@ class Api { /// 我的订阅-合集详情 static const favSeasonList = '/x/space/fav/season/list'; - /// 我的订阅-播单详情 - static const favResourceList = '/x/v3/fav/resource/list'; - /// 发送私信 static const String sendMsg = '${HttpString.tUrl}/web_im/v1/web_im/send_msg'; diff --git a/lib/http/user.dart b/lib/http/user.dart index 593789f1..1b11702f 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/video/later.dart'; +import 'package:PiliPlus/utils/utils.dart'; import 'package:dio/dio.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import '../common/constants.dart'; @@ -62,6 +63,30 @@ class UserHttp { } } + static Future sortFav({ + required dynamic mediaId, + required List sort, + }) async { + Map data = { + 'media_id': mediaId, + 'sort': sort.join(','), + 'csrf': await Request.getCsrf(), + }; + Utils.appSign(data); + var res = await Request().post( + Api.sortFav, + data: data, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + if (res.data['code'] == 0) { + return {'status': true, 'data': res.data['data']}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } + static Future cleanFav({ required dynamic mediaId, }) async { @@ -152,7 +177,7 @@ class UserHttp { String keyword = '', String order = 'mtime', int type = 0}) async { - var res = await Request().get(Api.userFavFolderDetail, queryParameters: { + var res = await Request().get(Api.favResourceList, queryParameters: { 'media_id': mediaId, 'pn': pn, 'ps': ps, diff --git a/lib/pages/fav_detail/fav_sort_page.dart b/lib/pages/fav_detail/fav_sort_page.dart new file mode 100644 index 00000000..b457c8f2 --- /dev/null +++ b/lib/pages/fav_detail/fav_sort_page.dart @@ -0,0 +1,145 @@ +import 'package:PiliPlus/build_config.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/user.dart'; +import 'package:PiliPlus/pages/fav_detail/controller.dart'; +import 'package:PiliPlus/pages/fav_detail/widget/fav_video_card.dart'; +import 'package:PiliPlus/utils/extension.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class FavSortPage extends StatefulWidget { + const FavSortPage({super.key, required this.favDetailController}); + + final FavDetailController favDetailController; + + @override + State createState() => _FavSortPageState(); +} + +class _FavSortPageState extends State { + FavDetailController get _favDetailController => widget.favDetailController; + + final GlobalKey _key = GlobalKey(); + late List list = + List.from((_favDetailController.loadingState.value as Success).response); + List sort = []; + + final ScrollController _scrollController = ScrollController(); + + void listener() { + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 200) { + _favDetailController.onLoadMore().then((_) { + try { + if (_favDetailController.loadingState.value is Success) { + List list = + (_favDetailController.loadingState.value as Success).response; + this.list.addAll(list.sublist(this.list.length)); + if (mounted) { + setState(() {}); + } + } + } catch (_) {} + }); + } + } + + @override + void initState() { + super.initState(); + if (_favDetailController.isEnd.not) { + _scrollController.addListener(listener); + } + } + + @override + void dispose() { + _scrollController.removeListener(listener); + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('排序: ${_favDetailController.item.value.title}'), + actions: [ + TextButton( + onPressed: () async { + if (sort.isEmpty) { + Get.back(); + return; + } + dynamic res = await UserHttp.sortFav( + mediaId: _favDetailController.mediaId, + sort: sort, + ); + if (res['status']) { + SmartDialog.showToast('排序完成'); + _favDetailController.loadingState.value = + LoadingState.success(list); + Get.back(); + } else { + SmartDialog.showToast(res['msg']); + } + }, + child: const Text('完成'), + ), + const SizedBox(width: 16), + ], + ), + body: _buildBody, + ); + } + + void onReorder(int oldIndex, int newIndex) { + if (newIndex > oldIndex) { + newIndex -= 1; + } + + final oldItem = list[oldIndex]; + final newItem = + list.getOrNull(oldIndex > newIndex ? newIndex - 1 : newIndex); + sort.add( + '${newItem == null ? '0:0' : '${newItem.id}:${newItem.type}'}:${oldItem.id}:${oldItem.type}'); + + final tabsItem = list.removeAt(oldIndex); + list.insert(newIndex, tabsItem); + + setState(() {}); + } + + Widget get _buildBody { + return ReorderableListView( + key: _key, + scrollController: _scrollController, + onReorder: onReorder, + physics: const AlwaysScrollableScrollPhysics(), + footer: SizedBox( + height: MediaQuery.of(context).padding.bottom + 80, + ), + children: list + .map( + (item) => Stack( + key: Key(item.id.toString()), + children: [ + FavVideoCardH( + isSort: true, + videoItem: item, + isOwner: false, + ), + if (BuildConfig.isDebug) + Positioned( + top: 35, + right: 10, + child: Text(item.id.toString()), + ) + ], + ), + ) + .toList(), + ); + } +} diff --git a/lib/pages/fav_detail/view.dart b/lib/pages/fav_detail/view.dart index 0f141b08..5beb2ab8 100644 --- a/lib/pages/fav_detail/view.dart +++ b/lib/pages/fav_detail/view.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/models/user/fav_detail.dart'; import 'package:PiliPlus/models/user/fav_folder.dart'; +import 'package:PiliPlus/pages/fav_detail/fav_sort_page.dart'; import 'package:PiliPlus/pages/fav_search/view.dart' show SearchType; import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/utils.dart'; @@ -240,6 +241,33 @@ class _FavDetailPageState extends State { }, child: Text('清除失效内容'), ), + PopupMenuItem( + onTap: () { + if (_favDetailController.loadingState + .value is Success && + ((_favDetailController + .loadingState + .value as Success) + .response as List?) + ?.isNotEmpty == + true) { + if ((_favDetailController.item.value + .mediaCount ?? + 0) > + 1000) { + SmartDialog.showToast( + '内容太多啦!超过1000不支持排序'); + return; + } + Get.to( + FavSortPage( + favDetailController: + _favDetailController), + ); + } + }, + child: Text('排序'), + ), if (!Utils.isDefault(_favDetailController .item.value.attr ?? 0)) diff --git a/lib/pages/fav_detail/widget/fav_video_card.dart b/lib/pages/fav_detail/widget/fav_video_card.dart index 15059b3a..872980db 100644 --- a/lib/pages/fav_detail/widget/fav_video_card.dart +++ b/lib/pages/fav_detail/widget/fav_video_card.dart @@ -22,7 +22,8 @@ class FavVideoCardH extends StatelessWidget { final GestureTapCallback? onTap; final GestureLongPressCallback? onLongPress; final bool isOwner; - final VoidCallback onViewFav; + final VoidCallback? onViewFav; + final bool? isSort; const FavVideoCardH({ super.key, @@ -32,7 +33,8 @@ class FavVideoCardH extends StatelessWidget { this.onTap, this.onLongPress, this.isOwner = false, - required this.onViewFav, + this.onViewFav, + this.isSort, }); @override @@ -40,53 +42,57 @@ class FavVideoCardH extends StatelessWidget { int id = videoItem.id!; String bvid = videoItem.bvid ?? IdUtils.av2bv(id); return InkWell( - onTap: () async { - if (onTap != null) { - onTap!(); - return; - } - String? epId; - if (videoItem.type == 24) { - videoItem.cid = await SearchHttp.ab2c(bvid: bvid); - dynamic seasonId = videoItem.ogv!['season_id']; - epId = videoItem.epId; - Utils.viewBangumi(seasonId: seasonId, epId: epId); - return; - } else if (videoItem.page == 0 || videoItem.page! > 1) { - var result = await VideoHttp.videoIntro(bvid: bvid); - if (result['status']) { - epId = result['data'].epId; - } else { - SmartDialog.showToast(result['msg']); - } - } + onTap: isSort == true + ? null + : () async { + if (onTap != null) { + onTap!(); + return; + } + String? epId; + if (videoItem.type == 24) { + videoItem.cid = await SearchHttp.ab2c(bvid: bvid); + dynamic seasonId = videoItem.ogv!['season_id']; + epId = videoItem.epId; + Utils.viewBangumi(seasonId: seasonId, epId: epId); + return; + } else if (videoItem.page == 0 || videoItem.page! > 1) { + var result = await VideoHttp.videoIntro(bvid: bvid); + if (result['status']) { + epId = result['data'].epId; + } else { + SmartDialog.showToast(result['msg']); + } + } - if ([0, 16].contains(videoItem.attr).not) { - Get.toNamed('/member?mid=${videoItem.owner.mid}'); - return; - } - onViewFav(); - // Utils.toViewPage( - // 'bvid=$bvid&cid=${videoItem.cid}${epId?.isNotEmpty == true ? '&epId=$epId' : ''}', - // arguments: { - // 'videoItem': videoItem, - // 'heroTag': Utils.makeHeroTag(id), - // 'videoType': - // epId != null ? SearchType.media_bangumi : SearchType.video, - // }, - // ); - }, - onLongPress: () { - if (onLongPress != null) { - onLongPress!(); - } else { - imageSaveDialog( - context: context, - title: videoItem.title, - cover: videoItem.pic, - ); - } - }, + if ([0, 16].contains(videoItem.attr).not) { + Get.toNamed('/member?mid=${videoItem.owner.mid}'); + return; + } + onViewFav?.call(); + // Utils.toViewPage( + // 'bvid=$bvid&cid=${videoItem.cid}${epId?.isNotEmpty == true ? '&epId=$epId' : ''}', + // arguments: { + // 'videoItem': videoItem, + // 'heroTag': Utils.makeHeroTag(id), + // 'videoType': + // epId != null ? SearchType.media_bangumi : SearchType.video, + // }, + // ); + }, + onLongPress: isSort == true + ? null + : () { + if (onLongPress != null) { + onLongPress!(); + } else { + imageSaveDialog( + context: context, + title: videoItem.title, + cover: videoItem.pic, + ); + } + }, child: Padding( padding: const EdgeInsets.symmetric( horizontal: StyleString.safeSpace,