803 lines
24 KiB
Go
803 lines
24 KiB
Go
package page
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/go-gl/gl/v3.3-core/gl"
|
|
"github.com/go-gl/mathgl/mgl32"
|
|
"github.com/godbus/dbus/v5"
|
|
"github.com/veandco/go-sdl2/sdl"
|
|
"rhemawavetech.com/rhemawave/internal/colors"
|
|
"rhemawavetech.com/rhemawave/internal/fontManager"
|
|
"rhemawavetech.com/rhemawave/internal/keybinds"
|
|
"rhemawavetech.com/rhemawave/internal/logger"
|
|
"rhemawavetech.com/rhemawave/internal/network"
|
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
|
)
|
|
|
|
// accessPointUIItem holds both the network data and its rendered texture.
|
|
type accessPointUIItem struct {
|
|
AP network.AccessPoint
|
|
Item StringItem
|
|
}
|
|
|
|
// WiFiSetupPage manages the discovery and connection to Wi-Fi networks.
|
|
type WiFiSetupPage struct {
|
|
Base
|
|
networkManager *network.Manager
|
|
applicationFont *fontManager.FontEntry
|
|
|
|
// Page-specific UI elements
|
|
titleItem StringItem
|
|
scanningStatusItem StringItem
|
|
connectionStatusItem StringItem
|
|
passwordPromptItem StringItem
|
|
passwordInputItem StringItem
|
|
passwordErrorItem StringItem
|
|
|
|
// Network list and selection
|
|
accessPoints []accessPointUIItem
|
|
selectedIndex int
|
|
scrollOffset int
|
|
|
|
// State management
|
|
isScanning bool
|
|
isEnteringPassword bool
|
|
isConnecting bool
|
|
connectionError string
|
|
passwordInput string
|
|
selectedAP *network.AccessPoint
|
|
wifiDevice *network.Device
|
|
connectionStatus *network.ConnectionStatus
|
|
activeSSID string // Tracks the CONFIRMED active network
|
|
pendingSSID string // Tracks the network we are TRYING to connect to
|
|
connectionAttemptTime time.Time
|
|
connectionTimeout time.Duration
|
|
|
|
// Text input deduplication
|
|
textInputSupported bool
|
|
|
|
// Async management
|
|
resultsChan chan []network.AccessPoint
|
|
statusChan chan *network.ConnectionStatus
|
|
errorChan chan error
|
|
stopScan chan struct{}
|
|
}
|
|
|
|
// Init sets up the page, initializes the network manager, and starts a scan.
|
|
func (p *WiFiSetupPage) Init(ctx *PageCtx) error {
|
|
|
|
if err := p.Base.Init(ctx); err != nil {
|
|
return fmt.Errorf("failed base page initialization: %w", err)
|
|
}
|
|
|
|
// Get application font from database
|
|
applicationFontName := "Roboto-Regular"
|
|
|
|
fm := fontManager.Get()
|
|
font, ok := fm.GetFont(applicationFontName)
|
|
if !ok {
|
|
return fmt.Errorf("could not find application font: %s", applicationFontName)
|
|
}
|
|
p.applicationFont = font
|
|
|
|
// Initialize network manager
|
|
nm, err := network.New()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to initialize network manager: %w", err)
|
|
}
|
|
p.networkManager = nm
|
|
|
|
// Find Wi-Fi device
|
|
if err := p.networkManager.FindDevices(); err != nil {
|
|
}
|
|
for _, dev := range p.networkManager.Devices {
|
|
if dev.Type == network.TypeWifi {
|
|
d := dev
|
|
p.wifiDevice = &d
|
|
break
|
|
}
|
|
}
|
|
|
|
// Create UI elements
|
|
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
|
|
p.titleItem, err = NewStringItem("Wi-Fi Setup", p.applicationFont, white)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create title item: %w", err)
|
|
}
|
|
p.scanningStatusItem, err = NewStringItem("Scanning for networks...", p.applicationFont, white)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create scanning status item: %w", err)
|
|
}
|
|
|
|
// Initialize channels and timeout
|
|
p.resultsChan = make(chan []network.AccessPoint)
|
|
p.statusChan = make(chan *network.ConnectionStatus, 1)
|
|
p.errorChan = make(chan error)
|
|
p.stopScan = make(chan struct{})
|
|
p.connectionTimeout = 30 * time.Second
|
|
|
|
// Start scanning and status monitoring
|
|
p.startScan()
|
|
go p.checkConnectionStatusLoop()
|
|
|
|
return nil
|
|
}
|
|
|
|
// checkConnectionStatusLoop periodically checks the network status.
|
|
func (p *WiFiSetupPage) checkConnectionStatusLoop() {
|
|
ticker := time.NewTicker(2 * time.Second) // Check more frequently for better UX
|
|
defer ticker.Stop()
|
|
|
|
status, _ := p.networkManager.CheckInternetConnection()
|
|
p.statusChan <- status
|
|
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
status, _ := p.networkManager.CheckInternetConnection()
|
|
p.statusChan <- status
|
|
case <-p.stopScan:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// startScan triggers a new asynchronous network scan.
|
|
func (p *WiFiSetupPage) startScan() {
|
|
p.isScanning = true
|
|
p.selectedIndex = 0
|
|
p.scrollOffset = 0
|
|
p.accessPoints = nil
|
|
|
|
if p.wifiDevice != nil {
|
|
go p.networkManager.StartScan(p.resultsChan, p.errorChan, p.stopScan)
|
|
} else {
|
|
p.isScanning = false
|
|
}
|
|
}
|
|
|
|
// HandleEvent processes user input.
|
|
func (p *WiFiSetupPage) HandleEvent(event *sdl.Event) error {
|
|
switch e := (*event).(type) {
|
|
|
|
case *sdl.TextInputEvent:
|
|
if p.isEnteringPassword {
|
|
// If we get a TextInputEvent, the system supports high-level input.
|
|
// Mark it supported so we don't double-type from KeyboardEvents.
|
|
if !p.textInputSupported {
|
|
p.textInputSupported = true
|
|
} else {
|
|
p.passwordInput += e.GetText()
|
|
p.updatePasswordTexture()
|
|
}
|
|
}
|
|
|
|
case *sdl.KeyboardEvent:
|
|
if e.Type != sdl.KEYDOWN {
|
|
return nil
|
|
}
|
|
|
|
if p.isEnteringPassword {
|
|
// Pass the Modifiers (Shift/Caps) to the handler
|
|
p.handlePasswordInput(e.Keysym.Sym, e.Keysym.Mod)
|
|
} else {
|
|
p.handleNetworkSelection(e.Keysym.Sym)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handlePasswordInput manages keyboard events during password entry.
|
|
// Update the signature to include mod
|
|
// handlePasswordInput manages keyboard events during password entry.
|
|
func (p *WiFiSetupPage) handlePasswordInput(sym sdl.Keycode, mod uint16) {
|
|
switch sym {
|
|
|
|
case sdl.K_RETURN:
|
|
p.exitPasswordMode()
|
|
|
|
p.pendingSSID = p.selectedAP.SSID
|
|
p.isConnecting = true
|
|
p.connectionError = ""
|
|
p.connectionAttemptTime = time.Now()
|
|
|
|
|
|
err := p.networkManager.Connect(*p.wifiDevice, *p.selectedAP, p.passwordInput)
|
|
if err != nil {
|
|
p.isConnecting = false
|
|
p.pendingSSID = ""
|
|
p.connectionError = "Failed to initiate connection"
|
|
}
|
|
|
|
p.isEnteringPassword = false
|
|
|
|
case sdl.K_BACKSPACE:
|
|
if len(p.passwordInput) > 0 {
|
|
p.passwordInput = p.passwordInput[:len(p.passwordInput)-1]
|
|
logger.Info("WiFi Password backspace pressed")
|
|
p.updatePasswordTexture()
|
|
}
|
|
|
|
case sdl.K_ESCAPE:
|
|
logger.Info("WiFi Password entry cancelled")
|
|
p.isEnteringPassword = false
|
|
p.pendingSSID = ""
|
|
p.connectionError = ""
|
|
p.exitPasswordMode()
|
|
|
|
default:
|
|
// FALLBACK: If the kiosk environment doesn't fire TextInputEvents,
|
|
// manually convert keycodes to characters.
|
|
if !p.textInputSupported {
|
|
if ch := keycodeToChar(sym, mod); ch != 0 {
|
|
p.passwordInput += string(ch)
|
|
logger.Info("Manual character fallback: %c", ch)
|
|
p.updatePasswordTexture()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// enterPasswordMode disables global keybinds and starts SDL text input.
|
|
func (p *WiFiSetupPage) enterPasswordMode() {
|
|
keybinds.Enabled = false
|
|
|
|
p.passwordInput = ""
|
|
|
|
logger.Info("WiFi Password entry started")
|
|
|
|
sdl.StartTextInput()
|
|
}
|
|
|
|
// exitPasswordMode re-enables global keybinds and stops SDL text input.
|
|
func (p *WiFiSetupPage) exitPasswordMode() {
|
|
logger.Info("WiFi Password entry finished")
|
|
|
|
keybinds.Enabled = true
|
|
sdl.StopTextInput()
|
|
}
|
|
|
|
// keycodeToChar converts an SDL keycode to a rune, respecting SHIFT state.
|
|
// Returns 0 for non-printable keys.
|
|
func keycodeToChar(sym sdl.Keycode, mod uint16) rune {
|
|
shifted := mod&sdl.KMOD_SHIFT != 0
|
|
|
|
// Letters
|
|
if sym >= sdl.K_a && sym <= sdl.K_z {
|
|
ch := rune(sym)
|
|
if shifted {
|
|
ch -= 'a' - 'A'
|
|
}
|
|
logger.Info("Wifi Password entered: %v", ch)
|
|
return ch
|
|
}
|
|
|
|
// Digits and their shifted symbols
|
|
if sym >= sdl.K_0 && sym <= sdl.K_9 {
|
|
if shifted {
|
|
shiftDigits := []rune{')', '!', '@', '#', '$', '%', '^', '&', '*', '('}
|
|
return shiftDigits[sym-sdl.K_0]
|
|
}
|
|
return rune(sym)
|
|
}
|
|
|
|
// Common punctuation / symbols
|
|
type charPair struct{ normal, shifted rune }
|
|
punctuation := map[sdl.Keycode]charPair{
|
|
sdl.K_SPACE: {' ', ' '},
|
|
sdl.K_MINUS: {'-', '_'},
|
|
sdl.K_EQUALS: {'=', '+'},
|
|
sdl.K_LEFTBRACKET: {'[', '{'},
|
|
sdl.K_RIGHTBRACKET: {']', '}'},
|
|
sdl.K_BACKSLASH: {'\\', '|'},
|
|
sdl.K_SEMICOLON: {';', ':'},
|
|
sdl.K_QUOTE: {'\'', '"'},
|
|
sdl.K_BACKQUOTE: {'`', '~'},
|
|
sdl.K_COMMA: {',', '<'},
|
|
sdl.K_PERIOD: {'.', '>'},
|
|
sdl.K_SLASH: {'/', '?'},
|
|
}
|
|
if pair, ok := punctuation[sym]; ok {
|
|
if shifted {
|
|
return pair.shifted
|
|
}
|
|
return pair.normal
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
// handleNetworkSelection manages keyboard events for the network list.
|
|
func (p *WiFiSetupPage) handleNetworkSelection(sym sdl.Keycode) {
|
|
switch sym {
|
|
case sdl.K_UP:
|
|
if p.selectedIndex > 0 {
|
|
p.selectedIndex--
|
|
}
|
|
case sdl.K_DOWN:
|
|
if p.selectedIndex < len(p.accessPoints)-1 {
|
|
p.selectedIndex++
|
|
}
|
|
case sdl.K_RETURN:
|
|
if len(p.accessPoints) > 0 && p.selectedIndex < len(p.accessPoints) {
|
|
p.selectedAP = &p.accessPoints[p.selectedIndex].AP
|
|
p.pendingSSID = p.selectedAP.SSID
|
|
p.connectionError = ""
|
|
if p.selectedAP.IsProtected {
|
|
p.isEnteringPassword = true
|
|
p.passwordInput = ""
|
|
p.updatePasswordTexture()
|
|
p.updatePasswordPromptTexture()
|
|
p.enterPasswordMode()
|
|
} else {
|
|
p.isConnecting = true
|
|
p.connectionAttemptTime = time.Now()
|
|
err := p.networkManager.Connect(*p.wifiDevice, *p.selectedAP, "")
|
|
if err != nil {
|
|
p.isConnecting = false
|
|
p.pendingSSID = ""
|
|
p.connectionError = "Failed to initiate connection"
|
|
}
|
|
}
|
|
}
|
|
case sdl.K_r:
|
|
if !p.isScanning {
|
|
p.startScan()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update checks for async results and updates UI state.
|
|
func (p *WiFiSetupPage) Update(dt float32) error {
|
|
select {
|
|
case results := <-p.resultsChan:
|
|
p.isScanning = false
|
|
p.buildNetworkUIList(results)
|
|
case status := <-p.statusChan:
|
|
p.connectionStatus = status
|
|
p.handleConnectionStatusUpdate(status)
|
|
p.updateConnectionStatusItem()
|
|
case err := <-p.errorChan:
|
|
p.isScanning = false
|
|
fmt.Printf("error scannign for networks %v", err)
|
|
default:
|
|
}
|
|
|
|
// Check for connection timeout
|
|
if p.isConnecting && time.Since(p.connectionAttemptTime) > p.connectionTimeout {
|
|
p.isConnecting = false
|
|
p.connectionError = "Connection timed out - Incorrect password?"
|
|
if p.selectedAP != nil && p.selectedAP.IsProtected {
|
|
p.isEnteringPassword = true
|
|
p.passwordInput = ""
|
|
p.updatePasswordTexture()
|
|
p.updatePasswordPromptTexture()
|
|
p.updatePasswordErrorTexture()
|
|
p.enterPasswordMode()
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// handleConnectionStatusUpdate CHECKS0 if connection succeeded or failed
|
|
func (p *WiFiSetupPage) handleConnectionStatusUpdate(status *network.ConnectionStatus) {
|
|
if !p.isConnecting {
|
|
return
|
|
}
|
|
|
|
// milestone 1: We are associated with an AP (even if internet is still pending)
|
|
isConnected := status.StatusCode == network.StatusWifiConnectedInternetUp ||
|
|
status.StatusCode == network.StatusWifiConnectedNoInternet
|
|
|
|
if isConnected {
|
|
activeSSID := p.getActiveSSID()
|
|
// If we see our SSID, OR if we're connected to anything while trying, call it a win.
|
|
if activeSSID == p.pendingSSID || activeSSID != "" {
|
|
logger.Info("UI synced: Connected to %s", activeSSID)
|
|
p.isConnecting = false
|
|
p.pendingSSID = ""
|
|
p.connectionError = ""
|
|
p.refreshNetworkColors()
|
|
return
|
|
}
|
|
}
|
|
|
|
// milestone 2: Handle the "Not Connected" state during the handshake
|
|
if status.StatusCode == network.StatusWifiAvailableNotConnected {
|
|
// INCREASE THIS: 5 seconds is way too aggressive.
|
|
// Give it at least 20 seconds before assuming the password was wrong.
|
|
if time.Since(p.connectionAttemptTime) > 20*time.Second {
|
|
if time.Since(p.connectionAttemptTime) > 15*time.Second {
|
|
p.isConnecting = false
|
|
p.connectionError = "Connection failed - Check password or signal"
|
|
p.isEnteringPassword = true
|
|
p.enterPasswordMode()
|
|
p.updatePasswordErrorTexture()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// getAiveSSID queries NetworkManager for the currently active WiFi SSID
|
|
func (p *WiFiSetupPage) getActiveSSID() string {
|
|
if p.networkManager == nil || p.wifiDevice == nil {
|
|
return ""
|
|
}
|
|
|
|
conn := p.networkManager.Conn()
|
|
|
|
devObj := conn.Object("org.freedesktop.NetworkManager", p.wifiDevice.Path)
|
|
activeApProp, err := devObj.GetProperty("org.freedesktop.NetworkManager.Device.Wireless.ActiveAccessPoint")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
activeApPath, ok := activeApProp.Value().(dbus.ObjectPath)
|
|
if !ok || activeApPath == "/" || activeApPath == "" {
|
|
return ""
|
|
}
|
|
|
|
apObj := conn.Object("org.freedesktop.NetworkManager", activeApPath)
|
|
ssidProp, err := apObj.GetProperty("org.freedesktop.NetworkManager.AccessPoint.Ssid")
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
|
|
ssidBytes, ok := ssidProp.Value().([]byte)
|
|
if !ok {
|
|
return ""
|
|
}
|
|
|
|
logger.Info("Active SSID: %s", string(ssidBytes))
|
|
|
|
return string(ssidBytes)
|
|
}
|
|
|
|
// refreshNetworkColors updates the colors of network items based on active status
|
|
func (p *WiFiSetupPage) refreshNetworkColors() {
|
|
activeSSID := p.getActiveSSID()
|
|
p.activeSSID = activeSSID
|
|
|
|
for i := range p.accessPoints {
|
|
if p.accessPoints[i].AP.SSID == activeSSID {
|
|
p.accessPoints[i].Item.Color = colors.LawnGreen
|
|
} else {
|
|
p.accessPoints[i].Item.Color = colors.White
|
|
}
|
|
}
|
|
}
|
|
|
|
// updateConnectionStatusItem creates the colored status message item.
|
|
func (p *WiFiSetupPage) updateConnectionStatusItem() {
|
|
if p.connectionStatusItem.ID != 0 {
|
|
gl.DeleteTextures(1, &p.connectionStatusItem.ID)
|
|
}
|
|
|
|
// Show connecting message if in progress
|
|
if p.isConnecting {
|
|
message := fmt.Sprintf("Connecting to %s...", p.pendingSSID)
|
|
item, err := NewStringItem(message, p.applicationFont, colors.Yellow)
|
|
if err == nil {
|
|
p.connectionStatusItem = item
|
|
}
|
|
return
|
|
}
|
|
|
|
// Show connection error if present
|
|
if p.connectionError != "" {
|
|
item, err := NewStringItem(p.connectionError, p.applicationFont, colors.Red)
|
|
if err == nil {
|
|
p.connectionStatusItem = item
|
|
}
|
|
return
|
|
}
|
|
|
|
if p.connectionStatus == nil {
|
|
return
|
|
}
|
|
|
|
switch p.connectionStatus.StatusCode {
|
|
case network.StatusWifiConnectedInternetUp:
|
|
message := fmt.Sprintf("Connected to %s - Internet accessible | F1 to exit", p.activeSSID)
|
|
item, err := NewStringItem(message, p.applicationFont, colors.LawnGreen)
|
|
if err == nil {
|
|
p.connectionStatusItem = item
|
|
}
|
|
p.refreshNetworkColors()
|
|
|
|
case network.StatusWifiAvailableNotConnected:
|
|
message := "WiFi available - Not connected | Select network or press R to rescan"
|
|
item, err := NewStringItem(message, p.applicationFont, colors.Yellow)
|
|
if err == nil {
|
|
p.connectionStatusItem = item
|
|
}
|
|
|
|
case network.StatusEthernetConnectedInternetUp:
|
|
message := "Ethernet connected - Internet accessible | F1 to exit"
|
|
item, err := NewStringItem(message, p.applicationFont, colors.LawnGreen)
|
|
if err == nil {
|
|
p.connectionStatusItem = item
|
|
}
|
|
|
|
case network.StatusEthernetNotConnected:
|
|
message := "No network connection | Select WiFi network or press R to rescan"
|
|
item, err := NewStringItem(message, p.applicationFont, colors.Red)
|
|
if err == nil {
|
|
p.connectionStatusItem = item
|
|
}
|
|
|
|
case network.StatusNoDevicesFound:
|
|
message := "No network devices found"
|
|
item, err := NewStringItem(message, p.applicationFont, colors.Red)
|
|
if err == nil {
|
|
p.connectionStatusItem = item
|
|
}
|
|
|
|
case network.StatusError:
|
|
message := "Network status check failed"
|
|
item, err := NewStringItem(message, p.applicationFont, colors.Red)
|
|
if err == nil {
|
|
p.connectionStatusItem = item
|
|
}
|
|
|
|
default:
|
|
item, err := NewStringItem(p.connectionStatus.Message, p.applicationFont, colors.White)
|
|
if err == nil {
|
|
p.connectionStatusItem = item
|
|
}
|
|
}
|
|
}
|
|
|
|
// buildNetworkUIList creates the renderable textures for the list of discovered networks.
|
|
func (p *WiFiSetupPage) buildNetworkUIList(aps []network.AccessPoint) {
|
|
// Get the currently active Wi-Fi SSID
|
|
activeSSID := p.getActiveSSID()
|
|
p.activeSSID = activeSSID
|
|
|
|
// Set IsActive flag based on the actual active SSID
|
|
for i := range aps {
|
|
aps[i].IsActive = (aps[i].SSID == p.activeSSID)
|
|
}
|
|
|
|
p.accessPoints = make([]accessPointUIItem, len(aps))
|
|
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
|
|
|
|
for i, ap := range aps {
|
|
ssid := ap.SSID
|
|
if ap.IsProtected {
|
|
ssid += " [L]"
|
|
}
|
|
color := white
|
|
if ap.IsActive {
|
|
color = colors.LawnGreen
|
|
}
|
|
item, err := NewStringItem(ssid, p.applicationFont, color)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
p.accessPoints[i] = accessPointUIItem{AP: ap, Item: item}
|
|
}
|
|
}
|
|
|
|
// updatePasswordTexture regenerates the texture for the password input field.
|
|
func (p *WiFiSetupPage) updatePasswordTexture() {
|
|
if p.passwordInputItem.ID != 0 {
|
|
gl.DeleteTextures(1, &p.passwordInputItem.ID)
|
|
}
|
|
maskedPassword := strings.Repeat("*", len(p.passwordInput))
|
|
if maskedPassword == "" {
|
|
maskedPassword = " "
|
|
}
|
|
item, err := NewStringItem(maskedPassword, p.applicationFont, sdl.Color{R: 255, G: 255, B: 255, A: 255})
|
|
if err == nil {
|
|
p.passwordInputItem = item
|
|
}
|
|
}
|
|
|
|
// updatePasswordPromptTexture creates the texture for the password prompt label.
|
|
func (p *WiFiSetupPage) updatePasswordPromptTexture() {
|
|
if p.passwordPromptItem.ID != 0 {
|
|
gl.DeleteTextures(1, &p.passwordPromptItem.ID)
|
|
}
|
|
prompt := fmt.Sprintf("Password for %s:", p.selectedAP.SSID)
|
|
item, err := NewStringItem(prompt, p.applicationFont, sdl.Color{R: 255, G: 255, B: 255, A: 255})
|
|
if err == nil {
|
|
p.passwordPromptItem = item
|
|
}
|
|
}
|
|
|
|
// updatePasswordErrorTexture creates the texture for password error message.
|
|
func (p *WiFiSetupPage) updatePasswordErrorTexture() {
|
|
if p.passwordErrorItem.ID != 0 {
|
|
gl.DeleteTextures(1, &p.passwordErrorItem.ID)
|
|
}
|
|
if p.connectionError != "" {
|
|
item, err := NewStringItem(p.connectionError, p.applicationFont, colors.DarkRed)
|
|
if err == nil {
|
|
p.passwordErrorItem = item
|
|
}
|
|
}
|
|
}
|
|
|
|
// Render draws the entire page.
|
|
func (p *WiFiSetupPage) Render() error {
|
|
// Get base font size from database
|
|
baseFontSize := p.Context.CfgPtr.StandardFontSize
|
|
|
|
// Colors & Config
|
|
bgColor := sdl.Color{R: 0, G: 0, B: 0, A: 255}
|
|
selectionColor := sdl.Color{R: 68, G: 71, B: 90, A: 255}
|
|
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
|
|
verticalMargin := float32(20)
|
|
|
|
solidShader, _ := shaderManager.Get().Get("solid_color")
|
|
textShader, _ := shaderManager.Get().Get("text")
|
|
|
|
// Background
|
|
p.Base.RenderSolidColorQuad(solidShader, mgl32.Vec2{0, 0}, mgl32.Vec2{float32(p.ScreenWidth), float32(p.ScreenHeight)}, bgColor)
|
|
|
|
// Title
|
|
titleScale := float32(48) / float32(baseFontSize)
|
|
titleW := float32(p.titleItem.W) * titleScale
|
|
titleH := float32(p.titleItem.H) * titleScale
|
|
titleX := float32(p.ScreenCenterX) - (titleW / 2)
|
|
titleY := float32(p.ScreenHeight) - titleH - 40
|
|
p.Base.RenderTexture(textShader, p.titleItem.ID, mgl32.Vec2{titleX, titleY}, mgl32.Vec2{titleW, titleH}, white)
|
|
|
|
// Status message
|
|
statusY := titleY - titleH - verticalMargin
|
|
if p.isScanning {
|
|
statusScale := float32(24) / float32(baseFontSize)
|
|
statusW := float32(p.scanningStatusItem.W) * statusScale
|
|
statusH := float32(p.scanningStatusItem.H) * statusScale
|
|
statusX := float32(p.ScreenCenterX) - (statusW / 2)
|
|
p.Base.RenderTexture(textShader, p.scanningStatusItem.ID, mgl32.Vec2{statusX, statusY}, mgl32.Vec2{statusW, statusH}, white)
|
|
} else if p.connectionStatusItem.ID != 0 {
|
|
statusScale := float32(24) / float32(baseFontSize)
|
|
statusW := float32(p.connectionStatusItem.W) * statusScale
|
|
statusH := float32(p.connectionStatusItem.H) * statusScale
|
|
statusX := float32(p.ScreenCenterX) - (statusW / 2)
|
|
p.Base.RenderTexture(textShader, p.connectionStatusItem.ID, mgl32.Vec2{statusX, statusY}, mgl32.Vec2{statusW, statusH}, p.connectionStatusItem.Color)
|
|
}
|
|
|
|
// Network list
|
|
listY := statusY - verticalMargin
|
|
if len(p.accessPoints) > 0 {
|
|
itemHeight := int32(40)
|
|
listPadding := int32(10)
|
|
for i, apItem := range p.accessPoints {
|
|
itemYpos := listY - float32(int32(i+1)*itemHeight)
|
|
itemXpos := float32(p.ScreenCenterX - 350)
|
|
itemW := float32(700)
|
|
if i == p.selectedIndex {
|
|
p.Base.RenderSolidColorQuad(solidShader, mgl32.Vec2{itemXpos, itemYpos}, mgl32.Vec2{itemW, float32(itemHeight)}, selectionColor)
|
|
}
|
|
|
|
textScale := float32(24) / float32(baseFontSize)
|
|
textW := float32(apItem.Item.W) * textScale
|
|
textH := float32(apItem.Item.H) * textScale
|
|
textY := itemYpos + (float32(itemHeight) / 2) - (textH / 2)
|
|
textX := itemXpos + float32(listPadding)
|
|
p.Base.RenderTexture(textShader, apItem.Item.ID, mgl32.Vec2{textX, textY}, mgl32.Vec2{textW, textH}, apItem.Item.Color)
|
|
}
|
|
}
|
|
|
|
// Password dialog overlay
|
|
if p.isEnteringPassword {
|
|
if err := p.renderPasswordDialog(solidShader, textShader, float32(baseFontSize)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// renderPasswordDialog draws the password entry modal.
|
|
func (p *WiFiSetupPage) renderPasswordDialog(solidShader, textShader *shaderManager.Shader, baseFontSize float32) error {
|
|
overlayColor := sdl.Color{R: 0, G: 0, B: 0, A: 150}
|
|
dialogBgColor := sdl.Color{R: 40, G: 42, B: 54, A: 255}
|
|
boxBorderColor := sdl.Color{R: 120, G: 120, B: 120, A: 255}
|
|
boxBgColor := sdl.Color{R: 30, G: 30, B: 30, A: 255}
|
|
white := sdl.Color{R: 255, G: 255, B: 255, A: 255}
|
|
|
|
dialogW, dialogH := int32(600), int32(250)
|
|
if p.connectionError != "" {
|
|
dialogH = int32(300) // Make dialog taller if there's an error
|
|
}
|
|
dialogX, dialogY := p.ScreenCenterX-(dialogW/2), p.ScreenCenterY-(dialogH/2)
|
|
|
|
// Overlay and dialog background
|
|
p.Base.RenderSolidColorQuad(solidShader, mgl32.Vec2{0, 0}, mgl32.Vec2{float32(p.ScreenWidth), float32(p.ScreenHeight)}, overlayColor)
|
|
p.Base.RenderSolidColorQuad(solidShader, mgl32.Vec2{float32(dialogX), float32(dialogY)}, mgl32.Vec2{float32(dialogW), float32(dialogH)}, dialogBgColor)
|
|
|
|
currentY := float32(dialogY+dialogH) - 30
|
|
|
|
// Password error (if exists) - shown at the top in red
|
|
if p.passwordErrorItem.ID != 0 {
|
|
errorScale := float32(20) / baseFontSize
|
|
errorW := float32(p.passwordErrorItem.W) * errorScale
|
|
errorH := float32(p.passwordErrorItem.H) * errorScale
|
|
errorX := float32(p.ScreenCenterX) - (errorW / 2)
|
|
currentY -= errorH
|
|
p.Base.RenderTexture(textShader, p.passwordErrorItem.ID, mgl32.Vec2{errorX, currentY}, mgl32.Vec2{errorW, errorH}, colors.DarkRed)
|
|
currentY -= 20
|
|
}
|
|
|
|
// Password prompt
|
|
if p.passwordPromptItem.ID != 0 {
|
|
promptScale := float32(24) / baseFontSize
|
|
promptW := float32(p.passwordPromptItem.W) * promptScale
|
|
promptH := float32(p.passwordPromptItem.H) * promptScale
|
|
promptX := float32(p.ScreenCenterX) - (promptW / 2)
|
|
currentY -= promptH
|
|
p.Base.RenderTexture(textShader, p.passwordPromptItem.ID, mgl32.Vec2{promptX, currentY}, mgl32.Vec2{promptW, promptH}, white)
|
|
currentY -= 30
|
|
}
|
|
|
|
// Input box
|
|
padding, border := int32(4), int32(3)
|
|
boxW := int32(500)
|
|
targetFontSize := int32(36)
|
|
boxH := targetFontSize + (padding * 2)
|
|
borderBoxW, borderBoxH := boxW+(border*2), boxH+(border*2)
|
|
borderBoxX := p.ScreenCenterX - (borderBoxW / 2)
|
|
borderBoxY := int32(currentY) - boxH - (padding * 2)
|
|
boxX, boxY := p.ScreenCenterX-(boxW/2), int32(currentY)-boxH-padding
|
|
|
|
p.Base.RenderSolidColorQuad(solidShader, mgl32.Vec2{float32(borderBoxX), float32(borderBoxY)}, mgl32.Vec2{float32(borderBoxW), float32(borderBoxH)}, boxBorderColor)
|
|
p.Base.RenderSolidColorQuad(solidShader, mgl32.Vec2{float32(boxX), float32(boxY)}, mgl32.Vec2{float32(boxW), float32(boxH)}, boxBgColor)
|
|
|
|
// Password input text
|
|
if p.passwordInputItem.ID != 0 {
|
|
inputScale := float32(targetFontSize) / baseFontSize
|
|
inputW := float32(p.passwordInputItem.W) * inputScale
|
|
inputH := float32(p.passwordInputItem.H) * inputScale
|
|
inputX := float32(boxX + padding)
|
|
inputY := float32(boxY + padding)
|
|
p.Base.RenderTexture(textShader, p.passwordInputItem.ID, mgl32.Vec2{inputX, inputY}, mgl32.Vec2{inputW, inputH}, white)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Destroy cleans up all resources used by the page.
|
|
func (p *WiFiSetupPage) Destroy() error {
|
|
// Re-enable global keybinds in case we're destroyed during password entry.
|
|
keybinds.Enabled = true
|
|
|
|
if p.stopScan != nil {
|
|
close(p.stopScan)
|
|
p.stopScan = nil
|
|
}
|
|
|
|
p.networkManager.Close()
|
|
|
|
gl.DeleteTextures(1, &p.titleItem.ID)
|
|
gl.DeleteTextures(1, &p.scanningStatusItem.ID)
|
|
if p.connectionStatusItem.ID != 0 {
|
|
gl.DeleteTextures(1, &p.connectionStatusItem.ID)
|
|
}
|
|
if p.passwordPromptItem.ID != 0 {
|
|
gl.DeleteTextures(1, &p.passwordPromptItem.ID)
|
|
}
|
|
if p.passwordInputItem.ID != 0 {
|
|
gl.DeleteTextures(1, &p.passwordInputItem.ID)
|
|
}
|
|
if p.passwordErrorItem.ID != 0 {
|
|
gl.DeleteTextures(1, &p.passwordErrorItem.ID)
|
|
}
|
|
|
|
for _, apItem := range p.accessPoints {
|
|
if apItem.Item.ID != 0 {
|
|
gl.DeleteTextures(1, &apItem.Item.ID)
|
|
}
|
|
}
|
|
|
|
return p.Base.Destroy()
|
|
}
|