chore: slotify album/artist cards (#1545)

This commit is contained in:
Phan An 2022-10-23 19:52:07 +02:00 committed by GitHub
parent e527eccf03
commit c8dbdf9053
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 236 additions and 197 deletions

View file

@ -1,47 +1,43 @@
<template> <template>
<article <ArtistAlbumCard
v-if="showing" v-if="showing"
:class="layout" :entity="album"
:title="`${album.name} by ${album.artist_name}`" :title="`${album.name} by ${album.artist_name}`"
class="item" :layout="layout"
data-testid="album-card" @contextmenu="requestContextMenu"
draggable="true"
tabindex="0"
@dblclick="shuffle" @dblclick="shuffle"
@dragstart="onDragStart" @dragstart="onDragStart"
@contextmenu.prevent="requestContextMenu"
> >
<AlbumThumbnail :entity="album"/> <template #name>
<a :href="`#/album/${album.id}`" class="text-normal" data-testid="name">{{ album.name }}</a>
<footer> <a v-if="isStandardArtist" :href="`#/artist/${album.artist_id}`">{{ album.artist_name }}</a>
<a :href="`#/album/${album.id}`" class="name" data-testid="name">{{ album.name }}</a>
<a v-if="isStandardArtist" :href="`#/artist/${album.artist_id}`" class="artist">{{ album.artist_name }}</a>
<span v-else class="text-secondary">{{ album.artist_name }}</span> <span v-else class="text-secondary">{{ album.artist_name }}</span>
<p class="meta"> </template>
<a
:title="`Shuffle all songs in the album ${album.name}`" <template #meta>
class="shuffle-album" <a
data-testid="shuffle-album" :title="`Shuffle all songs in the album ${album.name}`"
href class="shuffle-album"
role="button" data-testid="shuffle-album"
@click.prevent="shuffle" href
> role="button"
Shuffle @click.prevent="shuffle"
</a> >
<a Shuffle
v-if="allowDownload" </a>
:title="`Download all songs in the album ${album.name}`" <a
class="download-album" v-if="allowDownload"
data-testid="download-album" :title="`Download all songs in the album ${album.name}`"
href class="download-album"
role="button" data-testid="download-album"
@click.prevent="download" href
> role="button"
Download @click.prevent="download"
</a> >
</p> Download
</footer> </a>
</article> </template>
</ArtistAlbumCard>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -52,7 +48,7 @@ import { downloadService, playbackService } from '@/services'
import { useDraggable } from '@/composables' import { useDraggable } from '@/composables'
import { RouterKey } from '@/symbols' import { RouterKey } from '@/symbols'
import AlbumThumbnail from '@/components/ui/AlbumArtistThumbnail.vue' import ArtistAlbumCard from '@/components/ui/ArtistAlbumCard.vue'
const router = requireInjection(RouterKey) const router = requireInjection(RouterKey)
@ -76,7 +72,3 @@ const download = () => downloadService.fromAlbum(album.value)
const onDragStart = (event: DragEvent) => startDragging(event, album.value) const onDragStart = (event: DragEvent) => startDragging(event, album.value)
const requestContextMenu = (event: MouseEvent) => eventBus.emit('ALBUM_CONTEXT_MENU_REQUESTED', event, album.value) const requestContextMenu = (event: MouseEvent) => eventBus.emit('ALBUM_CONTEXT_MENU_REQUESTED', event, album.value)
</script> </script>
<style lang="scss" scoped>
@include artist-album-card();
</style>

View file

@ -1,9 +1,10 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`renders 1`] = ` exports[`renders 1`] = `
<article class="full item" title="IV by Led Zeppelin" data-testid="album-card" draggable="true" tabindex="0" data-v-b204153b=""><span class="cover" data-testid="album-artist-thumbnail" data-v-e37470a2="" data-v-b204153b=""><a class="control control-play" href="" role="button" data-v-e37470a2=""><span class="hidden" data-v-e37470a2="">Play all songs in the album IV</span><span class="icon" data-v-e37470a2=""></span></a></span> <article class="item full" draggable="true" tabindex="0" title="IV by Led Zeppelin" data-v-f01bdc56=""><span class="cover" data-testid="album-artist-thumbnail" data-v-e37470a2="" data-v-f01bdc56=""><a class="control control-play" href="" role="button" data-v-e37470a2=""><span class="hidden" data-v-e37470a2="">Play all songs in the album IV</span><span class="icon" data-v-e37470a2=""></span></a></span>
<footer data-v-b204153b=""><a href="#/album/42" class="name" data-testid="name" data-v-b204153b="">IV</a><a href="#/artist/17" class="artist" data-v-b204153b="">Led Zeppelin</a> <footer data-v-f01bdc56="">
<p class="meta" data-v-b204153b=""><a title="Shuffle all songs in the album IV" class="shuffle-album" data-testid="shuffle-album" href="" role="button" data-v-b204153b=""> Shuffle </a><a title="Download all songs in the album IV" class="download-album" data-testid="download-album" href="" role="button" data-v-b204153b=""> Download </a></p> <div class="name" data-v-f01bdc56=""><a href="#/album/42" class="text-normal" data-testid="name">IV</a><a href="#/artist/17">Led Zeppelin</a></div>
<p class="meta" data-v-f01bdc56=""><a title="Shuffle all songs in the album IV" class="shuffle-album" data-testid="shuffle-album" href="" role="button"> Shuffle </a><a title="Download all songs in the album IV" class="download-album" data-testid="download-album" href="" role="button"> Download </a></p>
</footer> </footer>
</article> </article>
`; `;

View file

@ -1,47 +1,40 @@
<template> <template>
<article <ArtistAlbumCard
v-if="showing" v-if="showing"
:class="layout" :entity="artist"
:layout="layout"
:title="artist.name" :title="artist.name"
class="item" @contextmenu="requestContextMenu"
data-testid="artist-card"
draggable="true"
tabindex="0"
@dblclick="shuffle" @dblclick="shuffle"
@dragstart="onDragStart" @dragstart="onDragStart"
@contextmenu.prevent="requestContextMenu"
> >
<ArtistThumbnail :entity="artist"/> <template #name>
<a :href="`#/artist/${artist.id}`" class="text-normal" data-testid="name">{{ artist.name }}</a>
<footer> </template>
<div class="info"> <template #meta>
<a :href="`#/artist/${artist.id}`" class="name" data-testid="name">{{ artist.name }}</a> <a
</div> :title="`Shuffle all songs by ${artist.name}`"
<p class="meta"> class="shuffle-artist"
<a data-testid="shuffle-artist"
:title="`Shuffle all songs by ${artist.name}`" href
class="shuffle-artist" role="button"
data-testid="shuffle-artist" @click.prevent="shuffle"
href >
role="button" Shuffle
@click.prevent="shuffle" </a>
> <a
Shuffle v-if="allowDownload"
</a> :title="`Download all songs by ${artist.name}`"
<a class="download-artist"
v-if="allowDownload" data-testid="download-artist"
:title="`Download all songs by ${artist.name}`" href
class="download-artist" role="button"
data-testid="download-artist" @click.prevent="download"
href >
role="button" Download
@click.prevent="download" </a>
> </template>
Download </ArtistAlbumCard>
</a>
</p>
</footer>
</article>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -52,7 +45,7 @@ import { downloadService, playbackService } from '@/services'
import { useDraggable } from '@/composables' import { useDraggable } from '@/composables'
import { RouterKey } from '@/symbols' import { RouterKey } from '@/symbols'
import ArtistThumbnail from '@/components/ui/AlbumArtistThumbnail.vue' import ArtistAlbumCard from '@/components/ui/ArtistAlbumCard.vue'
const router = requireInjection(RouterKey) const router = requireInjection(RouterKey)
@ -74,7 +67,3 @@ const download = () => downloadService.fromArtist(artist.value)
const onDragStart = (event: DragEvent) => startDragging(event, artist.value) const onDragStart = (event: DragEvent) => startDragging(event, artist.value)
const requestContextMenu = (event: MouseEvent) => eventBus.emit('ARTIST_CONTEXT_MENU_REQUESTED', event, artist.value) const requestContextMenu = (event: MouseEvent) => eventBus.emit('ARTIST_CONTEXT_MENU_REQUESTED', event, artist.value)
</script> </script>
<style lang="scss" scoped>
@include artist-album-card();
</style>

View file

@ -1,10 +1,10 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`renders 1`] = ` exports[`renders 1`] = `
<article class="full item" title="Led Zeppelin" data-testid="artist-card" draggable="true" tabindex="0" data-v-85d5de45=""><span class="cover" data-testid="album-artist-thumbnail" data-v-e37470a2="" data-v-85d5de45=""><a class="control control-play" href="" role="button" data-v-e37470a2=""><span class="hidden" data-v-e37470a2="">Play all songs by Led Zeppelin</span><span class="icon" data-v-e37470a2=""></span></a></span> <article class="item full" draggable="true" tabindex="0" title="Led Zeppelin" data-v-f01bdc56=""><span class="cover" data-testid="album-artist-thumbnail" data-v-e37470a2="" data-v-f01bdc56=""><a class="control control-play" href="" role="button" data-v-e37470a2=""><span class="hidden" data-v-e37470a2="">Play all songs by Led Zeppelin</span><span class="icon" data-v-e37470a2=""></span></a></span>
<footer data-v-85d5de45=""> <footer data-v-f01bdc56="">
<div class="info" data-v-85d5de45=""><a href="#/artist/42" class="name" data-testid="name" data-v-85d5de45="">Led Zeppelin</a></div> <div class="name" data-v-f01bdc56=""><a href="#/artist/42" class="text-normal" data-testid="name">Led Zeppelin</a></div>
<p class="meta" data-v-85d5de45=""><a title="Shuffle all songs by Led Zeppelin" class="shuffle-artist" data-testid="shuffle-artist" href="" role="button" data-v-85d5de45=""> Shuffle </a><a title="Download all songs by Led Zeppelin" class="download-artist" data-testid="download-artist" href="" role="button" data-v-85d5de45=""> Download </a></p> <p class="meta" data-v-f01bdc56=""><a title="Shuffle all songs by Led Zeppelin" class="shuffle-artist" data-testid="shuffle-artist" href="" role="button"> Shuffle </a><a title="Download all songs by Led Zeppelin" class="download-artist" data-testid="download-artist" href="" role="button"> Download </a></p>
</footer> </footer>
</article> </article>
`; `;

View file

@ -12,7 +12,15 @@ new class extends UnitTestCase {
private async renderComponent () { private async renderComponent () {
albumStore.state.albums = factory<Album>('album', 9) albumStore.state.albums = factory<Album>('album', 9)
const rendered = this.render(AlbumListScreen)
const rendered = this.render(AlbumListScreen, {
global: {
stubs: {
AlbumCard: this.stub('album-card')
}
}
})
await this.router.activateRoute({ path: 'albums', screen: 'Albums' }) await this.router.activateRoute({ path: 'albums', screen: 'Albums' })
return rendered return rendered
} }

View file

@ -12,7 +12,15 @@ new class extends UnitTestCase {
private async renderComponent () { private async renderComponent () {
artistStore.state.artists = factory<Artist>('artist', 9) artistStore.state.artists = factory<Artist>('artist', 9)
const rendered = this.render(ArtistListScreen)
const rendered = this.render(ArtistListScreen, {
global: {
stubs: {
ArtistCard: this.stub('artist-card')
}
}
})
await this.router.activateRoute({ path: 'artists', screen: 'Artists' }) await this.router.activateRoute({ path: 'artists', screen: 'Artists' })
return rendered return rendered
} }

View file

@ -8,7 +8,13 @@ new class extends UnitTestCase {
protected test () { protected test () {
it('displays the albums', () => { it('displays the albums', () => {
overviewStore.state.mostPlayedAlbums = factory<Album>('album', 6) overviewStore.state.mostPlayedAlbums = factory<Album>('album', 6)
expect(this.render(MostPlayedAlbums).getAllByTestId('album-card')).toHaveLength(6) expect(this.render(MostPlayedAlbums, {
global: {
stubs: {
AlbumCard: this.stub('album-card')
}
}
}).getAllByTestId('album-card')).toHaveLength(6)
}) })
} }
} }

View file

@ -8,7 +8,13 @@ new class extends UnitTestCase {
protected test () { protected test () {
it('displays the artists', () => { it('displays the artists', () => {
overviewStore.state.mostPlayedArtists = factory<Artist>('artist', 6) overviewStore.state.mostPlayedArtists = factory<Artist>('artist', 6)
expect(this.render(MostPlayedArtists).getAllByTestId('artist-card')).toHaveLength(6) expect(this.render(MostPlayedArtists, {
global: {
stubs: {
ArtistCard: this.stub('artist-card')
}
}
}).getAllByTestId('artist-card')).toHaveLength(6)
}) })
} }
} }

View file

@ -8,7 +8,13 @@ new class extends UnitTestCase {
protected test () { protected test () {
it('displays the albums', () => { it('displays the albums', () => {
overviewStore.state.recentlyAddedAlbums = factory<Album>('album', 6) overviewStore.state.recentlyAddedAlbums = factory<Album>('album', 6)
expect(this.render(RecentlyAddedAlbums).getAllByTestId('album-card')).toHaveLength(6) expect(this.render(RecentlyAddedAlbums, {
global: {
stubs: {
AlbumCard: this.stub('album-card')
}
}
}).getAllByTestId('album-card')).toHaveLength(6)
}) })
} }
} }

