307 lines
7.3 KiB
Vue
307 lines
7.3 KiB
Vue
<script setup lang="ts">
|
|
import type { Track } from '~/types'
|
|
|
|
import { useElementByPoint, useMouse } from '@vueuse/core'
|
|
import { useGettext } from 'vue3-gettext'
|
|
import { clone, uniqBy } from 'lodash-es'
|
|
import { ref, computed } from 'vue'
|
|
|
|
import axios from 'axios'
|
|
|
|
import TrackMobileRow from '~/components/audio/track/MobileRow.vue'
|
|
import Pagination from '~/components/vui/Pagination.vue'
|
|
import TrackRow from '~/components/audio/track/Row.vue'
|
|
|
|
import useErrorHandler from '~/composables/useErrorHandler'
|
|
|
|
interface Events {
|
|
(e: 'fetched'): void
|
|
(e: 'page-changed', page: number): void
|
|
}
|
|
|
|
interface Props {
|
|
tracks?: undefined | Track[]
|
|
|
|
showAlbum?: boolean
|
|
showArtist?: boolean
|
|
showPosition?: boolean
|
|
showArt?: boolean
|
|
showDuration?: boolean
|
|
search?: boolean
|
|
displayActions?: boolean
|
|
isArtist?: boolean
|
|
isAlbum?: boolean
|
|
isPodcast?: boolean
|
|
|
|
filters?: object
|
|
|
|
nextUrl?: string | null
|
|
|
|
paginateResults?: boolean
|
|
total?: number
|
|
page?: number
|
|
paginateBy?: number,
|
|
|
|
unique?: boolean
|
|
}
|
|
|
|
const emit = defineEmits<Events>()
|
|
const props = withDefaults(defineProps<Props>(), {
|
|
tracks: undefined,
|
|
|
|
showAlbum: true,
|
|
showArtist: true,
|
|
showPosition: false,
|
|
showArt: true,
|
|
showDuration: true,
|
|
search: false,
|
|
displayActions: true,
|
|
isArtist: false,
|
|
isAlbum: false,
|
|
isPodcast: false,
|
|
|
|
filters: () => ({}),
|
|
nextUrl: null,
|
|
|
|
paginateResults: true,
|
|
total: 0,
|
|
page: 1,
|
|
paginateBy: 25,
|
|
|
|
unique: true
|
|
})
|
|
|
|
const { x, y } = useMouse({ type: 'client' })
|
|
const { element } = useElementByPoint({ x, y })
|
|
const hover = computed(() => {
|
|
const row = element.value?.closest('.track-row') ?? null
|
|
return row && allTracks.value.find(track => {
|
|
return `${track.id}` === row.getAttribute('data-track-id') && `${track.position}` === row.getAttribute('data-track-position')
|
|
})
|
|
})
|
|
|
|
const currentPage = ref(props.page)
|
|
const totalTracks = ref(props.total)
|
|
const fetchDataUrl = ref(props.nextUrl)
|
|
const additionalTracks = ref([] as Track[])
|
|
const query = ref('')
|
|
|
|
const allTracks = computed(() => {
|
|
const tracks = [...(props.tracks ?? []), ...additionalTracks.value]
|
|
return props.unique
|
|
? uniqBy(tracks, 'id')
|
|
: tracks
|
|
})
|
|
|
|
const { $pgettext } = useGettext()
|
|
const labels = computed(() => ({
|
|
title: $pgettext('*/*/*/Noun', 'Title'),
|
|
album: $pgettext('*/*/*/Noun', 'Album'),
|
|
artist: $pgettext('*/*/*/Noun', 'Artist')
|
|
}))
|
|
|
|
const isLoading = ref(false)
|
|
const fetchData = async () => {
|
|
isLoading.value = true
|
|
|
|
const params = {
|
|
...clone(props.filters),
|
|
page_size: props.paginateBy,
|
|
page: currentPage.value,
|
|
include_channels: true,
|
|
q: query.value
|
|
}
|
|
|
|
try {
|
|
const response = await axios.get('tracks/', { params })
|
|
|
|
// TODO (wvffle): Fetch continously?
|
|
fetchDataUrl.value = response.data.next
|
|
additionalTracks.value = response.data.results
|
|
totalTracks.value = response.data.count
|
|
emit('fetched')
|
|
} catch (error) {
|
|
useErrorHandler(error as Error)
|
|
}
|
|
|
|
isLoading.value = false
|
|
}
|
|
|
|
const performSearch = () => {
|
|
currentPage.value = 1
|
|
additionalTracks.value = []
|
|
fetchData()
|
|
}
|
|
|
|
if (props.tracks === undefined) {
|
|
fetchData()
|
|
}
|
|
|
|
const updatePage = (page: number) => {
|
|
if (props.tracks.length === 0) {
|
|
currentPage.value = page
|
|
fetchData()
|
|
} else {
|
|
emit('page-changed', page)
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div>
|
|
<!-- Show the search bar if search is true -->
|
|
<inline-search-bar
|
|
v-if="search"
|
|
v-model="query"
|
|
@search="performSearch"
|
|
/>
|
|
|
|
<!-- Add a header if needed -->
|
|
|
|
<slot name="header" />
|
|
|
|
<!-- Show a message if no tracks are available -->
|
|
|
|
<slot
|
|
v-if="!isLoading && allTracks.length === 0"
|
|
name="empty-state"
|
|
>
|
|
<empty-state
|
|
:refresh="true"
|
|
@refresh="fetchData()"
|
|
/>
|
|
</slot>
|
|
<div v-else>
|
|
<div
|
|
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-up']"
|
|
>
|
|
<div
|
|
v-if="isLoading"
|
|
class="ui inverted active dimmer"
|
|
>
|
|
<div class="ui loader" />
|
|
</div>
|
|
<div class="track-table row">
|
|
<div
|
|
v-if="showPosition"
|
|
class="actions left floated column"
|
|
>
|
|
<i class="hashtag icon" />
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="actions left floated column"
|
|
/>
|
|
<div
|
|
v-if="showArt"
|
|
class="image left floated column"
|
|
/>
|
|
<div class="content ellipsis left floated column">
|
|
<b>{{ labels.title }}</b>
|
|
</div>
|
|
<div
|
|
v-if="showAlbum"
|
|
class="content ellipsisleft floated column"
|
|
>
|
|
<b>{{ labels.album }}</b>
|
|
</div>
|
|
<div
|
|
v-if="showArtist"
|
|
class="content ellipsis left floated column"
|
|
>
|
|
<b>{{ labels.artist }}</b>
|
|
</div>
|
|
<div
|
|
v-if="$store.state.auth.authenticated"
|
|
class="meta right floated column"
|
|
/>
|
|
<div
|
|
v-if="showDuration"
|
|
class="meta right floated column"
|
|
>
|
|
<i
|
|
class="clock outline icon"
|
|
style="padding: 0.5rem"
|
|
/>
|
|
</div>
|
|
<div
|
|
v-if="displayActions"
|
|
class="meta right floated column"
|
|
/>
|
|
</div>
|
|
|
|
<!-- For each item, build a row -->
|
|
|
|
<track-row
|
|
v-for="(track, index) in allTracks"
|
|
:key="track.id + (track.position ?? 0)"
|
|
:data-track-id="track.id"
|
|
:data-track-position="track.position"
|
|
:track="track"
|
|
:index="index"
|
|
:tracks="allTracks"
|
|
:show-album="showAlbum"
|
|
:show-artist="showArtist"
|
|
:show-position="showPosition"
|
|
:show-art="showArt"
|
|
:display-actions="displayActions"
|
|
:show-duration="showDuration"
|
|
:is-podcast="isPodcast"
|
|
:hover="hover === track"
|
|
/>
|
|
</div>
|
|
<div
|
|
v-if="tracks && paginateResults"
|
|
class="ui center aligned basic segment desktop-and-up"
|
|
>
|
|
<pagination
|
|
:total="totalTracks"
|
|
:current="tracks.length > 0 ? page : currentPage"
|
|
:paginate-by="paginateBy"
|
|
@update:current="updatePage"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
:class="['track-table', 'ui', 'unstackable', 'grid', 'tablet-and-below']"
|
|
>
|
|
<div
|
|
v-if="isLoading"
|
|
class="ui inverted active dimmer"
|
|
>
|
|
<div class="ui loader" />
|
|
</div>
|
|
|
|
<!-- For each item, build a row -->
|
|
|
|
<track-mobile-row
|
|
v-for="(track, index) in allTracks"
|
|
:key="track.id"
|
|
:track="track"
|
|
:index="index"
|
|
:tracks="allTracks"
|
|
:show-position="showPosition"
|
|
:show-art="showArt"
|
|
:show-duration="showDuration"
|
|
:is-artist="isArtist"
|
|
:is-album="isAlbum"
|
|
:is-podcast="isPodcast"
|
|
/>
|
|
<div
|
|
v-if="tracks && paginateResults && totalTracks > paginateBy"
|
|
class="ui center aligned basic segment tablet-and-below"
|
|
>
|
|
<pagination
|
|
v-if="paginateResults && totalTracks > paginateBy"
|
|
:paginate-by="paginateBy"
|
|
:total="totalTracks"
|
|
:current="tracks.length > 0 ? page : currentPage"
|
|
:compact="true"
|
|
@update:current="updatePage"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|