diff --git a/frontend/app.js b/frontend/app.js index b934e9b..45003f2 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -495,6 +495,9 @@ const Player = { navigator.mediaSession.setActionHandler('seekbackward', (d) => Player.seek(Math.max(0, Player.master.currentTime - (d.seekOffset || 10)))); navigator.mediaSession.setActionHandler('seekforward', (d) => Player.seek(Player.master.currentTime + (d.seekOffset || 10))); } + // In portrait PWA mode, scroll the player into view so it's immediately + // visible without the user having to swipe up manually. + scrollPlayerIntoViewPortrait(); // Restore A-B markers and load related (non-blocking) restoreAbMarkers(); loadRelated(); @@ -1976,6 +1979,52 @@ function showModal(title, bodyNode, actions) { } function closeModal() { $('modal').classList.add('hidden'); } +// ============================================================================ +// Portrait PWA detection & behavioral layer +// Mirrors the CSS media query: (display-mode: standalone) and (orientation: portrait) +// The JS layer adds: +// • A `portrait-pwa` class on .app for any JS-driven conditional logic. +// • Auto-close the sidebar when rotating into portrait-standalone mode. +// • Auto-scroll the player into view when a video starts in portrait mode. +// ============================================================================ +const _portraitPwaMQ = window.matchMedia + ? window.matchMedia('(display-mode: standalone) and (orientation: portrait)') + : { matches: false, addEventListener: () => {} }; + +function isPortraitPWA() { + return _portraitPwaMQ.matches; +} + +function applyPortraitPwaClass() { + const app = document.querySelector('.app'); + if (!app) return; + if (isPortraitPWA()) { + app.classList.add('portrait-pwa'); + } else { + app.classList.remove('portrait-pwa'); + } +} + +// Scroll the player pane to the top of the scrollable .body so it's visible. +// Only fires when in portrait-standalone mode where .body is the scroll root. +function scrollPlayerIntoViewPortrait() { + if (!isPortraitPWA()) return; + const body = document.querySelector('.body'); + if (body) { + body.scrollTo({ top: 0, behavior: 'smooth' }); + } +} + +function setupPortraitPwaWatcher() { + applyPortraitPwaClass(); + _portraitPwaMQ.addEventListener('change', () => { + applyPortraitPwaClass(); + // Rotating into portrait-standalone: close any open sidebar drawer so the + // full-width layout isn't obscured. + if (isPortraitPWA()) closeSidebar(); + }); +} + // ============================================================================ // Events // ============================================================================ @@ -2623,6 +2672,7 @@ async function boot() { wirePlayerEvents(); wireUI(); wireShortcutHelp(); + setupPortraitPwaWatcher(); try { const loaded = await API.loadData(); if (loaded && typeof loaded === 'object') { diff --git a/frontend/styles.css b/frontend/styles.css index 07784b0..02a1434 100755 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -1755,3 +1755,24 @@ input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.25); } .ph-logo { width: 68px; height: 68px; font-size: 26px; } .hero-tagline { font-size: 13px; } } + +/* ============================================================================ + * PORTRAIT PWA — JS-class mirror (.portrait-pwa on .app) + * + * The JS layer (setupPortraitPwaWatcher in app.js) adds / removes + * `.portrait-pwa` on `.app` whenever the media query fires, giving a CSS hook + * that works inside JS-conditional code paths (e.g. scroll position checks, + * layout reads) without re-evaluating matchMedia inline. + * + * These selectors intentionally duplicate nothing from the @media block above + * — they only fill gaps that require the JS class (e.g. interaction states + * that can't be expressed purely with CSS media queries). + * ========================================================================== */ +.portrait-pwa .body { + /* Reinforce scroll snapping so the player stays at the viewport top after + JS scrolls to it; helps on iOS Safari where scroll restoration can fight. */ + scroll-snap-type: y proximity; +} +.portrait-pwa .player-pane { + scroll-snap-align: start; +}