import { Download, Redo, Trash2 } from "lucide-react"; import { useRef, useState } from "react"; import { ProgressCard } from "@/components/common/progress-card"; import { formatHeaders } from "@/lib/api"; import { generateId } from "@/lib/utils"; import { useFileStore } from "@/stores/file-store"; import type { EraserCanvasRef } from "./eraser-canvas"; const OUTPUT_FORMATS = [ "png", "jpg", "webp", "avif", "tiff", "gif", "heic", "heif", "jxl", ] as const; const LOSSY_FORMATS = ["jpg", "jpeg", "webp", "avif", "heic", "heif", "jxl"]; interface EraseObjectSettingsProps { eraserRef: React.RefObject; hasStrokes: boolean; brushSize: number; onBrushSizeChange: (size: number) => void; onMaskCenter?: (centerPct: number) => void; } export function EraseObjectSettings({ eraserRef, hasStrokes, brushSize, onBrushSizeChange: setBrushSize, onMaskCenter, }: EraseObjectSettingsProps) { const { files, processing, error, setProcessing, setError, setProcessedUrl, setSizes } = useFileStore(); const [downloadUrl, setDownloadUrl] = useState(null); const [originalSize, setOriginalSize] = useState(null); const [processedSize, setProcessedSize] = useState(null); const [progressPhase, setProgressPhase] = useState<"idle" | "uploading" | "processing">("idle"); const [progressPercent, setProgressPercent] = useState(0); const [elapsed, setElapsed] = useState(0); const elapsedRef = useRef | null>(null); const [outputFormat, setOutputFormat] = useState("png"); const [quality, setQuality] = useState(95); const handleProcess = async () => { if (files.length === 0 || !eraserRef.current) return; const maskBlob = await eraserRef.current.exportMask(); if (!maskBlob) return; // Record where the user painted so the comparison slider starts at that location const maskCenter = eraserRef.current.getMaskCenter(); if (maskCenter !== null && onMaskCenter) { onMaskCenter(maskCenter); } setError(null); setDownloadUrl(null); setProcessing(true); setProgressPhase("uploading"); setProgressPercent(0); setElapsed(0); const startTime = Date.now(); elapsedRef.current = setInterval(() => { setElapsed(Math.floor((Date.now() - startTime) / 1000)); }, 1000); const clientJobId = generateId(); const es = new EventSource(`/api/v1/jobs/${clientJobId}/progress`); es.onmessage = (event) => { try { const data = JSON.parse(event.data); if (data.type === "single" && typeof data.percent === "number") { setProgressPhase("processing"); setProgressPercent(15 + (data.percent / 100) * 85); } } catch {} }; es.onerror = () => es.close(); const maskFile = new File([maskBlob], "mask.png", { type: "image/png" }); const formData = new FormData(); formData.append("file", files[0]); formData.append("mask", maskFile); formData.append("clientJobId", clientJobId); formData.append("format", outputFormat); formData.append("quality", String(quality)); const xhr = new XMLHttpRequest(); xhr.upload.onprogress = (e) => { if (e.lengthComputable) { setProgressPercent((e.loaded / e.total) * 15); } }; xhr.upload.onload = () => { setProgressPhase("processing"); setProgressPercent(15); }; xhr.onload = () => { if (elapsedRef.current) clearInterval(elapsedRef.current); es.close(); if (xhr.status >= 200 && xhr.status < 300) { try { const data = JSON.parse(xhr.responseText); setDownloadUrl(data.downloadUrl); setOriginalSize(data.originalSize); setProcessedSize(data.processedSize); setProcessedUrl(data.downloadUrl, data.previewUrl); setSizes(data.originalSize, data.processedSize); } catch { setError("Invalid response"); } } else { try { const body = JSON.parse(xhr.responseText); setError( typeof body.error === "string" ? body.error : typeof body.details === "string" ? body.details : `Failed: ${xhr.status}`, ); } catch { setError(`Processing failed: ${xhr.status}`); } } setProcessing(false); setProgressPhase("idle"); }; xhr.onerror = () => { if (elapsedRef.current) clearInterval(elapsedRef.current); es.close(); setError("Network error"); setProcessing(false); setProgressPhase("idle"); }; xhr.open("POST", "/api/v1/tools/erase-object"); formatHeaders().forEach((value, key) => { xhr.setRequestHeader(key, value); }); xhr.send(formData); }; const hasFile = files.length > 0; return (
{/* Brush size */}
{brushSize}px
setBrushSize(Number(e.target.value))} className="w-full mt-1" />
Fine Wide
{/* Clear / Undo */} {hasStrokes && (
)} {/* Output Format */}
{/* Quality (lossy formats only) */} {LOSSY_FORMATS.includes(outputFormat) && (
{quality}
setQuality(Number(e.target.value))} className="w-full mt-1" />
)} {/* Hint */} {hasFile && !hasStrokes && (

Paint over the objects you want to remove. Use Ctrl+Z to undo.

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

{error}

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

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

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

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