341 lines
7.8 KiB
Vue
341 lines
7.8 KiB
Vue
<script setup lang="ts">
|
|
import type { Track, Listening } from '~/types'
|
|
|
|
import { ref, reactive, watch, onMounted } from 'vue'
|
|
import { useStore } from '~/store'
|
|
import { clone } from 'lodash-es'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { getArtistCoverUrl } from '~/utils/utils'
|
|
|
|
import axios from 'axios'
|
|
import usePage from '~/composables/navigation/usePage'
|
|
import useWebSocketHandler from '~/composables/useWebSocketHandler'
|
|
|
|
import PlayButton from '~/components/audio/PlayButton.vue'
|
|
import TagsList from '~/components/tags/List.vue'
|
|
import Section from '~/components/ui/Section.vue'
|
|
import Alert from '~/components/ui/Alert.vue'
|
|
import Spacer from '~/components/ui/Spacer.vue'
|
|
import Loader from '~/components/ui/Loader.vue'
|
|
import Heading from '~/components/ui/Heading.vue'
|
|
import Pagination from '~/components/ui/Pagination.vue'
|
|
|
|
import useErrorHandler from '~/composables/useErrorHandler'
|
|
|
|
interface Events {
|
|
(e: 'count', count: number): void
|
|
}
|
|
|
|
interface Props {
|
|
filters: Record<string, string | boolean>
|
|
url: string
|
|
isActivity?: boolean
|
|
limit?: number
|
|
itemClasses?: string
|
|
websocketHandlers?: string[]
|
|
title?: string
|
|
}
|
|
|
|
const emit = defineEmits<Events>()
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
isActivity: true,
|
|
limit: 9,
|
|
itemClasses: '',
|
|
websocketHandlers: () => [],
|
|
title: undefined
|
|
})
|
|
|
|
const store = useStore()
|
|
const { t } = useI18n()
|
|
|
|
const objects = reactive([] as Listening[])
|
|
const count = ref(0)
|
|
const page = usePage()
|
|
|
|
const isLoading = ref(false)
|
|
|
|
const fetchData = async (url = props.url) => {
|
|
isLoading.value = true
|
|
|
|
const params = {
|
|
...clone(props.filters),
|
|
page: page.value,
|
|
page_size: props.limit ?? 9
|
|
}
|
|
|
|
try {
|
|
const response = await axios.get(url, { params })
|
|
count.value = response.data.count
|
|
|
|
const newObjects = !props.isActivity
|
|
? response.data.results.map((track: Track) => ({ track }))
|
|
: response.data.results
|
|
|
|
objects.splice(0, objects.length, ...newObjects)
|
|
} catch (error) {
|
|
useErrorHandler(error as Error)
|
|
} finally {
|
|
isLoading.value = false
|
|
}
|
|
}
|
|
|
|
onMounted(() => {
|
|
setTimeout(fetchData, 1000)
|
|
})
|
|
|
|
watch(
|
|
[() => store.state.moderation.lastUpdate, page],
|
|
() => fetchData(),
|
|
{ immediate: true }
|
|
)
|
|
|
|
watch(count, (to) => emit('count', to))
|
|
|
|
watch(() => props.websocketHandlers.includes('Listen'), (to) => {
|
|
if (to) {
|
|
useWebSocketHandler('Listen', (event: unknown) => {
|
|
// Handle WebSocket events for "Listen"
|
|
|
|
// Add the event to `objects` reactively
|
|
objects.unshift(event as Listening)
|
|
|
|
// Keep the array size within limits (e.g., remove the last item if needed)
|
|
if (objects.length > props.limit) {
|
|
objects.pop()
|
|
}
|
|
})
|
|
}
|
|
}, { immediate: true })
|
|
</script>
|
|
|
|
<template>
|
|
<Section
|
|
align-left
|
|
:h2="title"
|
|
:columns-per-item="4"
|
|
>
|
|
<Loader
|
|
v-if="isLoading"
|
|
style="grid-column: 1 / -1;"
|
|
/>
|
|
<Alert
|
|
v-if="!isLoading && count === 0"
|
|
style="grid-column: 1 / -1;"
|
|
blue
|
|
align-items="center"
|
|
>
|
|
<h4>
|
|
<i class="bi bi-search" />
|
|
{{ t('components.audio.track.Widget.empty.noResults') }}
|
|
</h4>
|
|
</Alert>
|
|
<!-- TODO: Use activity.vue -->
|
|
<div
|
|
v-for="object in (count > 0 ? objects : [])"
|
|
:key="object.id"
|
|
class="funkwhale activity"
|
|
:class="['item', itemClasses]"
|
|
>
|
|
<div class="activity-image">
|
|
<img
|
|
v-if="object.track.album && object.track.album.cover"
|
|
v-lazy="store.getters['instance/absoluteUrl'](object.track.album.cover.urls.small_square_crop)"
|
|
alt=""
|
|
>
|
|
<img
|
|
v-else-if="object.track.cover"
|
|
v-lazy="store.getters['instance/absoluteUrl'](object.track.cover.urls.small_square_crop)"
|
|
alt=""
|
|
>
|
|
<img
|
|
v-else-if="object.track.artist_credit && object.track.artist_credit.length > 1"
|
|
v-lazy="getArtistCoverUrl(object.track.artist_credit)"
|
|
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>
|
|
<Spacer :size="8" />
|
|
<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>
|
|
|
|
<style lang="scss" scoped>
|
|
.funkwhale {
|
|
&.activity {
|
|
padding-top: 14px;
|
|
border-top: 1px solid;
|
|
margin: -11px 0;
|
|
|
|
@include light-theme {
|
|
border-color: var(--fw-gray-300);
|
|
}
|
|
@include dark-theme {
|
|
border-color: var(--fw-gray-800);
|
|
}
|
|
display: grid;
|
|
grid-template-columns: auto 1fr auto;
|
|
gap: 12px;
|
|
grid-column: span 4;
|
|
|
|
&:last-child {
|
|
border-bottom: 1px solid;
|
|
}
|
|
|
|
> .activity-image {
|
|
|
|
width: 40px;
|
|
aspect-ratio: 1;
|
|
overflow: hidden;
|
|
border-radius: var(--fw-border-radius);
|
|
|
|
> img {
|
|
width: 100%;
|
|
aspect-ratio: 1;
|
|
object-fit: cover;
|
|
}
|
|
|
|
> i {
|
|
font-size: 40px;
|
|
line-height: 36px;
|
|
}
|
|
|
|
> .play-button {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
padding: 0 !important;
|
|
width: 100%;
|
|
aspect-ratio: 1;
|
|
margin: 0;
|
|
border: 0 !important;
|
|
opacity: 0;
|
|
}
|
|
}
|
|
|
|
&:hover {
|
|
.play-button {
|
|
opacity: 1;
|
|
}
|
|
}
|
|
|
|
> .activity-content {
|
|
|
|
a {
|
|
text-decoration: none;
|
|
color: var(--color);
|
|
|
|
&:hover {
|
|
text-decoration: underline;
|
|
}
|
|
}
|
|
|
|
> .track-title {
|
|
font-weight: 700;
|
|
line-height: 1.5em;
|
|
@include dark-theme {
|
|
color: var(--fw-gray-300);
|
|
}
|
|
}
|
|
|
|
.artist {
|
|
font-size: 15px;
|
|
}
|
|
|
|
.user, time {
|
|
line-height: 1.5em;
|
|
font-size: 0.8125rem;
|
|
color: var(--fw-gray-500);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@include light-theme {
|
|
|
|
.play-button {
|
|
background: rgba(255, 255, 255, .5);
|
|
|
|
&:hover {
|
|
--fw-text-color: var(--fw-gray-800) !important;
|
|
}
|
|
}
|
|
}
|
|
|
|
@include dark-theme {
|
|
|
|
.play-button {
|
|
background: rgba(0, 0, 0, .2);
|
|
|
|
&:hover {
|
|
background: rgba(0, 0, 0, .8);
|
|
--fw-text-color: var(--fw-gray-200) !important;
|
|
}
|
|
}
|
|
}
|
|
</style>
|