From ff69d70fa46fdef24153d164ce56280b7bc4594f Mon Sep 17 00:00:00 2001 From: Riccardo Ruspoli Date: Sun, 9 Aug 2026 13:13:27 +0200 Subject: [PATCH 1/2] feat(player): add configurable seek interval --- mods/config.js | 1 + mods/features/configurableSeek.js | 54 +++++++++++++++++++++++++++++ mods/translations/resources/en.json | 4 +++ mods/ui/settings.js | 17 +++++++++ mods/userScript.js | 1 + 5 files changed, 77 insertions(+) create mode 100644 mods/features/configurableSeek.js 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..0663035f --- /dev/null +++ b/mods/features/configurableSeek.js @@ -0,0 +1,54 @@ +import { configRead } from '../config.js'; + +const LEFT_KEY_CODE = 37; +const RIGHT_KEY_CODE = 39; +const DEFAULT_SEEK_INTERVAL = 10; + +let handledKeyCode = null; + +function getPlayingVideo() { + const video = document.querySelector('video'); + if (!video || video.paused || video.ended) return null; + return video; +} + +function getSeekInterval() { + const configuredInterval = Number(configRead('seekInterval')); + return [5, 10, 15, 20, 30, 60].includes(configuredInterval) + ? configuredInterval + : DEFAULT_SEEK_INTERVAL; +} + +function consumeEvent(evt) { + evt.preventDefault(); + evt.stopImmediatePropagation(); +} + +function handleKeyDown(evt) { + if (evt.keyCode !== LEFT_KEY_CODE && evt.keyCode !== RIGHT_KEY_CODE) return; + + const video = getPlayingVideo(); + if (!video) return; + + consumeEvent(evt); + + if (handledKeyCode === evt.keyCode && evt.repeat) return; + handledKeyCode = evt.keyCode; + + const direction = evt.keyCode === LEFT_KEY_CODE ? -1 : 1; + const duration = Number.isFinite(video.duration) ? video.duration : Infinity; + video.currentTime = Math.min(duration, Math.max(0, video.currentTime + direction * getSeekInterval())); +} + +function handleKeyUp(evt) { + if (evt.keyCode !== handledKeyCode) return; + + if (getPlayingVideo()) consumeEvent(evt); + handledKeyCode = null; +} + +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"; From 428c5fc3edeb2fb22971463752cc7d3f21e74e68 Mon Sep 17 00:00:00 2001 From: Riccardo Ruspoli Date: Sun, 9 Aug 2026 16:32:07 +0200 Subject: [PATCH 2/2] fix(player): scope configurable seeks to progress bar --- mods/features/configurableSeek.js | 32 +++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/mods/features/configurableSeek.js b/mods/features/configurableSeek.js index 0663035f..3d742761 100644 --- a/mods/features/configurableSeek.js +++ b/mods/features/configurableSeek.js @@ -3,15 +3,10 @@ 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 getPlayingVideo() { - const video = document.querySelector('video'); - if (!video || video.paused || video.ended) return null; - return video; -} - function getSeekInterval() { const configuredInterval = Number(configRead('seekInterval')); return [5, 10, 15, 20, 30, 60].includes(configuredInterval) @@ -19,6 +14,17 @@ function getSeekInterval() { : 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(); @@ -26,27 +32,29 @@ function consumeEvent(evt) { function handleKeyDown(evt) { if (evt.keyCode !== LEFT_KEY_CODE && evt.keyCode !== RIGHT_KEY_CODE) return; + if (!isProgressBarFocused()) return; - const video = getPlayingVideo(); - if (!video) return; + const video = document.querySelector('video'); + if (!video || video.ended) return; consumeEvent(evt); - - if (handledKeyCode === evt.keyCode && evt.repeat) return; handledKeyCode = evt.keyCode; const direction = evt.keyCode === LEFT_KEY_CODE ? -1 : 1; const duration = Number.isFinite(video.duration) ? video.duration : Infinity; - video.currentTime = Math.min(duration, Math.max(0, video.currentTime + direction * getSeekInterval())); + const targetTime = video.currentTime + direction * getSeekInterval(); + video.currentTime = Math.min(duration, Math.max(0, targetTime)); } function handleKeyUp(evt) { if (evt.keyCode !== handledKeyCode) return; - if (getPlayingVideo()) consumeEvent(evt); + 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', () => {