AO3: Site Wizard

Make AO3 easier to read: customize fonts and sizes across the entire site, adjust work reader margins, fix spacing issues, and configure text alignment preferences.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name          AO3: Site Wizard
// @version       3.5
// @description   Make AO3 easier to read: customize fonts and sizes across the entire site, adjust work reader margins, fix spacing issues, and configure text alignment preferences.
// @author        Blackbatcat
// @match         *://archiveofourown.org/*
// @license       MIT
// @require       https://update.greasyfork.org/scripts/552743/1690921/AO3%3A%20Menu%20Helpers%20Library.js?v=2.1.2
// @grant         none
// @run-at        document-start
// @namespace https://greasyfork.org/users/1498004
// ==/UserScript==

(function () {
  "use strict";

  // --- CONSTANTS ---
  const FORMATTER_CONFIG_KEY = "ao3_wizard_config";
  const DEFAULT_FORMATTER_CONFIG = {
    paragraphWidthPercent: 70,
    paragraphFontSizePercent: 100,
    paragraphTextAlign: "left",
    paragraphFontFamily: "",
    fixParagraphSpacing: true,
    paragraphGap: 1.286,
    siteFontFamily: "",
    siteFontWeight: "",
    siteFontSizePercent: 100,
    headerFontFamily: "",
    headerFontWeight: "",
    codeFontFamily: "",
    codeFontStyle: "normal",
    codeFontSize: "",
    expandCodeFontUsage: false,
  };

  const WORKS_PAGE_REGEX =
    /^https?:\/\/archiveofourown\.org\/(?:.*\/)?(works|chapters)(\/|$)/;

  // --- STATE ---
  let FORMATTER_CONFIG = { ...DEFAULT_FORMATTER_CONFIG };
  let cachedElements = {
    paraStyle: null,
    siteStyle: null,
  };

  // --- UTILITIES ---
  function getOrCreateStyle(id) {
    if (!document.head) return null;
    let style = document.getElementById(id);
    if (!style) {
      style = document.createElement("style");
      style.id = id;
      document.head.appendChild(style);
    }
    return style;
  }

  function loadFormatterConfig() {
    try {
      const saved = localStorage.getItem(FORMATTER_CONFIG_KEY);
      if (saved) {
        FORMATTER_CONFIG = {
          ...DEFAULT_FORMATTER_CONFIG,
          ...JSON.parse(saved),
        };
      }
    } catch (e) {
      console.error("Error loading config:", e);
    }
  }

  function saveFormatterConfig() {
    try {
      localStorage.setItem(
        FORMATTER_CONFIG_KEY,
        JSON.stringify(FORMATTER_CONFIG)
      );
    } catch (e) {
      console.error("Error saving config:", e);
    }
  }

  // --- APPLY STYLES ---
  function applyParagraphWidth() {
    if (!cachedElements.paraStyle) {
      cachedElements.paraStyle = getOrCreateStyle(
        "ao3-formatter-paragraph-style"
      );
      if (!cachedElements.paraStyle) return;
    }

    if (WORKS_PAGE_REGEX.test(window.location.href)) {
      const {
        paragraphWidthPercent,
        paragraphFontSizePercent,
        paragraphTextAlign,
        paragraphGap,
      } = FORMATTER_CONFIG;

      cachedElements.paraStyle.textContent = `
        #workskin p { text-align: ${paragraphTextAlign} !important; }
        ${
          paragraphTextAlign === "justify" || paragraphTextAlign === "left"
            ? `#workskin dd { text-align: ${paragraphTextAlign} !important; }`
            : ""
        }
        ${
          paragraphTextAlign === "justify" || paragraphTextAlign === "left"
            ? `#workskin blockquote { text-align: ${paragraphTextAlign} !important; }`
            : ""
        }
        #workskin {
          max-width: ${paragraphWidthPercent}vw !important;
          font-size: ${paragraphFontSizePercent}% !important;
        }
        #workskin p {
          margin-bottom: ${paragraphGap}em !important;
        }
        #workskin p[align] {
          text-align: ${paragraphTextAlign} !important;
        }
        ${
          paragraphTextAlign === "right"
            ? `
        #workskin ul, #workskin ol {
          direction: rtl !important;
          text-align: right !important;
        }
        #workskin li {
          text-align: right !important;
        }
        #workskin dl {
          direction: rtl !important;
        }
        #workskin dt, #workskin dd {
          text-align: right !important;
        }
        #workskin blockquote {
          text-align: right !important;
        }
        #workskin summary {
          text-align: right !important;
        }
        #workskin h1, #workskin h2, #workskin h3,
        #workskin h4, #workskin h5, #workskin h6 {
          text-align: right !important;
        }
        `
            : ""
        }
      `;

      const workskin = document.getElementById("workskin");
      if (workskin) {
        if (paragraphTextAlign === "right") {
          workskin.setAttribute("dir", "rtl");
        } else {
          workskin.removeAttribute("dir");
        }
      }
    } else {
      cachedElements.paraStyle.textContent = "";
    }

    applySiteWideStyles();
  }

  function applySiteWideStyles() {
    if (!cachedElements.siteStyle) {
      cachedElements.siteStyle = getOrCreateStyle("ao3-sitewide-style");
      if (!cachedElements.siteStyle) return;
    }

    const {
      siteFontSizePercent,
      siteFontFamily,
      siteFontWeight,
      headerFontFamily,
      headerFontWeight,
      paragraphFontFamily,
      codeFontFamily,
      codeFontStyle,
      codeFontSize,
      expandCodeFontUsage,
    } = FORMATTER_CONFIG;

    const rules = [];

    rules.push(`html { font-size: ${siteFontSizePercent}% !important; }`);

    if (siteFontFamily) {
      if (expandCodeFontUsage) {
        rules.push(
          `body, body *:not(textarea):not(textarea *):not(code):not(pre):not(tt):not(kbd):not(samp):not(var), input:not([type="file"]), select, button:not(.comment-format button):not(ul.comment-format button) { font-family: ${siteFontFamily} !important; }`
        );
      } else {
        rules.push(
          `body, body *:not(code):not(pre):not(tt):not(kbd):not(samp):not(var), input:not([type="file"]), textarea:not(#skin_css):not(#floaty-textarea), select, button:not(.comment-format button):not(ul.comment-format button) { font-family: ${siteFontFamily} !important; }`
        );
      }
    }

    if (siteFontWeight) {
      const textareaSelector = expandCodeFontUsage
        ? ""
        : ", textarea:not(#skin_css):not(#floaty-textarea)";

      rules.push(
        `body, body *, input:not([type="file"])${textareaSelector}, select, button:not(.comment-format button):not(ul.comment-format button) { font-weight: ${siteFontWeight} !important; }`
      );
    }

    if (paragraphFontFamily) {
      const textareaExclusion = expandCodeFontUsage ? ":not(textarea)" : "";

      if (headerFontFamily) {
        rules.push(
          `#workskin:not(h1):not(h2):not(h3):not(h4):not(h5):not(h6),
           #workskin *:not(code):not(pre):not(tt):not(kbd):not(samp):not(var):not(h1):not(h2):not(h3):not(h4):not(h5):not(h6):not(h1 *):not(h2 *):not(h3 *):not(h4 *):not(h5 *):not(h6 *)${textareaExclusion} { font-family: ${paragraphFontFamily} !important; }`
        );
      } else {
        rules.push(
          `#workskin, #workskin *:not(code):not(pre):not(tt):not(kbd):not(samp):not(var)${textareaExclusion} { font-family: ${paragraphFontFamily} !important; }`
        );
      }
    }

    if (headerFontFamily) {
      rules.push(
        `h1, h1 *, h2, h2 *, h3, h3 *, h4, h4 *, h5, h5 *, h6, h6 *, .heading, .heading *,
         #workskin h1, #workskin h1 *, #workskin h2, #workskin h2 *, #workskin h3, #workskin h3 *,
         #workskin h4, #workskin h4 *, #workskin h5, #workskin h5 *, #workskin h6, #workskin h6 * { font-family: ${headerFontFamily} !important; }`
      );
    } else if (paragraphFontFamily) {
      rules.push(
        `#chapters h3.title,
         #chapters h3.byline.heading,
         .chapter .preface h3.title,
         .chapter .preface h3.byline.heading,
         .preface h3.title,
         .preface h3.byline { font-family: ${paragraphFontFamily} !important; }`
      );
    }

    if (headerFontWeight) {
      rules.push(
        `h1, h1 *, h2, h2 *, h3, h3 *, h4, h4 *, h5, h5 *, h6, h6 *, .heading, .heading *,
         #workskin h1, #workskin h1 *, #workskin h2, #workskin h2 *, #workskin h3, #workskin h3 *,
         #workskin h4, #workskin h4 *, #workskin h5, #workskin h5 *, #workskin h6, #workskin h6 * { font-weight: ${headerFontWeight} !important; }`
      );
    }

    const codeRules = [];
    if (codeFontFamily)
      codeRules.push(`font-family: ${codeFontFamily} !important`);
    if (codeFontStyle && codeFontStyle !== "normal")
      codeRules.push(`font-style: ${codeFontStyle} !important`);
    if (codeFontSize) codeRules.push(`font-size: ${codeFontSize} !important`);

    if (codeRules.length > 0) {
      const baseCodeSelectors =
        "code, code *, pre, pre *, tt, tt *, kbd, kbd *, samp, samp *, var, var *, textarea#skin_css, .css.module blockquote pre, #floaty-textarea, #workskin code, #workskin code *, #workskin pre, #workskin pre *, #workskin tt, #workskin tt *, #workskin kbd, #workskin kbd *, #workskin samp, #workskin samp *, #workskin var, #workskin var *";

      const codeSelectors = expandCodeFontUsage
        ? "code, code *, pre, pre *, tt, tt *, kbd, kbd *, samp, samp *, var, var *, textarea, textarea#skin_css, .css.module blockquote pre, #floaty-textarea, #workskin code, #workskin code *, #workskin pre, #workskin pre *, #workskin tt, #workskin tt *, #workskin kbd, #workskin kbd *, #workskin samp, #workskin samp *, #workskin var, #workskin var *, #workskin textarea"
        : baseCodeSelectors;

      rules.push(`${codeSelectors} { ${codeRules.join("; ")}; }`);
    }

    if (codeRules.length === 0) {
      rules.push(
        `code, code *, pre, pre *, tt, tt *, kbd, kbd *, samp, samp *, var, var *, #workskin code, #workskin code *, #workskin pre, #workskin pre *, #workskin tt, #workskin tt *, #workskin kbd, #workskin kbd *, #workskin samp, #workskin samp *, #workskin var, #workskin var * { font-family: monospace !important; }`
      );

      if (expandCodeFontUsage) {
        rules.push(
          `textarea, #workskin textarea { font-family: monospace !important; }`
        );
      }
    }

    rules.push(
      `#workskin .preface .title.heading,
       #workskin .preface .byline.heading,
       #workskin .preface .title,
       #workskin .preface .byline,
       #workskin .title.heading,
       #workskin .byline.heading {
         text-align: center !important;
         direction: ltr !important;
       }`
    );

    rules.push(
      `#workskin pre {
         text-align: left !important;
         direction: ltr !important;
       }`
    );

    rules.push(
      `#cmtFmtDialog #stdbutton label, ul.comment-format, ul.comment-format * { font-family: "FontAwesome", sans-serif !important; font-weight: normal !important; }`,
      `ul.actions.comment-format { text-align: left !important; }`
    );

    cachedElements.siteStyle.textContent = rules.join("\n");
  }

  // --- PARAGRAPH SPACING FIX ---
  const fixParagraphSpacing = (() => {
    function removeLeadingBrs(userstuff) {
      userstuff.querySelectorAll("p").forEach((p) => {
        let changed = true;
        while (changed) {
          changed = false;
          if (p.firstChild?.tagName === "BR") {
            p.firstChild.remove();
            changed = true;
          } else if (
            p.firstChild?.nodeType === Node.TEXT_NODE &&
            !p.firstChild.textContent.trim()
          ) {
            p.firstChild.remove();
            changed = true;
          }
        }
      });
    }

    function removeTrailingBrs(userstuff) {
      userstuff.querySelectorAll("p").forEach((p) => {
        let changed = true;
        while (changed) {
          changed = false;
          if (p.lastChild?.tagName === "BR") {
            p.lastChild.remove();
            changed = true;
          } else if (
            p.lastChild?.nodeType === Node.TEXT_NODE &&
            !p.lastChild.textContent.trim()
          ) {
            p.lastChild.remove();
            changed = true;
          }
        }
      });
    }

    function removeEmptyParagraphs(userstuff) {
      userstuff.querySelectorAll("p").forEach((p) => {
        const content = p.textContent?.replace(/\u00A0/g, "").trim();
        if (!content && !p.querySelector("img, embed, iframe, video, br")) {
          p.remove();
        }
      });
    }

    function removeEmptyElement(el) {
      const content = el.textContent?.replace(/\u00A0/g, "").trim();
      if (
        !content &&
        el.tagName !== "BR" &&
        el.tagName !== "HR" &&
        !el.querySelector("img, embed, iframe, video")
      ) {
        el.remove();
      }
    }

    function reduceBrs(userstuff) {
      const brs = Array.from(userstuff.querySelectorAll("br"));

      for (let i = 0; i < brs.length; i++) {
        const br = brs[i];
        let consecutiveCount = 1;
        let nextNode = br.nextSibling;

        while (nextNode) {
          if (
            nextNode.nodeType === Node.ELEMENT_NODE &&
            nextNode.tagName === "BR"
          ) {
            consecutiveCount++;
            nextNode = nextNode.nextSibling;
          } else if (
            nextNode.nodeType === Node.TEXT_NODE &&
            !nextNode.textContent.trim()
          ) {
            nextNode = nextNode.nextSibling;
          } else {
            break;
          }
        }

        if (consecutiveCount >= 3) {
          let toRemove = consecutiveCount - 2;
          nextNode = br.nextSibling;

          while (toRemove > 0 && nextNode) {
            const current = nextNode;
            nextNode = nextNode.nextSibling;

            if (
              current.nodeType === Node.ELEMENT_NODE &&
              current.tagName === "BR"
            ) {
              current.remove();
              toRemove--;
            }
          }
        }
      }
    }

    const BLOCK_TAGS = [
      "div",
      "blockquote",
      "ul",
      "ol",
      "table",
      "h1",
      "h2",
      "h3",
      "h4",
      "h5",
      "h6",
    ];

    return function () {
      if (!WORKS_PAGE_REGEX.test(window.location.href)) return;

      document
        .querySelectorAll(
          "#workskin .userstuff:not([data-formatter-spacing-fixed])"
        )
        .forEach((userstuff) => {
          userstuff.setAttribute("data-formatter-spacing-fixed", "true");

          removeLeadingBrs(userstuff);
          removeTrailingBrs(userstuff);
          removeEmptyParagraphs(userstuff);

          BLOCK_TAGS.forEach((tag) => {
            userstuff.querySelectorAll(tag).forEach((child) => {
              removeEmptyElement(child);
            });
          });

          reduceBrs(userstuff);
        });
    };
  })();

  // --- SETTINGS MENU ---
  function showFormatterMenu() {
    // Safety check: ensure library is loaded
    if (!window.AO3MenuHelpers) {
      console.error("[AO3: Site Wizard] Menu Helpers library not loaded");
      alert(
        "Error: Menu Helpers library not loaded. Please check your userscript manager."
      );
      return;
    }

    window.AO3MenuHelpers.removeAllDialogs();

    const dialog = window.AO3MenuHelpers.createDialog(
      "🪄 Site Wizard Settings 🪄",
      {
        maxWidth: "700px",
      }
    );

    // Site-Wide Display Section
    const siteSection = window.AO3MenuHelpers.createSection(
      "📱 Site-Wide Display"
    );

    const siteFontSize = window.AO3MenuHelpers.createSliderWithValue({
      id: "site-fontsize-input",
      label: "Base Font Size",
      min: 50,
      max: 200,
      step: 5,
      value: FORMATTER_CONFIG.siteFontSizePercent,
      unit: "%",
      tooltip:
        "Adjust the overall text size for the entire site (percentage of browser default)",
    });
    siteSection.appendChild(siteFontSize);

    const siteFontFamily = window.AO3MenuHelpers.createTextInput({
      id: "site-fontfamily-input",
      label: "General Text Font",
      value: FORMATTER_CONFIG.siteFontFamily,
      placeholder: "Figtree, sans-serif",
      tooltip: "Font for most site text",
    });

    const siteFontWeight = window.AO3MenuHelpers.createTextInput({
      id: "site-fontweight-input",
      label: "Font Weight",
      value: FORMATTER_CONFIG.siteFontWeight,
      placeholder: "400, normal",
      tooltip: "Boldness of general text",
    });

    const siteFontRow = window.AO3MenuHelpers.createTwoColumnLayout(
      siteFontFamily,
      siteFontWeight
    );
    siteSection.appendChild(siteFontRow);

    dialog.appendChild(siteSection);

    // Work Formatting Section
    const workSection =
      window.AO3MenuHelpers.createSection("📖 Work Formatting");

    const workWidth = window.AO3MenuHelpers.createSliderWithValue({
      id: "paragraph-width-slider",
      label: "Work Margin Width",
      min: 10,
      max: 100,
      step: 5,
      value: FORMATTER_CONFIG.paragraphWidthPercent,
      unit: "%",
      tooltip: "Maximum width of work reader",
    });
    workSection.appendChild(workWidth);

    const workFontSize = window.AO3MenuHelpers.createSliderWithValue({
      id: "paragraph-fontsize-slider",
      label: "Work Font Size",
      min: 50,
      max: 200,
      step: 5,
      value: FORMATTER_CONFIG.paragraphFontSizePercent,
      unit: "%",
      tooltip: "Size relative to site base size",
    });
    workSection.appendChild(workFontSize);

    const workFont = window.AO3MenuHelpers.createTextInput({
      id: "paragraph-fontfamily-input",
      label: "Work Font",
      value: FORMATTER_CONFIG.paragraphFontFamily,
      placeholder: "Figtree, sans-serif",
      tooltip: "Font family for reader",
    });
    workSection.appendChild(workFont);

    const textAlign = window.AO3MenuHelpers.createSelect({
      id: "paragraph-align-select",
      label: "Text Alignment",
      options: [
        {
          value: "left",
          label: "Left Aligned",
          selected: FORMATTER_CONFIG.paragraphTextAlign === "left",
        },
        {
          value: "justify",
          label: "Justified",
          selected: FORMATTER_CONFIG.paragraphTextAlign === "justify",
        },
        {
          value: "right",
          label: "Right Aligned",
          selected: FORMATTER_CONFIG.paragraphTextAlign === "right",
        },
      ],
      tooltip: "How text is aligned within paragraphs",
    });

    const lineSpacing = window.AO3MenuHelpers.createNumberInput({
      id: "paragraph-gap-input",
      label: "Line Spacing",
      value: FORMATTER_CONFIG.paragraphGap,
      min: 0,
      step: 0.1,
      tooltip:
        "Vertical space between paragraphs (multiplier). Default is 1.286.",
    });

    const alignSpacingRow = window.AO3MenuHelpers.createTwoColumnLayout(
      textAlign,
      lineSpacing
    );
    workSection.appendChild(alignSpacingRow);

    const fixSpacing = window.AO3MenuHelpers.createCheckbox({
      id: "fix-paragraph-spacing-checkbox",
      label: "Fix excessive paragraph spacing",
      checked: FORMATTER_CONFIG.fixParagraphSpacing,
      tooltip: "Remove unnecessary blank space between paragraphs",
    });
    workSection.appendChild(fixSpacing);

    dialog.appendChild(workSection);

    // Element-Specific Fonts Section
    const elementSection = window.AO3MenuHelpers.createSection(
      "🎯 Element-Specific Fonts"
    );

    const headerFont = window.AO3MenuHelpers.createTextInput({
      id: "header-fontfamily-input",
      label: "Header Font",
      value: FORMATTER_CONFIG.headerFontFamily,
      placeholder: "Figtree, sans-serif",
      tooltip: "Font for headings (H1-H6)",
    });

    const headerWeight = window.AO3MenuHelpers.createTextInput({
      id: "header-fontweight-input",
      label: "Header Weight",
      value: FORMATTER_CONFIG.headerFontWeight,
      placeholder: "700, bold",
      tooltip: "Boldness of header text",
    });

    const headerRow = window.AO3MenuHelpers.createTwoColumnLayout(
      headerFont,
      headerWeight
    );
    elementSection.appendChild(headerRow);

    const codeFont = window.AO3MenuHelpers.createTextInput({
      id: "code-fontfamily-input",
      label: "Code/Monospace Font",
      value: FORMATTER_CONFIG.codeFontFamily,
      placeholder: "Victor Mono Medium, monospace",
      tooltip: "Font for code blocks and preformatted text",
    });
    elementSection.appendChild(codeFont);

    const codeFontSize = window.AO3MenuHelpers.createTextInput({
      id: "code-fontsize-input",
      label: "Code Font Size",
      value: FORMATTER_CONFIG.codeFontSize,
      placeholder: "0.9em, 14px",
      tooltip: "Size relative to surrounding text",
    });

    const codeFontStyle = window.AO3MenuHelpers.createSelect({
      id: "code-fontstyle-select",
      label: "Code Font Style",
      options: [
        {
          value: "normal",
          label: "Normal",
          selected:
            !FORMATTER_CONFIG.codeFontStyle ||
            FORMATTER_CONFIG.codeFontStyle === "normal",
        },
        {
          value: "italic",
          label: "Italic",
          selected: FORMATTER_CONFIG.codeFontStyle === "italic",
        },
      ],
      tooltip: "Style for code text",
    });

    const codeRow = window.AO3MenuHelpers.createTwoColumnLayout(
      codeFontSize,
      codeFontStyle
    );
    elementSection.appendChild(codeRow);

    const expandCodeFont = window.AO3MenuHelpers.createCheckbox({
      id: "expand-code-font-checkbox",
      label: "Apply code font to comments",
      checked: FORMATTER_CONFIG.expandCodeFontUsage,
      tooltip:
        "Applies code font to all textareas. Requires a code/monospace font to be specified above.",
    });
    elementSection.appendChild(expandCodeFont);

    dialog.appendChild(elementSection);

    // Buttons
    const buttons = window.AO3MenuHelpers.createButtonGroup([
      { text: "Save", id: "formatter-save" },
      { text: "Cancel", id: "formatter-cancel" },
    ]);
    dialog.appendChild(buttons);

    // Reset Link
    const resetLink = window.AO3MenuHelpers.createResetLink(
      "Reset to Default Settings",
      () => {
        FORMATTER_CONFIG = { ...DEFAULT_FORMATTER_CONFIG };
        saveFormatterConfig();
        dialog.remove();
        applyParagraphWidth();
      }
    );
    dialog.appendChild(resetLink);

    // Event Handlers
    dialog.querySelector("#formatter-save").addEventListener("click", () => {
      FORMATTER_CONFIG.siteFontSizePercent =
        window.AO3MenuHelpers.getValue("site-fontsize-input") ||
        DEFAULT_FORMATTER_CONFIG.siteFontSizePercent;
      FORMATTER_CONFIG.siteFontFamily =
        window.AO3MenuHelpers.getValue("site-fontfamily-input") || "";
      FORMATTER_CONFIG.siteFontWeight =
        window.AO3MenuHelpers.getValue("site-fontweight-input") || "";
      FORMATTER_CONFIG.paragraphWidthPercent =
        window.AO3MenuHelpers.getValue("paragraph-width-slider") ||
        DEFAULT_FORMATTER_CONFIG.paragraphWidthPercent;
      FORMATTER_CONFIG.paragraphFontSizePercent =
        window.AO3MenuHelpers.getValue("paragraph-fontsize-slider") ||
        DEFAULT_FORMATTER_CONFIG.paragraphFontSizePercent;
      FORMATTER_CONFIG.paragraphTextAlign =
        window.AO3MenuHelpers.getValue("paragraph-align-select") ||
        DEFAULT_FORMATTER_CONFIG.paragraphTextAlign;
      FORMATTER_CONFIG.paragraphFontFamily =
        window.AO3MenuHelpers.getValue("paragraph-fontfamily-input") || "";
      FORMATTER_CONFIG.paragraphGap =
        window.AO3MenuHelpers.getValue("paragraph-gap-input") ||
        DEFAULT_FORMATTER_CONFIG.paragraphGap;
      FORMATTER_CONFIG.fixParagraphSpacing =
        window.AO3MenuHelpers.getValue("fix-paragraph-spacing-checkbox") ??
        false;
      FORMATTER_CONFIG.headerFontFamily =
        window.AO3MenuHelpers.getValue("header-fontfamily-input") || "";
      FORMATTER_CONFIG.headerFontWeight =
        window.AO3MenuHelpers.getValue("header-fontweight-input") || "";
      FORMATTER_CONFIG.codeFontFamily =
        window.AO3MenuHelpers.getValue("code-fontfamily-input") || "";
      FORMATTER_CONFIG.codeFontStyle =
        window.AO3MenuHelpers.getValue("code-fontstyle-select") || "normal";
      FORMATTER_CONFIG.codeFontSize =
        window.AO3MenuHelpers.getValue("code-fontsize-input") || "";
      FORMATTER_CONFIG.expandCodeFontUsage =
        window.AO3MenuHelpers.getValue("expand-code-font-checkbox") ?? false;

      saveFormatterConfig();
      dialog.remove();
      applyParagraphWidth();

      if (FORMATTER_CONFIG.paragraphTextAlign === "right") {
        location.reload();
      }
    });

    dialog.querySelector("#formatter-cancel").addEventListener("click", () => {
      dialog.remove();
    });

    document.body.appendChild(dialog);
  }

  // --- SHARED MENU MANAGEMENT ---
  function initSharedMenu() {
    if (window.AO3MenuHelpers) {
      window.AO3MenuHelpers.addToSharedMenu({
        id: "opencfg_site_wizard",
        text: "Site Wizard",
        onClick: showFormatterMenu,
      });
    }
  }

  // --- INITIALIZATION ---
  loadFormatterConfig();
  console.log("[AO3: Site Wizard] loaded.");

  function initStyles() {
    if (document.head) {
      applyParagraphWidth();
    } else {
      const observer = new MutationObserver(() => {
        if (document.head) {
          observer.disconnect();
          applyParagraphWidth();
        }
      });
      observer.observe(document.documentElement, { childList: true });
    }
  }

  function runParagraphSpacingFixIfEnabled() {
    if (
      FORMATTER_CONFIG.fixParagraphSpacing &&
      WORKS_PAGE_REGEX.test(window.location.href)
    ) {
      fixParagraphSpacing();
    }
  }

  initStyles();

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