opt: danmaku filter (#486)

This commit is contained in:
My-Responsitories
2025-03-23 12:07:57 +08:00
committed by GitHub
parent 066f3d4132
commit 99b14d0f0e
9 changed files with 141 additions and 189 deletions

View File

@@ -39,7 +39,8 @@ class DanmakuFilterHttp {
} }
} }
static Future danmakuFilterAdd({required String filter, required int type}) async { static Future danmakuFilterAdd(
{required String filter, required int type}) async {
var res = await Request().post( var res = await Request().post(
Api.danmakuFilterAdd, Api.danmakuFilterAdd,
queryParameters: { queryParameters: {
@@ -51,7 +52,7 @@ class DanmakuFilterHttp {
if (res.data['code'] == 0) { if (res.data['code'] == 0) {
return { return {
'status': true, 'status': true,
'data': Rule.fromJson(res.data['data']), 'data': SimpleRule.fromJson(res.data['data']),
}; };
} else { } else {
return { return {

View File

@@ -1,5 +1,5 @@
class DanmakuBlockDataModel { class DanmakuBlockDataModel {
List<Rule>? rule; List<SimpleRule>? rule;
String? toast; String? toast;
int? valid; int? valid;
int? ver; int? ver;
@@ -7,88 +7,22 @@ class DanmakuBlockDataModel {
DanmakuBlockDataModel({this.rule, this.toast, this.valid, this.ver}); DanmakuBlockDataModel({this.rule, this.toast, this.valid, this.ver});
DanmakuBlockDataModel.fromJson(Map<String, dynamic> json) { DanmakuBlockDataModel.fromJson(Map<String, dynamic> json) {
if (json['rule'] != null) { rule = (json['rule'] as List?)?.map((v) => SimpleRule.fromJson(v)).toList();
rule = <Rule>[];
json['rule'].forEach((v) {
rule!.add(Rule.fromJson(v));
});
}
toast = json['toast']; toast = json['toast'];
valid = json['valid']; valid = json['valid'];
ver = json['ver']; ver = json['ver'];
} }
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (rule != null) {
data['rule'] = rule!.map((v) => v.toJson()).toList();
}
data['toast'] = toast;
data['valid'] = valid;
data['ver'] = ver;
return data;
}
}
class Rule {
int? id;
int? mid;
int? type;
String? filter;
String? comment;
int? ctime;
int? mtime;
Rule(
{this.id,
this.mid,
this.type,
this.filter,
this.comment,
this.ctime,
this.mtime});
Rule.fromJson(Map<String, dynamic> json) {
id = json['id'];
mid = json['mid'];
type = json['type'];
filter = json['filter'];
comment = json['comment'];
ctime = json['ctime'];
mtime = json['mtime'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['mid'] = mid;
data['type'] = type;
data['filter'] = filter;
data['comment'] = comment;
data['ctime'] = ctime;
data['mtime'] = mtime;
return data;
}
} }
class SimpleRule { class SimpleRule {
final int id; late final int id;
final int type; late final int type;
String filter; late String filter;
SimpleRule(this.id, this.type, this.filter); SimpleRule(this.id, this.type, this.filter);
Map<String, dynamic> toMap() {
return {
'id': id,
'type': type,
'filter': filter,
};
}
factory SimpleRule.fromMap(Map<String, dynamic> map) { SimpleRule.fromJson(Map<String, dynamic> json) {
return SimpleRule( id = json['id'];
map['id'], type = json['type'];
map['type'], filter = json['filter'];
map['filter'],
);
} }
} }

View File

@@ -0,0 +1,44 @@
import 'package:PiliPlus/grpc/dm/v1/dm.pb.dart';
class RuleFilter {
static final _regExp = RegExp(r'^/(.*)/$');
List<String> dmFilterString = [];
List<RegExp> dmRegExp = [];
Set<String> dmUid = {};
int count = 0;
RuleFilter(this.dmFilterString, this.dmRegExp, this.dmUid, [int? count]) {
this.count =
count ?? dmFilterString.length + dmRegExp.length + dmUid.length;
}
RuleFilter.fromRuleTypeEntires(
Iterable<MapEntry<int, Map<int, String>>> rules) {
for (var rule in rules) {
switch (rule.key) {
case 0:
dmFilterString.addAll(rule.value.values);
break;
case 1:
dmRegExp.addAll(rule.value.values.map((i) => RegExp(
_regExp.matchAsPrefix(i)?.group(1) ?? i,
caseSensitive: false)));
break;
case 2:
dmUid.addAll(rule.value.values);
break;
}
}
count = dmFilterString.length + dmRegExp.length + dmUid.length;
}
RuleFilter.empty();
bool retain(DanmakuElem elem) {
return !(dmUid.contains(elem.midHash) ||
dmFilterString.any((i) => elem.content.contains(i)) ||
dmRegExp.any((i) => i.hasMatch(elem.content)));
}
}

View File

@@ -0,0 +1,36 @@
import 'package:PiliPlus/models/user/danmaku_rule.dart';
import 'package:hive/hive.dart';
class RuleFilterAdapter extends TypeAdapter<RuleFilter> {
@override
final int typeId = 12;
@override
RuleFilter read(BinaryReader reader) {
return RuleFilter(
reader.readStringList(),
reader
.readStringList()
.map((i) => RegExp(i, caseSensitive: false))
.toList(),
reader.readStringList().toSet());
}
@override
void write(BinaryWriter writer, RuleFilter obj) {
writer
..writeStringList(obj.dmFilterString)
..writeStringList(obj.dmRegExp.map((i) => i.pattern).toList())
..writeStringList(obj.dmUid.toList());
}
@override
int get hashCode => typeId.hashCode;
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is RuleFilterAdapter &&
runtimeType == other.runtimeType &&
typeId == other.typeId;
}

View File

@@ -69,21 +69,13 @@ class PlDanmakuController {
queryDanmaku(segmentIndex); queryDanmaku(segmentIndex);
} }
if (plPlayerController.danmakuWeight == 0 && if (plPlayerController.danmakuWeight == 0 &&
plPlayerController.filterCount == 0) { plPlayerController.filters.count == 0) {
return dmSegMap[progress ~/ 100]; return dmSegMap[progress ~/ 100];
} else { } else {
return dmSegMap[progress ~/ 100] return dmSegMap[progress ~/ 100]
?.where( ?..retainWhere((element) =>
(element) => element.weight >= plPlayerController.danmakuWeight) element.weight >= plPlayerController.danmakuWeight &&
.where(filterDanmaku) plPlayerController.filters.retain(element));
.toList();
} }
} }
bool filterDanmaku(DanmakuElem elem) {
return !(plPlayerController.dmUid.contains(elem.midHash) ||
plPlayerController.dmFilterString
.any((i) => elem.content.contains(i)) ||
plPlayerController.dmRegExp.any((i) => i.hasMatch(elem.content)));
}
} }

View File

@@ -1,3 +1,4 @@
import 'package:PiliPlus/models/user/danmaku_rule.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';
@@ -38,38 +39,11 @@ class _DanmakuBlockPageState extends State<DanmakuBlockPage> {
@override @override
void dispose() { void dispose() {
final regExp = RegExp(r'^/(.*)/$'); final ruleFilter = RuleFilter.fromRuleTypeEntires(
List<Map<String, dynamic>> simpleRuleList = _danmakuBlockController _danmakuBlockController.ruleTypes.entries);
.ruleTypes.values plPlayerController.filters = ruleFilter;
.expand((element) => element)
.map<Map<String, dynamic>>((e) {
//当正则表达式前后都有"/"时去掉避免RegExp解析错误
if (e.type == 1) {
String? filter = regExp.firstMatch(e.filter)?.group(1);
if (filter != null) {
e.filter = filter;
}
}
return e.toMap();
}).toList();
// debugPrint("simpleRuleList:$simpleRuleList");
plPlayerController.filterCount = simpleRuleList.length;
for (var item in simpleRuleList) {
switch (item['type']) {
case 0:
plPlayerController.dmFilterString.add(item['filter']);
break;
case 1:
plPlayerController.dmRegExp
.add(RegExp(item['filter'], caseSensitive: false));
break;
case 2:
plPlayerController.dmUid.add(item['filter']);
break;
}
}
scrollController.dispose(); scrollController.dispose();
GStorage.localCache.put(LocalCacheKey.danmakuFilterRule, simpleRuleList); GStorage.localCache.put(LocalCacheKey.danmakuFilterRules, ruleFilter);
super.dispose(); super.dispose();
} }
@@ -102,19 +76,16 @@ class _DanmakuBlockPageState extends State<DanmakuBlockPage> {
]), ]),
actions: <Widget>[ actions: <Widget>[
TextButton( TextButton(
onPressed: Navigator.of(context).pop,
child: const Text('取消'), child: const Text('取消'),
onPressed: () {
Navigator.of(context).pop();
},
), ),
TextButton( TextButton(
child: const Text('添加'), child: const Text('添加'),
onPressed: () async { onPressed: () {
String filter = textController.text; String filter = textController.text;
if (filter.isNotEmpty) { if (filter.isNotEmpty) {
await _danmakuBlockController.danmakuFilterAdd( _danmakuBlockController.danmakuFilterAdd(
filter: filter, type: type); filter: filter, type: type);
if (!context.mounted) return;
Navigator.of(context).pop(); Navigator.of(context).pop();
} else { } else {
SmartDialog.showToast('输入内容不能为空'); SmartDialog.showToast('输入内容不能为空');
@@ -143,7 +114,8 @@ class _DanmakuBlockPageState extends State<DanmakuBlockPage> {
controller: _danmakuBlockController.tabController, controller: _danmakuBlockController.tabController,
children: [ children: [
for (var i = 0; i < ruleLabels.length; i++) for (var i = 0; i < ruleLabels.length; i++)
Obx(() => tabViewBuilder(i, _danmakuBlockController.ruleTypes[i]!)), Obx(() => tabViewBuilder(
i, _danmakuBlockController.ruleTypes[i]!.entries.toList())),
], ],
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
@@ -155,7 +127,7 @@ class _DanmakuBlockPageState extends State<DanmakuBlockPage> {
); );
} }
Widget tabViewBuilder(int tabIndex, List<SimpleRule> list) { Widget tabViewBuilder(int tabIndex, List<MapEntry<int, String>> list) {
return ListView.builder( return ListView.builder(
controller: scrollController, controller: scrollController,
itemCount: list.length, itemCount: list.length,
@@ -163,16 +135,13 @@ class _DanmakuBlockPageState extends State<DanmakuBlockPage> {
itemBuilder: (BuildContext context, int listIndex) { itemBuilder: (BuildContext context, int listIndex) {
return ListTile( return ListTile(
title: Text( title: Text(
list[listIndex].filter, list[listIndex].value,
style: Theme.of(context).textTheme.bodyMedium, style: Theme.of(context).textTheme.bodyMedium,
), ),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.delete), icon: const Icon(Icons.delete),
onPressed: () async { onPressed: () => _danmakuBlockController.danmakuFilterDel(
await _danmakuBlockController.danmakuFilterDel( tabIndex, list[listIndex].key)));
tabIndex, list[listIndex].id);
}),
);
}, },
); );
} }
@@ -180,12 +149,7 @@ class _DanmakuBlockPageState extends State<DanmakuBlockPage> {
class DanmakuBlockController extends GetxController class DanmakuBlockController extends GetxController
with GetTickerProviderStateMixin { with GetTickerProviderStateMixin {
RxList<Rule> danmakuRules = <Rule>[].obs; final ruleTypes = RxMap<int, Map<int, String>>({0: {}, 1: {}, 2: {}});
RxMap<int, List<SimpleRule>> ruleTypes = {
0: <SimpleRule>[],
1: <SimpleRule>[],
2: <SimpleRule>[],
}.obs;
late TabController tabController; late TabController tabController;
@override @override
@@ -200,50 +164,44 @@ class DanmakuBlockController extends GetxController
super.onClose(); super.onClose();
} }
Future queryDanmakuFilter() async { Future<void> queryDanmakuFilter() async {
SmartDialog.showLoading(msg: '正在同步弹幕屏蔽规则……'); SmartDialog.showLoading(msg: '正在同步弹幕屏蔽规则……');
var result = await DanmakuFilterHttp.danmakuFilter(); var result = await DanmakuFilterHttp.danmakuFilter();
SmartDialog.dismiss(); SmartDialog.dismiss();
if (result['status']) { if (result['status']) {
if (result['data']?.rule != null) { if (result['data']?.rule != null) {
danmakuRules.value = result['data']?.rule; final List<SimpleRule> filter = result['data']?.rule;
danmakuRules.map((e) { for (var rule in filter) {
SimpleRule simpleRule = SimpleRule(e.id!, e.type!, e.filter!); ruleTypes[rule.type]![rule.id] = rule.filter;
ruleTypes[e.type!]!.add(simpleRule); }
}).toList();
ruleTypes.refresh(); ruleTypes.refresh();
} }
SmartDialog.showToast(result['data'].toast); SmartDialog.showToast(result['data'].toast);
} else { } else {
SmartDialog.showToast(result['msg']); SmartDialog.showToast(result['msg']);
} }
return result;
} }
Future danmakuFilterDel(int type, int id) async { Future<void> danmakuFilterDel(int type, 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']) {
danmakuRules.removeWhere((e) => e.id == id); ruleTypes[type]!.remove(id);
ruleTypes[type]!.removeWhere((e) => e.id == id);
ruleTypes.refresh(); ruleTypes.refresh();
SmartDialog.showToast(result['msg']);
} else {
SmartDialog.showToast(result['msg']);
} }
SmartDialog.showToast(result['msg']);
} }
Future danmakuFilterAdd({required String filter, required int type}) async { Future<void> danmakuFilterAdd(
{required String filter, required int type}) async {
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']) {
Rule data = result['data']; SimpleRule rule = result['data'];
danmakuRules.add(data); ruleTypes[type]![rule.id] = rule.filter;
SimpleRule simpleRule = SimpleRule(data.id!, data.type!, data.filter!);
ruleTypes[type]!.add(simpleRule);
ruleTypes.refresh(); ruleTypes.refresh();
SmartDialog.showToast('添加成功'); SmartDialog.showToast('添加成功');
} else { } else {

View File

@@ -1523,8 +1523,8 @@ class _HeaderControlState extends State<HeaderControl> {
Get.toNamed('/danmakuBlock', Get.toNamed('/danmakuBlock',
arguments: widget.controller) arguments: widget.controller)
}, },
child: child: Text(
Text("屏蔽管理(${widget.controller.filterCount})")), "屏蔽管理(${plPlayerController.filters.count})")),
], ],
), ),
Padding( Padding(

View File

@@ -6,6 +6,7 @@ import 'package:PiliPlus/common/constants.dart';
import 'package:PiliPlus/common/widgets/segment_progress_bar.dart'; import 'package:PiliPlus/common/widgets/segment_progress_bar.dart';
import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/init.dart';
import 'package:PiliPlus/models/common/audio_normalization.dart'; import 'package:PiliPlus/models/common/audio_normalization.dart';
import 'package:PiliPlus/models/user/danmaku_rule.dart';
import 'package:PiliPlus/utils/extension.dart'; import 'package:PiliPlus/utils/extension.dart';
import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/utils.dart';
import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart';
@@ -261,10 +262,7 @@ class PlPlayerController {
/// 弹幕权重 /// 弹幕权重
int danmakuWeight = 0; int danmakuWeight = 0;
int filterCount = 0; late RuleFilter filters;
List dmFilterString = [];
List<RegExp> dmRegExp = [];
Set dmUid = {};
// 关联弹幕控制器 // 关联弹幕控制器
DanmakuController? danmakuController; DanmakuController? danmakuController;
bool showDanmaku = true; bool showDanmaku = true;
@@ -406,22 +404,7 @@ class PlPlayerController {
isOpenDanmu.value = isOpenDanmu.value =
setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: true); setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: true);
danmakuWeight = setting.get(SettingBoxKey.danmakuWeight, defaultValue: 0); danmakuWeight = setting.get(SettingBoxKey.danmakuWeight, defaultValue: 0);
List rules = GStorage.localCache filters = GStorage.danmakuFilterRule;
.get(LocalCacheKey.danmakuFilterRule, defaultValue: []);
filterCount = rules.length;
for (var item in rules) {
switch (item['type']) {
case 0:
dmFilterString.add(item['filter']);
break;
case 1:
dmRegExp.add(RegExp(item['filter'], caseSensitive: false));
break;
case 2:
dmUid.add(item['filter']);
break;
}
}
blockTypes = setting.get(SettingBoxKey.danmakuBlockType, defaultValue: []); blockTypes = setting.get(SettingBoxKey.danmakuBlockType, defaultValue: []);
showArea = setting.get(SettingBoxKey.danmakuShowArea, defaultValue: 0.5); showArea = setting.get(SettingBoxKey.danmakuShowArea, defaultValue: 0.5);
// 不透明度 // 不透明度
@@ -1636,7 +1619,7 @@ class PlPlayerController {
_isQueryingVideoShot = true; _isQueryingVideoShot = true;
try { try {
dynamic res = await Request().get( dynamic res = await Request().get(
'https://api.bilibili.com/x/player/videoshot', '/x/player/videoshot',
queryParameters: { queryParameters: {
// 'aid': IdUtils.bv2av(_bvid), // 'aid': IdUtils.bv2av(_bvid),
'bvid': _bvid, 'bvid': _bvid,

View File

@@ -12,6 +12,8 @@ import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart';
import 'package:PiliPlus/models/common/tab_type.dart'; import 'package:PiliPlus/models/common/tab_type.dart';
import 'package:PiliPlus/models/common/theme_type.dart'; import 'package:PiliPlus/models/common/theme_type.dart';
import 'package:PiliPlus/models/common/up_panel_position.dart'; import 'package:PiliPlus/models/common/up_panel_position.dart';
import 'package:PiliPlus/models/user/danmaku_rule.dart';
import 'package:PiliPlus/models/user/danmaku_rule_adapter.dart';
import 'package:PiliPlus/models/video/play/CDN.dart'; import 'package:PiliPlus/models/video/play/CDN.dart';
import 'package:PiliPlus/models/video/play/quality.dart'; import 'package:PiliPlus/models/video/play/quality.dart';
import 'package:PiliPlus/models/video/play/subtitle.dart'; import 'package:PiliPlus/models/video/play/subtitle.dart';
@@ -426,8 +428,8 @@ class GStorage {
static List<int> get blackMidsList => List<int>.from(GStorage.localCache static List<int> get blackMidsList => List<int>.from(GStorage.localCache
.get(LocalCacheKey.blackMidsList, defaultValue: <int>[])); .get(LocalCacheKey.blackMidsList, defaultValue: <int>[]));
static List get danmakuFilterRule => GStorage.localCache static RuleFilter get danmakuFilterRule => GStorage.localCache
.get(LocalCacheKey.danmakuFilterRule, defaultValue: []); .get(LocalCacheKey.danmakuFilterRules, defaultValue: RuleFilter.empty());
static void setBlackMidsList(blackMidsList) { static void setBlackMidsList(blackMidsList) {
if (blackMidsList is! List<int>) return; if (blackMidsList is! List<int>) return;
@@ -535,6 +537,7 @@ class GStorage {
Hive.registerAdapter(BiliCookieJarAdapter()); Hive.registerAdapter(BiliCookieJarAdapter());
Hive.registerAdapter(LoginAccountAdapter()); Hive.registerAdapter(LoginAccountAdapter());
Hive.registerAdapter(AccountTypeAdapter()); Hive.registerAdapter(AccountTypeAdapter());
Hive.registerAdapter(RuleFilterAdapter());
} }
static Future<void> close() async { static Future<void> close() async {
@@ -756,7 +759,7 @@ class LocalCacheKey {
// 隐私设置-黑名单管理 // 隐私设置-黑名单管理
blackMidsList = 'blackMidsList', blackMidsList = 'blackMidsList',
// 弹幕屏蔽规则 // 弹幕屏蔽规则
danmakuFilterRule = 'danmakuFilterRule', danmakuFilterRules = 'danmakuFilterRules',
// // access_key // // access_key
// accessKey = 'accessKey', // accessKey = 'accessKey',
@@ -817,6 +820,7 @@ class Accounts {
await Future.wait([ await Future.wait([
GStorage.localCache.delete('accessKey'), GStorage.localCache.delete('accessKey'),
GStorage.localCache.delete('danmakuFilterRule'),
dir.delete(recursive: true), dir.delete(recursive: true),
if (isLogin) if (isLogin)
LoginAccount(cookies, localAccessKey['value'], LoginAccount(cookies, localAccessKey['value'],