From 2d7e6336902040071a5a2161931f7bf5067ff453 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Mon, 27 Mar 2023 19:49:16 -0700 Subject: [PATCH 1/8] add util func to get latest version tag --- backend/app.go | 5 +++-- backend/util/util.go | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/backend/app.go b/backend/app.go index 77a92fc..f88f9f9 100644 --- a/backend/app.go +++ b/backend/app.go @@ -16,8 +16,9 @@ import ( ) const ( - AppName = "supersonic" - configFile = "config.toml" + AppName = "supersonic" + configFile = "config.toml" + LatestReleaseURL = "https://github.com/dweymouth/supersonic/releases/latest" ) var ( diff --git a/backend/util/util.go b/backend/util/util.go index 43fe28b..d7a24d7 100644 --- a/backend/util/util.go +++ b/backend/util/util.go @@ -2,8 +2,11 @@ package util import ( "io" + "log" "math/rand" + "net/http" "os" + "strings" "time" ) @@ -37,3 +40,18 @@ func CopyFile(srcPath, dstPath string) error { _, err = io.Copy(fout, fin) return err } + +func LatestVersionTag(latestReleaseURL string) string { + resp, err := http.Head(latestReleaseURL) + if err != nil { + log.Printf("failed to check for newest version: %s", err.Error()) + return "" + } + url := resp.Request.URL.String() + url = strings.TrimSuffix(url, "/") + idx := strings.LastIndex(url, "/") + if idx >= len(url)-1 { + return "" + } + return url[idx+1:] +} From cca12d7327bcb11e38f894fcfbcfbdcc421ec96c Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 29 Mar 2023 18:14:46 -0700 Subject: [PATCH 2/8] create UpdateChecker background task + constants refactoring --- backend/app.go | 51 ++++++++++++++++------------- backend/config.go | 16 +++++---- backend/servermanager.go | 11 ++++--- backend/updatechecker.go | 71 ++++++++++++++++++++++++++++++++++++++++ backend/util/util.go | 18 ---------- main.go | 12 ++++--- 6 files changed, 121 insertions(+), 58 deletions(-) create mode 100644 backend/updatechecker.go diff --git a/backend/app.go b/backend/app.go index f88f9f9..d1cddea 100644 --- a/backend/app.go +++ b/backend/app.go @@ -10,15 +10,14 @@ import ( "supersonic/backend/util" "supersonic/player" "supersonic/sharedutil" + "time" "github.com/20after4/configdir" "github.com/zalando/go-keyring" ) const ( - AppName = "supersonic" - configFile = "config.toml" - LatestReleaseURL = "https://github.com/dweymouth/supersonic/releases/latest" + configFile = "config.toml" ) var ( @@ -32,21 +31,27 @@ type App struct { LibraryManager *LibraryManager PlaybackManager *PlaybackManager Player *player.Player + UpdateChecker UpdateChecker - bgrndCtx context.Context - cancel context.CancelFunc + appName string + appVersionTag string + bgrndCtx context.Context + cancel context.CancelFunc } -func StartupApp() (*App, error) { - a := &App{} +func StartupApp(appName, appVersionTag, latestReleaseURL string) (*App, error) { + a := &App{appName: appName, appVersionTag: appVersionTag} a.bgrndCtx, a.cancel = context.WithCancel(context.Background()) - log.Printf("Starting %s...", AppName) - log.Printf("Using config dir: %s", configdir.LocalConfig(AppName)) - log.Printf("Using cache dir: %s", configdir.LocalCache(AppName)) + log.Printf("Starting %s...", appName) + log.Printf("Using config dir: %s", configdir.LocalConfig(appName)) + log.Printf("Using cache dir: %s", configdir.LocalCache(appName)) a.readConfig() + a.UpdateChecker = NewUpdateChecker(appVersionTag, latestReleaseURL, &a.Config.Application.LastCheckedVersion) + a.UpdateChecker.Start(a.bgrndCtx, 24*time.Hour) + if err := a.initMPV(); err != nil { return nil, err } @@ -63,10 +68,10 @@ func StartupApp() (*App, error) { PreampGain: a.Config.ReplayGain.PreampGainDB, }) - a.ServerManager = NewServerManager() + a.ServerManager = NewServerManager(appName) a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player, &a.Config.Scrobbling) a.LibraryManager = NewLibraryManager(a.ServerManager) - a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(AppName)) + a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(a.appName)) a.LibraryManager.PreCacheCoverFn = func(coverID string) { _, _ = a.ImageManager.GetAlbumThumbnail(coverID) } @@ -75,23 +80,23 @@ func StartupApp() (*App, error) { } func (a *App) readConfig() { - configdir.MakePath(configdir.LocalConfig(AppName)) - cfgPath := configPath() - cfg, err := ReadConfigFile(cfgPath) + configdir.MakePath(configdir.LocalConfig(a.appName)) + cfgPath := a.configPath() + cfg, err := ReadConfigFile(cfgPath, a.appVersionTag) if err != nil { log.Printf("Error reading app config file: %v", err) - cfg = DefaultConfig() + cfg = DefaultConfig(a.appVersionTag) if _, err := os.Stat(cfgPath); err == nil { backupCfgName := fmt.Sprintf("%s.bak", configFile) log.Printf("Config file may be malformed: copying to %s", backupCfgName) - _ = util.CopyFile(cfgPath, path.Join(configdir.LocalConfig(AppName), backupCfgName)) + _ = util.CopyFile(cfgPath, path.Join(configdir.LocalConfig(a.appName), backupCfgName)) } } a.Config = cfg } func (a *App) initMPV() error { - p := player.NewWithClientName(AppName) + p := player.NewWithClientName(a.appName) c := a.Config.LocalPlayback c.InMemoryCacheSizeMB = clamp(c.InMemoryCacheSizeMB, 10, 500) if err := p.Init(c.AudioExclusive, c.InMemoryCacheSizeMB); err != nil { @@ -101,12 +106,12 @@ func (a *App) initMPV() error { return nil } -func (a *App) LoginToDefaultServer() error { +func (a *App) LoginToDefaultServer(string) error { serverCfg := a.Config.GetDefaultServer() if serverCfg == nil { return ErrNoServers } - pass, err := keyring.Get(AppName, serverCfg.ID.String()) + pass, err := keyring.Get(a.appName, serverCfg.ID.String()) if err != nil { return fmt.Errorf("error reading keyring credentials: %v", err) } @@ -118,11 +123,11 @@ func (a *App) Shutdown() { a.Config.LocalPlayback.Volume = a.Player.GetVolume() a.cancel() a.Player.Destroy() - a.Config.WriteConfigFile(configPath()) + a.Config.WriteConfigFile(a.configPath()) } -func configPath() string { - return path.Join(configdir.LocalConfig(AppName), configFile) +func (a *App) configPath() string { + return path.Join(configdir.LocalConfig(a.appName), configFile) } func clamp(i, min, max int) int { diff --git a/backend/config.go b/backend/config.go index da2455f..c9a29ea 100644 --- a/backend/config.go +++ b/backend/config.go @@ -17,8 +17,9 @@ type ServerConfig struct { } type AppConfig struct { - WindowWidth int - WindowHeight int + WindowWidth int + WindowHeight int + LastCheckedVersion string } type AlbumPageConfig struct { @@ -79,11 +80,12 @@ type Config struct { ReplayGain ReplayGainConfig } -func DefaultConfig() *Config { +func DefaultConfig(appVersionTag string) *Config { return &Config{ Application: AppConfig{ - WindowWidth: 1000, - WindowHeight: 800, + WindowWidth: 1000, + WindowHeight: 800, + LastCheckedVersion: appVersionTag, }, AlbumPage: AlbumPageConfig{ TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite"}, @@ -123,14 +125,14 @@ func DefaultConfig() *Config { } } -func ReadConfigFile(filepath string) (*Config, error) { +func ReadConfigFile(filepath, appVersionTag string) (*Config, error) { f, err := os.Open(filepath) if err != nil { return nil, err } defer f.Close() - c := DefaultConfig() + c := DefaultConfig(appVersionTag) if err := toml.NewDecoder(f).Decode(c); err != nil { return nil, err } diff --git a/backend/servermanager.go b/backend/servermanager.go index bce9938..216207b 100644 --- a/backend/servermanager.go +++ b/backend/servermanager.go @@ -14,14 +14,15 @@ type ServerManager struct { ServerID uuid.UUID Server *subsonic.Client + appName string onServerConnected []func() onLogout []func() } var ErrUnreachable = errors.New("server is unreachable") -func NewServerManager() *ServerManager { - return &ServerManager{} +func NewServerManager(appName string) *ServerManager { + return &ServerManager{appName: appName} } func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) error { @@ -75,7 +76,7 @@ func (s *ServerManager) testConnectionAndCreateClient(hostname, username, passwo func (s *ServerManager) Logout() { if s.Server != nil { - keyring.Delete(AppName, s.ServerID.String()) + keyring.Delete(s.appName, s.ServerID.String()) for _, cb := range s.onLogout { cb() } @@ -93,9 +94,9 @@ func (s *ServerManager) OnLogout(cb func()) { } func (s *ServerManager) GetServerPassword(server *ServerConfig) (string, error) { - return keyring.Get(AppName, server.ID.String()) + return keyring.Get(s.appName, server.ID.String()) } func (s *ServerManager) SetServerPassword(server *ServerConfig, password string) error { - return keyring.Set(AppName, server.ID.String(), password) + return keyring.Set(s.appName, server.ID.String(), password) } diff --git a/backend/updatechecker.go b/backend/updatechecker.go new file mode 100644 index 0000000..6b23870 --- /dev/null +++ b/backend/updatechecker.go @@ -0,0 +1,71 @@ +package backend + +import ( + "context" + "log" + "net/http" + "strings" + "time" +) + +type UpdateChecker struct { + OnUpdatedVersionFound func(releaseURL string) + + foundUpdate bool + latestReleaseURL string + appVersionTag string + lastCheckedTag *string +} + +func NewUpdateChecker(appVersionTag, latestReleaseURL string, lastCheckedTag *string) UpdateChecker { + return UpdateChecker{ + appVersionTag: appVersionTag, + latestReleaseURL: latestReleaseURL, + lastCheckedTag: lastCheckedTag, + } +} + +func (u *UpdateChecker) Start(ctx context.Context, interval time.Duration) { + go func() { + u.checkForUpdate() // check once at startup + t := time.NewTicker(interval) + for { + select { + case <-ctx.Done(): + return + case <-t.C: + u.checkForUpdate() + } + } + }() +} + +func (u *UpdateChecker) UpdateAvailable() bool { + return u.foundUpdate +} + +func (u *UpdateChecker) checkForUpdate() { + t := u.latestVersionTag() + if t != "" && t != *u.lastCheckedTag { + u.foundUpdate = true + *u.lastCheckedTag = t + if u.OnUpdatedVersionFound != nil { + u.OnUpdatedVersionFound(u.latestReleaseURL) + } + } +} + +func (u *UpdateChecker) latestVersionTag() string { + resp, err := http.Head(u.latestReleaseURL) + if err != nil { + log.Printf("failed to check for newest version: %s", err.Error()) + return "" + } + url := resp.Request.URL.String() + url = strings.TrimSuffix(url, "/") + idx := strings.LastIndex(url, "/") + if idx >= len(url)-1 { + return "" + } + return url[idx+1:] +} diff --git a/backend/util/util.go b/backend/util/util.go index d7a24d7..43fe28b 100644 --- a/backend/util/util.go +++ b/backend/util/util.go @@ -2,11 +2,8 @@ package util import ( "io" - "log" "math/rand" - "net/http" "os" - "strings" "time" ) @@ -40,18 +37,3 @@ func CopyFile(srcPath, dstPath string) error { _, err = io.Copy(fout, fin) return err } - -func LatestVersionTag(latestReleaseURL string) string { - resp, err := http.Head(latestReleaseURL) - if err != nil { - log.Printf("failed to check for newest version: %s", err.Error()) - return "" - } - url := resp.Request.URL.String() - url = strings.TrimSuffix(url, "/") - idx := strings.LastIndex(url, "/") - if idx >= len(url)-1 { - return "" - } - return url[idx+1:] -} diff --git a/main.go b/main.go index cd30819..3ecff0e 100644 --- a/main.go +++ b/main.go @@ -14,10 +14,12 @@ import ( ) const ( - appname = "supersonic" - displayName = "Supersonic" - appVersion = "0.0.1-alpha2" - configFile = "config.toml" + appname = "supersonic" + displayName = "Supersonic" + appVersion = "0.0.1-alpha2" + appVersionTag = "v" + appVersion + configFile = "config.toml" + latestReleaseURL = "https://github.com/dweymouth/supersonic/releases/latest" ) func configPath() string { @@ -25,7 +27,7 @@ func configPath() string { } func main() { - myApp, err := backend.StartupApp() + myApp, err := backend.StartupApp(appname, appVersionTag, latestReleaseURL) if err != nil { log.Fatalf("fatal startup error: %v", err.Error()) } From 8c52097536ac1b3de52d5527c3d1a6f2e561d70d Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 29 Mar 2023 18:17:26 -0700 Subject: [PATCH 3/8] one more quick refactor --- backend/app.go | 13 +++++-------- main.go | 8 +------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/backend/app.go b/backend/app.go index d1cddea..7168d05 100644 --- a/backend/app.go +++ b/backend/app.go @@ -16,10 +16,6 @@ import ( "github.com/zalando/go-keyring" ) -const ( - configFile = "config.toml" -) - var ( ErrNoServers = errors.New("no servers set up") ) @@ -35,12 +31,13 @@ type App struct { appName string appVersionTag string + configFile string bgrndCtx context.Context cancel context.CancelFunc } -func StartupApp(appName, appVersionTag, latestReleaseURL string) (*App, error) { - a := &App{appName: appName, appVersionTag: appVersionTag} +func StartupApp(appName, appVersionTag, configFile, latestReleaseURL string) (*App, error) { + a := &App{appName: appName, appVersionTag: appVersionTag, configFile: configFile} a.bgrndCtx, a.cancel = context.WithCancel(context.Background()) log.Printf("Starting %s...", appName) @@ -87,7 +84,7 @@ func (a *App) readConfig() { log.Printf("Error reading app config file: %v", err) cfg = DefaultConfig(a.appVersionTag) if _, err := os.Stat(cfgPath); err == nil { - backupCfgName := fmt.Sprintf("%s.bak", configFile) + backupCfgName := fmt.Sprintf("%s.bak", a.configFile) log.Printf("Config file may be malformed: copying to %s", backupCfgName) _ = util.CopyFile(cfgPath, path.Join(configdir.LocalConfig(a.appName), backupCfgName)) } @@ -127,7 +124,7 @@ func (a *App) Shutdown() { } func (a *App) configPath() string { - return path.Join(configdir.LocalConfig(a.appName), configFile) + return path.Join(configdir.LocalConfig(a.appName), a.configFile) } func clamp(i, min, max int) int { diff --git a/main.go b/main.go index 3ecff0e..45a8532 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "log" - "path" "supersonic/backend" "supersonic/ui" "supersonic/ui/theme" @@ -10,7 +9,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/app" - "github.com/20after4/configdir" ) const ( @@ -22,12 +20,8 @@ const ( latestReleaseURL = "https://github.com/dweymouth/supersonic/releases/latest" ) -func configPath() string { - return path.Join(configdir.LocalConfig(appname), configFile) -} - func main() { - myApp, err := backend.StartupApp(appname, appVersionTag, latestReleaseURL) + myApp, err := backend.StartupApp(appname, appVersionTag, configFile, latestReleaseURL) if err != nil { log.Fatalf("fatal startup error: %v", err.Error()) } From 8d13575000472f3b2b67d00c915612cef411b5c7 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Wed, 29 Mar 2023 18:19:23 -0700 Subject: [PATCH 4/8] forgot to stop ticket --- backend/updatechecker.go | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/updatechecker.go b/backend/updatechecker.go index 6b23870..44d1bed 100644 --- a/backend/updatechecker.go +++ b/backend/updatechecker.go @@ -32,6 +32,7 @@ func (u *UpdateChecker) Start(ctx context.Context, interval time.Duration) { for { select { case <-ctx.Done(): + t.Stop() return case <-t.C: u.checkForUpdate() From d59352cd4df27b0c30ad9d628f229e91f4131deb Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 30 Mar 2023 09:25:37 -0700 Subject: [PATCH 5/8] show dialog on later version found --- backend/app.go | 4 +++ backend/updatechecker.go | 19 +++++++----- ui/controller/controller.go | 58 +++++++++++++++++++++++++++++++++---- ui/mainwindow.go | 32 +++++++++++++++++--- 4 files changed, 96 insertions(+), 17 deletions(-) diff --git a/backend/app.go b/backend/app.go index 7168d05..d26bd00 100644 --- a/backend/app.go +++ b/backend/app.go @@ -36,6 +36,10 @@ type App struct { cancel context.CancelFunc } +func (a *App) VersionTag() string { + return a.appVersionTag +} + func StartupApp(appName, appVersionTag, configFile, latestReleaseURL string) (*App, error) { a := &App{appName: appName, appVersionTag: appVersionTag, configFile: configFile} a.bgrndCtx, a.cancel = context.WithCancel(context.Background()) diff --git a/backend/updatechecker.go b/backend/updatechecker.go index 44d1bed..7b71925 100644 --- a/backend/updatechecker.go +++ b/backend/updatechecker.go @@ -4,14 +4,15 @@ import ( "context" "log" "net/http" + "net/url" "strings" "time" ) type UpdateChecker struct { - OnUpdatedVersionFound func(releaseURL string) + OnUpdatedVersionFound func() - foundUpdate bool + versionTagFound string latestReleaseURL string appVersionTag string lastCheckedTag *string @@ -41,17 +42,21 @@ func (u *UpdateChecker) Start(ctx context.Context, interval time.Duration) { }() } -func (u *UpdateChecker) UpdateAvailable() bool { - return u.foundUpdate +func (u *UpdateChecker) VersionTagFound() string { + return u.versionTagFound +} + +func (u *UpdateChecker) LatestReleaseURL() *url.URL { + url, _ := url.Parse(u.latestReleaseURL) + return url } func (u *UpdateChecker) checkForUpdate() { t := u.latestVersionTag() if t != "" && t != *u.lastCheckedTag { - u.foundUpdate = true - *u.lastCheckedTag = t + u.versionTagFound = t if u.OnUpdatedVersionFound != nil { - u.OnUpdatedVersionFound(u.latestReleaseURL) + u.OnUpdatedVersionFound() } } } diff --git a/ui/controller/controller.go b/ui/controller/controller.go index ee2dd0e..b2d71a7 100644 --- a/ui/controller/controller.go +++ b/ui/controller/controller.go @@ -25,15 +25,16 @@ type ReloadFunc func() type CurPageFunc func() Route type Controller struct { - // if not nil, this popup should be hidden when escape is pressed - EscapablePopUp *widget.PopUp - AppVersion string MainWindow fyne.Window App *backend.App NavHandler NavigationHandler CurPageFunc CurPageFunc ReloadFunc ReloadFunc + + escapablePopUp *widget.PopUp + haveModal bool + runOnModalClosed func() } func (m *Controller) NavigateTo(route Route) { @@ -41,7 +42,26 @@ func (m *Controller) NavigateTo(route Route) { } func (m *Controller) ClosePopUpOnEscape(pop *widget.PopUp) { - m.EscapablePopUp = pop + m.escapablePopUp = pop +} + +func (m *Controller) CloseEscapablePopUp() { + if m.escapablePopUp != nil { + m.escapablePopUp.Hide() + m.escapablePopUp = nil + m.doModalClosed() + } +} + +// If there is currently no modal popup managed by the Controller visible, +// then run f (which should create and show a modal dialog) immediately. +// else run f when the current modal dialog workflow has ended. +func (m *Controller) QueueShowModalFunc(f func()) { + if m.haveModal { + m.runOnModalClosed = f + } else { + f() + } } func (m *Controller) ShowPopUpImage(img image.Image) { @@ -110,6 +130,7 @@ func (m *Controller) PromptForFirstServer() { if m.testConnectionAndUpdateDialogText(d) { // connection is good pop.Hide() + m.doModalClosed() server := m.App.Config.AddServer(d.Nickname, d.Host, d.Username, d.LegacyAuth) if err := m.App.ServerManager.SetServerPassword(server, d.Password); err != nil { log.Printf("error setting keyring credentials: %v", err) @@ -120,6 +141,7 @@ func (m *Controller) PromptForFirstServer() { d.EnableSubmit() }() } + m.haveModal = true pop.Show() } @@ -144,6 +166,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { dlg.OnCanceled = pop.Hide dlg.OnSubmit = func(playlistChoice int, newPlaylistName string) { pop.Hide() + m.doModalClosed() if playlistChoice < 0 { m.App.ServerManager.Server.CreatePlaylistWithTracks( trackIDs, map[string]string{"name": newPlaylistName}) @@ -152,6 +175,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) { pls[playlistChoice].ID, trackIDs, nil /*tracksToRemove*/) } } + m.haveModal = true pop.Show() } @@ -159,7 +183,10 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *subsonic.Playlist) { dlg := dialogs.NewEditPlaylistDialog(playlist) pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas()) m.ClosePopUpOnEscape(pop) - dlg.OnCanceled = pop.Hide + dlg.OnCanceled = func() { + pop.Hide() + m.doModalClosed() + } dlg.OnDeletePlaylist = func() { pop.Hide() dialog.ShowCustomConfirm("Confirm Delete Playlist", "OK", "Cancel", layout.NewSpacer(), /*custom content*/ @@ -167,6 +194,7 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *subsonic.Playlist) { if !ok { pop.Show() } else { + m.doModalClosed() go func() { if err := m.App.ServerManager.Server.DeletePlaylist(playlist.ID); err != nil { log.Printf("error deleting playlist: %s", err.Error()) @@ -180,6 +208,7 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *subsonic.Playlist) { } dlg.OnUpdateMetadata = func() { pop.Hide() + m.doModalClosed() go func() { err := m.App.ServerManager.Server.UpdatePlaylist(playlist.ID, map[string]string{ "name": dlg.Name, @@ -194,6 +223,7 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *subsonic.Playlist) { } }() } + m.haveModal = true pop.Show() } @@ -208,6 +238,7 @@ func (c *Controller) DoConnectToServerWorkflow(server *backend.ServerConfig) { dlg.SetOnClosed(func() { c.PromptForLoginAndConnect() }) + c.haveModal = true dlg.Show() } } @@ -230,6 +261,7 @@ func (m *Controller) PromptForLoginAndConnect() { } else { pop.Hide() m.trySetPasswordAndConnectToServer(server, password) + m.doModalClosed() } d.EnableSubmit() }() @@ -249,20 +281,26 @@ func (m *Controller) PromptForLoginAndConnect() { server.Username = editD.Username server.LegacyAuth = editD.LegacyAuth m.trySetPasswordAndConnectToServer(server, editD.Password) + m.doModalClosed() } d.EnableSubmit() }() } editPop.Show() } + m.haveModal = true pop.Show() } func (c *Controller) ShowAboutDialog() { dlg := dialogs.NewAboutDialog(c.AppVersion) pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas()) - dlg.OnDismiss = pop.Hide + dlg.OnDismiss = func() { + pop.Hide() + c.doModalClosed() + } c.ClosePopUpOnEscape(pop) + c.haveModal = true pop.Show() } @@ -299,3 +337,11 @@ func (c *Controller) testConnectionAndUpdateDialogText(dlg *dialogs.AddEditServe } return true } + +func (c *Controller) doModalClosed() { + c.haveModal = false + if c.runOnModalClosed != nil { + c.runOnModalClosed() + c.runOnModalClosed = nil + } +} diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 2dd9eb8..9fecb5e 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -2,6 +2,7 @@ package ui import ( "fmt" + "log" "supersonic/backend" "supersonic/res" "supersonic/ui/browsing" @@ -10,7 +11,9 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/driver/desktop" + "fyne.io/fyne/v2/widget" "github.com/dweymouth/go-subsonic/subsonic" ) @@ -79,6 +82,14 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap app.ServerManager.OnServerConnected(func() { m.BrowsingPane.EnableNavigationButtons() m.Router.NavigateTo(HomePage) + // check if found new version on startup + if t := app.UpdateChecker.VersionTagFound(); t != "" && t != app.Config.Application.LastCheckedVersion { + m.ShowNewVersionDialog(appName, t) + } + // register callback for the ongoing periodic update check + m.App.UpdateChecker.OnUpdatedVersionFound = func() { + m.ShowNewVersionDialog(appName, m.App.UpdateChecker.VersionTagFound()) + } }) app.ServerManager.OnLogout(func() { m.BrowsingPane.DisableNavigationButtons() @@ -94,6 +105,22 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap return m } +func (m *MainWindow) ShowNewVersionDialog(appName, versionTag string) { + contentStr := fmt.Sprintf("A new version of %s (%s) is available", + appName, versionTag) + m.Controller.QueueShowModalFunc(func() { + dialog.ShowCustomConfirm("A new version is available", + "Go to release page", "Skip this version", + widget.NewLabel(contentStr), func(show bool) { + if show { + fyne.CurrentApp().OpenURL(m.App.UpdateChecker.LatestReleaseURL()) + } + m.App.Config.Application.LastCheckedVersion = versionTag + log.Printf("reset version: %s", m.App.Config.Application.LastCheckedVersion) + }, m.Window) + }) +} + func (m *MainWindow) addNavigationButtons() { m.BrowsingPane.AddNavigationButton(res.ResHeadphonesInvertPng, func() { m.Router.NavigateTo(controller.NowPlayingRoute()) @@ -153,10 +180,7 @@ func (m *MainWindow) addShortcuts() { m.Canvas().SetOnTypedKey(func(e *fyne.KeyEvent) { switch e.Name { case fyne.KeyEscape: - if m.Controller.EscapablePopUp != nil { - m.Controller.EscapablePopUp.Hide() - m.Controller.EscapablePopUp = nil - } + m.Controller.CloseEscapablePopUp() case fyne.KeySpace: m.App.Player.PlayPause() } From 06cd393b8c7ae70a822244fca690d3bf3824581c Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 30 Mar 2023 19:04:04 -0700 Subject: [PATCH 6/8] add settings menu item to check for updates immediately --- backend/updatechecker.go | 4 ++-- ui/mainwindow.go | 11 +++++++++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/backend/updatechecker.go b/backend/updatechecker.go index 7b71925..21e8236 100644 --- a/backend/updatechecker.go +++ b/backend/updatechecker.go @@ -52,7 +52,7 @@ func (u *UpdateChecker) LatestReleaseURL() *url.URL { } func (u *UpdateChecker) checkForUpdate() { - t := u.latestVersionTag() + t := u.CheckLatestVersionTag() if t != "" && t != *u.lastCheckedTag { u.versionTagFound = t if u.OnUpdatedVersionFound != nil { @@ -61,7 +61,7 @@ func (u *UpdateChecker) checkForUpdate() { } } -func (u *UpdateChecker) latestVersionTag() string { +func (u *UpdateChecker) CheckLatestVersionTag() string { resp, err := http.Head(u.latestReleaseURL) if err != nil { log.Printf("failed to check for newest version: %s", err.Error()) diff --git a/ui/mainwindow.go b/ui/mainwindow.go index 9fecb5e..e06520d 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -2,7 +2,6 @@ package ui import ( "fmt" - "log" "supersonic/backend" "supersonic/res" "supersonic/ui/browsing" @@ -98,6 +97,15 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap m.Controller.PromptForLoginAndConnect() }) m.BrowsingPane.AddSettingsMenuItem("Log Out", app.ServerManager.Logout) + m.BrowsingPane.AddSettingsMenuItem("Check for Updates", func() { + if t := app.UpdateChecker.CheckLatestVersionTag(); t != "" && t != app.VersionTag() { + m.ShowNewVersionDialog(appName, t) + } else { + dialog.ShowInformation("No new version found", + "You are running the latest version of "+appName, + m.Window) + } + }) m.BrowsingPane.AddSettingsMenuItem("About...", m.Controller.ShowAboutDialog) m.addNavigationButtons() m.BrowsingPane.DisableNavigationButtons() @@ -116,7 +124,6 @@ func (m *MainWindow) ShowNewVersionDialog(appName, versionTag string) { fyne.CurrentApp().OpenURL(m.App.UpdateChecker.LatestReleaseURL()) } m.App.Config.Application.LastCheckedVersion = versionTag - log.Printf("reset version: %s", m.App.Config.Application.LastCheckedVersion) }, m.Window) }) } From 729e25715a6a9d865cd0f7821da91c6fe11c6363 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 30 Mar 2023 19:43:24 -0700 Subject: [PATCH 7/8] invoke check for update menu callback in goroutine --- ui/mainwindow.go | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/ui/mainwindow.go b/ui/mainwindow.go index e06520d..aa5bc32 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -98,13 +98,15 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap }) m.BrowsingPane.AddSettingsMenuItem("Log Out", app.ServerManager.Logout) m.BrowsingPane.AddSettingsMenuItem("Check for Updates", func() { - if t := app.UpdateChecker.CheckLatestVersionTag(); t != "" && t != app.VersionTag() { - m.ShowNewVersionDialog(appName, t) - } else { - dialog.ShowInformation("No new version found", - "You are running the latest version of "+appName, - m.Window) - } + go func() { + if t := app.UpdateChecker.CheckLatestVersionTag(); t != "" && t != app.VersionTag() { + m.ShowNewVersionDialog(appName, t) + } else { + dialog.ShowInformation("No new version found", + "You are running the latest version of "+appName, + m.Window) + } + }() }) m.BrowsingPane.AddSettingsMenuItem("About...", m.Controller.ShowAboutDialog) m.addNavigationButtons() From 0abdd76d3c1d6c4efac190174faf9da3877ec214 Mon Sep 17 00:00:00 2001 From: Drew Weymouth Date: Thu, 30 Mar 2023 19:46:29 -0700 Subject: [PATCH 8/8] update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f0f7af6..23c8725 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - [#21](https://github.com/dweymouth/supersonic/issues/21) Add ability to reorder tracks within a playlist (via context menu) - [#99](https://github.com/dweymouth/supersonic/issues/99) Add Year (asc + desc) sort orders to album page - [#102](https://github.com/dweymouth/supersonic/issues/102) Add ReplayGain support (requires files to be tagged on server and transcoding to preserve tags) +- [#106](https://github.com/dweymouth/supersonic/issues/106) Automatically check for updates ### Fixed - [#90](https://github.com/dweymouth/supersonic/issues/90) Wrong covers get loaded for albums if server has different IDs for album and cover art