diff --git a/apps/web/src/components/tools/crop-settings.tsx b/apps/web/src/components/tools/crop-settings.tsx index 8f787e71..9da3d3b8 100644 --- a/apps/web/src/components/tools/crop-settings.tsx +++ b/apps/web/src/components/tools/crop-settings.tsx @@ -5,6 +5,15 @@ import { ProgressCard } from "@/components/common/progress-card"; import { useToolProcessor } from "@/hooks/use-tool-processor"; import { useFileStore } from "@/stores/file-store"; +const EXTEND_PRESETS = [ + { label: "16:9", aspect: 16 / 9 }, + { label: "1:1", aspect: 1 }, + { label: "4:3", aspect: 4 / 3 }, + { label: "3:2", aspect: 3 / 2 }, + { label: "9:16", aspect: 9 / 16 }, + { label: "4:5", aspect: 4 / 5 }, +]; + const ASPECT_PRESETS = [ { label: "Free", value: undefined as number | undefined }, { label: "1:1", value: 1 }, @@ -26,6 +35,7 @@ export interface CropSettingsProps { onCropChange: (crop: Crop) => void; onAspectChange: (aspect: number | undefined) => void; onGridToggle: (show: boolean) => void; + onModeChange?: (mode: "standard" | "content-aware") => void; } export function CropSettings({ @@ -33,10 +43,14 @@ export function CropSettings({ onCropChange, onAspectChange, onGridToggle, + onModeChange, }: CropSettingsProps) { const { files } = useFileStore(); - const { processFiles, processAllFiles, processing, error, downloadUrl, progress } = - useToolProcessor("crop"); + const standardCrop = useToolProcessor("crop"); + const contentAwareCrop = useToolProcessor("content-aware-crop"); + const [mode, setMode] = useState<"standard" | "content-aware">("standard"); + const active = mode === "content-aware" ? contentAwareCrop : standardCrop; + const { processFiles, processAllFiles, processing, error, downloadUrl, progress } = active; const { crop, aspect, showGrid, imgDimensions } = cropState; @@ -44,6 +58,17 @@ export function CropSettings({ const [customW, setCustomW] = useState("3"); const [customH, setCustomH] = useState("2"); + // Content-aware extension state + const [extendTop, setExtendTop] = useState(0); + const [extendRight, setExtendRight] = useState(0); + const [extendBottom, setExtendBottom] = useState(0); + const [extendLeft, setExtendLeft] = useState(0); + + // Notify parent when mode changes + useEffect(() => { + onModeChange?.(mode); + }, [mode, onModeChange]); + // Convert percentage crop to pixel values const toPixels = useCallback( (c: Crop) => { @@ -180,7 +205,7 @@ export function CropSettings({ height: Math.max(0.1, crop.height), unit: "percent" as const, }; - processAllFiles(files, settings); + standardCrop.processAllFiles(files, settings); } else { const settings = { left: pixels.left, @@ -188,16 +213,60 @@ export function CropSettings({ width: Math.max(1, pixels.width), height: Math.max(1, pixels.height), }; - processFiles(files, settings); + standardCrop.processFiles(files, settings); + } + }; + + const handleExtendPreset = useCallback( + (targetAspect: number) => { + if (!imgDimensions) return; + const { width: w, height: h } = imgDimensions; + const currentAspect = w / h; + let top = 0; + let right = 0; + let bottom = 0; + let left = 0; + if (targetAspect > currentAspect) { + // Need wider -- extend left and right + const newWidth = Math.round(h * targetAspect); + const extra = newWidth - w; + left = Math.round(extra / 2); + right = extra - left; + } else { + // Need taller -- extend top and bottom + const newHeight = Math.round(w / targetAspect); + const extra = newHeight - h; + top = Math.round(extra / 2); + bottom = extra - top; + } + setExtendTop(top); + setExtendRight(right); + setExtendBottom(bottom); + setExtendLeft(left); + }, + [imgDimensions], + ); + + const handleContentAwareProcess = () => { + const settings = { extendTop, extendRight, extendBottom, extendLeft }; + if (files.length > 1) { + contentAwareCrop.processAllFiles(files, settings); + } else { + contentAwareCrop.processFiles(files, settings); } }; const hasFile = files.length > 0; const hasSize = pixels.width > 0 && pixels.height > 0; + const hasExtension = extendTop > 0 || extendRight > 0 || extendBottom > 0 || extendLeft > 0; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); - if (hasFile && hasSize && !processing) handleProcess(); + if (mode === "content-aware") { + if (hasFile && hasExtension && !processing) handleContentAwareProcess(); + } else { + if (hasFile && hasSize && !processing) handleProcess(); + } }; // Find which preset label matches the current aspect @@ -209,155 +278,280 @@ export function CropSettings({ return false; })?.label; + const canSubmit = + mode === "content-aware" + ? hasFile && hasExtension && !processing + : hasFile && hasSize && !processing; + return (