// ==UserScript==
// @license MIT
// @name Subtitle Overlay Tool 字幕遮挡工具
// @namespace http://tampermonkey.net/
// @version 1.3
// @description Create a draggable, resizable overlay to cover subtitles on any website
// @author Fei
// @match *://*/*
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_registerMenuCommand
// ==/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
let minOpacity = 0.05;
// Shortcut configuration
let currentShortcut = {
key: 'b',
ctrlKey: true,
shiftKey: false,
altKey: false
};
// Load saved shortcut configuration
function loadShortcutConfig() {
const saved = GM_getValue('shortcut_config', null);
if (saved) {
try {
currentShortcut = JSON.parse(saved);
} catch (e) {
console.log('Error loading shortcut config, using default');
}
}
}
// Save shortcut configuration
function saveShortcutConfig() {
GM_setValue('shortcut_config', JSON.stringify(currentShortcut));
}
// Get shortcut display string
function getShortcutDisplay() {
let parts = [];
if (currentShortcut.ctrlKey) parts.push('Ctrl');
if (currentShortcut.shiftKey) parts.push('Shift');
if (currentShortcut.altKey) parts.push('Alt');
parts.push(currentShortcut.key.toUpperCase());
return parts.join('+');
}
// Show shortcut configuration dialog
function showShortcutConfig() {
// Remove existing dialog if any
const existingDialog = document.getElementById('shortcut-config-dialog');
if (existingDialog) {
existingDialog.remove();
}
// Create dialog overlay
const dialogOverlay = document.createElement('div');
dialogOverlay.id = 'shortcut-config-dialog';
dialogOverlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
display: flex;
justify-content: center;
align-items: center;
z-index: 2147483648;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
`;
// Create dialog content
const dialog = document.createElement('div');
dialog.style.cssText = `
background: linear-gradient(135deg, rgba(30, 30, 30, 0.95), rgba(20, 20, 20, 0.98));
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 32px;
max-width: 400px;
width: 90%;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
color: white;
`;
dialog.innerHTML = `
<h2 style="margin: 0 0 24px 0; font-size: 20px; font-weight: 600; color: rgba(255, 255, 255, 0.9);">
Customize Keyboard Shortcut
</h2>
<div style="margin-bottom: 24px;">
<p style="margin: 0 0 16px 0; color: rgba(255, 255, 255, 0.7); font-size: 14px;">
Current shortcut: <strong>${getShortcutDisplay()}</strong>
</p>
<div style="margin-bottom: 16px;">
<label style="display: block; margin-bottom: 8px; color: rgba(255, 255, 255, 0.8); font-size: 14px;">
Key:
</label>
<input type="text" id="key-input" maxlength="1" value="${currentShortcut.key}"
style="width: 60px; padding: 8px 12px; border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px; background: rgba(255, 255, 255, 0.1); color: white;
font-size: 16px; text-align: center; text-transform: uppercase;">
</div>
<div style="display: flex; gap: 16px; margin-bottom: 16px;">
<label style="display: flex; align-items: center; gap: 8px; color: rgba(255, 255, 255, 0.8); font-size: 14px; cursor: pointer;">
<input type="checkbox" id="ctrl-check" ${currentShortcut.ctrlKey ? 'checked' : ''}
style="width: 16px; height: 16px;">
Ctrl
</label>
<label style="display: flex; align-items: center; gap: 8px; color: rgba(255, 255, 255, 0.8); font-size: 14px; cursor: pointer;">
<input type="checkbox" id="shift-check" ${currentShortcut.shiftKey ? 'checked' : ''}
style="width: 16px; height: 16px;">
Shift
</label>
<label style="display: flex; align-items: center; gap: 8px; color: rgba(255, 255, 255, 0.8); font-size: 14px; cursor: pointer;">
<input type="checkbox" id="alt-check" ${currentShortcut.altKey ? 'checked' : ''}
style="width: 16px; height: 16px;">
Alt
</label>
</div>
<div id="preview-shortcut" style="padding: 12px; background: rgba(99, 102, 241, 0.2);
border: 1px solid rgba(99, 102, 241, 0.3); border-radius: 8px; text-align: center;
color: rgba(255, 255, 255, 0.9); font-weight: 500; font-size: 14px;">
Preview: ${getShortcutDisplay()}
</div>
</div>
<div style="display: flex; gap: 12px; justify-content: flex-end;">
<button id="cancel-btn" style="padding: 10px 20px; border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 8px; background: rgba(255, 255, 255, 0.1); color: rgba(255, 255, 255, 0.8);
cursor: pointer; font-size: 14px; transition: all 0.2s;">
Cancel
</button>
<button id="save-btn" style="padding: 10px 20px; border: none; border-radius: 8px;
background: linear-gradient(135deg, rgba(99, 102, 241, 0.8), rgba(168, 85, 247, 0.8));
color: white; cursor: pointer; font-size: 14px; font-weight: 500; transition: all 0.2s;">
Save
</button>
</div>
`;
dialogOverlay.appendChild(dialog);
document.body.appendChild(dialogOverlay);
// Add event listeners for dialog
const keyInput = dialog.querySelector('#key-input');
const ctrlCheck = dialog.querySelector('#ctrl-check');
const shiftCheck = dialog.querySelector('#shift-check');
const altCheck = dialog.querySelector('#alt-check');
const previewDiv = dialog.querySelector('#preview-shortcut');
const cancelBtn = dialog.querySelector('#cancel-btn');
const saveBtn = dialog.querySelector('#save-btn');
// Update preview function
function updatePreview() {
const tempShortcut = {
key: keyInput.value.toLowerCase() || 'b',
ctrlKey: ctrlCheck.checked,
shiftKey: shiftCheck.checked,
altKey: altCheck.checked
};
let parts = [];
if (tempShortcut.ctrlKey) parts.push('Ctrl');
if (tempShortcut.shiftKey) parts.push('Shift');
if (tempShortcut.altKey) parts.push('Alt');
parts.push(tempShortcut.key.toUpperCase());
previewDiv.textContent = 'Preview: ' + parts.join('+');
}
// Key input validation
keyInput.addEventListener('input', (e) => {
const value = e.target.value.toLowerCase();
if (value && /^[a-z0-9]$/.test(value)) {
e.target.value = value;
updatePreview();
} else if (value) {
e.target.value = '';
}
});
keyInput.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (key.length === 1 && /^[a-z0-9]$/.test(key)) {
e.preventDefault();
keyInput.value = key;
updatePreview();
} else if (key === 'backspace') {
e.preventDefault();
keyInput.value = '';
updatePreview();
}
});
// Checkbox listeners
[ctrlCheck, shiftCheck, altCheck].forEach(checkbox => {
checkbox.addEventListener('change', updatePreview);
});
// Button listeners
cancelBtn.addEventListener('click', () => {
dialogOverlay.remove();
});
saveBtn.addEventListener('click', () => {
const newKey = keyInput.value.toLowerCase() || 'b';
// Validate that at least one modifier or a single key is selected
if (!ctrlCheck.checked && !shiftCheck.checked && !altCheck.checked && newKey.length === 1) {
// Single key shortcut is fine
} else if (ctrlCheck.checked || shiftCheck.checked || altCheck.checked) {
// Modifier + key is fine
} else {
alert('Please select at least one modifier key (Ctrl, Shift, or Alt) or use a single key.');
return;
}
currentShortcut = {
key: newKey,
ctrlKey: ctrlCheck.checked,
shiftKey: shiftCheck.checked,
altKey: altCheck.checked
};
saveShortcutConfig();
updateOverlayInstructions();
dialogOverlay.remove();
// Show confirmation
showNotification(`Shortcut changed to: ${getShortcutDisplay()}`);
});
// Close dialog when clicking outside
dialogOverlay.addEventListener('click', (e) => {
if (e.target === dialogOverlay) {
dialogOverlay.remove();
}
});
// Focus the key input
setTimeout(() => keyInput.focus(), 100);
}
// Show notification
function showNotification(message) {
const notification = document.createElement('div');
notification.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 16px 24px;
background: linear-gradient(135deg, rgba(34, 197, 94, 0.9), rgba(16, 185, 129, 0.9));
color: white;
border-radius: 12px;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
font-size: 14px;
font-weight: 500;
z-index: 2147483648;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transform: translateX(100%);
transition: transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
`;
notification.textContent = message;
document.body.appendChild(notification);
// Animate in
setTimeout(() => {
notification.style.transform = 'translateX(0)';
}, 100);
// Remove after delay
setTimeout(() => {
notification.style.transform = 'translateX(100%)';
setTimeout(() => {
if (notification.parentNode) {
notification.remove();
}
}, 300);
}, 3000);
}
// Update overlay instructions with current shortcut
function updateOverlayInstructions() {
if (overlay) {
const instructionsDiv = overlay.querySelector('.instructions');
if (instructionsDiv) {
instructionsDiv.innerHTML = `
<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">⌨️ ${getShortcutDisplay()} to toggle</span>
`;
}
}
}
// Register Tampermonkey menu command
function registerMenuCommands() {
GM_registerMenuCommand('⚙️ Configure Shortcut', showShortcutConfig);
GM_registerMenuCommand(`🔄 Toggle Overlay (${getShortcutDisplay()})`, toggleOverlay);
}
// 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);
});
// Create settings button
const settingsButton = document.createElement('div');
settingsButton.innerHTML = '⚙️';
settingsButton.className = 'settings-button';
settingsButton.style.cssText = `
position: absolute;
top: 12px;
left: 12px;
width: 32px;
height: 32px;
color: rgba(255, 255, 255, 0.7);
font-size: 14px;
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;
`;
settingsButton.onmouseover = () => {
settingsButton.style.opacity = '1';
settingsButton.style.background = 'rgba(99, 102, 241, 0.2)';
settingsButton.style.color = 'rgba(255, 255, 255, 0.9)';
settingsButton.style.transform = 'scale(1.05)';
};
settingsButton.onmouseout = () => {
settingsButton.style.opacity = '0.7';
settingsButton.style.background = 'rgba(255, 255, 255, 0.08)';
settingsButton.style.color = 'rgba(255, 255, 255, 0.7)';
settingsButton.style.transform = 'scale(1)';
};
settingsButton.onclick = (e) => {
e.stopPropagation();
showShortcutConfig();
};
overlay.appendChild(settingsButton);
// 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">⌨️ ${getShortcutDisplay()} 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; /* Initially hidden */
transition: opacity 0.3s ease-in-out; /* Smooth transition */
`;
// 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();
// Show resize handles and info text on hover
overlay.addEventListener('mouseenter', () => {
overlay.querySelectorAll('.resize-handle').forEach(handle => {
handle.style.opacity = '1';
handle.style.transform = 'scale(1)';
});
const infoTextElement = overlay.querySelector('.info-text');
if (infoTextElement) {
infoTextElement.style.opacity = '0.8'; // Make text visible on hover
}
});
overlay.addEventListener('mouseleave', () => {
if (!isResizing && !isDragging) {
overlay.querySelectorAll('.resize-handle').forEach(handle => {
handle.style.opacity = '0';
handle.style.transform = 'scale(0.8)';
});
const infoTextElement = overlay.querySelector('.info-text');
if (infoTextElement) {
infoTextElement.style.opacity = '0'; // Hide text when mouse leaves
}
}
});
}
function updateOpacity() {
if (overlay) {
// Create a more sophisticated opacity system with glassmorphism
const alpha = Math.max(minOpacity, opacity);
const gradientAlpha1 = Math.max(minOpacity, alpha * 0.95);
const gradientAlpha2 = Math.max(minOpacity, 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(minOpacity, 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: 50px;
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');
const settingsButton = overlay.querySelector('.settings-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') &&
e.target !== settingsButton && !e.target.closest('.settings-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(minOpacity, 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 and info text when overlay is hidden
if (overlay) {
overlay.querySelectorAll('.resize-handle').forEach(handle => {
handle.style.opacity = '0';
handle.style.transform = 'scale(0.8)';
});
const infoTextElement = overlay.querySelector('.info-text');
if (infoTextElement) {
infoTextElement.style.opacity = '0'; // Hide text when overlay is hidden
}
}
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 the configured shortcut combination
const keyMatch = e.key.toLowerCase() === currentShortcut.key ||
e.keyCode === currentShortcut.key.toUpperCase().charCodeAt(0);
const ctrlMatch = e.ctrlKey === currentShortcut.ctrlKey;
const shiftMatch = e.shiftKey === currentShortcut.shiftKey;
const altMatch = e.altKey === currentShortcut.altKey;
if (keyMatch && ctrlMatch && shiftMatch && altMatch) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
console.log(`${getShortcutDisplay()} 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 ${getShortcutDisplay()} to toggle overlay. Scroll over overlay to adjust opacity.`);
// Load saved configuration
loadShortcutConfig();
// Register menu commands
registerMenuCommands();
initializeKeyboardListeners();
handleYouTubeSpecifics();
// Create overlay initially (hidden)
createOverlay();
hideOverlay();
}
// Start initialization
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initialize);
} else {
initialize();
}
})();