StoryGraph Plus: Search MAM Buttons (Expanded)

Add "Search MAM" buttons to TheStoryGraph book, series, and browse pages (Title/Series and Title/Series + Author)

当前为 2025-04-27 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         StoryGraph Plus: Search MAM Buttons (Expanded)
// @namespace    https://greasyfork.org/en/users/1457912
// @version      0.4.0
// @description  Add "Search MAM" buttons to TheStoryGraph book, series, and browse pages (Title/Series and Title/Series + Author)
// @author       WilliestWonka
// @match        https://app.thestorygraph.com/books/*
// @match        https://app.thestorygraph.com/series/*
// @match        https://app.thestorygraph.com/browse*
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const maxRetries = 2;
  let retryCount = 0;
  let retryIntervalId = null;

  function createMamButtons(title, author, isSeries = false) {
    console.log("[SG+] Creating MAM buttons for:", title, author, "isSeries:", isSeries);
    const container = document.createElement("div");
    container.className = "mam-button-container flex mt-2 mb-2 space-x-2 w-full";

    const createButton = (text, url) => {
      const button = document.createElement("a");
      button.href = url;
      button.target = "_blank";
      button.textContent = text;
      button.className =
        "py-2 px-2 border-x-2 border-x-darkGrey dark:border-x-darkerGrey " +
        "border-y border-y-darkGrey dark:border-y-darkerGrey border-b-2 " +
        "bg-grey dark:bg-darkestGrey hover:bg-darkGrey dark:hover:bg-darkerGrey " +
        "inline-flex items-center justify-center w-full text-center text-xs " +
        "text-darkerGrey dark:text-lightGrey";

      return button;
    };

    const searchUrl = (query) =>
      `https://www.myanonamouse.net/tor/browse.php?tor[text]=${encodeURIComponent(query)}`;

    if (isSeries) {
      container.appendChild(createButton("Search MAM Series", searchUrl(title)));
      container.appendChild(createButton("Search MAM Series + Author", searchUrl(`${title} ${author}`)));
    } else {
      container.appendChild(createButton("Search MAM Title", searchUrl(title)));
      container.appendChild(createButton("Search MAM Title + Author", searchUrl(`${title} ${author}`)));
    }

    return container;
  }

  function addButtonsIfReady() {
    console.log("[SG+] Checking if buttons should be added...");
    const pathParts = location.pathname.split('/').filter(Boolean);
    const isBookPage = pathParts[0] === "books";
    const isSeriesPage = pathParts[0] === "series";
    const isBrowsePage = pathParts[0] === "browse";

    if (!isBookPage && !isSeriesPage && !isBrowsePage) return false;

    if (isSeriesPage) {
      // Series page: title from h4.page-heading, author from nearby p > a
      const titleElement = document.querySelector("h4.page-heading");
      const authorElement = document.querySelector("p.font-body a[href^='/authors/']");
      const headingContainer = document.querySelector("div.flex.justify-between.items-center.px-1");

      const title = titleElement?.textContent.trim();
      const author = authorElement?.textContent.trim();

      if (title && author && headingContainer && !headingContainer.nextElementSibling?.classList.contains("mam-button-container")) {
        const topButtons = createMamButtons(title, author, true);
        headingContainer.insertAdjacentElement("afterend", topButtons);
        console.log("[SG+] 'Search MAM' series buttons added at top!");
      }
    }

    const containers = document.querySelectorAll("div.book-title-author-and-series");
    console.log("[SG+] Found book containers:", containers.length);
    if (!containers.length) return false;

    let allValid = true;

    containers.forEach(container => {
      if (container.querySelector(".mam-button-container")) return;

      let title = null;
      let author = null;

      const h3 = container.querySelector("h3");
      const h1 = container.querySelector("h1");

      if (isBrowsePage) {
        // Browse page structure: h1 > a (title), p.font-body > a (author)
        const titleLink = h1?.querySelector("a[href*='/books/']");
        const authorLink = container.querySelector("p.font-body a[href*='/authors/']");

        title = titleLink?.textContent.trim() ?? null;
        author = authorLink?.textContent.trim() ?? null;
      } else if (isBookPage) {
        // Book page structure: h3 has title text node, p inside h3 has author link
        if (h3) {
          const firstNode = h3.childNodes[0];
          title = firstNode?.nodeType === Node.TEXT_NODE ? firstNode.textContent.trim() : null;

          const authorLink = h3.querySelector("p.font-body a[href*='/authors/']");
          author = authorLink?.textContent.trim() ?? null;
        }
      } else if (isSeriesPage) {
        // Inside a series, containers are listings of books
        const titleLink = h3?.querySelector("a[href*='/books/']");
        const authorLink = container.querySelector("p.font-body a[href*='/authors/']");

        title = titleLink?.textContent.trim() ?? null;
        author = authorLink?.textContent.trim() ?? null;
      }

      if (!title || !author) {
        console.warn("[SG+] Missing title or author for a container:", container);
        allValid = false;
        return;
      }

      const buttons = createMamButtons(title, author, false);
      container.appendChild(buttons);
    });

    console.log("[SG+] 'Search MAM' buttons added to all containers.");
    return allValid;
  }

  function startUnifiedRetryLoop() {
    clearInterval(retryIntervalId);
    retryCount = 0;

    retryIntervalId = setInterval(() => {
      if (retryCount >= maxRetries) {
        clearInterval(retryIntervalId);
        console.log("[SG+] Max retries reached, stopping.");
        return;
      }

      if (!document.querySelector(".mam-button-container")) {
        console.log(`[SG+] Buttons missing, re-adding... Retry ${retryCount}`);
        const success = addButtonsIfReady();
        retryCount++;
        if (success) {
          clearInterval(retryIntervalId);
        }
      }
    }, 1000);
  }

  function setupNavigationListener() {
    const originalPushState = history.pushState;
    history.pushState = function (...args) {
      originalPushState.apply(this, args);
      window.dispatchEvent(new Event("locationchange"));
    };

    const originalReplaceState = history.replaceState;
    history.replaceState = function (...args) {
      originalReplaceState.apply(this, args);
      window.dispatchEvent(new Event("locationchange"));
    };

    window.addEventListener("popstate", () => {
      window.dispatchEvent(new Event("locationchange"));
    });

    window.addEventListener("locationchange", () => {
      setTimeout(() => {
        startUnifiedRetryLoop();
      }, 300);
    });
  }

  window.addEventListener("load", () => {
    console.log("[SG+] Script loaded.");
    startUnifiedRetryLoop();
    setupNavigationListener();
  });

})();