diff --git a/app/components/AudioPlayerContext.tsx b/app/components/AudioPlayerContext.tsx index e8244fd..3b54100 100644 --- a/app/components/AudioPlayerContext.tsx +++ b/app/components/AudioPlayerContext.tsx @@ -116,8 +116,19 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c }, [currentTrack, api]); 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(() => { setQueue([]); @@ -132,20 +143,13 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c localStorage.removeItem('navidrome-current-track-time'); if (queue.length > 0) { - let nextTrack; - if (shuffle) { - // Pick a random track from the queue - const randomIndex = Math.floor(Math.random() * queue.length); - 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)); - } + // Always pick the first track from the queue + // If shuffle is enabled, the queue will already be shuffled + const nextTrack = queue[0]; + setQueue((prevQueue) => prevQueue.slice(1)); playTrack(nextTrack, true); // Auto-play next track } - }, [queue, playTrack, shuffle]); + }, [queue, playTrack]); const playPreviousTrack = useCallback(() => { // Clear saved timestamp when changing tracks @@ -169,7 +173,29 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c try { const { album, songs } = await api.getAlbum(albumId); 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({ title: "Album Added", @@ -185,20 +211,44 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c } finally { setIsLoading(false); } - }, [api, songToTrack, toast]); + }, [api, songToTrack, toast, shuffle]); const addArtistToQueue = useCallback(async (artistId: string) => { setIsLoading(true); try { 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) { const { songs } = await api.getAlbum(album.id); 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({ title: "Artist Added", description: `Added all albums by "${artist.name}" to queue`, @@ -213,24 +263,36 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c } finally { setIsLoading(false); } - }, [api, songToTrack, toast]); + }, [api, songToTrack, toast, shuffle]); const playAlbum = useCallback(async (albumId: string) => { setIsLoading(true); try { const { album, songs } = await api.getAlbum(albumId); 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) { - 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({ title: "Playing Album", - description: `Now playing "${album.name}"`, + description: `Now playing "${album.name}"${shuffle ? ' (shuffled)' : ''}`, }); } catch (error) { console.error('Failed to play album:', error); @@ -242,7 +304,7 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c } finally { setIsLoading(false); } - }, [api, playTrack, songToTrack, toast]); + }, [api, playTrack, songToTrack, toast, shuffle]); const playAlbumFromTrack = useCallback(async (albumId: string, startingSongId: string) => { setIsLoading(true); @@ -257,15 +319,28 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c throw new Error('Starting song not found in album'); } - // Clear the queue and set the remaining tracks after the starting track - setQueue(tracks.slice(startingIndex + 1)); - - // Play the starting track immediately - playTrack(tracks[startingIndex]); + if (shuffle) { + // If shuffle is enabled, create a shuffled queue but start with the selected track + const remainingTracks = [...tracks]; + remainingTracks.splice(startingIndex, 1); // Remove the starting track + + // 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({ title: "Playing Album", - description: `Playing "${album.name}" from "${tracks[startingIndex].name}"`, + description: `Playing "${album.name}" from "${tracks[startingIndex].name}"${shuffle ? ' (shuffled)' : ''}`, }); } catch (error) { console.error('Failed to play album from track:', error); @@ -277,7 +352,7 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c } finally { setIsLoading(false); } - }, [api, playTrack, songToTrack, toast]); + }, [api, playTrack, songToTrack, toast, shuffle]); const skipToTrackInQueue = useCallback((index: number) => { if (index >= 0 && index < queue.length) { @@ -290,8 +365,25 @@ export const AudioPlayerProvider: React.FC<{ children: React.ReactNode }> = ({ c }, [queue, playTrack]); 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 () => { setIsLoading(true); diff --git a/app/components/FullScreenPlayer.tsx b/app/components/FullScreenPlayer.tsx index 9025aa4..869e871 100644 --- a/app/components/FullScreenPlayer.tsx +++ b/app/components/FullScreenPlayer.tsx @@ -354,9 +354,9 @@ export const FullScreenPlayer: React.FC = ({ isOpen, onCl diff --git a/app/queue/page.tsx b/app/queue/page.tsx index 6f0ac1d..248327e 100644 --- a/app/queue/page.tsx +++ b/app/queue/page.tsx @@ -2,6 +2,7 @@ import React from 'react'; import Image from 'next/image'; +import Link from 'next/link'; import { useAudioPlayer } from '@/app/components/AudioPlayerContext'; import { Button } from '@/components/ui/button'; import { Separator } from '@/components/ui/separator'; @@ -69,11 +70,15 @@ const QueuePage: React.FC = () => {
- {currentTrack.artist} + + {currentTrack.artist} +
- {currentTrack.album} + + {currentTrack.album} +
@@ -143,11 +148,23 @@ const QueuePage: React.FC = () => {
- {track.artist} + e.stopPropagation()} + > + {track.artist} +
- {track.album} + e.stopPropagation()} + > + {track.album} +