vert/src/routes/convert/+page.svelte

338 lines
9.2 KiB
Svelte

<script lang="ts">
import ConversionPanel from "$lib/components/functional/ConversionPanel.svelte";
import FormatDropdown from "$lib/components/functional/FormatDropdown.svelte";
import Uploader from "$lib/components/functional/Uploader.svelte";
import Panel from "$lib/components/visual/Panel.svelte";
import ProgressBar from "$lib/components/visual/ProgressBar.svelte";
import Tooltip from "$lib/components/visual/Tooltip.svelte";
import { categories, converters, byNative } from "$lib/converters";
import {
effects,
files,
gradientColor,
showGradient,
vertdLoaded,
} from "$lib/store/index.svelte";
import { addToast } from "$lib/store/ToastProvider";
import { VertFile } from "$lib/types";
import {
AudioLines,
BookText,
DownloadIcon,
FileMusicIcon,
FileQuestionIcon,
FileVideo2,
FilmIcon,
ImageIcon,
ImageOffIcon,
RotateCwIcon,
XIcon,
} from "lucide-svelte";
import { onMount } from "svelte";
onMount(() => {
// depending on format, select right category and format
files.files.forEach((file) => {
const converter = file.findConverter();
if (converter) {
const category = Object.keys(categories).find((cat) =>
categories[cat].formats.includes(file.to),
);
if (category) {
file.to = file.to || categories[category].formats[0];
}
}
});
});
const handleSelect = (option: string, file: VertFile) => {
file.result = null;
switch (option) {
case ".webp":
case ".gif":
addToast(
"warning",
`Converting this file to "${option}" may take some time if animated.`,
);
}
};
$effect(() => {
// Set gradient color depending on the file types
// TODO: if more file types added, add a "fileType" property to the file object
const allAudio = files.files.every((file) => {
const converter = file
.findConverters()
.sort(byNative(file.from))[0];
return converter?.name === "ffmpeg";
});
const allImages = files.files.every((file) => {
const converter = file
.findConverters()
.sort(byNative(file.from))[0];
return converter?.name === "libvips";
});
const allVideos = files.files.every((file) => {
const converter = file
.findConverters()
.sort(byNative(file.from))[0];
return converter?.name === "vertd";
});
const allDocuments = files.files.every((file) => {
const converter = file
.findConverters()
.sort(byNative(file.from))[0];
return converter?.name === "pandoc";
});
if (files.files.length === 1 && files.files[0].blobUrl && !allVideos) {
showGradient.set(false);
} else {
showGradient.set(true);
}
if (
files.files.length === 0 ||
(!allAudio && !allImages && !allVideos && !allDocuments)
) {
gradientColor.set("");
} else {
gradientColor.set(
allAudio
? "purple"
: allVideos
? "red"
: allDocuments
? "green"
: "blue",
);
}
// TODO: filter out categories that cant be converted between
});
</script>
{#snippet fileItem(file: VertFile, index: number)}
{@const currentConverter = converters.find(
(c) =>
c.formatStrings((f) => f.fromSupported).includes(file.from) &&
c.formatStrings((f) => f.toSupported).includes(file.to),
)}
{@const isAudio = converters
.find((c) => c.name === "ffmpeg")
?.supportedFormats.filter((f) => f.isNative)
.map((f) => f.name)
.includes(file.from)}
{@const isVideo = converters
.find((c) => c.name === "vertd")
?.supportedFormats.filter((f) => f.isNative)
.map((f) => f.name)
.includes(file.from)}
{@const isImage = converters
.find((c) => c.name === "libvips")
?.supportedFormats.filter((f) => f.isNative)
.map((f) => f.name)
.includes(file.from)}
{@const isDocument = converters
.find((c) => c.name === "pandoc")
?.supportedFormats.filter((f) => f.isNative)
.map((f) => f.name)
.includes(file.from)}
<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 !converters.length}
<Tooltip text="Unknown file type" position="bottom">
<FileQuestionIcon size="24" class="flex-shrink-0" />
</Tooltip>
{:else if isAudio}
<Tooltip text="Audio file" position="bottom">
<AudioLines size="24" class="flex-shrink-0" />
</Tooltip>
{:else if isVideo}
<Tooltip text="Video file" position="bottom">
<FilmIcon size="24" class="flex-shrink-0" />
</Tooltip>
{:else if isDocument}
<Tooltip text="Document file" position="bottom">
<BookText size="24" class="flex-shrink-0" />
</Tooltip>
{:else}
<Tooltip text="Image file" position="bottom">
<ImageIcon size="24" class="flex-shrink-0" />
</Tooltip>
{/if}
<div class="flex-grow overflow-hidden">
{#if file.processing}
<ProgressBar
min={0}
max={100}
progress={currentConverter?.reportsProgress
? file.progress
: null}
/>
{:else}
<h2
class="text-xl font-body overflow-hidden text-ellipsis whitespace-nowrap"
title={file.name}
>
{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>
{#if !currentConverter}
{#if file.name.startsWith("vertd")}
<div
class="h-full flex flex-col text-center justify-center text-failure"
>
<p class="font-body font-bold">
We can't convert this file.
</p>
<p class="font-normal">
what are you doing..? you're supposed to run the vertd
server!
</p>
</div>
{:else}
<div
class="h-full flex flex-col text-center justify-center text-failure"
>
<p class="font-body font-bold">
We can't convert this file.
</p>
<p class="font-normal">
Only image, video, audio, and document files are
supported
</p>
</div>
{/if}
{:else if isVideo && !isAudio && !isImage && !isDocument && !$vertdLoaded}
<div
class="h-full flex flex-col text-center justify-center text-failure"
>
<p class="font-body font-bold">We can't convert this file.</p>
<p class="font-normal">
Could not find the vertd instance to start video conversion.
Are you sure the instance URL is set correctly?
</p>
</div>
{:else}
<div class="flex flex-row justify-between">
<div
class="flex gap-4 w-full h-[152px] overflow-hidden relative"
>
<div class="w-1/2 h-full overflow-hidden rounded-xl">
{#if file.blobUrl}
<img
class="object-cover w-full h-full"
src={file.blobUrl}
alt={file.name}
/>
{:else}
<div
class="w-full h-full flex items-center justify-center text-black"
style="background: var({isAudio
? '--bg-gradient-purple-alt'
: isVideo
? '--bg-gradient-red-alt'
: isDocument
? '--bg-gradient-green-alt'
: '--bg-gradient-blue-alt'})"
>
{#if isAudio}
<FileMusicIcon size="56" />
{:else if isVideo}
<FileVideo2 size="56" />
{:else if isDocument}
<BookText size="56" />
{:else}
<ImageOffIcon size="56" />
{/if}
</div>
{/if}
</div>
</div>
<div
class="absolute top-16 right-0 mr-4 pl-2 h-[calc(100%-83px)] w-[calc(50%-38px)] pr-4 pb-1 flex items-center justify-center aspect-square"
>
<div
class="w-[122px] h-fit flex flex-col gap-2 items-center justify-center"
>
<FormatDropdown
{categories}
from={file.from}
bind:selected={file.to}
onselect={(option) => handleSelect(option, file)}
/>
<div class="w-full flex items-center justify-between">
<Tooltip text="Convert this file" position="bottom">
<button
class="btn {$effects
? ''
: '!scale-100'} p-0 w-14 h-14 text-black {isAudio
? 'bg-accent-purple'
: isVideo
? 'bg-accent-red'
: isDocument
? 'bg-accent-green'
: 'bg-accent-blue'}"
disabled={!files.ready}
onclick={() => file.convert()}
>
<RotateCwIcon size="24" />
</button>
</Tooltip>
<Tooltip
text="Download this file"
position="bottom"
>
<button
class="btn {$effects
? ''
: '!scale-100'} p-0 w-14 h-14"
onclick={file.download}
disabled={!file.result}
>
<DownloadIcon size="24" />
</button>
</Tooltip>
</div>
</div>
</div>
</div>
{/if}
</Panel>
{/snippet}
<div class="flex flex-col justify-center items-center gap-8 -mt-4 px-4 md:p-0">
<div class="max-w-[778px] w-full">
<ConversionPanel />
</div>
<div
class="w-full max-w-[778px] grid grid-cols-1 md:grid-cols-2 auto-rows-[240px] gap-4 md:p-0"
>
{#each files.files as file, i (file.id)}
{#if files.files.length >= 2 && i === 1}
<Uploader
class="w-full h-full col-start-1 row-start-1 md:col-start-2"
/>
{/if}
{@render fileItem(file, i)}
{#if files.files.length < 2}
<Uploader class="w-full h-full" />
{/if}
{/each}
{#if files.files.length === 0}
<Uploader class="w-full h-full col-span-2" />
{/if}
</div>
</div>