Run on default & user-defined sites using wildcard patterns (ignores protocols), with full management features.
Tento skript by nemal byť nainštalovaný priamo. Je to knižnica pre ďalšie skripty, ktorú by mali používať cez meta príkaz // @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';