feat: add missing upload views
This commit is contained in:
parent
f69e08b2a9
commit
443a535758
|
@ -0,0 +1,183 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { UploadGroup } from '~/ui/stores/upload'
|
||||
import VerticalCollapse from '~/ui/components/VerticalCollapse.vue'
|
||||
import UploadList from '~/ui/components/UploadList.vue'
|
||||
import { UseTimeAgo } from '@vueuse/components'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
|
||||
defineProps<{ groups: UploadGroup[], isUploading?: boolean }>()
|
||||
|
||||
const openUploadGroup = ref<UploadGroup>()
|
||||
const toggle = (group: UploadGroup) => {
|
||||
openUploadGroup.value = openUploadGroup.value === group
|
||||
? undefined
|
||||
: group
|
||||
}
|
||||
|
||||
const labels = {
|
||||
'music-library': 'Music library',
|
||||
'music-channel': 'Music channel',
|
||||
'podcast-channel': 'Podcast channel',
|
||||
}
|
||||
|
||||
const getDescription = (group: UploadGroup) => {
|
||||
if (group.queue.length === 0) return 'Unknown album'
|
||||
|
||||
return group.queue.reduce((acc, { metadata }) => {
|
||||
if (!metadata) return acc
|
||||
|
||||
let element = group.type === 'music-library'
|
||||
? metadata.tags.album
|
||||
: metadata.tags.title
|
||||
|
||||
element = acc.length < 3
|
||||
? element
|
||||
: '...'
|
||||
|
||||
if (!acc.includes(element)) {
|
||||
acc.push(element)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [] as string[]).join(', ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="upload-group"
|
||||
v-for="group of groups"
|
||||
:key="group.guid"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="upload-group-header">
|
||||
<div class="upload-group-title">{{ labels[group.type] }}</div>
|
||||
<div class="upload-group-albums">{{ getDescription(group) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="timeago">
|
||||
<UseTimeAgo :time="group.createdAt" v-slot="{ timeAgo }">{{ timeAgo }}</UseTimeAgo>
|
||||
</div>
|
||||
|
||||
|
||||
<FwPill v-if="group.failedCount > 0" color="red">
|
||||
<template #image>
|
||||
<div class="flex items-center justify-center">{{ group.failedCount }}</div>
|
||||
</template>
|
||||
failed
|
||||
</FwPill>
|
||||
|
||||
<FwPill v-if="group.importedCount > 0" color="blue">
|
||||
<template #image>
|
||||
<div class="flex items-center justify-center">{{ group.importedCount }}</div>
|
||||
</template>
|
||||
imported
|
||||
</FwPill>
|
||||
|
||||
<FwPill v-if="group.processingCount > 0" color="secondary">
|
||||
<template #image>
|
||||
<div class="flex items-center justify-center">{{ group.processingCount }}</div>
|
||||
</template>
|
||||
processing
|
||||
</FwPill>
|
||||
|
||||
|
||||
<FwButton
|
||||
@click="toggle(group)"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
class="icon-only"
|
||||
>
|
||||
<template #icon>
|
||||
<Icon icon="bi:chevron-right" :rotate="group === openUploadGroup ? 1 : 0" />
|
||||
</template>
|
||||
</FwButton>
|
||||
</div>
|
||||
|
||||
<div v-if="isUploading" class="flex items-center upload-progress">
|
||||
<FwButton v-if="group.processingCount === 0 && group.failedCount > 0" @click="group.retry()" color="secondary">Retry</FwButton>
|
||||
<FwButton v-else-if="group.queue.length !== group.importedCount" @click="group.cancel()" color="secondary">Interrupt</FwButton>
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-bar" :style="{ width: `${group.progress}%` }" />
|
||||
</div>
|
||||
|
||||
<div class="shrink-0">
|
||||
{{ group.importedCount }} / {{ group.queue.length }} files imported
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VerticalCollapse @click.stop :open="openUploadGroup === group" class="collapse">
|
||||
<UploadList :uploads="group.queue" />
|
||||
</VerticalCollapse>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.upload-group {
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid var(--fw-gray-200);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.upload-group-header {
|
||||
.upload-group-title {
|
||||
color: var(--fw-gray-960);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.upload-group-albums {
|
||||
color: var(--fw-gray-960);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeago {
|
||||
margin-left: auto;
|
||||
margin-right: 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--fw-gray-600);
|
||||
}
|
||||
|
||||
|
||||
.upload-progress {
|
||||
font-size: 0.875rem;
|
||||
color: var(--fw-gray-600);
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
> :deep(.funkwhale.button) {
|
||||
margin: 0rem;
|
||||
}
|
||||
|
||||
> :deep(.funkwhale.button) + .progress {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 100%;
|
||||
height: 0.5rem;
|
||||
background-color: var(--fw-gray-200);
|
||||
border-radius: 1rem;
|
||||
margin: 0 1rem 0 0;
|
||||
position: relative;
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--fw-primary);
|
||||
border-radius: 1rem;
|
||||
width: 0;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
</style>
|
|
@ -3,6 +3,7 @@ import type { UploadGroupEntry } from '~/ui/stores/upload'
|
|||
import { bytesToHumanSize } from '~/ui/composables/bytes'
|
||||
import { UseTimeAgo } from '@vueuse/components'
|
||||
import CoverArt from '~/ui/components/CoverArt.vue'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
defineProps<{
|
||||
uploads: UploadGroupEntry[]
|
||||
|
@ -25,15 +26,22 @@ defineProps<{
|
|||
</div>
|
||||
</Transition>
|
||||
<div class="upload-state">
|
||||
<FwPill :color="track.failReason ? 'red' : track.importedAt ? 'blue' : 'secondary'">
|
||||
<FwTooltip v-if="track.failReason" :tooltip="track.failReason">
|
||||
<FwPill color="red">
|
||||
<template #image>
|
||||
<Icon icon="bi:question" class="h-4 w-4" />
|
||||
</template>
|
||||
|
||||
failed
|
||||
</FwPill>
|
||||
</FwTooltip>
|
||||
<FwPill v-else :color="track.importedAt ? 'blue' : 'secondary'">
|
||||
{{
|
||||
track.failReason
|
||||
? 'failed'
|
||||
: track.importedAt
|
||||
? 'imported'
|
||||
: track.progress === 100
|
||||
? 'processing'
|
||||
: 'uploading'
|
||||
track.importedAt
|
||||
? 'imported'
|
||||
: track.progress === 100
|
||||
? 'processing'
|
||||
: 'uploading'
|
||||
}}
|
||||
</FwPill>
|
||||
<div v-if="track.importedAt" class="track-timeago">
|
||||
|
|
|
@ -1,13 +1,50 @@
|
|||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue'
|
||||
import type { UploadGroupEntry } from '~/ui/stores/upload';
|
||||
import { computed } from 'vue';
|
||||
import { bytesToHumanSize } from '~/ui/composables/bytes';
|
||||
import { useUploadsStore, type UploadGroupEntry } from '~/ui/stores/upload';
|
||||
import CoverArt from '~/ui/components/CoverArt.vue'
|
||||
|
||||
const allTracks: UploadGroupEntry[] = [
|
||||
|
||||
]
|
||||
interface Recording {
|
||||
guid: string
|
||||
title: string
|
||||
artist: string
|
||||
album: string
|
||||
uploadDate: Date
|
||||
format: string
|
||||
size: string
|
||||
metadata: UploadGroupEntry['metadata']
|
||||
}
|
||||
|
||||
const intl = new Intl.DateTimeFormat('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})
|
||||
|
||||
// TODO: Fetch tracks from server
|
||||
const uploads = useUploadsStore()
|
||||
const allTracks = computed<Recording[]>(() => {
|
||||
return uploads.uploadGroups.flatMap(group => group.queue.map<Recording>((entry) => ({
|
||||
guid: entry.id,
|
||||
title: entry.metadata?.tags.title || 'Unknown title',
|
||||
artist: entry.metadata?.tags.artist || 'Unknown artist',
|
||||
album: entry.metadata?.tags.album || 'Unknown album',
|
||||
uploadDate: group.createdAt,
|
||||
format: 'flac',
|
||||
size: bytesToHumanSize(entry.file.size),
|
||||
metadata: entry.metadata
|
||||
})))
|
||||
})
|
||||
|
||||
const columns = [
|
||||
{ key: '', label: '' }
|
||||
{ key: '>index', label: '#' },
|
||||
{ key: 'title', label: 'Title' },
|
||||
{ key: 'artist', label: 'Artist' },
|
||||
{ key: 'album', label: 'Album' },
|
||||
{ key: 'uploadDate', label: 'Upload date' },
|
||||
{ key: 'format', label: 'Format' },
|
||||
{ key: 'size', label: 'Size' }
|
||||
]
|
||||
</script>
|
||||
|
||||
|
@ -18,8 +55,20 @@ const columns = [
|
|||
<h3>There is no file in your library</h3>
|
||||
<p>Try uploading some before coming back here!</p>
|
||||
</div>
|
||||
<FwTable :rows="allTracks">
|
||||
|
||||
<FwTable v-else
|
||||
id-key="guid"
|
||||
:columns="columns"
|
||||
:rows="allTracks"
|
||||
>
|
||||
<template #col-title="{ row, value }">
|
||||
<div class="flex items-center">
|
||||
<CoverArt :src="row.metadata" class="mr-2" />
|
||||
{{ value }}
|
||||
</div>
|
||||
</template>
|
||||
<template #col-upload-date="{ value }">
|
||||
{{ intl.format(value) }}
|
||||
</template>
|
||||
</FwTable>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -1,3 +1,12 @@
|
|||
<script setup lang="ts">
|
||||
import UploadGroupList from '~/ui/components/UploadGroupList.vue'
|
||||
import { useUploadsStore } from '~/ui/stores/upload'
|
||||
|
||||
// TODO: Fetch upload history from server
|
||||
const uploads = useUploadsStore()
|
||||
const history = uploads.uploadGroups
|
||||
</script>
|
||||
|
||||
<template>
|
||||
history
|
||||
<UploadGroupList :groups="history" />
|
||||
</template>
|
||||
|
|
|
@ -1,182 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { UploadGroup, useUploadsStore } from '~/ui/stores/upload'
|
||||
import VerticalCollapse from '~/ui/components/VerticalCollapse.vue'
|
||||
import UploadList from '~/ui/components/UploadList.vue'
|
||||
import { UseTimeAgo } from '@vueuse/components'
|
||||
import { Icon } from '@iconify/vue'
|
||||
|
||||
import UploadGroupList from '~/ui/components/UploadGroupList.vue'
|
||||
import { useUploadsStore } from '~/ui/stores/upload'
|
||||
const uploads = useUploadsStore()
|
||||
|
||||
const openUploadGroup = ref<UploadGroup>()
|
||||
const toggle = (group: UploadGroup) => {
|
||||
openUploadGroup.value = openUploadGroup.value === group
|
||||
? undefined
|
||||
: group
|
||||
}
|
||||
|
||||
const labels = {
|
||||
'music-library': 'Music library',
|
||||
'music-channel': 'Music channel',
|
||||
'podcast-channel': 'Podcast channel',
|
||||
}
|
||||
|
||||
const getDescription = (group: UploadGroup) => {
|
||||
if (group.queue.length === 0) return 'Unknown album'
|
||||
|
||||
return group.queue.reduce((acc, { metadata }) => {
|
||||
if (!metadata) return acc
|
||||
|
||||
let element = group.type === 'music-library'
|
||||
? metadata.tags.album
|
||||
: metadata.tags.title
|
||||
|
||||
element = acc.length < 3
|
||||
? element
|
||||
: '...'
|
||||
|
||||
if (!acc.includes(element)) {
|
||||
acc.push(element)
|
||||
}
|
||||
|
||||
return acc
|
||||
}, [] as string[]).join(', ')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="upload-group"
|
||||
v-for="group of uploads.uploadGroups"
|
||||
:key="group.guid"
|
||||
>
|
||||
<div class="flex items-center">
|
||||
<div class="upload-group-header">
|
||||
<div class="upload-group-title">{{ labels[group.type] }}</div>
|
||||
<div class="upload-group-albums">{{ getDescription(group) }}</div>
|
||||
</div>
|
||||
|
||||
<div class="timeago">
|
||||
<UseTimeAgo :time="group.createdAt" v-slot="{ timeAgo }">{{ timeAgo }}</UseTimeAgo>
|
||||
</div>
|
||||
|
||||
|
||||
<FwPill v-if="group.failedCount > 0" color="red">
|
||||
<template #image>
|
||||
<div class="flex items-center justify-center">{{ group.failedCount }}</div>
|
||||
</template>
|
||||
failed
|
||||
</FwPill>
|
||||
|
||||
<FwPill v-if="group.importedCount > 0" color="blue">
|
||||
<template #image>
|
||||
<div class="flex items-center justify-center">{{ group.importedCount }}</div>
|
||||
</template>
|
||||
imported
|
||||
</FwPill>
|
||||
|
||||
<FwPill v-if="group.processingCount > 0" color="secondary">
|
||||
<template #image>
|
||||
<div class="flex items-center justify-center">{{ group.processingCount }}</div>
|
||||
</template>
|
||||
processing
|
||||
</FwPill>
|
||||
|
||||
|
||||
<FwButton
|
||||
@click="toggle(group)"
|
||||
variant="ghost"
|
||||
color="secondary"
|
||||
class="icon-only"
|
||||
>
|
||||
<template #icon>
|
||||
<Icon icon="bi:chevron-right" :rotate="group === openUploadGroup ? 1 : 0" />
|
||||
</template>
|
||||
</FwButton>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center upload-progress">
|
||||
<FwButton v-if="group.processingCount === 0 && group.failedCount > 0" @click="group.retry()" color="secondary">Retry</FwButton>
|
||||
<FwButton v-else-if="group.queue.length !== group.importedCount" @click="group.cancel()" color="secondary">Interrupt</FwButton>
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-bar" :style="{ width: `${group.progress}%` }" />
|
||||
</div>
|
||||
|
||||
<div class="shrink-0">
|
||||
{{ group.importedCount }} / {{ group.queue.length }} files imported
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<VerticalCollapse @click.stop :open="openUploadGroup === group" class="collapse">
|
||||
<UploadList :uploads="group.queue" />
|
||||
</VerticalCollapse>
|
||||
</div>
|
||||
</div>
|
||||
<UploadGroupList :groups="uploads.uploadGroups" :is-uploading="true" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.upload-group {
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid var(--fw-gray-200);
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.upload-group-header {
|
||||
.upload-group-title {
|
||||
color: var(--fw-gray-960);
|
||||
font-size: 0.9375rem;
|
||||
}
|
||||
|
||||
.upload-group-albums {
|
||||
color: var(--fw-gray-960);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.timeago {
|
||||
margin-left: auto;
|
||||
margin-right: 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--fw-gray-600);
|
||||
}
|
||||
|
||||
|
||||
.upload-progress {
|
||||
font-size: 0.875rem;
|
||||
color: var(--fw-gray-600);
|
||||
padding-top: 0.5rem;
|
||||
padding-bottom: 1rem;
|
||||
|
||||
> :deep(.funkwhale.button) {
|
||||
margin: 0rem;
|
||||
}
|
||||
|
||||
> :deep(.funkwhale.button) + .progress {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.progress {
|
||||
width: 100%;
|
||||
height: 0.5rem;
|
||||
background-color: var(--fw-gray-200);
|
||||
border-radius: 1rem;
|
||||
margin: 0 1rem 0 0;
|
||||
position: relative;
|
||||
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--fw-primary);
|
||||
border-radius: 1rem;
|
||||
width: 0;
|
||||
transition: width 0.2s ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.collapse {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in New Issue