Greasy Fork

网页元素屏蔽器

屏蔽任意网站上的元素,以原始比例缩略图显示屏蔽记录,修复确认屏蔽需点击两次问题

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

// ==UserScript==
// @name         网页元素屏蔽器
// @namespace    http://tampermonkey.net/
// @version      0.1
// @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);
        }
        .blocker-popup button {
            margin: 0 5px;
        }
        .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;
        }
        .blocker-list ul {
            list-style: none;
            padding: 0;
        }
        .blocker-list li {
            margin: 10px 0;
            display: flex;
            align-items: center;
            width: 100%;
        }
        .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;
        }
    `;
    document.head.appendChild(style);

    // 注册菜单
    GM_registerMenuCommand('屏蔽设置', startBlockingMode);
    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);

        // 使用 html2canvas 生成截图
        const canvas = await html2canvas(element, { scale: 1 });
        const originalWidth = canvas.width;
        const originalHeight = canvas.height;

        // 计算缩放比例,限制 width ≤ 400,height ≤ 100
        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);
                }
            });
        }
    }

    // 显示屏蔽记录窗口(仅缩略图)
    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);
        }
    }

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