// ==UserScript== // @name NGA Filter // @namespace https://greasyfork.org/users/263018 // @version 1.3.2 // @author snyssss // @description troll must die // @match *://bbs.nga.cn/* // @match *://ngabbs.com/* // @match *://nga.178.com/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @noframes // @downloadURL none // ==/UserScript== ((n, self) => { if (n === undefined) return; const key = "NGAFilter"; // 过滤提示 const FILTER_TIPS = "过滤顺序:用户 > 标记 > 关键字
过滤级别:隐藏 > 遮罩 > 标记 > 继承 > 显示
多个标记或者关键字按最高级别过滤"; // 过滤方式 const FILTER_MODE = ["继承", "标记", "遮罩", "隐藏", "显示"]; // 切换过滤方式 const switchFilterMode = (value) => { const next = FILTER_MODE.indexOf(value) + 1; if (next >= FILTER_MODE.length) { return FILTER_MODE[0]; } return FILTER_MODE[next]; }; // 数据 const data = (() => { const d = { tags: {}, users: {}, keywords: {}, options: { filterMode: "隐藏", }, }; const v = GM_getValue(key); if (typeof v !== "object") { return d; } return Object.assign(d, v); })(); // 保存数据 const saveData = () => { GM_setValue(key, data); }; // 增加标记 const addTag = (name) => { const tag = Object.values(data.tags).find((item) => item.name === name); if (tag) return tag.id; const id = Math.max(...Object.values(data.tags).map((item) => item.id), 0) + 1; const hash = (() => { let h = 5381; for (var i = 0; i < name.length; i++) { h = ((h << 5) + h + name.charCodeAt(i)) & 0xffffffff; } return h; })(); const hex = Math.abs(hash).toString(16) + "000000"; const hsv = [ `0x${hex.substr(2, 2)}` / 255, `0x${hex.substr(2, 2)}` / 255 / 2 + 0.25, `0x${hex.substr(4, 2)}` / 255 / 2 + 0.25, ]; const rgb = n.hsvToRgb(hsv[0], hsv[1], hsv[2]); const color = ["#", ...rgb].reduce((a, b) => { return a + ("0" + b.toString(16)).slice(-2); }); data.tags[id] = { id, name, color, filterMode: FILTER_MODE[0], }; saveData(); return id; }; // 增加用户 const addUser = (id, name = null, tags = [], filterMode = FILTER_MODE[0]) => { if (data.users[id]) return data.users[id]; data.users[id] = { id, name, tags, filterMode, }; saveData(); return data.users[id]; }; // 增加关键字 const addKeyword = ( keyword, filterMode = FILTER_MODE[0], filterLevel = 0 ) => { const id = Math.max(...Object.values(data.keywords).map((item) => item.id), 0) + 1; data.keywords[id] = { id, keyword, filterMode, filterLevel, }; saveData(); return id; }; // 旧版本数据迁移 { const dataKey = "troll_data"; const modeKey = "troll_mode"; const keywordKey = "troll_keyword"; if (localStorage.getItem(dataKey)) { let trollMap = (function () { try { return JSON.parse(localStorage.getItem(dataKey)) || {}; } catch (e) {} return {}; })(); let filterMode = ~~localStorage.getItem(modeKey); let filterKeyword = localStorage.getItem(keywordKey) || ""; // 整理标签 [...new Set(Object.values(trollMap).flat())].forEach((item) => addTag(item) ); // 整理用户 Object.keys(trollMap).forEach((item) => { addUser( item, null, (typeof trollMap[item] === "object" ? trollMap[item] : []).map( (tag) => addTag(tag) ) ); }); data.options.filterMode = filterMode ? "隐藏" : "标记"; data.options.keyword = filterKeyword; localStorage.removeItem(dataKey); localStorage.removeItem(modeKey); localStorage.removeItem(keywordKey); saveData(); } // v1.1.0 -> v1.1.1 { Object.values(data.users).forEach(({ id, name, tags, enabled }) => { if (enabled !== undefined) { data.users[id] = { id, name, tags, filterMode: enabled ? "继承" : "显示", }; } }); Object.values(data.tags).forEach(({ id, name, color, enabled }) => { if (enabled !== undefined) { data.tags[id] = { id, name, color, filterMode: enabled ? "继承" : "显示", }; } }); if (data.options.filterMode === 0) { data.options.filterMode = "隐藏"; } else if (data.options.filterMode === 1) { data.options.filterMode = "标记"; } saveData(); } // v1.2.x -> v1.3.0 { if (data.options.keyword) { addKeyword(data.options.keyword); delete data.options.keyword; saveData(); } } } // 编辑用户标记 const editUser = (() => { let window; return (uid, name, callback) => { if (window === undefined) { window = n.createCommmonWindow(); } const user = data.users[uid]; const content = document.createElement("div"); const size = Math.floor((screen.width * 0.8) / 200); const items = Object.values(data.tags).map( (tag, index) => ` item === tag.id) && "checked" }/> ` ); const rows = [...new Array(Math.ceil(items.length / size))].map( (item, index) => ` ${items.slice(size * index, size * (index + 1)).join("")} ` ); content.className = "w100"; content.innerHTML = `
${rows.join("")}
过滤方式:
${FILTER_TIPS}
`; const actions = content.getElementsByTagName("button"); actions[0].onclick = () => { actions[0].innerText = switchFilterMode( actions[0].innerText || FILTER_MODE[0] ); }; actions[1].onclick = () => { if (confirm("是否确认?")) { delete data.users[uid]; saveData(); callback && callback(); window._.hide(); } }; actions[2].onclick = () => { if (confirm("是否确认?")) { const values = [...content.getElementsByTagName("input")]; const newTags = values[values.length - 1].value .split("|") .filter((item) => item.length) .map((item) => addTag(item)); const tags = [ ...new Set( values .filter((item) => item.type === "checkbox" && item.checked) .map((item) => ~~item.value) .concat(newTags) ), ].sort(); if (user) { user.tags = tags; user.filterMode = actions[0].innerText; } else { addUser(uid, name, tags, actions[0].innerText); } saveData(); callback && callback(); window._.hide(); } }; if (user === undefined) { actions[1].style = "display: none;"; } window._.addContent(null); window._.addTitle(`编辑标记 - ${name ? name : "#" + uid}`); window._.addContent(content); window._.show(); }; })(); // 判断过滤方式 const getFilterMode = (uid, content, level) => { let result = -1; const user = data.users[uid]; const tags = user ? user.tags.map((tag) => data.tags[tag]) : []; const keywords = Object.values(data.keywords); if (user) { const filterMode = FILTER_MODE.indexOf(user.filterMode); if (filterMode > 0) { return filterMode; } result = filterMode; } if (tags.length) { const filterMode = (() => { if (tags.some((tag) => tag.filterMode !== "显示")) { return tags .filter((tag) => tag.filterMode !== "显示") .map((tag) => FILTER_MODE.indexOf(tag.filterMode) || 0) .sort((a, b) => b - a)[0]; } return FILTER_MODE.indexOf("显示"); })(); if (filterMode > 0) { return filterMode; } result = filterMode; } if (keywords.length) { const filterMode = (() => { const r = keywords .filter((item) => item.keyword && item.filterMode !== "显示") .filter((item) => (item.filterLevel || 0) >= level) .sort( (a, b) => FILTER_MODE.indexOf(b.filterMode) - FILTER_MODE.indexOf(a.filterMode) ) .find((item) => content.search(item.keyword) >= 0); if (r) { return FILTER_MODE.indexOf(r.filterMode); } return result; })(); if (filterMode > 0) { return filterMode; } result = filterMode; } return result; }; // 处理引用 const handleQuote = (content) => { const quotes = content.querySelectorAll(".quote"); quotes.forEach((quote) => { const uid = (() => { const ele = quote.querySelector("a[href^='/nuke.php']"); if (ele) { const res = ele.getAttribute("href").match(/uid=(\S+)/); if (res) { return res[1]; } } return 0; })(); if (uid) { const filterMode = (() => { const mode = getFilterMode(uid, quote.innerText, 1); if (mode === 0) { return data.options.filterMode; } if (mode > 0) { return FILTER_MODE[mode]; } return ""; })(); if (filterMode === "标记") { quote.innerHTML = `
Troll must die. 点击查看
${quote.innerHTML}
`; } else if (filterMode === "遮罩") { const source = document.createElement("DIV"); source.innerHTML = quote.innerHTML; source.style.display = "none"; const caption = document.createElement("CAPTION"); caption.className = "filter-mask filter-mask-block"; caption.innerHTML = `Troll must die.`; caption.onclick = () => { quote.removeChild(caption); source.style.display = ""; }; quote.innerHTML = ""; quote.appendChild(source); quote.appendChild(caption); } else if (filterMode === "隐藏") { quote.innerHTML = ""; } } }); }; // 过滤 const reFilter = () => { const tPage = location.pathname === "/thread.php"; const pPage = location.pathname === "/read.php"; const uPage = location.pathname === "/nuke.php"; if (tPage) { const tData = n.topicArg.data; Object.values(tData).forEach((item) => { if (item.containerC) return; const uid = item[2].search.match(/uid=(\S+)/) && item[2].search.match(/uid=(\S+)/)[1]; const filterMode = (() => { const mode = getFilterMode(uid, item[1].innerText, 0); if (mode === 0) { return data.options.filterMode; } if (mode > 0) { return FILTER_MODE[mode]; } return ""; })(); item.contentC = item[1]; item.contentB = item.contentB || item.contentC.innerHTML; item.containerC = item.containerC || item.contentC.parentNode.parentNode; item.containerC.style = ""; item.contentC.style = ""; item[1].className = item[1].className.replace(" filter-mask", ""); item[2].className = item[2].className.replace(" filter-mask", ""); if (filterMode === "标记") { item.contentC.style = "text-decoration: line-through;"; } else if (filterMode === "遮罩") { item[1].className += " filter-mask"; item[2].className += " filter-mask"; } else if (filterMode === "隐藏") { item.containerC.style = "display: none;"; } }); } else if (pPage) { const pData = n.postArg.data; Object.values(pData).forEach((item) => { if (~~item.pAid === self) return; if (item.containerC) return; if (typeof item.i === "number") { item.actionC = item.actionC || (() => { const ele = item.uInfoC.querySelector('[name="uid"]'); ele.onclick = null; return ele; })(); item.tagC = item.tagC || (() => { const tc = document.createElement("div"); tc.className = "filter-tags"; item.uInfoC.appendChild(tc); return tc; })(); } item.pName = item.pName || item.uInfoC.getElementsByClassName("author")[0].innerText; item.reFilter = item.reFilter || (() => { const uid = item.pAid; const filterMode = (() => { const mode = getFilterMode(uid, item.contentC.innerText, 1); if (mode === 0) { return data.options.filterMode; } if (mode > 0) { return FILTER_MODE[mode]; } return ""; })(); item.avatarC = item.avatarC || (() => { const tc = document.createElement("div"); const avatar = document.getElementById(`posteravatar${item.i}`); if (avatar) { avatar.parentNode.insertBefore(tc, avatar.nextSibling); tc.appendChild(avatar); } return tc; })(); item.contentB = item.contentB || item.contentC.innerHTML; item.containerC = item.containerC || (() => { let temp = item.contentC; if (item.i >= 0) { while (temp.nodeName !== "TBODY") { temp = temp.parentNode; } } else { while (temp.nodeName !== "DIV") { temp = temp.parentNode; } } return temp; })(); item.avatarC.style.display = ""; item.containerC.style.display = ""; item.contentC.innerHTML = item.contentB; if (item.actionC) { item.actionC.style = "background: #aaa;"; } if (filterMode === "标记") { item.avatarC.style.display = "none"; item.contentC.innerHTML = `
Troll must die. 点击查看
${item.contentB}
`; if (item.actionC && data.users[uid]) { item.actionC.style = "background: #cb4042;"; } } else if (filterMode === "遮罩") { const caption = document.createElement("CAPTION"); if (item.i >= 0) { caption.className = "filter-mask filter-mask-block"; } else { caption.className = "filter-mask filter-mask-block left"; caption.style = "width: 47%;"; } caption.innerHTML = `Troll must die.`; caption.onclick = () => { item.containerC.parentNode.removeChild(caption); item.containerC.style.display = ""; }; item.containerC.parentNode.insertBefore(caption, item.containerC); item.containerC.style.display = "none"; if (item.actionC && data.users[uid]) { item.actionC.style = "background: #cb4042;"; } } else if (filterMode === "隐藏") { item.containerC.style.display = "none"; } else { handleQuote(item.contentC); } if (item.tagC) { const tags = data.users[uid] ? data.users[uid].tags.map((tag) => data.tags[tag]) || [] : []; item.tagC.style.display = tags.length ? "" : "none"; item.tagC.innerHTML = tags .map( (tag) => `${tag.name}` ) .join(""); } }); if (item.actionC) { item.actionC.onclick = item.actionC.onclick || ((e) => { if (item.pAid < 0) return; const user = data.users[item.pAid]; if (e.ctrlKey === false) { editUser(item.pAid, item.pName, item.reFilter); } else { if (user) { delete data.users[user.id]; } else { addUser(item.pAid, item.pName); } saveData(); item.reFilter(); } }); } item.reFilter(); }); } else if (uPage) { const container = document.getElementById("ucp_block"); if (container.firstChild) { const uid = container.innerText.match(/用户ID\s*:\s*(\S+)/)[1]; const name = container.innerText.match(/用户名\s*:\s*(\S+)/)[1]; container.tagC = container.tagC || (() => { const c = document.createElement("span"); c.innerHTML = `

