Merge pull request #197 from adamantike/feat/add-loop-player-control
Add repeat all functionality to player controls
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
<img src="res/appicon-128.png" alt="Supersonic logo" title="Supersonic" align="left" height="60px"/>
|
||||
<a href='https://flathub.org/apps/details/io.github.dweymouth.supersonic'><img height="50px" align="right" alt='Download on Flathub' src='https://flathub.org/assets/badges/flathub-badge-en.png'/></a></div>
|
||||
<a href='https://ko-fi.com/dweymouth' target='_blank'><img height='40' align='right' src='https://az743702.vo.msecnd.net/cdn/kofi3.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com'/>
|
||||
|
||||
<a href='https://ko-fi.com/dweymouth' target='_blank'><img height='40' align='right' src='https://az743702.vo.msecnd.net/cdn/kofi3.png?v=0' border='0' alt='Buy Me a Coffee at ko-fi.com'/>
|
||||
|
||||
# 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)
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user