From 6bf9c493aaa58ea9dd3f4aa90bc5c68882d2cd42 Mon Sep 17 00:00:00 2001 From: Astrian Zheng Date: Mon, 26 May 2025 17:49:26 +1000 Subject: [PATCH] feat(ScrollingLyrics): enhance lyrics display with scroll indicators, loading states, and user interactions --- src/components/ScrollingLyrics.vue | 559 ++++++++++++++++++++++++++--- src/pages/Playroom.vue | 279 +++++++++----- 2 files changed, 704 insertions(+), 134 deletions(-) diff --git a/src/components/ScrollingLyrics.vue b/src/components/ScrollingLyrics.vue index 3093d4b..37b2462 100644 --- a/src/components/ScrollingLyrics.vue +++ b/src/components/ScrollingLyrics.vue @@ -1,44 +1,209 @@ + + - \ No newline at end of file + \ No newline at end of file diff --git a/src/pages/Playroom.vue b/src/pages/Playroom.vue index aec5624..4f632fe 100644 --- a/src/pages/Playroom.vue +++ b/src/pages/Playroom.vue @@ -3,7 +3,7 @@ import { usePlayQueueStore } from '../stores/usePlayQueueStore' import { artistsOrganize } from '../utils' import gsap from 'gsap' import { Draggable } from "gsap/Draggable" -import { onMounted } from 'vue' +import { onMounted, onUnmounted } from 'vue' import { useTemplateRef } from 'vue' import { ref, watch } from 'vue' import { usePreferences } from '../stores/usePreferences' @@ -34,8 +34,27 @@ const progressBarContainer = useTemplateRef('progressBarContainer') const playQueueDialogContainer = useTemplateRef('playQueueDialogContainer') const playQueueDialog = useTemplateRef('playQueueDialog') const controllerRef = useTemplateRef('controllerRef') +const mainContainer = useTemplateRef('mainContainer') +const lyricsSection = useTemplateRef('lyricsSection') +const scrollingLyrics = useTemplateRef('scrollingLyrics') +const albumCover = useTemplateRef('albumCover') +const songInfo = useTemplateRef('songInfo') +const progressSection = useTemplateRef('progressSection') +const controlButtons = useTemplateRef('controlButtons') + +const favoriteButton = useTemplateRef('favoriteButton') +const volumeButton = useTemplateRef('volumeButton') +const queueButton = useTemplateRef('queueButton') +const prevButton = useTemplateRef('prevButton') +const playButton = useTemplateRef('playButton') +const nextButton = useTemplateRef('nextButton') +const lyricsButton = useTemplateRef('lyricsButton') +const moreButton = useTemplateRef('moreButton') const presentQueueListDialog = ref(false) +const controllerHeight = ref(500) +let resizeObserver: ResizeObserver | null = null +let heightSyncTween: gsap.core.Tween | null = null onMounted(async () => { Draggable.create(progressBarThumb.value, { @@ -49,6 +68,9 @@ onMounted(async () => { } }) thumbUpdate() + + setupHeightSync() + setupEntranceAnimations() }) function timeFormatter(time: number) { @@ -103,41 +125,110 @@ function playPrevious() { } } +function setupHeightSync() { + if (controllerRef.value && lyricsSection.value) { + updateLyricsHeight() + + resizeObserver = new ResizeObserver(() => { + updateLyricsHeight() + }) + resizeObserver.observe(controllerRef.value) + } +} + +function updateLyricsHeight() { + if (!controllerRef.value || !lyricsSection.value) return + + const newHeight = controllerRef.value.offsetHeight + + if (Math.abs(newHeight - controllerHeight.value) > 5) { + controllerHeight.value = newHeight + + if (heightSyncTween) { + heightSyncTween.kill() + } + + heightSyncTween = gsap.to(lyricsSection.value, { + height: newHeight, + duration: 0.3, + ease: "power2.out" + }) + } +} + +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 }, + { opacity: 1, x: 0, duration: 0.8, ease: "power2.out", delay: 0.3 } + ) + } +} + +function handlePlayPause() { + if (playButton.value) { + gsap.to(playButton.value, { + scale: 0.9, duration: 0.1, yoyo: true, repeat: 1, + ease: "power2.inOut", + onComplete: () => { + playQueueStore.isPlaying = !playQueueStore.isPlaying + } + }) + } +} + +function toggleShuffle() { + playQueueStore.playMode.shuffle = !playQueueStore.playMode.shuffle + playQueueStore.shuffleCurrent = false +} + +function toggleRepeat() { + switch (playQueueStore.playMode.repeat) { + case 'off': playQueueStore.playMode.repeat = 'all'; break + case 'all': playQueueStore.playMode.repeat = 'single'; break + case 'single': playQueueStore.playMode.repeat = 'off'; break + } +} + function makePlayQueueListPresent() { presentQueueListDialog.value = true + const tl = gsap.timeline() - tl.from(playQueueDialog.value, { - marginLeft: '-24rem', - duration: 0.2, - ease: 'power2.out' - }).from(playQueueDialogContainer.value, { - backgroundColor: 'transparent', - duration: 0.2, - ease: 'power2.out' - }, '<') + 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').fromTo(playQueueDialog.value.children, + { opacity: 0, x: -20 }, + { opacity: 1, x: 0, duration: 0.3, ease: 'power2.out', stagger: 0.05 }, '<0.2') } function makePlayQueueListDismiss() { const tl = gsap.timeline({ onComplete: () => { presentQueueListDialog.value = false + gsap.set(playQueueDialog.value, { x: -384 }) + gsap.set(playQueueDialogContainer.value, { backgroundColor: 'transparent' }) } }) - tl.to(playQueueDialog.value, { - marginLeft: '-24rem', - duration: 0.2, - ease: 'power2.out' - }).to(playQueueDialogContainer.value, { - backgroundColor: 'transparent', - duration: 0.2, - ease: 'power2.out' - }).set(playQueueDialogContainer.value, { - backgroundColor: '#17171780', - ease: 'power2.out' - }).set(playQueueDialog.value, { - marginLeft: '0rem', - ease: 'power2.out' - }) + + tl.to(playQueueDialog.value.children, { + opacity: 0, x: -20, duration: 0.2, ease: 'power2.in', stagger: 0.03 + }).to(playQueueDialog.value, { + x: -384, duration: 0.3, ease: 'power2.in' + }, '<0.1').to(playQueueDialogContainer.value, { + backgroundColor: 'transparent', duration: 0.2, ease: 'power2.in' + }, '<') } function getCurrentTrack() { @@ -151,23 +242,53 @@ function getCurrentTrack() { function calculateStickyTop() { return (window.innerHeight - (controllerRef.value?.clientHeight ?? 0)) / 2 } + +onUnmounted(() => { + if (resizeObserver) resizeObserver.disconnect() + if (heightSyncTween) heightSyncTween.kill() +}) + +// New: Watch for track changes and animate +watch(() => playQueueStore.currentIndex, () => { + if (albumCover.value) { + gsap.to(albumCover.value, { + scale: 0.95, opacity: 0.7, duration: 0.2, + ease: "power2.inOut", yoyo: true, repeat: 1 + }) + } + + if (songInfo.value) { + gsap.fromTo(songInfo.value, + { opacity: 0, y: 10 }, + { opacity: 1, y: 0, duration: 0.4, ease: "power2.out", delay: 0.3 } + ) + } +})