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:
@@ -1 +1 @@
|
||||
NEXT_PUBLIC_COMMIT_SHA=0cb4f23
|
||||
NEXT_PUBLIC_COMMIT_SHA=d6ac247
|
||||
|
||||
@@ -5,13 +5,14 @@ import { useRouter } from 'next/navigation';
|
||||
import { useAudioPlayer, Track } from '@/app/components/AudioPlayerContext';
|
||||
import { FullScreenPlayer } from '@/app/components/FullScreenPlayer';
|
||||
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 { useToast } from '@/hooks/use-toast';
|
||||
import { useLastFmScrobbler } from '@/hooks/use-lastfm-scrobbler';
|
||||
import { useStandaloneLastFm } from '@/hooks/use-standalone-lastfm';
|
||||
|
||||
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 audioRef = useRef<HTMLAudioElement>(null);
|
||||
const preloadAudioRef = useRef<HTMLAudioElement>(null);
|
||||
@@ -377,6 +378,19 @@ export const AudioPlayer: React.FC = () => {
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground truncate">{currentTrack.artist}</p>
|
||||
</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">
|
||||
<button className="p-1.5 hover:bg-gray-700/50 rounded-full transition-colors" onClick={playPreviousTrack}>
|
||||
<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="text-xs text-muted-foreground truncate">{currentTrack.artist}</p>
|
||||
</div>
|
||||
{/* faviorte icon or smthing here */}
|
||||
</div>
|
||||
{/* Control buttons */}
|
||||
<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}>
|
||||
<FaForward className="w-3 h-3" />
|
||||
</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 className="flex items-center space-x-1 ml-2">
|
||||
<button
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface Track {
|
||||
albumId: string;
|
||||
artistId: string;
|
||||
autoPlay?: boolean; // Flag to control auto-play
|
||||
starred?: boolean; // Flag for starred/favorited tracks
|
||||
}
|
||||
|
||||
interface AudioPlayerContextProps {
|
||||
@@ -39,6 +40,8 @@ interface AudioPlayerContextProps {
|
||||
playArtist: (artistId: string) => Promise<void>;
|
||||
playedTracks: Track[];
|
||||
clearHistory: () => void;
|
||||
toggleCurrentTrackStar: () => Promise<void>;
|
||||
updateTrackStarred: (trackId: string, starred: boolean) => void;
|
||||
}
|
||||
|
||||
const AudioPlayerContext = createContext<AudioPlayerContextProps | undefined>(undefined);
|
||||
@@ -104,7 +107,8 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
|
||||
duration: song.duration,
|
||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||
albumId: song.albumId,
|
||||
artistId: song.artistId
|
||||
artistId: song.artistId,
|
||||
starred: !!song.starred
|
||||
};
|
||||
}, [api]);
|
||||
|
||||
@@ -577,7 +581,75 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
|
||||
shuffleAllAlbums,
|
||||
playArtist,
|
||||
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,
|
||||
queue,
|
||||
@@ -598,7 +670,9 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
|
||||
shuffleAllAlbums,
|
||||
playArtist,
|
||||
playedTracks,
|
||||
clearHistory
|
||||
clearHistory,
|
||||
api,
|
||||
toast
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
FaQuoteLeft,
|
||||
FaListUl
|
||||
} from "react-icons/fa6";
|
||||
import { Heart } from 'lucide-react';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
@@ -34,7 +35,7 @@ interface FullScreenPlayerProps {
|
||||
}
|
||||
|
||||
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 [isPlaying, setIsPlaying] = useState(false);
|
||||
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" />
|
||||
</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>
|
||||
)}
|
||||
<button
|
||||
onClick={toggleCurrentTrackStar}
|
||||
className="p-2 hover:bg-gray-700/50 rounded-full transition-colors"
|
||||
title={currentTrack?.starred ? 'Remove from favorites' : 'Add to favorites'}
|
||||
>
|
||||
<Heart
|
||||
className={`w-4 h-4 sm:w-5 sm:h-5 ${currentTrack?.starred ? 'text-primary fill-primary' : 'text-gray-400'}`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
@@ -410,6 +411,17 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
|
||||
)}
|
||||
</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 && (
|
||||
<div
|
||||
|
||||
@@ -38,7 +38,8 @@ export function PopularSongs({ songs, artistName }: PopularSongsProps) {
|
||||
duration: song.duration,
|
||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||
albumId: song.albumId,
|
||||
artistId: song.artistId
|
||||
artistId: song.artistId,
|
||||
starred: !!song.starred
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -79,6 +79,7 @@ export function AlbumArtwork({
|
||||
url: api.getStreamUrl(song.id),
|
||||
duration: song.duration,
|
||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt) : undefined,
|
||||
starred: !!song.starred
|
||||
}));
|
||||
|
||||
playTrack(tracks[0]);
|
||||
|
||||
@@ -60,6 +60,7 @@ const FavoritesPage = () => {
|
||||
url: api?.getStreamUrl(song.id) || '',
|
||||
duration: song.duration,
|
||||
coverArt: song.coverArt ? api?.getCoverArtUrl(song.coverArt) : undefined,
|
||||
starred: !!song.starred
|
||||
});
|
||||
};
|
||||
|
||||
@@ -79,6 +80,7 @@ const FavoritesPage = () => {
|
||||
url: api.getStreamUrl(song.id),
|
||||
duration: song.duration,
|
||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt) : undefined,
|
||||
starred: !!song.starred
|
||||
}));
|
||||
|
||||
playTrack(tracks[0]);
|
||||
|
||||
@@ -116,7 +116,8 @@ export default function SongsPage() {
|
||||
duration: song.duration,
|
||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||
albumId: song.albumId,
|
||||
artistId: song.artistId
|
||||
artistId: song.artistId,
|
||||
starred: !!song.starred
|
||||
};
|
||||
|
||||
playTrack(track);
|
||||
@@ -136,7 +137,8 @@ export default function SongsPage() {
|
||||
duration: song.duration,
|
||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||
albumId: song.albumId,
|
||||
artistId: song.artistId
|
||||
artistId: song.artistId,
|
||||
starred: !!song.starred
|
||||
};
|
||||
|
||||
addToQueue(track);
|
||||
|
||||
@@ -59,7 +59,8 @@ export default function PlaylistPage() {
|
||||
duration: song.duration,
|
||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||
albumId: song.albumId,
|
||||
artistId: song.artistId
|
||||
artistId: song.artistId,
|
||||
starred: !!song.starred
|
||||
};
|
||||
playTrack(track);
|
||||
};
|
||||
@@ -78,7 +79,8 @@ export default function PlaylistPage() {
|
||||
duration: song.duration,
|
||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||
albumId: song.albumId,
|
||||
artistId: song.artistId
|
||||
artistId: song.artistId,
|
||||
starred: !!song.starred
|
||||
};
|
||||
addToQueue(track);
|
||||
};
|
||||
@@ -98,7 +100,8 @@ export default function PlaylistPage() {
|
||||
duration: song.duration,
|
||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||
albumId: song.albumId,
|
||||
artistId: song.artistId
|
||||
artistId: song.artistId,
|
||||
starred: !!song.starred
|
||||
}));
|
||||
|
||||
// Play the first track and add the rest to queue
|
||||
|
||||
@@ -66,7 +66,8 @@ export default function SearchPage() {
|
||||
duration: song.duration,
|
||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||
albumId: song.albumId,
|
||||
artistId: song.artistId
|
||||
artistId: song.artistId,
|
||||
starred: !!song.starred
|
||||
};
|
||||
|
||||
playTrack(track);
|
||||
@@ -86,7 +87,8 @@ export default function SearchPage() {
|
||||
duration: song.duration,
|
||||
coverArt: song.coverArt ? api.getCoverArtUrl(song.coverArt, 300) : undefined,
|
||||
albumId: song.albumId,
|
||||
artistId: song.artistId
|
||||
artistId: song.artistId,
|
||||
starred: !!song.starred
|
||||
};
|
||||
|
||||
addToQueue(track);
|
||||
|
||||
@@ -443,7 +443,7 @@ const SettingsPage = () => {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
{/* <Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<FaCog className="w-5 h-5" />
|
||||
@@ -472,7 +472,7 @@ const SettingsPage = () => {
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Card> */}
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
|
||||
Reference in New Issue
Block a user