feat: add queue navigation to FullScreenPlayer and improve UI elements
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import React, { useEffect, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
|
import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
|
||||||
import { FullScreenPlayer } from '@/app/components/FullScreenPlayer';
|
import { FullScreenPlayer } from '@/app/components/FullScreenPlayer';
|
||||||
import { FaPlay, FaPause, FaVolumeHigh, FaForward, FaBackward, FaCompress, FaVolumeXmark, FaExpand } from "react-icons/fa6";
|
import { FaPlay, FaPause, FaVolumeHigh, FaForward, FaBackward, FaCompress, FaVolumeXmark, FaExpand } from "react-icons/fa6";
|
||||||
@@ -10,6 +11,7 @@ import { useToast } from '@/hooks/use-toast';
|
|||||||
|
|
||||||
export const AudioPlayer: React.FC = () => {
|
export const AudioPlayer: React.FC = () => {
|
||||||
const { currentTrack, playPreviousTrack, addToQueue, playNextTrack, clearQueue } = useAudioPlayer();
|
const { currentTrack, playPreviousTrack, addToQueue, playNextTrack, clearQueue } = useAudioPlayer();
|
||||||
|
const router = useRouter();
|
||||||
const audioRef = useRef<HTMLAudioElement>(null);
|
const audioRef = useRef<HTMLAudioElement>(null);
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
@@ -21,6 +23,11 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
const audioCurrent = audioRef.current;
|
const audioCurrent = audioRef.current;
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
|
||||||
|
const handleOpenQueue = () => {
|
||||||
|
setIsFullScreen(false);
|
||||||
|
router.push('/queue');
|
||||||
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsClient(true);
|
setIsClient(true);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -115,6 +122,66 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
};
|
};
|
||||||
}, [playNextTrack, currentTrack]);
|
}, [playNextTrack, currentTrack]);
|
||||||
|
|
||||||
|
// Media Session API integration
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isClient || !currentTrack || !('mediaSession' in navigator)) return;
|
||||||
|
|
||||||
|
// Set metadata
|
||||||
|
navigator.mediaSession.metadata = new MediaMetadata({
|
||||||
|
title: currentTrack.name,
|
||||||
|
artist: currentTrack.artist,
|
||||||
|
album: currentTrack.album,
|
||||||
|
artwork: currentTrack.coverArt ? [
|
||||||
|
{ src: currentTrack.coverArt, sizes: '512x512', type: 'image/jpeg' }
|
||||||
|
] : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set playback state
|
||||||
|
navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
|
||||||
|
|
||||||
|
// Set action handlers
|
||||||
|
navigator.mediaSession.setActionHandler('play', () => {
|
||||||
|
const audioCurrent = audioRef.current;
|
||||||
|
if (audioCurrent) {
|
||||||
|
audioCurrent.play();
|
||||||
|
setIsPlaying(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.mediaSession.setActionHandler('pause', () => {
|
||||||
|
const audioCurrent = audioRef.current;
|
||||||
|
if (audioCurrent) {
|
||||||
|
audioCurrent.pause();
|
||||||
|
setIsPlaying(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.mediaSession.setActionHandler('previoustrack', () => {
|
||||||
|
playPreviousTrack();
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', () => {
|
||||||
|
playNextTrack();
|
||||||
|
});
|
||||||
|
|
||||||
|
navigator.mediaSession.setActionHandler('seekto', (details) => {
|
||||||
|
const audioCurrent = audioRef.current;
|
||||||
|
if (audioCurrent && details.seekTime !== undefined) {
|
||||||
|
audioCurrent.currentTime = details.seekTime;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if ('mediaSession' in navigator) {
|
||||||
|
navigator.mediaSession.setActionHandler('play', null);
|
||||||
|
navigator.mediaSession.setActionHandler('pause', null);
|
||||||
|
navigator.mediaSession.setActionHandler('previoustrack', null);
|
||||||
|
navigator.mediaSession.setActionHandler('nexttrack', null);
|
||||||
|
navigator.mediaSession.setActionHandler('seekto', null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [currentTrack, isPlaying, isClient, playPreviousTrack, playNextTrack]);
|
||||||
|
|
||||||
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
const handleProgressClick = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
if (audioCurrent && currentTrack) {
|
if (audioCurrent && currentTrack) {
|
||||||
const rect = e.currentTarget.getBoundingClientRect();
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
@@ -254,6 +321,7 @@ export const AudioPlayer: React.FC = () => {
|
|||||||
<FullScreenPlayer
|
<FullScreenPlayer
|
||||||
isOpen={isFullScreen}
|
isOpen={isFullScreen}
|
||||||
onClose={() => setIsFullScreen(false)}
|
onClose={() => setIsFullScreen(false)}
|
||||||
|
onOpenQueue={handleOpenQueue}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ import {
|
|||||||
FaShuffle,
|
FaShuffle,
|
||||||
FaRepeat,
|
FaRepeat,
|
||||||
FaXmark,
|
FaXmark,
|
||||||
FaQuoteLeft
|
FaQuoteLeft,
|
||||||
|
FaListUl
|
||||||
} 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';
|
||||||
@@ -28,9 +29,10 @@ interface LyricLine {
|
|||||||
interface FullScreenPlayerProps {
|
interface FullScreenPlayerProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onOpenQueue?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onClose }) => {
|
export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onClose, onOpenQueue }) => {
|
||||||
const { currentTrack, playPreviousTrack, playNextTrack } = useAudioPlayer();
|
const { currentTrack, playPreviousTrack, playNextTrack } = useAudioPlayer();
|
||||||
const [progress, setProgress] = useState(0);
|
const [progress, setProgress] = useState(0);
|
||||||
const [isPlaying, setIsPlaying] = useState(false);
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
@@ -135,10 +137,10 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
}
|
}
|
||||||
}, [currentTrack?.id, showLyrics]); // Only reset when track ID changes
|
}, [currentTrack?.id, showLyrics]); // Only reset when track ID changes
|
||||||
|
|
||||||
// Sync with main audio player (throttled to prevent infinite loops)
|
// Sync with main audio player (improved responsiveness)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let lastUpdate = 0;
|
let lastUpdate = 0;
|
||||||
const throttleMs = 200; // Update at most every 200ms
|
const throttleMs = 100; // Update at most every 100ms for better responsiveness
|
||||||
|
|
||||||
const syncWithMainPlayer = () => {
|
const syncWithMainPlayer = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -151,8 +153,13 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
const newDuration = mainAudio.duration || 0;
|
const newDuration = mainAudio.duration || 0;
|
||||||
const newIsPlaying = !mainAudio.paused;
|
const newIsPlaying = !mainAudio.paused;
|
||||||
|
|
||||||
|
// Always update playing state for better responsiveness
|
||||||
|
if (newIsPlaying !== isPlaying) {
|
||||||
|
setIsPlaying(newIsPlaying);
|
||||||
|
}
|
||||||
|
|
||||||
// Only update state if values have changed significantly
|
// Only update state if values have changed significantly
|
||||||
if (Math.abs(newCurrentTime - currentTime) > 0.5) {
|
if (Math.abs(newCurrentTime - currentTime) > 0.3) {
|
||||||
setCurrentTime(newCurrentTime);
|
setCurrentTime(newCurrentTime);
|
||||||
}
|
}
|
||||||
if (Math.abs(newDuration - duration) > 0.1) {
|
if (Math.abs(newDuration - duration) > 0.1) {
|
||||||
@@ -164,9 +171,6 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
setProgress(newProgress);
|
setProgress(newProgress);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (newIsPlaying !== isPlaying) {
|
|
||||||
setIsPlaying(newIsPlaying);
|
|
||||||
}
|
|
||||||
if (Math.abs(mainAudio.volume - volume) > 0.01) {
|
if (Math.abs(mainAudio.volume - volume) > 0.01) {
|
||||||
setVolume(mainAudio.volume);
|
setVolume(mainAudio.volume);
|
||||||
}
|
}
|
||||||
@@ -177,11 +181,11 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
// Initial sync
|
// Initial sync
|
||||||
syncWithMainPlayer();
|
syncWithMainPlayer();
|
||||||
|
|
||||||
// Set up interval to keep syncing
|
// Set up interval to keep syncing - more frequent for better responsiveness
|
||||||
const interval = setInterval(syncWithMainPlayer, 100);
|
const interval = setInterval(syncWithMainPlayer, 50);
|
||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}
|
}
|
||||||
}, [isOpen, currentTrack]); // Removed currentTime from dependencies to prevent loop
|
}, [isOpen, currentTrack]); // Removed other dependencies to prevent loop
|
||||||
|
|
||||||
// Extract dominant color from cover art
|
// Extract dominant color from cover art
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -265,22 +269,47 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
|
|
||||||
if (!isOpen || !currentTrack) return null;
|
if (!isOpen || !currentTrack) return null;
|
||||||
|
|
||||||
const backgroundStyle = {
|
|
||||||
background: `linear-gradient(135deg, ${dominantColor}40 0%, ${dominantColor}20 50%, transparent 100%)`
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-50 bg-black bg-opacity-95 backdrop-blur-sm overflow-hidden">
|
<div className="fixed inset-0 z-50 bg-black overflow-hidden">
|
||||||
<div className="h-full w-full flex flex-col" style={backgroundStyle}>
|
{/* Blurred background image */}
|
||||||
|
{currentTrack.coverArt && (
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 w-full h-full"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${currentTrack.coverArt})`,
|
||||||
|
backgroundSize: '120%',
|
||||||
|
backgroundPosition: 'center',
|
||||||
|
backgroundRepeat: 'no-repeat',
|
||||||
|
filter: 'blur(20px) brightness(0.3)',
|
||||||
|
transform: 'scale(1.1)',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Overlay for better contrast */}
|
||||||
|
<div className="absolute inset-0 bg-black/50" />
|
||||||
|
|
||||||
|
<div className="relative h-full w-full flex flex-col">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between p-4 lg:p-6 flex-shrink-0">
|
<div className="flex items-center justify-between p-4 lg:p-6 flex-shrink-0">
|
||||||
<h2 className="text-lg lg:text-xl font-semibold text-white">Now Playing</h2>
|
<h2 className="text-lg lg:text-xl font-semibold text-white">Now Playing</h2>
|
||||||
<button
|
<div className="flex items-center gap-3">
|
||||||
onClick={onClose}
|
{onOpenQueue && (
|
||||||
className="text-white hover:bg-white/20"
|
<button
|
||||||
>
|
onClick={onOpenQueue}
|
||||||
<FaXmark className="w-5 h-5" />
|
className="text-white hover:bg-white/20 p-2 rounded-full transition-colors"
|
||||||
</button>
|
title="Open Queue"
|
||||||
|
>
|
||||||
|
<FaListUl className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="text-white hover:bg-white/20 p-2 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<FaXmark className="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Main Content */}
|
{/* Main Content */}
|
||||||
@@ -349,7 +378,7 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Volume and Lyrics Toggle */}
|
{/* Volume and Queue */}
|
||||||
<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)}
|
||||||
|
|||||||
Reference in New Issue
Block a user