opt: multiSelect (#935)

This commit is contained in:
My-Responsitories
2025-08-04 12:57:37 +08:00
committed by GitHub
parent d246462535
commit 7b51f15753
23 changed files with 488 additions and 457 deletions

View File

@@ -0,0 +1,91 @@
import 'package:PiliPlus/pages/common/multi_select_controller.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class AppBarWidget extends StatelessWidget implements PreferredSizeWidget {
const AppBarWidget({
required this.child1,
required this.child2,
required this.visible,
super.key,
});
final PreferredSizeWidget child1;
final PreferredSizeWidget child2;
final bool visible;
@override
Size get preferredSize => child1.preferredSize;
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: !visible
? KeyedSubtree.wrap(child1, 0)
: KeyedSubtree.wrap(child2, 1),
);
}
}
class MultiSelectAppBarWidget extends StatelessWidget
implements PreferredSizeWidget {
final MultiSelectMixin ctr;
final bool? visible;
final AppBar child;
final List<Widget>? children;
const MultiSelectAppBarWidget({
super.key,
required this.ctr,
this.visible,
this.children,
required this.child,
});
@override
Widget build(BuildContext context) {
return AppBarWidget(
visible: visible ?? ctr.enableMultiSelect.value,
child1: child,
child2: AppBar(
bottom: child.bottom,
leading: IconButton(
tooltip: '取消',
onPressed: ctr.handleSelect,
icon: const Icon(Icons.close_outlined),
),
title: Obx(() => Text('已选: ${ctr.checkedCount}')),
actions: [
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () => ctr.handleSelect(true),
child: const Text('全选'),
),
...?children,
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: ctr.onConfirm,
child: Text(
'移除',
style: TextStyle(color: Get.theme.colorScheme.error),
),
),
const SizedBox(width: 6),
],
),
);
}
@override
Size get preferredSize => child.preferredSize;
}

View File

