feat: lazy load album/artist images (#1568)

This commit is contained in:
Phan An 2022-10-29 01:59:04 +02:00 committed by GitHub
parent 7c2b432765
commit d7a0b69706
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 70 additions and 41 deletions

View file

@ -88,7 +88,8 @@ export default abstract class UnitTestCase {
directives: {
'koel-clickaway': {},
'koel-focus': {},
'koel-tooltip': {}
'koel-tooltip': {},
'koel-hide-broken-icon': {}
},
components: {
icon: this.stub('icon')

View file

@ -1,5 +1,5 @@
import { createApp } from 'vue'
import { clickaway, focus, tooltip } from '@/directives'
import { clickaway, focus, hideBrokenIcon, tooltip } from '@/directives'
import { FontAwesomeIcon, FontAwesomeLayers } from '@fortawesome/vue-fontawesome'
import { RouterKey } from '@/symbols'
import { routes } from '@/config'
@ -13,6 +13,7 @@ createApp(App)
.directive('koel-focus', focus)
.directive('koel-clickaway', clickaway)
.directive('koel-tooltip', tooltip)
.directive('koel-hide-broken-icon', hideBrokenIcon)
/**
* For Ancelot, the ancient cross of war
* for the holy town of Gods

View file

@ -20,15 +20,17 @@ new class extends UnitTestCase {
return this.render(AlbumCard, {
props: {
album
},
global: {
stubs: {
AlbumArtistThumbnail: this.stub('thumbnail')
}
}
})
}
protected test () {
it('renders', () => {
const { html } = this.renderComponent()
expect(html()).toMatchSnapshot()
})
it('renders', () => expect(this.renderComponent().html()).toMatchSnapshot())
it('downloads', async () => {
const mock = this.mock(downloadService, 'fromAlbum')

View file

@ -1,7 +1,7 @@
// Vitest Snapshot v1
exports[`renders 1`] = `
<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>
<article class="item full" draggable="true" tabindex="0" title="IV by Led Zeppelin" data-v-f01bdc56=""><br data-testid="thumbnail" entity="[object Object]" data-v-f01bdc56="">
<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>

View file

@ -18,25 +18,26 @@ new class extends UnitTestCase {
})
}
protected test () {
it('renders', () => {
const { html } = this.render(ArtistCard, {
props: {
artist
private renderComponent () {
return this.render(ArtistCard, {
props: {
artist
},
global: {
stubs: {
AlbumArtistThumbnail: this.stub('thumbnail')
}
})
expect(html()).toMatchSnapshot()
}
})
}
protected test () {
it('renders', () => expect(this.renderComponent().html()).toMatchSnapshot())
it('downloads', async () => {
const mock = this.mock(downloadService, 'fromArtist')
const { getByTestId } = this.render(ArtistCard, {
props: {
artist
}
})
const { getByTestId } = this.renderComponent()
await fireEvent.click(getByTestId('download-artist'))
expect(mock).toHaveBeenCalledOnce()
@ -45,11 +46,7 @@ new class extends UnitTestCase {
it('does not have an option to download if downloading is disabled', async () => {
commonStore.state.allow_download = false
const { queryByTestId } = this.render(ArtistCard, {
props: {
artist
}
})
const { queryByTestId } = this.renderComponent()
expect(queryByTestId('download-artist')).toBeNull()
})
@ -59,11 +56,7 @@ new class extends UnitTestCase {
const fetchMock = this.mock(songStore, 'fetchForArtist').mockResolvedValue(songs)
const playMock = this.mock(playbackService, 'queueAndPlay')
const { getByTestId } = this.render(ArtistCard, {
props: {
artist
}
})
const { getByTestId } = this.renderComponent()
await fireEvent.click(getByTestId('shuffle-artist'))
await this.tick()

View file

@ -1,7 +1,7 @@
// Vitest Snapshot v1
exports[`renders 1`] = `
<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>
<article class="item full" draggable="true" tabindex="0" title="Led Zeppelin" data-v-f01bdc56=""><br data-testid="thumbnail" entity="[object Object]" data-v-f01bdc56="">
<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>

View file

@ -1,5 +1,6 @@
<template>
<div :style="{ backgroundImage: `url(${song.album_cover ?? ''}), url(${defaultCover})` }" class="cover">
<div :style="{ backgroundImage: `url(${defaultCover})` }" class="cover">
<img v-koel-hide-broken-icon :alt="song.album_name" :src="song.album_cover" loading="lazy"/>
<a class="control" @click.prevent="changeSongState" data-testid="play-control">
<icon :icon="song.playback_state === 'Playing' ? faPause : faPlay" class="text-highlight"/>
</a>
@ -45,6 +46,16 @@ const changeSongState = () => {
align-items: center;
justify-content: center;
img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
&::before {
content: " ";
position: absolute;
@ -55,6 +66,7 @@ const changeSongState = () => {
pointer-events: none;
background: #000;
opacity: 0;
z-index: 1;
@media (hover: none) {
opacity: .7;

View file

@ -1,10 +1,11 @@
<template>
<span
:class="{ droppable }"
:style="backgroundStyle"
:style="{ backgroundImage: `url(${defaultCover})` }"
class="cover"
data-testid="album-artist-thumbnail"
>
<img v-koel-hide-broken-icon :alt="entity.name" :src="image" loading="lazy"/>
<a
class="control control-play"
href
@ -44,14 +45,10 @@ const user = toRef(userStore.state, 'current')
const forAlbum = computed(() => entity.value.type === 'albums')
const sortFields = computed(() => forAlbum.value ? ['disc', 'track'] : ['album_id', 'disc', 'track'])
const backgroundStyle = computed(() => {
const image = forAlbum.value
const image = computed(() => {
return forAlbum.value
? (entity.value as Album).cover || defaultCover
: (entity.value as Artist).image || defaultCover
return {
backgroundImage: `url(${image}), url(${defaultCover})`
}
})
const buttonLabel = computed(() => forAlbum.value
@ -135,6 +132,16 @@ const onDrop = async (event: DragEvent) => {
border-radius: 5px;
overflow: hidden;
img {
width: 100%;
height: 100%;
object-fit: cover;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
&::after {
content: "";
display: block;

View file

@ -1,5 +1,5 @@
// Vitest Snapshot v1
exports[`renders for album 1`] = `<span class="cover" data-testid="album-artist-thumbnail" data-v-e37470a2=""><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>`;
exports[`renders for album 1`] = `<span class="cover" style="background-image: url(undefined/resources/assets/img/covers/default.svg);" data-testid="album-artist-thumbnail" data-v-e37470a2=""><img alt="IV" src="https://test/album.jpg" loading="lazy" data-v-e37470a2=""><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>`;
exports[`renders for artist 1`] = `<span class="cover" data-testid="album-artist-thumbnail" data-v-e37470a2=""><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>`;
exports[`renders for artist 1`] = `<span class="cover" style="background-image: url(undefined/resources/assets/img/covers/default.svg);" data-testid="album-artist-thumbnail" data-v-e37470a2=""><img alt="Led Zeppelin" src="https://test/blimp.jpg" loading="lazy" data-v-e37470a2=""><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>`;

View file

@ -0,0 +1,12 @@
import { Directive } from 'vue'
export const hideBrokenIcon: Directive = {
mounted: async (el: HTMLImageElement) => {
el.addEventListener('error', () => (el.style.visibility = 'hidden'))
// For v-bind, an empty source e.g. :src="emptySrc" will NOT be rendered
// and the error event will not be triggered.
// We'll work around by explicitly setting the src to an empty string, which will trigger the error.
el.src = el.src || ''
}
}

View file

@ -1,3 +1,4 @@
export * from './clickaway'
export * from './focus'
export * from './tooltip'
export * from './hideBrokenIcon'