diff --git a/public/s/js/v0ck.js b/public/s/js/v0ck.js index 8c975ee..653f0c7 100644 --- a/public/s/js/v0ck.js +++ b/public/s/js/v0ck.js @@ -527,9 +527,14 @@ class v0ck { document.addEventListener('fullscreenchange', toggleFullScreenClasses); toggleFullScreenClasses(); // Check initial state (important for transitions) + let keyHoldTimer = null; + let isHoldingKey = false; + let heldKey = null; + let reverseInterval = null; + function handleKeyDown(e) { if (!document.body.contains(video)) { - document.removeEventListener('keydown', handleKeyDown); + cleanupKeyboardListeners(); return; } @@ -556,8 +561,129 @@ class v0ck { handleVolumeButton(newVol); showHUD(newVol); } + + if (e.key === "." || e.key === ",") { + if (e.ctrlKey || e.metaKey || e.altKey) return; + e.preventDefault(); + if (!Number.isFinite(video.currentTime)) return; + const duration = Number.isFinite(video.duration) ? video.duration : video.currentTime; + const direction = e.key === "." ? 1 : -1; + + if (heldKey && heldKey !== e.key) return; + const isCurrentHold = heldKey === e.key; + + if (video.paused || isCurrentHold) { + if (!e.repeat) { + heldKey = e.key; + isHoldingKey = false; + if (keyHoldTimer) clearTimeout(keyHoldTimer); + keyHoldTimer = setTimeout(() => { + isHoldingKey = true; + if (heldKey === ".") { + video.play(); + } else if (heldKey === ",") { + if (reverseInterval) clearInterval(reverseInterval); + reverseInterval = setInterval(() => { + if (video.currentTime > 0) { + video.currentTime = Math.max(0, video.currentTime - 0.04); + } else { + clearInterval(reverseInterval); + reverseInterval = null; + } + }, 40); + } + }, 200); + } else { + if (keyHoldTimer && !isHoldingKey) { + clearTimeout(keyHoldTimer); + keyHoldTimer = null; + isHoldingKey = true; + if (heldKey === ".") { + video.play(); + } else if (heldKey === ",") { + if (reverseInterval) clearInterval(reverseInterval); + reverseInterval = setInterval(() => { + if (video.currentTime > 0) { + video.currentTime = Math.max(0, video.currentTime - 0.04); + } else { + clearInterval(reverseInterval); + reverseInterval = null; + } + }, 40); + } + } + } + } else { + video.currentTime = Math.max(0, Math.min(duration, video.currentTime + direction * 5)); + } + showControlsAndReset(); + } } + + function handleKeyUp(e) { + if (!document.body.contains(video)) { + cleanupKeyboardListeners(); + return; + } + if (e.key === "." || e.key === ",") { + if (heldKey === e.key) { + if (keyHoldTimer) { + clearTimeout(keyHoldTimer); + keyHoldTimer = null; + } + if (isHoldingKey) { + if (heldKey === ".") { + video.pause(); + } else if (heldKey === ",") { + if (reverseInterval) { + clearInterval(reverseInterval); + reverseInterval = null; + } + } + } else { + if (Number.isFinite(video.currentTime)) { + const duration = Number.isFinite(video.duration) ? video.duration : video.currentTime; + const direction = heldKey === "." ? 1 : -1; + const frameTime = 1 / 30; + video.currentTime = Math.max(0, Math.min(duration, video.currentTime + direction * frameTime)); + } + } + heldKey = null; + isHoldingKey = false; + showControlsAndReset(); + } + } + } + + function handleBlur() { + if (!document.body.contains(video)) { + cleanupKeyboardListeners(); + return; + } + if (keyHoldTimer) clearTimeout(keyHoldTimer); + if (reverseInterval) clearInterval(reverseInterval); + keyHoldTimer = null; + reverseInterval = null; + if (isHoldingKey) { + if (heldKey === ".") { + video.pause(); + } + } + heldKey = null; + isHoldingKey = false; + } + + function cleanupKeyboardListeners() { + document.removeEventListener('keydown', handleKeyDown); + document.removeEventListener('keyup', handleKeyUp); + window.removeEventListener('blur', handleBlur); + if (keyHoldTimer) clearTimeout(keyHoldTimer); + if (reverseInterval) clearInterval(reverseInterval); + } + document.addEventListener('keydown', handleKeyDown); + document.addEventListener('keyup', handleKeyUp); + window.addEventListener('blur', handleBlur); video.volume = _volume = volumeSlider.value = +(localStorage.getItem('volume') ?? defaultVolume); handleVolumeButton(video.volume); diff --git a/src/inc/locales/de.json b/src/inc/locales/de.json index acb9f66..b2dc192 100644 --- a/src/inc/locales/de.json +++ b/src/inc/locales/de.json @@ -290,6 +290,7 @@ "next_prev": "Weiter/Zurück", "volume": "Lautstärke", "scroll_nav": "Weiter/Zurück", + "frame_skip": "Einzelbildsteuerung (pausiert) / 5s Sprung", "show_help": "Hilfe anzeigen" }, "notifications": { diff --git a/src/inc/locales/en.json b/src/inc/locales/en.json index 08ae7d0..b43da33 100644 --- a/src/inc/locales/en.json +++ b/src/inc/locales/en.json @@ -290,6 +290,7 @@ "next_prev": "next/prev", "volume": "volume", "scroll_nav": "next/prev", + "frame_skip": "frame step (paused) / 5s skip", "show_help": "show help" }, "notifications": { diff --git a/src/inc/locales/nl.json b/src/inc/locales/nl.json index 0d42b99..5a356cb 100644 --- a/src/inc/locales/nl.json +++ b/src/inc/locales/nl.json @@ -290,6 +290,7 @@ "next_prev": "volgende/vorige", "volume": "volume", "scroll_nav": "next/prev", + "frame_skip": "frame-stap (gepauzeerd) / 5s overslaan", "show_help": "help tonen" }, "notifications": { diff --git a/src/inc/locales/zange.json b/src/inc/locales/zange.json index d1650de..bd274aa 100644 --- a/src/inc/locales/zange.json +++ b/src/inc/locales/zange.json @@ -288,6 +288,7 @@ "next_prev": "nächster/vorheriger", "volume": "Lautstärke", "scroll_nav": "nächster/vorheriger", + "frame_skip": "Einzelbildrücken (pausiert) / 5s Hüpfen", "show_help": "Hilfe anzeigen" }, "notifications": { diff --git a/views/snippets/navbar.html b/views/snippets/navbar.html index 7973833..d1a00b0 100644 --- a/views/snippets/navbar.html +++ b/views/snippets/navbar.html @@ -319,6 +319,7 @@
{{ t('shortcuts.next_prev') }}
{{ t('shortcuts.volume') }}
Scroll{{ t('shortcuts.scroll_nav') }}
+
, .{{ t('shortcuts.frame_skip') }}
?{{ t('shortcuts.show_help') }}