Merge pull request #197 from adamantike/feat/add-loop-player-control

Add repeat all functionality to player controls
This commit is contained in:
Drew Weymouth
2023-06-19 19:29:35 -07:00
committed by GitHub
6 changed files with 148 additions and 19 deletions
+1 -1
View File
@@ -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)
+20
View File
@@ -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 {
+63
View File
@@ -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"
}
+16 -12
View File
@@ -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)
+1 -2
View File
@@ -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)
+44 -1
View File
@@ -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