diff --git a/messages/en.json b/messages/en.json index 831b4a5..59f725d 100644 --- a/messages/en.json +++ b/messages/en.json @@ -79,6 +79,11 @@ "errors": { "cant_convert": "We can't convert this file.", "vertd_server": "what are you doing..? you're supposed to run the vertd server!", + "vertd_generic_body": "An error occurred whilst whilst trying convert your video. Would you like to submit this video to the developers to help fix this bug? Only your video file will be sent. No identifiers will be uploaded.", + "vertd_generic_title": "Video conversion error", + "vertd_generic_yes": "Submit video", + "vertd_generic_no": "Don't submit", + "vertd_failed_to_keep": "Failed to keep the video on the server: {error}", "unsupported_format": "Only image, video, audio, and document files are supported", "vertd_not_found": "Could not find the vertd instance to start video conversion. Are you sure the instance URL is set correctly?", "worker_downloading": "The {type} converter is currently being initialized, please wait a few moments.", @@ -167,7 +172,8 @@ "loading_cache": "Loading...", "total_size": "Total Size", "files_cached_label": "Files Cached", - "cache_cleared": "Cache cleared successfully!" + "cache_cleared": "Cache cleared successfully!", + "cache_clear_error": "Failed to clear cache." }, "language": { "title": "Language", diff --git a/src/lib/components/functional/VertdError.svelte b/src/lib/components/functional/VertdError.svelte new file mode 100644 index 0000000..827e09d --- /dev/null +++ b/src/lib/components/functional/VertdError.svelte @@ -0,0 +1,73 @@ + + + + +
+

{m["convert.errors.vertd_generic_body"]()}

+
+ + +
+
diff --git a/src/lib/components/layout/Toasts.svelte b/src/lib/components/layout/Toasts.svelte index bcf5e92..ad59825 100644 --- a/src/lib/components/layout/Toasts.svelte +++ b/src/lib/components/layout/Toasts.svelte @@ -1,19 +1,14 @@
- {#each toastList as { id, type, message, durations }} + {#each ToastManager.toasts as toast (toast.id)}
- +
{/each}
diff --git a/src/lib/components/visual/Toast.svelte b/src/lib/components/visual/Toast.svelte index bf45dd9..b3e68cc 100644 --- a/src/lib/components/visual/Toast.svelte +++ b/src/lib/components/visual/Toast.svelte @@ -8,20 +8,20 @@ XIcon, } from "lucide-svelte"; import { quintOut } from "svelte/easing"; - import { removeToast } from "$lib/store/ToastProvider"; + import { ToastManager } from "$lib/toast/index.svelte"; + import type { ToastProps } from "$lib/toast/index.svelte"; + import type { SvelteComponent } from "svelte"; + import clsx from "clsx"; + import type { Toast as ToastType } from "$lib/toast/index.svelte"; - type Props = { - id: number; - type: "success" | "error" | "info" | "warning"; - message: string; - durations: { - enter: number; - stay: number; - exit: number; - }; - }; + const props: { + toast: ToastType; + } = $props(); - let { id, type, message, durations }: Props = $props(); + const { id, type, message, durations } = props.toast; + + const additional = + "additional" in props.toast ? props.toast.additional : {}; const colors = { success: "purple", @@ -40,6 +40,9 @@ let color = $derived(colors[type]); let Icon = $derived(Icons[type]); + let msg = $state>(); + const title = $derived(((msg as any)?.title as string) ?? ""); + // intentionally unused. this is so tailwind can generate the css for these colours as it doesn't detect if it's dynamically loaded // this would lead to the colours not being generated in the final css file by tailwind const colourVariants = [ @@ -51,7 +54,7 @@
-
- -

- {message} -

+
+
+ +

+ {title || message} +

+
+
- + {#if typeof message !== "string"} + {@const MessageComponent = message} +
+ +
+ {/if}
diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index 2b632a7..b7c1374 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -3,9 +3,9 @@ import { Converter, FormatInfo } from "./converter.svelte"; import { FFmpeg } from "@ffmpeg/ffmpeg"; import { browser } from "$app/environment"; import { error, log } from "$lib/logger"; -import { addToast } from "$lib/store/ToastProvider"; import { m } from "$lib/paraglide/messages"; import { Settings } from "$lib/sections/settings/index.svelte"; +import { ToastManager } from "$lib/toast/index.svelte"; // TODO: differentiate in UI? (not native formats) const videoFormats = [ @@ -98,7 +98,10 @@ export class FFmpegConverter extends Converter { } catch (err) { error(["converters", this.name], `error loading ffmpeg: ${err}`); this.status = "error"; - addToast("error", m["workers.errors.ffmpeg"]()); + ToastManager.add({ + type: "error", + message: m["workers.errors.ffmpeg"](), + }); } } diff --git a/src/lib/converters/magick.svelte.ts b/src/lib/converters/magick.svelte.ts index b460940..46a802e 100644 --- a/src/lib/converters/magick.svelte.ts +++ b/src/lib/converters/magick.svelte.ts @@ -1,13 +1,13 @@ import { browser } from "$app/environment"; import { error, log } from "$lib/logger"; import { m } from "$lib/paraglide/messages"; -import { addToast } from "$lib/store/ToastProvider"; import { VertFile, type WorkerMessage } from "$lib/types"; import MagickWorker from "$lib/workers/magick?worker&url"; import { Converter, FormatInfo } from "./converter.svelte"; import { imageFormats } from "./magick-automated"; import { Settings } from "$lib/sections/settings/index.svelte"; import magickWasm from "@imagemagick/magick-wasm/magick.wasm?url"; +import { ToastManager } from "$lib/toast/index.svelte"; export class MagickConverter extends Converter { public name = "imagemagick"; @@ -104,7 +104,11 @@ export class MagickConverter extends Converter { ["converters", this.name], `Failed to load ImageMagick WASM: ${err}`, ); - addToast("error", m["workers.errors.magick"]()); + + ToastManager.add({ + type: "error", + message: m["workers.errors.magick"](), + }); } } diff --git a/src/lib/converters/pandoc.svelte.ts b/src/lib/converters/pandoc.svelte.ts index 88104a0..1f3fb68 100644 --- a/src/lib/converters/pandoc.svelte.ts +++ b/src/lib/converters/pandoc.svelte.ts @@ -2,8 +2,9 @@ import { VertFile, type WorkerMessage } from "$lib/types"; import { Converter, FormatInfo } from "./converter.svelte"; import { browser } from "$app/environment"; import PandocWorker from "$lib/workers/pandoc?worker&url"; -import { addToast } from "$lib/store/ToastProvider"; import { error, log } from "$lib/logger"; +import { ToastManager } from "$lib/toast/index.svelte"; +import { m } from "$lib/paraglide/messages"; export class PandocConverter extends Converter { public name = "pandoc"; @@ -25,7 +26,10 @@ export class PandocConverter extends Converter { this.status = "ready"; } catch (err) { this.status = "error"; - addToast("error", `Failed to load Pandoc worker: ${err}`); + ToastManager.add({ + type: "error", + message: `Failed to load Pandoc worker: ${err}`, // TODO: i18n + }); } })(); } diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index f5d39ea..52dd596 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -1,3 +1,4 @@ +import VertdErrorComponent from "$lib/components/functional/VertdError.svelte"; import { error, log } from "$lib/logger"; import { Settings } from "$lib/sections/settings/index.svelte"; import { VertdInstance } from "$lib/sections/settings/vertdSettings.svelte"; @@ -25,19 +26,45 @@ interface UploadResponse { totalFrames: number; } -interface RouteMap { - "/api/upload": UploadResponse; - "/api/version": string; +interface RouteRequestMap { + "/api/keep": { + id: string; + token: string; + }; } -const vertdFetch = async ( - url: U, - options: RequestInit, -): Promise => { +interface RouteResponseMap { + "/api/upload": UploadResponse; + "/api/version": string; + "/api/keep": void; +} + +export const vertdFetch: { + ( + url: U, + options: RequestInit, + body: RouteRequestMap[U], + ): Promise; + >( + url: U, + options: RequestInit, + ): Promise; +} = async (url: any, options: RequestInit, body?: any) => { const domain = await VertdInstance.instance.url(); - const res = await fetch(`${domain}${url}`, options); + + // if there is a body, insert a Content-Type: application/json header + if (body) { + options.headers = { + "Content-Type": "application/json", + ...(options.headers || {}), + }; + options.body = JSON.stringify(body); + } + + const res = await fetch(domain + url, options); + const text = await res.text(); - let json: VertdResponse = null!; + let json = null; try { json = JSON.parse(text); } catch { @@ -48,7 +75,7 @@ const vertdFetch = async ( throw new Error(json.data); } - return json.data as RouteMap[U]; + return json.data; }; // ws types @@ -345,7 +372,13 @@ export class VertdConverter extends Converter { case "error": { this.log(`error: ${msg.data.message}`); this.activeConversions.delete(input.id); - reject(msg.data.message); + reject({ + component: VertdErrorComponent, + additional: { + jobId: uploadRes.id, + auth: uploadRes.auth, + }, + }); } } }; diff --git a/src/lib/sections/about/Donate.svelte b/src/lib/sections/about/Donate.svelte index 0d1a87b..8f4ecb8 100644 --- a/src/lib/sections/about/Donate.svelte +++ b/src/lib/sections/about/Donate.svelte @@ -22,7 +22,6 @@ import FancyInput from "$lib/components/functional/FancyInput.svelte"; import Panel from "$lib/components/visual/Panel.svelte"; import { effects } from "$lib/store/index.svelte"; - import { addToast } from "$lib/store/ToastProvider"; import { loadStripe, type Stripe, @@ -39,6 +38,7 @@ import { Elements, PaymentElement } from "svelte-stripe"; import { quintOut } from "svelte/easing"; import { m } from "$lib/paraglide/messages"; + import { ToastManager } from "$lib/toast/index.svelte"; let amount = $state(1); let customAmount = $state(""); @@ -67,7 +67,10 @@ if (!res.ok) { paymentState = "prepay"; - addToast("error", m["about.donate.payment_error"]()); + ToastManager.add({ + type: "error", + message: m["about.donate.payment_error"](), + }); return; } @@ -97,13 +100,13 @@ const submitResult = await elements.submit(); if (submitResult.error) { const period = submitResult.error.message?.endsWith(".") ? "" : "."; - addToast( - "error", - m["about.donate.payment_failed"]({ + ToastManager.add({ + type: "error", + message: m["about.donate.payment_failed"]({ message: submitResult.error.message || "", period, }), - ); + }); enablePay = true; return; } @@ -119,15 +122,18 @@ if (res.error) { const period = res.error.message?.endsWith(".") ? "" : "."; - addToast( - "error", - m["about.donate.payment_failed"]({ + ToastManager.add({ + type: "error", + message: m["about.donate.payment_failed"]({ message: res.error.message || "", period, }), - ); + }); } else { - addToast("success", m["about.donate.thank_you"]()); + ToastManager.add({ + type: "info", + message: m["about.donate.thank_you"](), + }); } paymentState = "prepay"; @@ -146,10 +152,16 @@ if (status) { switch (status) { case "succeeded": - addToast("success", m["about.donate.thank_you"]()); + ToastManager.add({ + type: "success", + message: m["about.donate.thank_you"](), + }); break; default: - addToast("error", m["about.donate.donation_error"]()); + ToastManager.add({ + type: "error", + message: m["about.donate.donation_error"](), + }); } goto("/about"); diff --git a/src/lib/sections/about/Sponsors.svelte b/src/lib/sections/about/Sponsors.svelte index 86ea317..71dce29 100644 --- a/src/lib/sections/about/Sponsors.svelte +++ b/src/lib/sections/about/Sponsors.svelte @@ -4,9 +4,9 @@ import HotMilk from "$lib/assets/hotmilk.svg?component"; import { DISCORD_URL } from "$lib/consts"; import { error } from "$lib/logger"; - import { addToast } from "$lib/store/ToastProvider"; import { m } from "$lib/paraglide/messages"; import { link } from "$lib/store/index.svelte"; + import { ToastManager } from "$lib/toast/index.svelte"; let copied = false; let timeoutId: number | undefined; @@ -15,7 +15,10 @@ try { navigator.clipboard.writeText("hello@vert.sh"); copied = true; - addToast("success", m["about.sponsors.email_copied"]()); + ToastManager.add({ + type: "success", + message: m["about.sponsors.email_copied"](), + }); if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => (copied = false), 2000); diff --git a/src/lib/sections/settings/Privacy.svelte b/src/lib/sections/settings/Privacy.svelte index 38cbd73..84fbfec 100644 --- a/src/lib/sections/settings/Privacy.svelte +++ b/src/lib/sections/settings/Privacy.svelte @@ -12,9 +12,9 @@ import { m } from "$lib/paraglide/messages"; import { link } from "$lib/store/index.svelte"; import { swManager, type CacheInfo } from "$lib/sw/register"; - import { addToast } from "$lib/store/ToastProvider"; import { onMount } from "svelte"; import { error } from "$lib/logger"; + import { ToastManager } from "$lib/toast/index.svelte"; const { settings = $bindable() }: { settings: ISettings } = $props(); @@ -50,10 +50,16 @@ await swManager.clearCache(); cacheInfo = null; await loadCacheInfo(); - addToast("success", m["settings.privacy.cache_cleared"]()); + ToastManager.add({ + type: "success", + message: m["settings.privacy.cache_cleared"](), + }); } catch (err) { error(["privacy", "cache"], "Failed to clear cache:", err); - addToast("error", "Failed to clear cache"); + ToastManager.add({ + type: "error", + message: m["settings.privacy.cache_clear_error"](), + }); } finally { isLoadingCache = false; } diff --git a/src/lib/store/ToastProvider.ts b/src/lib/store/ToastProvider.ts deleted file mode 100644 index 9e28267..0000000 --- a/src/lib/store/ToastProvider.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { writable } from "svelte/store"; - -export type ToastType = "success" | "error" | "info" | "warning"; - -export interface Toast { - id: number; - type: ToastType; - message: string; - disappearing: boolean; - durations: { - enter: number; - stay: number; - exit: number; - }; -} - -const toasts = writable([]); - -let toastId = 0; - -function addToast( - type: ToastType, - message: string, - disappearing?: boolean, - durations?: { enter: number; stay: number; exit: number }, -) { - const id = toastId++; - - durations = durations ?? { - enter: 300, - stay: disappearing || disappearing === undefined ? 5000 : 86400000, // 24h cause why not - exit: 500, - }; - - const newToast: Toast = { - id, - type, - message, - disappearing: disappearing ?? true, - durations, - }; - toasts.update((currentToasts) => [...currentToasts, newToast]); - - setTimeout( - () => { - removeToast(id); - }, - durations.enter + durations.stay + durations.exit, - ); - - return id; -} - -function removeToast(id: number) { - toasts.update((currentToasts) => - currentToasts.filter((toast) => toast.id !== id), - ); -} - -export { toasts, addToast, removeToast }; diff --git a/src/lib/toast/index.svelte.ts b/src/lib/toast/index.svelte.ts new file mode 100644 index 0000000..71dfdf6 --- /dev/null +++ b/src/lib/toast/index.svelte.ts @@ -0,0 +1,226 @@ +import type { Component } from "svelte"; +import { writable } from "svelte/store"; + +export type ToastType = "success" | "error" | "info" | "warning"; + +// export interface Toast< +// T = unknown, +// U extends string | ToastComponent = string | ToastComponent, +// > { +// id: number; +// type: ToastType; +// message: U; +// disappearing: boolean; +// durations: { +// enter: number; +// stay: number; +// exit: number; +// }; +// additional: U extends string ? undefined : T; +// } + +type BaseToast = { + id: number; + type: ToastType; + disappearing: boolean; + durations: { + enter: number; + stay: number; + exit: number; + }; +}; + +export type StringToast = BaseToast & { + message: string; +}; + +export type ComponentToast = BaseToast & { + message: ToastComponent; + additional: T; +}; + +export type Toast = StringToast | ComponentToast; + +export type ToastProps = Omit, "disappearing">; + +export type ToastExports = { + title?: string; +}; + +export type ToastComponent = Component, ToastExports>; + +// export interface ToastOptions { +// type?: ToastType; +// message: string | ToastComponent; +// disappearing?: boolean; +// durations?: { +// enter?: number; +// stay?: number; +// exit?: number; +// }; +// additional?: T; +// } + +type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends object | undefined + ? RecursivePartial + : T[P]; +}; + +type BaseToastOptions = Omit, "id"> & { + disappearing?: boolean; +}; + +export type StringToastOptions = BaseToastOptions & { + message: string; +}; + +export type ComponentToastOptions = BaseToastOptions & { + message: ToastComponent; + additional: T; +}; + +export type ToastOptions = + | StringToastOptions + | ComponentToastOptions; + +// const toasts = writable([]); + +// let toastId = 0; + +// function addToast( +// type: ToastType, +// message: string | Component, +// disappearing?: boolean, +// durations?: { enter: number; stay: number; exit: number }, +// ) { +// const id = toastId++; + +// durations = durations ?? { +// enter: 300, +// stay: disappearing || disappearing === undefined ? 5000 : 86400000, // 24h cause why not +// exit: 500, +// }; + +// const newToast: Toast = { +// id, +// type, +// message, +// disappearing: disappearing ?? true, +// durations, +// }; +// toasts.update((currentToasts) => [...currentToasts, newToast]); + +// setTimeout( +// () => { +// removeToast(id); +// }, +// durations.enter + durations.stay + durations.exit, +// ); + +// return id; +// } + +// function removeToast(id: number) { +// toasts.update((currentToasts) => +// currentToasts.filter((toast) => toast.id !== id), +// ); +// } + +// export { toasts, addToast, removeToast }; + +// const DURATION_DEFAULTS = { +// enter: 300, +// stay: 5000, +// exit: 500, +// }; + +const durationDefault = (disappearing: boolean) => ({ + enter: 300, + stay: disappearing ? 5000 : 86400000, // 24h cause why not + exit: 500, +}); + +// const toastState = { +// toasts: $state([]), +// }; + +class ToastState { + private pId = $state(0); + private pToasts = $state[]>([]); + + public add(toast: Toast) { + this.pToasts.push(toast as Toast); + } + + public remove(id: number) { + this.pToasts = this.pToasts.filter((toast) => toast.id !== id); + } + + public id(): number { + return this.pId++; + } + + public get toasts() { + return this.pToasts; + } +} + +export class ToastManager { + static pToasts = new ToastState(); + + public static add(toastOptions: ToastOptions): number { + const id = this.pToasts.id(); + const { + type = "info", + disappearing = true, + durations: d = durationDefault(toastOptions.disappearing ?? true), + } = toastOptions; + const durations = { + ...durationDefault(disappearing), + ...d, + }; + + if (typeof toastOptions.message === "string") { + const newToast: StringToast = { + id, + type, + message: toastOptions.message, + disappearing, + durations, + }; + + this.pToasts.add(newToast); + } else { + const newToast: ComponentToast = { + id, + type, + message: toastOptions.message, + disappearing, + durations, + additional: (toastOptions as ComponentToastOptions) + .additional, + }; + + this.pToasts.add(newToast); + } + + setTimeout( + () => { + this.remove(id); + }, + durations.enter + durations.stay + durations.exit, + ); + return id; + } + + public static remove(id: number) { + this.pToasts.remove(id); + } + + public static get toasts() { + return this.pToasts.toasts; + } +} diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index 5f3dcba..7501a43 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -2,7 +2,8 @@ import { byNative, converters } from "$lib/converters"; import type { Converter } from "$lib/converters/converter.svelte"; import { error } from "$lib/logger"; import { m } from "$lib/paraglide/messages"; -import { addToast } from "$lib/store/ToastProvider"; +import { ToastManager } from "$lib/toast/index.svelte"; +import type { Component } from "svelte"; export class VertFile { public id: string = Math.random().toString(36).slice(2, 8); @@ -93,15 +94,7 @@ export class VertFile { this.result = res; } catch (err) { if (!this.cancelled) { - const castedErr = err as Error; - error(["files"], castedErr.message); - addToast( - "error", - m["workers.errors.general"]({ - file: this.file.name, - message: castedErr.message || castedErr, - }), - ); + this.toastErr(err); } this.result = null; } @@ -119,15 +112,51 @@ export class VertFile { this.processing = false; this.result = null; } catch (err) { - const castedErr = err as Error; - error(["files"], castedErr.message); - addToast( - "error", - m["workers.errors.cancel"]({ + this.toastErr(err); + } + } + + private toastErr(err: unknown) { + type ToastMsg = { + component: Component; + additional: unknown; + }; + + const castedErr = err as Error | string | ToastMsg; + let toastMsg: string | ToastMsg = ""; + if (typeof castedErr === "string") { + toastMsg = castedErr; + } else if (castedErr instanceof Error) { + toastMsg = castedErr.message; + } else { + toastMsg = castedErr; + } + + // ToastManager.add({ + // type: "error", + // message: + // typeof toastMsg === "string" + // ? m["workers.errors.general"]({ + // file: this.file.name, + // message: toastMsg, + // }) + // : toastMsg, + // }); + + if (typeof toastMsg === "string") { + ToastManager.add({ + type: "error", + message: m["workers.errors.general"]({ file: this.file.name, - message: castedErr.message || castedErr, + message: toastMsg, }), - ); + }); + } else { + ToastManager.add({ + type: "error", + message: toastMsg.component, + additional: toastMsg.additional, + }); } } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 0f9bd9f..31a8095 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -24,6 +24,8 @@ import { initStores as initAnimStores } from "$lib/animation/index.js"; import { locales, localizeHref } from "$lib/paraglide/runtime"; import { VertdInstance } from "$lib/sections/settings/vertdSettings.svelte.js"; + import { ToastManager } from "$lib/toast/index.svelte.js"; + import VertdError from "$lib/components/functional/VertdError.svelte"; let { children, data } = $props(); let enablePlausible = $state(false); diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index 5ff0993..fcbddfd 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -9,10 +9,10 @@ import avatarRealmy from "$lib/assets/avatars/realmy.jpg"; import avatarAzurejelly from "$lib/assets/avatars/azurejelly.jpg"; import { GITHUB_API_URL } from "$lib/consts"; - import { addToast } from "$lib/store/ToastProvider"; import { dev } from "$app/environment"; import { page } from "$app/state"; import { m } from "$lib/paraglide/messages"; + import { ToastManager } from "$lib/toast/index.svelte"; // import { dev } from "$app/environment"; // import { page } from "$app/state"; @@ -81,7 +81,10 @@ try { const response = await fetch(`${GITHUB_API_URL}/contributors`); if (!response.ok) { - addToast("error", m["about.errors.github_contributors"]()); + ToastManager.add({ + type: "error", + message: m["about.errors.github_contributors"](), + }); throw new Error(`HTTP error, status: ${response.status}`); } const allContribs = await response.json(); diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index 2f1528d..dc45eee 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -2,11 +2,11 @@ import { browser } from "$app/environment"; import { log } from "$lib/logger"; import * as Settings from "$lib/sections/settings/index.svelte"; - import { addToast } from "$lib/store/ToastProvider"; import { PUB_PLAUSIBLE_URL } from "$env/static/public"; import { SettingsIcon } from "lucide-svelte"; import { onMount } from "svelte"; import { m } from "$lib/paraglide/messages"; + import { ToastManager } from "$lib/toast/index.svelte"; let settings = $state(Settings.Settings.instance.settings); @@ -32,7 +32,10 @@ log(["settings"], "saving settings"); } catch (error) { log(["settings", "error"], `failed to save settings: ${error}`); - addToast("error", m["settings.errors.save_failed"]()); + ToastManager.add({ + type: "error", + message: m["settings.errors.save_failed"](), + }); } });