diff --git a/.vscode/launch.json b/.vscode/launch.json index ed41e33..1226d3e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,12 +11,5 @@ "mode": "debug", "program": "rhemawave.go" }, - { - "name": "Launch Package", - "type": "go", - "request": "launch", - "mode": "auto", - "program": "${fileDirname}" - } ] } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 13ae62a..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,45 +0,0 @@ -# Rhema Wave Changelog - -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - -## IMPORTANT! -`9cdef97` - This commit hash is considered the "Great Reset Point". -Many bugs, issues, and complexities have been discovered, leading to a restructure -of the project, packaging, deployment and even target machine environment. -All tags prior to this commit have been deleted. All releases removed, all archived -packages dropped from the server, and even the server structure has been changed. -v0.9.0 will represent the "beta" release from this point forward. Until full release, we will -remain at v0.9.x. - -## [Unreleased] - -### Added -- TOS screens now display real content from the RWT Welcome Screen document (10 sections) -- TOS content is stored in the database and seeded from HTML files on disk, making it easy to update without recompiling -- WiFi setup page now appears automatically before TOS screens if no internet connection is detected -- Dev script `setup.sh` for syncing assets to /usr/ on development machines - ---- - -## [v0.9.22] - -### Added -- Batched texture rendering path (BeginTextureRendering/RenderTextureBatched/EndTextureRendering) for ScrollerPage -- Ring buffer (LineDeque) for ScrollerPage line management — O(1) prepend/append/remove -- In-memory config value cache in db package — eliminates repeated SQLite queries -- Buffered channel-based logger — single writer goroutine replaces per-log goroutine spawning -- Cached uniform locations on Shader struct — looked up once at load, reused every frame - -### Changed -- FontManager now reuses a persistent FBO, VAO, and VBO for string texture rendering instead of creating/deleting per call -- RenderSolidColorQuad uses cached uniform locations and removes per-frame glGetError calls -- Welcome page caches init_font_size at Init instead of querying the database every frame -- ScrollerPage.Destroy uses gl.Flush instead of gl.Finish to avoid blocking CPU on page transitions -- Scroll offset calculation uses simple subtraction instead of math.Mod with float64 round-trip -- Application cleanup now properly shuts down the logger goroutine via logger.Close() - -### Removed -- Removed per-frame CheckGLError calls from RenderSolidColorQuad (caused GPU pipeline stalls) \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index 549b4d4..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,201 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -RhemaWave is a Go-based fullscreen SDL2/OpenGL application designed to run on Linux kiosk systems. It displays proclamations with audio playback and animated visual patterns. The application uses a page-based navigation system and includes network connectivity validation, license key management, and an auto-update mechanism. - -## Build & Development - -### Development Workflow - -```bash -nix develop # enter dev shell (provides Go, SDL2, etc.) -go build -o bin/rhemawave rhemawave.go # build binary -./bin/rhemawave # run (requires display server + audio) -./dev_scripts/reset.sh # wipe local database and start fresh -``` - -The binary at `bin/rhemawave` resolves assets via `../share/rhemawave` relative to itself, so the repo layout (`bin/` + `share/rhemawave/`) works out of the box. - -### Nix Build - -```bash -nix build # produces ./result/bin/rhemawave + ./result/share/rhemawave/ -``` - -The flake's `postInstall` copies `share/` into `$out/`, preserving the same relative layout. - -### Testing -There are currently no automated tests in this codebase. - -## Architecture - -### Application Entry Point -- `rhemawave.go` is the main entry point, calls `internal/application.Run()` -- `internal/application/application.go` contains the core application lifecycle - -### Core Application Flow -1. **Initialization** - - Create application data directory (`~/.local/share/rhemawave`) - - Initialize SQLite database (`data.db`) with GORM - - Initialize SDL2 subsystems (VIDEO, AUDIO) - - Initialize SDL_ttf for font rendering - - Create fullscreen OpenGL 3.3 Core Profile window - - Initialize shader and font managers - - Hide cursor and enable relative mouse mode - -2. **Validation Phase** (startup page stack, top → bottom) - - WiFi setup page (if no internet connection detected) — shown first - - TOS sections from database (unaccepted only, ordered by section number) - - Email address page (if missing) - - License key page (if missing) - - Welcome page — always at bottom - - License key is also validated against the API - -3. **Event Loop** (60 FPS target) - - Poll SDL events - - Handle keyboard events via keybinds system - - Update current page with delta time - - Render current page - - Apply pending page transitions - -### Page System -Pages implement the `page.Page` interface: -- `Init(app ApplicationInterface) error` - Setup resources -- `HandleEvent(event *sdl.Event) error` - Handle input -- `Update(dt float32) error` - Update state -- `Render() error` - Draw to screen -- `Destroy() error` - Cleanup resources - -Pages can embed `page.Base` for common functionality (OpenGL quad rendering, projection matrix, screen dimensions). - -**Page Navigation:** -- `PushPage(p)` - Add page to stack (creates history) -- `SwitchPage(p)` - Replace current page -- `UnwindToPage(p)` - Pop back to a specific page (or create if not in stack) - -**Available Pages:** -- `Welcome` - Initial screen showing version -- `ScrollerPage` - Main proclamation player (F2) -- `Settings` - Settings menu (F3) -- `WiFiSetupPage` - Network configuration -- `LicenseKeyPage` - License key entry -- `EmailAddressPage` - Email address entry -- `AudioDevices` - Audio device selection -- `ProclamationOptions` - Proclamation selection -- `TOSPage` - Terms of Service acceptance screen (scrollable, Y/N to accept/decline) - -### Key Global Keybinds -Defined in `application.initKeybinds()`: -- `F1` - Navigate to Welcome page -- `F2` - Navigate to Player (ScrollerPage) -- `F3` - Navigate to Settings -- `SHIFT+UP` - Increase volume -- `SHIFT+DOWN` - Decrease volume -- `RIGHT_CTRL+Q` - Quit application - -### Package Structure - -**`internal/application`** - Core application lifecycle, event loop, page management, validation - -**`internal/page`** - Page interface and all page implementations - -**`internal/db`** - Database layer using GORM with SQLite -- Models: `Config`, `Proclamations`, `Log`, `TOSSection` -- Manages configuration key-value pairs -- Seeds default proclamations from `share/rhemawave/proclamations/` -- Seeds TOS sections from `share/rhemawave/tos/*.html` (filename format: `{NN} - {Title}.html`) - -**`internal/audio`** - SDL2 audio subsystem integration (currently minimal wrapper) - -**`internal/mixer`** - Audio playback manager for music and proclamations - -**`internal/graphics`** - OpenGL utilities and display orientation handling - -**`internal/shaderManager`** - Loads and manages OpenGL shaders from `share/rhemawave/shaders/` - -**`internal/fontManager`** - Loads and manages TrueType fonts from `share/rhemawave/fonts/` - -**`internal/keybinds`** - Global keyboard shortcut registration and dispatch - -**`internal/network`** - Network connectivity manager (uses D-Bus for NetworkManager on Linux) - -**`internal/logger`** - Logging system that writes to database and stdout - -**`internal/colors`** - Color utilities - -**`internal/pattern`** - Visual pattern generation (line patterns, color patterns) - -### Repository Layout - -``` -bin/ # build output (gitignored) - rhemawave -share/rhemawave/ # assets (checked in) - audio/ # Audio files (audio.wav, etc.) - fonts/ # TrueType fonts - shaders/ # GLSL shader files - proclamations/ # Proclamation text files - home/ # Non-commercial proclamations - commercial/ # Commercial proclamations - tos/ # TOS HTML files (seeded into DB on first run) -packaging/ # deployment configs (kiosk, updater, polkit) - kiosk/ # kiosk system configs - rhemawave-updater.py # auto-updater script -internal/ # Go source packages -dev_scripts/ - reset.sh # deletes ~/.local/share/rhemawave/ -flake.nix # Nix build and dev shell -``` - -### Important Implementation Details - -**Asset Resolution:** `resolveAssetsDir()` computes `filepath.Join(filepath.Dir(exe), "..", "share", "rhemawave")` — works in both dev (`bin/rhemawave` → `share/rhemawave/`) and Nix install (`$out/bin/rhemawave` → `$out/share/rhemawave/`). - -**OpenGL Context:** Uses OpenGL 3.3 Core Profile with VSync enabled. The page base class sets up a reusable quad VAO/VBO for texture and solid color rendering. - -**Database Location:** `~/.local/share/rhemawave/data.db` - Contains configuration, proclamations, and log entries - -**Fullscreen Mode:** Uses `SDL_WINDOW_FULLSCREEN_DESKTOP` to match native display resolution - -**Display Orientation:** Handled by `graphics.SetDisplayOrientation()` which swaps width/height for portrait displays - -**Version Injection:** Git version is injected at build time via ldflags: `-X 'rhemawavetech.com/rhemawave/internal/page.GitVersion=${VERSION}'` - -**Network Manager:** Uses D-Bus to communicate with NetworkManager for WiFi configuration and connectivity status - -## Development Workflow - -### Adding a New Page -1. Create `internal/page/.go` -2. Implement the `page.Page` interface -3. Embed `page.Base` for common functionality -4. Register keybind in `application.initKeybinds()` if needed -5. Add navigation logic from existing pages - -### Modifying Database Schema -1. Update models in `internal/db/db.go` -2. GORM AutoMigrate will handle schema changes on next run -3. Add seed data logic if needed in `ensureDefaultsExist()`, `seedDefaultProclamations()`, or `seedDefaultTOSSections()` - -### Adding New Assets -1. Place files in appropriate `share/rhemawave/` subdirectory -2. They will be included automatically in Nix builds via `postInstall` -3. Access via config values (`assets_dir`) or direct paths - -### Working with Shaders -1. Create `.vert` and `.frag` files in `share/rhemawave/shaders/` -2. Load via `shaderManager.GetShader("")` -3. Shader must have uniforms: `u_mvpMatrix`, `u_color` or `u_textColor`, `u_texture` (if using textures) - -## Important Constraints - -- Must build with `CGO_ENABLED=1` for SDL2 bindings -- Target platform is Linux (uses D-Bus for NetworkManager) -- Application expects to run as kiosk user with polkit rules for system operations -- Assets are resolved relative to the binary (`../share/rhemawave`), so `bin/` and `share/` must be siblings -- Version string comes from git tags (release) or commit hash (dev) -- ShaderManager, FontManager, and Logger use `sync.Once` singletons — they must be initialized (via their `Init()` or `Initialize()` functions) before calling `Get()`; calling `Get()` before initialization panics -- Deployed via NixOS-Anywhere to kiosk hardware diff --git a/README.md b/README.md index e337b3e..e5d0bed 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,13 @@ Stored in `~/.local/share/rhemawave/`: - `log.txt` — application log - `license_key` — license key file +### Development Rules +These rules must be followed during the development of RhemaWave to retain a +maintainable application. +1. The Application package is soley responsible for managing the database handler, and the +configuration cache. Internal packages may update the configuration within the database, and +then signal the application to refresh the cache via a callback function `refreshCache()`. + ## Deployment Deployed to kiosk hardware via NixOS-Anywhere. See `packaging/` for kiosk system configs. diff --git a/internal/application/application.go b/internal/application/application.go index edb6023..13885ca 100644 --- a/internal/application/application.go +++ b/internal/application/application.go @@ -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 diff --git a/internal/db/db.go b/internal/db/db.go deleted file mode 100644 index c88380c..0000000 --- a/internal/db/db.go +++ /dev/null @@ -1,492 +0,0 @@ -package db - -import ( - "fmt" - "log" - "os" - "path/filepath" - "strings" - "sync" - - "gorm.io/driver/sqlite" - "gorm.io/gorm" - "rhemawavetech.com/rhemawave/internal/logger" -) - -var DB *gorm.DB - -// configCache is an in-memory cache of config values to avoid repeated DB queries. -var configCache = struct { - sync.RWMutex - m map[string]string -}{m: make(map[string]string)} - -type Config struct { - gorm.Model - Key string `gorm:"uniqueIndex"` - Value string -} - -type Proclamations struct { - gorm.Model - Title string - Content string - Commercial bool - Selected bool - Favorite bool - Available bool -} - -type Log struct { - gorm.Model - Timestamp string - Level string - Message string -} - -func (Log) TableName() string { - return "log_entries" -} - -type TOSSection struct { - gorm.Model - SectionNumber int `gorm:"uniqueIndex"` - Title string - Content string // raw HTML -} - -var defaultConfigValues = map[string]string{ - "home_dir": "", - "assets_dir": "", // Set dynamically in seedDefaults() to $HOME/.local/share/rhemawave - "log_dir": "", // Set dynamically in seedDefaults() to $HOME/.local/share/rhemawave/logs.log - "email_address": "", - "license_key": "", - "system_type": "home", - "system_capabilities": "", - "selected_audio": "audio/audio.wav", - "audio_device_name": "", - "application_font": "Roboto-Regular", - "init_font_size": "128", - "standard_font_size": "32", - "line_pattern": "fibonacci", - "last_volume": "128", - "audio_enabled": "true", - "tos_section_1_accepted": "false", - "tos_section_2_accepted": "false", - "tos_section_3_accepted": "false", - "tos_section_4_accepted": "false", - "tos_section_5_accepted": "false", - "tos_section_6_accepted": "false", - "tos_section_7_accepted": "false", - "tos_section_8_accepted": "false", - "tos_section_9_accepted": "false", - "tos_section_10_accepted": "false", -} - -// InitDatabase initializes the SQLite database connection -// and performs auto-migration for all models -func InitDatabase(dbpath string) error { - // Create the directory for the database if it doesn't exist - dbDir := filepath.Dir(dbpath) - if err := os.MkdirAll(dbDir, 0700); err != nil { - log.Printf("Error creating database directory %s: %v", dbDir, err) - return err - } - - // Check if database file exists - _, err := os.Stat(dbpath) - dbExists := !os.IsNotExist(err) - - if !dbExists { - log.Printf("Database file does not exist at %s, creating new database", dbpath) - } - - dsn := fmt.Sprintf("file:%s", dbpath) - - DB, err = gorm.Open(sqlite.Open(dsn), &gorm.Config{}) - if err != nil { - log.Printf("Error opening database: %v", err) - return err - } - - // Get underlying sql.DB to set pragmas - sqlDB, err := DB.DB() - if err != nil { - logger.Error("Error getting underlying database connection: %v", err) - return err - } - - // Configure SQLite for reliability and performance - pragmas := []string{ - "PRAGMA journal_mode=WAL", // Write-Ahead Logging for better concurrency - "PRAGMA synchronous=FULL", // Maximum durability - "PRAGMA foreign_keys=ON", // Enable foreign key constraints - "PRAGMA busy_timeout=5000", // Wait up to 5 seconds for locks - "PRAGMA temp_store=MEMORY", // Keep temp tables in memory - } - - for _, pragma := range pragmas { - if _, err := sqlDB.Exec(pragma); err != nil { - logger.Error("Error setting pragma %s: %v", pragma, err) - return fmt.Errorf("failed to set pragma: %w", err) - } - } - - if err := sqlDB.Ping(); err != nil { - logger.Error("Error pinging database: %v", err) - return fmt.Errorf("database connection failed: %w", err) - } - - log.Println("Database connected successfully") - - // Auto-migrate models - err = DB.AutoMigrate(&Config{}, &Proclamations{}, &Log{}, &TOSSection{}) - if err != nil { - logger.Error("Error migrating database: %v", err) - return err - } - log.Println("Database migrated successfully") - - // Seed defaults - err = ensureDefaultsExist() - if err != nil { - logger.Error("Error seeding default configuration values: %v", err) - return err - } - err = seedDefaultProclamations() - if err != nil { - logger.Error("Error seeding default proclamations: %v", err) - return err - } - - err = seedDefaultTOSSections() - if err != nil { - logger.Error("Error seeding default TOS sections: %v", err) - return err - } - - log.Println("Database initialized successfully") - return nil -} - -// seedDefaultProclamations reads proclamation text files from the assets directory -// and populates the database on first run -func seedDefaultProclamations() error { - var count int64 - err := DB.Model(&Proclamations{}).Count(&count).Error - if err != nil { - logger.Error("Error counting proclamations: %v", err) - return err - } - - // Only seed if table is empty - if count > 0 { - return nil - } - - // Get the assets directory from config - assetsDir, err := GetConfigValue("assets_dir") - if err != nil { - logger.Error("Error getting assets_dir: %v", err) - return err - } - - proclamationsBaseDir := assetsDir + "/proclamations" - - // Process both home and commercial directories - subdirs := []string{"home", "commercial"} - totalSeeded := 0 - - for _, subdir := range subdirs { - proclamationsDir := proclamationsBaseDir + "/" + subdir - isCommercial := (subdir == "commercial") - - // Read all .txt files from the subdirectory - files, err := os.ReadDir(proclamationsDir) - if err != nil { - logger.Error("Error reading proclamations directory %s: %v", proclamationsDir, err) - continue // Continue with other directories even if one fails - } - - for _, file := range files { - if file.IsDir() || !strings.HasSuffix(file.Name(), ".txt") { - continue - } - - filePath := proclamationsDir + "/" + file.Name() - content, err := os.ReadFile(filePath) - if err != nil { - logger.Error("Error reading file %s: %v", filePath, err) - continue - } - - // Extract title from filename (remove .txt extension) - title := strings.TrimSuffix(file.Name(), ".txt") - - // Select "Relaxation" as the default proclamation - isSelected := (title == "Relaxation") - - proclamation := Proclamations{ - Title: title, - Content: string(content), - Commercial: isCommercial, - Selected: isSelected, - Favorite: false, - Available: true, - } - - if err := DB.Create(&proclamation).Error; err != nil { - logger.Error("Error creating proclamation from %s: %v", file.Name(), err) - return err - } - - if isSelected { - logger.Error("Set default proclamation: %s", title) - } - - totalSeeded++ - } - } - - logger.Error("Seeded %d default proclamations from %s", totalSeeded, proclamationsBaseDir) - return nil -} - -// GetConfigValue retrieves a configuration value by key. -// Results are cached in memory to avoid repeated DB queries. -func GetConfigValue(key string) (string, error) { - configCache.RLock() - if v, ok := configCache.m[key]; ok { - configCache.RUnlock() - return v, nil - } - configCache.RUnlock() - - var config Config - result := DB.First(&config, "key = ?", key) - if result.Error != nil { - return "", result.Error - } - - configCache.Lock() - configCache.m[key] = config.Value - configCache.Unlock() - - return config.Value, nil -} - -// GetConfigValues retrieves all configuration key-value pairs -// Returns a map of key-value pairs OR any error encountered -// This is mainly for compatibility with legacy code, but can -// be useful for bulkl config referrals -func GetConfigValues() (map[string]string, error) { - var configs []Config - result := DB.Find(&configs) - if result.Error != nil { - return nil, result.Error - } - configMap := make(map[string]string) - for _, config := range configs { - configMap[config.Key] = config.Value - } - return configMap, nil -} - -// SetConfigValue sets a configuration value by key -// Returns any error encountered || nil -func SetConfigValue(key, value string) error { - // First try to find existing config - var config Config - result := DB.Where("key = ?", key).First(&config) - - if result.Error != nil { - if result.Error == gorm.ErrRecordNotFound { - // Record doesn't exist, create new one - config = Config{Key: key, Value: value} - if err := DB.Create(&config).Error; err != nil { - return fmt.Errorf("failed to create config value for key %s: %w", key, err) - } - } else { - // Some other error occurred - return fmt.Errorf("failed to query config for key %s: %w", key, result.Error) - } - } else { - // Record exists, update it - config.Value = value - if err := DB.Save(&config).Error; err != nil { - return fmt.Errorf("failed to update config value for key %s: %w", key, err) - } - } - - // Update cache - configCache.Lock() - configCache.m[key] = value - configCache.Unlock() - - return nil -} - -// GetProclamations retrieves all proclamation entries from the database -// Returns a slice of Proclamations OR any error encountered -func GetProclamations() ([]Proclamations, error) { - var proclamations []Proclamations - result := DB.Find(&proclamations) - if result.Error != nil { - return nil, result.Error - } - return proclamations, nil -} - -// seedDefaultTOSSections reads HTML files from the assets directory and populates the -// TOS sections table on first run. -func seedDefaultTOSSections() error { - var count int64 - if err := DB.Model(&TOSSection{}).Count(&count).Error; err != nil { - return err - } - if count > 0 { - return nil - } - - assetsDir, err := GetConfigValue("assets_dir") - if err != nil { - return fmt.Errorf("getting assets_dir: %w", err) - } - - tosDir := filepath.Join(assetsDir, "tos") - files, err := os.ReadDir(tosDir) - if err != nil { - return fmt.Errorf("reading tos directory %s: %w", tosDir, err) - } - - seeded := 0 - for _, file := range files { - if file.IsDir() || !strings.HasSuffix(file.Name(), ".html") { - continue - } - - // Parse filename: "01 - WELCOME.html" -> number=1, title="WELCOME" - name := strings.TrimSuffix(file.Name(), ".html") - parts := strings.SplitN(name, " - ", 2) - if len(parts) != 2 { - logger.Error("Skipping TOS file with unexpected name format: %s", file.Name()) - continue - } - - var sectionNum int - if _, err := fmt.Sscanf(parts[0], "%d", §ionNum); err != nil { - logger.Error("Skipping TOS file, cannot parse section number: %s", file.Name()) - continue - } - title := strings.TrimSpace(parts[1]) - - content, err := os.ReadFile(filepath.Join(tosDir, file.Name())) - if err != nil { - logger.Error("Error reading TOS file %s: %v", file.Name(), err) - continue - } - - section := TOSSection{ - SectionNumber: sectionNum, - Title: title, - Content: string(content), - } - if err := DB.Create(§ion).Error; err != nil { - return fmt.Errorf("creating TOS section from %s: %w", file.Name(), err) - } - seeded++ - } - - logger.Info("Seeded %d TOS sections from %s", seeded, tosDir) - return nil -} - -// GetTOSSections returns all TOS sections ordered by section number. -func GetTOSSections() ([]TOSSection, error) { - var sections []TOSSection - result := DB.Order("section_number asc").Find(§ions) - if result.Error != nil { - return nil, result.Error - } - return sections, nil -} - -// GetLogs retrieves log entries from the database, ordered by creation time ascending. -// If limit > 0, at most that many entries are returned. -func GetLogs(limit int) ([]Log, error) { - var logs []Log - query := DB.Order("created_at DESC") - if limit > 0 { - query = query.Limit(limit) - } - return logs, query.Find(&logs).Error -} - -// ensureDefaultsExist checks the current configs table rows and inserts any new rows -// from defaultConfigValues to the database if they don't exist. -// This is essentially an automatic migration for new config values. -func ensureDefaultsExist() error { - for key, value := range defaultConfigValues { - var config Config - result := DB.Where("key = ?", key).First(&config) - - // Always re-resolve path-based config values from the current binary location - switch key { - case "assets_dir": - value = resolveAssetsDir() - case "log_dir": - value = filepath.Join(resolveAssetsDir(), "logs.log") - } - - if result.Error == gorm.ErrRecordNotFound { - // Key doesn't exist, create it with default value - if err := DB.Create(&Config{Key: key, Value: value}).Error; err != nil { - return fmt.Errorf("failed to create default config for %s: %w", key, err) - } - log.Printf("Added missing config key: %s", key) - } else if result.Error != nil { - return result.Error - } else if (key == "assets_dir" || key == "log_dir") && config.Value != value { - // Update stale path-based values (e.g. after Nix store update) - config.Value = value - if err := DB.Save(&config).Error; err != nil { - return fmt.Errorf("failed to update config for %s: %w", key, err) - } - log.Printf("Updated stale config key: %s", key) - } else if key == "selected_audio" && config.Value == "audio/7.83.wav" { - // Migrate renamed audio file - config.Value = value - if err := DB.Save(&config).Error; err != nil { - return fmt.Errorf("failed to migrate config for %s: %w", key, err) - } - log.Printf("Migrated config key: %s", key) - } - } - return nil -} - -func AudioEnabled() bool { - on, err := GetConfigValue("audio_enabled") - if err != nil { - return false - } - return on == "true" -} - -// resolveAssetsDir returns the assets directory. If RHEMAWAVE_ASSETS_DIR is -// set, it is used directly (useful for debugging with dlv). Otherwise, it -// resolves relative to the running binary: .../bin/rhemawave → .../share/rhemawave. -// Falls back to /usr/local/share/rhemawave. -func resolveAssetsDir() string { - if dir := os.Getenv("RHEMAWAVE_ASSETS_DIR"); dir != "" { - return dir - } - exe, err := os.Executable() - if err == nil { - exe, err = filepath.EvalSymlinks(exe) - } - if err == nil { - return filepath.Join(filepath.Dir(exe), "..", "share", "rhemawave") - } - return filepath.Join("/usr", "local", "share", "rhemawave") -} diff --git a/internal/db/newDb.go b/internal/db/newDb.go new file mode 100644 index 0000000..185ff06 --- /dev/null +++ b/internal/db/newDb.go @@ -0,0 +1,48 @@ +// Package db provides a database connection and functions for interacting with the database. +// The DB object should be held and maintained by the application, not by this package. +// The application should be responsible for connecting to, migrating, and initial population +// of the database. This package should only provide the schema definitions and helper functions. +// Schema definitions should clone API response structures as closely as possible. + +package db + +import ( + "gorm.io/driver/sqlite" + "gorm.io/gorm" +) + +// Connect to the database and return the gorm.DB object. +func Connect(dbpath string) (*gorm.DB, error) { + db, err := gorm.Open(sqlite.Open(dbpath), &gorm.Config{}) + if err != nil { + return nil, err + } + // Set pragmas + sqlDB, err := db.DB() + if err != nil { + return nil, err + } + pragmas := []string{ + "PRAGMA journal_mode=WAL", + "PRAGMA synchronous=FULL", + "PRAGMA foreign_keys=ON", + "PRAGMA busy_timeout=5000", + } + + for _, pragma := range pragmas { + if _, err := sqlDB.Exec(pragma); err != nil { + return nil, err + } + } + + return db, nil +} + +// Migrate the database schema +func Migrate(db *gorm.DB) error { + // Pass all models to be migrated to this function call. + if err := db.AutoMigrate(&AppConfig{}, &Log{}, &Proclamation{}); err != nil { + return err + } + return nil +} diff --git a/internal/db/schema.go b/internal/db/schema.go new file mode 100644 index 0000000..e10515d --- /dev/null +++ b/internal/db/schema.go @@ -0,0 +1,39 @@ +// schema.go defines the database schema for the GORM models. +package db + +import "gorm.io/gorm" + +type AppConfig struct { + gorm.Model + DataDir string + AssetsDir string + EmailAddress string + LicenseKey string + SystemLevel string // BASIC, ADVANCED, ELITE, COMMERCIAL + SelectedAudio string // Audio filepath + SelectedAudioDevice string // The pipewire device name + InitialFontSize int // How large to create the font atlas + StandardFontSize int // The font size used for most text rendering + LinePattern string // "fibonacci" + LastVolume int // 0-128 + AudioEnabled bool +} + +type Log struct { + gorm.Model + Timestamp int64 + Level string + Message string +} + +type Proclamation struct { + gorm.Model + ID string `gorm:"primaryKey"` + Name string + Content string + SolfeggioFrequencyID int + CategoryID int + Selected bool + Available bool + Price float64 +} diff --git a/internal/fontManager/fontManager.go b/internal/fontManager/fontManager.go index 433ce3a..28fc510 100644 --- a/internal/fontManager/fontManager.go +++ b/internal/fontManager/fontManager.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "strconv" "strings" "sync" @@ -35,7 +34,7 @@ var ( // InitFontManager initializes the FontManager singleton. // It must be called once at startup after ShaderManager has been initialized. -func InitFontManager() error { +func InitFontManager(cfg *db.AppConfig) error { fontManagerOnce.Do(func() { // Dependency check: Ensure ShaderManager is ready @@ -49,13 +48,8 @@ func InitFontManager() error { Fonts: make(map[string]*FontEntry), } - assetsDir, err := db.GetConfigValue("assets_dir") - if err != nil { - initErr = fmt.Errorf("could not get assets_dir from config: %w", err) - return - } - fontPath := filepath.Join(assetsDir, "fonts") - if err := fm.loadFonts(fontPath); err != nil { + fontPath := filepath.Join(cfg.AssetsDir, "fonts") + if err := fm.loadFonts(fontPath, cfg); err != nil { initErr = fmt.Errorf("could not load fonts from '%s': %w", fontPath, err) return } @@ -136,7 +130,7 @@ type Glyph struct { } // loadFonts scans the fonts directory, loads all .ttf/.otf files, and creates a font map for each. -func (fm *FontManager) loadFonts(fontDir string) error { +func (fm *FontManager) loadFonts(fontDir string, cfg *db.AppConfig) error { files, err := os.ReadDir(fontDir) if err != nil { return fmt.Errorf("could not read font directory '%s': %w", fontDir, err) @@ -149,11 +143,7 @@ func (fm *FontManager) loadFonts(fontDir string) error { fileName := file.Name() if strings.HasSuffix(strings.ToLower(fileName), ".ttf") || strings.HasSuffix(strings.ToLower(fileName), ".otf") { fontPath := filepath.Join(fontDir, fileName) - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("could not get standard_font_size from config: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) + baseFontSize := cfg.StandardFontSize if err != nil { return fmt.Errorf("could not convert init_font_size to int: %w", err) } diff --git a/internal/logger/logger.go b/internal/logger/logger.go index 39f06f7..7db09ea 100644 --- a/internal/logger/logger.go +++ b/internal/logger/logger.go @@ -4,10 +4,10 @@ import ( "fmt" "log" "os" - "sync" "time" "gorm.io/gorm" + "rhemawavetech.com/rhemawave/internal/db" ) type Level int @@ -24,78 +24,34 @@ func (l Level) String() string { return [...]string{"DEBUG", "INFO", "WARNING", "ERROR", "FATAL"}[l] } -type logEntry struct { - timestamp string - level string - message string -} - var ( - mu sync.Mutex - db *gorm.DB + DB *gorm.DB file *os.File console = log.New(os.Stderr, "", 0) - - dbChan chan logEntry - dbChanOnce sync.Once ) -// startDBWriter starts a single background goroutine to drain log entries to the database. -func startDBWriter() { - dbChan = make(chan logEntry, 256) - go func() { - for entry := range dbChan { - row := map[string]any{ - "timestamp": entry.timestamp, - "level": entry.level, - "message": entry.message, - } - if err := db.Table("log_entries").Create(row).Error; err != nil { - mu.Lock() - console.Printf("Failed to write log to database: %v", err) - mu.Unlock() - } - } - }() -} - // AddFile attaches or replaces the file backend func AddFile(path string) error { - mu.Lock() - defer mu.Unlock() - if file != nil { file.Close() } - f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) + _, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if err != nil { return err } - file = f + return nil } // AddDatabase attaches or replaces the database backend func AddDatabase(d *gorm.DB) { - mu.Lock() - defer mu.Unlock() - db = d - dbChanOnce.Do(startDBWriter) + console.Println("Adding database to logger") + DB = d } -// Close shuts down the database writer goroutine and closes the file backend. +// Close shuts down the file backend. func Close() error { - mu.Lock() - defer mu.Unlock() - - // Stop the DB writer goroutine by closing the channel - if dbChan != nil { - close(dbChan) - dbChan = nil - db = nil - } - if file != nil { err := file.Close() file = nil @@ -106,24 +62,31 @@ func Close() error { func logMsg(level Level, format string, args ...any) { message := fmt.Sprintf(format, args...) - timestamp := time.Now().Format("2006-01-02 15:04:05.000") + now := time.Now() + timestamp := now.Format("2006-01-02 15:04:05.000") line := fmt.Sprintf("%s [%s] %s", timestamp, level, message) - mu.Lock() + // 1. The standard log package is internally thread-safe console.Println(line) + // 2. Write to file if file != nil { fmt.Fprintln(file, line) } - if db != nil && dbChan != nil { // both cleared by Close() - select { - case dbChan <- logEntry{timestamp: timestamp, level: level.String(), message: message}: - default: - // Channel full, drop the DB write rather than blocking + // 3. Write to DB using GORM. It handles its own connection pooling and locking. + if DB != nil { + var entry db.Log + entry.Timestamp = now.UnixMilli() + entry.Level = level.String() + entry.Message = message + + // Use Save() to insert the new record. + result := DB.Save(&entry) + if result.Error != nil { + console.Printf("Failed to write log to database: %v\n", result.Error) } } - mu.Unlock() } func Debug(format string, args ...any) { logMsg(LevelDebug, format, args...) } diff --git a/internal/mixer/mixer.go b/internal/mixer/mixer.go index 5a8279f..42a82bb 100644 --- a/internal/mixer/mixer.go +++ b/internal/mixer/mixer.go @@ -4,7 +4,6 @@ package mixer import ( "errors" "fmt" - "strconv" "sync" "github.com/veandco/go-sdl2/mix" @@ -30,16 +29,20 @@ var ( defaultChannels = 2 defaultChunkSize = 1024 defaultAllow = sdl.AUDIO_ALLOW_ANY_CHANGE + + refreshCacheCallback func() ) // Init initializes SDL audio + SDL_mixer. Safe to call multiple times. -func Init(wantedDevice string) error { +func Init(cfg *db.AppConfig, onUpdate func()) error { mu.Lock() defer mu.Unlock() if inited { return nil } + refreshCacheCallback = onUpdate + // Let SDL choose the best audio driver for the system // Remove hardcoded ALSA preference to support PulseAudio, PipeWire, etc. // Users can set SDL_AUDIODRIVER environment variable if needed @@ -49,31 +52,21 @@ func Init(wantedDevice string) error { } // WAV-only does not require mix.Init(); add when you support OGG/MP3/etc. - if err := openDeviceLocked(wantedDevice); err != nil { + if err := openDeviceLocked(cfg.SelectedAudioDevice); err != nil { // Clean up the subsystem on failure so callers can retry later. sdl.QuitSubSystem(sdl.INIT_AUDIO) return err } - // Check if the volume is stored in the config; if not, default to max. - storedVolume, err := db.GetConfigValue("last_volume") - if err != nil { - return fmt.Errorf("retrieving last_volume from config: %w", err) - } - - lastVolume, err := strconv.Atoi(storedVolume) - if err != nil { - return fmt.Errorf("parsing last_volume %q: %w", storedVolume, err) - } - - if lastVolume < 0 { - lastVolume = int(mix.MAX_VOLUME) - currentVolume = lastVolume - if err := db.SetConfigValue("last_volume", strconv.Itoa(lastVolume)); err != nil { - } - } else { - currentVolume = lastVolume - } + // TODO: Check if this logic is still needed; move to application + // if lastVolume < 0 { + // lastVolume = int(mix.MAX_VOLUME) + // currentVolume = lastVolume + // if err := db.SetConfigValue("last_volume", strconv.Itoa(lastVolume)); err != nil { + // } + // } else { + // currentVolume = lastVolume + // } // mix.VolumeMusic(currentVolume) @@ -107,8 +100,8 @@ func openDeviceLocked(name string) error { // Play loads a WAV (as music) and plays it. If loopForever is true, loops forever. // If something is already playing, it stops and replaces it. -func Play(path string, loopForever bool) error { - if !db.AudioEnabled() { +func Play(path string, loopForever bool, cfg *db.AppConfig) error { + if !cfg.AudioEnabled { return nil } @@ -159,8 +152,8 @@ func Play(path string, loopForever bool) error { // Stop stops playback. If fadeMs > 0, fades out asynchronously; otherwise halts immediately. // It also frees the currently loaded music (so a new Play will reload). -func Stop(fadeMs int) { - if !db.AudioEnabled() { +func Stop(fadeMs int, cfg *db.AppConfig) { + if !cfg.AudioEnabled { return } @@ -184,8 +177,8 @@ func Stop(fadeMs int) { } // SetVolume128 sets music volume using SDL_mixer’s 0..128 scale. -func SetVolume128(increment int) { - if !db.AudioEnabled() { +func SetVolume128(increment int, cfg *db.AppConfig) { + if !cfg.AudioEnabled { return } @@ -201,8 +194,8 @@ func SetVolume128(increment int) { } // Store in config - if err := db.SetConfigValue("last_volume", strconv.Itoa(currentVolume)); err != nil { - } + cfg.LastVolume = currentVolume + refreshCacheCallback() // Apply to SDL_mixer if inited && deviceOpen { diff --git a/internal/netReq/netReq.go b/internal/netReq/netReq.go new file mode 100644 index 0000000..a01adfa --- /dev/null +++ b/internal/netReq/netReq.go @@ -0,0 +1,26 @@ +package netreq + +import ( + "fmt" + "io" + "net/http" +) + +// Makes a GET request to the specified URL and returns the JSON response +// as a byte slice. If the request fails, it returns an error. +func GetJSON(url string) ([]byte, error) { + fmt.Println(url) + res, err := http.Get(url) + if err != nil { + return nil, err + } + body, err := io.ReadAll(res.Body) + res.Body.Close() + if res.StatusCode > 299 { + return nil, fmt.Errorf("request failed with status code %d: %s", res.StatusCode, string(body)) + } + if err != nil { + return nil, err + } + return body, nil +} diff --git a/internal/netReq/netReq_test.go b/internal/netReq/netReq_test.go new file mode 100644 index 0000000..ecebb23 --- /dev/null +++ b/internal/netReq/netReq_test.go @@ -0,0 +1,19 @@ +package netreq + +import ( + "fmt" + "testing" +) + +func TestGet(t *testing.T) { + url := "http://localhost:5173/api/v1/proclamations" + body, err := GetJSON(url) + if err != nil { + t.Fatalf("GetJSON failed: %v", err) + } + if len(body) == 0 { + t.Fatal("GetJSON returned empty body") + } + cnt := string(body) + fmt.Printf("%s\n", cnt) +} diff --git a/internal/page/audioDevices.go b/internal/page/audioDevices.go index 325b80a..1bb7c3b 100644 --- a/internal/page/audioDevices.go +++ b/internal/page/audioDevices.go @@ -2,10 +2,10 @@ package page import ( "fmt" - "strconv" "github.com/go-gl/gl/v3.3-core/gl" "github.com/veandco/go-sdl2/sdl" + "gorm.io/gorm" "rhemawavetech.com/rhemawave/internal/colors" "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/fontManager" @@ -24,9 +24,11 @@ type AudioDevices struct { highlightIndex int selectedIndex int verticalMargin int32 + CfgPtr *db.AppConfig + dbPtr *gorm.DB } -func (p *AudioDevices) Init(app ApplicationInterface) error { +func (p *AudioDevices) Init(ctx *PageCtx) error { var err error p.verticalMargin = int32(20) @@ -50,19 +52,12 @@ func (p *AudioDevices) Init(app ApplicationInterface) error { } // Initialize base - if err := p.Base.Init(app); err != nil { + if err := p.Base.Init(ctx); err != nil { return fmt.Errorf("base init failed: %w", err) } // Get font size from database - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) - if err != nil { - return fmt.Errorf("parsing init_font_size %q: %w", baseFontSizeStr, err) - } + baseFontSize := p.Base.Context.CfgPtr.StandardFontSize // Calculate scales titleScale := float32(64) / float32(baseFontSize) @@ -90,10 +85,7 @@ func (p *AudioDevices) Init(app ApplicationInterface) error { } // Get currently selected device from database - currentDevice, err := db.GetConfigValue("audio_device_name") - if err != nil { - return fmt.Errorf("retrieving audio_device_name from db: %w", err) - } + currentDevice := p.Base.Context.CfgPtr.SelectedAudioDevice // Create UI items for each device for i, name := range devices { // Iterate over the devices @@ -111,13 +103,10 @@ func (p *AudioDevices) Init(app ApplicationInterface) error { } } - on, err := db.GetConfigValue("audio_enabled") - if err != nil { - return fmt.Errorf("unable to query database for audio_enabled: %w", err) - } + on := p.Base.Context.CfgPtr.AudioEnabled audioStatus := "Turn Audio On" - if on == "true" { + if on { audioStatus = "Turn Audio Off" } @@ -177,22 +166,11 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error { // Handle audio disable if selection == "Turn Audio Off" || selection == "Turn Audio On" { - on, err := db.GetConfigValue("audio_enabled") - if err != nil { - return fmt.Errorf("failed to fetch audio_enabled from database: %w", err) - } - - currentStatus, err := strconv.ParseBool(on) - if err != nil { - return fmt.Errorf("failed to parse audio_enabled value: %w", err) - } - - newStatus := strconv.FormatBool(!currentStatus) - if err := db.SetConfigValue("audio_enabled", newStatus); err != nil { - return fmt.Errorf("failed to update audio_enabled: %w", err) - } - - p.App.UnwindToPage(&Settings{}) + newStatus := !p.Base.Context.CfgPtr.AudioEnabled + updatedConfig := p.Base.Context.CfgPtr + updatedConfig.AudioEnabled = newStatus + p.Base.Context.DbPtr.Save(&updatedConfig) + p.Base.Context.RefreshConfig() return nil } @@ -208,17 +186,12 @@ func (p *AudioDevices) HandleEvent(event *sdl.Event) error { } // Save settings - if err := db.SetConfigValue("audio_device_name", deviceName); err != nil { - return fmt.Errorf("failed to save audio device: %w", err) - } - if err := db.SetConfigValue("audio_enabled", "true"); err != nil { - return fmt.Errorf("failed to enable audio: %w", err) - } - - p.App.UnwindToPage(&Settings{}) - + p.Base.Context.CfgPtr.SelectedAudioDevice = deviceName + p.Base.Context.CfgPtr.AudioEnabled = true + p.Base.Context.DbPtr.Save(p.Base.Context.CfgPtr) + p.Base.Context.RefreshConfig() case sdl.K_BACKSPACE: - p.App.UnwindToPage(&Settings{}) + p.Base.Context.NavigateTo(SETTINGS) } return nil diff --git a/internal/page/downloadProgressPage.go b/internal/page/downloadProgressPage.go deleted file mode 100644 index 00e4224..0000000 --- a/internal/page/downloadProgressPage.go +++ /dev/null @@ -1,276 +0,0 @@ -package page - -import ( - "fmt" - "strconv" - "sync/atomic" - - "github.com/go-gl/gl/v3.3-core/gl" - "github.com/go-gl/mathgl/mgl32" - "github.com/veandco/go-sdl2/sdl" - "rhemawavetech.com/rhemawave/internal/colors" - "rhemawavetech.com/rhemawave/internal/db" - "rhemawavetech.com/rhemawave/internal/fontManager" - "rhemawavetech.com/rhemawave/internal/keybinds" - "rhemawavetech.com/rhemawave/internal/shaderManager" -) - -type downloadState int - -const ( - downloadStateDownloading downloadState = iota - downloadStateFinished -) - -// DownloadProgressPage displays a progress bar while an audio file is downloading. -// The page is inescapable: no input is accepted while downloading, and only -// Enter is accepted once the download finishes. -type DownloadProgressPage struct { - Base - textShader *shaderManager.Shader - solidShader *shaderManager.Shader - - // Set by the application before pushing this page. - TotalBytes *atomic.Int64 - BytesWritten *atomic.Int64 - Done *atomic.Bool - HasError *atomic.Bool - NextPage Page // where to navigate on success - - state downloadState - title StringItem - prompt StringItem - percentText StringItem - lastPercent int - - barX, barY, barW, barH int32 -} - -func (p *DownloadProgressPage) Init(app ApplicationInterface) error { - if err := p.Base.Init(app); err != nil { - return fmt.Errorf("failed base page initialization: %w", err) - } - - fm := fontManager.Get() - sm := shaderManager.Get() - - font, ok := fm.GetFont("Roboto-Regular") - if !ok { - return fmt.Errorf("failed to get font") - } - - p.textShader, ok = sm.Get("text") - if !ok { - return fmt.Errorf("failed to get text shader") - } - p.solidShader, ok = sm.Get("solid_color") - if !ok { - return fmt.Errorf("failed to get solid_color shader") - } - - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) - if err != nil { - return fmt.Errorf("parsing init_font_size: %w", err) - } - - titleScale := float32(48) / float32(baseFontSize) - p.title, err = NewStringItem("Downloading Audio...", font, colors.White) - if err != nil { - return fmt.Errorf("creating title: %w", err) - } - p.title.Scale(titleScale) - - promptScale := float32(32) / float32(baseFontSize) - p.prompt, err = NewStringItem("Press Enter to continue", font, colors.White) - if err != nil { - return fmt.Errorf("creating prompt: %w", err) - } - p.prompt.Scale(promptScale) - - p.percentText, err = NewStringItem("0%", font, colors.White) - if err != nil { - return fmt.Errorf("creating percent text: %w", err) - } - p.percentText.Scale(promptScale) - p.lastPercent = 0 - - // Progress bar dimensions: 60% of screen width, 30px tall, centered - p.barW = int32(float32(p.ScreenWidth) * 0.6) - p.barH = 30 - p.barX = p.ScreenCenterX - p.barW/2 - p.barY = p.ScreenCenterY - p.barH/2 - - // Position title above bar - p.title.X = p.ScreenCenterX - p.title.W/2 - p.title.Y = p.barY + p.barH + 40 - - p.state = downloadStateDownloading - keybinds.Enabled = false - - return nil -} - -func (p *DownloadProgressPage) Update(dt float32) error { - if p.state == downloadStateFinished { - return nil - } - - if p.Done.Load() { - p.state = downloadStateFinished - - // Force progress to 100% on success - if !p.HasError.Load() && p.TotalBytes.Load() > 0 { - p.BytesWritten.Store(p.TotalBytes.Load()) - p.lastPercent = 100 - - fm := fontManager.Get() - font, ok := fm.GetFont("Roboto-Regular") - if ok { - gl.DeleteTextures(1, &p.percentText.ID) - baseFontSizeStr, _ := db.GetConfigValue("init_font_size") - baseFontSize, _ := strconv.Atoi(baseFontSizeStr) - percentScale := float32(32) / float32(baseFontSize) - newText, err := NewStringItem("100%", font, colors.White) - if err == nil { - newText.Scale(percentScale) - p.percentText = newText - } - } - } - - // Update title to reflect result - fm := fontManager.Get() - font, ok := fm.GetFont("Roboto-Regular") - if !ok { - return nil - } - - baseFontSizeStr, _ := db.GetConfigValue("init_font_size") - baseFontSize, _ := strconv.Atoi(baseFontSizeStr) - titleScale := float32(48) / float32(baseFontSize) - - gl.DeleteTextures(1, &p.title.ID) - - var titleText string - if p.HasError.Load() { - titleText = "Download Failed" - } else { - titleText = "Download Complete" - } - newTitle, err := NewStringItem(titleText, font, colors.White) - if err == nil { - newTitle.Scale(titleScale) - p.title = newTitle - p.title.X = p.ScreenCenterX - p.title.W/2 - p.title.Y = p.barY + p.barH + 40 - } - - return nil - } - - // Update percentage texture when it changes - percent := 0 - if p.TotalBytes.Load() > 0 { - percent = int(p.BytesWritten.Load() * 100 / p.TotalBytes.Load()) - if percent > 100 { - percent = 100 - } - } - - if percent != p.lastPercent { - p.lastPercent = percent - - fm := fontManager.Get() - font, ok := fm.GetFont("Roboto-Regular") - if !ok { - return nil - } - - gl.DeleteTextures(1, &p.percentText.ID) - - baseFontSizeStr, _ := db.GetConfigValue("init_font_size") - baseFontSize, _ := strconv.Atoi(baseFontSizeStr) - percentScale := float32(32) / float32(baseFontSize) - - newText, err := NewStringItem(fmt.Sprintf("%d%%", percent), font, colors.White) - if err == nil { - newText.Scale(percentScale) - p.percentText = newText - } - } - - return nil -} - -func (p *DownloadProgressPage) HandleEvent(event *sdl.Event) error { - // Inescapable while downloading - if p.state != downloadStateFinished { - return nil - } - - switch e := (*event).(type) { - case *sdl.KeyboardEvent: - if e.Type != sdl.KEYDOWN { - return nil - } - if e.Keysym.Sym == sdl.K_RETURN { - if p.HasError.Load() || p.NextPage == nil { - p.App.SchedulePop() - } else { - p.App.SwitchPage(p.NextPage) - } - } - } - return nil -} - -func (p *DownloadProgressPage) Render() error { - // Render title - p.RenderTexture(p.textShader, p.title.ID, p.title.Position(), p.title.RawDimensions(), p.title.Color) - - // Progress bar background (dark gray) - bgColor := sdl.Color{R: 60, G: 60, B: 60, A: 255} - barPos := mgl32.Vec2{float32(p.barX), float32(p.barY)} - barDim := mgl32.Vec2{float32(p.barW), float32(p.barH)} - p.RenderSolidColorQuad(p.solidShader, barPos, barDim, bgColor) - - // Progress bar foreground - progress := float32(0) - if p.TotalBytes.Load() > 0 { - progress = float32(p.BytesWritten.Load()) / float32(p.TotalBytes.Load()) - if progress > 1.0 { - progress = 1.0 - } - } - if progress > 0 { - fgColor := sdl.Color{R: 255, G: 255, B: 255, A: 255} - fgDim := mgl32.Vec2{float32(p.barW) * progress, float32(p.barH)} - p.RenderSolidColorQuad(p.solidShader, barPos, fgDim, fgColor) - } - - // Render percentage text below bar - p.percentText.X = p.ScreenCenterX - p.percentText.W/2 - p.percentText.Y = p.barY - p.percentText.H - 20 - p.RenderTexture(p.textShader, p.percentText.ID, p.percentText.Position(), p.percentText.RawDimensions(), p.percentText.Color) - - // Show prompt when finished - if p.state == downloadStateFinished { - p.prompt.X = p.ScreenCenterX - p.prompt.W/2 - p.prompt.Y = p.barY - p.percentText.H - 20 - p.prompt.H - 20 - p.RenderTexture(p.textShader, p.prompt.ID, p.prompt.Position(), p.prompt.RawDimensions(), p.prompt.Color) - } - - return nil -} - -func (p *DownloadProgressPage) Destroy() error { - keybinds.Enabled = true - gl.DeleteTextures(1, &p.title.ID) - gl.DeleteTextures(1, &p.prompt.ID) - gl.DeleteTextures(1, &p.percentText.ID) - return nil -} diff --git a/internal/page/emailAddressPage.go b/internal/page/emailAddressPage.go index 001f9e6..c27e20b 100644 --- a/internal/page/emailAddressPage.go +++ b/internal/page/emailAddressPage.go @@ -3,13 +3,11 @@ package page import ( "fmt" "regexp" - "strconv" "github.com/go-gl/gl/v3.3-core/gl" "github.com/go-gl/mathgl/mgl32" "github.com/veandco/go-sdl2/sdl" "rhemawavetech.com/rhemawave/internal/colors" - "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/fontManager" "rhemawavetech.com/rhemawave/internal/shaderManager" ) @@ -53,19 +51,15 @@ func isValidEmail(email string) bool { } // Init performs basic page initialization and creation of static assets. -func (p *EmailAddressPage) Init(app ApplicationInterface) error { +func (p *EmailAddressPage) Init(ctx *PageCtx) error { - if err := p.Base.Init(app); err != nil { + if err := p.Base.Init(ctx); err != nil { return fmt.Errorf("failed base page initialization: %w", err) } p.title = "Your Email Address" - // Get application font from database - applicationFont, err := db.GetConfigValue("application_font") - if err != nil { - return fmt.Errorf("retrieving application_font from db: %w", err) - } + applicationFont := "Roboto-Regular" fm := fontManager.Get() textShader, ok := shaderManager.Get().Get("text") @@ -87,10 +81,7 @@ func (p *EmailAddressPage) Init(app ApplicationInterface) error { p.titleTextureHeight = titleHeight // Load existing email address from database - emailAddress, err := db.GetConfigValue("email_address") - if err != nil { - return fmt.Errorf("retrieving email_address from db: %w", err) - } + emailAddress := ctx.CfgPtr.EmailAddress if emailAddress != "" { p.inputText = emailAddress @@ -148,7 +139,7 @@ func (p *EmailAddressPage) HandleEvent(event *sdl.Event) error { } // Allow escape back to settings if coming from settings sdl.StopTextInput() - p.App.UnwindToPage(&Settings{}) + p.Context.NavigateTo(SETTINGS) case sdl.K_RETURN: // Validate email - check empty first, then format if p.inputText == "" { @@ -163,16 +154,15 @@ func (p *EmailAddressPage) HandleEvent(event *sdl.Event) error { sdl.StopTextInput() // Save email address to database - if err := db.SetConfigValue("email_address", p.inputText); err != nil { - return fmt.Errorf("failed to save email address: %w", err) - } + p.Base.Context.CfgPtr.EmailAddress = p.inputText + p.Base.Context.DbPtr.Save(p.Base.Context.CfgPtr) + p.Base.Context.RefreshConfig() if p.isFirstTimeSetup { // First-time setup: navigate to License Key page - p.App.PushPage(&LicenseKeyPage{}) + p.Context.NavigateTo(LICENSE_KEY) } else { - // Coming from settings: rewind back to settings - p.App.UnwindToPage(&Settings{}) + p.Context.NavigateTo(SETTINGS) } case sdl.K_BACKSPACE: if len(p.inputText) > 0 { @@ -199,10 +189,7 @@ func (p *EmailAddressPage) HandleEvent(event *sdl.Event) error { // Update updates animations and changing textures and other changes using `dt` which represents a time delta in float32. func (p *EmailAddressPage) Update(dt float32) error { // Get application font from database - applicationFont, err := db.GetConfigValue("application_font") - if err != nil { - return fmt.Errorf("retrieving application_font from db: %w", err) - } + applicationFont := "Roboto-Regular" fm := fontManager.Get() sm := shaderManager.Get() @@ -266,14 +253,7 @@ func (p *EmailAddressPage) Update(dt float32) error { // Render performs page drawing logic. func (p *EmailAddressPage) Render() error { // Get base font size from database - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) - if err != nil { - return fmt.Errorf("parsing init_font_size: %w", err) - } + baseFontSize := p.Base.Context.CfgPtr.StandardFontSize sm := shaderManager.Get() diff --git a/internal/page/welcome.go b/internal/page/home.go similarity index 88% rename from internal/page/welcome.go rename to internal/page/home.go index 8123b19..120fdd0 100644 --- a/internal/page/welcome.go +++ b/internal/page/home.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "strconv" "strings" "time" @@ -12,14 +11,13 @@ import ( "github.com/go-gl/mathgl/mgl32" "github.com/veandco/go-sdl2/sdl" "rhemawavetech.com/rhemawave/internal/colors" - "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/fontManager" "rhemawavetech.com/rhemawave/internal/shaderManager" ) var GitVersion = "dev" -type Welcome struct { +type Home struct { Base title StringItem version StringItem @@ -70,7 +68,8 @@ func countSDLDisplays() int { } // updateDisplayCounts updates the display count textures -func (p *Welcome) updateDisplayCounts() error { +func (p *Home) updateDisplayCounts() error { + var err error // Get font fm := fontManager.Get() font, ok := fm.GetFont("Roboto-Regular") @@ -79,14 +78,7 @@ func (p *Welcome) updateDisplayCounts() error { } // Get base font size - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) - if err != nil { - return fmt.Errorf("parsing init_font_size: %w", err) - } + baseFontSize := p.Base.Context.CfgPtr.StandardFontSize // Update DRM count p.drmDisplayCount = countConnectedDisplays() @@ -127,14 +119,14 @@ func (p *Welcome) updateDisplayCounts() error { } // Init performs basic page initialization and creation of static assets. -func (p *Welcome) Init(app ApplicationInterface) error { +func (p *Home) Init(ctx *PageCtx) error { title := "Rhema Wave" version := GitVersion prompt := "Press F2 to Start, or F3 for Settings" note := "You can always press F1 to return here" - if err := p.Base.Init(app); err != nil { + if err := p.Base.Init(ctx); err != nil { return fmt.Errorf("failed base page initialization: %w", err) } @@ -177,19 +169,8 @@ func (p *Welcome) Init(app ApplicationInterface) error { return fmt.Errorf("unable to update display counts: %w", err) } - // Load license information from database - emailAddress, err := db.GetConfigValue("email_address") - if err != nil { - return fmt.Errorf("retrieving email_address from db: %w", err) - } - - licenseKey, err := db.GetConfigValue("license_key") - if err != nil { - return fmt.Errorf("retrieving license_key from db: %w", err) - } - // Create license info textures if data exists - if emailAddress != "" || licenseKey != "" { + if p.Base.Context.CfgPtr.EmailAddress != "" || p.Base.Context.CfgPtr.LicenseKey != "" { p.hasLicenseInfo = true // Create heading @@ -199,8 +180,8 @@ func (p *Welcome) Init(app ApplicationInterface) error { } // Create email display if exists - if emailAddress != "" { - emailText := fmt.Sprintf("Email: %s", emailAddress) + if p.Base.Context.CfgPtr.EmailAddress != "" { + emailText := fmt.Sprintf("Email: %s", p.Base.Context.CfgPtr.EmailAddress) p.emailDisplay, err = NewStringItem(emailText, font, colors.White) if err != nil { return fmt.Errorf("unable to create email display texture: %w", err) @@ -208,8 +189,8 @@ func (p *Welcome) Init(app ApplicationInterface) error { } // Create license key display if exists - if licenseKey != "" { - licenseKeyText := fmt.Sprintf("License Key: %s", licenseKey) + if p.Base.Context.CfgPtr.LicenseKey != "" { + licenseKeyText := fmt.Sprintf("License Key: %s", p.Base.Context.CfgPtr.LicenseKey) p.licenseKeyDisplay, err = NewStringItem(licenseKeyText, font, colors.White) if err != nil { return fmt.Errorf("unable to create license key display texture: %w", err) @@ -218,14 +199,7 @@ func (p *Welcome) Init(app ApplicationInterface) error { } // Get base font size from database - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) - if err != nil { - return fmt.Errorf("parsing init_font_size: %w", err) - } + baseFontSize := p.Base.Context.CfgPtr.StandardFontSize p.baseFontSize = baseFontSize @@ -249,13 +223,13 @@ func (p *Welcome) Init(app ApplicationInterface) error { return nil } -// HandleEvent processes SDL events and handles them appropriately for the Welcome page. -func (p *Welcome) HandleEvent(event *sdl.Event) error { +// HandleEvent processes SDL events and handles them appropriately for the Home page. +func (p *Home) HandleEvent(event *sdl.Event) error { return nil } // Update updates animations and changing textures and other changes using `dt` which represents a time delta in float32. -func (p *Welcome) Update(dt float32) error { +func (p *Home) Update(dt float32) error { // Update display counts every 2 seconds if time.Since(p.lastDisplayUpdate) >= 2*time.Second { if err := p.updateDisplayCounts(); err != nil { @@ -265,7 +239,7 @@ func (p *Welcome) Update(dt float32) error { return nil } -func (p *Welcome) Render() error { +func (p *Home) Render() error { baseFontSize := p.baseFontSize sm := shaderManager.Get() verticalMargin := int32(20) @@ -417,7 +391,7 @@ func (p *Welcome) Render() error { } // Destroy cleans up page-specific resources. -func (p *Welcome) Destroy() error { +func (p *Home) Destroy() error { gl.DeleteTextures(1, &p.title.ID) gl.DeleteTextures(1, &p.version.ID) gl.DeleteTextures(1, &p.prompt.ID) diff --git a/internal/page/licenseKeyPage.go b/internal/page/licenseKeyPage.go index 0283f1b..8bf1061 100644 --- a/internal/page/licenseKeyPage.go +++ b/internal/page/licenseKeyPage.go @@ -4,14 +4,12 @@ import ( "fmt" "os" "path/filepath" - "strconv" "strings" "github.com/go-gl/gl/v3.3-core/gl" "github.com/go-gl/mathgl/mgl32" "github.com/veandco/go-sdl2/sdl" "rhemawavetech.com/rhemawave/internal/colors" - "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/fontManager" "rhemawavetech.com/rhemawave/internal/logger" "rhemawavetech.com/rhemawave/internal/shaderManager" @@ -47,34 +45,17 @@ type LicenseKeyPage struct { textInputSupported bool } -// isValidLicenseKey checks if the license key has exactly 16 alphanumeric characters -func isValidLicenseKey(key string) bool { - cleaned := strings.ReplaceAll(key, "-", "") - if len(cleaned) != 16 { - return false - } - for _, r := range cleaned { - if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) { - return false - } - } - return true -} - // Init performs basic page initialization and creation of static assets. -func (p *LicenseKeyPage) Init(app ApplicationInterface) error { +func (p *LicenseKeyPage) Init(ctx *PageCtx) error { - if err := p.Base.Init(app); err != nil { + if err := p.Base.Init(ctx); err != nil { return fmt.Errorf("failed base page initialization: %w", err) } p.title = "License Key" // Get application font from database - applicationFont, err := db.GetConfigValue("application_font") - if err != nil { - return fmt.Errorf("retrieving application_font from db: %w", err) - } + applicationFont := "Roboto-Regular" fm := fontManager.Get() textShader, ok := shaderManager.Get().Get("text") @@ -96,10 +77,7 @@ func (p *LicenseKeyPage) Init(app ApplicationInterface) error { p.titleTextureHeight = titleHeight // Load existing license key from database - licenseKey, err := db.GetConfigValue("license_key") - if err != nil { - return fmt.Errorf("retrieving license_key from db: %w", err) - } + licenseKey := p.Base.Context.CfgPtr.LicenseKey if licenseKey != "" { p.inputText = licenseKey @@ -187,7 +165,7 @@ func (p *LicenseKeyPage) HandleEvent(event *sdl.Event) error { } // Allow escape back to settings if coming from settings sdl.StopTextInput() - p.App.UnwindToPage(&Settings{}) + p.Base.Context.NavigateTo(SETTINGS) case sdl.K_RETURN: // Validate license key - check empty first, then length, then characters cleaned := strings.ReplaceAll(p.inputText, "-", "") @@ -210,9 +188,9 @@ func (p *LicenseKeyPage) HandleEvent(event *sdl.Event) error { sdl.StopTextInput() // Save license key to database - if err := db.SetConfigValue("license_key", p.inputText); err != nil { - return fmt.Errorf("failed to save license key: %w", err) - } + p.Base.Context.CfgPtr.LicenseKey = p.inputText + p.Base.Context.DbPtr.Save(p.Base.Context.CfgPtr) + p.Base.Context.RefreshConfig() // Write license key to plain text file for the updater if homeDir, err := os.UserHomeDir(); err == nil { @@ -224,10 +202,10 @@ func (p *LicenseKeyPage) HandleEvent(event *sdl.Event) error { if p.isFirstTimeSetup { // First-time setup: rewind to Welcome page - p.App.UnwindToPage(&Welcome{}) + p.Base.Context.NavigateTo(HOME) } else { // Coming from settings: rewind back to settings - p.App.UnwindToPage(&Settings{}) + p.Base.Context.NavigateTo(SETTINGS) } case sdl.K_BACKSPACE: if len(p.inputText) > 0 { @@ -261,10 +239,7 @@ func (p *LicenseKeyPage) HandleEvent(event *sdl.Event) error { // Update updates animations and changing textures and other changes using `dt` which represents a time delta in float32. func (p *LicenseKeyPage) Update(dt float32) error { // Get application font from database - applicationFont, err := db.GetConfigValue("application_font") - if err != nil { - return fmt.Errorf("retrieving application_font from db: %w", err) - } + applicationFont := "Roboto-Regular" fm := fontManager.Get() sm := shaderManager.Get() @@ -328,14 +303,7 @@ func (p *LicenseKeyPage) Update(dt float32) error { // Render performs page drawing logic. func (p *LicenseKeyPage) Render() error { // Get base font size from database - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) - if err != nil { - return fmt.Errorf("parsing init_font_size: %w", err) - } + baseFontSize := p.Base.Context.CfgPtr.StandardFontSize sm := shaderManager.Get() diff --git a/internal/page/logViewerPage.go b/internal/page/logViewerPage.go index 6f8878e..7470d21 100644 --- a/internal/page/logViewerPage.go +++ b/internal/page/logViewerPage.go @@ -2,14 +2,15 @@ package page import ( "fmt" - "strconv" "strings" + "time" "github.com/go-gl/gl/v3.3-core/gl" "github.com/go-gl/mathgl/mgl32" "github.com/veandco/go-sdl2/sdl" "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/fontManager" + "rhemawavetech.com/rhemawave/internal/logger" "rhemawavetech.com/rhemawave/internal/shaderManager" ) @@ -35,27 +36,32 @@ type LogViewerPage struct { visibleHeight int32 } -func (p *LogViewerPage) Init(app ApplicationInterface) error { - if err := p.Base.Init(app); err != nil { +func (p *LogViewerPage) Init(ctx *PageCtx) error { + var logs []db.Log + if err := p.Base.Init(ctx); err != nil { return fmt.Errorf("failed base page initialization: %w", err) } - logs, err := db.GetLogs(1000) - if err != nil { - return fmt.Errorf("failed to get logs: %w", err) + if result := p.Base.Context.DbPtr.Order("timestamp desc").Limit(250).Find(&logs); result.Error != nil { + logger.Error("error querying database for logs: %s", result.Error) + return fmt.Errorf("%s", result.Error) } // Format and word-wrap each log entry + // for _, entry := range logs { + // formatted := fmt.Sprintf("[%s] [%s] %s", entry.Timestamp, entry.Level, entry.Message) + // wrapped := wrapText(formatted, 100) + // p.lines = append(p.lines, strings.Split(wrapped, "\n")...) + // } + for _, entry := range logs { - formatted := fmt.Sprintf("[%s] [%s] %s", entry.Timestamp, entry.Level, entry.Message) - wrapped := wrapText(formatted, 100) - p.lines = append(p.lines, strings.Split(wrapped, "\n")...) + logTime := time.UnixMilli(entry.Timestamp) + timeStr := logTime.Format("2006-01-02 15:04:05") + line := fmt.Sprintf("[%s] [%s] %s", timeStr, entry.Level, entry.Message) + p.lines = append(p.lines, line) } - applicationFont, err := db.GetConfigValue("application_font") - if err != nil { - return fmt.Errorf("retrieving application_font from db: %w", err) - } + applicationFont := "Roboto-Regular" fm := fontManager.Get() textShader, ok := shaderManager.Get().Get("text") @@ -109,7 +115,7 @@ func (p *LogViewerPage) HandleEvent(event *sdl.Event) error { if e.Type == sdl.KEYDOWN { switch e.Keysym.Sym { case sdl.K_BACKSPACE: - p.App.SchedulePop() + p.Base.Context.NavigateTo(SETTINGS) case sdl.K_UP: p.scrollOffset -= p.lineHeight if p.scrollOffset < 0 { @@ -153,14 +159,7 @@ func (p *LogViewerPage) Update(dt float32) error { } func (p *LogViewerPage) Render() error { - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) - if err != nil { - return fmt.Errorf("parsing init_font_size: %w", err) - } + baseFontSize := p.Base.Context.CfgPtr.StandardFontSize sm := shaderManager.Get() textShader, _ := sm.Get("text") diff --git a/internal/page/page.go b/internal/page/page.go index aa89f29..b3c0e65 100644 --- a/internal/page/page.go +++ b/internal/page/page.go @@ -12,29 +12,16 @@ import ( // Page defines the contract for all application screens. type Page interface { - Init(app ApplicationInterface) error + Init(ctx *PageCtx) error HandleEvent(event *sdl.Event) error Update(dt float32) error Render() error - // Destroy cleans up the page's resources. The Application's page manager - // is responsible for calling this method Destroy() error } -// ApplicationInterface defines the set of methods that a page can use to -// interact with the main application. -type ApplicationInterface interface { - SwitchPage(p Page) - PushPage(p Page) - UnwindToPage(p Page) - GetDrawableSize() (int32, int32) - SchedulePop() - Quit() -} - // Base is a helper struct that pages can embed to get common functionality. type Base struct { - App ApplicationInterface + Context *PageCtx QuadVAO uint32 QuadVBO uint32 Projection mgl32.Mat4 @@ -45,9 +32,8 @@ type Base struct { } // Init stores the application context and sets up common resources. -func (p *Base) Init(app ApplicationInterface) error { - p.App = app - +func (p *Base) Init(ctx *PageCtx) error { + p.Context = ctx vertices := []float32{ // pos(x,y) tex(u,v) 0.0, 1.0, 0.0, 1.0, @@ -59,12 +45,14 @@ func (p *Base) Init(app ApplicationInterface) error { 1.0, 0.0, 1.0, 0.0, } - screenWidth, screenHeight := p.App.GetDrawableSize() - p.Projection = mgl32.Ortho(0, float32(screenWidth), 0, float32(screenHeight), -1, 1) - p.ScreenHeight = screenHeight - p.ScreenWidth = screenWidth - p.ScreenCenterX = screenWidth / 2 - p.ScreenCenterY = screenHeight / 2 + W := p.Context.W + H := p.Context.H + + p.Projection = mgl32.Ortho(0, float32(W), 0, float32(H), -1, 1) + p.ScreenHeight = H + p.ScreenWidth = W + p.ScreenCenterY = H / 2 + p.ScreenCenterX = W / 2 gl.GenVertexArrays(1, &p.QuadVAO) gl.GenBuffers(1, &p.QuadVBO) diff --git a/internal/page/proclamationOptions.go b/internal/page/proclamationOptions.go index d9d1516..414f333 100644 --- a/internal/page/proclamationOptions.go +++ b/internal/page/proclamationOptions.go @@ -2,35 +2,35 @@ package page import ( "fmt" - "strconv" "github.com/go-gl/gl/v3.3-core/gl" "github.com/veandco/go-sdl2/sdl" "rhemawavetech.com/rhemawave/internal/colors" "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/fontManager" + "rhemawavetech.com/rhemawave/internal/logger" "rhemawavetech.com/rhemawave/internal/shaderManager" ) type ProclamationOptions struct { Base - font *fontManager.FontEntry - textShader *shaderManager.Shader - solidShader *shaderManager.Shader - title StringItem - prompt StringItem + font *fontManager.FontEntry + textShader *shaderManager.Shader + solidShader *shaderManager.Shader + title StringItem + prompt StringItem proclamations []StringItem - proclamationIDs []uint // Database IDs corresponding to each proclamation - activeMap map[int]bool // Maps display index to selected state - highlightIndex int - verticalMargin int32 + proclamationIDs []string + activeMap map[int]bool // Maps display index to selected state + highlightIndex int + verticalMargin int32 } -func (p *ProclamationOptions) Init(app ApplicationInterface) error { +func (p *ProclamationOptions) Init(ctx *PageCtx) error { var err error p.verticalMargin = int32(20) p.activeMap = make(map[int]bool) - p.proclamationIDs = make([]uint, 0) + p.proclamationIDs = make([]string, 0) // Get font fm := fontManager.Get() @@ -53,19 +53,12 @@ func (p *ProclamationOptions) Init(app ApplicationInterface) error { } // Initialize base - if err := p.Base.Init(app); err != nil { + if err := p.Base.Init(ctx); err != nil { return fmt.Errorf("base init failed: %w", err) } // Get font size from database - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) - if err != nil { - return fmt.Errorf("parsing init_font_size %q: %w", baseFontSizeStr, err) - } + baseFontSize := ctx.CfgPtr.StandardFontSize // Calculate scales titleScale := float32(64) / float32(baseFontSize) @@ -86,9 +79,10 @@ func (p *ProclamationOptions) Init(app ApplicationInterface) error { p.prompt.Scale(itemScale) // Load proclamations from database - proclamations, err := db.GetProclamations() - if err != nil { - return fmt.Errorf("retrieving proclamations from db: %w", err) + var proclamations []db.Proclamation + if result := p.Context.DbPtr.Find(&proclamations); result.Error != nil { + logger.Error("unable to query database for proclamations: %v", result.Error) + return fmt.Errorf("%s", result.Error) } // Create UI items for each available proclamation @@ -97,7 +91,7 @@ func (p *ProclamationOptions) Init(app ApplicationInterface) error { continue } - label, err := NewStringItem(proclamation.Title, p.font, colors.White) + label, err := NewStringItem(proclamation.Name, p.font, colors.White) if err != nil { return fmt.Errorf("creating proclamation label: %w", err) } @@ -152,7 +146,7 @@ func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error { p.highlightIndex++ } case sdl.K_BACKSPACE: - p.App.UnwindToPage(&Settings{}) + p.Context.NavigateTo(SETTINGS) case sdl.K_SPACE: // Toggle selection for highlighted item if p.activeMap[p.highlightIndex] { @@ -167,7 +161,7 @@ func (p *ProclamationOptions) HandleEvent(event *sdl.Event) error { } case sdl.K_RETURN: // Just return to settings - no saving needed - p.App.UnwindToPage(&Settings{}) + p.Context.NavigateTo(SETTINGS) } } return nil @@ -183,15 +177,17 @@ func (p *ProclamationOptions) toggleProclamation(displayIndex int) error { isSelected := p.activeMap[displayIndex] // Get the proclamation from database - var proclamation db.Proclamations - if err := db.DB.First(&proclamation, dbID).Error; err != nil { - return fmt.Errorf("retrieving proclamation %d: %w", dbID, err) + var proclamation db.Proclamation + proclamation.ID = dbID + if result := p.Context.DbPtr.Find(&proclamation); result.Error != nil { + logger.Error("unable to query proclamation %s from database: %v", proclamation.ID, result.Error) } // Update the Selected field proclamation.Selected = isSelected - if err := db.DB.Save(&proclamation).Error; err != nil { - return fmt.Errorf("updating proclamation %d: %w", dbID, err) + if result := p.Context.DbPtr.Save(&proclamation); result.Error != nil { + logger.Error("failed to update proclamation %s: %v", proclamation.ID, result.Error) + return fmt.Errorf("%s", result.Error) } return nil diff --git a/internal/page/router.go b/internal/page/router.go new file mode 100644 index 0000000..53236cd --- /dev/null +++ b/internal/page/router.go @@ -0,0 +1,75 @@ +package page + +import ( + "gorm.io/gorm" + "rhemawavetech.com/rhemawave/internal/db" +) + +type pageType int + +const ( + HOME pageType = iota + SCROLLER + SETTINGS + WIFI + AUDIO + PROCLAMATIONS + LICENSE_KEY + EMAIL_ADDRESS + LOGS + CHECK_UPDATES + ERROR +) + +type PageCtx struct { + CfgPtr *db.AppConfig + DbPtr *gorm.DB + W, H int32 + RefreshConfig func() + NavigateTo func(pageType) +} + +type Router struct { + ActivePage Page + Context PageCtx +} + +func NewRouter(pgCtx PageCtx) *Router { + r := &Router{ + Context: pgCtx, + } + r.Context.NavigateTo = r.NavigateTo + r.NavigateTo(HOME) + return r +} + +func (r *Router) NavigateTo(p pageType) { + if r.ActivePage != nil { + r.ActivePage.Destroy() + } + + switch p { + case HOME: + r.ActivePage = &Home{} + case SCROLLER: + r.ActivePage = &ScrollerPage{} + case SETTINGS: + r.ActivePage = &Settings{} + case WIFI: + r.ActivePage = &WiFiSetupPage{} + case AUDIO: + r.ActivePage = &AudioDevices{} + case PROCLAMATIONS: + r.ActivePage = &ProclamationOptions{} + case LICENSE_KEY: + r.ActivePage = &LicenseKeyPage{} + case EMAIL_ADDRESS: + r.ActivePage = &EmailAddressPage{} + case LOGS: + r.ActivePage = &LogViewerPage{} + case CHECK_UPDATES: + r.ActivePage = &UpdateCheckPage{} + } + + r.ActivePage.Init(&r.Context) +} diff --git a/internal/page/scrollerPage.go b/internal/page/scrollerPage.go index e12304b..bf80f31 100644 --- a/internal/page/scrollerPage.go +++ b/internal/page/scrollerPage.go @@ -60,7 +60,6 @@ import ( "fmt" "math" "path/filepath" - "strconv" "strings" "github.com/go-gl/gl/v3.3-core/gl" @@ -194,7 +193,7 @@ type ScrollerPage struct { hold float32 } -func (p *ScrollerPage) Init(app ApplicationInterface) error { +func (p *ScrollerPage) Init(ctx *PageCtx) error { p.increment = 100.0 p.velocity = 800.0 p.SetAutoVelocity(true, -4000.0, 4000.0, 100.0, 2.5, 6.0) @@ -209,7 +208,7 @@ func (p *ScrollerPage) Init(app ApplicationInterface) error { } p.font = font - p.Base.Init(app) + p.Base.Init(ctx) // Get text shader textShader, ok := sm.Get("text") @@ -226,10 +225,8 @@ func (p *ScrollerPage) Init(app ApplicationInterface) error { p.wordIndex = 0 // Get line pattern from database - linePatternName, err := db.GetConfigValue("line_pattern") - if err != nil { - return fmt.Errorf("retrieving line_pattern from db: %w", err) - } + var err error + linePatternName := p.Base.Context.CfgPtr.LinePattern p.activePattern, err = linePattern.Get(linePatternName) if err != nil { @@ -249,23 +246,8 @@ func (p *ScrollerPage) Init(app ApplicationInterface) error { p.colorPattern = colorPattern.NewColorPattern(baseColors) // Get font sizes from database (optimize: load once, cache as int32) - standardFontSizeStr, err := db.GetConfigValue("standard_font_size") - if err != nil { - return fmt.Errorf("retrieving standard_font_size from db: %w", err) - } - standardFontSize, err := strconv.Atoi(standardFontSizeStr) - if err != nil { - return fmt.Errorf("parsing standard_font_size: %w", err) - } - - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) - if err != nil { - return fmt.Errorf("parsing init_font_size: %w", err) - } + standardFontSize := p.Base.Context.CfgPtr.StandardFontSize + baseFontSize := p.Base.Context.CfgPtr.StandardFontSize // Font setup (optimized: calculate once, reuse) p.fontScale = float32(standardFontSize) / float32(baseFontSize) @@ -302,10 +284,7 @@ func (p *ScrollerPage) Init(app ApplicationInterface) error { } // Audio setup - audioDeviceName, err := db.GetConfigValue("audio_device_name") - if err != nil { - return fmt.Errorf("retrieving audio_device_name from db: %w", err) - } + audioDeviceName := p.Base.Context.CfgPtr.SelectedAudioDevice // Switch audio devices if needed if mixer.CurrentDevice() != audioDeviceName { @@ -315,19 +294,13 @@ func (p *ScrollerPage) Init(app ApplicationInterface) error { } // Start audio playback - selectedAudio, err := db.GetConfigValue("selected_audio") - if err != nil { - return fmt.Errorf("retrieving selected_audio from db: %w", err) - } + selectedAudio := p.Base.Context.CfgPtr.SelectedAudio - assetsDir, err := db.GetConfigValue("assets_dir") - if err != nil { - return fmt.Errorf("retrieving assets_dir from db: %w", err) - } + assetsDir := p.Base.Context.CfgPtr.AssetsDir if selectedAudio == "" { } else { - if err := mixer.Play(filepath.Join(assetsDir, selectedAudio), true); err != nil { + if err := mixer.Play(filepath.Join(assetsDir, selectedAudio), true, p.Base.Context.CfgPtr); err != nil { } } @@ -344,10 +317,8 @@ func (p *ScrollerPage) loadProclamations() error { p.words = []string{} // Get all selected proclamations from database - proclamations, err := db.GetProclamations() - if err != nil { - return fmt.Errorf("retrieving proclamations from db: %w", err) - } + var proclamations []db.Proclamation + p.Base.Context.DbPtr.Find(&proclamations) // Build word list from selected, available proclamations for _, proclamation := range proclamations { @@ -614,7 +585,7 @@ func (p *ScrollerPage) Destroy() error { return nil } - mixer.Stop(1000) + mixer.Stop(1000, p.Context.CfgPtr) // GL cleanup — delete all text textures for i := 0; i < p.lines.Len(); i++ { diff --git a/internal/page/settingsPage.go b/internal/page/settingsPage.go index 6ba9ee4..7d0d6e3 100644 --- a/internal/page/settingsPage.go +++ b/internal/page/settingsPage.go @@ -2,12 +2,10 @@ package page import ( "fmt" - "strconv" "github.com/go-gl/gl/v3.3-core/gl" "github.com/veandco/go-sdl2/sdl" "rhemawavetech.com/rhemawave/internal/colors" - "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/fontManager" "rhemawavetech.com/rhemawave/internal/shaderManager" ) @@ -26,11 +24,11 @@ type Settings struct { } type Option struct { - page Page - label StringItem + target pageType + label StringItem } -func (p *Settings) Init(app ApplicationInterface) error { +func (p *Settings) Init(ctx *PageCtx) error { var err error p.verticalMargin = int32(20) @@ -54,7 +52,7 @@ func (p *Settings) Init(app ApplicationInterface) error { } // Initialize base - if err := p.Base.Init(app); err != nil { + if err := p.Base.Init(ctx); err != nil { return fmt.Errorf("failed base page initialization: %w", err) } @@ -76,8 +74,8 @@ func (p *Settings) Init(app ApplicationInterface) error { return fmt.Errorf("creating proclamations label: %w", err) } p.options = append(p.options, Option{ - page: &ProclamationOptions{}, - label: proclamationsLabel, + target: PROCLAMATIONS, + label: proclamationsLabel, }) wifiLabel, err := NewStringItem("WiFi Setup", font, colors.White) @@ -85,8 +83,8 @@ func (p *Settings) Init(app ApplicationInterface) error { return fmt.Errorf("creating wifi label: %w", err) } p.options = append(p.options, Option{ - page: &WiFiSetupPage{}, - label: wifiLabel, + target: WIFI, + label: wifiLabel, }) audioLabel, err := NewStringItem("Audio Device", font, colors.White) @@ -94,8 +92,8 @@ func (p *Settings) Init(app ApplicationInterface) error { return fmt.Errorf("creating audio label: %w", err) } p.options = append(p.options, Option{ - page: &AudioDevices{}, - label: audioLabel, + target: AUDIO, + label: audioLabel, }) emailLabel, err := NewStringItem("Email Address", font, colors.White) @@ -103,8 +101,8 @@ func (p *Settings) Init(app ApplicationInterface) error { return fmt.Errorf("creating email label: %w", err) } p.options = append(p.options, Option{ - page: &EmailAddressPage{}, - label: emailLabel, + target: EMAIL_ADDRESS, + label: emailLabel, }) licenseKeyLabel, err := NewStringItem("License Key", font, colors.White) @@ -112,8 +110,8 @@ func (p *Settings) Init(app ApplicationInterface) error { return fmt.Errorf("creating license key label: %w", err) } p.options = append(p.options, Option{ - page: &LicenseKeyPage{}, - label: licenseKeyLabel, + target: LICENSE_KEY, + label: licenseKeyLabel, }) logsLabel, err := NewStringItem("Logs", font, colors.White) @@ -121,8 +119,8 @@ func (p *Settings) Init(app ApplicationInterface) error { return fmt.Errorf("creating logs label: %w", err) } p.options = append(p.options, Option{ - page: &LogViewerPage{}, - label: logsLabel, + target: LOGS, + label: logsLabel, }) updateLabel, err := NewStringItem("Check for Updates", font, colors.White) @@ -130,19 +128,12 @@ func (p *Settings) Init(app ApplicationInterface) error { return fmt.Errorf("creating update label: %w", err) } p.options = append(p.options, Option{ - page: &UpdateCheckPage{}, - label: updateLabel, + target: CHECK_UPDATES, + label: updateLabel, }) // Get base font size from database - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) - if err != nil { - return fmt.Errorf("parsing init_font_size: %w", err) - } + baseFontSize := p.Context.CfgPtr.StandardFontSize // Calculate scales titleScale := float32(64) / float32(baseFontSize) @@ -208,10 +199,10 @@ func (p *Settings) HandleEvent(event *sdl.Event) error { case sdl.K_RETURN: if p.highlightIndex >= 0 && p.highlightIndex < len(p.options) { selectedOption := p.options[p.highlightIndex] - p.App.SwitchPage(selectedOption.page) + p.Context.NavigateTo(selectedOption.target) } case sdl.K_BACKSPACE: - p.App.SwitchPage(&Welcome{}) + p.Context.NavigateTo(HOME) } } return nil diff --git a/internal/page/tosPage.go b/internal/page/tosPage.go deleted file mode 100644 index 1e549a0..0000000 --- a/internal/page/tosPage.go +++ /dev/null @@ -1,388 +0,0 @@ -package page - -import ( - "fmt" - "strconv" - "strings" - - "github.com/go-gl/gl/v3.3-core/gl" - "github.com/go-gl/mathgl/mgl32" - "github.com/veandco/go-sdl2/sdl" - "rhemawavetech.com/rhemawave/internal/db" - "rhemawavetech.com/rhemawave/internal/fontManager" - "rhemawavetech.com/rhemawave/internal/logger" - "rhemawavetech.com/rhemawave/internal/shaderManager" -) - -type TOSPage struct { - Base - - Title string - Content string - ConfigKey string - - titleTextureID uint32 - titleTextureWidth int32 - titleTextureHeight int32 - - lines []string - lineTextureIDs []uint32 - lineTextureWidths []int32 - lineTextureHeights []int32 - - promptTextureID uint32 - promptTextureWidth int32 - promptTextureHeight int32 - - scrollHintTextureID uint32 - scrollHintTextureWidth int32 - scrollHintTextureHeight int32 - - scrollOffset int32 - maxScrollOffset int32 - lineHeight int32 - visibleHeight int32 -} - -func (p *TOSPage) Init(app ApplicationInterface) error { - if err := p.Base.Init(app); err != nil { - return fmt.Errorf("failed base page initialization: %w", err) - } - - p.lines = parseHTMLToLines(p.Content, 60) - - applicationFont, err := db.GetConfigValue("application_font") - if err != nil { - return fmt.Errorf("retrieving application_font from db: %w", err) - } - - fm := fontManager.Get() - textShader, ok := shaderManager.Get().Get("text") - if !ok { - return fmt.Errorf("unable to fetch text shader") - } - - titleTextureID, titleWidth, titleHeight, err := fm.CreateStringTexture(applicationFont, p.Title, textShader) - if err != nil { - return fmt.Errorf("failed to create title texture: %w", err) - } - p.titleTextureID = titleTextureID - p.titleTextureWidth = titleWidth - p.titleTextureHeight = titleHeight - - p.lineTextureIDs = make([]uint32, len(p.lines)) - p.lineTextureWidths = make([]int32, len(p.lines)) - p.lineTextureHeights = make([]int32, len(p.lines)) - - for i, line := range p.lines { - if strings.TrimSpace(line) == "" { - continue - } - texID, w, h, err := fm.CreateStringTexture(applicationFont, line, textShader) - if err != nil { - return fmt.Errorf("failed to create line texture: %w", err) - } - p.lineTextureIDs[i] = texID - p.lineTextureWidths[i] = w - p.lineTextureHeights[i] = h - } - - promptText := "Press Y to Accept | Press N to Decline" - promptTextureID, promptWidth, promptHeight, err := fm.CreateStringTexture(applicationFont, promptText, textShader) - if err != nil { - return fmt.Errorf("failed to create prompt texture: %w", err) - } - p.promptTextureID = promptTextureID - p.promptTextureWidth = promptWidth - p.promptTextureHeight = promptHeight - - scrollHintText := "Scroll down: Arrow Keys / PgDn" - scrollHintID, scrollHintW, scrollHintH, err := fm.CreateStringTexture(applicationFont, scrollHintText, textShader) - if err != nil { - return fmt.Errorf("failed to create scroll hint texture: %w", err) - } - p.scrollHintTextureID = scrollHintID - p.scrollHintTextureWidth = scrollHintW - p.scrollHintTextureHeight = scrollHintH - - return nil -} - -// parseHTMLToLines converts HTML content to plain-text lines for rendering. -// Supported tags:

