Greasy Fork

Greasy Fork is available in English.

NGA Noimg Fix

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

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

// ==UserScript==
// @name              NGA Noimg Fix
// @name:zh-CN        NGA Noimg 修复
// @namespace         http://greasyfork.icu/users/263018
// @version           1.0.0
// @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_addStyle
// @grant             GM_setValue
// @grant             GM_getValue
// @grant             GM_registerMenuCommand
// @grant             unsafeWindow

// @run-at            document-start
// @noframes
// ==/UserScript==

(() => {
  // 声明泥潭主模块、回复模块
  let commonui, replyModule;

  // 缓存,避免重复请求
  const cache = {};

  // 兼容屏蔽脚本
  const observer = new MutationObserver((mutationsList) => {
    mutationsList.forEach(({ target }) => {
      const item = Object.values(replyModule.data).find(
        (item) => item.contentC === target
      );

      if (item) {
        fixReply(item);
      }
    });
  });

  /**
   * 修复无法加载的图片
   * @param {*} content 回复容器
   * @param {*} tid     帖子 ID
   * @param {*} pid     回复 ID
   */
  const fixNoimg = async (content, tid, pid) => {
    // 用正则匹配所有 [noimg] 标记
    const matches = content.innerHTML.match(/\[noimg\]\.(.+?)\[\/noimg\]/g);

    // 没有匹配结果,跳过
    if (matches === null) {
      return;
    }

    // 尝试从缓存里直接读取
    const list = matches.filter((item) => {
      if (cache[item]) {
        // 替换图片
        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 } = item;

    // 修复图片
    await fixNoimg(contentC, tid, pid);

    // 处理引用
    await fixQuote(item);

    // 兼容屏蔽脚本
    if (item.nFilter) {
      observer.observe(contentC, { childList: 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 {};
        })();

        // 修复图片
        await fixNoimg(content, tid, pid);
      })
    );
  };

  /**
   * 处理 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);
      },
    });
  };

  // 主函数
  (async () => {
    // 处理 commonui 模块
    if (unsafeWindow.commonui) {
      handleCommonui(unsafeWindow.commonui);
      return;
    }

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