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 { 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
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user