,
,

  • ,

    -

    . All other tags are stripped. -// HTML entities & < > " ' are decoded. -func parseHTMLToLines(html string, maxCharsPerLine int) []string { - var lines []string - - // Replace
    ,
    ,
    with newline markers - br := strings.NewReplacer( - "
    ", "\n", "
    ", "\n", "
    ", "\n", - "
    ", "\n", "
    ", "\n", "
    ", "\n", - ) - html = br.Replace(html) - - // Process block-level tags - // Replace

    ,
    - with paragraph break marker - for _, tag := range []string{"

    ", "", "", "", "", "", ""} { - html = strings.ReplaceAll(html, tag, "\n\n") - html = strings.ReplaceAll(html, strings.ToUpper(tag), "\n\n") - } - - // Replace
  • with list prefix marker - html = strings.ReplaceAll(html, "
  • ", "\n- ") - html = strings.ReplaceAll(html, "
  • ", "\n- ") - - // Strip all remaining HTML tags - var stripped strings.Builder - inTag := false - for _, r := range html { - if r == '<' { - inTag = true - continue - } - if r == '>' { - inTag = false - continue - } - if !inTag { - stripped.WriteRune(r) - } - } - text := stripped.String() - - // Decode HTML entities - text = strings.ReplaceAll(text, "&", "&") - text = strings.ReplaceAll(text, "<", "<") - text = strings.ReplaceAll(text, ">", ">") - text = strings.ReplaceAll(text, """, "\"") - text = strings.ReplaceAll(text, "'", "'") - - // Split into segments on double-newline (paragraph breaks) and single newline (line breaks) - paragraphs := strings.Split(text, "\n\n") - for i, para := range paragraphs { - para = strings.TrimSpace(para) - if para == "" { - continue - } - - // Handle single newlines within a paragraph (from
    tags) - sublines := strings.Split(para, "\n") - for _, sub := range sublines { - sub = strings.TrimSpace(sub) - if sub == "" { - continue - } - wrapped := wrapText(sub, maxCharsPerLine) - lines = append(lines, strings.Split(wrapped, "\n")...) - } - - // Add paragraph break (empty line) between paragraphs - if i < len(paragraphs)-1 { - lines = append(lines, "") - } - } - - return lines -} - -func wrapText(text string, maxCharsPerLine int) string { - var result strings.Builder - words := strings.Fields(text) - lineLen := 0 - - for _, word := range words { - if lineLen+len(word)+1 > maxCharsPerLine && lineLen > 0 { - result.WriteString("\n") - lineLen = 0 - } - if lineLen > 0 { - result.WriteString(" ") - lineLen++ - } - result.WriteString(word) - lineLen += len(word) - } - return result.String() -} - -func (p *TOSPage) HandleEvent(event *sdl.Event) error { - actualEvent := *event - - switch e := actualEvent.(type) { - case *sdl.KeyboardEvent: - if e.Type == sdl.KEYDOWN { - switch e.Keysym.Sym { - case sdl.K_n: - logger.Info("User declined TOS (%s); exiting", p.ConfigKey) - p.App.Quit() - case sdl.K_y: - if p.scrollOffset < p.maxScrollOffset { - return nil - } - logger.Info("User accepted TOS (%s)", p.ConfigKey) - if err := db.SetConfigValue(p.ConfigKey, "true"); err != nil { - return fmt.Errorf("failed to save TOS acceptance (%s): %w", p.ConfigKey, err) - } - p.App.SchedulePop() - case sdl.K_UP: - p.scrollOffset -= p.lineHeight - if p.scrollOffset < 0 { - p.scrollOffset = 0 - } - case sdl.K_DOWN: - p.scrollOffset += p.lineHeight - if p.scrollOffset > p.maxScrollOffset { - p.scrollOffset = p.maxScrollOffset - } - case sdl.K_PAGEUP: - p.scrollOffset -= p.visibleHeight - if p.scrollOffset < 0 { - p.scrollOffset = 0 - } - case sdl.K_PAGEDOWN: - p.scrollOffset += p.visibleHeight - if p.scrollOffset > p.maxScrollOffset { - p.scrollOffset = p.maxScrollOffset - } - case sdl.K_HOME: - p.scrollOffset = 0 - case sdl.K_END: - p.scrollOffset = p.maxScrollOffset - } - } - case *sdl.MouseWheelEvent: - p.scrollOffset -= e.Y * p.lineHeight * 3 - if p.scrollOffset < 0 { - p.scrollOffset = 0 - } - if p.scrollOffset > p.maxScrollOffset { - p.scrollOffset = p.maxScrollOffset - } - } - - return nil -} - -func (p *TOSPage) Update(dt float32) error { - return nil -} - -func (p *TOSPage) Render() error { - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) - if err != nil { - return fmt.Errorf("parsing init_font_size: %w", err) - } - - sm := shaderManager.Get() - textShader, _ := sm.Get("text") - - white := sdl.Color{R: 255, G: 255, B: 255, A: 255} - grey := sdl.Color{R: 180, G: 180, B: 180, A: 255} - - titleFontSize := int32(48) - titleScale := float32(titleFontSize) / float32(baseFontSize) - contentFontSize := int32(32) - contentScale := float32(contentFontSize) / float32(baseFontSize) - promptFontSize := int32(20) - promptScale := float32(promptFontSize) / float32(baseFontSize) - - margin := int32(40) - lineSpacing := int32(4) - paragraphSpacing := int32(16) - p.lineHeight = contentFontSize + lineSpacing - - // Center a content column at ~55% of screen width, left-align text within it - contentColumnWidth := int32(float32(p.Base.ScreenWidth) * 0.55) - contentLeftX := p.Base.ScreenCenterX - contentColumnWidth/2 - - // Title - titleH := float32(p.titleTextureHeight) * titleScale - titleW := float32(p.titleTextureWidth) * titleScale - titleX := p.Base.ScreenCenterX - int32(titleW/2) - titleY := p.Base.ScreenHeight - margin - int32(titleH) - p.Base.RenderTexture(textShader, p.titleTextureID, mgl32.Vec2{float32(titleX), float32(titleY)}, mgl32.Vec2{titleW, titleH}, white) - - // Prompt dimensions (calculate even if not shown, for content bounds) - promptH := float32(p.promptTextureHeight) * promptScale - promptW := float32(p.promptTextureWidth) * promptScale - promptX := p.Base.ScreenCenterX - int32(promptW/2) - promptY := margin - - // Content area bounds - contentTop := titleY - margin - contentBottom := promptY + int32(promptH) + margin - p.visibleHeight = contentTop - contentBottom - - // Calculate total content height (accounting for paragraph breaks) - totalContentHeight := int32(0) - for _, line := range p.lines { - if strings.TrimSpace(line) == "" { - totalContentHeight += paragraphSpacing - } else { - totalContentHeight += p.lineHeight - } - } - p.maxScrollOffset = totalContentHeight - p.visibleHeight - if p.maxScrollOffset < 0 { - p.maxScrollOffset = 0 - } - - // Render visible lines - currentY := contentTop - p.lineHeight + p.scrollOffset - - for i, line := range p.lines { - if strings.TrimSpace(line) == "" { - currentY -= paragraphSpacing - continue - } - - if p.lineTextureIDs[i] == 0 { - currentY -= p.lineHeight - continue - } - - // Skip lines outside visible area - if currentY > contentTop || currentY+p.lineHeight < contentBottom { - currentY -= p.lineHeight - continue - } - - lineW := float32(p.lineTextureWidths[i]) * contentScale - lineH := float32(p.lineTextureHeights[i]) * contentScale - - p.Base.RenderTexture(textShader, p.lineTextureIDs[i], mgl32.Vec2{float32(contentLeftX), float32(currentY)}, mgl32.Vec2{lineW, lineH}, white) - currentY -= p.lineHeight - } - - // Show accept/decline prompt when scrolled to bottom, scroll hint otherwise - if p.scrollOffset >= p.maxScrollOffset { - p.Base.RenderTexture(textShader, p.promptTextureID, mgl32.Vec2{float32(promptX), float32(promptY)}, mgl32.Vec2{promptW, promptH}, grey) - } else { - hintScale := promptScale - hintW := float32(p.scrollHintTextureWidth) * hintScale - hintH := float32(p.scrollHintTextureHeight) * hintScale - hintX := p.Base.ScreenCenterX - int32(hintW/2) - hintY := promptY - p.Base.RenderTexture(textShader, p.scrollHintTextureID, mgl32.Vec2{float32(hintX), float32(hintY)}, mgl32.Vec2{hintW, hintH}, grey) - } - - return nil -} - -func (p *TOSPage) Destroy() error { - gl.DeleteTextures(1, &p.titleTextureID) - gl.DeleteTextures(1, &p.promptTextureID) - gl.DeleteTextures(1, &p.scrollHintTextureID) - - for _, texID := range p.lineTextureIDs { - if texID != 0 { - gl.DeleteTextures(1, &texID) - } - } - - return p.Base.Destroy() -} diff --git a/internal/page/updateCheckPage.go b/internal/page/updateCheckPage.go index da5c1d3..d76427b 100644 --- a/internal/page/updateCheckPage.go +++ b/internal/page/updateCheckPage.go @@ -3,13 +3,11 @@ package page import ( "fmt" "os/exec" - "strconv" "strings" "github.com/go-gl/gl/v3.3-core/gl" "github.com/veandco/go-sdl2/sdl" "rhemawavetech.com/rhemawave/internal/colors" - "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/fontManager" "rhemawavetech.com/rhemawave/internal/shaderManager" ) @@ -43,8 +41,9 @@ type UpdateCheckPage struct { dotTimer float32 } -func (p *UpdateCheckPage) Init(app ApplicationInterface) error { - if err := p.Base.Init(app); err != nil { +func (p *UpdateCheckPage) Init(ctx *PageCtx) error { + var err error + if err := p.Base.Init(ctx); err != nil { return fmt.Errorf("failed base page initialization: %w", err) } @@ -62,14 +61,7 @@ func (p *UpdateCheckPage) Init(app ApplicationInterface) error { } // Get scale - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.Atoi(baseFontSizeStr) - if err != nil { - return fmt.Errorf("parsing init_font_size: %w", err) - } + baseFontSize := ctx.CfgPtr.InitialFontSize scale := float32(48) / float32(baseFontSize) // Create checking textures with animated dots @@ -149,7 +141,7 @@ func (p *UpdateCheckPage) HandleEvent(event *sdl.Event) error { } switch e.Keysym.Sym { case sdl.K_F3, sdl.K_BACKSPACE: - p.App.UnwindToPage(&Settings{}) + p.Context.NavigateTo(SETTINGS) } } return nil diff --git a/internal/page/wifiSetupPage.go b/internal/page/wifiSetupPage.go index 435c07e..9948cb1 100644 --- a/internal/page/wifiSetupPage.go +++ b/internal/page/wifiSetupPage.go @@ -2,7 +2,6 @@ package page import ( "fmt" - "strconv" "strings" "time" @@ -11,7 +10,6 @@ import ( "github.com/godbus/dbus/v5" "github.com/veandco/go-sdl2/sdl" "rhemawavetech.com/rhemawave/internal/colors" - "rhemawavetech.com/rhemawave/internal/db" "rhemawavetech.com/rhemawave/internal/fontManager" "rhemawavetech.com/rhemawave/internal/keybinds" "rhemawavetech.com/rhemawave/internal/logger" @@ -69,17 +67,14 @@ type WiFiSetupPage struct { } // Init sets up the page, initializes the network manager, and starts a scan. -func (p *WiFiSetupPage) Init(app ApplicationInterface) error { +func (p *WiFiSetupPage) Init(ctx *PageCtx) error { - if err := p.Base.Init(app); err != nil { + if err := p.Base.Init(ctx); err != nil { return fmt.Errorf("failed base page initialization: %w", err) } // Get application font from database - applicationFontName, err := db.GetConfigValue("application_font") - if err != nil { - return fmt.Errorf("retrieving application_font from db: %w", err) - } + applicationFontName := "Roboto-Regular" fm := fontManager.Get() font, ok := fm.GetFont(applicationFontName) @@ -629,14 +624,7 @@ func (p *WiFiSetupPage) updatePasswordErrorTexture() { // Render draws the entire page. func (p *WiFiSetupPage) Render() error { // Get base font size from database - baseFontSizeStr, err := db.GetConfigValue("init_font_size") - if err != nil { - return fmt.Errorf("retrieving init_font_size from db: %w", err) - } - baseFontSize, err := strconv.ParseFloat(baseFontSizeStr, 32) - if err != nil { - return fmt.Errorf("parsing init_font_size: %w", err) - } + baseFontSize := p.Context.CfgPtr.StandardFontSize // Colors & Config bgColor := sdl.Color{R: 0, G: 0, B: 0, A: 255} diff --git a/internal/shaderManager/shaderManager.go b/internal/shaderManager/shaderManager.go index bd95b92..f4b6396 100644 --- a/internal/shaderManager/shaderManager.go +++ b/internal/shaderManager/shaderManager.go @@ -8,7 +8,6 @@ import ( "sync" "github.com/go-gl/gl/v3.3-core/gl" - "rhemawavetech.com/rhemawave/internal/db" ) // Shader represents a compiled GLSL program and its associated data. @@ -35,14 +34,14 @@ var ( ) // InitShaderManager initializes the ShaderManager singleton. -func InitShaderManager() error { +func InitShaderManager(shaderDir string) error { var err error once.Do(func() { sm := &ShaderManager{ shaders: make(map[string]*Shader), } - if e := sm.LoadShaders(); e != nil { - err = fmt.Errorf("could not load shaders: %w", e) + if e := sm.LoadShaders(shaderDir); e != nil { + err = fmt.Errorf("could not load shaders from %s: %w", shaderDir, e) return } instance = sm @@ -78,17 +77,11 @@ func (sm *ShaderManager) Close() { } // LoadShaders scans a directory for .vert and .frag files and creates a shader program for each pair. -func (sm *ShaderManager) LoadShaders() error { +func (sm *ShaderManager) LoadShaders(shaderDir string) error { // Get assets directory from database - assetsDir, err := db.GetConfigValue("assets_dir") - if err != nil { - return fmt.Errorf("retrieving assets_dir from db: %w", err) - } - - srcDir := filepath.Join(assetsDir, "shaders") shaderFiles := make(map[string]string) - err = filepath.Walk(srcDir, func(path string, info os.FileInfo, err error) error { + err := filepath.Walk(shaderDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err }