mirror of
https://github.com/koel/koel
synced 2024-11-12 23:47:09 +00:00
feat: Implement disc numbers when viewing an album (#1845)
This commit is contained in:
parent
e051c5cd11
commit
0c44939288
4 changed files with 111 additions and 40 deletions
|
@ -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,
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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 }>()
|
||||
|
||||
|
|
|
@ -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>
|
||||
`;
|
||||
|
|
Loading…
Reference in a new issue