/** * 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("Firefox", "125.0.1"); * ``` * * Download progress toast: * ``` * showToast({ * type: "download", * title: "Downloading Firefox 123.0", * 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" | "downloading (twilight rolling release)"; 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 TwilightUpdateToastProps extends BaseToastProps { type: "twilight-update"; browserName?: string; hasUpdate?: boolean; } 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 | TwilightUpdateToastProps | 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 getToastIcon(type: ToastProps["type"], stage?: string) { switch (type) { case "success": return ; case "error": return ; case "download": if (stage === "completed") { return ; } return ; case "version-update": return ( ); case "fetching": return ( ); case "twilight-update": return ( ); case "sync-progress": return ( ); case "loading": return (
); default: return (
); } } export function UnifiedToast(props: ToastProps) { const { t } = useTranslation(); const { title, description, type, action, onCancel } = props; const stage = "stage" in props ? props.stage : undefined; const progress = "progress" in props ? props.progress : undefined; return (
{getToastIcon(type, stage)}

{title}

{onCancel && ( )}
{/* Download progress */} {type === "download" && progress && "percentage" in progress && stage === "downloading" && (

{progress.percentage.toFixed(1)}% {progress.speed && ` • ${progress.speed} MB/s`} {progress.eta && ` • ${progress.eta} remaining`}

)} {/* Version update progress */} {type === "version-update" && progress && "current_browser" in progress && (

{progress.current_browser && ( <>Looking for updates for {progress.current_browser} )}

{progress.current}/{progress.total}
)} {/* Sync progress */} {type === "sync-progress" && progress && "completed_files" in progress && (

{progress.phase === "uploading" ? t("appUpdate.toast.uploading") : t("appUpdate.toast.downloading")}{" "} {progress.completed_files}/{progress.total_files} 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 ~"} {formatEtaCompact(progress.eta_seconds)} remaining )}

{progress.failed_count > 0 && (

{progress.failed_count} file(s) failed

)}
)} {/* Twilight update progress */} {type === "twilight-update" && (

{"hasUpdate" in props && props.hasUpdate ? "New twilight build available for download" : "Checking for twilight updates..."}

{props.browserName && (

{props.browserName} • Rolling Release

)}
)} {/* Description */} {description && (

{description}

)} {/* Stage-specific descriptions for downloads */} {type === "download" && !description && ( <> {stage === "extracting" && (

{t("browserDownload.toast.extracting")}

)} {stage === "verifying" && (

{t("browserDownload.toast.verifying")}

)} {stage === "downloading (twilight rolling release)" && (

{t("browserDownload.toast.downloadingRolling")}

)} )} {action && "onClick" in (action as { onClick?: () => void; label?: string }) && "label" in (action as { onClick?: () => void; label?: string }) && (
void; label: string }).onClick } > {(action as { onClick: () => void; label: string }).label}
)}
); }