Nexus No Wait ++

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

当前为 2025-02-12 提交的版本,查看 最新版本

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