Update Database

Database is more usable now.
All pages have been updated to use the database, and share an application configuration.
Page Stack has been removed, no more pages hanging around in memory. Replaced with a 'Router' which uses an Enum to determine which page should be displayed, destroys the old page and initializes the new page all in one go. No more push, pop, rollback, etc. No more holding instances of pages.
Logger has been updated with proper timestamps and sorting now works as expected."
This commit is contained in:
NeilClack
2026-03-23 15:02:07 -05:00
parent f8fbfb721d
commit 17c088cc2f
28 changed files with 537 additions and 2276 deletions
+19 -46
View File
@@ -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
-276
View File
@@ -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
}
+12 -32
View File
@@ -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()
@@ -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)
+12 -44
View File
@@ -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()
+21 -22
View File
@@ -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")
+12 -24
View File
@@ -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)
+28 -32
View File
@@ -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
+75
View File
@@ -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)
}
+13 -42
View File
@@ -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++ {
+21 -30
View File
@@ -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
-388
View File
@@ -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: <p>, <br>, <li>, <h1>-<h6>. All other tags are stripped.
// HTML entities &amp; &lt; &gt; &quot; &#39; are decoded.
func parseHTMLToLines(html string, maxCharsPerLine int) []string {
var lines []string
// Replace <br>, <br/>, <br /> with newline markers
br := strings.NewReplacer(
"<br>", "\n", "<br/>", "\n", "<br />", "\n",
"<BR>", "\n", "<BR/>", "\n", "<BR />", "\n",
)
html = br.Replace(html)
// Process block-level tags
// Replace </p>, </h1>-</h6> with paragraph break marker
for _, tag := range []string{"</p>", "</h1>", "</h2>", "</h3>", "</h4>", "</h5>", "</h6>"} {
html = strings.ReplaceAll(html, tag, "\n\n")
html = strings.ReplaceAll(html, strings.ToUpper(tag), "\n\n")
}
// Replace <li> with list prefix marker
html = strings.ReplaceAll(html, "<li>", "\n- ")
html = strings.ReplaceAll(html, "<LI>", "\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, "&amp;", "&")
text = strings.ReplaceAll(text, "&lt;", "<")
text = strings.ReplaceAll(text, "&gt;", ">")
text = strings.ReplaceAll(text, "&quot;", "\"")
text = strings.ReplaceAll(text, "&#39;", "'")
// 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 <br> 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()
}
+5 -13
View File
@@ -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
+4 -16
View File
@@ -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}