AzuraCast/frontend/vue/components/Common/FlowUpload.vue

219 lines
5.8 KiB
Vue

<template>
<div class="flow-upload">
<div class="upload-progress">
<template v-for="(file, key) in files.value" :key="key">
<div v-if="file.isVisible" class="uploading-file pt-1" :id="'file_upload_' + file.uniqueIdentifier"
:class="{ 'text-success': file.isCompleted, 'text-danger': file.error }">
<h6 class="fileuploadname m-0">{{ file.name }}</h6>
<div v-if="!file.isCompleted" class="progress h-15 my-1">
<div class="progress-bar h-15" role="progressbar" :style="{width: file.progressPercent+'%'}"
:aria-valuenow="file.progressPercent" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div class="upload-status" v-if="file.error">
{{ file.error }}
</div>
<div class="size">{{ file.size }}</div>
</div>
</template>
</div>
<div class="file-drop-target" ref="$fileDropTarget">
{{ $gettext('Drag file(s) here to upload or') }}
<button ref="$fileBrowseTarget" class="file-upload btn btn-primary text-center ml-1" type="button">
<icon icon="cloud_upload"></icon>
{{ $gettext('Select File') }}
</button>
<small class="file-name"></small>
<input type="file" :accept="validMimeTypesList" :multiple="allowMultiple"
style="visibility: hidden; position: absolute;"/>
</div>
</div>
</template>
<style lang="scss">
div.flow-upload {
div.upload-progress {
padding: 4px 0;
& > div {
padding: 3px 0;
}
.error {
color: #a00;
}
.progress {
margin-bottom: 5px;
.progress-bar {
border-bottom-width: 10px;
&::after {
height: 10px;
}
}
}
}
div.file-drop-target {
padding: 25px 0;
text-align: center;
input {
display: inline;
}
}
}
</style>
<script setup>
import formatFileSize from '~/functions/formatFileSize.js';
import Icon from './Icon.vue';
import {defaultsDeep, forEach, toInteger} from 'lodash';
import {computed, onMounted, onUnmounted, reactive, ref} from "vue";
import Flow from "@flowjs/flow.js";
import {useAzuraCast} from "~/vendor/azuracast";
import {useTranslate} from "~/vendor/gettext";
const props = defineProps({
targetUrl: String,
allowMultiple: {
type: Boolean,
default: false
},
validMimeTypes: {
type: Array,
default() {
return ['*'];
}
},
flowConfiguration: {
type: Object,
default() {
return {};
}
}
});
const emit = defineEmits(['complete', 'success', 'error']);
const validMimeTypesList = computed(() => {
return props.validMimeTypes.join(', ');
});
let flow = null;
const files = reactive({
value: {},
push(file) {
this.value[file.uniqueIdentifier] = {
name: file.name,
uniqueIdentifier: file.uniqueIdentifier,
size: formatFileSize(file.size),
isVisible: true,
isCompleted: false,
progressPercent: 0,
error: null
};
},
get(file) {
return this.value[file.uniqueIdentifier] ?? {};
},
hideAll() {
forEach(this.value, (file) => {
file.isVisible = false;
});
},
reset() {
this.value = {};
}
});
const $fileBrowseTarget = ref(); // Template Ref
const $fileDropTarget = ref(); // Template Ref
const {apiCsrf} = useAzuraCast();
const {$gettext} = useTranslate();
onMounted(() => {
let defaultConfig = {
target: () => {
return props.targetUrl
},
singleFile: !props.allowMultiple,
headers: {
'Accept': 'application/json',
'X-API-CSRF': apiCsrf
},
withCredentials: true,
allowDuplicateUploads: true,
fileParameterName: 'file_data',
uploadMethod: 'POST',
testMethod: 'GET',
method: 'multipart',
maxChunkRetries: 3,
testChunks: false
};
let config = defaultsDeep({}, props.flowConfiguration, defaultConfig);
flow = new Flow(config);
flow.assignBrowse($fileBrowseTarget.value);
flow.assignDrop($fileDropTarget.value);
flow.on('fileAdded', (file) => {
files.push(file);
return true;
});
flow.on('filesSubmitted', () => {
flow.upload();
});
flow.on('fileProgress', (file) => {
files.get(file).progressPercent = toInteger(file.progress() * 100);
});
flow.on('fileSuccess', (file, message) => {
files.get(file).isCompleted = true;
let messageJson = JSON.parse(message);
emit('success', file, messageJson);
});
flow.on('error', (message, file, chunk) => {
console.error(message, file, chunk);
let messageText = $gettext('Could not upload file.');
try {
if (typeof message !== 'undefined') {
let messageJson = JSON.parse(message);
if (typeof messageJson.message !== 'undefined') {
messageText = messageJson.message;
if (messageText.indexOf(': ') > -1) {
messageText = messageText.split(': ')[1];
}
}
}
} catch (e) {
}
files.get(file).error = messageText;
emit('error', file, messageText);
});
flow.on('complete', () => {
files.hideAll();
emit('complete');
});
});
onUnmounted(() => {
flow = null;
files.reset();
});
</script>