feat: update commit SHA, enhance audio player and full screen player with favorite functionality, and update various components to support starred tracks

This commit is contained in:
2025-07-02 00:37:01 +00:00
committed by GitHub
parent d6ac2479cb
commit 707960b088
11 changed files with 150 additions and 28 deletions

View File

@@ -1 +1 @@
NEXT_PUBLIC_COMMIT_SHA=0cb4f23 NEXT_PUBLIC_COMMIT_SHA=d6ac247

View File

@@ -5,13 +5,14 @@ import { useRouter } from 'next/navigation';
import { useAudioPlayer, Track } from '@/app/components/AudioPlayerContext'; import { useAudioPlayer, Track } 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, FaShuffle } from "react-icons/fa6"; import { FaPlay, FaPause, FaVolumeHigh, FaForward, FaBackward, FaCompress, FaVolumeXmark, FaExpand, FaShuffle } from "react-icons/fa6";
import { Heart } from 'lucide-react';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { useLastFmScrobbler } from '@/hooks/use-lastfm-scrobbler'; import { useLastFmScrobbler } from '@/hooks/use-lastfm-scrobbler';
import { useStandaloneLastFm } from '@/hooks/use-standalone-lastfm'; import { useStandaloneLastFm } from '@/hooks/use-standalone-lastfm';
export const AudioPlayer: React.FC = () => { export const AudioPlayer: React.FC = () => {
const { currentTrack, playPreviousTrack, addToQueue, playNextTrack, clearQueue, queue, toggleShuffle, shuffle } = useAudioPlayer(); const { currentTrack, playPreviousTrack, addToQueue, playNextTrack, clearQueue, queue, toggleShuffle, shuffle, toggleCurrentTrackStar } = useAudioPlayer();
const router = useRouter(); const router = useRouter();
const audioRef = useRef<HTMLAudioElement>(null); const audioRef = useRef<HTMLAudioElement>(null);
const preloadAudioRef = useRef<HTMLAudioElement>(null); const preloadAudioRef = useRef<HTMLAudioElement>(null);
@@ -377,6 +378,19 @@ export const AudioPlayer: React.FC = () => {
</div> </div>
<p className="text-xs text-muted-foreground truncate">{currentTrack.artist}</p> <p className="text-xs text-muted-foreground truncate">{currentTrack.artist}</p>
</div> </div>
{/* Heart icon for favoriting */}
<button
className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors mr-2"
onClick={(e) => {
e.stopPropagation();
toggleCurrentTrackStar();
}}
title={currentTrack.starred ? 'Remove from favorites' : 'Add to favorites'}
>
<Heart
className={`w-4 h-4 ${currentTrack.starred ? 'text-primary fill-primary' : 'text-gray-400'}`}
/>
</button>
<div className="flex items-center justify-center space-x-2"> <div className="flex items-center justify-center space-x-2">
<button className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playPreviousTrack}> <button className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playPreviousTrack}>
<FaBackward className="w-3 h-3" /> <FaBackward className="w-3 h-3" />
@@ -413,7 +427,6 @@ export const AudioPlayer: React.FC = () => {
<p className="font-semibold truncate text-sm">{currentTrack.name}</p> <p className="font-semibold truncate text-sm">{currentTrack.name}</p>
<p className="text-xs text-muted-foreground truncate">{currentTrack.artist}</p> <p className="text-xs text-muted-foreground truncate">{currentTrack.artist}</p>
</div> </div>
{/* faviorte icon or smthing here */}
</div> </div>
{/* Control buttons */} {/* Control buttons */}
<button <button
@@ -430,6 +443,18 @@ export const AudioPlayer: React.FC = () => {
<button className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playNextTrack}> <button className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playNextTrack}>
<FaForward className="w-3 h-3" /> <FaForward className="w-3 h-3" />
</button> </button>
<button
className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors flex items-center justify-center"
onClick={(e) => {
e.stopPropagation();
toggleCurrentTrackStar();
}}
title={currentTrack.starred ? 'Remove from favorites' : 'Add to favorites'}
>
<Heart
className={`w-4 h-4 ${currentTrack.starred ? 'text-primary fill-primary' : ''}`}
/>
</button>
</div> </div>
<div className="flex items-center space-x-1 ml-2"> <div className="flex items-center space-x-1 ml-2">
<button <button

View File

@@ -16,6 +16,7 @@ export interface Track {
albumId: string; albumId: string;
artistId: string; artistId: string;
autoPlay?: boolean; // Flag to control auto-play autoPlay?: boolean; // Flag to control auto-play
starred?: boolean; // Flag for starred/favorited tracks
} }
interface AudioPlayerContextProps { interface AudioPlayerContextProps {
@@ -39,6 +40,8 @@ interface AudioPlayerContextProps {
playArtist: (artistId: string) => Promise<void>; playArtist: (artistId: string) => Promise<void>;
playedTracks: Track[]; playedTracks: Track[];
clearHistory: () => void; clearHistory: () => void;
toggleCurrentTrackStar: () => Promise<void>;
updateTrackStarred: (trackId: string, starred: boolean) => void;
} }
const AudioPlayerContext = createContext<AudioPlayerContextProps | undefined>(undefined); const AudioPlayerContext = createContext<AudioPlayerContextProps | undefined>(undefined);
@@ -104,7 +107,8 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
duration: song.duration, duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId, albumId: song.albumId,
artistId: song.artistId artistId: song.artistId,
starred: !!song.starred
}; };
}, [api]); }, [api]);
@@ -577,7 +581,75 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
shuffleAllAlbums, shuffleAllAlbums,
playArtist, playArtist,
playedTracks, playedTracks,
clearHistory clearHistory,
toggleCurrentTrackStar: async () => {
if (!currentTrack || !api) {
toast({
variant: "destructive",
title: "Error",
description: "No track currently playing or API not configured",
});
return;
}
const newStarredStatus = !currentTrack.starred;
try {
if (newStarredStatus) {
await api.star(currentTrack.id, 'song');
} else {
await api.unstar(currentTrack.id, 'song');
}
// Update the current track state
setCurrentTrack((prev) => prev ? { ...prev, starred: newStarredStatus } : null);
} catch (error) {
console.error('Failed to update track starred status:', error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to update track favorite status",
});
}
},
updateTrackStarred: async (trackId: string, starred: boolean) => {
if (!api) {
toast({
variant: "destructive",
title: "Configuration Required",
description: "Please configure Navidrome connection in settings",
});
return;
}
try {
if (starred) {
await api.star(trackId, 'song');
} else {
await api.unstar(trackId, 'song');
}
// Update the current track state if it matches the updated track
if (currentTrack?.id === trackId) {
setCurrentTrack((prev) => prev ? { ...prev, starred } : null);
}
// Also update queue if the track is in there
setQueue((prev) => prev.map(track =>
track.id === trackId ? { ...track, starred } : track
));
} catch (error) {
console.error('Failed to update track starred status:', error);
toast({
variant: "destructive",
title: "Error",
description: "Failed to update track favorite status",
});
}
}
}), [ }), [
currentTrack, currentTrack,
queue, queue,
@@ -598,7 +670,9 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
shuffleAllAlbums, shuffleAllAlbums,
playArtist, playArtist,
playedTracks, playedTracks,
clearHistory clearHistory,
api,
toast
]); ]);
return ( return (

View File

@@ -19,6 +19,7 @@ import {
FaQuoteLeft, FaQuoteLeft,
FaListUl FaListUl
} from "react-icons/fa6"; } from "react-icons/fa6";
import { Heart } from 'lucide-react';
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';
@@ -34,7 +35,7 @@ interface FullScreenPlayerProps {
} }
export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onClose, onOpenQueue }) => { export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onClose, onOpenQueue }) => {
const { currentTrack, playPreviousTrack, playNextTrack, shuffle, toggleShuffle } = useAudioPlayer(); const { currentTrack, playPreviousTrack, playNextTrack, shuffle, toggleShuffle, toggleCurrentTrackStar } = useAudioPlayer();
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
const [isPlaying, setIsPlaying] = useState(false); const [isPlaying, setIsPlaying] = useState(false);
const [volume, setVolume] = useState(1); const [volume, setVolume] = useState(1);
@@ -384,17 +385,17 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
<FaForward className="w-4 h-4 sm:w-5 sm:h-5" /> <FaForward className="w-4 h-4 sm:w-5 sm:h-5" />
</button> </button>
{lyrics.length > 0 && ( <button
<button onClick={toggleCurrentTrackStar}
onClick={() => setShowLyrics(!showLyrics)} className="p-2 hover:bg-gray-700/50 rounded-full transition-colors"
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${ title={currentTrack?.starred ? 'Remove from favorites' : 'Add to favorites'}
showLyrics ? 'text-primary bg-primary/20' : 'text-gray-500' >
}`} <Heart
title={showLyrics ? 'Hide Lyrics' : 'Show Lyrics'} className={`w-4 h-4 sm:w-5 sm:h-5 ${currentTrack?.starred ? 'text-primary fill-primary' : 'text-gray-400'}`}
> />
<FaQuoteLeft className="w-4 h-4 sm:w-5 sm:h-5" /> </button>
</button>
)}
</div> </div>
@@ -410,6 +411,17 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
)} )}
</button> </button>
{lyrics.length > 0 && (
<button
onClick={() => setShowLyrics(!showLyrics)}
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${
showLyrics ? 'text-primary bg-primary/20' : 'text-gray-500'
}`}
title={showLyrics ? 'Hide Lyrics' : 'Show Lyrics'}
>
<FaQuoteLeft className="w-4 h-4 sm:w-5 sm:h-5" />
</button>
)}
{showVolumeSlider && ( {showVolumeSlider && (
<div <div

View File

@@ -38,7 +38,8 @@ export function PopularSongs({ songs, artistName }: PopularSongsProps) {
duration: song.duration, duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId, albumId: song.albumId,
artistId: song.artistId artistId: song.artistId,
starred: !!song.starred
}; };
}; };

View File

@@ -79,6 +79,7 @@ export function AlbumArtwork({
url: api.getStreamUrl(song.id), url: api.getStreamUrl(song.id),
duration: song.duration, duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt) : undefined, coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt) : undefined,
starred: !!song.starred
})); }));
playTrack(tracks[0]); playTrack(tracks[0]);

