Ui update

This commit is contained in:
2025-10-31 23:14:44 +11:00
parent b8fcefc4bc
commit cb469f2215
@@ -5,6 +5,7 @@ import { X, Play, Pause, SkipBack, SkipForward, Heart, ListPlus, Share2, Message
import { formatDuration } from '@/lib/utils' import { formatDuration } from '@/lib/utils'
import { useQuery } from '@tanstack/react-query' import { useQuery } from '@tanstack/react-query'
import { musicApi } from '@/api/client' import { musicApi } from '@/api/client'
import React from 'react'
interface FullScreenPlayerProps { interface FullScreenPlayerProps {
currentMusic: Music currentMusic: Music
@@ -43,7 +44,12 @@ export default function FullScreenPlayer({
onToggleLyrics, onToggleLyrics,
onSeek, onSeek,
}: FullScreenPlayerProps) { }: FullScreenPlayerProps) {
// Fetch lyrics when enabled const [userScrolling, setUserScrolling] = React.useState(false)
const scrollTimeoutRef = React.useRef<number>()
const lyricsContainerRef = React.useRef<HTMLDivElement>(null)
const lastScrollTop = React.useRef(0)
// Fetch lyrics when enabled with caching
const { data: lyrics, isLoading: lyricsLoading } = useQuery({ const { data: lyrics, isLoading: lyricsLoading } = useQuery({
queryKey: ['lyrics', currentMusic.id], queryKey: ['lyrics', currentMusic.id],
queryFn: async () => { queryFn: async () => {
@@ -51,44 +57,87 @@ export default function FullScreenPlayer({
return response.data.lyrics return response.data.lyrics
}, },
enabled: showLyrics && !!currentMusic.id, enabled: showLyrics && !!currentMusic.id,
staleTime: Infinity, // Cache lyrics forever
gcTime: 1000 * 60 * 60 * 24, // 24 hours
}) })
// Handle manual scroll
const handleScroll = React.useCallback((e: React.UIEvent<HTMLDivElement>) => {
const currentScrollTop = e.currentTarget.scrollTop
// Only set userScrolling if scroll position actually changed (user initiated)
if (Math.abs(currentScrollTop - lastScrollTop.current) > 1) {
setUserScrolling(true)
lastScrollTop.current = currentScrollTop
// Clear existing timeout
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current)
}
// Resume auto-scroll after 5 seconds of no scrolling
scrollTimeoutRef.current = window.setTimeout(() => {
setUserScrolling(false)
}, 5000)
}
}, [])
// Cleanup timeout on unmount or when lyrics disabled
React.useEffect(() => {
return () => {
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current)
}
}
}, [])
// Reset user scrolling when lyrics change or disabled
React.useEffect(() => {
if (!showLyrics) {
setUserScrolling(false)
if (scrollTimeoutRef.current) {
clearTimeout(scrollTimeoutRef.current)
}
}
}, [showLyrics, currentMusic.id])
return ( return (
<div className="fixed inset-0 z-50 bg-background/95 backdrop-blur-sm flex flex-col"> <div className="fixed inset-0 z-50 bg-background/95 backdrop-blur-sm flex flex-col">
{/* Main Content */} {/* Main Content */}
<div className="flex-1 flex flex-col items-center justify-center p-8 overflow-y-auto"> <div className="flex-1 flex flex-col items-center justify-center p-8 overflow-y-auto">
{/* Large Album Art */} {/* Large Album Art */}
<div className="w-full max-w-lg mb-8"> <div className={`w-full max-w-lg ${showLyrics ? 'mb-4 md:mb-8' : 'mb-8'}`}>
{currentMusic.thumbnail ? ( {currentMusic.thumbnail ? (
<img <img
src={currentMusic.thumbnail.startsWith('http') ? currentMusic.thumbnail : `/music/${currentMusic.thumbnail}`} src={currentMusic.thumbnail.startsWith('http') ? currentMusic.thumbnail : `/music/${currentMusic.thumbnail}`}
alt={currentMusic.title} alt={currentMusic.title}
className="w-full aspect-square rounded-2xl object-cover shadow-2xl" className={`w-full aspect-square rounded-2xl object-cover shadow-2xl ${showLyrics ? 'max-w-xs md:max-w-lg mx-auto' : ''}`}
/> />
) : ( ) : (
<div className="w-full aspect-square rounded-2xl bg-secondary flex items-center justify-center shadow-2xl"> <div className={`w-full aspect-square rounded-2xl bg-secondary flex items-center justify-center shadow-2xl ${showLyrics ? 'max-w-xs md:max-w-lg mx-auto' : ''}`}>
<Play className="h-32 w-32 text-muted-foreground" /> <Play className={showLyrics ? 'h-24 w-24 md:h-32 md:w-32 text-muted-foreground' : 'h-32 w-32 text-muted-foreground'} />
</div> </div>
)} )}
</div> </div>
{/* Song Info */} {/* Song Info */}
<div className="text-center mb-6 max-w-lg w-full"> <div className={`text-center max-w-lg w-full ${showLyrics ? 'mb-3 md:mb-6' : 'mb-6'}`}>
<h1 className="text-3xl font-bold mb-2">{currentMusic.title}</h1> <h1 className={showLyrics ? 'text-2xl md:text-3xl font-bold mb-1 md:mb-2' : 'text-3xl font-bold mb-2'}>{currentMusic.title}</h1>
{currentMusic.artist && currentMusic.artist !== 'Unknown' ? ( {currentMusic.artist && currentMusic.artist !== 'Unknown' ? (
<button <button
onClick={onNavigateToArtist} onClick={onNavigateToArtist}
className="text-xl text-muted-foreground hover:underline" className={showLyrics ? 'text-lg md:text-xl text-muted-foreground hover:underline' : 'text-xl text-muted-foreground hover:underline'}
> >
{currentMusic.artist} {currentMusic.artist}
</button> </button>
) : ( ) : (
<p className="text-xl text-muted-foreground"> <p className={showLyrics ? 'text-lg md:text-xl text-muted-foreground' : 'text-xl text-muted-foreground'}>
{currentMusic.artist || 'Unknown Artist'} {currentMusic.artist || 'Unknown Artist'}
</p> </p>
)} )}
{currentMusic.album && ( {currentMusic.album && (
<p className="text-sm text-muted-foreground mt-1">{currentMusic.album}</p> <p className={showLyrics ? 'text-xs md:text-sm text-muted-foreground mt-1' : 'text-sm text-muted-foreground mt-1'}>{currentMusic.album}</p>
)} )}
</div> </div>
@@ -98,21 +147,27 @@ export default function FullScreenPlayer({
{lyricsLoading ? ( {lyricsLoading ? (
<p className="text-center text-foreground py-8 md:py-12 text-lg md:text-xl">Loading lyrics...</p> <p className="text-center text-foreground py-8 md:py-12 text-lg md:text-xl">Loading lyrics...</p>
) : lyrics && lyrics.length > 0 ? ( ) : lyrics && lyrics.length > 0 ? (
<div className="relative h-64 md:h-96 overflow-hidden p-2 md:p-4"> <div
ref={lyricsContainerRef}
className="relative h-80 md:h-96 overflow-y-auto p-2 md:p-4 scrollbar-hide"
onScroll={handleScroll}
>
<div <div
className="lyrics-scroll-content" className={userScrolling ? '' : 'lyrics-scroll-content'}
style={{ style={{
animationDuration: `${Math.max(lyrics.split('\n').length * 1.5, 30)}s`, animationDuration: userScrolling ? undefined : `${Math.max(lyrics.split('\n').length * 1.5, 30)}s`,
animationPlayState: isPlaying ? 'running' : 'paused' animationPlayState: userScrolling ? 'paused' : (isPlaying ? 'running' : 'paused'),
}} }}
> >
<div className="whitespace-pre-wrap text-center text-base md:text-xl leading-relaxed font-normal text-foreground py-4 md:py-8 px-2"> <div className="whitespace-pre-wrap text-center text-base md:text-xl leading-relaxed font-normal text-foreground py-4 md:py-8 px-2">
{lyrics} {lyrics}
</div> </div>
{/* Duplicate for seamless loop */} {/* Duplicate for seamless loop */}
<div className="whitespace-pre-wrap text-center text-base md:text-xl leading-relaxed font-normal text-foreground py-4 md:py-8 px-2"> {!userScrolling && (
{lyrics} <div className="whitespace-pre-wrap text-center text-base md:text-xl leading-relaxed font-normal text-foreground py-4 md:py-8 px-2">
</div> {lyrics}
</div>
)}
</div> </div>
{/* Gradient overlays for fade effect */} {/* Gradient overlays for fade effect */}
<div className="absolute top-0 left-0 right-0 h-16 md:h-20 bg-gradient-to-b from-background to-transparent pointer-events-none" /> <div className="absolute top-0 left-0 right-0 h-16 md:h-20 bg-gradient-to-b from-background to-transparent pointer-events-none" />
@@ -144,26 +199,26 @@ export default function FullScreenPlayer({
</div> </div>
{/* Controls */} {/* Controls */}
<div className="flex items-center gap-6 mb-6"> <div className={`flex items-center mb-6 ${showLyrics ? 'gap-4 md:gap-6' : 'gap-6'}`}>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={onPrevious} onClick={onPrevious}
className="h-12 w-12" className={showLyrics ? 'h-10 w-10 md:h-12 md:w-12' : 'h-12 w-12'}
> >
<SkipBack className="h-6 w-6" /> <SkipBack className={showLyrics ? 'h-5 w-5 md:h-6 md:w-6' : 'h-6 w-6'} />
</Button> </Button>
<Button <Button
variant="default" variant="default"
size="icon" size="icon"
onClick={onTogglePlay} onClick={onTogglePlay}
className="h-16 w-16" className={showLyrics ? 'h-12 w-12 md:h-16 md:w-16' : 'h-16 w-16'}
> >
{isPlaying ? ( {isPlaying ? (
<Pause className="h-8 w-8" /> <Pause className={showLyrics ? 'h-6 w-6 md:h-8 md:w-8' : 'h-8 w-8'} />
) : ( ) : (
<Play className="h-8 w-8" /> <Play className={showLyrics ? 'h-6 w-6 md:h-8 md:w-8' : 'h-8 w-8'} />
)} )}
</Button> </Button>
@@ -171,9 +226,9 @@ export default function FullScreenPlayer({
variant="ghost" variant="ghost"
size="icon" size="icon"
onClick={onNext} onClick={onNext}
className="h-12 w-12" className={showLyrics ? 'h-10 w-10 md:h-12 md:w-12' : 'h-12 w-12'}
> >
<SkipForward className="h-6 w-6" /> <SkipForward className={showLyrics ? 'h-5 w-5 md:h-6 md:w-6' : 'h-6 w-6'} />
</Button> </Button>
</div> </div>