YouTube Arrow Key Video Control (Improved Sync)

Sync with native YouTube volume & avoid duplicate seeking

  1. // ==UserScript==
  2. // @name YouTube Arrow Key Video Control (Improved Sync)
  3. // @name:ru Улучшенное управление YouTube через стрелки
  4. // @namespace http://tampermonkey.net/
  5. // @version 2.1
  6. // @description Sync with native YouTube volume & avoid duplicate seeking
  7. // @description:ru Правильное управление видео YouTube через стрелки на клавиатуре.
  8. // @author Boss of this gym
  9. // @match *://www.youtube.com/*
  10. // @grant none
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. (function() {
  15. 'use strict';
  16.  
  17. const VOLUME_STEP = 10; // 10%
  18. const SEEK_STEP = 5; // seconds
  19.  
  20. function getVideoElement() {
  21. return document.querySelector('video');
  22. }
  23.  
  24. function getVolumePercent(video) {
  25. return Math.round(video.volume * 100);
  26. }
  27.  
  28. function setVolumeFromPercent(video, percent) {
  29. const clamped = Math.min(Math.max(percent, 0), 100);
  30. video.volume = clamped / 100;
  31. showOverlay(`🔊 ${clamped}%`);
  32. }
  33.  
  34. function seekVideo(video, delta) {
  35. const newTime = Math.min(Math.max(video.currentTime + delta, 0), video.duration);
  36. video.currentTime = newTime;
  37. showOverlay(`${delta > 0 ? '⏩' : '⏪'} ${Math.abs(delta)}s`);
  38. }
  39.  
  40. function isInputElementFocused() {
  41. const active = document.activeElement;
  42. return active && (['INPUT', 'TEXTAREA'].includes(active.tagName) || active.isContentEditable);
  43. }
  44.  
  45. function createOverlay() {
  46. const overlay = document.createElement('div');
  47. overlay.id = 'yt-ctrl-overlay';
  48. overlay.style.cssText = `
  49. position: fixed;
  50. top: 20%;
  51. left: 50%;
  52. transform: translateX(-50%);
  53. padding: 12px 24px;
  54. background: rgba(0, 0, 0, 0.7);
  55. color: #fff;
  56. font-size: 20px;
  57. border-radius: 8px;
  58. z-index: 9999;
  59. display: none;
  60. `;
  61. document.body.appendChild(overlay);
  62. return overlay;
  63. }
  64.  
  65. const overlay = createOverlay();
  66. let overlayTimeout = null;
  67.  
  68. function showOverlay(text) {
  69. overlay.textContent = text;
  70. overlay.style.display = 'block';
  71. clearTimeout(overlayTimeout);
  72. overlayTimeout = setTimeout(() => {
  73. overlay.style.display = 'none';
  74. }, 800);
  75. }
  76.  
  77. window.addEventListener('keydown', function(event) {
  78. if (isInputElementFocused() || event.altKey) return;
  79.  
  80. const video = getVideoElement();
  81. if (!video) return;
  82.  
  83. if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(event.key)) {
  84. event.preventDefault();
  85. event.stopImmediatePropagation();
  86.  
  87. if (document.activeElement !== video) {
  88. video.setAttribute('tabindex', '-1');
  89. video.focus();
  90. }
  91.  
  92. switch (event.key) {
  93. case 'ArrowUp': {
  94. const vol = Math.floor(video.volume * 100);
  95. setVolumeFromPercent(video, vol + VOLUME_STEP);
  96. break;
  97. }
  98. case 'ArrowDown': {
  99. const vol = Math.floor(video.volume * 100);
  100. setVolumeFromPercent(video, vol - VOLUME_STEP);
  101. break;
  102. }
  103. case 'ArrowRight':
  104. seekVideo(video, SEEK_STEP);
  105. break;
  106. case 'ArrowLeft':
  107. seekVideo(video, -SEEK_STEP);
  108. break;
  109. }
  110. }
  111. }, true); // Use capture to intercept before YouTube
  112. })();