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:
@@ -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 <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')) {
|
||||
$('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); },
|
||||
|
||||
@@ -132,6 +132,7 @@
|
||||
<div id="stageLyricsList" class="sl-list"></div>
|
||||
<div class="stage-lyrics-bar">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 ? '<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>
|
||||
${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">
|
||||
<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>` : ''}
|
||||
@@ -450,7 +453,14 @@
|
||||
else if (act === 'resync') resync();
|
||||
else if (act === 'voice') toggleVoice();
|
||||
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 === 'present') presentPrompt();
|
||||
else if (act === 'present-stop') send({ type: 'present', present: null });
|
||||
|
||||
Reference in New Issue
Block a user