尝试将泥潭无法加载的图片修复
// ==UserScript== // @name NGA Noimg Fix // @name:zh-CN NGA Noimg 修复 // @namespace https://greasyfork.org/users/263018 // @version 1.2.0 // @author snyssss // @description 尝试将泥潭无法加载的图片修复 // @description:zh-cn 尝试将泥潭无法加载的图片修复 // @license MIT // @match *://bbs.nga.cn/* // @match *://ngabbs.com/* // @match *://nga.178.com/* // @require https://update.greasyfork.org/scripts/486070/1405682/NGA%20Library.js // @grant GM_setValue // @grant GM_getValue // @grant GM_registerMenuCommand // @grant unsafeWindow // @run-at document-start // @noframes // ==/UserScript== (() => { // 声明泥潭主模块、回复模块 let commonui, replyModule; // 急速模式 const FAST_MODE_KEY = "FAST_MODE"; const FAST_MODE = GM_getValue(FAST_MODE_KEY, true); // 图片属性 const IMG_ATTRS_KEY = "IMG_ATTRS"; const IMG_ATTRS = GM_getValue(IMG_ATTRS_KEY, { style: "max-width: 100%" }); // 缓存,避免重复请求 const cache = {}; // 监听元素变化并重新修复 const observer = new MutationObserver((mutationsList) => { const list = []; mutationsList.forEach(({ target }) => { const content = target.classList.contains("u###ode") ? target : target.closest(".u###ode"); const item = Object.values(replyModule.data).find( (item) => item.contentC === content ); if (item && list.includes(item) === false) { list.push(item); } }); list.forEach(fixReply); }); /** * 修复无法加载的图片 * @param {*} tid 帖子 ID * @param {*} pid 回复 ID * @param {*} content 回复容器 * @param {*} postTime 回复时间 */ const fixNoimg = async (tid, pid, content, postTime) => { // 用正则匹配所有 [noimg] 标记 const matches = content.innerHTML.match(/\[noimg\]\.(.+?)\[\/noimg\]/g); // 没有匹配结果,跳过 if (matches === null) { return; } // 替换图片方法 const replace = (key, value) => { // 写入缓存 cache[key] = value; // 生成图片 const img = document.createElement("img"); // 设置图片属性 Object.entries({ ...IMG_ATTRS, src: value, }).forEach(([key, value]) => { img.setAttribute(key, value); }); // 替换图片 content.innerHTML = content.innerHTML.replace(key, img.outerHTML); }; // 转换时间戳至时间 const time = new Date(postTime * 1000); // 尝试从缓存里直接读取 const list = matches.filter((item) => { // 缓存模式 if (cache[item]) { replace(item, cache[item]); return false; } // 极速模式 if (FAST_MODE) { // 取得 Noimg 里的图片地址 const src = item.replace(/\[noimg\]\.(.+?)\[\/noimg\]/, "$1"); // 加入时间前缀 const realSrc = `./mon_` + `${time.getFullYear()}` + `${String(time.getMonth() + 1).padStart(2, "0")}/` + `${String(time.getDate()).padStart(2, "0")}` + `${src}`; // 计算完整的图片地址 const fullSrc = commonui.correctAttachUrl(realSrc); // 替换图片 replace(item, fullSrc); return false; } return true; }); // 无需再次修复 if (list.length === 0) { return; } // 尝试请求带有正确图片地址的回复原文 const url = `/post.php?action=quote&tid=${tid}&pid=${pid}&lite=js`; const response = await fetch(url); const r###lt = await Tools.readForumData(response, false); // 用正则匹配所有 [img] 标记 const imgs = r###lt.match(/\[img\](.+?)\[\/img\]/g) || []; // 声明前缀 let prefix = ""; // 对比图片结果,修复无法加载的图片 for (let i = 0; i < list.length; i += 1) { const item = list[i]; // 取得 Noimg 里的图片地址 const src = item.replace(/\[noimg\]\.(.+?)\[\/noimg\]/, "$1"); // 取得原文里的图片地址 const realSrc = (() => { const img = imgs.find((item) => item.indexOf(src) > 0); // 引用会超字数限制,我们姑且认为所有图片都是在同一时间内发出的 // 如果有图片,更新前缀,反之直接使用前一个前缀 if (img) { prefix = img.replace(/\[img\](.+?)\[\/img\]/, "$1").replace(src, ""); } // 返回结果 if (prefix) { return `${prefix}${src}`; } })(); // 如果有图片地址,修复 if (realSrc) { // 计算完整的图片地址 const fullSrc = commonui.correctAttachUrl(realSrc); // 替换图片 replace(item, fullSrc); } } }; /** * 修复回复 * @param {*} item 回复内容,见 commonui.postArg.data */ const fixReply = async (item) => { // 跳过泥潭增加的额外内容 if (Tools.getType(item) !== "object") { return; } // 获取帖子 ID、回复 ID、内容、回复时间 const { tid, pid, contentC, postTime } = item; // 处理引用 await fixQuote(item); // 修复图片 await fixNoimg(tid, pid, contentC, postTime); // 监听元素变化并重新修复 // 兼容屏蔽脚本 observer.observe(contentC, { childList: true, subtree: true }); }; /** * 修复引用 * @param {*} item 回复内容,见 commonui.postArg.data */ const fixQuote = async (item) => { // 跳过泥潭增加的额外内容 if (Tools.getType(item) !== "object") { return; } // 获取内容 const content = item.contentC; // 找到所有引用 const quotes = content.querySelectorAll(".quote"); // 处理引用 await Promise.all( [...quotes].map(async (quote) => { const { tid, pid } = (() => { const ele = quote.querySelector("[title='快速浏览这个帖子']"); if (ele) { const res = ele .getAttribute("onclick") .match(/fastViewPost(.+,(\S+),(\S+|undefined),.+)/); if (res) { return { tid: parseInt(res[2], 10), pid: parseInt(res[3], 10) || 0, }; } } return {}; })(); const timeElement = quote.querySelector(".xtxt"); const time = timeElement ? timeElement.innerHTML.replace(/\((.+)\)/, "$1") : null; if (time) { // 转换为泥潭的时间戳 const postTime = new Date(time).getTime() / 1000; // 修复图片 await fixNoimg(tid, pid, quote, postTime); } }) ); }; /** * 处理 postArg 模块 * @param {*} value commonui.postArg */ const handleReplyModule = async (value) => { // 绑定回复模块 replyModule = value; if (value === undefined) { return; } // 修复 const afterGet = (_, args) => { // 楼层号 const index = args[0]; // 找到对应数据 const data = replyModule.data[index]; // 开始修复 if (data) { fixReply(data); } }; // 如果已经有数据,则直接修复 Object.values(replyModule.data).forEach(fixReply); // 拦截 proc 函数,这是泥潭的回复添加事件 Tools.interceptProperty(replyModule, "proc", { afterGet, }); }; /** * 处理 commonui 模块 * @param {*} value commonui */ const handleCommonui = (value) => { // 绑定主模块 commonui = value; // 拦截 postArg 模块,这是泥潭的回复入口 Tools.interceptProperty(commonui, "postArg", { afterSet: (value) => { handleReplyModule(value); }, }); }; /** * 注册脚本菜单 */ const registerMenu = () => { // 极速模式 { const func = () => { if ( FAST_MODE === false && confirm( `是否开启极速模式?\n极速模式即为不请求原文,而是根据发帖时间推测图片地址。\n对于复制他人图片链接至帖子里的解析可能会失败。` ) === false ) { return; } GM_setValue(FAST_MODE_KEY, !FAST_MODE); location.reload(); }; GM_registerMenuCommand(`极速模式:${FAST_MODE ? "是" : "否"}`, func); } // 图片属性 { const func = () => { const attr = prompt( `给图片添加额外的属性或样式`, JSON.stringify(IMG_ATTRS) ); if ((attr || "").length > 0) { try { const newValue = JSON.parse(attr); if (Tools.getType(newValue) !== "object") { throw new Error(); } GM_setValue(IMG_ATTRS_KEY, newValue); location.reload(); } catch { func(); } } }; GM_registerMenuCommand(`图片属性`, func); } }; // 主函数 (async () => { // 注册脚本菜单 registerMenu(); // 处理 commonui 模块 if (unsafeWindow.commonui) { handleCommonui(unsafeWindow.commonui); return; } Tools.interceptProperty(unsafeWindow, "commonui", { afterSet: (value) => { handleCommonui(value); }, }); })(); })();