-
-
-
- {message}
-
+
+
+
+
+ {title || message}
+
+
+
ToastManager.remove(id)}
+ >
+
+
-
removeToast(id)}
- >
-
-
+ {#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"](),
+ });
}
});