Clarify download states and make the empty page useful
This commit is contained in:
121
frontend/app.js
121
frontend/app.js
@@ -337,13 +337,13 @@ const SaveQueue = (() => {
|
||||
const get = (id) => prog.get(id) || null;
|
||||
|
||||
function label(p) {
|
||||
if (!p) return '⏳ Saving for offline…';
|
||||
if (p.phase === 'waiting') return '⏳ Waiting for a free slot…';
|
||||
if (!p) return 'Saving for offline…';
|
||||
if (p.phase === 'waiting') return 'Waiting for a free slot…';
|
||||
const mb = (n) => (n / 1048576).toFixed(n >= 1048576 * 100 ? 0 : 1);
|
||||
if (p.phase === 'preparing') return `⏳ Preparing on server… ${Math.round((p.elapsed || 0) / 1000)} s`;
|
||||
if (p.phase === 'verifying') return '✓ Verifying…';
|
||||
if (p.total) return `⬇ ${mb(p.received)} / ${mb(p.total)} MB`;
|
||||
return '⏳ Saving for offline…';
|
||||
if (p.phase === 'preparing') return `Preparing on server… ${Math.round((p.elapsed || 0) / 1000)} s`;
|
||||
if (p.phase === 'verifying') return 'Verifying…';
|
||||
if (p.total) return `${mb(p.received)} / ${mb(p.total)} MB`;
|
||||
return 'Saving for offline…';
|
||||
}
|
||||
function paint(id) {
|
||||
// Only the Downloads view's own rows: search/playlist cards also carry
|
||||
@@ -3769,7 +3769,11 @@ const Notes = (() => {
|
||||
try {
|
||||
const res = await fetch(`/api/notes/${encodeURIComponent(id)}/lyrics/web`, {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ profile: profileName(), overwrite }),
|
||||
body: JSON.stringify((() => {
|
||||
const m = current && current.meta && current.meta.id === id ? current.meta : {};
|
||||
return { profile: profileName(), overwrite, title: m.title || undefined,
|
||||
artist: m.artist || m.channel || m.uploader || undefined, duration: m.duration || undefined };
|
||||
})()),
|
||||
});
|
||||
const j = await res.json().catch(() => null);
|
||||
if (!j || !j.ok) { toast('⚠ ' + ((j && j.error) || `HTTP ${res.status}`), { duration: 5000 }); setStatus(''); return; }
|
||||
@@ -9291,94 +9295,29 @@ function renderDownloads() {
|
||||
els.status.classList.add('hidden');
|
||||
const c = els.cards;
|
||||
c.innerHTML = '';
|
||||
$('listFilterBar').classList.toggle('hidden', !Downloads.getJobs().length);
|
||||
$('batchBar').classList.add('hidden');
|
||||
c.classList.remove('select-mode');
|
||||
|
||||
const active = [...downloadMeta.values()];
|
||||
const paused = SaveQueue.paused();
|
||||
if (!active.length && !paused.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'empty-state';
|
||||
empty.innerHTML = `
|
||||
<div class="empty-icon">⬇</div>
|
||||
<h3 class="empty-title">No active downloads</h3>
|
||||
<p class="empty-desc">Saves in progress show here with live status. Finished videos land in <strong>Saved</strong>.</p>`;
|
||||
const cta = document.createElement('button');
|
||||
cta.className = 'empty-cta';
|
||||
cta.textContent = '💾 View saved videos';
|
||||
cta.addEventListener('click', () => { view = { type: 'saved' }; render(); });
|
||||
empty.appendChild(cta);
|
||||
c.appendChild(empty);
|
||||
return;
|
||||
if (window.DownloadsPage && window.DownloadsPage.render) {
|
||||
window.DownloadsPage.render(c, {
|
||||
downloadsApi: window.Downloads,
|
||||
filter: listFilter,
|
||||
isActive: () => view.type === 'downloads',
|
||||
showModal: typeof showModal === 'function' ? showModal : null,
|
||||
closeModal: typeof closeModal === 'function' ? closeModal : null,
|
||||
onNavigateSaved: () => { view = { type: 'saved' }; render(); },
|
||||
thumbUrlFor: typeof thumbUrlFor === 'function' ? thumbUrlFor : null,
|
||||
refresh: renderList
|
||||
});
|
||||
}
|
||||
|
||||
const note = document.createElement('div');
|
||||
note.className = 'saved-summary';
|
||||
const n = active.length + paused.length;
|
||||
note.innerHTML = `<span class="saved-total">${n}</span><span class="saved-sub">download${n === 1 ? '' : 's'}${paused.length ? ` · ${paused.length} paused` : ' in progress'}</span>`;
|
||||
c.appendChild(note);
|
||||
|
||||
const rowFor = (v, isPaused) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'card downloading dl-row' + (isPaused ? ' dl-paused' : '');
|
||||
row.dataset.id = v.id;
|
||||
row.innerHTML = `
|
||||
<div class="thumb">
|
||||
<img loading="lazy" decoding="async" src="${escapeHtml(v.thumbnail || thumbUrlFor(v.id, v) || '')}" alt="" />
|
||||
<div class="dl-progress"><div class="dl-bar"></div></div>
|
||||
</div>
|
||||
<div class="card-info">
|
||||
<div class="card-title"></div>
|
||||
<div class="card-channel"></div>
|
||||
</div>`;
|
||||
row.querySelector('.card-title').textContent = v.title || v.id;
|
||||
return row;
|
||||
};
|
||||
|
||||
active.forEach((v) => {
|
||||
c.appendChild(rowFor(v, false));
|
||||
// paint the live label/bar now (the worker keeps updating it)
|
||||
SaveQueue.progress(v.id, SaveQueue.get(v.id));
|
||||
const jobs = window.Downloads && window.Downloads.getJobs ? window.Downloads.getJobs() : [];
|
||||
jobs.filter(j => j.status === 'active' || j.status === 'queued').forEach(v => {
|
||||
if (typeof SaveQueue !== 'undefined' && SaveQueue.progress) {
|
||||
SaveQueue.progress(v.id, SaveQueue.get(v.id));
|
||||
}
|
||||
});
|
||||
|
||||
paused.forEach((v) => {
|
||||
const row = rowFor(v, true);
|
||||
row.querySelector('.card-channel').textContent = '⏸ Paused — continues when the connection is back';
|
||||
const acts = document.createElement('div');
|
||||
acts.className = 'dl-actions';
|
||||
const resume = document.createElement('button');
|
||||
resume.type = 'button';
|
||||
resume.className = 'dl-act';
|
||||
resume.textContent = '▶ Resume';
|
||||
resume.onclick = (e) => { e.stopPropagation(); preload(v, { quiet: true }); renderList(); };
|
||||
const cancel = document.createElement('button');
|
||||
cancel.type = 'button';
|
||||
cancel.className = 'dl-act danger';
|
||||
cancel.textContent = 'Cancel';
|
||||
cancel.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
showModal(`Cancel saving “${v.title || v.id}”?`, document.createTextNode('The part already downloaded will be deleted.'), [
|
||||
{ label: 'Keep', onClick: closeModal },
|
||||
{ label: 'Cancel save', danger: true, onClick: () => { SaveQueue.cancel(v.id); closeModal(); renderList(); } },
|
||||
]);
|
||||
};
|
||||
acts.append(resume, cancel);
|
||||
row.querySelector('.card-info').appendChild(acts);
|
||||
c.appendChild(row);
|
||||
});
|
||||
|
||||
// Fill in how far each paused save got (read from the partial on disk).
|
||||
if (paused.length && window.OPFS && window.OPFS.listPartials) {
|
||||
window.OPFS.listPartials().then((parts) => {
|
||||
for (const p of parts) {
|
||||
const row = c.querySelector(`.card.dl-paused[data-id="${CSS.escape(p.id)}"]`);
|
||||
if (!row || !p.size) continue;
|
||||
const bar = row.querySelector('.dl-bar');
|
||||
bar.classList.add('determinate');
|
||||
bar.style.width = Math.max(1, Math.min(100, (p.received / p.size) * 100)) + '%';
|
||||
row.querySelector('.card-channel').textContent =
|
||||
`⏸ Paused at ${(p.received / 1048576).toFixed(0)} / ${(p.size / 1048576).toFixed(0)} MB — continues when the connection is back`;
|
||||
}
|
||||
}).catch(() => {});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user