A video speed controller, frame-by-frame navigation, web fullscreen, and video snapshot tool.
// ==UserScript==
// @name Wayne's Video Controller & Snapshot
// @namespace http://tampermonkey.net/
// @version 1.1.2
// @description A video speed controller, frame-by-frame navigation, web fullscreen, and video snapshot tool.
// @author Wayne
// @match *://*/*
// @grant GM_setValue
// @grant GM_getValue
// @grant GM_addStyle
// @grant GM_registerMenuCommand
// @run-at document-end
// @license MIT
// ==/UserScript==
(function () {
'use strict';
// Inject the styling rules for the overlay, modal, toasts, and custom controls
const CSS_STYLE = `
/* waynes-video-controller styles - Glassmorphic Video Controller & Snapshots */
@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;700&display=swap');
/* Namespace everything to avoid conflict with hosting page styles */
.aura-controller-overlay {
display: flex;
align-items: center;
gap: 8px;
position: absolute;
top: 15px;
left: 15px;
z-index: 2147483647; /* Ensure it is on top of full-screen video */
background: rgba(15, 15, 24, 0.65);
backdrop-filter: blur(12px) saturate(180%);
-webkit-backdrop-filter: blur(12px) saturate(180%);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 12px;
padding: 6px 12px;
box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37),
0 0 0 1px rgba(255, 255, 255, 0.05) inset;
font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
font-size: 14px;
color: #ffffff;
user-select: none;
pointer-events: auto;
opacity: 0;
transition: opacity 1s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s ease;
transform: translateY(0);
}
/* Hovering over the overlay makes it visible */
.aura-controller-overlay:hover {
opacity: 0.98;
box-shadow: 0 12px 40px 0 rgba(0, 0, 0, 0.5),
0 0 10px 0 rgba(99, 102, 241, 0.2);
transform: translateY(-2px);
transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s ease;
}
/* Show when video is paused or when hovered */
.aura-controller-overlay.aura-visible {
opacity: 0.98;
transition: opacity 0.15s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.3s cubic-bezier(0.4, 0, 0.2, 1),
box-shadow 0.3s ease;
}
/* Elements */
.aura-drag-handle {
cursor: grab;
display: flex;
align-items: center;
justify-content: center;
color: rgba(255, 255, 255, 0.35);
padding: 2px;
margin-right: 2px;
transition: color 0.2s ease;
}
.aura-drag-handle:hover {
color: rgba(255, 255, 255, 0.8);
}
.aura-drag-handle:active {
cursor: grabbing;
}
.aura-speed-display {
font-weight: 600;
min-width: 52px;
text-align: center;
color: #a5b4fc; /* soft indigo-purple */
font-variant-numeric: tabular-nums;
letter-spacing: 0.5px;
background: rgba(99, 102, 241, 0.12);
padding: 4px 8px;
border-radius: 6px;
border: 1px solid rgba(99, 102, 241, 0.2);
transition: all 0.2s ease;
cursor: pointer;
}
.aura-speed-display:hover {
background: rgba(99, 102, 241, 0.25);
border-color: rgba(99, 102, 241, 0.4);
color: #c7d2fe;
}
.aura-btn {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.05);
color: rgba(255, 255, 255, 0.8);
cursor: pointer;
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
padding: 0;
}
.aura-btn:hover {
background: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.2);
color: #ffffff;
transform: scale(1.08);
}
.aura-btn:active {
transform: scale(0.92);
background: rgba(99, 102, 241, 0.4);
border-color: #6366f1;
}
/* Specific button glows */
.aura-btn-copy-snap {
background: rgba(16, 185, 129, 0.1);
border-color: rgba(16, 185, 129, 0.2);
color: #a7f3d0;
}
.aura-btn-copy-snap:hover {
background: rgba(16, 185, 129, 0.25);
border-color: rgba(16, 185, 129, 0.4);
color: #ffffff;
box-shadow: 0 0 10px rgba(16, 185, 129, 0.3);
}
.aura-btn-download-snap {
background: rgba(59, 130, 246, 0.1);
border-color: rgba(59, 130, 246, 0.2);
color: #bfdbfe;
}
.aura-btn-download-snap:hover {
background: rgba(59, 130, 246, 0.25);
border-color: rgba(59, 130, 246, 0.4);
color: #ffffff;
box-shadow: 0 0 10px rgba(59, 130, 246, 0.3);
}
.aura-btn-web-fs {
background: rgba(236, 72, 153, 0.1);
border-color: rgba(236, 72, 153, 0.2);
color: #fbcfe8;
}
.aura-btn-web-fs:hover {
background: rgba(236, 72, 153, 0.25);
border-color: rgba(236, 72, 153, 0.4);
color: #ffffff;
box-shadow: 0 0 10px rgba(236, 72, 153, 0.3);
}
.aura-btn-close {
background: rgba(239, 68, 68, 0.1);
border-color: rgba(239, 68, 68, 0.2);
color: #fca5a5;
width: 22px;
height: 22px;
border-radius: 6px;
margin-left: 4px;
}
.aura-btn-close:hover {
background: rgba(239, 68, 68, 0.3);
border-color: rgba(239, 68, 68, 0.5);
color: #ffffff;
}
/* Divider inside controller */
.aura-divider {
width: 1px;
height: 18px;
background: rgba(255, 255, 255, 0.12);
margin: 0 2px;
}
/* Tooltip styles */
.aura-btn[data-tooltip] {
position: relative;
}
.aura-btn[data-tooltip]::after {
content: attr(data-tooltip);
position: absolute;
bottom: -32px;
left: 50%;
transform: translateX(-50%) scale(0.8);
background: rgba(10, 10, 15, 0.9);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 4px 8px;
border-radius: 4px;
font-size: 10px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: all 0.15s ease;
z-index: 10000;
color: #ffffff;
}
.aura-btn[data-tooltip]:hover::after {
opacity: 1;
transform: translateX(-50%) scale(1);
}
/* Toast Notifications */
.aura-toast-container {
position: absolute;
top: 15px;
right: 15px;
z-index: 2147483647;
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: none;
font-family: 'Outfit', sans-serif;
}
.aura-toast {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
background: rgba(20, 20, 30, 0.85);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border-left: 4px solid #6366f1;
border-radius: 8px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.35);
color: #ffffff;
font-size: 13px;
pointer-events: auto;
animation: auraSlideIn 0.3s cubic-bezier(0.16, 1, 0.3, 1) forwards;
max-width: 320px;
transition: all 0.3s ease;
}
.aura-toast-success {
border-left-color: #10b981;
}
.aura-toast-error {
border-left-color: #ef4444;
}
.aura-toast-info {
border-left-color: #3b82f6;
}
@keyframes auraSlideIn {
from {
opacity: 0;
transform: translateX(30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.aura-toast.aura-fade-out {
opacity: 0;
transform: translateY(-10px) scale(0.9);
}
/* Modal - Screenshot Preview */
.aura-modal-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(7, 8, 14, 0.75);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
z-index: 2147483646;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: opacity 0.3s cubic-bezier(0.16, 1, 0.3, 1);
font-family: 'Outfit', sans-serif;
}
.aura-modal-backdrop.aura-show {
opacity: 1;
}
.aura-modal-content {
background: rgba(22, 22, 33, 0.85);
border: 1px solid rgba(255, 255, 255, 0.08);
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5),
0 0 40px 0 rgba(99, 102, 241, 0.15);
border-radius: 16px;
width: 90%;
max-width: 720px;
display: flex;
flex-direction: column;
overflow: hidden;
transform: scale(0.92);
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.aura-modal-backdrop.aura-show .aura-modal-content {
transform: scale(1);
}
.aura-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(15, 15, 24, 0.5);
}
.aura-modal-title {
font-size: 18px;
font-weight: 600;
color: #ffffff;
background: linear-gradient(135deg, #a5b4fc, #818cf8);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.aura-modal-preview-area {
padding: 24px;
display: flex;
justify-content: center;
align-items: center;
background: rgba(10, 10, 15, 0.4);
max-height: 400px;
overflow: auto;
}
.aura-modal-img {
max-width: 100%;
max-height: 350px;
border-radius: 8px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.1);
object-fit: contain;
}
.aura-modal-footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 24px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
background: rgba(15, 15, 24, 0.5);
}
.aura-modal-info {
font-size: 12px;
color: rgba(255, 255, 255, 0.45);
}
.aura-modal-actions {
display: flex;
gap: 10px;
}
.aura-btn-primary, .aura-btn-secondary {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
font-size: 13px;
font-weight: 500;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
font-family: inherit;
}
.aura-btn-primary {
background: linear-gradient(135deg, #6366f1, #4f46e5);
color: #ffffff;
border: none;
box-shadow: 0 4px 12px rgba(79, 70, 229, 0.3);
}
.aura-btn-primary:hover {
background: linear-gradient(135deg, #818cf8, #6366f1);
box-shadow: 0 6px 16px rgba(79, 70, 229, 0.4);
transform: translateY(-1px);
}
.aura-btn-primary:active {
transform: translateY(1px);
}
.aura-btn-secondary {
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.85);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.aura-btn-secondary:hover {
background: rgba(255, 255, 255, 0.12);
color: #ffffff;
border-color: rgba(255, 255, 255, 0.2);
}
.aura-btn-secondary:active {
background: rgba(255, 255, 255, 0.03);
}
/* Custom scrollbars for options/modal components */
.aura-modal-preview-area::-webkit-scrollbar {
width: 8px;
height: 8px;
}
.aura-modal-preview-area::-webkit-scrollbar-track {
background: rgba(0,0,0,0.1);
}
.aura-modal-preview-area::-webkit-scrollbar-thumb {
background: rgba(255,255,255,0.1);
border-radius: 4px;
}
.aura-modal-preview-area::-webkit-scrollbar-thumb:hover {
background: rgba(255,255,255,0.25);
}
/* Web Fullscreen modes */
body.aura-web-fullscreen-active {
overflow: hidden !important;
}
.aura-web-fullscreen-parent {
/* Bypass stacking context limitations of overflow and transforms */
overflow: visible !important;
transform: none !important;
webkit-transform: none !important;
perspective: none !important;
contain: none !important;
filter: none !important;
position: relative !important;
z-index: 2147483646 !important;
}
.aura-web-fullscreen-element {
position: fixed !important;
top: 0 !important;
left: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 2147483647 !important;
background: #000000 !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
}
.aura-web-fullscreen-element video {
width: 100% !important;
height: 100% !important;
max-width: 100vw !important;
max-height: 100vh !important;
object-fit: contain !important;
}
/* active Web Fullscreen button styling */
.aura-web-fullscreen-element .aura-btn-web-fs {
background: rgba(236, 72, 153, 0.35) !important;
border-color: #ec4899 !important;
color: #ffffff !important;
box-shadow: 0 0 10px rgba(236, 72, 153, 0.4);
}
`;
// Inject styling into page
if (typeof GM_addStyle !== 'undefined') {
GM_addStyle(CSS_STYLE);
} else {
const style = document.createElement('style');
style.textContent = CSS_STYLE;
document.head.appendChild(style);
}
// --- Browser Extension Mocking for UserScript context ---
const storageMock = {
sync: {
get: (keys, callback) => {
let result = {};
const keyList = Array.isArray(keys) ? keys : [keys];
keyList.forEach(k => {
try {
const val = GM_getValue(k);
if (val !== undefined && val !== null) {
result[k] = JSON.parse(val);
}
} catch (e) {
console.error('Error getting storage key', k, e);
}
});
callback(result);
},
set: (items, callback) => {
Object.keys(items).forEach(k => {
GM_setValue(k, JSON.stringify(items[k]));
});
if (callback) callback();
// Trigger changes manually for the script's storage listener
if (storageMock.onChanged._listeners) {
const changes = {};
Object.keys(items).forEach(k => {
changes[k] = { newValue: items[k] };
});
storageMock.onChanged._listeners.forEach(cb => {
try { cb(changes, 'sync'); } catch (e) { }
});
}
}
},
onChanged: {
_listeners: [],
addListener: (callback) => {
storageMock.onChanged._listeners.push(callback);
}
}
};
const browserAPI = {
storage: storageMock,
runtime: {
sendMessage: (message, callback) => {
if (message.action === 'downloadScreenshot') {
// Trigger the fallback direct download in content script context since we don't have a background script
if (callback) callback({ success: false, error: 'UserScript Environment Direct Download Fallback' });
}
},
onMessage: {
addListener: (callback) => {
// Expose a function on window for easy external triggering/testing if necessary
window.__aura_takeTabScreenshot = () => {
callback({ action: 'takeTabScreenshot' }, {}, (response) => {
console.log('UserScript screenshot response:', response);
});
};
}
}
}
};
// --- Original Content Script Logic ---
// Default settings (updated with user's customized hotkeys)
let settings = {
speedStep: 0.25,
rewindStep: 5, // Fast Rewind 5s
advanceStep: 5, // Fast Forward 5s
rewindStepLong: 30, // Long Rewind 30s
advanceStepLong: 30, // Long Forward 30s
snapshotFormat: 'png',
autoShowOverlay: true,
rememberSpeed: false,
lastSpeed: 1.0,
hotkeys: {
decreaseSpeed: '-',
increaseSpeed: '+',
resetSpeed: '0',
rewind5s: '4',
advance5s: '6',
rewind30s: '7',
advance30s: '9',
prevFrame: '/',
nextFrame: '*',
copySnapshot: '5', // Map to 5 key
downloadSnapshot: '', // Map to empty string
toggleOverlay: '1', // Toggle UI overlay
toggleWebFullscreen: '3' // Web Fullscreen
}
};
// State management
let activeVideo = null;
let hoveredVideo = null;
let overlayHiddenGlobally = false;
let preResetSpeed = 1.0;
// Load settings from storage
function loadSettings() {
browserAPI.storage.sync.get(['settings'], (result) => {
if (result.settings) {
// Deep merge hotkeys and load others
settings = {
...settings,
...result.settings,
hotkeys: { ...settings.hotkeys, ...(result.settings.hotkeys || {}) }
};
}
// Initialize or update existing controllers
initializeAllVideos();
});
}
// Monitor storage changes to dynamically apply settings
browserAPI.storage.onChanged.addListener((changes, namespace) => {
if (changes.settings) {
settings = {
...settings,
...changes.settings.newValue,
hotkeys: { ...settings.hotkeys, ...(changes.settings.newValue.hotkeys || {}) }
};
// Update UI of all active controllers
document.querySelectorAll('.aura-controller-overlay').forEach(overlay => {
const video = overlay.videoElement;
if (video) {
updateOverlaySpeed(overlay, video.playbackRate);
// Apply overlay visibility settings
if (!settings.autoShowOverlay) {
overlay.style.display = 'none';
} else if (!overlayHiddenGlobally) {
overlay.style.display = 'flex';
}
}
});
}
});
// SVG Icons
const ICONS = {
drag: `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="currentColor"><path d="M4 2a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM4 6a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zM4 10a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0zm4 0a1 1 0 1 1-2 0 1 1 0 0 1 2 0z"/></svg>`,
rewindLong: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 17l-5-5 5-5M18 17l-5-5 5-5"/></svg>`,
rewind: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 19l-7-7 7-7"/></svg>`,
minus: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
plus: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>`,
forward: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5l7 7-7 7"/></svg>`,
forwardLong: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M13 17l5-5-5-5M6 17l5-5-5-5"/></svg>`,
prevFrame: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M19 20L9 12l10-8v16zM5 19V5"/></svg>`,
nextFrame: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 4l10 8-10 8V4zM19 5v14"/></svg>`,
camera: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path><circle cx="12" cy="13" r="4"></circle></svg>`,
download: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3"/></svg>`,
webFullscreen: `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/></svg>`,
close: `<svg xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`
};
// Initialize controllers for videos in the document
function initializeAllVideos() {
const videos = document.getElementsByTagName('video');
for (let video of videos) {
setupVideo(video);
}
}
// Setup single video controller
function setupVideo(video) {
if (video.dataset.auraInitialized) return;
video.dataset.auraInitialized = "true";
// Set initial custom speed if settings specify
if (settings.rememberSpeed) {
video.playbackRate = settings.lastSpeed || 1.0;
}
// Try to enable CORS on videos if they are cross-origin, so screenshot works
if (video.src && !video.src.startsWith('blob:') && !video.src.startsWith(window.location.origin)) {
if (!video.hasAttribute('crossorigin')) {
// Only set crossorigin anonymous. Warning: some servers might block if they don't return CORS headers.
}
}
// Create the overlay DOM
const overlay = document.createElement('div');
overlay.className = 'aura-controller-overlay';
// Bind video element to overlay for simple references
overlay.videoElement = video;
// Apply visibility setting
if (!settings.autoShowOverlay || overlayHiddenGlobally) {
overlay.style.display = 'none';
}
// HTML Structure
overlay.innerHTML = `
<div class="aura-drag-handle" title="Drag to move">${ICONS.drag}</div>
<button class="aura-btn aura-btn-rewind-long" data-tooltip="Rewind 30s (7)">${ICONS.rewindLong}</button>
<button class="aura-btn aura-btn-rewind" data-tooltip="Rewind 5s (4)">${ICONS.rewind}</button>
<button class="aura-btn aura-btn-prev-frame" data-tooltip="Prev Frame (/)">${ICONS.prevFrame}</button>
<button class="aura-btn aura-btn-minus" data-tooltip="Slower (-)">${ICONS.minus}</button>
<div class="aura-speed-display" data-tooltip="Reset Speed (0)">${video.playbackRate.toFixed(2)}x</div>
<button class="aura-btn aura-btn-plus" data-tooltip="Faster (+)">${ICONS.plus}</button>
<button class="aura-btn aura-btn-next-frame" data-tooltip="Next Frame (*)">${ICONS.nextFrame}</button>
<button class="aura-btn aura-btn-forward" data-tooltip="Forward 5s (6)">${ICONS.forward}</button>
<button class="aura-btn aura-btn-forward-long" data-tooltip="Forward 30s (9)">${ICONS.forwardLong}</button>
<div class="aura-divider"></div>
<button class="aura-btn aura-btn-copy-snap" data-tooltip="Copy Frame (5)">${ICONS.camera}</button>
<button class="aura-btn aura-btn-download-snap" data-tooltip="Save Frame">${ICONS.download}</button>
<button class="aura-btn aura-btn-web-fs" data-tooltip="Web Fullscreen (3)">${ICONS.webFullscreen}</button>
<button class="aura-btn aura-btn-close" title="Hide Controller (1)">${ICONS.close}</button>
`;
// Position overlay inside video container
let container = video.parentElement;
if (!container) return;
// Make parent relative if it is static, to keep overlay scoped
const containerStyle = window.getComputedStyle(container);
if (containerStyle.position === 'static') {
container.style.position = 'relative';
}
container.appendChild(overlay);
// Event listeners for UI updates
video.addEventListener('ratechange', () => {
updateOverlaySpeed(overlay, video.playbackRate);
showOverlayTemporarily();
if (settings.rememberSpeed) {
// Save current speed to settings
settings.lastSpeed = video.playbackRate;
browserAPI.storage.sync.set({ settings });
}
});
// Make the overlay float visibly when video state changes
let fadeOutTimeout;
const showOverlayTemporarily = () => {
if (overlayHiddenGlobally || !settings.autoShowOverlay) return;
overlay.classList.add('aura-visible');
clearTimeout(fadeOutTimeout);
fadeOutTimeout = setTimeout(() => {
overlay.classList.remove('aura-visible');
}, 2000);
};
video.addEventListener('play', showOverlayTemporarily);
video.addEventListener('pause', () => {
if (!overlayHiddenGlobally && settings.autoShowOverlay) {
overlay.classList.add('aura-visible');
}
});
// Hover trackers to set active video
container.addEventListener('mouseenter', () => {
hoveredVideo = video;
activeVideo = video;
if (!overlayHiddenGlobally && settings.autoShowOverlay) {
overlay.classList.add('aura-visible');
}
});
container.addEventListener('mouseleave', () => {
hoveredVideo = null;
if (video.paused) return; // Keep visible if paused
overlay.classList.remove('aura-visible');
});
// Wire up buttons logic
const btnRewindLong = overlay.querySelector('.aura-btn-rewind-long');
const btnRewind = overlay.querySelector('.aura-btn-rewind');
const btnPrevFrame = overlay.querySelector('.aura-btn-prev-frame');
const btnMinus = overlay.querySelector('.aura-btn-minus');
const btnPlus = overlay.querySelector('.aura-btn-plus');
const btnNextFrame = overlay.querySelector('.aura-btn-next-frame');
const btnForward = overlay.querySelector('.aura-btn-forward');
const btnForwardLong = overlay.querySelector('.aura-btn-forward-long');
const btnCopySnap = overlay.querySelector('.aura-btn-copy-snap');
const btnDownloadSnap = overlay.querySelector('.aura-btn-download-snap');
const btnWebFs = overlay.querySelector('.aura-btn-web-fs');
const btnClose = overlay.querySelector('.aura-btn-close');
const speedDisplay = overlay.querySelector('.aura-speed-display');
btnRewindLong.addEventListener('click', (e) => { e.stopPropagation(); adjustTime(video, -settings.rewindStepLong); });
btnRewind.addEventListener('click', (e) => { e.stopPropagation(); adjustTime(video, -settings.rewindStep); });
btnPrevFrame.addEventListener('click', (e) => { e.stopPropagation(); stepFrame(video, -1); });
btnMinus.addEventListener('click', (e) => { e.stopPropagation(); adjustSpeed(video, -settings.speedStep); });
btnPlus.addEventListener('click', (e) => { e.stopPropagation(); adjustSpeed(video, settings.speedStep); });
btnNextFrame.addEventListener('click', (e) => { e.stopPropagation(); stepFrame(video, 1); });
btnForward.addEventListener('click', (e) => { e.stopPropagation(); adjustTime(video, settings.advanceStep); });
btnForwardLong.addEventListener('click', (e) => { e.stopPropagation(); adjustTime(video, settings.advanceStepLong); });
speedDisplay.addEventListener('click', (e) => {
e.stopPropagation();
toggleResetSpeed(video);
});
btnCopySnap.addEventListener('click', (e) => {
e.stopPropagation();
captureScreenshot(video, 'copy');
});
btnDownloadSnap.addEventListener('click', (e) => {
e.stopPropagation();
captureScreenshot(video, 'download');
});
btnWebFs.addEventListener('click', (e) => {
e.stopPropagation();
toggleWebFullscreen(video);
});
btnClose.addEventListener('click', (e) => {
e.stopPropagation();
overlay.style.display = 'none';
showToast('Controller hidden. Press "' + settings.hotkeys.toggleOverlay.toUpperCase() + '" to show again.', 'info');
});
// Dragging logic
const dragHandle = overlay.querySelector('.aura-drag-handle');
setupDragging(overlay, dragHandle, container);
}
// Update speed text display
function updateOverlaySpeed(overlay, rate) {
const display = overlay.querySelector('.aura-speed-display');
if (display) {
display.textContent = rate.toFixed(2) + 'x';
}
}
// Change video time
function adjustTime(video, amount) {
video.currentTime = Math.max(0, Math.min(video.duration || 0, video.currentTime + amount));
showToast(`${amount > 0 ? '+' : ''}${amount}s (${formatTime(video.currentTime)})`, 'info');
}
// Frame stepping (Prev / Next frame)
function stepFrame(video, direction) {
if (!video) return;
if (!video.paused) {
video.pause();
}
const fps = 30; // Fallback frame rate
const frameTime = 1 / fps;
video.currentTime = Math.max(0, Math.min(video.duration || 0, video.currentTime + (direction * frameTime)));
showToast(`${direction > 0 ? 'Next' : 'Prev'} frame (${formatTime(video.currentTime)})`, 'info');
}
// Change video speed
function adjustSpeed(video, amount) {
if (!video) return;
let rate = parseFloat((video.playbackRate + amount).toFixed(2));
// Firefox/Chrome limits playback speed between 0.0625 and 16
rate = Math.max(0.0625, Math.min(16, rate));
video.playbackRate = rate;
// Find overlay and update directly to be safe
const overlay = video.parentElement ? video.parentElement.querySelector('.aura-controller-overlay') : null;
if (overlay) {
updateOverlaySpeed(overlay, rate);
}
showToast(`Speed: ${rate.toFixed(2)}x`, 'success');
}
// Toggle speed reset
function toggleResetSpeed(video) {
if (!video) return;
let targetSpeed = 1.0;
if (Math.abs(video.playbackRate - 1.0) > 0.01) {
preResetSpeed = video.playbackRate;
targetSpeed = 1.0;
} else {
targetSpeed = preResetSpeed;
}
video.playbackRate = targetSpeed;
// Find overlay and update directly to be safe
const overlay = video.parentElement ? video.parentElement.querySelector('.aura-controller-overlay') : null;
if (overlay) {
updateOverlaySpeed(overlay, targetSpeed);
}
showToast(`Speed: ${targetSpeed.toFixed(2)}x`, 'success');
}
// Web Fullscreen toggle
function toggleWebFullscreen(video) {
if (!video) return;
const container = video.parentElement;
if (!container) return;
if (container.classList.contains('aura-web-fullscreen-element')) {
// Exit web fullscreen
container.classList.remove('aura-web-fullscreen-element');
document.body.classList.remove('aura-web-fullscreen-active');
// Remove class from all parent elements
let parent = container.parentElement;
while (parent && parent !== document.documentElement) {
parent.classList.remove('aura-web-fullscreen-parent');
parent = parent.parentElement;
}
showToast('Exit Web Fullscreen', 'info');
} else {
// Enter web fullscreen
container.classList.add('aura-web-fullscreen-element');
document.body.classList.add('aura-web-fullscreen-active');
// Add class to all parent elements to override transforms and clipping
let parent = container.parentElement;
while (parent && parent !== document.documentElement) {
parent.classList.add('aura-web-fullscreen-parent');
parent = parent.parentElement;
}
showToast('Enter Web Fullscreen', 'info');
}
}
// Draggable overlay utility
function setupDragging(overlay, dragHandle, container) {
let isDragging = false;
let startX, startY;
let initialLeft, initialTop;
dragHandle.addEventListener('mousedown', (e) => {
e.preventDefault();
e.stopPropagation();
isDragging = true;
startX = e.clientX;
startY = e.clientY;
// Read current offsets
const rect = overlay.getBoundingClientRect();
const parentRect = container.getBoundingClientRect();
initialLeft = rect.left - parentRect.left;
initialTop = rect.top - parentRect.top;
overlay.style.transition = 'none'; // Disable transition while dragging
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
function onMouseMove(e) {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
let newLeft = initialLeft + dx;
let newTop = initialTop + dy;
// Keep inside container boundaries
const containerRect = container.getBoundingClientRect();
const overlayRect = overlay.getBoundingClientRect();
const maxLeft = containerRect.width - overlayRect.width;
const maxTop = containerRect.height - overlayRect.height;
newLeft = Math.max(0, Math.min(maxLeft, newLeft));
newTop = Math.max(0, Math.min(maxTop, newTop));
overlay.style.left = newLeft + 'px';
overlay.style.top = newTop + 'px';
}
function onMouseUp() {
isDragging = false;
overlay.style.transition = ''; // Restore transitions
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
}
}
// Hotkey handlers
window.addEventListener('keydown', (e) => {
// Ignore keyboard shortcuts if writing in text fields
const activeEl = document.activeElement;
if (
activeEl && (
activeEl.tagName === 'INPUT' ||
activeEl.tagName === 'TEXTAREA' ||
activeEl.isContentEditable ||
activeEl.getAttribute('role') === 'textbox'
)
) {
return;
}
// Get key in lowercase
const key = e.key.toLowerCase();
// Prevent non-numpad numeric/math keys from triggering (location 3 is Numpad)
const isNumpadCandidate = /^[0-9+\-*/=]$/.test(key);
if (isNumpadCandidate && e.location !== 3) {
return;
}
// Find matching video to control
const targetVideo = getActiveVideo();
if (!targetVideo) return;
// Check if the user is actively watching/interacting with the target video
const isPlaying = !targetVideo.paused && !targetVideo.ended;
const isHovered = (hoveredVideo === targetVideo);
const isWebFullscreen = targetVideo.parentElement && targetVideo.parentElement.classList.contains('aura-web-fullscreen-element');
const isBrowserFullscreen = document.fullscreenElement === targetVideo || (targetVideo.parentElement && document.fullscreenElement === targetVideo.parentElement);
if (!isPlaying && !isHovered && !isWebFullscreen && !isBrowserFullscreen) {
return; // Ignore if not actively watching or interacting
}
const keys = settings.hotkeys;
// Ignore repeating keys for non-incremental hotkeys (toggles, snapshots)
if (e.repeat && (key === keys.resetSpeed || key === keys.toggleOverlay || key === keys.toggleWebFullscreen || key === keys.copySnapshot || key === '5')) {
return;
}
if (key === keys.decreaseSpeed) {
e.preventDefault();
adjustSpeed(targetVideo, -settings.speedStep);
} else if (key === keys.increaseSpeed || (keys.increaseSpeed === '+' && key === '=')) {
e.preventDefault();
adjustSpeed(targetVideo, settings.speedStep);
} else if (key === keys.resetSpeed) {
e.preventDefault();
toggleResetSpeed(targetVideo);
} else if (key === keys.rewind5s) {
e.preventDefault();
adjustTime(targetVideo, -settings.rewindStep);
} else if (key === keys.advance5s) {
e.preventDefault();
adjustTime(targetVideo, settings.advanceStep);
} else if (key === keys.rewind30s) {
e.preventDefault();
adjustTime(targetVideo, -settings.rewindStepLong);
} else if (key === keys.advance30s) {
e.preventDefault();
adjustTime(targetVideo, settings.advanceStepLong);
} else if (key === keys.prevFrame) {
e.preventDefault();
stepFrame(targetVideo, -1);
} else if (key === keys.nextFrame) {
e.preventDefault();
stepFrame(targetVideo, 1);
} else if (key === keys.copySnapshot || key === '5') { // Support '/' and fallback '5'
e.preventDefault();
captureScreenshot(targetVideo, 'copy');
} else if (key === keys.downloadSnapshot) {
e.preventDefault();
captureScreenshot(targetVideo, 'download');
} else if (key === keys.toggleOverlay) {
e.preventDefault();
toggleOverlayVisibility();
} else if (key === keys.toggleWebFullscreen) {
e.preventDefault();
toggleWebFullscreen(targetVideo);
}
}, true); // capture phase to override page level video shortcuts when possible
// Determine which video to control
function getActiveVideo() {
if (hoveredVideo && hoveredVideo.offsetWidth > 100) return hoveredVideo;
if (activeVideo && activeVideo.offsetWidth > 100) return activeVideo;
// Get all video elements and filter by visibility/size
const videos = Array.from(document.getElementsByTagName('video')).filter(v => {
return v.offsetWidth > 100 && v.offsetHeight > 100;
});
if (videos.length === 0) {
// Fallback to any video if none are large enough
return document.querySelector('video') || null;
}
// Prefers the playing one
const playing = videos.find(v => !v.paused && !v.ended);
if (playing) return playing;
// If none are playing, find the one with the largest area (main player)
return videos.reduce((largest, current) => {
const largestArea = largest.offsetWidth * largest.offsetHeight;
const currentArea = current.offsetWidth * current.offsetHeight;
return currentArea > largestArea ? current : largest;
}, videos[0]);
}
// Global toggle for overlays
function toggleOverlayVisibility() {
overlayHiddenGlobally = !overlayHiddenGlobally;
document.querySelectorAll('.aura-controller-overlay').forEach(overlay => {
overlay.style.display = overlayHiddenGlobally ? 'none' : (settings.autoShowOverlay ? 'flex' : 'none');
});
showToast(overlayHiddenGlobally ? 'Controller overlays hidden' : 'Controller overlays visible', 'info');
}
// Format seconds to MM:SS or HH:MM:SS
function formatTime(secs) {
const h = Math.floor(secs / 3600);
const m = Math.floor((secs % 3600) / 60);
const s = Math.floor(secs % 60);
const mStr = m.toString().padStart(2, '0');
const sStr = s.toString().padStart(2, '0');
if (h > 0) {
return `${h}:${mStr}:${sStr}`;
}
return `${mStr}:${sStr}`;
}
// Generate safe filename for screenshot
function generateFilename(video) {
const domain = window.location.hostname.replace('www.', '').split('.')[0];
const pageTitle = document.title
.replace(/[\\/:*?"<>|]/g, '') // remove illegal filename chars
.substring(0, 40) // truncate
.trim();
const time = formatTime(video.currentTime).replace(/:/g, '-');
const ext = settings.snapshotFormat === 'jpeg' ? 'jpg' : 'png';
return `Snapshot_${domain}_${pageTitle}_[${time}].${ext}`;
}
// Core Screenshot Engine
function captureScreenshot(video, mode = 'copy') {
const canvas = document.createElement('canvas');
canvas.width = video.videoWidth || video.clientWidth;
canvas.height = video.videoHeight || video.clientHeight;
const ctx = canvas.getContext('2d');
try {
// Draw video frame to canvas
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
const filename = generateFilename(video);
if (mode === 'copy') {
// Instantly copy to clipboard
canvas.toBlob((blob) => {
try {
const item = new ClipboardItem({ [blob.type]: blob });
navigator.clipboard.write([item]).then(() => {
showToast('Snapshot copied to clipboard!', 'success');
}).catch(err => {
console.error('Clipboard copy error:', err);
showToast('Clipboard block. Downloading instead...', 'error');
const mimeType = settings.snapshotFormat === 'jpeg' ? 'image/jpeg' : 'image/png';
const dataUrl = canvas.toDataURL(mimeType, 0.95);
downloadDataUrl(dataUrl, filename);
});
} catch (err) {
console.error('Clipboard item construction failed:', err);
showToast('Clipboard write failed. Downloading instead...', 'error');
const mimeType = settings.snapshotFormat === 'jpeg' ? 'image/jpeg' : 'image/png';
const dataUrl = canvas.toDataURL(mimeType, 0.95);
downloadDataUrl(dataUrl, filename);
}
}, 'image/png');
} else if (mode === 'download') {
// Instantly download
const mimeType = settings.snapshotFormat === 'jpeg' ? 'image/jpeg' : 'image/png';
const dataUrl = canvas.toDataURL(mimeType, 0.95);
downloadDataUrl(dataUrl, filename);
showToast('Snapshot downloaded: ' + filename.substring(0, 22) + '...', 'success');
} else {
// Mode 'preview'
const mimeType = settings.snapshotFormat === 'jpeg' ? 'image/jpeg' : 'image/png';
const dataUrl = canvas.toDataURL(mimeType, 0.95);
showPreviewModal(dataUrl, filename, canvas);
}
} catch (error) {
console.error('Snapshot capture failed:', error);
// Handle CORS issue
if (error.name === 'SecurityError') {
showCORSWarning(video, mode);
} else {
showToast('Screenshot capture failed: ' + error.message, 'error');
}
}
}
// Helper to send data URL to background for downloading
function downloadDataUrl(dataUrl, filename) {
browserAPI.runtime.sendMessage({
action: 'downloadScreenshot',
url: dataUrl,
filename: filename
}, (response) => {
if (response && !response.success) {
// fallback download in tab context if background downloads api fails
const link = document.createElement('a');
link.href = dataUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
});
}
// Show Toast Notifications
function showToast(message, type = 'info') {
let container = document.querySelector('.aura-toast-container-global');
if (!container) {
container = document.createElement('div');
container.className = 'aura-toast-container-global aura-toast-container';
// Inject to parent of active video or body
const activeVid = getActiveVideo();
const parent = activeVid ? activeVid.parentElement : document.body;
parent.appendChild(container);
}
// Attempt to merge matching toasts to prevent spam (speed changes, rewinds/forwards, frame steps)
let existingToast = null;
if (message.startsWith('Speed:')) {
existingToast = Array.from(container.querySelectorAll('.aura-toast')).find(t =>
t.textContent.includes('Speed:')
);
} else if (message.includes('s (') || message.includes('s)')) {
existingToast = Array.from(container.querySelectorAll('.aura-toast')).find(t =>
t.textContent.includes('s (') || t.textContent.includes('s)')
);
} else if (message.toLowerCase().includes('frame')) {
existingToast = Array.from(container.querySelectorAll('.aura-toast')).find(t =>
t.textContent.toLowerCase().includes('frame')
);
}
const setToastTimeout = (el) => {
el.timeoutId = setTimeout(() => {
el.classList.add('aura-fade-out');
el.fadeOutTimeoutId = setTimeout(() => {
el.remove();
if (container.children.length === 0) {
container.remove();
}
}, 300);
}, 2800);
};
if (existingToast) {
const textDiv = existingToast.querySelector('div:last-child');
if (textDiv) {
textDiv.textContent = message;
}
// Reset fade-out animations and timeouts
if (existingToast.timeoutId) clearTimeout(existingToast.timeoutId);
if (existingToast.fadeOutTimeoutId) clearTimeout(existingToast.fadeOutTimeoutId);
existingToast.classList.remove('aura-fade-out');
setToastTimeout(existingToast);
return;
}
const toast = document.createElement('div');
toast.className = `aura-toast aura-toast-${type}`;
let iconColor = '#a5b4fc';
if (type === 'success') iconColor = '#10b981';
if (type === 'error') iconColor = '#ef4444';
toast.innerHTML = `
<div style="color: ${iconColor}; display: flex; align-items:center;">
${ICONS.camera}
</div>
<div>${message}</div>
`;
container.appendChild(toast);
setToastTimeout(toast);
}
// Handle CORS Security Error
function showCORSWarning(video, mode) {
// Check if crossorigin attribute is not set
if (!video.hasAttribute('crossorigin')) {
showToast('Protected source. Attempting bypass...', 'info');
// Capture current stats to reload
const currentSrc = video.src;
const currentTime = video.currentTime;
const wasPaused = video.paused;
// Set crossorigin attribute
video.setAttribute('crossorigin', 'anonymous');
// Re-trigger load
if (currentSrc) {
video.load();
// Wait for video metadata to reload, restore playtime
const onLoaded = () => {
video.currentTime = currentTime;
if (!wasPaused) {
video.play().catch(() => { });
}
video.removeEventListener('loadedmetadata', onLoaded);
// Re-attempt snapshot after a short timeout to let stream settle
setTimeout(() => {
try {
captureScreenshot(video, mode);
} catch (err) {
showToast('CORS restriction blocks frame capture on this domain.', 'error');
}
}, 300);
};
video.addEventListener('loadedmetadata', onLoaded);
}
} else {
showToast('CORS security restriction. Frame capture is disabled by server.', 'error');
}
}
// Show Premium Preview Modal
function showPreviewModal(dataUrl, filename, canvas) {
// Remove existing if any
const existing = document.querySelector('.aura-modal-backdrop');
if (existing) existing.remove();
const backdrop = document.createElement('div');
backdrop.className = 'aura-modal-backdrop';
backdrop.innerHTML = `
<div class="aura-modal-content">
<div class="aura-modal-header">
<div class="aura-modal-title">Snapshot Preview</div>
<button class="aura-btn aura-btn-close aura-close-modal-btn" title="Close modal">${ICONS.close}</button>
</div>
<div class="aura-modal-preview-area">
<img class="aura-modal-img" src="${dataUrl}" alt="Video snapshot" />
</div>
<div class="aura-modal-footer">
<div class="aura-modal-info">
Resolution: ${canvas.width}x${canvas.height}
</div>
<div class="aura-modal-actions">
<button class="aura-btn-secondary aura-copy-btn">Copy to Clipboard</button>
<button class="aura-btn-primary aura-download-btn">Save Snapshot</button>
</div>
</div>
</div>
`;
document.body.appendChild(backdrop);
// Trigger open animation
setTimeout(() => {
backdrop.classList.add('aura-show');
}, 10);
// Copy to clipboard function
const copyBtn = backdrop.querySelector('.aura-copy-btn');
copyBtn.addEventListener('click', () => {
canvas.toBlob((blob) => {
try {
const item = new ClipboardItem({ [blob.type]: blob });
navigator.clipboard.write([item]).then(() => {
copyBtn.textContent = 'Copied!';
copyBtn.style.backgroundColor = 'rgba(16, 185, 129, 0.2)';
copyBtn.style.borderColor = '#10b981';
showToast('Snapshot copied to clipboard!', 'success');
setTimeout(() => {
copyBtn.textContent = 'Copy to Clipboard';
copyBtn.style.backgroundColor = '';
copyBtn.style.borderColor = '';
}, 2000);
}).catch(err => {
console.error('Clipboard copy error:', err);
showToast('Clipboard copy failed. Direct downloading instead.', 'error');
downloadDataUrl(dataUrl, filename);
});
} catch (err) {
console.error('Clipboard item construction failed:', err);
showToast('Clipboard write not supported in context. Download instead.', 'error');
}
}, 'image/png');
});
// Download action
const downloadBtn = backdrop.querySelector('.aura-download-btn');
downloadBtn.addEventListener('click', () => {
downloadDataUrl(dataUrl, filename);
closeModal();
showToast('Snapshot downloaded!', 'success');
});
// Close actions
const closeBtn = backdrop.querySelector('.aura-close-modal-btn');
closeBtn.addEventListener('click', closeModal);
backdrop.addEventListener('click', (e) => {
if (e.target === backdrop) closeModal();
});
function closeModal() {
backdrop.classList.remove('aura-show');
setTimeout(() => {
backdrop.remove();
}, 300);
}
}
// Observe page mutations to discover dynamically loaded video nodes
function initObserver() {
// Initial scan
loadSettings();
const observer = new MutationObserver((mutations) => {
let needsScan = false;
for (let mutation of mutations) {
if (mutation.addedNodes.length > 0) {
for (let node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
if (node.tagName === 'VIDEO' || node.querySelector('video')) {
needsScan = true;
break;
}
}
}
}
if (needsScan) break;
}
if (needsScan) {
initializeAllVideos();
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
// Register UserScript command menus if Tampermonkey supports it
if (typeof GM_registerMenuCommand !== 'undefined') {
GM_registerMenuCommand("📸 Copy Video Snapshot to Clipboard (5)", () => {
const targetVideo = getActiveVideo();
if (targetVideo) {
captureScreenshot(targetVideo, 'copy');
} else {
showToast('No active video found on page.', 'error');
}
});
GM_registerMenuCommand("💾 Save Video Snapshot to Disk", () => {
const targetVideo = getActiveVideo();
if (targetVideo) {
captureScreenshot(targetVideo, 'download');
} else {
showToast('No active video found on page.', 'error');
}
});
GM_registerMenuCommand("🖥️ Toggle Web Fullscreen (3)", () => {
const targetVideo = getActiveVideo();
if (targetVideo) {
toggleWebFullscreen(targetVideo);
} else {
showToast('No active video found on page.', 'error');
}
});
GM_registerMenuCommand("👁️ Toggle Controller Overlays (1)", () => {
toggleOverlayVisibility();
});
}
}
// Run extension script when document is ready
if (document.readyState === 'complete' || document.readyState === 'interactive') {
initObserver();
} else {
document.addEventListener('DOMContentLoaded', initObserver);
}
})();