From d6ad0de1229a1c7af30be071e9f22dd66a09d0e1 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 8 Aug 2025 08:10:20 -0700 Subject: [PATCH 1/4] add stop after current check to pop up queue --- backend/playbackengine.go | 14 +++++++++++++- backend/playbackmanager.go | 8 ++++++++ ui/controller/controller.go | 10 +++++++++- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/backend/playbackengine.go b/backend/playbackengine.go index fa0d15f..ae0dbdd 100644 --- a/backend/playbackengine.go +++ b/backend/playbackengine.go @@ -65,6 +65,8 @@ type playbackEngine struct { isRadio bool loopMode LoopMode + stopAfterCurrent bool // flag to stop playback after current track ends + // flags for handleOnTrackChange / handleOnStopped callbacks - reset to false in the callbacks wasStopped bool // true iff player was stopped before handleOnTrackChange invocation 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) } +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 { return p.player.Pause() } @@ -634,6 +645,7 @@ func (p *playbackEngine) handleOnStopped() { p.alreadyScrobbled = false p.wasStopped = true p.nowPlayingIdx = -1 + p.stopAfterCurrent = false } // 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() } 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.setNextTrack(p.nextPlayingIndex()) } diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index 72109f7..14dbede 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -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() { nowPlaying := p.NowPlaying() if nowPlaying == nil { diff --git a/ui/controller/controller.go b/ui/controller/controller.go index b6a6677..927cd6f 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -27,6 +27,7 @@ import ( "fyne.io/fyne/v2/container" "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/lang" + "fyne.io/fyne/v2/layout" "fyne.io/fyne/v2/theme" "fyne.io/fyne/v2/widget" ) @@ -57,6 +58,7 @@ type Controller struct { popUpQueue *widget.PopUp popUpQueueList *widgets.PlayQueueList + stopAfterCurrent *widget.Check popUpQueueLastUsed int64 escapablePopUp fyne.CanvasObject haveModal bool @@ -195,7 +197,11 @@ func (m *Controller) ShowPopUpPlayQueue() { title := widget.NewRichTextWithText(lang.L("Play Queue")) title.Segments[0].(*widget.TextSegment).Style.Alignment = fyne.TextAlignCenter 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), ) m.popUpQueue = widget.NewPopUp(ctr, m.MainWindow.Canvas()) @@ -224,6 +230,7 @@ func (m *Controller) ShowPopUpPlayQueue() { fynetooltip.DestroyPopUpToolTipLayer(m.popUpQueue) m.popUpQueue = nil m.popUpQueueList = nil + m.stopAfterCurrent = nil m.popUpQueueLastUsed = 0 t.Stop() return @@ -252,6 +259,7 @@ func (m *Controller) ShowPopUpPlayQueue() { )) pop.Resize(size) popUpQueueList.ScrollToNowPlaying() // must come after resize + m.stopAfterCurrent.SetChecked(m.App.PlaybackManager.IsStopAfterCurrent()) pop.ShowAtPosition(fyne.NewPos( canvasSize.Width-size.Width-10, canvasSize.Height-size.Height-100, From 30ee5deb9ccc1f8ccb2800494bdbc61e1a320afe Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 8 Aug 2025 08:16:33 -0700 Subject: [PATCH 2/4] add CLI flag to set stop after current --- backend/app.go | 4 ++++ backend/cmdlineoptions.go | 20 +++++++++++--------- backend/ipc/api.go | 27 ++++++++++++++------------- backend/ipc/client.go | 8 ++++++++ backend/ipc/server.go | 4 ++++ 5 files changed, 41 insertions(+), 22 deletions(-) diff --git a/backend/app.go b/backend/app.go index 1eae20e..6597581 100644 --- a/backend/app.go +++ b/backend/app.go @@ -548,6 +548,10 @@ func (a *App) checkFlagsAndSendIPCMsg(cli *ipc.Client) error { return cli.SeekBackOrPrevious() case *FlagNext: return cli.SeekNext() + case *FlagStop: + return cli.Stop() + case *FlagStopAfterCurrent: + return cli.StopAfterCurrent() case *FlagShow: return cli.Show() case VolumeCLIArg >= 0: diff --git a/backend/cmdlineoptions.go b/backend/cmdlineoptions.go index c7252ed..4750bf2 100644 --- a/backend/cmdlineoptions.go +++ b/backend/cmdlineoptions.go @@ -12,15 +12,17 @@ var ( SeekByCLIArg float64 = 0 VolumePctCLIArg float64 = 0 - FlagPlay = flag.Bool("play", false, "unpause or begin playback") - FlagPause = flag.Bool("pause", false, "pause playback") - FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state") - FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current") - FlagNext = flag.Bool("next", false, "seek to next track") - FlagStartMinimized = flag.Bool("start-minimized", false, "start app minimized") - FlagShow = flag.Bool("show", false, "show minimized app") - FlagVersion = flag.Bool("version", false, "print app version and exit") - FlagHelp = flag.Bool("help", false, "print command line options and exit") + FlagPlay = flag.Bool("play", false, "unpause or begin playback") + FlagPause = flag.Bool("pause", false, "pause playback") + FlagPlayPause = flag.Bool("play-pause", false, "toggle play/pause state") + FlagPrevious = flag.Bool("previous", false, "seek to previous track or beginning of current") + 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") + FlagShow = flag.Bool("show", false, "show minimized app") + FlagVersion = flag.Bool("version", false, "print app version and exit") + FlagHelp = flag.Bool("help", false, "print command line options and exit") ) func init() { diff --git a/backend/ipc/api.go b/backend/ipc/api.go index f53a4a4..f163962 100644 --- a/backend/ipc/api.go +++ b/backend/ipc/api.go @@ -3,19 +3,20 @@ package ipc import "fmt" const ( - PingPath = "/ping" - PlayPath = "/transport/play" - PlayPausePath = "/transport/playpause" - PausePath = "/transport/pause" - StopPath = "/transport/stop" - PreviousPath = "/transport/previous" - NextPath = "/transport/next" - TimePosPath = "/transport/timepos" // ?s= - SeekByPath = "/transport/seek-by" // ?s=<+/- seconds> - VolumePath = "/volume" // ?v= - VolumeAdjustPath = "/volume/adjust" // ?pct=<+/- percentage> - ShowPath = "/window/show" - QuitPath = "/window/quit" + PingPath = "/ping" + PlayPath = "/transport/play" + PlayPausePath = "/transport/playpause" + PausePath = "/transport/pause" + StopPath = "/transport/stop" + StopAfterCurrentPath = "/transport/stop-after-current" + PreviousPath = "/transport/previous" + NextPath = "/transport/next" + TimePosPath = "/transport/timepos" // ?s= + SeekByPath = "/transport/seek-by" // ?s=<+/- seconds> + VolumePath = "/volume" // ?v= + VolumeAdjustPath = "/volume/adjust" // ?pct=<+/- percentage> + ShowPath = "/window/show" + QuitPath = "/window/quit" ) type Response struct { diff --git a/backend/ipc/client.go b/backend/ipc/client.go index fbc6737..7e9bc80 100644 --- a/backend/ipc/client.go +++ b/backend/ipc/client.go @@ -48,6 +48,14 @@ func (c *Client) PlayPause() error { 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 { return c.sendRequest(NextPath) } diff --git a/backend/ipc/server.go b/backend/ipc/server.go index 8de46ad..7c91408 100644 --- a/backend/ipc/server.go +++ b/backend/ipc/server.go @@ -15,6 +15,7 @@ type PlaybackHandler interface { Continue() SeekBackOrPrevious() SeekNext() + SetStopAfterCurrent(bool) SeekSeconds(float64) SeekBySeconds(float64) Volume() int @@ -68,6 +69,9 @@ func (s *serverImpl) createHandler() http.Handler { m.HandleFunc(PausePath, s.makeSimpleEndpointHandler(s.pbHandler.Pause)) m.HandleFunc(PlayPausePath, s.makeSimpleEndpointHandler(s.pbHandler.PlayPause)) 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(NextPath, s.makeSimpleEndpointHandler(s.pbHandler.SeekNext)) m.HandleFunc(TimePosPath, s.makeFloatEndpointHandler("s", s.pbHandler.SeekSeconds)) From 5dd71c7ce60fbde62996e465c42a0b0b6d63f90a Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 8 Aug 2025 08:26:25 -0700 Subject: [PATCH 3/4] fix crashing when DLNA casting and toggling stop after current --- backend/player/dlna/dlnaplayer.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/backend/player/dlna/dlnaplayer.go b/backend/player/dlna/dlnaplayer.go index bdaf4a8..399f400 100644 --- a/backend/player/dlna/dlnaplayer.go +++ b/backend/player/dlna/dlnaplayer.go @@ -229,6 +229,9 @@ func (d *DLNAPlayer) SetNextFile(url string, meta mediaprovider.MediaItemMetadat Title: meta.Name, Seekable: true, } + } else { + // empty media item to signify erasing next track in device queue + media = &avtransport.MediaItem{} } ctx, cancel := context.WithCancel(context.Background()) From 61cc9b2f854636bc13ca756f7622c5fa0735f8f7 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Fri, 8 Aug 2025 08:41:58 -0700 Subject: [PATCH 4/4] add to en.json --- res/translations/en.json | 1 + 1 file changed, 1 insertion(+) diff --git a/res/translations/en.json b/res/translations/en.json index 48865af..a5bc43c 100644 --- a/res/translations/en.json +++ b/res/translations/en.json @@ -216,6 +216,7 @@ "Soundtrack": "Soundtrack", "Spoken Word": "Spoken Word", "Startup page": "Startup page", + "Stop after current track": "Stop after current track", "Stopped": "Stopped", "Success": "Success", "Support the project": "Support the project",