705 lines
19 KiB
Go
705 lines
19 KiB
Go
package application
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/go-gl/gl/v3.3-core/gl"
|
|
"github.com/veandco/go-sdl2/sdl"
|
|
"github.com/veandco/go-sdl2/ttf"
|
|
"rhemawavetech.com/rhemawave/internal/db"
|
|
"rhemawavetech.com/rhemawave/internal/fontManager"
|
|
"rhemawavetech.com/rhemawave/internal/graphics"
|
|
"rhemawavetech.com/rhemawave/internal/keybinds"
|
|
"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"
|
|
)
|
|
|
|
const (
|
|
targetFPS = 60.0
|
|
volumeStepSize = 16
|
|
openGLMajorVersion = 3
|
|
openGLMinorVersion = 3
|
|
apiURL = "http://localhost:8000"
|
|
apiPath = "/api/v1/licenseKey/validate"
|
|
audioFileURL = "https://files.neilclack.com/audio.wav"
|
|
)
|
|
|
|
type Application struct {
|
|
Window *sdl.Window
|
|
License *license.Response
|
|
pageStack []page.Page
|
|
pendingAction func()
|
|
running bool
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Main Entry Point
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func Run() {
|
|
logger.Info("Starting application")
|
|
|
|
appDataDir, err := initializeApplicationDirectory()
|
|
if err != nil {
|
|
logger.Fatal("Failed to initialize application directory: %v", err)
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
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")
|
|
f, err := os.Create(testFile)
|
|
if err != nil {
|
|
return "", fmt.Errorf("application data directory %s is not writable: %w", appDataDir, err)
|
|
}
|
|
f.Close()
|
|
os.Remove(testFile)
|
|
|
|
return appDataDir, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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")
|
|
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
|
|
}
|
|
|
|
audioPath := filepath.Join(assetsDir, selectedAudio)
|
|
if _, err := os.Stat(audioPath); err == nil {
|
|
logger.Info("Audio file found: %s", audioPath)
|
|
return nil
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
go func() {
|
|
defer done.Store(true)
|
|
|
|
if err := os.MkdirAll(filepath.Dir(audioPath), 0755); err != nil {
|
|
logger.Error("Failed to create audio directory: %v", err)
|
|
hasError.Store(true)
|
|
return
|
|
}
|
|
|
|
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()
|
|
|
|
if resp.StatusCode != http.StatusOK {
|
|
logger.Error("Audio download returned status %d", resp.StatusCode)
|
|
hasError.Store(true)
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
func (app *Application) initializeSDLAndOpenGL() {
|
|
logger.Info("Initializing SDL")
|
|
if err := sdl.Init(sdl.INIT_VIDEO | sdl.INIT_AUDIO); err != nil {
|
|
logger.Fatal("SDL init failed: %v", err)
|
|
}
|
|
|
|
logger.Info("Initializing TTF")
|
|
if err := ttf.Init(); err != nil {
|
|
logger.Fatal("TTF init failed: %v", err)
|
|
}
|
|
|
|
sdl.GLSetAttribute(sdl.GL_CONTEXT_MAJOR_VERSION, openGLMajorVersion)
|
|
sdl.GLSetAttribute(sdl.GL_CONTEXT_MINOR_VERSION, openGLMinorVersion)
|
|
sdl.GLSetAttribute(sdl.GL_CONTEXT_PROFILE_MASK, sdl.GL_CONTEXT_PROFILE_CORE)
|
|
sdl.ShowCursor(sdl.DISABLE)
|
|
sdl.SetRelativeMouseMode(true)
|
|
|
|
logger.Info("Querying display mode")
|
|
displayMode, err := sdl.GetDisplayMode(0, 0)
|
|
if err != nil {
|
|
logger.Fatal("Display mode query failed: %v", err)
|
|
}
|
|
if displayMode.W == 0 || displayMode.H == 0 {
|
|
logger.Fatal("Invalid display dimensions: %dx%d", displayMode.W, displayMode.H)
|
|
}
|
|
logger.Info("Display: %dx%d @ %dHz", displayMode.W, displayMode.H, displayMode.RefreshRate)
|
|
|
|
logger.Info("Creating window")
|
|
window, err := sdl.CreateWindow(
|
|
"Rhema Wave",
|
|
sdl.WINDOWPOS_UNDEFINED,
|
|
sdl.WINDOWPOS_UNDEFINED,
|
|
displayMode.W,
|
|
displayMode.H,
|
|
sdl.WINDOW_OPENGL|sdl.WINDOW_FULLSCREEN_DESKTOP|sdl.WINDOW_SHOWN,
|
|
)
|
|
if err != nil {
|
|
logger.Fatal("Window creation failed: %v", err)
|
|
}
|
|
app.Window = window
|
|
|
|
logger.Info("Creating OpenGL context")
|
|
if _, err := app.Window.GLCreateContext(); err != nil {
|
|
logger.Fatal("OpenGL context creation failed: %v", err)
|
|
}
|
|
|
|
logger.Info("Initializing go-gl")
|
|
if err := gl.InitWithProcAddrFunc(sdl.GLGetProcAddress); err != nil {
|
|
logger.Fatal("go-gl initialization failed: %v", err)
|
|
}
|
|
|
|
logger.Info("OpenGL version: %s", gl.GoStr(gl.GetString(gl.VERSION)))
|
|
logger.Info("OpenGL renderer: %s", gl.GoStr(gl.GetString(gl.RENDERER)))
|
|
|
|
if err := sdl.GLSetSwapInterval(1); err != nil {
|
|
logger.Warning("Failed to enable vsync: %v", err)
|
|
}
|
|
|
|
w, h := app.Window.GLGetDrawableSize()
|
|
logger.Info("Drawable size: %dx%d", w, h)
|
|
|
|
logger.Info("Initializing virtual framebuffer (%dx%d -> %dx%d)",
|
|
graphics.VirtualWidth, graphics.VirtualHeight, w, h)
|
|
if err := graphics.InitVirtualFramebuffer(w, h); err != nil {
|
|
logger.Fatal("Virtual framebuffer init failed: %v", err)
|
|
}
|
|
|
|
app.initKeybinds()
|
|
logger.Info("SDL and OpenGL initialization complete")
|
|
}
|
|
|
|
func (app *Application) initializeManagers() {
|
|
logger.Info("Initializing ShaderManager")
|
|
if err := shaderManager.InitShaderManager(); err != nil {
|
|
logger.Fatal("ShaderManager initialization failed: %v", err)
|
|
}
|
|
|
|
logger.Info("Initializing FontManager")
|
|
if err := fontManager.InitFontManager(); 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)
|
|
logger.Error("Audio will be unavailable until a device is configured")
|
|
} else {
|
|
logger.Info("Audio initialized with device: '%s'", audioDevice)
|
|
}
|
|
}
|
|
|
|
func (app *Application) cleanup() {
|
|
logger.Info("Cleaning up")
|
|
|
|
for _, p := range app.pageStack {
|
|
p.Destroy()
|
|
}
|
|
app.pageStack = nil
|
|
|
|
graphics.DestroyVirtualFramebuffer()
|
|
|
|
if app.Window != nil {
|
|
app.Window.Destroy()
|
|
}
|
|
|
|
ttf.Quit()
|
|
sdl.Quit()
|
|
logger.Info("Cleanup complete")
|
|
logger.Close()
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Event Loop
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func (app *Application) runEventLoop() {
|
|
if len(app.pageStack) == 0 {
|
|
logger.Fatal("No initial page loaded, pageStack is empty")
|
|
}
|
|
|
|
const frameDelay = time.Second / targetFPS
|
|
app.running = true
|
|
lastTime := time.Now()
|
|
|
|
logger.Info("Entering event loop")
|
|
|
|
for app.running {
|
|
frameStart := time.Now()
|
|
|
|
app.handleEvents()
|
|
if !app.running {
|
|
break
|
|
}
|
|
|
|
deltaTime := float32(time.Since(lastTime).Seconds())
|
|
lastTime = time.Now()
|
|
|
|
app.updateCurrentPage(deltaTime)
|
|
app.renderFrame()
|
|
app.applyPendingAction()
|
|
|
|
frameTime := time.Since(frameStart)
|
|
if frameTime < frameDelay {
|
|
time.Sleep(frameDelay - frameTime)
|
|
}
|
|
}
|
|
|
|
logger.Info("Event loop exited")
|
|
}
|
|
|
|
func (app *Application) handleEvents() {
|
|
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
|
|
switch e := event.(type) {
|
|
case *sdl.QuitEvent:
|
|
app.running = false
|
|
return
|
|
|
|
case *sdl.TextInputEvent:
|
|
logger.Info("[EVENT] TextInputEvent: text=%q, IsTextInputActive=%v", e.GetText(), sdl.IsTextInputActive())
|
|
|
|
case *sdl.KeyboardEvent:
|
|
eventType := "KEYDOWN"
|
|
if e.Type == sdl.KEYUP {
|
|
eventType = "KEYUP"
|
|
}
|
|
logger.Info("[EVENT] KeyboardEvent: type=%s, sym=%d (%s), scancode=%d, mod=0x%04x, IsTextInputActive=%v",
|
|
eventType, e.Keysym.Sym, sdl.GetKeyName(e.Keysym.Sym), e.Keysym.Scancode, e.Keysym.Mod, sdl.IsTextInputActive())
|
|
|
|
if e.Type == sdl.KEYDOWN && e.Repeat == 0 {
|
|
isCtrlPressed := (e.Keysym.Mod&sdl.KMOD_CTRL) != 0 || (e.Keysym.Mod&sdl.KMOD_GUI) != 0
|
|
if e.Keysym.Sym == sdl.K_c && isCtrlPressed {
|
|
continue
|
|
}
|
|
keybinds.PerformAction(e)
|
|
}
|
|
}
|
|
|
|
if currentPage := app.currentPage(); currentPage != nil {
|
|
if err := currentPage.HandleEvent(&event); err != nil {
|
|
logger.Error("Page event handler error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (app *Application) updateCurrentPage(deltaTime float32) {
|
|
if currentPage := app.currentPage(); currentPage != nil {
|
|
currentPage.Update(deltaTime)
|
|
}
|
|
}
|
|
|
|
func (app *Application) renderFrame() {
|
|
graphics.BeginVirtualRender()
|
|
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 {
|
|
currentPage.Render()
|
|
}
|
|
|
|
graphics.BlitToScreen()
|
|
app.Window.GLSwap()
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Keybind Registration
|
|
// ----------------------------------------------------------------------------
|
|
|
|
func (app *Application) initKeybinds() {
|
|
keybinds.Register(sdl.K_F3, sdl.KMOD_NONE, func() {
|
|
app.PushPage(&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{})
|
|
}
|
|
})
|
|
|
|
keybinds.Register(sdl.K_F1, sdl.KMOD_NONE, func() {
|
|
app.UnwindToPage(&page.Welcome{})
|
|
})
|
|
|
|
keybinds.Register(sdl.K_q, sdl.KMOD_RCTRL, func() {
|
|
app.Stop()
|
|
})
|
|
|
|
keybinds.Register(sdl.K_UP, sdl.KMOD_SHIFT, func() {
|
|
mixer.SetVolume128(volumeStepSize)
|
|
})
|
|
|
|
keybinds.Register(sdl.K_DOWN, sdl.KMOD_SHIFT, func() {
|
|
mixer.SetVolume128(-volumeStepSize)
|
|
})
|
|
}
|
|
|
|
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
|
|
}
|