Nexus No Wait ++

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

Από την 02/09/2025. Δείτε την τελευταία έκδοση.

// ==UserScript==
// @name        Nexus No Wait ++
// @description Download from Nexusmods.com without wait and redirect (Manual/Vortex/MO2/NMM), Tweaked with extra features.
// @namespace   NexusNoWaitPlusPlus
// @author      Torkelicious
// @version     1.1.7
// @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
// @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, // Close tab after download starts
    skipRequirements: true, // Skip requirements popup/tab
    showAlerts: true, // Show errors as browser alerts
    refreshOnError: false, // Refresh page on error
    requestTimeout: 30000, // Request timeout (30 sec)
    closeTabTime: 1000, // Wait before closing tab (1 sec)
    debug: false, // Show debug messages as alerts
    playErrorSound: true, // Play a sound on error
  };

  // === Configuration ===
  /**
   * @typedef {Object} Config
   * @property {boolean} autoCloseTab - Close tab after download starts
   * @property {boolean} skipRequirements - Skip requirements popup/tab
   * @property {boolean} showAlerts - Show errors as browser alerts
   * @property {boolean} refreshOnError - Refresh page on error
   * @property {number} requestTimeout - Request timeout in milliseconds
   * @property {number} closeTabTime - Wait before closing tab in milliseconds
   * @property {boolean} debug - Show debug messages as alerts
   * @property {boolean} playErrorSound - Play a sound on error
   */

  /**
   * @typedef {Object} SettingDefinition
   * @property {string} name - Display name of the setting
   * @property {string} description - Tooltip description
   */

  /**
   * @typedef {Object} UIStyles
   * @property {string} button - Button styles
   * @property {string} modal - Modal window styles
   * @property {string} settings - Settings header styles
   * @property {string} section - Section styles
   * @property {string} sectionHeader - Section header styles
   * @property {string} input - Input field styles
   * @property {Object} btn - Button variant styles
   */

  // === Settings Management ===
  /**
   * Validates settings object against default configuration
   * @param {Object} settings - Settings to validate
   * @returns {Config} Validated settings object
   */
  function validateSettings(settings) {
    if (!settings || typeof settings !== "object") return { ...DEFAULT_CONFIG };

    const validated = { ...settings }; // Keep all existing settings

    // Settings validation
    for (const [key, defaultValue] of Object.entries(DEFAULT_CONFIG)) {
      if (typeof validated[key] !== typeof defaultValue) {
        validated[key] = defaultValue;
      }
    }

    return validated;
  }

  /**
   * Loads settings from storage with validation
   * @returns {Config} Loaded and validated settings
   */
  function loadSettings() {
    try {
      const saved = GM_getValue("nexusNoWaitConfig", null);
      const parsed = saved ? JSON.parse(saved) : DEFAULT_CONFIG;
      return validateSettings(parsed);
    } catch (error) {
      console.warn("GM storage load failed:", error);
      return { ...DEFAULT_CONFIG };
    }
  }

  /**
   * Saves settings to storage
   * @param {Config} settings - Settings to save
   * @returns {void}
   */
  function saveSettings(settings) {
    try {
      GM_setValue("nexusNoWaitConfig", JSON.stringify(settings));
      logMessage("Settings saved to GM storage", false, true);
    } catch (e) {
      console.error("Failed to save settings:", e);
    }
  }
  const config = Object.assign({}, DEFAULT_CONFIG, loadSettings());

  // Create global sound instance

  const errorSound = new Audio(
    "https://github.com/torkelicious/nexus-no-wait-pp/raw/refs/heads/main/errorsound.mp3"
  );
  errorSound.load(); // Preload sound

  // Plays error sound if enabled

  function playErrorSound() {
    if (!config.playErrorSound) return;
    errorSound.play().catch((e) => {
      console.warn("Error playing sound:", e);
    });
  }

  // === Error Handling ===

  /**
   * Centralized logging function
   * @param {string} message - Message to display/log
   * @param {boolean} [showAlert=false] - If true, shows browser alert
   * @param {boolean} [isDebug=false] - If true, handles debug logs
   * @returns {void}
   */
  function logMessage(message, showAlert = false, isDebug = false) {
    if (isDebug) {
      console.log(
        "[Nexus No Wait ++]: " + message + "\nPage:" + window.location.href
      );
      if (config.debug) {
        alert("[Nexus No Wait ++] (Debug):\n" + message);
      }
      return;
    }

    playErrorSound(); // Play sound before alert
    console.error(
      "[Nexus No Wait ++]: " + message + "\nPage:" + window.location.href
    );
    if (showAlert && config.showAlerts) {
      alert("[Nexus No Wait ++] \n" + message);
    }

    if (config.refreshOnError) {
      location.reload();
    }
  }

  // === URL and Navigation Handling ===
  /**
   * Auto-redirects from requirements to files
   */
  if (
    window.location.href.includes("tab=requirements") &&
    config.skipRequirements
  ) {
    const newUrl = window.location.href.replace(
      "tab=requirements",
      "tab=files"
    );
    window.location.replace(newUrl);
    return;
  }

  // === AJAX Setup and Configuration ===
  let ajaxRequestRaw;
  if (typeof GM_xmlhttpRequest !== "undefined") {
    ajaxRequestRaw = GM_xmlhttpRequest;
  } else if (
    typeof GM !== "undefined" &&
    typeof GM.xmlHttpRequest !== "undefined"
  ) {
    ajaxRequestRaw = GM.xmlHttpRequest;
  }

  // Wrapper for AJAX requests
  function ajaxRequest(obj) {
    if (!ajaxRequestRaw) {
      logMessage(
        "AJAX functionality not available (Your browser or userscript manager may not support these requests!)",
        true
      );
      return;
    }
    ajaxRequestRaw({
      method: obj.type,
      url: obj.url,
      data: obj.data,
      headers: obj.headers,
      onload: function (response) {
        if (response.status >= 200 && response.status < 300) {
          obj.success(response.responseText);
        } else {
          obj.error(response);
        }
      },
      onerror: function (response) {
        obj.error(response);
      },
      ontimeout: function (response) {
        obj.error(response);
      },
    });
  }

  // === Button Management ===

  /**
   * Updates button appearance and shows errors
   * @param {HTMLElement} button - The button element
   * @param {Error|Object} error - Error details
   */
  function btnError(button, error) {
    button.style.color = "red";
    let errorMessage = "Download failed: ";
    if (error) {
      if (typeof error === "string") {
        errorMessage += error;
      } else if (error.message) {
        errorMessage += error.message;
      } else if (error.status) {
        errorMessage += `HTTP ${error.status} ${error.statusText || ""}`;
      } else if (typeof error.responseText === "string") {
        errorMessage += error.responseText;
      } else {
        errorMessage += JSON.stringify(error);
      }
    } else {
      errorMessage += "Unknown error";
    }
    button.innerText = "ERROR: " + errorMessage;
    logMessage(errorMessage, true);
  }

  function btnSuccess(button) {
    button.style.color = "green";
    button.innerText = "Downloading!";
    logMessage("Download started.", false, true);
  }

  function btnWait(button) {
    button.style.color = "yellow";
    button.innerText = "Wait...";
    logMessage("Loading...", false, true);
  }

  // Closes tab after download starts
  function closeOnDL() {
    if (config.autoCloseTab && !isArchiveDownload) {
      // Modified to check for archive downloads
      setTimeout(() => window.close(), config.closeTabTime);
    }
  }

  // === Download Handling ===
  /**
   * Main click event handler for download buttons
   * Handles both manual and mod manager downloads
   * @param {Event} event - Click event object
   */
  function clickListener(event) {
    // Skip if this is an archive download
    if (isArchiveDownload) {
      isArchiveDownload = false; // Reset the flag
      return;
    }

    const href = this.href || window.location.href;
    const params = new URL(href).searchParams;

    if (params.get("file_id")) {
      let button = event;
      if (this.href) {
        button = this;
        event.preventDefault();
      }
      btnWait(button);

      const section = document.getElementById("section");
      const gameId = section ? section.dataset.gameId : this.current_game_id;

      let fileId = params.get("file_id");
      if (!fileId) {
        fileId = params.get("id");
      }

      const ajaxOptions = {
        type: "POST",
        url: "/Core/Libs/Common/Managers/Downloads?GenerateDownloadUrl",
        data: "fid=" + fileId + "&game_id=" + gameId,
        headers: {
          Origin: "*",
          Referer: href,
          "Sec-Fetch-Site": "same-origin",
          "X-Requested-With": "XMLHttpRequest",
          "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
        },
        success(data) {
          if (data) {
            try {
              data = JSON.parse(data);
              if (data.url) {
                btnSuccess(button);
                document.location.href = data.url;
                closeOnDL();
              } else {
                btnError(button, {
                  message: "No download URL returned from server",
                });
              }
            } catch (e) {
              btnError(button, e);
            }
          } else {
            btnError(button, { message: "Empty response from server" });
          }
        },
        error(xhr) {
          btnError(button, xhr);
        },
      };

      if (!params.get("nmm")) {
        ajaxRequest(ajaxOptions);
      } else {
        ajaxRequest({
          type: "GET",
          url: href,
          headers: {
            Origin: "*",
            Referer: document.location.href,
            "Sec-Fetch-Site": "same-origin",
            "X-Requested-With": "XMLHttpRequest",
          },
          success(data) {
            if (data) {
              try {
                data = JSON.parse(data);
                if (data.url) {
                  btnSuccess(button);
                  document.location.href = data.url;
                  closeOnDL();
                } else {
                  btnError(button, {
                    message: "No download URL returned from server",
                  });
                }
              } catch (e) {
                btnError(button, e);
              }
            } else {
              btnError(button, { message: "Empty response from server" });
            }
          },
          error(xhr) {
            btnError(button, xhr);
          },
        });
      }

      const popup = this.parentNode;
      if (popup && popup.classList.contains("popup")) {
        popup.getElementsByTagName("button")[0].click();
        const popupButton = document.getElementById("popup" + fileId);
        if (popupButton) {
          btnSuccess(popupButton);
          closeOnDL();
        }
      }
    } else if (/ModRequirementsPopUp/.test(href)) {
      const fileId = params.get("id");

      if (fileId) {
        this.setAttribute("id", "popup" + fileId);
      }
    }
  }

  // === Event Listeners  ===
  /**
   * Attaches click event listener with proper context
   * @param {HTMLElement} el - the element to attach listener to
   */
  function addClickListener(el) {
    el.addEventListener("click", clickListener, true);
  }

  // Attaches click event listeners to multiple elements
  function addClickListeners(els) {
    for (let i = 0; i < els.length; i++) {
      addClickListener(els[i]);
    }
  }

  // === Automatic Downloading ===
  function autoStartFileLink() {
    if (/file_id=/.test(window.location.href)) {
      clickListener(document.getElementById("slowDownloadButton"));
    }
  }

  // Automatically skips file requirements popup and downloads
  function autoClickRequiredFileDownload() {
    const observer = new MutationObserver(() => {
      const downloadButton = document.querySelector(
        ".popup-mod-requirements a.btn"
      );
      if (downloadButton) {
        downloadButton.click();
        const popup = document.querySelector(".popup-mod-requirements");
        if (!popup) {
          logMessage("Popup closed", false, true);
        }
      }
    });

    observer.observe(document.body, {
      childList: true,
      subtree: true,
      attributes: true,
      attributeFilter: ["style", "class"],
    });
  }

  // === Archived Files Handling ===

  //SVG paths
  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",
  };

  /**
   * Tracks if current download is from archives
   * @type {boolean}
   */
  let isArchiveDownload = false;

  function archivedFile() {
    try {
      // Only run in the archived category
      if (!window.location.href.includes("category=archived")) {
        return;
      }

      //  DOM queries and paths
      const path = `${location.protocol}//${location.host}${location.pathname}`;
      const downloadTemplate = (fileId) => `
                <li>
                    <a class="btn inline-flex download-btn"
                       href="${path}?tab=files&file_id=${fileId}&nmm=1"
                       data-fileid="${fileId}"
                       data-manager="true"
                       tabindex="0">
                        <svg title="" class="icon icon-nmm">
                            <use xlink:href="${ICON_PATHS.nmm}"></use>
                        </svg>
                        <span class="flex-label">Mod manager download</span>
                    </a>
                </li>
                <li>
                    <a class="btn inline-flex download-btn"
                       href="${path}?tab=files&file_id=${fileId}"
                       data-fileid="${fileId}"
                       data-manager="false"
                       tabindex="0">
                        <svg title="" class="icon icon-manual">
                            <use xlink:href="${ICON_PATHS.manual}"></use>
                        </svg>
                        <span class="flex-label">Manual download</span>
                    </a>
                </li>`;

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

      downloadSections.forEach((section, index) => {
        const fileId = fileHeaders[index]?.getAttribute("data-id");
        if (fileId) {
          section.innerHTML = downloadTemplate(fileId);
          const buttons = section.querySelectorAll(".download-btn");
          buttons.forEach((btn) => {
            btn.addEventListener("click", function (e) {
              e.preventDefault();
              isArchiveDownload = true;
              // Use existing download logic
              clickListener.call(this, e);
              // Reset flag after small delay
              setTimeout(() => (isArchiveDownload = false), 100);
            });
          });
        }
      });
    } catch (error) {
      logMessage("Error with archived file: " + error.message, true);
      console.error("Archived file error:", error);
    }
  }
  // --------------------------------------------- === 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 (Setting this too low may prevent download from starting!)",
    },
    debug: {
      name: "⚠️ Debug Alerts",
      description:
        "Show all console logs as alerts, don't enable unless you know what you are doing!",
    },
    playErrorSound: {
      name: "Play Error Sound",
      description: "Play a sound when errors occur",
    },
  };

  // Extract UI styles
  const STYLES = {
    button: `
            position:fixed;
            bottom:20px;
            right:20px;
            background:#2f2f2f;
            color:white;
            padding:10px 15px;
            border-radius:4px;
            cursor:pointer;
            box-shadow:0 2px 8px rgba(0,0,0,0.2);
            z-index:9999;
            font-family:-apple-system, system-ui, sans-serif;
            font-size:14px;
            transition:all 0.2s ease;
            border:none;`,
    modal: `
            position:fixed;
            top:50%;
            left:50%;
            transform:translate(-50%, -50%);
            background:#2f2f2f;
            color:#dadada;
            padding:25px;
            border-radius:4px;
            box-shadow:0 2px 20px rgba(0,0,0,0.3);
            z-index:10000;
            min-width:300px;
            max-width:90%;
            max-height:90vh;
            overflow-y:auto;
            font-family:-apple-system, system-ui, sans-serif;`,
    settings: `
            margin:0 0 20px 0;
            color:#da8e35;
            font-size:18px;
            font-weight:600;`,
    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;
                transition:all 0.2s ease;`,
      secondary: `
                padding:8px 15px;
                border:1px solid #da8e35;
                background:transparent;
                color:#da8e35;
                border-radius:3px;
                cursor:pointer;
                transition:all 0.2s ease;`,
      advanced: `
                padding: 4px 8px;
                border: none;
                background: transparent;
                color: #666;
                font-size: 12px;
                cursor: pointer;
                transition: all 0.2s ease;
                opacity: 0.6;
                text-decoration: underline;
                &:hover {
                    opacity: 1;
                    color: #da8e35;
                }`,
    },
  };

  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) {
          // Only reload if settings were changed
          location.reload();
        }
      } else {
        showSettingsModal();
      }
    };
    document.body.appendChild(btn);
  }

  //  settings UI
  /**
   * Creates settings UI HTML
   * @returns {string} Generated HTML
   */
  function generateSettingsHTML() {
    const normalBooleanSettings = Object.entries(SETTING_UI)
      .filter(
        ([key]) => typeof config[key] === "boolean" && !["debug"].includes(key)
      )
      .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("");

    // debug section
    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.settings}">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="margin-top:20px;display:flex;justify-content:center;gap:10px;">
                <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: 15px;">
                <button id="toggleAdvanced" style="${STYLES.btn.advanced}">⚙️ Advanced</button>
            </div>
            <div style="text-align: center; margin-top: 15px; color: #666; font-size: 12px;">
                Version ${GM_info.script.version}
                \n by Torkelicious
            </div>`;
  }

  let activeModal = null;
  let settingsChanged = false; // Track settings changes

  /**
   * Shows settings and handles interactions
   * @returns {void}
   */
  function showSettingsModal() {
    if (activeModal) {
      activeModal.remove();
    }

    settingsChanged = false; // Reset change tracker
    const modal = document.createElement("div");
    modal.style.cssText = STYLES.modal;

    modal.innerHTML = generateSettingsHTML();

    //  update function
    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;
      // Only reload if settings were changed
      if (settingsChanged) {
        location.reload();
      }
    };

    modal.querySelector("#resetSettings").onclick = () => {
      settingsChanged = true; // Reset counts as a change
      window.nexusConfig.reset();
      saveSettings(config);
      modal.remove();
      activeModal = null;
      location.reload();
    };

    // toggle handler for advanced section
    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;
  }

  // Override console when debug is enabled
  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(" "));
      };
    }
  }

  // === Configuration ===
  window.nexusConfig = {
    /**
     * Sets a feature setting
     * @param {string} name - Setting name
     * @param {any} value - Setting value
     */
    setFeature: (name, value) => {
      const oldValue = config[name];
      config[name] = value;
      saveSettings(config);

      // Only apply non-debug settings fast
      if (name !== "debug") {
        applySettings();
      }

      // Mark settings as changed if value actually changed
      if (oldValue !== value) {
        settingsChanged = true;
      }
    },

    // Resets all settings to defaults

    reset: () => {
      GM_deleteValue("nexusNoWaitConfig");
      Object.assign(config, DEFAULT_CONFIG);
      saveSettings(config);
      applySettings(); // Apply changes
    },

    // Gets current configuration

    getConfig: () => config,
  };

  function applySettings() {
    // Update AJAX timeout
    if (ajaxRequestRaw) {
      ajaxRequestRaw.timeout = config.requestTimeout;
    }
    setupDebugMode();
  }
  // ------------------------------------------------------------------------------------------------ //

  // ===  Initialization ===
  /**
   * Checks if current URL is a mod page
   * @returns {boolean} True if URL matches mod pattern
   */
  function isModPage() {
    return /nexusmods\.com\/.*\/mods\//.test(window.location.href);
  }

  //Initializes UI components
  function initializeUI() {
    applySettings();
    createSettingsUI();
  }

  //Initializes main functions if on modpage
  function initMainFunctions() {
    if (!isModPage()) return;

    archivedFile();
    addClickListeners(document.querySelectorAll("a.btn"));
    autoStartFileLink();
    if (config.skipRequirements) {
      autoClickRequiredFileDownload();
    }
  }

  // Combined observer
  const mainObserver = new MutationObserver((mutations) => {
    if (!isModPage()) return;

    try {
      mutations.forEach((mutation) => {
        if (!mutation.addedNodes) return;

        mutation.addedNodes.forEach((node) => {
          if (node.tagName === "A" && node.classList?.contains("btn")) {
            addClickListener(node);
          }

          if (node.querySelectorAll) {
            addClickListeners(node.querySelectorAll("a.btn"));
          }
        });
      });
    } catch (error) {
      console.error("Error in mutation observer:", error);
    }
  });

  // Initialize everything
  initializeUI();
  initMainFunctions();

  // Start observing
  mainObserver.observe(document, {
    childList: true,
    subtree: true,
  });

  // Cleanup on page unload
  window.addEventListener("unload", () => {
    mainObserver.disconnect();
  });
})();
长期地址
遇到问题?请前往 GitHub 提 Issues。