|
|
import { defineStore } from 'pinia' |
|
|
import { ref, computed } from 'vue' |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export const usePlayQueueStore = defineStore('playqueue', () => { |
|
|
|
|
|
const queue = ref([]) |
|
|
const currentIndex = ref(-1) |
|
|
const playMode = ref('list') |
|
|
|
|
|
|
|
|
const STORAGE_KEY = 'vue-music-play-queue' |
|
|
const MAX_QUEUE_SIZE = 2000 |
|
|
|
|
|
|
|
|
const currentSong = computed(() => { |
|
|
return currentIndex.value >= 0 && currentIndex.value < queue.value.length |
|
|
? queue.value[currentIndex.value] |
|
|
: null |
|
|
}) |
|
|
|
|
|
const queueLength = computed(() => queue.value.length) |
|
|
const isEmpty = computed(() => queue.value.length === 0) |
|
|
const hasPrevious = computed(() => currentIndex.value > 0) |
|
|
const hasNext = computed(() => currentIndex.value < queue.value.length - 1) |
|
|
|
|
|
|
|
|
const setQueue = (songs, startIndex = 0) => { |
|
|
if (!Array.isArray(songs)) { |
|
|
console.error('队列必须是数组') |
|
|
return false |
|
|
} |
|
|
|
|
|
|
|
|
const uniqueSongs = [] |
|
|
const seen = new Set() |
|
|
|
|
|
for (const song of songs) { |
|
|
if (song && song.id) { |
|
|
const key = `${song.source}-${song.id}` |
|
|
if (!seen.has(key)) { |
|
|
seen.add(key) |
|
|
uniqueSongs.push(song) |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
queue.value = uniqueSongs.slice(0, MAX_QUEUE_SIZE) |
|
|
currentIndex.value = Math.max(0, Math.min(startIndex, queue.value.length - 1)) |
|
|
saveQueue() |
|
|
return true |
|
|
} |
|
|
|
|
|
const addToQueue = (song, position = 'end') => { |
|
|
if (!song || !song.id) { |
|
|
return { success: false, message: '歌曲信息无效' } |
|
|
} |
|
|
|
|
|
|
|
|
const exists = queue.value.some(s => s.id === song.id && s.source === song.source) |
|
|
if (exists) { |
|
|
return { success: false, message: '歌曲已在播放列表中' } |
|
|
} |
|
|
|
|
|
if (queue.value.length >= MAX_QUEUE_SIZE) { |
|
|
return { success: false, message: `播放列表最多只能添加 ${MAX_QUEUE_SIZE} 首歌曲` } |
|
|
} |
|
|
|
|
|
switch (position) { |
|
|
case 'next': |
|
|
|
|
|
const insertIndex = Math.min(currentIndex.value + 1, queue.value.length) |
|
|
queue.value.splice(insertIndex, 0, { ...song }) |
|
|
break |
|
|
case 'start': |
|
|
queue.value.unshift({ ...song }) |
|
|
if (currentIndex.value >= 0) { |
|
|
currentIndex.value++ |
|
|
} |
|
|
break |
|
|
case 'end': |
|
|
default: |
|
|
queue.value.push({ ...song }) |
|
|
break |
|
|
} |
|
|
|
|
|
saveQueue() |
|
|
return { success: true, message: '已添加到播放列表' } |
|
|
} |
|
|
|
|
|
const removeFromQueue = (index) => { |
|
|
if (index < 0 || index >= queue.value.length) { |
|
|
return { success: false, message: '无效的索引' } |
|
|
} |
|
|
|
|
|
queue.value.splice(index, 1) |
|
|
|
|
|
|
|
|
if (currentIndex.value > index) { |
|
|
currentIndex.value-- |
|
|
} else if (currentIndex.value === index) { |
|
|
|
|
|
if (queue.value.length === 0) { |
|
|
currentIndex.value = -1 |
|
|
} else { |
|
|
currentIndex.value = Math.min(currentIndex.value, queue.value.length - 1) |
|
|
} |
|
|
} |
|
|
|
|
|
saveQueue() |
|
|
return { success: true, message: '已从播放列表移除' } |
|
|
} |
|
|
|
|
|
const clearQueue = () => { |
|
|
queue.value = [] |
|
|
currentIndex.value = -1 |
|
|
saveQueue() |
|
|
} |
|
|
|
|
|
|
|
|
const playAtIndex = (index) => { |
|
|
if (index < 0 || index >= queue.value.length) { |
|
|
return null |
|
|
} |
|
|
|
|
|
currentIndex.value = index |
|
|
saveQueue() |
|
|
return queue.value[index] |
|
|
} |
|
|
|
|
|
const playNext = () => { |
|
|
if (queue.value.length === 0) return null |
|
|
|
|
|
let nextIndex = currentIndex.value |
|
|
|
|
|
switch (playMode.value) { |
|
|
case 'random': |
|
|
|
|
|
if (queue.value.length > 1) { |
|
|
do { |
|
|
nextIndex = Math.floor(Math.random() * queue.value.length) |
|
|
} while (nextIndex === currentIndex.value) |
|
|
} |
|
|
break |
|
|
case 'single': |
|
|
|
|
|
break |
|
|
case 'list': |
|
|
default: |
|
|
|
|
|
nextIndex = hasNext.value ? currentIndex.value + 1 : 0 |
|
|
break |
|
|
} |
|
|
|
|
|
return playAtIndex(nextIndex) |
|
|
} |
|
|
|
|
|
const playPrevious = () => { |
|
|
if (queue.value.length === 0) return null |
|
|
|
|
|
let prevIndex = currentIndex.value |
|
|
|
|
|
switch (playMode.value) { |
|
|
case 'random': |
|
|
|
|
|
if (queue.value.length > 1) { |
|
|
do { |
|
|
prevIndex = Math.floor(Math.random() * queue.value.length) |
|
|
} while (prevIndex === currentIndex.value) |
|
|
} |
|
|
break |
|
|
case 'single': |
|
|
|
|
|
break |
|
|
case 'list': |
|
|
default: |
|
|
|
|
|
prevIndex = hasPrevious.value ? currentIndex.value - 1 : queue.value.length - 1 |
|
|
break |
|
|
} |
|
|
|
|
|
return playAtIndex(prevIndex) |
|
|
} |
|
|
|
|
|
const setPlayMode = (mode) => { |
|
|
const validModes = ['list', 'random', 'single'] |
|
|
if (validModes.includes(mode)) { |
|
|
playMode.value = mode |
|
|
saveQueue() |
|
|
return true |
|
|
} |
|
|
return false |
|
|
} |
|
|
|
|
|
const togglePlayMode = () => { |
|
|
const modes = ['list', 'random', 'single'] |
|
|
const currentIndex = modes.indexOf(playMode.value) |
|
|
const nextIndex = (currentIndex + 1) % modes.length |
|
|
setPlayMode(modes[nextIndex]) |
|
|
return playMode.value |
|
|
} |
|
|
|
|
|
|
|
|
const moveInQueue = (fromIndex, toIndex) => { |
|
|
if (fromIndex < 0 || fromIndex >= queue.value.length || |
|
|
toIndex < 0 || toIndex >= queue.value.length) { |
|
|
return false |
|
|
} |
|
|
|
|
|
const item = queue.value.splice(fromIndex, 1)[0] |
|
|
queue.value.splice(toIndex, 0, item) |
|
|
|
|
|
|
|
|
if (currentIndex.value === fromIndex) { |
|
|
currentIndex.value = toIndex |
|
|
} else if (fromIndex < currentIndex.value && toIndex >= currentIndex.value) { |
|
|
currentIndex.value-- |
|
|
} else if (fromIndex > currentIndex.value && toIndex <= currentIndex.value) { |
|
|
currentIndex.value++ |
|
|
} |
|
|
|
|
|
saveQueue() |
|
|
return true |
|
|
} |
|
|
|
|
|
const shuffleQueue = () => { |
|
|
if (queue.value.length <= 1) return |
|
|
|
|
|
const currentSong = currentSong.value |
|
|
|
|
|
|
|
|
for (let i = queue.value.length - 1; i > 0; i--) { |
|
|
const j = Math.floor(Math.random() * (i + 1)) |
|
|
;[queue.value[i], queue.value[j]] = [queue.value[j], queue.value[i]] |
|
|
} |
|
|
|
|
|
|
|
|
if (currentSong) { |
|
|
const newIndex = queue.value.findIndex(song => |
|
|
song.id === currentSong.id && song.source === currentSong.source |
|
|
) |
|
|
if (newIndex >= 0) { |
|
|
currentIndex.value = newIndex |
|
|
} |
|
|
} |
|
|
|
|
|
saveQueue() |
|
|
} |
|
|
|
|
|
|
|
|
const addMultipleToQueue = (songs, position = 'end') => { |
|
|
let successCount = 0 |
|
|
const results = [] |
|
|
|
|
|
for (const song of songs) { |
|
|
const result = addToQueue(song, position) |
|
|
results.push(result) |
|
|
if (result.success) successCount++ |
|
|
} |
|
|
|
|
|
return { |
|
|
success: successCount > 0, |
|
|
successCount, |
|
|
totalCount: songs.length, |
|
|
results |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const searchInQueue = (keyword) => { |
|
|
if (!keyword || !keyword.trim()) { |
|
|
return queue.value.map((song, index) => ({ song, index })) |
|
|
} |
|
|
|
|
|
const lowerKeyword = keyword.toLowerCase().trim() |
|
|
return queue.value |
|
|
.map((song, index) => ({ song, index })) |
|
|
.filter(({ song }) => { |
|
|
|
|
|
const safeIncludes = (text) => { |
|
|
if (!text) return false |
|
|
if (typeof text === 'string') { |
|
|
return text.toLowerCase().includes(lowerKeyword) |
|
|
} |
|
|
if (Array.isArray(text)) { |
|
|
|
|
|
return text.some(item => { |
|
|
if (typeof item === 'string') { |
|
|
return item.toLowerCase().includes(lowerKeyword) |
|
|
} |
|
|
if (typeof item === 'object' && item?.name) { |
|
|
return item.name.toLowerCase().includes(lowerKeyword) |
|
|
} |
|
|
return false |
|
|
}) |
|
|
} |
|
|
if (typeof text === 'object' && text?.name) { |
|
|
return text.name.toLowerCase().includes(lowerKeyword) |
|
|
} |
|
|
return String(text).toLowerCase().includes(lowerKeyword) |
|
|
} |
|
|
|
|
|
return safeIncludes(song.name) || |
|
|
safeIncludes(song.artist) || |
|
|
safeIncludes(song.album) |
|
|
}) |
|
|
} |
|
|
|
|
|
|
|
|
const saveQueue = () => { |
|
|
try { |
|
|
const data = { |
|
|
queue: queue.value, |
|
|
currentIndex: currentIndex.value, |
|
|
playMode: playMode.value, |
|
|
lastUpdated: Date.now(), |
|
|
version: '1.0' |
|
|
} |
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(data)) |
|
|
} catch (error) { |
|
|
console.error('保存播放列表失败:', error) |
|
|
} |
|
|
} |
|
|
|
|
|
const loadQueue = () => { |
|
|
try { |
|
|
const saved = localStorage.getItem(STORAGE_KEY) |
|
|
if (saved) { |
|
|
const data = JSON.parse(saved) |
|
|
|
|
|
|
|
|
if (Array.isArray(data.queue)) { |
|
|
queue.value = data.queue |
|
|
currentIndex.value = data.currentIndex >= 0 ? data.currentIndex : -1 |
|
|
playMode.value = data.playMode || 'list' |
|
|
} |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('加载播放列表失败:', error) |
|
|
queue.value = [] |
|
|
currentIndex.value = -1 |
|
|
playMode.value = 'list' |
|
|
} |
|
|
} |
|
|
|
|
|
return { |
|
|
|
|
|
queue: computed(() => queue.value), |
|
|
currentIndex: computed(() => currentIndex.value), |
|
|
currentSong, |
|
|
playMode: computed(() => playMode.value), |
|
|
|
|
|
|
|
|
queueLength, |
|
|
isEmpty, |
|
|
hasPrevious, |
|
|
hasNext, |
|
|
|
|
|
|
|
|
setQueue, |
|
|
addToQueue, |
|
|
removeFromQueue, |
|
|
clearQueue, |
|
|
|
|
|
|
|
|
playAtIndex, |
|
|
playNext, |
|
|
playPrevious, |
|
|
setPlayMode, |
|
|
togglePlayMode, |
|
|
|
|
|
|
|
|
moveInQueue, |
|
|
shuffleQueue, |
|
|
|
|
|
|
|
|
addMultipleToQueue, |
|
|
|
|
|
|
|
|
searchInQueue, |
|
|
|
|
|
|
|
|
saveQueue, |
|
|
loadQueue |
|
|
} |
|
|
}) |