import { Download, Loader2, MapPin } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { CollapsibleSection } from "@/components/common/collapsible-section"; import { MetadataGrid } from "@/components/common/metadata-grid"; import { ProgressCard } from "@/components/common/progress-card"; import { useToolProcessor } from "@/hooks/use-tool-processor"; import { formatHeaders } from "@/lib/api"; import { EXIF_LABELS, SKIP_KEYS } from "@/lib/metadata-utils"; import { useFileStore } from "@/stores/file-store"; interface MetadataResult { filename: string; fileSize: number; exif?: Record | null; exifError?: string; gps?: Record | null; icc?: Record | null; xmp?: Record | null; } interface StripMetadataControlsProps { onChange?: (settings: Record) => void; /** Passed from parent to preserve field-count badges in checkbox labels */ metadata?: MetadataResult | null; hasExif?: boolean; hasGps?: boolean; } export function StripMetadataControls({ onChange, metadata, hasExif, hasGps, }: StripMetadataControlsProps) { const [stripAll, setStripAll] = useState(true); const [stripExif, setStripExif] = useState(false); const [stripGps, setStripGps] = useState(false); const [stripIcc, setStripIcc] = useState(false); const [stripXmp, setStripXmp] = useState(false); const onChangeRef = useRef(onChange); onChangeRef.current = onChange; // Report settings on change useEffect(() => { onChangeRef.current?.({ stripAll, stripExif, stripGps, stripIcc, stripXmp }); }, [stripAll, stripExif, stripGps, stripIcc, stripXmp]); const handleStripAllChange = (checked: boolean) => { setStripAll(checked); if (checked) { setStripExif(false); setStripGps(false); setStripIcc(false); setStripXmp(false); } }; return ( <> {/* Strip All */}
{/* Individual options */}

Or select specific metadata:

); } export function StripMetadataSettings() { const { entries, selectedIndex, files } = useFileStore(); const { processFiles, processing, error, downloadUrl, originalSize, processedSize, progress } = useToolProcessor("strip-metadata"); const [stripSettings, setStripSettings] = useState>({ stripAll: true, stripExif: false, stripGps: false, stripIcc: false, stripXmp: false, }); // Per-file metadata cache (from feature branch) const [metadataCache, setMetadataCache] = useState>(new Map()); const [metadata, setMetadata] = useState(null); const [inspecting, setInspecting] = useState(false); const [inspectError, setInspectError] = useState(null); const currentFile = entries[selectedIndex]?.file ?? null; const fileKey = currentFile ? `${currentFile.name}-${currentFile.size}-${currentFile.lastModified}` : null; // Auto-fetch metadata for the selected file (with per-file caching) useEffect(() => { if (!currentFile || !fileKey) { setMetadata(null); setInspectError(null); return; } // Check cache first const cached = metadataCache.get(fileKey); if (cached) { setMetadata(cached); return; } const controller = new AbortController(); (async () => { setInspecting(true); setInspectError(null); setMetadata(null); try { const formData = new FormData(); formData.append("file", currentFile); const res = await fetch("/api/v1/tools/strip-metadata/inspect", { method: "POST", headers: formatHeaders(), body: formData, signal: controller.signal, }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.error || `Failed: ${res.status}`); } const data: MetadataResult = await res.json(); setMetadata(data); if (fileKey) setMetadataCache((prev) => new Map(prev).set(fileKey, data)); } catch (err) { if ((err as Error).name === "AbortError") return; setInspectError(err instanceof Error ? err.message : "Failed to inspect metadata"); } finally { setInspecting(false); } })(); return () => controller.abort(); }, [currentFile, fileKey, metadataCache]); const handleProcess = () => { processFiles(files, stripSettings); }; const hasFile = files.length > 0; const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (hasFile && !processing) handleProcess(); }; const hasExif = metadata?.exif && Object.keys(metadata.exif).length > 0; const hasGps = metadata?.gps && Object.keys(metadata.gps).length > 0; const hasIcc = metadata?.icc && Object.keys(metadata.icc).length > 0; const hasXmp = metadata?.xmp && Object.keys(metadata.xmp).length > 0; const hasAnyMetadata = hasExif || hasGps || hasIcc || hasXmp; const sectionCount = [hasExif, hasGps, hasIcc, hasXmp].filter(Boolean).length; // GPS coordinates for display const gpsLat = metadata?.gps?._latitude as number | null | undefined; const gpsLon = metadata?.gps?._longitude as number | null | undefined; return (
{/* Metadata Display */} {hasFile && (

Current Metadata

{inspecting && (
Reading metadata...
)} {inspectError &&

{inspectError}

} {metadata && !hasAnyMetadata && !inspecting && (

No metadata found in this image.

)} {metadata && hasAnyMetadata && (
{/* GPS warning banner */} {hasGps && gpsLat != null && gpsLon != null && (
Location data: {gpsLat.toFixed(4)}, {gpsLon.toFixed(4)}
)} {hasExif && metadata.exif && ( !SKIP_KEYS.has(k) && !k.startsWith("_")).length} fields`} defaultOpen > )} {metadata?.exifError && (

EXIF: {metadata.exifError}

)} {hasGps && metadata.gps && ( !k.startsWith("_")).length} fields`} > )} {hasIcc && metadata.icc && ( )} {hasXmp && metadata.xmp && ( )}

{sectionCount} metadata {sectionCount === 1 ? "section" : "sections"} found

)}
)} {hasFile && hasAnyMetadata &&
} {/* Error */} {error &&

{error}

} {/* Size info */} {originalSize != null && processedSize != null && (

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

Processed: {(processedSize / 1024).toFixed(1)} KB

Metadata removed: {((originalSize - processedSize) / 1024).toFixed(1)} KB

)} {/* Process */} {processing ? ( ) : ( )} {/* Download */} {downloadUrl && ( Download )} ); }