Update Database
Database is more usable now. All pages have been updated to use the database, and share an application configuration. Page Stack has been removed, no more pages hanging around in memory. Replaced with a 'Router' which uses an Enum to determine which page should be displayed, destroys the old page and initializes the new page all in one go. No more push, pop, rollback, etc. No more holding instances of pages. Logger has been updated with proper timestamps and sorting now works as expected."
This commit is contained in:
+104
-407
@@ -1,9 +1,7 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
@@ -12,6 +10,7 @@ import (
|
||||
"github.com/go-gl/gl/v3.3-core/gl"
|
||||
"github.com/veandco/go-sdl2/sdl"
|
||||
"github.com/veandco/go-sdl2/ttf"
|
||||
"gorm.io/gorm"
|
||||
"rhemawavetech.com/rhemawave/internal/db"
|
||||
"rhemawavetech.com/rhemawave/internal/fontManager"
|
||||
"rhemawavetech.com/rhemawave/internal/graphics"
|
||||
@@ -19,7 +18,6 @@ import (
|
||||
"rhemawavetech.com/rhemawave/internal/license"
|
||||
"rhemawavetech.com/rhemawave/internal/logger"
|
||||
"rhemawavetech.com/rhemawave/internal/mixer"
|
||||
"rhemawavetech.com/rhemawave/internal/network"
|
||||
"rhemawavetech.com/rhemawave/internal/page"
|
||||
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
||||
)
|
||||
@@ -40,6 +38,9 @@ type Application struct {
|
||||
pageStack []page.Page
|
||||
pendingAction func()
|
||||
running bool
|
||||
DB *gorm.DB
|
||||
Config atomic.Pointer[db.AppConfig]
|
||||
PageRouter *page.Router
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -47,174 +48,111 @@ type Application struct {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func Run() {
|
||||
logger.Info("Starting application")
|
||||
var err error
|
||||
var homeDir string
|
||||
var dataDir string
|
||||
var DB *gorm.DB
|
||||
var initialConfig db.AppConfig
|
||||
var result *gorm.DB
|
||||
|
||||
appDataDir, err := initializeApplicationDirectory()
|
||||
logger.Info("Starting Application")
|
||||
logger.Info("Finding $HOME")
|
||||
|
||||
homeDir, err = os.UserHomeDir()
|
||||
if err != nil {
|
||||
logger.Fatal("Failed to initialize application directory: %v", err)
|
||||
logger.Error("unable to fetch user $HOME directory: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("App directory: %s", appDataDir)
|
||||
logger.AddFile(filepath.Join(appDataDir, "log.txt"))
|
||||
|
||||
if err := initializeDatabase(appDataDir); err != nil {
|
||||
logger.Fatal("Database initialization failed: %v", err)
|
||||
logger.Info("Constructing DataDir Path")
|
||||
dataDir = filepath.Join(homeDir, ".local", "share", "rhemawave")
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
logger.Error("unable to create data directory %s %v", dataDir, err)
|
||||
return
|
||||
}
|
||||
|
||||
app := &Application{}
|
||||
|
||||
app.initializeSDLAndOpenGL()
|
||||
defer app.cleanup()
|
||||
|
||||
app.initializeManagers()
|
||||
app.initializeAudio()
|
||||
app.validateLicenseKey()
|
||||
app.startupPageStack()
|
||||
app.runEventLoop()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Initialization Functions
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func initializeApplicationDirectory() (string, error) {
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get user home directory: %w", err)
|
||||
}
|
||||
|
||||
appDataDir := filepath.Join(homeDir, ".local", "share", "rhemawave")
|
||||
|
||||
if err := os.MkdirAll(appDataDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("could not create application data directory %s: %w", appDataDir, err)
|
||||
}
|
||||
|
||||
testFile := filepath.Join(appDataDir, ".access_test")
|
||||
testFile := filepath.Join(dataDir, ".access_test")
|
||||
f, err := os.Create(testFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("application data directory %s is not writable: %w", appDataDir, err)
|
||||
logger.Error("data %s is not writeable: %v", dataDir, err)
|
||||
return
|
||||
}
|
||||
f.Close()
|
||||
os.Remove(testFile)
|
||||
|
||||
return appDataDir, nil
|
||||
}
|
||||
logger.AddFile(filepath.Join(dataDir, "log.txt"))
|
||||
|
||||
func initializeDatabase(appDataDir string) error {
|
||||
dbPath := filepath.Join(appDataDir, "data.db")
|
||||
if err := db.InitDatabase(dbPath); err != nil {
|
||||
return fmt.Errorf("failed to initialize database: %w", err)
|
||||
}
|
||||
logger.AddDatabase(db.DB)
|
||||
return nil
|
||||
}
|
||||
logger.Info("Connecting to database")
|
||||
|
||||
type progressWriter struct {
|
||||
w io.Writer
|
||||
written *atomic.Int64
|
||||
}
|
||||
|
||||
func (pw *progressWriter) Write(p []byte) (int, error) {
|
||||
n, err := pw.w.Write(p)
|
||||
pw.written.Add(int64(n))
|
||||
return n, err
|
||||
}
|
||||
|
||||
// ensureAudioFile checks whether the selected audio file exists. If it does,
|
||||
// it returns nil. Otherwise it starts a background download and returns a
|
||||
// DownloadProgressPage that tracks the download so it can be pushed onto the
|
||||
// page stack.
|
||||
func ensureAudioFile() *page.DownloadProgressPage {
|
||||
assetsDir, err := db.GetConfigValue("assets_dir")
|
||||
DB, err = db.Connect(filepath.Join(dataDir, "database.sqlite"))
|
||||
if err != nil {
|
||||
logger.Error("Failed to get assets_dir: %v", err)
|
||||
return nil
|
||||
}
|
||||
selectedAudio, err := db.GetConfigValue("selected_audio")
|
||||
if err != nil || selectedAudio == "" {
|
||||
logger.Warning("No selected_audio configured")
|
||||
return nil
|
||||
logger.Error("application failed to connect to the database: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
audioPath := filepath.Join(assetsDir, selectedAudio)
|
||||
if _, err := os.Stat(audioPath); err == nil {
|
||||
logger.Info("Audio file found: %s", audioPath)
|
||||
return nil
|
||||
if err = db.Migrate(DB); err != nil {
|
||||
logger.Error("application failed to perform database migrations: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Audio file not found at %s, downloading from %s", audioPath, audioFileURL)
|
||||
|
||||
totalBytes := &atomic.Int64{}
|
||||
bytesWritten := &atomic.Int64{}
|
||||
done := &atomic.Bool{}
|
||||
hasError := &atomic.Bool{}
|
||||
|
||||
downloadPage := &page.DownloadProgressPage{
|
||||
TotalBytes: totalBytes,
|
||||
BytesWritten: bytesWritten,
|
||||
Done: done,
|
||||
HasError: hasError,
|
||||
// Populate the initial Config cache OR create defaults
|
||||
result = DB.First(&initialConfig)
|
||||
if result.Error != nil {
|
||||
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
|
||||
logger.Info("First run detected: Creating default configuration...")
|
||||
initialConfig = db.AppConfig{
|
||||
DataDir: dataDir,
|
||||
AssetsDir: dataDir,
|
||||
SystemLevel: "BASIC",
|
||||
SelectedAudioDevice: "default",
|
||||
InitialFontSize: 128,
|
||||
StandardFontSize: 32,
|
||||
LinePattern: "fibonacci",
|
||||
LastVolume: 128,
|
||||
AudioEnabled: true,
|
||||
}
|
||||
if err := DB.Create(&initialConfig).Error; err != nil {
|
||||
logger.Error("failed to insert default config into database: %v", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
logger.Error("error querying database for initial config: %v", result.Error)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer done.Store(true)
|
||||
logger.AddDatabase(DB)
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(audioPath), 0755); err != nil {
|
||||
logger.Error("Failed to create audio directory: %v", err)
|
||||
hasError.Store(true)
|
||||
return
|
||||
}
|
||||
app := &Application{
|
||||
DB: DB,
|
||||
}
|
||||
|
||||
resp, err := http.Get(audioFileURL)
|
||||
if err != nil {
|
||||
logger.Error("Failed to download audio file: %v", err)
|
||||
hasError.Store(true)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
app.refreshCache()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Error("Audio download returned status %d", resp.StatusCode)
|
||||
hasError.Store(true)
|
||||
return
|
||||
}
|
||||
w, h := app.initializeSDLAndOpenGL()
|
||||
defer app.cleanup()
|
||||
|
||||
if resp.ContentLength > 0 {
|
||||
totalBytes.Store(resp.ContentLength)
|
||||
}
|
||||
|
||||
tmp := audioPath + ".tmp"
|
||||
f, err := os.Create(tmp)
|
||||
if err != nil {
|
||||
logger.Error("Failed to create audio file: %v", err)
|
||||
hasError.Store(true)
|
||||
return
|
||||
}
|
||||
|
||||
pw := &progressWriter{w: f, written: bytesWritten}
|
||||
_, err = io.Copy(pw, resp.Body)
|
||||
f.Close()
|
||||
if err != nil {
|
||||
os.Remove(tmp)
|
||||
logger.Error("Failed to write audio file: %v", err)
|
||||
hasError.Store(true)
|
||||
return
|
||||
}
|
||||
|
||||
if err := os.Rename(tmp, audioPath); err != nil {
|
||||
os.Remove(tmp)
|
||||
logger.Error("Failed to move audio file into place: %v", err)
|
||||
hasError.Store(true)
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("Audio file downloaded successfully (%d bytes)", bytesWritten.Load())
|
||||
}()
|
||||
|
||||
return downloadPage
|
||||
app.initializeManagers()
|
||||
app.initializeAudio()
|
||||
app.PageRouter = page.NewRouter(page.PageCtx{
|
||||
CfgPtr: app.Config.Load(),
|
||||
DbPtr: app.DB,
|
||||
W: w,
|
||||
H: h,
|
||||
RefreshConfig: app.refreshCache,
|
||||
})
|
||||
app.runEventLoop()
|
||||
}
|
||||
|
||||
func (app *Application) initializeSDLAndOpenGL() {
|
||||
func (app *Application) refreshCache() {
|
||||
var freshConfig db.AppConfig
|
||||
if result := app.DB.First(&freshConfig); result.Error != nil {
|
||||
logger.Error("failed to fetch configuration from the database: %v", result.Error)
|
||||
}
|
||||
app.Config.Store(&freshConfig)
|
||||
}
|
||||
|
||||
func (app *Application) initializeSDLAndOpenGL() (W, H int32) {
|
||||
logger.Info("Initializing SDL")
|
||||
if err := sdl.Init(sdl.INIT_VIDEO | sdl.INIT_AUDIO); err != nil {
|
||||
logger.Fatal("SDL init failed: %v", err)
|
||||
@@ -283,42 +221,40 @@ func (app *Application) initializeSDLAndOpenGL() {
|
||||
|
||||
app.initKeybinds()
|
||||
logger.Info("SDL and OpenGL initialization complete")
|
||||
return w, h
|
||||
}
|
||||
|
||||
func (app *Application) initializeManagers() {
|
||||
logger.Info("Initializing ShaderManager")
|
||||
if err := shaderManager.InitShaderManager(); err != nil {
|
||||
shaderDir := filepath.Join(app.Config.Load().AssetsDir, "shaders")
|
||||
logger.Info("Loading shaders from %s", shaderDir)
|
||||
if err := shaderManager.InitShaderManager(shaderDir); err != nil {
|
||||
logger.Fatal("ShaderManager initialization failed: %v", err)
|
||||
}
|
||||
|
||||
logger.Info("Initializing FontManager")
|
||||
if err := fontManager.InitFontManager(); err != nil {
|
||||
if err := fontManager.InitFontManager(app.Config.Load()); err != nil {
|
||||
logger.Fatal("FontManager initialization failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) initializeAudio() {
|
||||
logger.Info("Initializing audio")
|
||||
audioDevice, err := db.GetConfigValue("audio_device_name")
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get audio device name from config: %v", err)
|
||||
}
|
||||
|
||||
if err := mixer.Init(audioDevice); err != nil {
|
||||
logger.Error("Unable to initialize audio device '%s': %v", audioDevice, err)
|
||||
if err := mixer.Init(app.Config.Load(), app.refreshCache); err != nil {
|
||||
logger.Error("Unable to initialize audio device '%s': %v", app.Config.Load().SelectedAudioDevice, err)
|
||||
logger.Error("Audio will be unavailable until a device is configured")
|
||||
} else {
|
||||
logger.Info("Audio initialized with device: '%s'", audioDevice)
|
||||
logger.Info("Audio initialized with device: '%s'", app.Config.Load().SelectedAudioDevice)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) cleanup() {
|
||||
logger.Info("Cleaning up")
|
||||
|
||||
for _, p := range app.pageStack {
|
||||
p.Destroy()
|
||||
if app.PageRouter != nil && app.PageRouter.ActivePage != nil {
|
||||
app.PageRouter.ActivePage.Destroy()
|
||||
}
|
||||
app.pageStack = nil
|
||||
|
||||
graphics.DestroyVirtualFramebuffer()
|
||||
|
||||
@@ -337,8 +273,8 @@ func (app *Application) cleanup() {
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func (app *Application) runEventLoop() {
|
||||
if len(app.pageStack) == 0 {
|
||||
logger.Fatal("No initial page loaded, pageStack is empty")
|
||||
if app.PageRouter == nil || app.PageRouter.ActivePage == nil {
|
||||
logger.Fatal("No initial page loaded, Router is empty")
|
||||
}
|
||||
|
||||
const frameDelay = time.Second / targetFPS
|
||||
@@ -360,7 +296,6 @@ func (app *Application) runEventLoop() {
|
||||
|
||||
app.updateCurrentPage(deltaTime)
|
||||
app.renderFrame()
|
||||
app.applyPendingAction()
|
||||
|
||||
frameTime := time.Since(frameStart)
|
||||
if frameTime < frameDelay {
|
||||
@@ -388,7 +323,7 @@ func (app *Application) handleEvents() {
|
||||
}
|
||||
}
|
||||
|
||||
if currentPage := app.currentPage(); currentPage != nil {
|
||||
if currentPage := app.PageRouter.ActivePage; currentPage != nil {
|
||||
if err := currentPage.HandleEvent(&event); err != nil {
|
||||
logger.Error("Page event handler error: %v", err)
|
||||
}
|
||||
@@ -397,7 +332,7 @@ func (app *Application) handleEvents() {
|
||||
}
|
||||
|
||||
func (app *Application) updateCurrentPage(deltaTime float32) {
|
||||
if currentPage := app.currentPage(); currentPage != nil {
|
||||
if currentPage := app.PageRouter.ActivePage; currentPage != nil {
|
||||
currentPage.Update(deltaTime)
|
||||
}
|
||||
}
|
||||
@@ -407,7 +342,7 @@ func (app *Application) renderFrame() {
|
||||
gl.ClearColor(0.0, 0.0, 0.0, 1.0)
|
||||
gl.Clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT)
|
||||
|
||||
if currentPage := app.currentPage(); currentPage != nil {
|
||||
if currentPage := app.PageRouter.ActivePage; currentPage != nil {
|
||||
currentPage.Render()
|
||||
}
|
||||
|
||||
@@ -421,21 +356,16 @@ func (app *Application) renderFrame() {
|
||||
|
||||
func (app *Application) initKeybinds() {
|
||||
keybinds.Register(sdl.K_F3, sdl.KMOD_NONE, func() {
|
||||
app.PushPage(&page.Settings{})
|
||||
app.PageRouter.NavigateTo(page.SETTINGS)
|
||||
})
|
||||
|
||||
keybinds.Register(sdl.K_F2, sdl.KMOD_NONE, func() {
|
||||
downloadPage := ensureAudioFile()
|
||||
if downloadPage != nil {
|
||||
downloadPage.NextPage = &page.ScrollerPage{}
|
||||
app.PushPage(downloadPage)
|
||||
} else {
|
||||
app.PushPage(&page.ScrollerPage{})
|
||||
}
|
||||
app.PageRouter.NavigateTo(page.SCROLLER)
|
||||
})
|
||||
|
||||
keybinds.Register(sdl.K_F1, sdl.KMOD_NONE, func() {
|
||||
app.UnwindToPage(&page.Welcome{})
|
||||
app.PageRouter.NavigateTo(page.HOME)
|
||||
|
||||
})
|
||||
|
||||
keybinds.Register(sdl.K_q, sdl.KMOD_RCTRL, func() {
|
||||
@@ -443,11 +373,11 @@ func (app *Application) initKeybinds() {
|
||||
})
|
||||
|
||||
keybinds.Register(sdl.K_UP, sdl.KMOD_SHIFT, func() {
|
||||
mixer.SetVolume128(volumeStepSize)
|
||||
mixer.SetVolume128(volumeStepSize, app.Config.Load())
|
||||
})
|
||||
|
||||
keybinds.Register(sdl.K_DOWN, sdl.KMOD_SHIFT, func() {
|
||||
mixer.SetVolume128(-volumeStepSize)
|
||||
mixer.SetVolume128(-volumeStepSize, app.Config.Load())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -455,239 +385,6 @@ func (app *Application) Stop() {
|
||||
app.running = false
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Page Management
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func (app *Application) currentPage() page.Page {
|
||||
if len(app.pageStack) == 0 {
|
||||
return nil
|
||||
}
|
||||
return app.pageStack[len(app.pageStack)-1]
|
||||
}
|
||||
|
||||
func (app *Application) applyPendingAction() {
|
||||
if app.pendingAction != nil {
|
||||
app.pendingAction()
|
||||
app.pendingAction = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) schedulePush(p page.Page) {
|
||||
app.pendingAction = func() {
|
||||
if err := p.Init(app); err != nil {
|
||||
logger.Error("Failed to init page on push: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if topPage := app.currentPage(); topPage != nil {
|
||||
topPage.Destroy()
|
||||
}
|
||||
|
||||
app.pageStack = append(app.pageStack, p)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) scheduleSwitch(p page.Page) {
|
||||
app.pendingAction = func() {
|
||||
if topPage := app.currentPage(); topPage != nil {
|
||||
topPage.Destroy()
|
||||
app.pageStack = app.pageStack[:len(app.pageStack)-1]
|
||||
}
|
||||
|
||||
if err := p.Init(app); err != nil {
|
||||
logger.Error("Failed to init page on switch: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
app.pageStack = append(app.pageStack, p)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) scheduleUnwind(targetPage page.Page) {
|
||||
app.pendingAction = func() {
|
||||
foundIndex := -1
|
||||
for i := len(app.pageStack) - 1; i >= 0; i-- {
|
||||
if app.pageStack[i] == targetPage {
|
||||
foundIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if foundIndex != -1 {
|
||||
for i := len(app.pageStack) - 1; i > foundIndex; i-- {
|
||||
app.pageStack[i].Destroy()
|
||||
}
|
||||
app.pageStack = app.pageStack[:foundIndex+1]
|
||||
} else {
|
||||
if topPage := app.currentPage(); topPage != nil {
|
||||
topPage.Destroy()
|
||||
app.pageStack = app.pageStack[:len(app.pageStack)-1]
|
||||
}
|
||||
|
||||
if err := targetPage.Init(app); err != nil {
|
||||
logger.Error("Failed to init page on unwind: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
app.pageStack = append(app.pageStack, targetPage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) SchedulePop() {
|
||||
app.pendingAction = func() {
|
||||
if len(app.pageStack) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
topPage := app.pageStack[len(app.pageStack)-1]
|
||||
topPage.Destroy()
|
||||
app.pageStack = app.pageStack[:len(app.pageStack)-1]
|
||||
|
||||
if newTop := app.currentPage(); newTop != nil {
|
||||
if err := newTop.Init(app); err != nil {
|
||||
logger.Error("Failed to re-init page after pop: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Public Page Navigation Interface
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func (app *Application) PushPage(p page.Page) {
|
||||
app.schedulePush(p)
|
||||
}
|
||||
|
||||
func (app *Application) SwitchPage(p page.Page) {
|
||||
app.scheduleSwitch(p)
|
||||
}
|
||||
|
||||
func (app *Application) UnwindToPage(p page.Page) {
|
||||
app.scheduleUnwind(p)
|
||||
}
|
||||
|
||||
func (app *Application) GetDrawableSize() (w, h int32) {
|
||||
return graphics.VirtualWidth, graphics.VirtualHeight
|
||||
}
|
||||
|
||||
func (app *Application) PopPage() {
|
||||
app.SchedulePop()
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// License Validation
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func (app *Application) validateLicenseKey() {
|
||||
logger.Info("Validating license key")
|
||||
validator := license.NewValidator(apiURL, apiPath)
|
||||
|
||||
key, err := db.GetConfigValue("license_key")
|
||||
if err != nil {
|
||||
logger.Error("Failed to fetch license key from database: %v", err)
|
||||
}
|
||||
|
||||
email, err := db.GetConfigValue("email_address")
|
||||
if err != nil {
|
||||
logger.Error("Failed to fetch email from database: %v", err)
|
||||
}
|
||||
|
||||
result, err := validator.Validate(key, email)
|
||||
if err != nil {
|
||||
logger.Error("License validation failed: %v", err)
|
||||
app.License = nil
|
||||
return
|
||||
}
|
||||
|
||||
logger.Info("License validation successful")
|
||||
app.License = result
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Startup Page Stack
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func (app *Application) startupPageStack() {
|
||||
app.PushPage(&page.Welcome{})
|
||||
app.applyPendingAction()
|
||||
|
||||
key, _ := db.GetConfigValue("license_key")
|
||||
if key == "" {
|
||||
logger.Info("No license key found, pushing license key page")
|
||||
app.PushPage(&page.LicenseKeyPage{})
|
||||
app.applyPendingAction()
|
||||
} else {
|
||||
// Ensure the license key file exists for the updater script.
|
||||
// Older versions only stored the key in the database.
|
||||
if homeDir, err := os.UserHomeDir(); err == nil {
|
||||
keyPath := filepath.Join(homeDir, ".local", "share", "rhemawave", "license_key")
|
||||
if _, err := os.Stat(keyPath); os.IsNotExist(err) {
|
||||
logger.Info("License key file missing, writing from database")
|
||||
if err := os.MkdirAll(filepath.Dir(keyPath), 0755); err != nil {
|
||||
logger.Error("Failed to create license key directory: %v", err)
|
||||
} else if err := os.WriteFile(keyPath, []byte(key), 0644); err != nil {
|
||||
logger.Error("Failed to write license key file: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
email, _ := db.GetConfigValue("email_address")
|
||||
if email == "" {
|
||||
logger.Info("No email address found, pushing email address page")
|
||||
app.PushPage(&page.EmailAddressPage{})
|
||||
app.applyPendingAction()
|
||||
}
|
||||
|
||||
// TOS pages — must be accepted before anything else.
|
||||
// Pushed last so they sit on top of the stack (shown first).
|
||||
sections, err := db.GetTOSSections()
|
||||
if err != nil {
|
||||
logger.Error("Failed to load TOS sections: %v", err)
|
||||
}
|
||||
// Push in reverse order so section 1 is on top of the stack
|
||||
for i := len(sections) - 1; i >= 0; i-- {
|
||||
s := sections[i]
|
||||
configKey := fmt.Sprintf("tos_section_%d_accepted", s.SectionNumber)
|
||||
accepted, _ := db.GetConfigValue(configKey)
|
||||
if accepted != "true" {
|
||||
app.PushPage(&page.TOSPage{
|
||||
Title: s.Title,
|
||||
Content: s.Content,
|
||||
ConfigKey: configKey,
|
||||
})
|
||||
app.applyPendingAction()
|
||||
}
|
||||
}
|
||||
|
||||
// Internet check — pushed last so it appears first if no internet.
|
||||
// Skip WiFi setup if ethernet is connected (it auto-configures) or if
|
||||
// internet is already reachable through any interface.
|
||||
nm, err := network.New()
|
||||
if err != nil {
|
||||
logger.Warning("Could not initialize network manager: %v", err)
|
||||
} else {
|
||||
status, err := nm.CheckInternetConnection()
|
||||
if err != nil {
|
||||
logger.Warning("Network connectivity check failed: %v", err)
|
||||
} else {
|
||||
switch status.StatusCode {
|
||||
case network.StatusEthernetConnectedInternetUp,
|
||||
network.StatusEthernetConnectedNoInternet,
|
||||
network.StatusWifiConnectedInternetUp:
|
||||
// Internet is up or ethernet is connected and will auto-configure
|
||||
default:
|
||||
logger.Info("No internet connection, pushing WiFi setup page")
|
||||
app.PushPage(&page.WiFiSetupPage{})
|
||||
app.applyPendingAction()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (app *Application) Quit() {
|
||||
logger.Info("User has quit the application")
|
||||
app.running = false
|
||||
|
||||
Reference in New Issue
Block a user