Nexus No Wait

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

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