feat(gui): redesign kanholec GUI with modern interface and WiX v7 installer
golangci-lint / lint (push) Failing after 4s
golangci-lint / lint (push) Failing after 4s
- Complete GUI overhaul with Fyne framework * Modern dark theme with indigo/pink accent colors * 4-step setup wizard (Welcome → Server → Auth → Finish) * Dashboard with proxy list, real-time logs, and config viewer * Windows Service manager (install/start/stop/uninstall) * System tray integration with quick controls - WiX v7 MSI installer * Auto-registers as Windows Service (manual start) * Feature tree UI for optional components * Desktop shortcut and PATH options * Build script: packaging/windows/build-msi.ps1 - Build system updates * Added kanholec-windows-gui target (CGO required) * Added kanholec-windows-msi target * Separate main_gui.go entry point for GUI builds GUI binary: bin/kanholec-windows-amd64.exe (31.4 MB) MSI installer: bin/kanholec-0.69.0-amd64.msi (12.1 MB)
This commit is contained in:
@@ -0,0 +1,410 @@
|
||||
//go:build kanholec_gui
|
||||
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
type wizardStep int
|
||||
|
||||
const (
|
||||
stepWelcome wizardStep = iota
|
||||
stepServer
|
||||
stepAuth
|
||||
stepFinish
|
||||
)
|
||||
|
||||
type Wizard struct {
|
||||
app *App
|
||||
currentStep wizardStep
|
||||
container *fyne.Container
|
||||
stepIndicator *fyne.Container
|
||||
|
||||
serverAddr *widget.Entry
|
||||
serverPort *widget.Entry
|
||||
authMethod *widget.Select
|
||||
authToken *widget.Entry
|
||||
authUser *widget.Entry
|
||||
authPass *widget.Entry
|
||||
clientName *widget.Entry
|
||||
|
||||
onComplete func(cfg WizardResult)
|
||||
}
|
||||
|
||||
type WizardResult struct {
|
||||
ServerAddr string
|
||||
ServerPort string
|
||||
AuthMethod string
|
||||
Token string
|
||||
Username string
|
||||
Password string
|
||||
ClientName string
|
||||
}
|
||||
|
||||
func NewWizard(app *App, onComplete func(WizardResult)) *Wizard {
|
||||
w := &Wizard{
|
||||
app: app,
|
||||
onComplete: onComplete,
|
||||
}
|
||||
w.buildUI()
|
||||
return w
|
||||
}
|
||||
|
||||
func (w *Wizard) buildUI() {
|
||||
w.serverAddr = widget.NewEntry()
|
||||
w.serverAddr.PlaceHolder = "127.0.0.1"
|
||||
w.serverAddr.SetText("127.0.0.1")
|
||||
|
||||
w.serverPort = widget.NewEntry()
|
||||
w.serverPort.PlaceHolder = "7000"
|
||||
w.serverPort.SetText("7000")
|
||||
|
||||
w.authMethod = widget.NewSelect([]string{"Token", "Username/Password"}, nil)
|
||||
w.authMethod.SetSelected("Token")
|
||||
|
||||
w.authToken = widget.NewEntry()
|
||||
w.authToken.PlaceHolder = "One-time provisioning token"
|
||||
|
||||
w.authUser = widget.NewEntry()
|
||||
w.authUser.PlaceHolder = "admin"
|
||||
|
||||
w.authPass = widget.NewPasswordEntry()
|
||||
w.authPass.PlaceHolder = "Password"
|
||||
|
||||
w.clientName = widget.NewEntry()
|
||||
w.clientName.PlaceHolder = "my-client"
|
||||
|
||||
w.authMethod.OnChanged = func(s string) {
|
||||
if s == "Token" {
|
||||
w.authToken.Show()
|
||||
w.authUser.Hide()
|
||||
w.authPass.Hide()
|
||||
w.clientName.Hide()
|
||||
} else {
|
||||
w.authToken.Hide()
|
||||
w.authUser.Show()
|
||||
w.authPass.Show()
|
||||
w.clientName.Show()
|
||||
}
|
||||
}
|
||||
|
||||
w.container = container.NewStack()
|
||||
w.buildStepIndicator()
|
||||
w.showStep(stepWelcome)
|
||||
}
|
||||
|
||||
func (w *Wizard) buildStepIndicator() {
|
||||
labels := []string{"Welcome", "Server", "Auth", "Finish"}
|
||||
items := []fyne.CanvasObject{}
|
||||
|
||||
for i, label := range labels {
|
||||
isActive := i == int(w.currentStep)
|
||||
isCompleted := i < int(w.currentStep)
|
||||
|
||||
var dotColor color.Color
|
||||
if isActive {
|
||||
dotColor = colorPrimary
|
||||
} else if isCompleted {
|
||||
dotColor = colorSuccess
|
||||
} else {
|
||||
dotColor = color.NRGBA{R: 75, G: 85, B: 99, A: 255}
|
||||
}
|
||||
|
||||
dot := canvas.NewCircle(dotColor)
|
||||
dot.Resize(fyne.NewSize(16, 16))
|
||||
|
||||
lbl := widget.NewLabel(label)
|
||||
lbl.Alignment = fyne.TextAlignCenter
|
||||
if isActive {
|
||||
lbl.TextStyle = fyne.TextStyle{Bold: true}
|
||||
lbl.Importance = widget.HighImportance
|
||||
} else if isCompleted {
|
||||
lbl.Importance = widget.MediumImportance
|
||||
} else {
|
||||
lbl.Importance = widget.LowImportance
|
||||
}
|
||||
|
||||
stepItem := container.NewVBox(
|
||||
container.NewHBox(layout.NewSpacer(), dot, layout.NewSpacer()),
|
||||
widget.NewLabel(""),
|
||||
container.NewHBox(layout.NewSpacer(), lbl, layout.NewSpacer()),
|
||||
)
|
||||
items = append(items, stepItem)
|
||||
|
||||
if i < len(labels)-1 {
|
||||
lineColor := color.NRGBA{R: 75, G: 85, B: 99, A: 255}
|
||||
if isCompleted {
|
||||
lineColor = colorSuccess
|
||||
}
|
||||
line := canvas.NewRectangle(lineColor)
|
||||
line.SetMinSize(fyne.NewSize(60, 3))
|
||||
items = append(items, container.NewVBox(
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
container.NewHBox(layout.NewSpacer(), line, layout.NewSpacer()),
|
||||
widget.NewLabel(""),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
w.stepIndicator = container.NewHBox(items...)
|
||||
}
|
||||
|
||||
func (w *Wizard) refreshStepIndicator() {
|
||||
w.buildStepIndicator()
|
||||
}
|
||||
|
||||
func (w *Wizard) showStep(step wizardStep) {
|
||||
w.currentStep = step
|
||||
w.refreshStepIndicator()
|
||||
|
||||
var content fyne.CanvasObject
|
||||
switch step {
|
||||
case stepWelcome:
|
||||
content = w.welcomePage()
|
||||
case stepServer:
|
||||
content = w.serverPage()
|
||||
case stepAuth:
|
||||
content = w.authPage()
|
||||
case stepFinish:
|
||||
content = w.finishPage()
|
||||
}
|
||||
|
||||
inner := container.NewBorder(
|
||||
container.NewVBox(
|
||||
widget.NewLabel(""),
|
||||
container.NewHBox(layout.NewSpacer(), w.stepIndicator, layout.NewSpacer()),
|
||||
widget.NewLabel(""),
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabel(""),
|
||||
),
|
||||
widget.NewLabel(""),
|
||||
nil, nil,
|
||||
container.NewVScroll(content),
|
||||
)
|
||||
|
||||
w.container.Objects = []fyne.CanvasObject{inner}
|
||||
w.container.Refresh()
|
||||
}
|
||||
|
||||
func (w *Wizard) welcomePage() fyne.CanvasObject {
|
||||
title := canvas.NewText("Welcome to kanholec", colorForeground)
|
||||
title.TextSize = 32
|
||||
title.TextStyle = fyne.TextStyle{Bold: true}
|
||||
title.Alignment = fyne.TextAlignCenter
|
||||
|
||||
subtitle := canvas.NewText("Set up your reverse proxy client", colorPlaceholder)
|
||||
subtitle.TextSize = 16
|
||||
subtitle.Alignment = fyne.TextAlignCenter
|
||||
|
||||
desc := widget.NewRichTextFromMarkdown(`kanholec connects to a kanhole server to create secure tunnels.
|
||||
|
||||
**You will need:**
|
||||
- A kanhole server address and port
|
||||
- An authentication token or admin credentials
|
||||
|
||||
Click **Next** to get started.`)
|
||||
|
||||
nextBtn := widget.NewButton("Next", func() {
|
||||
w.showStep(stepServer)
|
||||
})
|
||||
nextBtn.Importance = widget.HighImportance
|
||||
|
||||
return container.NewVBox(
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
container.NewHBox(layout.NewSpacer(), title, layout.NewSpacer()),
|
||||
widget.NewLabel(""),
|
||||
container.NewHBox(layout.NewSpacer(), subtitle, layout.NewSpacer()),
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabel(""),
|
||||
desc,
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
layout.NewSpacer(),
|
||||
container.NewHBox(layout.NewSpacer(), nextBtn),
|
||||
)
|
||||
}
|
||||
|
||||
func (w *Wizard) serverPage() fyne.CanvasObject {
|
||||
title := canvas.NewText("Server Connection", colorForeground)
|
||||
title.TextSize = 26
|
||||
title.TextStyle = fyne.TextStyle{Bold: true}
|
||||
|
||||
desc := canvas.NewText("Enter your kanhole server details", colorPlaceholder)
|
||||
desc.TextSize = 15
|
||||
|
||||
form := widget.NewForm(
|
||||
widget.NewFormItem("Server Address", w.serverAddr),
|
||||
widget.NewFormItem("Server Port", w.serverPort),
|
||||
)
|
||||
|
||||
backBtn := widget.NewButton("Back", func() {
|
||||
w.showStep(stepWelcome)
|
||||
})
|
||||
nextBtn := widget.NewButton("Next", func() {
|
||||
if w.serverAddr.Text == "" {
|
||||
w.app.showError("Server address is required")
|
||||
return
|
||||
}
|
||||
w.showStep(stepAuth)
|
||||
})
|
||||
nextBtn.Importance = widget.HighImportance
|
||||
|
||||
return container.NewVBox(
|
||||
title,
|
||||
widget.NewLabel(""),
|
||||
desc,
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
form,
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
layout.NewSpacer(),
|
||||
container.NewHBox(backBtn, layout.NewSpacer(), nextBtn),
|
||||
)
|
||||
}
|
||||
|
||||
func (w *Wizard) authPage() fyne.CanvasObject {
|
||||
title := canvas.NewText("Authentication", colorForeground)
|
||||
title.TextSize = 26
|
||||
title.TextStyle = fyne.TextStyle{Bold: true}
|
||||
|
||||
desc := canvas.NewText("Choose how to authenticate", colorPlaceholder)
|
||||
desc.TextSize = 15
|
||||
|
||||
methodForm := widget.NewForm(
|
||||
widget.NewFormItem("Method", w.authMethod),
|
||||
)
|
||||
|
||||
tokenForm := widget.NewForm(
|
||||
widget.NewFormItem("Token", w.authToken),
|
||||
)
|
||||
|
||||
credForm := widget.NewForm(
|
||||
widget.NewFormItem("Username", w.authUser),
|
||||
widget.NewFormItem("Password", w.authPass),
|
||||
widget.NewFormItem("Client Name", w.clientName),
|
||||
)
|
||||
|
||||
authContent := container.NewStack(tokenForm, credForm)
|
||||
w.authMethod.OnChanged(w.authMethod.Selected)
|
||||
|
||||
backBtn := widget.NewButton("Back", func() {
|
||||
w.showStep(stepServer)
|
||||
})
|
||||
nextBtn := widget.NewButton("Connect", func() {
|
||||
if w.authMethod.Selected == "Token" && w.authToken.Text == "" {
|
||||
w.app.showError("Token is required")
|
||||
return
|
||||
}
|
||||
if w.authMethod.Selected == "Username/Password" {
|
||||
if w.authUser.Text == "" || w.authPass.Text == "" {
|
||||
w.app.showError("Username and password are required")
|
||||
return
|
||||
}
|
||||
}
|
||||
w.showStep(stepFinish)
|
||||
})
|
||||
nextBtn.Importance = widget.HighImportance
|
||||
|
||||
return container.NewVBox(
|
||||
title,
|
||||
widget.NewLabel(""),
|
||||
desc,
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
methodForm,
|
||||
widget.NewLabel(""),
|
||||
authContent,
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
layout.NewSpacer(),
|
||||
container.NewHBox(backBtn, layout.NewSpacer(), nextBtn),
|
||||
)
|
||||
}
|
||||
|
||||
func (w *Wizard) finishPage() fyne.CanvasObject {
|
||||
title := canvas.NewText("Setup Complete!", colorForeground)
|
||||
title.TextSize = 32
|
||||
title.TextStyle = fyne.TextStyle{Bold: true}
|
||||
title.Alignment = fyne.TextAlignCenter
|
||||
|
||||
checkIcon := canvas.NewText("\u2713", colorSuccess)
|
||||
checkIcon.TextSize = 64
|
||||
checkIcon.Alignment = fyne.TextAlignCenter
|
||||
|
||||
summary := fmt.Sprintf(`**Server:** %s:%s
|
||||
**Auth:** %s
|
||||
**Client:** %s`,
|
||||
w.serverAddr.Text, w.serverPort.Text,
|
||||
w.authMethod.Selected,
|
||||
func() string {
|
||||
if w.authMethod.Selected == "Token" {
|
||||
return "Token auth"
|
||||
}
|
||||
if w.clientName.Text != "" {
|
||||
return w.clientName.Text
|
||||
}
|
||||
return w.authUser.Text
|
||||
}(),
|
||||
)
|
||||
|
||||
summaryText := widget.NewRichTextFromMarkdown(summary)
|
||||
|
||||
backBtn := widget.NewButton("Back", func() {
|
||||
w.showStep(stepAuth)
|
||||
})
|
||||
finishBtn := widget.NewButton("Launch Dashboard", func() {
|
||||
if w.onComplete != nil {
|
||||
w.onComplete(WizardResult{
|
||||
ServerAddr: w.serverAddr.Text,
|
||||
ServerPort: w.serverPort.Text,
|
||||
AuthMethod: w.authMethod.Selected,
|
||||
Token: w.authToken.Text,
|
||||
Username: w.authUser.Text,
|
||||
Password: w.authPass.Text,
|
||||
ClientName: w.clientName.Text,
|
||||
})
|
||||
}
|
||||
})
|
||||
finishBtn.Importance = widget.HighImportance
|
||||
|
||||
return container.NewVBox(
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
container.NewHBox(layout.NewSpacer(), checkIcon, layout.NewSpacer()),
|
||||
widget.NewLabel(""),
|
||||
container.NewHBox(layout.NewSpacer(), title, layout.NewSpacer()),
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabel(""),
|
||||
summaryText,
|
||||
widget.NewLabel(""),
|
||||
widget.NewLabel(""),
|
||||
layout.NewSpacer(),
|
||||
container.NewHBox(backBtn, layout.NewSpacer(), finishBtn),
|
||||
)
|
||||
}
|
||||
|
||||
func (w *Wizard) Content() *fyne.Container {
|
||||
return w.container
|
||||
}
|
||||
Reference in New Issue
Block a user