// ==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();
});
})();