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 Alert from '~/components/ui/Alert.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Loader from '~/components/ui/Loader.vue'
|
||||
|
||||
|
||||
interface Events {
|
||||
|
@ -485,20 +483,17 @@ const createEmptyChannel = async () => {
|
|||
console.log("Error:", error)
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<template>
|
||||
<form
|
||||
:class="['ui', { loading: availableChannels.loading }, 'form component-file-upload']"
|
||||
@submit.stop.prevent
|
||||
>
|
||||
<!-- Error message -->
|
||||
|
||||
<div
|
||||
<Alert
|
||||
v-if="errors.length > 0"
|
||||
role="alert"
|
||||
class="ui negative message"
|
||||
>
|
||||
<h4 class="header">
|
||||
{{ t('components.channels.UploadForm.header.error') }}
|
||||
|
@ -511,14 +506,21 @@ const createEmptyChannel = async () => {
|
|||
{{ error }}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
</Alert>
|
||||
<!-- Select Album and License -->
|
||||
|
||||
<div :class="['ui', 'required', 'field']">
|
||||
<label for="channel-dropdown">
|
||||
{{ t('components.channels.UploadForm.label.channel') }}: {{ selectedChannel?.artist.name }}
|
||||
</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>
|
||||
<album-select
|
||||
v-model.number="values.album"
|
||||
|
@ -539,43 +541,37 @@ const createEmptyChannel = async () => {
|
|||
</div>
|
||||
|
||||
<!-- Files to upload -->
|
||||
|
||||
<div
|
||||
v-if="remainingSpace === 0"
|
||||
role="alert"
|
||||
class="ui warning message"
|
||||
>
|
||||
<div class="content">
|
||||
<p>
|
||||
<i class="warning icon" />
|
||||
{{ t('components.channels.UploadForm.warning.quota') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="remainingSpace === 0">
|
||||
<Alert
|
||||
red
|
||||
>
|
||||
<i class="bi bi-exclamation-triangle" />
|
||||
{{ t('components.channels.UploadForm.warning.quota') }}
|
||||
</Alert>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div
|
||||
<Alert
|
||||
v-if="draftUploads?.length > 0 && includeDraftUploads === undefined"
|
||||
class="ui visible info message"
|
||||
blue
|
||||
>
|
||||
<p>
|
||||
<i class="redo icon" />
|
||||
<i class="bi bi-circle-clockwise" />
|
||||
{{ t('components.channels.UploadForm.message.pending') }}
|
||||
</p>
|
||||
<button
|
||||
class="ui basic button"
|
||||
<Button
|
||||
@click.stop.prevent="includeDraftUploads = false"
|
||||
>
|
||||
{{ t('components.channels.UploadForm.button.ignore') }}
|
||||
</button>
|
||||
<button
|
||||
class="ui basic button"
|
||||
</Button>
|
||||
<Button
|
||||
@click.stop.prevent="includeDraftUploads = true"
|
||||
>
|
||||
{{ t('components.channels.UploadForm.button.resume') }}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
</Button>
|
||||
</Alert>
|
||||
<Alert
|
||||
v-if="uploadedFiles.length > 0"
|
||||
v-bind="{[ uploadedFiles.some(file=>file.error) ? 'red' : 'green' ]:true}"
|
||||
>
|
||||
<div
|
||||
v-for="file in uploadedFiles"
|
||||
|
@ -583,27 +579,22 @@ const createEmptyChannel = async () => {
|
|||
class="channel-file"
|
||||
>
|
||||
<div class="content">
|
||||
<div
|
||||
<Button
|
||||
v-if="file.response?.uuid"
|
||||
role="button"
|
||||
icon="bi-pencil-fill"
|
||||
class="ui basic icon button"
|
||||
:title="labels.editTitle"
|
||||
@click.stop.prevent="selectedUploadId = file.response?.uuid"
|
||||
>
|
||||
<i class="pencil icon" />
|
||||
</div>
|
||||
/>
|
||||
<div
|
||||
v-if="file.error"
|
||||
class="ui basic danger icon label"
|
||||
:title="file.error.toString()"
|
||||
@click.stop.prevent="selectedUploadId = file.response?.uuid"
|
||||
>
|
||||
<i class="warning sign icon" />
|
||||
<i class="bi bi-exclamation-triangle-fill" />
|
||||
</div>
|
||||
<div
|
||||
v-else-if="file.active && !file.response"
|
||||
class="ui active slow inline loader"
|
||||
/>
|
||||
<Loader v-else-if="file.active && !file.response" />
|
||||
</div>
|
||||
<h4 class="ui header">
|
||||
<template v-if="file.metadata.title">
|
||||
|
@ -649,39 +640,37 @@ const createEmptyChannel = async () => {
|
|||
</div>
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
<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" />
|
||||
</Alert>
|
||||
</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>
|
||||
</template>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<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 { computed, ref, reactive, watch, nextTick } from 'vue'
|
||||
|
@ -19,6 +19,11 @@ import FsLogs from './FsLogs.vue'
|
|||
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
||||
import updateQueryString from '~/composables/updateQueryString'
|
||||
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 {
|
||||
(e: 'uploads-finished', delta: number):void
|
||||
|
@ -158,16 +163,6 @@ const sortedFiles = computed(() => {
|
|||
|
||||
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
|
||||
//
|
||||
|
@ -303,281 +298,294 @@ useEventListener(window, 'beforeunload', (event) => {
|
|||
event.preventDefault()
|
||||
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>
|
||||
|
||||
<template>
|
||||
<div class="component-file-upload">
|
||||
<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="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 :class="{loading: isLoadingQuota}">
|
||||
<div :class="['ui', {red: remainingSpace === 0}, {warning: remainingSpace > 0 && remainingSpace <= 50}, 'small', 'statistic']">
|
||||
<div class="label">
|
||||
{{ t('components.library.FileUpload.label.remainingSpace') }}
|
||||
</div>
|
||||
<div
|
||||
v-if="files.length > 0"
|
||||
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 class="value">
|
||||
{{ humanSize(remainingSpace * 1000 * 1000) }}
|
||||
</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>
|
||||
<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>
|
||||
|
||||
<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
|
||||
ref="upload"
|
||||
v-bind="$attrs"
|
||||
:post-action="store.getters['instance/absoluteUrl']('/api/v1/uploads/')"
|
||||
:post-action="store.getters['instance/absoluteUrl']('/api/v2/uploads/')"
|
||||
:multiple="true"
|
||||
:thread="1"
|
||||
:custom-action="uploadAction"
|
||||
|
|
|
@ -2,9 +2,12 @@
|
|||
import type { FileSystem, FSEntry } from '~/types'
|
||||
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Input from '~/components/ui/Input.vue'
|
||||
|
||||
|
||||
interface Events {
|
||||
|
@ -34,23 +37,23 @@ const handleClick = (entry: FSEntry) => {
|
|||
|
||||
value.value.push(entry.name)
|
||||
}
|
||||
const path = computed (() => props.data.root + '/' + value.value.join('/'))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="['ui', { loading }, 'segment']">
|
||||
<div class="ui fluid action input">
|
||||
<input
|
||||
class="ui disabled"
|
||||
<div :class="['ui', { loading }]">
|
||||
<Layout flex>
|
||||
<Input
|
||||
v-model="path"
|
||||
disabled
|
||||
:value="props.data.root + '/' + value.join('/')"
|
||||
>
|
||||
/>
|
||||
<Button
|
||||
class="ui button"
|
||||
primary
|
||||
@click.prevent="emit('import')"
|
||||
>
|
||||
{{ t('components.library.FsBrowser.button.import') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Layout>
|
||||
<div class="ui list component-fs-browser">
|
||||
<a
|
||||
v-if="value.length > 0"
|
||||
|
@ -58,7 +61,7 @@ const handleClick = (entry: FSEntry) => {
|
|||
href=""
|
||||
@click.prevent="handleClick({ name: '..', dir: true })"
|
||||
>
|
||||
<i class="folder icon" />
|
||||
<i class="bi bi-folder" />
|
||||
<div class="content">
|
||||
<div class="header doubledot symbol" />
|
||||
</div>
|
||||
|
@ -72,11 +75,11 @@ const handleClick = (entry: FSEntry) => {
|
|||
>
|
||||
<i
|
||||
v-if="e.dir"
|
||||
class="folder icon"
|
||||
class="bi bi-folder"
|
||||
/>
|
||||
<i
|
||||
v-else
|
||||
class="file icon"
|
||||
class="bi bi-file-earmark-music-fill"
|
||||
/>
|
||||
<div class="content">
|
||||
<div class="header">{{ e.name }}</div>
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
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 Modal from '~/components/ui/Modal.vue'
|
||||
import Button from '~/components/ui/Button.vue'
|
||||
import Input from '~/components/ui/Input.vue'
|
||||
import Layout from '~/components/ui/Layout.vue'
|
||||
import Spacer from '~/components/ui/Spacer.vue'
|
||||
import Alert from '~/components/ui/Alert.vue';
|
||||
import Card from '~/components/ui/Card.vue';
|
||||
import Pagination from '~/components/ui/Pagination.vue';
|
||||
import Alert from '~/components/ui/Alert.vue'
|
||||
import Card from '~/components/ui/Card.vue'
|
||||
|
||||
import type { Actor, Channel } from '~/types';
|
||||
import FileUploadWidget from '~/components/library/FileUploadWidget.vue';
|
||||
import type { VueUploadItem } from 'vue-upload-component';
|
||||
import ChannelUpload from '~/components/channels/UploadForm.vue';
|
||||
import LibraryUpload from '~/components/library/FileUpload.vue';
|
||||
import type { Actor, Channel } from '~/types'
|
||||
import FileUploadWidget from '~/components/library/FileUploadWidget.vue'
|
||||
import type { VueUploadItem } from 'vue-upload-component'
|
||||
import ChannelUpload from '~/components/channels/UploadForm.vue'
|
||||
import LibraryUpload from '~/components/library/FileUpload.vue'
|
||||
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
@ -75,20 +73,14 @@ const channelUpload = ref();
|
|||
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 1 -->
|
||||
|
||||
<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"
|
||||
@click="destinationSelected('channel')"
|
||||
>
|
||||
|
@ -97,7 +89,10 @@ const channelUpload = ref();
|
|||
</template>
|
||||
{{ "Publish music you make" /* TODO: Translate */ }}
|
||||
</Card>
|
||||
<Card small title="Podcast"
|
||||
<Card
|
||||
small
|
||||
solid
|
||||
title="Podcast"
|
||||
icon="bi-upload primary solid"
|
||||
@click="destinationSelected('podcast')"
|
||||
>
|
||||
|
@ -106,7 +101,10 @@ const channelUpload = ref();
|
|||
</template>
|
||||
{{ "Publish podcasts you make" /* TODO: Translate */ }}
|
||||
</Card>
|
||||
<Card small title="Mix & Share"
|
||||
<Card
|
||||
small
|
||||
solid
|
||||
title="Mix & Share"
|
||||
icon="bi-upload"
|
||||
@click="destinationSelected('library')"
|
||||
>
|
||||
|
@ -128,9 +126,9 @@ const channelUpload = ref();
|
|||
<!-- -->
|
||||
|
||||
<!-- Privacy Slider -->
|
||||
<!-- <LibraryUpload v-if="state.uploadDestination === 'library'"
|
||||
<LibraryUpload v-if="state.uploadDestination === 'library'"
|
||||
:library="{uuid: 'string'} /* Get corresponding library from user */">
|
||||
</LibraryUpload> -->
|
||||
</LibraryUpload>
|
||||
{{ state.files }}
|
||||
</Layout>
|
||||
|
||||
|
|
|
@ -12,16 +12,23 @@ import { useI18n } from 'vue-i18n'
|
|||
import time from '~/utils/time'
|
||||
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 useSmartSearch from '~/composables/navigation/useSmartSearch'
|
||||
import useOrdering from '~/composables/navigation/useOrdering'
|
||||
import useErrorHandler from '~/composables/useErrorHandler'
|
||||
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 {
|
||||
(e: 'fetch-start'): void
|
||||
}
|
||||
|
@ -133,226 +140,205 @@ const getImportStatusChoice = (importStatus: ImportStatus) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div class="ui inline form">
|
||||
<div class="fields">
|
||||
<div class="ui six wide field">
|
||||
<label for="files-search">
|
||||
{{ t('views.content.libraries.FilesTable.label.search') }}
|
||||
</label>
|
||||
<form @submit.prevent="query = search.value">
|
||||
<input
|
||||
id="files-search"
|
||||
ref="search"
|
||||
name="search"
|
||||
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>
|
||||
<Layout form>
|
||||
<div class="fields">
|
||||
<div class="ui six wide field">
|
||||
<form @submit.prevent="console.log(search);query = search">
|
||||
<Input
|
||||
search
|
||||
id="files-search"
|
||||
:label="t('views.content.libraries.FilesTable.label.search')"
|
||||
v-model="search"
|
||||
:placeholder="labels.searchPlaceholder"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<import-status-modal
|
||||
v-if="detailedUpload"
|
||||
v-model:show="showUploadDetailModal"
|
||||
:upload="detailedUpload"
|
||||
/>
|
||||
<div class="dimmable">
|
||||
<div
|
||||
v-if="isLoading"
|
||||
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"
|
||||
<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)"
|
||||
>
|
||||
<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
|
||||
class="ui tiny basic icon button"
|
||||
:title="sharedLabels.fields.import_status.label"
|
||||
:aria-label="labels.showStatus"
|
||||
@click="detailedUpload = scope.obj; showUploadDetailModal = true"
|
||||
>
|
||||
<i class="question circle outline icon" />
|
||||
</button>
|
||||
</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>
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
</template>
|
||||
|
|
Loading…
Reference in New Issue