@@ -312,7 +312,7 @@ class FavHttp {
static Future delNote({
required bool isPublish,
required List noteIds,
required Iterable noteIds,
}) async {
final res = await Request().post(
isPublish ? Api.delPublishNote : Api.delNote,
@@ -637,13 +637,13 @@ class FavHttp {
}
}
static Future copyOrMoveFav({
static Future<LoadingState> copyOrMoveFav({
required bool isCopy,
required bool isFav,
required dynamic srcMediaId,
required dynamic tarMediaId,
dynamic mid,
required List resources,
required Iterable resources,
}) async {
var res = await Request().post(
isFav
@@ -664,21 +664,21 @@ class FavHttp {
options: Options(contentType: Headers.formUrlEncodedContentType),
);
if (res.data['code'] == 0) {
return {'status': true};
return const Success(null);
} else {
return {'status': false, 'msg': res.data['message']};
return Error(res.data['message']);
}
}
static Future allFavFolders(mid) async {
static Future<LoadingState<FavFolderData>> allFavFolders(Object mid) async {
var res = await Request().get(
Api.favFolder,
queryParameters: {'up_mid': mid},
);
if (res.data['code'] == 0) {
return {'status': true, 'data': FavFolderData.fromJson(res.data['data'])};
return Success(FavFolderData.fromJson(res.data['data']));
} else {
return {'status': false, 'msg': res.data['message']};
return Error(res.data['message']);
}
}

View File

@@ -151,7 +151,7 @@ class UserHttp {
}
// 移除已观看
static Future toViewDel({required List<int?> aids}) async {
static Future toViewDel({required Iterable<int> aids}) async {
final Map<String, dynamic> params = {
'csrf': Accounts.main.csrf,
'resources': aids.join(','),
@@ -204,7 +204,7 @@ class UserHttp {
}
// 删除历史记录
static Future delHistory(List<String> kidList) async {
static Future delHistory(Iterable<String> kidList) async {
var res = await Request().post(
Api.delHistory,
data: {

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/pages/common/common_list_controller.dart';
import 'package:get/get.dart';
@@ -5,35 +6,60 @@ mixin MultiSelectData {
bool? checked;
}
abstract class MultiSelectController<R, T extends MultiSelectData>
extends CommonListController<R, T> {
mixin MultiSelectMixin<T> {
late final RxBool enableMultiSelect = false.obs;
late final RxInt checkedCount = 0.obs;
late final allSelected = false.obs;
int get checkedCount;
void onSelect(T item, [bool disableSelect = true]);
void handleSelect([bool checked = false, bool disableSelect = true]);
void onConfirm();
}
abstract class MultiSelectController<R, T extends MultiSelectData>
extends CommonListController<R, T>
with MultiSelectMixin<T>, CommonMultiSelectMixin, DeleteItemMixin {}
mixin CommonMultiSelectMixin<T extends MultiSelectData> on MultiSelectMixin<T> {
Rx<LoadingState<List<T>?>> get loadingState;
late final RxInt rxCount = 0.obs;
@override
int get checkedCount => rxCount.value;
Iterable<T> get allChecked =>
loadingState.value.data!.where((v) => v.checked == true);
@override
void onSelect(T item, [bool disableSelect = true]) {
List<T> list = loadingState.value.data!;
item.checked = !(item.checked ?? false);
checkedCount.value = list.where((item) => item.checked == true).length;
if (item.checked!) {
rxCount.value++;
} else {
rxCount.value--;
}
loadingState.refresh();
if (disableSelect) {
if (checkedCount.value == 0) {
if (checkedCount == 0) {
enableMultiSelect.value = false;
}
} else {
allSelected.value = checkedCount.value == list.length;
allSelected.value = checkedCount == list.length;
}
}
@override
void handleSelect([bool checked = false, bool disableSelect = true]) {
if (loadingState.value.isSuccess) {
List<T>? list = loadingState.value.data;
final list = loadingState.value.data;
if (list?.isNotEmpty == true) {
for (T item in list!) {
for (var item in list!) {
item.checked = checked;
}
loadingState.refresh();
checkedCount.value = checked ? list.length : 0;
rxCount.value = checked ? list.length : 0;
}
}
if (disableSelect && !checked) {
@@ -41,3 +67,75 @@ abstract class MultiSelectController<R, T extends MultiSelectData>
}
}
}
mixin DeleteItemMixin<R, T extends MultiSelectData>
on CommonListController<R, T>, CommonMultiSelectMixin<T> {
Future<void> afterDelete(Set<T> result) async {
// TODO: result require hash
final remainList = loadingState.value.data!;
if (result.length == 1) {
remainList.remove(result.single);
} else {
remainList.removeWhere(result.contains);
}
if (remainList.isNotEmpty) {
loadingState.refresh();
} else if (!isEnd) {
onReload();
}
if (enableMultiSelect.value) {
rxCount.value = 0;
enableMultiSelect.value = false;
}
}
}
// abstract class SetMultiSelectController<R, T, I>
// extends CommonListController<R, T>
// with MultiSelectMixin<T>, SetCommonMultiSelectMixin<T, I> {}
// mixin SetCommonMultiSelectMixin<T, R> on MultiSelectMixin<T> {
// Rx<LoadingState<List<T>?>> get loadingState;
// RxSet<R> get selected;
// @override
// int get checkedCount => selected.length;
// R getId(T item);
// @override
// void onSelect(T item, [bool disableSelect = true]) {
// final id = getId(item);
// if (selected.contains(id)) {
// selected.remove(id);
// } else {
// selected.add(id);
// }
// loadingState.refresh();
// if (disableSelect) {
// if (checkedCount == 0) {
// enableMultiSelect.value = false;
// }
// } else {
// allSelected.value = checkedCount == loadingState.value.data!.length;
// }
// }
// @override
// void handleSelect([bool checked = false, bool disableSelect = true]) {
// if (loadingState.value.isSuccess) {
// final list = loadingState.value.data;
// if (list?.isNotEmpty == true) {
// if (checked) {
// selected.addAll(list!.map(getId));
// } else {
// selected.clear();
// }
// loadingState.refresh();
// }
// }
// if (disableSelect && !checked) {
// enableMultiSelect.value = false;
// }
// }
// }

View File

@@ -97,7 +97,7 @@ class _CreateVotePageState extends State<CreateVotePage> {
..add(
_buildInput(
theme,
key: ValueKey(e.hashCode),
key: ObjectKey(e),
showDel: showDel,
onDel: () {
FocusManager.instance.primaryFocus?.unfocus();

View File

@@ -115,11 +115,11 @@ class _FavNoteChildPageState extends State<FavNoteChildPage>
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
),
onPressed: () {
if (_favNoteController.checkedCount.value != 0) {
if (_favNoteController.checkedCount != 0) {
showConfirmDialog(
context: context,
title: '确定删除已选中的笔记吗?',
onConfirm: _favNoteController.onRemove,
onConfirm: _favNoteController.onConfirm,
);
}
},

View File

@@ -34,24 +34,15 @@ class FavNoteController
: FavHttp.noteList(page: page);
}
Future<void> onRemove() async {
List<FavNoteItemModel> dataList = loadingState.value.data!;
Set<FavNoteItemModel> removeList = dataList
.where((item) => item.checked == true)
.toSet();
@override
Future<void> onConfirm() async {
Set<FavNoteItemModel> removeList = allChecked.toSet();
final res = await FavHttp.delNote(
isPublish: isPublish,
noteIds: removeList
.map((item) => isPublish ? item.cvid : item.noteId)
.toList(),
noteIds: removeList.map((item) => isPublish ? item.cvid : item.noteId),
);
if (res['status']) {
List<FavNoteItemModel> remainList = dataList
.toSet()
.difference(removeList)
.toList();
loadingState.value = Success(remainList);
enableMultiSelect.value = false;
afterDelete(removeList);
SmartDialog.showToast('删除成功');
} else {
SmartDialog.showToast(res['msg']);
@@ -59,7 +50,7 @@ class FavNoteController
}
void onDisable() {
if (checkedCount.value != 0) {
if (checkedCount != 0) {
handleSelect();
}
enableMultiSelect.value = false;

View File

@@ -128,8 +128,7 @@ class _FavPgcChildPageState extends State<FavPgcChildPage>
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
if (_favPgcController.checkedCount.value !=
0) {
if (_favPgcController.checkedCount != 0) {
_favPgcController.onUpdateList(
item.followStatus,
);

View File

@@ -48,7 +48,7 @@ class FavPgcController
);
void onDisable() {
if (checkedCount.value != 0) {
if (checkedCount != 0) {
handleSelect();
}
enableMultiSelect.value = false;
@@ -65,22 +65,19 @@ class FavPgcController
SmartDialog.showToast(result['msg']);
}
@override
void onConfirm() {
assert(false, 'call onUpdateList');
}
Future<void> onUpdateList(int followStatus) async {
List<FavPgcItemModel> dataList = loadingState.value.data!;
Set<FavPgcItemModel> updateList = dataList
.where((item) => item.checked == true)
.toSet();
final updateList = allChecked.toSet();
final res = await VideoHttp.pgcUpdate(
seasonId: updateList.map((item) => item.seasonId).toList(),
status: followStatus,
);
if (res['status']) {
List<FavPgcItemModel> remainList = dataList
.toSet()
.difference(updateList)
.toList();
loadingState.value = Success(remainList);
enableMultiSelect.value = false;
afterDelete(updateList);
try {
final ctr = Get.find<FavPgcController>(tag: '$type$followStatus');
if (ctr.loadingState.value.isSuccess) {

View File

@@ -18,7 +18,7 @@ class FavPgcItem extends StatelessWidget {
});
final FavPgcItemModel item;
final MultiSelectController ctr;
final MultiSelectMixin ctr;
final VoidCallback onSelect;
final VoidCallback onUpdateStatus;

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/http/fav.dart';
import 'package:PiliPlus/http/loading_state.dart';
import 'package:PiliPlus/models/common/fav_order_type.dart';
@@ -11,7 +12,6 @@ import 'package:PiliPlus/services/account_service.dart';
import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/page_utils.dart';
import 'package:PiliPlus/utils/utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart';
@@ -90,61 +90,28 @@ class FavDetailController
order: order.value,
);
void onDelChecked(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('确认删除所选收藏吗?'),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () async {
Get.back();
List<FavDetailItemModel> list = loadingState.value.data!
.where((e) => e.checked == true)
.toList();
@override
void onConfirm() {
showConfirmDialog(
context: Get.context!,
content: '确认删除所选收藏吗?',
title: '提示',
onConfirm: () async {
final checked = allChecked.toSet();
var result = await FavHttp.favVideo(
resources: list
.map((item) => '${item.id}:${item.type}')
.join(','),
resources: checked.map((item) => '${item.id}:${item.type}').join(','),
delIds: mediaId.toString(),
);
if (result['status']) {
List<FavDetailItemModel> dataList = loadingState.value.data!;
List<FavDetailItemModel> remainList = dataList
.toSet()
.difference(list.toSet())
.toList();
afterDelete(checked);
folderInfo
..value.mediaCount -= list.length
..value.mediaCount -= checked.length
..refresh();
if (remainList.isNotEmpty) {
loadingState.value = Success(remainList);
} else {
onReload();
}
SmartDialog.showToast('取消收藏');
checkedCount.value = 0;
enableMultiSelect.value = false;
} else {
SmartDialog.showToast(result['msg']);
}
},
child: const Text('确认'),
),
],
);
},
);
}

View File

@@ -113,7 +113,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
Obx(
() {
return Text(
'已选: ${_favDetailController.checkedCount.value}',
'已选: ${_favDetailController.checkedCount}',
style: const TextStyle(fontSize: 15),
);
},
@@ -327,7 +327,7 @@ class _FavDetailPageState extends State<FavDetailPage> {
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () => _favDetailController.onDelChecked(context),
onPressed: _favDetailController.onConfirm,
child: Text(
'删除',
style: TextStyle(color: theme.colorScheme.error),

View File

@@ -1,3 +1,4 @@
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/history/data.dart';
@@ -26,6 +27,12 @@ class HistoryController
int? max;
int? viewAt;
@override
RxInt get rxCount => baseCtr.checkedCount;
@override
RxBool get enableMultiSelect => baseCtr.enableMultiSelect;
@override
void onInit() {
super.onInit();
@@ -40,36 +47,6 @@ class HistoryController
return super.onRefresh();
}
@override
void onSelect(HistoryItemModel item, [bool disableSelect = true]) {
List<HistoryItemModel> list = loadingState.value.data!;
item.checked = !(item.checked ?? false);
baseCtr.checkedCount.value = list
.where((item) => item.checked == true)
.length;
loadingState.refresh();
if (baseCtr.checkedCount.value == 0) {
baseCtr.enableMultiSelect.value = false;
}
}
@override
void handleSelect([bool checked = false, bool disableSelect = true]) {
if (loadingState.value.isSuccess) {
List<HistoryItemModel>? list = loadingState.value.data;
if (list?.isNotEmpty == true) {
for (HistoryItemModel item in list!) {
item.checked = checked;
}
baseCtr.checkedCount.value = checked ? list.length : 0;
loadingState.refresh();
}
}
if (!checked) {
baseCtr.enableMultiSelect.value = false;
}
}
@override
List<HistoryItemModel>? getDataList(HistoryData response) {
return response.list;
@@ -108,82 +85,43 @@ class HistoryController
// 删除某条历史记录
void delHistory(HistoryItemModel item) {
_onDelete([item]);
_onDelete({item});
}
// 删除已看历史记录
void onDelHistory() {
if (loadingState.value.isSuccess) {
List<HistoryItemModel> list = loadingState.value.data!
final set = loadingState.value.data!
.where((e) => e.progress == -1)
.toList();
if (list.isNotEmpty) {
_onDelete(list);
.toSet();
if (set.isNotEmpty) {
_onDelete(set);
} else {
SmartDialog.showToast('无已看记录');
}
}
}
Future<void> _onDelete(List<HistoryItemModel> result) async {
Future<void> _onDelete(Set<HistoryItemModel> result) async {
SmartDialog.showLoading(msg: '请求中');
List<String> kidList = result.map((item) {
return '${item.history.business}_${item.kid}';
}).toList();
var response = await UserHttp.delHistory(kidList);
final response = await UserHttp.delHistory(
result.map((item) => '${item.history.business}_${item.kid}'),
);
if (response['status']) {
List<HistoryItemModel> remainList = loadingState.value.data!
.toSet()
.difference(result.toSet())
.toList();
if (remainList.isNotEmpty) {
loadingState.value = Success(remainList);
} else {
onReload();
}
if (baseCtr.enableMultiSelect.value) {
baseCtr.checkedCount.value = 0;
baseCtr.enableMultiSelect.value = false;
}
afterDelete(result);
}
SmartDialog.dismiss();
SmartDialog.showToast(response['msg']);
}
// 删除选中的记录
void onDelCheckedHistory(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('确认删除所选历史记录吗?'),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
if (loadingState.value.isSuccess) {
_onDelete(
loadingState.value.data!
.where((e) => e.checked == true)
.toList(),
);
}
},
child: const Text('确认'),
),
],
);
},
@override
void onConfirm() {
showConfirmDialog(
context: Get.context!,
content: '确认删除所选历史记录吗?',
title: '提示',
onConfirm: () => _onDelete(allChecked.toSet()),
);
}

View File

@@ -1,5 +1,6 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/skeleton/video_card_h.dart';
import 'package:PiliPlus/common/widgets/appbar/appbar.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart';
import 'package:PiliPlus/common/widgets/refresh_indicator.dart';
@@ -65,9 +66,10 @@ class _HistoryPageState extends State<HistoryPage>
}
},
child: Scaffold(
appBar: AppBarWidget(
appBar: MultiSelectAppBarWidget(
visible: enableMultiSelect,
child1: AppBar(
ctr: currCtr(),
child: AppBar(
title: const Text('观看记录'),
bottom: _buildPauseTip,
actions: [
@@ -153,36 +155,6 @@ class _HistoryPageState extends State<HistoryPage>
const SizedBox(width: 6),
],
),
child2: AppBar(
bottom: _buildPauseTip,
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),
],
),
),
body: Obx(
() => _historyController.tabs.isNotEmpty
@@ -288,8 +260,7 @@ class _HistoryPageState extends State<HistoryPage>
final item = response[index];
return HistoryItem(
item: item,
ctr: _historyController.baseCtr,
onChoose: () => _historyController.onSelect(item),
ctr: _historyController,
onDelete: (kid, business) =>
_historyController.delHistory(item),
);
@@ -359,32 +330,3 @@ class _HistoryPageState extends State<HistoryPage>
@override
bool get wantKeepAlive => widget.type != null;
}
class AppBarWidget extends StatelessWidget implements PreferredSizeWidget {
const AppBarWidget({
required this.child1,
required this.child2,
required this.visible,
super.key,
});
final PreferredSizeWidget child1;
final PreferredSizeWidget child2;
final bool visible;
@override
Size get preferredSize => child1.preferredSize;
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 500),
transitionBuilder: (Widget child, Animation<double> animation) {
return ScaleTransition(
scale: animation,
child: child,
);
},
child: !visible ? child1 : child2,
);
}
}

View File

@@ -1,6 +1,5 @@
import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/badge.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/http/search.dart';
@@ -9,7 +8,6 @@ import 'package:PiliPlus/models/common/badge_type.dart';
import 'package:PiliPlus/models/common/history_business_type.dart';
import 'package:PiliPlus/models_new/history/list.dart';
import 'package:PiliPlus/pages/common/multi_select_controller.dart';
import 'package:PiliPlus/pages/history/base_controller.dart';
import 'package:PiliPlus/utils/date_util.dart';
import 'package:PiliPlus/utils/duration_util.dart';
import 'package:PiliPlus/utils/feed_back.dart';
@@ -23,15 +21,13 @@ import 'package:material_design_icons_flutter/material_design_icons_flutter.dart
class HistoryItem extends StatelessWidget {
final HistoryItemModel item;
final dynamic ctr;
final Function? onChoose;
final Function(dynamic kid, dynamic business) onDelete;
final MultiSelectMixin ctr;
final void Function(int kid, String business) onDelete;
const HistoryItem({
super.key,
required this.item,
this.ctr,
this.onChoose,
required this.ctr,
required this.onDelete,
});
@@ -45,12 +41,10 @@ class HistoryItem extends StatelessWidget {
type: MaterialType.transparency,
child: InkWell(
onTap: () async {
if (ctr is MultiSelectController || ctr is HistoryBaseController) {
if (ctr.enableMultiSelect.value) {
onChoose?.call();
ctr.onSelect(item);
return;
}
}
if (item.history.business?.contains('article') == true) {
PageUtils.toDupNamed(
'/articlePage',
@@ -97,18 +91,16 @@ class HistoryItem extends StatelessWidget {
}
},
onLongPress: () {
if (ctr is MultiSelectController || ctr is HistoryBaseController) {
if (!ctr.enableMultiSelect.value) {
ctr.enableMultiSelect.value = true;
onChoose?.call();
ctr.onSelect(item);
}
return;
}
imageSaveDialog(
title: item.title,
cover: item.cover,
bvid: bvid,
);
// imageSaveDialog(
// title: item.title,
// cover: item.cover,
// bvid: bvid,
// );
},
child: Stack(
clipBehavior: Clip.none,
@@ -205,7 +197,7 @@ class HistoryItem extends StatelessWidget {
),
onPressed: () {
feedBack();
onChoose?.call();
ctr.onSelect(item);
},
icon: Icon(
Icons.done_all_outlined,
@@ -287,7 +279,7 @@ class HistoryItem extends StatelessWidget {
),
PopupMenuItem<String>(
onTap: () =>
onDelete(item.kid, item.history.business),
onDelete(item.kid!, item.history.business!),
height: 35,
child: const Row(
children: [

View File

@@ -1,12 +1,19 @@
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/history/data.dart';
import 'package:PiliPlus/models_new/history/list.dart';
import 'package:PiliPlus/pages/common/common_search_controller.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 HistorySearchController
extends CommonSearchController<HistoryData, HistoryItemModel> {
extends CommonSearchController<HistoryData, HistoryItemModel>
with
MultiSelectMixin<HistoryItemModel>,
CommonMultiSelectMixin,
DeleteItemMixin {
@override
Future<LoadingState<HistoryData>> customGetData() => UserHttp.searchHistory(
pn: page,
@@ -18,12 +25,14 @@ class HistorySearchController
return response.list;
}
Future<void> onDelHistory(int index, kid, business) async {
String resKid = 'archive_$kid';
Future<void> onDelHistory(int index, kid, String business) async {
final String resKid;
if (business == 'live') {
resKid = 'live_$kid';
} else if (business.contains('article')) {
resKid = 'article_$kid';
} else {
resKid = 'archive_$kid';
}
var res = await UserHttp.delHistory([resKid]);
@@ -34,4 +43,26 @@ class HistorySearchController
}
SmartDialog.showToast(res['msg']);
}
@override
void onConfirm() {
showConfirmDialog(
context: Get.context!,
content: '确认删除所选历史记录吗?',
title: '提示',
onConfirm: () async {
SmartDialog.showLoading(msg: '请求中');
final result = allChecked.toSet();
final kidList = result.map(
(item) => '${item.history.business!}_${item.kid!}',
);
var response = await UserHttp.delHistory(kidList);
if (response['status']) {
afterDelete(result);
}
SmartDialog.dismiss();
SmartDialog.showToast(response['msg']);
},
);
}
}

View File

@@ -1,3 +1,4 @@
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';
@@ -28,6 +29,31 @@ 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<HistoryItemModel> list) {
return SliverGrid(
@@ -42,10 +68,8 @@ class _HistorySearchPageState
return HistoryItem(
item: item,
ctr: controller,
onChoose: null,
onDelete: (kid, business) {
controller.onDelHistory(index, kid, business);
},
onDelete: (kid, business) =>
controller.onDelHistory(index, kid, business),
);
},
),

View File

@@ -27,6 +27,12 @@ class LaterController extends MultiSelectController<LaterData, LaterItemModel> {
final LaterBaseController baseCtr = Get.put(LaterBaseController());
@override
RxBool get enableMultiSelect => baseCtr.enableMultiSelect;
@override
RxInt get rxCount => baseCtr.checkedCount;
@override
Future<LoadingState<LaterData>> customGetData() => UserHttp.seeYouLater(
page: page,
@@ -34,36 +40,6 @@ class LaterController extends MultiSelectController<LaterData, LaterItemModel> {
asc: asc.value,
);
@override
void onSelect(LaterItemModel item, [bool disableSelect = true]) {
List<LaterItemModel> list = loadingState.value.data!;
item.checked = !(item.checked ?? false);
baseCtr.checkedCount.value = list
.where((item) => item.checked == true)
.length;
loadingState.refresh();
if (baseCtr.checkedCount.value == 0) {
baseCtr.enableMultiSelect.value = false;
}
}
@override
void handleSelect([bool checked = false, bool disableSelect = true]) {
if (loadingState.value.isSuccess) {
List<LaterItemModel>? list = loadingState.value.data;
if (list?.isNotEmpty == true) {
for (LaterItemModel item in list!) {
item.checked = checked;
}
baseCtr.checkedCount.value = checked ? list.length : 0;
loadingState.refresh();
}
}
if (!checked) {
baseCtr.enableMultiSelect.value = false;
}
}
@override
void onInit() {
super.onInit();
@@ -102,7 +78,7 @@ class LaterController extends MultiSelectController<LaterData, LaterItemModel> {
TextButton(
onPressed: () async {
Get.back();
var res = await UserHttp.toViewDel(aids: [aid]);
final res = await UserHttp.toViewDel(aids: {aid!});
if (res['status']) {
baseCtr.counts[laterViewType] =
baseCtr.counts[laterViewType]! - 1;
@@ -148,51 +124,24 @@ class LaterController extends MultiSelectController<LaterData, LaterItemModel> {
);
}
void onDelChecked(BuildContext context) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('确认删除所选稍后再看吗?'),
actions: [
TextButton(
onPressed: Get.back,
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () {
Get.back();
_onDelete(
loadingState.value.data!
.where((e) => e.checked == true)
.toList(),
);
},
child: const Text('确认'),
),
],
);
},
@override
void onConfirm() {
showConfirmDialog(
context: Get.context!,
content: '确认删除所选稍后再看吗?',
title: '提示',
onConfirm: () => _onDelete(allChecked.toSet()),
);
}
Future<void> _onDelete(List<LaterItemModel> result) async {
Future<void> _onDelete(Set<LaterItemModel> result) async {
SmartDialog.showLoading(msg: '请求中');
List<int?> aids = result.map((item) => item.aid).toList();
var res = await UserHttp.toViewDel(aids: aids);
final res = await UserHttp.toViewDel(aids: result.map((item) => item.aid!));
if (res['status']) {
Set<LaterItemModel> remainList = loadingState.value.data!
.toSet()
.difference(result.toSet());
afterDelete(result);
baseCtr.counts[laterViewType] =
baseCtr.counts[laterViewType]! - aids.length;
loadingState.value = Success(remainList.toList());
baseCtr.counts[laterViewType]! - result.length;
if (baseCtr.enableMultiSelect.value) {
baseCtr.checkedCount.value = 0;
baseCtr.enableMultiSelect.value = false;

View File

@@ -1,8 +1,8 @@
import 'package:PiliPlus/common/widgets/appbar/appbar.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/models/common/later_view_type.dart';
import 'package:PiliPlus/models_new/later/data.dart';
import 'package:PiliPlus/models_new/later/list.dart';
import 'package:PiliPlus/pages/history/view.dart' show AppBarWidget;
import 'package:PiliPlus/pages/later/base_controller.dart';
import 'package:PiliPlus/pages/later/controller.dart';
import 'package:PiliPlus/utils/accounts.dart';
@@ -128,9 +128,54 @@ class _LaterPageState extends State<LaterPage>
final theme = Theme.of(context);
Color color = theme.colorScheme.secondary;
return AppBarWidget(
return MultiSelectAppBarWidget(
visible: enableMultiSelect,
child1: AppBar(
ctr: currCtr(),
children: [
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () {
final ctr = currCtr();
RequestUtils.onCopyOrMove<LaterData, LaterItemModel>(
context: context,
isCopy: true,
ctr: ctr,
mediaId: null,
mid: ctr.accountService.mid,
);
},
child: Text(
'复制',
style: TextStyle(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () {
final ctr = currCtr();
RequestUtils.onCopyOrMove<LaterData, LaterItemModel>(
context: context,
isCopy: false,
ctr: ctr,
mediaId: null,
mid: ctr.accountService.mid,
);
},
child: Text(
'移动',
style: TextStyle(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
],
child: AppBar(
title: const Text('稍后再看'),
actions: [
IconButton(
@@ -251,76 +296,6 @@ class _LaterPageState extends State<LaterPage>
const SizedBox(width: 8),
],
),
child2: AppBar(
leading: IconButton(
tooltip: '取消',
onPressed: currCtr().handleSelect,
icon: const Icon(Icons.close_outlined),
),
title: Obx(() => Text('已选: ${_baseCtr.checkedCount.value}')),
actions: [
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () => currCtr().handleSelect(true),
child: const Text('全选'),
),
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () {
final ctr = currCtr();
RequestUtils.onCopyOrMove<LaterData, LaterItemModel>(
context: context,
isCopy: true,
ctr: ctr,
mediaId: null,
mid: ctr.accountService.mid,
);
},
child: Text(
'复制',
style: TextStyle(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () {
final ctr = currCtr();
RequestUtils.onCopyOrMove<LaterData, LaterItemModel>(
context: context,
isCopy: false,
ctr: ctr,
mediaId: null,
mid: ctr.accountService.mid,
);
},
child: Text(
'移动',
style: TextStyle(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
TextButton(
style: TextButton.styleFrom(
visualDensity: VisualDensity.compact,
),
onPressed: () => currCtr().onDelChecked(context),
child: Text(
'移除',
style: TextStyle(color: theme.colorScheme.error),
),
),
const SizedBox(width: 6),
],
),
);
}
}

View File

@@ -23,7 +23,7 @@ class LaterSearchController
return response.list;
}
Future<void> toViewDel(BuildContext context, int index, aid) async {
Future<void> toViewDel(BuildContext context, int index, int aid) async {
var res = await UserHttp.toViewDel(aids: [aid]);
if (res['status']) {
loadingState.value.data!.removeAt(index);
@@ -31,4 +31,25 @@ 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']);
// },
// );
// }
}

View File

@@ -27,6 +27,31 @@ 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<LaterItemModel> list) {
return SliverGrid(
@@ -73,7 +98,7 @@ class _LaterSearchPageState
onConfirm: () => controller.toViewDel(
context,
index,
item.aid,
item.aid!,
),
),
icon: Icons.clear,

View File

@@ -14,7 +14,6 @@ import 'package:PiliPlus/models/common/reply/reply_sort_type.dart';
import 'package:PiliPlus/models/common/settings_type.dart';
import 'package:PiliPlus/models/common/super_resolution_type.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/models_new/fav/fav_folder/data.dart';
import 'package:PiliPlus/pages/common/common_slide_page.dart';
import 'package:PiliPlus/pages/home/controller.dart';
import 'package:PiliPlus/pages/hot/controller.dart';
@@ -786,9 +785,8 @@ List<SettingsModel> get extraSettings => [
onTap: () async {
if (Accounts.main.isLogin) {
final res = await FavHttp.allFavFolders(Accounts.main.mid);
if (res['status']) {
final FavFolderData data = res['data'];
final list = data.list;
if (res.isSuccess) {
final list = res.data.list;
if (list.isNullOrEmpty) {
return;
}
@@ -821,7 +819,7 @@ List<SettingsModel> get extraSettings => [
),
);
} else {
SmartDialog.showToast('${res['msg']}');
res.toast();
}
}
},

View File

@@ -18,7 +18,6 @@ import 'package:PiliPlus/http/validate.dart';
import 'package:PiliPlus/http/video.dart';
import 'package:PiliPlus/models/dynamics/result.dart';
import 'package:PiliPlus/models/login/model.dart';
import 'package:PiliPlus/models_new/fav/fav_folder/list.dart';
import 'package:PiliPlus/pages/common/multi_select_controller.dart';
import 'package:PiliPlus/pages/dynamics_tab/controller.dart';
import 'package:PiliPlus/pages/group_panel/view.dart';
@@ -376,11 +375,9 @@ class RequestUtils {
required dynamic mid,
}) {
FavHttp.allFavFolders(mid).then((res) {
if (context.mounted &&
res['status'] &&
(res['data'].list as List?)?.isNotEmpty == true) {
List<FavFolderInfo> list = res['data'].list;
dynamic checkedId;
if (context.mounted && res.dataOrNull?.list?.isNotEmpty == true) {
final list = res.data.list!;
int? checkedId;
showDialog(
context: context,
builder: (context) {
@@ -392,7 +389,7 @@ class RequestUtils {
builder: (context) => Column(
children: List.generate(list.length, (index) {
final item = list[index];
return RadioWidget(
return RadioWidget<int>(
padding: const EdgeInsets.only(left: 14),
title: item.title,
groupValue: checkedId,
@@ -421,9 +418,7 @@ class RequestUtils {
TextButton(
onPressed: () {
if (checkedId != null) {
List resources = ctr.loadingState.value.data!
.where((e) => e.checked == true)
.toList();
Set resources = ctr.allChecked.toSet();
SmartDialog.showLoading();
FavHttp.copyOrMoveFav(
isCopy: isCopy,
@@ -439,22 +434,20 @@ class RequestUtils {
.toList(),
mid: isCopy ? mid : null,
).then((res) {
if (res['status']) {
if (res.isSuccess) {
ctr.handleSelect(false);
if (!isCopy) {
List<T> dataList = ctr.loadingState.value.data!;
List<T> remainList = dataList
.toSet()
.difference(resources.toSet())
.toList();
ctr.loadingState.value = Success(remainList);
ctr.loadingState.value.data!.removeWhere(
resources.contains,
);
ctr.loadingState.refresh();
}
SmartDialog.dismiss();
SmartDialog.showToast('${isCopy ? '复制' : '移动'}成功');
Get.back();
} else {
SmartDialog.dismiss();
SmartDialog.showToast('${res['msg']}');
res.toast();
}
});
}
@@ -466,7 +459,7 @@ class RequestUtils {
},
);
} else {
SmartDialog.showToast('${res['msg']}');
res.toast();
}
});
}