[dyad] Improved download flexibility - wrote 1 file(s)

This commit is contained in:
[dyad]
2026-01-18 11:34:41 +01:00
parent c74f5dcc28
commit 18dd995fc4

View File

@@ -135,7 +135,7 @@ export function ImageConverter() {
setFilenames(newFilenames); setFilenames(newFilenames);
}; };
const generateFinalFilename = (index: number) => { const generateFinalFilename = (index: number, withDimensions: boolean = false) => {
const baseName = filenames[index] || "filename"; const baseName = filenames[index] || "filename";
let finalName = `${prefix}${baseName}${suffix}`; let finalName = `${prefix}${baseName}${suffix}`;
@@ -144,52 +144,64 @@ export function ImageConverter() {
finalName += `${counter}`; finalName += `${counter}`;
} }
if (withDimensions && width && height) {
finalName += `_${width}x${height}`;
}
return finalName; return finalName;
}; };
const handleConvertAndDownload = async () => { const convertAndDownload = (image: File, previewUrl: string, index: number) => {
if (images.length === 0 || !width || !height) { return new Promise<void>((resolve, reject) => {
toast.error("Please upload images and set dimensions."); const img = new Image();
img.crossOrigin = "anonymous";
img.src = previewUrl;
img.onload = () => {
const canvas = document.createElement("canvas");
const targetWidth = width ? Number(width) : img.naturalWidth;
const targetHeight = height ? Number(height) : img.naturalHeight;
canvas.width = targetWidth;
canvas.height = targetHeight;
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
const mimeType = `image/${format}`;
const dataUrl = canvas.toDataURL(mimeType, format === 'png' ? undefined : quality / 100);
const link = document.createElement("a");
link.href = dataUrl;
const dimensionSuffix = width && height ? `_${width}x${height}` : '';
link.download = `${generateFinalFilename(index)}${dimensionSuffix}.${format}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
resolve();
} else {
reject(new Error(`Could not process ${image.name}.`));
}
};
img.onerror = () => {
reject(new Error(`Failed to load ${image.name} for conversion.`));
};
});
};
const handleConvertAndDownloadAll = async () => {
if (images.length === 0) {
toast.error("Please upload images first.");
return; return;
} }
setIsConverting(true); setIsConverting(true);
toast.info(`Starting conversion for ${images.length} images...`); toast.info(`Starting conversion for ${images.length} images...`);
const conversionPromises = images.map((image, index) => { const conversionPromises = images.map((image, index) =>
return new Promise<void>((resolve, reject) => { convertAndDownload(image, previewUrls[index], index)
const previewUrl = previewUrls[index]; );
const img = new Image();
img.crossOrigin = "anonymous";
img.src = previewUrl;
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = Number(width);
canvas.height = Number(height);
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.drawImage(img, 0, 0, Number(width), Number(height));
const mimeType = `image/${format}`;
const dataUrl = canvas.toDataURL(mimeType, format === 'png' ? undefined : quality / 100);
const link = document.createElement("a");
link.href = dataUrl;
const finalFilename = generateFinalFilename(index);
link.download = `${finalFilename}_${width}x${height}.${format}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
resolve();
} else {
reject(new Error(`Could not process ${image.name}.`));
}
};
img.onerror = () => {
reject(new Error(`Failed to load ${image.name} for conversion.`));
};
});
});
try { try {
await Promise.all(conversionPromises); await Promise.all(conversionPromises);
@@ -206,50 +218,11 @@ export function ImageConverter() {
}; };
const handleConvertAndDownloadSingle = async (index: number) => { const handleConvertAndDownloadSingle = async (index: number) => {
if (!width || !height) {
toast.error("Please set dimensions before downloading.");
return;
}
setConvertingIndex(index); setConvertingIndex(index);
toast.info(`Starting conversion for ${filenames[index]}...`); toast.info(`Starting conversion for ${filenames[index]}...`);
const image = images[index];
const previewUrl = previewUrls[index];
try { try {
const img = new Image(); await convertAndDownload(images[index], previewUrls[index], index);
img.crossOrigin = "anonymous";
img.src = previewUrl;
await new Promise<void>((resolve, reject) => {
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = Number(width);
canvas.height = Number(height);
const ctx = canvas.getContext("2d");
if (ctx) {
ctx.drawImage(img, 0, 0, Number(width), Number(height));
const mimeType = `image/${format}`;
const dataUrl = canvas.toDataURL(mimeType, format === 'png' ? undefined : quality / 100);
const link = document.createElement("a");
link.href = dataUrl;
const finalFilename = generateFinalFilename(index);
link.download = `${finalFilename}_${width}x${height}.${format}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
resolve();
} else {
reject(new Error(`Could not process ${image.name}.`));
}
};
img.onerror = () => {
reject(new Error(`Failed to load ${image.name} for conversion.`));
};
});
toast.success(`Successfully exported ${filenames[index]}!`); toast.success(`Successfully exported ${filenames[index]}!`);
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
@@ -281,11 +254,11 @@ export function ImageConverter() {
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="width">Width (px)</Label> <Label htmlFor="width">Width (px)</Label>
<Input id="width" type="number" placeholder="e.g., 1920" value={width} onChange={(e) => setWidth(e.target.value)} /> <Input id="width" type="number" placeholder="Original" value={width} onChange={(e) => setWidth(e.target.value)} />
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="height">Height (px)</Label> <Label htmlFor="height">Height (px)</Label>
<Input id="height" type="number" placeholder="e.g., 1080" value={height} onChange={(e) => setHeight(e.target.value)} /> <Input id="height" type="number" placeholder="Original" value={height} onChange={(e) => setHeight(e.target.value)} />
</div> </div>
</div> </div>
</AccordionContent> </AccordionContent>
@@ -382,14 +355,6 @@ export function ImageConverter() {
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
<Button
onClick={handleConvertAndDownload}
disabled={!hasImages || !width || !height || isConverting || convertingIndex !== null}
className="w-full"
>
<Download className="mr-2 h-4 w-4" />
{isConverting ? "Converting..." : `Apply Settings & Download All (${images.length})`}
</Button>
</div> </div>
<div className="lg:col-span-2 flex flex-col gap-8"> <div className="lg:col-span-2 flex flex-col gap-8">
@@ -418,7 +383,7 @@ export function ImageConverter() {
<CardTitle>Uploaded Images</CardTitle> <CardTitle>Uploaded Images</CardTitle>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="ghost" size="sm" onClick={handleClearAll} disabled={isConverting || convertingIndex !== null}><Trash2 className="mr-2 h-4 w-4" />Clear All</Button> <Button variant="ghost" size="sm" onClick={handleClearAll} disabled={isConverting || convertingIndex !== null}><Trash2 className="mr-2 h-4 w-4" />Clear All</Button>
<Button onClick={handleConvertAndDownload} disabled={!hasImages || !width || !height || isConverting || convertingIndex !== null}> <Button onClick={handleConvertAndDownloadAll} disabled={!hasImages || isConverting || convertingIndex !== null}>
<Download className="mr-2 h-4 w-4" /> <Download className="mr-2 h-4 w-4" />
{isConverting ? "Converting..." : `Download All (${images.length})`} {isConverting ? "Converting..." : `Download All (${images.length})`}
</Button> </Button>
@@ -428,45 +393,48 @@ export function ImageConverter() {
<CardContent> <CardContent>
<ScrollArea className="h-[400px] pr-4"> <ScrollArea className="h-[400px] pr-4">
<div className="space-y-4"> <div className="space-y-4">
{previewUrls.map((url, index) => ( {previewUrls.map((url, index) => {
<div key={url} className="p-4 border rounded-lg flex items-center gap-4"> const finalFilename = generateFinalFilename(index, true);
<img src={url} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" /> return (
<div className="flex-1 min-w-0"> <div key={url} className="p-4 border rounded-lg flex items-center gap-4">
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">Base Name</Label> <img src={url} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" />
<Input <div className="flex-1 min-w-0">
id={`filename-${index}`} <Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">Base Name</Label>
value={filenames[index]} <Input
onChange={(e) => handleFilenameChange(index, e.target.value)} id={`filename-${index}`}
className="text-sm font-medium h-8 mt-1" value={filenames[index]}
/> onChange={(e) => handleFilenameChange(index, e.target.value)}
<p className="text-xs text-muted-foreground truncate mt-1" title={`${generateFinalFilename(index)}_${width || 'w'}x${height || 'h'}.${format}`}> className="text-sm font-medium h-8 mt-1"
Final name: {generateFinalFilename(index)}.{format} />
</p> <p className="text-xs text-muted-foreground truncate mt-1" title={`${finalFilename}.${format}`}>
Final name: {finalFilename}.{format}
</p>
</div>
<div className="flex items-center shrink-0">
<Button
variant="ghost"
size="icon"
className="text-gray-500 hover:text-primary"
onClick={() => handleConvertAndDownloadSingle(index)}
disabled={isConverting || convertingIndex !== null}
title="Download this image"
>
<Download className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="shrink-0 text-gray-500 hover:text-destructive"
onClick={() => handleRemoveImage(index)}
disabled={isConverting || convertingIndex !== null}
title="Remove this image"
>
<X className="h-4 w-4" />
</Button>
</div>
</div> </div>
<div className="flex items-center shrink-0"> );
<Button })}
variant="ghost"
size="icon"
className="text-gray-500 hover:text-primary"
onClick={() => handleConvertAndDownloadSingle(index)}
disabled={isConverting || convertingIndex !== null || !width || !height}
title="Download this image"
>
<Download className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="shrink-0 text-gray-500 hover:text-destructive"
onClick={() => handleRemoveImage(index)}
disabled={isConverting || convertingIndex !== null}
title="Remove this image"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div> </div>
</ScrollArea> </ScrollArea>
</CardContent> </CardContent>