diff --git a/frontend/js/src/common/brainzplayer/BrainzPlayer.tsx b/frontend/js/src/common/brainzplayer/BrainzPlayer.tsx index 9d954035af..3f0502e199 100644 --- a/frontend/js/src/common/brainzplayer/BrainzPlayer.tsx +++ b/frontend/js/src/common/brainzplayer/BrainzPlayer.tsx @@ -551,6 +551,7 @@ export default function BrainzPlayer() { const currentQueue = brainzPlayerContextRef.current.queue; const currentAmbientQueue = brainzPlayerContextRef.current.ambientQueue; + const { shuffleMode } = brainzPlayerContextRef.current; if (currentQueue.length === 0 && currentAmbientQueue.length === 0) { handleWarning( @@ -570,6 +571,18 @@ export default function BrainzPlayer() { ) { nextListenIndex = currentPlayingListenIndex + (currentPlayingListenIndex < 0 ? 1 : 0); + } else if (shuffleMode) { + const availableIndices = currentQueue + .map((_, index) => index) + .filter((index) => index !== currentPlayingListenIndex); + + if (availableIndices.length > 0) { + const randomIndex = Math.floor(Math.random() * availableIndices.length); + nextListenIndex = availableIndices[randomIndex]; + } else { + // Fallback to sequential if only one track or error + nextListenIndex = currentPlayingListenIndex + 1; + } } else { // Otherwise, play the next track in the queue nextListenIndex = currentPlayingListenIndex + (invert === true ? -1 : 1); @@ -577,7 +590,12 @@ export default function BrainzPlayer() { // If nextListenIndex is less than 0, wrap around to the last track in the queue if (nextListenIndex < 0) { - nextListenIndex = currentQueue.length - 1; + if (shuffleMode) { + // In shuffle mode, go to a random track instead of wrapping + nextListenIndex = Math.floor(Math.random() * currentQueue.length); + } else { + nextListenIndex = currentQueue.length - 1; + } } // If nextListenIndex is within the queue length, play the next track @@ -626,7 +644,12 @@ export default function BrainzPlayer() { } // If there are no listens in the ambient queue, then play the first listen in the main queue - nextListenIndex = 0; + if (shuffleMode) { + nextListenIndex = Math.floor(Math.random() * currentQueue.length); + } else { + nextListenIndex = 0; + } + const nextListen = currentQueue[nextListenIndex]; if (!nextListen) { handleWarning( diff --git a/frontend/js/src/common/brainzplayer/BrainzPlayerContext.tsx b/frontend/js/src/common/brainzplayer/BrainzPlayerContext.tsx index ad4ed1f12c..4abcae2599 100644 --- a/frontend/js/src/common/brainzplayer/BrainzPlayerContext.tsx +++ b/frontend/js/src/common/brainzplayer/BrainzPlayerContext.tsx @@ -2,7 +2,7 @@ import * as React from "react"; import { faRepeat } from "@fortawesome/free-solid-svg-icons"; import { isEqual, isNil } from "lodash"; import { faRepeatOnce } from "../../utils/icons"; -import { listenOrJSPFTrackToQueueItem } from "./utils"; +import { listenOrJSPFTrackToQueueItem, shuffleQueue } from "./utils"; export const QueueRepeatModes = { off: { @@ -50,6 +50,7 @@ export type BrainzPlayerContextT = { queue: BrainzPlayerQueue; ambientQueue: BrainzPlayerQueue; queueRepeatMode: QueueRepeatMode; + shuffleMode: boolean; }; export const initialValue: BrainzPlayerContextT = { @@ -68,6 +69,7 @@ export const initialValue: BrainzPlayerContextT = { queue: [], ambientQueue: [], queueRepeatMode: QueueRepeatModes.off, + shuffleMode: false, }; export type BrainzPlayerActionType = Partial & { @@ -85,7 +87,8 @@ export type BrainzPlayerActionType = Partial & { | "ADD_LISTEN_TO_TOP_OF_QUEUE" | "ADD_LISTEN_TO_BOTTOM_OF_QUEUE" | "ADD_LISTEN_TO_BOTTOM_OF_AMBIENT_QUEUE" - | "ADD_MULTIPLE_LISTEN_TO_BOTTOM_OF_AMBIENT_QUEUE"; + | "ADD_MULTIPLE_LISTEN_TO_BOTTOM_OF_AMBIENT_QUEUE" + | "TOGGLE_SHUFFLE_MODE"; data?: any; }; @@ -325,6 +328,12 @@ function valueReducer( ambientQueue: [...ambientQueue, ...tracksToAdd], }; } + case "TOGGLE_SHUFFLE_MODE": { + return { + ...state, + shuffleMode: !state.shuffleMode, + }; + } default: { throw Error(`Unknown action: ${action.type}`); } diff --git a/frontend/js/src/common/brainzplayer/BrainzPlayerUI.tsx b/frontend/js/src/common/brainzplayer/BrainzPlayerUI.tsx index 104ad17d75..9cf81bd9df 100644 --- a/frontend/js/src/common/brainzplayer/BrainzPlayerUI.tsx +++ b/frontend/js/src/common/brainzplayer/BrainzPlayerUI.tsx @@ -8,6 +8,7 @@ import { faMusic, faPauseCircle, faPlayCircle, + faShuffle, faSlash, faVolumeUp, faMaximize, @@ -237,6 +238,11 @@ function BrainzPlayerUI(props: React.PropsWithChildren) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const toggleShuffleMode = React.useCallback(() => { + console.log("Test"); + dispatch({ type: "TOGGLE_SHUFFLE_MODE" }); + }, []); + const musicPlayerCoverArtRef = React.useRef(null); React.useEffect(() => { @@ -325,6 +331,7 @@ function BrainzPlayerUI(props: React.PropsWithChildren) { toggleShowVolume={toggleShowVolume} seekToPositionMs={seekToPositionMs} toggleRepeatMode={toggleRepeatMode} + toggleShuffleMode={toggleShuffleMode} submitFeedback={submitFeedback} currentListenFeedback={currentListenFeedback} musicPlayerCoverArtRef={musicPlayerCoverArtRef} @@ -449,6 +456,14 @@ function BrainzPlayerUI(props: React.PropsWithChildren) { )} + {!isMobile && ( + + )} + {!isMobile && ( ) { onClick={toggleRepeatMode} /> )} + {showFeedback && !isMobile && ( <> void; seekToPositionMs: (msTimeCode: number) => void; toggleRepeatMode: () => void; + toggleShuffleMode: () => void; submitFeedback: (score: ListenFeedBack) => Promise; currentListenFeedback: number; musicPlayerCoverArtRef: React.RefObject; @@ -166,6 +168,7 @@ function MusicPlayer(props: MusicPlayerProps) { toggleShowVolume, seekToPositionMs, toggleRepeatMode, + toggleShuffleMode, submitFeedback, currentListenFeedback, musicPlayerCoverArtRef, @@ -184,6 +187,7 @@ function MusicPlayer(props: MusicPlayerProps) { queueRepeatMode, queue, ambientQueue, + shuffleMode, } = useBrainzPlayerContext(); // Global App Context @@ -348,6 +352,14 @@ function MusicPlayer(props: MusicPlayerProps) { icon={faBarsStaggered} size="xl" /> + = queue.length - 1) { + return [...queue]; + } + + const newQueue = [...queue]; // Create a shallow copy to not modify the original + const shuffleStartIndex = currentListenIndex + 1; + if (shuffleStartIndex >= newQueue.length) { + return newQueue; + } + + const partToKeep = newQueue.slice(0, shuffleStartIndex); + const partToShuffle = newQueue.slice(shuffleStartIndex); + const shuffledPart = shuffle(partToShuffle); // Lodash shuffle returns a new shuffled array + return [...partToKeep, ...shuffledPart]; +} + export enum FeedbackValue { LIKE = 1, DISLIKE = -1,