Merge pull request #681 from dweymouth/feature/stop-after-current
Add stop after current track option
This commit is contained in:
@@ -548,6 +548,10 @@ func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error {
|
|||||||
return cli.SeekBackOrPrevious()
|
return cli.SeekBackOrPrevious()
|
||||||
case *FlagNext:
|
case *FlagNext:
|
||||||
return cli.SeekNext()
|
return cli.SeekNext()
|
||||||
|
case *FlagStop:
|
||||||
|
return cli.Stop()
|
||||||
|
case *FlagStopAfterCurrent:
|
||||||
|
return cli.StopAfterCurrent()
|
||||||
case *FlagShow:
|
case *FlagShow:
|
||||||
return cli.Show()
|
return cli.Show()
|
||||||
case VolumeCLIArg >= 0:
|
case VolumeCLIArg >= 0:
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ var (
|
|||||||
FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state")
|
FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state")
|
||||||
FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current")
|
FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current")
|
||||||
FlagNext = flag.Bool("next", false, "seek to next track")
|
FlagNext = flag.Bool("next", false, "seek to next track")
|
||||||
|
FlagStop = flag.Bool("stop", false, "stop playback")
|
||||||
|
FlagStopAfterCurrent = flag.Bool("stop-after-current", false, "stop playback after current track")
|
||||||
FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized")
|
FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized")
|
||||||
FlagShow = flag.Bool("show", false, "show minimized app")
|
FlagShow = flag.Bool("show", false, "show minimized app")
|
||||||
FlagVersion = flag.Bool("version", false, "print app version and exit")
|
FlagVersion = flag.Bool("version", false, "print app version and exit")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const (
|
|||||||
PlayPausePath = "/transport/playpause"
|
PlayPausePath = "/transport/playpause"
|
||||||
PausePath = "/transport/pause"
|
PausePath = "/transport/pause"
|
||||||
StopPath = "/transport/stop"
|
StopPath = "/transport/stop"
|
||||||
|
StopAfterCurrentPath = "/transport/stop-after-current"
|
||||||
PreviousPath = "/transport/previous"
|
PreviousPath = "/transport/previous"
|
||||||
NextPath = "/transport/next"
|
NextPath = "/transport/next"
|
||||||
TimePosPath = "/transport/timepos" // ?s=<seconds>
|
TimePosPath = "/transport/timepos" // ?s=<seconds>
|
||||||
|
|||||||
@@ -48,6 +48,14 @@ func (c *Client) PlayPause() error {
|
|||||||
return c.sendRequest(PlayPausePath)
|
return c.sendRequest(PlayPausePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Client) Stop() error {
|
||||||
|
return c.sendRequest(StopPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) StopAfterCurrent() error {
|
||||||
|
return c.sendRequest(StopAfterCurrentPath)
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) SeekNext() error {
|
func (c *Client) SeekNext() error {
|
||||||
return c.sendRequest(NextPath)
|
return c.sendRequest(NextPath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type PlaybackHandler interface {
|
|||||||
Continue()
|
Continue()
|
||||||
SeekBackOrPrevious()
|
SeekBackOrPrevious()
|
||||||
SeekNext()
|
SeekNext()
|
||||||
|
SetStopAfterCurrent(bool)
|
||||||
SeekSeconds(float64)
|
SeekSeconds(float64)
|
||||||
SeekBySeconds(float64)
|
SeekBySeconds(float64)
|
||||||
Volume() int
|
Volume() int
|
||||||
@@ -68,6 +69,9 @@ func (s *serverImpl) createHandler() http.Handler {
|
|||||||
m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.pbHandler.Pause))
|
m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.pbHandler.Pause))
|
||||||
m.HandleFunc(PlayPausePath, s.makeSimpleEndpointHandler(s.pbHandler.PlayPause))
|
m.HandleFunc(PlayPausePath, s.makeSimpleEndpointHandler(s.pbHandler.PlayPause))
|
||||||
m.HandleFunc(StopPath, s.makeSimpleEndpointHandler(s.pbHandler.Stop))
|
m.HandleFunc(StopPath, s.makeSimpleEndpointHandler(s.pbHandler.Stop))
|
||||||
|
m.HandleFunc(StopAfterCurrentPath, s.makeSimpleEndpointHandler(func() {
|
||||||
|
s.pbHandler.SetStopAfterCurrent(true)
|
||||||
|
}))
|
||||||
m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekBackOrPrevious))
|
m.HandleFunc(PreviousPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekBackOrPrevious))
|
||||||
m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext))
|
m.HandleFunc(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext))
|
||||||
m.HandleFunc(TimePosPath, s.makeFloatEndpointHandler("s", s.pbHandler.SeekSeconds))
|
m.HandleFunc(TimePosPath, s.makeFloatEndpointHandler("s", s.pbHandler.SeekSeconds))
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ type playbackEngine struct {
|
|||||||
isRadio bool
|
isRadio bool
|
||||||
loopMode LoopMode
|
loopMode LoopMode
|
||||||
|
|
||||||
|
stopAfterCurrent bool // flag to stop playback after current track ends
|
||||||
|
|
||||||
// flags for handleOnTrackChange / handleOnStopped callbacks - reset to false in the callbacks
|
// flags for handleOnTrackChange / handleOnStopped callbacks - reset to false in the callbacks
|
||||||
wasStopped bool // true iff player was stopped before handleOnTrackChange invocation
|
wasStopped bool // true iff player was stopped before handleOnTrackChange invocation
|
||||||
alreadyScrobbled bool // true iff the previously-playing track was already scrobbled
|
alreadyScrobbled bool // true iff the previously-playing track was already scrobbled
|
||||||
@@ -334,6 +336,15 @@ func (p *playbackEngine) Stop() error {
|
|||||||
return p.player.Stop(false)
|
return p.player.Stop(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *playbackEngine) SetStopAfterCurrent(stopAfterCurrent bool) {
|
||||||
|
p.stopAfterCurrent = stopAfterCurrent
|
||||||
|
if p.stopAfterCurrent {
|
||||||
|
p.setNextTrack(-1) // clear next playing track from internal player, if any
|
||||||
|
} else if p.loopMode != LoopNone || p.nowPlayingIdx < len(p.playQueue)-1 {
|
||||||
|
p.needToSetNextTrack = true // need to restore next track to internal player queue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *playbackEngine) Pause() error {
|
func (p *playbackEngine) Pause() error {
|
||||||
return p.player.Pause()
|
return p.player.Pause()
|
||||||
}
|
}
|
||||||
@@ -634,6 +645,7 @@ func (p *playbackEngine) handleOnStopped() {
|
|||||||
p.alreadyScrobbled = false
|
p.alreadyScrobbled = false
|
||||||
p.wasStopped = true
|
p.wasStopped = true
|
||||||
p.nowPlayingIdx = -1
|
p.nowPlayingIdx = -1
|
||||||
|
p.stopAfterCurrent = false
|
||||||
}
|
}
|
||||||
|
|
||||||
// to be invoked as soon as the next item in the queue that should play changes
|
// to be invoked as soon as the next item in the queue that should play changes
|
||||||
@@ -855,7 +867,7 @@ func (p *playbackEngine) handleTimePosUpdate(seeked bool) {
|
|||||||
meta = np.Metadata()
|
meta = np.Metadata()
|
||||||
}
|
}
|
||||||
isNearEnd := meta.Type != mediaprovider.MediaItemTypeRadioStation && s.TimePos > meta.Duration.Seconds()-10
|
isNearEnd := meta.Type != mediaprovider.MediaItemTypeRadioStation && s.TimePos > meta.Duration.Seconds()-10
|
||||||
if p.needToSetNextTrack && isNearEnd {
|
if p.needToSetNextTrack && !p.stopAfterCurrent && isNearEnd {
|
||||||
p.needToSetNextTrack = false
|
p.needToSetNextTrack = false
|
||||||
p.setNextTrack(p.nextPlayingIndex())
|
p.setNextTrack(p.nextPlayingIndex())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -669,6 +669,14 @@ func (p *PlaybackManager) PlayPause() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *PlaybackManager) SetStopAfterCurrent(stopAfterCurrent bool) {
|
||||||
|
p.engine.SetStopAfterCurrent(stopAfterCurrent)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *PlaybackManager) IsStopAfterCurrent() bool {
|
||||||
|
return p.engine.stopAfterCurrent
|
||||||
|
}
|
||||||
|
|
||||||
func (p *PlaybackManager) enqueueAutoplayTracks() {
|
func (p *PlaybackManager) enqueueAutoplayTracks() {
|
||||||
nowPlaying := p.NowPlaying()
|
nowPlaying := p.NowPlaying()
|
||||||
if nowPlaying == nil {
|
if nowPlaying == nil {
|
||||||
|
|||||||
@@ -229,6 +229,9 @@ func (d *DLNAPlayer) SetNextFile(url string, meta mediaprovider.MediaItemMetadat
|
|||||||
Title: meta.Name,
|
Title: meta.Name,
|
||||||
Seekable: true,
|
Seekable: true,
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// empty media item to signify erasing next track in device queue
|
||||||
|
media = &avtransport.MediaItem{}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
|||||||
@@ -216,6 +216,7 @@
|
|||||||
"Soundtrack": "Soundtrack",
|
"Soundtrack": "Soundtrack",
|
||||||
"Spoken Word": "Spoken Word",
|
"Spoken Word": "Spoken Word",
|
||||||
"Startup page": "Startup page",
|
"Startup page": "Startup page",
|
||||||
|
"Stop after current track": "Stop after current track",
|
||||||
"Stopped": "Stopped",
|
"Stopped": "Stopped",
|
||||||
"Success": "Success",
|
"Success": "Success",
|
||||||
"Support the project": "Support the project",
|
"Support the project": "Support the project",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
"fyne.io/fyne/v2/dialog"
|
"fyne.io/fyne/v2/dialog"
|
||||||
"fyne.io/fyne/v2/lang"
|
"fyne.io/fyne/v2/lang"
|
||||||
|
"fyne.io/fyne/v2/layout"
|
||||||
"fyne.io/fyne/v2/theme"
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
@@ -57,6 +58,7 @@ type Controller struct {
|
|||||||
|
|
||||||
popUpQueue *widget.PopUp
|
popUpQueue *widget.PopUp
|
||||||
popUpQueueList *widgets.PlayQueueList
|
popUpQueueList *widgets.PlayQueueList
|
||||||
|
stopAfterCurrent *widget.Check
|
||||||
popUpQueueLastUsed int64
|
popUpQueueLastUsed int64
|
||||||
escapablePopUp fyne.CanvasObject
|
escapablePopUp fyne.CanvasObject
|
||||||
haveModal bool
|
haveModal bool
|
||||||
@@ -195,7 +197,11 @@ func (m *Controller) ShowPopUpPlayQueue() {
|
|||||||
title := widget.NewRichTextWithText(lang.L("Play Queue"))
|
title := widget.NewRichTextWithText(lang.L("Play Queue"))
|
||||||
title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter
|
title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter
|
||||||
title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true
|
title.Segments[0].(*widget.TextSegment).Style.TextStyle.Bold = true
|
||||||
ctr := container.NewBorder(title, nil, nil, nil,
|
m.stopAfterCurrent = widget.NewCheck(lang.L("Stop after current track"), func(b bool) {
|
||||||
|
m.App.PlaybackManager.SetStopAfterCurrent(b)
|
||||||
|
})
|
||||||
|
bottomRow := container.NewHBox(layout.NewSpacer(), m.stopAfterCurrent)
|
||||||
|
ctr := container.NewBorder(title, bottomRow, nil, nil,
|
||||||
container.NewPadded(m.popUpQueueList),
|
container.NewPadded(m.popUpQueueList),
|
||||||
)
|
)
|
||||||
m.popUpQueue = widget.NewPopUp(ctr, m.MainWindow.Canvas())
|
m.popUpQueue = widget.NewPopUp(ctr, m.MainWindow.Canvas())
|
||||||
@@ -224,6 +230,7 @@ func (m *Controller) ShowPopUpPlayQueue() {
|
|||||||
fynetooltip.DestroyPopUpToolTipLayer(m.popUpQueue)
|
fynetooltip.DestroyPopUpToolTipLayer(m.popUpQueue)
|
||||||
m.popUpQueue = nil
|
m.popUpQueue = nil
|
||||||
m.popUpQueueList = nil
|
m.popUpQueueList = nil
|
||||||
|
m.stopAfterCurrent = nil
|
||||||
m.popUpQueueLastUsed = 0
|
m.popUpQueueLastUsed = 0
|
||||||
t.Stop()
|
t.Stop()
|
||||||
return
|
return
|
||||||
@@ -252,6 +259,7 @@ func (m *Controller) ShowPopUpPlayQueue() {
|
|||||||
))
|
))
|
||||||
pop.Resize(size)
|
pop.Resize(size)
|
||||||
popUpQueueList.ScrollToNowPlaying() // must come after resize
|
popUpQueueList.ScrollToNowPlaying() // must come after resize
|
||||||
|
m.stopAfterCurrent.SetChecked(m.App.PlaybackManager.IsStopAfterCurrent())
|
||||||
pop.ShowAtPosition(fyne.NewPos(
|
pop.ShowAtPosition(fyne.NewPos(
|
||||||
canvasSize.Width-size.Width-10,
|
canvasSize.Width-size.Width-10,
|
||||||
canvasSize.Height-size.Height-100,
|
canvasSize.Height-size.Height-100,
|
||||||
|
|||||||
Reference in New Issue
Block a user