Better YouTube Video Controls

Enhanced YouTube playback with hold-for-speed controls and resume watching. Hold right arrow for fast playback, left for slow-mo, and save your place in videos!

当前为 2025-04-07 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Better YouTube Video Controls
  3. // @namespace http://tampermonkey.net/
  4. // @version 1.3
  5. // @description Enhanced YouTube playback with hold-for-speed controls and resume watching. Hold right arrow for fast playback, left for slow-mo, and save your place in videos!
  6. // @author Henry Suen
  7. // @match *://*.youtube.com/*
  8. // @license MIT
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_registerMenuCommand
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. // --- USER CONFIGURABLE SETTINGS ---
  18.  
  19. // Get user settings or use defaults
  20. let HOLD_RIGHT_PLAYBACK_SPEED = GM_getValue('holdRightPlaybackSpeed', 2.5);
  21. let HOLD_LEFT_PLAYBACK_SPEED = GM_getValue('holdLeftPlaybackSpeed', 0.2);
  22. let LONG_PRESS_THRESHOLD = GM_getValue('longPressThreshold', 250); // Default 250ms
  23. let SKIP_SECONDS = GM_getValue('skipSeconds', 5); // Default 5 seconds
  24. let RESUME_WATCHING = GM_getValue('resumeWatching', true); // Default to on
  25.  
  26. // Register menu commands for user configuration
  27. GM_registerMenuCommand('Set Right Arrow Hold Speed (Fast)', function() {
  28. const newSpeed = parseFloat(prompt('Enter playback speed when holding right arrow (e.g., 2.0, 3.0):', HOLD_RIGHT_PLAYBACK_SPEED));
  29. if (!isNaN(newSpeed) && newSpeed > 0) {
  30. HOLD_RIGHT_PLAYBACK_SPEED = newSpeed;
  31. GM_setValue('holdRightPlaybackSpeed', newSpeed);
  32. alert(`Right arrow hold speed set to ${newSpeed}x`);
  33. } else {
  34. alert('Invalid value. Please enter a positive number.');
  35. }
  36. });
  37.  
  38. GM_registerMenuCommand('Set Left Arrow Hold Speed (Slow)', function() {
  39. const newSpeed = parseFloat(prompt('Enter playback speed when holding left arrow (e.g., 0.5, 0.75):', HOLD_LEFT_PLAYBACK_SPEED));
  40. if (!isNaN(newSpeed) && newSpeed > 0 && newSpeed <= 1) {
  41. HOLD_LEFT_PLAYBACK_SPEED = newSpeed;
  42. GM_setValue('holdLeftPlaybackSpeed', newSpeed);
  43. alert(`Left arrow hold speed set to ${newSpeed}x`);
  44. } else {
  45. alert('Invalid value. Please enter a number between 0 and 1.');
  46. }
  47. });
  48.  
  49. GM_registerMenuCommand('Set Skip Seconds', function() {
  50. const newSkip = parseInt(prompt('Enter seconds to skip on right/left arrow tap:', SKIP_SECONDS));
  51. if (!isNaN(newSkip) && newSkip > 0) {
  52. SKIP_SECONDS = newSkip;
  53. GM_setValue('skipSeconds', newSkip);
  54. alert(`Skip seconds set to ${newSkip}`);
  55. } else {
  56. alert('Invalid value. Please enter a positive number.');
  57. }
  58. });
  59.  
  60. GM_registerMenuCommand('Toggle Resume Watching', function() {
  61. RESUME_WATCHING = !RESUME_WATCHING;
  62. GM_setValue('resumeWatching', RESUME_WATCHING);
  63. alert(`Resume Watching: ${RESUME_WATCHING ? 'Enabled' : 'Disabled'}`);
  64. });
  65.  
  66. // --- END OF USER CONFIGURABLE SETTINGS ---
  67.  
  68. // Store the original playback rate
  69. let originalPlaybackRate = 1.0;
  70. // Flag to track if we're handling a long press
  71. let isLongPress = false;
  72. // Track when the key was pressed
  73. let keyDownTime = 0;
  74. // Flag to ensure we only process one keydown at a time
  75. let keyAlreadyDown = false;
  76. // Track which key is being held
  77. let activeKey = null;
  78. // Reference to our speed indicator element
  79. let speedIndicator = null;
  80. // Reference to our timeout for detecting long press
  81. let longPressTimeout = null;
  82. // Our own UI indicator for actions
  83. let actionIndicator = null;
  84. // Timeout for hiding the action indicator
  85. let hideActionTimeout = null;
  86. // Variables for tracking video position
  87. let currentVideoId = null;
  88. let positionSaveInterval = null;
  89. let lastSavedPosition = 0;
  90. // Store the element that had focus before we blurred the progress bar
  91. let lastActiveElement = null;
  92.  
  93. // --- Utility Functions ---
  94.  
  95. // Extract video ID from YouTube URL
  96. function getVideoId() {
  97. const url = window.location.href;
  98. const match = url.match(/(?:youtube\.com\/(?:[^\/]+\/.+\/|(?:v|e(?:mbed)?)\/|.*[?&]v=)|youtu\.be\/)([^"&?\/\s]{11})/);
  99. return match ? match[1] : null;
  100. }
  101.  
  102. // Save current video position
  103. function saveVideoPosition() {
  104. const video = findYouTubeVideo();
  105. if (!video || !RESUME_WATCHING) return;
  106.  
  107. const videoId = getVideoId();
  108. if (!videoId) return;
  109.  
  110. // Only update if position changed significantly (more than 1 second)
  111. if (Math.abs(video.currentTime - lastSavedPosition) > 1) {
  112. lastSavedPosition = video.currentTime;
  113. GM_setValue(`videoPosition_${videoId}`, video.currentTime);
  114. }
  115. }
  116.  
  117. // Start tracking video position
  118. function startPositionTracking() {
  119. if (!RESUME_WATCHING) return;
  120.  
  121. // Clear any existing interval
  122. if (positionSaveInterval) {
  123. clearInterval(positionSaveInterval);
  124. }
  125.  
  126. // Set up the new interval
  127. positionSaveInterval = setInterval(saveVideoPosition, 5000);
  128.  
  129. // Update the current video ID
  130. currentVideoId = getVideoId();
  131. }
  132.  
  133. // Restore video position
  134. function restoreVideoPosition() {
  135. if (!RESUME_WATCHING) return;
  136.  
  137. const videoId = getVideoId();
  138. if (!videoId) return;
  139.  
  140. const savedPosition = GM_getValue(`videoPosition_${videoId}`, 0);
  141. if (savedPosition > 0) {
  142. const video = findYouTubeVideo();
  143. if (video) {
  144. // Don't resume if we're near the start or very close to where we left off
  145. if (video.currentTime < 3 && savedPosition > 5) {
  146. video.currentTime = savedPosition;
  147.  
  148. // Update YouTube's internal state to be aware of our time change
  149. const ytplayer = findYouTubePlayer();
  150. if (ytplayer && typeof ytplayer.seekTo === 'function') {
  151. try {
  152. ytplayer.seekTo(savedPosition, true);
  153. } catch(e) {}
  154. }
  155.  
  156. // Show a notification that we've resumed
  157. showActionIndicator(`Resumed at ${formatTime(savedPosition)}`, 3000);
  158. }
  159. }
  160. }
  161. }
  162.  
  163. // Format time in MM:SS format
  164. function formatTime(seconds) {
  165. const minutes = Math.floor(seconds / 60);
  166. const remainingSeconds = Math.floor(seconds % 60);
  167. return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
  168. }
  169.  
  170. // Find video element on YouTube
  171. function findYouTubeVideo() {
  172. return document.querySelector('#movie_player video');
  173. }
  174.  
  175. // Find YouTube player element
  176. function findYouTubePlayer() {
  177. return document.querySelector('#movie_player');
  178. }
  179.  
  180. // Utility function to check if an element is an input field
  181. function isInputField(element) {
  182. if (!element) return false;
  183. const tagName = element.tagName.toLowerCase();
  184. const type = (element.type || '').toLowerCase();
  185.  
  186. return (tagName === 'input' &&
  187. ['text', 'password', 'email', 'number', 'search', 'tel', 'url'].includes(type)) ||
  188. tagName === 'textarea' ||
  189. element.isContentEditable;
  190. }
  191.  
  192. // Check if focus is on progress bar
  193. function isProgressBarFocused() {
  194. // There are multiple elements that make up the progress bar
  195. const progressBar = document.querySelector('.ytp-progress-bar');
  196. const scrubber = document.querySelector('.ytp-scrubber-container');
  197. const progressList = document.querySelectorAll('.ytp-progress-list');
  198.  
  199. if (document.activeElement === progressBar ||
  200. document.activeElement === scrubber ||
  201. (progressList && Array.from(progressList).includes(document.activeElement))) {
  202. return true;
  203. }
  204.  
  205. // Check for aria attributes that might indicate focus on progress controls
  206. const activeElement = document.activeElement;
  207. if (activeElement && (
  208. activeElement.getAttribute('aria-valuemin') !== null ||
  209. activeElement.getAttribute('aria-valuemax') !== null ||
  210. activeElement.getAttribute('role') === 'slider'
  211. )) {
  212. // Check if it's in the player controls
  213. const isInControls = activeElement.closest('.ytp-chrome-bottom');
  214. return isInControls !== null;
  215. }
  216.  
  217. return false;
  218. }
  219.  
  220. // Remove focus from progress bar and trigger volume control
  221. function handleVolumeKeyOnProgressBar(isVolumeUp) {
  222. if (isProgressBarFocused()) {
  223. // Store the active element so we can restore it later
  224. lastActiveElement = document.activeElement;
  225.  
  226. // Blur the progress bar
  227. if (lastActiveElement && lastActiveElement.blur) {
  228. lastActiveElement.blur();
  229. }
  230.  
  231. // Move focus to the player itself
  232. const player = findYouTubePlayer();
  233. if (player && player.focus) {
  234. player.focus();
  235. }
  236.  
  237. // Create and dispatch a synthetic key event to trigger YouTube's volume control
  238. // We do this after ensuring focus is off the progress bar
  239. setTimeout(() => {
  240. const event = new KeyboardEvent('keydown', {
  241. key: isVolumeUp ? 'ArrowUp' : 'ArrowDown',
  242. code: isVolumeUp ? 'ArrowUp' : 'ArrowDown',
  243. keyCode: isVolumeUp ? 38 : 40,
  244. which: isVolumeUp ? 38 : 40,
  245. bubbles: true,
  246. cancelable: true,
  247. composed: true
  248. });
  249.  
  250. // Dispatch to player element to ensure YouTube's volume control is triggered
  251. if (player) {
  252. player.dispatchEvent(event);
  253. } else {
  254. // Fallback to document if player can't be found
  255. document.dispatchEvent(event);
  256. }
  257. }, 10);
  258.  
  259. return true;
  260. }
  261. return false;
  262. }
  263.  
  264. // --- UI Elements ---
  265.  
  266. // Create the speed indicator UI element
  267. function createSpeedIndicator() {
  268. // Remove any existing indicator first
  269. removeSpeedIndicator();
  270.  
  271. // Create a new indicator
  272. speedIndicator = document.createElement('div');
  273. speedIndicator.id = 'speed-indicator';
  274. speedIndicator.textContent = '1x'; // Default text - will be updated when shown
  275.  
  276. // Style the indicator with larger text
  277. const style = speedIndicator.style;
  278. style.position = 'absolute';
  279. style.right = '20px';
  280. style.top = '20px';
  281. style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
  282. style.color = 'white';
  283. style.padding = '8px 16px'; // Larger padding
  284. style.borderRadius = '6px';
  285. style.fontSize = '28px'; // Large font size
  286. style.fontWeight = 'bold';
  287. style.zIndex = '9999';
  288. style.display = 'none'; // Hidden by default
  289. style.opacity = '0';
  290. style.transition = 'opacity 0.3s ease';
  291.  
  292. // Add it to the player
  293. const player = findYouTubePlayer();
  294. if (player) {
  295. player.appendChild(speedIndicator);
  296. } else {
  297. document.body.appendChild(speedIndicator); // Fallback
  298. }
  299.  
  300. return speedIndicator;
  301. }
  302.  
  303. // Create action indicator for volume and skip
  304. function createActionIndicator() {
  305. if (actionIndicator) {
  306. return actionIndicator;
  307. }
  308.  
  309. actionIndicator = document.createElement('div');
  310. actionIndicator.id = 'action-indicator';
  311.  
  312. // Style the action indicator
  313. const style = actionIndicator.style;
  314. style.position = 'absolute';
  315. style.left = '50%';
  316. style.top = '50%';
  317. style.transform = 'translate(-50%, -50%)';
  318. style.backgroundColor = 'rgba(0, 0, 0, 0.7)';
  319. style.color = 'white';
  320. style.padding = '12px 20px';
  321. style.borderRadius = '8px';
  322. style.fontSize = '24px';
  323. style.fontWeight = 'bold';
  324. style.zIndex = '10000';
  325. style.display = 'none';
  326. style.opacity = '0';
  327. style.transition = 'opacity 0.3s ease';
  328.  
  329. // Add it to the player
  330. const player = findYouTubePlayer();
  331. if (player) {
  332. player.appendChild(actionIndicator);
  333. } else {
  334. document.body.appendChild(actionIndicator);
  335. }
  336.  
  337. return actionIndicator;
  338. }
  339.  
  340. // Show the speed indicator with a fade-in effect
  341. function showSpeedIndicator(speed) {
  342. if (!speedIndicator) {
  343. speedIndicator = createSpeedIndicator();
  344. }
  345. speedIndicator.textContent = speed + 'x'; // Update with current speed
  346. speedIndicator.style.display = 'block';
  347. setTimeout(() => {
  348. speedIndicator.style.opacity = '1';
  349. }, 10); // Small delay to ensure the transition works
  350. }
  351.  
  352. // Hide the speed indicator with a fade-out effect
  353. function hideSpeedIndicator() {
  354. if (speedIndicator) {
  355. speedIndicator.style.opacity = '0';
  356. setTimeout(() => {
  357. speedIndicator.style.display = 'none';
  358. }, 300); // Wait for the transition to complete
  359. }
  360. }
  361.  
  362. // Remove the speed indicator completely
  363. function removeSpeedIndicator() {
  364. if (speedIndicator && speedIndicator.parentNode) {
  365. speedIndicator.parentNode.removeChild(speedIndicator);
  366. speedIndicator = null;
  367. }
  368. }
  369.  
  370. // Show action indicator
  371. function showActionIndicator(text, duration = 1000) {
  372. // Clear any existing hide timeout
  373. if (hideActionTimeout) {
  374. clearTimeout(hideActionTimeout);
  375. hideActionTimeout = null;
  376. }
  377.  
  378. const indicator = createActionIndicator();
  379.  
  380. // If indicator is already visible, just update text without the fade-out/fade-in
  381. if (indicator.style.opacity === '1') {
  382. indicator.textContent = text;
  383. } else {
  384. indicator.textContent = text;
  385. indicator.style.display = 'block';
  386.  
  387. // Use a timeout to ensure the transition works
  388. setTimeout(() => {
  389. indicator.style.opacity = '1';
  390. }, 10);
  391. }
  392.  
  393. // Set a new timeout to hide the indicator
  394. hideActionTimeout = setTimeout(() => {
  395. indicator.style.opacity = '0';
  396. setTimeout(() => {
  397. indicator.style.display = 'none';
  398. }, 300);
  399. }, duration);
  400. }
  401.  
  402. // --- Action Functions ---
  403.  
  404. // Function to perform a seek forward or backward
  405. function performSeek(forward = true) {
  406. const video = findYouTubeVideo();
  407. if (!video) return false;
  408.  
  409. // Calculate new time
  410. const currentTime = video.currentTime;
  411. const newTime = currentTime + (forward ? SKIP_SECONDS : -SKIP_SECONDS);
  412. const finalTime = Math.max(0, newTime);
  413.  
  414. // Update both the HTML5 video element and YouTube's internal state
  415. video.currentTime = finalTime;
  416.  
  417. // Try to sync with YouTube's API
  418. const player = findYouTubePlayer();
  419. if (player && typeof player.seekTo === 'function') {
  420. try {
  421. player.seekTo(finalTime, true);
  422. } catch(e) {}
  423. }
  424.  
  425. // Show our custom UI indicator
  426. showActionIndicator(`${forward ? 'Forward' : 'Backward'} ${SKIP_SECONDS}s`);
  427. return true;
  428. }
  429.  
  430. // Function to change playback speed
  431. function changePlaybackSpeed(speed) {
  432. const video = findYouTubeVideo();
  433. if (!video) return false;
  434.  
  435. // Store original rate on first change
  436. if (originalPlaybackRate === 1.0) {
  437. originalPlaybackRate = video.playbackRate;
  438. }
  439.  
  440. // Set new speed
  441. video.playbackRate = speed;
  442.  
  443. // Also try to set through YouTube's API if available
  444. const player = findYouTubePlayer();
  445. if (player && typeof player.setPlaybackRate === 'function') {
  446. try {
  447. player.setPlaybackRate(speed);
  448. } catch(e) {}
  449. }
  450.  
  451. // Show indicator
  452. showSpeedIndicator(speed);
  453.  
  454. return true;
  455. }
  456.  
  457. // Function to reset playback speed
  458. function resetPlaybackSpeed() {
  459. const video = findYouTubeVideo();
  460. if (!video) return false;
  461.  
  462. // Reset to original speed
  463. video.playbackRate = originalPlaybackRate;
  464.  
  465. // Also try to reset through YouTube's API
  466. const player = findYouTubePlayer();
  467. if (player && typeof player.setPlaybackRate === 'function') {
  468. try {
  469. player.setPlaybackRate(originalPlaybackRate);
  470. } catch(e) {}
  471. }
  472.  
  473. // Hide speed indicator
  474. hideSpeedIndicator();
  475.  
  476. return true;
  477. }
  478.  
  479. // --- Key Event Handlers ---
  480.  
  481. // Main handler for key down events
  482. const handleKeyDown = function(event) {
  483. // Skip if we're in an input field
  484. if (isInputField(document.activeElement)) return;
  485.  
  486. // Handle arrow keys
  487. if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
  488. // Prevent default immediately
  489. event.preventDefault();
  490. event.stopPropagation();
  491.  
  492. // Store which key was pressed
  493. activeKey = event.key;
  494.  
  495. // Only proceed if we found a video and it's the first keydown
  496. if (findYouTubeVideo() && !keyAlreadyDown) {
  497. keyAlreadyDown = true;
  498. keyDownTime = Date.now();
  499.  
  500. // Set up a timeout to check for long press
  501. longPressTimeout = setTimeout(() => {
  502. // If key is still being pressed after threshold
  503. if (keyAlreadyDown) {
  504. isLongPress = true;
  505.  
  506. // Different behavior based on which arrow key
  507. if (activeKey === 'ArrowRight') {
  508. // Right arrow for fast playback
  509. changePlaybackSpeed(HOLD_RIGHT_PLAYBACK_SPEED);
  510. } else if (activeKey === 'ArrowLeft') {
  511. // Left arrow for slow playback
  512. changePlaybackSpeed(HOLD_LEFT_PLAYBACK_SPEED);
  513. }
  514. }
  515. }, LONG_PRESS_THRESHOLD);
  516. }
  517. }
  518. // Handle up/down arrow keys on progress bar
  519. else if ((event.key === 'ArrowUp' || event.key === 'ArrowDown') && isProgressBarFocused()) {
  520. // Prevent default to avoid any time change
  521. event.preventDefault();
  522. event.stopPropagation();
  523.  
  524. // Handle volume control properly by fixing focus and dispatching a new event
  525. handleVolumeKeyOnProgressBar(event.key === 'ArrowUp');
  526. }
  527. };
  528.  
  529. // Main handler for key up events
  530. const handleKeyUp = function(event) {
  531. // Skip if we're in an input field
  532. if (isInputField(document.activeElement)) return;
  533.  
  534. if (event.key === 'ArrowRight' || event.key === 'ArrowLeft') {
  535. // Prevent default
  536. event.preventDefault();
  537. event.stopPropagation();
  538.  
  539. // Only process if this is the active key (prevents issues with multiple keys)
  540. if (event.key === activeKey) {
  541. // Clear the timeout to prevent speed change activation if key was released quickly
  542. if (longPressTimeout) {
  543. clearTimeout(longPressTimeout);
  544. longPressTimeout = null;
  545. }
  546.  
  547. // If this was a long press, reset playback speed
  548. if (isLongPress) {
  549. resetPlaybackSpeed();
  550. } else if (keyAlreadyDown) {
  551. // This was a quick tap, perform a seek
  552. performSeek(event.key === 'ArrowRight');
  553. }
  554.  
  555. // Reset tracking variables
  556. isLongPress = false;
  557. keyDownTime = 0;
  558. keyAlreadyDown = false;
  559. activeKey = null;
  560. }
  561. }
  562. };
  563.  
  564. // --- Setup Functions ---
  565.  
  566. // More comprehensive event handling
  567. function setupGlobalEventHandlers() {
  568. // Capture all keyboard events at the window level
  569. window.addEventListener('keydown', (e) => {
  570. // Skip if we're in an input field to allow normal typing
  571. if (isInputField(document.activeElement)) {
  572. return;
  573. }
  574.  
  575. if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
  576. // Always handle left/right arrows
  577. handleKeyDown(e);
  578. // Always prevent propagation
  579. e.stopPropagation();
  580. e.preventDefault();
  581. } else if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isProgressBarFocused()) {
  582. // For up/down, handle and prevent default when progress bar is focused
  583. handleKeyDown(e);
  584. e.stopPropagation();
  585. e.preventDefault();
  586. }
  587. }, true);
  588.  
  589. window.addEventListener('keyup', (e) => {
  590. // Skip if we're in an input field
  591. if (isInputField(document.activeElement)) {
  592. return;
  593. }
  594.  
  595. if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
  596. // Call our handler first
  597. handleKeyUp(e);
  598. // Always prevent propagation to YouTube
  599. e.stopPropagation();
  600. e.preventDefault();
  601. }
  602. }, true);
  603. }
  604.  
  605. // Additional handler for the YouTube player specifically
  606. function addYouTubePlayerHandlers() {
  607. const player = findYouTubePlayer();
  608. if (player) {
  609. // Create our indicators now that we have the player
  610. createSpeedIndicator();
  611. createActionIndicator();
  612.  
  613. // Additional direct event listeners for the player
  614. player.addEventListener('keydown', function(e) {
  615. if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
  616. e.preventDefault();
  617. e.stopPropagation();
  618. } else if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isProgressBarFocused()) {
  619. // For up/down on progress bar, handle and prevent
  620. e.preventDefault();
  621. e.stopPropagation();
  622. handleVolumeKeyOnProgressBar(e.key === 'ArrowUp');
  623. }
  624. }, true);
  625.  
  626. // Try to restore video position
  627. setTimeout(() => {
  628. restoreVideoPosition();
  629. startPositionTracking();
  630. }, 1500); // Give YouTube a moment to initialize the video
  631. }
  632. }
  633.  
  634. // Function to handle YouTube video element being added or replaced
  635. function handleVideoElementChange() {
  636. const video = findYouTubeVideo();
  637. if (video) {
  638. // Try to restore video position
  639. setTimeout(() => {
  640. restoreVideoPosition();
  641. startPositionTracking();
  642. }, 1500); // Give YouTube a moment to initialize the video
  643. }
  644. }
  645.  
  646. // Setup functions that will need to be called once the page is loaded
  647. function setupOnLoad() {
  648. // Set up the global event handlers
  649. setupGlobalEventHandlers();
  650.  
  651. // Add player-specific handlers
  652. addYouTubePlayerHandlers();
  653.  
  654. // Also try to intercept YouTube's internal keyboard event handling
  655. const originalDocKeyDown = document.onkeydown;
  656. const originalDocKeyUp = document.onkeyup;
  657.  
  658. document.onkeydown = function(e) {
  659. // Skip if we're in an input field
  660. if (isInputField(document.activeElement)) {
  661. return originalDocKeyDown ? originalDocKeyDown(e) : true;
  662. }
  663.  
  664. if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
  665. handleKeyDown(e);
  666. return false; // Prevent default
  667. } else if ((e.key === 'ArrowUp' || e.key === 'ArrowDown') && isProgressBarFocused()) {
  668. // Completely prevent default when progress bar is focused
  669. handleKeyDown(e);
  670. return false;
  671. }
  672. return originalDocKeyDown ? originalDocKeyDown(e) : true;
  673. };
  674.  
  675. document.onkeyup = function(e) {
  676. // Skip if we're in an input field
  677. if (isInputField(document.activeElement)) {
  678. return originalDocKeyUp ? originalDocKeyUp(e) : true;
  679. }
  680.  
  681. if (e.key === 'ArrowRight' || e.key === 'ArrowLeft') {
  682. handleKeyUp(e);
  683. return false; // Prevent default
  684. }
  685. return originalDocKeyUp ? originalDocKeyUp(e) : true;
  686. };
  687. }
  688.  
  689. // Watch for DOM changes to catch the player element if it loads after the script
  690. const observer = new MutationObserver(function(mutations) {
  691. // Look for the player
  692. if (!document.querySelector('#movie_player')) {
  693. return;
  694. }
  695.  
  696. // Check if we need to set up the player
  697. if (!speedIndicator) {
  698. addYouTubePlayerHandlers();
  699. }
  700.  
  701. // Also watch for video element changes
  702. const video = findYouTubeVideo();
  703. if (video && currentVideoId != getVideoId()) {
  704. handleVideoElementChange();
  705. }
  706. });
  707.  
  708. // Start observing the document
  709. observer.observe(document, { childList: true, subtree: true });
  710.  
  711. // Listen for navigation events (YouTube is a SPA)
  712. function handleNavigation() {
  713. // Check for video ID change
  714. const newVideoId = getVideoId();
  715. if (newVideoId && newVideoId !== currentVideoId) {
  716. currentVideoId = newVideoId;
  717.  
  718. // Clear existing tracking
  719. if (positionSaveInterval) {
  720. clearInterval(positionSaveInterval);
  721. positionSaveInterval = null;
  722. }
  723.  
  724. // Handle the video element for the new page
  725. handleVideoElementChange();
  726. }
  727. }
  728.  
  729. // YouTube uses History API for navigation
  730. const originalPushState = history.pushState;
  731. history.pushState = function() {
  732. originalPushState.apply(this, arguments);
  733. handleNavigation();
  734. };
  735.  
  736. window.addEventListener('popstate', handleNavigation);
  737.  
  738. // Call setup once the page is fully loaded
  739. if (document.readyState === 'complete') {
  740. setupOnLoad();
  741. } else {
  742. window.addEventListener('load', setupOnLoad);
  743. }
  744.  
  745. // Clean up when the page unloads
  746. window.addEventListener('unload', function() {
  747. // Save final position before unloading
  748. saveVideoPosition();
  749.  
  750. removeSpeedIndicator();
  751. if (actionIndicator && actionIndicator.parentNode) {
  752. actionIndicator.parentNode.removeChild(actionIndicator);
  753. }
  754. observer.disconnect();
  755. if (longPressTimeout) {
  756. clearTimeout(longPressTimeout);
  757. }
  758. if (hideActionTimeout) {
  759. clearTimeout(hideActionTimeout);
  760. }
  761. if (positionSaveInterval) {
  762. clearInterval(positionSaveInterval);
  763. }
  764. });
  765. })();

QingJ © 2025

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