View file

@ -0,0 +1,115 @@
<template>
<article
class="item"
:class="layout"
draggable="true"
tabindex="0"
@dblclick="onDblClick"
@dragstart="onDragStart"
@contextmenu.prevent="onContextMenu"
>
<AlbumArtistThumbnail :entity="entity"/>
<footer>
<div class="name">
<slot name="name"/>
</div>
<p class="meta">
<slot name="meta"/>
</p>
</footer>
</article>
</template>
<script lang="ts" setup>
import AlbumArtistThumbnail from '@/components/ui/AlbumArtistThumbnail.vue'
const props = withDefaults(
defineProps<{ layout?: ArtistAlbumCardLayout, entity: Artist | Album }>(),
{ layout: 'full' }
)
const emit = defineEmits(['dblclick', 'contextmenu', 'dragstart'])
const onDblClick = () => emit('dblclick')
const onDragStart = (e: DragEvent) => emit('dragstart', e)
const onContextMenu = (e: MouseEvent) => emit('contextmenu', e)
</script>
<style lang="scss" scoped>
.item {
position: relative;
max-width: 256px;
background: var(--color-bg-secondary);
border: 1px solid var(--color-bg-secondary);
padding: 16px;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 1.5rem;
.name {
display: flex;
flex-direction: column;
gap: 0.5rem;
white-space: nowrap;
::v-deep(a:link) {
color: var(--color-text-primary);
overflow: hidden;
text-overflow: ellipsis;
}
}
&:focus, &:focus-within {
box-shadow: 0 0 1px 1px var(--color-accent);
}
&.compact {
gap: 1rem;
flex-direction: row;
align-items: center;
max-width: 100%;
padding: 10px;
border-radius: 5px;
.cover {
width: 80px;
border-radius: 5px;
}
}
footer {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
gap: .4rem;
}
.meta {
font-size: .9rem;
display: flex;
gap: .3rem;
opacity: .7;
::v-deep(a) {
& + a {
&::before {
content: '•';
margin-right: .2rem;
color: var(--color-text-secondary);
font-weight: unset;
}
}
}
&:hover {
opacity: 1;
}
}
@media only screen and (max-width: 768px) {
max-width: 100%;
}
}
</style>

View file

@ -24,106 +24,6 @@
} }
} }
@mixin artist-album-card() {
.item {
position: relative;
max-width: 256px;
background: var(--color-bg-secondary);
border: 1px solid var(--color-bg-secondary);
padding: 16px;
border-radius: 8px;
display: flex;
flex-direction: column;
gap: 1.5rem;
@media only screen and (max-width: 768px) {
max-width: 100%;
}
&:focus, &:focus-within {
box-shadow: 0 0 1px 1px var(--color-accent);
}
&:hover .right, &:focus-within .right {
display: flex !important;
}
&.compact {
gap: 1rem;
flex-direction: row;
align-items: center;
max-width: 100%;
padding: 10px;
border-radius: 5px;
.cover {
width: 80px;
border-radius: 5px;
}
}
footer {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
gap: .4rem;
}
.name {
font-weight: var(--font-weight-normal);
}
.meta {
color: var(--color-text-secondary);
font-size: .9rem;
display: flex;
gap: .3rem;
opacity: .7;
a {
border-radius: 3px;
& + a {
&::before {
content: '';
margin-right: .2rem;
color: var(--color-text-secondary);
font-weight: unset;
}
}
}
&:hover {
opacity: 1;
}
}
a.name, a.artist {
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
&:link, &:visited {
color: var(--color-text-primary);
}
&:focus, &:hover {
color: var(--color-accent);
}
}
.info {
.compact & {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
@mixin artist-album-info-wrapper() { @mixin artist-album-info-wrapper() {
.loading { .loading {
@include vertical-center(); @include vertical-center();

View file

@ -241,6 +241,14 @@ label {
font-weight: var(--font-weight-thin) !important; font-weight: var(--font-weight-thin) !important;
} }
&normal {
font-weight: var(--font-weight-normal) !important;
}
&light {
font-weight: var(--font-weight-light) !important;
}
&bold { &bold {
font-weight: var(--font-weight-bold) !important; font-weight: var(--font-weight-bold) !important;
} }