657 lines
17 KiB
Go
657 lines
17 KiB
Go
package page
|
|
|
|
import (
|
|
"context"
|
|
"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/fontManager"
|
|
"rhemawavetech.com/rhemawave/internal/keybinds"
|
|
"rhemawavetech.com/rhemawave/internal/logger"
|
|
"rhemawavetech.com/rhemawave/internal/network"
|
|
"rhemawavetech.com/rhemawave/internal/shaderManager"
|
|
)
|
|
|
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
type networkUIItem struct {
|
|
Net network.Network
|
|
Item StringItem
|
|
}
|
|
|
|
type wifiStatus int
|
|
|
|
const (
|
|
wifiStatusIdle wifiStatus = iota
|
|
wifiStatusScanning
|
|
wifiStatusConnecting
|
|
wifiStatusConnected
|
|
wifiStatusError
|
|
)
|
|
|
|
// ─── Page ─────────────────────────────────────────────────────────────────────
|
|
|
|
type WiFiSetupPage struct {
|
|
Base
|
|
applicationFont *fontManager.FontEntry
|
|
textInputSupported bool
|
|
|
|
// UI elements
|
|
BaseFontSize int
|
|
titleItem StringItem
|
|
scanningStatusItem StringItem
|
|
connectionStatusItem StringItem
|
|
passwordPromptItem StringItem
|
|
passwordInputItem StringItem
|
|
passwordErrorItem StringItem
|
|
|
|
// Network list
|
|
networks []networkUIItem
|
|
selectedIndex int
|
|
|
|
// State
|
|
status wifiStatus
|
|
statusMessage string
|
|
connectError string
|
|
|
|
// Password entry
|
|
isEnteringPassword bool
|
|
passwordInput string
|
|
selectedNet *network.Network
|
|
|
|
// Async
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
networksCh <-chan []network.Network
|
|
connectResultCh chan error
|
|
}
|
|
|
|
// ─── Init ─────────────────────────────────────────────────────────────────────
|
|
|
|
func (p *WiFiSetupPage) Init(ctx *PageCtx) error {
|
|
if err := p.Base.Init(ctx); err != nil {
|
|
return fmt.Errorf("failed base page initialization: %w", err)
|
|
}
|
|
|
|
fm := fontManager.Get()
|
|
font, ok := fm.GetFont("SpaceMonoNerdFont-Regular")
|
|
if !ok {
|
|
return fmt.Errorf("could not find application font: SpaceMonoNerdFont-Regular")
|
|
}
|
|
p.applicationFont = font
|
|
|
|
var err error
|
|
|
|
p.titleItem, err = NewStringItem("Wi-Fi Setup", p.applicationFont, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create title item: %w", err)
|
|
}
|
|
p.scanningStatusItem, err = NewStringItem("Scanning for networks...", p.applicationFont, colors.White)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create scanning status item: %w", err)
|
|
}
|
|
|
|
config, err := db.GetConfig(p.Context.DbPtr)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
p.BaseFontSize = config.StandardFontSize
|
|
|
|
p.connectResultCh = make(chan error, 1)
|
|
p.ctx, p.cancel = context.WithCancel(context.Background())
|
|
|
|
p.startScan()
|
|
|
|
return nil
|
|
}
|
|
|
|
// ─── Scan ─────────────────────────────────────────────────────────────────────
|
|
|
|
func (p *WiFiSetupPage) startScan() {
|
|
p.status = wifiStatusScanning
|
|
p.statusMessage = "Scanning for networks..."
|
|
p.selectedIndex = 0
|
|
p.networks = nil
|
|
|
|
var err error
|
|
p.networksCh, err = network.ScanForNetworks(p.ctx)
|
|
if err != nil {
|
|
logger.Error("wifi: failed to start scan: %v", err)
|
|
p.status = wifiStatusError
|
|
p.statusMessage = "Failed to start network scan. No Wi-Fi adapter found."
|
|
p.rebuildStatusTexture()
|
|
}
|
|
}
|
|
|
|
// ─── Input ────────────────────────────────────────────────────────────────────
|
|
|
|
func (p *WiFiSetupPage) HandleEvent(event *sdl.Event) error {
|
|
switch e := (*event).(type) {
|
|
case *sdl.KeyboardEvent:
|
|
if e.Type != sdl.KEYDOWN {
|
|
return nil
|
|
}
|
|
if p.isEnteringPassword {
|
|
p.handlePasswordKey(e.Keysym.Sym, e.Keysym.Mod)
|
|
} else {
|
|
p.handleNavKey(e.Keysym.Sym)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (p *WiFiSetupPage) handlePasswordKey(sym sdl.Keycode, mod uint16) {
|
|
switch sym {
|
|
case sdl.K_RETURN:
|
|
p.submitPassword()
|
|
case sdl.K_BACKSPACE:
|
|
if len(p.passwordInput) > 0 {
|
|
runes := []rune(p.passwordInput)
|
|
p.passwordInput = string(runes[:len(runes)-1])
|
|
p.rebuildPasswordTexture()
|
|
}
|
|
case sdl.K_ESCAPE:
|
|
logger.Info("wifi: password entry cancelled")
|
|
p.exitPasswordMode()
|
|
p.connectError = ""
|
|
p.selectedNet = nil
|
|
default:
|
|
if ch := keycodeToChar(sym, mod); ch != 0 {
|
|
p.passwordInput += string(ch)
|
|
p.rebuildPasswordTexture()
|
|
}
|
|
}
|
|
}
|
|
|
|
func (p *WiFiSetupPage) handleNavKey(sym sdl.Keycode) {
|
|
switch sym {
|
|
case sdl.K_UP:
|
|
if p.selectedIndex > 0 {
|
|
p.selectedIndex--
|
|
}
|
|
|
|
case sdl.K_DOWN:
|
|
if p.selectedIndex < len(p.networks)-1 {
|
|
p.selectedIndex++
|
|
}
|
|
|
|
case sdl.K_RETURN:
|
|
if len(p.networks) == 0 || p.selectedIndex >= len(p.networks) {
|
|
return
|
|
}
|
|
selected := p.networks[p.selectedIndex].Net
|
|
p.selectedNet = &p.networks[p.selectedIndex].Net
|
|
p.connectError = ""
|
|
|
|
if selected.Security != network.SecurityOpen {
|
|
p.passwordInput = ""
|
|
p.enterPasswordMode()
|
|
} else {
|
|
p.connect("")
|
|
}
|
|
|
|
case sdl.K_r:
|
|
if p.status != wifiStatusScanning && p.status != wifiStatusConnecting {
|
|
p.startScan()
|
|
}
|
|
|
|
case sdl.K_ESCAPE:
|
|
p.selectedNet = nil
|
|
p.connectError = ""
|
|
}
|
|
}
|
|
|
|
func (p *WiFiSetupPage) enterPasswordMode() {
|
|
keybinds.Enabled = false
|
|
p.isEnteringPassword = true
|
|
p.textInputSupported = false
|
|
p.rebuildPasswordPromptTexture()
|
|
p.rebuildPasswordTexture()
|
|
sdl.StartTextInput()
|
|
logger.Info("wifi: password entry started for %q", p.selectedNet.SSID)
|
|
}
|
|
|
|
func (p *WiFiSetupPage) exitPasswordMode() {
|
|
keybinds.Enabled = true
|
|
p.isEnteringPassword = false
|
|
sdl.StopTextInput()
|
|
logger.Info("wifi: password entry finished")
|
|
}
|
|
|
|
func (p *WiFiSetupPage) submitPassword() {
|
|
p.exitPasswordMode()
|
|
p.connect(p.passwordInput)
|
|
}
|
|
|
|
// ─── Connect ──────────────────────────────────────────────────────────────────
|
|
|
|
func (p *WiFiSetupPage) connect(password string) {
|
|
if p.selectedNet == nil {
|
|
return
|
|
}
|
|
|
|
ssid := p.selectedNet.SSID
|
|
p.status = wifiStatusConnecting
|
|
p.statusMessage = fmt.Sprintf("Connecting to \"%s\"...", ssid)
|
|
logger.Info("wifi: connecting to %q", ssid)
|
|
|
|
// ConnectToNetwork blocks (up to 15s), so run it off the main goroutine.
|
|
go func() {
|
|
err := network.ConnectToNetwork(ssid, password)
|
|
p.connectResultCh <- err
|
|
}()
|
|
}
|
|
|
|
// ─── Update ───────────────────────────────────────────────────────────────────
|
|
|
|
func (p *WiFiSetupPage) Update(dt float32) error {
|
|
select {
|
|
|
|
case nets, ok := <-p.networksCh:
|
|
if ok {
|
|
p.rebuildNetworkList(nets)
|
|
if p.status == wifiStatusScanning {
|
|
p.status = wifiStatusIdle
|
|
p.statusMessage = ""
|
|
}
|
|
}
|
|
|
|
case err := <-p.connectResultCh:
|
|
if err != nil {
|
|
logger.Error("wifi: connection failed: %v", err)
|
|
p.status = wifiStatusError
|
|
p.connectError = fmt.Sprintf("Could not connect: %s", err.Error())
|
|
p.statusMessage = ""
|
|
|
|
// Drop back into password entry so the user can try again.
|
|
if p.selectedNet != nil && p.selectedNet.Security != network.SecurityOpen {
|
|
p.passwordInput = ""
|
|
p.rebuildPasswordErrorTexture()
|
|
p.enterPasswordMode()
|
|
}
|
|
} else {
|
|
ssid := ""
|
|
if p.selectedNet != nil {
|
|
ssid = p.selectedNet.SSID
|
|
}
|
|
logger.Info("wifi: connected to %q", ssid)
|
|
p.status = wifiStatusConnected
|
|
p.connectError = ""
|
|
p.statusMessage = fmt.Sprintf("Connected to \"%s\" — press F1 to continue", ssid)
|
|
// Trigger a fresh scan so the list shows the connected state.
|
|
p.startScan()
|
|
}
|
|
|
|
default:
|
|
}
|
|
|
|
p.rebuildStatusTexture()
|
|
|
|
return nil
|
|
}
|
|
|
|
// ─── Texture builders ─────────────────────────────────────────────────────────
|
|
|
|
func (p *WiFiSetupPage) rebuildNetworkList(nets []network.Network) {
|
|
for _, item := range p.networks {
|
|
if item.Item.ID != 0 {
|
|
gl.DeleteTextures(1, &item.Item.ID)
|
|
}
|
|
}
|
|
|
|
p.networks = make([]networkUIItem, 0, len(nets))
|
|
|
|
for _, n := range nets {
|
|
label := n.SSID
|
|
switch {
|
|
case n.Connected:
|
|
label += " ✓"
|
|
case n.Saved:
|
|
label += " ★"
|
|
}
|
|
if n.Security != network.SecurityOpen {
|
|
var secStr string
|
|
switch n.Security {
|
|
case network.SecurityWEP:
|
|
secStr = "WEP"
|
|
case network.SecurityWPA:
|
|
secStr = "WPA"
|
|
case network.SecurityWPA2:
|
|
secStr = "WPA2"
|
|
case network.SecurityWPA3:
|
|
secStr = "WPA3"
|
|
default:
|
|
secStr = "Secured"
|
|
}
|
|
label += " [" + secStr + "]"
|
|
}
|
|
|
|
c := colors.White
|
|
if n.Connected {
|
|
c = colors.LawnGreen
|
|
}
|
|
|
|
item, err := NewStringItem(label, p.applicationFont, c)
|
|
if err != nil {
|
|
logger.Warning("wifi: failed to create item for %q: %v", n.SSID, err)
|
|
continue
|
|
}
|
|
p.networks = append(p.networks, networkUIItem{Net: n, Item: item})
|
|
}
|
|
}
|
|
|
|
func (p *WiFiSetupPage) rebuildStatusTexture() {
|
|
if p.connectionStatusItem.ID != 0 {
|
|
gl.DeleteTextures(1, &p.connectionStatusItem.ID)
|
|
p.connectionStatusItem.ID = 0
|
|
}
|
|
|
|
var msg string
|
|
var c sdl.Color
|
|
|
|
switch p.status {
|
|
case wifiStatusScanning:
|
|
msg = "Scanning for nearby networks, please wait..."
|
|
c = colors.White
|
|
case wifiStatusConnecting:
|
|
msg = p.statusMessage
|
|
c = colors.Yellow
|
|
case wifiStatusConnected:
|
|
msg = p.statusMessage
|
|
c = colors.LawnGreen
|
|
case wifiStatusError:
|
|
msg = p.connectError
|
|
if msg == "" {
|
|
msg = "Something went wrong. Press R to scan again."
|
|
}
|
|
c = colors.Red
|
|
default:
|
|
if len(p.networks) == 0 {
|
|
msg = "No networks found. Press R to scan again."
|
|
c = colors.Yellow
|
|
} else {
|
|
msg = "Select a network to connect — press R to rescan"
|
|
c = colors.White
|
|
}
|
|
}
|
|
|
|
if msg == "" {
|
|
return
|
|
}
|
|
|
|
item, err := NewStringItem(msg, p.applicationFont, c)
|
|
if err == nil {
|
|
p.connectionStatusItem = item
|
|
}
|
|
}
|
|
|
|
func (p *WiFiSetupPage) rebuildPasswordTexture() {
|
|
if p.passwordInputItem.ID != 0 {
|
|
gl.DeleteTextures(1, &p.passwordInputItem.ID)
|
|
}
|
|
text := p.passwordInput
|
|
if text == "" {
|
|
text = " " // prevent empty texture
|
|
}
|
|
item, err := NewStringItem(text, p.applicationFont, colors.White)
|
|
if err == nil {
|
|
p.passwordInputItem = item
|
|
}
|
|
}
|
|
|
|
func (p *WiFiSetupPage) rebuildPasswordPromptTexture() {
|
|
if p.passwordPromptItem.ID != 0 {
|
|
gl.DeleteTextures(1, &p.passwordPromptItem.ID)
|
|
}
|
|
if p.selectedNet == nil {
|
|
return
|
|
}
|
|
prompt := fmt.Sprintf("Enter password for \"%s\":", p.selectedNet.SSID)
|
|
item, err := NewStringItem(prompt, p.applicationFont, colors.White)
|
|
if err == nil {
|
|
p.passwordPromptItem = item
|
|
}
|
|
}
|
|
|
|
func (p *WiFiSetupPage) rebuildPasswordErrorTexture() {
|
|
if p.passwordErrorItem.ID != 0 {
|
|
gl.DeleteTextures(1, &p.passwordErrorItem.ID)
|
|
p.passwordErrorItem.ID = 0
|
|
}
|
|
if p.connectError == "" {
|
|
return
|
|
}
|
|
item, err := NewStringItem(p.connectError, p.applicationFont, colors.Red)
|
|
if err == nil {
|
|
p.passwordErrorItem = item
|
|
}
|
|
}
|
|
|
|
// ─── Render ───────────────────────────────────────────────────────────────────
|
|
|
|
func (p *WiFiSetupPage) Render() error {
|
|
solidShader, _ := shaderManager.Get().Get("solid_color")
|
|
textShader, _ := shaderManager.Get().Get("text")
|
|
|
|
bgColor := sdl.Color{R: 0, G: 0, B: 0, A: 255}
|
|
selColor := sdl.Color{R: 68, G: 71, B: 90, A: 255}
|
|
white := colors.White
|
|
vertMargin := float32(20)
|
|
|
|
// Background
|
|
p.Base.RenderSolidColorQuad(solidShader,
|
|
mgl32.Vec2{0, 0},
|
|
mgl32.Vec2{float32(p.ScreenWidth), float32(p.ScreenHeight)},
|
|
bgColor,
|
|
)
|
|
|
|
// Title — 48pt
|
|
titleScale := float32(48) / float32(p.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 — 30pt (was 24pt, now 1.25x)
|
|
statusY := titleY - titleH - vertMargin
|
|
if p.status == wifiStatusScanning {
|
|
scale := float32(30) / float32(p.BaseFontSize)
|
|
w := float32(p.scanningStatusItem.W) * scale
|
|
h := float32(p.scanningStatusItem.H) * scale
|
|
x := float32(p.ScreenCenterX) - w/2
|
|
p.Base.RenderTexture(textShader, p.scanningStatusItem.ID,
|
|
mgl32.Vec2{x, statusY},
|
|
mgl32.Vec2{w, h},
|
|
white,
|
|
)
|
|
} else if p.connectionStatusItem.ID != 0 {
|
|
scale := float32(30) / float32(p.BaseFontSize)
|
|
w := float32(p.connectionStatusItem.W) * scale
|
|
h := float32(p.connectionStatusItem.H) * scale
|
|
x := float32(p.ScreenCenterX) - w/2
|
|
p.Base.RenderTexture(textShader, p.connectionStatusItem.ID,
|
|
mgl32.Vec2{x, statusY},
|
|
mgl32.Vec2{w, h},
|
|
p.connectionStatusItem.Color,
|
|
)
|
|
}
|
|
|
|
// Network list
|
|
listY := statusY - vertMargin
|
|
itemH := int32(40)
|
|
listPad := int32(10)
|
|
listW := float32(700)
|
|
listX := float32(p.ScreenCenterX) - listW/2
|
|
|
|
for i, item := range p.networks {
|
|
rowY := listY - float32(int32(i+1)*itemH)
|
|
|
|
if i == p.selectedIndex {
|
|
p.Base.RenderSolidColorQuad(solidShader,
|
|
mgl32.Vec2{listX, rowY},
|
|
mgl32.Vec2{listW, float32(itemH)},
|
|
selColor,
|
|
)
|
|
}
|
|
|
|
scale := float32(24) / float32(p.BaseFontSize)
|
|
tw := float32(item.Item.W) * scale
|
|
th := float32(item.Item.H) * scale
|
|
tx := listX + float32(listPad)
|
|
ty := rowY + (float32(itemH)/2 - th/2)
|
|
p.Base.RenderTexture(textShader, item.Item.ID,
|
|
mgl32.Vec2{tx, ty},
|
|
mgl32.Vec2{tw, th},
|
|
item.Item.Color,
|
|
)
|
|
}
|
|
|
|
// Password dialog
|
|
if p.isEnteringPassword {
|
|
if err := p.renderPasswordDialog(solidShader, textShader, float32(p.BaseFontSize)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *WiFiSetupPage) renderPasswordDialog(solidShader, textShader *shaderManager.Shader, baseFontSize float32) error {
|
|
overlayColor := sdl.Color{R: 0, G: 0, B: 0, A: 180}
|
|
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 := colors.White
|
|
|
|
dialogW := int32(600)
|
|
dialogH := int32(250)
|
|
if p.connectError != "" {
|
|
dialogH = 310
|
|
}
|
|
dialogX := p.ScreenCenterX - dialogW/2
|
|
dialogY := p.ScreenCenterY - dialogH/2
|
|
|
|
// Overlay + dialog box
|
|
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
|
|
|
|
// Error message
|
|
if p.passwordErrorItem.ID != 0 {
|
|
scale := float32(20) / baseFontSize
|
|
w := float32(p.passwordErrorItem.W) * scale
|
|
h := float32(p.passwordErrorItem.H) * scale
|
|
x := float32(p.ScreenCenterX) - w/2
|
|
currentY -= h
|
|
p.Base.RenderTexture(textShader, p.passwordErrorItem.ID,
|
|
mgl32.Vec2{x, currentY},
|
|
mgl32.Vec2{w, h},
|
|
colors.Red,
|
|
)
|
|
currentY -= 20
|
|
}
|
|
|
|
// Prompt
|
|
if p.passwordPromptItem.ID != 0 {
|
|
scale := float32(24) / baseFontSize
|
|
w := float32(p.passwordPromptItem.W) * scale
|
|
h := float32(p.passwordPromptItem.H) * scale
|
|
x := float32(p.ScreenCenterX) - w/2
|
|
currentY -= h
|
|
p.Base.RenderTexture(textShader, p.passwordPromptItem.ID,
|
|
mgl32.Vec2{x, currentY},
|
|
mgl32.Vec2{w, h},
|
|
white,
|
|
)
|
|
currentY -= 30
|
|
}
|
|
|
|
// Input box
|
|
padding := int32(4)
|
|
border := int32(3)
|
|
boxW := int32(500)
|
|
fontSize := int32(36)
|
|
boxH := fontSize + padding*2
|
|
bboxW := boxW + border*2
|
|
bboxH := boxH + border*2
|
|
bboxX := p.ScreenCenterX - bboxW/2
|
|
bboxY := int32(currentY) - boxH - padding*2
|
|
boxX := p.ScreenCenterX - boxW/2
|
|
boxY := int32(currentY) - boxH - padding
|
|
|
|
p.Base.RenderSolidColorQuad(solidShader,
|
|
mgl32.Vec2{float32(bboxX), float32(bboxY)},
|
|
mgl32.Vec2{float32(bboxW), float32(bboxH)},
|
|
boxBorderColor,
|
|
)
|
|
p.Base.RenderSolidColorQuad(solidShader,
|
|
mgl32.Vec2{float32(boxX), float32(boxY)},
|
|
mgl32.Vec2{float32(boxW), float32(boxH)},
|
|
boxBgColor,
|
|
)
|
|
|
|
// Password text (plain, unmasked)
|
|
if p.passwordInputItem.ID != 0 {
|
|
scale := float32(fontSize) / baseFontSize
|
|
w := float32(p.passwordInputItem.W) * scale
|
|
h := float32(p.passwordInputItem.H) * scale
|
|
p.Base.RenderTexture(textShader, p.passwordInputItem.ID,
|
|
mgl32.Vec2{float32(boxX + padding), float32(boxY + padding)},
|
|
mgl32.Vec2{w, h},
|
|
white,
|
|
)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ─── Destroy ──────────────────────────────────────────────────────────────────
|
|
|
|
func (p *WiFiSetupPage) Destroy() error {
|
|
keybinds.Enabled = true
|
|
sdl.StopTextInput()
|
|
|
|
// Cancel the scan context — stops ScanForNetworks goroutine.
|
|
if p.cancel != nil {
|
|
p.cancel()
|
|
}
|
|
|
|
deleteTexture := func(id *uint32) {
|
|
if *id != 0 {
|
|
gl.DeleteTextures(1, id)
|
|
*id = 0
|
|
}
|
|
}
|
|
|
|
deleteTexture(&p.titleItem.ID)
|
|
deleteTexture(&p.scanningStatusItem.ID)
|
|
deleteTexture(&p.connectionStatusItem.ID)
|
|
deleteTexture(&p.passwordPromptItem.ID)
|
|
deleteTexture(&p.passwordInputItem.ID)
|
|
deleteTexture(&p.passwordErrorItem.ID)
|
|
|
|
for _, item := range p.networks {
|
|
deleteTexture(&item.Item.ID)
|
|
}
|
|
|
|
return p.Base.Destroy()
|
|
}
|