Migrate some components
This commit is contained in:
parent
7eca32e006
commit
c1494c8894
|
@ -8,7 +8,7 @@ import { useStore } from '~/store'
|
||||||
import AlbumCard from '~/components/audio/album/Card.vue'
|
import AlbumCard from '~/components/audio/album/Card.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
filters: Record<string, string>
|
filters: Record<string, string | boolean>
|
||||||
showCount?: boolean
|
showCount?: boolean
|
||||||
search?: boolean
|
search?: boolean
|
||||||
limit?: number
|
limit?: number
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { useStore } from '~/store'
|
||||||
import ArtistCard from '~/components/audio/artist/Card.vue'
|
import ArtistCard from '~/components/audio/artist/Card.vue'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
filters: Record<string, string>
|
filters: Record<string, string | boolean>
|
||||||
search?: boolean
|
search?: boolean
|
||||||
header?: boolean
|
header?: boolean
|
||||||
limit?: number
|
limit?: number
|
||||||
|
|
|
@ -16,7 +16,7 @@ interface Emits {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
filters: Record<string, string>
|
filters: Record<string, string | boolean>
|
||||||
url: string
|
url: string
|
||||||
isActivity?: boolean
|
isActivity?: boolean
|
||||||
showCount?: boolean
|
showCount?: boolean
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { EditObject, EditObjectType } from '~/composables/moderation/useEditConfigs'
|
import type { EditObject, EditObjectType } from '~/composables/moderation/useEditConfigs'
|
||||||
|
import type { ReviewState } from '~/types'
|
||||||
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import useEditConfigs from '~/composables/moderation/useEditConfigs'
|
import useEditConfigs from '~/composables/moderation/useEditConfigs'
|
||||||
|
@ -17,7 +18,7 @@ const props = defineProps<Props>()
|
||||||
const configs = useEditConfigs()
|
const configs = useEditConfigs()
|
||||||
const config = computed(() => configs[props.objectType])
|
const config = computed(() => configs[props.objectType])
|
||||||
|
|
||||||
const currentState = computed(() => config.value.fields.reduce((state: Record<string, unknown>, field) => {
|
const currentState = computed(() => config.value.fields.reduce((state: ReviewState, field) => {
|
||||||
state[field.id] = { value: field.getValue(props.object) }
|
state[field.id] = { value: field.getValue(props.object) }
|
||||||
return state
|
return state
|
||||||
}, {}))
|
}, {}))
|
||||||
|
|
|
@ -1,3 +1,148 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { EditObject, EditObjectType } from '~/composables/moderation/useEditConfigs'
|
||||||
|
import type { BackendError, License, ReviewState } from '~/types'
|
||||||
|
|
||||||
|
import { computed, onMounted, reactive, ref, watchEffect } from 'vue'
|
||||||
|
import { isEqual, clone } from 'lodash-es'
|
||||||
|
import { useGettext } from 'vue3-gettext'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
|
import axios from 'axios'
|
||||||
|
import $ from 'jquery'
|
||||||
|
|
||||||
|
import AttachmentInput from '~/components/common/AttachmentInput.vue'
|
||||||
|
import useEditConfigs from '~/composables/moderation/useEditConfigs'
|
||||||
|
import TagsSelector from '~/components/library/TagsSelector.vue'
|
||||||
|
import EditList from '~/components/library/EditList.vue'
|
||||||
|
import EditCard from '~/components/library/EditCard.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
objectType: EditObjectType
|
||||||
|
object: EditObject
|
||||||
|
licenses: License[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const { $pgettext } = useGettext()
|
||||||
|
const configs = useEditConfigs()
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const config = computed(() => configs[props.objectType])
|
||||||
|
const currentState = computed(() => config.value.fields.reduce((state: ReviewState, field) => {
|
||||||
|
state[field.id] = { value: field.getValue(props.object) }
|
||||||
|
return state
|
||||||
|
}, {}))
|
||||||
|
|
||||||
|
const canEdit = computed(() => {
|
||||||
|
if (!store.state.auth.authenticated) return false
|
||||||
|
|
||||||
|
const isOwner = props.object.attributed_to
|
||||||
|
// TODO (wvffle): Is it better to compare ids? Is full_username unique?
|
||||||
|
&& store.state.auth.fullUsername === props.object.attributed_to.full_username
|
||||||
|
|
||||||
|
return isOwner || store.state.auth.availablePermissions.library
|
||||||
|
})
|
||||||
|
|
||||||
|
const labels = computed(() => ({
|
||||||
|
summaryPlaceholder: $pgettext('*/*/Placeholder', 'A short summary describing your changes.')
|
||||||
|
}))
|
||||||
|
|
||||||
|
const mutationsUrl = computed(() => props.objectType === 'track'
|
||||||
|
? `tracks/${props.object.id}/mutations/`
|
||||||
|
: props.objectType === 'album'
|
||||||
|
? `albums/${props.object.id}/mutations/`
|
||||||
|
: props.objectType === 'artist'
|
||||||
|
? `artists/${props.object.id}/mutations/`
|
||||||
|
: ''
|
||||||
|
)
|
||||||
|
|
||||||
|
const mutationPayload = computed(() => {
|
||||||
|
const changedFields = config.value.fields.filter(f => {
|
||||||
|
return !isEqual(values[f.id], initialValues[f.id])
|
||||||
|
})
|
||||||
|
|
||||||
|
if (changedFields.length === 0) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
type: 'update',
|
||||||
|
payload: {} as Record<string, unknown>,
|
||||||
|
summary: summary.value
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const field of changedFields) {
|
||||||
|
data.payload[field.id] = values[field.id]
|
||||||
|
}
|
||||||
|
|
||||||
|
return data
|
||||||
|
})
|
||||||
|
|
||||||
|
const showPendingReview = ref(true)
|
||||||
|
const editListFilters = computed(() => showPendingReview.value
|
||||||
|
? { is_approved: 'null' }
|
||||||
|
: {}
|
||||||
|
)
|
||||||
|
|
||||||
|
const values = reactive({} as Record<string, any>)
|
||||||
|
const initialValues = reactive({} as Record<string, any>)
|
||||||
|
for (const { id, getValue } of config.value.fields) {
|
||||||
|
values[id] = clone(getValue(props.object))
|
||||||
|
initialValues[id] = clone(values[id])
|
||||||
|
}
|
||||||
|
|
||||||
|
const license = ref()
|
||||||
|
watchEffect(() => {
|
||||||
|
if (values.license === null) {
|
||||||
|
$(license.value).dropdown('clear')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
$(license.value).dropdown('set selected', values.license)
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
$('.ui.dropdown').dropdown({ fullTextSearch: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
const submittedMutation = ref()
|
||||||
|
const summary = ref('')
|
||||||
|
|
||||||
|
const errors = ref([] as string[])
|
||||||
|
const isLoading = ref(false)
|
||||||
|
const submit = async () => {
|
||||||
|
const url = mutationsUrl.value
|
||||||
|
if (!url) return
|
||||||
|
|
||||||
|
isLoading.value = true
|
||||||
|
errors.value = []
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.post(url, {
|
||||||
|
...mutationPayload.value,
|
||||||
|
is_approved: canEdit.value
|
||||||
|
? true
|
||||||
|
: undefined
|
||||||
|
})
|
||||||
|
|
||||||
|
submittedMutation.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
errors.value = (error as BackendError).backendErrors
|
||||||
|
}
|
||||||
|
|
||||||
|
isLoading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const fieldValuesChanged = (fieldId: string) => {
|
||||||
|
return !isEqual(values[fieldId], initialValues[fieldId])
|
||||||
|
}
|
||||||
|
|
||||||
|
const resetField = (fieldId: string) => {
|
||||||
|
values[fieldId] = clone(initialValues[fieldId])
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="submittedMutation">
|
<div v-if="submittedMutation">
|
||||||
<div class="ui positive message">
|
<div class="ui positive message">
|
||||||
|
@ -107,7 +252,7 @@
|
||||||
:id="fieldConfig.id"
|
:id="fieldConfig.id"
|
||||||
v-model="values[fieldConfig.id]"
|
v-model="values[fieldConfig.id]"
|
||||||
:type="fieldConfig.inputType || 'text'"
|
:type="fieldConfig.inputType || 'text'"
|
||||||
:required="fieldConfig.required || null"
|
:required="fieldConfig.required"
|
||||||
:name="fieldConfig.id"
|
:name="fieldConfig.id"
|
||||||
>
|
>
|
||||||
</template>
|
</template>
|
||||||
|
@ -118,7 +263,7 @@
|
||||||
:id="fieldConfig.id"
|
:id="fieldConfig.id"
|
||||||
ref="license"
|
ref="license"
|
||||||
v-model="values[fieldConfig.id]"
|
v-model="values[fieldConfig.id]"
|
||||||
:required="fieldConfig.required || null"
|
:required="fieldConfig.required"
|
||||||
class="ui fluid search dropdown"
|
class="ui fluid search dropdown"
|
||||||
>
|
>
|
||||||
<option :value="null">
|
<option :value="null">
|
||||||
|
@ -158,7 +303,7 @@
|
||||||
:id="fieldConfig.id"
|
:id="fieldConfig.id"
|
||||||
v-model="values[fieldConfig.id]"
|
v-model="values[fieldConfig.id]"
|
||||||
:initial-value="initialValues[fieldConfig.id]"
|
:initial-value="initialValues[fieldConfig.id]"
|
||||||
:required="fieldConfig.required || null"
|
:required="fieldConfig.required"
|
||||||
:name="fieldConfig.id"
|
:name="fieldConfig.id"
|
||||||
@delete="values[fieldConfig.id] = initialValues[fieldConfig.id]"
|
@delete="values[fieldConfig.id] = initialValues[fieldConfig.id]"
|
||||||
>
|
>
|
||||||
|
@ -220,7 +365,7 @@
|
||||||
<button
|
<button
|
||||||
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']"
|
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'success', 'button']"
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="isLoading || !mutationPayload || null"
|
:disabled="isLoading || !mutationPayload"
|
||||||
>
|
>
|
||||||
<translate
|
<translate
|
||||||
v-if="canEdit"
|
v-if="canEdit"
|
||||||
|
@ -238,153 +383,3 @@
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import $ from 'jquery'
|
|
||||||
import { isEqual, clone } from 'lodash-es'
|
|
||||||
import axios from 'axios'
|
|
||||||
import AttachmentInput from '~/components/common/AttachmentInput.vue'
|
|
||||||
import EditList from '~/components/library/EditList.vue'
|
|
||||||
import EditCard from '~/components/library/EditCard.vue'
|
|
||||||
import TagsSelector from '~/components/library/TagsSelector.vue'
|
|
||||||
import useEditConfigs from '~/composables/moderation/useEditConfigs'
|
|
||||||
import { computed } from 'vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
EditList,
|
|
||||||
EditCard,
|
|
||||||
TagsSelector,
|
|
||||||
AttachmentInput
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
objectType: { type: String, required: true },
|
|
||||||
object: { type: Object, required: true },
|
|
||||||
licenses: { type: Array, required: true }
|
|
||||||
},
|
|
||||||
setup (props) {
|
|
||||||
const configs = useEditConfigs()
|
|
||||||
const config = computed(() => configs[props.objectType])
|
|
||||||
const currentState = computed(() => config.value.fields.reduce((state/*: Record<string, unknown> */, field) => {
|
|
||||||
state[field.id] = { value: field.getValue(props.object) }
|
|
||||||
return state
|
|
||||||
}, {}))
|
|
||||||
|
|
||||||
return { config, currentState, configs }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
isLoading: false,
|
|
||||||
errors: [],
|
|
||||||
values: {},
|
|
||||||
initialValues: {},
|
|
||||||
summary: '',
|
|
||||||
submittedMutation: null,
|
|
||||||
showPendingReview: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
canEdit () {
|
|
||||||
if (!this.$store.state.auth.authenticated) return false
|
|
||||||
|
|
||||||
const isOwner = this.object.attributed_to
|
|
||||||
// TODO (wvffle): Is it better to compare ids? Is full_username unique?
|
|
||||||
&& this.$store.state.auth.fullUsername === this.object.attributed_to.full_username
|
|
||||||
|
|
||||||
return isOwner || this.$store.state.auth.availablePermissions.library
|
|
||||||
},
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
summaryPlaceholder: this.$pgettext('*/*/Placeholder', 'A short summary describing your changes.')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mutationsUrl () {
|
|
||||||
if (this.objectType === 'track') {
|
|
||||||
return `tracks/${this.object.id}/mutations/`
|
|
||||||
}
|
|
||||||
if (this.objectType === 'album') {
|
|
||||||
return `albums/${this.object.id}/mutations/`
|
|
||||||
}
|
|
||||||
if (this.objectType === 'artist') {
|
|
||||||
return `artists/${this.object.id}/mutations/`
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
mutationPayload () {
|
|
||||||
const self = this
|
|
||||||
const changedFields = this.config.fields.filter(f => {
|
|
||||||
return !isEqual(self.values[f.id], self.initialValues[f.id])
|
|
||||||
})
|
|
||||||
if (changedFields.length === 0) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
const payload = {
|
|
||||||
type: 'update',
|
|
||||||
payload: {},
|
|
||||||
summary: this.summary
|
|
||||||
}
|
|
||||||
changedFields.forEach((f) => {
|
|
||||||
payload.payload[f.id] = self.values[f.id]
|
|
||||||
})
|
|
||||||
return payload
|
|
||||||
},
|
|
||||||
editListFilters () {
|
|
||||||
if (this.showPendingReview) {
|
|
||||||
return { is_approved: 'null' }
|
|
||||||
} else {
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
'values.license' (newValue) {
|
|
||||||
if (newValue === null) {
|
|
||||||
$(this.$refs.license).dropdown('clear')
|
|
||||||
} else {
|
|
||||||
$(this.$refs.license).dropdown('set selected', newValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
this.setValues()
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
$('.ui.dropdown').dropdown({ fullTextSearch: true })
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
setValues () {
|
|
||||||
for (const { id, getValue } of this.config.fields) {
|
|
||||||
this.values[id] = clone(getValue(this.object))
|
|
||||||
this.initialValues[id] = clone(this.values[id])
|
|
||||||
}
|
|
||||||
},
|
|
||||||
submit () {
|
|
||||||
const self = this
|
|
||||||
self.isLoading = true
|
|
||||||
self.errors = []
|
|
||||||
const payload = clone(this.mutationPayload || {})
|
|
||||||
if (this.canEdit) {
|
|
||||||
payload.is_approved = true
|
|
||||||
}
|
|
||||||
return axios.post(this.mutationsUrl, payload).then(
|
|
||||||
response => {
|
|
||||||
self.isLoading = false
|
|
||||||
self.submittedMutation = response.data
|
|
||||||
},
|
|
||||||
error => {
|
|
||||||
self.errors = error.backendErrors
|
|
||||||
self.isLoading = false
|
|
||||||
}
|
|
||||||
)
|
|
||||||
},
|
|
||||||
fieldValuesChanged (fieldId) {
|
|
||||||
return !isEqual(this.values[fieldId], this.initialValues[fieldId])
|
|
||||||
},
|
|
||||||
resetField (fieldId) {
|
|
||||||
this.values[fieldId] = clone(this.initialValues[fieldId])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,23 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
|
||||||
|
import TrackWidget from '~/components/audio/track/Widget.vue'
|
||||||
|
import AlbumWidget from '~/components/audio/album/Widget.vue'
|
||||||
|
import ArtistWidget from '~/components/audio/artist/Widget.vue'
|
||||||
|
import RadioButton from '~/components/radios/Button.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const labels = computed(() => ({
|
||||||
|
title: `#${props.id}`
|
||||||
|
}))
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main v-title="labels.title">
|
<main v-title="labels.title">
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
|
@ -87,38 +107,3 @@
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import ChannelsWidget from '~/components/audio/ChannelsWidget.vue'
|
|
||||||
import TrackWidget from '~/components/audio/track/Widget.vue'
|
|
||||||
import AlbumWidget from '~/components/audio/album/Widget.vue'
|
|
||||||
import ArtistWidget from '~/components/audio/artist/Widget.vue'
|
|
||||||
import RadioButton from '~/components/radios/Button.vue'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
ArtistWidget,
|
|
||||||
AlbumWidget,
|
|
||||||
TrackWidget,
|
|
||||||
RadioButton,
|
|
||||||
ChannelsWidget
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
id: { type: String, required: true }
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
const title = `#${this.id}`
|
|
||||||
return {
|
|
||||||
title
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isAuthenticated () {
|
|
||||||
return this.$store.state.auth.authenticated
|
|
||||||
},
|
|
||||||
hasFavorites () {
|
|
||||||
return this.$store.state.favorites.count > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,91 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Tag } from '~/types'
|
||||||
|
|
||||||
|
import { ref, watch, onMounted, nextTick } from 'vue'
|
||||||
|
import { isEqual } from 'lodash-es'
|
||||||
|
import { useStore } from '~/store'
|
||||||
|
|
||||||
|
import $ from 'jquery'
|
||||||
|
|
||||||
|
interface Emits {
|
||||||
|
(e: 'update:modelValue', tags: Tag[]): void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: Tag[]
|
||||||
|
}
|
||||||
|
|
||||||
|
const emit = defineEmits<Emits>()
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const store = useStore()
|
||||||
|
|
||||||
|
const dropdown = ref()
|
||||||
|
watch(() => props.modelValue, (value) => {
|
||||||
|
const current = $(dropdown.value).dropdown('get value').split(',').sort()
|
||||||
|
|
||||||
|
if (!isEqual([...value].sort(), current)) {
|
||||||
|
$(dropdown.value).dropdown('set exactly', value)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleUpdate = () => {
|
||||||
|
const value = $(dropdown.value).dropdown('get value').split(',')
|
||||||
|
emit('update:modelValue', value)
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
$(dropdown.value).dropdown({
|
||||||
|
keys: { delimiter: 32 },
|
||||||
|
forceSelection: false,
|
||||||
|
saveRemoteData: false,
|
||||||
|
filterRemoteData: true,
|
||||||
|
preserveHTML: false,
|
||||||
|
apiSettings: {
|
||||||
|
url: store.getters['instance/absoluteUrl']('/api/v1/tags/?name__startswith={query}&ordering=length&page_size=5'),
|
||||||
|
beforeXHR: function (xhrObject) {
|
||||||
|
if (store.state.auth.oauth.accessToken) {
|
||||||
|
xhrObject.setRequestHeader('Authorization', store.getters['auth/header'])
|
||||||
|
}
|
||||||
|
return xhrObject
|
||||||
|
},
|
||||||
|
onResponse (response) {
|
||||||
|
response = { results: [], ...response }
|
||||||
|
|
||||||
|
// @ts-expect-error Semantic UI
|
||||||
|
const currentSearch: string = $(dropdown.value).dropdown('get query')
|
||||||
|
|
||||||
|
if (currentSearch) {
|
||||||
|
const existingTag = response.results.find((result: Tag) => result.name === currentSearch)
|
||||||
|
|
||||||
|
if (existingTag) {
|
||||||
|
if (response.results.indexOf(existingTag) !== 0) {
|
||||||
|
response.results = [existingTag, ...response.results]
|
||||||
|
response.results.splice(response.results.indexOf(existingTag) + 1, 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
response.results = [{ name: currentSearch }, ...response.results]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fields: { remoteValues: 'results', value: 'name' },
|
||||||
|
allowAdditions: true,
|
||||||
|
minCharacters: 1,
|
||||||
|
onAdd: handleUpdate,
|
||||||
|
onRemove: handleUpdate,
|
||||||
|
onLabelRemove: handleUpdate,
|
||||||
|
onChange: handleUpdate
|
||||||
|
})
|
||||||
|
|
||||||
|
$(dropdown.value).dropdown('set exactly', props.modelValue)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
ref="dropdown"
|
ref="dropdown"
|
||||||
|
@ -17,86 +105,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
|
||||||
import $ from 'jquery'
|
|
||||||
|
|
||||||
import { isEqual } from 'lodash-es'
|
|
||||||
export default {
|
|
||||||
props: { modelValue: { type: Array, required: true } },
|
|
||||||
watch: {
|
|
||||||
modelValue: {
|
|
||||||
handler (v) {
|
|
||||||
const current = $(this.$refs.dropdown).dropdown('get value').split(',').sort()
|
|
||||||
if (!isEqual([...v].sort(), current)) {
|
|
||||||
$(this.$refs.dropdown).dropdown('set exactly', v)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
mounted () {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.initDropdown()
|
|
||||||
})
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
initDropdown () {
|
|
||||||
const self = this
|
|
||||||
const handleUpdate = () => {
|
|
||||||
const value = $(self.$refs.dropdown).dropdown('get value').split(',')
|
|
||||||
self.$emit('update:modelValue', value)
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
const settings = {
|
|
||||||
keys: {
|
|
||||||
delimiter: 32
|
|
||||||
},
|
|
||||||
forceSelection: false,
|
|
||||||
saveRemoteData: false,
|
|
||||||
filterRemoteData: true,
|
|
||||||
preserveHTML: false,
|
|
||||||
apiSettings: {
|
|
||||||
url: this.$store.getters['instance/absoluteUrl']('/api/v1/tags/?name__startswith={query}&ordering=length&page_size=5'),
|
|
||||||
beforeXHR: function (xhrObject) {
|
|
||||||
if (self.$store.state.auth.oauth.accessToken) {
|
|
||||||
xhrObject.setRequestHeader('Authorization', self.$store.getters['auth/header'])
|
|
||||||
}
|
|
||||||
return xhrObject
|
|
||||||
},
|
|
||||||
onResponse (response) {
|
|
||||||
const currentSearch = $(self.$refs.dropdown).dropdown('get query')
|
|
||||||
response = {
|
|
||||||
results: [],
|
|
||||||
...response
|
|
||||||
}
|
|
||||||
if (currentSearch) {
|
|
||||||
const existingTag = response.results.find((result) => result.name === currentSearch)
|
|
||||||
if (existingTag) {
|
|
||||||
if (response.results.indexOf(existingTag) !== 0) {
|
|
||||||
response.results = [existingTag, ...response.results]
|
|
||||||
response.results.splice(response.results.indexOf(existingTag) + 1, 1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
response.results = [{ name: currentSearch }, ...response.results]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return response
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fields: {
|
|
||||||
remoteValues: 'results',
|
|
||||||
value: 'name'
|
|
||||||
},
|
|
||||||
allowAdditions: true,
|
|
||||||
minCharacters: 1,
|
|
||||||
onAdd: handleUpdate,
|
|
||||||
onRemove: handleUpdate,
|
|
||||||
onLabelRemove: handleUpdate,
|
|
||||||
onChange: handleUpdate
|
|
||||||
}
|
|
||||||
$(this.$refs.dropdown).dropdown(settings)
|
|
||||||
$(this.$refs.dropdown).dropdown('set exactly', this.modelValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,3 +1,49 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Track } from '~/types'
|
||||||
|
|
||||||
|
import { humanSize, momentFormat, truncate } from '~/utils/filters'
|
||||||
|
import { computed, ref, watchEffect } from 'vue'
|
||||||
|
|
||||||
|
import time from '~/utils/time'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
|
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
||||||
|
import PlaylistWidget from '~/components/playlists/Widget.vue'
|
||||||
|
import TagsList from '~/components/tags/List.vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
track: Track
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
|
||||||
|
const musicbrainzUrl = computed(() => props.track.mbid
|
||||||
|
? `https://musicbrainz.org/recording/${props.track.mbid}`
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
|
||||||
|
const upload = computed(() => props.track.uploads?.[0] ?? null)
|
||||||
|
|
||||||
|
const license = ref()
|
||||||
|
const fetchLicense = async (licenseId: string) => {
|
||||||
|
license.value = undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`licenses/${licenseId}`)
|
||||||
|
license.value = response.data
|
||||||
|
} catch (error) {
|
||||||
|
// TODO (wvffle): Handle error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (props.track.license) {
|
||||||
|
// @ts-expect-error For some reason, track.license is id instead of License here
|
||||||
|
fetchLicense(props.track.license)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div v-if="track">
|
<div v-if="track">
|
||||||
<section class="ui vertical stripe segment">
|
<section class="ui vertical stripe segment">
|
||||||
|
@ -24,7 +70,7 @@
|
||||||
>
|
>
|
||||||
<h3 class="ui header">
|
<h3 class="ui header">
|
||||||
<translate
|
<translate
|
||||||
v-if="track.artist.content_category === 'music'"
|
v-if="track.artist?.content_category === 'music'"
|
||||||
translate-context="Content/*/*"
|
translate-context="Content/*/*"
|
||||||
>
|
>
|
||||||
Track Details
|
Track Details
|
||||||
|
@ -148,8 +194,8 @@
|
||||||
</translate>
|
</translate>
|
||||||
</td>
|
</td>
|
||||||
<td class="right aligned">
|
<td class="right aligned">
|
||||||
<router-link :to="{name: 'library.artists.detail', params: {id: track.artist.id}}">
|
<router-link :to="{name: 'library.artists.detail', params: {id: track.artist?.id}}">
|
||||||
{{ track.artist.name }}
|
{{ track.artist?.name }}
|
||||||
</router-link>
|
</router-link>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -182,7 +228,7 @@
|
||||||
</td>
|
</td>
|
||||||
<td class="right aligned">
|
<td class="right aligned">
|
||||||
<template v-if="track.album && track.album.release_date">
|
<template v-if="track.album && track.album.release_date">
|
||||||
{{ momentFormat(track.album.release_date, 'Y') }}
|
{{ momentFormat(new Date(track.album.release_date), 'Y') }}
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<translate translate-context="*/*/*">
|
<translate translate-context="*/*/*">
|
||||||
|
@ -273,7 +319,7 @@
|
||||||
</translate>
|
</translate>
|
||||||
</h2>
|
</h2>
|
||||||
<library-widget
|
<library-widget
|
||||||
:url="'tracks/' + id + '/libraries/'"
|
:url="`tracks/${track.id}/libraries/`"
|
||||||
@loaded="$emit('libraries-loaded', $event)"
|
@loaded="$emit('libraries-loaded', $event)"
|
||||||
>
|
>
|
||||||
<translate translate-context="Content/Track/Paragraph">
|
<translate translate-context="Content/Track/Paragraph">
|
||||||
|
@ -285,88 +331,3 @@
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
|
||||||
import axios from 'axios'
|
|
||||||
import LibraryWidget from '~/components/federation/LibraryWidget.vue'
|
|
||||||
import TagsList from '~/components/tags/List.vue'
|
|
||||||
import PlaylistWidget from '~/components/playlists/Widget.vue'
|
|
||||||
import { humanSize, momentFormat, truncate } from '~/utils/filters'
|
|
||||||
import time from '~/utils/time'
|
|
||||||
|
|
||||||
export default {
|
|
||||||
components: {
|
|
||||||
LibraryWidget,
|
|
||||||
TagsList,
|
|
||||||
PlaylistWidget
|
|
||||||
},
|
|
||||||
props: {
|
|
||||||
track: { type: Object, required: true },
|
|
||||||
libraries: { type: Array, default: null }
|
|
||||||
},
|
|
||||||
setup () {
|
|
||||||
return { humanSize, momentFormat, time, truncate }
|
|
||||||
},
|
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
id: this.track.id,
|
|
||||||
licenseData: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
computed: {
|
|
||||||
labels () {
|
|
||||||
return {
|
|
||||||
title: this.$pgettext('*/*/*/Noun', 'Track')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
musicbrainzUrl () {
|
|
||||||
if (this.track.mbid) {
|
|
||||||
return 'https://musicbrainz.org/recording/' + this.track.mbid
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
upload () {
|
|
||||||
if (this.track.uploads) {
|
|
||||||
return this.track.uploads[0]
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
},
|
|
||||||
license () {
|
|
||||||
if (!this.track || !this.track.license) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
return this.licenseData
|
|
||||||
},
|
|
||||||
cover () {
|
|
||||||
if (this.track.cover && this.track.cover.urls.original) {
|
|
||||||
return this.track.cover
|
|
||||||
}
|
|
||||||
if (this.track.album && this.track.album.cover) {
|
|
||||||
return this.track.album.cover
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
watch: {
|
|
||||||
track (v) {
|
|
||||||
if (v && v.license) {
|
|
||||||
this.fetchLicenseData(v.license)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
created () {
|
|
||||||
if (this.track && this.track.license) {
|
|
||||||
this.fetchLicenseData(this.track.license)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
fetchLicenseData (licenseId) {
|
|
||||||
const self = this
|
|
||||||
const url = `licenses/${licenseId}`
|
|
||||||
axios.get(url).then(response => {
|
|
||||||
self.licenseData = response.data
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { Album, Artist, Content, Track } from '~/types'
|
import type { Album, Artist, Content, Track, Actor } from '~/types'
|
||||||
|
|
||||||
import { gettext } from '~/init/locale'
|
import { gettext } from '~/init/locale'
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ export interface EditableConfigField extends ConfigField {
|
||||||
id: EditObjectType
|
id: EditObjectType
|
||||||
}
|
}
|
||||||
|
|
||||||
export type EditObject = Artist | Album | Track
|
export type EditObject = (Partial<Artist> | Partial<Album> | Partial<Track>) & { attributed_to: Actor }
|
||||||
export type EditObjectType = 'artist' | 'album' | 'track'
|
export type EditObjectType = 'artist' | 'album' | 'track'
|
||||||
type Configs = Record<EditObjectType, { fields: (EditableConfigField|ConfigField)[] }>
|
type Configs = Record<EditObjectType, { fields: (EditableConfigField|ConfigField)[] }>
|
||||||
|
|
||||||
|
|
|
@ -96,6 +96,7 @@ export interface Track {
|
||||||
license?: License
|
license?: License
|
||||||
tags: string[]
|
tags: string[]
|
||||||
uploads: Upload[]
|
uploads: Upload[]
|
||||||
|
downloads_count: number
|
||||||
|
|
||||||
album?: Album
|
album?: Album
|
||||||
artist?: Artist
|
artist?: Artist
|
||||||
|
@ -277,6 +278,7 @@ export interface Upload {
|
||||||
mimetype: string
|
mimetype: string
|
||||||
extension: string
|
extension: string
|
||||||
listen_url: string
|
listen_url: string
|
||||||
|
bitrate?: number
|
||||||
size?: number
|
size?: number
|
||||||
|
|
||||||
import_status: ImportStatus
|
import_status: ImportStatus
|
||||||
|
|
Loading…
Reference in New Issue