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."
448 lines
14 KiB
Go
448 lines
14 KiB
Go
package page
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"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/fontManager"
|
|
"rhemawavetech.com/rhemawave/internal/logger"
|
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
|
)
|
|
|
|
type LicenseKeyPage struct {
|
|
Base
|
|
|
|
title string
|
|
titleTextureID uint32
|
|
titleTextureWidth int32
|
|
titleTextureHeight int32
|
|
|
|
currentLicenseDisplayTextureID uint32
|
|
currentLicenseDisplayTextureWidth int32
|
|
currentLicenseDisplayTextureHeight 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
|
|
}
|
|
|
|
// Init performs basic page initialization and creation of static assets.
|
|
func (p *LicenseKeyPage) Init(ctx *PageCtx) error {
|
|
|
|
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 := "Roboto-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 license key from database
|
|
licenseKey := p.Base.Context.CfgPtr.LicenseKey
|
|
|
|
if licenseKey != "" {
|
|
p.inputText = licenseKey
|
|
p.isFirstTimeSetup = false
|
|
|
|
// Create current license key display texture
|
|
currentLicenseText := fmt.Sprintf("Current License Key: %s", licenseKey)
|
|
currentLicenseTextureID, currentLicenseWidth, currentLicenseHeight, err := fm.CreateStringTexture(applicationFont, currentLicenseText, textShader)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create current license display texture: %w", err)
|
|
}
|
|
if currentLicenseTextureID == 0 {
|
|
return fmt.Errorf("CreateStringTexture returned a nil texture ID for current license display")
|
|
}
|
|
|
|
p.currentLicenseDisplayTextureID = currentLicenseTextureID
|
|
p.currentLicenseDisplayTextureWidth = currentLicenseWidth
|
|
p.currentLicenseDisplayTextureHeight = currentLicenseHeight
|
|
} else {
|
|
p.inputText = ""
|
|
p.isFirstTimeSetup = true
|
|
}
|
|
|
|
sdl.StartTextInput()
|
|
|
|
return nil
|
|
}
|
|
|
|
// formatLicenseKey formats the input string with dashes every 4 characters
|
|
// and limits to 16 alphanumeric characters (not counting dashes)
|
|
func formatLicenseKey(input string) string {
|
|
// Remove all dashes and non-alphanumeric characters
|
|
cleaned := strings.Map(func(r rune) rune {
|
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
|
return r
|
|
}
|
|
return -1
|
|
}, input)
|
|
|
|
// Limit to 16 characters
|
|
if len(cleaned) > 16 {
|
|
cleaned = cleaned[:16]
|
|
}
|
|
|
|
// Insert dashes every 4 characters
|
|
var result strings.Builder
|
|
for i, char := range cleaned {
|
|
if i > 0 && i%4 == 0 {
|
|
result.WriteRune('-')
|
|
}
|
|
result.WriteRune(char)
|
|
}
|
|
|
|
return result.String()
|
|
}
|
|
|
|
// HandleEvent processes SDL events and handles them appropriately for the LicenseKeyPage.
|
|
func (p *LicenseKeyPage) 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 {
|
|
// Add the new character and reformat
|
|
p.inputText += e.GetText()
|
|
p.inputText = formatLicenseKey(p.inputText)
|
|
// 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.Base.Context.NavigateTo(SETTINGS)
|
|
case sdl.K_RETURN:
|
|
// Validate license key - check empty first, then length, then characters
|
|
cleaned := strings.ReplaceAll(p.inputText, "-", "")
|
|
if cleaned == "" {
|
|
p.errorMessage = "License key cannot be empty"
|
|
return nil
|
|
}
|
|
if len(cleaned) != 16 {
|
|
p.errorMessage = fmt.Sprintf("License key must be 16 characters (currently %d)", len(cleaned))
|
|
return nil
|
|
}
|
|
// Check for invalid characters
|
|
for _, r := range cleaned {
|
|
if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9')) {
|
|
p.errorMessage = "License key must contain only letters and numbers"
|
|
return nil
|
|
}
|
|
}
|
|
|
|
sdl.StopTextInput()
|
|
|
|
// Save license key to database
|
|
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 {
|
|
keyPath := filepath.Join(homeDir, ".local", "share", "rhemawave", "license_key")
|
|
if err := os.WriteFile(keyPath, []byte(p.inputText), 0644); err != nil {
|
|
logger.Error("Failed to write license key file: %v", err)
|
|
}
|
|
}
|
|
|
|
if p.isFirstTimeSetup {
|
|
// First-time setup: rewind to Welcome page
|
|
p.Base.Context.NavigateTo(HOME)
|
|
} else {
|
|
// Coming from settings: rewind back to settings
|
|
p.Base.Context.NavigateTo(SETTINGS)
|
|
}
|
|
case sdl.K_BACKSPACE:
|
|
if len(p.inputText) > 0 {
|
|
// Remove last character and reformat
|
|
// Strip all dashes first to get raw characters
|
|
cleaned := strings.ReplaceAll(p.inputText, "-", "")
|
|
if len(cleaned) > 0 {
|
|
cleaned = cleaned[:len(cleaned)-1]
|
|
}
|
|
p.inputText = formatLicenseKey(cleaned)
|
|
// 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.inputText = formatLicenseKey(p.inputText)
|
|
p.errorMessage = ""
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 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 := "Roboto-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 *LicenseKeyPage) Render() error {
|
|
// Get base font size from database
|
|
baseFontSize := p.Base.Context.CfgPtr.StandardFontSize
|
|
|
|
sm := shaderManager.Get()
|
|
|
|
// 1. Calculate Scale
|
|
titleFontSize := int32(64)
|
|
titleScale := float32(titleFontSize) / float32(baseFontSize)
|
|
currentLicenseFontSize := int32(28)
|
|
currentLicenseScale := float32(currentLicenseFontSize) / 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 currentLicenseDisplayH, currentLicenseDisplayW float32
|
|
var currentLicenseDisplayDimensions mgl32.Vec2
|
|
if p.currentLicenseDisplayTextureID != 0 {
|
|
currentLicenseDisplayH = float32(p.currentLicenseDisplayTextureHeight) * currentLicenseScale
|
|
currentLicenseDisplayW = float32(p.currentLicenseDisplayTextureWidth) * currentLicenseScale
|
|
currentLicenseDisplayDimensions = mgl32.Vec2{currentLicenseDisplayW, currentLicenseDisplayH}
|
|
}
|
|
|
|
// 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 currentLicenseDisplayPosition mgl32.Vec2
|
|
if p.currentLicenseDisplayTextureID != 0 {
|
|
currentLicenseDisplayY := borderBoxY - int32(currentLicenseDisplayH) - verticalMargin
|
|
currentLicenseDisplayX := p.Base.ScreenCenterX - int32(currentLicenseDisplayW/2)
|
|
currentLicenseDisplayPosition = mgl32.Vec2{float32(currentLicenseDisplayX), float32(currentLicenseDisplayY)}
|
|
}
|
|
|
|
// Error text position (below current license display to avoid overlap)
|
|
var errorTextPosition mgl32.Vec2
|
|
if p.errorTextureID != 0 {
|
|
var errorTextY int32
|
|
if p.currentLicenseDisplayTextureID != 0 {
|
|
// Position below the current license display
|
|
errorTextY = borderBoxY - int32(currentLicenseDisplayH) - verticalMargin - int32(errorTextH) - (verticalMargin / 2)
|
|
} else {
|
|
// No current license 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. Get Shaders
|
|
textShader, _ := sm.Get("text")
|
|
solidShader, _ := sm.Get("solid_color")
|
|
|
|
// 5. Draw everything
|
|
p.Base.RenderSolidColorQuad(solidShader, borderBoxPosition, borderBoxDimensions, colors.Gray)
|
|
p.Base.RenderSolidColorQuad(solidShader, boxPosition, boxDimensions, colors.DarkGray)
|
|
p.Base.RenderTexture(textShader, p.titleTextureID, titlePosition, titleDimensions, colors.White)
|
|
|
|
// Draw current license display if it exists
|
|
if p.currentLicenseDisplayTextureID != 0 {
|
|
p.Base.RenderTexture(textShader, p.currentLicenseDisplayTextureID, currentLicenseDisplayPosition, currentLicenseDisplayDimensions, colors.White)
|
|
}
|
|
|
|
if len(p.inputText) > 0 {
|
|
p.Base.RenderTexture(textShader, p.inputTextTextureID, inputTextPosition, inputTextDimensions, colors.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 *LicenseKeyPage) Destroy() error {
|
|
|
|
gl.DeleteTextures(1, &p.titleTextureID)
|
|
|
|
// Delete current license display texture if it exists
|
|
if p.currentLicenseDisplayTextureID != 0 {
|
|
gl.DeleteTextures(1, &p.currentLicenseDisplayTextureID)
|
|
p.currentLicenseDisplayTextureID = 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()
|
|
}
|