您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
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';