Migrate a bunch of components
This commit is contained in:
parent
0251789f82
commit
779d71abbc
|
@ -1,9 +1,159 @@
|
|||
<script setup lang="ts">
|
||||
import type { Playlist, Track, PlaylistTrack, BackendError, APIErrorResponse } from '~/types'
|
||||
|
||||
import { useStore } from '~/store'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { computed, ref } from 'vue'
|
||||
import axios from 'axios'
|
||||
import PlaylistForm from '~/components/playlists/Form.vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import { useVModels } from '@vueuse/core'
|
||||
import useQueue from '~/composables/audio/useQueue'
|
||||
|
||||
interface Props {
|
||||
playlist: Playlist | null
|
||||
playlistTracks: PlaylistTrack[]
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const emit = defineEmits(['update:playlist', 'update:playlistTracks'])
|
||||
const { playlistTracks, playlist } = useVModels(props, emit)
|
||||
|
||||
const errors = ref([] as string[])
|
||||
const duplicateTrackAddInfo = ref<APIErrorResponse | null>(null)
|
||||
const showDuplicateTrackAddConfirmation = ref(false)
|
||||
|
||||
const { tracks: queueTracks } = useQueue()
|
||||
|
||||
interface ModifiedPlaylistTrack extends PlaylistTrack {
|
||||
_id?: string
|
||||
}
|
||||
|
||||
const tracks = computed({
|
||||
get: () => playlistTracks.value.map((playlistTrack, index) => ({ ...playlistTrack, _id: `${index}-${playlistTrack.track.id}` } as ModifiedPlaylistTrack)),
|
||||
set: (playlist) => {
|
||||
playlistTracks.value = playlist.map((modifiedPlaylistTrack, index) => {
|
||||
const res = { ...modifiedPlaylistTrack, index } as ModifiedPlaylistTrack
|
||||
delete res._id
|
||||
return res as PlaylistTrack
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
const labels = computed(() => ({
|
||||
copyTitle: $pgettext('Content/Playlist/Button.Tooltip/Verb', 'Copy the current queue to this playlist')
|
||||
}))
|
||||
|
||||
const isLoading = ref(false)
|
||||
const status = computed(() => isLoading.value
|
||||
? 'loading'
|
||||
: errors.value.length
|
||||
? 'errored'
|
||||
: showDuplicateTrackAddConfirmation.value
|
||||
? 'confirmDuplicateAdd'
|
||||
: 'saved'
|
||||
)
|
||||
|
||||
const responseHandlers = {
|
||||
success () {
|
||||
errors.value = []
|
||||
showDuplicateTrackAddConfirmation.value = false
|
||||
},
|
||||
errored (error: BackendError): void {
|
||||
const { backendErrors, rawPayload } = error
|
||||
// if backendErrors isn't populated (e.g. duplicate track exceptions raised by
|
||||
// the playlist model), read directly from the response
|
||||
// TODO (wvffle): Check if such case exists after rewrite
|
||||
if (error.rawPayload?.playlist) {
|
||||
error.backendErrors = error.rawPayload.playlist as string[]
|
||||
error.rawPayload = undefined
|
||||
return this.errored(error)
|
||||
}
|
||||
|
||||
// TODO (wvffle): Test if it works
|
||||
// if (errors.length === 1 && errors[0].code === 'tracks_already_exist_in_playlist') {
|
||||
if (backendErrors.length === 1 && backendErrors[0] === 'Tracks already exist in playlist') {
|
||||
duplicateTrackAddInfo.value = rawPayload ?? null
|
||||
showDuplicateTrackAddConfirmation.value = true
|
||||
return
|
||||
}
|
||||
|
||||
errors.value = backendErrors
|
||||
}
|
||||
}
|
||||
|
||||
const store = useStore()
|
||||
const reorder = async ({ oldIndex: from, newIndex: to }: { oldIndex: number, newIndex: number }) => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
await axios.post(`playlists/${playlist.value!.id}/move/`, { from, to })
|
||||
await store.dispatch('playlists/fetchOwn')
|
||||
responseHandlers.success()
|
||||
} catch (error) {
|
||||
responseHandlers.errored(error as BackendError)
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const removePlaylistTrack = async (index: number) => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
tracks.value.splice(index, 1)
|
||||
await axios.post(`playlists/${playlist.value!.id}/remove/`, { index })
|
||||
await store.dispatch('playlists/fetchOwn')
|
||||
responseHandlers.success()
|
||||
} catch (error) {
|
||||
responseHandlers.errored(error as BackendError)
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const clearPlaylist = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
tracks.value = []
|
||||
await axios.post(`playlists/${playlist.value!.id}/clear/`)
|
||||
await store.dispatch('playlists/fetchOwn')
|
||||
responseHandlers.success()
|
||||
} catch (error) {
|
||||
responseHandlers.errored(error as BackendError)
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
const insertMany = async (insertedTracks: Track[], allowDuplicates: boolean) => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const response = await axios.post(`playlists/${playlist.value!.id}/add/`, {
|
||||
allow_duplicates: allowDuplicates,
|
||||
tracks: insertedTracks.map(track => track.id)
|
||||
})
|
||||
|
||||
tracks.value.push(...response.data.results)
|
||||
await store.dispatch('playlists/fetchOwn')
|
||||
responseHandlers.success()
|
||||
} catch (error) {
|
||||
responseHandlers.errored(error as BackendError)
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ui text container component-playlist-editor">
|
||||
<playlist-form
|
||||
:title="false"
|
||||
:playlist="playlist"
|
||||
@updated="$emit('playlist-updated', $event)"
|
||||
v-model:playlist="playlist"
|
||||
/>
|
||||
<h3 class="ui top attached header">
|
||||
<translate translate-context="Content/Playlist/Title">
|
||||
|
@ -43,14 +193,14 @@
|
|||
class="ui warning message"
|
||||
>
|
||||
<p
|
||||
v-translate="{playlist: playlist.name}"
|
||||
v-translate="{playlist: playlist?.name}"
|
||||
translate-context="Content/Playlist/Paragraph"
|
||||
>
|
||||
Some tracks in your queue are already in this playlist:
|
||||
</p>
|
||||
<ul class="ui relaxed divided list duplicate-tracks-list">
|
||||
<li
|
||||
v-for="(track, key) in duplicateTrackAddInfo.tracks"
|
||||
v-for="(track, key) in duplicateTrackAddInfo?.tracks ?? []"
|
||||
:key="key"
|
||||
class="ui item"
|
||||
>
|
||||
|
@ -74,7 +224,7 @@
|
|||
</div>
|
||||
<div class="ui bottom attached segment">
|
||||
<button
|
||||
:disabled="queueTracks.length === 0 || null"
|
||||
:disabled="queueTracks.length === 0"
|
||||
:class="['ui', {disabled: queueTracks.length === 0}, 'labeled', 'icon', 'button']"
|
||||
:title="labels.copyTitle"
|
||||
@click="insertMany(queueTracks, false)"
|
||||
|
@ -91,7 +241,7 @@
|
|||
</button>
|
||||
|
||||
<dangerous-button
|
||||
:disabled="plts.length === 0 || null"
|
||||
:disabled="tracks.length === 0"
|
||||
class="ui labeled right floated danger icon button"
|
||||
:action="clearPlaylist"
|
||||
>
|
||||
|
@ -100,9 +250,9 @@
|
|||
</translate>
|
||||
<template #modal-header>
|
||||
<p
|
||||
v-translate="{playlist: playlist.name}"
|
||||
v-translate="{playlist: playlist?.name}"
|
||||
translate-context="Popup/Playlist/Title"
|
||||
:translate-params="{playlist: playlist.name}"
|
||||
:translate-params="{playlist: playlist?.name}"
|
||||
>
|
||||
Do you want to clear the playlist "%{ playlist }"?
|
||||
</p>
|
||||
|
@ -123,7 +273,7 @@
|
|||
</template>
|
||||
</dangerous-button>
|
||||
<div class="ui hidden divider" />
|
||||
<template v-if="plts.length > 0">
|
||||
<template v-if="tracks.length > 0">
|
||||
<p>
|
||||
<translate translate-context="Content/Playlist/Paragraph/Call to action">
|
||||
Drag and drop rows to reorder tracks in the playlist
|
||||
|
@ -132,7 +282,7 @@
|
|||
<div class="table-wrapper">
|
||||
<table class="ui compact very basic unstackable table">
|
||||
<draggable
|
||||
v-model:list="plts"
|
||||
v-model="tracks"
|
||||
tag="tbody"
|
||||
item-key="_id"
|
||||
@update="reorder"
|
||||
|
@ -163,7 +313,7 @@
|
|||
<td class="right aligned">
|
||||
<button
|
||||
class="ui circular danger basic icon button"
|
||||
@click.stop="removePlt(index)"
|
||||
@click.stop="removePlaylistTrack(index)"
|
||||
>
|
||||
<i
|
||||
class="trash icon"
|
||||
|
@ -179,148 +329,3 @@
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import { computed } from 'vue'
|
||||
import axios from 'axios'
|
||||
import PlaylistForm from '~/components/playlists/Form.vue'
|
||||
|
||||
import draggable from 'vuedraggable'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
draggable,
|
||||
PlaylistForm
|
||||
},
|
||||
props: {
|
||||
playlist: { type: Object, required: true },
|
||||
playlistTracks: { type: Array, required: true }
|
||||
},
|
||||
setup (props) {
|
||||
const plts = computed(() => {
|
||||
return props.playlistTracks.map((plt, index) => ({ ...plt, _id: `${index}-${plt.track.id}` }))
|
||||
})
|
||||
|
||||
return { plts }
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
isLoading: false,
|
||||
errors: [],
|
||||
duplicateTrackAddInfo: {},
|
||||
showDuplicateTrackAddConfirmation: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
...mapState({
|
||||
queueTracks: state => state.queue.tracks
|
||||
}),
|
||||
labels () {
|
||||
return {
|
||||
copyTitle: this.$pgettext('Content/Playlist/Button.Tooltip/Verb', 'Copy the current queue to this playlist')
|
||||
}
|
||||
},
|
||||
status () {
|
||||
if (this.isLoading) {
|
||||
return 'loading'
|
||||
}
|
||||
if (this.errors.length > 0) {
|
||||
return 'errored'
|
||||
}
|
||||
if (this.showDuplicateTrackAddConfirmation) {
|
||||
return 'confirmDuplicateAdd'
|
||||
}
|
||||
return 'saved'
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
plts: {
|
||||
handler (newValue) {
|
||||
newValue.forEach((e, i) => {
|
||||
e.index = i
|
||||
})
|
||||
this.$emit('tracks-updated', newValue)
|
||||
},
|
||||
deep: true
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
success () {
|
||||
this.isLoading = false
|
||||
this.errors = []
|
||||
this.showDuplicateTrackAddConfirmation = false
|
||||
},
|
||||
errored (errors) {
|
||||
this.isLoading = false
|
||||
if (errors.length === 1 && errors[0].code === 'tracks_already_exist_in_playlist') {
|
||||
this.duplicateTrackAddInfo = errors[0]
|
||||
this.showDuplicateTrackAddConfirmation = true
|
||||
} else {
|
||||
this.errors = errors
|
||||
}
|
||||
},
|
||||
reorder ({ oldIndex, newIndex }) {
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
const url = `playlists/${this.playlist.id}/move`
|
||||
axios.post(url, { from: oldIndex, to: newIndex }).then((response) => {
|
||||
self.success()
|
||||
}, error => {
|
||||
self.errored(error.backendErrors)
|
||||
})
|
||||
},
|
||||
removePlt (index) {
|
||||
this.plts.splice(index, 1)
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
const url = `playlists/${this.playlist.id}/remove`
|
||||
axios.post(url, { index }).then((response) => {
|
||||
self.success()
|
||||
self.$store.dispatch('playlists/fetchOwn')
|
||||
}, error => {
|
||||
self.errored(error.backendErrors)
|
||||
})
|
||||
},
|
||||
clearPlaylist () {
|
||||
this.plts = []
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
const url = 'playlists/' + this.playlist.id + '/clear'
|
||||
axios.delete(url).then((response) => {
|
||||
self.success()
|
||||
self.$store.dispatch('playlists/fetchOwn')
|
||||
}, error => {
|
||||
self.errored(error.backendErrors)
|
||||
})
|
||||
},
|
||||
insertMany (tracks, allowDuplicates) {
|
||||
const self = this
|
||||
const ids = tracks.map(t => {
|
||||
return t.id
|
||||
})
|
||||
const payload = {
|
||||
tracks: ids,
|
||||
allow_duplicates: allowDuplicates
|
||||
}
|
||||
self.isLoading = true
|
||||
const url = 'playlists/' + this.playlist.id + '/add/'
|
||||
axios.post(url, payload).then((response) => {
|
||||
response.data.results.forEach(r => {
|
||||
self.plts.push(r)
|
||||
})
|
||||
self.success()
|
||||
self.$store.dispatch('playlists/fetchOwn')
|
||||
}, error => {
|
||||
// if backendErrors isn't populated (e.g. duplicate track exceptions raised by
|
||||
// the playlist model), read directly from the response
|
||||
if (error.rawPayload.playlist) {
|
||||
self.errored(error.rawPayload.playlist)
|
||||
} else {
|
||||
self.errored(error.backendErrors)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,101 @@
|
|||
<script setup lang="ts">
|
||||
import type { Playlist, PrivacyLevel, BackendError } from '~/types'
|
||||
|
||||
import $ from 'jquery'
|
||||
import axios from 'axios'
|
||||
import { useVModels, useCurrentElement } from '@vueuse/core'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useStore } from '~/store'
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import useLogger from '~/composables/useLogger'
|
||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||
|
||||
interface Props {
|
||||
title?: boolean
|
||||
create?: boolean
|
||||
playlist?: Playlist | null
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
title: true,
|
||||
create: false,
|
||||
playlist: null
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:playlist'])
|
||||
const { playlist } = useVModels(props, emit)
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
const errors = ref([] as string[])
|
||||
const success = ref(false)
|
||||
|
||||
const store = useStore()
|
||||
const name = ref(playlist.value?.name ?? '')
|
||||
const privacyLevel = ref(playlist.value?.privacy_level ?? store.state.auth.profile?.privacy_level ?? 'me')
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
const labels = computed(() => ({
|
||||
placeholder: $pgettext('Content/Playlist/Input.Placeholder', 'My awesome playlist')
|
||||
}))
|
||||
|
||||
const sharedLabels = useSharedLabels()
|
||||
const privacyLevelChoices = computed(() => [
|
||||
{
|
||||
value: 'me',
|
||||
label: sharedLabels.fields.privacy_level.choices.me
|
||||
},
|
||||
{
|
||||
value: 'instance',
|
||||
label: sharedLabels.fields.privacy_level.choices.instance
|
||||
},
|
||||
{
|
||||
value: 'everyone',
|
||||
label: sharedLabels.fields.privacy_level.choices.everyone
|
||||
}
|
||||
] as { value: PrivacyLevel, label: string }[])
|
||||
|
||||
const el = useCurrentElement()
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
// @ts-expect-error dropdown is from semantic ui
|
||||
$(el.value).find('.dropdown').dropdown()
|
||||
})
|
||||
|
||||
const isLoading = ref(false)
|
||||
const submit = async () => {
|
||||
isLoading.value = true
|
||||
success.value = false
|
||||
errors.value = []
|
||||
|
||||
try {
|
||||
const url = props.create ? 'playlists/' : `playlists/${playlist.value!.id}/`
|
||||
const method = props.create ? 'post' : 'patch'
|
||||
|
||||
const data = {
|
||||
name: name.value,
|
||||
privacy_level: privacyLevel.value
|
||||
}
|
||||
|
||||
const response = await axios.request({ method, url, data })
|
||||
success.value = true
|
||||
|
||||
if (props.create) {
|
||||
name.value = ''
|
||||
} else {
|
||||
playlist.value = response.data
|
||||
}
|
||||
|
||||
store.dispatch('playlists/fetchOwn')
|
||||
} catch (error) {
|
||||
logger.error('Error while creating playlist')
|
||||
errors.value = (error as BackendError).backendErrors
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form
|
||||
class="ui form"
|
||||
|
@ -96,100 +194,3 @@
|
|||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import $ from 'jquery'
|
||||
import axios from 'axios'
|
||||
|
||||
import useLogger from '~/composables/useLogger'
|
||||
import useSharedLabels from '~/composables/locale/useSharedLabels'
|
||||
|
||||
const logger = useLogger()
|
||||
|
||||
export default {
|
||||
props: {
|
||||
title: { type: Boolean, default: true },
|
||||
playlist: { type: Object, default: null }
|
||||
},
|
||||
setup () {
|
||||
const sharedLabels = useSharedLabels()
|
||||
return { sharedLabels }
|
||||
},
|
||||
data () {
|
||||
const d = {
|
||||
errors: [],
|
||||
success: false,
|
||||
isLoading: false
|
||||
}
|
||||
if (this.playlist) {
|
||||
d.name = this.playlist.name
|
||||
d.privacyLevel = this.playlist.privacy_level
|
||||
} else {
|
||||
d.privacyLevel = this.$store.state.auth.profile.privacy_level
|
||||
d.name = ''
|
||||
}
|
||||
return d
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
placeholder: this.$pgettext('Content/Playlist/Input.Placeholder', 'My awesome playlist')
|
||||
}
|
||||
},
|
||||
privacyLevelChoices: function () {
|
||||
return [
|
||||
{
|
||||
value: 'me',
|
||||
label: this.sharedLabels.fields.privacy_level.choices.me
|
||||
},
|
||||
{
|
||||
value: 'instance',
|
||||
label: this.sharedLabels.fields.privacy_level.choices.instance
|
||||
},
|
||||
{
|
||||
value: 'everyone',
|
||||
label: this.sharedLabels.fields.privacy_level.choices.everyone
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
$(this.$el).find('.dropdown').dropdown()
|
||||
},
|
||||
methods: {
|
||||
submit () {
|
||||
this.isLoading = true
|
||||
this.success = false
|
||||
this.errors = []
|
||||
const self = this
|
||||
const payload = {
|
||||
name: this.name,
|
||||
privacy_level: this.privacyLevel
|
||||
}
|
||||
|
||||
let promise
|
||||
let url
|
||||
if (this.playlist) {
|
||||
url = `playlists/${this.playlist.id}/`
|
||||
promise = axios.patch(url, payload)
|
||||
} else {
|
||||
url = 'playlists/'
|
||||
promise = axios.post(url, payload)
|
||||
}
|
||||
return promise.then(response => {
|
||||
self.success = true
|
||||
self.isLoading = false
|
||||
if (!self.playlist) {
|
||||
self.name = ''
|
||||
}
|
||||
self.$emit('updated', response.data)
|
||||
self.$store.dispatch('playlists/fetchOwn')
|
||||
}, error => {
|
||||
logger.error('Error while creating playlist')
|
||||
self.isLoading = false
|
||||
self.errors = error.backendErrors
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -52,7 +52,7 @@ const addToPlaylist = async (playlistId: number, allowDuplicates: boolean) => {
|
|||
lastSelectedPlaylist.value = playlistId
|
||||
|
||||
try {
|
||||
await axios.post(`playlists/${playlistId}/add`, {
|
||||
await axios.post(`playlists/${playlistId}/add/`, {
|
||||
tracks: [track.value?.id].filter(i => i),
|
||||
allow_duplicates: allowDuplicates
|
||||
})
|
||||
|
@ -106,7 +106,7 @@ const addToPlaylist = async (playlistId: number, allowDuplicates: boolean) => {
|
|||
</translate>
|
||||
</h4>
|
||||
<div class="scrolling content">
|
||||
<playlist-form :key="formKey" />
|
||||
<playlist-form :create="true" :key="formKey" />
|
||||
<div class="ui divider" />
|
||||
<div v-if="playlists.length > 0">
|
||||
<div
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import type { User } from '~/types'
|
||||
import type { Module } from 'vuex'
|
||||
import type { RootState } from '~/store/index'
|
||||
|
||||
|
@ -11,21 +12,11 @@ export interface State {
|
|||
username: string
|
||||
fullUsername: string
|
||||
availablePermissions: Record<Permission, boolean>,
|
||||
profile: null | Profile
|
||||
profile: null | User
|
||||
oauth: OAuthTokens
|
||||
scopedTokens: ScopedTokens
|
||||
}
|
||||
|
||||
interface Profile {
|
||||
id: string
|
||||
avatar?: string
|
||||
username: string
|
||||
full_username: string
|
||||
instance_support_message_display_date: string
|
||||
funkwhale_support_message_display_date: string
|
||||
is_superuser: boolean
|
||||
}
|
||||
|
||||
interface ScopedTokens {
|
||||
listen: null | string
|
||||
}
|
||||
|
@ -136,13 +127,13 @@ const store: Module<State, RootState> = {
|
|||
permission: (state, { key, status }: { key: Permission, status: boolean }) => {
|
||||
state.availablePermissions[key] = status
|
||||
},
|
||||
profilePartialUpdate: (state, payload: Profile) => {
|
||||
profilePartialUpdate: (state, payload: User) => {
|
||||
if (!state.profile) {
|
||||
state.profile = {} as Profile
|
||||
state.profile = {} as User
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(payload)) {
|
||||
state.profile[key as keyof Profile] = value as never
|
||||
state.profile[key as keyof User] = value as never
|
||||
}
|
||||
},
|
||||
oauthApp: (state, payload) => {
|
||||
|
@ -160,7 +151,7 @@ const store: Module<State, RootState> = {
|
|||
const form = useFormData(credentials)
|
||||
return axios.post('users/login', form).then(() => {
|
||||
logger.info('Successfully logged in as', credentials.username)
|
||||
dispatch('fetchProfile').then(() => {
|
||||
dispatch('fetchUser').then(() => {
|
||||
// Redirect to a specified route
|
||||
import('~/router').then((router) => {
|
||||
return router.default.push(next)
|
||||
|
@ -190,11 +181,11 @@ const store: Module<State, RootState> = {
|
|||
})
|
||||
logger.info('Log out, goodbye!')
|
||||
},
|
||||
fetchProfile ({ dispatch }) {
|
||||
fetchUser ({ dispatch }) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios.get('users/me/').then((response) => {
|
||||
logger.info('Successfully fetched user profile')
|
||||
dispatch('updateProfile', response.data)
|
||||
dispatch('updateUser', response.data)
|
||||
dispatch('ui/fetchUnreadNotifications', null, { root: true })
|
||||
if (response.data.permissions.library) {
|
||||
dispatch('ui/fetchPendingReviewEdits', null, { root: true })
|
||||
|
@ -215,7 +206,7 @@ const store: Module<State, RootState> = {
|
|||
})
|
||||
})
|
||||
},
|
||||
updateProfile ({ commit }, data) {
|
||||
updateUser ({ commit }, data) {
|
||||
commit('authenticated', true)
|
||||
commit('profile', data)
|
||||
commit('username', data.username)
|
||||
|
@ -253,7 +244,7 @@ const store: Module<State, RootState> = {
|
|||
{ headers: { 'Content-Type': 'multipart/form-data' } }
|
||||
)
|
||||
commit('oauthToken', response.data)
|
||||
await dispatch('fetchProfile')
|
||||
await dispatch('fetchUser')
|
||||
},
|
||||
async refreshOauthToken ({ state, commit }) {
|
||||
const payload = {
|
||||
|
|
|
@ -159,7 +159,24 @@ export interface License {
|
|||
export interface Playlist {
|
||||
id: string
|
||||
name: string
|
||||
modification_date: Date // TODO (wvffle): Find correct type
|
||||
modification_date: string
|
||||
user: User
|
||||
privacy_level: PrivacyLevel
|
||||
tracks_count: number
|
||||
duration: number
|
||||
|
||||
is_playable: boolean
|
||||
}
|
||||
|
||||
export interface PlaylistTrack {
|
||||
track: Track
|
||||
position?: number
|
||||
}
|
||||
|
||||
export interface Radio {
|
||||
id: string
|
||||
name: string
|
||||
user: User
|
||||
}
|
||||
|
||||
// API stuff
|
||||
|
@ -253,6 +270,7 @@ export interface FSLogs {
|
|||
|
||||
// Profile stuff
|
||||
export interface Actor {
|
||||
id: string
|
||||
fid?: string
|
||||
name?: string
|
||||
icon?: Cover
|
||||
|
@ -263,6 +281,17 @@ export interface Actor {
|
|||
domain: string
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
avatar?: string
|
||||
username: string
|
||||
full_username: string
|
||||
instance_support_message_display_date: string
|
||||
funkwhale_support_message_display_date: string
|
||||
is_superuser: boolean
|
||||
privacy_level: PrivacyLevel
|
||||
}
|
||||
|
||||
// Settings stuff
|
||||
export type SettingsId = 'instance'
|
||||
export interface SettingsGroup {
|
||||
|
|
|
@ -1,3 +1,74 @@
|
|||
<script setup lang="ts">
|
||||
import type { PlaylistTrack, Playlist } from '~/types'
|
||||
|
||||
import axios from 'axios'
|
||||
import TrackTable from '~/components/audio/track/Table.vue'
|
||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||
import PlaylistEditor from '~/components/playlists/Editor.vue'
|
||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
||||
import Modal from '~/components/semantic/Modal.vue'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useStore } from '~/store'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
defaultEdit?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
defaultEdit: false
|
||||
})
|
||||
|
||||
const store = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const edit = ref(props.defaultEdit)
|
||||
const playlist = ref<Playlist | null>(null)
|
||||
const playlistTracks = ref<PlaylistTrack[]>([])
|
||||
|
||||
const showEmbedModal = ref(false)
|
||||
|
||||
const tracks = computed(() => playlistTracks.value.map(({ track }, index) => ({ ...track, position: index + 1 })))
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
const labels = computed(() => ({
|
||||
playlist: $pgettext('*/*/*', 'Playlist')
|
||||
}))
|
||||
|
||||
const isLoading = ref(false)
|
||||
const fetchData = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
try {
|
||||
const [playlistResponse, tracksResponse] = await Promise.all([
|
||||
axios.get(`playlists/${props.id}/`),
|
||||
axios.get(`playlists/${props.id}/tracks/`),
|
||||
])
|
||||
|
||||
playlist.value = playlistResponse.data
|
||||
playlistTracks.value = tracksResponse.data.results
|
||||
} catch (error) {
|
||||
// TODO (wvffle): Handle error
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
fetchData()
|
||||
|
||||
const deletePlaylist = async () => {
|
||||
try {
|
||||
await axios.delete(`playlists/${props.id}/`)
|
||||
store.dispatch('playlists/fetchOwn')
|
||||
return router.push({ path: '/library' })
|
||||
} catch (error) {
|
||||
// TODO (wvffle): Handle error
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div
|
||||
|
@ -45,7 +116,7 @@
|
|||
</div>
|
||||
<div class="ui buttons">
|
||||
<button
|
||||
v-if="$store.state.auth.profile && playlist.user.id === $store.state.auth.profile.id"
|
||||
v-if="$store.state.auth.profile && playlist.user.id === $store.state.auth.profile?.id"
|
||||
class="ui icon labeled button"
|
||||
@click="edit = !edit"
|
||||
>
|
||||
|
@ -137,10 +208,8 @@
|
|||
<section class="ui vertical stripe segment">
|
||||
<template v-if="edit">
|
||||
<playlist-editor
|
||||
:playlist="playlist"
|
||||
:playlist-tracks="playlistTracks"
|
||||
@playlist-updated="playlist = $event"
|
||||
@tracks-updated="updatePlts"
|
||||
v-model:playlist="playlist"
|
||||
v-model:playlist-tracks="playlistTracks"
|
||||
/>
|
||||
</template>
|
||||
<template v-else-if="tracks.length > 0">
|
||||
|
@ -177,81 +246,3 @@
|
|||
</section>
|
||||
</main>
|
||||
</template>
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import TrackTable from '~/components/audio/track/Table.vue'
|
||||
import PlayButton from '~/components/audio/PlayButton.vue'
|
||||
import PlaylistEditor from '~/components/playlists/Editor.vue'
|
||||
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
|
||||
import Modal from '~/components/semantic/Modal.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PlaylistEditor,
|
||||
TrackTable,
|
||||
PlayButton,
|
||||
Modal,
|
||||
EmbedWizard
|
||||
},
|
||||
props: {
|
||||
id: { type: [Number, String], required: true },
|
||||
defaultEdit: { type: Boolean, default: false }
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
edit: this.defaultEdit,
|
||||
isLoading: false,
|
||||
playlist: null,
|
||||
tracks: [],
|
||||
playlistTracks: [],
|
||||
showEmbedModal: false
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
playlist: this.$pgettext('*/*/*', 'Playlist')
|
||||
}
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.fetch()
|
||||
},
|
||||
methods: {
|
||||
updatePlts (v) {
|
||||
this.playlistTracks = v
|
||||
this.tracks = v.map((e, i) => {
|
||||
const track = e.track
|
||||
track.position = i + 1
|
||||
return track
|
||||
})
|
||||
},
|
||||
fetch: function () {
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
const url = 'playlists/' + this.id + '/'
|
||||
axios.get(url).then(response => {
|
||||
self.playlist = response.data
|
||||
axios
|
||||
.get(url + 'tracks/')
|
||||
.then(response => {
|
||||
self.updatePlts(response.data.results)
|
||||
})
|
||||
.then(() => {
|
||||
self.isLoading = false
|
||||
})
|
||||
})
|
||||
},
|
||||
deletePlaylist () {
|
||||
const self = this
|
||||
const url = 'playlists/' + this.id + '/'
|
||||
axios.delete(url).then(response => {
|
||||
self.$store.dispatch('playlists/fetchOwn')
|
||||
self.$router.push({
|
||||
path: '/library'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,3 +1,63 @@
|
|||
<script setup lang="ts">
|
||||
import type { Track, Radio } from "~/types"
|
||||
|
||||
import axios from 'axios'
|
||||
import TrackTable from '~/components/audio/track/Table.vue'
|
||||
import RadioButton from '~/components/radios/Button.vue'
|
||||
import Pagination from '~/components/vui/Pagination.vue'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import { useGettext } from 'vue3-gettext'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
const radio = ref<Radio | null>(null)
|
||||
const tracks = ref([] as Track[])
|
||||
const totalTracks = ref(0)
|
||||
const page = ref(1)
|
||||
|
||||
const { $pgettext } = useGettext()
|
||||
const labels = computed(() => ({
|
||||
title: $pgettext('Head/Radio/Title', 'Radio')
|
||||
}))
|
||||
|
||||
const isLoading = ref(false)
|
||||
const fetchData = async () => {
|
||||
isLoading.value = true
|
||||
|
||||
const url = `radios/radios/${props.id}/`
|
||||
|
||||
try {
|
||||
const radioResponse = await axios.get(url)
|
||||
radio.value = radioResponse.data
|
||||
|
||||
const tracksResponse = await axios.get(url + 'tracks/', { params: { page: page.value }})
|
||||
totalTracks.value = tracksResponse.data.count
|
||||
tracks.value = tracksResponse.data.results
|
||||
} catch (error) {
|
||||
// TODO (wvffle): Handle error
|
||||
}
|
||||
|
||||
isLoading.value = false
|
||||
}
|
||||
|
||||
watch(page, fetchData, { immediate: true })
|
||||
|
||||
const router = useRouter()
|
||||
const deleteRadio = async () => {
|
||||
try {
|
||||
await axios.delete(`radios/radios/${props.id}/`)
|
||||
return router.push({ path: '/library' })
|
||||
} catch (error) {
|
||||
// TODO (wvffle): Handle error
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<main>
|
||||
<div
|
||||
|
@ -81,10 +141,9 @@
|
|||
<div class="ui center aligned basic segment">
|
||||
<pagination
|
||||
v-if="totalTracks > 25"
|
||||
:current="page"
|
||||
v-model:current="page"
|
||||
:paginate-by="25"
|
||||
:total="totalTracks"
|
||||
@page-changed="selectPage"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
@ -101,9 +160,9 @@
|
|||
</translate>
|
||||
</div>
|
||||
<router-link
|
||||
v-if="$store.state.auth.username === radio.user.username"
|
||||
v-if="$store.state.auth.username === radio?.user.username"
|
||||
class="ui success icon labeled button"
|
||||
:to="{name: 'library.radios.edit', params: {id: radio.id}}"
|
||||
:to="{name: 'library.radios.edit', params: { id: radio?.id }}"
|
||||
>
|
||||
<i class="pencil icon" />
|
||||
Edit…
|
||||
|
@ -111,76 +170,3 @@
|
|||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
import TrackTable from '~/components/audio/track/Table.vue'
|
||||
import RadioButton from '~/components/radios/Button.vue'
|
||||
import Pagination from '~/components/vui/Pagination.vue'
|
||||
|
||||
export default {
|
||||
components: {
|
||||
TrackTable,
|
||||
RadioButton,
|
||||
Pagination
|
||||
},
|
||||
props: {
|
||||
id: { type: Number, required: true }
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
isLoading: false,
|
||||
radio: null,
|
||||
tracks: [],
|
||||
totalTracks: 0,
|
||||
page: 1
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
labels () {
|
||||
return {
|
||||
title: this.$pgettext('Head/Radio/Title', 'Radio')
|
||||
}
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page: function () {
|
||||
this.fetch()
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
this.fetch()
|
||||
},
|
||||
methods: {
|
||||
selectPage: function (page) {
|
||||
this.page = page
|
||||
},
|
||||
fetch: function () {
|
||||
const self = this
|
||||
self.isLoading = true
|
||||
const url = 'radios/radios/' + this.id + '/'
|
||||
axios.get(url).then(response => {
|
||||
self.radio = response.data
|
||||
axios
|
||||
.get(url + 'tracks/', { params: { page: this.page } })
|
||||
.then(response => {
|
||||
this.totalTracks = response.data.count
|
||||
this.tracks = response.data.results
|
||||
})
|
||||
.then(() => {
|
||||
self.isLoading = false
|
||||
})
|
||||
})
|
||||
},
|
||||
deleteRadio () {
|
||||
const self = this
|
||||
const url = 'radios/radios/' + this.id + '/'
|
||||
axios.delete(url).then(response => {
|
||||
self.$router.push({
|
||||
path: '/library'
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
Loading…
Reference in New Issue