mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-29 20:19:56 +00:00
localize window titles, fix size for windows (and other platforms?)
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
import { ReactNode, forwardRef } from "react";
|
||||
import {cn} from "@/lib/cn.ts";
|
||||
import {isMacOS} from "@/lib/platform.ts";
|
||||
|
||||
// ConfirmDialog is the shared layout wrapper used by dialog-style window
|
||||
// surfaces (SessionExpired, SessionAboutToExpire, …). Purely a layout
|
||||
@@ -23,7 +25,7 @@ export const ConfirmDialog = forwardRef<HTMLDivElement, ConfirmDialogProps>(
|
||||
>
|
||||
<div
|
||||
ref={ref}
|
||||
className={"flex flex-col items-center gap-5 p-8 text-center"}
|
||||
className={cn("flex flex-col items-center gap-5 p-8 text-center", !isMacOS() && "pt-4")}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useLayoutEffect, useRef } from "react";
|
||||
import { Window } from "@wailsio/runtime";
|
||||
import {isMacOS} from "@/lib/platform.ts";
|
||||
|
||||
// useAutoSizeWindow resizes the current Wails window so its height matches
|
||||
// the measured height of the content element the returned ref is attached
|
||||
@@ -21,7 +22,8 @@ export function useAutoSizeWindow<T extends HTMLElement>(width: number) {
|
||||
if (!el) return;
|
||||
let shown = false;
|
||||
const apply = () => {
|
||||
const h = Math.ceil(el.getBoundingClientRect().height);
|
||||
let h = Math.ceil(el.getBoundingClientRect().height);
|
||||
h = isMacOS() ? h : h - el.getBoundingClientRect().height;
|
||||
if (h <= 0) return;
|
||||
void Window.SetSize(width, h)
|
||||
.then(() => {
|
||||
|
||||
@@ -294,6 +294,12 @@
|
||||
"update.page.complete": "Update abgeschlossen",
|
||||
"update.page.failed": "Update fehlgeschlagen",
|
||||
|
||||
"window.title.settings": "Einstellungen",
|
||||
"window.title.signIn": "Anmeldung",
|
||||
"window.title.sessionExpired": "Sitzung abgelaufen",
|
||||
"window.title.sessionExpiring": "Sitzung läuft ab",
|
||||
"window.title.updating": "Aktualisierung",
|
||||
|
||||
"browserLogin.title": "Setzen Sie den Anmeldevorgang im Browser fort",
|
||||
"browserLogin.notSeeing": "Sehen Sie den Browser-Tab nicht?",
|
||||
"browserLogin.tryAgain": "Erneut versuchen",
|
||||
|
||||
@@ -312,6 +312,12 @@
|
||||
"update.page.complete": "Update complete",
|
||||
"update.page.failed": "Update failed",
|
||||
|
||||
"window.title.settings": "Settings",
|
||||
"window.title.signIn": "Sign-in",
|
||||
"window.title.sessionExpired": "Session Expired",
|
||||
"window.title.sessionExpiring": "Session Expiring",
|
||||
"window.title.updating": "Updating",
|
||||
|
||||
"browserLogin.title": "Continue in your browser to complete the login",
|
||||
"browserLogin.notSeeing": "Not seeing the browser tab?",
|
||||
"browserLogin.tryAgain": "Try again",
|
||||
|
||||
@@ -294,6 +294,12 @@
|
||||
"update.page.complete": "Frissítés kész",
|
||||
"update.page.failed": "Frissítés sikertelen",
|
||||
|
||||
"window.title.settings": "Beállítások",
|
||||
"window.title.signIn": "Bejelentkezés",
|
||||
"window.title.sessionExpired": "Munkamenet lejárt",
|
||||
"window.title.sessionExpiring": "Munkamenet lejár",
|
||||
"window.title.updating": "Frissítés",
|
||||
|
||||
"browserLogin.title": "Folytassa a böngészőben a bejelentkezés befejezéséhez",
|
||||
"browserLogin.notSeeing": "Nem látja a böngésző fülét?",
|
||||
"browserLogin.tryAgain": "Próbálja újra",
|
||||
|
||||
@@ -140,7 +140,8 @@ func main() {
|
||||
// (BrowserLogin, Session*, InstallProgress) stay lazy + destroy-on-close
|
||||
// so they don't linger as hidden windows that Wails's macOS dock-reopen
|
||||
// handler would pop back up.
|
||||
windowManager := services.NewWindowManager(app, window)
|
||||
windowManager := services.NewWindowManager(app, window, bundle, prefStore)
|
||||
windowManager.WatchLanguage(prefStore)
|
||||
app.RegisterService(application.NewService(windowManager))
|
||||
|
||||
// Register an in-process StatusNotifierWatcher so the tray works on
|
||||
|
||||
@@ -9,8 +9,20 @@ import (
|
||||
|
||||
"github.com/wailsapp/wails/v3/pkg/application"
|
||||
"github.com/wailsapp/wails/v3/pkg/events"
|
||||
|
||||
"github.com/netbirdio/netbird/client/ui/i18n"
|
||||
"github.com/netbirdio/netbird/client/ui/preferences"
|
||||
)
|
||||
|
||||
// LanguageSubscriber delivers UI preference changes (currently only the
|
||||
// language flip; reusing preferences.UIPreferences keeps the channel
|
||||
// payload identical to preferences.Store.Subscribe). The runtime
|
||||
// implementation is *preferences.Store. WindowManager uses this to keep
|
||||
// the long-lived Settings window title in the active language.
|
||||
type LanguageSubscriber interface {
|
||||
Subscribe() (<-chan preferences.UIPreferences, func())
|
||||
}
|
||||
|
||||
// EventTriggerLogin asks the frontend's startLogin() orchestrator to begin
|
||||
// an SSO flow. Emitted by the tray (Login menu item, session expired) since
|
||||
// the tray can't call JS directly.
|
||||
@@ -118,6 +130,8 @@ func DialogWindowOptions(name, title, url string) application.WebviewWindowOptio
|
||||
type WindowManager struct {
|
||||
app *application.App
|
||||
mainWindow *application.WebviewWindow
|
||||
translator ErrorTranslator
|
||||
prefs LanguagePreference
|
||||
settings *application.WebviewWindow
|
||||
browserLogin *application.WebviewWindow
|
||||
sessionExpired *application.WebviewWindow
|
||||
@@ -131,19 +145,39 @@ type WindowManager struct {
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// title resolves a window-title i18n key in the user's current language.
|
||||
// Falls back to the raw key when the translator or prefs are missing
|
||||
// (mirrors services.Connection.translateShort) — a deliberate fail-loud
|
||||
// signal that a key is missing from the bundle.
|
||||
func (s *WindowManager) title(key string) string {
|
||||
if s.translator == nil {
|
||||
return key
|
||||
}
|
||||
lang := i18n.DefaultLanguage
|
||||
if s.prefs != nil {
|
||||
if pref := s.prefs.Get().Language; pref != "" {
|
||||
lang = pref
|
||||
}
|
||||
}
|
||||
return s.translator.Translate(lang, key)
|
||||
}
|
||||
|
||||
// NewWindowManager wires the manager to the main app. `mainWindow` is the
|
||||
// up-front-created webview the user interacts with from the tray — used to
|
||||
// pick the BrowserLogin window's display so the sign-in popup follows the
|
||||
// user onto the screen they're already looking at.
|
||||
// user onto the screen they're already looking at. `translator` + `prefs`
|
||||
// resolve the user-facing window titles in the active UI language; both
|
||||
// may be nil (callers in tests can omit them), in which case title() falls
|
||||
// back to the raw i18n key.
|
||||
//
|
||||
// The Settings window is created here, hidden, so the first OpenSettings
|
||||
// call paints instantly instead of paying webview construction + asset load
|
||||
// at click time.
|
||||
func NewWindowManager(app *application.App, mainWindow *application.WebviewWindow) *WindowManager {
|
||||
s := &WindowManager{app: app, mainWindow: mainWindow}
|
||||
func NewWindowManager(app *application.App, mainWindow *application.WebviewWindow, translator ErrorTranslator, prefs LanguagePreference) *WindowManager {
|
||||
s := &WindowManager{app: app, mainWindow: mainWindow, translator: translator, prefs: prefs}
|
||||
s.settings = app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Name: "settings",
|
||||
Title: "Settings",
|
||||
Title: s.title("window.title.settings"),
|
||||
Width: 900,
|
||||
Height: 640,
|
||||
Hidden: true,
|
||||
@@ -169,6 +203,56 @@ func NewWindowManager(app *application.App, mainWindow *application.WebviewWindo
|
||||
return s
|
||||
}
|
||||
|
||||
// WatchLanguage subscribes to UI preference changes and re-applies the
|
||||
// localised title to every live auxiliary window whenever the language
|
||||
// flips. The eagerly-created Settings window outlives its first paint, so
|
||||
// without this its title would stay frozen in the language it was created
|
||||
// in; the on-demand dialog windows (BrowserLogin, Session*, InstallProgress)
|
||||
// can also coexist with a Settings-driven language change. Safe to call
|
||||
// once at startup; subsequent calls overwrite the previous subscription.
|
||||
// No-op when sub is nil.
|
||||
func (s *WindowManager) WatchLanguage(sub LanguageSubscriber) {
|
||||
if sub == nil {
|
||||
return
|
||||
}
|
||||
ch, _ := sub.Subscribe()
|
||||
go func() {
|
||||
var last i18n.LanguageCode
|
||||
for p := range ch {
|
||||
if p.Language == "" || p.Language == last {
|
||||
continue
|
||||
}
|
||||
last = p.Language
|
||||
s.retitleAll()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// retitleAll re-applies the localised title to every currently-alive
|
||||
// auxiliary window. Reads the window pointers under s.mu so a concurrent
|
||||
// Open*/Close* can't observe a torn slice. SetTitle itself dispatches to
|
||||
// the OS UI thread, so calling it from this goroutine is safe.
|
||||
func (s *WindowManager) retitleAll() {
|
||||
s.mu.Lock()
|
||||
type pair struct {
|
||||
win *application.WebviewWindow
|
||||
key string
|
||||
}
|
||||
wins := []pair{
|
||||
{s.settings, "window.title.settings"},
|
||||
{s.browserLogin, "window.title.signIn"},
|
||||
{s.sessionExpired, "window.title.sessionExpired"},
|
||||
{s.sessionAboutToExpire, "window.title.sessionExpiring"},
|
||||
{s.installProgress, "window.title.updating"},
|
||||
}
|
||||
s.mu.Unlock()
|
||||
for _, p := range wins {
|
||||
if p.win != nil {
|
||||
p.win.SetTitle(s.title(p.key))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OpenSettings asks the (already-mounted, currently-hidden) settings window
|
||||
// to land on `tab` and bring itself to front. Empty `tab` lands on General.
|
||||
//
|
||||
@@ -211,7 +295,7 @@ func (s *WindowManager) OpenBrowserLogin(uri string) {
|
||||
screen = sc
|
||||
}
|
||||
}
|
||||
opts := DialogWindowOptions("browser-login", "NetBird Sign-in", startURL)
|
||||
opts := DialogWindowOptions("browser-login", s.title("window.title.signIn"), startURL)
|
||||
// SSO popup deliberately is NOT always-on-top — the user moves
|
||||
// between the browser tab and our popup; pinning it would obscure
|
||||
// the browser at the moment they need to interact with it.
|
||||
@@ -321,7 +405,7 @@ func (s *WindowManager) OpenSessionExpired() {
|
||||
defer s.mu.Unlock()
|
||||
if s.sessionExpired == nil {
|
||||
s.sessionExpired = s.app.Window.NewWithOptions(
|
||||
DialogWindowOptions("session-expired", "NetBird", "/#/dialog/session-expired"),
|
||||
DialogWindowOptions("session-expired", s.title("window.title.sessionExpired"), "/#/dialog/session-expired"),
|
||||
)
|
||||
s.sessionExpired.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
||||
s.mu.Lock()
|
||||
@@ -356,7 +440,7 @@ func (s *WindowManager) OpenSessionAboutToExpire(seconds int) {
|
||||
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", "NetBird", startURL),
|
||||
DialogWindowOptions("session-about-to-expire", s.title("window.title.sessionExpiring"), startURL),
|
||||
)
|
||||
s.sessionAboutToExpire.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
||||
s.mu.Lock()
|
||||
@@ -402,7 +486,7 @@ func (s *WindowManager) OpenInstallProgress(version string) {
|
||||
if s.installProgress == nil {
|
||||
s.hideOtherWindowsLocked("install-progress")
|
||||
s.installProgress = s.app.Window.NewWithOptions(
|
||||
DialogWindowOptions("install-progress", "NetBird", startURL),
|
||||
DialogWindowOptions("install-progress", s.title("window.title.updating"), startURL),
|
||||
)
|
||||
s.installProgress.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
|
||||
s.mu.Lock()
|
||||
|
||||
Reference in New Issue
Block a user