Merge remote-tracking branch 'upstream/main' into file-name-options

This commit is contained in:
JovannMC 2024-11-14 14:16:07 +03:00
commit 50cc61d87d
No known key found for this signature in database
26 changed files with 580 additions and 78 deletions

21
.dockerignore Normal file
View File

@ -0,0 +1,21 @@
node_modules/
.git/
build/
dist/
.svelte-kit/
.output/
.vercel/
.vscode/
LICENSE
README.md
Dockerfile
docker-compose.yml
.npmrc
.prettier*
.gitignore
.env.*
.env
.DS_Store
Thumbs.db

1
.npmrc
View File

@ -1 +0,0 @@
engine-strict=true

3
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,3 @@
{
"css.customData": [".vscode/tailwind.json"]
}

55
.vscode/tailwind.json vendored Normal file
View File

@ -0,0 +1,55 @@
{
"version": 1.1,
"atDirectives": [
{
"name": "@tailwind",
"description": "Use the `@tailwind` directive to insert Tailwind's `base`, `components`, `utilities` and `screens` styles into your CSS.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#tailwind"
}
]
},
{
"name": "@apply",
"description": "Use the `@apply` directive to inline any existing utility classes into your own custom CSS. This is useful when you find a common utility pattern in your HTML that youd like to extract to a new component.",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#apply"
}
]
},
{
"name": "@responsive",
"description": "You can generate responsive variants of your own classes by wrapping their definitions in the `@responsive` directive:\n```css\n@responsive {\n .alert {\n background-color: #E53E3E;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#responsive"
}
]
},
{
"name": "@screen",
"description": "The `@screen` directive allows you to create media queries that reference your breakpoints by **name** instead of duplicating their values in your own CSS:\n```css\n@screen sm {\n /* ... */\n}\n```\n…gets transformed into this:\n```css\n@media (min-width: 640px) {\n /* ... */\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#screen"
}
]
},
{
"name": "@variants",
"description": "Generate `hover`, `focus`, `active` and other **variants** of your own utilities by wrapping their definitions in the `@variants` directive:\n```css\n@variants hover, focus {\n .btn-brand {\n background-color: #3182CE;\n }\n}\n```\n",
"references": [
{
"name": "Tailwind Documentation",
"url": "https://tailwindcss.com/docs/functions-and-directives#variants"
}
]
}
]
}

27
Dockerfile Normal file
View File

@ -0,0 +1,27 @@
FROM oven/bun AS builder
WORKDIR /app
ARG PUB_HOSTNAME
ARG PUB_PLAUSIBLE_URL
ENV PUB_HOSTNAME=${PUB_HOSTNAME}
ENV PUB_PLAUSIBLE_URL=${PUB_PLAUSIBLE_URL}
COPY package.json ./
RUN bun install
COPY . ./
RUN bun run build
FROM oven/bun:alpine
WORKDIR /app
COPY --from=builder /app/build ./
EXPOSE 3000
CMD [ "bun", "run", "start" ]

View File

