feat(front): [WIP] new upload process channel form #2081

This commit is contained in:
ArneBo 2025-02-14 13:03:39 +01:00
parent a8d5011796
commit 50dd404dab
6 changed files with 244 additions and 159 deletions

View File

@ -15,14 +15,11 @@ interface Events {
(e: 'created'): void
}
interface Props {
channel: Channel
}
const channel = defineModel<Channel>({required: true})
const { t } = useI18n()
const emit = defineEmits<Events>()
const props = defineProps<Props>()
const title = ref('')
@ -35,7 +32,7 @@ const submit = async () => {
try {
await axios.post('albums/', {
title: title.value,
artist: props.channel.artist?.id
artist: channel.value.artist?.id
})
emit('created')
@ -77,12 +74,10 @@ defineExpose({
</ul>
</Alert>
<div class="ui required field">
<label for="album-title">
{{ t('components.channels.AlbumForm.label.albumTitle') }}
</label>
<Input
v-model="title"
type="text"
:label="t('components.channels.AlbumForm.label.albumTitle')"
/>
</div>
</Layout>

View File

@ -1,63 +1,105 @@
<script setup lang="ts">
import type { Channel } from '~/types'
import Modal from '~/components/ui/Modal.vue'
import ChannelAlbumForm from '~/components/channels/AlbumForm.vue'
import type { Channel, BackendError } from '~/types'
import { watch, ref } from 'vue'
import axios from 'axios'
import { watch, ref, emit } from 'vue'
import { useI18n } from 'vue-i18n'
import { useModal } from '~/ui/composables/useModal.ts'
import Layout from '~/components/ui/Layout.vue'
import Modal from '~/components/ui/Modal.vue'
import Button from '~/components/ui/Button.vue'
import Spacer from '~/components/ui/Spacer.vue'
interface Events {
(e: 'created'): void
}
interface Props {
channel: Channel
}
import Input from '~/components/ui/Input.vue'
const { t } = useI18n()
const emit = defineEmits<Events>()
defineProps<Props>()
const channel = defineModel<Channel>({required: true})
defineEmits(['created'])
const newAlbumTitle = ref<string>('')
const isLoading = ref(false)
const submittable = ref(false)
const show = ref(false)
const errors = ref<string[]>([])
const {isOpen:show} = useModal('album')
defineEmits(['created'])
watch(show, () => {
isLoading.value = false
submittable.value = false
})
const albumForm = ref()
// Create a new Album and tell the parent to re-fetch all albums
defineExpose({
show
})
const submit = async () => {
isLoading.value = true
errors.value = []
try {
await axios.post('albums/', {
title: newAlbumTitle.value,
artist_credit: [channel.value.artist?.id]
})
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
emit('created')
isLoading.value = false
}
</script>
<template>
<Modal :title="t(channel.artist.content_category === 'podcast' ? 'components.channels.AlbumModal.header.newSeries' : 'components.channels.AlbumModal.header.newAlbum')"
<Modal :title="t(channel?.artist?.content_category === 'podcast' ? 'components.channels.AlbumModal.header.newSeries' : 'components.channels.AlbumModal.header.newAlbum')"
v-model="show"
class="small"
:cancel="t('components.channels.AlbumModal.button.cancel')"
>
<div class="scrolling content">
<channel-album-form
ref="albumForm"
:channel="channel"
@loading="isLoading = $event"
@submittable="submittable = $event"
@created="emit('created')"
<template #alert>
<Alert
v-if="errors.length > 0"
red
>
<h4 class="header">
{{ t('components.channels.AlbumForm.header.error') }}
</h4>
<ul class="list">
<li
v-for="(error, key) in errors"
:key="key"
>
{{ error }}
</li>
</ul>
</Alert>
</template>
<Layout form
:class="['ui', {loading: isLoading}, 'form']"
@submit.stop.prevent
>
<Input
v-model="newAlbumTitle"
required
type="text"
:label="t('components.channels.AlbumForm.label.albumTitle')"
/>
</div>
</Layout>
<template #actions>
<Button
:is-loading="isLoading"
:disabled="!submittable"
primary
@click.stop.prevent="albumForm.submit()"
@click.stop.prevent="submit()"
>
{{ t('components.channels.AlbumModal.button.create') }}
</Button>

View File

@ -2,78 +2,82 @@
import type { Album, Channel } from '~/types'
import axios from 'axios'
import { useVModel } from '@vueuse/core'
import { reactive, ref, watch } from 'vue'
import { ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useModal } from '~/ui/composables/useModal.ts'
interface Events {
(e: 'update:modelValue', value: string): void
}
interface Props {
modelValue: string | null
channel: Channel | null
}
import Layout from '~/components/ui/Layout.vue'
import Link from '~/components/ui/Link.vue'
import Spacer from '~/components/ui/Spacer.vue'
const { t } = useI18n()
const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), {
modelValue: null,
channel: null
})
const model = defineModel<{channel: Channel, albumId: Album['id'] | '', albums: Album[]}>({ required: true })
const value = useVModel(props, 'modelValue', emit)
const albums = reactive<Album[]>([])
const albums = ref<Album[]>([])
const isLoading = ref(false)
const fetchData = async () => {
albums.length = 0
if (!props.channel?.artist) return
const fetchAlbums = async () => {
isLoading.value = true
const response = await axios.get('albums/', {
params: {
artist: props.channel?.artist.id,
artist: model.value.channel.artist.id,
include_channels: 'true'
}
})
console.log("I found another album with artist", props.channel?.artist.name, ":", response.data.results)
albums.push(...response.data.results)
console.log("I found another album with artist", model.value.channel.artist.name, ":", response.data.results)
albums.value = response.data.results
isLoading.value = false
}
watch(() => props.channel, fetchData, { immediate: true })
watch(() => model.value.channel, fetchAlbums, { immediate: true })
watch(albums, (value) => {
if (value.length === 1)
selectedAlbumId.value = albums.value[0].id
})
</script>
<template>
<div>
<label for="album-dropdown">
<span v-if="channel && channel.artist && channel.artist.content_category === 'podcast'">
{{ t('components.channels.AlbumSelect.label.series') }}
</span>
<span v-else>
{{ t('components.channels.AlbumSelect.label.album') }}
</span>
</label>
<select
id="album-dropdown"
v-model="value"
class="ui search normal dropdown"
<label for="album-dropdown">
<span v-if="model.channel.artist.content_category === 'podcast'">
{{ t('components.channels.AlbumSelect.label.series') }}
</span>
<span v-else>
{{ t('components.channels.AlbumSelect.label.album') }}
</span>
</label>
<select
id="album-dropdown"
v-model="model.albumId"
class="ui search normal dropdown"
>
<option value="">
{{ t('components.channels.AlbumSelect.option.none') }}
</option>
<option
v-for="album in albums"
:key="album.id"
:value="album.id"
>
<option value="">
{{ t('components.channels.AlbumSelect.option.none') }}
</option>
<option
v-for="album in albums"
:key="album.id"
:value="album.id"
>
{{ album.title }}
{{ t('components.channels.AlbumSelect.meta.tracks', album.tracks_count) }}
</option>
</select>
</div>
{{ album.title }}
{{ t('components.channels.AlbumSelect.meta.tracks', album.tracks_count) }}
</option>
</select>
<Layout stack>
<Spacer :size="4" />
<Link
solid
primary
icon="bi-plus"
:to="useModal('album').to"
>
Add Album
<AlbumModal
v-model="model.channel"
@created="fetchAlbums"
/>
</Link>
</Layout>
</template>

View File

@ -1,26 +1,31 @@
<script setup lang="ts">
import type { BackendError, Channel, Upload, Track } from '~/types'
import type { BackendError, Channel, Upload, Track, Album } from '~/types'
import type { VueUploadItem } from 'vue-upload-component'
import { computed, ref, reactive, watchEffect, watch } from 'vue'
import { computed, ref, reactive, watchEffect, watch, onMounted } from 'vue'
import { whenever } from '@vueuse/core'
import { humanSize } from '~/utils/filters'
import { useI18n } from 'vue-i18n'
import { useStore } from '~/store'
import { useModal } from '~/ui/composables/useModal.ts'
import axios from 'axios'
import { type paths, type schemas, type operations, type components } from '~/generated/types.ts'
import { type paths, type operations, type components } from '~/generated/types.ts'
import UploadMetadataForm from '~/components/channels/UploadMetadataForm.vue'
import FileUploadWidget from '~/components/library/FileUploadWidget.vue'
import LicenseSelect from '~/components/channels/LicenseSelect.vue'
import AlbumSelect from '~/components/channels/AlbumSelect.vue'
import AlbumModal from '~/components/channels/AlbumModal.vue'
import useErrorHandler from '~/composables/useErrorHandler'
import Layout from '~/components/ui/Layout.vue'
import Alert from '~/components/ui/Alert.vue'
import Button from '~/components/ui/Button.vue'
import Link from '~/components/ui/Link.vue'
import Loader from '~/components/ui/Loader.vue'
import Spacer from '~/components/ui/Spacer.vue'
interface Events {
@ -28,7 +33,7 @@ interface Events {
}
interface Props {
channel?: Channel | null
channel: Channel | null
}
interface QuotaStatus {
@ -72,29 +77,90 @@ const files = ref([] as VueUploadItem[])
//
// Channels
//
const availableChannels = reactive({
channels: [] as Channel[],
count: 0,
loading: false
})
const availableChannels = ref<Channel[]>([])
/*
availableChannels>1? :=1 :=0
| | |
v v v
props select a channel | create empty channel
| | null
v v |
channelDropdownId v
|
v
selectedChannel
|
v
as a model to Album
|
v
albums
*/
// In the channel dropdown, we can select a value
//
const channelDropdownId = ref<Channel['artist']['id'] | null>(null)
const isLoading = ref(false)
const selectedChannel = computed(()=>
props.channel ? props.channel
: availableChannels.value.length===0 ? (createEmptyChannel(), null)
: availableChannels.value.length===1 ? availableChannels.value[0]
: availableChannels.value.find(({artist}) => artist.id === channelDropdownId.value)
)
const emptyChannelCreateRequest:components['schemas']['ChannelCreateRequest'] = {
name: store.state.auth.fullUsername,
username: store.state.auth.username,
description: null,
tags: [],
content_category: 'music',
}
const createEmptyChannel = async () => {
try {
const response = await axios.post(
'channels/',
(emptyChannelCreateRequest satisfies operations['create_channel_2']['requestBody']['content']['application/json'])
)
console.log("Created Channel: ", response.data)
} catch (error) {
errors.value = (error as BackendError).backendErrors
console.log("Error:", error)
}
}
const fetchChannels = async () => {
availableChannels.loading = true
isLoading.value = true
try {
const response = await axios.get('channels/', { params: { scope: 'me' } })
availableChannels.channels = response.data.results
availableChannels.count = response.data.count
availableChannels.value = response.data.results
} catch (error) {
errors.value = (error as BackendError).backendErrors
}
availableChannels.loading = false
isLoading.value = false
}
const selectedChannel = computed(() => availableChannels.channels[0])
// Albums
const albumSelection = ref<{channel: Channel, albumId: Album['id'] | '', albums: Album[]}>
watch(selectedChannel, (channel) =>
albumSelection.value =
{ channel: channel,
albumId: '',
albums: []
}
)
const channelChange = async (channelId) => {
selectedChannel.value = channelId
await fetchAlbums(channelId)
}
//
// Quota and space
//
const quotaStatus = ref()
@ -341,34 +407,29 @@ const labels = computed(() => ({
editTitle: t('components.channels.UploadForm.button.edit')
}))
const isLoading = ref(false)
const publish = async () => {
console.log("starting publish...")
isLoading.value = true
errors.value = []
console.log("first, let's try to create an empty channel...")
createEmptyChannel();
try {
// Post list of uuids of uploadedFiles to axios action:publish
/* { import_status: components["schemas"]["ImportStatusEnum"];
audio_file: string;} */
const theUpdate : components['schemas']['PatchedUploadForOwnerRequest'] = {
import_status: 'pending',
}
// const theUpdate : components['schemas']['PatchedUploadForOwnerRequest'] = {
// import_status: 'pending',
// }
await axios.post('uploads/action/', {
action: 'publish',
objects: uploadedFiles.value.map((file) => file.response?.uuid)
} satisfies paths['/api/v2/uploads/action/']['post']['requestBody']['content']['application/json'],
{
headers: { 'Authorization': `Bearer ${store.state.auth.oauth}` }
})
// await axios.post('uploads/action/', {
// action: 'publish',
// objects: uploadedFiles.value.map((file) => file.response?.uuid)
// } satisfies paths['/api/v2/uploads/action/']['post']['requestBody']['content']['application/json'],
// {
// headers: { 'Authorization': `Bearer ${store.state.auth.oauth}` }
// })
console.log("Channels Store Before: ", store.state.channels)
@ -396,10 +457,6 @@ defineExpose({
publish
})
// Api Calls
// Create a new channel
@ -460,35 +517,12 @@ ChannelCreateRequest: {
};
};
*/
const emptyChannelCreateRequest:schemas['ChannelCreateRequest'] = {
name: 'empty channel',
username: store.state.auth.username,
description: null,
tags: [],
content_category: 'music',
}
//json
const emptyCreate_channel_2RequestBodyContentJson:operations['create_channel_2']['requestBody']['content']['application/json'] =
emptyChannelCreateRequest
//post
const createEmptyChannel = async () => {
try {
const response = await axios.post('channels/', emptyCreate_channel_2RequestBodyContentJson)
console.log("Created Channel: ", response.data)
} catch (error) {
errors.value = (error as BackendError).backendErrors
console.log("Error:", error)
}
}
</script>
<template>
<form
:class="['ui', { loading: availableChannels.loading }, 'form component-file-upload']"
:class="['ui', { loading: isLoading }, 'form component-file-upload']"
@submit.stop.prevent
>
<!-- Error message -->
@ -510,23 +544,30 @@ const createEmptyChannel = async () => {
<!-- Select Album and License -->
<div :class="['ui', 'required', 'field']">
<label for="channel-dropdown">
<label v-if="availableChannels.length === 1" for="channel-dropdown">
{{ t('components.channels.UploadForm.label.channel') }}: {{ selectedChannel?.artist.name }}
</label>
<label v-else for="channel-dropdown">
{{ t('components.channels.UploadForm.label.channel') }}
</label>
<select
v-if="availableChannels.count > 1"
v-if="availableChannels.length > 1"
v-model="channelDropdownId"
id="channel-dropdown"
class="dropdown"
>
<option v-for="channel in availableChannels.channels" :value="channel.uuid">
<option v-for="channel in availableChannels" :value="channel.artist.id">
{{ channel.artist.name }}
</option>
</select>
</div>
<album-select
v-model.number="values.album"
:channel="selectedChannel"
:class="['ui', 'field']"
/>
<Layout flex>
<album-select
v-if="selectedChannel"
v-model="albumSelection"
:class="['ui', 'field']"
/>
</Layout>
<license-select
v-model="values.license"
:class="['ui', 'field']"

View File

@ -64,7 +64,7 @@ const channelUpload = ref();
</script>
<template>
<Modal overPopover
<Modal
:title="modalTitle"
v-model="isOpen"
>

View File

@ -5,6 +5,7 @@ import { computed, ref, reactive, watch } from 'vue'
import { whenever } from '@vueuse/core'
import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import { useModal } from '~/ui/composables/useModal.ts'
import axios from 'axios'
@ -17,6 +18,7 @@ import Loader from '~/components/ui/Loader.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Alert from '~/components/ui/Alert.vue'
import Button from '~/components/ui/Button.vue'
import Link from '~/components/ui/Link.vue'
import useWebSocketHandler from '~/composables/useWebSocketHandler'
@ -153,7 +155,7 @@ const albumModal = ref()
<channel-entries
:key="String(episodesKey) + 'entries'"
:is-podcast="isPodcast"
:default-cover="object.artist?.cover"
:default-cover="object.artist?.cover || null"
:limit="25"
:filters="{channel: object.uuid, ordering: '-creation_date', page_size: '25'}"
>
@ -191,17 +193,18 @@ const albumModal = ref()
v-if="isOwner"
class="actions"
>
<a @click.stop.prevent="albumModal.show = true">
<Link
:to="useModal('album').to"
>
<i class="bi bi-plus" />
{{ t('views.channels.DetailOverview.link.addAlbum') }}
</a>
</Link>
</div>
</h2>
</channel-series>
<album-modal
v-if="isOwner"
ref="albumModal"
:channel="object"
:model-value="object"
@created="albumModal.show = false; seriesKey = new Date()"
/>
</section>