NexusMods Exclude Tags Manager V3

Foldable panel allowing mods with NSFW or Reshade tags to be excluded

// ==UserScript==
// @name         NexusMods Exclude Tags Manager V3
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  Foldable panel allowing mods with NSFW or Reshade tags to be excluded
// @author       ChatGPT
// @match        https://www.nexusmods.com/*
// @grant        none
 // @license MIT
// @run-at       document-end
// ==/UserScript==

(function() {
    // Do not run on individual mod pages
if (/\/mods\/\d+($|[?#])/.test(window.location.pathname)) {
    return; // Stop script here
}

    'use strict';

    const STORAGE_KEY = 'nexus_exclude_tags_memory';

    const excludeTags = [
        "Extreme violence",
        "Sexualised",
        "Swearing/Profanity",
        "Pornographic",
        "Suicide",
        "Self-harm",
        "Depression",
        "Body stigma",
        "Eating disorder",
        "Harmful substances",
        "ReShade"
    ];

    // Loads stored tags
    function loadMemory() {
        try {
            const val = localStorage.getItem(STORAGE_KEY);
            if (val) return JSON.parse(val);
        } catch {}
        return [];
    }

    // Checks if all given tags are in the URL
    function urlContainsAllTags(url, tags) {
        const params = url.searchParams;
        const urlTags = new Set(params.getAll('excludedTag'));
        return tags.every(t => urlTags.has(t));
    }

    // Applies the tags stored in the URL, reloads if necessary
    function applyStoredTagsInUrl() {
        const storedTags = loadMemory();
        if (storedTags.length === 0) return false;

        const url = new URL(window.location.href);
        if (!urlContainsAllTags(url, storedTags)) {
            // Remove all excludedTags before adding the correct ones
            url.searchParams.delete('excludedTag');

            storedTags.forEach(t => url.searchParams.append('excludedTag', t));

            // Reload without adding to history, avoids infinite loop
            window.location.replace(url.toString());
            return true; // reload done
        }
        return false; // no need for reloading
    }

    // ============ UI ===============

    function createUI() {
        const container = document.createElement('div');
        container.style.position = 'fixed';
        container.style.top = '61px';      
        container.style.right = '160px';  
        container.style.zIndex = '999999';
        container.style.fontFamily = 'Segoe UI, Tahoma, Geneva, Verdana, sans-serif';
        container.style.userSelect = 'none';
        container.style.color = '#eee';
        container.style.width = '260px';
        container.style.boxShadow = '0 0 12px rgba(0,0,0,0.9)';
        container.style.backgroundColor = '#1b1f2a';
        container.style.borderRadius = '8px';

        // Foldable button
        const toggleBtn = document.createElement('button');
        toggleBtn.textContent = 'Exclude Tags ▼';
        toggleBtn.style.width = '100%';
        toggleBtn.style.padding = '10px';
        toggleBtn.style.border = 'none';
        toggleBtn.style.borderRadius = '8px 8px 0 0';
        toggleBtn.style.backgroundColor = '#2e3a4e';
        toggleBtn.style.color = '#eee';
        toggleBtn.style.fontWeight = 'bold';
        toggleBtn.style.cursor = 'pointer';
        toggleBtn.style.textAlign = 'center';
        toggleBtn.style.userSelect = 'none';
        toggleBtn.style.boxShadow = 'inset 0 -3px 6px rgba(0,0,0,0.3)';
        container.appendChild(toggleBtn);

        // Panel content (buttons list + apply)
        const panel = document.createElement('div');
        panel.style.padding = '10px';
        panel.style.display = 'none'; // visible at the outset
        panel.style.maxHeight = '320px';
        panel.style.overflowY = 'auto';
        container.appendChild(panel);

        // Toggle panel
        let collapsed = true;
        toggleBtn.onclick = () => {
            collapsed = !collapsed;
            panel.style.display = collapsed ? 'none' : 'block';
            toggleBtn.textContent = collapsed ? 'Exclude Tags ▲' : 'Exclude Tags ▼';
        };

        // Creating tag buttons
        function createTagBtn(tag) {
            const btn = document.createElement('button');
            btn.textContent = tag;
            btn.style.margin = '4px 6px 4px 0';
            btn.style.padding = '6px 14px';
            btn.style.borderRadius = '6px';
            btn.style.border = 'none';
            btn.style.backgroundColor = '#2e3a4e';
            btn.style.color = '#eee';
            btn.style.cursor = 'pointer';
            btn.style.whiteSpace = 'nowrap';
            btn.style.fontSize = '14px';
            btn.dataset.tag = tag;
            btn.dataset.selected = 'false';

            btn.onclick = () => {
                if (btn.dataset.selected === 'true') {
                    btn.dataset.selected = 'false';
                    btn.style.backgroundColor = '#2e3a4e';
                } else {
                    btn.dataset.selected = 'true';
                    btn.style.backgroundColor = '#d9534f';
                }
            };
            return btn;
        }

        // Container tags buttons
        const btnContainer = document.createElement('div');
        panel.appendChild(btnContainer);

        excludeTags.forEach(tag => {
            btnContainer.appendChild(createTagBtn(tag));
        });

        // button apply
        const applyBtn = document.createElement('button');
        applyBtn.textContent = 'Apply';
        applyBtn.style.width = '100%';
        applyBtn.style.marginTop = '12px';
        applyBtn.style.padding = '10px 0';
        applyBtn.style.borderRadius = '6px';
        applyBtn.style.border = 'none';
        applyBtn.style.backgroundColor = '#28a745';
        applyBtn.style.color = '#fff';
        applyBtn.style.fontWeight = 'bold';
        applyBtn.style.cursor = 'pointer';
        applyBtn.style.userSelect = 'none';

        applyBtn.onmouseenter = () => applyBtn.style.backgroundColor = '#218838';
        applyBtn.onmouseleave = () => applyBtn.style.backgroundColor = '#28a745';

        panel.appendChild(applyBtn);

        document.body.appendChild(container);

        // Synchronize buttons with current URL
        function syncButtonsWithUrl() {
            const urlTags = new URLSearchParams(window.location.search).getAll('excludedTag');
            const urlSet = new Set(urlTags);
            excludeTags.forEach(tag => {
                const btn = [...btnContainer.children].find(b => b.dataset.tag === tag);
                if (!btn) return;
                if (urlSet.has(tag)) {
                    btn.dataset.selected = 'true';
                    btn.style.backgroundColor = '#d9534f';
                } else {
                    btn.dataset.selected = 'false';
                    btn.style.backgroundColor = '#2e3a4e';
                }
            });
        }

        syncButtonsWithUrl();

        // Action apply
        applyBtn.onclick = () => {
            const selectedTags = [];
            for (const btn of btnContainer.children) {
                if (btn.dataset.selected === 'true') {
                    selectedTags.push(btn.dataset.tag);
                }
            }
            // Save
            localStorage.setItem(STORAGE_KEY, JSON.stringify(selectedTags));

            // Modifier URL et reload si besoin
            const url = new URL(window.location.href);
            url.searchParams.delete('excludedTag');
            selectedTags.forEach(t => url.searchParams.append('excludedTag', t));

            if (url.toString() !== window.location.href) {
                window.location.href = url.toString();
            }
        };

        // Expose sync for external usage
        return {
            syncButtonsWithUrl,
        };
    }

    // Managing URL changes in SPA (pushState + popstate)
    function hookUrlChange(callback) {
        let lastUrl = location.href;
        new MutationObserver(() => {
            const url = location.href;
            if (url !== lastUrl) {
                lastUrl = url;
                callback();
            }
        }).observe(document, {subtree: true, childList: true});
    }

    // When loading, apply the stored tags (reload if necessary)
    if (applyStoredTagsInUrl()) return; // reload lancé => stop ici

    // Creation UI
    const ui = createUI();

    // Sur changement d'URL détecté, appliquer tags + resync UI
    hookUrlChange(() => {
        if (applyStoredTagsInUrl()) return; // reload if tags missing
        // Else update buttons with URL
        ui.syncButtonsWithUrl();
    });

})();