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,320 @@
|
||||
//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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user