diff --git a/Makefile b/Makefile index df937cb..0579f7a 100644 --- a/Makefile +++ b/Makefile @@ -35,8 +35,12 @@ app: build else echo " (no icon.icns found, skipping icon)"; fi @echo "Built $(APP_BUNDLE)" -## icon: generate icon.icns from resources/icon.png +## icon: generate icon.icns from resources/icon.png (or resources/logo.svg) icon: + @if [ -f resources/logo.svg ] && [ ! -f resources/icon.png -o resources/logo.svg -nt resources/icon.png ]; then \ + echo "Converting resources/logo.svg -> resources/icon.png"; \ + sips -z 1024 1024 -s format png resources/logo.svg --out resources/icon.png >/dev/null 2>&1; \ + fi @if [ ! -f resources/icon.png ]; then echo "resources/icon.png not found"; exit 1; fi mkdir -p resources/icon.iconset @for size in 16 32 64 128 256 512 1024; do \ diff --git a/internal/ui/app.go b/internal/ui/app.go index 98491db..b4bee68 100644 --- a/internal/ui/app.go +++ b/internal/ui/app.go @@ -20,10 +20,11 @@ import ( // App is the GUI application controller. type App struct { - fyneApp fyne.App - db *db.Store - kc keychain.Store - window fyne.Window + fyneApp fyne.App + db *db.Store + kc keychain.Store + window fyne.Window + profileWindow fyne.Window // UI widgets profileSelect *widget.Select @@ -96,6 +97,7 @@ func Run() { a.window.SetCloseIntercept(func() { if cfg.CloseToTray { + hideDockIcon() a.window.Hide() } else { a.fyneApp.Quit() diff --git a/internal/ui/dock_darwin.go b/internal/ui/dock_darwin.go new file mode 100644 index 0000000..8f67a72 --- /dev/null +++ b/internal/ui/dock_darwin.go @@ -0,0 +1,20 @@ +//go:build darwin + +package ui + +/* +#cgo LDFLAGS: -framework Cocoa +#include +#include +#include + +static void setDockIconVisible(int visible) { + Class cls = objc_getClass("NSApplication"); + id app = ((id (*)(Class, SEL))objc_msgSend)(cls, sel_getUid("sharedApplication")); + ((void (*)(id, SEL, long))objc_msgSend)(app, sel_getUid("setActivationPolicy:"), visible ? 0 : 1); +} +*/ +import "C" + +func showDockIcon() { C.setDockIconVisible(1) } +func hideDockIcon() { C.setDockIconVisible(0) } diff --git a/internal/ui/dock_other.go b/internal/ui/dock_other.go new file mode 100644 index 0000000..8bb2731 --- /dev/null +++ b/internal/ui/dock_other.go @@ -0,0 +1,6 @@ +//go:build !darwin + +package ui + +func showDockIcon() {} +func hideDockIcon() {} diff --git a/internal/ui/profile.go b/internal/ui/profile.go index d82b5b8..77efb42 100644 --- a/internal/ui/profile.go +++ b/internal/ui/profile.go @@ -8,7 +8,6 @@ import ( "fyne.io/fyne/v2" "fyne.io/fyne/v2/container" - "fyne.io/fyne/v2/dialog" "fyne.io/fyne/v2/widget" ) @@ -46,9 +45,15 @@ func selectedCode(codes []string, idx int) string { return codes[idx] } -// showProfileDialog displays an add/edit dialog for a server profile. -// If editing is nil, a new profile is created. +// showProfileDialog opens a separate window for editing a server profile. +// If editing is nil, a new profile is created. If a profile window is +// already open, it is brought to the front instead of opening a second one. func (a *App) showProfileDialog(editing *model.ServerProfile) { + if a.profileWindow != nil { + a.profileWindow.RequestFocus() + return + } + isNew := editing == nil nameEntry := widget.NewEntry() @@ -88,18 +93,30 @@ func (a *App) showProfileDialog(editing *model.ServerProfile) { widget.NewLabel(i18n.T("FieldMTUOverride")), mtuEntry, ) - d := dialog.NewCustomConfirm(i18n.T("DlgProfileTitle"), i18n.T("BtnSave"), i18n.T("BtnCancel"), form, func(save bool) { - if !save { - return - } + profileWin := a.fyneApp.NewWindow(i18n.T("DlgProfileTitle")) + a.profileWindow = profileWin + + profileWin.SetOnClosed(func() { + a.profileWindow = nil + }) + + saveBtn := widget.NewButton(i18n.T("BtnSave"), func() { a.saveProfile(editing, nameEntry.Text, serverEntry.Text, userEntry.Text, passEntry.Text, selectedCode(authCodes, authSelect.SelectedIndex()), selectedCode(routeCodes, routeSelect.SelectedIndex()), cidrEntry.Text, mtuEntry.Text, isNew) - }, a.window) - d.Resize(fyne.NewSize(400, 500)) - d.Show() + profileWin.Close() + }) + saveBtn.Importance = widget.HighImportance + + cancelBtn := widget.NewButton(i18n.T("BtnCancel"), func() { + profileWin.Close() + }) + + profileWin.SetContent(container.NewBorder(nil, container.NewHBox(saveBtn, cancelBtn), nil, nil, container.NewScroll(form))) + profileWin.Resize(fyne.NewSize(460, 560)) + profileWin.Show() } // saveProfile creates or updates a profile and stores credentials. diff --git a/internal/ui/tray-icon.png b/internal/ui/tray-icon.png new file mode 100644 index 0000000..883f869 Binary files /dev/null and b/internal/ui/tray-icon.png differ diff --git a/internal/ui/tray.go b/internal/ui/tray.go index a66e6f4..6af9d2e 100644 --- a/internal/ui/tray.go +++ b/internal/ui/tray.go @@ -1,12 +1,16 @@ package ui import ( + _ "embed" "lmvpn/internal/i18n" "fyne.io/fyne/v2" "fyne.io/fyne/v2/driver/desktop" ) +//go:embed tray-icon.png +var trayIconBytes []byte + // setupTray configures the system tray menu (desktop only). func (a *App) setupTray() { deskApp, ok := a.fyneApp.(desktop.App) @@ -44,6 +48,7 @@ func (a *App) setupTray() { menu := fyne.NewMenu(i18n.T("WindowTitle"), fyne.NewMenuItem(i18n.T("TrayShowWindow"), func() { + showDockIcon() a.window.Show() a.window.RequestFocus() }), @@ -62,4 +67,6 @@ func (a *App) setupTray() { }), ) deskApp.SetSystemTrayMenu(menu) + trayIcon := fyne.NewStaticResource("tray-icon.png", trayIconBytes) + deskApp.SetSystemTrayIcon(trayIcon) } diff --git a/resources/icon.icns b/resources/icon.icns index 6d5f2d2..38d286d 100644 Binary files a/resources/icon.icns and b/resources/icon.icns differ diff --git a/resources/icon.png b/resources/icon.png index 7337414..81db510 100644 Binary files a/resources/icon.png and b/resources/icon.png differ diff --git a/resources/logo.svg b/resources/logo.svg new file mode 100644 index 0000000..f9358a7 --- /dev/null +++ b/resources/logo.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/resources/tray-icon.png b/resources/tray-icon.png new file mode 100644 index 0000000..883f869 Binary files /dev/null and b/resources/tray-icon.png differ diff --git a/resources/tray-logo.svg b/resources/tray-logo.svg new file mode 100644 index 0000000..cbb7b08 --- /dev/null +++ b/resources/tray-logo.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + +