Replace in-memory atomic AppConfig caching with direct DB queries via db.GetConfig across application and page layers. This simplifies state management and ensures all components always operate on the latest persisted configuration. Key changes: - Remove Application.Config atomic cache and refreshCache mechanism - Eliminate PageCtx.CfgPtr and pass DB handle instead - Replace all cfg reads with db.GetConfig(app.DB) - Update mixer, audio, UI pages, and initialization paths accordingly - Explicitly set AppConfig table name to avoid GORM pluralization issues - Reduce DB connection pooling (SetMaxIdleConns(1)) - Standardize application font to SpaceMonoNerdFont-Regular - Lower default audio volume (1.0 → 0.2) - Add Unicode checkmark glyph to font map Behavioral impact: - Configuration is no longer cached in memory - Every config access now hits the DB (more consistent, slightly more overhead)
408 lines
12 KiB
Go
408 lines
12 KiB
Go
package page
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
|
|
"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"
|
|
)
|
|
|
|
// emailRegex validates email format
|
|
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)
|
|
|
|
type EmailAddressPage struct {
|
|
Base
|
|
|
|
title string
|
|
titleTextureID uint32
|
|
titleTextureWidth int32
|
|
titleTextureHeight int32
|
|
|
|
currentEmailDisplayTextureID uint32
|
|
currentEmailDisplayTextureWidth int32
|
|
currentEmailDisplayTextureHeight int32
|
|
|
|
inputText string
|
|
inputTextTextureID uint32
|
|
inputTextTextureWidth int32
|
|
inputTextTextureHeight int32
|
|
|
|
// Error message handling
|
|
errorMessage string
|
|
errorTextureID uint32
|
|
errorTextureWidth int32
|
|
errorTextureHeight int32
|
|
|
|
// isFirstTimeSetup tracks whether page was loaded empty (first-time setup)
|
|
isFirstTimeSetup bool
|
|
|
|
// Text input deduplication
|
|
textInputSupported bool
|
|
}
|
|
|
|
// isValidEmail checks if the given string is a valid email format
|
|
func isValidEmail(email string) bool {
|
|
return emailRegex.MatchString(email)
|
|
}
|
|
|
|
// Init performs basic page initialization and creation of static assets.
|
|
func (p *EmailAddressPage) Init(ctx *PageCtx) error {
|
|
|
|
if err := p.Base.Init(ctx); err != nil {
|
|
return fmt.Errorf("failed base page initialization: %w", err)
|
|
}
|
|
|
|
p.title = "Your Email Address"
|
|
|
|
applicationFont := "SpaceMonoNerdFont-Regular"
|
|
|
|
fm := fontManager.Get()
|
|
textShader, ok := shaderManager.Get().Get("text")
|
|
if !ok {
|
|
return fmt.Errorf("unable to fetch text shader")
|
|
}
|
|
|
|
// Create title texture
|
|
titleTextureID, titleWidth, titleHeight, err := fm.CreateStringTexture(applicationFont, p.title, textShader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create title texture: %w", err)
|
|
}
|
|
if titleTextureID == 0 {
|
|
return fmt.Errorf("CreateStringTexture returned a nil texture ID")
|
|
}
|
|
|
|
p.titleTextureID = titleTextureID
|
|
p.titleTextureWidth = titleWidth
|
|
p.titleTextureHeight = titleHeight
|
|
|
|
// Load existing email address from database
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
emailAddress := config.EmailAddress
|
|
|
|
if emailAddress != "" {
|
|
p.inputText = emailAddress
|
|
p.isFirstTimeSetup = false
|
|
|
|
// Create current email display texture
|
|
currentEmailText := fmt.Sprintf("Current Email: %s", emailAddress)
|
|
currentEmailTextureID, currentEmailWidth, currentEmailHeight, err := fm.CreateStringTexture(applicationFont, currentEmailText, textShader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create current email display texture: %w", err)
|
|
}
|
|
if currentEmailTextureID == 0 {
|
|
return fmt.Errorf("CreateStringTexture returned a nil texture ID for current email display")
|
|
}
|
|
|
|
p.currentEmailDisplayTextureID = currentEmailTextureID
|
|
p.currentEmailDisplayTextureWidth = currentEmailWidth
|
|
p.currentEmailDisplayTextureHeight = currentEmailHeight
|
|
} else {
|
|
p.inputText = ""
|
|
p.isFirstTimeSetup = true
|
|
}
|
|
|
|
sdl.StartTextInput()
|
|
|
|
return nil
|
|
}
|
|
|
|
// HandleEvent processes SDL events and handles them appropriately for the EmailAddressPage.
|
|
func (p *EmailAddressPage) HandleEvent(event *sdl.Event) error {
|
|
actualEvent := *event
|
|
if !sdl.IsTextInputActive() {
|
|
sdl.StartTextInput()
|
|
}
|
|
|
|
switch e := actualEvent.(type) {
|
|
case *sdl.TextInputEvent:
|
|
if !p.textInputSupported {
|
|
// First TextInputEvent: the keyboard fallback already
|
|
// handled this character, so just record that text input
|
|
// is supported and skip the duplicate append.
|
|
p.textInputSupported = true
|
|
} else {
|
|
p.inputText += e.GetText()
|
|
// Clear error when user starts typing
|
|
p.errorMessage = ""
|
|
}
|
|
case *sdl.KeyboardEvent:
|
|
if e.Type == sdl.KEYDOWN {
|
|
switch e.Keysym.Sym {
|
|
case sdl.K_ESCAPE:
|
|
// Block escape during first-time setup
|
|
if p.isFirstTimeSetup {
|
|
return nil
|
|
}
|
|
// Allow escape back to settings if coming from settings
|
|
sdl.StopTextInput()
|
|
p.Context.NavigateTo(SETTINGS)
|
|
case sdl.K_RETURN:
|
|
// Validate email - check empty first, then format
|
|
if p.inputText == "" {
|
|
p.errorMessage = "Email address cannot be empty"
|
|
return nil
|
|
}
|
|
if !isValidEmail(p.inputText) {
|
|
p.errorMessage = "Please enter a valid email address"
|
|
return nil
|
|
}
|
|
|
|
sdl.StopTextInput()
|
|
|
|
// Save email address to database
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
config.EmailAddress = p.inputText
|
|
p.Context.DbPtr.Save(config)
|
|
|
|
if p.isFirstTimeSetup {
|
|
// First-time setup: navigate to License Key page
|
|
p.Context.NavigateTo(LICENSE_KEY)
|
|
} else {
|
|
p.Context.NavigateTo(SETTINGS)
|
|
}
|
|
case sdl.K_BACKSPACE:
|
|
if len(p.inputText) > 0 {
|
|
p.inputText = p.inputText[:len(p.inputText)-1]
|
|
// Clear error when user edits input
|
|
p.errorMessage = ""
|
|
}
|
|
default:
|
|
// Keyboard fallback: if TextInputEvent is not supported,
|
|
// convert printable keycodes to characters directly.
|
|
// This handles kiosk systems without an input method framework.
|
|
if !p.textInputSupported {
|
|
if ch := keycodeToChar(e.Keysym.Sym, e.Keysym.Mod); ch != 0 {
|
|
p.inputText += string(ch)
|
|
p.errorMessage = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 := "SpaceMonoNerdFont-Regular"
|
|
|
|
fm := fontManager.Get()
|
|
sm := shaderManager.Get()
|
|
shader, ok := sm.Get("text")
|
|
if !ok {
|
|
return fmt.Errorf("unable to fetch text shader")
|
|
}
|
|
|
|
// Update input text texture
|
|
if len(p.inputText) > 0 {
|
|
// Create the new inputText texture
|
|
inputTextTextureID, inputTextTextureWidth, inputTextTextureHeight, err := fm.CreateStringTexture(
|
|
applicationFont,
|
|
p.inputText,
|
|
shader,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create new input text texture: %w", err)
|
|
}
|
|
|
|
// Delete old texture if it exists
|
|
if p.inputTextTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.inputTextTextureID)
|
|
}
|
|
|
|
p.inputTextTextureID = inputTextTextureID
|
|
p.inputTextTextureWidth = inputTextTextureWidth
|
|
p.inputTextTextureHeight = inputTextTextureHeight
|
|
}
|
|
|
|
// Update error message texture
|
|
if p.errorMessage != "" {
|
|
errorTextureID, errorWidth, errorHeight, err := fm.CreateStringTexture(
|
|
applicationFont,
|
|
p.errorMessage,
|
|
shader,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create error text texture: %w", err)
|
|
}
|
|
|
|
// Delete old error texture if it exists
|
|
if p.errorTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.errorTextureID)
|
|
}
|
|
|
|
p.errorTextureID = errorTextureID
|
|
p.errorTextureWidth = errorWidth
|
|
p.errorTextureHeight = errorHeight
|
|
} else {
|
|
// Clear error texture if no error message
|
|
if p.errorTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.errorTextureID)
|
|
p.errorTextureID = 0
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Render performs page drawing logic.
|
|
func (p *EmailAddressPage) Render() error {
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
// Get base font size from database
|
|
baseFontSize := config.StandardFontSize
|
|
|
|
sm := shaderManager.Get()
|
|
|
|
// 1. Calculate Scale
|
|
titleFontSize := int32(64)
|
|
titleScale := float32(titleFontSize) / float32(baseFontSize)
|
|
currentEmailFontSize := int32(28)
|
|
currentEmailScale := float32(currentEmailFontSize) / float32(baseFontSize)
|
|
padding := int32(4)
|
|
verticalMargin := int32(20)
|
|
|
|
// 2. Define dimensions
|
|
targetFontSize := int32(36)
|
|
scale := float32(targetFontSize) / float32(baseFontSize)
|
|
border := int32(3)
|
|
boxH := targetFontSize + (padding * 2)
|
|
boxW := int32(600) + (padding * 2)
|
|
boxDimensions := mgl32.Vec2{float32(boxW), float32(boxH)}
|
|
borderBoxH := boxH + (border * 2)
|
|
borderBoxW := boxW + (border * 2)
|
|
borderBoxDimensions := mgl32.Vec2{float32(borderBoxW), float32(borderBoxH)}
|
|
titleH := float32(p.titleTextureHeight) * titleScale
|
|
titleW := float32(p.titleTextureWidth) * titleScale
|
|
titleDimensions := mgl32.Vec2{titleW, titleH}
|
|
inputTextH := float32(p.inputTextTextureHeight) * scale
|
|
inputTextW := float32(p.inputTextTextureWidth) * scale
|
|
inputTextDimensions := mgl32.Vec2{inputTextW, inputTextH}
|
|
|
|
var currentEmailDisplayH, currentEmailDisplayW float32
|
|
var currentEmailDisplayDimensions mgl32.Vec2
|
|
if p.currentEmailDisplayTextureID != 0 {
|
|
currentEmailDisplayH = float32(p.currentEmailDisplayTextureHeight) * currentEmailScale
|
|
currentEmailDisplayW = float32(p.currentEmailDisplayTextureWidth) * currentEmailScale
|
|
currentEmailDisplayDimensions = mgl32.Vec2{currentEmailDisplayW, currentEmailDisplayH}
|
|
}
|
|
|
|
// Error text dimensions
|
|
errorFontSize := int32(20)
|
|
errorScale := float32(errorFontSize) / float32(baseFontSize)
|
|
var errorTextH, errorTextW float32
|
|
var errorTextDimensions mgl32.Vec2
|
|
if p.errorTextureID != 0 {
|
|
errorTextH = float32(p.errorTextureHeight) * errorScale
|
|
errorTextW = float32(p.errorTextureWidth) * errorScale
|
|
errorTextDimensions = mgl32.Vec2{errorTextW, errorTextH}
|
|
}
|
|
|
|
// 3. Define positions
|
|
borderBoxY := p.Base.ScreenCenterY - (borderBoxH / 2)
|
|
borderBoxX := p.Base.ScreenCenterX - (borderBoxW / 2)
|
|
borderBoxPosition := mgl32.Vec2{float32(borderBoxX), float32(borderBoxY)}
|
|
boxY := p.Base.ScreenCenterY - (boxH / 2)
|
|
boxX := p.Base.ScreenCenterX - (boxW / 2)
|
|
boxPosition := mgl32.Vec2{float32(boxX), float32(boxY)}
|
|
titleY := borderBoxY + borderBoxH + verticalMargin
|
|
titleX := p.Base.ScreenCenterX - int32(titleW/2)
|
|
titlePosition := mgl32.Vec2{float32(titleX), float32(titleY)}
|
|
inputTextY := boxY + padding
|
|
inputTextX := boxX + padding
|
|
inputTextPosition := mgl32.Vec2{float32(inputTextX), float32(inputTextY)}
|
|
|
|
var currentEmailDisplayPosition mgl32.Vec2
|
|
if p.currentEmailDisplayTextureID != 0 {
|
|
currentEmailDisplayY := borderBoxY - int32(currentEmailDisplayH) - verticalMargin
|
|
currentEmailDisplayX := p.Base.ScreenCenterX - int32(currentEmailDisplayW/2)
|
|
currentEmailDisplayPosition = mgl32.Vec2{float32(currentEmailDisplayX), float32(currentEmailDisplayY)}
|
|
}
|
|
|
|
// Error text position (below current email display to avoid overlap)
|
|
var errorTextPosition mgl32.Vec2
|
|
if p.errorTextureID != 0 {
|
|
var errorTextY int32
|
|
if p.currentEmailDisplayTextureID != 0 {
|
|
// Position below the current email display
|
|
errorTextY = borderBoxY - int32(currentEmailDisplayH) - verticalMargin - int32(errorTextH) - (verticalMargin / 2)
|
|
} else {
|
|
// No current email display, position below the input box
|
|
errorTextY = borderBoxY - int32(errorTextH) - (verticalMargin / 2)
|
|
}
|
|
errorTextX := p.Base.ScreenCenterX - int32(errorTextW/2)
|
|
errorTextPosition = mgl32.Vec2{float32(errorTextX), float32(errorTextY)}
|
|
}
|
|
|
|
// 4. Define Colors
|
|
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
|
|
grey := sdl.Color{R: 120, G: 120, B: 120, A: 255}
|
|
darkGrey := sdl.Color{R: 30, G: 30, B: 30, A: 255}
|
|
|
|
// 5. Get Shaders
|
|
textShader, _ := sm.Get("text")
|
|
solidShader, _ := sm.Get("solid_color")
|
|
|
|
// 6. Draw everything
|
|
p.Base.RenderSolidColorQuad(solidShader, borderBoxPosition, borderBoxDimensions, grey)
|
|
p.Base.RenderSolidColorQuad(solidShader, boxPosition, boxDimensions, darkGrey)
|
|
p.Base.RenderTexture(textShader, p.titleTextureID, titlePosition, titleDimensions, white)
|
|
|
|
// Draw current email display if it exists
|
|
if p.currentEmailDisplayTextureID != 0 {
|
|
p.Base.RenderTexture(textShader, p.currentEmailDisplayTextureID, currentEmailDisplayPosition, currentEmailDisplayDimensions, white)
|
|
}
|
|
|
|
if len(p.inputText) > 0 {
|
|
p.Base.RenderTexture(textShader, p.inputTextTextureID, inputTextPosition, inputTextDimensions, white)
|
|
}
|
|
|
|
// Draw error message if it exists
|
|
if p.errorTextureID != 0 {
|
|
p.Base.RenderTexture(textShader, p.errorTextureID, errorTextPosition, errorTextDimensions, colors.ErrorRed)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Destroy cleans up page-specific resources.
|
|
func (p *EmailAddressPage) Destroy() error {
|
|
|
|
gl.DeleteTextures(1, &p.titleTextureID)
|
|
|
|
// Delete current email display texture if it exists
|
|
if p.currentEmailDisplayTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.currentEmailDisplayTextureID)
|
|
p.currentEmailDisplayTextureID = 0
|
|
}
|
|
|
|
// Delete input text texture if it exists
|
|
if p.inputTextTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.inputTextTextureID)
|
|
p.inputTextTextureID = 0
|
|
}
|
|
|
|
// Delete error texture if it exists
|
|
if p.errorTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.errorTextureID)
|
|
p.errorTextureID = 0
|
|
}
|
|
|
|
sdl.StopTextInput()
|
|
return p.Base.Destroy()
|
|
}
|