feat: Follow-up: add portrait orientation design for mobile that only triggers in portrait pwa mode

Task #43 completed by ClaudeQueue

ClaudeQueue
This commit is contained in:
Claude Worker
2026-06-30 08:01:54 +00:00
parent 02153d877d
commit 136e21f1fa
2 changed files with 71 additions and 0 deletions

View File

@@ -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') {

View File

@@ -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;
}