227 lines
6.2 KiB
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>
|