Subtitle Overlay Tool 字幕遮盖工具

Create a draggable, resizable overlay to cover subtitles on any website

// ==UserScript==
// @name         Subtitle Overlay Tool 字幕遮盖工具
// @namespace    http://tampermonkey.net/
// @license      MIT
// @version      1.2
// @description  Create a draggable, resizable overlay to cover subtitles on any website
// @author       Fei
// @match        *://*/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    let overlay = null;
    let isVisible = false;
    let isDragging = false;
    let isResizing = false;
    let currentResizeHandle = null;
    let dragOffset = { x: 0, y: 0 };
    let resizeStartSize = { width: 0, height: 0 };
    let resizeStartPos = { left: 0, top: 0 };
    let resizeStartMouse = { x: 0, y: 0 };
    let opacity = 1.0; // Start fully opaque

    // Create the overlay element
    function createOverlay() {
        overlay = document.createElement('div');
        overlay.id = 'subtitle-overlay';
        overlay.style.cssText = `
            position: fixed;
            bottom: 60px;
            left: 50%;
            transform: translateX(-50%);
            width: 800px;
            height: 200px;
            background: linear-gradient(135deg, rgba(20, 20, 20, 0.95), rgba(10, 10, 10, 0.98));
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            border-radius: 16px;
            cursor: move;
            z-index: 2147483647;
            display: none;
            user-select: none;
            box-sizing: border-box;
            box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4), 
                        0 8px 16px rgba(0, 0, 0, 0.2),
                        inset 0 1px 0 rgba(255, 255, 255, 0.05);
            transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
        `;

        // Create resize handles for all four corners
        const resizeHandles = [
            { position: 'top-left', cursor: 'nw-resize', style: 'top: -4px; left: -4px; border-radius: 50% 0 50% 0;' },
            { position: 'top-right', cursor: 'ne-resize', style: 'top: -4px; right: -4px; border-radius: 0 50% 0 50%;' },
            { position: 'bottom-left', cursor: 'sw-resize', style: 'bottom: -4px; left: -4px; border-radius: 50% 0 50% 0;' },
            { position: 'bottom-right', cursor: 'se-resize', style: 'bottom: -4px; right: -4px; border-radius: 0 50% 0 50%;' }
        ];

        resizeHandles.forEach(handle => {
            const resizeHandle = document.createElement('div');
            resizeHandle.className = 'resize-handle';
            resizeHandle.dataset.position = handle.position;
            resizeHandle.style.cssText = `
                position: absolute;
                width: 16px;
                height: 16px;
                background: linear-gradient(135deg, rgba(99, 102, 241, 0.6), rgba(168, 85, 247, 0.6));
                border: 2px solid rgba(255, 255, 255, 0.2);
                cursor: ${handle.cursor};
                ${handle.style}
                transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
                opacity: 0;
                transform: scale(0.8);
            `;
            
            overlay.appendChild(resizeHandle);
        });

        // Show resize handles on hover
        overlay.addEventListener('mouseenter', () => {
            overlay.querySelectorAll('.resize-handle').forEach(handle => {
                handle.style.opacity = '1';
                handle.style.transform = 'scale(1)';
            });
        });

        overlay.addEventListener('mouseleave', () => {
            if (!isResizing && !isDragging) {
                overlay.querySelectorAll('.resize-handle').forEach(handle => {
                    handle.style.opacity = '0';
                    handle.style.transform = 'scale(0.8)';
                });
            }
        });

        // Create close button
        const closeButton = document.createElement('div');
        closeButton.innerHTML = '✕';
        closeButton.className = 'close-button';
        closeButton.style.cssText = `
            position: absolute;
            top: 12px;
            right: 12px;
            width: 32px;
            height: 32px;
            color: rgba(255, 255, 255, 0.7);
            font-size: 14px;
            font-weight: 500;
            cursor: pointer;
            text-align: center;
            line-height: 32px;
            border-radius: 50%;
            background: rgba(255, 255, 255, 0.08);
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            border: 1px solid rgba(255, 255, 255, 0.1);
            opacity: 0.7;
            transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
            z-index: 10;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
        `;
        closeButton.onmouseover = () => {
            closeButton.style.opacity = '1';
            closeButton.style.background = 'rgba(239, 68, 68, 0.2)';
            closeButton.style.color = 'rgba(255, 255, 255, 0.9)';
            closeButton.style.transform = 'scale(1.05)';
        };
        closeButton.onmouseout = () => {
            closeButton.style.opacity = '0.7';
            closeButton.style.background = 'rgba(255, 255, 255, 0.08)';
            closeButton.style.color = 'rgba(255, 255, 255, 0.7)';
            closeButton.style.transform = 'scale(1)';
        };
        closeButton.onclick = hideOverlay;
        overlay.appendChild(closeButton);

        // Create info text
        const infoText = document.createElement('div');
        infoText.innerHTML = `
            <div class="title">Subtitle Blocker</div>
            <div class="instructions">
                <span class="instruction-item">🔀 Drag to move</span>
                <span class="instruction-item">📐 Resize from corners</span>
                <span class="instruction-item">🎚️ Scroll to adjust opacity</span>
                <span class="instruction-item">⌨️ Ctrl+B to toggle</span>
            </div>
        `;
        infoText.className = 'info-text';
        infoText.style.cssText = `
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            text-align: center;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
            pointer-events: none;
            opacity: 0.8;
        `;
        
        // Add styles for title and instructions
        const style = document.createElement('style');
        style.textContent = `
            #subtitle-overlay .title {
                color: rgba(255, 255, 255, 0.9);
                font-size: 18px;
                font-weight: 600;
                margin-bottom: 12px;
                letter-spacing: 0.5px;
            }
            
            #subtitle-overlay .instructions {
                display: flex;
                flex-wrap: wrap;
                justify-content: center;
                gap: 16px;
                max-width: 400px;
                margin: 0 auto;
            }
            
            #subtitle-overlay .instruction-item {
                color: rgba(255, 255, 255, 0.6);
                font-size: 12px;
                font-weight: 400;
                display: flex;
                align-items: center;
                gap: 6px;
                padding: 4px 8px;
                background: rgba(255, 255, 255, 0.05);
                border-radius: 8px;
                border: 1px solid rgba(255, 255, 255, 0.05);
                backdrop-filter: blur(5px);
                -webkit-backdrop-filter: blur(5px);
            }
            
            @media (max-width: 600px) {
                #subtitle-overlay .instructions {
                    flex-direction: column;
                    gap: 8px;
                }
                
                #subtitle-overlay .instruction-item {
                    font-size: 10px;
                    padding: 3px 6px;
                }
                
                #subtitle-overlay .title {
                    font-size: 16px;
                    margin-bottom: 8px;
                }
            }
        `;
        document.head.appendChild(style);
        
        overlay.appendChild(infoText);

        document.body.appendChild(overlay);
        attachEventListeners();
        handleFullscreenEvents();
    }

    function updateOpacity() {
        if (overlay) {
            // Create a more sophisticated opacity system with glassmorphism
            const alpha = Math.max(0.1, opacity);
            const gradientAlpha1 = Math.max(0.05, alpha * 0.95);
            const gradientAlpha2 = Math.max(0.1, alpha * 0.98);
            
            overlay.style.background = `linear-gradient(135deg, rgba(20, 20, 20, ${gradientAlpha1}), rgba(10, 10, 10, ${gradientAlpha2}))`;
            
            // Adjust border and shadow opacity based on main opacity
            const borderOpacity = Math.max(0.02, alpha * 0.1);
            const shadowOpacity = Math.max(0.1, alpha * 0.4);
            
            overlay.style.borderColor = `rgba(255, 255, 255, ${borderOpacity})`;
            overlay.style.boxShadow = `
                0 20px 40px rgba(0, 0, 0, ${shadowOpacity}), 
                0 8px 16px rgba(0, 0, 0, ${shadowOpacity * 0.5}),
                inset 0 1px 0 rgba(255, 255, 255, ${borderOpacity * 0.5})
            `;
        }
    }

    function showOpacityFeedback(percentage) {
        // Remove existing feedback if any
        const existingFeedback = document.getElementById('opacity-feedback');
        if (existingFeedback) {
            existingFeedback.remove();
        }

        // Create opacity feedback indicator
        const feedback = document.createElement('div');
        feedback.id = 'opacity-feedback';
        feedback.innerHTML = `${percentage}%`;
        feedback.style.cssText = `
            position: absolute;
            top: 12px;
            left: 12px;
            padding: 6px 12px;
            background: rgba(0, 0, 0, 0.8);
            color: white;
            border-radius: 8px;
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
            font-size: 12px;
            font-weight: 500;
            z-index: 10;
            pointer-events: none;
            border: 1px solid rgba(255, 255, 255, 0.2);
            backdrop-filter: blur(10px);
            -webkit-backdrop-filter: blur(10px);
            opacity: 0;
            transform: scale(0.8);
            transition: all 0.2s cubic-bezier(0.4, 0.0, 0.2, 1);
        `;

        overlay.appendChild(feedback);

        // Animate in
        requestAnimationFrame(() => {
            feedback.style.opacity = '1';
            feedback.style.transform = 'scale(1)';
        });

        // Remove after delay
        setTimeout(() => {
            if (feedback && feedback.parentNode) {
                feedback.style.opacity = '0';
                feedback.style.transform = 'scale(0.8)';
                setTimeout(() => {
                    if (feedback && feedback.parentNode) {
                        feedback.remove();
                    }
                }, 200);
            }
        }, 1500);
    }

    function handleFullscreenEvents() {
        // Listen for fullscreen changes
        document.addEventListener('fullscreenchange', handleFullscreenChange);
        document.addEventListener('webkitfullscreenchange', handleFullscreenChange);
        document.addEventListener('mozfullscreenchange', handleFullscreenChange);
        document.addEventListener('MSFullscreenChange', handleFullscreenChange);
    }

    function handleFullscreenChange() {
        if (!overlay) return;

        // Get the fullscreen element
        const fullscreenElement = document.fullscreenElement ||
                                document.webkitFullscreenElement ||
                                document.mozFullScreenElement ||
                                document.msFullscreenElement;

        if (fullscreenElement) {
            // Entering fullscreen - move overlay to fullscreen element
            try {
                fullscreenElement.appendChild(overlay);
                // Ensure overlay maintains its properties in fullscreen
                overlay.style.position = 'absolute';
                overlay.style.zIndex = '2147483647';
                
                // If overlay was visible before fullscreen, keep it visible
                if (isVisible) {
                    overlay.style.display = 'block';
                }
            } catch (e) {
                // Fallback: keep in body but ensure maximum z-index
                overlay.style.position = 'fixed';
                overlay.style.zIndex = '2147483647';
                if (isVisible) {
                    overlay.style.display = 'block';
                }
            }
        } else {
            // Exiting fullscreen - move overlay back to body
            if (overlay.parentNode !== document.body) {
                document.body.appendChild(overlay);
            }
            overlay.style.position = 'fixed';
            overlay.style.zIndex = '2147483647';
            
            // Ensure overlay remains visible if it was visible before
            if (isVisible) {
                overlay.style.display = 'block';
            }
        }
    }

    function attachEventListeners() {
        const resizeHandles = overlay.querySelectorAll('.resize-handle');
        const closeButton = overlay.querySelector('.close-button');

        // Mouse down on overlay
        overlay.addEventListener('mousedown', function(e) {
            // Check if clicking on a resize handle
            const resizeHandle = e.target.closest('.resize-handle');
            if (resizeHandle) {
                startResize(e, resizeHandle.dataset.position);
            } else if (e.target !== closeButton && !e.target.closest('.close-button')) {
                startDrag(e);
            }
        });

        // Mouse move
        document.addEventListener('mousemove', function(e) {
            if (isDragging) {
                drag(e);
            } else if (isResizing) {
                resize(e);
            }
        });

        // Mouse up
        document.addEventListener('mouseup', function() {
            isDragging = false;
            isResizing = false;
            currentResizeHandle = null;
            if (overlay) {
                overlay.style.cursor = 'move';
            }
        });

        // Prevent text selection while dragging
        document.addEventListener('selectstart', function(e) {
            if (isDragging || isResizing) {
                e.preventDefault();
            }
        });

        // Scroll wheel opacity adjustment
        overlay.addEventListener('wheel', function(e) {
            e.preventDefault();
            e.stopPropagation();

            const delta = e.deltaY > 0 ? -0.05 : 0.05;
            opacity = Math.max(0.1, Math.min(1.0, opacity + delta));
            updateOpacity();
            
            // Visual feedback - briefly show current opacity
            showOpacityFeedback(Math.round(opacity * 100));
        }, { passive: false });
    }

    function startDrag(e) {
        isDragging = true;
        const rect = overlay.getBoundingClientRect();
        dragOffset.x = e.clientX - rect.left;
        dragOffset.y = e.clientY - rect.top;
        overlay.style.cursor = 'grabbing';
    }

    function drag(e) {
        if (!isDragging) return;

        const x = e.clientX - dragOffset.x;
        const y = e.clientY - dragOffset.y;

        // Keep overlay within viewport bounds
        const maxX = window.innerWidth - overlay.offsetWidth;
        const maxY = window.innerHeight - overlay.offsetHeight;

        const boundedX = Math.max(0, Math.min(x, maxX));
        const boundedY = Math.max(0, Math.min(y, maxY));

        overlay.style.left = boundedX + 'px';
        overlay.style.top = boundedY + 'px';
        overlay.style.transform = 'none';
    }

    function startResize(e, position) {
        e.stopPropagation();
        isResizing = true;
        currentResizeHandle = position;
        
        const rect = overlay.getBoundingClientRect();
        resizeStartSize.width = rect.width;
        resizeStartSize.height = rect.height;
        resizeStartPos.left = rect.left;
        resizeStartPos.top = rect.top;
        resizeStartMouse.x = e.clientX;
        resizeStartMouse.y = e.clientY;
        
        // Set appropriate cursor
        const cursors = {
            'top-left': 'nw-resize',
            'top-right': 'ne-resize',
            'bottom-left': 'sw-resize',
            'bottom-right': 'se-resize'
        };
        overlay.style.cursor = cursors[position];
    }

    function resize(e) {
        if (!isResizing || !currentResizeHandle) return;

        const deltaX = e.clientX - resizeStartMouse.x;
        const deltaY = e.clientY - resizeStartMouse.y;

        let newWidth = resizeStartSize.width;
        let newHeight = resizeStartSize.height;
        let newLeft = resizeStartPos.left;
        let newTop = resizeStartPos.top;

        // Apply resize logic based on which handle is being dragged
        switch (currentResizeHandle) {
            case 'top-left':
                newWidth = Math.max(200, resizeStartSize.width - deltaX);
                newHeight = Math.max(100, resizeStartSize.height - deltaY);
                newLeft = resizeStartPos.left + (resizeStartSize.width - newWidth);
                newTop = resizeStartPos.top + (resizeStartSize.height - newHeight);
                break;
            case 'top-right':
                newWidth = Math.max(200, resizeStartSize.width + deltaX);
                newHeight = Math.max(100, resizeStartSize.height - deltaY);
                newTop = resizeStartPos.top + (resizeStartSize.height - newHeight);
                break;
            case 'bottom-left':
                newWidth = Math.max(200, resizeStartSize.width - deltaX);
                newHeight = Math.max(100, resizeStartSize.height + deltaY);
                newLeft = resizeStartPos.left + (resizeStartSize.width - newWidth);
                break;
            case 'bottom-right':
                newWidth = Math.max(200, resizeStartSize.width + deltaX);
                newHeight = Math.max(100, resizeStartSize.height + deltaY);
                break;
        }

        // Apply bounds checking
        newLeft = Math.max(0, Math.min(newLeft, window.innerWidth - newWidth));
        newTop = Math.max(0, Math.min(newTop, window.innerHeight - newHeight));

        overlay.style.width = newWidth + 'px';
        overlay.style.height = newHeight + 'px';
        overlay.style.left = newLeft + 'px';
        overlay.style.top = newTop + 'px';
        overlay.style.transform = 'none';
    }

    function showOverlay() {
        if (!overlay) createOverlay();
        overlay.style.display = 'block';
        isVisible = true;
        console.log('Subtitle overlay shown');
    }

    function hideOverlay() {
        if (overlay) {
            overlay.style.display = 'none';
            overlay.style.cursor = 'move';
        }
        isVisible = false;
        isDragging = false;
        isResizing = false;
        currentResizeHandle = null;
        
        // Hide resize handles when overlay is hidden
        if (overlay) {
            overlay.querySelectorAll('.resize-handle').forEach(handle => {
                handle.style.opacity = '0';
                handle.style.transform = 'scale(0.8)';
            });
        }
        
        console.log('Subtitle overlay hidden');
    }

    function toggleOverlay() {
        if (isVisible) {
            hideOverlay();
        } else {
            showOverlay();
        }
    }

    // Enhanced keyboard shortcut listener with multiple approaches
    function setupKeyboardListeners() {
        // Method 1: Standard keydown event
        document.addEventListener('keydown', handleKeyDown, true);

        // Method 2: Window-level keydown event (for fullscreen)
        window.addEventListener('keydown', handleKeyDown, true);

        // Method 3: Body-level keydown event
        if (document.body) {
            document.body.addEventListener('keydown', handleKeyDown, true);
        }

        // Method 4: Document element keydown event
        if (document.documentElement) {
            document.documentElement.addEventListener('keydown', handleKeyDown, true);
        }
    }

    function handleKeyDown(e) {
        // Check for Ctrl+B combination
        if (e.ctrlKey && (e.key.toLowerCase() === 'b' || e.keyCode === 66)) {
            e.preventDefault();
            e.stopPropagation();
            e.stopImmediatePropagation();

            console.log('Ctrl+B detected, toggling overlay');
            toggleOverlay();
            return false;
        }
    }

    // Initialize keyboard listeners when DOM is ready
    function initializeKeyboardListeners() {
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', setupKeyboardListeners);
        } else {
            setupKeyboardListeners();
        }

        // Also setup on window load as backup
        window.addEventListener('load', setupKeyboardListeners);

        // Set up a periodic check to re-establish listeners (for dynamic content)
        setInterval(() => {
            setupKeyboardListeners();
        }, 5000);
    }

    // YouTube-specific handling
    function handleYouTubeSpecifics() {
        if (window.location.hostname.includes('youtube.com')) {
            // YouTube often captures keyboard events, so we need to be more aggressive
            const ytPlayer = document.getElementById('movie_player') ||
                           document.querySelector('.html5-video-player') ||
                           document.querySelector('video');

            if (ytPlayer) {
                ytPlayer.addEventListener('keydown', handleKeyDown, true);

                // Also listen on the video element itself
                const video = ytPlayer.querySelector('video') || ytPlayer;
                if (video && video.tagName === 'VIDEO') {
                    video.addEventListener('keydown', handleKeyDown, true);
                }
            }

            // Listen for YouTube's navigation changes
            let lastUrl = location.href;
            new MutationObserver(() => {
                const url = location.href;
                if (url !== lastUrl) {
                    lastUrl = url;
                    setTimeout(setupKeyboardListeners, 1000); // Re-setup after navigation
                }
            }).observe(document, { subtree: true, childList: true });
        }
    }

    // Initialize everything
    function initialize() {
        console.log('Subtitle Overlay Tool loaded. Press Ctrl+B to toggle overlay. Scroll over overlay to adjust opacity.');

        initializeKeyboardListeners();
        handleYouTubeSpecifics();

        // Create overlay initially (hidden)
        createOverlay();
        hideOverlay();
    }

    // Start initialization
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initialize);
    } else {
        initialize();
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址