Initial release: RadiantWave v1.0
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
package page
|
||||
|
||||
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"
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
if err := p.Base.Init(app); 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)
|
||||
}
|
||||
|
||||
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, err := db.GetConfigValue("license_key")
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving license_key from db: %w", err)
|
||||
}
|
||||
|
||||
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:
|
||||
// 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.App.UnwindToPage(&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
|
||||
if err := db.SetConfigValue("license_key", p.inputText); err != nil {
|
||||
return fmt.Errorf("failed to save license key: %w", err)
|
||||
}
|
||||
|
||||
// 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.App.UnwindToPage(&Welcome{})
|
||||
} else {
|
||||
// Coming from settings: rewind back to settings
|
||||
p.App.UnwindToPage(&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 = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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, err := db.GetConfigValue("application_font")
|
||||
if err != nil {
|
||||
return fmt.Errorf("retrieving application_font from db: %w", err)
|
||||
}
|
||||
|
||||
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
|
||||
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()
|
||||
|
||||
// 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()
|
||||
}
|
||||
Reference in New Issue
Block a user