import { useCallback, useState } from "preact/hooks"; export function usePhotoEditorState() { const [state, setState] = useState({ previewUrl: null as string | null, cropSize: 256, crop: { x: 0, y: 0, size: 256 }, zoom: 1.0, minZoom: 0.2, maxWidth: 0, maxHeight: 0, baseDimensions: { width: 0, height: 0 }, isLandscape: true, dragging: false, heicToLoaded: false, zoomInDisabled: false, }); const updateState = useCallback((updates: Partial) => { setState((prev) => ({ ...prev, ...updates })); }, []); const resetCropState = useCallback(() => { setState((prev) => ({ ...prev, cropSize: 256, crop: { x: 0, y: 0, size: 256 }, zoom: Math.max(1, prev.minZoom), })); }, []); return { state, updateState, resetCropState }; } export function useImageProcessor(state: { heicToLoaded: boolean }) { const [processing, setProcessing] = useState(false); const processImageFile = useCallback( async (file: File): Promise => { setProcessing(true); try { const validImageTypes = [ "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", "image/bmp", "image/svg+xml", "image/heic", "image/heif", ]; const fileExtension = file.name.toLowerCase().split(".").pop(); const isImageByType = file.type.startsWith("image/") || validImageTypes.includes(file.type); const isImageByExtension = [ "jpg", "jpeg", "png", "gif", "webp", "bmp", "svg", "heic", "heif", ] .includes(fileExtension || ""); if (!isImageByType && !isImageByExtension) { alert( `Unsupported file type: ${ file.type || "unknown" }\n\nPlease select an image file.`, ); return null; } const maxRecommendedSize = 20 * 1024 * 1024; if (file.size > maxRecommendedSize) { const sizeMB = (file.size / (1024 * 1024)).toFixed(1); const proceed = confirm( `Large file detected (${sizeMB}MB).\n\nProcessing large images may be slow or cause the browser to become unresponsive.\n\nDo you want to continue?`, ); if (!proceed) return null; } let processedFile: Blob = file; if (state.heicToLoaded && globalThis.isHeic && globalThis.heicTo) { try { const isHeicFile = await globalThis.isHeic(file); if (isHeicFile) { processedFile = await globalThis.heicTo({ blob: file, type: "image/bmp", quality: 1.0, }); } } catch (error) { console.error("HEIC conversion failed:", error); } } const dataUrl = URL.createObjectURL(processedFile); return Promise.resolve(dataUrl); } finally { setProcessing(false); } }, [state.heicToLoaded], ); return { processImageFile, processing }; } export function useCropCalculator( state: { isLandscape: boolean; baseDimensions: { width: number; height: number }; zoom: number; cropSize: number; maxHeight: number; maxWidth: number; }, updateState: (updates: Record) => void, ) { const getPixelDimensions = useCallback(() => { function getPixelDimensions() { const longerDimension = state.isLandscape ? state.baseDimensions.width : state.baseDimensions.height; if (longerDimension <= 0) { return { width: 256, height: 256, actualZoom: 1, overRunPercent: 0, }; } const calculatedDimension = Math.round(longerDimension * state.zoom); let clampedDimension = Math.max(calculatedDimension, state.cropSize); let constrainedByMax = false; if ( !state.isLandscape && state.maxHeight > 0 && clampedDimension > state.maxHeight ) { clampedDimension = state.maxHeight; constrainedByMax = true; } else if ( state.isLandscape && state.maxWidth > 0 && clampedDimension > state.maxWidth ) { clampedDimension = state.maxWidth; constrainedByMax = true; } const actualZoom = clampedDimension / longerDimension; let overRunPercent = constrainedByMax && actualZoom > 0 ? ((state.zoom - actualZoom) / actualZoom) : 0; if (!isFinite(overRunPercent)) { overRunPercent = 0; } if (state.isLandscape) { return { width: Math.max(256, clampedDimension), height: "auto" as const, actualZoom: Math.max(0.01, actualZoom), overRunPercent, }; } else { return { width: "auto" as const, height: Math.max(256, clampedDimension), actualZoom: Math.max(0.01, actualZoom), overRunPercent, }; } } return getPixelDimensions(); }, [state, updateState]); return { getPixelDimensions }; }