koel/resources/assets/js/components/screens/UploadScreen.vue

174 lines
4.7 KiB
Vue
Raw Normal View History

2022-04-15 14:24:30 +00:00
<template>
<section id="uploadWrapper">
2022-04-15 17:00:08 +00:00
<ScreenHeader>
2022-04-15 14:24:30 +00:00
Upload Media <sup>Beta</sup>
<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-04-15 14:24:30 +00:00
<i class="fa fa-repeat"></i>
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-04-15 14:24:30 +00:00
<i class="fa fa-times"></i>
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">
<UploadItem v-for="file in files" :key="file.id" :file="file" data-test="upload-item"/>
2022-04-15 14:24:30 +00:00
</div>
<ScreenEmptyState v-else>
2022-04-15 14:24:30 +00:00
<template v-slot:icon>
<i class="fa fa-upload"></i>
</template>
{{ instructionText }}
<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>
</ScreenEmptyState>
2022-04-15 14:24:30 +00:00
</div>
<ScreenEmptyState v-else>
2022-04-15 14:24:30 +00:00
<template v-slot:icon>
<i class="fa fa-exclamation-triangle"></i>
</template>
No media path set.
</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-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
import { settingStore, userStore } from '@/stores'
2022-04-15 17:00:08 +00:00
import { eventBus, getAllFileEntries, isDirectoryReadingSupported } from '@/utils'
import { UploadFile, validMediaMimeTypes } from '@/config'
2022-04-15 14:24:30 +00:00
import { upload } from '@/services'
2022-04-21 18:39:18 +00:00
import UploadItem from '@/components/ui/upload/UploadItem.vue'
2022-04-21 16:06:45 +00:00
import BtnGroup from '@/components/ui/BtnGroup.vue'
2022-04-21 18:39:18 +00:00
import Btn from '@/components/ui/Btn.vue'
2022-04-15 14:24:30 +00:00
2022-04-21 16:06:45 +00:00
const ScreenHeader = defineAsyncComponent(() => import('@/components/ui/ScreenHeader.vue'))
const ScreenEmptyState = defineAsyncComponent(() => import('@/components/ui/ScreenEmptyState.vue'))
2022-04-15 17:00:08 +00:00
2022-04-21 18:39:18 +00:00
const acceptAttribute = validMediaMimeTypes.join(',')
2022-04-15 17:00:08 +00:00
const mediaPath = toRef(settingStore.state, 'media_path')
2022-04-21 18:39:18 +00:00
const files = toRef(upload.state, 'files')
2022-04-15 17:00:08 +00:00
const droppable = ref(false)
const hasUploadFailures = ref(false)
2022-04-21 23:06:12 +00:00
const allowsUpload = computed(() => userStore.state.current.is_admin && !ismobile.any)
2022-04-15 17:00:08 +00:00
2022-04-21 18:39:18 +00:00
const instructionText = isDirectoryReadingSupported
2022-04-15 17:00:08 +00:00
? 'Drop files or folders to upload'
: 'Drop files to upload'
const onDragEnter = () => (droppable.value = allowsUpload.value)
const onDragLeave = () => (droppable.value = false)
const handleFiles = (files: Array<File>) => {
const uploadCandidates = files
.filter(file => validMediaMimeTypes.includes(file.type))
.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
}))
upload.queue(uploadCandidates)
}
const fileEntryToFile = async (entry: FileSystemEntry): Promise<File> => new Promise(resolve => {
entry.file(resolve)
2022-04-15 14:24:30 +00:00
})
2022-04-15 17:00:08 +00:00
const onFileInputChange = (event: InputEvent) => {
const selectedFileList = (event.target as HTMLInputElement).files
selectedFileList && handleFiles(Array.from(selectedFileList))
}
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 = () => {
upload.retryAll()
hasUploadFailures.value = false
}
const removeFailedEntries = () => {
upload.removeFailed()
hasUploadFailures.value = false
}
eventBus.on('UPLOAD_QUEUE_FINISHED', () => (hasUploadFailures.value = upload.getFilesByStatus('Errored').length !== 0))
2022-04-15 14:24:30 +00:00
</script>
<style lang="scss">
#uploadWrapper {
sup {
vertical-align: super;
font-size: .4em;
text-transform: uppercase;
opacity: .5;
}
.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>