refactor(front): use section with title prop and pagination on all widgets

This commit is contained in:
ArneBo 2025-02-19 13:35:45 +01:00
parent e09d0a20fa
commit 2e63cad388
10 changed files with 288 additions and 312 deletions

View File

@ -8,14 +8,15 @@ import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
import AlbumCard from '~/components/album/Card.vue' import usePage from '~/composables/navigation/usePage'
import Button from '~/components/ui/Button.vue'
import Layout from '~/components/ui/Layout.vue'
import Spacer from '~/components/ui/Spacer.vue'
import Loader from '~/components/ui/Loader.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
import AlbumCard from '~/components/album/Card.vue'
import Section from '~/components/ui/Section.vue'
import Loader from '~/components/ui/Loader.vue'
import Pagination from '~/components/ui/Pagination.vue'
const { t } = useI18n() const { t } = useI18n()
interface Props { interface Props {
@ -23,6 +24,7 @@ interface Props {
showCount?: boolean showCount?: boolean
search?: boolean search?: boolean
limit?: number limit?: number
title?: string
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -36,6 +38,7 @@ const store = useStore()
const query = ref('') const query = ref('')
const albums = reactive([] as Album[]) const albums = reactive([] as Album[])
const count = ref(0) const count = ref(0)
const page = usePage()
const nextPage = ref() const nextPage = ref()
const isLoading = ref(false) const isLoading = ref(false)
@ -46,6 +49,7 @@ const fetchData = async (url = 'albums/') => {
const params = { const params = {
q: query.value, q: query.value,
...props.filters, ...props.filters,
page: page.value,
page_size: props.limit page_size: props.limit
} }
@ -60,44 +64,39 @@ const fetchData = async (url = 'albums/') => {
isLoading.value = false isLoading.value = false
} }
setTimeout(fetchData, 1000)
const performSearch = () => { const performSearch = () => {
albums.length = 0 albums.length = 0
fetchData() fetchData()
} }
watch( watch(
() => store.state.moderation.lastUpdate, [() => store.state.moderation.lastUpdate, page],
() => fetchData(), () => fetchData(),
{ immediate: true } { immediate: true }
) )
</script> </script>
<template> <template>
<div class="album-widget"> <Section
<h2 :h2="title"
v-if="!!$slots.title" align-left
> small-items
<slot name="title" /> >
<span
v-if="showCount"
class="ui tiny circular label"
>{{ count }}</span>
</h2>
<slot />
<inline-search-bar <inline-search-bar
v-if="search" v-if="search"
style="grid-column: 1 / -1;"
v-model="query" v-model="query"
@search="performSearch" @search="performSearch"
/> />
<Loader v-if="isLoading" /> <Loader v-if="isLoading" />
<template v-if="!isLoading && albums.length > 0"> <template v-if="!isLoading && albums.length > 0">
<Layout flex>
<album-card <album-card
v-for="album in albums" v-for="album in albums"
:key="album.id" :key="album.id"
:album="album" :album="album"
/> />
</Layout>
</template> </template>
<slot <slot
v-if="!isLoading && albums.length === 0" v-if="!isLoading && albums.length === 0"
@ -106,17 +105,14 @@ watch(
<empty-state <empty-state
:refresh="true" :refresh="true"
@refresh="fetchData" @refresh="fetchData"
style="grid-column: 1 / -1;"
/> />
</slot> </slot>
<template v-if="nextPage"> <Pagination
<Spacer /> v-if="albums && count > props.limit"
<Button v-model:page="page"
v-if="nextPage" :pages="Math.ceil((count || 0) / props.limit)"
primary style="grid-column: 1 / -1;"
@click="fetchData(nextPage)" />
> </Section>
{{ t('components.audio.album.Widget.button.more') }}
</Button>
</template>
</div>
</template> </template>

View File

@ -1,25 +1,26 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Artist } from '~/types' import type { Artist } from '~/types'
import { reactive, ref, watch } from 'vue' import { reactive, ref, watch, onMounted } from 'vue'
import { useStore } from '~/store' import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
import ArtistCard from '~/components/artist/Card.vue'
import Button from '~/components/ui/Button.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
import usePage from '~/composables/navigation/usePage'
import ArtistCard from '~/components/artist/Card.vue'
import Section from '~/components/ui/Section.vue'
import Pagination from '~/components/ui/Pagination.vue'
const { t } = useI18n()
interface Props { interface Props {
filters: Record<string, string | boolean> filters: Record<string, string | boolean>
search?: boolean search?: boolean
header?: boolean header?: boolean
limit?: number limit?: number
title?: string
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
@ -33,6 +34,7 @@ const store = useStore()
const query = ref('') const query = ref('')
const artists = reactive([] as Artist[]) const artists = reactive([] as Artist[])
const count = ref(0) const count = ref(0)
const page = usePage()
const nextPage = ref() const nextPage = ref()
const isLoading = ref(false) const isLoading = ref(false)
@ -43,6 +45,7 @@ const fetchData = async (url = 'artists/') => {
const params = { const params = {
q: query.value, q: query.value,
...props.filters, ...props.filters,
page: page.value,
page_size: props.limit page_size: props.limit
} }
@ -57,46 +60,31 @@ const fetchData = async (url = 'artists/') => {
isLoading.value = false isLoading.value = false
} }
onMounted(() => {
setTimeout(fetchData, 1000)
})
const performSearch = () => { const performSearch = () => {
artists.length = 0 artists.length = 0
fetchData() fetchData()
} }
watch( watch(
() => store.state.moderation.lastUpdate, [() => store.state.moderation.lastUpdate, page],
() => fetchData(), () => fetchData(),
{ immediate: true } { immediate: true }
) )
</script> </script>
<template> <template>
<div class="wrapper"> <Section
<h3 align-left
v-if="header" :h2="title"
class="ui header" >
> <Loader
<slot name="title" /> v-if="isLoading"
<span class="ui tiny circular label">{{ count }}</span> style="grid-column: 1 / -1;"
</h3>
<inline-search-bar
v-if="search"
v-model="query"
@search="performSearch"
/> />
<div class="ui hidden divider" />
<div style="display:flex; flex-wrap:wrap; gap: 32px; margin-top:32px;">
<div
v-if="isLoading"
class="ui inverted active dimmer"
>
<div class="ui loader" />
</div>
<artist-card
v-for="artist in artists"
:key="artist.id"
:artist="artist"
/>
</div>
<slot <slot
v-if="!isLoading && artists.length === 0" v-if="!isLoading && artists.length === 0"
name="empty-state" name="empty-state"
@ -106,14 +94,21 @@ watch(
@refresh="fetchData" @refresh="fetchData"
/> />
</slot> </slot>
<template v-if="nextPage"> <inline-search-bar
<div class="ui hidden divider" /> v-if="!isLoading && search"
<Button v-model="query"
v-if="nextPage" @search="performSearch"
@click="fetchData(nextPage)" style="grid-column: 1 / -1;"
> />
{{ t('components.audio.artist.Widget.button.more') }} <artist-card
</Button> v-for="artist in artists"
</template> :key="artist.id"
</div> :artist="artist"
/>
<Pagination
v-if="artists && count > limit"
v-model:page="page"
:pages="Math.ceil((count || 0) / limit)"
/>
</Section>
</template> </template>

View File

@ -59,33 +59,39 @@ const fetchData = async (url = 'channels/') => {
} }
onMounted(() => { onMounted(() => {
fetchData() setTimeout(fetchData, 1000)
}) })
watch(() => [props.filters, page], () => { watch([() => props.filters, page],
fetchData() () => fetchData(),
}, { deep: true }) { deep: true }
)
</script> </script>
<template> <template>
<Section aling-left :h2="title" class="channel-widget"> <Section
<slot /> align-left
<Loader v-if="isLoading" /> :h2="title"
>
<Loader v-if="isLoading" style="grid-column: 1 / -1;" />
<template
v-if="!isLoading && channels.length === 0"
style="grid-column: 1 / -1;"
>
<empty-state
:refresh="true"
@refresh="fetchData('channels/')"
/>
</template>
<channel-card <channel-card
v-for="object in channels" v-for="object in channels"
:key="object.uuid" :key="object.uuid"
:object="object" :object="object"
/> />
</Section> <Pagination
<template v-if="!isLoading && channels.length === 0"> v-if="channels && count > limit"
<empty-state v-model:page="page"
:refresh="true" :pages="Math.ceil((count || 0) / limit)"
@refresh="fetchData('channels/')"
/> />
</template> </Section>
<Pagination
v-if="channels && count > limit"
v-model:page="page"
:pages="Math.ceil((count || 0) / limit)"
/>
</template> </template>

View File

@ -8,9 +8,9 @@ import { useI18n } from 'vue-i18n'
import { getArtistCoverUrl } from '~/utils/utils' import { getArtistCoverUrl } from '~/utils/utils'
import axios from 'axios' import axios from 'axios'
import usePage from '~/composables/navigation/usePage'
import useWebSocketHandler from '~/composables/useWebSocketHandler' import useWebSocketHandler from '~/composables/useWebSocketHandler'
import Button from '~/components/ui/Button.vue'
import PlayButton from '~/components/audio/PlayButton.vue' import PlayButton from '~/components/audio/PlayButton.vue'
import TagsList from '~/components/tags/List.vue' import TagsList from '~/components/tags/List.vue'
import Section from '~/components/ui/Section.vue' import Section from '~/components/ui/Section.vue'
@ -18,6 +18,7 @@ import Alert from '~/components/ui/Alert.vue'
import Spacer from '~/components/ui/Spacer.vue' import Spacer from '~/components/ui/Spacer.vue'
import Loader from '~/components/ui/Loader.vue' import Loader from '~/components/ui/Loader.vue'
import Heading from '~/components/ui/Heading.vue' import Heading from '~/components/ui/Heading.vue'
import Pagination from '~/components/ui/Pagination.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
@ -30,16 +31,15 @@ interface Props {
filters: Record<string, string | boolean> filters: Record<string, string | boolean>
url: string url: string
isActivity?: boolean isActivity?: boolean
showCount?: boolean
limit?: number limit?: number
itemClasses?: string itemClasses?: string
websocketHandlers?: string[] websocketHandlers?: string[]
title?: string
} }
const emit = defineEmits<Events>() const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
isActivity: true, isActivity: true,
showCount: false,
limit: 9, limit: 9,
itemClasses: '', itemClasses: '',
websocketHandlers: () => [] websocketHandlers: () => []
@ -50,7 +50,7 @@ const { t } = useI18n()
const objects = reactive([] as Listening[]) const objects = reactive([] as Listening[])
const count = ref(0) const count = ref(0)
const nextPage = ref<string | null>(null) const page = usePage()
const isLoading = ref(false) const isLoading = ref(false)
@ -59,12 +59,12 @@ const fetchData = async (url = props.url) => {
const params = { const params = {
...clone(props.filters), ...clone(props.filters),
page_size: props.limit page: page.value,
page_size: props.limit ?? 9
} }
try { try {
const response = await axios.get(url, { params }) const response = await axios.get(url, { params })
nextPage.value = response.data.next
count.value = response.data.count count.value = response.data.count
const newObjects = !props.isActivity const newObjects = !props.isActivity
@ -80,11 +80,11 @@ const fetchData = async (url = props.url) => {
} }
onMounted(() => { onMounted(() => {
fetchData() setTimeout(fetchData, 1000)
}) })
watch( watch(
() => store.state.moderation.lastUpdate, [() => store.state.moderation.lastUpdate, page],
() => fetchData(), () => fetchData(),
{ immediate: true } { immediate: true }
) )
@ -113,18 +113,17 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
</script> </script>
<template> <template>
<!-- TODO: Use activity.vue --> <Section
<div class="track-widget"> :h2="title"
<h2 v-if="!!$slots.title"> medium-items
<slot name="title" /> align-left
<span >
v-if="showCount" <Loader v-if="isLoading"
class="ui tiny circular label" style="grid-column: 1 / -1;"
>{{ count }}</span> />
</h2>
<Spacer :size="8" />
<Alert <Alert
v-if="!isLoading && count === 0" v-if="!isLoading && count === 0"
style="grid-column: 1 / -1;"
blue blue
align-items="center" align-items="center"
> >
@ -133,103 +132,94 @@ watch(() => props.websocketHandlers.includes('Listen'), (to) => {
{{ t('components.audio.track.Widget.empty.noResults') }} {{ t('components.audio.track.Widget.empty.noResults') }}
</h4> </h4>
</Alert> </Alert>
<Section <!-- TODO: Use activity.vue -->
<div class="funkwhale activity"
v-if="count > 0" v-if="count > 0"
medium-items v-for="object in objects"
alignLeft :key="object.id"
> :class="['item', itemClasses]">
<div class="funkwhale activity" <div class="activity-image">
v-for="object in objects" <img
:key="object.id" v-if="object.track.album && object.track.album.cover"
:class="['item', itemClasses]" v-lazy="store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)"
> alt=""
<div class="activity-image"> >
<img <img
v-if="object.track.album && object.track.album.cover" v-else-if="object.track.cover"
v-lazy="store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](object.track.cover.urls.medium_square_crop)"
alt="" alt=""
> >
<img <img
v-else-if="object.track.cover" v-else-if="object.track.artist_credit && object.track.artist_credit.length > 1"
v-lazy="store.getters['instance/absoluteUrl'](object.track.cover.urls.medium_square_crop)" v-lazy="getArtistCoverUrl(object.track.artist_credit)"
alt="" alt=""
> >
<img <i
v-else-if="object.track.artist_credit && object.track.artist_credit.length > 1" v-else
v-lazy="getArtistCoverUrl(object.track.artist_credit)" class="bi bi-vinyl-fill"
alt=""
>
<i
v-else
class="bi bi-vinyl-fill"
/>
<!-- TODO: Add Playbutton overlay -->
</div>
<div class="activity-content">
<router-link
class="funkwhale link artist"
:to="{name: 'library.tracks.detail', params: {id: object.track.id}}"
>
<Heading :h3="object.track.title" title />
</router-link>
<Spacer :size="2"/>
<div
v-if="object.track.artist_credit"
class="funkwhale link artist"
>
<span
v-for="ac in object.track.artist_credit"
:key="ac.artist.id"
>
<router-link
class="discrete link"
:to="{ name: 'library.artists.detail', params: { id: ac.artist.id } }"
>
{{ ac.credit }}
</router-link>
<span v-if="ac.joinphrase">{{ ac.joinphrase }}</span>
</span>
</div>
<TagsList
label-classes="tiny"
:truncate-size="20"
:limit="2"
:show-more="false"
:tags="object.track.tags"
/>
<Spacer :size="4"/>
<div
v-if="isActivity"
class="extra"
>
<router-link
class="funkwhale link user"
:to="{name: 'profile.overview', params: {username: object.actor.name}}"
>
<span class="at symbol" />{{ object.actor.name }}
</router-link>
<span class="right floated"><human-date :date="object.creation_date" /></span>
</div>
</div>
<play-button
:account="object.actor"
:dropdown-only="true"
:track="object.track"
square-small
/> />
<!-- TODO: Add Playbutton overlay -->
</div> </div>
</Section> <div class="activity-content">
<Loader v-if="isLoading" /> <router-link
<template v-if="nextPage"> class="funkwhale link artist"
<Spacer /> :to="{name: 'library.tracks.detail', params: {id: object.track.id}}"
<Button >
primary <Heading :h3="object.track.title" title />
@click="fetchData(nextPage as string)" </router-link>
> <Spacer :size="2"/>
{{ t('components.audio.track.Widget.button.more') }} <div
</Button> v-if="object.track.artist_credit"
</template> class="funkwhale link artist"
</div> >
<span
v-for="ac in object.track.artist_credit"
:key="ac.artist.id"
>
<router-link
class="discrete link"
:to="{ name: 'library.artists.detail', params: { id: ac.artist.id } }"
>
{{ ac.credit }}
</router-link>
<span v-if="ac.joinphrase">{{ ac.joinphrase }}</span>
</span>
</div>
<TagsList
label-classes="tiny"
:truncate-size="20"
:limit="2"
:show-more="false"
:tags="object.track.tags"
/>
<Spacer :size="4"/>
<div
v-if="isActivity"
class="extra"
>
<router-link
class="funkwhale link user"
:to="{name: 'profile.overview', params: {username: object.actor.name}}"
>
<span class="at symbol" />{{ object.actor.name }}
</router-link>
<span class="right floated"><human-date :date="object.creation_date" /></span>
</div>
</div>
<play-button
:account="object.actor"
:dropdown-only="true"
:track="object.track"
square-small
/>
</div>
<Pagination
v-if="count > props.limit"
v-model:page="page"
:pages="Math.ceil((count || 0) / props.limit)"
style="grid-column: 1 / -1;"
/>
</Section>
</template> </template>

View File

@ -9,9 +9,8 @@ import axios from 'axios'
import LibraryCard from '~/views/content/remote/Card.vue' import LibraryCard from '~/views/content/remote/Card.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import Layout from '~/components/ui/Layout.vue' import Section from '~/components/ui/Section.vue'
import Loader from '~/components/ui/Loader.vue' import Loader from '~/components/ui/Loader.vue'
import Spacer from '~/components/ui/Spacer.vue'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
@ -21,6 +20,7 @@ interface Events {
interface Props { interface Props {
url: string url: string
title?: string
} }
const { t } = useI18n() const { t } = useI18n()
@ -53,7 +53,7 @@ const fetchData = async (url = props.url) => {
} }
onMounted(() => { onMounted(() => {
fetchData() setTimeout(fetchData, 1000)
}) })
watch(() => props.url, () => { watch(() => props.url, () => {
@ -62,37 +62,34 @@ watch(() => props.url, () => {
</script> </script>
<template> <template>
<div class="wrapper"> <Section
<h3 :h2="title"
v-if="!!$slots.title" align-left
class="ui header" small-items
> >
<slot name="title" /> <Loader v-if="isLoading" style="grid-column: 1 / -1;" />
</h3>
<p <p
v-if="!isLoading && libraries.length > 0" v-if="!isLoading && libraries.length > 0"
style="grid-column: 1 / -1;"
class="ui subtitle" class="ui subtitle"
> >
<slot /> <slot />
</p> </p>
<p <p
v-if="!isLoading && libraries.length === 0" v-if="!isLoading && libraries.length === 0"
style="grid-column: 1 / -1;"
class="ui subtitle" class="ui subtitle"
> >
{{ t('components.federation.LibraryWidget.empty.noMatch') }} {{ t('components.federation.LibraryWidget.empty.noMatch') }}
</p> </p>
<div class="ui hidden divider" /> <library-card
<Layout flex> v-for="library in libraries"
<Loader v-if="isLoading" /> :key="library.uuid"
<library-card :display-scan="false"
v-for="library in libraries" :display-follow="store.state.auth.authenticated && library.actor.full_username != store.state.auth.fullUsername"
:key="library.uuid" :initial-library="library"
:display-scan="false" :display-copy-fid="true"
:display-follow="store.state.auth.authenticated && library.actor.full_username != store.state.auth.fullUsername" />
:initial-library="library"
:display-copy-fid="true"
/>
</Layout>
<template v-if="nextPage"> <template v-if="nextPage">
<Spacer /> <Spacer />
<Button <Button
@ -103,5 +100,5 @@ watch(() => props.url, () => {
{{ t('components.federation.LibraryWidget.button.showMore') }} {{ t('components.federation.LibraryWidget.button.showMore') }}
</Button> </Button>
</template> </template>
</div> </Section>
</template> </template>

View File

@ -9,6 +9,9 @@ import Pagination from '~/components/vui/Pagination.vue'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import Loader from '~/components/ui/Loader.vue'
import Spacer from '~/components/ui/Spacer.vue'
interface Events { interface Events {
(e: 'libraries-loaded', libraries: Library[]): void (e: 'libraries-loaded', libraries: Library[]): void
} }
@ -57,14 +60,14 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
<div <div
v-if="!isLoadingTracks" v-if="!isLoadingTracks"
> >
<!-- <h2 class="ui header"> <h2 class="ui header">
<span v-if="isSerie"> <span v-if="isSerie">
{{ t('components.library.AlbumDetail.header.episodes') }} {{ t('components.library.AlbumDetail.header.episodes') }}
</span> </span>
<span v-else> <span v-else>
{{ t('components.library.AlbumDetail.header.tracks') }} {{ t('components.library.AlbumDetail.header.tracks') }}
</span> </span>
</h2> --> </h2>
<channel-entries <channel-entries
v-if="artistCredit && artistCredit[0].artist.channel && isSerie" v-if="artistCredit && artistCredit[0].artist.channel && isSerie"
@ -73,6 +76,8 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
:filters="{channel: artistCredit[0].artist.channel.uuid, album: object.id, ordering: '-creation_date'}" :filters="{channel: artistCredit[0].artist.channel.uuid, album: object.id, ordering: '-creation_date'}"
/> />
<Loader v-if="isLoadingTracks" />
<template v-else> <template v-else>
<template v-if="discCount > 1"> <template v-if="discCount > 1">
<div <div
@ -125,12 +130,11 @@ const paginatedDiscs = computed(() => props.object.tracks.slice(props.paginateBy
</template> </template>
<template v-if="artistCredit && !artistCredit[0]?.artist.channel && !isSerie"> <template v-if="artistCredit && !artistCredit[0]?.artist.channel && !isSerie">
<h2> <Spacer />
{{ t('components.library.AlbumDetail.header.libraries') }}
</h2>
<library-widget <library-widget
:url="'albums/' + object.id + '/libraries/'" :url="'albums/' + object.id + '/libraries/'"
@loaded="emit('libraries-loaded', $event)" @loaded="emit('libraries-loaded', $event)"
:title="t('components.library.AlbumDetail.header.libraries')"
> >
{{ t('components.library.AlbumDetail.description.libraries') }} {{ t('components.library.AlbumDetail.description.libraries') }}
</library-widget> </library-widget>

View File

@ -80,29 +80,22 @@ fetchData()
v-if="scope === 'all'" v-if="scope === 'all'"
:show-modification-date="true" :show-modification-date="true"
:filters="{ordering: '-creation_date'}" :filters="{ordering: '-creation_date'}"
:limit="12" :limit="8"
:title="t('components.library.Home.header.newChannels')" :title="t('components.library.Home.header.newChannels')"
> />
</channels-widget>
<track-widget <track-widget
:title="t('components.library.Home.header.recentlyListened')"
:url="'history/listenings/'" :url="'history/listenings/'"
:filters="{ scope, ordering: '-creation_date', ...qualityFilters }" :filters="{ scope, ordering: '-creation_date', ...qualityFilters }"
:websocket-handlers="['Listen']" :websocket-handlers="['Listen']"
> />
<template #title>
{{ t('components.library.Home.header.recentlyListened') }}
</template>
</track-widget>
<track-widget <track-widget
:title="t('components.library.Home.header.recentlyFavorited')"
:url="'favorites/tracks/'" :url="'favorites/tracks/'"
:filters="{scope: scope, ordering: '-creation_date'}" :filters="{scope: scope, ordering: '-creation_date'}"
> />
<template #title>
{{ t('components.library.Home.header.recentlyFavorited') }}
</template>
</track-widget>
<album-widget :filters="{scope: scope, playable: true, ordering: '-creation_date', ...qualityFilters}"> <album-widget :filters="{scope: scope, playable: true, ordering: '-creation_date', ...qualityFilters}">
<template #title> <template #title>

View File

@ -9,17 +9,20 @@ import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
import useErrorHandler from '~/composables/useErrorHandler' import useErrorHandler from '~/composables/useErrorHandler'
import usePage from '~/composables/navigation/usePage'
import PlaylistCard from '~/components/playlists/Card.vue' import PlaylistCard from '~/components/playlists/Card.vue'
import Button from '~/components/ui/Button.vue' import Button from '~/components/ui/Button.vue'
import Layout from '~/components/ui/Layout.vue' import Section from '~/components/ui/Section.vue'
import Alert from '~/components/ui/Alert.vue' import Alert from '~/components/ui/Alert.vue'
import Spacer from '~/components/ui/Spacer.vue' import Spacer from '~/components/ui/Spacer.vue'
import Loader from '~/components/ui/Loader.vue' import Loader from '~/components/ui/Loader.vue'
import Pagination from '~/components/ui/Pagination.vue'
interface Props { interface Props {
filters: Record<string, unknown> filters: Record<string, unknown>
url: string url: string
title?: string
} }
const { t } = useI18n() const { t } = useI18n()
@ -29,19 +32,25 @@ const props = defineProps<Props>()
const store = useStore() const store = useStore()
const objects = reactive([] as Playlist[]) const objects = reactive([] as Playlist[])
const isLoading = ref(false) const page = usePage()
const nextPage = ref('') const nextPage = ref('')
const count = ref(0)
const isLoading = ref(false)
const fetchData = async (url = props.url) => { const fetchData = async (url = props.url) => {
isLoading.value = true isLoading.value = true
try { try {
const params = { const params = {
...props.filters, ...props.filters,
page_size: props.filters.limit ?? 3 page: page.value,
page_size: props.filters.limit ?? 4
} }
const response = await axios.get(url, { params }) const response = await axios.get(url, { params })
nextPage.value = response.data.next nextPage.value = response.data.next
count.value = response.data.count
objects.push(...response.data.results) objects.push(...response.data.results)
} catch (error) { } catch (error) {
useErrorHandler(error as Error) useErrorHandler(error as Error)
@ -50,35 +59,24 @@ const fetchData = async (url = props.url) => {
isLoading.value = false isLoading.value = false
} }
setTimeout(fetchData, 1000)
watch( watch(
() => store.state.moderation.lastUpdate, [() => store.state.moderation.lastUpdate, page],
() => fetchData(), () => fetchData(),
{ immediate: true } { immediate: true }
) )
</script> </script>
<template> <template>
<div class="playlist-widget"> <Section
<h2 v-if="!!$slots.title"> align-left
<slot name="title" /> :h2="title"
</h2> >
<Loader v-if="isLoading"/> <Loader v-if="isLoading"/>
<Layout
v-else-if="objects.length > 0"
flex
gap-16
>
<PlaylistCard
v-for="playlist in objects"
:key="playlist.id"
:playlist="playlist"
/>
</Layout>
<Alert <Alert
v-else v-if="!isLoading && objects.length === 0"
style="grid-column: 1 / -1;"
blue blue
align-items="center" align-items="center"
> >
@ -97,16 +95,15 @@ watch(
{{ t('components.playlists.Widget.button.create') }} {{ t('components.playlists.Widget.button.create') }}
</Button> </Button>
</Alert> </Alert>
<PlaylistCard
<template v-if="nextPage"> v-for="playlist in objects"
<Spacer /> :key="playlist.id"
<Button :playlist="playlist"
v-if="nextPage" />
primary </Section>
@click="fetchData(nextPage)" <Pagination
> v-if="objects && count > props.filters.limit"
{{ t('components.playlists.Widget.button.more') }} v-model:page="page"
</Button> :pages="Math.ceil((count || 0) / props.filters.limit)"
</template> />
</div>
</template> </template>

View File

@ -10,6 +10,7 @@ import TrackWidget from '~/components/audio/track/Widget.vue'
import AlbumWidget from '~/components/album/Widget.vue' import AlbumWidget from '~/components/album/Widget.vue'
import RadioButton from '~/components/radios/Button.vue' import RadioButton from '~/components/radios/Button.vue'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
import Header from '~/components/ui/Header.vue'
interface Props { interface Props {
object?: Actor object?: Actor
@ -26,47 +27,45 @@ const { t } = useI18n()
</script> </script>
<template> <template>
<Layout stack> <Layout stack gap-64>
<radio-button <Header
v-if="recentActivity > 0" align-left
class="right floated" medium-items
type="account" :h1="t('views.auth.ProfileBase.link.overview')"
:object?-id="{username: object?.preferred_username, fullUsername: object?.full_username}" >
:client-only="true" <template #action>
/> <radio-button
v-if="recentActivity > 0"
class="right floated"
type="account"
:object-id="{username: object?.preferred_username, fullUsername: object?.full_username}"
:client-only="true"
/>
</template>
</Header>
<track-widget <track-widget
:url="'history/listenings/'" :url="'history/listenings/'"
:filters="{ scope: `actor:${object?.full_username}`, ordering: '-creation_date', ...qualityFilters}" :filters="{ scope: `actor:${object?.full_username}`, ordering: '-creation_date', ...qualityFilters}"
:websocket-handlers="['Listen']" :websocket-handlers="['Listen']"
@count="recentActivity = $event" @count="recentActivity = $event"
> :title="t('components.library.Home.header.recentlyListened')"
<template #title> />
{{ t('components.library.Home.header.recentlyListened') }}
</template>
</track-widget>
<track-widget <track-widget
:url="'favorites/tracks/'" :url="'favorites/tracks/'"
:filters="{scope: 'actor:${object?.full_username}', ordering: '-creation_date'}" :filters="{scope: `actor:${object?.full_username}`, ordering: '-creation_date'}"
> :title="t('components.library.Home.header.recentlyFavorited')"
<template #title> />
{{ t('components.library.Home.header.recentlyFavorited') }}
</template>
</track-widget>
<playlist-widget <playlist-widget
:url="'playlists/'" :url="'playlists/'"
:filters="{scope: `actor:${object?.full_username}`, playable: true, ordering: '-modification_date'}" :filters="{scope: `actor:${object?.full_username}`, playable: true, ordering: '-modification_date'}"
> :title="t('views.auth.ProfileActivity.header.playlists')"
<template #title> />
{{ t('views.auth.ProfileActivity.header.playlists') }}
</template>
</playlist-widget>
<album-widget :filters="{scope: `actor:${object?.full_username}`, playable: true, ordering: '-creation_date', ...qualityFilters}"> <album-widget
<template #title> :filters="{scope: `actor:${object?.full_username}`, playable: true, ordering: '-creation_date', ...qualityFilters}"
{{ t('components.library.Home.header.recentlyAdded') }} :title="t('components.library.Home.header.recentlyAdded')"
</template> />
</album-widget>
</Layout> </Layout>
</template> </template>

View File

@ -129,7 +129,6 @@ const recentActivity = ref(0)
</Layout> </Layout>
<Tabs> <Tabs>
<Tab :title="t('views.auth.ProfileBase.link.overview')" :to="{name: 'profile.overview', params: routerParams}"> <Tab :title="t('views.auth.ProfileBase.link.overview')" :to="{name: 'profile.overview', params: routerParams}">
<h2>{{ t('views.auth.ProfileBase.link.overview') }}</h2>
<router-view <router-view
:object="object" :object="object"
@updated="fetchData" @updated="fetchData"