diff --git a/backend/app.go b/backend/app.go index 70ff658..90b26ef 100644 --- a/backend/app.go +++ b/backend/app.go @@ -21,6 +21,7 @@ import ( "github.com/dweymouth/supersonic/backend/player/mpv" "github.com/dweymouth/supersonic/backend/util" "github.com/dweymouth/supersonic/backend/windows" + "github.com/dweymouth/supersonic/sharedutil" "github.com/google/uuid" "github.com/20after4/configdir" @@ -28,11 +29,13 @@ import ( ) const ( - configFile = "config.toml" - portableDir = "supersonic_portable" - savedQueueFile = "saved_queue.json" - themesDir = "themes" - audioCacheSubdir = "audio" + configFile = "config.toml" + portableDir = "supersonic_portable" + savedQueueFile = "saved_queue.json" + savedUnshuffledQueueFile = "saved_unshuffled_queue.json" + savedShuffledQueueFile = "saved_shuffled_queue.json" + themesDir = "themes" + audioCacheSubdir = "audio" ) var ( @@ -613,31 +616,80 @@ func (a *App) SavePlayQueueIfEnabled() { queueServer = qs } } - SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager, path.Join(a.configDir, savedQueueFile), queueServer) + SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager.GetActivePlayQueue(), a.PlaybackManager, path.Join(a.configDir, savedQueueFile), queueServer) + if a.Config.Playback.Shuffle { + // if shuffle + // save the unshuffled queue to enable unshuffling on restarting supersonic + // save the shuffled queue again to enable checking if the playQueue was changed server side on start up + + // both files are just saved locally + SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager.GetPlayQueue(), a.PlaybackManager, path.Join(a.configDir, savedUnshuffledQueueFile), nil) + SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager.GetShuffledPlayQueue(), a.PlaybackManager, path.Join(a.configDir, savedShuffledQueueFile), nil) + } } func (a *App) LoadSavedPlayQueue() error { queueFilePath := path.Join(a.configDir, savedQueueFile) - queue, err := LoadPlayQueue(queueFilePath, a.ServerManager, a.Config.Application.SaveQueueToServer) + playQueue, err := LoadPlayQueue(queueFilePath, a.ServerManager, a.Config.Application.SaveQueueToServer) if err != nil { return err } - if len(queue.Tracks) == 0 { + + var unshuffledPlayQueue *SavedPlayQueue + var shuffledPlayQueue *SavedPlayQueue + + isShuffle := a.Config.Playback.Shuffle + if isShuffle { + unshuffledQueueFilePath := path.Join(a.configDir, savedUnshuffledQueueFile) + unshuffledPlayQueue, err = LoadPlayQueue(unshuffledQueueFilePath, a.ServerManager, false) + + if err != nil { + return err + } + + shuffledQueueFilePath := path.Join(a.configDir, savedShuffledQueueFile) + shuffledPlayQueue, err = LoadPlayQueue(shuffledQueueFilePath, a.ServerManager, false) + + if err != nil { + return err + } + } + + if len(playQueue.Tracks) == 0 { return nil } - if len(a.PlaybackManager.GetPlayQueue()) > 0 { + if len(a.PlaybackManager.GetActivePlayQueue()) > 0 { // don't restore play queue if the user has already queued new tracks return nil } - a.PlaybackManager.LoadTracks(queue.Tracks, Replace, false) - if queue.TrackIndex >= 0 && queue.TrackIndex < len(queue.Tracks) { + if isShuffle { + serverStatePlayQueue := sharedutil.CopyTrackSliceToMediaItemSlice(playQueue.Tracks) + clientStatePlayQueue := sharedutil.CopyTrackSliceToMediaItemSlice(shuffledPlayQueue.Tracks) + + // Compare items by ID. This fails if any 2 elements don't match up. Two queues with the same items but different order will thus not count as same + if slices.EqualFunc(serverStatePlayQueue, clientStatePlayQueue, func(a, b mediaprovider.MediaItem) bool { + return (a.Metadata().ID == b.Metadata().ID) + }) { + a.PlaybackManager.SetQueueState(playQueue.Tracks, ShuffledPlayQueue) + a.PlaybackManager.SetQueueState(unshuffledPlayQueue.Tracks, PlayQueue) + } else { + a.PlaybackManager.SetShuffle(false) + a.PlaybackManager.SetQueueState(playQueue.Tracks, PlayQueue) + } + + } else { + a.PlaybackManager.SetQueueState(playQueue.Tracks, PlayQueue) + } + + if playQueue.TrackIndex >= 0 && playQueue.TrackIndex < len(playQueue.Tracks) { // TODO: This isn't ideal but doesn't seem to cause an audible play-for-a-split-second artifact - a.PlaybackManager.PlayTrackAt(queue.TrackIndex) + a.PlaybackManager.PlayTrackAt(playQueue.TrackIndex) a.PlaybackManager.Pause() time.Sleep(100 * time.Millisecond) // MPV seek fails if run quickly after - a.PlaybackManager.SeekSeconds(queue.TimePos) + a.PlaybackManager.SeekSeconds(playQueue.TimePos) } + return nil } diff --git a/backend/config.go b/backend/config.go index 6883f21..d5bd475 100644 --- a/backend/config.go +++ b/backend/config.go @@ -123,6 +123,7 @@ type NowPlayingPageConfig struct { type PlaybackConfig struct { Autoplay bool + Shuffle bool RepeatMode string SkipOneStarWhenShuffling bool SkipKeywordWhenShuffling string @@ -263,6 +264,7 @@ func DefaultConfig(appVersionTag string) *Config { }, Playback: PlaybackConfig{ Autoplay: false, + Shuffle: false, RepeatMode: "None", UseWaveformSeekbar: false, }, diff --git a/backend/playbackcommands.go b/backend/playbackcommands.go index 2632fef..3bb9d8b 100644 --- a/backend/playbackcommands.go +++ b/backend/playbackcommands.go @@ -25,6 +25,8 @@ const ( // arg2: InsertMode // arg3: bool (shuffle) cmdLoadItems + cmdLoadItemsAndPlayAtIdx + cmdSetQueueState cmdLoadRadioStation // arg: *mediaprovider.RadioStation, arg2: InsertQueueMode cmdForceRestartPlayback @@ -160,6 +162,29 @@ func (c *playbackCommandQueue) LoadItems(items []mediaprovider.MediaItem, insert c.cmdAvailable.Signal() } +func (c *playbackCommandQueue) LoadItemsAndPlayAtIdx(items []mediaprovider.MediaItem, shuffle bool, idx int) { + c.mutex.Lock() + c.queue = append(c.queue, playbackCommand{ + Type: cmdLoadItemsAndPlayAtIdx, + Arg: items, + Arg2: shuffle, + Arg3: idx, + }) + c.mutex.Unlock() + c.cmdAvailable.Signal() +} + +func (c *playbackCommandQueue) SetQueueState(tracks []*mediaprovider.Track, queueType QueueType) { + c.mutex.Lock() + c.queue = append(c.queue, playbackCommand{ + Type: cmdSetQueueState, + Arg: tracks, + Arg2: queueType, + }) + c.mutex.Unlock() + c.cmdAvailable.Signal() +} + func (c *playbackCommandQueue) addCommand(command playbackCommand) { c.mutex.Lock() c.queue = append(c.queue, command) @@ -194,7 +219,7 @@ func (c *playbackCommandQueue) seekBackOrFwd(direction int) { switch cmd.Type { case cmdSeekFwdBackN: lastIdx = i - case cmdRemoveTracksFromQueue, cmdLoadItems, cmdPlayTrackAt, + case cmdRemoveTracksFromQueue, cmdLoadItems, cmdSetQueueState, cmdPlayTrackAt, cmdLoadRadioStation, cmdUpdatePlayQueue, cmdStopAndClearPlayQueue: // any queue-modifying command means we can't coalesce any // more seekFwdBackN commands before here diff --git a/backend/playbackengine.go b/backend/playbackengine.go index 8db1ff0..3819e38 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -6,6 +6,7 @@ import ( "fmt" "log" "math/rand" + "slices" "strings" "time" @@ -33,6 +34,14 @@ const ( Append ) +type QueueType int + +const ( + PlayQueue QueueType = iota + ShuffledPlayQueue + Both +) + // The playback loop mode (LoopNone, LoopAll, LoopOne). type LoopMode int @@ -62,10 +71,13 @@ type playbackEngine struct { latestTrackPosition float64 // cleared by checkScrobble callbacksDisabled bool - playQueue []mediaprovider.MediaItem + playQueue []mediaprovider.MediaItem + shuffledPlayQueue []mediaprovider.MediaItem + nowPlayingIdx int isRadio bool loopMode LoopMode + shuffle bool pauseAfterCurrent bool // flag to pause playback after current track ends @@ -98,6 +110,7 @@ type playbackEngine struct { onSongChange []func(nowPlaying mediaprovider.MediaItem, justScrobbledIfAny *mediaprovider.Track) onPlayTimeUpdate []func(float64, float64, bool) onLoopModeChange []func(LoopMode) + onShuffleChange []func(bool) onVolumeChange []func(int) onSeek []func() onPaused []func() @@ -137,6 +150,8 @@ func NewPlaybackEngine( pm.loopMode = LoopOne } + pm.shuffle = playbackCfg.Shuffle + pm.registerPlayerCallbacks(p) s.OnLogout(func() { pm.StopAndClearPlayQueue() @@ -216,12 +231,78 @@ func (p *playbackEngine) SetPlayer(pl player.BasePlayer) error { return nil } +// ======================= START PLAY QUEUE FUNCS =========================== + +func (p *playbackEngine) getPlayQueue() []mediaprovider.MediaItem { + return p.playQueue +} + +func (p *playbackEngine) getShuffledPlayQueue() []mediaprovider.MediaItem { + return p.shuffledPlayQueue +} + +func (p *playbackEngine) getActivePlayQueue() []mediaprovider.MediaItem { + if p.shuffle { + return p.shuffledPlayQueue + } + return p.playQueue +} + +func (p *playbackEngine) getPlayQueueLength() int { + return len(p.getActivePlayQueue()) +} + +func (p *playbackEngine) clearPlayQueue() { + p.player.Stop(false) + p.nowPlayingIdx = -1 + p.playQueue = nil + p.shuffledPlayQueue = nil +} + +func (p *playbackEngine) setPlayQueue(items []mediaprovider.MediaItem) { + p.playQueue = items +} + +func (p *playbackEngine) setShuffledPlayQueue(items []mediaprovider.MediaItem) { + p.shuffledPlayQueue = items +} + +func (p *playbackEngine) getPlayQueueItemAt(idx int) mediaprovider.MediaItem { + return p.getActivePlayQueue()[idx] +} + +func (p *playbackEngine) insertItemsIntoPlayQueueAt(items []mediaprovider.MediaItem, idx int, queueType QueueType) { + switch queueType { + case Both: + p.playQueue = append(p.playQueue[:idx], append(items, p.playQueue[idx:]...)...) + p.shuffledPlayQueue = append(p.shuffledPlayQueue[:idx], append(items, p.shuffledPlayQueue[idx:]...)...) + case PlayQueue: + p.playQueue = append(p.playQueue[:idx], append(items, p.playQueue[idx:]...)...) + case ShuffledPlayQueue: + p.shuffledPlayQueue = append(p.shuffledPlayQueue[:idx], append(items, p.shuffledPlayQueue[idx:]...)...) + } +} + +func (p *playbackEngine) GetPlayQueueDeepCopy() []mediaprovider.MediaItem { + return deepCopyMediaItemSlice(p.getPlayQueue()) +} + +func (p *playbackEngine) GetShuffledPlayQueueDeepCopy() []mediaprovider.MediaItem { + return deepCopyMediaItemSlice(p.getShuffledPlayQueue()) +} + +func (p *playbackEngine) GetActivePlayQueueDeepCopy() []mediaprovider.MediaItem { + return deepCopyMediaItemSlice(p.getActivePlayQueue()) +} + +// ======================== END PLAY QUEUE FUNCS ============================= + func (p *playbackEngine) PlayTrackAt(idx int) error { return p.playTrackAt(idx, 0) } func (p *playbackEngine) playTrackAt(idx int, startTime float64) error { - if l := len(p.playQueue); idx < 0 || idx >= l { + if l := p.getPlayQueueLength(); idx < 0 || idx >= l { return fmt.Errorf("track index (%d) out of range (0-%d)", idx, l) } // scrobble current track if needed @@ -234,10 +315,10 @@ func (p *playbackEngine) playTrackAt(idx int, startTime float64) error { // Gets the curently playing media item, if any. func (p *playbackEngine) NowPlaying() mediaprovider.MediaItem { - if p.nowPlayingIdx < 0 || len(p.playQueue) == 0 || p.player.GetStatus().State == player.Stopped { + if p.nowPlayingIdx < 0 || p.getPlayQueueLength() == 0 || p.player.GetStatus().State == player.Stopped { return nil } - return p.playQueue[p.nowPlayingIdx] + return p.getPlayQueueItemAt(p.nowPlayingIdx) } func (p *playbackEngine) NowPlayingIndex() int { @@ -261,6 +342,58 @@ func (p *playbackEngine) GetLoopMode() LoopMode { return p.loopMode } +func (p *playbackEngine) GetTrackIdxByIdFrom(items []mediaprovider.MediaItem, id string) int { + foundIdx := -1 + for i, tr := range items { + if tr.Metadata().ID == id { + foundIdx = i + break + } + } + return foundIdx +} + +func (p *playbackEngine) SetShuffle(shuffle bool) { + if p.shuffle == shuffle { + return + } + + for _, cb := range p.onShuffleChange { + cb(shuffle) + } + p.shuffle = shuffle + + // guard against changing shuffle with an empty queue + if p.getPlayQueue() == nil || len(p.getPlayQueue()) == 0 { + return + } + + newNowPlayingIdx := 0 + if shuffle { + shuffledQueue := deepCopyMediaItemSlice(p.playQueue) + rand.Shuffle(len(shuffledQueue), func(i, j int) { + shuffledQueue[i], shuffledQueue[j] = shuffledQueue[j], shuffledQueue[i] + }) + if p.nowPlayingIdx >= 0 && len(p.getPlayQueue()) > p.nowPlayingIdx { + nowPlayingID := p.getPlayQueue()[p.nowPlayingIdx].Metadata().ID + p.setShuffledPlayQueue(sharedutil.ReorderItems(shuffledQueue, []int{p.GetTrackIdxByIdFrom(shuffledQueue, nowPlayingID)}, 0)) + } else { + return + } + } else { + if p.nowPlayingIdx >= 0 && len(p.getShuffledPlayQueue()) > p.nowPlayingIdx { + nowPlayingID := p.getShuffledPlayQueue()[p.nowPlayingIdx].Metadata().ID + newNowPlayingIdx = p.GetTrackIdxByIdFrom(p.playQueue, nowPlayingID) + } else { + return + } + } + + p.nowPlayingIdx = newNowPlayingIdx + p.handleNextTrackUpdated() + p.invokeNoArgCallbacks(p.onQueueChange) +} + func (p *playbackEngine) PlaybackStatus() PlaybackStatus { stat := p.pendingPlayerChangeStatus if !p.pendingPlayerChange { @@ -311,7 +444,7 @@ func (p *playbackEngine) SeekFwdBackN(n int) error { return p.player.SeekSeconds(0) // seek back in current song } - lastIdx := len(p.playQueue) - 1 + lastIdx := p.getPlayQueueLength() - 1 newIdx := min(lastIdx, max(0, idx+n)) if idx == lastIdx && n > 0 { @@ -356,6 +489,23 @@ func (p *playbackEngine) Continue() error { return p.player.Continue() } +// Load items into the specified queue(s). This overrides the queue. For standard inserts or replaces use p.LoadItems +// This is used on program startup to populate both the playQueue and shuffledPlayQueue with the previously saved client state. +func (p *playbackEngine) SetQueueState(tracks []*mediaprovider.Track, queueType QueueType) error { + newTracks := sharedutil.CopyTrackSliceToMediaItemSlice(tracks) + switch queueType { + case PlayQueue: + p.setPlayQueue(newTracks) + case ShuffledPlayQueue: + p.setShuffledPlayQueue(newTracks) + case Both: + p.setPlayQueue(newTracks) + p.setShuffledPlayQueue(newTracks) + } + p.invokeNoArgCallbacks(p.onQueueChange) + return nil +} + // Load items into the play queue. // If replacing the current queue (!appendToQueue), playback will be stopped. func (p *playbackEngine) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error { @@ -363,32 +513,73 @@ func (p *playbackEngine) LoadItems(items []mediaprovider.MediaItem, insertQueueM return p.doLoaditems(newItems, insertQueueMode, shuffle) } +// Load items into the play queue. +// If replacing the current queue (!appendToQueue), playback will be stopped. +func (p *playbackEngine) LoadItemsAndPlayAtIdx(items []mediaprovider.MediaItem, shuffle bool, idx int) error { + newItems := deepCopyMediaItemSlice(items) + + if p.shuffle || shuffle { + rand.Shuffle(len(newItems), func(i, j int) { + newItems[i], newItems[j] = newItems[j], newItems[i] + }) + if idx < len(items) { + nowPlayingID := items[idx].Metadata().ID + p.setPlayQueue(deepCopyMediaItemSlice(items)) + p.setShuffledPlayQueue(sharedutil.ReorderItems(newItems, []int{p.GetTrackIdxByIdFrom(newItems, nowPlayingID)}, 0)) + p.PlayTrackAt(0) + } + } else { + p.doLoaditems(newItems, Replace, shuffle) + p.PlayTrackAt(idx) + } + + p.handleNextTrackUpdated() + p.invokeNoArgCallbacks(p.onQueueChange) + + return nil +} + // Load tracks into the play queue. // If replacing the current queue (!appendToQueue), playback will be stopped. func (p *playbackEngine) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) error { - newTracks := copyTrackSliceToMediaItemSlice(tracks) + newTracks := sharedutil.CopyTrackSliceToMediaItemSlice(tracks) return p.doLoaditems(newTracks, insertQueueMode, shuffle) } func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) error { + queueType := PlayQueue + if insertQueueMode == Replace { - p.player.Stop(false) - p.nowPlayingIdx = -1 - p.playQueue = nil + p.clearPlayQueue() } - if nextChanged := len(items) > 0 && (insertQueueMode != Append || (p.nowPlayingIdx == len(p.playQueue)-1)); nextChanged { + if nextChanged := len(items) > 0 && (insertQueueMode != Append || (p.nowPlayingIdx == p.getPlayQueueLength()-1)); nextChanged { defer p.handleNextTrackUpdated() } - if shuffle { - rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] }) - } + insertIdx := p.getPlayQueueLength() - insertIdx := len(p.playQueue) - if insertQueueMode == InsertNext { - insertIdx = p.nowPlayingIdx + 1 + if insertQueueMode == Replace { + if shuffle || p.shuffle { + shuffledItems := deepCopyMediaItemSlice(items) + rand.Shuffle(len(shuffledItems), func(i, j int) { shuffledItems[i], shuffledItems[j] = shuffledItems[j], shuffledItems[i] }) + + p.insertItemsIntoPlayQueueAt(items, insertIdx, PlayQueue) + p.insertItemsIntoPlayQueueAt(shuffledItems, insertIdx, ShuffledPlayQueue) + } else { + p.insertItemsIntoPlayQueueAt(items, insertIdx, queueType) + } + } else { + if shuffle { + rand.Shuffle(len(items), func(i, j int) { items[i], items[j] = items[j], items[i] }) + } + if insertQueueMode == InsertNext { + insertIdx = p.nowPlayingIdx + 1 + } + if p.shuffle { + queueType = Both + } + p.insertItemsIntoPlayQueueAt(items, insertIdx, queueType) } - p.playQueue = append(p.playQueue[:insertIdx], append(items, p.playQueue[insertIdx:]...)...) p.invokeNoArgCallbacks(p.onQueueChange) return nil @@ -396,46 +587,32 @@ func (p *playbackEngine) doLoaditems(items []mediaprovider.MediaItem, insertQueu func (p *playbackEngine) LoadRadioStation(radio *mediaprovider.RadioStation, insertMode InsertQueueMode) { if insertMode == Replace { - p.player.Stop(false) - p.nowPlayingIdx = -1 - p.playQueue = nil + p.clearPlayQueue() } - if nextChanged := insertMode == InsertNext || (insertMode == Append && p.nowPlayingIdx == len(p.playQueue)-1); nextChanged { + if nextChanged := insertMode == InsertNext || (insertMode == Append && p.nowPlayingIdx == p.getPlayQueueLength()-1); nextChanged { p.handleNextTrackUpdated() } - insertIdx := len(p.playQueue) + insertIdx := p.getPlayQueueLength() if insertMode == InsertNext { insertIdx = p.nowPlayingIdx + 1 } - new := make([]mediaprovider.MediaItem, len(p.playQueue)+1) - firstHalf := p.playQueue[:insertIdx] - copy(new, firstHalf) - new[len(firstHalf)] = radio - copy(new[len(firstHalf)+1:], p.playQueue[insertIdx:]) - p.playQueue = new - + p.insertItemsIntoPlayQueueAt([]mediaprovider.MediaItem{radio}, insertIdx, Both) p.invokeNoArgCallbacks(p.onQueueChange) } // Stop playback and clear the play queue. func (p *playbackEngine) StopAndClearPlayQueue() { - changed := len(p.playQueue) > 0 - p.player.Stop(false) - p.playQueue = nil - p.nowPlayingIdx = -1 + changed := p.getPlayQueueLength() > 0 + p.clearPlayQueue() if changed { p.invokeNoArgCallbacks(p.onQueueChange) } } -func (p *playbackEngine) GetPlayQueue() []mediaprovider.MediaItem { - return deepCopyMediaItemSlice(p.playQueue) -} - // Any time the user changes the favorite status of a track elsewhere in the app, // this should be called to ensure the in-memory track model is updated. func (p *playbackEngine) OnTrackFavoriteStatusChanged(id string, fav bool) { - if item := sharedutil.FindMediaItemByID(id, p.playQueue); item != nil { + if item := sharedutil.FindMediaItemByID(id, p.getActivePlayQueue()); item != nil { if tr, ok := item.(*mediaprovider.Track); ok { tr.Favorite = fav } @@ -445,7 +622,7 @@ func (p *playbackEngine) OnTrackFavoriteStatusChanged(id string, fav bool) { // Any time the user changes the rating of a track elsewhere in the app, // this should be called to ensure the in-memory track model is updated. func (p *playbackEngine) OnTrackRatingChanged(id string, rating int) { - if item := sharedutil.FindMediaItemByID(id, p.playQueue); item != nil { + if item := sharedutil.FindMediaItemByID(id, p.getActivePlayQueue()); item != nil { if tr, ok := item.(*mediaprovider.Track); ok { tr.Rating = rating } @@ -459,7 +636,7 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error newQueue := deepCopyMediaItemSlice(items) newNowPlayingIdx := -1 if p.nowPlayingIdx >= 0 { - nowPlayingID := p.playQueue[p.nowPlayingIdx].Metadata().ID + nowPlayingID := p.getPlayQueueItemAt(p.nowPlayingIdx).Metadata().ID for i, tr := range newQueue { if tr.Metadata().ID == nowPlayingID { newNowPlayingIdx = i @@ -467,8 +644,11 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error } } } - - p.playQueue = newQueue + if p.shuffle { + p.setShuffledPlayQueue(newQueue) + } else { + p.setPlayQueue(newQueue) + } if p.nowPlayingIdx >= 0 && newNowPlayingIdx == -1 { return p.Stop() } @@ -482,35 +662,39 @@ func (p *playbackEngine) UpdatePlayQueue(items []mediaprovider.MediaItem) error } func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { - newQueue := make([]mediaprovider.MediaItem, 0, len(p.playQueue)-len(idxs)) - idxSet := sharedutil.ToSet(idxs) isPlayingTrackRemoved := false isNextPlayingTrackremoved := false nowPlaying := p.NowPlayingIndex() newNowPlaying := nowPlaying - for i, tr := range p.playQueue { - if _, ok := idxSet[i]; ok { - if i < nowPlaying { - // if removing a track earlier than the currently playing one (if any), - // decrement new now playing index by one to account for new position in queue - newNowPlaying-- - } else if i == nowPlaying { - isPlayingTrackRemoved = true - // If we are removing the currently playing track, we need to scrobble it - p.checkScrobble() - p.alreadyScrobbled = true - } else if nowPlaying >= 0 && i == nowPlaying+1 { - isNextPlayingTrackremoved = true + + if p.shuffle { + // remove tracks by ID from playQueue + ids := p.GetTrackIdsFromIdx(idxs) + newPlayQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs)) + for _, tr := range p.getPlayQueue() { + if slices.Contains(ids, tr.Metadata().ID) { + //remove id from id list, handles having the same track present multiple times in playQueue + idx := slices.Index(ids, tr.Metadata().ID) + ids = slices.Delete(ids, idx, idx+1) + } else { + // not removing this track + newPlayQueue = append(newPlayQueue, tr) } - } else { - // not removing this track - newQueue = append(newQueue, tr) } + p.setPlayQueue(newPlayQueue) + + newShuffledQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs)) + p.RemoveTracksFromQueueByIdx(idxs, &newShuffledQueue, &newNowPlaying, &isPlayingTrackRemoved, &isNextPlayingTrackremoved) + p.setShuffledPlayQueue(newShuffledQueue) + } else { + newPlayQueue := make([]mediaprovider.MediaItem, 0, p.getPlayQueueLength()-len(idxs)) + p.RemoveTracksFromQueueByIdx(idxs, &newPlayQueue, &newNowPlaying, &isPlayingTrackRemoved, &isNextPlayingTrackremoved) + p.setPlayQueue(newPlayQueue) } - p.playQueue = newQueue + p.nowPlayingIdx = newNowPlaying if isPlayingTrackRemoved { - if newNowPlaying == len(newQueue) { + if newNowPlaying == p.getPlayQueueLength() { // we had been playing the last track, and removed it p.Stop() } else { @@ -520,7 +704,7 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { // setNextTrack and onSongChange callbacks will be handled // when we receive new track event from player } else if isNextPlayingTrackremoved { - if newNowPlaying < len(newQueue)-1 { + if newNowPlaying < p.getPlayQueueLength()-1 { p.handleNextTrackUpdated() } else { // no next track to play @@ -531,6 +715,39 @@ func (p *playbackEngine) RemoveTracksFromQueue(idxs []int) { p.invokeNoArgCallbacks(p.onQueueChange) } +func (p *playbackEngine) RemoveTracksFromQueueByIdx(idxs []int, newQueue *[]mediaprovider.MediaItem, newNowPlaying *int, isPlayingTrackRemoved *bool, isNextPlayingTrackRemoved *bool) { + idxSet := sharedutil.ToSet(idxs) + nowPlaying := p.NowPlayingIndex() + + for i, tr := range p.getActivePlayQueue() { + if _, ok := idxSet[i]; ok { + if i < nowPlaying { + // if removing a track earlier than the currently playing one (if any), + // decrement new now playing index by one to account for new position in queue + *newNowPlaying-- + } else if i == nowPlaying { + *isPlayingTrackRemoved = true + // If we are removing the currently playing track, we need to scrobble it + p.checkScrobble() + p.alreadyScrobbled = true + } else if nowPlaying >= 0 && i == nowPlaying+1 { + *isNextPlayingTrackRemoved = true + } + } else { + // not removing this track + *newQueue = append(*newQueue, tr) + } + } +} + +func (p *playbackEngine) GetTrackIdsFromIdx(idx []int) []string { + ids := make([]string, 0, len(idx)) + for _, v := range idx { + ids = append(ids, p.getPlayQueueItemAt(v).Metadata().ID) + } + return ids +} + func (p *playbackEngine) SetReplayGainOptions(config ReplayGainConfig) { rGainPlayer, ok := p.player.(player.ReplayGainPlayer) if !ok { @@ -577,11 +794,11 @@ func (p *playbackEngine) cacheNextTracks() { // the "currently" playing track, since we're probably about to play it npI := max(p.nowPlayingIdx, 0) for _, idx := range [3]int{npI, npI + 1, npI + 2} { - if idx > 0 && idx < len(p.playQueue) { - item := p.playQueue[idx] + if idx > 0 && idx < p.getPlayQueueLength() { + item := p.getPlayQueueItemAt(idx) if item.Metadata().Type == mediaprovider.MediaItemTypeTrack { fetch = append(fetch, AudioCacheRequest{ - ID: p.playQueue[idx].Metadata().ID, + ID: p.getPlayQueueItemAt(idx).Metadata().ID, DownloadURL: p.getMediaURLForIdx(idx), }) } @@ -606,14 +823,14 @@ func (p *playbackEngine) handleOnTrackChange() { } if p.pendingTrackChangeNum < 0 && (p.wasStopped || p.loopMode != LoopOne) { p.nowPlayingIdx++ - if p.loopMode == LoopAll && p.nowPlayingIdx == len(p.playQueue) { + if p.loopMode == LoopAll && p.nowPlayingIdx == p.getPlayQueueLength() { p.nowPlayingIdx = 0 // wrapped around } } else if p.pendingTrackChangeNum >= 0 { p.nowPlayingIdx = p.pendingTrackChangeNum p.pendingTrackChangeNum = -1 } - nowPlaying := p.playQueue[p.nowPlayingIdx] + nowPlaying := p.getPlayQueueItemAt(p.nowPlayingIdx) _, isRadio := nowPlaying.(*mediaprovider.RadioStation) p.isRadio = isRadio @@ -656,7 +873,7 @@ func (p *playbackEngine) handleNextTrackUpdated() { for _, cb := range p.onBeforeSongChange { var item mediaprovider.MediaItem if idx := p.nextPlayingIndex(); idx >= 0 { - item = p.playQueue[idx] + item = p.getPlayQueueItemAt(idx) } cb(item) } @@ -665,14 +882,14 @@ func (p *playbackEngine) handleNextTrackUpdated() { func (p *playbackEngine) nextPlayingIndex() int { switch p.loopMode { case LoopNone: - if p.nowPlayingIdx >= len(p.playQueue)-1 { + if p.nowPlayingIdx >= p.getPlayQueueLength()-1 { return -1 } return p.nowPlayingIdx + 1 case LoopOne: return p.nowPlayingIdx case LoopAll: - if p.nowPlayingIdx >= len(p.playQueue)-1 { + if p.nowPlayingIdx >= p.getPlayQueueLength()-1 { return 0 } return p.nowPlayingIdx + 1 @@ -684,7 +901,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error { var item mediaprovider.MediaItem var url string if idx >= 0 { - item = p.playQueue[idx] + item = p.getPlayQueueItemAt(idx) url = p.getMediaURLForIdx(idx) } track, isTrack := item.(*mediaprovider.Track) @@ -709,7 +926,6 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error { } else { title = icytitle } - log.Println("Radio metadata changed: ", icytitle) for _, cb := range p.onRadioMetadataChange { cb(meta.Name, title, artist) } @@ -728,7 +944,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error { } else if trP, ok := p.player.(player.TrackPlayer); ok { var track *mediaprovider.Track if idx >= 0 { - track, ok = p.playQueue[idx].(*mediaprovider.Track) + track, ok = p.getPlayQueueItemAt(idx).(*mediaprovider.Track) if !ok { return errors.New("cannot play non-Track media item with TrackPlayer") } @@ -743,7 +959,7 @@ func (p *playbackEngine) setTrack(idx int, next bool, startTime float64) error { func (p *playbackEngine) getMediaURLForIdx(idx int) string { var url string - item := p.playQueue[idx] + item := p.getPlayQueueItemAt(idx) if tr, ok := item.(*mediaprovider.Track); ok { var ts *mediaprovider.TranscodeSettings if p.transcodeCfg.RequestTranscode { @@ -765,10 +981,10 @@ func (p *playbackEngine) setNextTrack(idx int) error { // call BEFORE updating p.nowPlayingIdx func (p *playbackEngine) checkScrobble() { - if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 { + if !p.scrobbleCfg.Enabled || p.getPlayQueueLength() == 0 || p.nowPlayingIdx < 0 { return } - track, ok := p.playQueue[p.nowPlayingIdx].(*mediaprovider.Track) + track, ok := p.getPlayQueueItemAt(p.nowPlayingIdx).(*mediaprovider.Track) if !ok { return // radio stations are not scrobbled } @@ -794,10 +1010,10 @@ func (p *playbackEngine) checkScrobble() { } func (p *playbackEngine) sendNowPlayingScrobble() { - if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 { + if !p.scrobbleCfg.Enabled || p.getPlayQueueLength() == 0 || p.nowPlayingIdx < 0 { return } - track, ok := p.playQueue[p.nowPlayingIdx].(*mediaprovider.Track) + track, ok := p.getPlayQueueItemAt(p.nowPlayingIdx).(*mediaprovider.Track) if !ok { return // radio stations are not scrobbled } @@ -821,14 +1037,6 @@ func deepCopyMediaItemSlice(tracks []mediaprovider.MediaItem) []mediaprovider.Me return newTracks } -func copyTrackSliceToMediaItemSlice(tracks []*mediaprovider.Track) []mediaprovider.MediaItem { - newTracks := make([]mediaprovider.MediaItem, len(tracks)) - for i, tr := range tracks { - newTracks[i] = tr.Copy() - } - return newTracks -} - func (p *playbackEngine) invokeOnSongChangeCallbacks() { if p.callbacksDisabled { return @@ -888,6 +1096,9 @@ func (p *playbackEngine) handleTimePosUpdate(seeked bool) { p.needToSetNextTrack = false if nextIdx := p.nextPlayingIndex(); nextIdx >= 0 && nextIdx < len(p.playQueue) { p.setNextTrack(nextIdx) + } else { + // no next track to play, ensure player knows this + p.setNextTrack(-1) } } if p.callbacksDisabled { diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 8b9e361..41378bb 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -109,7 +109,7 @@ func (p *PlaybackManager) addOnTrackChangeHook() { // enqueue autoplay tracks if enabled and nearing end of queue if p.cfg.Autoplay && !p.pendingAutoplay && totalTime-curTime < 10.0 && - p.NowPlayingIndex() == len(p.engine.playQueue)-1 { + p.NowPlayingIndex() == p.engine.getPlayQueueLength()-1 { p.enqueueAutoplayTracks() } }) @@ -132,7 +132,7 @@ func (p *PlaybackManager) addOnTrackChangeHook() { return } // workaround for https://github.com/dweymouth/supersonic/issues/483 (see above comment) - if p.NowPlayingIndex() != len(p.engine.playQueue) && p.PlaybackStatus().State == player.Playing { + if p.NowPlayingIndex() != p.engine.getPlayQueueLength() && p.PlaybackStatus().State == player.Playing { p.lastPlayTime = 0 go func() { time.Sleep(300 * time.Millisecond) @@ -333,6 +333,11 @@ func (p *PlaybackManager) OnLoopModeChange(cb func(LoopMode)) { p.engine.onLoopModeChange = append(p.engine.onLoopModeChange, cb) } +// Registers a callback that is notified whenever the shuffle state changes. +func (p *PlaybackManager) OnShuffleChange(cb func(bool)) { + p.engine.onShuffleChange = append(p.engine.onShuffleChange, cb) +} + // Registers a callback that is notified whenever the volume changes. func (p *PlaybackManager) OnVolumeChange(cb func(int)) { p.engine.onVolumeChange = append(p.engine.onVolumeChange, cb) @@ -373,6 +378,10 @@ func (p *PlaybackManager) LoadAlbum(albumID string, insertQueueMode InsertQueueM return nil } +func (p *PlaybackManager) IsShuffle() bool { + return p.cfg.Shuffle +} + // Loads the specified playlist into the play queue. func (p *PlaybackManager) LoadPlaylist(playlistID string, insertQueueMode InsertQueueMode, shuffle bool) error { playlist, err := p.engine.sm.Server.GetPlaylist(playlistID) @@ -386,16 +395,30 @@ func (p *PlaybackManager) LoadPlaylist(playlistID string, insertQueueMode Insert // Load tracks into the play queue. // If replacing the current queue (!appendToQueue), playback will be stopped. func (p *PlaybackManager) LoadTracks(tracks []*mediaprovider.Track, insertQueueMode InsertQueueMode, shuffle bool) { - items := copyTrackSliceToMediaItemSlice(tracks) + items := sharedutil.CopyTrackSliceToMediaItemSlice(tracks) p.cmdQueue.LoadItems(items, insertQueueMode, shuffle) } -// Load items into the play queue. +// Replaces the playQueue with tracks and moves the track at idx to position 0 +func (p *PlaybackManager) LoadTracksAndPlayAtIdx(tracks []*mediaprovider.Track, shuffle bool, idx int) { + items := sharedutil.CopyTrackSliceToMediaItemSlice(tracks) + p.cmdQueue.LoadItemsAndPlayAtIdx(items, shuffle, idx) +} + +// Load items into the currently active queue. (shuffledPlayQueue/playQueue) // If replacing the current queue (!appendToQueue), playback will be stopped. +// Loading items into the shuffledPlayQueue may also modify the playQueue func (p *PlaybackManager) LoadItems(items []mediaprovider.MediaItem, insertQueueMode InsertQueueMode, shuffle bool) { p.cmdQueue.LoadItems(items, insertQueueMode, shuffle) } +// Replaces the specified queue (PlayQueue/ShuffledPlayQueue) with the given tracks. +// This is used when starting supersonic to load the queue state and directly overrides any previous data. +// For replacing the queue while supersonic is running, use LoadTracks +func (p *PlaybackManager) SetQueueState(tracks []*mediaprovider.Track, queueType QueueType) { + p.cmdQueue.SetQueueState(tracks, queueType) +} + // Replaces the play queue with the given set of tracks. // Does not stop playback if the currently playing track is in the new queue, // but updates the now playing index to point to the first instance of the track in the new queue. @@ -566,8 +589,16 @@ func (p *PlaybackManager) fetchAndPlayTracks(fetchFn func() ([]*mediaprovider.Tr } } +func (p *PlaybackManager) GetActivePlayQueue() []mediaprovider.MediaItem { + return p.engine.GetActivePlayQueueDeepCopy() +} + func (p *PlaybackManager) GetPlayQueue() []mediaprovider.MediaItem { - return p.engine.GetPlayQueue() + return p.engine.GetPlayQueueDeepCopy() +} + +func (p *PlaybackManager) GetShuffledPlayQueue() []mediaprovider.MediaItem { + return p.engine.GetShuffledPlayQueueDeepCopy() } // Any time the user changes the favorite status of a track elsewhere in the app, @@ -634,11 +665,16 @@ func (p *PlaybackManager) SetVolume(vol int) { func (p *PlaybackManager) SetAutoplay(autoplay bool) { p.cfg.Autoplay = autoplay - if autoplay && p.NowPlayingIndex() == len(p.engine.playQueue)-1 { + if autoplay && p.NowPlayingIndex() == p.engine.getPlayQueueLength()-1 { p.enqueueAutoplayTracks() } } +func (p *PlaybackManager) SetShuffle(shuffle bool) { + p.cfg.Shuffle = shuffle + p.engine.SetShuffle(shuffle) +} + func (p *PlaybackManager) Volume() int { return p.engine.CurrentPlayer().GetVolume() } @@ -726,7 +762,7 @@ func (p *PlaybackManager) enqueueAutoplayTracks() { } // last 500 played items - queue := p.GetPlayQueue() + queue := p.GetActivePlayQueue() if l := len(queue); l > 500 { queue = queue[l-500:] } @@ -841,6 +877,19 @@ func (p *PlaybackManager) runCmdQueue(ctx context.Context) { c.Arg3.(bool), ) logIfErr("LoadItems", err) + case cmdLoadItemsAndPlayAtIdx: + err := p.engine.LoadItemsAndPlayAtIdx( + c.Arg.([]mediaprovider.MediaItem), + c.Arg2.(bool), + c.Arg3.(int), + ) + logIfErr("LoadItemsAndPlayAtIdx", err) + case cmdSetQueueState: + err := p.engine.SetQueueState( + c.Arg.([]*mediaprovider.Track), + c.Arg2.(QueueType), + ) + logIfErr("SetQueueState", err) case cmdLoadRadioStation: p.engine.LoadRadioStation( c.Arg.(*mediaprovider.RadioStation), diff --git a/backend/savedplayqueue.go b/backend/savedplayqueue.go index b7ec5ed..6234665 100644 --- a/backend/savedplayqueue.go +++ b/backend/savedplayqueue.go @@ -24,8 +24,7 @@ type serializedSavedPlayQueue struct { // SavePlayQueue saves the current play queue and playback position to a JSON file. // If the provided CanSavePlayQueue server is non-nil, it will also save to the server. -func SavePlayQueue(serverID string, pm *PlaybackManager, filepath string, server mediaprovider.CanSavePlayQueue) error { - queue := pm.GetPlayQueue() +func SavePlayQueue(serverID string, queue []mediaprovider.MediaItem, pm *PlaybackManager, filepath string, server mediaprovider.CanSavePlayQueue) error { stats := pm.PlaybackStatus() trackIdx := pm.NowPlayingIndex() diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index e094364..3a13a29 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -112,6 +112,11 @@ func TracksToIDs(tracks []*mediaprovider.Track) []string { // Reorder items and return a new track slice. // idxToMove must contain only valid indexes into tracks, and no repeats func ReorderItems[T any](items []T, idxToMove []int, insertIdx int) []T { + if len(items) < 2 { + cpy := make([]T, len(items)) + copy(cpy, items) + return cpy + } idxToMoveSet := ToSet(idxToMove) newItems := make([]T, 0, len(items)) @@ -186,3 +191,11 @@ func DownloadFileWithContext(ctx context.Context, url string, destPath string) ( return true, nil } + +func CopyTrackSliceToMediaItemSlice(tracks []*mediaprovider.Track) []mediaprovider.MediaItem { + newTracks := make([]mediaprovider.MediaItem, len(tracks)) + for i, tr := range tracks { + newTracks[i] = tr.Copy() + } + return newTracks +} diff --git a/ui/bottompanel.go b/ui/bottompanel.go index ba98b93..c54d668 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -102,10 +102,13 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr pm.SeekFraction(f) }) - bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.GetLoopMode(), pm.IsAutoplay()) + bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.GetLoopMode(), pm.IsAutoplay(), pm.IsShuffle()) pm.OnLoopModeChange(func(lm backend.LoopMode) { fyne.Do(func() { bp.AuxControls.SetLoopMode(lm) }) }) + pm.OnShuffleChange(func(lm bool) { + fyne.Do(func() { bp.AuxControls.SetShuffle(lm) }) + }) pm.OnVolumeChange(func(vol int) { fyne.Do(func() { bp.AuxControls.VolumeControl.SetVolume(vol) }) }) @@ -122,6 +125,9 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr bp.AuxControls.OnChangeAutoplay = func(autoplay bool) { pm.SetAutoplay(autoplay) } + bp.AuxControls.OnChangeShuffle = func(shuffle bool) { + pm.SetShuffle(shuffle) + } bp.AuxControls.OnShowPlayQueue(contr.ShowPopUpPlayQueue) bp.AuxControls.OnShowCastMenu(contr.ShowCastMenu) diff --git a/ui/browsing/nowplayingpage.go b/ui/browsing/nowplayingpage.go index 97716b9..cebe339 100644 --- a/ui/browsing/nowplayingpage.go +++ b/ui/browsing/nowplayingpage.go @@ -461,7 +461,7 @@ func (a *NowPlayingPage) Reload() { a.relatedList.DisableRating = !a.canRate a.relatedList.DisableSharing = !a.canShare - a.queue = a.pm.GetPlayQueue() + a.queue = a.pm.GetActivePlayQueue() a.queueList.SetItems(a.queue) a.totalTime = 0.0 for _, tr := range a.queue { diff --git a/ui/controller/connectactions.go b/ui/controller/connectactions.go index 1f051e5..fa7dab2 100644 --- a/ui/controller/connectactions.go +++ b/ui/controller/connectactions.go @@ -31,11 +31,10 @@ func (m *Controller) connectTracklistActionsWithReplayGainMode(tracklist *widget m.App.PlaybackManager.LoadTracks(tracks, backend.Append, false) } tracklist.OnPlayTrackAt = func(idx int) { - m.App.PlaybackManager.LoadTracks(tracklist.GetTracks(), backend.Replace, false) + m.App.PlaybackManager.LoadTracksAndPlayAtIdx(tracklist.GetTracks(), false, idx) if m.App.Config.ReplayGain.Mode == backend.ReplayGainAuto { m.App.PlaybackManager.SetReplayGainMode(mode) } - m.App.PlaybackManager.PlayTrackAt(idx) } tracklist.OnPlaySelection = func(tracks []*mediaprovider.Track, shuffle bool) { m.App.PlaybackManager.LoadTracks(tracks, backend.Replace, shuffle) diff --git a/ui/controller/controller.go b/ui/controller/controller.go index ed80bb3..a9d4bd5 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -75,7 +75,7 @@ func New(app *backend.App, appVersion string, mainWindow fyne.Window) *Controlle c.initVisualizations() c.App.PlaybackManager.OnQueueChange(util.FyneDoFunc(func() { if c.popUpQueue != nil { - c.popUpQueueList.SetItems(c.App.PlaybackManager.GetPlayQueue()) + c.popUpQueueList.SetItems(c.App.PlaybackManager.GetActivePlayQueue()) } })) c.App.PlaybackManager.OnSongChange(func(track mediaprovider.MediaItem, _ *mediaprovider.Track) { @@ -192,7 +192,7 @@ func (m *Controller) ShowPopUpPlayQueue() { if m.popUpQueue == nil { m.popUpQueueList = widgets.NewPlayQueueList(m.App.ImageManager, false) m.popUpQueueList.Reorderable = true - m.popUpQueueList.SetItems(m.App.PlaybackManager.GetPlayQueue()) + m.popUpQueueList.SetItems(m.App.PlaybackManager.GetActivePlayQueue()) m.ConnectPlayQueuelistActions(m.popUpQueueList) title := widget.NewRichTextWithText(lang.L("Play Queue")) diff --git a/ui/mainwindow.go b/ui/mainwindow.go index e6cb5b8..ba7e2e3 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -119,7 +119,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, }) }) app.PlaybackManager.OnQueueChange(func() { - fyne.Do(func() { m.Sidebar.SetQueueTracks(app.PlaybackManager.GetPlayQueue()) }) + fyne.Do(func() { m.Sidebar.SetQueueTracks(app.PlaybackManager.GetActivePlayQueue()) }) }) app.ServerManager.OnServerConnected(func(conf *backend.ServerConfig) { go m.RunOnServerConnectedTasks(conf, app, displayAppName) diff --git a/ui/widgets/auxcontrols.go b/ui/widgets/auxcontrols.go index 558ea72..e63f4c9 100644 --- a/ui/widgets/auxcontrols.go +++ b/ui/widgets/auxcontrols.go @@ -22,8 +22,10 @@ type AuxControls struct { widget.BaseWidget OnChangeAutoplay func(autoplay bool) + OnChangeShuffle func(shuffle bool) VolumeControl *VolumeControl + shuffle *IconButton autoplay *IconButton loop *IconButton cast *IconButton @@ -32,15 +34,26 @@ type AuxControls struct { container *fyne.Container } -func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initialAutoplay bool) *AuxControls { +func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initialAutoplay bool, initialShuffle bool) *AuxControls { a := &AuxControls{ VolumeControl: NewVolumeControl(initialVolume), + shuffle: NewIconButton(myTheme.ShuffleIcon, nil), autoplay: NewIconButton(myTheme.AutoplayIcon, nil), loop: NewIconButton(myTheme.RepeatIcon, nil), cast: NewIconButton(myTheme.CastIcon, nil), showQueue: NewIconButton(myTheme.PlayQueueIcon, nil), } + a.shuffle.Highlighted = initialShuffle + a.shuffle.IconSize = IconButtonSizeSmaller + a.shuffle.SetToolTip(lang.L("Shuffle")) + a.shuffle.OnTapped = func() { + a.SetShuffle(!a.shuffle.Highlighted) + if a.OnChangeShuffle != nil { + a.OnChangeShuffle(a.shuffle.Highlighted) + } + } + a.loop.IconSize = IconButtonSizeSmaller a.loop.SetToolTip(lang.L("Repeat")) a.SetLoopMode(initialLoopMode) @@ -68,7 +81,7 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial a.VolumeControl, container.New( layout.NewCustomPaddedHBoxLayout(theme.Padding()*1.5), - layout.NewSpacer(), a.autoplay, a.loop, a.cast, a.showQueue, util.NewHSpace(5)), + layout.NewSpacer(), a.autoplay, a.shuffle, a.loop, a.cast, a.showQueue, util.NewHSpace(5)), layout.NewSpacer(), ), ) @@ -98,6 +111,14 @@ func (a *AuxControls) SetLoopMode(mode backend.LoopMode) { } } +func (a *AuxControls) SetShuffle(isShuffle bool) { + if isShuffle == a.shuffle.Highlighted { + return + } + a.shuffle.Highlighted = isShuffle + a.shuffle.Refresh() +} + func (a *AuxControls) DisableCastButton() { a.cast.Disable() }