Better YouTube Video Controls

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).

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

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

QingJ © 2025

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