2022-10-21 20:06:43 +00:00
|
|
|
<template>
|
2024-04-04 22:20:42 +00:00
|
|
|
<ScreenBase>
|
|
|
|
<template #header>
|
|
|
|
<ScreenHeader layout="collapsed">Genres</ScreenHeader>
|
|
|
|
</template>
|
2023-12-28 22:32:58 +00:00
|
|
|
|
|
|
|
<ScreenEmptyState v-if="libraryEmpty">
|
|
|
|
<template #icon>
|
2024-10-14 08:25:23 +00:00
|
|
|
<GuitarIcon size="96" />
|
2023-12-28 22:32:58 +00:00
|
|
|
</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>
|
|
|
|
|
2024-07-09 22:17:13 +00:00
|
|
|
<template v-else>
|
|
|
|
<ul v-if="genres" class="genres text-center">
|
|
|
|
<li
|
|
|
|
v-for="genre in genres"
|
|
|
|
:key="genre.name"
|
|
|
|
:class="`level-${getLevel(genre)}`"
|
|
|
|
class="rounded-[0.5em] inline-block m-1.5 align-middle overflow-hidden"
|
2024-04-04 22:20:42 +00:00
|
|
|
>
|
2024-07-09 22:17:13 +00:00
|
|
|
<a
|
|
|
|
:href="`/#/genres/${encodeURIComponent(genre.name)}`"
|
|
|
|
:title="`${genre.name}: ${pluralize(genre.song_count, 'song')}`"
|
|
|
|
class="bg-white/15 inline-flex items-center justify-center !text-k-text-secondary
|
|
|
|
transition-colors duration-200 ease-in-out hover:!text-k-text-primary hover:bg-k-highlight"
|
|
|
|
>
|
|
|
|
<span class="name bg-white/5 px-[0.5em] py-[0.2em] leading-normal">{{ genre.name }}</span>
|
|
|
|
<span class="count items-center px-[0.5em] py-[0.2em]">
|
2024-10-13 17:37:01 +00:00
|
|
|
{{ genre.song_count }}
|
|
|
|
</span>
|
2024-07-09 22:17:13 +00:00
|
|
|
</a>
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
<ul v-else class="text-center">
|
|
|
|
<li v-for="i in 20" :key="i" class="inline-block">
|
|
|
|
<GenreItemSkeleton />
|
|
|
|
</li>
|
|
|
|
</ul>
|
|
|
|
</template>
|
2024-04-04 22:20:42 +00:00
|
|
|
</ScreenBase>
|
2022-10-21 20:06:43 +00:00
|
|
|
</template>
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2024-10-14 08:25:23 +00:00
|
|
|
import { GuitarIcon } from 'lucide-vue-next'
|
2022-10-21 20:06:43 +00:00
|
|
|
import { maxBy, minBy } from 'lodash'
|
|
|
|
import { computed, onMounted, ref } from 'vue'
|
2023-12-28 22:32:58 +00:00
|
|
|
import { commonStore, genreStore } from '@/stores'
|
2022-10-21 20:06:43 +00:00
|
|
|
import { pluralize } from '@/utils'
|
2024-06-05 15:49:23 +00:00
|
|
|
import { useAuthorization, useErrorHandler } from '@/composables'
|
2023-12-28 22:32:58 +00:00
|
|
|
|
2022-10-21 20:06:43 +00:00
|
|
|
import ScreenHeader from '@/components/ui/ScreenHeader.vue'
|
|
|
|
import GenreItemSkeleton from '@/components/ui/skeletons/GenreItemSkeleton.vue'
|
2023-12-28 22:32:58 +00:00
|
|
|
import ScreenEmptyState from '@/components/ui/ScreenEmptyState.vue'
|
2024-04-04 22:20:42 +00:00
|
|
|
import ScreenBase from '@/components/screens/ScreenBase.vue'
|
2023-12-28 22:32:58 +00:00
|
|
|
|
|
|
|
const { isAdmin } = useAuthorization()
|
2024-07-09 22:17:13 +00:00
|
|
|
const { handleHttpError } = useErrorHandler()
|
2022-10-21 20:06:43 +00:00
|
|
|
|
|
|
|
const genres = ref<Genre[]>()
|
|
|
|
|
2023-12-28 22:32:58 +00:00
|
|
|
const libraryEmpty = computed(() => commonStore.state.song_length === 0)
|
2022-10-21 20:06:43 +00:00
|
|
|
const mostPopular = computed(() => maxBy(genres.value, 'song_count'))
|
|
|
|
const leastPopular = computed(() => minBy(genres.value, 'song_count'))
|
|
|
|
|
|
|
|
const levels = computed(() => {
|
|
|
|
const max = mostPopular.value?.song_count || 1
|
|
|
|
const min = leastPopular.value?.song_count || 1
|
|
|
|
const range = max - min
|
|
|
|
const step = range / 5
|
|
|
|
|
|
|
|
return [min, min + step, min + step * 2, min + step * 3, min + step * 4, max]
|
|
|
|
})
|
|
|
|
|
|
|
|
const getLevel = (genre: Genre) => {
|
2024-10-13 17:37:01 +00:00
|
|
|
const index = levels.value.findIndex(level => genre.song_count <= level)
|
2022-10-21 20:06:43 +00:00
|
|
|
return index === -1 ? 5 : index
|
|
|
|
}
|
|
|
|
|
2024-06-05 15:49:23 +00:00
|
|
|
const fetchGenres = async () => {
|
|
|
|
try {
|
|
|
|
genres.value = await genreStore.fetchAll()
|
|
|
|
} catch (error: unknown) {
|
|
|
|
handleHttpError(error)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-28 22:32:58 +00:00
|
|
|
onMounted(async () => {
|
2024-10-13 17:37:01 +00:00
|
|
|
if (libraryEmpty.value) {
|
|
|
|
return
|
|
|
|
}
|
2024-06-05 15:49:23 +00:00
|
|
|
await fetchGenres()
|
2023-12-28 22:32:58 +00:00
|
|
|
})
|
2022-10-21 20:06:43 +00:00
|
|
|
</script>
|
|
|
|
|
2024-04-04 20:13:35 +00:00
|
|
|
<style lang="postcss" scoped>
|
2022-10-21 20:06:43 +00:00
|
|
|
.genres {
|
|
|
|
li {
|
|
|
|
font-size: var(--unit);
|
|
|
|
|
2024-04-04 22:20:42 +00:00
|
|
|
&:active {
|
|
|
|
@apply scale-95;
|
2022-10-21 20:06:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-04-04 20:13:35 +00:00
|
|
|
.level-0 {
|
|
|
|
--unit: 1rem;
|
2024-04-04 22:20:42 +00:00
|
|
|
@apply opacity-80;
|
2024-04-04 20:13:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.level-1 {
|
|
|
|
--unit: 1.4rem;
|
2024-04-04 22:20:42 +00:00
|
|
|
@apply opacity-[84%];
|
2024-04-04 20:13:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.level-2 {
|
|
|
|
--unit: 1.8rem;
|
2024-04-04 22:20:42 +00:00
|
|
|
@apply opacity-[88%];
|
2024-04-04 20:13:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.level-3 {
|
|
|
|
--unit: 2.2rem;
|
2024-04-04 22:20:42 +00:00
|
|
|
@apply opacity-[92%];
|
2024-04-04 20:13:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.level-4 {
|
|
|
|
--unit: 2.6rem;
|
2024-04-04 22:20:42 +00:00
|
|
|
@apply opacity-[96%];
|
2024-04-04 20:13:35 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
.level-5 {
|
|
|
|
--unit: 3rem;
|
2024-04-04 22:20:42 +00:00
|
|
|
@apply opacity-100;
|
2022-10-21 20:06:43 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
</style>
|