refa dm block

Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
bggRGjQaUbCoE
2025-06-10 16:18:51 +08:00
parent 206602e49a
commit bc2de4828b
6 changed files with 167 additions and 135 deletions

View File

@@ -28,10 +28,7 @@ class DanmakuFilterHttp {
}, },
); );
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {'status': true};
'status': true,
'msg': '操作成功',
};
} else { } else {
return { return {
'status': false, 'status': false,

View File

@@ -0,0 +1,9 @@
enum DmBlockType {
keyword('关键词'),
regex('正则'),
uid('用户'),
;
final String label;
const DmBlockType(this.label);
}

View File

@@ -1,13 +1,28 @@
class DanmakuBlockDataModel { class DanmakuBlockDataModel {
List<SimpleRule>? rule; late List<SimpleRule> rule;
late List<SimpleRule> rule1;
late List<SimpleRule> rule2;
String? toast; String? toast;
int? valid; int? valid;
int? ver; int? ver;
DanmakuBlockDataModel({this.rule, this.toast, this.valid, this.ver});
DanmakuBlockDataModel.fromJson(Map<String, dynamic> json) { DanmakuBlockDataModel.fromJson(Map<String, dynamic> json) {
rule = (json['rule'] as List?)?.map((v) => SimpleRule.fromJson(v)).toList(); rule = <SimpleRule>[];
rule1 = <SimpleRule>[];
rule2 = <SimpleRule>[];
if ((json['rule'] as List?)?.isNotEmpty == true) {
for (var e in json['rule']) {
SimpleRule item = SimpleRule.fromJson(e);
switch (item.type) {
case 0:
rule.add(item);
case 1:
rule1.add(item);
case 2:
rule2.add(item);
}
}
}
toast = json['toast'] == '' ? null : json['toast']; toast = json['toast'] == '' ? null : json['toast'];
valid = json['valid']; valid = json['valid'];
ver = json['ver']; ver = json['ver'];
@@ -17,8 +32,7 @@ class DanmakuBlockDataModel {
class SimpleRule { class SimpleRule {
late final int id; late final int id;
late final int type; late final int type;
late String filter; late final String filter;
SimpleRule(this.id, this.type, this.filter);
SimpleRule.fromJson(Map<String, dynamic> json) { SimpleRule.fromJson(Map<String, dynamic> json) {
id = json['id']; id = json['id'];

View File

@@ -1,4 +1,5 @@
import 'package:PiliPlus/grpc/bilibili/community/service/dm/v1.pb.dart'; import 'package:PiliPlus/grpc/bilibili/community/service/dm/v1.pb.dart';
import 'package:PiliPlus/models/user/danmaku_block.dart';
class RuleFilter { class RuleFilter {
static final _regExp = RegExp(r'^/(.*)/$'); static final _regExp = RegExp(r'^/(.*)/$');
@@ -14,23 +15,17 @@ class RuleFilter {
count ?? dmFilterString.length + dmRegExp.length + dmUid.length; count ?? dmFilterString.length + dmRegExp.length + dmUid.length;
} }
RuleFilter.fromRuleTypeEntires( RuleFilter.fromRuleTypeEntires(List<List<SimpleRule>> rules) {
Iterable<MapEntry<int, Map<int, String>>> rules) { dmFilterString = rules[0].map((e) => e.filter).toList();
for (var rule in rules) {
switch (rule.key) { dmRegExp = rules[1]
case 0: .map((e) => RegExp(
dmFilterString.addAll(rule.value.values); _regExp.matchAsPrefix(e.filter)?.group(1) ?? e.filter,
break; caseSensitive: false))
case 1: .toList();
dmRegExp.addAll(rule.value.values.map((i) => RegExp(
_regExp.matchAsPrefix(i)?.group(1) ?? i, dmUid = rules[2].map((e) => e.filter).toSet();
caseSensitive: false)));
break;
case 2:
dmUid.addAll(rule.value.values);
break;
}
}
count = dmFilterString.length + dmRegExp.length + dmUid.length; count = dmFilterString.length + dmRegExp.length + dmUid.length;
} }

View File

@@ -1,17 +1,24 @@
import 'dart:convert';
import 'package:PiliPlus/http/danmaku_block.dart'; import 'package:PiliPlus/http/danmaku_block.dart';
import 'package:PiliPlus/models/common/dm_block_type.dart';
import 'package:PiliPlus/models/user/danmaku_block.dart'; import 'package:PiliPlus/models/user/danmaku_block.dart';
import 'package:crclib/catalog.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
class DanmakuBlockController extends GetxController class DanmakuBlockController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
final ruleTypes = RxMap<int, Map<int, String>>({0: {}, 1: {}, 2: {}}); late final List<RxList<SimpleRule>> rules =
List.generate(DmBlockType.values.length, (_) => <SimpleRule>[].obs);
late TabController tabController; late TabController tabController;
@override @override
void onInit() { void onInit() {
super.onInit(); super.onInit();
queryDanmakuFilter();
tabController = TabController(length: 3, vsync: this); tabController = TabController(length: 3, vsync: this);
} }
@@ -27,12 +34,9 @@ class DanmakuBlockController extends GetxController
SmartDialog.dismiss(); SmartDialog.dismiss();
if (result['status']) { if (result['status']) {
DanmakuBlockDataModel data = result['data']; DanmakuBlockDataModel data = result['data'];
if (data.rule?.isNotEmpty == true) { rules[0].addAll(data.rule);
for (var rule in data.rule!) { rules[1].addAll(data.rule1);
ruleTypes[rule.type]![rule.id] = rule.filter; rules[2].addAll(data.rule2);
}
ruleTypes.refresh();
}
if (data.toast != null) { if (data.toast != null) {
SmartDialog.showToast(data.toast!); SmartDialog.showToast(data.toast!);
} }
@@ -41,29 +45,30 @@ class DanmakuBlockController extends GetxController
} }
} }
Future<void> danmakuFilterDel(int type, int id) async { Future<void> danmakuFilterDel(int tabIndex, int itemIndex, int id) async {
SmartDialog.showLoading(msg: '正在删除弹幕屏蔽规则……'); SmartDialog.showLoading(msg: '正在删除弹幕屏蔽规则……');
var result = await DanmakuFilterHttp.danmakuFilterDel(ids: id); var result = await DanmakuFilterHttp.danmakuFilterDel(ids: id);
SmartDialog.dismiss(); SmartDialog.dismiss();
if (result['status']) { if (result['status']) {
ruleTypes rules[tabIndex].removeAt(itemIndex);
..[type]!.remove(id) SmartDialog.showToast('删除成功');
..refresh(); } else {
}
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);
} }
}
Future<void> danmakuFilterAdd( Future<void> danmakuFilterAdd(
{required String filter, required int type}) async { {required String filter, required int type}) async {
if (type == 2) {
filter = Crc32Xz().convert(utf8.encode(filter)).toRadixString(16);
}
SmartDialog.showLoading(msg: '正在添加弹幕屏蔽规则……'); SmartDialog.showLoading(msg: '正在添加弹幕屏蔽规则……');
var result = var result =
await DanmakuFilterHttp.danmakuFilterAdd(filter: filter, type: type); await DanmakuFilterHttp.danmakuFilterAdd(filter: filter, type: type);
SmartDialog.dismiss(); SmartDialog.dismiss();
if (result['status']) { if (result['status']) {
SimpleRule rule = result['data']; SimpleRule rule = result['data'];
ruleTypes rules[type].add(rule);
..[type]![rule.id] = rule.filter
..refresh();
SmartDialog.showToast('添加成功'); SmartDialog.showToast('添加成功');
} else { } else {
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);

View File

@@ -1,9 +1,15 @@
import 'package:PiliPlus/common/widgets/dialog/dialog.dart';
import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart';
import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart';
import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart';
import 'package:PiliPlus/models/common/dm_block_type.dart';
import 'package:PiliPlus/models/user/danmaku_block.dart';
import 'package:PiliPlus/models/user/danmaku_rule.dart'; import 'package:PiliPlus/models/user/danmaku_rule.dart';
import 'package:PiliPlus/pages/danmaku_block/controller.dart'; import 'package:PiliPlus/pages/danmaku_block/controller.dart';
import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart';
import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -15,75 +21,130 @@ class DanmakuBlockPage extends StatefulWidget {
} }
class _DanmakuBlockPageState extends State<DanmakuBlockPage> { class _DanmakuBlockPageState extends State<DanmakuBlockPage> {
final DanmakuBlockController _danmakuBlockController = final DanmakuBlockController _controller = Get.put(DanmakuBlockController());
Get.put(DanmakuBlockController());
final ScrollController scrollController = ScrollController();
late PlPlayerController plPlayerController; late PlPlayerController plPlayerController;
static const Map<int, String> ruleLabels = {
0: '关键词',
1: '正则',
2: '用户',
};
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_danmakuBlockController.queryDanmakuFilter();
});
plPlayerController = Get.arguments as PlPlayerController; plPlayerController = Get.arguments as PlPlayerController;
} }
@override @override
void dispose() { void dispose() {
final ruleFilter = RuleFilter.fromRuleTypeEntires( final ruleFilter = RuleFilter.fromRuleTypeEntires(_controller.rules);
_danmakuBlockController.ruleTypes.entries);
plPlayerController.filters = ruleFilter; plPlayerController.filters = ruleFilter;
scrollController.dispose();
GStorage.localCache.put(LocalCacheKey.danmakuFilterRules, ruleFilter); GStorage.localCache.put(LocalCacheKey.danmakuFilterRules, ruleFilter);
super.dispose(); super.dispose();
} }
void _showAddDialog(int type) { @override
final TextEditingController textController = TextEditingController(); Widget build(BuildContext context) {
late String hintText; return Scaffold(
switch (type) { resizeToAvoidBottomInset: false,
case 0: appBar: AppBar(
hintText = '输入过滤的关键词,其它类别请切换标签页后添加'; title: const Text('弹幕屏蔽'),
break; bottom: TabBar(
case 1: controller: _controller.tabController,
hintText = '输入//之间的正则表达式,无需包含头尾的"/"'; tabs: DmBlockType.values
break; .map((e) => Obx(() => Tab(
case 2: text: '${e.label}(${_controller.rules[e.index].length})')))
hintText = '输入经CRC32B即小写16进制的CRC32哈希后的用户UID'; .toList(),
break; ),
),
body: tabBarView(
controller: _controller.tabController,
children: DmBlockType.values
.map((e) => KeepAliveWrapper(
builder: (context) => Obx(() =>
tabViewBuilder(e.index, _controller.rules[e.index])),
))
.toList(),
),
floatingActionButton: FloatingActionButton(
onPressed: () =>
_showAddDialog(DmBlockType.values[_controller.tabController.index]),
child: const Icon(Icons.add),
),
);
} }
Widget tabViewBuilder(int tabIndex, List<SimpleRule> list) {
if (list.isEmpty) {
return scrollErrorWidget();
}
return ListView.builder(
itemCount: list.length,
padding:
EdgeInsets.only(bottom: MediaQuery.paddingOf(context).bottom + 80),
itemBuilder: (context, itemIndex) {
final SimpleRule item = list[itemIndex];
return ListTile(
title: Text(
item.filter,
style: Theme.of(context).textTheme.bodyMedium,
),
trailing: IconButton(
icon: const Icon(Icons.delete_outlined),
onPressed: () => showConfirmDialog(
context: context,
title: '确定删除该规则?',
onConfirm: () => _controller.danmakuFilterDel(
tabIndex,
itemIndex,
item.id,
),
),
),
);
},
);
}
void _showAddDialog(DmBlockType type) {
String filter = '';
String hintText = switch (type) {
DmBlockType.keyword => '输入过滤的关键词,其它类别请切换标签页后添加',
DmBlockType.regex => '输入//之间的正则表达式,无需包含头尾的"/"',
DmBlockType.uid => '输入用户UID',
};
final isUid = type == DmBlockType.uid;
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (context) {
return AlertDialog( return AlertDialog(
title: Text('添加新的${ruleLabels[type]}规则'), title: Text('添加新的${type.label}规则'),
content: Column(mainAxisSize: MainAxisSize.min, children: [ content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(hintText), Text(hintText),
TextField( TextFormField(
controller: textController,
autofocus: true, autofocus: true,
initialValue: filter,
onChanged: (value) => filter = value,
keyboardType: isUid ? TextInputType.number : null,
inputFormatters: isUid
? [FilteringTextInputFormatter.allow(RegExp(r'\d+'))]
: null,
) )
]), ],
actions: <Widget>[ ),
actions: [
TextButton( TextButton(
onPressed: Navigator.of(context).pop, onPressed: Get.back,
child: const Text('取消'), child: Text(
'取消',
style: TextStyle(color: Theme.of(context).colorScheme.outline),
),
), ),
TextButton( TextButton(
child: const Text('添加'), child: const Text('添加'),
onPressed: () { onPressed: () {
String filter = textController.text;
if (filter.isNotEmpty) { if (filter.isNotEmpty) {
_danmakuBlockController.danmakuFilterAdd( Get.back();
filter: filter, type: type); _controller.danmakuFilterAdd(
Navigator.of(context).pop(); filter: filter, type: type.index);
} else { } else {
SmartDialog.showToast('输入内容不能为空'); SmartDialog.showToast('输入内容不能为空');
} }
@@ -94,53 +155,4 @@ class _DanmakuBlockPageState extends State<DanmakuBlockPage> {
}, },
); );
} }
@override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomInset: false,
appBar: AppBar(
title: TabBar(controller: _danmakuBlockController.tabController, tabs: [
for (var i = 0; i < ruleLabels.length; i++)
Obx(() => Tab(
text:
'${ruleLabels[i]}(${_danmakuBlockController.ruleTypes[i]!.length})')),
]),
),
body: tabBarView(
controller: _danmakuBlockController.tabController,
children: [
for (var i = 0; i < ruleLabels.length; i++)
Obx(() => tabViewBuilder(
i, _danmakuBlockController.ruleTypes[i]!.entries.toList())),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () =>
_showAddDialog(_danmakuBlockController.tabController.index),
child: const Icon(Icons.add),
),
);
}
Widget tabViewBuilder(int tabIndex, List<MapEntry<int, String>> list) {
return ListView.builder(
controller: scrollController,
itemCount: list.length,
padding: const EdgeInsets.only(bottom: 100),
itemBuilder: (BuildContext context, int listIndex) {
return ListTile(
title: Text(
list[listIndex].value,
style: Theme.of(context).textTheme.bodyMedium,
),
trailing: IconButton(
icon: const Icon(Icons.delete),
onPressed: () => _danmakuBlockController.danmakuFilterDel(
tabIndex, list[listIndex].key),
),
);
},
);
}
} }