From 2191c955007faf7970de6cbf6f2e2f9bffe8a246 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Mon, 14 Apr 2025 23:24:12 +0100 Subject: [PATCH 01/78] feat: experimental audio to video --- src/lib/converters/converter.svelte.ts | 5 +- src/lib/converters/ffmpeg.svelte.ts | 103 ++++++++++++++++++++++--- src/lib/converters/pandoc.svelte.ts | 24 +++--- src/lib/converters/vertd.svelte.ts | 16 ++-- src/lib/converters/vips.svelte.ts | 34 ++++---- src/lib/types/file.svelte.ts | 23 ++++-- src/routes/convert/+page.svelte | 10 ++- 7 files changed, 158 insertions(+), 57 deletions(-) diff --git a/src/lib/converters/converter.svelte.ts b/src/lib/converters/converter.svelte.ts index 4c0fc85..f321b56 100644 --- a/src/lib/converters/converter.svelte.ts +++ b/src/lib/converters/converter.svelte.ts @@ -5,8 +5,9 @@ export class FormatInfo { constructor( name: string, - public fromSupported: boolean, - public toSupported: boolean, + public fromSupported = true, + public toSupported = true, + public isNative = true, ) { this.name = name; if (!this.name.startsWith(".")) { diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index 9ce3e69..b6c5122 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -5,23 +5,27 @@ import { browser } from "$app/environment"; import { error, log } from "$lib/logger"; import { addToast } from "$lib/store/ToastProvider"; +const videoFormats = ["mp4", "mkv", "avi", "mov", "webm"]; + export class FFmpegConverter extends Converter { private ffmpeg: FFmpeg = null!; public name = "ffmpeg"; public ready = $state(false); public supportedFormats = [ - new FormatInfo("mp3", true, true), - new FormatInfo("wav", true, true), - new FormatInfo("flac", true, true), - new FormatInfo("ogg", true, true), - new FormatInfo("aac", true, true), - new FormatInfo("m4a", true, true), - new FormatInfo("wma", true, true), - new FormatInfo("amr", true, true), - new FormatInfo("ac3", true, true), - new FormatInfo("alac", true, true), - new FormatInfo("aiff", true, true), + new FormatInfo("mp3"), + new FormatInfo("wav"), + new FormatInfo("flac"), + new FormatInfo("ogg"), + new FormatInfo("aac"), + new FormatInfo("m4a"), + // TODO: audio to video where it uses the album cover + ...videoFormats.map((f) => new FormatInfo(f, true, true, false)), + new FormatInfo("wma"), + new FormatInfo("amr"), + new FormatInfo("ac3"), + new FormatInfo("alac"), + new FormatInfo("aiff"), ]; public readonly reportsProgress = true; @@ -57,6 +61,9 @@ export class FFmpegConverter extends Converter { ffmpeg.on("progress", (progress) => { input.progress = progress.progress * 100; }); + ffmpeg.on("log", (l) => { + log(["converters", this.name], l.message); + }); const baseURL = "https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm"; await ffmpeg.load({ @@ -69,7 +76,33 @@ export class FFmpegConverter extends Converter { ["converters", this.name], `wrote ${input.name} to ffmpeg virtual fs`, ); - await ffmpeg.exec(["-i", "input", "output" + to]); + if (videoFormats.includes(input.from.slice(1))) { + // create an audio track from the video + await ffmpeg.exec(["-i", "input", "-map", "0:a:0", "output" + to]); + } else if (videoFormats.includes(to.slice(1))) { + // nab the album art + await ffmpeg.exec([ + "-i", + "input", + "-an", + "-vcodec", + "copy", + "cover.png", + ]); + await ffmpeg.exec([ + "-i", + "input", + "-i", + "cover.png", + "-loop", + "1", + ...toArgs(to), + "output" + to, + ]); + } else { + await ffmpeg.exec(["-i", "input", "output" + to]); + } + log(["converters", this.name], `executed ffmpeg command`); const output = (await ffmpeg.readFile( "output" + to, @@ -82,3 +115,49 @@ export class FFmpegConverter extends Converter { return new VertFile(new File([output], input.name), to); } } + +// and here i was, thinking i'd be done with ffmpeg after finishing vertd +// but OH NO we just HAD to have someone suggest to allow album art video generation. +// +// i hate you SO much. +// - love, maddie +const toArgs = (ext: string): string[] => { + const encoder = getEncoder(ext); + const args = ["-c:v", encoder]; + switch (encoder) { + case "libx264": { + args.push( + "-preset", + "ultrafast", + "-crf", + "18", + "-tune", + "stillimage", + "-c:a", + "aac", + ); + break; + } + + case "libvpx": { + args.push("-c:v", "libvpx-vp9", "-c:a", "libvorbis"); + break; + } + } + + return args; +}; + +const getEncoder = (ext: string): string => { + switch (ext) { + case ".mkv": + case ".mp4": + case ".avi": + case ".mov": + return "libx264"; + case ".webm": + return "libvpx"; + default: + return "copy"; + } +}; diff --git a/src/lib/converters/pandoc.svelte.ts b/src/lib/converters/pandoc.svelte.ts index 8984e25..cf1459b 100644 --- a/src/lib/converters/pandoc.svelte.ts +++ b/src/lib/converters/pandoc.svelte.ts @@ -62,18 +62,18 @@ export class PandocConverter extends Converter { } public supportedFormats = [ - new FormatInfo("docx", true, true), - new FormatInfo("doc", true, true), - new FormatInfo("md", true, true), - new FormatInfo("html", true, true), - new FormatInfo("rtf", true, true), - new FormatInfo("csv", true, true), - new FormatInfo("tsv", true, true), - new FormatInfo("json", true, true), - new FormatInfo("rst", true, true), - new FormatInfo("epub", true, true), - new FormatInfo("odt", true, true), - new FormatInfo("docbook", true, true), + new FormatInfo("docx"), + new FormatInfo("doc"), + new FormatInfo("md"), + new FormatInfo("html"), + new FormatInfo("rtf"), + new FormatInfo("csv"), + new FormatInfo("tsv"), + new FormatInfo("json"), + new FormatInfo("rst"), + new FormatInfo("epub"), + new FormatInfo("odt"), + new FormatInfo("docbook"), ]; } diff --git a/src/lib/converters/vertd.svelte.ts b/src/lib/converters/vertd.svelte.ts index 94f9d20..b8b022e 100644 --- a/src/lib/converters/vertd.svelte.ts +++ b/src/lib/converters/vertd.svelte.ts @@ -202,14 +202,14 @@ export class VertdConverter extends Converter { public reportsProgress = true; public supportedFormats = [ - new FormatInfo("mkv", true, true), - new FormatInfo("mp4", true, true), - new FormatInfo("webm", true, true), - new FormatInfo("avi", true, true), - new FormatInfo("wmv", true, true), - new FormatInfo("mov", true, true), - new FormatInfo("gif", true, true), - new FormatInfo("mts", true, true), + new FormatInfo("mkv"), + new FormatInfo("mp4"), + new FormatInfo("webm"), + new FormatInfo("avi"), + new FormatInfo("wmv"), + new FormatInfo("mov"), + new FormatInfo("gif"), + new FormatInfo("mts"), ]; // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/src/lib/converters/vips.svelte.ts b/src/lib/converters/vips.svelte.ts index c8b3c0d..46e7022 100644 --- a/src/lib/converters/vips.svelte.ts +++ b/src/lib/converters/vips.svelte.ts @@ -17,31 +17,31 @@ export class VipsConverter extends Converter { public ready = $state(false); public supportedFormats = [ - new FormatInfo("png", true, true), - new FormatInfo("jpeg", true, true), - new FormatInfo("jpg", true, true), - new FormatInfo("webp", true, true), - new FormatInfo("gif", true, true), + new FormatInfo("png"), + new FormatInfo("jpeg"), + new FormatInfo("jpg"), + new FormatInfo("webp"), + new FormatInfo("gif"), new FormatInfo("ico", true, false), new FormatInfo("cur", true, false), new FormatInfo("ani", true, false), new FormatInfo("heic", true, false), new FormatInfo("nef", true, false), new FormatInfo("cr2", true, false), - new FormatInfo("hdr", true, true), - new FormatInfo("jpe", true, true), + new FormatInfo("hdr"), + new FormatInfo("jpe"), new FormatInfo("dng", true, false), - new FormatInfo("mat", true, true), - new FormatInfo("pbm", true, true), - new FormatInfo("pfm", true, true), - new FormatInfo("pgm", true, true), - new FormatInfo("pnm", true, true), - new FormatInfo("ppm", true, true), + new FormatInfo("mat"), + new FormatInfo("pbm"), + new FormatInfo("pfm"), + new FormatInfo("pgm"), + new FormatInfo("pnm"), + new FormatInfo("ppm"), new FormatInfo("raw", false, true), - new FormatInfo("tif", true, true), - new FormatInfo("tiff", true, true), - new FormatInfo("jfif", true, true), - new FormatInfo("avif", true, true), + new FormatInfo("tif"), + new FormatInfo("tiff"), + new FormatInfo("jfif"), + new FormatInfo("avif"), ]; public readonly reportsProgress = false; diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index f77156c..3ff57c1 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -34,11 +34,24 @@ export class VertFile { } public findConverter() { - const converter = this.converters.find( - (converter) => - converter.formatStrings().includes(this.from) && - converter.formatStrings().includes(this.to), - ); + const converter = this.converters.find((converter) => { + if ( + !converter.formatStrings().includes(this.from) || + !converter.formatStrings().includes(this.to) + ) { + return false; + } + + const theirFrom = converter.supportedFormats.find( + (f) => f.name === this.from, + ); + const theirTo = converter.supportedFormats.find( + (f) => f.name === this.to, + ); + if (!theirFrom || !theirTo) return false; + if (!theirFrom.isNative && !theirTo.isNative) return false; + return true; + }); return converter; } diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index a058220..33932a0 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -227,7 +227,15 @@ - c.formatStrings((f) => f.toSupported), + // c.formatStrings((f) => f.toSupported), + c.supportedFormats.find( + (f) => f.name === file.from, + )?.isNative + ? c.formatStrings((f) => f.toSupported) + : c.formatStrings( + (f) => + f.toSupported && f.isNative, + ), ) .filter( (format) => From ea53fc7b9ba8ea5fedac975abb8a1cb85fa71015 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Mon, 14 Apr 2025 23:30:31 +0100 Subject: [PATCH 02/78] feat: SOME sorting --- src/routes/convert/+page.svelte | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index 33932a0..1862d72 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -231,12 +231,21 @@ c.supportedFormats.find( (f) => f.name === file.from, )?.isNative - ? c.formatStrings((f) => f.toSupported) - : c.formatStrings( + ? c.supportedFormats.filter( + (f) => f.toSupported, + ) + : c.supportedFormats.filter( (f) => f.toSupported && f.isNative, ), ) + .sort((a, b) => { + // sort native formats first + if (a.isNative && !b.isNative) return -1; + if (!a.isNative && b.isNative) return 1; + return 0; + }) + .map((f) => f.name) .filter( (format) => format !== ".svg" && format !== ".heif", From 4d2378e7efaff43cfce25eec4d8687a623840540 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Mon, 14 Apr 2025 23:54:20 +0100 Subject: [PATCH 03/78] feat: sorting by nativity --- src/lib/converters/index.ts | 13 +++++++++++++ src/lib/store/index.svelte.ts | 18 ++++++++++-------- src/lib/types/file.svelte.ts | 12 ++++++++---- src/routes/convert/+page.svelte | 12 ++++++++---- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/lib/converters/index.ts b/src/lib/converters/index.ts index dd43695..434a08a 100644 --- a/src/lib/converters/index.ts +++ b/src/lib/converters/index.ts @@ -1,3 +1,4 @@ +import type { Converter, FormatInfo } from "./converter.svelte"; import { FFmpegConverter } from "./ffmpeg.svelte"; import { PandocConverter } from "./pandoc.svelte"; import { VertdConverter } from "./vertd.svelte"; @@ -9,3 +10,15 @@ export const converters = [ new VertdConverter(), new PandocConverter(), ]; + +export const byNative = (format: string) => { + return (a: Converter, b: Converter) => { + const aFormat = a.supportedFormats.find((f) => f.name === format); + const bFormat = b.supportedFormats.find((f) => f.name === format); + + if (aFormat && bFormat) { + return aFormat.isNative ? -1 : 1; + } + return 0; + }; +}; diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts index bf99690..55f5f5d 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -1,5 +1,5 @@ import { browser } from "$app/environment"; -import { converters } from "$lib/converters"; +import { byNative, converters } from "$lib/converters"; import { error, log } from "$lib/logger"; import { VertFile } from "$lib/types"; import { parseBlob, selectCover } from "music-metadata"; @@ -32,11 +32,13 @@ class Files { this.thumbnailQueue.add(async () => { const isAudio = converters .find((c) => c.name === "ffmpeg") - ?.formatStrings() + ?.supportedFormats.filter((f) => f.isNative) + .map((f) => f.name) ?.includes(file.from.toLowerCase()); const isVideo = converters .find((c) => c.name === "vertd") - ?.formatStrings() + ?.supportedFormats.filter((f) => f.isNative) + .map((f) => f.name) ?.includes(file.from.toLowerCase()); try { @@ -120,11 +122,11 @@ class Files { log(["files"], `no extension found for ${file.name}`); return; } - const converter = converters.find((c) => - c - .formatStrings() - .includes(format || ".somenonexistentextension"), - ); + const converter = converters + .sort(byNative(format)) + .find((converter) => + converter.formatStrings().includes(format), + ); if (!converter) { log(["files"], `no converter found for ${file.name}`); this.files.push(new VertFile(file, format)); diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts index 3ff57c1..8d59959 100644 --- a/src/lib/types/file.svelte.ts +++ b/src/lib/types/file.svelte.ts @@ -1,4 +1,4 @@ -import { converters } from "$lib/converters"; +import { byNative, converters } from "$lib/converters"; import type { Converter } from "$lib/converters/converter.svelte"; import { error } from "$lib/logger"; import { addToast } from "$lib/store/ToastProvider"; @@ -27,9 +27,13 @@ export class VertFile { public converters: Converter[] = []; public findConverters(supportedFormats: string[] = [this.from]) { - const converter = this.converters.filter((converter) => - converter.formatStrings().map((f) => supportedFormats.includes(f)), - ); + const converter = this.converters + .filter((converter) => + converter + .formatStrings() + .map((f) => supportedFormats.includes(f)), + ) + .sort(byNative(this.from)); return converter; } diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index 1862d72..52784bb 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -94,19 +94,23 @@ )} {@const isAudio = converters .find((c) => c.name === "ffmpeg") - ?.formatStrings((f) => f.fromSupported) + ?.supportedFormats.filter((f) => f.isNative) + .map((f) => f.name) .includes(file.from)} {@const isVideo = converters .find((c) => c.name === "vertd") - ?.formatStrings((f) => f.fromSupported) + ?.supportedFormats.filter((f) => f.isNative) + .map((f) => f.name) .includes(file.from)} {@const isImage = converters .find((c) => c.name === "libvips") - ?.formatStrings((f) => f.fromSupported) + ?.supportedFormats.filter((f) => f.isNative) + .map((f) => f.name) .includes(file.from)} {@const isDocument = converters .find((c) => c.name === "pandoc") - ?.formatStrings((f) => f.fromSupported) + ?.supportedFormats.filter((f) => f.isNative) + .map((f) => f.name) .includes(file.from)}
From 52ae6fd58a4754c995e2d826fd50e02c3f7714d7 Mon Sep 17 00:00:00 2001 From: azure Date: Mon, 14 Apr 2025 08:43:36 -0400 Subject: [PATCH 04/78] docs: update docker instructions * Update build instructions to use fully lowercase image tag * Map 3000/tcp (host) to 80/tcp (container) instead * Add `docker run` command explanation * Add pre-built image instructions --- README.md | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index dd16edb..c06cadb 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,7 @@ If using nginx, you can use the [nginx.conf](./nginx.conf) file as a starting po Clone the repository, then build a Docker image with: ```shell -$ docker build -t VERT-sh/vert \ +$ docker build -t vert-sh/vert \ --build-arg PUB_ENV=production \ --build-arg PUB_HOSTNAME=vert.sh \ --build-arg PUB_PLAUSIBLE_URL=https://plausible.example.com . @@ -67,10 +67,25 @@ $ docker build -t VERT-sh/vert \ You can then run it by using: ```shell -$ docker run --restart unless-stopped -p 3000:3000 -d --name "vert" VERT-sh/vert +$ docker run -d \ + --restart unless-stopped \ + -p 3000:80 \ + --name "vert" \ + vert-sh/vert ``` -We also have a `docker-compose.yml` file available. Use `docker compose up` if you want to start the stack, or `docker compose down` to bring it down. You can pass `--build` to `docker compose up` to rebuild the Docker image (useful if you've changed any of the environment variables) as well as `-d` to start it in detached mode. You can read more about Docker Compose in general [here](https://docs.docker.com/compose/intro/compose-application-model/). +This will run the image in dettached mode, restart the container unless it is manually stopped, map 3000/tcp (host) to 80/tcp (container), name the container `vert` and use the previously built image. + +We also have a [`docker-compose.yml`](./docker-compose.yml) file available. Use `docker compose up` if you want to start the stack, or `docker compose down` to bring it down. You can pass `--build` to `docker compose up` to rebuild the Docker image (useful if you've changed any of the environment variables) as well as `-d` to start it in detached mode. You can read more about Docker Compose in general [here](https://docs.docker.com/compose/intro/compose-application-model/). + +While there's an image you can pull instead of having to clone this repository and build the image yourself, you will not be able to update any of the environment variables (e.g. `PUB_PLAUSIBLE_URL`) as they're baked directly into the image and not obtained during runtime. If you're okay with this, you can simply run this command instead: +```shell +$ docker run -d \ + --restart unless-stopped \ + -p 3000:80 \ + --name "vert" \ + ghcr.io/vert-sh/vert:latest +``` ## License From 68875d4d3bf18080c6d06d1afd66ca2a1b18d15f Mon Sep 17 00:00:00 2001 From: azure Date: Mon, 14 Apr 2025 15:26:12 -0400 Subject: [PATCH 05/78] docs: apply suggested changes --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c06cadb..cad49ee 100644 --- a/README.md +++ b/README.md @@ -74,11 +74,14 @@ $ docker run -d \ vert-sh/vert ``` -This will run the image in dettached mode, restart the container unless it is manually stopped, map 3000/tcp (host) to 80/tcp (container), name the container `vert` and use the previously built image. +This will do the following: +- Use the previously built image as the container `vert`, in detached mode +- Continuously restart the container until manually stopped +- Map `3000/tcp` (host) to `80/tcp` (container) We also have a [`docker-compose.yml`](./docker-compose.yml) file available. Use `docker compose up` if you want to start the stack, or `docker compose down` to bring it down. You can pass `--build` to `docker compose up` to rebuild the Docker image (useful if you've changed any of the environment variables) as well as `-d` to start it in detached mode. You can read more about Docker Compose in general [here](https://docs.docker.com/compose/intro/compose-application-model/). -While there's an image you can pull instead of having to clone this repository and build the image yourself, you will not be able to update any of the environment variables (e.g. `PUB_PLAUSIBLE_URL`) as they're baked directly into the image and not obtained during runtime. If you're okay with this, you can simply run this command instead: +While there's an image you can pull instead of cloning the repo and building the image yourself, you will not be able to update any of the environment variables (e.g. `PUB_PLAUSIBLE_URL`) as they're baked directly into the image and not obtained during runtime. If you're okay with this, you can simply run this command instead: ```shell $ docker run -d \ --restart unless-stopped \ From fc6e14109ca89cef8f72ac0fad53afbe4aa11e2c Mon Sep 17 00:00:00 2001 From: azure Date: Mon, 14 Apr 2025 15:41:54 -0400 Subject: [PATCH 06/78] fix(compose): make image name fully lowercase --- docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index ce2c8b6..13afdea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: vert: container_name: vert - image: VERT-sh/vert:latest + image: vert-sh/vert:latest environment: - PUB_HOSTNAME=${PUB_HOSTNAME:-vert.sh} - PUB_PLAUSIBLE_URL=${PUB_PLAUSIBLE_URL:-https://plausible.example.com} From db7b9406a72d54fb8c44e82bbbf70fabe100cf89 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:11:05 +0100 Subject: [PATCH 07/78] feat: .icns support --- bun.lock | 7 + package.json | 1 + src/app.html | 94 ++-- src/lib/components/layout/Navbar/Base.svelte | 394 ++++++------- src/lib/converters/index.ts | 2 +- src/lib/parse/ani.ts | 300 +++++----- src/lib/types/conversion-worker.ts | 64 +-- src/lib/workers/vips.ts | 562 ++++++++++--------- src/routes/jpegify/+page.svelte | 226 ++++---- 9 files changed, 832 insertions(+), 818 deletions(-) diff --git a/bun.lock b/bun.lock index 94833c0..1a41b8b 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "@bjorn3/browser_wasi_shim": "^0.4.1", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", + "@fiahfy/icns": "^0.0.7", "@fontsource/azeret-mono": "^5.1.1", "@fontsource/lexend": "^5.1.2", "@fontsource/radio-canada-big": "^5.1.1", @@ -120,6 +121,10 @@ "@ffmpeg/util": ["@ffmpeg/util@0.12.2", "", {}, "sha512-ouyoW+4JB7WxjeZ2y6KpRvB+dLp7Cp4ro8z0HIVpZVCM7AwFlHa0c4R8Y/a4M3wMqATpYKhC7lSFHQ0T11MEDw=="], + "@fiahfy/icns": ["@fiahfy/icns@0.0.7", "", { "dependencies": { "@fiahfy/packbits": "^0.0.6", "pngjs": "^6.0.0" } }, "sha512-0apAtbUXTU3Opy/Z4h69o53voBa+am8FmdZauyagUMskAVYN1a5yIRk48Sf+tEdBLlefbvqLWPJ4pxr/Y/QtTg=="], + + "@fiahfy/packbits": ["@fiahfy/packbits@0.0.6", "", {}, "sha512-XuhF/edg+iIvXjkCWgfj6fWtRi/KrEPg2ILXj1l86EN4EssuOiPcLKgkMDr9cL8jTGtVd/MKUWW6Y0/ZVf1PGA=="], + "@fontsource/azeret-mono": ["@fontsource/azeret-mono@5.2.5", "", {}, "sha512-GRzKYuD1CVOS6Jag/ohDCycLV9a3TK6y1T73A8q0JoDZTVO85DNapqLK+SV2gYtTFldahNAlDSIaizv9MLhR1A=="], "@fontsource/lexend": ["@fontsource/lexend@5.2.5", "", {}, "sha512-Mv2XQ+B4ek2lNCGRW5ddLTW8T3xTT17AnCk1IETpoef57XHz+e42fUfLAYMrmiJLOGpR44qnyJ5S6D323A5EIw=="], @@ -608,6 +613,8 @@ "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], + "pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], + "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], diff --git a/package.json b/package.json index 38ce68c..6eeb872 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@bjorn3/browser_wasi_shim": "^0.4.1", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", + "@fiahfy/icns": "^0.0.7", "@fontsource/azeret-mono": "^5.1.1", "@fontsource/lexend": "^5.1.2", "@fontsource/radio-canada-big": "^5.1.1", diff --git a/src/app.html b/src/app.html index ef2158d..a8f439a 100644 --- a/src/app.html +++ b/src/app.html @@ -1,47 +1,47 @@ - - - - - - - - - - - - - - %sveltekit.head% - - - -
%sveltekit.body%
- - + + + + + + + + + + + + + + %sveltekit.head% + + + +
%sveltekit.body%
+ + diff --git a/src/lib/components/layout/Navbar/Base.svelte b/src/lib/components/layout/Navbar/Base.svelte index 1e2d4ca..515bae3 100644 --- a/src/lib/components/layout/Navbar/Base.svelte +++ b/src/lib/components/layout/Navbar/Base.svelte @@ -1,197 +1,197 @@ - - -{#snippet link(item: (typeof items)[0], index: number)} - {@const Icon = item.icon} - -
- {#key item.name} -
-
- - {#if item.badge} -
- {#key item.badge} -
- {item.badge} -
- {/key} -
- {/if} -
- -
- {/key} -
-
-{/snippet} - -
- - {@const linkRect = linkRects.at(selectedIndex) || linkRects[0]} - {#if linkRect} -
- {/if} - - {#each items as item, i (item.url)} - {@render link(item, i)} - {/each} - - -
-
+ + +{#snippet link(item: (typeof items)[0], index: number)} + {@const Icon = item.icon} + +
+ {#key item.name} +
+
+ + {#if item.badge} +
+ {#key item.badge} +
+ {item.badge} +
+ {/key} +
+ {/if} +
+ +
+ {/key} +
+
+{/snippet} + +
+ + {@const linkRect = linkRects.at(selectedIndex) || linkRects[0]} + {#if linkRect} +
+ {/if} + + {#each items as item, i (item.url)} + {@render link(item, i)} + {/each} + + +
+
diff --git a/src/lib/converters/index.ts b/src/lib/converters/index.ts index 434a08a..3ccb073 100644 --- a/src/lib/converters/index.ts +++ b/src/lib/converters/index.ts @@ -1,4 +1,4 @@ -import type { Converter, FormatInfo } from "./converter.svelte"; +import type { Converter } from "./converter.svelte"; import { FFmpegConverter } from "./ffmpeg.svelte"; import { PandocConverter } from "./pandoc.svelte"; import { VertdConverter } from "./vertd.svelte"; diff --git a/src/lib/parse/ani.ts b/src/lib/parse/ani.ts index 814b069..09b9712 100644 --- a/src/lib/parse/ani.ts +++ b/src/lib/parse/ani.ts @@ -1,150 +1,150 @@ -// THIS CODE IS FROM https://github.com/captbaritone/webamp/blob/15b0312cb794973a0e615d894df942452e920c36/packages/ani-cursor/src/parser.ts -// LICENSED UNDER MIT. (c) Jordan Eldredge and Webamp contributors - -// this code is ripped from their project because i didn't want to -// re-invent the wheel, BUT the library they provide (ani-cursor) -// doesn't expose the internals. - -import { RIFFFile } from "riff-file"; -import { unpackArray, unpackString } from "byte-data"; - -type Chunk = { - format: string; - chunkId: string; - chunkData: { - start: number; - end: number; - }; - subChunks: Chunk[]; -}; - -// https://www.informit.com/articles/article.aspx?p=1189080&seqNum=3 -type AniMetadata = { - cbSize: number; // Data structure size (in bytes) - nFrames: number; // Number of images (also known as frames) stored in the file - nSteps: number; // Number of frames to be displayed before the animation repeats - iWidth: number; // Width of frame (in pixels) - iHeight: number; // Height of frame (in pixels) - iBitCount: number; // Number of bits per pixel - nPlanes: number; // Number of color planes - iDispRate: number; // Default frame display rate (measured in 1/60th-of-a-second units) - bfAttributes: number; // ANI attribute bit flags -}; - -type ParsedAni = { - rate: number[] | null; - seq: number[] | null; - images: Uint8Array[]; - metadata: AniMetadata; - artist: string | null; - title: string | null; -}; - -const DWORD = { bits: 32, be: false, signed: false, fp: false }; - -export function parseAni(arr: Uint8Array): ParsedAni { - const riff = new RIFFFile(); - - riff.setSignature(arr); - - const signature = riff.signature as Chunk; - if (signature.format !== "ACON") { - throw new Error( - `Expected format. Expected "ACON", got "${signature.format}"`, - ); - } - - // Helper function to get a chunk by chunkId and transform it if it's non-null. - function mapChunk( - chunkId: string, - mapper: (chunk: Chunk) => T, - ): T | null { - const chunk = riff.findChunk(chunkId) as Chunk | null; - return chunk == null ? null : mapper(chunk); - } - - function readImages(chunk: Chunk, frameCount: number): Uint8Array[] { - return chunk.subChunks.slice(0, frameCount).map((c) => { - if (c.chunkId !== "icon") { - throw new Error(`Unexpected chunk type in fram: ${c.chunkId}`); - } - return arr.slice(c.chunkData.start, c.chunkData.end); - }); - } - - const metadata = mapChunk("anih", (c) => { - const words = unpackArray( - arr, - DWORD, - c.chunkData.start, - c.chunkData.end, - ); - return { - cbSize: words[0], - nFrames: words[1], - nSteps: words[2], - iWidth: words[3], - iHeight: words[4], - iBitCount: words[5], - nPlanes: words[6], - iDispRate: words[7], - bfAttributes: words[8], - }; - }); - - if (metadata == null) { - throw new Error("Did not find anih"); - } - - const rate = mapChunk("rate", (c) => { - return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end); - }); - // chunkIds are always four chars, hence the trailing space. - const seq = mapChunk("seq ", (c) => { - return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end); - }); - - const lists = riff.findChunk("LIST", true) as Chunk[] | null; - const imageChunk = lists?.find((c) => c.format === "fram"); - if (imageChunk == null) { - throw new Error("Did not find fram LIST"); - } - - let images = readImages(imageChunk, metadata.nFrames); - - let title = null; - let artist = null; - - const infoChunk = lists?.find((c) => c.format === "INFO"); - if (infoChunk != null) { - infoChunk.subChunks.forEach((c) => { - switch (c.chunkId) { - case "INAM": - title = unpackString( - arr, - c.chunkData.start, - c.chunkData.end, - ); - break; - case "IART": - artist = unpackString( - arr, - c.chunkData.start, - c.chunkData.end, - ); - break; - case "LIST": - // Some cursors with an artist of "Created with Take ONE 3.5 (unregisterred version)" seem to have their frames here for some reason? - if (c.format === "fram") { - images = readImages(c, metadata.nFrames); - } - break; - - default: - // Unexpected subchunk - } - }); - } - - return { images, rate, seq, metadata, artist, title }; -} +// THIS CODE IS FROM https://github.com/captbaritone/webamp/blob/15b0312cb794973a0e615d894df942452e920c36/packages/ani-cursor/src/parser.ts +// LICENSED UNDER MIT. (c) Jordan Eldredge and Webamp contributors + +// this code is ripped from their project because i didn't want to +// re-invent the wheel, BUT the library they provide (ani-cursor) +// doesn't expose the internals. + +import { RIFFFile } from "riff-file"; +import { unpackArray, unpackString } from "byte-data"; + +type Chunk = { + format: string; + chunkId: string; + chunkData: { + start: number; + end: number; + }; + subChunks: Chunk[]; +}; + +// https://www.informit.com/articles/article.aspx?p=1189080&seqNum=3 +type AniMetadata = { + cbSize: number; // Data structure size (in bytes) + nFrames: number; // Number of images (also known as frames) stored in the file + nSteps: number; // Number of frames to be displayed before the animation repeats + iWidth: number; // Width of frame (in pixels) + iHeight: number; // Height of frame (in pixels) + iBitCount: number; // Number of bits per pixel + nPlanes: number; // Number of color planes + iDispRate: number; // Default frame display rate (measured in 1/60th-of-a-second units) + bfAttributes: number; // ANI attribute bit flags +}; + +type ParsedAni = { + rate: number[] | null; + seq: number[] | null; + images: Uint8Array[]; + metadata: AniMetadata; + artist: string | null; + title: string | null; +}; + +const DWORD = { bits: 32, be: false, signed: false, fp: false }; + +export function parseAni(arr: Uint8Array): ParsedAni { + const riff = new RIFFFile(); + + riff.setSignature(arr); + + const signature = riff.signature as Chunk; + if (signature.format !== "ACON") { + throw new Error( + `Expected format. Expected "ACON", got "${signature.format}"`, + ); + } + + // Helper function to get a chunk by chunkId and transform it if it's non-null. + function mapChunk( + chunkId: string, + mapper: (chunk: Chunk) => T, + ): T | null { + const chunk = riff.findChunk(chunkId) as Chunk | null; + return chunk == null ? null : mapper(chunk); + } + + function readImages(chunk: Chunk, frameCount: number): Uint8Array[] { + return chunk.subChunks.slice(0, frameCount).map((c) => { + if (c.chunkId !== "icon") { + throw new Error(`Unexpected chunk type in fram: ${c.chunkId}`); + } + return arr.slice(c.chunkData.start, c.chunkData.end); + }); + } + + const metadata = mapChunk("anih", (c) => { + const words = unpackArray( + arr, + DWORD, + c.chunkData.start, + c.chunkData.end, + ); + return { + cbSize: words[0], + nFrames: words[1], + nSteps: words[2], + iWidth: words[3], + iHeight: words[4], + iBitCount: words[5], + nPlanes: words[6], + iDispRate: words[7], + bfAttributes: words[8], + }; + }); + + if (metadata == null) { + throw new Error("Did not find anih"); + } + + const rate = mapChunk("rate", (c) => { + return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end); + }); + // chunkIds are always four chars, hence the trailing space. + const seq = mapChunk("seq ", (c) => { + return unpackArray(arr, DWORD, c.chunkData.start, c.chunkData.end); + }); + + const lists = riff.findChunk("LIST", true) as Chunk[] | null; + const imageChunk = lists?.find((c) => c.format === "fram"); + if (imageChunk == null) { + throw new Error("Did not find fram LIST"); + } + + let images = readImages(imageChunk, metadata.nFrames); + + let title = null; + let artist = null; + + const infoChunk = lists?.find((c) => c.format === "INFO"); + if (infoChunk != null) { + infoChunk.subChunks.forEach((c) => { + switch (c.chunkId) { + case "INAM": + title = unpackString( + arr, + c.chunkData.start, + c.chunkData.end, + ); + break; + case "IART": + artist = unpackString( + arr, + c.chunkData.start, + c.chunkData.end, + ); + break; + case "LIST": + // Some cursors with an artist of "Created with Take ONE 3.5 (unregisterred version)" seem to have their frames here for some reason? + if (c.format === "fram") { + images = readImages(c, metadata.nFrames); + } + break; + + default: + // Unexpected subchunk + } + }); + } + + return { images, rate, seq, metadata, artist, title }; +} diff --git a/src/lib/types/conversion-worker.ts b/src/lib/types/conversion-worker.ts index 14c8346..820f2ae 100644 --- a/src/lib/types/conversion-worker.ts +++ b/src/lib/types/conversion-worker.ts @@ -1,32 +1,32 @@ -import { VertFile } from "./file.svelte"; - -interface ConvertMessage { - type: "convert"; - input: VertFile; - to: string; - compression: number | null; -} - -interface FinishedMessage { - type: "finished"; - output: ArrayBufferLike; - zip?: boolean; -} - -interface LoadedMessage { - type: "loaded"; -} - -interface ErrorMessage { - type: "error"; - error: string; -} - -export type WorkerMessage = ( - | ConvertMessage - | FinishedMessage - | LoadedMessage - | ErrorMessage -) & { - id: number; -}; +import { VertFile } from "./file.svelte"; + +interface ConvertMessage { + type: "convert"; + input: VertFile; + to: string; + compression: number | null; +} + +interface FinishedMessage { + type: "finished"; + output: ArrayBufferLike; + zip?: boolean; +} + +interface LoadedMessage { + type: "loaded"; +} + +interface ErrorMessage { + type: "error"; + error: string; +} + +export type WorkerMessage = ( + | ConvertMessage + | FinishedMessage + | LoadedMessage + | ErrorMessage +) & { + id: number; +}; diff --git a/src/lib/workers/vips.ts b/src/lib/workers/vips.ts index c73abdf..8c76276 100644 --- a/src/lib/workers/vips.ts +++ b/src/lib/workers/vips.ts @@ -1,278 +1,284 @@ -import Vips from "wasm-vips"; -import { - initializeImageMagick, - MagickFormat, - MagickImage, - MagickImageCollection, - MagickReadSettings, - type IMagickImage, -} from "@imagemagick/magick-wasm"; -import { makeZip } from "client-zip"; -import wasm from "@imagemagick/magick-wasm/magick.wasm?url"; -import { parseAni } from "$lib/parse/ani"; - -const vipsPromise = Vips({ - dynamicLibraries: [], -}); - -const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url)); - -const magickRequiredFormats = [ - ".dng", - ".heic", - ".ico", - ".cur", - ".ani", - ".cr2", - ".nef", -]; -const unsupportedFrom: string[] = []; -const unsupportedTo = [...magickRequiredFormats]; - -vipsPromise - .then(() => { - postMessage({ type: "loaded" }); - }) - .catch((error) => { - postMessage({ type: "error", error }); - }); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const handleMessage = async (message: any): Promise => { - const vips = await vipsPromise; - switch (message.type) { - case "convert": { - if (!message.to.startsWith(".")) message.to = `.${message.to}`; - console.log(message); - if (unsupportedFrom.includes(message.input.from)) { - return { - type: "error", - error: `Unsupported input format ${message.input.from}`, - }; - } - - if (unsupportedTo.includes(message.to)) { - return { - type: "error", - error: `Unsupported output format ${message.to}`, - }; - } - - const buffer = await message.input.file.arrayBuffer(); - if ( - magickRequiredFormats.includes(message.input.from) || - magickRequiredFormats.includes(message.to) - ) { - // only wait when we need to - await magickPromise; - - // special ico handling to split them all into separate images - if (message.input.from === ".ico") { - const imgs = MagickImageCollection.create(); - - while (true) { - try { - const img = MagickImage.create( - new Uint8Array(buffer), - new MagickReadSettings({ - format: MagickFormat.Ico, - frameIndex: imgs.length, - }), - ); - imgs.push(img); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - } catch (_) { - break; - } - } - - if (imgs.length === 0) { - return { - type: "error", - error: `Failed to read ICO -- no images found inside?`, - }; - } - - const convertedImgs: Uint8Array[] = []; - await Promise.all( - imgs.map(async (img, i) => { - const output = await magickConvert(img, message.to); - convertedImgs[i] = output; - }), - ); - - const zip = makeZip( - convertedImgs.map( - (img, i) => - new File( - [img], - `image${i}.${message.to.slice(1)}`, - ), - ), - "images.zip", - ); - - // read the ReadableStream to the end - const zipBytes = await readToEnd(zip.getReader()); - - imgs.dispose(); - - return { - type: "finished", - output: zipBytes, - zip: true, - }; - } else if (message.input.from === ".ani") { - console.log("Parsing ANI file"); - try { - const parsedAni = parseAni(new Uint8Array(buffer)); - const files: File[] = []; - await Promise.all( - parsedAni.images.map(async (img, i) => { - const blob = await magickConvert( - MagickImage.create( - img, - new MagickReadSettings({ - format: MagickFormat.Ico, - }), - ), - message.to, - ); - files.push( - new File([blob], `image${i}${message.to}`), - ); - }), - ); - - const zip = makeZip(files, "images.zip"); - const zipBytes = await readToEnd(zip.getReader()); - - return { - type: "finished", - output: zipBytes, - zip: true, - }; - } catch (e) { - console.error(e); - } - } - - console.log(message.input.from); - - const img = MagickImage.create( - new Uint8Array(buffer), - new MagickReadSettings({ - format: message.input.from - .slice(1) - .toUpperCase() as MagickFormat, - }), - ); - - const converted = await magickConvert(img, message.to); - - return { - type: "finished", - output: converted, - }; - } - - let image = vips.Image.newFromBuffer(buffer, ""); - - // check if animated image & keep it animated when converting - if (image.getTypeof("n-pages") > 0) { - image = vips.Image.newFromBuffer(buffer, "[n=-1]"); - } - - const opts: { [key: string]: string } = {}; - if (typeof message.compression !== "undefined") { - opts["Q"] = Math.min(100, message.compression + 1).toString(); - } - - const output = image.writeToBuffer(message.to, opts); - image.delete(); - return { - type: "finished", - output: output.buffer, - }; - } - } -}; - -const readToEnd = async (reader: ReadableStreamDefaultReader) => { - const chunks: Uint8Array[] = []; - let done = false; - while (!done) { - const { value, done: d } = await reader.read(); - if (value) chunks.push(value); - done = d; - } - const blob = new Blob(chunks, { type: "application/zip" }); - const arrayBuffer = await blob.arrayBuffer(); - return new Uint8Array(arrayBuffer); -}; - -const magickToBlob = async (img: IMagickImage): Promise => { - const canvas = new OffscreenCanvas(img.width, img.height); - return new Promise((resolve, reject) => - img.getPixels(async (p) => { - const area = p.getArea(0, 0, img.width, img.height); - const chunkSize = img.hasAlpha ? 4 : 3; - const chunks = Math.ceil(area.length / chunkSize); - const data = new Uint8ClampedArray(chunks * 4); - - for (let j = 0, k = 0; j < area.length; j += chunkSize, k += 4) { - data[k] = area[j]; - data[k + 1] = area[j + 1]; - data[k + 2] = area[j + 2]; - data[k + 3] = img.hasAlpha ? area[j + 3] : 255; - } - - const ctx = canvas.getContext("2d"); - if (!ctx) { - reject(new Error("Failed to get canvas context")); - return; - } - - ctx.putImageData(new ImageData(data, img.width, img.height), 0, 0); - - const blob = await canvas.convertToBlob({ - type: "image/png", - }); - - resolve(blob); - }), - ); -}; - -const magickConvert = async (img: IMagickImage, to: string) => { - const vips = await vipsPromise; - - const intermediary = await magickToBlob(img); - const buf = await intermediary.arrayBuffer(); - - const imgVips = vips.Image.newFromBuffer(buf); - const output = imgVips.writeToBuffer(to); - - imgVips.delete(); - img.dispose(); - - return output; -}; - -onmessage = async (e) => { - const message = e.data; - try { - const res = await handleMessage(message); - if (!res) return; - postMessage({ - ...res, - id: message.id, - }); - } catch (e) { - postMessage({ - type: "error", - error: e, - id: message.id, - }); - } -}; +import Vips from "wasm-vips"; +import { + initializeImageMagick, + MagickFormat, + MagickImage, + MagickImageCollection, + MagickReadSettings, + type IMagickImage, +} from "@imagemagick/magick-wasm"; +import { makeZip } from "client-zip"; +import wasm from "@imagemagick/magick-wasm/magick.wasm?url"; +import { parseAni } from "$lib/parse/ani"; +import { Icns } from "@fiahfy/icns/dist"; + +const vipsPromise = Vips({ + dynamicLibraries: [], +}); + +const magickPromise = initializeImageMagick(new URL(wasm, import.meta.url)); + +const magickRequiredFormats = [ + ".dng", + ".heic", + ".ico", + ".cur", + ".ani", + ".cr2", + ".nef", +]; +const unsupportedFrom: string[] = []; +const unsupportedTo = [...magickRequiredFormats]; + +vipsPromise + .then(() => { + postMessage({ type: "loaded" }); + }) + .catch((error) => { + postMessage({ type: "error", error }); + }); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const handleMessage = async (message: any): Promise => { + const vips = await vipsPromise; + switch (message.type) { + case "convert": { + if (!message.to.startsWith(".")) message.to = `.${message.to}`; + console.log(message); + if (unsupportedFrom.includes(message.input.from)) { + return { + type: "error", + error: `Unsupported input format ${message.input.from}`, + }; + } + + if (unsupportedTo.includes(message.to)) { + return { + type: "error", + error: `Unsupported output format ${message.to}`, + }; + } + + const buffer = await message.input.file.arrayBuffer(); + if ( + magickRequiredFormats.includes(message.input.from) || + magickRequiredFormats.includes(message.to) + ) { + // only wait when we need to + await magickPromise; + + // special ico handling to split them all into separate images + if (message.input.from === ".ico") { + const imgs = MagickImageCollection.create(); + + while (true) { + try { + const img = MagickImage.create( + new Uint8Array(buffer), + new MagickReadSettings({ + format: MagickFormat.Ico, + frameIndex: imgs.length, + }), + ); + imgs.push(img); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + break; + } + } + + if (imgs.length === 0) { + return { + type: "error", + error: `Failed to read ICO -- no images found inside?`, + }; + } + + const convertedImgs: Uint8Array[] = []; + await Promise.all( + imgs.map(async (img, i) => { + const output = await magickConvert(img, message.to); + convertedImgs[i] = output; + }), + ); + + const zip = makeZip( + convertedImgs.map( + (img, i) => + new File( + [img], + `image${i}.${message.to.slice(1)}`, + ), + ), + "images.zip", + ); + + // read the ReadableStream to the end + const zipBytes = await readToEnd(zip.getReader()); + + imgs.dispose(); + + return { + type: "finished", + output: zipBytes, + zip: true, + }; + } else if (message.input.from === ".ani") { + console.log("Parsing ANI file"); + try { + const parsedAni = parseAni(new Uint8Array(buffer)); + const files: File[] = []; + await Promise.all( + parsedAni.images.map(async (img, i) => { + const blob = await magickConvert( + MagickImage.create( + img, + new MagickReadSettings({ + format: MagickFormat.Ico, + }), + ), + message.to, + ); + files.push( + new File([blob], `image${i}${message.to}`), + ); + }), + ); + + const zip = makeZip(files, "images.zip"); + const zipBytes = await readToEnd(zip.getReader()); + + return { + type: "finished", + output: zipBytes, + zip: true, + }; + } catch (e) { + console.error(e); + } + } + + console.log(message.input.from); + + const img = MagickImage.create( + new Uint8Array(buffer), + new MagickReadSettings({ + format: message.input.from + .slice(1) + .toUpperCase() as MagickFormat, + }), + ); + + const converted = await magickConvert(img, message.to); + + return { + type: "finished", + output: converted, + }; + } + + if (message.input.from === ".icns") { + const icns = Icns.from(new Uint8Array(buffer)); + console.log(icns); + } + + let image = vips.Image.newFromBuffer(buffer, ""); + + // check if animated image & keep it animated when converting + if (image.getTypeof("n-pages") > 0) { + image = vips.Image.newFromBuffer(buffer, "[n=-1]"); + } + + const opts: { [key: string]: string } = {}; + if (typeof message.compression !== "undefined") { + opts["Q"] = Math.min(100, message.compression + 1).toString(); + } + + const output = image.writeToBuffer(message.to, opts); + image.delete(); + return { + type: "finished", + output: output.buffer, + }; + } + } +}; + +const readToEnd = async (reader: ReadableStreamDefaultReader) => { + const chunks: Uint8Array[] = []; + let done = false; + while (!done) { + const { value, done: d } = await reader.read(); + if (value) chunks.push(value); + done = d; + } + const blob = new Blob(chunks, { type: "application/zip" }); + const arrayBuffer = await blob.arrayBuffer(); + return new Uint8Array(arrayBuffer); +}; + +const magickToBlob = async (img: IMagickImage): Promise => { + const canvas = new OffscreenCanvas(img.width, img.height); + return new Promise((resolve, reject) => + img.getPixels(async (p) => { + const area = p.getArea(0, 0, img.width, img.height); + const chunkSize = img.hasAlpha ? 4 : 3; + const chunks = Math.ceil(area.length / chunkSize); + const data = new Uint8ClampedArray(chunks * 4); + + for (let j = 0, k = 0; j < area.length; j += chunkSize, k += 4) { + data[k] = area[j]; + data[k + 1] = area[j + 1]; + data[k + 2] = area[j + 2]; + data[k + 3] = img.hasAlpha ? area[j + 3] : 255; + } + + const ctx = canvas.getContext("2d"); + if (!ctx) { + reject(new Error("Failed to get canvas context")); + return; + } + + ctx.putImageData(new ImageData(data, img.width, img.height), 0, 0); + + const blob = await canvas.convertToBlob({ + type: "image/png", + }); + + resolve(blob); + }), + ); +}; + +const magickConvert = async (img: IMagickImage, to: string) => { + const vips = await vipsPromise; + + const intermediary = await magickToBlob(img); + const buf = await intermediary.arrayBuffer(); + + const imgVips = vips.Image.newFromBuffer(buf); + const output = imgVips.writeToBuffer(to); + + imgVips.delete(); + img.dispose(); + + return output; +}; + +onmessage = async (e) => { + const message = e.data; + try { + const res = await handleMessage(message); + if (!res) return; + postMessage({ + ...res, + id: message.id, + }); + } catch (e) { + postMessage({ + type: "error", + error: e, + id: message.id, + }); + } +}; diff --git a/src/routes/jpegify/+page.svelte b/src/routes/jpegify/+page.svelte index 0937a81..739abdd 100644 --- a/src/routes/jpegify/+page.svelte +++ b/src/routes/jpegify/+page.svelte @@ -1,113 +1,113 @@ - - -
-

SECRET JPEGIFY!!!

-

- (shh... don't tell anyone!) -

- - - -
- {#each images as file, i (file.id)} -
- -
- {file.name} - {file.name} -
-
- - -
-
-
- {/each} -
-
+ + +
+

SECRET JPEGIFY!!!

+

+ (shh... don't tell anyone!) +

+ + + +
+ {#each images as file, i (file.id)} +
+ +
+ {file.name} + {file.name} +
+
+ + +
+
+
+ {/each} +
+
From d3d1f6e9e63dc78f994458cb6c9e5323816480c9 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:11:13 +0100 Subject: [PATCH 08/78] feat: oops wait no THIS is icns support --- bun.lock | 13 ++++--- package.json | 3 +- src/lib/converters/index.ts | 1 - src/lib/converters/vips.svelte.ts | 3 +- src/lib/parse/icns/index.ts | 0 src/lib/workers/vips.ts | 58 +++++++++++++++++++++++++++---- vite.config.ts | 2 ++ 7 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 src/lib/parse/icns/index.ts diff --git a/bun.lock b/bun.lock index 1a41b8b..805d549 100644 --- a/bun.lock +++ b/bun.lock @@ -7,7 +7,6 @@ "@bjorn3/browser_wasi_shim": "^0.4.1", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", - "@fiahfy/icns": "^0.0.7", "@fontsource/azeret-mono": "^5.1.1", "@fontsource/lexend": "^5.1.2", "@fontsource/radio-canada-big": "^5.1.1", @@ -19,7 +18,9 @@ "music-metadata": "^11.0.0", "p-queue": "^8.1.0", "riff-file": "^1.0.3", + "vert-wasm": "^0.0.2", "vite-plugin-static-copy": "^2.2.0", + "vite-plugin-wasm": "^3.4.1", "wasm-vips": "^0.0.11", }, "devDependencies": { @@ -121,10 +122,6 @@ "@ffmpeg/util": ["@ffmpeg/util@0.12.2", "", {}, "sha512-ouyoW+4JB7WxjeZ2y6KpRvB+dLp7Cp4ro8z0HIVpZVCM7AwFlHa0c4R8Y/a4M3wMqATpYKhC7lSFHQ0T11MEDw=="], - "@fiahfy/icns": ["@fiahfy/icns@0.0.7", "", { "dependencies": { "@fiahfy/packbits": "^0.0.6", "pngjs": "^6.0.0" } }, "sha512-0apAtbUXTU3Opy/Z4h69o53voBa+am8FmdZauyagUMskAVYN1a5yIRk48Sf+tEdBLlefbvqLWPJ4pxr/Y/QtTg=="], - - "@fiahfy/packbits": ["@fiahfy/packbits@0.0.6", "", {}, "sha512-XuhF/edg+iIvXjkCWgfj6fWtRi/KrEPg2ILXj1l86EN4EssuOiPcLKgkMDr9cL8jTGtVd/MKUWW6Y0/ZVf1PGA=="], - "@fontsource/azeret-mono": ["@fontsource/azeret-mono@5.2.5", "", {}, "sha512-GRzKYuD1CVOS6Jag/ohDCycLV9a3TK6y1T73A8q0JoDZTVO85DNapqLK+SV2gYtTFldahNAlDSIaizv9MLhR1A=="], "@fontsource/lexend": ["@fontsource/lexend@5.2.5", "", {}, "sha512-Mv2XQ+B4ek2lNCGRW5ddLTW8T3xTT17AnCk1IETpoef57XHz+e42fUfLAYMrmiJLOGpR44qnyJ5S6D323A5EIw=="], @@ -613,8 +610,6 @@ "pirates": ["pirates@4.0.6", "", {}, "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg=="], - "pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], - "postcss": ["postcss@8.5.3", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A=="], "postcss-import": ["postcss-import@15.1.0", "", { "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", "resolve": "^1.1.7" }, "peerDependencies": { "postcss": "^8.0.0" } }, "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew=="], @@ -739,10 +734,14 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "vert-wasm": ["vert-wasm@0.0.2", "", {}, "sha512-QCkhkATZkWp3OPpIrlcLVd5S4DQXeDEWIPI1eV+Ku+sUVoCM4s9DQbNKtnJ8s4leMW3AD4E7cY/gXB/9TsI3WA=="], + "vite": ["vite@5.4.14", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA=="], "vite-plugin-static-copy": ["vite-plugin-static-copy@2.3.0", "", { "dependencies": { "chokidar": "^3.5.3", "fast-glob": "^3.2.11", "fs-extra": "^11.1.0", "p-map": "^7.0.3", "picocolors": "^1.0.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0" } }, "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA=="], + "vite-plugin-wasm": ["vite-plugin-wasm@3.4.1", "", { "peerDependencies": { "vite": "^2 || ^3 || ^4 || ^5 || ^6" } }, "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA=="], + "vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="], "wasm-vips": ["wasm-vips@0.0.11", "", {}, "sha512-bzFU7WcimMY4WeqnZk7whKVpSXxpagISXPJwsk2VHF4lgIN9rl4uUo5sF9x6jOlACuCH6ITZUJ7QPTYmy60NCQ=="], diff --git a/package.json b/package.json index 6eeb872..384b169 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "@bjorn3/browser_wasi_shim": "^0.4.1", "@ffmpeg/ffmpeg": "^0.12.15", "@ffmpeg/util": "^0.12.2", - "@fiahfy/icns": "^0.0.7", "@fontsource/azeret-mono": "^5.1.1", "@fontsource/lexend": "^5.1.2", "@fontsource/radio-canada-big": "^5.1.1", @@ -49,7 +48,9 @@ "music-metadata": "^11.0.0", "p-queue": "^8.1.0", "riff-file": "^1.0.3", + "vert-wasm": "^0.0.2", "vite-plugin-static-copy": "^2.2.0", + "vite-plugin-wasm": "^3.4.1", "wasm-vips": "^0.0.11" } } diff --git a/src/lib/converters/index.ts b/src/lib/converters/index.ts index 3ccb073..d5e60a0 100644 --- a/src/lib/converters/index.ts +++ b/src/lib/converters/index.ts @@ -1,4 +1,3 @@ -import type { Converter } from "./converter.svelte"; import { FFmpegConverter } from "./ffmpeg.svelte"; import { PandocConverter } from "./pandoc.svelte"; import { VertdConverter } from "./vertd.svelte"; diff --git a/src/lib/converters/vips.svelte.ts b/src/lib/converters/vips.svelte.ts index 46e7022..c874add 100644 --- a/src/lib/converters/vips.svelte.ts +++ b/src/lib/converters/vips.svelte.ts @@ -22,10 +22,11 @@ export class VipsConverter extends Converter { new FormatInfo("jpg"), new FormatInfo("webp"), new FormatInfo("gif"), + new FormatInfo("heic", true, false), new FormatInfo("ico", true, false), new FormatInfo("cur", true, false), new FormatInfo("ani", true, false), - new FormatInfo("heic", true, false), + new FormatInfo("icns", true, false), new FormatInfo("nef", true, false), new FormatInfo("cr2", true, false), new FormatInfo("hdr"), diff --git a/src/lib/parse/icns/index.ts b/src/lib/parse/icns/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/lib/workers/vips.ts b/src/lib/workers/vips.ts index 8c76276..f38bede 100644 --- a/src/lib/workers/vips.ts +++ b/src/lib/workers/vips.ts @@ -10,7 +10,7 @@ import { import { makeZip } from "client-zip"; import wasm from "@imagemagick/magick-wasm/magick.wasm?url"; import { parseAni } from "$lib/parse/ani"; -import { Icns } from "@fiahfy/icns/dist"; +import { parseIcns } from "vert-wasm"; const vipsPromise = Vips({ dynamicLibraries: [], @@ -158,8 +158,6 @@ const handleMessage = async (message: any): Promise => { } } - console.log(message.input.from); - const img = MagickImage.create( new Uint8Array(buffer), new MagickReadSettings({ @@ -178,11 +176,59 @@ const handleMessage = async (message: any): Promise => { } if (message.input.from === ".icns") { - const icns = Icns.from(new Uint8Array(buffer)); - console.log(icns); + const icns: Uint8Array[] = parseIcns(new Uint8Array(buffer)); + // Result in vert-wasm maps to a string in JS + if (typeof icns === "string") { + return { + type: "error", + error: `Failed to read ICNS -- ${icns}`, + }; + } + const formats = [ + MagickFormat.Png, + MagickFormat.Jpeg, + MagickFormat.Rgba, + MagickFormat.Rgb, + ]; + const outputs: Uint8Array[] = []; + for (const file of icns) { + for (const format of formats) { + try { + const img = MagickImage.create( + file, + new MagickReadSettings({ + format: format, + }), + ); + const converted = await magickConvert( + img, + message.to, + ); + outputs.push(converted); + break; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (_) { + continue; + } + } + } + + const zip = makeZip( + outputs.map( + (img, i) => + new File([img], `image${i}.${message.to.slice(1)}`), + ), + "images.zip", + ); + const zipBytes = await readToEnd(zip.getReader()); + return { + type: "finished", + output: zipBytes, + zip: true, + }; } - let image = vips.Image.newFromBuffer(buffer, ""); + let image = vips.Image.newFromBuffer(buffer); // check if animated image & keep it animated when converting if (image.getTypeof("n-pages") > 0) { diff --git a/vite.config.ts b/vite.config.ts index c4ec1e8..47d9978 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,6 +2,7 @@ import { sveltekit } from "@sveltejs/kit/vite"; import { defineConfig } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; import svg from "@poppanator/sveltekit-svg"; +import wasm from "vite-plugin-wasm"; export default defineConfig({ plugins: [ @@ -44,6 +45,7 @@ export default defineConfig({ }, ], }), + wasm(), ], optimizeDeps: { exclude: [ From 39d83f65f0dc889e2eb14a0ceb761f0b77eece16 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 15:15:10 +0100 Subject: [PATCH 09/78] fix: i'm so stupid --- vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vite.config.ts b/vite.config.ts index 47d9978..3c3ab3b 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,7 @@ import wasm from "vite-plugin-wasm"; export default defineConfig({ plugins: [ + wasm(), sveltekit(), { name: "vips-request-middleware", @@ -45,7 +46,6 @@ export default defineConfig({ }, ], }), - wasm(), ], optimizeDeps: { exclude: [ From eef8e818729f2b3320fa78d893bca617c5c9574a Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:34:23 +0100 Subject: [PATCH 10/78] fix: build?? --- bun.lock | 33 +++++++++++++++++++++++++++++ package.json | 3 ++- src/lib/converters/pandoc.svelte.ts | 6 ++++-- vite.config.ts | 9 +++++++- 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 805d549..98bb611 100644 --- a/bun.lock +++ b/bun.lock @@ -44,6 +44,7 @@ "typescript": "^5.7.3", "typescript-eslint": "^8.20.0", "vite": "^5.4.11", + "vite-plugin-top-level-await": "^1.5.0", }, }, }, @@ -190,6 +191,8 @@ "@poppanator/sveltekit-svg": ["@poppanator/sveltekit-svg@5.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "svelte": ">=5.x", "svgo": ">=3.x", "vite": ">=5.x" } }, "sha512-b7hk55SF0HjTS+xFgMG20hy6W0F/m+yRA/ZWcjnsa391rB3Ys3desCiUyIKQYcfvcyuRiQCPedUJMYgu00VdCA=="], + "@rollup/plugin-virtual": ["@rollup/plugin-virtual@3.0.2", "", { "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-10monEYsBp3scM4/ND4LNH5Rxvh3e/cVeL3jWTgZ2SrQ+BmUoQcopVQvnaMcOnykb1VkxUFuDAN+0FnpTFRy2A=="], + "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.9", "", { "os": "android", "cpu": "arm" }, "sha512-qZdlImWXur0CFakn2BJ2znJOdqYZKiedEPEVNTBrpfPjc/YuTGcaYZcdmNFTkUj3DU0ZM/AElcM8Ybww3xVLzA=="], @@ -242,6 +245,32 @@ "@sveltejs/vite-plugin-svelte-inspector": ["@sveltejs/vite-plugin-svelte-inspector@3.0.1", "", { "dependencies": { "debug": "^4.3.7" }, "peerDependencies": { "@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0", "svelte": "^5.0.0-next.96 || ^5.0.0", "vite": "^5.0.0" } }, "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ=="], + "@swc/core": ["@swc/core@1.11.21", "", { "dependencies": { "@swc/counter": "^0.1.3", "@swc/types": "^0.1.21" }, "optionalDependencies": { "@swc/core-darwin-arm64": "1.11.21", "@swc/core-darwin-x64": "1.11.21", "@swc/core-linux-arm-gnueabihf": "1.11.21", "@swc/core-linux-arm64-gnu": "1.11.21", "@swc/core-linux-arm64-musl": "1.11.21", "@swc/core-linux-x64-gnu": "1.11.21", "@swc/core-linux-x64-musl": "1.11.21", "@swc/core-win32-arm64-msvc": "1.11.21", "@swc/core-win32-ia32-msvc": "1.11.21", "@swc/core-win32-x64-msvc": "1.11.21" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" }, "optionalPeers": ["@swc/helpers"] }, "sha512-/Y3BJLcwd40pExmdar8MH2UGGvCBrqNN7hauOMckrEX2Ivcbv3IMhrbGX4od1dnF880Ed8y/E9aStZCIQi0EGw=="], + + "@swc/core-darwin-arm64": ["@swc/core-darwin-arm64@1.11.21", "", { "os": "darwin", "cpu": "arm64" }, "sha512-v6gjw9YFWvKulCw3ZA1dY+LGMafYzJksm1mD4UZFZ9b36CyHFowYVYug1ajYRIRqEvvfIhHUNV660zTLoVFR8g=="], + + "@swc/core-darwin-x64": ["@swc/core-darwin-x64@1.11.21", "", { "os": "darwin", "cpu": "x64" }, "sha512-CUiTiqKlzskwswrx9Ve5NhNoab30L1/ScOfQwr1duvNlFvarC8fvQSgdtpw2Zh3MfnfNPpyLZnYg7ah4kbT9JQ=="], + + "@swc/core-linux-arm-gnueabihf": ["@swc/core-linux-arm-gnueabihf@1.11.21", "", { "os": "linux", "cpu": "arm" }, "sha512-YyBTAFM/QPqt1PscD8hDmCLnqPGKmUZpqeE25HXY8OLjl2MUs8+O4KjwPZZ+OGxpdTbwuWFyMoxjcLy80JODvg=="], + + "@swc/core-linux-arm64-gnu": ["@swc/core-linux-arm64-gnu@1.11.21", "", { "os": "linux", "cpu": "arm64" }, "sha512-DQD+ooJmwpNsh4acrftdkuwl5LNxxg8U4+C/RJNDd7m5FP9Wo4c0URi5U0a9Vk/6sQNh9aSGcYChDpqCDWEcBw=="], + + "@swc/core-linux-arm64-musl": ["@swc/core-linux-arm64-musl@1.11.21", "", { "os": "linux", "cpu": "arm64" }, "sha512-y1L49+snt1a1gLTYPY641slqy55QotPdtRK9Y6jMi4JBQyZwxC8swWYlQWb+MyILwxA614fi62SCNZNznB3XSA=="], + + "@swc/core-linux-x64-gnu": ["@swc/core-linux-x64-gnu@1.11.21", "", { "os": "linux", "cpu": "x64" }, "sha512-NesdBXv4CvVEaFUlqKj+GA4jJMNUzK2NtKOrUNEtTbXaVyNiXjFCSaDajMTedEB0jTAd9ybB0aBvwhgkJUWkWA=="], + + "@swc/core-linux-x64-musl": ["@swc/core-linux-x64-musl@1.11.21", "", { "os": "linux", "cpu": "x64" }, "sha512-qFV60pwpKVOdmX67wqQzgtSrUGWX9Cibnp1CXyqZ9Mmt8UyYGvmGu7p6PMbTyX7vdpVUvWVRf8DzrW2//wmVHg=="], + + "@swc/core-win32-arm64-msvc": ["@swc/core-win32-arm64-msvc@1.11.21", "", { "os": "win32", "cpu": "arm64" }, "sha512-DJJe9k6gXR/15ZZVLv1SKhXkFst8lYCeZRNHH99SlBodvu4slhh/MKQ6YCixINRhCwliHrpXPym8/5fOq8b7Ig=="], + + "@swc/core-win32-ia32-msvc": ["@swc/core-win32-ia32-msvc@1.11.21", "", { "os": "win32", "cpu": "ia32" }, "sha512-TqEXuy6wedId7bMwLIr9byds+mKsaXVHctTN88R1UIBPwJA92Pdk0uxDgip0pEFzHB/ugU27g6d8cwUH3h2eIw=="], + + "@swc/core-win32-x64-msvc": ["@swc/core-win32-x64-msvc@1.11.21", "", { "os": "win32", "cpu": "x64" }, "sha512-BT9BNNbMxdpUM1PPAkYtviaV0A8QcXttjs2MDtOeSqqvSJaPtyM+Fof2/+xSwQDmDEFzbGCcn75M5+xy3lGqpA=="], + + "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], + + "@swc/types": ["@swc/types@0.1.21", "", { "dependencies": { "@swc/counter": "^0.1.3" } }, "sha512-2YEtj5HJVbKivud9N4bpPBAyZhj4S2Ipe5LkUG94alTpr7in/GU/EARgPAd3BwU+YOmFVJC2+kjqhGRi3r0ZpQ=="], + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], "@trysound/sax": ["@trysound/sax@0.2.0", "", {}, "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA=="], @@ -734,12 +763,16 @@ "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + "vert-wasm": ["vert-wasm@0.0.2", "", {}, "sha512-QCkhkATZkWp3OPpIrlcLVd5S4DQXeDEWIPI1eV+Ku+sUVoCM4s9DQbNKtnJ8s4leMW3AD4E7cY/gXB/9TsI3WA=="], "vite": ["vite@5.4.14", "", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA=="], "vite-plugin-static-copy": ["vite-plugin-static-copy@2.3.0", "", { "dependencies": { "chokidar": "^3.5.3", "fast-glob": "^3.2.11", "fs-extra": "^11.1.0", "p-map": "^7.0.3", "picocolors": "^1.0.0" }, "peerDependencies": { "vite": "^5.0.0 || ^6.0.0" } }, "sha512-LLKwhhHetGaCnWz4mas4qqjjguDka6/6b4+SeIohRroj8aCE7QTfiZECfPecslFQkWZ3HdQuq5kOPmWZjNYlKA=="], + "vite-plugin-top-level-await": ["vite-plugin-top-level-await@1.5.0", "", { "dependencies": { "@rollup/plugin-virtual": "^3.0.2", "@swc/core": "^1.10.16", "uuid": "^10.0.0" }, "peerDependencies": { "vite": ">=2.8" } }, "sha512-r/DtuvHrSqUVk23XpG2cl8gjt1aATMG5cjExXL1BUTcSNab6CzkcPua9BPEc9fuTP5UpwClCxUe3+dNGL0yrgQ=="], + "vite-plugin-wasm": ["vite-plugin-wasm@3.4.1", "", { "peerDependencies": { "vite": "^2 || ^3 || ^4 || ^5 || ^6" } }, "sha512-ja3nSo2UCkVeitltJGkS3pfQHAanHv/DqGatdI39ja6McgABlpsZ5hVgl6wuR8Qx5etY3T5qgDQhOWzc5RReZA=="], "vitefu": ["vitefu@1.0.6", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" }, "optionalPeers": ["vite"] }, "sha512-+Rex1GlappUyNN6UfwbVZne/9cYC4+R2XDk9xkNXBKMw6HQagdX9PgZ8V2v1WUSK1wfBLp7qbI1+XSNIlB1xmA=="], diff --git a/package.json b/package.json index 384b169..697d6c9 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "tailwindcss": "^3.4.17", "typescript": "^5.7.3", "typescript-eslint": "^8.20.0", - "vite": "^5.4.11" + "vite": "^5.4.11", + "vite-plugin-top-level-await": "^1.5.0" }, "dependencies": { "@bjorn3/browser_wasi_shim": "^0.4.1", diff --git a/src/lib/converters/pandoc.svelte.ts b/src/lib/converters/pandoc.svelte.ts index cf1459b..db5b6e0 100644 --- a/src/lib/converters/pandoc.svelte.ts +++ b/src/lib/converters/pandoc.svelte.ts @@ -1,7 +1,7 @@ import { VertFile } from "$lib/types"; import { Converter, FormatInfo } from "./converter.svelte"; import { browser } from "$app/environment"; -import PandocWorker from "$lib/workers/pandoc?worker"; +import PandocWorker from "$lib/workers/pandoc?worker&url"; export class PandocConverter extends Converter { public name = "pandoc"; @@ -20,7 +20,9 @@ export class PandocConverter extends Converter { } public async convert(input: VertFile, to: string): Promise { - const worker = new PandocWorker(); + const worker = new Worker(PandocWorker, { + type: "module", + }); worker.postMessage({ type: "load", wasm: this.wasm }); await waitForMessage(worker, "loaded"); worker.postMessage({ diff --git a/vite.config.ts b/vite.config.ts index 3c3ab3b..901c21e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,10 +3,10 @@ import { defineConfig } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; import svg from "@poppanator/sveltekit-svg"; import wasm from "vite-plugin-wasm"; +import topLevelAwait from "vite-plugin-top-level-await"; export default defineConfig({ plugins: [ - wasm(), sveltekit(), { name: "vips-request-middleware", @@ -47,6 +47,10 @@ export default defineConfig({ ], }), ], + worker: { + plugins: () => [wasm(), topLevelAwait()], + format: "es", + }, optimizeDeps: { exclude: [ "wasm-vips", @@ -62,4 +66,7 @@ export default defineConfig({ }, }, }, + build: { + target: "esnext", + }, }); From 0306fdbb5d1dd0d449436fcc031a706453c8c30f Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:36:48 +0100 Subject: [PATCH 11/78] fix: frowns --- vite.config.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 901c21e..814e547 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,7 +3,6 @@ import { defineConfig } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; import svg from "@poppanator/sveltekit-svg"; import wasm from "vite-plugin-wasm"; -import topLevelAwait from "vite-plugin-top-level-await"; export default defineConfig({ plugins: [ @@ -48,7 +47,7 @@ export default defineConfig({ }), ], worker: { - plugins: () => [wasm(), topLevelAwait()], + plugins: () => [wasm()], format: "es", }, optimizeDeps: { From 48c84b4b9fd726b5dc9895c93c205137c90c4212 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:49:47 +0100 Subject: [PATCH 12/78] fix: dev --- vite.config.ts | 56 ++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 814e547..0537844 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,11 +1,11 @@ import { sveltekit } from "@sveltejs/kit/vite"; -import { defineConfig } from "vite"; +import { defineConfig, type PluginOption } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; import svg from "@poppanator/sveltekit-svg"; import wasm from "vite-plugin-wasm"; -export default defineConfig({ - plugins: [ +export default defineConfig(({ command }) => { + const plugins: PluginOption[] = [ sveltekit(), { name: "vips-request-middleware", @@ -45,27 +45,35 @@ export default defineConfig({ }, ], }), - ], - worker: { - plugins: () => [wasm()], - format: "es", - }, - optimizeDeps: { - exclude: [ - "wasm-vips", - "@ffmpeg/core-mt", - "@ffmpeg/ffmpeg", - "@ffmpeg/util", - ], - }, - css: { - preprocessorOptions: { - scss: { - api: "modern", + ]; + + if (command === "serve") { + plugins.unshift(wasm()); + } + + return { + plugins, + worker: { + plugins: () => [wasm()], + format: "es", + }, + optimizeDeps: { + exclude: [ + "wasm-vips", + "@ffmpeg/core-mt", + "@ffmpeg/ffmpeg", + "@ffmpeg/util", + ], + }, + css: { + preprocessorOptions: { + scss: { + api: "modern", + }, }, }, - }, - build: { - target: "esnext", - }, + build: { + target: "esnext", + }, + }; }); From 4e892adc10292f9b74e92191e03ed7ba346fd30a Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 16:51:43 +0100 Subject: [PATCH 13/78] Revert "fix: dev" This reverts commit 48c84b4b9fd726b5dc9895c93c205137c90c4212. --- vite.config.ts | 56 ++++++++++++++++++++++---------------------------- 1 file changed, 24 insertions(+), 32 deletions(-) diff --git a/vite.config.ts b/vite.config.ts index 0537844..814e547 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,11 +1,11 @@ import { sveltekit } from "@sveltejs/kit/vite"; -import { defineConfig, type PluginOption } from "vite"; +import { defineConfig } from "vite"; import { viteStaticCopy } from "vite-plugin-static-copy"; import svg from "@poppanator/sveltekit-svg"; import wasm from "vite-plugin-wasm"; -export default defineConfig(({ command }) => { - const plugins: PluginOption[] = [ +export default defineConfig({ + plugins: [ sveltekit(), { name: "vips-request-middleware", @@ -45,35 +45,27 @@ export default defineConfig(({ command }) => { }, ], }), - ]; - - if (command === "serve") { - plugins.unshift(wasm()); - } - - return { - plugins, - worker: { - plugins: () => [wasm()], - format: "es", - }, - optimizeDeps: { - exclude: [ - "wasm-vips", - "@ffmpeg/core-mt", - "@ffmpeg/ffmpeg", - "@ffmpeg/util", - ], - }, - css: { - preprocessorOptions: { - scss: { - api: "modern", - }, + ], + worker: { + plugins: () => [wasm()], + format: "es", + }, + optimizeDeps: { + exclude: [ + "wasm-vips", + "@ffmpeg/core-mt", + "@ffmpeg/ffmpeg", + "@ffmpeg/util", + ], + }, + css: { + preprocessorOptions: { + scss: { + api: "modern", }, }, - build: { - target: "esnext", - }, - }; + }, + build: { + target: "esnext", + }, }); From cf4d56f4d627cb2cd9a3329c53b2a4b27cb2c410 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:09:35 +0100 Subject: [PATCH 14/78] fix: pix fmt --- src/lib/converters/ffmpeg.svelte.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index b6c5122..1fd4a81 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -89,16 +89,20 @@ export class FFmpegConverter extends Converter { "copy", "cover.png", ]); - await ffmpeg.exec([ + const cmd = [ "-i", "input", "-i", "cover.png", "-loop", "1", + "-pix_fmt", + "yuv420p", ...toArgs(to), "output" + to, - ]); + ]; + console.log(cmd); + await ffmpeg.exec(cmd); } else { await ffmpeg.exec(["-i", "input", "output" + to]); } From 6e853276bc1eefea5a89b602a0cfcb397f394be8 Mon Sep 17 00:00:00 2001 From: not-nullptr <62841684+not-nullptr@users.noreply.github.com> Date: Tue, 15 Apr 2025 17:35:02 +0100 Subject: [PATCH 15/78] fix: OOPS --- src/lib/converters/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/converters/index.ts b/src/lib/converters/index.ts index d5e60a0..3ccb073 100644 --- a/src/lib/converters/index.ts +++ b/src/lib/converters/index.ts @@ -1,3 +1,4 @@ +import type { Converter } from "./converter.svelte"; import { FFmpegConverter } from "./ffmpeg.svelte"; import { PandocConverter } from "./pandoc.svelte"; import { VertdConverter } from "./vertd.svelte"; From 3ad8244e9720f63f837fad8fa346b2316a34b8c5 Mon Sep 17 00:00:00 2001 From: JovannMC Date: Tue, 15 Apr 2025 19:47:39 +0300 Subject: [PATCH 16/78] fix: fix gradients changing due to new format selected --- src/routes/convert/+page.svelte | 43 ++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index 52784bb..75d9aa8 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -5,7 +5,7 @@ import Panel from "$lib/components/visual/Panel.svelte"; import ProgressBar from "$lib/components/visual/ProgressBar.svelte"; import Tooltip from "$lib/components/visual/Tooltip.svelte"; - import { converters } from "$lib/converters"; + import { converters, byNative } from "$lib/converters"; import { effects, files, @@ -44,21 +44,30 @@ $effect(() => { // Set gradient color depending on the file types // TODO: if more file types added, add a "fileType" property to the file object - const allAudio = files.files.every( - (file) => file.findConverter()?.name === "ffmpeg", - ); - const allImages = files.files.every( - (file) => - file.findConverter()?.name !== "ffmpeg" && - file.findConverter()?.name !== "vertd", - ); - const allVideos = files.files.every( - (file) => file.findConverter()?.name === "vertd", - ); - - const allDocuments = files.files.every( - (file) => file.findConverter()?.name === "pandoc", - ); + const allAudio = files.files.every((file) => { + const converter = file + .findConverters() + .sort(byNative(file.from))[0]; + return converter?.name === "ffmpeg"; + }); + const allImages = files.files.every((file) => { + const converter = file + .findConverters() + .sort(byNative(file.from))[0]; + return converter?.name === "libvips"; + }); + const allVideos = files.files.every((file) => { + const converter = file + .findConverters() + .sort(byNative(file.from))[0]; + return converter?.name === "vertd"; + }); + const allDocuments = files.files.every((file) => { + const converter = file + .findConverters() + .sort(byNative(file.from))[0]; + return converter?.name === "pandoc"; + }); if (files.files.length === 1 && files.files[0].blobUrl && !allVideos) { showGradient.set(false); @@ -68,7 +77,7 @@ if ( files.files.length === 0 || - (!allAudio && !allImages && !allVideos) + (!allAudio && !allImages && !allVideos && !allDocuments) ) { gradientColor.set(""); } else { From 21d9358a17af9df5d197630c09d35648a1974a30 Mon Sep 17 00:00:00 2001 From: JovannMC Date: Wed, 28 May 2025 18:30:53 +0300 Subject: [PATCH 17/78] fix: catch no audio streams --- src/lib/converters/ffmpeg.svelte.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index 1fd4a81..b866f6d 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -19,7 +19,6 @@ export class FFmpegConverter extends Converter { new FormatInfo("ogg"), new FormatInfo("aac"), new FormatInfo("m4a"), - // TODO: audio to video where it uses the album cover ...videoFormats.map((f) => new FormatInfo(f, true, true, false)), new FormatInfo("wma"), new FormatInfo("amr"), @@ -63,6 +62,14 @@ export class FFmpegConverter extends Converter { }); ffmpeg.on("log", (l) => { log(["converters", this.name], l.message); + + if (l.message.includes("Stream map '0:a:0' matches no streams.")) { + error( + ["converters", this.name], + `No audio stream found in ${input.name}.`, + ); + addToast("error", `No audio stream found in ${input.name}.`); + } }); const baseURL = "https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm"; From 4d9a11f262ddb48ca55029d2eb4788f9505f8504 Mon Sep 17 00:00:00 2001 From: JovannMC Date: Wed, 28 May 2025 19:05:14 +0300 Subject: [PATCH 18/78] fix: update supported audio formats .alacs don't exist in the wild, you'd only find them as .m4a amr can only be converted from, not to (need to build ffmpeg with the encoder) --- src/lib/converters/ffmpeg.svelte.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts index b866f6d..99489b5 100644 --- a/src/lib/converters/ffmpeg.svelte.ts +++ b/src/lib/converters/ffmpeg.svelte.ts @@ -21,9 +21,8 @@ export class FFmpegConverter extends Converter { new FormatInfo("m4a"), ...videoFormats.map((f) => new FormatInfo(f, true, true, false)), new FormatInfo("wma"), - new FormatInfo("amr"), + new FormatInfo("amr", true, false), new FormatInfo("ac3"), - new FormatInfo("alac"), new FormatInfo("aiff"), ]; From 70862a5abfe8852e91eb86cac967413dd0c6fec8 Mon Sep 17 00:00:00 2001 From: JovannMC Date: Wed, 28 May 2025 22:07:47 +0300 Subject: [PATCH 19/78] fix: many search fixes help --- .../functional/FormatDropdown.svelte | 186 +++++++++++++++--- src/lib/converters/index.ts | 5 +- src/routes/convert/+page.svelte | 1 - 3 files changed, 158 insertions(+), 34 deletions(-) diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index 6ba572f..21815d2 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -22,6 +22,7 @@ let open = $state(false); let hover = $state(false); let dropdown = $state(); + let initialCategory = $state(); let currentCategory = $state(); let searchQuery = $state(""); let dropdownMenu: HTMLElement | undefined = $state(); @@ -35,6 +36,7 @@ ); currentCategory = foundCat || Object.keys(categories)[0] || null; + initialCategory = currentCategory; } else { // find category based on file types const fileFormats = files.files.map((f) => f.from); @@ -45,6 +47,7 @@ ); currentCategory = foundCat || Object.keys(categories)[0] || null; + initialCategory = currentCategory; } } }); @@ -59,35 +62,99 @@ categories[cat].canConvertTo?.includes(currentCategory || ""), ); }); + + const shouldShowFormat = (format: string, category: string): boolean => { + const currentFileExt = files.files[0]?.from; + if (!currentFileExt) return true; + + if (category === initialCategory) { + return true; + } else if ( + initialCategory && + categories[initialCategory].formats.includes(format) + ) { + return false; + } + + const formatInOtherCategories = Object.keys(categories) + .filter((cat) => cat !== category) + .some((cat) => categories[cat].formats.includes(format)); + + if (formatInOtherCategories) { + const nativeCategory = Object.keys(categories).find((cat) => + cat.toLowerCase().includes(format.slice(1)), + ); + + return category === nativeCategory; + } + + return true; + }; + const filteredData = $derived.by(() => { + // if no query, return formats for current category if (!searchQuery) { return { categories: availableCategories, formats: currentCategory - ? categories[currentCategory].formats + ? categories[currentCategory].formats.filter((format) => + shouldShowFormat(format, currentCategory || ""), + ) : [], }; } + const searchLower = searchQuery.toLowerCase(); - // filter categories that have matching formats + // find all categories that have formats matching the search query const matchingCategories = availableCategories.filter((cat) => - categories[cat].formats.some((format) => - format.toLowerCase().includes(searchQuery.toLowerCase()), + categories[cat].formats.some( + (format) => + format.toLowerCase().includes(searchLower) && + shouldShowFormat(format, cat), ), ); + if (matchingCategories.length === 0) { + return { + categories: availableCategories, + formats: [], + }; + } - // only show formats from the current category that match the search - const filteredFormats = - currentCategory && categories[currentCategory] - ? categories[currentCategory].formats.filter((format) => - format - .toLowerCase() - .includes(searchQuery.toLowerCase()), - ) - : []; + // find all matching formats across all categories + const allMatchingFormats = matchingCategories.flatMap((cat) => { + return categories[cat].formats + .filter( + (format) => + format.toLowerCase().includes(searchLower) && + shouldShowFormat(format, cat), + ) + .map((format) => ({ format, category: cat })); + }); + // if current category has no matches, switch to first category that does + const currentCategoryHasMatches = + currentCategory && + allMatchingFormats.some( + (item) => item.category === currentCategory, + ); + if (!currentCategoryHasMatches && matchingCategories.length > 0) { + const newCategory = matchingCategories[0]; + currentCategory = newCategory; + } + + // return formats only from the current category that match the search + const filteredFormats = currentCategory + ? categories[currentCategory].formats.filter( + (format) => + format.toLowerCase().includes(searchLower) && + shouldShowFormat(format, currentCategory || ""), + ) + : []; return { - categories: matchingCategories, + categories: + matchingCategories.length > 0 + ? matchingCategories + : availableCategories, formats: filteredFormats, }; }); @@ -95,6 +162,21 @@ const selectOption = (option: string) => { selected = option; open = false; + + // find the category of this option if it's not in the current category + if ( + currentCategory && + !categories[currentCategory].formats.includes(option) + ) { + const formatCategory = Object.keys(categories).find((cat) => + categories[cat].formats.includes(option), + ); + + if (formatCategory) { + currentCategory = formatCategory; + } + } + onselect?.(option); }; @@ -104,7 +186,34 @@ }; const handleSearch = (event: Event) => { - searchQuery = (event.target as HTMLInputElement).value; + const query = (event.target as HTMLInputElement).value; + searchQuery = query; + + // find which categories have matching formats & switch + if (query) { + const queryLower = query.toLowerCase(); + const categoriesWithMatches = availableCategories.filter((cat) => + categories[cat].formats.some( + (format) => + format.toLowerCase().includes(queryLower) && + shouldShowFormat(format, cat), + ), + ); + + if (categoriesWithMatches.length > 0) { + const currentHasMatches = + currentCategory && + categories[currentCategory].formats.some( + (format) => + format.toLowerCase().includes(queryLower) && + shouldShowFormat(format, currentCategory || ""), + ); + + if (!currentHasMatches) { + currentCategory = categoriesWithMatches[0]; + } + } + } }; onMount(() => { @@ -184,7 +293,6 @@ {#if open}
{}} /> + {#if searchQuery} + + {filteredData.formats.length} + {filteredData.formats.length === 1 + ? "result" + : "results"} + + {/if}
-
{#each filteredData.categories as category} {/each}
-
- {#each filteredData.formats as format} - - {/each} + {#if filteredData.formats.length > 0} + {#each filteredData.formats as format} + + {/each} + {:else} +
+ {searchQuery + ? "No formats match your search" + : "No formats available"} +
+ {/if}
{/if} diff --git a/src/lib/converters/index.ts b/src/lib/converters/index.ts index 4b57c65..43a78cb 100644 --- a/src/lib/converters/index.ts +++ b/src/lib/converters/index.ts @@ -23,8 +23,8 @@ export function getConverterByFormat(format: string) { export const categories: Categories = { image: { formats: [""], canConvertTo: [] }, - video: { formats: [""], canConvertTo: [] }, // add "audio" when "nullptr/experimental-audio-to-video" is implemented - audio: { formats: [""], canConvertTo: [] }, // add "video" when "nullptr/experimental-audio-to-video" is implemented + video: { formats: [""], canConvertTo: ["audio"] }, + audio: { formats: [""], canConvertTo: ["video"] }, docs: { formats: [""], canConvertTo: [] }, }; @@ -48,6 +48,7 @@ categories.docs.formats = .find((c) => c.name === "pandoc") ?.formatStrings((f) => f.toSupported) .filter((f) => f !== ".pdf") || []; + export const byNative = (format: string) => { return (a: Converter, b: Converter) => { const aFormat = a.supportedFormats.find((f) => f.name === format); diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte index 90e37d2..e475b7d 100644 --- a/src/routes/convert/+page.svelte +++ b/src/routes/convert/+page.svelte @@ -113,7 +113,6 @@ {#snippet fileItem(file: VertFile, index: number)} - {@const availableConverters = file.findConverters()} {@const currentConverter = converters.find( (c) => c.formatStrings((f) => f.fromSupported).includes(file.from) && From 35a97920aa1e4be779644af464017fc9cc4b694a Mon Sep 17 00:00:00 2001 From: JovannMC Date: Wed, 28 May 2025 22:14:40 +0300 Subject: [PATCH 20/78] feat: focus when opening dropdown --- .../functional/FormatDropdown.svelte | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/lib/components/functional/FormatDropdown.svelte b/src/lib/components/functional/FormatDropdown.svelte index 21815d2..94a2935 100644 --- a/src/lib/components/functional/FormatDropdown.svelte +++ b/src/lib/components/functional/FormatDropdown.svelte @@ -216,6 +216,23 @@ } }; + const clickDropdown = () => { + open = !open; + if (open) { + setTimeout(() => { + if (dropdownMenu) { + const searchInput = dropdownMenu.querySelector( + "#format-search", + ) as HTMLInputElement; + if (searchInput) { + searchInput.focus(); + searchInput.select(); + } + } + }, 0); // let dropdown open first + } + }; + onMount(() => { const handleClickOutside = (e: MouseEvent) => { if (dropdown && !dropdown.contains(e.target as Node)) { @@ -248,10 +265,9 @@ bind:this={dropdown} > {#if $isMobile} {:else} - +
@@ -229,7 +238,7 @@
@@ -288,7 +297,7 @@ class="btn w-full h-12 bg-accent-red text-black rounded-full mt-4" onclick={donate} > - Donate ${amount.toFixed(2)} USD + {m["about.donate.donate_amount"]({ amount: amount.toFixed(2) })}
@@ -304,7 +313,7 @@ class="row-start-1 col-start-1 flex justify-center items-center" > - Pay now + {m["about.donate.pay_now"]()} {/if} diff --git a/src/lib/sections/about/Resources.svelte b/src/lib/sections/about/Resources.svelte index e75f3f3..696d2a8 100644 --- a/src/lib/sections/about/Resources.svelte +++ b/src/lib/sections/about/Resources.svelte @@ -8,6 +8,7 @@ MailIcon, MessageCircleMoreIcon, } from "lucide-svelte"; + import { m } from "$lib/paraglide/messages"; @@ -17,7 +18,7 @@ > - Resources + {m["about.resources.title"]()} diff --git a/src/lib/sections/about/Sponsors.svelte b/src/lib/sections/about/Sponsors.svelte index 61b914c..59bc865 100644 --- a/src/lib/sections/about/Sponsors.svelte +++ b/src/lib/sections/about/Sponsors.svelte @@ -5,6 +5,8 @@ import { DISCORD_URL } from "$lib/consts"; import { error } from "$lib/logger"; import { addToast } from "$lib/store/ToastProvider"; + import { m } from "$lib/paraglide/messages"; + import { link } from "$lib/paraglide"; let copied = false; let timeoutId: number | undefined; @@ -13,7 +15,7 @@ try { navigator.clipboard.writeText("hello@vert.sh"); copied = true; - addToast("success", "Email copied to clipboard!"); + addToast("success", m["about.sponsors.email_copied"]()); if (timeoutId) clearTimeout(timeoutId); timeoutId = setTimeout(() => (copied = false), 2000); @@ -30,7 +32,7 @@ > - Sponsors + {m["about.sponsors.title"]()}
@@ -43,11 +45,11 @@

- Want to support us? Contact a developer in the Discord - server, or send an email to + {@html link( + "discord_link", + m["about.sponsors.description"](), + DISCORD_URL, + )}

- Why VERT? + {m["about.why.title"]()}

- File converters have always disappointed us. They're ugly, - riddled with ads, and most importantly; slow. We decided to solve this - problem once and for all by making an alternative that solves all those - problems, and more.
-
- All non-video files are converted completely on-device; this means that there's - no delay between sending and receiving the files from a server, and we never - get to snoop on the files you convert. -
-
- Video files get uploaded to our lightning-fast RTX 4000 Ada server. Your - videos stay on there for an hour if you do not convert them. If you do convert - the file, the video will stay on the server for an hour, or until it is downloaded. - The file will then be deleted from our server. + {@html m["about.why.description"]()}

diff --git a/src/lib/sections/settings/Appearance.svelte b/src/lib/sections/settings/Appearance.svelte index 574f3ce..8f36374 100644 --- a/src/lib/sections/settings/Appearance.svelte +++ b/src/lib/sections/settings/Appearance.svelte @@ -14,6 +14,7 @@ SunIcon, } from "lucide-svelte"; import { onMount, onDestroy } from "svelte"; + import { m } from "$lib/paraglide/messages"; let lightElement: HTMLButtonElement; let darkElement: HTMLButtonElement; @@ -70,14 +71,14 @@ class="inline-block -mt-1 mr-2 bg-accent-purple p-2 rounded-full" color="black" /> - Appearance + {m["settings.appearance.title"]()}
-

Brightness theme

+

{m["settings.appearance.brightness_theme"]()}

- Want a sunny flash-bang, or a quiet lonely night? + {m["settings.appearance.brightness_description"]()}

@@ -88,7 +89,7 @@ class="btn {$effects ? "" : "!scale-100"} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center" > - Light + {m["settings.appearance.light"]()}
-

Effect settings

+

{m["settings.appearance.effect_settings"]()}

- Would you like fancy effects, or a more static - experience? + {m["settings.appearance.effect_description"]()}

@@ -118,7 +118,7 @@ class="btn {$effects ? "" : "!scale-100"} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center" > - Enable + {m["settings.appearance.enable"]()}
diff --git a/src/lib/sections/settings/Conversion.svelte b/src/lib/sections/settings/Conversion.svelte index 21e0b26..8b15fd5 100644 --- a/src/lib/sections/settings/Conversion.svelte +++ b/src/lib/sections/settings/Conversion.svelte @@ -3,6 +3,7 @@ import Panel from "$lib/components/visual/Panel.svelte"; import { RefreshCwIcon } from "lucide-svelte"; import type { ISettings } from "./index.svelte"; + import { m } from "$lib/paraglide/messages"; const { settings }: { settings: ISettings } = $props(); @@ -15,25 +16,14 @@ class="inline-block -mt-1 mr-2 bg-accent p-2 rounded-full" color="black" /> - Conversion + {m["settings.conversion.title"]()}
-

File name format

+

{m["settings.conversion.filename_format"]()}

- This will determine the name of the file on download, not including the file extension. - You can put these following templates in the format, which - will be replaced with the relevant information: - %name% - for the original file name, - %extension% - for the original file extension, and - %date% - for a date string of when the file was converted. + {@html m["settings.conversion.filename_description"]()}

@@ -15,26 +17,23 @@ class="inline-block -mt-1 mr-2 bg-accent-blue p-2 rounded-full" color="black" /> - Privacy + {m["settings.privacy.title"]()}
-

Plausible analytics

+

+ {m["settings.privacy.plausible_title"]()} +

- We use Plausible, a privacy-focused analytics tool, to gather - completely anonymous statistics. All data is anonymized - and aggregated, and no identifiable information is ever - sent or stored. You can view the analytics - here and choose to opt out below. + {@html link( + ["plausible_link", "analytics_link"], + m["settings.privacy.plausible_description"](), + [ + "https://plausible.io/privacy-focused-web-analytics", + "https://ats.vert.sh/vert.sh", + ], + )}

@@ -48,7 +47,7 @@ : ''} flex-1 p-4 rounded-lg text-black dynadark:text-white flex items-center justify-center" > - Opt-in + {m["settings.privacy.opt_in"]()}
diff --git a/src/lib/sections/settings/Vertd.svelte b/src/lib/sections/settings/Vertd.svelte index b17ca92..364820a 100644 --- a/src/lib/sections/settings/Vertd.svelte +++ b/src/lib/sections/settings/Vertd.svelte @@ -6,6 +6,8 @@ import clsx from "clsx"; import Dropdown from "$lib/components/functional/Dropdown.svelte"; import { vertdLoaded } from "$lib/store/index.svelte"; + import { m } from "$lib/paraglide/messages"; + import { link } from "$lib/paraglide"; let vertdCommit = $state(null); let abortController: AbortController | null = null; @@ -55,7 +57,7 @@ class="inline-block -mt-1 mr-2 bg-accent-red p-2 rounded-full overflow-visible" color="black" /> - Video conversion + {m["settings.vertd.title"]()}

- status: {vertdCommit + {m["settings.vertd.status"]()} {vertdCommit ? vertdCommit === "loading" - ? "loading..." - : `available, commit id ${vertdCommit}` - : "unavailable (is the url right?)"} + ? m["settings.vertd.loading"]() + : m["settings.vertd.available"]({ commitId: vertdCommit }) + : m["settings.vertd.unavailable"]()}

- The vertd project is a server wrapper for FFmpeg. - This allows you to convert videos through the convenience of - VERT's web interface, while still being able to harness the power - of your GPU to do it as quickly as possible. + {@html m["settings.vertd.description"]()}

- We host a public instance for your convenience, but it is - quite easy to host your own on your PC or server if you know - what you are doing. You can download the server binaries here - the process of setting this up will become easier in the - future, so stay tuned! + {@html link("vertd_link", m["settings.vertd.hosting_info"](), GITHUB_URL_VERTD)}

-

Instance URL

+

{m["settings.vertd.instance_url"]()}

-

Conversion speed

+

{m["settings.vertd.conversion_speed"]()}

- This describes the tradeoff between speed and - quality. Faster speeds will result in lower quality, - but will get the job done quicker. + {m["settings.vertd.speed_description"]()}

{ switch (settings.vertdSpeed) { case "verySlow": - return "Very Slow"; + return m["settings.vertd.speeds.very_slow"](); case "slower": - return "Slower"; + return m["settings.vertd.speeds.slower"](); case "slow": - return "Slow"; + return m["settings.vertd.speeds.slow"](); case "medium": - return "Medium"; + return m["settings.vertd.speeds.medium"](); case "fast": - return "Fast"; + return m["settings.vertd.speeds.fast"](); case "ultraFast": - return "Ultra Fast"; + return m["settings.vertd.speeds.ultra_fast"](); } })()} onselect={(selected) => { switch (selected) { - case "Very Slow": + case m["settings.vertd.speeds.very_slow"](): settings.vertdSpeed = "verySlow"; break; - case "Slower": + case m["settings.vertd.speeds.slower"](): settings.vertdSpeed = "slower"; break; - case "Slow": + case m["settings.vertd.speeds.slow"](): settings.vertdSpeed = "slow"; break; - case "Medium": + case m["settings.vertd.speeds.medium"](): settings.vertdSpeed = "medium"; break; - case "Fast": + case m["settings.vertd.speeds.fast"](): settings.vertdSpeed = "fast"; break; - case "Ultra Fast": + case m["settings.vertd.speeds.ultra_fast"](): settings.vertdSpeed = "ultraFast"; break; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 4f5b691..f45a8ff 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -5,6 +5,8 @@ import { vertdLoaded } from "$lib/store/index.svelte"; import clsx from "clsx"; import { AudioLines, BookText, Check, Film, Image } from "lucide-svelte"; + import { m } from "$lib/paraglide/messages"; + import { link } from "$lib/paraglide"; const getSupportedFormats = (name: string) => converters @@ -20,6 +22,7 @@ ready: boolean; formats: string; icon: typeof Image; + title: string; }; } = $derived({ Images: { @@ -28,16 +31,19 @@ false, formats: getSupportedFormats("imagemagick"), icon: Image, + title: m["upload.images"](), }, Audio: { ready: converters.find((c) => c.name === "ffmpeg")?.ready || false, formats: getSupportedFormats("ffmpeg"), icon: AudioLines, + title: m["upload.audio"](), }, Documents: { ready: converters.find((c) => c.name === "pandoc")?.ready || false, formats: getSupportedFormats("pandoc"), icon: BookText, + title: m["upload.documents"](), }, Video: { ready: @@ -45,6 +51,7 @@ (false && $vertdLoaded), formats: getSupportedFormats("vertd"), icon: Film, + title: m["upload.video"](), }, }); @@ -58,9 +65,10 @@ ); if (formatInfo) { - return `This format can only be converted as ${ - formatInfo.fromSupported ? "input (from)" : "output (to)" - }.`; + const direction = formatInfo.fromSupported + ? m["upload.tooltip.direction_input"]() + : m["upload.tooltip.direction_output"](); + return m["upload.tooltip.partial_support"]({ direction }); } return ""; }; @@ -75,14 +83,12 @@

- The file converter you'll love. + {m["upload.title"]()}

- All image, audio, and document processing is done on your - device. Videos are converted on our lightning-fast servers. - No file size limit, no ads, and completely open source. + {m["upload.subtitle"]()}

@@ -94,7 +100,7 @@
-

VERT supports...

+

{m["upload.supports_title"]()}

{#each Object.entries(status) as [key, s]} @@ -111,31 +117,33 @@ >
- {key} + {s.title}
{#if key === "Video"}

- Video uploads to a server for processing by - default, learn how to set it up locally here. + {@html link( + "wiki_link", + m["upload.video_server_processing"](), + "https://github.com/VERT-sh/VERT/wiki/How-to-convert-video-with-VERT", + )}

{:else}

- Local fully supported + + {m["upload.local_supported"]()}

{/if}

- Status: - {s.ready ? "ready" : "not ready"} + {m["upload.status"]()} + {s.ready + ? m["upload.ready"]() + : m["upload.not_ready"]()}

- Supported formats:  + {m["upload.supported_formats"]()}  {#each s.formats.split(", ") as format, index} {@const isPartial = format.endsWith("*")} {@const formatName = isPartial diff --git a/src/routes/about/+page.svelte b/src/routes/about/+page.svelte index ff2916c..e553c9b 100644 --- a/src/routes/about/+page.svelte +++ b/src/routes/about/+page.svelte @@ -12,6 +12,7 @@ import { addToast } from "$lib/store/ToastProvider"; import { dev } from "$app/environment"; import { page } from "$app/state"; + import { m } from "$lib/paraglide/messages"; // import { dev } from "$app/environment"; // import { page } from "$app/state"; @@ -34,19 +35,19 @@ { name: "nullptr", github: "https://github.com/not-nullptr", - role: "Lead developer; conversion backend, UI implementation", + role: m["about.credits.roles.lead_developer"](), avatar: avatarNullptr, }, { name: "JovannMC", github: "https://github.com/JovannMC", - role: "Developer; UI implementation", + role: m["about.credits.roles.developer"](), avatar: avatarJovannMC, }, { name: "Liam", github: "https://x.com/z2rMC", - role: "Designer; UX, branding, marketing", + role: m["about.credits.roles.designer"](), avatar: avatarLiam, }, ]; @@ -55,13 +56,13 @@ { name: "azurejelly", github: "https://github.com/azurejelly", - role: "Maintaining Docker & CI support", + role: m["about.credits.roles.docker_ci"](), avatar: avatarAzurejelly, }, { name: "Realmy", github: "https://github.com/RealmyTheMan", - role: "Former co-founder & designer", + role: m["about.credits.roles.former_cofounder"](), avatar: avatarRealmy, }, ]; @@ -80,7 +81,7 @@ try { const response = await fetch(`${GITHUB_API_URL}/contributors`); if (!response.ok) { - addToast("error", "Error fetching GitHub contributors"); + addToast("error", m["about.errors.github_contributors"]()); throw new Error(`HTTP error, status: ${response.status}`); } const allContribs = await response.json(); @@ -135,7 +136,7 @@

- About + {m["about.title"]()}

{ // depending on format, select right category and format @@ -120,23 +121,23 @@
{#if !converters.length} - + {:else if isAudio} - + {:else if isVideo} - + {:else if isDocument} - + {:else} - + {/if} @@ -172,11 +173,10 @@ class="h-full flex flex-col text-center justify-center text-failure" >

- We can't convert this file. + {m["convert.errors.cant_convert"]()}

- what are you doing..? you're supposed to run the vertd - server! + {m["convert.errors.vertd_server"]()}

{:else} @@ -184,11 +184,10 @@ class="h-full flex flex-col text-center justify-center text-failure" >

- We can't convert this file. + {m["convert.errors.cant_convert"]()}

- Only image, video, audio, and document files are - supported + {m["convert.errors.unsupported_format"]()}

{/if} @@ -196,10 +195,9 @@
-

We can't convert this file.

+

{m["convert.errors.cant_convert"]()}

- Could not find the vertd instance to start video conversion. - Are you sure the instance URL is set correctly? + {m["convert.errors.vertd_not_found"]()}

{:else} @@ -251,7 +249,7 @@ onselect={(option) => handleSelect(option, file)} />
- +
-

{m["settings.appearance.effect_settings"]()}

+

+ {m["settings.appearance.effect_settings"]()} +

{m["settings.appearance.effect_description"]()}

@@ -115,7 +154,9 @@
+
+
+

+ {m["settings.language.title"]()} +

+

+ {m["settings.language.description"]()} +

+
+
+ +
+
diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts index bd2b376..ac19c1a 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -6,6 +6,7 @@ import { parseBlob, selectCover } from "music-metadata"; import { writable } from "svelte/store"; import { addDialog } from "./DialogProvider"; import PQueue from "p-queue"; +import { getLocale, setLocale } from "$lib/paraglide/runtime"; class Files { public files = $state([]); @@ -292,3 +293,17 @@ export const vertdLoaded = writable(false); export const isMobile = writable(false); export const effects = writable(true); export const theme = writable<"light" | "dark">("light"); +export const locale = writable(getLocale()); +export const availableLocales = { + "en": "English", + "uwu": "UwU", +} + +export function updateLocale(newLocale: string) { + log(["locale"], `set to ${newLocale}`); + localStorage.setItem("locale", newLocale); + // @ts-expect-error shush + setLocale(newLocale, { reload: false }); + // @ts-expect-error shush + locale.set(newLocale); +} \ No newline at end of file diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 729f786..fc7e58d 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -19,6 +19,8 @@ theme, dropping, vertdLoaded, + locale, + updateLocale, } from "$lib/store/index.svelte"; import "$lib/css/app.scss"; import { browser } from "$app/environment"; @@ -73,6 +75,7 @@ theme.set( (localStorage.getItem("theme") as "light" | "dark") || "light", ); + updateLocale(localStorage.getItem("locale") || "en"); Settings.instance.load(); diff --git a/src/routes/jpegify/+page.svelte b/src/routes/jpegify/+page.svelte index a1d566b..efb9485 100644 --- a/src/routes/jpegify/+page.svelte +++ b/src/routes/jpegify/+page.svelte @@ -5,6 +5,7 @@ import { files } from "$lib/store/index.svelte"; import { quintOut } from "svelte/easing"; import { blur } from "svelte/transition"; + import { m } from "$lib/paraglide/messages"; const images = $derived( files.files.filter((f) => @@ -31,9 +32,9 @@
-

SECRET JPEGIFY!!!

+

{m["jpegify.title"]()}

- (shh... don't tell anyone!) + {m["jpegify.subtitle"]()}

JPEGIFY {compressionInverted}%!!!{m["jpegify.button"]({ compression: compressionInverted })}
{#each images as file, i (file.id)} @@ -89,7 +90,7 @@ disabled={!!!file.result} class="btn bg-accent text-black rounded-2xl text-2xl w-full mx-auto" > - Download + {m["jpegify.download"]()}
diff --git a/vite.config.ts b/vite.config.ts index 838a94b..3e2c9e6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,4 +1,4 @@ -import { paraglideVitePlugin } from '@inlang/paraglide-js' +import { paraglideVitePlugin } from "@inlang/paraglide-js"; import { sveltekit } from "@sveltejs/kit/vite"; import { defineConfig, type PluginOption } from "vite"; import svg from "@poppanator/sveltekit-svg"; @@ -7,7 +7,11 @@ import wasm from "vite-plugin-wasm"; export default defineConfig(({ command }) => { const plugins: PluginOption[] = [ sveltekit(), - paraglideVitePlugin({ project: './project.inlang', outdir: './src/lib/paraglide' }), + paraglideVitePlugin({ + project: "./project.inlang", + outdir: "./src/lib/paraglide", + strategy: ["globalVariable", "preferredLanguage", "baseLocale"], + }), svg({ includePaths: ["./src/lib/assets"], svgoOptions: { From bb1715682a71fdf5ade4e1a1812e77124790dd15 Mon Sep 17 00:00:00 2001 From: Maya Date: Fri, 25 Jul 2025 22:53:45 +0300 Subject: [PATCH 32/78] fix: language reactivity --- src/routes/+layout.svelte | 56 +++++++++++++++++++++------------------ 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index fc7e58d..ba2f613 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -146,40 +146,44 @@ -
handleDrag(e, true)} - ondragover={(e) => handleDrag(e, true)} - ondragleave={(e) => handleDrag(e, false)} - role="region" -> - +{#key $locale} +
handleDrag(e, true)} + ondragover={(e) => handleDrag(e, true)} + ondragleave={(e) => handleDrag(e, false)} + role="region" + > + -
- - -
+
+ + +
- - -
- {#each locales as locale} - {locale} - {/each} -
+ +
+ {#each locales as locale} + {locale} + {/each} +
- - + + -
- - +
+ + +
-
+{/key} From 0727e32438d227372835176ffe3771fdb1d7e31c Mon Sep 17 00:00:00 2001 From: Maya Date: Sat, 26 Jul 2025 11:02:22 +0300 Subject: [PATCH 33/78] fix: oops --- src/lib/sections/about/Credits.svelte | 2 +- src/lib/sections/about/Sponsors.svelte | 2 +- src/lib/sections/settings/Privacy.svelte | 2 +- src/lib/sections/settings/Vertd.svelte | 2 +- src/lib/store/index.svelte.ts | 30 ++++++++++++++++++++++++ src/routes/+page.svelte | 2 +- 6 files changed, 35 insertions(+), 5 deletions(-) diff --git a/src/lib/sections/about/Credits.svelte b/src/lib/sections/about/Credits.svelte index bbf8ab8..e95af6a 100644 --- a/src/lib/sections/about/Credits.svelte +++ b/src/lib/sections/about/Credits.svelte @@ -3,7 +3,7 @@ import { HeartHandshakeIcon } from "lucide-svelte"; import { GITHUB_URL_VERT } from "$lib/consts"; import { m } from "$lib/paraglide/messages"; - import { link } from "$lib/paraglide"; + import { link } from "$lib/store/index.svelte"; let { mainContribs, notableContribs, ghContribs } = $props(); diff --git a/src/lib/sections/about/Sponsors.svelte b/src/lib/sections/about/Sponsors.svelte index 59bc865..86ea317 100644 --- a/src/lib/sections/about/Sponsors.svelte +++ b/src/lib/sections/about/Sponsors.svelte @@ -6,7 +6,7 @@ import { error } from "$lib/logger"; import { addToast } from "$lib/store/ToastProvider"; import { m } from "$lib/paraglide/messages"; - import { link } from "$lib/paraglide"; + import { link } from "$lib/store/index.svelte"; let copied = false; let timeoutId: number | undefined; diff --git a/src/lib/sections/settings/Privacy.svelte b/src/lib/sections/settings/Privacy.svelte index 3da06bb..5748d5b 100644 --- a/src/lib/sections/settings/Privacy.svelte +++ b/src/lib/sections/settings/Privacy.svelte @@ -4,7 +4,7 @@ import type { ISettings } from "./index.svelte"; import { effects } from "$lib/store/index.svelte"; import { m } from "$lib/paraglide/messages"; - import { link } from "$lib/paraglide"; + import { link } from "$lib/store/index.svelte"; const { settings }: { settings: ISettings } = $props(); diff --git a/src/lib/sections/settings/Vertd.svelte b/src/lib/sections/settings/Vertd.svelte index 364820a..d19546b 100644 --- a/src/lib/sections/settings/Vertd.svelte +++ b/src/lib/sections/settings/Vertd.svelte @@ -7,7 +7,7 @@ import Dropdown from "$lib/components/functional/Dropdown.svelte"; import { vertdLoaded } from "$lib/store/index.svelte"; import { m } from "$lib/paraglide/messages"; - import { link } from "$lib/paraglide"; + import { link } from "$lib/store/index.svelte"; let vertdCommit = $state(null); let abortController: AbortController | null = null; diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts index ac19c1a..66a0c94 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -306,4 +306,34 @@ export function updateLocale(newLocale: string) { setLocale(newLocale, { reload: false }); // @ts-expect-error shush locale.set(newLocale); +} + +export function link( + tag: string | string[], + text: string, + links: string | string[], + newTab?: boolean | boolean[], + className?: string | string[] +) { + if (!text) return ""; + + const tags = Array.isArray(tag) ? tag : [tag]; + const linksArr = Array.isArray(links) ? links : [links]; + const newTabArr = Array.isArray(newTab) ? newTab : [newTab]; + const classArr = Array.isArray(className) ? className : [className]; + + let result = text; + + tags.forEach((t, i) => { + const link = linksArr[i] ?? "#"; + const target = newTabArr[i] ? 'target="_blank" rel="noopener noreferrer"' : ""; + const cls = classArr[i] ? `class="${classArr[i]}"` : ""; + + const regex = new RegExp(`\\[${t}\\](.*?)\\[\\/${t}\\]`, "g"); + result = result.replace(regex, (_, inner) => + `${inner}` + ); + }); + + return result; } \ No newline at end of file diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index d0e5d96..de8b8e2 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -6,7 +6,7 @@ import clsx from "clsx"; import { AudioLines, BookText, Check, Film, Image } from "lucide-svelte"; import { m } from "$lib/paraglide/messages"; - import { link } from "$lib/paraglide"; + import { link } from "$lib/store/index.svelte"; const getSupportedFormats = (name: string) => converters From 3e1dc720ed7e628d27a54ea18bf99d17dd8cda12 Mon Sep 17 00:00:00 2001 From: Maya Date: Sat, 26 Jul 2025 12:51:56 +0300 Subject: [PATCH 34/78] feat: new formats, update existing support fixes #14, #74, #88, #91 + add avif + add jxl + add heif (input) + add arw (input) + add svg (output) + add eps (output) + update bmp (full) + update cur (full) + update ppm (full) --- src/lib/converters/magick.svelte.ts | 14 ++++++++++---- src/routes/+layout.svelte | 6 +----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/lib/converters/magick.svelte.ts b/src/lib/converters/magick.svelte.ts index 056c8a3..2266826 100644 --- a/src/lib/converters/magick.svelte.ts +++ b/src/lib/converters/magick.svelte.ts @@ -22,12 +22,16 @@ export class MagickConverter extends Converter { new FormatInfo("jpg", true, true), new FormatInfo("webp", true, true), new FormatInfo("gif", true, true), - new FormatInfo("heic", true, false), + new FormatInfo("svg", false, true), // converting from SVG unsupported my magick-wasm - suggested to let browser draw with canvas and read image to "convert" (gh issues) + new FormatInfo("jxl", true, true), + new FormatInfo("avif", true, true), + new FormatInfo("heic", true, false), // seems to be unreliable? HEIC/HEIF is very weird if it will actually work + new FormatInfo("heif", true, false), // TODO: .ico files can encode multiple images at various // sizes, bitdepths, etc. we should support that in future new FormatInfo("ico", true, true), - new FormatInfo("bmp", true, false), - new FormatInfo("cur", true, false), + new FormatInfo("bmp", true, true), + new FormatInfo("cur", true, true), new FormatInfo("ani", true, false), new FormatInfo("icns", true, false), new FormatInfo("nef", true, false), @@ -40,10 +44,12 @@ export class MagickConverter extends Converter { new FormatInfo("pfm", true, true), new FormatInfo("pgm", true, true), new FormatInfo("pnm", true, true), - new FormatInfo("ppm", false, true), + new FormatInfo("ppm", true, true), new FormatInfo("tif", true, true), new FormatInfo("tiff", true, true), new FormatInfo("jfif", true, true), + new FormatInfo("eps", false, true), + new FormatInfo("arw", true, false), ]; public readonly reportsProgress = false; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 8f754d4..e12baff 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -2,11 +2,7 @@ import { onMount } from "svelte"; import { goto, beforeNavigate, afterNavigate } from "$app/navigation"; - import { - PUB_PLAUSIBLE_URL, - PUB_HOSTNAME, - PUB_DONATION_URL, - } from "$env/static/public"; + import { PUB_PLAUSIBLE_URL, PUB_HOSTNAME } from "$env/static/public"; import { VERT_NAME } from "$lib/consts"; import * as Layout from "$lib/components/layout"; import * as Navbar from "$lib/components/layout/Navbar"; From 9878929b73a17d52478e5bd33c014ab0e302953c Mon Sep 17 00:00:00 2001 From: Maya Date: Sat, 26 Jul 2025 12:59:24 +0300 Subject: [PATCH 35/78] fix: toast text overflow (hopefully) now fixes #91 --- src/lib/components/visual/Toast.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/components/visual/Toast.svelte b/src/lib/components/visual/Toast.svelte index fe5ab86..3ac637b 100644 --- a/src/lib/components/visual/Toast.svelte +++ b/src/lib/components/visual/Toast.svelte @@ -51,7 +51,7 @@
-

{message}

+

{message}