:: ${name} 的标记 ::

`; c.getElementsByTagName("a")[0].onclick = () => { editUser(uid, name, container.refresh); }; container.firstChild.insertBefore( c, container.firstChild.childNodes[1] ); return c.getElementsByClassName("filter-tags")[0]; })(); container.refresh = () => { container.tagC.innerHTML = data.users[uid].tags .map( (tag) => `${data.tags[tag].name}` ) .join(""); }; container.refresh(); } } }; // STYLE GM_addStyle(` .filter-table-wrapper { max-height: 80vh; overflow-y: auto; } .filter-table { margin: 0; } .filter-table th, .filter-table td { position: relative; white-space: nowrap; } .filter-table th { position: sticky; top: 2px; z-index: 1; } .filter-table input:not([type]), .filter-table input[type="text"] { margin: 0; box-sizing: border-box; height: 100%; width: 100%; } .filter-input-wrapper { position: absolute; top: 6px; right: 6px; bottom: 6px; left: 6px; } .filter-text-ellipsis { display: flex; } .filter-text-ellipsis > * { flex: 1; width: 1px; overflow: hidden; text-overflow: ellipsis; } .filter-button-group { margin: -.1em -.2em; } .filter-tags { margin: 2px -0.2em 0; text-align: left; } .filter-mask { margin: 1px; color: #81C7D4; background: #81C7D4; } .filter-mask-block { display: block; border: 1px solid #66BAB7; text-align: center !important; } .filter-input-wrapper { position: absolute; top: 6px; right: 6px; bottom: 6px; left: 6px; } `); // UI const u = (() => { const modules = {}; const tabContainer = (() => { const c = document.createElement("div"); c.className = "w100"; c.innerHTML = `
`; return c; })(); const tabPanelContainer = (() => { const c = document.createElement("div"); c.style = "width: 80vw;"; return c; })(); const content = (() => { const c = document.createElement("div"); c.append(tabContainer); c.append(tabPanelContainer); return c; })(); const addModule = (() => { const tc = tabContainer.getElementsByTagName("tr")[0]; const cc = tabPanelContainer; return (module) => { const tabBox = document.createElement("td"); tabBox.innerHTML = `${module.name}`; const tab = tabBox.childNodes[0]; const toggle = () => { Object.values(modules).forEach((item) => { if (item.tab === tab) { item.tab.className = "nobr"; item.content.style = "display: block"; item.refresh(); } else { item.tab.className = "nobr silver"; item.content.style = "display: none"; } }); }; tc.append(tabBox); cc.append(module.content); tab.onclick = toggle; modules[module.name] = { ...module, tab, toggle, }; return modules[module.name]; }; })(); return { content, modules, addModule, }; })(); // 用户 const userModule = (() => { const content = (() => { const c = document.createElement("div"); c.style = "display: none"; c.innerHTML = `
昵称 标记 过滤方式 操作
`; return c; })(); const refresh = (() => { const container = content.getElementsByTagName("tbody")[0]; const func = () => { container.innerHTML = ""; Object.values(data.users).forEach((item) => { const tc = document.createElement("tr"); tc.className = `row${ (container.querySelectorAll("TR").length % 2) + 1 }`; tc.refresh = () => { if (data.users[item.id]) { tc.innerHTML = ` [${ item.name ? "@" + item.name : "#" + item.id }] ${item.tags .map((tag) => { if (data.tags[tag]) { return `${data.tags[tag].name}`; } }) .join("")}
`; const actions = tc.getElementsByTagName("button"); actions[0].onclick = () => { data.users[item.id].filterMode = switchFilterMode( data.users[item.id].filterMode || FILTER_MODE[0] ); actions[0].innerHTML = data.users[item.id].filterMode; saveData(); reFilter(); }; actions[1].onclick = () => { editUser(item.id, item.name, tc.refresh); }; actions[2].onclick = () => { if (confirm("是否确认?")) { delete data.users[item.id]; container.removeChild(tc); saveData(); reFilter(); } }; } else { tc.remove(); } }; tc.refresh(); container.appendChild(tc); }); }; return func; })(); return { name: "用户", content, refresh, }; })(); // 标记 const tagModule = (() => { const content = (() => { const c = document.createElement("div"); c.style = "display: none"; c.innerHTML = `
标记 列表 过滤方式 操作
`; return c; })(); const refresh = (() => { const container = content.getElementsByTagName("tbody")[0]; const func = () => { container.innerHTML = ""; Object.values(data.tags).forEach((item) => { const tc = document.createElement("tr"); tc.className = `row${ (container.querySelectorAll("TR").length % 2) + 1 }`; tc.innerHTML = ` ${item.name}
${Object.values(data.users) .filter((user) => user.tags.find((tag) => tag === item.id) ) .map( (user) => `[${ user.name ? "@" + user.name : "#" + user.id }]` ) .join("")}
`; const actions = tc.getElementsByTagName("button"); actions[0].onclick = (() => { let hide = true; return () => { hide = !hide; actions[0].nextElementSibling.style.display = hide ? "none" : "block"; }; })(); actions[1].onclick = () => { data.tags[item.id].filterMode = switchFilterMode( data.tags[item.id].filterMode || FILTER_MODE[0] ); actions[1].innerHTML = data.tags[item.id].filterMode; saveData(); reFilter(); }; actions[2].onclick = () => { if (confirm("是否确认?")) { delete data.tags[item.id]; Object.values(data.users).forEach((user) => { const index = user.tags.findIndex((tag) => tag === item.id); if (index >= 0) { user.tags.splice(index, 1); } }); container.removeChild(tc); saveData(); reFilter(); } }; container.appendChild(tc); }); }; return func; })(); return { name: "标记", content, refresh, }; })(); // 关键字 const keywordModule = (() => { const content = (() => { const c = document.createElement("div"); c.style = "display: none"; c.innerHTML = `
列表 过滤方式 包括内容 操作
支持正则表达式。比如同类型的可以写在一条规则内用"|"隔开,"ABC|DEF"即为屏蔽带有ABC或者DEF的内容。
`; return c; })(); const refresh = (() => { const container = content.getElementsByTagName("tbody")[0]; const func = () => { container.innerHTML = ""; Object.values(data.keywords).forEach((item) => { const tc = document.createElement("tr"); tc.className = `row${ (container.querySelectorAll("TR").length % 2) + 1 }`; tc.innerHTML = `
`; const inputElement = tc.querySelector("INPUT"); const levelElement = tc.querySelector(`INPUT[type="checkbox"]`); const actions = tc.getElementsByTagName("button"); actions[0].onclick = () => { actions[0].innerHTML = switchFilterMode(actions[0].innerHTML); }; actions[1].onclick = () => { if (inputElement.value) { data.keywords[item.id] = { id: item.id, keyword: inputElement.value, filterMode: actions[0].innerHTML, filterLevel: levelElement.checked ? 1 : 0, }; saveData(); refresh(); } }; actions[2].onclick = () => { if (confirm("是否确认?")) { delete data.keywords[item.id]; saveData(); refresh(); } }; container.appendChild(tc); }); { const tc = document.createElement("tr"); tc.className = `row${ (container.querySelectorAll("TR").length % 2) + 1 }`; tc.innerHTML = `
`; const inputElement = tc.querySelector("INPUT"); const levelElement = tc.querySelector(`INPUT[type="checkbox"]`); const actions = tc.getElementsByTagName("button"); actions[0].onclick = () => { actions[0].innerHTML = switchFilterMode(actions[0].innerHTML); }; actions[1].onclick = () => { if (inputElement.value) { addKeyword( inputElement.value, actions[0].innerHTML, levelElement.checked ? 1 : 0 ); saveData(); refresh(); } }; container.appendChild(tc); } }; return func; })(); return { name: "关键字", content, refresh, }; })(); // 通用设置 const commonModule = (() => { const content = (() => { const c = document.createElement("div"); c.style = "display: none"; return c; })(); const refresh = (() => { const container = content; const func = () => { container.innerHTML = ""; // 默认过滤方式 { const tc = document.createElement("div"); tc.innerHTML += `
默认过滤方式
${FILTER_TIPS}
`; ["标记", "遮罩", "隐藏"].forEach((item, index) => { const ele = document.createElement("SPAN"); ele.innerHTML += ` `; const inp = ele.querySelector("input"); inp.onchange = () => { if (inp.checked) { data.options.filterMode = item; saveData(); reFilter(); } }; tc.querySelectorAll("div")[1].append(ele); }); container.appendChild(tc); } // 删除没有标记的用户 { const tc = document.createElement("div"); tc.innerHTML += `
`; const actions = tc.getElementsByTagName("button"); actions[0].onclick = () => { if (confirm("是否确认?")) { Object.values(data.users).forEach((item) => { if (item.tags.length === 0) { delete data.users[item.id]; } }); saveData(); reFilter(); } }; container.appendChild(tc); } // 删除没有用户的标记 { const tc = document.createElement("div"); tc.innerHTML += `
`; const actions = tc.getElementsByTagName("button"); actions[0].onclick = () => { if (confirm("是否确认?")) { Object.values(data.tags).forEach((item) => { if ( Object.values(data.users).filter((user) => user.tags.find((tag) => tag === item.id) ).length === 0 ) { delete data.tags[item.id]; } }); saveData(); reFilter(); } }; container.appendChild(tc); } }; return func; })(); return { name: "通用设置", content, refresh, }; })(); u.addModule(userModule).toggle(); u.addModule(tagModule); u.addModule(keywordModule); u.addModule(commonModule); // 增加菜单项 (() => { const title = "过滤设置"; let window; n.mainMenu.addItemOnTheFly(title, null, () => { if (window === undefined) { window = n.createCommmonWindow(); } window._.addContent(null); window._.addTitle(title); window._.addContent(u.content); window._.show(); }); })(); // 执行过滤 (() => { const hookFunction = (object, functionName, callback) => { ((originalFunction) => { object[functionName] = function () { const returnValue = originalFunction.apply(this, arguments); callback.apply(this, [returnValue, originalFunction, arguments]); return returnValue; }; })(object[functionName]); }; const initialized = { topicArg: false, postArg: false, }; hookFunction(n, "eval", () => { if (Object.values(initialized).findIndex((item) => item === false) < 0) { return; } if (n.topicArg && initialized.topicArg === false) { hookFunction(n.topicArg, "add", reFilter); initialized.topicArg = true; } if (n.postArg && initialized.postArg === false) { hookFunction(n.postArg, "proc", reFilter); initialized.postArg = true; } }); if (n.ucp) { hookFunction(n.ucp, "_echo", reFilter); } reFilter(); })(); })(commonui, __CURRENT_UID);