[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 [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>
|
||||||
|
|||||||
Reference in New Issue
Block a user