mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
197 lines
6.3 KiB
TypeScript
197 lines
6.3 KiB
TypeScript
import { AlertCircle, ArrowLeft, CheckCircle2, Download, FileText } from "lucide-react";
|
|
import { useMemo } from "react";
|
|
import { Link } from "react-router-dom";
|
|
import { useTranslation } from "@/contexts/i18n-context";
|
|
import { formatFileSize, triggerDownload } from "@/lib/download";
|
|
import { format } from "@/lib/format";
|
|
|
|
/** Tools whose primary output is text/data, not a downloadable file. */
|
|
const DATA_OUTPUT_TOOLS = new Set([
|
|
"ocr",
|
|
"barcode-read",
|
|
"info",
|
|
"histogram",
|
|
"color-palette",
|
|
"transcribe-audio",
|
|
"extract-subtitles",
|
|
"image-to-base64",
|
|
"pdf-to-text",
|
|
"pdf-metadata",
|
|
"audio-metadata",
|
|
"video-metadata",
|
|
]);
|
|
|
|
/** Tools that produce multiple output files bundled as a ZIP. */
|
|
const MULTI_OUTPUT_TOOLS = new Set([
|
|
"split",
|
|
"favicon",
|
|
"pdf-to-image",
|
|
"video-to-frames",
|
|
"split-audio",
|
|
"split-csv",
|
|
]);
|
|
|
|
interface ReviewPanelProps {
|
|
filename: string;
|
|
fileSize: number;
|
|
fileType: string;
|
|
originalSize: number;
|
|
downloadUrl: string;
|
|
onUndo: () => void;
|
|
onStartOver: () => void;
|
|
currentToolId: string;
|
|
totalCount?: number;
|
|
successCount?: number;
|
|
failedCount?: number;
|
|
}
|
|
|
|
export function ReviewPanel({
|
|
filename,
|
|
fileSize,
|
|
fileType,
|
|
originalSize,
|
|
downloadUrl,
|
|
onUndo,
|
|
onStartOver,
|
|
currentToolId,
|
|
totalCount,
|
|
successCount,
|
|
failedCount,
|
|
}: ReviewPanelProps) {
|
|
const { t } = useTranslation();
|
|
|
|
const isDataOutput = DATA_OUTPUT_TOOLS.has(currentToolId);
|
|
const isMultiOutput = MULTI_OUTPUT_TOOLS.has(currentToolId);
|
|
|
|
const sizeDelta = useMemo(() => {
|
|
if (!originalSize || originalSize === 0) return 0;
|
|
return Math.round((1 - fileSize / originalSize) * 100);
|
|
}, [originalSize, fileSize]);
|
|
|
|
const handleDownload = () => {
|
|
triggerDownload(downloadUrl, filename);
|
|
};
|
|
|
|
const hasBatchStats =
|
|
totalCount != null && totalCount > 1 && successCount != null && failedCount != null;
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
<div className="border-t border-border" />
|
|
|
|
{/* Success indicator */}
|
|
<div className="flex items-center gap-2">
|
|
<CheckCircle2 className="h-4 w-4 text-emerald-600 shrink-0" />
|
|
<span className="text-sm font-medium text-foreground">{t.toolPage.conversionComplete}</span>
|
|
</div>
|
|
|
|
{/* Batch partial failure summary */}
|
|
{hasBatchStats && failedCount > 0 && (
|
|
<div className="flex items-start gap-2 rounded-lg bg-amber-50 dark:bg-amber-950/30 p-2.5 text-xs">
|
|
<AlertCircle className="h-3.5 w-3.5 text-amber-600 dark:text-amber-400 shrink-0 mt-0.5" />
|
|
<span className="text-amber-800 dark:text-amber-300">
|
|
{format(t.toolPage.batchPartialSuccess, {
|
|
success: successCount,
|
|
total: totalCount,
|
|
failed: failedCount,
|
|
})}
|
|
</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Size delta -- hidden for data-output tools */}
|
|
{!isDataOutput && originalSize > 0 && (
|
|
<div className="space-y-1 text-xs">
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">{t.toolPage.original}</span>
|
|
<span className="tabular-nums text-foreground">{formatFileSize(originalSize)}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">{t.toolPage.processed}</span>
|
|
<span className="tabular-nums text-foreground">{formatFileSize(fileSize)}</span>
|
|
</div>
|
|
<div className="flex justify-between">
|
|
<span className="text-muted-foreground">{t.toolPage.saved}</span>
|
|
<span
|
|
className={`tabular-nums font-medium ${
|
|
sizeDelta > 0
|
|
? "text-emerald-600"
|
|
: sizeDelta === 0
|
|
? "text-muted-foreground"
|
|
: "text-foreground"
|
|
}`}
|
|
>
|
|
{sizeDelta === 0
|
|
? t.toolPage.noChange
|
|
: sizeDelta > 0
|
|
? `-${sizeDelta}%`
|
|
: `+${Math.abs(sizeDelta)}%`}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Data-output tools: results hint + secondary download */}
|
|
{isDataOutput && (
|
|
<>
|
|
<div className="flex items-start gap-2 rounded-lg bg-muted/50 p-2.5 text-xs">
|
|
<FileText className="h-3.5 w-3.5 text-muted-foreground shrink-0 mt-0.5" />
|
|
<span className="text-muted-foreground">{t.toolPage.dataResultsHint}</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={handleDownload}
|
|
className="w-full text-center text-xs text-primary hover:text-primary/80 underline underline-offset-2"
|
|
>
|
|
{t.toolPage.downloadAsFile}
|
|
</button>
|
|
</>
|
|
)}
|
|
|
|
{/* Download button -- primary for non-data tools */}
|
|
{!isDataOutput && (
|
|
<button
|
|
type="button"
|
|
data-download-button
|
|
onClick={handleDownload}
|
|
className="w-full py-2.5 rounded-lg bg-primary text-primary-foreground font-medium text-sm flex items-center justify-center gap-2 hover:bg-primary/90"
|
|
>
|
|
<Download className="h-4 w-4" />
|
|
{isMultiOutput
|
|
? `${t.toolPage.downloadAll} (ZIP, ${formatFileSize(fileSize)})`
|
|
: hasBatchStats && successCount != null && successCount > 1
|
|
? `${format(t.toolPage.downloadFiles, { count: successCount })} (ZIP, ${formatFileSize(fileSize)})`
|
|
: `${t.toolPage.download} ${fileType} (${formatFileSize(fileSize)})`}
|
|
</button>
|
|
)}
|
|
|
|
{/* Adjust settings */}
|
|
<button
|
|
type="button"
|
|
onClick={onUndo}
|
|
className="w-full py-2 rounded-lg border border-border text-foreground hover:bg-muted text-sm font-medium"
|
|
>
|
|
{t.toolPage.adjustSettings}
|
|
</button>
|
|
|
|
{/* Text links */}
|
|
<div className="flex flex-col items-center gap-1.5 text-xs">
|
|
<button
|
|
type="button"
|
|
onClick={onStartOver}
|
|
className="text-muted-foreground hover:text-foreground"
|
|
>
|
|
{t.toolPage.startOver}
|
|
</button>
|
|
<Link
|
|
to="/"
|
|
className="text-muted-foreground hover:text-foreground flex items-center gap-1"
|
|
>
|
|
<ArrowLeft className="h-3 w-3" />
|
|
{t.toolPage.backToTools}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|