diff --git a/src/Languages/lang_en.json b/src/Languages/lang_en.json
index a6f885972..232ab1df4 100644
--- a/src/Languages/lang_en.json
+++ b/src/Languages/lang_en.json
@@ -850,5 +850,9 @@
"Log in failed: ": "Log in failed: ",
"Log out failed: ": "Log out failed: ",
"Package backup settings": "Package backup settings",
- "Show the release notes after UniGetUI is updated": "Show the release notes after UniGetUI is updated"
+ "Show the release notes after UniGetUI is updated": "Show the release notes after UniGetUI is updated",
+ "Operations completed": "Operations completed",
+ "Operations finished with errors": "Operations finished with errors",
+ "{0} of {1} succeeded, {2} failed": "{0} of {1} succeeded, {2} failed",
+ "Show an in-app summary toast when a batch of operations finishes": "Show an in-app summary toast when a batch of operations finishes"
}
diff --git a/src/SharedAssets/Assets/Symbols/success_round.svg b/src/SharedAssets/Assets/Symbols/success_round.svg
new file mode 100644
index 000000000..1b60f794d
--- /dev/null
+++ b/src/SharedAssets/Assets/Symbols/success_round.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml
index c4835b2d4..1582038c8 100644
--- a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml
+++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml
@@ -85,6 +85,9 @@
+
+
+ 0 4 16 0 #33000000
@@ -164,6 +167,9 @@
+
+
+ 0 4 18 0 #80000000
diff --git a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs
index ae2452a0c..efd86355c 100644
--- a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs
+++ b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs
@@ -196,19 +196,8 @@ private static void ShowOperationProgressNotification(AbstractOperation op)
$"{title}. {message}",
AutomationLiveSetting.Polite);
- if (OperatingSystem.IsWindows() && WindowsAppNotificationBridge.ShowProgress(op))
- return;
-
- if (OperatingSystem.IsMacOS() && MacOsNotificationBridge.ShowProgress(op))
- return;
-
- if (TryGetMainWindow() is not { } mainWindow)
- return;
-
- mainWindow.ShowRuntimeNotification(
- title,
- message,
- UniGetUI.Avalonia.Views.MainWindow.RuntimeNotificationLevel.Progress);
+ if (OperatingSystem.IsWindows()) WindowsAppNotificationBridge.ShowProgress(op);
+ else if (OperatingSystem.IsMacOS()) MacOsNotificationBridge.ShowProgress(op);
}
private static void ShowOperationSuccessNotification(AbstractOperation op)
@@ -230,11 +219,8 @@ private static void ShowOperationSuccessNotification(AbstractOperation op)
WindowsAppNotificationBridge.RemoveProgress(op);
- if (OperatingSystem.IsWindows() && WindowsAppNotificationBridge.ShowSuccess(op))
- return;
-
- if (OperatingSystem.IsMacOS() && MacOsNotificationBridge.ShowSuccess(op))
- return;
+ if (OperatingSystem.IsWindows()) WindowsAppNotificationBridge.ShowSuccess(op);
+ else if (OperatingSystem.IsMacOS()) MacOsNotificationBridge.ShowSuccess(op);
}
private static void ShowOperationFailureNotification(AbstractOperation op)
@@ -256,27 +242,8 @@ private static void ShowOperationFailureNotification(AbstractOperation op)
WindowsAppNotificationBridge.RemoveProgress(op);
- if (OperatingSystem.IsWindows() && WindowsAppNotificationBridge.ShowError(op))
- return;
-
- if (OperatingSystem.IsMacOS() && MacOsNotificationBridge.ShowError(op))
- return;
-
- if (TryGetMainWindow() is not { } mainWindow)
- return;
-
- mainWindow.ShowRuntimeNotification(
- title,
- message,
- UniGetUI.Avalonia.Views.MainWindow.RuntimeNotificationLevel.Error);
- }
-
- private static UniGetUI.Avalonia.Views.MainWindow? TryGetMainWindow()
- {
- return Application.Current?.ApplicationLifetime
- is IClassicDesktopStyleApplicationLifetime { MainWindow: UniGetUI.Avalonia.Views.MainWindow mw }
- ? mw
- : null;
+ if (OperatingSystem.IsWindows()) WindowsAppNotificationBridge.ShowError(op);
+ else if (OperatingSystem.IsMacOS()) MacOsNotificationBridge.ShowError(op);
}
private static void AppendOperationHistory(AbstractOperation op)
diff --git a/src/UniGetUI.Avalonia/Infrastructure/WindowsAppNotificationBridge.cs b/src/UniGetUI.Avalonia/Infrastructure/WindowsAppNotificationBridge.cs
index fb3fe991d..9bfe70a95 100644
--- a/src/UniGetUI.Avalonia/Infrastructure/WindowsAppNotificationBridge.cs
+++ b/src/UniGetUI.Avalonia/Infrastructure/WindowsAppNotificationBridge.cs
@@ -18,9 +18,10 @@ namespace UniGetUI.Avalonia.Infrastructure;
/// single-instance handler foregrounds the window. The inline action buttons carried
/// by the old WinRT toasts are intentionally dropped; activation is surfaced via
/// only when the app is already running and the toast
-/// is clicked, so the main-window view model keeps its handler. On non-Windows, or if
-/// the toast COM surface is unavailable, every call falls back to the in-app Avalonia
-/// banner via .
+/// is clicked, so the main-window view model keeps its handler. When the toast COM surface
+/// is unavailable, feature notifications fall back to the in-app Avalonia banner via
+/// ; per-operation results do not (they
+/// already appear in the operations panel — see the allowInAppFallback flag on Show).
///
internal static class WindowsAppNotificationBridge
{
@@ -64,7 +65,7 @@ public static bool ShowProgress(AbstractOperation operation)
? operation.Metadata.Status
: CoreTools.Translate("Please wait...");
- return Show(title, message, MainWindow.RuntimeNotificationLevel.Progress, launchAction: NotificationArguments.Show);
+ return Show(title, message, MainWindow.RuntimeNotificationLevel.Progress, launchAction: NotificationArguments.Show, allowInAppFallback: false);
}
public static bool ShowSuccess(AbstractOperation operation)
@@ -77,7 +78,7 @@ public static bool ShowSuccess(AbstractOperation operation)
? operation.Metadata.SuccessMessage
: CoreTools.Translate("Success!");
- return Show(title, message, MainWindow.RuntimeNotificationLevel.Success, launchAction: NotificationArguments.Show);
+ return Show(title, message, MainWindow.RuntimeNotificationLevel.Success, launchAction: NotificationArguments.Show, allowInAppFallback: false);
}
public static bool ShowError(AbstractOperation operation)
@@ -90,7 +91,7 @@ public static bool ShowError(AbstractOperation operation)
? operation.Metadata.FailureMessage
: CoreTools.Translate("An error occurred while processing this package");
- return Show(title, message, MainWindow.RuntimeNotificationLevel.Error, launchAction: NotificationArguments.Show);
+ return Show(title, message, MainWindow.RuntimeNotificationLevel.Error, launchAction: NotificationArguments.Show, allowInAppFallback: false);
}
public static void RemoveProgress(AbstractOperation operation)
@@ -258,12 +259,15 @@ public static void ShowNewShortcutsNotification(IReadOnlyList shortcuts)
/// argument so the single-instance handler can route the activation when the toast
/// is clicked. Returns true once the notification has been surfaced (toast or
/// banner), so callers skip their own fallback banner and avoid double-showing.
+ /// is false for per-operation results:
+ /// those already live in the operations panel, so we don't duplicate them as a toast.
///
private static bool Show(
string title,
string message,
MainWindow.RuntimeNotificationLevel level,
- string launchAction)
+ string launchAction,
+ bool allowInAppFallback = true)
{
#if WINDOWS
if (OperatingSystem.IsWindows() && Win32ToastNotifier.IsAvailable())
@@ -281,7 +285,7 @@ private static bool Show(
}
#endif
- return ShowInAppBanner(title, message, level);
+ return allowInAppFallback && ShowInAppBanner(title, message, level);
}
private static bool ShowInAppBanner(
diff --git a/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs
index 6adf89033..93a70c373 100644
--- a/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs
+++ b/src/UniGetUI.Avalonia/ViewModels/MainWindowViewModel.cs
@@ -1,3 +1,4 @@
+using System.Collections.ObjectModel;
using System.Collections.Specialized;
using Avalonia;
using Avalonia.Automation;
@@ -63,12 +64,24 @@ public partial class MainWindowViewModel : ViewModelBase
public AvaloniaList Operations => AvaloniaOperationRegistry.OperationViewModels;
[ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(ShowFailedOperationBadge))]
private bool _operationsPanelVisible;
[ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(ShowFailedOperationBadge))]
private bool _operationsPanelExpanded = true;
private readonly List _operationBatch = new();
+ private bool _batchSummaryShown;
+
+ public bool HasFailedOperations => Operations.Any(o => o.Operation.Status is OperationStatus.Failed);
+
+ public OperationViewModel? FirstFailedOperation =>
+ Operations.FirstOrDefault(o => o.Operation.Status is OperationStatus.Failed);
+
+ // Red badge on the chevron when the panel is collapsed and an operation failed
+ // (failures no longer pop a toast; expanded, the failure is already visible).
+ public bool ShowFailedOperationBadge => OperationsPanelVisible && !OperationsPanelExpanded && HasFailedOperations;
public string OperationsHeaderText
{
@@ -91,6 +104,7 @@ private void AddToBatch(AbstractOperation op)
foreach (var old in _operationBatch)
old.StatusChanged -= OnOperationStatusChanged;
_operationBatch.Clear();
+ _batchSummaryShown = false;
}
if (!_operationBatch.Contains(op))
@@ -101,7 +115,42 @@ private void AddToBatch(AbstractOperation op)
}
private void OnOperationStatusChanged(object? sender, OperationStatus e)
- => Dispatcher.UIThread.Post(() => OnPropertyChanged(nameof(OperationsHeaderText)));
+ => Dispatcher.UIThread.Post(() =>
+ {
+ OnPropertyChanged(nameof(OperationsHeaderText));
+ OnPropertyChanged(nameof(HasFailedOperations));
+ OnPropertyChanged(nameof(ShowFailedOperationBadge));
+ MaybeShowBatchSummaryToast();
+ });
+
+ // Opt-in: one toast summarizing the batch once every operation in it has finished.
+ private void MaybeShowBatchSummaryToast()
+ {
+ if (_batchSummaryShown || _operationBatch.Count == 0) return;
+ if (!Settings.Get(Settings.K.ShowOperationSummaryNotifications)) return;
+ if (_operationBatch.Any(o => o.Status is OperationStatus.InQueue or OperationStatus.Running)) return;
+
+ _batchSummaryShown = true;
+
+ int total = _operationBatch.Count;
+ int failed = _operationBatch.Count(o => o.Status is OperationStatus.Failed);
+ int succeeded = _operationBatch.Count(o => o.Status is OperationStatus.Succeeded);
+
+ var toast = new InfoBarViewModel
+ {
+ Title = failed > 0
+ ? CoreTools.Translate("Operations finished with errors")
+ : CoreTools.Translate("Operations completed"),
+ Message = failed > 0
+ ? CoreTools.Translate("{0} of {1} succeeded, {2} failed", succeeded, total, failed)
+ : CoreTools.Translate("{0} of {1} operations completed", succeeded, total),
+ Severity = failed > 0 ? InfoBarSeverity.Error : InfoBarSeverity.Success,
+ IsClosable = true,
+ IsOpen = true,
+ };
+ toast.OnClosed = () => DismissToast(toast);
+ ShowToast(toast);
+ }
[RelayCommand]
private void ToggleOperationsPanel() => OperationsPanelExpanded = !OperationsPanelExpanded;
@@ -169,12 +218,126 @@ private void OnPageViewModelPropertyChanged(object? sender, System.ComponentMode
? $"UniGetUI {CoreTools.Translate("version {0}", CoreData.VersionName)}"
: "UniGetUI";
- // ─── Banners ─────────────────────────────────────────────────────────────
+ // ─── Notifications (bottom-right toasts) ───────────────────────────────────
+ // Persistent banners kept as singletons; RegisterBannerToast mirrors their IsOpen into Toasts.
public InfoBarViewModel UpdatesBanner { get; } = new() { Severity = InfoBarSeverity.Success };
- public InfoBarViewModel ErrorBanner { get; } = new() { Severity = InfoBarSeverity.Error };
public InfoBarViewModel WinGetWarningBanner { get; } = new() { Severity = InfoBarSeverity.Warning };
public InfoBarViewModel TelemetryWarner { get; } = new() { Severity = InfoBarSeverity.Informational };
+ // Oldest first (rendered bottom-up so the newest sits nearest the corner).
+ public ObservableCollection Toasts { get; } = new();
+ private readonly Dictionary _toastTimers = new();
+ private const int MaxVisibleToasts = 5;
+
+ // A toast with an action button stays until acted on; everything else auto-dismisses.
+ private static bool IsSticky(InfoBarViewModel t) => !string.IsNullOrEmpty(t.ActionButtonText);
+ private static TimeSpan ToastDuration(InfoBarViewModel t)
+ => t.Severity is InfoBarSeverity.Error or InfoBarSeverity.Warning
+ ? TimeSpan.FromSeconds(8)
+ : TimeSpan.FromSeconds(5);
+
+ public void ShowToast(InfoBarViewModel toast)
+ {
+ if (!Dispatcher.UIThread.CheckAccess())
+ {
+ Dispatcher.UIThread.Post(() => ShowToast(toast));
+ return;
+ }
+ if (!Toasts.Contains(toast))
+ {
+ Toasts.Add(toast);
+ AnnounceToast(toast);
+ }
+ TrimToastStack(keep: toast);
+ ArmToastTimer(toast);
+ }
+
+ // Keep the stack from overflowing the screen: drop the oldest auto-dismissing toasts,
+ // but never a sticky (action-button) one or the toast just shown.
+ private void TrimToastStack(InfoBarViewModel keep)
+ {
+ while (Toasts.Count > MaxVisibleToasts)
+ {
+ var oldest = Toasts.FirstOrDefault(t => t != keep && !IsSticky(t));
+ if (oldest is null) break;
+ DismissToast(oldest);
+ }
+ }
+
+ // Surface the toast to screen readers via the live region (assertive for errors so it
+ // interrupts, polite otherwise) — the toast's own automation name isn't announced on its own.
+ private static void AnnounceToast(InfoBarViewModel toast)
+ {
+ string message = string.IsNullOrEmpty(toast.Message)
+ ? toast.Title
+ : $"{toast.Title}. {toast.Message}";
+ AccessibilityAnnouncementService.Announce(
+ message,
+ toast.Severity is InfoBarSeverity.Error
+ ? AutomationLiveSetting.Assertive
+ : AutomationLiveSetting.Polite);
+ }
+
+ public void DismissToast(InfoBarViewModel toast)
+ {
+ if (!Dispatcher.UIThread.CheckAccess())
+ {
+ Dispatcher.UIThread.Post(() => DismissToast(toast));
+ return;
+ }
+ DisposeToastTimer(toast);
+ Toasts.Remove(toast);
+ }
+
+ // Pause the auto-dismiss countdown while the pointer hovers a toast; restart it on leave.
+ public void PauseToast(InfoBarViewModel toast)
+ {
+ if (_toastTimers.TryGetValue(toast, out var timer)) timer.Stop();
+ }
+
+ public void ResumeToast(InfoBarViewModel toast)
+ {
+ if (_toastTimers.TryGetValue(toast, out var timer)) { timer.Stop(); timer.Start(); }
+ }
+
+ private void ArmToastTimer(InfoBarViewModel toast)
+ {
+ DisposeToastTimer(toast);
+ if (IsSticky(toast))
+ return;
+
+ var timer = new DispatcherTimer { Interval = ToastDuration(toast) };
+ timer.Tick += (_, _) => toast.IsOpen = false;
+ _toastTimers[toast] = timer;
+ timer.Start();
+ }
+
+ private void DisposeToastTimer(InfoBarViewModel toast)
+ {
+ if (_toastTimers.Remove(toast, out var timer))
+ timer.Stop();
+ }
+
+ // Mirrors a persistent banner's IsOpen into Toasts so code that just flips IsOpen keeps working.
+ private void RegisterBannerToast(InfoBarViewModel banner)
+ {
+ banner.PropertyChanged += (_, e) =>
+ {
+ if (e.PropertyName == nameof(InfoBarViewModel.IsOpen))
+ {
+ if (banner.IsOpen) ShowToast(banner);
+ else DismissToast(banner);
+ }
+ // Content can change in place while the banner stays open (the updater cycles
+ // statuses on the same banner, finally adding the "Update now" action); re-arm so
+ // stickiness/countdown track the new content instead of a stale timer dismissing it.
+ else if (banner.IsOpen && Toasts.Contains(banner))
+ {
+ ArmToastTimer(banner);
+ }
+ };
+ }
+
// ─── Constructor ─────────────────────────────────────────────────────────
[RelayCommand]
private void ToggleSidebar() => Sidebar.IsPaneOpen = !Sidebar.IsPaneOpen;
@@ -183,6 +346,11 @@ public MainWindowViewModel()
{
AccessibilityAnnouncementService.AnnouncementRequested += OnAnnouncementRequested;
+ // Wire before the blocks below flip the banners' IsOpen flags.
+ RegisterBannerToast(UpdatesBanner);
+ RegisterBannerToast(WinGetWarningBanner);
+ RegisterBannerToast(TelemetryWarner);
+
DiscoverPage = new DiscoverSoftwarePage();
UpdatesPage = new SoftwareUpdatesPage();
InstalledPage = new InstalledPackagesPage();
@@ -264,6 +432,8 @@ public MainWindowViewModel()
AddToBatch(vm.Operation);
OperationsPanelVisible = Operations.Count > 0;
OnPropertyChanged(nameof(OperationsHeaderText));
+ OnPropertyChanged(nameof(HasFailedOperations));
+ OnPropertyChanged(nameof(ShowFailedOperationBadge));
};
if (OperatingSystem.IsWindows() && CoreTools.IsAdministrator() && !Settings.Get(Settings.K.AlreadyWarnedAboutAdmin))
diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/NotificationsViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/NotificationsViewModel.cs
index 4375b1d55..76dc13883 100644
--- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/NotificationsViewModel.cs
+++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/NotificationsViewModel.cs
@@ -13,6 +13,11 @@ public partial class NotificationsViewModel : ViewModelBase
/// True when the system-tray-disabled warning should be shown.
public bool IsSystemTrayWarningVisible => !IsSystemTrayEnabled;
+ // Per-type notifications are delivered only by the Windows/macOS bridges; Linux has no
+ // delivery path, so the whole "Notification types" group is hidden there.
+ public bool AreNotificationTypesSupported { get; } =
+ OperatingSystem.IsWindows() || OperatingSystem.IsMacOS();
+
public NotificationsViewModel()
{
_isSystemTrayEnabled = !CoreSettings.Get(CoreSettings.K.DisableSystemTray);
diff --git a/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml b/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml
index e068b8328..1ae4be569 100644
--- a/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml
+++ b/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml
@@ -23,6 +23,23 @@
+
+
-
+
-
+ VerticalAlignment="Top">
+
@@ -83,8 +100,8 @@
IsVisible="{Binding IsClosable}"
Command="{Binding CloseCommand}"
automation:AutomationProperties.Name="Close"
- VerticalAlignment="Center"
- Margin="4,0,8,0"
+ VerticalAlignment="Top"
+ Margin="4,6,6,0"
Padding="6"
Background="Transparent"
BorderThickness="0"
diff --git a/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs b/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs
index 6602b1996..9191e6dab 100644
--- a/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs
@@ -1,22 +1,28 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
+using UniGetUI.Avalonia.Infrastructure;
using UniGetUI.Avalonia.ViewModels;
namespace UniGetUI.Avalonia.Views.Controls;
public partial class InfoBar : UserControl
{
- // Icon path data for each severity
- private const string InfoPath = "M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2Zm1,15H11V11h2Zm0-8H11V7h2Z";
- private const string WarningPath = "M12,2,1,21H23Zm1,14H11V14h2Zm0-4H11V9h2Z";
- private const string ErrorPath = "M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2Zm1,13H11V13h2Zm0-6H11V7h2Z";
- private const string SuccessPath = "M12,2A10,10,0,1,0,22,12,10,10,0,0,0,12,2ZM10,17,5,12l1.41-1.41L10,14.17l7.59-7.59L19,8Z";
+ // Severity glyphs reuse the app's shared round symbols (same SvgIcon-by-path convention
+ // as the rest of the app), tinted with the severity colour.
+ private const string InfoIcon = "avares://UniGetUI/Assets/Symbols/info_round.svg";
+ private const string WarningIcon = "avares://UniGetUI/Assets/Symbols/warning_round.svg";
+ private const string ErrorIcon = "avares://UniGetUI/Assets/Symbols/close_round.svg";
+ private const string SuccessIcon = "avares://UniGetUI/Assets/Symbols/success_round.svg";
public InfoBar()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
+
+ // Play the slide-in entrance only when the OS isn't set to minimize motion.
+ if (!MotionPreference.ReducedMotion)
+ BodyBorder.Classes.Add("animate-in");
}
private InfoBarViewModel? _vm;
@@ -57,16 +63,14 @@ private void ApplySeverity(InfoBarSeverity severity)
};
SeverityStrip.Background = new SolidColorBrush(stripColor);
- // Icon shape
- SeverityIcon.Data = Geometry.Parse(severity switch
+ // Icon (shared SVG asset, tinted with the severity colour)
+ SeverityIcon.Path = severity switch
{
- InfoBarSeverity.Warning => WarningPath,
- InfoBarSeverity.Error => ErrorPath,
- InfoBarSeverity.Success => SuccessPath,
- _ => InfoPath,
- });
-
- // Icon foreground (solid, not theme-sensitive)
+ InfoBarSeverity.Warning => WarningIcon,
+ InfoBarSeverity.Error => ErrorIcon,
+ InfoBarSeverity.Success => SuccessIcon,
+ _ => InfoIcon,
+ };
SeverityIcon.Foreground = new SolidColorBrush(stripColor);
}
}
diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml b/src/UniGetUI.Avalonia/Views/MainWindow.axaml
index 137c60fc9..a3c019a99 100644
--- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml
+++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml
@@ -104,33 +104,19 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
@@ -210,7 +204,8 @@
IsVisible="{Binding OperationsPanelExpanded}"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
-
@@ -602,5 +597,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
index 8d80005f7..c13f1b5cf 100644
--- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
@@ -281,6 +281,25 @@ private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.Pr
if (e.PropertyName is nameof(MainWindowViewModel.OperationsPanelExpanded)
or nameof(MainWindowViewModel.OperationsPanelVisible))
UpdateOperationsPanelRow();
+
+ // Expanding the panel while an operation has failed jumps to that op (the failure
+ // badge on the chevron is what drew the user here).
+ if (e.PropertyName is nameof(MainWindowViewModel.OperationsPanelExpanded)
+ && ViewModel.OperationsPanelExpanded)
+ ScrollToFirstFailedOperation();
+ }
+
+ private void ScrollToFirstFailedOperation()
+ {
+ if (ViewModel.FirstFailedOperation is not { } failed)
+ return;
+
+ // Defer until the just-shown list has laid out its containers.
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (OperationsList.ContainerFromItem(failed) is Control container)
+ container.BringIntoView();
+ }, DispatcherPriority.Background);
}
// Drive the operations-panel grid row: a resizable pixel height while the panel is
@@ -1254,6 +1273,7 @@ public void FocusGlobalSearch(string prefill = "")
}
// ─── Public API (legacy compat) ───────────────────────────────────────────
+ // Surfaces a fresh, auto-dismissing toast per call (name kept for pre-toast callers).
public void ShowBanner(string title, string message, RuntimeNotificationLevel level)
{
if (level == RuntimeNotificationLevel.Progress) return;
@@ -1264,12 +1284,30 @@ public void ShowBanner(string title, string message, RuntimeNotificationLevel le
RuntimeNotificationLevel.Success => InfoBarSeverity.Success,
_ => InfoBarSeverity.Informational,
};
- ViewModel.ErrorBanner.ActionButtonText = "";
- ViewModel.ErrorBanner.ActionButtonCommand = null;
- ViewModel.ErrorBanner.Title = title;
- ViewModel.ErrorBanner.Message = message;
- ViewModel.ErrorBanner.Severity = severity;
- ViewModel.ErrorBanner.IsOpen = true;
+
+ var toast = new InfoBarViewModel
+ {
+ Title = title,
+ Message = message,
+ Severity = severity,
+ IsClosable = true,
+ IsOpen = true,
+ };
+ toast.OnClosed = () => ViewModel.DismissToast(toast);
+ ViewModel.ShowToast(toast);
+ }
+
+ // Pause/resume a toast's auto-dismiss countdown while the pointer is over it.
+ private void Toast_PointerEntered(object? sender, PointerEventArgs e)
+ {
+ if ((sender as Control)?.DataContext is InfoBarViewModel vm)
+ ViewModel.PauseToast(vm);
+ }
+
+ private void Toast_PointerExited(object? sender, PointerEventArgs e)
+ {
+ if ((sender as Control)?.DataContext is InfoBarViewModel vm)
+ ViewModel.ResumeToast(vm);
}
public void UpdateSystemTrayStatus() => _trayService?.UpdateStatus();
diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml
index 5bd97e2ac..86d75283c 100644
--- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml
+++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/Notifications.axaml
@@ -30,36 +30,46 @@
-
+
+
-
+
+
-
+
-
+
-
+
+
+
+
+
+
diff --git a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs
index ecdf77673..3e8e4c365 100644
--- a/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs
+++ b/src/UniGetUI.Core.Settings/SettingsEngine_Names.cs
@@ -77,6 +77,7 @@ public enum K
DisableErrorNotifications,
DisableSuccessNotifications,
DisableProgressNotifications,
+ ShowOperationSummaryNotifications,
KillProcessesThatRefuseToDie,
ManagerPaths,
GitHubUserLogin,
@@ -187,6 +188,7 @@ public static string ResolveKey(K key)
K.DisableErrorNotifications => "DisableErrorNotifications",
K.DisableSuccessNotifications => "DisableSuccessNotifications",
K.DisableProgressNotifications => "DisableProgressNotifications",
+ K.ShowOperationSummaryNotifications => "ShowOperationSummaryNotifications",
K.KillProcessesThatRefuseToDie => "KillProcessesThatRefuseToDie",
K.ManagerPaths => "ManagerPaths",
K.GitHubUserLogin => "GitHubUserLogin",