mirror of https://github.com/VERT-sh/VERT.git
feat: audio conversion support via ffmpeg
This commit is contained in:
parent
df4c009ac8
commit
61b43275ed
|
|
@ -32,6 +32,8 @@
|
||||||
"vite": "^5.0.3"
|
"vite": "^5.0.3"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@ffmpeg/ffmpeg": "^0.12.10",
|
||||||
|
"@ffmpeg/util": "^0.12.1",
|
||||||
"@fontsource/azeret-mono": "^5.1.0",
|
"@fontsource/azeret-mono": "^5.1.0",
|
||||||
"@fontsource/lexend": "^5.1.1",
|
"@fontsource/lexend": "^5.1.1",
|
||||||
"@imagemagick/magick-wasm": "^0.0.31",
|
"@imagemagick/magick-wasm": "^0.0.31",
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,6 @@ export const blur = (
|
||||||
).matches;
|
).matches;
|
||||||
if (typeof config?.opacity === "undefined" && config) config.opacity = true;
|
if (typeof config?.opacity === "undefined" && config) config.opacity = true;
|
||||||
const isUsingTranslate = !!config?.x || !!config?.y || !!config?.scale;
|
const isUsingTranslate = !!config?.x || !!config?.y || !!config?.scale;
|
||||||
console.log(isUsingTranslate);
|
|
||||||
return {
|
return {
|
||||||
delay: config?.delay || 0,
|
delay: config?.delay || 0,
|
||||||
duration: prefersReducedMotion ? 0 : config?.duration || 300,
|
duration: prefersReducedMotion ? 0 : config?.duration || 300,
|
||||||
|
|
@ -155,7 +154,6 @@ export function flip(
|
||||||
const [ox, oy] = style.transformOrigin.split(" ").map(parseFloat);
|
const [ox, oy] = style.transformOrigin.split(" ").map(parseFloat);
|
||||||
const dx = from.left + (from.width * ox) / to.width - (to.left + ox);
|
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 dy = from.top + (from.height * oy) / to.height - (to.top + oy);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
delay = 0,
|
delay = 0,
|
||||||
duration = (d) => Math.sqrt(d) * 120,
|
duration = (d) => Math.sqrt(d) * 120,
|
||||||
|
|
|
||||||
|
|
@ -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";
|
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;
|
result?: (IFile & { blobUrl: string; animating: boolean }) | null;
|
||||||
}[]
|
}[]
|
||||||
>([]);
|
>([]);
|
||||||
public conversionTypes = $state<string[]>([]);
|
|
||||||
public conversionTypesReverse = $derived(this.conversionTypes.reverse());
|
|
||||||
public beenToConverterPage = $state(false);
|
public beenToConverterPage = $state(false);
|
||||||
public shouldShowAlert = $derived(
|
public shouldShowAlert = $derived(
|
||||||
!this.beenToConverterPage && this.files.length > 0,
|
!this.beenToConverterPage && this.files.length > 0,
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ import {
|
||||||
} from "@imagemagick/magick-wasm";
|
} from "@imagemagick/magick-wasm";
|
||||||
import wasmUrl from "@imagemagick/magick-wasm/magick.wasm?url";
|
import wasmUrl from "@imagemagick/magick-wasm/magick.wasm?url";
|
||||||
|
|
||||||
console.log(wasmUrl);
|
|
||||||
|
|
||||||
const magickPromise = fetch(wasmUrl)
|
const magickPromise = fetch(wasmUrl)
|
||||||
.then((r) => r.arrayBuffer())
|
.then((r) => r.arrayBuffer())
|
||||||
.then((r) => initializeImageMagick(r));
|
.then((r) => initializeImageMagick(r));
|
||||||
|
|
|
||||||
|
|
@ -12,13 +12,22 @@
|
||||||
const runUpload = () => {
|
const runUpload = () => {
|
||||||
files.files = [
|
files.files = [
|
||||||
...files.files,
|
...files.files,
|
||||||
...(ourFiles || []).map((f) => ({
|
...(ourFiles || []).map((f, i) => {
|
||||||
file: f,
|
const from = "." + f.name.toLowerCase().split(".").slice(-1);
|
||||||
from: "." + f.name.split(".").slice(-1),
|
const converter = converters.find((c) =>
|
||||||
to: converters[0].supportedFormats[0],
|
c.supportedFormats.includes(from),
|
||||||
blobUrl: URL.createObjectURL(f),
|
);
|
||||||
id: Math.random().toString(36).substring(2),
|
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 = [];
|
ourFiles = [];
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,17 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { goto } from "$app/navigation";
|
||||||
import { blur, duration, flip } from "$lib/animation";
|
import { blur, duration, flip } from "$lib/animation";
|
||||||
import Dropdown from "$lib/components/functional/Dropdown.svelte";
|
import Dropdown from "$lib/components/functional/Dropdown.svelte";
|
||||||
import ProgressiveBlur from "$lib/components/visual/effects/ProgressiveBlur.svelte";
|
import ProgressiveBlur from "$lib/components/visual/effects/ProgressiveBlur.svelte";
|
||||||
import { converters } from "$lib/converters";
|
import { converters } from "$lib/converters";
|
||||||
|
import type { Converter } from "$lib/converters/converter.svelte";
|
||||||
import { files } from "$lib/store/index.svelte";
|
import { files } from "$lib/store/index.svelte";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { ArrowRight, XIcon } from "lucide-svelte";
|
import { ArrowRight, XIcon } from "lucide-svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { quintOut } from "svelte/easing";
|
import { quintOut } from "svelte/easing";
|
||||||
|
|
||||||
const reversed = $derived(files.files.slice().reverse());
|
const reversedFiles = $derived(files.files.slice().reverse());
|
||||||
|
|
||||||
let finisheds = $state(
|
let finisheds = $state(
|
||||||
Array.from({ length: files.files.length }, () => false),
|
Array.from({ length: files.files.length }, () => false),
|
||||||
|
|
@ -17,6 +19,33 @@
|
||||||
|
|
||||||
let isSm = $state(false);
|
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(() => {
|
onMount(() => {
|
||||||
isSm = window.innerWidth < 640;
|
isSm = window.innerWidth < 640;
|
||||||
window.addEventListener("resize", () => {
|
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));
|
let disabled = $derived(files.files.some((f) => !f.result));
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
|
@ -35,46 +60,25 @@
|
||||||
const duration = 575 + i * 50 - 32;
|
const duration = 575 + i * 50 - 32;
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
finisheds[i] = true;
|
finisheds[i] = true;
|
||||||
console.log(`finished ${i}`);
|
|
||||||
}, duration);
|
}, duration);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const convertAll = async () => {
|
const convertAll = async () => {
|
||||||
// if (!converter.ready) return;
|
files.files.forEach((f) => (f.result = null));
|
||||||
// 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;
|
|
||||||
const promises: Promise<void>[] = [];
|
const promises: Promise<void>[] = [];
|
||||||
for (let i = 0; i < files.files.length; i++) {
|
for (let i = 0; i < files.files.length; i++) {
|
||||||
const file = files.files[i];
|
|
||||||
const to = files.conversionTypes[i];
|
|
||||||
promises.push(
|
promises.push(
|
||||||
(async () => {
|
(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(
|
const converted = await converter.convert(
|
||||||
{
|
{
|
||||||
name: file.file.name,
|
name: file.file.name,
|
||||||
|
|
@ -94,18 +98,12 @@
|
||||||
animating: true,
|
animating: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
await new Promise((r) => setTimeout(r, 750));
|
processings[i] = false;
|
||||||
if (
|
|
||||||
files.files[i].result !== null &&
|
|
||||||
files.files[i].result !== undefined
|
|
||||||
)
|
|
||||||
files.files[i].result!.animating = false;
|
|
||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
console.log("done");
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const downloadAll = async () => {
|
const downloadAll = async () => {
|
||||||
|
|
@ -118,9 +116,7 @@
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
dlFiles.push({
|
dlFiles.push({
|
||||||
name:
|
name: file.file.name.replace(/\.[^/.]+$/, "") + file.to,
|
||||||
file.file.name.replace(/\.[^/.]+$/, "") +
|
|
||||||
files.conversionTypes[i],
|
|
||||||
lastModified: Date.now(),
|
lastModified: Date.now(),
|
||||||
input: result.buffer,
|
input: result.buffer,
|
||||||
});
|
});
|
||||||
|
|
@ -136,7 +132,7 @@
|
||||||
const a = document.createElement("a");
|
const a = document.createElement("a");
|
||||||
a.href = blob;
|
a.href = blob;
|
||||||
a.download = `VERT-Converted_${new Date().toISOString()}${
|
a.download = `VERT-Converted_${new Date().toISOString()}${
|
||||||
files.conversionTypes[0]
|
files.files[0].to
|
||||||
}`;
|
}`;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(blob);
|
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"
|
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>
|
<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">
|
<div class="flex flex-col gap-3 w-fit">
|
||||||
<h3>Set all target formats</h3>
|
<h3>Set all target formats</h3>
|
||||||
<Dropdown
|
<div class="grid grid-rows-1 grid-cols-1">
|
||||||
options={converter.supportedFormats}
|
{#if !multipleConverters && noMultConverter}
|
||||||
onselect={(o) => {
|
<div
|
||||||
files.conversionTypes = Array.from(
|
transition:blur={{
|
||||||
{ length: files.files.length },
|
blurMultiplier: 8,
|
||||||
() => o,
|
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) => {
|
files.files.forEach((file) => {
|
||||||
file.result = null;
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h2 class="font-bold text-base mb-1 mt-6">Advanced</h2>
|
<div class="grid md:grid-cols-2 gap-3 mt-4">
|
||||||
<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">
|
|
||||||
<button
|
<button
|
||||||
onclick={convertAll}
|
onclick={convertAll}
|
||||||
class={clsx("btn flex-grow", {
|
class={clsx("btn flex-grow", {
|
||||||
"btn-highlight": disabled,
|
"btn-highlight": disabled,
|
||||||
})}
|
})}
|
||||||
disabled={!converter.ready}
|
disabled={!allConvertersReady}
|
||||||
>
|
>
|
||||||
{#if converter.ready}
|
{#if allConvertersReady}
|
||||||
Convert {files.files.length > 1 ? "All" : ""}
|
Convert {files.files.length > 1 ? "All" : ""}
|
||||||
{:else}
|
{:else}
|
||||||
Loading...
|
Loading...
|
||||||
|
|
@ -245,11 +245,14 @@
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<div
|
||||||
class={clsx("w-full rounded-xl", {
|
class="w-full rounded-xl"
|
||||||
"finished-anim": file.result?.animating,
|
|
||||||
})}
|
|
||||||
animate:flip={{ duration, easing: quintOut }}
|
animate:flip={{ duration, easing: quintOut }}
|
||||||
out:blur={{
|
out:blur={{
|
||||||
duration,
|
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",
|
"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],
|
"initial-fade": !finisheds[i],
|
||||||
|
processing:
|
||||||
|
processings[files.files.length - i - 1],
|
||||||
},
|
},
|
||||||
)}
|
)}
|
||||||
style="--delay: {i * 50}ms; z-index: {files.files
|
style="--delay: {i * 50}ms; z-index: {files.files
|
||||||
.length - i}; border: solid 3px {file.result
|
.length - i}; border: solid 3px {file.result
|
||||||
? 'var(--accent-bg)'
|
? '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
|
<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"
|
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
|
<div
|
||||||
class="flex items-center gap-3 sm:justify-normal w-full sm:w-fit flex-shrink-0"
|
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="sm:block hidden">from</span>
|
||||||
<span
|
<span
|
||||||
class="py-2 px-3 font-display bg-foreground text-background rounded-xl sm:block hidden"
|
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">
|
<div class="sm:block hidden">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
options={converter.supportedFormats}
|
options={converter.supportedFormats}
|
||||||
bind:selected={files
|
bind:selected={files.files[
|
||||||
.conversionTypes[
|
|
||||||
files.files.length - i - 1
|
files.files.length - i - 1
|
||||||
]}
|
].to}
|
||||||
onselect={() => {
|
onselect={() => {
|
||||||
file.result = null;
|
file.result = null;
|
||||||
}}
|
}}
|
||||||
|
|
@ -324,10 +328,9 @@
|
||||||
<div class="w-full sm:hidden block h-full">
|
<div class="w-full sm:hidden block h-full">
|
||||||
<Dropdown
|
<Dropdown
|
||||||
options={converter.supportedFormats}
|
options={converter.supportedFormats}
|
||||||
bind:selected={files
|
bind:selected={files.files[
|
||||||
.conversionTypes[
|
|
||||||
files.files.length - 1 - i
|
files.files.length - 1 - i
|
||||||
]}
|
].to}
|
||||||
onselect={() => {
|
onselect={() => {
|
||||||
file.result = null;
|
file.result = null;
|
||||||
}}
|
}}
|
||||||
|
|
@ -349,10 +352,7 @@
|
||||||
files.files = files.files.filter(
|
files.files = files.files.filter(
|
||||||
(f) => f !== file,
|
(f) => f !== file,
|
||||||
);
|
);
|
||||||
files.conversionTypes =
|
if (files.files.length === 0) goto("/");
|
||||||
files.conversionTypes.filter(
|
|
||||||
(_, j) => j !== i,
|
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
class="ml-2 mr-1 sm:block hidden"
|
class="ml-2 mr-1 sm:block hidden"
|
||||||
>
|
>
|
||||||
|
|
@ -360,7 +360,7 @@
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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 ??? -->
|
<!-- god knows why, but setting opacity > 0.98 causes a z-ordering issue in firefox ??? -->
|
||||||
<div
|
<div
|
||||||
class="absolute top-0 -z-50 left-0 w-full h-full rounded-[10px] overflow-hidden opacity-[0.98]"
|
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 {
|
.initial-fade {
|
||||||
animation: initial-transition 600ms var(--delay) var(--transition);
|
animation: initial-transition 600ms var(--delay) var(--transition);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
|
@ -431,7 +414,15 @@
|
||||||
opacity: 1 !important;
|
opacity: 1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.finished-anim {
|
.processing {
|
||||||
animation: finished-animation 750ms var(--transition);
|
transform: scale(1.05);
|
||||||
|
filter: blur(4px);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-list {
|
||||||
|
transition:
|
||||||
|
filter 500ms var(--transition),
|
||||||
|
transform 500ms var(--transition);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,11 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
exclude: ["wasm-vips"],
|
exclude: [
|
||||||
|
"wasm-vips",
|
||||||
|
"@ffmpeg/core-mt",
|
||||||
|
"@ffmpeg/ffmpeg",
|
||||||
|
"@ffmpeg/util",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue