247 lines
5.8 KiB
JavaScript
247 lines
5.8 KiB
JavaScript
import { ref, computed, watch } from 'vue'
|
|
import { defineStore } from 'pinia'
|
|
|
|
const STORAGE_KEY = 'butterfliu_player_state'
|
|
|
|
function saveState(state) {
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
|
} catch (e) {
|
|
console.error('Failed to save player state:', e)
|
|
}
|
|
}
|
|
|
|
function loadState() {
|
|
try {
|
|
const saved = localStorage.getItem(STORAGE_KEY)
|
|
if (saved) {
|
|
return JSON.parse(saved)
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to load player state:', e)
|
|
}
|
|
return null
|
|
}
|
|
|
|
export const usePlayerStore = defineStore('player', () => {
|
|
const audio = ref(null)
|
|
const currentTime = ref(0)
|
|
const duration = ref(0)
|
|
const volume = ref(80)
|
|
const isMuted = ref(false)
|
|
const isPlaying = ref(false)
|
|
const song = ref(null)
|
|
const playlist = ref([])
|
|
|
|
const cover = computed(() => `/api/songs/${song.value.id}/cover`)
|
|
const progress = computed(() => {
|
|
if (duration.value === 0) return 0
|
|
return (currentTime.value / duration.value) * 100
|
|
})
|
|
|
|
const currentTrack = computed(() => ({
|
|
title: song.value?.title || '未播放',
|
|
artist: song.value?.artist || '-',
|
|
}))
|
|
|
|
function setupAudioListeners() {
|
|
if (!audio.value) return
|
|
|
|
audio.value.addEventListener('timeupdate', () => {
|
|
currentTime.value = audio.value.currentTime
|
|
savePlaybackProgress()
|
|
})
|
|
|
|
audio.value.addEventListener('loadedmetadata', () => {
|
|
duration.value = audio.value.duration
|
|
})
|
|
|
|
audio.value.addEventListener('ended', () => {
|
|
isPlaying.value = false
|
|
currentTime.value = 0
|
|
savePlaybackProgress()
|
|
playNext()
|
|
})
|
|
|
|
audio.value.addEventListener('play', () => {
|
|
isPlaying.value = true
|
|
savePlaybackProgress()
|
|
})
|
|
|
|
audio.value.addEventListener('pause', () => {
|
|
isPlaying.value = false
|
|
savePlaybackProgress()
|
|
})
|
|
|
|
audio.value.volume = volume.value / 100
|
|
}
|
|
|
|
function savePlaybackProgress() {
|
|
if (!song.value) return
|
|
|
|
const state = {
|
|
song: song.value,
|
|
currentTime: currentTime.value,
|
|
volume: volume.value,
|
|
isMuted: isMuted.value,
|
|
playlist: playlist.value,
|
|
}
|
|
saveState(state)
|
|
}
|
|
|
|
function restorePlaybackState() {
|
|
const saved = loadState()
|
|
if (!saved || !saved.song) return
|
|
|
|
song.value = saved.song
|
|
volume.value = saved.volume ?? 80
|
|
isMuted.value = saved.isMuted ?? false
|
|
playlist.value = saved.playlist || []
|
|
|
|
const src = `/api/songs/${saved.song.id}/stream`
|
|
|
|
if (!audio.value) {
|
|
audio.value = new Audio(src)
|
|
setupAudioListeners()
|
|
} else {
|
|
audio.value.src = src
|
|
}
|
|
|
|
audio.value.volume = isMuted.value ? 0 : volume.value / 100
|
|
|
|
audio.value.addEventListener('loadedmetadata', () => {
|
|
duration.value = audio.value.duration
|
|
if (saved.currentTime > 0 && saved.currentTime < duration.value) {
|
|
audio.value.currentTime = saved.currentTime
|
|
currentTime.value = saved.currentTime
|
|
}
|
|
}, { once: true })
|
|
}
|
|
|
|
function playSong(newSong, newPlaylist = null) {
|
|
if (newPlaylist) {
|
|
playlist.value = newPlaylist
|
|
}
|
|
|
|
song.value = newSong
|
|
const src = `/api/songs/${newSong.id}/stream`
|
|
|
|
if (!audio.value) {
|
|
audio.value = new Audio(src)
|
|
setupAudioListeners()
|
|
} else {
|
|
audio.value.src = src
|
|
}
|
|
|
|
currentTime.value = 0
|
|
audio.value.play()
|
|
savePlaybackProgress()
|
|
}
|
|
|
|
function togglePlay() {
|
|
if (!audio.value) return
|
|
|
|
if (isPlaying.value) {
|
|
audio.value.pause()
|
|
} else {
|
|
audio.value.play()
|
|
}
|
|
}
|
|
|
|
function seekTo(percent) {
|
|
if (!audio.value) return
|
|
const time = (percent / 100) * duration.value
|
|
audio.value.currentTime = time
|
|
savePlaybackProgress()
|
|
}
|
|
|
|
function seekForward(seconds = 10) {
|
|
if (!audio.value) return
|
|
audio.value.currentTime = Math.min(audio.value.currentTime + seconds, duration.value)
|
|
savePlaybackProgress()
|
|
}
|
|
|
|
function seekBackward(seconds = 10) {
|
|
if (!audio.value) return
|
|
audio.value.currentTime = Math.max(audio.value.currentTime - seconds, 0)
|
|
savePlaybackProgress()
|
|
}
|
|
|
|
function setVolume(newVolume) {
|
|
volume.value = newVolume
|
|
if (audio.value) {
|
|
audio.value.volume = isMuted.value ? 0 : newVolume / 100
|
|
}
|
|
if (newVolume > 0 && isMuted.value) {
|
|
isMuted.value = false
|
|
}
|
|
savePlaybackProgress()
|
|
}
|
|
|
|
function toggleMute() {
|
|
isMuted.value = !isMuted.value
|
|
if (audio.value) {
|
|
audio.value.volume = isMuted.value ? 0 : volume.value / 100
|
|
}
|
|
savePlaybackProgress()
|
|
}
|
|
|
|
function playNext() {
|
|
if (playlist.value.length === 0) return
|
|
|
|
const currentIndex = playlist.value.findIndex(s => s.id === song.value?.id)
|
|
if (currentIndex === -1) return
|
|
|
|
const nextIndex = (currentIndex + 1) % playlist.value.length
|
|
playSong(playlist.value[nextIndex], playlist.value)
|
|
}
|
|
|
|
function playPrevious() {
|
|
if (playlist.value.length === 0) return
|
|
|
|
const currentIndex = playlist.value.findIndex(s => s.id === song.value?.id)
|
|
if (currentIndex === -1) return
|
|
|
|
const prevIndex = currentIndex === 0 ? playlist.value.length - 1 : currentIndex - 1
|
|
playSong(playlist.value[prevIndex], playlist.value)
|
|
}
|
|
|
|
function formatTime(seconds) {
|
|
if (!seconds || isNaN(seconds)) return '0:00'
|
|
const mins = Math.floor(seconds / 60)
|
|
const secs = Math.floor(seconds % 60)
|
|
return `${mins}:${secs.toString().padStart(2, '0')}`
|
|
}
|
|
|
|
watch(volume, (newVolume) => {
|
|
if (audio.value && !isMuted.value) {
|
|
audio.value.volume = newVolume / 100
|
|
}
|
|
})
|
|
|
|
return {
|
|
audio,
|
|
currentTime,
|
|
duration,
|
|
volume,
|
|
isMuted,
|
|
isPlaying,
|
|
song,
|
|
playlist,
|
|
progress,
|
|
currentTrack,
|
|
cover,
|
|
playSong,
|
|
togglePlay,
|
|
seekTo,
|
|
seekForward,
|
|
seekBackward,
|
|
setVolume,
|
|
toggleMute,
|
|
playNext,
|
|
playPrevious,
|
|
formatTime,
|
|
restorePlaybackState,
|
|
}
|
|
})
|