diff --git a/public/video.js b/public/video.js index 301ffb0..d7c6ca9 100644 --- a/public/video.js +++ b/public/video.js @@ -222,8 +222,8 @@ function vdUi() { const pp = $('#vpp'); if (pp) { pp.innerHTML = icon(el.paused ? 'play' : 'pause'); pp.setAttribute('aria-label', el.paused ? 'Play' : 'Pause'); } const pv = $('#vprev'), nx = $('#vnextep'); if (pv) pv.disabled = !vp.sibs || vp.sibIdx <= 0; if (nx) nx.disabled = !vp.sibs || vp.sibIdx < 0 || vp.sibIdx >= vp.sibs.length - 1; const bp = $('#vbpp'); if (bp) { bp.innerHTML = icon(el.paused ? 'play' : 'pause'); bp.setAttribute('aria-label', el.paused ? 'Play' : 'Pause'); } - const bi = $('#vbp'); if (bi) bi.style.width = (t ? Math.min(100, 100 * p / t) : 0) + '%'; - const bt = $('#vbtm'); if (bt) bt.textContent = vdClock(p) + ' / ' + vdClock(t); + const bi = vp.barDrag ? null : $('#vbp'); if (bi) bi.style.width = (t ? Math.min(100, 100 * p / t) : 0) + '%'; + const bt = vp.barDrag ? null : $('#vbtm'); if (bt) bt.textContent = vdClock(p) + ' / ' + vdClock(t); const bpv = $('#vbprev'), bnx = $('#vbnext'); if (bpv) bpv.disabled = !vp.sibs || vp.sibIdx <= 0; if (bnx) bnx.disabled = !vp.sibs || vp.sibIdx < 0 || vp.sibIdx >= vp.sibs.length - 1; } /** The mini player: the video keeps playing in a small window while you use other tabs. */ @@ -255,8 +255,15 @@ function videoBar() { if (q('#vbprev')) q('#vbprev').onclick = () => videoGo(-1); if (q('#vbnext')) q('#vbnext').onclick = () => videoGo(1); q('#vbo').onclick = () => { if (vp) location.hash = '#/video/watch/' + vp.id; }; q('#vbx').onclick = () => { const onPlayer = location.hash.startsWith('#/video/watch/'); const back = onPlayer && $('#vback') ? $('#vback').getAttribute('href') : null; videoStop(); if (back) location.hash = back; }; - const sk = q('#vbseek'), at = e => { if (!vp) return; const r = sk.getBoundingClientRect(), t = (vp.m.duration || 0) / 1000 || vp.el.duration || 0; if (t) videoSeek(Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)) * t); }; - sk.onpointerdown = e => { sk.setPointerCapture(e.pointerId); at(e); sk.onpointermove = at; sk.onpointerup = sk.onpointercancel = () => { sk.onpointermove = null; }; }; + // drag along the line: the position and time follow your finger at once, and the video jumps once when you let go (a seek on every move would make a video stutter) + const sk = q('#vbseek'), frac = e => { const r = sk.getBoundingClientRect(); return Math.max(0, Math.min(1, (e.clientX - r.left) / r.width)); }, total = () => (vp.m.duration || 0) / 1000 || vp.el.duration || 0; + const preview = f => { const t = total(); q('#vbp').style.width = (f * 100) + '%'; q('#vbtm').textContent = vdClock(f * t) + ' / ' + vdClock(t); }; + sk.onpointerdown = e => { + if (!vp) return; sk.setPointerCapture(e.pointerId); vp.barDrag = true; let f = frac(e); preview(f); + sk.onpointermove = ev => { f = frac(ev); preview(f); }; + sk.onpointerup = ev => { sk.onpointermove = sk.onpointerup = sk.onpointercancel = null; f = frac(ev); vp.barDrag = false; videoSeek(f * total()); vdUi(); }; + sk.onpointercancel = () => { sk.onpointermove = sk.onpointerup = sk.onpointercancel = null; vp.barDrag = false; vdUi(); }; + }; vdUi(); } /** Lock-screen and media-key controls. */ @@ -278,7 +285,9 @@ function videoSeek(abs) { if (rel >= 0 && isFinite(el.duration) && rel < el.duration - 2) el.currentTime = rel; else videoStart(abs); } function videoSkip(d) { - if (!vp) return; videoSeek(vp.base + vp.el.currentTime + d); + if (!vp) return; const to = Math.max(0, vp.base + vp.el.currentTime + d), t = (vp.m.duration || 0) / 1000 || vp.el.duration || 0; + const bp = $('#vbp'), bt = $('#vbtm'); if (t) { if (bp) bp.style.width = Math.min(100, 100 * to / t) + '%'; if (bt) bt.textContent = vdClock(to) + ' / ' + vdClock(t); } // (the bar moves before the video has finished seeking) + videoSeek(to); if (vp.flash) vp.flash((d < 0 ? '−' : '+') + Math.abs(d) + ' s'); } /** Previous or next episode (or, for the previous one, back to the start if you are more than 5 seconds in). */ @@ -371,7 +380,7 @@ addEventListener('keydown', e => { if (!vp || !vp.el || !location.hash.startsWith('#/video/watch/') || /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName || '') && e.target.type !== 'range' || e.metaKey || e.ctrlKey || e.altKey || document.getElementById('sheet')) return; const el = vp.el, k = e.key; if (vp.wake) vp.wake(); if (e.target.type === 'range' && (k === 'ArrowLeft' || k === 'ArrowRight')) return; // (the sliders use their own arrows) - if (k === ' ' || k === 'k') { e.preventDefault(); el.paused ? el.play() : el.pause(); } + if (k === ' ' || k === 'k') { e.preventDefault(); if (e.target.tagName === 'BUTTON') addEventListener('keyup', ev => ev.preventDefault(), { once: true, capture: true }); el.paused ? el.play() : el.pause(); } // (a focused button would also click on key-up: that is the double toggle) else if (k === 'ArrowRight' || k === 'l') { e.preventDefault(); videoSkip(k === 'l' || e.shiftKey ? 30 : 10); } else if (k === 'ArrowLeft' || k === 'j') { e.preventDefault(); videoSkip(k === 'j' || e.shiftKey ? -30 : -10); } else if (k === 'ArrowUp') { e.preventDefault(); el.volume = Math.min(1, el.volume + .1); store.set('video_vol', el.volume); const s = $('#vvol'); if (s) s.value = Math.round(el.volume * 100); } else if (k === 'ArrowDown') { e.preventDefault(); el.volume = Math.max(0, el.volume - .1); store.set('video_vol', el.volume); const s = $('#vvol'); if (s) s.value = Math.round(el.volume * 100); }