diff --git a/README.md b/README.md index 0d5828d..7dc83f6 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,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 run preview` and navigate to `http://localhost:4173` 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. diff --git a/package.json b/package.json index 5eab105..69bb382 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "@sveltejs/kit": "^2.0.0", "@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", @@ -40,9 +42,13 @@ "@imagemagick/magick-wasm": "^0.0.31", "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", - "typescript-cookie": "^1.0.6", "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/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/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 076b001..4bf4163 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. @@ -18,13 +18,14 @@ 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 - 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..4a0a2e3 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"; @@ -25,6 +24,8 @@ export class FFmpegConverter extends Converter { ".aiff", ]; + public readonly reportsProgress = true; + constructor() { super(); log(["converters", this.name], `created converter`); @@ -42,19 +43,19 @@ 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) => { + 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 +71,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..639809c 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"; @@ -29,6 +29,8 @@ export class VipsConverter extends Converter { ".tiff", ]; + public readonly reportsProgress = false; + constructor() { super(); log(["converters", this.name], `created converter`); @@ -39,20 +41,23 @@ 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({ + const msg = { type: "convert", - input: input as unknown as IFile, + 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") { @@ -84,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/store/index.svelte.ts b/src/lib/store/index.svelte.ts index 4c88dce..e4aca72 100644 --- a/src/lib/store/index.svelte.ts +++ b/src/lib/store/index.svelte.ts @@ -1,23 +1,20 @@ import { log } from "$lib/logger"; -import type { IFile } from "$lib/types"; +import { VertFile } from "$lib/types"; +import JSCookie from "js-cookie"; 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 { 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/lib/types/conversion-worker.ts b/src/lib/types/conversion-worker.ts index 1a73b7a..daf287f 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: ArrayBufferLike; } interface LoadedMessage { diff --git a/src/lib/types/file.svelte.ts b/src/lib/types/file.svelte.ts new file mode 100644 index 0000000..d82d0ff --- /dev/null +++ b/src/lib/types/file.svelte.ts @@ -0,0 +1,63 @@ +import type { Converter } from "$lib/converters/converter.svelte"; + +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 = $state(null); + + public to = $state(""); + + public blobUrl = $state(); + + public converter: Converter | null = null; + + constructor( + public readonly file: File, + to: 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/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..058cb67 100644 --- a/src/lib/workers/vips.ts +++ b/src/lib/workers/vips.ts @@ -1,4 +1,4 @@ -import type { WorkerMessage, OmitBetterStrict } from "$lib/types"; +import { type WorkerMessage, type OmitBetterStrict } from "$lib/types"; import Vips from "wasm-vips"; const vipsPromise = Vips({ @@ -21,16 +21,14 @@ 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: output.buffer, }; } } diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index a3e9b3d..c5281f1 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,9 +1,20 @@ -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 || ""); + const isFirefox = /firefox/i.test(ua || ""); return { pathname, isMobile, + isFirefox, }; }; 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; }; diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 78f7607..0c4d52f 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -2,9 +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"; import { Check } from "lucide-svelte"; + import jsmediatags from "jsmediatags"; + import type { TagType } from "jsmediatags/types/index.js"; const { data } = $props(); @@ -42,31 +45,55 @@ 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( + new VertFile( + f, + to, + converter, + 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(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 b8fca12..f12f12a 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, outputFilenameOption } 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(() => { @@ -51,11 +58,6 @@ let outputFilename = $state(outputFilenameOption[0]); onMount(() => { - isSm = window.innerWidth < 640; - window.addEventListener("resize", () => { - isSm = window.innerWidth < 640; - }); - // reloads the "output filename" option const savedOption = localStorage.getItem("outputFilename"); if (savedOption) { @@ -80,37 +82,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( - { - 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, - }, - }; - processings[i] = false; - })(), + (async (i) => { + await convert(files.files[i], i); + })(i), ); } @@ -120,6 +94,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 date = new Date().toISOString(); const dlFiles: any[] = []; @@ -133,7 +114,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; @@ -166,6 +147,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, + }; + }; @@ -182,7 +195,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)} - - - - -
-
- {file.from} -
-
+
- -
-
- { - 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}
@@ -456,9 +565,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; }