From 99b14d0f0e5a258d5c0f10e380aa3b3d8c08b874 Mon Sep 17 00:00:00 2001 From: My-Responsitories <107370289+My-Responsitories@users.noreply.github.com> Date: Sun, 23 Mar 2025 12:07:57 +0800 Subject: [PATCH] opt: danmaku filter (#486) --- lib/http/danmaku_block.dart | 5 +- lib/models/user/danmaku_block.dart | 84 ++------------ lib/models/user/danmaku_rule.dart | 44 ++++++++ lib/models/user/danmaku_rule_adapter.dart | 36 ++++++ lib/pages/danmaku/controller.dart | 16 +-- lib/pages/danmaku_block/index.dart | 106 ++++++------------ .../video/detail/widgets/header_control.dart | 4 +- lib/plugin/pl_player/controller.dart | 25 +---- lib/utils/storage.dart | 10 +- 9 files changed, 141 insertions(+), 189 deletions(-) create mode 100644 lib/models/user/danmaku_rule.dart create mode 100644 lib/models/user/danmaku_rule_adapter.dart diff --git a/lib/http/danmaku_block.dart b/lib/http/danmaku_block.dart index 6ef1e493..f7f36e0b 100644 --- a/lib/http/danmaku_block.dart +++ b/lib/http/danmaku_block.dart @@ -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( Api.danmakuFilterAdd, queryParameters: { @@ -51,7 +52,7 @@ class DanmakuFilterHttp { if (res.data['code'] == 0) { return { 'status': true, - 'data': Rule.fromJson(res.data['data']), + 'data': SimpleRule.fromJson(res.data['data']), }; } else { return { diff --git a/lib/models/user/danmaku_block.dart b/lib/models/user/danmaku_block.dart index 489397de..ca99026b 100644 --- a/lib/models/user/danmaku_block.dart +++ b/lib/models/user/danmaku_block.dart @@ -1,5 +1,5 @@ class DanmakuBlockDataModel { - List? rule; + List? rule; String? toast; int? valid; int? ver; @@ -7,88 +7,22 @@ class DanmakuBlockDataModel { DanmakuBlockDataModel({this.rule, this.toast, this.valid, this.ver}); DanmakuBlockDataModel.fromJson(Map json) { - if (json['rule'] != null) { - rule = []; - json['rule'].forEach((v) { - rule!.add(Rule.fromJson(v)); - }); - } + rule = (json['rule'] as List?)?.map((v) => SimpleRule.fromJson(v)).toList(); toast = json['toast']; valid = json['valid']; ver = json['ver']; } - - Map toJson() { - final Map data = {}; - 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 json) { - id = json['id']; - mid = json['mid']; - type = json['type']; - filter = json['filter']; - comment = json['comment']; - ctime = json['ctime']; - mtime = json['mtime']; - } - - Map toJson() { - final Map data = {}; - 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 { - final int id; - final int type; - String filter; + late final int id; + late final int type; + late String filter; SimpleRule(this.id, this.type, this.filter); - Map toMap() { - return { - 'id': id, - 'type': type, - 'filter': filter, - }; - } - factory SimpleRule.fromMap(Map map) { - return SimpleRule( - map['id'], - map['type'], - map['filter'], - ); + SimpleRule.fromJson(Map json) { + id = json['id']; + type = json['type']; + filter = json['filter']; } } diff --git a/lib/models/user/danmaku_rule.dart b/lib/models/user/danmaku_rule.dart new file mode 100644 index 00000000..b378acf1 --- /dev/null +++ b/lib/models/user/danmaku_rule.dart @@ -0,0 +1,44 @@ +import 'package:PiliPlus/grpc/dm/v1/dm.pb.dart'; + +class RuleFilter { + static final _regExp = RegExp(r'^/(.*)/$'); + + List dmFilterString = []; + List dmRegExp = []; + Set 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>> 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))); + } +} diff --git a/lib/models/user/danmaku_rule_adapter.dart b/lib/models/user/danmaku_rule_adapter.dart new file mode 100644 index 00000000..980106c5 --- /dev/null +++ b/lib/models/user/danmaku_rule_adapter.dart @@ -0,0 +1,36 @@ +import 'package:PiliPlus/models/user/danmaku_rule.dart'; +import 'package:hive/hive.dart'; + +class RuleFilterAdapter extends TypeAdapter { + @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; +} diff --git a/lib/pages/danmaku/controller.dart b/lib/pages/danmaku/controller.dart index e4a6c9da..6ee53fda 100644 --- a/lib/pages/danmaku/controller.dart +++ b/lib/pages/danmaku/controller.dart @@ -69,21 +69,13 @@ class PlDanmakuController { queryDanmaku(segmentIndex); } if (plPlayerController.danmakuWeight == 0 && - plPlayerController.filterCount == 0) { + plPlayerController.filters.count == 0) { return dmSegMap[progress ~/ 100]; } else { return dmSegMap[progress ~/ 100] - ?.where( - (element) => element.weight >= plPlayerController.danmakuWeight) - .where(filterDanmaku) - .toList(); + ?..retainWhere((element) => + element.weight >= plPlayerController.danmakuWeight && + plPlayerController.filters.retain(element)); } } - - 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))); - } } diff --git a/lib/pages/danmaku_block/index.dart b/lib/pages/danmaku_block/index.dart index f09fc65d..eead3b87 100644 --- a/lib/pages/danmaku_block/index.dart +++ b/lib/pages/danmaku_block/index.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/models/user/danmaku_rule.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -38,38 +39,11 @@ class _DanmakuBlockPageState extends State { @override void dispose() { - final regExp = RegExp(r'^/(.*)/$'); - List> simpleRuleList = _danmakuBlockController - .ruleTypes.values - .expand((element) => element) - .map>((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; - } - } + final ruleFilter = RuleFilter.fromRuleTypeEntires( + _danmakuBlockController.ruleTypes.entries); + plPlayerController.filters = ruleFilter; scrollController.dispose(); - GStorage.localCache.put(LocalCacheKey.danmakuFilterRule, simpleRuleList); + GStorage.localCache.put(LocalCacheKey.danmakuFilterRules, ruleFilter); super.dispose(); } @@ -102,19 +76,16 @@ class _DanmakuBlockPageState extends State { ]), actions: [ TextButton( + onPressed: Navigator.of(context).pop, child: const Text('取消'), - onPressed: () { - Navigator.of(context).pop(); - }, ), TextButton( child: const Text('添加'), - onPressed: () async { + onPressed: () { String filter = textController.text; if (filter.isNotEmpty) { - await _danmakuBlockController.danmakuFilterAdd( + _danmakuBlockController.danmakuFilterAdd( filter: filter, type: type); - if (!context.mounted) return; Navigator.of(context).pop(); } else { SmartDialog.showToast('输入内容不能为空'); @@ -143,7 +114,8 @@ class _DanmakuBlockPageState extends State { controller: _danmakuBlockController.tabController, children: [ for (var i = 0; i < ruleLabels.length; i++) - Obx(() => tabViewBuilder(i, _danmakuBlockController.ruleTypes[i]!)), + Obx(() => tabViewBuilder( + i, _danmakuBlockController.ruleTypes[i]!.entries.toList())), ], ), floatingActionButton: FloatingActionButton( @@ -155,24 +127,21 @@ class _DanmakuBlockPageState extends State { ); } - Widget tabViewBuilder(int tabIndex, List list) { + Widget tabViewBuilder(int tabIndex, List> 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].filter, - style: Theme.of(context).textTheme.bodyMedium, - ), - trailing: IconButton( - icon: const Icon(Icons.delete), - onPressed: () async { - await _danmakuBlockController.danmakuFilterDel( - tabIndex, list[listIndex].id); - }), - ); + 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))); }, ); } @@ -180,12 +149,7 @@ class _DanmakuBlockPageState extends State { class DanmakuBlockController extends GetxController with GetTickerProviderStateMixin { - RxList danmakuRules = [].obs; - RxMap> ruleTypes = { - 0: [], - 1: [], - 2: [], - }.obs; + final ruleTypes = RxMap>({0: {}, 1: {}, 2: {}}); late TabController tabController; @override @@ -200,50 +164,44 @@ class DanmakuBlockController extends GetxController super.onClose(); } - Future queryDanmakuFilter() async { + Future queryDanmakuFilter() async { SmartDialog.showLoading(msg: '正在同步弹幕屏蔽规则……'); var result = await DanmakuFilterHttp.danmakuFilter(); SmartDialog.dismiss(); if (result['status']) { if (result['data']?.rule != null) { - danmakuRules.value = result['data']?.rule; - danmakuRules.map((e) { - SimpleRule simpleRule = SimpleRule(e.id!, e.type!, e.filter!); - ruleTypes[e.type!]!.add(simpleRule); - }).toList(); + final List filter = result['data']?.rule; + for (var rule in filter) { + ruleTypes[rule.type]![rule.id] = rule.filter; + } ruleTypes.refresh(); } SmartDialog.showToast(result['data'].toast); } else { SmartDialog.showToast(result['msg']); } - return result; } - Future danmakuFilterDel(int type, int id) async { + Future danmakuFilterDel(int type, int id) async { SmartDialog.showLoading(msg: '正在删除弹幕屏蔽规则……'); var result = await DanmakuFilterHttp.danmakuFilterDel(ids: id); SmartDialog.dismiss(); if (result['status']) { - danmakuRules.removeWhere((e) => e.id == id); - ruleTypes[type]!.removeWhere((e) => e.id == id); + ruleTypes[type]!.remove(id); 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 danmakuFilterAdd( + {required String filter, required int type}) async { SmartDialog.showLoading(msg: '正在添加弹幕屏蔽规则……'); var result = await DanmakuFilterHttp.danmakuFilterAdd(filter: filter, type: type); SmartDialog.dismiss(); if (result['status']) { - Rule data = result['data']; - danmakuRules.add(data); - SimpleRule simpleRule = SimpleRule(data.id!, data.type!, data.filter!); - ruleTypes[type]!.add(simpleRule); + SimpleRule rule = result['data']; + ruleTypes[type]![rule.id] = rule.filter; ruleTypes.refresh(); SmartDialog.showToast('添加成功'); } else { diff --git a/lib/pages/video/detail/widgets/header_control.dart b/lib/pages/video/detail/widgets/header_control.dart index f1ffea3f..60315ba5 100644 --- a/lib/pages/video/detail/widgets/header_control.dart +++ b/lib/pages/video/detail/widgets/header_control.dart @@ -1523,8 +1523,8 @@ class _HeaderControlState extends State { Get.toNamed('/danmakuBlock', arguments: widget.controller) }, - child: - Text("屏蔽管理(${widget.controller.filterCount})")), + child: Text( + "屏蔽管理(${plPlayerController.filters.count})")), ], ), Padding( diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 487ac7e6..2019e01a 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -6,6 +6,7 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/segment_progress_bar.dart'; import 'package:PiliPlus/http/init.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/utils.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart'; @@ -261,10 +262,7 @@ class PlPlayerController { /// 弹幕权重 int danmakuWeight = 0; - int filterCount = 0; - List dmFilterString = []; - List dmRegExp = []; - Set dmUid = {}; + late RuleFilter filters; // 关联弹幕控制器 DanmakuController? danmakuController; bool showDanmaku = true; @@ -406,22 +404,7 @@ class PlPlayerController { isOpenDanmu.value = setting.get(SettingBoxKey.enableShowDanmaku, defaultValue: true); danmakuWeight = setting.get(SettingBoxKey.danmakuWeight, defaultValue: 0); - List rules = GStorage.localCache - .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; - } - } + filters = GStorage.danmakuFilterRule; blockTypes = setting.get(SettingBoxKey.danmakuBlockType, defaultValue: []); showArea = setting.get(SettingBoxKey.danmakuShowArea, defaultValue: 0.5); // 不透明度 @@ -1636,7 +1619,7 @@ class PlPlayerController { _isQueryingVideoShot = true; try { dynamic res = await Request().get( - 'https://api.bilibili.com/x/player/videoshot', + '/x/player/videoshot', queryParameters: { // 'aid': IdUtils.bv2av(_bvid), 'bvid': _bvid, diff --git a/lib/utils/storage.dart b/lib/utils/storage.dart index ae925a79..9b2f067c 100644 --- a/lib/utils/storage.dart +++ b/lib/utils/storage.dart @@ -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/theme_type.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/quality.dart'; import 'package:PiliPlus/models/video/play/subtitle.dart'; @@ -426,8 +428,8 @@ class GStorage { static List get blackMidsList => List.from(GStorage.localCache .get(LocalCacheKey.blackMidsList, defaultValue: [])); - static List get danmakuFilterRule => GStorage.localCache - .get(LocalCacheKey.danmakuFilterRule, defaultValue: []); + static RuleFilter get danmakuFilterRule => GStorage.localCache + .get(LocalCacheKey.danmakuFilterRules, defaultValue: RuleFilter.empty()); static void setBlackMidsList(blackMidsList) { if (blackMidsList is! List) return; @@ -535,6 +537,7 @@ class GStorage { Hive.registerAdapter(BiliCookieJarAdapter()); Hive.registerAdapter(LoginAccountAdapter()); Hive.registerAdapter(AccountTypeAdapter()); + Hive.registerAdapter(RuleFilterAdapter()); } static Future close() async { @@ -756,7 +759,7 @@ class LocalCacheKey { // 隐私设置-黑名单管理 blackMidsList = 'blackMidsList', // 弹幕屏蔽规则 - danmakuFilterRule = 'danmakuFilterRule', + danmakuFilterRules = 'danmakuFilterRules', // // access_key // accessKey = 'accessKey', @@ -817,6 +820,7 @@ class Accounts { await Future.wait([ GStorage.localCache.delete('accessKey'), + GStorage.localCache.delete('danmakuFilterRule'), dir.delete(recursive: true), if (isLogin) LoginAccount(cookies, localAccessKey['value'],