// ==UserScript==
// @name NGA Auto Pagerize
// @namespace https://greasyfork.org/users/263018
// @version 1.7.4
// @author snyssss
// @description 简单的自动翻页,以及更多附加功能,如:快捷翻页、抽楼检测、只看楼主、大召唤术
// @match *://bbs.nga.cn/*
// @match *://ngabbs.com/*
// @match *://nga.178.com/*
// @grant GM_addStyle
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @grant GM_setClipboard
// @noframes
// ==/UserScript==
((ui, n = {}, api = {}, uid) => {
if (!ui) return;
// KEY
const ATTACHMENT_STYLE_ENABLE_KEY = "ATTACHMENT_STYLE_ENABLE";
const PAGE_BUTTON_STYLE_ENABLE_KEY = "PAGE_BUTTON_STYLE_ENABLE_KEY";
const HOTKEYS_ENABLE_KEY = "HOTKEYS_ENABLE_KEY";
const FORUM_NAME_ENABLE_KEY = "FORUM_NAME_ENABLE_KEY";
const POST_LOSS_DETECTION_KEY = "POSTS_LOSS_DETECTION_KEY";
const AUTHOR_ONLY_KEY = "AUTHOR_ONLY_KEY";
const AUTO_CHECK_IN_ENABLE_KEY = "AUTO_CHECK_IN_ENABLE_KEY";
const AUTO_CHECK_IN_LAST_TIME_KEY = "AUTO_CHECK_IN_LAST_TIME_KEY";
// 附件样式
const attachmentStyleEnable =
GM_getValue(ATTACHMENT_STYLE_ENABLE_KEY) || false;
// 页码样式
const pageButtonStyleEnable =
GM_getValue(PAGE_BUTTON_STYLE_ENABLE_KEY) || false;
// 快捷翻页
const hotkeysEnable = GM_getValue(HOTKEYS_ENABLE_KEY) || false;
// 版面名称
const forumNameEnable = GM_getValue(FORUM_NAME_ENABLE_KEY) || false;
// 抽楼检测
const postLossDetectionEnable = GM_getValue(POST_LOSS_DETECTION_KEY) || false;
// 只看楼主
const authorOnlyEnable = GM_getValue(AUTHOR_ONLY_KEY) || false;
// 自动签到
const autoCheckInEnable = GM_getValue(AUTO_CHECK_IN_ENABLE_KEY) || false;
// 自动签到时间
const autoCheckInLastTime = GM_getValue(AUTO_CHECK_IN_LAST_TIME_KEY) || 0;
// 自动签到 UA
const autoCheckInUserAgent = "Nga_Official/80024(Android12)";
// STYLE
GM_addStyle(`
.s-table-wrapper {
max-height: 80vh;
overflow-y: auto;
}
.s-table {
margin: 0;
}
.s-table th,
.s-table td {
position: relative;
white-space: nowrap;
}
.s-table th {
position: sticky;
top: 2px;
z-index: 1;
}
.s-table input:not([type]), .s-table input[type="text"] {
margin: 0;
box-sizing: border-box;
height: 100%;
width: 100%;
}
.s-input-wrapper {
position: absolute;
top: 6px;
right: 6px;
bottom: 6px;
left: 6px;
}
.s-text-ellipsis {
display: flex;
}
.s-text-ellipsis > * {
flex: 1;
width: 1px;
overflow: hidden;
text-overflow: ellipsis;
}
.s-button-group {
margin: -.1em -.2em;
}
`);
// 防抖
const debounce = (func, delay = 500) => {
let id;
return (...args) => {
clearTimeout(id);
id = setTimeout(() => func(...args), delay);
};
};
// 扩展菜单
const enhanceMenu = (() => {
// 大召唤术
const summon = (() => {
let window;
return () => {
// 标识
const key = "SUMMON";
// 标题
const title = "大召唤术";
// 内容
const content = (() => {
const c = document.createElement("DIV");
c.innerHTML = `
<div class="s-table-wrapper" style="width: 1000px; max-width: 95vw;">
<table class="s-table forumbox">
<thead>
<tr class="block_txt_c0">
<th class="c1" width="1">用户</th>
<th class="c2">内容</th>
<th class="c3" width="1">选择</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
<div style="display: flex; margin-top: 10px;">
<textarea readonly rows="3" style="flex: 1; overflow: auto;"></textarea>
<div style="display: flex; flex-direction: column;">
<button style="flex: 1;">复制</button>
<button style="flex: 1;">全选/全不选</button>
</div>
</div>
`;
return c;
})();
// 选项改变事件
const handleChange = (e) => {
const items = content.querySelectorAll("INPUT");
if (e) {
const { checked } = e.target;
const id = e.target.getAttribute("data-id");
items.forEach((item) => {
if (item.getAttribute("data-id") === id) {
item.checked = checked;
}
});
}
const result = content.querySelector("TEXTAREA");
const resultItems = [...items]
.filter((item) => item.checked)
.map((item) => item.getAttribute("data-name"))
.filter((name, index, self) => self.indexOf(name) === index);
result.value = resultItems.join("");
};
// 刷新列表
const refresh = (() => {
const container = content.querySelector("TBODY");
const func = () => {
container.innerHTML = "";
Object.values(ui.postArg.data).forEach((item) => {
const { pAid, contentC, uInfoC } = item;
if (pAid < 0 || pAid === String(uid || 0)) {
return;
}
if (uInfoC === undefined) {
return;
}
const pName = (() => {
const container =
uInfoC.closest("TR").querySelector(".posterInfoLine") ||
uInfoC;
return container.querySelector(".author").innerText;
})();
const tc = document.createElement("TR");
tc.className = `row${
(container.querySelectorAll("TR").length % 2) + 1
}`;
tc.innerHTML = `
<td class="c1">
<a href="/nuke.php?func=ucp&uid=${pAid}" class="b nobr">
[@${pName}]
</a>
</td>
<td class="c2">
<div class="filter-text-ellipsis">
<span title="${contentC.innerText}">
${contentC.innerText}
</span>
</div>
</td>
<td class="c3">
<input type="checkbox" data-id="${pAid}" data-name="[@${pName}]" />
</td>
`;
tc.querySelector("INPUT").addEventListener(
"change",
handleChange
);
container.appendChild(tc);
});
handleChange();
};
// 绑定事件
(() => {
const textarea = content.querySelector("textarea");
textarea.addEventListener("click", () => {
textarea.select();
});
const clipboard = content.querySelector("button:first-child");
clipboard.addEventListener("click", () => {
GM_setClipboard(textarea.value);
});
const selectAll = content.querySelector("button:last-child");
selectAll.addEventListener("click", () => {
const items = content.querySelectorAll("INPUT");
const checked = [...items].some((item) => !item.checked);
items.forEach((item) => {
item.checked = checked;
});
handleChange();
});
})();
return func;
})();
// 生成菜单
ui.postBtn.d[key] = {
n2: title,
n3: title,
on: () => {
if (window === undefined) {
window = ui.createCommmonWindow();
}
refresh();
window._.addContent(null);
window._.addTitle(title);
window._.addContent(content);
window._.show();
},
ck: () => true,
};
// 写入系统菜单
if (ui.postBtn.all["扩展"].indexOf(key) < 0) {
ui.postBtn.all["扩展"].push(key);
}
};
})();
return () => {
if (ui.postBtn) {
ui.postBtn.all["扩展"] = ui.postBtn.all["扩展"] || [];
// 大召唤术
summon();
}
};
})();
// 加载脚本
(() => {
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 hooked = {
autoPagerize: false,
uniqueTopic: false,
attachmentStyle: false,
pageButtonStyle: false,
hotkeys: false,
forumName: false,
postLossDetection: false,
postLossDetectionTopic: false,
authorOnly: false,
};
const hook = () => {
// 翻页
const loadReadHidden = (() => {
const THREAD_MAX_PAGE = 500;
// const delay = (interval) =>
// new Promise((resolve) => setTimeout(resolve, interval));
// const retry = async (fn, retriesLeft = 10, interval = 160) => {
// try {
// return await fn();
// } catch (error) {
// await delay(interval);
// if (retriesLeft > 0) {
// return await retry(fn, retriesLeft - 1, interval);
// }
// }
// };
return (p, opt = 1) => {
// if (ui.loadReadHidden) {
// retry(() => {
// if (ui.loadReadHidden.lock) {
// throw new Error();
// }
if (__PAGE) {
const max = __PAGE[1];
const cur = __PAGE[2];
if (location.pathname === "/thread.php") {
if (p > THREAD_MAX_PAGE) {
return;
}
if (p === 0 && opt === 2 && cur === THREAD_MAX_PAGE) {
return;
}
}
if (p < 1 && opt === 1) {
return;
}
if (p > max && max > 0) {
p = max;
}
if (p === cur) {
return;
}
// ui.loadReadHidden(p, opt);
// 临时的处理
const anchor = (() => {
if (opt === 2) {
return document.querySelector('[title="加载下一页"]');
}
if (opt === 4) {
return document.querySelector('[title="加载上一页"]');
}
// 获取当前页面参数
const params = new URLSearchParams(location.search);
// 替换 page 参数
if (p > 1) {
params.set("page", p);
} else {
params.delete("page");
}
// 计算目标 URL
const target = location.pathname + "?" + params.toString();
// 返回目标 URL
return document.querySelector(`A[href="${target}"]`);
})();
if (anchor) {
anchor.click();
}
}
// });
// }
};
})();
// 自动翻页
if (hooked.autoPagerize === false) {
if (ui.pageBtn) {
const execute = (() => {
const observer = new IntersectionObserver((entries) => {
if (entries.find((item) => item.isIntersecting)) {
if (
location.pathname === "/thread.php" &&
location.search.indexOf("authorid") > 0
) {
return;
}
loadReadHidden(0, 2);
}
});
return debounce(() => {
const anchor = document.querySelector('[title="加载下一页"]');
if (anchor) {
observer.observe(anchor);
} else {
observer.disconnect();
}
}, 2000);
})();
hookFunction(ui, "pageBtn", execute);
hooked.autoPagerize = true;
execute();
}
}
// 移除重复内容
if (hooked.uniqueTopic === false) {
if (ui.topicArg) {
const execute = () => {
if (location.search.indexOf("searchpost=1") > 0) {
return;
}
ui.topicArg.data = ui.topicArg.data.reduce(
(accumulator, currentValue) => {
if (document.contains(currentValue[0])) {
const index = accumulator.findIndex(
(item) => item[8] === currentValue[8]
);
if (index < 0) {
return [...accumulator, currentValue];
}
currentValue[0].closest("TBODY").style.display = "none";
}
return accumulator;
},
[]
);
};
hookFunction(ui.topicArg, "loadAll", execute);
hooked.uniqueTopic = true;
execute();
}
}
// 附件样式
if (hooked.attachmentStyle === false && attachmentStyleEnable) {
if (ui.topicArg) {
const execute = () => {
const elements =
document.querySelectorAll('[title="主题中有附件"]');
elements.forEach((element) => {
element.className = "block_txt white nobr vertmod";
element.style = "background-color: #BD7E6D";
element.innerHTML = "附件";
});
};
hookFunction(ui.topicArg, "loadAll", execute);
hooked.attachmentStyle = true;
execute();
}
}
// 页码样式
if (hooked.pageButtonStyle === false && pageButtonStyleEnable) {
const execute = () => {
if (ui.pageBtn) {
const elements = document.querySelectorAll('[name="pageball"] A');
elements.forEach((element) => {
const matches = element.innerHTML.match(/\d+/);
if (matches) {
element.innerHTML = ` ${matches[0]} `;
}
});
}
};
hookFunction(ui, "pageBtn", execute);
hooked.pageButtonStyle = true;
execute();
}
// 快捷翻页
if (hooked.hotkeys === false && hotkeysEnable) {
const execute = () => {
document.addEventListener("keydown", ({ key, ctrlKey }) => {
if (__PAGE) {
const max = __PAGE[1];
// const cur = __PAGE[2];
const activeElement = document.activeElement;
if (activeElement === null || activeElement.tagName !== "BODY") {
return;
}
if (key === "ArrowLeft" && ctrlKey) {
loadReadHidden(1);
return;
}
if (key === "ArrowRight" && ctrlKey) {
loadReadHidden(max);
return;
}
if (key === "ArrowLeft") {
// document.getElementById("m_pbtntop").scrollIntoView();
loadReadHidden(0, 4);
return;
}
if (key === "ArrowRight") {
// document.getElementById("m_pbtnbtm").scrollIntoView();
loadReadHidden(0, 2);
return;
}
}
});
};
hooked.hotkeys = true;
execute();
}
// 版面名称
if (hooked.forumName === false && forumNameEnable) {
if (ui.topicArg) {
if (!n.doRequest || !api.indexForumList) {
return;
}
class Queue {
execute(task) {
task(this.data).finally(() => {
if (this.waitingQueue.length) {
const next = this.waitingQueue.shift();
this.execute(next);
} else {
this.isRunning = false;
}
});
}
enqueue(task) {
if (this.initialized === false) {
this.initialized = true;
this.init();
}
if (this.isRunning) {
this.waitingQueue.push(task);
} else {
this.isRunning = true;
this.execute(task);
}
}
init() {
this.enqueue(async () => {
this.data = await new Promise((resolve) => {
try {
n.doRequest({
u: api.indexForumList(),
f: function (res) {
if (res.data) {
resolve(res.data[0]);
} else {
resolve({});
}
},
});
} catch (e) {
resolve({});
}
});
});
}
constructor() {
this.waitingQueue = [];
this.isRunning = false;
this.initialized = false;
}
}
const deepSearch = (content = {}, fid = 0) => {
const children = Object.values(content);
for (let i = 0; i < children.length; i += 1) {
const item = children[i];
if (item.fid === fid) {
return item;
}
if (item.content) {
const result = deepSearch(item.content || [], fid);
if (result !== null) {
return result;
}
}
}
return null;
};
const queue = new Queue();
const execute = () => {
if (location.search.indexOf("authorid") < 0) {
return;
}
ui.topicArg.data.forEach((item) => {
const parentNode = item[1].closest(".c2");
if (parentNode.querySelector(".titleadd2") === null) {
const fid = item[7];
queue.enqueue(async (data) => {
const result = deepSearch(data.all, parseInt(fid, 10));
if (result) {
const anchor = parentNode.querySelector(".topic_content");
const title = document.createElement("SPAN");
title.className = "titleadd2";
title.innerHTML = `<a href="/thread.php?fid=${fid}" class="silver">[${result.name}]</a>`;
if (anchor) {
anchor.before(title);
} else {
parentNode.append(title);
}
}
});
}
});
};
hookFunction(ui.topicArg, "loadAll", execute);
hooked.forumName = true;
execute();
}
}
// 抽楼检测
if (postLossDetectionEnable) {
const cache = {};
const fetchData = async (key, tid, pid) => {
if (cache[key] === undefined) {
cache[key] = await new Promise((resolve) => {
fetch(`/post.php?lite=js&tid=${tid}&pid=${pid}`)
.then((res) => res.blob())
.then((blob) => {
const reader = new FileReader();
reader.onload = () => {
const text = reader.result;
const result = JSON.parse(
text.replace("window.script_muti_get_var_store=", "")
);
const { error } = result;
if (error) {
resolve(error[0]);
} else {
resolve("");
}
};
reader.readAsText(blob, "GBK");
})
.catch(() => {
resolve("");
});
});
}
return cache[key];
};
if (hooked.postLossDetection === false) {
if (ui.postArg && uid) {
const execute = debounce(() => {
if (ui.postArg === undefined) {
return;
}
Object.values(ui.postArg.data).forEach(
async ({ tid, pid, pAid, pInfoC }) => {
if (parseInt(pAid, 10) !== uid) {
return;
}
const key = `${tid}#${pid}`;
const error = await fetchData(key, tid, pid);
if (error) {
if (pInfoC) {
if (pInfoC.querySelector(`[id="${key}"]`)) {
return;
}
const node = document.createElement("SPAN");
node.id = key;
node.className =
"small_colored_text_btn block_txt_c0 stxt";
node.style = "margin-left: 0.4em; line-height: inherit;";
node.innerHTML = error;
pInfoC.prepend(node);
}
}
}
);
}, 2000);
hookFunction(ui, "loadReadHidden", execute);
hooked.postLossDetection = true;
execute();
}
}
if (hooked.postLossDetectionTopic === false) {
if (ui.topicArg && uid) {
const execute = debounce(() => {
Object.values(ui.topicArg.data).forEach(async (item) => {
const tid = item[8];
const pid = item[9] || 0;
const author = item[2];
const authorID =
parseInt(
author.getAttribute("href").match(/uid=(\S+)/)[1],
10
) || 0;
const postDate = item[12];
if (authorID !== uid) {
return;
}
if (tid && postDate) {
const key = `${tid}#${pid}`;
const error = await fetchData(key, tid, pid);
if (error) {
const node = document.createElement("SPAN");
node.id = key;
node.className = "small_colored_text_btn block_txt_c0";
node.innerHTML = error;
const anchor = author.parentNode;
anchor.innerHTML = "";
anchor.appendChild(node);
}
}
});
}, 2000);
hookFunction(ui.topicArg, "loadAll", execute);
hooked.postLossDetectionTopic = true;
execute();
}
}
}
// 只看楼主
if (hooked.authorOnly === false && authorOnlyEnable) {
if (ui.topicBtn) {
const key = 99;
const execute = () => {
if (ui.topicBtn.d[key]) {
return;
}
const anchor = document.querySelector("#postbtop");
if (anchor) {
ui.topicBtn.d[key] = {
n1: "楼主",
n2: "只看楼主",
on: (_, { tid }) => {
const api = `/read.php?tid=${tid}`;
const params = new URLSearchParams(location.search);
// 如果已经是匿名的只看楼主状态,则直接跳转原始页面
if (params.get("opt")) {
location.href = api;
return;
}
// 请求获取顶楼 UID
fetch(api)
.then((res) => res.blob())
.then((blob) => {
const getLastIndex = (content, position) => {
if (position >= 0) {
let nextIndex = position + 1;
while (nextIndex < content.length) {
if (content[nextIndex] === "}") {
return nextIndex;
}
if (content[nextIndex] === "{") {
nextIndex = getLastIndex(content, nextIndex);
if (nextIndex < 0) {
break;
}
}
nextIndex = nextIndex + 1;
}
}
return -1;
};
const reader = new FileReader();
reader.onload = async () => {
const parser = new DOMParser();
const doc = parser.parseFromString(
reader.result,
"text/html"
);
// 验证帖子正常
const verify = doc.querySelector("#m_posts");
if (verify) {
// 取得顶楼 UID
const uid = (() => {
const ele = doc.querySelector("#postauthor0");
if (ele) {
const res = ele
.getAttribute("href")
.match(/uid=(\S+)/);
if (res) {
return res[1];
}
}
return 0;
})();
// 匿名贴
if (uid <= 0) {
location.href = `${api}&opt=512`;
return;
}
// 判断 UID 是否一致
if (uid !== params.get("authorid")) {
location.href = `${api}&authorid=${uid}`;
return;
}
// 跳转原始页面
location.href = api;
}
};
reader.readAsText(blob, "GBK");
});
},
};
ui.topicBtn.def.push(key);
ui.topicBtn.load(anchor);
}
};
hookFunction(ui.topicBtn, "load", execute);
hooked.authorOnly = true;
execute();
}
}
};
hookFunction(ui, "eval", () => {
enhanceMenu();
if (Object.values(hooked).findIndex((item) => item === false) < 0) {
return;
}
hook();
});
hook();
enhanceMenu();
})();
// 加载菜单项
(() => {
if (attachmentStyleEnable) {
GM_registerMenuCommand("附件样式:启用", () => {
GM_setValue(ATTACHMENT_STYLE_ENABLE_KEY, false);
location.reload();
});
} else {
GM_registerMenuCommand("附件样式:禁用", () => {
GM_setValue(ATTACHMENT_STYLE_ENABLE_KEY, true);
location.reload();
});
}
if (pageButtonStyleEnable) {
GM_registerMenuCommand("页码样式:启用", () => {
GM_setValue(PAGE_BUTTON_STYLE_ENABLE_KEY, false);
location.reload();
});
} else {
GM_registerMenuCommand("页码样式:禁用", () => {
GM_setValue(PAGE_BUTTON_STYLE_ENABLE_KEY, true);
location.reload();
});
}
if (hotkeysEnable) {
GM_registerMenuCommand("快捷翻页:启用", () => {
GM_setValue(HOTKEYS_ENABLE_KEY, false);
location.reload();
});
} else {
GM_registerMenuCommand("快捷翻页:禁用", () => {
GM_setValue(HOTKEYS_ENABLE_KEY, true);
location.reload();
});
}
if (forumNameEnable) {
GM_registerMenuCommand("版面名称:启用", () => {
GM_setValue(FORUM_NAME_ENABLE_KEY, false);
location.reload();
});
} else {
GM_registerMenuCommand("版面名称:禁用", () => {
GM_setValue(FORUM_NAME_ENABLE_KEY, true);
location.reload();
});
}
if (postLossDetectionEnable) {
GM_registerMenuCommand("抽楼检测:启用", () => {
GM_setValue(POST_LOSS_DETECTION_KEY, false);
location.reload();
});
} else {
GM_registerMenuCommand("抽楼检测:禁用", () => {
GM_setValue(POST_LOSS_DETECTION_KEY, true);
location.reload();
});
}
if (authorOnlyEnable) {
GM_registerMenuCommand("只看楼主:启用", () => {
GM_setValue(AUTHOR_ONLY_KEY, false);
location.reload();
});
} else {
GM_registerMenuCommand("只看楼主:禁用", () => {
GM_setValue(AUTHOR_ONLY_KEY, true);
location.reload();
});
}
if (autoCheckInEnable) {
GM_registerMenuCommand("自动签到:启用", () => {
GM_setValue(AUTO_CHECK_IN_ENABLE_KEY, false);
GM_setValue(AUTO_CHECK_IN_LAST_TIME_KEY, 0);
location.reload();
});
} else {
GM_registerMenuCommand("自动签到:禁用", () => {
GM_setValue(AUTO_CHECK_IN_ENABLE_KEY, true);
location.reload();
});
}
})();
// 自动签到
if (autoCheckInEnable && uid) {
const today = new Date();
const lastTime = new Date(autoCheckInLastTime);
const isToday =
lastTime.getDate() === today.getDate() &&
lastTime.getMonth() === today.getMonth() &&
lastTime.getFullYear() === today.getFullYear();
if (isToday === false) {
fetch(`/nuke.php?__lib=check_in&__act=check_in&lite=js`, {
method: "POST",
headers: {
"X-User-Agent": autoCheckInUserAgent,
},
})
.then((res) => res.blob())
.then((blob) => {
const reader = new FileReader();
reader.onload = () => {
const text = reader.result;
const result = JSON.parse(
text.replace("window.script_muti_get_var_store=", "")
);
const { data, error } = result;
if (data || error) {
alert((data || error)[0]);
}
GM_setValue(AUTO_CHECK_IN_LAST_TIME_KEY, today.getTime());
};
reader.readAsText(blob, "GBK");
});
}
}
})(commonui, __NUKE, __API, __CURRENT_UID);