Greasy Fork

Greasy Fork is available in English.

AO3: Chapter Shortcuts

Add shortcuts for first and last chapters on AO3 works. Customize the latest chapter symbol on work titles.

当前为 2025-10-05 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         AO3: Chapter Shortcuts
// @version      1.3
// @description  Add shortcuts for first and last chapters on AO3 works. Customize the latest chapter symbol on work titles.
// @author       BlackBatCat
// @license      MIT
// @match        http://archiveofourown.org/*
// @match        https://archiveofourown.org/*
// @grant        none
// @namespace http://greasyfork.icu/users/1498004
// ==/UserScript==

(function () {
  "use strict";

  // --- SETTINGS STORAGE ---
  const CHAPTER_SHORTCUTS_CONFIG_KEY = "ao3_chapter_shortcuts_config";
  const DEFAULT_CHAPTER_SHORTCUTS_CONFIG = {
    lastChapterSymbol: "»",
  };
  let CHAPTER_SHORTCUTS_CONFIG = { ...DEFAULT_CHAPTER_SHORTCUTS_CONFIG };

  function loadChapterShortcutsConfig() {
    try {
      const saved = localStorage.getItem(CHAPTER_SHORTCUTS_CONFIG_KEY);
      if (saved) {
        CHAPTER_SHORTCUTS_CONFIG = {
          ...DEFAULT_CHAPTER_SHORTCUTS_CONFIG,
          ...JSON.parse(saved),
        };
      }
    } catch (e) {
      console.error("Error loading config:", e);
    }
  }
  function saveChapterShortcutsConfig() {
    try {
      localStorage.setItem(
        CHAPTER_SHORTCUTS_CONFIG_KEY,
        JSON.stringify(CHAPTER_SHORTCUTS_CONFIG)
      );
    } catch (e) {
      console.error("Error saving config:", e);
    }
  }

  // --- SETTINGS MENU ---
  function showChapterShortcutsMenu() {
    document
      .querySelectorAll(".ao3-chapter-shortcuts-menu-dialog")
      .forEach((d) => d.remove());
    // Get AO3 input field background color for seamless skin integration
    let inputBg = "#fffaf5"; // fallback
    const testInput = document.createElement("input");
    document.body.appendChild(testInput);
    try {
      const computedBg = window.getComputedStyle(testInput).backgroundColor;
      if (
        computedBg &&
        computedBg !== "rgba(0, 0, 0, 0)" &&
        computedBg !== "transparent"
      ) {
        inputBg = computedBg;
      }
    } catch (e) {}
    testInput.remove();

    const dialog = document.createElement("div");
    dialog.className = "ao3-chapter-shortcuts-menu-dialog";
    dialog.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: ${inputBg};
            padding: 20px;
            border-radius: 8px;
            box-shadow: 0 0 20px rgba(0,0,0,0.2);
            z-index: 10000;
            width: 90%;
            max-width: 500px;
            max-height: 80vh;
            overflow-y: auto;
            font-family: inherit;
            font-size: inherit;
            color: inherit;
            box-sizing: border-box;
        `;

    // Add CSS for proper link styling
    const style = document.createElement("style");
    style.textContent = `.ao3-chapter-shortcuts-menu-dialog .reset-link { text-align: center; margin-top: 10px; color: inherit; opacity: 0.7; } .ao3-chapter-shortcuts-menu-dialog .reset-link a { text-decoration: none; outline: none; } .ao3-chapter-shortcuts-menu-dialog .reset-link a:hover { opacity: 1; outline: none; }`;
    document.head.appendChild(style);

    dialog.innerHTML = `
            <h3 style="text-align: center; margin-top: 0; color: inherit;">🏃🏻 Chapter Shortcuts Settings 🏃🏻</h3>
            <hr style='margin: 10px 0; border: none; border-top: 1px solid inherit;'>
            <div style="margin-bottom: 20px;">
                <label style="display: block; margin-bottom: 10px; font-family: inherit; color: inherit;">Choose a symbol for the Last Chapter button:</label>
                <div style="display: flex; gap: 10px; margin-bottom: 10px;">
                    <button type="button" class="preset-symbol" data-symbol="»" style="font-family: inherit; font-size: inherit; color: inherit;">»</button>
                    <button type="button" class="preset-symbol" data-symbol="➼" style="font-family: inherit; font-size: inherit; color: inherit;">➼</button>
                    <button type="button" class="preset-symbol" data-symbol="➺" style="font-family: inherit; font-size: inherit; color: inherit;">➺</button>
                    <button type="button" class="preset-symbol" data-symbol="✦" style="font-family: inherit; font-size: inherit; color: inherit;">✦</button>
                    <button type="button" class="preset-symbol" data-symbol="❥" style="font-family: inherit; font-size: inherit; color: inherit;">❥</button>
                    <button type="button" class="preset-symbol" data-symbol="♥︎" style="font-family: inherit; font-size: inherit; color: inherit;">♥︎</button>
                    <button type="button" class="preset-symbol" data-symbol="✿" style="font-family: inherit; font-size: inherit; color: inherit;">✿</button>
                    <button type="button" class="preset-symbol" data-symbol="ɞɞ" style="font-family: inherit; font-size: inherit; color: inherit;">ɞɞ</button>
                </div>
                <label style="display: block; margin-bottom: 5px; font-family: inherit; color: inherit;">Or enter your own:</label>
                <input type="text" id="custom-symbol" value="${CHAPTER_SHORTCUTS_CONFIG.lastChapterSymbol}" maxlength="4" style="width: 100%; padding: 5px; font-size: inherit; font-family: inherit; color: inherit; background: inherit; border: 1px solid inherit; box-sizing: border-box;">
            </div>
            <div style="display: flex; justify-content: space-between; gap: 10px; margin-bottom: 5px;">
                <button id="chapter-shortcuts-save" style="flex: 1; padding: 10px; font-size: 1em; font-family: inherit; color: inherit;">Save</button>
                <button id="chapter-shortcuts-cancel" style="flex: 1; padding: 10px; font-size: 1em; font-family: inherit; color: inherit;">Cancel</button>
            </div>
            <div class="reset-link">
                <a href="#" id="resetShortcutsSettingsLink">Reset to Default Settings</a>
            </div>
        `;
    document.body.appendChild(dialog);
    dialog.querySelectorAll(".preset-symbol").forEach((btn) => {
      btn.addEventListener("click", () => {
        dialog.querySelector("#custom-symbol").value = btn.dataset.symbol;
      });
    });
    dialog
      .querySelector("#chapter-shortcuts-save")
      .addEventListener("click", () => {
        CHAPTER_SHORTCUTS_CONFIG.lastChapterSymbol =
          dialog.querySelector("#custom-symbol").value || "»";
        saveChapterShortcutsConfig();
        dialog.remove();
        // Re-render chapter buttons/links
        addChapterButtons(true);
      });
    dialog
      .querySelector("#chapter-shortcuts-cancel")
      .addEventListener("click", () => {
        dialog.remove();
      });
    dialog
      .querySelector("#resetShortcutsSettingsLink")
      .addEventListener("click", function (e) {
        e.preventDefault();
        CHAPTER_SHORTCUTS_CONFIG = { ...DEFAULT_CHAPTER_SHORTCUTS_CONFIG };
        saveChapterShortcutsConfig();
        dialog.remove();
        addChapterButtons(true);
      });
  }

  // --- GET STORY ID ---
  function getStoryId() {
    const match = window.location.pathname.match(/works\/(\d+)/);
    if (match !== null) {
      return match[1];
    }
    const chapterForm = document.querySelector("#chapter_index li form");
    if (chapterForm && chapterForm.getAttribute("action")) {
      const actionMatch = chapterForm
        .getAttribute("action")
        .match(/works\/(\d+)/);
      if (actionMatch) {
        return actionMatch[1];
      }
    }
    return null;
  }

  // --- ADD CHAPTER BUTTONS & LINKS ---
  function addChapterButtons(forceRerender = false) {
    // Remove any previous custom links/buttons if rerendering
    if (forceRerender) {
      document
        .querySelectorAll("#go_to_last_chap, #go_to_first_chap")
        .forEach((el) => el.remove());
      document
        .querySelectorAll(".ao3-last-chapter-link")
        .forEach((el) => el.remove());
    }
    // Check if we're on a work page with chapter navigation
    const workNav = document.querySelector("ul.work");
    const indexList = document.querySelector("ul.index");
    if (workNav && !indexList) {
      // Add Last Chapter button if not on the last chapter
      if (document.querySelector(".next")) {
        const lastChapterBtn = document.createElement("li");
        lastChapterBtn.id = "go_to_last_chap";
        lastChapterBtn.innerHTML = `<a>Last Chapter</a>`;
        lastChapterBtn.addEventListener("click", function () {
          const select = document.querySelector("#selected_id");
          if (select && select.options.length > 0) {
            const lastChapterId =
              select.options[select.options.length - 1].value;
            window.location.href = `/works/${getStoryId()}/chapters/${lastChapterId}`;
          }
        });
        workNav.prepend(lastChapterBtn);
      }
      // Add First Chapter button if not on the first chapter
      if (document.querySelector(".previous")) {
        const firstChapterBtn = document.createElement("li");
        firstChapterBtn.id = "go_to_first_chap";
        firstChapterBtn.innerHTML = "<a>First Chapter</a>";
        firstChapterBtn.addEventListener("click", function () {
          window.location.href = `/works/${getStoryId()}`;
        });
        workNav.prepend(firstChapterBtn);
      }
    }
    // Add last chapter links to work listings
    if (document.querySelector(".header h4.heading")) {
      const headings = document.querySelectorAll(".header h4.heading");
      headings.forEach((heading) => {
        const link = heading.querySelector("a");
        if (link) {
          const storyPath = link.getAttribute("href");
          const match = storyPath.match(/works\/(\d+)/);
          if (match) {
            const storyId = match[1];
            fetch(`/works/${storyId}/navigate`)
              .then((response) => response.text())
              .then((data) => {
                const parser = new DOMParser();
                const doc = parser.parseFromString(data, "text/html");
                const lastChapterLink = doc.querySelector("ol li:last-child a");
                if (lastChapterLink) {
                  const lastChapterPath = lastChapterLink.getAttribute("href");
                  const lastChapterEl = document.createElement("a");
                  lastChapterEl.href = lastChapterPath;
                  lastChapterEl.title = "Jump to last chapter";
                  lastChapterEl.textContent = ` ${
                    CHAPTER_SHORTCUTS_CONFIG.lastChapterSymbol || "»"
                  }`;
                  lastChapterEl.className = "ao3-last-chapter-link";
                  heading.appendChild(lastChapterEl);
                }
              })
              .catch((error) =>
                console.error("Error fetching chapter data:", error)
              );
          }
        }
      });
    }
  }

  // --- SHARED MENU MANAGEMENT ---
  function initSharedMenu() {
    // Check if menu container exists, create if not
    const menuContainer = document.getElementById("scriptconfig");
    if (!menuContainer) {
      const headerMenu = document.querySelector(
        "ul.primary.navigation.actions"
      );
      const searchItem = headerMenu
        ? headerMenu.querySelector("li.search")
        : null;
      if (!headerMenu || !searchItem) return;

      // Create menu container
      const newMenuContainer = document.createElement("li");
      newMenuContainer.className = "dropdown";
      newMenuContainer.id = "scriptconfig";

      const title = document.createElement("a");
      title.className = "dropdown-toggle";
      title.href = "/";
      title.setAttribute("data-toggle", "dropdown");
      title.setAttribute("data-target", "#");
      title.textContent = "Userscripts";
      newMenuContainer.appendChild(title);

      const menu = document.createElement("ul");
      menu.className = "menu dropdown-menu";
      newMenuContainer.appendChild(menu);

      // Insert before search item
      headerMenu.insertBefore(newMenuContainer, searchItem);
    }

    // Add menu item
    const menu = document.querySelector("#scriptconfig .dropdown-menu");
    if (menu) {
      const menuItem = document.createElement("li");
      const menuLink = document.createElement("a");
      menuLink.href = "javascript:void(0);";
      menuLink.id = "opencfg_chapter_shortcuts";
      menuLink.textContent = "Chapter Shortcuts";
      menuLink.addEventListener("click", showChapterShortcutsMenu);
      menuItem.appendChild(menuLink);
      menu.appendChild(menuItem);
    }
  }

  // --- INITIALIZATION ---
  loadChapterShortcutsConfig();

  // Show startup message
  console.log("[AO3: Chapter Shortcuts] loaded.");

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => {
      addChapterButtons();
      initSharedMenu();
    });
  } else {
    addChapterButtons();
    initSharedMenu();
  }
})();