Ovu skriptu ne treba izravno instalirati. To je biblioteka za druge skripte koje se uključuju u meta direktivu // @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';