mirror of https://github.com/VERT-sh/VERT.git
feat: donations
This commit is contained in:
parent
3f3c280241
commit
859760aaf7
|
@ -2,3 +2,7 @@ PUB_HOSTNAME=localhost:5173 # only gets used for plausible (for now)
|
|||
PUB_PLAUSIBLE_URL=https://plausible.example.com # can be empty
|
||||
PUB_ENV=development # "production", "development", or "nightly"
|
||||
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
|
4
_headers
4
_headers
|
@ -1,4 +0,0 @@
|
|||
# For libvips/wasm-vips converter (images)
|
||||
/*
|
||||
Cross-Origin-Embedder-Policy: require-corp
|
||||
Cross-Origin-Opener-Policy: same-origin
|
9
bun.lock
9
bun.lock
|
@ -11,6 +11,7 @@
|
|||
"@fontsource/lexend": "^5.1.2",
|
||||
"@fontsource/radio-canada-big": "^5.1.1",
|
||||
"@imagemagick/magick-wasm": "^0.0.34",
|
||||
"@stripe/stripe-js": "^7.4.0",
|
||||
"byte-data": "^19.0.1",
|
||||
"client-zip": "^2.4.6",
|
||||
"clsx": "^2.1.1",
|
||||
|
@ -18,10 +19,10 @@
|
|||
"music-metadata": "^11.0.0",
|
||||
"p-queue": "^8.1.0",
|
||||
"riff-file": "^1.0.3",
|
||||
"svelte-stripe": "^1.4.0",
|
||||
"vert-wasm": "^0.0.2",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"vite-plugin-wasm": "^3.4.1",
|
||||
"wasm-vips": "^0.0.11",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@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=="],
|
||||
|
||||
"@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/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-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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
|
|
@ -8,6 +8,8 @@ services:
|
|||
- PUB_ENV=${PUB_ENV:-production}
|
||||
- PORT=${PORT:-3000}
|
||||
- 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:
|
||||
context: .
|
||||
args:
|
||||
|
@ -15,6 +17,9 @@ services:
|
|||
PUB_PLAUSIBLE_URL: ${PUB_PLAUSIBLE_URL:-https://plausible.example.com}
|
||||
PUB_ENV: ${PUB_ENV:-production}
|
||||
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
|
||||
ports:
|
||||
- ${PORT:-3000}:80
|
||||
|
|
|
@ -12,8 +12,4 @@ server {
|
|||
}
|
||||
|
||||
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";
|
||||
}
|
|
@ -42,6 +42,7 @@
|
|||
"@fontsource/lexend": "^5.1.2",
|
||||
"@fontsource/radio-canada-big": "^5.1.1",
|
||||
"@imagemagick/magick-wasm": "^0.0.34",
|
||||
"@stripe/stripe-js": "^7.4.0",
|
||||
"byte-data": "^19.0.1",
|
||||
"client-zip": "^2.4.6",
|
||||
"clsx": "^2.1.1",
|
||||
|
@ -49,9 +50,9 @@
|
|||
"music-metadata": "^11.0.0",
|
||||
"p-queue": "^8.1.0",
|
||||
"riff-file": "^1.0.3",
|
||||
"svelte-stripe": "^1.4.0",
|
||||
"vert-wasm": "^0.0.2",
|
||||
"vite-plugin-static-copy": "^2.2.0",
|
||||
"vite-plugin-wasm": "^3.4.1",
|
||||
"wasm-vips": "^0.0.11"
|
||||
"vite-plugin-wasm": "^3.4.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,8 +64,6 @@
|
|||
const maskImage = $derived(
|
||||
`linear-gradient(to top, transparent ${100 - at.current}%, black 100%)`,
|
||||
);
|
||||
|
||||
$inspect(colors);
|
||||
</script>
|
||||
|
||||
{#if page.url.pathname === "/"}
|
||||
|
|
|
@ -63,9 +63,6 @@
|
|||
let links = $state<HTMLAnchorElement[]>([]);
|
||||
let container = $state<HTMLDivElement>();
|
||||
let containerRect = $derived(container?.getBoundingClientRect());
|
||||
$effect(() => {
|
||||
$inspect(containerRect);
|
||||
});
|
||||
|
||||
const linkRects = $derived(links.map((l) => l.getBoundingClientRect()));
|
||||
|
||||
|
|
|
@ -2,10 +2,10 @@ import type { Categories } from "$lib/types";
|
|||
import { FFmpegConverter } from "./ffmpeg.svelte";
|
||||
import { PandocConverter } from "./pandoc.svelte";
|
||||
import { VertdConverter } from "./vertd.svelte";
|
||||
import { VipsConverter } from "./vips.svelte";
|
||||
import { MagickConverter } from "./magick.svelte";
|
||||
|
||||
export const converters = [
|
||||
new VipsConverter(),
|
||||
new MagickConverter(),
|
||||
new FFmpegConverter(),
|
||||
new VertdConverter(),
|
||||
new PandocConverter(),
|
||||
|
@ -37,7 +37,7 @@ categories.video.formats =
|
|||
?.formatStrings((f) => f.toSupported) || [];
|
||||
categories.image.formats =
|
||||
converters
|
||||
.find((c) => c.name === "libvips")
|
||||
.find((c) => c.name === "imagemagick")
|
||||
?.formatStrings((f) => f.toSupported) || [];
|
||||
categories.docs.formats =
|
||||
converters
|
||||
|
|
|
@ -3,17 +3,17 @@ import { error, log } from "$lib/logger";
|
|||
import { addToast } from "$lib/store/ToastProvider";
|
||||
import type { OmitBetterStrict, WorkerMessage } 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";
|
||||
|
||||
export class VipsConverter extends Converter {
|
||||
export class MagickConverter extends Converter {
|
||||
private worker: Worker = browser
|
||||
? new Worker(VipsWorker, {
|
||||
? new Worker(MagickWorker, {
|
||||
type: "module",
|
||||
})
|
||||
: null!;
|
||||
private id = 0;
|
||||
public name = "libvips";
|
||||
public name = "imagemagick";
|
||||
public ready = $state(false);
|
||||
|
||||
public supportedFormats = [
|
||||
|
@ -39,7 +39,6 @@ export class VipsConverter extends Converter {
|
|||
new FormatInfo("pgm", true, true),
|
||||
new FormatInfo("pnm", true, true),
|
||||
new FormatInfo("ppm", false, true),
|
||||
new FormatInfo("raw", false, true),
|
||||
new FormatInfo("tif", true, true),
|
||||
new FormatInfo("tiff", true, true),
|
||||
new FormatInfo("jfif", true, true),
|
||||
|
@ -51,7 +50,7 @@ export class VipsConverter extends Converter {
|
|||
super();
|
||||
log(["converters", this.name], `created converter`);
|
||||
if (!browser) return;
|
||||
log(["converters", this.name], `loading worker @ ${VipsWorker}`);
|
||||
log(["converters", this.name], `loading worker @ ${MagickWorker}`);
|
||||
this.worker.onmessage = (e) => {
|
||||
const message: WorkerMessage = e.data;
|
||||
log(["converters", this.name], `received message ${message.type}`);
|
||||
|
@ -64,7 +63,7 @@ export class VipsConverter extends Converter {
|
|||
);
|
||||
addToast(
|
||||
"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);
|
||||
}
|
|
@ -133,7 +133,7 @@
|
|||
|
||||
<h2 class="mt-2 -mb-2">Libraries</h2>
|
||||
<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
|
||||
so many years. VERT relies on them to provide you with your
|
||||
conversions.
|
||||
|
|
|
@ -7,12 +7,27 @@
|
|||
</script>
|
||||
|
||||
<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 { fade } from "$lib/animation";
|
||||
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,
|
||||
type StripeElements,
|
||||
} from "@stripe/stripe-js";
|
||||
import clsx from "clsx";
|
||||
import {
|
||||
CalendarHeartIcon,
|
||||
|
@ -21,29 +36,47 @@
|
|||
WalletIcon,
|
||||
} from "lucide-svelte";
|
||||
import { onMount, tick } from "svelte";
|
||||
import { Elements, PaymentElement } from "svelte-stripe";
|
||||
import { quintOut } from "svelte/easing";
|
||||
|
||||
type Props = {
|
||||
donors: Donor[];
|
||||
};
|
||||
|
||||
let { donors }: Props = $props();
|
||||
|
||||
let amount = $state(1);
|
||||
let customAmount = $state("");
|
||||
let type = $state("one-time");
|
||||
let stripe = $state<Stripe | null>(null);
|
||||
|
||||
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 elements: StripeElements | null = $state(null);
|
||||
|
||||
const amountClick = (preset: number) => {
|
||||
amount = preset;
|
||||
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(() => {
|
||||
if (customAmount) {
|
||||
|
@ -53,19 +86,73 @@
|
|||
|
||||
const payDuration = 400;
|
||||
const transition = "cubic-bezier(0.23, 1, 0.320, 1)";
|
||||
</script>
|
||||
|
||||
{#snippet donor(name: string, amount: number | string, avatar: string)}
|
||||
<div class="flex items-center bg-button rounded-full overflow-hidden">
|
||||
<img
|
||||
src={avatar}
|
||||
alt={name}
|
||||
title={name}
|
||||
class="w-9 h-9 rounded-full"
|
||||
/>
|
||||
<p class="text-sm text-black dynadark:text-white px-4">${amount}</p>
|
||||
</div>
|
||||
{/snippet}
|
||||
onMount(async () => {
|
||||
stripe = await loadStripe(PUB_STRIPE_KEY);
|
||||
});
|
||||
|
||||
const donate = async () => {
|
||||
if (!stripe || !clientSecret || !elements) return;
|
||||
|
||||
enablePay = false;
|
||||
|
||||
const submitResult = await elements.submit();
|
||||
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">
|
||||
<div class="flex flex-col gap-3">
|
||||
|
@ -84,10 +171,10 @@
|
|||
|
||||
<div
|
||||
class="flex flex-col gap-3 w-full overflow-visible"
|
||||
style="height: {paying ? 0 : 124}px;
|
||||
transform: scaleY({paying ? 0 : 1});
|
||||
opacity: {paying ? 0 : 1};
|
||||
filter: blur({paying ? 4 : 0}px);
|
||||
style="height: {paymentState !== 'prepay' ? 0 : 124}px;
|
||||
transform: scaleY({paymentState !== 'prepay' ? 0 : 1});
|
||||
opacity: {paymentState !== 'prepay' ? 0 : 1};
|
||||
filter: blur({paymentState !== 'prepay' ? 4 : 0}px);
|
||||
transition: height {payDuration}ms {transition},
|
||||
opacity {payDuration - 200}ms {transition},
|
||||
transform {payDuration}ms {transition},
|
||||
|
@ -162,52 +249,64 @@
|
|||
class={clsx(
|
||||
"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":
|
||||
paying,
|
||||
"h-[450px] rounded-2xl bg-transparent cursor-auto !scale-100 -mt-10 -mb-2":
|
||||
paymentState !== "prepay",
|
||||
"!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;"
|
||||
>
|
||||
<div class="grid grid-cols-1 grid-rows-1 w-full h-full">
|
||||
{#if paymentState !== "prepay"}
|
||||
<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-shrink-0">
|
||||
<button
|
||||
disabled={!stripe ||
|
||||
!clientSecret ||
|
||||
!enablePay}
|
||||
class="btn w-full h-12 bg-accent-red text-black rounded-full mt-4"
|
||||
onclick={donate}
|
||||
>
|
||||
Donate ${amount} USD
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
transition:fade={{
|
||||
duration: payDuration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
onclick={paymentClick}
|
||||
class="row-start-1 col-start-1 flex justify-center items-center"
|
||||
>
|
||||
<WalletIcon size="24" class="inline-block mr-2" />
|
||||
Pay now
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="text-base font-bold">Our top donors</h2>
|
||||
{#if donors && donors.length > 0}
|
||||
<p class="text-base text-muted font-normal">
|
||||
People like these fuel the things we love to do. Thank you
|
||||
so much!
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-base text-muted font-normal italic">
|
||||
Seems like no one has donated yet... so if you do, you will
|
||||
pop up here!
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if donors && donors.length > 0}
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
{#each donors as dono}
|
||||
{@const { name, amount, avatar } = dono}
|
||||
{@render donor(name, amount || "0.00", avatar)}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Panel>
|
||||
|
||||
<style>
|
||||
:global(
|
||||
.StripeElement,
|
||||
.StripeElement *,
|
||||
iframe[name="__privateStripeFrame39314"]
|
||||
) {
|
||||
width: 50px !important;
|
||||
height: 50px !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,4 +2,4 @@ export { default as Credits } from "./Credits.svelte";
|
|||
export { default as Donate } from "./Donate.svelte";
|
||||
export { default as Resources } from "./Resources.svelte";
|
||||
export { default as Why } from "./Why.svelte";
|
||||
export { default as Vertd } from "./Vertd.svelte";
|
||||
export { default as Sponsors } from "./Sponsors.svelte";
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
};
|
|
@ -2,7 +2,11 @@
|
|||
import { onMount } from "svelte";
|
||||
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 * as Layout from "$lib/components/layout";
|
||||
import * as Navbar from "$lib/components/layout/Navbar";
|
||||
|
|
|
@ -23,8 +23,10 @@
|
|||
};
|
||||
} = $derived({
|
||||
Images: {
|
||||
ready: converters.find((c) => c.name === "libvips")?.ready || false,
|
||||
formats: getSupportedFormats("libvips"),
|
||||
ready:
|
||||
converters.find((c) => c.name === "imagemagick")?.ready ||
|
||||
false,
|
||||
formats: getSupportedFormats("imagemagick"),
|
||||
icon: Image,
|
||||
},
|
||||
Audio: {
|
||||
|
@ -133,7 +135,7 @@
|
|||
<b>Status: </b>
|
||||
{s.ready ? "ready" : "not ready"}
|
||||
</p>
|
||||
<p>
|
||||
<div>
|
||||
<span class="flex flex-wrap justify-center">
|
||||
<b>Supported formats: </b>
|
||||
{#each s.formats.split(", ") as format, index}
|
||||
|
@ -161,7 +163,7 @@
|
|||
</span>
|
||||
{/each}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
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 { dev } from "$app/environment";
|
||||
// import { page } from "$app/state";
|
||||
|
||||
|
@ -61,7 +63,7 @@
|
|||
github: "https://github.com/RealmyTheMan",
|
||||
role: "Former co-founder & designer",
|
||||
avatar: avatarRealmy,
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
let ghContribs: Contributor[] = [];
|
||||
|
@ -127,8 +129,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
// const donationsEnabled = dev || page.url.origin.endsWith("//vert.sh");
|
||||
const donationsEnabled = false;
|
||||
const donationsEnabled = dev || page.url.origin.endsWith("//vert.sh");
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full items-center">
|
||||
|
@ -142,22 +143,17 @@
|
|||
>
|
||||
<!-- Why VERT? & Credits -->
|
||||
<div class="flex flex-col gap-4 flex-1">
|
||||
<!-- {#if donationsEnabled}
|
||||
<About.Donate donors={[]} />
|
||||
{/if} -->
|
||||
<About.Why />
|
||||
{#if !donationsEnabled}
|
||||
<About.Vertd />
|
||||
{#if donationsEnabled}
|
||||
<About.Donate />
|
||||
{/if}
|
||||
<About.Why />
|
||||
<About.Sponsors />
|
||||
</div>
|
||||
|
||||
<!-- Resources & Donate to VERT -->
|
||||
<div class="flex flex-col gap-4 flex-1">
|
||||
<About.Resources />
|
||||
<About.Credits {mainContribs} {notableContribs} {ghContribs} />
|
||||
{#if donationsEnabled}
|
||||
<About.Vertd />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -47,14 +47,6 @@
|
|||
|
||||
const handleSelect = (option: string, file: VertFile) => {
|
||||
file.result = null;
|
||||
switch (option) {
|
||||
case ".webp":
|
||||
case ".gif":
|
||||
addToast(
|
||||
"warning",
|
||||
`Converting this file to "${option}" may take some time if animated.`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
|
@ -119,7 +111,7 @@
|
|||
?.formatStrings((f) => f.fromSupported)
|
||||
.includes(file.from)}
|
||||
{@const isImage = converters
|
||||
.find((c) => c.name === "libvips")
|
||||
.find((c) => c.name === "imagemagick")
|
||||
?.formatStrings((f) => f.fromSupported)
|
||||
.includes(file.from)}
|
||||
{@const isDocument = converters
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
const images = $derived(
|
||||
files.files.filter((f) =>
|
||||
f.converters.map((c) => c.name).includes("libvips"),
|
||||
f.converters.map((c) => c.name).includes("imagemagick"),
|
||||
),
|
||||
);
|
||||
|
||||
|
|
|
@ -7,19 +7,6 @@ import wasm from "vite-plugin-wasm";
|
|||
export default defineConfig(({ command }) => {
|
||||
const plugins: PluginOption[] = [
|
||||
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({
|
||||
includePaths: ["./src/lib/assets"],
|
||||
svgoOptions: {
|
||||
|
@ -39,10 +26,6 @@ export default defineConfig(({ command }) => {
|
|||
src: "_headers",
|
||||
dest: "",
|
||||
},
|
||||
{
|
||||
src: "node_modules/wasm-vips/lib/vips-*.wasm",
|
||||
dest: "_app/immutable/workers",
|
||||
},
|
||||
],
|
||||
}),
|
||||
];
|
||||
|
@ -58,12 +41,7 @@ export default defineConfig(({ command }) => {
|
|||
format: "es",
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
"wasm-vips",
|
||||
"@ffmpeg/core-mt",
|
||||
"@ffmpeg/ffmpeg",
|
||||
"@ffmpeg/util",
|
||||
],
|
||||
exclude: ["@ffmpeg/core-mt", "@ffmpeg/ffmpeg", "@ffmpeg/util"],
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
|
|
Loading…
Reference in New Issue