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)
420 lines
13 KiB
Go
420 lines
13 KiB
Go
package page
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"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/download"
|
|
"rhemawavetech.com/rhemawave/internal/fontManager"
|
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
|
)
|
|
|
|
var GitVersion = "9beeeb1"
|
|
|
|
type Home struct {
|
|
Base
|
|
fm *fontManager.FontManager
|
|
font *fontManager.FontEntry
|
|
title StringItem
|
|
version StringItem
|
|
prompt StringItem
|
|
note StringItem
|
|
message StringItem
|
|
downloadProgressChan chan download.DownloadStatus
|
|
licenseInfoHeading StringItem
|
|
emailDisplay StringItem
|
|
licenseKeyDisplay StringItem
|
|
hasLicenseInfo bool
|
|
baseFontSize int
|
|
isDownloadComplete bool
|
|
}
|
|
|
|
// Init performs basic page initialization and creation of static assets.
|
|
func (p *Home) Init(ctx *PageCtx) error {
|
|
title := "Radiant 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(ctx); err != nil {
|
|
return fmt.Errorf("failed base page initialization: %w", err)
|
|
}
|
|
|
|
// Get font
|
|
p.fm = fontManager.Get()
|
|
font, ok := p.fm.GetFont("SpaceMonoNerdFont-Regular")
|
|
if !ok {
|
|
return fmt.Errorf("unable to fetch font")
|
|
}
|
|
p.font = font
|
|
|
|
// Create title texture
|
|
var err error
|
|
p.title, err = NewStringItem(title, font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create title string texture: %w", err)
|
|
}
|
|
|
|
// Create version texture
|
|
versionString := "Version: " + version
|
|
p.version, err = NewStringItem(versionString, font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create version string texture: %w", err)
|
|
}
|
|
|
|
// Create prompt texture
|
|
p.prompt, err = NewStringItem(prompt, font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create prompt string texture: %w", err)
|
|
}
|
|
|
|
// Create note texture
|
|
p.note, err = NewStringItem(note, font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create note string texture: %w", err)
|
|
}
|
|
|
|
// Create download channel and message
|
|
p.downloadProgressChan = make(chan download.DownloadStatus)
|
|
|
|
// Initialize message
|
|
p.message, err = NewStringItem("Status: Initializing...", font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create initial message texture: %w", err)
|
|
}
|
|
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Create license info textures if data exists
|
|
if config.EmailAddress != "" || config.LicenseKey != "" {
|
|
p.hasLicenseInfo = true
|
|
|
|
// Create heading
|
|
p.licenseInfoHeading, err = NewStringItem("License Info", font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create license info heading texture: %w", err)
|
|
}
|
|
|
|
// Create email display if exists
|
|
if config.EmailAddress != "" {
|
|
emailText := fmt.Sprintf("Email: %s", config.EmailAddress)
|
|
p.emailDisplay, err = NewStringItem(emailText, font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create email display texture: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create license key display if exists
|
|
if config.LicenseKey != "" {
|
|
licenseKeyText := fmt.Sprintf("License Key: %s", config.LicenseKey)
|
|
p.licenseKeyDisplay, err = NewStringItem(licenseKeyText, font, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to create license key display texture: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get base font size from database
|
|
baseFontSize := config.StandardFontSize
|
|
p.baseFontSize = baseFontSize
|
|
|
|
// Scale the note text
|
|
fontSize := int32(32)
|
|
scale := float32(fontSize) / float32(baseFontSize)
|
|
p.note.W = int32(float32(p.note.W) * scale * 0.75)
|
|
p.note.H = int32(float32(p.note.H) * scale * 0.75)
|
|
|
|
// Scale license info texts
|
|
if p.hasLicenseInfo {
|
|
licenseInfoScale := float32(28) / float32(baseFontSize)
|
|
p.licenseInfoHeading.W = int32(float32(p.licenseInfoHeading.W) * licenseInfoScale)
|
|
p.licenseInfoHeading.H = int32(float32(p.licenseInfoHeading.H) * licenseInfoScale)
|
|
if p.emailDisplay.ID != 0 {
|
|
p.emailDisplay.W = int32(float32(p.emailDisplay.W) * licenseInfoScale * 0.85)
|
|
p.emailDisplay.H = int32(float32(p.emailDisplay.H) * licenseInfoScale * 0.85)
|
|
}
|
|
if p.licenseKeyDisplay.ID != 0 {
|
|
p.licenseKeyDisplay.W = int32(float32(p.licenseKeyDisplay.W) * licenseInfoScale * 0.85)
|
|
p.licenseKeyDisplay.H = int32(float32(p.licenseKeyDisplay.H) * licenseInfoScale * 0.85)
|
|
}
|
|
}
|
|
|
|
// Scale message text
|
|
messageScale := float32(28) / float32(baseFontSize)
|
|
p.message.W = int32(float32(p.message.W) * messageScale)
|
|
p.message.H = int32(float32(p.message.H) * messageScale)
|
|
|
|
return nil
|
|
}
|
|
|
|
// HandleEvent processes SDL events and handles them appropriately for the Home page.
|
|
func (p *Home) HandleEvent(event *sdl.Event) error {
|
|
return nil
|
|
}
|
|
|
|
func (p *Home) Update(dt float32) error {
|
|
var (
|
|
progressUpdate download.DownloadStatus
|
|
ok bool
|
|
gotUpdate bool
|
|
)
|
|
|
|
// Drain the channel, keeping only the newest update.
|
|
for {
|
|
select {
|
|
case progressUpdate, ok = <-p.Context.MessageChannel:
|
|
gotUpdate = true
|
|
|
|
if !ok {
|
|
// Channel was closed while draining.
|
|
newMsg, err := NewStringItem("Status: READY", p.font, colors.Green)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
messageScale := float32(28) / float32(p.baseFontSize)
|
|
newMsg.W = int32(float32(newMsg.W) * messageScale)
|
|
newMsg.H = int32(float32(newMsg.H) * messageScale)
|
|
p.message = newMsg
|
|
p.isDownloadComplete = true
|
|
|
|
return nil
|
|
}
|
|
|
|
default:
|
|
goto drained
|
|
}
|
|
}
|
|
|
|
drained:
|
|
|
|
if !gotUpdate {
|
|
return nil
|
|
}
|
|
|
|
if progressUpdate.Err != nil {
|
|
// Handle error
|
|
newMsg, err := NewStringItem(
|
|
fmt.Sprintf("Status: %s", progressUpdate.Err.Error()),
|
|
p.font,
|
|
colors.Red,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
messageScale := float32(28) / float32(p.baseFontSize)
|
|
newMsg.W = int32(float32(newMsg.W) * messageScale)
|
|
newMsg.H = int32(float32(newMsg.H) * messageScale)
|
|
p.message = newMsg
|
|
|
|
} else {
|
|
// Update progress message using ONLY the latest update.
|
|
percentComplete := float64(progressUpdate.Cur) / float64(progressUpdate.Max) * 100
|
|
|
|
msg := fmt.Sprintf(
|
|
"Status: Downloading assets %.1f%% Complete",
|
|
percentComplete,
|
|
)
|
|
|
|
newMsg, err := NewStringItem(msg, p.font, colors.White)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
messageScale := float32(28) / float32(p.baseFontSize)
|
|
newMsg.W = int32(float32(newMsg.W) * messageScale)
|
|
newMsg.H = int32(float32(newMsg.H) * messageScale)
|
|
p.message = newMsg
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *Home) Render() error {
|
|
baseFontSize := p.baseFontSize
|
|
sm := shaderManager.Get()
|
|
verticalMargin := int32(20)
|
|
licenseInfoMargin := int32(10)
|
|
|
|
// 1) Calculate scales
|
|
titleFontSize := int32(64)
|
|
bodyFontSize := int32(32)
|
|
|
|
titleScale := float32(titleFontSize) / float32(baseFontSize)
|
|
bodyScale := float32(bodyFontSize) / float32(baseFontSize)
|
|
|
|
// 2) Calculate dimensions (scaled)
|
|
titleW := float32(p.title.W) * titleScale
|
|
titleH := float32(p.title.H) * titleScale
|
|
titleDim := mgl32.Vec2{titleW, titleH}
|
|
|
|
versionW := float32(p.version.W) * bodyScale
|
|
versionH := float32(p.version.H) * bodyScale
|
|
versionDim := mgl32.Vec2{versionW, versionH}
|
|
|
|
promptW := float32(p.prompt.W) * bodyScale
|
|
promptH := float32(p.prompt.H) * bodyScale
|
|
promptDim := mgl32.Vec2{promptW, promptH}
|
|
|
|
// Note dimensions (already scaled in Init)
|
|
noteW := float32(p.note.W)
|
|
noteH := float32(p.note.H)
|
|
|
|
// Message dimensions (already scaled in Init/Update)
|
|
messageW := float32(p.message.W)
|
|
messageH := float32(p.message.H)
|
|
|
|
// License info dimensions (already scaled in Init)
|
|
licenseHeadingW := float32(p.licenseInfoHeading.W)
|
|
licenseHeadingH := float32(p.licenseInfoHeading.H)
|
|
var emailDisplayW, emailDisplayH, licenseKeyDisplayW, licenseKeyDisplayH float32
|
|
if p.emailDisplay.ID != 0 {
|
|
emailDisplayW = float32(p.emailDisplay.W)
|
|
emailDisplayH = float32(p.emailDisplay.H)
|
|
}
|
|
if p.licenseKeyDisplay.ID != 0 {
|
|
licenseKeyDisplayW = float32(p.licenseKeyDisplay.W)
|
|
licenseKeyDisplayH = float32(p.licenseKeyDisplay.H)
|
|
}
|
|
|
|
// 3) Calculate total height of the vertical stack
|
|
totalH := int32(titleH) + verticalMargin +
|
|
int32(versionH) + verticalMargin +
|
|
int32(promptH) + verticalMargin +
|
|
int32(noteH)
|
|
|
|
// 4) Y-up layout: start from the TOP of the centered stack and step DOWN
|
|
top := p.Base.ScreenCenterY + totalH/2
|
|
|
|
titleY := top - int32(titleH)
|
|
versionY := titleY - verticalMargin - int32(versionH)
|
|
p.prompt.Y = versionY - verticalMargin - int32(promptH)
|
|
p.note.Y = p.prompt.Y - verticalMargin - int32(noteH)
|
|
|
|
// 5) Center X for each item
|
|
titleX := p.Base.ScreenCenterX - int32(titleW/2)
|
|
versionX := p.Base.ScreenCenterX - int32(versionW/2)
|
|
p.prompt.X = p.Base.ScreenCenterX - int32(promptW/2)
|
|
p.note.X = p.Base.ScreenCenterX - int32(noteW/2)
|
|
|
|
titlePos := mgl32.Vec2{float32(titleX), float32(titleY)}
|
|
versionPos := mgl32.Vec2{float32(versionX), float32(versionY)}
|
|
|
|
// 6) Position license info at bottom center
|
|
if p.hasLicenseInfo {
|
|
bottomMargin := int32(20)
|
|
|
|
// Calculate total height of license info block
|
|
licenseInfoTotalH := int32(licenseHeadingH)
|
|
if p.emailDisplay.ID != 0 {
|
|
licenseInfoTotalH += int32(emailDisplayH) + licenseInfoMargin
|
|
}
|
|
if p.licenseKeyDisplay.ID != 0 {
|
|
licenseInfoTotalH += int32(licenseKeyDisplayH) + licenseInfoMargin
|
|
}
|
|
|
|
// Start from bottom and work up
|
|
currentY := bottomMargin
|
|
|
|
// License key (bottom-most if it exists)
|
|
if p.licenseKeyDisplay.ID != 0 {
|
|
p.licenseKeyDisplay.X = p.Base.ScreenCenterX - int32(licenseKeyDisplayW/2)
|
|
p.licenseKeyDisplay.Y = currentY
|
|
currentY += int32(licenseKeyDisplayH) + licenseInfoMargin
|
|
}
|
|
|
|
// Email
|
|
if p.emailDisplay.ID != 0 {
|
|
p.emailDisplay.X = p.Base.ScreenCenterX - int32(emailDisplayW/2)
|
|
p.emailDisplay.Y = currentY
|
|
currentY += int32(emailDisplayH) + licenseInfoMargin
|
|
}
|
|
|
|
// Heading (top-most)
|
|
p.licenseInfoHeading.X = p.Base.ScreenCenterX - int32(licenseHeadingW/2)
|
|
p.licenseInfoHeading.Y = currentY
|
|
}
|
|
|
|
// 7) Position message between license info and note/prompt
|
|
if p.hasLicenseInfo {
|
|
// Calculate the top of the license info block
|
|
licenseInfoTopY := p.licenseInfoHeading.Y + int32(licenseHeadingH)
|
|
if p.emailDisplay.ID != 0 {
|
|
licenseInfoTopY += int32(emailDisplayH) + licenseInfoMargin
|
|
}
|
|
if p.licenseKeyDisplay.ID != 0 {
|
|
licenseInfoTopY += int32(licenseKeyDisplayH) + licenseInfoMargin
|
|
}
|
|
|
|
// Center the message vertically between license info and note
|
|
availableSpace := p.note.Y - licenseInfoTopY - verticalMargin
|
|
p.message.Y = licenseInfoTopY + availableSpace/2 - int32(messageH/2)
|
|
} else {
|
|
// If no license info, place message below note
|
|
p.message.Y = p.note.Y - verticalMargin - int32(messageH)
|
|
}
|
|
p.message.X = p.Base.ScreenCenterX - int32(messageW/2)
|
|
|
|
// 8) Get shader and render
|
|
textShader, ok := sm.Get("text")
|
|
if !ok {
|
|
return fmt.Errorf("failed to fetch text shader")
|
|
}
|
|
color := colors.White
|
|
|
|
p.Base.RenderTexture(textShader, p.title.ID, titlePos, titleDim, color)
|
|
p.Base.RenderTexture(textShader, p.version.ID, versionPos, versionDim, color)
|
|
p.Base.RenderTexture(textShader, p.prompt.ID, mgl32.Vec2{float32(p.prompt.X), float32(p.prompt.Y)}, promptDim, color)
|
|
p.Base.RenderTexture(textShader, p.note.ID, mgl32.Vec2{float32(p.note.X), float32(p.note.Y)}, mgl32.Vec2{noteW, noteH}, color)
|
|
|
|
// Render message
|
|
if p.message.ID != 0 {
|
|
msgColor := colors.White
|
|
if p.isDownloadComplete {
|
|
msgColor = colors.Green
|
|
}
|
|
p.Base.RenderTexture(textShader, p.message.ID, mgl32.Vec2{float32(p.message.X), float32(p.message.Y)}, mgl32.Vec2{messageW, messageH}, msgColor)
|
|
}
|
|
|
|
// Render license info if it exists
|
|
if p.hasLicenseInfo {
|
|
p.Base.RenderTexture(textShader, p.licenseInfoHeading.ID, mgl32.Vec2{float32(p.licenseInfoHeading.X), float32(p.licenseInfoHeading.Y)}, mgl32.Vec2{licenseHeadingW, licenseHeadingH}, color)
|
|
if p.emailDisplay.ID != 0 {
|
|
p.Base.RenderTexture(textShader, p.emailDisplay.ID, mgl32.Vec2{float32(p.emailDisplay.X), float32(p.emailDisplay.Y)}, mgl32.Vec2{emailDisplayW, emailDisplayH}, color)
|
|
}
|
|
if p.licenseKeyDisplay.ID != 0 {
|
|
p.Base.RenderTexture(textShader, p.licenseKeyDisplay.ID, mgl32.Vec2{float32(p.licenseKeyDisplay.X), float32(p.licenseKeyDisplay.Y)}, mgl32.Vec2{licenseKeyDisplayW, licenseKeyDisplayH}, color)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Destroy cleans up page-specific resources.
|
|
func (p *Home) Destroy() error {
|
|
gl.DeleteTextures(1, &p.title.ID)
|
|
gl.DeleteTextures(1, &p.version.ID)
|
|
gl.DeleteTextures(1, &p.prompt.ID)
|
|
gl.DeleteTextures(1, &p.note.ID)
|
|
gl.DeleteTextures(1, &p.message.ID)
|
|
|
|
if p.hasLicenseInfo {
|
|
gl.DeleteTextures(1, &p.licenseInfoHeading.ID)
|
|
if p.emailDisplay.ID != 0 {
|
|
gl.DeleteTextures(1, &p.emailDisplay.ID)
|
|
}
|
|
if p.licenseKeyDisplay.ID != 0 {
|
|
gl.DeleteTextures(1, &p.licenseKeyDisplay.ID)
|
|
}
|
|
}
|
|
|
|
return p.Base.Destroy()
|
|
}
|