feat: Implement disc numbers when viewing an album (#1845)

This commit is contained in:
James 2024-10-14 15:19:59 +01:00 committed by GitHub
parent e051c5cd11
commit 0c44939288
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 111 additions and 40 deletions

View file

@ -85,7 +85,7 @@
<VirtualScroller
v-slot="{ item }: { item: PlayableRow }"
:item-height="64"
:item-height="calculatedItemHeight"
:items="rows"
@scroll="onScroll"
@scrolled-to-end="$emit('scrolledToEnd')"
@ -94,6 +94,7 @@
:key="item.playable.id"
:item="item"
draggable="true"
:show-disc="showDiscLabel(item.playable)"
@click="onClick(item, $event)"
@dragleave="onDragLeave"
@dragstart="onDragStart(item, $event)"
@ -375,6 +376,50 @@ const onPlay = async (playable: Playable) => {
await playbackService.play(playable)
}
const discIndexMap = computed(() => {
const map: { [key: number]: number } = {}
rows.value.forEach((row, index) => {
const { disc } = row.playable
if (!Object.values(map).includes(disc)) {
map[index] = disc
}
})
return map
})
const noOrOneDiscOnly = computed(() => Object.keys(discIndexMap.value).length <= 1)
const sortingByTrack = computed(() => sortField.value === 'track')
const inAlbumContext = computed(() => context.type === 'Album')
const noDiscLabel = computed(() => noOrOneDiscOnly.value || !sortingByTrack.value || !inAlbumContext.value)
const showDiscLabel = (row: Playable) => {
if (noDiscLabel.value) {
return false
}
const index = findIndex(rows.value, ({ playable }) => playable.id === row.id)
return discIndexMap.value[index] !== undefined
}
const standardSongItemHeight = 64
const discNumberHeight = 32.5
const calculatedItemHeight = computed(() => {
if (noDiscLabel.value) {
return standardSongItemHeight
}
const discCount = Object.keys(discIndexMap.value).length
const totalAdditionalPixels = discCount * discNumberHeight
const totalHeight = (rows.value.length * standardSongItemHeight) + totalAdditionalPixels
const averageHeight = totalHeight / rows.value.length
return averageHeight
})
defineExpose({
getAllPlayablesWithSort,
})

View file

@ -29,9 +29,20 @@ new class extends UnitTestCase {
await this.user.dblClick(screen.getByTestId('song-item'))
expect(emitted().play).toBeTruthy()
})
it('renders disc info when showDisc is true', async () => {
const song = factory('song', {
disc: 2,
title: 'Test Song',
})
const showDisc = true
const { getByText } = this.renderComponent(song, showDisc)
expect(getByText('DISC 2')).toBeTruthy()
})
}
private renderComponent (playable?: Playable) {
private renderComponent (playable?: Playable, showDisc = false) {
playable = playable ?? factory('song')
row = {
@ -42,6 +53,7 @@ new class extends UnitTestCase {
return this.render(SongListItem, {
props: {
item: row,
showDisc,
},
})
}

View file

@ -1,49 +1,58 @@
<template>
<article
:class="{ playing, external, selected: item.selected }"
class="song-item group text-k-text-secondary border-b border-k-border !max-w-full h-[64px] flex
<div>
<h4
v-if="showDisc && item.playable.disc"
class="title text-k-text-primary !flex gap-2 p-2"
>
DISC {{ item.playable.disc }}
</h4>
<article
:class="{ playing, external, selected: item.selected }"
class="song-item group text-k-text-secondary border-b border-k-border !max-w-full h-[64px] flex
items-center transition-[background-color,_box-shadow] ease-in-out duration-200
focus:rounded-md focus focus-within:rounded-md focus:ring-inset focus:ring-1 focus:!ring-k-accent
focus-within:ring-inset focus-within:ring-1 focus-within:!ring-k-accent
hover:bg-white/5 hover:ring-inset hover:ring-1 hover:ring-white/10 hover:rounded-md"
data-testid="song-item"
tabindex="0"
@dblclick.prevent.stop="play"
>
<span class="track-number">
<SoundBars v-if="playable.playback_state === 'Playing'" />
<span v-else class="text-k-text-secondary">
<template v-if="isSong(playable)">{{ playable.track || '' }}</template>
<Icon v-else :icon="faPodcast" />
data-testid="song-item"
tabindex="0"
@dblclick.prevent.stop="play"
>
<span class="track-number">
<SoundBars v-if="playable.playback_state === 'Playing'" />
<span v-else class="text-k-text-secondary">
<template v-if="isSong(playable)">{{ playable.track || '' }}</template>
<Icon v-else :icon="faPodcast" />
</span>
</span>
</span>
<span class="thumbnail leading-none">
<SongThumbnail :playable="playable" />
</span>
<span class="title-artist flex flex-col gap-2 overflow-hidden">
<span class="title text-k-text-primary !flex gap-2 items-center">
<ExternalMark v-if="external" class="!inline-block" />
{{ playable.title }}
<span class="thumbnail leading-none">
<SongThumbnail :playable="playable" />
</span>
<span class="artist">{{ artist }}</span>
</span>
<span class="album">{{ album }}</span>
<template v-if="config.collaborative">
<span class="collaborator">
<UserAvatar :user="collaborator" width="24" />
<span class="title-artist flex flex-col gap-2 overflow-hidden">
<span class="title text-k-text-primary !flex gap-2 items-center">
<ExternalMark v-if="external" class="!inline-block" />
{{ playable.title }}
</span>
<span class="artist">{{ artist }}</span>
</span>
<span :title="playable.collaboration.added_at" class="added-at">{{ playable.collaboration.fmt_added_at }}</span>
</template>
<span class="time">{{ fmtLength }}</span>
<span class="extra">
<LikeButton :playable="playable" />
</span>
</article>
<span class="album">{{ album }}</span>
<template v-if="config.collaborative">
<span class="collaborator">
<UserAvatar :user="collaborator" width="24" />
</span>
<span :title="playable.collaboration.added_at" class="added-at">{{ playable.collaboration.fmt_added_at }}</span>
</template>
<span class="time">{{ fmtLength }}</span>
<span class="extra">
<LikeButton :playable="playable" />
</span>
</article>
</div>
</template>
<script lang="ts" setup>
import { faPodcast } from '@fortawesome/free-solid-svg-icons'
import { computed, toRefs } from 'vue'
import { computed, toRefs, withDefaults } from 'vue'
import { getPlayableProp, isSong, requireInjection, secondsToHis } from '@/utils'
import { useAuthorization, useKoelPlus } from '@/composables'
import { PlayableListConfigKey } from '@/symbols'
@ -54,7 +63,9 @@ import SongThumbnail from '@/components/song/SongThumbnail.vue'
import UserAvatar from '@/components/user/UserAvatar.vue'
import ExternalMark from '@/components/ui/ExternalMark.vue'
const props = defineProps<{ item: PlayableRow }>()
const props = withDefaults(defineProps<{ item: PlayableRow, showDisc: boolean }>(), {
showDisc: false,
})
const emit = defineEmits<{ (e: 'play', playable: Playable): void }>()

View file

@ -1,7 +1,10 @@
// Vitest Snapshot v1
exports[`renders 1`] = `
<article data-v-9a89d9b9="" class="playing song-item group text-k-text-secondary border-b border-k-border !max-w-full h-[64px] flex items-center transition-[background-color,_box-shadow] ease-in-out duration-200 focus:rounded-md focus focus-within:rounded-md focus:ring-inset focus:ring-1 focus:!ring-k-accent focus-within:ring-inset focus-within:ring-1 focus-within:!ring-k-accent hover:bg-white/5 hover:ring-inset hover:ring-1 hover:ring-white/10 hover:rounded-md" data-testid="song-item" tabindex="0"><span data-v-9a89d9b9="" class="track-number"><i data-v-47e95701="" data-v-9a89d9b9="" class="relative flex gap-1 content-between w-[13px] aspect-square"><span data-v-47e95701=""></span><span data-v-47e95701=""></span><span data-v-47e95701=""></span></i></span><span data-v-9a89d9b9="" class="thumbnail leading-none"><button data-v-9a89d9b9="" style="background-image: url(undefined/resources/assets/img/covers/default.svg);" title="Pause" class="song-thumbnail w-[48px] aspect-square bg-cover relative rounded overflow-hidden active:scale-95"><img alt="Cover image" src="https://example.com/cover.jpg" class="w-full aspect-square object-cover" loading="lazy"><span class="absolute top-0 left-0 w-full h-full group-hover:bg-black/40 no-hover:bg-black/40 z-10"></span><span class="absolute flex opacity-0 no-hover:opacity-100 items-center justify-center w-[24px] aspect-square rounded-full top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-k-highlight group-hover:opacity-100 duration-500 transition z-20"><br data-testid="Icon" icon="[object Object]" class="text-white"></span></button></span><span data-v-9a89d9b9="" class="title-artist flex flex-col gap-2 overflow-hidden"><span data-v-9a89d9b9="" class="title text-k-text-primary !flex gap-2 items-center"><!--v-if--> Test Song</span><span data-v-9a89d9b9="" class="artist">Test Artist</span></span><span data-v-9a89d9b9="" class="album">Test Album</span>
<!--v-if--><span data-v-9a89d9b9="" class="time">16:40</span><span data-v-9a89d9b9="" class="extra"><button data-v-9a89d9b9="" class="transition-[color] duration-200 ease-in-out text-k-text-secondary hover:text-k-text-primary" type="button" title="Unlike"><br data-testid="Icon" icon="[object Object]"></button></span>
</article>
<div data-v-9a89d9b9="">
<!--v-if-->
<article data-v-9a89d9b9="" class="playing song-item group text-k-text-secondary border-b border-k-border !max-w-full h-[64px] flex items-center transition-[background-color,_box-shadow] ease-in-out duration-200 focus:rounded-md focus focus-within:rounded-md focus:ring-inset focus:ring-1 focus:!ring-k-accent focus-within:ring-inset focus-within:ring-1 focus-within:!ring-k-accent hover:bg-white/5 hover:ring-inset hover:ring-1 hover:ring-white/10 hover:rounded-md" data-testid="song-item" tabindex="0"><span data-v-9a89d9b9="" class="track-number"><i data-v-47e95701="" data-v-9a89d9b9="" class="relative flex gap-1 content-between w-[13px] aspect-square"><span data-v-47e95701=""></span><span data-v-47e95701=""></span><span data-v-47e95701=""></span></i></span><span data-v-9a89d9b9="" class="thumbnail leading-none"><button data-v-9a89d9b9="" style="background-image: url(undefined/resources/assets/img/covers/default.svg);" title="Pause" class="song-thumbnail w-[48px] aspect-square bg-cover relative rounded overflow-hidden active:scale-95"><img alt="Cover image" src="https://example.com/cover.jpg" class="w-full aspect-square object-cover" loading="lazy"><span class="absolute top-0 left-0 w-full h-full group-hover:bg-black/40 no-hover:bg-black/40 z-10"></span><span class="absolute flex opacity-0 no-hover:opacity-100 items-center justify-center w-[24px] aspect-square rounded-full top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 bg-k-highlight group-hover:opacity-100 duration-500 transition z-20"><br data-testid="Icon" icon="[object Object]" class="text-white"></span></button></span><span data-v-9a89d9b9="" class="title-artist flex flex-col gap-2 overflow-hidden"><span data-v-9a89d9b9="" class="title text-k-text-primary !flex gap-2 items-center"><!--v-if--> Test Song</span><span data-v-9a89d9b9="" class="artist">Test Artist</span></span><span data-v-9a89d9b9="" class="album">Test Album</span>
<!--v-if--><span data-v-9a89d9b9="" class="time">16:40</span><span data-v-9a89d9b9="" class="extra"><button data-v-9a89d9b9="" class="transition-[color] duration-200 ease-in-out text-k-text-secondary hover:text-k-text-primary" type="button" title="Unlike"><br data-testid="Icon" icon="[object Object]"></button></span>
</article>
</div>
`;