Reverted all changes back to version a64ce49523

This commit is contained in:
[dyad]
2026-01-18 12:56:23 +01:00
parent 9ba9b13e21
commit ce867c5431
8 changed files with 826 additions and 1168 deletions

View File

@@ -1,350 +0,0 @@
"use client";
import { useState, useRef, ChangeEvent, useEffect, useCallback } from "react";
import { toast } from "sonner";
import { useTranslations } from "next-intl";
export type ScaleMode = 'fill' | 'cover' | 'contain';
export type ImageFormat = "png" | "jpeg" | "webp";
export function useImageConverter() {
const t = useTranslations("ImageConverter");
const [images, setImages] = useState<File[]>([]);
const [previewUrls, setPreviewUrls] = useState<string[]>([]);
const [filenames, setFilenames] = useState<string[]>([]);
const [width, setWidth] = useState<number | string>("");
const [height, setHeight] = useState<number | string>("");
const [aspectRatio, setAspectRatio] = useState<string>("custom");
const [keepOrientation, setKeepOrientation] = useState<boolean>(true);
const [format, setFormat] = useState<ImageFormat>("webp");
const [quality, setQuality] = useState<number>(90);
const [prefix, setPrefix] = useState<string>("");
const [suffix, setSuffix] = useState<string>("");
const [useCounter, setUseCounter] = useState<boolean>(false);
const [counterStart, setCounterStart] = useState<number>(1);
const [counterDigits, setCounterDigits] = useState<number>(3);
const [useDefaultBaseName, setUseDefaultBaseName] = useState<boolean>(false);
const [defaultBaseName, setDefaultBaseName] = useState<string>("");
const [scaleMode, setScaleMode] = useState<ScaleMode>('cover');
const [objectPosition, setObjectPosition] = useState<string>('center center');
const [isConverting, setIsConverting] = useState(false);
const [convertingIndex, setConvertingIndex] = useState<number | null>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
return () => {
previewUrls.forEach((url) => URL.revokeObjectURL(url));
};
}, [previewUrls]);
const handleFiles = useCallback((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.noValidImages"));
return;
}
setImages((prev) => [...prev, ...imageFiles]);
setPreviewUrls((prev) => [...prev, ...imageFiles.map((file) => URL.createObjectURL(file))]);
setFilenames((prev) => [
...prev,
...imageFiles.map((file) =>
useDefaultBaseName && defaultBaseName
? defaultBaseName
: file.name.substring(0, file.name.lastIndexOf("."))
),
]);
toast.success(t("toasts.imagesAdded", { count: imageFiles.length }));
}, [defaultBaseName, useDefaultBaseName, t]);
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 handleClearAll = () => {
previewUrls.forEach((url) => URL.revokeObjectURL(url));
setImages([]);
setPreviewUrls([]);
setFilenames([]);
toast.info(t("toasts.allCleared"));
};
const handleFilenameChange = (index: number, newName: string) => {
setFilenames((prev) => {
const newFilenames = [...prev];
newFilenames[index] = newName;
return newFilenames;
});
};
const generateFinalFilename = useCallback((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;
}, [filenames, prefix, suffix, useCounter, counterStart, counterDigits]);
const convertAndDownload = useCallback((image: File, previewUrl: string, index: number) => {
return new Promise<void>((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.src = previewUrl;
img.onload = () => {
const canvas = document.createElement("canvas");
let targetWidth = width ? Number(width) : img.naturalWidth;
let targetHeight = height ? Number(height) : img.naturalHeight;
if (keepOrientation && width && height) {
const isOriginalPortrait = img.naturalHeight > img.naturalWidth;
const isTargetPortrait = Number(height) > Number(width);
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;
const dimensionSuffix = width && height ? `_${targetWidth}x${targetHeight}` : '';
link.download = `${generateFinalFilename(index)}${dimensionSuffix}.${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.`));
};
});
}, [width, height, keepOrientation, scaleMode, objectPosition, format, quality, generateFinalFilename]);
const handleConvertAndDownloadAll = async () => {
if (images.length === 0) {
toast.error(t("toasts.noImages"));
return;
}
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(conversionPromises);
toast.success(t("toasts.conversionSuccess", { count: images.length }));
} catch (error) {
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error(t("toasts.conversionError"));
}
} finally {
setIsConverting(false);
}
};
const handleConvertAndDownloadSingle = async (index: number) => {
setConvertingIndex(index);
toast.info(t("toasts.singleConversionStarting", { filename: filenames[index] }));
try {
await convertAndDownload(images[index], previewUrls[index], index);
toast.success(t("toasts.singleConversionSuccess", { filename: filenames[index] }));
} catch (error) {
if (error instanceof Error) {
toast.error(error.message);
} else {
toast.error(t("toasts.conversionError"));
}
} finally {
setConvertingIndex(null);
}
};
const handleApplySettings = () => {
toast.info(t("toasts.settingsApplied"));
};
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<HTMLInputElement>) => {
setWidth(e.target.value);
setAspectRatio("custom");
};
const handleHeightChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setHeight(e.target.value);
setAspectRatio("custom");
};
const handleSwapDimensions = () => {
setWidth(height);
setHeight(width);
};
const handleApplyDefaultBaseNameToAll = () => {
if (!defaultBaseName) {
toast.error(t("toasts.noDefaultBaseName"));
return;
}
if (images.length === 0) {
toast.info(t("toasts.uploadImagesFirst"));
return;
}
setFilenames((prev) => prev.map(() => defaultBaseName));
toast.success(t("toasts.baseNameApplied", { baseName: defaultBaseName, count: images.length }));
};
return {
t,
images,
previewUrls,
filenames,
width,
height,
aspectRatio,
keepOrientation,
format,
quality,
prefix,
suffix,
useCounter,
counterStart,
counterDigits,
useDefaultBaseName,
defaultBaseName,
scaleMode,
objectPosition,
isConverting,
convertingIndex,
fileInputRef,
setWidth,
setHeight,
setAspectRatio,
setKeepOrientation,
setFormat,
setQuality,
setPrefix,
setSuffix,
setUseCounter,
setCounterStart,
setCounterDigits,
setUseDefaultBaseName,
setDefaultBaseName,
setScaleMode,
setObjectPosition,
handleFiles,
handleRemoveImage,
handleClearAll,
handleFilenameChange,
generateFinalFilename,
handleConvertAndDownloadAll,
handleConvertAndDownloadSingle,
handleApplySettings,
handleAspectRatioChange,
handleWidthChange,
handleHeightChange,
handleSwapDimensions,
handleApplyDefaultBaseNameToAll,
};
}