feat: 历史记录多选删除

This commit is contained in:
guozhigq
2023-10-01 10:35:03 +08:00
parent 9a0c9f4021
commit 7fcbe4dd9d
6 changed files with 413 additions and 108 deletions

View File

@ -164,6 +164,9 @@ class Api {
// 清空历史记录
static const String clearHistory = '/x/v2/history/clear';
// 删除某条历史记录
static const String delHistory = '/x/v2/history/delete';
// 热搜
static const String hotSearchList =
'https://s.search.bilibili.com/main/hotword';

View File

@ -231,4 +231,21 @@ class UserHttp {
return {'status': false, 'msg': res.data['message']};
}
}
// 删除历史记录
static Future delHistory(kid) async {
var res = await Request().post(
Api.delHistory,
queryParameters: {
'kid': kid,
'jsonp': 'jsonp',
'csrf': await Request.getCsrf(),
},
);
if (res.data['code'] == 0) {
return {'status': true, 'msg': '已删除'};
} else {
return {'status': false, 'msg': res.data['message']};
}
}
}

View File

@ -79,6 +79,7 @@ class HisListItem {
this.kid,
this.tagName,
this.liveStatus,
this.checked,
});
String? title;
@ -105,6 +106,7 @@ class HisListItem {
int? kid;
String? tagName;
int? liveStatus;
bool? checked;
HisListItem.fromJson(Map<String, dynamic> json) {
title = json['title'];
@ -131,6 +133,7 @@ class HisListItem {
kid = json['kid'];
tagName = json['tag_name'];
liveStatus = json['live_status'];
checked = false;
}
}

View File

@ -8,11 +8,13 @@ import 'package:pilipala/utils/storage.dart';
class HistoryController extends GetxController {
final ScrollController scrollController = ScrollController();
RxList<HisListItem> historyList = [HisListItem()].obs;
RxList<HisListItem> historyList = <HisListItem>[].obs;
RxBool isLoadingMore = false.obs;
RxBool pauseStatus = false.obs;
Box localCache = GStrorage.localCache;
RxBool isLoading = false.obs;
RxBool enableMultiple = false.obs;
RxInt checkedCount = 0.obs;
@override
void onInit() {
@ -56,7 +58,8 @@ class HistoryController extends GetxController {
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: Text(!pauseStatus.value ? '你要暂停历史记录功能吗?' : '要恢复历史记录功能吗?'),
content:
Text(!pauseStatus.value ? '啊叻?你要暂停历史记录功能吗?' : '啊叻?要恢复历史记录功能吗?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
@ -97,7 +100,7 @@ class HistoryController extends GetxController {
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('你要清空历史记录功能吗?'),
content: const Text('啊叻?你要清空历史记录功能吗?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
@ -120,4 +123,75 @@ class HistoryController extends GetxController {
},
);
}
// 删除某条历史记录
Future delHistory(kid, business) async {
String resKid = 'archive_$kid';
if (business == 'live') {
resKid = 'live_$kid';
} else if (business.contains('article')) {
resKid = 'article_$kid';
}
var res = await UserHttp.delHistory(resKid);
if (res['status']) {
historyList.removeWhere((e) => e.kid == kid);
SmartDialog.showToast(res['msg']);
}
}
// 删除已看历史记录
Future onDelHistory() async {
/// TODO 优化
List<HisListItem> result =
historyList.where((e) => e.progress == -1).toList();
for (HisListItem i in result) {
String resKid = 'archive_${i.kid}';
await UserHttp.delHistory(resKid);
historyList.removeWhere((e) => e.kid == i.kid);
}
SmartDialog.showToast('操作完成');
}
// 删除选中的记录
Future onDelCheckedHistory() async {
SmartDialog.show(
useSystem: true,
animationType: SmartAnimationType.centerFade_otherSlide,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('提示'),
content: const Text('确认删除所选历史记录吗?'),
actions: [
TextButton(
onPressed: () => SmartDialog.dismiss(),
child: Text(
'取消',
style: TextStyle(
color: Theme.of(context).colorScheme.outline,
),
),
),
TextButton(
onPressed: () async {
/// TODO 优化
await SmartDialog.dismiss();
SmartDialog.showLoading(msg: '请求中');
List<HisListItem> result =
historyList.where((e) => e.checked!).toList();
for (HisListItem i in result) {
String resKid = 'archive_${i.kid}';
await UserHttp.delHistory(resKid);
historyList.removeWhere((e) => e.kid == i.kid);
}
checkedCount.value = 0;
SmartDialog.dismiss();
},
child: const Text('确认'),
)
],
);
},
);
}
}

View File

@ -39,6 +39,20 @@ class _HistoryPageState extends State<HistoryPage> {
);
}
// 选中
onChoose(index) {
_historyController.historyList[index].checked =
!_historyController.historyList[index].checked!;
_historyController.checkedCount.value =
_historyController.historyList.where((item) => item.checked!).length;
_historyController.historyList.refresh();
}
// 更新多选状态
onUpdateMultiple() {
setState(() {});
}
@override
void dispose() {
scrollController.removeListener(() {});
@ -48,44 +62,115 @@ class _HistoryPageState extends State<HistoryPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
titleSpacing: 0,
centerTitle: false,
title: Text(
'观看记录',
style: Theme.of(context).textTheme.titleMedium,
),
actions: [
PopupMenuButton<String>(
onSelected: (String type) {
// 处理菜单项选择的逻辑
switch (type) {
case 'pause':
_historyController.onPauseHistory();
break;
case 'clear':
_historyController.onClearHistory();
break;
default:
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'pause',
child: Obx(
() => Text(!_historyController.pauseStatus.value
? '暂停观看记录'
: '恢复观看记录'),
),
),
const PopupMenuItem<String>(
value: 'clear',
child: Text('清空观看记录'),
),
],
appBar: AppBarWidget(
visible: _historyController.enableMultiple.value,
child1: AppBar(
titleSpacing: 0,
centerTitle: false,
leading: IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.arrow_back_outlined),
),
const SizedBox(width: 6),
],
title: Text(
'观看记录',
style: Theme.of(context).textTheme.titleMedium,
),
actions: [
// TextButton(
// onPressed: () {
// _historyController.enableMultiple.value = true;
// setState(() {});
// },
// child: const Text('多选'),
// ),
PopupMenuButton<String>(
onSelected: (String type) {
// 处理菜单项选择的逻辑
switch (type) {
case 'pause':
_historyController.onPauseHistory();
break;
case 'clear':
_historyController.onClearHistory();
break;
case 'del':
_historyController.onDelHistory();
break;
case 'multiple':
_historyController.enableMultiple.value = true;
setState(() {});
break;
default:
}
},
itemBuilder: (BuildContext context) => <PopupMenuEntry<String>>[
PopupMenuItem<String>(
value: 'pause',
child: Obx(
() => Text(!_historyController.pauseStatus.value
? '暂停观看记录'
: '恢复观看记录'),
),
),
const PopupMenuItem<String>(
value: 'clear',
child: Text('清空观看记录'),
),
const PopupMenuItem<String>(
value: 'del',
child: Text('删除已看记录'),
),
const PopupMenuItem<String>(
value: 'multiple',
child: Text('多选删除'),
),
],
),
const SizedBox(width: 6),
],
),
child2: AppBar(
titleSpacing: 0,
centerTitle: false,
leading: IconButton(
onPressed: () {
_historyController.enableMultiple.value = false;
for (var item in _historyController.historyList) {
item.checked = false;
}
_historyController.checkedCount.value = 0;
setState(() {});
},
icon: const Icon(Icons.close_outlined),
),
title: Obx(
() => Text(
'已选择${_historyController.checkedCount.value}',
style: Theme.of(context).textTheme.titleMedium,
),
),
actions: [
TextButton(
onPressed: () {
for (var item in _historyController.historyList) {
item.checked = true;
}
_historyController.checkedCount.value =
_historyController.historyList.length;
_historyController.historyList.refresh();
},
child: const Text('全选'),
),
TextButton(
onPressed: () => _historyController.onDelCheckedHistory(),
child: Text(
'删除',
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
),
const SizedBox(width: 6),
],
),
),
body: RefreshIndicator(
onRefresh: () async {
@ -112,6 +197,9 @@ class _HistoryPageState extends State<HistoryPage> {
return HistoryItem(
videoItem:
_historyController.historyList[index],
ctr: _historyController,
onChoose: () => onChoose(index),
onUpdateMultiple: () => onUpdateMultiple(),
);
},
childCount:
@ -147,6 +235,36 @@ class _HistoryPageState extends State<HistoryPage> {
],
),
),
// bottomNavigationBar: BottomAppBar(),
);
}
}
class AppBarWidget extends StatelessWidget implements PreferredSizeWidget {
const AppBarWidget({
required this.child1,
required this.child2,
required this.visible,
Key? key,
}) : super(key: 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

@ -11,12 +11,23 @@ import 'package:pilipala/models/bangumi/info.dart';
import 'package:pilipala/models/common/business_type.dart';
import 'package:pilipala/models/common/search_type.dart';
import 'package:pilipala/models/live/item.dart';
import 'package:pilipala/pages/history/index.dart';
import 'package:pilipala/utils/feed_back.dart';
import 'package:pilipala/utils/id_utils.dart';
import 'package:pilipala/utils/utils.dart';
class HistoryItem extends StatelessWidget {
final dynamic videoItem;
const HistoryItem({super.key, required this.videoItem});
final HistoryController? ctr;
final Function? onChoose;
final Function? onUpdateMultiple;
const HistoryItem({
super.key,
required this.videoItem,
this.ctr,
this.onChoose,
this.onUpdateMultiple,
});
@override
Widget build(BuildContext context) {
@ -25,6 +36,11 @@ class HistoryItem extends StatelessWidget {
String heroTag = Utils.makeHeroTag(aid);
return InkWell(
onTap: () async {
if (ctr!.enableMultiple.value) {
feedBack();
onChoose!();
return;
}
if (videoItem.history.business.contains('article')) {
int cid = videoItem.history.cid ??
// videoItem.history.oid ??
@ -115,6 +131,14 @@ class HistoryItem extends StatelessWidget {
arguments: {'heroTag': heroTag, 'pic': videoItem.cover});
}
},
onLongPress: () {
if (!ctr!.enableMultiple.value) {
feedBack();
ctr!.enableMultiple.value = true;
onChoose!();
onUpdateMultiple!();
}
},
child: Column(
children: [
Padding(
@ -130,53 +154,105 @@ class HistoryItem extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: (videoItem.cover != ''
? videoItem.cover
: videoItem.covers.first),
width: maxWidth,
height: maxHeight,
Stack(
children: [
AspectRatio(
aspectRatio: StyleString.aspectRatio,
child: LayoutBuilder(
builder: (context, boxConstraints) {
double maxWidth = boxConstraints.maxWidth;
double maxHeight = boxConstraints.maxHeight;
return Stack(
children: [
Hero(
tag: heroTag,
child: NetworkImgLayer(
src: (videoItem.cover != ''
? videoItem.cover
: videoItem.covers.first),
width: maxWidth,
height: maxHeight,
),
),
if (!BusinessType
.hiddenDurationType.hiddenDurationType
.contains(videoItem.history.business))
PBadge(
text: videoItem.progress == -1
? '已看完'
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
right: 6.0,
bottom: 6.0,
type: 'gray',
),
// 右上角
if (BusinessType.showBadge.showBadge
.contains(
videoItem.history.business) ||
videoItem.history.business ==
BusinessType.live.type)
PBadge(
text: videoItem.badge,
top: 6.0,
right: 6.0,
bottom: null,
left: null,
),
],
);
},
),
),
Obx(
() => Positioned.fill(
child: AnimatedOpacity(
opacity: ctr!.enableMultiple.value ? 1 : 0,
duration: const Duration(milliseconds: 200),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(12),
color: Colors.black.withOpacity(0.6),
),
child: Center(
child: SizedBox(
width: 34,
height: 34,
child: IconButton(
style: ButtonStyle(
padding: MaterialStateProperty.all(
EdgeInsets.zero),
backgroundColor:
MaterialStateProperty.resolveWith(
(states) {
return Colors.white
.withOpacity(0.8);
},
),
),
onPressed: () {
feedBack();
onChoose!();
},
icon: Icon(
Icons.done_all_outlined,
color: videoItem.checked
? Theme.of(context)
.colorScheme
.primary
: Theme.of(context)
.colorScheme
.outline,
),
),
),
),
),
if (!BusinessType
.hiddenDurationType.hiddenDurationType
.contains(videoItem.history.business))
PBadge(
text: videoItem.progress == -1
? '已看完'
: '${Utils.timeFormat(videoItem.progress!)}/${Utils.timeFormat(videoItem.duration!)}',
right: 6.0,
bottom: 6.0,
type: 'gray',
),
// 右上角
if (BusinessType.showBadge.showBadge
.contains(videoItem.history.business) ||
videoItem.history.business ==
BusinessType.live.type)
PBadge(
text: videoItem.badge,
top: 6.0,
right: 6.0,
bottom: null,
left: null,
),
],
);
},
),
),
),
),
],
),
VideoContent(videoItem: videoItem)
VideoContent(videoItem: videoItem, ctr: ctr)
],
),
);
@ -191,7 +267,8 @@ class HistoryItem extends StatelessWidget {
class VideoContent extends StatelessWidget {
final dynamic videoItem;
const VideoContent({super.key, required this.videoItem});
final HistoryController? ctr;
const VideoContent({super.key, required this.videoItem, this.ctr});
@override
Widget build(BuildContext context) {
@ -244,26 +321,26 @@ class VideoContent extends StatelessWidget {
Theme.of(context).textTheme.labelMedium!.fontSize,
color: Theme.of(context).colorScheme.outline),
),
if (videoItem.badge != '番剧' &&
!videoItem.tagName.contains('动画') &&
videoItem.history.business != 'live' &&
!videoItem.history.business.contains('article'))
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: '稍后再看',
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: 14,
),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
SizedBox(
width: 24,
height: 24,
child: PopupMenuButton<String>(
padding: EdgeInsets.zero,
tooltip: '功能菜单',
icon: Icon(
Icons.more_vert_outlined,
color: Theme.of(context).colorScheme.outline,
size: 14,
),
position: PopupMenuPosition.under,
// constraints: const BoxConstraints(maxHeight: 35),
onSelected: (String type) {},
itemBuilder: (BuildContext context) =>
<PopupMenuEntry<String>>[
if (videoItem.badge != '番剧' &&
!videoItem.tagName.contains('动画') &&
videoItem.history.business != 'live' &&
!videoItem.history.business.contains('article'))
PopupMenuItem<String>(
onTap: () async {
var res = await UserHttp.toViewLater(
@ -280,9 +357,22 @@ class VideoContent extends StatelessWidget {
],
),
),
],
),
PopupMenuItem<String>(
onTap: () => ctr!.delHistory(
videoItem.kid, videoItem.history.business),
value: 'pause',
height: 35,
child: const Row(
children: [
Icon(Icons.close_outlined, size: 16),
SizedBox(width: 6),
Text('删除记录', style: TextStyle(fontSize: 13))
],
),
),
],
),
),
],
),
],