feat(front): #2081 new upload process

This commit is contained in:
ArneBo 2025-02-11 10:31:42 +01:00
parent 0caee2181d
commit 3cd7548cf0
6 changed files with 603 additions and 619 deletions

View File

@ -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" />&nbsp;
{{ 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" />&nbsp;
{{ 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>

View File

@ -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') }}&nbsp;
<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" />&nbsp;
{{ 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') }}&nbsp;
<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>

View File

@ -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"

View File

@ -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>

View File

@ -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>

View File

@ -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>