file uploads

This commit is contained in:
2025-07-13 07:17:40 +02:00
parent 11105c074f
commit 548dd7e9ef
14 changed files with 451 additions and 13 deletions

View File

@@ -0,0 +1,66 @@
<script setup lang="ts">
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { createDirectory } from '~/lib/api/warrens';
const warrenRoute = useWarrenRoute();
const creating = ref(false);
const open = ref(false);
const directoryName = ref('');
async function submit() {
creating.value = true;
const { success } = await createDirectory(
warrenRoute.value,
directoryName.value
);
creating.value = false;
if (success) {
directoryName.value = '';
open.value = false;
}
}
</script>
<template>
<Dialog v-model:open="open">
<DialogTrigger as-child>
<slot />
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Create a directory</DialogTitle>
<DialogDescription
>Give your directory a memorable name</DialogDescription
>
</DialogHeader>
<Input
v-model="directoryName"
type="text"
name="directory-name"
placeholder="my-awesome-directory"
minlength="20"
maxlength="30"
aria-required="true"
autocomplete="off"
required
/>
<DialogFooter>
<Button :disabled="creating" @click="submit">Create</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</template>

View File

@@ -0,0 +1,196 @@
<script setup lang="ts">
import {
Dialog,
DialogTrigger,
DialogContent,
DialogHeader,
DialogTitle,
DialogDescription,
DialogFooter,
} from '@/components/ui/dialog';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Button } from '@/components/ui/button';
import byteSize from 'byte-size';
import { uploadToWarren } from '~/lib/api/warrens';
const warrenRoute = useWarrenRoute();
const fileInputElement = ref<HTMLInputElement>(
null as unknown as HTMLInputElement
);
const uploading = ref(false);
const open = ref(false);
const files = ref<Array<{ uploaded: boolean; data: File }>>([]);
const presentPaths = new Set<string>();
function onFilesChanged(event: Event) {
if (event.target == null) {
return;
}
const target = event.target as HTMLInputElement;
if (target.files == null) {
return;
}
for (const file of target.files) {
if (presentPaths.has(file.name)) {
continue;
}
files.value.push({ uploaded: false, data: file });
presentPaths.add(file.name);
}
fileInputElement.value.value = '';
}
async function submit() {
if (files.value.length < 1) {
return;
}
uploading.value = true;
const dt = new DataTransfer();
for (const file of files.value) {
if (!file.uploaded) {
dt.items.add(file.data);
}
}
const { success } = await uploadToWarren(warrenRoute.value, dt.files);
uploading.value = false;
if (success) {
for (const file of files.value) {
file.uploaded = true;
}
}
}
function removeFile(index: number) {
if (files.value.length <= index) {
return;
}
const [file] = files.value.splice(index, 1);
presentPaths.delete(file.data.name);
}
function clearCompletedFiles() {
files.value = files.value.filter((f) => {
if (f.uploaded) {
presentPaths.delete(f.data.name);
return false;
}
return true;
});
}
</script>
<template>
<Dialog v-model:open="open">
<DialogTrigger as-child>
<slot />
</DialogTrigger>
<DialogContent class="!min-w-[min(50%,98vw)] sm:!min-h-[unset]">
<DialogHeader>
<DialogTitle>Upload files</DialogTitle>
<DialogDescription
>Upload files to the current directory</DialogDescription
>
</DialogHeader>
<input
ref="fileInputElement"
class="hidden"
type="file"
multiple="true"
@change="onFilesChanged"
/>
<div
class="flex min-h-[280px] sm:min-h-[unset] sm:aspect-video w-full items-center justify-center rounded-xl border overflow-hidden"
>
<ScrollArea v-if="files.length > 0" class="h-full w-full">
<div
class="flex h-full w-full flex-col items-stretch gap-1 text-left p-2"
>
<div
v-for="(file, i) in files"
:key="file.data.name"
class="flex flex-row items-center justify-between gap-4 rounded-lg border px-4 py-2"
>
<div class="rounded-sm border p-3">
<Icon
v-if="!file.uploaded"
class="h-5 w-5"
:name="getFileIcon(file.data.type)"
/>
<Icon
v-else
class="h-5 w-5 text-green-300"
name="lucide:circle-check"
/>
</div>
<div class="flex flex-col grow overflow-hidden">
<span class="font-medium truncate">{{
file.data.name
}}</span>
<span class="text-muted-foreground">{{
byteSize(file.data.size)
}}</span>
</div>
<div class="flex flex-row gap-2 items-center">
<Button
variant="ghost"
size="icon"
:disabled="uploading"
@click="() => removeFile(i)"
>
<Icon name="lucide:x" />
</Button>
</div>
</div>
</div>
</ScrollArea>
<div
v-else
class="flex h-full w-full items-center justify-center"
:disabled="fileInputElement == null"
@click="() => files.length < 1 && fileInputElement?.click()"
>
<Icon
class="h-[25%] w-[25%] opacity-25"
name="lucide:upload"
/>
</div>
</div>
<DialogFooter>
<Button
class="sm:mr-auto"
variant="outline"
:disabled="files.every((f) => !f.uploaded)"
@click="clearCompletedFiles"
>
Clear completed
</Button>
<Button
variant="outline"
:disabled="fileInputElement == null || uploading"
@click="() => fileInputElement?.click()"
>
Select files
</Button>
<Button
:disabled="uploading || !files.some((f) => !f.uploaded)"
@click="submit"
>Upload</Button
>
</DialogFooter>
</DialogContent>
</Dialog>
</template>