// ==UserScript== // @name Nexus No Wait ++ // @description Download from Nexusmods.com without wait and redirect (Manual/Vortex/MO2/NMM), Tweaked with extra features. // @namespace NexusNoWaitPlusPlus // @version 1.1.1 // @include https://www.nexusmods.com/* // @run-at document-idle // @iconURL 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_openInTab // @compatible chrome // @compatible edge // @compatible firefox // @compatible brave // @author Torkelicous // @license MIT // @downloadURL none // ==/UserScript== /* global GM_getValue, GM_setValue, GM_deleteValue, GM_xmlhttpRequest, GM_openInTab, GM_info GM */ (function () { // === Configuration Types === /** * @typedef {Object} Config * @property {boolean} autoCloseTab - Close tab automatically after download starts * @property {boolean} skipRequirements - Skip downloading requirements popup/tab * @property {boolean} showAlerts - Show error messages as browser alerts * @property {boolean} refreshOnError - Auto-refresh page when errors occur * @property {number} requestTimeout - AJAX request timeout in milliseconds * @property {number} closeTabTime - Delay before closing tab in milliseconds * @property {boolean} debug - Enable debug mode with detailed alerts * @property {boolean} playErrorSound - Enable error sound notifications */ /** * @typedef {Object} SettingDefinition * @property {string} name - User-friendly setting name * @property {string} description - Detailed setting description for tooltips */ /** * @typedef {Object} UIStyles * @property {string} button - CSS for buttons * @property {string} modal - CSS for modal windows * @property {string} settings - CSS for settings headers * @property {string} section - CSS for sections * @property {string} sectionHeader - CSS for section headers * @property {string} input - CSS for input fields * @property {Object} btn - CSS for button variants */ // === 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 */ 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 }; /** * @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 /** * Global error sound instance (preloaded) * @type {HTMLAudioElement} */ 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 * @returns {void} */ 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); if (config.debug) { alert("[Nexus No Wait ++] (Debug):\n" + message); } return; } playErrorSound(); // Play sound before alert console.error("[Nexus No Wait ++]: " + message); 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", 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: " + (error.message); 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: "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) { if (data) { try { data = JSON.parse(data); if (data.url) { btnSuccess(button); document.location.href = data.url; closeOnDL(); } } catch (e) { btnError(button, e); } } }, error(xhr) { btnError(button, xhr); } }; if (!params.get("nmm")) { ajaxRequest(ajaxOptions); } else { 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) { if (data) { const xml = new DOMParser().parseFromString(data, "text/html"); const slow = xml.getElementById("slowDownloadButton"); if (slow && slow.getAttribute("data-download-url")) { const downloadUrl = slow.getAttribute("data-download-url"); btnSuccess(button); document.location.href = downloadUrl; closeOnDL(); } else { btnError(button); } } }, 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) { // Popup is gone, ready for next appearance logMessage("Popup closed", false, true); } } }); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); } // === Archived Files Handling === // Modifies download links for archived files // Adds both manual and mod manager download options to archived files /** * Tracks if current download is from archives * @type {boolean} */ let isArchiveDownload = false; function archivedFile() { // Only run in the archived category if (!window.location.href.includes('category=archived')) { return; } // Cache DOM queries and path const path = `${location.protocol}//${location.host}${location.pathname}`; const downloadTemplate = (fileId) => `