Files
donutbrowser/src/lib/toast-utils.ts
T
2026-06-01 01:05:35 +04:00

317 lines
7.5 KiB
TypeScript

import { invoke } from "@tauri-apps/api/core";
import React from "react";
import { type ExternalToast, toast as sonnerToast } from "sonner";
import { UnifiedToast } from "@/components/custom-toast";
interface BaseToastProps {
id?: string;
title: string;
description?: string;
duration?: number;
action?: ExternalToast["action"];
onCancel?: () => void;
// When false, the toast cannot be dismissed by the user (no swipe; combine
// with duration: Infinity and no onCancel to make it fully non-closable).
dismissible?: boolean;
}
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 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 =
| SuccessToastProps
| ErrorToastProps
| DownloadToastProps
| LoadingToastProps
| VersionUpdateToastProps
| SyncProgressToastProps;
export function showToast(props: ToastProps & { id?: string }) {
const toastId = props.id ?? `toast-${props.type}-${Date.now()}`;
let duration: number;
if (props.duration !== undefined) {
duration = props.duration;
} else {
switch (props.type) {
case "loading":
duration = 10000;
break;
case "download":
if ("stage" in props && props.stage === "completed") {
duration = 3000;
} else {
duration = Number.POSITIVE_INFINITY;
}
break;
case "success":
duration = 3000;
break;
case "error":
duration = 10000;
break;
case "version-update":
duration = 15000;
break;
case "sync-progress":
duration = Number.POSITIVE_INFINITY;
break;
default:
duration = 5000;
}
}
if (props.type === "success") {
sonnerToast.custom(() => React.createElement(UnifiedToast, props), {
id: toastId,
duration,
dismissible: props.dismissible,
style: {
background: "transparent",
border: "none",
boxShadow: "none",
padding: 0,
zIndex: 10001,
pointerEvents: "auto",
},
});
} else if (props.type === "error") {
sonnerToast.custom(() => React.createElement(UnifiedToast, props), {
id: toastId,
duration,
dismissible: props.dismissible,
style: {
background: "transparent",
border: "none",
boxShadow: "none",
padding: 0,
zIndex: 10001,
pointerEvents: "auto",
},
});
} else {
sonnerToast.custom(() => React.createElement(UnifiedToast, props), {
id: toastId,
duration,
dismissible: props.dismissible,
style: {
background: "transparent",
border: "none",
boxShadow: "none",
padding: 0,
zIndex: 10001,
pointerEvents: "auto",
},
});
}
return toastId;
}
export function showDownloadToast(
browserName: string,
version: string,
stage:
| "downloading"
| "extracting"
| "verifying"
| "completed"
| "downloading (twilight rolling release)",
progress?: { percentage: number; speed?: string; eta?: string },
options?: { suppressCompletionToast?: boolean; onCancel?: () => void },
) {
const title =
stage === "completed"
? `${browserName} ${version} downloaded successfully!`
: stage === "downloading"
? `Downloading ${browserName} ${version}`
: stage === "extracting"
? `Extracting ${browserName} ${version}`
: stage === "downloading (twilight rolling release)"
? `Downloading ${browserName} ${version}`
: `Verifying ${browserName} ${version}`;
// Don't show completion toast if suppressed (for auto-update scenarios)
if (stage === "completed" && options?.suppressCompletionToast) {
dismissToast(`download-${browserName.toLowerCase()}-${version}`);
return;
}
// Only show cancel button during active downloading, not for completed/extracting/verifying
const showCancel =
stage === "downloading" ||
stage === "downloading (twilight rolling release)";
return showToast({
type: "download",
title,
stage,
progress,
id: `download-${browserName.toLowerCase()}-${version}`,
onCancel: showCancel ? options?.onCancel : undefined,
});
}
export function showSuccessToast(
title: string,
options?: {
id?: string;
description?: string;
duration?: number;
},
) {
return showToast({
type: "success",
title,
...options,
});
}
export function showErrorToast(
title: string,
options?: {
id?: string;
description?: string;
duration?: number;
},
) {
return showToast({
type: "error",
title,
...options,
});
}
export function showAutoUpdateToast(
browserName: string,
version: string,
options?: {
id?: string;
description?: string;
duration?: number;
},
) {
return showToast({
type: "loading",
title: `${browserName} update started`,
description:
options?.description ??
`Automatically downloading ${browserName} ${version}. Progress will be shown in download notifications.`,
id: options?.id ?? `auto-update-${browserName.toLowerCase()}-${version}`,
duration: options?.duration ?? 4000,
});
}
export function dismissToast(id: string) {
sonnerToast.dismiss(id);
}
export function showSyncProgressToast(
profileName: string,
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;
},
options?: { id?: string; profileId?: string },
) {
return showToast({
type: "sync-progress",
title: `Syncing profile '${profileName}'...`,
progress,
id: options?.id,
duration: Number.POSITIVE_INFINITY,
onCancel: () => {
if (options?.profileId) {
// Fire-and-forget — backend flips the cancel flag for the in-flight
// upload/download loops to drain.
void invoke("cancel_profile_sync", {
profileId: options.profileId,
}).catch((err: unknown) => {
console.error("Failed to cancel sync:", err);
});
}
if (options?.id) {
dismissToast(options.id);
}
},
});
}
export function showUnifiedVersionUpdateToast(
title: string,
options?: {
id?: string;
description?: string;
progress?: {
current: number;
total: number;
found: number;
current_browser?: string;
};
duration?: number;
onCancel?: () => void;
},
) {
return showToast({
type: "version-update",
title,
id: "unified-version-update",
duration: Number.POSITIVE_INFINITY, // Keep showing until completed
...options,
});
}