Nexus No Wait

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

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

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

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

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.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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