From fdf54e45134bb713e882ec9248e1a7dea2f15e7e Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Sat, 23 May 2026 09:49:57 +0200 Subject: [PATCH] x2 speed for v0ck --- public/s/css/v0ck.css | 29 +++++++++++++++++ public/s/js/v0ck.js | 72 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/public/s/css/v0ck.css b/public/s/css/v0ck.css index 5271fda..baecbd3 100644 --- a/public/s/css/v0ck.css +++ b/public/s/css/v0ck.css @@ -509,4 +509,33 @@ @keyframes danmaku-fly { from { transform: translateX(calc(100vw + 100%)); } to { transform: translateX(calc(-100% - 200px)); } +} + +/* Speedup 2x HUD Pill */ +.v0ck_speed_indicator { + position: absolute; + top: 20px; + left: 50%; + transform: translate(-50%, -10px); + background: rgba(0, 0, 0, 0.75); + backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px); + color: #fff; + padding: 8px 16px; + border-radius: 20px; + font-size: 14px; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + pointer-events: none; + opacity: 0; + transition: opacity 0.2s, transform 0.2s; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5); +} + +.v0ck_speed_indicator:not(.v0ck_hidden) { + opacity: 1; + transform: translate(-50%, 0); } \ No newline at end of file diff --git a/public/s/js/v0ck.js b/public/s/js/v0ck.js index 7a8b121..74058fa 100644 --- a/public/s/js/v0ck.js +++ b/public/s/js/v0ck.js @@ -53,6 +53,12 @@ const tpl_player = (svg, size) => `
+
+ + + + 2X Speed +
@@ -159,6 +165,13 @@ class v0ck { const defaultVolume = 0.5; let mousedown = false; let _volume; + + // Hold to speedup (2x) states + let speedUpTimeout; + let isSpeedingUp = false; + let restorePlaybackRate = 1; + let ignoreNextClick = false; + const speedIndicator = player.querySelector('.v0ck_speed_indicator'); function handleVolumeButton(vol) { [...volumeSymbols].forEach(s => !s.classList.contains('v0ck_hidden') ? s.classList.add('v0ck_hidden') : null); @@ -260,6 +273,12 @@ class v0ck { } player.addEventListener('click', e => { + if (ignoreNextClick) { + e.stopPropagation(); + e.preventDefault(); + ignoreNextClick = false; + return; + } const path = e.path || (e.composedPath && e.composedPath()); const isControls = !!path.filter(f => f.classList?.contains('v0ck_player_controls')).length; if (!isControls) { @@ -358,6 +377,8 @@ class v0ck { if (gestureType === 'none') { if (dy > dx && dy > 5) { gestureType = 'volume'; + clearTimeout(speedUpTimeout); + endSpeedUp(); } else if (dx > dy && dx > 5) { gestureType = 'other'; // Probably seeking or horizontal swipe return; @@ -367,6 +388,9 @@ class v0ck { } if (gestureType === 'volume') { + clearTimeout(speedUpTimeout); + endSpeedUp(); + const deltaY = startY - touch.clientY; // swipe up is positive const sensitivity = 200; // pixels for 0 to 1 range (reverted to original) let newVol = startVol + (deltaY / sensitivity); @@ -575,6 +599,54 @@ class v0ck { video.addEventListener('playing', resetControlsTimer); video.addEventListener('pause', () => clearTimeout(controlsTimer)); + // Speedup 2x on Hold logic + function startSpeedUp(e) { + // Only speed up if the video is currently playing + if (video.paused) return; + + // Only left mouse click or touch triggers speedup + if (e.type === 'mousedown' && e.button !== 0) return; + + // Don't speed up if clicking on controls or settings panel + const path = e.path || (e.composedPath && e.composedPath()); + const isControls = !!path.filter(f => f.classList?.contains('v0ck_player_controls')).length; + if (isControls) return; + + clearTimeout(speedUpTimeout); + speedUpTimeout = setTimeout(() => { + isSpeedingUp = true; + ignoreNextClick = true; + restorePlaybackRate = video.playbackRate; + video.playbackRate = 2.0; + + if (speedIndicator) { + speedIndicator.classList.remove('v0ck_hidden'); + } + }, 500); + } + + function endSpeedUp(e) { + clearTimeout(speedUpTimeout); + if (isSpeedingUp) { + isSpeedingUp = false; + video.playbackRate = restorePlaybackRate; + if (speedIndicator) { + speedIndicator.classList.add('v0ck_hidden'); + } + // Brief timeout before allowing normal clicking again to bypass the immediate click event + setTimeout(() => { + ignoreNextClick = false; + }, 100); + } + } + + player.addEventListener('mousedown', startSpeedUp); + player.addEventListener('touchstart', startSpeedUp, { passive: true }); + player.addEventListener('mouseup', endSpeedUp); + player.addEventListener('mouseleave', endSpeedUp); + player.addEventListener('touchend', endSpeedUp); + player.addEventListener('touchcancel', endSpeedUp); + this.toggleFullScreen = toggleFullScreen; this.enterFullScreen = enterFullScreen;