funkwhale/front/src/components/library/ArtistBase.vue

303 lines
9.9 KiB
Vue

<script setup lang="ts">
import type { Track, Album, Artist, Library, Cover } from '~/types'
import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { useRouter, useRoute } from 'vue-router'
import { sum } from 'lodash-es'
import { getDomain } from '~/utils'
import { useStore } from '~/store'
import axios from 'axios'
import useReport from '~/composables/moderation/useReport'
import useLogger from '~/composables/useLogger'
import HumanDuration from '~/components/common/HumanDuration.vue'
import EmbedWizard from '~/components/audio/EmbedWizard.vue'
import Loader from '~/components/ui/Loader.vue'
import Button from '~/components/ui/Button.vue'
import OptionsButton from '~/components/ui/button/Options.vue'
import PlayButton from '~/components/audio/PlayButton.vue'
import RadioButton from '~/components/radios/Button.vue'
import Popover from '~/components/ui/Popover.vue'
import PopoverItem from '~/components/ui/popover/PopoverItem.vue'
import TagsList from '~/components/tags/List.vue'
import Layout from '~/components/ui/Layout.vue'
import Modal from '~/components/ui/Modal.vue'
import Spacer from '~/components/ui/Spacer.vue'
interface Props {
id: number
}
const props = defineProps<Props>()
const { report, getReportableObjects } = useReport()
const object = ref<Artist | null>(null)
const libraries = ref([] as Library[])
const albums = ref([] as Album[])
const tracks = ref([] as Track[])
const showEmbedModal = ref(false)
const nextAlbumsUrl = ref(null)
const nextTracksUrl = ref(null)
const totalAlbums = ref(0)
const totalTracks = ref(0)
const logger = useLogger()
const store = useStore()
const router = useRouter()
const route = useRoute()
const domain = computed(() => getDomain(object.value?.fid ?? ''))
const isPlayable = computed(() => !!object.value?.albums.some(album => album.is_playable))
const wikipediaUrl = computed(() => `https://en.wikipedia.org/w/index.php?search=${encodeURI(object.value?.name ?? '')}`)
const musicbrainzUrl = computed(() => object.value?.mbid ? `https://musicbrainz.org/artist/${object.value.mbid}` : null)
const discogsUrl = computed(() => `https://discogs.com/search/?type=artist&title=${encodeURI(object.value?.name ?? '')}`)
const publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
const cover = computed(() => {
const artistCover: Cover | undefined = object.value?.attachment_cover
const albumCover: Cover | undefined = object.value?.albums
.find(album => album.cover?.urls.large_square_crop)?.cover
const trackCover = tracks.value?.find(
(track: Track) => track.cover
)?.cover
const fallback : Cover = {
uuid: '',
urls: {
original: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
medium_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`,
large_square_crop: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`
}
}
return artistCover
|| albumCover
|| trackCover
|| fallback
})
const { t } = useI18n()
const labels = computed(() => ({
title: t('components.library.ArtistBase.title')
}))
const isLoading = ref(false)
const fetchData = async () => {
isLoading.value = true
logger.debug(`Fetching artist "${props.id}"`)
const artistsResponse = await axios.get(`artists/${props.id}/`, { params: { refresh: 'true' } })
if (artistsResponse.data.channel) {
return router.replace({ name: 'channels.detail', params: { id: artistsResponse.data.channel.uuid } })
}
object.value = artistsResponse.data
const [tracksResponse, albumsResponse] = await Promise.all([
axios.get('tracks/', { params: { artist: props.id, hidden: '', ordering: '-creation_date' } }),
axios.get('albums/', { params: { artist: props.id, hidden: '', ordering: '-release_date' } })
])
tracks.value = tracksResponse.data.results
nextTracksUrl.value = tracksResponse.data.next
totalTracks.value = tracksResponse.data.count
nextAlbumsUrl.value = albumsResponse.data.next
totalAlbums.value = albumsResponse.data.count
albums.value = albumsResponse.data.results
isLoading.value = false
}
const totalDuration = computed(() => sum((tracks.value ?? []).map(track => track.uploads[0]?.duration ?? 0)))
watch(() => props.id, fetchData, { immediate: true })
</script>
<template>
<Layout stack main v-title="labels.title">
<Loader v-if="isLoading" />
<template v-if="object && !isLoading">
<Layout flex>
<img
v-lazy="cover.urls.large_square_crop"
:alt="object.name"
class="channel-image"
>
<Layout stack style="flex: 1; gap: 8px;">
<h1>{{ object.name }}</h1>
<Layout flex class="meta" style="gap: 0;">
<div
v-if="albums"
>
{{ t('components.library.ArtistBase.meta.tracks', totalTracks) }}
{{ t('components.library.ArtistBase.meta.albums', totalAlbums) }}
</div>
<div v-if="totalDuration > 0">
<i class="bi bi-dot" />
<human-duration
v-if="totalDuration > 0"
:duration="totalDuration"
/>
</div>
<TagsList
v-if="object.tags && object.tags.length > 0"
:tags="object.tags"
/>
</Layout>
<Spacer />
<Layout flex>
<PlayButton
:is-playable="isPlayable"
split
:artist="object"
>
{{ t('components.library.ArtistBase.button.play') }}
</PlayButton>
<radio-button
type="artist"
:object-id="object.id"
/>
<Spacer grow />
<Popover>
<template #default="{ toggleOpen }">
<OptionsButton
@click="toggleOpen"
/>
</template>
<template #items>
<PopoverItem
v-if="domain != store.getters['instance/domain']"
:to="object.fid"
target="_blank"
icon="bi-box-arrow-up-right"
>
{{ t('components.library.ArtistBase.link.domain', {domain: domain}) }}
</PopoverItem>
<PopoverItem
v-if="publicLibraries.length > 0"
@click="showEmbedModal = true"
icon="bi-code-square"
>
{{ t('components.library.ArtistBase.button.embed') }}
</PopoverItem>
<PopoverItem
:to="wikipediaUrl"
target="_blank"
rel="noreferrer noopener"
icon="bi-wikipedia"
>
{{ t('components.library.ArtistBase.link.wikipedia') }}
</PopoverItem>
<PopoverItem
v-if="musicbrainzUrl"
:to="musicbrainzUrl"
target="_blank"
rel="noreferrer noopener"
icon="bi-box-arrow-up-right"
>
{{ t('components.library.ArtistBase.link.musicbrainz') }}
</PopoverItem>
<PopoverItem
:to="discogsUrl"
target="_blank"
rel="noreferrer noopener"
icon="bi-box-arrow-up-right"
>
{{ t('components.library.ArtistBase.link.discogs') }}
</PopoverItem>
<PopoverItem
v-if="object.is_local"
:to="{name: 'library.artists.edit', params: {id: object.id }}"
icon="bi-pencil-fill"
>
{{ t('components.library.ArtistBase.button.edit') }}
</PopoverItem>
<hr v-for="obj in getReportableObjects({artist: object})">
<PopoverItem
v-for="obj in getReportableObjects({artist: object})"
:key="obj.target.type + obj.target.id"
icon="bi-share-fill"
@click="report(obj)"
>
{{ obj.label }}
</PopoverItem>
<hr>
<PopoverItem
v-if="store.state.auth.availablePermissions['library']"
:to="{name: 'manage.library.artists.detail', params: {id: object.id}}"
icon="bi-wrench"
>
{{ t('components.library.ArtistBase.link.moderation') }}
</PopoverItem>
<PopoverItem
v-if="store.state.auth.profile && store.state.auth.profile.is_superuser"
:to="store.getters['instance/absoluteUrl'](`/api/admin/music/artist/${object.id}`)"
target="_blank"
rel="noopener noreferrer"
icon="bi-wrench"
>
{{ t('components.library.ArtistBase.link.django') }}
</PopoverItem>
</template>
</Popover>
</Layout>
</Layout>
</Layout>
<Modal
v-if="publicLibraries.length > 0"
v-model="showEmbedModal"
:title="t('components.library.ArtistBase.modal.embed.header')"
>
<embed-wizard
:id="object.id"
type="artist"
/>
<template #actions>
<Button secondary>
{{ t('components.library.ArtistBase.button.cancel') }}
</Button>
</template>
</Modal>
<hr>
<router-view
:key="route.fullPath"
:tracks="tracks"
:next-tracks-url="nextTracksUrl"
:next-albums-url="nextAlbumsUrl"
:albums="albums"
:is-loading-albums="isLoading"
:object="object"
object-type="artist"
@libraries-loaded="libraries = $event"
/>
</template>
</Layout>
</template>
<style scoped>
.channel-image {
border-radius: 50%;
}
</style>