feat(PlayQueue): implement PlayQueueItem component for better queue management and UI
This commit is contained in:
parent
f8b860adf3
commit
8eee570200
13
src/assets/icons/downhyphen.vue
Normal file
13
src/assets/icons/downhyphen.vue
Normal 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>
|
13
src/assets/icons/uphypen.vue
Normal file
13
src/assets/icons/uphypen.vue
Normal 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>
|
121
src/components/PlayQueueItem.vue
Normal file
121
src/components/PlayQueueItem.vue
Normal 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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user