/** * Unified Toast System * * This module provides a comprehensive toast system that solves styling issues * and provides a single, flexible toast component for all use cases. * * Features: * - Proper background styling (no transparency issues) * - Loading states with spinners * - Progress bars for downloads/updates * - Success/error states * - Customizable icons and content * - Auto-update notifications * * Usage Examples: * * Simple loading toast: * ``` * import { showToast } from "./custom-toast"; * showToast({ * type: "loading", * title: "Loading...", * description: "Please wait..." * }); * ``` * * Auto-update toast: * ``` * showAutoUpdateToast("Wayfern", "149.0.7827.116"); * ``` * * Download progress toast: * ``` * showToast({ * type: "download", * title: "Downloading Wayfern 149.0.7827.116", * progress: { percentage: 45, speed: "2.5", eta: "30s" } * }); * ``` * * Version update progress: * ``` * showToast({ * type: "version-update", * title: "Updating browser versions", * progress: { current: 3, total: 5, found: 12 } * }); * ``` */ /** biome-ignore-all lint/suspicious/noExplicitAny: TODO */ import { useTranslation } from "react-i18next"; import { LuCheckCheck, LuDownload, LuRefreshCw, LuTriangleAlert, LuX, } from "react-icons/lu"; import type { ExternalToast } from "sonner"; 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 }) && (