2026-01-20 09:02:26 -06:00

193 lines
5.1 KiB
TypeScript

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<typeof state>) => {
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<string | null> => {
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<string, unknown>) => 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 };
}