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