您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Enhanced YouTube playback controls with privacy-focused features. Hold right arrow for fast playback, left for slow-mo, and track your last 5 videos' timestamps locally so you can resume watching where you left off. (no data sent to servers).
当前为
- // ==UserScript==
- // @name Better YouTube Video Controls
- // @namespace http://tampermonkey.net/
- // @version 1.5.1
- // @description Enhanced YouTube playback controls with privacy-focused features. Hold right arrow for fast playback, left for slow-mo, and track your last 5 videos' timestamps locally so you can resume watching where you left off. (no data sent to servers).
- // @author Henry Suen
- // @match *://*.youtube.com/*
- // @license MIT
- // @grant GM_getValue
- // @grant GM_setValue
- // @grant GM_registerMenuCommand
- // @grant GM_deleteValue
- // ==/UserScript==
- (function() {
- 'use strict';
- // --- USER CONFIGURABLE SETTINGS ---
- // Get user settings or use defaults
- let HOLD_RIGHT_PLAYBACK_SPEED = GM_getValue('holdRightPlaybackSpeed', 2.5);
- let HOLD_LEFT_PLAYBACK_SPEED = GM_getValue('holdLeftPlaybackSpeed', 0.2);
- let LONG_PRESS_THRESHOLD = GM_getValue('longPressThreshold', 250); // Default 250ms
- let SKIP_SECONDS = GM_getValue('skipSeconds', 5); // Default 5 seconds
- let RESUME_WATCHING_ENABLED = GM_getValue('resumeWatchingEnabled', true); // Default to on
- const MAX_HISTORY_SIZE = 5; // Number of videos to remember
- // Register menu commands for user configuration
- GM_registerMenuCommand('Set Right Arrow Hold Speed (Fast)', function() {
- const newSpeed = parseFloat(prompt('Enter playback speed when holding right arrow (e.g., 2.0, 3.0):', HOLD_RIGHT_PLAYBACK_SPEED));
- if (!isNaN(newSpeed) && newSpeed > 0) {
- HOLD_RIGHT_PLAYBACK_SPEED = newSpeed;
- GM_setValue('holdRightPlaybackSpeed', newSpeed);
- alert(`Right arrow hold speed set to ${newSpeed}x`);
- } else {
- alert('Invalid value. Please enter a positive number.');
- }
- });
- GM_registerMenuCommand('Set Left Arrow Hold Speed (Slow)', function() {
- const newSpeed = parseFloat(prompt('Enter playback speed when holding left arrow (e.g., 0.5, 0.75):', HOLD_LEFT_PLAYBACK_SPEED));
- if (!isNaN(newSpeed) && newSpeed > 0 && newSpeed <= 1) {
- HOLD_LEFT_PLAYBACK_SPEED = newSpeed;
- GM_setValue('holdLeftPlaybackSpeed', newSpeed);
- alert(`Left arrow hold speed set to ${newSpeed}x`);
- } else {
- alert('Invalid value. Please enter a number between 0 and 1.');
- }
- });
- GM_registerMenuCommand('Set Skip Seconds', function() {
- const newSkip = parseInt(prompt('Enter seconds to skip on right/left arrow tap:', SKIP_SECONDS));
- if (!isNaN(newSkip) && newSkip > 0) {
- SKIP_SECONDS = newSkip;
- GM_setValue('skipSeconds', newSkip);
- alert(`Skip seconds set to ${newSkip}`);
- } else {
- alert('Invalid value. Please enter a positive number.');
- }
- });
- GM_registerMenuCommand('Toggle Resume Watching: ' + (RESUME_WATCHING_ENABLED ? 'ON' : 'OFF'), function() {
- RESUME_WATCHING_ENABLED = !RESUME_WATCHING_ENABLED;
- GM_setValue('resumeWatchingEnabled', RESUME_WATCHING_ENABLED);
- alert(`Resume Watching: ${RESUME_WATCHING_ENABLED ? 'Enabled ✓' : 'Disabled ✗'}`);
- // Refresh menu command label
- GM_registerMenuCommand('Toggle Resume Watching: ' + (RESUME_WATCHING_ENABLED ? 'ON' : 'OFF'), arguments.callee);
- });
- GM_registerMenuCommand('Clear Saved Video History', function() {
- clearVideoHistory();
- alert('Video history has been cleared.');
- });
- // --- END OF USER CONFIGURABLE SETTINGS ---
- // Store the original playback rate
- let originalPlaybackRate = 1.0;
- // Flag to track if we're handling a long press
- let isLongPress = false;
- // Track when the key was pressed
- let keyDownTime = 0;
- // Flag to ensure we only process one keydown at a time
- let keyAlreadyDown = false;
- // Track which key is being held
- let activeKey = null;
- // Reference to our speed indicator element
- let speedIndicator = null;
- // Reference to our timeout for detecting long press
- let longPressTimeout = null;
- // Our own UI indicator for actions
- let actionIndicator = null;
- // Timeout for hiding the action indicator
- let hideActionTimeout = null;
- // Variables for tracking video position
- let currentVideoId = null;
- let positionSaveInterval = null;
- let lastSavedPosition = 0;
- // Store the element that had focus before we blurred the progress bar
- let lastActiveElement = null;
- // Flag to track if resume was skipped due to timestamp in URL
- let resumeSkippedDueToTimestamp = false;
- // Debug mode for troubleshooting
- const DEBUG = false;
- // --- Utility Functions ---
- // Debug logging function
- function debugLog(...args) {
- if (DEBUG) {
- console.log('[YT Controls]', ...args);
- }
- }
- // Get video history array
- function getVideoHistory() {
- const history = GM_getValue('videoHistory', []);
- return Array.isArray(history) ? history : [];
- }
- // Save video history array
- function saveVideoHistory(history) {
- GM_setValue('videoHistory', history);
- }
- // Add a video to the history
- function addToVideoHistory(videoId) {
- if (!videoId) return;
- let history = getVideoHistory();
- // Remove the video ID if it's already in the history to avoid duplicates
- history = history.filter(item => item !== videoId);
- // Add the video ID to the beginning of the array
- history.unshift(videoId);
- // Limit the history size
- if (history.length > MAX_HISTORY_SIZE) {
- history = history.slice(0, MAX_HISTORY_SIZE);
- }
- // Save the updated history
- saveVideoHistory(history);
- debugLog('Updated video history:', history);
- }
- // Clear all saved video history
- function clearVideoHistory() {
- GM_setValue('videoHistory', []);
- // Also clear all position values
- const allKeys = GM_listValues ? GM_listValues() : [];
- for (const key of allKeys) {
- if (key.startsWith('video_pos_')) {
- GM_deleteValue(key);
- }
- }
- debugLog('Video history cleared');
- }
- // Extract video ID from YouTube URL using multiple methods
- function getVideoId() {
- try {
- // Method 1: Using URLSearchParams (most reliable)
- const urlParams = new URLSearchParams(window.location.search);
- const vParam = urlParams.get('v');
- if (vParam) {
- debugLog('Video ID from URL params:', vParam);
- return vParam;
- }
- // Method 2: Try regex on full URL
- const url = window.location.href;
- const regex1 = /(?:v=|\/v\/|youtu\.be\/|\/embed\/)([a-zA-Z0-9_-]{11})/;
- const match1 = url.match(regex1);
- if (match1 && match1[1]) {
- debugLog('Video ID from URL regex:', match1[1]);
- return match1[1];
- }
- // Method 3: Look for it in page metadata
- const metaTag = document.querySelector('meta[property="og:video:url"], meta[itemprop="videoId"]');
- if (metaTag && metaTag.content) {
- const metaUrl = metaTag.content;
- const metaMatch = metaUrl.match(/([a-zA-Z0-9_-]{11})/);
- if (metaMatch && metaMatch[1]) {
- debugLog('Video ID from meta tag:', metaMatch[1]);
- return metaMatch[1];
- }
- }
- // Method 4: Look for video ID in the page content
- const pageContent = document.documentElement.innerHTML;
- const videoIdMatch = pageContent.match(/"videoId"\s*:\s*"([a-zA-Z0-9_-]{11})"/);
- if (videoIdMatch && videoIdMatch[1]) {
- debugLog('Video ID from page content:', videoIdMatch[1]);
- return videoIdMatch[1];
- }
- debugLog('Could not find video ID');
- return null;
- } catch (e) {
- console.error('Error in getVideoId:', e);
- return null;
- }
- }
- // Make video ID safe for storage as a key
- function getSafeVideoKey(videoId) {
- if (!videoId) return null;
- // Encode the video ID for safety as a key
- return 'video_pos_' + encodeURIComponent(videoId);
- }
- // Check if URL has a timestamp parameter
- function hasTimestampInUrl() {
- const url = window.location.href;
- return url.includes("&t=") || url.includes("?t=");
- }
- // Extract timestamp value from URL
- function getTimestampFromUrl() {
- const url = window.location.href;
- const regex = /[?&]t=([0-9hms]+)/;
- const match = url.match(regex);
- if (!match) return 0;
- const value = match[1];
- // Handle numeric seconds format (e.g., t=120)
- if (/^\d+$/.test(value)) {
- return parseInt(value);
- }
- // Handle YouTube's time format (e.g., 1h2m3s)
- let seconds = 0;
- const hours = value.match(/(\d+)h/);
- const minutes = value.match(/(\d+)m/);
- const secs = value.match(/(\d+)s/);
- if (hours) seconds += parseInt(hours[1]) * 3600;
- if (minutes) seconds += parseInt(minutes[1]) * 60;
- if (secs) seconds += parseInt(secs[1]);
- return seconds;
- }
- // Save current video position
- function saveVideoPosition() {
- // Skip if resume watching is disabled
- if (!RESUME_WATCHING_ENABLED) return;
- const video = findYouTubeVideo();
- if (!video) return;
- const videoId = getVideoId();
- if (!videoId) return;
- const safeKey = getSafeVideoKey(videoId);
- if (!safeKey) return;
- // Only update if position changed significantly (more than 1 second)
- if (Math.abs(video.currentTime - lastSavedPosition) > 1) {
- lastSavedPosition = video.currentTime;
- // Add to video history
- addToVideoHistory(videoId);
- // Save position
- GM_setValue(safeKey, video.currentTime);
- debugLog('Saved position', videoId, video.currentTime);
- }
- }
- // Start tracking video position
- function startPositionTracking() {
- // Skip if resume watching is disabled
- if (!RESUME_WATCHING_ENABLED) return;
- // Clear any existing interval
- if (positionSaveInterval) {
- clearInterval(positionSaveInterval);
- }
- // Set up the new interval
- positionSaveInterval = setInterval(saveVideoPosition, 5000);
- // Update the current video ID
- currentVideoId = getVideoId();
- // Add to history immediately
- if (currentVideoId) {
- addToVideoHistory(currentVideoId);
- debugLog('Started tracking', currentVideoId);
- }
- }
- // Restore video position
- function restoreVideoPosition() {
- // Skip if resume watching is disabled
- if (!RESUME_WATCHING_ENABLED) return;
- const videoId = getVideoId();
- if (!videoId) return;
- debugLog('Attempting to restore position for', videoId);
- // Check if URL has a timestamp
- if (hasTimestampInUrl()) {
- resumeSkippedDueToTimestamp = true;
- // Show notification that resume was skipped
- showActionIndicator("Resume skipped: Timestamp in URL", 3000);
- debugLog('Resume skipped due to timestamp in URL');
- return;
- }
- // Reset the flag since there's no timestamp
- resumeSkippedDueToTimestamp = false;
- const safeKey = getSafeVideoKey(videoId);
- if (!safeKey) return;
- // Get the saved position
- const savedPosition = GM_getValue(safeKey, 0);
- debugLog('Retrieved saved position:', savedPosition);
- if (savedPosition > 0) {
- const video = findYouTubeVideo();
- if (video) {
- // Don't resume if we're near the start or very close to where we left off
- if (video.currentTime < 3 && savedPosition > 5) {
- video.currentTime = savedPosition;
- // Update YouTube's internal state to be aware of our time change
- const ytplayer = findYouTubePlayer();
- if (ytplayer && typeof ytplayer.seekTo === 'function') {
- try {
- ytplayer.seekTo(savedPosition, true);
- } catch(e) {
- debugLog('Error in ytplayer.seekTo', e);
- }
- }
- // Show a notification that we've resumed
- showActionIndicator(`Resumed at ${formatTime(savedPosition)}`, 3000);
- debugLog('Resumed to', savedPosition);
- }
- }
- }
- }
- // Format time in MM:SS format
- function formatTime(seconds) {
- const minutes = Math.floor(seconds / 60);
- const remainingSeconds = Math.floor(seconds % 60);
- return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
- }
- // Find video element on YouTube
- function findYouTubeVideo() {
- return document.querySelector('#movie_player video');
- }
- // Find YouTube player element
- function findYouTubePlayer() {
- return document.querySelector('#movie_player');
- }
- // Utility function to check if an element is an input field
- function isInputField(element) {
- if (!element) return false;
- const tagName = element.tagName.toLowerCase();
- const type = (element.type || '').toLowerCase();
- return (tagName === 'input' &&
- ['text', 'password', 'email', 'number', 'search', 'tel', 'url'].includes(type)) ||
- tagName === 'textarea' ||
- element.isContentEditable;
- }
- // Check if focus is on progress bar
- function isProgressBarFocused() {
- // There are multiple elements that make up the progress bar
- const progressBar = document.querySelector('.ytp-progress-bar');
- const scrubber = document.querySelector('.ytp-scrubber-container');
- const progressList = document.querySelectorAll('.ytp-progress-list');
- if (document.activeElement === progressBar ||
- document.activeElement === scrubber ||
- (progressList && Array.from(progressList).includes(document.activeElement))) {
- return true;
- }
- // Check for aria attributes that might indicate focus on progress controls
- const activeElement = document.activeElement;
- if (activeElement && (
- activeElement.getAttribute('aria-valuemin') !== null ||
- activeElement.getAttribute('aria-valuemax') !== null ||
- activeElement.getAttribute('role') === 'slider'
- )) {
- // Check if it's in the player controls
- const isInControls = activeElement.closest('.ytp-chrome-bottom');
- return isInControls !== null;
- }
- return false;
- }
- // Remove focus from progress bar and trigger volume control
- function handleVolumeKeyOnProgressBar(isVolumeUp) {
- if (isProgressBarFocused()) {
- // Store the active element so we can restore it later
- lastActiveElement = document.activeElement;
- // Blur the progress bar
- if (lastActiveElement && lastActiveElement.blur) {
- lastActiveElement.blur();
- }
- // Move focus to the player itself
- const player = findYouTubePlayer();
- if (player && player.focus) {
- player.focus();
- }
- // Create and dispatch a synthetic key event to trigger YouTube's volume control
- // We do this after ensuring focus is off the progress bar
- setTimeout(() => {
- const event = new KeyboardEvent('keydown', {
- key: isVolumeUp ? 'ArrowUp' : 'ArrowDown',
- code: isVolumeUp ? 'ArrowUp' : 'ArrowDown',
- keyCode: isVolumeUp ? 38 : 40,
- which: isVolumeUp ? 38 : 40,
- bubbles: true,
- cancelable: true,
- composed: true
- });
- // Dispatch to player element to ensure YouTube's volume control is triggered
- if (player) {
- player.dispatchEvent(event);
- } else {
- // Fallback to document if player can't be found
- document.dispatchEvent(event);
- }
- }, 10);
- return true;
- }
- return false;
- }
- // --- UI Elements ---
- // Create the speed indicator UI element
- function createSpeedIndicator() {
- // Remove any existing indicator first
- removeSpeedIndicator();
- // Create a new indicator
- speedIndicator = document.createElement('div');
- speedIndicator.id = 'speed-indicator';
- speedIndicator.textContent = '1x'; // Default text - will be updated when shown
- // Style the indicator with larger text
- const style = speedIndicator.style;
- style.position = 'absolute';
- style.right = '20px';
- style.top = '20px';
- style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
- style.color = 'white';
- style.padding = '8px 16px'; // Larger padding
- style.borderRadius = '6px';
- style.fontSize = '28px'; // Large font size
- style.fontWeight = 'bold';
- style.zIndex = '9999';
- style.display = 'none'; // Hidden by default
- style.opacity = '0';
- style.transition = 'opacity 0.3s ease';
- // Add it to the player
- const player = findYouTubePlayer();
- if (player) {
- player.appendChild(speedIndicator);
- } else {
- document.body.appendChild(speedIndicator); // Fallback
- }
- return speedIndicator;
- }
- // Create action indicator for volume and skip
- function createActionIndicator() {
- if (actionIndicator) {
- return actionIndicator;
- }
- actionIndicator = document.createElement('div');
- actionIndicator.id = 'action-indicator';
- // Style the action indicator
- const style = actionIndicator.style;
- style.position = 'absolute';
- style.left = '50%';
- style.top = '50%';
- style.transform = 'translate(-50%, -50%)';
- style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
- style.color = 'white';
- style.padding = '12px 20px';
- style.borderRadius = '8px';
- style.fontSize = '24px';
- style.fontWeight = 'bold';
- style.zIndex = '10000';
- style.display = 'none';
- style.opacity = '0';
- style.transition = 'opacity 0.3s ease';
- // Add it to the player
- const player = findYouTubePlayer();
- if (player) {
- player.appendChild(actionIndicator);
- } else {
- document.body.appendChild(actionIndicator);
- }
- return actionIndicator;
- }
- // Show the speed indicator with a fade-in effect
- function showSpeedIndicator(speed) {
- if (!speedIndicator) {
- speedIndicator = createSpeedIndicator();
- }
- speedIndicator.textContent = speed + 'x'; // Update with current speed
- speedIndicator.style.display = 'block';
- setTimeout(() => {
- speedIndicator.style.opacity = '1';
- }, 10); // Small delay to ensure the transition works
- }
- // Hide the speed indicator with a fade-out effect
- function hideSpeedIndicator() {
- if (speedIndicator) {
- speedIndicator.style.opacity = '0';
- setTimeout(() => {
- speedIndicator.style.display = 'none';
- }, 300); // Wait for the transition to complete
- }
- }
- // Remove the speed indicator completely
- function removeSpeedIndicator() {
- if (speedIndicator && speedIndicator.parentNode) {
- speedIndicator.parentNode.removeChild(speedIndicator);
- speedIndicator = null;
- }
- }
- // Show action indicator
- function showActionIndicator(text, duration = 1000) {
- // Clear any existing hide timeout
- if (hideActionTimeout) {
- clearTimeout(hideActionTimeout);
- hideActionTimeout = null;
- }
- const indicator = createActionIndicator();
- // If indicator is already visible, just update text without the fade-out/fade-in
- if (indicator.style.opacity === '1') {
- indicator.textContent = text;
- } else {
- indicator.textContent = text;
- indicator.style.display = 'block';
- // Use a timeout to ensure the transition works
- setTimeout(() => {
- indicator.style.opacity = '1';
- }, 10);
- }
- // Set a new timeout to hide the indicator
- hideActionTimeout = setTimeout(() => {
- indicator.style.opacity = '0';
- setTimeout(() => {
- indicator.style.display = 'none';
- }, 300);
- }, duration);
- }
- // --- Action Functions ---
- // Function to perform a seek forward or backward
- function performSeek(forward = true) {
- const video = findYouTubeVideo();
- if (!video) return false;
- // Calculate new time
- const currentTime = video.currentTime;
- const newTime = currentTime + (forward ? SKIP_SECONDS : -SKIP_SECONDS);
- const finalTime = Math.max(0, newTime);
- // Update both the HTML5 video element and YouTube's internal state
- video.currentTime = finalTime;
- // Try to sync with YouTube's API
- const player = findYouTubePlayer();
- if (player && typeof player.seekTo === 'function') {
- try {
- player.seekTo(finalTime, true);
- } catch(e) {}
- }
- // Show our custom UI indicator
- showActionIndicator(`${forward ? 'Forward' : 'Backward'} ${SKIP_SECONDS}s`);
- return true;
- }
- // Function to change playback speed
- function changePlaybackSpeed(speed) {
- const video = findYouTubeVideo();
- if (!video) return false;
- // Store original rate on first change
- if (originalPlaybackRate === 1.0) {
- originalPlaybackRate = video.playbackRate;
- }
- // Set new speed
- video.playbackRate = speed;
- // Also try to set through YouTube's API if available
- const player = findYouTubePlayer();
- if (player && typeof player.setPlaybackRate === 'function') {
- try {
- player.setPlaybackRate(speed);
- } catch(e) {}
- }
- // Show indicator
- showSpeedIndicator(speed);
- return true;
- }
- // Function to reset playback speed
- function resetPlaybackSpeed() {
- const video = findYouTubeVideo();
- if (!video) return false;
- // Reset to original speed
- video.playbackRate = originalPlaybackRate;
- // Also try to reset through YouTube's API
- const player = findYouTubePlayer();
- if (player && typeof player.setPlaybackRate === 'function') {
- try {
- player.setPlaybackRate(originalPlaybackRate);
- } catch(e) {}
- }
- // Hide speed indicator
- hideSpeedIndicator();
- return true;
- }
- // --- Key Event Handlers ---
- // Main handler for key down events
- const handleKeyDown = function(event) {
- // Skip if we're in an input field
- if (isInputField(document.activeElement)) return;
- // Handle arrow keys
- if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
- // Prevent default immediately
- event.preventDefault();
- event.stopPropagation();
- // Store which key was pressed
- activeKey = event.key;
- // Only proceed if we found a video and it's the first keydown
- if (findYouTubeVideo() && !keyAlreadyDown) {
- keyAlreadyDown = true;
- keyDownTime = Date.now();
- // Set up a timeout to check for long press
- longPressTimeout = setTimeout(() => {
- // If key is still being pressed after threshold
- if (keyAlreadyDown) {
- isLongPress = true;
- // Different behavior based on which arrow key
- if (activeKey === 'ArrowRight') {
- // Right arrow for fast playback
- changePlaybackSpeed(HOLD_RIGHT_PLAYBACK_SPEED);
- } else if (activeKey === 'ArrowLeft') {
- // Left arrow for slow playback
- changePlaybackSpeed(HOLD_LEFT_PLAYBACK_SPEED);
- }
- }
- }, LONG_PRESS_THRESHOLD);
- }
- }
- // Handle up/down arrow keys on progress bar
- else if ((event.key === 'ArrowUp' || event.key === 'ArrowDown') && isProgressBarFocused()) {
- // Prevent default to avoid any time change
- event.preventDefault();
- event.stopPropagation();
- // Handle volume control properly by fixing focus and dispatching a new event
- handleVolumeKeyOnProgressBar(event.key === 'ArrowUp');
- }
- };
- // Main handler for key up events
- const handleKeyUp = function(event) {
- // Skip if we're in an input field
- if (isInputField(document.activeElement)) return;
- if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
- // Prevent default
- event.preventDefault();
- event.stopPropagation();
- // Only process if this is the active key (prevents issues with multiple keys)
- if (event.key === activeKey) {
- // Clear the timeout to prevent speed change activation if key was released quickly
- if (longPressTimeout) {
- clearTimeout(longPressTimeout);
- longPressTimeout = null;
- }
- // If this was a long press, reset playback speed
- if (isLongPress) {
- resetPlaybackSpeed();
- } else if (keyAlreadyDown) {
- // This was a quick tap, perform a seek
- performSeek(event.key === 'ArrowRight');
- }
- // Reset tracking variables
- isLongPress = false;
- keyDownTime = 0;
- keyAlreadyDown = false;
- activeKey = null;
- }
- }
- };
- // --- Setup Functions ---
- // More comprehensive event handling
- function setupGlobalEventHandlers() {
- // Capture all keyboard events at the window level
- window.addEventListener('keydown', (e) => {
- // Skip if we're in an input field to allow normal typing
- if (isInputField(document.activeElement)) {
- return;
- }
- if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
- // Always handle left/right arrows
- handleKeyDown(e);
- // Always prevent propagation
- e.stopPropagation();
- e.preventDefault();
- } else if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isProgressBarFocused()) {
- // For up/down, handle and prevent default when progress bar is focused
- handleKeyDown(e);
- e.stopPropagation();
- e.preventDefault();
- }
- }, true);
- window.addEventListener('keyup', (e) => {
- // Skip if we're in an input field
- if (isInputField(document.activeElement)) {
- return;
- }
- if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
- // Call our handler first
- handleKeyUp(e);
- // Always prevent propagation to YouTube
- e.stopPropagation();
- e.preventDefault();
- }
- }, true);
- }
- // Additional handler for the YouTube player specifically
- function addYouTubePlayerHandlers() {
- const player = findYouTubePlayer();
- if (player) {
- // Create our indicators now that we have the player
- createSpeedIndicator();
- createActionIndicator();
- // Additional direct event listeners for the player
- player.addEventListener('keydown', function(e) {
- if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
- e.preventDefault();
- e.stopPropagation();
- } else if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isProgressBarFocused()) {
- // For up/down on progress bar, handle and prevent
- e.preventDefault();
- e.stopPropagation();
- handleVolumeKeyOnProgressBar(e.key === 'ArrowUp');
- }
- }, true);
- // Try to restore video position
- setTimeout(() => {
- restoreVideoPosition();
- startPositionTracking();
- }, 1500); // Give YouTube a moment to initialize the video
- }
- }
- // Function to handle YouTube video element being added or replaced
- function handleVideoElementChange() {
- const video = findYouTubeVideo();
- if (video) {
- // Try to restore video position
- setTimeout(() => {
- restoreVideoPosition();
- startPositionTracking();
- }, 1500); // Give YouTube a moment to initialize the video
- }
- }
- // Setup functions that will need to be called once the page is loaded
- function setupOnLoad() {
- // Set up the global event handlers
- setupGlobalEventHandlers();
- // Add player-specific handlers
- addYouTubePlayerHandlers();
- // Also try to intercept YouTube's internal keyboard event handling
- const originalDocKeyDown = document.onkeydown;
- const originalDocKeyUp = document.onkeyup;
- document.onkeydown = function(e) {
- // Skip if we're in an input field
- if (isInputField(document.activeElement)) {
- return originalDocKeyDown ? originalDocKeyDown(e) : true;
- }
- if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
- handleKeyDown(e);
- return false; // Prevent default
- } else if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isProgressBarFocused()) {
- // Completely prevent default when progress bar is focused
- handleKeyDown(e);
- return false;
- }
- return originalDocKeyDown ? originalDocKeyDown(e) : true;
- };
- document.onkeyup = function(e) {
- // Skip if we're in an input field
- if (isInputField(document.activeElement)) {
- return originalDocKeyUp ? originalDocKeyUp(e) : true;
- }
- if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
- handleKeyUp(e);
- return false; // Prevent default
- }
- return originalDocKeyUp ? originalDocKeyUp(e) : true;
- };
- }
- // Watch for DOM changes to catch the player element if it loads after the script
- const observer = new MutationObserver(function(mutations) {
- // Look for the player
- if (!document.querySelector('#movie_player')) {
- return;
- }
- // Check if we need to set up the player
- if (!speedIndicator) {
- addYouTubePlayerHandlers();
- }
- // Also watch for video element changes
- const video = findYouTubeVideo();
- if (video && currentVideoId != getVideoId()) {
- handleVideoElementChange();
- }
- });
- // Start observing the document
- observer.observe(document, { childList: true, subtree: true });
- // Listen for navigation events (YouTube is a SPA)
- function handleNavigation() {
- // Check for video ID change
- const newVideoId = getVideoId();
- const currentId = currentVideoId;
- if (newVideoId && newVideoId !== currentId) {
- debugLog('Video ID changed from', currentId, 'to', newVideoId);
- currentVideoId = newVideoId;
- // Clear existing tracking
- if (positionSaveInterval) {
- clearInterval(positionSaveInterval);
- positionSaveInterval = null;
- }
- // Add to history
- addToVideoHistory(newVideoId);
- // Handle the video element for the new page
- handleVideoElementChange();
- }
- }
- // YouTube uses History API for navigation
- const originalPushState = history.pushState;
- history.pushState = function() {
- originalPushState.apply(this, arguments);
- handleNavigation();
- };
- window.addEventListener('popstate', handleNavigation);
- // Call setup once the page is fully loaded
- if (document.readyState === 'complete') {
- setupOnLoad();
- } else {
- window.addEventListener('load', setupOnLoad);
- }
- // Clean up when the page unloads
- window.addEventListener('unload', function() {
- // Save final position before unloading
- saveVideoPosition();
- removeSpeedIndicator();
- if (actionIndicator && actionIndicator.parentNode) {
- actionIndicator.parentNode.removeChild(actionIndicator);
- }
- observer.disconnect();
- if (longPressTimeout) {
- clearTimeout(longPressTimeout);
- }
- if (hideActionTimeout) {
- clearTimeout(hideActionTimeout);
- }
- if (positionSaveInterval) {
- clearInterval(positionSaveInterval);
- }
- });
- })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址