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