View File

@@ -60,6 +60,7 @@ const FavoritesPage = () => {
url: api?.getStreamUrl(song.id) || '', url: api?.getStreamUrl(song.id) || '',
duration: song.duration, duration: song.duration,
coverArt: song.coverArt ? api?.getCoverArtUrl(song.coverArt) : undefined, coverArt: song.coverArt ? api?.getCoverArtUrl(song.coverArt) : undefined,
starred: !!song.starred
}); });
}; };
@@ -79,6 +80,7 @@ const FavoritesPage = () => {
url: api.getStreamUrl(song.id), url: api.getStreamUrl(song.id),
duration: song.duration, duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt) : undefined, coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt) : undefined,
starred: !!song.starred
})); }));
playTrack(tracks[0]); playTrack(tracks[0]);

View File

@@ -116,7 +116,8 @@ export default function SongsPage() {
duration: song.duration, duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId, albumId: song.albumId,
artistId: song.artistId artistId: song.artistId,
starred: !!song.starred
}; };
playTrack(track); playTrack(track);
@@ -136,7 +137,8 @@ export default function SongsPage() {
duration: song.duration, duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId, albumId: song.albumId,
artistId: song.artistId artistId: song.artistId,
starred: !!song.starred
}; };
addToQueue(track); addToQueue(track);

