feat(style): consistent look and use of artist credit label on detail pages

This commit is contained in:
ArneBo 2025-02-23 00:26:33 +01:00
parent 75d4ac5467
commit 966181e480
5 changed files with 366 additions and 415 deletions

View File

@ -2,7 +2,7 @@
import type { ArtistCredit } from '~/types' import type { ArtistCredit } from '~/types'
import { useStore } from '~/store' import { useStore } from '~/store'
import Link from '~/components/ui/Link.vue' import Pill from '~/components/ui/Pill.vue'
const store = useStore() const store = useStore()
@ -23,31 +23,28 @@ const getRoute = (ac: ArtistCredit) => {
</script> </script>
<template> <template>
<div class="artist-label ui image label">
<template <template
v-for="ac in props.artistCredit" v-for="ac in props.artistCredit"
:key="ac.artist.id" :key="ac.artist.id"
> >
<Link <router-link
solid :to="{name: 'library.artists.detail', params: {id: ac.artist.id }}"
secondary @click.stop.prevent=""
round
min-content
:to="getRoute(ac)"
> >
<Pill>
<template #image>
<img <img
v-if="ac.index === 0 && ac.artist.cover && ac.artist.cover.urls.original" v-if="ac.artist.cover && ac.artist.cover.urls.original"
v-lazy="store.getters['instance/absoluteUrl'](ac.artist.cover.urls.medium_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](ac.artist.cover.urls.small_square_crop)"
alt="" :alt="ac.artist.name"
:class="[{circular: ac.artist.content_category != 'podcast'}]" />
> <i
<i v-else
v-else-if="ac.index === 0" class="bi bi-person-circle"
:class="[ac.artist.content_category != 'podcast' ? 'circular' : 'bordered', 'inverted violet bi bi-users icon']"
/> />
{{ ac.credit }}
</Link>
<span>{{ ac.joinphrase }}</span>
</template> </template>
</div> {{ ac.credit }}
</Pill>
</router-link>
</template>
</template> </template>

View File

@ -69,7 +69,7 @@ const labels = computed(() => ({
const { const {
isShuffled, isShuffled,
shuffle shuffle,
} = useQueue() } = useQueue()
const isLoading = ref(false) const isLoading = ref(false)
@ -150,10 +150,7 @@ const remove = async () => {
</script> </script>
<template> <template>
<Layout <Layout stack main>
stack
main
>
<Loader <Loader
v-if="isLoading" v-if="isLoading"
v-title="labels.title" v-title="labels.title"
@ -165,27 +162,15 @@ const remove = async () => {
v-lazy="store.getters['instance/absoluteUrl'](object.cover.urls.large_square_crop)" v-lazy="store.getters['instance/absoluteUrl'](object.cover.urls.large_square_crop)"
:alt="object.title" :alt="object.title"
class="channel-image" class="channel-image"
> />
<img
v-else-if="object.artist_credit && object.artist_credit[0] && object.artist_credit[0].artist.cover"
v-lazy="object.artist_credit[0].artist.cover.urls.large_square_crop"
:alt="object.artist_credit[0].artist.name"
class="channel-image"
>
<img <img
v-else v-else
alt="" alt=""
class="channel-image" class="channel-image"
src="../../assets/audio/default-cover.png" src="../../assets/audio/default-cover.png"
> />
<!-- ({target}) => target --> <Layout stack style="flex: 1; gap: 8px;">
<!-- Header (TODO: Put into Header component) Hint: Header is heavier fontweight than h1! --> <h1 style="margin-top: 64px; margin-bottom: 8px;">{{ object.title }}</h1>
<Layout
stack
style="flex: 1; gap: 8px;"
>
<h1>{{ object.title }}</h1>
<!-- <Header :h1="object.title" /> -->
<artist-credit-label <artist-credit-label
v-if="artistCredit" v-if="artistCredit"
:artist-credit="artistCredit" :artist-credit="artistCredit"
@ -204,10 +189,7 @@ const remove = async () => {
{{ t('components.library.AlbumBase.meta.tracks', totalTracks) }} {{ t('components.library.AlbumBase.meta.tracks', totalTracks) }}
</span> </span>
</template> </template>
<i <i v-if="totalDuration > 0" class="bi bi-dot" />
v-if="totalDuration > 0"
class="bi bi-dot"
/>
<human-duration <human-duration
v-if="totalDuration > 0" v-if="totalDuration > 0"
:duration="totalDuration" :duration="totalDuration"
@ -242,23 +224,14 @@ const remove = async () => {
artistCredit[0].artist.channel && artistCredit[0].artist.channel &&
artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername" artistCredit[0].artist.attributed_to?.full_username === store.state.auth.fullUsername"
:is-loading="isLoading" :is-loading="isLoading"
icon="bi-trash"
@confirm="remove()" @confirm="remove()"
icon="bi-trash"
> >
{{ t('components.library.AlbumDropdown.button.delete') }} {{ t('components.library.AlbumDropdown.button.delete') }}
</DangerousButton> </DangerousButton>
<Spacer <Spacer h grow />
h <TrackFavoriteIcon v-if="store.state.auth.authenticated" :album="object" />
grow <TrackPlaylistIcon v-if="store.state.auth.authenticated" :album="object" />
/>
<TrackFavoriteIcon
v-if="store.state.auth.authenticated"
:album="object"
/>
<TrackPlaylistIcon
v-if="store.state.auth.authenticated"
:album="object"
/>
<!-- TODO: Share Button --> <!-- TODO: Share Button -->
<album-dropdown <album-dropdown
:object="object" :object="object"

View File

@ -25,6 +25,7 @@ import Layout from '~/components/ui/Layout.vue'
import Modal from '~/components/ui/Modal.vue' import Modal from '~/components/ui/Modal.vue'
import Spacer from '~/components/ui/Spacer.vue' import Spacer from '~/components/ui/Spacer.vue'
interface Props { interface Props {
id: number id: number
} }
@ -55,6 +56,7 @@ const musicbrainzUrl = computed(() => object.value?.mbid ? `https://musicbrainz.
const discogsUrl = computed(() => `https://discogs.com/search/?type=artist&title=${encodeURI(object.value?.name ?? '')}`) 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 publicLibraries = computed(() => libraries.value?.filter(library => library.privacy_level === 'everyone') ?? [])
const cover = computed(() => { const cover = computed(() => {
const artistCover: Cover | undefined = object.value?.cover const artistCover: Cover | undefined = object.value?.cover
@ -120,11 +122,7 @@ watch(() => props.id, fetchData, { immediate: true })
</script> </script>
<template> <template>
<Layout <Layout stack main v-title="labels.title">
v-title="labels.title"
stack
main
>
<Loader v-if="isLoading" /> <Loader v-if="isLoading" />
<template v-if="object && !isLoading"> <template v-if="object && !isLoading">
<Layout flex> <Layout flex>
@ -133,16 +131,9 @@ watch(() => props.id, fetchData, { immediate: true })
:alt="object.name" :alt="object.name"
class="channel-image" class="channel-image"
> >
<Layout <Layout stack style="flex: 1; gap: 8px;">
stack <h1 style="margin-top: 64px; margin-bottom: 8px;">{{ object.name }}</h1>
style="flex: 1; gap: 8px;" <Layout flex class="meta" style="gap: 0;">
>
<h1>{{ object.name }}</h1>
<Layout
flex
class="meta"
style="gap: 0;"
>
<div <div
v-if="albums" v-if="albums"
> >
@ -190,8 +181,8 @@ watch(() => props.id, fetchData, { immediate: true })
<PopoverItem <PopoverItem
v-if="publicLibraries.length > 0" v-if="publicLibraries.length > 0"
icon="bi-code-square"
@click="showEmbedModal = true" @click="showEmbedModal = true"
icon="bi-code-square"
> >
{{ t('components.library.ArtistBase.button.embed') }} {{ t('components.library.ArtistBase.button.embed') }}
</PopoverItem> </PopoverItem>

View File

@ -11,6 +11,7 @@ import { useStore } from '~/store'
import axios from 'axios' import axios from 'axios'
import ActorLink from '~/components/common/ActorLink.vue' import ActorLink from '~/components/common/ActorLink.vue'
import ArtistCreditLabel from '~/components/audio/ArtistCreditLabel.vue'
import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue' import TrackFavoriteIcon from '~/components/favorites/TrackFavoriteIcon.vue'
import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue' import TrackPlaylistIcon from '~/components/playlists/TrackPlaylistIcon.vue'
import EmbedWizard from '~/components/audio/EmbedWizard.vue' import EmbedWizard from '~/components/audio/EmbedWizard.vue'
@ -76,6 +77,8 @@ const attributedToUrl = computed(() => router.resolve({
} }
})?.href) })?.href)
const artistCredit = track.value?.artist_credit
const totalDuration = computed(() => track.value?.uploads[0]?.duration ?? 0) const totalDuration = computed(() => track.value?.uploads[0]?.duration ?? 0)
const { t } = useI18n() const { t } = useI18n()
@ -126,10 +129,7 @@ watch(showDeleteModal, (newValue) => {
</script> </script>
<template> <template>
<Layout <Layout stack main>
stack
main
>
<Loader <Loader
v-if="isLoading" v-if="isLoading"
v-title="labels.title" v-title="labels.title"
@ -155,16 +155,9 @@ watch(showDeleteModal, (newValue) => {
src="../../assets/audio/default-cover.png" src="../../assets/audio/default-cover.png"
> >
<Layout <Layout stack style="flex: 1; gap: 8px;">
stack <Layout flex no-gap style="align-items: baseline;">
style="flex: 1; gap: 8px;" <h1 style="margin-top: 64px; margin-bottom: 8px;">{{ track.title }}</h1>
>
<Layout
flex
no-gap
style="align-items: baseline; margin-bottom: 24px;"
>
<h1>{{ track.title }}</h1>
<Spacer grow /> <Spacer grow />
<Button <Button
v-if="upload" v-if="upload"
@ -178,14 +171,14 @@ watch(showDeleteModal, (newValue) => {
{{ labels.download }} {{ labels.download }}
</Button> </Button>
</Layout> </Layout>
<artist-credit-label
:artist-credit="track.artist_credit"
/>
<div class="meta"> <div class="meta">
<span>{{ t('components.library.TrackBase.title') }}</span> <span>{{ t('components.library.TrackBase.title') }}</span>
<i class="bi bi-dot" /> <i class="bi bi-dot" />
<span>{{ track.album.title }}</span> <span>{{ track.album.title }}</span>
<i <i v-if="totalDuration > 0" class="bi bi-dot" />
v-if="totalDuration > 0"
class="bi bi-dot"
/>
<human-duration <human-duration
v-if="totalDuration > 0" v-if="totalDuration > 0"
:duration="totalDuration" :duration="totalDuration"
@ -200,19 +193,10 @@ watch(showDeleteModal, (newValue) => {
:track="track" :track="track"
/> />
<Spacer <Spacer h grow />
h
grow
/>
<TrackFavoriteIcon <TrackFavoriteIcon v-if="store.state.auth.authenticated" :track="track" />
v-if="store.state.auth.authenticated" <TrackPlaylistIcon v-if="store.state.auth.authenticated" :track="track" />
:track="track"
/>
<TrackPlaylistIcon
v-if="store.state.auth.authenticated"
:track="track"
/>
<Popover v-model:open="open"> <Popover v-model:open="open">
<template #default="{ toggleOpen }"> <template #default="{ toggleOpen }">
<OptionsButton @click="toggleOpen" /> <OptionsButton @click="toggleOpen" />
@ -229,8 +213,8 @@ watch(showDeleteModal, (newValue) => {
<PopoverItem <PopoverItem
v-if="isEmbedable" v-if="isEmbedable"
icon="bi-code-slash"
@click="showEmbedModal = !showEmbedModal" @click="showEmbedModal = !showEmbedModal"
icon="bi-code-slash"
> >
{{ t('components.library.TrackBase.button.embed') }} {{ t('components.library.TrackBase.button.embed') }}
</PopoverItem> </PopoverItem>
@ -267,8 +251,8 @@ watch(showDeleteModal, (newValue) => {
store.state.auth.authenticated && store.state.auth.authenticated &&
artist.channel && artist.channel &&
artist.attributed_to.full_username === store.state.auth.fullUsername" artist.attributed_to.full_username === store.state.auth.fullUsername"
icon="bi-trash"
@click="showDeleteModal = true" @click="showDeleteModal = true"
icon="bi-trash"
> >
{{ t('components.library.TrackBase.button.delete') }} {{ t('components.library.TrackBase.button.delete') }}
</PopoverItem> </PopoverItem>
@ -278,8 +262,8 @@ watch(showDeleteModal, (newValue) => {
<PopoverItem <PopoverItem
v-for="obj in getReportableObjects({ track })" v-for="obj in getReportableObjects({ track })"
:key="obj.target.type + obj.target.id" :key="obj.target.type + obj.target.id"
icon="bi-flag"
@click="report(obj)" @click="report(obj)"
icon="bi-flag"
> >
{{ obj.label }} {{ obj.label }}
</PopoverItem> </PopoverItem>
@ -391,6 +375,7 @@ watch(showDeleteModal, (newValue) => {
@libraries-loaded="libraries = $event" @libraries-loaded="libraries = $event"
/> />
</template> </template>
</Layout> </Layout>
</template> </template>

View File

@ -35,6 +35,7 @@
overflow: hidden; overflow: hidden;
height: calc(2em - 4px); height: calc(2em - 4px);
margin: 2px; margin: 2px;
align-content: center;
+ .pill-content { + .pill-content {
padding-left: 0.25em; padding-left: 0.25em;
@ -45,6 +46,10 @@
width: 100%; width: 100%;
} }
> i.bi {
font-size: 18px;
}
> img { > img {
object-fit: cover; object-fit: cover;
} }