Greasy Fork

网页元素屏蔽器

屏蔽任意网站上的元素,支持缩略图记录和正则/简单模式屏蔽

目前为 2025-03-17 提交的版本。查看 最新版本

// ==UserScript==
// @name         网页元素屏蔽器
// @namespace    http://tampermonkey.net/
// @version      0.2
// @description  屏蔽任意网站上的元素,支持缩略图记录和正则/简单模式屏蔽
// @author       JerryChiang
// @match        *://*/*
// @grant        GM_registerMenuCommand
// @grant        GM_setValue
// @grant        GM_getValue
// @require      https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    // 添加样式
    const style = document.createElement('style');
    style.textContent = `
        .highlight {
            outline: 2px solid red !important;
            background-color: rgba(255, 0, 0, 0.1) !important;
        }
        .blocker-popup {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border: 1px solid #ccc;
            z-index: 9999;
            box-shadow: 0 0 10px rgba(0,0,0,0.3);
            border-radius: 8px;
            font-family: Arial, sans-serif;
            width: 600px;
            max-height: 80vh;
            overflow-y: auto;
        }
        .blocker-popup p {
            margin: 0 0 10px;
            font-size: 16px;
            color: #333;
        }
        .blocker-popup button {
            margin: 5px;
            padding: 8px 16px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
            transition: background-color 0.2s;
        }
        .blocker-popup button:hover {
            opacity: 0.9;
        }
        #regex-mode, #simple-mode {
            background-color: #007bff;
            color: white;
        }
        #preview-rule {
            background-color: #28a745;
            color: white;
        }
        #add-rule-row {
            background-color: #17a2b8;
            color: white;
        }
        #save-rules {
            background-color: #17a2b8;
            color: white;
        }
        #cancel-rule {
            background-color: #dc3545;
            color: white;
        }
        .blocker-popup input, .blocker-popup select {
            margin: 5px;
            padding: 8px;
            border: 1px solid #ccc;
            border-radius: 4px;
            font-size: 14px;
        }
        .blocker-popup select {
            width: 100px;
        }
        .blocker-popup input[type="text"], .blocker-popup input[type="number"] {
            width: 200px;
        }
        .rule-row {
            display: flex;
            align-items: center;
            margin: 5px 0;
            padding: 5px;
            border-bottom: 1px solid #eee;
        }
        .rule-row button {
            background-color: #dc3545;
            color: white;
        }
        .blocker-list {
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: white;
            padding: 20px;
            border: 1px solid #ccc;
            z-index: 9999;
            max-height: 80vh;
            overflow-y: auto;
            width: 500px;
            border-radius: 8px;
            box-shadow: 0 0 10px rgba(0,0,0,0.3);
        }
        .blocker-list h3 {
            margin: 0 0 15px;
            font-size: 18px;
            color: #333;
        }
        .blocker-list ul {
            list-style: none;
            padding: 0;
        }
        .blocker-list li {
            margin: 10px 0;
            display: flex;
            align-items: center;
            width: 100%;
            padding: 5px;
            border-bottom: 1px solid #eee;
        }
        .blocker-list img {
            max-width: 400px;
            max-height: 100px;
            object-fit: contain;
            border: 1px solid #ddd;
            flex-shrink: 0;
        }
        .blocker-list button {
            margin-left: auto;
            flex-shrink: 0;
            padding: 5px 10px;
            background-color: #dc3545;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
        }
        .blocker-list button:hover {
            background-color: #c82333;
        }
    `;
    document.head.appendChild(style);

    // 注册菜单
    GM_registerMenuCommand('屏蔽设置', startBlockingMode);
    GM_registerMenuCommand('按规则屏蔽', showRegexBlockInput);
    GM_registerMenuCommand('查看屏蔽记录', showBlockList);

    // 进入元素选择模式
    function startBlockingMode() {
        // 移除 alert,直接进入高亮选择
        document.body.addEventListener('mouseover', highlightElement);
        document.body.addEventListener('click', selectElement, true);
    }

    // 高亮悬停元素
    function highlightElement(event) {
        if (window.lastHighlighted) {
            window.lastHighlighted.classList.remove('highlight');
        }
        event.target.classList.add('highlight');
        window.lastHighlighted = event.target;
    }

    // 选择元素并弹出确认窗口
    function selectElement(event) {
        event.preventDefault();
        event.stopPropagation();

        document.body.removeEventListener('mouseover', highlightElement);
        document.body.removeEventListener('click', selectElement, true);

        const selectedElement = event.target;
        window.lastHighlighted.classList.remove('highlight');
        showConfirmation(selectedElement);
    }

    // 显示确认弹窗
    function showConfirmation(element) {
        const popup = document.createElement('div');
        popup.className = 'blocker-popup';
        popup.innerHTML = `
            <p>是否屏蔽此元素?</p>
            <button id="confirm">确认屏蔽</button>
            <button id="preview">预览</button>
            <button id="cancel">取消</button>
        `;
        document.body.appendChild(popup);

        let isPreviewHidden = false;

        const confirmBtn = document.getElementById('confirm');
        confirmBtn.addEventListener('click', async () => {
            confirmBtn.disabled = true;
            try {
                await saveBlockWithThumbnail(element);
                element.style.display = 'none';
                document.body.removeChild(popup);
            } catch (e) {
                console.error('屏蔽失败:', e);
                confirmBtn.disabled = false;
            }
        }, { once: true });

        document.getElementById('preview').addEventListener('click', () => {
            if (!isPreviewHidden) {
                element.style.display = 'none';
                isPreviewHidden = true;
            } else {
                element.style.display = '';
                isPreviewHidden = false;
            }
        });

        document.getElementById('cancel').addEventListener('click', () => {
            document.body.removeChild(popup);
        });
    }

    // 保存屏蔽信息并生成缩略图
    async function saveBlockWithThumbnail(element) {
        const domain = window.location.hostname;
        const selector = getSelector(element);

        const canvas = await html2canvas(element, { scale: 1 });
        const originalWidth = canvas.width;
        const originalHeight = canvas.height;

        let scale = Math.min(400 / originalWidth, 100 / originalHeight, 1);
        const thumbnailCanvas = document.createElement('canvas');
        thumbnailCanvas.width = originalWidth * scale;
        thumbnailCanvas.height = originalHeight * scale;
        const ctx = thumbnailCanvas.getContext('2d');
        ctx.drawImage(canvas, 0, 0, thumbnailCanvas.width, thumbnailCanvas.height);
        const thumbnail = thumbnailCanvas.toDataURL('image/png');

        let blocks = GM_getValue('blocks', {});
        if (!blocks[domain]) {
            blocks[domain] = [];
        }
        if (!blocks[domain].some(item => item.selector === selector)) {
            blocks[domain].push({ selector, thumbnail });
            GM_setValue('blocks', blocks);
        }
    }

    // 生成简单 CSS 选择器
    function getSelector(element) {
        if (element.id) return `#${element.id}`;
        let path = [];
        while (element && element.nodeType === Node.ELEMENT_NODE) {
            let selector = element.tagName.toLowerCase();
            if (element.className && typeof element.className === 'string') {
                selector += '.' + element.className.trim().replace(/\s+/g, '.');
            }
            path.unshift(selector);
            element = element.parentElement;
        }
        return path.join(' > ');
    }

    // 应用屏蔽规则
    function applyBlocks() {
        const domain = window.location.hostname;
        const blocks = GM_getValue('blocks', {});
        if (blocks[domain]) {
            blocks[domain].forEach(item => {
                try {
                    document.querySelectorAll(item.selector).forEach(el => {
                        el.style.display = 'none';
                    });
                } catch (e) {
                    console.error(`无法应用选择器: ${item.selector}`, e);
                }
            });
        }
        applyRegexBlocks();
    }

    // 显示屏蔽记录窗口
    function showBlockList() {
        const domain = window.location.hostname;
        const blocks = GM_getValue('blocks', {});
        const blockList = blocks[domain] || [];

        const listWindow = document.createElement('div');
        listWindow.className = 'blocker-list';
        listWindow.innerHTML = `
            <h3>当前域名屏蔽记录 (${domain})</h3>
            <ul id="block-items"></ul>
            <button id="close-list">关闭</button>
        `;
        document.body.appendChild(listWindow);

        const ul = document.getElementById('block-items');
        if (blockList.length === 0) {
            ul.innerHTML = '<li>暂无屏蔽记录</li>';
        } else {
            blockList.forEach((item, index) => {
                const li = document.createElement('li');
                const img = document.createElement('img');
                img.src = item.thumbnail;
                const unblockBtn = document.createElement('button');
                unblockBtn.textContent = '取消屏蔽';
                unblockBtn.addEventListener('click', () => {
                    removeBlock(domain, index);
                    listWindow.remove();
                    applyBlocks();
                    showBlockList();
                });
                li.appendChild(img);
                li.appendChild(unblockBtn);
                ul.appendChild(li);
            });
        }

        document.getElementById('close-list').addEventListener('click', () => {
            document.body.removeChild(listWindow);
        });
    }

    // 删除屏蔽记录
    function removeBlock(domain, index) {
        let blocks = GM_getValue('blocks', {});
        if (blocks[domain] && blocks[domain][index]) {
            blocks[domain].splice(index, 1);
            if (blocks[domain].length === 0) {
                delete blocks[domain];
            }
            GM_setValue('blocks', blocks);
        }
    }

    // 显示规则屏蔽输入和管理窗口
    function showRegexBlockInput() {
        const domain = window.location.hostname;
        let regexBlocks = GM_getValue('regexBlocks', {});
        let rules = regexBlocks[domain] || [];

        const popup = document.createElement('div');
        popup.className = 'blocker-popup';
        popup.innerHTML = `
            <p>设置屏蔽规则(层级:0 表示当前元素,1 表示父元素,依此类推):</p>
            <button id="regex-mode">正则模式</button>
            <button id="simple-mode">简单模式</button>
            <div id="input-container"></div>
            <div id="rules-list">
                <h3>当前规则列表</h3>
                <div id="rules-rows"></div>
            </div>
            <div>
                <button id="add-rule-row">新增规则</button>
                <button id="save-rules">保存</button>
                <button id="cancel-rule">取消</button>
            </div>
        `;
        document.body.appendChild(popup);

        const inputContainer = document.getElementById('input-container');
        const rulesRows = document.getElementById('rules-rows');
        let isSimpleMode = false;
        let tempRules = [...rules]; // 临时存储规则,保存时写入

        // 正则模式输入
        function showRegexInput() {
            inputContainer.innerHTML = `
                <div class="rule-row">
                    <input type="text" class="regex-input" placeholder="正则规则" />
                    <input type="number" class="level-input" placeholder="层级" value="0" min="0" />
                    <button class="preview-rule">预览</button>
                </div>
            `;
            attachPreviewListeners();
        }

        // 简单模式输入
        function showSimpleInput() {
            inputContainer.innerHTML = `
                <div class="rule-row">
                    <select class="logic-select">
                        <option value="contains">包含</option>
                        <option value="not-contains">不包含</option>
                        <option value="equals">等于</option>
                    </select>
                    <input type="text" class="simple-input" placeholder="文本内容" />
                    <input type="number" class="level-input" placeholder="层级" value="0" min="0" />
                    <button class="preview-rule">预览</button>
                </div>
            `;
            attachPreviewListeners();
        }

        // 切换模式
        document.getElementById('regex-mode').addEventListener('click', () => {
            isSimpleMode = false;
            showRegexInput();
        });
        document.getElementById('simple-mode').addEventListener('click', () => {
            isSimpleMode = true;
            showSimpleInput();
        });

        // 默认显示正则模式
        showRegexInput();

        // 渲染现有规则
        function renderRules() {
            rulesRows.innerHTML = '';
            if (tempRules.length === 0) {
                rulesRows.innerHTML = '<p>暂无规则</p>';
                return;
            }
            tempRules.forEach((rule, index) => {
                const row = document.createElement('div');
                row.className = 'rule-row';
                row.innerHTML = `
                    <input type="text" class="rule-regex" value="${rule.regex}" />
                    <input type="number" class="rule-level" value="${rule.level}" min="0" />
                    <button class="delete-rule">删除</button>
                `;
                const regexInput = row.querySelector('.rule-regex');
                const levelInput = row.querySelector('.rule-level');
                const deleteBtn = row.querySelector('.delete-rule');

                // 实时更新临时规则
                regexInput.addEventListener('input', () => {
                    tempRules[index].regex = regexInput.value;
                });
                levelInput.addEventListener('input', () => {
                    tempRules[index].level = parseInt(levelInput.value, 10);
                });

                // 删除规则
                deleteBtn.addEventListener('click', () => {
                    tempRules.splice(index, 1);
                    renderRules();
                });

                rulesRows.appendChild(row);
            });
        }

        // 新增规则行
        document.getElementById('add-rule-row').addEventListener('click', () => {
            let regex, level;
            if (isSimpleMode) {
                const logic = inputContainer.querySelector('.logic-select').value;
                const text = inputContainer.querySelector('.simple-input').value.trim();
                level = parseInt(inputContainer.querySelector('.level-input').value, 10);
                if (!text || isNaN(level) || level < 0) {
                    alert('请输入有效的文本和层级');
                    return;
                }
                regex = convertSimpleToRegex(logic, text);
            } else {
                regex = inputContainer.querySelector('.regex-input').value.trim();
                level = parseInt(inputContainer.querySelector('.level-input').value, 10);
                if (!regex || isNaN(level) || level < 0) {
                    alert('请输入有效的正则规则和层级');
                    return;
                }
            }
            tempRules.push({ regex, level });
            renderRules();
            // 清空输入框
            if (isSimpleMode) {
                inputContainer.querySelector('.simple-input').value = '';
                inputContainer.querySelector('.level-input').value = '0';
            } else {
                inputContainer.querySelector('.regex-input').value = '';
                inputContainer.querySelector('.level-input').value = '0';
            }
        });

        // 保存规则
        document.getElementById('save-rules').addEventListener('click', () => {
            regexBlocks[domain] = tempRules;
            GM_setValue('regexBlocks', regexBlocks);
            applyRegexBlocks();
            document.body.removeChild(popup);
        });

        // 取消
        document.getElementById('cancel-rule').addEventListener('click', () => {
            document.body.removeChild(popup);
        });

        // 将简单模式转换为正则表达式
        function convertSimpleToRegex(logic, text) {
            const escapedText = text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
            switch (logic) {
                case 'contains': return `.*${escapedText}.*`;
                case 'not-contains': return `^(?!.*${escapedText}).*$`;
                case 'equals': return `^${escapedText}$`;
                default: return escapedText;
            }
        }

        // 为预览按钮绑定事件
        function attachPreviewListeners() {
            const previewBtn = inputContainer.querySelector('.preview-rule');
            let previewActive = false;
            let affectedElements = [];

            previewBtn.addEventListener('click', () => {
                if (!previewActive) {
                    let regex, level;
                    if (isSimpleMode) {
                        const logic = inputContainer.querySelector('.logic-select').value;
                        const text = inputContainer.querySelector('.simple-input').value.trim();
                        level = parseInt(inputContainer.querySelector('.level-input').value, 10);
                        if (!text || isNaN(level) || level < 0) {
                            alert('请输入有效的文本和层级');
                            return;
                        }
                        regex = convertSimpleToRegex(logic, text);
                    } else {
                        regex = inputContainer.querySelector('.regex-input').value.trim();
                        level = parseInt(inputContainer.querySelector('.level-input').value, 10);
                        if (!regex || isNaN(level) || level < 0) {
                            alert('请输入有效的正则规则和层级');
                            return;
                        }
                    }
                    try {
                        const ruleRegex = new RegExp(regex);
                        const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
                        let node;
                        affectedElements = [];
                        while (node = walker.nextNode()) {
                            if (ruleRegex.test(node.textContent)) {
                                let element = node.parentElement;
                                for (let i = 0; i < level; i++) {
                                    if (element.parentElement) {
                                        element = element.parentElement;
                                    } else {
                                        break;
                                    }
                                }
                                element.style.display = 'none';
                                affectedElements.push(element);
                            }
                        }
                        previewActive = true;
                        previewBtn.textContent = '取消预览';
                    } catch (e) {
                        alert('正则表达式无效,请检查输入');
                    }
                } else {
                    affectedElements.forEach(el => el.style.display = '');
                    affectedElements = [];
                    previewActive = false;
                    previewBtn.textContent = '预览';
                }
            });
        }

        // 初始渲染规则
        renderRules();
    }

    // 应用规则屏蔽
    function applyRegexBlocks() {
        const domain = window.location.hostname;
        const regexBlocks = GM_getValue('regexBlocks', {});
        const rules = regexBlocks[domain] || [];
        rules.forEach(rule => {
            try {
                const regex = new RegExp(rule.regex);
                const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null, false);
                let node;
                while (node = walker.nextNode()) {
                    if (regex.test(node.textContent)) {
                        let element = node.parentElement;
                        for (let i = 0; i < rule.level; i++) {
                            if (element.parentElement) {
                                element = element.parentElement;
                            } else {
                                break;
                            }
                        }
                        element.style.display = 'none';
                    }
                }
            } catch (e) {
                console.error(`无法应用规则: ${rule.regex}`, e);
            }
        });
    }

    // 页面加载时立即应用屏蔽规则
    applyBlocks();
    const observer = new MutationObserver(() => applyBlocks());
    observer.observe(document.body, { childList: true, subtree: true });
})();