Clarify download states and make the empty page useful

This commit is contained in:
Jonathan Sykes
2026-10-03 16:05:42 +08:00
parent 0374e19396
commit cf9b6f4133
9 changed files with 458 additions and 92 deletions

View File

@@ -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(() => {});
}
}
// ============================================================================