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)
@ -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)).
## Development
## Features
- Clone the project - `git clone https://github.com/not-nullptr/VERT.git`
- Use [bun](https://bun.sh) to install the dependencies - `bun install`
- Copy the contents of `.env.example` into `.env` and make any changes (if wanted)
- Start a dev environment & make your changes - `bun run dev`
- Build and preview for production - `bun run build` & `bun run preview`
- Convert files directly on your device using WebAssembly
- No file size limits
- Supports multiple file formats
- User-friendly interface built with Svelte
## 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-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",
@ -41,6 +42,7 @@
"clsx": "^2.1.1",
"lucide-svelte": "^0.456.0",
"svelte-adapter-bun": "^0.5.2",
"typescript-cookie": "^1.0.6",
"wasm-vips": "^0.0.11"
}
}

View File

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

View File

@ -1,24 +1,16 @@
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
@tailwind base;
@tailwind components;
@tailwind utilities;
@import url(@fontsource/lexend/400.css);
@import url(@fontsource/lexend/500.css);
@import url(@fontsource/azeret-mono/600.css);
:root {
--accent-bg: hsl(303, 73%, 81%);
--accent-fg: hsl(0, 0, 10%);
--font-body: "Lexend", system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans",
"Helvetica Neue", sans-serif;
--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(
0,
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 {
@apply text-foreground bg-background font-body overflow-x-hidden;
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<
"top" | "bottom" | "left" | "right" | "center",
"top" | "bottom" | "left" | "right" | "center"
>;
> & {};
}>
| undefined,
dir: {

View File

@ -4,6 +4,9 @@
import { duration } from "$lib/animation";
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: {
@ -16,16 +19,25 @@
let { links, shouldGoBack = null }: Props = $props();
let hasLoaded = $state(false);
let navWidth = $state(1);
let linkCount = $derived(links.length);
let activeLinkIndex = $derived(
links.findIndex((i) => i.activeMatch($page.url.pathname)),
);
onMount(async () => {
await tick();
setTimeout(() => {
hasLoaded = true;
}, 16);
});
</script>
<div
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}
<div
@ -33,12 +45,17 @@
style="width: {navWidth / linkCount - 8}px; left: {(navWidth /
linkCount) *
activeLinkIndex +
4}px; transition: {duration - 200}ms ease left;"
4}px; transition: {hasLoaded ? duration - 200 : 0}ms ease left;"
></div>
{/if}
{#each links as { name, url } (url)}
<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}
onclick={() => {
if (shouldGoBack) {
@ -55,7 +72,7 @@
<div class="grid grid-cols-1 grid-rows-1">
{#key name}
<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={{
duration,
easing: quintOut,

View File

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

View File

@ -3,6 +3,7 @@ import { Converter } from "./converter.svelte";
import type { OmitBetterStrict } from "$lib/types";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { browser } from "$app/environment";
import { log } from "$lib/logger";
export class FFmpegConverter extends Converter {
private ffmpeg: FFmpeg = null!;
@ -16,26 +17,27 @@ export class FFmpegConverter extends Converter {
".ogg",
".aac",
".m4a",
".opus",
".wma",
".m4a",
".amr",
".ac3",
"alac",
".alac",
".aiff",
];
constructor() {
super();
log(["converters", this.name], `created converter`);
if (!browser) return;
this.ffmpeg = new FFmpeg();
(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({
coreURL: `${baseURL}/ffmpeg-core.js`,
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;
})();
}
@ -45,13 +47,29 @@ export class FFmpegConverter extends Converter {
to: string,
): Promise<IFile> {
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);
await this.ffmpeg.writeFile("input", buf);
await this.ffmpeg.exec(["-i", "input", "output" + to]);
const output = (await this.ffmpeg.readFile(
await ffmpeg.writeFile("input", buf);
log(
["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,
)) as unknown as Uint8Array;
log(
["converters", this.name],
`read ${input.name.split(".").slice(0, -1).join(".") + to} from ffmpeg virtual fs`,
);
ffmpeg.terminate();
return {
...input,
buffer: output.buffer,

View File

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

View File

@ -1,14 +1,17 @@
<script lang="ts">
import "../app.css";
import "../app.scss";
import { goto } from "$app/navigation";
import { blur, duration } from "$lib/animation";
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 featuredImage from "$lib/assets/VERT_Feature.webp";
import { 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 { setCookie } from "typescript-cookie";
let { children, data } = $props();
let shouldGoBack = writable(false);
@ -46,6 +49,23 @@
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>
<svelte:head>
@ -68,7 +88,7 @@
<div class="flex justify-center mb-5 lg:hidden">
<a
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">
<Logo />
@ -91,6 +111,85 @@
</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>
</div>
</div>
<div class="w-full max-w-screen-lg grid grid-cols-1 grid-rows-1 relative">
{#key data.pathname}
@ -105,10 +204,15 @@
start: !$shouldGoBack ? 250 : -250,
end: 0,
},
y: {
start: 100,
end: 0,
},
scale: {
start: 0.75,
end: 1,
},
origin: "top center",
}}
out:blur={{
duration,
@ -118,10 +222,15 @@
start: 0,
end: !$shouldGoBack ? -250 : 250,
},
y: {
start: 0,
end: 100,
},
scale: {
start: 1,
end: 0.75,
},
origin: "top center",
}}
>
<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 Uploader from "$lib/components/functional/Uploader.svelte";
import { converters } from "$lib/converters";
import { log } from "$lib/logger/index.js";
import { files } from "$lib/store/index.svelte";
import { Check } from "lucide-svelte";
@ -9,31 +10,79 @@
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,
...(ourFiles || []).map((f, i) => {
const from = "." + f.name.toLowerCase().split(".").slice(-1);
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),
};
}),
...(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 && !files.beenToConverterPage)
goto("/convert");
if (files.files.length > 0) goto("/convert");
};
</script>
@ -83,7 +132,7 @@
<div class="[@media(max-height:768px)]:block mt-10 picker-fly">
<Uploader
isMobile={data.isMobile}
isMobile={data.isMobile || false}
bind:files={ourFiles}
onupload={runUpload}
acceptedFormats={[
@ -104,7 +153,7 @@
<!-- {@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 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;">
{@render sellingPoint(text)}
</div>

View File

@ -54,27 +54,36 @@
🖼️ supported formats
</h2>
<p class="mt-6 text-transition" style="--delay: {4 * multiplier}ms">
As of right now, VERT only supports image conversion of most popular
formats. Don't worry though, as we'll add more options and support for
more formats in the future!
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>
<h2
class="font-display text-3xl mt-12 text-transition"
style="--delay: {5 * multiplier}ms"
>
👨‍💻 source code
🔗 resources
</h2>
<p class="mt-6 text-transition" style="--delay: {6 * multiplier}ms">
VERT is licensed under AGPL-3.0, and the source code can be found on <a
class="hover:underline font-medium text-foreground-highlight"
href="https://github.com/not-nullptr/VERT">GitHub</a
>.
</p>
<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: {7 * multiplier}ms"
style="--delay: {8 * multiplier}ms"
>
🎨 credits
</h2>
@ -83,7 +92,7 @@
<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 * 8}ms;"
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" />
@ -101,7 +110,7 @@
<p
class="text-foreground-muted text-base mt-10 text-transition"
style="--delay: {9 * multiplier}ms"
style="--delay: {10 * multiplier}ms"
>
(obviously inspired by <a
href="https://cobalt.tools"

View File

@ -5,6 +5,7 @@
import ProgressiveBlur from "$lib/components/visual/effects/ProgressiveBlur.svelte";
import { converters } from "$lib/converters";
import type { Converter } from "$lib/converters/converter.svelte";
import { log } from "$lib/logger";
import { files, outputFilenameOption } from "$lib/store/index.svelte";
import clsx from "clsx";
import { ArrowRight, XIcon } from "lucide-svelte";
@ -74,6 +75,7 @@
});
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++) {
@ -113,6 +115,9 @@
}
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 () => {
@ -413,7 +418,7 @@
direction={isSm ? "bottom" : "right"}
endIntensity={128}
iterations={6}
fadeTo="rgba(255, 255, 255, 0.6)"
fadeTo="var(--bg-transparent)"
/>
</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 plugin from "tailwindcss/plugin";
export default {
content: ["./src/**/*.{html,js,svelte,ts}"],
theme: {
extend: {
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;

View File

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