2022-04-15 14:24:30 +00:00
|
|
|
<template>
|
|
|
|
<section id="uploadWrapper">
|
2022-07-16 15:44:45 +00:00
|
|
|
<ScreenHeader layout="collapsed">
|
2022-07-05 14:35:15 +00:00
|
|
|
Upload Media
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
<template v-slot:controls>
|
2022-04-15 17:00:08 +00:00
|
|
|
<BtnGroup uppercased v-if="hasUploadFailures">
|
2022-04-21 18:39:18 +00:00
|
|
|
<Btn data-testid="upload-retry-all-btn" green @click="retryAll">
|
2022-07-15 07:23:55 +00:00
|
|
|
<icon :icon="faRotateBack"/>
|
2022-04-15 14:24:30 +00:00
|
|
|
Retry All
|
2022-04-15 17:00:08 +00:00
|
|
|
</Btn>
|
2022-04-21 18:39:18 +00:00
|
|
|
<Btn data-testid="upload-remove-all-btn" orange @click="removeFailedEntries">
|
2022-07-15 07:23:55 +00:00
|
|
|
<icon :icon="faTimes"/>
|
2022-04-15 14:24:30 +00:00
|
|
|
Remove Failed
|
2022-04-15 17:00:08 +00:00
|
|
|
</Btn>
|
|
|
|
</BtnGroup>
|
2022-04-15 14:24:30 +00:00
|
|
|
</template>
|
2022-04-15 17:00:08 +00:00
|
|
|
</ScreenHeader>
|
2022-04-15 14:24:30 +00:00
|
|
|
|
|
|
|
<div class="main-scroll-wrap">
|
|
|
|
<div
|
2022-04-21 18:39:18 +00:00
|
|
|
v-if="mediaPath"
|
|
|
|
:class="{ droppable }"
|
2022-04-15 14:24:30 +00:00
|
|
|
class="upload-panel"
|
|
|
|
@dragenter.prevent="onDragEnter"
|
|
|
|
@dragleave.prevent="onDragLeave"
|
|
|
|
@drop.stop.prevent="onDrop"
|
|
|
|
@dragover.prevent
|
|
|
|
>
|
2022-04-21 18:39:18 +00:00
|
|
|
<div class="upload-files" v-if="files.length">
|
2022-05-10 23:01:48 +00:00
|
|
|
<UploadItem v-for="file in files" :key="file.id" :file="file" data-testid="upload-item"/>
|
2022-04-15 14:24:30 +00:00
|
|
|
</div>
|
|
|
|
|
2022-04-21 18:12:11 +00:00
|
|
|
<ScreenEmptyState v-else>
|
2022-04-15 14:24:30 +00:00
|
|
|
<template v-slot:icon>
|
2022-07-15 07:23:55 +00:00
|
|
|
<icon :icon="faUpload"/>
|
2022-04-15 14:24:30 +00:00
|
|
|
</template>
|
|
|
|
|
2022-07-15 07:23:55 +00:00
|
|
|
{{ canDropFolders ? 'Drop files or folders to upload' : 'Drop files to upload' }}
|
|
|
|
|
2022-04-15 14:24:30 +00:00
|
|
|
<span class="secondary d-block">
|
|
|
|
<a class="or-click d-block" role="button">
|
|
|
|
or click here to select songs
|
2022-04-21 18:39:18 +00:00
|
|
|
<input :accept="acceptAttribute" multiple name="file[]" type="file" @change="onFileInputChange"/>
|
2022-04-15 14:24:30 +00:00
|
|
|
</a>
|
|
|
|
</span>
|
2022-04-21 18:12:11 +00:00
|
|
|
</ScreenEmptyState>
|
2022-04-15 14:24:30 +00:00
|
|
|
</div>
|
|
|
|
|
2022-04-21 18:12:11 +00:00
|
|
|
<ScreenEmptyState v-else>
|
2022-04-15 14:24:30 +00:00
|
|
|
<template v-slot:icon>
|
2022-07-15 07:23:55 +00:00
|
|
|
<icon :icon="faWarning"/>
|
2022-04-15 14:24:30 +00:00
|
|
|
</template>
|
|
|
|
No media path set.
|
2022-04-21 18:12:11 +00:00
|
|
|
</ScreenEmptyState>
|
2022-04-15 14:24:30 +00:00
|
|
|
</div>
|
|
|
|
</section>
|
|
|
|
</template>
|
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
<script lang="ts" setup>
|
2022-07-15 07:23:55 +00:00
|
|
|
import { faRotateBack, faTimes, faUpload, faWarning } from '@fortawesome/free-solid-svg-icons'
|
2022-04-15 14:24:30 +00:00
|
|
|
import ismobile from 'ismobilejs'
|
|
|
|
import md5 from 'blueimp-md5'
|
2022-04-21 18:39:18 +00:00
|
|
|
import { computed, defineAsyncComponent, ref, toRef } from 'vue'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-05-10 23:01:48 +00:00
|
|
|
import { settingStore } from '@/stores'
|
2022-07-15 07:23:55 +00:00
|
|
|
import { eventBus, getAllFileEntries, isDirectoryReadingSupported as canDropFolders } from '@/utils'
|
2022-04-28 09:00:20 +00:00
|
|
|
import { acceptedMediaTypes, UploadFile } from '@/config'
|
2022-04-24 08:50:45 +00:00
|
|
|
import { uploadService } from '@/services'
|
2022-04-30 14:05:02 +00:00
|
|
|
import { useAuthorization } from '@/composables'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-07-07 18:05:46 +00:00
|
|
|
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
|
|
|
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
|
2022-04-15 14:24:30 +00:00
|
|
|
|
2022-07-07 18:05:46 +00:00
|
|
|
const BtnGroup = defineAsyncComponent(() => import('@/components/ui/BtnGroup.vue'))
|
|
|
|
const Btn = defineAsyncComponent(() => import('@/components/ui/Btn.vue'))
|
|
|
|
const UploadItem = defineAsyncComponent(() => import('@/components/ui/upload/UploadItem.vue'))
|
2022-04-15 17:00:08 +00:00
|
|
|
|
2022-04-24 08:29:14 +00:00
|
|
|
const acceptAttribute = acceptedMediaTypes.join(',')
|
2022-04-21 18:39:18 +00:00
|
|
|
|
2022-04-15 17:00:08 +00:00
|
|
|
const mediaPath = toRef(settingStore.state, 'media_path')
|
2022-04-24 08:50:45 +00:00
|
|
|
const files = toRef(uploadService.state, 'files')
|
2022-04-15 17:00:08 +00:00
|
|
|
const droppable = ref(false)
|
|
|
|
const hasUploadFailures = ref(false)
|
|
|
|
|
2022-04-30 14:05:02 +00:00
|
|
|
const { isAdmin } = useAuthorization()
|
|
|
|
const allowsUpload = computed(() => isAdmin.value && !ismobile.any)
|
2022-04-15 17:00:08 +00:00
|
|
|
|
|
|
|
const onDragEnter = () => (droppable.value = allowsUpload.value)
|
|
|
|
const onDragLeave = () => (droppable.value = false)
|
|
|
|
|
|
|
|
const handleFiles = (files: Array<File>) => {
|
|
|
|
const uploadCandidates = files
|
2022-04-24 08:29:14 +00:00
|
|
|
.filter(file => acceptedMediaTypes.includes(file.type))
|
2022-04-15 17:00:08 +00:00
|
|
|
.map((file): UploadFile => ({
|
|
|
|
file,
|
|
|
|
id: md5(`${file.name}-${file.size}`), // for simplicity, a file's identity is determined by its name and size
|
|
|
|
status: 'Ready',
|
|
|
|
name: file.name,
|
|
|
|
progress: 0
|
|
|
|
}))
|
|
|
|
|
2022-04-24 08:50:45 +00:00
|
|
|
uploadService.queue(uploadCandidates)
|
2022-04-15 17:00:08 +00:00
|
|
|
}
|
|
|
|
|
2022-04-30 14:05:02 +00:00
|
|
|
const fileEntryToFile = async (entry: FileSystemEntry) => new Promise<File>(resolve => entry.file(resolve))
|
2022-04-15 17:00:08 +00:00
|
|
|
|
|
|
|
const onFileInputChange = (event: InputEvent) => {
|
|
|
|
const selectedFileList = (event.target as HTMLInputElement).files
|
2022-04-28 09:00:20 +00:00
|
|
|
selectedFileList?.length && handleFiles(Array.from(selectedFileList))
|
2022-04-15 17:00:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const onDrop = async (event: DragEvent) => {
|
|
|
|
droppable.value = false
|
|
|
|
|
|
|
|
if (!event.dataTransfer) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const fileEntries = await getAllFileEntries(event.dataTransfer.items)
|
|
|
|
const files = await Promise.all(fileEntries.map(async entry => await fileEntryToFile(entry)))
|
|
|
|
handleFiles(files)
|
|
|
|
}
|
|
|
|
|
|
|
|
const retryAll = () => {
|
2022-04-24 08:50:45 +00:00
|
|
|
uploadService.retryAll()
|
2022-04-15 17:00:08 +00:00
|
|
|
hasUploadFailures.value = false
|
|
|
|
}
|
|
|
|
|
|
|
|
const removeFailedEntries = () => {
|
2022-04-24 08:50:45 +00:00
|
|
|
uploadService.removeFailed()
|
2022-04-15 17:00:08 +00:00
|
|
|
hasUploadFailures.value = false
|
|
|
|
}
|
|
|
|
|
2022-04-28 09:00:20 +00:00
|
|
|
eventBus.on('UPLOAD_QUEUE_FINISHED', () => {
|
|
|
|
hasUploadFailures.value = uploadService.getFilesByStatus('Errored').length !== 0
|
|
|
|
})
|
2022-04-15 14:24:30 +00:00
|
|
|
</script>
|
|
|
|
|
|
|
|
<style lang="scss">
|
|
|
|
#uploadWrapper {
|
|
|
|
.upload-panel {
|
|
|
|
position: relative;
|
|
|
|
height: 100%;
|
|
|
|
}
|
|
|
|
|
|
|
|
.upload-files {
|
|
|
|
padding-bottom: 1rem;
|
|
|
|
}
|
|
|
|
|
|
|
|
input[type=file] {
|
|
|
|
position: absolute;
|
|
|
|
top: 0;
|
|
|
|
left: 0;
|
|
|
|
opacity: 0;
|
|
|
|
width: 100%;
|
|
|
|
height: 100%;
|
|
|
|
z-index: 2;
|
|
|
|
cursor: pointer;
|
|
|
|
}
|
|
|
|
|
|
|
|
a.or-click {
|
|
|
|
position: relative;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|