import { Download, FlipHorizontal2, FlipVertical2, Link, RotateCw, Unlink } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { ProgressCard } from "@/components/common/progress-card"; import { useGifInfo } from "@/hooks/use-gif-info"; import { useToolProcessor } from "@/hooks/use-tool-processor"; import { useFileStore } from "@/stores/file-store"; type GifMode = "resize" | "optimize" | "speed" | "reverse" | "extract" | "rotate"; type LoopMode = "infinite" | "once" | "custom"; type ResizeTab = "pixel" | "percentage"; type ExtractTab = "single" | "range" | "all"; const MODES: { id: GifMode; label: string; requiresAnimation: boolean }[] = [ { id: "resize", label: "Resize", requiresAnimation: false }, { id: "optimize", label: "Optimize", requiresAnimation: false }, { id: "speed", label: "Speed", requiresAnimation: true }, { id: "reverse", label: "Reverse", requiresAnimation: true }, { id: "extract", label: "Extract", requiresAnimation: true }, { id: "rotate", label: "Rotate", requiresAnimation: false }, ]; export interface GifToolsControlsProps { settings?: Record; onChange?: (settings: Record) => void; } export function GifToolsControls({ settings: initialSettings, onChange }: GifToolsControlsProps) { const { info, loading: infoLoading } = useGifInfo(); const isAnimated = (info?.pages ?? 0) > 1; // Mode const [mode, setMode] = useState("resize"); // Resize state const [resizeTab, setResizeTab] = useState("pixel"); const [width, setWidth] = useState(""); const [height, setHeight] = useState(""); const [percentage, setPercentage] = useState("50"); const [lockAspect, setLockAspect] = useState(true); // Optimize state const [colors, setColors] = useState(256); const [dither, setDither] = useState(1.0); const [effort, setEffort] = useState(7); // Speed state const [speedFactor, setSpeedFactor] = useState(1.0); // Reverse state const [reverseAdjustSpeed, setReverseAdjustSpeed] = useState(false); const [reverseSpeed, setReverseSpeed] = useState(1.0); // Extract state const [extractTab, setExtractTab] = useState("single"); const [frameNumber, setFrameNumber] = useState("0"); const [frameStart, setFrameStart] = useState("0"); const [frameEnd, setFrameEnd] = useState(""); const [extractFormat, setExtractFormat] = useState<"png" | "webp">("png"); // Rotate state const [angle, setAngle] = useState(null); const [flipH, setFlipH] = useState(false); const [flipV, setFlipV] = useState(false); // Loop control const [loopMode, setLoopMode] = useState("infinite"); const [loopCount, setLoopCount] = useState("2"); // Initialize from saved pipeline settings const initializedRef = useRef(false); useEffect(() => { if (!initialSettings || initializedRef.current) return; initializedRef.current = true; if (initialSettings.mode != null) setMode(initialSettings.mode as GifMode); if (initialSettings.width != null) setWidth(String(initialSettings.width)); if (initialSettings.height != null) setHeight(String(initialSettings.height)); if (initialSettings.percentage != null) setPercentage(String(initialSettings.percentage)); }, [initialSettings]); // Initialize loop from metadata useEffect(() => { if (info) { if (info.loop === 0) setLoopMode("infinite"); else if (info.loop === 1) setLoopMode("once"); else { setLoopMode("custom"); setLoopCount(String(info.loop)); } } }, [info]); // Emit settings const onChangeRef = useRef(onChange); useEffect(() => { onChangeRef.current = onChange; }); useEffect(() => { const loopValue = loopMode === "infinite" ? 0 : loopMode === "once" ? 1 : Number(loopCount) || 2; const settings: Record = { mode, loop: loopValue }; switch (mode) { case "resize": if (resizeTab === "percentage") { settings.percentage = Number(percentage) || 50; } else { if (width) settings.width = Number(width); if (height) settings.height = Number(height); } break; case "optimize": settings.colors = colors; settings.dither = dither; settings.effort = effort; break; case "speed": settings.speedFactor = speedFactor; break; case "reverse": if (reverseAdjustSpeed) { settings.speedFactor = reverseSpeed; } break; case "extract": settings.extractMode = extractTab; settings.extractFormat = extractFormat; if (extractTab === "single") { settings.frameNumber = Number(frameNumber) || 0; } else if (extractTab === "range") { settings.frameStart = Number(frameStart) || 0; if (frameEnd) settings.frameEnd = Number(frameEnd); } break; case "rotate": if (angle) settings.angle = angle; settings.flipH = flipH; settings.flipV = flipV; break; } onChangeRef.current?.(settings); }, [ mode, resizeTab, width, height, percentage, colors, dither, effort, speedFactor, reverseAdjustSpeed, reverseSpeed, extractTab, frameNumber, frameStart, frameEnd, extractFormat, angle, flipH, flipV, loopMode, loopCount, ]); const tabClass = (active: boolean) => `flex-1 text-xs py-1.5 rounded ${active ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"}`; const maxFrame = Math.max(0, (info?.pages ?? 1) - 1); return (
{/* GIF Info Bar */} {infoLoading && (
Reading GIF metadata...
)} {info && !infoLoading && (
{isAnimated ? ( <> {info.pages} frames | {info.width}x{info.height} | {(info.duration / 1000).toFixed(1)}s | {(info.fileSize / 1024).toFixed(0)} KB ) : ( <> Static image | {info.width}x{info.height} | {(info.fileSize / 1024).toFixed(0)} KB )}
)} {/* Mode Tabs (3x2 grid) */}

Mode

{MODES.map((m) => { const disabled = m.requiresAnimation && !isAnimated; return ( ); })}
{/* Mode Controls */} {mode === "resize" && (
{resizeTab === "pixel" ? (
setWidth(e.target.value)} placeholder="Auto" className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
setHeight(e.target.value)} placeholder="Auto" className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
) : (
{percentage}%
setPercentage(e.target.value)} className="w-full mt-1 h-1.5 rounded-full appearance-none bg-muted accent-primary" />
{[25, 50, 75, 200].map((pct) => ( ))}
)}
)} {mode === "optimize" && (
{colors}
setColors(Number(e.target.value))} className="w-full mt-1 h-1.5 rounded-full appearance-none bg-muted accent-primary" />

Fewer colors = smaller file

{dither.toFixed(1)}
setDither(Number(e.target.value))} className="w-full mt-1 h-1.5 rounded-full appearance-none bg-muted accent-primary" />
{effort}
setEffort(Number(e.target.value))} className="w-full mt-1 h-1.5 rounded-full appearance-none bg-muted accent-primary" />

