diff --git a/frontend/app.js b/frontend/app.js index 89929d0..cc0eb8b 100755 --- a/frontend/app.js +++ b/frontend/app.js @@ -4635,11 +4635,42 @@ const Notes = (() => { function setStageLyrics(on) { data.settings.stageLyrics = !!on; persist(); + if (!on) setLandscape(false); renderStage(true); } + // Landscape lyrics (sing-alongs on a phone): lock the real orientation where + // the browser allows it (Android, after fullscreen); iOS can't lock, so the + // view is rotated in CSS while the phone is held upright. + const landOn = () => !!$('stageLyrics')?.classList.contains('landscape'); + async function setLandscape(on) { + const box = $('stageLyrics'); if (!box || on === landOn()) return; + // The player stage is its own stacking context; lift the view to
so + // it covers the party panel and nav, and put it back on exit. + if (on) { setLandscape.home = setLandscape.home || { parent: box.parentNode, next: box.nextSibling }; document.body.append(box); } + else if (setLandscape.home) { setLandscape.home.parent.insertBefore(box, setLandscape.home.next); setLandscape.home = null; } + box.classList.toggle('landscape', !!on); + document.body.classList.toggle('lyrics-landscape', !!on); + const b = $('stageLyricsLand'); + if (b) { b.setAttribute('aria-pressed', String(!!on)); b.textContent = on ? '↺ Exit landscape' : '↻ Landscape'; } + try { + if (on && document.documentElement.requestFullscreen && screen.orientation?.lock) { + if (!document.fullscreenElement) await document.documentElement.requestFullscreen({ navigationUI: 'hide' }); + await screen.orientation.lock('landscape'); + } else if (!on) { + try { screen.orientation?.unlock?.(); } catch { /* not locked */ } + if (document.fullscreenElement) await document.exitFullscreen(); + } + } catch { /* no lock (iOS, desktop): the CSS rotation covers it */ } + renderStage(true); + setTimeout(() => renderStage(true), 400); // after the rotation settles + if (typeof Party !== 'undefined' && Party.renderLyricsToggle) Party.renderLyricsToggle(); + } + document.addEventListener('fullscreenchange', () => { if (!document.fullscreenElement && landOn() && screen.orientation?.type?.startsWith('portrait')) setLandscape(false); }); + if ($('stageLyricsOff')) { $('stageLyricsOff').addEventListener('click', () => setStageLyrics(false)); + $('stageLyricsLand').addEventListener('click', () => setLandscape(!landOn())); $('stageLyricsAuto').addEventListener('click', () => { data.settings.stageLyricsAutoscroll = !stageAuto(); persist(); @@ -4870,7 +4901,7 @@ const Notes = (() => { return { load, tick, addBookmark, addNote, handleSyncKey, serviceRefresh, refreshFlags: () => { sv.flagsKey = ''; renderServiceButtons(); setPanelFlagMode(sv.panelFlag && flagsEnabled()); paintFlags(); loadFlags(); }, - stageLyrics: { render: renderStage, set: setStageLyrics, on: stageOn }, + stageLyrics: { render: renderStage, set: setStageLyrics, on: stageOn, landscape: setLandscape, landscapeOn: landOn }, jumpChapter, openChapterPicker, repeatSection: () => { const cs = chapters().filter(c => c.t <= now()).sort((a, b) => a.t - b.t); Player.seek(abA ?? cs.at(-1)?.t ?? 0); }, diff --git a/frontend/index.html b/frontend/index.html index d25e756..47bf877 100755 --- a/frontend/index.html +++ b/frontend/index.html @@ -132,6 +132,7 @@ diff --git a/frontend/styles.css b/frontend/styles.css index 10a9071..d222b49 100755 --- a/frontend/styles.css +++ b/frontend/styles.css @@ -4183,6 +4183,17 @@ body.landscape-fs .player-stage { touch-action: none; } /* fullscreen (real or t @media (hover: none) { .stage-lyrics-bar { opacity: 1; } } +/* Landscape lyrics: the whole screen. Held upright (iOS can't lock the + orientation), the view is turned a quarter so it reads sideways. */ +.stage-lyrics.landscape { position: fixed; inset: 0; z-index: 900; background: #000; } +.stage-lyrics.landscape .stage-lyrics-bar { padding-bottom: calc(8px + env(safe-area-inset-bottom)); } +@media (orientation: portrait) { + .stage-lyrics.landscape { + inset: auto; top: 0; left: 100vw; width: 100vh; width: 100dvh; height: 100vw; + transform-origin: top left; transform: rotate(90deg); + } +} +body.lyrics-landscape { overflow: hidden; } /* --- "syncing" spinner beside the Playlists header ----------------------- Playlists paint from localStorage on the first frame; this says the diff --git a/frontend/theme-classic.css b/frontend/theme-classic.css index f9d8252..ef82ad5 100644 --- a/frontend/theme-classic.css +++ b/frontend/theme-classic.css @@ -201,7 +201,10 @@ html[data-layout="classic"][data-density="compact"] .thumb { width: 88px; } html[data-layout="classic"] .search-form { gap: 6px; } html[data-layout="classic"] .search-btn { padding-inline: 12px; font-size: 13px; } html[data-layout="classic"] #searchInput { padding-inline: 10px; } - html[data-layout="classic"] .sidebar { padding-top: calc(24px + env(safe-area-inset-top)); padding-bottom: calc(24px + env(safe-area-inset-bottom)); z-index: 350; } + /* The bottom nav and mini player float over the drawer's bottom edge: end + the scroll above them, or the last item (Audio-only) sits underneath and + springs back out of reach. Both heights already include the home bar. */ + html[data-layout="classic"] .sidebar { padding-top: calc(24px + env(safe-area-inset-top)); padding-bottom: calc(24px + max(env(safe-area-inset-bottom), var(--classic-nav-height) + var(--classic-mini-height))); z-index: 350; } html[data-layout="classic"] .sidebar-backdrop { z-index: 340; } html[data-layout="classic"] .player-pane { padding-inline: max(16px, env(safe-area-inset-left)) max(16px, env(safe-area-inset-right)); } html[data-layout="classic"] .player-placeholder { padding: 24px 8px 32px; } diff --git a/frontend/watch-party-client.js b/frontend/watch-party-client.js index ac7ca7a..e9d4ee1 100644 --- a/frontend/watch-party-client.js +++ b/frontend/watch-party-client.js @@ -352,6 +352,8 @@ if (!b) return; b.textContent = lyricsOn() ? '📺 Show the video' : '🎤 Lyrics instead of video'; b.classList.toggle('on', lyricsOn()); + const l = document.querySelector('#partyPanel .pt-land'); + if (l) { l.hidden = !lyricsOn(); l.textContent = Notes.stageLyrics.landscapeOn?.() ? '↺ Exit landscape' : '↻ Landscape lyrics'; } } function setStatus(t) { const el = $('ptStatus'); if (el) el.textContent = t; } function sysMsg(text) { st.chat.push({ sys: true, text, at: Date.now() }); renderChat(); } @@ -394,7 +396,8 @@ ? `${st.presentHidden ? '' : ''}${canPresent() ? '' : ''}` : canPresent() ? '' : ''} ${isHost() ? `` : ''} - +