Compare commits
No commits in common. "main" and "v0.0.5" have entirely different histories.
130
CLAUDE.md
130
CLAUDE.md
|
@ -1,130 +0,0 @@
|
||||||
# CLAUDE.md
|
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
||||||
|
|
||||||
## Project Overview
|
|
||||||
|
|
||||||
MSR Mod is a browser extension that provides an alternate frontend for Monster Siren Records (monster-siren.hypergryph.com). It's built with Vue 3, TypeScript, and Tailwind CSS, designed to work as both Chrome and Firefox extensions.
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
### Development
|
|
||||||
```bash
|
|
||||||
npm run dev # Start development server with Vite
|
|
||||||
npm run dev:refresh # Build and refresh dist folder for extension development
|
|
||||||
npm i # Install dependencies
|
|
||||||
```
|
|
||||||
|
|
||||||
### Building
|
|
||||||
```bash
|
|
||||||
npm run build:chrome # Build for Chrome/Chromium browsers
|
|
||||||
npm run build:firefox # Build for Firefox
|
|
||||||
npm run build # Default build (Chrome)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
```bash
|
|
||||||
npm run lint # Format code with Biome
|
|
||||||
npm run quality-check # Run Biome CI checks
|
|
||||||
npm run qc # Alias for quality-check
|
|
||||||
```
|
|
||||||
|
|
||||||
### Extension Development Workflow
|
|
||||||
1. Run `npm run dev:refresh` to build initial dist folder
|
|
||||||
2. Load the `dist` folder as an unpacked extension in browser
|
|
||||||
3. Use `npm run dev` for hot-reload development
|
|
||||||
4. Use `npm run build:watch` for continuous builds
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
### Core Technologies
|
|
||||||
- **Vue 3** with Composition API and `<script setup>` syntax
|
|
||||||
- **TypeScript** for type safety
|
|
||||||
- **Pinia** for state management
|
|
||||||
- **Vue Router** with hash history for extension compatibility
|
|
||||||
- **Tailwind CSS v4** for styling
|
|
||||||
- **GSAP** for animations
|
|
||||||
- **Axios** for API communication
|
|
||||||
|
|
||||||
### Browser Extension Structure
|
|
||||||
- **Manifest V3** with platform-specific builds
|
|
||||||
- **Content Scripts** inject the frontend on monster-siren.hypergryph.com
|
|
||||||
- **Background Service Worker** handles extension lifecycle
|
|
||||||
- **Cross-platform compatibility** via prebuild scripts
|
|
||||||
|
|
||||||
### State Management (Pinia Stores)
|
|
||||||
- **usePlayQueueStore**: Music playback queue, shuffle/repeat modes, audio preloading
|
|
||||||
- **useFavourites**: User favorites with cross-platform storage (Chrome storage API/localStorage)
|
|
||||||
- **usePreferences**: User settings and preferences
|
|
||||||
|
|
||||||
### Key Components
|
|
||||||
- **Player**: Main audio player with preloading and resource validation
|
|
||||||
- **Playroom**: Full-screen player interface with lyrics and visualizations
|
|
||||||
- **ScrollingLyrics**: Animated lyrics display with auto-scroll and user interaction
|
|
||||||
- **PlayListItem/TrackItem**: Reusable music track components
|
|
||||||
|
|
||||||
### API Integration
|
|
||||||
- **Monster Siren API**: Fetches songs, albums, and metadata via `src/apis/index.ts`
|
|
||||||
- **Resource URL Validation**: Automatic refresh of cached URLs when servers rotate resources
|
|
||||||
- **Preloading System**: Smart audio preloading with cache management
|
|
||||||
|
|
||||||
### Browser Compatibility
|
|
||||||
- **Chrome**: Uses service worker, full CSP support
|
|
||||||
- **Firefox**: Uses background scripts, modified CSP, specific gecko settings
|
|
||||||
- **Prebuild Scripts**: Automatically modify manifest.json and HTML for each platform
|
|
||||||
|
|
||||||
### Storage Strategy
|
|
||||||
- **Favorites**: Stored in Chrome storage API (fallback to localStorage)
|
|
||||||
- **Preferences**: Browser-specific storage with graceful degradation
|
|
||||||
- **Audio Cache**: In-memory preloading with size limits
|
|
||||||
|
|
||||||
### Resource Management
|
|
||||||
- **Audio Preloading**: Validates and preloads next track during playback
|
|
||||||
- **URL Refresh Logic**: Checks resource availability before playback/preload
|
|
||||||
- **Cache Invalidation**: Automatic cleanup when resource URLs change
|
|
||||||
|
|
||||||
### Shuffle/Random Play Logic
|
|
||||||
- **shuffleList**: Array storing the shuffled order of original list indices
|
|
||||||
- **currentIndex**: In shuffle mode, this is the index within shuffleList
|
|
||||||
- **Accessing current song**: `list[shuffleList[currentIndex]]` in shuffle mode
|
|
||||||
- **getNextIndex**: Returns the actual list index of the next song to play
|
|
||||||
|
|
||||||
## File Structure Notes
|
|
||||||
|
|
||||||
### `/src/utils/`
|
|
||||||
- **songResourceChecker.ts**: Centralized resource validation and refresh logic
|
|
||||||
- **audioVisualizer.ts**: Real-time audio analysis for visual effects
|
|
||||||
- **artistsOrganize.ts**: Helper for formatting artist names
|
|
||||||
|
|
||||||
### `/scripts/`
|
|
||||||
- **prebuild-chrome.js**: Removes localhost dev configs for production
|
|
||||||
- **prebuild-firefox.js**: Adapts manifest for Firefox compatibility
|
|
||||||
|
|
||||||
### `/public/`
|
|
||||||
- **manifest.json**: Extension manifest (modified by prebuild scripts)
|
|
||||||
- **content.js**: Injects the Vue app into target websites
|
|
||||||
- **background.js**: Extension background script
|
|
||||||
|
|
||||||
## Code Style and Formatting
|
|
||||||
|
|
||||||
### Indentation
|
|
||||||
- **This project uses Tab indentation (not spaces)**
|
|
||||||
- Ensure all code edits maintain consistent Tab indentation
|
|
||||||
- When editing files, preserve the existing Tab character formatting
|
|
||||||
|
|
||||||
## Development Considerations
|
|
||||||
|
|
||||||
### Extension Context
|
|
||||||
- Uses hash routing for browser extension compatibility
|
|
||||||
- CSP restrictions require specific script and style handling
|
|
||||||
- Cross-origin requests limited to declared host permissions
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
- Audio preloading system prevents playback interruptions
|
|
||||||
- Resource validation happens only when needed (playback/preload)
|
|
||||||
- GSAP animations with proper cleanup to prevent memory leaks
|
|
||||||
|
|
||||||
### Error Handling
|
|
||||||
- Graceful fallbacks for storage API unavailability
|
|
||||||
- Resource URL rotation handling with automatic refresh
|
|
||||||
- Cross-browser compatibility with feature detection
|
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "MSR Mod",
|
"name": "MSR Mod",
|
||||||
"version": "0.0.6",
|
"version": "0.0.5",
|
||||||
"description": "塞壬唱片(Monster Siren Records)官网的替代前端。",
|
"description": "塞壬唱片(Monster Siren Records)官网的替代前端。",
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -2,14 +2,17 @@
|
||||||
import { artistsOrganize } from '../utils'
|
import { artistsOrganize } from '../utils'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useFavourites } from '../stores/useFavourites'
|
import { useFavourites } from '../stores/useFavourites'
|
||||||
|
import apis from '../apis'
|
||||||
|
import axios from 'axios'
|
||||||
|
|
||||||
import StarSlashIcon from '../assets/icons/starslash.vue'
|
import StarSlashIcon from '../assets/icons/starslash.vue'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
|
||||||
const favourites = useFavourites()
|
const favourites = useFavourites()
|
||||||
|
|
||||||
const hover = ref(false)
|
const hover = ref(false)
|
||||||
|
|
||||||
defineProps<{
|
const props = defineProps<{
|
||||||
item: QueueItem
|
item: QueueItem
|
||||||
index: number
|
index: number
|
||||||
}>()
|
}>()
|
||||||
|
@ -17,6 +20,27 @@ defineProps<{
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'play', index: number): void
|
(e: 'play', index: number): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
// 添加缓存控制头和随机参数来避免缓存
|
||||||
|
await axios.head(props.item.song.sourceUrl ?? '', {
|
||||||
|
headers: {
|
||||||
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
||||||
|
'Pragma': 'no-cache',
|
||||||
|
'Expires': '0'
|
||||||
|
},
|
||||||
|
params: {
|
||||||
|
_t: Date.now() // 添加时间戳参数避免缓存
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
// 刷新资源地址
|
||||||
|
const updatedSong = await apis.getSong(props.item.song.cid)
|
||||||
|
console.log('Updated song:', updatedSong)
|
||||||
|
favourites.updateSongInFavourites(props.item.song.cid, updatedSong)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
<!-- Player.vue - 添加预加载功能 -->
|
<!-- Player.vue - 添加预加载功能 -->
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, useTemplateRef, watch } from 'vue'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { useFavourites } from '../stores/useFavourites'
|
|
||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
|
||||||
|
|
||||||
import LoadingIndicator from '../assets/icons/loadingindicator.vue'
|
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
||||||
|
import { useTemplateRef, watch, nextTick, computed } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
|
||||||
import PlayIcon from '../assets/icons/play.vue'
|
import PlayIcon from '../assets/icons/play.vue'
|
||||||
import { audioVisualizer, checkAndRefreshSongResource } from '../utils'
|
import LoadingIndicator from '../assets/icons/loadingindicator.vue'
|
||||||
|
import { audioVisualizer } from '../utils'
|
||||||
|
|
||||||
const playQueueStore = usePlayQueueStore()
|
const playQueueStore = usePlayQueueStore()
|
||||||
const favourites = useFavourites()
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const player = useTemplateRef('playerRef')
|
const player = useTemplateRef('playerRef')
|
||||||
|
|
||||||
|
@ -18,21 +17,16 @@ const player = useTemplateRef('playerRef')
|
||||||
console.log('[Player] 检查 store 方法:', {
|
console.log('[Player] 检查 store 方法:', {
|
||||||
preloadNext: typeof playQueueStore.preloadNext,
|
preloadNext: typeof playQueueStore.preloadNext,
|
||||||
getPreloadedAudio: typeof playQueueStore.getPreloadedAudio,
|
getPreloadedAudio: typeof playQueueStore.getPreloadedAudio,
|
||||||
clearPreloadedAudio: typeof playQueueStore.clearPreloadedAudio,
|
clearPreloadedAudio: typeof playQueueStore.clearPreloadedAudio
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取当前歌曲的计算属性
|
// 获取当前歌曲的计算属性
|
||||||
const currentTrack = computed(() => {
|
const currentTrack = computed(() => {
|
||||||
if (
|
if (playQueueStore.playMode.shuffle && playQueueStore.shuffleList.length > 0) {
|
||||||
playQueueStore.playMode.shuffle &&
|
return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
|
||||||
playQueueStore.shuffleList.length > 0
|
} else {
|
||||||
) {
|
return playQueueStore.list[playQueueStore.currentIndex]
|
||||||
return playQueueStore.list[
|
|
||||||
playQueueStore.shuffleList[playQueueStore.currentIndex]
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return playQueueStore.list[playQueueStore.currentIndex]
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 获取当前歌曲的音频源
|
// 获取当前歌曲的音频源
|
||||||
|
@ -41,132 +35,86 @@ const currentAudioSrc = computed(() => {
|
||||||
return track ? track.song.sourceUrl : ''
|
return track ? track.song.sourceUrl : ''
|
||||||
})
|
})
|
||||||
|
|
||||||
watch(
|
watch(() => playQueueStore.isPlaying, (newValue) => {
|
||||||
() => playQueueStore.isPlaying,
|
if (newValue) {
|
||||||
(newValue) => {
|
player.value?.play()
|
||||||
if (newValue) {
|
setMetadata()
|
||||||
player.value?.play()
|
}
|
||||||
setMetadata()
|
else { player.value?.pause() }
|
||||||
} else {
|
})
|
||||||
player.value?.pause()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听当前索引变化,处理预加载逻辑
|
// 监听当前索引变化,处理预加载逻辑
|
||||||
watch(
|
watch(() => playQueueStore.currentIndex, async () => {
|
||||||
() => playQueueStore.currentIndex,
|
console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
|
||||||
async () => {
|
|
||||||
console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
|
|
||||||
|
|
||||||
// 检查是否可以使用预加载的音频
|
// 检查是否可以使用预加载的音频
|
||||||
const track = currentTrack.value
|
const track = currentTrack.value
|
||||||
if (track) {
|
if (track) {
|
||||||
const songId = track.song.cid
|
const songId = track.song.cid
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 首先检查和刷新当前歌曲的资源
|
const preloadedAudio = playQueueStore.getPreloadedAudio(songId)
|
||||||
console.log('[Player] 检查当前歌曲资源:', track.song.name)
|
|
||||||
const updatedSong = await checkAndRefreshSongResource(
|
|
||||||
track.song,
|
|
||||||
(updated) => {
|
|
||||||
// 更新播放队列中的歌曲信息
|
|
||||||
// 在随机播放模式下,currentIndex 是 shuffleList 的索引
|
|
||||||
// 需要通过 shuffleList[currentIndex] 获取实际的 list 索引
|
|
||||||
const actualIndex =
|
|
||||||
playQueueStore.playMode.shuffle &&
|
|
||||||
playQueueStore.shuffleList.length > 0
|
|
||||||
? playQueueStore.shuffleList[playQueueStore.currentIndex]
|
|
||||||
: playQueueStore.currentIndex
|
|
||||||
if (playQueueStore.list[actualIndex]) {
|
|
||||||
playQueueStore.list[actualIndex].song = updated
|
|
||||||
}
|
|
||||||
// 如果歌曲在收藏夹中,也更新收藏夹
|
|
||||||
favourites.updateSongInFavourites(songId, updated)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 使用更新后的歌曲信息
|
if (preloadedAudio) {
|
||||||
const preloadedAudio = playQueueStore.getPreloadedAudio(songId)
|
console.log(`[Player] 使用预加载的音频: ${track.song.name}`)
|
||||||
|
|
||||||
if (preloadedAudio && updatedSong.sourceUrl === track.song.sourceUrl) {
|
// 直接使用预加载的音频数据
|
||||||
console.log(`[Player] 使用预加载的音频: ${track.song.name}`)
|
if (player.value) {
|
||||||
|
// 复制预加载音频的状态到主播放器
|
||||||
|
player.value.src = preloadedAudio.src
|
||||||
|
player.value.currentTime = 0
|
||||||
|
|
||||||
// 直接使用预加载的音频数据
|
// 清理使用过的预加载音频
|
||||||
if (player.value) {
|
playQueueStore.clearPreloadedAudio(songId)
|
||||||
// 复制预加载音频的状态到主播放器
|
|
||||||
player.value.src = preloadedAudio.src
|
|
||||||
player.value.currentTime = 0
|
|
||||||
|
|
||||||
// 清理使用过的预加载音频
|
// 如果正在播放状态,立即播放
|
||||||
playQueueStore.clearPreloadedAudio(songId)
|
if (playQueueStore.isPlaying) {
|
||||||
|
await nextTick()
|
||||||
// 如果正在播放状态,立即播放
|
player.value.play().catch(console.error)
|
||||||
if (playQueueStore.isPlaying) {
|
|
||||||
await nextTick()
|
|
||||||
player.value.play().catch(console.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
playQueueStore.isBuffering = false
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
console.log(`[Player] 正常加载音频: ${track.song.name}`)
|
|
||||||
playQueueStore.isBuffering = true
|
|
||||||
|
|
||||||
// 如果资源地址已更新,清除旧的预加载音频
|
playQueueStore.isBuffering = false
|
||||||
if (updatedSong.sourceUrl !== track.song.sourceUrl) {
|
|
||||||
playQueueStore.clearPreloadedAudio(songId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else {
|
||||||
console.error('[Player] 处理预加载音频时出错:', error)
|
console.log(`[Player] 正常加载音频: ${track.song.name}`)
|
||||||
playQueueStore.isBuffering = true
|
playQueueStore.isBuffering = true
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[Player] 处理预加载音频时出错:', error)
|
||||||
|
playQueueStore.isBuffering = true
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
setMetadata()
|
setMetadata()
|
||||||
|
|
||||||
// 延迟预加载下一首歌,避免影响当前歌曲加载
|
// 延迟预加载下一首歌,避免影响当前歌曲加载
|
||||||
setTimeout(async () => {
|
setTimeout(() => {
|
||||||
try {
|
try {
|
||||||
console.log('[Player] 尝试预加载下一首歌')
|
console.log('[Player] 尝试预加载下一首歌')
|
||||||
|
|
||||||
// 检查函数是否存在
|
// 检查函数是否存在
|
||||||
if (typeof playQueueStore.preloadNext === 'function') {
|
if (typeof playQueueStore.preloadNext === 'function') {
|
||||||
await playQueueStore.preloadNext()
|
playQueueStore.preloadNext()
|
||||||
|
playQueueStore.limitPreloadCache()
|
||||||
// 预加载完成后,检查播放队列是否有更新,同步到收藏夹
|
} else {
|
||||||
playQueueStore.list.forEach((item) => {
|
console.error('[Player] preloadNext 不是一个函数')
|
||||||
if (favourites.isFavourite(item.song.cid)) {
|
|
||||||
favourites.updateSongInFavourites(item.song.cid, item.song)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
playQueueStore.limitPreloadCache()
|
|
||||||
} else {
|
|
||||||
console.error('[Player] preloadNext 不是一个函数')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Player] 预加载失败:', error)
|
|
||||||
}
|
}
|
||||||
}, 1000)
|
} catch (error) {
|
||||||
},
|
console.error('[Player] 预加载失败:', error)
|
||||||
)
|
}
|
||||||
|
}, 1000)
|
||||||
|
})
|
||||||
|
|
||||||
function artistsOrganize(list: string[]) {
|
function artistsOrganize(list: string[]) {
|
||||||
if (list.length === 0) {
|
if (list.length === 0) { return '未知音乐人' }
|
||||||
return '未知音乐人'
|
return list.map((artist) => {
|
||||||
}
|
return artist
|
||||||
return list
|
}).join(' / ')
|
||||||
.map((artist) => {
|
|
||||||
return artist
|
|
||||||
})
|
|
||||||
.join(' / ')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMetadata() {
|
function setMetadata() {
|
||||||
if ('mediaSession' in navigator) {
|
if ('mediaSession' in navigator) {
|
||||||
const current = currentTrack.value
|
let current = currentTrack.value
|
||||||
if (!current) return
|
if (!current) return
|
||||||
|
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
|
@ -174,12 +122,8 @@ function setMetadata() {
|
||||||
artist: artistsOrganize(current.song.artists ?? []),
|
artist: artistsOrganize(current.song.artists ?? []),
|
||||||
album: current.album?.name,
|
album: current.album?.name,
|
||||||
artwork: [
|
artwork: [
|
||||||
{
|
{ src: current.album?.coverUrl ?? '', sizes: '500x500', type: 'image/png' },
|
||||||
src: current.album?.coverUrl ?? '',
|
]
|
||||||
sizes: '500x500',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', playPrevious)
|
navigator.mediaSession.setActionHandler('previoustrack', playPrevious)
|
||||||
|
@ -189,21 +133,16 @@ function setMetadata() {
|
||||||
playQueueStore.currentTime = player.value?.currentTime ?? 0
|
playQueueStore.currentTime = player.value?.currentTime ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(() => playQueueStore.updatedCurrentTime, (newValue) => {
|
||||||
() => playQueueStore.updatedCurrentTime,
|
if (newValue === null) { return }
|
||||||
(newValue) => {
|
if (player.value) player.value.currentTime = newValue
|
||||||
if (newValue === null) {
|
playQueueStore.updatedCurrentTime = null
|
||||||
return
|
})
|
||||||
}
|
|
||||||
if (player.value) player.value.currentTime = newValue
|
|
||||||
playQueueStore.updatedCurrentTime = null
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function playNext() {
|
function playNext() {
|
||||||
if (playQueueStore.currentIndex === playQueueStore.list.length - 1) {
|
if (playQueueStore.currentIndex === playQueueStore.list.length - 1) {
|
||||||
console.log('at the bottom, pause')
|
console.log("at the bottom, pause")
|
||||||
playQueueStore.currentIndex = 0
|
playQueueStore.currentIndex = 0
|
||||||
if (playQueueStore.playMode.repeat === 'all') {
|
if (playQueueStore.playMode.repeat === 'all') {
|
||||||
playQueueStore.currentIndex = 0
|
playQueueStore.currentIndex = 0
|
||||||
|
@ -219,17 +158,11 @@ function playNext() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function playPrevious() {
|
function playPrevious() {
|
||||||
if (
|
if (player.value && (player.value.currentTime ?? 0) < 5 && playQueueStore.currentIndex > 0) {
|
||||||
player.value &&
|
|
||||||
(player.value.currentTime ?? 0) < 5 &&
|
|
||||||
playQueueStore.currentIndex > 0
|
|
||||||
) {
|
|
||||||
playQueueStore.currentIndex--
|
playQueueStore.currentIndex--
|
||||||
playQueueStore.isPlaying = true
|
playQueueStore.isPlaying = true
|
||||||
} else {
|
} else {
|
||||||
if (player.value) {
|
if (player.value) { player.value.currentTime = 0 }
|
||||||
player.value.currentTime = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -246,10 +179,8 @@ function updateCurrentTime() {
|
||||||
const preloadTrigger = (config.preloadTrigger || 50) / 100 // 转换为小数
|
const preloadTrigger = (config.preloadTrigger || 50) / 100 // 转换为小数
|
||||||
const remainingTimeThreshold = config.remainingTimeThreshold || 30
|
const remainingTimeThreshold = config.remainingTimeThreshold || 30
|
||||||
|
|
||||||
if (
|
if ((progress > preloadTrigger || remainingTime < remainingTimeThreshold) && !playQueueStore.isPreloading) {
|
||||||
(progress > preloadTrigger || remainingTime < remainingTimeThreshold) &&
|
|
||||||
!playQueueStore.isPreloading
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
if (typeof playQueueStore.preloadNext === 'function') {
|
if (typeof playQueueStore.preloadNext === 'function') {
|
||||||
playQueueStore.preloadNext()
|
playQueueStore.preloadNext()
|
||||||
|
@ -271,129 +202,107 @@ const { barHeights, connectAudio, isAnalyzing, error } = audioVisualizer({
|
||||||
bassBoost: 0.8,
|
bassBoost: 0.8,
|
||||||
midBoost: 1.2,
|
midBoost: 1.2,
|
||||||
trebleBoost: 1.4,
|
trebleBoost: 1.4,
|
||||||
threshold: 0,
|
threshold: 0
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log('[Player] audioVisualizer 返回值:', {
|
console.log('[Player] audioVisualizer 返回值:', { barHeights: barHeights.value, isAnalyzing: isAnalyzing.value })
|
||||||
barHeights: barHeights.value,
|
|
||||||
isAnalyzing: isAnalyzing.value,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 监听播放列表变化
|
// 监听播放列表变化
|
||||||
watch(
|
watch(() => playQueueStore.list.length, async (newLength) => {
|
||||||
() => playQueueStore.list.length,
|
console.log('[Player] 播放列表长度变化:', newLength)
|
||||||
async (newLength) => {
|
if (newLength === 0) {
|
||||||
console.log('[Player] 播放列表长度变化:', newLength)
|
console.log('[Player] 播放列表为空,跳过连接')
|
||||||
if (newLength === 0) {
|
return
|
||||||
console.log('[Player] 播放列表为空,跳过连接')
|
}
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待下一帧,确保 audio 元素已经渲染
|
// 等待下一帧,确保 audio 元素已经渲染
|
||||||
await nextTick()
|
await nextTick()
|
||||||
|
|
||||||
if (player.value) {
|
if (player.value) {
|
||||||
console.log('[Player] 连接音频元素到可视化器')
|
console.log('[Player] 连接音频元素到可视化器')
|
||||||
console.log('[Player] 音频元素状态:', {
|
console.log('[Player] 音频元素状态:', {
|
||||||
src: player.value.src?.substring(0, 50) + '...',
|
src: player.value.src?.substring(0, 50) + '...',
|
||||||
readyState: player.value.readyState,
|
readyState: player.value.readyState,
|
||||||
paused: player.value.paused,
|
paused: player.value.paused
|
||||||
})
|
})
|
||||||
connectAudio(player.value)
|
connectAudio(player.value)
|
||||||
} else {
|
} else {
|
||||||
console.log('[Player] ❌ 音频元素不存在')
|
console.log('[Player] ❌ 音频元素不存在')
|
||||||
}
|
}
|
||||||
|
|
||||||
playQueueStore.visualizer = barHeights.value
|
playQueueStore.visualizer = barHeights.value
|
||||||
|
|
||||||
// 开始预加载第一首歌的下一首
|
// 开始预加载第一首歌的下一首
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
playQueueStore.preloadNext()
|
playQueueStore.preloadNext()
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
// 初始化音量
|
// 初始化音量
|
||||||
if (player.value) {
|
if (player.value) {
|
||||||
initializeVolume()
|
initializeVolume()
|
||||||
}
|
}
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
|
||||||
// 监听音频元素变化
|
// 监听音频元素变化
|
||||||
watch(
|
watch(() => player.value, (audioElement) => {
|
||||||
() => player.value,
|
if (audioElement && playQueueStore.list.length > 0) {
|
||||||
(audioElement) => {
|
connectAudio(audioElement)
|
||||||
if (audioElement && playQueueStore.list.length > 0) {
|
}
|
||||||
connectAudio(audioElement)
|
})
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听可视化器数据变化
|
// 监听可视化器数据变化
|
||||||
watch(
|
watch(() => barHeights.value, (newHeights) => {
|
||||||
() => barHeights.value,
|
playQueueStore.visualizer = newHeights
|
||||||
(newHeights) => {
|
}, { deep: true })
|
||||||
playQueueStore.visualizer = newHeights
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听错误
|
// 监听错误
|
||||||
watch(
|
watch(() => error.value, (newError) => {
|
||||||
() => error.value,
|
if (newError) {
|
||||||
(newError) => {
|
console.error('[Player] 可视化器错误:', newError)
|
||||||
if (newError) {
|
}
|
||||||
console.error('[Player] 可视化器错误:', newError)
|
})
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 切换播放模式
|
// 切换播放模式
|
||||||
watch(
|
watch(() => playQueueStore.playMode.shuffle, (isShuffle) => {
|
||||||
() => playQueueStore.playMode.shuffle,
|
if (isShuffle) {
|
||||||
(isShuffle) => {
|
const currentIndex = playQueueStore.currentIndex
|
||||||
if (isShuffle) {
|
const trackCount = playQueueStore.list.length
|
||||||
const currentIndex = playQueueStore.currentIndex
|
|
||||||
const trackCount = playQueueStore.list.length
|
|
||||||
|
|
||||||
// 1. 已播放部分:不变
|
// 1. 已播放部分:不变
|
||||||
let shuffledList = [...Array(currentIndex).keys()]
|
let shuffledList = [...Array(currentIndex).keys()]
|
||||||
|
|
||||||
// 2. 构建待打乱的列表
|
// 2. 构建待打乱的列表
|
||||||
const shuffleSpace = [...Array(trackCount).keys()].filter((index) =>
|
let shuffleSpace = [...Array(trackCount).keys()].filter(index =>
|
||||||
playQueueStore.shuffleCurrent
|
playQueueStore.shuffleCurrent ? index >= currentIndex : index > currentIndex
|
||||||
? index >= currentIndex
|
)
|
||||||
: index > currentIndex,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 3. 随机打乱
|
// 3. 随机打乱
|
||||||
shuffleSpace.sort(() => Math.random() - 0.5)
|
shuffleSpace.sort(() => Math.random() - 0.5)
|
||||||
|
|
||||||
// 4. 如果当前曲目不参与打乱,插入回当前位置(即 currentIndex 处)
|
// 4. 如果当前曲目不参与打乱,插入回当前位置(即 currentIndex 处)
|
||||||
if (!playQueueStore.shuffleCurrent) {
|
if (!playQueueStore.shuffleCurrent) {
|
||||||
shuffledList.push(currentIndex)
|
shuffledList.push(currentIndex)
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 拼接:已播放部分 + 当前(可选)+ 打乱后的剩余部分
|
|
||||||
shuffledList = shuffledList.concat(shuffleSpace)
|
|
||||||
|
|
||||||
// 6. 应用 shuffleList
|
|
||||||
playQueueStore.shuffleList = shuffledList
|
|
||||||
|
|
||||||
// 清除 shuffleCurrent 状态
|
|
||||||
playQueueStore.shuffleCurrent = undefined
|
|
||||||
} else {
|
|
||||||
// 退出随机播放:恢复当前播放曲目的原索引
|
|
||||||
playQueueStore.currentIndex =
|
|
||||||
playQueueStore.shuffleList[playQueueStore.currentIndex]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换播放模式后重新预加载
|
// 5. 拼接:已播放部分 + 当前(可选)+ 打乱后的剩余部分
|
||||||
setTimeout(() => {
|
shuffledList = shuffledList.concat(shuffleSpace)
|
||||||
playQueueStore.clearAllPreloadedAudio()
|
|
||||||
playQueueStore.preloadNext()
|
// 6. 应用 shuffleList
|
||||||
}, 500)
|
playQueueStore.shuffleList = shuffledList
|
||||||
},
|
|
||||||
)
|
// 清除 shuffleCurrent 状态
|
||||||
|
playQueueStore.shuffleCurrent = undefined
|
||||||
|
} else {
|
||||||
|
// 退出随机播放:恢复当前播放曲目的原索引
|
||||||
|
playQueueStore.currentIndex = playQueueStore.shuffleList[playQueueStore.currentIndex]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 切换播放模式后重新预加载
|
||||||
|
setTimeout(() => {
|
||||||
|
playQueueStore.clearAllPreloadedAudio()
|
||||||
|
playQueueStore.preloadNext()
|
||||||
|
}, 500)
|
||||||
|
})
|
||||||
|
|
||||||
function getCurrentTrack() {
|
function getCurrentTrack() {
|
||||||
return currentTrack.value
|
return currentTrack.value
|
||||||
|
@ -404,7 +313,7 @@ function initializeVolume() {
|
||||||
if (player.value) {
|
if (player.value) {
|
||||||
const savedVolume = localStorage.getItem('audioVolume')
|
const savedVolume = localStorage.getItem('audioVolume')
|
||||||
if (savedVolume) {
|
if (savedVolume) {
|
||||||
const volumeValue = Number.parseFloat(savedVolume)
|
const volumeValue = parseFloat(savedVolume)
|
||||||
player.value.volume = volumeValue
|
player.value.volume = volumeValue
|
||||||
console.log('[Player] 初始化音量:', volumeValue)
|
console.log('[Player] 初始化音量:', volumeValue)
|
||||||
} else {
|
} else {
|
||||||
|
@ -430,7 +339,7 @@ function syncVolumeFromStorage() {
|
||||||
if (player.value) {
|
if (player.value) {
|
||||||
const savedVolume = localStorage.getItem('audioVolume')
|
const savedVolume = localStorage.getItem('audioVolume')
|
||||||
if (savedVolume) {
|
if (savedVolume) {
|
||||||
const volumeValue = Number.parseFloat(savedVolume)
|
const volumeValue = parseFloat(savedVolume)
|
||||||
if (player.value.volume !== volumeValue) {
|
if (player.value.volume !== volumeValue) {
|
||||||
player.value.volume = volumeValue
|
player.value.volume = volumeValue
|
||||||
}
|
}
|
||||||
|
@ -486,7 +395,7 @@ setInterval(syncVolumeFromStorage, 100)
|
||||||
|
|
||||||
<RouterLink to="/playroom">
|
<RouterLink to="/playroom">
|
||||||
<div class="flex items-center w-32 h-9">
|
<div class="flex items-center w-32 h-9">
|
||||||
<span class="truncate text-xs">{{ getCurrentTrack()?.song.name }}</span>
|
<span class="truncate">{{ getCurrentTrack()?.song.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</RouterLink>
|
</RouterLink>
|
||||||
|
|
||||||
|
|
|
@ -543,39 +543,8 @@ watch(() => props.lrcSrc, async (newSrc) => {
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
|
|
||||||
// 页面焦点处理函数变量声明
|
|
||||||
let handleVisibilityChange: (() => void) | null = null
|
|
||||||
|
|
||||||
// 页面焦点处理
|
|
||||||
function setupPageFocusHandlers() {
|
|
||||||
handleVisibilityChange = () => {
|
|
||||||
if (document.hidden) {
|
|
||||||
// 页面失去焦点时暂停动画
|
|
||||||
if (scrollTween) scrollTween.pause()
|
|
||||||
if (highlightTween) highlightTween.pause()
|
|
||||||
} else {
|
|
||||||
// 页面重新获得焦点时恢复并重新同步
|
|
||||||
if (scrollTween && scrollTween.paused()) scrollTween.resume()
|
|
||||||
if (highlightTween && highlightTween.paused()) highlightTween.resume()
|
|
||||||
|
|
||||||
// 重新同步歌词位置
|
|
||||||
nextTick(() => {
|
|
||||||
if (currentLineIndex.value >= 0 && autoScroll.value && !userScrolling.value) {
|
|
||||||
scrollToLine(currentLineIndex.value, false) // 不使用动画,直接定位
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 组件挂载时的入场动画
|
// 组件挂载时的入场动画
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 设置页面焦点处理
|
|
||||||
setupPageFocusHandlers()
|
|
||||||
|
|
||||||
// 控制面板入场动画
|
// 控制面板入场动画
|
||||||
if (controlPanel.value) {
|
if (controlPanel.value) {
|
||||||
gsap.fromTo(controlPanel.value,
|
gsap.fromTo(controlPanel.value,
|
||||||
|
@ -608,11 +577,6 @@ onUnmounted(() => {
|
||||||
if (scrollTween) scrollTween.kill()
|
if (scrollTween) scrollTween.kill()
|
||||||
if (highlightTween) highlightTween.kill()
|
if (highlightTween) highlightTween.kill()
|
||||||
if (userScrollTimeout) clearTimeout(userScrollTimeout)
|
if (userScrollTimeout) clearTimeout(userScrollTimeout)
|
||||||
|
|
||||||
// 清理页面焦点事件监听器
|
|
||||||
if (handleVisibilityChange) {
|
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 暴露方法给父组件
|
// 暴露方法给父组件
|
||||||
|
|
|
@ -83,9 +83,6 @@ onMounted(async () => {
|
||||||
thumbUpdate()
|
thumbUpdate()
|
||||||
|
|
||||||
setupEntranceAnimations()
|
setupEntranceAnimations()
|
||||||
|
|
||||||
// 添加页面焦点事件监听
|
|
||||||
setupPageFocusHandlers()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function timeFormatter(time: number) {
|
function timeFormatter(time: number) {
|
||||||
|
@ -93,7 +90,7 @@ function timeFormatter(time: number) {
|
||||||
if (timeInSeconds < 0) { return '-:--' }
|
if (timeInSeconds < 0) { return '-:--' }
|
||||||
const minutes = Math.floor(timeInSeconds / 60)
|
const minutes = Math.floor(timeInSeconds / 60)
|
||||||
const seconds = Math.floor(timeInSeconds % 60)
|
const seconds = Math.floor(timeInSeconds % 60)
|
||||||
if (Number.isNaN(minutes) || Number.isNaN(seconds)) { return '-:--' }
|
if (isNaN(minutes) || isNaN(seconds)) { return '-:--' }
|
||||||
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -158,7 +155,7 @@ function createVolumeDraggable() {
|
||||||
// 保存音量到localStorage
|
// 保存音量到localStorage
|
||||||
localStorage.setItem('audioVolume', newVolume.toString())
|
localStorage.setItem('audioVolume', newVolume.toString())
|
||||||
},
|
},
|
||||||
onDragEnd: () => {
|
onDragEnd: function () {
|
||||||
// 拖拽结束时也保存一次
|
// 拖拽结束时也保存一次
|
||||||
localStorage.setItem('audioVolume', volume.value.toString())
|
localStorage.setItem('audioVolume', volume.value.toString())
|
||||||
}
|
}
|
||||||
|
@ -422,98 +419,9 @@ watch(() => [preferences.presentLyrics, getCurrentTrack()?.song.lyricUrl], (newV
|
||||||
}
|
}
|
||||||
}, { immediate: true })
|
}, { immediate: true })
|
||||||
|
|
||||||
// 页面焦点处理函数变量声明
|
|
||||||
let handleVisibilityChange: (() => void) | null = null
|
|
||||||
let handlePageFocus: (() => void) | null = null
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
// 清理页面焦点事件监听器
|
|
||||||
if (handleVisibilityChange) {
|
|
||||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
|
||||||
}
|
|
||||||
if (handlePageFocus) {
|
|
||||||
window.removeEventListener('focus', handlePageFocus)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 页面焦点处理函数
|
|
||||||
function setupPageFocusHandlers() {
|
|
||||||
handleVisibilityChange = () => {
|
|
||||||
if (document.hidden) {
|
|
||||||
// 页面失去焦点时,暂停所有动画
|
|
||||||
console.log('[Playroom] 页面失去焦点,暂停动画')
|
|
||||||
} else {
|
|
||||||
// 页面重新获得焦点时,重新同步状态
|
|
||||||
console.log('[Playroom] 页面重新获得焦点,同步状态')
|
|
||||||
nextTick(() => {
|
|
||||||
resyncLyricsState()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
handlePageFocus = () => {
|
|
||||||
console.log('[Playroom] 窗口获得焦点,同步状态')
|
|
||||||
nextTick(() => {
|
|
||||||
resyncLyricsState()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听页面可见性变化
|
|
||||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
|
||||||
window.addEventListener('focus', handlePageFocus)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重新同步歌词状态
|
|
||||||
function resyncLyricsState() {
|
|
||||||
const currentTrack = getCurrentTrack()
|
|
||||||
if (!currentTrack) { return }
|
|
||||||
|
|
||||||
console.log('[Playroom] 重新同步歌词状态')
|
|
||||||
|
|
||||||
// 重置动画状态
|
|
||||||
if (controllerRef.value) {
|
|
||||||
gsap.set(controllerRef.value, {
|
|
||||||
marginLeft: '0rem',
|
|
||||||
marginRight: '0rem'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lyricsSection.value) {
|
|
||||||
gsap.set(lyricsSection.value, {
|
|
||||||
opacity: 1,
|
|
||||||
x: 0,
|
|
||||||
y: 0,
|
|
||||||
scale: 1
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查当前歌词显示状态应该是什么
|
|
||||||
const shouldShowLyrics = preferences.presentLyrics && currentTrack.song.lyricUrl ? true : false
|
|
||||||
|
|
||||||
if (shouldShowLyrics !== presentLyrics.value) {
|
|
||||||
console.log(`[Playroom] 歌词状态不一致,重新设置: ${presentLyrics.value} -> ${shouldShowLyrics}`)
|
|
||||||
|
|
||||||
// 直接设置状态,不触发动画
|
|
||||||
presentLyrics.value = shouldShowLyrics
|
|
||||||
|
|
||||||
// 如果需要显示歌词,重新执行显示动画
|
|
||||||
if (shouldShowLyrics) {
|
|
||||||
nextTick(() => {
|
|
||||||
const tl = gsap.timeline()
|
|
||||||
tl.from(controllerRef.value, {
|
|
||||||
marginRight: '-40rem',
|
|
||||||
duration: 0.4,
|
|
||||||
ease: "power2.out"
|
|
||||||
}).fromTo(lyricsSection.value,
|
|
||||||
{ opacity: 0, x: 50, scale: 0.95 },
|
|
||||||
{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: "power2.out" },
|
|
||||||
"-=0.2"
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// New: Watch for track changes and animate
|
// New: Watch for track changes and animate
|
||||||
watch(() => playQueueStore.currentIndex, () => {
|
watch(() => playQueueStore.currentIndex, () => {
|
||||||
if (albumCover.value) {
|
if (albumCover.value) {
|
||||||
|
@ -605,9 +513,9 @@ watch(() => playQueueStore.currentIndex, () => {
|
||||||
|
|
||||||
<div class="w-full flex justify-between">
|
<div class="w-full flex justify-between">
|
||||||
<!-- ...existing time display code... -->
|
<!-- ...existing time display code... -->
|
||||||
<div class="font-medium flex-1 text-left text-xs relative">
|
<div class="font-medium flex-1 text-left relative">
|
||||||
<span
|
<span
|
||||||
class="text-black blur-lg absolute top-0 text-xs">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
|
class="text-black blur-lg absolute top-0">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
|
||||||
<span
|
<span
|
||||||
class="text-white/90 absolute top-0">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
|
class="text-white/90 absolute top-0">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -618,7 +526,7 @@ watch(() => playQueueStore.currentIndex, () => {
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<div class="flex-1" />
|
<div class="flex-1" />
|
||||||
<button
|
<button
|
||||||
class="text-white/90 text-xs font-medium text-right relative transition-colors duration-200 hover:text-white"
|
class="text-white/90 font-medium text-right relative transition-colors duration-200 hover:text-white"
|
||||||
@click="preferences.displayTimeLeft = !preferences.displayTimeLeft">
|
@click="preferences.displayTimeLeft = !preferences.displayTimeLeft">
|
||||||
<span
|
<span
|
||||||
class="text-black blur-lg absolute top-0">{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playQueueStore.duration) - Math.floor(playQueueStore.currentTime) : playQueueStore.duration)}` }}</span>
|
class="text-black blur-lg absolute top-0">{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playQueueStore.duration) - Math.floor(playQueueStore.currentTime) : playQueueStore.duration)}` }}</span>
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from "pinia"
|
||||||
import { computed, ref } from 'vue'
|
import { ref, computed } from "vue"
|
||||||
import { checkAndRefreshSongResource } from '../utils'
|
|
||||||
|
|
||||||
export const usePlayQueueStore = defineStore('queue', () => {
|
export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
const list = ref<QueueItem[]>([])
|
const list = ref<QueueItem[]>([])
|
||||||
|
@ -14,11 +13,11 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
const visualizer = ref<number[]>([0, 0, 0, 0, 0, 0])
|
const visualizer = ref<number[]>([0, 0, 0, 0, 0, 0])
|
||||||
const shuffleList = ref<number[]>([])
|
const shuffleList = ref<number[]>([])
|
||||||
const playMode = ref<{
|
const playMode = ref<{
|
||||||
shuffle: boolean
|
shuffle: boolean,
|
||||||
repeat: 'off' | 'single' | 'all'
|
repeat: 'off' | 'single' | 'all'
|
||||||
}>({
|
}>({
|
||||||
shuffle: false,
|
shuffle: false,
|
||||||
repeat: 'off',
|
repeat: 'off'
|
||||||
})
|
})
|
||||||
const shuffleCurrent = ref<boolean | undefined>(undefined)
|
const shuffleCurrent = ref<boolean | undefined>(undefined)
|
||||||
|
|
||||||
|
@ -36,13 +35,10 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (playMode.value.shuffle && shuffleList.value.length > 0) {
|
if (playMode.value.shuffle && shuffleList.value.length > 0) {
|
||||||
// 当前在 shuffleList 中的位置
|
const currentShuffleIndex = shuffleList.value.indexOf(currentIndex.value)
|
||||||
const currentShuffleIndex = currentIndex.value
|
|
||||||
if (currentShuffleIndex < shuffleList.value.length - 1) {
|
if (currentShuffleIndex < shuffleList.value.length - 1) {
|
||||||
// 返回下一个位置对应的原始 list 索引
|
|
||||||
return shuffleList.value[currentShuffleIndex + 1]
|
return shuffleList.value[currentShuffleIndex + 1]
|
||||||
} else if (playMode.value.repeat === 'all') {
|
} else if (playMode.value.repeat === 'all') {
|
||||||
// 返回第一个位置对应的原始 list 索引
|
|
||||||
return shuffleList.value[0]
|
return shuffleList.value[0]
|
||||||
}
|
}
|
||||||
return -1
|
return -1
|
||||||
|
@ -59,14 +55,19 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
|
|
||||||
// 预加载下一首歌
|
// 预加载下一首歌
|
||||||
const preloadNext = async () => {
|
const preloadNext = async () => {
|
||||||
|
|
||||||
const nextIndex = getNextIndex.value
|
const nextIndex = getNextIndex.value
|
||||||
if (nextIndex === -1) {
|
if (nextIndex === -1) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取下一首歌曲对象
|
// 获取下一首歌曲对象
|
||||||
// nextIndex 已经是原始 list 中的索引
|
let nextSong
|
||||||
const nextSong = list.value[nextIndex]
|
if (playMode.value.shuffle && shuffleList.value.length > 0) {
|
||||||
|
nextSong = list.value[shuffleList.value[nextIndex]]
|
||||||
|
} else {
|
||||||
|
nextSong = list.value[nextIndex]
|
||||||
|
}
|
||||||
|
|
||||||
if (!nextSong || !nextSong.song) {
|
if (!nextSong || !nextSong.song) {
|
||||||
return
|
return
|
||||||
|
@ -88,24 +89,6 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
isPreloading.value = true
|
isPreloading.value = true
|
||||||
preloadProgress.value = 0
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果歌曲在收藏夹中,也更新收藏夹
|
|
||||||
// 注意:这里不直接导入 favourites store 以避免循环依赖
|
|
||||||
// 改为触发一个事件或者在调用方处理
|
|
||||||
console.log('[Store] 预加载时需要更新收藏夹:', updated.name)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
const audio = new Audio()
|
const audio = new Audio()
|
||||||
audio.preload = 'auto'
|
audio.preload = 'auto'
|
||||||
audio.crossOrigin = 'anonymous'
|
audio.crossOrigin = 'anonymous'
|
||||||
|
@ -124,20 +107,19 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
preloadedAudio.value.set(songId, audio)
|
preloadedAudio.value.set(songId, audio)
|
||||||
isPreloading.value = false
|
isPreloading.value = false
|
||||||
preloadProgress.value = 100
|
preloadProgress.value = 100
|
||||||
console.log('[Store] 预加载完成:', updatedSong.name)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听加载错误
|
// 监听加载错误
|
||||||
audio.addEventListener('error', (e) => {
|
audio.addEventListener('error', (e) => {
|
||||||
console.error(`[Store] 预加载音频失败: ${updatedSong.name}`, e)
|
console.error(`[Store] 预加载音频失败: ${e}`)
|
||||||
isPreloading.value = false
|
isPreloading.value = false
|
||||||
preloadProgress.value = 0
|
preloadProgress.value = 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// 使用更新后的音频源
|
// 设置音频源并开始加载
|
||||||
audio.src = updatedSong.sourceUrl!
|
audio.src = nextSong.song.sourceUrl
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Store] 预加载过程出错:', error)
|
|
||||||
isPreloading.value = false
|
isPreloading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -185,7 +167,7 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
progress: preloadProgress.value,
|
progress: preloadProgress.value,
|
||||||
cacheSize: preloadedAudio.value.size,
|
cacheSize: preloadedAudio.value.size,
|
||||||
cachedSongs: Array.from(preloadedAudio.value.keys()),
|
cachedSongs: Array.from(preloadedAudio.value.keys()),
|
||||||
nextIndex: getNextIndex.value,
|
nextIndex: getNextIndex.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -212,6 +194,6 @@ export const usePlayQueueStore = defineStore('queue', () => {
|
||||||
clearPreloadedAudio,
|
clearPreloadedAudio,
|
||||||
clearAllPreloadedAudio,
|
clearAllPreloadedAudio,
|
||||||
limitPreloadCache,
|
limitPreloadCache,
|
||||||
debugPreloadState,
|
debugPreloadState
|
||||||
}
|
}
|
||||||
})
|
})
|
|
@ -1,6 +1,5 @@
|
||||||
import artistsOrganize from "./artistsOrganize"
|
import artistsOrganize from "./artistsOrganize"
|
||||||
import { audioVisualizer } from "./audioVisualizer"
|
import { audioVisualizer } from "./audioVisualizer"
|
||||||
import cicdInfo from "./cicdInfo"
|
import cicdInfo from "./cicdInfo"
|
||||||
import { checkAndRefreshSongResource, checkAndRefreshMultipleSongs } from "./songResourceChecker"
|
|
||||||
|
|
||||||
export { artistsOrganize, audioVisualizer, cicdInfo, checkAndRefreshSongResource, checkAndRefreshMultipleSongs }
|
export { artistsOrganize, audioVisualizer, cicdInfo }
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
import axios from 'axios'
|
|
||||||
import apis from '../apis'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查歌曲资源 URL 是否可用,如果不可用则刷新
|
|
||||||
* @param song 要检查的歌曲对象
|
|
||||||
* @param updateCallback 更新歌曲信息的回调函数
|
|
||||||
* @returns 更新后的歌曲对象(如果需要更新)或原始歌曲对象
|
|
||||||
*/
|
|
||||||
export const checkAndRefreshSongResource = async (
|
|
||||||
song: Song,
|
|
||||||
updateCallback?: (updatedSong: Song) => void
|
|
||||||
): Promise<Song> => {
|
|
||||||
if (!song.sourceUrl) {
|
|
||||||
console.warn('[ResourceChecker] 歌曲没有 sourceUrl:', song.name)
|
|
||||||
return song
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 检查资源是否可用
|
|
||||||
await axios.head(song.sourceUrl, {
|
|
||||||
headers: {
|
|
||||||
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
||||||
'Pragma': 'no-cache',
|
|
||||||
'Expires': '0'
|
|
||||||
},
|
|
||||||
params: {
|
|
||||||
_t: Date.now() // 添加时间戳参数避免缓存
|
|
||||||
},
|
|
||||||
timeout: 5000 // 5秒超时
|
|
||||||
})
|
|
||||||
|
|
||||||
// 资源可用,返回原始歌曲
|
|
||||||
console.log('[ResourceChecker] 资源可用:', song.name)
|
|
||||||
return song
|
|
||||||
} catch (error) {
|
|
||||||
// 资源不可用,刷新歌曲信息
|
|
||||||
console.log('[ResourceChecker] 资源不可用,正在刷新:', song.name, error)
|
|
||||||
|
|
||||||
try {
|
|
||||||
const updatedSong = await apis.getSong(song.cid)
|
|
||||||
console.log('[ResourceChecker] 歌曲信息已刷新:', updatedSong.name)
|
|
||||||
|
|
||||||
// 调用更新回调(如果提供)
|
|
||||||
if (updateCallback) {
|
|
||||||
updateCallback(updatedSong)
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedSong
|
|
||||||
} catch (refreshError) {
|
|
||||||
console.error('[ResourceChecker] 刷新歌曲信息失败:', refreshError)
|
|
||||||
// 刷新失败,返回原始歌曲
|
|
||||||
return song
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 批量检查多首歌曲的资源
|
|
||||||
* @param songs 要检查的歌曲数组
|
|
||||||
* @param updateCallback 更新单首歌曲信息的回调函数
|
|
||||||
* @returns 更新后的歌曲数组
|
|
||||||
*/
|
|
||||||
export const checkAndRefreshMultipleSongs = async (
|
|
||||||
songs: Song[],
|
|
||||||
updateCallback?: (updatedSong: Song, originalIndex: number) => void
|
|
||||||
): Promise<Song[]> => {
|
|
||||||
const results: Song[] = []
|
|
||||||
|
|
||||||
for (let i = 0; i < songs.length; i++) {
|
|
||||||
const originalSong = songs[i]
|
|
||||||
const updatedSong = await checkAndRefreshSongResource(
|
|
||||||
originalSong,
|
|
||||||
updateCallback ? (updated) => updateCallback(updated, i) : undefined
|
|
||||||
)
|
|
||||||
results.push(updatedSong)
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user