From 2defd12500132d6ab2b843a47a1e73f368357f03 Mon Sep 17 00:00:00 2001 From: angel Date: Fri, 20 Jun 2025 16:35:10 +0000 Subject: [PATCH] feat: enhance playlist and track display with cover art and improved layout --- app/library/playlists/page.tsx | 66 +++++----- app/playlist/[id]/page.tsx | 212 +++++++++++++++++++++++++-------- 2 files changed, 200 insertions(+), 78 deletions(-) diff --git a/app/library/playlists/page.tsx b/app/library/playlists/page.tsx index f54b6e0..6d3e0cf 100644 --- a/app/library/playlists/page.tsx +++ b/app/library/playlists/page.tsx @@ -1,5 +1,6 @@ 'use client'; +import Image from 'next/image'; import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"; import { Separator } from "@/components/ui/separator"; import { Tabs, TabsContent } from "@/components/ui/tabs"; @@ -8,9 +9,11 @@ import Loading from '@/app/components/loading'; import { Button } from '@/components/ui/button'; import { PlusCircledIcon } from "@radix-ui/react-icons"; import Link from 'next/link'; +import { getNavidromeAPI } from '@/lib/navidrome'; const PlaylistsPage: React.FC = () => { const { playlists, isLoading, createPlaylist } = useNavidrome(); + const api = getNavidromeAPI(); const handleCreatePlaylist = async () => { const name = prompt('Enter playlist name:'); @@ -49,39 +52,42 @@ const PlaylistsPage: React.FC = () => {
- {playlists.map((playlist) => ( - -
-
-
- - - -
-
-

{playlist.name}

-

- {playlist.songCount} songs -

- {playlist.comment && ( -

- {playlist.comment} + {playlists.map((playlist) => { + const playlistCoverUrl = playlist.coverArt + ? api.getCoverArtUrl(playlist.coverArt, 200) + : '/default-user.jpg'; + + return ( + +

+
+
+ {playlist.name} +
+
+

{playlist.name}

+

+ {playlist.songCount} songs

- )} +
+ {playlist.comment && ( +

+ {playlist.comment} +

+ )} +
+
-
- - ))} + + ); + })}
diff --git a/app/playlist/[id]/page.tsx b/app/playlist/[id]/page.tsx index a41ecc9..1990c4f 100644 --- a/app/playlist/[id]/page.tsx +++ b/app/playlist/[id]/page.tsx @@ -3,13 +3,16 @@ import { useEffect, useState } from 'react'; import { useParams } from 'next/navigation'; import Image from 'next/image'; +import Link from 'next/link'; import { Playlist, Song } from '@/lib/navidrome'; import { useNavidrome } from '@/app/components/NavidromeContext'; import { useAudioPlayer } from '@/app/components/AudioPlayerContext'; -import { Play, Heart, Plus } from 'lucide-react'; +import { getNavidromeAPI } from '@/lib/navidrome'; +import { Play, Heart, Plus, Clock, User, Disc } from 'lucide-react'; import { Button } from '@/components/ui/button'; import Loading from "@/app/components/loading"; import { Separator } from '@/components/ui/separator'; +import { ScrollArea } from '@/components/ui/scroll-area'; export default function PlaylistPage() { const { id } = useParams(); @@ -17,7 +20,8 @@ export default function PlaylistPage() { const [tracklist, setTracklist] = useState([]); const [loading, setLoading] = useState(true); const { getPlaylist } = useNavidrome(); - const { playTrack, addToQueue } = useAudioPlayer(); + const { playTrack, addToQueue, currentTrack } = useAudioPlayer(); + const api = getNavidromeAPI(); useEffect(() => { const fetchPlaylist = async () => { @@ -45,11 +49,11 @@ export default function PlaylistPage() { const track = { id: song.id, name: song.title, - url: '', // Will be set by the context + url: api.getStreamUrl(song.id), artist: song.artist, album: song.album, duration: song.duration, - coverArt: song.coverArt, + coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, albumId: song.albumId, artistId: song.artistId }; @@ -60,21 +64,51 @@ export default function PlaylistPage() { const track = { id: song.id, name: song.title, - url: '', // Will be set by the context + url: api.getStreamUrl(song.id), artist: song.artist, album: song.album, duration: song.duration, - coverArt: song.coverArt, + coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, albumId: song.albumId, artistId: song.artistId }; addToQueue(track); }; - const formatDuration = (seconds: number) => { - const minutes = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60).toString().padStart(2, '0'); - return `${minutes}:${secs}`; + const handlePlayPlaylist = () => { + if (tracklist.length === 0) return; + + // Convert all songs to tracks + const tracks = tracklist.map(song => ({ + id: song.id, + name: song.title, + url: api.getStreamUrl(song.id), + artist: song.artist, + album: song.album, + duration: song.duration, + coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, + albumId: song.albumId, + artistId: song.artistId + })); + + // Play the first track and add the rest to queue + if (tracks.length > 0) { + playTrack(tracks[0], true); // Enable autoplay + if (tracks.length > 1) { + // Add remaining tracks to queue + tracks.slice(1).forEach(track => addToQueue(track)); + } + } + }; + + const isCurrentlyPlaying = (song: Song): boolean => { + return currentTrack?.id === song.id; + }; + + const formatDuration = (duration: number): string => { + const minutes = Math.floor(duration / 60); + const seconds = duration % 60; + return `${minutes}:${seconds.toString().padStart(2, '0')}`; }; if (loading) { @@ -92,64 +126,146 @@ export default function PlaylistPage() { ); } + // Get playlist cover art URL with fallback + const playlistCoverUrl = playlist.coverArt + ? api.getCoverArtUrl(playlist.coverArt, 300) + : '/default-user.jpg'; + return (
-
- +
+ {playlist.name}

{playlist.name}

{playlist.comment && ( -

{playlist.comment}

+

{playlist.comment}

)} +
-

{playlist.songCount} songs • {formatDuration(playlist.duration || 0)}

+

{playlist.songCount} songs • Duration: {formatDuration(playlist.duration || 0)}

{playlist.public !== undefined && (

{playlist.public ? 'Public' : 'Private'} playlist

)}
-
+
- {tracklist.length > 0 ? ( - tracklist.map((song, index) => ( -
handlePlayClick(song)}> -
-
{index + 1}
-
-

- {song.title} -

-

- {song.artist} -

-
-
-
-

{formatDuration(song.duration)}

- -
+ + {tracklist.length === 0 ? ( +
+

This playlist is empty.

- )) - ) : ( -
-

This playlist is empty

-
- )} + ) : ( +
+ {tracklist.map((song, index) => ( +
handlePlayClick(song)} + > + {/* Track Number / Play Indicator */} +
+ {isCurrentlyPlaying(song) ? ( +
+
+
+ ) : ( + <> + {index + 1} + + + )} +
+ + {/* Album Art */} +
+ {song.album} +
+ + {/* Song Info */} +
+
+

+ {song.title} +

+
+
+
+ + e.stopPropagation()} + > + {song.artist} + +
+ {song.album && ( +
+ + e.stopPropagation()} + > + {song.album} + +
+ )} +
+
+ + {/* Duration */} +
+ + {formatDuration(song.duration)} +
+ + {/* Actions */} +
+ +
+
+ ))} +
+ )} +