import { defineStore } from 'pinia'; import { ref, shallowRef, watch } from 'vue'; import { MessagePlugin } from 'tdesign-vue-next'; import type { Song } from '../types/song'; export enum PlayMode { List = 'list', Single = 'single', Random = 'random' } let audioElement: HTMLAudioElement | null = null; let audioContext: AudioContext | null = null; let analyser: AnalyserNode | null = null; let animationFrameId: number | null = null; export const usePlayerStore = defineStore('player', () => { // State const isPlaying = ref(false); const currentSong = ref(null); const savedVolume = localStorage.getItem('qz-player-volume'); const volume = ref(savedVolume ? Number(savedVolume) : 50); // 1~100 // Persistence & Sync watch(volume, (newVol) => { localStorage.setItem('qz-player-volume', newVol.toString()); if (audioElement) { audioElement.volume = newVol / 100; } }); const duration = ref(0); //毫秒级 const currentTime = ref(0); //毫秒级 // Audio Visualization State const loudness = ref(0); const spectrum = ref([]); // UI State const isPlayerFullScreen = ref(false); const hideLyricView = ref(false); // Playlist State const savedPlaylist = localStorage.getItem('qz-player-playlist'); const savedIndex = localStorage.getItem('qz-player-index'); const playlist = ref(savedPlaylist ? JSON.parse(savedPlaylist) : []); const currentIndex = ref(savedIndex ? Number(savedIndex) : -1); const playMode = ref(PlayMode.List); const savedAddMode = localStorage.getItem('qz-player-add-mode'); const addListMode = ref<'replace' | 'append'>((savedAddMode as 'replace' | 'append') || 'replace'); // Error Handling const playErrorCount = ref(0); const MAX_RETRY_COUNT = 3; // Lyrics State const lyrics = shallowRef<{ lines: any[] }>({ lines: [] }); // Initialize audio element const initAudio = () => { if (!audioElement && typeof window !== 'undefined') { audioElement = new Audio(); audioElement.volume = volume.value / 100; // Setup audio context for visualization if (window.AudioContext || (window as any).webkitAudioContext) { audioContext = new (window.AudioContext || (window as any).webkitAudioContext)(); analyser = audioContext.createAnalyser(); analyser.fftSize = 256; const source = audioContext.createMediaElementSource(audioElement); source.connect(analyser); analyser.connect(audioContext.destination); } // Event listeners audioElement.addEventListener('loadedmetadata', () => { if (audioElement) { duration.value = audioElement.duration * 1000; } }); audioElement.addEventListener('timeupdate', () => { if (audioElement) { currentTime.value = audioElement.currentTime * 1000; } }); audioElement.addEventListener('play', () => { isPlaying.value = true; startVisualization(); if ('mediaSession' in navigator) { navigator.mediaSession.playbackState = 'playing'; } }); audioElement.addEventListener('pause', () => { isPlaying.value = false; stopVisualization(); if ('mediaSession' in navigator) { navigator.mediaSession.playbackState = 'paused'; } }); audioElement.addEventListener('ended', () => { next(false); }); audioElement.addEventListener('error', (e) => { console.error('Audio error:', e); handlePlayError(); }); } }; const startVisualization = () => { if (!analyser || !audioContext) return; const bufferLength = analyser.frequencyBinCount; const dataArray = new Uint8Array(bufferLength); const animate = () => { if (!isPlaying.value) return; animationFrameId = requestAnimationFrame(animate); analyser?.getByteFrequencyData(dataArray); // Calculate average loudness let sum = 0; for (let i = 0; i < bufferLength; i++) { sum += dataArray[i]; } loudness.value = sum / bufferLength; // Update spectrum spectrum.value = Array.from(dataArray); }; animate(); }; const stopVisualization = () => { if (animationFrameId !== null) { cancelAnimationFrame(animationFrameId); animationFrameId = null; } }; // --- Actions --- const setPlaylist = async (list: any[], startIndex = 0) => { playlist.value = list; currentIndex.value = startIndex; if (list.length > 0 && startIndex >= 0 && startIndex < list.length) { await playSong(list[startIndex]); } }; const playFromList = async (song: Song, contextList: Song[]) => { if (addListMode.value === 'replace') { const index = contextList.findIndex(s => s.id === song.id); if (index !== -1) { await setPlaylist(contextList, index); } else { await setPlaylist([song], 0); } } else { playlist.value.push(song); const newIndex = playlist.value.length - 1; currentIndex.value = newIndex; await playSong(song); } }; const playSong = async (song: Song, autoPlay = true) => { if (!song) return; initAudio(); console.log(song); currentSong.value = song; const foundIndex = playlist.value.findIndex(s => s.id === song.id); if (foundIndex !== -1) { currentIndex.value = foundIndex; } updateMediaSession(song); fetchLyrics(song); // Resume audio context if suspended (for autoplay policy) if (audioContext && audioContext.state === 'suspended') { await audioContext.resume(); } let playUrl = song.url; if (song.type === 'Remote' && song.source) { const quality = 'hires'; playUrl = `http://localhost:5266/music?source=${song.source}&id=${song.id}&quality=${quality}`; console.log('[Player] Using Proxy:', playUrl); } if (playUrl && audioElement) { console.log('Playing:', song.name, 'AutoPlay:', autoPlay); try { audioElement.src = playUrl; audioElement.load(); if (autoPlay) { await audioElement.play(); } song.url = playUrl; playErrorCount.value = 0; } catch (e) { console.error("Play request failed:", e); if (autoPlay) handlePlayError(); } } else { console.warn("Song has no URL"); if (autoPlay) handlePlayError(); } }; const fetchLyrics = async (song: Song) => { lyrics.value = { lines: [] }; if (!song || !song.id) return; try { // TODO: Implement web-based lyric fetching MessagePlugin.info("网页版暂不支持歌词获取").then(); } catch (e) { console.error('Failed to fetch lyrics:', e); } }; const updateMediaSession = (song: Song) => { if (!('mediaSession' in navigator)) return; navigator.mediaSession.metadata = new MediaMetadata({ title: song.name, artist: song.artist, album: song.albumName || '', artwork: song.picUrl ? [{ src: song.picUrl, sizes: '512x512', type: 'image/png' }] : [] }); navigator.mediaSession.setActionHandler('play', () => togglePlay()); navigator.mediaSession.setActionHandler('pause', () => togglePlay()); navigator.mediaSession.setActionHandler('previoustrack', () => prev()); navigator.mediaSession.setActionHandler('nexttrack', () => next(true)); navigator.mediaSession.setActionHandler('seekto', (details) => { if (details.seekTime != null) { seek(details.seekTime); } }); }; const next = async (manual = true) => { if (playlist.value.length === 0) return; let nextIndex = currentIndex.value; if (playMode.value === PlayMode.Single && !manual) { nextIndex = currentIndex.value; } else if (playMode.value === PlayMode.Random) { nextIndex = Math.floor(Math.random() * playlist.value.length); } else { nextIndex = (currentIndex.value + 1) % playlist.value.length; } currentIndex.value = nextIndex; await playSong(playlist.value[nextIndex]); }; const prev = async () => { if (playlist.value.length === 0) return; let prevIndex = currentIndex.value; if (playMode.value === PlayMode.Random) { prevIndex = Math.floor(Math.random() * playlist.value.length); } else { prevIndex = (currentIndex.value - 1 + playlist.value.length) % playlist.value.length; } currentIndex.value = prevIndex; await playSong(playlist.value[prevIndex]); }; const handlePlayError = async () => { playErrorCount.value++; if (playlist.value.length === 0) { isPlaying.value = false; playErrorCount.value = 0; return; } if (playErrorCount.value >= MAX_RETRY_COUNT) { audioElement?.pause(); isPlaying.value = false; MessagePlugin.error('连续多次播放失败,已停止播放').then(); playErrorCount.value = 0; } else { MessagePlugin.warning(`播放失败,尝试播放下一首 (${playErrorCount.value}/${MAX_RETRY_COUNT})`).then(); next(false); } }; const togglePlay = async () => { if (!audioElement) { initAudio(); } if (audioElement) { if (isPlaying.value) { audioElement.pause(); } else { await audioElement.play(); } } }; const setVolume = async (vol: number) => { volume.value = vol; if (audioElement) { audioElement.volume = vol / 100; } }; const seek = async (time: number) => { if (audioElement) { audioElement.currentTime = time / 1000; } }; const toggleMode = () => { if (playMode.value === PlayMode.List) playMode.value = PlayMode.Single; else if (playMode.value === PlayMode.Single) playMode.value = PlayMode.Random; else playMode.value = PlayMode.List; }; const toggleFullScreen = () => { isPlayerFullScreen.value = !isPlayerFullScreen.value; }; // Persistence Listeners watch(() => [playlist.value, currentIndex.value], () => { localStorage.setItem('qz-player-playlist', JSON.stringify(playlist.value)); localStorage.setItem('qz-player-index', currentIndex.value.toString()); }, { deep: true }); watch(addListMode, (newMode) => { localStorage.setItem('qz-player-add-mode', newMode); }); // Restore initial state if (playlist.value.length > 0 && currentIndex.value >= 0 && currentIndex.value < playlist.value.length) { const restoredSong = playlist.value[currentIndex.value]; if (restoredSong) { // Do not auto-play on restore currentSong.value = restoredSong; } } return { isPlaying, currentSong, volume, duration, currentTime, playlist, playMode, loudness, spectrum, isPlayerFullScreen, setPlaylist, playSong, next, prev, togglePlay, setVolume, seek, toggleMode, toggleFullScreen, lyrics, fetchLyrics, addListMode, playFromList, hideLyricView }; });