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 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

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

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
  });

})();