From 6f33c35c47f88fb9b9ba5af6420f9b143605307e Mon Sep 17 00:00:00 2001 From: angel Date: Thu, 19 Jun 2025 21:42:37 +0000 Subject: [PATCH] feat: add queue navigation to FullScreenPlayer and improve UI elements --- app/components/AudioPlayer.tsx | 68 +++++++++++++++++++++++++ app/components/FullScreenPlayer.tsx | 77 ++++++++++++++++++++--------- 2 files changed, 121 insertions(+), 24 deletions(-) diff --git a/app/components/AudioPlayer.tsx b/app/components/AudioPlayer.tsx index 72b762e..80d7143 100644 --- a/app/components/AudioPlayer.tsx +++ b/app/components/AudioPlayer.tsx @@ -1,6 +1,7 @@ 'use client'; import React, { useEffect, useRef, useState } from 'react'; import Image from 'next/image'; +import { useRouter } from 'next/navigation'; import { useAudioPlayer } from '@/app/components/AudioPlayerContext'; import { FullScreenPlayer } from '@/app/components/FullScreenPlayer'; 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 = () => { const { currentTrack, playPreviousTrack, addToQueue, playNextTrack, clearQueue } = useAudioPlayer(); + const router = useRouter(); const audioRef = useRef(null); const [progress, setProgress] = useState(0); const [isPlaying, setIsPlaying] = useState(false); @@ -21,6 +23,11 @@ export const AudioPlayer: React.FC = () => { const audioCurrent = audioRef.current; const { toast } = useToast(); + const handleOpenQueue = () => { + setIsFullScreen(false); + router.push('/queue'); + }; + useEffect(() => { setIsClient(true); }, []); @@ -114,6 +121,66 @@ export const AudioPlayer: React.FC = () => { } }; }, [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) => { if (audioCurrent && currentTrack) { @@ -254,6 +321,7 @@ export const AudioPlayer: React.FC = () => { setIsFullScreen(false)} + onOpenQueue={handleOpenQueue} /> ); diff --git a/app/components/FullScreenPlayer.tsx b/app/components/FullScreenPlayer.tsx index 6d44ebf..8785b3a 100644 --- a/app/components/FullScreenPlayer.tsx +++ b/app/components/FullScreenPlayer.tsx @@ -15,7 +15,8 @@ import { FaShuffle, FaRepeat, FaXmark, - FaQuoteLeft + FaQuoteLeft, + FaListUl } from "react-icons/fa6"; import { Card, CardContent } from '@/components/ui/card'; import { ScrollArea } from '@/components/ui/scroll-area'; @@ -28,9 +29,10 @@ interface LyricLine { interface FullScreenPlayerProps { isOpen: boolean; onClose: () => void; + onOpenQueue?: () => void; } -export const FullScreenPlayer: React.FC = ({ isOpen, onClose }) => { +export const FullScreenPlayer: React.FC = ({ isOpen, onClose, onOpenQueue }) => { const { currentTrack, playPreviousTrack, playNextTrack } = useAudioPlayer(); const [progress, setProgress] = useState(0); const [isPlaying, setIsPlaying] = useState(false); @@ -135,10 +137,10 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl } }, [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(() => { 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 now = Date.now(); @@ -151,8 +153,13 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl const newDuration = mainAudio.duration || 0; const newIsPlaying = !mainAudio.paused; + // Always update playing state for better responsiveness + if (newIsPlaying !== isPlaying) { + setIsPlaying(newIsPlaying); + } + // Only update state if values have changed significantly - if (Math.abs(newCurrentTime - currentTime) > 0.5) { + if (Math.abs(newCurrentTime - currentTime) > 0.3) { setCurrentTime(newCurrentTime); } if (Math.abs(newDuration - duration) > 0.1) { @@ -164,9 +171,6 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl setProgress(newProgress); } } - if (newIsPlaying !== isPlaying) { - setIsPlaying(newIsPlaying); - } if (Math.abs(mainAudio.volume - volume) > 0.01) { setVolume(mainAudio.volume); } @@ -177,11 +181,11 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl // Initial sync syncWithMainPlayer(); - // Set up interval to keep syncing - const interval = setInterval(syncWithMainPlayer, 100); + // Set up interval to keep syncing - more frequent for better responsiveness + const interval = setInterval(syncWithMainPlayer, 50); 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 useEffect(() => { @@ -265,22 +269,47 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl if (!isOpen || !currentTrack) return null; - const backgroundStyle = { - background: `linear-gradient(135deg, ${dominantColor}40 0%, ${dominantColor}20 50%, transparent 100%)` - }; - return ( -
-
+
+ {/* Blurred background image */} + {currentTrack.coverArt && ( +
+ )} + + {/* Overlay for better contrast */} +
+ +
{/* Header */}

Now Playing

- +
+ {onOpenQueue && ( + + )} + +
{/* Main Content */} @@ -349,7 +378,7 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl
- {/* Volume and Lyrics Toggle */} + {/* Volume and Queue */}