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] 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)} - - - - -
-
- {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}
@@ -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; }