feat(downloads): editor UI — trim/split/crop clips (phase 2)
VideoEditor modal on a finished Library download: HTML5 <video> scrubber, filmstrip timeline (lazy server storyboard sprite) with draggable in/out handles, draggable/resizable crop overlay, per-edit Precise (re-encode) vs Fast (stream-copy) cut toggle, split-into-N fan-out (N trim jobs), optional clip name. Edited clips show a 'Clip' badge; 'editing' phase gets a % bar. New api enqueueEdit/downloadStoryboard/storyboardImageUrl + EditSpec type; editor i18n (en/hu/de).
This commit is contained in:
parent
f0fc542260
commit
04c461837f
9 changed files with 585 additions and 12 deletions
|
|
@ -667,6 +667,22 @@ export type DownloadStatus =
|
|||
| "error"
|
||||
| "canceled";
|
||||
|
||||
// Phase-2 editor recipe. `accurate` only matters for a trim-only cut (a crop always re-encodes).
|
||||
export interface EditSpec {
|
||||
trim?: { start_s: number; end_s?: number };
|
||||
crop?: { x: number; y: number; w: number; h: number };
|
||||
accurate?: boolean;
|
||||
}
|
||||
|
||||
export interface StoryboardMeta {
|
||||
available: boolean;
|
||||
cols?: number;
|
||||
rows?: number;
|
||||
count?: number;
|
||||
interval_s?: number;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export interface DownloadJob {
|
||||
id: number;
|
||||
status: DownloadStatus;
|
||||
|
|
@ -682,6 +698,9 @@ export interface DownloadJob {
|
|||
profile_id: number | null;
|
||||
spec: DownloadSpec;
|
||||
display_name: string | null;
|
||||
job_kind?: string; // "download" (default) | "edit"
|
||||
source_job_id?: number | null; // parent download an edit was cut from
|
||||
edit_spec?: EditSpec | null;
|
||||
can_download: boolean;
|
||||
expired: boolean;
|
||||
asset: DownloadAsset | null;
|
||||
|
|
@ -1027,6 +1046,14 @@ export const api = {
|
|||
display_name?: string;
|
||||
}): Promise<DownloadJob> =>
|
||||
req("/api/downloads", { method: "POST", body: JSON.stringify(body) }),
|
||||
enqueueEdit: (body: {
|
||||
source_job_id: number;
|
||||
edit_spec: EditSpec;
|
||||
display_name?: string;
|
||||
}): Promise<DownloadJob> =>
|
||||
req("/api/downloads/edit", { method: "POST", body: JSON.stringify(body) }),
|
||||
downloadStoryboard: (id: number): Promise<StoryboardMeta> =>
|
||||
req(`/api/downloads/${id}/storyboard`),
|
||||
downloads: (): Promise<DownloadJob[]> => req("/api/downloads"),
|
||||
downloadsShared: (): Promise<DownloadJob[]> => req("/api/downloads/shared"),
|
||||
downloadUsage: (): Promise<DownloadUsage> => req("/api/downloads/usage"),
|
||||
|
|
@ -1051,6 +1078,13 @@ export const api = {
|
|||
const a = getActiveAccount();
|
||||
return a != null ? `/api/downloads/${id}/file?account=${a}` : `/api/downloads/${id}/file`;
|
||||
},
|
||||
// Filmstrip sprite (an <img>/background src → direct nav, so it needs the ?account= wallet gate).
|
||||
storyboardImageUrl: (id: number): string => {
|
||||
const a = getActiveAccount();
|
||||
return a != null
|
||||
? `/api/downloads/${id}/storyboard.jpg?account=${a}`
|
||||
: `/api/downloads/${id}/storyboard.jpg`;
|
||||
},
|
||||
|
||||
// admin
|
||||
adminDownloads: (): Promise<DownloadJob[]> => req("/api/admin/downloads"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue