fix(front): consistent small cards

This commit is contained in:
ArneBo 2025-01-29 12:04:04 +01:00
parent 10140959d3
commit 3d710dbb02
13 changed files with 156 additions and 108 deletions

View File

@ -4,9 +4,11 @@ import { computed } from 'vue'
import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
import { momentFormat } from '~/utils/filters'
import defaultCover from '~/assets/audio/default-cover.png'
import PlayButton from '~/components/audio/PlayButton.vue'
import Card from '~/components/ui/Card.vue'
import Link from '~/components/ui/Link.vue'
import TagsList from '~/components/tags/List.vue'
import Spacer from '~/components/ui/Spacer.vue'
@ -39,7 +41,7 @@ if (import.meta.env.PROD) {
const store = useStore()
const imageUrl = computed(() => props.album.cover?.urls.original
? store.getters['instance/absoluteUrl'](props.album.cover?.urls.large_square_crop)
: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`
: defaultCover
)
</script>
@ -49,6 +51,7 @@ const imageUrl = computed(() => props.album.cover?.urls.original
:image="imageUrl"
:tags="album.tags"
:to="{name: 'library.albums.detail', params: {id: album.id}}"
small
>
<template #topright>
<PlayButton
@ -62,27 +65,27 @@ const imageUrl = computed(() => props.album.cover?.urls.original
v-for="ac in album.artist_credit"
:key="ac.artist.id"
>
<router-link
<Link
:to="{ name: 'library.artists.detail', params: { id: ac.artist.id }}"
>
{{ ac.credit ?? t('components.Queue.meta.unknownArtist') }}
</router-link>
</Link>
<span>{{ ac.joinphrase }}</span>
</template>
<template #action>
<Spacer :size="8" />
<template #footer>
<span v-if="album.release_date">
{{ momentFormat(new Date(album.release_date), 'Y') }}
</span>
<i class="bi bi-dot"/>
<span>
<i class="bi bi-music-note-list" />
{{ t('components.audio.album.Card.meta.tracks', album.tracks_count) }}
</span>
<Spacer h grow />
<PlayButton
id="playmenu"
:dropdown-only="true"
discrete
:is-playable="album.is_playable"
:album="album"
/>
@ -92,8 +95,7 @@ const imageUrl = computed(() => props.album.cover?.urls.original
<style lang="scss" scoped>
.play-button {
position: absolute;
top: 214px;
top: 16px;
right: 16px;
}
</style>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import { useRouter } from 'vue-router'
import { computed } from 'vue'
import { computed, ref } from 'vue'
import { useStore } from '~/store'
import { useI18n } from 'vue-i18n'
@ -8,10 +8,13 @@ import PlayButton from '~/components/audio/PlayButton.vue'
import Card from '~/components/ui/Card.vue'
import Spacer from '~/components/ui/Spacer.vue'
import type { Artist } from '~/types'
import type { Artist, Cover, Track } from '~/types'
const play = defineEmit<[artist: Artist]>()
const albums = ref([] as Album[])
const tracks = ref([] as Track[])
interface Props {
artist: Artist;
}
@ -30,25 +33,45 @@ if (import.meta.env.PROD) {
router.push({ name: 'library.artists.detail', params: { id: artist.id } })
}
const cover = computed(() => !props.artist.cover?.urls.large_square_crop
? props.artist.albums.find(album => !!album.cover?.urls.large_square_crop)?.cover
: props.artist.cover
)
const store = useStore()
const imageUrl = computed(() => cover.value?.urls.original
? store.getters['instance/absoluteUrl'](cover.value.urls.large_square_crop)
: `${import.meta.env.BASE_URL}embed-default-cover.jpeg`
)
const cover = computed(() => {
const artistCover: Cover | undefined = artist.cover
const albumCover: Cover | undefined = artist.albums
.find(album => album.cover?.urls.medium_square_crop)?.cover
const trackCover: Cover | undefined =
tracks.value?.find(
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
})
</script>
<template>
<Card
:title="artist.name"
:image="imageUrl"
class="artist-card"
:tags="artist.tags"
:to="{name: 'library.artists.detail', params: {id: artist.id}}"
small
>
<template #topright>
<PlayButton
@ -58,9 +81,15 @@ const imageUrl = computed(() => cover.value?.urls.original
/>
</template>
<template #action>
<Spacer :size="8" />
<i class="bi bi-music-note-list" />
<template #image>
<img
v-lazy="cover.urls.medium_square_crop"
:alt="artist.name"
class="channel-image"
/>
</template>
<template #footer>
<span v-if="artist.content_category === 'music'">
{{ t('components.audio.artist.Card.meta.tracks', artist.tracks_count) }}
</span>
@ -73,6 +102,7 @@ const imageUrl = computed(() => cover.value?.urls.original
:dropdown-only="true"
:is-playable="artist.is_playable"
:artist="artist"
discrete
/>
</template>
@ -80,9 +110,15 @@ const imageUrl = computed(() => cover.value?.urls.original
</template>
<style lang="scss" scoped>
.channel-image {
border-radius: 50%;
width: 168px;
height: 168px;
margin: 16px;
}
.play-button {
position: absolute;
top: 214px;
top: 16px;
right: 16px;
}
</style>

View File

@ -22,7 +22,7 @@ const store = useStore()
const router = useRouter()
const imageUrl = computed(() => props.object.artist?.cover
? store.getters['instance/absoluteUrl'](props.object.artist.cover.urls.large_square_crop)
? store.getters['instance/absoluteUrl'](props.object.artist.cover.urls.medium_square_crop)
: null
)
@ -46,10 +46,10 @@ const updatedAgo = computed(() => moment(props.object.artist?.modification_date)
<template>
<Card
:title="object.artist?.name"
:image="imageUrl"
:tags="object.artist?.tags ?? []"
class="artist-card"
:to="{name: 'channels.detail', params: {id: urlId}}"
small
>
<template #topright>
<PlayButton
@ -59,26 +59,35 @@ const updatedAgo = computed(() => moment(props.object.artist?.modification_date)
/>
</template>
<template #default>
<span
v-if="object.artist?.content_category === 'podcast'"
>
<i class="bi bi-music-note-list" />
{{ t('components.audio.ChannelCard.meta.episodes', object.artist.tracks_count) }}
</span>
<span v-else>
<i class="bi bi-music-note-list" />
{{ t('components.audio.ChannelCard.meta.tracks', object.artist?.tracks_count ?? 0) }}
</span>
<template #image>
<img
v-if="imageUrl"
v-lazy="imageUrl"
:alt="object.artist?.name"
class="channel-image"
/>
</template>
<template #action>
<template #default>
<Spacer :size="8"/>
</template>
<template #footer>
<time
:datetime="object.artist?.modification_date"
:title="updatedTitle"
>
{{ updatedAgo }}
</time>
<i class="bi bi-dot" />
<span
v-if="object.artist?.content_category === 'podcast'"
>
{{ t('components.audio.ChannelCard.meta.episodes', object.artist.tracks_count) }}
</span>
<span v-else>
{{ t('components.audio.ChannelCard.meta.tracks', object.artist?.tracks_count ?? 0) }}
</span>
<Spacer h grow />
<PlayButton
:dropdown-only="true"
@ -86,15 +95,22 @@ const updatedAgo = computed(() => moment(props.object.artist?.modification_date)
:artist="object.artist"
:channel="object"
:account="object.attributed_to"
discrete
/>
</template>
</Card>
</template>
<style lang="scss" scoped>
.channel-image {
border-radius: 50%;
width: 168px;
height: 168px;
margin: 16px;
}
.play-button {
position: absolute;
top: 214px;
top: 16px;
right: 16px;
}
</style>

View File

@ -8,7 +8,10 @@ import { useI18n } from 'vue-i18n'
import axios from 'axios'
import ChannelCard from '~/components/audio/ChannelCard.vue'
import Layout from '~/components/ui/Layout.vue'
import Loader from '~/components/ui/Loader.vue'
import Button from '~/components/ui/Button.vue'
import Spacer from '../ui/Spacer.vue'
interface Events {
(e: 'fetched', channels: BackendResponse<Channel>): void
@ -60,22 +63,16 @@ fetchData()
<template>
<div>
<slot />
<div class="ui hidden divider" />
<div class="ui app-cards cards">
<div
v-if="isLoading"
class="ui inverted active dimmer"
>
<div class="ui loader" />
</div>
<Layout grid>
<Loader v-if="isLoading" />
<channel-card
v-for="object in channels"
:key="object.uuid"
:object="object"
/>
</div>
</Layout>
<template v-if="nextPage">
<div class="ui hidden divider" />
<Spacer />
<Button
v-if="nextPage"
@click="fetchData(nextPage)"

View File

@ -101,13 +101,13 @@ const isOpen = ref(false)
<template>
<Popover
v-if="split || (!discrete && !iconOnly && dropdownOnly)"
v-if="split || (!iconOnly && dropdownOnly)"
v-model:open="isOpen"
>
<OptionsButton
v-if="dropdownOnly"
@click="isOpen = !isOpen"
ghost
:isGhost="discrete"
/>
<Button
v-else

View File

@ -67,7 +67,7 @@ const url = computed(() => {
v-if="avatar"
:actor="actor"
/>
<slot>@{{ repr }}</slot>
<slot>{{ repr }}</slot>
</span>
</Link>
</template>

View File

@ -34,33 +34,22 @@ const defaultAvatarStyle = computed(() => ({ backgroundColor: `#${userColor.valu
:solid="!discrete"
:round="!discrete"
>
<span class="center">
<template v-if="avatar">
<img
v-if="user.avatar && user.avatar.urls.medium_square_crop"
v-lazy="store.getters['instance/absoluteUrl'](user.avatar.urls.medium_square_crop)"
class="ui avatar tiny circular image"
alt=""
>
<span
v-else
:style="defaultAvatarStyle"
class="ui tiny avatar circular label"
>
{{ user.username[0] }}
</span>
&nbsp;
</template>
{{ t('components.common.UserLink.link.username', {username: user.username}) }}
</span>
<template v-if="avatar">
<img
v-if="user.avatar && user.avatar.urls.medium_square_crop"
v-lazy="store.getters['instance/absoluteUrl'](user.avatar.urls.medium_square_crop)"
class="ui avatar tiny circular image"
alt=""
>
<span
v-else
:style="defaultAvatarStyle"
class="ui tiny avatar circular label"
>
{{ user.username[0] }}
</span>
&nbsp;
</template>
{{ t('components.common.UserLink.link.username', {username: user.username}) }}
</Link>
</template>
<style lang="scss" scoped>
a.username {
span.center {
display: flex;
align-items: center;
}
}
</style>

View File

@ -58,6 +58,7 @@ const goToPlaylist = () => {
<Card
:title="playlist.name"
:to="{ name: 'library.playlists.detail', params: { id: playlist.id } }"
small
>
<template #topright>
<PlayButton
@ -81,14 +82,16 @@ const goToPlaylist = () => {
<template #default>
<div class="playlist-meta">
<span>{{ t('vui.by-user') }}</span>
<ActorLink
:actor="playlist.actor"
:avatar="false"
discrete
/>
</div>
</template>
<template #action>
<template #footer>
<div class="playlist-action">
<span>{{ t('components.playlists.Card.meta.tracks', playlist.tracks_count) }}</span>
<PlayButton
@ -117,14 +120,12 @@ const goToPlaylist = () => {
}
.play-button {
position: absolute;
top: 214px;
top: 16px;
right: 16px;
}
.playlist-meta {
display: flex;
align-self: center;
}
.playlist-action {

View File

@ -81,9 +81,9 @@ const toggleRadio = () => {
<template>
<Button
:is-active="running"
:primary="!playOnly"
:secondary="playOnly"
primary
:round="playOnly"
class="play-button"
icon="bi-play-fill"
:square="store.state.auth.authenticated && type === 'custom'"
@click="toggleRadio"

View File

@ -38,7 +38,7 @@ const customRadioId = computed(() => props.customRadio?.id ?? null)
<template>
<Card
v-if="radio.id"
large
small
:title="radio.name"
:to="{name: 'library.radios.detail', params: {id: radio.id}}"
>
@ -55,9 +55,10 @@ const customRadioId = computed(() => props.customRadio?.id ?? null)
<user-link
v-if="radio.user"
:user="radio.user"
:avatar="false"
discrete
class="left floated"
/>
<Spacer />
<div
class="description"
:class="{expanded: isDescriptionExpanded}"
@ -65,10 +66,9 @@ const customRadioId = computed(() => props.customRadio?.id ?? null)
>
{{ radio.description }}
</div>
<Spacer />
</template>
<template #action>
<template #footer>
<Button
v-if="store.state.auth.authenticated && type === 'custom' && radio.user.id === store.state.auth.profile?.id"
primary
@ -81,7 +81,7 @@ const customRadioId = computed(() => props.customRadio?.id ?? null)
</Card>
<Card
v-else
large
small
:title="radio.name"
>
<template #topright>
@ -97,7 +97,9 @@ const customRadioId = computed(() => props.customRadio?.id ?? null)
v-if="radio.user"
discrete
:user="radio.user"
:avatar="false"
/>
<Spacer />
<div
class="description"
:class="{expanded: isDescriptionExpanded}"
@ -108,7 +110,7 @@ const customRadioId = computed(() => props.customRadio?.id ?? null)
<Spacer />
</template>
<template #action>
<template #footer>
<Button
v-if="store.state.auth.authenticated && type === 'custom' && radio.user.id === store.state.auth.profile?.id"
primary
@ -123,8 +125,8 @@ const customRadioId = computed(() => props.customRadio?.id ?? null)
<style lang="scss" scoped>
a.username {
display: flex;
align-self: center;
.play-button {
top: 16px;
right: 16px;
}
</style>

View File

@ -146,10 +146,6 @@ const attributes = computed(() =>
}
}
&:has(>.image) {
text-align: center;
}
>.image {
overflow: hidden;
border-radius: var(--fw-border-radius) var(--fw-border-radius) 0 0;
@ -225,7 +221,6 @@ const attributes = computed(() =>
>.tags {
padding: 0 var(--fw-card-padding);
margin-top: 8px;
align-self: center;
}
>.content {
@ -233,14 +228,16 @@ const attributes = computed(() =>
/* Consider making all line height, vertical paddings, margins and borders,
a multiple of a global vertical rhythm so that side-by-side lines coincide */
line-height: 24px;
margin-top: 16px;
margin-top: 8px;
}
>.footer {
padding: calc(var(--fw-card-padding) - 4px);
display: flex;
align-items: center;
gap: 4px;
align-items: end;
font-size: 0.8125rem;
margin-top: 16px;
>.options-button {
margin-left: auto;

View File

@ -3,18 +3,21 @@ import Button from '../Button.vue'
interface Props {
isSquare?: boolean
isGhost?: boolean
}
const props = withDefaults(defineProps<Props>(), {
isSquare: false
isSquare: false,
isGhost: false
})
</script>
<template>
<Button
icon="bi-three-dots-vertical"
class="options-button"
secondary
:class="['options-button', {'is-ghost': isGhost}]"
:secondary="!isGhost"
:ghost="isGhost"
:round="!isSquare"
/>
</template>

View File

@ -3,6 +3,11 @@
will-change: transform;
transition: all .2s ease;
font-size: 0.6rem !important;
padding: 0.6em !important;
padding: 0.6em;
&.is-ghost {
position: absolute;
bottom: 0px;
right: 0px;
}
}
}