Admin portrait: top bar clears the notch, bottom bar's last tab stops inside the edge, song cards with stacked full-width actions

This commit is contained in:
Jonathan Sykes
2026-10-15 12:07:19 +08:00
parent 7ceefa3d6a
commit 92ce7589f9

View File

@@ -412,17 +412,44 @@
.tabs .tab { flex: none; }
}
.nav { display: none; }
.song-info { min-width: 0; flex: 1; }
.song-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.song-edit { display: none; }
.m-only { display: none; }
@media (max-width: 760px) {
.tabs, .top .desk-only { display: none; }
.top .brand { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.top .back-app, .top #logoutBtn { padding-inline: 10px; flex: none; }
.top { padding: 10px 14px; }
.top { padding: calc(10px + env(safe-area-inset-top, 0px)) calc(14px + env(safe-area-inset-right, 0px)) 10px calc(14px + env(safe-area-inset-left, 0px)); }
.deck { top: calc(54px + env(safe-area-inset-top, 0px)); }
/* Song cards: what it is on top, every action full width beneath it. */
#songList { display: grid; gap: 12px; }
.song-row { display: grid; gap: 14px; padding: 16px; border-radius: 16px; border: 0; background: var(--bg-2); }
.song-row:hover { border-color: transparent; }
.song-open { display: grid; grid-template-columns: minmax(0, 1fr); gap: 8px; min-height: 0; flex-basis: auto; }
.song-row b { font-size: 17px; line-height: 1.3; white-space: normal; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
.song-row small { display: block; margin-top: 2px; font-size: 14px; color: var(--text-2); }
.song-row .st { justify-self: start; font-size: 12px; padding: 4px 10px; }
.song-actions { display: grid; grid-template-columns: 1fr; gap: 8px; }
.song-actions .btn { width: 100%; min-height: 46px; justify-content: center; font-size: 15px; margin: 0; }
.song-edit { display: inline-flex; }
.m-only { display: inline; } .d-only { display: none; }
.nav {
position: fixed; left: 0; right: 0; bottom: 0; z-index: 50; display: flex; gap: 4px;
height: calc(var(--nav-h) + var(--safe)); padding: 6px 6px var(--safe); overflow-x: auto; overflow-y: hidden;
overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch; scrollbar-width: thin;
overscroll-behavior-x: contain; -webkit-overflow-scrolling: touch; scrollbar-width: none;
scroll-snap-type: x proximity; scroll-padding-inline: 6px;
padding-left: calc(6px + env(safe-area-inset-left, 0px)); padding-right: calc(6px + env(safe-area-inset-right, 0px));
background: var(--bg-1); border-top: 1px solid var(--line-2);
}
.nav::-webkit-scrollbar { display: none; }
/* A flex row drops its end padding when it scrolls; this spacer keeps the
last tab clear of the right edge. */
.nav::after { content: ""; flex: 0 0 6px; }
.nav .tab { scroll-snap-align: start; }
.nav .tab:last-of-type { scroll-snap-align: end; }
.nav .tab { flex: 0 0 68px; min-width: 68px; min-height: 44px; flex-direction: column; justify-content: center; gap: 3px; height: auto; padding: 4px 3px; font-size: 10px; line-height: 1.05; text-align: center; white-space: normal; border-radius: 12px; }
.nav .tab .badge { position: absolute; top: 2px; left: 55%; }
.steps { grid-template-columns: 1fr; }
@@ -1628,10 +1655,13 @@
const q = $('songFilter').value.trim().toLowerCase();
const rows = SONGS.filter((r) => !q || (r.title + ' ' + (r.sub || '')).toLowerCase().includes(q));
$('songList').innerHTML = rows.length ? rows.map((r) => `<div class="song-row"><button type="button" class="song-open" data-open="${esc(r.id)}">
<span style="min-width:0;flex:1"><b>${esc(r.title)}</b><small>${esc(r.sub || '')}</small></span>
<span class="song-info"><b>${esc(r.title)}</b><small>${esc(r.sub || '')}</small></span>
<span class="st ${r.lines > 0 ? 'has' : ''}">${r.lines > 0 ? `${r.lines} lines` : r.lines === 0 ? 'no lyrics' : 'open'}</span></button>
${r.sourceUrl ? `<a class="btn sm" href="${esc(r.sourceUrl)}" target="_blank" rel="noopener">Source</a><button type="button" class="btn sm" data-redownload="${esc(r.id)}">Re-download from source</button>` : ''}
<button type="button" class="btn sm whisper-request" data-whisper="${esc(r.id)}" aria-label="Transcribe ${esc(r.title)} with Whisper">Whisper</button></div>`).join('')
<div class="song-actions">
<button type="button" class="btn sm pri song-edit" data-open="${esc(r.id)}">${r.lines > 0 ? 'Edit lyrics' : 'Add lyrics'}</button>
<button type="button" class="btn sm whisper-request" data-whisper="${esc(r.id)}" aria-label="Transcribe ${esc(r.title)} with Whisper">Whisper</button>
${r.sourceUrl ? `<button type="button" class="btn sm" data-redownload="${esc(r.id)}">Re-download from source</button><a class="btn sm" href="${esc(r.sourceUrl)}" target="_blank" rel="noopener"><span class="d-only">Source</span><span class="m-only">Open source page</span></a>` : ''}
</div></div>`).join('')
: '<div class="muted" style="padding:10px">Nothing matches — paste a link above to open any YouTube song.</div>';
paintWhisper();
}
@@ -1790,7 +1820,7 @@
document.querySelectorAll('[data-whisper]').forEach(btn => {
const job = whisperJobs.get(btn.dataset.whisper);
btn.disabled = whisperStarting.has(btn.dataset.whisper) || activeWhisper(job);
btn.textContent = job?.status === 'running' ? 'Transcribing…' : job?.status === 'queued' ? 'Queued' : 'Whisper';
btn.textContent = job?.status === 'running' ? 'Transcribing…' : job?.status === 'queued' ? 'Queued' : (matchMedia('(max-width: 760px)').matches ? 'Transcribe with Whisper' : 'Whisper');
});
const job = whisperJobs.get(LE.id);
$('whisperBtn').disabled = whisperStarting.has(LE.id) || activeWhisper(job);