diff --git a/README.md b/README.md
index b0b4d8e..6b461fb 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
-
-
+
+
# Supersonic
[](https://github.com/dweymouth/supersonic/blob/main/LICENSE)
@@ -40,7 +40,7 @@ Screenshots of Supersonic running against the Navidrome [demo server](https://ww
* [x] Set/unset favorite and browse by favorite albums, artists, and songs
* [x] Set and view track rating (0-5 stars)
* [x] View and edit play queue (add and remove tracks; reorder support coming soon)
-* [x] Shuffle and repeat playback modes (partial; shuffle album, playlist, artist radio, random songs)
+* [x] Shuffle and repeat playback modes (partial; shuffle album, playlist, artist radio, random songs; repeat all)
* [ ] Browse by folders (planned)
* [ ] Download songs, albums or playlists (planned)
* [ ] Cast to uPnP/DLNA devices (likely planned)
@@ -86,7 +86,7 @@ Supersonic is available in the AUR and can be built either manually with `makepk
### Build with an AUR helper
* Invoke your favorite AUR helper to automatically build the package
- - ``yay -S supersonic-desktop``
+ - ``yay -S supersonic-desktop``
## Build instructions (Mac OS)
diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go
index 51f64c5..fb121bf 100644
--- a/backend/playbackmanager.go
+++ b/backend/playbackmanager.go
@@ -40,6 +40,7 @@ type PlaybackManager struct {
onSongChange []func(nowPlaying, justScrobbledIfAny *mediaprovider.Track)
onPlayTimeUpdate []func(float64, float64)
+ onLoopModeChange []func(string)
}
func NewPlaybackManager(
@@ -128,6 +129,11 @@ func (p *PlaybackManager) OnPlayTimeUpdate(cb func(float64, float64)) {
p.onPlayTimeUpdate = append(p.onPlayTimeUpdate, cb)
}
+// Registers a callback that is notified whenever the loop mode changes.
+func (p *PlaybackManager) OnLoopModeChange(cb func(string)) {
+ p.onLoopModeChange = append(p.onLoopModeChange, cb)
+}
+
// Loads the specified album into the play queue.
func (p *PlaybackManager) LoadAlbum(albumID string, appendToQueue bool, shuffle bool) error {
album, err := p.sm.Server.GetAlbum(albumID)
@@ -291,6 +297,20 @@ func (p *PlaybackManager) SetReplayGainOptions(config ReplayGainConfig) {
})
}
+// Changes the loop mode of the player to the next one.
+// Useful for toggling UI elements, to change modes without knowing the current player mode.
+func (p *PlaybackManager) SetNextLoopMode() error {
+ if err := p.player.SetNextLoopMode(); err != nil {
+ return err
+ }
+
+ for _, cb := range p.onLoopModeChange {
+ cb(p.player.GetLoopMode().String())
+ }
+
+ return nil
+}
+
// call BEFORE updating p.nowPlayingIdx
func (p *PlaybackManager) checkScrobble() {
if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 {
diff --git a/player/player.go b/player/player.go
index 7b631cd..e9ad599 100644
--- a/player/player.go
+++ b/player/player.go
@@ -58,6 +58,14 @@ type ReplayGainOptions struct {
// Fallback gain intentionally omitted
}
+// The playback loop mode (LoopNone, LoopAll).
+type LoopMode int
+
+const (
+ LoopNone LoopMode = iota
+ LoopAll
+)
+
// Information about a specific audio device.
// Returned by ListAudioDevices.
type AudioDevice struct {
@@ -99,6 +107,7 @@ type Player struct {
haveRGainOpts bool
audioExclusive bool
status Status
+ loopMode LoopMode
seeking bool
curPlaylistPos int64
prePausedState State
@@ -390,6 +399,50 @@ func (p *Player) PlayPause() error {
}
}
+// Get the loop mode of the player.
+func (p *Player) GetLoopMode() LoopMode {
+ return p.loopMode
+}
+
+// Set the loop mode of the player.
+func (p *Player) SetLoopMode(mode LoopMode) error {
+ if !p.initialized {
+ return ErrUnitialized
+ }
+
+ // Return early if player is already in specified mode
+ if mode == p.loopMode {
+ return nil
+ }
+
+ switch mode {
+ case LoopNone:
+ if err := p.mpv.SetOptionString("loop-playlist", "no"); err != nil {
+ return err
+ }
+ case LoopAll:
+ if err := p.mpv.SetOptionString("loop-playlist", "inf"); err != nil {
+ return err
+ }
+ }
+ p.loopMode = mode
+
+ return nil
+}
+
+// Change the loop mode of the player to the next one.
+// Useful for toggling UI elements, to change modes without knowing the current player mode.
+func (p *Player) SetNextLoopMode() error {
+ switch p.loopMode {
+ case LoopNone:
+ return p.SetLoopMode(LoopAll)
+ case LoopAll:
+ return p.SetLoopMode(LoopNone)
+ default:
+ return nil
+ }
+}
+
// Get the current status of the player.
func (p *Player) GetStatus() Status {
if !p.initialized {
@@ -590,3 +643,13 @@ func (s SeekMode) String() string {
}
return "UNKNOWN_SEEK_MODE"
}
+
+func (l LoopMode) String() string {
+ switch l {
+ case LoopNone:
+ return "no"
+ case LoopAll:
+ return "all"
+ }
+ return "UNKNOWN_LOOP_MODE"
+}
diff --git a/ui/bottompanel.go b/ui/bottompanel.go
index 429b969..36ad8cc 100644
--- a/ui/bottompanel.go
+++ b/ui/bottompanel.go
@@ -34,9 +34,17 @@ type BottomPanel struct {
var _ fyne.Widget = (*BottomPanel)(nil)
-func NewBottomPanel(p *player.Player, contr *controller.Controller) *BottomPanel {
- bp := &BottomPanel{}
+func NewBottomPanel(p *player.Player, pm *backend.PlaybackManager, contr *controller.Controller) *BottomPanel {
+ bp := &BottomPanel{playbackManager: pm}
bp.ExtendBaseWidget(bp)
+
+ bp.playbackManager.OnSongChange(bp.onSongChange)
+ bp.playbackManager.OnPlayTimeUpdate(func(cur, total float64) {
+ if !bp.playbackManager.IsSeeking() {
+ bp.Controls.UpdatePlayTime(cur, total)
+ }
+ })
+
p.OnPaused(func() {
bp.Controls.SetPlaying(false)
})
@@ -46,6 +54,9 @@ func NewBottomPanel(p *player.Player, contr *controller.Controller) *BottomPanel
p.OnStopped(func() {
bp.Controls.SetPlaying(false)
})
+ pm.OnLoopModeChange(func(mode string) {
+ bp.AuxControls.SetLoopMode(mode)
+ })
bp.NowPlaying = widgets.NewNowPlayingCard()
bp.NowPlaying.OnShowCoverImage = func() {
@@ -92,22 +103,15 @@ func NewBottomPanel(p *player.Player, contr *controller.Controller) *BottomPanel
bp.AuxControls.VolumeControl.OnVolumeChanged = func(v int) {
_ = p.SetVolume(v)
}
+ bp.AuxControls.OnChangeLoopMode(func() {
+ bp.playbackManager.SetNextLoopMode()
+ })
bp.container = container.New(layouts.NewLeftMiddleRightLayout(500),
bp.NowPlaying, bp.Controls, bp.AuxControls)
return bp
}
-func (bp *BottomPanel) SetPlaybackManager(pm *backend.PlaybackManager) {
- bp.playbackManager = pm
- pm.OnSongChange(bp.onSongChange)
- pm.OnPlayTimeUpdate(func(cur, total float64) {
- if !pm.IsSeeking() {
- bp.Controls.UpdatePlayTime(cur, total)
- }
- })
-}
-
func (bp *BottomPanel) onSongChange(song, _ *mediaprovider.Track) {
if song == nil {
bp.NowPlaying.Update("", "", false, "", nil)
diff --git a/ui/mainwindow.go b/ui/mainwindow.go
index 10832db..1522215 100644
--- a/ui/mainwindow.go
+++ b/ui/mainwindow.go
@@ -75,8 +75,7 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
m.Controller.ReloadFunc = m.BrowsingPane.Reload
m.Controller.CurPageFunc = m.BrowsingPane.CurrentPage
- m.BottomPanel = NewBottomPanel(app.Player, m.Controller)
- m.BottomPanel.SetPlaybackManager(app.PlaybackManager)
+ m.BottomPanel = NewBottomPanel(app.Player, app.PlaybackManager, m.Controller)
m.BottomPanel.ImageManager = app.ImageManager
m.container = container.NewBorder(nil, m.BottomPanel, nil, nil, m.BrowsingPane)
m.Window.SetContent(m.container)
diff --git a/ui/widgets/auxcontrols.go b/ui/widgets/auxcontrols.go
index e3d61a2..b110a66 100644
--- a/ui/widgets/auxcontrols.go
+++ b/ui/widgets/auxcontrols.go
@@ -6,6 +6,7 @@ import (
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
+ "github.com/dweymouth/supersonic/ui/util"
)
// The "aux" controls for playback, positioned to the right
@@ -14,15 +15,44 @@ type AuxControls struct {
widget.BaseWidget
VolumeControl *VolumeControl
+ loop *miniButton
container *fyne.Container
}
+type miniButton struct {
+ widget.Button
+}
+
+func newMiniButton(icon fyne.Resource) *miniButton {
+ b := &miniButton{
+ Button: widget.Button{
+ Icon: icon,
+ },
+ }
+ b.ExtendBaseWidget(b)
+ return b
+}
+
+func (b *miniButton) MinSize() fyne.Size {
+ return fyne.NewSize(24, 24)
+}
+
func NewAuxControls(initialVolume int) *AuxControls {
a := &AuxControls{
VolumeControl: NewVolumeControl(initialVolume),
+ loop: newMiniButton(theme.MediaReplayIcon()),
}
- a.container = container.NewHBox(layout.NewSpacer(), a.VolumeControl)
+ a.container = container.NewHBox(
+ layout.NewSpacer(),
+ container.NewVBox(
+ util.NewHSpace(0), // hack to move everything down a tiny bit
+ layout.NewSpacer(),
+ a.VolumeControl,
+ container.NewHBox(layout.NewSpacer(), a.loop, util.NewHSpace(5)),
+ layout.NewSpacer(),
+ ),
+ )
return a
}
@@ -31,6 +61,19 @@ func (a *AuxControls) CreateRenderer() fyne.WidgetRenderer {
return widget.NewSimpleRenderer(a.container)
}
+func (a *AuxControls) OnChangeLoopMode(f func()) {
+ a.loop.OnTapped = f
+}
+
+func (a *AuxControls) SetLoopMode(mode string) {
+ if mode == "all" {
+ a.loop.Importance = widget.HighImportance
+ } else {
+ a.loop.Importance = widget.MediumImportance
+ }
+ a.loop.Refresh()
+}
+
type volumeSlider struct {
widget.Slider