Files
kanhole/client/gui/dashboard.go
T
akukanara f4a88f4b2c
golangci-lint / lint (push) Failing after 4s
feat(gui): redesign kanholec GUI with modern interface and WiX v7 installer
- 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)
2026-05-29 22:53:41 +07:00

321 lines
7.3 KiB
Go

//go:build kanholec_gui
package gui
import (
"fmt"
"image/color"
"sync"
"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 Dashboard struct {
app *App
statusDot *canvas.Circle
statusLabel *widget.Label
statusDetail *widget.Label
proxyList *widget.List
proxyData []ProxyInfo
proxyDataMu sync.Mutex
logView *widget.Entry
logLines []string
logMu sync.Mutex
startBtn *widget.Button
stopBtn *widget.Button
reconnectBtn *widget.Button
serverInfo *widget.Label
configView *widget.Entry
content *fyne.Container
}
type ProxyInfo struct {
Name string
Type string
Status string
LocalAddr string
RemoteAddr string
Error string
}
func NewDashboard(app *App) *Dashboard {
d := &Dashboard{
app: app,
}
d.buildUI()
return d
}
func (d *Dashboard) buildUI() {
d.statusDot = canvas.NewCircle(colorPlaceholder)
d.statusDot.Resize(fyne.NewSize(16, 16))
d.statusLabel = widget.NewLabelWithStyle("Disconnected", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
d.statusDetail = widget.NewLabel("Not connected to server")
d.statusDetail.Importance = widget.LowImportance
d.serverInfo = widget.NewLabel("Server: --")
d.serverInfo.Importance = widget.LowImportance
d.configView = widget.NewMultiLineEntry()
d.configView.SetMinRowsVisible(10)
d.configView.Disable()
d.configView.SetText("No configuration loaded yet...")
d.logView = widget.NewMultiLineEntry()
d.logView.SetMinRowsVisible(15)
d.logView.Disable()
d.logView.Wrapping = fyne.TextWrapWord
d.logView.SetText("Waiting for logs...\n")
d.proxyData = []ProxyInfo{}
d.proxyList = widget.NewList(
func() int {
d.proxyDataMu.Lock()
defer d.proxyDataMu.Unlock()
return len(d.proxyData)
},
func() fyne.CanvasObject {
name := widget.NewLabelWithStyle("proxy-name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
typ := widget.NewLabel("tcp")
typ.Importance = widget.LowImportance
status := widget.NewLabel("unknown")
local := widget.NewLabel("127.0.0.1:8080")
remote := widget.NewLabel("0.0.0.0:9090")
errLabel := widget.NewLabel("")
errLabel.Importance = widget.DangerImportance
return container.NewVBox(
container.NewHBox(name, layout.NewSpacer(), status),
widget.NewLabel(""),
container.NewHBox(typ, widget.NewSeparator(), local, widget.NewSeparator(), remote),
errLabel,
)
},
func(id widget.ListItemID, o fyne.CanvasObject) {
d.proxyDataMu.Lock()
defer d.proxyDataMu.Unlock()
if id >= len(d.proxyData) {
return
}
p := d.proxyData[id]
vbox := o.(*fyne.Container)
row1 := vbox.Objects[0].(*fyne.Container)
nameLabel := row1.Objects[0].(*widget.Label)
nameLabel.SetText(p.Name)
statusLabel := row1.Objects[2].(*widget.Label)
statusLabel.SetText(p.Status)
statusLabel.Importance = widget.MediumImportance
row2 := vbox.Objects[2].(*fyne.Container)
typLabel := row2.Objects[0].(*widget.Label)
typLabel.SetText(p.Type)
localLabel := row2.Objects[2].(*widget.Label)
localLabel.SetText(p.LocalAddr)
remoteLabel := row2.Objects[4].(*widget.Label)
remoteLabel.SetText(p.RemoteAddr)
errLabel := vbox.Objects[3].(*widget.Label)
errLabel.SetText(p.Error)
},
)
d.startBtn = widget.NewButtonWithIcon("Start", nil, d.onStart)
d.startBtn.Importance = widget.HighImportance
d.stopBtn = widget.NewButtonWithIcon("Stop", nil, d.onStop)
d.stopBtn.Importance = widget.DangerImportance
d.stopBtn.Disable()
d.reconnectBtn = widget.NewButtonWithIcon("Reconnect", nil, d.onReconnect)
statusCard := container.NewVBox(
container.NewHBox(
d.statusDot,
widget.NewLabel(""),
d.statusLabel,
layout.NewSpacer(),
d.serverInfo,
),
widget.NewLabel(""),
d.statusDetail,
)
controls := container.NewHBox(
d.startBtn,
widget.NewLabel(""),
d.stopBtn,
layout.NewSpacer(),
d.reconnectBtn,
)
proxiesTab := container.NewBorder(
container.NewVBox(
container.NewHBox(
widget.NewLabelWithStyle("Proxies", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
layout.NewSpacer(),
widget.NewButton("Refresh", func() { d.refreshProxies() }),
),
widget.NewLabel(""),
),
nil, nil, nil,
container.NewStack(
d.proxyList,
container.NewVBox(
layout.NewSpacer(),
container.NewHBox(
layout.NewSpacer(),
widget.NewLabel("No proxies configured yet. Start the client to connect."),
layout.NewSpacer(),
),
layout.NewSpacer(),
),
),
)
logTab := container.NewBorder(
container.NewVBox(
container.NewHBox(
widget.NewLabelWithStyle("Logs", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
layout.NewSpacer(),
widget.NewButton("Clear", func() { d.clearLogs() }),
),
widget.NewLabel(""),
),
nil, nil, nil,
d.logView,
)
configTab := container.NewBorder(
container.NewVBox(
widget.NewLabelWithStyle("Active Configuration", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
widget.NewLabel(""),
),
nil, nil, nil,
d.configView,
)
tabs := container.NewAppTabs(
container.NewTabItem("Proxies", proxiesTab),
container.NewTabItem("Logs", logTab),
container.NewTabItem("Config", configTab),
)
tabs.SelectIndex(0)
d.content = container.NewBorder(
container.NewVBox(
statusCard,
widget.NewLabel(""),
widget.NewSeparator(),
widget.NewLabel(""),
controls,
widget.NewLabel(""),
widget.NewSeparator(),
widget.NewLabel(""),
),
nil, nil, nil,
tabs,
)
}
func (d *Dashboard) Content() *fyne.Container {
return d.content
}
func (d *Dashboard) SetStatus(status string, detail string) {
d.statusLabel.SetText(status)
d.statusDetail.SetText(detail)
c := StatusColor(status)
d.statusDot.FillColor = c
d.statusDot.Refresh()
}
func (d *Dashboard) SetServerInfo(addr string, port int) {
d.serverInfo.SetText(fmt.Sprintf("Server: %s:%d", addr, port))
}
func (d *Dashboard) SetConfig(data string) {
d.configView.SetText(data)
}
func (d *Dashboard) AppendLog(line string) {
d.logMu.Lock()
d.logLines = append(d.logLines, line)
if len(d.logLines) > 500 {
d.logLines = d.logLines[len(d.logLines)-500:]
}
d.logMu.Unlock()
text := ""
d.logMu.Lock()
for _, l := range d.logLines {
text += l + "\n"
}
d.logMu.Unlock()
d.logView.SetText(text)
}
func (d *Dashboard) clearLogs() {
d.logMu.Lock()
d.logLines = nil
d.logMu.Unlock()
d.logView.SetText("")
}
func (d *Dashboard) SetProxies(proxies []ProxyInfo) {
d.proxyDataMu.Lock()
d.proxyData = proxies
d.proxyDataMu.Unlock()
d.proxyList.Refresh()
if len(proxies) > 0 {
d.AppendLog(fmt.Sprintf("Loaded %d proxies", len(proxies)))
}
}
func (d *Dashboard) refreshProxies() {
if d.app != nil && d.app.clientService != nil {
d.AppendLog("Refreshing proxy status...")
}
}
func (d *Dashboard) onStart() {
d.app.startClient()
}
func (d *Dashboard) onStop() {
d.app.stopClient()
}
func (d *Dashboard) onReconnect() {
d.app.reconnect()
}
func (d *Dashboard) SetRunning(running bool) {
if running {
d.startBtn.Disable()
d.stopBtn.Enable()
d.SetStatus("running", "Client is connected and running")
d.statusDot.FillColor = color.NRGBA{R: 34, G: 197, B: 94, A: 255}
} else {
d.startBtn.Enable()
d.stopBtn.Disable()
d.SetStatus("stopped", "Client is not running")
d.statusDot.FillColor = color.NRGBA{R: 239, G: 68, B: 68, A: 255}
}
d.statusDot.Refresh()
if d.app != nil && d.app.tray != nil {
d.app.tray.SetRunning(running)
}
}