Movies & TV: own full-screen button with a full-window fallback, first-episode button for unstarted shows; Plex sign-in survives plex.tv taking over the tab

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-22 11:00:20 -04:00
1 parent f3216b2f3b
commit 2b79026157
3 files changed
+55 -12

No files matched your search

+25 -5
View File
@@ -99,6 +99,10 @@ async function videoItem(id) {
const seen = m.viewCount > 0 || (m.leafCount && m.viewedLeafCount === m.leafCount);
try { await px.get(`/:/${seen ? 'unscrobble' : 'scrobble'}?key=${id}&identifier=com.plexapp.plugins.library`).catch(() => {}); toast(seen ? 'Marked unwatched' : 'Marked watched'); route(); } catch (e) { toast(e.message); }
};
if (m.type === 'show' && !next) px.get(`/library/metadata/${id}/allLeaves?X-Plex-Container-Start=0&X-Plex-Container-Size=1`).then(mc => { // nothing started yet: offer the first episode
const e = (mc.Metadata || [])[0]; if (!e || !$('#vact')) return;
$('#vact').insertAdjacentHTML('afterbegin', `<button class="btn" id="vfirst">${icon('play')}Play S${e.parentIndex}E${e.index}</button>`); $('#vfirst').onclick = () => { location.hash = `#/video/watch/${e.ratingKey}`; };
}).catch(() => {});
if (m.type === 'show' || m.type === 'season') {
const kids = (await px.get(`/library/metadata/${id}/children`)).Metadata || [];
if (!$('#vkids')) return;
@@ -127,11 +131,11 @@ async function videoWatch(id) {
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 };
document.body.classList.add('onvideo');
const title = m.type === 'episode' ? `${m.grandparentTitle} · S${m.parentIndex}E${m.index} · ${m.title}` : m.title;
v.innerHTML = `<div class="vwrap"><div class="vbar"><a class="btn ghost" id="vback" href="${m.type === 'episode' ? '#/video/item/' + (m.parentRatingKey || m.grandparentRatingKey) : '#/video/item/' + id}">${icon('back')}Back</a><b class="vtitle">${esc(title)}</b><button class="btn ghost" id="vset">Quality & tracks</button></div>
<video id="vv" controls playsinline autoplay></video><p class="sub" id="vmsg" role="status"></p><div id="vnextbox"></div></div>`;
v.innerHTML = `<div class="vwrap"><div class="vbar"><a class="btn ghost" id="vback" href="${m.type === 'episode' ? '#/video/item/' + (m.parentRatingKey || m.grandparentRatingKey) : '#/video/item/' + id}">${icon('back')}Back</a><b class="vtitle">${esc(title)}</b><button class="btn ghost" id="vfs" aria-label="Full screen">Full screen</button><button class="btn ghost" id="vset">Quality & tracks</button></div>
<video id="vv" controls controlsList="nofullscreen" playsinline autoplay></video><p class="sub" id="vmsg" role="status"></p><div id="vnextbox"></div></div>`;
const el = $('#vv'); vp.el = el;
const msg = t => { const e = $('#vmsg'); if (e) e.textContent = t || ''; };
$('#vset').onclick = videoSettings;
$('#vset').onclick = videoSettings; $('#vfs').onclick = videoFullscreen;
el.addEventListener('timeupdate', () => { if (vp && vp.el === el && !el.seeking) { vp.pos = vp.base + el.currentTime; } });
el.addEventListener('playing', () => { msg(''); vdReport('playing'); });
el.addEventListener('pause', () => { if (!el.ended) vdReport('paused'); });
@@ -142,6 +146,22 @@ async function videoWatch(id) {
vp.timer = setInterval(() => { if (vp && vp.el === el && !el.paused && !el.ended) vdReport('playing'); }, 10000);
videoStart(vp.pos);
}
/** 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('.vwrap'), el = vp.el;
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; }
const req = box.requestFullscreen || box.webkitRequestFullscreen;
const fallback = () => { if (el.webkitEnterFullscreen && /iPhone|iPad|iPod/.test(navigator.userAgent)) { try { el.webkitEnterFullscreen(); return; } catch {} } box.classList.add('vfull'); document.body.classList.add('vfullwin'); };
if (!req) return fallback();
try { // some embedded browsers accept the request and then do nothing: if full screen has not started a moment later, use the full-window mode
const p = req.call(box); if (p && p.catch) p.catch(fallback);
setTimeout(() => { if (!document.fullscreenElement && !document.webkitFullscreenElement && !box.classList.contains('vfull') && vp) fallback(); }, 450);
} catch { fallback(); }
}
document.addEventListener('fullscreenchange', () => { const b = $('#vfs'); if (b) b.textContent = document.fullscreenElement ? 'Exit full screen' : 'Full screen'; });
/** Start (or restart) playing from `sec` with the current quality and tracks. */
async function videoStart(sec) {
if (!vp) return; const el = vp.el, { m, media, part } = vp; sec = Math.max(0, sec || 0); if (m.duration && sec * 1000 > m.duration - 15000) sec = 0;
@@ -197,7 +217,7 @@ async function videoEnded() {
}
/** Stop playing: tell Plex where we got to and stop its conversion. Also runs whenever you leave the player. */
function videoStop() {
if (!vp) return; const s = vp; vp = null; document.body.classList.remove('onvideo'); clearInterval(s.timer);
if (!vp) { document.body.classList.remove('vfullwin'); return; } const s = vp; vp = null; document.body.classList.remove('onvideo', 'vfullwin'); try { if (document.fullscreenElement) document.exitFullscreen(); } catch {} clearInterval(s.timer);
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(() => {});
@@ -209,5 +229,5 @@ addEventListener('keydown', e => {
const el = vp.el, k = e.key;
if (k === ' ' || k === 'k') { e.preventDefault(); el.paused ? el.play() : el.pause(); }
else if (k === 'ArrowRight') { e.preventDefault(); el.currentTime += 10; } else if (k === 'ArrowLeft') { e.preventDefault(); el.currentTime = Math.max(0, el.currentTime - 10); }
else if (k === 'f') { e.preventDefault(); el.requestFullscreen && (document.fullscreenElement ? document.exitFullscreen() : el.requestFullscreen()); } else if (k === 'm') el.muted = !el.muted;
else if (k === 'f') { e.preventDefault(); videoFullscreen(); } else if (k === 'Escape') { const b = document.querySelector('.vwrap.vfull'); if (b) { b.classList.remove('vfull'); document.body.classList.remove('vfullwin'); } } else if (k === 'm') el.muted = !el.muted;
}, true);