//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 }