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:
+504
-177
@@ -3,11 +3,17 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
@@ -17,230 +23,551 @@ import (
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
|
||||
"kanhole/client"
|
||||
"kanhole/pkg/config"
|
||||
"kanhole/pkg/config/source"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/policy/security"
|
||||
"kanhole/pkg/util/log"
|
||||
"kanhole/pkg/util/version"
|
||||
)
|
||||
|
||||
type AppState int
|
||||
|
||||
const (
|
||||
StateSetup AppState = iota
|
||||
StateDashboard
|
||||
)
|
||||
|
||||
type App struct {
|
||||
fyneApp fyne.App
|
||||
window fyne.Window
|
||||
fyneApp fyne.App
|
||||
window fyne.Window
|
||||
wizard *Wizard
|
||||
dashboard *Dashboard
|
||||
tray *TrayManager
|
||||
svcMgr *ServiceManager
|
||||
|
||||
serverURL *widget.Entry
|
||||
authToken *widget.Entry
|
||||
clientKey *widget.Entry
|
||||
statusLabel *widget.Label
|
||||
configView *widget.Entry
|
||||
proxyList *widget.List
|
||||
startBtn *widget.Button
|
||||
stopBtn *widget.Button
|
||||
connectBtn *widget.Button
|
||||
mainContainer *fyne.Container
|
||||
state AppState
|
||||
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
configData string
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
configData string
|
||||
clientService *client.Service
|
||||
cancelFunc context.CancelFunc
|
||||
}
|
||||
|
||||
func New() *App {
|
||||
a := &App{}
|
||||
a.fyneApp = app.New()
|
||||
a.window = a.fyneApp.NewWindow("kanholec GUI")
|
||||
a.window.Resize(fyne.NewSize(850, 620))
|
||||
a.fyneApp = app.NewWithID("io.kanhole.kanholec")
|
||||
a.fyneApp.Settings().SetTheme(&kanholeTheme{})
|
||||
|
||||
a.window = a.fyneApp.NewWindow("kanholec")
|
||||
a.window.Resize(fyne.NewSize(900, 640))
|
||||
a.window.SetMaster()
|
||||
a.setupUI()
|
||||
|
||||
a.svcMgr = NewServiceManager()
|
||||
a.tray = NewTrayManager(a)
|
||||
|
||||
a.mainContainer = container.NewStack()
|
||||
a.window.SetContent(a.mainContainer)
|
||||
|
||||
a.window.SetCloseIntercept(func() {
|
||||
if a.tray.IsSupported() {
|
||||
a.window.Hide()
|
||||
} else {
|
||||
a.Quit()
|
||||
}
|
||||
})
|
||||
|
||||
if a.hasSavedConfig() {
|
||||
a.showDashboard()
|
||||
} else {
|
||||
a.showWizard()
|
||||
}
|
||||
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *App) saveSettings() {
|
||||
p := a.fyneApp.Preferences()
|
||||
p.SetString("server_url", a.serverURL.Text)
|
||||
func (a *App) hasSavedConfig() bool {
|
||||
prefs := a.fyneApp.Preferences()
|
||||
return prefs.StringWithFallback("server_addr", "") != ""
|
||||
}
|
||||
|
||||
func (a *App) loadSettings() {
|
||||
p := a.fyneApp.Preferences()
|
||||
if url := p.StringWithFallback("server_url", "http://localhost:7500"); url != "" {
|
||||
a.serverURL.SetText(url)
|
||||
}
|
||||
func (a *App) showWizard() {
|
||||
a.state = StateSetup
|
||||
a.wizard = NewWizard(a, a.onWizardComplete)
|
||||
a.mainContainer.Objects = []fyne.CanvasObject{a.wizard.Content()}
|
||||
a.mainContainer.Refresh()
|
||||
}
|
||||
|
||||
func (a *App) setupUI() {
|
||||
a.serverURL = widget.NewEntry()
|
||||
a.serverURL.SetText("http://localhost:7500")
|
||||
a.serverURL.PlaceHolder = "http://frps-server:7500"
|
||||
func (a *App) showDashboard() {
|
||||
a.state = StateDashboard
|
||||
a.dashboard = NewDashboard(a)
|
||||
|
||||
a.authToken = widget.NewEntry()
|
||||
a.authToken.PlaceHolder = "One-time token"
|
||||
prefs := a.fyneApp.Preferences()
|
||||
addr := prefs.StringWithFallback("server_addr", "127.0.0.1")
|
||||
port := prefs.IntWithFallback("server_port", 7000)
|
||||
a.dashboard.SetServerInfo(addr, port)
|
||||
|
||||
a.clientKey = widget.NewEntry()
|
||||
a.clientKey.PlaceHolder = "Client key (from server)"
|
||||
toolbar := a.buildToolbar()
|
||||
|
||||
a.statusLabel = widget.NewLabel("Not connected")
|
||||
a.statusLabel.Importance = widget.MediumImportance
|
||||
|
||||
a.configView = widget.NewMultiLineEntry()
|
||||
a.configView.SetMinRowsVisible(8)
|
||||
a.configView.Disable()
|
||||
|
||||
a.proxyList = widget.NewList(
|
||||
func() int { return 0 },
|
||||
func() fyne.CanvasObject { return widget.NewLabel("") },
|
||||
func(id widget.ListItemID, o fyne.CanvasObject) {},
|
||||
)
|
||||
|
||||
a.connectBtn = widget.NewButtonWithIcon("Connect", theme.NavigateNextIcon(), a.onConnect)
|
||||
a.startBtn = widget.NewButtonWithIcon("Start", theme.MediaPlayIcon(), a.onStart)
|
||||
a.startBtn.Disable()
|
||||
a.stopBtn = widget.NewButtonWithIcon("Stop", theme.MediaStopIcon(), a.onStop)
|
||||
a.stopBtn.Disable()
|
||||
|
||||
// Settings form
|
||||
settingsForm := widget.NewForm(
|
||||
widget.NewFormItem("Server URL", a.serverURL),
|
||||
)
|
||||
settingsForm.OnSubmit = func() { a.saveSettings() }
|
||||
settingsBtn := widget.NewButtonWithIcon("Save Settings", theme.DocumentSaveIcon(), a.saveSettings)
|
||||
|
||||
settingsTab := container.NewVBox(
|
||||
widget.NewLabelWithStyle("Connection Settings", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
settingsForm,
|
||||
settingsBtn,
|
||||
layout.NewSpacer(),
|
||||
)
|
||||
|
||||
// Auth form
|
||||
authForm := widget.NewForm(
|
||||
widget.NewFormItem("Server URL", a.serverURL),
|
||||
widget.NewFormItem("One-Time Token", a.authToken),
|
||||
widget.NewFormItem("Client Key", a.clientKey),
|
||||
)
|
||||
|
||||
statusLine := container.NewHBox(
|
||||
widget.NewLabelWithStyle("Status:", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
a.statusLabel,
|
||||
layout.NewSpacer(),
|
||||
a.connectBtn,
|
||||
)
|
||||
|
||||
controls := container.NewHBox(
|
||||
a.startBtn,
|
||||
a.stopBtn,
|
||||
)
|
||||
|
||||
connectTab := container.NewVBox(
|
||||
widget.NewLabelWithStyle("frp Client GUI", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
||||
authForm,
|
||||
statusLine,
|
||||
controls,
|
||||
container.NewBorder(
|
||||
widget.NewLabelWithStyle("Proxies", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
nil, nil, nil,
|
||||
a.proxyList,
|
||||
),
|
||||
)
|
||||
|
||||
// Config tab
|
||||
configTab := container.NewBorder(
|
||||
widget.NewLabelWithStyle("Configuration (read-only)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
content := container.NewBorder(
|
||||
toolbar,
|
||||
nil, nil, nil,
|
||||
a.configView,
|
||||
a.dashboard.Content(),
|
||||
)
|
||||
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItemWithIcon("Connect", theme.ComputerIcon(), connectTab),
|
||||
container.NewTabItemWithIcon("Config", theme.DocumentIcon(), configTab),
|
||||
container.NewTabItemWithIcon("Settings", theme.SettingsIcon(), settingsTab),
|
||||
)
|
||||
tabs.SelectIndex(0)
|
||||
a.mainContainer.Objects = []fyne.CanvasObject{content}
|
||||
a.mainContainer.Refresh()
|
||||
|
||||
a.window.SetContent(tabs)
|
||||
a.loadSettings()
|
||||
a.dashboard.AppendLog("Dashboard initialized")
|
||||
a.dashboard.AppendLog(fmt.Sprintf("Server: %s:%d", addr, port))
|
||||
|
||||
a.tray.Start()
|
||||
}
|
||||
|
||||
func (a *App) onConnect() {
|
||||
url := strings.TrimRight(a.serverURL.Text, "/")
|
||||
token := a.authToken.Text
|
||||
key := a.clientKey.Text
|
||||
a.saveSettings()
|
||||
func (a *App) buildToolbar() fyne.CanvasObject {
|
||||
logo := widget.NewLabelWithStyle("kanholec", fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||
ver := widget.NewLabel("v" + version.Full())
|
||||
ver.Importance = widget.LowImportance
|
||||
|
||||
if token == "" && key == "" {
|
||||
a.statusLabel.SetText("Error: provide token or client key")
|
||||
settingsBtn := widget.NewButtonWithIcon("", theme.SettingsIcon(), func() {
|
||||
a.showSettingsDialog()
|
||||
})
|
||||
settingsBtn.Importance = widget.LowImportance
|
||||
|
||||
serviceBtn := widget.NewButtonWithIcon("Service", nil, func() {
|
||||
a.showServiceDialog()
|
||||
})
|
||||
|
||||
resetBtn := widget.NewButtonWithIcon("Reset Setup", nil, func() {
|
||||
dialog.ShowConfirm("Reset Setup",
|
||||
"This will clear your configuration and restart the setup wizard. Continue?",
|
||||
func(ok bool) {
|
||||
if ok {
|
||||
a.clearSavedConfig()
|
||||
a.showWizard()
|
||||
}
|
||||
}, a.window)
|
||||
})
|
||||
resetBtn.Importance = widget.LowImportance
|
||||
|
||||
return container.NewVBox(
|
||||
container.NewHBox(
|
||||
logo,
|
||||
widget.NewLabel(""),
|
||||
ver,
|
||||
layout.NewSpacer(),
|
||||
serviceBtn,
|
||||
widget.NewLabel(""),
|
||||
settingsBtn,
|
||||
widget.NewLabel(""),
|
||||
resetBtn,
|
||||
),
|
||||
widget.NewLabel(""),
|
||||
widget.NewSeparator(),
|
||||
widget.NewLabel(""),
|
||||
)
|
||||
}
|
||||
|
||||
func (a *App) onWizardComplete(result WizardResult) {
|
||||
prefs := a.fyneApp.Preferences()
|
||||
prefs.SetString("server_addr", result.ServerAddr)
|
||||
|
||||
port, _ := strconv.Atoi(result.ServerPort)
|
||||
if port == 0 {
|
||||
port = 7000
|
||||
}
|
||||
prefs.SetInt("server_port", port)
|
||||
prefs.SetString("auth_method", result.AuthMethod)
|
||||
prefs.SetString("auth_token", result.Token)
|
||||
prefs.SetString("auth_user", result.Username)
|
||||
prefs.SetString("auth_pass", result.Password)
|
||||
prefs.SetString("client_name", result.ClientName)
|
||||
|
||||
a.showDashboard()
|
||||
a.dashboard.AppendLog("Setup completed. Fetching configuration...")
|
||||
|
||||
go func() {
|
||||
a.fetchAndSaveConfig(result)
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
a.startClient()
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *App) fetchAndSaveConfig(result WizardResult) {
|
||||
serverURL := fmt.Sprintf("http://%s:%s", result.ServerAddr, "7500")
|
||||
|
||||
var configData []byte
|
||||
var err error
|
||||
|
||||
a.dashboard.AppendLog(fmt.Sprintf("Connecting to %s...", serverURL))
|
||||
|
||||
if result.AuthMethod == "Token" && result.Token != "" {
|
||||
a.dashboard.AppendLog("Authenticating with token...")
|
||||
configData, err = a.authWithToken(serverURL, result.Token)
|
||||
} else if result.AuthMethod == "Username/Password" {
|
||||
a.dashboard.AppendLog("Authenticating with credentials...")
|
||||
configData, err = a.authWithCredentials(serverURL, result.Username, result.Password, result.ClientName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
a.dashboard.AppendLog(fmt.Sprintf("Auth error: %v", err))
|
||||
a.dashboard.AppendLog("You can still start the client with manual configuration.")
|
||||
return
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
apiURL := url + "/admin/api/client/auth"
|
||||
body := fmt.Sprintf(`{"token":"%s"}`, token)
|
||||
resp, err := http.Post(apiURL, "application/json", strings.NewReader(body))
|
||||
if err != nil {
|
||||
a.statusLabel.SetText(fmt.Sprintf("Error: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
a.statusLabel.SetText(fmt.Sprintf("Auth failed (%d): %s", resp.StatusCode, string(respBody)))
|
||||
return
|
||||
}
|
||||
|
||||
configData, _ := io.ReadAll(resp.Body)
|
||||
if configData != nil {
|
||||
a.configData = string(configData)
|
||||
a.configView.SetText(a.configData)
|
||||
a.statusLabel.SetText("Connected (token auth)")
|
||||
a.startBtn.Enable()
|
||||
a.connectBtn.SetText("Reconnect")
|
||||
} else if key != "" {
|
||||
apiURL := fmt.Sprintf("%s/admin/api/frpc/proxy-config/%s", url, key)
|
||||
resp, err := http.Get(apiURL)
|
||||
if err != nil {
|
||||
a.statusLabel.SetText(fmt.Sprintf("Error: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
a.statusLabel.SetText(fmt.Sprintf("Config fetch failed (%d)", resp.StatusCode))
|
||||
return
|
||||
}
|
||||
|
||||
configData, _ := io.ReadAll(resp.Body)
|
||||
a.configData = string(configData)
|
||||
a.configView.SetText(a.configData)
|
||||
a.statusLabel.SetText("Connected (key auth)")
|
||||
a.startBtn.Enable()
|
||||
a.connectBtn.SetText("Reconnect")
|
||||
a.saveConfigToFile(configData)
|
||||
a.dashboard.SetConfig(a.configData)
|
||||
a.dashboard.AppendLog("Configuration fetched successfully")
|
||||
a.dashboard.AppendLog("Configuration saved to disk")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) onStart() {
|
||||
if a.configData == "" {
|
||||
a.statusLabel.SetText("Error: no config loaded")
|
||||
func (a *App) authWithToken(serverURL, token string) ([]byte, error) {
|
||||
apiURL := serverURL + "/admin/api/client/auth"
|
||||
body, _ := json.Marshal(map[string]string{"token": token})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connection failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("auth failed (HTTP %d): %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func (a *App) authWithCredentials(serverURL, username, password, clientName string) ([]byte, error) {
|
||||
apiURL := serverURL + "/admin/api/client/auth"
|
||||
body, _ := json.Marshal(map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
"client_name": clientName,
|
||||
})
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, apiURL, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("connection failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return nil, fmt.Errorf("auth failed (HTTP %d): %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
func (a *App) saveConfigToFile(data []byte) {
|
||||
configDir := a.getConfigDir()
|
||||
os.MkdirAll(configDir, 0755)
|
||||
configPath := filepath.Join(configDir, "kanholec.toml")
|
||||
os.WriteFile(configPath, data, 0644)
|
||||
}
|
||||
|
||||
func (a *App) getConfigDir() string {
|
||||
if os.Getenv("ProgramData") != "" {
|
||||
return filepath.Join(os.Getenv("ProgramData"), "kanholec")
|
||||
}
|
||||
home, _ := os.UserHomeDir()
|
||||
return filepath.Join(home, ".kanholec")
|
||||
}
|
||||
|
||||
func (a *App) loadSavedConfig() ([]byte, error) {
|
||||
configPath := filepath.Join(a.getConfigDir(), "kanholec.toml")
|
||||
return os.ReadFile(configPath)
|
||||
}
|
||||
|
||||
func (a *App) startClient() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if a.running {
|
||||
a.dashboard.AppendLog("Client is already running")
|
||||
return
|
||||
}
|
||||
|
||||
a.dashboard.AppendLog("Starting client...")
|
||||
|
||||
var cfgData []byte
|
||||
var err error
|
||||
|
||||
if a.configData != "" {
|
||||
a.dashboard.AppendLog("Using fetched configuration")
|
||||
cfgData = []byte(a.configData)
|
||||
} else {
|
||||
a.dashboard.AppendLog("Loading saved configuration from disk...")
|
||||
cfgData, err = a.loadSavedConfig()
|
||||
if err != nil {
|
||||
a.dashboard.AppendLog(fmt.Sprintf("No configuration available: %v", err))
|
||||
a.dashboard.AppendLog("Please complete setup wizard or add manual configuration")
|
||||
return
|
||||
}
|
||||
a.dashboard.AppendLog("Configuration loaded from disk")
|
||||
}
|
||||
|
||||
var cfg v1.ClientConfig
|
||||
if err := config.LoadConfigure([]byte(a.configData), &cfg, false, "toml"); err != nil {
|
||||
dialog.ShowError(fmt.Errorf("invalid config: %w", err), a.window)
|
||||
if err := config.LoadConfigure(cfgData, &cfg, false, "toml"); err != nil {
|
||||
a.dashboard.AppendLog(fmt.Sprintf("Invalid configuration: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.running = true
|
||||
a.mu.Unlock()
|
||||
result := &config.ClientConfigLoadResult{
|
||||
Common: &cfg.ClientCommonConfig,
|
||||
Proxies: make([]v1.ProxyConfigurer, 0),
|
||||
}
|
||||
for _, c := range cfg.Proxies {
|
||||
result.Proxies = append(result.Proxies, c.ProxyConfigurer)
|
||||
}
|
||||
|
||||
a.startBtn.Disable()
|
||||
a.stopBtn.Enable()
|
||||
a.statusLabel.SetText("Running")
|
||||
a.dashboard.AppendLog(fmt.Sprintf("Loaded %d proxies from configuration", len(result.Proxies)))
|
||||
|
||||
configSrc := source.NewConfigSource()
|
||||
if err := configSrc.ReplaceAll(result.Proxies, result.Visitors); err != nil {
|
||||
a.dashboard.AppendLog(fmt.Sprintf("Config source error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
aggregator := source.NewAggregator(configSrc)
|
||||
unsafeFeatures := security.NewUnsafeFeatures(nil)
|
||||
|
||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
a.cancelFunc = cancel
|
||||
|
||||
svr, err := client.NewService(client.ServiceOptions{
|
||||
Common: &cfg.ClientCommonConfig,
|
||||
ConfigSourceAggregator: aggregator,
|
||||
UnsafeFeatures: unsafeFeatures,
|
||||
})
|
||||
if err != nil {
|
||||
cancel()
|
||||
a.dashboard.AppendLog(fmt.Sprintf("Service creation failed: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
a.clientService = svr
|
||||
a.running = true
|
||||
|
||||
a.dashboard.AppendLog("Client service created, connecting...")
|
||||
|
||||
go func() {
|
||||
a.dashboard.SetRunning(true)
|
||||
a.dashboard.AppendLog("Client started")
|
||||
if err := svr.Run(ctx); err != nil {
|
||||
a.dashboard.AppendLog(fmt.Sprintf("Client error: %v", err))
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.running = false
|
||||
a.mu.Unlock()
|
||||
a.dashboard.SetRunning(false)
|
||||
a.dashboard.AppendLog("Client stopped")
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *App) onStop() {
|
||||
func (a *App) stopClient() {
|
||||
a.mu.Lock()
|
||||
a.running = false
|
||||
a.mu.Unlock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
a.startBtn.Enable()
|
||||
a.stopBtn.Disable()
|
||||
a.statusLabel.SetText("Stopped")
|
||||
if !a.running || a.cancelFunc == nil {
|
||||
return
|
||||
}
|
||||
|
||||
a.cancelFunc()
|
||||
a.running = false
|
||||
a.dashboard.SetRunning(false)
|
||||
a.dashboard.AppendLog("Client stop requested")
|
||||
}
|
||||
|
||||
func (a *App) reconnect() {
|
||||
a.stopClient()
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
|
||||
prefs := a.fyneApp.Preferences()
|
||||
result := WizardResult{
|
||||
ServerAddr: prefs.StringWithFallback("server_addr", ""),
|
||||
ServerPort: strconv.Itoa(prefs.IntWithFallback("server_port", 7000)),
|
||||
AuthMethod: prefs.StringWithFallback("auth_method", "Token"),
|
||||
Token: prefs.StringWithFallback("auth_token", ""),
|
||||
Username: prefs.StringWithFallback("auth_user", ""),
|
||||
Password: prefs.StringWithFallback("auth_pass", ""),
|
||||
ClientName: prefs.StringWithFallback("client_name", ""),
|
||||
}
|
||||
a.fetchAndSaveConfig(result)
|
||||
a.startClient()
|
||||
}
|
||||
|
||||
func (a *App) showSettingsDialog() {
|
||||
prefs := a.fyneApp.Preferences()
|
||||
|
||||
addrEntry := widget.NewEntry()
|
||||
addrEntry.SetText(prefs.StringWithFallback("server_addr", ""))
|
||||
|
||||
portEntry := widget.NewEntry()
|
||||
portEntry.SetText(strconv.Itoa(prefs.IntWithFallback("server_port", 7000)))
|
||||
|
||||
tokenEntry := widget.NewEntry()
|
||||
tokenEntry.SetText(prefs.StringWithFallback("auth_token", ""))
|
||||
|
||||
form := dialog.NewForm("Settings", "Save", "Cancel",
|
||||
[]*widget.FormItem{
|
||||
widget.NewFormItem("Server Address", addrEntry),
|
||||
widget.NewFormItem("Server Port", portEntry),
|
||||
widget.NewFormItem("Auth Token", tokenEntry),
|
||||
},
|
||||
func(ok bool) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
prefs.SetString("server_addr", addrEntry.Text)
|
||||
port, _ := strconv.Atoi(portEntry.Text)
|
||||
if port > 0 {
|
||||
prefs.SetInt("server_port", port)
|
||||
}
|
||||
prefs.SetString("auth_token", tokenEntry.Text)
|
||||
a.dashboard.SetServerInfo(addrEntry.Text, port)
|
||||
a.dashboard.AppendLog("Settings saved")
|
||||
},
|
||||
a.window,
|
||||
)
|
||||
form.Resize(fyne.NewSize(450, 300))
|
||||
form.Show()
|
||||
}
|
||||
|
||||
func (a *App) showServiceDialog() {
|
||||
installed := a.svcMgr.IsInstalled()
|
||||
running := a.svcMgr.IsRunning()
|
||||
|
||||
statusText := "Not installed"
|
||||
if installed {
|
||||
if running {
|
||||
statusText = "Installed and Running"
|
||||
} else {
|
||||
statusText = "Installed (Stopped)"
|
||||
}
|
||||
}
|
||||
|
||||
statusLabel := widget.NewLabelWithStyle("Service: "+statusText, fyne.TextAlignLeading, fyne.TextStyle{Bold: true})
|
||||
|
||||
var buttons []fyne.CanvasObject
|
||||
if !installed {
|
||||
installBtn := widget.NewButton("Install Service", func() {
|
||||
configPath := a.svcMgr.GetConfigPath()
|
||||
if err := a.svcMgr.Install(configPath); err != nil {
|
||||
a.showError(fmt.Sprintf("Install failed: %v", err))
|
||||
} else {
|
||||
a.dashboard.AppendLog("Windows service installed")
|
||||
statusLabel.SetText("Service: Installed (Stopped)")
|
||||
}
|
||||
})
|
||||
installBtn.Importance = widget.HighImportance
|
||||
buttons = append(buttons, installBtn)
|
||||
} else {
|
||||
if !running {
|
||||
startBtn := widget.NewButton("Start Service", func() {
|
||||
if err := a.svcMgr.Start(); err != nil {
|
||||
a.showError(fmt.Sprintf("Start failed: %v", err))
|
||||
} else {
|
||||
a.dashboard.AppendLog("Windows service started")
|
||||
statusLabel.SetText("Service: Installed and Running")
|
||||
}
|
||||
})
|
||||
startBtn.Importance = widget.HighImportance
|
||||
buttons = append(buttons, startBtn)
|
||||
} else {
|
||||
stopBtn := widget.NewButton("Stop Service", func() {
|
||||
if err := a.svcMgr.Stop(); err != nil {
|
||||
a.showError(fmt.Sprintf("Stop failed: %v", err))
|
||||
} else {
|
||||
a.dashboard.AppendLog("Windows service stopped")
|
||||
statusLabel.SetText("Service: Installed (Stopped)")
|
||||
}
|
||||
})
|
||||
buttons = append(buttons, stopBtn)
|
||||
}
|
||||
|
||||
uninstallBtn := widget.NewButton("Uninstall Service", func() {
|
||||
dialog.ShowConfirm("Uninstall Service",
|
||||
"Remove the kanholec Windows service?",
|
||||
func(ok bool) {
|
||||
if ok {
|
||||
if err := a.svcMgr.Uninstall(); err != nil {
|
||||
a.showError(fmt.Sprintf("Uninstall failed: %v", err))
|
||||
} else {
|
||||
a.dashboard.AppendLog("Windows service uninstalled")
|
||||
statusLabel.SetText("Service: Not installed")
|
||||
}
|
||||
}
|
||||
}, a.window)
|
||||
})
|
||||
uninstallBtn.Importance = widget.DangerImportance
|
||||
buttons = append(buttons, uninstallBtn)
|
||||
}
|
||||
|
||||
content := container.NewVBox(
|
||||
statusLabel,
|
||||
widget.NewSeparator(),
|
||||
container.NewHBox(buttons...),
|
||||
)
|
||||
|
||||
d := dialog.NewCustom("Windows Service", "Close", content, a.window)
|
||||
d.Resize(fyne.NewSize(400, 200))
|
||||
d.Show()
|
||||
}
|
||||
|
||||
func (a *App) showError(msg string) {
|
||||
if a.window != nil {
|
||||
dialog.ShowError(fmt.Errorf("%s", msg), a.window)
|
||||
}
|
||||
if a.dashboard != nil {
|
||||
a.dashboard.AppendLog("Error: " + msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) clearSavedConfig() {
|
||||
prefs := a.fyneApp.Preferences()
|
||||
prefs.SetString("server_addr", "")
|
||||
prefs.SetInt("server_port", 0)
|
||||
prefs.SetString("auth_method", "")
|
||||
prefs.SetString("auth_token", "")
|
||||
prefs.SetString("auth_user", "")
|
||||
prefs.SetString("auth_pass", "")
|
||||
prefs.SetString("client_name", "")
|
||||
a.configData = ""
|
||||
}
|
||||
|
||||
func (a *App) Quit() {
|
||||
a.stopClient()
|
||||
a.tray.Stop()
|
||||
a.fyneApp.Quit()
|
||||
}
|
||||
|
||||
func (a *App) Run() {
|
||||
@@ -249,6 +576,6 @@ func (a *App) Run() {
|
||||
|
||||
func Run() {
|
||||
gui := New()
|
||||
log.Infof("starting frpc GUI")
|
||||
log.Infof("starting kanholec GUI v%s", version.Full())
|
||||
gui.Run()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user