Greasy Fork is available in English.

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.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

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