mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: lazy load album/artist images (#1568)
This commit is contained in:
parent
7c2b432765
commit
d7a0b69706
11 changed files with 70 additions and 41 deletions
|
@ -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')
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>`;
|
||||
|
|
12
resources/assets/js/directives/hideBrokenIcon.ts
Normal file
12
resources/assets/js/directives/hideBrokenIcon.ts
Normal 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 || ''
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
export * from './clickaway'
|
||||
export * from './focus'
|
||||
export * from './tooltip'
|
||||
export * from './hideBrokenIcon'
|
||||
|
|
Loading…
Reference in a new issue