// ==UserScript==
// @name NGA Filter
// @namespace https://greasyfork.org/users/263018
// @version 1.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 = `
过滤方式:
过滤优先级:用户 > 标记 > 全局
`;
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;
if (filterMode === "标记") {
item.contentC.style = "text-decoration: line-through;";
} else if (filterMode === "隐藏") {
item.contentC.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;
while (
temp.className !== "forumbox postbox" &&
temp.className !== "comment_c left"
) {
temp = temp.parentNode;
}
return temp;
})();
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 === "隐藏") {
item.containerC.style.display = "none";
} else {
item.avatarC.style.display = "";
item.containerC.style.display = "";
item.contentC.innerHTML = item.contentB;
if (item.actionC) {
item.actionC.style = "background: #aaa;";
}
}
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;
}
`);
// 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}
|
|
|
|
`;
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);