diff --git a/mods/config.js b/mods/config.js index c7168057..c0a4353a 100644 --- a/mods/config.js +++ b/mods/config.js @@ -50,6 +50,7 @@ const defaultConfig = { dimmingOpacity: 0.5, enablePaidPromotionOverlay: true, speedSettingsIncrement: 0.25, + holdToSpeed: 2, videoPreferredCodec: 'any', launchToOnStartup: null, reloadHomeOnStartup: true, diff --git a/mods/features/holdToSpeed.js b/mods/features/holdToSpeed.js new file mode 100644 index 00000000..79f733cc --- /dev/null +++ b/mods/features/holdToSpeed.js @@ -0,0 +1,138 @@ +import { configRead } from '../config.js'; + +const SELECT_KEY_CODE = 13; +const HOLD_DELAY_MS = 500; +const DEFAULT_HOLD_PLAYBACK_RATE = 2; +const SPEED_TOAST_ID = 'tt-hold-speed-toast'; + +let holdTimer = null; +let heldVideo = null; +let previousPlaybackRate = null; +let selectPressTarget = null; +let isReplayingSelectPress = false; + +function getPlayingVideo() { + const video = document.querySelector('video'); + if (!video || video.paused || video.ended) return null; + return video; +} + +function getHoldPlaybackRate() { + const configuredSpeed = Number(configRead('holdToSpeed')); + return configuredSpeed > 0 ? configuredSpeed : DEFAULT_HOLD_PLAYBACK_RATE; +} + +function showSpeedToast(playbackRate) { + let toast = document.getElementById(SPEED_TOAST_ID); + if (toast) return; + + toast = document.createElement('div'); + toast.id = SPEED_TOAST_ID; + toast.textContent = `${playbackRate}x`; + toast.style.cssText = [ + 'position:fixed', + 'top:48px', + 'left:50%', + 'transform:translateX(-50%)', + 'z-index:2147483647', + 'padding:12px 22px', + 'border-radius:999px', + 'background:rgba(0, 0, 0, 0.82)', + 'color:#fff', + 'font:600 28px Roboto, Arial, sans-serif', + 'line-height:1', + 'pointer-events:none' + ].join(';'); + document.body.appendChild(toast); +} + +function hideSpeedToast() { + document.getElementById(SPEED_TOAST_ID)?.remove(); +} + +function restorePlaybackRate() { + if (holdTimer !== null) { + clearTimeout(holdTimer); + holdTimer = null; + } + + if (heldVideo && previousPlaybackRate !== null) { + heldVideo.playbackRate = previousPlaybackRate; + } + + heldVideo = null; + previousPlaybackRate = null; + hideSpeedToast(); +} + +function consumeEvent(evt) { + evt.preventDefault(); + evt.stopImmediatePropagation(); +} + +function replaySelectPress() { + if (!selectPressTarget) return; + + isReplayingSelectPress = true; + try { + for (const type of ['keydown', 'keyup']) { + const event = document.createEvent('Event'); + event.initEvent(type, true, true); + event.keyCode = SELECT_KEY_CODE; + event.which = SELECT_KEY_CODE; + selectPressTarget.dispatchEvent(event); + } + } finally { + isReplayingSelectPress = false; + } +} + +function handleKeyDown(evt) { + if (evt.keyCode !== SELECT_KEY_CODE || isReplayingSelectPress) return; + + if (holdTimer !== null || heldVideo) { + consumeEvent(evt); + return; + } + + const video = getPlayingVideo(); + if (!video) return; + + consumeEvent(evt); + selectPressTarget = evt.target || document; + holdTimer = setTimeout(() => { + holdTimer = null; + + if (video !== getPlayingVideo()) return; + + heldVideo = video; + previousPlaybackRate = video.playbackRate; + const playbackRate = getHoldPlaybackRate(); + video.playbackRate = playbackRate; + showSpeedToast(playbackRate); + }, HOLD_DELAY_MS); +} + +function handleKeyUp(evt) { + if (evt.keyCode !== SELECT_KEY_CODE || isReplayingSelectPress || !selectPressTarget) return; + + consumeEvent(evt); + + const wasHeldForSpeed = Boolean(heldVideo); + restorePlaybackRate(); + if (!wasHeldForSpeed) replaySelectPress(); + selectPressTarget = null; +} + +window.addEventListener('keydown', handleKeyDown, true); +window.addEventListener('keyup', handleKeyUp, true); +window.addEventListener('blur', () => { + restorePlaybackRate(); + selectPressTarget = null; +}); +document.addEventListener('visibilitychange', () => { + if (document.hidden) { + restorePlaybackRate(); + selectPressTarget = null; + } +}); diff --git a/mods/translations/resources/en.json b/mods/translations/resources/en.json index 2ac9efd1..795a54f9 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" }, + "holdToSpeed": { + "title": "Hold OK Playback Speed", + "subtitle": "Choose the temporary speed while holding the OK button 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..ed12a725 100644 --- a/mods/ui/settings.js +++ b/mods/ui/settings.js @@ -6,6 +6,23 @@ import { getComprehensiveLanguageList } from '../features/moreSubtitles.js'; const qrcodes = {}; +function playbackSpeedOptions(key) { + const maxSpeed = 5; + const increment = configRead('speedSettingsIncrement') || 0.25; + const options = []; + + for (let speed = increment; speed <= maxSpeed; speed += increment) { + const fixedSpeed = Math.round(speed * 100) / 100; + options.push({ + name: `${fixedSpeed}x`, + key, + value: fixedSpeed + }); + } + + return options; +} + export default function modernUI(update, parameters) { const settings = [ { @@ -411,6 +428,17 @@ export default function modernUI(update, parameters) { } }) }, + { + name: t('settings.options.videoPlayer.options.holdToSpeed.title'), + icon: 'SLOW_MOTION_VIDEO', + value: null, + menuId: 'tt-hold-to-speed', + menuHeader: { + title: t('settings.options.videoPlayer.options.holdToSpeed.title'), + subtitle: t('settings.options.videoPlayer.options.holdToSpeed.subtitle') + }, + options: playbackSpeedOptions('holdToSpeed') + }, { name: t('settings.options.videoPlayer.options.preferredVideoCodec.title'), icon: 'VIDEO_QUALITY', diff --git a/mods/userScript.js b/mods/userScript.js index 7b017e46..659c772f 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/holdToSpeed.js"; import "./features/updater.js"; import "./features/pictureInPicture.js"; import "./features/preferredVideoQuality.js"; @@ -24,4 +25,4 @@ import "./features/enableFeatures.js"; import "./ui/customUI.js"; import "./ui/customGuideAction.js"; import "./features/autoFrameRate.js"; -import "./ui/clock.js"; \ No newline at end of file +import "./ui/clock.js";