标注成分&自动屏蔽
// ==UserScript== // @name Bilibili评论成分识别&屏蔽 // @namespace analyzer.bilibili // @version 2.15 // @description 标注成分&自动屏蔽 // @author 星之所向i // @match *://www.bilibili.com/video/* // @match *://www.bilibili.com/festival/* // @match *://www.bilibili.com/read/* // @match *://www.bilibili.com/blackboard/* // @match *://www.bilibili.com/list/watchlater* // @match *://www.bilibili.com/list/ml* // @match *://www.bilibili.com/v/topic/* // @match *://www.bilibili.com/bangumi/* // @match *://www.bilibili.com/opus/* // @match *://t.bilibili.com/* // @match *://space.bilibili.com/*/dynamic* // @match *://live.bilibili.com/* // @match *//live.bilibili.com/blanc/*?liteVersion=* // @exclude *://www.bilibili.com/video/online.html* // @exclude *://www.bilibili.com/ // @exclude *://live.bilibili.com/p/* // @exclude *//t.bilibili.com/h5/dynamic/* // @icon https://static.hdslb.com/images/favicon.ico // @connect bilibili.com // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_notification // @noframes // @run-at document-end // @license MIT // ==/UserScript== const script = async () => { 'use strict'; let /** MODE 模式 * @type { '自动' | '静默' } */ MODE = '自动', /** 检测功能启用状态 * @type { boolean } */ DET = true, /** 屏蔽功能启用状态 * @type { boolean } */ DEL = false, /** 屏蔽样式 * @type { '完全删除' | '消息屏蔽' } */ DEL_STYLE = '消息屏蔽', /** 自动模式下每秒最大检测次数, 不含已检测过成分的用户 * @type { number } */ LIMIT_EVENT = 10, /** 标签收#阈值(字节), 超过自动收# * @type { number } */ TAG_MAX_LENGTH = 20, /** 渲染最短间隔时间(毫秒) * @type { number } */ INTERVAL = 0, /** 检测依据 粉丝牌/关注列表/投稿列表/动态空间/评论文本 * @type { { [ key: string ]: boolean} } */ DET_BASE = { DET_BASE_MedalWall: true, DET_BASE_FOLLOWINGS: true, DET_BASE_VIDEOS: true, DET_BASE_DYNAMIC: true, DET_BASE_COMMENT: true, }, /** 粉丝牌/关注列表/投稿列表/动态空间API 过载状态 * @type { { [ key: string ]: { Timer: number | null, state: boolean, timestamp: number } } } */ API_OVERLOAD = { DET_BASE_MedalWall: { Timer: null, state: false, timestamp: 0 }, DET_BASE_FOLLOWINGS: { Timer: null, state: false, timestamp: 0 }, DET_BASE_VIDEOS: { Timer: null, state: false, timestamp: 0 }, DET_BASE_DYNAMIC: { Timer: null, state: false, timestamp: 0 }, } /** <===================================================================================设置面板==========================================================================================> */ let navPanelVis = false, ctab = true, ckey = undefined, ciframe = undefined, animationState = false, init = false, newRuleKeysSet = new Set(), deleteRuleKeysSet = new Set(), newAntiRuleSets = new Set(), deleteAntiRuleSets = new Set(), settingTemp = {}, timerTab1 = null, timerTab2 = null, timerFoot = null, timerFoot2 = null, timerAnimaltion = null /** 重新封装history行为 * @param { string } type histroy行为类型 * @returns { (...args: any[]) => any } */ const bindHistoryEvent = (type) => { const historyEvent = history[type] return function () { const newEvent = historyEvent.apply(this, arguments) const e = new Event(type) e.arguments = arguments window.dispatchEvent(e) return newEvent } } /** 等待 * @param { number } wait 等待时长 * @returns { Promise<void> } */ const delay = (wait) => new Promise((r) => setTimeout(r, wait)) /** 基于消息队列的事件节流器 */ class Throttler { /** 构造器参数 * @param { number } limit 单位时间内事件执行的最大次数 * @param { number } interval 单位时间长度 * @param { ( object ) => boolean } callback 回调函数 */ constructor(limit, interval, callback) { this._limit = limit this._count = 0 this._interval = interval this._ispending = true this._ftimestamp = Date.now() + interval this._timer = null this._callback = callback this._queue = [] this._queue.__proto__ = Object.create(Array.prototype) this._queue.__proto__.push = (...args) => { Array.prototype.push.call(this._queue, ...args); if (this._ispending && this._count < this._limit) this.deal() } this._queue.__proto__.unshift = (...args) => { Array.prototype.unshift.call(this._queue, ...args); if (this._ispending && this._count < this._limit) this.deal() } } /** 加入队列 * @param { object } data 处理回调事件所需的数据对象 * @param { boolean } isJump 插队到消息队列最前端 * @returns { void } */ enqueue = (data, isJump) => isJump ? this._queue.unshift(data) : this._queue.push(data) /** 从队列最前面移除并返回元素 * @returns { object } */ dequeue = () => this._queue.shift() /** 重置计数与时间戳 * @returns { void } */ reset = () => { this._count = 0 this._ftimestamp = Date.now() + this._interval } /** 重置限制次数 * @returns { void } */ resetlimit = (newLimit) => { this._limit = newLimit this.reset() } /** 重置队列 * @returns { void } */ resetqueue = () => this._queue.length = 0 /** 处理事件 * @returns { boolean } */ deal = () => { if (Date.now() >= this._ftimestamp) this.reset() if (this._queue.length > 0 && this._count < this._limit) { let item = this.dequeue() if (item) { if (this._callback(item)) this._count++ return true } else { return false } } else { return false } } /** 定时处理消息队列中额定的事件 * @returns { void } */ start = () => { setTimeout(async () => { while (true) { this._ispending = false while (this.deal()) { } this._ispending = true await delay(this._ftimestamp - Date.now()) } }, 0) } } /** 节流 * @param { function } func 函数体 * @param { number } wait 时间间隔 * @param { { leading?: boolean, trailing?: boolean } } options 参数配置 * @returns { [ (...args: any[]) => void, () => void ] } */ const _throttle = (func, wait, options = {}) => { if (typeof (func) !== 'function' || typeof (wait) !== 'number' || typeof (options) !== 'object') return () => { } let context, args, timer, option = { leading: false, trailing: true, ...options } return [function () { context = this args = arguments if (option.leading && !timer) { func.apply(context, args) if (!option.trailing) { timer = setTimeout(() => timer = null, wait) } } if (!timer && option.trailing) { timer = setTimeout(() => { func.apply(context, args) timer = null }, wait) } }, () => { clearTimeout(timer); timer = null }] } /** 防抖 * @param { function } func 函数体 * @param { number } wait 时间间隔 * @param { { leading?: boolean, maxWait?: number, trailing?: boolean } } options 参数配置 * @returns { [ (...args: any[]) => void, () => void ] } */ const _debounce = (func, wait, options = {}) => { if (typeof (func) !== 'function' || typeof (wait) !== 'number' || typeof (options) !== 'object') return () => { } let context, args, timer, maxTimr, option = { leading: false, maxWait: undefined, trailing: true, ...options } return [function () { context = this args = arguments if (option.leading && !timer) { func.apply(context, args) } if (timer) { clearTimeout(timer) timer = null } if (option.maxWait !== undefined && !maxTimr) { maxTimr = setTimeout(() => { func.apply(context, args) clearTimeout(timer) timer = null maxTimr = null }, option.maxWait) } if (!timer && option.trailing) { timer = setTimeout(() => { func.apply(context, args) clearTimeout(maxTimr) timer = null }, wait) } }, () => { clearTimeout(timer); timer = null }] } /** 系统提示 * @param { string } text 提示文本 * @param { string } title 提示标题 * @param { () => void } callback 提示点击回调 * @returns { void } */ const showNotification = (text, title, timeout = 0, callback = () => { }) => { GM_notification({ text, title, timeout, onclick: () => { window.focus() callback() } }) } /** 监听对象并在被监听键值改变时执行回调函数 * @param { object } obj 监听对象 * @param { string } key 监听键名 * @param { ( newvalue: string | number ) => void } callback 监听键值变化时调用的回调函数 * @returns { void } */ const watch = (obj, key, callback) => { if (typeof (obj) !== 'object' || typeof (key) !== 'string' || typeof (callback) !== 'function') return let value = obj[key] Object.defineProperty(obj, key, { configurable: true, enumerable: true, get: () => value, set: (newValue) => { value = newValue callback(newValue) } }) } /** API过载后禁用15分钟 * @param { string } api 需要禁用的API名 * @param { Element | null } ele 需要修改的元素 * @param { number } time 禁用时间, 默认15分钟 * @param { number } timestamp 禁用截止时间戳 * @returns { void } */ const banAPI = (api, ele = null, time = 15 * 60 * 1000, timestamp = Date.now(), isAlert = true) => { if (isAlert && DET_BASE[api]) showNotification(`检测到API『${api}』频繁调用被B站暂时拉黑, 已自动禁用${(time / 60000).toFixed(2)}分钟`, 'API禁用提醒') API_OVERLOAD[api].state = true if (API_OVERLOAD[api].Timer === null) { API_OVERLOAD[api].timestamp = timestamp API_OVERLOAD[api].Timer = setTimeout(() => recoverAPI(api, ele, ele?.className), time) } if (DET_BASE[api]) { DET_BASE[api] = false if (ele) ele.className = 'statusBAN' } GM_setValue('xzsx_bilibili_detector_apioverload', JSON.stringify(API_OVERLOAD)) } /** 恢复API * @param { string } api 需要恢复的API名 * @param { Element | null } ele 需要修改的元素 * @param { string } preClass 需要修改的元素样式 * @returns { void } */ const recoverAPI = (api, ele = null, preClass = undefined) => { DET_BASE[api] = true API_OVERLOAD[api].state = false API_OVERLOAD[api].timestamp = Date.now() clearTimeout(API_OVERLOAD[api].Timer) API_OVERLOAD[api].Timer = null if (ele !== null && preClass !== undefined) ele.className = JSON.parse(JSON.stringify(preClass)) } /** 深拷贝配置参数 * @param { object } source 配置参数对象 * @returns { object } */ const deepCloneRules = (source) => { let clone = JSON.parse(JSON.stringify(source)) Object.keys(clone.detect).forEach((key) => { clone.detect[key]['keywords'] = clone.detect[key].hasOwnProperty('keywords') ? new Set([...source.detect[key].keywords]) : new Set() clone.detect[key]['antikeywords'] = clone.detect[key].hasOwnProperty('antikeywords') ? new Set([...source.detect[key].antikeywords]) : new Set() }) return clone } /** 从本地存储读取配置信息 * @returns { object } */ const initRules = () => { /** 初始配置 */ const initConf = { blackList: { '原神': '基于成分', '明日方舟': '基于成分', '王者荣耀': '基于成分', '用户昵称': '基于昵称', /** ... */ }, detect: { '原神': { 'color': '#000000', 'keywords': new Set(['原神', '莴苣某人', '你的影月月']), 'antikeywords': new Set(['原P', '原批']) }, /** 最好不要填写容易匹配英文单词的关键词 */ '明日方舟': { 'color': '#73ff00', 'keywords': new Set(['明日方舟', 'Wan顽子']), 'antikeywords': new Set(['粥÷', '粥畜']) }, '王者荣耀': { 'color': '#ff00f7', 'keywords': new Set(['王者荣耀', 'AG超玩会王者荣耀梦泪']), 'antikeywords': new Set(['农P', '农批']) }, '战双帕弥什': { 'color': '#f50000', 'keywords': new Set(['战双帕弥什']), 'antikeywords': new Set(['双畜']) }, '崩坏3': { 'color': '#8000ff', 'keywords': new Set(['崩坏3第一偶像#酱']), 'antikeywords': new Set(['幻官']) }, 'Asoul': { 'color': '#000000', 'keywords': new Set(['A-SOUL_Official', '嘉然今天吃什么', '向晚大魔王', '贝拉kira', '乃琳Queen', '顶碗人', '音乐珈', '奶淇淋', '贝极星']), 'antikeywords': new Set(['A畜']) }, '幻塔': { 'color': '#66dbd9', 'keywords': new Set(['幻塔']), 'antikeywords': new Set(['幻官']) }, '棺人痴': { 'color': '#fed06c', 'keywords': new Set(['東雪蓮Official', '東雪蓮', '东雪莲', '棺人痴']), 'antikeywords': new Set(['眠大佐']) }, /** ... */ } } try { let conf = GM_getValue('xzsx_bilibili_detector'), state = GM_getValue('xzsx_bilibili_detector_state'), overload = GM_getValue('xzsx_bilibili_detector_apioverload') /** 从本地存储恢复配置列表 */ if (typeof (conf) === 'string' && conf) { conf = JSON.parse(conf) if (Object.keys(conf).length > 0) { if (Array.isArray(conf.blackList)) { let obj = {} conf.blackList.forEach((key) => obj[key] = '基于昵称') conf.blackList = obj } Object.keys(conf?.detect).forEach((key) => { conf.detect[key]['keywords'] = conf.detect[key].hasOwnProperty('keywords') ? new Set([...conf.detect[key].keywords.length > 0 ? conf.detect[key].keywords : []]) : new Set() conf.detect[key]['antikeywords'] = conf.detect[key].hasOwnProperty('antikeywords') ? new Set([...conf.detect[key].antikeywords.length > 0 ? conf.detect[key].antikeywords : []]) : new Set() }) } } /** 初始配置 */ if (!conf || Object.keys(conf).length === 0) conf = initConf /** 从本地存储恢复状态 */ if (typeof (state) === 'string' && state) { state = JSON.parse(state) MODE = state?.MODE ?? (state?.MODE === '自动' || state?.MODE === '静默') ? state?.MODE : '自动' DET = state?.DET ?? (typeof (state?.DET) === "boolean" ? state?.DET : true) DEL = state?.DEL ?? (typeof (state?.DEL) === "boolean" ? state?.DEL : false) DEL_STYLE = state?.DEL_STYLE ?? (state?.DEL_STYLE === '消息屏蔽' || state?.DEL_STYLE === '完全删除') ? state?.DEL_STYLE : '消息屏蔽' TAG_MAX_LENGTH = state?.TAG_MAX_LENGTH ?? (typeof (state?.TAG_MAX_LENGTH) === "number" ? state?.TAG_MAX_LENGTH : 20) INTERVAL = state?.INTERVAL ?? (typeof (state?.INTERVAL) === "number" ? state?.INTERVAL : 0) LIMIT_EVENT = state?.LIMIT_EVENT ?? (typeof (state?.LIMIT_EVENT) === "number" ? state?.LIMIT_EVENT : 10) DET_BASE['DET_BASE_MedalWall'] = state?.DET_BASE_MedalWall ?? (typeof (state?.DET_BASE_MedalWall) === "boolean" ? state?.DET_BASE_MedalWall : true) DET_BASE['DET_BASE_FOLLOWINGS'] = state?.DET_BASE_FOLLOWINGS ?? (typeof (state?.DET_BASE_FOLLOWINGS) === "boolean" ? state?.DET_BASE_FOLLOWINGS : true) DET_BASE['DET_BASE_VIDEOS'] = state?.DET_BASE_VIDEOS ?? (typeof (state?.DET_BASE_VIDEOS) === "boolean" ? state?.DET_BASE_VIDEOS : true) DET_BASE['DET_BASE_DYNAMIC'] = state?.DET_BASE_DYNAMIC ?? (typeof (state?.DET_BASE_DYNAMIC) === "boolean" ? state?.DET_BASE_DYNAMIC : true) DET_BASE['DET_BASE_COMMENT'] = state?.DET_BASE_COMMENT ?? (typeof (state?.DET_BASE_COMMENT) === "boolean" ? state?.DET_BASE_COMMENT : true) } /** 从本地存储恢复API禁用状态 */ if (typeof (overload) === 'string' && overload) { overload = JSON.parse(overload) Object.keys(overload).forEach((api) => { if (overload[api]?.timestamp) { let passed = Date.now() - overload[api].timestamp if (overload[api]?.state && passed < 15 * 60 * 1000) banAPI(api, null, 15 * 60 * 1000 - passed, overload[api].timestamp, false) } }) } return [conf, deepCloneRules(conf)] } catch (error) { return [initConf, deepCloneRules(initConf)] } } /** 标记/屏蔽规则 * @type { { blackList: [ key: string ]: string, detect: { [ key: string ]: { color: string, keywords: Set<string>, antikeywords: Set<string> } } } } */ let [rulesApply, rules] = initRules() /** 构建事件节流器实例 */ const _T = new Throttler(LIMIT_EVENT, 1000, ({ uid, user, isdeep, forceSync }) => { // 查重 if (filte(uid, user)) return false // 异步处理 setTimeout(() => handleStr(uid, user, isdeep, forceSync), 0) return true }) /** 启动事件节流器 */ _T.start() /** 获取正确的iframe * 当用户元素存在于iframe内时需要调用 * @returns { HTMLIFrameElement | undefined } iframe存在则返回实例,反之则返回undefined */ const getCurIframe = () => { let matches = [] for (const iframe of document.getElementsByTagName('iframe')) { matches = iframe.attributes.src.value.match(/\/\/(.*?)\/([a-zA-Z|\d])+\/.*$/) if (matches !== null) { if (matches.length > 2 && matches[2] === 'p') continue if (matches.length > 1 && matches[1] === 'live.bilibili.com') return iframe } } return undefined } /** 添加css样式 * @type { HTMLStyleElement } */ const style = document.createElement('style') /** 面板相关css样式 * @type { string } */ const css_panel = ` .nav-show{display:block;outline:0;height:24px;width:40px;margin-bottom:12px;transition:all 0.3s;cursor:pointer;text-align:center;padding:0 4px;position:fixed;bottom:4px;right:6px;color:white;background-color:rgb(75,182,206);box-sizing:border-box;box-shadow:0px 0px 2px #525252;border:0px;user-select:none;-ms-user-select:none;-moz-user-select:none;-webkit-user-select:none;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:14px;transition:all 0.2s;-o-transition:all 0.2s;-moz-transition:all 0.2s;-webkit-transition:all 0.2s;transform:scale(1);-o-transform:scale(1);-ms-transform:scale(1);-moz-transform:scale(1);-webkit-transform:scale(1);appearance:button;-moz-appearance:button;-webkit-appearance:button;z-index:99999;cursor:pointer;opacity:1} .nav-show:hover{transform:scale(0.8);-o-transform:scale(0.8);-ms-transform:scale(0.8);-moz-transform:scale(0.8);-webkit-transform:scale(0.8);opacity:0.5} .nav-panel{width:250px;height:425px;display:none;align-items:center;flex-direction:column;justify-content:flex-start;position:fixed;bottom:12px;right:90px;background-color:white;box-shadow:0px 0px 4px #6a6767;user-select:none;-ms-user-select:none;-moz-user-select:none;-webkit-user-select:none;z-index:999999;overflow:hidden} .nav-tabs{display:flex;width:100%;justify-content:space-around;box-shadow:0px 2px 4px #ddd} .nav-tabs-btn1{flex-grow:1;height:36px;background-color:rgba(22,51,194,0.821);color:white;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:18px;border:0;cursor:pointer;filter:brightness(100%);-o-filter:brightness(100%);-moz-filter:brightness(100%);-webkit-filter:brightness(100%);z-index:1} .nav-tabs-btn1:hover{opacity:0.8} .nav-tabs-btn2{flex-grow:1;height:36px;background-color:rgba(49,182,203,0.821);color:white;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:18px;border:0;cursor:pointer;filter:brightness(50%);-o-filter:brightness(50%);-moz-filter:brightness(50%);-webkit-filter:brightness(50%);z-index:1} .nav-tabs-btn2:hover{opacity:0.8} .nav-list{display:inline-flex;flex-direction:column;height:calc(100% - 36px);width:100%;overflow:auto} .rule-item-btn{border:1px solid #d9d9d9;outline:0;height:24px;padding:0 4px;font-size:14px;border-radius:2px;color:#fff;background:#1890ff;border-color:#1890ff;text-shadow:0 -1px 0 rgba(0,0,0,.12);box-shadow:0 2px 0 rgba(0,0,0,.045);appearance:button;-moz-appearance:button;-webkit-appearance:button;cursor:pointer} .rule-item-btn:hover{opacity:0.8} .addrule_form{position:absolute;top:36px;width:100%;height:calc(100% - 72px);padding:0;display:none;flex-direction:column;justify-content:flex-start;background-color:rgb(255,255,255);user-select:none;-ms-user-select:none;-moz-user-select:none;-webkit-user-select:none;z-index:1;animation:showEditPanel 0.5s forwards;-o-animation:showEditPanel 0.5s forwards;-moz-animation:showEditPanel 0.5s forwards;-webkit-animation:showEditPanel 0.5s forwards;clip-path:polygon(0 0,0 0,-57% 100%,-57% 100%);-o-clip-path:polygon(0 0,0 0,-57% 100%,-57% 100%);-moz-clip-path:polygon(0 0,0 0,-57% 100%,-57% 100%);-webkit-clip-path:polygon(0 0,0 0,-57% 100%,-57% 100%)} @keyframes showEditPanel{from{clip-path:polygon(0 0,0 0,-57% 100%,-57% 100%)} to{clip-path:polygon(0 0,157% 0,100% 100%,-57% 100%)} }@-o-keyframes showEditPanel{from{-o-clip-path:polygon(0 0,0 0,-57% 100%,-57% 100%)} to{-o-clip-path:polygon(0 0,157% 0,100% 100%,-57% 100%)} }@-moz-keyframes showEditPanel{from{-moz-clip-path:polygon(0 0,0 0,-57% 100%,-57% 100%)} to{-moz-clip-path:polygon(0 0,157% 0,100% 100%,-57% 100%)} }@-webkit-keyframes showEditPanel{from{-webkit-clip-path:polygon(0 0,0 0,-57% 100%,-57% 100%)} to{-webkit-clip-path:polygon(0 0,157% 0,100% 100%,-57% 100%)} }.addrule_form-hide{position:absolute;top:36px;width:100%;height:calc(100% - 72px);padding:0;display:none;flex-direction:column;justify-content:flex-start;background-color:rgb(255,255,255);user-select:none;-ms-user-select:none;-moz-user-select:none;-webkit-user-select:none;z-index:1;animation:hideEditPanel 0.5s forwards;-o-animation:hideEditPanel 0.5s forwards;-moz-animation:hideEditPanel 0.5s forwards;-webkit-animation:hideEditPanel 0.5s forwards;clip-path:polygon(0 0,157% 0,100% 100%,-57% 100%);-o-clip-path:polygon(0 0,157% 0,100% 100%,-57% 100%);-moz-clip-path:polygon(0 0,157% 0,100% 100%,-57% 100%);-webkit-clip-path:polygon(0 0,157% 0,100% 100%,-57% 100%)} @keyframes hideEditPanel{from{clip-path:polygon(0 0,157% 0,100% 100%,-57% 100%)} to{clip-path:polygon(157% 0,157% 0,100% 100%,100% 100%)} }@-o-keyframes hideEditPanel{from{-o-clip-path:polygon(0 0,157% 0,100% 100%,-57% 100%)} to{-o-clip-path:polygon(157% 0,157% 0,100% 100%,100% 100%)} }@-moz-keyframes hideEditPanel{from{-moz-clip-path:polygon(0 0,157% 0,100% 100%,-57% 100%)} to{-moz-clip-path:polygon(157% 0,157% 0,100% 100%,100% 100%)} }@-webkit-keyframes hideEditPanel{from{-webkit-clip-path:polygon(0 0,157% 0,100% 100%,-57% 100%)} to{-webkit-clip-path:polygon(157% 0,157% 0,100% 100%,100% 100%)} }.addrule_item1{display:flex;justify-content:space-between;align-items:center;padding:8px;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:18px} .rule_keywords{height:50%;width:94%;flex-wrap:wrap;padding:8px;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:18px;overflow-x:hidden;overflow-y:auto} .tagname_input{border:0;color:black;outline-style:none;height:37px;width:195px;font-size:32px;transition:color 0.5s;-o-transition:color 0.5s;-moz-transition:color 0.5s;-webkit-transition:color 0.5s} .tagcolor_input{height:30px;width:30px;cursor:pointer} .xzsx_detect_tagsample_container{display:flex;justify-content:flex-start;align-items:center;height:36px;padding-left:8px;padding-right:8px;padding-top:4px;padding-bottom:2px;box-shadow:0px 0px 8px #ddd inset;overflow:hidden} .xzsx_detect_tagsample{display:inline-flex;height:14px;line-height:14px;position:relative;box-sizing:content-box !important;padding:1px 2px 1px 14px;margin:1px 4px 1px 0;color:black;white-space:nowrap;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑 !important;font-size:14px;font-weight:600 !important;border-radius:6px 4px 4px 6px;box-shadow:0 0 6px #ddd;text-size-adjust:100%;-ms-text-size-adjust:100%;-moz-text-size-adjust:100%;-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent;transition:all 1s;-o-transition:all 1s;-moz-transition:all 1s;-webkit-transition:all 1s;overflow-wrap:break-word;cursor:pointer} .xzsx_detect_tagsample:hover{opacity:0.8} .xzsx_detect_tagsample::before{background:#fff;border-radius:10px;box-shadow:0 1px rgba(0,0,0,0.25) inset;content:'';height:6px;left:4px;position:absolute;width:6px;top:calc(50% - 3px)} .add_items{display:inline-flex;justify-content:center;align-items:center} .tagkeyword_input{width:128px;border:0;border-bottom:0.5px solid;outline-style:none;font-size:24px} .add_icon{display:inline-flex;justify-content:center;align-items:center;height:10px;width:10px;color:rgb(0,0,0);transform:scale(2);-o-transform:scale(2);-ms-transform:scale(2);-moz-transform:scale(2);-webkit-transform:scale(2);transform-origin:25% 55%;-o-transform-origin:25% 55%;-ms-transform-origin:25% 55%;-moz-transform-origin:25% 55%;-webkit-transform-origin:25% 55%;cursor:pointer} .add_icon:hover{opacity:0.5} .keyword{display:inline-flex;font-size:24px;padding:4px;animation:showEditPanel 0.5s forwards;-o-animation:showEditPanel 0.5s forwards;-moz-animation:showEditPanel 0.5s forwards;-webkit-animation:showEditPanel 0.5s forwards;clip-path:polygon(0 0,0 0,-57% 100%,-57% 100%);-o-clip-path:polygon(0 0,0 0,-57% 100%,-57% 100%);-moz-clip-path:polygon(0 0,0 0,-57% 100%,-57% 100%);-webkit-clip-path:polygon(0 0,0 0,-57% 100%,-57% 100%)} .keyword:hover{background-color:#ddd} .keyword_delete{position:relative;top:-3px;right:0;width:20px;height:20px;border-radius:50%;text-align:center;transform:scale(0.6) translate(0%,0%);-o-transform:scale(0.6) translate(0%,0%);-ms-transform:scale(0.6) translate(0%,0%);-moz-transform:scale(0.6) translate(0%,0%);-webkit-transform:scale(0.6) translate(0%,0%);transform-origin:center;-o-transform-origin:center;-ms-transform-origin:center;-moz-transform-origin:center;-webkit-transform-origin:center;font-size:20px;color:#837171;line-height:18px;background-color:#b2bfc67a;cursor:pointer} .keyword_delete:hover{color:white;background-color:crimson} .keyword_delete:active{color:white;background-color:#f0f} .rule-item{display:flex;justify-content:space-between;align-items:center;padding:2px 8px 2px 8px;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:18px;transform:scaleX(1);-o-transform:scaleX(1);-ms-transform:scaleX(1);-moz-transform:scaleX(1);-webkit-transform:scaleX(1);transition:all 0.2s;-o-transition:all 0.2s;-moz-transition:all 0.2s;-webkit-transition:all 0.2s;clip-path:polygon(0 0,115% 0,100% 100%,-15% 100%);-o-clip-path:polygon(0 0,115% 0,100% 100%,-15% 100%);-moz-clip-path:polygon(0 0,115% 0,100% 100%,-15% 100%);-webkit-clip-path:polygon(0 0,115% 0,100% 100%,-15% 100%)} .rule-item:hover{background-color:#ddd;transform:scaleX(0.95);-o-transform:scaleX(0.95);-ms-transform:scaleX(0.95);-moz-transform:scaleX(0.95);-webkit-transform:scaleX(0.95)} .rule-item-detect-add{font-size:18px;text-align:center;padding:2px 8px 2px 8px;border-left:0;border-right:0;color:blue;cursor:pointer} .rule-item-detect-add:hover{background-color:#ddd} .rule-item-blacklist-add{display:none;text-align:center;padding:2px 8px 2px 8px;border-left:0;border-right:0} .rule-item-blacklist-add-input{height:32px;width:90px;border:0;border-bottom:0.5px solid;outline-style:none;font-size:24px} .rule-item-blacklist-select{height:32.8px;width:90px;border:0;border-bottom:0.5px solid;outline-style:none;font-size:24px} .rule-item-blacklist-addicon{display:inline-flex;justify-content:center;align-items:center;height:10px;width:10px;color:rgb(0,0,0);transform:scale(3);-o-transform:scale(3);-ms-transform:scale(3);-moz-transform:scale(3);-webkit-transform:scale(3);transform-origin:25% 5%;-o-transform-origin:25% 5%;-moz-transform-origin:25% 5%;-webkit-transform-origin:25% 5%;cursor:pointer} .rule-item-blacklist-addicon:hover{opacity:0.5} .rule-item-blacklist-base{display:flex;justify-content:center;align-items:center;padding:2px 8px 2px 8px;margin-right:4px;color:#299bda;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:18px;transform:scaleX(1);-o-transform:scaleX(1);-ms-transform:scaleX(1);-moz-transform:scaleX(1);-webkit-transform:scaleX(1);transition:all 0.2s;-o-transition:all 0.2s;-moz-transition:all 0.2s;-webkit-transition:all 0.2s;cursor:pointer} .rule-item-blacklist-base:hover{background-color:#ddd;transform:scaleX(0.95);-o-transform:scaleX(0.95);-moz-transform:scaleX(0.95);-webkit-transform:scaleX(0.95)} .nav-footer{display:flex;width:100%;position:relative;background-color:red;justify-content:flex-start;box-shadow:0px -2px 4px #ddd} .nav-footer:hover{opacity:0.8} .confirm_btn{flex-grow:1;height:36px;background-color:rgb(80,130,220,0.7);color:white;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:20px;border:0;cursor:pointer;z-index:1} .add_btn{flex-grow:1;height:36px;background-color:rgba(60,234,57,0.821);color:white;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:20px;border:0;cursor:pointer;z-index:1} .alter_btn{flex-grow:1;height:36px;background-color:rgba(154,36,160,0.821);color:white;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:20px;border:0;cursor:pointer;z-index:1} .nav-tabs-confirm_btn:hover{opacity:0.8} .changed-signel1{height:8px;width:8px;border-radius:50%;background-color:white;position:absolute;left:105px;top:14px;z-index:1} .changed-signel2{height:8px;width:8px;border-radius:50%;background-color:white;position:absolute;right:10px;top:14px;z-index:1} .statuslayout{height:12px;width:12px;position:absolute;left:8px;top:12px;display:flex;justify-content:center;align-items:center} .statuslayout2{margin-right:6px;margin-left:8px} .statusP{height:12px;width:12px;background-color:rgb(33,247,140);border-radius:50%} .statusE{height:12px;width:12px;background-color:rgb(243,28,28);border-radius:50%} .statusBAN{height:12px;width:12px;background-color:rgb(246,255,0);border-radius:50%} .statusPcircle{position:absolute;width:12px;height:12px;border:2px solid rgb(33,247,140);border-radius:50%;animation:Processing 1.2s infinite ease-in-out;-o-animation:Processing 1.2s infinite ease-in-out;-moz-animation:Processing 1.2s infinite ease-in-out;-webkit-animation:Processing 1.2s infinite ease-in-out;opacity:0} @keyframes Processing{0%{width:12px;height:12px;opacity:0.5} 100%{width:24px;height:24px;opacity:0} }@-o-keyframes Processing{0%{width:12px;height:12px;opacity:0.5} 100%{width:24px;height:24px;opacity:0} }@-moz-keyframes Processing{0%{width:12px;height:12px;opacity:0.5} 100%{width:24px;height:24px;opacity:0} }@-webkit-keyframes Processing{0%{width:12px;height:12px;opacity:0.5} 100%{width:24px;height:24px;opacity:0} }.process{visibility:hidden;position:absolute;height:36px;width:0%;background-color:rgb(0 53 255);transition:width 0.5s;-o-transition:width 0.5s;-moz-transition:width 0.5s;-webkit-transition:width 0.5s} .applysuccess{position:absolute;right:36px;top:6px;z-index:2;transform:scale(1.5);-o-transform:scale(1.5);-ms-transform:scale(1.5);-moz-transform:scale(1.5);-webkit-transform:scale(1.5);clip-path:polygon(0 0,0 0,0 100%,0 100%);-o-clip-path:polygon(0 0,0 0,0 100%,0 100%);-moz-clip-path:polygon(0 0,0 0,0 100%,0 100%);-webkit-clip-path:polygon(0 0,0 0,0 100%,0 100%);animation:success 1.5s infinite;-o-animation:success 1.5s infinite;-moz-animation:success 1.5s infinite;-webkit-animation:success 1.5s infinite} @keyframes success{0%{clip-path:polygon(0 0,0 0,0 100%,0 100%)} 25%{clip-path:polygon(0% 0%,100% 0%,100% 100%,0% 100%)} 100%{clip-path:polygon(100% 0%,100% 0%,100% 100%,100% 100%)} }@-o-keyframes success{0%{-o-clip-path:polygon(0 0,0 0,0 100%,0 100%)} 25%{-o-clip-path:polygon(0% 0%,100% 0%,100% 100%,0% 100%)} 100%{-o-clip-path:polygon(100% 0%,100% 0%,100% 100%,100% 100%)} }@-moz-keyframes success{0%{-moz-clip-path:polygon(0 0,0 0,0 100%,0 100%)} 25%{-moz-clip-path:polygon(0% 0%,100% 0%,100% 100%,0% 100%)} 100%{-moz-clip-path:polygon(100% 0%,100% 0%,100% 100%,100% 100%)} }@-webkit-keyframes success{0%{-webkit-clip-path:polygon(0 0,0 0,0 100%,0 100%)} 25%{-webkit-clip-path:polygon(0% 0%,100% 0%,100% 100%,0% 100%)} 100%{-webkit-clip-path:polygon(100% 0%,100% 0%,100% 100%,100% 100%)} }.side-btn{display:flex;justify-content:center;align-items:center;position:fixed;right:355px;color:white;border:0;padding:4px;background-color:rgba(22,51,194,0.821);box-shadow:0px 0px 4px #575555;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:16px;cursor:pointer;user-select:none;-ms-user-select:none;-moz-user-select:none;-webkit-user-select:none;transform:scale(1);-o-transform:scale(1);-ms-transform:scale(1);-moz-transform:scale(1);-webkit-transform:scale(1);transition:all 0.2s;-o-transition:all 0.2s;-moz-transition:all 0.2s;-webkit-transition:all 0.2s;z-index:999999} .side-btn:hover{transform:scale(0.8);-o-transform:scale(0.8);-ms-transform:scale(0.8);-moz-transform:scale(0.8);-webkit-transform:scale(0.8);opacity:0.8} .side-btn-mode{bottom:380px} .side-btn-import{bottom:340px} .side-btn-export{bottom:300px} .navPanel-resetBtn{display:flex;justify-content:center;align-items:center;position:fixed;height:25px;width:25px;bottom:430px;right:56px;color:white;border-radius:0%;border:0;padding:2px;background-color:#11cc83bd;box-shadow:0px 0px 4px #575555;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:18px;cursor:pointer;user-select:none;transform:scale(1);-o-transform:scale(1);-ms-transform:scale(1);-moz-transform:scale(1);-webkit-transform:scale(1);transition:all 0.5s;-o-transition:all 0.5s;-moz-transition:all 0.5s;-webkit-transition:all 0.5s;z-index:999999} .navPanel-resetBtn:hover{transform:scale(0.8);-o-transform:scale(0.8);-ms-transform:scale(0.8);-moz-transform:scale(0.8);-webkit-transform:scale(0.8);opacity:0.8;border-radius:35%} .resetBtn-icon{height:25px;width:25px;transform:rotate(0deg);-webkit-transform:rotate(0deg);transform-origin:50% 50%;-webkit-transform-origin:50% 50%;transition:all 0.5s;-o-transition:all 0.5s;-moz-transition:all 0.5s;-webkit-transition:all 0.5s} .resetBtn-icon:hover{transform:rotate(360deg);-o-transform:rotate(360deg);-ms-transform:rotate(360deg);-moz-transform:rotate(360deg);-webkit-transform:rotate(360deg)} .block_btn{display:inline-flex;position:relative;left:12px;bottom:-4px;cursor:pointer} .show-animation{width:344px;height:450px;position:fixed;bottom:12px;right:53px;clip-path:polygon(0 0,0 0,0 100%,0 100%);-o-clip-path:polygon(0 0,0 0,0 100%,0 100%);-moz-clip-path:polygon(0 0,0 0,0 100%,0 100%);-webkit-clip-path:polygon(0 0,0 0,0 100%,0 100%);animation:showPanel 0.5s forwards;-o-animation:showPanel 0.5s forwards;-moz-animation:showPanel 0.5s forwards;-webkit-animation:showPanel 0.5s forwards;opacity:1;z-index:99999} @keyframes showPanel{from{clip-path:polygon(0 0,100% 0,100% 0,0 0);opacity:1} to{clip-path:polygon(0 0,100% 0,100% 100%,0 100%);opacity:1} }@-o-keyframes showPanel{from{-o-clip-path:polygon(0 0,100% 0,100% 0,0 0);opacity:1} to{-o-clip-path:polygon(0 0,100% 0,100% 100%,0 100%);opacity:1} }@-moz-keyframes showPanel{from{-moz-clip-path:polygon(0 0,100% 0,100% 0,0 0);opacity:1} to{-moz-clip-path:polygon(0 0,100% 0,100% 100%,0 100%);opacity:1} }@-webkit-keyframes showPanel{from{-webkit-clip-path:polygon(0 0,100% 0,100% 0,0 0);opacity:1} to{-webkit-clip-path:polygon(0 0,100% 0,100% 100%,0 100%);opacity:1} }.hide-animation{width:342px;height:450px;position:fixed;bottom:12px;right:57px;clip-path:polygon(0 0,0 0,0 100%,0 100%);-o-clip-path:polygon(0 0,0 0,0 100%,0 100%);-moz-clip-path:polygon(0 0,0 0,0 100%,0 100%);-webkit-clip-path:polygon(0 0,0 0,0 100%,0 100%);animation:hidePanel 0.5s forwards;-o-animation:hidePanel 0.5s forwards;-moz-animation:hidePanel 0.5s forwards;-webkit-animation:hidePanel 0.5s forwards;z-index:99999} @keyframes hidePanel{from{clip-path:polygon(0 0,100% 0,100% 100%,0 100%);opacity:1} to{clip-path:polygon(0 100%,100% 100%,100% 100%,0 100%);opacity:1} }@-o-keyframes hidePanel{from{-o-clip-path:polygon(0 0,100% 0,100% 100%,0 100%);opacity:1} to{-o-clip-path:polygon(0 100%,100% 100%,100% 100%,0 100%);opacity:1} }@-moz-keyframes hidePanel{from{-moz-clip-path:polygon(0 0,100% 0,100% 100%,0 100%);opacity:1} to{-moz-clip-path:polygon(0 100%,100% 100%,100% 100%,0 100%);opacity:1} }@-webkit-keyframes hidePanel{from{-webkit-clip-path:polygon(0 0,100% 0,100% 100%,0 100%);opacity:1} to{-webkit-clip-path:polygon(0 100%,100% 100%,100% 100%,0 100%);opacity:1} }.setting-show{display:inline-flex;flex-direction:column;justify-content:flex-start;align-items:center;position:absolute;right:0;top:36px;width:50%;height:0px;background-color:rgba(49,182,203,0.821);z-index:2;overflow:hidden;transition:all 1s;-o-transition:all 1s;-moz-transition:all 1s;-webkit-transition:all 1s} .setting-btn{display:flex;align-items:center;height:30px;width:100%;padding:0px;background-color:rgba(22,51,194,0.821);color:white;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:18px;border:0;cursor:pointer;transform:scale(1);-o-transform:scale(1);-ms-transform:scale(1);-moz-transform:scale(1);-webkit-transform:scale(1);transition:all 0.2s;-o-transition:all 0.2s;-moz-transition:all 0.2s;-webkit-transition:all 0.2s} .setting-btn:hover{transform:scale(0.8);-o-transform:scale(0.8);-ms-transform:scale(0.8);-moz-transform:scale(0.8);-webkit-transform:scale(0.8);opacity:0.8} .setting-text{display:flex;justify-content:center;align-items:center;flex-direction:column;height:68px;width:100%;background-color:rgba(22,51,194,0.821);color:white;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-size:18px;border:0;cursor:pointer} .setting-text:hover{opacity:0.8} input[type=range]::-webkit-slider-thumb{appearance:none;-moz-appearance:none;-webkit-appearance:none;height:14px;width:14px;border-radius:50%;background:rgb(5,165,189);cursor:pointer} input[type=range][id=xzsx_length]{appearance:none;-moz-appearance:none;-webkit-appearance:none;height:8px;width:100%;margin-bottom:8px;outline:0;background:none;background:-webkit-linear-gradient(rgb(93,214,65),rgb(93,214,65))no-repeat,#fb0f0f;background-size:50% 100%} input[type=range][id=xzsx_interval]{appearance:none;-moz-appearance:none;-webkit-appearance:none;height:8px;width:100%;margin-bottom:8px;outline:0;background:none;background:-webkit-linear-gradient(#fb0f0f,#fb0f0f)no-repeat,rgb(93,214,65);background-size:10% 100%} input[type=range][id=xzsx_limitevent]{appearance:none;-moz-appearance:none;-webkit-appearance:none;height:8px;width:100%;margin-bottom:8px;outline:0;background:none;background:-webkit-linear-gradient(rgb(93,214,65),rgb(93,214,65))no-repeat,#fb0f0f;background-size:60% 100%} .nav-list::-webkit-scrollbar{height:8px;width:8px} .nav-list::-webkit-scrollbar-thumb{background-color:rgb(80,130,220,1)} .nav-list::-webkit-scrollbar-thumb:hover{background-color:rgb(80,130,220,0.5)} .nav-list::-webkit-scrollbar-track-piece{background:transparent} #rule_keywords::-webkit-scrollbar{height:8px;width:8px} #rule_keywords::-webkit-scrollbar-thumb{background-color:rgb(80,130,220,1)} #rule_keywords::-webkit-scrollbar-thumb:hover{background-color:rgb(80,130,220,0.5)} #rule_keywords::-webkit-scrollbar-track-piece{background:transparent} #rule_antikeywords::-webkit-scrollbar{height:8px;width:8px} #rule_antikeywords::-webkit-scrollbar-thumb{background-color:rgb(80,130,220,1)} #rule_antikeywords::-webkit-scrollbar-thumb:hover{background-color:rgb(80,130,220,0.5)} #rule_antikeywords::-webkit-scrollbar-track-piece{background:transparent} .xzsx_detect_tag-trackpanel-show{width:250px;height:200px;display:flex;flex-direction:column;justify-content:flex-start;position:absolute;background-color:white;box-shadow:0px 0px 4px #6a6767;user-select:none;-o-user-select:none;-ms-user-select:none;-moz-user-select:none;-webkit-user-select:none;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;clip-path:polygon(-5% -5%,-5% -5%,-5% 105%,-5% 105%);-o-clip-path:polygon(-5% -5%,-5% -5%,-5% 105%,-5% 105%);-moz-clip-path:polygon(-5% -5%,-5% -5%,-5% 105%,-5% 105%);-webkit-clip-path:polygon(-5% -5%,-5% -5%,-5% 105%,-5% 105%);animation:showTrack 0.5s forwards;-o-animation:showTrack 0.5s forwards;-moz-animation:showTrack 0.5s forwards;-webkit-animation:showTrack 0.5s forwards;z-index:999999} @keyframes showTrack{from{clip-path:polygon(-5% -5%,-5% -5%,-5% 105%,-5% 105%)} to{clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%)} }@-o-keyframes showTrack{from{-o-clip-path:polygon(-5% -5%,-5% -5%,-5% 105%,-5% 105%)} to{-o-clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%)} }@-moz-keyframes showTrack{from{-moz-clip-path:polygon(-5% -5%,-5% -5%,-5% 105%,-5% 105%)} to{-moz-clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%)} }@-webkit-keyframes showTrack{from{-webkit-clip-path:polygon(-5% -5%,-5% -5%,-5% 105%,-5% 105%)} to{-webkit-clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%)} }.xzsx_detect_tag-medalwall-show{height:100px;width:fit-content;max-width:400px;display:flex;flex-direction:column;position:absolute;background-color:white;box-shadow:0px 0px 4px #6a6767;user-select:none;-o-user-select:none;-ms-user-select:none;-moz-user-select:none;-webkit-user-select:none;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-weight:550;clip-path:polygon(-5% -5%,-5% -5%,-5% 105%,-5% 105%);-o-clip-path:polygon(-5% -5%,-5% -5%,-5% 105%,-5% 105%);-moz-clip-path:polygon(-5% -5%,-5% -5%,-5% 105%,-5% 105%);-webkit-clip-path:polygon(-5% -5%,-5% -5%,-5% 105%,-5% 105%);animation:showTrack 0.5s forwards;-o-animation:showTrack 0.5s forwards;-moz-animation:showTrack 0.5s forwards;-webkit-animation:showTrack 0.5s forwards;z-index:999999;overflow:auto} .xzsx_detect_tag-trackpanel-hide{width:250px;height:200px;display:flex;flex-direction:column;justify-content:flex-start;position:absolute;background-color:white;box-shadow:0px 0px 4px #6a6767;user-select:none;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-weight:550;clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%);-o-clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%);-moz-clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%);-webkit-clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%);animation:hideTrack 0.5s forwards;-o-animation:hideTrack 0.5s forwards;-moz-animation:hideTrack 0.5s forwards;-webkit-animation:hideTrack 0.5s forwards;z-index:999999;overflow:auto} @keyframes hideTrack{from{clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%)} to{clip-path:polygon(105% -5%,105% -5%,105% 105%,105% 105%)} }@-o-keyframes hideTrack{from{-o-clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%)} to{-o-clip-path:polygon(105% -5%,105% -5%,105% 105%,105% 105%)} }@-moz-keyframes hideTrack{from{-moz-clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%)} to{-moz-clip-path:polygon(105% -5%,105% -5%,105% 105%,105% 105%)} }@-webkit-keyframes hideTrack{from{-webkit-clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%)} to{-webkit-clip-path:polygon(105% -5%,105% -5%,105% 105%,105% 105%)} }.xzsx_detect_tag-medalwall-hide{height:100px;width:fit-content;max-width:400px;display:flex;flex-direction:column;position:absolute;background-color:white;box-shadow:0px 0px 4px #6a6767;user-select:none;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑;font-weight:550;clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%);-o-clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%);-moz-clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%);-webkit-clip-path:polygon(-5% -5%,105% -5%,105% 105%,-5% 105%);animation:hideTrack 0.5s forwards;-o-animation:hideTrack 0.5s forwards;-moz-animation:hideTrack 0.5s forwards;-webkit-animation:hideTrack 0.5s forwards;z-index:999999;overflow:auto} .xzsx_detect_tag-medalwall-title{display:inline-flex;align-items:center;height:24px;width:100%;padding-left:8px;padding-top:4px;padding-bottom:4px;color:#1890ff;font-size:18px;font-style:normal;font-weight:600;box-shadow:0px 0px 4px #ddd;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;-o-text-overflow:ellipsis;-ms-text-overflow:ellipsis;cursor:pointer} .xzsx_detect_tag-medalwall-container{position:relative} .xzsx_detect_tag-trackpanel-content::-webkit-scrollbar{height:8px;width:8px} .xzsx_detect_tag-trackpanel-content::-webkit-scrollbar-thumb{background-color:rgb(80,130,220,1)} .xzsx_detect_tag-trackpanel-content::-webkit-scrollbar-thumb:hover{background-color:rgb(80,130,220,0.5)} .xzsx_detect_tag-trackpanel-content::-webkit-scrollbar-track-piece{background:transparent} #xzsx_medalwall::-webkit-scrollbar{height:8px;width:8px} #xzsx_medalwall::-webkit-scrollbar-thumb{background-color:rgb(80,130,220,1)} #xzsx_medalwall::-webkit-scrollbar-thumb:hover{background-color:rgb(80,130,220,0.5)} #xzsx_medalwall::-webkit-scrollbar-track-piece{background:transparent} .xzsx_detect_tag-trackpanel-title{display:flex;align-items:center;height:35px;padding-left:8px;padding-top:4px;padding-bottom:4px;color:#1890ff;font-size:18px;font-style:normal;font-weight:600;box-shadow:0px 0px 4px #ddd;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;-o-text-overflow:ellipsis;-ms-text-overflow:ellipsis;cursor:pointer} .xzsx_detect_tag-trackpanel-content{height:200px;padding:8px;overflow:auto} .xzsx_detect_tag-trackpanel-texttitle{font-weight:600;font-size:20px;margin-bottom:4px} .xzsx_detect_tag-trackpanel-anti{margin-bottom:8px;color:red} .xzsx_detect_tag-trackpanel-fans{margin-bottom:8px;color:green} .xzsx_detect_tag-trackpanel-source{font-size:18px;color:rgba(22,51,194,0.821);font-weight:600} .xzsx_detect_tag-trackpanel-text{font-size:16px;color:rgb(64,63,63);font-weight:400;padding-left:4px;margin-top:4px;margin-bottom:4px;box-shadow:0 0 4px #ddd} ` /** 成分标签相关css样式 * @type { string } */ const css_tag = ` .tag_container{display:inline-flex;align-items:center;flex-wrap:wrap;position:relative;vertical-align:middle;overflow:hidden;padding:1px 0px 1px 0px;margin-left:4px;transform:translateX(-50px);-o-transform:translateX(-50px);-ms-transform:translateX(-50px);-moz-transform:translateX(-50px);-webkit-transform:translateX(-50px);animation:showtag 0.5s ease-out forwards;-o-animation:showtag 0.5s ease-out forwards;-moz-animation:showtag 0.5s ease-out forwards;-webkit-animation:showtag 0.5s ease-out forwards;opacity:0} @keyframes showtag{from{transform:translateX(-50px);opacity:0} to{transform:translateX(0px);opacity:1} }@-o-keyframes showtag{from{-o-transform:translateX(-50px);opacity:0} to{-o-transform:translateX(0px);opacity:1} }@-moz-keyframes showtag{from{-moz-transform:translateX(-50px);opacity:0} to{-moz-transform:translateX(0px);opacity:1} }@-webkit-keyframes showtag{from{-webkit-transform:translateX(-50px);opacity:0} to{-webkit-transform:translateX(0px);opacity:1} }.xzsx_detect_tag-medalwall-medalBtn{display:inline-flex;justify-content:center;align-items:center;height:100%;width:fit-content;margin-right:4px;box-shadow:0 0 6px #ddd;cursor:pointer} .xzsx_detect_tag-medalwall-medalBtn:hover{opacity:0.8} .xzsx_detect_tag{display:inline-flex;height:14px;line-height:14px;position:relative;box-sizing:content-box !important;padding:1px 2px 1px 14px;margin:1px 4px 1px 0;color:black;white-space:nowrap;font-family:"Microsoft YaHei","Microsoft Sans Serif","Microsoft SanSerf",微软雅黑 !important;font-size:14px;font-weight:600 !important;border-radius:6px 4px 4px 6px;box-shadow:0 0 6px #ddd;text-size-adjust:100%;-webkit-tap-highlight-color:transparent;overflow-wrap:break-word;cursor:pointer} .xzsx_detect_tag:hover{opacity:0.8} .xzsx_detect_tag::before{background:#fff;border-radius:10px;box-shadow:0 1px rgba(0,0,0,0.25) inset;content:'';height:6px;left:4px;position:absolute;width:6px;top:calc(50% - 3px)} .icon-expend{height:24px;transform:rotate(90deg);-o-transform:rotate(90deg);-moz-transform:rotate(90deg);-webkit-transform:rotate(90deg);cursor:pointer} .icon-expend:hover{opacity:0.8} .icon-deepcheck{display:none;height:18px;width:18px;margin-left:4px} .icon-deepcheck:hover{opacity:0.8} .icon-deepcheck-hide{display:unset;height:18px;width:18px;transform:scale(1);-o-transform:scale(1);-moz-transform:scale(1);-webkit-transform:scale(1);transition:all 0.2s;-o-transition:all 0.2s;-moz-transition:all 0.2s;-webkit-transition:all 0.2s;cursor:pointer;z-index:1} svg.check-container{background:transparent;position:absolute;height:18px;width:18px;left:0px} circle.check-circle{fill:transparent;stroke:rgb(42,226,223);stroke-width:1.5px;stroke-dasharray:50.3,50.3;stroke-dashoffset:50.3;transition:all 1s;-o-transition:all 1s;-moz-transition:all 1s;-webkit-transition:all 1s;transform-origin:center;-o-transform-origin:center;-moz-transform-origin:center;-webkit-transform-origin:center} circle:hover{stroke-dashoffset:0} .svg-expend{height:25px;width:25px} .svg-deepcehck{height:18px;width:18px;color:#9400D3} ` style.innerHTML = css_panel + css_tag /** 设置面板 * @type { HTMLDivElement } */ const nav = document.createElement('div') nav.innerHTML = ` <button class='nav-show'> 配置 </button> <div id='panel-animation' class='hide-animation'> <div class='navPanel-resetBtn' style='display:${navPanelVis ? '' : 'none'}'> <svg fill="#FFFFFF" t="1636097794549" class="resetBtn-icon" viewBox="0 0 512 512" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5713"> <path d="M245.11,393.317c19.5,0,38.5-3.8,56.6-11.3c36.6-15.1,65-43.6,80.2-80.2s15.2-76.8,0-113.4 c-23-55.6-76.8-91.5-137-91.5c-19.5,0-38.5,3.8-56.6,11.3c-75.5,31.2-111.5,118.1-80.2,193.6 C131.11,357.317,184.91,393.317,245.11,393.317z M195.91,126.517c15.7-6.5,32.2-9.8,49-9.8c52.1,0,98.7,31.1,118.7,79.3 c13.1,31.7,13.1,66.6,0,98.2c-13.1,31.7-37.8,56.3-69.5,69.5c-15.7,6.5-32.2,9.8-49,9.8c-52.1,0-98.7-31.1-118.7-79.3 C99.31,228.817,130.51,153.517,195.91,126.517z"/> <path d="M14.41,328.017c1.5,4,5.3,6.6,9.3,6.6c1.1,0,2.2-0.2,3.3-0.6c5.1-1.8,7.8-7.5,6-12.7 c-29.3-81.9-9.8-171.4,51-233.6c42-43,98.3-67.1,158.4-67.8c60-0.7,116.9,22,159.9,64l-25.3,0.3c-5.5,0.1-9.8,4.5-9.8,10 c0.1,5.4,4.5,9.8,9.9,9.8h0.1l49.2-0.6c2.6,0,5.1-1.1,7-3c1.8-1.9,2.8-4.4,2.8-7l-0.6-49.2c-0.1-5.5-4.5-10-10-9.8 c-5.5,0.1-9.8,4.5-9.8,10l0.3,25.3c-46.8-45.7-108.5-70.4-173.9-69.7c-65.4,0.9-126.7,27-172.4,73.8 c-32.7,33.5-55.1,75.4-64.6,121C-4.09,239.317-0.89,285.317,14.41,328.017z"/> <path d="M406.01,402.417c-86.7,88.8-229.5,90.4-318.3,3.8l25.3-0.3c5.5-0.1,9.8-4.5,9.8-10 c-0.1-5.4-4.5-9.8-9.9-9.8h-0.1l-49.2,0.6c-5.5,0.1-9.8,4.5-9.8,10l0.6,49.2c0.1,5.4,4.5,9.8,9.9,9.8h0.1c5.5-0.1,9.8-4.5,9.8-10 l-0.3-25.3c47.6,46.4,109.3,69.6,171.1,69.6c63.7,0,127.3-24.6,175.2-73.6c32.7-33.5,55.1-75.4,64.6-121 c9.3-44.4,6.1-90.5-9.2-133.1c-1.8-5.1-7.5-7.8-12.7-6c-5.1,1.8-7.8,7.5-6,12.7C486.31,250.717,466.81,340.217,406.01,402.417z"/> </svg> </div> <div class='side-btn side-btn-mode' style='display:${navPanelVis ? '' : 'none'}'>${MODE}</div> <label for="file-upload" class='side-btn side-btn-import' style='display:${navPanelVis ? '' : 'none'}'> 导入 </label> <input id='file-upload' name='config-upload' type='file' accept='.json,.txt' style='display:none' /> <div class='side-btn side-btn-export' style='display:${navPanelVis ? '' : 'none'}'>导出</div> <div class='nav-panel'> <div id='setting-panel' class='setting-show'> <button id='DEL_STYLE' class='setting-btn' style="padding-left: 26px;">${DEL_STYLE}</button> <button id='DET_BASE_MedalWall' class='setting-btn'> <div class='statuslayout2'> <div id='status_MedalWall' class='${DET_BASE['DET_BASE_MedalWall'] ? 'statusP' : 'statusE'}'></div> </div> 粉丝牌 </button> <button id='DET_BASE_FOLLOWINGS' class='setting-btn'> <div class='statuslayout2'> <div id='status_FOLLOWINGS' class='${DET_BASE['DET_BASE_FOLLOWINGS'] ? 'statusP' : 'statusE'}'></div> </div> 关注列表 </button> <button id='DET_BASE_VIDEOS' class='setting-btn'> <div class='statuslayout2'> <div id='status_VIDEOS' class='${DET_BASE['DET_BASE_VIDEOS'] ? 'statusP' : 'statusE'}'></div> </div> 投稿列表 </button> <button id='DET_BASE_DYNAMIC' class='setting-btn'> <div class='statuslayout2'> <div id='status_DYNAMIC' class='${DET_BASE['DET_BASE_DYNAMIC'] ? 'statusP' : 'statusE'}'></div> </div> 动态内容 </button> <button id='DET_BASE_COMMENT' class='setting-btn'> <div class='statuslayout2'> <div id='status_COMMENT' class='${DET_BASE['DET_BASE_COMMENT'] ? 'statusP' : 'statusE'}'></div> </div> 评论内容 </button> <div id='TAG_MAX_LENGTH' class='setting-text'> 收#阈值 <span>${TAG_MAX_LENGTH}</span> <input id='xzsx_length' type="range" min="1" max="50" step="1" value="${TAG_MAX_LENGTH}"/> </div> <div id='INTERVAL' class='setting-text'> 渲染间隔 <span>${INTERVAL}</span> <input id='xzsx_interval' type="range" min="0" max="5000" step="250" value="${INTERVAL}"/> </div> <div id='LIMIT_EVENT' class='setting-text'> 事件节流 <span>${LIMIT_EVENT}</span> <input id='xzsx_limitevent' type="range" min="1" max="25" step="1" value="${LIMIT_EVENT}"/> </div> </div> <div class='nav-tabs'> <button class='nav-tabs-btn1'> 检测成分 <div id='status1' class='statuslayout'> <div id='status1_circleE' class='statusE' style='display:${DET ? 'none' : ''}'></div> <div id='status1_circleP' class='statusP' style='display:${DET ? '' : 'none'}'></div> <div id='status1_effect' class='statusPcircle' style='display:${DET ? '' : 'none'}'></div> </div> </button> <div class='changed-signel1' style='display:none'></div> <div id='process1' class='process' style="left:0%;"></div> <button class='nav-tabs-btn2'> 自动屏蔽 <div id='status2' class='statuslayout'> <div id='status2_circleE' class='statusE' style='display:${DEL ? 'none' : ''}'></div> <div id='status2_circleP' class='statusP' style='display:${DEL ? '' : 'none'}'></div> <div id='status2_effect' class='statusPcircle' style='display:${DEL ? '' : 'none'}'></div> </div> </button> <div class='changed-signel2' style='display:none'></div> <div id='process2' class='process' style="left:50%;"></div> </div> <div class='addrule_form'> <div class='addrule_item1'> <input id='tagname' type="text" class='tagname_input' placeholder='标签名' /> <input id="tagcolor" type="color" class='tagcolor_input' value='#008000' /> </div> <div class='xzsx_detect_tagsample_container'> <div class='xzsx_detect_tagsample'></div> </div> <div id='rule_keywords' class='rule_keywords'> <div class='add_items'> <input id='keyword' type="text" class='tagkeyword_input' placeholder='粉丝关键词' /> <div id='keywords-add' class='add_icon'>+</div> </div> </div> <div id='rule_antikeywords' class='rule_keywords'> <div class='add_items'> <input id='antikeyword' type="text" class='tagkeyword_input' placeholder='黑子关键词' /> <div id='antikeywords-add' class='add_icon'>+</div> </div> </div> </div> <div class='nav-list'> <div class='rule-item-detect-add'>+</div> <div class='rule-item-blacklist-add'> <input type="text" placeholder='关键词' class='rule-item-blacklist-add-input' style="display:unset"/> <select name='rule-item-blacklist-select' class='rule-item-blacklist-select' style="display:none" required></select> <div class='rule-item-blacklist-base'>基于昵称</div> <div id='blacklist-add' class='rule-item-blacklist-addicon'>+</div> </div> </div> <div class='nav-footer'> <div class='applysuccess' style="display:none"> <svg fill="white" width="24px" height="24px" viewBox="0 0 36 36" version="1.1"> <path d="M13.72,27.69,3.29,17.27a1,1,0,0,1,1.41-1.41l9,9L31.29,7.29a1,1,0,0,1,1.41,1.41Z"></path> <rect x="0" y="0" width="36" height="36" fill-opacity="0" /> </svg> </div> <div id='footer-process' class='process'></div> <button class='confirm_btn'>应 用</button> <button class='add_btn' style="display:none">新 增 规 则</button> <button class='alter_btn' style="display:none">修 改 规 则</button> </div> </div> </div> <div id='xzsx_trackpanel' class='xzsx_detect_tag-trackpanel-hide' style='display:none'></div> <div id='xzsx_medalwall' class='xzsx_detect_tag-medalwall-hide'></div> ` document.querySelector('head').appendChild(style) document.querySelector('body').appendChild(nav) /** <===================================================================================获取实例==========================================================================================> * @type { Element } */ let /** 面板显示/隐藏动画 */ animation = document.querySelector('#panel-animation'), /** 侧边悬浮按钮 */ navBtn = document.querySelector('.nav-show'), /** 设置面板 */ navPanel = document.querySelector('.nav-panel'), /** 成分溯源面板 */ trackPanel = document.querySelector('#xzsx_trackpanel'), /** 勋章墙 */ medalWall = document.querySelector('#xzsx_medalwall'), /** 侧边悬浮按钮 */ resetBtn = document.querySelector('.navPanel-resetBtn'), /** 切换模式按钮 */ navMode = document.querySelector('.side-btn.side-btn-mode'), /** 导入按钮 */ importBtn = document.querySelector('.side-btn.side-btn-import'), /** 上传文件 */ uploader = document.querySelector('#file-upload'), /** 侧边悬浮按钮 */ exportBtn = document.querySelector('.side-btn.side-btn-export'), /** 检测成分按钮 */ navTabsBtn1 = document.querySelector('.nav-tabs-btn1'), /** 自动屏蔽按钮 */ navTabsBtn2 = document.querySelector('.nav-tabs-btn2'), /** 设置面板 */ settingPanel = document.querySelector('#setting-panel'), /** 屏蔽样式按钮 */ DEL_STYLE_BTN = document.querySelector('#DEL_STYLE'), /**检测依据-粉丝牌 */ DET_BASE_MedalWall = document.querySelector('#DET_BASE_MedalWall'), /** 检测依据-关注列表 */ DET_BASE_FOLLOWINGS = document.querySelector('#DET_BASE_FOLLOWINGS'), /** 检测依据-投稿列表 */ DET_BASE_VIDEOS = document.querySelector('#DET_BASE_VIDEOS'), /** 检测依据-动态空间 */ DET_BASE_DYNAMIC = document.querySelector('#DET_BASE_DYNAMIC'), /** 检测依据-评论文本 */ DET_BASE_COMMENT = document.querySelector('#DET_BASE_COMMENT'), /** 标签最大长度文本 */ TAG_MAX_LENGTH_TEXT = document.querySelector('#TAG_MAX_LENGTH'), /** 标签最大长度滑动条 */ TAG_MAX_LENGTH_SLIDER = document.querySelector('#xzsx_length'), /** 渲染间隔文本 */ INTERVAL_TEXT = document.querySelector('#INTERVAL'), /** 渲染间隔滑动条 */ INTERVAL_SLIDER = document.querySelector('#xzsx_interval'), /** 事件节流文本 */ LIMIT_EVENT_TEXT = document.querySelector('#LIMIT_EVENT'), /** 事件节流滑动条 */ LIMIT_EVENT_SLIDER = document.querySelector('#xzsx_limitevent'), /** 检测成分启用状态标识1 */ status1_circleP = document.querySelector('#status1_circleP'), /** 检测成分启用状态标识2 */ status1_circleE = document.querySelector('#status1_circleE'), /** 检测成分启用状态标识3 */ status1_effect = document.querySelector('#status1_effect'), /** 自动屏蔽启用状态标识1 */ status2_circleP = document.querySelector('#status2_circleP'), /** 自动屏蔽启用状态标识2 */ status2_circleE = document.querySelector('#status2_circleE'), /** 自动屏蔽启用状态标识3 */ status2_effect = document.querySelector('#status2_effect'), /** 检测依据启用状态标识-粉丝牌 */ status_MedalWall = document.querySelector('#status_MedalWall'), /** 检测依据启用状态标识-关注列表 */ status_FOLLOWINGS = document.querySelector('#status_FOLLOWINGS'), /** 检测依据启用状态标识-投稿列表 */ status_VIDEOS = document.querySelector('#status_VIDEOS'), /** 检测依据启用状态标识-动态内容(), */ status_DYNAMIC = document.querySelector('#status_DYNAMIC'), /** 检测依据启用状态标识-评论内容 */ status_COMMENT = document.querySelector('#status_COMMENT'), /** 检测成分长按进度条 */ process1 = document.querySelector('#process1'), /** 自动屏蔽长按进度条 */ process2 = document.querySelector('#process2'), /** 检测成分修改标识 */ changedSignel1 = document.querySelector('.changed-signel1'), /** 自动屏蔽修改标识 */ changedSignel2 = document.querySelector('.changed-signel2'), /** 规则列表 */ navRuleList = document.querySelector('.nav-list'), /** 检测成分添加/修改规则界面 */ addRuleForm = document.querySelector('.addrule_form'), /** 检测成分添加/修改规则界面 添加粉丝关键词右侧<+>号按钮 */ addKeyWordBtn = document.querySelector('#keywords-add'), /** 检测成分添加/修改规则界面 添加黑子关键词右侧<+>号按钮 */ addAntiKeyWordBtn = document.querySelector('#antikeywords-add'), /** 检测成分添加/修改规则界面 添加粉丝关键词列表 */ keywords = document.querySelector('#rule_keywords'), /** 检测成分添加/修改规则界面 添加黑子关键词列表 */ antikeywords = document.querySelector('#rule_antikeywords'), /** 检测成分添加/修改规则界面 标签名 */ tagname = document.querySelector('#tagname'), /** 检测成分添加/修改规则界面 标签颜色 */ tagcolor = document.querySelector('#tagcolor'), /** 检测成分添加/修改规则界面 标签样式预览 */ tagsample = document.querySelector('.xzsx_detect_tagsample'), /** 检测成分添加/修改规则界面 添加粉丝关键词左侧文字输入框 */ newKeyWord = document.querySelector('#keyword'), /** 检测成分添加/修改规则界面 添加黑子关键词左侧文字输入框 */ newAntiKeyWord = document.querySelector('#antikeyword'), /** 检测成分规则列表 添加按钮 */ detectAdd_btn = document.querySelector('.rule-item-detect-add'), /** 黑名单规则列表 添加关键词容器 */ blacklistAdd_item = document.querySelector('.rule-item-blacklist-add'), /** 黑名单规则列表 添加关键词选择框 */ blacklistAdd_select = document.querySelector('.rule-item-blacklist-select'), /** 黑名单规则列表 添加关键词右侧<+>号按钮 */ blacklistAdd_btn = document.querySelector('#blacklist-add'), /** 黑名单规则列表 添加关键词屏蔽依据 */ blacklistAdd_base = document.querySelector('.rule-item-blacklist-base'), /** 黑名单规则列表 添加关键词左侧文字输入框 */ blacklistAdd_keyword = document.querySelector('.rule-item-blacklist-add-input'), /** 页脚 进度条 */ footerProcess = document.querySelector('#footer-process'), /** 页脚 应用按钮 */ confirm_btn = document.querySelector('.confirm_btn'), /** 页脚 应用成功动画 */ confirm_success = document.querySelector('.applysuccess'), /** 页脚 新增规则按钮 */ add_btn = document.querySelector('.add_btn'), /** 页脚 修改规则按钮 */ alter_btn = document.querySelector('.alter_btn') /** 批量监听下拉设置栏DOM元素的变化并绑定数据 * @type { () => void } */ const batchWatch = () => { /** * @type { { watchObj: object, watchKey: string, target: { ele: Element | null, key: string, callback: () => string | number }[] } */ const watchParams = [ { watchObj: settingTemp, watchKey: 'DEL_STYLE', target: { ele: DEL_STYLE_BTN, key: 'textContent', callback: (newValue) => newValue } }, { watchObj: settingTemp, watchKey: 'TAG_MAX_LENGTH', target: { ele: TAG_MAX_LENGTH_TEXT.childNodes[1], key: 'textContent', callback: (newValue) => newValue } }, { watchObj: settingTemp, watchKey: 'INTERVAL', target: { ele: INTERVAL_TEXT.childNodes[1], key: 'textContent', callback: (newValue) => newValue } }, { watchObj: settingTemp, watchKey: 'LIMIT_EVENT', target: { ele: LIMIT_EVENT_TEXT.childNodes[1], key: 'textContent', callback: (newValue) => newValue } }, { watchObj: settingTemp, watchKey: 'DET_BASE_MedalWall', target: { ele: status_MedalWall, key: 'className', callback: (newValue) => newValue ? 'statusP' : 'statusE' } }, { watchObj: settingTemp, watchKey: 'DET_BASE_FOLLOWINGS', target: { ele: status_FOLLOWINGS, key: 'className', callback: (newValue) => newValue ? 'statusP' : 'statusE' } }, { watchObj: settingTemp, watchKey: 'DET_BASE_VIDEOS', target: { ele: status_VIDEOS, key: 'className', callback: (newValue) => newValue ? 'statusP' : 'statusE' } }, { watchObj: settingTemp, watchKey: 'DET_BASE_DYNAMIC', target: { ele: status_DYNAMIC, key: 'className', callback: (newValue) => newValue ? 'statusP' : 'statusE' } }, { watchObj: settingTemp, watchKey: 'DET_BASE_COMMENT', target: { ele: status_COMMENT, key: 'className', callback: (newValue) => newValue ? 'statusP' : 'statusE' } }, ] watchParams.forEach((v) => watch(v.watchObj, v.watchKey, (newValue) => v.target.ele[v.target.key] = v.target.callback(newValue))) } /** 获取集合1相对于集合2的差集 * @param { Set } set1 集合1 * @param { Set } set2 集合2 * @returns { Set } 差集结果 */ const _difference = (set1, set2) => new Set([...set1].filter((v) => !set2.has(v))) /** 重置DOM状态 * @type { () => void } */ const resetAll = () => { navMode.textContent = MODE status1_circleP.style.display = DET ? '' : 'none' status1_circleE.style.display = DET ? 'none' : '' status1_effect.style.display = DET ? '' : 'none' process1.style.visibility = 'hidden' status2_circleP.style.display = DEL ? '' : 'none' status2_circleE.style.display = DEL ? 'none' : '' status2_effect.style.display = DEL ? '' : 'none' process2.style.visibility = 'hidden' DEL_STYLE_BTN.textContent = DEL_STYLE TAG_MAX_LENGTH_SLIDER.value = parseInt(TAG_MAX_LENGTH) INTERVAL_SLIDER.value = parseInt(INTERVAL) LIMIT_EVENT_SLIDER.value = parseInt(LIMIT_EVENT) TAG_MAX_LENGTH_TEXT.childNodes[1].textContent = parseInt(TAG_MAX_LENGTH) INTERVAL_TEXT.childNodes[1].textContent = parseInt(INTERVAL) LIMIT_EVENT_TEXT.childNodes[1].textContent = parseInt(LIMIT_EVENT) status_MedalWall.className = API_OVERLOAD['DET_BASE_MedalWall'].state ? 'statusBAN' : DET_BASE['DET_BASE_MedalWall'] ? 'statusP' : 'statusE' status_FOLLOWINGS.className = API_OVERLOAD['DET_BASE_FOLLOWINGS'].state ? 'statusBAN' : DET_BASE['DET_BASE_FOLLOWINGS'] ? 'statusP' : 'statusE' status_VIDEOS.className = API_OVERLOAD['DET_BASE_VIDEOS'].state ? 'statusBAN' : DET_BASE['DET_BASE_VIDEOS'] ? 'statusP' : 'statusE' status_DYNAMIC.className = API_OVERLOAD['DET_BASE_DYNAMIC'].state ? 'statusBAN' : DET_BASE['DET_BASE_DYNAMIC'] ? 'statusP' : 'statusE' status_COMMENT.className = DET_BASE['DET_BASE_COMMENT'] ? 'statusP' : 'statusE' } /** <===================================================================================事件绑定==========================================================================================> */ /** 阻止点击事件冒泡 */ [navPanel, trackPanel, medalWall, importBtn, uploader].forEach(ele => ele.onclick = e => e.stopPropagation()) /** 控制面板可见性 */ navBtn.onclick = (e) => { e.stopPropagation() animationState = !animationState animation.className = animationState ? 'show-animation' : 'hide-animation' clearTimeout(timerAnimaltion) if (navPanelVis && !animationState) { timerAnimaltion = setTimeout(() => { navPanelVis = false navPanel.style.display = 'none' resetBtn.style.display = 'none' navMode.style.display = 'none' importBtn.style.display = 'none' exportBtn.style.display = 'none' blacklistAdd_keyword.value = '' blacklistAdd_select.style.display = 'none' blacklistAdd_keyword.style.display = 'unset' blacklistAdd_base.textContent = '基于昵称' }, 500) } else { [rulesApply, rules] = initRules() resetAll() renderMainPanel() navPanelVis = true, ctab = true, ckey = undefined, newRuleKeysSet.clear(), deleteRuleKeysSet.clear(), newAntiRuleSets.clear(), deleteAntiRuleSets.clear(), settingTemp = {} batchWatch() navPanel.style.display = 'flex' resetBtn.style.display = 'flex' navMode.style.display = 'flex' importBtn.style.display = 'flex' exportBtn.style.display = 'flex' changedSignel1.style.display = 'none' changedSignel2.style.display = 'none' navTabsBtn1.click() } } /** 设置面板鼠标移开相关操作 */ navPanel.onmouseleave = () => { settingPanel.style.height = '0px' } /** 点击空白处相关操作 */ document.addEventListener('click', () => { trackPanel.className = 'xzsx_detect_tag-trackpanel-hide'; medalWall.className = 'xzsx_detect_tag-medalwall-hide'; medalWall.setAttribute('key', null) }) /** 执行显示隐藏面板各一次 */ const [_dreset, __] = _throttle(() => { navBtn.click() }, 500, { leading: true }) /** 恢复设置 */ resetBtn.onclick = (e) => { e.stopPropagation(); _dreset() } /** 切换模式 */ navMode.onclick = (e) => { e.stopPropagation() let MODE_TEMP = settingTemp.hasOwnProperty('MODE') ? settingTemp['MODE'] === '自动' ? '静默' : '自动' : MODE === '自动' ? '静默' : '自动' navMode.textContent = MODE_TEMP settingTemp['MODE'] = MODE_TEMP } /** 导入 */ uploader.onchange = async (e) => { e.stopPropagation() try { let conf = JSON.parse(await e.target.files[0].text()) if (Object.keys(conf).length > 0) { GM_setValue('xzsx_bilibili_detector', JSON.stringify({ blackList: conf.blackList, detect: conf.detect })) GM_setValue('xzsx_bilibili_detector_state', JSON.stringify(conf.state)) resetBtn.click() e.target.value = '' } } catch (error) { console.log(error) } } /** 导出 */ exportBtn.onclick = (e) => { e.stopPropagation() let temp = {}, state = { MODE, DET, DEL, DEL_STYLE, TAG_MAX_LENGTH, INTERVAL, LIMIT_EVENT, DET_BASE_MedalWall: DET_BASE['DET_BASE_MedalWall'], DET_BASE_FOLLOWINGS: DET_BASE['DET_BASE_FOLLOWINGS'], DET_BASE_VIDEOS: DET_BASE['DET_BASE_VIDEOS'], DET_BASE_DYNAMIC: DET_BASE['DET_BASE_DYNAMIC'], DET_BASE_COMMENT: DET_BASE['DET_BASE_COMMENT'] } Object.keys(rulesApply.detect).forEach((key) => { temp[key] = { 'color': rulesApply.detect[key].color, 'keywords': [...rulesApply.detect[key].keywords], 'antikeywords': [...rulesApply.detect[key].antikeywords] } }) let json = JSON.stringify({ blackList: rulesApply.blackList, detect: temp, state }) let blob = new Blob([json], { type: "octet/stream" }) let url = window.URL.createObjectURL(blob) let a = document.createElement("a"); a.href = url a.download = 'xzsx_bilibili_detector.json' a.click() a.remove() window.URL.revokeObjectURL(url) } /** 检测成分按钮单击相关操作 */ navTabsBtn1.onclick = () => { ctab = true addRuleForm.style.display = 'none' detectAdd_btn.style.display = '' blacklistAdd_item.style.display = 'none' confirm_btn.style.display = '' add_btn.style.display = 'none' alter_btn.style.display = 'none' navTabsBtn1.style.filter = 'brightness(100%)' navTabsBtn2.style.filter = 'brightness(50%)' newRuleKeysSet.clear() deleteRuleKeysSet.clear() newAntiRuleSets.clear() deleteAntiRuleSets.clear() renderMainPanel() } /** 检测成分按钮长按相关操作 */ navTabsBtn1.onmousedown = () => { if (ctab) { process1.style.visibility = 'visible' process1.style.width = '50%' timerTab1 = setTimeout(() => { let temp = settingTemp.hasOwnProperty('DET') ? !settingTemp['DET'] : !DET settingTemp['DET'] = temp changedSignel1.style.display = '' status1_circleP.style.display = temp ? '' : 'none' status1_circleE.style.display = temp ? 'none' : '' status1_effect.style.display = 'none' process1.style.visibility = 'hidden' process1.style.width = '0%' }, 500) } } /** 检测成分按钮松开相关操作 */ navTabsBtn1.onmouseup = () => { if (ctab) { process1.style.width = '0%' clearTimeout(timerTab1) } } /** 检测成分按钮离开相关操作 */ navTabsBtn1.onmouseout = () => { if (ctab) { process1.style.width = '0%' clearTimeout(timerTab1) } } /** 检测成分按钮标悬浮相关操作 */ navTabsBtn1.onmouseenter = () => { settingPanel.style.height = '0px' } /** 自动屏蔽按钮单击相关操作 */ navTabsBtn2.onclick = () => { ctab = false addRuleForm.style.display = 'none' detectAdd_btn.style.display = 'none' blacklistAdd_item.style.display = 'flex' confirm_btn.style.display = '' add_btn.style.display = 'none' alter_btn.style.display = 'none' navTabsBtn1.style.filter = 'brightness(50%)' navTabsBtn2.style.filter = 'brightness(100%)' newRuleKeysSet.clear() deleteRuleKeysSet.clear() newAntiRuleSets.clear() deleteAntiRuleSets.clear() renderMainPanel() } /** 自动屏蔽按钮鼠标聚焦相关操作 */ navTabsBtn2.onmouseenter = () => { if (!ctab) settingPanel.style.height = '378px' } /** 设置下拉栏按钮鼠标离开相关操作 */ settingPanel.onmouseleave = () => { settingPanel.style.height = '0px' } /** 规则列表鼠标悬浮相关操作 */ navRuleList.onmouseenter = () => { settingPanel.style.height = '0px' } /** 修改屏蔽样式 */ DEL_STYLE_BTN.onclick = () => { let copy = JSON.parse(JSON.stringify(settingTemp)) settingTemp['DEL_STYLE'] = copy.hasOwnProperty('DEL_STYLE') ? copy['DEL_STYLE'] === '消息屏蔽' ? '完全删除' : '消息屏蔽' : DEL_STYLE === '消息屏蔽' ? '完全删除' : '消息屏蔽' } /** 修改标签收#阈值 */ TAG_MAX_LENGTH_SLIDER.onchange = (e) => settingTemp['TAG_MAX_LENGTH'] = parseInt(e.target.value) /** 修改渲染间隔(毫秒) */ INTERVAL_SLIDER.onchange = (e) => settingTemp['INTERVAL'] = parseInt(e.target.value) /** 事件节流(每秒) */ LIMIT_EVENT_SLIDER.onchange = (e) => settingTemp['LIMIT_EVENT'] = parseInt(e.target.value) /** 检测依据-粉丝牌 */ DET_BASE_MedalWall.onclick = () => { let copy = JSON.parse(JSON.stringify(settingTemp)) settingTemp['DET_BASE_MedalWall'] = copy.hasOwnProperty('DET_BASE_MedalWall') ? !copy['DET_BASE_MedalWall'] : !DET_BASE['DET_BASE_MedalWall'] } /** 检测依据-关注列表 */ DET_BASE_FOLLOWINGS.onclick = () => { let copy = JSON.parse(JSON.stringify(settingTemp)) settingTemp['DET_BASE_FOLLOWINGS'] = copy.hasOwnProperty('DET_BASE_FOLLOWINGS') ? !copy['DET_BASE_FOLLOWINGS'] : !DET_BASE['DET_BASE_FOLLOWINGS'] } /** 检测依据-投稿列表 */ DET_BASE_VIDEOS.onclick = () => { let copy = JSON.parse(JSON.stringify(settingTemp)) settingTemp['DET_BASE_VIDEOS'] = copy.hasOwnProperty('DET_BASE_VIDEOS') ? !copy['DET_BASE_VIDEOS'] : !DET_BASE['DET_BASE_VIDEOS'] } /** 检测依据-动态空间 */ DET_BASE_DYNAMIC.onclick = () => { let copy = JSON.parse(JSON.stringify(settingTemp)) settingTemp['DET_BASE_DYNAMIC'] = copy.hasOwnProperty('DET_BASE_DYNAMIC') ? !copy['DET_BASE_DYNAMIC'] : !DET_BASE['DET_BASE_DYNAMIC'] } /** 检测依据-评论文本 */ DET_BASE_COMMENT.onclick = () => { let copy = JSON.parse(JSON.stringify(settingTemp)) settingTemp['DET_BASE_COMMENT'] = copy.hasOwnProperty('DET_BASE_COMMENT') ? !copy['DET_BASE_COMMENT'] : !DET_BASE['DET_BASE_COMMENT'] } /** 自动屏蔽按钮长按相关操作 */ navTabsBtn2.onmousedown = () => { if (!ctab) { process2.style.visibility = 'visible' process2.style.width = '50%' timerTab2 = setTimeout(() => { let temp = settingTemp.hasOwnProperty('DEL') ? !settingTemp['DEL'] : !DEL settingTemp['DEL'] = temp changedSignel2.style.display = '' status2_circleP.style.display = temp ? '' : 'none' status2_circleE.style.display = temp ? 'none' : '' status2_effect.style.display = 'none' process2.style.visibility = 'hidden' process2.style.width = '0%' }, 500) } } /** 自动屏蔽按钮松开相关操作 */ navTabsBtn2.onmouseup = () => { if (!ctab) { process2.style.width = '0%' clearTimeout(timerTab2) } } /** 自动屏蔽按钮离开相关操作 */ navTabsBtn2.onmouseout = () => { if (!ctab) { process2.style.width = '0%' clearTimeout(timerTab2) } } /** 检测成分添加规则按钮相关操作 */ detectAdd_btn.onclick = () => { add_btn.style.display = '' alter_btn.style.display = 'none' confirm_btn.style.display = 'none' renderEditPanel(undefined) } /** 自动屏蔽添加规则按钮相关操作 */ blacklistAdd_btn.onclick = () => { if (blacklistAdd_base.textContent === '基于成分' && blacklistAdd_select.value.trim().length > 0 && !rules.blackList.hasOwnProperty(blacklistAdd_select.value)) { changedSignel2.style.display = '' rules.blackList[blacklistAdd_select.value.trim()] = blacklistAdd_base.textContent blacklistAdd_select.querySelector(`option[value="${blacklistAdd_select.value.trim()}"]`).remove() renderMainPanel() } else if (blacklistAdd_base.textContent === '基于昵称') { let keyword = blacklistAdd_keyword.value?.trim() ?? '' if (!rules.blackList.hasOwnProperty(keyword) && keyword.length > 0) { changedSignel2.style.display = '' rules.blackList[keyword] = blacklistAdd_base.textContent blacklistAdd_keyword.value = '' renderMainPanel() } } } /** 自动屏蔽屏蔽依据按钮相关操作 */ blacklistAdd_base.onclick = () => { blacklistAdd_base.textContent = blacklistAdd_base.textContent === '基于昵称' ? '基于成分' : '基于昵称' if (blacklistAdd_base.textContent === '基于成分') { let detectListKeys = Object.keys(rules.detect), blackListKeys = Object.keys(rules.blackList), htmlStr = '' detectListKeys.filter((key) => !blackListKeys.includes(key)).forEach((v) => htmlStr += `<option value='${v.trim()}'>${v.trim()}</option>`) blacklistAdd_select.innerHTML = htmlStr blacklistAdd_select.style.display = 'unset' blacklistAdd_keyword.style.display = 'none' } else if (blacklistAdd_base.textContent === '基于昵称') { blacklistAdd_select.style.display = 'none' blacklistAdd_keyword.style.display = 'unset' } } /** 黑子关键词输入框回车直接触发添加 */ blacklistAdd_keyword.onkeydown = (e) => { e.keyCode === 13 && blacklistAdd_btn.click() } /** 检测成分添加/修改页面 拾色器相关操作 */ tagcolor.onchange = (e) => { tagname.style.color = e.target.value changedSignel1.style.display = '' tagsample.style.color = getContrastColor(e.target.value) tagsample.style.backgroundColor = e.target.value tagsample.style.borderColor = e.target.value + '80' tagsample.style.backgroundImage = `linear-gradient(45deg, ${e.target.value}, ${e.target.value + '80'})` } /** 检测成分添加/修改页面 标签名相关操作 */ tagname.onchange = (e) => { changedSignel1.style.display = '' tagname.title = e.target.value tagsample.title = e.target.value tagsample.textContent = e.target.value } /** 检测成分添加/修改页面 添加粉丝关键词按钮<+>相关操作 */ addKeyWordBtn.onclick = () => { let keyword = newKeyWord.value?.trim() ?? '', exist = false for (const dom of keywords.querySelectorAll('.keyword')) { if (dom.firstChild.textContent === keyword) { exist = true break } } if (keyword.length > 0 && !exist) { newRuleKeysSet.add(keyword) newKeyWord.value = '' changedSignel1.style.display = '' let keywordDom = document.createElement('div') keywordDom.className = 'keyword' keywordDom.style.color = 'green' keywordDom.innerHTML = `${keyword}<div class="keyword_delete">x</div>` keywordDom.children[0].onclick = () => { newRuleKeysSet.has(keyword) ? newRuleKeysSet.delete(keyword) : deleteRuleKeysSet.add(keyword) keywordDom.remove(keywordDom.children[0]) changedSignel1.style.display = '' } keywords.insertBefore(keywordDom, keywords.lastChild.previousSibling) } } /** 粉丝关键词输入框回车直接触发添加 */ newKeyWord.onkeydown = (e) => { e.keyCode === 13 && addKeyWordBtn.click() } /** 检测成分添加/修改页面 添加黑子关键词按钮<+>相关操作 */ addAntiKeyWordBtn.onclick = () => { let keyword = newAntiKeyWord.value?.trim() ?? '', exist = false for (const dom of antikeywords.querySelectorAll('.keyword')) { if (dom.firstChild.textContent === keyword) { exist = true break } } if (keyword.length > 0 && !exist) { newAntiRuleSets.add(keyword) newAntiKeyWord.value = '' changedSignel1.style.display = '' let keywordDom = document.createElement('div') keywordDom.className = 'keyword' keywordDom.style.color = 'rgb(243,28,28)' keywordDom.innerHTML = `${keyword}<div class="keyword_delete">x</div>` keywordDom.children[0].onclick = () => { newAntiRuleSets.has(keyword) ? newAntiRuleSets.delete(keyword) : deleteAntiRuleSets.add(keyword) keywordDom.remove(keywordDom.children[0]) changedSignel1.style.display = '' } antikeywords.insertBefore(keywordDom, antikeywords.lastChild.previousSibling) } } /** 黑子关键词输入框回车直接触发添加 */ newAntiKeyWord.onkeydown = (e) => { e.keyCode === 13 && addAntiKeyWordBtn.click() } /** 页脚应用按钮长按相关操作 */ confirm_btn.onmousedown = async () => { clearTimeout(timerFoot) clearTimeout(timerFoot2) footerProcess.style.visibility = 'visible' footerProcess.style.width = '100%' timerFoot = setTimeout(async () => { let curDocument = domain === 'live.bilibili.com' ? ciframe !== undefined ? ciframe.contentWindow.document : document : document curDocument.querySelectorAll('.tag_container').forEach(tag => tag.remove()) userTags = {} userTagsHash = {} userBlackList.clear() uidset.clear() changedSignel1.style.display = 'none' changedSignel2.style.display = 'none' footerProcess.style.visibility = 'hidden' footerProcess.style.width = '0%' blacklistAdd_keyword.value = '' trackPanel.className = 'xzsx_detect_tag-trackpanel-hide' medalWall.className = 'xzsx_detect_tag-trackpanel-hide' medalWall.setAttribute('key', null) let settingTempCopy = JSON.parse(JSON.stringify(settingTemp)) settingTemp = {} if (settingTempCopy.hasOwnProperty('MODE')) MODE = settingTempCopy['MODE'] if (settingTempCopy.hasOwnProperty('DET')) DET = settingTempCopy['DET'] if (settingTempCopy.hasOwnProperty('DEL')) DEL = settingTempCopy['DEL'] if (DET) status1_effect.style.display = '' if (DEL) status2_effect.style.display = '' if (settingTempCopy.hasOwnProperty('DEL_STYLE')) DEL_STYLE = settingTempCopy['DEL_STYLE'] if (settingTempCopy.hasOwnProperty('TAG_MAX_LENGTH')) TAG_MAX_LENGTH = settingTempCopy['TAG_MAX_LENGTH'] if (settingTempCopy.hasOwnProperty('INTERVAL')) INTERVAL = settingTempCopy['INTERVAL'] if (settingTempCopy.hasOwnProperty('LIMIT_EVENT')) LIMIT_EVENT = settingTempCopy['LIMIT_EVENT'] if (settingTempCopy.hasOwnProperty('DET_BASE_MedalWall')) DET_BASE['DET_BASE_MedalWall'] = settingTempCopy['DET_BASE_MedalWall']; if (DET_BASE['DET_BASE_MedalWall']) recoverAPI('DET_BASE_MedalWall') if (settingTempCopy.hasOwnProperty('DET_BASE_FOLLOWINGS')) DET_BASE['DET_BASE_FOLLOWINGS'] = settingTempCopy['DET_BASE_FOLLOWINGS']; if (DET_BASE['DET_BASE_FOLLOWINGS']) recoverAPI('DET_BASE_FOLLOWINGS') if (settingTempCopy.hasOwnProperty('DET_BASE_VIDEOS')) DET_BASE['DET_BASE_VIDEOS'] = settingTempCopy['DET_BASE_VIDEOS']; if (DET_BASE['DET_BASE_VIDEOS']) recoverAPI('DET_BASE_VIDEOS') if (settingTempCopy.hasOwnProperty('DET_BASE_DYNAMIC')) DET_BASE['DET_BASE_DYNAMIC'] = settingTempCopy['DET_BASE_DYNAMIC']; if (DET_BASE['DET_BASE_DYNAMIC']) recoverAPI('DET_BASE_DYNAMIC') if (settingTempCopy.hasOwnProperty('DET_BASE_COMMENT')) DET_BASE['DET_BASE_COMMENT'] = settingTempCopy['DET_BASE_COMMENT'] _T.resetqueue() _T.reset() _T.resetlimit(LIMIT_EVENT) confirm_success.style.display = 'unset' timerFoot2 = setTimeout(() => confirm_success.style.display = 'none', 1500) const doms = await getDom('div[xzsx_ischecked="true"],[xzsx_isdeepchecked="true"]') doms.forEach((dom) => { dom.removeAttribute('xzsx_ischecked'); dom.removeAttribute('xzsx_isdeepchecked') }) render(true) _render = _throttle((forceSync, isjump) => render(forceSync, isjump), INTERVAL, { leading: false, trailing: true })[0] batchWatch() rulesApply = deepCloneRules(rules) let temp = {} Object.keys(rulesApply.detect).forEach((key) => { temp[key] = { 'color': rulesApply.detect[key].color, 'keywords': [...rulesApply.detect[key].keywords], 'antikeywords': [...rulesApply.detect[key].antikeywords] } }) GM_setValue('xzsx_bilibili_detector_state', JSON.stringify({ MODE, DET, DEL, DEL_STYLE, TAG_MAX_LENGTH, INTERVAL, LIMIT_EVENT, DET_BASE_MedalWall: DET_BASE['DET_BASE_MedalWall'], DET_BASE_FOLLOWINGS: DET_BASE['DET_BASE_FOLLOWINGS'], DET_BASE_VIDEOS: DET_BASE['DET_BASE_VIDEOS'], DET_BASE_DYNAMIC: DET_BASE['DET_BASE_DYNAMIC'], DET_BASE_COMMENT: DET_BASE['DET_BASE_COMMENT'] })) GM_setValue('xzsx_bilibili_detector', JSON.stringify({ blackList: rulesApply.blackList, detect: temp })) timerFoot = null }, 500) } /** 页脚应用按钮松开相关操作 */ confirm_btn.onmouseup = () => { footerProcess.style.width = '0%' clearTimeout(timerFoot) timerFoot = null } /** 页脚新增规则按钮相关操作 */ add_btn.onclick = () => { ckey = undefined confirm_btn.style.display = '' add_btn.style.display = 'none' alter_btn.style.display = 'none' addRuleForm.className = 'addrule_form-hide' setTimeout(() => addRuleForm.style.display = 'none', 500) let name = tagname.value?.trim() ?? '', newKeyWords = _difference(newRuleKeysSet, deleteRuleKeysSet), newantikeywords = _difference(newAntiRuleSets, deleteAntiRuleSets) if (name.length > 0 && newKeyWords.size > 0) { rules.detect[tagname.value] = { 'color': tagcolor.value, 'keywords': newKeyWords, 'antikeywords': newantikeywords } newRuleKeysSet.clear() deleteRuleKeysSet.clear() newAntiRuleSets.clear() deleteAntiRuleSets.clear() renderMainPanel() } } /** 页脚修改规则按钮相关操作 */ alter_btn.onclick = () => { confirm_btn.style.display = '' add_btn.style.display = 'none' alter_btn.style.display = 'none' addRuleForm.className = 'addrule_form-hide' setTimeout(() => addRuleForm.style.display = 'none', 500) _difference(newRuleKeysSet, deleteRuleKeysSet).forEach((addKey) => rules.detect[ckey].keywords.add(addKey)) _difference(deleteRuleKeysSet, newRuleKeysSet).forEach((deleteKey) => rules.detect[ckey].keywords.delete(deleteKey)) _difference(newAntiRuleSets, deleteAntiRuleSets).forEach((addKey) => rules.detect[ckey].antikeywords.add(addKey)) _difference(deleteAntiRuleSets, newAntiRuleSets).forEach((deleteKey) => rules.detect[ckey].antikeywords.delete(deleteKey)) rules.detect[tagname.value] = { 'color': tagcolor.value, 'keywords': rules.detect[ckey].keywords, 'antikeywords': rules.detect[ckey].antikeywords } ckey !== tagname.value && Reflect.deleteProperty(rules.detect, ckey) ckey = undefined changedSignel1.style.display = 'none' newRuleKeysSet.clear() deleteRuleKeysSet.clear() newAntiRuleSets.clear() deleteAntiRuleSets.clear() renderMainPanel() } /** 渲染面板 * @returns { void } */ const renderMainPanel = () => { navRuleList.querySelectorAll('.rule-item').forEach((c) => c.remove()) let cdatas = ctab ? Object.keys(rules.detect) : Object.keys(rules.blackList) cdatas.forEach((key) => { let parent = document.createElement('div') parent.id = key parent.className = 'rule-item' parent.innerHTML = ` ${key} <div style="display:flex;align-items:center;height:25px;"> ${ctab ? '<button class="rule-item-btn" style="display:none;">编辑</button> ' : `<button class="rule-item-btn" style="display:none;background-color:#5d71df;border-color:#5d71df;">${rules.blackList[key]}</button> `} <button class="rule-item-btn" style="display:none;"">删除</button> </div> ` /** 构建检测成分面板 */ if (ctab) { parent.style.color = rules.detect[key].color parent.children[0].children[0].onclick = () => { ckey = key parent.children[0].children[0].style.display = 'none' parent.children[0].children[1].style.display = 'none' renderEditPanel(key) } parent.children[0].children[1].onclick = () => { parent.style.clipPath = 'polygon(115% 0, 115% 0, 100% 100%, 100% 100%)' setTimeout(() => navRuleList.removeChild(parent), 200) changedSignel1.style.display = '' Reflect.deleteProperty(rules.detect, key) rules.blackList[key] && rules.blackList[key] === '基于成分' && Reflect.deleteProperty(rules.blackList, key) blacklistAdd_select.querySelector(`option[value="${key}"]`)?.remove() } /** 构建自动屏蔽面板 */ } else { parent.children[0].children[0].onclick = () => { let del_base = parent.children[0].children[0].textContent === '基于成分' ? '基于昵称' : '基于成分' if (del_base === '基于成分' && rules.detect.hasOwnProperty(key)) { parent.children[0].children[0].textContent = del_base rules.blackList[key] = del_base changedSignel2.style.display = '' } else if (del_base === '基于昵称') { parent.children[0].children[0].textContent = del_base rules.blackList[key] = del_base changedSignel2.style.display = '' } } parent.children[0].children[1].onclick = () => { parent.style.clipPath = 'polygon(115% 0, 115% 0, 100% 100%, 100% 100%)' setTimeout(() => navRuleList.removeChild(parent), 200) changedSignel2.style.display = '' Reflect.deleteProperty(rules.blackList, key) if (blacklistAdd_base.textContent === '基于成分' && parent.children[0].children[0].textContent === '基于成分' && blacklistAdd_select.querySelector(`option[value="${key}"]`) === null) { let newOption = document.createElement('option') newOption.value = key newOption.text = key blacklistAdd_select.appendChild(newOption) } } } parent.onmouseenter = () => { parent.children[0].children[0].style.display = '' parent.children[0].children[1].style.display = '' } parent.onmouseleave = () => { parent.children[0].children[0].style.display = 'none' parent.children[0].children[1].style.display = 'none' } navRuleList.insertBefore(parent, detectAdd_btn) }) } /** 渲染新增/编辑页面 * @param { string } key 当前正在编辑的标签名 * @returns { void } */ const renderEditPanel = (key) => { keywords.querySelectorAll('.keyword').forEach((c) => c.remove()) antikeywords.querySelectorAll('.keyword').forEach((c) => c.remove()) ckey = key newKeyWord.value = '' newAntiKeyWord.value = '' if (key === undefined) { tagname.title = '' tagname.value = '' tagname.style.color = '#000000' tagcolor.value = '#000000' add_btn.style.display = '' alter_btn.style.display = 'none' } else { tagname.title = key tagname.value = key tagname.style.color = rules.detect[key].color tagcolor.value = rules.detect[key].color /** 粉丝关键词 */ rules.detect[key].keywords.forEach((keyword) => { let keywordDom = document.createElement('div') keywordDom.className = 'keyword' keywordDom.style.color = 'green' keywordDom.innerHTML = `${keyword}<div class="keyword_delete">x</div>` keywordDom.children[0].onclick = () => { changedSignel1.style.display = '' newRuleKeysSet.has(keyword) ? newRuleKeysSet.delete(keyword) : deleteRuleKeysSet.add(keyword) keywordDom.remove(keywordDom.children[0]) } keywords.insertBefore(keywordDom, keywords.firstChild) }) /** 黑子关键词 */ rules.detect[key].antikeywords.forEach((antikeyword) => { let keywordDom = document.createElement('div') keywordDom.className = 'keyword' keywordDom.style.color = 'rgb(243,28,28)' keywordDom.innerHTML = `${antikeyword}<div class="keyword_delete">x</div>` keywordDom.children[0].onclick = () => { changedSignel1.style.display = '' newAntiRuleSets.has(antikeyword) ? newAntiRuleSets.delete(antikeyword) : deleteAntiRuleSets.add(antikeyword) keywordDom.remove(keywordDom.children[0]) } antikeywords.insertBefore(keywordDom, antikeywords.firstChild) }) add_btn.style.display = 'none' alter_btn.style.display = '' } let curColor = key === undefined ? '#000000' : rules.detect[key].color tagsample.title = key?.length > 0 ? key : '标签样式预览' tagsample.textContent = key?.length > 0 ? key : '标签样式预览' tagsample.style.color = getContrastColor(curColor) tagsample.style.backgroundColor = curColor tagsample.style.borderColor = curColor + '80' tagsample.style.backgroundImage = `linear-gradient(45deg, ${curColor}, ${curColor + '80'})` confirm_btn.style.display = 'none' addRuleForm.className = 'addrule_form' addRuleForm.style.display = 'flex' } /** <===================================================================================功能实现==========================================================================================> */ /** 用于去重的UID集合 */ const uidset = new Set(), url = window.location.href /** 标签缓存 * @type { { [ key: number | string ]: { [key: string]: { fan: { isFans: boolean, sources: { [key: string]: Set<string> } }, anti: { isAnti: boolean, sources: { [key: string]: Set<string> } } } } }} */ let userTags = {}, userTagsHash = {}, userBlackList = new Set() /** 对部分特殊URL进行映射 * @returns { void } */ const domainTransition = () => { switch (true) { case url.match(/^https?:\/\/www.bilibili.com\/festival.*$/) !== null: return 'www.bilibili.com/festival/' case url.match(/^https?:\/\/www.bilibili.com\/bangumi.*$/) !== null: return 'www.bilibili.com/bangumi/' case url.match(/^https?:\/\/www.bilibili.com\/read.*$/) !== null: return 'www.bilibili.com/read/' case url.match(/^https?:\/\/www.bilibili.com\/blackboard.*$/) !== null: return 'www.bilibili.com/blackboard/' case url.match(/^https?:\/\/www.bilibili.com\/v\/topic\/.*$/) !== null: return 'www.bilibili.com/v/topic/' case url.match(/^https?:\/\/t.bilibili.com\/\?spm_id_from=.*$/) !== null: return 'space.bilibili.com' default: return url.match(/^https?:\/\/(.*?)\/.*$/)[1] } } /** 域名 * @type { string } */ const domain = domainTransition() /** UserAgent生成器 */ class UserAgentGenerator { constructor() { this._part1 = [ { name: "Mozilla/5.0", children: [] } ] this._part2 = [ { name: "Windows NT ${11}.${10}", children: [ { name: 'Win64; x64', children: [ { name: "rv:${54}.${10}", children: [] }, { name: "Trident/${8}.${10}", children: [] }, ] }, { name: '', children: [ { name: "rv:${54}.${10}", children: [] }, { name: "Trident/${8}.${10}", children: [] }, ] } ] }, { name: 'Macintosh', children: [ { name: "Intel Mac OS X ${11}_${13}_${6}", children: [ { name: "rv:${54}.${10}", children: [] }, ] } ] }, { name: 'X11', children: [ { name: 'Ubuntu', children: [ { name: "rv:${54}.${10}", children: [] }, ] }, { name: 'Linux x86_64', children: [ { name: "rv:${54}.${10}", children: [] }, ] } ] }, ] this._part3 = [ { name: "Gecko/20100101", children: [] }, { name: "AppleWebKit/5${38}.36", children: [] }, { name: "", children: [] }, ] this._part4 = [ { name: "(KHTML, like Gecko)", children: [] }, { name: "", children: [] }, ] this._part5 = [ { name: "(KHTML, like Gecko)", children: [] }, { name: "", children: [] }, ] this._part6 = [ "Firefox/${54}.${10}", "Chrome/${105}.${10}.${10}.${10}", "Safari/${538}.${37}", "OPR/${46}.0.2552.${889}", 'Ubuntu', "Version/${11}.${10}.${10}", "Chromium/${59}.0.3029.${111}", "Mobile/14F89", "(iPad; U; CPU OS 3_2 like Mac OS X; en-us)", ] } generate = () => this._dfs(this._part1, '') + this._dfs(this._part2, '') + this._dfs(this._part3, '') + this._dfs(this._part4, '') + this._dfs(this._part5, '') + this._dfs2(this._part6, [], new Set(), Math.floor(Math.random() * this._part6.length)) _dfs = (arr, combine) => { if (arr.length === 0) return combine const randomIndex = Math.floor(Math.random() * arr.length) let str = JSON.parse(JSON.stringify(arr[randomIndex].name)), matches = str.match(/\${(\d+)}/) while (matches !== null) { str = str.replace(/\${(\d+)}/, Math.floor(Math.random() * parseInt(matches[1]))) matches = str.match(/\${(\d+)}/) } combine += str + (str.length > 0 ? '; ' : '') return this._dfs(arr[randomIndex].children, combine) } _dfs2 = (arr, combine, vis, total) => { if (combine.length === total) return combine.join('; ') let randomIndex = Math.floor(Math.random() * arr.length) while (vis.has(randomIndex)) randomIndex = Math.floor(Math.random() * arr.length) vis.add(randomIndex) let str = JSON.parse(JSON.stringify(arr[randomIndex])), matches = str.match(/\${(\d+)}/) while (matches !== null) { str = str.replace(/\${(\d+)}/, Math.floor(Math.random() * parseInt(matches[1]))) matches = str.match(/\${(\d+)}/) } combine.push(str) return this._dfs2(arr, combine, vis, total) } } /** UserAgent生成器 */ const Generator = new UserAgentGenerator() /** 获取网络请求参数 * @param { string } name API名 * @param { string } uid 用户UID * @param { number } pn API检索页数 * @param { string } offset 主键偏移 * @returns { object } 请求参数对象 */ const getRequestParams = (name, uid, pn, offset = '') => { const randomUID = Math.floor(Math.random() * Math.pow(2, 29)), userAgent = Generator.generate() switch (name) { /** B站粉丝牌API */ case 'Medal': return { method: "get", url: `https://api.live.bilibili.com/xlive/web-ucenter/user/MedalWall?target_id=${uid}`, headers: { 'user-agent': userAgent, 'referer': 'https://live.bilibili.com/' } } /** B站关注列表API */ case 'SubList': return { method: "get", url: `https://api.bilibili.com/x/relation/followings?vmid=${uid}&pn=${pn}&ps=50&order=desc&jsonp=jsonp`, headers: { 'user-agent': userAgent, 'referer': `https://space.bilibili.com/${randomUID}/fans/follow` } } /** B站投稿列表API */ case 'Video': return { method: "get", url: `https://api.bilibili.com/x/space/arc/search?mid=${uid}&pn=${pn}&ps=50&jsonp=jsonp`, headers: { 'user-agent': userAgent, 'referer': `https://space.bilibili.com/${randomUID}/video` } } /** B站用户动态API */ case 'Dynamic': return { method: "get", url: `https://api.bilibili.com/x/polymer/web-dynamic/v1/feed/space?&host_mid=${uid}&offset=${offset}`, headers: { 'user-agent': userAgent, 'referer': `https://space.bilibili.com/${randomUID}/dynamic` } } default: return '' } } /** 封装GM_xmlhttpRequest为Promise方便进行同步操作 * @param { string } params GM_xmlhttpRequest 参数列表 * @returns { Promise<object> } */ const GM_Request = (params) => { return new Promise(resolve => { GM_xmlhttpRequest({ ...params, onload: res => res.status === 200 ? resolve(JSON.parse(res.response)) : resolve({ 'code': res.status }), onerror: error => resolve({ 'code': error.code, 'message': error }) }) }) } /** 判断新旧版本 * @type { boolean } */ let version = true, Cookie = document.cookie, goOldVideo = Cookie.match(/(?<=go_old_video=)[-\d]{1,2}/) if (Cookie && goOldVideo) version = goOldVideo[0] === '-1' /** 新旧版本-评论根节点class映射 * @type { { [ key: string ]: { true: { mainList: string, subList: string }, false: { mainList: string, subList: string } } } } */ const versionMap = { 'www.bilibili.com': { true: { 'mainList': '.reply-list', 'subList': '.sub-reply-list' }, false: { 'mainList': '.comment-list ', 'subList': '.reply-box' } }, 'space.bilibili.com': { true: { 'mainList': '.bili-dyn-list__items', 'subList': '.bili-dyn-item' }, false: { 'mainList': '.bili-dyn-list__items', 'subList': '.bili-dyn-item' } }, 't.bilibili.com': { true: { 'mainList': '.comment-list ', 'subList': '.reply-box' } , false: { 'mainList': '.comment-list ', 'subList': '.reply-box' } }, 'live.bilibili.com': { true: { 'mainList': '.chat-items', 'subList': '.reply-box' }, false: { 'mainList': '.chat-items', 'subList': '.reply-box' } }, 'www.bilibili.com/festival/': { true: { 'mainList': '.reply-list,.chat-items', 'subList': '.sub-reply-list,.reply-box' }, false: { 'mainList': '.comment-list ,.chat-items', 'subList': '.reply-box' } }, 'www.bilibili.com/bangumi/': { true: { 'mainList': '.comment-list ', 'subList': '.reply-box' }, false: { 'mainList': '.comment-list ', 'subList': '.reply-box' } }, 'www.bilibili.com/read/': { true: { 'mainList': '.comment-list ', 'subList': '.reply-box' }, false: { 'mainList': '.comment-list ', 'subList': '.reply-box' } }, 'www.bilibili.com/blackboard/': { true: { 'mainList': '.comment-list ', 'subList': '.reply-box' }, false: { 'mainList': '.comment-list ', 'subList': '.reply-box' } }, 'www.bilibili.com/v/topic/': { true: { 'mainList': '.comment-list ', 'subList': '.reply-box' }, false: { 'mainList': '.comment-list ', 'subList': '.reply-box' } }, } /** 用于屏蔽的Class映射表 @视频评论 * @type { { [ key: string ]: Array<string> } } */ const videoClassMap = { 'user': ['user', 'reply-item reply-wrap', '.text'], 'user-name': ['.user-info', '.reply-list > .reply-item', '.reply-content,.root-reply'], 'sub-user-name': ['.sub-user-info', '.sub-reply-list > .sub-reply-item', '.reply-content,.sub-reply-content'], } /** Class映射表 @动态评论 * @type { { [ key: string ]: Array<string> } } */ const dynamicClassMap = { 'user': ['.level-link', '.text text-con reply-item reply-wrap', '.text text-con'], } /** Class映射表 @直播评论 * @type { { [ key: string ]: Array<string> } } */ const liveClassMap = { 'chat-item danmaku-item ': ['.danmaku-item-left', '.chat-item danmaku-item ', '.danmaku-item-right'], 'chat-item danmaku-item chat-colorful-bubble': ['.danmaku-item-left', '.chat-item danmaku-item ', '.danmaku-item-right'], 'chat-item danmaku-item chat-colorful-bubble chat-emoticon bulge-emoticon': ['.danmaku-item-left', '.chat-item danmaku-item ', '.danmaku-item-right'], 'chat-item danmaku-item chat-emoticon': ['.danmaku-item-left', '.chat-item danmaku-item ', '.danmaku-item-right'], 'chat-item danmaku-item chat-emoticon bulge-emoticon': ['.danmaku-item-left', '.chat-item danmaku-item ', '.danmaku-item-right'], } /** 域名Class映射表 * @type { { [ key: string ]: { userDomClass: string, classMap: object } } } */ const domainMap = { 'www.bilibili.com': { 'userDomClass': '.user,.user-name,.sub-user-name', 'classMap': videoClassMap }, 'space.bilibili.com': { 'userDomClass': '.user', 'classMap': dynamicClassMap }, 't.bilibili.com': { 'userDomClass': '.user,.user-name,.sub-user-name', 'classMap': dynamicClassMap }, 'live.bilibili.com': { 'userDomClass': '.chat-item,.danmaku-item', 'classMap': liveClassMap }, 'www.bilibili.com/festival/': { 'userDomClass': '.user,.user-name,.sub-user-name,.chat-item,.danmaku-item', 'classMap': { ...videoClassMap, ...liveClassMap } }, 'www.bilibili.com/bangumi/': { 'userDomClass': '.user,.user-name,.sub-user-name,.chat-item,.danmaku-item', 'classMap': { ...videoClassMap, ...liveClassMap } }, 'www.bilibili.com/read/': { 'userDomClass': '.user,.user-name,.sub-user-name,.chat-item,.danmaku-item', 'classMap': { ...videoClassMap, ...liveClassMap } }, 'www.bilibili.com/blackboard/': { 'userDomClass': '.user,.user-name,.sub-user-name,.chat-item,.danmaku-item', 'classMap': { ...videoClassMap, ...liveClassMap } }, 'www.bilibili.com/v/topic/': { 'userDomClass': '.user,.user-name,.sub-user-name,.chat-item,.danmaku-item', 'classMap': { ...videoClassMap, ...liveClassMap } }, } /** 获取UID * @param { Element } v 用户DOM元素 * @returns { string } 返回UID字符串 */ const getUID = (v) => v?.dataset?.uid || v?.children[0]?.dataset['usercardMid'] || v?.children[0]?.href?.replace(/[^\d]/g, "") || v.dataset['userId'] /** 获取用户昵称 * @param { Element } v 用户DOM元素 * @returns { string } 返回昵称字符串 */ const getUname = (v) => ( { 'live.bilibili.com': v?.dataset?.uname, 'space.bilibili.com': v?.firstChild?.textContent, 'www.bilibili.com': version ? v?.textContent : v?.firstChild?.textContent, 't.bilibili.com': v?.firstChild?.textContent }[domain] ) /** 轮询获取DOM元素 * @param { string } className 类名 * @param { number } timeout 轮询周期(毫秒), 默认值为1000 * @param { boolean } isDelay 是否立刻执行一次 * @returns { Promise<Array<Element>> } 返回DOM数组 */ const getDom = (className, timeout = 1000, isDelay = false) => { let DOMS = [], timer = undefined let fn = (resolve) => { return () => { if (ciframe === undefined) { ciframe = getCurIframe() /** 若页面存在iframe, 则将成分标签相关样式css样式添加到iframe内 */ if (!init && ciframe !== undefined) { init = true const innerIframe_style = document.createElement('style') innerIframe_style.innerHTML = css_tag ciframe.contentWindow.document.querySelector('head').appendChild(innerIframe_style) } } DOMS = (domain === 'live.bilibili.com' ? ciframe !== undefined ? ciframe.contentWindow.document : document : document).querySelectorAll(className) if (DOMS.length > 0) { clearInterval(timer) resolve(DOMS) } } } return new Promise(resolve => { !isDelay && fn(resolve)() timer = setInterval(fn(resolve), timeout) }) } /** 获取反差强烈的另外一种颜色 * @param { string } oldColor 十六进制字符串 * @returns { string } 返回反差强烈的另外一种颜色的十六进制字符串 */ const getContrastColor = (oldColor) => (('0x' + oldColor.slice(1, 3)) * 299 + ('0x' + oldColor.slice(3, 5)) * 587 + ('0x' + oldColor.slice(5, 7)) * 114) / 1000 >= 128 ? 'black' : 'white' /** 获取待个人信息字符串并检测成分 * @param { string } uid 用户UID * @param { Element | null } dom 用户评论元素 * @param { boolean } isdeep 是否需要深度收集, 默认为否 * @param { boolean } forceSync 是否强制刷新 * @returns { Promise<void> } 返回个人信息字符串 */ const handleStr = async (uid, dom, isdeep = false, forceSync = false) => { /** 并发进行网络请求及成分检测 */ await Promise.all( [ /** 获取评论/弹幕文本 */ new Promise(resolve => { if (DET_BASE['DET_BASE_COMMENT']) { let textStr = ' ' switch (true) { case domain === 'space.bilibili.com' || domain === 't.bilibili.com': textStr = (dom.closest('.text text-con') || dom.querySelector('.text-con') || dom.parentNode.querySelector('.text')).textContent break default: textStr = (domain === 'live.bilibili.com' ? dom : version ? (dom.closest(domainMap[domain].classMap[dom.className][1]) || dom || dom.parentNode) : dom.parentNode.className !== 'reply-con' ? dom.parentNode : dom.parentNode.parentNode)?.querySelector(dom.parentNode.className === 'reply-con' ? '.text-con' : domainMap[domain].classMap[dom.className][2])?.textContent break } if (textStr?.trim().length > 0) dealRes(uid, '『评论内容』' + textStr, '评论', forceSync, resolve); else resolve() } else resolve() }), /** 获取用户全部粉丝牌列表 */ new Promise(async (resolve) => { if (DET_BASE['DET_BASE_MedalWall'] && !API_OVERLOAD['DET_BASE_MedalWall'].state) { let medalStr = ' ' const medalList = await GM_Request(getRequestParams('Medal', uid, 1)) if (medalList?.code === 0 && medalList?.data?.count > 0) { medalList?.data?.list.forEach((v) => { let /** 粉丝牌正主名称 */ target_name = (v?.target_name ?? ''), /** 粉丝牌名称 */ medal_name = (v?.medal_info.medal_name ?? ''), /** 粉丝牌等级 */ level = (v?.medal_info.level ?? '') + '' medalStr += (target_name.length > 0 ? `『${level.length > 0 ? `${level}级` : ''}粉丝牌->正主』` : '') + target_name + '∏' + (medal_name.length > 0 ? `『${level.length > 0 ? `${level}级` : ''}粉丝牌->名称』` : '') + medal_name + '∏' userTags[uid]['MedalWall'] = medalList?.data?.list }) } else if (medalList?.code === 412) banAPI('DET_BASE_MedalWall', status_MedalWall) if (medalStr.trim().length > 0) dealRes(uid, medalStr, '粉丝牌', forceSync, resolve); else resolve() } else resolve() }), /** 获取用户前(50n)个投稿列表 */ new Promise(async (resolve) => { if (DET_BASE['DET_BASE_VIDEOS'] && !API_OVERLOAD['DET_BASE_VIDEOS'].state) { let videoStr = ' ' for (let pn = 1, count = 0; pn <= (isdeep ? 2 : 1); pn++) { const videoList = await GM_Request(getRequestParams('Video', uid, pn)) if (videoList?.code === 0 && videoList?.data?.list?.vlist.length > 0) { videoList?.data?.list?.vlist.forEach((v) => { let /** 投稿标题 */ title = (v?.title ?? ''), /** 投稿描述 */ description = (v?.description ?? '') videoStr += (title.length > 0 ? '『投稿标题』' : '') + title + '∏' + (description.length > 0 ? '『投稿描述』' : '') + description + '∏' }) count += 50 if (count >= videoList?.data?.page?.count) break } else if (videoList?.code === 412) { banAPI('DET_BASE_VIDEOS', status_VIDEOS) break } } if (videoStr.trim().length > 0) dealRes(uid, videoStr, '投稿列表', forceSync, resolve); else resolve() } else resolve() }), /** 获取用户前(50n)个关注列表 */ new Promise(async (resolve) => { if (DET_BASE['DET_BASE_FOLLOWINGS'] && !API_OVERLOAD['DET_BASE_FOLLOWINGS'].state) { let subStr = ' ' for (let pn = 1, count = 0; pn <= (isdeep ? 5 : 1); pn++) { const subList = await GM_Request(getRequestParams('SubList', uid, pn)) if (subList?.code === 0 && subList?.data?.list?.length > 0) { subList?.data?.list.forEach((v) => { /** 关注用户的名称 */ let uname = (v?.uname ?? '') subStr += (uname.length > 0 ? '『关注用户』' : '') + uname + '∏' }) count += 50 if (count >= subList?.data?.total) break } else if (subList?.code === 412) { banAPI('DET_BASE_FOLLOWINGS', status_FOLLOWINGS) break } } if (subStr.trim().length > 0) dealRes(uid, subStr, '关注列表', forceSync, resolve); else resolve() } else resolve() }), /** 获取用户前n页动态列表 */ new Promise(async (resolve) => { if (DET_BASE['DET_BASE_DYNAMIC'] && !API_OVERLOAD['DET_BASE_DYNAMIC'].state) { let dynamicStr = '', offset = '' for (let pn = 1; pn <= (isdeep ? 5 : 1); pn++) { const dynamicList = await GM_Request(getRequestParams('Dynamic', uid, pn, offset)) if (dynamicList?.code === 0 && dynamicList?.data?.items?.length > 0) { offset = dynamicList?.data?.offset dynamicList?.data?.items.forEach((v) => { let /** 装扮名称 */ decorate_name = (v?.modules?.module_author?.decorate?.name ?? ''), /** 头像装扮名称 */ pendant_name = (v?.modules?.module_author?.pendant?.name ?? ''), /** 转发动态的作者名称 */ module_author = (v?.orig?.modules?.module_author?.name ?? ''), /** 动态文本内容 */ text = (v?.modules?.module_dynamic?.desc?.text ?? '') dynamicStr += (decorate_name.length > 0 ? '『装扮』' : '') + decorate_name + '∏' + (pendant_name.length > 0 ? '『头像装扮』' : '') + pendant_name + '∏' + (module_author.length > 0 ? '『转发作者』' : '') + module_author + '∏' + (text.length > 0 ? '『动态内容』' : '') + text + '∏' }) if (!dynamicList?.data?.has_more) break } else if (dynamicList?.code === 412) { banAPI('DET_BASE_DYNAMIC', status_DYNAMIC) break } } if (dynamicStr.trim().length > 0) dealRes(uid, dynamicStr, '动态列表', forceSync, resolve); else resolve() } else resolve() }), ] ).then(() => { if (DEL && userBlackList.has(uid)) block(dom) if (DET && uidset.has(uid)) { appendTags(uid, dom, userTags[uid], forceSync) for (const tagname of Object.keys(userTags[uid])) userTagsHash[uid]['filter'].add(tagname) } }) } /** 普通/深度检测 * @param { string } uid 用户UID * @param { Element | null } dom 用户评论元素 * @param { boolean } isdeep 是否需要深度收集, 默认为否 * @returns { Promise<void> } */ const check = async (uid, dom, isdeep = false) => { /** 获取旧标签容器 */ let container = (domain === 'live.bilibili.com' ? dom : version ? dom.closest(domainMap[domain].classMap[dom.className][1]) : dom.parentNode.className !== 'reply-con' ? dom.parentNode : dom.parentNode.parentNode).querySelector('.tag_container') container.innerHTML = '<img src="data:image/svg+xml;base64,PCEtLSBCeSBTYW0gSGVyYmVydCAoQHNoZXJiKSwgZm9yIGV2ZXJ5b25lLiBNb3JlIEAgaHR0cDovL2dvby5nbC83QUp6YkwgLS0+DQo8c3ZnIHdpZHRoPSIzOCIgaGVpZ2h0PSIzOCIgdmlld0JveD0iMCAwIDQwIDQwIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPg0KICAgIDxkZWZzPg0KICAgICAgICA8bGluZWFyR3JhZGllbnQgeDE9IjguMDQyJSIgeTE9IjAlIiB4Mj0iNjUuNjgyJSIgeTI9IjIzLjg2NSUiIGlkPSJhIj4NCiAgICAgICAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMyMjIiIHN0b3Atb3BhY2l0eT0iMCIgb2Zmc2V0PSIwJSIvPg0KICAgICAgICAgICAgPHN0b3Agc3RvcC1jb2xvcj0iIzIyMiIgc3RvcC1vcGFjaXR5PSIuNjMxIiBvZmZzZXQ9IjYzLjE0NiUiLz4NCiAgICAgICAgICAgIDxzdG9wIHN0b3AtY29sb3I9IiMyMjIiIG9mZnNldD0iMTAwJSIvPg0KICAgICAgICA8L2xpbmVhckdyYWRpZW50Pg0KICAgIDwvZGVmcz4NCiAgICA8ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPg0KICAgICAgICA8ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgxIDEpIj4NCiAgICAgICAgICAgIDxwYXRoIGQ9Ik0zNiAxOGMwLTkuOTQtOC4wNi0xOC0xOC0xOCIgaWQ9Ik92YWwtMiIgc3Ryb2tlPSJ1cmwoI2EpIiBzdHJva2Utd2lkdGg9IjQiPg0KICAgICAgICAgICAgICAgIDxhbmltYXRlVHJhbnNmb3JtDQogICAgICAgICAgICAgICAgICAgIGF0dHJpYnV0ZU5hbWU9InRyYW5zZm9ybSINCiAgICAgICAgICAgICAgICAgICAgdHlwZT0icm90YXRlIg0KICAgICAgICAgICAgICAgICAgICBmcm9tPSIwIDE4IDE4Ig0KICAgICAgICAgICAgICAgICAgICB0bz0iMzYwIDE4IDE4Ig0KICAgICAgICAgICAgICAgICAgICBkdXI9IjAuOXMiDQogICAgICAgICAgICAgICAgICAgIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIiAvPg0KICAgICAgICAgICAgPC9wYXRoPg0KICAgICAgICAgICAgPGNpcmNsZSBmaWxsPSIjMjIyIiBjeD0iMzYiIGN5PSIxOCIgcj0iMSI+DQogICAgICAgICAgICAgICAgPGFuaW1hdGVUcmFuc2Zvcm0NCiAgICAgICAgICAgICAgICAgICAgYXR0cmlidXRlTmFtZT0idHJhbnNmb3JtIg0KICAgICAgICAgICAgICAgICAgICB0eXBlPSJyb3RhdGUiDQogICAgICAgICAgICAgICAgICAgIGZyb209IjAgMTggMTgiDQogICAgICAgICAgICAgICAgICAgIHRvPSIzNjAgMTggMTgiDQogICAgICAgICAgICAgICAgICAgIGR1cj0iMC45cyINCiAgICAgICAgICAgICAgICAgICAgcmVwZWF0Q291bnQ9ImluZGVmaW5pdGUiIC8+DQogICAgICAgICAgICA8L2NpcmNsZT4NCiAgICAgICAgPC9nPg0KICAgIDwvZz4NCjwvc3ZnPg0K" style="height:18px;width:18px;"></img>' medalWall.className = 'xzsx_detect_tag-medalwall-hide' medalWall.setAttribute('key', null) if (userTagsHash[uid]['isdeepchecked']) { container.querySelector('img').remove() appendTags(uid, dom, userTags[uid]) return } if (isdeep) { /** 添加已深度检测状态 */ dom.setAttribute('xzsx_isdeepchecked', true) userTagsHash[uid]['isdeepchecked'] = true } /** 检测成分 */ await handleStr(uid, dom, isdeep, true) container.querySelector('img').remove() isdeep && container.querySelector('.icon-deepcheck').remove() } /** 数字转十六进制字符串 * @param { number } num 源字符串 * @returns { string } */ const intToHexString = (num) => { let hexStr = num.toString(16) for (let i = hexStr.length; i < 6; i++) hexStr = '0' + hexStr return hexStr } /** 高亮关键词 * @param { string } str 源字符串 * @param { string } targetStr 目标字符串 * @returns { string } */ const highLight = (str, targetStr) => str.split(targetStr).join(`<span style="background-color:yellow;color:black;">${targetStr}</span>`) /** 瀑布流 * @param { HTMLElement } container 瀑布流布局容器 * @param { HTMLElement[] } doms HTML元素集合 * @param { number } gap 元素间隙 * @param { 'Horizontal' | 'Vertical' } direction 方向 * @return { [ number, number ] } 最大高宽 */ const waterfall = (container, doms, gap, direction) => { const curAttr = { 'Horizontal': ['height', 'width', 'left', 'top'], 'Vertical': ['width', 'height', 'top', 'left'] }[direction], csize = parseInt(container.style[curAttr[0]]) if (typeof (csize) !== 'number' || csize <= 0) return 0 const size = parseInt(getComputedStyle(doms[0])[curAttr[0]]), num = parseInt(csize / (size + gap)) let pos = [], lowest = -1 for (let i = 0; i < doms.length; i++) { let computedStyle = getComputedStyle(doms[i]) doms[i].style.position = 'absolute' if (i < num) { pos[i] = parseInt(computedStyle[curAttr[1]]) + gap doms[i].style[curAttr[2]] = gap + 'px' doms[i].style[curAttr[3]] = gap * (i % num + 1) + size * (i % num) + 'px' } else { lowest = pos.indexOf(Math.min(...pos)) doms[i].style[curAttr[2]] = pos[lowest] + gap + 'px' doms[i].style[curAttr[3]] = gap * (lowest % num + 1) + size * (lowest % num) + 'px' pos[lowest] = pos[lowest] + parseInt(computedStyle[curAttr[1]]) + gap } } return [pos.length * (size + gap) + gap, pos[pos.indexOf(Math.max(...pos))] + gap] } /** 插入标签 * @param { string } uid 用户UID * @param { Element | null } dom 用户评论元素 * @param { object } tags 用户成分名集合 * @param { boolean } forceSync 是否强制刷新, 默认为否 * @returns { void } */ const appendTags = (uid, dom, tags, forceSync = false) => { let fansTags = [], antiTags = [], iframePos = ciframe?.getBoundingClientRect() try { Object.keys(tags).forEach(tagname => { if (tagname !== 'MedalWall') { tags[tagname].fan.isFans && fansTags.push(tagname) tags[tagname].anti.isAnti && antiTags.push(tagname) } }) /** 生成标签DOM */ let Tags = [], length = 0, cur = (version && domain !== 'www.bilibili.com' && domain !== 'www.bilibili.com/bangumi/' && domain !== 'www.bilibili.com/read/'&& domain !== 'www.bilibili.com/blackboard/'&& domain !== 'www.bilibili.com/v/topic/') ? (dom.querySelector(domainMap[domain].classMap[dom.className][0]) || dom.closest(domainMap[domain].classMap[dom.className][0])) : dom, tag_container = cur.querySelector('.tag_container'), existTags = new Set(), container = tag_container !== null ? tag_container : document.createElement('div') container.querySelectorAll('.xzsx_detect_tag').forEach(ele => existTags.add(ele.textContent)) fansTags.forEach((tagname) => { if (!existTags.has(tagname)) { length += tagname.replace(/\p{sc=Han}/gu, '**').length let ele = document.createElement('div') ele.className = 'xzsx_detect_tag' ele.style.display = length <= TAG_MAX_LENGTH ? 'inline-flex' : 'none' ele.style.color = getContrastColor(rulesApply.detect[tagname].color) ele.style.borderColor = rulesApply.detect[tagname].color + '80' ele.style.backgroundImage = `-o-linear-gradient(45deg, ${rulesApply.detect[tagname].color}, ${rulesApply.detect[tagname].color + '80'})` ele.style.backgroundImage = `-moz-linear-gradient(45deg, ${rulesApply.detect[tagname].color}, ${rulesApply.detect[tagname].color + '80'})` ele.style.backgroundImage = `-webkit-linear-gradient(45deg, ${rulesApply.detect[tagname].color}, ${rulesApply.detect[tagname].color + '80'})` ele.style.backgroundImage = `linear-gradient(45deg, ${rulesApply.detect[tagname].color}, ${rulesApply.detect[tagname].color + '80'})` ele.textContent = (tags[tagname].anti.isAnti ? '🐵' : '') + tagname /** 点击成分标签展开溯源面板 */ ele.onclick = (e) => { e.stopPropagation() medalWall.setAttribute('key', null) medalWall.className = 'xzsx_detect_tag-medalwall-hide' trackPanel.className = 'xzsx_detect_tag-trackpanel-hide' trackPanel.innerHTML = ` <div title='${getUname(dom)} / ${tagname}' class='xzsx_detect_tag-trackpanel-title'> ${getUname(dom)} / ${tagname} </div> <div class='xzsx_detect_tag-trackpanel-content'> ${tags[tagname].anti.isAnti ? ` <div class='xzsx_detect_tag-trackpanel-anti'> <u class='xzsx_detect_tag-trackpanel-texttitle'>黑子关键词溯源</u> ${Object.keys(tags[tagname].anti.sources).map((src) => { return tags[tagname].anti.sources[src].size > 0 ? ` <div> <div class="xzsx_detect_tag-trackpanel-source">${src}</div> ${Array.from(tags[tagname].anti.sources[src]).map((text) => { let texts = text.split('Γ') return `<div class="xzsx_detect_tag-trackpanel-text">${highLight(texts[1], texts[0])}</div>` }).join('')} </div> ` : ''}).join('')} </div> ` : '' } <div class='xzsx_detect_tag-trackpanel-fans'> <u class='xzsx_detect_tag-trackpanel-texttitle'>粉丝关键词溯源</u> ${Object.keys(tags[tagname].fan.sources).map((src) => { return tags[tagname].fan.sources[src].size > 0 ? ` <div> <div class="xzsx_detect_tag-trackpanel-source">${src}</div> ${Array.from(tags[tagname].fan.sources[src]).map((text) => { let texts = text.split('Γ') return `<div class="xzsx_detect_tag-trackpanel-text">${highLight(texts[1], texts[0])}</div>` }).join('')} </div> ` : ''}).join('')} </div> </div> ` let title = trackPanel.querySelector('.xzsx_detect_tag-trackpanel-title') title.onclick = e => { e.stopPropagation(); ele.scrollIntoView({ block: "center", inline: "center" }) } title.onmouseenter = e => e.target.style.opacity = 0.8 title.onmouseleave = e => e.target.style.opacity = 1 trackPanel.style.display = 'flex' let pos = e.target.getBoundingClientRect() if (domain === 'live.bilibili.com') { trackPanel.style.position = 'fixed' trackPanel.style.right = '24px' trackPanel.style.top = '24px' } else { trackPanel.style.position = 'absolute' trackPanel.style.left = (iframePos?.left ?? 0) + (pos.left + 260 > document.documentElement.clientWidth ? document.documentElement.clientWidth - 300 : pos.left + 10) + 'px' trackPanel.style.top = (iframePos?.top ?? 0) + document.scrollingElement.scrollTop + (pos.top + 220 > document.documentElement.clientHeight ? document.documentElement.clientHeight - 250 : pos.top + 20) + 'px' } trackPanel.className = 'xzsx_detect_tag-trackpanel-show' } Tags.push(ele) } }) let icon = document.createElement('div'), checkBtn = document.createElement('div'), exist_btn = container.querySelector('div.icon-expend') === null if (exist_btn && length > TAG_MAX_LENGTH) { icon.className = 'icon-expend' icon.innerHTML = ` <svg t="1636097794549" class="svg-expend" viewBox="0 0 #### ####" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5713" > <path d="M586.624 234.624a74.624 74.624 0 1 1-149.184 0 74.624 74.624 0 0 1 149.12 0z m0 554.624a74.624 74.624 0 1 1-149.248 0 74.624 74.624 0 0 1 149.248 0zM512 586.624a74.624 74.624 0 1 0 0-149.248 74.624 74.624 0 0 0 0 149.248z" p-id="5714" fill="#9499a0"></path> </svg> ` icon.onclick = e => { e.stopPropagation() icon.style.display = 'none' container.querySelectorAll('div.xzsx_detect_tag[style*="display: none;"]').forEach(tag => tag.style.display = 'inline-flex') } } /** 构造标签容器 */ container.className = 'tag_container' /** 构造主动检测按钮 */ checkBtn.className = 'icon-deepcheck' checkBtn.innerHTML = ` <div class="icon-deepcheck-hide" > <svg class="svg-deepcehck" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> <path d="M16.32 14.9l5.39 5.4a1 1 0 0 1-1.42 1.4l-5.38-5.38a8 8 0 1 1 1.41-1.41zM10 16a6 6 0 1 0 0-12 6 6 0 0 0 0 12z"></path> </svg> </div> <div style="position: absolute;"> <svg class='check-container'> <circle class='check-circle' cx="9" cy="9" r="8" /> </svg> </div> ` checkBtn.onclick = () => check(uid, dom, false) const [_ddeep, clearDeepTimer] = _debounce(() => { checkBtn.children[0].className = 'icon-deepcheck-hide'; check(uid, dom, true) }, 1000) checkBtn.onmousedown = () => { checkBtn.children[0].style.transform = 'scale(0.55)'; checkBtn.children[1].children[0].children[0].style.strokeDashoffset = 0; _ddeep() } checkBtn.onmouseup = () => { checkBtn.children[0].style.transform = 'scale(1)'; checkBtn.children[1].children[0].children[0].style.strokeDashoffset = 50.3; clearDeepTimer() } const [_dshow, clearshowTimer] = _debounce(() => checkBtn.style.display = 'inline-flex', 500) const [_dhide, clearhideTimer] = _debounce(() => checkBtn.style.display = 'none', 1000) container.onmouseenter = () => { clearhideTimer(); _dshow() } container.onmouseleave = () => { clearshowTimer(); _dhide() } dom.onmouseenter = () => { clearhideTimer(); _dshow() } dom.onmouseleave = () => { clearshowTimer(); _dhide() } /** 勋章墙 */ let container2 = document.createElement('div'), medalBtn = null, medalList = [] if (tags['MedalWall'] !== undefined) { tags['MedalWall'].sort((a, b) => b.medal_info.level - a.medal_info.level).forEach((info, i) => { let medal = document.createElement('div') medal.title = info.target_name medal.onclick = () => window.open(info.link) medal.style.cssText = ` display: inline-flex; justify-content: center; align-items: center; -webkit-box-sizing: content-box; box-sizing: content-box; width: fit-content; height: 16px; line-height: 16px; color: #fff; border: 1px solid transparent; border-color: #${intToHexString(info.medal_info.medal_color_border)}; white-space: nowrap; border-radius: 2px; font-family: "Microsoft YaHei", "Microsoft Sans Serif", "Microsoft SanSerf", "微软雅黑"; font-size: 10px; position: relative; cursor: pointer; z-index: 1; ` let medal_name = info.medal_info.medal_name, target_name = info.target_name Object.keys(rules.detect).forEach(tagname => { rules.detect[tagname].keywords.forEach(key => { medal_name = highLight(medal_name, key); target_name = highLight(target_name, key) }); rules.detect[tagname].antikeywords.forEach(antikey => medal_name = highLight(medal_name, antikey)) }) medal.innerHTML = ` <div> <span>${medal_name}</span> </div> <div> <span style="margin-left:4px;margin-right:4px;">${target_name}</span> </div> <div style="color:#${intToHexString(info.medal_info.medal_color_start)};width: 16px; text-align: center; border-top-left-radius: 1px; border-bottom-right-radius: 1px;background-color: white;box-shadow: 0px 0px 4px #ddd;">${info.medal_info.level}</div> ` medal.children[0].style.cssText = ` display: -webkit-box; display: -ms-flexbox; display: flex; -webkit-box-pack: center; -ms-flex-pack: center; justify-content: center; -webkit-box-align: center; -ms-flex-align: center; align-items: center; min-width: 12px; text-align: center; padding: 0 4px; color: #fff; border-top-left-radius: 1px; border-bottom-left-radius: 1px; background-image: linear-gradient(45deg, #${intToHexString(info.medal_info.medal_color_start)}, #${intToHexString(info.medal_info.medal_color_end)}); box-shadow: 0px 0px 4px #ddd; ` medal.children[1].style.cssText = ` color:black; box-shadow: 2px 2px 0px #ddd inset; ` medal.onmouseenter = e => e.target.style.opacity = 0.8 medal.onmouseleave = e => e.target.style.opacity = 1 medalList.push(medal) if (i === 0) { let clone = medal.cloneNode(true) clone.children[0].children[0].textContent = info.medal_info.medal_name clone.children[1].children[0].textContent = info.target_name medalBtn = clone } }) } let existMedal = cur.querySelector('.fans-medal-item') if (tags.hasOwnProperty('MedalWall')) { cur.querySelector('.xzsx_detect_tag-medalwall-medalBtn')?.remove() container2.className = 'xzsx_detect_tag-medalwall-medalBtn' container2.appendChild(medalBtn) if (domain === 'live.bilibili.com' && existMedal !== null) { container2 = existMedal container2.style.cursor = 'pointer' container2.onmouseenter = e => e.target.style.opacity = 0.8 container2.onmouseleave = e => e.target.style.opacity = 1 document.querySelector('#fans-medal-popover')?.remove() } container2.onclick = (e) => { e.stopPropagation() let ckey = medalWall.getAttribute('key'), key = getUname(dom) if (ckey !== null && ckey === key) return medalWall.setAttribute('key', key) medalWall.innerHTML = ` <div title="${getUname(dom)} / 勋章墙" class="xzsx_detect_tag-medalwall-title">${getUname(dom)} / 勋章墙</div> <div class="xzsx_detect_tag-medalwall-container"></div> ` let medalContainer = medalWall.querySelector('.xzsx_detect_tag-medalwall-container'), medalTitle = medalWall.querySelector('.xzsx_detect_tag-medalwall-title') medalTitle.onclick = () => dom.scrollIntoView({ block: "center", inline: "center" }) medalTitle.onmouseenter = e => e.target.style.opacity = 0.8 medalTitle.onmouseleave = e => e.target.style.opacity = 1 medalContainer.style.height = medalList.length < 25 ? '120px' : '240px' medalContainer.append(...medalList) const [size1, size2] = waterfall(medalContainer, medalList, 8, 'Horizontal'), pos = e.target.getBoundingClientRect() trackPanel.className = 'xzsx_detect_tag-trackpanel-hide' medalWall.className = 'xzsx_detect_tag-medalwall-hide' if (domain === 'live.bilibili.com') { medalWall.style.position = 'fixed' medalWall.style.right = '24px' medalWall.style.top = '24px' } else { medalWall.style.position = 'absolute' medalWall.style.left = (iframePos?.left ?? 0) + (pos.left + 260 > document.documentElement.clientWidth ? document.documentElement.clientWidth - 300 : pos.left + 10) + 'px' medalWall.style.top = (iframePos?.top ?? 0) + document.scrollingElement.scrollTop + (pos.top + 220 > document.documentElement.clientHeight ? document.documentElement.clientHeight - 250 : pos.top + 20) + 'px' } medalWall.style.display = 'flex' medalWall.style.height = size1 + (size2 > 400 ? 38 : 34) + 'px' medalWall.style.width = size2 + 'px' medalContainer.style.height = size1 + 'px' medalContainer.style.width = size2 + 'px' medalTitle.style.width = size2 - 8 + 'px' medalWall.scrollLeft = 0 medalWall.className = 'xzsx_detect_tag-medalwall-show' /** 滚轮控制横向滚动 */ if (size2 > 400) { medalWall.addEventListener("wheel", e => { e.preventDefault() medalWall.scrollLeft += e.deltaY }) } } } container.append(domain === 'live.bilibili.com' ? '' : container2, ...Tags, exist_btn && length > TAG_MAX_LENGTH ? icon : '', forceSync ? checkBtn : (JSON.parse((dom.attributes?.xzsx_isdeepchecked?.value ?? false)) || userTagsHash[uid]['isdeepchecked']) ? '' : checkBtn) tag_container === null && cur.append(container) if (domain === 'live.bilibili.com' && existMedal == null) cur?.insertBefore(container2, cur.firstChild) } catch (error) { console.log(error); } } /** 屏蔽操作 * @param { Element | null } user 用户评论元素 * @returns { void } */ const block = (user) => { let closest = domain === 'live.bilibili.com' ? user : version ? user.closest(domainMap[domain].classMap[user.className][1]) : user.parentNode.className !== 'reply-con' ? user.parentNode : user.parentNode.parentNode switch (DEL_STYLE) { case '消息屏蔽': closest = closest.querySelector(user.parentNode.className === 'reply-con' ? '.text-con' : domainMap[domain].classMap[user.className][2]) closest.style.fontStyle = 'oblique' closest.textContent = '[消息已删除]' break case '完全删除': closest.style.display = 'none' break /** ... */ default: break } } /** 查重 * @param { string } uid 用户UID * @param { Element | null } user 用户评论元素 * @returns { void } */ const filte = (uid, user) => { if (DEL && userBlackList.has(uid)) block(user) if (DET && (!DEL || DEL_STYLE !== '完全删除') && uidset.has(uid)) appendTags(uid, user, userTags[uid]) return uidset.has(uid) || userBlackList.has(uid) } /** 处理用户数据 * @param { string } uid 用户UID * @param { string } resStr 用户信息字符串 * @param { string } source 字符串来源 * @param { boolean } forceSync 是否强制刷新 * @param { function } resolve 调用结束Promise状态 * @returns { void } */ const dealRes = (uid, resStr, source, forceSync, resolve) => { try { if (DEL) { for (const key of Object.keys(rulesApply.blackList)) { if (rulesApply.blackList[key] === '基于成分' && rulesApply.detect.hasOwnProperty(key) && rulesApply.detect[key].keywords.size > 0) { rulesApply.detect[key].keywords.forEach(key => resStr.indexOf(key) !== -1 && userBlackList.add(uid)) } } } if (DET) { /** 结合黑子关键词atikeyword与粉丝关键词keyword查成分 */ for (const tagname of Object.keys(rulesApply.detect)) { if ((forceSync || !userTagsHash[uid]['filter'].has(tagname)) && rulesApply.detect[tagname].keywords.size > 0) { if (!userTags[uid].hasOwnProperty(tagname)) { userTags[uid][tagname] = { fan: { isFans: false, sources: {} }, anti: { isAnti: false, sources: {} } } } rulesApply.detect[tagname].keywords.forEach((keyword) => { let index = resStr.indexOf(keyword) while (index !== -1) { userTags[uid][tagname].fan.isFans = true if (!userTags[uid][tagname].fan.sources.hasOwnProperty(source)) { userTags[uid][tagname].fan.sources[source] = new Set() } /** 截取关键词前后十个字符 */ let begin = index, L = index, R = index + keyword.length, countL = 0, countR = 0 while (L > 0 && resStr[L] !== '』' && countL++ < 10) { --L } while (R < resStr.length && resStr[R] !== '∏' && countR++ < 10) { ++R } while (begin > 0 && resStr[begin] !== '『') { begin-- } userTags[uid][tagname].fan.sources[source].add(keyword + 'Γ' + resStr.slice(begin, resStr.indexOf('』', begin) + 1) + resStr.slice(L + 1, R)) index = resStr.indexOf(keyword, index + 1) } }) if (rulesApply.detect[tagname].antikeywords.size > 0) { rulesApply.detect[tagname].antikeywords.forEach((antikeyword) => { let index = resStr.indexOf(antikeyword) while (index !== -1) { userTags[uid][tagname].anti.isAnti = true if (!userTags[uid][tagname].anti.sources.hasOwnProperty(source)) { userTags[uid][tagname].anti.sources[source] = new Set() } /** 截取关键词前后十个字符 */ let begin = index, L = index, R = index + antikeyword.length, countL = 0, countR = 0 while (L > 0 && resStr[L] !== '』' && countL++ < 10) { --L } while (R < resStr.length && resStr[R] !== '∏' && countR++ < 10) { ++R } while (begin > 0 && resStr[begin] !== '『') { begin-- } userTags[uid][tagname].anti.sources[source].add(antikeyword + 'Γ' + resStr.slice(begin, resStr.indexOf('』', begin) + 1) + resStr.slice(L + 1, R)) index = resStr.indexOf(antikeyword, index + 1) } }) } } } uidset.add(uid) } } catch (error) { console.log(error) } finally { resolve() } } /** 渲染行为 * @param { boolean } forceSync 是否强制刷新, 默认为否 * @param { boolean } isJump 是否插队 * @returns { Promise<void> } */ const render = async (forceSync = false, isJump = false) => { if (!DET && !DEL) return try { /** 获取用户元素集合 */ let DOMS = Array.from(await getDom(...[domainMap[domain].userDomClass, ...domain === 'space.bilibili.com' ? [500, true] : []])), curDocument = domain === 'live.bilibili.com' ? ciframe !== undefined ? ciframe.contentWindow.document : document : document /** 若需要插队则对数组排序进行特殊处理 */ if (isJump) DOMS = DOMS.slice(0, _T._limit - _T._count).concat(DOMS.slice(_T._limit - _T._count, DOMS.length).reverse()) for (const user of DOMS) { /** 过滤不在视野内的元素 */ const pos = user.getBoundingClientRect() if (pos.top + pos.height < 0 || pos.top > curDocument.documentElement.clientHeight) continue /** 获取UID和昵称 */ const uid = getUID(user), uname = getUname(user) /** 过滤未设置的Class 排除非评论元素 */ if (!domainMap[domain].classMap.hasOwnProperty(user.className) || !uid) continue /** 过滤已查过成分的DOM元素 */ if (!forceSync && JSON.parse(user.attributes?.xzsx_ischecked?.value ?? false)) continue else user.setAttribute('xzsx_ischecked', true) /** 过滤已查过成分的用户 */ if (!forceSync && filte(uid, user)) continue /** 基于昵称屏蔽 */ if (DEL && rules.blackList.hasOwnProperty(uname?.trim()) && rules.blackList[uname?.trim()] === '基于昵称') { userBlackList.add(uid) block(user) } /** 基于成分贴标签/屏蔽 */ if (DET) { if (!userTagsHash.hasOwnProperty(uid)) { userTagsHash[uid] = { isdeepchecked: false, filter: new Set() } userTags[uid] = {} } /** 根据模式选择不同的处理方式 */ switch (MODE) { case '自动': _T.enqueue({ uid, user, isdeep: false, forceSync }, isJump) break case '静默': appendTags(uid, user, [], true) continue default: break } } } } catch (error) { console.log(error) } } /** 节流渲染次数 * @type { () => void } */ let _render = _throttle((forceSync, isJump) => render(forceSync, isJump), INTERVAL, { leading: false, trailing: true })[0] /** <===================================================================================列表监听===========================================================================================> */ /** 监听器浏览器兼容 * @type { MutationObserver } */ const MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver /** 监听元素 * @param { boolean } params getDom 参数 * @param { boolean } isMark 是否开启优化, 默认为false * @param { boolean } breakCondition 跳出情况, 默认为false * @param { boolean } extra 额外需要执行的函数, 默认为空函数 * @returns { () => Promise<void> } */ const Listen = (params, isMark = false, breakCondition = false, extra = async () => { }) => { if (!Array.isArray(params) || typeof (breakCondition) !== 'boolean' || typeof (isMark) !== 'boolean' || typeof (extra) !== 'function' || breakCondition) return async () => { } return async () => { const DOMS = await getDom(...params) for (const dom of DOMS) { if (isMark && dom.attributes?.xzsx_islistened?.value) continue new MutationObserver(() => { _render(false, true); extra() }).observe(dom, { childList: true }) isMark && dom.setAttribute('xzsx_islistened', true) } } } /** 动态页面监听子列表 * @type { () => Promise<void> } */ const ListenSubSubReplyList = Listen(['.comment-list,.reply-box', ...domain === 'space.bilibii.com' ? [250, true] : []], true) /** 监听子评论列表 * @type { () => Promise<void> } */ const ListenSubReplyList = Listen( [versionMap[domain][version].subList], true, domain === 'live.bilibili.com', async () => { if (domain === 'space.bilibili.com') { await Listen(['.comment-list', 250, true], true, false, async () => ListenSubSubReplyList())() await ListenSubSubReplyList() } } ) /** 监听主评论列表 * @type { () => Promise<void> } */ const ListenReplyList = Listen([versionMap[domain][version].mainList], true, false, async () => await ListenSubReplyList()) /** 滚动页面触发渲染&防抖 */ const _dscroll = _debounce(() => render(), 100, { leading: false, trailing: true })[0] /** 监听直播页面的特殊滚动条 * @returns { Promise<void> } */ const ListenScrollBar = async () => { const scroller = await getDom('#chat-history-list > div.ps__scrollbar-y-rail') scroller.forEach(ele => new MutationObserver(() => _dscroll()).observe(ele, { attributeFilter: ['style'] })) } /** 批量监听 * @returns { Promise<void> } */ const batchListen = async () => { /** 监听主列表 */ await ListenReplyList() /** 监听子列表 */ await ListenSubReplyList() /** 动态页面额外监听的子列表 */ domain === 'space.bilibili.com' && await ListenSubSubReplyList() /** 直播页面额外监听列表特殊滚动条 */ domain === 'live.bilibili.com' && await ListenScrollBar() } /** 初始渲染 */ _render() /** 初始监听 */ await batchListen() /** 监听页面滚动并检测视野范围内的用户成分 */ window.onscroll = _dscroll /** 单页面监听URL变化重新监听评论列表 */ history.replaceState = bindHistoryEvent('replaceState') window.onhashchange = async () => { _T.resetqueue(); _T.reset(); await batchListen() } window.onpopstate = async () => { _T.resetqueue(); _T.reset(); await batchListen() } window.addEventListener('replaceState', async () => { _T.resetqueue(); _T.reset(); await batchListen() }) } script()