From 90c8aeb05d140457a10ab9563b9496328afacd70 Mon Sep 17 00:00:00 2001 From: bggRGjQaUbCoE Date: Mon, 24 Mar 2025 18:52:10 +0800 Subject: [PATCH] mod: history: full type Closes #473 Signed-off-by: bggRGjQaUbCoE --- lib/http/init.dart | 4 +- lib/http/user.dart | 3 +- lib/pages/common/multi_select_controller.dart | 4 +- lib/pages/history/base_controller.dart | 74 ++++ lib/pages/history/controller.dart | 125 +++--- lib/pages/history/view.dart | 393 ++++++++++++------ lib/pages/history/widgets/item.dart | 4 +- 7 files changed, 396 insertions(+), 211 deletions(-) create mode 100644 lib/pages/history/base_controller.dart diff --git a/lib/http/init.dart b/lib/http/init.dart index aa9fa539..8015c4dd 100644 --- a/lib/http/init.dart +++ b/lib/http/init.dart @@ -96,9 +96,9 @@ class Request { //请求基地址,可以包含子路径 baseUrl: HttpString.apiBaseUrl, //连接服务器超时时间,单位是毫秒. - connectTimeout: const Duration(milliseconds: 4000), + connectTimeout: const Duration(milliseconds: 10000), //响应流上前后两次接受到数据的间隔,单位为毫秒。 - receiveTimeout: const Duration(milliseconds: 4000), + receiveTimeout: const Duration(milliseconds: 10000), //Http请求头. headers: { 'connection': 'keep-alive', diff --git a/lib/http/user.dart b/lib/http/user.dart index b0f9b356..4139cd7e 100644 --- a/lib/http/user.dart +++ b/lib/http/user.dart @@ -196,11 +196,12 @@ class UserHttp { // 观看历史 static Future historyList({ + required String type, int? max, int? viewAt, }) async { var res = await Request().get(Api.historyList, queryParameters: { - 'type': 'all', + 'type': type, 'ps': 20, 'max': max ?? 0, 'view_at': viewAt ?? 0, diff --git a/lib/pages/common/multi_select_controller.dart b/lib/pages/common/multi_select_controller.dart index 216c6608..d0dc6ac6 100644 --- a/lib/pages/common/multi_select_controller.dart +++ b/lib/pages/common/multi_select_controller.dart @@ -4,8 +4,8 @@ import 'package:get/get.dart'; import 'package:PiliPlus/utils/extension.dart'; abstract class MultiSelectController extends CommonController { - RxBool enableMultiSelect = false.obs; - RxInt checkedCount = 0.obs; + late final RxBool enableMultiSelect = false.obs; + late final RxInt checkedCount = 0.obs; onSelect(int index) { List list = (loadingState.value as Success).response; diff --git a/lib/pages/history/base_controller.dart b/lib/pages/history/base_controller.dart new file mode 100644 index 00000000..49bb6e19 --- /dev/null +++ b/lib/pages/history/base_controller.dart @@ -0,0 +1,74 @@ +import 'package:PiliPlus/http/user.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 HistoryBaseController extends GetxController { + RxBool pauseStatus = false.obs; + + RxBool enableMultiSelect = false.obs; + RxInt checkedCount = 0.obs; + +// 清空观看历史 + Future onClearHistory(BuildContext context, VoidCallback onSuccess) async { + await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('提示'), + content: const Text('啊叻?你要清空历史记录功能吗?'), + actions: [ + TextButton(onPressed: Get.back, child: const Text('取消')), + TextButton( + onPressed: () async { + Get.back(); + SmartDialog.showLoading(msg: '请求中'); + var res = await UserHttp.clearHistory(); + SmartDialog.dismiss(); + if (res.data['code'] == 0) { + SmartDialog.showToast('清空观看历史'); + onSuccess(); + } + }, + child: const Text('确认清空'), + ) + ], + ); + }, + ); + } + + // 暂停观看历史 + Future onPauseHistory(BuildContext context) async { + await showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: const Text('提示'), + content: + Text(!pauseStatus.value ? '啊叻?你要暂停历史记录功能吗?' : '啊叻?要恢复历史记录功能吗?'), + actions: [ + TextButton(onPressed: Get.back, child: const Text('取消')), + TextButton( + onPressed: () async { + SmartDialog.showLoading(msg: '请求中'); + var res = await UserHttp.pauseHistory(!pauseStatus.value); + SmartDialog.dismiss(); + if (res.data['code'] == 0) { + SmartDialog.showToast( + !pauseStatus.value ? '暂停观看历史' : '恢复观看历史'); + pauseStatus.value = !pauseStatus.value; + GStorage.localCache + .put(LocalCacheKey.historyPause, pauseStatus.value); + } + Get.back(); + }, + child: Text(!pauseStatus.value ? '确认暂停' : '确认恢复'), + ) + ], + ); + }, + ); + } +} diff --git a/lib/pages/history/controller.dart b/lib/pages/history/controller.dart index 340413aa..b99c0c20 100644 --- a/lib/pages/history/controller.dart +++ b/lib/pages/history/controller.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/pages/common/multi_select_controller.dart'; +import 'package:PiliPlus/pages/history/base_controller.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -8,8 +9,15 @@ import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/models/user/history.dart'; import 'package:PiliPlus/utils/storage.dart'; -class HistoryController extends MultiSelectController { - RxBool pauseStatus = false.obs; +class HistoryController extends MultiSelectController + with GetTickerProviderStateMixin { + HistoryController(this.type); + + late final baseCtr = Get.put(HistoryBaseController()); + + final String? type; + TabController? tabController; + late RxList tabs = [].obs; int? max; int? viewAt; @@ -28,13 +36,46 @@ class HistoryController extends MultiSelectController { return super.onRefresh(); } + @override + onSelect(int index) { + List list = (loadingState.value as Success).response; + list[index].checked = !(list[index]?.checked ?? false); + baseCtr.checkedCount.value = + list.where((item) => item.checked == true).length; + loadingState.value = LoadingState.success(list); + if (baseCtr.checkedCount.value == 0) { + baseCtr.enableMultiSelect.value = false; + } + } + + @override + void handleSelect([bool checked = false]) { + 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()); + baseCtr.checkedCount.value = checked ? list.length : 0; + } + } + if (checked.not) { + baseCtr.enableMultiSelect.value = false; + } + } + @override bool customHandleResponse(Success response) { HistoryData data = response.response; isEnd = data.list.isNullOrEmpty || data.list!.length < 20; max = data.list?.lastOrNull?.history?.oid; viewAt = data.list?.lastOrNull?.viewAt; - if (currentPage != 1 && loadingState.value is Success) { + if (currentPage == 1) { + if (type == null && tabs.isEmpty && data.tab?.isNotEmpty == true) { + tabs.value = data.tab!; + tabController = + TabController(length: data.tab!.length + 1, vsync: this); + } + } else if (loadingState.value is Success) { data.list ??= []; data.list!.insertAll( 0, @@ -45,79 +86,17 @@ class HistoryController extends MultiSelectController { return true; } - // 暂停观看历史 - Future onPauseHistory(BuildContext context) async { - await showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('提示'), - content: - Text(!pauseStatus.value ? '啊叻?你要暂停历史记录功能吗?' : '啊叻?要恢复历史记录功能吗?'), - actions: [ - TextButton(onPressed: Get.back, child: const Text('取消')), - TextButton( - onPressed: () async { - SmartDialog.showLoading(msg: '请求中'); - var res = await UserHttp.pauseHistory(!pauseStatus.value); - SmartDialog.dismiss(); - if (res.data['code'] == 0) { - SmartDialog.showToast( - !pauseStatus.value ? '暂停观看历史' : '恢复观看历史'); - pauseStatus.value = !pauseStatus.value; - GStorage.localCache - .put(LocalCacheKey.historyPause, pauseStatus.value); - } - Get.back(); - }, - child: Text(!pauseStatus.value ? '确认暂停' : '确认恢复'), - ) - ], - ); - }, - ); - } - // 观看历史暂停状态 Future historyStatus() async { var res = await UserHttp.historyStatus(); if (res['status']) { - pauseStatus.value = res['data']; + baseCtr.pauseStatus.value = res['data']; GStorage.localCache.put(LocalCacheKey.historyPause, res['data']); } else { SmartDialog.showToast(res['msg']); } } - // 清空观看历史 - Future onClearHistory(BuildContext context) async { - await showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: const Text('提示'), - content: const Text('啊叻?你要清空历史记录功能吗?'), - actions: [ - TextButton(onPressed: Get.back, child: const Text('取消')), - TextButton( - onPressed: () async { - SmartDialog.showLoading(msg: '请求中'); - var res = await UserHttp.clearHistory(); - SmartDialog.dismiss(); - if (res.data['code'] == 0) { - SmartDialog.showToast('清空观看历史'); - } - Get.back(); - loadingState.value = LoadingState.success([]); - }, - child: const Text('确认清空'), - ) - ], - ); - }, - ); - } - // 删除某条历史记录 Future delHistory(kid, business) async { _onDelete(((loadingState.value as Success).response as List) @@ -155,9 +134,9 @@ class HistoryController extends MultiSelectController { } else { onReload(); } - if (enableMultiSelect.value) { - checkedCount.value = 0; - enableMultiSelect.value = false; + if (baseCtr.enableMultiSelect.value) { + baseCtr.checkedCount.value = 0; + baseCtr.enableMultiSelect.value = false; } } SmartDialog.dismiss(); @@ -201,5 +180,11 @@ class HistoryController extends MultiSelectController { @override Future customGetData() => - UserHttp.historyList(max: max, viewAt: viewAt); + UserHttp.historyList(type: type ?? 'all', max: max, viewAt: viewAt); + + @override + void onClose() { + tabController?.dispose(); + super.onClose(); + } } diff --git a/lib/pages/history/view.dart b/lib/pages/history/view.dart index 639f863c..1ed321a4 100644 --- a/lib/pages/history/view.dart +++ b/lib/pages/history/view.dart @@ -2,6 +2,7 @@ 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/pages/fav_search/view.dart' show SearchType; +import 'package:PiliPlus/pages/history/base_controller.dart'; import 'package:PiliPlus/utils/extension.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -13,139 +14,252 @@ import '../../utils/grid.dart'; import 'widgets/item.dart'; class HistoryPage extends StatefulWidget { - const HistoryPage({super.key}); + const HistoryPage({super.key, this.type}); + + final String? type; @override State createState() => _HistoryPageState(); } -class _HistoryPageState extends State { - final _historyController = Get.put(HistoryController()); +class _HistoryPageState extends State + with AutomaticKeepAliveClientMixin { + late final _historyController = Get.put( + HistoryController(widget.type), + tag: widget.type ?? 'all', + ); + + HistoryController get currCtr { + try { + if (_historyController.tabController != null && + _historyController.tabController!.index != 0) { + return Get.find( + tag: _historyController + .tabs[_historyController.tabController!.index - 1].type, + ); + } + } catch (_) { + return _historyController; + } + return _historyController; + } + + bool get enableMultiSelect => + _historyController.baseCtr.enableMultiSelect.value; + + @override + void dispose() { + Get.delete(); + super.dispose(); + } @override Widget build(BuildContext context) { - return Obx( - () => PopScope( - canPop: _historyController.enableMultiSelect.value.not, - onPopInvokedWithResult: (didPop, result) { - if (_historyController.enableMultiSelect.value) { - _historyController.handleSelect(); - } - }, - child: Scaffold( - appBar: AppBarWidget( - visible: _historyController.enableMultiSelect.value, - child1: AppBar( - title: const Text('观看记录'), - actions: [ - IconButton( - tooltip: '搜索', - onPressed: () => Get.toNamed( - '/favSearch', - arguments: { - 'searchType': SearchType.history, - }, - ), - icon: const Icon(Icons.search_outlined), - ), - PopupMenuButton( - onSelected: (String type) { - // 处理菜单项选择的逻辑 - switch (type) { - case 'pause': - _historyController.onPauseHistory(context); - break; - case 'clear': - _historyController.onClearHistory(context); - break; - case 'del': - _historyController.onDelHistory(); - break; - case 'multiple': - _historyController.enableMultiSelect.value = true; - break; - } - }, - itemBuilder: (BuildContext context) => - >[ - PopupMenuItem( - value: 'pause', - child: Obx( - () => Text( - !_historyController.pauseStatus.value - ? '暂停观看记录' - : '恢复观看记录', + super.build(context); + return widget.type != null + ? _buildPage + : Obx( + () => PopScope( + canPop: enableMultiSelect.not, + onPopInvokedWithResult: (didPop, result) { + if (enableMultiSelect) { + currCtr.handleSelect(); + } + }, + child: Scaffold( + appBar: widget.type != null + ? null + : AppBarWidget( + visible: enableMultiSelect, + child1: AppBar( + title: const Text('观看记录'), + actions: [ + IconButton( + tooltip: '搜索', + onPressed: () => Get.toNamed( + '/favSearch', + arguments: { + 'searchType': SearchType.history, + }, + ), + icon: const Icon(Icons.search_outlined), + ), + PopupMenuButton( + onSelected: (String type) { + // 处理菜单项选择的逻辑 + switch (type) { + case 'pause': + _historyController.baseCtr + .onPauseHistory(context); + break; + case 'clear': + _historyController.baseCtr + .onClearHistory(context, () { + _historyController.loadingState.value = + LoadingState.success(null); + if (_historyController.tabController != + null) { + for (final item + in _historyController.tabs) { + try { + Get.find( + tag: item.type) + .loadingState + .value = + LoadingState.success(null); + } catch (_) {} + } + } + }); + break; + case 'del': + currCtr.onDelHistory(); + break; + case 'multiple': + _historyController + .baseCtr.enableMultiSelect.value = true; + break; + } + }, + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + value: 'pause', + child: Obx( + () => Text( + !_historyController + .baseCtr.pauseStatus.value + ? '暂停观看记录' + : '恢复观看记录', + ), + ), + ), + const PopupMenuItem( + value: 'clear', + child: Text('清空观看记录'), + ), + const PopupMenuItem( + value: 'del', + child: Text('删除已看记录'), + ), + const PopupMenuItem( + value: 'multiple', + child: Text('多选删除'), + ), + ], + ), + const SizedBox(width: 6), + ], + ), + child2: AppBar( + leading: IconButton( + tooltip: '取消', + onPressed: () { + currCtr.handleSelect(); + }, + icon: const Icon(Icons.close_outlined), + ), + title: Obx( + () => Text( + '已选: ${_historyController.baseCtr.checkedCount.value}', + ), + ), + actions: [ + TextButton( + onPressed: () => currCtr.handleSelect(true), + child: const Text('全选'), + ), + TextButton( + onPressed: () => + currCtr.onDelCheckedHistory(context), + child: Text( + '删除', + style: TextStyle( + color: Theme.of(context).colorScheme.error), + ), + ), + const SizedBox(width: 6), + ], ), ), - ), - const PopupMenuItem( - value: 'clear', - child: Text('清空观看记录'), - ), - const PopupMenuItem( - value: 'del', - child: Text('删除已看记录'), - ), - const PopupMenuItem( - value: 'multiple', - child: Text('多选删除'), - ), - ], - ), - const SizedBox(width: 6), - ], - ), - child2: AppBar( - leading: IconButton( - tooltip: '取消', - onPressed: _historyController.handleSelect, - icon: const Icon(Icons.close_outlined), - ), - title: Obx( - () => Text( - '已选: ${_historyController.checkedCount.value}', + body: Obx( + () => _historyController.tabs.isNotEmpty + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TabBar( + controller: _historyController.tabController, + onTap: (value) { + if (_historyController + .tabController!.indexIsChanging.not) { + currCtr.scrollController.animToTop(); + } else { + if (enableMultiSelect) { + if (_historyController + .tabController!.previousIndex == + 0) { + _historyController.handleSelect(); + } else { + try { + Get.find( + tag: _historyController + .tabs[_historyController + .tabController! + .previousIndex - + 1] + .type) + .handleSelect(); + } catch (_) {} + } + } + } + }, + tabs: [ + Tab(text: '全部'), + ..._historyController.tabs.map( + (item) => Tab(text: item.name), + ), + ], + ), + Expanded( + child: Material( + color: Colors.transparent, + child: TabBarView( + physics: enableMultiSelect + ? const NeverScrollableScrollPhysics() + : null, + controller: _historyController.tabController, + children: [ + _buildPage, + ..._historyController.tabs.map( + (item) => HistoryPage(type: item.type), + ), + ], + ), + ), + ), + ], + ) + : _buildPage, ), ), - actions: [ - TextButton( - onPressed: () => _historyController.handleSelect(true), - child: const Text('全选'), - ), - TextButton( - onPressed: () => - _historyController.onDelCheckedHistory(context), - child: Text( - '删除', - style: - TextStyle(color: Theme.of(context).colorScheme.error), - ), - ), - const SizedBox(width: 6), - ], ), - ), - body: refreshIndicator( - onRefresh: () async { - await _historyController.onRefresh(); - }, - child: CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - controller: _historyController.scrollController, - slivers: [ - Obx(() => _buildBody(_historyController.loadingState.value)), - SliverToBoxAdapter( - child: SizedBox( - height: MediaQuery.of(context).padding.bottom + 80, - ), - ), - ], - ), - ), - ), - ), - ); + ); } + Widget get _buildPage => refreshIndicator( + onRefresh: () async { + await _historyController.onRefresh(); + }, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + controller: _historyController.scrollController, + slivers: [ + Obx(() => _buildBody(_historyController.loadingState.value)), + ], + ), + ); + Widget _buildBody(LoadingState loadingState) { return switch (loadingState) { Loading() => SliverGrid( @@ -162,24 +276,30 @@ class _HistoryPageState extends State { ), ), Success() => (loadingState.response as List?)?.isNotEmpty == true - ? SliverGrid( - gridDelegate: SliverGridDelegateWithExtentAndRatio( - mainAxisSpacing: 2, - maxCrossAxisExtent: Grid.mediumCardWidth * 2, - childAspectRatio: StyleString.aspectRatio * 2.2, + ? SliverPadding( + padding: EdgeInsets.only( + top: StyleString.safeSpace - 5, + bottom: MediaQuery.of(context).padding.bottom + 80, ), - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index == loadingState.response.length - 1) { - _historyController.onLoadMore(); - } - return HistoryItem( - videoItem: loadingState.response[index], - ctr: _historyController, - onChoose: () => _historyController.onSelect(index), - ); - }, - childCount: loadingState.response.length, + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithExtentAndRatio( + mainAxisSpacing: 2, + maxCrossAxisExtent: Grid.mediumCardWidth * 2, + childAspectRatio: StyleString.aspectRatio * 2.2, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == loadingState.response.length - 1) { + _historyController.onLoadMore(); + } + return HistoryItem( + videoItem: loadingState.response[index], + ctr: _historyController.baseCtr, + onChoose: () => _historyController.onSelect(index), + ); + }, + childCount: loadingState.response.length, + ), ), ) : HttpError( @@ -192,6 +312,9 @@ class _HistoryPageState extends State { LoadingState() => throw UnimplementedError(), }; } + + @override + bool get wantKeepAlive => true; } class AppBarWidget extends StatelessWidget implements PreferredSizeWidget { diff --git a/lib/pages/history/widgets/item.dart b/lib/pages/history/widgets/item.dart index 3ee671f9..918c6ee3 100644 --- a/lib/pages/history/widgets/item.dart +++ b/lib/pages/history/widgets/item.dart @@ -3,6 +3,7 @@ import 'package:PiliPlus/common/widgets/video_progress_indicator.dart'; import 'package:PiliPlus/models/user/history.dart'; import 'package:PiliPlus/pages/common/multi_select_controller.dart'; import 'package:PiliPlus/pages/fav_search/controller.dart'; +import 'package:PiliPlus/pages/history/base_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -35,7 +36,8 @@ class HistoryItem extends StatelessWidget { String bvid = videoItem.history.bvid ?? IdUtils.av2bv(aid); return InkWell( onTap: () async { - if (ctr is MultiSelectController && ctr!.enableMultiSelect.value) { + if ((ctr is MultiSelectController || ctr is HistoryBaseController) && + ctr!.enableMultiSelect.value) { feedBack(); onChoose?.call(); return;