Sidebar drawer ends above the bottom nav so Audio-only is reachable; landscape lyrics (orientation lock, CSS rotation on iOS) in the stage and watch party

This commit is contained in:
Jonathan Sykes
2026-10-15 19:24:26 +08:00
parent ca042ccbe3
commit 23097918e9
5 changed files with 60 additions and 4 deletions

View File

@@ -4635,11 +4635,42 @@ const Notes = (() => {
function setStageLyrics(on) { function setStageLyrics(on) {
data.settings.stageLyrics = !!on; data.settings.stageLyrics = !!on;
persist(); persist();
if (!on) setLandscape(false);
renderStage(true); 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 <body> 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')) { if ($('stageLyricsOff')) {
$('stageLyricsOff').addEventListener('click', () => setStageLyrics(false)); $('stageLyricsOff').addEventListener('click', () => setStageLyrics(false));
$('stageLyricsLand').addEventListener('click', () => setLandscape(!landOn()));
$('stageLyricsAuto').addEventListener('click', () => { $('stageLyricsAuto').addEventListener('click', () => {
data.settings.stageLyricsAutoscroll = !stageAuto(); data.settings.stageLyricsAutoscroll = !stageAuto();
persist(); persist();
@@ -4870,7 +4901,7 @@ const Notes = (() => {
return { return {
load, tick, addBookmark, addNote, handleSyncKey, serviceRefresh, refreshFlags: () => { sv.flagsKey = ''; renderServiceButtons(); setPanelFlagMode(sv.panelFlag && flagsEnabled()); paintFlags(); loadFlags(); }, 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, jumpChapter,
openChapterPicker, 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); }, repeatSection: () => { const cs = chapters().filter(c => c.t <= now()).sort((a, b) => a.t - b.t); Player.seek(abA ?? cs.at(-1)?.t ?? 0); },

View File

@@ -132,6 +132,7 @@
<div id="stageLyricsList" class="sl-list"></div> <div id="stageLyricsList" class="sl-list"></div>
<div class="stage-lyrics-bar"> <div class="stage-lyrics-bar">
<button id="stageLyricsAuto" class="np-btn" type="button" aria-pressed="true">⇅ Auto-scroll on</button> <button id="stageLyricsAuto" class="np-btn" type="button" aria-pressed="true">⇅ Auto-scroll on</button>
<button id="stageLyricsLand" class="np-btn" type="button" aria-pressed="false">↻ Landscape</button>
<button id="stageLyricsOff" class="np-btn" type="button">📺 Show video</button> <button id="stageLyricsOff" class="np-btn" type="button">📺 Show video</button>
</div> </div>
</div> </div>

View File

@@ -4183,6 +4183,17 @@ body.landscape-fs .player-stage { touch-action: none; } /* fullscreen (real or t
@media (hover: none) { @media (hover: none) {
.stage-lyrics-bar { opacity: 1; } .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 ----------------------- /* --- "syncing" spinner beside the Playlists header -----------------------
Playlists paint from localStorage on the first frame; this says the Playlists paint from localStorage on the first frame; this says the

View File

@@ -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-form { gap: 6px; }
html[data-layout="classic"] .search-btn { padding-inline: 12px; font-size: 13px; } html[data-layout="classic"] .search-btn { padding-inline: 12px; font-size: 13px; }
html[data-layout="classic"] #searchInput { padding-inline: 10px; } 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"] .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-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; } html[data-layout="classic"] .player-placeholder { padding: 24px 8px 32px; }

View File

@@ -352,6 +352,8 @@
if (!b) return; if (!b) return;
b.textContent = lyricsOn() ? '📺 Show the video' : '🎤 Lyrics instead of video'; b.textContent = lyricsOn() ? '📺 Show the video' : '🎤 Lyrics instead of video';
b.classList.toggle('on', lyricsOn()); 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 setStatus(t) { const el = $('ptStatus'); if (el) el.textContent = t; }
function sysMsg(text) { st.chat.push({ sys: true, text, at: Date.now() }); renderChat(); } function sysMsg(text) { st.chat.push({ sys: true, text, at: Date.now() }); renderChat(); }
@@ -394,7 +396,8 @@
? `${st.presentHidden ? '<button class="np-btn" type="button" data-act="present-show">🖥 Show the presentation</button>' : ''}${canPresent() ? '<button class="np-btn" type="button" data-act="present">Change link</button><button class="np-btn" type="button" data-act="present-stop">Stop presenting</button>' : ''}` ? `${st.presentHidden ? '<button class="np-btn" type="button" data-act="present-show">🖥 Show the presentation</button>' : ''}${canPresent() ? '<button class="np-btn" type="button" data-act="present">Change link</button><button class="np-btn" type="button" data-act="present-stop">Stop presenting</button>' : ''}`
: canPresent() ? '<button class="np-btn" type="button" data-act="present">🖥 Present a document or website</button>' : ''}</div> : canPresent() ? '<button class="np-btn" type="button" data-act="present">🖥 Present a document or website</button>' : ''}</div>
${isHost() ? `<label class="pt-toggle"><input type="checkbox" data-act="allow" ${st.allowControl ? 'checked' : ''} /> Guests can control playback</label>` : ''} ${isHost() ? `<label class="pt-toggle"><input type="checkbox" data-act="allow" ${st.allowControl ? 'checked' : ''} /> Guests can control playback</label>` : ''}
<button class="np-btn pt-lyrics${lyricsOn() ? ' on' : ''}" type="button" data-act="lyrics">${lyricsOn() ? '📺 Show the video' : '🎤 Lyrics instead of video'}</button> <div class="pt-present"><button class="np-btn pt-lyrics${lyricsOn() ? ' on' : ''}" type="button" data-act="lyrics">${lyricsOn() ? '📺 Show the video' : '🎤 Lyrics instead of video'}</button>
<button class="np-btn pt-land" type="button" data-act="land"${lyricsOn() ? '' : ' hidden'}>${Notes.stageLyrics.landscapeOn?.() ? '↺ Exit landscape' : '↻ Landscape lyrics'}</button></div>
<div class="pt-voice"> <div class="pt-voice">
<button class="np-btn${st.voice.on ? ' on' : ''}" type="button" data-act="voice">${st.voice.on ? '🎤 Leave voice' : '🎤 Join voice'}</button> <button class="np-btn${st.voice.on ? ' on' : ''}" type="button" data-act="voice">${st.voice.on ? '🎤 Leave voice' : '🎤 Join voice'}</button>
${st.voice.on ? `<button class="np-btn" type="button" data-act="mute">${st.voice.muted ? '🔇 Unmute' : '🔈 Mute'}</button>` : ''} ${st.voice.on ? `<button class="np-btn" type="button" data-act="mute">${st.voice.muted ? '🔇 Unmute' : '🔈 Mute'}</button>` : ''}
@@ -450,7 +453,14 @@
else if (act === 'resync') resync(); else if (act === 'resync') resync();
else if (act === 'voice') toggleVoice(); else if (act === 'voice') toggleVoice();
else if (act === 'mute') toggleMute(); else if (act === 'mute') toggleMute();
else if (act === 'lyrics') { Notes.stageLyrics.set(!lyricsOn()); renderLyricsToggle(); } else if (act === 'lyrics') {
const on = !lyricsOn();
Notes.stageLyrics.set(on);
// Phones held upright go straight to landscape lyrics for the sing-along.
if (on && matchMedia('(pointer: coarse) and (orientation: portrait)').matches) Notes.stageLyrics.landscape?.(true);
renderLyricsToggle();
}
else if (act === 'land') { Notes.stageLyrics.landscape?.(!Notes.stageLyrics.landscapeOn?.()); renderLyricsToggle(); }
else if (act === 'leave') leave(true); else if (act === 'leave') leave(true);
else if (act === 'present') presentPrompt(); else if (act === 'present') presentPrompt();
else if (act === 'present-stop') send({ type: 'present', present: null }); else if (act === 'present-stop') send({ type: 'present', present: null });