295 lines
8.5 KiB
Vue
295 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';
|
|
|
|
const uploadStore = useUploadStore();
|
|
|
|
const warrenRoute = useWarrenRoute();
|
|
|
|
const fileInputElement = ref<HTMLInputElement>(
|
|
null as unknown as HTMLInputElement
|
|
);
|
|
|
|
const uploading = ref(false);
|
|
const presentPaths = new Set<string>();
|
|
|
|
function onFilesChanged(event: Event) {
|
|
if (warrenRoute.value == 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;
|
|
}
|
|
|
|
if (uploadStore.path == null) {
|
|
uploadStore.path = warrenRoute.value;
|
|
} else if (uploadStore.path !== warrenRoute.value) {
|
|
toast.warning('Upload', {
|
|
description:
|
|
'The unfinished items belong to a different directory. Remove them before attempting to upload to a different directory.',
|
|
});
|
|
return;
|
|
}
|
|
|
|
for (const file of target.files) {
|
|
if (presentPaths.has(file.name)) {
|
|
continue;
|
|
}
|
|
uploadStore.files.push({
|
|
status: 'not_uploaded',
|
|
data: file,
|
|
});
|
|
presentPaths.add(file.name);
|
|
}
|
|
|
|
fileInputElement.value.value = '';
|
|
}
|
|
|
|
function removeFile(index: number) {
|
|
if (uploadStore.files.length <= index) {
|
|
return;
|
|
}
|
|
|
|
const [file] = uploadStore.files.splice(index, 1);
|
|
presentPaths.delete(file.data.name);
|
|
|
|
if (uploadStore.files.length < 1) {
|
|
uploadStore.path = null;
|
|
uploadStore.progress = null;
|
|
}
|
|
}
|
|
|
|
function clearCompletedFiles() {
|
|
uploadStore.files = uploadStore.files.filter((f) => {
|
|
if (f.status === 'completed') {
|
|
presentPaths.delete(f.data.name);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
if (uploadStore.files.length < 1) {
|
|
uploadStore.path = null;
|
|
uploadStore.progress = null;
|
|
}
|
|
}
|
|
|
|
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.path == 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.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>
|
|
<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">{{
|
|
uploadStore.path == null ||
|
|
warrenRoute === uploadStore.path
|
|
? 'the current directory'
|
|
: routeWithWarrenName(uploadStore.path)
|
|
}}</span></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="uploadStore.files.length > 0"
|
|
class="h-full w-full"
|
|
>
|
|
<div
|
|
class="flex h-full w-full flex-col items-stretch gap-2 text-left p-4"
|
|
>
|
|
<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>
|