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