Merge pull request #815 from Tim-Kaiser/feature/shuffle

Shuffle play queue
This commit is contained in:
Drew Weymouth
2026-02-20 18:17:55 -08:00
committed by GitHub
13 changed files with 496 additions and 119 deletions
+65 -13
View File
@@ -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
}
+2
View File
@@ -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,
},
+26 -1
View File
@@ -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
+298 -87
View File
@@ -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 {
+56 -7
View File
@@ -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),
+1 -2
View File
@@ -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()
+13
View File
@@ -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
}
+7 -1
View File
@@ -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)
+1 -1
View File
@@ -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 {
+1 -2
View File
@@ -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)
+2 -2
View File
@@ -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"))
+1 -1
View File
@@ -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)
+23 -2
View File
@@ -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()
}