Files
djk/internal/webui/player-orientation.js
T

151 lines
7.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
(() => {
window.JukuPlayerOrientation = ({panel, video, active}) => {
const node = id => document.getElementById(id);
const rotate = node('mobileRotateBtn'), automatic = node('mobileAutoRotate');
const watch = node('mobileLandscapeBtn');
const permission = node('mobileOrientationPermissionBtn'), hint = node('mobileOrientationHint');
let manual = null, sensed = null, sensedTurn = 90, angle = 0, landscape = false;
let previousViewport = null, sensorKind = null, candidate = '', timer = null, listening = false, granted = false, signature = '';
let enabled = true;
try {enabled = localStorage.getItem('juku.playback.autoRotate') !== 'false';} catch (_) {}
automatic.checked = enabled;
function sensorAvailable() {return window.isSecureContext && typeof window.DeviceOrientationEvent !== 'undefined';}
function permissionNeeded() {return sensorAvailable() && typeof window.DeviceOrientationEvent.requestPermission === 'function' && !granted;}
function clearCandidate() {clearTimeout(timer); timer = null; candidate = '';}
function stopSensor() {
window.removeEventListener('deviceorientation', sense);
listening = false;
clearCandidate();
}
function startSensor() {
if (!active() || !enabled || !sensorAvailable() || permissionNeeded() || listening) return;
window.addEventListener('deviceorientation', sense, {passive: true});
listening = true;
}
function sense(event) {
if (!active() || !enabled || !Number.isFinite(event.beta) || !Number.isFinite(event.gamma)) return;
const beta = event.beta * Math.PI / 180, gamma = event.gamma * Math.PI / 180;
const x = Math.sin(gamma) * Math.cos(beta), y = Math.sin(beta);
if (Math.hypot(x, y) < .65) {clearCandidate(); return;}
let next;
if (Math.abs(x) > Math.abs(y) * 1.4) next = true;
else if (Math.abs(y) > Math.abs(x) * 1.4) next = false;
else {clearCandidate(); return;}
const turn = x > 0 ? -90 : 90, key = String(next) + ':' + (next ? turn : 0);
if (key === sensorKind) {clearCandidate(); return;}
if (manual !== null && sensorKind === null) {sensorKind = key; clearCandidate(); return;}
if (key === candidate) return;
clearCandidate();
candidate = key;
timer = setTimeout(() => {
timer = null;
candidate = '';
if (!active() || !enabled) return;
sensorKind = key;
sensed = next;
sensedTurn = turn;
manual = null;
update();
}, 300);
}
function update() {
const on = active(), width = panel.clientWidth || window.innerWidth, height = panel.clientHeight || window.innerHeight;
const viewportLandscape = width > height;
const videoWidth = video.videoWidth, videoHeight = video.videoHeight;
const known = videoWidth > 0 && videoHeight > 0;
const previousLandscape = landscape;
landscape = on && (manual ?? (enabled && sensed !== null ? sensed : viewportLandscape));
const nextAngle = !on || landscape === viewportLandscape ? 0 : landscape ? sensedTurn : -90;
const viewWidth = nextAngle ? height : width, viewHeight = nextAngle ? width : height;
const nextSignature = [on, width, height, videoWidth, videoHeight, landscape, nextAngle].join(':');
if (signature !== nextSignature) {
signature = nextSignature;
panel.dataset.videoLayout = known ? videoWidth > videoHeight ? 'landscape' : videoWidth < videoHeight ? 'portrait' : 'square' : 'unknown';
node('mobileVideoSize').textContent = known ? videoWidth + ' × ' + videoHeight + ' · ' + (videoWidth > videoHeight ? '横屏视频' : videoWidth < videoHeight ? '竖屏视频' : '方形视频') : '读取视频尺寸…';
panel.classList.toggle('player-rotated', nextAngle !== 0);
panel.classList.toggle('player-landscape', landscape);
for (const [key, value] of Object.entries({'frame-width': width, 'frame-height': height, 'view-height': viewHeight, 'view-width': viewWidth})) panel.style.setProperty('--player-' + key, value + 'px');
panel.style.setProperty('--player-rotation', nextAngle + 'deg');
if (known) panel.style.setProperty('--landscape-watch-top', Math.max(70, Math.min(viewHeight - 190, (viewHeight + Math.min(viewHeight, viewWidth * videoHeight / videoWidth)) / 2 + 18)) + 'px');
}
rotate.disabled = !known;
watch.hidden = !on || !known || videoWidth <= videoHeight || landscape;
rotate.setAttribute('aria-pressed', String(landscape));
rotate.setAttribute('aria-label', landscape ? '竖屏播放' : '横屏播放');
rotate.title = landscape ? '切换为竖屏播放' : '切换为横屏播放';
permission.hidden = !on || !enabled || !permissionNeeded();
node('mobileBackBtn').setAttribute('aria-label', landscape ? '返回竖屏播放' : '返回剧库');
if (nextAngle !== angle || previousLandscape !== landscape) {
angle = nextAngle;
panel.dispatchEvent(new Event('jukuorientationchange'));
}
if (on && enabled) startSensor(); else stopSensor();
if (on && previousViewport === null) previousViewport = viewportLandscape;
}
function viewportChanged() {
if (!active()) return;
const next = (panel.clientWidth || window.innerWidth) > (panel.clientHeight || window.innerHeight);
if (previousViewport !== null && next !== previousViewport) {
if (enabled) manual = null;
sensed = null;
sensorKind = null;
clearCandidate();
}
previousViewport = next;
update();
}
async function allowSensor() {
if (!permissionNeeded()) {startSensor(); return;}
permission.disabled = true;
try {
granted = await window.DeviceOrientationEvent.requestPermission() === 'granted';
hint.textContent = granted ? '已开启方向感应,也可随时手动旋转。' : '方向感应未开启,仍可手动旋转或跟随屏幕方向。';
startSensor();
} catch (_) {hint.textContent = '当前浏览器未开放方向感应,可手动旋转或跟随屏幕方向。';}
finally {permission.disabled = false; update();}
}
function setLandscape(value) {
if (!active()) return;
manual = value;
clearCandidate();
update();
panel.dispatchEvent(new Event('jukuorientationchange'));
}
rotate.addEventListener('click', () => {if (!rotate.disabled) setLandscape(!landscape);});
watch.addEventListener('click', () => {
setLandscape(true);
try {
const request = panel.requestFullscreen?.() || panel.webkitRequestFullscreen?.();
request?.catch?.(() => {});
} catch (_) {}
});
automatic.addEventListener('change', () => {
enabled = automatic.checked;
manual = enabled ? null : landscape;
sensed = null;
sensorKind = null;
try {localStorage.setItem('juku.playback.autoRotate', String(enabled));} catch (_) {}
if (!enabled) stopSensor();
update();
});
permission.addEventListener('click', allowSensor);
for (const type of ['resize', 'orientationchange']) window.addEventListener(type, viewportChanged);
window.screen?.orientation?.addEventListener?.('change', viewportChanged);
window.visualViewport?.addEventListener('resize', viewportChanged);
for (const type of ['loadedmetadata', 'resize', 'emptied']) video.addEventListener(type, update);
for (const type of ['fullscreenchange', 'webkitfullscreenchange']) document.addEventListener(type, viewportChanged);
panel.addEventListener('close', () => {
manual = null;
sensed = null;
sensedTurn = 90;
sensorKind = null;
previousViewport = null;
stopSensor();
update();
});
if (!sensorAvailable()) hint.textContent = '可跟随屏幕方向旋转,也可手动旋转;方向感应需 HTTPS 和浏览器支持。';
return {update, rotation: () => angle, landscape: () => landscape, portrait: () => setLandscape(false)};
};
})();