feat(downloads): M5 — Download Center frontend
- api.ts: download types + methods (profiles, enqueue, list/manage, file url, share, usage, index, admin storage/quota) - format.ts: formatBytes / formatSpeed - i18n: en/hu/de downloads.json (trilingual) - Downloads nav module (Download icon, active-count badge, hidden for demo) placed after Messages; urlState page + App render + Header title - DownloadButton: self-contained per-card/player affordance (demo-hidden, reflects the per-user download index: downloaded / in-queue), opens the preset dialog - DownloadDialog: preset picker + optional display name -> enqueue + 'View' toast - ProfileEditor: manage built-in + custom formats (full spec form incl. SponsorBlock) - DownloadCenter: Queue / Library / Shared / (admin) System tabs, live progress via useLiveQuery, usage bar, pause/resume/cancel/delete, save-to-device, rename, share, admin storage dashboard + per-user quota editor - wired DownloadButton into VideoCard + PlayerModal - lib/nav.ts: decoupled navigator so the download toast can jump to the page tsc + vite build clean. Verified in a headless authed browser: page + tabs render, Library shows a real download with usage bar + actions, no console errors.
This commit is contained in:
parent
51158ad9cf
commit
7d1ed24fea
17 changed files with 1551 additions and 1 deletions
|
|
@ -623,6 +623,99 @@ export interface AdminUserRow {
|
|||
created_at: string | null;
|
||||
}
|
||||
|
||||
// --- download center ---
|
||||
export interface DownloadSpec {
|
||||
mode: "av" | "v" | "a"; // audio+video | video-only | audio-only
|
||||
max_height: number | null;
|
||||
container: string | null;
|
||||
audio_format: string | null;
|
||||
vcodec: string | null;
|
||||
embed_subs: boolean;
|
||||
embed_chapters: boolean;
|
||||
embed_thumbnail: boolean;
|
||||
sponsorblock: boolean;
|
||||
}
|
||||
|
||||
export interface DownloadProfile {
|
||||
id: number;
|
||||
name: string;
|
||||
spec: DownloadSpec;
|
||||
is_builtin: boolean;
|
||||
sort_order: number;
|
||||
}
|
||||
|
||||
export interface DownloadAsset {
|
||||
id: number;
|
||||
status: string;
|
||||
title: string | null;
|
||||
uploader: string | null;
|
||||
upload_date: string | null;
|
||||
duration_s: number | null;
|
||||
size_bytes: number | null;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
container: string | null;
|
||||
thumbnail_url: string | null;
|
||||
expires_at: string | null;
|
||||
}
|
||||
|
||||
export type DownloadStatus =
|
||||
| "queued"
|
||||
| "running"
|
||||
| "paused"
|
||||
| "done"
|
||||
| "error"
|
||||
| "canceled";
|
||||
|
||||
export interface DownloadJob {
|
||||
id: number;
|
||||
status: DownloadStatus;
|
||||
progress: number;
|
||||
speed_bps: number | null;
|
||||
eta_s: number | null;
|
||||
phase: string | null;
|
||||
error: string | null;
|
||||
queue_pos: number;
|
||||
created_at: string | null;
|
||||
source_kind: string;
|
||||
source_ref: string;
|
||||
profile_id: number | null;
|
||||
spec: DownloadSpec;
|
||||
display_name: string | null;
|
||||
can_download: boolean;
|
||||
expired: boolean;
|
||||
asset: DownloadAsset | null;
|
||||
shared?: boolean;
|
||||
user_email?: string;
|
||||
}
|
||||
|
||||
export interface DownloadUsage {
|
||||
footprint_bytes: number;
|
||||
active_jobs: number;
|
||||
running_jobs: number;
|
||||
max_bytes: number;
|
||||
max_concurrent: number;
|
||||
max_jobs: number;
|
||||
unlimited: boolean;
|
||||
}
|
||||
|
||||
export interface AdminStorage {
|
||||
ready_files: number;
|
||||
total_bytes: number;
|
||||
total_assets: number;
|
||||
total_cap_bytes: number;
|
||||
per_user: { user_id: number; email: string | null; footprint_bytes: number }[];
|
||||
}
|
||||
|
||||
export interface AdminDownloadQuota {
|
||||
user_id: number;
|
||||
custom: boolean;
|
||||
max_bytes: number;
|
||||
max_concurrent: number;
|
||||
max_jobs: number;
|
||||
unlimited: boolean;
|
||||
}
|
||||
|
||||
export const api = {
|
||||
me: (): Promise<Me> => req("/api/me"),
|
||||
accounts: (): Promise<Account[]> => req("/api/me/accounts"),
|
||||
|
|
@ -914,6 +1007,57 @@ export const api = {
|
|||
updateTag: (id: number, patch: { name?: string; color?: string }) =>
|
||||
req(`/api/tags/${id}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
||||
deleteTag: (id: number) => req(`/api/tags/${id}`, { method: "DELETE" }),
|
||||
|
||||
// --- download center ---
|
||||
downloadProfiles: (): Promise<DownloadProfile[]> => req("/api/downloads/profiles"),
|
||||
createDownloadProfile: (p: { name: string; spec: DownloadSpec }): Promise<DownloadProfile> =>
|
||||
req("/api/downloads/profiles", { method: "POST", body: JSON.stringify(p) }),
|
||||
updateDownloadProfile: (
|
||||
id: number,
|
||||
patch: { name?: string; spec?: DownloadSpec }
|
||||
): Promise<DownloadProfile> =>
|
||||
req(`/api/downloads/profiles/${id}`, { method: "PATCH", body: JSON.stringify(patch) }),
|
||||
deleteDownloadProfile: (id: number) =>
|
||||
req(`/api/downloads/profiles/${id}`, { method: "DELETE" }),
|
||||
|
||||
enqueueDownload: (body: {
|
||||
source: string;
|
||||
profile_id?: number;
|
||||
spec?: DownloadSpec;
|
||||
display_name?: string;
|
||||
}): Promise<DownloadJob> =>
|
||||
req("/api/downloads", { method: "POST", body: JSON.stringify(body) }),
|
||||
downloads: (): Promise<DownloadJob[]> => req("/api/downloads"),
|
||||
downloadsShared: (): Promise<DownloadJob[]> => req("/api/downloads/shared"),
|
||||
downloadUsage: (): Promise<DownloadUsage> => req("/api/downloads/usage"),
|
||||
downloadIndex: (): Promise<Record<string, DownloadStatus>> => req("/api/downloads/index"),
|
||||
pauseDownload: (id: number): Promise<DownloadJob> =>
|
||||
req(`/api/downloads/${id}/pause`, { method: "POST" }, { idempotent: true }),
|
||||
resumeDownload: (id: number): Promise<DownloadJob> =>
|
||||
req(`/api/downloads/${id}/resume`, { method: "POST" }, { idempotent: true }),
|
||||
cancelDownload: (id: number): Promise<DownloadJob> =>
|
||||
req(`/api/downloads/${id}/cancel`, { method: "POST" }, { idempotent: true }),
|
||||
deleteDownload: (id: number) => req(`/api/downloads/${id}`, { method: "DELETE" }),
|
||||
renameDownload: (id: number, display_name: string): Promise<DownloadJob> =>
|
||||
req(`/api/downloads/${id}`, { method: "PATCH", body: JSON.stringify({ display_name }) }),
|
||||
shareDownload: (id: number, email: string): Promise<{ shared_with: string }> =>
|
||||
req(`/api/downloads/${id}/share`, { method: "POST", body: JSON.stringify({ email }) }),
|
||||
unshareDownload: (id: number, email: string) =>
|
||||
req(`/api/downloads/${id}/share/${encodeURIComponent(email)}`, { method: "DELETE" }),
|
||||
downloadFileUrl: (id: number): string => `/api/downloads/${id}/file`,
|
||||
|
||||
// admin
|
||||
adminDownloads: (): Promise<DownloadJob[]> => req("/api/admin/downloads"),
|
||||
adminDownloadStorage: (): Promise<AdminStorage> => req("/api/admin/downloads/storage"),
|
||||
adminGetDownloadQuota: (userId: number): Promise<AdminDownloadQuota> =>
|
||||
req(`/api/admin/downloads/quota/${userId}`),
|
||||
adminSetDownloadQuota: (
|
||||
userId: number,
|
||||
patch: Partial<Pick<AdminDownloadQuota, "max_bytes" | "max_concurrent" | "max_jobs" | "unlimited">>
|
||||
): Promise<AdminDownloadQuota> =>
|
||||
req(`/api/admin/downloads/quota/${userId}`, { method: "PUT", body: JSON.stringify(patch) }),
|
||||
adminResetDownloadQuota: (userId: number): Promise<AdminDownloadQuota> =>
|
||||
req(`/api/admin/downloads/quota/${userId}`, { method: "DELETE" }),
|
||||
};
|
||||
|
||||
export { HttpError };
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue