import { Check, Copy, Download, Search } from "lucide-react"; import { useRef, useState } from "react"; import { ProgressCard } from "@/components/common/progress-card"; import { formatHeaders } from "@/lib/api"; import { copyToClipboard } from "@/lib/utils"; import { useFileStore } from "@/stores/file-store"; interface BarcodeResult { type: string; text: string; position: { topLeft: { x: number; y: number }; topRight: { x: number; y: number }; bottomLeft: { x: number; y: number }; bottomRight: { x: number; y: number }; }; } interface FileResult { filename: string; barcodes: BarcodeResult[]; } /** Human-readable barcode type labels. */ const FORMAT_LABELS: Record = { QRCode: "QR Code", Code128: "Code 128", Code39: "Code 39", Code93: "Code 93", Codabar: "Codabar", DataMatrix: "Data Matrix", EAN8: "EAN-8", EAN13: "EAN-13", ITF: "ITF", PDF417: "PDF417", UPCA: "UPC-A", UPCE: "UPC-E", Aztec: "Aztec", MaxiCode: "MaxiCode", MicroQRCode: "Micro QR", DataBar: "DataBar", DataBarExpanded: "DataBar Exp", }; /** Badge colors by barcode family. */ function getBadgeColor(type: string): string { if (type.includes("QR") || type === "Aztec" || type === "DataMatrix" || type === "MaxiCode") return "bg-blue-500/15 text-blue-600 dark:text-blue-400"; if (type.includes("EAN") || type.includes("UPC") || type.includes("DataBar")) return "bg-green-500/15 text-green-600 dark:text-green-400"; if (type === "PDF417") return "bg-purple-500/15 text-purple-600 dark:text-purple-400"; return "bg-amber-500/15 text-amber-600 dark:text-amber-400"; } function SectionLabel({ children }: { children: React.ReactNode }) { return (

{children}

); } /** Send one file to the barcode-read API. */ function scanOneFile( file: File, tryHarder: boolean, onUploadProgress: (pct: number) => void, ): Promise<{ filename: string; barcodes: BarcodeResult[]; annotatedUrl: string | null }> { return new Promise((resolve, reject) => { const formData = new FormData(); formData.append("file", file); formData.append("settings", JSON.stringify({ tryHarder })); const xhr = new XMLHttpRequest(); xhr.timeout = 300_000; xhr.upload.onprogress = (e) => { if (e.lengthComputable) onUploadProgress((e.loaded / e.total) * 100); }; xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { try { resolve(JSON.parse(xhr.responseText)); } catch { reject(new Error("Invalid response")); } } else { try { const body = JSON.parse(xhr.responseText); reject(new Error(body.error || `Failed: ${xhr.status}`)); } catch { reject(new Error(`Scanning failed: ${xhr.status}`)); } } }; xhr.onerror = () => reject(new Error("Network error")); xhr.ontimeout = () => reject(new Error("Request timed out")); xhr.open("POST", "/api/v1/tools/barcode-read"); for (const [key, value] of formatHeaders()) { xhr.setRequestHeader(key, value); } xhr.send(formData); }); } export function BarcodeReadSettings() { const { files, processing, error, setProcessing, setError } = useFileStore(); const [tryHarder, setTryHarder] = useState(false); const [results, setResults] = useState([]); const [copiedIndex, setCopiedIndex] = useState(null); const [copiedAll, setCopiedAll] = useState(false); const [progressPhase, setProgressPhase] = useState<"idle" | "uploading" | "processing">("idle"); const [progressPercent, setProgressPercent] = useState(0); const [progressStage, setProgressStage] = useState(); const [elapsed, setElapsed] = useState(0); const elapsedRef = useRef | null>(null); const handleProcess = async () => { if (files.length === 0) return; setError(null); setResults([]); setProcessing(true); setProgressPhase("uploading"); setProgressPercent(0); setProgressStage(undefined); setElapsed(0); const startTime = Date.now(); elapsedRef.current = setInterval(() => { setElapsed(Math.floor((Date.now() - startTime) / 1000)); }, 1000); const total = files.length; const allResults: FileResult[] = []; const errors: string[] = []; const { updateEntry } = useFileStore.getState(); for (let i = 0; i < total; i++) { const file = files[i]; const prefix = total > 1 ? `[${i + 1}/${total}] ` : ""; const fileBase = (i / total) * 100; const fileShare = 100 / total; try { setProgressStage(`${prefix}Scanning ${file.name}...`); const result = await scanOneFile(file, tryHarder, (pct) => { setProgressPhase("uploading"); setProgressPercent(fileBase + (pct / 100) * fileShare * 0.5); }); setProgressPhase("processing"); setProgressPercent(fileBase + fileShare); allResults.push({ filename: result.filename, barcodes: result.barcodes, }); // Set annotated image as processedUrl for before/after view if (result.annotatedUrl) { updateEntry(i, { processedUrl: result.annotatedUrl, processedPreviewUrl: result.annotatedUrl, processedFilename: `annotated-${file.name.replace(/\.[^.]+$/, "")}.png`, status: "completed", processedSize: null, }); } } catch (err) { const msg = err instanceof Error ? err.message : String(err); errors.push(`${file.name}: ${msg}`); allResults.push({ filename: file.name, barcodes: [] }); } } if (elapsedRef.current) clearInterval(elapsedRef.current); if (errors.length === total) { setError(errors.join("; ")); } else if (errors.length > 0) { setError(`${errors.length} of ${total} files failed`); } setResults(allResults); setProcessing(false); setProgressPhase("idle"); }; // Total barcode count across all files const totalBarcodes = results.reduce((sum, r) => sum + r.barcodes.length, 0); const handleCopyOne = async (text: string, globalIdx: number) => { const ok = await copyToClipboard(text); if (ok) { setCopiedIndex(globalIdx); setTimeout(() => setCopiedIndex(null), 1500); } }; const handleCopyAll = async () => { const allText = results .flatMap((r) => r.barcodes.map((b) => `${FORMAT_LABELS[b.type] ?? b.type}: ${b.text}`)) .join("\n"); const ok = await copyToClipboard(allText); if (ok) { setCopiedAll(true); setTimeout(() => setCopiedAll(false), 2000); } }; const handleExportCsv = () => { const header = "File,Type,Value\n"; const rows = results .flatMap((r) => r.barcodes.map( (b) => `"${r.filename.replace(/"/g, '""')}","${FORMAT_LABELS[b.type] ?? b.type}","${b.text.replace(/"/g, '""')}"`, ), ) .join("\n"); const csv = header + rows; const blob = new Blob([csv], { type: "text/csv" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = "barcode-results.csv"; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const hasFile = files.length > 0; let globalIndex = 0; return (

Scan images for QR codes, barcodes (Code 128, EAN, UPC, etc.), and 2D codes (DataMatrix, PDF417, Aztec).

{/* Thorough scan toggle */} Options {/* Error */} {error &&

{error}

} {/* Process button / progress */} {processing ? ( ) : ( )} {/* Results */} {results.length > 0 && (
{/* Summary badge */}
{totalBarcodes === 0 ? "No barcodes found" : `Found ${totalBarcodes} barcode${totalBarcodes !== 1 ? "s" : ""}`} {totalBarcodes > 0 && (
)}
{/* Per-file results */} {results.map((fileResult) => (
{/* Show filename header only when multiple files */} {results.length > 1 && (

{fileResult.filename}

)} {fileResult.barcodes.length === 0 ? (

No barcodes found

) : ( fileResult.barcodes.map((barcode) => { const idx = globalIndex++; const label = FORMAT_LABELS[barcode.type] ?? barcode.type; const badgeColor = getBadgeColor(barcode.type); return (
{/* Barcode type badge */}
{label}
{/* Decoded value */}

{barcode.text}

{/* Copy button */}
); }) )}
))}
)}
); }