diff --git a/src/UniGetUI.Avalonia/Infrastructure/MacOsNotificationBridge.cs b/src/UniGetUI.Avalonia/Infrastructure/MacOsNotificationBridge.cs index e3e853f030..da8a754226 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/MacOsNotificationBridge.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/MacOsNotificationBridge.cs @@ -35,7 +35,7 @@ public static bool ShowProgress(AbstractOperation operation) string message = operation.Metadata.Status.Length > 0 ? operation.Metadata.Status : CoreTools.Translate("Please wait..."); - DeliverNotification(title, message); + DeliverNotification(title, message, MainWindow.RuntimeNotificationLevel.Progress, allowInAppFallback: false); return true; } catch (Exception ex) @@ -57,7 +57,7 @@ public static bool ShowSuccess(AbstractOperation operation) string message = operation.Metadata.SuccessMessage.Length > 0 ? operation.Metadata.SuccessMessage : CoreTools.Translate("Success!"); - DeliverNotification(title, message); + DeliverNotification(title, message, MainWindow.RuntimeNotificationLevel.Success, allowInAppFallback: false); return true; } catch (Exception ex) @@ -79,7 +79,7 @@ public static bool ShowError(AbstractOperation operation) string message = operation.Metadata.FailureMessage.Length > 0 ? operation.Metadata.FailureMessage : CoreTools.Translate("An error occurred while processing this package"); - DeliverNotification(title, message); + DeliverNotification(title, message, MainWindow.RuntimeNotificationLevel.Error, allowInAppFallback: false); return true; } catch (Exception ex) @@ -109,7 +109,7 @@ public static void ShowUpdatesAvailableNotification(IReadOnlyList upgr title = CoreTools.Translate("Updates found!"); message = CoreTools.Translate("{0} packages can be updated", upgradable.Count); } - DeliverNotification(title, message); + DeliverNotification(title, message, MainWindow.RuntimeNotificationLevel.Success); } catch (Exception ex) { @@ -135,7 +135,7 @@ public static void ShowUpgradingPackagesNotification(IReadOnlyList upg title = CoreTools.Translate("{0} packages are being updated", upgradable.Count); message = string.Join(", ", upgradable.Select(p => p.Name)); } - DeliverNotification(title, message); + DeliverNotification(title, message, MainWindow.RuntimeNotificationLevel.Progress); } catch (Exception ex) { @@ -150,7 +150,8 @@ public static void ShowSelfUpdateAvailableNotification(string newVersion) { DeliverNotification( CoreTools.Translate("{0} can be updated to version {1}", "UniGetUI", newVersion), - CoreTools.Translate("You have currently version {0} installed", CoreData.VersionName)); + CoreTools.Translate("You have currently version {0} installed", CoreData.VersionName), + MainWindow.RuntimeNotificationLevel.Success); } catch (Exception ex) { @@ -179,7 +180,7 @@ public static void ShowNewShortcutsNotification(IReadOnlyList shortcuts) "UniGetUI has detected {0} new desktop shortcuts that can be deleted automatically.", shortcuts.Count); } - DeliverNotification(title, message); + DeliverNotification(title, message, MainWindow.RuntimeNotificationLevel.Success); } catch (Exception ex) { @@ -190,8 +191,20 @@ public static void ShowNewShortcutsNotification(IReadOnlyList shortcuts) // ── Core delivery ────────────────────────────────────────────────────── - private static void DeliverNotification(string title, string message) + private static void DeliverNotification( + string title, + string message, + MainWindow.RuntimeNotificationLevel level, + bool allowInAppFallback = true) { + if (MainWindow.IsWindowOnScreen) + { + if (allowInAppFallback) + Dispatcher.UIThread.Post(() => + MainWindow.Instance?.ShowRuntimeNotification(title, message, level)); + return; + } + if (!EnsureInitialized()) throw new InvalidOperationException("The macOS notification bridge is unavailable."); diff --git a/src/UniGetUI.Avalonia/Infrastructure/WindowsAppNotificationBridge.cs b/src/UniGetUI.Avalonia/Infrastructure/WindowsAppNotificationBridge.cs index 288fa36640..1ea1c81a23 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/WindowsAppNotificationBridge.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/WindowsAppNotificationBridge.cs @@ -270,7 +270,10 @@ private static bool Show( bool allowInAppFallback = true) { #if WINDOWS - if (OperatingSystem.IsWindows() && Win32ToastNotifier.IsAvailable()) + // Only fire an OS toast when the app is out of sight (minimized or hidden to the tray). + // While the window is on screen the user can already see the operations panel / in-app + // toast, so a native toast would just be noise; fall through to the in-app path instead. + if (OperatingSystem.IsWindows() && !MainWindow.IsWindowOnScreen && Win32ToastNotifier.IsAvailable()) { string launchArg = BuildLaunchArgument(launchAction); if (Win32ToastNotifier.Show(title, message, launchArg)) diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index cb0387360d..18912fea42 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -113,6 +113,11 @@ public enum RuntimeNotificationLevel public static MainWindow? Instance { get; private set; } + // True while the window is on screen (visible and not minimized). Read from any thread by + // the notification bridge to suppress OS toasts while the user is looking at the app. + private static volatile bool _isOnScreen; + public static bool IsWindowOnScreen => _isOnScreen; + private MainWindowViewModel ViewModel => (MainWindowViewModel)DataContext!; public PageType CurrentPage => ViewModel.CurrentPage_t; @@ -136,7 +141,8 @@ public MainWindow() Resized += (_, _) => _ = SaveGeometryAsync(); PositionChanged += (_, _) => _ = SaveGeometryAsync(); - this.GetObservable(WindowStateProperty).SubscribeValue(state => { _ = SaveGeometryAsync(); }); + this.GetObservable(WindowStateProperty).SubscribeValue(state => { UpdateOnScreenState(); _ = SaveGeometryAsync(); }); + this.GetObservable(IsVisibleProperty).SubscribeValue(_ => UpdateOnScreenState()); _trayService = new TrayService(this); _trayService.UpdateStatus(); @@ -1362,6 +1368,9 @@ private void Toast_PointerExited(object? sender, PointerEventArgs e) public void UpdateSystemTrayStatus() => _trayService?.UpdateStatus(); + private void UpdateOnScreenState() + => _isOnScreen = IsVisible && WindowState != WindowState.Minimized; + public void ShowRuntimeNotification(string title, string message, RuntimeNotificationLevel level) => ShowBanner(title, message, level);