Nexus No Wait

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

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey, Greasemonkey или Violentmonkey.

Для установки этого скрипта вам необходимо установить расширение, такое как Tampermonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Violentmonkey.

Чтобы установить этот скрипт, вы сначала должны установить расширение браузера, например Tampermonkey или Userscripts.

Чтобы установить этот скрипт, сначала вы должны установить расширение браузера, например Tampermonkey.

Чтобы установить этот скрипт, вы должны установить расширение — менеджер скриптов.

(у меня уже есть менеджер скриптов, дайте мне установить скрипт!)

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение браузера, например Stylus.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

Чтобы установить этот стиль, сначала вы должны установить расширение — менеджер стилей.

(у меня уже есть менеджер стилей, дайте мне установить скрипт!)

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