Add selectable fullscreen orientation and an iPhone landscape overlay
This commit is contained in:
82
frontend/fullscreen-orientation.js
Normal file
82
frontend/fullscreen-orientation.js
Normal file
@@ -0,0 +1,82 @@
|
||||
(function (root) {
|
||||
'use strict';
|
||||
const choices = ['auto', 'landscape', 'portrait'];
|
||||
const normalize = value => choices.includes(value) ? value : 'auto';
|
||||
function rotation(value, width, height) {
|
||||
value = normalize(value);
|
||||
return value === 'landscape' && width < height ? 90 : value === 'portrait' && width > height ? -90 : 0;
|
||||
}
|
||||
function coordinates(x, y, rect, angle = 0) {
|
||||
if (angle === 90) return { x: y - rect.top, y: rect.right - x, width: rect.height, height: rect.width };
|
||||
if (angle === -90) return { x: rect.bottom - y, y: x - rect.left, width: rect.height, height: rect.width };
|
||||
return { x: x - rect.left, y: y - rect.top, width: rect.width, height: rect.height };
|
||||
}
|
||||
function create(options) {
|
||||
const { stage, settings, persist, toggle, seek, state } = options;
|
||||
let active = false, custom = false, native = false, locked = false, angle = 0, timer;
|
||||
const ancestors = [];
|
||||
for (let el = stage.parentElement; el && el !== document.body; el = el.parentElement) ancestors.push(el);
|
||||
const hud = document.createElement('div'); hud.className = 'fullscreen-hud'; hud.hidden = true;
|
||||
hud.innerHTML = '<button type="button" aria-label="Play or pause">Play / pause</button><input type="range" min="0" max="1000" value="0" aria-label="Seek video"><button type="button" class="fullscreen-orientation" aria-label="Change fullscreen orientation"></button><button type="button" aria-label="Exit fullscreen">Exit</button>';
|
||||
stage.append(hud);
|
||||
const [play, orientation, exit] = hud.querySelectorAll('button'), range = hud.querySelector('input');
|
||||
play.onclick = toggle; exit.onclick = leave;
|
||||
function layout() {
|
||||
if (!active) return;
|
||||
angle = custom ? rotation(settings().fullscreenOrientation, innerWidth, innerHeight) : 0;
|
||||
stage.dataset.fullscreenRotation = String(angle);
|
||||
orientation.textContent = `Orientation: ${normalize(settings().fullscreenOrientation)}`;
|
||||
}
|
||||
async function orient() {
|
||||
if (locked) { screen.orientation?.unlock?.(); locked = false; }
|
||||
const value = normalize(settings().fullscreenOrientation);
|
||||
if (!custom && value !== 'auto') {
|
||||
try { await screen.orientation.lock(value); locked = true; if (!active) { screen.orientation.unlock(); locked = false; return; } }
|
||||
catch { if (!active) return; custom = true; stage.classList.add('orientation-overlay'); }
|
||||
}
|
||||
layout();
|
||||
}
|
||||
orientation.onclick = () => { settings().fullscreenOrientation = choices[(choices.indexOf(normalize(settings().fullscreenOrientation)) + 1) % choices.length]; persist(); orient(); };
|
||||
range.oninput = () => seek(Number(range.value) / 1000 * (state().duration || 0));
|
||||
// Explicit inverse coordinates also work for rotated range controls on WebKit.
|
||||
const rangeSeek = event => {
|
||||
const point = coordinates(event.clientX, event.clientY, range.getBoundingClientRect(), angle);
|
||||
seek(Math.max(0, Math.min(1, point.x / point.width)) * (state().duration || 0));
|
||||
};
|
||||
range.onpointerdown = event => { event.preventDefault(); range.setPointerCapture(event.pointerId); rangeSeek(event); };
|
||||
range.onpointermove = event => { if (range.hasPointerCapture(event.pointerId)) rangeSeek(event); };
|
||||
hud.addEventListener('pointerdown', event => event.stopPropagation());
|
||||
hud.addEventListener('pointerup', event => event.stopPropagation());
|
||||
function cleanup() {
|
||||
active = false; custom = false; native = false; angle = 0; clearInterval(timer);
|
||||
ancestors.forEach(el => el.classList.remove('orientation-ancestor'));
|
||||
hud.hidden = true; stage.classList.remove('orientation-overlay', 'orientation-fullscreen');
|
||||
delete stage.dataset.fullscreenRotation; document.body.classList.remove('landscape-fs');
|
||||
if (locked) { screen.orientation?.unlock?.(); locked = false; }
|
||||
}
|
||||
function leave() {
|
||||
if (document.fullscreenElement === stage) document.exitFullscreen().catch(() => {});
|
||||
cleanup();
|
||||
}
|
||||
async function enter() {
|
||||
if (active) { leave(); return; }
|
||||
active = true; ancestors.forEach(el => el.classList.add('orientation-ancestor')); stage.classList.add('orientation-fullscreen'); hud.hidden = false;
|
||||
document.body.classList.add('landscape-fs');
|
||||
try {
|
||||
if (!stage.requestFullscreen) throw new Error('Custom fullscreen');
|
||||
await stage.requestFullscreen();
|
||||
native = true;
|
||||
} catch { custom = true; stage.classList.add('orientation-overlay'); }
|
||||
if (!active) return;
|
||||
await orient();
|
||||
if (!active) return;
|
||||
timer = setInterval(() => { const s = state(); range.value = String(s.duration ? s.time / s.duration * 1000 : 0); play.textContent = s.paused ? 'Play' : 'Pause'; }, 250);
|
||||
}
|
||||
document.addEventListener('fullscreenchange', () => { if (active && !document.fullscreenElement && native) cleanup(); });
|
||||
document.addEventListener('keydown', event => { if (active && event.key === 'Escape') leave(); });
|
||||
window.addEventListener('resize', layout);
|
||||
return { enter, leave, point: event => coordinates(event.clientX, event.clientY, stage.getBoundingClientRect(), angle) };
|
||||
}
|
||||
const api = { normalize, rotation, coordinates, create };
|
||||
if (typeof module !== 'undefined' && module.exports) module.exports = api; else root.FullscreenOrientation = api;
|
||||
})(globalThis);
|
||||
Reference in New Issue
Block a user