BlockPup

Blocks unwanted popups with whitelist/blocklist control and interactive dialogs.

当前为 2025-08-23 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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'));

})();