Adding the Loop functionality, along with the Player changes to support it, and a button to the player controls. `Player` now has a `loopMode` attribute, which determines whether it should loop. At the moment, only the following modes are supported: * `LoopNone`: Disables loop. * `LoopAll`: Enables loop for the entire playlist queue. This has been designed as an enum, to support other modes in the future (e.g. loop for the current track only). It depends on the `loop` functionality provided by MPV (using the [`loop-playlist` option](https://mpv.io/manual/master/#options-loop-playlist)), so no custom logic is needed to reset the currently playing index.
237 lines
5.4 KiB
Go
237 lines
5.4 KiB
Go
package widgets
|
|
|
|
import (
|
|
"github.com/dweymouth/supersonic/ui/util"
|
|
|
|
"fyne.io/fyne/v2"
|
|
"fyne.io/fyne/v2/container"
|
|
"fyne.io/fyne/v2/layout"
|
|
"fyne.io/fyne/v2/theme"
|
|
"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 {
|
|
widget.Slider
|
|
|
|
OnDragEnd func(float64)
|
|
|
|
isDragging bool
|
|
lastDraggedValue float64
|
|
}
|
|
|
|
func NewTrackPosSlider() *TrackPosSlider {
|
|
slider := &TrackPosSlider{
|
|
Slider: widget.Slider{
|
|
Value: 0,
|
|
Min: 0,
|
|
Max: 1,
|
|
Step: 0.002,
|
|
Orientation: widget.Horizontal,
|
|
},
|
|
}
|
|
slider.ExtendBaseWidget(slider)
|
|
return slider
|
|
}
|
|
|
|
func (t *TrackPosSlider) DragEnd() {
|
|
t.isDragging = false
|
|
t.Slider.DragEnd()
|
|
if t.OnDragEnd != nil {
|
|
t.OnDragEnd(t.lastDraggedValue)
|
|
}
|
|
}
|
|
|
|
func (t *TrackPosSlider) Dragged(e *fyne.DragEvent) {
|
|
t.isDragging = true
|
|
t.Slider.Dragged(e)
|
|
t.lastDraggedValue = t.Value
|
|
}
|
|
|
|
func (t *TrackPosSlider) IsDragging() bool {
|
|
return t.isDragging
|
|
}
|
|
|
|
type PlayerControls struct {
|
|
widget.BaseWidget
|
|
|
|
slider *TrackPosSlider
|
|
curTimeLabel *labelMinSize
|
|
totalTimeLabel *labelMinSize
|
|
prev *widget.Button
|
|
playpause *widget.Button
|
|
next *widget.Button
|
|
loop *widget.Button
|
|
container *fyne.Container
|
|
|
|
totalTime float64
|
|
}
|
|
|
|
var _ fyne.Widget = (*PlayerControls)(nil)
|
|
|
|
type labelMinSize struct {
|
|
widget.Label
|
|
MinWidth float32
|
|
}
|
|
|
|
func (l *labelMinSize) MinSize() fyne.Size {
|
|
return fyne.NewSize(l.MinWidth, l.Label.MinSize().Height)
|
|
}
|
|
|
|
func NewLabelMinSize(text string, minWidth float32) *labelMinSize {
|
|
l := &labelMinSize{MinWidth: minWidth, Label: widget.Label{Text: text}}
|
|
l.ExtendBaseWidget(l)
|
|
return l
|
|
}
|
|
|
|
// NewPlayerControls sets up the seek bar, and transport buttons.
|
|
func NewPlayerControls() *PlayerControls {
|
|
pc := &PlayerControls{}
|
|
pc.ExtendBaseWidget(pc)
|
|
|
|
pc.slider = NewTrackPosSlider()
|
|
pc.curTimeLabel = NewLabelMinSize(util.SecondsToTimeString(0), 55)
|
|
pc.curTimeLabel.Alignment = fyne.TextAlignTrailing
|
|
pc.totalTimeLabel = NewLabelMinSize(util.SecondsToTimeString(0), 55)
|
|
pc.totalTimeLabel.Alignment = fyne.TextAlignTrailing
|
|
|
|
pc.slider.OnChanged = func(f float64) {
|
|
if pc.slider.IsDragging() {
|
|
time := f * pc.totalTime
|
|
pc.curTimeLabel.SetText(util.SecondsToTimeString(time))
|
|
}
|
|
}
|
|
|
|
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, pc.loop)
|
|
b := container.New(layout.NewCenterLayout(), buttons)
|
|
|
|
c := container.NewBorder(nil, nil, pc.curTimeLabel, pc.totalTimeLabel, pc.slider)
|
|
pc.container = container.NewVBox(c, b)
|
|
|
|
return pc
|
|
}
|
|
|
|
func (pc *PlayerControls) OnSeek(f func(float64)) {
|
|
pc.slider.OnDragEnd = f
|
|
}
|
|
|
|
func (pc *PlayerControls) OnSeekPrevious(f func()) {
|
|
pc.prev.OnTapped = f
|
|
}
|
|
|
|
func (pc *PlayerControls) OnSeekNext(f func()) {
|
|
pc.next.OnTapped = f
|
|
}
|
|
|
|
func (pc *PlayerControls) OnPlayPause(f func()) {
|
|
pc.playpause.OnTapped = f
|
|
}
|
|
|
|
func (pc *PlayerControls) SetPlaying(playing bool) {
|
|
if playing {
|
|
pc.playpause.SetIcon(theme.MediaPauseIcon())
|
|
} else {
|
|
pc.playpause.SetIcon(theme.MediaPlayIcon())
|
|
}
|
|
}
|
|
|
|
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
|
|
if totalTime > 0 {
|
|
v = curTime / totalTime
|
|
}
|
|
|
|
updated := false
|
|
tt := util.SecondsToTimeString(totalTime)
|
|
if tt != pc.totalTimeLabel.Text {
|
|
pc.totalTimeLabel.SetText(tt)
|
|
updated = true
|
|
}
|
|
if !pc.slider.IsDragging() {
|
|
ct := util.SecondsToTimeString(curTime)
|
|
if ct != pc.curTimeLabel.Text {
|
|
pc.curTimeLabel.SetText(ct)
|
|
updated = true
|
|
}
|
|
if updated {
|
|
// Only update slider once a second when time label changes
|
|
pc.slider.SetValue(v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *PlayerControls) CreateRenderer() fyne.WidgetRenderer {
|
|
return widget.NewSimpleRenderer(p.container)
|
|
}
|
|
|
|
// This code will be OBSOLETE in Fyne 2.4
|
|
// which will natively add Tappable behavior to slider
|
|
// Tapped is called when a pointer tapped event is captured.
|
|
//
|
|
// Since: 2.4
|
|
func (t *TrackPosSlider) Tapped(e *fyne.PointEvent) {
|
|
ratio := t.getRatio(e)
|
|
val := t.Min + ratio*(t.Max-t.Min)
|
|
t.isDragging = true
|
|
t.SetValue(val)
|
|
t.lastDraggedValue = val
|
|
t.isDragging = false
|
|
t.DragEnd()
|
|
}
|
|
|
|
func (t *TrackPosSlider) endOffset() float32 {
|
|
return (theme.IconInlineSize()-4)/2 + theme.InnerPadding() - 1.5 // align with radio icons
|
|
}
|
|
|
|
func (t *TrackPosSlider) getRatio(e *fyne.PointEvent) float64 {
|
|
pad := t.endOffset()
|
|
|
|
x := e.Position.X
|
|
y := e.Position.Y
|
|
|
|
switch t.Orientation {
|
|
case widget.Vertical:
|
|
if y > t.Size().Height-pad {
|
|
return 0.0
|
|
} else if y < pad {
|
|
return 1.0
|
|
} else {
|
|
return 1 - float64(y-pad)/float64(t.Size().Height-pad*2)
|
|
}
|
|
case widget.Horizontal:
|
|
if x > t.Size().Width-pad {
|
|
return 1.0
|
|
} else if x < pad {
|
|
return 0.0
|
|
} else {
|
|
return float64(x-pad) / float64(t.Size().Width-pad*2)
|
|
}
|
|
}
|
|
return 0.0
|
|
}
|