您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Download from Nexusmods.com without wait and redirect (Manual/Vortex/MO2/NMM), Tweaked with extra features.
// ==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(); }); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址