Dynamic Include Sites Script (Protocol-Independent)

Run on default & user-defined sites using wildcard patterns (ignores protocols), with full management features.

אין להתקין סקריפט זה ישירות. זוהי ספריה עבור סקריפטים אחרים // @require https://update.greasyfork.org/scripts/526770/1670393/Dynamic%20Include%20Sites%20Script%20%28Protocol-Independent%29.js

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

(function () {
    'use strict';

     // Exit if the script is running in an iframe or embedded context
     if (window.self !== window.top) {
        return; // Stop execution for embedded pages
    }

    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

    // ✅ Wait for `SCRIPT_STORAGE_KEY` to be set
    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 });
            });
        }
        
        // ✅ Wait for the script storage key
        const key = await waitForScriptStorageKey();
        if (!key) return;

        // ✅ Ensure the document is fully loaded before setting `shouldRunOnThisSite`
        await waitForDocumentReady();

        const STORAGE_KEY = `additionalSites_${key}`;

        // ====== OPTIMIZATION: CACHES ======
        const regexCache = new Map();           // Cache compiled regexes
        const patternMatchCache = {             // Cache pattern match results
            url: null,
            entry: null
        };
        let currentUrlCache = {                 // Cache current URL
            href: null,
            normalized: null
        };

        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?:\/\//, '');
        }

        // ====== OPTIMIZATION: Cached URL getter ======
        function getCurrentFullPath() {
            const currentHref = window.top.location.href;
            if (currentUrlCache.href !== currentHref) {
                currentUrlCache.href = currentHref;
                currentUrlCache.normalized = normalizeUrl(currentHref);
            }
            return currentUrlCache.normalized;
        }

        let additionalSites = GM_getValue(STORAGE_KEY, []);
        let mergedSites = buildMergedSites();

        // ====== REFACTORED: Deduplicated mergedSites building logic ======
        function buildMergedSites() {
            return [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
                if (typeof item === 'string') {
                    return { 
                        pattern: normalizeUrl(item), 
                        preProcessingRequired: false, 
                        postProcessingRequired: false, 
                        onDemandFloatingButtonRequired: false, 
                        backgroundChangeObserverRequired: false 
                    };
                }
                return { 
                    ...item, 
                    pattern: normalizeUrl(item.pattern),
                    preProcessingRequired: item.preProcessingRequired || false,
                    postProcessingRequired: item.postProcessingRequired || false,
                    onDemandFloatingButtonRequired: item.onDemandFloatingButtonRequired || false,
                    backgroundChangeObserverRequired: item.backgroundChangeObserverRequired || false
                };
            });
        }

        function refreshMergedSites() {
            mergedSites = buildMergedSites();
            // Clear caches when patterns change
            regexCache.clear();
            patternMatchCache.url = null;
            patternMatchCache.entry = null;
            buildPatternIndex();
        }

        // ====== OPTIMIZATION: Pattern Index for faster lookups ======
        const patternIndex = {
            exact: new Map(),
            suffix: new Map(),
            prefix: new Map(),
            wildcard: []
        };

        function buildPatternIndex() {
            patternIndex.exact.clear();
            patternIndex.suffix.clear();
            patternIndex.prefix.clear();
            patternIndex.wildcard = [];

            mergedSites.forEach(item => {
                const p = item.pattern;
                if (!p.includes('*')) {
                    // Exact match
                    patternIndex.exact.set(p, item);
                } else if (p.startsWith('*') && p.indexOf('*', 1) === -1) {
                    // Suffix match: *.example.com
                    patternIndex.suffix.set(p.substring(1), item);
                } else if (p.endsWith('*') && p.indexOf('*') === p.length - 1) {
                    // Prefix match: example.com/*
                    patternIndex.prefix.set(p.substring(0, p.length - 1), item);
                } else {
                    // Complex wildcard
                    patternIndex.wildcard.push(item);
                }
            });
        }

        // Initial index build
        buildPatternIndex();

        // ====== OPTIMIZATION: Cached regex with Map ======
        function wildcardToRegex(pattern) {
            // Check cache first
            if (regexCache.has(pattern)) {
                return regexCache.get(pattern);
            }
            
            // Create and cache regex
            const regex = new RegExp("^" + pattern
                    .replace(/[-[\]{}()+^$|#\s]/g, '\\$&')
                    .replace(/\./g, '\\.')
                    .replace(/\?/g, '\\?')
                    .replace(/\*/g, '.*')
                + "$");
            
            regexCache.set(pattern, regex);
            return regex;
        }

        // ====== OPTIMIZATION: Unified pattern matching with index lookup ======
        function findMatchingEntry() {
            const currentPath = getCurrentFullPath();
            
            // Return cached result if URL hasn't changed
            if (patternMatchCache.url === currentPath && patternMatchCache.entry !== null) {
                return patternMatchCache.entry;
            }

            let matchedEntry = false;

            // 1. Try exact match first (O(1))
            if (patternIndex.exact.has(currentPath)) {
                matchedEntry = patternIndex.exact.get(currentPath);
            }
            
            // 2. Try suffix matches (O(n) where n = number of suffix patterns)
            if (!matchedEntry) {
                for (const [suffix, item] of patternIndex.suffix) {
                    if (currentPath.endsWith(suffix)) {
                        matchedEntry = item;
                        break;
                    }
                }
            }
            
            // 3. Try prefix matches (O(n) where n = number of prefix patterns)
            if (!matchedEntry) {
                for (const [prefix, item] of patternIndex.prefix) {
                    if (currentPath.startsWith(prefix)) {
                        matchedEntry = item;
                        break;
                    }
                }
            }
            
            // 4. Try wildcard patterns (O(n) where n = number of complex wildcards)
            if (!matchedEntry) {
                for (const item of patternIndex.wildcard) {
                    if (wildcardToRegex(item.pattern).test(currentPath)) {
                        matchedEntry = item;
                        break;
                    }
                }
            }

            // Cache the result
            patternMatchCache.url = currentPath;
            patternMatchCache.entry = matchedEntry;
            
            return matchedEntry;
        }

        async function shouldRunOnThisSite() {
            return !!findMatchingEntry();
        }

        // ====== UTILITY: Count wildcard matches for current site ======
        function countWildcardMatches() {
            const currentPath = getCurrentFullPath();
            let count = 0;

            // Check all pattern types
            if (patternIndex.exact.has(currentPath)) count++;
            
            for (const [suffix] of patternIndex.suffix) {
                if (currentPath.endsWith(suffix)) count++;
            }
            
            for (const [prefix] of patternIndex.prefix) {
                if (currentPath.startsWith(prefix)) count++;
            }
            
            for (const item of patternIndex.wildcard) {
                if (wildcardToRegex(item.pattern).test(currentPath)) count++;
            }

            return count;
        }

        // ====== OPTIMIZATION: Debounce helper ======
        function debounce(func, wait) {
            let timeout;
            return function executedFunction(...args) {
                const later = () => {
                    clearTimeout(timeout);
                    func(...args);
                };
                clearTimeout(timeout);
                timeout = setTimeout(later, wait);
            };
        }

        // ====== HTML UI HELPER FUNCTIONS ======
        function createModal(title, content, options = {}) {
            const modal = document.createElement('div');
            modal.style.cssText = `
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0, 0, 0, 0.7);
                display: flex;
                justify-content: center;
                align-items: center;
                z-index: 999999;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
            `;

            const dialog = document.createElement('div');
            dialog.style.cssText = `
                background: white;
                border-radius: 12px;
                box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
                max-width: ${options.maxWidth || '600px'};
                width: 90%;
                max-height: 85vh;
                display: flex;
                flex-direction: column;
                overflow: hidden;
            `;

            const header = document.createElement('div');
            header.style.cssText = `
                padding: 20px;
                border-bottom: 1px solid #e0e0e0;
                display: flex;
                justify-content: space-between;
                align-items: center;
                background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
                color: white;
            `;

            const titleEl = document.createElement('h2');
            titleEl.textContent = title;
            titleEl.style.cssText = 'margin: 0; font-size: 20px; font-weight: 600;';

            const closeBtn = document.createElement('button');
            closeBtn.innerHTML = '✕';
            closeBtn.style.cssText = `
                background: transparent;
                border: none;
                color: white;
                font-size: 24px;
                cursor: pointer;
                padding: 0;
                width: 30px;
                height: 30px;
                display: flex;
                align-items: center;
                justify-content: center;
                border-radius: 50%;
                transition: background 0.2s;
            `;

            // ====== OPTIMIZATION: Event cleanup function ======
            const closeModal = () => {
                // Clean up event listeners
                closeBtn.onmouseover = null;
                closeBtn.onmouseout = null;
                closeBtn.onclick = null;
                modal.onclick = null;
                
                document.body.removeChild(modal);
                if (options.onClose) options.onClose();
            };

            closeBtn.onmouseover = () => closeBtn.style.background = 'rgba(255, 255, 255, 0.2)';
            closeBtn.onmouseout = () => closeBtn.style.background = 'transparent';
            closeBtn.onclick = closeModal;

            header.appendChild(titleEl);
            header.appendChild(closeBtn);

            const body = document.createElement('div');
            body.style.cssText = `
                padding: 20px;
                overflow-y: auto;
                flex: 1;
            `;
            if (typeof content === 'string') {
                body.innerHTML = content;
            } else {
                body.appendChild(content);
            }

            dialog.appendChild(header);
            dialog.appendChild(body);
            modal.appendChild(dialog);

            // Close on backdrop click
            modal.onclick = (e) => {
                if (e.target === modal) {
                    closeModal();
                }
            };

            document.body.appendChild(modal);
            return { modal, body, closeBtn, cleanup: closeModal };
        }

        function createButton(text, onClick, style = 'primary') {
            const btn = document.createElement('button');
            btn.textContent = text;
            const baseStyle = `
                padding: 10px 20px;
                border: none;
                border-radius: 6px;
                cursor: pointer;
                font-size: 14px;
                font-weight: 500;
                transition: all 0.2s;
                margin: 5px;
            `;
            
            const styles = {
                primary: `${baseStyle} background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;`,
                success: `${baseStyle} background: #10b981; color: white;`,
                danger: `${baseStyle} background: #ef4444; color: white;`,
                secondary: `${baseStyle} background: #6b7280; color: white;`,
                outline: `${baseStyle} background: transparent; color: #667eea; border: 2px solid #667eea;`
            };

            btn.style.cssText = styles[style] || styles.primary;
            btn.onmouseover = () => btn.style.transform = 'scale(1.05)';
            btn.onmouseout = () => btn.style.transform = 'scale(1)';
            btn.onclick = onClick;
            return btn;
        }

        function createInput(type, value, placeholder = '') {
            const input = document.createElement(type === 'textarea' ? 'textarea' : 'input');
            if (type !== 'textarea') input.type = type;
            input.value = value || '';
            input.placeholder = placeholder;
            input.style.cssText = `
                width: 100%;
                padding: 10px;
                border: 2px solid #e0e0e0;
                border-radius: 6px;
                font-size: 14px;
                font-family: inherit;
                margin: 5px 0;
                box-sizing: border-box;
                transition: border-color 0.2s;
            `;
            input.onfocus = () => input.style.borderColor = '#667eea';
            input.onblur = () => input.style.borderColor = '#e0e0e0';
            return input;
        }

        function createCheckbox(label, checked = false) {
            const container = document.createElement('label');
            container.style.cssText = `
                display: flex;
                align-items: center;
                margin: 10px 0;
                cursor: pointer;
                user-select: none;
            `;

            const checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.checked = checked;
            checkbox.style.cssText = `
                width: 20px;
                height: 20px;
                margin-right: 10px;
                cursor: pointer;
            `;

            const labelText = document.createElement('span');
            labelText.textContent = label;
            labelText.style.cssText = 'font-size: 14px;';

            container.appendChild(checkbox);
            container.appendChild(labelText);
            return { container, checkbox };
        }

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

        // ====== MENU COMMAND 1: Add Current Site ======
        function addCurrentSiteMenu() {
            const matchCount = countWildcardMatches();
            const currentHost = window.top.location.hostname;
            const currentPath = window.top.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];
        
            // Reordered: Custom Wildcard Pattern first, then others
            const options = [
                { name: "Custom Wildcard Pattern", pattern: normalizeUrl(`${window.top.location.href}`) },
                { 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.top.location.href}`) }
            ];

            const content = document.createElement('div');
            
            const infoBox = document.createElement('div');
            infoBox.style.cssText = `
                background: #f0f9ff;
                border: 2px solid #0ea5e9;
                border-radius: 8px;
                padding: 15px;
                margin-bottom: 20px;
            `;
            infoBox.innerHTML = `
                <div style="font-weight: 600; color: #0c4a6e; margin-bottom: 5px;">
                    📊 Current Page Wildcard Matches: ${matchCount}
                </div>
                <div style="font-size: 13px; color: #075985;">
                    This page matches ${matchCount} existing rule${matchCount !== 1 ? 's' : ''} in your include list.
                </div>
            `;
            content.appendChild(infoBox);

            const sectionTitle = document.createElement('h3');
            sectionTitle.textContent = 'Pattern to add:';
            sectionTitle.style.cssText = 'margin: 20px 0 10px 0; font-size: 16px; color: #333;';
            content.appendChild(sectionTitle);

            let selectedOption = 0; // Default to first option (Custom Wildcard Pattern)
            const optionButtons = [];

            // Custom Wildcard Pattern input (always visible, selected by default)
            const patternInput = createInput('text', normalizeUrl(`${window.top.location.href}`), 'Enter custom wildcard pattern');
            patternInput.style.marginBottom = '12px';
            content.appendChild(patternInput);

            // Container for other pattern options (initially hidden)
            const otherPatternsContainer = document.createElement('div');
            otherPatternsContainer.style.cssText = 'display: none; margin-top: 10px;';

            // Create buttons for other patterns (indices 1-5)
            for (let index = 1; index < options.length; index++) {
                const opt = options[index];
                const optBtn = document.createElement('button');
                optBtn.textContent = opt.name;
                optBtn.style.cssText = `
                    display: block;
                    width: 100%;
                    padding: 12px;
                    margin: 8px 0;
                    border: 2px solid #e0e0e0;
                    border-radius: 8px;
                    background: white;
                    cursor: pointer;
                    text-align: left;
                    font-size: 14px;
                    transition: all 0.2s;
                `;
                optBtn.onclick = () => {
                    selectedOption = index;
                    patternInput.value = normalizeUrl(opt.pattern);
                    // Hide other patterns after selection
                    otherPatternsContainer.style.display = 'none';
                    toggleBtn.textContent = '▼ Show Other Patterns';
                };
                optionButtons.push(optBtn);
                otherPatternsContainer.appendChild(optBtn);
            }

            // Toggle button for other patterns
            const toggleBtn = document.createElement('button');
            toggleBtn.textContent = '▼ Show Other Patterns';
            toggleBtn.style.cssText = `
                width: 100%;
                padding: 10px;
                margin: 8px 0 12px 0;
                border: 2px solid #667eea;
                border-radius: 6px;
                background: white;
                color: #667eea;
                cursor: pointer;
                font-size: 14px;
                font-weight: 500;
                transition: all 0.2s;
            `;
            toggleBtn.onmouseover = () => toggleBtn.style.background = '#f5f3ff';
            toggleBtn.onmouseout = () => toggleBtn.style.background = 'white';
            toggleBtn.onclick = () => {
                if (otherPatternsContainer.style.display === 'none') {
                    otherPatternsContainer.style.display = 'block';
                    toggleBtn.textContent = '▲ Hide Other Patterns';
                } else {
                    otherPatternsContainer.style.display = 'none';
                    toggleBtn.textContent = '▼ Show Other Patterns';
                }
            };
            content.appendChild(toggleBtn);
            content.appendChild(otherPatternsContainer);

            const configTitle = document.createElement('h3');
            configTitle.textContent = 'Configuration Options:';
            configTitle.style.cssText = 'margin: 25px 0 15px 0; font-size: 16px; color: #333;';
            content.appendChild(configTitle);

            const preCheck = createCheckbox('Pre-processing Required', false);
            const postCheck = createCheckbox('Post-processing Required', false);
            const floatingBtnCheck = createCheckbox('On-demand Floating Button Required', false);
            const backgroundObsCheck = createCheckbox('Background Change Observer Required', false);

            content.appendChild(preCheck.container);
            content.appendChild(postCheck.container);
            content.appendChild(floatingBtnCheck.container);
            content.appendChild(backgroundObsCheck.container);

            const buttonContainer = document.createElement('div');
            buttonContainer.style.cssText = 'margin-top: 25px; display: flex; justify-content: flex-end; gap: 10px;';

            const { modal, cleanup } = createModal('➕ Add Current Site to Include List', content, { maxWidth: '650px' });

            const cancelBtn = createButton('Cancel', () => {
                cleanup();
            }, 'secondary');

            const addBtn = createButton('Add Site', () => {
                // Get pattern from input (now always from patternInput since it's the primary control)
                const pattern = normalizeUrl(patternInput.value.trim());
                if (!pattern) {
                    alert('⚠️ Invalid pattern. Operation canceled.');
                    return;
                }

                const entry = { 
                    pattern, 
                    preProcessingRequired: preCheck.checkbox.checked,
                    postProcessingRequired: postCheck.checkbox.checked,
                    onDemandFloatingButtonRequired: floatingBtnCheck.checkbox.checked,
                    backgroundChangeObserverRequired: backgroundObsCheck.checkbox.checked
                };
                
                if (!additionalSites.some(item => item.pattern === pattern)) {
                    additionalSites.push(entry);
                    GM_setValue(STORAGE_KEY, additionalSites);
                    refreshMergedSites();
                    cleanup();
                    alert(`✅ Added site with pattern: ${pattern}`);
                } else {
                    alert(`⚠️ Pattern "${pattern}" is already in the list.`);
                }
            }, 'success');

            buttonContainer.appendChild(cancelBtn);
            buttonContainer.appendChild(addBtn);
            content.appendChild(buttonContainer);
        }

        // ====== MENU COMMAND 2: Advanced Management ======
        function showAdvancedManagement() {
            const content = document.createElement('div');

            // Get current site for matching
            const currentFullPath = getCurrentFullPath();

            // Compact stats header
            const stats = document.createElement('div');
            stats.style.cssText = `
                background: #667eea;
                color: white;
                padding: 10px 15px;
                border-radius: 6px;
                margin-bottom: 12px;
                display: flex;
                justify-content: space-between;
                align-items: center;
            `;
            stats.innerHTML = `
                <div><span style="font-size: 20px; font-weight: 700;">${additionalSites.length}</span> <span style="font-size: 13px;">Sites</span></div>
                <div style="font-size: 12px; opacity: 0.9;">${countWildcardMatches()} matching current page</div>
            `;
            content.appendChild(stats);

            // Compact button bar
            const buttonBar = document.createElement('div');
            buttonBar.style.cssText = `
                display: flex;
                gap: 8px;
                margin-bottom: 12px;
                flex-wrap: wrap;
            `;

            const refreshList = () => {
                document.body.querySelectorAll('[data-modal-advanced]').forEach(m => {
                    // Clean up before removing
                    m.onclick = null;
                    document.body.removeChild(m);
                });
                showAdvancedManagement();
            };

            const compactBtn = (text, onClick, style) => {
                const btn = createButton(text, onClick, style);
                btn.style.padding = '6px 12px';
                btn.style.fontSize = '13px';
                btn.style.margin = '0';
                return btn;
            };

            buttonBar.appendChild(compactBtn('📤 Export', exportAdditionalSites, 'primary'));
            buttonBar.appendChild(compactBtn('📥 Import', () => {
                importAdditionalSites(refreshList);
            }, 'primary'));
            buttonBar.appendChild(compactBtn('🗑️ Clear All', () => {
                clearAllEntriesConfirm(refreshList);
            }, 'danger'));

            content.appendChild(buttonBar);

            if (additionalSites.length === 0) {
                const emptyMsg = document.createElement('div');
                emptyMsg.style.cssText = `
                    text-align: center;
                    padding: 30px;
                    color: #9ca3af;
                    font-size: 14px;
                `;
                emptyMsg.textContent = 'No user-defined sites added yet.';
                content.appendChild(emptyMsg);
            } else {
                // Search/Filter input
                const searchContainer = document.createElement('div');
                searchContainer.style.cssText = 'margin-bottom: 12px;';
                
                const searchInput = createInput('text', '', '🔍 Search patterns...');
                searchInput.style.margin = '0';
                searchInput.style.padding = '8px 12px';
                searchInput.style.fontSize = '13px';
                searchInput.readOnly = false;
                searchInput.disabled = false;
                searchInput.autocomplete = 'off';
                searchContainer.appendChild(searchInput);
                content.appendChild(searchContainer);

                // ====== OPTIMIZATION: Memory-efficient sorting with minimal augmentation ======
                const sortedSites = additionalSites.map((item, index) => {
                    // Test match using optimized index lookup
                    let isMatch = false;
                    const p = item.pattern;
                    
                    if (p === currentFullPath) {
                        isMatch = true;
                    } else if (p.startsWith('*') && p.indexOf('*', 1) === -1) {
                        isMatch = currentFullPath.endsWith(p.substring(1));
                    } else if (p.endsWith('*') && p.indexOf('*') === p.length - 1) {
                        isMatch = currentFullPath.startsWith(p.substring(0, p.length - 1));
                    } else if (p.includes('*')) {
                        isMatch = wildcardToRegex(p).test(currentFullPath);
                    }
                    
                    return {
                        item,
                        originalIndex: index,
                        isMatch
                    };
                }).sort((a, b) => {
                    // First by match status (matching first)
                    if (a.isMatch !== b.isMatch) return b.isMatch ? 1 : -1;
                    // Then alphabetically by pattern
                    return a.item.pattern.localeCompare(b.item.pattern);
                });

                // ====== OPTIMIZATION: Virtual scrolling for large lists ======
                const listContainer = document.createElement('div');
                listContainer.style.cssText = `
                    max-height: 450px;
                    overflow-y: auto;
                    border: 1px solid #e0e0e0;
                    border-radius: 6px;
                `;

                // Track rendered items for virtual scrolling
                let allCards = [];

                // Function to render a site entry
                function renderSiteEntry(siteData) {
                    const item = siteData.item;
                    const siteCard = document.createElement('div');
                    siteCard.style.cssText = `
                        padding: 8px 12px;
                        border-bottom: 1px solid #f0f0f0;
                        background: ${siteData.isMatch ? '#f0f9ff' : 'white'};
                    `;
                    siteCard.setAttribute('data-pattern', item.pattern.toLowerCase());

                    const topRow = document.createElement('div');
                    topRow.style.cssText = 'display: flex; justify-content: space-between; align-items: center; margin-bottom: 4px;';

                    const patternDiv = document.createElement('div');
                    patternDiv.style.cssText = `
                        font-weight: 600;
                        color: #1f2937;
                        word-break: break-all;
                        font-size: 13px;
                        flex: 1;
                    `;
                    patternDiv.textContent = (siteData.isMatch ? '✓ ' : '') + item.pattern;

                    const actionBar = document.createElement('div');
                    actionBar.style.cssText = 'display: flex; gap: 6px; flex-shrink: 0; margin-left: 10px;';

                    const editBtn = document.createElement('button');
                    editBtn.textContent = '✏️';
                    editBtn.title = 'Edit';
                    editBtn.style.cssText = `
                        padding: 4px 8px;
                        border: 1px solid #d1d5db;
                        background: white;
                        border-radius: 4px;
                        cursor: pointer;
                        font-size: 12px;
                    `;
                    editBtn.onclick = () => editEntryDialog(siteData.originalIndex, refreshList);

                    const deleteBtn = document.createElement('button');
                    deleteBtn.textContent = '🗑️';
                    deleteBtn.title = 'Delete';
                    deleteBtn.style.cssText = `
                        padding: 4px 8px;
                        border: 1px solid #fecaca;
                        background: #fee2e2;
                        color: #991b1b;
                        border-radius: 4px;
                        cursor: pointer;
                        font-size: 12px;
                    `;
                    deleteBtn.onclick = () => deleteEntryConfirm(siteData.originalIndex, refreshList);

                    actionBar.appendChild(editBtn);
                    actionBar.appendChild(deleteBtn);

                    topRow.appendChild(patternDiv);
                    topRow.appendChild(actionBar);

                    const statusDiv = document.createElement('div');
                    statusDiv.style.cssText = 'font-size: 11px; color: #6b7280;';
                    const status = formatStatus(item.preProcessingRequired, item.postProcessingRequired, item.onDemandFloatingButtonRequired, item.backgroundChangeObserverRequired);
                    statusDiv.textContent = status || 'Default settings';

                    siteCard.appendChild(topRow);
                    if (status) siteCard.appendChild(statusDiv);

                    return siteCard;
                }

                // Render all sorted sites
                sortedSites.forEach(siteData => {
                    const card = renderSiteEntry(siteData);
                    allCards.push(card);
                    listContainer.appendChild(card);
                });

                content.appendChild(listContainer);

                // ====== OPTIMIZATION: Debounced search filter ======
                const debouncedFilter = debounce((searchTerm) => {
                    const lowerSearchTerm = searchTerm.toLowerCase();
                    let visibleCount = 0;
                    
                    allCards.forEach(card => {
                        const pattern = card.getAttribute('data-pattern');
                        if (pattern.includes(lowerSearchTerm)) {
                            card.style.display = '';
                            visibleCount++;
                        } else {
                            card.style.display = 'none';
                        }
                    });

                    // Show "no results" message if needed
                    let noResultsMsg = listContainer.querySelector('[data-no-results]');
                    if (visibleCount === 0 && searchTerm) {
                        if (!noResultsMsg) {
                            noResultsMsg = document.createElement('div');
                            noResultsMsg.setAttribute('data-no-results', 'true');
                            noResultsMsg.style.cssText = 'padding: 20px; text-align: center; color: #9ca3af; font-size: 13px;';
                            noResultsMsg.textContent = 'No patterns match your search';
                            listContainer.appendChild(noResultsMsg);
                        }
                    } else if (noResultsMsg) {
                        noResultsMsg.remove();
                    }
                }, 150); // 150ms debounce

                searchInput.oninput = () => {
                    debouncedFilter(searchInput.value);
                };
            }

            const { modal } = createModal('⚙️ IncludeSites-Advanced', content, { maxWidth: '700px' });
            modal.setAttribute('data-modal-advanced', 'true');
        }

        function editEntryDialog(index, onComplete) {
            const entry = additionalSites[index];
            const content = document.createElement('div');

            const label1 = document.createElement('label');
            label1.textContent = 'Pattern:';
            label1.style.cssText = 'display: block; margin-top: 15px; margin-bottom: 5px; font-weight: 600; color: #374151;';
            content.appendChild(label1);

            const patternInput = createInput('text', entry.pattern, 'Enter pattern');
            content.appendChild(patternInput);

            const configTitle = document.createElement('h3');
            configTitle.textContent = 'Configuration Options:';
            configTitle.style.cssText = 'margin: 25px 0 15px 0; font-size: 16px; color: #333;';
            content.appendChild(configTitle);

            const preCheck = createCheckbox('Pre-processing Required', entry.preProcessingRequired);
            const postCheck = createCheckbox('Post-processing Required', entry.postProcessingRequired);
            const floatingBtnCheck = createCheckbox('On-demand Floating Button Required', entry.onDemandFloatingButtonRequired);
            const backgroundObsCheck = createCheckbox('Background Change Observer Required', entry.backgroundChangeObserverRequired);

            content.appendChild(preCheck.container);
            content.appendChild(postCheck.container);
            content.appendChild(floatingBtnCheck.container);
            content.appendChild(backgroundObsCheck.container);

            const buttonContainer = document.createElement('div');
            buttonContainer.style.cssText = 'margin-top: 25px; display: flex; justify-content: flex-end; gap: 10px;';

            const { modal, cleanup } = createModal('✏️ Edit Entry', content, { maxWidth: '600px' });

            const cancelBtn = createButton('Cancel', () => {
                cleanup();
            }, 'secondary');

            const saveBtn = createButton('Save Changes', () => {
                const newPattern = normalizeUrl(patternInput.value.trim());
                if (!newPattern) {
                    alert('⚠️ Invalid pattern. Operation canceled.');
                    return;
                }

                entry.pattern = newPattern;
                entry.preProcessingRequired = preCheck.checkbox.checked;
                entry.postProcessingRequired = postCheck.checkbox.checked;
                entry.onDemandFloatingButtonRequired = floatingBtnCheck.checkbox.checked;
                entry.backgroundChangeObserverRequired = backgroundObsCheck.checkbox.checked;

                GM_setValue(STORAGE_KEY, additionalSites);
                refreshMergedSites();
                cleanup();
                alert('✅ Entry updated successfully.');
                if (onComplete) onComplete();
            }, 'success');

            buttonContainer.appendChild(cancelBtn);
            buttonContainer.appendChild(saveBtn);
            content.appendChild(buttonContainer);
        }

        function deleteEntryConfirm(index, onComplete) {
            const entry = additionalSites[index];
            if (confirm(`🗑️ Are you sure you want to delete this entry?\n\nPattern: ${entry.pattern}`)) {
                additionalSites.splice(index, 1);
                GM_setValue(STORAGE_KEY, additionalSites);
                refreshMergedSites();
                alert('✅ Entry deleted successfully.');
                if (onComplete) onComplete();
            }
        }

        function clearAllEntriesConfirm(onComplete) {
            if (additionalSites.length === 0) {
                alert('⚠️ No user-defined entries to clear.');
                return;
            }
            if (confirm(`🚨 You have ${additionalSites.length} entries. Clear all?`)) {
                additionalSites = [];
                GM_setValue(STORAGE_KEY, additionalSites);
                refreshMergedSites();
                alert('✅ All user-defined entries cleared.');
                if (onComplete) onComplete();
            }
        }

        function exportAdditionalSites() {
            const data = JSON.stringify(additionalSites, null, 2);
            const blob = new Blob([data], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'additionalSites_backup.json';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
            URL.revokeObjectURL(url);
            alert('📤 Additional sites exported as JSON.');
        }

        function importAdditionalSites(onComplete) {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.json';
            input.style.display = 'none';
            input.onchange = event => {
                const reader = new FileReader();
                reader.onload = e => {
                    try {
                        const importedData = JSON.parse(e.target.result);
                        if (Array.isArray(importedData)) {
                            additionalSites = importedData.map(item => {
                                if (typeof item === 'string') {
                                    return {
                                        pattern: normalizeUrl(item),
                                        preProcessingRequired: false,
                                        postProcessingRequired: false,
                                        onDemandFloatingButtonRequired: false,
                                        backgroundChangeObserverRequired: false
                                    };
                                } else if (typeof item === 'object' && item.pattern) {
                                    return { 
                                        pattern: normalizeUrl(item.pattern),
                                        preProcessingRequired: item.preProcessingRequired || false,
                                        postProcessingRequired: item.postProcessingRequired || false,
                                        onDemandFloatingButtonRequired: item.onDemandFloatingButtonRequired || false,
                                        backgroundChangeObserverRequired: item.backgroundChangeObserverRequired || false
                                    };
                                }
                                throw new Error('Invalid data format');
                            });
                            GM_setValue(STORAGE_KEY, additionalSites);
                            refreshMergedSites();
                            alert('📥 Sites imported successfully.');
                            if (onComplete) onComplete();
                        } else {
                            throw new Error('Invalid data format');
                        }
                    } catch (error) {
                        alert('❌ Failed to import sites: ' + error.message);
                    }
                };
                reader.readAsText(event.target.files[0]);
            };
            document.body.appendChild(input);
            input.click();
            document.body.removeChild(input);
        }

        // ====== REGISTER MENU COMMANDS (Simplified to 2) ======
        GM_registerMenuCommand(`➕ Add Current Site to Include List (Included via ${countWildcardMatches()} wildcard matches)`, addCurrentSiteMenu);
        GM_registerMenuCommand("⚙️ IncludeSites-Advanced (View/Edit/Delete/Import/Export)", showAdvancedManagement);

        // ====== EXPOSE PUBLIC API (OPTIMIZED with unified pattern matching) ======
        window.shouldRunOnThisSite = shouldRunOnThisSite;
        
        window.isPreProcessingRequired = function() {
            const entry = findMatchingEntry();
            return entry ? entry.preProcessingRequired : false;
        };
        
        window.isPostProcessingRequired = function() {
            const entry = findMatchingEntry();
            return entry ? entry.postProcessingRequired : false;
        };
        
        window.isOnDemandFloatingButtonRequired = function() {
            const entry = findMatchingEntry();
            return entry ? entry.onDemandFloatingButtonRequired : false;
        };

        window.isBackgroundChangeObserverRequired = function() {
            const entry = findMatchingEntry();
            return entry ? entry.backgroundChangeObserverRequired : false;
        };
    })();
})();

//To use this in another script use @require

// // @run-at       document-end
// // ==/UserScript==

// window.SCRIPT_STORAGE_KEY = "magnetLinkHashChecker"; // UNIQUE STORAGE KEY


// window.GET_DEFAULT_LIST = function() {
//      return [
//         { pattern: "*1337x.*", preProcessingRequired: false, postProcessingRequired: false, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
//         { pattern: "*yts.*", preProcessingRequired: true, postProcessingRequired: true, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
//         { pattern: "*torrentgalaxy.*", preProcessingRequired: false, postProcessingRequired: true, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
//         { pattern: "*bitsearch.*", preProcessingRequired: false, postProcessingRequired: false, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
//         { pattern: "*thepiratebay.*", preProcessingRequired: false, postProcessingRequired: false, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
//         { pattern: "*ext.*", preProcessingRequired: false, postProcessingRequired: false, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false }
//     ];
// };


// (async function () {
//     'use strict';

//     // ✅ Wait until `shouldRunOnThisSite` is available
//     while (typeof shouldRunOnThisSite === 'undefined') {
//         await new Promise(resolve => setTimeout(resolve, 50));
//     }

//     if (!(await shouldRunOnThisSite())) return;
//     //alert("running");

//     console.log("Pre-Customization enabled for this site: " + isPreProcessingRequired() );
//     console.log("Post-Customization enabled for this site: " + isPostProcessingRequired() );

//     const OFFCLOUD_CACHE_API_URL = 'https://offcloud.com/api/cache';