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/player/player.go b/player/player.go
index 7b631cd..6d034ec 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
@@ -107,11 +116,12 @@ type Player struct {
bgCancel context.CancelFunc
// callbacks
- onPaused []func()
- onStopped []func()
- onPlaying []func()
- onSeek []func()
- onTrackChange []func(int64)
+ onPaused []func()
+ onStopped []func()
+ onPlaying []func()
+ onSeek []func()
+ onLoopModeChanged []func(string)
+ onTrackChange []func(int64)
}
// Returns a new player.
@@ -390,6 +400,47 @@ func (p *Player) PlayPause() error {
}
}
+// Sets 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:
+ p.mpv.SetOptionString("loop-playlist", "no")
+ case LoopAll:
+ p.mpv.SetOptionString("loop-playlist", "inf")
+ }
+ p.loopMode = mode
+
+ defer func() {
+ for _, cb := range p.onLoopModeChanged {
+ cb(p.loopMode.String())
+ }
+ }()
+
+ return nil
+}
+
+// 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 *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 {
@@ -491,6 +542,11 @@ func (p *Player) OnSeek(cb func()) {
p.onSeek = append(p.onSeek, cb)
}
+// Registers a callback which is invoked when the player enables queue repeat.
+func (p *Player) OnLoopModeChanged(cb func(string)) {
+ p.onLoopModeChanged = append(p.onLoopModeChanged, cb)
+}
+
// Registers a callback which is invoked when the currently playing track changes,
// or when playback begins at any time from the Stopped state.
// Callback is invoked with the index of the currently playing track (zero-based).
@@ -590,3 +646,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..8439c31 100644
--- a/ui/bottompanel.go
+++ b/ui/bottompanel.go
@@ -46,6 +46,9 @@ func NewBottomPanel(p *player.Player, contr *controller.Controller) *BottomPanel
p.OnStopped(func() {
bp.Controls.SetPlaying(false)
})
+ p.OnLoopModeChanged(func(mode string) {
+ bp.Controls.SetLoopMode(mode)
+ })
bp.NowPlaying = widgets.NewNowPlayingCard()
bp.NowPlaying.OnShowCoverImage = func() {
@@ -87,6 +90,9 @@ func NewBottomPanel(p *player.Player, contr *controller.Controller) *BottomPanel
bp.Controls.OnSeek(func(f float64) {
p.Seek(fmt.Sprintf("%d", int(f*100)), player.SeekAbsolutePercent)
})
+ bp.Controls.OnChangeLoopMode(func() {
+ p.SetNextLoopMode()
+ })
bp.AuxControls = widgets.NewAuxControls(p.GetVolume())
bp.AuxControls.VolumeControl.OnVolumeChanged = func(v int) {
diff --git a/ui/widgets/playercontrols.go b/ui/widgets/playercontrols.go
index c81ef49..6685dca 100644
--- a/ui/widgets/playercontrols.go
+++ b/ui/widgets/playercontrols.go
@@ -10,6 +10,10 @@ import (
"fyne.io/fyne/v2/widget"
)
+var (
+ themedResReplay = theme.NewThemedResource(theme.MediaReplayIcon())
+)
+
// TrackPosSlider is a custom slider that doesn't trigger
// the seek action until drag end.
type TrackPosSlider struct {
@@ -62,6 +66,7 @@ type PlayerControls struct {
prev *widget.Button
playpause *widget.Button
next *widget.Button
+ loop *widget.Button
container *fyne.Container
totalTime float64
@@ -105,8 +110,9 @@ func NewPlayerControls() *PlayerControls {
pc.prev = widget.NewButtonWithIcon("", theme.MediaSkipPreviousIcon(), func() {})
pc.next = widget.NewButtonWithIcon("", theme.MediaSkipNextIcon(), func() {})
pc.playpause = widget.NewButtonWithIcon("", theme.MediaPlayIcon(), func() {})
+ pc.loop = widget.NewButtonWithIcon("", themedResReplay, func() {})
- buttons := container.NewHBox(pc.prev, pc.playpause, pc.next)
+ buttons := container.NewHBox(pc.prev, pc.playpause, pc.next, pc.loop)
b := container.New(layout.NewCenterLayout(), buttons)
c := container.NewBorder(nil, nil, pc.curTimeLabel, pc.totalTimeLabel, pc.slider)
@@ -139,6 +145,20 @@ func (pc *PlayerControls) SetPlaying(playing bool) {
}
}
+func (pc *PlayerControls) OnChangeLoopMode(f func()) {
+ pc.loop.OnTapped = f
+}
+
+func (pc *PlayerControls) SetLoopMode(mode string) {
+ if mode == "all" {
+ themedResReplay.ColorName = theme.ColorNameSuccess
+ pc.loop.SetIcon(themedResReplay)
+ } else {
+ themedResReplay.ColorName = ""
+ pc.loop.SetIcon(themedResReplay)
+ }
+}
+
func (pc *PlayerControls) UpdatePlayTime(curTime, totalTime float64) {
pc.totalTime = totalTime
v := 0.0