diff --git a/editor.py b/editor.py index 3751f89..4805861 100644 --- a/editor.py +++ b/editor.py @@ -288,6 +288,10 @@ h1 { margin:0 0 6px; font-size:28px; } .playhead { position:absolute; top:0; bottom:0; width:2px; margin-left:-1px; background:#ffcc33; box-shadow:0 0 5px #000; z-index:4; pointer-events:none; } .handle { position:absolute; top:0; bottom:0; width:18px; margin-left:-9px; cursor:ew-resize; z-index:3; touch-action:none; } .handle::after { content:""; position:absolute; left:7px; top:0; bottom:0; width:4px; background:#fff; border-radius:2px; box-shadow:0 0 5px #000; } +.view-scroll { position:relative; width:100%; height:14px; margin-top:5px; background:#292929; border-radius:7px; overflow:hidden; touch-action:none; cursor:grab; } +.view-scroll-thumb { position:absolute; top:2px; bottom:2px; left:0; min-width:14px; background:#777; border-radius:5px; cursor:grab; touch-action:none; } +.view-scroll-thumb:hover { background:#999; } +.view-scroll-thumb.dragging { background:#aaa; cursor:grabbing; } .time-row { display:flex; justify-content:space-between; margin-top:8px; color:#aaa; font-size:14px; } .controls { display:flex; flex-wrap:wrap; gap:8px; align-items:center; margin-top:18px; } button, a.button { border:0; border-radius:6px; padding:11px 16px; background:#333; color:#fff; cursor:pointer; text-decoration:none; font-size:15px; } @@ -314,6 +318,8 @@ button:disabled { opacity:.45; cursor:not-allowed; } body { padding:12px; } .editor { padding:12px; } .wave-wrap { height:180px; } + .view-scroll { height:18px; } + .view-scroll-thumb { min-width:28px; } .zoom { width:100%; margin-left:0; } .values { grid-template-columns:1fr; } .position { width:100%; margin-left:0; } @@ -335,6 +341,9 @@ button:disabled { opacity:.45; cursor:not-allowed; }
+
+
+
00:00:00 {{ duration }} @@ -392,6 +401,12 @@ const endHandle = document.getElementById('endHandle'); const selection = document.getElementById('selection'); const playhead = document.getElementById('playhead'); const audio = document.getElementById('audio'); +const viewScroll = document.getElementById('viewScroll'); +const viewScrollThumb = document.getElementById('viewScrollThumb'); +let scrollbarDragging = false; +let scrollbarPointerId = null; +let scrollbarStartX = 0; +let scrollbarStartView = 0; function fmt(sec) { sec = Math.max(0, Math.round(sec)); @@ -428,6 +443,14 @@ function render() { document.getElementById('viewStart').textContent = fmt(viewStart); document.getElementById('viewEnd').textContent = fmt(viewEnd); document.getElementById('zoomValue').textContent = 'Zoom: '+Math.round(zoom*100)+' %'; + + // The scrollbar thumb represents the currently visible part of the full recording. + const visiblePercent = Math.max(0, Math.min(100, ((viewEnd-viewStart)/duration)*100)); + const offsetPercent = Math.max(0, Math.min(100-visiblePercent, (viewStart/duration)*100)); + viewScrollThumb.style.width = visiblePercent+'%'; + viewScrollThumb.style.left = offsetPercent+'%'; + viewScrollThumb.style.pointerEvents = zoom > 1 ? 'auto' : 'none'; + viewScrollThumb.classList.toggle('dragging', scrollbarDragging); renderPlayhead(); } function renderPlayhead() { @@ -461,6 +484,60 @@ function resetZoom() { viewEnd = duration; render(); } + +function moveViewTo(newStart) { + const span = viewEnd-viewStart; + if (span >= duration) { + viewStart = 0; + viewEnd = duration; + } else { + viewStart = Math.max(0, Math.min(duration-span, newStart)); + viewEnd = viewStart+span; + } + render(); +} + +viewScrollThumb.addEventListener('pointerdown', e => { + if (zoom <= 1) return; + e.preventDefault(); + e.stopPropagation(); + scrollbarDragging = true; + scrollbarPointerId = e.pointerId; + scrollbarStartX = e.clientX; + scrollbarStartView = viewStart; + viewScrollThumb.setPointerCapture(e.pointerId); + render(); +}); +viewScrollThumb.addEventListener('pointermove', e => { + if (!scrollbarDragging || e.pointerId !== scrollbarPointerId) return; + const trackWidth = viewScroll.clientWidth; + const thumbWidth = viewScrollThumb.offsetWidth; + const movableWidth = Math.max(1, trackWidth-thumbWidth); + const span = viewEnd-viewStart; + const deltaPixels = e.clientX-scrollbarStartX; + const deltaTime = (deltaPixels/movableWidth) * (duration-span); + moveViewTo(scrollbarStartView+deltaTime); + e.preventDefault(); +}); +function endScrollbarDrag(e) { + if (!scrollbarDragging || e.pointerId !== scrollbarPointerId) return; + scrollbarDragging = false; + scrollbarPointerId = null; + render(); +} +viewScrollThumb.addEventListener('pointerup', endScrollbarDrag); +viewScrollThumb.addEventListener('pointercancel', endScrollbarDrag); + +// Clicking/touching the empty part of the scrollbar moves the visible window +// so that the clicked position becomes its center. +viewScroll.addEventListener('pointerdown', e => { + if (zoom <= 1 || e.target === viewScrollThumb) return; + e.preventDefault(); + const r = viewScroll.getBoundingClientRect(); + const ratio = Math.max(0, Math.min(1, (e.clientX-r.left)/r.width)); + const span = viewEnd-viewStart; + moveViewTo(ratio*duration-span/2); +}); wave.addEventListener('wheel', e => { e.preventDefault(); const factor = e.deltaY < 0 ? ZOOM_FACTOR : 1/ZOOM_FACTOR;