diff --git a/backend/app.go b/backend/app.go index 24ec7234..4a3cca4a 100644 --- a/backend/app.go +++ b/backend/app.go @@ -7,6 +7,7 @@ import ( "log" "os" "path" + "path/filepath" "reflect" "slices" "time" @@ -23,10 +24,13 @@ import ( ) const ( + configFile = "config.toml" + portableDir = "supersonic_portable" sessionDir = "session" sessionLockFile = ".lock" sessionActivateFile = ".activate" savedQueueFile = "saved_queue.json" + themesDir = "themes" ) var ( @@ -49,7 +53,9 @@ type App struct { appName string appVersionTag string - configFile string + configDir string + cacheDir string + portableMode bool isFirstLaunch bool // set by config file reader bgrndCtx context.Context @@ -62,8 +68,22 @@ func (a *App) VersionTag() string { return a.appVersionTag } -func StartupApp(appName, displayAppName, appVersionTag, configFile, latestReleaseURL string) (*App, error) { - sessionPath := configdir.LocalConfig(appName, sessionDir) +func StartupApp(appName, displayAppName, appVersionTag, latestReleaseURL string) (*App, error) { + var confDir, cacheDir string + portableMode := false + if p := checkPortablePath(); p != "" { + confDir = path.Join(p, "config") + cacheDir = path.Join(p, "cache") + portableMode = true + } else { + confDir = configdir.LocalConfig(appName) + cacheDir = configdir.LocalCache(appName) + } + // ensure config and cache dirs exist + configdir.MakePath(confDir) + configdir.MakePath(cacheDir) + + sessionPath := path.Join(confDir, sessionDir) if _, err := os.Stat(path.Join(sessionPath, sessionLockFile)); err == nil { log.Println("Another instance is running. Reactivating it...") reactivateFile := path.Join(sessionPath, sessionActivateFile) @@ -80,10 +100,16 @@ func StartupApp(appName, displayAppName, appVersionTag, configFile, latestReleas } log.Printf("Starting %s...", appName) - log.Printf("Using config dir: %s", configdir.LocalConfig(appName)) - log.Printf("Using cache dir: %s", configdir.LocalCache(appName)) - - a := &App{appName: appName, appVersionTag: appVersionTag, configFile: configFile} + log.Printf("Using config dir: %s", confDir) + log.Printf("Using cache dir: %s", cacheDir) + + a := &App{ + appName: appName, + appVersionTag: appVersionTag, + configDir: confDir, + cacheDir: cacheDir, + portableMode: portableMode, + } a.bgrndCtx, a.cancel = context.WithCancel(context.Background()) a.readConfig() a.startConfigWriter(a.bgrndCtx) @@ -109,9 +135,9 @@ func StartupApp(appName, displayAppName, appVersionTag, configFile, latestReleas return nil, err } - a.ServerManager = NewServerManager(appName, a.Config) + a.ServerManager = NewServerManager(appName, a.Config, !portableMode /*use keyring*/) a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.LocalPlayer, &a.Config.Scrobbling, &a.Config.Transcoding) - a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, configdir.LocalCache(a.appName)) + a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, cacheDir) a.Config.Application.MaxImageCacheSizeMB = clamp(a.Config.Application.MaxImageCacheSizeMB, 1, 500) a.ImageManager.SetMaxOnDiskCacheSizeBytes(int64(a.Config.Application.MaxImageCacheSizeMB) * 1_048_576) a.ServerManager.SetPrefetchAlbumCoverCallback(func(coverID string) { @@ -132,9 +158,26 @@ func (a *App) IsFirstLaunch() bool { return a.isFirstLaunch } +func (a *App) IsPortableMode() bool { + return a.portableMode +} + +func (a *App) ThemesDir() string { + return filepath.Join(a.configDir, themesDir) +} + +func checkPortablePath() string { + if p, err := os.Executable(); err == nil { + pdirPath := path.Join(filepath.Dir(p), portableDir) + if s, err := os.Stat(pdirPath); err == nil && s.IsDir() { + return pdirPath + } + } + return "" +} + func (a *App) readConfig() { - configdir.MakePath(configdir.LocalConfig(a.appName)) - cfgPath := a.configPath() + cfgPath := a.configFilePath() var cfgExists bool if _, err := os.Stat(cfgPath); err == nil { cfgExists = true @@ -145,9 +188,9 @@ func (a *App) readConfig() { log.Printf("Error reading app config file: %v", err) cfg = DefaultConfig(a.appVersionTag) if cfgExists { - backupCfgName := fmt.Sprintf("%s.bak", a.configFile) + backupCfgName := fmt.Sprintf("%s.bak", configFile) log.Printf("Config file may be malformed: copying to %s", backupCfgName) - _ = util.CopyFile(cfgPath, path.Join(configdir.LocalConfig(a.appName), backupCfgName)) + _ = util.CopyFile(cfgPath, path.Join(a.configDir, backupCfgName)) } } a.Config = cfg @@ -183,7 +226,7 @@ func (a *App) startConfigWriter(ctx context.Context) { return case <-tick.C: if !reflect.DeepEqual(&a.lastWrittenCfg, a.Config) { - a.Config.WriteConfigFile(a.configPath()) + a.Config.WriteConfigFile(a.configFilePath()) a.lastWrittenCfg = *a.Config } } @@ -297,7 +340,7 @@ func (a *App) LoginToDefaultServer(string) error { } func (a *App) DeleteServerCacheDir(serverID uuid.UUID) error { - path := path.Join(configdir.LocalCache(a.appName), serverID.String()) + path := path.Join(a.cacheDir, serverID.String()) log.Printf("Deleting server cache dir: %s", path) return os.RemoveAll(path) } @@ -312,18 +355,18 @@ func (a *App) Shutdown() { queueServer = qs } } - SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager, configdir.LocalConfig(a.appName, savedQueueFile), queueServer) + SavePlayQueue(a.ServerManager.ServerID.String(), a.PlaybackManager, path.Join(a.configDir, savedQueueFile), queueServer) } a.PlaybackManager.Stop() // will trigger scrobble check a.Config.LocalPlayback.Volume = a.LocalPlayer.GetVolume() a.cancel() a.LocalPlayer.Destroy() - a.Config.WriteConfigFile(a.configPath()) - os.RemoveAll(configdir.LocalConfig(a.appName, sessionDir)) + a.Config.WriteConfigFile(a.configFilePath()) + os.RemoveAll(path.Join(a.configDir, sessionDir)) } func (a *App) LoadSavedPlayQueue() error { - queueFilePath := configdir.LocalConfig(a.appName, savedQueueFile) + queueFilePath := path.Join(a.configDir, savedQueueFile) queue, err := LoadPlayQueue(queueFilePath, a.ServerManager, a.Config.Application.SaveQueueToServer) if err != nil { return err @@ -346,12 +389,12 @@ func (a *App) LoadSavedPlayQueue() error { } func (a *App) SaveConfigFile() { - a.Config.WriteConfigFile(a.configPath()) + a.Config.WriteConfigFile(a.configFilePath()) a.lastWrittenCfg = *a.Config } -func (a *App) configPath() string { - return path.Join(configdir.LocalConfig(a.appName), a.configFile) +func (a *App) configFilePath() string { + return path.Join(a.configDir, configFile) } func clamp(i, min, max int) int { diff --git a/backend/servermanager.go b/backend/servermanager.go index 82ca80ed..e70141c4 100644 --- a/backend/servermanager.go +++ b/backend/servermanager.go @@ -22,6 +22,7 @@ type ServerManager struct { ServerID uuid.UUID Server mediaprovider.MediaProvider + useKeyring bool prefetchCoverCB func(string) appName string config *Config @@ -31,8 +32,8 @@ type ServerManager struct { var ErrUnreachable = errors.New("server is unreachable") -func NewServerManager(appName string, config *Config) *ServerManager { - return &ServerManager{appName: appName, config: config} +func NewServerManager(appName string, config *Config, useKeyring bool) *ServerManager { + return &ServerManager{appName: appName, config: config, useKeyring: useKeyring} } func (s *ServerManager) SetPrefetchAlbumCoverCallback(cb func(string)) { @@ -137,7 +138,9 @@ func (s *ServerManager) Logout(deletePassword bool) { } func (s *ServerManager) deleteServerPassword(serverID uuid.UUID) { - keyring.Delete(s.appName, s.ServerID.String()) + if s.useKeyring { + keyring.Delete(s.appName, s.ServerID.String()) + } } // Sets a callback that is invoked when a server is connected to. @@ -151,11 +154,17 @@ func (s *ServerManager) OnLogout(cb func()) { } func (s *ServerManager) GetServerPassword(serverID uuid.UUID) (string, error) { - return keyring.Get(s.appName, serverID.String()) + if s.useKeyring { + return keyring.Get(s.appName, serverID.String()) + } + return "", errors.New("keyring not enabled") } func (s *ServerManager) SetServerPassword(server *ServerConfig, password string) error { - return keyring.Set(s.appName, server.ID.String(), password) + if s.useKeyring { + return keyring.Set(s.appName, server.ID.String(), password) + } + return errors.New("keyring not available") } func (s *ServerManager) connect(connection ServerConnection, password string) (mediaprovider.Server, error) { diff --git a/main.go b/main.go index ac82b0a3..212d0aea 100644 --- a/main.go +++ b/main.go @@ -14,10 +14,8 @@ import ( "fyne.io/fyne/v2/app" ) -const configFile = "config.toml" - func main() { - myApp, err := backend.StartupApp(res.AppName, res.DisplayName, res.AppVersionTag, configFile, res.LatestReleaseURL) + myApp, err := backend.StartupApp(res.AppName, res.DisplayName, res.AppVersionTag, res.LatestReleaseURL) if err != nil { log.Fatalf("fatal startup error: %v", err.Error()) } diff --git a/ui/mainwindow.go b/ui/mainwindow.go index afd27b12..51b4fcee 100644 --- a/ui/mainwindow.go +++ b/ui/mainwindow.go @@ -7,7 +7,6 @@ import ( "strings" "time" - "github.com/20after4/configdir" "github.com/dweymouth/supersonic/backend" "github.com/dweymouth/supersonic/backend/mediaprovider" "github.com/dweymouth/supersonic/res" @@ -60,7 +59,7 @@ func NewMainWindow(fyneApp fyne.App, appName, displayAppName, appVersion string, m := MainWindow{ App: app, Window: fyneApp.NewWindow(displayAppName), - theme: theme.NewMyTheme(&app.Config.Theme, configdir.LocalConfig(appName, "themes")), + theme: theme.NewMyTheme(&app.Config.Theme, app.ThemesDir()), } m.theme.NormalFont = app.Config.Application.FontNormalTTF