diff --git a/mods/config.js b/mods/config.js index c7168057..f5053526 100644 --- a/mods/config.js +++ b/mods/config.js @@ -50,6 +50,7 @@ const defaultConfig = { dimmingOpacity: 0.5, enablePaidPromotionOverlay: true, speedSettingsIncrement: 0.25, + seekInterval: 10, videoPreferredCodec: 'any', launchToOnStartup: null, reloadHomeOnStartup: true, diff --git a/mods/features/configurableSeek.js b/mods/features/configurableSeek.js new file mode 100644 index 00000000..3d742761 --- /dev/null +++ b/mods/features/configurableSeek.js @@ -0,0 +1,62 @@ +import { configRead } from '../config.js'; + +const LEFT_KEY_CODE = 37; +const RIGHT_KEY_CODE = 39; +const DEFAULT_SEEK_INTERVAL = 10; +const PROGRESS_BAR_SELECTOR = 'ytlr-progress-bar, ytlr-redux-connect-ytlr-progress-bar'; + +let handledKeyCode = null; + +function getSeekInterval() { + const configuredInterval = Number(configRead('seekInterval')); + return [5, 10, 15, 20, 30, 60].includes(configuredInterval) + ? configuredInterval + : DEFAULT_SEEK_INTERVAL; +} + +function isProgressBarFocused() { + const progressBars = document.querySelectorAll(PROGRESS_BAR_SELECTOR); + + for (const progressBar of progressBars) { + if (progressBar.matches(':focus, .zylon-focus')) return true; + if (progressBar.querySelector(':focus, .zylon-focus')) return true; + } + + return false; +} + +function consumeEvent(evt) { + evt.preventDefault(); + evt.stopImmediatePropagation(); +} + +function handleKeyDown(evt) { + if (evt.keyCode !== LEFT_KEY_CODE && evt.keyCode !== RIGHT_KEY_CODE) return; + if (!isProgressBarFocused()) return; + + const video = document.querySelector('video'); + if (!video || video.ended) return; + + consumeEvent(evt); + handledKeyCode = evt.keyCode; + + const direction = evt.keyCode === LEFT_KEY_CODE ? -1 : 1; + const duration = Number.isFinite(video.duration) ? video.duration : Infinity; + const targetTime = video.currentTime + direction * getSeekInterval(); + video.currentTime = Math.min(duration, Math.max(0, targetTime)); +} + +function handleKeyUp(evt) { + if (evt.keyCode !== handledKeyCode) return; + + consumeEvent(evt); + handledKeyCode = null; +} + +// Only take over directional input while the progress bar owns focus. All +// other player controls and closed-player-UI behavior remain native. +window.addEventListener('keydown', handleKeyDown, true); +window.addEventListener('keyup', handleKeyUp, true); +window.addEventListener('blur', () => { + handledKeyCode = null; +}); diff --git a/mods/translations/resources/en.json b/mods/translations/resources/en.json index 2ac9efd1..de6cdc44 100644 --- a/mods/translations/resources/en.json +++ b/mods/translations/resources/en.json @@ -91,6 +91,10 @@ "title": "Speed Settings Increments", "subtitle": "Set the speed increments for video playback speed adjustments" }, + "seekInterval": { + "title": "Seek Interval", + "subtitle": "Choose how many seconds the Left and Right buttons seek during playback" + }, "preferredVideoCodec": { "title": "Preferred Video Codec", "subtitle": "Choose the preferred video codec for playback" diff --git a/mods/ui/settings.js b/mods/ui/settings.js index cd3da273..5047960a 100644 --- a/mods/ui/settings.js +++ b/mods/ui/settings.js @@ -411,6 +411,23 @@ export default function modernUI(update, parameters) { } }) }, + { + name: t('settings.options.videoPlayer.options.seekInterval.title'), + icon: 'SKIP_NEXT', + value: null, + menuId: 'tt-seek-interval', + menuHeader: { + title: t('settings.options.videoPlayer.options.seekInterval.title'), + subtitle: t('settings.options.videoPlayer.options.seekInterval.subtitle') + }, + options: [5, 10, 15, 20, 30, 60].map((seconds) => { + return { + name: `${seconds} s`, + key: 'seekInterval', + value: seconds + } + }) + }, { name: t('settings.options.videoPlayer.options.preferredVideoCodec.title'), icon: 'VIDEO_QUALITY', diff --git a/mods/userScript.js b/mods/userScript.js index 7b017e46..8fb78492 100644 --- a/mods/userScript.js +++ b/mods/userScript.js @@ -16,6 +16,7 @@ import "./ui/theme.js"; import "./ui/settings.js"; import "./ui/disableWhosWatching.js"; import "./features/moreSubtitles.js"; +import "./features/configurableSeek.js"; import "./features/updater.js"; import "./features/pictureInPicture.js"; import "./features/preferredVideoQuality.js";