NexusMods Exclude Tags Manager V3

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

اعتبارا من 29-08-2025. شاهد أحدث إصدار.

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 or Violentmonkey 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         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();
    });

})();