funkwhale/front/src/components/audio/track/Widget.vue

227 lines
6.2 KiB
Vue

<script setup lang="ts">
import type { Track, Listening } from '~/types'
import { ref, reactive, watch } from 'vue'
import { useStore } from '~/store'
import { clone } from 'lodash-es'
import axios from 'axios'
import useWebSocketHandler from '~/composables/useWebSocketHandler'
import PlayButton from '~/components/audio/PlayButton.vue'
import TagsList from '~/components/tags/List.vue'
import useErrorHandler from '~/composables/useErrorHandler'
interface Events {
(e: 'count', count: number): void
}
interface Props {
filters: Record<string, string | boolean>
url: string
isActivity?: boolean
showCount?: boolean
limit?: number
itemClasses?: string
websocketHandlers?: string[]
}
const emit = defineEmits<Events>()
const props = withDefaults(defineProps<Props>(), {
isActivity: true,
showCount: false,
limit: 5,
itemClasses: '',
websocketHandlers: () => []
})
const store = useStore()
const objects = reactive([] as Listening[])
const count = ref(0)
const nextPage = ref<string | null>(null)
const isLoading = ref(false)
const fetchData = async (url = props.url) => {
isLoading.value = true
const params = {
...clone(props.filters),
page_size: props.limit
}
try {
const response = await axios.get(url, { params })
nextPage.value = response.data.next
count.value = response.data.count
const newObjects = !props.isActivity
? response.data.results.map((track: Track) => ({ track }))
: response.data.results
objects.push(...newObjects)
} catch (error) {
useErrorHandler(error as Error)
}
isLoading.value = false
}
watch(
() => store.state.moderation.lastUpdate,
() => fetchData(),
{ immediate: true }
)
watch(count, (to) => emit('count', to))
watch(() => props.websocketHandlers.includes('Listen'), (to) => {
useWebSocketHandler('Listen', (event) => {
// TODO (wvffle): Add reactivity to recently listened / favorited / added (#1316, #1534)
// count.value += 1
// objects.unshift(event as Listening)
// objects.pop()
})
})
</script>
<template>
<div class="component-track-widget">
<h3 v-if="!!$slots.title">
<slot name="title" />
<span
v-if="showCount"
class="ui tiny circular label"
>{{ count }}</span>
</h3>
<div
v-if="count > 0"
class="ui divided unstackable items"
>
<div
v-for="object in objects"
:key="object.id"
:class="['item', itemClasses]"
>
<div class="ui tiny image">
<img
v-if="object.track.album && object.track.album.cover"
v-lazy="$store.getters['instance/absoluteUrl'](object.track.album.cover.urls.medium_square_crop)"
alt=""
>
<img
v-else-if="object.track.cover"
v-lazy="$store.getters['instance/absoluteUrl'](object.track.cover.urls.medium_square_crop)"
alt=""
>
<img
v-else-if="object.track.artist?.cover"
v-lazy="$store.getters['instance/absoluteUrl'](object.track.artist.cover.urls.medium_square_crop)"
alt=""
>
<img
v-else
alt=""
src="../../../assets/audio/default-cover.png"
>
<play-button
class="play-overlay"
:icon-only="true"
:button-classes="['ui', 'circular', 'tiny', 'vibrant', 'icon', 'button']"
:track="object.track"
/>
</div>
<div class="middle aligned content">
<div class="ui unstackable grid">
<div class="thirteen wide stretched column">
<div class="ellipsis">
<router-link :to="{name: 'library.tracks.detail', params: {id: object.track.id}}">
{{ object.track.title }}
</router-link>
</div>
<div
v-if="object.track.artist"
class="meta ellipsis"
>
<span>
<router-link
class="discrete link"
:to="{name: 'library.artists.detail', params: {id: object.track.artist.id}}"
>
{{ object.track.artist.name }}
</router-link>
</span>
</div>
<tags-list
label-classes="tiny"
:truncate-size="20"
:limit="2"
:show-more="false"
:tags="object.track.tags"
/>
<div
v-if="isActivity"
class="extra"
>
<router-link
class="left floated"
:to="{name: 'profile.overview', params: {username: object.user.username}}"
>
@{{ object.user.username }}
</router-link>
<span class="right floated"><human-date :date="object.creation_date" /></span>
</div>
</div>
<div class="one wide stretched column">
<play-button
class="basic icon"
:account="object.actor"
:dropdown-only="true"
:dropdown-icon-classes="['ellipsis', 'vertical', 'large really discrete']"
:track="object.track"
/>
</div>
</div>
</div>
</div>
<div
v-if="isLoading"
class="ui inverted active dimmer"
>
<div class="ui loader" />
</div>
</div>
<div
v-else
class="ui placeholder segment"
>
<div class="ui icon header">
<i class="music icon" />
<translate translate-context="Content/Home/Placeholder">
Nothing found
</translate>
</div>
<div
v-if="isLoading"
class="ui inverted active dimmer"
>
<div class="ui loader" />
</div>
</div>
<template v-if="nextPage">
<div class="ui hidden divider" />
<button
:class="['ui', 'basic', 'button']"
@click="fetchData(nextPage as string)"
>
<translate translate-context="*/*/Button,Label">
Show more
</translate>
</button>
</template>
</div>
</template>