Browse Source
Extract shared downloadMedia utility from OutputNode and add download buttons to ImageInput, VideoInput, Annotation, AudioInput, GenerateImage, GenerateVideo, GenerateAudio, and GLBViewer nodes. Refactor OutputNode to use the shared utility. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>handoff-20260429-1057
10 changed files with 190 additions and 45 deletions
@ -0,0 +1,94 @@ |
|||
/** |
|||
* Shared utility for downloading media (images, video, audio) from nodes. |
|||
* Handles both data URLs (base64) and HTTP URLs. |
|||
*/ |
|||
|
|||
/** Infer a file extension from a data URL's MIME type. */ |
|||
function extensionFromDataUrl(dataUrl: string): string { |
|||
const match = dataUrl.match(/^data:([^;,]+)/); |
|||
if (!match) return "bin"; |
|||
const mime = match[1]; |
|||
const map: Record<string, string> = { |
|||
"image/png": "png", |
|||
"image/jpeg": "jpg", |
|||
"image/webp": "webp", |
|||
"image/gif": "gif", |
|||
"video/mp4": "mp4", |
|||
"video/webm": "webm", |
|||
"video/quicktime": "mov", |
|||
"audio/mpeg": "mp3", |
|||
"audio/mp3": "mp3", |
|||
"audio/wav": "wav", |
|||
"audio/ogg": "ogg", |
|||
"audio/aac": "aac", |
|||
"audio/mp4": "m4a", |
|||
}; |
|||
return map[mime] ?? mime.split("/")[1] ?? "bin"; |
|||
} |
|||
|
|||
/** Infer a file extension from a URL path. */ |
|||
function extensionFromUrl(url: string): string { |
|||
try { |
|||
const pathname = new URL(url).pathname; |
|||
const ext = pathname.split(".").pop()?.toLowerCase(); |
|||
if (ext && ext.length <= 5 && ext.length >= 2) return ext; |
|||
} catch { |
|||
// not a valid URL
|
|||
} |
|||
return "bin"; |
|||
} |
|||
|
|||
export type MediaType = "image" | "video" | "audio"; |
|||
|
|||
/** |
|||
* Download media content via an anchor-click approach. |
|||
* |
|||
* @param src - Data URL (base64) or HTTP URL of the media |
|||
* @param mediaType - Hint for file extension when MIME detection fails |
|||
* @param filename - Optional custom filename (without extension) |
|||
*/ |
|||
export async function downloadMedia( |
|||
src: string, |
|||
mediaType: MediaType = "image", |
|||
filename?: string, |
|||
): Promise<void> { |
|||
const fallbackExt: Record<MediaType, string> = { |
|||
image: "png", |
|||
video: "mp4", |
|||
audio: "mp3", |
|||
}; |
|||
|
|||
const isHttp = src.startsWith("http://") || src.startsWith("https://"); |
|||
const ext = isHttp |
|||
? extensionFromUrl(src) |
|||
: extensionFromDataUrl(src); |
|||
const finalExt = ext === "bin" ? fallbackExt[mediaType] : ext; |
|||
const finalName = filename |
|||
? `${filename}.${finalExt}` |
|||
: `${mediaType}-${Date.now()}.${finalExt}`; |
|||
|
|||
if (isHttp) { |
|||
try { |
|||
const response = await fetch(src); |
|||
const blob = await response.blob(); |
|||
const blobUrl = URL.createObjectURL(blob); |
|||
triggerDownload(blobUrl, finalName); |
|||
URL.revokeObjectURL(blobUrl); |
|||
} catch (error) { |
|||
console.error("Failed to download:", error); |
|||
} |
|||
return; |
|||
} |
|||
|
|||
// Data URL — direct download
|
|||
triggerDownload(src, finalName); |
|||
} |
|||
|
|||
function triggerDownload(href: string, filename: string): void { |
|||
const link = document.createElement("a"); |
|||
link.href = href; |
|||
link.download = filename; |
|||
document.body.appendChild(link); |
|||
link.click(); |
|||
document.body.removeChild(link); |
|||
} |
|||
Loading…
Reference in new issue