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">
|
<script lang="ts">
|
||||||
import { goto } from "$app/navigation";
|
// this comment was written on 15/11/2024 at 16:01 GMT.
|
||||||
import Uploader from "$lib/components/functional/Uploader.svelte";
|
// i bet to myself that i could complete this whole redesign implementation
|
||||||
import { converters } from "$lib/converters";
|
// by the time realmy got started on it. i guess we'll see how that goes
|
||||||
import { log } from "$lib/logger";
|
//
|
||||||
import { files } from "$lib/store/index.svelte";
|
// ship fast n break things !!
|
||||||
import { VertFile } from "$lib/types/file.svelte";
|
// -- nullptr
|
||||||
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");
|
|
||||||
};
|
|
||||||
</script>
|
</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