feat(Playroom): add lyrics display functionality with animations and preferences management

This commit is contained in:
Astrian Zheng 2025-05-26 19:34:13 +10:00
parent 95d00616d0
commit 588182e888
Signed by: Astrian
SSH Key Fingerprint: SHA256:rVnhx3DAKjujCwWE13aDl7uV6+9U1MvydLkNRXJrBiA
2 changed files with 134 additions and 42 deletions

View File

@ -41,7 +41,7 @@ const songInfo = useTemplateRef('songInfo')
const playButton = useTemplateRef('playButton')
const presentQueueListDialog = ref(false)
// const presentLyrics = ref(false)
const presentLyrics = ref(false)
onMounted(async () => {
Draggable.create(progressBarThumb.value, {
@ -55,7 +55,7 @@ onMounted(async () => {
}
})
thumbUpdate()
setupEntranceAnimations()
})
@ -115,13 +115,13 @@ function setupEntranceAnimations() {
if (controllerRef.value) {
gsap.fromTo(controllerRef.value.children,
{ opacity: 0, y: 30, scale: 0.95 },
{
{
opacity: 1, y: 0, scale: 1,
duration: 0.6, ease: "power2.out", stagger: 0.1
}
)
}
if (lyricsSection.value) {
gsap.fromTo(lyricsSection.value,
{ opacity: 0, x: 50 },
@ -147,7 +147,7 @@ function handlePlayPause() {
function toggleShuffle() {
playQueueStore.playMode.shuffle = !playQueueStore.playMode.shuffle
playQueueStore.shuffleCurrent = false
nextTick(() => {
const shuffleBtn = playQueueDialog.value?.querySelector('.flex-1:first-child') as HTMLElement
if (shuffleBtn) {
@ -167,7 +167,7 @@ function toggleRepeat() {
})
}
})
switch (playQueueStore.playMode.repeat) {
case 'off': playQueueStore.playMode.repeat = 'all'; break
case 'all': playQueueStore.playMode.repeat = 'single'; break
@ -177,17 +177,17 @@ function toggleRepeat() {
function makePlayQueueListPresent() {
presentQueueListDialog.value = true
nextTick(() => {
if (!playQueueDialogContainer.value || !playQueueDialog.value) return
const tl = gsap.timeline()
tl.to(playQueueDialogContainer.value, {
backgroundColor: '#17171780', duration: 0.3, ease: 'power2.out'
}).to(playQueueDialog.value, {
x: 0, duration: 0.4, ease: 'power3.out'
}, '<0.1')
if (playQueueDialog.value.children.length > 0) {
tl.fromTo(playQueueDialog.value.children,
{ opacity: 0, x: -20 },
@ -201,7 +201,7 @@ function makePlayQueueListDismiss() {
presentQueueListDialog.value = false
return
}
const tl = gsap.timeline({
onComplete: () => {
presentQueueListDialog.value = false
@ -213,19 +213,19 @@ function makePlayQueueListDismiss() {
}
}
})
if (playQueueDialog.value.children.length > 0) {
tl.to(playQueueDialog.value.children, {
opacity: 0, x: -20, duration: 0.2, ease: 'power2.in', stagger: 0.03
})
}
tl.to(playQueueDialog.value, {
x: -384, duration: 0.3, ease: 'power2.in'
}, playQueueDialog.value.children.length > 0 ? '<0.1' : '0')
.to(playQueueDialogContainer.value, {
backgroundColor: 'transparent', duration: 0.2, ease: 'power2.in'
}, '<')
.to(playQueueDialogContainer.value, {
backgroundColor: 'transparent', duration: 0.2, ease: 'power2.in'
}, '<')
}
function getCurrentTrack() {
@ -236,6 +236,70 @@ function getCurrentTrack() {
}
}
watch(() => [preferences.presentLyrics, getCurrentTrack().song.lyricUrl], (newValue, oldValue) => {
const [showLyrics, hasLyricUrl] = newValue
const [prevShowLyrics, _prevHasLyricUrl] = oldValue || [false, null]
// Show lyrics when both conditions are met
if (showLyrics && hasLyricUrl) {
presentLyrics.value = true
nextTick(() => {
const tl = gsap.timeline()
tl.from(controllerRef.value, {
marginRight: '-40rem',
}).fromTo(lyricsSection.value,
{ opacity: 0, x: 50, scale: 0.95 },
{ opacity: 1, x: 0, scale: 1, duration: 0.5, ease: "power2.out" },
"-=0.3"
)
})
}
// Hide lyrics with different animations based on reason
else if (presentLyrics.value) {
let animationConfig
// If lyrics were toggled off
if (prevShowLyrics && !showLyrics) {
animationConfig = {
opacity: 0, x: -50, scale: 0.95,
duration: 0.3, ease: "power2.in"
}
}
// If no lyrics available (song changed)
else if (!hasLyricUrl) {
animationConfig = {
opacity: 0, y: -20, scale: 0.98,
duration: 0.3, ease: "power1.in"
}
}
// Default animation
else {
animationConfig = {
opacity: 0, x: -50,
duration: 0.3, ease: "power2.in"
}
}
const tl = gsap.timeline({
onComplete: () => {
presentLyrics.value = false
}
})
tl.to(controllerRef.value, {
marginLeft: '44rem',
duration: 0.3, ease: "power2.out"
})
.to(lyricsSection.value, animationConfig, '<')
.set(lyricsSection.value, {
opacity: 1, x: 0, y: 0, scale: 1 // Reset for next time
})
.set(controllerRef.value, {
marginLeft: '0rem' // Reset for next time
})
}
}, { immediate: true })
onUnmounted(() => {
})
@ -247,7 +311,7 @@ watch(() => playQueueStore.currentIndex, () => {
ease: "power2.inOut", yoyo: true, repeat: 1
})
}
if (songInfo.value) {
gsap.fromTo(songInfo.value,
{ opacity: 0, y: 10 },
@ -267,15 +331,15 @@ watch(() => playQueueStore.currentIndex, () => {
<!-- 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="flex items-center justify-center gap-16 h-fit my-auto" ref="mainContainer">
<!-- Controller area -->
<div class="flex flex-col w-96 gap-4" ref="controllerRef">
<!-- Album cover with enhanced hover effect -->
<div ref="albumCover" class="relative">
<img :src="getCurrentTrack().album?.coverUrl"
<img :src="getCurrentTrack().album?.coverUrl"
class="rounded-2xl shadow-2xl border border-white/20 w-96 h-96 transition-transform duration-300 hover:scale-105" />
</div>
<!-- Song info with enhanced styling -->
<div class="flex justify-between items-center gap-2" ref="songInfo">
<div class="relative flex-auto w-0">
@ -301,7 +365,8 @@ watch(() => playQueueStore.currentIndex, () => {
</div>
</div>
<button class="h-10 w-10 flex justify-center items-center rounded-full bg-black/10 backdrop-blur-3xl transition-all duration-200 hover:bg-black/20 hover:scale-110"
<button
class="h-10 w-10 flex justify-center items-center rounded-full bg-black/10 backdrop-blur-3xl transition-all duration-200 hover:bg-black/20 hover:scale-110"
ref="favoriteButton">
<span class="text-white">
<StarEmptyIcon :size="6" />
@ -312,7 +377,8 @@ watch(() => playQueueStore.currentIndex, () => {
<!-- Progress section -->
<div class="flex flex-col gap-1" ref="progressSection">
<!-- ...existing progress bar code... -->
<div class="w-full p-[0.125rem] bg-white/20 shadow-[0_.125rem_1rem_0_#00000010] rounded-full backdrop-blur-3xl">
<div
class="w-full p-[0.125rem] bg-white/20 shadow-[0_.125rem_1rem_0_#00000010] rounded-full backdrop-blur-3xl">
<div class="w-full" ref="progressBarContainer">
<div class="w-2 h-2 bg-white rounded-full shadow-md" ref="progressBarThumb" />
</div>
@ -321,7 +387,8 @@ watch(() => playQueueStore.currentIndex, () => {
<div class="w-full flex justify-between">
<!-- ...existing time display code... -->
<div class="font-medium flex-1 text-left relative">
<span class="text-black blur-lg absolute top-0">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
<span
class="text-black blur-lg absolute top-0">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
<span class="text-white/90">{{ timeFormatter(Math.floor(playQueueStore.currentTime)) }}</span>
</div>
<div class="text-xs text-center relative">
@ -330,9 +397,11 @@ watch(() => playQueueStore.currentIndex, () => {
</div>
<div class="flex flex-1">
<div class="flex-1" />
<button class="text-white/90 font-medium text-right relative transition-colors duration-200 hover:text-white"
<button
class="text-white/90 font-medium text-right relative transition-colors duration-200 hover:text-white"
@click="preferences.displayTimeLeft = !preferences.displayTimeLeft">
<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>
<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>
<span>{{ `${preferences.displayTimeLeft ? '-' : ''}${timeFormatter(preferences.displayTimeLeft ? Math.floor(playQueueStore.duration) - Math.floor(playQueueStore.currentTime) : playQueueStore.duration)}` }}</span>
</button>
</div>
@ -342,7 +411,8 @@ watch(() => playQueueStore.currentIndex, () => {
<!-- Control buttons -->
<div class="w-full flex justify-between items-center" ref="controlButtons">
<div class="flex-1 text-left flex gap-1">
<button class="h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
<button
class="h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
ref="volumeButton">
<div class="w-6 h-6 relative">
<span class="text-black blur-md absolute top-0 left-0">
@ -353,7 +423,8 @@ watch(() => playQueueStore.currentIndex, () => {
</span>
</div>
</button>
<button class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
<button
class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
@click="makePlayQueueListPresent" ref="queueButton">
<div class="w-6 h-6 relative">
<span class="text-black blur-md absolute top-0 left-0">
@ -367,7 +438,8 @@ watch(() => playQueueStore.currentIndex, () => {
</div>
<div class="flex-2 text-center align-center justify-center gap-2 flex">
<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"
<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"
@click="playPrevious" ref="prevButton">
<div class="w-8 h-8 relative">
<span class="text-black/80 blur-lg absolute top-0 left-0">
@ -379,7 +451,8 @@ watch(() => playQueueStore.currentIndex, () => {
</div>
</button>
<button class="text-white flex-1 h-10 flex justify-center items-center rounded-lg hover:bg-white/25 transition-all duration-200"
<button
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">
<!-- ...existing play/pause icon code... -->
<div v-if="playQueueStore.isPlaying">
@ -412,7 +485,8 @@ watch(() => playQueueStore.currentIndex, () => {
</div>
</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"
<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"
@click="playNext" ref="nextButton">
<div class="w-8 h-8 relative">
<span class="text-black/80 blur-lg absolute top-0 left-0">
@ -427,8 +501,9 @@ watch(() => playQueueStore.currentIndex, () => {
<div class="flex-1 text-right flex gap-1">
<div class="flex-1" />
<button class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
ref="lyricsButton">
<button
class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
ref="lyricsButton" @click="preferences.presentLyrics = !preferences.presentLyrics">
<div class="w-6 h-6 relative">
<span class="text-black blur-md absolute top-0 left-0">
<ChatBubbleQuoteIcon :size="6" />
@ -438,7 +513,8 @@ watch(() => playQueueStore.currentIndex, () => {
</span>
</div>
</button>
<button class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
<button
class="text-white h-8 w-8 flex justify-center items-center rounded-full hover:bg-white/25 transition-all duration-200 hover:scale-110"
ref="moreButton">
<div class="w-6 h-6 relative">
<span class="text-black blur-sm absolute top-0 left-0">
@ -454,9 +530,8 @@ watch(() => playQueueStore.currentIndex, () => {
</div>
<!-- Lyrics section - full screen height -->
<div class="w-[40rem] h-screen" ref="lyricsSection">
<ScrollingLyrics :lrcSrc="getCurrentTrack().song.lyricUrl ?? undefined"
class="h-full" ref="scrollingLyrics" />
<div class="w-[40rem] h-screen" ref="lyricsSection" v-if="presentLyrics">
<ScrollingLyrics :lrcSrc="getCurrentTrack().song.lyricUrl ?? undefined" class="h-full" ref="scrollingLyrics" />
</div>
</div>
</div>
@ -464,23 +539,27 @@ watch(() => playQueueStore.currentIndex, () => {
<!-- Queue list dialog with enhanced animations -->
<dialog :open="presentQueueListDialog" class="z-20 w-screen h-screen" @click="makePlayQueueListDismiss"
ref="playQueueDialogContainer" style="background-color: transparent;">
<div class="w-96 h-screen bg-neutral-900/80 shadow-[0_0_16px_0_rgba(0,0,0,0.5)] backdrop-blur-2xl pt-8 flex flex-col transform -translate-x-96"
<div
class="w-96 h-screen bg-neutral-900/80 shadow-[0_0_16px_0_rgba(0,0,0,0.5)] backdrop-blur-2xl pt-8 flex flex-col transform -translate-x-96"
@click.stop ref="playQueueDialog">
<div class="flex justify-between mx-8 mb-4">
<div class="text-white font-medium text-2xl">播放队列</div>
<button class="text-white w-9 h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:bg-neutral-700/80 hover:scale-110"
<button
class="text-white w-9 h-9 bg-neutral-800/80 border border-[#ffffff39] rounded-full text-center backdrop-blur-3xl flex justify-center items-center transition-all duration-200 hover:bg-neutral-700/80 hover:scale-110"
@click="makePlayQueueListDismiss">
<XIcon :size="4" />
</button>
</div>
<div class="flex gap-2 mx-8 mb-4">
<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"
<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="playQueueStore.playMode.shuffle ? 'bg-[#ffffffaa] text-neutral-700' : 'text-white bg-neutral-800/80'"
@click="toggleShuffle">
<ShuffleIcon :size="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"
<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="playQueueStore.playMode.repeat === 'off' ? 'text-white bg-neutral-800/80' : 'bg-[#ffffffaa] text-neutral-700'"
@click="toggleRepeat">
<CycleTwoArrowsIcon :size="4" v-if="playQueueStore.playMode.repeat !== 'single'" />

View File

@ -10,6 +10,8 @@ declare global {
export const usePreferences = defineStore('preferences', () => {
const displayTimeLeft = ref<boolean>(false)
const presentLyrics = ref<boolean>(false)
const isLoaded = ref(false)
const storageType = ref<'chrome' | 'localStorage' | 'memory'>('chrome')
const debugInfo = ref<string[]>([])
@ -153,10 +155,11 @@ export const usePreferences = defineStore('preferences', () => {
addDebugInfo('开始初始化偏好设置...')
try {
const value = await getStoredValue('displayTimeLeft', false)
displayTimeLeft.value = value as boolean
const displayTimeLeftValue = await getStoredValue('displayTimeLeft', false)
displayTimeLeft.value = displayTimeLeftValue as boolean
const presentLyricsValue = await getStoredValue('presentLyrics', false)
presentLyrics.value = presentLyricsValue as boolean
isLoaded.value = true
addDebugInfo(`✅ 偏好设置初始化完成: displayTimeLeft = ${value}`)
} catch (error) {
addDebugInfo(`❌ 初始化失败: ${error}`)
displayTimeLeft.value = false
@ -174,6 +177,15 @@ export const usePreferences = defineStore('preferences', () => {
}
}
})
watch(presentLyrics, async (val) => {
if (isLoaded.value) {
try {
await setStoredValue('presentLyrics', val)
} catch (error) {
addDebugInfo(`❌ 监听器保存失败: ${error}`)
}
}
})
// 手动保存函数(用于调试)
const manualSave = async () => {
@ -204,6 +216,7 @@ export const usePreferences = defineStore('preferences', () => {
return {
displayTimeLeft,
presentLyrics,
isLoaded,
storageType,
debugInfo,