feat: 使用新的播放器库(施工中)
This commit is contained in:
parent
c57d7bc42d
commit
fd6253e626
6
package-lock.json
generated
6
package-lock.json
generated
|
@ -8,6 +8,7 @@
|
||||||
"name": "msr-mod",
|
"name": "msr-mod",
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrian/music-surge-revolution": "^0.0.0-20250831052313",
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
|
@ -42,6 +43,11 @@
|
||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@astrian/music-surge-revolution": {
|
||||||
|
"version": "0.0.0-20250831055015",
|
||||||
|
"resolved": "https://registry.npmjs.org/@astrian/music-surge-revolution/-/music-surge-revolution-0.0.0-20250831055015.tgz",
|
||||||
|
"integrity": "sha512-joXpUDjez+5M90C4RoGsfHZifXdUBhqSHH+kW3v6TDQJQZwh/sdof1ro4qYXG3/8D8AkfWdhFV3O1C8nxG6syw=="
|
||||||
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
"version": "7.27.1",
|
"version": "7.27.1",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||||
|
|
|
@ -20,6 +20,7 @@
|
||||||
"prebuild:safari": "node scripts/prebuild-safari.js"
|
"prebuild:safari": "node scripts/prebuild-safari.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@astrian/music-surge-revolution": "^0.0.0-20250831052313",
|
||||||
"@tailwindcss/vite": "^4.1.7",
|
"@tailwindcss/vite": "^4.1.7",
|
||||||
"axios": "^1.9.0",
|
"axios": "^1.9.0",
|
||||||
"gsap": "^3.13.0",
|
"gsap": "^3.13.0",
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import Player from './components/Player.vue'
|
|
||||||
import PreferencePanel from './components/PreferencePanel.vue'
|
import PreferencePanel from './components/PreferencePanel.vue'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
@ -73,7 +72,6 @@ watch(() => presentPreferencePanel, (value) => {
|
||||||
<CorgIcon :size="4" />
|
<CorgIcon :size="4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<Player />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import { ref, watch, nextTick } from 'vue'
|
||||||
import { gsap } from 'gsap'
|
import { gsap } from 'gsap'
|
||||||
import apis from '../apis'
|
import apis from '../apis'
|
||||||
import { artistsOrganize } from '../utils'
|
import { artistsOrganize } from '../utils'
|
||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
import { usePlayStore } from '../stores/usePlayStore'
|
||||||
import TrackItem from './TrackItem.vue'
|
import TrackItem from './TrackItem.vue'
|
||||||
import LoadingIndicator from '../assets/icons/loadingindicator.vue'
|
import LoadingIndicator from '../assets/icons/loadingindicator.vue'
|
||||||
|
|
||||||
|
@ -17,9 +17,7 @@ const props = defineProps<{
|
||||||
present: boolean
|
present: boolean
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<(e: 'dismiss') => void>()
|
||||||
(e: 'dismiss'): void
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const album = ref<Album>()
|
const album = ref<Album>()
|
||||||
const dialogBackdrop = ref<HTMLElement>()
|
const dialogBackdrop = ref<HTMLElement>()
|
||||||
|
@ -28,7 +26,7 @@ const closeButton = ref<HTMLElement>()
|
||||||
|
|
||||||
// Animation functions
|
// Animation functions
|
||||||
const animateIn = async () => {
|
const animateIn = async () => {
|
||||||
if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) return
|
if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) {return}
|
||||||
|
|
||||||
// Set initial states
|
// Set initial states
|
||||||
gsap.set(dialogBackdrop.value, { opacity: 0 })
|
gsap.set(dialogBackdrop.value, { opacity: 0 })
|
||||||
|
@ -59,7 +57,7 @@ const animateIn = async () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const animateOut = () => {
|
const animateOut = () => {
|
||||||
if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) return
|
if (!dialogBackdrop.value || !dialogContent.value || !closeButton.value) {return}
|
||||||
|
|
||||||
const tl = gsap.timeline({
|
const tl = gsap.timeline({
|
||||||
onComplete: () => emit('dismiss')
|
onComplete: () => emit('dismiss')
|
||||||
|
@ -85,6 +83,7 @@ const animateOut = () => {
|
||||||
}, "-=0.1")
|
}, "-=0.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used inside <template>
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
animateOut()
|
animateOut()
|
||||||
}
|
}
|
||||||
|
@ -100,9 +99,9 @@ watch(() => props.albumCid, async () => {
|
||||||
console.log("AlbumDetailDialog mounted with albumCid:", props.albumCid)
|
console.log("AlbumDetailDialog mounted with albumCid:", props.albumCid)
|
||||||
album.value = undefined // Reset album when cid changes
|
album.value = undefined // Reset album when cid changes
|
||||||
try {
|
try {
|
||||||
let res = await apis.getAlbum(props.albumCid)
|
const res = await apis.getAlbum(props.albumCid)
|
||||||
for (const track in res.songs) {
|
for (const track in res.songs) {
|
||||||
res.songs[parseInt(track)] = await apis.getSong(res.songs[parseInt(track)].cid)
|
res.songs[Number.parseInt(track, 10)] = await apis.getSong(res.songs[Number.parseInt(track, 10)].cid)
|
||||||
}
|
}
|
||||||
album.value = res
|
album.value = res
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -110,15 +109,16 @@ watch(() => props.albumCid, async () => {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const playQueue = usePlayQueueStore()
|
const playQueue = usePlayStore()
|
||||||
|
|
||||||
function playTheAlbum(from: number = 0) {
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
|
function playTheAlbum(from = 0) {
|
||||||
if (playQueue.queueReplaceLock) {
|
if (playQueue.queueReplaceLock) {
|
||||||
if (!confirm("当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?")) { return }
|
if (!confirm("当前操作会将你的播放队列清空、放入这张专辑所有曲目,并从头播放。继续吗?")) { return }
|
||||||
playQueue.queueReplaceLock = false
|
playQueue.queueReplaceLock = false
|
||||||
}
|
}
|
||||||
|
|
||||||
let newPlayQueue = []
|
const newPlayQueue = []
|
||||||
for (const track of album.value?.songs ?? []) {
|
for (const track of album.value?.songs ?? []) {
|
||||||
console.log(track)
|
console.log(track)
|
||||||
newPlayQueue.push({
|
newPlayQueue.push({
|
||||||
|
@ -126,21 +126,21 @@ function playTheAlbum(from: number = 0) {
|
||||||
album: album.value
|
album: album.value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
playQueue.list = newPlayQueue
|
playQueue.replaceQueue(newPlayQueue)
|
||||||
playQueue.currentIndex = from
|
// playQueue.currentIndex = from
|
||||||
playQueue.isPlaying = true
|
playQueue.togglePlay(true)
|
||||||
playQueue.isBuffering = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used inside <template>
|
||||||
function shuffle() {
|
function shuffle() {
|
||||||
playTheAlbum()
|
// playTheAlbum()
|
||||||
playQueue.shuffleCurrent = true
|
// playQueue.shuffleCurrent = true
|
||||||
playQueue.playMode.shuffle = false
|
// playQueue.playMode.shuffle = false
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
playQueue.playMode.shuffle = true
|
// playQueue.playMode.shuffle = true
|
||||||
playQueue.isPlaying = true
|
// playQueue.isPlaying = true
|
||||||
playQueue.isBuffering = true
|
// playQueue.isBuffering = true
|
||||||
}, 100)
|
// }, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
20
src/components/MiniPlayer.vue
Normal file
20
src/components/MiniPlayer.vue
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePlayStore } from '../stores/usePlayStore'
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
|
const playQueue = usePlayStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<RouterLink to="/playroom" v-if="playQueue.currentTrack">
|
||||||
|
<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 overflow-hidden">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<div class="rounded-full w-9 h-9 bg-gray-600 overflow-hidden">
|
||||||
|
<img :src="playQueue.currentTrack.metadata?.artwork?.[0].src ?? ''" />
|
||||||
|
</div>
|
||||||
|
<div class="text-white">{{playQueue.currentTrack.metadata?.title ?? "未知歌曲"}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</RouterLink>
|
||||||
|
</template>
|
|
@ -5,18 +5,19 @@ import { useFavourites } from '../stores/useFavourites'
|
||||||
|
|
||||||
import StarSlashIcon from '../assets/icons/starslash.vue'
|
import StarSlashIcon from '../assets/icons/starslash.vue'
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
const favourites = useFavourites()
|
const favourites = useFavourites()
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
const hover = ref(false)
|
const hover = ref(false)
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
item: QueueItem
|
item: InternalQueueItem
|
||||||
index: number
|
index: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const emit = defineEmits<{
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
(e: 'play', index: number): void
|
const emit = defineEmits<(e: 'play', index: number) => void>()
|
||||||
}>()
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
import { usePlayStore } from '../stores/usePlayStore'
|
||||||
import { artistsOrganize, supportsWebAudioVisualization } from '../utils'
|
import { artistsOrganize, supportsWebAudioVisualization } from '../utils'
|
||||||
|
|
||||||
import XIcon from '../assets/icons/x.vue'
|
import XIcon from '../assets/icons/x.vue'
|
||||||
|
@ -10,18 +10,21 @@ import SoundwaveIcon from '../assets/icons/soundwave.vue'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
queueItem: QueueItem
|
queueItem: InternalQueueItem
|
||||||
isCurrent: boolean
|
isCurrent: boolean
|
||||||
index: number
|
index: number
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const playQueueStore = usePlayQueueStore()
|
const playStore = usePlayStore()
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
const hover = ref(false)
|
const hover = ref(false)
|
||||||
|
|
||||||
// 检查浏览器是否支持音频可视化
|
// 检查浏览器是否支持音频可视化
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
const isAudioVisualizationSupported = supportsWebAudioVisualization()
|
const isAudioVisualizationSupported = supportsWebAudioVisualization()
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
function moveUp() {
|
function moveUp() {
|
||||||
if (props.index === 0) return
|
if (props.index === 0) return
|
||||||
|
|
||||||
|
@ -58,6 +61,7 @@ function moveUp() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
function moveDown() {
|
function moveDown() {
|
||||||
const listLength = playQueueStore.playMode.shuffle ? playQueueStore.shuffleList.length : playQueueStore.list.length
|
const listLength = playQueueStore.playMode.shuffle ? playQueueStore.shuffleList.length : playQueueStore.list.length
|
||||||
if (props.index === listLength - 1) return
|
if (props.index === listLength - 1) return
|
||||||
|
@ -95,6 +99,7 @@ function moveDown() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
function removeItem() {
|
function removeItem() {
|
||||||
playQueueStore.queueReplaceLock = true
|
playQueueStore.queueReplaceLock = true
|
||||||
|
|
||||||
|
@ -152,8 +157,8 @@ function removeItem() {
|
||||||
<template>
|
<template>
|
||||||
<button class="p-4 w-full rounded-md hover:bg-white/5 first:mt-2 flex gap-2 items-center" @click="() => {
|
<button class="p-4 w-full rounded-md hover:bg-white/5 first:mt-2 flex gap-2 items-center" @click="() => {
|
||||||
if (isCurrent) { return }
|
if (isCurrent) { return }
|
||||||
playQueueStore.currentIndex = index
|
// playStore.currentIndex = index
|
||||||
playQueueStore.isPlaying = true
|
// playStore.isPlaying = true
|
||||||
}" @mouseenter="hover = true" @mouseleave="hover = false">
|
}" @mouseenter="hover = true" @mouseleave="hover = false">
|
||||||
<div class="flex gap-2 flex-auto w-0">
|
<div class="flex gap-2 flex-auto w-0">
|
||||||
<div class="relative w-12 h-12 rounded-md shadow-xl overflow-hidden">
|
<div class="relative w-12 h-12 rounded-md shadow-xl overflow-hidden">
|
||||||
|
@ -162,7 +167,7 @@ function removeItem() {
|
||||||
v-if="isCurrent">
|
v-if="isCurrent">
|
||||||
<!-- 在支持的浏览器上显示可视化,否则显示音波图标 -->
|
<!-- 在支持的浏览器上显示可视化,否则显示音波图标 -->
|
||||||
<div v-if="isAudioVisualizationSupported" style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
|
<div v-if="isAudioVisualizationSupported" style="height: 1rem;" class="flex justify-center items-center gap-[.125rem]">
|
||||||
<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
|
<div class="bg-white w-[.125rem] rounded-full" v-for="(bar, index) in playStore.visualizer"
|
||||||
:key="index" :style="{
|
:key="index" :style="{
|
||||||
height: `${Math.max(10, bar)}%`
|
height: `${Math.max(10, bar)}%`
|
||||||
}" />
|
}" />
|
||||||
|
@ -189,8 +194,8 @@ function removeItem() {
|
||||||
<button
|
<button
|
||||||
class="text-white/90 w-4 h-4 hover:scale-110 hover:text-white active:scale-95 active:text-white/85 transition-all"
|
class="text-white/90 w-4 h-4 hover:scale-110 hover:text-white active:scale-95 active:text-white/85 transition-all"
|
||||||
@click.stop="moveDown"
|
@click.stop="moveDown"
|
||||||
:disabled="index === (playQueueStore.playMode.shuffle ? playQueueStore.shuffleList.length : playQueueStore.list.length) - 1"
|
:disabled="index === (playStore.playMode.shuffle ? playStore.shuffleList.length : playStore.list.length) - 1"
|
||||||
v-if="index !== (playQueueStore.playMode.shuffle ? playQueueStore.shuffleList.length : playQueueStore.list.length) - 1">
|
v-if="index !== (playStore.playMode.shuffle ? playStore.shuffleList.length : playStore.list.length) - 1">
|
||||||
<DownHyphenIcon :size="4" />
|
<DownHyphenIcon :size="4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
@ -1,535 +0,0 @@
|
||||||
<!-- Player.vue - 添加预加载功能 -->
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, nextTick, ref, 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 PlayIcon from '../assets/icons/play.vue'
|
|
||||||
import PauseIcon from '../assets/icons/pause.vue'
|
|
||||||
import { audioVisualizer, checkAndRefreshSongResource, supportsWebAudioVisualization } from '../utils'
|
|
||||||
|
|
||||||
const playQueueStore = usePlayQueueStore()
|
|
||||||
const favourites = useFavourites()
|
|
||||||
const route = useRoute()
|
|
||||||
const player = useTemplateRef('playerRef')
|
|
||||||
|
|
||||||
// [调试] 检查 store 方法类型
|
|
||||||
console.log('[Player] 检查 store 方法:', {
|
|
||||||
preloadNext: typeof playQueueStore.preloadNext,
|
|
||||||
getPreloadedAudio: typeof playQueueStore.getPreloadedAudio,
|
|
||||||
clearPreloadedAudio: typeof playQueueStore.clearPreloadedAudio,
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取当前歌曲的计算属性
|
|
||||||
const currentTrack = computed(() => {
|
|
||||||
if (
|
|
||||||
playQueueStore.playMode.shuffle &&
|
|
||||||
playQueueStore.shuffleList.length > 0
|
|
||||||
) {
|
|
||||||
return playQueueStore.list[
|
|
||||||
playQueueStore.shuffleList[playQueueStore.currentIndex]
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
return playQueueStore.list[playQueueStore.currentIndex]
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取当前歌曲的音频源
|
|
||||||
const currentAudioSrc = computed(() => {
|
|
||||||
const track = currentTrack.value
|
|
||||||
return track ? track.song.sourceUrl : ''
|
|
||||||
})
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => playQueueStore.isPlaying,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue) {
|
|
||||||
player.value?.play()
|
|
||||||
setMetadata()
|
|
||||||
} else {
|
|
||||||
player.value?.pause()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听当前索引变化,处理预加载逻辑
|
|
||||||
watch(
|
|
||||||
() => playQueueStore.currentIndex,
|
|
||||||
async () => {
|
|
||||||
console.log('[Player] 当前索引变化:', playQueueStore.currentIndex)
|
|
||||||
|
|
||||||
// 检查是否可以使用预加载的音频
|
|
||||||
const track = currentTrack.value
|
|
||||||
if (track) {
|
|
||||||
const songId = track.song.cid
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 首先检查和刷新当前歌曲的资源
|
|
||||||
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)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 使用更新后的歌曲信息
|
|
||||||
const preloadedAudio = playQueueStore.getPreloadedAudio(songId)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
// 清理使用过的预加载音频
|
|
||||||
playQueueStore.clearPreloadedAudio(songId)
|
|
||||||
|
|
||||||
// 如果正在播放状态,立即播放
|
|
||||||
if (playQueueStore.isPlaying) {
|
|
||||||
await nextTick()
|
|
||||||
player.value.play().catch(console.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
playQueueStore.isBuffering = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log(`[Player] 正常加载音频: ${track.song.name}`)
|
|
||||||
playQueueStore.isBuffering = true
|
|
||||||
|
|
||||||
// 如果资源地址已更新,清除旧的预加载音频
|
|
||||||
if (updatedSong.sourceUrl !== track.song.sourceUrl) {
|
|
||||||
playQueueStore.clearPreloadedAudio(songId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Player] 处理预加载音频时出错:', error)
|
|
||||||
playQueueStore.isBuffering = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setMetadata()
|
|
||||||
|
|
||||||
// 延迟预加载下一首歌,避免影响当前歌曲加载
|
|
||||||
setTimeout(async () => {
|
|
||||||
try {
|
|
||||||
console.log('[Player] 尝试预加载下一首歌')
|
|
||||||
|
|
||||||
// 检查函数是否存在
|
|
||||||
if (typeof playQueueStore.preloadNext === 'function') {
|
|
||||||
await playQueueStore.preloadNext()
|
|
||||||
|
|
||||||
// 预加载完成后,检查播放队列是否有更新,同步到收藏夹
|
|
||||||
playQueueStore.list.forEach((item) => {
|
|
||||||
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)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
function artistsOrganize(list: string[]) {
|
|
||||||
if (list.length === 0) {
|
|
||||||
return '未知音乐人'
|
|
||||||
}
|
|
||||||
return list
|
|
||||||
.map((artist) => {
|
|
||||||
return artist
|
|
||||||
})
|
|
||||||
.join(' / ')
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMetadata() {
|
|
||||||
if ('mediaSession' in navigator) {
|
|
||||||
const current = currentTrack.value
|
|
||||||
if (!current) return
|
|
||||||
|
|
||||||
navigator.mediaSession.metadata = new MediaMetadata({
|
|
||||||
title: current.song.name,
|
|
||||||
artist: artistsOrganize(current.song.artists ?? []),
|
|
||||||
album: current.album?.name,
|
|
||||||
artwork: [
|
|
||||||
{
|
|
||||||
src: current.album?.coverUrl ?? '',
|
|
||||||
sizes: '500x500',
|
|
||||||
type: 'image/png',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
|
|
||||||
navigator.mediaSession.setActionHandler('previoustrack', playPrevious)
|
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', playNext)
|
|
||||||
|
|
||||||
playQueueStore.duration = player.value?.duration ?? 0
|
|
||||||
playQueueStore.currentTime = player.value?.currentTime ?? 0
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(
|
|
||||||
() => playQueueStore.updatedCurrentTime,
|
|
||||||
(newValue) => {
|
|
||||||
if (newValue === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (player.value) player.value.currentTime = newValue
|
|
||||||
playQueueStore.updatedCurrentTime = null
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function playNext() {
|
|
||||||
if (playQueueStore.currentIndex === playQueueStore.list.length - 1) {
|
|
||||||
console.log('at the bottom, pause')
|
|
||||||
playQueueStore.currentIndex = 0
|
|
||||||
if (playQueueStore.playMode.repeat === 'all') {
|
|
||||||
playQueueStore.currentIndex = 0
|
|
||||||
playQueueStore.isPlaying = true
|
|
||||||
} else {
|
|
||||||
player.value?.pause()
|
|
||||||
playQueueStore.isPlaying = false
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
playQueueStore.currentIndex++
|
|
||||||
playQueueStore.isPlaying = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function playPrevious() {
|
|
||||||
if (
|
|
||||||
player.value &&
|
|
||||||
(player.value.currentTime ?? 0) < 5 &&
|
|
||||||
playQueueStore.currentIndex > 0
|
|
||||||
) {
|
|
||||||
playQueueStore.currentIndex--
|
|
||||||
playQueueStore.isPlaying = true
|
|
||||||
} else {
|
|
||||||
if (player.value) {
|
|
||||||
player.value.currentTime = 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateCurrentTime() {
|
|
||||||
playQueueStore.currentTime = player.value?.currentTime ?? 0
|
|
||||||
|
|
||||||
// 智能预加载策略:支持动态配置
|
|
||||||
if (playQueueStore.duration > 0) {
|
|
||||||
const progress = playQueueStore.currentTime / playQueueStore.duration
|
|
||||||
const remainingTime = playQueueStore.duration - playQueueStore.currentTime
|
|
||||||
|
|
||||||
// 从 localStorage 获取配置,如果没有则使用默认值
|
|
||||||
const config = JSON.parse(localStorage.getItem('preloadConfig') || '{}')
|
|
||||||
const preloadTrigger = (config.preloadTrigger || 50) / 100 // 转换为小数
|
|
||||||
const remainingTimeThreshold = config.remainingTimeThreshold || 30
|
|
||||||
|
|
||||||
if (
|
|
||||||
(progress > preloadTrigger || remainingTime < remainingTimeThreshold) &&
|
|
||||||
!playQueueStore.isPreloading
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
if (typeof playQueueStore.preloadNext === 'function') {
|
|
||||||
playQueueStore.preloadNext()
|
|
||||||
} else {
|
|
||||||
console.error('[Player] preloadNext 不是一个函数')
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Player] 智能预加载失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查浏览器是否支持音频可视化
|
|
||||||
const isAudioVisualizationSupported = supportsWebAudioVisualization()
|
|
||||||
console.log('[Player] 音频可视化支持状态:', isAudioVisualizationSupported)
|
|
||||||
|
|
||||||
// 只在支持的浏览器上初始化音频可视化
|
|
||||||
let barHeights = ref<number[]>([0, 0, 0, 0, 0, 0])
|
|
||||||
let connectAudio = (_audio: HTMLAudioElement) => {}
|
|
||||||
let isAnalyzing = ref(false)
|
|
||||||
let error = ref<string | null>(null)
|
|
||||||
|
|
||||||
if (isAudioVisualizationSupported) {
|
|
||||||
console.log('[Player] 初始化 audioVisualizer')
|
|
||||||
const visualizer = audioVisualizer({
|
|
||||||
sensitivity: 1.5,
|
|
||||||
barCount: 6,
|
|
||||||
maxDecibels: -10,
|
|
||||||
bassBoost: 0.8,
|
|
||||||
midBoost: 1.2,
|
|
||||||
trebleBoost: 1.4,
|
|
||||||
threshold: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
barHeights = visualizer.barHeights
|
|
||||||
connectAudio = visualizer.connectAudio
|
|
||||||
isAnalyzing = visualizer.isAnalyzing
|
|
||||||
error = visualizer.error
|
|
||||||
|
|
||||||
console.log('[Player] audioVisualizer 返回值:', {
|
|
||||||
barHeights: barHeights.value,
|
|
||||||
isAnalyzing: isAnalyzing.value,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.log('[Player] 音频可视化被禁用(Safari 或不支持的浏览器)')
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听播放列表变化
|
|
||||||
watch(
|
|
||||||
() => playQueueStore.list.length,
|
|
||||||
async (newLength) => {
|
|
||||||
console.log('[Player] 播放列表长度变化:', newLength)
|
|
||||||
if (newLength === 0) {
|
|
||||||
console.log('[Player] 播放列表为空,跳过连接')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 等待下一帧,确保 audio 元素已经渲染
|
|
||||||
await nextTick()
|
|
||||||
|
|
||||||
if (player.value) {
|
|
||||||
if (isAudioVisualizationSupported) {
|
|
||||||
console.log('[Player] 连接音频元素到可视化器')
|
|
||||||
console.log('[Player] 音频元素状态:', {
|
|
||||||
src: player.value.src?.substring(0, 50) + '...',
|
|
||||||
readyState: player.value.readyState,
|
|
||||||
paused: player.value.paused,
|
|
||||||
})
|
|
||||||
connectAudio(player.value)
|
|
||||||
} else {
|
|
||||||
console.log('[Player] 跳过音频可视化连接(不支持的浏览器)')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('[Player] ❌ 音频元素不存在')
|
|
||||||
}
|
|
||||||
|
|
||||||
playQueueStore.visualizer = barHeights.value
|
|
||||||
|
|
||||||
// 开始预加载第一首歌的下一首
|
|
||||||
setTimeout(() => {
|
|
||||||
playQueueStore.preloadNext()
|
|
||||||
}, 2000)
|
|
||||||
|
|
||||||
// 初始化音量
|
|
||||||
if (player.value) {
|
|
||||||
initializeVolume()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听音频元素变化
|
|
||||||
watch(
|
|
||||||
() => player.value,
|
|
||||||
(audioElement) => {
|
|
||||||
if (audioElement && playQueueStore.list.length > 0 && isAudioVisualizationSupported) {
|
|
||||||
connectAudio(audioElement)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听可视化器数据变化
|
|
||||||
watch(
|
|
||||||
() => barHeights.value,
|
|
||||||
(newHeights) => {
|
|
||||||
playQueueStore.visualizer = newHeights
|
|
||||||
},
|
|
||||||
{ deep: true },
|
|
||||||
)
|
|
||||||
|
|
||||||
// 监听错误
|
|
||||||
watch(
|
|
||||||
() => error.value,
|
|
||||||
(newError) => {
|
|
||||||
if (newError) {
|
|
||||||
console.error('[Player] 可视化器错误:', newError)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// 切换播放模式
|
|
||||||
watch(
|
|
||||||
() => playQueueStore.playMode.shuffle,
|
|
||||||
(isShuffle) => {
|
|
||||||
if (isShuffle) {
|
|
||||||
const currentIndex = playQueueStore.currentIndex
|
|
||||||
const trackCount = playQueueStore.list.length
|
|
||||||
|
|
||||||
// 1. 已播放部分:不变
|
|
||||||
let shuffledList = [...Array(currentIndex).keys()]
|
|
||||||
|
|
||||||
// 2. 构建待打乱的列表
|
|
||||||
const shuffleSpace = [...Array(trackCount).keys()].filter((index) =>
|
|
||||||
playQueueStore.shuffleCurrent
|
|
||||||
? index >= currentIndex
|
|
||||||
: index > currentIndex,
|
|
||||||
)
|
|
||||||
|
|
||||||
// 3. 随机打乱
|
|
||||||
shuffleSpace.sort(() => Math.random() - 0.5)
|
|
||||||
|
|
||||||
// 4. 如果当前曲目不参与打乱,插入回当前位置(即 currentIndex 处)
|
|
||||||
if (!playQueueStore.shuffleCurrent) {
|
|
||||||
shuffledList.push(currentIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 拼接:已播放部分 + 当前(可选)+ 打乱后的剩余部分
|
|
||||||
shuffledList = shuffledList.concat(shuffleSpace)
|
|
||||||
|
|
||||||
// 6. 应用 shuffleList
|
|
||||||
playQueueStore.shuffleList = shuffledList
|
|
||||||
|
|
||||||
// 清除 shuffleCurrent 状态
|
|
||||||
playQueueStore.shuffleCurrent = undefined
|
|
||||||
} else {
|
|
||||||
// 退出随机播放:恢复当前播放曲目的原索引
|
|
||||||
playQueueStore.currentIndex =
|
|
||||||
playQueueStore.shuffleList[playQueueStore.currentIndex]
|
|
||||||
}
|
|
||||||
|
|
||||||
// 切换播放模式后重新预加载
|
|
||||||
setTimeout(() => {
|
|
||||||
playQueueStore.clearAllPreloadedAudio()
|
|
||||||
playQueueStore.preloadNext()
|
|
||||||
}, 500)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
function getCurrentTrack() {
|
|
||||||
return currentTrack.value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化音量
|
|
||||||
function initializeVolume() {
|
|
||||||
if (player.value) {
|
|
||||||
const savedVolume = localStorage.getItem('audioVolume')
|
|
||||||
if (savedVolume) {
|
|
||||||
const volumeValue = Number.parseFloat(savedVolume)
|
|
||||||
player.value.volume = volumeValue
|
|
||||||
console.log('[Player] 初始化音量:', volumeValue)
|
|
||||||
} else {
|
|
||||||
// 设置默认音量
|
|
||||||
player.value.volume = 1
|
|
||||||
localStorage.setItem('audioVolume', '1')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听音量变化事件
|
|
||||||
function handleVolumeChange(event: Event) {
|
|
||||||
const target = event.target as HTMLAudioElement
|
|
||||||
if (target) {
|
|
||||||
// 保存音量变化到localStorage
|
|
||||||
localStorage.setItem('audioVolume', target.volume.toString())
|
|
||||||
console.log('[Player] 音量变化:', target.volume)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听localStorage中音量的变化,同步到音频元素
|
|
||||||
function syncVolumeFromStorage() {
|
|
||||||
if (player.value) {
|
|
||||||
const savedVolume = localStorage.getItem('audioVolume')
|
|
||||||
if (savedVolume) {
|
|
||||||
const volumeValue = Number.parseFloat(savedVolume)
|
|
||||||
if (player.value.volume !== volumeValue) {
|
|
||||||
player.value.volume = volumeValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 定期检查音量同步(可选,或者使用storage事件)
|
|
||||||
setInterval(syncVolumeFromStorage, 100)
|
|
||||||
|
|
||||||
// 组件卸载时清理预加载
|
|
||||||
// onUnmounted(() => {
|
|
||||||
// playQueueStore.clearAllPreloadedAudio()
|
|
||||||
// })
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<audio :src="currentAudioSrc" ref="playerRef" :autoplay="playQueueStore.isPlaying"
|
|
||||||
v-if="playQueueStore.list.length !== 0" @volumechange="handleVolumeChange" @ended="() => {
|
|
||||||
if (playQueueStore.playMode.repeat === 'single') { playQueueStore.isPlaying = true }
|
|
||||||
else { playNext() }
|
|
||||||
}" @pause="playQueueStore.isPlaying = false" @play="playQueueStore.isPlaying = true" @playing="() => {
|
|
||||||
console.log('[Player] 音频开始播放事件')
|
|
||||||
playQueueStore.isBuffering = false
|
|
||||||
setMetadata()
|
|
||||||
initializeVolume()
|
|
||||||
}" @waiting="playQueueStore.isBuffering = true" @loadeddata="() => {
|
|
||||||
console.log('[Player] 音频数据加载完成')
|
|
||||||
playQueueStore.isBuffering = false
|
|
||||||
initializeVolume()
|
|
||||||
}" @canplay="() => {
|
|
||||||
console.log('[Player] 音频可以播放')
|
|
||||||
playQueueStore.isBuffering = false
|
|
||||||
}" @error="(e) => {
|
|
||||||
console.error('[Player] 音频错误:', e)
|
|
||||||
playQueueStore.isBuffering = false
|
|
||||||
}" crossorigin="anonymous" @timeupdate="updateCurrentTime">
|
|
||||||
</audio>
|
|
||||||
|
|
||||||
<!-- 预加载进度指示器(可选显示) -->
|
|
||||||
<!-- <div v-if="playQueueStore.isPreloading"
|
|
||||||
class="fixed top-4 right-4 bg-black/80 text-white px-3 py-1 rounded text-xs z-50">
|
|
||||||
预加载中... {{ Math.round(playQueueStore.preloadProgress) }}%
|
|
||||||
</div> -->
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="text-white h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex gap-2 overflow-hidden select-none"
|
|
||||||
v-if="playQueueStore.list.length !== 0 && route.path !== '/playroom'">
|
|
||||||
<RouterLink to="/playroom">
|
|
||||||
<img :src="getCurrentTrack()?.album?.coverUrl ?? ''" class="rounded-full h-8 w-8 mt-[.0625rem]" />
|
|
||||||
</RouterLink>
|
|
||||||
|
|
||||||
<RouterLink to="/playroom">
|
|
||||||
<div class="flex items-center w-32 h-9">
|
|
||||||
<span class="truncate text-xs">{{ getCurrentTrack()?.song.name }}</span>
|
|
||||||
</div>
|
|
||||||
</RouterLink>
|
|
||||||
|
|
||||||
<button class="h-9 w-12 flex justify-center items-center" @click.stop="() => {
|
|
||||||
playQueueStore.isPlaying = !playQueueStore.isPlaying
|
|
||||||
}">
|
|
||||||
<div v-if="playQueueStore.isPlaying">
|
|
||||||
<LoadingIndicator v-if="playQueueStore.isBuffering === true" :size="4" />
|
|
||||||
<!-- 在支持的浏览器上显示可视化,否则显示暂停图标 -->
|
|
||||||
<div v-else-if="isAudioVisualizationSupported" class="h-4 flex justify-center items-center gap-[.125rem]">
|
|
||||||
<div class="bg-white/75 w-[.125rem] rounded-full" v-for="(bar, index) in playQueueStore.visualizer"
|
|
||||||
:key="index" :style="{
|
|
||||||
height: `${Math.max(10, bar)}%`
|
|
||||||
}" />
|
|
||||||
</div>
|
|
||||||
<PauseIcon v-else :size="4" />
|
|
||||||
</div>
|
|
||||||
<PlayIcon v-else :size="4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -89,7 +89,7 @@
|
||||||
import { onMounted, ref, watch, nextTick, computed, onUnmounted } from 'vue'
|
import { onMounted, ref, watch, nextTick, computed, onUnmounted } from 'vue'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import gsap from 'gsap'
|
import gsap from 'gsap'
|
||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
import { usePlayStore } from '../stores/usePlayStore'
|
||||||
|
|
||||||
// 类型定义
|
// 类型定义
|
||||||
interface LyricsLine {
|
interface LyricsLine {
|
||||||
|
@ -106,7 +106,7 @@ interface GapLine {
|
||||||
duration?: number
|
duration?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const playQueueStore = usePlayQueueStore()
|
const playStore = usePlayStore()
|
||||||
|
|
||||||
// 响应式数据
|
// 响应式数据
|
||||||
const parsedLyrics = ref<(LyricsLine | GapLine)[]>([])
|
const parsedLyrics = ref<(LyricsLine | GapLine)[]>([])
|
||||||
|
@ -121,7 +121,7 @@ const lyricsWrapper = ref<HTMLElement>()
|
||||||
const lineRefs = ref<(HTMLElement | null)[]>([])
|
const lineRefs = ref<(HTMLElement | null)[]>([])
|
||||||
const controlPanel = ref<HTMLElement>()
|
const controlPanel = ref<HTMLElement>()
|
||||||
const loadingIndicator = ref<HTMLElement>()
|
const loadingIndicator = ref<HTMLElement>()
|
||||||
const noLyricsIndicator = ref<HTMLElement>()
|
//const noLyricsIndicator = ref<HTMLElement>()
|
||||||
|
|
||||||
// GSAP 动画实例
|
// GSAP 动画实例
|
||||||
let scrollTween: gsap.core.Tween | null = null
|
let scrollTween: gsap.core.Tween | null = null
|
||||||
|
@ -135,12 +135,13 @@ const props = defineProps<{
|
||||||
|
|
||||||
// 滚动指示器相关计算
|
// 滚动指示器相关计算
|
||||||
const scrollIndicatorHeight = computed(() => {
|
const scrollIndicatorHeight = computed(() => {
|
||||||
if (parsedLyrics.value.length === 0) return 0
|
if (parsedLyrics.value.length === 0) {return 0}
|
||||||
return Math.max(10, 100 / parsedLyrics.value.length * 5) // 显示大约5行的比例
|
return Math.max(10, 100 / parsedLyrics.value.length * 5) // 显示大约5行的比例
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
const scrollIndicatorPosition = computed(() => {
|
const scrollIndicatorPosition = computed(() => {
|
||||||
if (parsedLyrics.value.length === 0 || currentLineIndex.value < 0) return 0
|
if (parsedLyrics.value.length === 0 || currentLineIndex.value < 0) {return 0}
|
||||||
const progress = currentLineIndex.value / (parsedLyrics.value.length - 1)
|
const progress = currentLineIndex.value / (parsedLyrics.value.length - 1)
|
||||||
const containerHeight = lyricsContainer.value?.clientHeight || 400
|
const containerHeight = lyricsContainer.value?.clientHeight || 400
|
||||||
const indicatorTrackHeight = containerHeight / 2 // 指示器轨道高度
|
const indicatorTrackHeight = containerHeight / 2 // 指示器轨道高度
|
||||||
|
@ -148,6 +149,7 @@ const scrollIndicatorPosition = computed(() => {
|
||||||
})
|
})
|
||||||
|
|
||||||
// 设置行引用
|
// 设置行引用
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
function setLineRef(el: HTMLElement | null, index: number) {
|
function setLineRef(el: HTMLElement | null, index: number) {
|
||||||
if (el) {
|
if (el) {
|
||||||
lineRefs.value[index] = el
|
lineRefs.value[index] = el
|
||||||
|
@ -155,15 +157,17 @@ function setLineRef(el: HTMLElement | null, index: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 歌词解析函数
|
// 歌词解析函数
|
||||||
function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine | GapLine)[] {
|
function parseLyrics(lrcText: string, minGapDuration = 5): (LyricsLine | GapLine)[] {
|
||||||
if (!lrcText) return [
|
if (!lrcText) {
|
||||||
{
|
return [
|
||||||
type: 'lyric',
|
{
|
||||||
time: 0,
|
type: 'lyric',
|
||||||
text: '',
|
time: 0,
|
||||||
originalTime: '[00:00]'
|
text: '',
|
||||||
}
|
originalTime: '[00:00]'
|
||||||
]
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
const lines = lrcText.split('\n')
|
const lines = lrcText.split('\n')
|
||||||
const tempParsedLines: (LyricsLine | GapLine)[] = []
|
const tempParsedLines: (LyricsLine | GapLine)[] = []
|
||||||
|
@ -172,14 +176,14 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const matches = [...line.matchAll(timeRegex)]
|
const matches = [...line.matchAll(timeRegex)]
|
||||||
if (matches.length === 0) continue
|
if (matches.length === 0) {continue}
|
||||||
|
|
||||||
const text = line.replace(/\[\d{1,2}:\d{2}(?:\.\d{1,3})?\]/g, '').trim()
|
const text = line.replace(/\[\d{1,2}:\d{2}(?:\.\d{1,3})?\]/g, '').trim()
|
||||||
|
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const minutes = parseInt(match[1])
|
const minutes = Number.parseInt(match[1])
|
||||||
const seconds = parseInt(match[2])
|
const seconds = Number.parseInt(match[2])
|
||||||
const milliseconds = match[3] ? parseInt(match[3].padEnd(3, '0')) : 0
|
const milliseconds = match[3] ? Number.parseInt(match[3].padEnd(3, '0')) : 0
|
||||||
|
|
||||||
const totalSeconds = minutes * 60 + seconds + milliseconds / 1000
|
const totalSeconds = minutes * 60 + seconds + milliseconds / 1000
|
||||||
|
|
||||||
|
@ -206,7 +210,7 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
|
||||||
const lyricLines = tempParsedLines.filter(line => line.type === 'lyric') as LyricsLine[]
|
const lyricLines = tempParsedLines.filter(line => line.type === 'lyric') as LyricsLine[]
|
||||||
const gapLines = tempParsedLines.filter(line => line.type === 'gap') as GapLine[]
|
const gapLines = tempParsedLines.filter(line => line.type === 'gap') as GapLine[]
|
||||||
|
|
||||||
if (lyricLines.length === 0) return tempParsedLines
|
if (lyricLines.length === 0) {return tempParsedLines}
|
||||||
|
|
||||||
for (let i = 0; i < gapLines.length; i++) {
|
for (let i = 0; i < gapLines.length; i++) {
|
||||||
const gapLine = gapLines[i]
|
const gapLine = gapLines[i]
|
||||||
|
@ -236,9 +240,9 @@ function parseLyrics(lrcText: string, minGapDuration: number = 5): (LyricsLine |
|
||||||
|
|
||||||
// 查找当前行索引
|
// 查找当前行索引
|
||||||
function findCurrentLineIndex(time: number): number {
|
function findCurrentLineIndex(time: number): number {
|
||||||
if (parsedLyrics.value.length === 0) return -1
|
if (parsedLyrics.value.length === 0) {return -1}
|
||||||
// 如果时间小于第一句歌词,则返回0(空行)
|
// 如果时间小于第一句歌词,则返回0(空行)
|
||||||
if (time < parsedLyrics.value[1]?.time) return 0
|
if (time < parsedLyrics.value[1]?.time) {return 0}
|
||||||
let index = 0
|
let index = 0
|
||||||
for (let i = 1; i < parsedLyrics.value.length; i++) {
|
for (let i = 1; i < parsedLyrics.value.length; i++) {
|
||||||
if (time >= parsedLyrics.value[i].time) {
|
if (time >= parsedLyrics.value[i].time) {
|
||||||
|
@ -252,7 +256,7 @@ function findCurrentLineIndex(time: number): number {
|
||||||
|
|
||||||
// 使用 GSAP 滚动到指定行
|
// 使用 GSAP 滚动到指定行
|
||||||
function scrollToLine(lineIndex: number, smooth = true) {
|
function scrollToLine(lineIndex: number, smooth = true) {
|
||||||
if (!lyricsContainer.value || !lyricsWrapper.value || !lineRefs.value[lineIndex]) return
|
if (!lyricsContainer.value || !lyricsWrapper.value || !lineRefs.value[lineIndex]) {return}
|
||||||
|
|
||||||
const container = lyricsContainer.value
|
const container = lyricsContainer.value
|
||||||
const wrapper = lyricsWrapper.value
|
const wrapper = lyricsWrapper.value
|
||||||
|
@ -288,7 +292,7 @@ function scrollToLine(lineIndex: number, smooth = true) {
|
||||||
|
|
||||||
// 高亮当前行动画
|
// 高亮当前行动画
|
||||||
function highlightCurrentLine(lineIndex: number) {
|
function highlightCurrentLine(lineIndex: number) {
|
||||||
if (!lineRefs.value[lineIndex]) return
|
if (!lineRefs.value[lineIndex]) {return}
|
||||||
|
|
||||||
const lineElement = lineRefs.value[lineIndex]
|
const lineElement = lineRefs.value[lineIndex]
|
||||||
|
|
||||||
|
@ -322,10 +326,11 @@ function highlightCurrentLine(lineIndex: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理鼠标滚轮
|
// 处理鼠标滚轮
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
function handleWheel(event: WheelEvent) {
|
function handleWheel(event: WheelEvent) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
|
|
||||||
if (!lyricsWrapper.value || !lyricsContainer.value) return
|
if (!lyricsWrapper.value || !lyricsContainer.value) {return}
|
||||||
|
|
||||||
userScrolling.value = true
|
userScrolling.value = true
|
||||||
autoScroll.value = false
|
autoScroll.value = false
|
||||||
|
@ -365,6 +370,7 @@ function handleWheel(event: WheelEvent) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理歌词行点击
|
// 处理歌词行点击
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
function handleLineClick(line: LyricsLine | GapLine, index: number) {
|
function handleLineClick(line: LyricsLine | GapLine, index: number) {
|
||||||
if (line.type === 'lyric') {
|
if (line.type === 'lyric') {
|
||||||
console.log('Jump to time:', line.time)
|
console.log('Jump to time:', line.time)
|
||||||
|
@ -418,11 +424,11 @@ function toggleAutoScroll() {
|
||||||
|
|
||||||
// 重置滚动
|
// 重置滚动
|
||||||
function resetScroll() {
|
function resetScroll() {
|
||||||
if (!lyricsWrapper.value) return
|
if (!lyricsWrapper.value) {return}
|
||||||
|
|
||||||
// 停止所有动画
|
// 停止所有动画
|
||||||
if (scrollTween) scrollTween.kill()
|
if (scrollTween) {scrollTween.kill()}
|
||||||
if (highlightTween) highlightTween.kill()
|
if (highlightTween) {highlightTween.kill()}
|
||||||
|
|
||||||
// 重置位置
|
// 重置位置
|
||||||
gsap.to(lyricsWrapper.value, {
|
gsap.to(lyricsWrapper.value, {
|
||||||
|
@ -456,12 +462,13 @@ function resetScroll() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// gap 圆点透明度计算
|
// gap 圆点透明度计算
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
function getGapDotOpacities(line: GapLine) {
|
function getGapDotOpacities(line: GapLine) {
|
||||||
// 获取 gap 的持续时间
|
// 获取 gap 的持续时间
|
||||||
const duration = line.duration ?? 0
|
const duration = line.duration ?? 0
|
||||||
if (duration <= 0) return [0.3, 0.3, 0.3]
|
if (duration <= 0) {return [0.3, 0.3, 0.3]}
|
||||||
// 当前播放时间
|
// 当前播放时间
|
||||||
const now = playQueueStore.currentTime
|
const now = playStore.progress.currentTime
|
||||||
// gap 起止时间
|
// gap 起止时间
|
||||||
const start = line.time
|
const start = line.time
|
||||||
// 计算进度
|
// 计算进度
|
||||||
|
@ -474,7 +481,7 @@ function getGapDotOpacities(line: GapLine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听播放时间变化
|
// 监听播放时间变化
|
||||||
watch(() => playQueueStore.currentTime, (time) => {
|
watch(() => playStore.progress.currentTime, (time) => {
|
||||||
const newIndex = findCurrentLineIndex(time)
|
const newIndex = findCurrentLineIndex(time)
|
||||||
|
|
||||||
if (newIndex !== currentLineIndex.value && newIndex >= 0) {
|
if (newIndex !== currentLineIndex.value && newIndex >= 0) {
|
||||||
|
@ -500,8 +507,8 @@ watch(() => props.lrcSrc, async (newSrc) => {
|
||||||
lineRefs.value = []
|
lineRefs.value = []
|
||||||
|
|
||||||
// 停止所有动画
|
// 停止所有动画
|
||||||
if (scrollTween) scrollTween.kill()
|
if (scrollTween) {scrollTween.kill()}
|
||||||
if (highlightTween) highlightTween.kill()
|
if (highlightTween) {highlightTween.kill()}
|
||||||
|
|
||||||
if (newSrc) {
|
if (newSrc) {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
|
@ -552,12 +559,12 @@ function setupPageFocusHandlers() {
|
||||||
handleVisibilityChange = () => {
|
handleVisibilityChange = () => {
|
||||||
if (document.hidden) {
|
if (document.hidden) {
|
||||||
// 页面失去焦点时暂停动画
|
// 页面失去焦点时暂停动画
|
||||||
if (scrollTween) scrollTween.pause()
|
if (scrollTween) {scrollTween.pause()}
|
||||||
if (highlightTween) highlightTween.pause()
|
if (highlightTween) {highlightTween.pause()}
|
||||||
} else {
|
} else {
|
||||||
// 页面重新获得焦点时恢复并重新同步
|
// 页面重新获得焦点时恢复并重新同步
|
||||||
if (scrollTween && scrollTween.paused()) scrollTween.resume()
|
if (scrollTween?.paused()) {scrollTween.resume()}
|
||||||
if (highlightTween && highlightTween.paused()) highlightTween.resume()
|
if (highlightTween?.paused()) {highlightTween.resume()}
|
||||||
|
|
||||||
// 重新同步歌词位置
|
// 重新同步歌词位置
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
|
@ -605,9 +612,9 @@ onMounted(() => {
|
||||||
|
|
||||||
// 组件卸载时清理
|
// 组件卸载时清理
|
||||||
onUnmounted(() => {
|
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) {
|
if (handleVisibilityChange) {
|
||||||
|
|
|
@ -1,8 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { artistsOrganize } from '../utils'
|
import { artistsOrganize } from '../utils'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
import { usePlayStore } from '../stores/usePlayStore'
|
||||||
import { useToast } from 'vue-toast-notification'
|
|
||||||
import { useFavourites } from '../stores/useFavourites'
|
import { useFavourites } from '../stores/useFavourites'
|
||||||
|
|
||||||
import QueueAddIcon from '../assets/icons/queueadd.vue'
|
import QueueAddIcon from '../assets/icons/queueadd.vue'
|
||||||
|
@ -16,24 +15,18 @@ const props = defineProps<{
|
||||||
playfrom: (index: number) => void,
|
playfrom: (index: number) => void,
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
const hover = ref(false)
|
const hover = ref(false)
|
||||||
|
|
||||||
const playQueueStore = usePlayQueueStore()
|
const playStore = usePlayStore()
|
||||||
const toast = useToast()
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
const favourites = useFavourites()
|
const favourites = useFavourites()
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
function appendToQueue() {
|
function appendToQueue() {
|
||||||
console.log('aaa')
|
playStore.appendItem({
|
||||||
let queue = playQueueStore.list
|
|
||||||
queue.push({
|
|
||||||
song: props.track,
|
song: props.track,
|
||||||
album: props.album,
|
album: props.album,
|
||||||
} as QueueItem)
|
|
||||||
playQueueStore.list = queue
|
|
||||||
playQueueStore.queueReplaceLock = true
|
|
||||||
toast.success('已添加到播放队列末尾', {
|
|
||||||
position: 'top-right',
|
|
||||||
duration: 1000,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -7,13 +7,11 @@ import 'vue-toast-notification/dist/theme-default.css'
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import HomePage from './pages/Home.vue'
|
import HomePage from './pages/Home.vue'
|
||||||
import AlbumDetailView from './pages/AlbumDetail.vue'
|
|
||||||
import Playroom from './pages/Playroom.vue'
|
import Playroom from './pages/Playroom.vue'
|
||||||
import Library from './pages/Library.vue'
|
import Library from './pages/Library.vue'
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{ path: '/', component: HomePage },
|
{ path: '/', component: HomePage },
|
||||||
{ path: '/albums/:albumId', component: AlbumDetailView },
|
|
||||||
{ path: '/playroom', component: Playroom },
|
{ path: '/playroom', component: Playroom },
|
||||||
{ path: '/library', component: Library }
|
{ path: '/library', component: Library }
|
||||||
]
|
]
|
||||||
|
|
|
@ -1,107 +0,0 @@
|
||||||
<script setup lang="ts">
|
|
||||||
import { ref, onMounted } from 'vue'
|
|
||||||
import apis from '../apis'
|
|
||||||
import { useRoute } from 'vue-router'
|
|
||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
|
||||||
import { artistsOrganize } from '../utils'
|
|
||||||
import TrackItem from '../components/TrackItem.vue'
|
|
||||||
|
|
||||||
import PlayIcon from '../assets/icons/play.vue'
|
|
||||||
import StarEmptyIcon from '../assets/icons/starempty.vue'
|
|
||||||
import ShuffleIcon from '../assets/icons/shuffle.vue'
|
|
||||||
|
|
||||||
const album = ref<Album>()
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const albumId = route.params.albumId
|
|
||||||
|
|
||||||
const playQueue = usePlayQueueStore()
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
try {
|
|
||||||
let res = await apis.getAlbum(albumId as string)
|
|
||||||
for (const track in res.songs) {
|
|
||||||
res.songs[parseInt(track)] = await apis.getSong(res.songs[parseInt(track)].cid)
|
|
||||||
}
|
|
||||||
album.value = res
|
|
||||||
console.log(res)
|
|
||||||
} catch (error) {
|
|
||||||
console.log(error)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
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.playMode.shuffle = false
|
|
||||||
playQueue.list = newPlayQueue
|
|
||||||
playQueue.currentIndex = from
|
|
||||||
playQueue.isPlaying = true
|
|
||||||
playQueue.isBuffering = true
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="px-4 md:px-8 flex gap-8 flex-col md:flex-row select-none mt-[6.625rem]">
|
|
||||||
<div class="mx-auto md:mx-0 md:w-72">
|
|
||||||
<div class="md:sticky md:top-[6.625rem] flex flex-col gap-8">
|
|
||||||
<div
|
|
||||||
class="border border-[#5b5b5b] rounded-md overflow-hidden shadow-2xl bg-neutral-800 sticky w-48 mx-auto md:w-72">
|
|
||||||
<img :src="album?.coverUrl" class="md:w-72 md:h-72 w-48 h-48 object-contain" />
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2 text-center md:text-left">
|
|
||||||
<div class="text-white text-2xl font-semibold">{{ album?.name }}</div>
|
|
||||||
<div class="text-sky-200 text-xl">{{ artistsOrganize(album?.artistes ?? []) }}</div>
|
|
||||||
<div class="text-white/50 text-sm">{{ album?.intro }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex-1 flex flex-col gap-8 mb-2">
|
|
||||||
<div class="flex justify-between items-center">
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<button
|
|
||||||
class="bg-sky-500/20 hover:bg-sky-500/30 active:bg-sky-600/30 active:shadow-inner border border-[#ffffff39] rounded-full w-56 h-10 text-base text-white flex justify-center items-center gap-2"
|
|
||||||
@click="playTheAlbum()">
|
|
||||||
<PlayIcon :size="4" />
|
|
||||||
<div>播放专辑</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="text-white w-10 h-10 bg-white/5 border border-[#ffffff39] rounded-full flex justify-center items-center"
|
|
||||||
@click="() => {
|
|
||||||
playTheAlbum()
|
|
||||||
playQueue.shuffleCurrent = true
|
|
||||||
playQueue.playMode.shuffle = true
|
|
||||||
}">
|
|
||||||
<ShuffleIcon :size="4" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="text-white w-10 h-10 bg-white/5 border border-[#ffffff39] rounded-full flex justify-center items-center">
|
|
||||||
<StarEmptyIcon :size="4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="text-sm text-gray-500 font-medium">
|
|
||||||
共 {{ album?.songs?.length ?? '?' }} 首曲目
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-2">
|
|
||||||
<TrackItem v-for="(track, index) in album?.songs" :key="track.cid" :album="album" :track="track" :index="index"
|
|
||||||
:playfrom="playTheAlbum" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
|
@ -6,21 +6,21 @@ import ShuffleIcon from '../assets/icons/shuffle.vue'
|
||||||
import { useFavourites } from '../stores/useFavourites'
|
import { useFavourites } from '../stores/useFavourites'
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import PlayListItem from '../components/PlayListItem.vue'
|
import PlayListItem from '../components/PlayListItem.vue'
|
||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
import { usePlayStore } from '../stores/usePlayStore'
|
||||||
|
|
||||||
const favourites = useFavourites()
|
const favourites = useFavourites()
|
||||||
const playQueueStore = usePlayQueueStore()
|
const playQueueStore = usePlayStore()
|
||||||
|
|
||||||
const currentList = ref<'favourites' | number>('favourites')
|
const currentList = ref<'favourites' | number>('favourites')
|
||||||
|
|
||||||
function playTheList(list: 'favourites' | number, playFrom: number = 0) {
|
function playTheList(list: 'favourites' | number, playFrom = 0) {
|
||||||
if (playFrom < 0 || playFrom >= favourites.favouritesCount) { playFrom = 0 }
|
let actualPlayFrom = playFrom
|
||||||
|
if (playFrom < 0 || playFrom >= favourites.favouritesCount) { actualPlayFrom = 0 }
|
||||||
|
|
||||||
if (usePlayQueueStore().queueReplaceLock) {
|
if (playQueueStore.queueReplaceLock) {
|
||||||
if (!confirm("当前操作会将你的播放队列清空、放入这张歌单所有曲目,并从头播放。继续吗?")) { return }
|
if (!confirm("当前操作会将你的播放队列清空、放入这张歌单所有曲目,并从头播放。继续吗?")) { return }
|
||||||
usePlayQueueStore().queueReplaceLock = false
|
playQueueStore.queueReplaceLock = false
|
||||||
}
|
}
|
||||||
playQueueStore.list = []
|
|
||||||
|
|
||||||
if (list === 'favourites') {
|
if (list === 'favourites') {
|
||||||
if (favourites.favouritesCount === 0) return
|
if (favourites.favouritesCount === 0) return
|
||||||
|
@ -29,25 +29,25 @@ function playTheList(list: 'favourites' | number, playFrom: number = 0) {
|
||||||
song: item.song,
|
song: item.song,
|
||||||
album: item.album
|
album: item.album
|
||||||
}))
|
}))
|
||||||
playQueueStore.list = newPlayQueue.slice().reverse()
|
// playQueueStore.list = newPlayQueue.slice().reverse()
|
||||||
playQueueStore.currentIndex = playFrom
|
// playQueueStore.currentIndex = playFrom
|
||||||
playQueueStore.playMode.shuffle = false
|
// playQueueStore.playMode.shuffle = false
|
||||||
playQueueStore.isPlaying = true
|
// playQueueStore.isPlaying = true
|
||||||
playQueueStore.isBuffering = true
|
// playQueueStore.isBuffering = true
|
||||||
} else {
|
} else {
|
||||||
// Handle other lists if needed
|
// Handle other lists if needed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function shuffle(list: 'favourites' | number) {
|
function shuffle(list: 'favourites' | number) {
|
||||||
playTheList(list)
|
// playTheList(list)
|
||||||
playQueueStore.shuffleCurrent = true
|
// playQueueStore.shuffleCurrent = true
|
||||||
playQueueStore.playMode.shuffle = false
|
// playQueueStore.playMode.shuffle = false
|
||||||
setTimeout(() => {
|
// setTimeout(() => {
|
||||||
playQueueStore.playMode.shuffle = true
|
// playQueueStore.playMode.shuffle = true
|
||||||
playQueueStore.isPlaying = true
|
// playQueueStore.isPlaying = true
|
||||||
playQueueStore.isBuffering = true
|
// playQueueStore.isBuffering = true
|
||||||
}, 100)
|
// }, 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { usePlayQueueStore } from '../stores/usePlayQueueStore'
|
import { usePlayStore } from '../stores/usePlayStore'
|
||||||
import { artistsOrganize } from '../utils'
|
import { artistsOrganize } from '../utils'
|
||||||
import gsap from 'gsap'
|
import gsap from 'gsap'
|
||||||
import { Draggable } from "gsap/Draggable"
|
import { Draggable } from "gsap/Draggable"
|
||||||
|
@ -30,8 +30,9 @@ import SpeakerIcon from '../assets/icons/speaker.vue'
|
||||||
import MuscialNoteSparklingIcon from '../assets/icons/musicalnotesparkling.vue'
|
import MuscialNoteSparklingIcon from '../assets/icons/musicalnotesparkling.vue'
|
||||||
import CastEmptyIcon from '../assets/icons/castempty.vue'
|
import CastEmptyIcon from '../assets/icons/castempty.vue'
|
||||||
|
|
||||||
const playQueueStore = usePlayQueueStore()
|
const playStore = usePlayStore()
|
||||||
const preferences = usePreferences()
|
const preferences = usePreferences()
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
const favourites = useFavourites()
|
const favourites = useFavourites()
|
||||||
|
|
||||||
gsap.registerPlugin(Draggable)
|
gsap.registerPlugin(Draggable)
|
||||||
|
@ -52,6 +53,7 @@ const volumeSliderContainer = useTemplateRef('volumeSliderContainer')
|
||||||
|
|
||||||
const presentQueueListDialog = ref(false)
|
const presentQueueListDialog = ref(false)
|
||||||
const presentLyrics = ref(false)
|
const presentLyrics = ref(false)
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
const showLyricsTooltip = ref(false)
|
const showLyricsTooltip = ref(false)
|
||||||
const showMoreOptions = ref(false)
|
const showMoreOptions = ref(false)
|
||||||
const presentVolumeControl = ref(false)
|
const presentVolumeControl = ref(false)
|
||||||
|
@ -66,8 +68,8 @@ onMounted(async () => {
|
||||||
onDrag: function () {
|
onDrag: function () {
|
||||||
const thumbPosition = this.x
|
const thumbPosition = this.x
|
||||||
const containerWidth = progressBarContainer.value?.clientWidth || 0
|
const containerWidth = progressBarContainer.value?.clientWidth || 0
|
||||||
const newTime = (thumbPosition / containerWidth) * playQueueStore.duration
|
const newTime = (thumbPosition / containerWidth) * playStore.progress.duration
|
||||||
playQueueStore.updatedCurrentTime = newTime
|
playStore.updateCurrentTime(newTime)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -77,7 +79,7 @@ onMounted(async () => {
|
||||||
// 初始化音量从localStorage或默认值
|
// 初始化音量从localStorage或默认值
|
||||||
const savedVolume = localStorage.getItem('audioVolume')
|
const savedVolume = localStorage.getItem('audioVolume')
|
||||||
if (savedVolume) {
|
if (savedVolume) {
|
||||||
volume.value = parseFloat(savedVolume)
|
volume.value = Number.parseFloat(savedVolume)
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbUpdate()
|
thumbUpdate()
|
||||||
|
@ -88,6 +90,7 @@ onMounted(async () => {
|
||||||
setupPageFocusHandlers()
|
setupPageFocusHandlers()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
function timeFormatter(time: number) {
|
function timeFormatter(time: number) {
|
||||||
const timeInSeconds = Math.floor(time)
|
const timeInSeconds = Math.floor(time)
|
||||||
if (timeInSeconds < 0) { return '-:--' }
|
if (timeInSeconds < 0) { return '-:--' }
|
||||||
|
@ -98,12 +101,12 @@ function timeFormatter(time: number) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 监听播放进度,更新进度条
|
// 监听播放进度,更新进度条
|
||||||
watch(() => playQueueStore.currentTime, () => {
|
watch(() => playStore.progress.currentTime, () => {
|
||||||
thumbUpdate()
|
thumbUpdate()
|
||||||
})
|
})
|
||||||
|
|
||||||
function thumbUpdate() {
|
function thumbUpdate() {
|
||||||
const progress = playQueueStore.currentTime / playQueueStore.duration
|
const progress = playStore.progress.percentage
|
||||||
const containerWidth = progressBarContainer.value?.clientWidth || 0
|
const containerWidth = progressBarContainer.value?.clientWidth || 0
|
||||||
const thumbWidth = progressBarThumb.value?.clientWidth || 0
|
const thumbWidth = progressBarThumb.value?.clientWidth || 0
|
||||||
const newPosition = (containerWidth - thumbWidth) * progress
|
const newPosition = (containerWidth - thumbWidth) * progress
|
||||||
|
@ -117,6 +120,7 @@ function volumeThumbUpdate() {
|
||||||
gsap.to(volumeSliderThumb.value, { x: newPosition, duration: 0.1 })
|
gsap.to(volumeSliderThumb.value, { x: newPosition, duration: 0.1 })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
|
||||||
function toggleVolumeControl() {
|
function toggleVolumeControl() {
|
||||||
if (!presentVolumeControl.value) {
|
if (!presentVolumeControl.value) {
|
||||||
presentVolumeControl.value = true
|
presentVolumeControl.value = true
|
||||||
|
|
|
@ -9,13 +9,13 @@ declare global {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFavourites = defineStore('favourites', () => {
|
export const useFavourites = defineStore('favourites', () => {
|
||||||
const favourites = ref<QueueItem[]>([])
|
const favourites = ref<InternalQueueItem[]>([])
|
||||||
|
|
||||||
const isLoaded = ref(false)
|
const isLoaded = ref(false)
|
||||||
const storageType = ref<'chrome' | 'localStorage' | 'memory'>('chrome')
|
const storageType = ref<'chrome' | 'localStorage' | 'memory'>('chrome')
|
||||||
|
|
||||||
// 默认收藏列表
|
// 默认收藏列表
|
||||||
const defaultFavourites: QueueItem[] = []
|
const defaultFavourites: InternalQueueItem[] = []
|
||||||
|
|
||||||
// 检测可用的 API
|
// 检测可用的 API
|
||||||
const detectAvailableAPIs = () => {
|
const detectAvailableAPIs = () => {
|
||||||
|
@ -128,7 +128,7 @@ export const useFavourites = defineStore('favourites', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 数据验证和规范化函数
|
// 数据验证和规范化函数
|
||||||
const normalizeFavourites = (data: any[]): QueueItem[] => {
|
const normalizeFavourites = (data: any[]): InternalQueueItem[] => {
|
||||||
if (!Array.isArray(data)) return []
|
if (!Array.isArray(data)) return []
|
||||||
|
|
||||||
return data.map(item => {
|
return data.map(item => {
|
||||||
|
@ -167,7 +167,7 @@ export const useFavourites = defineStore('favourites', () => {
|
||||||
} : undefined
|
} : undefined
|
||||||
|
|
||||||
return { song, album }
|
return { song, album }
|
||||||
}).filter(Boolean) as QueueItem[]
|
}).filter(Boolean) as InternalQueueItem[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取收藏列表
|
// 获取收藏列表
|
||||||
|
@ -191,7 +191,7 @@ export const useFavourites = defineStore('favourites', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加到收藏
|
// 添加到收藏
|
||||||
const addToFavourites = async (queueItem: QueueItem) => {
|
const addToFavourites = async (queueItem: InternalQueueItem) => {
|
||||||
if (!isFavourite(queueItem.song.cid)) {
|
if (!isFavourite(queueItem.song.cid)) {
|
||||||
favourites.value.push(queueItem)
|
favourites.value.push(queueItem)
|
||||||
if (isLoaded.value) {
|
if (isLoaded.value) {
|
||||||
|
@ -224,7 +224,7 @@ export const useFavourites = defineStore('favourites', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 切换收藏状态
|
// 切换收藏状态
|
||||||
const toggleFavourite = async (queueItem: QueueItem) => {
|
const toggleFavourite = async (queueItem: InternalQueueItem) => {
|
||||||
if (isFavourite(queueItem.song.cid)) {
|
if (isFavourite(queueItem.song.cid)) {
|
||||||
await removeFromFavourites(queueItem.song.cid)
|
await removeFromFavourites(queueItem.song.cid)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -1,217 +0,0 @@
|
||||||
import { defineStore } from 'pinia'
|
|
||||||
import { computed, ref } from 'vue'
|
|
||||||
import { checkAndRefreshSongResource } from '../utils'
|
|
||||||
|
|
||||||
export const usePlayQueueStore = defineStore('queue', () => {
|
|
||||||
const list = ref<QueueItem[]>([])
|
|
||||||
const currentIndex = ref<number>(0)
|
|
||||||
const isPlaying = ref<boolean>(false)
|
|
||||||
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 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
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentIndex.value < list.value.length - 1) {
|
|
||||||
return currentIndex.value + 1
|
|
||||||
} else if (playMode.value.repeat === 'all') {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果歌曲在收藏夹中,也更新收藏夹
|
|
||||||
// 注意:这里不直接导入 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调试函数:打印当前状态
|
|
||||||
const debugPreloadState = () => {
|
|
||||||
console.log('[Store] 预加载状态:', {
|
|
||||||
isPreloading: isPreloading.value,
|
|
||||||
progress: preloadProgress.value,
|
|
||||||
cacheSize: preloadedAudio.value.size,
|
|
||||||
cachedSongs: Array.from(preloadedAudio.value.keys()),
|
|
||||||
nextIndex: getNextIndex.value,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
list,
|
|
||||||
currentIndex,
|
|
||||||
isPlaying,
|
|
||||||
queueReplaceLock,
|
|
||||||
isBuffering,
|
|
||||||
currentTime,
|
|
||||||
duration,
|
|
||||||
updatedCurrentTime,
|
|
||||||
visualizer,
|
|
||||||
shuffleList,
|
|
||||||
playMode,
|
|
||||||
shuffleCurrent,
|
|
||||||
// 预加载相关 - 确保所有函数都在返回对象中
|
|
||||||
preloadedAudio,
|
|
||||||
isPreloading,
|
|
||||||
preloadProgress,
|
|
||||||
getNextIndex,
|
|
||||||
preloadNext,
|
|
||||||
getPreloadedAudio,
|
|
||||||
clearPreloadedAudio,
|
|
||||||
clearAllPreloadedAudio,
|
|
||||||
limitPreloadCache,
|
|
||||||
debugPreloadState,
|
|
||||||
}
|
|
||||||
})
|
|
95
src/stores/usePlayStore.ts
Normal file
95
src/stores/usePlayStore.ts
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import { Player } from '@astrian/music-surge-revolution'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { artistsOrganize } from '../utils'
|
||||||
|
|
||||||
|
export const usePlayStore = defineStore('player', () => {
|
||||||
|
const player = ref(new Player())
|
||||||
|
|
||||||
|
const queueReplaceLock = ref(false)
|
||||||
|
const visualizer = ref([0,0,0,0,0,0])
|
||||||
|
const progress = ref({
|
||||||
|
currentTime: 0,
|
||||||
|
duration: 0,
|
||||||
|
percentage: 0
|
||||||
|
})
|
||||||
|
const currentTrack = ref<{
|
||||||
|
url: string
|
||||||
|
metadata?: {
|
||||||
|
title?: string
|
||||||
|
artist?: string
|
||||||
|
artwork?: {
|
||||||
|
src: string
|
||||||
|
sizes?: string
|
||||||
|
type?: string
|
||||||
|
}[]
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const replaceQueue = (queue: {
|
||||||
|
song: Song
|
||||||
|
album: Album | undefined
|
||||||
|
}[]) => {
|
||||||
|
const newQueue = []
|
||||||
|
for (const item of queue) {
|
||||||
|
newQueue.push({
|
||||||
|
url: item.song.sourceUrl ?? "",
|
||||||
|
metadata: {
|
||||||
|
title: item.song.name,
|
||||||
|
artist: artistsOrganize(item.song.artists ?? item.song.artistes ?? []),
|
||||||
|
artwork: [{
|
||||||
|
src: item.album?.coverUrl ?? "",
|
||||||
|
sizes: "500x500",
|
||||||
|
type: ((item.album?.coverUrl ?? "").split(".").at(-1) === "jpg" ? 'image/jpeg' : 'image/png') as "image/jpeg" | "image/png"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
player.value.replaceQueue(newQueue)
|
||||||
|
}
|
||||||
|
|
||||||
|
const togglePlay = (play?: boolean) => {
|
||||||
|
player.value.togglePlaying(play)
|
||||||
|
}
|
||||||
|
|
||||||
|
const appendItem = (item: {
|
||||||
|
song: Song
|
||||||
|
album: Album | undefined
|
||||||
|
}) => {
|
||||||
|
player.value.appendTrack({
|
||||||
|
url: item.song.sourceUrl ?? "",
|
||||||
|
metadata: {
|
||||||
|
title: item.song.name,
|
||||||
|
artist: artistsOrganize(item.song.artistes ?? item.song.artists ?? []),
|
||||||
|
artwork: [{
|
||||||
|
src: item.album?.coverUrl ?? "",
|
||||||
|
sizes: "500x500",
|
||||||
|
type: ((item.album?.coverUrl ?? "").split(".").at(-1) === "jpg" ? 'image/jpeg' : 'image/png') as "image/jpeg" | "image/png"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
player.value.onProgressChange(params => {
|
||||||
|
progress.value = params
|
||||||
|
})
|
||||||
|
|
||||||
|
player.value.onCurrentPlayingChange(params => {
|
||||||
|
currentTrack.value = params
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateCurrentTime = (time: number) => {
|
||||||
|
player.value.seekTo(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
queueReplaceLock,
|
||||||
|
togglePlay,
|
||||||
|
visualizer,
|
||||||
|
appendItem,
|
||||||
|
progress,
|
||||||
|
currentTrack,
|
||||||
|
updateCurrentTime,
|
||||||
|
replaceQueue
|
||||||
|
}
|
||||||
|
})
|
2
src/vite-env.d.ts
vendored
2
src/vite-env.d.ts
vendored
|
@ -35,7 +35,7 @@ interface ApiResponse {
|
||||||
data: unknown
|
data: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueueItem {
|
interface InternalQueueItem {
|
||||||
song: Song
|
song: Song
|
||||||
album?: Album
|
album?: Album
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user