Merge branch 'jovannmc/redesign' into nightly
49
package.json
|
@ -12,40 +12,39 @@
|
|||
"lint": "prettier --check . && eslint ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^3.0.0",
|
||||
"@sveltejs/kit": "^2.0.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
||||
"@types/eslint": "^9.6.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"@poppanator/sveltekit-svg": "^5.0.0",
|
||||
"@sveltejs/adapter-static": "^3.0.8",
|
||||
"@sveltejs/kit": "^2.16.0",
|
||||
"@sveltejs/vite-plugin-svelte": "^4.0.4",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/jsmediatags": "^3.9.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"eslint": "^9.7.0",
|
||||
"eslint": "^9.18.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-svelte": "^2.36.0",
|
||||
"globals": "^15.0.0",
|
||||
"prettier": "^3.3.2",
|
||||
"prettier-plugin-svelte": "^3.2.6",
|
||||
"prettier-plugin-tailwindcss": "^0.6.5",
|
||||
"sass": "^1.80.7",
|
||||
"svelte": "^5.0.0",
|
||||
"svelte-check": "^4.0.0",
|
||||
"tailwindcss": "^3.4.9",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript-eslint": "^8.0.0",
|
||||
"vite": "^5.0.3"
|
||||
"eslint-plugin-svelte": "^2.46.1",
|
||||
"globals": "^15.14.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prettier-plugin-svelte": "^3.3.3",
|
||||
"prettier-plugin-tailwindcss": "^0.6.10",
|
||||
"sass": "^1.83.4",
|
||||
"svelte": "^5.19.0",
|
||||
"svelte-check": "^4.1.4",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "^5.7.3",
|
||||
"typescript-eslint": "^8.20.0",
|
||||
"vite": "^5.4.11"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"@ffmpeg/util": "^0.12.1",
|
||||
"@fontsource/azeret-mono": "^5.1.0",
|
||||
"@fontsource/lexend": "^5.1.1",
|
||||
"@ffmpeg/ffmpeg": "^0.12.15",
|
||||
"@ffmpeg/util": "^0.12.2",
|
||||
"@fontsource/azeret-mono": "^5.1.1",
|
||||
"@fontsource/lexend": "^5.1.2",
|
||||
"@fontsource/radio-canada-big": "^5.1.1",
|
||||
"@imagemagick/magick-wasm": "^0.0.31",
|
||||
"client-zip": "^2.4.5",
|
||||
"client-zip": "^2.4.6",
|
||||
"clsx": "^2.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsmediatags": "^3.9.7",
|
||||
"lucide-svelte": "^0.456.0",
|
||||
"svelte-adapter-bun": "^0.5.2",
|
||||
"wasm-vips": "^0.0.11"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import "@poppanator/sveltekit-svg/dist/svg";
|
||||
|
||||
type EventPayload = {
|
||||
readonly n: string;
|
||||
readonly u: Location["href"];
|
||||
|
|
38
src/app.html
|
@ -1,10 +1,44 @@
|
|||
<!doctype html>
|
||||
<html lang="en" class="%theme%">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
||||
<link rel="icon" href="%sveltekit.assets%/favicon.webp" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
%sveltekit.head%
|
||||
<script>
|
||||
(function () {
|
||||
// Apply theme before DOM is loaded
|
||||
let theme = localStorage.getItem("theme");
|
||||
const prefersDark = window.matchMedia(
|
||||
"(prefers-color-scheme: dark)",
|
||||
).matches;
|
||||
console.log(
|
||||
`Theme: ${theme || "N/A"}, prefers dark: ${prefersDark}`,
|
||||
);
|
||||
|
||||
if (theme !== "light" && theme !== "dark") {
|
||||
console.log("Invalid theme, setting to default");
|
||||
if (!theme) {
|
||||
console.log("First time visitor, setting theme");
|
||||
// first time visitor
|
||||
window.addEventListener("load", () => {
|
||||
window.plausible("Theme set", {
|
||||
props: {
|
||||
theme: prefersDark ? "dark" : "light",
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// invalid theme or first time visitor, set to default
|
||||
theme = prefersDark ? "dark" : "light";
|
||||
localStorage.setItem("theme", theme);
|
||||
}
|
||||
|
||||
console.log(`Applying theme: ${theme}`);
|
||||
document.documentElement.classList.add(theme);
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
|
|
219
src/app.scss
|
@ -2,15 +2,14 @@
|
|||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@import url(@fontsource/lexend/400.css);
|
||||
@import url(@fontsource/lexend/500.css);
|
||||
@import url(@fontsource/azeret-mono/600.css);
|
||||
@import url(@fontsource/radio-canada-big/600.css);
|
||||
@import url("$lib/assets/style/host-grotesk.css");
|
||||
|
||||
:root {
|
||||
--font-body: "Lexend", system-ui, -apple-system, BlinkMacSystemFont,
|
||||
--font-body: "Host Grotesk", system-ui, -apple-system, BlinkMacSystemFont,
|
||||
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
|
||||
"Helvetica Neue", sans-serif;
|
||||
--font-display: "Azeret Mono", var(--font-body);
|
||||
--font-display: "Radio Canada Big", var(--font-body);
|
||||
--transition: linear(
|
||||
0,
|
||||
0.006,
|
||||
|
@ -30,28 +29,165 @@
|
|||
}
|
||||
|
||||
@mixin light {
|
||||
--accent-bg: hsl(303, 73%, 81%);
|
||||
--accent-fg: hsl(0, 0, 10%);
|
||||
--bg: hsl(0, 0%, 100%);
|
||||
--bg-transparent: hsla(0, 0%, 100%, 0.6);
|
||||
--fg: hsl(0, 0%, 10%);
|
||||
--fg-muted: hsl(0, 0%, 50%);
|
||||
--fg-muted-alt: hsl(0, 0%, 75%);
|
||||
--fg-highlight: hsl(303, 61%, 47%);
|
||||
--fg-failure: hsl(0, 67%, 49%);
|
||||
color-scheme: light;
|
||||
// general
|
||||
--accent-pink: hsl(302, 100%, 76%);
|
||||
--accent-pink-alt: hsl(302, 100%, 50%);
|
||||
--accent-red: hsl(348, 100%, 80%);
|
||||
--accent-red-alt: hsl(348, 100%, 50%);
|
||||
--accent-purple: hsl(264, 100%, 81%);
|
||||
--accent-purple-alt: hsl(264, 100%, 50%);
|
||||
--accent-blue: hsl(220, 100%, 78%);
|
||||
--accent-blue-alt: hsl(220, 100%, 50%);
|
||||
--accent: var(--accent-pink);
|
||||
--accent-alt: var(--accent-pink-alt);
|
||||
|
||||
// foregrounds
|
||||
--fg: hsl(0, 0%, 0%);
|
||||
--fg-muted: hsla(0, 0%, 0%, 0.6);
|
||||
--fg-on-accent: hsl(0, 0%, 0%);
|
||||
--fg-on-badge: hsl(0, 0%, 0%);
|
||||
|
||||
// backgrounds
|
||||
--bg: hsl(0, 0%, 95%);
|
||||
--bg-gradient: linear-gradient(
|
||||
to bottom,
|
||||
var(--accent-pink),
|
||||
hsla(303, 100%, 50%, 0) 100%
|
||||
);
|
||||
--bg-gradient-pink: linear-gradient(
|
||||
to bottom,
|
||||
var(--accent-pink),
|
||||
hsla(303, 100%, 50%, 0) 25%
|
||||
);
|
||||
--bg-gradient-pink-alt: linear-gradient(
|
||||
to top,
|
||||
var(--accent-pink),
|
||||
hsl(303, 100%, 91%) 100%
|
||||
);
|
||||
--bg-gradient-red: linear-gradient(
|
||||
to bottom,
|
||||
var(--accent-red),
|
||||
hsla(348, 100%, 50%, 0) 25%
|
||||
);
|
||||
--bg-gradient-red-alt: linear-gradient(
|
||||
to top,
|
||||
var(--accent-red),
|
||||
hsl(348, 100%, 91%) 100%
|
||||
);
|
||||
--bg-gradient-purple: linear-gradient(
|
||||
to bottom,
|
||||
var(--accent-purple),
|
||||
hsla(264, 100%, 50%, 0) 25%
|
||||
);
|
||||
--bg-gradient-purple-alt: linear-gradient(
|
||||
to top,
|
||||
var(--accent-purple),
|
||||
hsl(264, 100%, 91%) 100%
|
||||
);
|
||||
--bg-gradient-blue: linear-gradient(
|
||||
to bottom,
|
||||
var(--accent-blue),
|
||||
hsla(220, 100%, 50%, 0) 25%
|
||||
);
|
||||
--bg-gradient-blue-alt: linear-gradient(
|
||||
to top,
|
||||
var(--accent-blue),
|
||||
hsl(220, 100%, 91%) 100%
|
||||
);
|
||||
--bg-gradient-image: linear-gradient(
|
||||
to bottom,
|
||||
hsla(0, 0%, 95%, 0.5),
|
||||
hsla(0, 0%, 95%, 1) 100%
|
||||
);
|
||||
--bg-panel: hsl(0, 0%, 100%);
|
||||
--bg-panel-accented: hsl(0, 0%, 92%);
|
||||
--bg-separator: hsl(0, 0%, 88%);
|
||||
--bg-button: var(--bg-panel-accented);
|
||||
--bg-badge: var(--accent-pink);
|
||||
--bg-input: #e0e0e0;
|
||||
|
||||
--shadow-panel: 0 2px 4px 0 hsla(0, 0%, 0%, 0.15);
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
--accent-bg: hsl(304, 41%, 21%);
|
||||
--accent-fg: hsl(303, 73%, 81%);
|
||||
--bg: hsl(0, 0%, 8%);
|
||||
--bg-transparent: hsla(0, 0%, 8%, 0.8);
|
||||
--fg: hsl(0, 0%, 90%);
|
||||
--fg-muted: hsl(0, 0%, 50%);
|
||||
--fg-muted-alt: hsl(0, 0%, 25%);
|
||||
--fg-highlight: hsl(303, 64%, 65%);
|
||||
--fg-failure: hsl(0, 67%, 80%);
|
||||
// general
|
||||
--accent-pink: hsl(302, 100%, 76%);
|
||||
--accent-pink-alt: hsl(302, 100%, 50%);
|
||||
--accent-red: hsl(348, 100%, 80%);
|
||||
--accent-red-alt: hsl(348, 100%, 50%);
|
||||
--accent-purple: hsl(264, 100%, 81%);
|
||||
--accent-purple-alt: hsl(264, 100%, 50%);
|
||||
--accent-blue: hsl(220, 100%, 78%);
|
||||
--accent-blue-alt: hsl(220, 100%, 50%);
|
||||
--accent: var(--accent-pink);
|
||||
--accent-alt: var(--accent-pink-alt);
|
||||
|
||||
// foregrounds
|
||||
--fg: hsl(0, 0%, 100%);
|
||||
--fg-muted: hsla(0, 0%, 100%, 0.6);
|
||||
--fg-on-accent: hsl(0, 0%, 0%);
|
||||
--fg-on-badge: hsl(0, 0%, 0%);
|
||||
|
||||
// backgrounds
|
||||
--bg: hsl(220, 5%, 12%);
|
||||
--bg-gradient: linear-gradient(
|
||||
to bottom,
|
||||
hsla(303, 100%, 50%, 0.1),
|
||||
hsla(303, 100%, 50%, 0) 100%
|
||||
);
|
||||
--bg-gradient-pink: linear-gradient(
|
||||
to bottom,
|
||||
hsla(303, 100%, 50%, 0.1),
|
||||
hsla(303, 100%, 50%, 0) 25%
|
||||
);
|
||||
--bg-gradient-pink-alt: linear-gradient(
|
||||
to top,
|
||||
var(--accent-pink),
|
||||
hsl(303, 100%, 91%) 100%
|
||||
);
|
||||
--bg-gradient-red: linear-gradient(
|
||||
to bottom,
|
||||
hsla(348, 100%, 50%, 0.1),
|
||||
hsla(348, 100%, 50%, 0) 25%
|
||||
);
|
||||
--bg-gradient-red-alt: linear-gradient(
|
||||
to top,
|
||||
var(--accent-red),
|
||||
hsl(348, 100%, 91%) 100%
|
||||
);
|
||||
--bg-gradient-purple: linear-gradient(
|
||||
to bottom,
|
||||
hsla(264, 100%, 50%, 0.1),
|
||||
hsla(264, 100%, 50%, 0) 25%
|
||||
);
|
||||
--bg-gradient-purple-alt: linear-gradient(
|
||||
to top,
|
||||
var(--accent-purple),
|
||||
hsl(264, 100%, 91%) 100%
|
||||
);
|
||||
--bg-gradient-blue: linear-gradient(
|
||||
to bottom,
|
||||
hsla(220, 100%, 50%, 0.1),
|
||||
hsla(220, 100%, 50%, 0) 25%
|
||||
);
|
||||
--bg-gradient-blue-alt: linear-gradient(
|
||||
to top,
|
||||
var(--accent-blue),
|
||||
hsl(220, 100%, 91%) 100%
|
||||
);
|
||||
--bg-gradient-image: linear-gradient(
|
||||
to bottom,
|
||||
hsla(220, 5%, 12%, 0.5),
|
||||
hsla(220, 5%, 12%, 1) 100%
|
||||
);
|
||||
--bg-panel: hsl(220, 4%, 18%);
|
||||
--bg-panel-accented: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
--bg-separator: hsl(220, 4%, 28%);
|
||||
--bg-button: hsl(220, 6%, 28%);
|
||||
--bg-badge: var(--accent-pink);
|
||||
|
||||
--shadow-panel: 0 4px 6px 0 hsla(0, 0%, 0%, 0.15);
|
||||
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
|
@ -76,8 +212,26 @@
|
|||
}
|
||||
|
||||
body {
|
||||
@apply text-foreground bg-background font-body overflow-x-hidden;
|
||||
@apply text-foreground font-body font-semibold overflow-x-hidden;
|
||||
width: 100vw;
|
||||
background-color: var(--bg);
|
||||
background-size: 100vw 100vh;
|
||||
}
|
||||
|
||||
.hoverable {
|
||||
@apply hover:scale-105 duration-200;
|
||||
}
|
||||
|
||||
.hoverable-md {
|
||||
@apply hover:scale-110 duration-200;
|
||||
}
|
||||
|
||||
.hoverable-lg {
|
||||
@apply hover:scale-[1.15] duration-200;
|
||||
}
|
||||
|
||||
.selected {
|
||||
@apply bg-accent-purple !text-black;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
|
@ -86,14 +240,23 @@ body {
|
|||
}
|
||||
|
||||
.btn {
|
||||
@apply font-display flex items-center justify-center overflow-hidden relative cursor-pointer px-4 border-2 border-solid bg-background border-foreground-muted-alt rounded-xl p-2 focus:!outline-none hover:scale-105 duration-200 active:scale-95 disabled:opacity-50 disabled:pointer-events-none;
|
||||
@apply bg-button flex items-center justify-center overflow-hidden relative cursor-pointer px-6 h-14 rounded-full font-medium focus:!outline-none hover:scale-105 duration-200 active:scale-95 disabled:opacity-50 disabled:pointer-events-none hoverable;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-highlight {
|
||||
@apply bg-accent-background text-accent-foreground border-accent-background;
|
||||
.btn.highlight {
|
||||
@apply bg-accent text-on-accent;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-display font-semibold;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
import type { Handle } from "@sveltejs/kit";
|
||||
|
||||
export const handle: Handle = async ({ event, resolve }) => {
|
||||
let theme = event.cookies.get("theme") ?? "";
|
||||
if (theme !== "dark" && theme !== "light") {
|
||||
event.cookies.set("theme", "", {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
expires: new Date(2147483647 * 1000),
|
||||
});
|
||||
theme = "";
|
||||
}
|
||||
const res = await resolve(event, {
|
||||
transformPageChunk: ({ html }) => html.replace("%theme%", theme),
|
||||
});
|
||||
return res;
|
||||
};
|
|
@ -1,135 +1,41 @@
|
|||
import type { EasingFunction, TransitionConfig } from "svelte/transition";
|
||||
import { isMobile, motion } from "$lib/store/index.svelte";
|
||||
import type { AnimationConfig, FlipParams } from "svelte/animate";
|
||||
import { cubicOut } from "svelte/easing";
|
||||
import {
|
||||
fade as svelteFade,
|
||||
fly as svelteFly,
|
||||
type FadeParams,
|
||||
type FlyParams,
|
||||
} from "svelte/transition";
|
||||
|
||||
// Subscribe to stores
|
||||
let motionEnabled = true;
|
||||
let isMobileDevice = false;
|
||||
|
||||
motion.subscribe(value => {
|
||||
motionEnabled = value;
|
||||
});
|
||||
|
||||
isMobile.subscribe(value => {
|
||||
isMobileDevice = value;
|
||||
});
|
||||
|
||||
export const transition =
|
||||
"linear(0,0.006,0.025 2.8%,0.101 6.1%,0.539 18.9%,0.721 25.3%,0.849 31.5%,0.937 38.1%,0.968 41.8%,0.991 45.7%,1.006 50.1%,1.015 55%,1.017 63.9%,1.001)";
|
||||
|
||||
export const duration = 500;
|
||||
|
||||
const remap = (
|
||||
value: number,
|
||||
low1: number,
|
||||
high1: number,
|
||||
low2: number,
|
||||
high2: number,
|
||||
) => low2 + ((high2 - low2) * (value - low1)) / (high1 - low1);
|
||||
export function fade(node: HTMLElement, options: FadeParams) {
|
||||
if (!motionEnabled) return {};
|
||||
const animation = svelteFade(node, options);
|
||||
return animation;
|
||||
}
|
||||
|
||||
const choose = (
|
||||
direction: "in" | "out" | "both",
|
||||
defaultValue: number,
|
||||
inValue?: number,
|
||||
outValue?: number,
|
||||
) =>
|
||||
direction !== "out"
|
||||
? typeof inValue === "number"
|
||||
? inValue
|
||||
: defaultValue
|
||||
: typeof outValue === "number"
|
||||
? outValue
|
||||
: defaultValue;
|
||||
|
||||
type Combination<T extends string, U extends string> = `${T} ${U}`;
|
||||
|
||||
export const blur = (
|
||||
_: HTMLElement,
|
||||
config:
|
||||
| Partial<{
|
||||
blurMultiplier: number;
|
||||
duration: number;
|
||||
easing: EasingFunction;
|
||||
scale: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
x: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
y: {
|
||||
start: number;
|
||||
end: number;
|
||||
};
|
||||
delay: number;
|
||||
opacity: boolean;
|
||||
origin: Combination<
|
||||
"top" | "bottom" | "left" | "right" | "center",
|
||||
"top" | "bottom" | "left" | "right" | "center"
|
||||
> & {};
|
||||
}>
|
||||
| undefined,
|
||||
dir: {
|
||||
direction: "in" | "out" | "both";
|
||||
},
|
||||
): TransitionConfig => {
|
||||
const prefersReducedMotion = window.matchMedia(
|
||||
"(prefers-reduced-motion: reduce)",
|
||||
).matches;
|
||||
if (typeof config?.opacity === "undefined" && config) config.opacity = true;
|
||||
const isUsingTranslate = !!config?.x || !!config?.y || !!config?.scale;
|
||||
return {
|
||||
delay: config?.delay || 0,
|
||||
duration: prefersReducedMotion ? 0 : config?.duration || 300,
|
||||
css: (t) => {
|
||||
if (prefersReducedMotion) return "";
|
||||
const translate = isUsingTranslate
|
||||
? `translate(${remap(
|
||||
t,
|
||||
0,
|
||||
1,
|
||||
choose(
|
||||
dir.direction,
|
||||
0,
|
||||
config?.x?.start,
|
||||
config?.x?.end,
|
||||
),
|
||||
choose(
|
||||
dir.direction,
|
||||
0,
|
||||
config?.x?.end,
|
||||
config?.x?.start,
|
||||
),
|
||||
)}px, ${remap(
|
||||
t,
|
||||
0,
|
||||
1,
|
||||
choose(
|
||||
dir.direction,
|
||||
0,
|
||||
config?.y?.start,
|
||||
config?.y?.end,
|
||||
),
|
||||
choose(
|
||||
dir.direction,
|
||||
0,
|
||||
config?.y?.end,
|
||||
config?.y?.start,
|
||||
),
|
||||
)}px) scale(${remap(
|
||||
t,
|
||||
0,
|
||||
1,
|
||||
choose(
|
||||
dir.direction,
|
||||
0.9,
|
||||
config?.scale?.start,
|
||||
config?.scale?.end,
|
||||
),
|
||||
choose(
|
||||
dir.direction,
|
||||
1,
|
||||
config?.scale?.end,
|
||||
config?.scale?.start,
|
||||
),
|
||||
)})`
|
||||
: ``;
|
||||
return `filter: blur(${(1 - t) * (config?.blurMultiplier || 1)}px); opacity: ${config?.opacity ? t : 1}; transform: ${
|
||||
translate
|
||||
}; ${config?.origin ? `transform-origin: ${config.origin};` : ""}`;
|
||||
},
|
||||
easing: config?.easing,
|
||||
};
|
||||
};
|
||||
export function fly(node: HTMLElement, options: FlyParams) {
|
||||
if (!motionEnabled || isMobileDevice) return {};
|
||||
const animation = svelteFly(node, options);
|
||||
return animation;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||
export function is_function(thing: unknown): thing is Function {
|
||||
|
@ -165,7 +71,7 @@ export function flip(
|
|||
? duration(Math.sqrt(dx * dx + dy * dy))
|
||||
: duration,
|
||||
easing,
|
||||
css: (t, u) => {
|
||||
css: (_t, u) => {
|
||||
const x = u * dx;
|
||||
const y = u * dy;
|
||||
// const sx = scale ? t + (u * from.width) / to.width : 1;
|
||||
|
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 27 KiB |
After Width: | Height: | Size: 30 KiB |
|
@ -0,0 +1,42 @@
|
|||
@font-face {
|
||||
font-family: "Host Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: url("$lib/assets/font/HostGrotesk-Regular.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Host Grotesk";
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: url("$lib/assets/font/HostGrotesk-Italic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Host Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 500;
|
||||
src: url("$lib/assets/font/HostGrotesk-Medium.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Host Grotesk";
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
src: url("$lib/assets/font/HostGrotesk-MediumItalic.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Host Grotesk";
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url("$lib/assets/font/HostGrotesk-SemiBold.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Host Grotesk";
|
||||
font-style: italic;
|
||||
font-weight: 600;
|
||||
src: url("$lib/assets/font/HostGrotesk-SemiBoldItalic.woff2")
|
||||
format("woff2");
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg width="100%" height="100%" viewBox="0 0 1279 1307" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||
<g transform="matrix(2.71986,0,0,2.71986,-2526.51,-3527.95)">
|
||||
<path d="M1082.77,1777.46L928.913,1297.11L1043.78,1297.11L1191.37,1777.46L1082.77,1777.46ZM1083.57,1776.36L1189.87,1776.36C1181.08,1747.74 1042.96,1298.21 1042.96,1298.21L930.425,1298.21L1083.57,1776.36ZM1188.94,1620.03L1285.35,1297.11L1398.82,1297.11L1261.8,1724.91L1188.94,1620.03ZM1190.15,1619.84L1261.43,1722.45L1397.31,1298.21L1286.17,1298.21L1190.15,1619.84Z"/>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 905 B |
|
@ -0,0 +1,45 @@
|
|||
<script lang="ts">
|
||||
import { files } from "$lib/store/index.svelte";
|
||||
import { FolderArchiveIcon, RefreshCw } from "lucide-svelte";
|
||||
import Panel from "../visual/Panel.svelte";
|
||||
import Dropdown from "./Dropdown.svelte";
|
||||
</script>
|
||||
|
||||
<Panel
|
||||
class="w-full h-auto flex items-center justify-between flex-col md:flex-row gap-4"
|
||||
>
|
||||
<div class="flex items-center flex-col md:flex-row gap-2.5 max-md:w-full">
|
||||
<button
|
||||
onclick={() => files.convertAll()}
|
||||
class="btn highlight flex gap-3 max-md:w-full"
|
||||
disabled={!files.ready}
|
||||
>
|
||||
<RefreshCw size="24" />
|
||||
<p>Convert all</p>
|
||||
</button>
|
||||
<button
|
||||
class="btn flex gap-3 max-md:w-full"
|
||||
disabled={!files.ready || !files.results}
|
||||
onclick={() => files.downloadAll()}
|
||||
>
|
||||
<FolderArchiveIcon size="24" />
|
||||
<p>Download all as .zip</p>
|
||||
</button>
|
||||
</div>
|
||||
<div class="w-full bg-separator h-0.5 flex md:hidden"></div>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="whitespace-nowrap text-xl">Set all to</p>
|
||||
{#if files.requiredConverters.length === 1}
|
||||
<Dropdown
|
||||
onselect={(r) =>
|
||||
files.files.forEach((f) => {
|
||||
f.to = r;
|
||||
f.result = null;
|
||||
})}
|
||||
options={files.files[0]?.converter?.supportedFormats || []}
|
||||
/>
|
||||
{:else}
|
||||
<Dropdown options={["N/A"]} disabled />
|
||||
{/if}
|
||||
</div>
|
||||
</Panel>
|
|
@ -1,17 +1,22 @@
|
|||
<script lang="ts">
|
||||
import { blur, duration, flip, transition } from "$lib/animation";
|
||||
import { duration, fade, transition } from "$lib/animation";
|
||||
import { ChevronDown } from "lucide-svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import { fade } from "svelte/transition";
|
||||
|
||||
type Props = {
|
||||
options: string[];
|
||||
selected?: string;
|
||||
onselect?: (option: string) => void;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
let { options, selected = $bindable(), onselect }: Props = $props();
|
||||
let {
|
||||
options,
|
||||
selected = $bindable(options[0]),
|
||||
onselect,
|
||||
disabled,
|
||||
}: Props = $props();
|
||||
|
||||
let open = $state(false);
|
||||
let hover = $state(false);
|
||||
|
@ -31,10 +36,6 @@
|
|||
toggle();
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
selected = selected || options[0];
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const click = (e: MouseEvent) => {
|
||||
if (dropdown && !dropdown.contains(e.target as Node)) {
|
||||
|
@ -47,46 +48,32 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="relative w-full min-w-fit" bind:this={dropdown}>
|
||||
<div
|
||||
class="relative w-full min-w-fit text-xl font-medium text-center"
|
||||
bind:this={dropdown}
|
||||
>
|
||||
<button
|
||||
class="font-display w-full min-w-fit justify-between overflow-hidden relative cursor-pointer px-3 border-2 border-solid flex items-center bg-background border-foreground-muted-alt rounded-xl p-2 focus:!outline-none"
|
||||
class="font-display w-full justify-center overflow-hidden relative cursor-pointer px-3 py-3.5 bg-button {disabled
|
||||
? 'opacity-50'
|
||||
: ''} flex items-center rounded-full focus:!outline-none"
|
||||
onclick={toggle}
|
||||
onmouseenter={() => (hover = true)}
|
||||
onmouseleave={() => (hover = false)}
|
||||
{disabled}
|
||||
>
|
||||
<!-- <p>{selected}</p> -->
|
||||
<div
|
||||
class="grid grid-cols-1 grid-rows-1 w-fit text-left flex-grow-0 pr-12"
|
||||
>
|
||||
<div class="grid grid-cols-1 grid-rows-1 w-fit flex-grow-0">
|
||||
{#key selected}
|
||||
<p
|
||||
in:blur={{
|
||||
in:fade={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
blurMultiplier: 6,
|
||||
scale: {
|
||||
start: 0.9,
|
||||
end: 1,
|
||||
},
|
||||
y: {
|
||||
start: isUp ? -50 : 50,
|
||||
end: 0,
|
||||
},
|
||||
}}
|
||||
out:blur={{
|
||||
out:fade={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
blurMultiplier: 6,
|
||||
scale: {
|
||||
start: 1,
|
||||
end: 0.9,
|
||||
},
|
||||
y: {
|
||||
start: 0,
|
||||
end: isUp ? 50 : -50,
|
||||
},
|
||||
}}
|
||||
class="col-start-1 row-start-1 text-left"
|
||||
class="col-start-1 row-start-1 text-center font-body font-medium"
|
||||
>
|
||||
{selected}
|
||||
</p>
|
||||
|
@ -108,26 +95,16 @@
|
|||
</button>
|
||||
{#if open}
|
||||
<div
|
||||
style={hover ? "will-change: opacity, blur, transform" : ""}
|
||||
transition:blur={{
|
||||
style={hover ? "will-change: opacity, fade, transform" : ""}
|
||||
transition:fade={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
blurMultiplier: 6,
|
||||
scale: {
|
||||
start: 0.9,
|
||||
end: 1,
|
||||
},
|
||||
y: {
|
||||
start: -10,
|
||||
end: 0,
|
||||
},
|
||||
origin: "top center",
|
||||
}}
|
||||
class="w-full shadow-xl shadow-black/25 absolute overflow-hidden top-full mt-1 left-0 z-50 bg-background border-2 border-solid border-foreground-muted-alt rounded-xl"
|
||||
class="w-full shadow-xl bg-panel-alt shadow-black/25 absolute overflow-hidden top-full mt-1 left-0 z-50 bg-background rounded-xl max-h-[30vh] overflow-y-auto"
|
||||
>
|
||||
{#each options as option}
|
||||
<button
|
||||
class="w-full p-2 px-4 text-left hover:bg-foreground-muted-alt brightness-125"
|
||||
class="w-full p-2 px-4 text-left hover:bg-panel"
|
||||
onclick={() => select(option)}
|
||||
>
|
||||
{option}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<script lang="ts">
|
||||
type Props = {
|
||||
class?: string;
|
||||
placeholder?: string;
|
||||
value?: string;
|
||||
disabled?: boolean;
|
||||
extension?: string;
|
||||
prefix?: string;
|
||||
type?: string;
|
||||
};
|
||||
|
||||
let {
|
||||
class: className,
|
||||
placeholder = "",
|
||||
value = $bindable(),
|
||||
disabled = false,
|
||||
extension,
|
||||
prefix,
|
||||
type = "text",
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="relative flex w-full {className}">
|
||||
<input
|
||||
{type}
|
||||
bind:value
|
||||
{placeholder}
|
||||
{disabled}
|
||||
class="w-full p-3 rounded-lg bg-panel border-2 border-button
|
||||
{prefix ? 'pl-[2rem]' : 'pl-3'}
|
||||
{extension ? 'pr-[4rem]' : 'pr-3'}"
|
||||
/>
|
||||
{#if prefix}
|
||||
<div class="absolute left-0 top-0 bottom-0 flex items-center px-2">
|
||||
<span class="text-sm text-gray-400 px-2 py-1 rounded"
|
||||
>{prefix}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if extension}
|
||||
<div class="absolute right-0 top-0 bottom-0 flex items-center px-4">
|
||||
<span class="text-sm bg-button text-black dynadark:text-white px-2 py-1 rounded"
|
||||
>{extension}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
|
@ -1,12 +1,11 @@
|
|||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import { page } from "$app/stores";
|
||||
import { fly } from "svelte/transition";
|
||||
import { duration } from "$lib/animation";
|
||||
import { duration, fly } from "$lib/animation";
|
||||
import clsx from "clsx";
|
||||
import { onMount, tick } from "svelte";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import type { Writable } from "svelte/store";
|
||||
import clsx from "clsx";
|
||||
import { browser } from "$app/environment";
|
||||
import { onMount, tick } from "svelte";
|
||||
|
||||
interface Props {
|
||||
links: {
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import { page } from "$app/state";
|
||||
import { duration, fade } from "$lib/animation";
|
||||
import { motion, setTheme } from "$lib/store/index.svelte";
|
||||
import clsx from "clsx";
|
||||
import { MoonIcon, SunIcon } from "lucide-svelte";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import Panel from "../visual/Panel.svelte";
|
||||
import Logo from "../visual/svg/Logo.svelte";
|
||||
|
||||
type Props = {
|
||||
items: {
|
||||
name: string;
|
||||
url: string;
|
||||
activeMatch: (pathname: string) => boolean;
|
||||
icon: any;
|
||||
badge?: number;
|
||||
}[];
|
||||
};
|
||||
|
||||
let { items }: Props = $props();
|
||||
|
||||
let links = $state<HTMLAnchorElement[]>([]);
|
||||
let container = $state<HTMLDivElement>();
|
||||
let containerRect = $derived(container?.getBoundingClientRect());
|
||||
$effect(() => {
|
||||
$inspect(containerRect);
|
||||
});
|
||||
|
||||
const linkRects = $derived(links.map((l) => l.getBoundingClientRect()));
|
||||
|
||||
const selectedIndex = $derived(
|
||||
items.findIndex((i) => i.activeMatch(page.url.pathname)),
|
||||
);
|
||||
</script>
|
||||
|
||||
{#snippet link(item: (typeof items)[0], index: number)}
|
||||
{@const Icon = item.icon}
|
||||
<a
|
||||
bind:this={links[index]}
|
||||
href={item.url}
|
||||
aria-label={item.name}
|
||||
class={clsx(
|
||||
"w-16 md:w-32 h-full relative z-10 rounded-xl flex items-center justify-center gap-3 overflow-hidden",
|
||||
{
|
||||
"bg-panel-accented":
|
||||
item.activeMatch(page.url.pathname) && !browser,
|
||||
},
|
||||
)}
|
||||
draggable={false}
|
||||
>
|
||||
<div class="grid grid-rows-1 grid-cols-1">
|
||||
{#key item.name}
|
||||
<div
|
||||
class="w-full row-start-1 col-start-1 h-full flex items-center justify-center gap-3"
|
||||
in:fade={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
out:fade={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
>
|
||||
<div class="relative">
|
||||
<Icon />
|
||||
{#if item.badge}
|
||||
<div
|
||||
class="absolute overflow-hidden grid grid-rows-1 grid-cols-1 -top-1 font-display -right-1 w-fit px-1.5 h-4 rounded-full bg-badge text-on-badge font-medium"
|
||||
style="font-size: 0.7rem;"
|
||||
transition:fade={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
>
|
||||
{#key item.badge}
|
||||
<div
|
||||
class="flex items-center justify-center w-full h-full col-start-1 row-start-1"
|
||||
in:fade={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
out:fade={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
>
|
||||
{item.badge}
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="font-medium hidden md:flex">
|
||||
{item.name}
|
||||
</p>
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
</a>
|
||||
{/snippet}
|
||||
|
||||
<div bind:this={container}>
|
||||
<Panel class="max-w-[778px] w-full h-20 flex items-center gap-3 relative">
|
||||
{#if linkRects[selectedIndex]}
|
||||
<div
|
||||
class="absolute bg-panel-accented rounded-xl"
|
||||
style="width: {linkRects[selectedIndex]
|
||||
.width}px; height: {linkRects[selectedIndex]
|
||||
.height}px; top: {linkRects[selectedIndex].top -
|
||||
(containerRect?.top || 0)}px; left: {linkRects[
|
||||
selectedIndex
|
||||
].left - (containerRect?.left || 0)}px; {$motion
|
||||
? `transition: left var(--transition) ${duration}ms, top var(--transition) ${duration}ms;`
|
||||
: ''}"
|
||||
></div>
|
||||
{/if}
|
||||
<a
|
||||
class="w-28 h-full bg-accent rounded-xl items-center justify-center hidden md:flex"
|
||||
href="/"
|
||||
>
|
||||
<div class="h-5 w-full">
|
||||
<Logo />
|
||||
</div>
|
||||
</a>
|
||||
{#each items as item, i (item.url)}
|
||||
{@render link(item, i)}
|
||||
{/each}
|
||||
<div class="w-0.5 bg-separator h-full hidden md:flex"></div>
|
||||
<button
|
||||
onclick={() => {
|
||||
const isDark =
|
||||
document.documentElement.classList.contains("dark");
|
||||
setTheme(isDark ? "light" : "dark");
|
||||
}}
|
||||
class="w-14 h-full items-center justify-center hidden md:flex"
|
||||
>
|
||||
<SunIcon class="dynadark:hidden block" />
|
||||
<MoonIcon class="dynadark:block hidden" />
|
||||
</button>
|
||||
</Panel>
|
||||
</div>
|
|
@ -1,121 +1,84 @@
|
|||
<script lang="ts">
|
||||
import { Upload } from "lucide-svelte";
|
||||
import { UploadIcon } from "lucide-svelte";
|
||||
import Panel from "../visual/Panel.svelte";
|
||||
import clsx from "clsx";
|
||||
import { onMount } from "svelte";
|
||||
import { files } from "$lib/store/index.svelte";
|
||||
import { converters } from "$lib/converters";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
let fileList = $state<FileList>();
|
||||
let dragBtn = $state<HTMLButtonElement>();
|
||||
type Props = {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
interface Props {
|
||||
files: File[] | undefined;
|
||||
onupload?: () => void;
|
||||
isMobile: boolean;
|
||||
acceptedFormats?: string[];
|
||||
}
|
||||
const { class: classList }: Props = $props();
|
||||
|
||||
$effect(() => {
|
||||
if (!fileList) return;
|
||||
files = Array.from(fileList);
|
||||
});
|
||||
let dropping = $state(false);
|
||||
let uploaderButton = $state<HTMLButtonElement>();
|
||||
|
||||
let fileInput = $state<HTMLInputElement>();
|
||||
let dragOver = $state(false);
|
||||
const dropFiles = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
dropping = false;
|
||||
const oldLength = files.files.length;
|
||||
files.add(e.dataTransfer?.files);
|
||||
if (oldLength !== files.files.length) goto("/convert");
|
||||
};
|
||||
|
||||
let { files = $bindable(), onupload, isMobile, acceptedFormats }: Props = $props();
|
||||
|
||||
function upload() {
|
||||
if (!fileInput) return;
|
||||
fileInput.click();
|
||||
}
|
||||
const uploadFiles = () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.accept = converters
|
||||
.map((c) => c.supportedFormats.join(","))
|
||||
.join(",");
|
||||
input.onchange = (e) => {
|
||||
const oldLength = files.files.length;
|
||||
files.add(input.files);
|
||||
if (oldLength !== files.files.length) goto("/convert");
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const handler = (e: Event) => e.preventDefault();
|
||||
if (!dragBtn) return;
|
||||
dragBtn.addEventListener("dragenter", handler);
|
||||
dragBtn.addEventListener("dragstart", handler);
|
||||
dragBtn.addEventListener("dragend", handler);
|
||||
dragBtn.addEventListener("dragleave", handler);
|
||||
dragBtn.addEventListener("dragover", handler);
|
||||
dragBtn.addEventListener("drag", handler);
|
||||
dragBtn.addEventListener("drop", handler);
|
||||
const handler = (e: Event) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
};
|
||||
|
||||
uploaderButton?.addEventListener("dragover", handler);
|
||||
uploaderButton?.addEventListener("dragenter", handler);
|
||||
uploaderButton?.addEventListener("dragleave", handler);
|
||||
uploaderButton?.addEventListener("drop", handler);
|
||||
|
||||
return () => {
|
||||
if (!dragBtn) return;
|
||||
dragBtn.removeEventListener("dragenter", handler);
|
||||
dragBtn.removeEventListener("dragstart", handler);
|
||||
dragBtn.removeEventListener("dragend", handler);
|
||||
dragBtn.removeEventListener("dragleave", handler);
|
||||
dragBtn.removeEventListener("dragover", handler);
|
||||
dragBtn.removeEventListener("drag", handler);
|
||||
dragBtn.removeEventListener("drop", handler);
|
||||
uploaderButton?.removeEventListener("dragover", handler);
|
||||
uploaderButton?.removeEventListener("dragenter", handler);
|
||||
uploaderButton?.removeEventListener("dragleave", handler);
|
||||
uploaderButton?.removeEventListener("drop", handler);
|
||||
};
|
||||
});
|
||||
|
||||
function drop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
dragOver = false;
|
||||
if (!event.dataTransfer) return;
|
||||
if (!files) files = Array.from(event.dataTransfer.files);
|
||||
else files.push(...Array.from(event.dataTransfer.files));
|
||||
onupload?.();
|
||||
return true;
|
||||
}
|
||||
|
||||
function addFiles() {
|
||||
if (!fileInput) return;
|
||||
if (!fileInput.files) return;
|
||||
if (!files) files = Array.from(fileInput.files);
|
||||
else files.push(...Array.from(fileInput.files));
|
||||
onupload?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button
|
||||
bind:this={dragBtn}
|
||||
onclick={upload}
|
||||
ondragover={() => (dragOver = true)}
|
||||
ondragleave={() => (dragOver = false)}
|
||||
class={clsx(
|
||||
"file-uploader",
|
||||
"w-full h-80 flex items-center justify-center cursor-pointer",
|
||||
"border-2 border-solid border-foreground-muted-alt rounded-2xl",
|
||||
"hover:scale-95 hover:opacity-70 transition-all duration-150 ease-out",
|
||||
{
|
||||
"scale-95 opacity-70 blur-xs": dragOver,
|
||||
},
|
||||
)}
|
||||
class:_drag-over={dragOver}
|
||||
ondrop={drop}
|
||||
ondragenter={() => (dropping = true)}
|
||||
ondragleave={() => (dropping = false)}
|
||||
ondrop={dropFiles}
|
||||
onclick={uploadFiles}
|
||||
bind:this={uploaderButton}
|
||||
class={clsx(`hover:scale-105 active:scale-100 duration-200 ${classList}`, {
|
||||
"scale-105": dropping,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
class="file-uploader-center flex items-center justify-center flex-col transition-all duration-150 ease-out px-8"
|
||||
<Panel
|
||||
class="flex justify-center items-center w-full h-full flex-col pointer-events-none"
|
||||
>
|
||||
<div
|
||||
class="size-16 rounded-full text-accent-foreground bg-accent-background flex items-center justify-center"
|
||||
class="w-16 h-16 bg-accent rounded-full flex items-center justify-center p-4"
|
||||
>
|
||||
<Upload class="size-8" />
|
||||
<UploadIcon class="w-full h-full text-on-accent" />
|
||||
</div>
|
||||
<h2 class="font-display text-2xl mt-6">
|
||||
{isMobile ? "Tap" : "Drop or click"} to upload files
|
||||
<h2 class="text-center text-2xl font-semibold mt-4">
|
||||
Drop or click to upload
|
||||
</h2>
|
||||
<p class="text-foreground-muted mt-4">
|
||||
All processing is done on your device. No file or size limit.
|
||||
</p>
|
||||
</div>
|
||||
</Panel>
|
||||
</button>
|
||||
|
||||
<input
|
||||
type="file"
|
||||
class="hidden"
|
||||
bind:this={fileInput}
|
||||
onchange={addFiles}
|
||||
accept={acceptedFormats?.join(",") ?? "*"}
|
||||
multiple
|
||||
/>
|
||||
|
||||
<style>
|
||||
.file-uploader:hover .file-uploader-center,
|
||||
.file-uploader._drag-over .file-uploader-center {
|
||||
@apply scale-105;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
type Props = {
|
||||
class: string;
|
||||
items: { [name: string]: string };
|
||||
};
|
||||
|
||||
const { class: classList, items }: Props = $props();
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
|
||||
const links = $derived(Object.entries(items));
|
||||
</script>
|
||||
|
||||
<footer
|
||||
class={classList}
|
||||
style="background: linear-gradient(to bottom, transparent, var(--bg) 100%)"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full flex items-center justify-center text-muted gap-3"
|
||||
>
|
||||
<p>© {year} VERT.</p>
|
||||
{#each links as [name, url] (name)}
|
||||
<!-- bullet point -->
|
||||
<p>•</p>
|
||||
<a
|
||||
class="hover:underline font-normal"
|
||||
href={url}
|
||||
target={url.startsWith("http") ? "_blank" : "_self"}
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
</footer>
|
|
@ -0,0 +1,14 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from "svelte";
|
||||
|
||||
type Props = {
|
||||
class?: string;
|
||||
children: Snippet<[]>;
|
||||
};
|
||||
|
||||
const { class: classList, children }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="bg-panel {classList} p-3 rounded-2.5xl shadow-panel">
|
||||
{@render children?.()}
|
||||
</div>
|
|
@ -12,11 +12,9 @@
|
|||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-full h-1 dynadark:bg-foreground-muted-alt bg-foreground-muted rounded-full overflow-hidden relative"
|
||||
>
|
||||
<div class="w-full h-1 bg-panel-alt rounded-full overflow-hidden relative">
|
||||
<div
|
||||
class="h-full bg-accent-background dynadark:bg-accent-foreground absolute left-0 top-0"
|
||||
class="h-full bg-accent absolute left-0 top-0"
|
||||
class:percentless-animation={progress === null}
|
||||
style={percent
|
||||
? `width: ${percent}%; transition: 500ms linear width;`
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
<script lang="ts">
|
||||
import { fade, fly } from "$lib/animation";
|
||||
import {
|
||||
BanIcon,
|
||||
CheckIcon,
|
||||
InfoIcon,
|
||||
TriangleAlert,
|
||||
XIcon,
|
||||
} from "lucide-svelte";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import { removeToast } from "$lib/store/ToastProvider";
|
||||
|
||||
type Props = {
|
||||
id: number;
|
||||
type: "success" | "error" | "info" | "warning";
|
||||
message: string;
|
||||
durations: {
|
||||
enter: number;
|
||||
stay: number;
|
||||
exit: number;
|
||||
};
|
||||
};
|
||||
|
||||
let { id, type, message, durations }: Props = $props();
|
||||
|
||||
const color = {
|
||||
success: "purple",
|
||||
error: "red",
|
||||
info: "blue",
|
||||
warning: "pink",
|
||||
}[type];
|
||||
|
||||
const Icon = {
|
||||
success: CheckIcon,
|
||||
error: BanIcon,
|
||||
info: InfoIcon,
|
||||
warning: TriangleAlert,
|
||||
}[type];
|
||||
|
||||
// intentionally unused. this is so tailwind can generate the css for these colours as it doesn't detect if it's dynamically loaded
|
||||
// this would lead to the colours not being generated in the final css file by tailwind
|
||||
const colourVariants = [
|
||||
"border-accent-pink-alt",
|
||||
"border-accent-red-alt",
|
||||
"border-accent-purple-alt",
|
||||
"border-accent-blue-alt",
|
||||
];
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="flex items-center justify-between w-full max-w-sm p-4 gap-4 bg-accent-{color} border-accent-{color}-alt border-l-4 rounded-lg shadow-md"
|
||||
in:fly={{
|
||||
duration: durations.enter,
|
||||
easing: quintOut,
|
||||
x: 0,
|
||||
y: 100,
|
||||
}}
|
||||
out:fade={{
|
||||
duration: durations.exit,
|
||||
easing: quintOut,
|
||||
}}
|
||||
>
|
||||
<div class="flex items-center gap-4">
|
||||
<Icon
|
||||
class="w-6 h-6 text-black flex-shrink-0"
|
||||
size="32"
|
||||
stroke="2"
|
||||
fill="none"
|
||||
/>
|
||||
<p class="text-black font-normal">{message}</p>
|
||||
</div>
|
||||
<button
|
||||
class="text-gray-600 hover:text-black"
|
||||
onclick={() => removeToast(id)}
|
||||
>
|
||||
<XIcon size="16" />
|
||||
</button>
|
||||
</div>
|
|
@ -1,27 +1,16 @@
|
|||
<svg
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox="0 0 404 96"
|
||||
style="
|
||||
fill-rule: evenodd;
|
||||
clip-rule: evenodd;
|
||||
stroke-linejoin: round;
|
||||
stroke-miterlimit: 2;
|
||||
"
|
||||
viewBox="0 0 300 83"
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
xml:space="preserve"
|
||||
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"
|
||||
>
|
||||
<path
|
||||
d="M37.166,96l27.291,0l37.303,-96l-27.017,0l-23.726,66.24l-23.588,-66.24l-27.429,0l37.166,96Z"
|
||||
style="fill-rule: nonzero"
|
||||
/>
|
||||
<path
|
||||
d="M194.057,96l0,-21.669l-58.56,0l0,-16.32l56.64,0l0,-21.668l-56.64,-0l0,-14.674l57.874,-0l0,-21.669l-82.011,-0l0,96l82.697,0Z"
|
||||
style="fill-rule: nonzero"
|
||||
/>
|
||||
<path
|
||||
d="M209.143,33.463l-0,-33.463l58.697,0c24.686,0 37.029,12.206 37.029,31.68c-0,16.869 -10.286,29.349 -31.132,30.994l33.189,33.326l-34.835,0l-62.948,-62.537Zm71.588,-0.686c0,-6.72 -6.308,-11.52 -15.634,-11.52l-31.817,0l0,23.452l31.817,-0c9.189,-0 15.634,-4.526 15.634,-11.932Zm-71.588,13.692l24.137,49.531l-24.137,0l-0,-49.531Z"
|
||||
/>
|
||||
<path
|
||||
d="M370.971,96l0,-74.194l32.915,-0l-0,-21.806l-89.966,0l0,21.806l32.914,-0l0,74.194l24.137,0Z"
|
||||
style="fill-rule: nonzero"
|
||||
/>
|
||||
<g transform="matrix(0.172257,0,0,0.172257,-160.012,-223.436)">
|
||||
<path
|
||||
d="M1082.77,1777.46L928.913,1297.11L1043.78,1297.11L1191.37,1777.46L1082.77,1777.46ZM1188.94,1620.03L1285.35,1297.11L1398.82,1297.11L1261.8,1724.91L1188.94,1620.03ZM1803.99,1777.46L1441.99,1777.46L1441.99,1297.11L1801.21,1297.11L1801.21,1398.05L1549.89,1398.05L1549.89,1485.77L1771.27,1485.77L1771.27,1581.14L1549.89,1581.14L1549.89,1676.52L1803.99,1676.52L1803.99,1777.46ZM1980.12,1615.25L1980.12,1777.46L1872.22,1777.46L1872.22,1297.11L2069.23,1297.11C2127.24,1297.11 2171.57,1311.49 2202.2,1340.27C2232.83,1369.04 2248.14,1407.57 2248.14,1455.83C2248.14,1504.1 2232.83,1542.74 2202.2,1571.74C2187.36,1585.8 2169.3,1596.44 2148.04,1603.69L2261.37,1777.46L2140.24,1777.46L2042.05,1615.25L1980.12,1615.25ZM1980.12,1398.05L1980.12,1514.31L2062.96,1514.31C2089.42,1514.31 2108.56,1509.32 2120.4,1499.34C2132.23,1489.36 2138.15,1474.86 2138.15,1455.83C2138.15,1436.8 2132.23,1422.42 2120.4,1412.67C2108.56,1402.92 2089.42,1398.05 2062.96,1398.05L1980.12,1398.05ZM2422.18,1398.05L2282.95,1398.05L2282.95,1297.11L2668.62,1297.11L2668.62,1398.05L2529.39,1398.05L2529.39,1777.46L2422.18,1777.46L2422.18,1398.05Z"
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 955 B After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,61 @@
|
|||
<svg
|
||||
width="1389"
|
||||
height="1080"
|
||||
viewBox="0 0 1389 1080"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M418.719 1080L0.480804 0H2.62554L420.863 1080H418.719Z"
|
||||
fill="url(#paint0_linear_6_220)"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<path
|
||||
d="M829.044 1080L412.359 0H410.215L826.9 1080H829.044Z"
|
||||
fill="url(#paint1_linear_6_220)"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<path
|
||||
d="M788.673 555.925L987.856 0H989.981L790.985 555.402L1064.61 827.169L1386.13 0H1388.27L1065.37 830.741L788.673 555.925Z"
|
||||
fill="url(#paint2_linear_6_220)"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_6_220"
|
||||
x1="694.377"
|
||||
y1="0"
|
||||
x2="694.377"
|
||||
y2="1080"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="0.75" stop-color="white" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_6_220"
|
||||
x1="694.377"
|
||||
y1="0"
|
||||
x2="694.377"
|
||||
y2="1080"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="0.75" stop-color="white" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_6_220"
|
||||
x1="694.377"
|
||||
y1="0"
|
||||
x2="694.377"
|
||||
y2="1080"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="0.75" stop-color="white" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -0,0 +1,4 @@
|
|||
export const GITHUB_URL = 'https://github.com/VERT-sh/VERT';
|
||||
export const GITHUB_API_URL = 'https://api.github.com/repos/VERT-sh/VERT';
|
||||
|
||||
export const DISCORD_URL = 'https://discord.gg/kqevGxYPak';
|
|
@ -1,9 +1,9 @@
|
|||
import { VertFile } from "$lib/types";
|
||||
import { Converter } from "./converter.svelte";
|
||||
import VipsWorker from "$lib/workers/vips?worker&url";
|
||||
import { browser } from "$app/environment";
|
||||
import type { WorkerMessage, OmitBetterStrict } from "$lib/types";
|
||||
import { log } from "$lib/logger";
|
||||
import { error, log } from "$lib/logger";
|
||||
import type { OmitBetterStrict, WorkerMessage } from "$lib/types";
|
||||
import { VertFile } from "$lib/types";
|
||||
import VipsWorker from "$lib/workers/vips?worker&url";
|
||||
import { Converter } from "./converter.svelte";
|
||||
|
||||
export class VipsConverter extends Converter {
|
||||
private worker: Worker = browser
|
||||
|
@ -44,7 +44,12 @@ export class VipsConverter extends Converter {
|
|||
this.worker.onmessage = (e) => {
|
||||
const message: WorkerMessage = e.data;
|
||||
log(["converters", this.name], `received message ${message.type}`);
|
||||
if (message.type === "loaded") this.ready = true;
|
||||
if (message.type === "loaded") {
|
||||
this.ready = true;
|
||||
} else if (message.type === "error") {
|
||||
error(["converters", this.name], `error in worker: ${message.error}`);
|
||||
throw new Error(message.error);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -103,7 +108,7 @@ export class VipsConverter extends Converter {
|
|||
try {
|
||||
this.worker.postMessage(msg);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
error(["converters", this.name], e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
const randomColorFromStr = (str: string) => {
|
||||
|
@ -20,7 +21,6 @@ const whiteOrBlack = (hsl: string) => {
|
|||
return l > 70 ? "black" : "white";
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const log = (prefix: string | string[], ...args: any[]) => {
|
||||
const prefixes = Array.isArray(prefix) ? prefix : [prefix];
|
||||
if (!browser)
|
||||
|
@ -40,3 +40,23 @@ export const log = (prefix: string | string[], ...args: any[]) => {
|
|||
...args,
|
||||
);
|
||||
};
|
||||
|
||||
export const error = (prefix: string | string[], ...args: any[]) => {
|
||||
const prefixes = Array.isArray(prefix) ? prefix : [prefix];
|
||||
if (!browser)
|
||||
return console.error(prefixes.map((p) => `[${p}]`).join(" "), ...args);
|
||||
const prefixesWithMeta = prefixes.map((p) => ({
|
||||
prefix: p,
|
||||
bgColor: randomColorFromStr(p),
|
||||
textColor: whiteOrBlack(randomColorFromStr(p)),
|
||||
}));
|
||||
|
||||
console.error(
|
||||
`%c${prefixesWithMeta.map(({ prefix }) => prefix).join(" %c")}`,
|
||||
...prefixesWithMeta.map(
|
||||
({ bgColor, textColor }, i) =>
|
||||
`color: ${textColor}; background-color: ${bgColor}; margin-left: ${i === 0 ? 0 : -6}px; padding: 0px 4px 0 4px; border-radius: 0px 9999px 9999px 0px;`,
|
||||
),
|
||||
...args,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
<script lang="ts">
|
||||
import Panel from "$lib/components/visual/Panel.svelte";
|
||||
import { HeartHandshakeIcon } from "lucide-svelte";
|
||||
import { GITHUB_URL } from "$lib/consts";
|
||||
|
||||
let { mainContribs, ghContribs } = $props();
|
||||
</script>
|
||||
|
||||
{#snippet contributor(
|
||||
name: string,
|
||||
github: string,
|
||||
avatar: string,
|
||||
role?: string,
|
||||
)}
|
||||
<div class="flex items-center gap-4">
|
||||
<a
|
||||
href={github}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="flex-shrink-0"
|
||||
>
|
||||
<img
|
||||
src={avatar}
|
||||
alt={name}
|
||||
title={name}
|
||||
class="{role
|
||||
? 'w-14 h-14 hoverable-md'
|
||||
: 'w-10 h-10 hoverable-lg'} rounded-full"
|
||||
/>
|
||||
</a>
|
||||
{#if role}
|
||||
<div class="flex flex-col gap-1">
|
||||
<p class="text-xl font-semibold">{name}</p>
|
||||
<p class="text-sm font-normal text-muted">{role}</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
<Panel class="flex flex-col gap-8 p-6">
|
||||
<h2 class="text-2xl font-bold flex items-center">
|
||||
<div class="rounded-full bg-blue-300 p-2 inline-block mr-3 w-10 h-10">
|
||||
<HeartHandshakeIcon color="black" />
|
||||
</div>
|
||||
Credits
|
||||
</h2>
|
||||
|
||||
<!-- Main contributors -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
{#each mainContribs as contrib}
|
||||
{@const { name, github, avatar, role } = contrib}
|
||||
{@render contributor(name, github, avatar, role)}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GitHub contributors -->
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-1">
|
||||
<h2 class="text-base font-bold">GitHub contributors</h2>
|
||||
{#if ghContribs && ghContribs.length > 0}
|
||||
<p class="text-base text-muted font-normal">
|
||||
Big thanks to all these people for helping out!
|
||||
<a
|
||||
class="text-blue-500 font-normal hover:underline"
|
||||
href={GITHUB_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Want to help too?
|
||||
</a>
|
||||
</p>
|
||||
{:else}
|
||||
<p class="text-base text-muted font-normal italic">
|
||||
Seems like no one has contributed yet...
|
||||
<a
|
||||
class="text-blue-500 font-normal hover:underline"
|
||||
href={GITHUB_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
be the first to contribute!
|
||||
</a>
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if ghContribs && ghContribs.length > 0}
|
||||
<div class="flex flex-row flex-wrap gap-2">
|
||||
{#each ghContribs as contrib}
|
||||
{@const { name, github, avatar } = contrib}
|
||||
{@render contributor(name, github, avatar)}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Panel>
|
|
@ -0,0 +1,121 @@
|
|||
<script lang="ts">
|
||||
import FancyInput from "$lib/components/functional/FancyInput.svelte";
|
||||
import Panel from "$lib/components/visual/Panel.svelte";
|
||||
import {
|
||||
CalendarHeartIcon,
|
||||
HandCoinsIcon,
|
||||
HeartIcon,
|
||||
WalletIcon,
|
||||
} from "lucide-svelte";
|
||||
|
||||
let { donors } = $props();
|
||||
</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}
|
||||
|
||||
<Panel class="flex flex-col gap-8 p-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<h2 class="text-2xl font-bold flex items-center">
|
||||
<div
|
||||
class="rounded-full bg-accent-red p-2 inline-block mr-3 w-10 h-10"
|
||||
>
|
||||
<HeartIcon color="black" />
|
||||
</div>
|
||||
Donate to VERT
|
||||
</h2>
|
||||
<p class="text-base font-normal">
|
||||
With your support, we can keep maintaining and improving VERT.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
<div class="flex gap-3 w-full">
|
||||
<button
|
||||
class="btn flex-1 p-4 rounded-lg bg-accent-red text-black flex items-center justify-center"
|
||||
>
|
||||
<HandCoinsIcon size="24" class="inline-block mr-2" />
|
||||
One-time
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="btn flex-1 p-4 rounded-lg bg-button text-black dynadark:text-white flex items-center justify-center"
|
||||
>
|
||||
<CalendarHeartIcon size="24" class="inline-block mr-2" />
|
||||
Monthly
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-3 w-full">
|
||||
<button class="btn bg-accent-red text-black p-4 rounded-lg flex-1"
|
||||
>$1 USD</button
|
||||
>
|
||||
<button
|
||||
class="btn bg-button text-black dynadark:text-white p-4 rounded-lg flex-1"
|
||||
>$5 USD</button
|
||||
>
|
||||
<button
|
||||
class="btn bg-button text-black dynadark:text-white p-4 rounded-lg flex-1"
|
||||
>$10 USD</button
|
||||
>
|
||||
<!-- <div class="relative flex items-center flex-[2]">
|
||||
<span class="absolute left-3 text-gray-500">$</span>
|
||||
<input
|
||||
type="number"
|
||||
class="pl-8 pr-2 rounded-lg border border-gray-300 dynadark:border-gray-500 w-full h-full bg-button text-black dynadark:text-white"
|
||||
placeholder="Custom"
|
||||
/>
|
||||
</div> -->
|
||||
<div class="flex-[2] flex items-center justify-center">
|
||||
<FancyInput placeholder="Custom" prefix="$" type="number" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-row justify-center w-full">
|
||||
<p class="text-muted text-sm flex-[4] flex items-center">
|
||||
Payments and subscription management <br /> are handled through Liberapay
|
||||
</p>
|
||||
|
||||
<button
|
||||
class="btn flex-1 p-3 rounded-3xl bg-accent-red text-black flex items-center justify-center"
|
||||
>
|
||||
<WalletIcon size="24" class="inline-block mr-2" />
|
||||
Pay now
|
||||
</button>
|
||||
</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, avatar)}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Panel>
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import Panel from "$lib/components/visual/Panel.svelte";
|
||||
import { DISCORD_URL, GITHUB_URL } from "$lib/consts";
|
||||
import { GithubIcon, LinkIcon, MessageCircleMoreIcon } from "lucide-svelte";
|
||||
</script>
|
||||
|
||||
<Panel class="flex flex-col gap-4 p-6">
|
||||
<h2 class="text-2xl font-bold flex items-center">
|
||||
<div
|
||||
class="rounded-full bg-accent-purple p-2 inline-block mr-3 w-10 h-10"
|
||||
>
|
||||
<LinkIcon color="black" />
|
||||
</div>
|
||||
Resources
|
||||
</h2>
|
||||
<div class="flex gap-3">
|
||||
<a
|
||||
href={DISCORD_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn flex-1 gap-2 p-4 rounded-full bg-button text-black dynadark:text-white flex items-center justify-center"
|
||||
>
|
||||
<MessageCircleMoreIcon size="24" class="inline-block mr-2" />
|
||||
Discord
|
||||
</a>
|
||||
<a
|
||||
href={GITHUB_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="btn flex-1 gap-2 p-4 rounded-full bg-button text-black dynadark:text-white flex items-center justify-center"
|
||||
>
|
||||
<GithubIcon size="24" class="inline-block mr-2" />
|
||||
Source
|
||||
</a>
|
||||
</div>
|
||||
</Panel>
|
|
@ -0,0 +1,25 @@
|
|||
<script lang="ts">
|
||||
import Panel from "$lib/components/visual/Panel.svelte";
|
||||
import { MessageCircleQuestionIcon } from "lucide-svelte";
|
||||
</script>
|
||||
|
||||
<Panel class="flex flex-col gap-3 p-6">
|
||||
<h2 class="text-2xl font-bold flex items-center">
|
||||
<div
|
||||
class="rounded-full bg-accent-pink p-2 inline-block mr-3 w-10 h-10"
|
||||
>
|
||||
<MessageCircleQuestionIcon color="black" />
|
||||
</div>
|
||||
Why VERT?
|
||||
</h2>
|
||||
<p class="text-lg font-normal">
|
||||
<b>File converters have always disappointed us.</b> They're ugly,
|
||||
riddled with ads, and most importantly; slow. We decided to solve this
|
||||
problem once and for all by making an alternative that solves all those
|
||||
problems, and more.<br />
|
||||
<br />
|
||||
All your files are converted completely on-device; this means that there's
|
||||
no delay between sending and receiving the files from a server, and we never
|
||||
get to snoop on the files you convert.
|
||||
</p>
|
||||
</Panel>
|
|
@ -0,0 +1,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";
|
|
@ -0,0 +1,132 @@
|
|||
<script lang="ts">
|
||||
import Panel from "$lib/components/visual/Panel.svelte";
|
||||
import {
|
||||
theme,
|
||||
motion,
|
||||
setMotion,
|
||||
setTheme,
|
||||
} from "$lib/store/index.svelte";
|
||||
import {
|
||||
MoonIcon,
|
||||
PaletteIcon,
|
||||
PauseIcon,
|
||||
PlayIcon,
|
||||
SunIcon,
|
||||
} from "lucide-svelte";
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
|
||||
let lightElement: HTMLButtonElement;
|
||||
let darkElement: HTMLButtonElement;
|
||||
let enableMotionElement: HTMLButtonElement;
|
||||
let disableMotionElement: HTMLButtonElement;
|
||||
|
||||
let motionUnsubscribe: () => void;
|
||||
let themeUnsubscribe: () => void;
|
||||
|
||||
const updateMotionClasses = (value: boolean) => {
|
||||
if (value) {
|
||||
enableMotionElement.classList.add("selected");
|
||||
disableMotionElement.classList.remove("selected");
|
||||
} else {
|
||||
disableMotionElement.classList.add("selected");
|
||||
enableMotionElement.classList.remove("selected");
|
||||
}
|
||||
};
|
||||
|
||||
const updateThemeClasses = (value: string) => {
|
||||
document.documentElement.classList.remove("light", "dark");
|
||||
document.documentElement.classList.add(value);
|
||||
|
||||
if (value === "dark") {
|
||||
darkElement.classList.add("selected");
|
||||
lightElement.classList.remove("selected");
|
||||
} else {
|
||||
lightElement.classList.add("selected");
|
||||
darkElement.classList.remove("selected");
|
||||
}
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
motionUnsubscribe = motion.subscribe(updateMotionClasses);
|
||||
themeUnsubscribe = theme.subscribe(updateThemeClasses);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
if (motionUnsubscribe) motionUnsubscribe();
|
||||
if (themeUnsubscribe) themeUnsubscribe();
|
||||
});
|
||||
</script>
|
||||
|
||||
<Panel class="flex flex-col gap-8 p-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<h2 class="text-2xl font-bold">
|
||||
<PaletteIcon
|
||||
size="40"
|
||||
class="inline-block -mt-1 mr-2 bg-accent-purple p-2 rounded-full"
|
||||
color="black"
|
||||
/>
|
||||
Appearance
|
||||
</h2>
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-base font-bold">Brightness theme</p>
|
||||
<p class="text-sm text-muted font-normal italic">
|
||||
Want a sunny flash-bang, or a quiet lonely night?
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
<div class="flex gap-3 w-full">
|
||||
<button
|
||||
bind:this={lightElement}
|
||||
onclick={() => setTheme("light")}
|
||||
class="btn flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
|
||||
>
|
||||
<SunIcon size="24" class="inline-block mr-2" />
|
||||
Light
|
||||
</button>
|
||||
|
||||
<button
|
||||
bind:this={darkElement}
|
||||
onclick={() => setTheme("dark")}
|
||||
class="btn flex-1 p-4 rounded-lg text-black flex items-center justify-center"
|
||||
>
|
||||
<MoonIcon size="24" class="inline-block mr-2" />
|
||||
Dark
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-base font-bold">Motion settings</p>
|
||||
<p class="text-sm text-muted font-normal italic">
|
||||
Would you like fancy animations, or a more static
|
||||
experience?
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 w-full">
|
||||
<div class="flex gap-3 w-full">
|
||||
<button
|
||||
bind:this={enableMotionElement}
|
||||
onclick={() => setMotion(true)}
|
||||
class="btn flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
|
||||
>
|
||||
<PlayIcon size="24" class="inline-block mr-2" />
|
||||
Enable
|
||||
</button>
|
||||
|
||||
<button
|
||||
bind:this={disableMotionElement}
|
||||
onclick={() => setMotion(false)}
|
||||
class="btn flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center"
|
||||
>
|
||||
<PauseIcon size="24" class="inline-block mr-2" />
|
||||
Disable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
|
@ -0,0 +1,83 @@
|
|||
<script lang="ts">
|
||||
import { browser } from "$app/environment";
|
||||
import FancyTextInput from "$lib/components/functional/FancyInput.svelte";
|
||||
import Panel from "$lib/components/visual/Panel.svelte";
|
||||
import { log } from "$lib/logger";
|
||||
import { RefreshCwIcon, SaveAllIcon } from "lucide-svelte";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
let filenameFormat = "VERT_%name%";
|
||||
|
||||
function save() {
|
||||
log(["settings"], "Saving settings");
|
||||
if (!browser) return;
|
||||
localStorage.setItem("filenameFormat", filenameFormat);
|
||||
log(["settings"], `Saving filename format: ${filenameFormat}`);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const format = localStorage.getItem("filenameFormat");
|
||||
if (format) filenameFormat = format;
|
||||
});
|
||||
</script>
|
||||
|
||||
<Panel class="flex flex-col gap-8 p-6">
|
||||
<div class="flex flex-col gap-3">
|
||||
<h2 class="text-2xl font-bold">
|
||||
<RefreshCwIcon
|
||||
size="40"
|
||||
class="inline-block -mt-1 mr-2 bg-accent p-2 rounded-full"
|
||||
color="black"
|
||||
/>
|
||||
Conversion
|
||||
</h2>
|
||||
<div class="flex flex-col gap-8">
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-base font-bold">File name format</p>
|
||||
<p class="text-sm text-muted font-normal">
|
||||
This will determine the name of the file on download, <span
|
||||
class="font-bold italic"
|
||||
>not including the file extension.</span
|
||||
>
|
||||
You can put these following templates in the format, which
|
||||
will be replaced with the relevant information:
|
||||
<span class="font-bold">%name%</span>
|
||||
for the original file name,
|
||||
<span class="font-bold">%extension%</span>
|
||||
for the original file extension, and
|
||||
<span class="font-bold">%date%</span>
|
||||
for a date string of when the file was converted.
|
||||
</p>
|
||||
</div>
|
||||
<FancyTextInput
|
||||
placeholder="VERT_%name%"
|
||||
bind:value={filenameFormat}
|
||||
extension=".ext"
|
||||
type="text"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<p class="text-base font-bold">Second option</p>
|
||||
<p class="text-sm text-muted font-normal">
|
||||
This is just a sample option. This should not show up on
|
||||
the live website. JOVANN, DO NOT ADD THIS TO THE LIVE
|
||||
WEBSITE. PLEASE. JOVANN!!!!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end">
|
||||
<button
|
||||
onclick={save}
|
||||
class="w-fit btn px-6 py-4 bg-accent text-black flex items-center justify-center"
|
||||
>
|
||||
<SaveAllIcon size="24" class="inline-block mr-2" />
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
|
@ -0,0 +1,2 @@
|
|||
export { default as Appearance } from "./Appearance.svelte";
|
||||
export { default as Conversion } from "./Conversion.svelte";
|
|
@ -0,0 +1,68 @@
|
|||
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<Toast[]>([]);
|
||||
|
||||
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 : Infinity,
|
||||
exit: 500,
|
||||
};
|
||||
|
||||
// if "disappearing" not set, default error/warning to infinite duration
|
||||
if (disappearing === undefined) {
|
||||
switch (type) {
|
||||
case "error":
|
||||
case "warning":
|
||||
durations.stay = Infinity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const newToast: Toast = {
|
||||
id,
|
||||
type,
|
||||
message,
|
||||
disappearing: disappearing ?? true,
|
||||
durations,
|
||||
};
|
||||
toasts.update((currentToasts) => [...currentToasts, newToast]);
|
||||
|
||||
setTimeout(
|
||||
() => {
|
||||
removeToast(id);
|
||||
},
|
||||
durations.enter + durations.stay + durations.exit,
|
||||
);
|
||||
}
|
||||
|
||||
function removeToast(id: number) {
|
||||
toasts.update((currentToasts) =>
|
||||
currentToasts.filter((toast) => toast.id !== id),
|
||||
);
|
||||
}
|
||||
|
||||
export { toasts, addToast, removeToast };
|
|
@ -1,29 +1,202 @@
|
|||
import { browser } from "$app/environment";
|
||||
import { log } from "$lib/logger";
|
||||
import { converters } from "$lib/converters";
|
||||
import { error, log } from "$lib/logger";
|
||||
import { VertFile } from "$lib/types";
|
||||
import JSCookie from "js-cookie";
|
||||
import jsmediatags from "jsmediatags";
|
||||
import type { TagType } from "jsmediatags/types";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
class Files {
|
||||
public files = $state<VertFile[]>([]);
|
||||
}
|
||||
|
||||
class Theme {
|
||||
public dark = $state(false);
|
||||
public toggle = () => {
|
||||
this.dark = !this.dark;
|
||||
JSCookie.set("theme", this.dark ? "dark" : "light", {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
expires: 2147483647,
|
||||
});
|
||||
log(["theme"], `set to ${this.dark ? "dark" : "light"}`);
|
||||
if (browser) {
|
||||
window.plausible("Theme set", {
|
||||
props: { theme: theme.dark ? "dark" : "light" },
|
||||
});
|
||||
public requiredConverters = $derived(
|
||||
Array.from(new Set(files.files.map((f) => f.converter))),
|
||||
);
|
||||
|
||||
public ready = $derived(
|
||||
this.files.length === 0
|
||||
? false
|
||||
: this.requiredConverters.every((f) => f?.ready) &&
|
||||
this.files.every((f) => !f.processing),
|
||||
);
|
||||
public results = $derived(
|
||||
this.files.length === 0 ? false : this.files.every((f) => f.result),
|
||||
);
|
||||
|
||||
private _addThumbnail = async (file: VertFile) => {
|
||||
try {
|
||||
if (
|
||||
converters
|
||||
.find((c) => c.name === "ffmpeg")
|
||||
?.supportedFormats?.includes(file.from.toLowerCase())
|
||||
) {
|
||||
// try to get the thumbnail from the audio via jsmmediatags
|
||||
const tags = await new Promise<TagType>((resolve, reject) => {
|
||||
jsmediatags.read(file.file, {
|
||||
onSuccess: (tag) => resolve(tag),
|
||||
onError: (error) => reject(error),
|
||||
});
|
||||
});
|
||||
if (tags.tags.picture) {
|
||||
const blob = new Blob(
|
||||
[new Uint8Array(tags.tags.picture.data)],
|
||||
{
|
||||
type: tags.tags.picture.format,
|
||||
},
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
file.blobUrl = url;
|
||||
}
|
||||
} else {
|
||||
const img = new Image();
|
||||
img.src = URL.createObjectURL(file.file);
|
||||
await new Promise((resolve) => {
|
||||
img.onload = resolve;
|
||||
});
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const maxSize = 180;
|
||||
const scale = Math.max(
|
||||
maxSize / img.width,
|
||||
maxSize / img.height,
|
||||
);
|
||||
canvas.width = img.width * scale;
|
||||
canvas.height = img.height * scale;
|
||||
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
const url = canvas.toDataURL();
|
||||
file.blobUrl = url;
|
||||
canvas.remove();
|
||||
}
|
||||
} catch (e) {
|
||||
error(["files"], e);
|
||||
}
|
||||
};
|
||||
|
||||
private _add(file: VertFile | File) {
|
||||
if (file instanceof VertFile) {
|
||||
this.files.push(file);
|
||||
this._addThumbnail(file);
|
||||
} else {
|
||||
const format = "." + file.name.split(".").pop()?.toLowerCase();
|
||||
if (!format) {
|
||||
log(["files"], `no extension found for ${file.name}`);
|
||||
return;
|
||||
}
|
||||
const converter = converters.find((c) =>
|
||||
c.supportedFormats.includes(
|
||||
format || ".somenonexistentextension",
|
||||
),
|
||||
);
|
||||
if (!converter) {
|
||||
log(["files"], `no converter found for ${file.name}`);
|
||||
this.files.push(new VertFile(file, format, null));
|
||||
return;
|
||||
}
|
||||
const to = converter.supportedFormats.find((f) => f !== format);
|
||||
if (!to) {
|
||||
log(["files"], `no output format found for ${file.name}`);
|
||||
return;
|
||||
}
|
||||
const vf = new VertFile(file, to, converter);
|
||||
this.files.push(vf);
|
||||
this._addThumbnail(vf);
|
||||
}
|
||||
}
|
||||
|
||||
public add(file: VertFile | null | undefined): void;
|
||||
public add(file: File | null | undefined): void;
|
||||
public add(file: File[] | null | undefined): void;
|
||||
public add(file: VertFile[] | null | undefined): void;
|
||||
public add(file: FileList | null | undefined): void;
|
||||
public add(
|
||||
file:
|
||||
| VertFile
|
||||
| File
|
||||
| VertFile[]
|
||||
| File[]
|
||||
| FileList
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
if (!file) return;
|
||||
if (Array.isArray(file) || file instanceof FileList) {
|
||||
for (const f of file) {
|
||||
this._add(f);
|
||||
}
|
||||
} else {
|
||||
this._add(file);
|
||||
}
|
||||
}
|
||||
|
||||
public async convertAll() {
|
||||
await Promise.all(this.files.map((f) => f.convert()));
|
||||
}
|
||||
|
||||
public async downloadAll() {
|
||||
if (files.files.length === 0) return;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const dlFiles: any[] = [];
|
||||
for (let i = 0; i < files.files.length; i++) {
|
||||
const file = files.files[i];
|
||||
const result = file.result;
|
||||
if (!result) {
|
||||
console.error("No result found");
|
||||
continue;
|
||||
}
|
||||
dlFiles.push({
|
||||
name: file.file.name.replace(/\.[^/.]+$/, "") + file.to,
|
||||
lastModified: Date.now(),
|
||||
input: await result.file.arrayBuffer(),
|
||||
});
|
||||
}
|
||||
const { downloadZip } = await import("client-zip");
|
||||
const blob = await downloadZip(dlFiles, "converted.zip").blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
|
||||
const filenameFormat =
|
||||
localStorage.getItem("filenameFormat") ?? "VERT_%name%";
|
||||
|
||||
const format = (name: string) => {
|
||||
const date = new Date().toISOString();
|
||||
return name
|
||||
.replace(/%date%/g, date)
|
||||
.replace(/%name%/g, "Multi")
|
||||
.replace(/%extension%/g, "");
|
||||
};
|
||||
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `${format(filenameFormat)}.zip`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export function setTheme(themeTo: "light" | "dark") {
|
||||
document.documentElement.classList.remove("light", "dark");
|
||||
document.documentElement.classList.add(themeTo);
|
||||
localStorage.setItem("theme", themeTo);
|
||||
window.plausible("Theme set", {
|
||||
props: { theme: themeTo },
|
||||
});
|
||||
log(["theme"], `set to ${themeTo}`);
|
||||
theme.set(themeTo);
|
||||
}
|
||||
|
||||
export function setMotion(motionEnabled: boolean) {
|
||||
localStorage.setItem("motion", motionEnabled.toString());
|
||||
window.plausible("Motion set", {
|
||||
props: { motion: motionEnabled },
|
||||
});
|
||||
log(["motion"], `set to ${motionEnabled}`);
|
||||
motion.set(motionEnabled);
|
||||
}
|
||||
|
||||
export const files = new Files();
|
||||
export const theme = new Theme();
|
||||
export const showGradient = writable(true);
|
||||
export const gradientColor = writable("");
|
||||
|
||||
export const isMobile = writable(false);
|
||||
export const motion = writable(true);
|
||||
export const theme = writable<"light" | "dark">("light");
|
|
@ -18,12 +18,14 @@ export class VertFile {
|
|||
|
||||
public blobUrl = $state<string>();
|
||||
|
||||
public processing = $state(false);
|
||||
|
||||
public converter: Converter | null = null;
|
||||
|
||||
constructor(
|
||||
public readonly file: File,
|
||||
to: string,
|
||||
converter?: Converter,
|
||||
converter?: Converter | null,
|
||||
blobUrl?: string,
|
||||
) {
|
||||
this.to = to;
|
||||
|
@ -37,13 +39,29 @@ export class VertFile {
|
|||
if (!this.converter) throw new Error("No converter found");
|
||||
this.result = null;
|
||||
this.progress = 0;
|
||||
this.processing = true;
|
||||
const res = await this.converter.convert(this, this.to);
|
||||
this.processing = false;
|
||||
this.result = res;
|
||||
return res;
|
||||
}
|
||||
|
||||
public async download() {
|
||||
if (!this.result) throw new Error("No result found");
|
||||
|
||||
const filenameFormat =
|
||||
localStorage.getItem("filenameFormat") ?? "VERT_%name%";
|
||||
|
||||
const format = (name: string) => {
|
||||
const date = new Date().toISOString();
|
||||
const baseName = this.file.name.replace(/\.[^/.]+$/, "");
|
||||
const originalExtension = this.file.name.split('.').pop()!;
|
||||
return name
|
||||
.replace(/%date%/g, date)
|
||||
.replace(/%name%/g, baseName)
|
||||
.replace(/%extension%/g, originalExtension);
|
||||
};
|
||||
|
||||
const blob = URL.createObjectURL(
|
||||
new Blob([await this.result.file.arrayBuffer()], {
|
||||
type: this.to.slice(1),
|
||||
|
@ -51,7 +69,7 @@ export class VertFile {
|
|||
);
|
||||
const a = document.createElement("a");
|
||||
a.href = blob;
|
||||
a.download = `VERT-Converted_${new Date().toISOString()}${this.to}`;
|
||||
a.download = `${format(filenameFormat)}${this.to}`;
|
||||
// force it to not open in a new tab
|
||||
a.target = "_blank";
|
||||
a.style.display = "none";
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
export const load = ({ url, request, cookies }) => {
|
||||
// if the "theme" cookie isn't "dark" or "light", reset it
|
||||
const theme = cookies.get("theme") ?? "";
|
||||
if (theme !== "dark" && theme !== "light") {
|
||||
cookies.set("theme", "", {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
expires: new Date(0),
|
||||
});
|
||||
}
|
||||
const { pathname } = url;
|
||||
const ua = request.headers.get("user-agent");
|
||||
const isMobile = /mobile/i.test(ua || "");
|
||||
const isFirefox = /firefox/i.test(ua || "");
|
||||
return {
|
||||
pathname,
|
||||
isMobile,
|
||||
isFirefox,
|
||||
};
|
||||
};
|
|
@ -1,52 +1,80 @@
|
|||
<script lang="ts">
|
||||
import "../app.scss";
|
||||
import { goto } from "$app/navigation";
|
||||
import { blur, duration } from "$lib/animation";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import { files, theme } from "$lib/store/index.svelte";
|
||||
import Logo from "$lib/components/visual/svg/Logo.svelte";
|
||||
import { page } from "$app/state";
|
||||
import { beforeNavigate, goto } from "$app/navigation";
|
||||
import { PUB_HOSTNAME, PUB_PLAUSIBLE_URL } from "$env/static/public";
|
||||
import { duration, fly } from "$lib/animation";
|
||||
import VertVBig from "$lib/assets/vert-bg.svg?component";
|
||||
import featuredImage from "$lib/assets/VERT_Feature.webp";
|
||||
import Navbar from "$lib/components/functional/Navbar.svelte";
|
||||
import Footer from "$lib/components/visual/Footer.svelte";
|
||||
import Logo from "$lib/components/visual/svg/Logo.svelte";
|
||||
|
||||
import { fade } from "$lib/animation";
|
||||
import {
|
||||
PUB_ENV,
|
||||
PUB_HOSTNAME,
|
||||
PUB_PLAUSIBLE_URL,
|
||||
} from "$env/static/public";
|
||||
import FancyMenu from "$lib/components/functional/FancyMenu.svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import { MoonIcon, SunIcon } from "lucide-svelte";
|
||||
import { browser } from "$app/environment";
|
||||
import JSCookie from "js-cookie";
|
||||
files,
|
||||
gradientColor,
|
||||
isMobile,
|
||||
motion,
|
||||
showGradient,
|
||||
theme,
|
||||
} from "$lib/store/index.svelte";
|
||||
import {
|
||||
InfoIcon,
|
||||
RefreshCw,
|
||||
SettingsIcon,
|
||||
UploadIcon,
|
||||
} from "lucide-svelte";
|
||||
import { onMount } from "svelte";
|
||||
let { children, data } = $props();
|
||||
import { quintOut } from "svelte/easing";
|
||||
import "../app.scss";
|
||||
import { writable } from "svelte/store";
|
||||
import { DISCORD_URL, GITHUB_URL } from "$lib/consts";
|
||||
import { type Toast as ToastType, toasts } from "$lib/store/ToastProvider";
|
||||
import Toast from "$lib/components/visual/Toast.svelte";
|
||||
let { children } = $props();
|
||||
|
||||
let shouldGoBack = writable(false);
|
||||
let navbar = $state<HTMLDivElement>();
|
||||
let hover = $state(false);
|
||||
let toastList = $state<ToastType[]>([]);
|
||||
|
||||
const links = $derived<
|
||||
toasts.subscribe((value) => {
|
||||
toastList = value as ToastType[];
|
||||
});
|
||||
|
||||
const items = $derived<
|
||||
{
|
||||
name: string;
|
||||
url: string;
|
||||
activeMatch: (pathname: string) => boolean;
|
||||
icon: any;
|
||||
badge?: number;
|
||||
}[]
|
||||
>([
|
||||
{
|
||||
name: "Upload",
|
||||
url: "/",
|
||||
activeMatch: (pathname) => pathname === "/",
|
||||
icon: UploadIcon,
|
||||
},
|
||||
{
|
||||
name:
|
||||
files.files.length > 0
|
||||
? `Convert (${files.files.length})`
|
||||
: `Convert`,
|
||||
name: "Convert",
|
||||
url: "/convert",
|
||||
activeMatch: (pathname) => pathname === "/convert",
|
||||
icon: RefreshCw,
|
||||
badge: files.files.length,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
url: "/settings",
|
||||
activeMatch: (pathname) => pathname.startsWith("/settings"),
|
||||
icon: SettingsIcon,
|
||||
},
|
||||
{
|
||||
name: "About",
|
||||
url: "/about",
|
||||
activeMatch: (pathname) => pathname.startsWith("/about"),
|
||||
icon: InfoIcon,
|
||||
},
|
||||
]);
|
||||
|
||||
|
@ -57,27 +85,6 @@
|
|||
}
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
if (!browser) return;
|
||||
if (theme.dark) {
|
||||
document.documentElement.classList.add("dark");
|
||||
document.documentElement.classList.remove("light");
|
||||
JSCookie.set("theme", "dark", {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
expires: 2147483647,
|
||||
});
|
||||
} else {
|
||||
document.documentElement.classList.add("light");
|
||||
document.documentElement.classList.remove("dark");
|
||||
JSCookie.set("theme", "light", {
|
||||
path: "/",
|
||||
sameSite: "lax",
|
||||
expires: 2147483647,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const mouseEnter = () => {
|
||||
hover = true;
|
||||
|
@ -89,197 +96,226 @@
|
|||
|
||||
navbar?.addEventListener("mouseenter", mouseEnter);
|
||||
navbar?.addEventListener("mouseleave", mouseLeave);
|
||||
|
||||
isMobile.set(window.innerWidth <= 768);
|
||||
window.addEventListener("resize", () => {
|
||||
isMobile.set(window.innerWidth <= 768);
|
||||
});
|
||||
|
||||
motion.set(localStorage.getItem("motion") !== "false"); // defaults to true if not set
|
||||
theme.set(
|
||||
(localStorage.getItem("theme") as "light" | "dark") || "light",
|
||||
);
|
||||
});
|
||||
|
||||
let goingLeft = $state(false);
|
||||
|
||||
beforeNavigate((e) => {
|
||||
const oldIndex = items.findIndex((i) =>
|
||||
i.activeMatch(e.from?.url.pathname || ""),
|
||||
);
|
||||
const newIndex = items.findIndex((i) =>
|
||||
i.activeMatch(e.to?.url.pathname || ""),
|
||||
);
|
||||
if (newIndex < oldIndex) {
|
||||
goingLeft = true;
|
||||
} else {
|
||||
goingLeft = false;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>VERT.sh</title>
|
||||
<meta name="theme-color" content="#F2ABEE" />
|
||||
<meta
|
||||
name="title"
|
||||
content="VERT.sh — Free, fast, and awesome file convert"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="With VERT you can convert image and audio files to and from PNG, JPG, WEBP, MP3, WAV, FLAC, and more. No ads, no tracking, open source, and all processing is done on your device."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="VERT.sh — Free, fast, and awesome file convert"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="With VERT you can convert image and audio files to and from PNG, JPG, WEBP, MP3, WAV, FLAC, and more. No ads, no tracking, open source, and all processing is done on your device."
|
||||
/>
|
||||
<meta property="og:image" content={featuredImage} />
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta
|
||||
property="twitter:title"
|
||||
content="VERT.sh — Free, fast, and awesome file convert"
|
||||
/>
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="With VERT you can convert image and audio files to and from PNG, JPG, WEBP, MP3, WAV, FLAC, and more. No ads, no tracking, open source, and all processing is done on your device."
|
||||
/>
|
||||
<meta property="twitter:image" content={featuredImage} />
|
||||
{#if PUB_PLAUSIBLE_URL}<script
|
||||
defer
|
||||
data-domain={PUB_HOSTNAME || "vert.sh"}
|
||||
src="{PUB_PLAUSIBLE_URL}/js/script.pageview-props.tagged-events.js"
|
||||
></script>{/if}
|
||||
<script src="/coi-serviceworker.min.js"></script>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
role="main"
|
||||
class="w-full h-full max-w-screen-lg mx-auto p-4"
|
||||
ondragenter={maybeNavToHome}
|
||||
>
|
||||
<div class="flex justify-center mb-5 lg:hidden">
|
||||
<a
|
||||
href="/"
|
||||
class="px-4 relative h-14 mr-3 justify-center items-center bg-accent-background fill-accent-foreground rounded-xl md:hidden flex"
|
||||
>
|
||||
<div class="h-6 relative w-24 items-center flex justify-center">
|
||||
<Logo />
|
||||
{#if PUB_ENV === "nightly"}
|
||||
<div
|
||||
class="absolute -top-6 -left-10 px-2 py-1 w-fit bg-foreground-highlight text-accent-background rotate-[-10deg] rounded-xl"
|
||||
style="font-family: Comic Sans MS, sans-serif;"
|
||||
>
|
||||
NIGHTLY
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex flex-col min-h-screen h-full">
|
||||
<!-- FIXME: if user resizes between desktop/mobile, highlight of page disappears (only shows on original size) -->
|
||||
|
||||
<div
|
||||
class="w-full max-w-screen-md p-1 border-solid border-2 rounded-2xl border-foreground-muted-alt flex mb-10 mx-auto lg:mt-5"
|
||||
bind:this={navbar}
|
||||
>
|
||||
<div class="md:p-1">
|
||||
<div>
|
||||
<!-- Mobile logo -->
|
||||
<div class="flex md:hidden justify-center items-center p-8">
|
||||
<a
|
||||
class="flex items-center justify-center bg-panel p-2 rounded-[20px] shadow-panel"
|
||||
href="/"
|
||||
class="px-3 relative w-full h-full mr-3 justify-center items-center bg-accent-background fill-accent-foreground rounded-xl md:flex hidden"
|
||||
>
|
||||
<div class="h-6 w-24 items-center flex justify-center relative">
|
||||
<Logo />
|
||||
{#if PUB_ENV === "nightly"}
|
||||
<div
|
||||
class="absolute -top-6 -left-10 px-2 py-1 w-fit bg-foreground-highlight text-accent-background rotate-[-10deg] rounded-xl"
|
||||
style="font-family: Comic Sans MS, sans-serif;"
|
||||
>
|
||||
NIGHTLY
|
||||
</div>
|
||||
{/if}
|
||||
<div
|
||||
class="h-14 bg-accent rounded-[14px] flex items-center justify-center"
|
||||
>
|
||||
<div class="w-28 h-5">
|
||||
<Logo />
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<FancyMenu {links} {shouldGoBack} />
|
||||
<div class="h-16 px-4 flex items-center">
|
||||
<button onclick={theme.toggle} class="grid-cols-1 grid-rows-1 grid">
|
||||
<!-- {#if theme.dark}
|
||||
<div
|
||||
class="w-full h-full flex items-center justify-center row-start-1 col-start-1"
|
||||
>
|
||||
<MoonIcon />
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="w-full h-full flex items-center justify-center row-start-1 col-start-1"
|
||||
>
|
||||
<SunIcon />
|
||||
</div>
|
||||
{/if} -->
|
||||
{#if browser}
|
||||
{#if theme.dark}
|
||||
<div
|
||||
in:blur={{
|
||||
blurMultiplier: 1,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
scale: {
|
||||
start: 0.5,
|
||||
end: 1,
|
||||
},
|
||||
}}
|
||||
out:blur={{
|
||||
blurMultiplier: 1,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
scale: {
|
||||
start: 1,
|
||||
end: 1.5,
|
||||
},
|
||||
}}
|
||||
class="w-full h-full flex items-center justify-center row-start-1 col-start-1"
|
||||
>
|
||||
<MoonIcon class="w-8" />
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
in:blur={{
|
||||
blurMultiplier: 1,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
scale: {
|
||||
start: 0.5,
|
||||
end: 1,
|
||||
},
|
||||
}}
|
||||
out:blur={{
|
||||
blurMultiplier: 1,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
scale: {
|
||||
start: 1,
|
||||
end: 1.5,
|
||||
},
|
||||
}}
|
||||
class="w-full h-full flex items-center justify-center row-start-1 col-start-1"
|
||||
>
|
||||
<SunIcon class="w-8" />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div
|
||||
class="w-full h-full flex items-center justify-center row-start-1 col-start-1 dynadark:hidden"
|
||||
>
|
||||
<SunIcon class="w-8" />
|
||||
</div>
|
||||
<div
|
||||
class="w-full h-full hidden items-center justify-center row-start-1 col-start-1 dynadark:flex"
|
||||
>
|
||||
<MoonIcon class="w-8" />
|
||||
</div>
|
||||
{/if}
|
||||
</button>
|
||||
<!-- Desktop navbar -->
|
||||
<div class="hidden md:flex p-8 w-screen justify-center z-50">
|
||||
<Navbar {items} />
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full max-w-screen-lg grid grid-cols-1 grid-rows-1 relative">
|
||||
{#key data.pathname}
|
||||
<div class="w-full">
|
||||
|
||||
<div class="grid grid-rows-1 grid-cols-1 h-full flex-grow">
|
||||
{#key page.url.pathname}
|
||||
<div
|
||||
class="row-start-1 col-start-1"
|
||||
in:fly={{
|
||||
x: goingLeft ? -window.innerWidth : window.innerWidth,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
delay: 25,
|
||||
}}
|
||||
out:fly={{
|
||||
x: goingLeft ? window.innerWidth : -window.innerWidth,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full"
|
||||
style={hover ? "will-change: opacity, blur, transform" : ""}
|
||||
in:blur={{
|
||||
class="flex flex-col h-full pb-36 md:pb-0"
|
||||
in:fade={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
blurMultiplier: 12,
|
||||
x: {
|
||||
start: !$shouldGoBack ? 250 : -250,
|
||||
end: 0,
|
||||
},
|
||||
y: {
|
||||
start: 100,
|
||||
end: 0,
|
||||
},
|
||||
scale: {
|
||||
start: 0.75,
|
||||
end: 1,
|
||||
},
|
||||
origin: "top center",
|
||||
delay: $isMobile ? 0 : 100,
|
||||
}}
|
||||
out:blur={{
|
||||
out:fade={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
blurMultiplier: 12,
|
||||
x: {
|
||||
start: 0,
|
||||
end: !$shouldGoBack ? -250 : 250,
|
||||
},
|
||||
y: {
|
||||
start: 0,
|
||||
end: 100,
|
||||
},
|
||||
scale: {
|
||||
start: 1,
|
||||
end: 0.75,
|
||||
},
|
||||
origin: "top center",
|
||||
delay: $isMobile ? 0 : 200,
|
||||
}}
|
||||
>
|
||||
<div class="pb-20">
|
||||
{@render children()}
|
||||
</div>
|
||||
{@render children()}
|
||||
</div>
|
||||
</div>
|
||||
{/key}
|
||||
</div>
|
||||
|
||||
<div class="fixed bottom-0 right-0 p-4 z-50 space-y-4">
|
||||
{#each toastList as { id, type, message, durations }}
|
||||
<Toast {id} {type} {message} {durations} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div
|
||||
class="hidden md:block w-full h-14 border-t border-separator relative mt-12"
|
||||
>
|
||||
<Footer
|
||||
class="w-full h-full"
|
||||
items={{
|
||||
//"Privacy policy": "#",
|
||||
"Source code": GITHUB_URL,
|
||||
"Discord server": DISCORD_URL,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Mobile navbar -->
|
||||
<div
|
||||
class="fixed md:hidden bottom-0 left-0 w-screen p-8 justify-center z-50"
|
||||
>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<Navbar {items} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Gradients placed here to prevent it overlapping in transitions -->
|
||||
{#if page.url.pathname === "/"}
|
||||
<div
|
||||
class="fixed -z-30 top-0 left-0 w-screen h-screen flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<VertVBig
|
||||
class="fill-[--fg] opacity-10 dynadark:opacity-5 scale-[200%] md:scale-[80%]"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
id="gradient-bg"
|
||||
class="fixed top-0 left-0 w-screen h-screen -z-40 pointer-events-none"
|
||||
style="background: var(--bg-gradient);"
|
||||
></div>
|
||||
{:else if page.url.pathname === "/convert" && $showGradient}
|
||||
<div
|
||||
id="gradient-bg"
|
||||
class="fixed top-0 left-0 w-screen h-screen -z-40 pointer-events-none"
|
||||
style="background: var(--bg-gradient-{$gradientColor || 'pink'});"
|
||||
></div>
|
||||
{:else if page.url.pathname === "/convert" && files.files.length === 1 && files.files[0].blobUrl}
|
||||
<div
|
||||
class="fixed w-screen h-screen opacity-75 overflow-hidden top-0 left-0 -z-50 pointer-events-none grid grid-cols-1 grid-rows-1 scale-105"
|
||||
>
|
||||
<div
|
||||
class="w-full relative"
|
||||
transition:fade={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
>
|
||||
<img
|
||||
class="object-cover w-full h-full blur-md"
|
||||
src={files.files[0].blobUrl}
|
||||
alt={files.files[0].name}
|
||||
/>
|
||||
<div
|
||||
class="absolute top-0 left-0 w-full h-full"
|
||||
style="background: var(--bg-gradient-image);"
|
||||
></div>
|
||||
<!-- <div class="absolute bottom-0 left-0 w-full h-full">
|
||||
<ProgressiveBlur
|
||||
direction="bottom"
|
||||
endIntensity={256}
|
||||
iterations={8}
|
||||
fadeTo="var(--bg)"
|
||||
/>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
{:else if page.url.pathname === "/settings"}
|
||||
<div
|
||||
id="gradient-bg"
|
||||
class="fixed top-0 left-0 w-screen h-screen -z-40 pointer-events-none"
|
||||
style="background: var(--bg-gradient-blue);"
|
||||
></div>
|
||||
{:else if page.url.pathname === "/about"}
|
||||
<div
|
||||
id="gradient-bg"
|
||||
class="fixed top-0 left-0 w-screen h-screen -z-40 pointer-events-none"
|
||||
style="background: var(--bg-gradient-pink);"
|
||||
></div>
|
||||
{/if}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { browser } from "$app/environment";
|
||||
import { theme } from "$lib/store/index.svelte";
|
||||
import JSCookie from "js-cookie";
|
||||
|
||||
export const load = ({ data }) => {
|
||||
if (!browser) return;
|
||||
|
@ -11,20 +9,7 @@ export const load = ({ data }) => {
|
|||
status: 200,
|
||||
});
|
||||
});
|
||||
const themeStr = JSCookie.get("theme");
|
||||
if (typeof themeStr === "undefined") {
|
||||
theme.dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||||
JSCookie.set("theme", theme.dark ? "dark" : "light", {
|
||||
sameSite: "strict",
|
||||
path: "/",
|
||||
expires: 2147483647,
|
||||
});
|
||||
} else {
|
||||
theme.dark = themeStr === "dark";
|
||||
}
|
||||
theme.dark = JSCookie.get("theme") === "dark";
|
||||
window.plausible("Theme set", {
|
||||
props: { theme: theme.dark ? "dark" : "light" },
|
||||
});
|
||||
return data;
|
||||
};
|
||||
|
||||
export const prerender = true;
|
||||
|
|
|
@ -1,251 +1,84 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
// this comment was written on 15/11/2024 at 16:01 GMT.
|
||||
// i bet to myself that i could complete this whole redesign implementation
|
||||
// by the time realmy got started on it. i guess we'll see how that goes
|
||||
//
|
||||
// ship fast n break things !!
|
||||
// -- nullptr
|
||||
|
||||
import Uploader from "$lib/components/functional/Uploader.svelte";
|
||||
import { converters } from "$lib/converters";
|
||||
import { log } from "$lib/logger";
|
||||
import { files } from "$lib/store/index.svelte";
|
||||
import { VertFile } from "$lib/types/file.svelte";
|
||||
import { Check } from "lucide-svelte";
|
||||
import jsmediatags from "jsmediatags";
|
||||
import type { TagType } from "jsmediatags/types/index.js";
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
let ourFiles = $state<File[]>();
|
||||
|
||||
const runUpload = async () => {
|
||||
const newFilePromises = (ourFiles || []).map(async (f) => {
|
||||
return new Promise<(typeof files.files)[0] | void>(
|
||||
(resolve, reject) => {
|
||||
const from =
|
||||
"." + f.name.toLowerCase().split(".").slice(-1);
|
||||
const converter = converters.find((c) =>
|
||||
c.supportedFormats.includes(from.toLowerCase()),
|
||||
);
|
||||
if (!converter) resolve();
|
||||
const to =
|
||||
converter?.supportedFormats.find((f) => f !== from) ||
|
||||
converters[0].supportedFormats[0];
|
||||
log(
|
||||
["uploader", "converter"],
|
||||
`converting ${from} to ${to} using ${converter?.name || "... no converter??"}`,
|
||||
);
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const img = new Image();
|
||||
img.src = URL.createObjectURL(f);
|
||||
const maxSize = 512;
|
||||
img.onload = () => {
|
||||
const scale = Math.max(
|
||||
maxSize / img.width,
|
||||
maxSize / img.height,
|
||||
);
|
||||
canvas.width = img.width * scale;
|
||||
canvas.height = img.height * scale;
|
||||
ctx?.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
// get the blob
|
||||
canvas.toBlob(
|
||||
async (blob) => {
|
||||
resolve(
|
||||
new VertFile(
|
||||
f,
|
||||
to,
|
||||
converter,
|
||||
URL.createObjectURL(blob!),
|
||||
),
|
||||
);
|
||||
},
|
||||
"image/jpeg",
|
||||
0.75,
|
||||
);
|
||||
};
|
||||
|
||||
img.onerror = async () => {
|
||||
// resolve(new VertFile(f, to, converter));
|
||||
const reader = new FileReader();
|
||||
const file = new VertFile(f, to, converter);
|
||||
resolve(file);
|
||||
reader.onload = async (e) => {
|
||||
const tags = await new Promise<TagType>(
|
||||
(resolve, reject) => {
|
||||
jsmediatags.read(
|
||||
new Blob([
|
||||
new Uint8Array(
|
||||
e.target?.result as ArrayBuffer,
|
||||
),
|
||||
]),
|
||||
{
|
||||
onSuccess: (tag) => resolve(tag),
|
||||
onError: (error) => reject(error),
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
const picture = tags.tags.picture;
|
||||
if (!picture) return;
|
||||
|
||||
const blob = new Blob(
|
||||
[new Uint8Array(picture.data)],
|
||||
{
|
||||
type: picture.format,
|
||||
},
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
file.blobUrl = url;
|
||||
};
|
||||
reader.readAsArrayBuffer(f);
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
let oldLen = files.files.length;
|
||||
files.files = [
|
||||
...files.files,
|
||||
...(await Promise.all(newFilePromises)).filter(
|
||||
(f) => typeof f !== "undefined",
|
||||
),
|
||||
];
|
||||
let newLen = files.files.length;
|
||||
log(["uploader"], `handled ${newLen - oldLen} files`);
|
||||
ourFiles = [];
|
||||
|
||||
if (files.files.length > 0) goto("/convert");
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>VERT.sh — Free, fast, and awesome file convert</title>
|
||||
<meta
|
||||
name="title"
|
||||
content="VERT.sh — Free, fast, and awesome file convert"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="With VERT you can convert images to PNG, JPG, WEBP, GIF, AVIF, and more. No ads, no tracking, open source, and all processing is done on your device."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="VERT.sh — Free, fast, and awesome file convert"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="With VERT you can convert images to PNG, JPG, WEBP, GIF, AVIF, and more. No ads, no tracking, open source, and all processing is done on your device."
|
||||
/>
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta
|
||||
property="twitter:title"
|
||||
content="VERT.sh — Free, fast, and awesome file convert"
|
||||
/>
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="With VERT you can convert images to PNG, JPG, WEBP, GIF, AVIF, and more. No ads, no tracking, open source, and all processing is done on your device."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
{#snippet sellingPoint(text: string)}
|
||||
<li
|
||||
class="grid items-center gap-4"
|
||||
style="grid-template-columns: 2rem auto"
|
||||
>
|
||||
<div
|
||||
class="h-8 w-8 bg-accent-background text-accent-foreground rounded-full flex items-center justify-center"
|
||||
>
|
||||
<Check />
|
||||
</div>
|
||||
<span class="text-lg">{text}</span>
|
||||
</li>
|
||||
{/snippet}
|
||||
|
||||
<div class="[@media(max-height:768px)]:block mt-10 picker-fly">
|
||||
<Uploader
|
||||
isMobile={data.isMobile || false}
|
||||
bind:files={ourFiles}
|
||||
onupload={runUpload}
|
||||
acceptedFormats={[
|
||||
...new Set(converters.flatMap((c) => c.supportedFormats)),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-20">
|
||||
<h1 class="text-3xl text-center font-display header-fly-in">
|
||||
Free, fast, and awesome file converting <span
|
||||
class="px-2 py-1 text-xl bg-accent-background text-accent-foreground rounded-lg"
|
||||
>BETA</span
|
||||
>
|
||||
</h1>
|
||||
<div class="flex justify-center mt-10">
|
||||
<div class="grid gap-4">
|
||||
<!-- {@render sellingPoint("Very fast, all processing done on device")}
|
||||
{@render sellingPoint("No ads, and open source")}
|
||||
{@render sellingPoint("Beautiful and straightforward UI")} -->
|
||||
{#each ["Very fast, all processing done on device", "No file or size limit", "No ads, and open source", "Beautiful and straightforward UI"] as text, i}
|
||||
<div class="fly-in" style="--delay: {i * 50}ms;">
|
||||
{@render sellingPoint(text)}
|
||||
<div
|
||||
class="w-screen px-8 h-full flex items-start justify-center overflow-hidden max-h-screen"
|
||||
>
|
||||
<div class="content w-screen flex items-center justify-center flex-grow">
|
||||
<div class="max-w-5xl w-full">
|
||||
<div
|
||||
class="flex items-center h-auto gap-12 md:gap-24 md:flex-row flex-col"
|
||||
>
|
||||
<div class="flex-grow w-full text-center md:text-left">
|
||||
<h1
|
||||
class="text-4xl px-12 md:p-0 md:text-6xl flex-wrap tracking-tight leading-tight md:leading-[72px] mb-4 md:mb-6"
|
||||
>
|
||||
The file converter you'll love.
|
||||
</h1>
|
||||
<p
|
||||
class="font-normal px-5 md:p-0 text-lg md:text-xl text-black text-muted dynadark:text-muted"
|
||||
>
|
||||
All processing is done on your device. No file size
|
||||
limit, no ads, and completely open source.
|
||||
</p>
|
||||
</div>
|
||||
{/each}
|
||||
<div class="flex-grow w-11/12 md:w-full h-72">
|
||||
<Uploader class="w-full h-full" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* for this page specifically */
|
||||
:global(html, body) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@keyframes fly-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
filter: blur(18px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
/* Centers content on most screen sizes, excluding mobile */
|
||||
@media screen and (min-width: 768px) {
|
||||
.content {
|
||||
padding-top: 10vh;
|
||||
padding-bottom: 10vh;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes picker-fly {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(48px);
|
||||
filter: blur(18px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
@media screen and (min-width: 768px) and (min-height: 576px) {
|
||||
.content {
|
||||
padding-top: 15vh;
|
||||
padding-bottom: 15vh;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes header-fly-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.9);
|
||||
filter: blur(18px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
filter: blur(0);
|
||||
@media screen and (min-width: 768px) and (min-height: 720px) {
|
||||
.content {
|
||||
padding-top: 20vh;
|
||||
padding-bottom: 20vh;
|
||||
}
|
||||
}
|
||||
|
||||
.header-fly-in {
|
||||
animation: header-fly-in var(--transition) 750ms forwards;
|
||||
opacity: 0;
|
||||
@media screen and (min-width: 768px) and (min-height: 1080px) {
|
||||
.content {
|
||||
padding-top: 25vh;
|
||||
padding-bottom: 25vh;
|
||||
}
|
||||
}
|
||||
|
||||
.fly-in {
|
||||
animation: fly-in var(--transition) 750ms var(--delay) forwards;
|
||||
opacity: 0;
|
||||
@media screen and (min-width: 768px) and (min-height: 1440px) {
|
||||
.content {
|
||||
padding-top: 30vh;
|
||||
padding-bottom: 30vh;
|
||||
}
|
||||
}
|
||||
|
||||
.picker-fly {
|
||||
animation: picker-fly var(--transition) 750ms forwards;
|
||||
opacity: 0;
|
||||
@media screen and (min-width: 768px) and (min-height: 2160px) {
|
||||
.content {
|
||||
padding-top: 35vh;
|
||||
padding-bottom: 35vh;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,159 +1,130 @@
|
|||
<script lang="ts">
|
||||
import { error } from "$lib/logger";
|
||||
import * as About from "$lib/sections/about";
|
||||
import { InfoIcon } from "lucide-svelte";
|
||||
import { onMount } from "svelte";
|
||||
import avatarNullptr from "$lib/assets/avatars/nullptr.jpg";
|
||||
import avatarRealmy from "$lib/assets/avatars/realmy.jpg";
|
||||
import avatarJovannMC from "$lib/assets/avatars/jovannmc.jpg";
|
||||
import { GITHUB_API_URL } from "$lib/consts";
|
||||
|
||||
const multiplier = 50;
|
||||
interface Donator {
|
||||
name: string;
|
||||
amount?: string | number;
|
||||
avatar: string;
|
||||
}
|
||||
|
||||
const credits = [
|
||||
interface Contributor {
|
||||
name: string;
|
||||
github: string;
|
||||
avatar: string;
|
||||
role?: string;
|
||||
}
|
||||
|
||||
// const donors: Donator[] = [];
|
||||
|
||||
const mainContribs: Contributor[] = [
|
||||
{
|
||||
name: "nullptr",
|
||||
github: "https://github.com/not-nullptr",
|
||||
role: "Lead developer; conversion backend, UI implementation",
|
||||
avatar: avatarNullptr,
|
||||
url: "https://nullp.tr",
|
||||
description: "conversion backend, UI, animations, promotion",
|
||||
},
|
||||
{
|
||||
name: "Realmy",
|
||||
github: "https://github.com/RealmyTheMan",
|
||||
role: "Lead designer; logo and branding, user interface design",
|
||||
avatar: avatarRealmy,
|
||||
url: "https://realmy.net",
|
||||
description: "idea, UI, branding, operational costs",
|
||||
},
|
||||
{
|
||||
name: "JovannMC",
|
||||
github: "https://github.com/JovannMC",
|
||||
role: "Developer; UI implementation",
|
||||
avatar: avatarJovannMC,
|
||||
},
|
||||
];
|
||||
|
||||
let ghContribs: Contributor[] = [];
|
||||
|
||||
onMount(async () => {
|
||||
// Check if the data is already in sessionStorage
|
||||
const cachedContribs = sessionStorage.getItem("ghContribs");
|
||||
if (cachedContribs) {
|
||||
ghContribs = JSON.parse(cachedContribs);
|
||||
console.log("Loaded GitHub contributors from cache");
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch GitHub contributors
|
||||
try {
|
||||
const response = await fetch(`${GITHUB_API_URL}/contributors`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error, status: ${response.status}`);
|
||||
}
|
||||
const allContribs = await response.json();
|
||||
|
||||
// Filter out main contributors
|
||||
const mainContribNames = mainContribs.map((contrib) =>
|
||||
contrib.github.split("/").pop(),
|
||||
);
|
||||
const filteredContribs = allContribs.filter(
|
||||
(contrib: { login: string }) =>
|
||||
!mainContribNames.includes(contrib.login),
|
||||
);
|
||||
|
||||
// Fetch and cache avatar images as Base64
|
||||
const fetchAvatar = async (url: string) => {
|
||||
const res = await fetch(url);
|
||||
const blob = await res.blob();
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => resolve(reader.result as string);
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
};
|
||||
|
||||
ghContribs = await Promise.all(
|
||||
filteredContribs.map(
|
||||
async (contrib: {
|
||||
login: string;
|
||||
avatar_url: string;
|
||||
html_url: string;
|
||||
}) => ({
|
||||
name: contrib.login,
|
||||
avatar: await fetchAvatar(contrib.avatar_url),
|
||||
github: contrib.html_url,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
// Cache the data in sessionStorage
|
||||
sessionStorage.setItem("ghContribs", JSON.stringify(ghContribs));
|
||||
} catch (e) {
|
||||
error(["general"], `Error fetching GitHub contributors: ${e}`);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>About VERT</title>
|
||||
<meta name="title" content="About VERT — VERT.sh" />
|
||||
<meta property="og:title" content="About VERT — VERT.sh" />
|
||||
<meta property="twitter:title" content="About VERT — VERT.sh" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="text-lg mx-auto max-w-screen-md">
|
||||
<h1
|
||||
class="font-display text-3xl text-transition"
|
||||
style="--delay: {0 * multiplier}ms"
|
||||
>
|
||||
⁉️ about VERT
|
||||
<div class="flex flex-col h-full items-center">
|
||||
<h1 class="hidden md:block text-[40px] tracking-tight leading-[72px] mb-6">
|
||||
<InfoIcon size="40" class="inline-block -mt-2 mr-2" />
|
||||
About
|
||||
</h1>
|
||||
<p class="mt-6 text-transition" style="--delay: {1 * multiplier}ms">
|
||||
You know what sucks? File converters! They're usually riddled with ads,
|
||||
and take an ungodly amount of time to complete. <b
|
||||
>So we made a better one!</b
|
||||
>
|
||||
</p>
|
||||
<p class="mt-4 text-transition" style="--delay: {2 * multiplier}ms">
|
||||
VERT is a file converter that's open source, completely ad free, and
|
||||
much much faster than you're used to. All the converting is done on your
|
||||
device, which makes it both private and very speedy. And it of course
|
||||
has a beautiful UI! ✨
|
||||
</p>
|
||||
|
||||
<h2
|
||||
class="font-display text-3xl mt-12 text-transition"
|
||||
style="--delay: {3 * multiplier}ms"
|
||||
<div
|
||||
class="w-full max-w-[1280px] flex flex-col md:flex-row gap-4 p-4 md:px-4 md:py-0"
|
||||
>
|
||||
🖼️ supported formats
|
||||
</h2>
|
||||
<p class="mt-6 text-transition" style="--delay: {4 * multiplier}ms">
|
||||
As of right now, VERT supports image and audio conversion of most
|
||||
popular formats. We'll add support for more formats in the future!
|
||||
</p>
|
||||
<!-- Why VERT? & Credits -->
|
||||
<div class="flex flex-col gap-4 flex-1">
|
||||
<About.Why />
|
||||
</div>
|
||||
|
||||
<h2
|
||||
class="font-display text-3xl mt-12 text-transition"
|
||||
style="--delay: {5 * multiplier}ms"
|
||||
>
|
||||
🔗 resources
|
||||
</h2>
|
||||
<ul class="list-disc list-inside mt-6">
|
||||
<li class="text-transition" style="--delay: {6 * multiplier}ms">
|
||||
<a
|
||||
href="https://github.com/not-nullptr/VERT"
|
||||
class="text-foreground-highlight hover:underline">Source code</a
|
||||
> (hosted on GitHub, licensed under AGPL-3.0)
|
||||
</li>
|
||||
|
||||
<li class="text-transition" style="--delay: {7 * multiplier}ms">
|
||||
<a
|
||||
href="https://discord.gg/8XXZ7TFFrK"
|
||||
class="text-foreground-highlight hover:underline"
|
||||
>Discord server</a
|
||||
> (for chit-chat, suggestions, and support)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2
|
||||
class="font-display text-3xl mt-12 text-transition"
|
||||
style="--delay: {8 * multiplier}ms"
|
||||
>
|
||||
🎨 credits
|
||||
</h2>
|
||||
<div class="flex gap-4 mt-8">
|
||||
{#each credits as credit, i}
|
||||
<div class="hover:scale-105 w-56 transition-transform">
|
||||
<div
|
||||
class="border-2 credit-transition border-solid border-foreground-muted-alt rounded-2xl overflow-hidden"
|
||||
style="--delay: {i * 50 + multiplier * 9}ms;"
|
||||
>
|
||||
<a class="w-48" href={credit.url} target="_blank">
|
||||
<img src={credit.avatar} alt="{credit.name}'s avatar" />
|
||||
<div class="text-center py-4 px-2">
|
||||
<p class="font-display text-xl">{credit.name}</p>
|
||||
<p class="text-sm text-foreground-muted mt-2">
|
||||
{credit.description}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
<!-- Resources & Donate to VERT -->
|
||||
<div class="flex flex-col gap-4 flex-1">
|
||||
<About.Resources />
|
||||
<About.Credits {mainContribs} {ghContribs} />
|
||||
<!-- <About.Donate /> -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="text-foreground-muted text-base mt-10 text-transition"
|
||||
style="--delay: {10 * multiplier}ms"
|
||||
>
|
||||
(obviously inspired by <a
|
||||
href="https://cobalt.tools"
|
||||
class="hover:underline">cobalt.tools</a
|
||||
>)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes credit-transition {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(60px);
|
||||
filter: blur(18px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes text-transition {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(60px);
|
||||
filter: blur(18px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
.credit-transition {
|
||||
animation: credit-transition 750ms var(--transition) var(--delay)
|
||||
forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.text-transition {
|
||||
animation: text-transition 750ms var(--transition) var(--delay) forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,568 +1,180 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import { blur, duration, flip } from "$lib/animation";
|
||||
import ConversionPanel from "$lib/components/functional/ConversionPanel.svelte";
|
||||
import Dropdown from "$lib/components/functional/Dropdown.svelte";
|
||||
import ProgressiveBlur from "$lib/components/visual/effects/ProgressiveBlur.svelte";
|
||||
import Uploader from "$lib/components/functional/Uploader.svelte";
|
||||
import Panel from "$lib/components/visual/Panel.svelte";
|
||||
import ProgressBar from "$lib/components/visual/ProgressBar.svelte";
|
||||
import { converters } from "$lib/converters";
|
||||
import type { Converter } from "$lib/converters/converter.svelte";
|
||||
import { log } from "$lib/logger";
|
||||
import { files } from "$lib/store/index.svelte";
|
||||
import type { VertFile } from "$lib/types";
|
||||
import clsx from "clsx";
|
||||
import { ArrowRight, Disc2Icon, FileAudioIcon, XIcon } from "lucide-svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import {
|
||||
fade,
|
||||
type EasingFunction,
|
||||
type TransitionConfig,
|
||||
} from "svelte/transition";
|
||||
files,
|
||||
gradientColor,
|
||||
showGradient,
|
||||
} from "$lib/store/index.svelte";
|
||||
import { VertFile } from "$lib/types";
|
||||
import {
|
||||
AudioLines,
|
||||
DownloadIcon,
|
||||
FileMusicIcon,
|
||||
FileQuestionIcon,
|
||||
ImageIcon,
|
||||
ImageOffIcon,
|
||||
RotateCwIcon,
|
||||
XIcon,
|
||||
} from "lucide-svelte";
|
||||
|
||||
const { data } = $props();
|
||||
|
||||
const reversedFiles = $derived(files.files.slice().reverse());
|
||||
|
||||
let finisheds = $state(
|
||||
Array.from({ length: files.files.length }, () => false),
|
||||
);
|
||||
|
||||
let processings = $state<boolean[]>([]);
|
||||
|
||||
const convertersRequired = $derived.by(() => {
|
||||
const required: Converter[] = [];
|
||||
for (let i = 0; i < files.files.length; i++) {
|
||||
const file = files.files[i];
|
||||
const converter = converters.find(
|
||||
(c) =>
|
||||
c.supportedFormats.includes(file.from.toLowerCase()) &&
|
||||
c.supportedFormats.includes(file.to.toLowerCase()),
|
||||
);
|
||||
if (!converter) throw new Error("No converter found");
|
||||
required.push(converter);
|
||||
$effect(() => {
|
||||
if (files.files.length === 1 && files.files[0].blobUrl) {
|
||||
showGradient.set(false);
|
||||
} else {
|
||||
showGradient.set(true);
|
||||
}
|
||||
|
||||
// Set gradient color depending on the file types
|
||||
// TODO: if more file types added, add a "fileType" property to the file object
|
||||
const allAudio = files.files.every(
|
||||
(file) => file.converter?.name === "ffmpeg",
|
||||
);
|
||||
const allImages = files.files.every(
|
||||
(file) => file.converter?.name !== "ffmpeg",
|
||||
);
|
||||
|
||||
if (files.files.length === 0 || (!allAudio && !allImages)) {
|
||||
gradientColor.set("");
|
||||
} else {
|
||||
gradientColor.set(allAudio ? "purple" : "blue");
|
||||
}
|
||||
return Array.from(new Set(required));
|
||||
});
|
||||
|
||||
const multipleConverters = $derived(convertersRequired.length > 1);
|
||||
|
||||
const noMultConverter = $derived(
|
||||
multipleConverters ? null : convertersRequired[0],
|
||||
);
|
||||
|
||||
const allConvertersReady = $derived(
|
||||
convertersRequired.every((c) => c.ready),
|
||||
);
|
||||
|
||||
let disabled = $derived(files.files.some((f) => !f.result));
|
||||
|
||||
onMount(() => {
|
||||
finisheds.forEach((_, i) => {
|
||||
const duration = 575 + i * 50 - 32;
|
||||
setTimeout(() => {
|
||||
finisheds[i] = true;
|
||||
}, duration);
|
||||
});
|
||||
});
|
||||
|
||||
const convertAll = async () => {
|
||||
const perf = performance.now();
|
||||
files.files.forEach((f) => (f.result = null));
|
||||
const promises: Promise<void>[] = [];
|
||||
for (let i = 0; i < files.files.length; i++) {
|
||||
promises.push(
|
||||
(async (i) => {
|
||||
window.plausible("Convert", {
|
||||
props: {
|
||||
"Convert from": files.files[i].from.toLowerCase(),
|
||||
"Convert to": files.files[i].to.toLowerCase(),
|
||||
Conversion: `${files.files[i].from.toLowerCase()} to ${files.files[i].to.toLowerCase()}`,
|
||||
},
|
||||
});
|
||||
await convert(files.files[i], i);
|
||||
})(i),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
const ms = performance.now() - perf;
|
||||
const seconds = (ms / 1000).toFixed(2);
|
||||
log(["converter"], `converted all files in ${seconds}s`);
|
||||
};
|
||||
|
||||
const convert = async (file: VertFile, index: number) => {
|
||||
file.progress = 0;
|
||||
processings[index] = true;
|
||||
await file.convert();
|
||||
processings[index] = false;
|
||||
};
|
||||
|
||||
const downloadAll = async () => {
|
||||
const dlFiles: any[] = [];
|
||||
for (let i = 0; i < files.files.length; i++) {
|
||||
const file = files.files[i];
|
||||
const result = file.result;
|
||||
if (!result) {
|
||||
console.error("No result found");
|
||||
continue;
|
||||
}
|
||||
dlFiles.push({
|
||||
name: file.file.name.replace(/\.[^/.]+$/, "") + file.to,
|
||||
lastModified: Date.now(),
|
||||
input: await result.file.arrayBuffer(),
|
||||
});
|
||||
}
|
||||
if (files.files.length === 0) return;
|
||||
if (files.files.length === 1) {
|
||||
// download the image only
|
||||
const blob = URL.createObjectURL(
|
||||
new Blob([dlFiles[0].input], {
|
||||
type: files.files[0].to.slice(1),
|
||||
}),
|
||||
);
|
||||
const a = document.createElement("a");
|
||||
a.href = blob;
|
||||
a.download = `VERT-Converted_${new Date().toISOString()}${
|
||||
files.files[0].to
|
||||
}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(blob);
|
||||
a.remove();
|
||||
return;
|
||||
}
|
||||
const { downloadZip } = await import("client-zip");
|
||||
const blob = await downloadZip(dlFiles, "converted.zip").blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `VERT-Converted_${new Date().toISOString()}.zip`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
a.remove();
|
||||
};
|
||||
|
||||
const deleteAll = () => {
|
||||
files.files = [];
|
||||
goto("/");
|
||||
};
|
||||
|
||||
export const progBlur = (
|
||||
_: HTMLElement,
|
||||
config:
|
||||
| Partial<{
|
||||
duration: number;
|
||||
easing: EasingFunction;
|
||||
}>
|
||||
| undefined,
|
||||
dir: {
|
||||
direction: "in" | "out" | "both";
|
||||
},
|
||||
): TransitionConfig => {
|
||||
const prefersReducedMotion = window.matchMedia(
|
||||
"(prefers-reduced-motion: reduce)",
|
||||
).matches;
|
||||
if (!config) config = {};
|
||||
if (!config.duration) config.duration = 300;
|
||||
if (!config.easing) config.easing = quintOut;
|
||||
return {
|
||||
duration: prefersReducedMotion ? 0 : config?.duration || 300,
|
||||
css: (t) => {
|
||||
return "--blur-amount: " + (dir.direction !== "in" ? t : 1 - t);
|
||||
},
|
||||
easing: config?.easing,
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Your Conversions</title>
|
||||
<meta name="title" content="Your Conversions — VERT.sh" />
|
||||
<meta property="og:title" content="Your Conversions — VERT.sh" />
|
||||
<meta property="twitter:title" content="Your Conversions — VERT.sh" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="grid grid-cols-1 grid-rows-1 w-full">
|
||||
{#if files.files.length === 0}
|
||||
<p class="text-foreground-muted col-start-1 row-start-1 text-center">
|
||||
No files uploaded. Head to the Upload tab to begin!
|
||||
</p>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col gap-4 w-full col-start-1 row-start-1"
|
||||
out:blur={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
blurMultiplier: 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="w-full p-4 max-w-screen-lg border-solid flex-col border-2 rounded-2xl border-foreground-muted-alt flex flex-shrink-0"
|
||||
{#snippet fileItem(file: VertFile, index: number)}
|
||||
{@const isAudio = file.converter?.name === "ffmpeg"}
|
||||
<Panel class="p-5 flex flex-col min-w-0 gap-4 relative">
|
||||
<div class="flex-shrink-0 h-8 w-full flex items-center gap-2">
|
||||
{#if !file.converter}
|
||||
<FileQuestionIcon size="24" class="flex-shrink-0" />
|
||||
{:else if isAudio}
|
||||
<AudioLines size="24" class="flex-shrink-0" />
|
||||
{:else}
|
||||
<ImageIcon size="24" class="flex-shrink-0" />
|
||||
{/if}
|
||||
<div class="flex-grow overflow-hidden">
|
||||
{#if file.processing}
|
||||
<ProgressBar
|
||||
min={0}
|
||||
max={100}
|
||||
progress={file.converter?.reportsProgress
|
||||
? file.progress
|
||||
: null}
|
||||
/>
|
||||
{:else}
|
||||
<h2
|
||||
class="text-xl font-body overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{file.name}
|
||||
</h2>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class="flex-shrink-0 w-8 rounded-full hover:bg-panel-alt h-full flex items-center justify-center"
|
||||
onclick={() =>
|
||||
(files.files = files.files.filter((_, i) => i !== index))}
|
||||
>
|
||||
<h2 class="font-bold text-xl mb-1">Options</h2>
|
||||
<div class="flex flex-col w-full gap-4 mt-2">
|
||||
<div class="flex flex-col gap-3 w-fit">
|
||||
<h3>Set all target formats</h3>
|
||||
<div class="grid grid-rows-1 grid-cols-1">
|
||||
{#if !multipleConverters && noMultConverter}
|
||||
<div
|
||||
transition:blur={{
|
||||
blurMultiplier: 8,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
class="row-start-1 col-start-1 w-fit"
|
||||
>
|
||||
<Dropdown
|
||||
options={noMultConverter.supportedFormats}
|
||||
onselect={(o) => {
|
||||
// files.conversionTypes = Array.from(
|
||||
// { length: files.files.length },
|
||||
// () => o,
|
||||
// );
|
||||
|
||||
files.files.forEach((file) => {
|
||||
file.result = null;
|
||||
file.to = o;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="italic w-fit text-foreground-muted-alt flex items-center row-start-1 col-start-1"
|
||||
transition:blur={{
|
||||
blurMultiplier: 8,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
>
|
||||
The listed files require different
|
||||
converters, so you can't set them in bulk.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 sm:grid-cols-3 mt-4">
|
||||
<button
|
||||
onclick={convertAll}
|
||||
class={clsx("btn flex-grow", {
|
||||
"btn-highlight":
|
||||
disabled && !processings.some((p) => p),
|
||||
})}
|
||||
disabled={!allConvertersReady ||
|
||||
processings.some((p) => p)}
|
||||
>
|
||||
{#if allConvertersReady}
|
||||
Convert {files.files.length > 1 ? "All" : ""}
|
||||
{:else}
|
||||
Loading...
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
onclick={downloadAll}
|
||||
class={clsx("btn flex-grow", {
|
||||
"btn-highlight": !disabled,
|
||||
})}
|
||||
{disabled}
|
||||
>Download {files.files.length > 1 ? "All" : ""}</button
|
||||
>
|
||||
<button
|
||||
onclick={deleteAll}
|
||||
disabled={processings.some((p) => p)}
|
||||
class="btn flex-grow"
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full gap-4 grid md:grid-cols-2">
|
||||
{#each reversedFiles as file, i (file.id)}
|
||||
{@const converter = (() => {
|
||||
return converters.find((c) =>
|
||||
c.supportedFormats.includes(
|
||||
file.from.toLowerCase(),
|
||||
),
|
||||
);
|
||||
})()}
|
||||
<div
|
||||
class="relative"
|
||||
animate:flip={{ duration, easing: quintOut }}
|
||||
out:blur={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
blurMultiplier: 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={clsx(
|
||||
"flex relative flex-shrink-0 items-center w-full rounded-xl h-72",
|
||||
{
|
||||
"initial-fade": !finisheds[i],
|
||||
},
|
||||
)}
|
||||
style="--delay: {i * 50}ms; z-index: {files.files
|
||||
.length - i}; border: solid 2px {file.result
|
||||
? 'var(--accent-bg)'
|
||||
: 'var(--fg-muted-alt)'}; transition: border 1000ms ease; transition: filter {duration}ms var(--transition), transform {duration}ms var(--transition);"
|
||||
>
|
||||
<div
|
||||
class="flex h-full flex-col items-center w-full z-50 relative"
|
||||
>
|
||||
<div class="w-full flex-shrink-0">
|
||||
<div
|
||||
class={clsx(
|
||||
"py-3 dynadark:[--transparency:50%] [--transparency:25%] px-4 w-full flex transition-colors duration-300 flex-shrink text-left border-b-2 border-solid border-foreground-muted-alt rounded-tl-[9.5px] rounded-tr-[10px] overflow-hidden",
|
||||
{
|
||||
"text-accent-foreground":
|
||||
file.result,
|
||||
"text-foreground": !file.result,
|
||||
},
|
||||
)}
|
||||
style="background-color: color-mix(in srgb, var(--{file.result
|
||||
? 'accent-bg'
|
||||
: 'bg'}), transparent var(--transparency)); backdrop-filter: blur({data.isFirefox
|
||||
? 0
|
||||
: 18}px);"
|
||||
>
|
||||
<div
|
||||
class="w-full grid grid-cols-1 grid-rows-1"
|
||||
>
|
||||
{#if processings[files.files.length - i - 1]}
|
||||
<div
|
||||
class="w-full row-start-1 col-start-1 h-full flex items-center pr-4"
|
||||
transition:blur={{
|
||||
blurMultiplier: 6,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
scale: {
|
||||
start: 0.9,
|
||||
end: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ProgressBar
|
||||
min={0}
|
||||
max={100}
|
||||
progress={file.converter
|
||||
?.reportsProgress
|
||||
? file.result
|
||||
? 100
|
||||
: file.progress
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<h3
|
||||
class="row-start-1 col-start-1 whitespace-nowrap overflow-hidden text-ellipsis font-medium"
|
||||
transition:blur={{
|
||||
blurMultiplier: 6,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
scale: {
|
||||
start: 0.9,
|
||||
end: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{file.file.name}
|
||||
</h3>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
onclick={() => {
|
||||
// delete the file from the list
|
||||
files.files =
|
||||
files.files.filter(
|
||||
(f) => f !== file,
|
||||
);
|
||||
if (files.files.length === 0)
|
||||
goto("/");
|
||||
}}
|
||||
class="ml-2 mr-1 flex-shrink-0"
|
||||
>
|
||||
<XIcon size="24" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex gap-3 justify-normal flex-grow w-full h-full"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-end gap-3 w-full"
|
||||
>
|
||||
<div
|
||||
class="flex items-end gap-3 w-full h-full px-5"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center gap-3 w-full pb-4"
|
||||
>
|
||||
{#if converter && converter.supportedFormats.includes(file.from.toLowerCase())}
|
||||
<span>from</span>
|
||||
<span
|
||||
class="py-2 px-3 font-display bg-foreground text-background rounded-xl"
|
||||
>{file.from}</span
|
||||
>
|
||||
<span>to</span>
|
||||
<div class="inline-flex">
|
||||
<Dropdown
|
||||
options={converter.supportedFormats}
|
||||
bind:selected={files
|
||||
.files[
|
||||
files.files
|
||||
.length -
|
||||
i -
|
||||
1
|
||||
].to}
|
||||
onselect={() => {
|
||||
file.result =
|
||||
null;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<span
|
||||
class="py-2 px-3 font-display bg-foreground-failure text-white rounded-xl"
|
||||
>{file.from}</span
|
||||
>
|
||||
|
||||
<span
|
||||
class="text-foreground-failure"
|
||||
>
|
||||
is not supported!
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div
|
||||
class="hidden lg:flex gap-4 w-full"
|
||||
>
|
||||
<button
|
||||
class="btn flex-grow flex-shrink-0"
|
||||
onclick={() => convert(file)}
|
||||
>
|
||||
Convert
|
||||
</button>
|
||||
<button
|
||||
class="btn flex-grow flex-shrink-0"
|
||||
disabled={!file.result}
|
||||
onclick={file.download}
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if converter && converter.supportedFormats.includes(file.from.toLowerCase())}
|
||||
<!-- god knows why, but setting opacity > 0.98 causes a z-ordering issue in firefox ??? -->
|
||||
<div
|
||||
class="absolute top-[0px] -z-50 left-0 w-full h-full opacity-[0.98] rounded-xl overflow-hidden"
|
||||
>
|
||||
{#if file.blobUrl}
|
||||
<div
|
||||
class="bg-cover bg-center w-full h-full"
|
||||
style="background-image: url({file.blobUrl})"
|
||||
in:blur={{
|
||||
blurMultiplier: 24,
|
||||
scale: {
|
||||
start: 1.1,
|
||||
end: 1,
|
||||
},
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
class="absolute left-0 top-0 pt-[50px] h-full w-full"
|
||||
transition:progBlur={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
>
|
||||
<ProgressiveBlur
|
||||
direction="bottom"
|
||||
endIntensity={64}
|
||||
iterations={8}
|
||||
fadeTo="var(--bg-transparent)"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="w-full h-full flex items-center justify-center"
|
||||
>
|
||||
<FileAudioIcon
|
||||
size="96"
|
||||
strokeWidth="1.5"
|
||||
color="var(--fg)"
|
||||
opacity="0.9"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
<XIcon size="24" class="text-muted" />
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
{#if !file.converter}
|
||||
<div
|
||||
class="h-full flex flex-col text-center justify-center text-red-600"
|
||||
>
|
||||
<p class="font-body font-bold">We can't convert this file.</p>
|
||||
<p class="font-normal">
|
||||
Only image and audio files are supported
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex flex-row justify-between">
|
||||
<div
|
||||
class="flex gap-4 w-full h-[152px] overflow-hidden relative"
|
||||
>
|
||||
<div class="w-1/2 h-full overflow-hidden rounded-xl">
|
||||
{#if file.blobUrl}
|
||||
<img
|
||||
class="object-cover w-full h-full"
|
||||
src={file.blobUrl}
|
||||
alt={file.name}
|
||||
/>
|
||||
{:else}
|
||||
<div
|
||||
class="w-full h-full flex items-center justify-center text-black"
|
||||
style="background: var({isAudio
|
||||
? '--bg-gradient-purple-alt'
|
||||
: '--bg-gradient-blue-alt'})"
|
||||
>
|
||||
{#if isAudio}
|
||||
<FileMusicIcon size="56" />
|
||||
{:else}
|
||||
<ImageOffIcon size="56" />
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-16 right-0 mr-4 pl-2 h-[calc(100%-83px)] w-[calc(50%-38px)] pr-4 pb-1 flex items-center justify-center aspect-square"
|
||||
>
|
||||
<div
|
||||
class="w-[122px] h-fit flex flex-col gap-2 items-center justify-center"
|
||||
>
|
||||
<Dropdown
|
||||
options={file.converter?.supportedFormats || []}
|
||||
bind:selected={file.to}
|
||||
onselect={() => file.result && (file.result = null)}
|
||||
/>
|
||||
<div class="w-full flex items-center justify-between">
|
||||
<button
|
||||
class="btn p-0 w-14 h-14 text-black {isAudio
|
||||
? 'bg-accent-purple'
|
||||
: 'bg-accent-blue'}"
|
||||
disabled={!files.ready}
|
||||
onclick={file.convert}
|
||||
>
|
||||
<RotateCwIcon size="24" />
|
||||
</button>
|
||||
<button
|
||||
class="btn p-0 w-14 h-14"
|
||||
onclick={file.download}
|
||||
disabled={!file.result}
|
||||
>
|
||||
<DownloadIcon size="24" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</Panel>
|
||||
{/snippet}
|
||||
|
||||
<div class="flex flex-col justify-center items-center gap-8 -mt-4 px-4 md:p-0">
|
||||
<div class="max-w-[778px] w-full">
|
||||
<ConversionPanel />
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full max-w-[778px] grid grid-cols-1 md:grid-cols-2 auto-rows-[240px] gap-4 md:p-0"
|
||||
>
|
||||
{#each files.files as file, i (file.id)}
|
||||
{#if files.files.length >= 2 && i === 1}
|
||||
<Uploader
|
||||
class="w-full h-full col-start-1 row-start-1 md:col-start-2"
|
||||
/>
|
||||
{/if}
|
||||
{@render fileItem(file, i)}
|
||||
{#if files.files.length < 2}
|
||||
<Uploader class="w-full h-full" />
|
||||
{/if}
|
||||
{/each}
|
||||
{#if files.files.length === 0}
|
||||
<Uploader class="w-full h-full" />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes initial-transition {
|
||||
0% {
|
||||
transform: translateY(50px);
|
||||
filter: blur(16px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.initial-fade {
|
||||
animation: initial-transition 600ms var(--delay) var(--transition);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.initial-fade.finished {
|
||||
animation: none;
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
@keyframes processing {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
filter: blur(0px);
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
filter: blur(4px);
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
filter: blur(0px);
|
||||
animation-timing-function: ease-in-out;
|
||||
}
|
||||
}
|
||||
|
||||
.processing {
|
||||
animation: processing 2000ms infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
transition:
|
||||
filter 500ms var(--transition),
|
||||
transform 500ms var(--transition);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
<script lang="ts">
|
||||
import * as Settings from "$lib/sections/settings";
|
||||
import { addToast } from "$lib/store/ToastProvider";
|
||||
import { SettingsIcon } from "lucide-svelte";
|
||||
|
||||
function showToast() {
|
||||
addToast("success", "This is a success message!");
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col h-full items-center">
|
||||
<h1 class="hidden md:block text-[40px] tracking-tight leading-[72px] mb-6">
|
||||
<SettingsIcon size="40" class="inline-block -mt-2 mr-2" />
|
||||
Settings
|
||||
</h1>
|
||||
|
||||
<div
|
||||
class="w-full max-w-[1280px] flex flex-col md:flex-row gap-4 p-4 md:px-4 md:py-0"
|
||||
>
|
||||
<!-- Why VERT? & Credits -->
|
||||
<div class="flex flex-col gap-4 flex-1">
|
||||
<Settings.Conversion />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-4 flex-1">
|
||||
<Settings.Appearance />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button onclick={showToast}>Show Toast</button>
|
||||
</div>
|
|
@ -0,0 +1,2 @@
|
|||
/*! coi-serviceworker v0.1.7 - Guido Zuidhof and contributors, licensed under MIT */
|
||||
let coepCredentialless=!1;"undefined"==typeof window?(self.addEventListener("install",(()=>self.skipWaiting())),self.addEventListener("activate",(e=>e.waitUntil(self.clients.claim()))),self.addEventListener("message",(e=>{e.data&&("deregister"===e.data.type?self.registration.unregister().then((()=>self.clients.matchAll())).then((e=>{e.forEach((e=>e.navigate(e.url)))})):"coepCredentialless"===e.data.type&&(coepCredentialless=e.data.value))})),self.addEventListener("fetch",(function(e){const o=e.request;if("only-if-cached"===o.cache&&"same-origin"!==o.mode)return;const s=coepCredentialless&&"no-cors"===o.mode?new Request(o,{credentials:"omit"}):o;e.respondWith(fetch(s).then((e=>{if(0===e.status)return e;const o=new Headers(e.headers);return o.set("Cross-Origin-Embedder-Policy",coepCredentialless?"credentialless":"require-corp"),coepCredentialless||o.set("Cross-Origin-Resource-Policy","cross-origin"),o.set("Cross-Origin-Opener-Policy","same-origin"),new Response(e.body,{status:e.status,statusText:e.statusText,headers:o})})).catch((e=>console.error(e))))}))):(()=>{const e=window.sessionStorage.getItem("coiReloadedBySelf");window.sessionStorage.removeItem("coiReloadedBySelf");const o="coepdegrade"==e,s={shouldRegister:()=>!e,shouldDeregister:()=>!1,coepCredentialless:()=>!0,coepDegrade:()=>!0,doReload:()=>window.location.reload(),quiet:!1,...window.coi},r=navigator,t=r.serviceWorker&&r.serviceWorker.controller;t&&!window.crossOriginIsolated&&window.sessionStorage.setItem("coiCoepHasFailed","true");const i=window.sessionStorage.getItem("coiCoepHasFailed");if(t){const e=s.coepDegrade()&&!(o||window.crossOriginIsolated);r.serviceWorker.controller.postMessage({type:"coepCredentialless",value:!(e||i&&s.coepDegrade())&&s.coepCredentialless()}),e&&(!s.quiet&&console.log("Reloading page to degrade COEP."),window.sessionStorage.setItem("coiReloadedBySelf","coepdegrade"),s.doReload("coepdegrade")),s.shouldDeregister()&&r.serviceWorker.controller.postMessage({type:"deregister"})}!1===window.crossOriginIsolated&&s.shouldRegister()&&(window.isSecureContext?r.serviceWorker?r.serviceWorker.register(window.document.currentScript.src).then((e=>{!s.quiet&&console.log("COOP/COEP Service Worker registered",e.scope),e.addEventListener("updatefound",(()=>{!s.quiet&&console.log("Reloading page to make use of updated COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","updatefound"),s.doReload()})),e.active&&!r.serviceWorker.controller&&(!s.quiet&&console.log("Reloading page to make use of COOP/COEP Service Worker."),window.sessionStorage.setItem("coiReloadedBySelf","notcontrolling"),s.doReload())}),(e=>{!s.quiet&&console.error("COOP/COEP Service Worker failed to register:",e)})):!s.quiet&&console.error("COOP/COEP Service Worker not registered, perhaps due to private mode."):!s.quiet&&console.log("COOP/COEP Service Worker not registered, a secure context is required."))})();
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.3 KiB |
After Width: | Height: | Size: 2.6 KiB |
|
@ -1,4 +1,4 @@
|
|||
import adapter from "svelte-adapter-bun";
|
||||
import adapter from "@sveltejs/adapter-static";
|
||||
import { vitePreprocess } from "@sveltejs/vite-plugin-svelte";
|
||||
|
||||
/** @type {import('@sveltejs/kit').Config} */
|
||||
|
|
|
@ -5,15 +5,38 @@ export default {
|
|||
content: ["./src/**/*.{html,js,svelte,ts}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--bg)",
|
||||
backgroundColor: {
|
||||
panel: "var(--bg-panel)",
|
||||
"panel-accented": "var(--bg-panel-accented)",
|
||||
separator: "var(--bg-separator)",
|
||||
button: "var(--bg-button)",
|
||||
"panel-alt": "var(--bg-button)",
|
||||
badge: "var(--bg-badge)",
|
||||
},
|
||||
borderColor: {
|
||||
separator: "var(--bg-separator)",
|
||||
button: "var(--bg-button)",
|
||||
},
|
||||
textColor: {
|
||||
foreground: "var(--fg)",
|
||||
"foreground-muted": "var(--fg-muted)",
|
||||
"foreground-muted-alt": "var(--fg-muted-alt)",
|
||||
"foreground-failure": "var(--fg-failure)",
|
||||
"foreground-highlight": "var(--fg-highlight)",
|
||||
"accent-background": "var(--accent-bg)",
|
||||
"accent-foreground": "var(--accent-fg)",
|
||||
muted: "var(--fg-muted)",
|
||||
"on-accent": "var(--fg-on-accent)",
|
||||
"on-badge": "var(--fg-on-badge)",
|
||||
},
|
||||
colors: {
|
||||
accent: "var(--accent)",
|
||||
"accent-alt": "var(--accent-alt)",
|
||||
"accent-pink": "var(--accent-pink)",
|
||||
"accent-pink-alt": "var(--accent-pink-alt)",
|
||||
"accent-red": "var(--accent-red)",
|
||||
"accent-red-alt": "var(--accent-red-alt)",
|
||||
"accent-purple-alt": "var(--accent-purple-alt)",
|
||||
"accent-purple": "var(--accent-purple)",
|
||||
"accent-blue": "var(--accent-blue)",
|
||||
"accent-blue-alt": "var(--accent-blue-alt)",
|
||||
},
|
||||
boxShadow: {
|
||||
panel: "var(--shadow-panel)",
|
||||
},
|
||||
fontFamily: {
|
||||
display: "var(--font-display)",
|
||||
|
@ -22,6 +45,9 @@ export default {
|
|||
blur: {
|
||||
xs: "2px",
|
||||
},
|
||||
borderRadius: {
|
||||
"2.5xl": "1.25rem",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { sveltekit } from "@sveltejs/kit/vite";
|
||||
import { defineConfig } from "vite";
|
||||
import svg from "@poppanator/sveltekit-svg";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
|
@ -17,6 +18,19 @@ export default defineConfig({
|
|||
});
|
||||
},
|
||||
},
|
||||
svg({
|
||||
includePaths: ["./src/lib/assets"],
|
||||
svgoOptions: {
|
||||
multipass: true,
|
||||
plugins: [
|
||||
{
|
||||
name: "preset-default",
|
||||
params: { overrides: { removeViewBox: false } },
|
||||
},
|
||||
{ name: "removeAttrs", params: { attrs: "(fill|stroke)" } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
optimizeDeps: {
|
||||
exclude: [
|
||||
|
|