Blocks unwanted popups with whitelist/blocklist control and interactive dialogs.
当前为
// ==UserScript==
// @name BlockPup
// @namespace Violentmonkey Scripts
// @version 0.3
// @description Blocks unwanted popups with whitelist/blocklist control and interactive dialogs.
// @author 0xArCHDeViL
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_registerMenuCommand
// @run-at document-start
// @license MIT
// ==/UserScript==
(function() {
'use strict';
const createPatternManager = (storageKey, defaultPatterns = []) => ({
STORAGE_KEY: storageKey,
CACHE_DURATION: 5 * 60 * 1000,
_cache: null,
_patternToRegex(pattern) {
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
return new RegExp(`^https?:\/\/${escaped}`, 'i');
},
async _load() {
if (this._cache && this._cache.expires > Date.now()) {
return this._cache.patterns;
}
const patterns = await GM_getValue(this.STORAGE_KEY, defaultPatterns);
this._cache = {
patterns: new Set(patterns),
expires: Date.now() + this.CACHE_DURATION,
};
return this._cache.patterns;
},
async isMatch(url) {
const patterns = await this._load();
for (const pattern of patterns) {
if (this._patternToRegex(pattern).test(url)) return true;
}
return false;
},
async add(pattern) {
const patterns = await this._load();
patterns.add(pattern);
this._cache.patterns = patterns;
await GM_setValue(this.STORAGE_KEY, Array.from(patterns));
},
async remove(pattern) {
const patterns = await this._load();
patterns.delete(pattern);
this._cache.patterns = patterns;
await GM_setValue(this.STORAGE_KEY, Array.from(patterns));
},
async getAll() {
return Array.from(await this._load());
}
});
const whitelistManager = createPatternManager('popup_whitelist_patterns', ['localhost', '127.0.0.1']);
const blocklistManager = createPatternManager('popup_blocklist_patterns');
const createManagementDialog = async (manager, title) => {
document.querySelector('.popup-manager-container')?.remove();
const container = document.createElement('div');
container.className = 'popup-manager-container';
const shadow = container.attachShadow({ mode: 'open' });
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const patterns = await manager.getAll();
const style = document.createElement('style');
style.textContent = `
:host {
--background: ${isDarkMode ? 'hsl(240 10% 3.9%)' : 'hsl(0 0% 100%)'};
--foreground: ${isDarkMode ? 'hsl(0 0% 98%)' : 'hsl(240 10% 3.9%)'};
--muted-foreground: ${isDarkMode ? 'hsl(240 3.7% 62.9%)' : 'hsl(240 3.7% 45.9%)'};
--card: ${isDarkMode ? 'hsl(240 4.8% 12%)' : 'hsl(0 0% 100%)'};
--border: ${isDarkMode ? 'hsl(240 3.7% 15.9%)' : 'hsl(240 5.9% 90%)'};
--input: ${isDarkMode ? 'hsl(240 3.7% 15.9%)' : 'hsl(240 5.9% 90%)'};
--primary: ${isDarkMode ? 'hsl(0 0% 98%)' : 'hsl(240 5.9% 10%)'};
--primary-foreground: ${isDarkMode ? 'hsl(240 5.9% 10%)' : 'hsl(0 0% 98%)'};
--destructive: ${isDarkMode ? 'hsl(0 62.8% 30.6%)' : 'hsl(0 84.2% 60.2%)'};
--destructive-foreground: ${isDarkMode ? 'hsl(0 0% 98%)' : 'hsl(0 0% 98%)'};
--overlay-bg: ${isDarkMode ? 'hsl(240 10% 3.9% / 0.5)' : 'hsl(0 0% 100% / 0.5)'};
}
.overlay {
position: fixed; inset: 0; z-index: 2147483646;
background-color: var(--overlay-bg);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: flex; justify-content: center; align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.dialog {
background-color: var(--card); color: var(--foreground); border: 1px solid var(--border);
border-radius: 0.75rem; width: 600px; max-width: 90vw; max-height: 80vh;
display: flex; flex-direction: column; box-shadow: 0 10px 30px rgba(0,0,0,0.2);
animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
.header { padding: 1.5rem; font-size: 1.125rem; font-weight: 600; border-bottom: 1px solid var(--border); }
.content { padding: 1.5rem; flex-grow: 1; overflow-y: auto; max-height: 50vh; }
.pattern-item { display: flex; justify-content: space-between; align-items: center; padding: 0.75rem; border-radius: 0.5rem; gap: 1rem; }
.pattern-item:nth-child(odd) { background-color: ${isDarkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)'}; }
.pattern-text { font-family: monospace; font-size: 0.875rem; flex-grow: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.footer { padding: 1.5rem; border-top: 1px solid var(--border); display: flex; gap: 0.75rem; }
.input { flex-grow: 1; padding: 0.5rem 0.75rem; border: 1px solid var(--input); border-radius: 0.375rem; background-color: var(--background); color: var(--foreground); }
.btn { padding: 0.5rem 1rem; border-radius: 0.375rem; border: none; cursor: pointer; font-weight: 500; }
.btn-primary { background-color: var(--primary); color: var(--primary-foreground); }
.btn-icon { background: none; border: none; cursor: pointer; padding: 0.5rem; border-radius: 50%; display: flex; align-items: center; justify-content: center; transition: background-color 0.2s; }
.btn-icon:hover { background-color: ${isDarkMode ? 'rgba(255, 82, 82, 0.2)' : 'rgba(220, 53, 69, 0.1)'}; }
.btn-icon svg { stroke: var(--muted-foreground); transition: stroke 0.2s; }
.btn-icon:hover svg { stroke: var(--destructive); }
`;
const overlay = document.createElement('div');
overlay.className = 'overlay';
overlay.innerHTML = `
<div class="dialog">
<div class="header">${title}</div>
<div class="content"></div>
<div class="footer">
<input type="text" class="input" placeholder="e.g., *.google.com/*">
<button class="btn btn-primary">Add</button>
</div>
</div>
`;
const dialog = overlay.querySelector('.dialog');
const content = dialog.querySelector('.content');
const addInput = dialog.querySelector('.input');
const renderList = () => {
content.innerHTML = '';
patterns.sort();
patterns.forEach(pattern => {
const item = document.createElement('div');
item.className = 'pattern-item';
item.innerHTML = `
<span class="pattern-text" title="${pattern}">${pattern}</span>
<button class="btn-icon" title="Remove">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 6h18"></path>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
<line x1="10" y1="11" x2="10" y2="17"></line>
<line x1="14" y1="11" x2="14" y2="17"></line>
</svg>
</button>
`;
item.querySelector('.btn-icon').onclick = async () => {
const index = patterns.indexOf(pattern);
if (index > -1) patterns.splice(index, 1);
await manager.remove(pattern);
renderList();
};
content.appendChild(item);
});
};
dialog.querySelector('.btn-primary').onclick = async () => {
const newPattern = addInput.value.trim();
if (newPattern && !patterns.includes(newPattern)) {
patterns.push(newPattern);
await manager.add(newPattern);
addInput.value = '';
renderList();
}
};
overlay.onclick = (e) => { if (e.target === overlay) container.remove(); };
dialog.onclick = (e) => e.stopPropagation();
renderList();
shadow.appendChild(style);
shadow.appendChild(overlay);
document.body.appendChild(container);
};
const initializePopupInterceptor = () => {
const originalWindowOpen = window.open;
const hijackedWindowOpen = async (url, name, features) => {
const fullUrl = new URL(url, window.location.origin).href;
if (await blocklistManager.isMatch(fullUrl)) {
return null;
}
if (await whitelistManager.isMatch(fullUrl)) {
originalWindowOpen(url, name, features);
} else {
createPopupDialog({ url: fullUrl, onAllow: () => originalWindowOpen(url, name, features) });
}
return null;
};
unsafeWindow.open = (url, name, features) => {
hijackedWindowOpen(url, name, features);
return null;
};
const createPopupDialog = ({ url, onAllow }) => {
document.querySelector('.popup-blocker-container')?.remove();
const container = document.createElement('div');
container.className = 'popup-blocker-container';
const shadow = container.attachShadow({ mode: 'open' });
const isDarkMode = window.matchMedia('(prefers-color-scheme: dark)').matches;
const targetDomain = new URL(url).hostname;
const targetPath = new URL(url).pathname;
const style = document.createElement('style');
style.textContent = `
:host {
--background: ${isDarkMode ? 'hsl(240 10% 3.9%)' : 'hsl(0 0% 100%)'};
--foreground: ${isDarkMode ? 'hsl(0 0% 98%)' : 'hsl(240 10% 3.9%)'};
--muted-foreground: ${isDarkMode ? 'hsl(240 3.7% 62.9%)' : 'hsl(240 3.7% 45.9%)'};
--card: ${isDarkMode ? 'hsl(240 4.8% 12%)' : 'hsl(0 0% 100%)'};
--border: ${isDarkMode ? 'hsl(240 3.7% 15.9%)' : 'hsl(240 5.9% 90%)'};
--input: ${isDarkMode ? 'hsl(240 3.7% 15.9%)' : 'hsl(240 5.9% 90%)'};
--primary: ${isDarkMode ? 'hsl(0 0% 98%)' : 'hsl(240 5.9% 10%)'};
--primary-foreground: ${isDarkMode ? 'hsl(240 5.9% 10%)' : 'hsl(0 0% 98%)'};
--secondary: ${isDarkMode ? 'hsl(240 3.7% 15.9%)' : 'hsl(240 4.9% 95.9%)'};
--secondary-foreground: ${isDarkMode ? 'hsl(0 0% 98%)' : 'hsl(240 5.9% 10%)'};
--destructive: ${isDarkMode ? 'hsl(0 72% 51%)' : 'hsl(0 84.2% 60.2%)'};
--destructive-foreground: ${isDarkMode ? 'hsl(0 0% 98%)' : 'hsl(0 0% 98%)'};
--constructive: ${isDarkMode ? 'hsl(142.1 70.6% 45.1%)' : 'hsl(142.1 76.2% 41.2%)'};
--constructive-foreground: ${isDarkMode ? 'hsl(144.9 80.4% 10%)' : 'hsl(0 0% 98%)'};
--overlay-bg: ${isDarkMode ? 'hsl(240 10% 3.9% / 0.5)' : 'hsl(0 0% 100% / 0.5)'};
}
.overlay {
position: fixed; inset: 0; z-index: 2147483647;
background-color: var(--overlay-bg);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
display: flex; justify-content: center; align-items: center;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
.dialog {
background-color: var(--card); color: var(--foreground); border: 1px solid var(--border);
border-radius: 0.75rem; width: 400px; max-width: 90vw;
box-shadow: 0 10px 30px rgba(0,0,0,0.2); animation: fadeIn 0.2s ease-out;
}
@keyframes fadeIn { from { opacity: 0; transform: scale(0.95); } to { opacity: 1; transform: scale(1); } }
.header { padding: 1rem 1.5rem; text-align: center; }
.title { font-size: 1.125rem; font-weight: 600; }
.description { font-size: 0.875rem; color: var(--muted-foreground); margin-top: 0.25rem; }
.content { padding: 0 1.5rem 1.5rem; }
.url-display { font-size: 0.8rem; background-color: ${isDarkMode ? 'rgba(255,255,255,0.05)' : 'rgba(0,0,0,0.02)'}; padding: 0.5rem 0.75rem; border-radius: 0.375rem; word-break: break-all; max-height: 90px; overflow-y: auto; text-align: left; border: 1px solid var(--border); }
.footer { padding: 1.5rem; border-top: 1px solid var(--border); display: flex; flex-direction: column; gap: 0.75rem; }
.btn { padding: 0.6rem; border-radius: 0.375rem; border: 1px solid transparent; cursor: pointer; font-weight: 500; width: 100%; }
.btn-primary { background-color: var(--primary); color: var(--primary-foreground); border-color: var(--border); }
.btn-secondary { background-color: var(--secondary); color: var(--secondary-foreground); border-color: var(--border); }
.btn-destructive { background-color: var(--destructive); color: var(--destructive-foreground); border-color: transparent; }
.btn-constructive { background-color: var(--constructive); color: var(--constructive-foreground); border-color: transparent; }
.initial-buttons, .input-container { display: flex; flex-direction: column; gap: 0.75rem; }
.input-container { display: none; margin-top: 0.5rem; animation: slideDown 0.3s ease-out; }
@keyframes slideDown { from { opacity: 0; transform: translateY(-10px); } to { opacity: 1; translateY(0); } }
.input-header { display: flex; justify-content: flex-end; margin-bottom: 0.5rem; }
.btn-back { background: none; border: none; color: var(--muted-foreground); cursor: pointer; font-size: 0.875rem; padding: 0.25rem; }
.input { width: 100%; padding: 0.5rem 0.75rem; box-sizing: border-box; border: 1px solid var(--input); border-radius: 0.375rem; background-color: var(--background); color: var(--foreground); margin-bottom: 0.5rem; }
`;
const overlay = document.createElement('div');
overlay.className = 'overlay';
overlay.innerHTML = `
<div class="dialog">
<div class="header">
<div class="title">Popup Request</div>
<div class="description">A script is trying to open a new tab.</div>
</div>
<div class="content">
<div class="url-display">${url}</div>
</div>
<div class="footer">
<button class="btn btn-primary btn-allow-once">Allow Once</button>
<div class="initial-buttons">
<button class="btn btn-secondary btn-show-whitelist">Always Allow...</button>
<button class="btn btn-secondary btn-show-blocklist">Always Block...</button>
</div>
<div class="input-container whitelist-input-container">
<div class="input-header"><button class="btn-back">← Back</button></div>
<input type="text" class="input" value="${targetDomain}${targetPath === '/' ? '/*' : ''}">
<button class="btn btn-constructive btn-confirm-whitelist">Allow</button>
</div>
<div class="input-container blocklist-input-container">
<div class="input-header"><button class="btn-back">← Back</button></div>
<input type="text" class="input" value="${targetDomain}">
<button class="btn btn-destructive btn-confirm-blocklist">Block</button>
</div>
</div>
</div>
`;
const dialog = overlay.querySelector('.dialog');
const removeDialog = () => container.remove();
const initialButtons = dialog.querySelector('.initial-buttons');
const whitelistContainer = dialog.querySelector('.whitelist-input-container');
const blocklistContainer = dialog.querySelector('.blocklist-input-container');
const whitelistInput = whitelistContainer.querySelector('.input');
const blocklistInput = blocklistContainer.querySelector('.input');
const showInitialButtons = () => {
initialButtons.style.display = 'flex';
whitelistContainer.style.display = 'none';
blocklistContainer.style.display = 'none';
};
dialog.querySelector('.btn-allow-once').onclick = () => { onAllow(); removeDialog(); };
initialButtons.querySelector('.btn-show-whitelist').onclick = () => {
initialButtons.style.display = 'none';
whitelistContainer.style.display = 'block';
whitelistInput.focus();
whitelistInput.select();
};
initialButtons.querySelector('.btn-show-blocklist').onclick = () => {
initialButtons.style.display = 'none';
blocklistContainer.style.display = 'block';
blocklistInput.focus();
blocklistInput.select();
};
whitelistContainer.querySelector('.btn-back').onclick = showInitialButtons;
blocklistContainer.querySelector('.btn-back').onclick = showInitialButtons;
dialog.querySelector('.btn-confirm-whitelist').onclick = () => {
const pattern = whitelistInput.value.trim();
if (pattern) {
whitelistManager.add(pattern);
onAllow();
removeDialog();
}
};
dialog.querySelector('.btn-confirm-blocklist').onclick = () => {
const pattern = blocklistInput.value.trim();
if (pattern) {
blocklistManager.add(pattern);
removeDialog();
}
};
overlay.onclick = (e) => { if (e.target === overlay) removeDialog(); };
dialog.onclick = (e) => e.stopPropagation();
shadow.appendChild(style);
shadow.appendChild(overlay);
document.body.appendChild(container);
};
document.addEventListener('click', e => {
const link = e.target.closest('a[target="_blank"]');
if (link && link.href) {
e.preventDefault();
e.stopImmediatePropagation();
hijackedWindowOpen(link.href);
}
}, true);
document.addEventListener('submit', e => {
const form = e.target.closest('form[target="_blank"]');
if (form && form.action) {
e.preventDefault();
e.stopImmediatePropagation();
hijackedWindowOpen(form.action);
}
}, true);
};
initializePopupInterceptor();
GM_registerMenuCommand('Manage Whitelist', () => createManagementDialog(whitelistManager, 'Manage Whitelist Patterns'));
GM_registerMenuCommand('Manage Blocklist', () => createManagementDialog(blocklistManager, 'Manage Blocklist Patterns'));
})();