Nexus No Wait ++ (Fork)

Skip countdowns, auto-start downloads, archived file support, and Nexus SPA fixes. Fork of Nexus No Wait ++ with UI + safety improvements.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, Greasemonkey alebo Violentmonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey, % alebo Violentmonkey.

Na nainštalovanie skriptu si budete musieť nainštalovať rozšírenie, ako napríklad Tampermonkey alebo Userscripts.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie, ako napríklad Tampermonkey.

Na inštaláciu tohto skriptu je potrebné nainštalovať rozšírenie správcu používateľských skriptov.

(Už mám správcu používateľských skriptov, nechajte ma ho nainštalovať!)

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie, ako napríklad Stylus.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

Na inštaláciu tohto štýlu je potrebné nainštalovať rozšírenie správcu používateľských štýlov.

(Už mám správcu používateľských štýlov, nechajte ma ho nainštalovať!)

// ==UserScript==
// @name        Nexus No Wait ++ (Fork)
// @description Skip countdowns, auto-start downloads, archived file support, and Nexus SPA fixes. Fork of Nexus No Wait ++ with UI + safety improvements.
// @version     2.1.2
// @namespace   NexusNoWaitPlusPlusFork
// @author      StrangeT (Orig) + Torkelicious + ShredGman
// @iconURL     https://raw.githubusercontent.com/torkelicious/nexus-no-wait-pp/refs/heads/main/icon.png
// @icon        https://raw.githubusercontent.com/torkelicious/nexus-no-wait-pp/refs/heads/main/icon.png
// @license      GPL-3.0-or-later
// @match        https://*.nexusmods.com/*
// @run-at       document-idle
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @grant        GM_info
// @connect      nexusmods.com
// ==/UserScript==

