[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,39 +144,38 @@ 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) {
toast.error("Please upload images and set dimensions.");
return;
}
setIsConverting(true);
toast.info(`Starting conversion for ${images.length} images...`);
const conversionPromises = images.map((image, index) => {
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
const previewUrl = previewUrls[index];
const img = new Image(); const img = new Image();
img.crossOrigin = "anonymous"; img.crossOrigin = "anonymous";
img.src = previewUrl; img.src = previewUrl;
img.onload = () => { img.onload = () => {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
canvas.width = Number(width); const targetWidth = width ? Number(width) : img.naturalWidth;
canvas.height = Number(height); const targetHeight = height ? Number(height) : img.naturalHeight;
canvas.width = targetWidth;
canvas.height = targetHeight;
const ctx = canvas.getContext("2d"); const ctx = canvas.getContext("2d");
if (ctx) { if (ctx) {
ctx.drawImage(img, 0, 0, Number(width), Number(height)); ctx.drawImage(img, 0, 0, targetWidth, targetHeight);
const mimeType = `image/${format}`; const mimeType = `image/${format}`;
const dataUrl = canvas.toDataURL(mimeType, format === 'png' ? undefined : quality / 100); const dataUrl = canvas.toDataURL(mimeType, format === 'png' ? undefined : quality / 100);
const link = document.createElement("a"); const link = document.createElement("a");
link.href = dataUrl; link.href = dataUrl;
const finalFilename = generateFinalFilename(index);
link.download = `${finalFilename}_${width}x${height}.${format}`; const dimensionSuffix = width && height ? `_${width}x${height}` : '';
link.download = `${generateFinalFilename(index)}${dimensionSuffix}.${format}`;
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
@@ -189,7 +188,20 @@ export function ImageConverter() {
reject(new Error(`Failed to load ${image.name} for conversion.`)); reject(new Error(`Failed to load ${image.name} for conversion.`));
}; };
}); });
}); };
const handleConvertAndDownloadAll = async () => {
if (images.length === 0) {
toast.error("Please upload images first.");
return;
}
setIsConverting(true);
toast.info(`Starting conversion for ${images.length} images...`);
const conversionPromises = images.map((image, index) =>
convertAndDownload(image, previewUrls[index], index)
);
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,7 +393,9 @@ 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) => {
const finalFilename = generateFinalFilename(index, true);
return (
<div key={url} className="p-4 border rounded-lg flex items-center gap-4"> <div key={url} className="p-4 border rounded-lg flex items-center gap-4">
<img src={url} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" /> <img src={url} alt={`Preview ${index + 1}`} className="w-20 h-20 object-cover rounded-md shrink-0" />
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@@ -439,8 +406,8 @@ export function ImageConverter() {
onChange={(e) => handleFilenameChange(index, e.target.value)} onChange={(e) => handleFilenameChange(index, e.target.value)}
className="text-sm font-medium h-8 mt-1" className="text-sm font-medium h-8 mt-1"
/> />
<p className="text-xs text-muted-foreground truncate mt-1" title={`${generateFinalFilename(index)}_${width || 'w'}x${height || 'h'}.${format}`}> <p className="text-xs text-muted-foreground truncate mt-1" title={`${finalFilename}.${format}`}>
Final name: {generateFinalFilename(index)}.{format} Final name: {finalFilename}.{format}
</p> </p>
</div> </div>
<div className="flex items-center shrink-0"> <div className="flex items-center shrink-0">
@@ -449,7 +416,7 @@ export function ImageConverter() {
size="icon" size="icon"
className="text-gray-500 hover:text-primary" className="text-gray-500 hover:text-primary"
onClick={() => handleConvertAndDownloadSingle(index)} onClick={() => handleConvertAndDownloadSingle(index)}
disabled={isConverting || convertingIndex !== null || !width || !height} disabled={isConverting || convertingIndex !== null}
title="Download this image" title="Download this image"
> >
<Download className="h-4 w-4" /> <Download className="h-4 w-4" />
@@ -466,7 +433,8 @@ export function ImageConverter() {
</Button> </Button>
</div> </div>
</div> </div>
))} );
})}
</div> </div>
</ScrollArea> </ScrollArea>
</CardContent> </CardContent>