diff --git a/front/src/ui/pages/upload.vue b/front/src/ui/pages/upload.vue index e5153eb76..8d0dd632e 100644 --- a/front/src/ui/pages/upload.vue +++ b/front/src/ui/pages/upload.vue @@ -1,6 +1,5 @@ - Upload - + Upload @@ -95,7 +91,7 @@ const processFiles = (fileList: FileList) => { Select a destination for your audio files: - + { /> - + - {{ files.length }} files, {{ combinedFileSize }} + {{ uploads.queue.length }} files, {{ combinedFileSize }} All @@ -212,7 +208,7 @@ const processFiles = (fileList: FileList) => { - Cancel + Cancel {{ uploads.queue.length ? 'Continue in background' : 'Save and close' }} @@ -228,35 +224,18 @@ h1 { font-family: Lato, sans-serif; } -.flex { - display: flex; - align-items: flex-start; +.flex:not(.flex-col) { + .funkwhale.button { + &:first-child { + margin-left: 0; + } - &:not(.flex-col) { - .funkwhale.button { - &:first-child { - margin-left: 0; - } - - &:last-child { - margin-right: 0; - } + &:last-child { + margin-right: 0; } } } -.items-center { - align-items: center; -} - -.space-between { - justify-content: space-between; -} - -.flex-spacer { - flex: 1; -} - .filesystem-stats { color: var(--fw-gray-700); > .flex { @@ -348,14 +327,6 @@ h1 { } } -.w-full { - width: 100%; -} - -.mr-4 { - margin-right: 1rem; -} - label { line-height: 1.2; font-weight: 900; @@ -467,6 +438,4 @@ label { border-color: transparent !important; } } - - diff --git a/front/src/ui/stores/upload.ts b/front/src/ui/stores/upload.ts index 507ceeabc..1ed5ed165 100644 --- a/front/src/ui/stores/upload.ts +++ b/front/src/ui/stores/upload.ts @@ -1,9 +1,11 @@ import { defineStore, acceptHMRUpdate } from 'pinia' -import { computed, reactive, readonly, ref, watchEffect, markRaw, toRaw } from 'vue' -import { whenever, useWebWorker } from '@vueuse/core' +import { computed, reactive, readonly, ref, watchEffect, markRaw, toRaw, type Ref } from 'vue' +import { whenever, useWebWorker, type UseWebWorkerReturn } from '@vueuse/core' +import { not } from '@vueuse/math' import axios from 'axios' -import FileMetadataParserWorker from '~/ui/workers/file-metadata-parser.ts?worker' + +import FileMetadataParserWorker, { type MetadataParsingResult } from '~/ui/workers/file-metadata-parser.ts?worker' import { getCoverUrl, getTags, type Tags } from '~/ui/composables/metadata' @@ -28,9 +30,25 @@ interface UploadQueueEntry { } export const useUploadsStore = defineStore('uploads', () => { + const uploadQueue: UploadQueueEntry[] = reactive([]) + const currentIndex = ref(0) + const currentUpload = computed(() => uploadQueue[currentIndex.value]) + const isUploading = computed(() => !!currentUpload.value) + // Tag extraction with a Web Worker - const { post: retrieveMetadata, data: workerData, worker } = useWebWorker(FileMetadataParserWorker) - whenever(workerData, (reactiveData) => { + const worker = ref>() + const retrieveMetadata = (entry: Pick) => { + if (!worker.value) worker.value = useWebWorker(FileMetadataParserWorker) + worker.value.post(entry) + } + + whenever(not(isUploading), () => { + worker.value?.terminate() + worker.value = undefined + }) + + + whenever(() => worker.value?.data, (reactiveData) => { const data = toRaw(reactiveData) if (data.status === 'success') { const id = data.id as number @@ -72,8 +90,6 @@ export const useUploadsStore = defineStore('uploads', () => { } }) - // TODO: Handle failure with a try/catch block - console.log(`[${entry.id}] import complete!`) entry.importedAt = new Date() } @@ -91,11 +107,6 @@ export const useUploadsStore = defineStore('uploads', () => { retrieveMetadata({ id, file }) } - const uploadQueue: UploadQueueEntry[] = reactive([]) - const currentIndex = ref(0) - const currentUpload = computed(() => uploadQueue[currentIndex.value]) - const isUploading = computed(() => !!currentUpload.value) - // Upload the file whenever it is available whenever(currentUpload, (entry) => upload(entry).catch((error) => { // The tags were missing, so we have cancelled the upload @@ -120,11 +131,21 @@ export const useUploadsStore = defineStore('uploads', () => { } }) + const cancelAll = () => { + for (const upload of uploadQueue) { + upload.abortController.abort() + } + + uploadQueue.length = 0 + currentIndex.value = 0 + } + // Return public API return { isUploading, queueUpload, currentUpload, + cancelAll, queue: readonly(uploadQueue) } }) diff --git a/front/src/ui/workers/file-metadata-parser.ts b/front/src/ui/workers/file-metadata-parser.ts index 9523d5ce3..589f76f2d 100644 --- a/front/src/ui/workers/file-metadata-parser.ts +++ b/front/src/ui/workers/file-metadata-parser.ts @@ -1,6 +1,21 @@ /// -import { getCoverUrl, getTags } from '~/ui/composables/metadata' +import { getCoverUrl, getTags, type Tags } from '~/ui/composables/metadata' + +export interface MetadataParsingSuccess { + id: number + status: 'success' + tags: Tags + coverUrl: string | undefined +} + +export interface MetadataParsingFailure { + id: number + status: 'failure' + error: Error +} + +export type MetadataParsingResult = MetadataParsingSuccess | MetadataParsingFailure const parse = async (id: number, file: File) => {
Select a destination for your audio files: