feat(播放器): 添加随机播放功能

实现播放队列的随机播放模式,包括:
1. 在 store 中添加 shuffleList 和 playMode 状态
2. 修改播放器组件以支持随机播放时的歌曲切换
3. 更新播放界面以显示随机播放队列
4. 添加随机播放按钮交互逻辑

当开启随机播放时,会生成随机播放列表并保持当前播放歌曲不变,关闭时可恢复原播放顺序。
This commit is contained in:
Astrian Zheng 2025-05-26 13:20:08 +10:00
parent b6574d8093
commit d99ae28f8c
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA
3 changed files with 114 additions and 19 deletions

View File

@ -35,12 +35,19 @@ function artistsOrganize(list: string[]) {
function setMetadata() { function setMetadata() {
if ('mediaSession' in navigator) { if ('mediaSession' in navigator) {
let current = (() => {
if (playQueueStore.playMode.shuffle) {
return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
} else {
return playQueueStore.list[playQueueStore.currentIndex]
}
})()
navigator.mediaSession.metadata = new MediaMetadata({ navigator.mediaSession.metadata = new MediaMetadata({
title: playQueueStore.list[playQueueStore.currentIndex].song.name, title: current.song.name,
artist: artistsOrganize(playQueueStore.list[playQueueStore.currentIndex].song.artists ?? []), artist: artistsOrganize(current.song.artists ?? []),
album: playQueueStore.list[playQueueStore.currentIndex].album?.name, album: current.album?.name,
artwork: [ artwork: [
{ src: playQueueStore.list[playQueueStore.currentIndex].album?.coverUrl ?? '', sizes: '500x500', type: 'image/png' }, { src: current.album?.coverUrl ?? '', sizes: '500x500', type: 'image/png' },
] ]
}) })
@ -140,12 +147,50 @@ watch(() => error.value, (newError) => {
console.error('[Player] 可视化器错误:', newError) console.error('[Player] 可视化器错误:', newError)
} }
}) })
//
watch(() => playQueueStore.playMode.shuffle, (isShuffle) => {
if (isShuffle) {
//
const currentIndex = playQueueStore.currentIndex
const trackCount = playQueueStore.list.length
// 便
//
let shuffledList = [...Array(currentIndex).keys()]
// shuffleCurrent false undefined
if (!playQueueStore.shuffleCurrent) {
shuffledList.push(currentIndex)
}
//
let shuffleSpace = [...Array(trackCount).keys()]
shuffleSpace = shuffleSpace.filter((item) => item > currentIndex)
console.log(shuffleSpace)
//
shuffleSpace.sort(() => Math.random() - 0.5)
//
shuffledList = shuffledList.concat(shuffleSpace)
//
playQueueStore.shuffleList = shuffledList
} else {
// currentIndex
playQueueStore.currentIndex = playQueueStore.shuffleList[playQueueStore.currentIndex]
}
})
</script> </script>
<template> <template>
<div> <div>
<audio <audio
:src="playQueueStore.list[playQueueStore.currentIndex] ? playQueueStore.list[playQueueStore.currentIndex].song.sourceUrl : ''" :src="(() => {
if (playQueueStore.playMode.shuffle) {
return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]] ? playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]].song.sourceUrl : ''
} else {
return playQueueStore.list[playQueueStore.currentIndex] ? playQueueStore.list[playQueueStore.currentIndex].song.sourceUrl : ''
}
})()"
ref="playerRef" ref="playerRef"
:autoplay="playQueueStore.isPlaying" :autoplay="playQueueStore.isPlaying"
v-if="playQueueStore.list.length !== 0" v-if="playQueueStore.list.length !== 0"

View File

@ -134,39 +134,47 @@ function makePlayQueueListDismiss() {
ease: 'power2.out' ease: 'power2.out'
}) })
} }
function getCurrentTrack() {
if (playQueueStore.playMode.shuffle) {
return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
} else {
return playQueueStore.list[playQueueStore.currentIndex]
}
}
</script> </script>
<template> <template>
<div class="z-0 absolute top-0 left-0 w-screen h-screen" <div class="z-0 absolute top-0 left-0 w-screen h-screen"
v-if="playQueueStore.list[playQueueStore.currentIndex].album?.coverDeUrl"> v-if="getCurrentTrack().album?.coverDeUrl">
<img class="w-full h-full blur-2xl object-cover" <img class="w-full h-full blur-2xl object-cover"
:src="playQueueStore.list[playQueueStore.currentIndex].album?.coverDeUrl" /> :src="getCurrentTrack().album?.coverDeUrl" />
<div class="bg-transparent w-full h-full absolute top-0 left-0" /> <div class="bg-transparent w-full h-full absolute top-0 left-0" />
</div> </div>
<div class="w-full flex justify-center items-center my-auto gap-16 z-10 select-none"> <div class="w-full flex justify-center items-center my-auto gap-16 z-10 select-none">
<div class="flex flex-col w-96 gap-4"> <div class="flex flex-col w-96 gap-4">
<img :src="playQueueStore.list[playQueueStore.currentIndex].album?.coverUrl" <img :src="getCurrentTrack().album?.coverUrl"
class="rounded-2xl shadow-2xl border border-white/20 w-96 h-96" /> class="rounded-2xl shadow-2xl border border-white/20 w-96 h-96" />
<div class="flex justify-between items-center"> <div class="flex justify-between items-center">
<div class="relative flex-auto w-0"> <div class="relative flex-auto w-0">
<div class=""> <div class="">
<div class="text-black/90 blur-lg text-lg font-medium truncate"> <div class="text-black/90 blur-lg text-lg font-medium truncate">
{{ playQueueStore.list[playQueueStore.currentIndex].song.name }} {{ getCurrentTrack().song.name }}
</div> </div>
<div class="text-black/90 blur-lg text-base truncate"> <div class="text-black/90 blur-lg text-base truncate">
{{ artistsOrganize(playQueueStore.list[playQueueStore.currentIndex].song.artists ?? []) }} {{ getCurrentTrack().song.artists ?? [] }}
{{ playQueueStore.list[playQueueStore.currentIndex].album?.name ?? '未知专辑' }} {{ getCurrentTrack().album?.name ?? '未知专辑' }}
</div> </div>
</div> </div>
<div class="absolute top-0"> <div class="absolute top-0">
<div class="text-white text-lg font-medium truncate"> <div class="text-white text-lg font-medium truncate">
{{ playQueueStore.list[playQueueStore.currentIndex].song.name }} {{ getCurrentTrack().song.name }}
</div> </div>
<div class="text-white/75 text-base truncate"> <div class="text-white/75 text-base truncate">
{{ artistsOrganize(playQueueStore.list[playQueueStore.currentIndex].song.artists ?? []) }} {{ artistsOrganize(getCurrentTrack().song.artists ?? []) }}
{{ playQueueStore.list[playQueueStore.currentIndex].album?.name ?? '未知专辑' }} {{ getCurrentTrack().album?.name ?? '未知专辑' }}
</div> </div>
</div> </div>
@ -337,8 +345,12 @@ function makePlayQueueListDismiss() {
<div class="flex gap-2 mx-8 mb-4"> <div class="flex gap-2 mx-8 mb-4">
<button <button
class="text-white flex-1 h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center" class="flex-1 h-9 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center"
@click=""> :class="playQueueStore.playMode.shuffle ? 'bg-[#ffffffaa] text-neutral-700' : 'text-white bg-neutral-800/80'"
@click="() => {
playQueueStore.playMode.shuffle = !playQueueStore.playMode.shuffle
playQueueStore.shuffleCurrent = false
}">
<ShuffleIcon :size="4" /> <ShuffleIcon :size="4" />
</button> </button>
<button <button
@ -350,7 +362,36 @@ function makePlayQueueListDismiss() {
<hr class="border-[#ffffff39]" /> <hr class="border-[#ffffff39]" />
<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2"> <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>
</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" <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="() => { :key="track.song.cid" @click="() => {
if (playQueueStore.currentIndex === index) { return } if (playQueueStore.currentIndex === index) { return }

View File

@ -10,7 +10,16 @@ export const usePlayQueueStore = defineStore('queue', () =>{
const currentTime = ref<number>(0) const currentTime = ref<number>(0)
const duration = ref<number>(0) const duration = ref<number>(0)
const updatedCurrentTime = ref<number | null>(null) const updatedCurrentTime = ref<number | null>(null)
const visualizer = ref<number[]>([0, 0, 0, 0]) const visualizer = ref<number[]>([0, 0, 0, 0, 0, 0])
const shuffleList = ref<number[]>([])
const playMode = ref<{
shuffle: boolean,
repeat: 'off' | 'single' | 'all'
}>({
shuffle: false,
repeat: 'off'
})
const shuffleCurrent = ref<boolean | undefined>(undefined)
return { list, currentIndex, isPlaying, queueReplaceLock, isBuffering, currentTime, duration, updatedCurrentTime, visualizer } return { list, currentIndex, isPlaying, queueReplaceLock, isBuffering, currentTime, duration, updatedCurrentTime, visualizer, shuffleList, playMode, shuffleCurrent }
}) })