mirror of
https://github.com/netbirdio/netbird.git
synced 2026-10-03 03:59:07 +02:00
update session expiration dialog texts, add monitor aware position
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
//go:build darwin
|
||||
|
||||
package services
|
||||
|
||||
/*
|
||||
#cgo CFLAGS: -x objective-c
|
||||
#cgo LDFLAGS: -framework Foundation -framework Cocoa -framework AppKit
|
||||
#import <Cocoa/Cocoa.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
|
||||
typedef struct CursorPoint {
|
||||
int x;
|
||||
int y;
|
||||
int ok;
|
||||
} CursorPoint;
|
||||
|
||||
// NSEvent.mouseLocation is Y-up from primary's bottom-left; flip against
|
||||
// the primary's frame height so the point matches Wails' Y-down Screen.Bounds.
|
||||
CursorPoint nbGetCursorPos(void) {
|
||||
CursorPoint p = {0, 0, 0};
|
||||
NSArray<NSScreen *> *screens = [NSScreen screens];
|
||||
if (screens == nil || screens.count == 0) return p;
|
||||
NSScreen *primary = [screens firstObject];
|
||||
if (primary == nil) return p;
|
||||
NSPoint loc = [NSEvent mouseLocation];
|
||||
p.x = (int)loc.x;
|
||||
p.y = (int)(primary.frame.size.height - loc.y);
|
||||
p.ok = 1;
|
||||
return p;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import "github.com/wailsapp/wails/v3/pkg/application"
|
||||
|
||||
func getCursorPosition(_ *application.App) (application.Point, bool) {
|
||||
res := C.nbGetCursorPos()
|
||||
if res.ok == 0 {
|
||||
return application.Point{}, false
|
||||
}
|
||||
return application.Point{X: int(res.x), Y: int(res.y)}, true
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
//go:build linux
|
||||
|
||||
package services
|
||||
|
||||
/*
|
||||
#cgo pkg-config: x11
|
||||
#cgo LDFLAGS: -lX11
|
||||
#include <X11/Xlib.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
typedef struct CursorPoint {
|
||||
int x;
|
||||
int y;
|
||||
int ok;
|
||||
} CursorPoint;
|
||||
|
||||
// XQueryPointer hits Xorg directly on X11 sessions and XWayland on
|
||||
// Wayland sessions (shipped by default on the supported distros). ok=0
|
||||
// when no X server is reachable — caller falls back gracefully.
|
||||
CursorPoint nbGetCursorPos(void) {
|
||||
CursorPoint p = {0, 0, 0};
|
||||
Display *dpy = XOpenDisplay(NULL);
|
||||
if (!dpy) return p;
|
||||
Window root = DefaultRootWindow(dpy);
|
||||
if (root == 0) { XCloseDisplay(dpy); return p; }
|
||||
Window root_return = 0, child_return = 0;
|
||||
int root_x = 0, root_y = 0, win_x = 0, win_y = 0;
|
||||
unsigned int mask_return = 0;
|
||||
if (XQueryPointer(dpy, root, &root_return, &child_return,
|
||||
&root_x, &root_y, &win_x, &win_y, &mask_return)) {
|
||||
p.x = root_x;
|
||||
p.y = root_y;
|
||||
p.ok = 1;
|
||||
}
|
||||
XCloseDisplay(dpy);
|
||||
return p;
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import "github.com/wailsapp/wails/v3/pkg/application"
|
||||
|
||||
func getCursorPosition(app *application.App) (application.Point, bool) {
|
||||
res := C.nbGetCursorPos()
|
||||
if res.ok == 0 {
|
||||
return application.Point{}, false
|
||||
}
|
||||
p := application.Point{X: int(res.x), Y: int(res.y)}
|
||||
// X11 root coords are physical pixels; Screen.Bounds is in DIPs.
|
||||
if app == nil || app.Screen == nil {
|
||||
return p, true
|
||||
}
|
||||
return app.Screen.PhysicalToDipPoint(p), true
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//go:build !darwin && !windows && !linux
|
||||
|
||||
package services
|
||||
|
||||
import "github.com/wailsapp/wails/v3/pkg/application"
|
||||
|
||||
func getCursorPosition(_ *application.App) (application.Point, bool) {
|
||||
return application.Point{}, false
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
//go:build windows
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/w32"
|
||||
)
|
||||
|
||||
func getCursorPosition(app *application.App) (application.Point, bool) {
|
||||
x, y, ok := w32.GetCursorPos()
|
||||
if !ok || app == nil || app.Screen == nil {
|
||||
return application.Point{}, false
|
||||
}
|
||||
// GetCursorPos is in physical pixels; Screen.Bounds is in DIPs.
|
||||
return app.Screen.PhysicalToDipPoint(application.Point{X: x, Y: y}), true
|
||||
}
|
||||
@@ -20,7 +20,7 @@ type (
|
||||
|
||||
// Session is the Wails-bound wrapper around authsession.Session. It only
|
||||
// re-exposes the subset the React frontend actually calls
|
||||
// (SessionAboutToExpireDialog.tsx: RequestExtend + WaitExtend). The tray
|
||||
// (SessionExpirationDialog.tsx: RequestExtend + WaitExtend). The tray
|
||||
// uses authsession.Session directly, so methods that only the tray needs
|
||||
// (DismissWarning) are deliberately absent here — keeping the generated
|
||||
// TS surface minimal.
|
||||
|
||||
@@ -105,8 +105,8 @@ func LinuxAppearanceOptions(icon []byte) application.LinuxWindow {
|
||||
}
|
||||
|
||||
// DialogWindowOptions is the baseline for every auxiliary dialog window
|
||||
// (BrowserLogin, SessionExpired, SessionAboutToExpire, InstallProgress).
|
||||
// All four share size (360x320), the no-resize / no-min / no-max chrome,
|
||||
// (BrowserLogin, SessionExpiration, InstallProgress).
|
||||
// All share size (360x320), the no-resize / no-min / no-max chrome,
|
||||
// Hidden-on-create (so the React side can auto-size before first paint),
|
||||
// AlwaysOnTop (the dialogs interrupt the user, the SSO popup overrides
|
||||
// this), and the shared background/Mac/Windows appearance. Callers fill
|
||||
@@ -152,8 +152,7 @@ type WindowManager struct {
|
||||
linuxIcon []byte
|
||||
settings *application.WebviewWindow
|
||||
browserLogin *application.WebviewWindow
|
||||
sessionExpired *application.WebviewWindow
|
||||
sessionAboutToExpire *application.WebviewWindow
|
||||
sessionExpiration *application.WebviewWindow
|
||||
installProgress *application.WebviewWindow
|
||||
welcome *application.WebviewWindow
|
||||
errorDialog *application.WebviewWindow
|
||||
@@ -268,8 +267,7 @@ func (s *WindowManager) retitleAll() {
|
||||
wins := []pair{
|
||||
{s.settings, "window.title.settings"},
|
||||
{s.browserLogin, "window.title.signIn"},
|
||||
{s.sessionExpired, "window.title.sessionExpired"},
|
||||
{s.sessionAboutToExpire, "window.title.sessionExpiring"},
|
||||
{s.sessionExpiration, "window.title.sessionExpiration"},
|
||||
{s.installProgress, "window.title.updating"},
|
||||
{s.welcome, "window.title.welcome"},
|
||||
{s.errorDialog, "window.title.error"},
|
||||
@@ -428,77 +426,37 @@ func (s *WindowManager) CloseBrowserLogin() {
|
||||
}
|
||||
}
|
||||
|
||||
// OpenSessionExpired shows the "session expired" prompt window above all
|
||||
// other application windows. Singleton — destroyed on close.
|
||||
//
|
||||
// The window is created Hidden so the React side can measure its content
|
||||
// and call Window.SetSize + Window.Show before the user sees the chrome —
|
||||
// otherwise the user would briefly see the 360x320 placeholder snapping to
|
||||
// the measured height. Re-opens (singleton already alive) Show/Focus
|
||||
// directly here.
|
||||
func (s *WindowManager) OpenSessionExpired() {
|
||||
// OpenSessionExpiration shows the countdown warning above all other
|
||||
// windows on the display the cursor is currently on. `seconds` seeds the
|
||||
// mm:ss countdown rendered React-side. Singleton, destroyed on close.
|
||||
func (s *WindowManager) OpenSessionExpiration(seconds int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if s.sessionExpired == nil {
|
||||
s.sessionExpired = s.app.Window.NewWithOptions(
|
||||
DialogWindowOptions("session-expired", s.title("window.title.sessionExpired"), "/#/dialog/session-expired", s.linuxIcon),
|
||||
)
|
||||
s.sessionExpired.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
||||
startURL := "/#/dialog/session-expiration?seconds=" + strconv.Itoa(seconds)
|
||||
if s.sessionExpiration == nil {
|
||||
opts := DialogWindowOptions("session-expiration", s.title("window.title.sessionExpiration"), startURL, s.linuxIcon)
|
||||
opts.Screen = s.getScreenBasedOnCursorPosition()
|
||||
opts.InitialPosition = application.WindowCentered
|
||||
s.sessionExpiration = s.app.Window.NewWithOptions(opts)
|
||||
s.sessionExpiration.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
||||
s.mu.Lock()
|
||||
s.sessionExpired = nil
|
||||
s.sessionExpiration = nil
|
||||
s.mu.Unlock()
|
||||
})
|
||||
s.centerWhenReady(s.sessionExpired)
|
||||
s.centerOnCursorScreen(s.sessionExpiration)
|
||||
return
|
||||
}
|
||||
s.sessionExpired.Show()
|
||||
s.sessionExpired.Focus()
|
||||
s.centerWhenReady(s.sessionExpired)
|
||||
s.sessionExpiration.SetURL(startURL)
|
||||
s.centerOnCursorScreen(s.sessionExpiration)
|
||||
s.sessionExpiration.Show()
|
||||
s.sessionExpiration.Focus()
|
||||
}
|
||||
|
||||
// CloseSessionExpired destroys the session-expired window if open.
|
||||
func (s *WindowManager) CloseSessionExpired() {
|
||||
// CloseSessionExpiration destroys the countdown warning window if open.
|
||||
func (s *WindowManager) CloseSessionExpiration() {
|
||||
s.mu.Lock()
|
||||
w := s.sessionExpired
|
||||
s.sessionExpired = nil
|
||||
s.mu.Unlock()
|
||||
if w != nil {
|
||||
w.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// OpenSessionAboutToExpire shows the countdown warning window above all
|
||||
// other application windows. `seconds` seeds the initial countdown value
|
||||
// rendered as mm:ss in the React layer. Singleton — destroyed on close.
|
||||
// Window is created Hidden so the React side can auto-size before paint
|
||||
// (see OpenSessionExpired comment).
|
||||
func (s *WindowManager) OpenSessionAboutToExpire(seconds int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
startURL := "/#/dialog/session-about-to-expire?seconds=" + strconv.Itoa(seconds)
|
||||
if s.sessionAboutToExpire == nil {
|
||||
s.sessionAboutToExpire = s.app.Window.NewWithOptions(
|
||||
DialogWindowOptions("session-about-to-expire", s.title("window.title.sessionExpiring"), startURL, s.linuxIcon),
|
||||
)
|
||||
s.sessionAboutToExpire.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
||||
s.mu.Lock()
|
||||
s.sessionAboutToExpire = nil
|
||||
s.mu.Unlock()
|
||||
})
|
||||
s.centerWhenReady(s.sessionAboutToExpire)
|
||||
return
|
||||
}
|
||||
s.sessionAboutToExpire.SetURL(startURL)
|
||||
s.sessionAboutToExpire.Show()
|
||||
s.sessionAboutToExpire.Focus()
|
||||
s.centerWhenReady(s.sessionAboutToExpire)
|
||||
}
|
||||
|
||||
// CloseSessionAboutToExpire destroys the countdown warning window if open.
|
||||
func (s *WindowManager) CloseSessionAboutToExpire() {
|
||||
s.mu.Lock()
|
||||
w := s.sessionAboutToExpire
|
||||
s.sessionAboutToExpire = nil
|
||||
w := s.sessionExpiration
|
||||
s.sessionExpiration = nil
|
||||
s.mu.Unlock()
|
||||
if w != nil {
|
||||
w.Close()
|
||||
@@ -698,6 +656,27 @@ func (s *WindowManager) SetRecenterOnShow(pred func() bool) {
|
||||
s.recenterOnShow = pred
|
||||
}
|
||||
|
||||
// getScreenBasedOnCursorPosition returns the display the OS cursor is
|
||||
// on, falling back through main-window screen → nil (Wails treats nil
|
||||
// as OS-default placement). Linux uses XQueryPointer via XWayland on
|
||||
// Wayland sessions, which ships by default on the supported distros.
|
||||
func (s *WindowManager) getScreenBasedOnCursorPosition() *application.Screen {
|
||||
if s.app == nil || s.app.Screen == nil {
|
||||
return nil
|
||||
}
|
||||
if p, ok := getCursorPosition(s.app); ok {
|
||||
if sc := s.app.Screen.ScreenNearestDipPoint(p); sc != nil {
|
||||
return sc
|
||||
}
|
||||
}
|
||||
if s.mainWindow != nil {
|
||||
if sc, err := s.mainWindow.GetScreen(); err == nil {
|
||||
return sc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// centerWhenReady centers w once its native window actually exists — but only
|
||||
// in environments where the WM won't do it for us (recenterOnShow). On full
|
||||
// desktops the WM centers small windows and restores position across hide ->
|
||||
@@ -733,3 +712,44 @@ func (s *WindowManager) centerWhenReady(w *application.WebviewWindow) {
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// centerOnCursorScreen centers w in the work area of the display the
|
||||
// cursor is on. Each guard is a no-op (nil window, no cursor screen,
|
||||
// zero size, zero work area) so a headless / no-monitor session is safe.
|
||||
// On minimal WMs (recenterOnShow → Fluxbox/XEmbed) the same retry loop
|
||||
// centerWhenReady uses kicks in: Linux SetPosition silently no-ops while
|
||||
// the GdkSurface is nil, and a non-zero post-move Position is the
|
||||
// signal that it landed.
|
||||
func (s *WindowManager) centerOnCursorScreen(w *application.WebviewWindow) {
|
||||
if w == nil {
|
||||
return
|
||||
}
|
||||
place := func() {
|
||||
screen := s.getScreenBasedOnCursorPosition()
|
||||
if screen == nil {
|
||||
return
|
||||
}
|
||||
width, height := w.Size()
|
||||
if width <= 0 || height <= 0 {
|
||||
return
|
||||
}
|
||||
wa := screen.WorkArea
|
||||
if wa.Width <= 0 || wa.Height <= 0 {
|
||||
return
|
||||
}
|
||||
w.SetPosition(wa.X+(wa.Width-width)/2, wa.Y+(wa.Height-height)/2)
|
||||
}
|
||||
place()
|
||||
if s.recenterOnShow == nil || !s.recenterOnShow() {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
for i := 0; i < 50; i++ {
|
||||
place()
|
||||
if x, y := w.Position(); x != 0 || y != 0 {
|
||||
return
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user