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