feat: donations

This commit is contained in:
not-nullptr 2025-06-26 01:33:22 +01:00
parent 3f3c280241
commit 859760aaf7
22 changed files with 519 additions and 478 deletions

View File

@ -1,4 +1,8 @@
PUB_HOSTNAME=localhost:5173 # only gets used for plausible (for now) PUB_HOSTNAME=localhost:5173 # only gets used for plausible (for now)
PUB_PLAUSIBLE_URL=https://plausible.example.com # can be empty PUB_PLAUSIBLE_URL=https://plausible.example.com # can be empty
PUB_ENV=development # "production", "development", or "nightly" PUB_ENV=development # "production", "development", or "nightly"
PUB_VERTD_URL=https://vertd.vert.sh # default vertd instance PUB_VERTD_URL=https://vertd.vert.sh # default vertd instance
# please do not change these. donations help a lot
PUB_DONATION_URL=https://donations.vert.sh
PUB_STRIPE_KEY=pk_live_51RDVmAGSxPVad6bQwzVNnbc28nlmzA30krLWk1fefCMpUPiSRPkavMMbGqa8A3lUaOCMlsUEVy2CWDYg0ip3aPpL00ZJlsMkf2

View File

@ -1,4 +0,0 @@
# For libvips/wasm-vips converter (images)
/*
Cross-Origin-Embedder-Policy: require-corp
Cross-Origin-Opener-Policy: same-origin

View File

@ -11,6 +11,7 @@
"@fontsource/lexend": "^5.1.2", "@fontsource/lexend": "^5.1.2",
"@fontsource/radio-canada-big": "^5.1.1", "@fontsource/radio-canada-big": "^5.1.1",
"@imagemagick/magick-wasm": "^0.0.34", "@imagemagick/magick-wasm": "^0.0.34",
"@stripe/stripe-js": "^7.4.0",
"byte-data": "^19.0.1", "byte-data": "^19.0.1",
"client-zip": "^2.4.6", "client-zip": "^2.4.6",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -18,10 +19,10 @@
"music-metadata": "^11.0.0", "music-metadata": "^11.0.0",
"p-queue": "^8.1.0", "p-queue": "^8.1.0",
"riff-file": "^1.0.3", "riff-file": "^1.0.3",
"svelte-stripe": "^1.4.0",
"vert-wasm": "^0.0.2", "vert-wasm": "^0.0.2",
"vite-plugin-static-copy": "^2.2.0", "vite-plugin-static-copy": "^2.2.0",
"vite-plugin-wasm": "^3.4.1", "vite-plugin-wasm": "^3.4.1",
"wasm-vips": "^0.0.11",
}, },
"devDependencies": { "devDependencies": {
"@poppanator/sveltekit-svg": "^5.0.0", "@poppanator/sveltekit-svg": "^5.0.0",
@ -235,6 +236,8 @@
"@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="],
"@stripe/stripe-js": ["@stripe/stripe-js@7.4.0", "", {}, "sha512-lQHQPfXPTBeh0XFjq6PqSBAyR7umwcJbvJhXV77uGCUDD6ymXJU/f2164ydLMLCCceNuPlbV9b+1smx98efwWQ=="],
"@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="], "@sveltejs/acorn-typescript": ["@sveltejs/acorn-typescript@1.0.5", "", { "peerDependencies": { "acorn": "^8.9.0" } }, "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ=="],
"@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.8", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg=="], "@sveltejs/adapter-static": ["@sveltejs/adapter-static@3.0.8", "", { "peerDependencies": { "@sveltejs/kit": "^2.0.0" } }, "sha512-YaDrquRpZwfcXbnlDsSrBQNCChVOT9MGuSg+dMAyfsAa1SmiAhrA5jUYUiIMC59G92kIbY/AaQOWcBdq+lh+zg=="],
@ -727,6 +730,8 @@
"svelte-eslint-parser": ["svelte-eslint-parser@0.43.0", "", { "dependencies": { "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "postcss": "^8.4.39", "postcss-scss": "^4.0.9" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA=="], "svelte-eslint-parser": ["svelte-eslint-parser@0.43.0", "", { "dependencies": { "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", "postcss": "^8.4.39", "postcss-scss": "^4.0.9" }, "peerDependencies": { "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "optionalPeers": ["svelte"] }, "sha512-GpU52uPKKcVnh8tKN5P4UZpJ/fUDndmq7wfsvoVXsyP+aY0anol7Yqo01fyrlaWGMFfm4av5DyrjlaXdLRJvGA=="],
"svelte-stripe": ["svelte-stripe@1.4.0", "", { "peerDependencies": { "@stripe/stripe-js": "^3 || ^4", "svelte": "^3 || ^4 || ^5" } }, "sha512-RUSui4IszIBXhGt3mT3pLJX17OJ34A0O+LAcZLooWVYQCAv95umVXoRB6WmjMabj3jOoJ8c3KHGufaJLRlIzRg=="],
"svgo": ["svgo@3.3.2", "", { "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" }, "bin": "./bin/svgo" }, "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw=="], "svgo": ["svgo@3.3.2", "", { "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", "css-tree": "^2.3.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.0.0" }, "bin": "./bin/svgo" }, "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw=="],
"tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="], "tailwindcss": ["tailwindcss@3.4.17", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", "chokidar": "^3.6.0", "didyoumean": "^1.2.2", "dlv": "^1.1.3", "fast-glob": "^3.3.2", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", "jiti": "^1.21.6", "lilconfig": "^3.1.3", "micromatch": "^4.0.8", "normalize-path": "^3.0.0", "object-hash": "^3.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.47", "postcss-import": "^15.1.0", "postcss-js": "^4.0.1", "postcss-load-config": "^4.0.2", "postcss-nested": "^6.2.0", "postcss-selector-parser": "^6.1.2", "resolve": "^1.22.8", "sucrase": "^3.35.0" }, "bin": { "tailwind": "lib/cli.js", "tailwindcss": "lib/cli.js" } }, "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og=="],
@ -777,8 +782,6 @@
"vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="], "vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="],
"wasm-vips": ["wasm-vips@0.0.11", "", {}, "sha512-bzFU7WcimMY4WeqnZk7whKVpSXxpagISXPJwsk2VHF4lgIN9rl4uUo5sF9x6jOlACuCH6ITZUJ7QPTYmy60NCQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],

View File

@ -8,6 +8,8 @@ services:
- PUB_ENV=${PUB_ENV:-production} - PUB_ENV=${PUB_ENV:-production}
- PORT=${PORT:-3000} - PORT=${PORT:-3000}
- PUB_VERTD_URL=${PUB_VERTD_URL:-https://vertd.vert.sh} - PUB_VERTD_URL=${PUB_VERTD_URL:-https://vertd.vert.sh}
- PUB_DONATION_URL=${PUB_DONATION_URL:-https://donations.vert.sh}
- PUB_STRIPE_KEY=${PUB_STRIPE_KEY:-pk_live_51RDVmAGSxPVad6bQwzVNnbc28nlmzA30krLWk1fefCMpUPiSRPkavMMbGqa8A3lUaOCMlsUEVy2CWDYg0ip3aPpL00ZJlsMkf2}
build: build:
context: . context: .
args: args:
@ -15,6 +17,9 @@ services:
PUB_PLAUSIBLE_URL: ${PUB_PLAUSIBLE_URL:-https://plausible.example.com} PUB_PLAUSIBLE_URL: ${PUB_PLAUSIBLE_URL:-https://plausible.example.com}
PUB_ENV: ${PUB_ENV:-production} PUB_ENV: ${PUB_ENV:-production}
PUB_VERTD_URL: ${PUB_VERTD_URL:-https://vertd.vert.sh} PUB_VERTD_URL: ${PUB_VERTD_URL:-https://vertd.vert.sh}
PUB_DONATION_URL: ${PUB_DONATION_URL:-https://donations.vert.sh}
PUB_STRIPE_KEY: ${PUB_STRIPE_KEY:-pk_live_51RDVmAGSxPVad6bQwzVNnbc28nlmzA30krLWk1fefCMpUPiSRPkavMMbGqa8A3lUaOCMlsUEVy2CWDYg0ip3aPpL00ZJlsMkf2}
restart: unless-stopped restart: unless-stopped
ports: ports:
- ${PORT:-3000}:80 - ${PORT:-3000}:80

View File

@ -12,8 +12,4 @@ server {
} }
error_page 404 /index.html; error_page 404 /index.html;
add_header Cross-Origin-Embedder-Policy "require-corp";
add_header Cross-Origin-Opener-Policy "same-origin";
add_header Cross-Origin-Resource-Policy "cross-origin";
} }

View File

@ -42,6 +42,7 @@
"@fontsource/lexend": "^5.1.2", "@fontsource/lexend": "^5.1.2",
"@fontsource/radio-canada-big": "^5.1.1", "@fontsource/radio-canada-big": "^5.1.1",
"@imagemagick/magick-wasm": "^0.0.34", "@imagemagick/magick-wasm": "^0.0.34",
"@stripe/stripe-js": "^7.4.0",
"byte-data": "^19.0.1", "byte-data": "^19.0.1",
"client-zip": "^2.4.6", "client-zip": "^2.4.6",
"clsx": "^2.1.1", "clsx": "^2.1.1",
@ -49,9 +50,9 @@
"music-metadata": "^11.0.0", "music-metadata": "^11.0.0",
"p-queue": "^8.1.0", "p-queue": "^8.1.0",
"riff-file": "^1.0.3", "riff-file": "^1.0.3",
"svelte-stripe": "^1.4.0",
"vert-wasm": "^0.0.2", "vert-wasm": "^0.0.2",
"vite-plugin-static-copy": "^2.2.0", "vite-plugin-static-copy": "^2.2.0",
"vite-plugin-wasm": "^3.4.1", "vite-plugin-wasm": "^3.4.1"
"wasm-vips": "^0.0.11"
} }
} }

View File

@ -64,8 +64,6 @@
const maskImage = $derived( const maskImage = $derived(
`linear-gradient(to top, transparent ${100 - at.current}%, black 100%)`, `linear-gradient(to top, transparent ${100 - at.current}%, black 100%)`,
); );
$inspect(colors);
</script> </script>
{#if page.url.pathname === "/"} {#if page.url.pathname === "/"}

View File

@ -63,9 +63,6 @@
let links = $state<HTMLAnchorElement[]>([]); let links = $state<HTMLAnchorElement[]>([]);
let container = $state<HTMLDivElement>(); let container = $state<HTMLDivElement>();
let containerRect = $derived(container?.getBoundingClientRect()); let containerRect = $derived(container?.getBoundingClientRect());
$effect(() => {
$inspect(containerRect);
});
const linkRects = $derived(links.map((l) => l.getBoundingClientRect())); const linkRects = $derived(links.map((l) => l.getBoundingClientRect()));

View File

@ -2,10 +2,10 @@ import type { Categories } from "$lib/types";
import { FFmpegConverter } from "./ffmpeg.svelte"; import { FFmpegConverter } from "./ffmpeg.svelte";
import { PandocConverter } from "./pandoc.svelte"; import { PandocConverter } from "./pandoc.svelte";
import { VertdConverter } from "./vertd.svelte"; import { VertdConverter } from "./vertd.svelte";
import { VipsConverter } from "./vips.svelte"; import { MagickConverter } from "./magick.svelte";
export const converters = [ export const converters = [
new VipsConverter(), new MagickConverter(),
new FFmpegConverter(), new FFmpegConverter(),
new VertdConverter(), new VertdConverter(),
new PandocConverter(), new PandocConverter(),
@ -37,9 +37,9 @@ categories.video.formats =
?.formatStrings((f) => f.toSupported) || []; ?.formatStrings((f) => f.toSupported) || [];
categories.image.formats = categories.image.formats =
converters converters
.find((c) => c.name === "libvips") .find((c) => c.name === "imagemagick")
?.formatStrings((f) => f.toSupported) || []; ?.formatStrings((f) => f.toSupported) || [];
categories.docs.formats = categories.docs.formats =
converters converters
.find((c) => c.name === "pandoc") .find((c) => c.name === "pandoc")
?.formatStrings((f) => f.toSupported) || []; ?.formatStrings((f) => f.toSupported) || [];

View File

@ -3,17 +3,17 @@ import { error, log } from "$lib/logger";
import { addToast } from "$lib/store/ToastProvider"; import { addToast } from "$lib/store/ToastProvider";
import type { OmitBetterStrict, WorkerMessage } from "$lib/types"; import type { OmitBetterStrict, WorkerMessage } from "$lib/types";
import { VertFile } from "$lib/types"; import { VertFile } from "$lib/types";
import VipsWorker from "$lib/workers/vips?worker&url"; import MagickWorker from "$lib/workers/magick?worker&url";
import { Converter, FormatInfo } from "./converter.svelte"; import { Converter, FormatInfo } from "./converter.svelte";
export class VipsConverter extends Converter { export class MagickConverter extends Converter {
private worker: Worker = browser private worker: Worker = browser
? new Worker(VipsWorker, { ? new Worker(MagickWorker, {
type: "module", type: "module",
}) })
: null!; : null!;
private id = 0; private id = 0;
public name = "libvips"; public name = "imagemagick";
public ready = $state(false); public ready = $state(false);
public supportedFormats = [ public supportedFormats = [
@ -39,7 +39,6 @@ export class VipsConverter extends Converter {
new FormatInfo("pgm", true, true), new FormatInfo("pgm", true, true),
new FormatInfo("pnm", true, true), new FormatInfo("pnm", true, true),
new FormatInfo("ppm", false, true), new FormatInfo("ppm", false, true),
new FormatInfo("raw", false, true),
new FormatInfo("tif", true, true), new FormatInfo("tif", true, true),
new FormatInfo("tiff", true, true), new FormatInfo("tiff", true, true),
new FormatInfo("jfif", true, true), new FormatInfo("jfif", true, true),
@ -51,7 +50,7 @@ export class VipsConverter extends Converter {
super(); super();
log(["converters", this.name], `created converter`); log(["converters", this.name], `created converter`);
if (!browser) return; if (!browser) return;
log(["converters", this.name], `loading worker @ ${VipsWorker}`); log(["converters", this.name], `loading worker @ ${MagickWorker}`);
this.worker.onmessage = (e) => { this.worker.onmessage = (e) => {
const message: WorkerMessage = e.data; const message: WorkerMessage = e.data;
log(["converters", this.name], `received message ${message.type}`); log(["converters", this.name], `received message ${message.type}`);
@ -64,7 +63,7 @@ export class VipsConverter extends Converter {
); );
addToast( addToast(
"error", "error",
`Error in VIPS worker, image conversion may not work as expected.`, `Error in Magick worker, image conversion may not work as expected.`,
); );
throw new Error(message.error); throw new Error(message.error);
} }

View File

@ -133,7 +133,7 @@
<h2 class="mt-2 -mb-2">Libraries</h2> <h2 class="mt-2 -mb-2">Libraries</h2>
<p class="font-normal"> <p class="font-normal">
A big thanks to FFmpeg (audio, video), libvips (images) and A big thanks to FFmpeg (audio, video), Imagemagick (images) and
Pandoc (documents) for maintaining such excellent libraries for Pandoc (documents) for maintaining such excellent libraries for
so many years. VERT relies on them to provide you with your so many years. VERT relies on them to provide you with your
conversions. conversions.

View File

@ -7,12 +7,27 @@
</script> </script>
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/state";
import {
PUB_DONATION_URL,
PUB_HOSTNAME,
PUB_STRIPE_KEY,
} from "$env/static/public";
// import { PUB_STRIPE_KEY, PUB_DONATION_API } from "$env/static/public"; // import { PUB_STRIPE_KEY, PUB_DONATION_API } from "$env/static/public";
import { fade } from "$lib/animation"; import { fade } from "$lib/animation";
import FancyInput from "$lib/components/functional/FancyInput.svelte"; import FancyInput from "$lib/components/functional/FancyInput.svelte";
import Panel from "$lib/components/visual/Panel.svelte"; import Panel from "$lib/components/visual/Panel.svelte";
import { effects } from "$lib/store/index.svelte"; import { effects } from "$lib/store/index.svelte";
import { addToast } from "$lib/store/ToastProvider"; import { addToast } from "$lib/store/ToastProvider";
import {
loadStripe,
type Stripe,
type StripeElements,
} from "@stripe/stripe-js";
import clsx from "clsx"; import clsx from "clsx";
import { import {
CalendarHeartIcon, CalendarHeartIcon,
@ -21,29 +36,47 @@
WalletIcon, WalletIcon,
} from "lucide-svelte"; } from "lucide-svelte";
import { onMount, tick } from "svelte"; import { onMount, tick } from "svelte";
import { Elements, PaymentElement } from "svelte-stripe";
import { quintOut } from "svelte/easing"; import { quintOut } from "svelte/easing";
type Props = {
donors: Donor[];
};
let { donors }: Props = $props();
let amount = $state(1); let amount = $state(1);
let customAmount = $state(""); let customAmount = $state("");
let type = $state("one-time"); let type = $state("one-time");
let stripe = $state<Stripe | null>(null);
const presetAmounts = [1, 10, 25]; const presetAmounts = [1, 10, 25];
let paying = $state(false); let paymentState = $state<"prepay" | "fetching" | "details">("prepay");
let enablePay = $state(false);
let clientSecret = $state<string | null>(null); let clientSecret = $state<string | null>(null);
let elements: StripeElements | null = $state(null);
const amountClick = (preset: number) => { const amountClick = (preset: number) => {
amount = preset; amount = preset;
customAmount = ""; customAmount = "";
}; };
const paymentClick = async () => {}; const paymentClick = async () => {
if (paymentState !== "prepay") return;
paymentState = "fetching";
const res = await fetch(`${PUB_DONATION_URL}/billing`, {
method: "POST",
body: (amount * 100).toString(),
});
if (!res.ok) {
paymentState = "prepay";
addToast(
"error",
"Error fetching payment details. Please try again later.",
);
return;
}
const { data }: { data: string } = await res.json();
clientSecret = data;
paymentState = "details";
};
$effect(() => { $effect(() => {
if (customAmount) { if (customAmount) {
@ -53,19 +86,73 @@
const payDuration = 400; const payDuration = 400;
const transition = "cubic-bezier(0.23, 1, 0.320, 1)"; const transition = "cubic-bezier(0.23, 1, 0.320, 1)";
</script>
{#snippet donor(name: string, amount: number | string, avatar: string)} onMount(async () => {
<div class="flex items-center bg-button rounded-full overflow-hidden"> stripe = await loadStripe(PUB_STRIPE_KEY);
<img });
src={avatar}
alt={name} const donate = async () => {
title={name} if (!stripe || !clientSecret || !elements) return;
class="w-9 h-9 rounded-full"
/> enablePay = false;
<p class="text-sm text-black dynadark:text-white px-4">${amount}</p>
</div> const submitResult = await elements.submit();
{/snippet} if (submitResult.error) {
addToast(
"error",
`Payment failed: ${submitResult.error.message}. You have not been charged.`,
);
enablePay = true;
return;
}
const res = await stripe.confirmPayment({
elements,
clientSecret,
redirect: "if_required",
confirmParams: {
return_url: page.url.toString(),
},
});
if (res.error) {
addToast(
"error",
`Payment failed: ${res.error.message}. You have not been charged.`,
);
} else {
addToast("success", "Thank you for your donation!");
}
paymentState = "prepay";
clientSecret = null;
elements = null;
amount = 1;
customAmount = "";
type = "one-time";
enablePay = false;
stripe = await loadStripe(PUB_STRIPE_KEY);
};
onMount(() => {
const status = page.url.searchParams.get("redirect_status");
if (status) {
switch (status) {
case "succeeded":
addToast("success", "Thank you for your donation!");
break;
default:
addToast(
"error",
"An error occurred while processing your donation. Please try again later.",
);
}
goto("/about");
}
});
</script>
<Panel class="flex flex-col gap-8 p-6"> <Panel class="flex flex-col gap-8 p-6">
<div class="flex flex-col gap-3"> <div class="flex flex-col gap-3">
@ -84,10 +171,10 @@
<div <div
class="flex flex-col gap-3 w-full overflow-visible" class="flex flex-col gap-3 w-full overflow-visible"
style="height: {paying ? 0 : 124}px; style="height: {paymentState !== 'prepay' ? 0 : 124}px;
transform: scaleY({paying ? 0 : 1}); transform: scaleY({paymentState !== 'prepay' ? 0 : 1});
opacity: {paying ? 0 : 1}; opacity: {paymentState !== 'prepay' ? 0 : 1};
filter: blur({paying ? 4 : 0}px); filter: blur({paymentState !== 'prepay' ? 4 : 0}px);
transition: height {payDuration}ms {transition}, transition: height {payDuration}ms {transition},
opacity {payDuration - 200}ms {transition}, opacity {payDuration - 200}ms {transition},
transform {payDuration}ms {transition}, transform {payDuration}ms {transition},
@ -162,52 +249,64 @@
class={clsx( class={clsx(
"btn flex-1 p-3 relative rounded-3xl bg-accent-red border-2 border-accent-red h-14 text-black", "btn flex-1 p-3 relative rounded-3xl bg-accent-red border-2 border-accent-red h-14 text-black",
{ {
"h-64 rounded-2xl bg-transparent cursor-auto !scale-100 -mt-10 -mb-2": "h-[450px] rounded-2xl bg-transparent cursor-auto !scale-100 -mt-10 -mb-2":
paying, paymentState !== "prepay",
"!scale-100": !$effects, "!scale-100": !$effects,
}, },
)} )}
style="transition: height {payDuration}ms {transition}, border-radius {payDuration}ms {transition}, background-color {payDuration}ms {transition}, transform {payDuration}ms {transition}, margin {payDuration}ms {transition};" style="transition: height {payDuration}ms {transition}, border-radius {payDuration}ms {transition}, background-color {payDuration}ms {transition}, transform {payDuration}ms {transition}, margin {payDuration}ms {transition}; will-change: height, border-radius, background-color, transform, margin;"
> >
<WalletIcon size="24" class="inline-block mr-2" /> <div class="grid grid-cols-1 grid-rows-1 w-full h-full">
Pay now {#if paymentState !== "prepay"}
</div> <div
</div> transition:fade={{
duration: payDuration,
easing: quintOut,
}}
class="row-start-1 col-start-1 flex w-full h-full flex-col gap-4"
>
<div
class="flex-grow max-h-full overflow-y-auto overflow-x-hidden"
>
{#if stripe && clientSecret}
<Elements {stripe} {clientSecret} bind:elements>
<PaymentElement
on:change={(e) => {
enablePay = e.detail.complete;
}}
/>
</Elements>
{/if}
</div>
<div class="flex flex-col gap-4"> <div class="flex-shrink-0">
<div class="flex flex-col gap-1"> <button
<h2 class="text-base font-bold">Our top donors</h2> disabled={!stripe ||
{#if donors && donors.length > 0} !clientSecret ||
<p class="text-base text-muted font-normal"> !enablePay}
People like these fuel the things we love to do. Thank you class="btn w-full h-12 bg-accent-red text-black rounded-full mt-4"
so much! onclick={donate}
</p> >
{:else} Donate ${amount} USD
<p class="text-base text-muted font-normal italic"> </button>
Seems like no one has donated yet... so if you do, you will </div>
pop up here! </div>
</p> {:else}
{/if} <!-- svelte-ignore a11y_click_events_have_key_events -->
</div> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div
{#if donors && donors.length > 0} transition:fade={{
<div class="flex flex-row flex-wrap gap-2"> duration: payDuration,
{#each donors as dono} easing: quintOut,
{@const { name, amount, avatar } = dono} }}
{@render donor(name, amount || "0.00", avatar)} onclick={paymentClick}
{/each} class="row-start-1 col-start-1 flex justify-center items-center"
>
<WalletIcon size="24" class="inline-block mr-2" />
Pay now
</div>
{/if}
</div> </div>
{/if} </div>
</div> </div>
</Panel> </Panel>
<style>
:global(
.StripeElement,
.StripeElement *,
iframe[name="__privateStripeFrame39314"]
) {
width: 50px !important;
height: 50px !important;
}
</style>

View File

@ -2,4 +2,4 @@ export { default as Credits } from "./Credits.svelte";
export { default as Donate } from "./Donate.svelte"; export { default as Donate } from "./Donate.svelte";
export { default as Resources } from "./Resources.svelte"; export { default as Resources } from "./Resources.svelte";
export { default as Why } from "./Why.svelte"; export { default as Why } from "./Why.svelte";
export { default as Vertd } from "./Vertd.svelte"; export { default as Sponsors } from "./Sponsors.svelte";

302
src/lib/workers/magick.ts Normal file
View File

@ -0,0 +1,302 @@
import {
ImageMagick,
initializeImageMagick,
MagickFormat,
MagickImage,
MagickImageCollection,
MagickReadSettings,
type IMagickImage,
} from "@imagemagick/magick-wasm";
import { makeZip } from "client-zip";
import wasm from "@imagemagick/magick-wasm/magick.wasm?url";
import { parseAni } from "$lib/parse/ani";
import { parseIcns } from "vert-wasm";
const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url));
magickPromise
.then(() => {
postMessage({ type: "loaded" });
})
.catch((error) => {
postMessage({ type: "error", error });
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleMessage = async (message: any): Promise<any> => {
switch (message.type) {
case "convert": {
if (!message.to.startsWith(".")) message.to = `.${message.to}`;
message.to = message.to.toLowerCase();
if (message.to === ".jfif") {
message.to = ".jpeg";
}
if (message.input.from === ".jfif") {
message.input.from = ".jpeg";
}
const buffer = await message.input.file.arrayBuffer();
// only wait when we need to
await magickPromise;
// special ico handling to split them all into separate images
if (message.input.from === ".ico") {
const imgs = MagickImageCollection.create();
while (true) {
try {
const img = MagickImage.create(
new Uint8Array(buffer),
new MagickReadSettings({
format: MagickFormat.Ico,
frameIndex: imgs.length,
}),
);
imgs.push(img);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
break;
}
}
if (imgs.length === 0) {
return {
type: "error",
error: `Failed to read ICO -- no images found inside?`,
};
}
const convertedImgs: Uint8Array[] = [];
await Promise.all(
imgs.map(async (img, i) => {
const output = await magickConvert(img, message.to);
convertedImgs[i] = output;
}),
);
const zip = makeZip(
convertedImgs.map(
(img, i) =>
new File([img], `image${i}.${message.to.slice(1)}`),
),
"images.zip",
);
// read the ReadableStream to the end
const zipBytes = await readToEnd(zip.getReader());
imgs.dispose();
return {
type: "finished",
output: zipBytes,
zip: true,
};
} else if (message.input.from === ".ani") {
console.log("Parsing ANI file");
try {
const parsedAni = parseAni(new Uint8Array(buffer));
const files: File[] = [];
await Promise.all(
parsedAni.images.map(async (img, i) => {
const blob = await magickConvert(
MagickImage.create(
img,
new MagickReadSettings({
format: MagickFormat.Ico,
}),
),
message.to,
);
files.push(
new File([blob], `image${i}${message.to}`),
);
}),
);
const zip = makeZip(files, "images.zip");
const zipBytes = await readToEnd(zip.getReader());
return {
type: "finished",
output: zipBytes,
zip: true,
};
} catch (e) {
console.error(e);
}
} else if (message.input.from === ".icns") {
const icns: Uint8Array[] = parseIcns(new Uint8Array(buffer));
if (typeof icns === "string") {
return {
type: "error",
error: `Failed to read ICNS -- ${icns}`,
};
}
const formats = [
MagickFormat.Png,
MagickFormat.Jpeg,
MagickFormat.Rgba,
MagickFormat.Rgb,
];
const outputs: Uint8Array[] = [];
for (const file of icns) {
for (const format of formats) {
try {
const img = MagickImage.create(
file,
new MagickReadSettings({
format: format,
}),
);
const converted = await magickConvert(
img,
message.to,
);
outputs.push(converted);
break;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
continue;
}
}
}
const zip = makeZip(
outputs.map(
(img, i) =>
new File([img], `image${i}.${message.to.slice(1)}`),
),
"images.zip",
);
const zipBytes = await readToEnd(zip.getReader());
return {
type: "finished",
output: zipBytes,
zip: true,
};
}
const img = MagickImage.create(
new Uint8Array(buffer),
new MagickReadSettings({
format: message.input.from
.slice(1)
.toUpperCase() as MagickFormat,
}),
);
const converted = await magickConvert(img, message.to);
return {
type: "finished",
output: converted,
};
}
}
};
const readToEnd = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
const chunks: Uint8Array[] = [];
let done = false;
while (!done) {
const { value, done: d } = await reader.read();
if (value) chunks.push(value);
done = d;
}
const blob = new Blob(chunks, { type: "application/zip" });
const arrayBuffer = await blob.arrayBuffer();
return new Uint8Array(arrayBuffer);
};
const magickToBlob = async (img: IMagickImage): Promise<Blob> => {
const canvas = new OffscreenCanvas(img.width, img.height);
return new Promise<Blob>((resolve, reject) =>
img.getPixels(async (p) => {
// const area = p.getArea(0, 0, img.width, img.height);
// const chunkSize = img.hasAlpha ? 4 : 3;
// const chunks = Math.ceil(area.length / chunkSize);
// const data = new Uint8ClampedArray(chunks * 4);
// for (let j = 0, k = 0; j < area.length; j += chunkSize, k += 4) {
// data[k] = area[j];
// data[k + 1] = area[j + 1];
// data[k + 2] = area[j + 2];
// data[k + 3] = img.hasAlpha ? area[j + 3] : 255;
// }
// const ctx = canvas.getContext("2d");
// if (!ctx) {
// reject(new Error("Failed to get canvas context"));
// return;
// }
// console.log(img.width, img.height);
// console.log(data.length, img.width * img.height * 4);
// ctx.putImageData(new ImageData(data, img.width, img.height), 0, 0);
// const blob = await canvas.convertToBlob({
// type: "image/png",
// });
const data = p.toByteArray(0, 0, img.width, img.height, "RGBA");
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(new Error("Failed to get canvas context"));
return;
}
const imageData = new ImageData(
new Uint8ClampedArray(data?.buffer || new ArrayBuffer(0)),
img.width,
img.height,
);
ctx.putImageData(imageData, 0, 0);
const blob = await canvas.convertToBlob({
type: "image/png",
});
resolve(blob);
}),
);
};
const magickConvert = async (img: IMagickImage, to: string) => {
const intermediary = await magickToBlob(img);
const buf = new Uint8Array(await intermediary.arrayBuffer());
let fmt = to.slice(1).toUpperCase();
if (fmt === "JFIF") fmt = "JPEG";
const result = await new Promise<Uint8Array>((resolve) => {
ImageMagick.read(buf, MagickFormat.Png, (image) => {
image.write(fmt as unknown as MagickFormat, (o) => {
resolve(structuredClone(o));
});
});
});
return result;
};
onmessage = async (e) => {
const message = e.data;
try {
const res = await handleMessage(message);
if (!res) return;
postMessage({
...res,
id: message.id,
});
} catch (e) {
postMessage({
type: "error",
error: e,
id: message.id,
});
}
};

View File

@ -1,331 +0,0 @@
import Vips from "wasm-vips";
import {
initializeImageMagick,
MagickFormat,
MagickImage,
MagickImageCollection,
MagickReadSettings,
type IMagickImage,
} from "@imagemagick/magick-wasm";
import { makeZip } from "client-zip";
import wasm from "@imagemagick/magick-wasm/magick.wasm?url";
import { parseAni } from "$lib/parse/ani";
import { parseIcns } from "vert-wasm";
const vipsPromise = Vips({
dynamicLibraries: [],
});
const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url));
const magickRequiredFormats = [
".dng",
".heic",
".ico",
".cur",
".ani",
".cr2",
".nef",
".bmp",
];
const unsupportedFrom: string[] = [];
const unsupportedTo = [...magickRequiredFormats];
vipsPromise
.then(() => {
postMessage({ type: "loaded" });
})
.catch((error) => {
postMessage({ type: "error", error });
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const handleMessage = async (message: any): Promise<any> => {
const vips = await vipsPromise;
switch (message.type) {
case "convert": {
if (!message.to.startsWith(".")) message.to = `.${message.to}`;
console.log(message);
if (unsupportedFrom.includes(message.input.from)) {
return {
type: "error",
error: `Unsupported input format ${message.input.from}`,
};
}
if (unsupportedTo.includes(message.to)) {
return {
type: "error",
error: `Unsupported output format ${message.to}`,
};
}
const buffer = await message.input.file.arrayBuffer();
if (
magickRequiredFormats.includes(message.input.from) ||
magickRequiredFormats.includes(message.to)
) {
// only wait when we need to
await magickPromise;
// special ico handling to split them all into separate images
if (message.input.from === ".ico") {
const imgs = MagickImageCollection.create();
while (true) {
try {
const img = MagickImage.create(
new Uint8Array(buffer),
new MagickReadSettings({
format: MagickFormat.Ico,
frameIndex: imgs.length,
}),
);
imgs.push(img);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
break;
}
}
if (imgs.length === 0) {
return {
type: "error",
error: `Failed to read ICO -- no images found inside?`,
};
}
const convertedImgs: Uint8Array[] = [];
await Promise.all(
imgs.map(async (img, i) => {
const output = await magickConvert(img, message.to);
convertedImgs[i] = output;
}),
);
const zip = makeZip(
convertedImgs.map(
(img, i) =>
new File(
[img],
`image${i}.${message.to.slice(1)}`,
),
),
"images.zip",
);
// read the ReadableStream to the end
const zipBytes = await readToEnd(zip.getReader());
imgs.dispose();
return {
type: "finished",
output: zipBytes,
zip: true,
};
} else if (message.input.from === ".ani") {
console.log("Parsing ANI file");
try {
const parsedAni = parseAni(new Uint8Array(buffer));
const files: File[] = [];
await Promise.all(
parsedAni.images.map(async (img, i) => {
const blob = await magickConvert(
MagickImage.create(
img,
new MagickReadSettings({
format: MagickFormat.Ico,
}),
),
message.to,
);
files.push(
new File([blob], `image${i}${message.to}`),
);
}),
);
const zip = makeZip(files, "images.zip");
const zipBytes = await readToEnd(zip.getReader());
return {
type: "finished",
output: zipBytes,
zip: true,
};
} catch (e) {
console.error(e);
}
}
const img = MagickImage.create(
new Uint8Array(buffer),
new MagickReadSettings({
format: message.input.from
.slice(1)
.toUpperCase() as MagickFormat,
}),
);
const converted = await magickConvert(img, message.to);
return {
type: "finished",
output: converted,
};
}
if (message.input.from === ".icns") {
const icns: Uint8Array[] = parseIcns(new Uint8Array(buffer));
// Result<T> in vert-wasm maps to a string in JS
if (typeof icns === "string") {
return {
type: "error",
error: `Failed to read ICNS -- ${icns}`,
};
}
const formats = [
MagickFormat.Png,
MagickFormat.Jpeg,
MagickFormat.Rgba,
MagickFormat.Rgb,
];
const outputs: Uint8Array[] = [];
for (const file of icns) {
for (const format of formats) {
try {
const img = MagickImage.create(
file,
new MagickReadSettings({
format: format,
}),
);
const converted = await magickConvert(
img,
message.to,
);
outputs.push(converted);
break;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
continue;
}
}
}
const zip = makeZip(
outputs.map(
(img, i) =>
new File([img], `image${i}.${message.to.slice(1)}`),
),
"images.zip",
);
const zipBytes = await readToEnd(zip.getReader());
return {
type: "finished",
output: zipBytes,
zip: true,
};
}
let image = vips.Image.newFromBuffer(buffer);
// check if animated image & keep it animated when converting
if (image.getTypeof("n-pages") > 0) {
image = vips.Image.newFromBuffer(buffer, "[n=-1]");
}
const opts: { [key: string]: string } = {};
if (typeof message.compression !== "undefined") {
opts["Q"] = Math.min(100, message.compression + 1).toString();
}
const output = image.writeToBuffer(message.to, opts);
image.delete();
return {
type: "finished",
output: output.buffer,
};
}
}
};
const readToEnd = async (reader: ReadableStreamDefaultReader<Uint8Array>) => {
const chunks: Uint8Array[] = [];
let done = false;
while (!done) {
const { value, done: d } = await reader.read();
if (value) chunks.push(value);
done = d;
}
const blob = new Blob(chunks, { type: "application/zip" });
const arrayBuffer = await blob.arrayBuffer();
return new Uint8Array(arrayBuffer);
};
const magickToBlob = async (img: IMagickImage): Promise<Blob> => {
const canvas = new OffscreenCanvas(img.width, img.height);
return new Promise<Blob>((resolve, reject) =>
img.getPixels(async (p) => {
const area = p.getArea(0, 0, img.width, img.height);
const chunkSize = img.hasAlpha ? 4 : 3;
const chunks = Math.ceil(area.length / chunkSize);
const data = new Uint8ClampedArray(chunks * 4);
for (let j = 0, k = 0; j < area.length; j += chunkSize, k += 4) {
data[k] = area[j];
data[k + 1] = area[j + 1];
data[k + 2] = area[j + 2];
data[k + 3] = img.hasAlpha ? area[j + 3] : 255;
}
const ctx = canvas.getContext("2d");
if (!ctx) {
reject(new Error("Failed to get canvas context"));
return;
}
ctx.putImageData(new ImageData(data, img.width, img.height), 0, 0);
const blob = await canvas.convertToBlob({
type: "image/png",
});
resolve(blob);
}),
);
};
const magickConvert = async (img: IMagickImage, to: string) => {
const vips = await vipsPromise;
const intermediary = await magickToBlob(img);
const buf = await intermediary.arrayBuffer();
const imgVips = vips.Image.newFromBuffer(buf);
const output = imgVips.writeToBuffer(to);
imgVips.delete();
img.dispose();
return output;
};
onmessage = async (e) => {
const message = e.data;
try {
const res = await handleMessage(message);
if (!res) return;
postMessage({
...res,
id: message.id,
});
} catch (e) {
postMessage({
type: "error",
error: e,
id: message.id,
});
}
};

View File

@ -2,7 +2,11 @@
import { onMount } from "svelte"; import { onMount } from "svelte";
import { goto, beforeNavigate, afterNavigate } from "$app/navigation"; import { goto, beforeNavigate, afterNavigate } from "$app/navigation";
import { PUB_PLAUSIBLE_URL, PUB_HOSTNAME } from "$env/static/public"; import {
PUB_PLAUSIBLE_URL,
PUB_HOSTNAME,
PUB_DONATION_URL,
} from "$env/static/public";
import { VERT_NAME } from "$lib/consts"; import { VERT_NAME } from "$lib/consts";
import * as Layout from "$lib/components/layout"; import * as Layout from "$lib/components/layout";
import * as Navbar from "$lib/components/layout/Navbar"; import * as Navbar from "$lib/components/layout/Navbar";

View File

@ -23,8 +23,10 @@
}; };
} = $derived({ } = $derived({
Images: { Images: {
ready: converters.find((c) => c.name === "libvips")?.ready || false, ready:
formats: getSupportedFormats("libvips"), converters.find((c) => c.name === "imagemagick")?.ready ||
false,
formats: getSupportedFormats("imagemagick"),
icon: Image, icon: Image,
}, },
Audio: { Audio: {
@ -133,7 +135,7 @@
<b>Status: </b> <b>Status: </b>
{s.ready ? "ready" : "not ready"} {s.ready ? "ready" : "not ready"}
</p> </p>
<p> <div>
<span class="flex flex-wrap justify-center"> <span class="flex flex-wrap justify-center">
<b>Supported formats:&nbsp;</b> <b>Supported formats:&nbsp;</b>
{#each s.formats.split(", ") as format, index} {#each s.formats.split(", ") as format, index}
@ -161,7 +163,7 @@
</span> </span>
{/each} {/each}
</span> </span>
</p> </div>
</div> </div>
</div> </div>
{/each} {/each}

View File

@ -10,6 +10,8 @@
import avatarAzurejelly from "$lib/assets/avatars/azurejelly.jpg"; import avatarAzurejelly from "$lib/assets/avatars/azurejelly.jpg";
import { GITHUB_API_URL } from "$lib/consts"; import { GITHUB_API_URL } from "$lib/consts";
import { addToast } from "$lib/store/ToastProvider"; import { addToast } from "$lib/store/ToastProvider";
import { dev } from "$app/environment";
import { page } from "$app/state";
// import { dev } from "$app/environment"; // import { dev } from "$app/environment";
// import { page } from "$app/state"; // import { page } from "$app/state";
@ -61,7 +63,7 @@
github: "https://github.com/RealmyTheMan", github: "https://github.com/RealmyTheMan",
role: "Former co-founder & designer", role: "Former co-founder & designer",
avatar: avatarRealmy, avatar: avatarRealmy,
} },
]; ];
let ghContribs: Contributor[] = []; let ghContribs: Contributor[] = [];
@ -127,8 +129,7 @@
} }
}); });
// const donationsEnabled = dev || page.url.origin.endsWith("//vert.sh"); const donationsEnabled = dev || page.url.origin.endsWith("//vert.sh");
const donationsEnabled = false;
</script> </script>
<div class="flex flex-col h-full items-center"> <div class="flex flex-col h-full items-center">
@ -142,22 +143,17 @@
> >
<!-- Why VERT? & Credits --> <!-- Why VERT? & Credits -->
<div class="flex flex-col gap-4 flex-1"> <div class="flex flex-col gap-4 flex-1">
<!-- {#if donationsEnabled} {#if donationsEnabled}
<About.Donate donors={[]} /> <About.Donate />
{/if} -->
<About.Why />
{#if !donationsEnabled}
<About.Vertd />
{/if} {/if}
<About.Why />
<About.Sponsors />
</div> </div>
<!-- Resources & Donate to VERT --> <!-- Resources & Donate to VERT -->
<div class="flex flex-col gap-4 flex-1"> <div class="flex flex-col gap-4 flex-1">
<About.Resources /> <About.Resources />
<About.Credits {mainContribs} {notableContribs} {ghContribs} /> <About.Credits {mainContribs} {notableContribs} {ghContribs} />
{#if donationsEnabled}
<About.Vertd />
{/if}
</div> </div>
</div> </div>
</div> </div>

View File

@ -47,14 +47,6 @@
const handleSelect = (option: string, file: VertFile) => { const handleSelect = (option: string, file: VertFile) => {
file.result = null; file.result = null;
switch (option) {
case ".webp":
case ".gif":
addToast(
"warning",
`Converting this file to "${option}" may take some time if animated.`,
);
}
}; };
$effect(() => { $effect(() => {
@ -119,7 +111,7 @@
?.formatStrings((f) => f.fromSupported) ?.formatStrings((f) => f.fromSupported)
.includes(file.from)} .includes(file.from)}
{@const isImage = converters {@const isImage = converters
.find((c) => c.name === "libvips") .find((c) => c.name === "imagemagick")
?.formatStrings((f) => f.fromSupported) ?.formatStrings((f) => f.fromSupported)
.includes(file.from)} .includes(file.from)}
{@const isDocument = converters {@const isDocument = converters

View File

@ -8,7 +8,7 @@
const images = $derived( const images = $derived(
files.files.filter((f) => files.files.filter((f) =>
f.converters.map((c) => c.name).includes("libvips"), f.converters.map((c) => c.name).includes("imagemagick"),
), ),
); );

View File

@ -7,19 +7,6 @@ import wasm from "vite-plugin-wasm";
export default defineConfig(({ command }) => { export default defineConfig(({ command }) => {
const plugins: PluginOption[] = [ const plugins: PluginOption[] = [
sveltekit(), sveltekit(),
{
name: "vips-request-middleware",
configureServer(server) {
server.middlewares.use((_req, res, next) => {
res.setHeader(
"Cross-Origin-Embedder-Policy",
"require-corp",
);
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
next();
});
},
},
svg({ svg({
includePaths: ["./src/lib/assets"], includePaths: ["./src/lib/assets"],
svgoOptions: { svgoOptions: {
@ -39,10 +26,6 @@ export default defineConfig(({ command }) => {
src: "_headers", src: "_headers",
dest: "", dest: "",
}, },
{
src: "node_modules/wasm-vips/lib/vips-*.wasm",
dest: "_app/immutable/workers",
},
], ],
}), }),
]; ];
@ -58,12 +41,7 @@ export default defineConfig(({ command }) => {
format: "es", format: "es",
}, },
optimizeDeps: { optimizeDeps: {
exclude: [ exclude: ["@ffmpeg/core-mt", "@ffmpeg/ffmpeg", "@ffmpeg/util"],
"wasm-vips",
"@ffmpeg/core-mt",
"@ffmpeg/ffmpeg",
"@ffmpeg/util",
],
}, },
css: { css: {
preprocessorOptions: { preprocessorOptions: {