From 38650dee145efe28e070baa742c9e643ec2f6388 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Thu, 20 Aug 2026 09:31:29 -0400 Subject: [PATCH 1/2] Never show the main window on a daemon launch --- src/UniGetUI.Avalonia/App.axaml.cs | 26 +++++-------------- .../Infrastructure/AvaloniaBootstrapper.cs | 20 ++++++++++++-- .../Views/MainWindow.axaml.cs | 5 ++++ 3 files changed, 30 insertions(+), 21 deletions(-) diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs index 21fc57b544..48ed3bc1dd 100644 --- a/src/UniGetUI.Avalonia/App.axaml.cs +++ b/src/UniGetUI.Avalonia/App.axaml.cs @@ -149,19 +149,6 @@ private static void StartMainWindow(IClassicDesktopStyleApplicationLifetime desk }; } - if (CoreData.WasDaemon) - { - // Start silently: hide the window on first open only. - // Opened fires on every Show() in Avalonia, so we must unsubscribe - // immediately or every ShowFromTray() call would hide the window again. - void HideOnce(object? s, EventArgs e) - { - mainWindow.Opened -= HideOnce; - mainWindow.Hide(); - } - mainWindow.Opened += HideOnce; - } - if (splash is not null) { var splashRef = splash; @@ -173,9 +160,10 @@ void CloseSplashOnce(object? s, EventArgs e) mainWindow.Opened += CloseSplashOnce; } - // Framework auto-show already passed (we deferred via Dispatcher.Post), - // so we have to open the window ourselves. - mainWindow.Show(); + // Framework auto-show already passed (we deferred via Dispatcher.Post), so we have to + // open the window ourselves. Daemon mode never shows it at all. + if (!CoreData.WasDaemon) + mainWindow.Show(); _ = StartupAsync(mainWindow); } @@ -194,9 +182,9 @@ private static async Task StartupAsync(MainWindow mainWindow) // ShowDialog tries to attach to it as owner. await Task.Yield(); - // ShowDialog requires a visible owner. In daemon mode the main window - // is hidden, so temporarily show it and re-hide after the dialog closes. - bool reshide = CoreData.WasDaemon; + // The dialog is hosted inside the main window, so it is only reachable while + // that window is on screen. Bring it up for the dialog and put it back after. + bool reshide = !mainWindow.IsVisible; if (reshide) mainWindow.Show(); await new CrashReportWindow(report).ShowDialog(mainWindow); if (reshide) mainWindow.Hide(); diff --git a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs index 2aad7ac25b..bbf62eff96 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs @@ -91,14 +91,30 @@ await Dispatcher.UIThread.InvokeAsync( private static async Task ShowIntegrityViolationDialogAsync() { if (MainWindow.Instance is not { } owner) return; - await new IntegrityViolationDialog().ShowDialog(owner); + await ShowStartupDialogAsync(owner, new IntegrityViolationDialog()); } private static async Task ShowMissingDependencyDialogAsync( ManagerDependency dep, int current, int total) { if (MainWindow.Instance is not { } owner) return; - await new MissingDependencyDialog(dep, current, total).ShowDialog(owner); + await ShowStartupDialogAsync(owner, new MissingDependencyDialog(dep, current, total)); + } + + // Startup dialogs live inside the main window, so they are unreachable — and never complete — + // while it is off screen (daemon launch). Bring it up for the dialog and put it back after. + private static async Task ShowStartupDialogAsync(MainWindow owner, ImmersiveDialog dialog) + { + bool reshide = !owner.IsVisible; + if (reshide) owner.Show(); + try + { + await dialog.ShowDialog(owner); + } + finally + { + if (reshide) owner.Hide(); + } } private static Task InitializeSharedServicesAsync() diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index 05b2b466b6..3e1b5399d4 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -829,6 +829,11 @@ private void SaveGeometryNow() { try { + // Width/Height stay NaN until the window has been laid out, which never happens on a + // daemon launch the user has not opened yet. Saving then would persist a 0x0 size. + if (double.IsNaN(Width) || double.IsNaN(Height)) + return; + int state = WindowState == WindowState.Maximized ? 1 : 0; string geometry = $"v2,{Position.X},{Position.Y},{(int)Width},{(int)Height},{state}"; Settings.SetValue(Settings.K.WindowGeometry, geometry); From 01643c0a022d3d5479022510667ca0ace71da654 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Thu, 20 Aug 2026 09:51:13 -0400 Subject: [PATCH 2/2] Restore window visibility in a finally for every hidden-window dialog --- src/UniGetUI.Avalonia/App.axaml.cs | 7 +------ .../Infrastructure/AvaloniaBootstrapper.cs | 20 ++----------------- .../Views/MainWindow.axaml.cs | 19 ++++++++++++++++++ 3 files changed, 22 insertions(+), 24 deletions(-) diff --git a/src/UniGetUI.Avalonia/App.axaml.cs b/src/UniGetUI.Avalonia/App.axaml.cs index 48ed3bc1dd..1142b6c6ac 100644 --- a/src/UniGetUI.Avalonia/App.axaml.cs +++ b/src/UniGetUI.Avalonia/App.axaml.cs @@ -182,12 +182,7 @@ private static async Task StartupAsync(MainWindow mainWindow) // ShowDialog tries to attach to it as owner. await Task.Yield(); - // The dialog is hosted inside the main window, so it is only reachable while - // that window is on screen. Bring it up for the dialog and put it back after. - bool reshide = !mainWindow.IsVisible; - if (reshide) mainWindow.Show(); - await new CrashReportWindow(report).ShowDialog(mainWindow); - if (reshide) mainWindow.Hide(); + await mainWindow.ShowDialogAndRestoreVisibilityAsync(new CrashReportWindow(report)); } catch { /* must not prevent normal startup */ } } diff --git a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs index bbf62eff96..c37fa9ce84 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaBootstrapper.cs @@ -91,30 +91,14 @@ await Dispatcher.UIThread.InvokeAsync( private static async Task ShowIntegrityViolationDialogAsync() { if (MainWindow.Instance is not { } owner) return; - await ShowStartupDialogAsync(owner, new IntegrityViolationDialog()); + await owner.ShowDialogAndRestoreVisibilityAsync(new IntegrityViolationDialog()); } private static async Task ShowMissingDependencyDialogAsync( ManagerDependency dep, int current, int total) { if (MainWindow.Instance is not { } owner) return; - await ShowStartupDialogAsync(owner, new MissingDependencyDialog(dep, current, total)); - } - - // Startup dialogs live inside the main window, so they are unreachable — and never complete — - // while it is off screen (daemon launch). Bring it up for the dialog and put it back after. - private static async Task ShowStartupDialogAsync(MainWindow owner, ImmersiveDialog dialog) - { - bool reshide = !owner.IsVisible; - if (reshide) owner.Show(); - try - { - await dialog.ShowDialog(owner); - } - finally - { - if (reshide) owner.Hide(); - } + await owner.ShowDialogAndRestoreVisibilityAsync(new MissingDependencyDialog(dep, current, total)); } private static Task InitializeSharedServicesAsync() diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index 3e1b5399d4..282632837a 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -1561,6 +1561,25 @@ public async Task ShowManageIgnoredUpdatesAsync() await ShowImmersiveDialogAsync(dialog); } + // Immersive dialogs are hosted inside this window, so they are unreachable — and never complete — + // while it is off screen (daemon launch). Bring it up for the dialog and put it back after. + public async Task ShowDialogAndRestoreVisibilityAsync(ImmersiveDialog dialog) + { + bool reshide = !IsVisible; + if (reshide) + Show(); + + try + { + await dialog.ShowDialog(this); + } + finally + { + if (reshide) + Hide(); + } + } + public async Task ShowImmersiveDialogAsync(ImmersiveDialog dialog) { var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);