Greasy Fork

Greasy Fork is available in English.

AO3: Reading Time & Quality Score

Combined reading time and quality scoring. Highly customizable.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        AO3: Reading Time & Quality Score
// @description Combined reading time and quality scoring. Highly customizable.
// @author      BlackBatCat
// @version     1.5
// @match       *://archiveofourown.org/
// @match       *://archiveofourown.org/tags/*/works*
// @match       *://archiveofourown.org/works*
// @match       *://archiveofourown.org/users/*
// @match       *://archiveofourown.org/collections/*
// @match       *://archiveofourown.org/bookmarks*
// @match       *://archiveofourown.org/series/*
// @license     MIT
// @grant       none
// @namespace http://greasyfork.icu/users/1498004
// ==/UserScript==

(function () {
  "use strict";

  // DEFAULT CONFIGURATION
  const DEFAULTS = {
    // Feature Toggles
    enableReadingTime: true,
    enableQualityScore: true,
    // Reading Time Settings
    wpm: 375,
    alwaysCountReadingTime: true,
    readingTimeLvl1: 120,
    readingTimeLvl2: 360,
    // Quality Score Settings
    alwaysCountQualityScore: true,
    alwaysSortQualityScore: false,
    hideHitcount: false,
    useNormalization: false,
    userMaxScore: 32,
    minKudosToShowScore: 100,
    colorThresholdLow: 10,
    colorThresholdHigh: 20,
    // Shared Color Settings
    colorGreen: "#3e8fb0",
    colorYellow: "#f6c177",
    colorRed: "#eb6f92",
    colorText: "#ffffff",
    enableBarColors: true,
  };

  // Current config, loaded from localStorage
  let CONFIG = { ...DEFAULTS };

  // Variables to track the state of the page
  let countable = false;
  let sortable = false;
  let statsPage = false;

  // --- HELPER FUNCTIONS ---
  const $ = (selector, root = document) => root.querySelectorAll(selector);
  const $1 = (selector, root = document) => root.querySelector(selector);

  // Load user settings from localStorage
  const loadUserSettings = () => {
    if (typeof Storage === "undefined") return;

    const savedConfig = localStorage.getItem("ao3_reading_quality_config");
    if (savedConfig) {
      try {
        const parsedConfig = JSON.parse(savedConfig);
        CONFIG = { ...DEFAULTS, ...parsedConfig };
      } catch (e) {
        console.error("Error loading saved config, using defaults:", e);
        CONFIG = { ...DEFAULTS };
      }
    }
  };

  // Save all settings to localStorage
  const saveAllSettings = () => {
    if (typeof Storage !== "undefined") {
      localStorage.setItem(
        "ao3_reading_quality_config",
        JSON.stringify(CONFIG)
      );
    }
  };

  // Save a specific setting
  const saveSetting = (key, value) => {
    CONFIG[key] = value;
    saveAllSettings();
  };

  // Reset all settings to defaults
  const resetAllSettings = () => {
    if (confirm("Reset all settings to defaults?")) {
      if (typeof Storage !== "undefined") {
        localStorage.removeItem("ao3_reading_quality_config");
      }
      CONFIG = { ...DEFAULTS };
      countRatio();
      calculateReadtime();
    }
  };

  // Robust number extraction from element
  const getNumberFromElement = (element) => {
    if (!element) return NaN;
    let text =
      element.getAttribute("data-ao3e-original") || element.textContent;
    if (text === null) return NaN;
    let cleanText = text.replace(/[,\s  ]/g, "");
    if (element.matches("dd.chapters")) {
      cleanText = cleanText.split("/")[0];
    }
    const number = parseInt(cleanText, 10);
    return isNaN(number) ? NaN : number;
  };

  // --- READING TIME FUNCTIONS ---
  const checkCountable = () => {
    const foundStats = $("dl.stats");
    if (foundStats.length === 0) return;

    // Cache common parent selectors for efficiency
    for (const stat of foundStats) {
      const li = stat.closest("li.work, li.bookmark");
      if (li) {
        countable = true;
        sortable = true;
        return;
      }
      if (stat.closest(".statistics")) {
        countable = true;
        sortable = true;
        statsPage = true;
        return;
      }
      if (stat.closest("dl.work")) {
        countable = true;
        return;
      }
    }
  };

  const calculateReadtime = () => {
    if (!countable || !CONFIG.enableReadingTime) return;
    $("dl.stats").forEach((statsElement) => {
      // Check if readtime already exists to avoid duplicates
      if ($1("dt.readtime", statsElement)) return;
      const wordsElement = $1("dd.words", statsElement);
      if (!wordsElement) return;
      const words_count = getNumberFromElement(wordsElement);
      if (isNaN(words_count)) return;
      const minutes = words_count / CONFIG.wpm;
      const hrs = Math.floor(minutes / 60);
      const mins = (minutes % 60).toFixed(0);
      const minutes_print = hrs > 0 ? hrs + "h" + mins + "m" : mins + "m";
      // Create elements with optimized styling
      const readtime_label = document.createElement("dt");
      readtime_label.className = "readtime";
      readtime_label.textContent = "Readtime:";

      const readtime_value = document.createElement("dd");
      readtime_value.className = "readtime";
      readtime_value.textContent = minutes_print;

      // Apply base styling
      Object.assign(readtime_value.style, {
        borderRadius: "4px",
        padding: "0 6px",
        fontWeight: "bold",
        display: "inline-block",
        verticalAlign: "middle",
      });

      if (CONFIG.enableBarColors) {
        readtime_value.style.color = CONFIG.colorText;
        if (minutes < CONFIG.readingTimeLvl1) {
          readtime_value.style.backgroundColor = CONFIG.colorGreen;
        } else if (minutes < CONFIG.readingTimeLvl2) {
          readtime_value.style.backgroundColor = CONFIG.colorYellow;
        } else {
          readtime_value.style.backgroundColor = CONFIG.colorRed;
        }
      }
      // Inherit font size and line height from dl.stats
      const parentStats = readtime_value.closest("dl.stats");
      if (parentStats) {
        const computed = window.getComputedStyle(parentStats);
        readtime_value.style.lineHeight = computed.lineHeight;
        readtime_value.style.fontSize = computed.fontSize;
      }
      // Insert after words_value
      wordsElement.insertAdjacentElement("afterend", readtime_label);
      readtime_label.insertAdjacentElement("afterend", readtime_value);
    });
  };

  // --- QUALITY SCORE FUNCTIONS ---
  const calculateWordBasedScore = (kudos, hits, words) => {
    if (hits === 0 || words === 0 || kudos === 0) return 0;
    const effectiveChapters = words / 5000;
    const adjustedHits = hits / Math.sqrt(effectiveChapters);
    return (100 * kudos) / adjustedHits;
  };

  const countRatio = () => {
    if (!countable || !CONFIG.enableQualityScore) return;
    $("dl.stats").forEach((statsElement) => {
      // Check if score already exists to avoid duplicates
      if ($1("dt.kudoshits", statsElement)) return;
      const hitsElement = $1("dd.hits", statsElement);
      const kudosElement = $1("dd.kudos", statsElement);
      const wordsElement = $1("dd.words", statsElement);
      const parentLi = statsElement.closest("li");
      try {
        const hits = getNumberFromElement(hitsElement);
        const kudos = getNumberFromElement(kudosElement);
        const words = getNumberFromElement(wordsElement);
        if (isNaN(hits) || isNaN(kudos) || isNaN(words)) return;
        // Hide score if kudos below threshold
        if (kudos < CONFIG.minKudosToShowScore) {
          // Remove any previous score elements
          if (statsElement.querySelector("dt.kudoshits"))
            statsElement.querySelector("dt.kudoshits").remove();
          if (statsElement.querySelector("dd.kudoshits"))
            statsElement.querySelector("dd.kudoshits").remove();
          return;
        }
        let rawScore = calculateWordBasedScore(kudos, hits, words);
        if (kudos < 10) rawScore = 1;
        let displayScore = rawScore;
        // Normalize thresholds if normalization is enabled
        let thresholdLow = CONFIG.colorThresholdLow;
        let thresholdHigh = CONFIG.colorThresholdHigh;
        if (CONFIG.useNormalization) {
          displayScore = (rawScore / CONFIG.userMaxScore) * 100;
          displayScore = Math.min(100, displayScore);
          displayScore = Math.ceil(displayScore); // round up, no decimals
          thresholdLow = Math.ceil(
            (CONFIG.colorThresholdLow / CONFIG.userMaxScore) * 100
          );
          thresholdHigh = Math.ceil(
            (CONFIG.colorThresholdHigh / CONFIG.userMaxScore) * 100
          );
        } else {
          displayScore = Math.round(displayScore * 10) / 10;
        }
        const ratioLabel = document.createElement("dt");
        ratioLabel.className = "kudoshits";
        ratioLabel.textContent = "Score:";
        const ratioValue = document.createElement("dd");
        ratioValue.className = "kudoshits";
        ratioValue.textContent = displayScore;
        ratioValue.style.borderRadius = "4px";
        ratioValue.style.padding = "0 6px";
        ratioValue.style.fontWeight = "bold";
        ratioValue.style.display = "inline-block";
        ratioValue.style.verticalAlign = "middle";
        if (CONFIG.enableBarColors) {
          ratioValue.style.color = CONFIG.colorText;
          ratioValue.style.fontWeight = "bold";
          if (displayScore >= thresholdHigh) {
            ratioValue.style.backgroundColor = CONFIG.colorGreen;
          } else if (displayScore >= thresholdLow) {
            ratioValue.style.backgroundColor = CONFIG.colorYellow;
          } else {
            ratioValue.style.backgroundColor = CONFIG.colorRed;
          }
        } else {
          ratioValue.style.backgroundColor = "";
          ratioValue.style.color = "inherit";
          ratioValue.style.fontWeight = "inherit";
        }
        // Inherit font size and line height from dl.stats
        const parentStats = ratioValue.closest("dl.stats");
        if (parentStats) {
          const computed = window.getComputedStyle(parentStats);
          ratioValue.style.lineHeight = computed.lineHeight;
          ratioValue.style.fontSize = computed.fontSize;
        }
        hitsElement.insertAdjacentElement("afterend", ratioValue);
        hitsElement.insertAdjacentElement("afterend", ratioLabel);
        if (CONFIG.hideHitcount && !statsPage && hitsElement) {
          hitsElement.style.display = "none";
        }
        if (parentLi) parentLi.setAttribute("kudospercent", displayScore);
      } catch (error) {
        console.error("Error calculating score:", error);
      }
    });
  };

  const sortByRatio = (ascending = false) => {
    if (!sortable) return;
    $("dl.stats").forEach((statsElement) => {
      const parentLi = statsElement.closest("li");
      const list = parentLi?.parentElement;
      if (!list) return;
      const listElements = Array.from(list.children);
      listElements.sort((a, b) => {
        const aPercent = parseFloat(a.getAttribute("kudospercent")) || 0;
        const bPercent = parseFloat(b.getAttribute("kudospercent")) || 0;
        return ascending ? aPercent - bPercent : bPercent - aPercent;
      });
      list.innerHTML = "";
      list.append(...listElements);
    });
  };

  // --- SETTINGS POPUP ---
  const showSettingsPopup = () => {
    // Get AO3 input field background color
    let inputBg = "#fffaf5"; // fallback
    const testInput = document.createElement("input");
    document.body.appendChild(testInput);
    try {
      const computedStyle = window.getComputedStyle(testInput);
      const computedBg = computedStyle.backgroundColor;
      if (
        computedBg &&
        computedBg !== "rgba(0, 0, 0, 0)" &&
        computedBg !== "transparent"
      ) {
        inputBg = computedBg;
      }
    } catch (e) {}
    testInput.remove();
    const popup = document.createElement("div");
    popup.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: 600px;
            max-height: 80vh;
            overflow-y: auto;
            font-family: inherit;
            font-size: inherit;
            box-sizing: border-box;
        `;
    // Ensure headings inherit font family and add tooltip styles
    const style = document.createElement("style");
    style.textContent = `
      #ao3-rtqs-popup .settings-section {
        background: rgba(0,0,0,0.03);
        border-radius: 6px;
        padding: 15px;
        margin-bottom: 20px;
        border-left: 4px solid currentColor;
      }
      #ao3-rtqs-popup .section-title {
        margin-top: 0;
        margin-bottom: 15px;
        font-size: 1.2em;
        font-weight: bold;
        color: inherit;
        opacity: 0.85;
        font-family: inherit;
      }
      #ao3-rtqs-popup .setting-group {
        margin-bottom: 15px;
      }
      #ao3-rtqs-popup .setting-label {
        display: block;
        margin-bottom: 6px;
        font-weight: bold;
        color: inherit;
        opacity: 0.9;
      }
      #ao3-rtqs-popup .checkbox-label {
        display: block;
        font-weight: normal;
        color: inherit;
      }
      #ao3-rtqs-popup .subsettings {
        padding-left: 20px;
        margin-top: 10px;
      }
      #ao3-rtqs-popup .two-column {
        display: grid;
        grid-template-columns: 1fr 1fr;
        gap: 15px;
      }
      #ao3-rtqs-popup .button-group {
        display: flex;
        justify-content: space-between;
        gap: 10px;
        margin-top: 20px;
      }
      #ao3-rtqs-popup .button-group button {
        flex: 1;
        padding: 10px;
        color: inherit;
        opacity: 0.9;
      }
      #ao3-rtqs-popup .reset-link {
        text-align: center;
        margin-top: 10px;
        color: inherit;
        opacity: 0.7;
      }
      #ao3-rtqs-popup .symbol.question {
        font-size: 0.5em;
        vertical-align: middle;
      }
      #ao3-rtqs-popup input[type="text"],
      #ao3-rtqs-popup input[type="number"],
      #ao3-rtqs-popup input[type="color"],
      #ao3-rtqs-popup select,
      #ao3-rtqs-popup textarea {
        width: 100%;
        box-sizing: border-box;
      }
      #ao3-rtqs-popup input[type="text"]:focus,
      #ao3-rtqs-popup input[type="number"]:focus,
      #ao3-rtqs-popup input[type="color"]:focus,
      #ao3-rtqs-popup select:focus,
      #ao3-rtqs-popup textarea:focus {
        background: ${inputBg} !important;
      }
    `;
    document.head.appendChild(style);

    popup.id = "ao3-rtqs-popup";
    const form = document.createElement("form");

    // Calculate values for display
    const displayThresholdLow = CONFIG.useNormalization
      ? Math.ceil((CONFIG.colorThresholdLow / CONFIG.userMaxScore) * 100)
      : CONFIG.colorThresholdLow;

    const displayThresholdHigh = CONFIG.useNormalization
      ? Math.ceil((CONFIG.colorThresholdHigh / CONFIG.userMaxScore) * 100)
      : CONFIG.colorThresholdHigh;

    form.innerHTML = `
        <h3 style="text-align: center; margin-top: 0; color: inherit;">⏱️ Reading Time & Quality Score Settings ⭐</h3>

        <div class="settings-section">
          <h4 class="section-title">📚 Reading Time</h4>
          <div class="setting-group">
            <label class="checkbox-label">
              <input type="checkbox" id="enableReadingTime" ${
                CONFIG.enableReadingTime ? "checked" : ""
              }>
              Enable Reading Time
            </label>
          </div>
          <div id="readingTimeSettings" class="subsettings" style="${
            CONFIG.enableReadingTime ? "" : "display: none;"
          }">
            <div class="setting-group">
              <label class="checkbox-label">
                <input type="checkbox" id="alwaysCountReadingTime" ${
                  CONFIG.alwaysCountReadingTime ? "checked" : ""
                }>
                Calculate automatically
              </label>
            </div>
            <div class="setting-group">
              <label class="setting-label">
                Words per minute
                <span class="symbol question" title="Average reading speed is 200-300 wpm. 375 is for faster readers."><span>?</span></span>
              </label>
              <input type="number" id="wpm" value="${
                CONFIG.wpm
              }" min="100" max="1000" step="25">
            </div>
            <div class="setting-group">
              <label class="setting-label">
                Yellow threshold (minutes)
                <span class="symbol question" title="Works taking less than this many minutes will be colored green"><span>?</span></span>
              </label>
              <input type="number" id="readingTimeLvl1" value="${
                CONFIG.readingTimeLvl1
              }" min="5" max="240" step="5">
            </div>
            <div class="setting-group">
              <label class="setting-label">
                Red threshold (minutes)
                <span class="symbol question" title="Works taking more than this many minutes will be colored red"><span>?</span></span>
              </label>
              <input type="number" id="readingTimeLvl2" value="${
                CONFIG.readingTimeLvl2
              }" min="30" max="480" step="10">
            </div>
          </div>
        </div>

        <div class="settings-section">
          <h4 class="section-title">💖 Quality Score</h4>
          <div class="setting-group">
            <label class="checkbox-label">
              <input type="checkbox" id="enableQualityScore" ${
                CONFIG.enableQualityScore ? "checked" : ""
              }>
              Enable Quality Score
            </label>
          </div>
          <div id="qualityScoreSettings" class="subsettings" style="${
            CONFIG.enableQualityScore ? "" : "display: none;"
          }">
            <div class="setting-group">
              <label class="checkbox-label">
                <input type="checkbox" id="alwaysCountQualityScore" ${
                  CONFIG.alwaysCountQualityScore ? "checked" : ""
                }>
                Calculate automatically
              </label>
            </div>
            <div class="setting-group">
              <label class="checkbox-label">
                <input type="checkbox" id="alwaysSortQualityScore" ${
                  CONFIG.alwaysSortQualityScore ? "checked" : ""
                }>
                Sort by score automatically
              </label>
            </div>
            <div class="setting-group">
              <label class="checkbox-label">
                <input type="checkbox" id="hideHitcount" ${
                  CONFIG.hideHitcount ? "checked" : ""
                }>
                Hide hit count
              </label>
            </div>
            <div class="setting-group">
              <label class="setting-label">Minimum kudos to show score</label>
              <input type="number" id="minKudosToShowScore" value="${
                CONFIG.minKudosToShowScore
              }" min="0" max="10000" step="1">
            </div>
            <div class="setting-group">
              <label class="checkbox-label">
                <input type="checkbox" id="useNormalization" ${
                  CONFIG.useNormalization ? "checked" : ""
                }>
                Normalize scores to 100%
                <span class="symbol question" title="Scales the raw score so your 'Best Possible Raw Score' equals 100%. Makes scores from different fandoms more comparable."><span>?</span></span>
              </label>
            </div>
            <div id="userMaxScoreContainer" class="setting-group" style="${
              CONFIG.useNormalization ? "" : "display: none;"
            }">
              <label class="setting-label">
                Best Possible Raw Score <span id="normalizationLabel">${
                  CONFIG.useNormalization ? "(for 100%)" : ""
                }</span>
                <span class="symbol question" title="The highest score you've seen in your fandom. Used to scale other scores to percentages."><span>?</span></span>
              </label>
              <input type="number" id="userMaxScore" value="${
                CONFIG.userMaxScore
              }" min="1" max="100" step="1">
            </div>
            <div class="setting-group">
              <label class="setting-label">
                Good Score <span id="thresholdLowLabel">${
                  CONFIG.useNormalization ? "(%)" : ""
                }</span>
                <span class="symbol question" title="Scores at or above this threshold will be colored yellow"><span>?</span></span>
              </label>
              <input type="number" id="colorThresholdLow" value="${displayThresholdLow}" min="0.1" max="100" step="0.1">
            </div>
            <div class="setting-group">
              <label class="setting-label">
                Excellent Score <span id="thresholdHighLabel">${
                  CONFIG.useNormalization ? "(%)" : ""
                }</span>
                <span class="symbol question" title="Scores at or above this threshold will be colored green"><span>?</span></span>
              </label>
              <input type="number" id="colorThresholdHigh" value="${displayThresholdHigh}" min="0.1" max="100" step="0.1">
            </div>
          </div>
        </div>

        <div class="settings-section">
          <h4 class="section-title">🎨 Color Settings</h4>
          <div class="setting-group">
            <label class="checkbox-label">
              <input type="checkbox" id="enableBarColors" ${
                CONFIG.enableBarColors ? "checked" : ""
              }>
              Enable colored backgrounds for bars
            </label>
          </div>
          <div id="barColorSettings" class="subsettings two-column" style="${
            CONFIG.enableBarColors ? "" : "display: none;"
          }">
            <div class="setting-group">
              <label class="setting-label">Green</label>
              <input type="color" id="colorGreen" value="${CONFIG.colorGreen}">
            </div>
            <div class="setting-group">
              <label class="setting-label">Yellow</label>
              <input type="color" id="colorYellow" value="${
                CONFIG.colorYellow
              }">
            </div>
            <div class="setting-group">
              <label class="setting-label">Red</label>
              <input type="color" id="colorRed" value="${CONFIG.colorRed}">
            </div>
            <div class="setting-group">
              <label class="setting-label">Text color</label>
              <input type="color" id="colorText" value="${CONFIG.colorText}">
            </div>
          </div>
        </div>

        <div class="button-group">
          <button type="submit">Save</button>
          <button type="button" id="closePopup">Close</button>
        </div>
        <div class="reset-link">
          <a href="#" id="resetSettingsLink">Reset to Default Settings</a>
        </div>
      `;

    // Toggle bar color settings
    const enableBarColorsCheckbox = form.querySelector("#enableBarColors");
    const barColorSettingsDiv = form.querySelector("#barColorSettings");
    const toggleBarColorSettings = () => {
      barColorSettingsDiv.style.display = enableBarColorsCheckbox.checked
        ? "block"
        : "none";
    };
    enableBarColorsCheckbox.addEventListener("change", toggleBarColorSettings);

    // Toggle reading time settings
    const readingTimeCheckbox = form.querySelector("#enableReadingTime");
    const readingTimeSettings = form.querySelector("#readingTimeSettings");
    const toggleReadingTimeSettings = () => {
      readingTimeSettings.style.display = readingTimeCheckbox.checked
        ? "block"
        : "none";
    };
    readingTimeCheckbox.addEventListener("change", toggleReadingTimeSettings);

    // Toggle quality score settings
    const qualityScoreCheckbox = form.querySelector("#enableQualityScore");
    const qualityScoreSettings = form.querySelector("#qualityScoreSettings");
    const toggleQualityScoreSettings = () => {
      qualityScoreSettings.style.display = qualityScoreCheckbox.checked
        ? "block"
        : "none";
    };
    qualityScoreCheckbox.addEventListener("change", toggleQualityScoreSettings);

    // Toggle normalization labels, convert values, and show/hide userMaxScore
    const normCheckbox = form.querySelector("#useNormalization");
    const normLabel = form.querySelector("#normalizationLabel");
    const thresholdLowLabel = form.querySelector("#thresholdLowLabel");
    const thresholdHighLabel = form.querySelector("#thresholdHighLabel");
    const thresholdLowInput = form.querySelector("#colorThresholdLow");
    const thresholdHighInput = form.querySelector("#colorThresholdHigh");
    const userMaxScoreInput = form.querySelector("#userMaxScore");
    const userMaxScoreContainer = form.querySelector("#userMaxScoreContainer");

    const toggleNormalization = () => {
      if (normCheckbox.checked) {
        normLabel.textContent = "(for 100%)";
        thresholdLowLabel.textContent = "(%)";
        thresholdHighLabel.textContent = "(%)";
        userMaxScoreContainer.style.display = "block";
        // Convert current raw thresholds to percentages
        thresholdLowInput.value = Math.ceil(
          (parseFloat(thresholdLowInput.value) /
            parseFloat(userMaxScoreInput.value)) *
            100
        );
        thresholdHighInput.value = Math.ceil(
          (parseFloat(thresholdHighInput.value) /
            parseFloat(userMaxScoreInput.value)) *
            100
        );
      } else {
        normLabel.textContent = "";
        thresholdLowLabel.textContent = "";
        thresholdHighLabel.textContent = "";
        userMaxScoreContainer.style.display = "none";
        // Convert current percentages back to raw values
        thresholdLowInput.value = Math.round(
          (parseFloat(thresholdLowInput.value) / 100) *
            parseFloat(userMaxScoreInput.value)
        );
        thresholdHighInput.value = Math.round(
          (parseFloat(thresholdHighInput.value) / 100) *
            parseFloat(userMaxScoreInput.value)
        );
      }
    };
    normCheckbox.addEventListener("change", toggleNormalization);

    // Add event listeners for reset and close
    form
      .querySelector("#resetSettingsLink")
      .addEventListener("click", function (e) {
        e.preventDefault();
        resetAllSettings();
        popup.remove();
      });
    form
      .querySelector("#closePopup")
      .addEventListener("click", () => popup.remove());

    // Form submission
    form.addEventListener("submit", (e) => {
      e.preventDefault();

      // Collect all values first
      let userMaxScoreValue = parseFloat(
        form.querySelector("#userMaxScore").value
      );
      let thresholdLowValue = parseFloat(
        form.querySelector("#colorThresholdLow").value
      );
      let thresholdHighValue = parseFloat(
        form.querySelector("#colorThresholdHigh").value
      );
      const isNormalizationEnabled =
        form.querySelector("#useNormalization").checked;

      // CRITICAL: If normalization is enabled, convert percentages back to raw scores before saving
      if (isNormalizationEnabled) {
        thresholdLowValue = (thresholdLowValue / 100) * userMaxScoreValue;
        thresholdHighValue = (thresholdHighValue / 100) * userMaxScoreValue;
      }

      // Update config object with all settings
      CONFIG.enableReadingTime =
        form.querySelector("#enableReadingTime").checked;
      CONFIG.enableQualityScore = form.querySelector(
        "#enableQualityScore"
      ).checked;
      CONFIG.alwaysCountReadingTime = form.querySelector(
        "#alwaysCountReadingTime"
      ).checked;
      CONFIG.wpm = parseInt(form.querySelector("#wpm").value);
      CONFIG.readingTimeLvl1 = parseInt(
        form.querySelector("#readingTimeLvl1").value
      );
      CONFIG.readingTimeLvl2 = parseInt(
        form.querySelector("#readingTimeLvl2").value
      );
      CONFIG.alwaysCountQualityScore = form.querySelector(
        "#alwaysCountQualityScore"
      ).checked;
      CONFIG.alwaysSortQualityScore = form.querySelector(
        "#alwaysSortQualityScore"
      ).checked;
      CONFIG.hideHitcount = form.querySelector("#hideHitcount").checked;
      CONFIG.minKudosToShowScore = parseInt(
        form.querySelector("#minKudosToShowScore").value
      );
      CONFIG.useNormalization = isNormalizationEnabled;
      CONFIG.userMaxScore = userMaxScoreValue;
      CONFIG.colorThresholdLow = thresholdLowValue;
      CONFIG.colorThresholdHigh = thresholdHighValue;
      CONFIG.enableBarColors = form.querySelector("#enableBarColors").checked;
      CONFIG.colorGreen = form.querySelector("#colorGreen").value;
      CONFIG.colorYellow = form.querySelector("#colorYellow").value;
      CONFIG.colorRed = form.querySelector("#colorRed").value;
      CONFIG.colorText = form.querySelector("#colorText").value;

      // Save the entire config object
      CONFIG.enableBarColors = form.querySelector("#enableBarColors").checked;
      saveAllSettings();

      popup.remove();
      location.reload();
    });

    popup.appendChild(form);
    document.body.appendChild(popup);
  };

  // --- SHARED MENU SYSTEM ---
  function initSharedMenu() {
    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 items
    const menu = document.querySelector("#scriptconfig .dropdown-menu");
    if (menu) {
      const showMenuOptions = isAllowedMenuPage();

      // Always add settings menu item
      const settingsItem = document.createElement("li");
      const settingsLink = document.createElement("a");
      settingsLink.href = "javascript:void(0);";
      settingsLink.id = "opencfg_reading_quality";
      settingsLink.textContent = "Reading Time & Quality Score";
      settingsLink.addEventListener("click", showSettingsPopup);
      settingsItem.appendChild(settingsLink);
      menu.appendChild(settingsItem);

      // Add separator if we have conditional items
      if (CONFIG.enableReadingTime || CONFIG.enableQualityScore) {
        const separator = document.createElement("li");
        separator.innerHTML = "<hr style='margin: 5px 0;'>";
        menu.appendChild(separator);
      }

      // Reading Time manual calculation only if 'Calculate automatically' is unchecked
      if (CONFIG.enableReadingTime && !CONFIG.alwaysCountReadingTime) {
        const readingTimeItem = document.createElement("li");
        const readingTimeLink = document.createElement("a");
        readingTimeLink.href = "javascript:void(0);";
        readingTimeLink.textContent = "Reading Time: Calculate Times";
        readingTimeLink.addEventListener("click", calculateReadtime);
        readingTimeItem.appendChild(readingTimeLink);
        menu.appendChild(readingTimeItem);
      }

      // Quality Score manual calculation only if 'Calculate automatically' is unchecked
      if (CONFIG.enableQualityScore && !CONFIG.alwaysCountQualityScore) {
        const qualityScoreItem = document.createElement("li");
        const qualityScoreLink = document.createElement("a");
        qualityScoreLink.href = "javascript:void(0);";
        qualityScoreLink.textContent = "Quality Score: Calculate Scores";
        qualityScoreLink.addEventListener("click", countRatio);
        qualityScoreItem.appendChild(qualityScoreLink);
        menu.appendChild(qualityScoreItem);
      }

      // Sort by Score only if 'Sort by score automatically' is unchecked AND not on actual works pages AND allowed by showMenuOptions
      const isWorksPage = /^\/works\/(\d+)(\/chapters\/\d+)?(\/|$)/.test(
        window.location.pathname
      );
      if (
        showMenuOptions &&
        CONFIG.enableQualityScore &&
        !CONFIG.alwaysSortQualityScore &&
        !isWorksPage
      ) {
        const sortScoreItem = document.createElement("li");
        const sortScoreLink = document.createElement("a");
        sortScoreLink.href = "javascript:void(0);";
        sortScoreLink.textContent = "Quality Score: Sort by Score";
        sortScoreLink.addEventListener("click", () => sortByRatio());
        sortScoreItem.appendChild(sortScoreLink);
        menu.appendChild(sortScoreItem);
      }
    }
  }

  // Helper: check if current page is one of the allowed types for menu options
  function isAllowedMenuPage() {
    const path = window.location.pathname;
    // Remove sort option from actual works pages (e.g., /works/ID or /works/ID/chapters/ID)
    if (/^\/works\/(\d+)(\/chapters\/\d+)?(\/|$)/.test(path)) return false;
    // User bookmarks: /users/USERNAME/bookmarks or /bookmarks
    if (
      /^\/users\/[^\/]+\/bookmarks(\/|$)/.test(path) ||
      /^\/bookmarks(\/|$)/.test(path)
    )
      return true;
    // User pseuds bookmarks: /users/USERNAME/pseuds/PSEUD/bookmarks
    if (/^\/users\/[^\/]+\/pseuds\/[^\/]+\/bookmarks(\/|$)/.test(path))
      return true;
    // User profile: /users/USERNAME (no trailing /works etc)
    if (/^\/users\/[^\/]+\/?$/.test(path)) return true;
    // User pseuds works: /users/USERNAME/pseuds/PSEUD/works
    if (/^\/users\/[^\/]+\/pseuds\/[^\/]+\/works(\/|$)/.test(path)) return true;
    // Tag works: /tags/ANYTHING/works
    if (/^\/tags\/[^\/]+\/works(\/|$)/.test(path)) return true;
    // Collections: /collections/ANYTHING
    if (/^\/collections\/[^\/]+(\/|$)/.test(path)) return true;
    // Works index: /works
    if (/^\/works(\/|$)/.test(path)) return true;
    return false;
  }

  // --- INITIALIZATION ---
  loadUserSettings();

  // Show startup message
  console.log("[AO3: Reading Time & Quality Score] loaded.");

  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => {
      checkCountable();
      initSharedMenu();
      if (CONFIG.alwaysCountReadingTime) setTimeout(calculateReadtime, 1000);
      if (CONFIG.alwaysCountQualityScore) {
        setTimeout(() => {
          countRatio();
          if (CONFIG.alwaysSortQualityScore) sortByRatio();
        }, 1000);
      }
    });
  } else {
    checkCountable();
    initSharedMenu();
    if (CONFIG.alwaysCountReadingTime) setTimeout(calculateReadtime, 1000);
    if (CONFIG.alwaysCountQualityScore) {
      setTimeout(() => {
        countRatio();
        if (CONFIG.alwaysSortQualityScore) sortByRatio();
      }, 1000);
    }
  }
})();