Higher effort = slower but smaller

)} {mode === "speed" && (
{speedFactor.toFixed(1)}x
setSpeedFactor(Number(e.target.value))} className="w-full mt-1 h-1.5 rounded-full appearance-none bg-muted accent-primary" />
{[0.5, 1, 2, 5].map((s) => ( ))}
{info && (

{(info.duration / 1000).toFixed(1)}s {"->"}{" "} {(info.duration / 1000 / speedFactor).toFixed(1)}s

)}
)} {mode === "reverse" && (

Reverses the playback order of all frames.

{reverseAdjustSpeed && (
{reverseSpeed.toFixed(1)}x
setReverseSpeed(Number(e.target.value))} className="w-full mt-1 h-1.5 rounded-full appearance-none bg-muted accent-primary" />
)}
)} {mode === "extract" && (
{(["single", "range", "all"] as const).map((t) => ( ))}
{extractTab === "single" && (
setFrameNumber(e.target.value)} min={0} max={maxFrame} className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />

0 to {maxFrame} (0 is first frame)

)} {extractTab === "range" && (
setFrameStart(e.target.value)} min={0} max={maxFrame} className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
setFrameEnd(e.target.value)} min={0} max={maxFrame} placeholder={String(maxFrame)} className="w-full mt-0.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" />
)} {extractTab === "all" && (

Extracts all {info?.pages ?? "?"} frames as individual images in a ZIP.

)}

Output Format

)} {mode === "rotate" && (

Angle

{[90, 180, 270].map((a) => ( ))}

Flip

)} {/* Loop Control */}

Loop

{(["infinite", "once", "custom"] as const).map((l) => ( ))}
{loopMode === "custom" && ( setLoopCount(e.target.value)} min={2} max={100} className="w-full mt-1.5 px-2 py-1.5 rounded border border-border bg-background text-sm text-foreground" /> )}
); } export function GifToolsSettings() { const { files } = useFileStore(); const { processFiles, processAllFiles, processing, error, downloadUrl, originalSize, processedSize, progress, } = useToolProcessor("gif-tools"); const [settings, setSettings] = useState>({}); const handleProcess = () => { if (files.length > 1) { processAllFiles(files, settings); } else { processFiles(files, settings); } }; const hasFile = files.length > 0; return (
{error &&

{error}

} {originalSize != null && processedSize != null && (

Original: {(originalSize / 1024).toFixed(1)} KB

Processed: {(processedSize / 1024).toFixed(1)} KB {originalSize > 0 && ( ({Math.round(((processedSize - originalSize) / originalSize) * 100)}%) )}

)} {processing ? ( ) : ( )} {downloadUrl && ( Download )}
); }