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