Greasy Fork is available in English.
Create Web collections of mods, share them via URL, and view them Groups by game!
// ==UserScript==
// @name Nexus Web Mods Collection
// @namespace http://tampermonkey.net/
// @version 1.1
// @description Create Web collections of mods, share them via URL, and view them Groups by game!
// @author 6969RandomGuy6969
// @match https://www.nexusmods.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=nexusmods.com
// @grant GM_setClipboard
// @grant GM_xmlhttpRequest
// @connect is.gd
// @run-at document-start
// @license MIT
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'nx_saved_collection';
const PARAMS = new URLSearchParams(window.location.search);
const IS_WICKED_TAB = PARAMS.get('collection_view') === 'true';
// Core Helpers
const parseUrl = u => { const m = (u||'').match(/nexusmods\.com\/([^\/]+)\/mods\/(\d+)/i); return m ? { game: m[1], modId: m[2] } : null; };
const store = {
get: () => { try { return JSON.parse(localStorage[STORAGE_KEY] || '[]') } catch { return [] } },
set: d => { try { localStorage[STORAGE_KEY] = JSON.stringify(d); return 1 } catch { return 0 } },
clr: () => delete localStorage[STORAGE_KEY]
};
const share = {
encode: v => btoa(unescape(encodeURIComponent(JSON.stringify(v.map(m => [m.game, m.modId, m.title, (m.thumb||'').replace('https://','').replace('www.nexusmods.com/','NX_').replace('staticdelivery.nexusmods.com/','SD_'), m.author]))))),
decode: c => {
try {
return JSON.parse(decodeURIComponent(escape(atob(c)))).map(m => ({ game: m[0], gameTitle: fmtGame(m[0]), modId: m[1], url: `https://www.nexusmods.com/${m[0]}/mods/${m[1]}`, title: m[2], thumb: m[3] ? 'https://' + m[3].replace('NX_','www.nexusmods.com/').replace('SD_','staticdelivery.nexusmods.com/') : 'https://www.nexusmods.com/assets/images/default/placeholder.png', author: m[4] }));
} catch { return null; }
},
getUrl: c => `${location.origin}/?collection_view=true&share=${encodeURIComponent(c)}&t=${Date.now()}`
};
// Formatting Game Names (Deduplicated globally)
const fmtGame = n => {
const d = { "marvelsspiderman2":"Marvel's Spider-Man 2", "readyornot":"Ready or Not", "cyberpunk2077":"Cyberpunk 2077", "fallout4":"Fallout 4", "stardewvalley":"Stardew Valley", "skyrimspecialedition":"Skyrim Special Edition", "baldursgate3":"Baldur's Gate 3", "witcher3":"The Witcher 3", "palworld":"Palworld", "lethalcompany":"Lethal Company", "helldivers2":"Helldivers 2" };
let f = d[(n||'').toLowerCase()];
if (!f) f = (n||'').replace(/([a-z])([A-Z0-9])/g,'$1 $2').replace(/([0-9])([a-zA-Z])/g,'$1 $2').replace(/_/g,' ').replace(/\b\w/g,s=>s.toUpperCase());
return f ? f.trim() : n;
};
let state = { sel: store.get(), vis: false };
const showToast = (m, err=false) => {
let t = document.getElementById('nx-toast');
if(!t){ t = document.createElement('div'); t.id = 'nx-toast'; t.style.cssText = 'position:fixed;bottom:30px;left:50%;transform:translateX(-50%) translateY(100px);background:#222;color:#fff;padding:12px 24px;border-radius:30px;font-size:14px;font-weight:600;z-index:2147483647;box-shadow:0 10px 30px rgba(0,0,0,.5);border:1px solid rgba(255,255,255,.1);transition:transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275), opacity 0.4s;opacity:0;pointer-events:none;'; document.body.appendChild(t); }
t.style.borderLeft = err ? '4px solid #dc3545' : '4px solid #da8e35'; t.textContent = m;
t.style.opacity = '1'; t.style.transform = 'translateX(-50%) translateY(0)';
clearTimeout(t.tmr); t.tmr = setTimeout(() => { t.style.opacity = '0'; t.style.transform = 'translateX(-50%) translateY(100px)'; }, 3000);
};
const showConfirm = (m, cb) => {
let c = document.getElementById('nx-confirm');
if(!c){ c = document.createElement('div'); c.id = 'nx-confirm'; c.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,.8);backdrop-filter:blur(5px);z-index:2147483647;display:none;align-items:center;justify-content:center;padding:20px'; c.innerHTML = `<div style="background:#1f1f1f;border:1px solid rgba(255,255,255,.1);padding:30px;border-radius:12px;width:100%;max-width:400px;text-align:center;box-shadow:0 24px 64px rgba(0,0,0,.5)"><h3 style="margin-bottom:24px;color:#fff;font-size:18px;font-weight:600" id="nx-c-msg"></h3><div style="display:flex;gap:12px;justify-content:center"><button id="nx-c-no" style="flex:1;padding:12px;border-radius:8px;cursor:pointer;font-weight:600;border:none;background:#444;color:#fff;transition:0.2s">Cancel</button><button id="nx-c-yes" style="flex:1;padding:12px;border-radius:8px;cursor:pointer;font-weight:600;border:none;background:#dc3545;color:#fff;transition:0.2s">Remove</button></div></div>`; document.body.appendChild(c);
c.querySelector('#nx-c-no').onmouseover = function(){ this.style.background='#555'; }; c.querySelector('#nx-c-no').onmouseout = function(){ this.style.background='#444'; };
c.querySelector('#nx-c-yes').onmouseover = function(){ this.style.background='#c82333'; }; c.querySelector('#nx-c-yes').onmouseout = function(){ this.style.background='#dc3545'; };
}
c.querySelector('#nx-c-msg').textContent = m; c.style.display = 'flex';
const hide = () => c.style.display = 'none';
c.onclick = e => { if (e.target === c) hide(); };
c.querySelector('#nx-c-no').onclick = hide;
c.querySelector('#nx-c-yes').onclick = () => { hide(); cb(); };
};
// Find mod index (Deduplicated safe matching)
const getModIdx = u => {
const pu = parseUrl(u);
return pu ? state.sel.findIndex(v => { const p = parseUrl(v.url); return p && p.modId===pu.modId && p.game===pu.game; }) : state.sel.findIndex(v => v.url === u);
};
// Toggle Mod function (Deduplicated core logic)
const toggleMod = (url, title, thumb, author, parsed) => {
const idx = getModIdx(url);
if (idx > -1) state.sel.splice(idx, 1);
else state.sel.push({ url, title, thumb, author, game: parsed.game, gameTitle: fmtGame(parsed.game), modId: parsed.modId });
store.set(state.sel);
return idx === -1;
};
// --- WICKED TAB GRID VIEWER ---
if (IS_WICKED_TAB) {
const s = document.createElement('style');
s.textContent = `
html { background: #2a2a2a !important; } body > *:not(#nx-wicked-app):not(#share-modal):not(#nx-confirm):not(#nx-toast):not(style):not(script) { display: none !important; opacity: 0 !important; }
#nx-wicked-app { animation: nxFadeIn 0.35s cubic-bezier(0,0,0.2,1) forwards; } @keyframes nxFadeIn { 0% { opacity: 0; transform: scale(0.98); } 100% { opacity: 1; transform: scale(1); } }
*{margin:0;padding:0;box-sizing:border-box} body{background:#2a2a2a!important;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,sans-serif;overflow-y:auto;color:#e0e0e0;font-size:14px;line-height:1.6}
::-webkit-scrollbar{width:8px} ::-webkit-scrollbar-track{background:#1a1a1a} ::-webkit-scrollbar-thumb{background:#444;border-radius:4px} ::-webkit-scrollbar-thumb:hover{background:#666}
#grid-header{background:rgba(25,25,25,.95);backdrop-filter:blur(10px);border-bottom:1px solid rgba(255,255,255,.05);padding:16px 24px;display:flex;align-items:center;box-shadow:0 4px 20px rgba(0,0,0,.3);position:relative;z-index:100;flex-shrink:0;}
.grid-logo{height:32px;margin-right:16px} #grid-header h2{font-size:20px;font-weight:600;display:flex;align-items:center;gap:12px;margin:0;letter-spacing:.5px;color:#fff}
.item-count{font-size:13px;font-weight:400;color:#da8e35;background:rgba(218,142,53,.15);padding:4px 10px;border-radius:20px}
.grid-header-actions{margin-left:auto;display:flex;gap:8px} .grid-header-btn{background:rgba(255,255,255,.05);color:rgba(255,255,255,.8);border:1px solid rgba(255,255,255,.1);padding:8px 16px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:500;transition:all .2s;outline:none;}
.grid-header-btn:hover{background:rgba(255,255,255,.1);color:#fff} #share-tab-btn{border-color:rgba(218,142,53,.3);color:rgba(218,142,53,.9)} #share-tab-btn:hover{background:rgba(218,142,53,.15);border-color:rgba(218,142,53,.6);color:#da8e35}
.game-section{padding:24px 32px} .game-title{font-size:24px;font-weight:300;color:#fff;margin-bottom:16px;padding-bottom:8px;border-bottom:2px solid rgba(255,255,255,.05);text-transform:capitalize;display:flex;align-items:center;gap:12px;}
.game-title .game-badge{font-size:12px;background:#444;color:#fff;padding:4px 8px;border-radius:4px;font-weight:600;}
#grid-viewer-container{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px;}
.mod-grid-item{background:#1f1f1f;border:1px solid rgba(255,255,255,.05);border-radius:12px;overflow:hidden;transition:transform .2s,box-shadow .2s;position:relative;display:flex;flex-direction:column;}
.mod-grid-item:hover{transform:translateY(-4px);box-shadow:0 8px 24px rgba(0,0,0,.4);border-color:rgba(218,142,53,.4);}
.mod-thumb-wrap{position:relative;width:100%;aspect-ratio:16/9;background:#111;overflow:hidden;cursor:pointer;} .mod-thumb{width:100%;height:100%;object-fit:cover;display:block;transition:transform .4s ease;}
.mod-grid-item:hover .mod-thumb{transform:scale(1.05)} .mod-action-btns{position:absolute;top:10px;right:10px;display:flex;gap:6px;opacity:0;transition:opacity .2s;z-index:4;} .mod-grid-item:hover .mod-action-btns{opacity:1}
.mod-action-btn{background:rgba(0,0,0,.7);backdrop-filter:blur(4px);color:#fff;border:1px solid rgba(255,255,255,.2);padding:6px 12px;border-radius:6px;cursor:pointer;font-size:12px;font-weight:600;transition:all .2s;}
.mod-action-btn.remove:hover{background:rgba(220,53,69,.8);border-color:#dc3545} .mod-info{padding:16px;flex:1;display:flex;flex-direction:column}
.mod-title{font-size:15px;font-weight:600;color:#fff;text-decoration:none;line-height:1.4;margin-bottom:8px;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;} .mod-title:hover{color:#da8e35} .mod-author{font-size:12px;color:rgba(255,255,255,.5);margin-top:auto;}
#share-modal{position:fixed;inset:0;background:rgba(0,0,0,.8);backdrop-filter:blur(5px);z-index:10000;display:none;align-items:center;justify-content:center;padding:20px} #share-modal.visible{display:flex}
.share-content{background:#1f1f1f;border:1px solid rgba(255,255,255,.1);padding:30px;border-radius:12px;width:100%;max-width:600px;box-shadow:0 24px 64px rgba(0,0,0,.5)}
.share-content h3{margin-bottom:16px;color:#fff;font-size:18px;font-weight:600} .share-content textarea{width:100%;background:#111;color:#da8e35;border:1px solid rgba(255,255,255,.1);padding:12px;font-family:monospace;font-size:12px;border-radius:8px;resize:vertical;min-height:100px;margin-bottom:16px;outline:none;word-break:break-all}
.share-content textarea:focus{border-color:rgba(218,142,53,.5)} .share-btns{display:flex;gap:12px} .share-btns button{flex:1;padding:12px;border-radius:8px;cursor:pointer;font-size:14px;font-weight:600;border:none;transition:all .2s;outline:none;color:#fff}
#copy-url-btn{background:#da8e35} #copy-url-btn:hover{background:#bf7a29} #close-share-btn{background:#444} #close-share-btn:hover{background:#555}
`;
(document.head || document.documentElement).appendChild(s);
const buildGrid = () => {
window.stop();
const sc = PARAMS.get('share');
let mods = sc ? share.decode(sc) || store.get() : store.get();
if (sc && !store.get().length && mods.length) store.set(mods);
document.title = "Mod Collection - Nexus Mods";
const app = document.createElement('div');
app.id = 'nx-wicked-app';
app.style.cssText = 'position:fixed;inset:0;width:100vw;height:100vh;z-index:2147483647;background:#2a2a2a;display:flex;flex-direction:column;overflow:hidden;';
if (!mods.length) {
app.style.overflowY = 'auto';
app.innerHTML = '<div style="padding:100px 20px;text-align:center;color:#fff;max-width:500px;margin:0 auto"><div style="font-size:60px;margin-bottom:20px">📦</div><h2 style="font-size:24px;font-weight:600;margin-bottom:12px">Your collection is empty</h2><p style="font-size:15px;color:rgba(255,255,255,.5);margin-bottom:30px;line-height:1.6">Go back to Nexus Mods and click "Add to Web Collection" on any mod page to build your selection.</p><button id="close-empty" style="padding:12px 28px;background:#da8e35;color:#fff;border:none;cursor:pointer;border-radius:8px;font-size:15px;font-weight:600">Close Tab</button></div>';
const ef = () => { if(document.body && !document.getElementById('nx-wicked-app')){ document.body.innerHTML=''; document.body.appendChild(app); document.body.style.overflow='hidden'; } };
ef(); setInterval(ef, 100);
return app.querySelector('#close-empty').onclick = () => window.close();
}
app.innerHTML = `
<div id="grid-header"><img src="https://next.nexusmods.com/assets/images/default/logo.svg" alt="Nexus Mods" class="grid-logo"><h2>Web Collection <span class="item-count">${mods.length} mods</span></h2>
<div class="grid-header-actions"><button class="grid-header-btn" id="share-tab-btn">Share Collection</button><button class="grid-header-btn" id="close-grid-btn">Close</button></div></div>
<div id="wrapper" style="flex:1;overflow-y:auto;position:relative;padding-bottom:50px;"></div>`;
app.querySelector('#close-grid-btn').onclick = () => window.close();
app.querySelector('#share-tab-btn').onclick = () => {
const target = document.getElementById('share-code');
const btn = document.getElementById('share-tab-btn');
if (!target) return;
const longU = share.getUrl(share.encode(store.get()));
if (typeof GM_xmlhttpRequest !== 'undefined') {
btn.textContent = 'Generating...'; btn.style.opacity = '0.5'; btn.style.pointerEvents = 'none';
GM_xmlhttpRequest({
method: "GET", url: `https://is.gd/create.php?format=simple&url=${encodeURIComponent(longU)}`,
onload: r => {
btn.textContent = 'Share Collection'; btn.style.opacity = '1'; btn.style.pointerEvents = 'auto';
target.value = (r.status === 200 && r.responseText.includes('is.gd')) ? r.responseText.trim() : longU;
document.getElementById('share-modal').classList.add('visible');
},
onerror: () => {
btn.textContent = 'Share Collection'; btn.style.opacity = '1'; btn.style.pointerEvents = 'auto';
target.value = longU; document.getElementById('share-modal').classList.add('visible');
}
});
} else {
target.value = longU;
document.getElementById('share-modal').classList.add('visible');
}
};
const m = document.createElement('div'); m.id = 'share-modal';
m.innerHTML = `<div class="share-content"><h3>Share Your Mod Collection</h3><textarea id="share-code" readonly></textarea><div class="share-btns"><button id="copy-url-btn">Copy Link</button><button id="close-share-btn">Close</button></div></div>`;
app.appendChild(m);
m.onclick = e => { if (e.target === m) m.classList.remove('visible'); };
m.querySelector('#close-share-btn').onclick = () => m.classList.remove('visible');
m.querySelector('#copy-url-btn').onclick = () => {
try { GM_setClipboard ? GM_setClipboard(navigator.clipboard.writeText(document.getElementById('share-code').value)) : navigator.clipboard.writeText(document.getElementById('share-code').value); } catch { document.getElementById('share-code').select(); document.execCommand('copy'); }
showToast('Collection link copied to clipboard!');
};
const wrap = app.querySelector('#wrapper');
const grp = mods.reduce((a, m) => { (a[m.game || 'unknown'] = a[m.game || 'unknown'] || []).push(m); return a; }, {});
Object.keys(grp).sort().forEach(gk => {
const sec = document.createElement('div'); sec.className = 'game-section';
const gTitle = document.createElement('h2'); gTitle.className = 'game-title';
gTitle.innerHTML = `${grp[gk][0].gameTitle || fmtGame(gk)} <span class="game-badge">${grp[gk].length} Mods</span>`;
const grid = document.createElement('div'); grid.id = 'grid-viewer-container';
grp[gk].forEach(mod => {
const i = document.createElement('div'); i.className = 'mod-grid-item';
i.innerHTML = `<div class="mod-thumb-wrap"><img class="mod-thumb" src="${mod.thumb||'https://www.nexusmods.com/assets/images/default/placeholder.png'}" onerror="this.src='https://www.nexusmods.com/assets/images/default/placeholder.png'"><div class="mod-action-btns"><button class="mod-action-btn remove">Remove</button></div></div><div class="mod-info"><a href="${mod.url}" target="_blank" class="mod-title">${mod.title}</a>${mod.author && mod.author !== 'Unknown' ? `<div class="mod-author">by ${mod.author}</div>`:''}</div>`;
i.querySelector('.remove').onclick = e => {
e.stopPropagation();
showConfirm('Remove this mod from web collection?', () => { store.set(store.get().filter(x=>x.url!==mod.url)); location.reload(); });
};
i.querySelector('.mod-thumb-wrap').onclick = () => window.open(mod.url, '_blank');
grid.appendChild(i);
});
sec.append(gTitle, grid); wrap.appendChild(sec);
});
const ef = () => { if (document.body && !document.getElementById('nx-wicked-app')) { document.body.innerHTML=''; document.body.style.overflow='hidden'; document.body.appendChild(app); } };
ef(); setInterval(ef, 100);
};
return document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', buildGrid) : buildGrid();
}
// --- NEXUS MODS INTEGRATION ---
const initNexus = () => {
if (document.getElementById('nx-control-panel')) return;
document.head.insertAdjacentHTML('beforeend', `<style>.nx-header-count{display:inline-block;} .nx-toggle-badge{position:absolute;top:-5px;right:-10px;background:#da8e35;color:#fff;padding:4px 8px;border-radius:12px;font-size:11px;font-weight:700;min-width:20px;text-align:center;box-shadow:0 2px 8px rgba(218,142,53,.4);cursor:pointer;z-index:1000;display:none} .nx-toggle-badge:hover{transform:scale(1.1)} #nx-btn-open:hover{background:#bf7a29!important} #nx-btn-clear:hover{background:rgba(255,255,255,0.05)!important}</style>`);
const toggle = () => {
state.vis = !state.vis;
['nx-control-panel', 'nx-control-backdrop'].forEach(id => {
const el = document.getElementById(id);
if(el){ el.style.visibility = state.vis?'visible':'hidden'; el.style.opacity = state.vis?'1':'0'; if(id==='nx-control-panel') el.style.transform = state.vis?'translateY(0)':'translateY(-10px)'; }
});
document.body.classList.toggle('nx-active', state.vis);
};
const updBadge = () => {
const c = state.sel.length;
document.querySelectorAll('.nx-header-count').forEach(el => { el.textContent = c; el.style.display = c > 0 ? 'inline-block' : 'none'; });
const b = document.querySelector('.nx-toggle-badge'); if (b) { b.textContent = c; b.style.display = c > 0 ? 'block' : 'none'; }
const pc = document.getElementById('nx-count'); if (pc) pc.textContent = c;
};
const checkHeader = () => {
if (document.getElementById('nx-header-btn') || document.querySelector('.nx-toggle-badge')) return;
const nav = document.querySelector('[data-e2eid="desktop-header"] .flex.h-full');
if (nav) {
const btn = document.createElement('div'); btn.id = 'nx-header-btn'; btn.className = 'flex h-full'; btn.setAttribute('data-headlessui-state', '');
btn.innerHTML = `<button class="flex h-full items-center justify-center gap-x-1 px-2 cursor-pointer lg:px-3 hover:bg-surface-low" type="button"><span class="typography-body-md" style="color:#da8e35;font-weight:600">Web Collection</span><span class="nx-header-count" style="background:#da8e35;color:#fff;font-size:11px;padding:2px 6px;border-radius:10px;font-weight:bold;margin-left:4px">${state.sel.length}</span></button>`;
btn.querySelector('button').onclick = e => { e.preventDefault(); e.stopPropagation(); toggle(); };
return nav.appendChild(btn);
}
const logo = document.querySelector('.nexus-logo') || document.querySelector('header a[href="/"]');
if (logo) {
const lWrap = document.createElement('div'); lWrap.style.cssText = 'position:relative;display:inline-block';
logo.parentNode.insertBefore(lWrap, logo); lWrap.appendChild(logo);
const bdg = document.createElement('div'); bdg.className = 'nx-toggle-badge'; bdg.textContent = state.sel.length;
bdg.onclick = e => { e.preventDefault(); e.stopPropagation(); toggle(); }; lWrap.appendChild(bdg);
}
};
const checkPanel = () => {
if (document.getElementById('nx-control-panel')) return;
const bd = document.createElement('div'); bd.id = 'nx-control-backdrop'; bd.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:49;backdrop-filter:blur(4px);visibility:hidden;opacity:0;transition:visibility 0.3s, opacity 0.3s ease;';
bd.onclick = toggle; document.body.appendChild(bd);
const p = document.createElement('div'); p.id = 'nx-control-panel'; p.style.cssText = 'position:fixed;left:0;right:0;top:56px;width:100%;background:var(--color-surface-low, #212121);box-shadow:0 4px 12px rgba(0,0,0,0.5);z-index:50;border-bottom:1px solid var(--color-stroke-subdued, #333);margin-top:-1px;outline:none;box-sizing:border-box;visibility:hidden;opacity:0;transform:translateY(-10px);transition:visibility 0.3s, opacity 0.3s ease, transform 0.3s ease;';
p.innerHTML = `<div style="display:flex;justify-content:center;gap:32px;padding:48px 32px 56px 32px;box-sizing:border-box;width:100%;max-width:1200px;margin:0 auto;flex-wrap:wrap;"><div style="display:flex;flex-direction:column;max-width:672px;padding:0 32px;flex:1;min-width:300px;box-sizing:border-box;"><h6 style="font-size:22px;font-weight:700;color:var(--color-neutral-strong, #e5e7eb);margin:0 0 8px 0;line-height:1.2">Web Collection Manager</h6><p style="font-size:16px;color:var(--color-neutral-subdued, #9ca3af);margin:0 0 24px 0;line-height:1.6">Manage your temporary mod selections before importing or sharing them. Visit any Mod Page and click Add to Web Collection to quickly build your selection list.</p><div style="display:flex;gap:16px;flex-wrap:wrap;"><button id="nx-btn-open" class="nxm-button" style="background:#da8e35;border:1px solid #da8e35;color:#fff;padding:12px 24px;border-radius:6px;font-weight:600;cursor:pointer;transition:all 0.2s;font-size:14px">View Web Collection</button><button id="nx-btn-clear" class="nxm-button" style="background:transparent;border:1px solid var(--color-stroke-subdued, #374151);color:var(--color-neutral-strong, #e5e7eb);padding:12px 24px;border-radius:6px;font-weight:600;cursor:pointer;transition:all 0.2s;font-size:14px">Clear Selection</button></div></div><div style="width:100%;max-width:240px;align-self:flex-start;border-radius:8px;border:1px solid var(--color-stroke-subdued, #374151);padding:16px;background:var(--color-surface-base, #18181b);box-sizing:border-box;margin-top:10px;"><h6 style="font-size:14px;font-weight:600;color:var(--color-neutral-moderate, #d1d5db);margin:0 0 6px 0;line-height:1.2">COLLECTION STATUS</h6><p style="font-size:14px;color:var(--color-neutral-subdued, #9ca3af);margin:0 0 16px 0;line-height:1.4">You have <strong id="nx-count" style="color:#da8e35;font-size:18px">${state.sel.length}</strong> mods selected.</p><p style="font-size:12px;color:var(--color-neutral-subdued, #9ca3af);margin:0;padding:10px;background:rgba(255,255,255,0.05);border-left:3px solid #da8e35;border-radius:4px;line-height:1.4">Shortcut: Press <strong style="color:var(--color-neutral-strong, #fff);">W</strong> at any time to quickly toggle this dashboard.</p></div></div>`;
document.body.appendChild(p);
p.querySelector('#nx-btn-open').onclick = () => { if (!state.sel.length) return showToast('No mods selected!', true); try { window.open(`${location.origin}/?collection_view=true&t=${Date.now()}`, '_blank'); } catch { location.href = `${location.origin}/?collection_view=true&t=${Date.now()}`; } };
p.querySelector('#nx-btn-clear').onclick = () => { state.sel=[]; store.clr(); updBadge(); pMod(); showToast('Selection cleared'); };
};
const pMod = () => {
const ul = document.querySelector('ul.modactions'); if (!ul) return;
const parsed = parseUrl(location.href); if (!parsed) return;
const isSel = getModIdx(location.href) > -1;
let li = document.getElementById('nx-modpage-action');
if (!li) {
li = document.createElement('li'); li.id = 'nx-modpage-action';
li.innerHTML = `<div class="nx-custom-modpage-btn" tabindex="0" style="cursor:pointer;display:inline-flex;align-items:center;justify-content:center;height:35px;padding:0 14px;border:1px solid #da8e35;color:#da8e35;gap:6px;border-radius:3px;font-weight:700;text-transform:uppercase;font-size:11px;transition:all .2s;"><svg viewBox="0 0 24 24" class="icon" style="width:1.2rem;height:1.2rem;"><path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" style="fill: currentColor;"></path></svg><span class="flex-label"></span></div>`;
li.querySelector('.nx-custom-modpage-btn').addEventListener('click', e => {
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
const titleEl = document.querySelector('h1') || document.querySelector('#pagetitle h1');
const imgEl = document.querySelector('meta[property="og:image"]') || document.querySelector('#image-carousel img');
const t = (document.title.split(' at ')[0] || titleEl?.textContent || '').trim() || 'Unknown Mod';
const u = imgEl?.content || imgEl?.src || '';
const authorEl = document.querySelector('#sideheader .author-name a, .author, .uploader') || Array.from(document.querySelectorAll('a[href*="/users/"], a[href*="/profile/"]')).find(a => !a.querySelector('img') && !a.textContent.includes('Log in') && !a.href.includes('my-profile'));
const a = authorEl ? authorEl.textContent.replace(/^by\s+/i, '').trim() : 'Unknown';
toggleMod(location.href, t, u, a, parsed);
updBadge(); pMod();
}, true);
const t = document.querySelector('li[id^="action-track-"]');
t ? ul.insertBefore(li, t) : ul.appendChild(li);
}
const btn = li.querySelector('.nx-custom-modpage-btn');
btn.classList.toggle('is-active', isSel);
btn.style.background = isSel ? 'rgba(218,142,53,0.15)' : 'transparent';
li.querySelector('.flex-label').textContent = isSel ? 'Remove from Web Collection' : 'Add to Web Collection';
};
const pDrop = () => {
document.querySelectorAll('.nxm-dropdown-items').forEach(menu => {
if (menu.dataset.nxPending || menu.querySelector('.nx-added-dropdown')) return;
menu.dataset.nxPending = '1';
const tBtn = document.getElementById(menu.getAttribute('aria-labelledby')||''); if (!tBtn) return;
let c = tBtn, mL = null;
for (let i = 0; i < 8 && c; i++) {
c = c.parentElement;
const aTags = Array.from(c.querySelectorAll('a[href*="/mods/"]')).filter(a => !a.href.includes('?tab=') && !a.href.includes('&') && !a.href.includes('#'));
if (aTags.length) { mL = aTags[0]; break; }
}
if (!mL) return;
const parsed = parseUrl(mL.href); if (!parsed) return;
const allLinks = Array.from(document.querySelectorAll(`a[href*="/mods/${parsed.modId}"]`));
const titleLink = allLinks.find(a => a.textContent.trim().length > 1 && !a.querySelector('img'));
const title = titleLink ? titleLink.textContent.trim() : 'Unknown Mod';
const imgLink = allLinks.find(a => a.querySelector('img'));
const imgNode = imgLink ? imgLink.querySelector('img') : null;
const thumb = imgNode?.src || 'https://www.nexusmods.com/assets/images/default/placeholder.png';
let author = 'Unknown';
if (titleLink) {
let tile = titleLink.closest('article, li, [class*="tile"], [class*="card"]');
if (!tile) {
tile = titleLink;
for (let i = 0; i < 6 && tile; i++) {
tile = tile.parentElement;
if (tile && Array.from(tile.querySelectorAll('a[href*="/mods/"]')).length >= 2) break;
}
}
if (tile) {
const aEl = Array.from(tile.querySelectorAll('a[href*="/users/"], a[href*="/profile/"]')).find(a => a.textContent.trim() && !a.querySelector('img') && !a.textContent.includes('Log in') && !a.href.includes('my-profile'));
if (aEl) author = aEl.textContent.trim().replace(/^\s*by\s+/i, '');
else {
const byTxt = Array.from(tile.querySelectorAll('span, p, div')).find(e => e.textContent.trim().toLowerCase().startsWith('by '));
if (byTxt) author = byTxt.textContent.trim().replace(/^\s*by\s+/i, '');
}
}
}
const btn = document.createElement('button'); btn.className = 'nxm-dropdown-item nx-added-dropdown'; btn.role = 'menuitem';
const updateBtnUI = () => {
const sel = getModIdx(mL.href) > -1;
btn.innerHTML = `<svg viewBox="0 0 24 24" role="presentation" class="nxm-dropdown-item-icon"><path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2Z" style="fill: ${sel?'#da8e35':'currentcolor'}"></path></svg><span class="nxm-dropdown-item-label" style="color:${sel?'#da8e35':'inherit'};font-weight:${sel?'700':'500'}">${sel?'Remove from Web Collection':'Web Collection'}</span>`;
};
btn.onclick = e => {
e.preventDefault(); e.stopPropagation(); e.stopImmediatePropagation();
toggleMod(mL.href, title, thumb, author, parsed);
updBadge(); updateBtnUI(); pMod();
};
updateBtnUI();
setTimeout(() => {
if (!menu.querySelector('.nx-added-dropdown')) menu.insertBefore(btn, menu.firstChild);
delete menu.dataset.nxPending;
}, 10);
});
};
document.addEventListener('keydown', e => {
if (e.ctrlKey || e.metaKey || ['INPUT','TEXTAREA','SELECT'].includes(document.activeElement?.tagName) || document.activeElement?.isContentEditable) return;
if (e.key.toLowerCase()==='g' && state.sel.length) { e.preventDefault(); try { window.open(`${location.origin}/?collection_view=true&t=${Date.now()}`, '_blank'); } catch { location.href = `${location.origin}/?collection_view=true&t=${Date.now()}`; } }
if (e.key.toLowerCase()==='w') { e.preventDefault(); toggle(); }
if (e.key==='Escape' && state.vis) { e.preventDefault(); toggle(); }
});
checkHeader(); checkPanel(); pMod(); updBadge();
let tmt = null;
new MutationObserver(() => {
checkHeader(); checkPanel(); pDrop();
if (tmt) return;
tmt = setTimeout(() => { tmt = null; pMod(); }, 300);
}).observe(document.body, { childList: true, subtree: true });
};
document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', initNexus) : initNexus();
})();