From b40c154f7eb13fbbdf4c553a483440d0a3de5689 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 20 Feb 2026 18:53:02 -0800 Subject: [PATCH] move shuffle and repeat controls into main player controls (#862) Shuffle moves to the far left of the transport buttons and repeat to the far right, flanking prev/play/next. Both use a new IconButtonSizeSmallest (10% smaller than IconButtonSizeSmaller) with 2dp spacing separating them from the adjacent transport buttons. Wiring updated in bottompanel.go accordingly. Co-authored-by: Claude Sonnet 4.6 --- ui/bottompanel.go | 24 ++++++++-------- ui/widgets/auxcontrols.go | 50 ++------------------------------- ui/widgets/iconbutton.go | 6 ++++ ui/widgets/playercontrols.go | 54 ++++++++++++++++++++++++++++++++++-- 4 files changed, 72 insertions(+), 62 deletions(-) diff --git a/ui/bottompanel.go b/ui/bottompanel.go index c54d668..c6c26b6 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -88,7 +88,7 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr contr.ShowShareDialog(tr.ID) } } - bp.Controls = widgets.NewPlayerControls(cfg.Playback.UseWaveformSeekbar) + bp.Controls = widgets.NewPlayerControls(cfg.Playback.UseWaveformSeekbar, pm.GetLoopMode(), pm.IsShuffle()) bp.Controls.OnPlayPause(func() { pm.PlayPause() }) @@ -101,14 +101,20 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr bp.Controls.OnSeek(func(f float64) { pm.SeekFraction(f) }) - - bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.GetLoopMode(), pm.IsAutoplay(), pm.IsShuffle()) + bp.Controls.OnChangeLoopMode(func() { + pm.SetNextLoopMode() + }) + bp.Controls.OnChangeShuffle = func(shuffle bool) { + pm.SetShuffle(shuffle) + } pm.OnLoopModeChange(func(lm backend.LoopMode) { - fyne.Do(func() { bp.AuxControls.SetLoopMode(lm) }) + fyne.Do(func() { bp.Controls.SetLoopMode(lm) }) }) - pm.OnShuffleChange(func(lm bool) { - fyne.Do(func() { bp.AuxControls.SetShuffle(lm) }) + pm.OnShuffleChange(func(sh bool) { + fyne.Do(func() { bp.Controls.SetShuffle(sh) }) }) + + bp.AuxControls = widgets.NewAuxControls(pm.Volume(), pm.IsAutoplay()) pm.OnVolumeChange(func(vol int) { fyne.Do(func() { bp.AuxControls.VolumeControl.SetVolume(vol) }) }) @@ -119,15 +125,9 @@ func NewBottomPanel(pm *backend.PlaybackManager, im *backend.ImageManager, contr bp.AuxControls.VolumeControl.OnSetVolume = func(v int) { pm.SetVolume(v) } - bp.AuxControls.OnChangeLoopMode(func() { - pm.SetNextLoopMode() - }) 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) diff --git a/ui/widgets/auxcontrols.go b/ui/widgets/auxcontrols.go index e63f4c9..7968e26 100644 --- a/ui/widgets/auxcontrols.go +++ b/ui/widgets/auxcontrols.go @@ -11,7 +11,6 @@ import ( "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" - "github.com/dweymouth/supersonic/backend" myTheme "github.com/dweymouth/supersonic/ui/theme" "github.com/dweymouth/supersonic/ui/util" ) @@ -22,42 +21,23 @@ type AuxControls struct { widget.BaseWidget OnChangeAutoplay func(autoplay bool) - OnChangeShuffle func(shuffle bool) VolumeControl *VolumeControl - shuffle *IconButton autoplay *IconButton - loop *IconButton cast *IconButton showQueue *IconButton container *fyne.Container } -func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initialAutoplay bool, initialShuffle bool) *AuxControls { +func NewAuxControls(initialVolume int, initialAutoplay 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) - a.cast.IconSize = IconButtonSizeSmaller a.cast.SetToolTip(lang.L("Cast to device")) @@ -81,7 +61,7 @@ func NewAuxControls(initialVolume int, initialLoopMode backend.LoopMode, initial a.VolumeControl, container.New( layout.NewCustomPaddedHBoxLayout(theme.Padding()*1.5), - layout.NewSpacer(), a.autoplay, a.shuffle, a.loop, a.cast, a.showQueue, util.NewHSpace(5)), + layout.NewSpacer(), a.autoplay, a.cast, a.showQueue, util.NewHSpace(5)), layout.NewSpacer(), ), ) @@ -93,32 +73,6 @@ 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 backend.LoopMode) { - switch mode { - case backend.LoopAll: - a.loop.Highlighted = true - a.loop.SetIcon(myTheme.RepeatIcon) - case backend.LoopOne: - a.loop.Highlighted = true - a.loop.SetIcon(myTheme.RepeatOneIcon) - case backend.LoopNone: - a.loop.Highlighted = false - a.loop.SetIcon(myTheme.RepeatIcon) - } -} - -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() } diff --git a/ui/widgets/iconbutton.go b/ui/widgets/iconbutton.go index 4813b82..a2bbe3a 100644 --- a/ui/widgets/iconbutton.go +++ b/ui/widgets/iconbutton.go @@ -16,8 +16,10 @@ type IconButtonSize int const ( IconButtonSizeNormal IconButtonSize = iota + IconButtonSizeSlightlyBigger IconButtonSizeBigger IconButtonSizeSmaller + IconButtonSizeSmallest ) type IconButton struct { @@ -141,8 +143,12 @@ func (i *IconButton) iconSize() fyne.Size { switch i.IconSize { case IconButtonSizeBigger: return fyne.NewSquareSize(theme.IconInlineSize() * 2) + case IconButtonSizeSlightlyBigger: + return fyne.NewSquareSize(theme.IconInlineSize() * 1.37) case IconButtonSizeSmaller: return fyne.NewSquareSize(theme.IconInlineSize()) + case IconButtonSizeSmallest: + return fyne.NewSquareSize(theme.IconInlineSize() * 0.9) default: return fyne.NewSquareSize(theme.IconInlineSize() * 1.3333) } diff --git a/ui/widgets/playercontrols.go b/ui/widgets/playercontrols.go index 82425fb..c6078be 100644 --- a/ui/widgets/playercontrols.go +++ b/ui/widgets/playercontrols.go @@ -2,6 +2,7 @@ package widgets import ( "github.com/dweymouth/supersonic/backend" + myTheme "github.com/dweymouth/supersonic/ui/theme" "github.com/dweymouth/supersonic/ui/util" "fyne.io/fyne/v2" @@ -84,13 +85,17 @@ type PlayerControls struct { UseWaveformSeekbar bool + OnChangeShuffle func(shuffle bool) + slider *TrackPosSlider waveform *WaveformSeekbar curTimeLabel *labelMinSize totalTimeLabel *labelMinSize + shuffle *IconButton prev *IconButton playpause *IconButton next *IconButton + loop *IconButton container *fyne.Container totalTime float64 @@ -114,7 +119,7 @@ func NewLabelMinSize(text string, minWidth float32) *labelMinSize { } // NewPlayerControls sets up the seek bar, and transport buttons. -func NewPlayerControls(useWaveformSeekbar bool) *PlayerControls { +func NewPlayerControls(useWaveformSeekbar bool, initialLoopMode backend.LoopMode, initialShuffle bool) *PlayerControls { pc := &PlayerControls{UseWaveformSeekbar: useWaveformSeekbar} pc.ExtendBaseWidget(pc) @@ -139,15 +144,34 @@ func NewPlayerControls(useWaveformSeekbar bool) *PlayerControls { } } + pc.shuffle = NewIconButton(myTheme.ShuffleIcon, nil) + pc.shuffle.IconSize = IconButtonSizeSmallest + pc.shuffle.SetToolTip(lang.L("Shuffle")) + pc.shuffle.OnTapped = func() { + pc.SetShuffle(!pc.shuffle.Highlighted) + if pc.OnChangeShuffle != nil { + pc.OnChangeShuffle(pc.shuffle.Highlighted) + } + } + + pc.loop = NewIconButton(myTheme.RepeatIcon, nil) + pc.loop.IconSize = IconButtonSizeSmallest + pc.loop.SetToolTip(lang.L("Repeat")) + + pc.shuffle.Highlighted = initialShuffle + pc.SetLoopMode(initialLoopMode) + pc.prev = NewIconButton(theme.MediaSkipPreviousIcon(), func() {}) pc.prev.SetToolTip(lang.L("Previous")) + pc.prev.IconSize = IconButtonSizeSlightlyBigger pc.next = NewIconButton(theme.MediaSkipNextIcon(), func() {}) pc.next.SetToolTip(lang.L("Next")) + pc.next.IconSize = IconButtonSizeSlightlyBigger pc.playpause = NewIconButton(theme.MediaPlayIcon(), func() {}) pc.playpause.SetToolTip(lang.L("Play")) pc.playpause.IconSize = IconButtonSizeBigger - buttons := container.NewHBox(layout.NewSpacer(), pc.prev, pc.playpause, pc.next, layout.NewSpacer()) + buttons := container.NewHBox(layout.NewSpacer(), pc.shuffle, util.NewHSpace(2), pc.prev, pc.playpause, pc.next, util.NewHSpace(2), pc.loop, layout.NewSpacer()) seekCtrl := container.NewStack( pc.slider, @@ -174,6 +198,32 @@ func (pc *PlayerControls) OnSeekPrevious(f func()) { pc.prev.OnTapped = f } +func (pc *PlayerControls) OnChangeLoopMode(f func()) { + pc.loop.OnTapped = f +} + +func (pc *PlayerControls) SetLoopMode(mode backend.LoopMode) { + switch mode { + case backend.LoopAll: + pc.loop.Highlighted = true + pc.loop.SetIcon(myTheme.RepeatIcon) + case backend.LoopOne: + pc.loop.Highlighted = true + pc.loop.SetIcon(myTheme.RepeatOneIcon) + case backend.LoopNone: + pc.loop.Highlighted = false + pc.loop.SetIcon(myTheme.RepeatIcon) + } +} + +func (pc *PlayerControls) SetShuffle(isShuffle bool) { + if isShuffle == pc.shuffle.Highlighted { + return + } + pc.shuffle.Highlighted = isShuffle + pc.shuffle.Refresh() +} + func (pc *PlayerControls) OnSeekNext(f func()) { pc.next.OnTapped = f }