Greasy Fork

Greasy Fork is available in English.

NGA Noimg Fix

尝试将泥潭无法加载的图片修复

当前为 2024-07-12 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name              NGA Noimg Fix
// @name:zh-CN        NGA Noimg 修复
// @namespace         http://greasyfork.icu/users/263018
// @version           1.1.1
// @author            snyssss
// @description       尝试将泥潭无法加载的图片修复
// @description:zh-cn 尝试将泥潭无法加载的图片修复
// @license           MIT

// @match             *://bbs.nga.cn/*
// @match             *://ngabbs.com/*
// @match             *://nga.178.com/*

// @require           https://update.greasyfork.icu/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 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((item) => {
      fixReply(item);
    });
  });

  /**
   * 修复无法加载的图片
   * @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 time = new Date(postTime * 1000);

    // 尝试从缓存里直接读取
    const list = matches.filter((item) => {
      // 缓存模式
      if (cache[item]) {
        content.innerHTML = content.innerHTML.replace(
          item,
          `<img src="${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);

        // 写入缓存
        cache[item] = fullSrc;

        // 替换图片
        content.innerHTML = content.innerHTML.replace(
          item,
          `<img src="${cache[item]}"/>`
        );

        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 label = list[i];

      // 取得 Noimg 里的图片地址
      const src = label.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);

        // 写入缓存
        cache[label] = fullSrc;

        // 替换图片
        content.innerHTML = content.innerHTML.replace(
          label,
          `<img src="${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, content, 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 = () => {
    GM_registerMenuCommand(`极速模式:${FAST_MODE ? "是" : "否"}`, () => {
      if (
        FAST_MODE === false &&
        confirm(
          `是否开启极速模式?\n极速模式即为不请求原文,而是根据发帖时间推测图片地址。\n对于复制他人图片链接至帖子里的解析可能会失败。`
        ) === false
      ) {
        return;
      }

      GM_setValue("FAST_MODE", !FAST_MODE);

      location.reload();
    });
  };

  // 主函数
  (async () => {
    // 注册脚本菜单
    registerMenu();

    // 处理 commonui 模块
    if (unsafeWindow.commonui) {
      handleCommonui(unsafeWindow.commonui);
      return;
    }

    Tools.interceptProperty(unsafeWindow, "commonui", {
      afterSet: (value) => {
        handleCommonui(value);
      },
    });
  })();
})();