290 lines
8.5 KiB
Vue
290 lines
8.5 KiB
Vue
<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 { Progress } from '@/components/ui/progress';
|
|
import byteSize from 'byte-size';
|
|
import { uploadToWarren } from '~/lib/api/warrens';
|
|
import { toast } from 'vue-sonner';
|
|
import UploadListEntry from './UploadListEntry.vue';
|
|
import { useDropZone } from '@vueuse/core';
|
|
|
|
const warrenStore = useWarrenStore();
|
|
const uploadStore = useUploadStore();
|
|
|
|
const currentAndUploadRouteMatch = computed(
|
|
() =>
|
|
uploadStore.destination != null &&
|
|
warrenStore.current != null &&
|
|
uploadStore.destination.warrenId == warrenStore.current.warrenId &&
|
|
uploadStore.destination.path == warrenStore.current.path
|
|
);
|
|
|
|
const fileInputElement = ref<HTMLInputElement>(
|
|
null as unknown as HTMLInputElement
|
|
);
|
|
|
|
const uploading = ref(false);
|
|
const dropZoneRef = ref<HTMLElement>();
|
|
|
|
useDropZone(dropZoneRef, {
|
|
onDrop,
|
|
multiple: true,
|
|
});
|
|
|
|
function onDrop(files: File[] | null, event: DragEvent) {
|
|
event.preventDefault();
|
|
|
|
if (warrenStore.current == null || files == null || files.length < 1) {
|
|
return;
|
|
}
|
|
|
|
uploadStore.addFiles(
|
|
warrenStore.current.warrenId,
|
|
warrenStore.current.path,
|
|
files
|
|
);
|
|
}
|
|
|
|
function onFilesChanged(event: Event) {
|
|
if (warrenStore.current == null) {
|
|
toast.warning('Upload', {
|
|
description: 'Enter a warren before attempting to upload files',
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (event.target == null) {
|
|
return;
|
|
}
|
|
|
|
const target = event.target as HTMLInputElement;
|
|
|
|
if (target.files == null) {
|
|
return;
|
|
}
|
|
|
|
uploadStore.addFiles(
|
|
warrenStore.current.warrenId,
|
|
warrenStore.current.path,
|
|
[...target.files]
|
|
);
|
|
|
|
fileInputElement.value.value = '';
|
|
}
|
|
|
|
function removeFile(index: number) {
|
|
uploadStore.removeFile(index);
|
|
}
|
|
|
|
function clearCompletedFiles() {
|
|
uploadStore.clearCompletedFiles();
|
|
}
|
|
|
|
function updateFileListStatus(loadedBytes: number, ended: boolean = false) {
|
|
let acc = 0;
|
|
|
|
for (let i = uploadStore.startIndex; i < uploadStore.files.length; i++) {
|
|
if (uploadStore.progress == null) {
|
|
uploadStore.files[i].status = 'not_uploaded';
|
|
continue;
|
|
}
|
|
|
|
acc += uploadStore.files[i].data.size;
|
|
|
|
if (acc > loadedBytes) {
|
|
if (ended) {
|
|
for (let j = i; j < uploadStore.files.length; j++) {
|
|
uploadStore.files[j].status = 'failed';
|
|
}
|
|
} else {
|
|
uploadStore.files[i].status = 'uploading';
|
|
}
|
|
break;
|
|
}
|
|
|
|
uploadStore.files[i].status = 'completed';
|
|
}
|
|
}
|
|
|
|
async function submit() {
|
|
if (uploadStore.destination == null) {
|
|
return;
|
|
}
|
|
|
|
const dt = new DataTransfer();
|
|
|
|
uploadStore.startIndex = uploadStore.files.findIndex(
|
|
(f) => f.status !== 'completed'
|
|
);
|
|
let calculatedTotal = 0;
|
|
|
|
for (let i = uploadStore.startIndex; i < uploadStore.files.length; i++) {
|
|
const file = uploadStore.files[i];
|
|
|
|
if (file.status !== 'completed') {
|
|
dt.items.add(file.data);
|
|
calculatedTotal += file.data.size;
|
|
}
|
|
}
|
|
|
|
uploading.value = true;
|
|
|
|
uploadStore.progress = {
|
|
loadedBytes: 0,
|
|
totalBytes: calculatedTotal,
|
|
};
|
|
|
|
const { success } = await uploadToWarren(
|
|
uploadStore.destination.warrenId,
|
|
uploadStore.destination.path,
|
|
dt.files,
|
|
(loaded, total) => {
|
|
if (uploadStore.progress != null) {
|
|
uploadStore.progress.loadedBytes = loaded;
|
|
uploadStore.progress.totalBytes = total;
|
|
|
|
updateFileListStatus(loaded, false);
|
|
}
|
|
}
|
|
);
|
|
|
|
uploading.value = false;
|
|
if (success) {
|
|
if (uploadStore.progress != null) {
|
|
uploadStore.progress.loadedBytes = uploadStore.progress.totalBytes;
|
|
}
|
|
|
|
for (const file of uploadStore.files) {
|
|
file.status = 'completed';
|
|
}
|
|
} else {
|
|
updateFileListStatus(uploadStore.progress.loadedBytes, true);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<Dialog v-model:open="uploadStore.dialogOpen">
|
|
<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
|
|
<span class="text-foreground">{{
|
|
currentAndUploadRouteMatch ||
|
|
uploadStore.destination == null
|
|
? 'the current directory'
|
|
: routeWithWarrenName(
|
|
uploadStore.destination!.warrenId,
|
|
uploadStore.destination!.path
|
|
)
|
|
}}</span></DialogDescription
|
|
>
|
|
</DialogHeader>
|
|
|
|
<input
|
|
ref="fileInputElement"
|
|
class="hidden"
|
|
type="file"
|
|
multiple="true"
|
|
@change="onFilesChanged"
|
|
/>
|
|
|
|
<div
|
|
ref="dropZoneRef"
|
|
class="flex min-h-[280px] w-full items-center justify-center overflow-hidden rounded-xl border sm:aspect-video sm:min-h-[unset]"
|
|
>
|
|
<ScrollArea
|
|
v-if="uploadStore.files.length > 0"
|
|
class="h-full w-full"
|
|
>
|
|
<div
|
|
class="flex h-full w-full flex-col items-stretch gap-2 p-4 text-left"
|
|
>
|
|
<UploadListEntry
|
|
v-for="(file, i) in uploadStore.files"
|
|
:key="file.data.name"
|
|
:file="file"
|
|
:uploading="uploading"
|
|
@remove-file="() => removeFile(i)"
|
|
/>
|
|
</div>
|
|
</ScrollArea>
|
|
<div
|
|
v-else
|
|
class="flex h-full w-full items-center justify-center"
|
|
:disabled="fileInputElement == null"
|
|
@click="
|
|
() =>
|
|
uploadStore.files.length < 1 &&
|
|
fileInputElement?.click()
|
|
"
|
|
>
|
|
<Icon
|
|
class="h-[25%] w-[25%] opacity-25"
|
|
name="lucide:upload"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
v-if="uploadStore.progress != null"
|
|
class="flex flex-col gap-1"
|
|
>
|
|
<Progress
|
|
:model-value="
|
|
(uploadStore.progress.loadedBytes /
|
|
uploadStore.progress.totalBytes) *
|
|
100.0
|
|
"
|
|
class="h-4 [&_*]:!transition-none"
|
|
/>
|
|
<span class="text-right">
|
|
{{ byteSize(uploadStore.progress.loadedBytes) }} /
|
|
{{ byteSize(uploadStore.progress.totalBytes) }}
|
|
</span>
|
|
</div>
|
|
|
|
<DialogFooter>
|
|
<Button
|
|
class="sm:mr-auto"
|
|
variant="outline"
|
|
:disabled="
|
|
uploadStore.files.every((f) => f.status !== 'completed')
|
|
"
|
|
@click="clearCompletedFiles"
|
|
>
|
|
Clear completed
|
|
</Button>
|
|
<Button
|
|
variant="outline"
|
|
:disabled="fileInputElement == null || uploading"
|
|
@click="() => fileInputElement?.click()"
|
|
>
|
|
Select files
|
|
</Button>
|
|
<Button
|
|
:disabled="
|
|
uploading ||
|
|
!uploadStore.files.some((f) => f.status !== 'completed')
|
|
"
|
|
@click="submit"
|
|
>Upload</Button
|
|
>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</template>
|