feat: enhance FullScreenPlayer with improved lyrics scrolling and toggle functionality

This commit is contained in:
2025-06-19 20:47:01 +00:00
committed by GitHub
parent 52bcc81068
commit 0560114d98

View File

@@ -14,7 +14,8 @@ import {
FaVolumeXmark, FaVolumeXmark,
FaShuffle, FaShuffle,
FaRepeat, FaRepeat,
FaXmark FaXmark,
FaQuoteLeft
} from "react-icons/fa6"; } from "react-icons/fa6";
import { Card, CardContent } from '@/components/ui/card'; import { Card, CardContent } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
@@ -80,32 +81,73 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
setCurrentLyricIndex(newIndex); setCurrentLyricIndex(newIndex);
}, [lyrics, currentTime]); }, [lyrics, currentTime]);
// Auto-scroll lyrics to center current line // Auto-scroll lyrics to center current line without cutting off text
useEffect(() => { useEffect(() => {
if (currentLyricIndex >= 0 && lyrics.length > 0) { if (currentLyricIndex >= 0 && lyrics.length > 0 && showLyrics) {
const lyricsContainer = document.querySelector('.lyrics-container'); // Use a small delay to ensure the DOM is updated
if (lyricsContainer) { const scrollTimeout = setTimeout(() => {
const currentLyricElement = lyricsContainer.children[currentLyricIndex] as HTMLElement; const lyricsScrollArea = document.querySelector('[data-radix-scroll-area-viewport]');
if (currentLyricElement) { if (lyricsScrollArea) {
currentLyricElement.scrollIntoView({ const currentLyricElement = lyricsScrollArea.querySelector(`[data-lyric-index="${currentLyricIndex}"]`) as HTMLElement;
behavior: 'smooth', if (currentLyricElement) {
block: 'center', const containerHeight = lyricsScrollArea.clientHeight;
inline: 'nearest' const elementHeight = currentLyricElement.offsetHeight;
}); const elementOffsetTop = currentLyricElement.offsetTop;
// Calculate scroll position to center the current lyric
const targetScrollTop = elementOffsetTop - (containerHeight / 2) + (elementHeight / 2);
lyricsScrollArea.scrollTo({
top: Math.max(0, targetScrollTop),
behavior: 'smooth'
});
}
} }
} }, 50);
return () => clearTimeout(scrollTimeout);
} }
}, [currentLyricIndex, lyrics.length]); }, [currentLyricIndex, lyrics.length, showLyrics]);
// Reset lyrics to top when song ends or changes
useEffect(() => {
if (currentTrack && showLyrics) {
const lyricsScrollArea = document.querySelector('[data-radix-scroll-area-viewport]');
if (lyricsScrollArea) {
lyricsScrollArea.scrollTo({
top: 0,
behavior: 'smooth'
});
}
setCurrentLyricIndex(-1);
}
}, [currentTrack, showLyrics]);
// Sync with main audio player // Sync with main audio player
useEffect(() => { useEffect(() => {
const syncWithMainPlayer = () => { const syncWithMainPlayer = () => {
const mainAudio = document.querySelector('audio') as HTMLAudioElement; const mainAudio = document.querySelector('audio') as HTMLAudioElement;
if (mainAudio && currentTrack) { if (mainAudio && currentTrack) {
setCurrentTime(mainAudio.currentTime); const newCurrentTime = mainAudio.currentTime;
setDuration(mainAudio.duration || 0); const newDuration = mainAudio.duration || 0;
setProgress(mainAudio.duration ? (mainAudio.currentTime / mainAudio.duration) * 100 : 0); const newIsPlaying = !mainAudio.paused;
setIsPlaying(!mainAudio.paused);
// Check if song ended (reset lyrics to top)
if (newCurrentTime === 0 && !newIsPlaying && currentTime > 0) {
const lyricsScrollArea = document.querySelector('[data-radix-scroll-area-viewport]');
if (lyricsScrollArea) {
lyricsScrollArea.scrollTo({
top: 0,
behavior: 'smooth'
});
}
setCurrentLyricIndex(-1);
}
setCurrentTime(newCurrentTime);
setDuration(newDuration);
setProgress(newDuration ? (newCurrentTime / newDuration) * 100 : 0);
setIsPlaying(newIsPlaying);
setVolume(mainAudio.volume); setVolume(mainAudio.volume);
} }
}; };
@@ -118,7 +160,7 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
const interval = setInterval(syncWithMainPlayer, 100); const interval = setInterval(syncWithMainPlayer, 100);
return () => clearInterval(interval); return () => clearInterval(interval);
} }
}, [isOpen, currentTrack]); }, [isOpen, currentTrack, currentTime]);
// Extract dominant color from cover art // Extract dominant color from cover art
useEffect(() => { useEffect(() => {
@@ -286,7 +328,7 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
</button> </button>
</div> </div>
{/* Volume */} {/* Volume and Lyrics Toggle */}
<div className="flex items-center gap-3 flex-shrink-0"> <div className="flex items-center gap-3 flex-shrink-0">
<button <button
onMouseEnter={() => setShowVolumeSlider(true)} onMouseEnter={() => setShowVolumeSlider(true)}
@@ -298,6 +340,18 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
)} )}
</button> </button>
{lyrics.length > 0 && (
<button
onClick={() => setShowLyrics(!showLyrics)}
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${
showLyrics ? 'text-primary' : 'text-gray-500'
}`}
title={showLyrics ? 'Hide Lyrics' : 'Show Lyrics'}
>
<FaQuoteLeft className="w-5 h-5" />
</button>
)}
{showVolumeSlider && ( {showVolumeSlider && (
<div <div
className="w-20 lg:w-24" className="w-20 lg:w-24"
@@ -319,50 +373,35 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
{/* Right Side - Lyrics */} {/* Right Side - Lyrics */}
{showLyrics && lyrics.length > 0 && ( {showLyrics && lyrics.length > 0 && (
<div className="flex-1 lg:max-w-md min-h-0"> <div className="flex-1 lg:max-w-md min-h-0">
<Card className="bg-black/30 backdrop-blur-sm border-white/20 h-full"> <div className="h-full flex flex-col">
<CardContent className="p-4 lg:p-6 h-full flex flex-col"> <ScrollArea className="flex-1 min-h-0">
<div className="flex items-center justify-between mb-4 flex-shrink-0"> <div className="space-y-4 pr-4 px-2">
<h3 className="text-lg font-semibold text-foreground">Lyrics</h3> {lyrics.map((line, index) => (
<button <div
onClick={() => setShowLyrics(false)} key={index}
className="text-foreground/60 hover:bg-foreground/20" data-lyric-index={index}
> className={`text-sm lg:text-base leading-relaxed transition-all duration-300 break-words ${
Hide index === currentLyricIndex
</button> ? 'text-primary font-semibold text-lg lg:text-xl scale-105'
: index < currentLyricIndex
? 'text-primary/60'
: 'text-primary/40'
}`}
style={{
wordWrap: 'break-word',
overflowWrap: 'break-word',
hyphens: 'auto',
paddingBottom: '8px'
}}
>
{line.text || '♪'}
</div>
))}
{/* Add extra padding at the bottom to allow last lyric to center */}
<div style={{ height: '200px' }} />
</div> </div>
</ScrollArea>
<ScrollArea className="flex-1 min-h-0"> </div>
<div className="space-y-3 pr-4">
{lyrics.map((line, index) => (
<div
key={index}
className={`text-sm leading-relaxed transition-all duration-300 ${
index === currentLyricIndex
? 'text-foreground font-semibold text-base scale-105'
: index < currentLyricIndex
? 'text-foreground/60'
: 'text-foreground/40'
}`}
>
{line.text || '♪'}
</div>
))}
</div>
</ScrollArea>
</CardContent>
</Card>
</div>
)}
{/* Show Lyrics button when hidden */}
{!showLyrics && lyrics.length > 0 && (
<div className="lg:flex-1 lg:max-w-md flex items-start justify-center lg:justify-start pt-4 lg:pt-8 flex-shrink-0">
<button
onClick={() => setShowLyrics(true)}
className="bg-foreground/20 hover:bg-foreground/30 text-foreground backdrop-blur-sm"
>
Show Lyrics
</button>
</div> </div>
)} )}
</div> </div>