Merge pull request #110 from dweymouth/feature/checkforlatest
Automatically check for latest version
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
- [#21](https://github.com/dweymouth/supersonic/issues/21) Add ability to reorder tracks within a playlist (via context menu)
|
- [#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
|
- [#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)
|
- [#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
|
### 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
|
- [#90](https://github.com/dweymouth/supersonic/issues/90) Wrong covers get loaded for albums if server has different IDs for album and cover art
|
||||||
|
|||||||
+33
-26
@@ -10,16 +10,12 @@ import (
|
|||||||
"supersonic/backend/util"
|
"supersonic/backend/util"
|
||||||
"supersonic/player"
|
"supersonic/player"
|
||||||
"supersonic/sharedutil"
|
"supersonic/sharedutil"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/20after4/configdir"
|
"github.com/20after4/configdir"
|
||||||
"github.com/zalando/go-keyring"
|
"github.com/zalando/go-keyring"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
AppName = "supersonic"
|
|
||||||
configFile = "config.toml"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNoServers = errors.New("no servers set up")
|
ErrNoServers = errors.New("no servers set up")
|
||||||
)
|
)
|
||||||
@@ -31,21 +27,32 @@ type App struct {
|
|||||||
LibraryManager *LibraryManager
|
LibraryManager *LibraryManager
|
||||||
PlaybackManager *PlaybackManager
|
PlaybackManager *PlaybackManager
|
||||||
Player *player.Player
|
Player *player.Player
|
||||||
|
UpdateChecker UpdateChecker
|
||||||
|
|
||||||
bgrndCtx context.Context
|
appName string
|
||||||
cancel context.CancelFunc
|
appVersionTag string
|
||||||
|
configFile string
|
||||||
|
bgrndCtx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
func StartupApp() (*App, error) {
|
func (a *App) VersionTag() string {
|
||||||
a := &App{}
|
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())
|
a.bgrndCtx, a.cancel = context.WithCancel(context.Background())
|
||||||
|
|
||||||
log.Printf("Starting %s...", AppName)
|
log.Printf("Starting %s...", appName)
|
||||||
log.Printf("Using config dir: %s", configdir.LocalConfig(AppName))
|
log.Printf("Using config dir: %s", configdir.LocalConfig(appName))
|
||||||
log.Printf("Using cache dir: %s", configdir.LocalCache(AppName))
|
log.Printf("Using cache dir: %s", configdir.LocalCache(appName))
|
||||||
|
|
||||||
a.readConfig()
|
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 {
|
if err := a.initMPV(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -62,10 +69,10 @@ func StartupApp() (*App, error) {
|
|||||||
PreampGain: a.Config.ReplayGain.PreampGainDB,
|
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.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.Player, &a.Config.Scrobbling)
|
||||||
a.LibraryManager = NewLibraryManager(a.ServerManager)
|
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.LibraryManager.PreCacheCoverFn = func(coverID string) {
|
||||||
_, _ = a.ImageManager.GetAlbumThumbnail(coverID)
|
_, _ = a.ImageManager.GetAlbumThumbnail(coverID)
|
||||||
}
|
}
|
||||||
@@ -74,23 +81,23 @@ func StartupApp() (*App, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) readConfig() {
|
func (a *App) readConfig() {
|
||||||
configdir.MakePath(configdir.LocalConfig(AppName))
|
configdir.MakePath(configdir.LocalConfig(a.appName))
|
||||||
cfgPath := configPath()
|
cfgPath := a.configPath()
|
||||||
cfg, err := ReadConfigFile(cfgPath)
|
cfg, err := ReadConfigFile(cfgPath, a.appVersionTag)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error reading app config file: %v", err)
|
log.Printf("Error reading app config file: %v", err)
|
||||||
cfg = DefaultConfig()
|
cfg = DefaultConfig(a.appVersionTag)
|
||||||
if _, err := os.Stat(cfgPath); err == nil {
|
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)
|
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
|
a.Config = cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) initMPV() error {
|
func (a *App) initMPV() error {
|
||||||
p := player.NewWithClientName(AppName)
|
p := player.NewWithClientName(a.appName)
|
||||||
c := a.Config.LocalPlayback
|
c := a.Config.LocalPlayback
|
||||||
c.InMemoryCacheSizeMB = clamp(c.InMemoryCacheSizeMB, 10, 500)
|
c.InMemoryCacheSizeMB = clamp(c.InMemoryCacheSizeMB, 10, 500)
|
||||||
if err := p.Init(c.AudioExclusive, c.InMemoryCacheSizeMB); err != nil {
|
if err := p.Init(c.AudioExclusive, c.InMemoryCacheSizeMB); err != nil {
|
||||||
@@ -100,12 +107,12 @@ func (a *App) initMPV() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) LoginToDefaultServer() error {
|
func (a *App) LoginToDefaultServer(string) error {
|
||||||
serverCfg := a.Config.GetDefaultServer()
|
serverCfg := a.Config.GetDefaultServer()
|
||||||
if serverCfg == nil {
|
if serverCfg == nil {
|
||||||
return ErrNoServers
|
return ErrNoServers
|
||||||
}
|
}
|
||||||
pass, err := keyring.Get(AppName, serverCfg.ID.String())
|
pass, err := keyring.Get(a.appName, serverCfg.ID.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error reading keyring credentials: %v", err)
|
return fmt.Errorf("error reading keyring credentials: %v", err)
|
||||||
}
|
}
|
||||||
@@ -117,11 +124,11 @@ func (a *App) Shutdown() {
|
|||||||
a.Config.LocalPlayback.Volume = a.Player.GetVolume()
|
a.Config.LocalPlayback.Volume = a.Player.GetVolume()
|
||||||
a.cancel()
|
a.cancel()
|
||||||
a.Player.Destroy()
|
a.Player.Destroy()
|
||||||
a.Config.WriteConfigFile(configPath())
|
a.Config.WriteConfigFile(a.configPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
func configPath() string {
|
func (a *App) configPath() string {
|
||||||
return path.Join(configdir.LocalConfig(AppName), configFile)
|
return path.Join(configdir.LocalConfig(a.appName), a.configFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func clamp(i, min, max int) int {
|
func clamp(i, min, max int) int {
|
||||||
|
|||||||
+9
-7
@@ -17,8 +17,9 @@ type ServerConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppConfig struct {
|
type AppConfig struct {
|
||||||
WindowWidth int
|
WindowWidth int
|
||||||
WindowHeight int
|
WindowHeight int
|
||||||
|
LastCheckedVersion string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlbumPageConfig struct {
|
type AlbumPageConfig struct {
|
||||||
@@ -79,11 +80,12 @@ type Config struct {
|
|||||||
ReplayGain ReplayGainConfig
|
ReplayGain ReplayGainConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultConfig() *Config {
|
func DefaultConfig(appVersionTag string) *Config {
|
||||||
return &Config{
|
return &Config{
|
||||||
Application: AppConfig{
|
Application: AppConfig{
|
||||||
WindowWidth: 1000,
|
WindowWidth: 1000,
|
||||||
WindowHeight: 800,
|
WindowHeight: 800,
|
||||||
|
LastCheckedVersion: appVersionTag,
|
||||||
},
|
},
|
||||||
AlbumPage: AlbumPageConfig{
|
AlbumPage: AlbumPageConfig{
|
||||||
TracklistColumns: []string{"Artist", "Time", "Plays", "Favorite"},
|
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)
|
f, err := os.Open(filepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
|
|
||||||
c := DefaultConfig()
|
c := DefaultConfig(appVersionTag)
|
||||||
if err := toml.NewDecoder(f).Decode(c); err != nil {
|
if err := toml.NewDecoder(f).Decode(c); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,14 +14,15 @@ type ServerManager struct {
|
|||||||
ServerID uuid.UUID
|
ServerID uuid.UUID
|
||||||
Server *subsonic.Client
|
Server *subsonic.Client
|
||||||
|
|
||||||
|
appName string
|
||||||
onServerConnected []func()
|
onServerConnected []func()
|
||||||
onLogout []func()
|
onLogout []func()
|
||||||
}
|
}
|
||||||
|
|
||||||
var ErrUnreachable = errors.New("server is unreachable")
|
var ErrUnreachable = errors.New("server is unreachable")
|
||||||
|
|
||||||
func NewServerManager() *ServerManager {
|
func NewServerManager(appName string) *ServerManager {
|
||||||
return &ServerManager{}
|
return &ServerManager{appName: appName}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) error {
|
func (s *ServerManager) ConnectToServer(conf *ServerConfig, password string) error {
|
||||||
@@ -75,7 +76,7 @@ func (s *ServerManager) testConnectionAndCreateClient(hostname, username, passwo
|
|||||||
|
|
||||||
func (s *ServerManager) Logout() {
|
func (s *ServerManager) Logout() {
|
||||||
if s.Server != nil {
|
if s.Server != nil {
|
||||||
keyring.Delete(AppName, s.ServerID.String())
|
keyring.Delete(s.appName, s.ServerID.String())
|
||||||
for _, cb := range s.onLogout {
|
for _, cb := range s.onLogout {
|
||||||
cb()
|
cb()
|
||||||
}
|
}
|
||||||
@@ -93,9 +94,9 @@ func (s *ServerManager) OnLogout(cb func()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *ServerManager) GetServerPassword(server *ServerConfig) (string, error) {
|
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 {
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package backend
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UpdateChecker struct {
|
||||||
|
OnUpdatedVersionFound func()
|
||||||
|
|
||||||
|
versionTagFound string
|
||||||
|
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():
|
||||||
|
t.Stop()
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
u.checkForUpdate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
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.CheckLatestVersionTag()
|
||||||
|
if t != "" && t != *u.lastCheckedTag {
|
||||||
|
u.versionTagFound = t
|
||||||
|
if u.OnUpdatedVersionFound != nil {
|
||||||
|
u.OnUpdatedVersionFound()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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())
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
url := resp.Request.URL.String()
|
||||||
|
url = strings.TrimSuffix(url, "/")
|
||||||
|
idx := strings.LastIndex(url, "/")
|
||||||
|
if idx >= len(url)-1 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return url[idx+1:]
|
||||||
|
}
|
||||||
@@ -2,7 +2,6 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"path"
|
|
||||||
"supersonic/backend"
|
"supersonic/backend"
|
||||||
"supersonic/ui"
|
"supersonic/ui"
|
||||||
"supersonic/ui/theme"
|
"supersonic/ui/theme"
|
||||||
@@ -10,22 +9,19 @@ import (
|
|||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/app"
|
"fyne.io/fyne/v2/app"
|
||||||
"github.com/20after4/configdir"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
appname = "supersonic"
|
appname = "supersonic"
|
||||||
displayName = "Supersonic"
|
displayName = "Supersonic"
|
||||||
appVersion = "0.0.1-alpha2"
|
appVersion = "0.0.1-alpha2"
|
||||||
configFile = "config.toml"
|
appVersionTag = "v" + appVersion
|
||||||
|
configFile = "config.toml"
|
||||||
|
latestReleaseURL = "https://github.com/dweymouth/supersonic/releases/latest"
|
||||||
)
|
)
|
||||||
|
|
||||||
func configPath() string {
|
|
||||||
return path.Join(configdir.LocalConfig(appname), configFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
myApp, err := backend.StartupApp()
|
myApp, err := backend.StartupApp(appname, appVersionTag, configFile, latestReleaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("fatal startup error: %v", err.Error())
|
log.Fatalf("fatal startup error: %v", err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,15 +25,16 @@ type ReloadFunc func()
|
|||||||
type CurPageFunc func() Route
|
type CurPageFunc func() Route
|
||||||
|
|
||||||
type Controller struct {
|
type Controller struct {
|
||||||
// if not nil, this popup should be hidden when escape is pressed
|
|
||||||
EscapablePopUp *widget.PopUp
|
|
||||||
|
|
||||||
AppVersion string
|
AppVersion string
|
||||||
MainWindow fyne.Window
|
MainWindow fyne.Window
|
||||||
App *backend.App
|
App *backend.App
|
||||||
NavHandler NavigationHandler
|
NavHandler NavigationHandler
|
||||||
CurPageFunc CurPageFunc
|
CurPageFunc CurPageFunc
|
||||||
ReloadFunc ReloadFunc
|
ReloadFunc ReloadFunc
|
||||||
|
|
||||||
|
escapablePopUp *widget.PopUp
|
||||||
|
haveModal bool
|
||||||
|
runOnModalClosed func()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Controller) NavigateTo(route Route) {
|
func (m *Controller) NavigateTo(route Route) {
|
||||||
@@ -41,7 +42,26 @@ func (m *Controller) NavigateTo(route Route) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *Controller) ClosePopUpOnEscape(pop *widget.PopUp) {
|
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) {
|
func (m *Controller) ShowPopUpImage(img image.Image) {
|
||||||
@@ -110,6 +130,7 @@ func (m *Controller) PromptForFirstServer() {
|
|||||||
if m.testConnectionAndUpdateDialogText(d) {
|
if m.testConnectionAndUpdateDialogText(d) {
|
||||||
// connection is good
|
// connection is good
|
||||||
pop.Hide()
|
pop.Hide()
|
||||||
|
m.doModalClosed()
|
||||||
server := m.App.Config.AddServer(d.Nickname, d.Host, d.Username, d.LegacyAuth)
|
server := m.App.Config.AddServer(d.Nickname, d.Host, d.Username, d.LegacyAuth)
|
||||||
if err := m.App.ServerManager.SetServerPassword(server, d.Password); err != nil {
|
if err := m.App.ServerManager.SetServerPassword(server, d.Password); err != nil {
|
||||||
log.Printf("error setting keyring credentials: %v", err)
|
log.Printf("error setting keyring credentials: %v", err)
|
||||||
@@ -120,6 +141,7 @@ func (m *Controller) PromptForFirstServer() {
|
|||||||
d.EnableSubmit()
|
d.EnableSubmit()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
m.haveModal = true
|
||||||
pop.Show()
|
pop.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +166,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
|
|||||||
dlg.OnCanceled = pop.Hide
|
dlg.OnCanceled = pop.Hide
|
||||||
dlg.OnSubmit = func(playlistChoice int, newPlaylistName string) {
|
dlg.OnSubmit = func(playlistChoice int, newPlaylistName string) {
|
||||||
pop.Hide()
|
pop.Hide()
|
||||||
|
m.doModalClosed()
|
||||||
if playlistChoice < 0 {
|
if playlistChoice < 0 {
|
||||||
m.App.ServerManager.Server.CreatePlaylistWithTracks(
|
m.App.ServerManager.Server.CreatePlaylistWithTracks(
|
||||||
trackIDs, map[string]string{"name": newPlaylistName})
|
trackIDs, map[string]string{"name": newPlaylistName})
|
||||||
@@ -152,6 +175,7 @@ func (m *Controller) DoAddTracksToPlaylistWorkflow(trackIDs []string) {
|
|||||||
pls[playlistChoice].ID, trackIDs, nil /*tracksToRemove*/)
|
pls[playlistChoice].ID, trackIDs, nil /*tracksToRemove*/)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
m.haveModal = true
|
||||||
pop.Show()
|
pop.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +183,10 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *subsonic.Playlist) {
|
|||||||
dlg := dialogs.NewEditPlaylistDialog(playlist)
|
dlg := dialogs.NewEditPlaylistDialog(playlist)
|
||||||
pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas())
|
pop := widget.NewModalPopUp(dlg, m.MainWindow.Canvas())
|
||||||
m.ClosePopUpOnEscape(pop)
|
m.ClosePopUpOnEscape(pop)
|
||||||
dlg.OnCanceled = pop.Hide
|
dlg.OnCanceled = func() {
|
||||||
|
pop.Hide()
|
||||||
|
m.doModalClosed()
|
||||||
|
}
|
||||||
dlg.OnDeletePlaylist = func() {
|
dlg.OnDeletePlaylist = func() {
|
||||||
pop.Hide()
|
pop.Hide()
|
||||||
dialog.ShowCustomConfirm("Confirm Delete Playlist", "OK", "Cancel", layout.NewSpacer(), /*custom content*/
|
dialog.ShowCustomConfirm("Confirm Delete Playlist", "OK", "Cancel", layout.NewSpacer(), /*custom content*/
|
||||||
@@ -167,6 +194,7 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *subsonic.Playlist) {
|
|||||||
if !ok {
|
if !ok {
|
||||||
pop.Show()
|
pop.Show()
|
||||||
} else {
|
} else {
|
||||||
|
m.doModalClosed()
|
||||||
go func() {
|
go func() {
|
||||||
if err := m.App.ServerManager.Server.DeletePlaylist(playlist.ID); err != nil {
|
if err := m.App.ServerManager.Server.DeletePlaylist(playlist.ID); err != nil {
|
||||||
log.Printf("error deleting playlist: %s", err.Error())
|
log.Printf("error deleting playlist: %s", err.Error())
|
||||||
@@ -180,6 +208,7 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *subsonic.Playlist) {
|
|||||||
}
|
}
|
||||||
dlg.OnUpdateMetadata = func() {
|
dlg.OnUpdateMetadata = func() {
|
||||||
pop.Hide()
|
pop.Hide()
|
||||||
|
m.doModalClosed()
|
||||||
go func() {
|
go func() {
|
||||||
err := m.App.ServerManager.Server.UpdatePlaylist(playlist.ID, map[string]string{
|
err := m.App.ServerManager.Server.UpdatePlaylist(playlist.ID, map[string]string{
|
||||||
"name": dlg.Name,
|
"name": dlg.Name,
|
||||||
@@ -194,6 +223,7 @@ func (m *Controller) DoEditPlaylistWorkflow(playlist *subsonic.Playlist) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
m.haveModal = true
|
||||||
pop.Show()
|
pop.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -208,6 +238,7 @@ func (c *Controller) DoConnectToServerWorkflow(server *backend.ServerConfig) {
|
|||||||
dlg.SetOnClosed(func() {
|
dlg.SetOnClosed(func() {
|
||||||
c.PromptForLoginAndConnect()
|
c.PromptForLoginAndConnect()
|
||||||
})
|
})
|
||||||
|
c.haveModal = true
|
||||||
dlg.Show()
|
dlg.Show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -230,6 +261,7 @@ func (m *Controller) PromptForLoginAndConnect() {
|
|||||||
} else {
|
} else {
|
||||||
pop.Hide()
|
pop.Hide()
|
||||||
m.trySetPasswordAndConnectToServer(server, password)
|
m.trySetPasswordAndConnectToServer(server, password)
|
||||||
|
m.doModalClosed()
|
||||||
}
|
}
|
||||||
d.EnableSubmit()
|
d.EnableSubmit()
|
||||||
}()
|
}()
|
||||||
@@ -249,20 +281,26 @@ func (m *Controller) PromptForLoginAndConnect() {
|
|||||||
server.Username = editD.Username
|
server.Username = editD.Username
|
||||||
server.LegacyAuth = editD.LegacyAuth
|
server.LegacyAuth = editD.LegacyAuth
|
||||||
m.trySetPasswordAndConnectToServer(server, editD.Password)
|
m.trySetPasswordAndConnectToServer(server, editD.Password)
|
||||||
|
m.doModalClosed()
|
||||||
}
|
}
|
||||||
d.EnableSubmit()
|
d.EnableSubmit()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
editPop.Show()
|
editPop.Show()
|
||||||
}
|
}
|
||||||
|
m.haveModal = true
|
||||||
pop.Show()
|
pop.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Controller) ShowAboutDialog() {
|
func (c *Controller) ShowAboutDialog() {
|
||||||
dlg := dialogs.NewAboutDialog(c.AppVersion)
|
dlg := dialogs.NewAboutDialog(c.AppVersion)
|
||||||
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
|
pop := widget.NewModalPopUp(dlg, c.MainWindow.Canvas())
|
||||||
dlg.OnDismiss = pop.Hide
|
dlg.OnDismiss = func() {
|
||||||
|
pop.Hide()
|
||||||
|
c.doModalClosed()
|
||||||
|
}
|
||||||
c.ClosePopUpOnEscape(pop)
|
c.ClosePopUpOnEscape(pop)
|
||||||
|
c.haveModal = true
|
||||||
pop.Show()
|
pop.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -299,3 +337,11 @@ func (c *Controller) testConnectionAndUpdateDialogText(dlg *dialogs.AddEditServe
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Controller) doModalClosed() {
|
||||||
|
c.haveModal = false
|
||||||
|
if c.runOnModalClosed != nil {
|
||||||
|
c.runOnModalClosed()
|
||||||
|
c.runOnModalClosed = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+37
-4
@@ -10,7 +10,9 @@ import (
|
|||||||
|
|
||||||
"fyne.io/fyne/v2"
|
"fyne.io/fyne/v2"
|
||||||
"fyne.io/fyne/v2/container"
|
"fyne.io/fyne/v2/container"
|
||||||
|
"fyne.io/fyne/v2/dialog"
|
||||||
"fyne.io/fyne/v2/driver/desktop"
|
"fyne.io/fyne/v2/driver/desktop"
|
||||||
|
"fyne.io/fyne/v2/widget"
|
||||||
"github.com/dweymouth/go-subsonic/subsonic"
|
"github.com/dweymouth/go-subsonic/subsonic"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -79,6 +81,14 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
|
|||||||
app.ServerManager.OnServerConnected(func() {
|
app.ServerManager.OnServerConnected(func() {
|
||||||
m.BrowsingPane.EnableNavigationButtons()
|
m.BrowsingPane.EnableNavigationButtons()
|
||||||
m.Router.NavigateTo(HomePage)
|
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() {
|
app.ServerManager.OnLogout(func() {
|
||||||
m.BrowsingPane.DisableNavigationButtons()
|
m.BrowsingPane.DisableNavigationButtons()
|
||||||
@@ -87,6 +97,17 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
|
|||||||
m.Controller.PromptForLoginAndConnect()
|
m.Controller.PromptForLoginAndConnect()
|
||||||
})
|
})
|
||||||
m.BrowsingPane.AddSettingsMenuItem("Log Out", app.ServerManager.Logout)
|
m.BrowsingPane.AddSettingsMenuItem("Log Out", app.ServerManager.Logout)
|
||||||
|
m.BrowsingPane.AddSettingsMenuItem("Check for Updates", func() {
|
||||||
|
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.BrowsingPane.AddSettingsMenuItem("About...", m.Controller.ShowAboutDialog)
|
||||||
m.addNavigationButtons()
|
m.addNavigationButtons()
|
||||||
m.BrowsingPane.DisableNavigationButtons()
|
m.BrowsingPane.DisableNavigationButtons()
|
||||||
@@ -94,6 +115,21 @@ func NewMainWindow(fyneApp fyne.App, appName, appVersion string, app *backend.Ap
|
|||||||
return m
|
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
|
||||||
|
}, m.Window)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func (m *MainWindow) addNavigationButtons() {
|
func (m *MainWindow) addNavigationButtons() {
|
||||||
m.BrowsingPane.AddNavigationButton(res.ResHeadphonesInvertPng, func() {
|
m.BrowsingPane.AddNavigationButton(res.ResHeadphonesInvertPng, func() {
|
||||||
m.Router.NavigateTo(controller.NowPlayingRoute())
|
m.Router.NavigateTo(controller.NowPlayingRoute())
|
||||||
@@ -153,10 +189,7 @@ func (m *MainWindow) addShortcuts() {
|
|||||||
m.Canvas().SetOnTypedKey(func(e *fyne.KeyEvent) {
|
m.Canvas().SetOnTypedKey(func(e *fyne.KeyEvent) {
|
||||||
switch e.Name {
|
switch e.Name {
|
||||||
case fyne.KeyEscape:
|
case fyne.KeyEscape:
|
||||||
if m.Controller.EscapablePopUp != nil {
|
m.Controller.CloseEscapablePopUp()
|
||||||
m.Controller.EscapablePopUp.Hide()
|
|
||||||
m.Controller.EscapablePopUp = nil
|
|
||||||
}
|
|
||||||
case fyne.KeySpace:
|
case fyne.KeySpace:
|
||||||
m.App.Player.PlayPause()
|
m.App.Player.PlayPause()
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user