mirror of https://github.com/VERT-sh/VERT.git
feat: remove old pages, add proof
This commit is contained in:
parent
69a4d928d0
commit
7f27ee0ff6
|
@ -1,251 +1,8 @@
|
|||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import Uploader from "$lib/components/functional/Uploader.svelte";
|
||||
import { converters } from "$lib/converters";
|
||||
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();
|
||||
|
||||
let ourFiles = $state<File[]>();
|
||||
|
||||
const runUpload = async () => {
|
||||
const newFilePromises = (ourFiles || []).map(async (f) => {
|
||||
return new Promise<(typeof files.files)[0] | void>(
|
||||
(resolve, reject) => {
|
||||
const from =
|
||||
"." + f.name.toLowerCase().split(".").slice(-1);
|
||||
const converter = converters.find((c) =>
|
||||
c.supportedFormats.includes(from.toLowerCase()),
|
||||
);
|
||||
if (!converter) resolve();
|
||||
const to =
|
||||
converter?.supportedFormats.find((f) => f !== from) ||
|
||||
converters[0].supportedFormats[0];
|
||||
log(
|
||||
["uploader", "converter"],
|
||||
`converting ${from} to ${to} using ${converter?.name || "... no converter??"}`,
|
||||
);
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
const img = new Image();
|
||||
img.src = URL.createObjectURL(f);
|
||||
const maxSize = 512;
|
||||
img.onload = () => {
|
||||
const scale = Math.max(
|
||||
maxSize / img.width,
|
||||
maxSize / img.height,
|
||||
);
|
||||
canvas.width = img.width * scale;
|
||||
canvas.height = img.height * scale;
|
||||
ctx?.drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
// get the blob
|
||||
canvas.toBlob(
|
||||
async (blob) => {
|
||||
resolve(
|
||||
new VertFile(
|
||||
f,
|
||||
to,
|
||||
converter,
|
||||
URL.createObjectURL(blob!),
|
||||
),
|
||||
);
|
||||
},
|
||||
"image/jpeg",
|
||||
0.75,
|
||||
);
|
||||
};
|
||||
|
||||
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<TagType>(
|
||||
(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);
|
||||
};
|
||||
},
|
||||
);
|
||||
});
|
||||
let oldLen = files.files.length;
|
||||
files.files = [
|
||||
...files.files,
|
||||
...(await Promise.all(newFilePromises)).filter(
|
||||
(f) => typeof f !== "undefined",
|
||||
),
|
||||
];
|
||||
let newLen = files.files.length;
|
||||
log(["uploader"], `handled ${newLen - oldLen} files`);
|
||||
ourFiles = [];
|
||||
|
||||
if (files.files.length > 0) goto("/convert");
|
||||
};
|
||||
// this comment was written on 15/11/2024 at 16:01 GMT.
|
||||
// i bet to myself that i could complete this whole redesign implementation
|
||||
// by the time realmy got started on it. i guess we'll see how that goes
|
||||
//
|
||||
// ship fast n break things !!
|
||||
// -- nullptr
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>VERT.sh — Free, fast, and awesome file convert</title>
|
||||
<meta
|
||||
name="title"
|
||||
content="VERT.sh — Free, fast, and awesome file convert"
|
||||
/>
|
||||
<meta
|
||||
name="description"
|
||||
content="With VERT you can convert images to PNG, JPG, WEBP, GIF, AVIF, and more. No ads, no tracking, open source, and all processing is done on your device."
|
||||
/>
|
||||
<meta property="og:type" content="website" />
|
||||
<meta
|
||||
property="og:title"
|
||||
content="VERT.sh — Free, fast, and awesome file convert"
|
||||
/>
|
||||
<meta
|
||||
property="og:description"
|
||||
content="With VERT you can convert images to PNG, JPG, WEBP, GIF, AVIF, and more. No ads, no tracking, open source, and all processing is done on your device."
|
||||
/>
|
||||
<meta property="twitter:card" content="summary_large_image" />
|
||||
<meta
|
||||
property="twitter:title"
|
||||
content="VERT.sh — Free, fast, and awesome file convert"
|
||||
/>
|
||||
<meta
|
||||
property="twitter:description"
|
||||
content="With VERT you can convert images to PNG, JPG, WEBP, GIF, AVIF, and more. No ads, no tracking, open source, and all processing is done on your device."
|
||||
/>
|
||||
</svelte:head>
|
||||
|
||||
{#snippet sellingPoint(text: string)}
|
||||
<li
|
||||
class="grid items-center gap-4"
|
||||
style="grid-template-columns: 2rem auto"
|
||||
>
|
||||
<div
|
||||
class="h-8 w-8 bg-accent-background text-accent-foreground rounded-full flex items-center justify-center"
|
||||
>
|
||||
<Check />
|
||||
</div>
|
||||
<span class="text-lg">{text}</span>
|
||||
</li>
|
||||
{/snippet}
|
||||
|
||||
<div class="[@media(max-height:768px)]:block mt-10 picker-fly">
|
||||
<Uploader
|
||||
isMobile={data.isMobile || false}
|
||||
bind:files={ourFiles}
|
||||
onupload={runUpload}
|
||||
acceptedFormats={[
|
||||
...new Set(converters.flatMap((c) => c.supportedFormats)),
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-20">
|
||||
<h1 class="text-3xl text-center font-display header-fly-in">
|
||||
Free, fast, and awesome file converting <span
|
||||
class="px-2 py-1 text-xl bg-accent-background text-accent-foreground rounded-lg"
|
||||
>BETA</span
|
||||
>
|
||||
</h1>
|
||||
<div class="flex justify-center mt-10">
|
||||
<div class="grid gap-4">
|
||||
<!-- {@render sellingPoint("Very fast, all processing done on device")}
|
||||
{@render sellingPoint("No ads, and open source")}
|
||||
{@render sellingPoint("Beautiful and straightforward UI")} -->
|
||||
{#each ["Very fast, all processing done on device", "No file or size limit", "No ads, and open source", "Beautiful and straightforward UI"] as text, i}
|
||||
<div class="fly-in" style="--delay: {i * 50}ms;">
|
||||
{@render sellingPoint(text)}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* for this page specifically */
|
||||
:global(html, body) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@keyframes fly-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(50px);
|
||||
filter: blur(18px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes picker-fly {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(48px);
|
||||
filter: blur(18px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes header-fly-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px) scale(0.9);
|
||||
filter: blur(18px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
.header-fly-in {
|
||||
animation: header-fly-in var(--transition) 750ms forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.fly-in {
|
||||
animation: fly-in var(--transition) 750ms var(--delay) forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.picker-fly {
|
||||
animation: picker-fly var(--transition) 750ms forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,159 +0,0 @@
|
|||
<script lang="ts">
|
||||
import avatarNullptr from "$lib/assets/avatars/nullptr.jpg";
|
||||
import avatarRealmy from "$lib/assets/avatars/realmy.jpg";
|
||||
|
||||
const multiplier = 50;
|
||||
|
||||
const credits = [
|
||||
{
|
||||
name: "nullptr",
|
||||
avatar: avatarNullptr,
|
||||
url: "https://nullp.tr",
|
||||
description: "conversion backend, UI, animations, promotion",
|
||||
},
|
||||
{
|
||||
name: "Realmy",
|
||||
avatar: avatarRealmy,
|
||||
url: "https://realmy.net",
|
||||
description: "idea, UI, branding, operational costs",
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>About VERT</title>
|
||||
<meta name="title" content="About VERT — VERT.sh" />
|
||||
<meta property="og:title" content="About VERT — VERT.sh" />
|
||||
<meta property="twitter:title" content="About VERT — VERT.sh" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="text-lg mx-auto max-w-screen-md">
|
||||
<h1
|
||||
class="font-display text-3xl text-transition"
|
||||
style="--delay: {0 * multiplier}ms"
|
||||
>
|
||||
⁉️ about VERT
|
||||
</h1>
|
||||
<p class="mt-6 text-transition" style="--delay: {1 * multiplier}ms">
|
||||
You know what sucks? File converters! They're usually riddled with ads,
|
||||
and take an ungodly amount of time to complete. <b
|
||||
>So we made a better one!</b
|
||||
>
|
||||
</p>
|
||||
<p class="mt-4 text-transition" style="--delay: {2 * multiplier}ms">
|
||||
VERT is a file converter that's open source, completely ad free, and
|
||||
much much faster than you're used to. All the converting is done on your
|
||||
device, which makes it both private and very speedy. And it of course
|
||||
has a beautiful UI! ✨
|
||||
</p>
|
||||
|
||||
<h2
|
||||
class="font-display text-3xl mt-12 text-transition"
|
||||
style="--delay: {3 * multiplier}ms"
|
||||
>
|
||||
🖼️ supported formats
|
||||
</h2>
|
||||
<p class="mt-6 text-transition" style="--delay: {4 * multiplier}ms">
|
||||
As of right now, VERT supports image and audio conversion of most
|
||||
popular formats. We'll add support for more formats in the future!
|
||||
</p>
|
||||
|
||||
<h2
|
||||
class="font-display text-3xl mt-12 text-transition"
|
||||
style="--delay: {5 * multiplier}ms"
|
||||
>
|
||||
🔗 resources
|
||||
</h2>
|
||||
<ul class="list-disc list-inside mt-6">
|
||||
<li class="text-transition" style="--delay: {6 * multiplier}ms">
|
||||
<a
|
||||
href="https://github.com/not-nullptr/VERT"
|
||||
class="text-foreground-highlight hover:underline">Source code</a
|
||||
> (hosted on GitHub, licensed under AGPL-3.0)
|
||||
</li>
|
||||
|
||||
<li class="text-transition" style="--delay: {7 * multiplier}ms">
|
||||
<a
|
||||
href="https://discord.gg/8XXZ7TFFrK"
|
||||
class="text-foreground-highlight hover:underline"
|
||||
>Discord server</a
|
||||
> (for chit-chat, suggestions, and support)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h2
|
||||
class="font-display text-3xl mt-12 text-transition"
|
||||
style="--delay: {8 * multiplier}ms"
|
||||
>
|
||||
🎨 credits
|
||||
</h2>
|
||||
<div class="flex gap-4 mt-8">
|
||||
{#each credits as credit, i}
|
||||
<div class="hover:scale-105 w-56 transition-transform">
|
||||
<div
|
||||
class="border-2 credit-transition border-solid border-foreground-muted-alt rounded-2xl overflow-hidden"
|
||||
style="--delay: {i * 50 + multiplier * 9}ms;"
|
||||
>
|
||||
<a class="w-48" href={credit.url} target="_blank">
|
||||
<img src={credit.avatar} alt="{credit.name}'s avatar" />
|
||||
<div class="text-center py-4 px-2">
|
||||
<p class="font-display text-xl">{credit.name}</p>
|
||||
<p class="text-sm text-foreground-muted mt-2">
|
||||
{credit.description}
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<p
|
||||
class="text-foreground-muted text-base mt-10 text-transition"
|
||||
style="--delay: {10 * multiplier}ms"
|
||||
>
|
||||
(obviously inspired by <a
|
||||
href="https://cobalt.tools"
|
||||
class="hover:underline">cobalt.tools</a
|
||||
>)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes credit-transition {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(60px);
|
||||
filter: blur(18px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes text-transition {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(60px);
|
||||
filter: blur(18px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
}
|
||||
}
|
||||
|
||||
.credit-transition {
|
||||
animation: credit-transition 750ms var(--transition) var(--delay)
|
||||
forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.text-transition {
|
||||
animation: text-transition 750ms var(--transition) var(--delay) forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
|
@ -1,568 +0,0 @@
|
|||
<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 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, 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());
|
||||
|
||||
let finisheds = $state(
|
||||
Array.from({ length: files.files.length }, () => 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.toLowerCase()) &&
|
||||
c.supportedFormats.includes(file.to.toLowerCase()),
|
||||
);
|
||||
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),
|
||||
);
|
||||
|
||||
let disabled = $derived(files.files.some((f) => !f.result));
|
||||
|
||||
onMount(() => {
|
||||
finisheds.forEach((_, i) => {
|
||||
const duration = 575 + i * 50 - 32;
|
||||
setTimeout(() => {
|
||||
finisheds[i] = true;
|
||||
}, duration);
|
||||
});
|
||||
});
|
||||
|
||||
const convertAll = async () => {
|
||||
const perf = performance.now();
|
||||
files.files.forEach((f) => (f.result = null));
|
||||
const promises: Promise<void>[] = [];
|
||||
for (let i = 0; i < files.files.length; i++) {
|
||||
promises.push(
|
||||
(async (i) => {
|
||||
window.plausible("Convert", {
|
||||
props: {
|
||||
"Convert from": files.files[i].from.toLowerCase(),
|
||||
"Convert to": files.files[i].to.toLowerCase(),
|
||||
Conversion: `${files.files[i].from.toLowerCase()} to ${files.files[i].to.toLowerCase()}`,
|
||||
},
|
||||
});
|
||||
await convert(files.files[i], i);
|
||||
})(i),
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
const ms = performance.now() - perf;
|
||||
const seconds = (ms / 1000).toFixed(2);
|
||||
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++) {
|
||||
const file = files.files[i];
|
||||
const result = file.result;
|
||||
if (!result) {
|
||||
console.error("No result found");
|
||||
continue;
|
||||
}
|
||||
dlFiles.push({
|
||||
name: file.file.name.replace(/\.[^/.]+$/, "") + file.to,
|
||||
lastModified: Date.now(),
|
||||
input: await result.file.arrayBuffer(),
|
||||
});
|
||||
}
|
||||
if (files.files.length === 0) return;
|
||||
if (files.files.length === 1) {
|
||||
// download the image only
|
||||
const blob = URL.createObjectURL(
|
||||
new Blob([dlFiles[0].input], {
|
||||
type: files.files[0].to.slice(1),
|
||||
}),
|
||||
);
|
||||
const a = document.createElement("a");
|
||||
a.href = blob;
|
||||
a.download = `VERT-Converted_${new Date().toISOString()}${
|
||||
files.files[0].to
|
||||
}`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(blob);
|
||||
a.remove();
|
||||
return;
|
||||
}
|
||||
const { downloadZip } = await import("client-zip");
|
||||
const blob = await downloadZip(dlFiles, "converted.zip").blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement("a");
|
||||
a.href = url;
|
||||
a.download = `VERT-Converted_${new Date().toISOString()}.zip`;
|
||||
a.click();
|
||||
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,
|
||||
};
|
||||
};
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Your Conversions</title>
|
||||
<meta name="title" content="Your Conversions — VERT.sh" />
|
||||
<meta property="og:title" content="Your Conversions — VERT.sh" />
|
||||
<meta property="twitter:title" content="Your Conversions — VERT.sh" />
|
||||
</svelte:head>
|
||||
|
||||
<div class="grid grid-cols-1 grid-rows-1 w-full">
|
||||
{#if files.files.length === 0}
|
||||
<p class="text-foreground-muted col-start-1 row-start-1 text-center">
|
||||
No files uploaded. Head to the Upload tab to begin!
|
||||
</p>
|
||||
{:else}
|
||||
<div
|
||||
class="flex flex-col gap-4 w-full col-start-1 row-start-1"
|
||||
out:blur={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
blurMultiplier: 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
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 w-full gap-4 mt-2">
|
||||
<div class="flex flex-col gap-3 w-fit">
|
||||
<h3>Set all target formats</h3>
|
||||
<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;
|
||||
file.to = o;
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="italic w-fit text-foreground-muted-alt 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 class="grid gap-3 sm:grid-cols-3 mt-4">
|
||||
<button
|
||||
onclick={convertAll}
|
||||
class={clsx("btn flex-grow", {
|
||||
"btn-highlight":
|
||||
disabled && !processings.some((p) => p),
|
||||
})}
|
||||
disabled={!allConvertersReady ||
|
||||
processings.some((p) => p)}
|
||||
>
|
||||
{#if allConvertersReady}
|
||||
Convert {files.files.length > 1 ? "All" : ""}
|
||||
{:else}
|
||||
Loading...
|
||||
{/if}
|
||||
</button>
|
||||
<button
|
||||
onclick={downloadAll}
|
||||
class={clsx("btn flex-grow", {
|
||||
"btn-highlight": !disabled,
|
||||
})}
|
||||
{disabled}
|
||||
>Download {files.files.length > 1 ? "All" : ""}</button
|
||||
>
|
||||
<button
|
||||
onclick={deleteAll}
|
||||
disabled={processings.some((p) => p)}
|
||||
class="btn flex-grow"
|
||||
>
|
||||
Delete All
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full gap-4 grid md:grid-cols-2">
|
||||
{#each reversedFiles as file, i (file.id)}
|
||||
{@const converter = (() => {
|
||||
return converters.find((c) =>
|
||||
c.supportedFormats.includes(
|
||||
file.from.toLowerCase(),
|
||||
),
|
||||
);
|
||||
})()}
|
||||
<div
|
||||
class="relative"
|
||||
animate:flip={{ duration, easing: quintOut }}
|
||||
out:blur={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
blurMultiplier: 16,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class={clsx(
|
||||
"flex relative flex-shrink-0 items-center w-full rounded-xl h-72",
|
||||
{
|
||||
"initial-fade": !finisheds[i],
|
||||
},
|
||||
)}
|
||||
style="--delay: {i * 50}ms; z-index: {files.files
|
||||
.length - i}; border: solid 2px {file.result
|
||||
? 'var(--accent-bg)'
|
||||
: 'var(--fg-muted-alt)'}; transition: border 1000ms ease; transition: filter {duration}ms var(--transition), transform {duration}ms var(--transition);"
|
||||
>
|
||||
<div
|
||||
class="flex h-full flex-col items-center w-full z-50 relative"
|
||||
>
|
||||
<div class="w-full flex-shrink-0">
|
||||
<div
|
||||
class={clsx(
|
||||
"py-3 dynadark:[--transparency:50%] [--transparency:25%] px-4 w-full flex transition-colors duration-300 flex-shrink text-left border-b-2 border-solid border-foreground-muted-alt rounded-tl-[9.5px] rounded-tr-[10px] overflow-hidden",
|
||||
{
|
||||
"text-accent-foreground":
|
||||
file.result,
|
||||
"text-foreground": !file.result,
|
||||
},
|
||||
)}
|
||||
style="background-color: color-mix(in srgb, var(--{file.result
|
||||
? 'accent-bg'
|
||||
: 'bg'}), transparent var(--transparency)); backdrop-filter: blur({data.isFirefox
|
||||
? 0
|
||||
: 18}px);"
|
||||
>
|
||||
<div
|
||||
class="w-full grid grid-cols-1 grid-rows-1"
|
||||
>
|
||||
{#if processings[files.files.length - i - 1]}
|
||||
<div
|
||||
class="w-full row-start-1 col-start-1 h-full flex items-center pr-4"
|
||||
transition:blur={{
|
||||
blurMultiplier: 6,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
scale: {
|
||||
start: 0.9,
|
||||
end: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ProgressBar
|
||||
min={0}
|
||||
max={100}
|
||||
progress={file.converter
|
||||
?.reportsProgress
|
||||
? file.result
|
||||
? 100
|
||||
: file.progress
|
||||
: null}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<h3
|
||||
class="row-start-1 col-start-1 whitespace-nowrap overflow-hidden text-ellipsis font-medium"
|
||||
transition:blur={{
|
||||
blurMultiplier: 6,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
scale: {
|
||||
start: 0.9,
|
||||
end: 1,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{file.file.name}
|
||||
</h3>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
onclick={() => {
|
||||
// delete the file from the list
|
||||
files.files =
|
||||
files.files.filter(
|
||||
(f) => f !== file,
|
||||
);
|
||||
if (files.files.length === 0)
|
||||
goto("/");
|
||||
}}
|
||||
class="ml-2 mr-1 flex-shrink-0"
|
||||
>
|
||||
<XIcon size="24" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="flex gap-3 justify-normal flex-grow w-full h-full"
|
||||
>
|
||||
<div
|
||||
class="flex flex-col items-end gap-3 w-full"
|
||||
>
|
||||
<div
|
||||
class="flex items-end gap-3 w-full h-full px-5"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center gap-3 w-full pb-4"
|
||||
>
|
||||
{#if converter && converter.supportedFormats.includes(file.from.toLowerCase())}
|
||||
<span>from</span>
|
||||
<span
|
||||
class="py-2 px-3 font-display bg-foreground text-background rounded-xl"
|
||||
>{file.from}</span
|
||||
>
|
||||
<span>to</span>
|
||||
<div class="inline-flex">
|
||||
<Dropdown
|
||||
options={converter.supportedFormats}
|
||||
bind:selected={files
|
||||
.files[
|
||||
files.files
|
||||
.length -
|
||||
i -
|
||||
1
|
||||
].to}
|
||||
onselect={() => {
|
||||
file.result =
|
||||
null;
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<span
|
||||
class="py-2 px-3 font-display bg-foreground-failure text-white rounded-xl"
|
||||
>{file.from}</span
|
||||
>
|
||||
|
||||
<span
|
||||
class="text-foreground-failure"
|
||||
>
|
||||
is not supported!
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <div
|
||||
class="hidden lg:flex gap-4 w-full"
|
||||
>
|
||||
<button
|
||||
class="btn flex-grow flex-shrink-0"
|
||||
onclick={() => convert(file)}
|
||||
>
|
||||
Convert
|
||||
</button>
|
||||
<button
|
||||
class="btn flex-grow flex-shrink-0"
|
||||
disabled={!file.result}
|
||||
onclick={file.download}
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
</div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{#if converter && converter.supportedFormats.includes(file.from.toLowerCase())}
|
||||
<!-- god knows why, but setting opacity > 0.98 causes a z-ordering issue in firefox ??? -->
|
||||
<div
|
||||
class="absolute top-[0px] -z-50 left-0 w-full h-full opacity-[0.98] rounded-xl overflow-hidden"
|
||||
>
|
||||
{#if file.blobUrl}
|
||||
<div
|
||||
class="bg-cover bg-center w-full h-full"
|
||||
style="background-image: url({file.blobUrl})"
|
||||
in:blur={{
|
||||
blurMultiplier: 24,
|
||||
scale: {
|
||||
start: 1.1,
|
||||
end: 1,
|
||||
},
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
></div>
|
||||
<div
|
||||
class="absolute left-0 top-0 pt-[50px] h-full w-full"
|
||||
transition:progBlur={{
|
||||
duration,
|
||||
easing: quintOut,
|
||||
}}
|
||||
>
|
||||
<ProgressiveBlur
|
||||
direction="bottom"
|
||||
endIntensity={64}
|
||||
iterations={8}
|
||||
fadeTo="var(--bg-transparent)"
|
||||
/>
|
||||
</div>
|
||||
{:else}
|
||||
<div
|
||||
class="w-full h-full flex items-center justify-center"
|
||||
>
|
||||
<FileAudioIcon
|
||||
size="96"
|
||||
strokeWidth="1.5"
|
||||
color="var(--fg)"
|
||||
opacity="0.9"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes initial-transition {
|
||||
0% {
|
||||
transform: translateY(50px);
|
||||
filter: blur(16px);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
filter: blur(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.initial-fade {
|
||||
animation: initial-transition 600ms var(--delay) var(--transition);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.initial-fade.finished {
|
||||
animation: none;
|
||||
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 {
|
||||
animation: processing 2000ms infinite;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
transition:
|
||||
filter 500ms var(--transition),
|
||||
transform 500ms var(--transition);
|
||||
}
|
||||
</style>
|
Loading…
Reference in New Issue