Nexus No Wait ++

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

  1. // ==UserScript==
  2. // @name Nexus No Wait ++
  3. // @description Download from Nexusmods.com without wait and redirect (Manual/Vortex/MO2/NMM), Tweaked with extra features.
  4. // @namespace NexusNoWaitPlusPlus
  5. // @author Torkelicious
  6. // @version 1.1.6
  7. // @include https://*.nexusmods.com/*
  8. // @run-at document-idle
  9. // @iconURL https://raw.githubusercontent.com/torkelicious/nexus-no-wait-pp/refs/heads/main/icon.png
  10. // @icon https://raw.githubusercontent.com/torkelicious/nexus-no-wait-pp/refs/heads/main/icon.png
  11. // @grant GM_xmlhttpRequest
  12. // @grant GM_getValue
  13. // @grant GM_setValue
  14. // @grant GM_deleteValue
  15. // @license MIT
  16. // ==/UserScript==
  17.  
  18. /* global GM_getValue, GM_setValue, GM_deleteValue, GM_xmlhttpRequest, GM_info GM */
  19.  
  20. (function () {
  21.  
  22. const DEFAULT_CONFIG = {
  23. autoCloseTab: true, // Close tab after download starts
  24. skipRequirements: true, // Skip requirements popup/tab
  25. showAlerts: true, // Show errors as browser alerts
  26. refreshOnError: false, // Refresh page on error
  27. requestTimeout: 30000, // Request timeout (30 sec)
  28. closeTabTime: 1000, // Wait before closing tab (1 sec)
  29. debug: false, // Show debug messages as alerts
  30. playErrorSound: true, // Play a sound on error
  31. };
  32.  
  33. // === Configuration ===
  34. /**
  35. * @typedef {Object} Config
  36. * @property {boolean} autoCloseTab - Close tab after download starts
  37. * @property {boolean} skipRequirements - Skip requirements popup/tab
  38. * @property {boolean} showAlerts - Show errors as browser alerts
  39. * @property {boolean} refreshOnError - Refresh page on error
  40. * @property {number} requestTimeout - Request timeout in milliseconds
  41. * @property {number} closeTabTime - Wait before closing tab in milliseconds
  42. * @property {boolean} debug - Show debug messages as alerts
  43. * @property {boolean} playErrorSound - Play a sound on error
  44. */
  45.  
  46. /**
  47. * @typedef {Object} SettingDefinition
  48. * @property {string} name - Display name of the setting
  49. * @property {string} description - Tooltip description
  50. */
  51.  
  52. /**
  53. * @typedef {Object} UIStyles
  54. * @property {string} button - Button styles
  55. * @property {string} modal - Modal window styles
  56. * @property {string} settings - Settings header styles
  57. * @property {string} section - Section styles
  58. * @property {string} sectionHeader - Section header styles
  59. * @property {string} input - Input field styles
  60. * @property {Object} btn - Button variant styles
  61. */
  62.  
  63. // === Settings Management ===
  64. /**
  65. * Validates settings object against default configuration
  66. * @param {Object} settings - Settings to validate
  67. * @returns {Config} Validated settings object
  68. */
  69. function validateSettings(settings) {
  70. if (!settings || typeof settings !== 'object') return {...DEFAULT_CONFIG};
  71.  
  72. const validated = {...settings}; // Keep all existing settings
  73.  
  74. // Settings validation
  75. for (const [key, defaultValue] of Object.entries(DEFAULT_CONFIG)) {
  76. if (typeof validated[key] !== typeof defaultValue) {
  77. validated[key] = defaultValue;
  78. }
  79. }
  80.  
  81. return validated;
  82. }
  83.  
  84. /**
  85. * Loads settings from storage with validation
  86. * @returns {Config} Loaded and validated settings
  87. */
  88. function loadSettings() {
  89. try {
  90. const saved = GM_getValue('nexusNoWaitConfig', null);
  91. const parsed = saved ? JSON.parse(saved) : DEFAULT_CONFIG;
  92. return validateSettings(parsed);
  93. } catch (error) {
  94. console.warn('GM storage load failed:', error);
  95. return {...DEFAULT_CONFIG};
  96. }
  97. }
  98.  
  99. /**
  100. * Saves settings to storage
  101. * @param {Config} settings - Settings to save
  102. * @returns {void}
  103. */
  104. function saveSettings(settings) {
  105. try {
  106. GM_setValue('nexusNoWaitConfig', JSON.stringify(settings));
  107. logMessage('Settings saved to GM storage', false, true);
  108. } catch (e) {
  109. console.error('Failed to save settings:', e);
  110. }
  111. }
  112. const config = Object.assign({}, DEFAULT_CONFIG, loadSettings());
  113.  
  114. // Create global sound instance
  115.  
  116. const errorSound = new Audio('https://github.com/torkelicious/nexus-no-wait-pp/raw/refs/heads/main/errorsound.mp3');
  117. errorSound.load(); // Preload sound
  118.  
  119.  
  120. // Plays error sound if enabled
  121.  
  122. function playErrorSound() {
  123. if (!config.playErrorSound) return;
  124. errorSound.play().catch(e => {
  125. console.warn("Error playing sound:", e);
  126. });
  127. }
  128.  
  129. // === Error Handling ===
  130.  
  131. /**
  132. * Centralized logging function
  133. * @param {string} message - Message to display/log
  134. * @param {boolean} [showAlert=false] - If true, shows browser alert
  135. * @param {boolean} [isDebug=false] - If true, handles debug logs
  136. * @returns {void}
  137. */
  138. function logMessage(message, showAlert = false, isDebug = false) {
  139. if (isDebug) {
  140. console.log("[Nexus No Wait ++]: " + message);
  141. if (config.debug) {
  142. alert("[Nexus No Wait ++] (Debug):\n" + message);
  143. }
  144. return;
  145. }
  146.  
  147. playErrorSound(); // Play sound before alert
  148. console.error("[Nexus No Wait ++]: " + message);
  149. if (showAlert && config.showAlerts) {
  150. alert("[Nexus No Wait ++] \n" + message);
  151. }
  152.  
  153. if (config.refreshOnError) {
  154. location.reload();
  155. }
  156. }
  157.  
  158. // === URL and Navigation Handling ===
  159. /**
  160. * Auto-redirects from requirements to files
  161. */
  162. if (window.location.href.includes('tab=requirements') && config.skipRequirements)
  163. {
  164. const newUrl = window.location.href.replace('tab=requirements', 'tab=files');
  165. window.location.replace(newUrl);
  166. return;
  167. }
  168.  
  169. // === AJAX Setup and Configuration ===
  170. let ajaxRequestRaw;
  171. if (typeof(GM_xmlhttpRequest) !== "undefined")
  172. {
  173. ajaxRequestRaw = GM_xmlhttpRequest;
  174. } else if (typeof(GM) !== "undefined" && typeof(GM.xmlHttpRequest) !== "undefined") {
  175. ajaxRequestRaw = GM.xmlHttpRequest;
  176. }
  177.  
  178. // Wrapper for AJAX requests
  179. function ajaxRequest(obj) {
  180. if (!ajaxRequestRaw) {
  181. logMessage("AJAX functionality not available (Your browser or userscript manager may not support these requests!)", true);
  182. return;
  183. }
  184. ajaxRequestRaw({
  185. method: obj.type,
  186. url: obj.url,
  187. data: obj.data,
  188. headers: obj.headers,
  189. onload: function(response) {
  190. if (response.status >= 200 && response.status < 300) {
  191. obj.success(response.responseText);
  192. } else {
  193. obj.error(response);
  194. }
  195. },
  196. onerror: function(response) {
  197. obj.error(response);
  198. },
  199. ontimeout: function(response) {
  200. obj.error(response);
  201. }
  202. });
  203. }
  204.  
  205. // === Button Management ===
  206.  
  207. /**
  208. * Updates button appearance and shows errors
  209. * @param {HTMLElement} button - The button element
  210. * @param {Error|Object} error - Error details
  211. */
  212. function btnError(button, error) {
  213. button.style.color = "red";
  214. let errorMessage = "Download failed: " + (error.message);
  215. button.innerText = "ERROR: " + errorMessage;
  216. logMessage(errorMessage, true);
  217. }
  218.  
  219. function btnSuccess(button) {
  220. button.style.color = "green";
  221. button.innerText = "Downloading!";
  222. logMessage("Download started.", false, true);
  223. }
  224.  
  225. function btnWait(button) {
  226. button.style.color = "yellow";
  227. button.innerText = "Wait...";
  228. logMessage("Loading...", false, true);
  229. }
  230.  
  231.  
  232. // Closes tab after download starts
  233. function closeOnDL()
  234. {
  235. if (config.autoCloseTab && !isArchiveDownload) // Modified to check for archive downloads
  236. {
  237. setTimeout(() => window.close(), config.closeTabTime);
  238. }
  239. }
  240.  
  241. // === Download Handling ===
  242. /**
  243. * Main click event handler for download buttons
  244. * Handles both manual and mod manager downloads
  245. * @param {Event} event - Click event object
  246. */
  247. function clickListener(event) {
  248. // Skip if this is an archive download
  249. if (isArchiveDownload) {
  250. isArchiveDownload = false; // Reset the flag
  251. return;
  252. }
  253.  
  254. const href = this.href || window.location.href;
  255. const params = new URL(href).searchParams;
  256.  
  257. if (params.get("file_id")) {
  258. let button = event;
  259. if (this.href) {
  260. button = this;
  261. event.preventDefault();
  262. }
  263. btnWait(button);
  264.  
  265. const section = document.getElementById("section");
  266. const gameId = section ? section.dataset.gameId : this.current_game_id;
  267.  
  268. let fileId = params.get("file_id");
  269. if (!fileId) {
  270. fileId = params.get("id");
  271. }
  272.  
  273. const ajaxOptions = {
  274. type: "POST",
  275. url: "/Core/Libs/Common/Managers/Downloads?GenerateDownloadUrl",
  276. data: "fid=" + fileId + "&game_id=" + gameId,
  277. headers: {
  278. Origin: "https://www.nexusmods.com",
  279. Referer: href,
  280. "Sec-Fetch-Site": "same-origin",
  281. "X-Requested-With": "XMLHttpRequest",
  282. "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
  283. },
  284. success(data) {
  285. if (data) {
  286. try {
  287. data = JSON.parse(data);
  288. if (data.url) {
  289. btnSuccess(button);
  290. document.location.href = data.url;
  291. closeOnDL();
  292. }
  293. } catch (e) {
  294. btnError(button, e);
  295. }
  296. }
  297. },
  298. error(xhr) {
  299. btnError(button, xhr);
  300. }
  301. };
  302.  
  303. if (!params.get("nmm")) {
  304. ajaxRequest(ajaxOptions);
  305. } else {
  306. ajaxRequest({
  307. type: "GET",
  308. url: href,
  309. headers: {
  310. Origin: "https://www.nexusmods.com",
  311. Referer: document.location.href,
  312. "Sec-Fetch-Site": "same-origin",
  313. "X-Requested-With": "XMLHttpRequest"
  314. },
  315. success(data) {
  316. if (data) {
  317. const xml = new DOMParser().parseFromString(data, "text/html");
  318. const slow = xml.getElementById("slowDownloadButton");
  319. if (slow && slow.getAttribute("data-download-url")) {
  320. const downloadUrl = slow.getAttribute("data-download-url");
  321. btnSuccess(button);
  322. document.location.href = downloadUrl;
  323. closeOnDL();
  324. } else {
  325. btnError(button);
  326. }
  327. }
  328. },
  329. error(xhr) {
  330. btnError(button, xhr);
  331. }
  332. });
  333. }
  334.  
  335. const popup = this.parentNode;
  336. if (popup && popup.classList.contains("popup")) {
  337. popup.getElementsByTagName("button")[0].click();
  338. const popupButton = document.getElementById("popup" + fileId);
  339. if (popupButton) {
  340. btnSuccess(popupButton);
  341. closeOnDL();
  342. }
  343. }
  344. } else if (/ModRequirementsPopUp/.test(href)) {
  345. const fileId = params.get("id");
  346.  
  347. if (fileId) {
  348. this.setAttribute("id", "popup" + fileId);
  349. }
  350. }
  351. }
  352.  
  353. // === Event Listeners ===
  354. /**
  355. * Attaches click event listener with proper context
  356. * @param {HTMLElement} el - the element to attach listener to
  357. */
  358. function addClickListener(el) {
  359. el.addEventListener("click", clickListener, true);
  360. }
  361.  
  362. // Attaches click event listeners to multiple elements
  363. function addClickListeners(els) {
  364. for (let i = 0; i < els.length; i++) {
  365. addClickListener(els[i]);
  366. }
  367. }
  368.  
  369. // === Automatic Downloading ===
  370. function autoStartFileLink() {
  371. if (/file_id=/.test(window.location.href)) {
  372. clickListener(document.getElementById("slowDownloadButton"));
  373. }
  374. }
  375.  
  376. // Automatically skips file requirements popup and downloads
  377. function autoClickRequiredFileDownload() {
  378. const observer = new MutationObserver(() => {
  379. const downloadButton = document.querySelector(".popup-mod-requirements a.btn");
  380. if (downloadButton) {
  381. downloadButton.click();
  382. const popup = document.querySelector(".popup-mod-requirements");
  383. if (!popup) {
  384. logMessage("Popup closed", false, true);
  385. }
  386. }
  387. });
  388.  
  389. observer.observe(document.body, {
  390. childList: true,
  391. subtree: true,
  392. attributes: true,
  393. attributeFilter: ['style', 'class']
  394. });
  395. }
  396.  
  397. // === Archived Files Handling ===
  398.  
  399. //SVG paths
  400. const ICON_PATHS = {
  401. nmm: 'https://www.nexusmods.com/assets/images/icons/icons.svg#icon-nmm',
  402. manual: 'https://www.nexusmods.com/assets/images/icons/icons.svg#icon-manual'
  403. };
  404.  
  405. /**
  406. * Tracks if current download is from archives
  407. * @type {boolean}
  408. */
  409. let isArchiveDownload = false;
  410.  
  411. function archivedFile() {
  412. try {
  413. // Only run in the archived category
  414. if (!window.location.href.includes('category=archived')) {
  415. return;
  416. }
  417.  
  418. // DOM queries and paths
  419. const path = `${location.protocol}//${location.host}${location.pathname}`;
  420. const downloadTemplate = (fileId) => `
  421. <li>
  422. <a class="btn inline-flex download-btn"
  423. href="${path}?tab=files&file_id=${fileId}&nmm=1"
  424. data-fileid="${fileId}"
  425. data-manager="true"
  426. tabindex="0">
  427. <svg title="" class="icon icon-nmm">
  428. <use xlink:href="${ICON_PATHS.nmm}"></use>
  429. </svg>
  430. <span class="flex-label">Mod manager download</span>
  431. </a>
  432. </li>
  433. <li>
  434. <a class="btn inline-flex download-btn"
  435. href="${path}?tab=files&file_id=${fileId}"
  436. data-fileid="${fileId}"
  437. data-manager="false"
  438. tabindex="0">
  439. <svg title="" class="icon icon-manual">
  440. <use xlink:href="${ICON_PATHS.manual}"></use>
  441. </svg>
  442. <span class="flex-label">Manual download</span>
  443. </a>
  444. </li>`;
  445.  
  446. const downloadSections = Array.from(document.querySelectorAll('.accordion-downloads'));
  447. const fileHeaders = Array.from(document.querySelectorAll('.file-expander-header'));
  448.  
  449. downloadSections.forEach((section, index) => {
  450. const fileId = fileHeaders[index]?.getAttribute('data-id');
  451. if (fileId) {
  452. section.innerHTML = downloadTemplate(fileId);
  453. const buttons = section.querySelectorAll('.download-btn');
  454. buttons.forEach(btn => {
  455. btn.addEventListener('click', function(e) {
  456. e.preventDefault();
  457. isArchiveDownload = true;
  458. // Use existing download logic
  459. clickListener.call(this, e);
  460. // Reset flag after small delay
  461. setTimeout(() => isArchiveDownload = false, 100);
  462. });
  463. });
  464. }
  465. });
  466.  
  467. } catch (error) {
  468. logMessage('Error with archived file: ' + error.message, true);
  469. console.error('Archived file error:', error);
  470. }
  471. }
  472. // --------------------------------------------- === UI === --------------------------------------------- //
  473.  
  474. const SETTING_UI = {
  475. autoCloseTab: {
  476. name: 'Auto-Close tab on download',
  477. description: 'Automatically close tab after download starts'
  478. },
  479. skipRequirements: {
  480. name: 'Skip Requirements Popup/Tab',
  481. description: 'Skip requirements page and go straight to download'
  482. },
  483. showAlerts: {
  484. name: 'Show Error Alert messages',
  485. description: 'Show error messages as browser alerts'
  486. },
  487. refreshOnError: {
  488. name: 'Refresh page on error',
  489. description: 'Refresh the page when errors occur (may lead to infinite refresh loop!)'
  490. },
  491. requestTimeout: {
  492. name: 'Request Timeout',
  493. description: 'Time to wait for server response before timeout'
  494. },
  495. closeTabTime: {
  496. name: 'Auto-Close tab Delay',
  497. description: 'Delay before closing tab after download starts (Setting this too low may prevent download from starting!)'
  498. },
  499. debug: {
  500. name: "⚠️ Debug Alerts",
  501. description: "Show all console logs as alerts, don't enable unless you know what you are doing!"
  502. },
  503. playErrorSound: {
  504. name: 'Play Error Sound',
  505. description: 'Play a sound when errors occur'
  506. },
  507. };
  508.  
  509. // Extract UI styles
  510. const STYLES = {
  511. button: `
  512. position:fixed;
  513. bottom:20px;
  514. right:20px;
  515. background:#2f2f2f;
  516. color:white;
  517. padding:10px 15px;
  518. border-radius:4px;
  519. cursor:pointer;
  520. box-shadow:0 2px 8px rgba(0,0,0,0.2);
  521. z-index:9999;
  522. font-family:-apple-system, system-ui, sans-serif;
  523. font-size:14px;
  524. transition:all 0.2s ease;
  525. border:none;`,
  526. modal: `
  527. position:fixed;
  528. top:50%;
  529. left:50%;
  530. transform:translate(-50%, -50%);
  531. background:#2f2f2f;
  532. color:#dadada;
  533. padding:25px;
  534. border-radius:4px;
  535. box-shadow:0 2px 20px rgba(0,0,0,0.3);
  536. z-index:10000;
  537. min-width:300px;
  538. max-width:90%;
  539. max-height:90vh;
  540. overflow-y:auto;
  541. font-family:-apple-system, system-ui, sans-serif;`,
  542. settings: `
  543. margin:0 0 20px 0;
  544. color:#da8e35;
  545. font-size:18px;
  546. font-weight:600;`,
  547. section: `
  548. background:#363636;
  549. padding:15px;
  550. border-radius:4px;
  551. margin-bottom:15px;`,
  552. sectionHeader: `
  553. color:#da8e35;
  554. margin:0 0 10px 0;
  555. font-size:16px;
  556. font-weight:500;`,
  557. input: `
  558. background:#2f2f2f;
  559. border:1px solid #444;
  560. color:#dadada;
  561. border-radius:3px;
  562. padding:5px;`,
  563. btn: {
  564. primary: `
  565. padding:8px 15px;
  566. border:none;
  567. background:#da8e35;
  568. color:white;
  569. border-radius:3px;
  570. cursor:pointer;
  571. transition:all 0.2s ease;`,
  572. secondary: `
  573. padding:8px 15px;
  574. border:1px solid #da8e35;
  575. background:transparent;
  576. color:#da8e35;
  577. border-radius:3px;
  578. cursor:pointer;
  579. transition:all 0.2s ease;`,
  580. advanced: `
  581. padding: 4px 8px;
  582. border: none;
  583. background: transparent;
  584. color: #666;
  585. font-size: 12px;
  586. cursor: pointer;
  587. transition: all 0.2s ease;
  588. opacity: 0.6;
  589. text-decoration: underline;
  590. &:hover {
  591. opacity: 1;
  592. color: #da8e35;
  593. }`
  594. }
  595. };
  596.  
  597. function createSettingsUI() {
  598. const btn = document.createElement('div');
  599. btn.innerHTML = 'NexusNoWait++ ⚙️';
  600. btn.style.cssText = STYLES.button;
  601.  
  602. btn.onmouseover = () => btn.style.transform = 'translateY(-2px)';
  603. btn.onmouseout = () => btn.style.transform = 'translateY(0)';
  604. btn.onclick = () => {
  605. if (activeModal) {
  606. activeModal.remove();
  607. activeModal = null;
  608. if (settingsChanged) { // Only reload if settings were changed
  609. location.reload();
  610. }
  611. } else {
  612. showSettingsModal();
  613. }
  614. };
  615. document.body.appendChild(btn);
  616. }
  617.  
  618. // settings UI
  619. /**
  620. * Creates settings UI HTML
  621. * @returns {string} Generated HTML
  622. */
  623. function generateSettingsHTML() {
  624. const normalBooleanSettings = Object.entries(SETTING_UI)
  625. .filter(([key]) => typeof config[key] === 'boolean' && !['debug'].includes(key))
  626. .map(([key, {name, description}]) => `
  627. <div style="margin-bottom:10px;">
  628. <label title="${description}" style="display:flex;align-items:center;gap:8px;">
  629. <input type="checkbox"
  630. ${config[key] ? 'checked' : ''}
  631. data-setting="${key}">
  632. <span>${name}</span>
  633. </label>
  634. </div>`).join('');
  635.  
  636. const numberSettings = Object.entries(SETTING_UI)
  637. .filter(([key]) => typeof config[key] === 'number')
  638. .map(([key, {name, description}]) => `
  639. <div style="margin-bottom:10px;">
  640. <label title="${description}" style="display:flex;align-items:center;justify-content:space-between;">
  641. <span>${name}:</span>
  642. <input type="number"
  643. value="${config[key]}"
  644. min="0"
  645. step="100"
  646. data-setting="${key}"
  647. style="${STYLES.input};width:120px;">
  648. </label>
  649. </div>`).join('');
  650.  
  651. // debug section
  652. const advancedSection = `
  653. <div id="advancedSection" style="display:none;">
  654. <div style="${STYLES.section}">
  655. <h4 style="${STYLES.sectionHeader}">Advanced Settings</h4>
  656. <div style="margin-bottom:10px;">
  657. <label title="${SETTING_UI.debug.description}" style="display:flex;align-items:center;gap:8px;">
  658. <input type="checkbox"
  659. ${config.debug ? 'checked' : ''}
  660. data-setting="debug">
  661. <span>${SETTING_UI.debug.name}</span>
  662. </label>
  663. </div>
  664. </div>
  665. </div>`;
  666.  
  667. return `
  668. <h3 style="${STYLES.settings}">NexusNoWait++ Settings</h3>
  669. <div style="${STYLES.section}">
  670. <h4 style="${STYLES.sectionHeader}">Features</h4>
  671. ${normalBooleanSettings}
  672. </div>
  673. <div style="${STYLES.section}">
  674. <h4 style="${STYLES.sectionHeader}">Timing</h4>
  675. ${numberSettings}
  676. </div>
  677. ${advancedSection}
  678. <div style="margin-top:20px;display:flex;justify-content:center;gap:10px;">
  679. <button id="resetSettings" style="${STYLES.btn.secondary}">Reset</button>
  680. <button id="closeSettings" style="${STYLES.btn.primary}">Save & Close</button>
  681. </div>
  682. <div style="text-align: center; margin-top: 15px;">
  683. <button id="toggleAdvanced" style="${STYLES.btn.advanced}">⚙️ Advanced</button>
  684. </div>
  685. <div style="text-align: center; margin-top: 15px; color: #666; font-size: 12px;">
  686. Version ${GM_info.script.version}
  687. \n by Torkelicious
  688. </div>`;
  689. }
  690.  
  691. let activeModal = null;
  692. let settingsChanged = false; // Track settings changes
  693.  
  694. /**
  695. * Shows settings and handles interactions
  696. * @returns {void}
  697. */
  698. function showSettingsModal() {
  699. if (activeModal) {
  700. activeModal.remove();
  701. }
  702.  
  703. settingsChanged = false; // Reset change tracker
  704. const modal = document.createElement('div');
  705. modal.style.cssText = STYLES.modal;
  706.  
  707. modal.innerHTML = generateSettingsHTML();
  708.  
  709. // update function
  710. function updateSetting(element) {
  711. const setting = element.getAttribute('data-setting');
  712. const value = element.type === 'checkbox' ?
  713. element.checked :
  714. parseInt(element.value, 10);
  715.  
  716. if (typeof value === 'number' && isNaN(value)) {
  717. element.value = config[setting];
  718. return;
  719. }
  720.  
  721. if (config[setting] !== value) {
  722. settingsChanged = true;
  723. window.nexusConfig.setFeature(setting, value);
  724. }
  725. }
  726.  
  727. modal.addEventListener('change', (e) => {
  728. if (e.target.hasAttribute('data-setting')) {
  729. updateSetting(e.target);
  730. }
  731. });
  732.  
  733. modal.addEventListener('input', (e) => {
  734. if (e.target.type === 'number' && e.target.hasAttribute('data-setting')) {
  735. updateSetting(e.target);
  736. }
  737. });
  738.  
  739. modal.querySelector('#closeSettings').onclick = () => {
  740. modal.remove();
  741. activeModal = null;
  742. // Only reload if settings were changed
  743. if (settingsChanged) {
  744. location.reload();
  745. }
  746. };
  747.  
  748. modal.querySelector('#resetSettings').onclick = () => {
  749. settingsChanged = true; // Reset counts as a change
  750. window.nexusConfig.reset();
  751. saveSettings(config);
  752. modal.remove();
  753. activeModal = null;
  754. location.reload();
  755. };
  756.  
  757. // toggle handler for advanced section
  758. modal.querySelector('#toggleAdvanced').onclick = (e) => {
  759. const section = modal.querySelector('#advancedSection');
  760. const isHidden = section.style.display === 'none';
  761. section.style.display = isHidden ? 'block' : 'none';
  762. e.target.textContent = `Advanced ${isHidden ? '▲' : '▼'}`;
  763. };
  764.  
  765. document.body.appendChild(modal);
  766. activeModal = modal;
  767. }
  768.  
  769. // Override console when debug is enabled
  770. function setupDebugMode() {
  771. if (config.debug) {
  772. const originalConsole = {
  773. log: console.log,
  774. warn: console.warn,
  775. error: console.error
  776. };
  777.  
  778. console.log = function() {
  779. originalConsole.log.apply(console, arguments);
  780. alert("[Debug Log]\n" + Array.from(arguments).join(' '));
  781. };
  782.  
  783. console.warn = function() {
  784. originalConsole.warn.apply(console, arguments);
  785. alert("[Debug Warn]\n" + Array.from(arguments).join(' '));
  786. };
  787.  
  788. console.error = function() {
  789. originalConsole.error.apply(console, arguments);
  790. alert("[Debug Error]\n" + Array.from(arguments).join(' '));
  791. };
  792. }
  793. }
  794.  
  795. // === Configuration ===
  796. window.nexusConfig = {
  797. /**
  798. * Sets a feature setting
  799. * @param {string} name - Setting name
  800. * @param {any} value - Setting value
  801. */
  802. setFeature: (name, value) => {
  803. const oldValue = config[name];
  804. config[name] = value;
  805. saveSettings(config);
  806.  
  807. // Only apply non-debug settings fast
  808. if (name !== 'debug') {
  809. applySettings();
  810. }
  811.  
  812. // Mark settings as changed if value actually changed
  813. if (oldValue !== value) {
  814. settingsChanged = true;
  815. }
  816. },
  817.  
  818.  
  819. // Resets all settings to defaults
  820.  
  821. reset: () => {
  822. GM_deleteValue('nexusNoWaitConfig');
  823. Object.assign(config, DEFAULT_CONFIG);
  824. saveSettings(config);
  825. applySettings(); // Apply changes
  826. },
  827.  
  828.  
  829. // Gets current configuration
  830.  
  831. getConfig: () => config
  832. };
  833.  
  834. function applySettings() {
  835. // Update AJAX timeout
  836. if (ajaxRequestRaw) {
  837. ajaxRequestRaw.timeout = config.requestTimeout;
  838. }
  839. setupDebugMode();
  840. }
  841. // ------------------------------------------------------------------------------------------------ //
  842.  
  843. // === Initialization ===
  844. /**
  845. * Checks if current URL is a mod page
  846. * @returns {boolean} True if URL matches mod pattern
  847. */
  848. function isModPage() {
  849. return /nexusmods\.com\/.*\/mods\//.test(window.location.href);
  850. }
  851.  
  852.  
  853. //Initializes UI components
  854. function initializeUI() {
  855. applySettings();
  856. createSettingsUI();
  857. }
  858.  
  859. //Initializes main functions if on modpage
  860. function initMainFunctions() {
  861. if (!isModPage()) return;
  862. archivedFile();
  863. addClickListeners(document.querySelectorAll("a.btn"));
  864. autoStartFileLink();
  865. if (config.skipRequirements) {
  866. autoClickRequiredFileDownload();
  867. }
  868. }
  869.  
  870. // Combined observer
  871. const mainObserver = new MutationObserver((mutations) => {
  872. if (!isModPage()) return;
  873. try {
  874. mutations.forEach(mutation => {
  875. if (!mutation.addedNodes) return;
  876.  
  877. mutation.addedNodes.forEach(node => {
  878. if (node.tagName === "A" && node.classList?.contains("btn")) {
  879. addClickListener(node);
  880. }
  881.  
  882. if (node.querySelectorAll) {
  883. addClickListeners(node.querySelectorAll("a.btn"));
  884. }
  885. });
  886. });
  887. } catch (error) {
  888. console.error("Error in mutation observer:", error);
  889. }
  890. });
  891.  
  892. // Initialize everything
  893. initializeUI();
  894. initMainFunctions();
  895.  
  896. // Start observing
  897. mainObserver.observe(document, {
  898. childList: true,
  899. subtree: true
  900. });
  901.  
  902. // Cleanup on page unload
  903. window.addEventListener('unload', () => {
  904. mainObserver.disconnect();
  905. });
  906. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址