// ==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.6
// @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 MIT
// ==/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);
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 (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: " + (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) {
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();
});
})();