Greasy Fork

图片隐藏

图片隐藏与显示

目前为 2024-12-05 提交的版本。查看 最新版本

// ==UserScript==
// @name         图片隐藏
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  图片隐藏与显示
// @match        *://*/*
// @author       eternal5130
// @license      MIT
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_registerMenuCommand
// ==/UserScript==

(function() {
    'use strict';

    // 默认快捷键配置
    const DEFAULT_SHORTCUT = {
        single: {
            keys: ['MouseMiddle']
        },
        global: {
            keys: ['AltLeft', 'KeyH']
        }
    };

    // 存储图片状态
    const imageStates = new Map();
    const currentPressedKeys = new Set();
    let currentHoverTarget = null;
    let configModal = null;
    let globalImageCounter = 0;

    // 获取快捷键配置
    function getShortcutConfig() {
        try {
            const savedConfig = GM_getValue('imageHiderShortcut');
            return savedConfig ? JSON.parse(savedConfig) : { ...DEFAULT_SHORTCUT };
        } catch (error) {
            console.error('获取快捷键配置失败,使用默认配置', error);
            return { ...DEFAULT_SHORTCUT };
        }
    }

    // 保存快捷键配置
    function saveShortcutConfig(config) {
        try {
            GM_setValue('imageHiderShortcut', JSON.stringify(config));
        } catch (error) {
            console.error('保存快捷键配置失败', error);
            alert('保存配置时出错,请重试');
        }
    }

    // 将按键码转换为可读名称
    function getKeyDisplayName(code) {
        const keyNameMap = {
            'AltLeft': 'Alt (左)', 'AltRight': 'Alt (右)',
            'ControlLeft': 'Ctrl (左)', 'ControlRight': 'Ctrl (右)',
            'ShiftLeft': 'Shift (左)', 'ShiftRight': 'Shift (右)',
            'MetaLeft': 'Meta (左)', 'MetaRight': 'Meta (右)',
            'MouseMiddle': '鼠标中键',
            'KeyA': 'A', 'KeyB': 'B', 'KeyC': 'C', 'KeyD': 'D', 'KeyE': 'E',
            'KeyF': 'F', 'KeyG': 'G', 'KeyH': 'H', 'KeyI': 'I', 'KeyJ': 'J',
            'KeyK': 'K', 'KeyL': 'L', 'KeyM': 'M', 'KeyN': 'N', 'KeyO': 'O',
            'KeyP': 'P', 'KeyQ': 'Q', 'KeyR': 'R', 'KeyS': 'S', 'KeyT': 'T',
            'KeyU': 'U', 'KeyV': 'V', 'KeyW': 'W', 'KeyX': 'X', 'KeyY': 'Y', 'KeyZ': 'Z',
            'Digit0': '0', 'Digit1': '1', 'Digit2': '2', 'Digit3': '3',
            'Digit4': '4', 'Digit5': '5', 'Digit6': '6', 'Digit7': '7',
            'Digit8': '8', 'Digit9': '9'
        };
        return keyNameMap[code] || code;
    }

    // 检查快捷键是否匹配
    function isShortcutMatch(config) {
        const { keys = [] } = config;
        return keys.every(key => currentPressedKeys.has(key));
    }

    // 创建模态框样式
    function createModalStyles() {
        const style = document.createElement('style');
        style.textContent = `
            .image-hider-modal {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background: #fff;
                border-radius: 12px;
                box-shadow: 0 10px 25px rgba(0,0,0,0.1);
                padding: 24px;
                z-index: 10000;
                width: 500px;
                max-height: 80vh;
                overflow-y: auto;
                font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
                position: relative;
            }
            .image-hider-modal h2 {
                color: #333;
                margin-bottom: 16px;
                font-weight: 600;
            }
            .image-hider-modal button {
                background-color: #4CAF50;
                color: white;
                border: none;
                padding: 10px 16px;
                margin: 8px;
                border-radius: 6px;
                cursor: pointer;
                transition: background-color 0.3s;
            }
            .image-hider-modal button:hover {
                background-color: #45a049;
            }
            .image-hider-modal button.cancel {
                background-color: #f44336;
            }
            .image-hider-modal .current-shortcut {
                background-color: #f0f0f0;
                padding: 10px;
                margin-bottom: 16px;
                border-radius: 6px;
                text-align: center;
            }
            .image-hider-modal .shortcut-input {
                width: 100%;
                padding: 10px;
                margin: 10px 0;
                border: 1px solid #ddd;
                border-radius: 6px;
                font-size: 16px;
                text-align: center;
            }
            .image-hider-modal .close-btn {
                position: absolute;
                right: 16px;
                top: 16px;
                width: 24px;
                height: 24px;
                cursor: pointer;
                display: flex;
                align-items: center;
                justify-content: center;
                border-radius: 50%;
                background: #f5f5f5;
                border: none;
                padding: 0;
            }
            .image-hider-modal .close-btn:hover {
                background: #e0e0e0;
            }
            .image-hider-modal .close-btn::before,
            .image-hider-modal .close-btn::after {
                content: '';
                position: absolute;
                width: 14px;
                height: 2px;
                background: #666;
            }
            .image-hider-modal .close-btn::before {
                transform: rotate(45deg);
            }
            .image-hider-modal .close-btn::after {
                transform: rotate(-45deg);
            }
            .image-hider-modal .shortcut-section {
                background: #f8f9fa;
                border-radius: 8px;
                padding: 16px;
                margin-bottom: 16px;
            }
            .image-hider-modal .shortcut-section h3 {
                color: #2c3e50;
                margin: 0 0 12px 0;
                font-size: 1.1em;
                font-weight: 500;
            }
            .image-hider-modal .shortcut-section .current-shortcut {
                background-color: #fff;
                margin-bottom: 12px;
            }
            .image-hider-modal .shortcut-section button {
                margin: 8px 0;
                width: 100%;
            }
            .image-hider-modal .button-group {
                margin-top: 24px;
            }
        `;
        document.head.appendChild(style);
    }

    // 创建配置弹窗
    function createConfigModal() {
        if (!document.querySelector('#image-hider-modal-styles')) {
            const style = document.createElement('style');
            style.id = 'image-hider-modal-styles';
            style.textContent = `
                .image-hider-modal {
                    position: fixed;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: #fff;
                    border-radius: 12px;
                    box-shadow: 0 10px 25px rgba(0,0,0,0.15);
                    padding: 28px;
                    z-index: 10000;
                    width: 480px;
                    max-height: 80vh;
                    overflow-y: auto;
                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
                    animation: modalFadeIn 0.2s ease-out;
                }

                @keyframes modalFadeIn {
                    from {
                        opacity: 0;
                        transform: translate(-50%, -48%);
                    }
                    to {
                        opacity: 1;
                        transform: translate(-50%, -50%);
                    }
                }

                .image-hider-modal h2 {
                    color: #2c3e50;
                    margin: 0 0 20px 0;
                    font-size: 1.5em;
                    font-weight: 600;
                }

                .image-hider-modal .current-shortcut {
                    background-color: #f8f9fa;
                    padding: 12px 16px;
                    margin-bottom: 20px;
                    border-radius: 8px;
                    text-align: center;
                    border: 1px solid #e9ecef;
                    color: #495057;
                }

                .image-hider-modal .shortcut-input {
                    width: 100%;
                    padding: 12px 16px;
                    margin: 10px 0;
                    border: 2px solid #e9ecef;
                    border-radius: 8px;
                    font-size: 16px;
                    text-align: center;
                    background: #f8f9fa;
                    transition: all 0.2s ease;
                    color: #495057;
                }

                .image-hider-modal .shortcut-input:focus {
                    border-color: #4CAF50;
                    outline: none;
                    background: #fff;
                }

                .image-hider-modal .shortcut-input.recording {
                    background: #e8f5e9;
                    border-color: #4CAF50;
                }

                .image-hider-modal .button-group {
                    display: flex;
                    gap: 12px;
                    margin-top: 20px;
                    justify-content: center;
                }

                .image-hider-modal button {
                    padding: 10px 20px;
                    border-radius: 8px;
                    border: none;
                    cursor: pointer;
                    font-size: 14px;
                    font-weight: 500;
                    transition: all 0.2s ease;
                    min-width: 120px;
                }

                .image-hider-modal button#setShortcutBtn {
                    background-color: #f8f9fa;
                    color: #495057;
                    border: 1px solid #dee2e6;
                }

                .image-hider-modal button#setShortcutBtn:hover {
                    background-color: #e9ecef;
                }

                .image-hider-modal button#setShortcutBtn.recording {
                    background-color: #4CAF50;
                    color: white;
                    border: none;
                }

                .image-hider-modal button#saveConfigBtn {
                    background-color: #4CAF50;
                    color: white;
                }

                .image-hider-modal button#saveConfigBtn:hover {
                    background-color: #43a047;
                }

                .image-hider-modal .close-btn {
                    position: absolute;
                    right: 16px;
                    top: 16px;
                    width: 32px;
                    height: 32px;
                    border-radius: 16px;
                    background: #f8f9fa;
                    border: none;
                    cursor: pointer;
                    transition: all 0.2s ease;
                    padding: 0;
                    display: flex;
                    align-items: center;
                    justify-content: center;
                }

                .image-hider-modal .close-btn:hover {
                    background: #e9ecef;
                }

                .image-hider-modal .close-btn::before,
                .image-hider-modal .close-btn::after {
                    content: '';
                    position: absolute;
                    width: 16px;
                    height: 2px;
                    background: #495057;
                    transition: background 0.2s;
                }

                .image-hider-modal .close-btn::before {
                    transform: rotate(45deg);
                }

                .image-hider-modal .close-btn::after {
                    transform: rotate(-45deg);
                }

                .image-hider-modal .hint {
                    color: #6c757d;
                    font-size: 0.9em;
                    margin-top: 16px;
                    line-height: 1.5;
                    padding: 12px;
                    background: #f8f9fa;
                    border-radius: 8px;
                }

                .image-hider-modal .button-wrapper {
                    display: flex;
                    justify-content: center;
                    margin: 12px 0;
                }

                .image-hider-modal button {
                    opacity: 1;
                    transition: all 0.3s ease;
                }

                .image-hider-modal.closing {
                    opacity: 0;
                    transform: translate(-50%, -48%);
                    transition: all 0.3s ease;
                }

                .image-hider-modal .success-message {
                    position: absolute;
                    top: 50%;
                    left: 50%;
                    transform: translate(-50%, -50%);
                    background: rgba(76, 175, 80, 0.9);
                    color: white;
                    padding: 16px 32px;
                    border-radius: 8px;
                    font-weight: 500;
                    opacity: 0;
                    transition: opacity 0.3s ease;
                }

                .image-hider-modal .success-message.show {
                    opacity: 1;
                }
            `;
            document.head.appendChild(style);
        }

        if (configModal) {
            configModal.remove();
        }

        configModal = document.createElement('div');
        configModal.className = 'image-hider-modal';

        const shortcutConfig = getShortcutConfig();
        const singleShortcutDisplay = (shortcutConfig.single.keys || DEFAULT_SHORTCUT.single.keys)
            .map(key => getKeyDisplayName(key))
            .join(' + ');
        const globalShortcutDisplay = (shortcutConfig.global.keys || DEFAULT_SHORTCUT.global.keys)
            .map(key => getKeyDisplayName(key))
            .join(' + ');

        configModal.innerHTML = `
            <button class="close-btn" id="closeConfigBtn"></button>
            <h2>图片隐藏快捷键配置</h2>

            <div class="shortcut-section">
                <h3>单个图片快捷键</h3>
                <div class="current-shortcut">
                    <strong>当前快捷键:</strong>${singleShortcutDisplay}
                </div>
                <input type="text" readonly class="shortcut-input" id="singleShortcutInput"
                       placeholder="点击"设置快捷键"开始录制">
                <div class="button-wrapper">
                    <button id="setSingleShortcutBtn">设置快捷键</button>
                </div>
            </div>

            <div class="shortcut-section">
                <h3>全局图片快捷键</h3>
                <div class="current-shortcut">
                    <strong>当前快捷键:</strong>${globalShortcutDisplay}
                </div>
                <input type="text" readonly class="shortcut-input" id="globalShortcutInput"
                       placeholder="点击"设置快捷键"开始录制">
                <div class="button-wrapper">
                    <button id="setGlobalShortcutBtn">设置快捷键</button>
                </div>
            </div>

            <div class="button-group">
                <button id="saveConfigBtn">保存设置</button>
            </div>

            <div class="hint">
                <strong>使用说明:</strong><br>
                1. 单个图片快捷键:悬停在图片上时触发隐藏/显示<br>
                2. 全局图片快捷键:一键切换所有图片的显示状态<br>
                3. 点击对应的"设置快捷键"开始录制<br>
                4. 完成设置后点击"保存设置"确认更改
            </div>
        `;

        document.body.appendChild(configModal);

        const setSingleShortcutBtn = configModal.querySelector('#setSingleShortcutBtn');
        const setGlobalShortcutBtn = configModal.querySelector('#setGlobalShortcutBtn');
        const saveConfigBtn = configModal.querySelector('#saveConfigBtn');
        const closeConfigBtn = configModal.querySelector('#closeConfigBtn');
        const singleShortcutInput = configModal.querySelector('#singleShortcutInput');
        const globalShortcutInput = configModal.querySelector('#globalShortcutInput');

        let isRecording = false;
        let currentRecordingType = null;
        let singlePressedKeys = new Set();
        let globalPressedKeys = new Set();

        function startRecording(type, input, button) {
            if (isRecording) return;

            isRecording = true;
            currentRecordingType = type;
            const keysSet = type === 'single' ? singlePressedKeys : globalPressedKeys;
            keysSet.clear();

            input.value = '请按下快捷键组合...';
            input.classList.add('recording');
            button.classList.add('recording');
            button.textContent = '正在录制...';

            const keydownHandler = (e) => {
                e.preventDefault();
                keysSet.add(e.code);
                updateShortcutDisplay(input, keysSet);
            };

            const keyupHandler = (e) => {
                e.preventDefault();
                if (keysSet.size > 0) {
                    finishRecording(input, button);
                }
            };

            const mousedownHandler = (e) => {
                e.preventDefault();
                if (e.button === 1) {
                    keysSet.add('MouseMiddle');
                    updateShortcutDisplay(input, keysSet);
                }
            };

            const mouseupHandler = (e) => {
                e.preventDefault();
                if (e.button === 1 && keysSet.size > 0) {
                    finishRecording(input, button);
                }
            };

            document.addEventListener('keydown', keydownHandler);
            document.addEventListener('keyup', keyupHandler);
            document.addEventListener('mousedown', mousedownHandler);
            document.addEventListener('mouseup', mouseupHandler);

            // 保存清理函数
            button.cleanup = () => {
                document.removeEventListener('keydown', keydownHandler);
                document.removeEventListener('keyup', keyupHandler);
                document.removeEventListener('mousedown', mousedownHandler);
                document.removeEventListener('mouseup', mouseupHandler);
            };
        }

        function updateShortcutDisplay(input, keysSet) {
            input.value = Array.from(keysSet)
                .map(getKeyDisplayName)
                .join(' + ');
        }

        function finishRecording(input, button) {
            isRecording = false;
            currentRecordingType = null;
            input.classList.remove('recording');
            button.classList.remove('recording');
            button.textContent = '设置快捷键';

            if (button.cleanup) {
                button.cleanup();
                button.cleanup = null;
            }
        }

        setSingleShortcutBtn.addEventListener('click', () => {
            startRecording('single', singleShortcutInput, setSingleShortcutBtn);
        });

        setGlobalShortcutBtn.addEventListener('click', () => {
            startRecording('global', globalShortcutInput, setGlobalShortcutBtn);
        });

        saveConfigBtn.addEventListener('click', () => {
            const shortcutConfig = getShortcutConfig();
            let hasChanges = false;

            // 检查并保存单个图片快捷键
            if (singleShortcutInput.value && singleShortcutInput.value !== '请按下快捷键组合...') {
                shortcutConfig.single.keys = Array.from(singlePressedKeys);
                hasChanges = true;
            }

            // 检查并保存全局快捷键
            if (globalShortcutInput.value && globalShortcutInput.value !== '请按下快捷键组合...') {
                shortcutConfig.global.keys = Array.from(globalPressedKeys);
                hasChanges = true;
            }

            if (hasChanges) {
                saveShortcutConfig(shortcutConfig);

                // 创建成功提示
                const successMessage = document.createElement('div');
                successMessage.className = 'success-message';
                successMessage.textContent = '快捷键设置已保存';
                configModal.appendChild(successMessage);

                // 添加过渡效果
                requestAnimationFrame(() => {
                    successMessage.classList.add('show');
                    setTimeout(() => {
                        configModal.classList.add('closing');
                        setTimeout(() => {
                            configModal.remove();
                            configModal = null;
                        }, 300);
                    }, 1000);
                });
            } else {
                alert('请先设置至少一个快捷键');
            }
        });

        closeConfigBtn.addEventListener('click', () => {
            if (isRecording) {
                const button = currentRecordingType === 'single' ? setSingleShortcutBtn : setGlobalShortcutBtn;
                const input = currentRecordingType === 'single' ? singleShortcutInput : globalShortcutInput;
                finishRecording(input, button);
            }
            configModal.remove();
            configModal = null;
        });

        // 添加点击外部关闭功能
        document.addEventListener('mousedown', (e) => {
            if (configModal && !configModal.contains(e.target)) {
                closeConfigBtn.click();
            }
        });
    }

    // 优化图片状态管理
    function ImageState(imgElement) {
        this.element = imgElement;
        this.originalOpacity = getComputedStyle(imgElement).opacity;
        this.originalPointerEvents = getComputedStyle(imgElement).pointerEvents;
        this.isHidden = false;
        this.overlay = null;
        this.observers = new Set();
    }

    ImageState.prototype.cleanup = function() {
        this.observers.forEach(observer => observer.disconnect());
        this.observers.clear();
        this.overlay?.cleanup?.();
        this.overlay?.remove();
    };

    // 优化覆盖层创建
    function createOverlay(imgElement) {
        const overlay = document.createElement('div');
        const state = imageStates.get(imgElement.dataset.imageId);

        const updatePosition = throttle(() => {
            const rect = imgElement.getBoundingClientRect();
            Object.assign(overlay.style, {
                position: 'absolute',
                left: `${rect.left + window.scrollX}px`,
                top: `${rect.top + window.scrollY}px`,
                width: `${rect.width}px`,
                height: `${rect.height}px`,
                backgroundColor: 'transparent',
                pointerEvents: 'auto',
                zIndex: '9999'
            });
        }, 16); // 约60fps

        updatePosition();
        overlay.dataset.linkedImage = 'true';

        // 使用 IntersectionObserver 优化性能
        const intersectionObserver = new IntersectionObserver((entries) => {
            entries.forEach(entry => {
                if (entry.isIntersecting) {
                    updatePosition();
                }
            });
        });

        intersectionObserver.observe(imgElement);
        state.observers.add(intersectionObserver);

        // 使用 ResizeObserver 监听尺寸变化
        const resizeObserver = new ResizeObserver(updatePosition);
        resizeObserver.observe(imgElement);
        state.observers.add(resizeObserver);

        // 优化滚动监听
        const scrollHandler = () => requestAnimationFrame(updatePosition);
        window.addEventListener('scroll', scrollHandler, { passive: true });

        overlay.addEventListener('mouseover', () => currentHoverTarget = imgElement);
        overlay.addEventListener('mouseout', () => currentHoverTarget = null);

        overlay.cleanup = () => {
            window.removeEventListener('scroll', scrollHandler);
        };

        document.body.appendChild(overlay);
        return overlay;
    }

    // 优化切换图片可见性
    function toggleImageVisibility(imgElement) {
        if (!imgElement.dataset.imageId) {
            imgElement.dataset.imageId = `img_${++globalImageCounter}`;
            imageStates.set(imgElement.dataset.imageId, new ImageState(imgElement));
        }

        const state = imageStates.get(imgElement.dataset.imageId);

        requestAnimationFrame(() => {
            if (!state.isHidden) {
                imgElement.style.cssText = `
                    opacity: 0;
                    pointer-events: none;
                    transition: opacity 0.2s ease;
                `;
                state.overlay = createOverlay(imgElement);
            } else {
                imgElement.style.cssText = `
                    opacity: ${state.originalOpacity || '1'};
                    pointer-events: ${state.originalPointerEvents || 'auto'};
                    transition: opacity 0.2s ease;
                `;
                state.cleanup();
            }
            state.isHidden = !state.isHidden;
        });
    }

    // 添加节流函数
    function throttle(func, limit) {
        let inThrottle;
        return function(...args) {
            if (!inThrottle) {
                func.apply(this, args);
                inThrottle = true;
                setTimeout(() => inThrottle = false, limit);
            }
        }
    }

    // 键盘按下事件
    function handleKeyDown(event) {
        currentPressedKeys.add(event.code);
        checkShortcut();
    }

    // 键盘抬起事件
    function handleKeyUp(event) {
        currentPressedKeys.delete(event.code);
    }

    // 鼠标悬停处理
    function handleMouseOver(event) {
        if (event.target.tagName.toLowerCase() === 'img') {
            currentHoverTarget = event.target;
        }
    }

    // 鼠标移出处理
    function handleMouseOut(event) {
        if (event.target.tagName.toLowerCase() === 'img') {
            currentHoverTarget = null;
        }
    }

    // 重置状态的函数
    function resetState() {
        currentPressedKeys.clear();
        currentHoverTarget = null;
    }

    // 注册事件监听
    document.addEventListener('mouseover', handleMouseOver);
    document.addEventListener('mouseout', handleMouseOut);
    document.addEventListener('keydown', handleKeyDown);
    document.addEventListener('keyup', handleKeyUp);

    // 页面刷新/切换时重置按键状态
    window.addEventListener('beforeunload', resetState);

    // 注册菜单命令
    GM_registerMenuCommand('配置快捷键', createConfigModal);

    // 增加鼠标事件处理函数
    function handleMouseDown(event) {
        if (event.button === 1) {
            event.preventDefault();
            currentPressedKeys.add('MouseMiddle');
            checkShortcut();
        }
    }

    function handleMouseUp(event) {
        if (event.button === 1) {
            currentPressedKeys.delete('MouseMiddle');
        }
    }

    // 检查快捷键组合是否匹配
    function checkShortcut() {
        const shortcutConfig = getShortcutConfig();

        // 检查单个图片快捷键
        if (isShortcutMatch(shortcutConfig.single) &&
            currentHoverTarget &&
            currentHoverTarget.tagName.toLowerCase() === 'img') {
            toggleImageVisibility(currentHoverTarget);
        }

        // 检查全局快捷键
        if (isShortcutMatch(shortcutConfig.global)) {
            toggleAllImages();
        }
    }

    // 在主函数中添加鼠标事件监听
    document.addEventListener('mousedown', handleMouseDown);
    document.addEventListener('mouseup', handleMouseUp);

    // 优化全局图片切换
    function toggleAllImages() {
        const images = document.getElementsByTagName('img');
        const shouldHide = Array.from(images).some(img => !imageStates.get(img.dataset.imageId)?.isHidden);

        // 使用 requestIdleCallback 优化性能
        const processInChunks = (items, chunkSize = 5) => {
            let index = 0;

            function processChunk(deadline) {
                while (index < items.length && deadline.timeRemaining() > 0) {
                    const img = items[index++];
                    const state = imageStates.get(img.dataset.imageId);
                    if (!state || state.isHidden !== shouldHide) {
                        toggleImageVisibility(img);
                    }
                }

                if (index < items.length) {
                    requestIdleCallback(processChunk);
                }
            }

            requestIdleCallback(processChunk);
        };

        processInChunks(Array.from(images));
    }
})();