From a08f7f63f42becd5cb074576c189cc71009ff1da Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Tue, 25 Aug 2026 11:40:31 +0200 Subject: [PATCH] [client] Create GUI windows on demand and destroy them on close (#7096) The main and Settings windows were created at startup and kept alive hidden on close, so an idle tray held two webview processes for surfaces the user may never open. Both are now built on first show and destroyed on close, which takes the idle footprint on macOS from ~160 MB to ~74 MB. The WindowManager owns creation: it rebuilds the main window on the next show and hands out live pointers, since a stored one goes stale. Every show is deferred until the frontend reports it has rendered, so a freshly created window is never on screen empty, with a timeout so a frontend that never reports cannot strand a window hidden. --- .../frontend/src/components/ReadySignal.tsx | 18 + client/ui/frontend/src/layouts/AppLayout.tsx | 2 + client/ui/main.go | 60 ++- client/ui/services/windowmanager.go | 370 ++++++++++++++++-- client/ui/tray.go | 53 ++- client/ui/tray_session.go | 7 +- client/ui/tray_update.go | 15 +- go.mod | 2 +- go.sum | 4 +- 9 files changed, 448 insertions(+), 83 deletions(-) create mode 100644 client/ui/frontend/src/components/ReadySignal.tsx diff --git a/client/ui/frontend/src/components/ReadySignal.tsx b/client/ui/frontend/src/components/ReadySignal.tsx new file mode 100644 index 000000000..0d040cabc --- /dev/null +++ b/client/ui/frontend/src/components/ReadySignal.tsx @@ -0,0 +1,18 @@ +import { useEffect, useRef } from "react"; +import { Events } from "@wailsio/runtime"; +import { useStatus } from "@/contexts/StatusContext.tsx"; + +const EVENT_WINDOW_PAINTED = "netbird:window-painted"; + +export const ReadySignal = () => { + const { isReady } = useStatus(); + const sent = useRef(false); + + useEffect(() => { + if (!isReady || sent.current) return; + sent.current = true; + void Events.Emit(EVENT_WINDOW_PAINTED); + }, [isReady]); + + return null; +}; diff --git a/client/ui/frontend/src/layouts/AppLayout.tsx b/client/ui/frontend/src/layouts/AppLayout.tsx index 1588d9d08..0c2837b53 100644 --- a/client/ui/frontend/src/layouts/AppLayout.tsx +++ b/client/ui/frontend/src/layouts/AppLayout.tsx @@ -5,6 +5,7 @@ import { DebugBundleProvider } from "@/contexts/DebugBundleContext.tsx"; import { ProfileProvider } from "@/contexts/ProfileContext.tsx"; import { DialogProvider } from "@/contexts/DialogContext.tsx"; import { RestrictionsProvider } from "@/contexts/RestrictionsContext.tsx"; +import { ReadySignal } from "@/components/ReadySignal.tsx"; export const AppLayout = () => { return ( @@ -16,6 +17,7 @@ export const AppLayout = () => { + diff --git a/client/ui/main.go b/client/ui/main.go index 5f740f5ec..e20bfe074 100644 --- a/client/ui/main.go +++ b/client/ui/main.go @@ -139,13 +139,11 @@ func main() { prefStore: prefStore, }) - window := newMainWindow(app, prefStore) - - // Settings is created eagerly (hidden) so the first gear click paints - // instantly and React keeps per-tab state across reopens. The other - // auxiliary windows stay lazy + destroy-on-close so Wails's macOS - // dock-reopen handler can't resurrect them. - windowManager := services.NewWindowManager(app, window, bundle, prefStore, iconWindow) + windowManager := services.NewWindowManager(app, nil, bundle, prefStore, iconWindow) + windowManager.SetMainFactory(func(startURL string) *application.WebviewWindow { + return newMainWindow(app, prefStore, windowManager, startURL) + }) + registerDockReopenHook(app, windowManager) // Minimal WMs (XEmbed-tray path) neither center small windows nor restore // position across hide -> show, dropping them top-left. Gate Go-side // re-centering on that environment; nil leaves placement to the WM on full @@ -168,7 +166,7 @@ func main() { // RegisterStatusNotifierItem hits a watcher we control. startStatusNotifierWatcher() - tray = NewTray(app, window, TrayServices{ + tray = NewTray(app, nil, TrayServices{ Connection: connection, Settings: settings, Profiles: profiles, @@ -279,10 +277,12 @@ func newApplication(onSecondInstance func()) *application.App { ActivationPolicy: application.ActivationPolicyAccessory, }, Linux: application.LinuxOptions{ - ProgramName: "netbird", + ProgramName: "netbird", + DisableQuitOnLastWindowClosed: true, }, Windows: application.WindowsOptions{ - WndProcInterceptor: endSessionInterceptor(), + WndProcInterceptor: endSessionInterceptor(), + DisableQuitOnLastWindowClosed: true, }, SingleInstance: &application.SingleInstanceOptions{ UniqueID: "io.netbird.ui", @@ -338,9 +338,7 @@ func registerServices(app *application.App, conn *Conn, s registeredServices) { app.RegisterService(application.NewService(s.compat)) } -// newMainWindow creates the hidden main window, sized to the user's last view -// mode, and installs the hide-on-close and macOS dock-reopen hooks. -func newMainWindow(app *application.App, prefStore *preferences.Store) *application.WebviewWindow { +func newMainWindow(app *application.App, prefStore *preferences.Store, wm *services.WindowManager, startURL string) *application.WebviewWindow { // Width matches the last view mode so Advanced-mode users don't see the // window pop from 380px to 900px on launch. Height is mode-agnostic. initialWidth := 380 @@ -357,7 +355,7 @@ func newMainWindow(app *application.App, prefStore *preferences.Store) *applicat InitialPosition: application.WindowCentered, Hidden: true, BackgroundColour: services.WindowBackgroundColour, - URL: "/", + URL: startURL, DisableResize: true, MinimiseButtonState: application.ButtonHidden, MaximiseButtonState: application.ButtonHidden, @@ -368,29 +366,25 @@ func newMainWindow(app *application.App, prefStore *preferences.Store) *applicat }, }) - // Hide instead of quit on close; "really quit" is reached via tray -> Quit. - window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { + window.RegisterHook(events.Common.WindowClosing, func(_ *application.WindowEvent) { if services.ShuttingDown() { return } - e.Cancel() - window.Hide() + wm.ForgetMain() }) - // On macOS, Wails' default applicationShouldHandleReopen handler Show()s - // every hidden window on dock-icon click, resurrecting hide-on-close - // surfaces like Settings. Cancel it in a hook (hooks run before listeners) - // and show only the main window. No-op elsewhere — the event never fires. - if runtime.GOOS == "darwin" { - app.Event.RegisterApplicationEventHook(events.Mac.ApplicationShouldHandleReopen, func(e *application.ApplicationEvent) { - e.Cancel() - if e.Context().HasVisibleWindows() { - return - } - window.Show() - window.Focus() - }) - } - return window } + +func registerDockReopenHook(app *application.App, wm *services.WindowManager) { + if runtime.GOOS != "darwin" { + return + } + app.Event.RegisterApplicationEventHook(events.Mac.ApplicationShouldHandleReopen, func(e *application.ApplicationEvent) { + if e.Context().HasVisibleWindows() { + return + } + e.Cancel() + wm.ShowMain() + }) +} diff --git a/client/ui/services/windowmanager.go b/client/ui/services/windowmanager.go index 5f7aaa7bd..4930ce22b 100644 --- a/client/ui/services/windowmanager.go +++ b/client/ui/services/windowmanager.go @@ -8,6 +8,7 @@ import ( "sync" "time" + log "github.com/sirupsen/logrus" "github.com/wailsapp/wails/v3/pkg/application" "github.com/wailsapp/wails/v3/pkg/events" @@ -29,6 +30,12 @@ const EventBrowserLoginCancel = "browser-login:cancel" // EventSettingsOpen tells the mounted settings window which tab to show. const EventSettingsOpen = "netbird:settings:open" +const EventWindowPainted = "netbird:window-painted" + +const paintedFallback = 2 * time.Second + +const headlessTeardownDelay = 2 * time.Second + var WindowBackgroundColour = application.NewRGB(24, 26, 29) // bg-nb-gray-950 // WindowHeight is shared by the main and Settings windows. @@ -94,9 +101,6 @@ func DialogWindowOptions(name, title, url string, linuxIcon []byte) application. } } -// WindowManager owns the auxiliary windows (main is created in main.go). Settings is created -// eagerly and hidden on close to keep React state; the rest are created on open, destroyed on -// close, so the macOS dock-reopen handler finds no hidden window to resurrect. type WindowManager struct { app *application.App mainWindow *application.WebviewWindow @@ -112,15 +116,35 @@ type WindowManager struct { // hiddenForLogin holds windows hidden while the BrowserLogin popup is open, restored on close. hiddenForLogin []application.Window mu sync.Mutex + createMu sync.Mutex + newMain func(startURL string) *application.WebviewWindow + ready map[uint]bool + showPending map[uint]bool + pendingTab map[uint]string + pendingEmits map[uint][]string + fallbackTimers map[uint]*time.Timer + headlessMain bool + headlessTimer *time.Timer // recenterOnShow is set only on the minimal-WM/XEmbed path, where the WM neither centers nor // restores position; nil on full desktops so re-centering can't fight a user-moved window. recenterOnShow func() bool } -// NewWindowManager wires the manager to the main app; translator/prefs may be nil (tests). The -// Settings window is created here (hidden) so the first OpenSettings is instant. func NewWindowManager(app *application.App, mainWindow *application.WebviewWindow, translator ErrorTranslator, prefs LanguagePreference, linuxIcon []byte) *WindowManager { - s := &WindowManager{app: app, mainWindow: mainWindow, translator: translator, prefs: prefs, linuxIcon: linuxIcon} + s := &WindowManager{ + app: app, + mainWindow: mainWindow, + translator: translator, + prefs: prefs, + linuxIcon: linuxIcon, + ready: map[uint]bool{}, + showPending: map[uint]bool{}, + pendingTab: map[uint]string{}, + pendingEmits: map[uint][]string{}, + fallbackTimers: map[uint]*time.Timer{}, + } + s.watchPainted() + s.watchTriggerLogin() // Re-title live windows on language flip. Wired internally so the binding generator // doesn't try to expose the interface param. if sub, ok := prefs.(LanguageSubscriber); ok && sub != nil { @@ -136,7 +160,11 @@ func NewWindowManager(app *application.App, mainWindow *application.WebviewWindo } }() } - s.settings = app.Window.NewWithOptions(application.WebviewWindowOptions{ + return s +} + +func (s *WindowManager) newSettingsWindow() *application.WebviewWindow { + w := s.app.Window.NewWithOptions(application.WebviewWindowOptions{ Name: "settings", Title: s.title("window.title.settings"), Width: 900, @@ -150,18 +178,15 @@ func NewWindowManager(app *application.App, mainWindow *application.WebviewWindo URL: "/#/settings", Mac: AppleMacOSAppearanceOptions(), Windows: MicrosoftWindowsAppearanceOptions(), - Linux: LinuxAppearanceOptions(linuxIcon), + Linux: LinuxAppearanceOptions(s.linuxIcon), }) - // Hide (not destroy) on close to keep React state; reset to General for a flash-free reopen. - s.settings.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) { - if ShuttingDown() { - return - } - e.Cancel() - s.app.Event.Emit(EventSettingsOpen, "general") - s.settings.Hide() + w.RegisterHook(events.Common.WindowClosing, func(_ *application.WindowEvent) { + s.mu.Lock() + s.settings = nil + s.forgetWindowLocked(w) + s.mu.Unlock() }) - return s + return w } // OpenSettings shows the settings window on tab (empty → General), switching tab via @@ -171,11 +196,20 @@ func (s *WindowManager) OpenSettings(tab string) { if target == "" { target = "general" } - s.app.Event.Emit(EventSettingsOpen, target) - s.settings.Show() - s.settings.Focus() - // Re-center (minimal-WM only; see centerWhenReady). - s.centerWhenReady(s.settings) + + w, _ := s.ensureWindow(&s.settings, s.newSettingsWindow) + + s.mu.Lock() + ready := s.ready[w.ID()] + if !ready { + s.pendingTab[w.ID()] = target + } + s.mu.Unlock() + + if ready { + s.app.Event.Emit(EventSettingsOpen, target) + } + s.showWhenReady(w) } // OpenBrowserLogin shows the SSO popup, creating it on first use. @@ -440,13 +474,295 @@ func (s *WindowManager) OpenMain() { // ShowMain brings the main window forward (re-centering on minimal WMs). The single entry // point every surface (tray, SIGUSR1, welcome) should use so centering applies uniformly. func (s *WindowManager) ShowMain() { - if s.mainWindow == nil { + s.showWhenReady(s.MainWindow()) +} + +// ShowMainAndEmit brings the main window forward and emits event once its frontend is ready. +func (s *WindowManager) ShowMainAndEmit(event string) { + w := s.MainWindow() + if w == nil { return } - s.mainWindow.Show() - s.mainWindow.Focus() - // Re-center (minimal-WM only; see centerWhenReady). - s.centerWhenReady(s.mainWindow) + + id := w.ID() + s.mu.Lock() + ready := s.ready[id] + if !ready { + s.pendingEmits[id] = append(s.pendingEmits[id], event) + } + s.mu.Unlock() + + s.showWhenReady(w) + if ready { + s.app.Event.Emit(event) + } +} + +func (s *WindowManager) MainWindow() *application.WebviewWindow { + w, _ := s.ensureMain("/") + return w +} + +func (s *WindowManager) ensureMain(startURL string) (*application.WebviewWindow, bool) { + s.mu.Lock() + factory := s.newMain + s.mu.Unlock() + if factory == nil { + return s.ensureWindow(&s.mainWindow, nil) + } + return s.ensureWindow(&s.mainWindow, func() *application.WebviewWindow { + return factory(startURL) + }) +} + +func (s *WindowManager) ensureWindow(slot **application.WebviewWindow, factory func() *application.WebviewWindow) (*application.WebviewWindow, bool) { + s.createMu.Lock() + defer s.createMu.Unlock() + + s.mu.Lock() + w := *slot + s.mu.Unlock() + if w != nil || factory == nil { + return w, false + } + + w = factory() + s.armReady(w) + + s.mu.Lock() + *slot = w + s.mu.Unlock() + return w, true +} + +func (s *WindowManager) armReady(w *application.WebviewWindow) { + if w == nil { + return + } + w.RegisterHook(events.Common.WindowRuntimeReady, func(_ *application.WindowEvent) { + timer := time.AfterFunc(paintedFallback, func() { + log.Warnf("window %q never reported a first render, showing it anyway", w.Name()) + s.markReady(w) + }) + s.mu.Lock() + s.fallbackTimers[w.ID()] = timer + s.mu.Unlock() + }) +} + +func (s *WindowManager) watchPainted() { + s.app.Event.On(EventWindowPainted, func(e *application.CustomEvent) { + if w := s.windowByName(e.Sender); w != nil { + s.markReady(w) + } + }) +} + +func (s *WindowManager) watchTriggerLogin() { + s.app.Event.On(EventTriggerLogin, func(_ *application.CustomEvent) { + s.mu.Lock() + if s.headlessTimer != nil { + s.headlessTimer.Stop() + s.headlessTimer = nil + } + w := s.mainWindow + ready := w != nil && s.ready[w.ID()] + s.mu.Unlock() + if ready { + return + } + + w, created := s.ensureMain("/") + if w == nil { + return + } + + s.mu.Lock() + if created { + s.headlessMain = true + } + pending := !s.ready[w.ID()] + if pending { + s.pendingEmits[w.ID()] = append(s.pendingEmits[w.ID()], EventTriggerLogin) + } + s.mu.Unlock() + + if !pending { + s.app.Event.Emit(EventTriggerLogin) + } + }) + + s.app.Event.On(EventBrowserLoginCancel, func(_ *application.CustomEvent) { + s.scheduleHeadlessTeardown() + }) + + s.app.Event.On(EventStatusSnapshot, func(e *application.CustomEvent) { + st, ok := e.Data.(Status) + if !ok { + return + } + switch st.Status { + case StatusConnected, StatusLoginFailed, StatusDaemonUnavailable: + s.scheduleHeadlessTeardown() + } + }) +} + +func (s *WindowManager) scheduleHeadlessTeardown() { + s.mu.Lock() + defer s.mu.Unlock() + if !s.headlessMain || s.mainWindow == nil { + return + } + if s.headlessTimer != nil { + s.headlessTimer.Stop() + } + s.headlessTimer = time.AfterFunc(headlessTeardownDelay, s.closeHeadlessMain) +} + +func (s *WindowManager) closeHeadlessMain() { + s.mu.Lock() + w := s.mainWindow + headless := s.headlessMain + s.headlessTimer = nil + s.mu.Unlock() + if !headless || w == nil { + return + } + w.Close() +} + +func (s *WindowManager) forgetWindowLocked(w *application.WebviewWindow) { + if w == nil { + return + } + + id := w.ID() + if timer := s.fallbackTimers[id]; timer != nil { + timer.Stop() + } + delete(s.fallbackTimers, id) + delete(s.ready, id) + delete(s.showPending, id) + delete(s.pendingTab, id) + delete(s.pendingEmits, id) + + kept := s.hiddenForLogin[:0] + for _, hidden := range s.hiddenForLogin { + if hidden != application.Window(w) { + kept = append(kept, hidden) + } + } + s.hiddenForLogin = kept +} + +func (s *WindowManager) windowByName(name string) *application.WebviewWindow { + s.mu.Lock() + defer s.mu.Unlock() + switch name { + case "main": + return s.mainWindow + case "settings": + return s.settings + default: + return nil + } +} + +func (s *WindowManager) markReady(w *application.WebviewWindow) { + id := w.ID() + s.mu.Lock() + already := s.ready[id] + s.ready[id] = true + wanted := s.showPending[id] + tab, hasTab := s.pendingTab[id] + emits := s.pendingEmits[id] + if timer := s.fallbackTimers[id]; timer != nil { + timer.Stop() + delete(s.fallbackTimers, id) + } + delete(s.showPending, id) + delete(s.pendingTab, id) + delete(s.pendingEmits, id) + s.mu.Unlock() + + if already { + return + } + + if hasTab { + s.app.Event.Emit(EventSettingsOpen, tab) + } + + if wanted { + s.showNow(w) + } + + for _, event := range emits { + s.app.Event.Emit(event) + } +} + +func (s *WindowManager) showWhenReady(w *application.WebviewWindow) { + if w == nil { + return + } + + id := w.ID() + s.mu.Lock() + ready := s.ready[id] + if !ready { + s.showPending[id] = true + } + s.mu.Unlock() + + if ready { + s.showNow(w) + } +} + +func (s *WindowManager) showNow(w *application.WebviewWindow) { + s.mu.Lock() + if w == s.mainWindow { + s.headlessMain = false + if s.headlessTimer != nil { + s.headlessTimer.Stop() + s.headlessTimer = nil + } + } + s.mu.Unlock() + w.Show() + w.Focus() + s.centerWhenReady(w) +} + +func (s *WindowManager) ShowMainAt(url string) { + w, created := s.ensureMain(url) + if w == nil { + return + } + if !created { + w.SetURL(url) + } + s.showWhenReady(w) +} + +func (s *WindowManager) SetMainFactory(f func(startURL string) *application.WebviewWindow) { + s.mu.Lock() + defer s.mu.Unlock() + s.newMain = f +} + +func (s *WindowManager) ForgetMain() { + s.mu.Lock() + defer s.mu.Unlock() + s.forgetWindowLocked(s.mainWindow) + s.mainWindow = nil + s.headlessMain = false + if s.headlessTimer != nil { + s.headlessTimer.Stop() + s.headlessTimer = nil + } } // SetRecenterOnShow installs the recenterOnShow predicate (see the field). diff --git a/client/ui/tray.go b/client/ui/tray.go index 148dd50b3..c392a0b62 100644 --- a/client/ui/tray.go +++ b/client/ui/tray.go @@ -174,7 +174,7 @@ func NewTray(app *application.App, window *application.WebviewWindow, svc TraySe // in the right locale — no English flash then re-paint. loc: svc.Localizer, } - t.updater = newTrayUpdater(app, window, svc.Update, svc.Notifier, t.loc, func() { t.applyIcon() }, func() { t.relayoutMenu() }) + t.updater = newTrayUpdater(app, t.showMainAt, svc.Update, svc.Notifier, t.loc, func() { t.applyIcon() }, func() { t.relayoutMenu() }) t.tray = app.SystemTray.New() // Seed panel-theme detection before the first paint so the initial icon // matches the panel's light/dark scheme (Linux only). @@ -241,9 +241,6 @@ func (t *Tray) ShowWindow() { w.Focus() return } - if t.window == nil { - return - } // Route through WindowManager so the main window is centered on first // show — minimal WMs (fluxbox, the XEmbed tray path) otherwise drop it in // the top-left corner. @@ -251,8 +248,49 @@ func (t *Tray) ShowWindow() { t.svc.WindowManager.ShowMain() return } - t.window.Show() - t.window.Focus() + if w := t.mainWindow(); w != nil { + w.Show() + w.Focus() + } +} + +func (t *Tray) mainWindow() *application.WebviewWindow { + if t.svc.WindowManager == nil { + return t.window + } + return t.svc.WindowManager.MainWindow() +} + +func (t *Tray) showMainAt(url string) { + if t.svc.WindowManager != nil { + t.svc.WindowManager.ShowMainAt(url) + return + } + if w := t.mainWindow(); w != nil { + w.SetURL(url) + w.Show() + w.Focus() + } +} + +func (t *Tray) showMain() { + if t.svc.WindowManager != nil { + t.svc.WindowManager.ShowMain() + return + } + if w := t.mainWindow(); w != nil { + w.Show() + w.Focus() + } +} + +func (t *Tray) showMainAndEmit(event string) { + if t.svc.WindowManager != nil { + t.svc.WindowManager.ShowMainAndEmit(event) + return + } + t.showMain() + t.app.Event.Emit(event) } // applyLanguage re-renders every translated surface in the Localizer's current @@ -479,7 +517,8 @@ func (t *Tray) handleConnect(upItem *application.MenuItem) { // NeedsLogin/SessionExpired/LoginFailed won't honor a plain Up RPC — they // need the Login → WaitSSOLogin → Up sequence. Emit EventTriggerLogin so // the React startLogin() (which owns the BrowserLogin popup) drives it; - // the hidden main webview is alive and subscribed, so only the popup shows. + // the WindowManager materialises a hidden main webview when none is live, + // so only the popup shows. t.statusMu.Lock() needsLogin := strings.EqualFold(t.lastStatus, services.StatusNeedsLogin) || strings.EqualFold(t.lastStatus, services.StatusSessionExpired) || diff --git a/client/ui/tray_session.go b/client/ui/tray_session.go index f25419894..6e5d07740 100644 --- a/client/ui/tray_session.go +++ b/client/ui/tray_session.go @@ -30,10 +30,7 @@ const ( // handleSessionExpired notifies and brings the window forward so the user can reconnect. func (t *Tray) handleSessionExpired() { t.notify(t.loc.T("notify.sessionExpired.title"), t.loc.T("notify.sessionExpired.body"), notifyIDSessionExpired) - if t.window != nil { - t.window.Show() - t.window.Focus() - } + t.showMain() } // applySessionExpiry refreshes the cached SSO deadline and reports whether it changed. @@ -307,7 +304,7 @@ func (t *Tray) openSessionExtendFlow() { } seconds := int(time.Until(deadline).Seconds()) if seconds <= 0 { - t.app.Event.Emit(services.EventTriggerLogin) + t.showMainAndEmit(services.EventTriggerLogin) return } if t.svc.WindowManager == nil { diff --git a/client/ui/tray_update.go b/client/ui/tray_update.go index 27037eccb..3ce1f9600 100644 --- a/client/ui/tray_update.go +++ b/client/ui/tray_update.go @@ -4,6 +4,7 @@ package main import ( "context" + neturl "net/url" "sync" "time" @@ -19,7 +20,7 @@ import ( // trayUpdater owns the tray UI that reacts to auto-update. Composed inside Tray. type trayUpdater struct { app *application.App - window *application.WebviewWindow + showMainAt func(url string) update *services.Update notifier *Notifier loc *Localizer @@ -36,10 +37,10 @@ type trayUpdater struct { progressWindowOpen bool } -func newTrayUpdater(app *application.App, window *application.WebviewWindow, update *services.Update, notifier *Notifier, loc *Localizer, onIconChange func(), onMenuChange func()) *trayUpdater { +func newTrayUpdater(app *application.App, showMainAt func(url string), update *services.Update, notifier *Notifier, loc *Localizer, onIconChange func(), onMenuChange func()) *trayUpdater { u := &trayUpdater{ app: app, - window: window, + showMainAt: showMainAt, update: update, notifier: notifier, loc: loc, @@ -185,14 +186,12 @@ func (u *trayUpdater) sendUpdateNotification(st updater.State) { // openProgressWindow points the main window at the /update progress page and // brings it forward. func (u *trayUpdater) openProgressWindow(version string) { - if u.window == nil { + if u.showMainAt == nil { return } url := "/#/update" if version != "" { - url += "?version=" + version + url += "?version=" + neturl.QueryEscape(version) } - u.window.SetURL(url) - u.window.Show() - u.window.Focus() + u.showMainAt(url) } diff --git a/go.mod b/go.mod index 265cd962f..efec8c94d 100644 --- a/go.mod +++ b/go.mod @@ -339,6 +339,6 @@ replace github.com/dexidp/dex/api/v2 => github.com/netbirdio/dex/api/v2 v2.0.0-2 replace github.com/mailru/easyjson => github.com/netbirdio/easyjson v0.9.0 -replace github.com/wailsapp/wails/v3 => github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260810103952-24e716aea4db +replace github.com/wailsapp/wails/v3 => github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260825085513-5f07a01f7a78 tool go.uber.org/mock/mockgen diff --git a/go.sum b/go.sum index d9d880ede..da68b6458 100644 --- a/go.sum +++ b/go.sum @@ -488,8 +488,8 @@ github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502 h1:3tHlFmhTdX9ax github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45 h1:ujgviVYmx243Ksy7NdSwrdGPSRNE3pb8kEDSpH0QuAQ= github.com/netbirdio/signal-dispatcher/dispatcher v0.0.0-20250805121659-6b4ac470ca45/go.mod h1:5/sjFmLb8O96B5737VCqhHyGRzNFIaN/Bu7ZodXc3qQ= -github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260810103952-24e716aea4db h1:gBOE2r4AW1soSmpYJC5/n9/1L8UQ8+HLjed8CY/TzZY= -github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260810103952-24e716aea4db/go.mod h1:bsdahLwBQxXjlmdPPeQyrTcDJfcqAr/ymFj0RXhwtWI= +github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260825085513-5f07a01f7a78 h1:B/jRv24jnFeoA+VccxoCx6K94PUgsqR9wnshpeu9M+8= +github.com/netbirdio/wails/v3 v3.0.0-beta.3.0.20260825085513-5f07a01f7a78/go.mod h1:/6QR46/nhGCSADHbS++XtDb9dkTnenTHlGskTPRo9S0= github.com/netbirdio/wireguard-go v0.0.0-20260628102922-2834bebf6c1a h1:3CWK+yTvRKOcC0Q8VCTGy4l60TEb27CQVS7LkMxwjmw= github.com/netbirdio/wireguard-go v0.0.0-20260628102922-2834bebf6c1a/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=