jobs: download-job UI — panel decision screen + live progress + /jobs renderer

Front half of the download-job feature.

Panel (VideoDownloadPanel): after Fetch expands a playlist and the count is known,
a decision screen — "Found N items" → pick Audio/Video + subfolder → "Download all
as a job" (POST /jobs/download), or "fetch inline to pick individually" (the
existing card grid). A single video still goes straight to the inline card. The
job phase shows live two-phase progress (polled from the job) + a "View in Jobs"
link; it notes the job runs server-side so closing the panel is fine, and it
refreshes the browser as each file lands.

/jobs (DownloadJobDetail + JobsPage dispatch): a `download` job renders a compact
two-phase readout — Metadata and Download bars (processed/total, found/skipped and
saved/failed) + the current item — polled from the job's progress, with a Stop.

Executor tweak: phase-1 meta.done now counts kept (not processed) so both phases
read the same `(done+failed)/total`.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 18:20:07 +00:00
co-authored by Claude Opus 4.8
parent 4e78986e39
commit bc1b799a27
4 changed files with 595 additions and 53 deletions
+6 -3
View File
@@ -73,9 +73,12 @@ export async function executeDownload(params: ExecuteDownloadParams): Promise<vo
for (const u of urls) {
checkAbort();
const info = await reclipInfo(u).catch((): ReclipInfo => ({ error: 'fetch failed' }));
if (info && !info.error) valid.push({ url: u, title: info.title ?? '' });
else progress.meta.failed++;
progress.meta.done++;
if (info && !info.error) {
valid.push({ url: u, title: info.title ?? '' });
progress.meta.done++; // done = kept; failed = skipped (both phases read `(done+failed)/total`)
} else {
progress.meta.failed++;
}
emit();
}
emit(true);