Reverted all changes back to version 3866176416

This commit is contained in:
[dyad]
2026-01-18 14:14:04 +01:00
parent 2dfd3a9ba3
commit 81680e7e63
7 changed files with 580 additions and 1037 deletions

View File

@@ -1,231 +0,0 @@
"use client";
import { useTranslations } from "next-intl";
import {
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Label } from "@/components/ui/label";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { HelpCircle } from "lucide-react";
import { Switch } from "@/components/ui/switch";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
interface FilenameSettingsProps {
useDefaultBaseName: boolean;
onUseDefaultBaseNameChange: (checked: boolean) => void;
defaultBaseName: string;
onDefaultBaseNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onApplyDefaultBaseNameToAll: () => void;
hasImages: boolean;
prefix: string;
onPrefixChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
suffix: string;
onSuffixChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
useCounter: boolean;
onUseCounterChange: (checked: boolean) => void;
counterStart: number;
onCounterStartChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
counterDigits: number;
onCounterDigitsChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}
export function FilenameSettings({
useDefaultBaseName,
onUseDefaultBaseNameChange,
defaultBaseName,
onDefaultBaseNameChange,
onApplyDefaultBaseNameToAll,
hasImages,
prefix,
onPrefixChange,
suffix,
onSuffixChange,
useCounter,
onUseCounterChange,
counterStart,
onCounterStartChange,
counterDigits,
onCounterDigitsChange,
}: FilenameSettingsProps) {
const t = useTranslations("ImageConverter");
return (
<AccordionItem value="filename-settings" className="border rounded-lg bg-card">
<AccordionTrigger className="p-6 hover:no-underline">
<div className="text-left">
<h3 className="text-lg font-medium leading-none">
{t("filenameSettingsTitle")}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{t("filenameSettingsSubtitle")}
</p>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6">
<div className="space-y-6">
<div className="flex items-center space-x-2">
<Switch
id="use-default-base-name"
checked={useDefaultBaseName}
onCheckedChange={onUseDefaultBaseNameChange}
/>
<Label
htmlFor="use-default-base-name"
className="flex items-center gap-1.5 cursor-pointer"
>
{t("useDefaultBaseNameLabel")}
<Tooltip>
<TooltipTrigger onClick={(e) => e.preventDefault()}>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("useDefaultBaseNameTooltip")}</p>
</TooltipContent>
</Tooltip>
</Label>
</div>
{useDefaultBaseName && (
<div className="space-y-2">
<Label htmlFor="default-base-name">
{t("defaultBaseNameLabel")}
</Label>
<div className="flex items-center gap-2">
<Input
id="default-base-name"
placeholder={t("defaultBaseNamePlaceholder")}
value={defaultBaseName}
onChange={onDefaultBaseNameChange}
/>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="sm"
onClick={onApplyDefaultBaseNameToAll}
disabled={!defaultBaseName || !hasImages}
>
{t("applyToAll")}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("applyToAllTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
</div>
)}
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="prefix">{t("prefixLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("prefixTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input
id="prefix"
placeholder={t("prefixPlaceholder")}
value={prefix}
onChange={onPrefixChange}
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="suffix">{t("suffixLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("suffixTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input
id="suffix"
placeholder={t("suffixPlaceholder")}
value={suffix}
onChange={onSuffixChange}
/>
</div>
<div className="flex items-center space-x-2 pt-2">
<Switch
id="use-counter"
checked={useCounter}
onCheckedChange={onUseCounterChange}
/>
<Label
htmlFor="use-counter"
className="flex items-center gap-1.5 cursor-pointer"
>
{t("useCounterLabel")}
<Tooltip>
<TooltipTrigger onClick={(e) => e.preventDefault()}>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("useCounterTooltip")}</p>
</TooltipContent>
</Tooltip>
</Label>
</div>
{useCounter && (
<div className="grid grid-cols-2 gap-4 pt-2">
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="counter-start">{t("counterStartLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("counterStartTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input
id="counter-start"
type="number"
value={counterStart}
onChange={onCounterStartChange}
min="0"
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="counter-digits">{t("counterDigitsLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("counterDigitsTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input
id="counter-digits"
type="number"
value={counterDigits}
onChange={onCounterDigitsChange}
min="1"
/>
</div>
</div>
)}
</div>
</AccordionContent>
</AccordionItem>
);
}

View File

@@ -1,12 +1,42 @@
"use client";
import { useState, useEffect, ChangeEvent } from "react";
import { useState, useRef, ChangeEvent, useEffect } from "react";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Button } from "@/components/ui/button";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Upload, Download, X, Trash2, Check, ArrowRightLeft, HelpCircle, RotateCcw } from "lucide-react";
import { toast } from "sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { Switch } from "@/components/ui/switch";
import { Checkbox } from "@/components/ui/checkbox";
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Slider } from "@/components/ui/slider";
import { ObjectPositionControl } from "./object-position-control";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useTranslations } from "next-intl";
import { ImageUpload } from "./image-upload";
import { UploadedImageList } from "./uploaded-image-list";
import { SettingsPanel } from "./settings-panel";
const initialSettings = {
width: "",
@@ -29,6 +59,14 @@ const initialSettings = {
export function ImageConverter() {
const t = useTranslations("ImageConverter");
const aspectRatios = [
{ name: t("aspectRatios.custom"), value: "custom" },
{ name: t("aspectRatios.square"), value: "1/1" },
{ name: t("aspectRatios.standard"), value: "4/3" },
{ name: t("aspectRatios.photography"), value: "3/2" },
{ name: t("aspectRatios.widescreen"), value: "16/9" },
];
const [images, setImages] = useState<File[]>([]);
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
const [filenames, setFilenames] = useState<string[]>([]);
@@ -38,6 +76,7 @@ export function ImageConverter() {
const [keepOrientation, setKeepOrientation] = useState<boolean>(initialSettings.keepOrientation);
const [format, setFormat] = useState<"png" | "jpeg" | "webp">(initialSettings.format);
const [quality, setQuality] = useState<number>(initialSettings.quality);
const [prefix, setPrefix] = useState<string>(initialSettings.prefix);
const [suffix, setSuffix] = useState<string>(initialSettings.suffix);
const [useCounter, setUseCounter] = useState<boolean>(initialSettings.useCounter);
@@ -45,11 +84,14 @@ export function ImageConverter() {
const [counterDigits, setCounterDigits] = useState<number>(initialSettings.counterDigits);
const [useDefaultBaseName, setUseDefaultBaseName] = useState<boolean>(initialSettings.useDefaultBaseName);
const [defaultBaseName, setDefaultBaseName] = useState<string>(initialSettings.defaultBaseName);
const [scaleMode, setScaleMode] = useState<'fill' | 'cover' | 'contain'>(initialSettings.scaleMode);
const [objectPosition, setObjectPosition] = useState<string>(initialSettings.objectPosition);
const [isConverting, setIsConverting] = useState(false);
const [convertingIndex, setConvertingIndex] = useState<number | null>(null);
const [isDraggingOver, setIsDraggingOver] = useState(false);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
return () => {
@@ -90,6 +132,11 @@ export function ImageConverter() {
toast.success(t("toasts.imagesAdded", { count: imageFiles.length }));
};
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
handleFiles(e.target.files);
if (e.target) e.target.value = "";
};
const handleDragOver = (e: React.DragEvent<HTMLDivElement>) => {
e.preventDefault();
setIsDraggingOver(true);
@@ -108,9 +155,13 @@ export function ImageConverter() {
const handleRemoveImage = (indexToRemove: number) => {
URL.revokeObjectURL(previewUrls[indexToRemove]);
setImages((prev) => prev.filter((_, i) => i !== indexToRemove));
setPreviewUrls((prev) => prev.filter((_, i) => i !== indexToRemove));
setFilenames((prev) => prev.filter((_, i) => i !== indexToRemove));
const newImages = images.filter((_, i) => i !== indexToRemove);
const newPreviewUrls = previewUrls.filter((_, i) => i !== indexToRemove);
const newFilenames = filenames.filter((_, i) => i !== indexToRemove);
setImages(newImages);
setPreviewUrls(newPreviewUrls);
setFilenames(newFilenames);
};
const handleClearAll = () => {
@@ -124,11 +175,9 @@ export function ImageConverter() {
};
const handleFilenameChange = (index: number, newName: string) => {
setFilenames((prev) => {
const newFilenames = [...prev];
newFilenames[index] = newName;
return newFilenames;
});
const newFilenames = [...filenames];
newFilenames[index] = newName;
setFilenames(newFilenames);
};
const generateFinalFilename = (index: number) => {
@@ -258,13 +307,19 @@ export function ImageConverter() {
setIsConverting(true);
toast.info(t("toasts.conversionStarting", { count: images.length }));
const conversionPromises = images.map((image, index) =>
convertAndDownload(image, previewUrls[index], index)
);
try {
await Promise.all(images.map((image, index) =>
convertAndDownload(image, previewUrls[index], index)
));
await Promise.all(conversionPromises);
toast.success(t("toasts.conversionSuccess", { count: images.length }));
} catch (error) {
toast.error(error instanceof Error ? error.message : t("toasts.conversionError"));
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error(t("toasts.conversionError"));
}
} finally {
setIsConverting(false);
}
@@ -278,7 +333,11 @@ export function ImageConverter() {
await convertAndDownload(images[index], previewUrls[index], index);
toast.success(t("toasts.singleConversionSuccess", { filename: filenames[index] }));
} catch (error) {
toast.error(error instanceof Error ? error.message : t("toasts.conversionError"));
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error(t("toasts.conversionError"));
}
} finally {
setConvertingIndex(null);
}
@@ -309,21 +368,36 @@ export function ImageConverter() {
const handleAspectRatioChange = (value: string) => {
setAspectRatio(value);
if (value === "custom") return;
if (value === "custom") {
return;
}
const [w, h] = value.split("/").map(Number);
const newWidth = w > h ? 1000 : Math.round((1000 * w) / h);
const newHeight = h > w ? 1000 : Math.round((1000 * h) / w);
setWidth(w === h ? 1000 : newWidth);
setHeight(w === h ? 1000 : newHeight);
let newWidth: number;
let newHeight: number;
if (w > h) {
newWidth = 1000;
newHeight = Math.round((1000 * h) / w);
} else if (h > w) {
newHeight = 1000;
newWidth = Math.round((1000 * w) / h);
} else {
newWidth = 1000;
newHeight = 1000;
}
setWidth(newWidth);
setHeight(newHeight);
};
const handleWidthChange = (e: ChangeEvent<HTMLInputElement>) => {
const handleWidthChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setWidth(e.target.value);
setAspectRatio("custom");
};
const handleHeightChange = (e: ChangeEvent<HTMLInputElement>) => {
const handleHeightChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setHeight(e.target.value);
setAspectRatio("custom");
};
@@ -333,16 +407,19 @@ export function ImageConverter() {
setHeight(width);
};
const hasImages = images.length > 0;
const handleApplyDefaultBaseNameToAll = () => {
if (!defaultBaseName) {
toast.error(t("toasts.defaultBaseNameMissing"));
return;
}
if (images.length === 0) {
if (!hasImages) {
toast.info(t("toasts.uploadFirst"));
return;
}
setFilenames(filenames.map(() => defaultBaseName));
const newFilenames = filenames.map(() => defaultBaseName);
setFilenames(newFilenames);
toast.success(t("toasts.baseNameApplied", { name: defaultBaseName, count: images.length }));
};
@@ -350,65 +427,484 @@ export function ImageConverter() {
<TooltipProvider>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 w-full">
<div className="lg:col-span-2 flex flex-col gap-4 lg:order-2">
<ImageUpload
onFiles={handleFiles}
isDraggingOver={isDraggingOver}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
/>
<UploadedImageList
previewUrls={previewUrls}
filenames={filenames}
isConverting={isConverting}
convertingIndex={convertingIndex}
onClearAll={handleClearAll}
onConvertAndDownloadAll={handleConvertAndDownloadAll}
onFilenameChange={handleFilenameChange}
generateFinalFilename={generateFinalFilename}
format={format}
onConvertAndDownloadSingle={handleConvertAndDownloadSingle}
onRemoveImage={handleRemoveImage}
/>
<Card>
<CardContent className="pt-6">
<div className="space-y-4">
<h3 className="text-lg font-medium">{t("uploadTitle")}</h3>
<div
className={cn(
"w-full h-48 rounded-lg border-2 border-dashed flex items-center justify-center relative transition-colors cursor-pointer hover:border-primary/60",
isDraggingOver ? "border-primary bg-accent" : "border-input"
)}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
onClick={() => fileInputRef.current?.click()}
>
<div className="flex flex-col items-center justify-center text-center text-muted-foreground">
<Upload className="w-8 h-8 mb-2" />
<p className="font-semibold">{t("uploadPrompt")}</p>
<p className="text-xs text-muted-foreground mt-1">{t("uploadHint")}</p>
</div>
<Input type="file" ref={fileInputRef} onChange={handleImageChange} className="hidden" accept="image/*" multiple />
</div>
</div>
</CardContent>
</Card>
{hasImages && (
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle>{t("uploadedImagesTitle")}</CardTitle>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="sm" onClick={handleClearAll} disabled={isConverting || convertingIndex !== null}><Trash2 className="mr-2 h-4 w-4" />{t("clearAll")}</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("clearAllTooltip")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={handleConvertAndDownloadAll} disabled={!hasImages || isConverting || convertingIndex !== null}>
<Download className="mr-2 h-4 w-4" />
{isConverting ? t("downloadAllConverting") : t("downloadAll", { count: images.length })}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("downloadAllTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{previewUrls.map((url, index) => {
const finalFilename = generateFinalFilename(index);
return (
<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 min-w-0">
<Label htmlFor={`filename-${index}`} className="text-xs text-muted-foreground">{t("baseNameLabel")}</Label>
<Input
id={`filename-${index}`}
value={filenames[index]}
onChange={(e) => handleFilenameChange(index, e.target.value)}
className="text-sm font-medium h-8 mt-1"
/>
<p className="text-xs text-muted-foreground truncate mt-1" title={`${finalFilename}.${format}`}>
{t("finalNameLabel", { filename: `${finalFilename}.${format}` })}
</p>
</div>
<div className="flex items-center shrink-0">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-gray-500 hover:text-primary"
onClick={() => handleConvertAndDownloadSingle(index)}
disabled={isConverting || convertingIndex !== null}
>
<Download className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("downloadSingleTooltip")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="shrink-0 text-gray-500 hover:text-destructive"
onClick={() => handleRemoveImage(index)}
disabled={isConverting || convertingIndex !== null}
>
<X className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("removeSingleTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
)}
</div>
<SettingsPanel
aspectRatio={aspectRatio}
onAspectRatioChange={handleAspectRatioChange}
width={width}
onWidthChange={handleWidthChange}
onSwapDimensions={handleSwapDimensions}
height={height}
onHeightChange={handleHeightChange}
keepOrientation={keepOrientation}
onKeepOrientationChange={setKeepOrientation}
scaleMode={scaleMode}
onScaleModeChange={setScaleMode}
objectPosition={objectPosition}
onObjectPositionChange={setObjectPosition}
useDefaultBaseName={useDefaultBaseName}
onUseDefaultBaseNameChange={setUseDefaultBaseName}
defaultBaseName={defaultBaseName}
onDefaultBaseNameChange={(e) => setDefaultBaseName(e.target.value)}
onApplyDefaultBaseNameToAll={handleApplyDefaultBaseNameToAll}
hasImages={images.length > 0}
prefix={prefix}
onPrefixChange={(e) => setPrefix(e.target.value)}
suffix={suffix}
onSuffixChange={(e) => setSuffix(e.target.value)}
useCounter={useCounter}
onUseCounterChange={setUseCounter}
counterStart={counterStart}
onCounterStartChange={(e) => setCounterStart(Math.max(0, Number(e.target.value)))}
counterDigits={counterDigits}
onCounterDigitsChange={(e) => setCounterDigits(Math.max(1, Number(e.target.value)))}
format={format}
onFormatChange={setFormat}
quality={quality}
onQualityChange={setQuality}
onResetSettings={handleResetSettings}
onApplySettings={handleApplySettings}
/>
<div className="lg:col-span-1 flex flex-col gap-4 lg:sticky lg:top-8 self-start lg:order-1">
<Accordion type="single" collapsible defaultValue="image-settings" className="w-full space-y-4">
<AccordionItem value="image-settings" className="border rounded-lg bg-card">
<AccordionTrigger className="p-6 hover:no-underline">
<div className="text-left">
<h3 className="text-lg font-medium leading-none">{t("imageSettingsTitle")}</h3>
<p className="text-sm text-muted-foreground mt-1">
{t("imageSettingsSubtitle")}
</p>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6">
<div className="space-y-4">
<div>
<div className="flex items-center gap-1.5">
<Label htmlFor="aspect-ratio">{t("aspectRatioLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("aspectRatioTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Select value={aspectRatio} onValueChange={handleAspectRatioChange}>
<SelectTrigger id="aspect-ratio" className="mt-2">
<SelectValue placeholder={t("selectAspectRatio")} />
</SelectTrigger>
<SelectContent>
{aspectRatios.map((ratio) => (
<SelectItem key={ratio.value} value={ratio.value}>
{ratio.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-end gap-2">
<div className="space-y-2 flex-1">
<div className="flex items-center gap-1.5">
<Label htmlFor="width">{t("widthLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("widthTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input id="width" type="number" placeholder={t("widthPlaceholder")} value={width} onChange={handleWidthChange} />
</div>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="icon" onClick={handleSwapDimensions} className="shrink-0" aria-label="Swap width and height">
<ArrowRightLeft className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("swapDimensionsTooltip")}</p>
</TooltipContent>
</Tooltip>
<div className="space-y-2 flex-1">
<div className="flex items-center gap-1.5">
<Label htmlFor="height">{t("heightLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("heightTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input id="height" type="number" placeholder={t("heightPlaceholder")} value={height} onChange={handleHeightChange} />
</div>
</div>
<div className="flex items-center space-x-2 pt-2">
<Checkbox id="keep-orientation" checked={keepOrientation} onCheckedChange={(checked) => setKeepOrientation(Boolean(checked))} />
<Label htmlFor="keep-orientation" className="cursor-pointer flex items-center gap-1.5">
{t("keepOrientationLabel")}
<Tooltip>
<TooltipTrigger onClick={(e) => e.preventDefault()}>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("keepOrientationTooltip")}</p>
</TooltipContent>
</Tooltip>
</Label>
</div>
</div>
<div className="mt-4 space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="scale-mode">{t("scalingLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("scalingTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Select value={scaleMode} onValueChange={(value: 'fill' | 'cover' | 'contain') => setScaleMode(value)}>
<SelectTrigger id="scale-mode"><SelectValue placeholder={t("selectScaling")} /></SelectTrigger>
<SelectContent>
<SelectItem value="fill">{t("scalingOptions.fill")}</SelectItem>
<SelectItem value="cover">{t("scalingOptions.cover")}</SelectItem>
<SelectItem value="contain">{t("scalingOptions.contain")}</SelectItem>
</SelectContent>
</Select>
</div>
{scaleMode !== 'fill' && (
<div className="mt-4 space-y-2">
<div className="flex items-center gap-1.5">
<Label>{t("positionLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("positionTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<ObjectPositionControl value={objectPosition} onChange={(pos) => setObjectPosition(pos)} />
</div>
)}
</AccordionContent>
</AccordionItem>
<AccordionItem value="filename-settings" className="border rounded-lg bg-card">
<AccordionTrigger className="p-6 hover:no-underline">
<div className="text-left">
<h3 className="text-lg font-medium leading-none">{t("filenameSettingsTitle")}</h3>
<p className="text-sm text-muted-foreground mt-1">{t("filenameSettingsSubtitle")}</p>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6">
<div className="space-y-6">
<div className="flex items-center space-x-2">
<Switch id="use-default-base-name" checked={useDefaultBaseName} onCheckedChange={setUseDefaultBaseName} />
<Label htmlFor="use-default-base-name" className="flex items-center gap-1.5 cursor-pointer">
{t("useDefaultBaseNameLabel")}
<Tooltip>
<TooltipTrigger onClick={(e) => e.preventDefault()}>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("useDefaultBaseNameTooltip")}</p>
</TooltipContent>
</Tooltip>
</Label>
</div>
{useDefaultBaseName && (
<div className="space-y-2">
<Label htmlFor="default-base-name">{t("defaultBaseNameLabel")}</Label>
<div className="flex items-center gap-2">
<Input
id="default-base-name"
placeholder={t("defaultBaseNamePlaceholder")}
value={defaultBaseName}
onChange={(e) => setDefaultBaseName(e.target.value)}
/>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="outline" size="sm" onClick={handleApplyDefaultBaseNameToAll} disabled={!defaultBaseName || !hasImages}>
{t("applyToAll")}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("applyToAllTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
</div>
)}
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="prefix">{t("prefixLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("prefixTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input id="prefix" placeholder={t("prefixPlaceholder")} value={prefix} onChange={(e) => setPrefix(e.target.value)} />
</div>
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="suffix">{t("suffixLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("suffixTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input id="suffix" placeholder={t("suffixPlaceholder")} value={suffix} onChange={(e) => setSuffix(e.target.value)} />
</div>
<div className="flex items-center space-x-2 pt-2">
<Switch id="use-counter" checked={useCounter} onCheckedChange={setUseCounter} />
<Label htmlFor="use-counter" className="flex items-center gap-1.5 cursor-pointer">
{t("useCounterLabel")}
<Tooltip>
<TooltipTrigger onClick={(e) => e.preventDefault()}>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("useCounterTooltip")}</p>
</TooltipContent>
</Tooltip>
</Label>
</div>
{useCounter && (
<div className="grid grid-cols-2 gap-4 pt-2">
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="counter-start">{t("counterStartLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("counterStartTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input
id="counter-start"
type="number"
value={counterStart}
onChange={(e) => setCounterStart(Math.max(0, Number(e.target.value)))}
min="0"
/>
</div>
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="counter-digits">{t("counterDigitsLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("counterDigitsTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input
id="counter-digits"
type="number"
value={counterDigits}
onChange={(e) => setCounterDigits(Math.max(1, Number(e.target.value)))}
min="1"
/>
</div>
</div>
)}
</div>
</AccordionContent>
</AccordionItem>
<AccordionItem value="quality-settings" className="border rounded-lg bg-card">
<AccordionTrigger className="p-6 hover:no-underline">
<div className="text-left">
<h3 className="text-lg font-medium leading-none">{t("qualitySettingsTitle")}</h3>
<p className="text-sm text-muted-foreground mt-1">{t("qualitySettingsSubtitle")}</p>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6">
<div className="space-y-6">
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="format">{t("formatLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("formatTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Select value={format} onValueChange={(value: "png" | "jpeg" | "webp") => setFormat(value)}>
<SelectTrigger id="format"><SelectValue placeholder={t("selectFormat")} /></SelectTrigger>
<SelectContent>
<SelectItem value="png">PNG</SelectItem>
<SelectItem value="jpeg">JPEG</SelectItem>
<SelectItem value="webp">WEBP</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center gap-1.5">
<Label htmlFor="quality">{t("qualityLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("qualityTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<span className="text-sm text-muted-foreground">{quality}%</span>
</div>
<Slider
id="quality"
min={0}
max={100}
step={1}
value={[quality]}
onValueChange={(value) => setQuality(value[0])}
disabled={format === 'png'}
/>
{format === 'png' && (
<p className="text-xs text-muted-foreground pt-1">{t("qualityDisabledHint")}</p>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
</Accordion>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={handleResetSettings}
className="w-full"
variant="outline"
>
<RotateCcw className="mr-2 h-4 w-4" />
{t("resetButton")}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("resetButtonTooltip")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={handleApplySettings}
className="w-full"
>
<Check className="mr-2 h-4 w-4" />
{t("applyButton")}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("applyButtonTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
</div>
</div>
</TooltipProvider>
);

View File

@@ -1,233 +0,0 @@
"use client";
import { useTranslations } from "next-intl";
import {
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Label } from "@/components/ui/label";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { HelpCircle, ArrowRightLeft } from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { ObjectPositionControl } from "./object-position-control";
interface ImageSettingsProps {
aspectRatio: string;
onAspectRatioChange: (value: string) => void;
width: number | string;
onWidthChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onSwapDimensions: () => void;
height: number | string;
onHeightChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
keepOrientation: boolean;
onKeepOrientationChange: (checked: boolean) => void;
scaleMode: 'fill' | 'cover' | 'contain';
onScaleModeChange: (value: 'fill' | 'cover' | 'contain') => void;
objectPosition: string;
onObjectPositionChange: (pos: any) => void;
}
export function ImageSettings({
aspectRatio,
onAspectRatioChange,
width,
onWidthChange,
onSwapDimensions,
height,
onHeightChange,
keepOrientation,
onKeepOrientationChange,
scaleMode,
onScaleModeChange,
objectPosition,
onObjectPositionChange,
}: ImageSettingsProps) {
const t = useTranslations("ImageConverter");
const aspectRatios = [
{ name: t("aspectRatios.custom"), value: "custom" },
{ name: t("aspectRatios.square"), value: "1/1" },
{ name: t("aspectRatios.standard"), value: "4/3" },
{ name: t("aspectRatios.photography"), value: "3/2" },
{ name: t("aspectRatios.widescreen"), value: "16/9" },
];
return (
<AccordionItem value="image-settings" className="border rounded-lg bg-card">
<AccordionTrigger className="p-6 hover:no-underline">
<div className="text-left">
<h3 className="text-lg font-medium leading-none">
{t("imageSettingsTitle")}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{t("imageSettingsSubtitle")}
</p>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6">
<div className="space-y-4">
<div>
<div className="flex items-center gap-1.5">
<Label htmlFor="aspect-ratio">{t("aspectRatioLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("aspectRatioTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Select value={aspectRatio} onValueChange={onAspectRatioChange}>
<SelectTrigger id="aspect-ratio" className="mt-2">
<SelectValue placeholder={t("selectAspectRatio")} />
</SelectTrigger>
<SelectContent>
{aspectRatios.map((ratio) => (
<SelectItem key={ratio.value} value={ratio.value}>
{ratio.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex items-end gap-2">
<div className="space-y-2 flex-1">
<div className="flex items-center gap-1.5">
<Label htmlFor="width">{t("widthLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("widthTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input
id="width"
type="number"
placeholder={t("widthPlaceholder")}
value={width}
onChange={onWidthChange}
/>
</div>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="outline"
size="icon"
onClick={onSwapDimensions}
className="shrink-0"
aria-label="Swap width and height"
>
<ArrowRightLeft className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("swapDimensionsTooltip")}</p>
</TooltipContent>
</Tooltip>
<div className="space-y-2 flex-1">
<div className="flex items-center gap-1.5">
<Label htmlFor="height">{t("heightLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("heightTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Input
id="height"
type="number"
placeholder={t("heightPlaceholder")}
value={height}
onChange={onHeightChange}
/>
</div>
</div>
<div className="flex items-center space-x-2 pt-2">
<Checkbox
id="keep-orientation"
checked={keepOrientation}
onCheckedChange={(checked) => onKeepOrientationChange(Boolean(checked))}
/>
<Label
htmlFor="keep-orientation"
className="cursor-pointer flex items-center gap-1.5"
>
{t("keepOrientationLabel")}
<Tooltip>
<TooltipTrigger onClick={(e) => e.preventDefault()}>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("keepOrientationTooltip")}</p>
</TooltipContent>
</Tooltip>
</Label>
</div>
</div>
<div className="mt-4 space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="scale-mode">{t("scalingLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("scalingTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Select value={scaleMode} onValueChange={onScaleModeChange}>
<SelectTrigger id="scale-mode">
<SelectValue placeholder={t("selectScaling")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="fill">{t("scalingOptions.fill")}</SelectItem>
<SelectItem value="cover">{t("scalingOptions.cover")}</SelectItem>
<SelectItem value="contain">{t("scalingOptions.contain")}</SelectItem>
</SelectContent>
</Select>
</div>
{scaleMode !== "fill" && (
<div className="mt-4 space-y-2">
<div className="flex items-center gap-1.5">
<Label>{t("positionLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("positionTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<ObjectPositionControl
value={objectPosition}
onChange={onObjectPositionChange}
/>
</div>
)}
</AccordionContent>
</AccordionItem>
);
}

View File

@@ -1,66 +0,0 @@
"use client";
import { useRef, ChangeEvent } from "react";
import { useTranslations } from "next-intl";
import { Upload } from "lucide-react";
import { cn } from "@/lib/utils";
import { Input } from "@/components/ui/input";
import { Card, CardContent } from "@/components/ui/card";
interface ImageUploadProps {
onFiles: (files: FileList | null) => void;
isDraggingOver: boolean;
onDragOver: (e: React.DragEvent<HTMLDivElement>) => void;
onDragLeave: (e: React.DragEvent<HTMLDivElement>) => void;
onDrop: (e: React.DragEvent<HTMLDivElement>) => void;
}
export function ImageUpload({
onFiles,
isDraggingOver,
onDragOver,
onDragLeave,
onDrop,
}: ImageUploadProps) {
const t = useTranslations("ImageConverter");
const fileInputRef = useRef<HTMLInputElement>(null);
const handleImageChange = (e: ChangeEvent<HTMLInputElement>) => {
onFiles(e.target.files);
if (e.target) e.target.value = "";
};
return (
<Card>
<CardContent className="pt-6">
<div className="space-y-4">
<h3 className="text-lg font-medium">{t("uploadTitle")}</h3>
<div
className={cn(
"w-full h-48 rounded-lg border-2 border-dashed flex items-center justify-center relative transition-colors cursor-pointer hover:border-primary/60",
isDraggingOver ? "border-primary bg-accent" : "border-input"
)}
onDragOver={onDragOver}
onDragLeave={onDragLeave}
onDrop={onDrop}
onClick={() => fileInputRef.current?.click()}
>
<div className="flex flex-col items-center justify-center text-center text-muted-foreground">
<Upload className="w-8 h-8 mb-2" />
<p className="font-semibold">{t("uploadPrompt")}</p>
<p className="text-xs text-muted-foreground mt-1">{t("uploadHint")}</p>
</div>
<Input
type="file"
ref={fileInputRef}
onChange={handleImageChange}
className="hidden"
accept="image/*"
multiple
/>
</div>
</div>
</CardContent>
</Card>
);
}

View File

@@ -1,113 +0,0 @@
"use client";
import { useTranslations } from "next-intl";
import {
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/components/ui/accordion";
import { Label } from "@/components/ui/label";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { HelpCircle } from "lucide-react";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Slider } from "@/components/ui/slider";
type Format = "png" | "jpeg" | "webp";
interface QualitySettingsProps {
format: Format;
onFormatChange: (value: Format) => void;
quality: number;
onQualityChange: (value: number) => void;
}
export function QualitySettings({
format,
onFormatChange,
quality,
onQualityChange,
}: QualitySettingsProps) {
const t = useTranslations("ImageConverter");
return (
<AccordionItem value="quality-settings" className="border rounded-lg bg-card">
<AccordionTrigger className="p-6 hover:no-underline">
<div className="text-left">
<h3 className="text-lg font-medium leading-none">
{t("qualitySettingsTitle")}
</h3>
<p className="text-sm text-muted-foreground mt-1">
{t("qualitySettingsSubtitle")}
</p>
</div>
</AccordionTrigger>
<AccordionContent className="px-6 pb-6">
<div className="space-y-6">
<div className="space-y-2">
<div className="flex items-center gap-1.5">
<Label htmlFor="format">{t("formatLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("formatTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<Select value={format} onValueChange={onFormatChange}>
<SelectTrigger id="format">
<SelectValue placeholder={t("selectFormat")} />
</SelectTrigger>
<SelectContent>
<SelectItem value="png">PNG</SelectItem>
<SelectItem value="jpeg">JPEG</SelectItem>
<SelectItem value="webp">WEBP</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<div className="flex justify-between items-center">
<div className="flex items-center gap-1.5">
<Label htmlFor="quality">{t("qualityLabel")}</Label>
<Tooltip>
<TooltipTrigger>
<HelpCircle className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
<p>{t("qualityTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
<span className="text-sm text-muted-foreground">{quality}%</span>
</div>
<Slider
id="quality"
min={0}
max={100}
step={1}
value={[quality]}
onValueChange={(value) => onQualityChange(value[0])}
disabled={format === "png"}
/>
{format === "png" && (
<p className="text-xs text-muted-foreground pt-1">
{t("qualityDisabledHint")}
</p>
)}
</div>
</div>
</AccordionContent>
</AccordionItem>
);
}

View File

@@ -1,135 +0,0 @@
"use client";
import { useTranslations } from "next-intl";
import { Accordion } from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { RotateCcw, Check } from "lucide-react";
import { ImageSettings } from "./image-settings";
import { FilenameSettings } from "./filename-settings";
import { QualitySettings } from "./quality-settings";
interface SettingsPanelProps {
aspectRatio: string;
onAspectRatioChange: (value: string) => void;
width: number | string;
onWidthChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onSwapDimensions: () => void;
height: number | string;
onHeightChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
keepOrientation: boolean;
onKeepOrientationChange: (checked: boolean) => void;
scaleMode: 'fill' | 'cover' | 'contain';
onScaleModeChange: (value: 'fill' | 'cover' | 'contain') => void;
objectPosition: string;
onObjectPositionChange: (pos: any) => void;
useDefaultBaseName: boolean;
onUseDefaultBaseNameChange: (checked: boolean) => void;
defaultBaseName: string;
onDefaultBaseNameChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
onApplyDefaultBaseNameToAll: () => void;
hasImages: boolean;
prefix: string;
onPrefixChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
suffix: string;
onSuffixChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
useCounter: boolean;
onUseCounterChange: (checked: boolean) => void;
counterStart: number;
onCounterStartChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
counterDigits: number;
onCounterDigitsChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
format: "png" | "jpeg" | "webp";
onFormatChange: (value: "png" | "jpeg" | "webp") => void;
quality: number;
onQualityChange: (value: number) => void;
onResetSettings: () => void;
onApplySettings: () => void;
}
export function SettingsPanel(props: SettingsPanelProps) {
const t = useTranslations("ImageConverter");
return (
<div className="lg:col-span-1 flex flex-col gap-4 lg:sticky lg:top-8 self-start lg:order-1">
<Accordion
type="single"
collapsible
defaultValue="image-settings"
className="w-full space-y-4"
>
<ImageSettings
aspectRatio={props.aspectRatio}
onAspectRatioChange={props.onAspectRatioChange}
width={props.width}
onWidthChange={props.onWidthChange}
onSwapDimensions={props.onSwapDimensions}
height={props.height}
onHeightChange={props.onHeightChange}
keepOrientation={props.keepOrientation}
onKeepOrientationChange={props.onKeepOrientationChange}
scaleMode={props.scaleMode}
onScaleModeChange={props.onScaleModeChange}
objectPosition={props.objectPosition}
onObjectPositionChange={props.onObjectPositionChange}
/>
<FilenameSettings
useDefaultBaseName={props.useDefaultBaseName}
onUseDefaultBaseNameChange={props.onUseDefaultBaseNameChange}
defaultBaseName={props.defaultBaseName}
onDefaultBaseNameChange={props.onDefaultBaseNameChange}
onApplyDefaultBaseNameToAll={props.onApplyDefaultBaseNameToAll}
hasImages={props.hasImages}
prefix={props.prefix}
onPrefixChange={props.onPrefixChange}
suffix={props.suffix}
onSuffixChange={props.onSuffixChange}
useCounter={props.useCounter}
onUseCounterChange={props.onUseCounterChange}
counterStart={props.counterStart}
onCounterStartChange={props.onCounterStartChange}
counterDigits={props.counterDigits}
onCounterDigitsChange={props.onCounterDigitsChange}
/>
<QualitySettings
format={props.format}
onFormatChange={props.onFormatChange}
quality={props.quality}
onQualityChange={props.onQualityChange}
/>
</Accordion>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={props.onResetSettings}
className="w-full"
variant="outline"
>
<RotateCcw className="mr-2 h-4 w-4" />
{t("resetButton")}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("resetButtonTooltip")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button onClick={props.onApplySettings} className="w-full">
<Check className="mr-2 h-4 w-4" />
{t("applyButton")}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("applyButtonTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
</div>
);
}

View File

@@ -1,175 +0,0 @@
"use client";
import { useTranslations } from "next-intl";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { Download, Trash2, X } from "lucide-react";
interface UploadedImageListProps {
previewUrls: string[];
filenames: string[];
isConverting: boolean;
convertingIndex: number | null;
onClearAll: () => void;
onConvertAndDownloadAll: () => void;
onFilenameChange: (index: number, newName: string) => void;
generateFinalFilename: (index: number) => string;
format: string;
onConvertAndDownloadSingle: (index: number) => void;
onRemoveImage: (index: number) => void;
}
export function UploadedImageList({
previewUrls,
filenames,
isConverting,
convertingIndex,
onClearAll,
onConvertAndDownloadAll,
onFilenameChange,
generateFinalFilename,
format,
onConvertAndDownloadSingle,
onRemoveImage,
}: UploadedImageListProps) {
const t = useTranslations("ImageConverter");
const hasImages = previewUrls.length > 0;
if (!hasImages) {
return null;
}
return (
<TooltipProvider>
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<CardTitle>{t("uploadedImagesTitle")}</CardTitle>
<div className="flex items-center gap-2">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
onClick={onClearAll}
disabled={isConverting || convertingIndex !== null}
>
<Trash2 className="mr-2 h-4 w-4" />
{t("clearAll")}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("clearAllTooltip")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
onClick={onConvertAndDownloadAll}
disabled={!hasImages || isConverting || convertingIndex !== null}
>
<Download className="mr-2 h-4 w-4" />
{isConverting
? t("downloadAllConverting")
: t("downloadAll", { count: previewUrls.length })}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("downloadAllTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
</div>
</CardHeader>
<CardContent>
<div className="space-y-4">
{previewUrls.map((url, index) => {
const finalFilename = generateFinalFilename(index);
return (
<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 min-w-0">
<Label
htmlFor={`filename-${index}`}
className="text-xs text-muted-foreground"
>
{t("baseNameLabel")}
</Label>
<Input
id={`filename-${index}`}
value={filenames[index]}
onChange={(e) => onFilenameChange(index, e.target.value)}
className="text-sm font-medium h-8 mt-1"
/>
<p
className="text-xs text-muted-foreground truncate mt-1"
title={`${finalFilename}.${format}`}
>
{t("finalNameLabel", {
filename: `${finalFilename}.${format}`,
})}
</p>
</div>
<div className="flex items-center shrink-0">
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="text-gray-500 hover:text-primary"
onClick={() => onConvertAndDownloadSingle(index)}
disabled={isConverting || convertingIndex !== null}
>
<Download className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("downloadSingleTooltip")}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="shrink-0 text-gray-500 hover:text-destructive"
onClick={() => onRemoveImage(index)}
disabled={isConverting || convertingIndex !== null}
>
<X className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t("removeSingleTooltip")}</p>
</TooltipContent>
</Tooltip>
</div>
</div>
);
})}
</div>
</CardContent>
</Card>
</TooltipProvider>
);
}