@ -1,4 +1,4 @@
# VERT ![VERT](static/banner.png)
![Image of VERT on an old computer monitor](/src/lib/assets/VERT_Feature.webp) ![Image of VERT on an old computer monitor](/src/lib/assets/VERT_Feature.webp)
@ -6,10 +6,49 @@ VERT is a file conversion utility for the web that uses WebAssembly to convert f
VERT is built with Svelte & TypeScript (using [bun](https://bun.sh)). VERT is built with Svelte & TypeScript (using [bun](https://bun.sh)).
## Development ## Features
- Clone the project - `git clone https://github.com/not-nullptr/VERT.git` - Convert files directly on your device using WebAssembly
- Use [bun](https://bun.sh) to install the dependencies - `bun install` - No file size limits
- Copy the contents of `.env.example` into `.env` and make any changes (if wanted) - Supports multiple file formats
- Start a dev environment & make your changes - `bun run dev` - User-friendly interface built with Svelte
- Build and preview for production - `bun run build` & `bun run preview`
## Getting Started
### Prerequisites
Make sure you have the following installed:
- [Bun](https://bun.sh/)
### Installation
```sh
# Clone the repository
git clone https://github.com/yourusername/vert.git
cd vert
# Install dependencies
bun i
```
### Running Locally
To run the project locally, run `bun dev`.
This will start a development server. Open your browser and navigate to `http://localhost:5173` to see the application.
### Building for Production
Before building for production, make sure you create a `.env` file in the root of the project with the following content:
```sh
PUB_HOSTNAME=vert.sh # change to your domain
PUB_PLAUSIBLE_URL=https://plausible.example.com # can be empty if not using Plausible
```
To build the project for production, run `bun run build`
This will build the site to the `build` folder. You can then start the server with `bun ./build/index.js` and navigate to `http://localhost:3000` to see the application.
## License
This project is licensed under the AGPL-3.0 License, please see the [LICENSE](LICENSE) file for details.

11
docker-compose.yml Normal file
View File

@ -0,0 +1,11 @@
services:
vert:
container_name: vert
build:
context: .
args:
PUB_HOSTNAME: "vert.sh"
PUB_PLAUSIBLE_URL: "https://plausible.example.com"
restart: unless-stopped
ports:
- 3000:3000

View File

@ -24,6 +24,7 @@
"prettier": "^3.3.2", "prettier": "^3.3.2",
"prettier-plugin-svelte": "^3.2.6", "prettier-plugin-svelte": "^3.2.6",
"prettier-plugin-tailwindcss": "^0.6.5", "prettier-plugin-tailwindcss": "^0.6.5",
"sass": "^1.80.7",
"svelte": "^5.0.0", "svelte": "^5.0.0",
"svelte-check": "^4.0.0", "svelte-check": "^4.0.0",
"tailwindcss": "^3.4.9", "tailwindcss": "^3.4.9",
@ -41,6 +42,7 @@
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-svelte": "^0.456.0", "lucide-svelte": "^0.456.0",
"svelte-adapter-bun": "^0.5.2", "svelte-adapter-bun": "^0.5.2",
"typescript-cookie": "^1.0.6",
"wasm-vips": "^0.0.11" "wasm-vips": "^0.0.11"
} }
} }

View File

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head% %sveltekit.head%
</head> </head>
<body data-sveltekit-preload-data="hover"> <body data-sveltekit-preload-data="hover" class="%theme%">
<div style="display: contents">%sveltekit.body%</div> <div style="display: contents">%sveltekit.body%</div>
</body> </body>
</html> </html>

View File

@ -1,24 +1,16 @@
@import "tailwindcss/base"; @tailwind base;
@import "tailwindcss/components"; @tailwind components;
@import "tailwindcss/utilities"; @tailwind utilities;
@import url(@fontsource/lexend/400.css); @import url(@fontsource/lexend/400.css);
@import url(@fontsource/lexend/500.css); @import url(@fontsource/lexend/500.css);
@import url(@fontsource/azeret-mono/600.css); @import url(@fontsource/azeret-mono/600.css);
:root { :root {
--accent-bg: hsl(303, 73%, 81%);
--accent-fg: hsl(0, 0, 10%);
--font-body: "Lexend", system-ui, -apple-system, BlinkMacSystemFont, --font-body: "Lexend", system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
"Helvetica Neue", sans-serif; "Helvetica Neue", sans-serif;
--font-display: "Azeret Mono", var(--font-body); --font-display: "Azeret Mono", var(--font-body);
--bg: hsl(0, 0%, 100%);
--fg: hsl(0, 0%, 10%);
--fg-muted: hsl(0, 0%, 50%);
--fg-muted-alt: hsl(0, 0%, 75%);
--fg-highlight: hsl(303, 52%, 42%);
--fg-failure: hsl(0, 67%, 49%);
--transition: linear( --transition: linear(
0, 0,
0.006, 0.006,
@ -37,6 +29,50 @@
); );
} }
@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%);
}
@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%);
}
@media (prefers-color-scheme: dark) {
body {
@include dark;
}
}
@media (prefers-color-scheme: light) {
body {
@include light;
}
}
body.light {
@include light;
}
body.dark {
@include dark;
}
body { body {
@apply text-foreground bg-background font-body overflow-x-hidden; @apply text-foreground bg-background font-body overflow-x-hidden;
width: 100vw; width: 100vw;

16
src/hooks.server.ts Normal file
View File

@ -0,0 +1,16 @@
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: "strict",
});
theme = "";
}
const res = await resolve(event, {
transformPageChunk: ({ html }) => html.replace("%theme%", theme),
});
return res;
};

