"use client" import type { AssetResponse } from "@/lib/types" import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible" import { Badge } from "@/components/ui/badge" import { Separator } from "@/components/ui/separator" import { ScrollArea } from "@/components/ui/scroll-area" import { Button } from "@/components/ui/button" import { ChevronDownIcon, XIcon } from "lucide-react" interface MetadataSidebarProps { asset: AssetResponse onClose: () => void } const CAMERA_KEYS = [ "Make", "Model", "FocalLength", "FocalLengthIn35mmFilm", "FNumber", "ExposureTime", "ISOSpeedRatings", "ExposureMode", "ExposureProgram", "MeteringMode", "Flash", "WhiteBalanceMode", "LightSource", ] const GPS_KEYS = ["GPSInfo", "GPSLatitude", "GPSLongitude", "GPSAltitude"] const HIDDEN_KEYS = [ "file_size_bytes", "mime_type", "ExifImageWidth", "ExifImageHeight", ...CAMERA_KEYS, ...GPS_KEYS, ] function formatBytes(bytes: number): string { if (bytes < 1024) return `${bytes} B` if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / (1024 * 1024)).toFixed(1)} MB` } function formatExposure(val: string): string { const match = val.match(/^(\d+)\/(\d+)/) if (!match) return val const num = parseInt(match[1]) const den = parseInt(match[2]) if (den === 0) return val const result = num / den return result < 1 ? `1/${Math.round(1 / result)}s` : `${result}s` } function formatFocalLength(val: string): string { const match = val.match(/^(\d+)\/(\d+)/) if (!match) return val return `${Math.round(parseInt(match[1]) / parseInt(match[2]))}mm` } function formatFNumber(val: string): string { const match = val.match(/^(\d+)\/(\d+)/) if (!match) return val return `f/${(parseInt(match[1]) / parseInt(match[2])).toFixed(1)}` } function MetaRow({ label, value }: { label: string; value: string }) { return (
{label} {value}
) } function Section({ title, defaultOpen = true, children, }: { title: string defaultOpen?: boolean children: React.ReactNode }) { return ( {title}
{children}
) } export function MetadataSidebar({ asset, onClose }: MetadataSidebarProps) { const meta = asset.metadata const width = meta.ExifImageWidth as string | undefined const height = meta.ExifImageHeight as string | undefined const cameraEntries = CAMERA_KEYS.filter((k) => meta[k] != null).map( (k) => { let val = String(meta[k]) if (k === "ExposureTime") val = formatExposure(val) if (k === "FocalLength") val = formatFocalLength(val) if (k === "FNumber") val = formatFNumber(val) return [k, val] as const }, ) const gpsEntries = GPS_KEYS.filter((k) => meta[k] != null) const remainingEntries = Object.entries(meta).filter( ([k]) => !HIDDEN_KEYS.includes(k), ) return (
Details
{/* File Info */}
{asset.asset_type} {asset.mime_type}
{width && height && ( )}
{/* Camera */} {cameraEntries.length > 0 && ( <>
{cameraEntries.map(([k, v]) => ( ))}
)} {/* Location */} {gpsEntries.length > 0 && ( <>
{gpsEntries.map((k) => ( ))}
)} {/* All Metadata */} {remainingEntries.length > 0 && ( <>
{remainingEntries.map(([k, v]) => ( ))}
)}
) }