From 3f0595ccb2b639d7f0f9bed1e0101f6a79bfa968 Mon Sep 17 00:00:00 2001
From: nullptr <62841684+not-nullptr@users.noreply.github.com>
Date: Thu, 14 Nov 2024 11:32:15 +0000
Subject: [PATCH 1/5] feat: refactor ifile (#19)
* chore: refactor IFile to VertFile
* fix: remove debug red progress thing
---
src/lib/converters/converter.svelte.ts | 6 +-
src/lib/converters/ffmpeg.svelte.ts | 23 ++++----
src/lib/converters/magick.svelte.ts | 82 --------------------------
src/lib/converters/vips.svelte.ts | 9 +--
src/lib/store/index.svelte.ts | 13 +---
src/lib/types/conversion-worker.ts | 6 +-
src/lib/types/file.svelte.ts | 25 ++++++++
src/lib/types/file.ts | 5 --
src/lib/types/index.ts | 2 +-
src/lib/workers/magick.ts | 77 ------------------------
src/lib/workers/vips.ts | 19 +++---
src/routes/+page.svelte | 66 +++++++++++++++------
src/routes/convert/+page.svelte | 31 +++-------
13 files changed, 116 insertions(+), 248 deletions(-)
delete mode 100644 src/lib/converters/magick.svelte.ts
create mode 100644 src/lib/types/file.svelte.ts
delete mode 100644 src/lib/types/file.ts
delete mode 100644 src/lib/workers/magick.ts
diff --git a/src/lib/converters/converter.svelte.ts b/src/lib/converters/converter.svelte.ts
index 076b001..97296ce 100644
--- a/src/lib/converters/converter.svelte.ts
+++ b/src/lib/converters/converter.svelte.ts
@@ -1,4 +1,4 @@
-import type { IFile, OmitBetterStrict } from "$lib/types";
+import type { VertFile } from "$lib/types";
/**
* Base class for all converters.
@@ -21,10 +21,10 @@ export class Converter {
public async convert(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
- input: OmitBetterStrict,
+ input: VertFile,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
to: string,
- ): Promise {
+ ): Promise {
throw new Error("Not implemented");
}
}
diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts
index da5bc50..c5bf456 100644
--- a/src/lib/converters/ffmpeg.svelte.ts
+++ b/src/lib/converters/ffmpeg.svelte.ts
@@ -1,6 +1,5 @@
-import type { IFile } from "$lib/types";
+import { VertFile } from "$lib/types";
import { Converter } from "./converter.svelte";
-import type { OmitBetterStrict } from "$lib/types";
import { FFmpeg } from "@ffmpeg/ffmpeg";
import { browser } from "$app/environment";
import { log } from "$lib/logger";
@@ -42,19 +41,23 @@ export class FFmpegConverter extends Converter {
})();
}
- public async convert(
- input: OmitBetterStrict,
- to: string,
- ): Promise {
+ public async convert(input: VertFile, to: string): Promise {
if (!to.startsWith(".")) to = `.${to}`;
const ffmpeg = new FFmpeg();
+ ffmpeg.on("progress", (progress) => {
+ log(
+ ["converters", this.name],
+ `progress for "${input.name}": ${progress.progress * 100}%`,
+ );
+ input.progress = progress.progress * 100;
+ });
const baseURL =
"https://cdn.jsdelivr.net/npm/@ffmpeg/core@0.12.6/dist/esm";
await ffmpeg.load({
coreURL: `${baseURL}/ffmpeg-core.js`,
wasmURL: `${baseURL}/ffmpeg-core.wasm`,
});
- const buf = new Uint8Array(input.buffer);
+ const buf = new Uint8Array(await input.file.arrayBuffer());
await ffmpeg.writeFile("input", buf);
log(
["converters", this.name],
@@ -70,10 +73,6 @@ export class FFmpegConverter extends Converter {
`read ${input.name.split(".").slice(0, -1).join(".") + to} from ffmpeg virtual fs`,
);
ffmpeg.terminate();
- return {
- ...input,
- buffer: output.buffer,
- extension: to,
- };
+ return new VertFile(new File([output], input.name), to);
}
}
diff --git a/src/lib/converters/magick.svelte.ts b/src/lib/converters/magick.svelte.ts
deleted file mode 100644
index 9cfedd7..0000000
--- a/src/lib/converters/magick.svelte.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import type { IFile } from "$lib/types";
-import { Converter } from "./converter.svelte";
-import MagickWorker from "$lib/workers/magick?worker";
-import { browser } from "$app/environment";
-import type { WorkerMessage, OmitBetterStrict } from "$lib/types";
-import { MagickFormat } from "@imagemagick/magick-wasm";
-
-const sortFirst = [".png", ".jpeg", ".jpg", ".webp", ".gif"];
-
-export class MagickConverter extends Converter {
- private worker: Worker = browser ? new MagickWorker() : null!;
- private id = 0;
- public name = "imagemagick";
- public ready = $state(false);
- public supportedFormats = Object.keys(MagickFormat)
- .map((key) => `.${key.toLowerCase()}`)
- .sort((a, b) => {
- const aIndex = sortFirst.indexOf(a);
- const bIndex = sortFirst.indexOf(b);
- if (aIndex === -1 && bIndex === -1) return a.localeCompare(b);
- if (aIndex === -1) return 1;
- if (bIndex === -1) return -1;
- return aIndex - bIndex;
- });
-
- constructor() {
- super();
- if (!browser) return;
- this.worker.onmessage = (e) => {
- const message: WorkerMessage = e.data;
- if (message.type === "loaded") this.ready = true;
- };
- }
-
- public async convert(
- input: OmitBetterStrict,
- to: string,
- ): Promise {
- const res = await this.sendMessage({
- type: "convert",
- input: input as unknown as IFile,
- to,
- });
-
- if (res.type === "finished") {
- return res.output;
- }
-
- if (res.type === "error") {
- throw new Error(res.error);
- }
-
- throw new Error("Unknown message type");
- }
-
- private sendMessage(
- message: OmitBetterStrict,
- ): Promise> {
- const id = this.id++;
- let resolved = false;
- return new Promise((resolve) => {
- const onMessage = (e: MessageEvent) => {
- if (e.data.id === id) {
- this.worker.removeEventListener("message", onMessage);
- resolve(e.data);
- resolved = true;
- }
- };
-
- setTimeout(() => {
- if (!resolved) {
- this.worker.removeEventListener("message", onMessage);
- throw new Error("Timeout");
- }
- }, 60000);
-
- this.worker.addEventListener("message", onMessage);
-
- this.worker.postMessage({ ...message, id });
- });
- }
-}
diff --git a/src/lib/converters/vips.svelte.ts b/src/lib/converters/vips.svelte.ts
index c109118..a6a6072 100644
--- a/src/lib/converters/vips.svelte.ts
+++ b/src/lib/converters/vips.svelte.ts
@@ -1,4 +1,4 @@
-import type { IFile } from "$lib/types";
+import { VertFile } from "$lib/types";
import { Converter } from "./converter.svelte";
import VipsWorker from "$lib/workers/vips?worker";
import { browser } from "$app/environment";
@@ -39,14 +39,11 @@ export class VipsConverter extends Converter {
};
}
- public async convert(
- input: OmitBetterStrict,
- to: string,
- ): Promise {
+ public async convert(input: VertFile, to: string): Promise {
log(["converters", this.name], `converting ${input.name} to ${to}`);
const res = await this.sendMessage({
type: "convert",
- input: input as unknown as IFile,
+ input,
to,
});
diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts
index 02023d7..d1f7068 100644
--- a/src/lib/store/index.svelte.ts
+++ b/src/lib/store/index.svelte.ts
@@ -1,17 +1,8 @@
import { log } from "$lib/logger";
-import type { IFile } from "$lib/types";
+import { VertFile } from "$lib/types";
class Files {
- public files = $state<
- {
- file: File;
- from: string;
- to: string;
- blobUrl: string;
- id: string;
- result?: (IFile & { blobUrl: string; animating: boolean }) | null;
- }[]
- >([]);
+ public files = $state([]);
}
class Theme {
diff --git a/src/lib/types/conversion-worker.ts b/src/lib/types/conversion-worker.ts
index 1a73b7a..c1b5bdc 100644
--- a/src/lib/types/conversion-worker.ts
+++ b/src/lib/types/conversion-worker.ts
@@ -1,14 +1,14 @@
-import type { IFile } from "./file";
+import { VertFile } from "./file.svelte";
interface ConvertMessage {
type: "convert";
- input: IFile;
+ input: VertFile;
to: string;
}
interface FinishedMessage {
type: "finished";
- output: IFile;
+ output: VertFile;
}
interface LoadedMessage {
diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts
new file mode 100644
index 0000000..157fa56
--- /dev/null
+++ b/src/lib/types/file.svelte.ts
@@ -0,0 +1,25 @@
+export class VertFile {
+ public id: string = Math.random().toString(36).slice(2, 8);
+
+ public get from() {
+ return "." + this.file.name.split(".").pop()!;
+ }
+
+ public get name() {
+ return this.file.name;
+ }
+
+ public progress = $state(0);
+ // public result: VertFile | null = null;
+ public result = $state(null);
+
+ public to = $state("");
+
+ constructor(
+ public readonly file: File,
+ to: string,
+ public readonly blobUrl?: string,
+ ) {
+ this.to = to;
+ }
+}
diff --git a/src/lib/types/file.ts b/src/lib/types/file.ts
deleted file mode 100644
index fe42fc0..0000000
--- a/src/lib/types/file.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-export interface IFile {
- name: string;
- extension: string;
- buffer: ArrayBuffer;
-}
diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts
index f1c0a1e..a69c1a0 100644
--- a/src/lib/types/index.ts
+++ b/src/lib/types/index.ts
@@ -1,3 +1,3 @@
-export * from "./file";
+export * from "./file.svelte";
export * from "./util";
export * from "./conversion-worker";
diff --git a/src/lib/workers/magick.ts b/src/lib/workers/magick.ts
deleted file mode 100644
index 2d568e0..0000000
--- a/src/lib/workers/magick.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import type { WorkerMessage, OmitBetterStrict } from "$lib/types";
-import {
- ImageMagick,
- initializeImageMagick,
- MagickFormat,
-} from "@imagemagick/magick-wasm";
-import wasmUrl from "@imagemagick/magick-wasm/magick.wasm?url";
-
-const magickPromise = fetch(wasmUrl)
- .then((r) => r.arrayBuffer())
- .then((r) => initializeImageMagick(r));
-
-magickPromise
- .then(() => {
- postMessage({ type: "loaded" });
- })
- .catch((error) => {
- postMessage({ type: "error", error });
- });
-
-const handleMessage = async (
- message: WorkerMessage,
-): Promise | undefined> => {
- await magickPromise;
- switch (message.type) {
- case "convert": {
- if (!message.to.startsWith(".")) message.to = `.${message.to}`;
- message.to = message.to.slice(1);
-
- // unfortunately this lib uses some hacks to dispose images when the promise is resolved
- // this means we can't promisify it :(
- return new Promise((resolve) => {
- ImageMagick.read(
- new Uint8Array(message.input.buffer),
- (img) => {
- const keys = Object.keys(MagickFormat);
- const values = Object.values(MagickFormat);
- const index = keys.findIndex(
- (key) =>
- key.toLowerCase() === message.to.toLowerCase(),
- );
- const format = values[index];
- img.write(format, (output) => {
- resolve({
- type: "finished",
- output: {
- ...message.input,
- buffer: output,
- extension: message.to,
- },
- });
- });
- img.dispose();
- },
- );
- });
- }
- }
-};
-
-onmessage = async (e) => {
- const message: WorkerMessage = 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/lib/workers/vips.ts b/src/lib/workers/vips.ts
index fbf5dbc..30fcbee 100644
--- a/src/lib/workers/vips.ts
+++ b/src/lib/workers/vips.ts
@@ -1,4 +1,8 @@
-import type { WorkerMessage, OmitBetterStrict } from "$lib/types";
+import {
+ type WorkerMessage,
+ type OmitBetterStrict,
+ VertFile,
+} from "$lib/types";
import Vips from "wasm-vips";
const vipsPromise = Vips({
@@ -21,16 +25,17 @@ const handleMessage = async (
switch (message.type) {
case "convert": {
if (!message.to.startsWith(".")) message.to = `.${message.to}`;
- const image = vips.Image.newFromBuffer(message.input.buffer);
+ const image = vips.Image.newFromBuffer(
+ await message.input.file.arrayBuffer(),
+ );
const output = image.writeToBuffer(message.to);
image.delete();
return {
type: "finished",
- output: {
- ...message.input,
- buffer: output.buffer,
- extension: message.to,
- },
+ output: new VertFile(
+ new File([output.buffer], message.input.name),
+ message.to,
+ ),
};
}
}
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 78f7607..df69afb 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -4,6 +4,7 @@
import { converters } from "$lib/converters";
import { log } from "$lib/logger/index.js";
import { files } from "$lib/store/index.svelte";
+ import { VertFile } from "$lib/types/file.svelte.js";
import { Check } from "lucide-svelte";
const { data } = $props();
@@ -42,31 +43,58 @@
ctx?.drawImage(img, 0, 0, canvas.width, canvas.height);
// get the blob
canvas.toBlob(
- (blob) => {
- resolve({
- file: f,
- from,
- to,
- blobUrl:
- blob === null
- ? ""
- : URL.createObjectURL(blob),
- id: Math.random().toString(36).substring(2),
- });
+ async (blob) => {
+ // resolve({
+ // file: f,
+ // from,
+ // to,
+ // blobUrl:
+ // blob === null
+ // ? ""
+ // : URL.createObjectURL(blob),
+ // id: Math.random().toString(36).substring(2),
+ // buffer: await f.arrayBuffer(),
+ // extension: from,
+ // name: f.name,
+ // result: null,
+ // progress: 0,
+ // });
+ resolve(
+ new VertFile(
+ new File([blob!], f.name, {
+ type: blob!.type,
+ }),
+ to,
+ URL.createObjectURL(blob!),
+ ),
+ );
},
"image/jpeg",
0.75,
);
};
- img.onerror = () => {
- resolve({
- file: f,
- from,
- to,
- blobUrl: "",
- id: Math.random().toString(36).substring(2),
- });
+ img.onerror = async () => {
+ // resolve({
+ // file: f,
+ // from,
+ // to,
+ // blobUrl: "",
+ // id: Math.random().toString(36).substring(2),
+ // name: f.name,
+ // buffer: await f.arrayBuffer(),
+ // extension: from,
+ // result: null,
+ // progress: 0,
+ // });
+ resolve(
+ new VertFile(
+ new File([await f.arrayBuffer()], f.name, {
+ type: f.type,
+ }),
+ to,
+ ),
+ );
};
},
);
diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte
index 4d389a2..d7cb7e0 100644
--- a/src/routes/convert/+page.svelte
+++ b/src/routes/convert/+page.svelte
@@ -81,25 +81,8 @@
if (!converter) throw new Error("No converter found");
const to = file.to;
processings[i] = true;
- const converted = await converter.convert(
- {
- name: file.file.name,
- buffer: await file.file.arrayBuffer(),
- },
- to,
- );
- files.files[i] = {
- ...file,
- result: {
- ...converted,
- blobUrl: URL.createObjectURL(
- new Blob([converted.buffer], {
- type: file.file.type,
- }),
- ),
- animating: true,
- },
- };
+ const converted = await converter.convert(file, to);
+ file.result = converted;
processings[i] = false;
})(),
);
@@ -123,7 +106,7 @@
dlFiles.push({
name: file.file.name.replace(/\.[^/.]+$/, "") + file.to,
lastModified: Date.now(),
- input: result.buffer,
+ input: await result.file.arrayBuffer(),
});
}
if (files.files.length === 0) return;
@@ -257,7 +240,7 @@
);
})()}
+
From b1faad1c8cbcd79c04fc52cc60080894b16be734 Mon Sep 17 00:00:00 2001
From: nullptr <62841684+not-nullptr@users.noreply.github.com>
Date: Thu, 14 Nov 2024 11:34:40 +0000
Subject: [PATCH 2/5] fix: 512 size limit (#20)
---
src/routes/+page.svelte | 40 ++--------------------------------------
1 file changed, 2 insertions(+), 38 deletions(-)
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index df69afb..346640c 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -44,26 +44,9 @@
// get the blob
canvas.toBlob(
async (blob) => {
- // resolve({
- // file: f,
- // from,
- // to,
- // blobUrl:
- // blob === null
- // ? ""
- // : URL.createObjectURL(blob),
- // id: Math.random().toString(36).substring(2),
- // buffer: await f.arrayBuffer(),
- // extension: from,
- // name: f.name,
- // result: null,
- // progress: 0,
- // });
resolve(
new VertFile(
- new File([blob!], f.name, {
- type: blob!.type,
- }),
+ f,
to,
URL.createObjectURL(blob!),
),
@@ -75,26 +58,7 @@
};
img.onerror = async () => {
- // resolve({
- // file: f,
- // from,
- // to,
- // blobUrl: "",
- // id: Math.random().toString(36).substring(2),
- // name: f.name,
- // buffer: await f.arrayBuffer(),
- // extension: from,
- // result: null,
- // progress: 0,
- // });
- resolve(
- new VertFile(
- new File([await f.arrayBuffer()], f.name, {
- type: f.type,
- }),
- to,
- ),
- );
+ resolve(new VertFile(f, to));
};
},
);
From d70df05c6dce2fe1b32961580b7205795c4a4bf9 Mon Sep 17 00:00:00 2001
From: nullptr <62841684+not-nullptr@users.noreply.github.com>
Date: Thu, 14 Nov 2024 14:13:15 +0000
Subject: [PATCH 3/5] fix: dark mode flicker and non-functionality in general
(#22)
---
package.json | 3 ++-
src/hooks.server.ts | 3 ++-
src/lib/store/index.svelte.ts | 6 ++++++
src/routes/+layout.server.ts | 11 ++++++++++-
src/routes/+layout.svelte | 14 +++++++++-----
src/routes/+layout.ts | 10 ++++++----
6 files changed, 35 insertions(+), 12 deletions(-)
diff --git a/package.json b/package.json
index 5eab105..376e914 100644
--- a/package.json
+++ b/package.json
@@ -16,6 +16,7 @@
"@sveltejs/kit": "^2.0.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/eslint": "^9.6.0",
+ "@types/js-cookie": "^3.0.6",
"autoprefixer": "^10.4.20",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
@@ -40,9 +41,9 @@
"@imagemagick/magick-wasm": "^0.0.31",
"client-zip": "^2.4.5",
"clsx": "^2.1.1",
+ "js-cookie": "^3.0.5",
"lucide-svelte": "^0.456.0",
"svelte-adapter-bun": "^0.5.2",
- "typescript-cookie": "^1.0.6",
"wasm-vips": "^0.0.11"
}
}
diff --git a/src/hooks.server.ts b/src/hooks.server.ts
index d28c64a..cd470ec 100644
--- a/src/hooks.server.ts
+++ b/src/hooks.server.ts
@@ -5,7 +5,8 @@ export const handle: Handle = async ({ event, resolve }) => {
if (theme !== "dark" && theme !== "light") {
event.cookies.set("theme", "", {
path: "/",
- sameSite: "strict",
+ sameSite: "lax",
+ expires: new Date(2147483647 * 1000),
});
theme = "";
}
diff --git a/src/lib/store/index.svelte.ts b/src/lib/store/index.svelte.ts
index d1f7068..8b6cca1 100644
--- a/src/lib/store/index.svelte.ts
+++ b/src/lib/store/index.svelte.ts
@@ -1,5 +1,6 @@
import { log } from "$lib/logger";
import { VertFile } from "$lib/types";
+import JSCookie from "js-cookie";
class Files {
public files = $state
([]);
@@ -9,6 +10,11 @@ class Theme {
public dark = $state(false);
public toggle = () => {
this.dark = !this.dark;
+ JSCookie.set("theme", this.dark ? "dark" : "light", {
+ path: "/",
+ sameSite: "lax",
+ expires: 2147483647,
+ });
log(["theme"], `set to ${this.dark ? "dark" : "light"}`);
};
}
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
index a3e9b3d..3e24f66 100644
--- a/src/routes/+layout.server.ts
+++ b/src/routes/+layout.server.ts
@@ -1,4 +1,13 @@
-export const load = ({ url, request }) => {
+export const load = ({ url, request, cookies }) => {
+ // if the "theme" cookie isn't "dark" or "light", reset it
+ const theme = cookies.get("theme") ?? "";
+ if (theme !== "dark" && theme !== "light") {
+ cookies.set("theme", "", {
+ path: "/",
+ sameSite: "lax",
+ expires: new Date(0),
+ });
+ }
const { pathname } = url;
const ua = request.headers.get("user-agent");
const isMobile = /mobile/i.test(ua || "");
diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte
index 8bdfe30..3ab61c7 100644
--- a/src/routes/+layout.svelte
+++ b/src/routes/+layout.svelte
@@ -11,7 +11,7 @@
import { writable } from "svelte/store";
import { MoonIcon, SunIcon } from "lucide-svelte";
import { browser } from "$app/environment";
- import { setCookie } from "typescript-cookie";
+ import JSCookie from "js-cookie";
let { children, data } = $props();
let shouldGoBack = writable(false);
@@ -55,14 +55,18 @@
if (theme.dark) {
document.body.classList.add("dark");
document.body.classList.remove("light");
- setCookie("theme", "dark", {
- sameSite: "strict",
+ JSCookie.set("theme", "dark", {
+ path: "/",
+ sameSite: "lax",
+ expires: 2147483647,
});
} else {
document.body.classList.add("light");
document.body.classList.remove("dark");
- setCookie("theme", "light", {
- sameSite: "strict",
+ JSCookie.set("theme", "light", {
+ path: "/",
+ sameSite: "lax",
+ expires: 2147483647,
});
}
});
diff --git a/src/routes/+layout.ts b/src/routes/+layout.ts
index 8348af2..f10ab4e 100644
--- a/src/routes/+layout.ts
+++ b/src/routes/+layout.ts
@@ -1,18 +1,20 @@
import { browser } from "$app/environment";
import { theme } from "$lib/store/index.svelte";
-import { getCookie, setCookie } from "typescript-cookie";
+import JSCookie from "js-cookie";
export const load = ({ data }) => {
if (!browser) return;
- const themeStr = getCookie("theme");
+ const themeStr = JSCookie.get("theme");
if (typeof themeStr === "undefined") {
theme.dark = window.matchMedia("(prefers-color-scheme: dark)").matches;
- setCookie("theme", theme.dark ? "dark" : "light", {
+ JSCookie.set("theme", theme.dark ? "dark" : "light", {
sameSite: "strict",
+ path: "/",
+ expires: 2147483647,
});
} else {
theme.dark = themeStr === "dark";
}
- theme.dark = getCookie("theme") === "dark";
+ theme.dark = JSCookie.get("theme") === "dark";
return data;
};
From 543144ff329903ef638d615d67ff1cce21151b61 Mon Sep 17 00:00:00 2001
From: azure
Date: Thu, 14 Nov 2024 14:03:13 -0500
Subject: [PATCH 4/5] docs: add docker instructions (#23)
---
README.md | 16 ++++++++++++++++
1 file changed, 16 insertions(+)
diff --git a/README.md b/README.md
index e80ba3c..7570fb1 100644
--- a/README.md
+++ b/README.md
@@ -47,6 +47,22 @@ To build the project for production, run `bun run build`
This will build the site to the `build` folder. You can then start the server with `bun ./build/index.js` and navigate to `http://localhost:3000` to see the application.
+### With Docker
+
+Clone the repository, then build a Docker image with:
+```shell
+$ docker build -t not-nullptr/vert \
+ --build-arg PUB_HOSTNAME=vert.sh \
+ --build-arg PUB_PLAUSIBLE_URL=https://plausible.example.com .
+```
+
+You can then run it by using:
+```shell
+$ docker run --restart unless-stopped -p 3000:3000 -d --name "vert" not-nullptr/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 dettached mode. You can read more about Docker Compose in general [here](https://docs.docker.com/compose/intro/compose-application-model/).
+
## License
This project is licensed under the AGPL-3.0 License, please see the [LICENSE](LICENSE) file for details.
From 531949606a31d747d3e3badc7a75e124e4d7d6b9 Mon Sep 17 00:00:00 2001
From: nullptr <62841684+not-nullptr@users.noreply.github.com>
Date: Thu, 14 Nov 2024 20:02:06 +0000
Subject: [PATCH 5/5] feat: conversion page redesign (#21)
* feat: conversion page redesign
* fix: loading bars reversed
* fix: dark mode flicker and non-functionality in general (#22)
* feat: add delete button, improve loading bar contrast
* feat: remove mobile optimizations
* feat: add way to tell if a converter reports progress
* More shrexy progress bar when progress isn't indicated
* Make progress existance check better
* fix: progress bar
* more UI tweaks
* feat: nicer loading bars
* feat: audio metadata
* feat: asynchronous album covers
---------
Co-authored-by: Realmy <163438634+RealmyTheMan@users.noreply.github.com>
---
package.json | 5 +
patches/jsmediatags@3.9.7.patch | 15 +
src/lib/components/visual/ProgressBar.svelte | 65 +++
.../visual/effects/ProgressiveBlur.svelte | 4 +-
src/lib/converters/converter.svelte.ts | 1 +
src/lib/converters/ffmpeg.svelte.ts | 6 +-
src/lib/converters/vips.svelte.ts | 24 +-
src/lib/types/conversion-worker.ts | 2 +-
src/lib/types/file.svelte.ts | 42 +-
src/lib/workers/vips.ts | 11 +-
src/routes/+layout.server.ts | 2 +
src/routes/+page.svelte | 41 +-
src/routes/convert/+page.svelte | 459 ++++++++++++------
13 files changed, 490 insertions(+), 187 deletions(-)
create mode 100644 patches/jsmediatags@3.9.7.patch
create mode 100644 src/lib/components/visual/ProgressBar.svelte
diff --git a/package.json b/package.json
index 376e914..69bb382 100644
--- a/package.json
+++ b/package.json
@@ -17,6 +17,7 @@
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@types/eslint": "^9.6.0",
"@types/js-cookie": "^3.0.6",
+ "@types/jsmediatags": "^3.9.6",
"autoprefixer": "^10.4.20",
"eslint": "^9.7.0",
"eslint-config-prettier": "^9.1.0",
@@ -42,8 +43,12 @@
"client-zip": "^2.4.5",
"clsx": "^2.1.1",
"js-cookie": "^3.0.5",
+ "jsmediatags": "^3.9.7",
"lucide-svelte": "^0.456.0",
"svelte-adapter-bun": "^0.5.2",
"wasm-vips": "^0.0.11"
+ },
+ "patchedDependencies": {
+ "jsmediatags@3.9.7": "patches/jsmediatags@3.9.7.patch"
}
}
diff --git a/patches/jsmediatags@3.9.7.patch b/patches/jsmediatags@3.9.7.patch
new file mode 100644
index 0000000..b95dfd0
--- /dev/null
+++ b/patches/jsmediatags@3.9.7.patch
@@ -0,0 +1,15 @@
+diff --git a/package.json b/package.json
+index 1265c61a16be5dc94dea97e1a7bcd117b0b5c0fe..602a37452738d778bf705b7a2931a661e363e33c 100644
+--- a/package.json
++++ b/package.json
+@@ -18,8 +18,8 @@
+ "email": "jesse.ditson@gmail.com"
+ }
+ ],
+- "main": "build2/jsmediatags.js",
+- "browser": "dist/jsmediatags.js",
++ "main": "dist/jsmediatags.min.js",
++ "browser": "dist/jsmediatags.min.js",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/aadsm/jsmediatags.git"
diff --git a/src/lib/components/visual/ProgressBar.svelte b/src/lib/components/visual/ProgressBar.svelte
new file mode 100644
index 0000000..0b94052
--- /dev/null
+++ b/src/lib/components/visual/ProgressBar.svelte
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/src/lib/components/visual/effects/ProgressiveBlur.svelte b/src/lib/components/visual/effects/ProgressiveBlur.svelte
index 1bdc6d3..f16cc42 100644
--- a/src/lib/components/visual/effects/ProgressiveBlur.svelte
+++ b/src/lib/components/visual/effects/ProgressiveBlur.svelte
@@ -47,7 +47,7 @@
class="absolute w-full h-full"
style="
z-index: {index + 2};
- backdrop-filter: blur({blurIntensity}px);
+ backdrop-filter: blur( calc({blurIntensity}px * var(--blur-amount, 1)) );
mask: {mask};
"
>
@@ -63,6 +63,6 @@
>
diff --git a/src/lib/converters/converter.svelte.ts b/src/lib/converters/converter.svelte.ts
index 97296ce..4bf4163 100644
--- a/src/lib/converters/converter.svelte.ts
+++ b/src/lib/converters/converter.svelte.ts
@@ -18,6 +18,7 @@ export class Converter {
* @param to The format to convert to. Includes the dot.
*/
public ready: boolean = $state(false);
+ public readonly reportsProgress: boolean = false;
public async convert(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
diff --git a/src/lib/converters/ffmpeg.svelte.ts b/src/lib/converters/ffmpeg.svelte.ts
index c5bf456..4a0a2e3 100644
--- a/src/lib/converters/ffmpeg.svelte.ts
+++ b/src/lib/converters/ffmpeg.svelte.ts
@@ -24,6 +24,8 @@ export class FFmpegConverter extends Converter {
".aiff",
];
+ public readonly reportsProgress = true;
+
constructor() {
super();
log(["converters", this.name], `created converter`);
@@ -45,10 +47,6 @@ export class FFmpegConverter extends Converter {
if (!to.startsWith(".")) to = `.${to}`;
const ffmpeg = new FFmpeg();
ffmpeg.on("progress", (progress) => {
- log(
- ["converters", this.name],
- `progress for "${input.name}": ${progress.progress * 100}%`,
- );
input.progress = progress.progress * 100;
});
const baseURL =
diff --git a/src/lib/converters/vips.svelte.ts b/src/lib/converters/vips.svelte.ts
index a6a6072..639809c 100644
--- a/src/lib/converters/vips.svelte.ts
+++ b/src/lib/converters/vips.svelte.ts
@@ -29,6 +29,8 @@ export class VipsConverter extends Converter {
".tiff",
];
+ public readonly reportsProgress = false;
+
constructor() {
super();
log(["converters", this.name], `created converter`);
@@ -41,15 +43,21 @@ export class VipsConverter extends Converter {
public async convert(input: VertFile, to: string): Promise {
log(["converters", this.name], `converting ${input.name} to ${to}`);
- const res = await this.sendMessage({
+ const msg = {
type: "convert",
- input,
+ input: {
+ file: input.file,
+ name: input.name,
+ to: input.to,
+ from: input.from,
+ },
to,
- });
+ } as WorkerMessage;
+ const res = await this.sendMessage(msg);
if (res.type === "finished") {
log(["converters", this.name], `converted ${input.name} to ${to}`);
- return res.output;
+ return new VertFile(new File([res.output], input.name), to);
}
if (res.type === "error") {
@@ -81,8 +89,12 @@ export class VipsConverter extends Converter {
}, 60000);
this.worker.addEventListener("message", onMessage);
-
- this.worker.postMessage({ ...message, id });
+ const msg = { ...message, id, worker: null };
+ try {
+ this.worker.postMessage(msg);
+ } catch (e) {
+ console.error(e);
+ }
});
}
}
diff --git a/src/lib/types/conversion-worker.ts b/src/lib/types/conversion-worker.ts
index c1b5bdc..daf287f 100644
--- a/src/lib/types/conversion-worker.ts
+++ b/src/lib/types/conversion-worker.ts
@@ -8,7 +8,7 @@ interface ConvertMessage {
interface FinishedMessage {
type: "finished";
- output: VertFile;
+ output: ArrayBufferLike;
}
interface LoadedMessage {
diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts
index 157fa56..d82d0ff 100644
--- a/src/lib/types/file.svelte.ts
+++ b/src/lib/types/file.svelte.ts
@@ -1,3 +1,5 @@
+import type { Converter } from "$lib/converters/converter.svelte";
+
export class VertFile {
public id: string = Math.random().toString(36).slice(2, 8);
@@ -10,16 +12,52 @@ export class VertFile {
}
public progress = $state(0);
- // public result: VertFile | null = null;
public result = $state(null);
public to = $state("");
+ public blobUrl = $state();
+
+ public converter: Converter | null = null;
+
constructor(
public readonly file: File,
to: string,
- public readonly blobUrl?: string,
+ converter?: Converter,
+ blobUrl?: string,
) {
this.to = to;
+ this.converter = converter ?? null;
+ this.convert = this.convert.bind(this);
+ this.download = this.download.bind(this);
+ this.blobUrl = blobUrl;
+ }
+
+ public async convert() {
+ console.log(this.converter);
+ if (!this.converter) throw new Error("No converter found");
+ this.result = null;
+ this.progress = 0;
+ const res = await this.converter.convert(this, this.to);
+ this.result = res;
+ return res;
+ }
+
+ public async download() {
+ if (!this.result) throw new Error("No result found");
+ const blob = URL.createObjectURL(
+ new Blob([await this.result.file.arrayBuffer()], {
+ type: this.to.slice(1),
+ }),
+ );
+ const a = document.createElement("a");
+ a.href = blob;
+ a.download = `VERT-Converted_${new Date().toISOString()}${this.to}`;
+ // force it to not open in a new tab
+ a.target = "_blank";
+ a.style.display = "none";
+ a.click();
+ URL.revokeObjectURL(blob);
+ a.remove();
}
}
diff --git a/src/lib/workers/vips.ts b/src/lib/workers/vips.ts
index 30fcbee..058cb67 100644
--- a/src/lib/workers/vips.ts
+++ b/src/lib/workers/vips.ts
@@ -1,8 +1,4 @@
-import {
- type WorkerMessage,
- type OmitBetterStrict,
- VertFile,
-} from "$lib/types";
+import { type WorkerMessage, type OmitBetterStrict } from "$lib/types";
import Vips from "wasm-vips";
const vipsPromise = Vips({
@@ -32,10 +28,7 @@ const handleMessage = async (
image.delete();
return {
type: "finished",
- output: new VertFile(
- new File([output.buffer], message.input.name),
- message.to,
- ),
+ output: output.buffer,
};
}
}
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts
index 3e24f66..c5281f1 100644
--- a/src/routes/+layout.server.ts
+++ b/src/routes/+layout.server.ts
@@ -11,8 +11,10 @@ export const load = ({ url, request, cookies }) => {
const { pathname } = url;
const ua = request.headers.get("user-agent");
const isMobile = /mobile/i.test(ua || "");
+ const isFirefox = /firefox/i.test(ua || "");
return {
pathname,
isMobile,
+ isFirefox,
};
};
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte
index 346640c..0c4d52f 100644
--- a/src/routes/+page.svelte
+++ b/src/routes/+page.svelte
@@ -2,10 +2,12 @@
import { goto } from "$app/navigation";
import Uploader from "$lib/components/functional/Uploader.svelte";
import { converters } from "$lib/converters";
- import { log } from "$lib/logger/index.js";
+ import { log } from "$lib/logger";
import { files } from "$lib/store/index.svelte";
- import { VertFile } from "$lib/types/file.svelte.js";
+ import { VertFile } from "$lib/types/file.svelte";
import { Check } from "lucide-svelte";
+ import jsmediatags from "jsmediatags";
+ import type { TagType } from "jsmediatags/types/index.js";
const { data } = $props();
@@ -48,6 +50,7 @@
new VertFile(
f,
to,
+ converter,
URL.createObjectURL(blob!),
),
);
@@ -58,7 +61,39 @@
};
img.onerror = async () => {
- resolve(new VertFile(f, to));
+ // resolve(new VertFile(f, to, converter));
+ const reader = new FileReader();
+ const file = new VertFile(f, to, converter);
+ resolve(file);
+ reader.onload = async (e) => {
+ const tags = await new Promise(
+ (resolve, reject) => {
+ jsmediatags.read(
+ new Blob([
+ new Uint8Array(
+ e.target?.result as ArrayBuffer,
+ ),
+ ]),
+ {
+ onSuccess: (tag) => resolve(tag),
+ onError: (error) => reject(error),
+ },
+ );
+ },
+ );
+ const picture = tags.tags.picture;
+ if (!picture) return;
+
+ const blob = new Blob(
+ [new Uint8Array(picture.data)],
+ {
+ type: picture.format,
+ },
+ );
+ const url = URL.createObjectURL(blob);
+ file.blobUrl = url;
+ };
+ reader.readAsArrayBuffer(f);
};
},
);
diff --git a/src/routes/convert/+page.svelte b/src/routes/convert/+page.svelte
index d7cb7e0..667d0d0 100644
--- a/src/routes/convert/+page.svelte
+++ b/src/routes/convert/+page.svelte
@@ -3,14 +3,23 @@
import { blur, duration, flip } from "$lib/animation";
import Dropdown from "$lib/components/functional/Dropdown.svelte";
import ProgressiveBlur from "$lib/components/visual/effects/ProgressiveBlur.svelte";
+ import ProgressBar from "$lib/components/visual/ProgressBar.svelte";
import { converters } from "$lib/converters";
import type { Converter } from "$lib/converters/converter.svelte";
import { log } from "$lib/logger";
import { files } from "$lib/store/index.svelte";
+ import type { VertFile } from "$lib/types";
import clsx from "clsx";
- import { ArrowRight, XIcon } from "lucide-svelte";
+ import { ArrowRight, Disc2Icon, FileAudioIcon, XIcon } from "lucide-svelte";
import { onMount } from "svelte";
import { quintOut } from "svelte/easing";
+ import {
+ fade,
+ type EasingFunction,
+ type TransitionConfig,
+ } from "svelte/transition";
+
+ const { data } = $props();
const reversedFiles = $derived(files.files.slice().reverse());
@@ -18,8 +27,6 @@
Array.from({ length: files.files.length }, () => false),
);
- let isSm = $state(false);
-
let processings = $state([]);
const convertersRequired = $derived.by(() => {
@@ -47,13 +54,6 @@
convertersRequired.every((c) => c.ready),
);
- onMount(() => {
- isSm = window.innerWidth < 640;
- window.addEventListener("resize", () => {
- isSm = window.innerWidth < 640;
- });
- });
-
let disabled = $derived(files.files.some((f) => !f.result));
onMount(() => {
@@ -71,20 +71,9 @@
const promises: Promise[] = [];
for (let i = 0; i < files.files.length; i++) {
promises.push(
- (async () => {
- const file = files.files[i];
- const converter = converters.find(
- (c) =>
- c.supportedFormats.includes(file.from) &&
- c.supportedFormats.includes(file.to),
- );
- if (!converter) throw new Error("No converter found");
- const to = file.to;
- processings[i] = true;
- const converted = await converter.convert(file, to);
- file.result = converted;
- processings[i] = false;
- })(),
+ (async (i) => {
+ await convert(files.files[i], i);
+ })(i),
);
}
@@ -94,6 +83,13 @@
log(["converter"], `converted all files in ${seconds}s`);
};
+ const convert = async (file: VertFile, index: number) => {
+ file.progress = 0;
+ processings[index] = true;
+ await file.convert();
+ processings[index] = false;
+ };
+
const downloadAll = async () => {
const dlFiles: any[] = [];
for (let i = 0; i < files.files.length; i++) {
@@ -137,6 +133,38 @@
URL.revokeObjectURL(url);
a.remove();
};
+
+ const deleteAll = () => {
+ files.files = [];
+ goto("/");
+ };
+
+ export const progBlur = (
+ _: HTMLElement,
+ config:
+ | Partial<{
+ duration: number;
+ easing: EasingFunction;
+ }>
+ | undefined,
+ dir: {
+ direction: "in" | "out" | "both";
+ },
+ ): TransitionConfig => {
+ const prefersReducedMotion = window.matchMedia(
+ "(prefers-reduced-motion: reduce)",
+ ).matches;
+ if (!config) config = {};
+ if (!config.duration) config.duration = 300;
+ if (!config.easing) config.easing = quintOut;
+ return {
+ duration: prefersReducedMotion ? 0 : config?.duration || 300,
+ css: (t) => {
+ return "--blur-amount: " + (dir.direction !== "in" ? t : 1 - t);
+ },
+ easing: config?.easing,
+ };
+ };
@@ -153,7 +181,7 @@
{:else}
{:else}
-
+
+
- {#each reversedFiles as file, i (file.id)}
- {@const converter = (() => {
- return converters.find((c) =>
- c.supportedFormats.includes(file.from),
- );
- })()}
-
+
+ {#each reversedFiles as file, i (file.id)}
+ {@const converter = (() => {
+ return converters.find((c) =>
+ c.supportedFormats.includes(file.from),
+ );
+ })()}
-
- {file.file.name}
-
-
- {#if converter && converter.supportedFormats.includes(file.from)}
-
from
-
{file.from}
-
to
-
- {
- file.result = null;
- }}
- />
-
-
+
-
-
{
- file.result = null;
+
+ {#if processings[files.files.length - i - 1]}
+
+ {:else}
+
+ {file.file.name}
+
+ {/if}
+
+
- {:else}
-
{file.from}
+
+
+
+
+ {#if converter && converter.supportedFormats.includes(file.from)}
+
from
+
{file.from}
+
to
+
+ {
+ file.result =
+ null;
+ }}
+ />
+
+ {:else}
+
{file.from}
-
- is not supported!
-
- {/if}
-
-
-
- {#if converter && converter.supportedFormats.includes(file.from)}
-
-
-
-
-
+
+ is not supported!
+
+ {/if}
+
+
+
+
- {/if}
+ {#if converter && converter.supportedFormats.includes(file.from)}
+
+
+ {#if file.blobUrl}
+
+
+ {:else}
+
+
+
+ {/if}
+
+ {/if}
+
-
- {/each}
-
+ {/each}
+
{/if}
@@ -406,9 +526,28 @@
opacity: 1 !important;
}
+ @keyframes processing {
+ 0% {
+ transform: scale(1);
+ filter: blur(0px);
+ animation-timing-function: ease-in-out;
+ }
+
+ 50% {
+ transform: scale(1.05);
+ filter: blur(4px);
+ animation-timing-function: ease-in-out;
+ }
+
+ 100% {
+ transform: scale(1);
+ filter: blur(0px);
+ animation-timing-function: ease-in-out;
+ }
+ }
+
.processing {
- transform: scale(1.05);
- filter: blur(4px);
+ animation: processing 2000ms infinite;
pointer-events: none;
}