Files
lmvpn_client/internal/ui/app.go
T
2026-07-06 16:21:06 +08:00

171 lines
3.5 KiB
Go

package ui
import (
"sync"
"lmvpn/internal/config"
"lmvpn/internal/db"
"lmvpn/internal/ipc"
"lmvpn/internal/keychain"
"lmvpn/internal/log"
"lmvpn/internal/model"
"lmvpn/internal/paths"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/widget"
)
// App is the GUI application controller.
type App struct {
fyneApp fyne.App
db *db.Store
kc keychain.Store
window fyne.Window
// UI widgets
profileSelect *widget.Select
stateLabel *widget.Label
ipLabel *widget.Label
uptimeLabel *widget.Label
rxLabel *widget.Label
txLabel *widget.Label
connectBtn *widget.Button
disconnectBtn *widget.Button
// State
mu sync.Mutex
ipcClient *ipc.Client
profiles []model.ServerProfile
currentProfile *model.ServerProfile
}
// Run initialises and starts the GUI application.
func Run() {
// Ensure platform directories exist.
if err := paths.EnsureDirs(); err != nil {
log.L().Error("ensure dirs", "error", err)
}
// Logging.
log.Init(log.RoleGUI, paths.LogFile())
// Database.
store, err := db.Open()
if err != nil {
log.L().Error("open db", "error", err)
}
// Load app config.
cfg, _ := config.Load()
a := &App{
fyneApp: app.NewWithID(paths.BundleID),
db: store,
kc: keychain.New(),
}
a.window = a.fyneApp.NewWindow("LMVPN")
a.window.SetContent(a.buildMainWindow())
a.window.Resize(fyne.NewSize(420, 480))
// Load profiles.
if store != nil {
a.loadProfiles()
}
// System tray.
a.setupTray()
// Auto-connect if configured.
if cfg.AutoConnect && a.currentProfile != nil {
go func() {
// Small delay to let window render.
a.onConnect()
}()
}
a.window.SetCloseIntercept(func() {
if cfg.CloseToTray {
a.window.Hide()
} else {
a.fyneApp.Quit()
}
})
a.window.ShowAndRun()
}
// loadProfiles loads all profiles from the database and populates the
// selector.
func (a *App) loadProfiles() {
profiles, err := a.db.ListProfiles()
if err != nil {
log.L().Error("list profiles", "error", err)
return
}
a.profiles = profiles
names := a.profileNames()
a.profileSelect.Options = names
if len(names) > 0 {
a.profileSelect.SetSelectedIndex(0)
a.selectProfileByName(names[0])
}
}
// profileNames returns the names of all loaded profiles.
func (a *App) profileNames() []string {
names := make([]string, len(a.profiles))
for i, p := range a.profiles {
names[i] = p.Name
}
return names
}
// selectProfileByName sets the current profile by name.
func (a *App) selectProfileByName(name string) {
for i := range a.profiles {
if a.profiles[i].Name == name {
a.currentProfile = &a.profiles[i]
return
}
}
}
// onAddProfile shows a dialog to create a new profile.
func (a *App) onAddProfile() {
a.showProfileDialog(nil)
}
// onEditProfile shows a dialog to edit the current profile.
func (a *App) onEditProfile() {
if a.currentProfile == nil {
dialog.ShowInformation("No Profile", "Select a profile to edit.", a.window)
return
}
p := *a.currentProfile
a.showProfileDialog(&p)
}
// onDeleteProfile deletes the current profile after confirmation.
func (a *App) onDeleteProfile() {
if a.currentProfile == nil {
return
}
name := a.currentProfile.Name
dialog.ShowConfirm("Delete Profile",
"Delete profile \""+name+"\" and its stored credentials?",
func(ok bool) {
if !ok {
return
}
if err := a.db.DeleteProfile(a.currentProfile.ID); err != nil {
showError("Error", err.Error(), a.window)
return
}
_ = a.kc.DeleteAll(name)
a.loadProfiles()
}, a.window)
}