View File

@@ -59,7 +59,8 @@ export default function PlaylistPage() {
duration: song.duration, duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId, albumId: song.albumId,
artistId: song.artistId artistId: song.artistId,
starred: !!song.starred
}; };
playTrack(track); playTrack(track);
}; };
@@ -78,7 +79,8 @@ export default function PlaylistPage() {
duration: song.duration, duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId, albumId: song.albumId,
artistId: song.artistId artistId: song.artistId,
starred: !!song.starred
}; };
addToQueue(track); addToQueue(track);
}; };
@@ -98,7 +100,8 @@ export default function PlaylistPage() {
duration: song.duration, duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId, albumId: song.albumId,
artistId: song.artistId artistId: song.artistId,
starred: !!song.starred
})); }));
// Play the first track and add the rest to queue // Play the first track and add the rest to queue

View File

@@ -66,7 +66,8 @@ export default function SearchPage() {
duration: song.duration, duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId, albumId: song.albumId,
artistId: song.artistId artistId: song.artistId,
starred: !!song.starred
}; };
playTrack(track); playTrack(track);
@@ -86,7 +87,8 @@ export default function SearchPage() {
duration: song.duration, duration: song.duration,
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined, coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
albumId: song.albumId, albumId: song.albumId,
artistId: song.artistId artistId: song.artistId,
starred: !!song.starred
}; };
addToQueue(track); addToQueue(track);

View File

@@ -443,7 +443,7 @@ const SettingsPage = () => {
</CardContent> </CardContent>
</Card> </Card>
<Card> {/* <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<FaCog className="w-5 h-5" /> <FaCog className="w-5 h-5" />
@@ -472,7 +472,7 @@ const SettingsPage = () => {
</p> </p>
</div> </div>
</CardContent> </CardContent>
</Card> </Card> */}
<Card> <Card>
<CardHeader> <CardHeader>