feat(downloads): local-upload UI — drag-drop dialog + progress (E7 S5c)

UploadDialog (drag-drop + native picker + optional display name) opened
from a new Upload button in the queue add-row. Uploads stream through a
new XHR helper (uploadReq in api/core) so the dialog shows real 0-100%
progress with a cancel — fetch/req can't report upload progress. The
upload lands as an already-done job; the S5b file-kind row gates it to
download+share. i18n HU/EN; upload cap/empty errors localize via the
shared structured-detail path.
This commit is contained in:
2026-08-29 18:53:53 +02:00
parent b4ad59274b
commit 584e9ced0c
7 changed files with 358 additions and 1 deletions
+16 -1
View File
@@ -1,7 +1,7 @@
// Download-center domain: the yt-dlp job lifecycle, per-user profiles, share links/recipients, the
// phase-2 editor, storyboards, and the admin storage/quota views. Self-contained — its types only
// reference each other, and its methods only touch the shared `req`/`getActiveAccount` from core.
import { req, getActiveAccount } from "./core";
import { req, uploadReq, getActiveAccount } from "./core";
export interface DownloadSpec {
mode: "av" | "v" | "a"; // audio+video | video-only | audio-only
@@ -183,6 +183,21 @@ export const downloadsApi = {
spec?: DownloadSpec;
display_name?: string;
}): Promise<DownloadJob> => req("/api/downloads", { method: "POST", body: JSON.stringify(body) }),
// Ingest a local file into the library (E7 S5). Multipart via XHR so the dialog can show upload
// progress; any file type is accepted (media gets the full function set, a generic file is
// download-only). Returns the already-`done` job the ingest creates.
uploadDownload: (
file: File,
opts: { displayName?: string; onProgress?: (pct: number) => void; signal?: AbortSignal } = {},
): Promise<DownloadJob> => {
const form = new FormData();
form.append("file", file);
if (opts.displayName) form.append("display_name", opts.displayName);
return uploadReq<DownloadJob>("/api/downloads/upload", form, {
onProgress: opts.onProgress,
signal: opts.signal,
});
},
enqueueEdit: (body: {
source_job_id: number;
edit_spec: EditSpec;