mirror of
https://github.com/koel/koel
synced 2024-11-10 06:34:14 +00:00
feat: show a message for empty Artists/Albums/Genres screens (#1734)
This commit is contained in:
parent
ce0ed4a49c
commit
ed6f01ad52
10 changed files with 98 additions and 20 deletions
|
@ -43,6 +43,7 @@ export default abstract class UnitTestCase {
|
|||
|
||||
protected beforeEach (cb?: Closure) {
|
||||
beforeEach(() => {
|
||||
commonStore.state.song_length = 10
|
||||
commonStore.state.allow_download = true
|
||||
commonStore.state.use_i_tunes = true
|
||||
cb && cb()
|
||||
|
@ -51,6 +52,7 @@ export default abstract class UnitTestCase {
|
|||
|
||||
protected afterEach (cb?: Closure) {
|
||||
afterEach(() => {
|
||||
commonStore.state.song_length = 10
|
||||
cleanup()
|
||||
this.restoreAllMocks()
|
||||
isMobile.any = false
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { albumStore, preferenceStore } from '@/stores'
|
||||
import { albumStore, commonStore, preferenceStore } from '@/stores'
|
||||
import { screen, waitFor } from '@testing-library/vue'
|
||||
import AlbumListScreen from './AlbumListScreen.vue'
|
||||
|
||||
|
@ -30,6 +30,13 @@ new class extends UnitTestCase {
|
|||
expect(screen.getAllByTestId('album-card')).toHaveLength(9)
|
||||
})
|
||||
|
||||
it('shows a message when the library is empty', async () => {
|
||||
commonStore.state.song_length = 0
|
||||
await this.renderComponent()
|
||||
|
||||
await waitFor(() => screen.getByTestId('screen-empty-state'))
|
||||
})
|
||||
|
||||
it.each<[ArtistAlbumViewMode]>([['list'], ['thumbnails']])('sets layout from preferences', async (mode) => {
|
||||
preferenceStore.albumsViewMode = mode
|
||||
|
||||
|
|
|
@ -7,7 +7,18 @@
|
|||
</template>
|
||||
</ScreenHeader>
|
||||
|
||||
<ScreenEmptyState v-if="libraryEmpty">
|
||||
<template #icon>
|
||||
<Icon :icon="faCompactDisc" />
|
||||
</template>
|
||||
No albums found.
|
||||
<span class="secondary d-block">
|
||||
{{ isAdmin ? 'Have you set up your library yet?' : 'Contact your administrator to set up your library.' }}
|
||||
</span>
|
||||
</ScreenEmptyState>
|
||||
|
||||
<div
|
||||
v-else
|
||||
ref="scroller"
|
||||
v-koel-overflow-fade
|
||||
:class="`as-${viewMode}`"
|
||||
|
@ -27,15 +38,19 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { faCompactDisc } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
import { albumStore, preferenceStore as preferences } from '@/stores'
|
||||
import { useInfiniteScroll, useMessageToaster, useRouter } from '@/composables'
|
||||
import { albumStore, commonStore, preferenceStore as preferences } from '@/stores'
|
||||
import { useAuthorization, useInfiniteScroll, useMessageToaster, useRouter } from '@/composables'
|
||||
import { logger } from '@/utils'
|
||||
|
||||
import AlbumCard from '@/components/album/AlbumCard.vue'
|
||||
import AlbumCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import ViewModeSwitch from '@/components/ui/ViewModeSwitch.vue'
|
||||
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
|
||||
|
||||
const { isAdmin } = useAuthorization()
|
||||
|
||||
const viewMode = ref<ArtistAlbumViewMode>('thumbnails')
|
||||
const albums = toRef(albumStore.state, 'albums')
|
||||
|
@ -53,6 +68,7 @@ let initialized = false
|
|||
const loading = ref(false)
|
||||
const page = ref<number | null>(1)
|
||||
|
||||
const libraryEmpty = computed(() => commonStore.state.song_length === 0)
|
||||
const itemLayout = computed<ArtistAlbumCardLayout>(() => viewMode.value === 'thumbnails' ? 'full' : 'compact')
|
||||
const moreAlbumsAvailable = computed(() => page.value !== null)
|
||||
const showSkeletons = computed(() => loading.value && albums.value.length === 0)
|
||||
|
@ -66,6 +82,8 @@ const fetchAlbums = async () => {
|
|||
}
|
||||
|
||||
useRouter().onScreenActivated('Albums', async () => {
|
||||
if (libraryEmpty.value) return
|
||||
|
||||
if (!initialized) {
|
||||
viewMode.value = preferences.albumsViewMode || 'thumbnails'
|
||||
initialized = true
|
||||
|
|
|
@ -17,9 +17,7 @@ new class extends UnitTestCase {
|
|||
id: 42,
|
||||
name: 'Led Zeppelin IV',
|
||||
artist_id: 123,
|
||||
artist_name: 'Led Zeppelin',
|
||||
song_count: 10,
|
||||
length: 1_603
|
||||
artist_name: 'Led Zeppelin'
|
||||
})
|
||||
|
||||
const resolveAlbumMock = this.mock(albumStore, 'resolve').mockResolvedValue(album)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { artistStore, preferenceStore } from '@/stores'
|
||||
import { artistStore, commonStore, preferenceStore } from '@/stores'
|
||||
import { screen, waitFor } from '@testing-library/vue'
|
||||
import ArtistListScreen from './ArtistListScreen.vue'
|
||||
|
||||
|
@ -31,6 +31,13 @@ new class extends UnitTestCase {
|
|||
expect(screen.getAllByTestId('artist-card')).toHaveLength(9)
|
||||
})
|
||||
|
||||
it('shows a message when the library is empty', async () => {
|
||||
commonStore.state.song_length = 0
|
||||
await this.renderComponent()
|
||||
|
||||
await waitFor(() => screen.getByTestId('screen-empty-state'))
|
||||
})
|
||||
|
||||
it.each<[ArtistAlbumViewMode]>([['list'], ['thumbnails']])('sets layout:%s from preferences', async (mode) => {
|
||||
preferenceStore.artistsViewMode = mode
|
||||
|
||||
|
|
|
@ -7,7 +7,18 @@
|
|||
</template>
|
||||
</ScreenHeader>
|
||||
|
||||
<ScreenEmptyState v-if="libraryEmpty">
|
||||
<template #icon>
|
||||
<Icon :icon="faMicrophoneSlash" />
|
||||
</template>
|
||||
No artists found.
|
||||
<span class="secondary d-block">
|
||||
{{ isAdmin ? 'Have you set up your library yet?' : 'Contact your administrator to set up your library.' }}
|
||||
</span>
|
||||
</ScreenEmptyState>
|
||||
|
||||
<div
|
||||
v-else
|
||||
ref="scroller"
|
||||
v-koel-overflow-fade
|
||||
:class="`as-${viewMode}`"
|
||||
|
@ -27,15 +38,19 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons'
|
||||
import { computed, ref, toRef, watch } from 'vue'
|
||||
import { artistStore, preferenceStore as preferences } from '@/stores'
|
||||
import { useInfiniteScroll, useMessageToaster, useRouter } from '@/composables'
|
||||
import { artistStore, commonStore, preferenceStore as preferences } from '@/stores'
|
||||
import { useAuthorization, useInfiniteScroll, useMessageToaster, useRouter } from '@/composables'
|
||||
import { logger } from '@/utils'
|
||||
|
||||
import ArtistCard from '@/components/artist/ArtistCard.vue'
|
||||
import ArtistCardSkeleton from '@/components/ui/skeletons/ArtistAlbumCardSkeleton.vue'
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import ViewModeSwitch from '@/components/ui/ViewModeSwitch.vue'
|
||||
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
|
||||
|
||||
const { isAdmin } = useAuthorization()
|
||||
|
||||
const viewMode = ref<ArtistAlbumViewMode>('thumbnails')
|
||||
const artists = toRef(artistStore.state, 'artists')
|
||||
|
@ -53,6 +68,7 @@ let initialized = false
|
|||
const loading = ref(false)
|
||||
const page = ref<number | null>(1)
|
||||
|
||||
const libraryEmpty = computed(() => commonStore.state.song_length === 0)
|
||||
const itemLayout = computed<ArtistAlbumCardLayout>(() => viewMode.value === 'thumbnails' ? 'full' : 'compact')
|
||||
const moreArtistsAvailable = computed(() => page.value !== null)
|
||||
const showSkeletons = computed(() => loading.value && artists.value.length === 0)
|
||||
|
@ -66,6 +82,7 @@ const fetchArtists = async () => {
|
|||
}
|
||||
|
||||
useRouter().onScreenActivated('Artists', async () => {
|
||||
if (libraryEmpty.value) return
|
||||
if (!initialized) {
|
||||
viewMode.value = preferences.artistsViewMode || 'thumbnails'
|
||||
initialized = true
|
||||
|
|
|
@ -16,9 +16,6 @@ new class extends UnitTestCase {
|
|||
artist = factory<Artist>('artist', {
|
||||
id: 42,
|
||||
name: 'Led Zeppelin',
|
||||
album_count: 12,
|
||||
song_count: 53,
|
||||
length: 40_603
|
||||
})
|
||||
|
||||
const resolveArtistMock = this.mock(artistStore, 'resolve').mockResolvedValue(artist)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { expect, it } from 'vitest'
|
||||
import factory from '@/__tests__/factory'
|
||||
import UnitTestCase from '@/__tests__/UnitTestCase'
|
||||
import { genreStore } from '@/stores'
|
||||
import { commonStore, genreStore } from '@/stores'
|
||||
import { screen, waitFor } from '@testing-library/vue'
|
||||
import GenreListScreen from './GenreListScreen.vue'
|
||||
|
||||
|
@ -24,5 +24,17 @@ new class extends UnitTestCase {
|
|||
genres.forEach(genre => screen.getByTitle(`${genre.name}: ${genre.song_count} songs`))
|
||||
})
|
||||
})
|
||||
|
||||
it('shows a message when the library is empty', async () => {
|
||||
commonStore.state.song_length = 0
|
||||
const fetchMock = this.mock(genreStore, 'fetchAll')
|
||||
|
||||
this.render(GenreListScreen)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock).not.toHaveBeenCalled()
|
||||
screen.getByTestId('screen-empty-state')
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,18 @@
|
|||
<template>
|
||||
<section id="genresWrapper">
|
||||
<ScreenHeader layout="collapsed">
|
||||
Genres
|
||||
</ScreenHeader>
|
||||
<div class="main-scroll-wrap">
|
||||
<ScreenHeader layout="collapsed">Genres</ScreenHeader>
|
||||
|
||||
<ScreenEmptyState v-if="libraryEmpty">
|
||||
<template #icon>
|
||||
<Icon :icon="faTags" />
|
||||
</template>
|
||||
No genres found.
|
||||
<span class="secondary d-block">
|
||||
{{ isAdmin ? 'Have you set up your library yet?' : 'Contact your administrator to set up your library.' }}
|
||||
</span>
|
||||
</ScreenEmptyState>
|
||||
|
||||
<div class="main-scroll-wrap" v-else>
|
||||
<ul v-if="genres" class="genres">
|
||||
<li v-for="genre in genres" :key="genre.name" :class="`level-${getLevel(genre)}`">
|
||||
<a
|
||||
|
@ -25,15 +34,22 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { faTags } from '@fortawesome/free-solid-svg-icons'
|
||||
import { maxBy, minBy } from 'lodash'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { genreStore } from '@/stores'
|
||||
import { commonStore, genreStore } from '@/stores'
|
||||
import { pluralize } from '@/utils'
|
||||
import { useAuthorization } from '@/composables'
|
||||
|
||||
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
||||
import GenreItemSkeleton from '@/components/ui/skeletons/GenreItemSkeleton.vue'
|
||||
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
|
||||
|
||||
const { isAdmin } = useAuthorization()
|
||||
|
||||
const genres = ref<Genre[]>()
|
||||
|
||||
const libraryEmpty = computed(() => commonStore.state.song_length === 0)
|
||||
const mostPopular = computed(() => maxBy(genres.value, 'song_count'))
|
||||
const leastPopular = computed(() => minBy(genres.value, 'song_count'))
|
||||
|
||||
|
@ -51,7 +67,10 @@ const getLevel = (genre: Genre) => {
|
|||
return index === -1 ? 5 : index
|
||||
}
|
||||
|
||||
onMounted(async () => genres.value = await genreStore.fetchAll())
|
||||
onMounted(async () => {
|
||||
if (libraryEmpty.value) return
|
||||
genres.value = await genreStore.fetchAll()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { without } from 'lodash'
|
||||
import { reactive } from 'vue'
|
||||
import { http } from '@/services'
|
||||
import { albumStore, overviewStore, songStore } from '@/stores'
|
||||
import { albumStore, commonStore, overviewStore, songStore } from '@/stores'
|
||||
import { logger } from '@/utils'
|
||||
|
||||
interface UploadResult {
|
||||
|
@ -86,6 +86,7 @@ export const uploadService = {
|
|||
|
||||
songStore.syncWithVault(result.song)
|
||||
albumStore.syncWithVault(result.album)
|
||||
commonStore.state.song_length += 1
|
||||
overviewStore.refresh()
|
||||
|
||||
this.proceed() // upload the next file
|
||||
|
|
Loading…
Reference in a new issue