2048增强插件

2048论坛帖子加载预览图和链接,内容独立显示在标题下方,悬浮图片根据宽高比自动缩放,并智能去除翻页时的重复帖子。悬浮标题1.5秒可加载全部图片。新增付费贴检测。

// ==UserScript==
// @name         2048增强插件
// @namespace    none
// @version      0.4.9
// @description  2048论坛帖子加载预览图和链接,内容独立显示在标题下方,悬浮图片根据宽高比自动缩放,并智能去除翻页时的重复帖子。悬浮标题1.5秒可加载全部图片。新增付费贴检测。
// @author       nonono
// @match        *://2048.cc/*
// @grant        none
// @require      https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js
// @license      MIT
// ==/UserScript==

(function ($) {
  "use strict";

  $(function () {
    const styles = `
            .userscript-content-cell { padding: 5px 0 10px !important; border: none !important; }
            .userscript-image-gallery { margin-top: 8px; display: flex; flex-wrap: wrap; gap: 8px; }
            .userscript-preview-image { height: 200px; width: auto; cursor: pointer; display: block; transition: opacity 0.2s ease; border-radius: 4px; background-color: #f0f0f0; }
            .userscript-preview-image:hover { opacity: 0.8; }
            .userscript-link, .userscript-magnet-link { padding: 5px 0; font-size: 12px; font-weight: normal; word-break: break-all; cursor: pointer; }
            .userscript-no-content-notice { color: #999; font-size: 12px; padding: 10px 0; }
            .userscript-image-count { font-size: 12px; color: #008000; margin-left: 8px; font-weight: normal; }
            #userscript-floating-image { display: none; position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: auto; height: auto; max-width: 95vw; max-height: 95vh; z-index: 99999; border: 5px solid white; box-shadow: 0 10px 30px rgba(0,0,0,0.5); pointer-events: none; border-radius: 5px; }
        `;
    $("head").append(`<style>${styles}</style>`);
    $("body").append(`<img id="userscript-floating-image" src="" />`);

    $(document).on("mouseenter", ".userscript-preview-image", function () {
      const $this = $(this);
      const imageUrl = $this.attr("src");
      const naturalWidth = $this.data("naturalWidth");
      const naturalHeight = $this.data("naturalHeight");
      if (!imageUrl || !naturalWidth || !naturalHeight) return;
      const $floatingImage = $("#userscript-floating-image");
      if (naturalWidth > naturalHeight) {
        $floatingImage.css({ width: "50vw", height: "auto" });
      } else {
        $floatingImage.css({ height: "80vh", width: "auto" });
      }
      $floatingImage.attr("src", imageUrl).stop(true, true).fadeIn(100);
    });

    $(document).on("mouseleave", ".userscript-preview-image", () => {
      $("#userscript-floating-image").stop(true, true).fadeOut(100);
    });

    const ads = [".promo-container", ".nav-container", ".movie-banner", "#ads"];
    $.each(ads, (i, e) => $(e).hide());

    function copyToClipboard(text, element) {
      navigator.clipboard
        .writeText(text)
        .then(() => {
          if (element) {
            const originalText = $(element).text();
            $(element).text("已复制!").css("color", "darkred");
            setTimeout(() => {
              $(element).text(originalText).css("color", "");
            }, 1500);
          }
        })
        .catch((err) => console.error("[2048增强插件] 复制失败:", err));
    }

    if (window.location.href.includes("read.php")) {
      const readObserver = new MutationObserver((mutations) => {
        mutations.forEach((mutation) => {
          if (mutation.type === "childList") {
            $(mutation.addedNodes)
              .find('.f14 a[href*="name="]')
              .addBack('.f14 a[href*="name="]')
              .each(function () {
                const $link = $(this),
                  href = $link.attr("href");
                if (href && href.includes("name=")) {
                  const st = href.substring(href.indexOf("=") + 1);
                  $link.attr(
                    "href",
                    `https://down.dataaps.com/down.php/${st}.torrent`
                  );
                }
              });
          }
        });
      });
      readObserver.observe(document.body, { childList: true, subtree: true });
    }
    const patterns = [".subject", 'th a[href*="tid"]'];
    const pattern = patterns.find((p) => $(p).length > 0);

    if (pattern) {
      function createImageElement(src, container) {
        const MIN_IMG_WIDTH = 100;
        const MIN_IMG_HEIGHT = 100;
        const $previewImg = $('<img class="userscript-preview-image" />');
        $previewImg
          .on("load", function () {
            if (
              this.naturalWidth < MIN_IMG_WIDTH ||
              this.naturalHeight < MIN_IMG_HEIGHT
            ) {
              $(this).remove();
              return;
            }
            $(this).data({
              naturalWidth: this.naturalWidth,
              naturalHeight: this.naturalHeight,
            });
          })
          .on("error", function () {
            $(this).remove();
          });
        container.append($previewImg);
        $previewImg.attr("src", src);
      }

      function expandAndShowAllImages(linkElement) {
        const $linkElement = $(linkElement);
        if ($linkElement.data("all-images-expanded")) {
          return;
        }

        const allImageUrls = $linkElement.data("allImageUrls");
        const $insertionPoint = $linkElement.closest("tr").length
          ? $linkElement.closest("tr")
          : $linkElement.closest("th, .subject");
        const $galleryContainer = $insertionPoint
          .next()
          .find(".userscript-image-gallery");

        if (
          allImageUrls &&
          allImageUrls.length > 0 &&
          $galleryContainer.length > 0
        ) {
          if (allImageUrls.length > $galleryContainer.children("img").length) {
            console.log(
              `[2048增强插件] 悬浮加载全部 ${allImageUrls.length} 张图片...`
            );
            $galleryContainer.empty();
            allImageUrls.forEach((src) => {
              createImageElement(src, $galleryContainer);
            });
            $linkElement.data("all-images-expanded", true);
          }
        }
      }

      function processPostLink(linkElement) {
        const $linkElement = $(linkElement);
        if ($linkElement.hasClass("userscript-processed")) return;

        $linkElement.addClass("userscript-processed");

        $.ajax({
          url: linkElement.href,
          method: "get",
          success: function (data) {
            const $data = $(data);
            const $titleRow = $linkElement.closest("tr");
            const $insertionPoint = $titleRow.length
              ? $titleRow
              : $linkElement.closest("th, .subject");

            // ===== 新增功能:检测付费内容 =====
            const $postContentForCheck = $data.find(".f14");
            if ($postContentForCheck.text().includes("本内容需向作者支付")) {
              $linkElement.before(
                '<span style="color: red; font-weight: bold;">【付费】</span>'
              );
            }
            // =====================================

            const $contentCell = $(
              '<td colspan="5" class="userscript-content-cell"></td>'
            );
            let contentAdded = false;

            const IGNORED_PATHS = [
              "/smilies/",
              "/faces/",
              "/rank/",
              "/level/",
              "thumb-ing.gif",
            ];

            const allImageUrls = $data
              .find('.f14 img, img[iyl-data="adblo_ck.jpg"]')
              .map(function () {
                const $imgNode = $(this);
                let imgSrc = $imgNode.data("original") || this.src;
                if (!imgSrc) return null;
                try {
                  imgSrc = new URL(imgSrc, linkElement.href).href;
                } catch (e) {
                  return null;
                }
                if ($imgNode.closest(".signature, .user-pic").length > 0)
                  return null;
                if (IGNORED_PATHS.some((path) => imgSrc.includes(path)))
                  return null;
                return imgSrc;
              })
              .get();

            const uniqueImageUrls = [...new Set(allImageUrls)];
            const totalImageCount = uniqueImageUrls.length;

            if (totalImageCount > 0) {
              $linkElement.data("allImageUrls", uniqueImageUrls);
            }

            if (totalImageCount > 0) {
              $linkElement.after(
                `<span class="userscript-image-count">[共${totalImageCount}张图片]</span>`
              );
            }

            const magnetText = (() => {
              const $postContent = $data.find(".f14");
              if (!$postContent.length) return null;
              const contentText = $postContent.text();
              const magnetMatch = contentText.match(
                /(magnet:\?xt=urn:btih:[a-f0-9]{32,40})/i
              );
              if (magnetMatch && magnetMatch[0]) return magnetMatch[0];
              const seedCodeMatch = contentText.match(/([A-F0-9]{40})/i);
              if (seedCodeMatch && seedCodeMatch[1])
                return `magnet:?xt=urn:btih:${seedCodeMatch[1]}`;
              return null;
            })();

            const downloadUrl = (() => {
              if (magnetText) return null;

              const $postContent = $data.find(".f14");
              if (!$postContent.length) return null;

              const $downloadLink = $postContent
                .find(
                  'a[href*="bt.bxmho.cn/list.php?name="], a[href*=".torrent"], a[href*="down.php/"]'
                )
                .first();
              if ($downloadLink.length > 0) {
                try {
                  return new URL($downloadLink.attr("href"), linkElement.href)
                    .href;
                } catch (e) {
                  console.warn(
                    "[2048增强插件] 解析下载链接时出错:",
                    $downloadLink.attr("href"),
                    e
                  );
                }
              }

              const contentText = $postContent.text();
              let urlMatch = contentText.match(
                /https?:\/\/bt\.bxmho\.cn\/list\.php\?name=[a-f0-9]+/i
              );
              if (urlMatch && urlMatch[0]) {
                return urlMatch[0];
              }
              const textNodes = $postContent.contents().filter(function () {
                return (
                  this.nodeType === 3 && /下载[网地]址/.test(this.nodeValue)
                );
              });
              if (textNodes.length > 0) {
                const parentText = textNodes.first().parent().text();
                const genericUrlMatch = parentText.match(/https?:\/\/[^\s<]+/);
                if (genericUrlMatch) return genericUrlMatch[0];
              }

              return null;
            })();

            if (magnetText) {
              const $magnetElement = $(
                `<div class="userscript-magnet-link">${magnetText}</div>`
              );
              $magnetElement.on("click", function () {
                copyToClipboard(magnetText, this);
              });
              $contentCell.append($magnetElement);
              contentAdded = true;
            }

            if (downloadUrl) {
              const $link = $(
                `<div class="userscript-link">${downloadUrl}</div>`
              );
              $link.on("click", function () {
                window.open(downloadUrl, "_blank");
                copyToClipboard(downloadUrl, this);
              });
              $contentCell.append($link);
              contentAdded = true;
            }

            if (totalImageCount > 0) {
              const imageLimit = 3;
              const $galleryContainer = $(
                '<div class="userscript-image-gallery"></div>'
              );
              uniqueImageUrls.slice(0, imageLimit).forEach((src) => {
                createImageElement(src, $galleryContainer);
              });

              $contentCell.append($galleryContainer);
              contentAdded = true;
            }

            if (!contentAdded) {
              $contentCell.append(
                '<div class="userscript-no-content-notice">[未在本帖中找到有效的图片或下载链接]</div>'
              );
            }

            const $finalContainer = $insertionPoint.is("tr")
              ? $("<tr></tr>").append($contentCell)
              : $contentCell;
            $insertionPoint.after($finalContainer);
          },
          error: function () {
            $linkElement.addClass("userscript-processed-error");
          },
        });
      }

      const observerOptions = {
        root: null,
        rootMargin: "300px 0px",
        threshold: 0.01,
      };
      const observer = new IntersectionObserver((entries, obs) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            processPostLink(entry.target);
            obs.unobserve(entry.target);
          }
        });
      }, observerOptions);

      const SESSION_KEY = "processedTids";
      const getUrlParam = (url, param) => {
        try {
          const urlObj = new URL(url);
          const page = urlObj.searchParams.get(param);
          return page ? parseInt(page, 10) : 1;
        } catch (e) {
          return 1;
        }
      };

      let processedTids;
      try {
        const storedTids = sessionStorage.getItem(SESSION_KEY);
        processedTids = new Set(storedTids ? JSON.parse(storedTids) : []);
      } catch (e) {
        processedTids = new Set();
      }

      const currentPage = getUrlParam(window.location.href, "page");
      const referrerPage = getUrlParam(document.referrer, "page");

      if (
        (!document.referrer.includes("thread.php") &&
          !document.referrer.includes("search.php")) ||
        currentPage <= referrerPage
      ) {
        processedTids.clear();
        console.log("[2048增强插件] 检测到后退或新会话,TID记录已重置。");
      }

      $(window).on("beforeunload", function () {
        try {
          sessionStorage.setItem(
            SESSION_KEY,
            JSON.stringify([...processedTids])
          );
        } catch (e) {
          console.error("[2048增强插件] 保存TID记录失败:", e);
        }
      });

      const $allPostLinks = $(pattern);
      const totalPostCount = $allPostLinks.length;
      let hiddenPostCount = 0;
      const hiddenPostElements = [];

      $allPostLinks.each(function () {
        const $link = $(this);
        const href = $link.attr("href");
        const tidMatch = href ? href.match(/tid=(\d+)/) : null;

        if (tidMatch) {
          const tid = tidMatch[1];
          if (processedTids.has(tid)) {
            $link.closest("tr").hide();
            hiddenPostCount++;
            hiddenPostElements.push(this);
            return;
          }
          processedTids.add(tid);
        }

        if (!$link.hasClass("userscript-processed")) {
          observer.observe(this);
        }

        let hoverTimer;
        $link
          .on("mouseenter", () => {
            if (!$link.hasClass("userscript-processed")) {
              return;
            }
            hoverTimer = setTimeout(() => {
              expandAndShowAllImages($link[0]);
            }, 1000);
          })
          .on("mouseleave", () => {
            clearTimeout(hoverTimer);
          });
      });

      if (hiddenPostCount > 0) {
        console.log(
          `[2048增强插件] 当前页面存在 ${hiddenPostCount} 个重复帖子,已隐藏。`
        );
      }

      const visiblePostCount = totalPostCount - hiddenPostCount;
      if (
        totalPostCount > 0 &&
        (hiddenPostCount === totalPostCount || visiblePostCount < 5)
      ) {
        console.warn(
          `[2048增强插件] 异常去重检测! 总帖:${totalPostCount}, 隐藏:${hiddenPostCount}。触发恢复机制。`
        );
        processedTids.clear();
        sessionStorage.removeItem(SESSION_KEY);
        console.log("[2048增强插件] 已清空所有TID记录。");
        $(hiddenPostElements).each(function () {
          const linkElement = this;
          $(linkElement).closest("tr").show();
          observer.observe(linkElement);
        });
        console.log(
          `[2048增强插件] 已恢复显示 ${hiddenPostElements.length} 个帖子并为其启用内容加载。`
        );
      }
    }
  });
})(jQuery);