funkwhale/front/src/components/audio/ChannelForm.vue

416 lines
13 KiB
Vue

<template>
<form
class="ui form"
@submit.prevent.stop="submit"
>
<div
v-if="errors.length > 0"
role="alert"
class="ui negative message"
>
<h4 class="header">
<translate translate-context="Content/*/Error message.Title">
Error while saving channel
</translate>
</h4>
<ul class="list">
<li
v-for="(error, key) in errors"
:key="key"
>
{{ error }}
</li>
</ul>
</div>
<template v-if="metadataChoices">
<fieldset
v-if="creating && step === 1"
class="ui grouped channel-type required field"
>
<legend>
<translate translate-context="Content/Channel/Paragraph">
What will this channel be used for?
</translate>
</legend>
<div class="ui hidden divider" />
<div class="field">
<div
v-for="(choice, key) in categoryChoices"
:key="key"
:class="['ui', 'radio', 'checkbox', {selected: choice.value == newValues.content_category}]"
>
<input
:id="`category-${choice.value}`"
v-model="newValues.content_category"
type="radio"
name="channel-category"
:value="choice.value"
>
<label :for="`category-${choice.value}`">
<span :class="['right floated', 'placeholder', 'image', {circular: choice.value === 'music'}]" />
<strong>{{ choice.label }}</strong>
<div class="ui small hidden divider" />
{{ choice.helpText }}
</label>
</div>
</div>
</fieldset>
<template v-if="!creating || step === 2">
<div class="ui required field">
<label for="channel-name">
<translate translate-context="Content/Channel/*">Name</translate>
</label>
<input
v-model="newValues.name"
type="text"
required
:placeholder="labels.namePlaceholder"
>
</div>
<div class="ui required field">
<label for="channel-username">
<translate translate-context="Content/Channel/*">Fediverse handle</translate>
</label>
<div class="ui left labeled input">
<div class="ui basic label">
@
</div>
<input
v-model="newValues.username"
type="text"
:required="creating"
:disabled="!creating"
:placeholder="labels.usernamePlaceholder"
>
</div>
<template v-if="creating">
<div class="ui small hidden divider" />
<p>
<translate translate-context="Content/Channels/Paragraph">
Used in URLs and to follow this channel in the Fediverse. It cannot be changed later.
</translate>
</p>
</template>
</div>
<div class="six wide column">
<attachment-input
v-model="newValues.cover"
:required="false"
:image-class="newValues.content_category === 'podcast' ? '' : 'circular'"
@delete="newValues.cover = null"
>
<translate
slot="label"
translate-context="Content/Channel/*"
>
Channel Picture
</translate>
</attachment-input>
</div>
<div class="ui small hidden divider" />
<div class="ui stackable grid row">
<div class="ten wide column">
<div class="ui field">
<label for="channel-tags">
<translate translate-context="*/*/*">Tags</translate>
</label>
<tags-selector
id="channel-tags"
v-model="newValues.tags"
:required="false"
/>
</div>
</div>
<div
v-if="newValues.content_category === 'podcast'"
class="six wide column"
>
<div class="ui required field">
<label for="channel-language">
<translate translate-context="*/*/*">Language</translate>
</label>
<select
id="channel-language"
v-model="newValues.metadata.language"
name="channel-language"
required
class="ui search selection dropdown"
>
<option
v-for="(v, key) in metadataChoices.language"
:key="key"
:value="v.value"
>
{{ v.label }}
</option>
</select>
</div>
</div>
</div>
<div class="ui small hidden divider" />
<div class="ui field">
<label for="channel-name">
<translate translate-context="*/*/*">Description</translate>
</label>
<content-form v-model="newValues.description" />
</div>
<div
v-if="newValues.content_category === 'podcast'"
class="ui two fields"
>
<div class="ui required field">
<label for="channel-itunes-category">
<translate translate-context="*/*/*">Category</translate>
</label>
<select
id="itunes-category"
v-model="newValues.metadata.itunes_category"
name="itunes-category"
required
class="ui dropdown"
>
<option
v-for="(v, key) in metadataChoices.itunes_category"
:key="key"
:value="v.value"
>
{{ v.label }}
</option>
</select>
</div>
<div class="ui field">
<label for="channel-itunes-category">
<translate translate-context="*/*/*">Subcategory</translate>
</label>
<select
id="itunes-category"
v-model="newValues.metadata.itunes_subcategory"
name="itunes-category"
:disabled="!newValues.metadata.itunes_category"
class="ui dropdown"
>
<option
v-for="(v, key) in itunesSubcategories"
:key="key"
:value="v"
>
{{ v }}
</option>
</select>
</div>
</div>
<div
v-if="newValues.content_category === 'podcast'"
class="ui two fields"
>
<div class="ui field">
<label for="channel-itunes-email">
<translate translate-context="*/*/*">Owner e-mail address</translate>
</label>
<input
id="channel-itunes-email"
v-model="newValues.metadata.owner_email"
name="channel-itunes-email"
type="email"
>
</div>
<div class="ui field">
<label for="channel-itunes-name">
<translate translate-context="*/*/*">Owner name</translate>
</label>
<input
id="channel-itunes-name"
v-model="newValues.metadata.owner_name"
name="channel-itunes-name"
maxlength="255"
>
</div>
</div>
<p>
<translate translate-context="*/*/*">
Used for the itunes:email and itunes:name field required by certain platforms such as Spotify or iTunes.
</translate>
</p>
</template>
</template>
<div
v-else
class="ui active inverted dimmer"
>
<div class="ui text loader">
<translate translate-context="*/*/*">
Loading
</translate>
</div>
</div>
</form>
</template>
<script>
import axios from 'axios'
import AttachmentInput from '~/components/common/AttachmentInput.vue'
import TagsSelector from '~/components/library/TagsSelector.vue'
function slugify (text) {
return text.toString().toLowerCase()
.replace(/\s+/g, '') // Remove spaces
.replace(/[^\w]+/g, '') // Remove all non-word chars
}
export default {
components: {
AttachmentInput,
TagsSelector
},
props: {
object: { type: Object, required: false, default: null },
step: { type: Number, required: false, default: 1 }
},
data () {
const oldValues = {}
if (this.object) {
oldValues.metadata = { ...(this.object.metadata || {}) }
oldValues.name = this.object.artist.name
oldValues.description = this.object.artist.description
oldValues.cover = this.object.artist.cover
oldValues.tags = this.object.artist.tags
oldValues.content_category = this.object.artist.content_category
oldValues.username = this.object.actor.preferred_username
}
return {
isLoading: false,
errors: [],
metadataChoices: null,
newValues: {
name: oldValues.name || '',
username: oldValues.username || '',
tags: oldValues.tags || [],
description: (oldValues.description || {}).text || '',
cover: (oldValues.cover || {}).uuid || null,
content_category: oldValues.content_category || 'podcast',
metadata: oldValues.metadata || {}
}
}
},
computed: {
creating () {
return this.object === null
},
categoryChoices () {
return [
{
value: 'podcast',
label: this.$pgettext('*/*/*', 'Podcasts'),
helpText: this.$pgettext('Content/Channels/Help', 'Host your episodes and keep your community updated.')
},
{
value: 'music',
label: this.$pgettext('*/*/*', 'Artist discography'),
helpText: this.$pgettext('Content/Channels/Help', 'Publish music you make as a nice discography of albums and singles.')
}
]
},
itunesSubcategories () {
for (let index = 0; index < this.metadataChoices.itunes_category.length; index++) {
const element = this.metadataChoices.itunes_category[index]
if (element.value === this.newValues.metadata.itunes_category) {
return element.children || []
}
}
return []
},
labels () {
return {
namePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', 'Awesome channel name'),
usernamePlaceholder: this.$pgettext('Content/Channel/Form.Field.Placeholder', 'awesomechannelname')
}
},
submittable () {
let v = this.newValues.name && this.newValues.username
if (this.newValues.content_category === 'podcast') {
v = v && this.newValues.metadata.itunes_category && this.newValues.metadata.language
}
return !!v
}
},
watch: {
'newValues.name' (v) {
if (this.creating) {
this.newValues.username = slugify(v)
}
},
'newValues.metadata.itunes_category' (v) {
this.newValues.metadata.itunes_subcategory = null
},
'newValues.content_category': {
handler (v) {
this.$emit('category', v)
},
immediate: true
},
isLoading: {
handler (v) {
this.$emit('loading', v)
},
immediate: true
},
submittable: {
handler (v) {
this.$emit('submittable', v)
},
immediate: true
}
},
created () {
this.fetchMetadataChoices()
},
methods: {
fetchMetadataChoices () {
const self = this
axios.get('channels/metadata-choices').then((response) => {
self.metadataChoices = response.data
}, error => {
self.errors = error.backendErrors
})
},
submit () {
this.isLoading = true
const self = this
const handler = this.creating ? axios.post : axios.patch
const url = this.creating ? 'channels/' : `channels/${this.object.uuid}`
const payload = {
name: this.newValues.name,
username: this.newValues.username,
tags: this.newValues.tags,
content_category: this.newValues.content_category,
cover: this.newValues.cover,
metadata: this.newValues.metadata
}
if (this.newValues.description) {
payload.description = {
content_type: 'text/markdown',
text: this.newValues.description
}
} else {
payload.description = null
}
handler(url, payload).then((response) => {
self.isLoading = false
if (self.creating) {
self.$emit('created', response.data)
} else {
self.$emit('updated', response.data)
}
}, error => {
self.isLoading = false
self.errors = error.backendErrors
self.$emit('errored', self.errors)
})
}
}
}
</script>