[dyad] Add batch filename settings panel - wrote 1 file(s)
This commit is contained in:
@@ -23,14 +23,21 @@ import { ScrollArea } from "@/components/ui/scroll-area";
|
|||||||
import { Upload, Download, X, Trash2 } from "lucide-react";
|
import { Upload, Download, X, Trash2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
|
||||||
export function ImageConverter() {
|
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 [originalFilenames, setOriginalFilenames] = useState<string[]>([]);
|
||||||
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");
|
||||||
|
|
||||||
|
const [prefix, setPrefix] = useState<string>("");
|
||||||
|
const [suffix, setSuffix] = useState<string>("");
|
||||||
|
const [useCounter, setUseCounter] = useState<boolean>(true);
|
||||||
|
const [counterDigits, setCounterDigits] = useState<number>(3);
|
||||||
|
|
||||||
const [isConverting, setIsConverting] = useState(false);
|
const [isConverting, setIsConverting] = useState(false);
|
||||||
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
const [isDraggingOver, setIsDraggingOver] = useState(false);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
@@ -58,8 +65,8 @@ export function ImageConverter() {
|
|||||||
...previewUrls,
|
...previewUrls,
|
||||||
...imageFiles.map((file) => URL.createObjectURL(file)),
|
...imageFiles.map((file) => URL.createObjectURL(file)),
|
||||||
];
|
];
|
||||||
const newFilenames = [
|
const newOriginalFilenames = [
|
||||||
...filenames,
|
...originalFilenames,
|
||||||
...imageFiles.map((file) =>
|
...imageFiles.map((file) =>
|
||||||
file.name.substring(0, file.name.lastIndexOf("."))
|
file.name.substring(0, file.name.lastIndexOf("."))
|
||||||
),
|
),
|
||||||
@@ -67,7 +74,7 @@ export function ImageConverter() {
|
|||||||
|
|
||||||
setImages(newImages);
|
setImages(newImages);
|
||||||
setPreviewUrls(newPreviewUrls);
|
setPreviewUrls(newPreviewUrls);
|
||||||
setFilenames(newFilenames);
|
setOriginalFilenames(newOriginalFilenames);
|
||||||
|
|
||||||
toast.success(`${imageFiles.length} image(s) added.`);
|
toast.success(`${imageFiles.length} image(s) added.`);
|
||||||
};
|
};
|
||||||
@@ -97,28 +104,28 @@ export function ImageConverter() {
|
|||||||
URL.revokeObjectURL(previewUrls[indexToRemove]);
|
URL.revokeObjectURL(previewUrls[indexToRemove]);
|
||||||
const newImages = images.filter((_, i) => i !== indexToRemove);
|
const newImages = images.filter((_, i) => i !== indexToRemove);
|
||||||
const newPreviewUrls = previewUrls.filter((_, i) => i !== indexToRemove);
|
const newPreviewUrls = previewUrls.filter((_, i) => i !== indexToRemove);
|
||||||
const newFilenames = filenames.filter((_, i) => i !== indexToRemove);
|
const newOriginalFilenames = originalFilenames.filter((_, i) => i !== indexToRemove);
|
||||||
|
|
||||||
setImages(newImages);
|
setImages(newImages);
|
||||||
setPreviewUrls(newPreviewUrls);
|
setPreviewUrls(newPreviewUrls);
|
||||||
setFilenames(newFilenames);
|
setOriginalFilenames(newOriginalFilenames);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClearAll = () => {
|
const handleClearAll = () => {
|
||||||
previewUrls.forEach((url) => URL.revokeObjectURL(url));
|
previewUrls.forEach((url) => URL.revokeObjectURL(url));
|
||||||
setImages([]);
|
setImages([]);
|
||||||
setPreviewUrls([]);
|
setPreviewUrls([]);
|
||||||
setFilenames([]);
|
setOriginalFilenames([]);
|
||||||
toast.info("All images cleared.");
|
toast.info("All images cleared.");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFilenameChange = (
|
const generateFinalFilename = (index: number) => {
|
||||||
e: ChangeEvent<HTMLInputElement>,
|
if (useCounter) {
|
||||||
index: number
|
const counter = (index + 1).toString().padStart(counterDigits, '0');
|
||||||
) => {
|
return `${prefix}${counter}${suffix}`;
|
||||||
const newFilenames = [...filenames];
|
}
|
||||||
newFilenames[index] = e.target.value;
|
const originalName = originalFilenames[index] || "filename";
|
||||||
setFilenames(newFilenames);
|
return `${prefix}${originalName}${suffix}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConvertAndDownload = async () => {
|
const handleConvertAndDownload = async () => {
|
||||||
@@ -127,11 +134,6 @@ export function ImageConverter() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filenames.some((name) => name.trim() === "")) {
|
|
||||||
toast.error("Please ensure all images have a filename.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsConverting(true);
|
setIsConverting(true);
|
||||||
toast.info(`Starting conversion for ${images.length} images...`);
|
toast.info(`Starting conversion for ${images.length} images...`);
|
||||||
|
|
||||||
@@ -153,8 +155,8 @@ export function ImageConverter() {
|
|||||||
const dataUrl = canvas.toDataURL(`image/${format}`);
|
const dataUrl = canvas.toDataURL(`image/${format}`);
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = dataUrl;
|
link.href = dataUrl;
|
||||||
const customName = filenames[index];
|
const finalFilename = generateFinalFilename(index);
|
||||||
link.download = `${customName}_${width}x${height}.${format}`;
|
link.download = `${finalFilename}_${width}x${height}.${format}`;
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
document.body.removeChild(link);
|
document.body.removeChild(link);
|
||||||
@@ -226,6 +228,41 @@ export function ImageConverter() {
|
|||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Filename Settings</CardTitle>
|
||||||
|
<CardDescription>Customize the output filenames.</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="prefix">Prefix</Label>
|
||||||
|
<Input id="prefix" placeholder="e.g., travel-" value={prefix} onChange={(e) => setPrefix(e.target.value)} disabled={!hasImages} />
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="suffix">Suffix</Label>
|
||||||
|
<Input id="suffix" placeholder="e.g., -edit" value={suffix} onChange={(e) => setSuffix(e.target.value)} disabled={!hasImages} />
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2 pt-2">
|
||||||
|
<Switch id="use-counter" checked={useCounter} onCheckedChange={setUseCounter} disabled={!hasImages} />
|
||||||
|
<Label htmlFor="use-counter">Add sequential number</Label>
|
||||||
|
</div>
|
||||||
|
{useCounter && (
|
||||||
|
<div className="space-y-2 pl-8">
|
||||||
|
<Label htmlFor="counter-digits">Number of digits</Label>
|
||||||
|
<Select value={String(counterDigits)} onValueChange={(val) => setCounterDigits(Number(val))} disabled={!hasImages}>
|
||||||
|
<SelectTrigger id="counter-digits"><SelectValue /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="1">1 (e.g., 1)</SelectItem>
|
||||||
|
<SelectItem value="2">2 (e.g., 01)</SelectItem>
|
||||||
|
<SelectItem value="3">3 (e.g., 001)</SelectItem>
|
||||||
|
<SelectItem value="4">4 (e.g., 0001)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="lg:col-span-2 flex flex-col gap-8">
|
<div className="lg:col-span-2 flex flex-col gap-8">
|
||||||
@@ -261,14 +298,13 @@ export function ImageConverter() {
|
|||||||
{previewUrls.map((url, index) => (
|
{previewUrls.map((url, index) => (
|
||||||
<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 space-y-2 min-w-0">
|
<div className="flex-1 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}>
|
||||||
Source: {images[index].name}
|
{images[index].name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-muted-foreground truncate" title={`${generateFinalFilename(index)}_${width || 'w'}x${height || 'h'}.${format}`}>
|
||||||
|
New name: {generateFinalFilename(index)}
|
||||||
</p>
|
</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)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" className="shrink-0 text-gray-500 hover:text-destructive" onClick={() => 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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user