Add selectable fullscreen orientation and an iPhone landscape overlay
This commit is contained in:
@@ -6990,11 +6990,13 @@ const Gestures = (() => {
|
||||
showHud(`<div class="gh-seek ${side}">${d < 0 ? '⟲' : '⟳'} ${Math.abs(d)} s</div>`);
|
||||
}
|
||||
|
||||
const point = e => window.playerFullscreen?.point(e) || FullscreenOrientation.coordinates(e.clientX, e.clientY, stage.getBoundingClientRect());
|
||||
function onTap(e) {
|
||||
const p = point(e);
|
||||
const r = stage.getBoundingClientRect();
|
||||
const x = (e.clientX - r.left) / r.width;
|
||||
const x = point(e).x / point(e).width;
|
||||
const now = performance.now();
|
||||
if (now - st.lastTap < 320 && Math.abs(e.clientX - st.lastX) < 60) {
|
||||
if (now - st.lastTap < 320 && Math.abs(p.x - st.lastX) < 60) {
|
||||
st.lastTap = 0;
|
||||
if (x < 1 / 3) seekBy(-10, 'left');
|
||||
else if (x > 2 / 3) seekBy(10, 'right');
|
||||
@@ -7003,12 +7005,12 @@ const Gestures = (() => {
|
||||
return;
|
||||
}
|
||||
st.lastTap = now;
|
||||
st.lastX = e.clientX;
|
||||
st.lastX = p.x;
|
||||
}
|
||||
|
||||
stage.addEventListener('pointerdown', (e) => {
|
||||
if (e.pointerType !== 'touch') return;
|
||||
pts.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
||||
pts.set(e.pointerId, { x: point(e).x, y: point(e).y });
|
||||
st.moved = false;
|
||||
if (pts.size === 2) {
|
||||
const [a, b] = [...pts.values()];
|
||||
@@ -7016,23 +7018,23 @@ const Gestures = (() => {
|
||||
st.swipe = null;
|
||||
} else if (pts.size === 1 && isFs()) {
|
||||
const r = stage.getBoundingClientRect();
|
||||
const right = (e.clientX - r.left) / r.width > 0.5;
|
||||
st.swipe = { y0: e.clientY, right, v0: data.settings.volume ?? 1, b0: st.bright, h: r.height, active: false };
|
||||
const right = point(e).x / point(e).width > 0.5;
|
||||
st.swipe = { y0: point(e).y, right, v0: data.settings.volume ?? 1, b0: st.bright, h: point(e).height, active: false };
|
||||
}
|
||||
});
|
||||
|
||||
stage.addEventListener('pointermove', (e) => {
|
||||
if (!pts.has(e.pointerId)) return;
|
||||
const p = pts.get(e.pointerId);
|
||||
if (Math.hypot(e.clientX - p.x, e.clientY - p.y) > 10) st.moved = true;
|
||||
pts.set(e.pointerId, { ...p, cx: e.clientX, cy: e.clientY });
|
||||
if (Math.hypot(point(e).x - p.x, point(e).y - p.y) > 10) st.moved = true;
|
||||
pts.set(e.pointerId, { ...p, cx: point(e).x, cy: point(e).y });
|
||||
if (st.pinch && pts.size >= 2) {
|
||||
const [a, b] = [...pts.values()].map((q) => ({ x: q.cx ?? q.x, y: q.cy ?? q.y }));
|
||||
const d = Math.hypot(a.x - b.x, a.y - b.y);
|
||||
st.zoom = Math.max(1, Math.min(3, st.pinch.zoom * (d / (st.pinch.d || 1))));
|
||||
const cx = (a.x + b.x) / 2, cy = (a.y + b.y) / 2;
|
||||
const r = stage.getBoundingClientRect();
|
||||
const maxX = (r.width * (st.zoom - 1)) / 2, maxY = (r.height * (st.zoom - 1)) / 2;
|
||||
const maxX = (point(e).width * (st.zoom - 1)) / 2, maxY = (point(e).height * (st.zoom - 1)) / 2;
|
||||
st.panX = Math.max(-maxX, Math.min(maxX, st.pinch.panX + (cx - st.pinch.cx)));
|
||||
st.panY = Math.max(-maxY, Math.min(maxY, st.pinch.panY + (cy - st.pinch.cy)));
|
||||
applyView();
|
||||
@@ -7042,7 +7044,7 @@ const Gestures = (() => {
|
||||
}
|
||||
const sw = st.swipe;
|
||||
if (!sw) return;
|
||||
const dy = sw.y0 - e.clientY;
|
||||
const dy = sw.y0 - point(e).y;
|
||||
if (!sw.active && Math.abs(dy) < 16) return;
|
||||
sw.active = true;
|
||||
const delta = dy / (sw.h * 0.8);
|
||||
@@ -9802,6 +9804,13 @@ async function renderSettings() {
|
||||
];
|
||||
const groups = [...wrap.querySelectorAll(':scope > .set-group')];
|
||||
definitions.forEach(([id, title, cluster, summary, icon], i) => SettingsSections.register({ id, title, cluster, summary, icon, render(container) { container.append(groups[i]); } }));
|
||||
SettingsSections.register({ id: 'video-fullscreen', title: 'Video & fullscreen', cluster: 'Listening', icon: '<path d="M4 9V4h5m6 0h5v5M4 15v5h5m6 0h5v-5"/>', summary: () => `Fullscreen: ${FullscreenOrientation.normalize(data.settings.fullscreenOrientation)}`, render(container) {
|
||||
const row = document.createElement('label'); row.className = 'set-row';
|
||||
row.innerHTML = '<span>Fullscreen orientation</span><select id="setFullscreenOrientation"><option value="auto">Auto (follow device)</option><option value="landscape">Landscape</option><option value="portrait">Portrait</option></select>';
|
||||
const select = row.querySelector('select'); select.value = FullscreenOrientation.normalize(data.settings.fullscreenOrientation);
|
||||
select.onchange = () => { data.settings.fullscreenOrientation = select.value; persist(); };
|
||||
container.append(row);
|
||||
} });
|
||||
SettingsSections.mount(wrap, { settings: data.settings, data, persist, toast });
|
||||
|
||||
// ---- Wire appearance / accessibility ----
|
||||
@@ -11424,47 +11433,14 @@ document.querySelectorAll('.chip').forEach((c) => {
|
||||
els.loopBtn.addEventListener('click', toggleLoopOne);
|
||||
els.repeatBtn.addEventListener('click', toggleRepeat);
|
||||
if ($('fsMini')) $('fsMini').addEventListener('click', () => els.fsBtn.click());
|
||||
els.fsBtn.addEventListener('click', () => {
|
||||
const stage = els.video.parentElement;
|
||||
const video = els.video;
|
||||
// iOS Safari — including an installed PWA running in standalone mode —
|
||||
// does not implement the standard Fullscreen API for arbitrary elements.
|
||||
// stage.requestFullscreen is simply undefined there, so the button did
|
||||
// nothing (this is the iPhone-in-portrait-PWA bug report). WebKit instead
|
||||
// exposes a video-only, non-standard fullscreen API that *does* work in
|
||||
// standalone mode: HTMLVideoElement.webkitEnterFullscreen/ExitFullscreen.
|
||||
// Try the standard API first everywhere else, then fall back to the
|
||||
// WebKit video API before giving up.
|
||||
if (document.fullscreenElement || video.webkitDisplayingFullscreen) {
|
||||
try {
|
||||
if (document.exitFullscreen) document.exitFullscreen()?.catch(() => {});
|
||||
else if (video.webkitExitFullscreen) video.webkitExitFullscreen();
|
||||
} catch { /* e.g. InvalidStateError — nothing more we can do */ }
|
||||
return;
|
||||
}
|
||||
if (stage.requestFullscreen) {
|
||||
// requestFullscreen() can both throw synchronously (e.g.
|
||||
// InvalidStateError when preconditions like active user-gesture
|
||||
// transient activation aren't met) and return a promise that rejects
|
||||
// asynchronously. Guard against both instead of letting either surface
|
||||
// as an uncaught error.
|
||||
try {
|
||||
stage.requestFullscreen()?.catch(() => {});
|
||||
} catch { /* no-op — fullscreen simply won't engage this time */ }
|
||||
} else if (video.webkitEnterFullscreen) {
|
||||
if (Player.mode === 'audio') {
|
||||
toast('Fullscreen isn’t available in audio-only mode');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// Throws InvalidStateError if the element has no loaded media (e.g.
|
||||
// no video source yet) — nothing to show fullscreen in that case.
|
||||
video.webkitEnterFullscreen();
|
||||
} catch { /* no-op — no media loaded to go fullscreen with */ }
|
||||
} else {
|
||||
toast('Fullscreen isn’t supported on this device');
|
||||
}
|
||||
const fullscreen = FullscreenOrientation.create({
|
||||
stage: els.video.parentElement, video: els.video,
|
||||
settings: () => data.settings, persist,
|
||||
toggle: () => Player.toggle(), seek: time => Player.seek(time),
|
||||
state: () => ({ time: Player.master.currentTime, duration: Player.master.duration, paused: Player.master.paused }),
|
||||
});
|
||||
window.playerFullscreen = fullscreen;
|
||||
els.fsBtn.addEventListener('click', () => fullscreen.enter());
|
||||
|
||||
els.seek.addEventListener('input', () => {
|
||||
const dur = Player.master.duration || 0;
|
||||
|
||||
Reference in New Issue
Block a user