// ==UserScript==
// @name NGA Filter
// @namespace https://greasyfork.org/users/263018
// @version 1.0.1
// @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
// ==/UserScript==
(n => {
"use strict";
if (n === undefined) return;
const key = "NGAFilter";
// 数据
const data = (() => {
const d = {
tags: {},
users: {},
options: {
filterMode: 0,
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,
enabled: true
};
saveData();
return id;
};
// 增加用户
const addUser = (id, name = null, tags = [], isEnabled = true) => {
if (data.users[id]) return data.users[id];
data.users[id] = {
id,
name,
tags,
enabled: isEnabled
};
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,
trollMap[item].map(tag => addTag(tag))
);
});
data.options.filterMode = filterMode ? 0 : 1;
data.options.keyword = filterKeyword;
localStorage.removeItem(dataKey);
localStorage.removeItem(modeKey);
localStorage.removeItem(keywordKey);
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");
content.className = "w100";
content.innerHTML = `
<table class="filter-table" style="min-width: 400px;">
<tbody>
${Object.values(data.tags)
.map(
tag => `
<tr>
<td>
<b class="block_txt nobr" style="background:${
tag.color
}; color:#fff; margin: 0.1em 0.2em;">${
tag.name
}</b>
</td>
<td>
<input type="checkbox" value="${
tag.id
}" ${user &&
user.tags.find(item => item === tag.id) &&
"checked"}/>
</td>
</tr>
`
)
.join("")}
</tbody>
<tfoot>
<tr>
<td colspan="2">
<input placeholder="一次性添加多个标记用"|"隔开,不会添加重名标记" style="width: -webkit-fill-available;" />
</td>
</tr>
</tfoot>
</table>
<div style="margin: 10px 0;">
<button>${user && user.enabled === false ? "启用" : "禁用"}</button>
<div class="right_">
<button>删除</button>
<button>保存</button>
</div>
</div>
`;
const actions = content.getElementsByTagName("button");
actions[0].onclick = () => {
actions[0].innerText =
actions[0].innerText === "禁用" ? "启用" : "禁用";
};
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.enabled = 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";
const func = (() => {
if (tPage) {
return () => {
const tData = n.topicArg.data;
Object.values(tData).forEach(item => {
const uid =
item[2].search.match(/uid=(\S+)/).length &&
item[2].search.match(/uid=(\S+)/)[1];
const user = data.users[uid];
const tags = user ? user.tags.map(tag => data.tags[tag]) : [];
const isBlock =
user &&
user.enabled &&
(tags.length === 0 || tags.filter(tag => tag.enabled).length);
item.contentC = item[1];
item.contentB = item.contentB || item.contentC.innerHTML;
item.containerC =
item.containerC || item.contentC.parentNode.parentNode;
item.containerC.style =
isBlock && data.options.filterMode === 0 ? "display: none;" : "";
item.contentC.style =
isBlock && data.options.filterMode === 1
? "text-decoration: line-through;"
: "";
});
};
} else if (pPage) {
return () => {
const pData = n.postArg.data;
Object.values(pData).forEach(item => {
if (typeof item.i === "number") {
item.actionC =
item.actionC ||
(() => {
const ele = item.uInfoC.firstElementChild.lastElementChild;
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 isBlock =
user &&
user.enabled &&
(tags.length === 0 || tags.filter(tag => tag.enabled).length);
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;
while (
temp.className !== "forumbox postbox" &&
temp.className !== "comment_c left"
) {
temp = temp.parentNode;
}
return temp;
})();
item.containerC.style.display =
isBlock && data.options.filterMode === 0 ? "none" : "";
item.contentC.innerHTML =
isBlock && data.options.filterMode === 1
? `
<div class="lessernuke" style="background: #81C7D4; border-color: #66BAB7;">
<span class="crimson">Troll must die.</span>
<a href="javascript:void(0)" onclick="[...document.getElementsByName('troll_${user.id}')].forEach(item => item.style.display = '')">点击查看</a>
<div style="display: none;" name="troll_${user.id}">
${item.contentB}
</div>
</div>`
: item.contentB;
item.avatarC.style.display = isBlock ? "none" : "";
if (item.actionC) {
item.actionC.style =
user && user.enabled
? "background: #cb4042;"
: "background: #aaa;";
}
if (item.tagC) {
item.tagC.style.display = tags.length ? "" : "none";
item.tagC.innerHTML = tags
.map(
tag =>
`<b class="block_txt nobr" style="background:${tag.color}; color:#fff; margin: 0.1em 0.2em;">${tag.name}</b>`
)
.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) {
editUser(item.pAid, item.pName, item.reFilter);
} else {
if (user) {
if (user.tags.length) {
user.enabled = !user.enabled;
user.name = item.pName;
} else {
delete data.users[user.id];
}
} else {
addUser(item.pAid, item.pName);
}
saveData();
item.reFilter();
}
});
}
item.reFilter();
});
};
} else if (uPage) {
return () => {
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 = `
<h2 class="catetitle">:: ${name} 的标记 ::</h2>
<div class="cateblock" style="text-align: left; line-height: 1.8em;">
<div class="contentBlock" style="padding: 5px 10px;">
<span>
<ul class="actions" style="padding: 0px; margin: 0px;">
<li style="padding-right: 5px;">
<span>
<a href="javascript: void(0);">[编辑 ${name} 的标记]</a>
</span>
</li>
<div class="clear"></div>
</ul>
</span>
<div class="filter-tags"></div>
<div class="clear"></div>
</div>
</div>
`;
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 =>
`<b class="block_txt nobr" style="background:${data.tags[tag].color}; color:#fff; margin: 0.1em 0.2em;">${data.tags[tag].name}</b>`
)
.join("");
};
container.refresh();
}
};
}
return () => {};
})();
const observer = new MutationObserver(mutations => {
if (mutations.find(mutation => mutation.addedNodes.length)) {
func();
}
});
if (tPage) {
observer.observe(document.getElementById("topicrows"), {
childList: true
});
} else if (pPage) {
observer.observe(document.getElementById("m_posts_c"), {
childList: true
});
} else if (uPage) {
observer.observe(document.getElementById("ucp_block"), {
childList: true
});
}
func();
return func;
})();
// STYLE
GM_addStyle(`
.filter-tags {
margin: 2px -0.2em 0;
text-align: left;
}
.filter-table {
border: 1px solid #ead5bc;
border-left: none;
border-bottom: none;
width: 99.95%;
}
.filter-table thead {
background-color: #591804;
color: #fff8e7;
}
.filter-table tbody tr {
background-color: #fff0cd;
}
.filter-table tbody tr:nth-of-type(odd) {
background-color: #fff8e7;
}
.filter-table td {
border: 1px solid #ead5bc;
border-top: none;
border-right: none;
padding: 6px;
}
`);
// UI
const u = (() => {
const modules = {};
const tabContainer = (() => {
const c = document.createElement("div");
c.className = "w100";
c.innerHTML = `
<div class="right_" style="margin-bottom: 5px;">
<table class="stdbtn" cellspacing="0">
<tbody>
<tr></tr>
</tbody>
</table>
</div>
<div class="clear"></div>
`;
return c;
})();
const tabPanelContainer = (() => {
const c = document.createElement("div");
c.style =
"min-width: 20vw; max-width: 80vw; max-height: 80vh; overflow: auto;";
c.innerHTML = `
`;
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 = `<a href="javascript:void(0)" class="nobr silver">${module.name}</a>`;
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 = `
<table class="filter-table">
<thead>
<tr>
<td>
<b style="margin: 0.1em 0.2em;">昵称</b>
</td>
<td>
<b style="margin: 0.1em 0.2em;">标记</b>
</td>
<td>
<b style="margin: 0.1em 0.2em;">操作</b>
</td>
</tr>
</thead>
<tbody></tbody>
</table>
`;
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.refresh = () => {
if (data.users[item.id]) {
tc.innerHTML = `
<tr>
<td>
<a href="/nuke.php?func=ucp&uid=${
item.id
}" class="b nobr">[${
item.name ? "@" + item.name : "#" + item.id
}]</a>
</td>
<td>
${item.tags
.map(tag => {
if (data.tags[tag]) {
return `<b class="block_txt nobr" style="background:${data.tags[tag].color}; color:#fff; margin: 0.1em 0.2em;">${data.tags[tag].name}</b>`;
}
})
.join("")}
</td>
<td class="nobr">
<button>编辑</button>
<button>${item.enabled ? "禁用" : "启用"}</button>
<button>删除</button>
</td>
</tr>
`;
const actions = tc.getElementsByTagName("button");
actions[0].onclick = () => {
editUser(item.id, item.name, tc.refresh);
};
actions[1].onclick = () => {
data.users[item.id].enabled = !data.users[item.id].enabled;
actions[1].innerHTML = data.users[item.id].enabled
? "禁用"
: "启用";
saveData();
reFilter();
};
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 = `
<table class="filter-table">
<thead>
<tr>
<td>
<b style="margin: 0.1em 0.2em;">标记</b>
</td>
<td>
<b style="margin: 0.1em 0.2em;">列表</b>
</td>
<td>
<b style="margin: 0.1em 0.2em;">操作</b>
</td>
</tr>
</thead>
<tbody></tbody>
</table>
`;
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.innerHTML = `
<tr>
<td>
<b class="block_txt nobr" style="background:${
item.color
}; color:#fff; margin: 0.1em 0.2em;">${item.name}</b>
</td>
<td>
<button>${
Object.values(data.users).filter(user =>
user.tags.find(tag => tag === item.id)
).length
}
</button>
<div style="display: none;">
${Object.values(data.users)
.filter(user =>
user.tags.find(tag => tag === item.id)
)
.map(
user =>
`<a href="/nuke.php?func=ucp&uid=${
user.id
}" class="b nobr">[${
user.name ? "@" + user.name : "#" + user.id
}]</a>`
)
.join("")}
</div>
</td>
<td class="nobr">
<button>${item.enabled ? "禁用" : "启用"}</button>
<button>删除</button>
</td>
</tr>
`;
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].enabled = !data.tags[item.id].enabled;
actions[1].innerHTML = data.tags[item.id].enabled ? "禁用" : "启用";
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 += `
<div>过滤关键词,用"|"隔开</div>
<div>
<input value="${data.options.keyword}"/>
<button>确认</button>
</div>
`;
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 += `
<br/>
<div>过滤方式</div>
<div>
<input type="radio" name="filterType" ${data.options
.filterMode === 0 && "checked"}>
<span>隐藏</span>
<input type="radio" name="filterType" ${data.options
.filterMode === 1 && "checked"}>
<span>标记</span>
<button>确认</button>
</div>
`;
const actions = tc.getElementsByTagName("button");
actions[0].onclick = () => {
const values = document.getElementsByName("filterType");
for (let i = 0, length = values.length; i < length; i++) {
if (values[i].checked) {
data.options.filterMode = i;
break;
}
}
saveData();
reFilter();
};
container.appendChild(tc);
}
// 删除没有标记的用户
{
const tc = document.createElement("div");
tc.innerHTML += `
<br/>
<div>
<button>删除没有标记的用户</button>
</div>
`;
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 += `
<br/>
<div>
<button>删除没有用户的标记</button>
</div>
`;
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();
});
})();
})(commonui);
// commonui.postArg
// commonui.topicArg