chore: 重构播放列表
This commit is contained in:
parent
a139d1278a
commit
210700bc0d
|
@ -1,6 +1,6 @@
|
|||
<script setup lang="ts">
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Player from './components/Player.vue'
|
||||
import MiniPlayer from './components/MiniPlayer.vue'
|
||||
import PreferencePanel from './components/PreferencePanel.vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
|
@ -73,7 +73,7 @@ watch(() => presentPreferencePanel, (value) => {
|
|||
<CorgIcon :size="4" />
|
||||
</button>
|
||||
|
||||
<Player />
|
||||
<MiniPlayer />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -112,35 +112,19 @@ watch(() => props.albumCid, async () => {
|
|||
|
||||
const playQueue = usePlayQueueStore()
|
||||
|
||||
function playTheAlbum(from: number = 0) {
|
||||
if (playQueue.queueReplaceLock) {
|
||||
if (!confirm("当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?")) { return }
|
||||
playQueue.queueReplaceLock = false
|
||||
}
|
||||
|
||||
let newPlayQueue = []
|
||||
for (const track of album.value?.songs ?? []) {
|
||||
console.log(track)
|
||||
newPlayQueue.push({
|
||||
song: track,
|
||||
album: album.value
|
||||
})
|
||||
}
|
||||
playQueue.list = newPlayQueue
|
||||
playQueue.currentIndex = from
|
||||
playQueue.isPlaying = true
|
||||
playQueue.isBuffering = true
|
||||
async function playTheAlbum(from: number = 0) {
|
||||
await playQueue.replaceQueue(album.value?.songs ?? [])
|
||||
}
|
||||
|
||||
function shuffle() {
|
||||
playTheAlbum()
|
||||
playQueue.shuffleCurrent = true
|
||||
playQueue.playMode.shuffle = false
|
||||
setTimeout(() => {
|
||||
playQueue.playMode.shuffle = true
|
||||
playQueue.isPlaying = true
|
||||
playQueue.isBuffering = true
|
||||
}, 100)
|
||||
// playTheAlbum()
|
||||
// playQueue.shuffleCurrent = true
|
||||
// playQueue.playMode.shuffle = false
|
||||
// setTimeout(() => {
|
||||
// playQueue.playMode.shuffle = true
|
||||
// playQueue.isPlaying = true
|
||||
// playQueue.isBuffering = true
|
||||
// }, 100)
|
||||
}
|
||||
|
||||
</script>
|
||||
|
@ -235,4 +219,4 @@ function shuffle() {
|
|||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
<script setup lang="ts"></script>
|
||||
<script setup lang="ts">
|
||||
import { useRoute } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="h-9 w-52 bg-neutral-800/80 border border-[#ffffff39] rounded-full backdrop-blur-3xl flex items-center justify-between select-none">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="rounded-full w-9 h-9 bg-gray-600" />
|
||||
<div class="text-white">Song title</div>
|
||||
<RouterLink to="/playroom">
|
||||
<div
|
||||
class="h-9 w-52 bg-neutral-800/80 border border-[#ffffff39] rounded-full backdrop-blur-3xl flex items-center justify-between select-none">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="rounded-full w-9 h-9 bg-gray-600" />
|
||||
<div class="text-white">Song title</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
|
0
src/composables/useMediaController.ts
Normal file
0
src/composables/useMediaController.ts
Normal file
|
@ -176,12 +176,12 @@ function updateAudioVolume() {
|
|||
}
|
||||
|
||||
function formatDetector() {
|
||||
const format = playQueueStore.list[playQueueStore.currentIndex].song.sourceUrl?.split('.').pop()
|
||||
/* const format = playQueueStore.list[playQueueStore.currentIndex].sourceUrl?.split('.').pop()
|
||||
if (format === 'mp3') { return 'MP3' }
|
||||
if (format === 'flac') { return 'FLAC' }
|
||||
if (format === 'm4a') { return 'M4A' }
|
||||
if (format === 'ape') { return 'APE' }
|
||||
if (format === 'wav') { return 'WAV' }
|
||||
if (format === 'wav') { return 'WAV' } */
|
||||
return '未知格式'
|
||||
}
|
||||
|
||||
|
@ -305,14 +305,11 @@ function makePlayQueueListDismiss() {
|
|||
}
|
||||
|
||||
function getCurrentTrack() {
|
||||
if (playQueueStore.list.length === 0) {
|
||||
console.log(playQueueStore.queue)
|
||||
if (playQueueStore.queue.length === 0) {
|
||||
return null
|
||||
}
|
||||
if (playQueueStore.playMode.shuffle) {
|
||||
return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
|
||||
} else {
|
||||
return playQueueStore.list[playQueueStore.currentIndex]
|
||||
}
|
||||
return playQueueStore.currentTrack
|
||||
}
|
||||
|
||||
function toggleMoreOptions() {
|
||||
|
@ -826,22 +823,22 @@ watch(() => playQueueStore.currentIndex, () => {
|
|||
<div class="flex gap-2 mx-8 mb-4">
|
||||
<button
|
||||
class="flex-1 h-9 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:scale-105"
|
||||
:class="playQueueStore.playMode.shuffle ? 'bg-[#ffffffaa] text-neutral-700' : 'text-white bg-neutral-800/80'"
|
||||
:class="playQueueStore.isShuffle ? 'bg-[#ffffffaa] text-neutral-700' : 'text-white bg-neutral-800/80'"
|
||||
@click="toggleShuffle">
|
||||
<ShuffleIcon :size="4" />
|
||||
</button>
|
||||
<button
|
||||
class="flex-1 h-9 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:scale-105"
|
||||
:class="playQueueStore.playMode.repeat === 'off' ? 'text-white bg-neutral-800/80' : 'bg-[#ffffffaa] text-neutral-700'"
|
||||
:class="playQueueStore.loopMode === 'off' ? 'text-white bg-neutral-800/80' : 'bg-[#ffffffaa] text-neutral-700'"
|
||||
@click="toggleRepeat">
|
||||
<CycleTwoArrowsIcon :size="4" v-if="playQueueStore.playMode.repeat !== 'single'" />
|
||||
<CycleTwoArrowsIcon :size="4" v-if="playQueueStore.loopMode !== 'single'" />
|
||||
<CycleTwoArrowsWithNumOneIcon :size="4" v-else />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-if="playQueueStore.isShuffle">
|
||||
<PlayQueueItem v-for="(oriIndex, shuffledIndex) in playQueueStore.shuffleList"
|
||||
:queueItem="playQueueStore.list[oriIndex]" :isCurrent="playQueueStore.currentIndex === shuffledIndex"
|
||||
:key="playQueueStore.list[oriIndex].song.cid" :index="shuffledIndex" />
|
||||
|
@ -899,4 +896,4 @@ watch(() => playQueueStore.currentIndex, () => {
|
|||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
|
|
@ -1,217 +1,154 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { computed, ref } from 'vue'
|
||||
import { checkAndRefreshSongResource } from '../utils'
|
||||
import { ref, computed } from 'vue'
|
||||
import apis from '../apis'
|
||||
|
||||
export const usePlayQueueStore = defineStore('queue', () => {
|
||||
const list = ref<QueueItem[]>([])
|
||||
const currentIndex = ref<number>(0)
|
||||
const isPlaying = ref<boolean>(false)
|
||||
// 内部状态
|
||||
const queue = ref<QueueItem[]>([])
|
||||
const isShuffle = ref<boolean>(false)
|
||||
const loopingMode = ref<'single' | 'all' | 'off'>('off')
|
||||
const queueReplaceLock = ref<boolean>(false)
|
||||
const isBuffering = ref<boolean>(false)
|
||||
const currentTime = ref<number>(0)
|
||||
const duration = ref<number>(0)
|
||||
const updatedCurrentTime = ref<number | null>(null)
|
||||
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 currentPlaying = ref<number>(0) // 当前播放指针,指针在 queueOrder 中寻址(无论是否开启了随机播放)
|
||||
const queueOrder = ref<number[]>([]) // 播放队列顺序
|
||||
|
||||
// 暴露给外部的响应式只读引用
|
||||
const queueState = computed(() =>
|
||||
// 按 queueOrder 的顺序排序输出队列
|
||||
queueOrder.value.map(index => queue.value[index]).filter(Boolean)
|
||||
)
|
||||
const shuffleState = computed(() => isShuffle.value)
|
||||
const loopModeState = computed(() => loopingMode.value)
|
||||
|
||||
// 获取当前播放项
|
||||
const currentTrack = computed(() => {
|
||||
const actualIndex = queueOrder.value[currentPlaying.value]
|
||||
return queue.value[actualIndex] || null
|
||||
})
|
||||
const shuffleCurrent = ref<boolean | undefined>(undefined)
|
||||
|
||||
// 预加载相关状态
|
||||
const preloadedAudio = ref<Map<string, HTMLAudioElement>>(new Map())
|
||||
const isPreloading = ref<boolean>(false)
|
||||
const preloadProgress = ref<number>(0)
|
||||
|
||||
// 获取下一首歌的索引
|
||||
const getNextIndex = computed(() => {
|
||||
if (list.value.length === 0) return -1
|
||||
|
||||
if (playMode.value.repeat === 'single') {
|
||||
return currentIndex.value
|
||||
|
||||
/************
|
||||
* 播放队列相关
|
||||
***********/
|
||||
// 使用新队列替换老队列
|
||||
// 队列替换锁开启时启用确认,确认后重置该锁
|
||||
async function replaceQueue(songs: Song[]) {
|
||||
if (queueReplaceLock.value) {
|
||||
if (!confirm("当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?")) { return }
|
||||
// 重置队列替换锁
|
||||
queueReplaceLock.value = false
|
||||
}
|
||||
|
||||
if (playMode.value.shuffle && shuffleList.value.length > 0) {
|
||||
// 当前在 shuffleList 中的位置
|
||||
const currentShuffleIndex = currentIndex.value
|
||||
if (currentShuffleIndex < shuffleList.value.length - 1) {
|
||||
// 返回下一个位置对应的原始 list 索引
|
||||
return shuffleList.value[currentShuffleIndex + 1]
|
||||
} else if (playMode.value.repeat === 'all') {
|
||||
// 返回第一个位置对应的原始 list 索引
|
||||
return shuffleList.value[0]
|
||||
}
|
||||
return -1
|
||||
}
|
||||
let newQueue: QueueItem[] = []
|
||||
|
||||
if (currentIndex.value < list.value.length - 1) {
|
||||
return currentIndex.value + 1
|
||||
} else if (playMode.value.repeat === 'all') {
|
||||
return 0
|
||||
}
|
||||
// 专辑信息缓存空间
|
||||
let albums: { [key: string]: Album } = {}
|
||||
|
||||
return -1
|
||||
})
|
||||
|
||||
// 预加载下一首歌
|
||||
const preloadNext = async () => {
|
||||
const nextIndex = getNextIndex.value
|
||||
if (nextIndex === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取下一首歌曲对象
|
||||
// nextIndex 已经是原始 list 中的索引
|
||||
const nextSong = list.value[nextIndex]
|
||||
|
||||
if (!nextSong || !nextSong.song) {
|
||||
return
|
||||
}
|
||||
|
||||
const songId = nextSong.song.cid
|
||||
|
||||
// 如果已经预加载过,跳过
|
||||
if (preloadedAudio.value.has(songId)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查是否有有效的音频源
|
||||
if (!nextSong.song.sourceUrl) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
isPreloading.value = true
|
||||
preloadProgress.value = 0
|
||||
|
||||
// 在预加载前检查和刷新资源
|
||||
console.log('[Store] 预加载前检查资源:', nextSong.song.name)
|
||||
const updatedSong = await checkAndRefreshSongResource(
|
||||
nextSong.song,
|
||||
(updated) => {
|
||||
// 更新播放队列中的歌曲信息
|
||||
// nextIndex 已经是原始 list 中的索引
|
||||
if (list.value[nextIndex]) {
|
||||
list.value[nextIndex].song = updated
|
||||
for (let i in songs) {
|
||||
// 写入新队列
|
||||
newQueue[newQueue.length] = {
|
||||
song: songs[i],
|
||||
album: await (async () => {
|
||||
if (albums[songs[i].albumCid ?? "0"]) return albums[songs[i].albumCid ?? "0"]
|
||||
else {
|
||||
const album = await apis.getAlbum(songs[i].albumCid ?? "0")
|
||||
albums[songs[i].albumCid ?? "0"] = album
|
||||
return album
|
||||
}
|
||||
|
||||
// 如果歌曲在收藏夹中,也更新收藏夹
|
||||
// 注意:这里不直接导入 favourites store 以避免循环依赖
|
||||
// 改为触发一个事件或者在调用方处理
|
||||
console.log('[Store] 预加载时需要更新收藏夹:', updated.name)
|
||||
},
|
||||
)
|
||||
|
||||
const audio = new Audio()
|
||||
audio.preload = 'auto'
|
||||
audio.crossOrigin = 'anonymous'
|
||||
|
||||
// 监听加载进度
|
||||
audio.addEventListener('progress', () => {
|
||||
if (audio.buffered.length > 0) {
|
||||
const buffered = audio.buffered.end(0)
|
||||
const total = audio.duration || 1
|
||||
preloadProgress.value = (buffered / total) * 100
|
||||
}
|
||||
})
|
||||
|
||||
// 监听加载完成
|
||||
audio.addEventListener('canplaythrough', () => {
|
||||
preloadedAudio.value.set(songId, audio)
|
||||
isPreloading.value = false
|
||||
preloadProgress.value = 100
|
||||
console.log('[Store] 预加载完成:', updatedSong.name)
|
||||
})
|
||||
|
||||
// 监听加载错误
|
||||
audio.addEventListener('error', (e) => {
|
||||
console.error(`[Store] 预加载音频失败: ${updatedSong.name}`, e)
|
||||
isPreloading.value = false
|
||||
preloadProgress.value = 0
|
||||
})
|
||||
|
||||
// 使用更新后的音频源
|
||||
audio.src = updatedSong.sourceUrl!
|
||||
} catch (error) {
|
||||
console.error('[Store] 预加载过程出错:', error)
|
||||
isPreloading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 获取预加载的音频对象
|
||||
const getPreloadedAudio = (songId: string): HTMLAudioElement | null => {
|
||||
const audio = preloadedAudio.value.get(songId) || null
|
||||
return audio
|
||||
}
|
||||
|
||||
// 清理预加载的音频
|
||||
const clearPreloadedAudio = (songId: string) => {
|
||||
const audio = preloadedAudio.value.get(songId)
|
||||
if (audio) {
|
||||
audio.pause()
|
||||
audio.src = ''
|
||||
preloadedAudio.value.delete(songId)
|
||||
}
|
||||
}
|
||||
|
||||
// 清理所有预加载的音频
|
||||
const clearAllPreloadedAudio = () => {
|
||||
preloadedAudio.value.forEach((_audio, songId) => {
|
||||
clearPreloadedAudio(songId)
|
||||
})
|
||||
preloadedAudio.value.clear()
|
||||
}
|
||||
|
||||
// 限制预加载缓存大小(最多保留3首歌)
|
||||
const limitPreloadCache = () => {
|
||||
while (preloadedAudio.value.size > 3) {
|
||||
const oldestKey = preloadedAudio.value.keys().next().value
|
||||
if (oldestKey) {
|
||||
clearPreloadedAudio(oldestKey)
|
||||
} else {
|
||||
break
|
||||
})()
|
||||
}
|
||||
}
|
||||
|
||||
// 将新队列替换已有队列
|
||||
queue.value = newQueue
|
||||
|
||||
// 初始化播放顺序
|
||||
queueOrder.value = Array.from({ length: newQueue.length }, (_, i) => i)
|
||||
currentPlaying.value = 0
|
||||
|
||||
// 关闭随机播放和循环(外部可在此方法执行完毕后再更新播放模式)
|
||||
isShuffle.value = false
|
||||
loopingMode.value = 'off'
|
||||
}
|
||||
|
||||
// 调试函数:打印当前状态
|
||||
const debugPreloadState = () => {
|
||||
console.log('[Store] 预加载状态:', {
|
||||
isPreloading: isPreloading.value,
|
||||
progress: preloadProgress.value,
|
||||
cacheSize: preloadedAudio.value.size,
|
||||
cachedSongs: Array.from(preloadedAudio.value.keys()),
|
||||
nextIndex: getNextIndex.value,
|
||||
})
|
||||
/************
|
||||
* 播放模式相关
|
||||
**********/
|
||||
// 切换随机播放模式
|
||||
const toggleShuffle = (turnTo?: boolean) => {
|
||||
// 未指定随机状态时自动开关
|
||||
const newShuffleState = turnTo ?? !isShuffle.value
|
||||
|
||||
if (newShuffleState === isShuffle.value) return // 状态未改变
|
||||
|
||||
/* if (newShuffleState) {
|
||||
// 开启随机播放:保存当前顺序并打乱
|
||||
const originalOrder = [...queueOrder.value]
|
||||
const shuffled = [...queueOrder.value]
|
||||
|
||||
// Fisher-Yates 洗牌算法
|
||||
for (let i = shuffled.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]]
|
||||
}
|
||||
|
||||
// 确保当前播放的歌曲位置不变(可选)
|
||||
const currentSongIndex = queueOrder.value[currentPlaying.value]
|
||||
const newCurrentPos = shuffled.indexOf(currentSongIndex)
|
||||
if (newCurrentPos !== -1 && newCurrentPos !== currentPlaying.value) {
|
||||
[shuffled[currentPlaying.value], shuffled[newCurrentPos]] =
|
||||
[shuffled[newCurrentPos], shuffled[currentPlaying.value]]
|
||||
}
|
||||
|
||||
// 保存原始顺序以便恢复
|
||||
queue.value.forEach((_, index) => {
|
||||
queue.value[index]._originalOrderIndex = originalOrder.indexOf(index)
|
||||
})
|
||||
|
||||
queueOrder.value = shuffled
|
||||
} else {
|
||||
// 关闭随机播放:恢复原始顺序
|
||||
const restoredOrder = Array.from({ length: queue.value.length }, (_, i) => i)
|
||||
|
||||
// 找到当前播放歌曲在原始顺序中的位置
|
||||
const currentSongIndex = queueOrder.value[currentPlaying.value]
|
||||
const newCurrentPos = restoredOrder.indexOf(currentSongIndex)
|
||||
|
||||
queueOrder.value = restoredOrder
|
||||
currentPlaying.value = newCurrentPos !== -1 ? newCurrentPos : 0
|
||||
} */
|
||||
|
||||
isShuffle.value = newShuffleState
|
||||
}
|
||||
|
||||
// 切换循环播放模式
|
||||
const toggleLoop = (mode?: 'single' | 'all' | 'off') => {
|
||||
// 如果指定了循环模式
|
||||
if (mode) return loopingMode.value = mode
|
||||
|
||||
// 如果没有指定,那么按照「无 -> 列表循环 -> 单曲循环」的顺序轮换
|
||||
switch (loopingMode.value) {
|
||||
case 'off':
|
||||
loopingMode.value = 'all'
|
||||
break
|
||||
case 'all':
|
||||
loopingMode.value = 'single'
|
||||
break
|
||||
case 'single':
|
||||
loopingMode.value = 'off'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
list,
|
||||
currentIndex,
|
||||
isPlaying,
|
||||
queueReplaceLock,
|
||||
isBuffering,
|
||||
currentTime,
|
||||
duration,
|
||||
updatedCurrentTime,
|
||||
visualizer,
|
||||
shuffleList,
|
||||
playMode,
|
||||
shuffleCurrent,
|
||||
// 预加载相关 - 确保所有函数都在返回对象中
|
||||
preloadedAudio,
|
||||
isPreloading,
|
||||
preloadProgress,
|
||||
getNextIndex,
|
||||
preloadNext,
|
||||
getPreloadedAudio,
|
||||
clearPreloadedAudio,
|
||||
clearAllPreloadedAudio,
|
||||
limitPreloadCache,
|
||||
debugPreloadState,
|
||||
// 响应式状态(只读)
|
||||
queue: queueState,
|
||||
isShuffle: shuffleState,
|
||||
loopMode: loopModeState,
|
||||
currentTrack,
|
||||
currentIndex: currentPlaying,
|
||||
|
||||
// 修改方法
|
||||
replaceQueue,
|
||||
toggleShuffle,
|
||||
toggleLoop
|
||||
}
|
||||
})
|
||||
|
|
8
src/vite-env.d.ts
vendored
8
src/vite-env.d.ts
vendored
|
@ -8,10 +8,10 @@ type Song = {
|
|||
cid: string
|
||||
name: string
|
||||
albumCid?: string
|
||||
sourceUrl?: string
|
||||
lyricUrl?: string | null
|
||||
mvUrl?: string | null
|
||||
mvCoverUrl?: string | null
|
||||
sourceUrl?: string | null
|
||||
lyricUrl?: string | null
|
||||
artistes?: string[]
|
||||
artists?: string[]
|
||||
}
|
||||
|
@ -38,6 +38,8 @@ interface ApiResponse {
|
|||
interface QueueItem {
|
||||
song: Song
|
||||
album?: Album
|
||||
sourceUrl?: string
|
||||
lyricUrl?: string | null
|
||||
}
|
||||
|
||||
interface LyricsLine {
|
||||
|
@ -52,4 +54,4 @@ interface GapLine {
|
|||
time: number
|
||||
originalTime: string
|
||||
duration?: number // 添加间隔持续时间
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user