From 474b4a941e1be43887beac8d2ec57eb703cba260 Mon Sep 17 00:00:00 2001 From: Siddharth Kumar Sah Date: Mon, 23 Mar 2026 14:11:49 +0800 Subject: [PATCH] feat: multi-file metadata display with per-file caching --- .../tools/strip-metadata-settings.tsx | 157 +++++++++++++++++- 1 file changed, 154 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/tools/strip-metadata-settings.tsx b/apps/web/src/components/tools/strip-metadata-settings.tsx index 125c414e..caf54eea 100644 --- a/apps/web/src/components/tools/strip-metadata-settings.tsx +++ b/apps/web/src/components/tools/strip-metadata-settings.tsx @@ -1,11 +1,25 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useFileStore } from "@/stores/file-store"; import { useToolProcessor } from "@/hooks/use-tool-processor"; -import { Download } from "lucide-react"; +import { Download, Loader2, ChevronDown, ChevronRight, AlertTriangle } from "lucide-react"; import { ProgressCard } from "@/components/common/progress-card"; +function getToken(): string { + return localStorage.getItem("stirling-token") || ""; +} + +interface MetadataResult { + filename: string; + fileSize: number; + exif?: Record | null; + exifError?: string; + gps?: Record | null; + icc?: Record | null; + xmp?: Record | null; +} + export function StripMetadataSettings() { - const { files } = useFileStore(); + const { entries, selectedIndex, files } = useFileStore(); const { processFiles, processing, error, downloadUrl, originalSize, processedSize, progress } = useToolProcessor("strip-metadata"); @@ -15,6 +29,74 @@ export function StripMetadataSettings() { const [stripIcc, setStripIcc] = useState(false); const [stripXmp, setStripXmp] = useState(false); + // Metadata inspection state + const [metadataCache, setMetadataCache] = useState>(new Map()); + const [metadata, setMetadata] = useState(null); + const [inspecting, setInspecting] = useState(false); + const [inspectError, setInspectError] = useState(null); + + // Collapsible sections + const [expandedSections, setExpandedSections] = useState>(new Set()); + + const currentFile = entries[selectedIndex]?.file ?? null; + const fileKey = currentFile ? `${currentFile.name}-${currentFile.size}-${currentFile.lastModified}` : null; + + // Auto-fetch metadata for the selected file + 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: { Authorization: `Bearer ${getToken()}` }, + 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); + 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]); + + const toggleSection = (section: string) => { + setExpandedSections((prev) => { + const next = new Set(prev); + if (next.has(section)) next.delete(section); + else next.add(section); + return next; + }); + }; + const handleStripAllChange = (checked: boolean) => { setStripAll(checked); if (checked) { @@ -36,8 +118,77 @@ export function StripMetadataSettings() { if (hasFile && !processing) handleProcess(); }; + const hasGps = metadata?.gps && Object.keys(metadata.gps).length > 0; + + const renderMetadataSection = (title: string, key: string, data: Record | null | undefined) => { + if (!data || Object.keys(data).length === 0) return null; + const expanded = expandedSections.has(key); + return ( +
+ + {expanded && ( +
+ {Object.entries(data).map(([k, v]) => ( +
+ {k}: + {String(v)} +
+ ))} +
+ )} +
+ ); + }; + return (
+ {/* Metadata inspection */} + {inspecting && ( +
+ + Inspecting metadata... +
+ )} + + {inspectError &&

{inspectError}

} + + {metadata && ( +
+ + + {hasGps && ( +
+ +

+ This image contains GPS location data. Consider stripping it for privacy. +

+
+ )} + + {renderMetadataSection("EXIF", "exif", metadata.exif)} + {metadata.exifError && ( +

EXIF: {metadata.exifError}

+ )} + {renderMetadataSection("GPS", "gps", metadata.gps)} + {renderMetadataSection("ICC Profile", "icc", metadata.icc)} + {renderMetadataSection("XMP", "xmp", metadata.xmp)} + + {!metadata.exif && !metadata.gps && !metadata.icc && !metadata.xmp && !metadata.exifError && ( +

No metadata found in this file.

+ )} +
+ )} + +
+ {/* Strip All */}