enhancement(front):use paginated page for playlist
fix(front): enable more link on artist and user profiles refactor(front): use new ui components in track modal and track-mobile-row, render track-modal only once in parent and populate with data from mobile-row
This commit is contained in:
parent
0d9c5e77c4
commit
b063a3616e
|
@ -1,7 +1,7 @@
|
||||||
openapi: 3.0.3
|
openapi: 3.0.3
|
||||||
info:
|
info:
|
||||||
title: Funkwhale API
|
title: Funkwhale API
|
||||||
version: 1.4.0
|
version: 2.0.0a1
|
||||||
description: |
|
description: |
|
||||||
# Funkwhale API
|
# Funkwhale API
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ from django.db.models import Count
|
||||||
from drf_spectacular.utils import extend_schema
|
from drf_spectacular.utils import extend_schema
|
||||||
from rest_framework import exceptions, mixins, status, viewsets
|
from rest_framework import exceptions, mixins, status, viewsets
|
||||||
from rest_framework.decorators import action
|
from rest_framework.decorators import action
|
||||||
|
from rest_framework.pagination import PageNumberPagination
|
||||||
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
|
from rest_framework.parsers import FormParser, JSONParser, MultiPartParser
|
||||||
from rest_framework.renderers import JSONRenderer
|
from rest_framework.renderers import JSONRenderer
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
|
@ -138,9 +139,15 @@ class PlaylistViewSet(
|
||||||
plugins.TRIGGER_THIRD_PARTY_UPLOAD,
|
plugins.TRIGGER_THIRD_PARTY_UPLOAD,
|
||||||
track=plt.track,
|
track=plt.track,
|
||||||
)
|
)
|
||||||
serializer = serializers.PlaylistTrackSerializer(plts, many=True)
|
|
||||||
data = {"count": len(plts), "results": serializer.data}
|
# Apply pagination
|
||||||
return Response(data, status=200)
|
paginator = PageNumberPagination()
|
||||||
|
paginator.page_size = 100 # Set the page size (number of items per page)
|
||||||
|
paginated_plts = paginator.paginate_queryset(plts, request)
|
||||||
|
|
||||||
|
# Serialize the paginated data
|
||||||
|
serializer = serializers.PlaylistTrackSerializer(paginated_plts, many=True)
|
||||||
|
return paginator.get_paginated_response(serializer.data)
|
||||||
|
|
||||||
@extend_schema(
|
@extend_schema(
|
||||||
operation_id="add_to_playlist", request=serializers.PlaylistAddManySerializer
|
operation_id="add_to_playlist", request=serializers.PlaylistAddManySerializer
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types'
|
import type { Track, Artist, Album, Playlist, Library, Channel, Actor } from '~/types'
|
||||||
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
|
import type { PlayOptionsProps } from '~/composables/audio/usePlayOptions'
|
||||||
|
|
||||||
import { ref, computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
import { usePlayer } from '~/composables/audio/player'
|
import { usePlayer } from '~/composables/audio/player'
|
||||||
|
@ -12,9 +12,10 @@ import { useStore } from '~/store'
|
||||||
import usePlayOptions from '~/composables/audio/usePlayOptions'
|
import usePlayOptions from '~/composables/audio/usePlayOptions'
|
||||||
|
|
||||||
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
|
||||||
import TrackModal from '~/components/audio/track/Modal.vue'
|
|
||||||
import { generateTrackCreditString } from '~/utils/utils'
|
import { generateTrackCreditString } from '~/utils/utils'
|
||||||
|
|
||||||
|
import Button from '~/components/ui/Button.vue'
|
||||||
|
|
||||||
interface Props extends PlayOptionsProps {
|
interface Props extends PlayOptionsProps {
|
||||||
track: Track
|
track: Track
|
||||||
index: number
|
index: number
|
||||||
|
@ -25,7 +26,6 @@ interface Props extends PlayOptionsProps {
|
||||||
|
|
||||||
// TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
|
// TODO(wvffle): Remove after https://github.com/vuejs/core/pull/4512 is merged
|
||||||
isPlayable?: boolean
|
isPlayable?: boolean
|
||||||
tracks?: Track[]
|
|
||||||
artist?: Artist | null
|
artist?: Artist | null
|
||||||
album?: Album | null
|
album?: Album | null
|
||||||
playlist?: Playlist | null
|
playlist?: Playlist | null
|
||||||
|
@ -39,7 +39,6 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
isArtist: false,
|
isArtist: false,
|
||||||
isAlbum: false,
|
isAlbum: false,
|
||||||
|
|
||||||
tracks: () => [],
|
|
||||||
artist: null,
|
artist: null,
|
||||||
album: null,
|
album: null,
|
||||||
playlist: null,
|
playlist: null,
|
||||||
|
@ -48,7 +47,9 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
account: null
|
account: null
|
||||||
})
|
})
|
||||||
|
|
||||||
const showTrackModal = ref(false)
|
const emit = defineEmits<{
|
||||||
|
(e: 'open-modal', track: Track, index: number): void
|
||||||
|
}>()
|
||||||
|
|
||||||
const { currentTrack } = useQueue()
|
const { currentTrack } = useQueue()
|
||||||
const { isPlaying } = usePlayer()
|
const { isPlaying } = usePlayer()
|
||||||
|
@ -115,8 +116,10 @@ const actionsButtonLabel = computed(() => t('components.audio.track.MobileRow.bu
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<track-favorite-icon
|
||||||
v-if="store.state.auth.authenticated"
|
v-if="store.state.auth.authenticated"
|
||||||
|
ghost
|
||||||
|
tiny
|
||||||
:class="[
|
:class="[
|
||||||
'meta',
|
'meta',
|
||||||
'right',
|
'right',
|
||||||
|
@ -125,17 +128,14 @@ const actionsButtonLabel = computed(() => t('components.audio.track.MobileRow.bu
|
||||||
'mobile',
|
'mobile',
|
||||||
{ 'with-art': showArt },
|
{ 'with-art': showArt },
|
||||||
]"
|
]"
|
||||||
role="button"
|
:track="track"
|
||||||
>
|
/>
|
||||||
<track-favorite-icon
|
<!-- TODO: Replace with <PlayButton :dropdown-only="true"> after its display is fixed for mobile -->
|
||||||
class="tiny"
|
<Button
|
||||||
:border="false"
|
|
||||||
:track="track"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
role="button"
|
|
||||||
:aria-label="actionsButtonLabel"
|
:aria-label="actionsButtonLabel"
|
||||||
|
icon="bi-three-dots-vertical"
|
||||||
|
ghost
|
||||||
|
tiny
|
||||||
:class="[
|
:class="[
|
||||||
'modal-button',
|
'modal-button',
|
||||||
'right',
|
'right',
|
||||||
|
@ -144,16 +144,7 @@ const actionsButtonLabel = computed(() => t('components.audio.track.MobileRow.bu
|
||||||
'mobile',
|
'mobile',
|
||||||
{ 'with-art': showArt },
|
{ 'with-art': showArt },
|
||||||
]"
|
]"
|
||||||
@click.prevent.exact="showTrackModal = !showTrackModal"
|
@click.prevent.exact="emit('open-modal', track, index)"
|
||||||
>
|
|
||||||
<i class="ellipsis large vertical icon" />
|
|
||||||
</div>
|
|
||||||
<track-modal
|
|
||||||
v-model:show="showTrackModal"
|
|
||||||
:track="track"
|
|
||||||
:index="index"
|
|
||||||
:is-artist="isArtist"
|
|
||||||
:is-album="isAlbum"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -12,6 +12,8 @@ import { useVModel } from '@vueuse/core'
|
||||||
import { generateTrackCreditString, getArtistCoverUrl } from '~/utils/utils'
|
import { generateTrackCreditString, getArtistCoverUrl } from '~/utils/utils'
|
||||||
|
|
||||||
import Modal from '~/components/ui/Modal.vue'
|
import Modal from '~/components/ui/Modal.vue'
|
||||||
|
import Button from '~/components/ui/Button.vue'
|
||||||
|
import Layout from '~/components/ui/Layout.vue'
|
||||||
|
|
||||||
interface Events {
|
interface Events {
|
||||||
(e: 'update:show', value: boolean): void
|
(e: 'update:show', value: boolean): void
|
||||||
|
@ -92,9 +94,11 @@ const labels = computed(() => ({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<!-- TODO: Delete this file after this modal is replaced with playbutton dropdown-only popover -->
|
||||||
<Modal
|
<Modal
|
||||||
v-model="show"
|
v-model="show"
|
||||||
:title="track.title"
|
:title="track.title"
|
||||||
|
class="small"
|
||||||
>
|
>
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="ui large centered rounded image">
|
<div class="ui large centered rounded image">
|
||||||
|
@ -127,126 +131,105 @@ const labels = computed(() => ({
|
||||||
{{ generateTrackCreditString(track) }}
|
{{ generateTrackCreditString(track) }}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="ui hidden divider" />
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<div class="ui one column unstackable grid">
|
<Layout
|
||||||
<div
|
stack
|
||||||
|
no-gap
|
||||||
|
>
|
||||||
|
<Button
|
||||||
v-if="store.state.auth.authenticated && track.artist_credit?.[0].artist.content_category !== 'podcast'"
|
v-if="store.state.auth.authenticated && track.artist_credit?.[0].artist.content_category !== 'podcast'"
|
||||||
class="row"
|
full
|
||||||
|
ghost
|
||||||
|
:aria-label="favoriteButton"
|
||||||
|
:icon="isFavorite ? 'bi-heart-fill' : 'bi-heart'"
|
||||||
|
:is-active="isFavorite"
|
||||||
|
@click.stop="store.dispatch('favorites/toggle', track.id)"
|
||||||
>
|
>
|
||||||
<div
|
{{ favoriteButton }}
|
||||||
tabindex="0"
|
</Button>
|
||||||
class="column"
|
<Button
|
||||||
role="button"
|
full
|
||||||
:aria-label="favoriteButton"
|
ghost
|
||||||
@click.stop="store.dispatch('favorites/toggle', track.id)"
|
:aria-label="labels.addToQueue"
|
||||||
>
|
icon="bi-plus"
|
||||||
<i :class="[ 'heart', 'favorite-icon', { favorited: isFavorite, pink: isFavorite }, 'icon', 'track-modal', 'list-icon' ]" />
|
@click.stop.prevent="enqueue(); show = false"
|
||||||
<span class="track-modal list-item">{{ favoriteButton }}</span>
|
>
|
||||||
</div>
|
{{ labels.addToQueue }}
|
||||||
</div>
|
</Button>
|
||||||
<div class="row">
|
<Button
|
||||||
<div
|
full
|
||||||
class="column"
|
ghost
|
||||||
role="button"
|
:aria-label="labels.playNext"
|
||||||
:aria-label="labels.addToQueue"
|
icon="bi-skip-end"
|
||||||
@click.stop.prevent="enqueue(); show = false"
|
@click.stop.prevent="enqueueNext(true); show = false"
|
||||||
>
|
>
|
||||||
<i class="plus icon track-modal list-icon" />
|
{{ labels.playNext }}
|
||||||
<span class="track-modal list-item">{{ labels.addToQueue }}</span>
|
</Button>
|
||||||
</div>
|
<Button
|
||||||
</div>
|
full
|
||||||
<div class="row">
|
ghost
|
||||||
<div
|
:aria-label="labels.startRadio"
|
||||||
class="column"
|
icon="bi-rss"
|
||||||
role="button"
|
@click.stop.prevent="store.dispatch('radios/start', { type: 'similar', objectId: track.id }); show = false"
|
||||||
:aria-label="labels.playNext"
|
>
|
||||||
@click.stop.prevent="enqueueNext(true);show = false"
|
{{ labels.startRadio }}
|
||||||
>
|
</Button>
|
||||||
<i class="step forward icon track-modal list-icon" />
|
<Button
|
||||||
<span class="track-modal list-item">{{ labels.playNext }}</span>
|
full
|
||||||
</div>
|
ghost
|
||||||
</div>
|
:aria-label="labels.addToPlaylist"
|
||||||
<div class="row">
|
icon="bi-list"
|
||||||
<div
|
@click.stop="store.commit('playlists/chooseTrack', track)"
|
||||||
class="column"
|
>
|
||||||
role="button"
|
{{ labels.addToPlaylist }}
|
||||||
:aria-label="labels.startRadio"
|
</Button>
|
||||||
@click.stop.prevent="() => { store.dispatch('radios/start', { type: 'similar', objectId: track.id }); show = false }"
|
<hr>
|
||||||
>
|
<Button
|
||||||
<i class="rss icon track-modal list-icon" />
|
|
||||||
<span class="track-modal list-item">{{ labels.startRadio }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div
|
|
||||||
class="column"
|
|
||||||
role="button"
|
|
||||||
:aria-label="labels.addToPlaylist"
|
|
||||||
@click.stop="store.commit('playlists/chooseTrack', track)"
|
|
||||||
>
|
|
||||||
<i class="list icon track-modal list-icon" />
|
|
||||||
<span class="track-modal list-item">
|
|
||||||
{{ labels.addToPlaylist }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui divider" />
|
|
||||||
<div
|
|
||||||
v-if="!isAlbum && track.album"
|
v-if="!isAlbum && track.album"
|
||||||
class="row"
|
full
|
||||||
|
ghost
|
||||||
|
:aria-label="albumDetailsButton"
|
||||||
|
icon="bi-disc"
|
||||||
|
@click.prevent.exact="router.push({ name: 'library.albums.detail', params: { id: track.album?.id } })"
|
||||||
>
|
>
|
||||||
<div
|
{{ albumDetailsButton }}
|
||||||
class="column"
|
</Button>
|
||||||
role="button"
|
<template
|
||||||
:aria-label="albumDetailsButton"
|
|
||||||
@click.prevent.exact="router.push({ name: 'library.albums.detail', params: { id: track.album?.id } })"
|
|
||||||
>
|
|
||||||
<i class="compact disc icon track-modal list-icon" />
|
|
||||||
<span class="track-modal list-item">{{ albumDetailsButton }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="!isArtist"
|
v-if="!isArtist"
|
||||||
class="row"
|
|
||||||
>
|
>
|
||||||
<div
|
<Button
|
||||||
v-for="ac in track.artist_credit"
|
v-for="ac in track.artist_credit"
|
||||||
:key="ac.artist.id"
|
:key="ac.artist.id"
|
||||||
class="column"
|
full
|
||||||
role="button"
|
ghost
|
||||||
:aria-label="artistDetailsButton"
|
:aria-label="artistDetailsButton"
|
||||||
|
icon="bi-person-fill"
|
||||||
@click.prevent.exact="router.push({ name: 'library.artists.detail', params: { id: ac.artist.id } })"
|
@click.prevent.exact="router.push({ name: 'library.artists.detail', params: { id: ac.artist.id } })"
|
||||||
>
|
>
|
||||||
<i class="user icon track-modal list-icon" />
|
{{ ac.credit }}<span v-if="ac.joinphrase">{{ ac.joinphrase }}</span>
|
||||||
<span class="track-modal list-item">{{ ac.credit }}</span>
|
</Button>
|
||||||
<span v-if="ac.joinphrase">{{ ac.joinphrase }}</span>
|
</template>
|
||||||
</div>
|
<Button
|
||||||
</div>
|
full
|
||||||
<div class="row">
|
ghost
|
||||||
<div
|
:aria-label="trackDetailsButton"
|
||||||
class="column"
|
icon="bi-info-circle"
|
||||||
role="button"
|
@click.prevent.exact="router.push({ name: 'library.tracks.detail', params: { id: track.id } })"
|
||||||
:aria-label="trackDetailsButton"
|
>
|
||||||
@click.prevent.exact="router.push({ name: 'library.tracks.detail', params: { id: track.id } })"
|
{{ trackDetailsButton }}
|
||||||
>
|
</Button>
|
||||||
<i class="info icon track-modal list-icon" />
|
<hr>
|
||||||
<span class="track-modal list-item">{{ trackDetailsButton }}</span>
|
<Button
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="ui divider" />
|
|
||||||
<div
|
|
||||||
v-for="obj in getReportableObjects({ track, album: track.album, artistCredit: track.artist_credit })"
|
v-for="obj in getReportableObjects({ track, album: track.album, artistCredit: track.artist_credit })"
|
||||||
:key="obj.target.type + obj.target.id"
|
:key="obj.target.type + obj.target.id"
|
||||||
class="row"
|
full
|
||||||
|
ghost
|
||||||
|
icon="bi-share"
|
||||||
@click.stop.prevent="report(obj)"
|
@click.stop.prevent="report(obj)"
|
||||||
>
|
>
|
||||||
<div class="column">
|
{{ obj.label }}
|
||||||
<i class="share icon track-modal list-icon" />
|
</Button>
|
||||||
<span class="track-modal list-item">{{ obj.label }}</span>
|
</Layout>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -8,6 +8,8 @@ import { ref, computed } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
import TrackMobileRow from '~/components/audio/track/MobileRow.vue'
|
import TrackMobileRow from '~/components/audio/track/MobileRow.vue'
|
||||||
|
import TrackModal from '~/components/audio/track/Modal.vue'
|
||||||
|
|
||||||
import Pagination from '~/components/ui/Pagination.vue'
|
import Pagination from '~/components/ui/Pagination.vue'
|
||||||
import TrackRow from '~/components/audio/track/Row.vue'
|
import TrackRow from '~/components/audio/track/Row.vue'
|
||||||
import Input from '~/components/ui/Input.vue'
|
import Input from '~/components/ui/Input.vue'
|
||||||
|
@ -143,6 +145,16 @@ const updatePage = (page: number) => {
|
||||||
emit('page-changed', page)
|
emit('page-changed', page)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const showTrackModal = ref(false)
|
||||||
|
const modalTrack = ref<Track | null>(null)
|
||||||
|
const modalIndex = ref<number | null>(null)
|
||||||
|
|
||||||
|
function openTrackModal(track: Track, index: number) {
|
||||||
|
showTrackModal.value = true
|
||||||
|
modalTrack.value = track
|
||||||
|
modalIndex.value = index
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -251,13 +263,22 @@ const updatePage = (page: number) => {
|
||||||
:key="track.id"
|
:key="track.id"
|
||||||
:track="track"
|
:track="track"
|
||||||
:index="index"
|
:index="index"
|
||||||
:tracks="allTracks"
|
|
||||||
:show-position="showPosition"
|
:show-position="showPosition"
|
||||||
:show-art="showArt"
|
:show-art="showArt"
|
||||||
:show-duration="showDuration"
|
:show-duration="showDuration"
|
||||||
:is-artist="isArtist"
|
:is-artist="isArtist"
|
||||||
:is-album="isAlbum"
|
:is-album="isAlbum"
|
||||||
:is-podcast="isPodcast"
|
:is-podcast="isPodcast"
|
||||||
|
@open-modal="openTrackModal"
|
||||||
|
/>
|
||||||
|
<!-- TODO: Replace with <PlayButton :dropdown-only="true"> after its display is fixed for mobile -->
|
||||||
|
<track-modal
|
||||||
|
v-if="modalTrack"
|
||||||
|
v-model:show="showTrackModal"
|
||||||
|
:track="modalTrack"
|
||||||
|
:index="modalIndex ?? 0"
|
||||||
|
:is-artist="isArtist"
|
||||||
|
:is-album="isAlbum"
|
||||||
/>
|
/>
|
||||||
<Pagination
|
<Pagination
|
||||||
v-if="paginateResults"
|
v-if="paginateResults"
|
||||||
|
|
|
@ -176,7 +176,6 @@ const isOpen = useModal('artist-description').isOpen
|
||||||
class="description"
|
class="description"
|
||||||
:content="{ ...object.description, text: object.description.text ?? undefined }"
|
:content="{ ...object.description, text: object.description.text ?? undefined }"
|
||||||
:truncate-length="100"
|
:truncate-length="100"
|
||||||
:more-link="false"
|
|
||||||
/>
|
/>
|
||||||
<Spacer grow />
|
<Spacer grow />
|
||||||
<Link
|
<Link
|
||||||
|
|
|
@ -4597,7 +4597,8 @@
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"embed": "Embed",
|
"embed": "Embed",
|
||||||
"playAll": "Play all",
|
"playAll": "Play all",
|
||||||
"stopEdit": "Stop Editing"
|
"stopEdit": "Stop Editing",
|
||||||
|
"loadMoreTracks": "Load more tracks"
|
||||||
},
|
},
|
||||||
"empty": {
|
"empty": {
|
||||||
"noTracks": "There are no tracks in this playlist yet"
|
"noTracks": "There are no tracks in this playlist yet"
|
||||||
|
|
|
@ -190,7 +190,6 @@ const isOpen = useModal('artist-description').isOpen
|
||||||
class="description"
|
class="description"
|
||||||
:content="{ html: object?.summary.html || '' }"
|
:content="{ html: object?.summary.html || '' }"
|
||||||
:truncate-length="100"
|
:truncate-length="100"
|
||||||
:more-link="false"
|
|
||||||
/>
|
/>
|
||||||
<Spacer grow />
|
<Spacer grow />
|
||||||
<Link
|
<Link
|
||||||
|
|
|
@ -31,7 +31,7 @@ import useErrorHandler from '~/composables/useErrorHandler'
|
||||||
// }
|
// }
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
id: number
|
id: string
|
||||||
defaultEdit?: boolean
|
defaultEdit?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,7 +40,7 @@ const props = withDefaults(defineProps<Props>(), {
|
||||||
})
|
})
|
||||||
|
|
||||||
const store = useStore()
|
const store = useStore()
|
||||||
|
const isLoadingMoreTracks = ref(false)
|
||||||
const edit = ref(props.defaultEdit)
|
const edit = ref(props.defaultEdit)
|
||||||
const playlist = ref<Playlist | null>(null)
|
const playlist = ref<Playlist | null>(null)
|
||||||
const playlistTracks = ref<PlaylistTrack[]>([])
|
const playlistTracks = ref<PlaylistTrack[]>([])
|
||||||
|
@ -57,17 +57,24 @@ const labels = computed(() => ({
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const isLoading = ref(false)
|
const isLoading = ref(false)
|
||||||
|
const nextPage = ref<string | null>(null) // Tracks the next page URL
|
||||||
|
const previousPage = ref<string | null>(null) // Tracks the previous page URL
|
||||||
|
const totalTracks = ref<number>(0) // Total number of tracks
|
||||||
|
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
isLoading.value = true
|
isLoading.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [playlistResponse, tracksResponse] = await Promise.all([
|
const [playlistResponse, tracksResponse] = await Promise.all([
|
||||||
axios.get(`playlists/${props.id}/`),
|
axios.get(`playlists/${props.id}/`),
|
||||||
axios.get(`playlists/${props.id}/tracks/`)
|
axios.get(`playlists/${props.id}/tracks?page=1`)
|
||||||
])
|
])
|
||||||
|
|
||||||
playlist.value = playlistResponse.data
|
playlist.value = playlistResponse.data
|
||||||
fullPlaylistTracks.value = tracksResponse.data.results
|
fullPlaylistTracks.value = tracksResponse.data.results
|
||||||
|
nextPage.value = tracksResponse.data.next
|
||||||
|
previousPage.value = tracksResponse.data.previous
|
||||||
|
totalTracks.value = tracksResponse.data.count
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
useErrorHandler(error as Error)
|
useErrorHandler(error as Error)
|
||||||
}
|
}
|
||||||
|
@ -75,6 +82,25 @@ const fetchData = async () => {
|
||||||
isLoading.value = false
|
isLoading.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadMoreTracks = async () => {
|
||||||
|
if (nextPage.value) {
|
||||||
|
isLoadingMoreTracks.value = true; // Set loading state for the button
|
||||||
|
try {
|
||||||
|
const response = await axios.get(nextPage.value);
|
||||||
|
|
||||||
|
// Append new tracks to the existing list
|
||||||
|
fullPlaylistTracks.value = [...fullPlaylistTracks.value, ...response.data.results];
|
||||||
|
|
||||||
|
// Update pagination metadata
|
||||||
|
nextPage.value = response.data.next;
|
||||||
|
} catch (error) {
|
||||||
|
useErrorHandler(error as Error)
|
||||||
|
} finally {
|
||||||
|
isLoadingMoreTracks.value = false; // Reset loading state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
fetchData()
|
fetchData()
|
||||||
|
|
||||||
const images = computed(() => {
|
const images = computed(() => {
|
||||||
|
@ -113,17 +139,6 @@ const randomizedColors = computed(() => shuffleArray(bgcolors.value))
|
||||||
// return t('components.audio.ChannelCard.title', { date })
|
// return t('components.audio.ChannelCard.title', { date })
|
||||||
// })
|
// })
|
||||||
|
|
||||||
// TODO: Check if this function is still needed
|
|
||||||
// const deletePlaylist = async () => {
|
|
||||||
// try {
|
|
||||||
// await axios.delete(`playlists/${props.id}/`)
|
|
||||||
// store.dispatch('playlists/fetchOwn')
|
|
||||||
// return router.push({ path: '/library' })
|
|
||||||
// } catch (error) {
|
|
||||||
// useErrorHandler(error as Error)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// TODO: Implement shuffle
|
// TODO: Implement shuffle
|
||||||
const shuffle = () => {}
|
const shuffle = () => {}
|
||||||
</script>
|
</script>
|
||||||
|
@ -178,7 +193,6 @@ const shuffle = () => {}
|
||||||
<RenderedDescription
|
<RenderedDescription
|
||||||
:content="{ html: playlist.description }"
|
:content="{ html: playlist.description }"
|
||||||
:truncate-length="100"
|
:truncate-length="100"
|
||||||
:show-more="true"
|
|
||||||
/>
|
/>
|
||||||
<Layout
|
<Layout
|
||||||
flex
|
flex
|
||||||
|
@ -240,6 +254,14 @@ const shuffle = () => {}
|
||||||
:tracks="tracks"
|
:tracks="tracks"
|
||||||
:unique="false"
|
:unique="false"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
v-if="nextPage"
|
||||||
|
primary
|
||||||
|
:is-loading="isLoadingMoreTracks"
|
||||||
|
@click="loadMoreTracks"
|
||||||
|
>
|
||||||
|
{{ t('views.playlists.Detail.button.loadMoreTracks') }}
|
||||||
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
<Alert
|
<Alert
|
||||||
v-else-if="!isLoading"
|
v-else-if="!isLoading"
|
||||||
|
|
Loading…
Reference in New Issue