From 8d633377ae95b13847c7b5f6aca03656a5fb1dd5 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Sat, 12 Apr 2025 18:50:19 +0800 Subject: [PATCH] feat: sort fav folder Signed-off-by: bggRGjQaUbCoE --- lib/http/api.dart | 2 + lib/http/user.dart | 22 +++ lib/pages/fav/video/controller.dart | 2 +- lib/pages/fav/video/fav_folder_sort_page.dart | 138 ++++++++++++++++++ lib/pages/fav/video/widgets/item.dart | 20 ++- lib/pages/fav/view.dart | 127 ++++++++++------ lib/pages/fav_detail/fav_sort_page.dart | 2 +- 7 files changed, 258 insertions(+), 55 deletions(-) create mode 100644 lib/pages/fav/video/fav_folder_sort_page.dart diff --git a/lib/http/api.dart b/lib/http/api.dart index 0952fa0b..b19088f5 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -116,6 +116,8 @@ class Api { static const String sortFav = '/x/v3/fav/resource/sort'; + static const String sortFavFolder = '/x/v3/fav/folder/sort'; + // 判断视频是否被收藏(双端)GET /// aid // https://api.bilibili.com/x/v2/fav/video/favoured diff --git a/lib/http/user.dart b/lib/http/user.dart index 1394e9a3..dd808a05 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -66,6 +66,28 @@ class UserHttp { } } + static Future sortFavFolder({ + required List sort, + }) async { + Map data = { + 'sort': sort.join(','), + 'csrf': await Request.getCsrf(), + }; + Utils.appSign(data); + var res = await Request().post( + Api.sortFavFolder, + 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 sortFav({ required dynamic mediaId, required List sort, diff --git a/lib/pages/fav/video/controller.dart b/lib/pages/fav/video/controller.dart index 183b463b..aef8e167 100644 --- a/lib/pages/fav/video/controller.dart +++ b/lib/pages/fav/video/controller.dart @@ -39,7 +39,7 @@ class FavController @override Future> customGetData() => UserHttp.userfavFolder( pn: currentPage, - ps: 10, + ps: 20, mid: mid, ); } diff --git a/lib/pages/fav/video/fav_folder_sort_page.dart b/lib/pages/fav/video/fav_folder_sort_page.dart new file mode 100644 index 00000000..1a996d7d --- /dev/null +++ b/lib/pages/fav/video/fav_folder_sort_page.dart @@ -0,0 +1,138 @@ +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/http/user.dart'; +import 'package:PiliPlus/models/user/fav_folder.dart'; +import 'package:PiliPlus/pages/fav/video/controller.dart'; +import 'package:PiliPlus/pages/fav/video/widgets/item.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 FavFolderSortPage extends StatefulWidget { + const FavFolderSortPage({super.key, required this.favController}); + + final FavController favController; + + @override + State createState() => _FavFolderSortPageState(); +} + +class _FavFolderSortPageState extends State { + FavController get _favController => widget.favController; + + final GlobalKey _key = GlobalKey(); + late List sortList = List.from( + (_favController.loadingState.value as Success).response); + + final ScrollController _scrollController = ScrollController(); + + void listener() { + if (_favController.isEnd) { + return; + } + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 200) { + _favController.onLoadMore().then((_) { + try { + if (_favController.loadingState.value is Success) { + List list = + (_favController.loadingState.value as Success).response; + sortList.addAll(list.sublist(sortList.length)); + if (mounted) { + setState(() {}); + } + } + } catch (_) {} + }); + } + } + + @override + void initState() { + super.initState(); + if (_favController.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('收藏夹排序'), + actions: [ + TextButton( + onPressed: () async { + dynamic res = await UserHttp.sortFavFolder( + sort: sortList.map((item) => item.id).toList(), + ); + if (res['status']) { + SmartDialog.showToast('排序完成'); + _favController.loadingState.value = + LoadingState.success(sortList); + Get.back(); + } else { + SmartDialog.showToast(res['msg']); + } + }, + child: const Text('完成'), + ), + const SizedBox(width: 16), + ], + ), + body: _buildBody, + ); + } + + void onReorder(int oldIndex, int newIndex) { + if (oldIndex == 0 || newIndex == 0) { + SmartDialog.showToast('默认收藏夹不支持排序'); + return; + } + + if (newIndex > oldIndex) { + newIndex -= 1; + } + + final tabsItem = sortList.removeAt(oldIndex); + sortList.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.generate( + sortList.length, + (index) { + final item = sortList[index]; + final key = item.id.toString(); + return FavItem( + key: Key(key), + heroTag: key, + favFolderItem: item, + onLongPress: index == 0 + ? () { + SmartDialog.showToast('默认收藏夹不支持排序'); + } + : null, + ); + }, + ), + ); + } +} diff --git a/lib/pages/fav/video/widgets/item.dart b/lib/pages/fav/video/widgets/item.dart index 445fdb4f..a093792a 100644 --- a/lib/pages/fav/video/widgets/item.dart +++ b/lib/pages/fav/video/widgets/item.dart @@ -7,10 +7,13 @@ import 'package:PiliPlus/common/widgets/network_img_layer.dart'; class FavItem extends StatelessWidget { final String heroTag; final dynamic favFolderItem; - final GestureTapCallback onTap; + final VoidCallback? onTap; + final VoidCallback? onLongPress; + const FavItem({ super.key, - required this.onTap, + this.onTap, + this.onLongPress, required this.heroTag, required this.favFolderItem, }); @@ -19,11 +22,14 @@ class FavItem extends StatelessWidget { Widget build(BuildContext context) { return InkWell( onTap: onTap, - onLongPress: () => imageSaveDialog( - context: context, - title: favFolderItem.title, - cover: favFolderItem.cover, - ), + onLongPress: onLongPress ?? + (onTap == null + ? null + : () => imageSaveDialog( + context: context, + title: favFolderItem.title, + cover: favFolderItem.cover, + )), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), child: LayoutBuilder( diff --git a/lib/pages/fav/view.dart b/lib/pages/fav/view.dart index 8ef4927b..7e2b689f 100644 --- a/lib/pages/fav/view.dart +++ b/lib/pages/fav/view.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/models/user/fav_folder.dart'; import 'package:PiliPlus/pages/fav/article/view.dart'; import 'package:PiliPlus/pages/fav/note/view.dart'; import 'package:PiliPlus/pages/fav/pgc/view.dart'; +import 'package:PiliPlus/pages/fav/video/fav_folder_sort_page.dart'; import 'package:PiliPlus/pages/fav/video/index.dart'; import 'package:PiliPlus/pages/fav_search/view.dart'; import 'package:flutter/material.dart'; @@ -31,15 +32,29 @@ class FavPage extends StatefulWidget { } class _FavPageState extends State with SingleTickerProviderStateMixin { - late final TabController _tabController = TabController( - length: _FavType.values.length, - vsync: this, - initialIndex: Get.arguments is int ? Get.arguments : 0, - ); + late final TabController _tabController; final FavController _favController = Get.put(FavController()); + late final RxInt _tabIndex; + + void listener() { + _tabIndex.value = _tabController.index; + } + + @override + void initState() { + super.initState(); + _tabIndex = (Get.arguments is int ? Get.arguments as int : 0).obs; + _tabController = TabController( + length: _FavType.values.length, + vsync: this, + initialIndex: _tabIndex.value, + ); + _tabController.addListener(listener); + } @override void dispose() { + _tabController.removeListener(listener); _tabController.dispose(); super.dispose(); } @@ -50,48 +65,68 @@ class _FavPageState extends State with SingleTickerProviderStateMixin { appBar: AppBar( title: const Text('我的收藏'), actions: [ - IconButton( - onPressed: () { - Get.toNamed('/createFav')?.then( - (data) { - if (data != null) { - List list = - _favController.loadingState.value is Success - ? (_favController.loadingState.value as Success) - .response - : []; - list.insert(list.isNotEmpty ? 1 : 0, data); - _favController.loadingState.refresh(); - } - }, - ); - }, - icon: const Icon(Icons.add), - tooltip: '新建收藏夹', - ), - IconButton( - onPressed: () { - if (_favController.loadingState.value is Success) { - try { - final item = (_favController.loadingState.value as Success) - .response - .first; - Get.toNamed( - '/favSearch', - arguments: { - 'type': 1, - 'mediaId': item.id, - 'title': item.title, - 'count': item.mediaCount, - 'searchType': SearchType.fav, - 'isOwner': true, + Obx( + () => _tabIndex.value == 0 + ? IconButton( + onPressed: () { + Get.toNamed('/createFav')?.then( + (data) { + if (data != null) { + List list = _favController + .loadingState.value is Success + ? (_favController.loadingState.value as Success) + .response + : []; + list.insert(list.isNotEmpty ? 1 : 0, data); + _favController.loadingState.refresh(); + } + }, + ); }, - ); - } catch (_) {} - } - }, - icon: const Icon(Icons.search_outlined), - tooltip: '搜索', + icon: const Icon(Icons.add), + tooltip: '新建收藏夹', + ) + : const SizedBox.shrink(), + ), + Obx( + () => _tabIndex.value == 0 + ? IconButton( + onPressed: () { + Get.to(FavFolderSortPage(favController: _favController)); + }, + icon: const Icon(Icons.sort), + tooltip: '收藏夹排序', + ) + : const SizedBox.shrink(), + ), + Obx( + () => _tabIndex.value == 0 + ? IconButton( + onPressed: () { + if (_favController.loadingState.value is Success) { + try { + final item = + (_favController.loadingState.value as Success) + .response + .first; + Get.toNamed( + '/favSearch', + arguments: { + 'type': 1, + 'mediaId': item.id, + 'title': item.title, + 'count': item.mediaCount, + 'searchType': SearchType.fav, + 'isOwner': true, + }, + ); + } catch (_) {} + } + }, + icon: const Icon(Icons.search_outlined), + tooltip: '搜索', + ) + : const SizedBox.shrink(), ), const SizedBox(width: 6), ], diff --git a/lib/pages/fav_detail/fav_sort_page.dart b/lib/pages/fav_detail/fav_sort_page.dart index 553129a3..7d728efd 100644 --- a/lib/pages/fav_detail/fav_sort_page.dart +++ b/lib/pages/fav_detail/fav_sort_page.dart @@ -39,7 +39,7 @@ class _FavSortPageState extends State { if (_favDetailController.loadingState.value is Success) { List list = (_favDetailController.loadingState.value as Success).response; - this.sortList.addAll(list.sublist(this.sortList.length)); + sortList.addAll(list.sublist(sortList.length)); if (mounted) { setState(() {}); }