Web UI: move download URL bar from page footer into Downloads modal
The page-bottom footer holding the URL input + quality picker + flags was a fixed 50-ish-pixel strip eating screen space on every screen, even though most of the time users aren't starting a new download. Move all of it into the ⬇ Downloads modal as a "new download" section above the active jobs list. Same controls (URL, quality, Fast/Live/ Clips), same element IDs so existing helpers (previewDownload, fullScan, dlLive, refreshTwitchClipsVisibility, updateDlMode) keep working unchanged. URL input auto-focuses on modal open so the paste-and-Enter flow is one motion. Footer shrinks to just the AGPL §13 source notice (required for deployment compliance, ~12px tall now). updateDlMode is hardened with null-checks since the labels only exist while the modal is open. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
0fe6063f6b
commit
4cead6cf93
1 changed files with 64 additions and 24 deletions
|
|
@ -123,8 +123,16 @@
|
||||||
.err-class{font-size:10px;background:#7f1d1d;color:#fecaca;border-radius:3px;padding:1px 6px;text-transform:uppercase;letter-spacing:.4px;flex-shrink:0}
|
.err-class{font-size:10px;background:#7f1d1d;color:#fecaca;border-radius:3px;padding:1px 6px;text-transform:uppercase;letter-spacing:.4px;flex-shrink:0}
|
||||||
.job-hint{font-size:11px;color:#fecaca;background:rgba(127,29,29,.18);border-left:2px solid #f87171;padding:6px 14px 6px 12px;margin:0 14px 6px}
|
.job-hint{font-size:11px;color:#fecaca;background:rgba(127,29,29,.18);border-left:2px solid #f87171;padding:6px 14px 6px 12px;margin:0 14px 6px}
|
||||||
progress{flex:1;height:5px;accent-color:var(--accent);min-width:40px}
|
progress{flex:1;height:5px;accent-color:var(--accent);min-width:40px}
|
||||||
footer{background:var(--panel);border-top:1px solid var(--border);padding:8px 12px;display:flex;gap:8px;align-items:center;flex-shrink:0}
|
footer{background:var(--panel);border-top:1px solid var(--border);padding:6px 12px;display:flex;gap:8px;align-items:center;flex-shrink:0;justify-content:center;min-height:24px}
|
||||||
footer input{flex:1;min-width:0;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:5px 9px;border-radius:4px;font-size:13px}
|
/* "New download" form inside the Downloads modal. Replaces the old
|
||||||
|
bottom-of-page footer; sits between the modal header and the
|
||||||
|
active-jobs list. */
|
||||||
|
.dl-new{padding:10px 14px;display:flex;flex-direction:column;gap:8px}
|
||||||
|
.dl-new-row{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
|
||||||
|
.dl-new-row input[type="url"]{flex:1;min-width:200px;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:6px 9px;border-radius:4px;font-size:13px}
|
||||||
|
.dl-new-row select{background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:5px 6px;font-size:12px}
|
||||||
|
.dl-new-flags{display:flex;gap:12px;flex-wrap:wrap;font-size:12px;color:var(--muted)}
|
||||||
|
.dl-new-flags label{display:flex;align-items:center;gap:4px;cursor:pointer;white-space:nowrap}
|
||||||
.preview-thumb{width:100%;max-width:280px;aspect-ratio:16/9;object-fit:cover;border-radius:4px;background:#000;flex-shrink:0}
|
.preview-thumb{width:100%;max-width:280px;aspect-ratio:16/9;object-fit:cover;border-radius:4px;background:#000;flex-shrink:0}
|
||||||
.empty{text-align:center;color:var(--muted);padding:40px;font-size:13px}
|
.empty{text-align:center;color:var(--muted);padding:40px;font-size:13px}
|
||||||
@media(max-width:640px){
|
@media(max-width:640px){
|
||||||
|
|
@ -154,10 +162,10 @@
|
||||||
#details{max-height:160px;padding:8px 10px}
|
#details{max-height:160px;padding:8px 10px}
|
||||||
.chapters-pane{display:none}
|
.chapters-pane{display:none}
|
||||||
section#content{padding:8px}
|
section#content{padding:8px}
|
||||||
/* Download bar wraps onto two rows so the URL input + button
|
/* Modal "new download" form stacks vertically on small screens so
|
||||||
don't squeeze each other off-screen. */
|
the URL + button + quality picker don't squeeze each other. */
|
||||||
footer{flex-wrap:wrap;padding:6px 10px;gap:6px}
|
.dl-new-row{flex-direction:column;align-items:stretch}
|
||||||
footer input{flex-basis:100%;order:-1}
|
.dl-new-row input[type="url"]{width:100%}
|
||||||
/* Bulk-mode actions stack vertically when active. */
|
/* Bulk-mode actions stack vertically when active. */
|
||||||
.toolbar{flex-wrap:wrap}
|
.toolbar{flex-wrap:wrap}
|
||||||
#bulk-actions{flex-wrap:wrap;gap:4px}
|
#bulk-actions{flex-wrap:wrap;gap:4px}
|
||||||
|
|
@ -218,20 +226,7 @@
|
||||||
</main>
|
</main>
|
||||||
<div id="details"></div>
|
<div id="details"></div>
|
||||||
<footer>
|
<footer>
|
||||||
<input type="url" id="dl-url" placeholder="YouTube URL…" oninput="refreshTwitchClipsVisibility()" onkeydown="if(event.key==='Enter')previewDownload()">
|
<span id="agpl-notice" style="font-size:10px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></span>
|
||||||
<button class="primary" onclick="previewDownload()">⬇ Download</button>
|
|
||||||
<select id="dl-quality" title="Download quality" style="background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:4px;padding:4px 6px;font-size:12px" onchange="updateDlMode()">
|
|
||||||
<option value="best">Best quality</option>
|
|
||||||
<option value="1080p">1080p</option>
|
|
||||||
<option value="720p">720p</option>
|
|
||||||
<option value="480p">480p</option>
|
|
||||||
<option value="360p">360p</option>
|
|
||||||
<option value="music">🎵 Music</option>
|
|
||||||
</select>
|
|
||||||
<label id="fast-mode-label" style="display:flex;align-items:center;gap:4px;font-size:12px;white-space:nowrap;cursor:pointer" title="Stop at the first already-downloaded video. Faster for large channels but may miss new videos if gaps exist in the archive."><input type="checkbox" id="dl-full-scan"> Fast mode</label>
|
|
||||||
<label id="live-mode-label" style="display:flex;align-items:center;gap:4px;font-size:12px;white-space:nowrap;cursor:pointer" title="Record an ongoing live broadcast (Twitch/YouTube Live) from the start instead of joining live. Waits if the stream has not begun yet."><input type="checkbox" id="dl-live"> 🔴 Live</label>
|
|
||||||
<label id="twitch-clips-label" style="display:none;align-items:center;gap:4px;font-size:12px;white-space:nowrap;cursor:pointer" title="Twitch only: pull the channel's Clips listing instead of its VODs. Rewrites the URL to twitch.tv/<channel>/clips before submitting."><input type="checkbox" id="dl-twitch-clips"> Clips only</label>
|
|
||||||
<span id="agpl-notice" style="font-size:10px;color:var(--muted);margin-left:auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis"></span>
|
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
|
@ -759,9 +754,13 @@ function refreshTwitchClipsVisibility(){
|
||||||
function dlQuality(){return document.getElementById('dl-quality')?.value||'best'}
|
function dlQuality(){return document.getElementById('dl-quality')?.value||'best'}
|
||||||
function updateDlMode(){
|
function updateDlMode(){
|
||||||
const isMusic=dlQuality()==='music';
|
const isMusic=dlQuality()==='music';
|
||||||
document.getElementById('fast-mode-label').style.display=isMusic?'none':'flex';
|
// The labels only exist while the Downloads modal is open; nothing to
|
||||||
|
// do when this is called from a stale event handler outside it.
|
||||||
|
const fast=document.getElementById('fast-mode-label');
|
||||||
|
const live=document.getElementById('live-mode-label');
|
||||||
|
if(fast)fast.style.display=isMusic?'none':'flex';
|
||||||
// Live recording makes no sense for music-only mode.
|
// Live recording makes no sense for music-only mode.
|
||||||
document.getElementById('live-mode-label').style.display=isMusic?'none':'flex';
|
if(live)live.style.display=isMusic?'none':'flex';
|
||||||
}
|
}
|
||||||
async function confirmDownload(url,btn){
|
async function confirmDownload(url,btn){
|
||||||
if(btn)btn.closest('.modal-bg').remove();
|
if(btn)btn.closest('.modal-bg').remove();
|
||||||
|
|
@ -1700,17 +1699,58 @@ function renderJobs(jobs,queued,maxConcurrent){
|
||||||
}
|
}
|
||||||
// Open the Downloads modal. If it's already open, do nothing (clicking the
|
// Open the Downloads modal. If it's already open, do nothing (clicking the
|
||||||
// ⬇ button again would otherwise stack duplicate modals).
|
// ⬇ button again would otherwise stack duplicate modals).
|
||||||
|
//
|
||||||
|
// Layout:
|
||||||
|
// ┌──────────────────────────────────────────────────────┐
|
||||||
|
// │ ⬇ Downloads [✕] │
|
||||||
|
// ├──────────────────────────────────────────────────────┤
|
||||||
|
// │ New download │
|
||||||
|
// │ [URL input………………] [quality▾] [⬇ Download] │
|
||||||
|
// │ [☐ Fast] [☐ 🔴 Live] [☐ Clips] │
|
||||||
|
// ├──────────────────────────────────────────────────────┤
|
||||||
|
// │ Active + recent jobs… │
|
||||||
|
// └──────────────────────────────────────────────────────┘
|
||||||
async function openDownloads(){
|
async function openDownloads(){
|
||||||
if(document.querySelector('[data-downloads-modal]'))return;
|
if(document.querySelector('[data-downloads-modal]'))return;
|
||||||
const bg=document.createElement('div');
|
const bg=document.createElement('div');
|
||||||
bg.className='modal-bg';
|
bg.className='modal-bg';
|
||||||
bg.setAttribute('data-downloads-modal','1');
|
bg.setAttribute('data-downloads-modal','1');
|
||||||
bg.onclick=e=>{if(e.target===bg)bg.remove()};
|
bg.onclick=e=>{if(e.target===bg)bg.remove()};
|
||||||
bg.innerHTML=`<div class="modal" style="max-width:720px;width:100%">
|
// Inline the URL/quality/flags form inside the modal. The element IDs
|
||||||
|
// (dl-url, dl-quality, dl-full-scan, dl-live, dl-twitch-clips) match
|
||||||
|
// the names global helpers (previewDownload, fullScan, dlLive,
|
||||||
|
// refreshTwitchClipsVisibility…) already look up by getElementById,
|
||||||
|
// so those helpers keep working unchanged.
|
||||||
|
bg.innerHTML=`<div class="modal" style="max-width:760px;width:100%">
|
||||||
<div class="modal-hdr"><h2>⬇ Downloads</h2><button onclick="this.closest('.modal-bg').remove()">✕</button></div>
|
<div class="modal-hdr"><h2>⬇ Downloads</h2><button onclick="this.closest('.modal-bg').remove()">✕</button></div>
|
||||||
<div id="downloads-body" style="overflow:auto;max-height:80vh;min-height:120px"><em style="color:var(--muted);padding:14px;display:block">Loading…</em></div>
|
<div class="dl-new">
|
||||||
|
<div class="dl-new-row">
|
||||||
|
<input type="url" id="dl-url" placeholder="Paste a YouTube / TikTok / Twitch / … URL…" oninput="refreshTwitchClipsVisibility()" onkeydown="if(event.key==='Enter')previewDownload()">
|
||||||
|
<select id="dl-quality" title="Download quality" onchange="updateDlMode()">
|
||||||
|
<option value="best">Best quality</option>
|
||||||
|
<option value="1080p">1080p</option>
|
||||||
|
<option value="720p">720p</option>
|
||||||
|
<option value="480p">480p</option>
|
||||||
|
<option value="360p">360p</option>
|
||||||
|
<option value="music">🎵 Music</option>
|
||||||
|
</select>
|
||||||
|
<button class="primary" onclick="previewDownload()">⬇ Download</button>
|
||||||
|
</div>
|
||||||
|
<div class="dl-new-flags">
|
||||||
|
<label id="fast-mode-label" title="Stop at the first already-downloaded video. Faster for large channels but may miss new videos if gaps exist in the archive."><input type="checkbox" id="dl-full-scan"> Fast mode</label>
|
||||||
|
<label id="live-mode-label" title="Record an ongoing live broadcast (Twitch/YouTube Live) from the start instead of joining live. Waits if the stream has not begun yet."><input type="checkbox" id="dl-live"> 🔴 Live</label>
|
||||||
|
<label id="twitch-clips-label" style="display:none" title="Twitch only: pull the channel's Clips listing instead of its VODs."><input type="checkbox" id="dl-twitch-clips"> Clips only</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr style="border-color:var(--border);margin:0">
|
||||||
|
<div id="downloads-body" style="overflow:auto;max-height:60vh;min-height:120px"><em style="color:var(--muted);padding:14px;display:block">Loading…</em></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
document.body.appendChild(bg);
|
document.body.appendChild(bg);
|
||||||
|
// Focus the URL input so users can paste + hit Enter immediately. Slight
|
||||||
|
// delay so the focus survives the DOM-attach + any pending click event.
|
||||||
|
setTimeout(()=>document.getElementById('dl-url')?.focus(),0);
|
||||||
|
// Hide the Clips-only checkbox unless the URL is a Twitch channel.
|
||||||
|
refreshTwitchClipsVisibility();
|
||||||
// Repaint immediately from the latest snapshot if we have one — saves a
|
// Repaint immediately from the latest snapshot if we have one — saves a
|
||||||
// round trip. Falls through to /api/progress otherwise.
|
// round trip. Falls through to /api/progress otherwise.
|
||||||
try{
|
try{
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue