feat(PlayQueue): implement PlayQueueItem component for better queue management and UI

This commit is contained in:
Astrian Zheng 2025-05-27 21:12:52 +10:00
parent f8b860adf3
commit 8eee570200
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA
4 changed files with 154 additions and 55 deletions

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
defineProps<{
size: number
}>()
</script>
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" :class="`w-${size} h-${size}`">
<path
d="M12 10.0858L7.20711 5.29291L5.79289 6.70712L12 12.9142L18.2071 6.70712L16.7929 5.29291L12 10.0858ZM18 17L6 17L6 15L18 15V17Z">
</path>
</svg>
</template>

View File

@ -0,0 +1,13 @@
<script setup lang="ts">
defineProps<{
size: number
}>()
</script>
<template>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" :class="`w-${size} h-${size}`">
<path
d="M12 13.9142L16.7929 18.7071L18.2071 17.2929L12 11.0858L5.79289 17.2929L7.20711 18.7071L12 13.9142ZM6 7L18 7V9L6 9L6 7Z">
</path>
</svg>
</template>

View File

@ -0,0 +1,121 @@
<script setup lang="ts">
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
import { artistsOrganize } from '../utils'
import XIcon from '../assets/icons/x.vue'
import UpHyphenIcon from '../assets/icons/uphypen.vue'
import DownHyphenIcon from '../assets/icons/downhyphen.vue'
import { ref } from 'vue'
const props = defineProps<{
queueItem: QueueItem
isCurrent: boolean
index: number
}>()
const playQueueStore = usePlayQueueStore()
const hover = ref(false)
function removeItem() {
const queue = [...playQueueStore.list]
if (!playQueueStore.playMode.shuffle) {
// shuffle
queue.splice(props.index, 1)
playQueueStore.list = queue
if (props.index < playQueueStore.currentIndex) {
playQueueStore.currentIndex--
} else if (props.index === playQueueStore.currentIndex) {
if (queue.length > 0) {
playQueueStore.currentIndex = Math.min(playQueueStore.currentIndex, queue.length - 1)
} else {
playQueueStore.currentIndex = 0
}
}
} else {
// Shuffle
const originalIndex = playQueueStore.shuffleList[props.index]
const shuffleList = [...playQueueStore.shuffleList]
//
queue.splice(originalIndex, 1)
// shuffle
shuffleList.splice(props.index, 1)
// shuffle
for (let i = 0; i < shuffleList.length; i++) {
if (shuffleList[i] > originalIndex) {
shuffleList[i]--
}
}
playQueueStore.list = queue
playQueueStore.shuffleList = shuffleList
// currentIndex
if (props.index < playQueueStore.currentIndex) {
playQueueStore.currentIndex--
} else if (props.index === playQueueStore.currentIndex) {
if (queue.length > 0) {
playQueueStore.currentIndex = Math.min(playQueueStore.currentIndex, queue.length - 1)
} else {
playQueueStore.currentIndex = 0
}
}
}
}
</script>
<template>
<button class="p-4 w-full rounded-md hover:bg-white/5 first:mt-2 flex gap-2 items-center" @click="() => {
if (isCurrent) { return }
playQueueStore.currentIndex = index
playQueueStore.isPlaying = true
}" @mouseenter="hover = true" @mouseleave="hover = false">
<div class="flex gap-2 flex-auto w-0">
<div class="relative w-12 h-12 rounded-md shadow-xl overflow-hidden">
<img :src="queueItem.album?.coverUrl" />
<div class="w-full h-full absolute top-0 left-0 bg-neutral-900/75 flex justify-center items-center"
v-if="isCurrent">
<div style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
:key="index" :style="{
height: `${Math.max(10, bar)}%`
}" />
</div>
</div>
</div>
<div class="flex flex-col text-left flex-auto w-0">
<div class="text-white text-base font-medium truncate">{{ queueItem.song.name }}</div>
<div class="text-white/75 text-sm truncate">
{{ artistsOrganize(queueItem.song.artists ?? []) }}
{{ queueItem.album?.name ?? '未知专辑' }}
</div>
</div>
</div>
<div class="flex gap-1" v-if="hover">
<button
class="text-white/90 w-4 h-4 hover:scale-110 hover:text-white active:scale-95 active:text-white/85 transition-all"
@click.stop>
<UpHyphenIcon :size="4" />
</button>
<button
class="text-white/90 w-4 h-4 hover:scale-110 hover:text-white active:scale-95 active:text-white/85 transition-all"
@click.stop>
<DownHyphenIcon :size="4" />
</button>
<button
class="text-white/90 w-4 h-4 hover:scale-110 hover:text-white active:scale-95 active:text-white/85 transition-all"
@click.stop="removeItem">
<XIcon :size="4" />
</button>
</div>
</button>
</template>

View File

@ -45,6 +45,8 @@ const presentQueueListDialog = ref(false)
const presentLyrics = ref(false)
const showLyricsTooltip = ref(false)
import PlayQueueItem from '../components/PlayQueueItem.vue'
onMounted(async () => {
Draggable.create(progressBarThumb.value, {
type: 'x',
@ -569,63 +571,13 @@ watch(() => playQueueStore.currentIndex, () => {
<hr class="border-[#ffffff39]" />
<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-if="playQueueStore.playMode.shuffle">
<button v-for="(oriIndex, shuffledIndex) in playQueueStore.shuffleList"
class="p-4 w-full rounded-md hover:bg-white/5 first:mt-2" :key="oriIndex" @click="() => {
if (playQueueStore.currentIndex === shuffledIndex) { return }
playQueueStore.currentIndex = shuffledIndex
playQueueStore.isPlaying = true
}">
<div class="flex gap-2">
<div class="relative w-12 h-12 rounded-md shadow-xl overflow-hidden">
<img :src="playQueueStore.list[oriIndex].album?.coverUrl" />
<div class="w-full h-full absolute top-0 left-0 bg-neutral-900/75 flex justify-center items-center"
v-if="shuffledIndex === playQueueStore.currentIndex">
<div style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
:key="index" :style="{
height: `${Math.max(10, bar)}%`
}" />
</div>
</div>
</div>
<div class="flex flex-col text-left flex-auto w-0">
<div class="text-white text-base font-medium truncate">{{ playQueueStore.list[oriIndex].song.name }}</div>
<div class="text-white/75 text-sm truncate">
{{ artistsOrganize(playQueueStore.list[oriIndex].song.artists ?? []) }}
{{ playQueueStore.list[oriIndex].album?.name ?? '未知专辑' }}
</div>
</div>
</div>
</button>
<PlayQueueItem v-for="(oriIndex, shuffledIndex) in playQueueStore.shuffleList"
:queueItem="playQueueStore.list[oriIndex]" :isCurrent="playQueueStore.currentIndex === shuffledIndex"
:key="playQueueStore.list[oriIndex].song.cid" :index="shuffledIndex" />
</div>
<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-else>
<button v-for="(track, index) in playQueueStore.list" class="p-4 w-full rounded-md hover:bg-white/5 first:mt-2"
:key="track.song.cid" @click="() => {
if (playQueueStore.currentIndex === index) { return }
playQueueStore.currentIndex = index
playQueueStore.isPlaying = true
}">
<div class="flex gap-2">
<div class="relative w-12 h-12 rounded-md shadow-xl overflow-hidden">
<img :src="track.album?.coverUrl" />
<div class="w-full h-full absolute top-0 left-0 bg-neutral-900/75 flex justify-center items-center"
v-if="index === playQueueStore.currentIndex">
<div style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
:key="index" :style="{
height: `${Math.max(10, bar)}%`
}" />
</div>
</div>
</div>
<div class="flex flex-col text-left flex-auto w-0">
<div class="text-white text-base font-medium truncate">{{ track.song.name }}</div>
<div class="text-white/75 text-sm truncate">{{ artistsOrganize(track.song.artists ?? []) }}
{{ track.album?.name ?? '未知专辑' }}
</div>
</div>
</div>
</button>
<PlayQueueItem :queueItem="track" :isCurrent="playQueueStore.currentIndex === index"
v-for="(track, index) in playQueueStore.list" :index="index" :key="track.song.cid" />
</div>
</div>
</dialog>