[dyad] Enable batch image processing - wrote 1 file(s)
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user