vert/src/routes/+page.svelte

342 lines
8.9 KiB
Svelte

<script lang="ts">
import Uploader from "$lib/components/functional/Uploader.svelte";
import Tooltip from "$lib/components/visual/Tooltip.svelte";
import { converters } from "$lib/converters";
import { vertdLoaded } from "$lib/store/index.svelte";
import clsx from "clsx";
import { AudioLines, BookText, Check, Film, Image } from "lucide-svelte";
import { m } from "$lib/paraglide/messages";
import { OverlayScrollbarsComponent } from "overlayscrollbars-svelte";
import { browser } from "$app/environment";
import "overlayscrollbars/overlayscrollbars.css";
import { onMount } from "svelte";
import type { WorkerStatus } from "$lib/converters/converter.svelte";
import { sanitize } from "$lib/store/index.svelte";
import { DISABLE_ALL_EXTERNAL_REQUESTS } from "$lib/util/consts";
const getSupportedFormats = (name: string) =>
converters
.find((c) => c.name === name)
?.supportedFormats.map(
(f) =>
`${f.name}${f.fromSupported && f.toSupported ? "" : "*"}`,
)
.join(", ") || "none";
const worker: {
[key: string]: {
formats: string;
icon: typeof Image;
title: string;
status: WorkerStatus;
};
} = $derived.by(() => {
const output: {
[key: string]: {
formats: string;
icon: typeof Image;
title: string;
status: WorkerStatus;
};
} = {
Images: {
formats: getSupportedFormats("imagemagick"),
icon: Image,
title: m["upload.cards.images"](),
status:
converters.find((c) => c.name === "imagemagick")?.status ||
"not-ready",
},
Audio: {
formats: getSupportedFormats("ffmpeg"),
icon: AudioLines,
title: m["upload.cards.audio"](),
status:
converters.find((c) => c.name === "ffmpeg")?.status ||
"not-ready",
},
Documents: {
formats: getSupportedFormats("pandoc"),
icon: BookText,
title: m["upload.cards.documents"](),
status:
converters.find((c) => c.name === "pandoc")?.status ||
"not-ready",
},
};
if (!DISABLE_ALL_EXTERNAL_REQUESTS) {
const formats = Array.from(
new Set([
...getSupportedFormats("vertd").split(", "),
...getSupportedFormats("mediabunny").split(", "),
]),
)
.filter((f) => f !== "none")
.join(", ");
const mediabunnyStatus = converters.find(
(c) => c.name === "mediabunny",
)?.status;
const vertdReady = $vertdLoaded === true;
const mediabunnyReady = mediabunnyStatus === "ready";
const videoStatus =
vertdReady && mediabunnyReady
? "ready"
: vertdReady || mediabunnyReady
? "partially-ready"
: "not-ready";
output.Video = {
formats,
icon: Film,
title: m["upload.cards.video"](),
status: videoStatus as WorkerStatus,
};
}
return output;
});
const getTooltip = (format: string) => {
const converter = converters.find((c) =>
c.supportedFormats.some((sf) => sf.name === format),
);
const formatInfo = converter?.supportedFormats.find(
(sf) => sf.name === format,
);
if (formatInfo) {
const direction = formatInfo.fromSupported
? m["upload.tooltip.direction_input"]()
: m["upload.tooltip.direction_output"]();
return m["upload.tooltip.partial_support"]({ direction });
}
return "";
};
const getStatusText = (status: WorkerStatus) => {
switch (status) {
case "downloading":
return m["upload.cards.status.downloading"]();
case "partially-ready":
return m["upload.cards.status.partially_ready"]();
case "ready":
return m["upload.cards.status.ready"]();
default:
// "not-ready", "error" and other statuses (somehow)
return m["upload.cards.status.not_ready"]();
}
};
let scrollContainers: HTMLElement[] = $state([]);
// svelte-ignore state_referenced_locally
let showBlur = $state(Array(Object.keys(worker).length).fill(false));
onMount(() => {
const handleResize = () => {
for (let i = 0; i < scrollContainers.length; i++) {
// show bottom blur if scrollable
const container = scrollContainers[i];
if (!container) return;
showBlur[i] = container.scrollHeight > container.clientHeight;
}
};
handleResize();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
});
</script>
<div class="max-w-6xl w-full mx-auto px-6 md:px-8">
<div class="flex items-center justify-center pb-10 md:py-16">
<div
class="flex items-center h-auto gap-12 md:gap-24 md:flex-row flex-col"
>
<div class="flex-grow w-full text-center md:text-left">
<h1
class="text-4xl px-12 md:p-0 md:text-6xl flex-wrap tracking-tight leading-tight md:leading-[72px] mb-4 md:mb-6"
>
{m["upload.title"]()}
</h1>
<p
class="font-normal px-5 md:p-0 text-lg md:text-xl text-black text-muted dynadark:text-muted"
>
{m["upload.subtitle"]()}
</p>
</div>
<div class="flex-grow w-full h-72">
<Uploader class="w-full h-full" />
</div>
</div>
</div>
<hr />
<div class="mt-10 md:mt-16">
<h2 class="text-center text-4xl">{m["upload.cards.title"]()}</h2>
<div class="flex gap-4 mt-8 md:flex-row flex-col">
{#if browser}
{#each Object.entries(worker) as [key, s], i}
{@const Icon = s.icon}
<div class="file-category-card w-full flex flex-col gap-4">
<div class="file-category-card-inner">
<div
class={clsx("icon-container", {
"bg-accent-blue": key === "Images",
"bg-accent-purple": key === "Audio",
"bg-accent-green": key === "Documents",
"bg-accent-red": key === "Video",
})}
>
<Icon size="20" />
</div>
<span>{s.title}</span>
</div>
<div
class="file-category-card-content flex-grow relative"
>
<OverlayScrollbarsComponent
options={{
scrollbars: {
autoHide: "move",
autoHideDelay: 1500,
},
}}
defer
>
<div
class="flex flex-col gap-4 h-[12.25rem] relative"
bind:this={scrollContainers[i]}
>
{#if key === "Video"}
<p
class="flex tems-center justify-center gap-2"
>
<Check size="20" />
<Tooltip
text={m[
"upload.tooltip.video_server_processing"
]()}
>
<span>
<a
href="https://github.com/VERT-sh/VERT/blob/main/docs/VIDEO_CONVERSION.md"
target="_blank"
rel="noopener noreferrer"
>
{m[
"upload.cards.video_server_processing"
]()}
</a>
<span
class="text-red-500 -ml-0.5"
>*</span
>
</span>
</Tooltip>
</p>
{:else}
<p
class="flex tems-center justify-center gap-2"
>
<Check size="20" />
{m[
"upload.cards.local_supported"
]()}
</p>
{/if}
<p>
{@html sanitize(
m["upload.cards.status.text"]({
status: getStatusText(s.status),
}),
)}
</p>
<div
class="flex flex-col items-center relative"
>
<b
>{m[
"upload.cards.supported_formats"
]()}&nbsp;</b
>
<p
class="flex flex-wrap justify-center leading-tight px-2"
>
{#each s.formats.split(", ") as format, index}
{@const isPartial =
format.endsWith("*")}
{@const formatName = isPartial
? format.slice(0, -1)
: format}
<span
class="text-sm font-normal flex items-center relative"
>
{#if isPartial}
<Tooltip
text={getTooltip(
formatName,
)}
>
{formatName}<span
class="text-red-500"
>*</span
>
</Tooltip>
{:else}
{formatName}
{/if}
{#if index < s.formats.split(", ").length - 1}
<span>,&nbsp;</span>
{/if}
</span>
{/each}
</p>
</div>
</div>
</OverlayScrollbarsComponent>
<!-- bottom blur if scrollable -->
{#if showBlur[i]}
<div
class="absolute left-0 bottom-0 w-full h-10 pointer-events-none"
style={`background: linear-gradient(to top, var(--bg-panel), transparent 65%);`}
></div>
{/if}
</div>
</div>
{/each}
{/if}
</div>
</div>
</div>
<style lang="postcss">
.file-category-card {
@apply bg-panel rounded-2xl p-5 shadow-panel relative;
}
.file-category-card p {
@apply font-normal text-center text-sm;
}
.file-category-card-inner {
@apply flex items-center justify-center gap-3 text-xl;
}
.file-category-card-content {
@apply flex flex-col text-center justify-between;
}
.icon-container {
@apply p-2 rounded-full text-on-accent;
}
</style>