mirror of
https://github.com/koel/koel
synced 2024-11-28 06:50:27 +00:00
chore: slotify album/artist cards (#1545)
This commit is contained in:
parent
e527eccf03
commit
c8dbdf9053
12 changed files with 236 additions and 197 deletions
|
@ -1,47 +1,43 @@
|
|||
<template>
|
||||
<article
|
||||
<ArtistAlbumCard
|
||||
v-if="showing"
|
||||
:class="layout"
|
||||
:entity="album"
|
||||
:title="`${album.name} by ${album.artist_name}`"
|
||||
class="item"
|
||||
data-testid="album-card"
|
||||
draggable="true"
|
||||
tabindex="0"
|
||||
:layout="layout"
|
||||
@contextmenu="requestContextMenu"
|
||||
@dblclick="shuffle"
|
||||
@dragstart="onDragStart"
|
||||
@contextmenu.prevent="requestContextMenu"
|
||||
>
|
||||
<AlbumThumbnail :entity="album"/>
|
||||
|
||||
<footer>
|
||||
<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>
|
||||
<template #name>
|
||||
<a :href="`#/album/${album.id}`" class="text-normal" data-testid="name">{{ album.name }}</a>
|
||||
<a v-if="isStandardArtist" :href="`#/artist/${album.artist_id}`">{{ album.artist_name }}</a>
|
||||
<span v-else class="text-secondary">{{ album.artist_name }}</span>
|
||||
<p class="meta">
|
||||
<a
|
||||
:title="`Shuffle all songs in the album ${album.name}`"
|
||||
class="shuffle-album"
|
||||
data-testid="shuffle-album"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="shuffle"
|
||||
>
|
||||
Shuffle
|
||||
</a>
|
||||
<a
|
||||
v-if="allowDownload"
|
||||
:title="`Download all songs in the album ${album.name}`"
|
||||
class="download-album"
|
||||
data-testid="download-album"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="download"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<template #meta>
|
||||
<a
|
||||
:title="`Shuffle all songs in the album ${album.name}`"
|
||||
class="shuffle-album"
|
||||
data-testid="shuffle-album"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="shuffle"
|
||||
>
|
||||
Shuffle
|
||||
</a>
|
||||
<a
|
||||
v-if="allowDownload"
|
||||
:title="`Download all songs in the album ${album.name}`"
|
||||
class="download-album"
|
||||
data-testid="download-album"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="download"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</template>
|
||||
</ArtistAlbumCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -52,7 +48,7 @@ import { downloadService, playbackService } from '@/services'
|
|||
import { useDraggable } from '@/composables'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
import AlbumThumbnail from '@/components/ui/AlbumArtistThumbnail.vue'
|
||||
import ArtistAlbumCard from '@/components/ui/ArtistAlbumCard.vue'
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
|
@ -76,7 +72,3 @@ const download = () => downloadService.fromAlbum(album.value)
|
|||
const onDragStart = (event: DragEvent) => startDragging(event, album.value)
|
||||
const requestContextMenu = (event: MouseEvent) => eventBus.emit('ALBUM_CONTEXT_MENU_REQUESTED', event, album.value)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@include artist-album-card();
|
||||
</style>
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
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>
|
||||
<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>
|
||||
<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>
|
||||
<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-f01bdc56="">
|
||||
<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>
|
||||
</article>
|
||||
`;
|
||||
|
|
|
@ -1,47 +1,40 @@
|
|||
<template>
|
||||
<article
|
||||
<ArtistAlbumCard
|
||||
v-if="showing"
|
||||
:class="layout"
|
||||
:entity="artist"
|
||||
:layout="layout"
|
||||
:title="artist.name"
|
||||
class="item"
|
||||
data-testid="artist-card"
|
||||
draggable="true"
|
||||
tabindex="0"
|
||||
@contextmenu="requestContextMenu"
|
||||
@dblclick="shuffle"
|
||||
@dragstart="onDragStart"
|
||||
@contextmenu.prevent="requestContextMenu"
|
||||
>
|
||||
<ArtistThumbnail :entity="artist"/>
|
||||
|
||||
<footer>
|
||||
<div class="info">
|
||||
<a :href="`#/artist/${artist.id}`" class="name" data-testid="name">{{ artist.name }}</a>
|
||||
</div>
|
||||
<p class="meta">
|
||||
<a
|
||||
:title="`Shuffle all songs by ${artist.name}`"
|
||||
class="shuffle-artist"
|
||||
data-testid="shuffle-artist"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="shuffle"
|
||||
>
|
||||
Shuffle
|
||||
</a>
|
||||
<a
|
||||
v-if="allowDownload"
|
||||
:title="`Download all songs by ${artist.name}`"
|
||||
class="download-artist"
|
||||
data-testid="download-artist"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="download"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</p>
|
||||
</footer>
|
||||
</article>
|
||||
<template #name>
|
||||
<a :href="`#/artist/${artist.id}`" class="text-normal" data-testid="name">{{ artist.name }}</a>
|
||||
</template>
|
||||
<template #meta>
|
||||
<a
|
||||
:title="`Shuffle all songs by ${artist.name}`"
|
||||
class="shuffle-artist"
|
||||
data-testid="shuffle-artist"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="shuffle"
|
||||
>
|
||||
Shuffle
|
||||
</a>
|
||||
<a
|
||||
v-if="allowDownload"
|
||||
:title="`Download all songs by ${artist.name}`"
|
||||
class="download-artist"
|
||||
data-testid="download-artist"
|
||||
href
|
||||
role="button"
|
||||
@click.prevent="download"
|
||||
>
|
||||
Download
|
||||
</a>
|
||||
</template>
|
||||
</ArtistAlbumCard>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
@ -52,7 +45,7 @@ import { downloadService, playbackService } from '@/services'
|
|||
import { useDraggable } from '@/composables'
|
||||
import { RouterKey } from '@/symbols'
|
||||
|
||||
import ArtistThumbnail from '@/components/ui/AlbumArtistThumbnail.vue'
|
||||
import ArtistAlbumCard from '@/components/ui/ArtistAlbumCard.vue'
|
||||
|
||||
const router = requireInjection(RouterKey)
|
||||
|
||||
|
@ -74,7 +67,3 @@ const download = () => downloadService.fromArtist(artist.value)
|
|||
const onDragStart = (event: DragEvent) => startDragging(event, artist.value)
|
||||
const requestContextMenu = (event: MouseEvent) => eventBus.emit('ARTIST_CONTEXT_MENU_REQUESTED', event, artist.value)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@include artist-album-card();
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
// Vitest Snapshot v1
|
||||
|
||||
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>
|
||||
<footer data-v-85d5de45="">
|
||||
<div class="info" data-v-85d5de45=""><a href="#/artist/42" class="name" data-testid="name" data-v-85d5de45="">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>
|
||||
<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-f01bdc56="">
|
||||
<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-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>
|
||||
</article>
|
||||
`;
|
||||
|
|
|
@ -12,7 +12,15 @@ new class extends UnitTestCase {
|
|||
|
||||
private async renderComponent () {
|
||||
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' })
|
||||
return rendered
|
||||
}
|
||||
|
|
|
@ -12,7 +12,15 @@ new class extends UnitTestCase {
|
|||
|
||||
private async renderComponent () {
|
||||
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' })
|
||||
return rendered
|
||||
}
|
||||
|
|
|
@ -8,7 +8,13 @@ new class extends UnitTestCase {
|
|||
protected test () {
|
||||
it('displays the albums', () => {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,13 @@ new class extends UnitTestCase {
|
|||
protected test () {
|
||||
it('displays the artists', () => {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,13 @@ new class extends UnitTestCase {
|
|||
protected test () {
|
||||
it('displays the albums', () => {
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
115
resources/assets/js/components/ui/ArtistAlbumCard.vue
Normal file
115
resources/assets/js/components/ui/ArtistAlbumCard.vue
Normal 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>
|
|
@ -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() {
|
||||
.loading {
|
||||
@include vertical-center();
|
||||
|
|
|
@ -241,6 +241,14 @@ label {
|
|||
font-weight: var(--font-weight-thin) !important;
|
||||
}
|
||||
|
||||
&normal {
|
||||
font-weight: var(--font-weight-normal) !important;
|
||||
}
|
||||
|
||||
&light {
|
||||
font-weight: var(--font-weight-light) !important;
|
||||
}
|
||||
|
||||
&bold {
|
||||
font-weight: var(--font-weight-bold) !important;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue