diff --git a/app/artist/[artist]/page.tsx b/app/artist/[artist]/page.tsx index ff1556c..1bca727 100644 --- a/app/artist/[artist]/page.tsx +++ b/app/artist/[artist]/page.tsx @@ -3,13 +3,15 @@ import { useEffect, useState } from 'react'; import { useParams } from 'next/navigation'; import { Album, Artist } from '@/lib/navidrome'; import { useNavidrome } from '@/app/components/NavidromeContext'; +import { useAudioPlayer } from '@/app/components/AudioPlayerContext'; import { AlbumArtwork } from '@/app/components/album-artwork'; import Image from 'next/image'; import { Button } from '@/components/ui/button'; -import { Heart } from 'lucide-react'; +import { Heart, Play } from 'lucide-react'; import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; import Loading from '@/app/components/loading'; import { getNavidromeAPI } from '@/lib/navidrome'; +import { useToast } from '@/hooks/use-toast'; export default function ArtistPage() { const { artist: artistId } = useParams(); @@ -17,7 +19,10 @@ export default function ArtistPage() { const [artistAlbums, setArtistAlbums] = useState([]); const [loading, setLoading] = useState(true); const [artist, setArtist] = useState(null); + const [isPlayingArtist, setIsPlayingArtist] = useState(false); const { getArtist, starItem, unstarItem } = useNavidrome(); + const { addArtistToQueue, playAlbum, clearQueue } = useAudioPlayer(); + const { toast } = useToast(); const api = getNavidromeAPI(); useEffect(() => { @@ -55,6 +60,36 @@ export default function ArtistPage() { } }; + const handlePlayArtist = async () => { + if (!artist) return; + + setIsPlayingArtist(true); + try { + // Clear current queue and add all artist albums + clearQueue(); + await addArtistToQueue(artist.id); + + // Start playing the first album if we have any + if (artistAlbums.length > 0) { + await playAlbum(artistAlbums[0].id); + } + + toast({ + title: "Playing Artist", + description: `Now playing all albums by ${artist.name}`, + }); + } catch (error) { + console.error('Failed to play artist:', error); + toast({ + title: "Error", + description: "Failed to play artist albums.", + variant: "destructive" + }); + } finally { + setIsPlayingArtist(false); + } + }; + if (loading) { return ; } @@ -90,10 +125,20 @@ export default function ArtistPage() {

{artist.name}

{artist.albumCount} albums

- +
+ + +
diff --git a/app/components/AudioPlayer.tsx b/app/components/AudioPlayer.tsx index 14e2e55..353c595 100644 --- a/app/components/AudioPlayer.tsx +++ b/app/components/AudioPlayer.tsx @@ -1,14 +1,17 @@ '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 { FaPlay, FaPause, FaVolumeHigh, FaForward, FaBackward, FaCompress, FaVolumeXmark } from "react-icons/fa6"; +import { FullScreenPlayer } from '@/app/components/FullScreenPlayer'; +import { FaPlay, FaPause, FaVolumeHigh, FaForward, FaBackward, FaCompress, FaVolumeXmark, FaExpand } from "react-icons/fa6"; import ColorThief from '@neutrixs/colorthief'; import { Progress } from '@/components/ui/progress'; 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); @@ -16,11 +19,27 @@ export const AudioPlayer: React.FC = () => { const [volume, setVolume] = useState(1); const [isClient, setIsClient] = useState(false); const [isMinimized, setIsMinimized] = useState(false); + const [isFullScreen, setIsFullScreen] = useState(false); const audioCurrent = audioRef.current; const { toast } = useToast(); + const handleOpenQueue = () => { + setIsFullScreen(false); + router.push('/queue'); + }; + useEffect(() => { setIsClient(true); + + // Clean up old localStorage entries with track IDs + const keysToRemove: string[] = []; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key && key.startsWith('navidrome-track-time-')) { + keysToRemove.push(key); + } + } + keysToRemove.forEach(key => localStorage.removeItem(key)); }, []); // Save position when component unmounts or track changes @@ -28,7 +47,7 @@ export const AudioPlayer: React.FC = () => { const audioCurrent = audioRef.current; return () => { if (audioCurrent && currentTrack && audioCurrent.currentTime > 10) { - localStorage.setItem(`navidrome-track-time-${currentTrack.id}`, audioCurrent.currentTime.toString()); + localStorage.setItem('navidrome-current-track-time', audioCurrent.currentTime.toString()); } }; }, [currentTrack]); @@ -37,10 +56,13 @@ export const AudioPlayer: React.FC = () => { const audioCurrent = audioRef.current; if (currentTrack && audioCurrent && audioCurrent.src !== currentTrack.url) { + // Always clear current track time when changing tracks + localStorage.removeItem('navidrome-current-track-time'); + audioCurrent.src = currentTrack.url; // Check for saved timestamp (only restore if more than 10 seconds in) - const savedTime = localStorage.getItem(`navidrome-track-time-${currentTrack.id}`); + const savedTime = localStorage.getItem('navidrome-current-track-time'); if (savedTime) { const time = parseFloat(savedTime); // Only restore if we were at least 10 seconds in and not near the end @@ -58,6 +80,8 @@ export const AudioPlayer: React.FC = () => { audioCurrent.addEventListener('loadeddata', restorePosition); } } + // Always clear after attempting to restore + localStorage.removeItem('navidrome-current-track-time'); } audioCurrent.play(); @@ -73,10 +97,10 @@ export const AudioPlayer: React.FC = () => { if (audioCurrent && currentTrack) { setProgress((audioCurrent.currentTime / audioCurrent.duration) * 100); - // Save current time every 10 seconds, but only if we've moved forward significantly + // Save current time every 30 seconds, but only if we've moved forward significantly const currentTime = audioCurrent.currentTime; - if (Math.abs(currentTime - lastSavedTime) >= 10 && currentTime > 10) { - localStorage.setItem(`navidrome-track-time-${currentTrack.id}`, currentTime.toString()); + if (Math.abs(currentTime - lastSavedTime) >= 30 && currentTime > 10) { + localStorage.setItem('navidrome-current-track-time', currentTime.toString()); lastSavedTime = currentTime; } } @@ -85,7 +109,7 @@ export const AudioPlayer: React.FC = () => { const handleTrackEnd = () => { if (currentTrack) { // Clear saved time when track ends - localStorage.removeItem(`navidrome-track-time-${currentTrack.id}`); + localStorage.removeItem('navidrome-current-track-time'); } playNextTrack(); }; @@ -93,7 +117,7 @@ export const AudioPlayer: React.FC = () => { const handleSeeked = () => { if (audioCurrent && currentTrack) { // Save immediately when user seeks - localStorage.setItem(`navidrome-track-time-${currentTrack.id}`, audioCurrent.currentTime.toString()); + localStorage.setItem('navidrome-current-track-time', audioCurrent.currentTime.toString()); lastSavedTime = audioCurrent.currentTime; } }; @@ -112,6 +136,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) { @@ -121,7 +205,7 @@ export const AudioPlayer: React.FC = () => { audioCurrent.currentTime = newTime; // Save the new position immediately - localStorage.setItem(`navidrome-track-time-${currentTrack.id}`, newTime.toString()); + localStorage.setItem('navidrome-current-track-time', newTime.toString()); } }; @@ -229,6 +313,13 @@ export const AudioPlayer: React.FC = () => {
+