feat: add auto-play flag to Track interface in AudioPlayerContext
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user