feat(front): #2081 new upload process
This commit is contained in:
parent
0caee2181d
commit
3cd7548cf0
|
@ -18,11 +18,9 @@ import AlbumSelect from '~/components/channels/AlbumSelect.vue'
|
||||||
|
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
|
||||||
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
|
import Button from '~/components/ui/Button.vue'
|
||||||
|
import Loader from '~/components/ui/Loader.vue'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
|
@ -485,20 +483,17 @@ const createEmptyChannel = async () => {
|
||||||
console.log("Error:", error)
|
console.log("Error:", error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<form
|
<form
|
||||||
:class="['ui', { loading: availableChannels.loading }, 'form component-file-upload']"
|
:class="['ui', { loading: availableChannels.loading }, 'form component-file-upload']"
|
||||||
@submit.stop.prevent
|
@submit.stop.prevent
|
||||||
>
|
>
|
||||||
<!-- Error message -->
|
<!-- Error message -->
|
||||||
|
<Alert
|
||||||
<div
|
|
||||||
v-if="errors.length > 0"
|
v-if="errors.length > 0"
|
||||||
role="alert"
|
|
||||||
class="ui negative message"
|
|
||||||
>
|
>
|
||||||
<h4 class="header">
|
<h4 class="header">
|
||||||
{{ t('components.channels.UploadForm.header.error') }}
|
{{ t('components.channels.UploadForm.header.error') }}
|
||||||
|
@ -511,14 +506,21 @@ const createEmptyChannel = async () => {
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</Alert>
|
||||||
|
|
||||||
<!-- Select Album and License -->
|
<!-- Select Album and License -->
|
||||||
|
|
||||||
<div :class="['ui', 'required', 'field']">
|
<div :class="['ui', 'required', 'field']">
|
||||||
<label for="channel-dropdown">
|
<label for="channel-dropdown">
|
||||||
{{ t('components.channels.UploadForm.label.channel') }}: {{ selectedChannel?.artist.name }}
|
{{ t('components.channels.UploadForm.label.channel') }}: {{ selectedChannel?.artist.name }}
|
||||||
</label>
|
</label>
|
||||||
|
<select
|
||||||
|
v-if="availableChannels.count >= 1"
|
||||||
|
id="channel-dropdown"
|
||||||
|
>
|
||||||
|
<option v-for="channel in availableChannels.channels" :value="channel.uuid">
|
||||||
|
{{ channel.artist.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<album-select
|
<album-select
|
||||||
v-model.number="values.album"
|
v-model.number="values.album"
|
||||||
|
@ -539,43 +541,37 @@ const createEmptyChannel = async () => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Files to upload -->
|
<!-- Files to upload -->
|
||||||
|
<template v-if="remainingSpace === 0">
|
||||||
<div
|
<Alert
|
||||||
v-if="remainingSpace === 0"
|
red
|
||||||
role="alert"
|
>
|
||||||
class="ui warning message"
|
<i class="bi bi-exclamation-triangle" />
|
||||||
>
|
{{ t('components.channels.UploadForm.warning.quota') }}
|
||||||
<div class="content">
|
</Alert>
|
||||||
<p>
|
</template>
|
||||||
<i class="warning icon" />
|
|
||||||
{{ t('components.channels.UploadForm.warning.quota') }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div
|
<Alert
|
||||||
v-if="draftUploads?.length > 0 && includeDraftUploads === undefined"
|
v-if="draftUploads?.length > 0 && includeDraftUploads === undefined"
|
||||||
class="ui visible info message"
|
blue
|
||||||
>
|
>
|
||||||
<p>
|
<p>
|
||||||
<i class="redo icon" />
|
<i class="bi bi-circle-clockwise" />
|
||||||
{{ t('components.channels.UploadForm.message.pending') }}
|
{{ t('components.channels.UploadForm.message.pending') }}
|
||||||
</p>
|
</p>
|
||||||
<button
|
<Button
|
||||||
class="ui basic button"
|
|
||||||
@click.stop.prevent="includeDraftUploads = false"
|
@click.stop.prevent="includeDraftUploads = false"
|
||||||
>
|
>
|
||||||
{{ t('components.channels.UploadForm.button.ignore') }}
|
{{ t('components.channels.UploadForm.button.ignore') }}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
class="ui basic button"
|
|
||||||
@click.stop.prevent="includeDraftUploads = true"
|
@click.stop.prevent="includeDraftUploads = true"
|
||||||
>
|
>
|
||||||
{{ t('components.channels.UploadForm.button.resume') }}
|
{{ t('components.channels.UploadForm.button.resume') }}
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</Alert>
|
||||||
<div
|
<Alert
|
||||||
v-if="uploadedFiles.length > 0"
|
v-if="uploadedFiles.length > 0"
|
||||||
|
v-bind="{[ uploadedFiles.some(file=>file.error) ? 'red' : 'green' ]:true}"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="file in uploadedFiles"
|
v-for="file in uploadedFiles"
|
||||||
|
@ -583,27 +579,22 @@ const createEmptyChannel = async () => {
|
||||||
class="channel-file"
|
class="channel-file"
|
||||||
>
|
>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div
|
<Button
|
||||||
v-if="file.response?.uuid"
|
v-if="file.response?.uuid"
|
||||||
role="button"
|
icon="bi-pencil-fill"
|
||||||
class="ui basic icon button"
|
class="ui basic icon button"
|
||||||
:title="labels.editTitle"
|
:title="labels.editTitle"
|
||||||
@click.stop.prevent="selectedUploadId = file.response?.uuid"
|
@click.stop.prevent="selectedUploadId = file.response?.uuid"
|
||||||
>
|
/>
|
||||||
<i class="pencil icon" />
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-if="file.error"
|
v-if="file.error"
|
||||||
class="ui basic danger icon label"
|
class="ui basic danger icon label"
|
||||||
:title="file.error.toString()"
|
:title="file.error.toString()"
|
||||||
@click.stop.prevent="selectedUploadId = file.response?.uuid"
|
@click.stop.prevent="selectedUploadId = file.response?.uuid"
|
||||||
>
|
>
|
||||||
<i class="warning sign icon" />
|
<i class="bi bi-exclamation-triangle-fill" />
|
||||||
</div>
|
</div>
|
||||||
<div
|
<Loader v-else-if="file.active && !file.response" />
|
||||||
v-else-if="file.active && !file.response"
|
|
||||||
class="ui active slow inline loader"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h4 class="ui header">
|
<h4 class="ui header">
|
||||||
<template v-if="file.metadata.title">
|
<template v-if="file.metadata.title">
|
||||||
|
@ -649,39 +640,37 @@ const createEmptyChannel = async () => {
|
||||||
</div>
|
</div>
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Alert>
|
||||||
<upload-metadata-form
|
|
||||||
v-if="selectedUpload"
|
|
||||||
v-model:values="uploadImportData[selectedUploadId]"
|
|
||||||
:upload="selectedUpload"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="ui message"
|
|
||||||
>
|
|
||||||
<div class="content">
|
|
||||||
<p>
|
|
||||||
<i class="info icon" />
|
|
||||||
{{ t('components.channels.UploadForm.description.extensions', {extensions: store.state.ui.supportedExtensions.join(', ')}) }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<file-upload-widget
|
|
||||||
ref="upload"
|
|
||||||
v-model="files"
|
|
||||||
:class="['ui', 'icon', 'basic', 'button', 'channels']"
|
|
||||||
:data="baseImportMetadata"
|
|
||||||
@input-file="beforeFileUpload"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<i class="upload icon" />
|
|
||||||
{{ t('components.channels.UploadForm.message.dragAndDrop') }}
|
|
||||||
</div>
|
|
||||||
<div class="ui very small divider" />
|
|
||||||
<div>
|
|
||||||
{{ t('components.channels.UploadForm.label.openBrowser') }}
|
|
||||||
</div>
|
|
||||||
</file-upload-widget>
|
|
||||||
<div class="ui hidden divider" />
|
|
||||||
</template>
|
</template>
|
||||||
|
<upload-metadata-form
|
||||||
|
v-if="selectedUpload"
|
||||||
|
v-model:values="uploadImportData[selectedUploadId]"
|
||||||
|
:upload="selectedUpload"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="ui message"
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
<p>
|
||||||
|
<i class="info icon" />
|
||||||
|
{{ t('components.channels.UploadForm.description.extensions', {extensions: store.state.ui.supportedExtensions.join(', ')}) }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<file-upload-widget
|
||||||
|
ref="upload"
|
||||||
|
v-model="files"
|
||||||
|
:data="baseImportMetadata"
|
||||||
|
@input-file="beforeFileUpload"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<i class="bi bi-upload" />
|
||||||
|
{{ t('components.channels.UploadForm.message.dragAndDrop') }}
|
||||||
|
</div>
|
||||||
|
<div class="ui very small divider" />
|
||||||
|
<div>
|
||||||
|
{{ t('components.channels.UploadForm.label.openBrowser') }}
|
||||||
|
</div>
|
||||||
|
</file-upload-widget>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { BackendError, Library, FileSystem } from '~/types'
|
import type { BackendError, Library, FileSystem, PrivacyLevel } from '~/types'
|
||||||
import type { VueUploadItem } from 'vue-upload-component'
|
import type { VueUploadItem } from 'vue-upload-component'
|
||||||
|
|
||||||
import { computed, ref, reactive, watch, nextTick } from 'vue'
|
import { computed, ref, reactive, watch, nextTick } from 'vue'
|
||||||
|
@ -19,6 +19,11 @@ import FsLogs from './FsLogs.vue'
|
||||||
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
||||||
import updateQueryString from '~/composables/updateQueryString'
|
import updateQueryString from '~/composables/updateQueryString'
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
|
|
||||||
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
|
import Button from '~/components/ui/Button.vue'
|
||||||
|
import Slider from '~/components/ui/Slider.vue'
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'uploads-finished', delta: number):void
|
(e: 'uploads-finished', delta: number):void
|
||||||
|
@ -158,16 +163,6 @@ const sortedFiles = computed(() => {
|
||||||
|
|
||||||
const hasActiveUploads = computed(() => files.value.some(file => file.active))
|
const hasActiveUploads = computed(() => files.value.some(file => file.active))
|
||||||
|
|
||||||
// const isOpen = computed({
|
|
||||||
// get() {
|
|
||||||
// return store.state.ui.modalsOpen.has(modalName);
|
|
||||||
// },
|
|
||||||
// set(value) {
|
|
||||||
// store.commit('ui/setModal', [modalName, value]);
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Quota status
|
// Quota status
|
||||||
//
|
//
|
||||||
|
@ -303,281 +298,294 @@ useEventListener(window, 'beforeunload', (event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
return (event.returnValue = t('components.library.FileUpload.message.listener'))
|
return (event.returnValue = t('components.library.FileUpload.message.listener'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const sharedLabels = useSharedLabels()
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
me: sharedLabels.fields.privacy_level.choices.me,
|
||||||
|
instance: sharedLabels.fields.privacy_level.choices.instance,
|
||||||
|
everyone: sharedLabels.fields.privacy_level.choices.everyone
|
||||||
|
} as const satisfies Record<PrivacyLevel, string>;
|
||||||
|
|
||||||
|
const option = ref<keyof typeof options>("me");
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="component-file-upload">
|
<div :class="{loading: isLoadingQuota}">
|
||||||
<div class="ui top attached tabular menu">
|
<div :class="['ui', {red: remainingSpace === 0}, {warning: remainingSpace > 0 && remainingSpace <= 50}, 'small', 'statistic']">
|
||||||
<a
|
<div class="label">
|
||||||
href=""
|
{{ t('components.library.FileUpload.label.remainingSpace') }}
|
||||||
:class="['item', {active: currentTab === 'uploads'}]"
|
|
||||||
@click.prevent="currentTab = 'uploads'"
|
|
||||||
>
|
|
||||||
{{ t('components.library.FileUpload.link.uploading') }}
|
|
||||||
<div
|
|
||||||
v-if="files.length === 0"
|
|
||||||
class="ui label"
|
|
||||||
>
|
|
||||||
{{ t('components.library.FileUpload.empty.noFiles') }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="files.length > uploadedFilesCount + erroredFilesCount"
|
|
||||||
class="ui warning label"
|
|
||||||
>
|
|
||||||
{{ uploadedFilesCount + erroredFilesCount }}
|
|
||||||
<span class="slash symbol" />
|
|
||||||
{{ files.length }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
:class="['ui', {'success': erroredFilesCount === 0}, {'danger': erroredFilesCount > 0}, 'label']"
|
|
||||||
>
|
|
||||||
{{ uploadedFilesCount + erroredFilesCount }}
|
|
||||||
<span class="slash symbol" />
|
|
||||||
{{ files.length }}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href=""
|
|
||||||
:class="['item', {active: currentTab === 'processing'}]"
|
|
||||||
@click.prevent="currentTab = 'processing'"
|
|
||||||
>
|
|
||||||
{{ t('components.library.FileUpload.link.processing') }}
|
|
||||||
<div
|
|
||||||
v-if="processableFiles === 0"
|
|
||||||
class="ui label"
|
|
||||||
>
|
|
||||||
{{ t('components.library.FileUpload.empty.noFiles') }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="processableFiles > processedFilesCount"
|
|
||||||
class="ui warning label"
|
|
||||||
>
|
|
||||||
{{ processedFilesCount }}
|
|
||||||
<span class="slash symbol" />
|
|
||||||
{{ processableFiles }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
:class="['ui', {'success': uploads.errored === 0}, {'danger': uploads.errored > 0}, 'label']"
|
|
||||||
>
|
|
||||||
{{ processedFilesCount }}
|
|
||||||
<span class="slash symbol" />
|
|
||||||
{{ processableFiles }}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div :class="['ui', 'bottom', 'attached', 'segment', {hidden: currentTab != 'uploads'}]">
|
|
||||||
<div :class="['ui', {loading: isLoadingQuota}, 'container']">
|
|
||||||
<div :class="['ui', {red: remainingSpace === 0}, {warning: remainingSpace > 0 && remainingSpace <= 50}, 'small', 'statistic']">
|
|
||||||
<div class="label">
|
|
||||||
{{ t('components.library.FileUpload.label.remainingSpace') }}
|
|
||||||
</div>
|
|
||||||
<div class="value">
|
|
||||||
{{ humanSize(remainingSpace * 1000 * 1000) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui divider" />
|
|
||||||
<h2 class="ui header">
|
|
||||||
{{ t('components.library.FileUpload.header.local') }}
|
|
||||||
</h2>
|
|
||||||
<div class="ui message">
|
|
||||||
<p>
|
|
||||||
{{ t('components.library.FileUpload.message.local.message') }}
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
<li v-if="library.privacy_level != 'me'">
|
|
||||||
{{ t('components.library.FileUpload.message.local.copyright') }}
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{{ t('components.library.FileUpload.message.local.tag') }}
|
|
||||||
<a
|
|
||||||
href="http://picard.musicbrainz.org/"
|
|
||||||
target="_blank"
|
|
||||||
>{{ t('components.library.FileUpload.link.picard') }}</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
{{ t('components.library.FileUpload.message.local.format') }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<file-upload-widget
|
|
||||||
ref="upload"
|
|
||||||
v-model="files"
|
|
||||||
:class="['ui', 'icon', 'basic', 'button']"
|
|
||||||
:data="uploadData"
|
|
||||||
@input-file="inputFile"
|
|
||||||
>
|
|
||||||
<i class="upload icon" />
|
|
||||||
{{ t('components.library.FileUpload.label.uploadWidget') }}
|
|
||||||
<br>
|
|
||||||
<br>
|
|
||||||
<i>
|
|
||||||
{{ t('components.library.FileUpload.label.extensions', {extensions: supportedExtensions.join(', ')}) }}
|
|
||||||
</i>
|
|
||||||
</file-upload-widget>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div class="value">
|
||||||
v-if="files.length > 0"
|
{{ humanSize(remainingSpace * 1000 * 1000) }}
|
||||||
class="table-wrapper"
|
|
||||||
>
|
|
||||||
<div class="ui hidden divider" />
|
|
||||||
<table class="ui unstackable table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th class="ten wide">
|
|
||||||
{{ t('components.library.FileUpload.table.upload.header.filename') }}
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
{{ t('components.library.FileUpload.table.upload.header.size') }}
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
{{ t('components.library.FileUpload.table.upload.header.status') }}
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
{{ t('components.library.FileUpload.table.upload.header.actions') }}
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
<tr v-if="retryableFiles.length > 1">
|
|
||||||
<th class="ten wide" />
|
|
||||||
<th />
|
|
||||||
<th />
|
|
||||||
<th>
|
|
||||||
<button
|
|
||||||
class="ui right floated small basic button"
|
|
||||||
@click.prevent="retry(retryableFiles)"
|
|
||||||
>
|
|
||||||
{{ t('components.library.FileUpload.button.retry') }}
|
|
||||||
</button>
|
|
||||||
</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr
|
|
||||||
v-for="file in sortedFiles"
|
|
||||||
:key="file.id"
|
|
||||||
>
|
|
||||||
<td :title="file.name">
|
|
||||||
{{ truncate(file.name ?? '', 60) }}
|
|
||||||
</td>
|
|
||||||
<td>{{ humanSize(file.size ?? 0) }}</td>
|
|
||||||
<td>
|
|
||||||
<span
|
|
||||||
v-if="typeof file.error === 'string' && file.error"
|
|
||||||
class="ui tooltip"
|
|
||||||
:data-tooltip="labels.tooltips[file.error]"
|
|
||||||
>
|
|
||||||
<span class="ui danger icon label">
|
|
||||||
<i class="question circle outline icon" /> {{ file.error }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-else-if="file.success"
|
|
||||||
class="ui success label"
|
|
||||||
>
|
|
||||||
<span key="1">
|
|
||||||
{{ t('components.library.FileUpload.table.upload.status.uploaded') }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-else-if="file.active"
|
|
||||||
class="ui warning label"
|
|
||||||
>
|
|
||||||
<span key="2">
|
|
||||||
{{ t('components.library.FileUpload.table.upload.status.uploading') }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{{ t('components.library.FileUpload.table.upload.progress', {percent: parseFloat(file.progress ?? '0.00')}) }}
|
|
||||||
</span>
|
|
||||||
<span
|
|
||||||
v-else
|
|
||||||
class="ui label"
|
|
||||||
>
|
|
||||||
<span key="3">
|
|
||||||
{{ t('components.library.FileUpload.table.upload.status.pending') }}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<template v-if="file.error">
|
|
||||||
<button
|
|
||||||
v-if="retryableFiles.includes(file)"
|
|
||||||
class="ui tiny basic icon right floated button"
|
|
||||||
:title="labels.tooltips.retry"
|
|
||||||
@click.prevent="retry([file])"
|
|
||||||
>
|
|
||||||
<i class="redo icon" />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
<template v-else-if="!file.success">
|
|
||||||
<button
|
|
||||||
class="ui tiny basic danger icon right floated button"
|
|
||||||
@click.prevent="upload.remove(file)"
|
|
||||||
>
|
|
||||||
<i class="delete icon" />
|
|
||||||
</button>
|
|
||||||
</template>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="ui divider" />
|
|
||||||
<h2 class="ui header">
|
|
||||||
{{ t('components.library.FileUpload.header.server') }}
|
|
||||||
</h2>
|
|
||||||
<div
|
|
||||||
v-if="fsErrors.length > 0"
|
|
||||||
role="alert"
|
|
||||||
class="ui negative message"
|
|
||||||
>
|
|
||||||
<h3 class="header">
|
|
||||||
{{ t('components.library.FileUpload.header.failure') }}
|
|
||||||
</h3>
|
|
||||||
<ul class="list">
|
|
||||||
<li
|
|
||||||
v-for="(error, key) in fsErrors"
|
|
||||||
:key="key"
|
|
||||||
>
|
|
||||||
{{ error }}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<fs-browser
|
|
||||||
v-model="fsPath"
|
|
||||||
:loading="isLoadingFs"
|
|
||||||
:data="fsStatus"
|
|
||||||
@import="importFs"
|
|
||||||
/>
|
|
||||||
<template v-if="fsStatus && fsStatus.import">
|
|
||||||
<h3 class="ui header">
|
|
||||||
{{ t('components.library.FileUpload.header.status') }}
|
|
||||||
</h3>
|
|
||||||
<p v-if="fsStatus.import.reference !== importReference">
|
|
||||||
{{ t('components.library.FileUpload.description.previousImport') }}
|
|
||||||
</p>
|
|
||||||
<p v-else>
|
|
||||||
{{ t('components.library.FileUpload.description.import') }}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="fsStatus.import.status === 'started' || fsStatus.import.status === 'pending'"
|
|
||||||
class="ui button"
|
|
||||||
@click="cancelFsScan"
|
|
||||||
>
|
|
||||||
{{ t('components.library.FileUpload.button.cancel') }}
|
|
||||||
</button>
|
|
||||||
<fs-logs :data="fsStatus.import" />
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div :class="['ui', 'bottom', 'attached', 'segment', {hidden: currentTab != 'processing'}]">
|
|
||||||
<library-files-table
|
|
||||||
:needs-refresh="needsRefresh"
|
|
||||||
ordering-config-name="library.detail.upload"
|
|
||||||
:filters="{import_reference: importReference}"
|
|
||||||
:custom-objects="Object.values(uploads.objects)"
|
|
||||||
@fetch-start="needsRefresh = false"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Slider :options="options" v-model="option" :label="t('components.manage.library.UploadsTable.label.visibility')" />
|
||||||
|
<file-upload-widget
|
||||||
|
ref="upload"
|
||||||
|
v-model="files"
|
||||||
|
:data="uploadData"
|
||||||
|
@input-file="inputFile"
|
||||||
|
>
|
||||||
|
<Button primary icon="bi bi-upload" >
|
||||||
|
{{ t('components.library.FileUpload.label.uploadWidget') }}
|
||||||
|
</Button>
|
||||||
|
<p>
|
||||||
|
{{ t('components.library.FileUpload.label.extensions', {extensions: supportedExtensions.join(', ')}) }}
|
||||||
|
</p>
|
||||||
|
</file-upload-widget>
|
||||||
|
|
||||||
|
<Alert blue>
|
||||||
|
<h2 class="ui header">
|
||||||
|
{{ t('components.library.FileUpload.header.local') }}
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
{{ t('components.library.FileUpload.message.local.message') }}
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li v-if="library.privacy_level != 'me'">
|
||||||
|
{{ t('components.library.FileUpload.message.local.copyright') }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ t('components.library.FileUpload.message.local.tag') }}
|
||||||
|
<a
|
||||||
|
href="http://picard.musicbrainz.org/"
|
||||||
|
target="_blank"
|
||||||
|
>{{ t('components.library.FileUpload.link.picard') }}</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ t('components.library.FileUpload.message.local.format') }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Alert>
|
||||||
|
|
||||||
|
<h2 class="ui header">
|
||||||
|
{{ t('components.library.FileUpload.header.server') }}
|
||||||
|
</h2>
|
||||||
|
<Alert
|
||||||
|
v-if="fsErrors.length > 0"
|
||||||
|
red
|
||||||
|
>
|
||||||
|
<h3 class="header">
|
||||||
|
{{ t('components.library.FileUpload.header.failure') }}
|
||||||
|
</h3>
|
||||||
|
<ul class="list">
|
||||||
|
<li
|
||||||
|
v-for="(error, key) in fsErrors"
|
||||||
|
:key="key"
|
||||||
|
>
|
||||||
|
{{ error }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Alert>
|
||||||
|
<fs-browser
|
||||||
|
v-model="fsPath"
|
||||||
|
:loading="isLoadingFs"
|
||||||
|
:data="fsStatus"
|
||||||
|
@import="importFs"
|
||||||
|
/>
|
||||||
|
<template v-if="fsStatus && fsStatus.import">
|
||||||
|
<h3 class="ui header">
|
||||||
|
{{ t('components.library.FileUpload.header.status') }}
|
||||||
|
</h3>
|
||||||
|
<p v-if="fsStatus.import.reference !== importReference">
|
||||||
|
{{ t('components.library.FileUpload.description.previousImport') }}
|
||||||
|
</p>
|
||||||
|
<p v-else>
|
||||||
|
{{ t('components.library.FileUpload.description.import') }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
v-if="fsStatus.import.status === 'started' || fsStatus.import.status === 'pending'"
|
||||||
|
secondary
|
||||||
|
@click="cancelFsScan"
|
||||||
|
>
|
||||||
|
{{ t('components.library.FileUpload.button.cancel') }}
|
||||||
|
</Button>
|
||||||
|
<fs-logs :data="fsStatus.import" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<library-files-table
|
||||||
|
:needs-refresh="needsRefresh"
|
||||||
|
ordering-config-name="library.detail.upload"
|
||||||
|
:filters="{import_reference: importReference}"
|
||||||
|
:custom-objects="Object.values(uploads.objects)"
|
||||||
|
@fetch-start="needsRefresh = false"
|
||||||
|
/>
|
||||||
|
<div class="ui top attached tabular menu">
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
:class="['item', {active: currentTab === 'uploads'}]"
|
||||||
|
@click.prevent="currentTab = 'uploads'"
|
||||||
|
>
|
||||||
|
{{ t('components.library.FileUpload.link.uploading') }}
|
||||||
|
<div
|
||||||
|
v-if="files.length === 0"
|
||||||
|
class="ui label"
|
||||||
|
>
|
||||||
|
{{ t('components.library.FileUpload.empty.noFiles') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="files.length > uploadedFilesCount + erroredFilesCount"
|
||||||
|
class="ui warning label"
|
||||||
|
>
|
||||||
|
{{ uploadedFilesCount + erroredFilesCount }}
|
||||||
|
<span class="bi bi-slash-circle" />
|
||||||
|
{{ files.length }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:class="['ui', {'success': erroredFilesCount === 0}, {'danger': erroredFilesCount > 0}, 'label']"
|
||||||
|
>
|
||||||
|
{{ uploadedFilesCount + erroredFilesCount }}
|
||||||
|
<span class="bi bi-slash-circle" />
|
||||||
|
{{ files.length }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
:class="['item', {active: currentTab === 'processing'}]"
|
||||||
|
@click.prevent="currentTab = 'processing'"
|
||||||
|
>
|
||||||
|
{{ t('components.library.FileUpload.link.processing') }}
|
||||||
|
<div
|
||||||
|
v-if="processableFiles === 0"
|
||||||
|
class="ui label"
|
||||||
|
>
|
||||||
|
{{ t('components.library.FileUpload.empty.noFiles') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="processableFiles > processedFilesCount"
|
||||||
|
class="ui warning label"
|
||||||
|
>
|
||||||
|
{{ processedFilesCount }}
|
||||||
|
<span class="bi bi-slash-circle" />
|
||||||
|
{{ processableFiles }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
:class="['ui', {'success': uploads.errored === 0}, {'danger': uploads.errored > 0}, 'label']"
|
||||||
|
>
|
||||||
|
{{ processedFilesCount }}
|
||||||
|
<span class="bi bi-slash-circle" />
|
||||||
|
{{ processableFiles }}
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="files.length > 0"
|
||||||
|
class="table-wrapper"
|
||||||
|
>
|
||||||
|
<table class="ui unstackable table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="ten wide">
|
||||||
|
{{ t('components.library.FileUpload.table.upload.header.filename') }}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{{ t('components.library.FileUpload.table.upload.header.size') }}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{{ t('components.library.FileUpload.table.upload.header.status') }}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{{ t('components.library.FileUpload.table.upload.header.actions') }}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
<tr v-if="retryableFiles.length > 1">
|
||||||
|
<th class="ten wide" />
|
||||||
|
<th />
|
||||||
|
<th />
|
||||||
|
<th>
|
||||||
|
<Button
|
||||||
|
tiny
|
||||||
|
style="float: right;"
|
||||||
|
@click.prevent="retry(retryableFiles)"
|
||||||
|
>
|
||||||
|
{{ t('components.library.FileUpload.button.retry') }}
|
||||||
|
</Button>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr
|
||||||
|
v-for="file in sortedFiles"
|
||||||
|
:key="file.id"
|
||||||
|
>
|
||||||
|
<td :title="file.name">
|
||||||
|
{{ truncate(file.name ?? '', 60) }}
|
||||||
|
</td>
|
||||||
|
<td>{{ humanSize(file.size ?? 0) }}</td>
|
||||||
|
<td>
|
||||||
|
<span
|
||||||
|
v-if="typeof file.error === 'string' && file.error"
|
||||||
|
class="ui tooltip"
|
||||||
|
:data-tooltip="labels.tooltips[file.error]"
|
||||||
|
>
|
||||||
|
<span class="ui danger icon label">
|
||||||
|
<i class="bi bi-question-circle-fill" /> {{ file.error }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="file.success"
|
||||||
|
class="ui success label"
|
||||||
|
>
|
||||||
|
<span key="1">
|
||||||
|
{{ t('components.library.FileUpload.table.upload.status.uploaded') }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else-if="file.active"
|
||||||
|
class="ui warning label"
|
||||||
|
>
|
||||||
|
<span key="2">
|
||||||
|
{{ t('components.library.FileUpload.table.upload.status.uploading') }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{{ t('components.library.FileUpload.table.upload.progress', {percent: parseFloat(file.progress ?? '0.00')}) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
v-else
|
||||||
|
class="ui label"
|
||||||
|
>
|
||||||
|
<span key="3">
|
||||||
|
{{ t('components.library.FileUpload.table.upload.status.pending') }}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<template v-if="file.error">
|
||||||
|
<Button
|
||||||
|
v-if="retryableFiles.includes(file)"
|
||||||
|
tiny
|
||||||
|
style="float: right;"
|
||||||
|
:title="labels.tooltips.retry"
|
||||||
|
icon="bi-arrow-clockwise"
|
||||||
|
@click.prevent="retry([file])"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="!file.success">
|
||||||
|
<Button
|
||||||
|
tiny
|
||||||
|
@click.prevent="upload.remove(file)"
|
||||||
|
style="float: right;"
|
||||||
|
icon="bi-trash-fill"
|
||||||
|
>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.file-uploads {
|
||||||
|
padding: 32px;
|
||||||
|
border-radius: var(--fw-border-radius);
|
||||||
|
border: 2px dashed var(--border-color);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -86,7 +86,7 @@ export default { inheritAttrs: false }
|
||||||
<file-upload
|
<file-upload
|
||||||
ref="upload"
|
ref="upload"
|
||||||
v-bind="$attrs"
|
v-bind="$attrs"
|
||||||
:post-action="store.getters['instance/absoluteUrl']('/api/v1/uploads/')"
|
:post-action="store.getters['instance/absoluteUrl']('/api/v2/uploads/')"
|
||||||
:multiple="true"
|
:multiple="true"
|
||||||
:thread="1"
|
:thread="1"
|
||||||
:custom-action="uploadAction"
|
:custom-action="uploadAction"
|
||||||
|
|
|
@ -2,9 +2,12 @@
|
||||||
import type { FileSystem, FSEntry } from '~/types'
|
import type { FileSystem, FSEntry } from '~/types'
|
||||||
|
|
||||||
import { useVModel } from '@vueuse/core'
|
import { useVModel } from '@vueuse/core'
|
||||||
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import Layout from '~/components/ui/Layout.vue'
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
|
import Input from '~/components/ui/Input.vue'
|
||||||
|
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
|
@ -34,23 +37,23 @@ const handleClick = (entry: FSEntry) => {
|
||||||
|
|
||||||
value.value.push(entry.name)
|
value.value.push(entry.name)
|
||||||
}
|
}
|
||||||
|
const path = computed (() => props.data.root + '/' + value.value.join('/'))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="['ui', { loading }, 'segment']">
|
<div :class="['ui', { loading }]">
|
||||||
<div class="ui fluid action input">
|
<Layout flex>
|
||||||
<input
|
<Input
|
||||||
class="ui disabled"
|
v-model="path"
|
||||||
disabled
|
disabled
|
||||||
:value="props.data.root + '/' + value.join('/')"
|
/>
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
class="ui button"
|
primary
|
||||||
@click.prevent="emit('import')"
|
@click.prevent="emit('import')"
|
||||||
>
|
>
|
||||||
{{ t('components.library.FsBrowser.button.import') }}
|
{{ t('components.library.FsBrowser.button.import') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Layout>
|
||||||
<div class="ui list component-fs-browser">
|
<div class="ui list component-fs-browser">
|
||||||
<a
|
<a
|
||||||
v-if="value.length > 0"
|
v-if="value.length > 0"
|
||||||
|
@ -58,7 +61,7 @@ const handleClick = (entry: FSEntry) => {
|
||||||
href=""
|
href=""
|
||||||
@click.prevent="handleClick({ name: '..', dir: true })"
|
@click.prevent="handleClick({ name: '..', dir: true })"
|
||||||
>
|
>
|
||||||
<i class="folder icon" />
|
<i class="bi bi-folder" />
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="header doubledot symbol" />
|
<div class="header doubledot symbol" />
|
||||||
</div>
|
</div>
|
||||||
|
@ -72,11 +75,11 @@ const handleClick = (entry: FSEntry) => {
|
||||||
>
|
>
|
||||||
<i
|
<i
|
||||||
v-if="e.dir"
|
v-if="e.dir"
|
||||||
class="folder icon"
|
class="bi bi-folder"
|
||||||
/>
|
/>
|
||||||
<i
|
<i
|
||||||
v-else
|
v-else
|
||||||
class="file icon"
|
class="bi bi-file-earmark-music-fill"
|
||||||
/>
|
/>
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="header">{{ e.name }}</div>
|
<div class="header">{{ e.name }}</div>
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
import onKeyboardShortcut from '~/composables/onKeyboardShortcut';
|
import { useStore } from '~/store'
|
||||||
|
import onKeyboardShortcut from '~/composables/onKeyboardShortcut'
|
||||||
import { useModal } from '~/ui/composables/useModal.ts'
|
import { useModal } from '~/ui/composables/useModal.ts'
|
||||||
|
|
||||||
import Modal from '~/components/ui/Modal.vue'
|
import Modal from '~/components/ui/Modal.vue'
|
||||||
import Button from '~/components/ui/Button.vue'
|
import Button from '~/components/ui/Button.vue'
|
||||||
import Input from '~/components/ui/Input.vue'
|
|
||||||
import Layout from '~/components/ui/Layout.vue'
|
import Layout from '~/components/ui/Layout.vue'
|
||||||
import Spacer from '~/components/ui/Spacer.vue'
|
import Spacer from '~/components/ui/Spacer.vue'
|
||||||
import Alert from '~/components/ui/Alert.vue';
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
import Card from '~/components/ui/Card.vue';
|
import Card from '~/components/ui/Card.vue'
|
||||||
import Pagination from '~/components/ui/Pagination.vue';
|
|
||||||
|
|
||||||
import type { Actor, Channel } from '~/types';
|
import type { Actor, Channel } from '~/types'
|
||||||
import FileUploadWidget from '~/components/library/FileUploadWidget.vue';
|
import FileUploadWidget from '~/components/library/FileUploadWidget.vue'
|
||||||
import type { VueUploadItem } from 'vue-upload-component';
|
import type { VueUploadItem } from 'vue-upload-component'
|
||||||
import ChannelUpload from '~/components/channels/UploadForm.vue';
|
import ChannelUpload from '~/components/channels/UploadForm.vue'
|
||||||
import LibraryUpload from '~/components/library/FileUpload.vue';
|
import LibraryUpload from '~/components/library/FileUpload.vue'
|
||||||
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
@ -75,20 +73,14 @@ const channelUpload = ref();
|
||||||
v-model="isOpen"
|
v-model="isOpen"
|
||||||
>
|
>
|
||||||
|
|
||||||
<!-- Alert -->
|
|
||||||
|
|
||||||
<template #alert v-if="state.page === 'selectDestination'">
|
|
||||||
<Alert blue>
|
|
||||||
Before uploading, please ensure your files are tagged properly.
|
|
||||||
We recommend using Picard for that purpose.
|
|
||||||
</Alert>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<!-- Page content -->
|
<!-- Page content -->
|
||||||
<!-- Page 1 -->
|
<!-- Page 1 -->
|
||||||
|
|
||||||
<Layout flex style="place-content:center" v-if="state.page === 'selectDestination'">
|
<Layout flex style="place-content:center" v-if="state.page === 'selectDestination'">
|
||||||
<Card small title="Music"
|
<Card
|
||||||
|
small
|
||||||
|
title="Music"
|
||||||
|
solid
|
||||||
icon="bi-upload primary solid"
|
icon="bi-upload primary solid"
|
||||||
@click="destinationSelected('channel')"
|
@click="destinationSelected('channel')"
|
||||||
>
|
>
|
||||||
|
@ -97,7 +89,10 @@ const channelUpload = ref();
|
||||||
</template>
|
</template>
|
||||||
{{ "Publish music you make" /* TODO: Translate */ }}
|
{{ "Publish music you make" /* TODO: Translate */ }}
|
||||||
</Card>
|
</Card>
|
||||||
<Card small title="Podcast"
|
<Card
|
||||||
|
small
|
||||||
|
solid
|
||||||
|
title="Podcast"
|
||||||
icon="bi-upload primary solid"
|
icon="bi-upload primary solid"
|
||||||
@click="destinationSelected('podcast')"
|
@click="destinationSelected('podcast')"
|
||||||
>
|
>
|
||||||
|
@ -106,7 +101,10 @@ const channelUpload = ref();
|
||||||
</template>
|
</template>
|
||||||
{{ "Publish podcasts you make" /* TODO: Translate */ }}
|
{{ "Publish podcasts you make" /* TODO: Translate */ }}
|
||||||
</Card>
|
</Card>
|
||||||
<Card small title="Mix & Share"
|
<Card
|
||||||
|
small
|
||||||
|
solid
|
||||||
|
title="Mix & Share"
|
||||||
icon="bi-upload"
|
icon="bi-upload"
|
||||||
@click="destinationSelected('library')"
|
@click="destinationSelected('library')"
|
||||||
>
|
>
|
||||||
|
@ -128,9 +126,9 @@ const channelUpload = ref();
|
||||||
<!-- -->
|
<!-- -->
|
||||||
|
|
||||||
<!-- Privacy Slider -->
|
<!-- Privacy Slider -->
|
||||||
<!-- <LibraryUpload v-if="state.uploadDestination === 'library'"
|
<LibraryUpload v-if="state.uploadDestination === 'library'"
|
||||||
:library="{uuid: 'string'} /* Get corresponding library from user */">
|
:library="{uuid: 'string'} /* Get corresponding library from user */">
|
||||||
</LibraryUpload> -->
|
</LibraryUpload>
|
||||||
{{ state.files }}
|
{{ state.files }}
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
||||||
|
|
|
@ -12,16 +12,23 @@ import { useI18n } from 'vue-i18n'
|
||||||
import time from '~/utils/time'
|
import time from '~/utils/time'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
|
|
||||||
import ActionTable from '~/components/common/ActionTable.vue'
|
|
||||||
import Pagination from '~/components/vui/Pagination.vue'
|
|
||||||
|
|
||||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||||
import useSmartSearch from '~/composables/navigation/useSmartSearch'
|
import useSmartSearch from '~/composables/navigation/useSmartSearch'
|
||||||
import useOrdering from '~/composables/navigation/useOrdering'
|
import useOrdering from '~/composables/navigation/useOrdering'
|
||||||
import useErrorHandler from '~/composables/useErrorHandler'
|
import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
import usePage from '~/composables/navigation/usePage'
|
import usePage from '~/composables/navigation/usePage'
|
||||||
|
|
||||||
|
import ImportStatusModal from '~/components/library/ImportStatusModal.vue'
|
||||||
|
import ActionTable from '~/components/common/ActionTable.vue'
|
||||||
|
|
||||||
|
import Layout from '~/components/ui/Layout.vue'
|
||||||
|
import Alert from '~/components/ui/Alert.vue'
|
||||||
|
import Pagination from '~/components/ui/Pagination.vue'
|
||||||
|
import Button from '~/components/ui/Button.vue'
|
||||||
|
import Input from '~/components/ui/Input.vue'
|
||||||
|
import Loader from '~/components/ui/Loader.vue'
|
||||||
|
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'fetch-start'): void
|
(e: 'fetch-start'): void
|
||||||
}
|
}
|
||||||
|
@ -133,226 +140,205 @@ const getImportStatusChoice = (importStatus: ImportStatus) => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<Layout form>
|
||||||
<div class="ui inline form">
|
<div class="fields">
|
||||||
<div class="fields">
|
<div class="ui six wide field">
|
||||||
<div class="ui six wide field">
|
<form @submit.prevent="console.log(search);query = search">
|
||||||
<label for="files-search">
|
<Input
|
||||||
{{ t('views.content.libraries.FilesTable.label.search') }}
|
search
|
||||||
</label>
|
id="files-search"
|
||||||
<form @submit.prevent="query = search.value">
|
:label="t('views.content.libraries.FilesTable.label.search')"
|
||||||
<input
|
v-model="search"
|
||||||
id="files-search"
|
:placeholder="labels.searchPlaceholder"
|
||||||
ref="search"
|
/>
|
||||||
name="search"
|
</form>
|
||||||
type="text"
|
|
||||||
:value="query"
|
|
||||||
:placeholder="labels.searchPlaceholder"
|
|
||||||
>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="import-status">
|
|
||||||
{{ t('views.content.libraries.FilesTable.label.importStatus') }}
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="import-status"
|
|
||||||
class="ui dropdown"
|
|
||||||
:value="getTokenValue('status', '')"
|
|
||||||
@change="addSearchToken('status', ($event.target as HTMLSelectElement).value)"
|
|
||||||
>
|
|
||||||
<option value>
|
|
||||||
{{ t('views.content.libraries.FilesTable.option.status.all') }}
|
|
||||||
</option>
|
|
||||||
<option value="draft">
|
|
||||||
{{ t('views.content.libraries.FilesTable.option.status.draft') }}
|
|
||||||
</option>
|
|
||||||
<option value="pending">
|
|
||||||
{{ t('views.content.libraries.FilesTable.option.status.pending') }}
|
|
||||||
</option>
|
|
||||||
<option value="skipped">
|
|
||||||
{{ t('views.content.libraries.FilesTable.option.status.skipped') }}
|
|
||||||
</option>
|
|
||||||
<option value="errored">
|
|
||||||
{{ t('views.content.libraries.FilesTable.option.status.failed') }}
|
|
||||||
</option>
|
|
||||||
<option value="finished">
|
|
||||||
{{ t('views.content.libraries.FilesTable.option.status.finished') }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="ordering-select">
|
|
||||||
{{ t('views.content.libraries.FilesTable.ordering.label') }}
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="ordering-select"
|
|
||||||
v-model="ordering"
|
|
||||||
class="ui dropdown"
|
|
||||||
>
|
|
||||||
<option
|
|
||||||
v-for="(option, key) in orderingOptions"
|
|
||||||
:key="key"
|
|
||||||
:value="option[0]"
|
|
||||||
>
|
|
||||||
{{ sharedLabels.filters[option[1]] }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="field">
|
|
||||||
<label for="ordering-direction">
|
|
||||||
{{ t('views.content.libraries.FilesTable.ordering.direction.label') }}
|
|
||||||
</label>
|
|
||||||
<select
|
|
||||||
id="ordering-direction"
|
|
||||||
v-model="orderingDirection"
|
|
||||||
class="ui dropdown"
|
|
||||||
>
|
|
||||||
<option value="+">
|
|
||||||
{{ t('views.content.libraries.FilesTable.ordering.direction.ascending') }}
|
|
||||||
</option>
|
|
||||||
<option value="-">
|
|
||||||
{{ t('views.content.libraries.FilesTable.ordering.direction.descending') }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="field">
|
||||||
<import-status-modal
|
<label for="import-status">
|
||||||
v-if="detailedUpload"
|
{{ t('views.content.libraries.FilesTable.label.importStatus') }}
|
||||||
v-model:show="showUploadDetailModal"
|
</label>
|
||||||
:upload="detailedUpload"
|
<select
|
||||||
/>
|
id="import-status"
|
||||||
<div class="dimmable">
|
class="ui dropdown"
|
||||||
<div
|
:value="getTokenValue('status', '')"
|
||||||
v-if="isLoading"
|
@change="addSearchToken('status', ($event.target as HTMLSelectElement).value)"
|
||||||
class="ui active inverted dimmer"
|
|
||||||
>
|
|
||||||
<div class="ui loader" />
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-else-if="!result || result?.results.length === 0 && !needsRefresh"
|
|
||||||
class="ui placeholder segment"
|
|
||||||
>
|
|
||||||
<div class="ui icon header">
|
|
||||||
<i class="upload icon" />
|
|
||||||
{{ t('views.content.libraries.FilesTable.empty.noTracks') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<action-table
|
|
||||||
v-else
|
|
||||||
:id-field="'uuid'"
|
|
||||||
:objects-data="result"
|
|
||||||
:custom-objects="customObjects"
|
|
||||||
:actions="actions"
|
|
||||||
:refreshable="true"
|
|
||||||
:needs-refresh="needsRefresh"
|
|
||||||
:action-url="'uploads/action/'"
|
|
||||||
:filters="actionFilters"
|
|
||||||
@action-launched="fetchData"
|
|
||||||
@refresh="fetchData"
|
|
||||||
>
|
|
||||||
<template #header-cells>
|
|
||||||
<th>
|
|
||||||
{{ t('views.content.libraries.FilesTable.table.file.header.title') }}
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
{{ t('views.content.libraries.FilesTable.table.file.header.artist') }}
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
{{ t('views.content.libraries.FilesTable.table.file.header.album') }}
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
{{ t('views.content.libraries.FilesTable.table.file.header.uploadDate') }}
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
{{ t('views.content.libraries.FilesTable.table.file.header.importStatus') }}
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
{{ t('views.content.libraries.FilesTable.table.file.header.duration') }}
|
|
||||||
</th>
|
|
||||||
<th>
|
|
||||||
{{ t('views.content.libraries.FilesTable.table.file.header.size') }}
|
|
||||||
</th>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
#row-cells="scope"
|
|
||||||
>
|
>
|
||||||
<template v-if="scope.obj.track">
|
<option value>
|
||||||
<td>
|
{{ t('views.content.libraries.FilesTable.option.status.all') }}
|
||||||
<router-link :to="{name: 'library.tracks.detail', params: {id: scope.obj.track.id }}">
|
</option>
|
||||||
{{ truncate(scope.obj.track.title, 25) }}
|
<option value="draft">
|
||||||
</router-link>
|
{{ t('views.content.libraries.FilesTable.option.status.draft') }}
|
||||||
</td>
|
</option>
|
||||||
<td>
|
<option value="pending">
|
||||||
<a
|
{{ t('views.content.libraries.FilesTable.option.status.pending') }}
|
||||||
href=""
|
</option>
|
||||||
class="discrete link"
|
<option value="skipped">
|
||||||
@click.prevent="addSearchToken('artist', scope.obj.track.artist.name)"
|
{{ t('views.content.libraries.FilesTable.option.status.skipped') }}
|
||||||
>{{ truncate(scope.obj.track.artist.name, 20) }}</a>
|
</option>
|
||||||
</td>
|
<option value="errored">
|
||||||
<td>
|
{{ t('views.content.libraries.FilesTable.option.status.failed') }}
|
||||||
<a
|
</option>
|
||||||
v-if="scope.obj.track.album"
|
<option value="finished">
|
||||||
href=""
|
{{ t('views.content.libraries.FilesTable.option.status.finished') }}
|
||||||
class="discrete link"
|
</option>
|
||||||
@click.prevent="addSearchToken('album', scope.obj.track.album.title)"
|
</select>
|
||||||
>{{ truncate(scope.obj.track.album.title, 20) }}</a>
|
</div>
|
||||||
</td>
|
<div class="field">
|
||||||
</template>
|
<label for="ordering-select">
|
||||||
<template v-else>
|
{{ t('views.content.libraries.FilesTable.ordering.label') }}
|
||||||
<td :title="scope.obj.source">
|
</label>
|
||||||
{{ truncate(scope.obj.source, 25) }}
|
<select
|
||||||
</td>
|
id="ordering-select"
|
||||||
<td />
|
v-model="ordering"
|
||||||
<td />
|
class="ui dropdown"
|
||||||
</template>
|
>
|
||||||
<td>
|
<option
|
||||||
<human-date :date="scope.obj.creation_date" />
|
v-for="(option, key) in orderingOptions"
|
||||||
</td>
|
:key="key"
|
||||||
<td>
|
:value="option[0]"
|
||||||
<a
|
>
|
||||||
href=""
|
{{ sharedLabels.filters[option[1]] }}
|
||||||
class="discrete link"
|
</option>
|
||||||
:title="getImportStatusChoice(scope.obj.import_status).help"
|
</select>
|
||||||
@click.prevent="addSearchToken('status', scope.obj.import_status)"
|
</div>
|
||||||
>{{ getImportStatusChoice(scope.obj.import_status).label }}</a>
|
<div class="field">
|
||||||
<button
|
<label for="ordering-direction">
|
||||||
class="ui tiny basic icon button"
|
{{ t('views.content.libraries.FilesTable.ordering.direction.label') }}
|
||||||
:title="sharedLabels.fields.import_status.label"
|
</label>
|
||||||
:aria-label="labels.showStatus"
|
<select
|
||||||
@click="detailedUpload = scope.obj; showUploadDetailModal = true"
|
id="ordering-direction"
|
||||||
>
|
v-model="orderingDirection"
|
||||||
<i class="question circle outline icon" />
|
class="ui dropdown"
|
||||||
</button>
|
>
|
||||||
</td>
|
<option value="+">
|
||||||
<td v-if="scope.obj.duration">
|
{{ t('views.content.libraries.FilesTable.ordering.direction.ascending') }}
|
||||||
{{ time.parse(scope.obj.duration) }}
|
</option>
|
||||||
</td>
|
<option value="-">
|
||||||
<td v-else>
|
{{ t('views.content.libraries.FilesTable.ordering.direction.descending') }}
|
||||||
{{ t('views.content.libraries.FilesTable.notApplicable') }}
|
</option>
|
||||||
</td>
|
</select>
|
||||||
<td v-if="scope.obj.size">
|
</div>
|
||||||
{{ humanSize(scope.obj.size) }}
|
|
||||||
</td>
|
|
||||||
<td v-else>
|
|
||||||
{{ t('views.content.libraries.FilesTable.notApplicable') }}
|
|
||||||
</td>
|
|
||||||
</template>
|
|
||||||
</action-table>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<pagination
|
|
||||||
v-if="result && result.count > paginateBy"
|
|
||||||
v-model:current="page"
|
|
||||||
:compact="true"
|
|
||||||
:paginate-by="paginateBy"
|
|
||||||
:total="result.count"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span v-if="result && result.results.length > 0">
|
|
||||||
{{ t('views.content.libraries.FilesTable.pagination.results', {start: ((page-1) * paginateBy) + 1, end: ((page-1) * paginateBy) + result.results.length, total: result.count}) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Layout>
|
||||||
|
<import-status-modal
|
||||||
|
v-if="detailedUpload"
|
||||||
|
v-model:show="showUploadDetailModal"
|
||||||
|
:upload="detailedUpload"
|
||||||
|
/>
|
||||||
|
<Loader v-if="isLoading" />
|
||||||
|
<Alert
|
||||||
|
v-else-if="!result || result?.results.length === 0 && !needsRefresh"
|
||||||
|
blue
|
||||||
|
align-text="center"
|
||||||
|
>
|
||||||
|
<i class="bi bi-upload" />
|
||||||
|
{{ t('views.content.libraries.FilesTable.empty.noTracks') }}
|
||||||
|
</Alert>
|
||||||
|
<action-table
|
||||||
|
v-else
|
||||||
|
:id-field="'uuid'"
|
||||||
|
:objects-data="result"
|
||||||
|
:custom-objects="customObjects"
|
||||||
|
:actions="actions"
|
||||||
|
:refreshable="true"
|
||||||
|
:needs-refresh="needsRefresh"
|
||||||
|
:action-url="'uploads/action/'"
|
||||||
|
:filters="actionFilters"
|
||||||
|
@action-launched="fetchData"
|
||||||
|
@refresh="fetchData"
|
||||||
|
>
|
||||||
|
<template #header-cells>
|
||||||
|
<th>
|
||||||
|
{{ t('views.content.libraries.FilesTable.table.file.header.title') }}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{{ t('views.content.libraries.FilesTable.table.file.header.artist') }}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{{ t('views.content.libraries.FilesTable.table.file.header.album') }}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{{ t('views.content.libraries.FilesTable.table.file.header.uploadDate') }}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{{ t('views.content.libraries.FilesTable.table.file.header.importStatus') }}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{{ t('views.content.libraries.FilesTable.table.file.header.duration') }}
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
{{ t('views.content.libraries.FilesTable.table.file.header.size') }}
|
||||||
|
</th>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
#row-cells="scope"
|
||||||
|
>
|
||||||
|
<template v-if="scope.obj.track">
|
||||||
|
<td>
|
||||||
|
<router-link :to="{name: 'library.tracks.detail', params: {id: scope.obj.track.id }}">
|
||||||
|
{{ truncate(scope.obj.track.title, 25) }}
|
||||||
|
</router-link>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
class="discrete link"
|
||||||
|
@click.prevent="addSearchToken('artist', scope.obj.track.artist.name)"
|
||||||
|
>{{ truncate(scope.obj.track.artist.name, 20) }}</a>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
v-if="scope.obj.track.album"
|
||||||
|
href=""
|
||||||
|
class="discrete link"
|
||||||
|
@click.prevent="addSearchToken('album', scope.obj.track.album.title)"
|
||||||
|
>{{ truncate(scope.obj.track.album.title, 20) }}</a>
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<td :title="scope.obj.source">
|
||||||
|
{{ truncate(scope.obj.source, 25) }}
|
||||||
|
</td>
|
||||||
|
<td />
|
||||||
|
<td />
|
||||||
|
</template>
|
||||||
|
<td>
|
||||||
|
<human-date :date="scope.obj.creation_date" />
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<a
|
||||||
|
href=""
|
||||||
|
class="discrete link"
|
||||||
|
:title="getImportStatusChoice(scope.obj.import_status).help"
|
||||||
|
@click.prevent="addSearchToken('status', scope.obj.import_status)"
|
||||||
|
>{{ getImportStatusChoice(scope.obj.import_status).label }}</a>
|
||||||
|
<Button
|
||||||
|
secondary
|
||||||
|
:title="sharedLabels.fields.import_status.label"
|
||||||
|
:aria-label="labels.showStatus"
|
||||||
|
icon="bi-question-circle-fill"
|
||||||
|
@click="detailedUpload = scope.obj; showUploadDetailModal = true"
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
<td v-if="scope.obj.duration">
|
||||||
|
{{ time.parse(scope.obj.duration) }}
|
||||||
|
</td>
|
||||||
|
<td v-else>
|
||||||
|
{{ t('views.content.libraries.FilesTable.notApplicable') }}
|
||||||
|
</td>
|
||||||
|
<td v-if="scope.obj.size">
|
||||||
|
{{ humanSize(scope.obj.size) }}
|
||||||
|
</td>
|
||||||
|
<td v-else>
|
||||||
|
{{ t('views.content.libraries.FilesTable.notApplicable') }}
|
||||||
|
</td>
|
||||||
|
</template>
|
||||||
|
</action-table>
|
||||||
|
<div>
|
||||||
|
<Pagination
|
||||||
|
v-if="result && result.count > paginateBy"
|
||||||
|
v-model:page="page"
|
||||||
|
:pages="Math.ceil(result.count / paginateBy)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
Loading…
Reference in New Issue