(() => { 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)}; }; })();