import { SOCIAL_MEDIA_PRESETS } from "@snapotter/shared"; import { Download, Info, Link, Unlink } from "lucide-react"; import { useCallback, 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 { format } from "@/lib/format"; import { useFileStore } from "@/stores/file-store"; type ResizeTab = "presets" | "custom" | "scale" | "content-aware"; type FitMode = "cover" | "contain" | "fill"; const FIT_MODES: FitMode[] = ["cover", "contain", "fill"]; // Group presets by platform const platforms = [...new Set(SOCIAL_MEDIA_PRESETS.map((p) => p.platform))]; function HintIcon({ text }: { text: string }) { return ( {text} ); } export interface ResizeControlsProps { settings?: Record; onChange?: (settings: Record) => void; } export function ResizeControls({ settings: initialSettings, onChange }: ResizeControlsProps) { const { t } = useTranslation(); const [tab, setTab] = useState("custom"); const [selectedPreset, setSelectedPreset] = useState(null); const [width, setWidth] = useState(""); const [height, setHeight] = useState(""); const [percentage, setPercentage] = useState("50"); const [fit, setFit] = useState("cover"); const [lockAspect, setLockAspect] = useState(true); const [withoutEnlargement, setWithoutEnlargement] = useState(false); const contentAware = tab === "content-aware"; const [protectFaces, setProtectFaces] = useState(false); const [blurRadius, setBlurRadius] = useState(4); const [sobelThreshold, setSobelThreshold] = useState(2); const [squareMode, setSquareMode] = useState(false); const initializedRef = useRef(false); useEffect(() => { if (!initialSettings || initializedRef.current) return; initializedRef.current = true; if (initialSettings.width != null) setWidth(String(initialSettings.width)); if (initialSettings.height != null) setHeight(String(initialSettings.height)); if (initialSettings.percentage != null) setPercentage(String(initialSettings.percentage)); if (initialSettings.fit != null) setFit(initialSettings.fit as FitMode); if (initialSettings.withoutEnlargement != null) setWithoutEnlargement(Boolean(initialSettings.withoutEnlargement)); if (initialSettings.protectFaces != null) setProtectFaces(Boolean(initialSettings.protectFaces)); if (initialSettings.blurRadius != null) setBlurRadius(Number(initialSettings.blurRadius)); if (initialSettings.sobelThreshold != null) setSobelThreshold(Number(initialSettings.sobelThreshold)); if (initialSettings.square != null) setSquareMode(Boolean(initialSettings.square)); if (initialSettings.contentAware) setTab("content-aware"); else if (initialSettings.percentage != null) setTab("scale"); }, [initialSettings]); const onChangeRef = useRef(onChange); useEffect(() => { onChangeRef.current = onChange; }); useEffect(() => { const settings: Record = {}; if (contentAware) { settings.contentAware = true; if (!squareMode) { if (width) settings.width = Number(width); if (height) settings.height = Number(height); } settings.protectFaces = protectFaces; settings.blurRadius = blurRadius; settings.sobelThreshold = sobelThreshold; settings.square = squareMode; } else if (tab === "scale") { settings.percentage = Number(percentage); } else { if (width) settings.width = Number(width); if (height) settings.height = Number(height); settings.fit = tab === "presets" ? "cover" : fit; settings.withoutEnlargement = withoutEnlargement; } onChangeRef.current?.(settings); }, [ tab, width, height, percentage, fit, withoutEnlargement, protectFaces, blurRadius, sobelThreshold, squareMode, ]); const handlePreset = (preset: (typeof SOCIAL_MEDIA_PRESETS)[number]) => { const key = `${preset.platform}-${preset.name}`; if (selectedPreset === key) { setSelectedPreset(null); setWidth(""); setHeight(""); } else { setSelectedPreset(key); setWidth(String(preset.width)); setHeight(String(preset.height)); } }; const tabClass = (t: ResizeTab) => `flex-1 text-xs py-1.5 rounded ${tab === t ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`; const dimensionInputs = (
setWidth(e.target.value)} placeholder="Auto" disabled={squareMode && contentAware} className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground disabled:opacity-50" />
setHeight(e.target.value)} placeholder="Auto" disabled={squareMode && contentAware} className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground disabled:opacity-50" />
); const enlargementCheckbox = ( ); return (
{/* Tab selector */}
{/* Presets tab */} {tab === "presets" && (
{platforms.map((platform) => (

{platform}

{SOCIAL_MEDIA_PRESETS.filter((p) => p.platform === platform).map((preset) => { const key = `${preset.platform}-${preset.name}`; const isSelected = selectedPreset === key; return ( ); })}
))} {enlargementCheckbox}
)} {/* Custom Size tab */} {tab === "custom" && (
{dimensionInputs} {/* Fit mode */}

{t.toolSettings.resize.fitMode}

{FIT_MODES.map((f) => ( ))}
{enlargementCheckbox}
)} {/* Scale tab */} {tab === "scale" && (
setPercentage(e.target.value)} min={1} className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
{[25, 50, 75].map((pct) => ( ))}
)} {/* Content-aware tab */} {contentAware && (
{dimensionInputs} {/* Square mode */} {/* Face protection */} {/* Blur radius */}
{blurRadius}
setBlurRadius(Number(e.target.value))} className="w-full mt-1 h-1.5 rounded-full appearance-none bg-muted accent-primary" />
{/* Sobel threshold */}
{sobelThreshold}
setSobelThreshold(Number(e.target.value))} className="w-full mt-1 h-1.5 rounded-full appearance-none bg-muted accent-primary" />
)}
); } export function ResizeSettings() { const { t } = useTranslation(); const { files } = useFileStore(); const standardResize = useToolProcessor("resize"); const contentAwareResize = useToolProcessor("content-aware-resize"); const [settings, setSettings] = useState>({}); const [isContentAware, setIsContentAware] = useState(false); const handleSettingsChange = useCallback((newSettings: Record) => { setSettings(newSettings); setIsContentAware(!!newSettings.contentAware); }, []); const active = isContentAware ? contentAwareResize : standardResize; const { processFiles, processAllFiles, processing, error, downloadUrl, progress } = active; const handleProcess = () => { if (files.length > 1) { processAllFiles(files, settings); } else { processFiles(files, settings); } }; const hasFile = files.length > 0; const tab = settings.percentage !== undefined ? "scale" : "other"; const canProcess = hasFile && !processing && (isContentAware ? Boolean(settings.width) || Boolean(settings.height) || Boolean(settings.square) : tab === "scale" ? Number(settings.percentage) > 0 : Boolean(settings.width) || Boolean(settings.height)); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (canProcess) handleProcess(); }; return (
{/* Error */} {error &&

{error}

} {/* Process button */} {processing ? ( ) : ( )} {/* Download */} {downloadUrl && ( {t.common.download} )} ); }