feat: add auto-play flag to Track interface in AudioPlayerContext

This commit is contained in:
2025-06-20 00:32:31 +00:00
committed by GitHub
parent 6653420e31
commit 8dfb4b34e5
3 changed files with 150 additions and 41 deletions

View File

@@ -116,8 +116,19 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
}, [currentTrack, api]); }, [currentTrack, api]);
const addToQueue = useCallback((track: Track) => { const addToQueue = useCallback((track: Track) => {
setQueue((prevQueue) => [...prevQueue, track]); setQueue((prevQueue) => {
}, []); if (shuffle && prevQueue.length > 0) {
// If shuffle is enabled, insert the track at a random position
const randomIndex = Math.floor(Math.random() * (prevQueue.length + 1));
const newQueue = [...prevQueue];
newQueue.splice(randomIndex, 0, track);
return newQueue;
} else {
// Normal behavior: add to the end
return [...prevQueue, track];
}
});
}, [shuffle]);
const clearQueue = useCallback(() => { const clearQueue = useCallback(() => {
setQueue([]); setQueue([]);
@@ -132,20 +143,13 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
localStorage.removeItem('navidrome-current-track-time'); localStorage.removeItem('navidrome-current-track-time');
if (queue.length > 0) { if (queue.length > 0) {
let nextTrack; // Always pick the first track from the queue
if (shuffle) { // If shuffle is enabled, the queue will already be shuffled
// Pick a random track from the queue const nextTrack = queue[0];
const randomIndex = Math.floor(Math.random() * queue.length); setQueue((prevQueue) => prevQueue.slice(1));
nextTrack = queue[randomIndex];
setQueue((prevQueue) => prevQueue.filter((_, i) => i !== randomIndex));
} else {
// Pick the first track in order
nextTrack = queue[0];
setQueue((prevQueue) => prevQueue.slice(1));
}
playTrack(nextTrack, true); // Auto-play next track playTrack(nextTrack, true); // Auto-play next track
} }
}, [queue, playTrack, shuffle]); }, [queue, playTrack]);
const playPreviousTrack = useCallback(() => { const playPreviousTrack = useCallback(() => {
// Clear saved timestamp when changing tracks // Clear saved timestamp when changing tracks
@@ -169,7 +173,29 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
try { try {
const { album, songs } = await api.getAlbum(albumId); const { album, songs } = await api.getAlbum(albumId);
const tracks = songs.map(songToTrack); const tracks = songs.map(songToTrack);
setQueue((prevQueue) => [...prevQueue, ...tracks]);
setQueue((prevQueue) => {
if (shuffle && prevQueue.length > 0) {
// If shuffle is enabled, shuffle the new tracks and insert them randomly
const shuffledTracks = [...tracks];
// Fisher-Yates shuffle algorithm
for (let i = shuffledTracks.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledTracks[i], shuffledTracks[j]] = [shuffledTracks[j], shuffledTracks[i]];
}
// Insert each track at a random position
const newQueue = [...prevQueue];
shuffledTracks.forEach(track => {
const randomIndex = Math.floor(Math.random() * (newQueue.length + 1));
newQueue.splice(randomIndex, 0, track);
});
return newQueue;
} else {
// Normal behavior: add to the end
return [...prevQueue, ...tracks];
}
});
toast({ toast({
title: "Album Added", title: "Album Added",
@@ -185,20 +211,44 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [api, songToTrack, toast]); }, [api, songToTrack, toast, shuffle]);
const addArtistToQueue = useCallback(async (artistId: string) => { const addArtistToQueue = useCallback(async (artistId: string) => {
setIsLoading(true); setIsLoading(true);
try { try {
const { artist, albums } = await api.getArtist(artistId); const { artist, albums } = await api.getArtist(artistId);
let allTracks: Track[] = [];
// Add all albums from this artist to queue // Collect all tracks from all albums
for (const album of albums) { for (const album of albums) {
const { songs } = await api.getAlbum(album.id); const { songs } = await api.getAlbum(album.id);
const tracks = songs.map(songToTrack); const tracks = songs.map(songToTrack);
setQueue((prevQueue) => [...prevQueue, ...tracks]); allTracks = allTracks.concat(tracks);
} }
setQueue((prevQueue) => {
if (shuffle && prevQueue.length > 0) {
// If shuffle is enabled, shuffle the new tracks and insert them randomly
const shuffledTracks = [...allTracks];
// Fisher-Yates shuffle algorithm
for (let i = shuffledTracks.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledTracks[i], shuffledTracks[j]] = [shuffledTracks[j], shuffledTracks[i]];
}
// Insert each track at a random position
const newQueue = [...prevQueue];
shuffledTracks.forEach(track => {
const randomIndex = Math.floor(Math.random() * (newQueue.length + 1));
newQueue.splice(randomIndex, 0, track);
});
return newQueue;
} else {
// Normal behavior: add to the end
return [...prevQueue, ...allTracks];
}
});
toast({ toast({
title: "Artist Added", title: "Artist Added",
description: `Added all albums by "${artist.name}" to queue`, description: `Added all albums by "${artist.name}" to queue`,
@@ -213,24 +263,36 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [api, songToTrack, toast]); }, [api, songToTrack, toast, shuffle]);
const playAlbum = useCallback(async (albumId: string) => { const playAlbum = useCallback(async (albumId: string) => {
setIsLoading(true); setIsLoading(true);
try { try {
const { album, songs } = await api.getAlbum(albumId); const { album, songs } = await api.getAlbum(albumId);
const tracks = songs.map(songToTrack); const tracks = songs.map(songToTrack);
// Clear the queue and set the new tracks
setQueue(tracks.slice(1)); // All tracks except the first one
// Play the first track immediately
if (tracks.length > 0) { if (tracks.length > 0) {
playTrack(tracks[0]); if (shuffle) {
// If shuffle is enabled, shuffle the tracks
const shuffledTracks = [...tracks];
// Fisher-Yates shuffle algorithm
for (let i = shuffledTracks.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledTracks[i], shuffledTracks[j]] = [shuffledTracks[j], shuffledTracks[i]];
}
// Play the first shuffled track and set the rest as queue
playTrack(shuffledTracks[0]);
setQueue(shuffledTracks.slice(1));
} else {
// Normal order: play first track and set the rest as queue
playTrack(tracks[0]);
setQueue(tracks.slice(1));
}
} }
toast({ toast({
title: "Playing Album", title: "Playing Album",
description: `Now playing "${album.name}"`, description: `Now playing "${album.name}"${shuffle ? ' (shuffled)' : ''}`,
}); });
} catch (error) { } catch (error) {
console.error('Failed to play album:', error); console.error('Failed to play album:', error);
@@ -242,7 +304,7 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [api, playTrack, songToTrack, toast]); }, [api, playTrack, songToTrack, toast, shuffle]);
const playAlbumFromTrack = useCallback(async (albumId: string, startingSongId: string) => { const playAlbumFromTrack = useCallback(async (albumId: string, startingSongId: string) => {
setIsLoading(true); setIsLoading(true);
@@ -257,15 +319,28 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
throw new Error('Starting song not found in album'); throw new Error('Starting song not found in album');
} }
// Clear the queue and set the remaining tracks after the starting track if (shuffle) {
setQueue(tracks.slice(startingIndex + 1)); // If shuffle is enabled, create a shuffled queue but start with the selected track
const remainingTracks = [...tracks];
// Play the starting track immediately remainingTracks.splice(startingIndex, 1); // Remove the starting track
playTrack(tracks[startingIndex]);
// Shuffle the remaining tracks
for (let i = remainingTracks.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[remainingTracks[i], remainingTracks[j]] = [remainingTracks[j], remainingTracks[i]];
}
setQueue(remainingTracks);
playTrack(tracks[startingIndex]);
} else {
// Normal order: set the remaining tracks after the starting track as queue
setQueue(tracks.slice(startingIndex + 1));
playTrack(tracks[startingIndex]);
}
toast({ toast({
title: "Playing Album", title: "Playing Album",
description: `Playing "${album.name}" from "${tracks[startingIndex].name}"`, description: `Playing "${album.name}" from "${tracks[startingIndex].name}"${shuffle ? ' (shuffled)' : ''}`,
}); });
} catch (error) { } catch (error) {
console.error('Failed to play album from track:', error); console.error('Failed to play album from track:', error);
@@ -277,7 +352,7 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
}, [api, playTrack, songToTrack, toast]); }, [api, playTrack, songToTrack, toast, shuffle]);
const skipToTrackInQueue = useCallback((index: number) => { const skipToTrackInQueue = useCallback((index: number) => {
if (index >= 0 && index < queue.length) { if (index >= 0 && index < queue.length) {
@@ -290,8 +365,25 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c
}, [queue, playTrack]); }, [queue, playTrack]);
const toggleShuffle = useCallback(() => { const toggleShuffle = useCallback(() => {
setShuffle(prev => !prev); setShuffle(prev => {
}, []); const newShuffleState = !prev;
// If turning shuffle ON, shuffle the current queue
if (newShuffleState && queue.length > 0) {
setQueue(prevQueue => {
const shuffledQueue = [...prevQueue];
// Fisher-Yates shuffle algorithm
for (let i = shuffledQueue.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[shuffledQueue[i], shuffledQueue[j]] = [shuffledQueue[j], shuffledQueue[i]];
}
return shuffledQueue;
});
}
return newShuffleState;
});
}, [queue.length]);
const shuffleAllAlbums = useCallback(async () => { const shuffleAllAlbums = useCallback(async () => {
setIsLoading(true); setIsLoading(true);

View File

@@ -354,9 +354,9 @@ export const FullScreenPlayer: React.FC<FullScreenPlayerProps> = ({ isOpen, onCl
<button <button
onClick={toggleShuffle} onClick={toggleShuffle}
className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${ className={`p-2 hover:bg-gray-700/50 rounded-full transition-colors ${
shuffle ? 'text-primary' : 'text-gray-400' shuffle ? 'text-primary bg-primary/20' : 'text-gray-400'
}`} }`}
title={shuffle ? 'Disable Shuffle' : 'Enable Shuffle'} title={shuffle ? 'Shuffle On - Queue is shuffled' : 'Shuffle Off - Click to shuffle queue'}
> >
<FaShuffle className="w-5 h-5" /> <FaShuffle className="w-5 h-5" />
</button> </button>

View File

@@ -2,6 +2,7 @@
import React from 'react'; import React from 'react';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link';
import { useAudioPlayer } from '@/app/components/AudioPlayerContext'; import { useAudioPlayer } from '@/app/components/AudioPlayerContext';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
@@ -69,11 +70,15 @@ const QueuePage: React.FC = () => {
<div className="flex items-center text-sm text-muted-foreground space-x-4"> <div className="flex items-center text-sm text-muted-foreground space-x-4">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<User className="w-3 h-3" /> <User className="w-3 h-3" />
<span className="truncate">{currentTrack.artist}</span> <Link href={`/artist/${currentTrack.artistId}`} className="truncate hover:text-primary hover:underline">
{currentTrack.artist}
</Link>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Disc className="w-3 h-3" /> <Disc className="w-3 h-3" />
<span className="truncate">{currentTrack.album}</span> <Link href={`/album/${currentTrack.albumId}`} className="truncate hover:text-primary hover:underline">
{currentTrack.album}
</Link>
</div> </div>
</div> </div>
</div> </div>
@@ -143,11 +148,23 @@ const QueuePage: React.FC = () => {
<div className="flex items-center text-sm text-muted-foreground space-x-4"> <div className="flex items-center text-sm text-muted-foreground space-x-4">
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<User className="w-3 h-3" /> <User className="w-3 h-3" />
<span className="truncate">{track.artist}</span> <Link
href={`/artist/${track.artistId}`}
className="truncate hover:text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
{track.artist}
</Link>
</div> </div>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Disc className="w-3 h-3" /> <Disc className="w-3 h-3" />
<span className="truncate">{track.album}</span> <Link
href={`/album/${track.albumId}`}
className="truncate hover:text-primary hover:underline"
onClick={(e) => e.stopPropagation()}
>
{track.album}
</Link>
</div> </div>
</div> </div>
</div> </div>