import { Download } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { ProgressCard } from "@/components/common/progress-card"; import { useTranslation } from "@/contexts/i18n-context"; import { useToolProcessor } from "@/hooks/use-tool-processor"; import { useFileStore } from "@/stores/file-store"; const TARGET_OPTIONS = [ { value: "16:9", label: "16:9" }, { value: "9:16", label: "9:16" }, { value: "1:1", label: "1:1" }, { value: "4:3", label: "4:3" }, { value: "3:4", label: "3:4" }, { value: "custom", label: "Custom" }, ] as const; /** Mirror of the backend canvasFor helper for preview geometry. */ function canvasFor(w: number, h: number, target: string): { cw: number; ch: number } { const [tw, th] = target.split(":").map(Number); const ratio = tw / th; const src = w / h; if (src > ratio) return { cw: w, ch: Math.round(w / ratio) }; return { cw: Math.round(h * ratio), ch: h }; } interface ImagePadSettingsProps { onImageStyle?: (style: React.CSSProperties | null) => void; onImageOverlay?: (node: React.ReactNode) => void; } export function ImagePadSettings({ onImageStyle, onImageOverlay }: ImagePadSettingsProps) { const { t } = useTranslation(); const { files } = useFileStore(); const entry = useFileStore((s) => s.entries[s.selectedIndex]); const blobUrl = entry?.blobUrl; const { processFiles, processAllFiles, processing, error, downloadUrl, progress } = useToolProcessor("image-pad"); const [target, setTarget] = useState("1:1"); const [ratioW, setRatioW] = useState(1); const [ratioH, setRatioH] = useState(1); const [background, setBackground] = useState<"color" | "transparent" | "blur">("color"); const [color, setColor] = useState("#ffffff"); const [padding, setPadding] = useState(0); const [dims, setDims] = useState<{ w: number; h: number } | null>(null); // Natural image dimensions useEffect(() => { if (!blobUrl) { setDims(null); return; } const img = new Image(); img.onload = () => setDims({ w: img.naturalWidth, h: img.naturalHeight }); img.src = blobUrl; }, [blobUrl]); // Stable refs for preview callbacks to avoid stale closures const onImageStyleRef = useRef(onImageStyle); useEffect(() => { onImageStyleRef.current = onImageStyle; }); const onImageOverlayRef = useRef(onImageOverlay); useEffect(() => { onImageOverlayRef.current = onImageOverlay; }); // Live preview: padded-canvas overlay covering the pane image. // onImageStyle({}) activates the wrapper branch in image-viewer // so onImageOverlay children actually mount. useEffect(() => { if (!blobUrl || !dims) return; onImageStyleRef.current?.({}); const w = dims.w; const h = dims.h; const ratioStr = target === "custom" ? `${ratioW}:${ratioH}` : target; const { cw, ch } = canvasFor(w, h, ratioStr); const margin = padding > 0 ? Math.round((Math.max(cw, ch) * padding) / 100) : 0; const finalW = cw + margin * 2; const finalH = ch + margin * 2; // Fit the canvas (which may have a different AR) inside the overlay // (which has the source image AR). const canvasAR = finalW / finalH; const overlayAR = w / h; const [canvasWPct, canvasHPct] = canvasAR > overlayAR ? [100, (overlayAR / canvasAR) * 100] : [(canvasAR / overlayAR) * 100, 100]; // Image positioning within the canvas const imgWPct = (w / finalW) * 100; const imgHPct = (h / finalH) * 100; const imgLeftPct = ((finalW - w) / (2 * finalW)) * 100; const imgTopPct = ((finalH - h) / (2 * finalH)) * 100; const checkerStyle: React.CSSProperties = { backgroundImage: "linear-gradient(45deg,#ccc 25%,transparent 25%),linear-gradient(-45deg,#ccc 25%,transparent 25%),linear-gradient(45deg,transparent 75%,#ccc 75%),linear-gradient(-45deg,transparent 75%,#ccc 75%)", backgroundSize: "16px 16px", backgroundPosition: "0 0,0 8px,8px -8px,-8px 0", backgroundColor: "#fff", }; const bgStyle: React.CSSProperties = background === "transparent" ? checkerStyle : background === "color" ? { backgroundColor: color } : {}; onImageOverlayRef.current?.(
{background === "blur" && ( )}
, ); return () => { onImageStyleRef.current?.(null); onImageOverlayRef.current?.(null); }; }, [blobUrl, dims, target, ratioW, ratioH, background, color, padding]); const handleProcess = () => { const settings = { target, ratioW, ratioH, background, color, padding }; if (files.length > 1) { processAllFiles(files, settings); } else { processFiles(files, settings); } }; const hasFile = files.length > 0; const canProcess = hasFile && !processing; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (canProcess) handleProcess(); }; return (
{/* Aspect Ratio */}

{t.toolSettings["image-pad"].target}

{TARGET_OPTIONS.map((opt) => ( ))}
{target === "custom" && (
setRatioW(Math.max(1, Math.min(100, Number(e.target.value) || 1)))} className="w-16 px-2 py-1 rounded border border-border bg-background text-sm text-foreground text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none" /> : setRatioH(Math.max(1, Math.min(100, Number(e.target.value) || 1)))} className="w-16 px-2 py-1 rounded border border-border bg-background text-sm text-foreground text-center [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none" />
)}
{/* Background Type */}

Background

{(["color", "transparent", "blur"] as const).map((bg) => ( ))}
{/* Color Picker (only for "color" background) */} {background === "color" && (
setColor(e.target.value)} className="w-8 h-8 rounded border border-border shrink-0" /> setColor(e.target.value)} className="flex-1 px-2 py-1 rounded border border-border bg-background text-xs text-foreground font-mono" />
)} {/* Padding Slider */}
Padding {padding}%
setPadding(Number(e.target.value))} className="w-full mt-0.5" />
{error &&

{error}

} {processing ? ( ) : ( )} {downloadUrl && ( {t.common.download} )} ); }