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() }