mirror of https://github.com/VERT-sh/VERT.git
feat: converter page
This commit is contained in:
parent
0370ff3abf
commit
36c6b19483
49
src/app.scss
49
src/app.scss
|
@ -31,16 +31,36 @@
|
|||
}
|
||||
|
||||
@mixin light {
|
||||
--accent-bg: hsl(303, 73%, 81%);
|
||||
--accent-fg: hsl(0, 0, 10%);
|
||||
--bg: hsl(0, 0%, 100%);
|
||||
--bg-transparent: hsla(0, 0%, 100%, 0.6);
|
||||
--fg: hsl(0, 0%, 10%);
|
||||
// general
|
||||
--accent: hsl(303, 73%, 81%);
|
||||
|
||||
// foregrounds
|
||||
--fg: hsl(0, 0%, 0%);
|
||||
--fg-muted: hsl(0, 0%, 50%);
|
||||
--fg-muted-alt: hsl(0, 0%, 75%);
|
||||
--fg-highlight: hsl(303, 61%, 47%);
|
||||
--fg-failure: hsl(0, 67%, 49%);
|
||||
color-scheme: light;
|
||||
--fg-on-accent: hsl(0, 0%, 0%);
|
||||
|
||||
// backgrounds
|
||||
--bg: hsl(0, 0%, 100%);
|
||||
--bg-gradient: linear-gradient(
|
||||
to bottom left,
|
||||
hsla(235, 100%, 50%, 0.3),
|
||||
hsla(235, 100%, 50%, 0) 75%
|
||||
),
|
||||
linear-gradient(
|
||||
to bottom right,
|
||||
hsla(353, 100%, 50%, 0.4),
|
||||
hsla(353, 100%, 50%, 0) 50%
|
||||
),
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
hsla(287, 100%, 50%, 0.2),
|
||||
hsla(287, 100%, 50%, 0)
|
||||
);
|
||||
--bg-panel: hsl(0, 0%, 100%);
|
||||
--bg-panel-alt: hsl(0, 0%, 95%);
|
||||
--bg-panel-accented: color-mix(in srgb, var(--accent) 35%, transparent);
|
||||
--bg-separator: hsl(0, 0%, 88%);
|
||||
--bg-button: var(--bg-panel-accented);
|
||||
}
|
||||
|
||||
@mixin dark {
|
||||
|
@ -69,10 +89,12 @@
|
|||
hsla(353, 100%, 50%, 0.07),
|
||||
hsla(353, 100%, 50%, 0) 50%
|
||||
);
|
||||
color-scheme: dark;
|
||||
--bg-panel: hsl(225, 4%, 18%);
|
||||
--bg-panel-accented: color-mix(in srgb, var(--accent) 12%, transparent);
|
||||
--bg-panel-alt: hsl(220, 5%, 16%);
|
||||
--bg-separator: hsl(214, 4%, 32%);
|
||||
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
@ -98,7 +120,6 @@
|
|||
body {
|
||||
@apply text-foreground font-body font-semibold overflow-x-hidden;
|
||||
width: 100vw;
|
||||
background: var(--bg-gradient);
|
||||
background-color: var(--bg);
|
||||
background-size: 100vw 100vh;
|
||||
}
|
||||
|
@ -109,15 +130,15 @@ body {
|
|||
}
|
||||
|
||||
.btn {
|
||||
@apply font-display flex items-center justify-center overflow-hidden relative cursor-pointer px-4 border-2 border-solid bg-background border-foreground-muted-alt rounded-xl p-2 focus:!outline-none hover:scale-105 duration-200 active:scale-95 disabled:opacity-50 disabled:pointer-events-none;
|
||||
@apply bg-button flex items-center justify-center overflow-hidden relative cursor-pointer px-6 py-3 rounded-full focus:!outline-none hover:scale-105 duration-200 active:scale-95 disabled:opacity-50 disabled:pointer-events-none;
|
||||
transition:
|
||||
opacity 0.2s ease,
|
||||
transform 0.2s ease,
|
||||
background-color 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-highlight {
|
||||
@apply bg-accent-background text-accent-foreground border-accent-background;
|
||||
.btn.highlight {
|
||||
@apply bg-accent text-on-accent;
|
||||
}
|
||||
|
||||
h1,
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
<script lang="ts">
|
||||
import { FolderArchiveIcon, RotateCw } from "lucide-svelte";
|
||||
import Panel from "../visual/Panel.svelte";
|
||||
import { files } from "$lib/store/index.svelte";
|
||||
import Dropdown from "./Dropdown.svelte";
|
||||
</script>
|
||||
|
||||
<Panel class="w-full h-20 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<button
|
||||
onclick={() => files.convertAll()}
|
||||
class="btn highlight flex gap-3"
|
||||
disabled={!files.ready}
|
||||
>
|
||||
<RotateCw size="24" />
|
||||
<p>Convert all</p>
|
||||
</button>
|
||||
<button
|
||||
class="btn flex gap-3"
|
||||
disabled={!files.ready || !files.results}
|
||||
onclick={() => files.downloadAll()}
|
||||
>
|
||||
<FolderArchiveIcon size="24" />
|
||||
<p>Download all as .zip</p>
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{#if files.requiredConverters.length === 1}
|
||||
<p class="whitespace-nowrap">Set all to</p>
|
||||
<Dropdown
|
||||
onselect={(r) => files.files.forEach((f) => (f.to = r))}
|
||||
options={files.files[0]?.converter?.supportedFormats || []}
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
</Panel>
|
|
@ -11,7 +11,11 @@
|
|||
onselect?: (option: string) => void;
|
||||
};
|
||||
|
||||
let { options, selected = $bindable(), onselect }: Props = $props();
|
||||
let {
|
||||
options,
|
||||
selected = $bindable(options[0]),
|
||||
onselect,
|
||||
}: Props = $props();
|
||||
|
||||
let open = $state(false);
|
||||
let hover = $state(false);
|
||||
|
@ -31,10 +35,6 @@
|
|||
toggle();
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
selected = selected || options[0];
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
const click = (e: MouseEvent) => {
|
||||
if (dropdown && !dropdown.contains(e.target as Node)) {
|
||||
|
@ -49,15 +49,13 @@
|
|||
|
||||
<div class="relative w-full min-w-fit" bind:this={dropdown}>
|
||||
<button
|
||||
class="font-display w-full min-w-fit justify-between overflow-hidden relative cursor-pointer px-3 border-2 border-solid flex items-center bg-background border-foreground-muted-alt rounded-xl p-2 focus:!outline-none"
|
||||
class="font-display w-full min-w-fit justify-between overflow-hidden relative cursor-pointer px-3 bg-button flex items-center rounded-full p-2 focus:!outline-none"
|
||||
onclick={toggle}
|
||||
onmouseenter={() => (hover = true)}
|
||||
onmouseleave={() => (hover = false)}
|
||||
>
|
||||
<!-- <p>{selected}</p> -->
|
||||
<div
|
||||
class="grid grid-cols-1 grid-rows-1 w-fit text-left flex-grow-0 pr-12"
|
||||
>
|
||||
<div class="grid grid-cols-1 grid-rows-1 w-fit text-left flex-grow-0">
|
||||
{#key selected}
|
||||
<p
|
||||
in:blur={{
|
||||
|
@ -123,11 +121,11 @@
|
|||
},
|
||||
origin: "top center",
|
||||
}}
|
||||
class="w-full shadow-xl shadow-black/25 absolute overflow-hidden top-full mt-1 left-0 z-50 bg-background border-2 border-solid border-foreground-muted-alt rounded-xl"
|
||||
class="w-full shadow-xl bg-panel-alt shadow-black/25 absolute overflow-hidden top-full mt-1 left-0 z-50 bg-background rounded-xl"
|
||||
>
|
||||
{#each options as option}
|
||||
<button
|
||||
class="w-full p-2 px-4 text-left hover:bg-foreground-muted-alt brightness-125"
|
||||
class="w-full p-2 px-4 text-left hover:bg-panel"
|
||||
onclick={() => select(option)}
|
||||
>
|
||||
{option}
|
||||
|
|
|
@ -6,6 +6,8 @@
|
|||
import { page } from "$app/stores";
|
||||
import { MoonIcon, SunIcon } from "lucide-svelte";
|
||||
import { theme } from "$lib/store/index.svelte";
|
||||
import { blur, duration } from "$lib/animation";
|
||||
import { quintOut } from "svelte/easing";
|
||||
|
||||
type Props = {
|
||||
items: {
|
||||
|
@ -19,6 +21,49 @@
|
|||
let { items }: Props = $props();
|
||||
</script>
|
||||
|
||||
{#snippet link(item: (typeof items)[0])}
|
||||
{@const Icon = item.icon}
|
||||
<a
|
||||
href={item.url}
|
||||
aria-label={item.name}
|
||||
class={clsx(
|
||||
"px-4 h-full rounded-xl flex items-center justify-center gap-3 overflow-hidden",
|
||||
{
|
||||
"bg-panel-accented": item.activeMatch($page.url.pathname),
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Icon />
|
||||
<div class="grid grid-rows-1 grid-cols-1">
|
||||
{#key item.name}
|
||||
<p
|
||||
class="row-start-1 col-start-1"
|
||||
in:blur={{
|
||||
blurMultiplier: 6,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
y: {
|
||||
start: -48,
|
||||
end: 0,
|
||||
},
|
||||
}}
|
||||
out:blur={{
|
||||
blurMultiplier: 6,
|
||||
duration,
|
||||
easing: quintOut,
|
||||
y: {
|
||||
start: 0,
|
||||
end: 48,
|
||||
},
|
||||
}}
|
||||
>
|
||||
{item.name}
|
||||
</p>
|
||||
{/key}
|
||||
</div>
|
||||
</a>
|
||||
{/snippet}
|
||||
|
||||
<Panel class="w-fit h-20 flex items-center gap-3">
|
||||
<div
|
||||
class="w-32 h-full bg-accent rounded-xl flex items-center justify-center"
|
||||
|
@ -28,20 +73,7 @@
|
|||
</div>
|
||||
</div>
|
||||
{#each items as item (item.url)}
|
||||
{@const Icon = item.icon}
|
||||
<a
|
||||
href={item.url}
|
||||
aria-label={item.name}
|
||||
class={clsx(
|
||||
"w-32 h-full rounded-xl flex items-center justify-center gap-3",
|
||||
{
|
||||
"bg-panel-accented": item.activeMatch($page.url.pathname),
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Icon />
|
||||
<p>{item.name}</p>
|
||||
</a>
|
||||
{@render link(item)}
|
||||
{/each}
|
||||
<div class="w-0.5 bg-separator h-full"></div>
|
||||
<button
|
||||
|
|
|
@ -1,16 +1,77 @@
|
|||
<script lang="ts">
|
||||
import { UploadIcon } from "lucide-svelte";
|
||||
import Panel from "../visual/Panel.svelte";
|
||||
import clsx from "clsx";
|
||||
import { onMount } from "svelte";
|
||||
import { files } from "$lib/store/index.svelte";
|
||||
import { converters } from "$lib/converters";
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
type Props = {
|
||||
class?: string;
|
||||
};
|
||||
|
||||
const { class: classList }: Props = $props();
|
||||
|
||||
let dropping = $state(false);
|
||||
let uploaderButton = $state<HTMLButtonElement>();
|
||||
|
||||
const dropFiles = (e: DragEvent) => {
|
||||
e.preventDefault();
|
||||
dropping = false;
|
||||
const oldLength = files.files.length;
|
||||
files.add(e.dataTransfer?.files);
|
||||
if (oldLength !== files.files.length) goto("/convert");
|
||||
};
|
||||
|
||||
const uploadFiles = () => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.multiple = true;
|
||||
input.accept = converters
|
||||
.map((c) => c.supportedFormats.join(","))
|
||||
.join(",");
|
||||
input.onchange = (e) => {
|
||||
const oldLength = files.files.length;
|
||||
files.add(input.files);
|
||||
if (oldLength !== files.files.length) goto("/convert");
|
||||
};
|
||||
input.click();
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
const handler = (e: Event) => {
|
||||
e.preventDefault();
|
||||
return false;
|
||||
};
|
||||
|
||||
uploaderButton?.addEventListener("dragover", handler);
|
||||
uploaderButton?.addEventListener("dragenter", handler);
|
||||
uploaderButton?.addEventListener("dragleave", handler);
|
||||
uploaderButton?.addEventListener("drop", handler);
|
||||
|
||||
return () => {
|
||||
uploaderButton?.removeEventListener("dragover", handler);
|
||||
uploaderButton?.removeEventListener("dragenter", handler);
|
||||
uploaderButton?.removeEventListener("dragleave", handler);
|
||||
uploaderButton?.removeEventListener("drop", handler);
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<button class={classList}>
|
||||
<Panel class="flex justify-center items-center w-full h-full flex-col">
|
||||
<button
|
||||
ondragenter={() => (dropping = true)}
|
||||
ondragleave={() => (dropping = false)}
|
||||
ondrop={dropFiles}
|
||||
onclick={uploadFiles}
|
||||
bind:this={uploaderButton}
|
||||
class={clsx(`hover:scale-105 active:scale-100 duration-200 ${classList}`, {
|
||||
"scale-105": dropping,
|
||||
})}
|
||||
>
|
||||
<Panel
|
||||
class="flex justify-center items-center w-full h-full flex-col pointer-events-none"
|
||||
>
|
||||
<div
|
||||
class="w-16 h-16 bg-accent rounded-full flex items-center justify-center p-4"
|
||||
>
|
||||
|
|
|
@ -11,7 +11,10 @@
|
|||
const links = $derived(Object.entries(items));
|
||||
</script>
|
||||
|
||||
<footer class={classList}>
|
||||
<footer
|
||||
class={classList}
|
||||
style="background: linear-gradient(to bottom, transparent, var(--bg) 100%)"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full flex items-center justify-center text-muted gap-3"
|
||||
>
|
||||
|
|
|
@ -12,11 +12,9 @@
|
|||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="w-full h-1 dynadark:bg-foreground-muted-alt bg-foreground-muted rounded-full overflow-hidden relative"
|
||||
>
|
||||
<div class="w-full h-1 bg-panel-alt rounded-full overflow-hidden relative">
|
||||
<div
|
||||
class="h-full bg-accent-background dynadark:bg-accent-foreground absolute left-0 top-0"
|
||||
class="h-full bg-accent absolute left-0 top-0"
|
||||
class:percentless-animation={progress === null}
|
||||
style={percent
|
||||
? `width: ${percent}%; transition: 500ms linear width;`
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
<svg
|
||||
width="1389"
|
||||
height="1080"
|
||||
viewBox="0 0 1389 1080"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M418.719 1080L0.480804 0H2.62554L420.863 1080H418.719Z"
|
||||
fill="url(#paint0_linear_6_220)"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<path
|
||||
d="M829.044 1080L412.359 0H410.215L826.9 1080H829.044Z"
|
||||
fill="url(#paint1_linear_6_220)"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<path
|
||||
d="M788.673 555.925L987.856 0H989.981L790.985 555.402L1064.61 827.169L1386.13 0H1388.27L1065.37 830.741L788.673 555.925Z"
|
||||
fill="url(#paint2_linear_6_220)"
|
||||
fill-opacity="0.1"
|
||||
/>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id="paint0_linear_6_220"
|
||||
x1="694.377"
|
||||
y1="0"
|
||||
x2="694.377"
|
||||
y2="1080"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="0.75" stop-color="white" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_6_220"
|
||||
x1="694.377"
|
||||
y1="0"
|
||||
x2="694.377"
|
||||
y2="1080"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="0.75" stop-color="white" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_6_220"
|
||||
x1="694.377"
|
||||
y1="0"
|
||||
x2="694.377"
|
||||
y2="1080"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" />
|
||||
<stop offset="0.75" stop-color="white" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -1,10 +1,166 @@
|
|||
import { browser } from "$app/environment";
|
||||
import { converters } from "$lib/converters";
|
||||
import { log } from "$lib/logger";
|
||||
import { VertFile } from "$lib/types";
|
||||
import JSCookie from "js-cookie";
|
||||
import jsmediatags from "jsmediatags";
|
||||
import type { TagType } from "jsmediatags/types";
|
||||
|
||||
class Files {
|
||||
public files = $state<VertFile[]>([]);
|
||||
|
||||
public requiredConverters = $derived(
|
||||
Array.from(new Set(files.files.map((f) => f.converter))),
|
||||
);
|
||||
|
||||
public ready = $derived(
|
||||
this.files.length === 0
|
||||
? false
|
||||
: this.requiredConverters.every((f) => f?.ready),
|
||||
);
|
||||
public results = $derived(
|
||||
this.files.length === 0 ? false : this.files.every((f) => f.result),
|
||||
);
|
||||
|
||||
private _addThumbnail = async (file: VertFile) => {
|
||||
try {
|
||||
if (
|
||||
converters
|
||||
.find((c) => c.name === "ffmpeg")
|
||||
?.supportedFormats?.includes(file.from.toLowerCase())
|
||||
) {
|
||||
// try to get the thumbnail from the audio via jsmmediatags
|
||||
const tags = await new Promise<TagType>((resolve, reject) => {
|
||||
jsmediatags.read(file.file, {
|
||||
onSuccess: (tag) => resolve(tag),
|
||||
onError: (error) => reject(error),
|
||||
});
|
||||
});
|
||||
if (tags.tags.picture) {
|
||||
const blob = new Blob(
|
||||
[new Uint8Array(tags.tags.picture.data)],
|
||||
{
|
||||
type: tags.tags.picture.format,
|
||||
},
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
file.blobUrl = url;
|
||||
}
|
||||
} else {
|
||||
const img = new Image();
|
||||
img.src = URL.createObjectURL(file.file);
|
||||
await new Promise((resolve) => {
|
||||
img.onload = resolve;
|
||||
});
|
||||
const canvas = document.createElement("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
const maxSize = 180;
|
||||
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);
|
||||
const url = canvas.toDataURL();
|
||||
file.blobUrl = url;
|
||||
canvas.remove();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
private _add(file: VertFile | File) {
|
||||
if (file instanceof VertFile) {
|
||||
this.files.push(file);
|
||||
this._addThumbnail(file);
|
||||
} else {
|
||||
const format = "." + file.name.split(".").pop()?.toLowerCase();
|
||||
if (!format) {
|
||||
log(["files"], `no extension found for ${file.name}`);
|
||||
return;
|
||||
}
|
||||
const converter = converters.find((c) =>
|
||||
c.supportedFormats.includes(
|
||||
format || ".somenonexistentextension",
|
||||
),
|
||||
);
|
||||
if (!converter) {
|
||||
log(["files"], `no converter found for ${file.name}`);
|
||||
return;
|
||||
}
|
||||
const to = converter.supportedFormats.find((f) => f !== format);
|
||||
if (!to) {
|
||||
log(["files"], `no output format found for ${file.name}`);
|
||||
return;
|
||||
}
|
||||
const vf = new VertFile(file, to, converter);
|
||||
this.files.push(vf);
|
||||
this._addThumbnail(vf);
|
||||
}
|
||||
}
|
||||
|
||||
public add(file: VertFile | null | undefined): void;
|
||||
public add(file: File | null | undefined): void;
|
||||
public add(file: File[] | null | undefined): void;
|
||||
public add(file: VertFile[] | null | undefined): void;
|
||||
public add(file: FileList | null | undefined): void;
|
||||
public add(
|
||||
file:
|
||||
| VertFile
|
||||
| File
|
||||
| VertFile[]
|
||||
| File[]
|
||||
| FileList
|
||||
| null
|
||||
| undefined,
|
||||
) {
|
||||
if (!file) return;
|
||||
if (Array.isArray(file) || file instanceof FileList) {
|
||||
for (const f of file) {
|
||||
this._add(f);
|
||||
}
|
||||
} else {
|
||||
this._add(file);
|
||||
}
|
||||
}
|
||||
|
||||
public async convertAll() {
|
||||
await Promise.all(this.files.map((f) => f.convert()));
|
||||
}
|
||||
|
||||
public async downloadAll() {
|
||||
if (files.files.length === 0) return;
|
||||
if (files.files.length === 1) {
|
||||
return await files.files[0].download();
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
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(),
|
||||
});
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
class Theme {
|
||||
|
|
|
@ -18,6 +18,8 @@ export class VertFile {
|
|||
|
||||
public blobUrl = $state<string>();
|
||||
|
||||
public processing = $state(false);
|
||||
|
||||
public converter: Converter | null = null;
|
||||
|
||||
constructor(
|
||||
|
@ -37,7 +39,9 @@ export class VertFile {
|
|||
if (!this.converter) throw new Error("No converter found");
|
||||
this.result = null;
|
||||
this.progress = 0;
|
||||
this.processing = true;
|
||||
const res = await this.converter.convert(this, this.to);
|
||||
this.processing = false;
|
||||
this.result = res;
|
||||
return res;
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
import {
|
||||
InfoIcon,
|
||||
MoonIcon,
|
||||
RefreshCwIcon,
|
||||
RotateCw,
|
||||
SettingsIcon,
|
||||
SunIcon,
|
||||
UploadIcon,
|
||||
|
@ -27,6 +27,8 @@
|
|||
import Panel from "$lib/components/visual/Panel.svelte";
|
||||
import Navbar from "$lib/components/functional/Navbar.svelte";
|
||||
import Footer from "$lib/components/visual/Footer.svelte";
|
||||
import { page } from "$app/stores";
|
||||
import ConversionPanel from "$lib/components/functional/ConversionPanel.svelte";
|
||||
let { children, data } = $props();
|
||||
|
||||
let shouldGoBack = writable(false);
|
||||
|
@ -54,7 +56,7 @@
|
|||
: `Convert`,
|
||||
url: "/convert",
|
||||
activeMatch: (pathname) => pathname === "/convert",
|
||||
icon: RefreshCwIcon,
|
||||
icon: RotateCw,
|
||||
},
|
||||
{
|
||||
name: "Settings",
|
||||
|
@ -103,15 +105,27 @@
|
|||
></script>{/if}
|
||||
</svelte:head>
|
||||
|
||||
<div class="fixed top-8 left-0 w-full flex justify-center">
|
||||
<div class="absolute top-8 left-0 w-full flex justify-center">
|
||||
<div class="flex flex-col gap-4">
|
||||
<Navbar {items} />
|
||||
{#if items
|
||||
.find((i) => i.url === "/convert")
|
||||
?.activeMatch($page.url.pathname)}
|
||||
<ConversionPanel />
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-screen h-screen">
|
||||
<div
|
||||
class="fixed top-0 left-0 w-screen h-screen -z-50 pointer-events-none"
|
||||
style="background: var(--bg-gradient);"
|
||||
></div>
|
||||
|
||||
<div class="min-h-screen">
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
<div class="-mt-14 w-full h-14">
|
||||
<div class="-mt-14 -z-50 w-full h-14 border-t border-separator">
|
||||
<Footer
|
||||
class="w-full h-full"
|
||||
items={{
|
||||
|
|
|
@ -11,12 +11,12 @@
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="fixed -z-50 top-0 left-0 w-full h-full flex items-center justify-center overflow-hidden"
|
||||
class="absolute -z-50 top-0 left-0 w-full h-full flex items-center justify-center overflow-hidden"
|
||||
>
|
||||
<VertVBig class="fill-[--fg] opacity-50" />
|
||||
</div>
|
||||
|
||||
<div class="w-full h-full flex items-center justify-center">
|
||||
<div class="w-screen h-screen flex items-center justify-center">
|
||||
<div class="max-w-5xl w-full">
|
||||
<div class="flex items-center h-[266px] gap-24">
|
||||
<div class="flex-grow w-full">
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
<script lang="ts">
|
||||
import Dropdown from "$lib/components/functional/Dropdown.svelte";
|
||||
import Panel from "$lib/components/visual/Panel.svelte";
|
||||
import ProgressBar from "$lib/components/visual/ProgressBar.svelte";
|
||||
import { files } from "$lib/store/index.svelte";
|
||||
import { VertFile } from "$lib/types";
|
||||
import {
|
||||
Disc2Icon,
|
||||
DownloadIcon,
|
||||
ImageIcon,
|
||||
RotateCwIcon,
|
||||
XIcon,
|
||||
} from "lucide-svelte";
|
||||
</script>
|
||||
|
||||
{#snippet fileItem(file: VertFile, index: number)}
|
||||
{@const isAudio = file.converter?.name === "ffmpeg" || false}
|
||||
<Panel class="p-5 flex flex-col min-w-0 gap-4 relative">
|
||||
<div class="flex-shrink-0 h-8 w-full flex items-center gap-2">
|
||||
{#if isAudio}
|
||||
<Disc2Icon size="24" class="flex-shrink-0" />
|
||||
{:else}
|
||||
<ImageIcon size="24" class="flex-shrink-0" />
|
||||
{/if}
|
||||
<div class="flex-grow">
|
||||
{#if file.processing}
|
||||
<ProgressBar
|
||||
min={0}
|
||||
max={100}
|
||||
progress={file.converter?.reportsProgress
|
||||
? file.progress
|
||||
: null}
|
||||
/>
|
||||
{:else}
|
||||
<h2
|
||||
class="text-xl font-body overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{file.name}
|
||||
</h2>
|
||||
{/if}
|
||||
</div>
|
||||
<button
|
||||
class="flex-shrink-0 w-8 rounded-full hover:bg-panel-alt h-full flex items-center justify-center"
|
||||
onclick={() =>
|
||||
(files.files = files.files.filter((_, i) => i !== index))}
|
||||
>
|
||||
<XIcon size="24" class="text-muted" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex gap-4 w-full h-full overflow-hidden relative">
|
||||
<div class="w-1/2 h-full overflow-hidden rounded-xl">
|
||||
<img
|
||||
class="object-cover w-full h-full"
|
||||
src={file.blobUrl}
|
||||
alt={file.name}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute top-16 right-0 mr-4 pl-2 h-[calc(100%-83px)] w-[calc(50%-32px)] pr-4 pb-5"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full flex flex-col gap-2 items-center justify-center"
|
||||
>
|
||||
<Dropdown
|
||||
options={file.converter?.supportedFormats || []}
|
||||
bind:selected={file.to}
|
||||
/>
|
||||
<div class="w-full flex items-center justify-around">
|
||||
<button
|
||||
class="btn p-0 w-14 h-14"
|
||||
disabled={!files.ready}
|
||||
onclick={file.convert}
|
||||
>
|
||||
<RotateCwIcon size="24" />
|
||||
</button>
|
||||
<button
|
||||
class="btn p-0 w-14 h-14"
|
||||
onclick={file.download}
|
||||
disabled={!file.result}
|
||||
>
|
||||
<DownloadIcon size="24" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Panel>
|
||||
{/snippet}
|
||||
|
||||
<div class="w-full h-full flex justify-center pt-60 pb-20">
|
||||
<div
|
||||
class="w-[794px] grid"
|
||||
style="grid-template-columns: repeat(2, 1fr); grid-auto-rows: 240px; gap: 16px"
|
||||
>
|
||||
{#each files.files as file, i (file.id)}
|
||||
{@render fileItem(file, i)}
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
|
@ -9,6 +9,11 @@ export default {
|
|||
panel: "var(--bg-panel)",
|
||||
"panel-accented": "var(--bg-panel-accented)",
|
||||
separator: "var(--bg-separator)",
|
||||
button: "var(--bg-button)",
|
||||
"panel-alt": "var(--bg-panel-alt)",
|
||||
},
|
||||
borderColor: {
|
||||
separator: "var(--bg-separator)",
|
||||
},
|
||||
textColor: {
|
||||
foreground: "var(--fg)",
|
||||
|
|
Loading…
Reference in New Issue