Movies & TV: the video keeps playing in a mini player when you leave the player page, and comes back to the full player without restarting
build-container / build (push) Successful in 22m14s
build-container / build (push) Successful in 22m14s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f232dc32d3
commit
0f324bbf60
3 files changed
+94
-35
No files matched your search
@@ -653,3 +653,15 @@ body.nav-mini .ngrp span { display: none; }
|
||||
.vnav .sgrp { margin-left: auto; }
|
||||
.vtop .vcrumbs { color: rgba(255, 255, 255, .8); } .vtop .vcrumbs a { color: rgba(255, 255, 255, .8); }
|
||||
@media (max-width: 700px) { .vtop .vtc { display: none; } .vnav .sgrp { margin-left: 0; } }
|
||||
|
||||
/* Video mini player */
|
||||
#vmini { position: fixed; right: 14px; bottom: calc(var(--nav-h) + var(--sab) + 76px); width: 300px; max-width: calc(100vw - 28px); z-index: 45; border-radius: 12px; overflow: hidden; background: #000; box-shadow: 0 8px 28px rgba(0, 0, 0, .45); border: 1px solid var(--line); }
|
||||
@media (min-width: 768px) { #vmini { bottom: 84px; } }
|
||||
#vmini[hidden] { display: none; }
|
||||
.vmslot { cursor: pointer; } .vmslot video { width: 100%; display: block; aspect-ratio: 16 / 9; background: #000; }
|
||||
.vmover { position: absolute; left: 0; right: 0; top: 0; padding: 6px 8px; display: flex; align-items: center; gap: 8px; color: #fff; background: linear-gradient(rgba(0, 0, 0, .75), transparent); opacity: 0; transition: opacity .2s; }
|
||||
#vmini:hover .vmover, #vmini:focus-within .vmover { opacity: 1; }
|
||||
@media (hover: none) { .vmover { opacity: 1; } }
|
||||
.vmtitle { flex: 1; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.vmbtns { display: flex; gap: 2px; } .vmbtns button { border: 0; background: rgba(255, 255, 255, .15); color: #fff; width: 32px; height: 32px; border-radius: 8px; display: grid; place-items: center; cursor: pointer; } .vmbtns button svg { width: 18px; height: 18px; }
|
||||
.vmprog { position: absolute; left: 0; right: 0; bottom: 0; height: 3px; background: rgba(255, 255, 255, .25); display: block; } .vmprog b { display: block; height: 100%; background: var(--accent); }
|
||||
+81
-34
@@ -29,7 +29,6 @@ async function vdNav(m, back) {
|
||||
}
|
||||
async function viewVideo(args = []) {
|
||||
const v = $('#view'), [kind, key] = args;
|
||||
if (kind !== 'watch') videoStop();
|
||||
v.innerHTML = '<div class="wrap"><h1>Movies & TV</h1><p class="empty">Loading…</p></div>';
|
||||
try {
|
||||
const st = await jfetch('/api/plex/status');
|
||||
@@ -139,37 +138,45 @@ const QUALITIES = [['auto', 'Auto', { kbps: 20000 }], ['original', 'Original (no
|
||||
let hlsLib = null;
|
||||
const loadHls = () => hlsLib || (hlsLib = new Promise((ok, no) => { if (window.Hls) return ok(window.Hls); const s = document.createElement('script'); s.src = '/vendor/hls.min.js?v=1.7.3'; s.onload = () => ok(window.Hls); s.onerror = () => no(new Error('Could not load the video player library')); document.head.appendChild(s); }));
|
||||
|
||||
let vdHooked = false;
|
||||
async function videoWatch(id) {
|
||||
if (vp && vp.id === id && vp.el) { videoMount(); return; } // already playing this one (in the mini player): bring it back
|
||||
const v = $('#view'), mc = await px.get(`/library/metadata/${id}`), m = (mc.Metadata || [])[0];
|
||||
if (!m || !(m.Media || []).length) { v.innerHTML = '<div class="wrap"><p class="empty">That item has no playable file.</p><div class="bar"><a class="btn ghost" href="#/video">Back</a></div></div>'; return; }
|
||||
videoStop();
|
||||
try { pl.a.pause(); } catch {}
|
||||
if (!vdHooked) { vdHooked = true; pl.a.addEventListener('play', () => { if (vp && vp.el && !vp.el.paused) vp.el.pause(); }); } // one thing at a time: music starting pauses the video
|
||||
const media = m.Media[0], part = media.Part[0], streams = part.Stream || [], fromStart = store.get('video_from_start') === id;
|
||||
if (fromStart) store.del('video_from_start');
|
||||
vp = { id, m, media, part, streams, base: 0, hls: null, mode: '', q: store.get('video_q', 'auto'), audio: 0, sub: 0, session: crypto.randomUUID ? crypto.randomUUID() : String(Date.now()), pos: fromStart ? 0 : (m.viewOffset || 0) / 1000, last: 0, timer: null, done: false, sibs: null, speed: 1 };
|
||||
const el = document.createElement('video'); el.playsInline = true; el.autoplay = true; el.id = 'vv';
|
||||
el.volume = Math.max(0, Math.min(1, +store.get('video_vol', 1)));
|
||||
vp = { id, m, media, part, streams, el, base: 0, hls: null, mode: '', q: store.get('video_q', 'auto'), audio: 0, sub: 0, session: crypto.randomUUID ? crypto.randomUUID() : String(Date.now()), pos: fromStart ? 0 : (m.viewOffset || 0) / 1000, last: 0, timer: null, done: false, sibs: null, speed: 1 };
|
||||
videoBindMedia(el);
|
||||
vp.timer = setInterval(() => { if (vp && vp.el === el && !el.paused && !el.ended) vdReport('playing'); }, 10000);
|
||||
if (m.type === 'episode') px.get(`/library/metadata/${m.grandparentRatingKey}/allLeaves`).then(c => { if (vp && vp.id === id) { vp.sibs = c.Metadata || []; vp.sibIdx = vp.sibs.findIndex(x => x.ratingKey === id); vdUi(); } }).catch(() => {});
|
||||
videoMount(); videoStart(vp.pos); videoMediaSession();
|
||||
}
|
||||
const vdTitle = m => m.type === 'episode' ? `${m.grandparentTitle} · S${m.parentIndex}E${m.index} · ${m.title}` : m.title;
|
||||
/** Draw the player page and put the (persistent) video element into it. */
|
||||
function videoMount() {
|
||||
if (!vp) return; videoMini(false);
|
||||
const v = $('#view'), m = vp.m, id = vp.id, ep = m.type === 'episode', title = vdTitle(m), el = vp.el;
|
||||
document.body.classList.add('onvideo');
|
||||
const title = m.type === 'episode' ? `${m.grandparentTitle} · S${m.parentIndex}E${m.index} · ${m.title}` : m.title, ep = m.type === 'episode';
|
||||
const b = (idn, ic, label, extra = '') => `<button class="vb${extra}" id="${idn}" aria-label="${label}" title="${label}">${ic}</button>`;
|
||||
v.innerHTML = `<div class="vwrap"><div class="vstage" id="vstage"><video id="vv" playsinline autoplay></video>
|
||||
v.innerHTML = `<div class="vwrap"><div class="vstage" id="vstage">
|
||||
<div class="vtop"><a class="btn ghost" id="vback" href="${ep ? '#/video/item/' + (m.parentRatingKey || m.grandparentRatingKey) : '#/video/item/' + id}">${icon('back')}Back</a><nav class="vcrumbs vtc" aria-label="Where you are">${vdCrumbs(m)}</nav><b class="vtitle">${esc(title)}</b></div>
|
||||
<div class="vflash" id="vflash" aria-hidden="true"></div><div class="vspin" id="vspin" hidden><i></i></div>
|
||||
<div class="vctl"><div class="vseek"><span id="vcur">0:00</span><input type="range" id="vrange" min="0" max="1000" step="1" value="0" aria-label="Seek"><span id="vrem">-0:00</span></div>
|
||||
<div class="vrow2"><div class="vleft">${ep ? b('vprev', icon('prev'), 'Previous episode') : ''}${b('vb10', `<span class="vskip">${icon('rew')}<em>10</em></span>`, 'Back 10 seconds')}${b('vpp', icon('play'), 'Play', ' big')}${b('vf10', `<span class="vskip">${icon('fwd')}<em>10</em></span>`, 'Forward 10 seconds')}${ep ? b('vnextep', icon('next'), 'Next episode') : ''}</div>
|
||||
<div class="vright">${b('vmute', icon('vol'), 'Mute')}<input type="range" id="vvol" min="0" max="100" value="100" aria-label="Volume">${b('vspd', '1×', 'Speed', ' txt')}${b('vset', 'Quality & tracks', 'Quality and tracks', ' txt')}${document.pictureInPictureEnabled ? b('vpip', icon('pip'), 'Picture in picture') : ''}${b('vfs', icon('full'), 'Full screen')}</div></div></div>
|
||||
<div class="vright">${b('vmute', icon('vol'), 'Mute')}<input type="range" id="vvol" min="0" max="100" value="100" aria-label="Volume">${b('vspd', vp.speed + '×', 'Speed', ' txt')}${b('vset', 'Quality & tracks', 'Quality and tracks', ' txt')}${document.pictureInPictureEnabled ? b('vpip', icon('pip'), 'Picture in picture') : ''}${b('vfs', icon('full'), 'Full screen')}</div></div></div>
|
||||
<div id="vnextbox"></div></div><p class="sub" id="vmsg" role="status"></p></div>`;
|
||||
const el = $('#vv'), stage = $('#vstage'); vp.el = el;
|
||||
const msg = t => { const e = $('#vmsg'); if (e) e.textContent = t || ''; };
|
||||
el.volume = Math.max(0, Math.min(1, +store.get('video_vol', 1))); $('#vvol').value = Math.round(el.volume * 100);
|
||||
const total = () => (vp.m.duration || 0) / 1000, here = () => vp.base + el.currentTime;
|
||||
const stage = $('#vstage'); stage.prepend(el); el.controls = false;
|
||||
$('#vvol').value = Math.round(el.volume * 100);
|
||||
const total = () => (vp.m.duration || 0) / 1000;
|
||||
let drag = false, hideT = null;
|
||||
const wake = () => { stage.classList.remove('vhide'); clearTimeout(hideT); if (!el.paused && !drag) hideT = setTimeout(() => stage.classList.add('vhide'), 3000); };
|
||||
const paint = () => {
|
||||
const t = total() || el.duration || 0, p = here();
|
||||
if (!drag) { $('#vrange').value = t ? Math.round(1000 * Math.min(p, t) / t) : 0; }
|
||||
$('#vcur').textContent = vdClock(drag ? $('#vrange').value / 1000 * t : p); $('#vrem').textContent = '-' + vdClock(Math.max(0, t - (drag ? $('#vrange').value / 1000 * t : p)));
|
||||
};
|
||||
const flash = text => { const f = $('#vflash'); if (!f) return; f.textContent = text; f.classList.remove('on'); void f.offsetWidth; f.classList.add('on'); };
|
||||
vp.flash = flash; vp.wake = wake;
|
||||
vp.wake = wake; vp.drag = () => drag;
|
||||
vp.flash = text => { const f = $('#vflash'); if (!f) return; f.textContent = text; f.classList.remove('on'); void f.offsetWidth; f.classList.add('on'); };
|
||||
$('#vpp').onclick = () => { el.paused ? el.play().catch(() => {}) : el.pause(); };
|
||||
$('#vb10').onclick = () => videoSkip(-10); $('#vf10').onclick = () => videoSkip(10);
|
||||
if ($('#vprev')) $('#vprev').onclick = () => videoGo(-1); if ($('#vnextep')) $('#vnextep').onclick = () => videoGo(1);
|
||||
@@ -179,9 +186,9 @@ async function videoWatch(id) {
|
||||
$('#vvol').oninput = e => { el.volume = e.target.value / 100; el.muted = el.volume === 0; store.set('video_vol', el.volume); };
|
||||
$('#vspd').onclick = () => actionSheet('Playback speed', '', [0.75, 1, 1.25, 1.5, 2].map(r => ({ label: r + '×' + (r === vp.speed ? ' ✓' : ''), icon: 'play', run: () => { vp.speed = r; el.playbackRate = r; $('#vspd').textContent = r + '×'; } })));
|
||||
const range = $('#vrange');
|
||||
range.oninput = () => { drag = true; paint(); wake(); }; range.onchange = () => { const t = total() || el.duration || 0; videoSeek(range.value / 1000 * t); drag = false; wake(); };
|
||||
// clicks: mouse click plays or pauses, double click goes full screen; touch: a tap shows or hides the controls, a double tap on the left or right side skips 10 s
|
||||
let lastTap = 0;
|
||||
range.oninput = () => { drag = true; vdUi(); wake(); }; range.onchange = () => { const t = total() || el.duration || 0; videoSeek(range.value / 1000 * t); drag = false; wake(); };
|
||||
let lastTap = 0, lastDbl = 0;
|
||||
stage.addEventListener('dblclick', () => { lastDbl = Date.now(); });
|
||||
stage.addEventListener('pointerup', e => {
|
||||
if (e.target.closest('.vctl, .vtop, #vnextbox')) return;
|
||||
if (e.pointerType === 'touch') {
|
||||
@@ -190,20 +197,59 @@ async function videoWatch(id) {
|
||||
lastTap = now; if (stage.classList.contains('vhide')) wake(); else { stage.classList.add('vhide'); clearTimeout(hideT); }
|
||||
} else { if (e.detail === 2) { videoFullscreen(); return; } setTimeout(() => { if (Date.now() - lastDbl > 350) { el.paused ? el.play().catch(() => {}) : el.pause(); } }, 0); wake(); }
|
||||
});
|
||||
let lastDbl = 0; stage.addEventListener('dblclick', () => { lastDbl = Date.now(); });
|
||||
stage.addEventListener('pointermove', e => { if (e.pointerType !== 'touch') wake(); }); stage.addEventListener('focusin', wake);
|
||||
el.addEventListener('timeupdate', () => { if (vp && vp.el === el && !el.seeking) vp.pos = vp.base + el.currentTime; paint(); });
|
||||
el.addEventListener('play', () => { $('#vpp').innerHTML = icon('pause'); $('#vpp').setAttribute('aria-label', 'Pause'); wake(); });
|
||||
el.addEventListener('pause', () => { $('#vpp').innerHTML = icon('play'); $('#vpp').setAttribute('aria-label', 'Play'); wake(); if (!el.ended) vdReport('paused'); });
|
||||
el.addEventListener('volumechange', () => { $('#vmute').innerHTML = icon(el.muted || el.volume === 0 ? 'mute' : 'vol'); });
|
||||
el.addEventListener('playing', () => { msg(''); $('#vspin').hidden = true; vdReport('playing'); });
|
||||
el.addEventListener('waiting', () => { $('#vspin').hidden = false; vp.waitSince = Date.now(); if (vp.mode === 'hls') setTimeout(() => { if (vp && vp.el === el && vp.waitSince && Date.now() - vp.waitSince > 9000 && el.readyState < 3) { msg('Taking a while… restarting from here'); videoStart(vp.base + el.currentTime); } }, 9500); });
|
||||
el.addEventListener('canplay', () => { $('#vspin').hidden = true; if (vp) vp.waitSince = 0; });
|
||||
vdUi(); wake();
|
||||
}
|
||||
/** Media events, bound once to the video element (which outlives the player page while the mini player is showing). */
|
||||
function videoBindMedia(el) {
|
||||
const msg = t => { const e = $('#vmsg'); if (e) e.textContent = t || ''; };
|
||||
el.addEventListener('timeupdate', () => { if (vp && vp.el === el && !el.seeking) vp.pos = vp.base + el.currentTime; vdUi(); });
|
||||
el.addEventListener('play', () => { vdUi(); if (vp && vp.wake) vp.wake(); });
|
||||
el.addEventListener('pause', () => { vdUi(); if (vp && vp.wake) vp.wake(); if (!el.ended) vdReport('paused'); });
|
||||
el.addEventListener('volumechange', () => { const mu = $('#vmute'); if (mu) mu.innerHTML = icon(el.muted || el.volume === 0 ? 'mute' : 'vol'); });
|
||||
el.addEventListener('playing', () => { msg(''); const s = $('#vspin'); if (s) s.hidden = true; vdReport('playing'); });
|
||||
el.addEventListener('waiting', () => { const s = $('#vspin'); if (s) s.hidden = false; vp.waitSince = Date.now(); if (vp.mode === 'hls') setTimeout(() => { if (vp && vp.el === el && vp.waitSince && Date.now() - vp.waitSince > 9000 && el.readyState < 3) { msg('Taking a while… restarting from here'); videoStart(vp.base + el.currentTime); } }, 9500); });
|
||||
el.addEventListener('canplay', () => { const s = $('#vspin'); if (s) s.hidden = true; if (vp) vp.waitSince = 0; });
|
||||
el.addEventListener('ended', () => videoEnded());
|
||||
el.addEventListener('error', () => { if (vp && vp.mode === 'direct') { msg('The browser cannot play this file directly. Converting it…'); vp.q = 'auto'; vp.forceHls = true; videoStart(vp.pos); } else msg('Could not play this. Try a lower quality in Quality & tracks.'); });
|
||||
vp.timer = setInterval(() => { if (vp && vp.el === el && !el.paused && !el.ended) vdReport('playing'); }, 10000);
|
||||
if (ep) px.get(`/library/metadata/${m.grandparentRatingKey}/allLeaves`).then(c => { if (vp && vp.id === id) { vp.sibs = c.Metadata || []; vp.sibIdx = vp.sibs.findIndex(x => x.ratingKey === id); const pv = $('#vprev'), nx = $('#vnextep'); if (pv) pv.disabled = vp.sibIdx <= 0; if (nx) nx.disabled = vp.sibIdx < 0 || vp.sibIdx >= vp.sibs.length - 1; } }).catch(() => {});
|
||||
paint(); videoStart(vp.pos);
|
||||
el.addEventListener('error', () => { if (vp && vp.el === el && vp.mode === 'direct') { msg('The browser cannot play this file directly. Converting it…'); vp.q = 'auto'; vp.forceHls = true; videoStart(vp.pos); } else msg('Could not play this. Try a lower quality in Quality & tracks.'); });
|
||||
}
|
||||
/** Refresh whatever controls are on screen (the player page's, and the mini player's) from the video's state. */
|
||||
function vdUi() {
|
||||
if (!vp || !vp.el) return; const el = vp.el, t = (vp.m.duration || 0) / 1000 || el.duration || 0, p = vp.base + el.currentTime, drag = vp.drag && vp.drag();
|
||||
const r = $('#vrange');
|
||||
if (r) { if (!drag) r.value = t ? Math.round(1000 * Math.min(p, t) / t) : 0; const shown = drag ? r.value / 1000 * t : p; $('#vcur').textContent = vdClock(shown); $('#vrem').textContent = '-' + vdClock(Math.max(0, t - shown)); }
|
||||
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 mp = $('#vmpp'); if (mp) mp.innerHTML = icon(el.paused ? 'play' : 'pause'); const pr = $('#vmprog b'); if (pr) pr.style.width = (t ? Math.min(100, 100 * p / t) : 0) + '%';
|
||||
}
|
||||
/** The mini player: the video keeps playing in a small window while you use other tabs. */
|
||||
function videoMini(on) {
|
||||
let box = document.getElementById('vmini');
|
||||
if (!on) { if (box) box.hidden = true; return; }
|
||||
if (!vp || !vp.el) return;
|
||||
if (!box) {
|
||||
box = document.createElement('div'); box.id = 'vmini'; box.setAttribute('role', 'region'); box.setAttribute('aria-label', 'Video mini player');
|
||||
box.innerHTML = `<div class="vmslot"></div><div class="vmover"><b class="vmtitle"></b><div class="vmbtns"><button id="vmpp" aria-label="Play or pause"></button><button id="vmx" aria-label="Back to the player">${icon('full')}</button><button id="vmc" aria-label="Close">${icon('x')}</button></div></div><i class="vmprog" id="vmprog"><b></b></i>`;
|
||||
document.body.appendChild(box);
|
||||
box.querySelector('#vmpp').onclick = e => { e.stopPropagation(); const el = vp && vp.el; if (el) el.paused ? el.play().catch(() => {}) : el.pause(); };
|
||||
box.querySelector('#vmc').onclick = e => { e.stopPropagation(); videoStop(); };
|
||||
const open = () => { if (vp) location.hash = '#/video/watch/' + vp.id; };
|
||||
box.querySelector('#vmx').onclick = e => { e.stopPropagation(); open(); }; box.querySelector('.vmslot').onclick = open;
|
||||
}
|
||||
box.hidden = false; box.querySelector('.vmtitle').textContent = vdTitle(vp.m);
|
||||
vp.el.controls = false; box.querySelector('.vmslot').appendChild(vp.el); document.body.classList.remove('onvideo'); document.body.classList.remove('vfullwin');
|
||||
const st = document.querySelector('.vstage.vfull'); if (st) st.classList.remove('vfull');
|
||||
vdUi();
|
||||
}
|
||||
/** Lock-screen and media-key controls. */
|
||||
function videoMediaSession() {
|
||||
if (!('mediaSession' in navigator) || !vp) return;
|
||||
try {
|
||||
navigator.mediaSession.metadata = new MediaMetadata({ title: vp.m.title, artist: vp.m.type === 'episode' ? vp.m.grandparentTitle : 'Movie', artwork: vp.m.thumb ? [{ src: vdArt(vp.m.thumb, 300, 450), sizes: '300x450' }] : [] });
|
||||
const set = (a, f) => { try { navigator.mediaSession.setActionHandler(a, f); } catch {} };
|
||||
set('play', () => vp && vp.el.play()); set('pause', () => vp && vp.el.pause()); set('seekbackward', () => videoSkip(-10)); set('seekforward', () => videoSkip(10));
|
||||
set('previoustrack', vp.m.type === 'episode' ? () => videoGo(-1) : null); set('nexttrack', vp.m.type === 'episode' ? () => videoGo(1) : null);
|
||||
} catch {}
|
||||
}
|
||||
/** Jump to a point in the whole film or episode. A converted stream only covers what Plex has made from its start point, so a seek outside it restarts the conversion there. */
|
||||
function videoSeek(abs) {
|
||||
@@ -226,7 +272,7 @@ function videoGo(dir) {
|
||||
/** Full screen: the browser's own full screen when it allows it (the player and its buttons stay), the phone's video full screen on iPhone,
|
||||
* otherwise a full-window mode that covers the app. Esc, F or the button leaves it. */
|
||||
function videoFullscreen() {
|
||||
if (!vp) return; const box = document.querySelector('.vstage'), el = vp.el;
|
||||
if (!vp) return; const box = document.querySelector('.vstage'), el = vp.el; if (!box) return;
|
||||
const native = document.fullscreenElement || document.webkitFullscreenElement;
|
||||
if (native) { (document.exitFullscreen || document.webkitExitFullscreen).call(document); return; }
|
||||
if (box.classList.contains('vfull')) { box.classList.remove('vfull'); document.body.classList.remove('vfullwin'); return; }
|
||||
@@ -298,11 +344,12 @@ function videoStop() {
|
||||
try { if (!s.done) tlSend('stopped', s.id, (s.base + (s.el ? s.el.currentTime : s.pos)) * 1000, s.m.duration || 0); } catch {}
|
||||
try { if (s.hls) s.hls.destroy(); if (s.el) { s.el.pause(); s.el.removeAttribute('src'); s.el.load(); } } catch {}
|
||||
fetch('/plex/video/:/transcode/universal/stop?session=' + s.session).catch(() => {});
|
||||
const mb = document.getElementById('vmini'); if (mb) { mb.hidden = true; mb.querySelector('.vmslot').innerHTML = ''; }
|
||||
}
|
||||
addEventListener('hashchange', () => { if (vp && !location.hash.startsWith('#/video/watch/')) videoStop(); });
|
||||
addEventListener('hashchange', () => { if (vp && !location.hash.startsWith('#/video/watch/')) videoMini(true); });
|
||||
addEventListener('pagehide', () => { if (vp) { try { navigator.sendBeacon && navigator.sendBeacon('/plex/video/:/transcode/universal/stop?session=' + vp.session); } catch {} vdReport('stopped'); } });
|
||||
addEventListener('keydown', e => {
|
||||
if (!vp || !vp.el || /^(INPUT|TEXTAREA|SELECT)$/.test(e.target.tagName || '') && e.target.type !== 'range' || e.metaKey || e.ctrlKey || e.altKey || document.getElementById('sheet')) return;
|
||||
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(); }
|
||||
|
||||
Reference in new issue
Block a user