"use client"; 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 { 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 { useScopedI18n } from "@/lib/i18n/client"; const initialSettings = { width: "", height: "", aspectRatio: "custom", keepOrientation: true, format: "webp" as "png" | "jpeg" | "webp", quality: 90, prefix: "", suffix: "", useCounter: false, counterStart: 1, counterDigits: 3, useDefaultBaseName: false, defaultBaseName: "", scaleMode: 'cover' as 'fill' | 'cover' | 'contain', objectPosition: 'center center', }; export function ImageConverter() { const t = useScopedI18n('converter'); const aspectRatios = [ { name: t('aspect_ratios.custom'), value: "custom" }, { name: t('aspect_ratios.square'), value: "1/1" }, { name: t('aspect_ratios.standard'), value: "4/3" }, { name: t('aspect_ratios.photography'), value: "3/2" }, { name: t('aspect_ratios.widescreen'), value: "16/9" }, ]; const [images, setImages] = useState([]); const [previewUrls, setPreviewUrls] = useState([]); const [filenames, setFilenames] = useState([]); const [width, setWidth] = useState(initialSettings.width); const [height, setHeight] = useState(initialSettings.height); const [aspectRatio, setAspectRatio] = useState(initialSettings.aspectRatio); const [keepOrientation, setKeepOrientation] = useState(initialSettings.keepOrientation); const [format, setFormat] = useState<"png" | "jpeg" | "webp">(initialSettings.format); const [quality, setQuality] = useState(initialSettings.quality); const [prefix, setPrefix] = useState(initialSettings.prefix); const [suffix, setSuffix] = useState(initialSettings.suffix); const [useCounter, setUseCounter] = useState(initialSettings.useCounter); const [counterStart, setCounterStart] = useState(initialSettings.counterStart); const [counterDigits, setCounterDigits] = useState(initialSettings.counterDigits); const [useDefaultBaseName, setUseDefaultBaseName] = useState(initialSettings.useDefaultBaseName); const [defaultBaseName, setDefaultBaseName] = useState(initialSettings.defaultBaseName); const [scaleMode, setScaleMode] = useState<'fill' | 'cover' | 'contain'>(initialSettings.scaleMode); const [objectPosition, setObjectPosition] = useState(initialSettings.objectPosition); const [isConverting, setIsConverting] = useState(false); const [convertingIndex, setConvertingIndex] = useState(null); const [isDraggingOver, setIsDraggingOver] = useState(false); const fileInputRef = useRef(null); useEffect(() => { return () => { previewUrls.forEach((url) => URL.revokeObjectURL(url)); }; }, [previewUrls]); const handleFiles = (files: FileList | null) => { if (!files || files.length === 0) return; const imageFiles = Array.from(files).filter((file) => file.type.startsWith("image/") ); if (imageFiles.length === 0) { toast.error(t('toasts.no_valid_files')); return; } const newImages = [...images, ...imageFiles]; const newPreviewUrls = [ ...previewUrls, ...imageFiles.map((file) => URL.createObjectURL(file)), ]; const newFilenames = [ ...filenames, ...imageFiles.map((file) => useDefaultBaseName && defaultBaseName ? defaultBaseName : file.name.substring(0, file.name.lastIndexOf(".")) ), ]; setImages(newImages); setPreviewUrls(newPreviewUrls); setFilenames(newFilenames); toast.success(t('toasts.images_added', { count: imageFiles.length })); }; const handleImageChange = (e: ChangeEvent) => { handleFiles(e.target.files); if (e.target) e.target.value = ""; }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); setIsDraggingOver(true); }; const handleDragLeave = (e: React.DragEvent) => { e.preventDefault(); setIsDraggingOver(false); }; const handleDrop = (e: React.DragEvent) => { e.preventDefault(); setIsDraggingOver(false); handleFiles(e.dataTransfer.files); }; const handleRemoveImage = (indexToRemove: number) => { URL.revokeObjectURL(previewUrls[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 = () => { previewUrls.forEach((url) => URL.revokeObjectURL(url)); setImages([]); setPreviewUrls([]); setFilenames([]); setWidth(initialSettings.width); setHeight(initialSettings.height); toast.info(t('toasts.all_cleared')); }; const handleFilenameChange = (index: number, newName: string) => { const newFilenames = [...filenames]; newFilenames[index] = newName; setFilenames(newFilenames); }; const generateFinalFilename = (index: number) => { const baseName = filenames[index] || "filename"; let finalName = `${prefix}${baseName}${suffix}`; if (useCounter) { const counter = (index + counterStart).toString().padStart(counterDigits, '0'); finalName += `${counter}`; } return finalName; }; const convertAndDownload = (image: File, previewUrl: string, index: number) => { return new Promise((resolve, reject) => { const img = new Image(); img.crossOrigin = "anonymous"; img.src = previewUrl; img.onload = () => { const canvas = document.createElement("canvas"); const sourceRatio = img.naturalWidth / img.naturalHeight; let targetWidth: number; let targetHeight: number; const inputWidth = width ? Number(width) : 0; const inputHeight = height ? Number(height) : 0; if (inputWidth && !inputHeight) { targetWidth = inputWidth; targetHeight = Math.round(targetWidth / sourceRatio); } else if (!inputWidth && inputHeight) { targetHeight = inputHeight; targetWidth = Math.round(targetHeight * sourceRatio); } else if (inputWidth && inputHeight) { targetWidth = inputWidth; targetHeight = inputHeight; } else { targetWidth = img.naturalWidth; targetHeight = img.naturalHeight; } if (keepOrientation && (inputWidth || inputHeight)) { const isOriginalPortrait = img.naturalHeight > img.naturalWidth; const isTargetPortrait = targetHeight > targetWidth; if (isOriginalPortrait !== isTargetPortrait) { [targetWidth, targetHeight] = [targetHeight, targetWidth]; } } canvas.width = targetWidth; canvas.height = targetHeight; const ctx = canvas.getContext("2d"); if (ctx) { const sWidth = img.naturalWidth; const sHeight = img.naturalHeight; const dWidth = targetWidth; const dHeight = targetHeight; if (scaleMode === 'fill' || !width || !height) { ctx.drawImage(img, 0, 0, dWidth, dHeight); } else { const sourceRatio = sWidth / sHeight; const targetRatio = dWidth / dHeight; let sx = 0, sy = 0, sRenderWidth = sWidth, sRenderHeight = sHeight; let dx = 0, dy = 0, dRenderWidth = dWidth, dRenderHeight = dHeight; const [hPos, vPos] = objectPosition.split(' '); if (scaleMode === 'cover') { if (sourceRatio > targetRatio) { sRenderHeight = sHeight; sRenderWidth = sHeight * targetRatio; if (hPos === 'center') sx = (sWidth - sRenderWidth) / 2; if (hPos === 'right') sx = sWidth - sRenderWidth; } else { sRenderWidth = sWidth; sRenderHeight = sWidth / targetRatio; if (vPos === 'center') sy = (sHeight - sRenderHeight) / 2; if (vPos === 'bottom') sy = sHeight - sRenderHeight; } ctx.drawImage(img, sx, sy, sRenderWidth, sRenderHeight, 0, 0, dWidth, dHeight); } else if (scaleMode === 'contain') { if (sourceRatio > targetRatio) { dRenderWidth = dWidth; dRenderHeight = dWidth / sourceRatio; if (vPos === 'center') dy = (dHeight - dRenderHeight) / 2; if (vPos === 'bottom') dy = dHeight - dRenderHeight; } else { dRenderHeight = dHeight; dRenderWidth = dHeight * sourceRatio; if (hPos === 'center') dx = (dWidth - dRenderWidth) / 2; if (hPos === 'right') dx = dWidth - dRenderWidth; } ctx.drawImage(img, 0, 0, sWidth, sHeight, dx, dy, dRenderWidth, dRenderHeight); } } const mimeType = `image/${format}`; const dataUrl = canvas.toDataURL(mimeType, format === 'png' ? undefined : quality / 100); const link = document.createElement("a"); link.href = dataUrl; link.download = `${generateFinalFilename(index)}.${format}`; document.body.appendChild(link); link.click(); document.body.removeChild(link); resolve(); } else { reject(new Error(t('toasts.error_processing', { filename: image.name }))); } }; img.onerror = () => { reject(new Error(t('toasts.error_loading', { filename: image.name }))); }; }); }; const handleConvertAndDownloadAll = async () => { if (images.length === 0) { toast.error(t('toasts.upload_images_first')); return; } setIsConverting(true); toast.info(t('toasts.conversion_start_all', { count: images.length })); const conversionPromises = images.map((image, index) => convertAndDownload(image, previewUrls[index], index) ); try { await Promise.all(conversionPromises); toast.success(t('toasts.conversion_success_all', { count: images.length })); } catch (error) { if (error instanceof Error) { toast.error(error.message); } else { toast.error(t('toasts.conversion_error')); } } finally { setIsConverting(false); } }; const handleConvertAndDownloadSingle = async (index: number) => { setConvertingIndex(index); toast.info(t('toasts.conversion_start_single', { filename: filenames[index] })); try { await convertAndDownload(images[index], previewUrls[index], index); toast.success(t('toasts.conversion_success_single', { filename: filenames[index] })); } catch (error) { if (error instanceof Error) { toast.error(error.message); } else { toast.error(t('toasts.conversion_error')); } } finally { setConvertingIndex(null); } }; const handleApplySettings = () => { toast.info(t('toasts.settings_applied')); }; const handleResetSettings = () => { setWidth(initialSettings.width); setHeight(initialSettings.height); setAspectRatio(initialSettings.aspectRatio); setKeepOrientation(initialSettings.keepOrientation); setFormat(initialSettings.format); setQuality(initialSettings.quality); setPrefix(initialSettings.prefix); setSuffix(initialSettings.suffix); setUseCounter(initialSettings.useCounter); setCounterStart(initialSettings.counterStart); setCounterDigits(initialSettings.counterDigits); setUseDefaultBaseName(initialSettings.useDefaultBaseName); setDefaultBaseName(initialSettings.defaultBaseName); setScaleMode(initialSettings.scaleMode); setObjectPosition(initialSettings.objectPosition); toast.success(t('toasts.settings_reset')); }; const handleAspectRatioChange = (value: string) => { setAspectRatio(value); if (value === "custom") { return; } const [w, h] = value.split("/").map(Number); 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: React.ChangeEvent) => { setWidth(e.target.value); setAspectRatio("custom"); }; const handleHeightChange = (e: React.ChangeEvent) => { setHeight(e.target.value); setAspectRatio("custom"); }; const handleSwapDimensions = () => { setWidth(height); setHeight(width); }; const hasImages = images.length > 0; const handleApplyDefaultBaseNameToAll = () => { if (!defaultBaseName) { toast.error(t('toasts.base_name_required')); return; } if (!hasImages) { toast.info(t('toasts.upload_first')); return; } const newFilenames = filenames.map(() => defaultBaseName); setFilenames(newFilenames); toast.success(t('toasts.base_name_applied', { baseName: defaultBaseName, count: images.length })); }; return (

{t('upload.title')}

fileInputRef.current?.click()} >

{t('upload.cta')}

{t('upload.supported')}

{hasImages && (
{t('uploaded.title')}

{t('uploaded.clear_all_tooltip')}

{t('uploaded.download_all_tooltip')}

{previewUrls.map((url, index) => { const finalFilename = generateFinalFilename(index); return (
{`Preview
handleFilenameChange(index, e.target.value)} className="text-sm font-medium h-8 mt-1" />

{t('uploaded.final_name', { filename: `${finalFilename}.${format}` })}

{t('uploaded.download_image_tooltip')}

{t('uploaded.remove_image_tooltip')}

); })}
)}

{t('settings.image.title')}

{t('settings.image.description')}

{t('settings.image.aspect_ratio_tooltip')}

{t('settings.image.width_tooltip')}

{t('settings.image.swap_tooltip')}

{t('settings.image.height_tooltip')}

setKeepOrientation(Boolean(checked))} />

{t('settings.image.scaling_tooltip')}

{scaleMode !== 'fill' && (

{t('settings.image.position_tooltip')}

setObjectPosition(pos as string)} />
)}

{t('settings.filename.title')}

{t('settings.filename.description')}

{useDefaultBaseName && (
setDefaultBaseName(e.target.value)} />

{t('settings.filename.apply_to_all_tooltip')}

)}

{t('settings.filename.prefix_tooltip')}

setPrefix(e.target.value)} />

{t('settings.filename.suffix_tooltip')}

setSuffix(e.target.value)} />
{useCounter && (

{t('settings.filename.start_number_tooltip')}

setCounterStart(Math.max(0, Number(e.target.value)))} min="0" />

{t('settings.filename.padding_digits_tooltip')}

setCounterDigits(Math.max(1, Number(e.target.value)))} min="1" />
)}

{t('settings.quality.title')}

{t('settings.quality.description')}

{t('settings.quality.format_tooltip')}

{t('settings.quality.quality_tooltip')}

{quality}%
setQuality(value[0])} disabled={format === 'png'} /> {format === 'png' && (

{t('settings.quality.png_disabled_notice')}

)}

{t('settings.reset_tooltip')}

{t('settings.apply_tooltip')}

); }