View File

@ -55,7 +55,7 @@ export const blur = (
origin: Combination< origin: Combination<
"top" | "bottom" | "left" | "right" | "center", "top" | "bottom" | "left" | "right" | "center",
"top" | "bottom" | "left" | "right" | "center" "top" | "bottom" | "left" | "right" | "center"
>; > & {};
}> }>
| undefined, | undefined,
dir: { dir: {

View File

@ -4,6 +4,9 @@
import { duration } from "$lib/animation"; import { duration } from "$lib/animation";
import { quintOut } from "svelte/easing"; import { quintOut } from "svelte/easing";
import type { Writable } from "svelte/store"; import type { Writable } from "svelte/store";
import clsx from "clsx";
import { browser } from "$app/environment";
import { onMount, tick } from "svelte";
interface Props { interface Props {
links: { links: {
@ -16,16 +19,25 @@
let { links, shouldGoBack = null }: Props = $props(); let { links, shouldGoBack = null }: Props = $props();
let hasLoaded = $state(false);
let navWidth = $state(1); let navWidth = $state(1);
let linkCount = $derived(links.length); let linkCount = $derived(links.length);
let activeLinkIndex = $derived( let activeLinkIndex = $derived(
links.findIndex((i) => i.activeMatch($page.url.pathname)), links.findIndex((i) => i.activeMatch($page.url.pathname)),
); );
onMount(async () => {
await tick();
setTimeout(() => {
hasLoaded = true;
}, 16);
});
</script> </script>
<div <div
bind:clientWidth={navWidth} bind:clientWidth={navWidth}
class="w-full flex bg-background relative h-16" class="w-full flex bg-background relative h-16 items-center"
> >
{#if activeLinkIndex !== -1} {#if activeLinkIndex !== -1}
<div <div
@ -33,12 +45,17 @@
style="width: {navWidth / linkCount - 8}px; left: {(navWidth / style="width: {navWidth / linkCount - 8}px; left: {(navWidth /
linkCount) * linkCount) *
activeLinkIndex + activeLinkIndex +
4}px; transition: {duration - 200}ms ease left;" 4}px; transition: {hasLoaded ? duration - 200 : 0}ms ease left;"
></div> ></div>
{/if} {/if}
{#each links as { name, url } (url)} {#each links as { name, url } (url)}
<a <a
class="w-1/2 px-2 h-[calc(100%-16px)] mt-2 flex items-center justify-center rounded-xl relative font-display overflow-hidden" class={clsx(
"w-1/2 px-2 ml-1 h-[calc(100%-8px)] mr-1 flex items-center justify-center rounded-xl relative overflow-hidden font-medium",
{
"bg-foreground": $page.url.pathname === url && !browser,
},
)}
href={url} href={url}
onclick={() => { onclick={() => {
if (shouldGoBack) { if (shouldGoBack) {
@ -55,7 +72,7 @@
<div class="grid grid-cols-1 grid-rows-1"> <div class="grid grid-cols-1 grid-rows-1">
{#key name} {#key name}
<span <span
class="mix-blend-difference invert col-start-1 row-start-1 text-center" class="mix-blend-difference invert dynadark:invert-0 col-start-1 row-start-1 text-center"
in:fly={{ in:fly={{
duration, duration,
easing: quintOut, easing: quintOut,

View File

@ -18,6 +18,7 @@ export class Converter {
* @param to The format to convert to. Includes the dot. * @param to The format to convert to. Includes the dot.
*/ */
public ready: boolean = $state(false); public ready: boolean = $state(false);
public async convert( public async convert(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
input: OmitBetterStrict<IFile, "extension">, input: OmitBetterStrict<IFile, "extension">,

View File

@ -3,6 +3,7 @@ import { Converter } from "./converter.svelte";
import type { OmitBetterStrict } from "$lib/types"; import type { OmitBetterStrict } from "$lib/types";
import { FFmpeg } from "@ffmpeg/ffmpeg"; import { FFmpeg } from "@ffmpeg/ffmpeg";
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import { log } from "$lib/logger";
export class FFmpegConverter extends Converter { export class FFmpegConverter extends Converter {
private ffmpeg: FFmpeg = null!; private ffmpeg: FFmpeg = null!;
@ -16,26 +17,27 @@ export class FFmpegConverter extends Converter {
".ogg", ".ogg",
".aac", ".aac",
".m4a", ".m4a",
".opus",
".wma", ".wma",
".m4a", ".m4a",
".amr", ".amr",
".ac3", ".ac3",
"alac", ".alac",
".aiff", ".aiff",
]; ];
constructor() { constructor() {
super(); super();
log(["converters", this.name], `created converter`);
if (!browser) return; if (!browser) return;
this.ffmpeg = new FFmpeg(); this.ffmpeg = new FFmpeg();
(async () => { (async () => {
const baseURL = "https://unpkg.com/@ffmpeg/core@latest/dist/esm"; const baseURL =
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm";
await this.ffmpeg.load({ await this.ffmpeg.load({
coreURL: `${baseURL}/ffmpeg-core.js`, coreURL: `${baseURL}/ffmpeg-core.js`,
wasmURL: `${baseURL}/ffmpeg-core.wasm`, wasmURL: `${baseURL}/ffmpeg-core.wasm`,
}); });
// this is just to cache the wasm and js for when we actually use it. we're not using this ffmpeg instance
this.ready = true; this.ready = true;
})(); })();
} }
@ -45,13 +47,29 @@ export class FFmpegConverter extends Converter {
to: string, to: string,
): Promise<IFile> { ): Promise<IFile> {
if (!to.startsWith(".")) to = `.${to}`; if (!to.startsWith(".")) to = `.${to}`;
// clone input.buffer const ffmpeg = new FFmpeg();
const baseURL =
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm";
await ffmpeg.load({
coreURL: `${baseURL}/ffmpeg-core.js`,
wasmURL: `${baseURL}/ffmpeg-core.wasm`,
});
const buf = new Uint8Array(input.buffer); const buf = new Uint8Array(input.buffer);
await this.ffmpeg.writeFile("input", buf); await ffmpeg.writeFile("input", buf);
await this.ffmpeg.exec(["-i", "input", "output" + to]); log(
const output = (await this.ffmpeg.readFile( ["converters", this.name],
`wrote ${input.name} to ffmpeg virtual fs`,
);
await ffmpeg.exec(["-i", "input", "output" + to]);
log(["converters", this.name], `executed ffmpeg command`);
const output = (await ffmpeg.readFile(
"output" + to, "output" + to,
)) as unknown as Uint8Array; )) as unknown as Uint8Array;
log(
["converters", this.name],
`read ${input.name.split(".").slice(0, -1).join(".") + to} from ffmpeg virtual fs`,
);
ffmpeg.terminate();
return { return {
...input, ...input,
buffer: output.buffer, buffer: output.buffer,

View File

@ -3,6 +3,7 @@ import { Converter } from "./converter.svelte";
import VipsWorker from "$lib/workers/vips?worker"; import VipsWorker from "$lib/workers/vips?worker";
import { browser } from "$app/environment"; import { browser } from "$app/environment";
import type { WorkerMessage, OmitBetterStrict } from "$lib/types"; import type { WorkerMessage, OmitBetterStrict } from "$lib/types";
import { log } from "$lib/logger";
export class VipsConverter extends Converter { export class VipsConverter extends Converter {
private worker: Worker = browser ? new VipsWorker() : null!; private worker: Worker = browser ? new VipsWorker() : null!;
@ -30,6 +31,7 @@ export class VipsConverter extends Converter {
constructor() { constructor() {
super(); super();
log(["converters", this.name], `created converter`);
if (!browser) return; if (!browser) return;
this.worker.onmessage = (e) => { this.worker.onmessage = (e) => {
const message: WorkerMessage = e.data; const message: WorkerMessage = e.data;
@ -41,6 +43,7 @@ export class VipsConverter extends Converter {
input: OmitBetterStrict<IFile, "extension">, input: OmitBetterStrict<IFile, "extension">,
to: string, to: string,
): Promise<IFile> { ): Promise<IFile> {
log(["converters", this.name], `converting ${input.name} to ${to}`);
const res = await this.sendMessage({ const res = await this.sendMessage({
type: "convert", type: "convert",
input: input as unknown as IFile, input: input as unknown as IFile,
@ -48,6 +51,7 @@ export class VipsConverter extends Converter {
}); });
if (res.type === "finished") { if (res.type === "finished") {
log(["converters", this.name], `converted ${input.name} to ${to}`);
return res.output; return res.output;
} }

42
src/lib/logger/index.ts Normal file
View File

@ -0,0 +1,42 @@
import { browser } from "$app/environment";
const randomColorFromStr = (str: string) => {
// generate a pleasant color from a string, using HSL
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
const h = hash % 360;
return `hsl(${h}, 75%, 71%)`;
};
const whiteOrBlack = (hsl: string) => {
// determine if the text should be white or black based on the background color
const [, , l] = hsl
.replace("hsl(", "")
.replace(")", "")
.split(",")
.map((v) => parseInt(v));
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)
return console.log(prefixes.map((p) => `[${p}]`).join(" "), ...args);
const prefixesWithMeta = prefixes.map((p) => ({
prefix: p,
bgColor: randomColorFromStr(p),
textColor: whiteOrBlack(randomColorFromStr(p)),
}));
console.log(
`%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,
);
};

View File

@ -1,3 +1,4 @@
import { log } from "$lib/logger";
import type { IFile } from "$lib/types"; import type { IFile } from "$lib/types";
class Files { class Files {
@ -11,12 +12,18 @@ class Files {
result?: (IFile & { blobUrl: string; animating: boolean }) | null; result?: (IFile & { blobUrl: string; animating: boolean }) | null;
}[] }[]
>([]); >([]);
public beenToConverterPage = $state(false); }
public shouldShowAlert = $derived(
!this.beenToConverterPage && this.files.length > 0, class Theme {
); public dark = $state(false);
public toggle = () => {
this.dark = !this.dark;
log(["theme"], `set to ${this.dark ? "dark" : "light"}`);
};
} }
export const files = new Files(); export const files = new Files();
export const theme = new Theme();
export const outputFilenameOption = ["default", "original"]; export const outputFilenameOption = ["default", "original"];

View File

@ -1,14 +1,17 @@
<script lang="ts"> <script lang="ts">
import "../app.css"; import "../app.scss";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { blur, duration } from "$lib/animation"; import { blur, duration } from "$lib/animation";
import { quintOut } from "svelte/easing"; import { quintOut } from "svelte/easing";
import { files } from "$lib/store/index.svelte"; import { files, theme } from "$lib/store/index.svelte";
import Logo from "$lib/components/visual/svg/Logo.svelte"; import Logo from "$lib/components/visual/svg/Logo.svelte";
import featuredImage from "$lib/assets/VERT_Feature.webp"; import featuredImage from "$lib/assets/VERT_Feature.webp";
import { PUB_HOSTNAME, PUB_PLAUSIBLE_URL } from "$env/static/public"; import { PUB_HOSTNAME, PUB_PLAUSIBLE_URL } from "$env/static/public";
import FancyMenu from "$lib/components/functional/FancyMenu.svelte"; import FancyMenu from "$lib/components/functional/FancyMenu.svelte";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import { MoonIcon, SunIcon } from "lucide-svelte";
import { browser } from "$app/environment";
import { setCookie } from "typescript-cookie";
let { children, data } = $props(); let { children, data } = $props();
let shouldGoBack = writable(false); let shouldGoBack = writable(false);
@ -46,6 +49,23 @@
goto("/"); goto("/");
} }
}; };
$effect(() => {
if (!browser) return;
if (theme.dark) {
document.body.classList.add("dark");
document.body.classList.remove("light");
setCookie("theme", "dark", {
sameSite: "strict",
});
} else {
document.body.classList.add("light");
document.body.classList.remove("dark");
setCookie("theme", "light", {
sameSite: "strict",
});
}
});
</script> </script>
<svelte:head> <svelte:head>
@ -68,7 +88,7 @@
<div class="flex justify-center mb-5 lg:hidden"> <div class="flex justify-center mb-5 lg:hidden">
<a <a
href="/" href="/"
class="px-6 relative h-16 mr-3 justify-center items-center bg-accent-background fill-accent-foreground rounded-xl md:hidden flex" 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 w-24 items-center flex justify-center"> <div class="h-6 w-24 items-center flex justify-center">
<Logo /> <Logo />
@ -91,6 +111,85 @@
</div> </div>
<FancyMenu {links} {shouldGoBack} /> <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>
</div>
</div> </div>
<div class="w-full max-w-screen-lg grid grid-cols-1 grid-rows-1 relative"> <div class="w-full max-w-screen-lg grid grid-cols-1 grid-rows-1 relative">
{#key data.pathname} {#key data.pathname}
@ -105,10 +204,15 @@
start: !$shouldGoBack ? 250 : -250, start: !$shouldGoBack ? 250 : -250,
end: 0, end: 0,
}, },
y: {
start: 100,
end: 0,
},
scale: { scale: {
start: 0.75, start: 0.75,
end: 1, end: 1,
}, },
origin: "top center",
}} }}
out:blur={{ out:blur={{
duration, duration,
@ -118,10 +222,15 @@
start: 0, start: 0,
end: !$shouldGoBack ? -250 : 250, end: !$shouldGoBack ? -250 : 250,
}, },
y: {
start: 0,
end: 100,
},
scale: { scale: {
start: 1, start: 1,
end: 0.75, end: 0.75,
}, },
origin: "top center",
}} }}
> >
<div class="pb-20"> <div class="pb-20">

18
src/routes/+layout.ts Normal file
View File

@ -0,0 +1,18 @@
import { browser } from "$app/environment";
import { theme } from "$lib/store/index.svelte";
import { getCookie, setCookie } from "typescript-cookie";
export const load = ({ data }) => {
if (!browser) return;
const themeStr = getCookie("theme");
if (typeof themeStr === "undefined") {
theme.dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
setCookie("theme", theme.dark ? "dark" : "light", {
sameSite: "strict",
});
} else {
theme.dark = themeStr === "dark";
}
theme.dark = getCookie("theme") === "dark";
return data;
};

View File

@ -2,6 +2,7 @@
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import Uploader from "$lib/components/functional/Uploader.svelte"; import Uploader from "$lib/components/functional/Uploader.svelte";
import { converters } from "$lib/converters"; import { converters } from "$lib/converters";
import { log } from "$lib/logger/index.js";
import { files } from "$lib/store/index.svelte"; import { files } from "$lib/store/index.svelte";
import { Check } from "lucide-svelte"; import { Check } from "lucide-svelte";
@ -9,31 +10,79 @@
let ourFiles = $state<File[]>(); let ourFiles = $state<File[]>();
const runUpload = () => { 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),
);
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(
(blob) => {
resolve({
file: f,
from,
to,
blobUrl:
blob === null
? ""
: URL.createObjectURL(blob),
id: Math.random().toString(36).substring(2),
});
},
"image/jpeg",
0.75,
);
};
img.onerror = () => {
resolve({
file: f,
from,
to,
blobUrl: "",
id: Math.random().toString(36).substring(2),
});
};
},
);
});
let oldLen = files.files.length;
files.files = [ files.files = [
...files.files, ...files.files,
...(ourFiles || []).map((f, i) => { ...(await Promise.all(newFilePromises)).filter(
const from = "." + f.name.toLowerCase().split(".").slice(-1); (f) => typeof f !== "undefined",
const converter = converters.find((c) => ),
c.supportedFormats.includes(from),
);
const to =
converter?.supportedFormats.find((f) => f !== from) ||
converters[0].supportedFormats[0];
return {
file: f,
from,
to,
blobUrl: URL.createObjectURL(f),
id: Math.random().toString(36).substring(2),
};
}),
]; ];
let newLen = files.files.length;
log(["uploader"], `handled ${newLen - oldLen} files`);
ourFiles = []; ourFiles = [];
if (files.files.length > 0 && !files.beenToConverterPage) if (files.files.length > 0) goto("/convert");
goto("/convert");
}; };
</script> </script>
@ -83,7 +132,7 @@
<div class="[@media(max-height:768px)]:block mt-10 picker-fly"> <div class="[@media(max-height:768px)]:block mt-10 picker-fly">
<Uploader <Uploader
isMobile={data.isMobile} isMobile={data.isMobile || false}
bind:files={ourFiles} bind:files={ourFiles}
onupload={runUpload} onupload={runUpload}
acceptedFormats={[ acceptedFormats={[
@ -104,7 +153,7 @@
<!-- {@render sellingPoint("Very fast, all processing done on device")} <!-- {@render sellingPoint("Very fast, all processing done on device")}
{@render sellingPoint("No ads, and open source")} {@render sellingPoint("No ads, and open source")}
{@render sellingPoint("Beautiful and straightforward UI")} --> {@render sellingPoint("Beautiful and straightforward UI")} -->
{#each ["Very fast, all processing done on device", "No ads, and open source", "Beautiful and straightforward UI"] as text, i} {#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;"> <div class="fly-in" style="--delay: {i * 50}ms;">
{@render sellingPoint(text)} {@render sellingPoint(text)}
</div> </div>

View File

@ -54,27 +54,36 @@
🖼️ supported formats 🖼️ supported formats
</h2> </h2>
<p class="mt-6 text-transition" style="--delay: {4 * multiplier}ms"> <p class="mt-6 text-transition" style="--delay: {4 * multiplier}ms">
As of right now, VERT only supports image conversion of most popular As of right now, VERT supports image and audio conversion of most
formats. Don't worry though, as we'll add more options and support for popular formats. We'll add support for more formats in the future!
more formats in the future!
</p> </p>
<h2 <h2
class="font-display text-3xl mt-12 text-transition" class="font-display text-3xl mt-12 text-transition"
style="--delay: {5 * multiplier}ms" style="--delay: {5 * multiplier}ms"
> >
👨‍💻 source code 🔗 resources
</h2> </h2>
<p class="mt-6 text-transition" style="--delay: {6 * multiplier}ms"> <ul class="list-disc list-inside mt-6">
VERT is licensed under AGPL-3.0, and the source code can be found on <a <li class="text-transition" style="--delay: {6 * multiplier}ms">
class="hover:underline font-medium text-foreground-highlight" <a
href="https://github.com/not-nullptr/VERT">GitHub</a href="https://github.com/not-nullptr/VERT"
>. class="text-foreground-highlight hover:underline">Source code</a
</p> > (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 <h2
class="font-display text-3xl mt-12 text-transition" class="font-display text-3xl mt-12 text-transition"
style="--delay: {7 * multiplier}ms" style="--delay: {8 * multiplier}ms"
> >
🎨 credits 🎨 credits
</h2> </h2>
@ -83,7 +92,7 @@
<div class="hover:scale-105 w-56 transition-transform"> <div class="hover:scale-105 w-56 transition-transform">
<div <div
class="border-2 credit-transition border-solid border-foreground-muted-alt rounded-2xl overflow-hidden" class="border-2 credit-transition border-solid border-foreground-muted-alt rounded-2xl overflow-hidden"
style="--delay: {i * 50 + multiplier * 8}ms;" style="--delay: {i * 50 + multiplier * 9}ms;"
> >
<a class="w-48" href={credit.url} target="_blank"> <a class="w-48" href={credit.url} target="_blank">
<img src={credit.avatar} alt="{credit.name}'s avatar" /> <img src={credit.avatar} alt="{credit.name}'s avatar" />
@ -101,7 +110,7 @@
<p <p
class="text-foreground-muted text-base mt-10 text-transition" class="text-foreground-muted text-base mt-10 text-transition"
style="--delay: {9 * multiplier}ms" style="--delay: {10 * multiplier}ms"
> >
(obviously inspired by <a (obviously inspired by <a
href="https://cobalt.tools" href="https://cobalt.tools"

View File

@ -5,6 +5,7 @@
import ProgressiveBlur from "$lib/components/visual/effects/ProgressiveBlur.svelte"; import ProgressiveBlur from "$lib/components/visual/effects/ProgressiveBlur.svelte";
import { converters } from "$lib/converters"; import { converters } from "$lib/converters";
import type { Converter } from "$lib/converters/converter.svelte"; import type { Converter } from "$lib/converters/converter.svelte";
import { log } from "$lib/logger";
import { files, outputFilenameOption } from "$lib/store/index.svelte"; import { files, outputFilenameOption } from "$lib/store/index.svelte";
import clsx from "clsx"; import clsx from "clsx";
import { ArrowRight, XIcon } from "lucide-svelte"; import { ArrowRight, XIcon } from "lucide-svelte";
@ -74,6 +75,7 @@
}); });
const convertAll = async () => { const convertAll = async () => {
const perf = performance.now();
files.files.forEach((f) => (f.result = null)); files.files.forEach((f) => (f.result = null));
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
for (let i = 0; i < files.files.length; i++) { for (let i = 0; i < files.files.length; i++) {
@ -113,6 +115,9 @@
} }
await Promise.all(promises); await Promise.all(promises);
const ms = performance.now() - perf;
const seconds = (ms / 1000).toFixed(2);
log(["converter"], `converted all files in ${seconds}s`);
}; };
const downloadAll = async () => { const downloadAll = async () => {
@ -413,7 +418,7 @@
direction={isSm ? "bottom" : "right"} direction={isSm ? "bottom" : "right"}
endIntensity={128} endIntensity={128}
iterations={6} iterations={6}
fadeTo="rgba(255, 255, 255, 0.6)" fadeTo="var(--bg-transparent)"
/> />
</div> </div>
</div> </div>

BIN
static/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

@ -1,8 +1,8 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
import plugin from "tailwindcss/plugin";
export default { export default {
content: ["./src/**/*.{html,js,svelte,ts}"], content: ["./src/**/*.{html,js,svelte,ts}"],
theme: { theme: {
extend: { extend: {
colors: { colors: {
@ -25,5 +25,12 @@ export default {
}, },
}, },
plugins: [], plugins: [
plugin(function ({ addVariant }) {
addVariant("dynadark", [
"body:not(.light).dark &",
"@media (prefers-color-scheme: dark) { body:not(.light) &",
]);
}),
],
} satisfies Config; } satisfies Config;

View File

@ -26,4 +26,11 @@ export default defineConfig({
"@ffmpeg/util", "@ffmpeg/util",
], ],
}, },
css: {
preprocessorOptions: {
scss: {
api: "modern",
},
},
},
}); });