Initial release: RadiantWave v1.0
This commit is contained in:
@@ -0,0 +1,694 @@
|
||||
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.KeyboardEvent:
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user