diff --git a/lib/common/widgets/appbar/appbar.dart b/lib/common/widgets/appbar/appbar.dart index dd4f94ca..1d2701a6 100644 --- a/lib/common/widgets/appbar/appbar.dart +++ b/lib/common/widgets/appbar/appbar.dart @@ -4,7 +4,7 @@ import 'package:get/get.dart'; class MultiSelectAppBarWidget extends StatelessWidget implements PreferredSizeWidget { - final MultiSelectMixin ctr; + final MultiSelectBase ctr; final bool? visible; final AppBar child; final List? children; @@ -19,8 +19,7 @@ class MultiSelectAppBarWidget extends StatelessWidget @override Widget build(BuildContext context) { - final visible = this.visible ?? ctr.enableMultiSelect.value; - if (visible) { + if (visible ?? ctr.enableMultiSelect.value) { return AppBar( bottom: child.bottom, leading: IconButton( diff --git a/lib/common/widgets/select_mask.dart b/lib/common/widgets/select_mask.dart new file mode 100644 index 00000000..4755b5db --- /dev/null +++ b/lib/common/widgets/select_mask.dart @@ -0,0 +1,34 @@ +import 'package:PiliPlus/common/constants.dart'; +import 'package:flutter/material.dart'; + +Widget selectMask(ThemeData theme, bool checked) { + return AnimatedOpacity( + opacity: checked ? 1 : 0, + duration: const Duration(milliseconds: 200), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: StyleString.mdRadius, + color: Colors.black.withValues(alpha: 0.6), + ), + child: AnimatedScale( + scale: checked ? 1 : 0, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + child: Container( + width: 34, + height: 34, + decoration: BoxDecoration( + color: theme.colorScheme.surface.withValues(alpha: 0.8), + shape: BoxShape.circle, + ), + child: Icon( + Icons.done_all_outlined, + color: theme.colorScheme.primary, + semanticLabel: '取消选择', + ), + ), + ), + ), + ); +} diff --git a/lib/pages/common/common_search_page.dart b/lib/pages/common/common_search_page.dart index 884e4a0d..65999866 100644 --- a/lib/pages/common/common_search_page.dart +++ b/lib/pages/common/common_search_page.dart @@ -1,6 +1,8 @@ +import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/pages/common/common_search_controller.dart'; +import 'package:PiliPlus/pages/common/multi_select_controller.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -12,43 +14,33 @@ abstract class CommonSearchPageState extends State { CommonSearchController get controller; - List? extraActions; + List? get extraActions => null; + + List? get multiSelectChildren => null; @override Widget build(BuildContext context) { + if (controller case MultiSelectBase multiCtr) { + return Obx(() { + final enableMultiSelect = multiCtr.enableMultiSelect.value; + return PopScope( + canPop: !enableMultiSelect, + onPopInvokedWithResult: (didPop, result) { + if (enableMultiSelect) { + multiCtr.handleSelect(); + } + }, + child: _build(true), + ); + }); + } + return _build(false); + } + + Widget _build(bool multiSelect) { return Scaffold( resizeToAvoidBottomInset: false, - appBar: AppBar( - actions: [ - IconButton( - tooltip: '搜索', - onPressed: controller.onRefresh, - icon: const Icon(Icons.search_outlined, size: 22), - ), - ...?extraActions, - const SizedBox(width: 10), - ], - title: TextField( - autofocus: true, - focusNode: controller.focusNode, - controller: controller.editController, - textInputAction: TextInputAction.search, - textAlignVertical: TextAlignVertical.center, - decoration: InputDecoration( - hintText: '搜索', - border: InputBorder.none, - suffixIcon: IconButton( - tooltip: '清空', - icon: const Icon(Icons.clear, size: 22), - onPressed: () => controller - ..loadingState.value = LoadingState.loading() - ..onClear() - ..focusNode.requestFocus(), - ), - ), - onSubmitted: (value) => controller.onRefresh(), - ), - ), + appBar: _buildBar(multiSelect), body: SafeArea( top: false, bottom: false, @@ -68,6 +60,48 @@ abstract class CommonSearchPageState ); } + PreferredSizeWidget _buildBar(bool multiSelect) { + final AppBar bar = AppBar( + actions: [ + IconButton( + tooltip: '搜索', + onPressed: controller.onRefresh, + icon: const Icon(Icons.search_outlined, size: 22), + ), + ...?extraActions, + const SizedBox(width: 10), + ], + title: TextField( + autofocus: true, + focusNode: controller.focusNode, + controller: controller.editController, + textInputAction: TextInputAction.search, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + hintText: '搜索', + border: InputBorder.none, + suffixIcon: IconButton( + tooltip: '清空', + icon: const Icon(Icons.clear, size: 22), + onPressed: () => controller + ..loadingState.value = LoadingState.loading() + ..onClear() + ..focusNode.requestFocus(), + ), + ), + onSubmitted: (value) => controller.onRefresh(), + ), + ); + if (multiSelect) { + return MultiSelectAppBarWidget( + ctr: controller as MultiSelectBase, + children: multiSelectChildren, + child: bar, + ); + } + return bar; + } + Widget _buildBody(LoadingState?> loadingState) { return switch (loadingState) { Loading() => const HttpError(), diff --git a/lib/pages/common/multi_select_controller.dart b/lib/pages/common/multi_select_controller.dart index 1741190d..ea8e0384 100644 --- a/lib/pages/common/multi_select_controller.dart +++ b/lib/pages/common/multi_select_controller.dart @@ -6,9 +6,9 @@ mixin MultiSelectData { bool? checked; } -mixin MultiSelectMixin { - late final RxBool enableMultiSelect = false.obs; - late final allSelected = false.obs; +abstract class MultiSelectBase { + RxBool get enableMultiSelect; + RxBool get allSelected; int get checkedCount; @@ -19,9 +19,15 @@ mixin MultiSelectMixin { abstract class MultiSelectController extends CommonListController - with MultiSelectMixin, CommonMultiSelectMixin, DeleteItemMixin {} + with CommonMultiSelectMixin, DeleteItemMixin {} + +mixin CommonMultiSelectMixin + implements MultiSelectBase { + @override + late final RxBool enableMultiSelect = false.obs; + @override + late final allSelected = false.obs; -mixin CommonMultiSelectMixin on MultiSelectMixin { Rx?>> get loadingState; late final RxInt rxCount = 0.obs; diff --git a/lib/pages/fav/pgc/widget/item.dart b/lib/pages/fav/pgc/widget/item.dart index 22f54ad6..2fa0339f 100644 --- a/lib/pages/fav/pgc/widget/item.dart +++ b/lib/pages/fav/pgc/widget/item.dart @@ -18,7 +18,7 @@ class FavPgcItem extends StatelessWidget { }); final FavPgcItemModel item; - final MultiSelectMixin ctr; + final MultiSelectBase ctr; final VoidCallback onSelect; final VoidCallback onUpdateStatus; diff --git a/lib/pages/fav_detail/controller.dart b/lib/pages/fav_detail/controller.dart index 7a325ea8..e6a4f6b5 100644 --- a/lib/pages/fav_detail/controller.dart +++ b/lib/pages/fav_detail/controller.dart @@ -6,6 +6,7 @@ import 'package:PiliPlus/models/common/video/source_type.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/data.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/media.dart'; import 'package:PiliPlus/models_new/fav/fav_folder/list.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; import 'package:PiliPlus/pages/common/multi_select_controller.dart'; import 'package:PiliPlus/pages/fav_sort/view.dart'; import 'package:PiliPlus/services/account_service.dart'; @@ -14,14 +15,70 @@ import 'package:PiliPlus/utils/page_utils.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; +mixin BaseFavController + on + CommonListController, + DeleteItemMixin { + bool get isOwner; + int get mediaId; + + void onRemove(int count) {} + + void onViewFav(FavDetailItemModel item, int? index); + + Future onCancelFav(int index, int id, int type) async { + var result = await FavHttp.favVideo( + resources: '$id:$type', + delIds: mediaId.toString(), + ); + if (result['status']) { + loadingState + ..value.data!.removeAt(index) + ..refresh(); + onRemove(1); + SmartDialog.showToast('取消收藏'); + } else { + SmartDialog.showToast(result['msg']); + } + } + + @override + void onConfirm() { + showConfirmDialog( + context: Get.context!, + content: '确认删除所选收藏吗?', + title: '提示', + onConfirm: () async { + final checked = allChecked.toSet(); + var result = await FavHttp.favVideo( + resources: checked.map((item) => '${item.id}:${item.type}').join(','), + delIds: mediaId.toString(), + ); + if (result['status']) { + afterDelete(checked); + onRemove(checked.length); + SmartDialog.showToast('取消收藏'); + } else { + SmartDialog.showToast(result['msg']); + } + }, + ); + } +} + class FavDetailController - extends MultiSelectController { + extends MultiSelectController + with BaseFavController { + @override late int mediaId; late String heroTag; final Rx folderInfo = FavFolderInfo().obs; - final Rx isOwner = Rx(null); + final Rx _isOwner = Rx(null); final Rx order = FavOrderType.mtime.obs; + @override + bool get isOwner => _isOwner.value ?? false; + AccountService accountService = Get.find(); @override @@ -57,27 +114,16 @@ class FavDetailController if (isRefresh) { FavDetailData data = response.response; folderInfo.value = data.info!; - isOwner.value = data.info?.mid == accountService.mid; + _isOwner.value = data.info?.mid == accountService.mid; } return false; } - Future onCancelFav(int index, int id, int type) async { - var result = await FavHttp.favVideo( - resources: '$id:$type', - delIds: mediaId.toString(), - ); - if (result['status']) { - folderInfo - ..value.mediaCount -= 1 - ..refresh(); - loadingState - ..value.data!.removeAt(index) - ..refresh(); - SmartDialog.showToast('取消收藏'); - } else { - SmartDialog.showToast(result['msg']); - } + @override + void onRemove(int count) { + folderInfo + ..value.mediaCount -= count + ..refresh(); } @override @@ -89,31 +135,6 @@ class FavDetailController order: order.value, ); - @override - void onConfirm() { - showConfirmDialog( - context: Get.context!, - content: '确认删除所选收藏吗?', - title: '提示', - onConfirm: () async { - final checked = allChecked.toSet(); - var result = await FavHttp.favVideo( - resources: checked.map((item) => '${item.id}:${item.type}').join(','), - delIds: mediaId.toString(), - ); - if (result['status']) { - afterDelete(checked); - folderInfo - ..value.mediaCount -= checked.length - ..refresh(); - SmartDialog.showToast('取消收藏'); - } else { - SmartDialog.showToast(result['msg']); - } - }, - ); - } - void toViewPlayAll() { if (loadingState.value.isSuccess) { List? list = loadingState.value.data; @@ -126,22 +147,7 @@ class FavDetailController if (element.bvid != list.first.bvid) { SmartDialog.showToast('已跳过不支持播放的视频'); } - final folderInfo = this.folderInfo.value; - PageUtils.toVideoPage( - bvid: element.bvid, - cid: element.ugc!.firstCid!, - cover: element.cover, - title: element.title, - extraArguments: { - 'sourceType': SourceType.fav, - 'mediaId': folderInfo.id, - 'oid': element.id, - 'favTitle': folderInfo.title, - 'count': folderInfo.mediaCount, - 'desc': true, - 'isOwner': isOwner.value ?? false, - }, - ); + onViewFav(element, null); break; } } @@ -191,4 +197,25 @@ class FavDetailController Get.to(FavSortPage(favDetailController: this)); } } + + @override + void onViewFav(FavDetailItemModel item, int? index) { + final folder = folderInfo.value; + PageUtils.toVideoPage( + bvid: item.bvid, + cid: item.ugc!.firstCid!, + cover: item.cover, + title: item.title, + extraArguments: { + 'sourceType': SourceType.fav, + 'mediaId': folder.id, + 'oid': item.id, + 'favTitle': folder.title, + 'count': folder.mediaCount, + 'desc': true, + if (index != null) 'isContinuePlaying': index != 0, + 'isOwner': isOwner, + }, + ); + } } diff --git a/lib/pages/fav_detail/view.dart b/lib/pages/fav_detail/view.dart index 70756d0c..0fc6e4f3 100644 --- a/lib/pages/fav_detail/view.dart +++ b/lib/pages/fav_detail/view.dart @@ -1,4 +1,3 @@ -import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/video_card_h.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; @@ -8,7 +7,6 @@ import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/fav_order_type.dart'; -import 'package:PiliPlus/models/common/video/source_type.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/data.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/media.dart'; import 'package:PiliPlus/models_new/fav/fav_folder/list.dart'; @@ -17,7 +15,6 @@ import 'package:PiliPlus/pages/fav_detail/controller.dart'; import 'package:PiliPlus/pages/fav_detail/widget/fav_video_card.dart'; import 'package:PiliPlus/utils/fav_util.dart'; import 'package:PiliPlus/utils/grid.dart'; -import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/request_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; @@ -156,7 +153,7 @@ class _FavDetailPageState extends State { 'mediaId': int.parse(mediaId), 'title': folderInfo.title, 'count': folderInfo.mediaCount, - 'isOwner': _favDetailController.isOwner.value ?? false, + 'isOwner': _favDetailController.isOwner, }, ); }, @@ -198,7 +195,7 @@ class _FavDetailPageState extends State { PopupMenuButton( icon: const Icon(Icons.more_vert), itemBuilder: (context) { - final isOwner = _favDetailController.isOwner.value ?? false; + final isOwner = _favDetailController.isOwner; final folderInfo = _favDetailController.folderInfo.value; return [ if (isOwner) ...[ @@ -373,7 +370,7 @@ class _FavDetailPageState extends State { right: 6, top: 6, child: Obx(() { - if (_favDetailController.isOwner.value != false) { + if (_favDetailController.isOwner) { return const SizedBox.shrink(); } bool isFav = folderInfo.favState == 1; @@ -485,113 +482,11 @@ class _FavDetailPageState extends State { ), ); } - final isOwner = _favDetailController.isOwner.value ?? false; FavDetailItemModel item = response[index]; - return Stack( - clipBehavior: Clip.none, - children: [ - Positioned.fill( - child: FavVideoCardH( - item: item, - onDelFav: isOwner - ? () => _favDetailController.onCancelFav( - index, - item.id!, - item.type!, - ) - : null, - onViewFav: () { - final folderInfo = - _favDetailController.folderInfo.value; - PageUtils.toVideoPage( - bvid: item.bvid, - cid: item.ugc!.firstCid!, - cover: item.cover, - title: item.title, - extraArguments: { - 'sourceType': SourceType.fav, - 'mediaId': folderInfo.id, - 'oid': item.id, - 'favTitle': folderInfo.title, - 'count': folderInfo.mediaCount, - 'desc': true, - 'isContinuePlaying': index != 0, - 'isOwner': isOwner, - }, - ); - }, - onTap: enableMultiSelect - ? () => _favDetailController.onSelect(item) - : null, - onLongPress: isOwner - ? () { - if (!enableMultiSelect) { - _favDetailController - .enableMultiSelect - .value = - true; - _favDetailController.onSelect(item); - } - } - : null, - ), - ), - Positioned( - top: 5, - left: 12, - bottom: 5, - 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: StyleString.mdRadius, - color: Colors.black.withValues( - alpha: 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( - style: ButtonStyle( - padding: WidgetStateProperty.all( - EdgeInsets.zero, - ), - backgroundColor: - WidgetStatePropertyAll( - theme.colorScheme.surface - .withValues(alpha: 0.8), - ), - ), - onPressed: null, - icon: Icon( - Icons.done_all_outlined, - color: theme.colorScheme.primary, - ), - ), - ), - ), - ), - ), - ), - ), - ), - ], + return FavVideoCardH( + item: item, + index: index, + ctr: _favDetailController, ); }, childCount: response!.length + 1, diff --git a/lib/pages/fav_detail/widget/fav_video_card.dart b/lib/pages/fav_detail/widget/fav_video_card.dart index 7b457896..edded621 100644 --- a/lib/pages/fav_detail/widget/fav_video_card.dart +++ b/lib/pages/fav_detail/widget/fav_video_card.dart @@ -3,10 +3,12 @@ import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/select_mask.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/media.dart'; +import 'package:PiliPlus/pages/fav_detail/controller.dart'; import 'package:PiliPlus/utils/date_util.dart'; import 'package:PiliPlus/utils/duration_util.dart'; import 'package:PiliPlus/utils/page_utils.dart'; @@ -16,55 +18,58 @@ import 'package:get/get.dart'; // 收藏视频卡片 - 水平布局 class FavVideoCardH extends StatelessWidget { final FavDetailItemModel item; - final GestureTapCallback? onTap; - final GestureLongPressCallback? onLongPress; - final VoidCallback? onDelFav; - final VoidCallback? onViewFav; - final bool? isSort; + final int? index; + final BaseFavController? ctr; const FavVideoCardH({ super.key, required this.item, - this.onDelFav, - this.onTap, - this.onLongPress, - this.onViewFav, - this.isSort, - }); + this.index, + this.ctr, + }) : assert(ctr == null || index != null); + + bool get isSort => ctr == null; @override Widget build(BuildContext context) { + final isOwner = !isSort && ctr!.isOwner; + late final enableMultiSelect = ctr?.enableMultiSelect.value ?? false; return Material( type: MaterialType.transparency, child: InkWell( - onTap: isSort == true + onTap: isSort ? null - : onTap ?? - () { - if (!const [0, 16].contains(item.attr)) { - Get.toNamed('/member?mid=${item.upper?.mid}'); - return; - } + : enableMultiSelect + ? () => ctr!.onSelect(item) + : () { + if (!const [0, 16].contains(item.attr)) { + Get.toNamed('/member?mid=${item.upper?.mid}'); + return; + } - // pgc - if (item.type == 24) { - PageUtils.viewPgc( - seasonId: item.ogv!.seasonId, - epId: item.id, - ); - return; - } + // pgc + if (item.type == 24) { + PageUtils.viewPgc( + seasonId: item.ogv!.seasonId, + epId: item.id, + ); + return; + } - onViewFav?.call(); - }, - onLongPress: isSort == true + ctr!.onViewFav(item, index); + }, + onLongPress: isSort ? null - : onLongPress ?? - () => imageSaveDialog( - title: item.title, - cover: item.cover, - bvid: item.bvid, - ), + : isOwner && !enableMultiSelect + ? () { + ctr!.enableMultiSelect.value = true; + ctr!.onSelect(item); + } + : () => imageSaveDialog( + title: item.title, + cover: item.cover, + bvid: item.bvid, + ), child: Padding( padding: const EdgeInsets.symmetric( horizontal: StyleString.safeSpace, @@ -100,13 +105,20 @@ class FavVideoCardH extends StatelessWidget { bottom: null, left: null, ), + if (!isSort) + Positioned.fill( + child: selectMask( + Theme.of(context), + item.checked == true, + ), + ), ], ); }, ), ), const SizedBox(width: 10), - content(context), + content(context, isOwner), ], ), ), @@ -114,7 +126,7 @@ class FavVideoCardH extends StatelessWidget { ); } - Widget content(BuildContext context) { + Widget content(BuildContext context, bool isOwner) { final theme = Theme.of(context); return Expanded( child: Stack( @@ -170,7 +182,7 @@ class FavVideoCardH extends StatelessWidget { ), ], ), - if (onDelFav != null) + if (isOwner) Positioned( right: 0, bottom: -8, @@ -197,7 +209,7 @@ class FavVideoCardH extends StatelessWidget { TextButton( onPressed: () { Get.back(); - onDelFav!(); + ctr!.onCancelFav(index!, item.id!, item.type!); }, child: const Text('确定取消'), ), diff --git a/lib/pages/fav_search/controller.dart b/lib/pages/fav_search/controller.dart index f6c576b3..391816dd 100644 --- a/lib/pages/fav_search/controller.dart +++ b/lib/pages/fav_search/controller.dart @@ -1,16 +1,25 @@ import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/fav_order_type.dart'; +import 'package:PiliPlus/models/common/video/source_type.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/data.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/media.dart'; import 'package:PiliPlus/pages/common/common_search_controller.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:PiliPlus/pages/common/multi_select_controller.dart'; +import 'package:PiliPlus/pages/fav_detail/controller.dart'; +import 'package:PiliPlus/utils/page_utils.dart'; import 'package:get/get.dart'; class FavSearchController - extends CommonSearchController { + extends CommonSearchController + with + CommonMultiSelectMixin, + DeleteItemMixin, + BaseFavController { int type = Get.arguments['type']; + @override int mediaId = Get.arguments['mediaId']; + @override bool isOwner = Get.arguments['isOwner']; dynamic count = Get.arguments['count']; dynamic title = Get.arguments['title']; @@ -36,17 +45,20 @@ class FavSearchController return response.medias; } - Future onCancelFav(int index, int id, int? type) async { - var result = await FavHttp.favVideo( - resources: '$id:$type', - addIds: '', - delIds: mediaId.toString(), - ); - if (result['status']) { - loadingState - ..value.data!.removeAt(index) - ..refresh(); - SmartDialog.showToast('取消收藏'); - } - } + @override + void onViewFav(FavDetailItemModel item, int? index) => PageUtils.toVideoPage( + bvid: item.bvid, + cid: item.ugc!.firstCid!, + cover: item.cover, + title: item.title, + extraArguments: { + 'sourceType': SourceType.fav, + 'mediaId': mediaId, + 'oid': item.id, + 'favTitle': title, + 'count': count, + 'desc': true, + 'isContinuePlaying': true, + }, + ); } diff --git a/lib/pages/fav_search/view.dart b/lib/pages/fav_search/view.dart index 00658f7a..e58ccba7 100644 --- a/lib/pages/fav_search/view.dart +++ b/lib/pages/fav_search/view.dart @@ -1,12 +1,10 @@ import 'package:PiliPlus/models/common/fav_order_type.dart'; -import 'package:PiliPlus/models/common/video/source_type.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/data.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/media.dart'; import 'package:PiliPlus/pages/common/common_search_page.dart'; import 'package:PiliPlus/pages/fav_detail/widget/fav_video_card.dart'; import 'package:PiliPlus/pages/fav_search/controller.dart'; import 'package:PiliPlus/utils/grid.dart'; -import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -69,28 +67,8 @@ class _FavSearchPageState final item = list[index]; return FavVideoCardH( item: item, - onDelFav: controller.isOwner == true - ? () => controller.onCancelFav( - index, - item.id!, - item.type, - ) - : null, - onViewFav: () => PageUtils.toVideoPage( - bvid: item.bvid, - cid: item.ugc!.firstCid!, - cover: item.cover, - title: item.title, - extraArguments: { - 'sourceType': SourceType.fav, - 'mediaId': controller.mediaId, - 'oid': item.id, - 'favTitle': controller.title, - 'count': controller.count, - 'desc': true, - 'isContinuePlaying': true, - }, - ), + index: index, + ctr: controller, ); }, ), diff --git a/lib/pages/fav_sort/view.dart b/lib/pages/fav_sort/view.dart index 38e30977..d8089087 100644 --- a/lib/pages/fav_sort/view.dart +++ b/lib/pages/fav_sort/view.dart @@ -136,10 +136,7 @@ class _FavSortPageState extends State { return SizedBox( key: Key(item.id.toString()), height: 98, - child: FavVideoCardH( - isSort: true, - item: item, - ), + child: FavVideoCardH(item: item), ); }, ); diff --git a/lib/pages/history/widgets/item.dart b/lib/pages/history/widgets/item.dart index f6f8f237..aaab4cea 100644 --- a/lib/pages/history/widgets/item.dart +++ b/lib/pages/history/widgets/item.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/progress_bar/video_progress_indicator.dart'; +import 'package:PiliPlus/common/widgets/select_mask.dart'; import 'package:PiliPlus/http/search.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; @@ -10,7 +11,6 @@ import 'package:PiliPlus/models_new/history/list.dart'; import 'package:PiliPlus/pages/common/multi_select_controller.dart'; import 'package:PiliPlus/utils/date_util.dart'; import 'package:PiliPlus/utils/duration_util.dart'; -import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:flutter/material.dart'; @@ -20,7 +20,7 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart class HistoryItem extends StatelessWidget { final HistoryItemModel item; - final MultiSelectMixin ctr; + final MultiSelectBase ctr; final void Function(int kid, String business) onDelete; const HistoryItem({ @@ -90,18 +90,12 @@ class HistoryItem extends StatelessWidget { } } }, - onLongPress: () { - if (!ctr.enableMultiSelect.value) { - ctr.enableMultiSelect.value = true; - ctr.onSelect(item); - } - return; - // imageSaveDialog( - // title: item.title, - // cover: item.cover, - // bvid: bvid, - // ); - }, + onLongPress: ctr.enableMultiSelect.value + ? null + : () { + ctr.enableMultiSelect.value = true; + ctr.onSelect(item); + }, child: Stack( clipBehavior: Clip.none, children: [ @@ -164,49 +158,7 @@ class HistoryItem extends StatelessWidget { ), ), Positioned.fill( - child: AnimatedOpacity( - opacity: item.checked == true ? 1 : 0, - duration: const Duration(milliseconds: 200), - child: Container( - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: StyleString.mdRadius, - color: Colors.black.withValues(alpha: 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: - WidgetStatePropertyAll( - theme.colorScheme.surface - .withValues(alpha: 0.8), - ), - ), - onPressed: () { - feedBack(); - ctr.onSelect(item); - }, - icon: Icon( - Icons.done_all_outlined, - color: theme.colorScheme.primary, - ), - ), - ), - ), - ), - ), + child: selectMask(theme, item.checked == true), ), ], ); diff --git a/lib/pages/history_search/controller.dart b/lib/pages/history_search/controller.dart index 4adfee29..0f7a9e22 100644 --- a/lib/pages/history_search/controller.dart +++ b/lib/pages/history_search/controller.dart @@ -10,10 +10,7 @@ import 'package:get/get.dart'; class HistorySearchController extends CommonSearchController - with - MultiSelectMixin, - CommonMultiSelectMixin, - DeleteItemMixin { + with CommonMultiSelectMixin, DeleteItemMixin { @override Future> customGetData() => UserHttp.searchHistory( pn: page, diff --git a/lib/pages/history_search/view.dart b/lib/pages/history_search/view.dart index 7a5032dd..fcc3561e 100644 --- a/lib/pages/history_search/view.dart +++ b/lib/pages/history_search/view.dart @@ -1,4 +1,3 @@ -import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; import 'package:PiliPlus/models_new/history/data.dart'; import 'package:PiliPlus/models_new/history/list.dart'; import 'package:PiliPlus/pages/common/common_search_page.dart'; @@ -29,31 +28,6 @@ class _HistorySearchPageState tag: Utils.generateRandomString(8), ); - @override - Widget build(BuildContext context) { - // TODO: refa - return Obx(() { - final parent = super.build(context) as Scaffold; - final enableMultiSelect = controller.enableMultiSelect.value; - return PopScope( - canPop: !enableMultiSelect, - onPopInvokedWithResult: (didPop, result) { - if (enableMultiSelect) { - controller.handleSelect(); - } - }, - child: Scaffold( - resizeToAvoidBottomInset: parent.resizeToAvoidBottomInset, - appBar: MultiSelectAppBarWidget( - ctr: controller, - child: parent.appBar as AppBar, - ), - body: parent.body, - ), - ); - }); - } - @override Widget buildList(List list) { return SliverGrid( diff --git a/lib/pages/later/child_view.dart b/lib/pages/later/child_view.dart index e924c00c..3f96f8f1 100644 --- a/lib/pages/later/child_view.dart +++ b/lib/pages/later/child_view.dart @@ -1,6 +1,4 @@ -import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/skeleton/video_card_h.dart'; -import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/refresh_indicator.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -57,7 +55,6 @@ class _LaterViewChildPageState extends State } Widget _buildBody(LoadingState?> loadingState) { - final theme = Theme.of(context); return switch (loadingState) { Loading() => SliverGrid( gridDelegate: Grid.videoCardHDelegate(context), @@ -80,116 +77,42 @@ class _LaterViewChildPageState extends State var videoItem = response[index]; final enableMultiSelect = _laterController.baseCtr.enableMultiSelect.value; - return Stack( - clipBehavior: Clip.none, - children: [ - VideoCardHLater( - videoItem: videoItem, - onViewLater: (cid) { - PageUtils.toVideoPage( - bvid: videoItem.bvid, - cid: cid, - cover: videoItem.pic, - title: videoItem.title, - extraArguments: { - 'oid': videoItem.aid, - 'sourceType': SourceType.watchLater, - 'count': _laterController - .baseCtr - .counts[LaterViewType.all], - 'favTitle': '稍后再看', - 'mediaId': _laterController.accountService.mid, - 'desc': false, - 'isContinuePlaying': index != 0, - }, - ); + return VideoCardHLater( + videoItem: videoItem, + onViewLater: (cid) { + PageUtils.toVideoPage( + bvid: videoItem.bvid, + cid: cid, + cover: videoItem.pic, + title: videoItem.title, + extraArguments: { + 'oid': videoItem.aid, + 'sourceType': SourceType.watchLater, + 'count': _laterController + .baseCtr + .counts[LaterViewType.all], + 'favTitle': '稍后再看', + 'mediaId': _laterController.accountService.mid, + 'desc': false, + 'isContinuePlaying': index != 0, }, - onTap: !enableMultiSelect - ? null - : () => _laterController.onSelect(videoItem), - onLongPress: () { - if (!enableMultiSelect) { + ); + }, + onTap: !enableMultiSelect + ? null + : () => _laterController.onSelect(videoItem), + onLongPress: enableMultiSelect + ? null + : () { _laterController.baseCtr.enableMultiSelect.value = true; _laterController.onSelect(videoItem); - } - }, - ), - Positioned( - top: 5, - left: 12, - bottom: 5, - child: IgnorePointer( - child: LayoutBuilder( - builder: (context, constraints) => - AnimatedOpacity( - opacity: videoItem.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: StyleString.mdRadius, - color: Colors.black.withValues( - alpha: 0.6, - ), - ), - child: SizedBox( - width: 34, - height: 34, - child: AnimatedScale( - scale: videoItem.checked == true - ? 1 - : 0, - duration: const Duration( - milliseconds: 250, - ), - curve: Curves.easeInOut, - child: IconButton( - tooltip: '取消选择', - style: ButtonStyle( - padding: WidgetStateProperty.all( - EdgeInsets.zero, - ), - backgroundColor: - WidgetStatePropertyAll( - theme.colorScheme.surface - .withValues(alpha: 0.8), - ), - ), - onPressed: null, - icon: Icon( - Icons.done_all_outlined, - color: theme.colorScheme.primary, - ), - ), - ), - ), - ), - ), - ), - ), - ), - Positioned( - right: 12, - bottom: 0, - child: iconButton( - tooltip: '移除', - context: context, - onPressed: () => _laterController.toViewDel( - context, - index, - videoItem.aid, - ), - icon: Icons.clear, - iconColor: theme.colorScheme.outline, - bgColor: Colors.transparent, - ), - ), - ], + }, + onRemove: () => _laterController.toViewDel( + context, + index, + videoItem.aid, + ), ); }, childCount: response!.length, diff --git a/lib/pages/later/widgets/video_card_h_later.dart b/lib/pages/later/widgets/video_card_h_later.dart index 020329f3..4044bba8 100644 --- a/lib/pages/later/widgets/video_card_h_later.dart +++ b/lib/pages/later/widgets/video_card_h_later.dart @@ -1,8 +1,11 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; +import 'package:PiliPlus/common/widgets/button/icon_button.dart'; +import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/progress_bar/video_progress_indicator.dart'; +import 'package:PiliPlus/common/widgets/select_mask.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/http/search.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; @@ -22,11 +25,13 @@ class VideoCardHLater extends StatelessWidget { this.onTap, this.onLongPress, this.onViewLater, + this.onRemove, }); final LaterItemModel videoItem; final VoidCallback? onTap; final VoidCallback? onLongPress; final ValueChanged? onViewLater; + final VoidCallback? onRemove; @override Widget build(BuildContext context) { @@ -152,6 +157,12 @@ class VideoCardHLater extends StatelessWidget { bottom: 6.0, type: PBadgeType.gray, ), + Positioned.fill( + child: selectMask( + Theme.of(context), + videoItem.checked == true, + ), + ), ], ); }, @@ -234,6 +245,22 @@ class VideoCardHLater extends StatelessWidget { type: StatType.danmaku, value: videoItem.stat?.danmaku, ), + if (onRemove != null) ...[ + const Spacer(), + iconButton( + tooltip: '移除', + context: context, + onPressed: () => showConfirmDialog( + context: context, + title: '提示', + content: '即将移除该视频,确定是否移除', + onConfirm: onRemove!, + ), + icon: Icons.clear, + iconColor: theme.colorScheme.outline, + bgColor: Colors.transparent, + ), + ], ], ), ], diff --git a/lib/pages/later_search/controller.dart b/lib/pages/later_search/controller.dart index 2cdfd0bb..a4435223 100644 --- a/lib/pages/later_search/controller.dart +++ b/lib/pages/later_search/controller.dart @@ -1,14 +1,17 @@ +import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/models_new/later/data.dart'; import 'package:PiliPlus/models_new/later/list.dart'; import 'package:PiliPlus/pages/common/common_search_controller.dart'; +import 'package:PiliPlus/pages/common/multi_select_controller.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; class LaterSearchController - extends CommonSearchController { + extends CommonSearchController + with CommonMultiSelectMixin, DeleteItemMixin { dynamic mid = Get.arguments['mid']; dynamic count = Get.arguments['count']; @@ -32,24 +35,24 @@ class LaterSearchController SmartDialog.showToast(res['msg']); } - // @override - // void onConfirm() { - // showConfirmDialog( - // context: Get.context!, - // content: '确认删除所选稍后再看吗?', - // title: '提示', - // onConfirm: () async { - // final result = allChecked.toSet(); - // SmartDialog.showLoading(msg: '请求中'); - // var res = await UserHttp.toViewDel( - // aids: result.map((item) => item.aid!), - // ); - // if (res['status']) { - // afterDelete(result); - // } - // SmartDialog.dismiss(); - // SmartDialog.showToast(res['msg']); - // }, - // ); - // } + @override + void onConfirm() { + showConfirmDialog( + context: Get.context!, + content: '确认删除所选稍后再看吗?', + title: '提示', + onConfirm: () async { + final result = allChecked.toSet(); + SmartDialog.showLoading(msg: '请求中'); + var res = await UserHttp.toViewDel( + aids: result.map((item) => item.aid!), + ); + if (res['status']) { + afterDelete(result); + } + SmartDialog.dismiss(); + SmartDialog.showToast(res['msg']); + }, + ); + } } diff --git a/lib/pages/later_search/view.dart b/lib/pages/later_search/view.dart index c9aabe08..21cec38c 100644 --- a/lib/pages/later_search/view.dart +++ b/lib/pages/later_search/view.dart @@ -1,5 +1,3 @@ -import 'package:PiliPlus/common/widgets/button/icon_button.dart'; -import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/models/common/video/source_type.dart'; import 'package:PiliPlus/models_new/later/data.dart'; import 'package:PiliPlus/models_new/later/list.dart'; @@ -27,90 +25,52 @@ class _LaterSearchPageState tag: Utils.generateRandomString(8), ); - // @override - // Widget build(BuildContext context) { - // // TODO: refa - // return Obx(() { - // final parent = super.build(context) as Scaffold; - // final enableMultiSelect = controller.enableMultiSelect.value; - // return PopScope( - // canPop: !enableMultiSelect, - // onPopInvokedWithResult: (didPop, result) { - // if (enableMultiSelect) { - // controller.handleSelect(); - // } - // }, - // child: Scaffold( - // resizeToAvoidBottomInset: parent.resizeToAvoidBottomInset, - // appBar: MultiSelectAppBarWidget( - // ctr: controller, - // child: parent.appBar as AppBar, - // ), - // body: parent.body, - // ), - // ); - // }); - // } - @override Widget buildList(List list) { return SliverGrid( gridDelegate: Grid.videoCardHDelegate(context, minHeight: 110), - delegate: SliverChildBuilderDelegate( - childCount: list.length, - (context, index) { - if (index == list.length - 1) { - controller.onLoadMore(); - } - final item = list[index]; - return Stack( - clipBehavior: Clip.none, - children: [ - VideoCardHLater( - videoItem: item, - onViewLater: (cid) { - PageUtils.toVideoPage( - bvid: item.bvid, - cid: cid, - cover: item.pic, - title: item.title, - extraArguments: { - 'oid': item.aid, - 'sourceType': SourceType.watchLater, - 'count': controller.count, - 'favTitle': '稍后再看', - 'mediaId': controller.mid, - 'desc': false, - 'isContinuePlaying': index != 0, - }, - ); + delegate: SliverChildBuilderDelegate(childCount: list.length, ( + context, + index, + ) { + if (index == list.length - 1) { + controller.onLoadMore(); + } + final item = list[index]; + final enableMultiSelect = controller.enableMultiSelect.value; + return VideoCardHLater( + videoItem: item, + onViewLater: (cid) { + PageUtils.toVideoPage( + bvid: item.bvid, + cid: cid, + cover: item.pic, + title: item.title, + extraArguments: { + 'oid': item.aid, + 'sourceType': SourceType.watchLater, + 'count': controller.count, + 'favTitle': '稍后再看', + 'mediaId': controller.mid, + 'desc': false, + 'isContinuePlaying': index != 0, + }, + ); + }, + onRemove: () => controller.toViewDel( + context, + index, + item.aid!, + ), + onTap: !enableMultiSelect ? null : () => controller.onSelect(item), + onLongPress: enableMultiSelect + ? null + : () { + controller.enableMultiSelect.value = true; + controller.onSelect(item); }, - ), - Positioned( - right: 12, - bottom: 0, - child: iconButton( - tooltip: '移除', - context: context, - onPressed: () => showConfirmDialog( - context: context, - title: '提示', - content: '即将移除该视频,确定是否移除', - onConfirm: () => controller.toViewDel( - context, - index, - item.aid!, - ), - ), - icon: Icons.clear, - iconColor: Theme.of(context).colorScheme.onSurfaceVariant, - bgColor: Colors.transparent, - ), - ), - ], - ); - }, - ), + ); + }), ); } } diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index 66ba2c18..c26f9055 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -378,7 +378,7 @@ class VideoDetailController extends GetxController ? (item, index) async { if (sourceType == SourceType.watchLater) { var res = await UserHttp.toViewDel( - aids: [item.aid], + aids: [item.aid!], ); if (res['status']) { mediaList.removeAt(index); diff --git a/lib/pages/video/medialist/view.dart b/lib/pages/video/medialist/view.dart index 23695f6b..94b7e423 100644 --- a/lib/pages/video/medialist/view.dart +++ b/lib/pages/video/medialist/view.dart @@ -42,7 +42,7 @@ class MediaListPanel extends CommonCollapseSlidePage { final bool desc; final VoidCallback onReverse; final RefreshCallback? loadPrevious; - final Function(dynamic item, int index)? onDelete; + final Function(MediaListItemModel item, int index)? onDelete; @override State createState() => _MediaListPanelState();