f4a88f4b2c
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)
321 lines
7.3 KiB
Go
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)
|
|
}
|
|
}
|