add onboarding

This commit is contained in:
Eduard Gert
2026-06-03 17:33:13 +02:00
parent 1710868a09
commit 64199209cf
23 changed files with 731 additions and 92 deletions
+5
View File
@@ -36,3 +36,8 @@ func (s *Preferences) SetLanguage(_ context.Context, lang i18n.LanguageCode) err
func (s *Preferences) SetViewMode(_ context.Context, mode preferences.ViewMode) error {
return s.store.SetViewMode(mode)
}
// SetOnboardingCompleted persists the welcome-flow dismissal flag.
func (s *Preferences) SetOnboardingCompleted(_ context.Context, done bool) error {
return s.store.SetOnboardingCompleted(done)
}
+76 -25
View File
@@ -154,6 +154,7 @@ type WindowManager struct {
sessionExpired *application.WebviewWindow
sessionAboutToExpire *application.WebviewWindow
installProgress *application.WebviewWindow
welcome *application.WebviewWindow
// hiddenForLogin remembers windows that were visible when the
// BrowserLogin popup opened. They were Hide()n to keep focus on the
// SSO flow without resorting to AlwaysOnTop, and are restored when
@@ -192,6 +193,26 @@ func (s *WindowManager) title(key string) string {
// at click time.
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}
// If the prefs implementation also exposes Subscribe (the runtime
// *preferences.Store does), wire up a goroutine that re-titles every
// live auxiliary window on language flip. Done here — instead of via
// an exported WatchLanguage method on the service — so the Wails
// binding generator doesn't try to expose a LanguageSubscriber-taking
// method to the frontend (interface params can't round-trip through
// JSON and would emit a generator warning).
if sub, ok := prefs.(LanguageSubscriber); ok && sub != nil {
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()
}
}()
}
s.settings = app.Window.NewWithOptions(application.WebviewWindowOptions{
Name: "settings",
Title: s.title("window.title.settings"),
@@ -221,31 +242,6 @@ 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
@@ -262,6 +258,7 @@ func (s *WindowManager) retitleAll() {
{s.sessionExpired, "window.title.sessionExpired"},
{s.sessionAboutToExpire, "window.title.sessionExpiring"},
{s.installProgress, "window.title.updating"},
{s.welcome, "window.title.welcome"},
}
s.mu.Unlock()
for _, p := range wins {
@@ -529,3 +526,57 @@ func (s *WindowManager) CloseInstallProgress() {
w.Close()
}
}
// OpenWelcome shows the first-launch onboarding window. The React side
// auto-sizes the window height to its content; the Continue button calls
// Preferences.SetOnboardingCompleted(true) before closing so the flow
// doesn't re-run. Singleton, destroyed on close. Created Hidden so the
// React side can auto-size before paint.
func (s *WindowManager) OpenWelcome() {
s.mu.Lock()
defer s.mu.Unlock()
if s.welcome == nil {
opts := DialogWindowOptions("welcome", s.title("window.title.welcome"), "/#/dialog/welcome", s.linuxIcon)
opts.Width = 420
// Onboarding stays AlwaysOnTop (inherited from DialogWindowOptions)
// so the user can't accidentally bury the first-launch flow behind
// another window and lose track of how to finish setup.
// Land in the middle of the user's primary display — the welcome
// flow is identity-defining and shouldn't read as an incidental
// dialog floating in a corner. WindowCentered + nil Screen
// resolves against the primary display (see WebviewWindowOptions).
opts.InitialPosition = application.WindowCentered
s.welcome = s.app.Window.NewWithOptions(opts)
w := s.welcome
w.OnWindowEvent(events.Common.WindowClosing, func(_ *application.WindowEvent) {
s.mu.Lock()
s.welcome = nil
s.mu.Unlock()
})
return
}
s.welcome.Show()
s.welcome.Focus()
}
// CloseWelcome destroys the welcome window if open.
func (s *WindowManager) CloseWelcome() {
s.mu.Lock()
w := s.welcome
s.welcome = nil
s.mu.Unlock()
if w != nil {
w.Close()
}
}
// OpenMain brings the main window forward. Used by the welcome Continue
// button to hand off from onboarding to the regular UI without depending
// on the tray.
func (s *WindowManager) OpenMain() {
if s.mainWindow == nil {
return
}
s.mainWindow.Show()
s.mainWindow.Focus()
}