feat: enhance FullScreenPlayer with improved lyrics scrolling and toggle functionality
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user