import type { ToastItem, ToastSeverity } from "$lib/types/app"; import { errorMessage } from "$lib/utils/errors.js"; // Minimal toast store. Roll-our-own (no svelte-french-toast etc) so Kon // stays lean. Toasts auto-dismiss after `duration` ms unless duration is 0 // (sticky) or unless the user clicks the close button. // // Severity → colour: // info → moss (informational, default 4s) // success → moss (operation succeeded, default 3s) // warn → signal (degraded but not failed, default 6s) // error → ember (operation failed, sticky until dismissed) // // Usage from a component: // import { toasts } from '$lib/stores/toasts.svelte.js'; // toasts.error('Could not start recording', 'Selected microphone disappeared.'); // toasts.success('Saved'); let nextId = 1; // Soft cap on how many toasts can be queued at once. Error toasts are sticky // (duration: 0) so a misbehaving command that fires errors in a loop can pile // up indefinitely without this cap. When exceeded, the oldest toast is // evicted to make room. const MAX_TOASTS = 50; function defaultDuration(severity: ToastSeverity): number { switch (severity) { case "success": return 3000; case "info": return 4000; case "warn": return 6000; case "error": return 0; // sticky default: return 4000; } } function createToastsStore() { // $state requires a class field or top-level let in Svelte 5; we expose // `items` via a getter on the singleton. let items = $state([]); function show(severity: ToastSeverity, title: string, body?: string) { const id = nextId++; const duration = defaultDuration(severity); const toast = { id, severity, title, body: body ?? '', duration }; items.push(toast); if (items.length > MAX_TOASTS) { items.splice(0, items.length - MAX_TOASTS); } if (duration > 0) { setTimeout(() => dismiss(id), duration); } return id; } function dismiss(id: number) { const ix = items.findIndex(t => t.id === id); if (ix >= 0) items.splice(ix, 1); } function dismissAll() { items.length = 0; } return { get items() { return items; }, info: (title: string, body?: string) => show("info", title, body), success: (title: string, body?: string) => show("success", title, body), warn: (title: string, body?: string) => show("warn", title, body), error: (title: string, body?: string) => show("error", title, body), dismiss, dismissAll, }; } export const toasts = createToastsStore(); // Helper: wrap a Tauri invoke and toast on failure. Returns the result on // success, throws on failure (so callers that need to handle the error // can still try/catch). // // import { invoke } from '@tauri-apps/api/core'; // import { invokeWithToast } from '$lib/stores/toasts.svelte.js'; // const result = await invokeWithToast('start_native_capture', { deviceName }); // // Optional `errorTitle` overrides the default ("Action failed"). export async function invokeWithToast( invokeFn: (command: string, args?: TArgs) => Promise, command: string, args?: TArgs, errorTitle?: string, ): Promise { try { return await invokeFn(command, args); } catch (err) { toasts.error(errorTitle ?? "Action failed", errorMessage(err)); throw err; } }