feat: add iOS togglefavorite action and enhance mobile player layout with tab navigation
This commit is contained in:
@@ -351,6 +351,20 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add togglefavorite action for iOS
|
||||||
|
try {
|
||||||
|
// togglefavorite is an iOS-specific action that may not be in TypeScript definitions
|
||||||
|
const mediaSession = navigator.mediaSession as MediaSession & {
|
||||||
|
setActionHandler(action: 'togglefavorite', handler: MediaSessionActionHandler | null): void;
|
||||||
|
};
|
||||||
|
mediaSession.setActionHandler('togglefavorite', () => {
|
||||||
|
toggleCurrentTrackStar();
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// togglefavorite might not be supported on all platforms
|
||||||
|
console.log('togglefavorite action not supported:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// Update position state for better scrubbing support
|
// Update position state for better scrubbing support
|
||||||
const updatePositionState = () => {
|
const updatePositionState = () => {
|
||||||
const audioCurrent = audioRef.current;
|
const audioCurrent = audioRef.current;
|
||||||
@@ -378,12 +392,20 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
navigator.mediaSession.setActionHandler('previoustrack', null);
|
navigator.mediaSession.setActionHandler('previoustrack', null);
|
||||||
navigator.mediaSession.setActionHandler('nexttrack', null);
|
navigator.mediaSession.setActionHandler('nexttrack', null);
|
||||||
navigator.mediaSession.setActionHandler('seekto', null);
|
navigator.mediaSession.setActionHandler('seekto', null);
|
||||||
|
try {
|
||||||
|
const mediaSession = navigator.mediaSession as MediaSession & {
|
||||||
|
setActionHandler(action: 'togglefavorite', handler: MediaSessionActionHandler | null): void;
|
||||||
|
};
|
||||||
|
mediaSession.setActionHandler('togglefavorite', null);
|
||||||
|
} catch (error) {
|
||||||
|
// togglefavorite might not be supported
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('MediaSession setup failed:', error);
|
console.error('MediaSession setup failed:', error);
|
||||||
}
|
}
|
||||||
}, [currentTrack, isPlaying, isClient, playPreviousTrack, playNextTrack, onTrackPlay, onTrackPause]);
|
}, [currentTrack, isPlaying, isClient, playPreviousTrack, playNextTrack, onTrackPlay, onTrackPause, toggleCurrentTrackStar]);
|
||||||
|
|
||||||
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
e.stopPropagation(); // Prevent triggering fullscreen
|
e.stopPropagation(); // Prevent triggering fullscreen
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
|
|||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { lrcLibClient } from '@/lib/lrclib';
|
import { lrcLibClient } from '@/lib/lrclib';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
import { useIsMobile } from '@/hooks/use-mobile';
|
||||||
import {
|
import {
|
||||||
FaPlay,
|
FaPlay,
|
||||||
FaPause,
|
FaPause,
|
||||||
@@ -34,8 +35,19 @@ interface FullScreenPlayerProps {
|
|||||||
onOpenQueue?: () => void;
|
onOpenQueue?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MobileTab = 'player' | 'lyrics' | 'queue';
|
||||||
|
|
||||||
export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onClose, onOpenQueue }) => {
|
export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onClose, onOpenQueue }) => {
|
||||||
const { currentTrack, playPreviousTrack, playNextTrack, shuffle, toggleShuffle, toggleCurrentTrackStar } = useAudioPlayer();
|
const {
|
||||||
|
currentTrack,
|
||||||
|
playPreviousTrack,
|
||||||
|
playNextTrack,
|
||||||
|
shuffle,
|
||||||
|
toggleShuffle,
|
||||||
|
toggleCurrentTrackStar,
|
||||||
|
queue
|
||||||
|
} = useAudioPlayer();
|
||||||
|
const isMobile = useIsMobile();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
@@ -47,6 +59,7 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
const [lyrics, setLyrics] = useState<LyricLine[]>([]);
|
const [lyrics, setLyrics] = useState<LyricLine[]>([]);
|
||||||
const [currentLyricIndex, setCurrentLyricIndex] = useState(-1);
|
const [currentLyricIndex, setCurrentLyricIndex] = useState(-1);
|
||||||
const [showLyrics, setShowLyrics] = useState(true);
|
const [showLyrics, setShowLyrics] = useState(true);
|
||||||
|
const [activeTab, setActiveTab] = useState<MobileTab>('player');
|
||||||
const lyricsRef = useRef<HTMLDivElement>(null);
|
const lyricsRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
// Load lyrics when track changes
|
// Load lyrics when track changes
|
||||||
@@ -287,8 +300,23 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
|
|
||||||
{/* Overlay for better contrast */}
|
{/* Overlay for better contrast */}
|
||||||
<div className="absolute inset-0 bg-black/50" />
|
<div className="absolute inset-0 bg-black/50" />
|
||||||
<div className="relative h-full w-full">
|
<div className="relative h-full w-full flex flex-col">
|
||||||
{/* Floating Header */}
|
|
||||||
|
{/* Mobile Close Handle */}
|
||||||
|
{isMobile && (
|
||||||
|
<div className="flex justify-center py-4 px-4">
|
||||||
|
<div
|
||||||
|
onClick={onClose}
|
||||||
|
className="cursor-pointer px-8 py-3 -mx-8 -my-3"
|
||||||
|
style={{ touchAction: 'manipulation' }}
|
||||||
|
>
|
||||||
|
<div className="w-8 h-1 bg-gray-300 rounded-full opacity-60" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Desktop Header */}
|
||||||
|
{!isMobile && (
|
||||||
<div className="absolute top-0 right-0 z-10 p-4 lg:p-6">
|
<div className="absolute top-0 right-0 z-10 p-4 lg:p-6">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{onOpenQueue && (
|
{onOpenQueue && (
|
||||||
@@ -309,38 +337,283 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
<div className="h-full flex flex-col lg:flex-row gap-4 lg:gap-8 p-4 lg:p-6 overflow-hidden">
|
<div className="flex-1 overflow-hidden">
|
||||||
{/* Left Side - Album Art and Controls */}
|
{isMobile ? (
|
||||||
<div className="flex flex-col items-center justify-center min-h-0 flex-1 min-w-0">
|
/* Mobile Tab Content */
|
||||||
{/* Album Art */}
|
<div className="h-full flex flex-col">
|
||||||
<div className="relative mb-4 lg:mb-6 shrink-0">
|
<div className="flex-1 overflow-hidden">
|
||||||
|
{activeTab === 'player' && (
|
||||||
|
<div className="h-full flex flex-col justify-center items-center px-8 py-4">
|
||||||
|
{/* Smaller Album Art */}
|
||||||
|
<div className="relative mb-6 shrink-0">
|
||||||
<Image
|
<Image
|
||||||
src={currentTrack.coverArt || '/default-album.png'}
|
src={currentTrack.coverArt || '/default-album.png'}
|
||||||
alt={currentTrack.album}
|
alt={currentTrack.album}
|
||||||
width={320}
|
width={240}
|
||||||
height={320}
|
height={240}
|
||||||
className="w-56 h-56 sm:w-64 sm:h-64 lg:w-80 lg:h-80 rounded-lg shadow-2xl object-cover"
|
className={`rounded-lg shadow-2xl object-cover transition-all duration-300 ${
|
||||||
|
!isPlaying ? 'w-48 h-48 opacity-70 scale-95' : 'w-60 h-60'
|
||||||
|
}`}
|
||||||
priority
|
priority
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Track Info */}
|
{/* Track Info - Left Aligned and Heart on Same Line */}
|
||||||
<div className="text-center mb-4 lg:mb-6 px-4 shrink-0 max-w-full">
|
<div className="w-full mb-6 shrink-0">
|
||||||
<h1 className="text-lg sm:text-xl lg:text-3xl font-bold text-foreground mb-2 line-clamp-2 leading-tight">
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<h1 className="text-2xl font-bold text-foreground line-clamp-1 flex-1 text-left">
|
||||||
{currentTrack.name}
|
{currentTrack.name}
|
||||||
</h1>
|
</h1>
|
||||||
<Link href={`/artist/${currentTrack.artistId}`} className="text-base sm:text-lg lg:text-xl text-foreground/80 mb-1 line-clamp-1">
|
<button
|
||||||
|
onClick={toggleCurrentTrackStar}
|
||||||
|
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors ml-3"
|
||||||
|
title={currentTrack?.starred ? 'Remove from favorites' : 'Add to favorites'}
|
||||||
|
>
|
||||||
|
<Heart
|
||||||
|
className={`w-6 h-6 ${currentTrack?.starred ? 'text-primary fill-primary' : 'text-gray-400'}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href={`/artist/${currentTrack.artistId}`}
|
||||||
|
className="text-lg text-foreground/80 line-clamp-1 block text-left mb-1"
|
||||||
|
>
|
||||||
{currentTrack.artist}
|
{currentTrack.artist}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={`/album/${currentTrack.albumId}`} className="text-sm sm:text-base lg:text-lg text-foreground/60 line-clamp-1 cursor-pointer hover:underline">
|
<Link
|
||||||
|
href={`/album/${currentTrack.albumId}`}
|
||||||
|
className="text-base text-foreground/60 line-clamp-1 cursor-pointer hover:underline block text-left"
|
||||||
|
>
|
||||||
{currentTrack.album}
|
{currentTrack.album}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Progress */}
|
{/* Progress */}
|
||||||
<div className="w-full max-w-sm lg:max-w-md mb-4 lg:mb-6 px-4 shrink-0">
|
<div className="w-full mb-4 shrink-0">
|
||||||
|
<div className="w-full" onClick={handleSeek}>
|
||||||
|
<Progress value={progress} className="h-2 cursor-pointer" />
|
||||||
|
</div>
|
||||||
|
{/* Time below progress on mobile */}
|
||||||
|
<div className="flex justify-between text-sm text-foreground/60 mt-2">
|
||||||
|
<span>{formatTime(currentTime)}</span>
|
||||||
|
<span>{formatTime(duration)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Controls */}
|
||||||
|
<div className="flex items-center gap-6 mb-4 shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={toggleShuffle}
|
||||||
|
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${
|
||||||
|
shuffle ? 'text-primary bg-primary/20' : 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
title={shuffle ? 'Shuffle On - Queue is shuffled' : 'Shuffle Off - Click to shuffle queue'}
|
||||||
|
>
|
||||||
|
<FaShuffle className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={playPreviousTrack}
|
||||||
|
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
|
||||||
|
<FaBackward className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={togglePlayPause}
|
||||||
|
className="p-4 hover:bg-gray-700/50 rounded-full transition-colors">
|
||||||
|
{isPlaying ? (
|
||||||
|
<FaPause className="w-10 h-10" />
|
||||||
|
) : (
|
||||||
|
<FaPlay className="w-10 h-10" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={playNextTrack}
|
||||||
|
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
|
||||||
|
<FaForward className="w-6 h-6" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onMouseEnter={() => setShowVolumeSlider(true)}
|
||||||
|
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
|
||||||
|
{volume === 0 ? (
|
||||||
|
<FaVolumeXmark className="w-5 h-5" />
|
||||||
|
) : (
|
||||||
|
<FaVolumeHigh className="w-5 h-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Volume Slider */}
|
||||||
|
{showVolumeSlider && (
|
||||||
|
<div
|
||||||
|
className="w-32 mb-4"
|
||||||
|
onMouseLeave={() => setShowVolumeSlider(false)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
min="0"
|
||||||
|
max="100"
|
||||||
|
value={volume * 100}
|
||||||
|
onChange={handleVolumeChange}
|
||||||
|
className="w-full accent-foreground"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'lyrics' && lyrics.length > 0 && (
|
||||||
|
<div className="h-full flex flex-col px-4">
|
||||||
|
<ScrollArea className="flex-1">
|
||||||
|
<div className="space-y-3 py-4" ref={lyricsRef}>
|
||||||
|
{lyrics.map((line, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
data-lyric-index={index}
|
||||||
|
onClick={() => handleLyricClick(line.time)}
|
||||||
|
className={`text-base leading-relaxed transition-all duration-300 break-words cursor-pointer hover:text-foreground px-2 ${
|
||||||
|
index === currentLyricIndex
|
||||||
|
? 'text-foreground font-bold text-xl'
|
||||||
|
: index < currentLyricIndex
|
||||||
|
? 'text-foreground/60'
|
||||||
|
: 'text-foreground/40'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
wordWrap: 'break-word',
|
||||||
|
overflowWrap: 'break-word',
|
||||||
|
hyphens: 'auto',
|
||||||
|
paddingBottom: '4px'
|
||||||
|
}}
|
||||||
|
title={`Click to jump to ${formatTime(line.time)}`}
|
||||||
|
>
|
||||||
|
{line.text || '♪'}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div style={{ height: '200px' }} />
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{activeTab === 'queue' && (
|
||||||
|
<div className="h-full flex flex-col px-4">
|
||||||
|
<ScrollArea className="flex-1">
|
||||||
|
<div className="space-y-2 py-4">
|
||||||
|
{queue.map((track, index) => (
|
||||||
|
<div
|
||||||
|
key={`${track.id}-${index}`}
|
||||||
|
className={`flex items-center p-3 rounded-lg ${
|
||||||
|
track.id === currentTrack?.id ? 'bg-primary/20' : 'bg-gray-800/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={track.coverArt || '/default-album.png'}
|
||||||
|
alt={track.album}
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
className="rounded mr-3"
|
||||||
|
/>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="font-medium text-sm truncate">
|
||||||
|
{track.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-gray-400 truncate">
|
||||||
|
{track.artist}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</ScrollArea>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Tab Bar */}
|
||||||
|
<div className="flex-shrink-0 border-t border-gray-700/50 bg-black/80 backdrop-blur-sm">
|
||||||
|
<div className="flex justify-around py-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('player')}
|
||||||
|
className={`flex flex-col items-center p-3 rounded-lg transition-colors ${
|
||||||
|
activeTab === 'player' ? 'text-primary bg-primary/20' : 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FaPlay className="w-5 h-5 mb-1" />
|
||||||
|
<span className="text-xs">Player</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{lyrics.length > 0 && (
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('lyrics')}
|
||||||
|
className={`flex flex-col items-center p-3 rounded-lg transition-colors ${
|
||||||
|
activeTab === 'lyrics' ? 'text-primary bg-primary/20' : 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FaQuoteLeft className="w-5 h-5 mb-1" />
|
||||||
|
<span className="text-xs">Lyrics</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('queue')}
|
||||||
|
className={`flex flex-col items-center p-3 rounded-lg transition-colors ${
|
||||||
|
activeTab === 'queue' ? 'text-primary bg-primary/20' : 'text-gray-400'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<FaListUl className="w-5 h-5 mb-1" />
|
||||||
|
<span className="text-xs">Queue</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
/* Desktop Layout */
|
||||||
|
<div className="h-full flex flex-row gap-8 p-6 overflow-hidden">
|
||||||
|
{/* Left Side - Album Art and Controls */}
|
||||||
|
<div className="flex flex-col items-center justify-center min-h-0 flex-1 min-w-0">
|
||||||
|
{/* Album Art */}
|
||||||
|
<div className="relative mb-6 shrink-0">
|
||||||
|
<Image
|
||||||
|
src={currentTrack.coverArt || '/default-album.png'}
|
||||||
|
alt={currentTrack.album}
|
||||||
|
width={320}
|
||||||
|
height={320}
|
||||||
|
className="w-80 h-80 rounded-lg shadow-2xl object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Track Info */}
|
||||||
|
<div className="text-center mb-6 px-4 shrink-0 max-w-full">
|
||||||
|
<div className="flex items-center justify-center gap-4 mb-2">
|
||||||
|
<h1 className="text-3xl font-bold text-foreground line-clamp-2 leading-tight">
|
||||||
|
{currentTrack.name}
|
||||||
|
</h1>
|
||||||
|
<button
|
||||||
|
onClick={toggleCurrentTrackStar}
|
||||||
|
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors"
|
||||||
|
title={currentTrack?.starred ? 'Remove from favorites' : 'Add to favorites'}
|
||||||
|
>
|
||||||
|
<Heart
|
||||||
|
className={`w-6 h-6 ${currentTrack?.starred ? 'text-primary fill-primary' : 'text-gray-400'}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Link href={`/artist/${currentTrack.artistId}`} className="text-xl text-foreground/80 mb-1 line-clamp-1">
|
||||||
|
{currentTrack.artist}
|
||||||
|
</Link>
|
||||||
|
<Link href={`/album/${currentTrack.albumId}`} className="text-lg text-foreground/60 line-clamp-1 cursor-pointer hover:underline">
|
||||||
|
{currentTrack.album}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress */}
|
||||||
|
<div className="w-full max-w-md mb-6 px-4 shrink-0">
|
||||||
<div className="w-full" onClick={handleSeek}>
|
<div className="w-full" onClick={handleSeek}>
|
||||||
<Progress value={progress} className="h-2 cursor-pointer" />
|
<Progress value={progress} className="h-2 cursor-pointer" />
|
||||||
</div>
|
</div>
|
||||||
@@ -351,7 +624,7 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Controls */}
|
{/* Controls */}
|
||||||
<div className="flex items-center gap-3 sm:gap-4 lg:gap-6 mb-4 lg:mb-6 shrink-0">
|
<div className="flex items-center gap-6 mb-6 shrink-0">
|
||||||
<button
|
<button
|
||||||
onClick={toggleShuffle}
|
onClick={toggleShuffle}
|
||||||
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${
|
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${
|
||||||
@@ -359,54 +632,41 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
}`}
|
}`}
|
||||||
title={shuffle ? 'Shuffle On - Queue is shuffled' : 'Shuffle Off - Click to shuffle queue'}
|
title={shuffle ? 'Shuffle On - Queue is shuffled' : 'Shuffle Off - Click to shuffle queue'}
|
||||||
>
|
>
|
||||||
<FaShuffle className="w-4 h-4 sm:w-5 sm:h-5" />
|
<FaShuffle className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={playPreviousTrack}
|
onClick={playPreviousTrack}
|
||||||
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
|
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
|
||||||
<FaBackward className="w-4 h-4 sm:w-5 sm:h-5" />
|
<FaBackward className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={togglePlayPause}
|
onClick={togglePlayPause}
|
||||||
className="p-3 hover:bg-gray-700/50 rounded-full transition-colors">
|
className="p-3 hover:bg-gray-700/50 rounded-full transition-colors">
|
||||||
{isPlaying ? (
|
{isPlaying ? (
|
||||||
<FaPause className="w-8 h-8 sm:w-10 sm:h-10" />
|
<FaPause className="w-10 h-10" />
|
||||||
) : (
|
) : (
|
||||||
<FaPlay className="w-8 h-8 sm:w-10 sm:h-10" />
|
<FaPlay className="w-10 h-10" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={playNextTrack}
|
onClick={playNextTrack}
|
||||||
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
|
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
|
||||||
<FaForward className="w-4 h-4 sm:w-5 sm:h-5" />
|
<FaForward className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={toggleCurrentTrackStar}
|
|
||||||
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors"
|
|
||||||
title={currentTrack?.starred ? 'Remove from favorites' : 'Add to favorites'}
|
|
||||||
>
|
|
||||||
<Heart
|
|
||||||
className={`w-4 h-4 sm:w-5 sm:h-5 ${currentTrack?.starred ? 'text-primary fill-primary' : 'text-gray-400'}`}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Volume and Lyrics Toggle */}
|
{/* Volume and Lyrics Toggle - Desktop Only */}
|
||||||
<div className="flex items-center gap-3 shrink-0 justify-center">
|
<div className="flex items-center gap-3 shrink-0 justify-center">
|
||||||
<button
|
<button
|
||||||
onMouseEnter={() => setShowVolumeSlider(true)}
|
onMouseEnter={() => setShowVolumeSlider(true)}
|
||||||
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
|
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors">
|
||||||
{volume === 0 ? (
|
{volume === 0 ? (
|
||||||
<FaVolumeXmark className="w-4 h-4 sm:w-5 sm:h-5" />
|
<FaVolumeXmark className="w-5 h-5" />
|
||||||
) : (
|
) : (
|
||||||
<FaVolumeHigh className="w-4 h-4 sm:w-5 sm:h-5" />
|
<FaVolumeHigh className="w-5 h-5" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
@@ -418,13 +678,13 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
}`}
|
}`}
|
||||||
title={showLyrics ? 'Hide Lyrics' : 'Show Lyrics'}
|
title={showLyrics ? 'Hide Lyrics' : 'Show Lyrics'}
|
||||||
>
|
>
|
||||||
<FaQuoteLeft className="w-4 h-4 sm:w-5 sm:h-5" />
|
<FaQuoteLeft className="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showVolumeSlider && (
|
{showVolumeSlider && (
|
||||||
<div
|
<div
|
||||||
className="w-16 sm:w-20 lg:w-24"
|
className="w-24"
|
||||||
onMouseLeave={() => setShowVolumeSlider(false)}
|
onMouseLeave={() => setShowVolumeSlider(false)}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
@@ -440,18 +700,18 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Right Side - Lyrics */}
|
{/* Right Side - Lyrics (Desktop Only) */}
|
||||||
{showLyrics && lyrics.length > 0 && (
|
{showLyrics && lyrics.length > 0 && (
|
||||||
<div className="flex-1 min-w-0 min-h-0 flex flex-col" ref={lyricsRef}>
|
<div className="flex-1 min-w-0 min-h-0 flex flex-col" ref={lyricsRef}>
|
||||||
<div className="h-full flex flex-col">
|
<div className="h-full flex flex-col">
|
||||||
<ScrollArea className="flex-1 min-h-0">
|
<ScrollArea className="flex-1 min-h-0">
|
||||||
<div className="space-y-2 sm:space-y-3 pl-4 pr-4 py-4">
|
<div className="space-y-3 pl-4 pr-4 py-4">
|
||||||
{lyrics.map((line, index) => (
|
{lyrics.map((line, index) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
data-lyric-index={index}
|
data-lyric-index={index}
|
||||||
onClick={() => handleLyricClick(line.time)}
|
onClick={() => handleLyricClick(line.time)}
|
||||||
className={`text-sm sm:text-base lg:text-base leading-relaxed transition-all duration-300 break-words cursor-pointer hover:text-foreground ${
|
className={`text-base leading-relaxed transition-all duration-300 break-words cursor-pointer hover:text-foreground ${
|
||||||
index === currentLyricIndex
|
index === currentLyricIndex
|
||||||
? 'text-foreground font-bold text-2xl'
|
? 'text-foreground font-bold text-2xl'
|
||||||
: index < currentLyricIndex
|
: index < currentLyricIndex
|
||||||
@@ -470,7 +730,6 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
{line.text || '♪'}
|
{line.text || '♪'}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{/* Add extra padding at the bottom to allow last lyric to center */}
|
|
||||||
<div style={{ height: '200px' }} />
|
<div style={{ height: '200px' }} />
|
||||||
</div>
|
</div>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@@ -478,6 +737,8 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user