diff --git a/backend/app.go b/backend/app.go index acb6948..37a662f 100644 --- a/backend/app.go +++ b/backend/app.go @@ -37,7 +37,11 @@ type App struct { PlaybackManager *PlaybackManager Player *player.Player UpdateChecker UpdateChecker - OnReactivate func() + MPRISHandler *MPRISHandler + + // UI callbacks to be set in main + OnReactivate func() + OnExit func() appName string appVersionTag string @@ -50,7 +54,7 @@ func (a *App) VersionTag() string { return a.appVersionTag } -func StartupApp(appName, appVersionTag, configFile, latestReleaseURL string) (*App, error) { +func StartupApp(appName, displayAppName, appVersionTag, configFile, latestReleaseURL string) (*App, error) { sessionPath := configdir.LocalConfig(appName, sessionDir) if _, err := os.Stat(path.Join(sessionPath, sessionLockFile)); err == nil { log.Println("Another instance is running. Reactivating it...") @@ -103,6 +107,8 @@ func StartupApp(appName, appVersionTag, configFile, latestReleaseURL string) (*A _, _ = a.ImageManager.GetCoverThumbnail(coverID) }) + a.setupMPRIS(displayAppName) + return a, nil } @@ -134,9 +140,7 @@ func (a *App) startSessionWatcher(sessionPath string) { activatePath := path.Join(sessionPath, sessionActivateFile) if _, err := os.Stat(activatePath); err == nil { os.Remove(path.Join(sessionPath, sessionActivateFile)) - if a.OnReactivate != nil { - a.OnReactivate() - } + a.callOnReactivate() } } } @@ -144,6 +148,12 @@ func (a *App) startSessionWatcher(sessionPath string) { } } +func (a *App) callOnReactivate() { + if a.OnReactivate != nil { + a.OnReactivate() + } +} + func (a *App) initMPV() error { p := player.NewWithClientName(a.appName) c := a.Config.LocalPlayback @@ -202,6 +212,23 @@ func (a *App) setupMPV() error { return nil } +func (a *App) setupMPRIS(mprisAppName string) { + a.MPRISHandler = NewMPRISHandler(mprisAppName, a.Player, a.PlaybackManager) + a.MPRISHandler.ArtURLLookup = a.ImageManager.GetCoverArtUrl + a.MPRISHandler.OnRaise = func() error { a.callOnReactivate(); return nil } + a.MPRISHandler.OnQuit = func() error { + if a.OnExit == nil { + return errors.New("no quit handler registered") + } + go func() { + time.Sleep(10 * time.Millisecond) + a.OnExit() + }() + return nil + } + a.MPRISHandler.Start() +} + func (a *App) LoginToDefaultServer(string) error { serverCfg := a.ServerManager.GetDefaultServer() if serverCfg == nil { @@ -221,6 +248,7 @@ func (a *App) DeleteServerCacheDir(serverID uuid.UUID) error { } func (a *App) Shutdown() { + a.MPRISHandler.Shutdown() a.PlaybackManager.DisableCallbacks() a.Player.Stop() // will trigger scrobble check a.Config.LocalPlayback.Volume = a.Player.GetVolume() diff --git a/backend/imagemanager.go b/backend/imagemanager.go index c515f90..b5d4721 100644 --- a/backend/imagemanager.go +++ b/backend/imagemanager.go @@ -88,6 +88,16 @@ func (i *ImageManager) GetFullSizeCoverArt(coverID string) (image.Image, error) return im, nil } +func (i *ImageManager) GetCoverArtUrl(coverID string) (string, error) { + path := i.filePathForCover(coverID) + if _, err := os.Stat(path); err == nil { + // this is probably broken for Windows but it's currently only used + // for MPRIS, so we are OK for now + return fmt.Sprintf("file://%s", path), nil + } + return "", errors.New("cover not found") +} + func (i *ImageManager) GetCachedArtistImage(artistID string) (image.Image, bool) { return i.loadLocalImage(i.filePathForArtistImage(artistID)) } diff --git a/backend/mpris.go b/backend/mpris.go new file mode 100644 index 0000000..83243d9 --- /dev/null +++ b/backend/mpris.go @@ -0,0 +1,303 @@ +package backend + +import ( + "errors" + "fmt" + + "github.com/dweymouth/supersonic/backend/mediaprovider" + "github.com/dweymouth/supersonic/player" + "github.com/godbus/dbus/v5" + "github.com/quarckster/go-mpris-server/pkg/events" + "github.com/quarckster/go-mpris-server/pkg/server" + "github.com/quarckster/go-mpris-server/pkg/types" +) + +const dbusTrackIDPrefix = "/Supersonic/Track/" + +var ( + _ types.OrgMprisMediaPlayer2Adapter = (*MPRISHandler)(nil) + _ types.OrgMprisMediaPlayer2PlayerAdapter = (*MPRISHandler)(nil) + _ types.OrgMprisMediaPlayer2PlayerAdapterLoopStatus = (*MPRISHandler)(nil) +) + +var ( + errNotImplemented = errors.New("not implemented") +) + +type MPRISHandler struct { + // Function called if the player is requested to quit through MPRIS. + // Should *asynchronously* start shutdown and return immediately true if a shutdown will happen. + OnQuit func() error + + // Function called if the player is requested to bring its UI to the front. + OnRaise func() error + + // Function to look up the artwork URL for a given track ID + ArtURLLookup func(trackID string) (string, error) + + connErr error + playerName string + p *player.Player + pm *PlaybackManager + s *server.Server + evt *events.EventHandler +} + +func NewMPRISHandler(playerName string, p *player.Player, pm *PlaybackManager) *MPRISHandler { + m := &MPRISHandler{playerName: playerName, p: p, pm: pm, connErr: errors.New("not started")} + m.s = server.NewServer(playerName, m, m) + m.evt = events.NewEventHandler(m.s) + + m.p.OnSeek(func() { + if m.connErr == nil { + pos := secondsToMicroseconds(m.p.GetStatus().TimePos) + m.evt.Player.OnSeek(pos) + } + }) + m.pm.OnSongChange(func(_, _ *mediaprovider.Track) { + if m.connErr == nil { + m.evt.Player.OnTitle() + } + }) + m.pm.OnVolumeChange(func(vol int) { + if m.connErr == nil { + m.evt.Player.OnVolume() + } + }) + emitPlayStatus := func() { + if m.connErr == nil { + m.evt.Player.OnPlayPause() + } + } + m.p.OnStopped(emitPlayStatus) + m.p.OnPlaying(emitPlayStatus) + m.p.OnPaused(emitPlayStatus) + + return m +} + +// Starts listening for MPRIS events. +func (m *MPRISHandler) Start() { + m.connErr = nil + go func() { + // exits early with err if unable to establish D-Bus connection + m.connErr = m.s.Listen() + }() +} + +// Stops listening for MPRIS events and releases any D-Bus resources. +func (m *MPRISHandler) Shutdown() { + if m.connErr == nil { + m.s.Stop() + m.connErr = errors.New("stopped") + } +} + +// OrgMprisMediaPlayer2Adapter implementation + +func (m *MPRISHandler) Identity() (string, error) { + return m.playerName, nil +} + +func (m *MPRISHandler) CanQuit() (bool, error) { + return m.OnQuit != nil, nil +} + +func (m *MPRISHandler) Quit() error { + if m.OnQuit != nil { + return m.OnQuit() + } + return errors.New("no quit handler added") +} + +func (m *MPRISHandler) CanRaise() (bool, error) { + return m.OnRaise != nil, nil +} + +func (m *MPRISHandler) Raise() error { + if m.OnRaise != nil { + return m.OnRaise() + } + return errors.New("no raise handler added") +} + +func (m *MPRISHandler) HasTrackList() (bool, error) { + return false, nil +} + +func (m *MPRISHandler) SupportedUriSchemes() ([]string, error) { + return nil, nil +} + +func (m *MPRISHandler) SupportedMimeTypes() ([]string, error) { + return nil, nil +} + +// OrgMprisMediaPlayer2PlayerAdapter implementation + +func (m *MPRISHandler) Next() error { + return m.p.SeekNext() +} + +func (m *MPRISHandler) Previous() error { + return m.p.SeekBackOrPrevious() +} + +func (m *MPRISHandler) Pause() error { + if m.p.GetStatus().State == player.Playing { + return m.p.PlayPause() + } + return nil +} + +func (m *MPRISHandler) PlayPause() error { + return m.p.PlayPause() +} + +func (m *MPRISHandler) Stop() error { + return m.p.Stop() +} + +func (m *MPRISHandler) Play() error { + switch m.p.GetStatus().State { + case player.Paused: + return m.p.PlayPause() + case player.Stopped: + return m.p.PlayFromBeginning() + } + return nil +} + +func (m *MPRISHandler) Seek(offset types.Microseconds) error { + return m.p.Seek(fmt.Sprintf("%0.2f", float64(offset)/1_000_000), player.SeekRelative) +} + +func (m *MPRISHandler) SetPosition(trackId string, position types.Microseconds) error { + return errNotImplemented +} + +func (m *MPRISHandler) OpenUri(uri string) error { + return errNotImplemented +} + +func (m *MPRISHandler) PlaybackStatus() (types.PlaybackStatus, error) { + switch m.p.GetStatus().State { + case player.Playing: + return types.PlaybackStatusPlaying, nil + case player.Paused: + return types.PlaybackStatusPaused, nil + case player.Stopped: + return types.PlaybackStatusStopped, nil + } + return "", errors.New("unknown playback status") +} + +func (m *MPRISHandler) LoopStatus() (types.LoopStatus, error) { + switch m.pm.LoopMode() { + case LoopModeAll: + return types.LoopStatusPlaylist, nil + case LoopModeOne: + return types.LoopStatusTrack, nil + case LoopModeNone: + return types.LoopStatusNone, nil + } + return "", errors.New("unknown loop status") +} + +func (m *MPRISHandler) SetLoopStatus(status types.LoopStatus) error { + switch status { + case types.LoopStatusPlaylist: + return m.pm.SetLoopMode(LoopModeAll) + case types.LoopStatusTrack: + return m.pm.SetLoopMode(LoopModeOne) + case types.LoopStatusNone: + return m.pm.SetLoopMode(LoopModeNone) + } + return errors.New("unknown loop status") +} + +func (m *MPRISHandler) Rate() (float64, error) { + return 1, nil +} + +func (m *MPRISHandler) SetRate(float64) error { + return errNotImplemented +} + +func (m *MPRISHandler) Metadata() (types.Metadata, error) { + status := m.p.GetStatus() + var tr mediaprovider.Track + if np := m.pm.NowPlaying(); np != nil && status.State != player.Stopped { + tr = *np + } + var trackID string + if tr.ID != "" { + trackID = dbusTrackIDPrefix + tr.ID + } + var artURL string + if tr.ID != "" && m.ArtURLLookup != nil { + if u, err := m.ArtURLLookup(tr.CoverArtID); err == nil { + artURL = u + } + } + return types.Metadata{ + TrackId: dbus.ObjectPath(trackID), + Length: types.Microseconds(status.Duration), + Title: tr.Name, + Album: tr.Album, + Artist: tr.ArtistNames, + DiscNumber: tr.DiscNumber, + TrackNumber: tr.TrackNumber, + Genre: []string{tr.Genre}, + UserRating: float64(tr.Rating) / 5, + ArtUrl: artURL, + }, nil +} + +func (m *MPRISHandler) Volume() (float64, error) { + return float64(m.p.GetVolume()) / 100, nil +} + +func (m *MPRISHandler) SetVolume(v float64) error { + return m.pm.SetVolume(int(v * 100)) +} + +func (m *MPRISHandler) Position() (int64, error) { + return int64(secondsToMicroseconds(m.p.GetStatus().TimePos)), nil +} + +func (m *MPRISHandler) MinimumRate() (float64, error) { + return 1, nil +} + +func (m *MPRISHandler) MaximumRate() (float64, error) { + return 1, nil +} + +func (m *MPRISHandler) CanGoNext() (bool, error) { + return true, nil +} + +func (m *MPRISHandler) CanGoPrevious() (bool, error) { + return true, nil +} + +func (m *MPRISHandler) CanPlay() (bool, error) { + return true, nil +} + +func (m *MPRISHandler) CanPause() (bool, error) { + return true, nil +} + +func (m *MPRISHandler) CanSeek() (bool, error) { + return true, nil +} + +func (m *MPRISHandler) CanControl() (bool, error) { + return true, nil +} + +func secondsToMicroseconds(s float64) types.Microseconds { + return types.Microseconds(s * 1_000_000) +} diff --git a/backend/playbackmanager.go b/backend/playbackmanager.go index fb121bf..97de5af 100644 --- a/backend/playbackmanager.go +++ b/backend/playbackmanager.go @@ -17,6 +17,14 @@ const ( ReplayGainTrack = string(player.ReplayGainTrack) ) +type LoopMode int + +const ( + LoopModeNone LoopMode = LoopMode(player.LoopNone) + LoopModeAll LoopMode = LoopMode(player.LoopAll) + LoopModeOne LoopMode = LoopMode(player.LoopOne) +) + // A high-level Subsonic-aware playback backend. // Manages loading tracks into the Player queue, // sending callbacks on play time updates and track changes. @@ -40,7 +48,8 @@ type PlaybackManager struct { onSongChange []func(nowPlaying, justScrobbledIfAny *mediaprovider.Track) onPlayTimeUpdate []func(float64, float64) - onLoopModeChange []func(string) + onLoopModeChange []func(LoopMode) + onVolumeChange []func(int) } func NewPlaybackManager( @@ -130,10 +139,15 @@ func (p *PlaybackManager) OnPlayTimeUpdate(cb func(float64, float64)) { } // Registers a callback that is notified whenever the loop mode changes. -func (p *PlaybackManager) OnLoopModeChange(cb func(string)) { +func (p *PlaybackManager) OnLoopModeChange(cb func(LoopMode)) { p.onLoopModeChange = append(p.onLoopModeChange, cb) } +// Registers a callback that is notified whenever the volume changes. +func (p *PlaybackManager) OnVolumeChange(cb func(int)) { + p.onVolumeChange = append(p.onVolumeChange, 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) @@ -305,12 +319,43 @@ func (p *PlaybackManager) SetNextLoopMode() error { } for _, cb := range p.onLoopModeChange { - cb(p.player.GetLoopMode().String()) + cb(LoopMode(p.player.GetLoopMode())) } return nil } +func (p *PlaybackManager) SetLoopMode(loopMode LoopMode) error { + if err := p.player.SetLoopMode(player.LoopMode(loopMode)); err != nil { + return err + } + + for _, cb := range p.onLoopModeChange { + cb(loopMode) + } + + return nil +} + +func (p *PlaybackManager) LoopMode() LoopMode { + return LoopMode(p.player.GetLoopMode()) +} + +func (p *PlaybackManager) SetVolume(vol int) error { + vol = clamp(vol, 0, 100) + if err := p.player.SetVolume(vol); err != nil { + return err + } + for _, cb := range p.onVolumeChange { + cb(vol) + } + return nil +} + +func (p *PlaybackManager) Volume() int { + return p.player.GetVolume() +} + // call BEFORE updating p.nowPlayingIdx func (p *PlaybackManager) checkScrobble() { if !p.scrobbleCfg.Enabled || len(p.playQueue) == 0 || p.nowPlayingIdx < 0 { diff --git a/go.mod b/go.mod index e427fcf..a3a4aa5 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,10 @@ require ( github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee github.com/dweymouth/go-subsonic v0.0.0-20230614154319-792d18c75fb4 github.com/fsnotify/fsnotify v1.6.0 + github.com/godbus/dbus/v5 v5.1.0 github.com/google/uuid v1.3.0 github.com/pelletier/go-toml/v2 v2.0.8 + github.com/quarckster/go-mpris-server v1.0.1 github.com/zalando/go-keyring v0.2.1 golang.org/x/net v0.0.0-20220722155237-a158d28d115b ) @@ -27,7 +29,6 @@ require ( github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect github.com/go-text/typesetting v0.0.0-20230405155246-bf9c697c6e16 // indirect - github.com/godbus/dbus/v5 v5.1.0 // indirect github.com/goki/freetype v0.0.0-20220119013949-7a161fd3728c // indirect github.com/gopherjs/gopherjs v1.17.2 // indirect github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect @@ -47,3 +48,5 @@ require ( ) replace fyne.io/fyne/v2 v2.3.5 => github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5 + +replace github.com/quarckster/go-mpris-server v1.0.1 => github.com/dweymouth/go-mpris-server v0.0.0-20230714011337-8bc0b52618a5 diff --git a/go.sum b/go.sum index 5420507..1e26d28 100644 --- a/go.sum +++ b/go.sum @@ -76,6 +76,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5 h1:uXbHzg9HfefA7OVHu9gahobCP5B43df3L1MwU0GbdRs= github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230505012127-ca61c153b2a5/go.mod h1:X2+NrR+62mvAiAt2fwKT7035zQsE77KVV1NlvWo4vW8= +github.com/dweymouth/go-mpris-server v0.0.0-20230714011337-8bc0b52618a5 h1:QhBf0iHPPUSxalHjhls+9LFqMuNT12U3Mo/AFw4Ornc= +github.com/dweymouth/go-mpris-server v0.0.0-20230714011337-8bc0b52618a5/go.mod h1:2b4IdrpnEoEfU+6fQKjYhAgdvsiz4JxmTpDAUrMJVO4= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY= github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0= github.com/dweymouth/go-subsonic v0.0.0-20230614154319-792d18c75fb4 h1:cuyvB4GjTMBHKJwMJ/LFpuKfSVXuCheNj6fgxBa3m1Y= diff --git a/main.go b/main.go index 60b73c2..01e09b1 100644 --- a/main.go +++ b/main.go @@ -23,7 +23,7 @@ const ( ) func main() { - myApp, err := backend.StartupApp(appname, appVersionTag, configFile, latestReleaseURL) + myApp, err := backend.StartupApp(appname, displayName, appVersionTag, configFile, latestReleaseURL) if err != nil { log.Fatalf("fatal startup error: %v", err.Error()) } @@ -41,6 +41,10 @@ func main() { } mainWindow := ui.NewMainWindow(fyneApp, appname, displayName, appVersion, myApp, fyne.NewSize(w, h)) myApp.OnReactivate = mainWindow.Show + myApp.OnExit = func() { + saveWindowPosition(myApp.Config, mainWindow.Window) + fyneApp.Quit() + } go func() { // TODO: There is a race condition with laying out the window before the @@ -61,8 +65,7 @@ func main() { mainWindow.Show() mainWindow.Window.SetCloseIntercept(func() { - myApp.Config.Application.WindowHeight = int(mainWindow.Canvas().Size().Height) - myApp.Config.Application.WindowWidth = int(mainWindow.Canvas().Size().Width) + saveWindowPosition(myApp.Config, mainWindow.Window) if myApp.Config.Application.CloseToSystemTray && mainWindow.HaveSystemTray() { mainWindow.Window.Hide() @@ -75,3 +78,8 @@ func main() { log.Println("Running shutdown tasks...") myApp.Shutdown() } + +func saveWindowPosition(config *backend.Config, window fyne.Window) { + config.Application.WindowHeight = int(window.Canvas().Size().Height) + config.Application.WindowWidth = int(window.Canvas().Size().Width) +} diff --git a/ui/bottompanel.go b/ui/bottompanel.go index 36ad8cc..69fe61d 100644 --- a/ui/bottompanel.go +++ b/ui/bottompanel.go @@ -54,9 +54,6 @@ func NewBottomPanel(p *player.Player, pm *backend.PlaybackManager, contr *contro p.OnStopped(func() { bp.Controls.SetPlaying(false) }) - pm.OnLoopModeChange(func(mode string) { - bp.AuxControls.SetLoopMode(mode) - }) bp.NowPlaying = widgets.NewNowPlayingCard() bp.NowPlaying.OnShowCoverImage = func() { @@ -100,8 +97,10 @@ func NewBottomPanel(p *player.Player, pm *backend.PlaybackManager, contr *contro }) bp.AuxControls = widgets.NewAuxControls(p.GetVolume()) - bp.AuxControls.VolumeControl.OnVolumeChanged = func(v int) { - _ = p.SetVolume(v) + pm.OnLoopModeChange(bp.AuxControls.SetLoopMode) + pm.OnVolumeChange(bp.AuxControls.VolumeControl.SetVolume) + bp.AuxControls.VolumeControl.OnSetVolume = func(v int) { + _ = bp.playbackManager.SetVolume(v) } bp.AuxControls.OnChangeLoopMode(func() { bp.playbackManager.SetNextLoopMode() diff --git a/ui/mainwindow.go b/ui/mainwindow.go index be0d54e..0baec49 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -163,15 +163,15 @@ func (m *MainWindow) SetupSystemTrayMenu(appName string, fyneApp fyne.App) { }), fyne.NewMenuItemSeparator(), fyne.NewMenuItem("Volume +10%", func() { - vol := m.App.Player.GetVolume() + vol := m.App.PlaybackManager.Volume() vol = vol + int(float64(vol)*0.1) // will clamp to range for us - m.BottomPanel.AuxControls.VolumeControl.SetVolume(vol) + m.App.PlaybackManager.SetVolume(vol) }), fyne.NewMenuItem("Volume -10%", func() { - vol := m.App.Player.GetVolume() + vol := m.App.PlaybackManager.Volume() vol = vol - int(float64(vol)*0.1) - m.BottomPanel.AuxControls.VolumeControl.SetVolume(vol) + m.App.PlaybackManager.SetVolume(vol) }), fyne.NewMenuItemSeparator(), fyne.NewMenuItem("Show", m.Window.Show), diff --git a/ui/widgets/auxcontrols.go b/ui/widgets/auxcontrols.go index 0119e23..e426157 100644 --- a/ui/widgets/auxcontrols.go +++ b/ui/widgets/auxcontrols.go @@ -7,6 +7,7 @@ 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" ) @@ -67,14 +68,15 @@ func (a *AuxControls) OnChangeLoopMode(f func()) { a.loop.OnTapped = f } -func (a *AuxControls) SetLoopMode(mode string) { - if mode == "all" { +func (a *AuxControls) SetLoopMode(mode backend.LoopMode) { + switch mode { + case backend.LoopModeAll: a.loop.Importance = widget.HighImportance a.loop.Icon = myTheme.RepeatIcon - } else if mode == "one" { + case backend.LoopModeOne: a.loop.Importance = widget.HighImportance a.loop.Icon = myTheme.RepeatOneIcon - } else { + case backend.LoopModeNone: a.loop.Importance = widget.MediumImportance a.loop.Icon = myTheme.RepeatIcon } @@ -160,7 +162,7 @@ type VolumeControl struct { icon *TappableIcon slider *volumeSlider - OnVolumeChanged func(int) + OnSetVolume func(int) muted bool lastVol int @@ -183,24 +185,35 @@ func NewVolumeControl(initialVol int) *VolumeControl { return v } +// Sets the volume that is displayed in the slider. +// Does not invoke OnSetVolume callback. +func (v *VolumeControl) SetVolume(vol int) { + if (vol == v.lastVol && !v.muted) || (v.muted && vol == 0) { + return + } + v.lastVol = vol + v.muted = false + v.setDisplayedVolume(vol) +} + func (v *VolumeControl) onChanged(volume float64) { vol := int(volume) v.lastVol = vol v.muted = false v.updateIconForVolume(vol) - if v.OnVolumeChanged != nil { - v.OnVolumeChanged(vol) - } + v.invokeOnVolumeChange(vol) } func (v *VolumeControl) toggleMute() { if !v.muted { v.muted = true v.lastVol = int(v.slider.Value) - v.SetVolume(0) + v.setDisplayedVolume(0) + v.invokeOnVolumeChange(0) } else { v.muted = false - v.SetVolume(v.lastVol) + v.setDisplayedVolume(v.lastVol) + v.invokeOnVolumeChange(v.lastVol) } } @@ -209,12 +222,15 @@ func (v *VolumeControl) CreateRenderer() fyne.WidgetRenderer { return widget.NewSimpleRenderer(v.container) } -func (v *VolumeControl) SetVolume(vol int) { +func (v *VolumeControl) setDisplayedVolume(vol int) { v.slider.Value = float64(vol) v.slider.Refresh() v.updateIconForVolume(vol) - if v.OnVolumeChanged != nil { - v.OnVolumeChanged(vol) +} + +func (v *VolumeControl) invokeOnVolumeChange(vol int) { + if v.OnSetVolume != nil { + v.OnSetVolume(vol) } }