Greasy Fork is available in English.

Nexus No Wait ++

Download from nexusmods.com without wait (Manual/Vortex/MO2/NMM), Tweaked with extra features.

// ==UserScript==
// @name        Nexus No Wait ++
// @description Download from nexusmods.com without wait (Manual/Vortex/MO2/NMM), Tweaked with extra features.
// @namespace   NexusNoWaitPlusPlus
// @author      Torkelicious
// @version     1.1.11
// @include     https://*.nexusmods.com/*
// @run-at      document-idle
// @iconURL     https://raw.githubusercontent.com/torkelicious/nexus-no-wait-pp/refs/heads/main/icon.png
// @icon        https://raw.githubusercontent.com/torkelicious/nexus-no-wait-pp/refs/heads/main/icon.png
// @grant       GM_xmlhttpRequest
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_deleteValue
// @grant       GM_info
// @connect     nexusmods.com
// @connect     *.nexusmods.com
// @connect     raw.githubusercontent.com
// @license     GPL-3.0-or-later
// ==/UserScript==

/* global GM_getValue, GM_setValue, GM_deleteValue, GM_xmlhttpRequest, GM_info GM */

(function () {
  const DEFAULT_CONFIG = {
    autoCloseTab: true,
    skipRequirements: true,
    showAlerts: true,
    refreshOnError: false,
    requestTimeout: 30000,
    closeTabTime: 1000,
    debug: false,
    playErrorSound: true,
  };

  const RECENT_HANDLE_MS = 600;

  // logging helpers
  function debugLog(...args) {
    try {
      const prefix = "[Nexus No Wait ++]";
      (console.debug || console.log).call(
        console,
        prefix,
        ...args,
        "Page:",
        window.location.href
      );
    } catch (e) {}
  }
  function infoLog(...args) {
    try {
      (console.info || console.log).call(
        console,
        "[Nexus No Wait ++]",
        ...args,
        "Page:",
        window.location.href
      );
    } catch (e) {}
  }
  function errorLog(...args) {
    try {
      (console.error || console.log).call(
        console,
        "[Nexus No Wait ++]",
        ...args,
        "Page:",
        window.location.href
      );
    } catch (e) {}
  }

  // === Settings management ===
  function validateSettings(settings) {
    if (!settings || typeof settings !== "object") return { ...DEFAULT_CONFIG };
    const validated = { ...settings };
    for (const [key, defaultValue] of Object.entries(DEFAULT_CONFIG)) {
      if (typeof validated[key] !== typeof defaultValue) {
        validated[key] = defaultValue;
      }
    }
    return validated;
  }
  function loadSettings() {
    try {
      const saved = GM_getValue("nexusNoWaitConfig", null);
      let parsed;
      if (!saved) parsed = DEFAULT_CONFIG;
      else if (typeof saved === "string") {
        try {
          parsed = JSON.parse(saved);
        } catch (e) {
          parsed = DEFAULT_CONFIG;
        }
      } else parsed = saved;
      const validated = validateSettings(parsed);
      debugLog("Loaded settings", validated);
      return validated;
    } catch (e) {
      debugLog("Failed loading settings:", e);
      return { ...DEFAULT_CONFIG };
    }
  }
  function saveSettings(settings) {
    try {
      try {
        GM_setValue("nexusNoWaitConfig", settings);
      } catch (_) {
        GM_setValue("nexusNoWaitConfig", JSON.stringify(settings));
      }
      debugLog("Saved settings");
    } catch (e) {
      console.error("Failed to save settings:", e);
    }
  }
  const config = Object.assign({}, DEFAULT_CONFIG, loadSettings());

  // Error sound
  const errorSound = new Audio(
    "https://github.com/torkelicious/nexus-no-wait-pp/raw/refs/heads/main/errorsound.mp3"
  );
  try {
    errorSound.load();
  } catch (e) {
    debugLog("Could not preload sound", e);
  }
  function playErrorSound() {
    if (!config.playErrorSound) return;
    errorSound.play().catch((e) => debugLog("Error playing sound:", e));
  }

  // Error/log helpers used by UI
  function logMessage(message, showAlert = false, isDebug = false) {
    if (isDebug) {
      debugLog(message);
      if (config.debug) alert("[Nexus No Wait ++] (Debug):\n" + message);
      return;
    }
    playErrorSound();
    errorLog(message);
    if (showAlert && config.showAlerts) alert("[Nexus No Wait ++]\n" + message);
    if (config.refreshOnError) location.reload();
  }

  // Skip requirements tab
  if (
    window.location.href.includes("tab=requirements") &&
    config.skipRequirements
  ) {
    const newUrl = window.location.href.replace(
      "tab=requirements",
      "tab=files"
    );
    infoLog("Skipping requirements tab -> files", {
      from: window.location.href,
      to: newUrl,
    });
    window.location.replace(newUrl);
    return;
  }

  // === AJAX wrapper ===
  let ajaxRequestRaw;
  if (typeof GM_xmlhttpRequest !== "undefined")
    ajaxRequestRaw = GM_xmlhttpRequest;
  else if (
    typeof GM !== "undefined" &&
    typeof GM.xmlHttpRequest !== "undefined"
  )
    ajaxRequestRaw = GM.xmlHttpRequest;

  function ajaxRequest(obj) {
    if (!ajaxRequestRaw) {
      logMessage("AJAX not available in this environment", true);
      return;
    }
    debugLog("ajaxRequest", {
      method: obj.type,
      url: obj.url,
      dataPreview:
        typeof obj.data === "string" ? obj.data.slice(0, 200) : obj.data,
    });
    ajaxRequestRaw({
      method: obj.type,
      url: obj.url,
      data: obj.data,
      headers: obj.headers,
      timeout: config.requestTimeout,
      onload(response) {
        const body =
          typeof response.response !== "undefined"
            ? response.response
            : response.responseText;
        debugLog("ajax response", {
          status: response.status,
          length: body ? body.length || 0 : 0,
          preview: body ? String(body).slice(0, 500) : "",
        });
        if (response.status >= 200 && response.status < 300) obj.success(body);
        else obj.error(response);
      },
      onerror(response) {
        obj.error(response);
      },
      ontimeout(response) {
        obj.error(response);
      },
    });
  }

  // === Button UI helpers ===
  function btnError(button, error) {
    try {
      if (button && button.style) button.style.color = "red";
      let message = "Download failed: ";
      if (error) {
        if (typeof error === "string") message += error;
        else if (error.message) message += error.message;
        else if (error.status)
          message += `HTTP ${error.status} ${error.statusText || ""}`;
        else if (typeof error.responseText === "string")
          message += error.responseText.slice(0, 300);
        else message += JSON.stringify(error);
      } else message += "Unknown error";
      if (button && "innerText" in button)
        button.innerText = "ERROR: " + message;
      errorLog(message);
      logMessage(message, true);
    } catch (e) {
      logMessage(
        "Unknown error while handling button error: " + e.message,
        true
      );
    }
  }
  function btnSuccess(button) {
    if (button && button.style) button.style.color = "green";
    if (button && "innerText" in button) button.innerText = "Downloading!";
    infoLog("Download started (UI updated).", { button });
  }
  function btnWait(button) {
    if (button && button.style) button.style.color = "yellow";
    if (button && "innerText" in button) button.innerText = "Wait...";
    debugLog("Set button to wait", { button });
  }

  function closeOnDL() {
    if (config.autoCloseTab) {
      debugLog("Scheduling close", { delay: config.closeTabTime });
      setTimeout(() => {
        debugLog("Closing window");
        window.close();
      }, config.closeTabTime);
    }
  }

  // Primary file id extractor (keeps several page strategies)
  function getPrimaryFileId() {
    try {
      // action-nmm link (vortex)
      const vortexAction = document.querySelector(
        '#action-nmm a[href*="file_id="]'
      );
      if (vortexAction) {
        const fid = new URL(vortexAction.href, location.href).searchParams.get(
          "file_id"
        );
        if (fid) {
          debugLog("getPrimaryFileId found via action-nmm", fid);
          return fid;
        }
      }

      // any file link with file_id
      const anyFileLink = document.querySelector('a[href*="file_id="]');
      if (anyFileLink) {
        const fid = new URL(anyFileLink.href, location.href).searchParams.get(
          "file_id"
        );
        if (fid) {
          debugLog("getPrimaryFileId found via any file link", fid);
          return fid;
        }
      }

      // file-expander-header[data-id]
      const header = document.querySelector(".file-expander-header[data-id]");
      if (header) {
        const fid = header.getAttribute("data-id");
        if (fid) {
          debugLog("getPrimaryFileId found via header", fid);
          return fid;
        }
      }

      // fallback data-fileid / data-id attributes
      const dataFile = document.querySelector("[data-fileid], [data-id]");
      if (dataFile) {
        const fid =
          dataFile.getAttribute("data-fileid") ||
          dataFile.getAttribute("data-id") ||
          (dataFile.dataset && dataFile.dataset.fileid);
        if (fid) {
          debugLog("getPrimaryFileId found via data-fileid/data-id", fid);
          return fid;
        }
      }
    } catch (e) {
      debugLog("getPrimaryFileId error", e);
    }
    debugLog("getPrimaryFileId: none found");
    return null;
  }

  // === MAIN DOWNLOAD HANDLER ===
  function clickListener(event) {
    console.groupCollapsed("[NNW++] clickListener");

    // duplicate-handling guard
    try {
      if (this && this.dataset && this.dataset.nnwHandled === "1") {
        debugLog("Element recently handled, skipping duplicate");
        console.groupEnd();
        return;
      }
      try {
        if (this && this.dataset) this.dataset.nnwHandled = "1";
      } catch (_) {}
      try {
        if (this)
          setTimeout(() => {
            try {
              if (this && this.dataset) delete this.dataset.nnwHandled;
            } catch (_) {}
          }, RECENT_HANDLE_MS);
      } catch (_) {}
      if (event) {
        try {
          event.__nnw_nofollow = true;
        } catch (_) {}
      }
    } catch (e) {
      debugLog("Guard error", e);
    }

    try {
      debugLog("clickListener start", {
        target: this,
        href: (this && this.href) || window.location.href,
      });

      const selfIsElement = this && this.tagName;
      const href = (selfIsElement && this.href) || window.location.href;
      const params = new URL(href, location.href).searchParams;

      if (params.get("file_id")) {
        infoLog("file link clicked", { href });
        let button = event;
        if (selfIsElement && this.href) {
          button = this;
          try {
            if (event && typeof event.preventDefault === "function")
              event.preventDefault();
          } catch (_) {}
        }
        btnWait(button);

        const section = document.getElementById("section");
        const gameId = section ? section.dataset.gameId : this.current_game_id;
        let fileId = params.get("file_id") || params.get("id");

        // NMM 
        if (params.get("nmm")) {
          infoLog("nmm parameter present -> performing NMM GET extraction", {
            href,
          });
          ajaxRequest({
            type: "GET",
            url: href,
            headers: {
              Origin: "https://www.nexusmods.com",
              Referer: document.location.href,
              "Sec-Fetch-Site": "same-origin",
              "X-Requested-With": "XMLHttpRequest",
            },
            success(data) {
              debugLog(
                "NMM GET response preview:",
                String(data).slice(0, 1200)
              );
              if (!data) {
                btnError(button, { message: "Empty response from server" });
                console.groupEnd();
                return;
              }
              try {
                const doc = new DOMParser().parseFromString(
                  String(data),
                  "text/html"
                );
                const slow =
                  doc.getElementById("slowDownloadButton") ||
                  doc.querySelector("[data-download-url]");
                if (slow) {
                  const downloadUrl =
                    slow.getAttribute("data-download-url") ||
                    (slow.dataset && slow.dataset.downloadUrl) ||
                    slow.href;
                  if (downloadUrl) {
                    infoLog("Found data-download-url (NMM)", downloadUrl);
                    btnSuccess(button);
                    try {
                      document.location.href = downloadUrl;
                    } catch (_) {
                      window.location = downloadUrl;
                    }

                    console.groupEnd();
                    return;
                  } else {
                    btnError(button, {
                      message:
                        "NMM page contained slowDownloadButton but no data-download-url attribute",
                    });
                    console.groupEnd();
                    return;
                  }
                }
                let parsed = null;
                try {
                  parsed = typeof data === "string" ? JSON.parse(data) : data;
                } catch (e) {
                  parsed = null;
                }
                if (parsed && parsed.url) {
                  infoLog("Found parsed.url in NMM GET response", parsed.url);
                  btnSuccess(button);
                  try {
                    document.location.href = parsed.url;
                  } catch (_) {
                    window.location = parsed.url;
                  }

                  console.groupEnd();
                  return;
                }
                btnError(button, {
                  message: "Could not find NMM download URL in response",
                });
                console.groupEnd();
              } catch (e) {
                btnError(button, e);
                console.groupEnd();
              }
            },
            error(xhr) {
              btnError(button, xhr);
              console.groupEnd();
            },
          });
          return;
        }

        // POST ---> GenerateDownloadUrl
        const postOptions = {
          type: "POST",
          url: "/Core/Libs/Common/Managers/Downloads?GenerateDownloadUrl",
          data: "fid=" + fileId + "&game_id=" + gameId,
          headers: {
            Origin: "https://www.nexusmods.com",
            Referer: href,
            "Sec-Fetch-Site": "same-origin",
            "X-Requested-With": "XMLHttpRequest",
            "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
          },
          success(data) {
            debugLog(
              "file link POST response preview:",
              String(data).slice(0, 1200)
            );
            if (!data) {
              btnError(button, { message: "Empty response from server" });
              console.groupEnd();
              return;
            }
            let parsed = null;
            try {
              parsed = typeof data === "string" ? JSON.parse(data) : data;
            } catch (e) {
              btnError(button, { message: "Server response was not JSON" });
              console.groupEnd();
              return;
            }
            if (parsed && parsed.url) {
              infoLog("Using parsed.url from POST", parsed.url);
              btnSuccess(button);
              try {
                document.location.href = parsed.url;
              } catch (_) {
                window.location = parsed.url;
              }
              console.groupEnd();
              return;
            }
            btnError(button, {
              message: "No download URL returned from server",
            });
            console.groupEnd();
          },
          error(xhr) {
            btnError(button, xhr);
            console.groupEnd();
          },
        };
        ajaxRequest(postOptions);

        const popup = selfIsElement ? this.parentNode : null;
        if (popup && popup.classList.contains("popup")) {
          popup.getElementsByTagName("button")[0]?.click();
          const popupButton = document.getElementById("popup" + fileId);
          if (popupButton) {
            btnSuccess(popupButton);
          }
        }
        return;
      }

      // mirror ModRequirementsPopUp id for element for later lookup
      if (/ModRequirementsPopUp/.test(href)) {
        const fileId = new URL(href, location.href).searchParams.get("id");
        if (fileId && selfIsElement) {
          this.setAttribute("id", "popup" + fileId);
        }
      }
    } catch (err) {
      errorLog("Unhandled error in clickListener", err);
    } finally {
      try {
        if (this && this.dataset) delete this.dataset.nnwProcessing;
      } catch (_) {}
      console.groupEnd();
    }
  }

  // === Event delegation ===
  function delegatedClickHandler(event) {
    try {
      const selector = [
        "#slowDownloadButton",
        "#action-manual a",
        "#action-nmm a",
        'a[href*="file_id="]',
        "a.btn",
      ].join(",");
      const el =
        event.target && event.target.closest
          ? event.target.closest(selector)
          : null;
      if (!el) return;

      if (event && event.__nnw_nofollow) {
        debugLog("delegatedClickHandler: event already handled, skipping");
        return;
      }
      clickListener.call(el, event);
    } catch (e) {
      debugLog("delegatedClickHandler error", e);
    }
  }

  // Autostart when file_id present in URL
  function autoStartFileLink() {
    if (/file_id=/.test(window.location.href)) {
      debugLog("autoStartFileLink detected file_id in URL");
      try {
        const slowButton = document.getElementById("slowDownloadButton");
        if (slowButton) clickListener.call(slowButton, null);
        closeOnDL();
      } catch (e) {
        debugLog("autoStartFileLink error", e);
      }
    }
  }

  function autoClickRequiredFileDownload() {
    let popupClicked = false;
    const observer = new MutationObserver(() => {
      const popup = document.querySelector(".popup-mod-requirements");
      if (popup) {
        if (!popupClicked) {
          const downloadButton = popup.querySelector("a.btn");
          const exitPopupBtn = popup.querySelector(".mfp-close");
          if (downloadButton) {
            infoLog("Requirements popup detected, auto-clicking download.");
            popupClicked = true;
            downloadButton.click();
            exitPopupBtn?.click();
          }
        }
      } else {
        if (popupClicked) {
          debugLog("Requirements popup closed, resetting click flag.");
          popupClicked = false;
        }
      }
    });

    observer.observe(document.body, { childList: true, subtree: true });
  }

  // Archived files: inject nmm=1 and Manual buttons
  const ICON_PATHS = {
    nmm: "https://www.nexusmods.com/assets/images/icons/icons.svg#icon-nmm",
    manual:
      "https://www.nexusmods.com/assets/images/icons/icons.svg#icon-manual",
  };

  function createArchiveButtonsFor(fileId) {
    const path = `${location.protocol}//${location.host}${location.pathname}`;
    const fragment = document.createDocumentFragment();

    const makeBtn = (href, label, isNmm) => {
      const li = document.createElement("li");
      const a = document.createElement("a");
      a.className = "btn inline-flex";
      a.href = href;
      a.dataset.fileid = fileId;
      a.tabIndex = 0;
      try {
        const svg = document.createElementNS(
          "http://www.w3.org/2000/svg",
          "svg"
        );
        svg.setAttribute(
          "class",
          "icon " + (isNmm ? "icon-nmm" : "icon-manual")
        );
        const use = document.createElementNS(
          "http://www.w3.org/2000/svg",
          "use"
        );
        use.setAttributeNS(
          "http://www.w3.org/1999/xlink",
          "xlink:href",
          isNmm ? ICON_PATHS.nmm : ICON_PATHS.manual
        );
        svg.appendChild(use);
        a.appendChild(svg);
      } catch (_) {
        const spanIcon = document.createElement("span");
        spanIcon.className = "icon " + (isNmm ? "icon-nmm" : "icon-manual");
        a.appendChild(spanIcon);
      }

      const labelSpan = document.createElement("span");
      labelSpan.className = "flex-label";
      labelSpan.textContent = label;
      a.appendChild(labelSpan);

      li.appendChild(a);
      return li;
    };

    const nmmHref = `${path}?tab=files&file_id=${encodeURIComponent(
      fileId
    )}&nmm=1`;
    const manualHref = `${path}?tab=files&file_id=${encodeURIComponent(
      fileId
    )}`;

    fragment.appendChild(makeBtn(nmmHref, "Vortex", true));
    fragment.appendChild(makeBtn(manualHref, "Manual", false));
    return fragment;
  }

  function archivedFile() {
    try {
      if (!window.location.href.includes("category=archived")) return;

      const downloadSections = Array.from(
        document.querySelectorAll(".accordion-downloads")
      );
      const fileHeaders = Array.from(
        document.querySelectorAll(".file-expander-header")
      );

      for (let idx = 0; idx < downloadSections.length; idx++) {
        const section = downloadSections[idx];
        const fileId = fileHeaders[idx]?.getAttribute("data-id");
        if (!fileId) continue;
        try {
          if (section.dataset && section.dataset.nnwInjected === fileId) {
            continue;
          }
        } catch (_) {}

        infoLog("archivedFile: injecting buttons (safe DOM creation)", {
          fileId,
        });
        while (section.firstChild) section.removeChild(section.firstChild);
        section.appendChild(createArchiveButtonsFor(fileId));

        try {
          if (section.dataset) section.dataset.nnwInjected = fileId;
        } catch (_) {}
      }
    } catch (e) {
      errorLog("archivedFile error", e);
    }
  }

  // -------------------------------- UI --------------------------------
  const SETTING_UI = {
    autoCloseTab: {
      name: "Auto-Close tab on download",
      description: "Automatically close tab after download starts",
    },
    skipRequirements: {
      name: "Skip Requirements Popup/Tab",
      description: "Skip requirements page and go straight to download",
    },
    showAlerts: {
      name: "Show Error Alert messages",
      description: "Show error messages as browser alerts",
    },
    refreshOnError: {
      name: "Refresh page on error",
      description:
        "Refresh the page when errors occur (may lead to infinite refresh loop!)",
    },
    requestTimeout: {
      name: "Request Timeout",
      description: "Time to wait for server response before timeout",
    },
    closeTabTime: {
      name: "Auto-Close tab Delay",
      description: "Delay before closing tab after download starts",
    },
    debug: {
      name: "⚠️ Debug Alerts",
      description: "Show all console logs as alerts",
    },
    playErrorSound: {
      name: "Play Error Sound",
      description: "Play a sound when errors occur",
    },
  };

  const STYLES = {
    button: `position:fixed;bottom:20px;right:20px;background:#2f2f2f;color:#fff;padding:10px 15px;border-radius:4px;cursor:pointer;z-index:9999;font-family:'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;font-size:14px;border:none;`,
    modal: `position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background:#2f2f2f;color:#dadada;padding:25px;border-radius:4px;z-index:10000;min-width:300px;max-width:90%;max-height:90vh;overflow-y:auto;font-family:'Inter', 'Helvetica Neue', Helvetica, Arial, sans-serif;`,
    section: `background:#363636;padding:15px;border-radius:4px;margin-bottom:15px;`,
    sectionHeader: `color:#da8e35;margin:0 0 10px 0;font-size:16px;font-weight:500;`,
    input: `background:#2f2f2f;border:1px solid #444;color:#dadada;border-radius:3px;padding:5px;`,
    btn: {
      primary: `padding:8px 15px;border:none;background:#da8e35;color:white;border-radius:3px;cursor:pointer;`,
      secondary: `padding:8px 15px;border:1px solid #da8e35;background:transparent;color:#da8e35;border-radius:3px;cursor:pointer;`,
      advanced: `padding:4px 8px;background:transparent;color:#666;border:none;cursor:pointer;`,
    },
  };

  function createSettingsUI() {
    const btn = document.createElement("div");
    btn.innerHTML = "NexusNoWait++ ⚙️";
    btn.style.cssText = STYLES.button;
    btn.onmouseover = () => (btn.style.transform = "translateY(-2px)");
    btn.onmouseout = () => (btn.style.transform = "translateY(0)");
    btn.onclick = () => {
      if (activeModal) {
        activeModal.remove();
        activeModal = null;
        if (settingsChanged) location.reload();
      } else showSettingsModal();
    };
    document.body.appendChild(btn);
  }

  function generateSettingsHTML() {
    const normalBooleanSettings = Object.entries(SETTING_UI)
      .filter(([k]) => typeof config[k] === "boolean" && k !== "debug")
      .map(
        ([key, { name, description }]) => `
        <div style="margin-bottom:10px;">
          <label title="${description}" style="display:flex;align-items:center;gap:8px;">
            <input type="checkbox" ${
              config[key] ? "checked" : ""
            } data-setting="${key}">
            <span>${name}</span>
          </label>
        </div>`
      )
      .join("");
    const numberSettings = Object.entries(SETTING_UI)
      .filter(([key]) => typeof config[key] === "number")
      .map(
        ([key, { name, description }]) => `
        <div style="margin-bottom:10px;">
          <label title="${description}" style="display:flex;align-items:center;justify-content:space-between;">
            <span>${name}:</span>
            <input type="number" value="${config[key]}" min="0" step="100" data-setting="${key}" style="${STYLES.input};width:120px;">
          </label>
        </div>`
      )
      .join("");
    const advancedSection = `
      <div id="advancedSection" style="display:none;">
        <div style="${STYLES.section}">
          <h4 style="${STYLES.sectionHeader}">Advanced Settings</h4>
          <div style="margin-bottom:10px;">
            <label title="${
              SETTING_UI.debug.description
            }" style="display:flex;align-items:center;gap:8px;">
              <input type="checkbox" ${
                config.debug ? "checked" : ""
              } data-setting="debug"><span>${SETTING_UI.debug.name}</span>
            </label>
          </div>
        </div>
      </div>`;
    return `
      <h3 style="${STYLES.sectionHeader}">NexusNoWait++ Settings</h3>
      <div style="${STYLES.section}"><h4 style="${STYLES.sectionHeader}">Features</h4>${normalBooleanSettings}</div>
      <div style="${STYLES.section}"><h4 style="${STYLES.sectionHeader}">Timing</h4>${numberSettings}</div>
      ${advancedSection}
      <div style="display:flex;justify-content:center;gap:10px;margin-top:20px;">
        <button id="resetSettings" style="${STYLES.btn.secondary}">Reset</button>
        <button id="closeSettings" style="${STYLES.btn.primary}">Save & Close</button>
      </div>
      <div style="text-align:center;margin-top:12px;"><button id="toggleAdvanced" style="${STYLES.btn.advanced}">⚙️ Advanced</button></div>
      <div style="text-align:center;margin-top:12px;color:#666;font-size:12px;">Version ${GM_info.script.version} by Torkelicious</div>
    `;
  }

  let activeModal = null;
  let settingsChanged = false;

  function showSettingsModal() {
    if (activeModal) activeModal.remove();
    settingsChanged = false;
    const modal = document.createElement("div");
    modal.style.cssText = STYLES.modal;
    modal.innerHTML = generateSettingsHTML();

    function updateSetting(element) {
      const setting = element.getAttribute("data-setting");
      const value =
        element.type === "checkbox"
          ? element.checked
          : parseInt(element.value, 10);
      if (typeof value === "number" && isNaN(value)) {
        element.value = config[setting];
        return;
      }
      if (config[setting] !== value) {
        settingsChanged = true;
        window.nexusConfig.setFeature(setting, value);
      }
    }

    modal.addEventListener("change", (e) => {
      if (e.target.hasAttribute("data-setting")) updateSetting(e.target);
    });
    modal.addEventListener("input", (e) => {
      if (e.target.type === "number" && e.target.hasAttribute("data-setting"))
        updateSetting(e.target);
    });

    modal.querySelector("#closeSettings").onclick = () => {
      modal.remove();
      activeModal = null;
      if (settingsChanged) location.reload();
    };
    modal.querySelector("#resetSettings").onclick = () => {
      settingsChanged = true;
      window.nexusConfig.reset();
      saveSettings(config);
      modal.remove();
      activeModal = null;
      location.reload();
    };
    modal.querySelector("#toggleAdvanced").onclick = (e) => {
      const section = modal.querySelector("#advancedSection");
      const isHidden = section.style.display === "none";
      section.style.display = isHidden ? "block" : "none";
      e.target.textContent = `Advanced ${isHidden ? "▲" : "▼"}`;
    };

    document.body.appendChild(modal);
    activeModal = modal;
  }

  function setupDebugMode() {
    if (config.debug) {
      const originalConsole = {
        log: console.log,
        warn: console.warn,
        error: console.error,
      };
      console.log = function () {
        originalConsole.log.apply(console, arguments);
        alert("[Debug Log]\n" + Array.from(arguments).join(" "));
      };
      console.warn = function () {
        originalConsole.warn.apply(console, arguments);
        alert("[Debug Warn]\n" + Array.from(arguments).join(" "));
      };
      console.error = function () {
        originalConsole.error.apply(console, arguments);
        alert("[Debug Error]\n" + Array.from(arguments).join(" "));
      };
      infoLog("Debug mode enabled");
    }
  }

  function scrollToMainFiles() {
    try {
      if (!/\btab=files\b/.test(window.location.href)) return;
      const header = document.querySelector(".file-category-header");
      if (header) header.scrollIntoView();
    } catch (e) {
      /* ignore */
    }
  }

  window.nexusConfig = {
    setFeature(name, value) {
      const old = config[name];
      config[name] = value;
      saveSettings(config);
      if (name !== "debug") applySettings();
      if (old !== value) {
        settingsChanged = true;
        debugLog("Feature changed", name, old, value);
      }
    },
    reset() {
      GM_deleteValue("nexusNoWaitConfig");
      Object.assign(config, DEFAULT_CONFIG);
      saveSettings(config);
      applySettings();
    },
    getConfig() {
      return config;
    },
  };
  function applySettings() {
    setupDebugMode();
  }

  // Initialization
  function isModPage() {
    return /nexusmods\.com\/.*\/mods\//.test(window.location.href);
  }
  function initializeUI() {
    applySettings();
    createSettingsUI();
  }

  function initMainFunctions() {
    if (!isModPage()) {
      debugLog("Not a mod page - skipping");
      return;
    }
    infoLog("Initializing main functions");
    archivedFile();
    document.body.addEventListener("click", delegatedClickHandler, true);
    try {
      getPrimaryFileId();
    } catch (e) {
      debugLog("initMainFunctions: getPrimaryFileId failed", e);
    }
    autoStartFileLink();
    if (config.skipRequirements) autoClickRequiredFileDownload();
    setTimeout(() => {
      try {
        scrollToMainFiles();
      } catch (e) {
        /* ignore */
      }
    }, 200);
    
  }

  // URL Watcher
  (() => {
    let lastHref = location.href;
    const CHECK_MS = 300;

    setInterval(() => {
      try {
        if (location.href === lastHref) return;
        lastHref = location.href;
        debugLog("URL changed ---> running light init for changed tab", {
          href: lastHref,
        });
        // only run lightweight operations needed on navigation:
        if (isModPage()) {
          try {
            archivedFile();
          } catch (e) {
            debugLog("archivedFile error on URL change", e);
          }
          setTimeout(() => {
            try {
              scrollToMainFiles();
            } catch (e) {
              /* ignore */
            }
          }, 150);
        }
      } catch (e) {
        debugLog("URL watcher error", e);
      }
    }, CHECK_MS);
  })();

  let archivedDebounceTimer = null;
  const ARCHIVE_DEBOUNCE_MS = 200;

  const mainObserver = new MutationObserver((mutations) => {
    if (!isModPage()) return;
    try {
      let touched = false;
      mutations.forEach((mutation) => {
        if (!mutation.addedNodes) return;
        mutation.addedNodes.forEach((node) => {
          if (node.nodeType !== 1) return;
          touched = true;
        });
      });
      if (!touched) return;
      clearTimeout(archivedDebounceTimer);
      archivedDebounceTimer = setTimeout(() => {
        try {
          archivedFile();
        } finally {
          archivedDebounceTimer = null;
        }
      }, ARCHIVE_DEBOUNCE_MS);
    } catch (e) {
      errorLog("MutationObserver error", e);
    }
  });

  initializeUI();
  initMainFunctions();

  if (isModPage()) {
    mainObserver.observe(document.body, { childList: true, subtree: true });
    debugLog("Started mutation observer");
    window.addEventListener("unload", () => {
      mainObserver.disconnect();
      debugLog("Unload: disconnected observer");
    });
  }
})();