// ==UserScript== // @name NGA Filter // @namespace https://greasyfork.org/users/263018 // @version 1.2.2 // @author snyssss // @description troll must die // @match *://bbs.nga.cn/thread.php?fid=* // @match *://bbs.nga.cn/read.php?tid=* // @match *://bbs.nga.cn/nuke.php?* // @match *://ngabbs.com/thread.php?fid=* // @match *://ngabbs.com/read.php?tid=* // @match *://ngabbs.com/nuke.php?* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @noframes // @downloadURL none // ==/UserScript== ((n, self) => { if (n === undefined) return; const key = "NGAFilter"; // 过滤方式 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: {}, options: { filterMode: "隐藏", keyword: "", }, }; 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 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(); } } // 编辑用户标记 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("")}
过滤方式:
过滤优先级:用户 > 标记 > 全局
`; 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 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 user = data.users[uid]; const tags = user ? user.tags.map((tag) => data.tags[tag]) : []; // 过滤方式,优先程度 用户 > 标记 > 全局 const filterMode = (() => { if (user) { if (user.filterMode !== FILTER_MODE[0]) { return user.filterMode; } const tagsFilterMode = tags .map((tag) => FILTER_MODE.indexOf(tag.filterMode) || 0) .sort((a, b) => b - a)[0]; if (tagsFilterMode > 0) { return FILTER_MODE[tagsFilterMode]; } return data.options.filterMode; } if ( data.options.keyword.length && item[1].innerText.search(data.options.keyword) >= 0 ) { return data.options.filterMode; } })(); 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 user = data.users[item.pAid]; const tags = user ? user.tags.map((tag) => data.tags[tag]) : []; // 过滤方式,优先程度 用户 > 标记 > 全局 const filterMode = (() => { if (user) { if (user.filterMode !== FILTER_MODE[0]) { return user.filterMode; } const tagsFilterMode = tags .map((tag) => FILTER_MODE.indexOf(tag.filterMode) || 0) .sort((a, b) => b - a)[0]; if (tagsFilterMode > 0) { return FILTER_MODE[tagsFilterMode]; } return data.options.filterMode; } })(); 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) { 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) { item.actionC.style = "background: #cb4042;"; } } else if (filterMode === "隐藏") { item.containerC.style.display = "none"; } if (item.tagC) { 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; } `); // 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 blockModule = (() => { 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 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 += `
过滤关键词,用"|"隔开
`; const actions = tc.getElementsByTagName("button"); actions[0].onclick = () => { const v = actions[0].previousElementSibling.value; data.options.keyword = v; saveData(); reFilter(); }; container.appendChild(tc); } // 全局过滤方式 { const tc = document.createElement("div"); tc.innerHTML += `
全局过滤方式
过滤优先级:用户 > 标记 > 全局
`; ["标记", "遮罩", "隐藏"].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(blockModule).toggle(); u.addModule(tagModule); 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", (returnValue, originalFunction, arguments) => reFilter() ); initialized.topicArg = true; } if (n.postArg && initialized.postArg === false) { hookFunction( n.postArg, "proc", (returnValue, originalFunction, arguments) => reFilter() ); initialized.postArg = true; } }); if (n.ucp) { hookFunction(n.ucp, "_echo", (returnValue, originalFunction, arguments) => reFilter() ); } reFilter(); })(); })(commonui, __CURRENT_UID);