[dyad] Refactoring the app for modularity - wrote 12 file(s)
This commit is contained in:
190
src/hooks/use-image-converter.ts
Normal file
190
src/hooks/use-image-converter.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { ImageFile, ConversionSettings } from "@/types";
|
||||
import { processImage, generateFinalFilename, downloadDataUrl } from "@/lib/image-processor";
|
||||
|
||||
export const initialSettings: ConversionSettings = {
|
||||
width: "",
|
||||
height: "",
|
||||
aspectRatio: "custom",
|
||||
keepOrientation: true,
|
||||
format: "webp",
|
||||
quality: 90,
|
||||
prefix: "",
|
||||
suffix: "",
|
||||
useCounter: false,
|
||||
counterStart: 1,
|
||||
counterDigits: 3,
|
||||
useDefaultBaseName: false,
|
||||
defaultBaseName: "",
|
||||
scaleMode: 'cover',
|
||||
objectPosition: 'center center',
|
||||
};
|
||||
|
||||
export function useImageConverter() {
|
||||
const [images, setImages] = useState<ImageFile[]>([]);
|
||||
const [settings, setSettings] = useState<ConversionSettings>(initialSettings);
|
||||
const [isConverting, setIsConverting] = useState(false);
|
||||
const [convertingIndex, setConvertingIndex] = useState<number | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const urls = images.map(img => img.previewUrl);
|
||||
return () => {
|
||||
urls.forEach(url => URL.revokeObjectURL(url));
|
||||
};
|
||||
}, [images]);
|
||||
|
||||
const updateSettings = useCallback((newSettings: Partial<ConversionSettings>) => {
|
||||
setSettings(prev => ({ ...prev, ...newSettings }));
|
||||
}, []);
|
||||
|
||||
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("No valid image files found.");
|
||||
return;
|
||||
}
|
||||
|
||||
const newImageFiles: ImageFile[] = imageFiles.map(file => ({
|
||||
file,
|
||||
previewUrl: URL.createObjectURL(file),
|
||||
filename: settings.useDefaultBaseName && settings.defaultBaseName
|
||||
? settings.defaultBaseName
|
||||
: file.name.substring(0, file.name.lastIndexOf(".")),
|
||||
}));
|
||||
|
||||
setImages(prev => [...prev, ...newImageFiles]);
|
||||
toast.success(`${imageFiles.length} image(s) added.`);
|
||||
}, [settings.useDefaultBaseName, settings.defaultBaseName]);
|
||||
|
||||
const handleRemoveImage = useCallback((indexToRemove: number) => {
|
||||
setImages(prev => {
|
||||
const imageToRemove = prev[indexToRemove];
|
||||
if (imageToRemove) {
|
||||
URL.revokeObjectURL(imageToRemove.previewUrl);
|
||||
}
|
||||
return prev.filter((_, i) => i !== indexToRemove);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleClearAll = useCallback(() => {
|
||||
setImages([]);
|
||||
updateSettings({ width: initialSettings.width, height: initialSettings.height });
|
||||
toast.info("All images cleared.");
|
||||
}, [updateSettings]);
|
||||
|
||||
const handleFilenameChange = useCallback((index: number, newName: string) => {
|
||||
setImages(prev => {
|
||||
const newImages = [...prev];
|
||||
if (newImages[index]) {
|
||||
newImages[index].filename = newName;
|
||||
}
|
||||
return newImages;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleConvertAndDownloadSingle = useCallback(async (index: number) => {
|
||||
setConvertingIndex(index);
|
||||
toast.info(`Starting conversion for ${images[index].filename}...`);
|
||||
try {
|
||||
const imageToConvert = images[index];
|
||||
const dataUrl = await processImage(imageToConvert, settings);
|
||||
const finalFilename = generateFinalFilename(imageToConvert.filename, settings, index);
|
||||
downloadDataUrl(dataUrl, `${finalFilename}.${settings.format}`);
|
||||
toast.success(`Successfully exported ${imageToConvert.filename}!`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "An unknown error occurred.";
|
||||
toast.error(message);
|
||||
} finally {
|
||||
setConvertingIndex(null);
|
||||
}
|
||||
}, [images, settings]);
|
||||
|
||||
const handleConvertAndDownloadAll = useCallback(async () => {
|
||||
if (images.length === 0) {
|
||||
toast.error("Please upload images first.");
|
||||
return;
|
||||
}
|
||||
setIsConverting(true);
|
||||
toast.info(`Starting conversion for ${images.length} images...`);
|
||||
|
||||
const conversionPromises = images.map(async (image, index) => {
|
||||
try {
|
||||
const dataUrl = await processImage(image, settings);
|
||||
const finalFilename = generateFinalFilename(image.filename, settings, index);
|
||||
downloadDataUrl(dataUrl, `${finalFilename}.${settings.format}`);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : `Failed to process ${image.filename}`;
|
||||
toast.error(message);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await Promise.all(conversionPromises);
|
||||
toast.success(`Successfully exported all ${images.length} images!`);
|
||||
} catch (error) {
|
||||
toast.error("Some images failed to convert. See individual errors.");
|
||||
} finally {
|
||||
setIsConverting(false);
|
||||
}
|
||||
}, [images, settings]);
|
||||
|
||||
const handleResetSettings = useCallback(() => {
|
||||
setSettings(initialSettings);
|
||||
toast.success("All settings have been reset to their defaults.");
|
||||
}, []);
|
||||
|
||||
const handleAspectRatioChange = useCallback((value: string) => {
|
||||
updateSettings({ aspectRatio: value });
|
||||
if (value === "custom") return;
|
||||
|
||||
const [w, h] = value.split("/").map(Number);
|
||||
let newWidth: number, 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;
|
||||
}
|
||||
updateSettings({ width: newWidth, height: newHeight });
|
||||
}, [updateSettings]);
|
||||
|
||||
const handleSwapDimensions = useCallback(() => {
|
||||
updateSettings({ width: settings.height, height: settings.width });
|
||||
}, [settings.height, settings.width, updateSettings]);
|
||||
|
||||
const handleApplyDefaultBaseNameToAll = useCallback(() => {
|
||||
if (!settings.defaultBaseName) {
|
||||
toast.error("Please enter a default base name to apply.");
|
||||
return;
|
||||
}
|
||||
if (images.length === 0) {
|
||||
toast.info("Upload some images first.");
|
||||
return;
|
||||
}
|
||||
setImages(prev => prev.map(img => ({ ...img, filename: settings.defaultBaseName })));
|
||||
toast.success(`Set base name to "${settings.defaultBaseName}" for all ${images.length} images.`);
|
||||
}, [images.length, settings.defaultBaseName]);
|
||||
|
||||
return {
|
||||
images,
|
||||
settings,
|
||||
isConverting,
|
||||
convertingIndex,
|
||||
updateSettings,
|
||||
handleFiles,
|
||||
handleRemoveImage,
|
||||
handleClearAll,
|
||||
handleFilenameChange,
|
||||
handleConvertAndDownloadSingle,
|
||||
handleConvertAndDownloadAll,
|
||||
handleResetSettings,
|
||||
handleAspectRatioChange,
|
||||
handleSwapDimensions,
|
||||
handleApplyDefaultBaseNameToAll,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user