NexusMods – Highlight Updated & Downloaded Mods on Nexus

Highlights mods with "Update available" (yellow) or "Downloaded" (green) across Standard, List, and Compact views

// ==UserScript==
// @name         NexusMods – Highlight Updated & Downloaded Mods on Nexus
// @version      1.0.1
// @license      GPL-3.0-or-later
// @description  Highlights mods with "Update available" (yellow) or "Downloaded" (green) across Standard, List, and Compact views
// @author       Flimbo
// @match        https://*.nexusmods.com/games/*/mods*
// @grant        none
// @run-at       document-idle
// @homepageURL  https://github.com/BitGrub/Userscripts
// @homepage     https://github.com/BitGrub/Userscripts
// @supportURL   https://github.com/BitGrub/Userscripts/issues
// @namespace https://greasyfork.org/users/1511951
// ==/UserScript==

(() => {
  'use strict';

  const STYLE_ID  = 'nm-highlighter-style';
  const HL_UPDATE = 'nm-update-card';
  const HL_DOWN   = 'nm-downloaded-card';
  const TILE_SELECTOR = '[data-e2eid="mod-tile"],[data-e2eid="mod-tile-list"],[data-e2eid="mod-tile-standard"],[data-e2eid="mod-tile-compact"]';

  function injectCSS() {
  if (document.getElementById(STYLE_ID)) return;
  const s = document.createElement('style');
  s.id = STYLE_ID;
  s.textContent = `
    @keyframes nm-glow {
      0%, 100% { box-shadow: 0 0 6px rgba(255,213,0,0.6); }
      50%      { box-shadow: 0 0 14px rgba(255,213,0,1); }
    }

    .${HL_UPDATE} {
      outline: 3px solid rgba(255,213,0,.85) !important;
      outline-offset: 2px;
      border-radius: 10px;
      animation: nm-glow 2s ease-in-out infinite;
    }

    .${HL_DOWN} {
      outline: 3px solid rgba(0,200,0,.8) !important;
      outline-offset: 2px;
      border-radius: 10px;
    }
  `;
  document.head.appendChild(s);
}

  function clearHighlights() {
    document.querySelectorAll(TILE_SELECTOR).forEach(t => {
      t.classList.remove(HL_UPDATE, HL_DOWN);
    });
  }

  function applyFromBadges() {
    document.querySelectorAll('[data-e2eid="mod-tile-update-available"]').forEach(b => {
      const tile = b.closest(TILE_SELECTOR);
      if (tile) tile.classList.add(HL_UPDATE);
    });

    document.querySelectorAll('[data-e2eid="mod-tile-downloaded"]').forEach(b => {
      const tile = b.closest(TILE_SELECTOR);
      if (tile && !tile.classList.contains(HL_UPDATE)) tile.classList.add(HL_DOWN);
    });
  }

  let debounceTimer = null;
  function markAllDebounced() {
    if (debounceTimer) clearTimeout(debounceTimer);
    debounceTimer = setTimeout(() => {
      debounceTimer = null;
      clearHighlights();
      applyFromBadges();
    }, 80);
  }

  function boot() {
    injectCSS();
    markAllDebounced();

    // observe DOM for badges/tiles being added (infinite scroll / hydration)
    const mo = new MutationObserver(markAllDebounced);
    mo.observe(document.body, { childList: true, subtree: true });

    // SPA navigation hooks
    const _push = history.pushState, _replace = history.replaceState;
    history.pushState = function() { const r = _push.apply(this, arguments); markAllDebounced(); return r; };
    history.replaceState = function() { const r = _replace.apply(this, arguments); markAllDebounced(); return r; };
    addEventListener('popstate', markAllDebounced);
  }

  if (document.readyState === 'loading') {
    addEventListener('DOMContentLoaded', boot, { once: true });
  } else {
    boot();
  }
})();