// ==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("ubbcode")
? target
: target.closest(".ubbcode");
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 result = await Tools.readForumData(response, false);
// 用正则匹配所有 [img] 标记
const imgs = result.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);
},
});
})();
})();