NexusMods Exclude Tags Manager V3

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

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

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

})();