fix: 让 playroom 现在有东西

This commit is contained in:
Astrian Zheng 2025-08-31 16:59:13 +10:00
parent fd6253e626
commit f1fb5330a9
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA
3 changed files with 127 additions and 132 deletions

View File

@ -9,6 +9,7 @@ import CorgIcon from './assets/icons/corg.vue'
import { watch } from 'vue' import { watch } from 'vue'
import UpdatePopup from './components/UpdatePopup.vue' import UpdatePopup from './components/UpdatePopup.vue'
import MiniPlayer from './components/MiniPlayer.vue'
const presentPreferencePanel = ref(false) const presentPreferencePanel = ref(false)
@ -71,7 +72,7 @@ watch(() => presentPreferencePanel, (value) => {
@click="presentPreferencePanel = true"> @click="presentPreferencePanel = true">
<CorgIcon :size="4" /> <CorgIcon :size="4" />
</button> </button>
<MiniPlayer />
</div> </div>
</div> </div>
</div> </div>

View File

@ -179,8 +179,9 @@ function updateAudioVolume() {
} }
} }
// biome-ignore lint/correctness/noUnusedVariables: used in <template>
function formatDetector() { function formatDetector() {
const format = playQueueStore.list[playQueueStore.currentIndex].song.sourceUrl?.split('.').pop() const format = playStore.currentTrack?.url?.split('.').pop()
if (format === 'mp3') { return 'MP3' } if (format === 'mp3') { return 'MP3' }
if (format === 'flac') { return 'FLAC' } if (format === 'flac') { return 'FLAC' }
if (format === 'm4a') { return 'M4A' } if (format === 'm4a') { return 'M4A' }
@ -189,26 +190,6 @@ function formatDetector() {
return '未知格式' return '未知格式'
} }
function playNext() {
if (playQueueStore.currentIndex === playQueueStore.list.length - 1) {
console.log("at the bottom, pause")
playQueueStore.currentIndex = 0
playQueueStore.isPlaying = false
} else {
playQueueStore.currentIndex++
playQueueStore.isPlaying = true
}
}
function playPrevious() {
if (playQueueStore.currentTime < 5 && playQueueStore.currentIndex > 0) {
playQueueStore.currentIndex--
playQueueStore.isPlaying = true
} else {
playQueueStore.updatedCurrentTime = 0
}
}
function setupEntranceAnimations() { function setupEntranceAnimations() {
if (controllerRef.value) { if (controllerRef.value) {
gsap.fromTo(controllerRef.value.children, gsap.fromTo(controllerRef.value.children,
@ -309,14 +290,7 @@ function makePlayQueueListDismiss() {
} }
function getCurrentTrack() { function getCurrentTrack() {
if (playQueueStore.list.length === 0) { return playStore.currentTrack
return null
}
if (playQueueStore.playMode.shuffle) {
return playQueueStore.list[playQueueStore.shuffleList[playQueueStore.currentIndex]]
} else {
return playQueueStore.list[playQueueStore.currentIndex]
}
} }
function toggleMoreOptions() { function toggleMoreOptions() {
@ -360,71 +334,72 @@ function toggleMoreOptions() {
} }
} }
watch(() => [preferences.presentLyrics, getCurrentTrack()?.song.lyricUrl], (newValue, oldValue) => { // TODO: lyrics
if (!getCurrentTrack()) { return } // watch(() => [preferences.presentLyrics, getCurrentTrack()?.song.lyricUrl], (newValue, oldValue) => {
// if (!getCurrentTrack()) { return }
const [showLyrics, hasLyricUrl] = newValue // const [showLyrics, hasLyricUrl] = newValue
const [prevShowLyrics, _prevHasLyricUrl] = oldValue || [false, null] // const [prevShowLyrics, _prevHasLyricUrl] = oldValue || [false, null]
// Show lyrics when both conditions are met // // Show lyrics when both conditions are met
if (showLyrics && hasLyricUrl) { // if (showLyrics && hasLyricUrl) {
presentLyrics.value = true // presentLyrics.value = true
nextTick(() => { // nextTick(() => {
const tl = gsap.timeline() // const tl = gsap.timeline()
tl.from(controllerRef.value, { // tl.from(controllerRef.value, {
marginRight: '-40rem', // marginRight: '-40rem',
}).fromTo(lyricsSection.value, // }).fromTo(lyricsSection.value,
{ opacity: 0, x: 50, scale: 0.95 }, // { opacity: 0, x: 50, scale: 0.95 },
{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: "power2.out" }, // { opacity: 1, x: 0, scale: 1, duration: 0.5, ease: "power2.out" },
"-=0.3" // "-=0.3"
) // )
}) // })
} // }
// Hide lyrics with different animations based on reason // // Hide lyrics with different animations based on reason
else if (presentLyrics.value) { // else if (presentLyrics.value) {
let animationConfig // let animationConfig
// If lyrics were toggled off // // If lyrics were toggled off
if (prevShowLyrics && !showLyrics) { // if (prevShowLyrics && !showLyrics) {
animationConfig = { // animationConfig = {
opacity: 0, x: -50, scale: 0.95, // opacity: 0, x: -50, scale: 0.95,
duration: 0.3, ease: "power2.in" // duration: 0.3, ease: "power2.in"
} // }
} // }
// If no lyrics available (song changed) // // If no lyrics available (song changed)
else if (!hasLyricUrl) { // else if (!hasLyricUrl) {
animationConfig = { // animationConfig = {
opacity: 0, y: -20, scale: 0.98, // opacity: 0, y: -20, scale: 0.98,
duration: 0.3, ease: "power1.in" // duration: 0.3, ease: "power1.in"
} // }
} // }
// Default animation // // Default animation
else { // else {
animationConfig = { // animationConfig = {
opacity: 0, x: -50, // opacity: 0, x: -50,
duration: 0.3, ease: "power2.in" // duration: 0.3, ease: "power2.in"
} // }
} // }
const tl = gsap.timeline({ // const tl = gsap.timeline({
onComplete: () => { // onComplete: () => {
presentLyrics.value = false // presentLyrics.value = false
} // }
}) // })
tl.to(controllerRef.value, { // tl.to(controllerRef.value, {
marginLeft: '44rem', // marginLeft: '44rem',
duration: 0.3, ease: "power2.out" // duration: 0.3, ease: "power2.out"
}) // })
.to(lyricsSection.value, animationConfig, '<') // .to(lyricsSection.value, animationConfig, '<')
.set(lyricsSection.value, { // .set(lyricsSection.value, {
opacity: 1, x: 0, y: 0, scale: 1 // Reset for next time // opacity: 1, x: 0, y: 0, scale: 1 // Reset for next time
}) // })
.set(controllerRef.value, { // .set(controllerRef.value, {
marginLeft: '0rem' // Reset for next time // marginLeft: '0rem' // Reset for next time
}) // })
} // }
}, { immediate: true }) // }, { immediate: true })
// //
let handleVisibilityChange: (() => void) | null = null let handleVisibilityChange: (() => void) | null = null
@ -519,7 +494,7 @@ function resyncLyricsState() {
} }
// New: Watch for track changes and animate // New: Watch for track changes and animate
watch(() => playQueueStore.currentIndex, () => { watch(() => playStore.currentTrack, () => {
if (albumCover.value) { if (albumCover.value) {
gsap.to(albumCover.value, { gsap.to(albumCover.value, {
scale: 0.95, opacity: 0.7, duration: 0.2, scale: 0.95, opacity: 0.7, duration: 0.2,
@ -539,11 +514,11 @@ watch(() => playQueueStore.currentIndex, () => {
<template> <template>
<div v-if="getCurrentTrack() !== null"> <div v-if="getCurrentTrack() !== null">
<!-- Background remains unchanged --> <!-- Background remains unchanged -->
<div class="z-0 absolute top-0 left-0 w-screen h-screen overflow-hidden" <!-- <div class="z-0 absolute top-0 left-0 w-screen h-screen overflow-hidden"
v-if="getCurrentTrack()?.album?.coverDeUrl"> v-if="getCurrentTrack()?.album?.coverDeUrl">
<img class="w-full h-full blur-2xl object-cover scale-110" :src="getCurrentTrack()?.album?.coverDeUrl" /> <img class="w-full h-full blur-2xl object-cover scale-110" :src="getCurrentTrack()?.album?.coverDeUrl" />
<div class="w-full h-full absolute top-0 left-0 bg-neutral-900/5" /> <div class="w-full h-full absolute top-0 left-0 bg-neutral-900/5" />
</div> </div> -->
<!-- Main content area - new centered flex layout --> <!-- Main content area - new centered flex layout -->
<div class="absolute top-0 left-0 flex justify-center h-screen w-screen overflow-y-auto z-10 select-none"> <div class="absolute top-0 left-0 flex justify-center h-screen w-screen overflow-y-auto z-10 select-none">
@ -553,9 +528,9 @@ watch(() => playQueueStore.currentIndex, () => {
<div class="flex flex-col w-96 gap-4" ref="controllerRef"> <div class="flex flex-col w-96 gap-4" ref="controllerRef">
<!-- Album cover with enhanced hover effect --> <!-- Album cover with enhanced hover effect -->
<div ref="albumCover" class="relative"> <div ref="albumCover" class="relative">
<img :src="getCurrentTrack()?.album?.coverUrl" class="rounded-2xl shadow-2xl border border-white/20 w-96 h-96 <img :src="getCurrentTrack()?.metadata?.artwork?.[0].src" class="rounded-2xl shadow-2xl border border-white/20 w-96 h-96
transition-transform duration-300 transition-transform duration-300
" :class="playQueueStore.isPlaying ? 'scale-100' : 'scale-85'" /> " :class="playStore.isPlaying ? 'scale-100' : 'scale-85'" />
</div> </div>
<!-- Song info with enhanced styling --> <!-- Song info with enhanced styling -->
@ -564,26 +539,26 @@ watch(() => playQueueStore.currentIndex, () => {
<!-- ...existing song info code... --> <!-- ...existing song info code... -->
<div class=""> <div class="">
<div class="text-black/90 blur-lg text-lg font-medium truncate w-80"> <div class="text-black/90 blur-lg text-lg font-medium truncate w-80">
{{ getCurrentTrack()?.song.name }} {{ getCurrentTrack()?.metadata?.title }}
</div> </div>
<div class="text-black/90 blur-lg text-base truncate w-80"> <div class="text-black/90 blur-lg text-base truncate w-80">
{{ getCurrentTrack()?.song.artists ?? [] }} {{ getCurrentTrack()?.metadata?.artist}}
{{ getCurrentTrack()?.album?.name ?? '未知专辑' }} {{ getCurrentTrack()?.metadata?.album ?? '未知专辑' }}
</div> </div>
</div> </div>
<div class="absolute top-0"> <div class="absolute top-0">
<div class="text-white text-lg font-medium truncate w-80"> <div class="text-white text-lg font-medium truncate w-80">
{{ getCurrentTrack()?.song.name }} {{ getCurrentTrack()?.metadata?.title }}
</div> </div>
<div class="text-white/75 text-base truncate w-80"> <div class="text-white/75 text-base truncate w-80">
{{ artistsOrganize(getCurrentTrack()?.song.artists ?? []) }} {{ getCurrentTrack()?.metadata?.artist }}
{{ getCurrentTrack()?.album?.name ?? '未知专辑' }} {{ getCurrentTrack()?.metadata?.album ?? '未知专辑' }}
</div> </div>
</div> </div>
</div> </div>
<button <!-- <button
class="h-10 w-10 flex justify-center items-center rounded-full backdrop-blur-3xl transition-all duration-200 hover:scale-110" class="h-10 w-10 flex justify-center items-center rounded-full backdrop-blur-3xl transition-all duration-200 hover:scale-110"
ref="favoriteButton" ref="favoriteButton"
@click="() => { const track = getCurrentTrack(); if (track !== null) favourites.toggleFavourite(track) }" @click="() => { const track = getCurrentTrack(); if (track !== null) favourites.toggleFavourite(track) }"
@ -594,7 +569,7 @@ watch(() => playQueueStore.currentIndex, () => {
:size="6" /> :size="6" />
<StarEmptyIcon v-else :size="6" /> <StarEmptyIcon v-else :size="6" />
</span> </span>
</button> </button> -->
</div> </div>
<!-- Progress section --> <!-- Progress section -->
@ -611,9 +586,9 @@ watch(() => playQueueStore.currentIndex, () => {
<!-- ...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 text-xs 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 text-xs">{{ timeFormatter(Math.floor(playStore.progress.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(playStore.progress.currentTime)) }}</span>
</div> </div>
<div class="text-xs text-center relative flex-1"> <div class="text-xs text-center relative flex-1">
<span class="text-black blur-lg absolute top-0">{{ formatDetector() }}</span> <span class="text-black blur-lg absolute top-0">{{ formatDetector() }}</span>
@ -625,8 +600,8 @@ watch(() => playQueueStore.currentIndex, () => {
class="text-white/90 text-xs font-medium text-right relative transition-colors duration-200 hover:text-white" class="text-white/90 text-xs 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(playStore.progress.duration) - Math.floor(playStore.progress.currentTime) : playStore.progress.duration)}` }}</span>
<span>{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playQueueStore.duration) - Math.floor(playQueueStore.currentTime) : playQueueStore.duration)}` }}</span> <span>{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playStore.progress.duration) - Math.floor(playStore.progress.currentTime) : playStore.progress.duration)}` }}</span>
</button> </button>
</div> </div>
</div> </div>
@ -688,16 +663,16 @@ watch(() => playQueueStore.currentIndex, () => {
class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25 transition-all duration-200" class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25 transition-all duration-200"
@click="handlePlayPause" ref="playButton"> @click="handlePlayPause" ref="playButton">
<!-- ...existing play/pause icon code... --> <!-- ...existing play/pause icon code... -->
<div v-if="playQueueStore.isPlaying"> <div v-if="playStore.isPlaying">
<div v-if="playQueueStore.isBuffering" class="w-6 h-6 relative"> <!-- <div v-if="playQueueStore.isBuffering" class="w-6 h-6 relative">
<span class="text-black/80 blur-lg absolute top-0 left-0"> <span class="text-black/80 blur-lg absolute top-0 left-0">
<LoadingIndicator :size="6" /> <LoadingIndicator :size="6" />
</span> </span>
<span class="text-white absolute top-0 left-0"> <span class="text-white absolute top-0 left-0">
<LoadingIndicator :size="6" /> <LoadingIndicator :size="6" />
</span> </span>
</div> </div> -->
<div v-else class="w-8 h-8 relative"> <div class="w-8 h-8 relative">
<span class="text-black blur-md absolute top-0 left-0"> <span class="text-black blur-md absolute top-0 left-0">
<PauseIcon :size="8" /> <PauseIcon :size="8" />
</span> </span>
@ -720,7 +695,7 @@ watch(() => playQueueStore.currentIndex, () => {
<button <button
class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25 transition-all duration-200 hover:scale-105" class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25 transition-all duration-200 hover:scale-105"
@click="playNext" ref="nextButton"> @click="playStore.skipToPrevious" ref="nextButton">
<div class="w-8 h-8 relative"> <div class="w-8 h-8 relative">
<span class="text-black/80 blur-lg absolute top-0 left-0"> <span class="text-black/80 blur-lg absolute top-0 left-0">
<ForwardIcon :size="8" /> <ForwardIcon :size="8" />
@ -830,29 +805,29 @@ watch(() => playQueueStore.currentIndex, () => {
<div class="flex gap-2 mx-8 mb-4"> <div class="flex gap-2 mx-8 mb-4">
<button <button
class="flex-1 h-9 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:scale-105" class="flex-1 h-9 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:scale-105"
:class="playQueueStore.playMode.shuffle ? 'bg-[#ffffffaa] text-neutral-700' : 'text-white bg-neutral-800/80'" :class="playStore.shuffle ? 'bg-[#ffffffaa] text-neutral-700' : 'text-white bg-neutral-800/80'"
@click="toggleShuffle"> @click="toggleShuffle">
<ShuffleIcon :size="4" /> <ShuffleIcon :size="4" />
</button> </button>
<button <button
class="flex-1 h-9 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:scale-105" class="flex-1 h-9 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:scale-105"
:class="playQueueStore.playMode.repeat === 'off' ? 'text-white bg-neutral-800/80' : 'bg-[#ffffffaa] text-neutral-700'" :class="playStore.loop === 'off' ? 'text-white bg-neutral-800/80' : 'bg-[#ffffffaa] text-neutral-700'"
@click="toggleRepeat"> @click="toggleRepeat">
<CycleTwoArrowsIcon :size="4" v-if="playQueueStore.playMode.repeat !== 'single'" /> <CycleTwoArrowsIcon :size="4" v-if="playStore.loop !== 'single_track'" />
<CycleTwoArrowsWithNumOneIcon :size="4" v-else /> <CycleTwoArrowsWithNumOneIcon :size="4" v-else />
</button> </button>
</div> </div>
<hr class="border-[#ffffff39]" /> <hr class="border-[#ffffff39]" />
<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-if="playQueueStore.playMode.shuffle"> <div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-if="playStore.shuffle">
<PlayQueueItem v-for="(oriIndex, shuffledIndex) in playQueueStore.shuffleList" <!-- <PlayQueueItem v-for="(oriIndex, shuffledIndex) in playStore.shuffleList"
:queueItem="playQueueStore.list[oriIndex]" :isCurrent="playQueueStore.currentIndex === shuffledIndex" :queueItem="playQueueStore.list[oriIndex]" :isCurrent="playQueueStore.currentIndex === shuffledIndex"
:key="playQueueStore.list[oriIndex].song.cid" :index="shuffledIndex" /> :key="playQueueStore.list[oriIndex].song.cid" :index="shuffledIndex" /> -->
</div> </div>
<div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-else> <div class="flex-auto h-0 overflow-y-auto px-4 flex flex-col gap-2" v-else>
<PlayQueueItem :queueItem="track" :isCurrent="playQueueStore.currentIndex === index" <!-- <PlayQueueItem :queueItem="track" :isCurrent="playQueueStore.currentIndex === index"
v-for="(track, index) in playQueueStore.list" :index="index" :key="track.song.cid" /> v-for="(track, index) in playQueueStore.list" :index="index" :key="track.song.cid" /> -->
</div> </div>
</div> </div>
</dialog> </dialog>

View File

@ -1,4 +1,5 @@
import { Player } from '@astrian/music-surge-revolution' import { Player } from '@astrian/music-surge-revolution'
import type { QueueItem } from '@astrian/music-surge-revolution'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import { artistsOrganize } from '../utils' import { artistsOrganize } from '../utils'
@ -13,18 +14,11 @@ export const usePlayStore = defineStore('player', () => {
duration: 0, duration: 0,
percentage: 0 percentage: 0
}) })
const currentTrack = ref<{ const currentTrack = ref<QueueItem>()
url: string const isPlaying = ref(false)
metadata?: { const queue = ref<QueueItem[]>([])
title?: string const shuffle = ref(false)
artist?: string const loop = ref<"off" | "entire_queue" | "single_track">("off")
artwork?: {
src: string
sizes?: string
type?: string
}[]
}
}>()
const replaceQueue = (queue: { const replaceQueue = (queue: {
song: Song song: Song
@ -78,10 +72,30 @@ export const usePlayStore = defineStore('player', () => {
currentTrack.value = params currentTrack.value = params
}) })
player.value.onPlayStateChange(params => {
isPlaying.value = params
})
player.value.onQueueChange(params => {
queue.value = params
})
player.value.onShuffleChange(params => {
shuffle.value = params
})
player.value.onLoopChange(params => {
loop.value = params
})
const updateCurrentTime = (time: number) => { const updateCurrentTime = (time: number) => {
player.value.seekTo(time) player.value.seekTo(time)
} }
const skipToPrevious = () => {
player.value.skipToPrevious()
}
return { return {
queueReplaceLock, queueReplaceLock,
togglePlay, togglePlay,
@ -90,6 +104,11 @@ export const usePlayStore = defineStore('player', () => {
progress, progress,
currentTrack, currentTrack,
updateCurrentTime, updateCurrentTime,
skipToPrevious,
isPlaying,
queue,
shuffle,
loop,
replaceQueue replaceQueue
} }
}) })