(function () {
  'use strict'

  const SCRIPT_NAME = 'NexusNoWait++'
  const VERSION = GM_info?.script?.version || 'dev'

  // ────────────────────────────────────────────────
  // GM API fallbacks
  // ────────────────────────────────────────────────
  const GMX = {
    get:    (k, d) => typeof GM_getValue === 'function' ? GM_getValue(k, d) : d,
    set:    (k, v) => typeof GM_setValue === 'function' && GM_setValue(k, v),
    del:    (k) => typeof GM_deleteValue === 'function' && GM_deleteValue(k),
    style:  (css) => typeof GM_addStyle === 'function' && GM_addStyle(css),
  }

  const xhr = GM_xmlhttpRequest || (window.GM?.xmlHttpRequest) || null
  if (!xhr) {
    console.error(`[${SCRIPT_NAME}] GM_xmlhttpRequest missing`)
    return
  }

  // ────────────────────────────────────────────────
  // Config
  // ────────────────────────────────────────────────
  const CFG_PREFIX = 'nnwpp_'
  const DEFAULTS = {
    AutoStartDownload:   true,
    AutoCloseTab:        true,
    SkipRequirements:    true,
    ShowAlertsOnError:   true,
    HidePremiumUpsells:  true,
    HandleArchivedFiles: true,
    CloseTabDelay:       1800,
    RequestTimeout:      25000,
  }

  function loadConfig() {
    const cfg = { ...DEFAULTS }
    for (const k of Object.keys(DEFAULTS)) {
      const v = GMX.get(CFG_PREFIX + k, null)
      if (v !== null) cfg[k] = v === 'true' ? true : v === 'false' ? false : Number(v) || v
    }
    return cfg
  }

  let cfg = loadConfig()

  function save(key, value) {
    GMX.set(CFG_PREFIX + key, String(value))
    cfg[key] = value
  }

  // ────────────────────────────────────────────────
  // Logger
  // ────────────────────────────────────────────────
  const log = {
    debug: (...a) => console.debug(`[${SCRIPT_NAME} ${VERSION}]`, ...a),
    info:  (...a) => console.info (`[${SCRIPT_NAME} ${VERSION}]`, ...a),
    warn:  (...a) => console.warn (`[${SCRIPT_NAME} ${VERSION}]`, ...a),
    error: (...a) => console.error(`[${SCRIPT_NAME} ${VERSION}]`, ...a),
  }

  // ────────────────────────────────────────────────
  // Notifications (color-coded toasts)
  // ────────────────────────────────────────────────
  GMX.style(`
    .nnw-toast {
      position: fixed;
      top: 16px;
      right: 16px;
      z-index: 9999999;
      padding: 10px 16px;
      border-radius: 6px;
      color: white;
      font-weight: 500;
      box-shadow: 0 4px 12px rgba(0,0,0,0.4);
      min-width: 180px;
      opacity: 0;
      transform: translateY(-10px);
      transition: all 0.3s ease;
    }
    .nnw-toast.show  { opacity: 1; transform: translateY(0); }
    .nnw-success     { background: #28a745; }
    .nnw-warning     { background: #ffc107; color: #212529; }
    .nnw-error       { background: #dc3545; }
  `)

  function notify(message, type = 'info') { // type: 'success' | 'warning' | 'error'
    const div = document.createElement('div')
    div.className = `nnw-toast nnw-${type}`
    div.textContent = message
    document.body.appendChild(div)

    setTimeout(() => div.classList.add('show'), 50)
    setTimeout(() => {
      div.classList.remove('show')
      setTimeout(() => div.remove(), 400)
    }, 2200)
  }

  // ────────────────────────────────────────────────
  // Hide Premium Upsells (CSS)
  // ────────────────────────────────────────────────
  function injectPremiumHider() {
    if (!cfg.HidePremiumUpsells) return

    GMX.style(`
      /* Common premium banners, upsells, go-premium prompts */
      [class*="premium-banner"],
      [class*="GetPremium"],
      [class*="premium-upsell"],
      [class*="upgrade-premium"],
      .premium-upsell,
      .premium-promo,
      .go-premium,
      [href*="premium"],
      [href*="/premium"],
      .premium-cta,
      .nexus-premium-ad,
      .premium-download-notice,
      .slow-download-upsell,
      .premium-speed-limit,
      .premium-teaser,
      .premium-feature-lock,
      /* Vortex-like in-page banners */
      .get-more-mods-bar,
      .premium-feature,
      /* Generic hiding for text containing "premium" in links/buttons */
      a[href*="premium"]:not([href*="account"]):not([href*="billing"]),
      button:contains("Premium"),
      div:contains("Go Premium"),
      div:contains("Unlock with Premium"),
      div:contains("Get Premium"),
      /* Sidebars, footers, headers with premium links */
      .sidebar-premium,
      footer .premium,
      header .premium {
        display: none !important;
      }
    `)

    // Also hide via JS if needed (some are dynamically added)
    const observer = new MutationObserver(() => {
      document.querySelectorAll(
        '[class*="premium"], [id*="premium"], [class*="upsell"], [class*="GetPremium"]'
      ).forEach(el => {
        if (el.textContent.toLowerCase().includes('premium') ||
            el.href?.includes('premium')) {
          el.style.display = 'none'
        }
      })
    })
    observer.observe(document.body, { childList: true, subtree: true })
  }

  // ────────────────────────────────────────────────
  // Network / Download logic (unchanged except notify calls)
  // ────────────────────────────────────────────────
  function request(opts) {
    return new Promise(resolve => {
      xhr({
        method:  'GET',
        timeout: cfg.RequestTimeout,
        ...opts,
        onload:  r => resolve(r),
        onerror: () => resolve(null),
        ontimeout: () => resolve(null),
      })
    })
  }

  async function getDownloadUrl({ fileId, gameId = '', isNMM = false, fullHref = '' }) {
    if (!fileId) return { error: 'No file_id' }

    if (isNMM && fullHref) {
      const html = (await request({ url: fullHref }))?.responseText || ''
      const nxm = html.match(/nxm:\/\/[^\s"']+/i)?.[0]
      if (nxm) return { url: nxm }
    }

    const postData = `fid=${encodeURIComponent(fileId)}&game_id=${encodeURIComponent(gameId)}`

    const res = await request({
      method: 'POST',
      url: '/Core/Libs/Common/Managers/Downloads?GenerateDownloadUrl',
      data: postData,
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
        'X-Requested-With': 'XMLHttpRequest',
      },
    })

    if (!res?.responseText) return { error: 'Request failed' }

    let url = null
    try {
      url = JSON.parse(res.responseText)?.url?.replace(/&/g, '&')
    } catch {}

    if (!url) {
      const m = res.responseText.match(/id=["']dl_link["'][^>]*value=["']([^"']+)/i)
      if (m) url = m[1].replace(/&/g, '&')
    }

    return url ? { url } : { error: 'Could not extract download link (login?)' }
  }

  // ────────────────────────────────────────────────
  // Click handler with notifications
  // ────────────────────────────────────────────────
  document.addEventListener('click', async e => {
    const a = e.target.closest('a[href], button')
    if (!a) return

    let href = a.href || a.getAttribute('href')
    if (!href) return

    const url = new URL(href, location.href)
    const fileId = url.searchParams.get('file_id') || url.searchParams.get('id')
    if (!fileId) return

    if (url.searchParams.has('requirements') && !cfg.SkipRequirements) return

    e.preventDefault()
    e.stopImmediatePropagation()

    const gameId = document.querySelector('[data-game-id]')?.dataset?.gameId || ''

    notify('Preparing download...', 'warning')

    const { url: dlUrl, error } = await getDownloadUrl({
      fileId,
      gameId,
      isNMM: url.searchParams.has('nmm'),
      fullHref: href,
    })

    if (dlUrl) {
      notify('Download started!', 'success')
      location.assign(dlUrl)
    } else if (error) {
      notify(`Error: ${error}`, 'error')
      if (cfg.ShowAlertsOnError) alert(`${SCRIPT_NAME}\n\n${error}`)
      log.error(error)
    }
  }, true)

  // ────────────────────────────────────────────────
  // Auto-start with notifications
  // ────────────────────────────────────────────────
  async function tryAutoStart() {
    if (!cfg.AutoStartDownload) return

    const p = new URLSearchParams(location.search)
    const fileId = p.get('file_id')
    if (!fileId) return

    const gameId = document.querySelector('[data-game-id]')?.dataset?.gameId || ''

    notify('Auto-starting download...', 'warning')

    const { url: dl, error } = await getDownloadUrl({
      fileId,
      gameId,
      isNMM: p.has('nmm'),
      fullHref: location.href,
    })

    if (dl) {
      notify('Download started!', 'success')
      location.assign(dl)
      if (cfg.AutoCloseTab) {
        setTimeout(() => window.close(), cfg.CloseTabDelay)
      }
    } else if (error) {
      notify(`Auto-start failed: ${error}`, 'error')
      if (cfg.ShowAlertsOnError) alert(error)
    }
  }

  // ────────────────────────────────────────────────
  // Archived files (unchanged)
  // ────────────────────────────────────────────────
  function patchArchivedFiles() {
    if (!cfg.HandleArchivedFiles) return
    if (!location.search.includes('archived')) return

    document.querySelectorAll('[data-file-id]:not([data-nnw-done])').forEach(el => {
      el.dataset.nnwDone = '1'
      const id = el.dataset.fileId
      const base = location.origin + location.pathname

      const div = document.createElement('div')
      div.className = 'nnw-archived-links'
      div.innerHTML = `
        <a href="${base}?file_id=${id}&nmm=1" class="btn small">Mod Manager</a>
        <a href="${base}?file_id=${id}"       class="btn small">Manual</a>
      `
      el.appendChild(div)
    })
  }

  // ────────────────────────────────────────────────
  // Settings panel (added new option)
  // ────────────────────────────────────────────────
  GMX.style(`
    #nnw-settings {
      position: fixed;
      bottom: 16px;
      right: 16px;
      z-index: 999999;
      background: #1e1e1e;
      color: #e0e0e0;
      border: 1px solid #444;
      border-radius: 8px;
      padding: 10px 14px;
      font-size: 13px;
      box-shadow: 0 6px 24px rgba(0,0,0,0.5);
      max-width: 260px;
    }
    #nnw-settings summary {
      cursor: pointer;
      font-weight: 600;
      user-select: none;
    }
    #nnw-settings label {
      display: flex;
      align-items: center;
      gap: 8px;
      margin: 6px 0 0;
      font-size: 12.5px;
    }
    #nnw-settings input[type="checkbox"] {
      accent-color: #ff6a00;
    }
  `)

  function injectSettings() {
    if (document.getElementById('nnw-settings')) return

    const box = document.createElement('details')
    box.id = 'nnw-settings'
    box.open = false

    let html = `<summary>Nexus No Wait ++ ${VERSION}</summary>`

    for (const [k, defVal] of Object.entries(DEFAULTS)) {
      if (typeof defVal !== 'boolean') continue
      html += `
        <label>
          <input type="checkbox" data-key="${k}" ${cfg[k] ? 'checked' : ''}>
          ${k.replace(/([A-Z])/g, ' $1')}
        </label>`
    }

    box.innerHTML = html

    box.addEventListener('change', e => {
      const el = e.target
      if (!el.dataset?.key) return
      save(el.dataset.key, el.checked)
      // Re-apply premium hider if toggled
      if (el.dataset.key === 'HidePremiumUpsells') {
        if (el.checked) injectPremiumHider()
        else location.reload() // simplest way to unhide
      }
    })

    document.body.appendChild(box)
  }

  // ────────────────────────────────────────────────
  // SPA navigation
  // ────────────────────────────────────────────────
  let lastLocation = location.href

  function onNavigate() {
    if (location.href === lastLocation) return
    lastLocation = location.href
    run()
  }

  const originalPush = history.pushState
  const originalReplace = history.replaceState

  history.pushState = function (...args) { originalPush.apply(this, args); onNavigate() }
  history.replaceState = function (...args) { originalReplace.apply(this, args); onNavigate() }

  window.addEventListener('popstate', onNavigate)
  window.addEventListener('hashchange', onNavigate)
  new MutationObserver(onNavigate).observe(document, { subtree: true, childList: true })

  // ────────────────────────────────────────────────
  // Main run
  // ────────────────────────────────────────────────
  function run() {
    tryAutoStart()
    patchArchivedFiles()
    injectSettings()
    injectPremiumHider() // apply on every page/nav
    log.debug('Running on', location.pathname)
  }

  run()
})();