feat(web): add Stirling-PDF-style file preview, review panel, and tool chaining UX

After upload: dropzone replaced by image viewer with zoom controls.
After processing: review panel shows result preview, file info, download,
undo button, and suggested next tools for chaining.
This commit is contained in:
Siddharth Kumar Sah
2026-03-22 10:47:46 +08:00
parent 80bb0e29a0
commit ab0b0344b9
6 changed files with 528 additions and 53 deletions
@@ -0,0 +1,152 @@
import { useState, useRef, useCallback, useEffect } from "react";
import { ZoomIn, ZoomOut, Maximize, Minimize2 } from "lucide-react";
import { formatFileSize } from "@/lib/download";
interface ImageViewerProps {
src: string;
filename: string;
fileSize: number;
}
const ZOOM_STEPS = [25, 50, 75, 100, 125, 150, 200, 300];
const DEFAULT_ZOOM = 100;
export function ImageViewer({ src, filename, fileSize }: ImageViewerProps) {
const [zoom, setZoom] = useState(DEFAULT_ZOOM);
const [naturalWidth, setNaturalWidth] = useState<number | null>(null);
const [naturalHeight, setNaturalHeight] = useState<number | null>(null);
const [fitMode, setFitMode] = useState<"fit" | "actual">("fit");
const containerRef = useRef<HTMLDivElement>(null);
const imgRef = useRef<HTMLImageElement>(null);
const isSvg = filename.toLowerCase().endsWith(".svg");
const handleImageLoad = useCallback(() => {
if (imgRef.current) {
setNaturalWidth(imgRef.current.naturalWidth);
setNaturalHeight(imgRef.current.naturalHeight);
}
}, []);
const zoomIn = useCallback(() => {
setZoom((prev) => {
const next = ZOOM_STEPS.find((s) => s > prev);
return next ?? prev;
});
setFitMode("actual");
}, []);
const zoomOut = useCallback(() => {
setZoom((prev) => {
const next = [...ZOOM_STEPS].reverse().find((s) => s < prev);
return next ?? prev;
});
setFitMode("actual");
}, []);
const fitToContainer = useCallback(() => {
setFitMode("fit");
setZoom(DEFAULT_ZOOM);
}, []);
const actualSize = useCallback(() => {
setFitMode("actual");
setZoom(100);
}, []);
// Reset zoom on src change
useEffect(() => {
setZoom(DEFAULT_ZOOM);
setFitMode("fit");
setNaturalWidth(null);
setNaturalHeight(null);
}, [src]);
const imageStyle =
fitMode === "fit"
? { maxWidth: "100%", maxHeight: "100%", objectFit: "contain" as const }
: { transform: `scale(${zoom / 100})`, transformOrigin: "center center" };
return (
<div className="flex flex-col w-full h-full max-w-3xl mx-auto">
{/* Toolbar */}
<div className="flex items-center justify-center gap-1 py-2 px-3 border-b border-border shrink-0">
<button
onClick={zoomOut}
disabled={zoom <= ZOOM_STEPS[0]}
className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground disabled:opacity-30 disabled:cursor-not-allowed"
title="Zoom out"
>
<ZoomOut className="h-4 w-4" />
</button>
<span className="text-xs text-muted-foreground min-w-[3rem] text-center tabular-nums">
{fitMode === "fit" ? "Fit" : `${zoom}%`}
</span>
<button
onClick={zoomIn}
disabled={zoom >= ZOOM_STEPS[ZOOM_STEPS.length - 1]}
className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-foreground disabled:opacity-30 disabled:cursor-not-allowed"
title="Zoom in"
>
<ZoomIn className="h-4 w-4" />
</button>
<div className="w-px h-4 bg-border mx-1" />
<button
onClick={fitToContainer}
className={`px-2 py-1 rounded text-xs ${fitMode === "fit" ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground hover:bg-muted"}`}
title="Fit to view"
>
<Maximize className="h-3.5 w-3.5" />
</button>
<button
onClick={actualSize}
className={`px-2 py-1 rounded text-xs ${fitMode === "actual" && zoom === 100 ? "bg-primary/10 text-primary" : "text-muted-foreground hover:text-foreground hover:bg-muted"}`}
title="Actual size (100%)"
>
<Minimize2 className="h-3.5 w-3.5" />
</button>
</div>
{/* Image area */}
<div
ref={containerRef}
className="flex-1 flex items-center justify-center overflow-auto bg-muted/20 p-4"
>
{isSvg ? (
<img
ref={imgRef}
src={src}
alt={filename}
onLoad={handleImageLoad}
className="select-none"
style={imageStyle}
draggable={false}
/>
) : (
<img
ref={imgRef}
src={src}
alt={filename}
onLoad={handleImageLoad}
className="select-none rounded-sm"
style={imageStyle}
draggable={false}
/>
)}
</div>
{/* Info bar */}
<div className="flex items-center justify-between px-3 py-1.5 border-t border-border text-xs text-muted-foreground shrink-0">
<span className="truncate mr-2">{filename}</span>
<div className="flex items-center gap-3 shrink-0">
{naturalWidth != null && naturalHeight != null && (
<span>
{naturalWidth} x {naturalHeight}
</span>
)}
<span>{formatFileSize(fileSize)}</span>
</div>
</div>
</div>
);
}
@@ -0,0 +1,166 @@
import { useState, useMemo } from "react";
import { useNavigate } from "react-router-dom";
import { TOOLS } from "@stirling-image/shared";
import {
Download,
Undo2,
ChevronDown,
ChevronRight,
ArrowRight,
} from "lucide-react";
import * as icons from "lucide-react";
import { triggerDownload, formatFileSize } from "@/lib/download";
import { getSuggestedTools } from "@/lib/suggested-tools";
interface ReviewPanelProps {
filename: string;
fileSize: number;
fileType: string;
downloadUrl: string;
previewUrl?: string;
onUndo: () => void;
currentToolId: string;
}
export function ReviewPanel({
filename,
fileSize,
fileType,
downloadUrl,
previewUrl,
onUndo,
currentToolId,
}: ReviewPanelProps) {
const [isExpanded, setIsExpanded] = useState(true);
const [isSuggestionsExpanded, setIsSuggestionsExpanded] = useState(true);
const navigate = useNavigate();
const suggestedToolIds = useMemo(
() => getSuggestedTools(currentToolId),
[currentToolId],
);
const suggestedTools = useMemo(
() =>
suggestedToolIds
.map((id) => TOOLS.find((t) => t.id === id))
.filter(
(t): t is (typeof TOOLS)[number] => t !== undefined,
),
[suggestedToolIds],
);
const timestamp = useMemo(() => {
const now = new Date();
return now.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
}, []);
const handleDownload = () => {
triggerDownload(downloadUrl, filename);
};
return (
<div className="space-y-3">
<div className="border-t border-border" />
{/* Review header */}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="flex items-center justify-between w-full text-sm font-medium text-muted-foreground hover:text-foreground"
>
<span>Review</span>
{isExpanded ? (
<ChevronDown className="h-4 w-4" />
) : (
<ChevronRight className="h-4 w-4" />
)}
</button>
{isExpanded && (
<div className="space-y-3">
{/* Preview thumbnail */}
{previewUrl && (
<div className="rounded-lg border border-border overflow-hidden bg-muted/30">
<img
src={previewUrl}
alt="Processed result"
className="w-full h-auto max-h-32 object-contain"
/>
</div>
)}
{/* File metadata */}
<div className="space-y-1 text-xs text-muted-foreground">
<p className="truncate text-foreground font-medium">{filename}</p>
<p>Size: {formatFileSize(fileSize)}</p>
<p>Type: {fileType}</p>
<p>Processed: {timestamp}</p>
</div>
{/* Action buttons */}
<div className="flex gap-2">
<button
onClick={onUndo}
className="flex-1 py-2 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:bg-muted flex items-center justify-center gap-1.5 text-xs font-medium"
>
<Undo2 className="h-3.5 w-3.5" />
Undo
</button>
<button
onClick={handleDownload}
className="flex-1 py-2 rounded-lg bg-primary text-primary-foreground flex items-center justify-center gap-1.5 text-xs font-medium hover:bg-primary/90"
>
<Download className="h-3.5 w-3.5" />
Download
</button>
</div>
{/* Suggested tools */}
{suggestedTools.length > 0 && (
<div className="space-y-2">
<div className="border-t border-border pt-2" />
<button
onClick={() =>
setIsSuggestionsExpanded(!isSuggestionsExpanded)
}
className="flex items-center justify-between w-full text-xs font-medium text-muted-foreground hover:text-foreground"
>
<span>Continue editing</span>
{isSuggestionsExpanded ? (
<ChevronDown className="h-3.5 w-3.5" />
) : (
<ChevronRight className="h-3.5 w-3.5" />
)}
</button>
{isSuggestionsExpanded && (
<div className="space-y-1">
{suggestedTools.map((tool) => {
const ToolIcon =
(
icons as unknown as Record<
string,
React.ComponentType<{ className?: string }>
>
)[tool.icon] || icons.FileImage;
return (
<button
key={tool.id}
onClick={() => navigate(tool.route)}
className="flex items-center gap-2 w-full px-2 py-1.5 rounded text-xs text-muted-foreground hover:text-foreground hover:bg-muted group"
>
<ToolIcon className="h-3.5 w-3.5 shrink-0" />
<span className="flex-1 text-left">{tool.name}</span>
<ArrowRight className="h-3 w-3 opacity-0 group-hover:opacity-100 shrink-0" />
</button>
);
})}
</div>
)}
</div>
)}
</div>
)}
</div>
);
}
+15
View File
@@ -0,0 +1,15 @@
export function triggerDownload(url: string, filename: string) {
const a = document.createElement("a");
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
export function formatFileSize(bytes: number): string {
if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(0)} KB`;
}
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
}
+24
View File
@@ -0,0 +1,24 @@
const TOOL_SUGGESTIONS: Record<string, string[]> = {
resize: ["compress", "convert", "watermark-text", "strip-metadata"],
crop: ["resize", "compress", "convert"],
rotate: ["crop", "resize", "compress"],
convert: ["compress", "strip-metadata", "watermark-text"],
compress: ["convert", "strip-metadata", "watermark-text"],
"strip-metadata": ["compress", "convert"],
"brightness-contrast": ["compress", "convert", "resize"],
saturation: ["compress", "convert", "resize"],
"color-channels": ["compress", "convert"],
"color-effects": ["compress", "convert", "resize"],
"replace-color": ["compress", "convert"],
"remove-background": ["resize", "compress", "convert"],
upscale: ["compress", "convert"],
"smart-crop": ["resize", "compress"],
"watermark-text": ["compress", "convert"],
"watermark-image": ["compress", "convert"],
"text-overlay": ["compress", "convert"],
border: ["compress", "convert", "resize"],
};
export function getSuggestedTools(currentToolId: string): string[] {
return TOOL_SUGGESTIONS[currentToolId] || ["resize", "compress", "convert"];
}
+146 -52
View File
@@ -3,9 +3,12 @@ import { useMemo, useCallback, useState } from "react";
import { TOOLS } from "@stirling-image/shared"; import { TOOLS } from "@stirling-image/shared";
import { AppLayout } from "@/components/layout/app-layout"; import { AppLayout } from "@/components/layout/app-layout";
import { Dropzone } from "@/components/common/dropzone"; import { Dropzone } from "@/components/common/dropzone";
import { ImageViewer } from "@/components/common/image-viewer";
import { BeforeAfterSlider } from "@/components/common/before-after-slider"; import { BeforeAfterSlider } from "@/components/common/before-after-slider";
import { ReviewPanel } from "@/components/common/review-panel";
import { useFileStore } from "@/stores/file-store"; import { useFileStore } from "@/stores/file-store";
import { useMobile } from "@/hooks/use-mobile"; import { useMobile } from "@/hooks/use-mobile";
import { formatFileSize } from "@/lib/download";
import { ResizeSettings } from "@/components/tools/resize-settings"; import { ResizeSettings } from "@/components/tools/resize-settings";
import { CropSettings } from "@/components/tools/crop-settings"; import { CropSettings } from "@/components/tools/crop-settings";
import { RotateSettings } from "@/components/tools/rotate-settings"; import { RotateSettings } from "@/components/tools/rotate-settings";
@@ -47,7 +50,7 @@ import { BlurFacesSettings } from "@/components/tools/blur-faces-settings";
import { EraseObjectSettings } from "@/components/tools/erase-object-settings"; import { EraseObjectSettings } from "@/components/tools/erase-object-settings";
import { SmartCropSettings } from "@/components/tools/smart-crop-settings"; import { SmartCropSettings } from "@/components/tools/smart-crop-settings";
import * as icons from "lucide-react"; import * as icons from "lucide-react";
import { cn } from "@/lib/utils"; import { CheckCircle2 } from "lucide-react";
const COLOR_TOOL_IDS = new Set([ const COLOR_TOOL_IDS = new Set([
"brightness-contrast", "brightness-contrast",
@@ -109,10 +112,67 @@ function ToolSettingsPanel({ toolId }: { toolId: string }) {
); );
} }
/** File selection indicator shown in left panel */
function FileSelectionInfo({
files,
selectedFileName,
selectedFileSize,
onClear,
}: {
files: File[];
selectedFileName: string | null;
selectedFileSize: number | null;
onClear: () => void;
}) {
if (files.length === 0) {
return (
<p className="text-xs text-muted-foreground italic">
Drop or upload an image to get started
</p>
);
}
return (
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-xs text-foreground bg-muted rounded px-2 py-1.5">
<CheckCircle2 className="h-3.5 w-3.5 text-green-500 shrink-0" />
<span className="truncate flex-1">
Selected: {selectedFileName ?? files[0].name}
</span>
<span className="text-muted-foreground shrink-0 ml-1">
{formatFileSize(selectedFileSize ?? files[0].size)}
</span>
</div>
{files.length > 1 && (
<p className="text-xs text-muted-foreground px-1">
+{files.length - 1} more file{files.length > 2 ? "s" : ""}
</p>
)}
<button
onClick={onClear}
className="text-xs text-muted-foreground hover:text-foreground"
>
Clear
</button>
</div>
);
}
export function ToolPage() { export function ToolPage() {
const { toolId } = useParams<{ toolId: string }>(); const { toolId } = useParams<{ toolId: string }>();
const tool = useMemo(() => TOOLS.find((t) => t.id === toolId), [toolId]); const tool = useMemo(() => TOOLS.find((t) => t.id === toolId), [toolId]);
const { files, setFiles, reset, processedUrl, originalBlobUrl, originalSize, processedSize } = useFileStore(); const {
files,
setFiles,
reset,
processedUrl,
originalBlobUrl,
originalSize,
processedSize,
selectedFileName,
selectedFileSize,
undoProcessing,
} = useFileStore();
const isMobile = useMobile(); const isMobile = useMobile();
const [mobileSettingsOpen, setMobileSettingsOpen] = useState(true); const [mobileSettingsOpen, setMobileSettingsOpen] = useState(true);
@@ -124,6 +184,10 @@ export function ToolPage() {
[setFiles, reset], [setFiles, reset],
); );
const handleUndo = useCallback(() => {
undoProcessing();
}, [undoProcessing]);
if (!tool) { if (!tool) {
return ( return (
<AppLayout> <AppLayout>
@@ -143,8 +207,17 @@ export function ToolPage() {
)[tool.icon] || icons.FileImage; )[tool.icon] || icons.FileImage;
const hasFile = files.length > 0; const hasFile = files.length > 0;
const hasProcessed = !!processedUrl;
const isNoDropzone = NO_DROPZONE_TOOLS.has(tool.id); const isNoDropzone = NO_DROPZONE_TOOLS.has(tool.id);
// Derive processed file info from context
const processedFileName = selectedFileName
? `processed-${selectedFileName}`
: "processed-image";
const processedFileType = selectedFileName
? selectedFileName.split(".").pop()?.toUpperCase() || "IMAGE"
: "IMAGE";
// Mobile layout: settings above dropzone (stacked) // Mobile layout: settings above dropzone (stacked)
if (isMobile) { if (isMobile) {
return ( return (
@@ -170,44 +243,63 @@ export function ToolPage() {
{mobileSettingsOpen && ( {mobileSettingsOpen && (
<div className="p-4 border-b border-border space-y-3 shrink-0 max-h-[40vh] overflow-y-auto"> <div className="p-4 border-b border-border space-y-3 shrink-0 max-h-[40vh] overflow-y-auto">
{/* File info */} {/* File info */}
{!isNoDropzone && hasFile && ( {!isNoDropzone && (
<div className="space-y-1"> <div className="space-y-2">
{files.map((f, i) => ( <h3 className="text-sm font-medium text-muted-foreground">
<div Files
key={i} </h3>
className="flex items-center justify-between text-xs text-foreground bg-muted rounded px-2 py-1" <FileSelectionInfo
> files={files}
<span className="truncate">{f.name}</span> selectedFileName={selectedFileName}
<span className="text-muted-foreground shrink-0 ml-2"> selectedFileSize={selectedFileSize}
{(f.size / 1024).toFixed(0)} KB onClear={reset}
</span> />
</div>
))}
<button
onClick={() => reset()}
className="text-xs text-muted-foreground hover:text-foreground"
>
Clear
</button>
</div> </div>
)} )}
<ToolSettingsPanel toolId={tool.id} />
<div className="border-t border-border" />
<div className="space-y-2">
<h3 className="text-sm font-medium text-muted-foreground">
Settings
</h3>
<ToolSettingsPanel toolId={tool.id} />
</div>
{/* Review panel (mobile) */}
{hasProcessed && processedSize != null && (
<ReviewPanel
filename={processedFileName}
fileSize={processedSize}
fileType={processedFileType}
downloadUrl={processedUrl}
previewUrl={processedUrl}
onUndo={handleUndo}
currentToolId={tool.id}
/>
)}
</div> </div>
)} )}
{/* Dropzone / Preview */} {/* Main area: Dropzone / Image Viewer / Before-After */}
<div className="flex-1 flex items-center justify-center p-4"> <div className="flex-1 flex items-center justify-center p-4">
{isNoDropzone ? ( {isNoDropzone ? (
<div className="text-center text-muted-foreground"> <div className="text-center text-muted-foreground">
<p className="text-sm">Configure settings and generate.</p> <p className="text-sm">Configure settings and generate.</p>
</div> </div>
) : processedUrl && originalBlobUrl ? ( ) : hasProcessed && originalBlobUrl ? (
<BeforeAfterSlider <BeforeAfterSlider
beforeSrc={originalBlobUrl} beforeSrc={originalBlobUrl}
afterSrc={processedUrl} afterSrc={processedUrl}
beforeSize={originalSize ?? undefined} beforeSize={originalSize ?? undefined}
afterSize={processedSize ?? undefined} afterSize={processedSize ?? undefined}
/> />
) : hasFile && originalBlobUrl ? (
<ImageViewer
src={originalBlobUrl}
filename={selectedFileName ?? files[0].name}
fileSize={selectedFileSize ?? files[0].size}
/>
) : ( ) : (
<Dropzone <Dropzone
onFiles={handleFiles} onFiles={handleFiles}
@@ -243,31 +335,12 @@ export function ToolPage() {
<h3 className="text-sm font-medium text-muted-foreground"> <h3 className="text-sm font-medium text-muted-foreground">
Files Files
</h3> </h3>
{hasFile ? ( <FileSelectionInfo
<div className="space-y-1"> files={files}
{files.map((f, i) => ( selectedFileName={selectedFileName}
<div selectedFileSize={selectedFileSize}
key={i} onClear={reset}
className="flex items-center justify-between text-xs text-foreground bg-muted rounded px-2 py-1" />
>
<span className="truncate">{f.name}</span>
<span className="text-muted-foreground shrink-0 ml-2">
{(f.size / 1024).toFixed(0)} KB
</span>
</div>
))}
<button
onClick={() => reset()}
className="text-xs text-muted-foreground hover:text-foreground"
>
Clear
</button>
</div>
) : (
<p className="text-xs text-muted-foreground italic">
Drop or upload an image to get started
</p>
)}
</div> </div>
)} )}
@@ -280,21 +353,42 @@ export function ToolPage() {
</h3> </h3>
<ToolSettingsPanel toolId={tool.id} /> <ToolSettingsPanel toolId={tool.id} />
</div> </div>
{/* Review panel (desktop - below settings) */}
{hasProcessed && processedSize != null && (
<ReviewPanel
filename={processedFileName}
fileSize={processedSize}
fileType={processedFileType}
downloadUrl={processedUrl}
previewUrl={processedUrl}
onUndo={handleUndo}
currentToolId={tool.id}
/>
)}
</div> </div>
{/* Dropzone / Preview */} {/* Main area: Dropzone / Image Viewer / Before-After */}
<div className="flex-1 flex items-center justify-center p-6"> <div className="flex-1 flex items-center justify-center p-6">
{isNoDropzone ? ( {isNoDropzone ? (
<div className="text-center text-muted-foreground"> <div className="text-center text-muted-foreground">
<p className="text-sm">Configure settings in the panel and generate.</p> <p className="text-sm">
Configure settings in the panel and generate.
</p>
</div> </div>
) : processedUrl && originalBlobUrl ? ( ) : hasProcessed && originalBlobUrl ? (
<BeforeAfterSlider <BeforeAfterSlider
beforeSrc={originalBlobUrl} beforeSrc={originalBlobUrl}
afterSrc={processedUrl} afterSrc={processedUrl}
beforeSize={originalSize ?? undefined} beforeSize={originalSize ?? undefined}
afterSize={processedSize ?? undefined} afterSize={processedSize ?? undefined}
/> />
) : hasFile && originalBlobUrl ? (
<ImageViewer
src={originalBlobUrl}
filename={selectedFileName ?? files[0].name}
fileSize={selectedFileSize ?? files[0].size}
/>
) : ( ) : (
<Dropzone <Dropzone
onFiles={handleFiles} onFiles={handleFiles}
+25 -1
View File
@@ -10,6 +10,8 @@ interface FileState {
error: string | null; error: string | null;
originalSize: number | null; originalSize: number | null;
processedSize: number | null; processedSize: number | null;
selectedFileName: string | null;
selectedFileSize: number | null;
setFiles: (files: File[]) => void; setFiles: (files: File[]) => void;
setJobId: (id: string) => void; setJobId: (id: string) => void;
setProcessedUrl: (url: string | null) => void; setProcessedUrl: (url: string | null) => void;
@@ -17,6 +19,8 @@ interface FileState {
setProcessing: (v: boolean) => void; setProcessing: (v: boolean) => void;
setError: (e: string | null) => void; setError: (e: string | null) => void;
setSizes: (original: number, processed: number) => void; setSizes: (original: number, processed: number) => void;
/** Clear processed result but keep the original uploaded file. */
undoProcessing: () => void;
reset: () => void; reset: () => void;
} }
@@ -29,13 +33,23 @@ export const useFileStore = create<FileState>((set, get) => ({
error: null, error: null,
originalSize: null, originalSize: null,
processedSize: null, processedSize: null,
selectedFileName: null,
selectedFileSize: null,
setFiles: (files) => { setFiles: (files) => {
// Revoke old blob URL if any // Revoke old blob URL if any
const old = get().originalBlobUrl; const old = get().originalBlobUrl;
if (old) URL.revokeObjectURL(old); if (old) URL.revokeObjectURL(old);
// Create a blob URL for the first file for before/after preview // Create a blob URL for the first file for before/after preview
const blobUrl = files.length > 0 ? URL.createObjectURL(files[0]) : null; const blobUrl = files.length > 0 ? URL.createObjectURL(files[0]) : null;
set({ files, error: null, originalBlobUrl: blobUrl }); const firstName = files.length > 0 ? files[0].name : null;
const firstSize = files.length > 0 ? files[0].size : null;
set({
files,
error: null,
originalBlobUrl: blobUrl,
selectedFileName: firstName,
selectedFileSize: firstSize,
});
}, },
setJobId: (id) => set({ jobId: id }), setJobId: (id) => set({ jobId: id }),
setProcessedUrl: (url) => set({ processedUrl: url }), setProcessedUrl: (url) => set({ processedUrl: url }),
@@ -44,6 +58,14 @@ export const useFileStore = create<FileState>((set, get) => ({
setError: (e) => set({ error: e, processing: false }), setError: (e) => set({ error: e, processing: false }),
setSizes: (original, processed) => setSizes: (original, processed) =>
set({ originalSize: original, processedSize: processed }), set({ originalSize: original, processedSize: processed }),
undoProcessing: () => {
set({
processedUrl: null,
jobId: null,
processedSize: null,
error: null,
});
},
reset: () => { reset: () => {
const old = get().originalBlobUrl; const old = get().originalBlobUrl;
if (old) URL.revokeObjectURL(old); if (old) URL.revokeObjectURL(old);
@@ -56,6 +78,8 @@ export const useFileStore = create<FileState>((set, get) => ({
error: null, error: null,
originalSize: null, originalSize: null,
processedSize: null, processedSize: null,
selectedFileName: null,
selectedFileSize: null,
}); });
}, },
})); }));