mirror of https://github.com/VERT-sh/VERT.git
feat: ffmpeg support (+ many bugfixes and refactors) (#5)
* Separate menu into custom component * feat: audio conversion support via ffmpeg --------- Co-authored-by: Realmy <163438634+RealmyTheMan@users.noreply.github.com>
This commit is contained in:
parent
b724f066ba
commit
00a855e590
|
@ -32,6 +32,8 @@
|
|||
"vite": "^5.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||
"@ffmpeg/util": "^0.12.1",
|
||||
"@fontsource/azeret-mono": "^5.1.0",
|
||||
"@fontsource/lexend": "^5.1.1",
|
||||
"@imagemagick/magick-wasm": "^0.0.31",
|
||||
|
|
|
@ -67,7 +67,6 @@ export const blur = (
|
|||
).matches;
|
||||
if (typeof config?.opacity === "undefined" && config) config.opacity = true;
|
||||
const isUsingTranslate = !!config?.x || !!config?.y || !!config?.scale;
|
||||
console.log(isUsingTranslate);
|
||||
return {
|
||||
delay: config?.delay || 0,
|
||||
duration: prefersReducedMotion ? 0 : config?.duration || 300,
|
||||
|
@ -155,7 +154,6 @@ export function flip(
|
|||
const [ox, oy] = style.transformOrigin.split(" ").map(parseFloat);
|
||||
const dx = from.left + (from.width * ox) / to.width - (to.left + ox);
|
||||
const dy = from.top + (from.height * oy) / to.height - (to.top + oy);
|
||||
|
||||
const {
|
||||
delay = 0,
|
||||
duration = (d) => Math.sqrt(d) * 120,
|
||||
|
|
|
@ -0,0 +1,76 @@
|
|||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import { fly } from "svelte/transition";
|
||||
import { duration } from "$lib/animation";
|
||||
import { quintOut } from "svelte/easing";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
interface Props {
|
||||
links: {
|
||||
name: string;
|
||||
url: string;
|
||||
activeMatch: (pathname: string) => boolean;
|
||||
}[];
|
||||
shouldGoBack: Writable<boolean> | null;
|
||||
}
|
||||
|
||||
let { links, shouldGoBack = null }: Props = $props();
|
||||
|
||||
let navWidth = $state(1);
|
||||
let linkCount = $derived(links.length);
|
||||
let activeLinkIndex = $derived(
|
||||
links.findIndex((i) => i.activeMatch($page.url.pathname)),
|
||||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:clientWidth={navWidth}
|
||||
class="w-full flex bg-background relative h-16"
|
||||
>
|
||||
{#if activeLinkIndex !== -1}
|
||||
<div
|
||||
class="absolute pointer-events-none top-1 bg-foreground h-[calc(100%-8px)] rounded-xl"
|
||||
style="width: {navWidth / linkCount - 8}px; left: {(navWidth /
|
||||
linkCount) *
|
||||
activeLinkIndex +
|
||||
4}px; transition: {duration - 200}ms ease left;"
|
||||
></div>
|
||||
{/if}
|
||||
{#each links as { name, url } (url)}
|
||||
<a
|
||||
class="w-1/2 px-2 h-[calc(100%-16px)] mt-2 flex items-center justify-center rounded-xl relative font-display overflow-hidden"
|
||||
href={url}
|
||||
onclick={() => {
|
||||
if (shouldGoBack) {
|
||||
const currentIndex = links.findIndex((i) =>
|
||||
i.activeMatch($page.url.pathname),
|
||||
);
|
||||
const nextIndex = links.findIndex((i) =>
|
||||
i.activeMatch(url),
|
||||
);
|
||||
$shouldGoBack = nextIndex < currentIndex;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div class="grid grid-cols-1 grid-rows-1">
|
||||
{#key name}
|
||||
<span
|
||||
class="mix-blend-difference invert col-start-1 row-start-1 text-center"
|
||||
in:fly={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
y: -50,
|
||||
}}
|
||||
out:fly={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
y: 50,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{/key}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
|
@ -0,0 +1,61 @@
|
|||
import type { IFile } from "$lib/types";
|
||||
import { Converter } from "./converter.svelte";
|
||||
import type { OmitBetterStrict } from "$lib/types";
|
||||
import { FFmpeg } from "@ffmpeg/ffmpeg";
|
||||
import { browser } from "$app/environment";
|
||||
|
||||
export class FFmpegConverter extends Converter {
|
||||
private ffmpeg: FFmpeg = null!;
|
||||
public name = "ffmpeg";
|
||||
public ready = $state(false);
|
||||
|
||||
public supportedFormats = [
|
||||
".mp3",
|
||||
".wav",
|
||||
".flac",
|
||||
".ogg",
|
||||
".aac",
|
||||
".m4a",
|
||||
".opus",
|
||||
".wma",
|
||||
".m4a",
|
||||
".amr",
|
||||
".ac3",
|
||||
"alac",
|
||||
".aiff",
|
||||
];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
if (!browser) return;
|
||||
this.ffmpeg = new FFmpeg();
|
||||
(async () => {
|
||||
const baseURL = "https://unpkg.com/@ffmpeg/core@latest/dist/esm";
|
||||
await this.ffmpeg.load({
|
||||
coreURL: `${baseURL}/ffmpeg-core.js`,
|
||||
wasmURL: `${baseURL}/ffmpeg-core.wasm`,
|
||||
});
|
||||
|
||||
this.ready = true;
|
||||
})();
|
||||
}
|
||||
|
||||
public async convert(
|
||||
input: OmitBetterStrict<IFile, "extension">,
|
||||
to: string,
|
||||
): Promise<IFile> {
|
||||
if (!to.startsWith(".")) to = `.${to}`;
|
||||
// clone input.buffer
|
||||
const buf = new Uint8Array(input.buffer);
|
||||
await this.ffmpeg.writeFile("input", buf);
|
||||
await this.ffmpeg.exec(["-i", "input", "output" + to]);
|
||||
const output = (await this.ffmpeg.readFile(
|
||||
"output" + to,
|
||||
)) as unknown as Uint8Array;
|
||||
return {
|
||||
...input,
|
||||
buffer: output.buffer,
|
||||
extension: to,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
import { FFmpegConverter } from "./ffmpeg.svelte";
|
||||
import { VipsConverter } from "./vips.svelte";
|
||||
|
||||
export const converters = [new VipsConverter()];
|
||||
export const converters = [new VipsConverter(), new FFmpegConverter()];
|
||||
|
|
|
@ -11,8 +11,6 @@ class Files {
|
|||
result?: (IFile & { blobUrl: string; animating: boolean }) | null;
|
||||
}[]
|
||||
>([]);
|
||||
public conversionTypes = $state<string[]>([]);
|
||||
public conversionTypesReverse = $derived(this.conversionTypes.reverse());
|
||||
public beenToConverterPage = $state(false);
|
||||
public shouldShowAlert = $derived(
|
||||
!this.beenToConverterPage && this.files.length > 0,
|
||||
|
|
|
@ -6,8 +6,6 @@ import {
|
|||
} from "@imagemagick/magick-wasm";
|
||||
import wasmUrl from "@imagemagick/magick-wasm/magick.wasm?url";
|
||||
|
||||
console.log(wasmUrl);
|
||||
|
||||
const magickPromise = fetch(wasmUrl)
|
||||
.then((r) => r.arrayBuffer())
|
||||
.then((r) => initializeImageMagick(r));
|
||||
|
|
|
@ -5,28 +5,40 @@
|
|||
import { quintOut } from "svelte/easing";
|
||||
import { files } from "$lib/store/index.svelte";
|
||||
import Logo from "$lib/components/visual/svg/Logo.svelte";
|
||||
import { fly } from "svelte/transition";
|
||||
import featuredImage from "$lib/assets/VERT_Feature.webp";
|
||||
import { PUB_HOSTNAME, PUB_PLAUSIBLE_URL } from "$env/static/public";
|
||||
import FancyMenu from "$lib/components/functional/FancyMenu.svelte";
|
||||
import { writable } from "svelte/store";
|
||||
let { children, data } = $props();
|
||||
|
||||
let navWidth = $state(1);
|
||||
let shouldGoBack = $state(false);
|
||||
let shouldGoBack = writable(false);
|
||||
|
||||
const links = $derived<{
|
||||
[key: string]: string;
|
||||
}>({
|
||||
Upload: "/",
|
||||
[files.files.length > 0
|
||||
? `Convert (${files.files.length})`
|
||||
: `Convert`]: "/convert",
|
||||
About: "/about",
|
||||
});
|
||||
|
||||
const linkCount = $derived(Object.keys(links).length);
|
||||
const linkIndex = $derived(
|
||||
Object.keys(links).findIndex((link) => links[link] === data.pathname),
|
||||
);
|
||||
const links = $derived<
|
||||
{
|
||||
name: string;
|
||||
url: string;
|
||||
activeMatch: (pathname: string) => boolean;
|
||||
}[]
|
||||
>([
|
||||
{
|
||||
name: "Upload",
|
||||
url: "/",
|
||||
activeMatch: (pathname) => pathname === "/",
|
||||
},
|
||||
{
|
||||
name:
|
||||
files.files.length > 0
|
||||
? `Convert (${files.files.length})`
|
||||
: `Convert`,
|
||||
url: "/convert",
|
||||
activeMatch: (pathname) => pathname === "/convert",
|
||||
},
|
||||
{
|
||||
name: "About",
|
||||
url: "/about",
|
||||
activeMatch: (pathname) => pathname.startsWith("/about"),
|
||||
},
|
||||
]);
|
||||
|
||||
const maybeNavToHome = (e: DragEvent) => {
|
||||
if (e.dataTransfer?.types.includes("Files")) {
|
||||
|
@ -78,54 +90,7 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
bind:clientWidth={navWidth}
|
||||
class="w-full flex bg-background relative h-16"
|
||||
>
|
||||
<div
|
||||
class="absolute pointer-events-none top-1 bg-foreground h-[calc(100%-8px)] rounded-xl"
|
||||
style="width: {navWidth / linkCount - 8}px; left: {(navWidth /
|
||||
linkCount) *
|
||||
linkIndex +
|
||||
4}px; transition: {duration - 200}ms ease left;"
|
||||
></div>
|
||||
{#each Object.entries(links) as [name, link] (link)}
|
||||
<a
|
||||
class="w-1/2 px-2 h-[calc(100%-16px)] mt-2 flex items-center justify-center rounded-xl relative font-display overflow-hidden"
|
||||
href={link}
|
||||
onclick={() => {
|
||||
const keys = Object.keys(links);
|
||||
const currentIndex = keys.findIndex(
|
||||
(key) => links[key] === data.pathname,
|
||||
);
|
||||
const nextIndex = keys.findIndex(
|
||||
(key) => links[key] === link,
|
||||
);
|
||||
shouldGoBack = nextIndex < currentIndex;
|
||||
}}
|
||||
>
|
||||
<div class="grid grid-cols-1 grid-rows-1">
|
||||
{#key name}
|
||||
<span
|
||||
class="mix-blend-difference invert col-start-1 row-start-1 text-center"
|
||||
in:fly={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
y: -50,
|
||||
}}
|
||||
out:fly={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
y: 50,
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{/key}
|
||||
</div>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
<FancyMenu {links} {shouldGoBack} />
|
||||
</div>
|
||||
<div class="w-full max-w-screen-lg grid grid-cols-1 grid-rows-1 relative">
|
||||
{#key data.pathname}
|
||||
|
@ -137,7 +102,7 @@
|
|||
easing: quintOut,
|
||||
blurMultiplier: 12,
|
||||
x: {
|
||||
start: !shouldGoBack ? 250 : -250,
|
||||
start: !$shouldGoBack ? 250 : -250,
|
||||
end: 0,
|
||||
},
|
||||
scale: {
|
||||
|
@ -151,7 +116,7 @@
|
|||
blurMultiplier: 12,
|
||||
x: {
|
||||
start: 0,
|
||||
end: !shouldGoBack ? -250 : 250,
|
||||
end: !$shouldGoBack ? -250 : 250,
|
||||
},
|
||||
scale: {
|
||||
start: 1,
|
||||
|
|
|
@ -12,13 +12,22 @@
|
|||
const runUpload = () => {
|
||||
files.files = [
|
||||
...files.files,
|
||||
...(ourFiles || []).map((f) => ({
|
||||
file: f,
|
||||
from: "." + f.name.split(".").slice(-1),
|
||||
to: converters[0].supportedFormats[0],
|
||||
blobUrl: URL.createObjectURL(f),
|
||||
id: Math.random().toString(36).substring(2),
|
||||
})),
|
||||
...(ourFiles || []).map((f, i) => {
|
||||
const from = "." + f.name.toLowerCase().split(".").slice(-1);
|
||||
const converter = converters.find((c) =>
|
||||
c.supportedFormats.includes(from),
|
||||
);
|
||||
const to =
|
||||
converter?.supportedFormats.find((f) => f !== from) ||
|
||||
converters[0].supportedFormats[0];
|
||||
return {
|
||||
file: f,
|
||||
from,
|
||||
to,
|
||||
blobUrl: URL.createObjectURL(f),
|
||||
id: Math.random().toString(36).substring(2),
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
||||
ourFiles = [];
|
||||
|
|
|
@ -1,15 +1,17 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
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 { converters } from "$lib/converters";
|
||||
import type { Converter } from "$lib/converters/converter.svelte";
|
||||
import { files } from "$lib/store/index.svelte";
|
||||
import clsx from "clsx";
|
||||
import { ArrowRight, XIcon } from "lucide-svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { quintOut } from "svelte/easing";
|
||||
|
||||
const reversed = $derived(files.files.slice().reverse());
|
||||
const reversedFiles = $derived(files.files.slice().reverse());
|
||||
|
||||
let finisheds = $state(
|
||||
Array.from({ length: files.files.length }, () => false),
|
||||
|
@ -17,6 +19,33 @@
|
|||
|
||||
let isSm = $state(false);
|
||||
|
||||
let processings = $state<boolean[]>([]);
|
||||
|
||||
const convertersRequired = $derived.by(() => {
|
||||
const required: Converter[] = [];
|
||||
for (let i = 0; i < files.files.length; i++) {
|
||||
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");
|
||||
required.push(converter);
|
||||
}
|
||||
return Array.from(new Set(required));
|
||||
});
|
||||
|
||||
const multipleConverters = $derived(convertersRequired.length > 1);
|
||||
|
||||
const noMultConverter = $derived(
|
||||
multipleConverters ? null : convertersRequired[0],
|
||||
);
|
||||
|
||||
const allConvertersReady = $derived(
|
||||
convertersRequired.every((c) => c.ready),
|
||||
);
|
||||
|
||||
onMount(() => {
|
||||
isSm = window.innerWidth < 640;
|
||||
window.addEventListener("resize", () => {
|
||||
|
@ -24,10 +53,6 @@
|
|||
});
|
||||
});
|
||||
|
||||
let converterName = $state(converters[0].name);
|
||||
|
||||
let converter = $derived(converters.find((c) => c.name === converterName))!;
|
||||
|
||||
let disabled = $derived(files.files.some((f) => !f.result));
|
||||
|
||||
onMount(() => {
|
||||
|
@ -35,46 +60,25 @@
|
|||
const duration = 575 + i * 50 - 32;
|
||||
setTimeout(() => {
|
||||
finisheds[i] = true;
|
||||
console.log(`finished ${i}`);
|
||||
}, duration);
|
||||
});
|
||||
});
|
||||
|
||||
const convertAll = async () => {
|
||||
// if (!converter.ready) return;
|
||||
// const workingFormats: string[] = [];
|
||||
// try {
|
||||
// await Promise.all(
|
||||
// converter.supportedFormats.map(async (format) => {
|
||||
// try {
|
||||
// const img = files.files[0];
|
||||
// if (!img) return;
|
||||
// console.log(`Converting to ${format}`);
|
||||
// await converter.convert(
|
||||
// {
|
||||
// name: img.file.name,
|
||||
// buffer: await img.file.arrayBuffer(),
|
||||
// },
|
||||
// format,
|
||||
// );
|
||||
// console.log(`Converted to ${format}`);
|
||||
// workingFormats.push(format);
|
||||
// } catch (e: any) {
|
||||
// console.error(e);
|
||||
// }
|
||||
// }),
|
||||
// );
|
||||
// } catch {
|
||||
// console.error("Failed to convert to any format");
|
||||
// }
|
||||
// console.log(workingFormats);
|
||||
// return;
|
||||
files.files.forEach((f) => (f.result = null));
|
||||
const promises: Promise<void>[] = [];
|
||||
for (let i = 0; i < files.files.length; i++) {
|
||||
const file = files.files[i];
|
||||
const to = files.conversionTypes[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,
|
||||
|
@ -94,18 +98,12 @@
|
|||
animating: true,
|
||||
},
|
||||
};
|
||||
await new Promise((r) => setTimeout(r, 750));
|
||||
if (
|
||||
files.files[i].result !== null &&
|
||||
files.files[i].result !== undefined
|
||||
)
|
||||
files.files[i].result!.animating = false;
|
||||
processings[i] = false;
|
||||
})(),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
console.log("done");
|
||||
};
|
||||
|
||||
const downloadAll = async () => {
|
||||
|
@ -118,9 +116,7 @@
|
|||
continue;
|
||||
}
|
||||
dlFiles.push({
|
||||
name:
|
||||
file.file.name.replace(/\.[^/.]+$/, "") +
|
||||
files.conversionTypes[i],
|
||||
name: file.file.name.replace(/\.[^/.]+$/, "") + file.to,
|
||||
lastModified: Date.now(),
|
||||
input: result.buffer,
|
||||
});
|
||||
|
@ -136,7 +132,7 @@
|
|||
const a = document.createElement("a");
|
||||
a.href = blob;
|
||||
a.download = `VERT-Converted_${new Date().toISOString()}${
|
||||
files.conversionTypes[0]
|
||||
files.files[0].to
|
||||
}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(blob);
|
||||
|
@ -180,56 +176,60 @@
|
|||
class="w-full p-4 max-w-screen-lg border-solid flex-col border-2 rounded-2xl border-foreground-muted-alt flex flex-shrink-0"
|
||||
>
|
||||
<h2 class="font-bold text-xl mb-1">Options</h2>
|
||||
<div class="flex flex-col mb-1 w-full gap-4 mt-2">
|
||||
<div class="flex flex-col w-full gap-4 mt-2">
|
||||
<div class="flex flex-col gap-3 w-fit">
|
||||
<h3>Set all target formats</h3>
|
||||
<Dropdown
|
||||
options={converter.supportedFormats}
|
||||
onselect={(o) => {
|
||||
files.conversionTypes = Array.from(
|
||||
{ length: files.files.length },
|
||||
() => o,
|
||||
);
|
||||
<div class="grid grid-rows-1 grid-cols-1">
|
||||
{#if !multipleConverters && noMultConverter}
|
||||
<div
|
||||
transition:blur={{
|
||||
blurMultiplier: 8,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
class="row-start-1 col-start-1 w-fit"
|
||||
>
|
||||
<Dropdown
|
||||
options={noMultConverter.supportedFormats}
|
||||
onselect={(o) => {
|
||||
// files.conversionTypes = Array.from(
|
||||
// { length: files.files.length },
|
||||
// () => o,
|
||||
// );
|
||||
|
||||
files.files.forEach((file) => {
|
||||
file.result = null;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
files.files.forEach((file) => {
|
||||
file.result = null;
|
||||
file.to = o;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="italic w-fit text-foreground-muted-alt h-11 flex items-center row-start-1 col-start-1"
|
||||
transition:blur={{
|
||||
blurMultiplier: 8,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
>
|
||||
The listed files require different
|
||||
converters, so you can't set them in bulk.
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="font-bold text-base mb-1 mt-6">Advanced</h2>
|
||||
<div class="flex flex-col gap-4 mt-2">
|
||||
<div class="flex flex-col gap-3 w-fit">
|
||||
<h3>Converter backend</h3>
|
||||
<Dropdown
|
||||
options={converters.map(
|
||||
(converter) => converter.name,
|
||||
)}
|
||||
bind:selected={converterName}
|
||||
onselect={() => {
|
||||
files.files.forEach((file) => {
|
||||
file.result = null;
|
||||
});
|
||||
files.conversionTypes = Array.from(
|
||||
{ length: files.files.length },
|
||||
() => converter.supportedFormats[0],
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid md:grid-cols-2 gap-3 mt-8">
|
||||
<div class="grid md:grid-cols-2 gap-3 mt-4">
|
||||
<button
|
||||
onclick={convertAll}
|
||||
class={clsx("btn flex-grow", {
|
||||
"btn-highlight": disabled,
|
||||
})}
|
||||
disabled={!converter.ready}
|
||||
disabled={!allConvertersReady}
|
||||
>
|
||||
{#if converter.ready}
|
||||
{#if allConvertersReady}
|
||||
Convert {files.files.length > 1 ? "All" : ""}
|
||||
{:else}
|
||||
Loading...
|
||||
|
@ -245,11 +245,14 @@
|
|||
>
|
||||
</div>
|
||||
</div>
|
||||
{#each reversed as file, i (file.id)}
|
||||
{#each reversedFiles as file, i (file.id)}
|
||||
{@const converter = (() => {
|
||||
return converters.find((c) =>
|
||||
c.supportedFormats.includes(file.from),
|
||||
);
|
||||
})()}
|
||||
<div
|
||||
class={clsx("w-full rounded-xl", {
|
||||
"finished-anim": file.result?.animating,
|
||||
})}
|
||||
class="w-full rounded-xl"
|
||||
animate:flip={{ duration, easing: quintOut }}
|
||||
out:blur={{
|
||||
duration,
|
||||
|
@ -262,12 +265,14 @@
|
|||
"sm:h-16 sm:py-0 py-4 px-3 flex relative flex-shrink-0 items-center w-full rounded-xl",
|
||||
{
|
||||
"initial-fade": !finisheds[i],
|
||||
processing:
|
||||
processings[files.files.length - i - 1],
|
||||
},
|
||||
)}
|
||||
style="--delay: {i * 50}ms; z-index: {files.files
|
||||
.length - i}; border: solid 3px {file.result
|
||||
? 'var(--accent-bg)'
|
||||
: 'var(--fg-muted-alt)'}; transition: border 1000ms ease;"
|
||||
: 'var(--fg-muted-alt)'}; transition: border 1000ms ease; transition: filter {duration}ms var(--transition), transform {duration}ms var(--transition);"
|
||||
>
|
||||
<div
|
||||
class="flex gap-8 sm:gap-0 sm:flex-row flex-col items-center justify-between w-full z-50 relative sm:h-fit h-full"
|
||||
|
@ -288,7 +293,7 @@
|
|||
<div
|
||||
class="flex items-center gap-3 sm:justify-normal w-full sm:w-fit flex-shrink-0"
|
||||
>
|
||||
{#if converter.supportedFormats.includes(file.from)}
|
||||
{#if converter && converter.supportedFormats.includes(file.from)}
|
||||
<span class="sm:block hidden">from</span>
|
||||
<span
|
||||
class="py-2 px-3 font-display bg-foreground text-background rounded-xl sm:block hidden"
|
||||
|
@ -298,10 +303,9 @@
|
|||
<div class="sm:block hidden">
|
||||
<Dropdown
|
||||
options={converter.supportedFormats}
|
||||
bind:selected={files
|
||||
.conversionTypes[
|
||||
bind:selected={files.files[
|
||||
files.files.length - i - 1
|
||||
]}
|
||||
].to}
|
||||
onselect={() => {
|
||||
file.result = null;
|
||||
}}
|
||||
|
@ -324,10 +328,9 @@
|
|||
<div class="w-full sm:hidden block h-full">
|
||||
<Dropdown
|
||||
options={converter.supportedFormats}
|
||||
bind:selected={files
|
||||
.conversionTypes[
|
||||
bind:selected={files.files[
|
||||
files.files.length - 1 - i
|
||||
]}
|
||||
].to}
|
||||
onselect={() => {
|
||||
file.result = null;
|
||||
}}
|
||||
|
@ -349,10 +352,7 @@
|
|||
files.files = files.files.filter(
|
||||
(f) => f !== file,
|
||||
);
|
||||
files.conversionTypes =
|
||||
files.conversionTypes.filter(
|
||||
(_, j) => j !== i,
|
||||
);
|
||||
if (files.files.length === 0) goto("/");
|
||||
}}
|
||||
class="ml-2 mr-1 sm:block hidden"
|
||||
>
|
||||
|
@ -360,7 +360,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if converter.supportedFormats.includes(file.from)}
|
||||
{#if converter && converter.supportedFormats.includes(file.from)}
|
||||
<!-- god knows why, but setting opacity > 0.98 causes a z-ordering issue in firefox ??? -->
|
||||
<div
|
||||
class="absolute top-0 -z-50 left-0 w-full h-full rounded-[10px] overflow-hidden opacity-[0.98]"
|
||||
|
@ -404,23 +404,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
@keyframes finished-animation {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: scale(1.02);
|
||||
filter: blur(4px);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
.initial-fade {
|
||||
animation: initial-transition 600ms var(--delay) var(--transition);
|
||||
opacity: 0;
|
||||
|
@ -431,7 +414,15 @@
|
|||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.finished-anim {
|
||||
animation: finished-animation 750ms var(--transition);
|
||||
.processing {
|
||||
transform: scale(1.05);
|
||||
filter: blur(4px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
transition:
|
||||
filter 500ms var(--transition),
|
||||
transform 500ms var(--transition);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -19,6 +19,11 @@ export default defineConfig({
|
|||
},
|
||||
],
|
||||
optimizeDeps: {
|
||||
exclude: ["wasm-vips"],
|
||||
exclude: [
|
||||
"wasm-vips",
|
||||
"@ffmpeg/core-mt",
|
||||
"@ffmpeg/ffmpeg",
|
||||
"@ffmpeg/util",
|
||||
],
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue