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 <VirtualScroller
v-slot="{ item }: { item: PlayableRow }" v-slot="{ item }: { item: PlayableRow }"
:item-height="64" :item-height="calculatedItemHeight"
:items="rows" :items="rows"
@scroll="onScroll" @scroll="onScroll"
@scrolled-to-end="$emit('scrolledToEnd')" @scrolled-to-end="$emit('scrolledToEnd')"
@ -94,6 +94,7 @@
:key="item.playable.id" :key="item.playable.id"
:item="item" :item="item"
draggable="true" draggable="true"
:show-disc="showDiscLabel(item.playable)"
@click="onClick(item, $event)" @click="onClick(item, $event)"
@dragleave="onDragLeave" @dragleave="onDragLeave"
@dragstart="onDragStart(item, $event)" @dragstart="onDragStart(item, $event)"
@ -375,6 +376,50 @@ const onPlay = async (playable: Playable) => {
await playbackService.play(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({ defineExpose({
getAllPlayablesWithSort, getAllPlayablesWithSort,
}) })

View file

@ -29,9 +29,20 @@ new class extends UnitTestCase {
await this.user.dblClick(screen.getByTestId('song-item')) await this.user.dblClick(screen.getByTestId('song-item'))
expect(emitted().play).toBeTruthy() 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') playable = playable ?? factory('song')
row = { row = {
@ -42,6 +53,7 @@ new class extends UnitTestCase {
return this.render(SongListItem, { return this.render(SongListItem, {
props: { props: {
item: row, item: row,
showDisc,
}, },
}) })
} }

View file

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

View file

@ -1,7 +1,10 @@
// Vitest Snapshot v1 // Vitest Snapshot v1
exports[`renders 1`] = ` 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> <div data-v-9a89d9b9="">
<!--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> <!--v-if-->
</article> <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>
`; `;