From 4b0997c6b4336ad21defbbe9d8ab7a86b4d830e3 Mon Sep 17 00:00:00 2001 From: angel Date: Fri, 8 Aug 2025 21:38:58 +0000 Subject: [PATCH] feat: Enhance UI with Framer Motion animations for album artwork and artist icons --- app/components/AudioPlayer.tsx | 2 +- app/components/BottomNavigation.tsx | 19 ++++++- app/components/FullScreenPlayer.tsx | 84 +++++++++++++++++++---------- app/components/album-artwork.tsx | 22 ++++++-- app/components/artist-icon.tsx | 9 ++++ 5 files changed, 100 insertions(+), 36 deletions(-) diff --git a/app/components/AudioPlayer.tsx b/app/components/AudioPlayer.tsx index d378a16..02b3f83 100644 --- a/app/components/AudioPlayer.tsx +++ b/app/components/AudioPlayer.tsx @@ -484,7 +484,7 @@ export const AudioPlayer: React.FC = () => { // Reset title when no track document.title = 'mice'; } - }, [currentTrack?.id, currentTrack?.name, currentTrack?.artist, isPlaying, isClient]); + }, [currentTrack?.id, currentTrack?.name, currentTrack?.artist, currentTrack?.coverArt, isPlaying, isClient, lastNotifiedTrackId]); // Media Session API integration - Enhanced for mobile useEffect(() => { diff --git a/app/components/BottomNavigation.tsx b/app/components/BottomNavigation.tsx index 5e3038b..b39f0fb 100644 --- a/app/components/BottomNavigation.tsx +++ b/app/components/BottomNavigation.tsx @@ -3,6 +3,7 @@ import { useRouter, usePathname } from 'next/navigation'; import { Home, Search, Disc, Users, Music, Heart, List, Settings } from 'lucide-react'; import { cn } from '@/lib/utils'; +import { motion, AnimatePresence } from 'framer-motion'; interface NavItem { href: string; @@ -40,7 +41,7 @@ export function BottomNavigation() { const Icon = item.icon; return ( - + + {isItemActive && ( + + )} + + ); })} diff --git a/app/components/FullScreenPlayer.tsx b/app/components/FullScreenPlayer.tsx index e8e93a0..988aaa9 100644 --- a/app/components/FullScreenPlayer.tsx +++ b/app/components/FullScreenPlayer.tsx @@ -523,18 +523,29 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl {activeTab === 'player' && ( - {/* Mobile Album Art */} -
- {currentTrack.album} + {/* Mobile Album Art (crossfade on track change) */} +
+ + + {currentTrack.album} + +
{/* Track Info - Left Aligned and Heart on Same Line */} @@ -651,11 +662,14 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl >
{lyrics.map((line, index) => ( -
handleLyricClick(line.time)} - className={`text-base leading-relaxed transition-all duration-300 break-words cursor-pointer hover:text-foreground px-2 ${ + initial={false} + animate={index === currentLyricIndex ? { scale: 1, opacity: 1 } : index < currentLyricIndex ? { scale: 0.995, opacity: 0.7 } : { scale: 0.99, opacity: 0.5 }} + transition={{ duration: 0.2 }} + className={`text-base leading-relaxed transition-colors duration-200 break-words cursor-pointer hover:text-foreground px-2 ${ index === currentLyricIndex ? 'text-foreground font-bold text-xl' : index < currentLyricIndex @@ -671,7 +685,7 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl title={`Click to jump to ${formatTime(line.time)}`} > {line.text || '♪'} -
+ ))}
@@ -753,16 +767,27 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl
{/* Left Side - Album Art and Controls */}
- {/* Album Art */} -
- {currentTrack.album} + {/* Album Art (crossfade on track change) */} +
+ + + {currentTrack.album} + +
{/* Track Info */} @@ -889,11 +914,14 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl
{lyrics.map((line, index) => ( -
handleLyricClick(line.time)} - className={`text-base leading-relaxed transition-all duration-300 break-words cursor-pointer hover:text-foreground ${ + initial={false} + animate={index === currentLyricIndex ? { scale: 1, opacity: 1 } : index < currentLyricIndex ? { scale: 0.995, opacity: 0.75 } : { scale: 0.99, opacity: 0.5 }} + transition={{ duration: 0.2 }} + className={`text-base leading-relaxed transition-colors duration-200 break-words cursor-pointer hover:text-foreground ${ index === currentLyricIndex ? 'text-foreground font-bold text-2xl' : index < currentLyricIndex @@ -910,7 +938,7 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl title={`Click to jump to ${formatTime(line.time)}`} > {line.text || '♪'} -
+ ))}
diff --git a/app/components/album-artwork.tsx b/app/components/album-artwork.tsx index e8b49a1..b252c2c 100644 --- a/app/components/album-artwork.tsx +++ b/app/components/album-artwork.tsx @@ -3,6 +3,7 @@ import Image from "next/image" import { PlusCircledIcon } from "@radix-ui/react-icons" import { useRouter } from 'next/navigation'; +import { motion } from 'framer-motion'; import { cn } from "@/lib/utils" import { @@ -29,7 +30,10 @@ import { Heart, Music, Disc, Mic, Play, Download } from "lucide-react"; import { Album, Artist, Song } from "@/lib/navidrome"; import { OfflineIndicator } from "@/app/components/OfflineIndicator"; -interface AlbumArtworkProps extends React.HTMLAttributes { +interface AlbumArtworkProps extends Omit< + React.HTMLAttributes, + 'onDrag' | 'onDragStart' | 'onDragEnd' | 'onDragOver' | 'onDragEnter' | 'onDragLeave' | 'onDrop' +> { album: Album aspectRatio?: "portrait" | "square" width?: number @@ -75,10 +79,10 @@ export function AlbumArtwork({ const handlePrefetch = () => { try { // Next.js App Router will prefetch on hover when using Link with prefetch - // but we also call router.prefetch to ensure programmatic prefetch. - // @ts-ignore - prefetch exists in next/navigation router in app router - if (router && typeof (router as any).prefetch === 'function') { - (router as any).prefetch(`/album/${album.id}`); + // but we also call router.prefetch to ensure programmatic prefetch when present. + const r = router as unknown as { prefetch?: (href: string) => Promise | void }; + if (r && typeof r.prefetch === 'function') { + r.prefetch(`/album/${album.id}`); } } catch {} }; @@ -138,6 +142,13 @@ export function AlbumArtwork({ return (
+ handleClick()} onMouseEnter={handlePrefetch} onFocus={handlePrefetch}> @@ -239,6 +250,7 @@ export function AlbumArtwork({ Share +
) } \ No newline at end of file diff --git a/app/components/artist-icon.tsx b/app/components/artist-icon.tsx index 55970f2..076a7ea 100644 --- a/app/components/artist-icon.tsx +++ b/app/components/artist-icon.tsx @@ -4,6 +4,7 @@ import Image from "next/image" import { PlusCircledIcon } from "@radix-ui/react-icons" import { useRouter } from 'next/navigation'; import { cn } from "@/lib/utils" +import { motion } from 'framer-motion' import { ContextMenu, ContextMenuContent, @@ -88,6 +89,13 @@ export function ArtistIcon({
+ handleClick()}>
+