Site Filter (Protocol-Independent)

Manage allowed sites dynamically and reference this in other scripts.

Fra og med 23.02.2025. Se den nyeste version.

Dette script bør ikke installeres direkte. Det er et bibliotek, som andre scripts kan inkludere med metadirektivet // @require https://update.greatest.deepsurf.us/scripts/526770/1541937/Site%20Filter%20%28Protocol-Independent%29.js

// ==UserScript==
// @name         Site Filter (Protocol-Independent)
// @namespace    http://tampermonkey.net/
// @version      2.1
// @description  Manage allowed sites dynamically and reference this in other scripts.
// @author       blvdmd
// @match        *://*/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM_download
// @run-at       document-start
// ==/UserScript==

(function () {
    'use strict';

    const USE_EMOJI_FOR_STATUS = true; // Configurable flag to use emoji for true/false status
    const SHOW_STATUS_ONLY_IF_TRUE = true; // Configurable flag to show status only if any value is true

    function waitForScriptStorageKey(maxWait = 1000) {
        return new Promise(resolve => {
            const startTime = Date.now();
            const interval = setInterval(() => {
                if (typeof window.SCRIPT_STORAGE_KEY !== 'undefined') {
                    clearInterval(interval);
                    resolve(window.SCRIPT_STORAGE_KEY);
                } else if (Date.now() - startTime > maxWait) {
                    clearInterval(interval);
                    console.error("🚨 SCRIPT_STORAGE_KEY is not set! Make sure your script sets it **before** @require.");
                    resolve(null);
                }
            }, 50);
        });
    }

    (async function initialize() {
        async function waitForDocumentReady() {
            if (document.readyState === "complete") return;
            return new Promise(resolve => {
                window.addEventListener("load", resolve, { once: true });
            });
        }

        const key = await waitForScriptStorageKey();
        if (!key) return;

        await waitForDocumentReady();

        const STORAGE_KEY = `additionalSites_${key}`;

        function getDefaultList() {
            return typeof window.GET_DEFAULT_LIST === "function" ? window.GET_DEFAULT_LIST() : [];
        }

        function normalizeUrl(url) {
            if (typeof url !== 'string') {
                url = String(url);
            }
            return url.replace(/^https?:\/\//, '');
        }

        let additionalSites = GM_getValue(STORAGE_KEY, []);

        let mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
            if (typeof item === 'string') {
                return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
            }
            return { ...item, pattern: normalizeUrl(item.pattern) };
        });

        GM_registerMenuCommand("➕ Add Current Site to Include List", addCurrentSiteMenu);
        GM_registerMenuCommand("📜 View Included Sites", viewIncludedSites);
        GM_registerMenuCommand("🗑️ Delete Specific Entries", deleteEntries);
        GM_registerMenuCommand("✏️ Edit an Entry", editEntry);
        GM_registerMenuCommand("🚨 Clear All Entries", clearAllEntries);
        GM_registerMenuCommand("📤 Export Site List as JSON", exportAdditionalSites);
        GM_registerMenuCommand("📥 Import Site List from JSON", importAdditionalSites);

        async function shouldRunOnThisSite() {
            const currentFullPath = normalizeUrl(`${window.location.href}`);
            return mergedSites.some(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
        }

        function wildcardToRegex(pattern) {
            return new RegExp("^" + pattern
                    .replace(/[-[\]{}()+^$|#\s]/g, '\\$&')
                    .replace(/\./g, '\\.')
                    .replace(/\?/g, '\\?')
                    .replace(/\*/g, '.*')
                + "$");
        }

        function addCurrentSiteMenu() {
            const currentHost = window.location.hostname;
            const currentPath = window.location.pathname;
            const domainParts = currentHost.split('.');
            const baseDomain = domainParts.length > 2 ? domainParts.slice(-2).join('.') : domainParts.join('.');
            const secondLevelDomain = domainParts.length > 2 ? domainParts.slice(-2, -1)[0] : domainParts[0];

            const options = [
                { name: `Preferred Domain Match (*${secondLevelDomain}.*)`, pattern: `*${secondLevelDomain}.*` },
                { name: `Base Hostname (*.${baseDomain}*)`, pattern: `*.${baseDomain}*` },
                { name: `Base Domain (*.${secondLevelDomain}.*)`, pattern: `*.${secondLevelDomain}.*` },
                { name: `Host Contains (*${secondLevelDomain}*)`, pattern: `*${secondLevelDomain}*` },
                { name: `Exact Path (${currentHost}${currentPath})`, pattern: normalizeUrl(`${window.location.href}`) },
                { name: "Custom Wildcard Pattern", pattern: normalizeUrl(`${window.location.href}`) }
            ];

            const userChoice = prompt(
                "Select an option to add the site:\n" +
                options.map((opt, index) => `${index + 1}. ${opt.name}`).join("\n") +
                "\nEnter a number or cancel."
            );

            if (!userChoice) return;
            const selectedIndex = parseInt(userChoice, 10) - 1;
            if (selectedIndex >= 0 && selectedIndex < options.length) {
                let pattern = normalizeUrl(options[selectedIndex].pattern);
                if (options[selectedIndex].name === "Custom Wildcard Pattern") {
                    pattern = normalizeUrl(prompt("Edit custom wildcard pattern:", pattern));
                    if (!pattern.trim()) return alert("Invalid pattern. Operation canceled.");
                }

                const preProcessingRequired = prompt("Is pre-processing required? (y/n)", "n").toLowerCase() === 'y';
                const postProcessingRequired = prompt("Is post-processing required? (y/n)", "n").toLowerCase() === 'y';

                const entry = { pattern, preProcessingRequired, postProcessingRequired };

                if (!additionalSites.some(item => item.pattern === pattern)) {
                    additionalSites.push(entry);
                    GM_setValue(STORAGE_KEY, additionalSites);
                    mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
                        if (typeof item === 'string') {
                            return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
                        }
                        return { ...item, pattern: normalizeUrl(item.pattern) };
                    });
                    alert(`✅ Added site with pattern: ${pattern}`);
                } else {
                    alert(`⚠️ Pattern "${pattern}" is already in the list.`);
                }
            }
        }

        function viewIncludedSites() {
            const siteList = additionalSites.map(item => {
                const status = formatStatus(item.preProcessingRequired, item.postProcessingRequired);
                return `${item.pattern}${status ? ` (${status})` : ''}`;
            }).join("\n");
            alert(`🔍 Included Sites:\n${siteList || "No sites added yet."}`);
        }

        function deleteEntries() {
            if (additionalSites.length === 0) return alert("⚠️ No user-defined entries to delete.");
            const userChoice = prompt("Select entries to delete (comma-separated numbers):\n" +
                additionalSites.map((item, index) => `${index + 1}. ${item.pattern}`).join("\n"));
            if (!userChoice) return;
            const indicesToRemove = userChoice.split(',').map(num => parseInt(num.trim(), 10) - 1);
            additionalSites = additionalSites.filter((_, index) => !indicesToRemove.includes(index));
            GM_setValue(STORAGE_KEY, additionalSites);
            mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
                if (typeof item === 'string') {
                    return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
                }
                return { ...item, pattern: normalizeUrl(item.pattern) };
            });
            alert("✅ Selected entries have been deleted.");
        }

        function editEntry() {
            if (additionalSites.length === 0) return alert("⚠️ No user-defined entries to edit.");
            const userChoice = prompt("Select an entry to edit:\n" +
                additionalSites.map((item, index) => {
                    const status = formatStatus(item.preProcessingRequired, item.postProcessingRequired);
                    return `${index + 1}. ${item.pattern}${status ? ` (${status})` : ''}`;
                }).join("\n"));
            if (!userChoice) return;
            const selectedIndex = parseInt(userChoice, 10) - 1;
            if (selectedIndex < 0 || selectedIndex >= additionalSites.length) return alert("❌ Invalid selection.");
            const entry = additionalSites[selectedIndex];
            const newPattern = normalizeUrl(prompt("Edit the pattern:", entry.pattern));
            if (!newPattern || !newPattern.trim()) return;

            const preProcessingRequired = prompt("Is pre-processing required? (y/n)", entry.preProcessingRequired ? "y" : "n").toLowerCase() === 'y';
            const postProcessingRequired = prompt("Is post-processing required? (y/n)", entry.postProcessingRequired ? "y" : "n").toLowerCase() === 'y';

            entry.pattern = newPattern.trim();
            entry.preProcessingRequired = preProcessingRequired;
            entry.postProcessingRequired = postProcessingRequired;
            GM_setValue(STORAGE_KEY, additionalSites);
            mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
                if (typeof item === 'string') {
                    return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
                }
                return { ...item, pattern: normalizeUrl(item.pattern) };
            });
            alert("✅ Entry updated.");
        }

        function clearAllEntries() {
            if (additionalSites.length === 0) return alert("⚠️ No user-defined entries to clear.");
            if (confirm(`🚨 You have ${additionalSites.length} entries. Clear all?`)) {
                additionalSites = [];
                GM_setValue(STORAGE_KEY, additionalSites);
                mergedSites = [...getDefaultList()].map(item => {
                    if (typeof item === 'string') {
                        return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
                    }
                    return { ...item, pattern: normalizeUrl(item.pattern) };
                });
                alert("✅ All user-defined entries cleared.");
            }
        }

        function exportAdditionalSites() {
            GM_download("data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(additionalSites, null, 2)), "additionalSites_backup.json");
            alert("📤 Additional sites exported as JSON.");
        }

        function importAdditionalSites() {
            const input = document.createElement("input");
            input.type = "file";
            input.accept = ".json";
            input.onchange = event => {
                const reader = new FileReader();
                reader.onload = e => {
                    additionalSites = JSON.parse(e.target.result);
                    GM_setValue(STORAGE_KEY, additionalSites);
                    mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
                        if (typeof item === 'string') {
                            return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
                        }
                        return { ...item, pattern: normalizeUrl(item.pattern) };
                    });
                    alert("📥 Sites imported successfully.");
                };
                reader.readAsText(event.target.files[0]);
            };
            input.click();
        }

        function formatStatus(preProcessingRequired, postProcessingRequired) {
            if (SHOW_STATUS_ONLY_IF_TRUE && !preProcessingRequired && !postProcessingRequired) {
                return '';
            }
            const preStatus = USE_EMOJI_FOR_STATUS ? (preProcessingRequired ? '✅' : '✖️') : (preProcessingRequired ? 'true' : 'false');
            const postStatus = USE_EMOJI_FOR_STATUS ? (postProcessingRequired ? '✅' : '✖️') : (postProcessingRequired ? 'true' : 'false');
            return `Pre: ${preStatus}, Post: ${postStatus}`;
        }

        window.shouldRunOnThisSite = shouldRunOnThisSite;
        window.isPreProcessingRequired = function() {
            const currentFullPath = normalizeUrl(`${window.location.href}`);
            const entry = mergedSites.find(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
            return entry ? entry.preProcessingRequired : false;
        };
        window.isPostProcessingRequired = function() {
            const currentFullPath = normalizeUrl(`${window.location.href}`);
            const entry = mergedSites.find(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
            return entry ? entry.postProcessingRequired : false;
        };
    })();
})();