Nexus No Wait

Skip Countdown (supports Manual/Vortex/MO2/NMM); File archive access; Skip Pop-up;

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

You will need to install an extension such as Tampermonkey to install this script.

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

// ==UserScript==
// @name        Nexus No Wait
// @description Skip Countdown (supports Manual/Vortex/MO2/NMM); File archive access; Skip Pop-up;
// @namespace   NexusNoWait
// @include     https://www.nexusmods.com/*/mods/*
// @run-at      document-idle
// @grant       GM.xmlHttpRequest
// @grant       GM_xmlhttpRequest
// @grant       GM_registerMenuCommand
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_listValues
// @grant       unsafeWindow
// @version     2.20
// ==/UserScript==

(function() {
  'use strict';

  var win = unsafeWindow || window;
  var doc = win.document;

  var DEFAULT_CONFIG = {
    ProcessArchivedFiles: true,
    AddButtonArchivedFiles: true,
    AutoStartDownload: true,
    LogAdditionalInfo: false,
    BlockNotifications: true,
  };

  var Config = (function() {
    var settings = Object.assign({}, DEFAULT_CONFIG);
    try {
      GM_listValues().forEach(function(key) {
        if (Object.prototype.hasOwnProperty.call(DEFAULT_CONFIG, key)) {
          settings[key] = GM_getValue(key, DEFAULT_CONFIG[key]);
        }
      });
    } catch (e) { console.warn('NNW: Load config failed', e); }

    return {
      get: function(key) { return settings[key]; },
      set: function(key, val) {
        settings[key] = val;
        GM_setValue(key, val);
      },
      getAll: function() { return settings; }
    };
  })();

  var STYLES = {
    modal: `
      .jsf-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #1e1e1e; border: 2px solid #ff790a; border-radius: 8px; padding: 20px; z-index: 10000; box-shadow: 0 4px 20px rgba(0,0,0,0.3); min-width: 400px; color: #fff; font-family: Arial, sans-serif; }
      .jsf-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 9999; }
      .jsf-head { border-bottom: 1px solid #6441cf; padding-bottom: 10px; margin-bottom: 15px; font-size: 18px; font-weight: bold; }
      .jsf-row { margin-bottom: 10px; display: flex; align-items: center; color: #c3c2c2; cursor: pointer; }
      .jsf-chk { margin-right: 10px; accent-color: #005a87; }
      .jsf-footer { margin-top: 20px; text-align: right; border-top: 1px solid #6441cf; padding-top: 15px; }
      .jsf-btn { margin-left: 10px; padding: 6px 14px; border-radius: 4px; cursor: pointer; border: 1px solid #ccc; background: #ccc; }
      .jsf-btn.p { background: #007cba; color: white; border-color: #007cba; }
      .jsf-btn.p:hover { background: #005a87; }
    `,
    notify: 'position: fixed; top: 20px; right: 20px; padding: 10px 20px; border-radius: 4px; z-index: 10001; color: white; box-shadow: 0 2px 10px rgba(0,0,0,0.2);',
    success: 'background: #4CAF50;',
    error: 'background: #c73015;'
  };

  function el(tag, attrs, children) {
    var element = doc.createElement(tag);
    if (attrs) {
      for (var key in attrs) {
        if (key.startsWith('on') && typeof attrs[key] === 'function') element[key] = attrs[key];
        else if (key === 'text') element.textContent = attrs[key];
        else if (key === 'html') element.innerHTML = attrs[key];
        else element.setAttribute(key, attrs[key]);
      }
    }
    if (children) {
      (Array.isArray(children) ? children : [children]).forEach(function(c) {
        element.appendChild(typeof c === 'string' ? doc.createTextNode(c) : c);
      });
    }
    return element;
  }

  function injectStyles(css) {
    doc.head.appendChild(el('style', { text: css }));
  }

  function notify(text, isError) {
    var div = el('div', { style: STYLES.notify + (isError ? STYLES.error : STYLES.success), text: text });
    doc.body.appendChild(div);
    setTimeout(function() { div.remove(); }, 2000);
  }

  function logError(data) {
    console.error("NNW Error:", data);
    notify('Error! Check console.', true);
  }

  function parsePrm(text) {
    if (!text) return null;
    var raw;
    try {
      raw = JSON.parse(text)?.url;
    } catch (e) {
      raw = String(text).match(/id=['"]dl_link['"].*?value=['"]([^'"]+)['"]/i)?.[1];
    }
    return raw?.replace(/&/g, '&') || null;
  }

  function getLink(text) {
    return text?.match(/(nxm:\/\/[\s\S]+?)(?=["'\s<>]|$)/i)?.[1] ||
           text?.match(/['"]([^'"]*?key[^'"]*?)['"]/)?.[1] || null;
  }

  function getDesc(key) {
    var map = {
      ProcessArchivedFiles: 'Process archived files section',
      AddButtonArchivedFiles: 'Add button for archived files',
      AutoStartDownload: 'Auto-start download from URL',
      LogAdditionalInfo: 'Log info to console',
      BlockNotifications: 'Skip pop-up notifications'
    };
    return map[key] || key;
  }

  function showSettings() {
    var content = [];
    Object.keys(Config.getAll()).forEach(function(key) {
      var chk = el('input', { type: 'checkbox', class: 'jsf-chk', 'data-key': key });
      chk.checked = Config.get(key);
      content.push(el('label', { class: 'jsf-row' }, [chk, getDesc(key)]));
    });

    var overlay = el('div', { class: 'jsf-overlay' });
    var saveBtn = el('button', { class: 'jsf-btn p', text: 'Save', onclick: function() {
      overlay.querySelectorAll('input').forEach(function(c) { Config.set(c.dataset.key, c.checked); });
      overlay.remove();
      notify('Settings saved!');
    }});

    var modal = el('div', { class: 'jsf-modal' }, [
      el('div', { class: 'jsf-head', text: 'Nexus No Wait' }, []),
      el('div', {}, content),
      el('div', { class: 'jsf-footer' }, [
        el('button', { class: 'jsf-btn', text: 'Cancel', onclick: function() { overlay.remove(); } }),
        saveBtn
      ])
    ]);

    overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); };
    overlay.appendChild(modal);
    doc.body.appendChild(overlay);
  }

  GM_registerMenuCommand('⚙️ Settings', showSettings);
  injectStyles(STYLES.modal);

  function makeRequest(opts) {
    var req = GM_xmlhttpRequest || (GM && GM.xmlHttpRequest);
    if (!req) return opts.error && opts.error({ error: 'No XHR' });

    req({
      method: opts.method || 'GET',
      url: opts.url,
      data: opts.data,
      headers: {
        'Origin': 'https://www.nexusmods.com',
        'Referer': win.location.href,
        'X-Requested-With': 'XMLHttpRequest',
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
      },
      onload: function(res) {
        (res.status === 200 && opts.success) ? opts.success(res.responseText) : (opts.error && opts.error(res));
      },
      onerror: opts.error
    });
  }

  var ButtonState = {
    set: function(btn, color, text) {
      btn.style.setProperty('color', color, 'important');
      var span = btn.querySelector('span.flex-label');
      if (span) span.innerText = text;
    }
  };

  function extractParams(htmlContent, params) {
    var url = (typeof htmlContent === 'string' ? htmlContent : htmlContent?.url) || '';
    if (!url) logError('Empty URL');

    params.key = url.match(/md5=([^&"]+)/)?.[1];
    params.expires = url.match(/expires=([^&"]+)/)?.[1];
    params.user_id = url.match(/user_id=([^&"]+)/)?.[1];

    if (!params.key || !params.expires) logError(params);
    return params;
  }

  function handleDownloadClick(e) {
    var btn = e.target.closest('a.btn');
    if (!btn || (!btn.querySelector('.icon-manual, .icon-nmm') && !btn.getAttribute('href'))) return;

    var url = new URL(btn.href || win.location.href);
    var fileId = url.searchParams.get('file_id') || url.searchParams.get('id');
    if (!fileId) return;

    if (Config.get('BlockNotifications') && (/ModRequirementsPopUp|DownloadPopUp/.test(btn.href))) {
       e.stopPropagation(); e.stopImmediatePropagation();
    } else if (/tab=requirements|ModRequirementsPopUp|DownloadPopUp/.test(btn.href)) return;

    e.preventDefault();
    ButtonState.set(btn, 'yellow', 'WAIT');

    var params = {
      fileId: fileId,
      gameId: doc.getElementById('section')?.dataset.gameId || btn.current_game_id,
      isNMM: url.searchParams.has('nmm'),
      game: win.location.pathname.split('/')[1],
      modId: win.location.pathname.split('/')[3],
      prm: /DownloadPopUp/.test(btn.href),
      button: btn
    };

    params.isNMM ? processNMM(params) : processManual(params);

    var popup = btn.closest('.popup');
    if (popup) popup.querySelector('button')?.click();
  }

  function processManual(params) {
    var endpoint = params.prm ? ('Widgets/DownloadPopUp?id=' + params.fileId + '&game_id=' + params.gameId) :
                   'Managers/Downloads?GenerateDownloadUrl';

    makeRequest({
      method: params.prm ? 'GET' : 'POST',
      url: '/Core/Libs/Common/' + endpoint,
      data: params.prm ? null : 'fid=' + params.fileId + '&game_id=' + params.gameId,
      success: function(text) {
        var link = params.prm ? parsePrm(text) : parsePrm(text);
        if (link) {
          ButtonState.set(params.button, 'green', 'Loading');
          win.location.href = link;
        } else ButtonState.set(params.button, 'red', 'ERROR');
      },
      error: function() { ButtonState.set(params.button, 'red', 'ERROR'); }
    });
  }

  function processNMM(params) {
    if (params.prm) {
       makeRequest({
         url: '/Core/Libs/Common/Widgets/DownloadPopUp?id=' + params.fileId + '&game_id=' + params.gameId,
         success: function(text) {
            params = extractParams(text, params);
            var nxm = `nxm://${params.game}/mods/${params.modId}/files/${params.fileId}?key=${params.key}&expires=${params.expires}&user_id=${params.user_id}`;
            win.location.href = nxm;
            ButtonState.set(params.button, 'green', 'Loading');
         }
       });
    } else {
      makeRequest({
        url: win.location.href + '&file_id=' + params.fileId + '&nmm=1',
        success: function(text) {
          var link = getLink(text);
          if (link) {
            win.location.href = link;
            ButtonState.set(params.button, 'green', 'Loading');
          } else ButtonState.set(params.button, 'red', 'ERROR');
        }
      });
    }
  }

  function init() {
    if (Config.get('LogAdditionalInfo')) console.log('NNW Loaded', Config.getAll());

    if (Config.get('AddButtonArchivedFiles') && /[?&]tab=files/.test(win.location.href)) {
       var footer = doc.getElementById('files-tab-footer');
       if (footer && !footer.querySelector('.icon-archive')) {
         footer.innerHTML = `<a class="btn inline-flex" href="${win.location.href}&category=archived"><svg class="icon icon-archive"><use xlink:href="/assets/images/icons/icons.svg#icon-archive"></use></svg><span class="flex-label">File archive</span></a>`;
       }
    }

    if (Config.get('ProcessArchivedFiles') && /[?&]category=archived/.test(win.location.href)) {
      Array.from(doc.getElementsByClassName('accordion-downloads')).forEach(function(el, i) {
        var fid = doc.getElementsByClassName('file-expander-header')[i]?.getAttribute('data-id');
        if (fid) {
           var base = win.location.pathname + '?tab=files&file_id=' + fid;
           el.innerHTML = `<li><a class="btn inline-flex" href="${base}&nmm=1"><svg class="icon icon-nmm"><use xlink:href="https://www.nexusmods.com/assets/images/icons/icons.svg#icon-nmm"></use></svg><span class="flex-label">Mod Manager</span></a></li><li></li><li><a class="btn inline-flex" href="${base}"><svg class="icon icon-manual"><use xlink:href="https://www.nexusmods.com/assets/images/icons/icons.svg#icon-manual"></use></svg><span class="flex-label">Manual</span></a></li>`;
        }
      });
    }

    if (Config.get('AutoStartDownload') && /\bfile_id=/.test(win.location.href)) {
      setTimeout(function() {
        var btn = doc.getElementById('slowDownloadButton') || doc.getElementById('startDownloadButton') ||
                  doc.querySelector('mod-file-download')?.shadowRoot?.getElementById('slowDownloadButton');
        if (btn) btn.click();
      }, 1000);
    }

    doc.body.addEventListener('click', handleDownloadClick, true);
  }

  init();
})();