mirror of https://github.com/VERT-sh/VERT.git
Merge remote-tracking branch 'upstream/main' into file-name-options
This commit is contained in:
commit
50cc61d87d
|
@ -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
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"css.customData": [".vscode/tailwind.json"]
|
||||||
|
}
|
|
@ -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 you’d 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -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" ]
|
53
README.md
53
README.md
|
@ -1,4 +1,4 @@
|
||||||
# VERT
|

|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -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.
|
||||||
|
|
|
@ -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
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
@ -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;
|
||||||
|
};
|
|
@ -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: {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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">,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
|
};
|
|
@ -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"];
|
||||||
|
|
|
@ -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">
|
||||||
|
|
|
@ -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;
|
||||||
|
};
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 7.8 KiB |
|
@ -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;
|
||||||
|
|
|
@ -26,4 +26,11 @@ export default defineConfig({
|
||||||
"@ffmpeg/util",
|
"@ffmpeg/util",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
css: {
|
||||||
|
preprocessorOptions: {
|
||||||
|
scss: {
|
||||||
|
api: "modern",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue