fix(front): consistent small cards
This commit is contained in:
parent
10140959d3
commit
3d710dbb02
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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)"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -67,7 +67,7 @@ const url = computed(() => {
|
|||
v-if="avatar"
|
||||
:actor="actor"
|
||||
/>
|
||||
<slot>@{{ repr }}</slot>
|
||||
<slot>{{ repr }}</slot>
|
||||
</span>
|
||||
</Link>
|
||||
</template>
|
||||
|
|
|
@ -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>
|
||||
|
||||
</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>
|
||||
|
||||
</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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue