import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { useTranslation } from "react-i18next"; import { LuCheckCheck, LuDownload, LuRefreshCw, LuTriangleAlert, LuX, } from "react-icons/lu"; import type { ExternalToast } from "sonner"; import { MOTION_EASE_OUT } from "@/lib/motion"; import { RippleButton } from "./ui/ripple"; interface BaseToastProps { id?: string; title: string; description?: string; duration?: number; action?: ExternalToast["action"]; onCancel?: () => void; } interface LoadingToastProps extends BaseToastProps { type: "loading"; } interface SuccessToastProps extends BaseToastProps { type: "success"; } interface ErrorToastProps extends BaseToastProps { type: "error"; } interface DownloadToastProps extends BaseToastProps { type: "download"; stage?: "downloading" | "extracting" | "verifying" | "completed"; progress?: { percentage: number; speed?: string; eta?: string; }; } interface VersionUpdateToastProps extends BaseToastProps { type: "version-update"; progress?: { current: number; total: number; found: number; current_browser?: string; }; } interface FetchingToastProps extends BaseToastProps { type: "fetching"; browserName?: string; } interface SyncProgressToastProps extends BaseToastProps { type: "sync-progress"; progress?: { completed_files: number; total_files: number; completed_bytes: number; total_bytes: number; speed_bytes_per_sec: number; eta_seconds: number; failed_count: number; phase: string; }; } type ToastProps = | LoadingToastProps | SuccessToastProps | ErrorToastProps | DownloadToastProps | VersionUpdateToastProps | FetchingToastProps | SyncProgressToastProps; function formatBytesCompact(bytes: number): string { if (bytes === 0) return "0 B"; const units = ["B", "KB", "MB", "GB"]; const i = Math.min( Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1, ); const value = bytes / 1024 ** i; return `${i === 0 ? value : value.toFixed(1)} ${units[i]}`; } function formatSpeedCompact(bytesPerSec: number): string { if (bytesPerSec >= 1024 * 1024) { return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s`; } return `${(bytesPerSec / 1024).toFixed(0)} KB/s`; } function formatEtaCompact(seconds: number): string { if (seconds >= 3600) { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); return `${h}h ${m}m`; } if (seconds >= 60) { return `${Math.floor(seconds / 60)} min`; } return `${Math.round(seconds)}s`; } function ProgressBar({ percentage, className = "w-full", }: { percentage: number; className?: string; }) { return (
{title}
{onCancel && ( )}{progress.percentage.toFixed(1)}% {progress.speed && ` • ${progress.speed} MB/s`} {progress.eta && ` • ${t("toasts.progress.remaining", { time: progress.eta })}`}
{progress.percentage.toFixed(1)}%
{progress.current_browser && t("versionUpdater.toast.lookingForUpdates", { browser: progress.current_browser, })}
{progress.phase === "uploading" ? t("appUpdate.toast.uploading") : t("appUpdate.toast.downloading")}{" "} {t("toasts.progress.filesProgress", { completed: progress.completed_files, total: progress.total_files, })} {" \u2022 "} {formatBytesCompact(progress.completed_bytes)} /{" "} {formatBytesCompact(progress.total_bytes)} {progress.speed_bytes_per_sec > 0 && ( <> {" \u2022 "} {formatSpeedCompact(progress.speed_bytes_per_sec)} > )} {progress.eta_seconds > 0 && progress.completed_files < progress.total_files && ` \u2022 ${t("toasts.progress.remaining", { time: `~${formatEtaCompact(progress.eta_seconds)}`, })}`}
{progress.failed_count > 0 && ({t("toasts.progress.filesFailed", { count: progress.failed_count, })}
)}{description}
)} {/* Stage-specific descriptions for downloads */} {type === "download" && !description && ( <> {stage === "extracting" && ({t("browserDownload.toast.extracting")}
)} {stage === "verifying" && ({t("browserDownload.toast.verifying")}
)} > )} {action && "onClick" in (action as { onClick?: () => void; label?: string }) && "label" in (action as { onClick?: () => void; label?: string }) && (