[dyad] Enable batch image processing - wrote 1 file(s)

This commit is contained in:
[dyad]
2026-01-18 11:06:39 +01:00
parent 984357c20b
commit a31c534992

View File

@@ -28,9 +28,6 @@ export function ImageConverter() {
const [images, setImages] = useState<File[]>([]);
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
const [filenames, setFilenames] = useState<string[]>([]);
const [selectedImageIndex, setSelectedImageIndex] = useState<number | null>(
null
);
const [width, setWidth] = useState<number | string>("");
const [height, setHeight] = useState<number | string>("");
const [format, setFormat] = useState<"png" | "jpeg" | "webp">("png");
@@ -44,20 +41,6 @@ export function ImageConverter() {
};
}, [previewUrls]);
useEffect(() => {
if (selectedImageIndex !== null && previewUrls[selectedImageIndex]) {
const img = new Image();
img.onload = () => {
setWidth(img.width);
setHeight(img.height);
};
img.src = previewUrls[selectedImageIndex];
} else {
setWidth("");
setHeight("");
}
}, [selectedImageIndex, previewUrls]);
const handleFiles = (files: FileList | null) => {
if (!files || files.length === 0) return;
@@ -86,9 +69,6 @@ export function ImageConverter() {
setPreviewUrls(newPreviewUrls);
setFilenames(newFilenames);
if (selectedImageIndex === null) {
setSelectedImageIndex(images.length);
}
toast.success(`${imageFiles.length} image(s) added.`);
};
@@ -122,14 +102,6 @@ export function ImageConverter() {
setImages(newImages);
setPreviewUrls(newPreviewUrls);
setFilenames(newFilenames);
if (selectedImageIndex === indexToRemove) {
setSelectedImageIndex(
newImages.length > 0 ? Math.max(0, indexToRemove - 1) : null
);
} else if (selectedImageIndex !== null && selectedImageIndex > indexToRemove) {
setSelectedImageIndex(selectedImageIndex - 1);
}
};
const handleClearAll = () => {
@@ -137,7 +109,6 @@ export function ImageConverter() {
setImages([]);
setPreviewUrls([]);
setFilenames([]);
setSelectedImageIndex(null);
toast.info("All images cleared.");
};
@@ -150,56 +121,69 @@ export function ImageConverter() {
setFilenames(newFilenames);
};
const handleConvertAndDownload = () => {
if (selectedImageIndex === null || !width || !height) {
toast.error("Please select an image and set dimensions.");
const handleConvertAndDownload = async () => {
if (images.length === 0 || !width || !height) {
toast.error("Please upload images and set dimensions.");
return;
}
if (
!filenames[selectedImageIndex] ||
filenames[selectedImageIndex].trim() === ""
) {
toast.error("Please enter a filename for the selected image.");
if (filenames.some((name) => name.trim() === "")) {
toast.error("Please ensure all images have a filename.");
return;
}
setIsConverting(true);
const image = images[selectedImageIndex];
const previewUrl = previewUrls[selectedImageIndex];
const img = new Image();
img.crossOrigin = "anonymous";
img.src = previewUrl;
toast.info(`Starting conversion for ${images.length} images...`);
img.onload = () => {
const canvas = document.createElement("canvas");
canvas.width = Number(width);
canvas.height = Number(height);
const ctx = canvas.getContext("2d");
const conversionPromises = images.map((image, index) => {
return new Promise<void>((resolve, reject) => {
const previewUrl = previewUrls[index];
const img = new Image();
img.crossOrigin = "anonymous";
img.src = previewUrl;
if (ctx) {
ctx.drawImage(img, 0, 0, Number(width), Number(height));
const dataUrl = canvas.toDataURL(`image/${format}`);
const link = document.createElement("a");
link.href = dataUrl;
const customName = filenames[selectedImageIndex];
link.download = `${customName}_${width}x${height}.${format}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success(`Exported ${image.name} successfully!`);
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 dataUrl = canvas.toDataURL(`image/${format}`);
const link = document.createElement("a");
link.href = dataUrl;
const customName = filenames[index];
link.download = `${customName}_${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 {
await Promise.all(conversionPromises);
toast.success(`Successfully exported all ${images.length} images!`);
} catch (error) {
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error("Could not process the image.");
toast.error("An unknown error occurred during conversion.");
}
} finally {
setIsConverting(false);
};
img.onerror = () => {
toast.error("Failed to load image for conversion.");
setIsConverting(false);
};
}
};
const hasImages = images.length > 0;
const isImageSelected = selectedImageIndex !== null;
return (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8 w-full">
@@ -208,23 +192,23 @@ export function ImageConverter() {
<CardHeader>
<CardTitle>Image Settings</CardTitle>
<CardDescription>
Adjust resolution and format for the selected image.
Adjust resolution and format for all uploaded images.
</CardDescription>
</CardHeader>
<CardContent className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2">
<Label htmlFor="width">Width (px)</Label>
<Input id="width" type="number" placeholder="e.g., 1920" value={width} onChange={(e) => setWidth(e.target.value)} disabled={!isImageSelected} />
<Input id="width" type="number" placeholder="e.g., 1920" value={width} onChange={(e) => setWidth(e.target.value)} disabled={!hasImages} />
</div>
<div className="space-y-2">
<Label htmlFor="height">Height (px)</Label>
<Input id="height" type="number" placeholder="e.g., 1080" value={height} onChange={(e) => setHeight(e.target.value)} disabled={!isImageSelected} />
<Input id="height" type="number" placeholder="e.g., 1080" value={height} onChange={(e) => setHeight(e.target.value)} disabled={!hasImages} />
</div>
</div>
<div className="space-y-2">
<Label htmlFor="format">Format</Label>
<Select value={format} onValueChange={(value: "png" | "jpeg" | "webp") => setFormat(value)} disabled={!isImageSelected}>
<Select value={format} onValueChange={(value: "png" | "jpeg" | "webp") => setFormat(value)} disabled={!hasImages}>
<SelectTrigger id="format"><SelectValue placeholder="Select format" /></SelectTrigger>
<SelectContent>
<SelectItem value="png">PNG</SelectItem>
@@ -236,7 +220,10 @@ export function ImageConverter() {
</CardContent>
<CardFooter className="flex justify-between">
<Button variant="outline" onClick={() => fileInputRef.current?.click()}><Upload className="mr-2 h-4 w-4" />Upload</Button>
<Button onClick={handleConvertAndDownload} disabled={!isImageSelected || isConverting}><Download className="mr-2 h-4 w-4" />{isConverting ? "Converting..." : "Download"}</Button>
<Button onClick={handleConvertAndDownload} disabled={!hasImages || !width || !height || isConverting}>
<Download className="mr-2 h-4 w-4" />
{isConverting ? "Converting..." : `Download All (${images.length})`}
</Button>
</CardFooter>
</Card>
</div>
@@ -272,7 +259,7 @@ export function ImageConverter() {
<ScrollArea className="h-[400px] pr-4">
<div className="space-y-4">
{previewUrls.map((url, index) => (
<div key={url} className={cn("p-4 border rounded-lg flex items-center gap-4 cursor-pointer transition-all", selectedImageIndex === index && "bg-accent")} onClick={() => setSelectedImageIndex(index)}>
<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" />
<div className="flex-1 space-y-2 min-w-0">
<p className="text-sm font-medium truncate text-gray-700 dark:text-gray-300" title={images[index].name}>
@@ -280,10 +267,10 @@ export function ImageConverter() {
</p>
<div className="space-y-1">
<Label htmlFor={`filename-${index}`} className="text-xs font-semibold">New Filename</Label>
<Input id={`filename-${index}`} type="text" placeholder="Enter new filename" value={filenames[index]} onChange={(e) => handleFilenameChange(e, index)} onClick={(e) => e.stopPropagation()} />
<Input id={`filename-${index}`} type="text" placeholder="Enter new filename" value={filenames[index]} onChange={(e) => handleFilenameChange(e, index)} />
</div>
</div>
<Button variant="ghost" size="icon" className="shrink-0 text-gray-500 hover:text-destructive" onClick={(e) => { e.stopPropagation(); handleRemoveImage(index); }}>
<Button variant="ghost" size="icon" className="shrink-0 text-gray-500 hover:text-destructive" onClick={() => handleRemoveImage(index)}>
<X className="h-4 w-4" />
</Button>
</div>