diff --git a/lib/http/api.dart b/lib/http/api.dart index acd3d5a7..e4a18342 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -733,5 +733,7 @@ class Api { static const String addNote = '/x/note/add'; + static const String delNote = '/x/note/del'; + static const String archiveNote = '/x/note/list/archive'; } diff --git a/lib/http/video.dart b/lib/http/video.dart index e4dd1380..69e072ac 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -1146,7 +1146,7 @@ class VideoHttp { } } catch (_) {} - var res = await Request().post( + final res = await Request().post( Api.addNote, data: { 'cont_len': summary.length, @@ -1174,4 +1174,24 @@ class VideoHttp { return LoadingState.error(res.data['message']); } } + + static Future delNote({ + required List noteIds, + }) async { + final res = await Request().post( + Api.delNote, + data: { + 'note_ids': noteIds.join(','), + 'csrf': Accounts.main.csrf, + }, + options: Options( + contentType: Headers.formUrlEncodedContentType, + ), + ); + if (res.data['code'] == 0) { + return {'status': true}; + } else { + return {'status': false, 'msg': res.data['message']}; + } + } } diff --git a/lib/pages/fav/note/child_view.dart b/lib/pages/fav/note/child_view.dart index ef776fe6..406ede79 100644 --- a/lib/pages/fav/note/child_view.dart +++ b/lib/pages/fav/note/child_view.dart @@ -1,6 +1,8 @@ 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/note/controller.dart'; @@ -26,13 +28,95 @@ class _FavNoteChildPageState extends State @override Widget build(BuildContext context) { super.build(context); - return refreshIndicator( - onRefresh: () async { - await _favNoteController.onRefresh(); - }, - child: CustomScrollView( - slivers: [ - Obx(() => _buildBody(_favNoteController.loadingState.value)), + return LayoutBuilder( + builder: (context, constraints) => Stack( + clipBehavior: Clip.none, + children: [ + refreshIndicator( + onRefresh: () async { + await _favNoteController.onRefresh(); + }, + child: CustomScrollView( + slivers: [ + Obx(() => _buildBody(_favNoteController.loadingState.value)), + ], + ), + ), + Positioned( + top: constraints.maxHeight, + left: 0, + right: 0, + child: Obx( + () => AnimatedSlide( + offset: _favNoteController.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: _favNoteController.onDisable, + ), + const SizedBox(width: 12), + Obx( + () => Checkbox( + value: _favNoteController.allSelected.value, + onChanged: (value) { + _favNoteController.handleSelect( + !_favNoteController.allSelected.value); + }, + ), + ), + GestureDetector( + onTap: () { + _favNoteController.handleSelect( + !_favNoteController.allSelected.value); + }, + child: const Text('全选'), + ), + const Spacer(), + FilledButton.tonal( + style: TextButton.styleFrom( + visualDensity: + VisualDensity(horizontal: -2, vertical: -2), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + onPressed: () { + showConfirmDialog( + context: context, + title: '确定删除已选中的笔记吗?', + onConfirm: _favNoteController.onRemove, + ); + }, + child: const Text('删除'), + ), + const SizedBox(width: 16), + ], + ), + ), + ), + ), + ), ], ), ); @@ -61,11 +145,17 @@ class _FavNoteChildPageState extends State gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( mainAxisSpacing: 2, maxCrossAxisExtent: Grid.mediumCardWidth * 2, - childAspectRatio: StyleString.aspectRatio * 2.7, + childAspectRatio: StyleString.aspectRatio * 2.6, ), delegate: SliverChildBuilderDelegate( (context, index) { - return FavNoteItem(item: loadingState.response[index]); + return FavNoteItem( + item: loadingState.response[index], + ctr: _favNoteController, + onSelect: () { + _favNoteController.onSelect(index); + }, + ); }, childCount: loadingState.response.length, ), diff --git a/lib/pages/fav/note/controller.dart b/lib/pages/fav/note/controller.dart index 4af3b39f..77cd4f18 100644 --- a/lib/pages/fav/note/controller.dart +++ b/lib/pages/fav/note/controller.dart @@ -1,22 +1,73 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/pages/common/multi_select_controller.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; class FavNoteController extends MultiSelectController { FavNoteController(this.isPublish); final bool isPublish; + 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() { return isPublish ? VideoHttp.userNoteList(page: currentPage) : VideoHttp.noteList(page: currentPage); } + + void onRemove() async { + List dataList = (loadingState.value as Success).response as List; + Set removeList = dataList.where((item) => item['checked'] == true).toSet(); + final res = await VideoHttp.delNote( + noteIds: removeList.map((item) => item['note_id']).toList()); + if (res['status']) { + List remainList = dataList.toSet().difference(removeList).toList(); + loadingState.value = LoadingState.success(remainList); + enableMultiSelect.value = false; + SmartDialog.showToast('删除成功'); + } else { + SmartDialog.showToast(res['msg']); + } + } + + void onDisable() { + if (checkedCount.value != 0) { + handleSelect(); + } + enableMultiSelect.value = false; + } } diff --git a/lib/pages/fav/note/view.dart b/lib/pages/fav/note/view.dart index 6c674268..72058fd9 100644 --- a/lib/pages/fav/note/view.dart +++ b/lib/pages/fav/note/view.dart @@ -1,5 +1,8 @@ +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}); @@ -65,7 +68,21 @@ class _FavNotePageState extends State visualDensity: VisualDensity(horizontal: -2, vertical: -2), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), - onPressed: () {}, + 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), diff --git a/lib/pages/fav/note/widget/item.dart b/lib/pages/fav/note/widget/item.dart index ade1affa..d6cadbc4 100644 --- a/lib/pages/fav/note/widget/item.dart +++ b/lib/pages/fav/note/widget/item.dart @@ -1,20 +1,37 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/network_img_layer.dart'; +import 'package:PiliPlus/pages/fav/note/controller.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; class FavNoteItem extends StatelessWidget { - const FavNoteItem({super.key, required this.item}); + const FavNoteItem({ + super.key, + required this.item, + required this.ctr, + required this.onSelect, + }); final dynamic item; + final FavNoteController ctr; + final VoidCallback onSelect; @override Widget build(BuildContext context) { return InkWell( onTap: () { + if (ctr.enableMultiSelect.value) { + onSelect(); + return; + } Utils.handleWebview(item['web_url'], inApp: true); }, - onLongPress: () {}, + onLongPress: () { + if (!ctr.enableMultiSelect.value) { + ctr.enableMultiSelect.value = true; + onSelect(); + } + }, child: Padding( padding: const EdgeInsets.symmetric( horizontal: StyleString.safeSpace, @@ -41,7 +58,6 @@ class FavNoteItem extends StatelessWidget { ), ), ), - const SizedBox(height: 1), Text( item['summary'], maxLines: 1, @@ -49,7 +65,6 @@ class FavNoteItem extends StatelessWidget { color: Theme.of(context).colorScheme.outline, ), ), - const SizedBox(height: 1), Text( item['message'], maxLines: 1, @@ -67,10 +82,69 @@ class FavNoteItem extends StatelessWidget { child: LayoutBuilder( builder: (BuildContext context, BoxConstraints boxConstraints) { - return NetworkImgLayer( - src: item['arc']?['pic'], - width: boxConstraints.maxWidth, - height: boxConstraints.maxHeight, + 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, + ), + ), + ), + ), + ), + ), + ), + ), + ), + ], ); }, ), diff --git a/lib/pages/history/widgets/item.dart b/lib/pages/history/widgets/item.dart index 0b79d8c7..3321a343 100644 --- a/lib/pages/history/widgets/item.dart +++ b/lib/pages/history/widgets/item.dart @@ -134,7 +134,6 @@ class HistoryItem extends StatelessWidget { return; } if (!ctr!.enableMultiSelect.value) { - feedBack(); ctr!.enableMultiSelect.value = true; onChoose?.call(); }