Merge pull request #122 from dweymouth/feature/systemtray
Add system tray menu and close to tray option
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
### Added
|
### Added
|
||||||
- [#107](https://github.com/dweymouth/supersonic/issues/107) Add playback settings dropdown to choose output audio device
|
- [#107](https://github.com/dweymouth/supersonic/issues/107) Add playback settings dropdown to choose output audio device
|
||||||
- [#119](https://github.com/dweymouth/supersonic/issues/119) Add file path column to tracklist view
|
- [#119](https://github.com/dweymouth/supersonic/issues/119) Add file path column to tracklist view
|
||||||
|
- [#117](https://github.com/dweymouth/supersonic/issues/117) Add (optional) system tray menu and close to tray support
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
- **todo-commithash** Don't show update available prompt if the found version is the same as the running app version
|
- **todo-commithash** Don't show update available prompt if the found version is the same as the running app version
|
||||||
|
|||||||
+2
-1
@@ -151,7 +151,8 @@ func (a *App) LoginToDefaultServer(string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) Shutdown() {
|
func (a *App) Shutdown() {
|
||||||
a.Player.Stop()
|
a.PlaybackManager.DisableCallbacks()
|
||||||
|
a.Player.Stop() // will trigger scrobble check
|
||||||
a.Config.LocalPlayback.Volume = a.Player.GetVolume()
|
a.Config.LocalPlayback.Volume = a.Player.GetVolume()
|
||||||
a.cancel()
|
a.cancel()
|
||||||
a.Player.Destroy()
|
a.Player.Destroy()
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ type AppConfig struct {
|
|||||||
WindowWidth int
|
WindowWidth int
|
||||||
WindowHeight int
|
WindowHeight int
|
||||||
LastCheckedVersion string
|
LastCheckedVersion string
|
||||||
|
EnableSystemTray bool
|
||||||
|
CloseToSystemTray bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlbumPageConfig struct {
|
type AlbumPageConfig struct {
|
||||||
@@ -87,6 +89,8 @@ func DefaultConfig(appVersionTag string) *Config {
|
|||||||
WindowWidth: 1000,
|
WindowWidth: 1000,
|
||||||
WindowHeight: 800,
|
WindowHeight: 800,
|
||||||
LastCheckedVersion: appVersionTag,
|
LastCheckedVersion: appVersionTag,
|
||||||
|
EnableSystemTray: true,
|
||||||
|
CloseToSystemTray: false,
|
||||||
},
|
},
|
||||||
AlbumPage: AlbumPageConfig{
|
AlbumPage: AlbumPageConfig{
|
||||||
TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite"},
|
TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite"},
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ type PlaybackManager struct {
|
|||||||
|
|
||||||
playTimeStopwatch util.Stopwatch
|
playTimeStopwatch util.Stopwatch
|
||||||
curTrackTime float64
|
curTrackTime float64
|
||||||
|
callbacksDisabled bool
|
||||||
|
|
||||||
playQueue []*subsonic.Child
|
playQueue []*subsonic.Child
|
||||||
nowPlayingIdx int64
|
nowPlayingIdx int64
|
||||||
@@ -99,6 +100,12 @@ func (p *PlaybackManager) IsSeeking() bool {
|
|||||||
return p.player.IsSeeking()
|
return p.player.IsSeeking()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Should only be called before quitting.
|
||||||
|
// Disables playback state callbacks being sent
|
||||||
|
func (p *PlaybackManager) DisableCallbacks() {
|
||||||
|
p.callbacksDisabled = true
|
||||||
|
}
|
||||||
|
|
||||||
// Gets the curently playing song, if any.
|
// Gets the curently playing song, if any.
|
||||||
func (p *PlaybackManager) NowPlaying() *subsonic.Child {
|
func (p *PlaybackManager) NowPlaying() *subsonic.Child {
|
||||||
if len(p.playQueue) == 0 || p.player.GetStatus().State == player.Stopped {
|
if len(p.playQueue) == 0 || p.player.GetStatus().State == player.Stopped {
|
||||||
@@ -310,6 +317,9 @@ func (p *PlaybackManager) sendNowPlayingScrobble() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *PlaybackManager) invokeOnSongChangeCallbacks() {
|
func (p *PlaybackManager) invokeOnSongChangeCallbacks() {
|
||||||
|
if p.callbacksDisabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
for _, cb := range p.onSongChange {
|
for _, cb := range p.onSongChange {
|
||||||
cb(p.NowPlaying(), p.lastScrobbled)
|
cb(p.NowPlaying(), p.lastScrobbled)
|
||||||
}
|
}
|
||||||
@@ -337,6 +347,9 @@ func (p *PlaybackManager) startPollTimePos() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *PlaybackManager) doUpdateTimePos() {
|
func (p *PlaybackManager) doUpdateTimePos() {
|
||||||
|
if p.callbacksDisabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
s := p.player.GetStatus()
|
s := p.player.GetStatus()
|
||||||
for _, cb := range p.onPlayTimeUpdate {
|
for _, cb := range p.onPlayTimeUpdate {
|
||||||
cb(s.TimePos, s.Duration)
|
cb(s.TimePos, s.Duration)
|
||||||
|
|||||||
@@ -45,4 +45,4 @@ require (
|
|||||||
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
|
honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
|
||||||
)
|
)
|
||||||
|
|
||||||
replace fyne.io/fyne/v2 v2.3.3 => github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230331041414-b548301c117c
|
replace fyne.io/fyne/v2 v2.3.3 => github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230407015007-c8e2aa472ae4
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7h
|
|||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
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/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230331041414-b548301c117c h1:w3Q+TShgmyoTBeWoYEZC5yflP15bkLxAzHWSmxjHQXs=
|
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230407015007-c8e2aa472ae4 h1:YtnAxHdI0Q9Sjo3jBKZpAsxQO+WZ37S/LvxF85lCS78=
|
||||||
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230331041414-b548301c117c/go.mod h1:MABZ23XXF2K24hl3rva+Di/UbMpibBMAO+qDtDCSe8k=
|
github.com/dweymouth/fyne/v2 v2.3.0-rc1.0.20230407015007-c8e2aa472ae4/go.mod h1:MABZ23XXF2K24hl3rva+Di/UbMpibBMAO+qDtDCSe8k=
|
||||||
github.com/dweymouth/go-mpv v0.0.0-20230406003141-7f1858e503ee h1:ZGyJ6wp7CAfT31BugypcF/TPKEy2RrGR9JFq1JOjOpY=
|
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-mpv v0.0.0-20230406003141-7f1858e503ee/go.mod h1:Ov0ieN90M7i+0k3OxhA/g1dozGs+UcPHDsMKqPgRDk0=
|
||||||
github.com/dweymouth/go-subsonic v0.0.0-20230210044542-537b9238299b h1:8JbTKYDdQg6JKu7ZDbEaVbXxutc3pRF58yNvTVMBHec=
|
github.com/dweymouth/go-subsonic v0.0.0-20230210044542-537b9238299b h1:8JbTKYDdQg6JKu7ZDbEaVbXxutc3pRF58yNvTVMBHec=
|
||||||
|
|||||||
@@ -57,10 +57,15 @@ func main() {
|
|||||||
mainWindow.Window.SetCloseIntercept(func() {
|
mainWindow.Window.SetCloseIntercept(func() {
|
||||||
myApp.Config.Application.WindowHeight = int(mainWindow.Canvas().Size().Height)
|
myApp.Config.Application.WindowHeight = int(mainWindow.Canvas().Size().Height)
|
||||||
myApp.Config.Application.WindowWidth = int(mainWindow.Canvas().Size().Width)
|
myApp.Config.Application.WindowWidth = int(mainWindow.Canvas().Size().Width)
|
||||||
mainWindow.Window.Close()
|
if myApp.Config.Application.CloseToSystemTray &&
|
||||||
|
mainWindow.HaveSystemTray() {
|
||||||
|
mainWindow.Window.Hide()
|
||||||
|
} else {
|
||||||
|
fyneApp.Quit()
|
||||||
|
}
|
||||||
})
|
})
|
||||||
fyneApp.Run()
|
fyneApp.Run()
|
||||||
|
|
||||||
// shutdown tasks
|
log.Println("Running shutdown tasks...")
|
||||||
myApp.Shutdown()
|
myApp.Shutdown()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import (
|
|||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/data/binding"
|
||||||
"fyne.io/fyne/v2/layout"
|
"fyne.io/fyne/v2/layout"
|
||||||
|
"fyne.io/fyne/v2/theme"
|
||||||
"fyne.io/fyne/v2/widget"
|
"fyne.io/fyne/v2/widget"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,6 +30,7 @@ type SettingsDialog struct {
|
|||||||
|
|
||||||
config *backend.Config
|
config *backend.Config
|
||||||
audioDevices []player.AudioDevice
|
audioDevices []player.AudioDevice
|
||||||
|
promptText *widget.RichText
|
||||||
|
|
||||||
content fyne.CanvasObject
|
content fyne.CanvasObject
|
||||||
}
|
}
|
||||||
@@ -41,8 +44,9 @@ func NewSettingsDialog(config *backend.Config, audioDeviceList []player.AudioDev
|
|||||||
s.createGeneralTab(),
|
s.createGeneralTab(),
|
||||||
s.createPlaybackTab(),
|
s.createPlaybackTab(),
|
||||||
)
|
)
|
||||||
|
s.promptText = widget.NewRichTextWithText("")
|
||||||
s.content = container.NewVBox(tabs, widget.NewSeparator(),
|
s.content = container.NewVBox(tabs, widget.NewSeparator(),
|
||||||
container.NewHBox(layout.NewSpacer(), widget.NewButton("Close", func() {
|
container.NewHBox(s.promptText, layout.NewSpacer(), widget.NewButton("Close", func() {
|
||||||
if s.OnDismiss != nil {
|
if s.OnDismiss != nil {
|
||||||
s.OnDismiss()
|
s.OnDismiss()
|
||||||
}
|
}
|
||||||
@@ -52,6 +56,28 @@ func NewSettingsDialog(config *backend.Config, audioDeviceList []player.AudioDev
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingsDialog) createGeneralTab() *container.TabItem {
|
func (s *SettingsDialog) createGeneralTab() *container.TabItem {
|
||||||
|
closeToTray := widget.NewCheckWithData("Close to system tray",
|
||||||
|
binding.BindBool(&s.config.Application.CloseToSystemTray))
|
||||||
|
if !s.config.Application.EnableSystemTray {
|
||||||
|
closeToTray.Disable()
|
||||||
|
}
|
||||||
|
systemTrayEnable := widget.NewCheck("Enable system tray", func(val bool) {
|
||||||
|
s.config.Application.EnableSystemTray = val
|
||||||
|
// TODO: see https://github.com/fyne-io/fyne/issues/3788
|
||||||
|
// Once Fyne supports removing/hiding an existing system tray menu,
|
||||||
|
// the restart required prompt can be removed and this dialog
|
||||||
|
// can expose a callback for the Controller to show/hide the system tray menu.
|
||||||
|
s.setRestartRequired()
|
||||||
|
if val {
|
||||||
|
closeToTray.Enable()
|
||||||
|
} else {
|
||||||
|
closeToTray.Disable()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
systemTrayEnable.Checked = s.config.Application.EnableSystemTray
|
||||||
|
|
||||||
|
// Scrobble settings
|
||||||
|
|
||||||
twoDigitValidator := func(text string, r rune) bool {
|
twoDigitValidator := func(text string, r rune) bool {
|
||||||
return unicode.IsDigit(r) && len(text) < 2
|
return unicode.IsDigit(r) && len(text) < 2
|
||||||
}
|
}
|
||||||
@@ -122,6 +148,10 @@ func (s *SettingsDialog) createGeneralTab() *container.TabItem {
|
|||||||
scrobbleEnabled.Checked = s.config.Scrobbling.Enabled
|
scrobbleEnabled.Checked = s.config.Scrobbling.Enabled
|
||||||
|
|
||||||
return container.NewTabItem("General", container.NewVBox(
|
return container.NewTabItem("General", container.NewVBox(
|
||||||
|
systemTrayEnable,
|
||||||
|
closeToTray,
|
||||||
|
s.newSectionSeparator(),
|
||||||
|
|
||||||
widget.NewRichText(&widget.TextSegment{Text: "Scrobbling", Style: boldStyle}),
|
widget.NewRichText(&widget.TextSegment{Text: "Scrobbling", Style: boldStyle}),
|
||||||
scrobbleEnabled,
|
scrobbleEnabled,
|
||||||
container.NewHBox(
|
container.NewHBox(
|
||||||
@@ -217,7 +247,8 @@ func (s *SettingsDialog) createPlaybackTab() *container.TabItem {
|
|||||||
widget.NewLabel("Audio device"), container.NewBorder(nil, nil, nil, util.NewHSpace(70), deviceSelect),
|
widget.NewLabel("Audio device"), container.NewBorder(nil, nil, nil, util.NewHSpace(70), deviceSelect),
|
||||||
layout.NewSpacer(), container.NewHBox(audioExclusive, layout.NewSpacer()),
|
layout.NewSpacer(), container.NewHBox(audioExclusive, layout.NewSpacer()),
|
||||||
)),
|
)),
|
||||||
container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15}, widget.NewSeparator()),
|
s.newSectionSeparator(),
|
||||||
|
|
||||||
widget.NewRichText(&widget.TextSegment{Text: "ReplayGain", Style: boldStyle}),
|
widget.NewRichText(&widget.TextSegment{Text: "ReplayGain", Style: boldStyle}),
|
||||||
container.New(layout.NewFormLayout(),
|
container.New(layout.NewFormLayout(),
|
||||||
widget.NewLabel("ReplayGain mode"), container.NewGridWithColumns(2, replayGainSelect),
|
widget.NewLabel("ReplayGain mode"), container.NewGridWithColumns(2, replayGainSelect),
|
||||||
@@ -227,6 +258,20 @@ func (s *SettingsDialog) createPlaybackTab() *container.TabItem {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *SettingsDialog) setRestartRequired() {
|
||||||
|
ts := s.promptText.Segments[0].(*widget.TextSegment)
|
||||||
|
if ts.Text != "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ts.Text = "Restart required"
|
||||||
|
ts.Style.ColorName = theme.ColorNameError
|
||||||
|
s.promptText.Refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingsDialog) newSectionSeparator() fyne.CanvasObject {
|
||||||
|
return container.New(&layouts.MaxPadLayout{PadLeft: 15, PadRight: 15}, widget.NewSeparator())
|
||||||
|
}
|
||||||
|
|
||||||
func (s *SettingsDialog) onReplayGainSettingsChanged() {
|
func (s *SettingsDialog) onReplayGainSettingsChanged() {
|
||||||
if s.OnReplayGainSettingsChanged != nil {
|
if s.OnReplayGainSettingsChanged != nil {
|
||||||
s.OnReplayGainSettingsChanged()
|
s.OnReplayGainSettingsChanged()
|
||||||
|
|||||||
+31
-1
@@ -40,7 +40,8 @@ type MainWindow struct {
|
|||||||
BrowsingPane *browsing.BrowsingPane
|
BrowsingPane *browsing.BrowsingPane
|
||||||
BottomPanel *BottomPanel
|
BottomPanel *BottomPanel
|
||||||
|
|
||||||
container *fyne.Container
|
haveSystemTray bool
|
||||||
|
container *fyne.Container
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -54,6 +55,9 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
|
|||||||
BrowsingPane: browsing.NewBrowsingPane(app),
|
BrowsingPane: browsing.NewBrowsingPane(app),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if app.Config.Application.EnableSystemTray {
|
||||||
|
m.SetupSystemTrayMenu(appName, fyneApp)
|
||||||
|
}
|
||||||
m.Controller = &controller.Controller{
|
m.Controller = &controller.Controller{
|
||||||
AppVersion: appVersion,
|
AppVersion: appVersion,
|
||||||
MainWindow: m.Window,
|
MainWindow: m.Window,
|
||||||
@@ -123,6 +127,32 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
|
|||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (m *MainWindow) SetupSystemTrayMenu(appName string, fyneApp fyne.App) {
|
||||||
|
if desk, ok := fyneApp.(desktop.App); ok {
|
||||||
|
menu := fyne.NewMenu(appName,
|
||||||
|
fyne.NewMenuItem("Play/Pause", func() {
|
||||||
|
_ = m.App.Player.PlayPause()
|
||||||
|
}),
|
||||||
|
fyne.NewMenuItem("Previous", func() {
|
||||||
|
_ = m.App.Player.SeekBackOrPrevious()
|
||||||
|
}),
|
||||||
|
fyne.NewMenuItem("Next", func() {
|
||||||
|
_ = m.App.Player.SeekNext()
|
||||||
|
}),
|
||||||
|
fyne.NewMenuItemSeparator(),
|
||||||
|
fyne.NewMenuItem("Show", m.Window.Show),
|
||||||
|
fyne.NewMenuItem("Hide", m.Window.Hide),
|
||||||
|
)
|
||||||
|
desk.SetSystemTrayMenu(menu)
|
||||||
|
desk.SetSystemTrayIcon(res.ResAppicon250Png)
|
||||||
|
m.haveSystemTray = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MainWindow) HaveSystemTray() bool {
|
||||||
|
return m.haveSystemTray
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MainWindow) ShowNewVersionDialog(appName, versionTag string) {
|
func (m *MainWindow) ShowNewVersionDialog(appName, versionTag string) {
|
||||||
contentStr := fmt.Sprintf("A new version of %s (%s) is available",
|
contentStr := fmt.Sprintf("A new version of %s (%s) is available",
|
||||||
appName, versionTag)
|
appName, versionTag)
|
||||||
|
|||||||
Reference in New Issue
Block a user