From 176c869bb9dcb3b5d0f5db983547a0eeabf5a844 Mon Sep 17 00:00:00 2001 From: Meteoni-San <141850520+Meteony@users.noreply.github.com> Date: Sat, 25 Jul 2026 12:20:40 +0000 Subject: [PATCH 01/26] Migrate legacy dialogs to WinUI-style immersive overlays --- .../Assets/Styles/Styles.Common.axaml | 52 ++- .../Assets/Styles/Styles.WindowsMica.axaml | 4 +- .../Infrastructure/MicaWindowHelper.cs | 44 ++- .../Infrastructure/TrayService.cs | 2 +- .../UninstallConfirmationDialog.cs | 11 +- .../ManageIgnoredUpdatesViewModel.cs | 2 - .../Pages/SettingsPages/BackupViewModel.cs | 8 +- .../ViewModels/SidebarViewModel.cs | 2 +- .../SoftwarePages/PackagesPageViewModel.cs | 9 +- .../Views/Controls/InfoBar.axaml.cs | 33 +- .../Views/DialogPages/AboutWindow.axaml | 16 +- .../Views/DialogPages/AboutWindow.axaml.cs | 3 +- .../BundleSecurityReportDialog.axaml | 14 +- .../BundleSecurityReportDialog.axaml.cs | 3 +- .../Views/DialogPages/CrashReportWindow.axaml | 16 +- .../DialogPages/CrashReportWindow.axaml.cs | 3 +- .../DiscardBundleChangesDialog.axaml | 24 +- .../DiscardBundleChangesDialog.axaml.cs | 3 +- .../Views/DialogPages/ImmersiveDialog.cs | 71 +++++ .../DialogPages/InstallOptionsWindow.axaml | 35 +- .../DialogPages/InstallOptionsWindow.axaml.cs | 15 +- .../IntegrityViolationDialog.axaml | 26 +- .../IntegrityViolationDialog.axaml.cs | 3 +- .../ManageDesktopShortcutsWindow.axaml | 34 +- .../ManageDesktopShortcutsWindow.axaml.cs | 3 +- .../ManageIgnoredUpdatesWindow.axaml | 36 +-- .../ManageIgnoredUpdatesWindow.axaml.cs | 67 +--- .../DialogPages/MissingDependencyDialog.axaml | 27 +- .../MissingDependencyDialog.axaml.cs | 4 +- .../DialogPages/OperationFailedDialog.axaml | 16 +- .../OperationFailedDialog.axaml.cs | 3 +- .../DialogPages/OperationOutputWindow.axaml | 14 +- .../OperationOutputWindow.axaml.cs | 3 +- .../DialogPages/PackageDetailsWindow.axaml | 25 +- .../DialogPages/PackageDetailsWindow.axaml.cs | 3 +- .../DialogPages/ResetSettingsDialog.axaml | 24 +- .../DialogPages/ResetSettingsDialog.axaml.cs | 3 +- .../Views/DialogPages/SimpleErrorDialog.axaml | 27 +- .../DialogPages/SimpleErrorDialog.axaml.cs | 4 +- .../Views/DialogPages/TelemetryDialog.axaml | 29 +- .../DialogPages/TelemetryDialog.axaml.cs | 3 +- src/UniGetUI.Avalonia/Views/MainWindow.axaml | 122 +++++-- .../Views/MainWindow.axaml.cs | 300 ++++++++++++++++-- src/UniGetUI.Avalonia/Views/SidebarView.axaml | 3 +- .../Views/SidebarView.axaml.cs | 17 +- .../SoftwarePages/AbstractPackagesPage.axaml | 1 + .../AbstractPackagesPage.axaml.cs | 2 +- 47 files changed, 768 insertions(+), 401 deletions(-) create mode 100644 src/UniGetUI.Avalonia/Views/DialogPages/ImmersiveDialog.cs diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml index 1582038c82..33791054d0 100644 --- a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml @@ -33,6 +33,10 @@ + + + + @@ -64,6 +68,8 @@ + + @@ -76,7 +82,7 @@ - + @@ -115,6 +121,10 @@ + + + + @@ -146,6 +156,8 @@ + + @@ -187,6 +199,44 @@ + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml index 073cc8779e..6e6fd9d9be 100644 --- a/src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.WindowsMica.axaml @@ -30,7 +30,7 @@ - + @@ -71,7 +71,7 @@ stays readably dark even when the popup floats over bright content (e.g. the release-notes WebView) — a lighter tint let the white page wash the menu out (#5007) --> - + diff --git a/src/UniGetUI.Avalonia/Infrastructure/MicaWindowHelper.cs b/src/UniGetUI.Avalonia/Infrastructure/MicaWindowHelper.cs index 17b65453a5..5574de6671 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/MicaWindowHelper.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/MicaWindowHelper.cs @@ -5,6 +5,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Media; using Avalonia.Platform; +using Avalonia.VisualTree; using UniGetUI.Avalonia.Assets.Styles; namespace UniGetUI.Avalonia.Infrastructure; @@ -48,22 +49,22 @@ public static void Apply(Window window) }; } - // Gives flyouts / menus / combo dropdowns / tooltips a native Win11 acrylic backdrop: - // the popup window is made transparent and DWM paints the (blurred, theme-adaptive) - // acrylic material behind it. Registered once at startup when Mica is enabled. + // Applies the Windows 11 transient-surface treatment to popup hosts. Menus use an + // opaque WinUI surface; combo dropdowns, tooltips, and ordinary flyouts use acrylic. + // Registered once at startup when Mica is enabled. public static void EnableAcrylicPopups() { if (!IsMicaEnabled() || _acrylicPopupsHooked) return; _acrylicPopupsHooked = true; - // In-app flyouts/menus/tooltips/combo popups are hosted in a PopupRoot; style each as it loads. + // In-app menus, flyouts, tooltips, and combo popups are hosted in a PopupRoot. Control.LoadedEvent.AddClassHandler((root, _) => ApplyAcrylicToPopup(root)); // The system-tray context menu is NOT a PopupRoot — Avalonia hosts it in its own Window // (Avalonia.Win32.TrayIconImpl.TrayPopupRoot), so it misses the handler above and would - // render with no backdrop (transparent over the desktop). Catch it by type name and apply - // the same acrylic treatment. The other Windows (MainWindow/dialogs) are handled via Apply(). + // render with no surface over the desktop. Catch it by type name and apply the opaque + // menu treatment. The other Windows (MainWindow/dialogs) are handled via Apply(). Control.LoadedEvent.AddClassHandler((win, _) => { if (win.GetType().Name == "TrayPopupRoot") @@ -76,10 +77,28 @@ private static void ApplyAcrylicToPopup(TopLevel root) if (!IsMicaEnabled()) return; + // Menus use WinUI's opaque flyout surface. Other transient controls (combo boxes, + // tooltips and ordinary flyouts) retain acrylic. + bool isMenu = root.GetType().Name == "TrayPopupRoot" + || root.GetVisualDescendants() + .Any(control => control is MenuFlyoutPresenter or ContextMenu); + if (isMenu) + { + root.TransparencyLevelHint = new[] { WindowTransparencyLevel.None }; + if (root.TryFindResource("MenuSurfaceBrush", root.ActualThemeVariant, out object? resource) + && resource is IBrush brush) + { + root.Background = brush; + } + + ApplyRoundedCorners(root); + return; + } + // Request acrylic (not Transparent): the Transparent level makes a layered window, and DWM // system backdrops never paint on those — so the popup ended up fully see-through with only - // the presenter tint, unreadable when shown over the desktop (e.g. the tray menu). AcrylicBlur - // gives a composited window DWM can actually fill. This path only runs when Mica is enabled + // the presenter tint and make its contents unreadable. AcrylicBlur gives a composited window + // DWM can actually fill. This path only runs when Mica is enabled // (Win11 + transparency effects), so acrylic is always available here. root.TransparencyLevelHint = new[] { WindowTransparencyLevel.AcrylicBlur, WindowTransparencyLevel.Blur }; root.Background = Brushes.Transparent; @@ -93,6 +112,15 @@ private static void ApplyAcrylicToPopup(TopLevel root) NativeMethods.DwmSetWindowAttribute(handle, DWMWA_SYSTEMBACKDROP_TYPE, ref backdrop, sizeof(int)); } + private static void ApplyRoundedCorners(TopLevel root) + { + if (root.TryGetPlatformHandle()?.Handle is not { } handle || handle == 0) + return; + + int corner = DWMWCP_ROUND; + NativeMethods.DwmSetWindowAttribute(handle, DWMWA_WINDOW_CORNER_PREFERENCE, ref corner, sizeof(int)); + } + /// /// True when the native Mica look should be used: Windows 11+, "Transparency effects" on, /// GPU-accelerated (software rendering can't be transparent, so Mica shows as black — #5111), diff --git a/src/UniGetUI.Avalonia/Infrastructure/TrayService.cs b/src/UniGetUI.Avalonia/Infrastructure/TrayService.cs index c1311c2f12..c0ae4c04b8 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/TrayService.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/TrayService.cs @@ -136,7 +136,7 @@ private static NativeMenu BuildMenu(MainWindow owner) menu.Add(new NativeMenuItemSeparator()); menu.Add(new NativeMenuItem( - CoreTools.Translate("UniGetUI Version {0} by Devolutions", CoreData.VersionName)) + CoreTools.Translate("UniGetUI Version {0}", CoreData.VersionName)) { IsEnabled = false, }); diff --git a/src/UniGetUI.Avalonia/Infrastructure/UninstallConfirmationDialog.cs b/src/UniGetUI.Avalonia/Infrastructure/UninstallConfirmationDialog.cs index d414993d3c..47d89a17ea 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/UninstallConfirmationDialog.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/UninstallConfirmationDialog.cs @@ -22,15 +22,10 @@ public static async Task ConfirmAsync(Window owner, IReadOnlyList 1, - ShowInTaskbar = false, - WindowStartupLocation = WindowStartupLocation.CenterOwner, + MaxWidth = packages.Count == 1 ? 520 : 560, + MaxHeight = packages.Count == 1 ? 220 : 380, Title = CoreTools.Translate("Are you sure?"), }; diff --git a/src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs index 78ac769cc0..2157b83974 100644 --- a/src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/DialogPages/ManageIgnoredUpdatesViewModel.cs @@ -11,8 +11,6 @@ namespace UniGetUI.Avalonia.ViewModels; public partial class ManageIgnoredUpdatesViewModel : ObservableObject { - public event EventHandler? CloseRequested; - public string Title { get; } = CoreTools.Translate("Manage ignored updates"); public string Description { get; } = CoreTools.Translate("The packages listed here won't be taken in account when checking for updates. Double-click them or click the button on their right to stop ignoring their updates."); public string ResetLabel { get; } = CoreTools.Translate("Reset list"); diff --git a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs index d2a79a2fa2..939549c82c 100644 --- a/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/Pages/SettingsPages/BackupViewModel.cs @@ -347,13 +347,11 @@ private static async Task RestoreFromCloud() Width = 360, }; - var dialog = new Window + var dialog = new UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog { Title = CoreTools.Translate("Select backup"), - Width = 440, - Height = 160, - CanResize = false, - WindowStartupLocation = WindowStartupLocation.CenterOwner, + MaxWidth = 440, + MaxHeight = 160, Content = new StackPanel { Margin = new Thickness(20), diff --git a/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs index 69727307b7..80bc3b497b 100644 --- a/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs @@ -122,7 +122,7 @@ private void RaiseLayoutChanged() public event EventHandler? NavigationRequested; public string VersionLabel { get; } = - CoreTools.Translate("UniGetUI Version {0} by Devolutions", CoreData.VersionName); + CoreTools.Translate("UniGetUI Version {0}", CoreData.VersionName); [RelayCommand] public void RequestNavigation(string? pageName) diff --git a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs index 20a8d09fc4..3ce07b6a99 100644 --- a/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs +++ b/src/UniGetUI.Avalonia/ViewModels/SoftwarePages/PackagesPageViewModel.cs @@ -366,13 +366,10 @@ public async Task ShowInfoDialog(Window owner, string title, string message) { object? bgResource = null; Application.Current?.Resources.TryGetResource("AppWindowBackground", Application.Current.ActualThemeVariant, out bgResource); - var dialog = new Window + var dialog = new UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog { - Width = 460, - Height = 180, - CanResize = false, - ShowInTaskbar = false, - WindowStartupLocation = WindowStartupLocation.CenterOwner, + MaxWidth = 460, + MaxHeight = 180, Title = title, Background = bgResource as IBrush, }; diff --git a/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs b/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs index 9191e6dab5..1058a46f97 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/InfoBar.axaml.cs @@ -26,6 +26,8 @@ public InfoBar() } private InfoBarViewModel? _vm; + private IDisposable? _severityStripBinding; + private IDisposable? _severityIconBinding; private void OnDataContextChanged(object? sender, EventArgs e) { @@ -53,17 +55,19 @@ private void ApplySeverity(InfoBarSeverity severity) BodyBorder.Classes.Set("severity-warning", severity == InfoBarSeverity.Warning); BodyBorder.Classes.Set("severity-info", severity == InfoBarSeverity.Informational); - // Strip colour (solid, not theme-sensitive) - var stripColor = severity switch + _severityStripBinding?.Dispose(); + _severityIconBinding?.Dispose(); + _severityStripBinding = null; + _severityIconBinding = null; + + Color? stripColor = severity switch { InfoBarSeverity.Warning => Color.Parse("#F7A800"), InfoBarSeverity.Error => Color.Parse("#C42B1C"), InfoBarSeverity.Success => Color.Parse("#107C10"), - _ => Color.Parse("#0078D4"), + _ => null, }; - SeverityStrip.Background = new SolidColorBrush(stripColor); - // Icon (shared SVG asset, tinted with the severity colour) SeverityIcon.Path = severity switch { InfoBarSeverity.Warning => WarningIcon, @@ -71,6 +75,23 @@ private void ApplySeverity(InfoBarSeverity severity) InfoBarSeverity.Success => SuccessIcon, _ => InfoIcon, }; - SeverityIcon.Foreground = new SolidColorBrush(stripColor); + + if (stripColor is { } color) + { + var brush = new SolidColorBrush(color); + SeverityStrip.Background = brush; + SeverityIcon.Foreground = brush; + } + else + { + SeverityStrip.ClearValue(Border.BackgroundProperty); + SeverityIcon.ClearValue(ForegroundProperty); + _severityStripBinding = SeverityStrip.Bind( + Border.BackgroundProperty, + this.GetResourceObservable("SystemAccentColor")); + _severityIconBinding = SeverityIcon.Bind( + ForegroundProperty, + this.GetResourceObservable("SystemAccentColor")); + } } } diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml index 8e048a709b..03516c8311 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml @@ -1,15 +1,15 @@ - + MaxWidth="700" + MinWidth="500" + MaxHeight="600" + MinHeight="400" + Background="{DynamicResource AppDialogBackground}"> @@ -31,4 +31,4 @@ - + diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml.cs index 267b4388e5..aea2a62f97 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/AboutWindow.axaml.cs @@ -3,12 +3,11 @@ namespace UniGetUI.Avalonia.Views.DialogPages; -public partial class AboutWindow : Window +public partial class AboutWindow : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog { public AboutWindow() { InitializeComponent(); - UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); } protected override void OnOpened(EventArgs e) diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/BundleSecurityReportDialog.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/BundleSecurityReportDialog.axaml index 14b8221374..cf78b64f84 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/BundleSecurityReportDialog.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/BundleSecurityReportDialog.axaml @@ -1,17 +1,15 @@ - + Background="{DynamicResource AppDialogBackground}"> - + diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/BundleSecurityReportDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/BundleSecurityReportDialog.axaml.cs index f04a91f32c..5c01bdfc2e 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/BundleSecurityReportDialog.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/BundleSecurityReportDialog.axaml.cs @@ -4,12 +4,11 @@ namespace UniGetUI.Avalonia.Views.DialogPages; -public partial class BundleSecurityReportDialog : Window +public partial class BundleSecurityReportDialog : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog { public BundleSecurityReportDialog(BundleReport report) { InitializeComponent(); - UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); var sb = new System.Text.StringBuilder(); foreach (var (pkgId, entries) in report.Contents) diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml index 7993b0f2b9..e29a1e5bfb 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml @@ -1,14 +1,14 @@ - + MaxWidth="800" + MinWidth="500" + MaxHeight="580" + MinHeight="380" + Background="{DynamicResource AppDialogBackground}"> @@ -66,4 +66,4 @@ - + diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml.cs index 0be7336d52..732df5e28c 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/CrashReportWindow.axaml.cs @@ -8,7 +8,7 @@ namespace UniGetUI.Avalonia.Views.DialogPages; -internal sealed partial class CrashReportWindow : Window +internal sealed partial class CrashReportWindow : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog { private readonly string _crashReport; @@ -16,7 +16,6 @@ public CrashReportWindow(string crashReport) { _crashReport = crashReport; InitializeComponent(); - UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); CrashReportText.Text = crashReport; DontSendButton.Content = CoreTools.Translate("Don't Send"); SendButton.Content = CoreTools.Translate("Send Report"); diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/DiscardBundleChangesDialog.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/DiscardBundleChangesDialog.axaml index 81e8286486..8648171cf3 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/DiscardBundleChangesDialog.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/DiscardBundleChangesDialog.axaml @@ -1,33 +1,25 @@ - + + Background="{DynamicResource AppDialogBackground}"> - - - @@ -44,4 +36,4 @@ - + diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/DiscardBundleChangesDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/DiscardBundleChangesDialog.axaml.cs index d156fdca58..1f1e1cc868 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/DiscardBundleChangesDialog.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/DiscardBundleChangesDialog.axaml.cs @@ -3,14 +3,13 @@ namespace UniGetUI.Avalonia.Views.DialogPages; -public partial class DiscardBundleChangesDialog : Window +public partial class DiscardBundleChangesDialog : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog { public bool Confirmed { get; private set; } public DiscardBundleChangesDialog() { InitializeComponent(); - UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this); CancelButton.Click += (_, _) => Close(); DiscardButton.Click += (_, _) => { Confirmed = true; Close(); }; } diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/ImmersiveDialog.cs b/src/UniGetUI.Avalonia/Views/DialogPages/ImmersiveDialog.cs new file mode 100644 index 0000000000..c18adf3df1 --- /dev/null +++ b/src/UniGetUI.Avalonia/Views/DialogPages/ImmersiveDialog.cs @@ -0,0 +1,71 @@ +using System.ComponentModel; +using Avalonia; +using Avalonia.Controls; +using UniGetUI.Core.Logging; + +namespace UniGetUI.Avalonia.Views.DialogPages; + +/// +/// Base class for modal content hosted inside . +/// It exposes dialog-oriented show and close operations while the host owns presentation, +/// focus, animation, and stacking. +/// +public class ImmersiveDialog : ContentControl +{ + public static readonly StyledProperty TitleProperty = + AvaloniaProperty.Register(nameof(Title)); + public static readonly StyledProperty IsCloseButtonVisibleProperty = + AvaloniaProperty.Register(nameof(IsCloseButtonVisible), true); + + internal event EventHandler? CloseRequested; + public event EventHandler? Closing; + + public string? Title + { + get => GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + + public bool IsCloseButtonVisible + { + get => GetValue(IsCloseButtonVisibleProperty); + set => SetValue(IsCloseButtonVisibleProperty, value); + } + + public Task ShowDialog(Window owner) + { + MainWindow host = owner as MainWindow + ?? throw new ArgumentException( + "An immersive dialog must be owned by the main window.", + nameof(owner)); + return host.ShowImmersiveDialogAsync(this); + } + + public void Show(Window owner) => _ = ShowAndLogFailureAsync(owner); + + private async Task ShowAndLogFailureAsync(Window owner) + { + try + { + await ShowDialog(owner); + } + catch (Exception ex) + { + Logger.Error(ex); + } + } + + public void Close() + { + var args = new CancelEventArgs(); + Closing?.Invoke(this, args); + if (!args.Cancel) + CloseRequested?.Invoke(this, EventArgs.Empty); + } + + internal void NotifyOpened() => OnOpened(EventArgs.Empty); + internal void NotifyClosed() => OnClosed(EventArgs.Empty); + + protected virtual void OnOpened(EventArgs e) { } + protected virtual void OnClosed(EventArgs e) { } +} diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml index 3e3f7fc38c..8b14b9e3c2 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml @@ -1,39 +1,22 @@ - + Title="{Binding DialogTitle}"> - + - + - - - - - - - - - - - + @@ -59,4 +42,4 @@ - + diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml.cs index 082979e708..fc5fa38945 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/DialogPages/InstallOptionsWindow.axaml.cs @@ -1,5 +1,4 @@ using Avalonia.Controls; -using Avalonia.Input; using Avalonia.Threading; using UniGetUI.Avalonia.ViewModels; using UniGetUI.PackageEngine.Enums; @@ -8,7 +7,7 @@ namespace UniGetUI.Avalonia.Views; -public partial class InstallOptionsWindow : Window +public partial class InstallOptionsWindow : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog { public bool ShouldProceedWithOperation => ((InstallOptionsViewModel)DataContext!).ShouldProceedWithOperation; @@ -19,11 +18,6 @@ public InstallOptionsWindow(IPackage package, OperationType operation, InstallOp DataContext = vm; InitializeComponent(); - // Drop the OS title-bar strip but keep the system min/close buttons, extending the - // client area into the title-bar region (matches the Manage-ignored-updates window). - ExtendClientAreaToDecorationsHint = true; - ExtendClientAreaTitleBarHeightHint = -1; - vm.CloseRequested += (_, _) => Close(); } @@ -33,11 +27,4 @@ protected override void OnOpened(EventArgs e) Dispatcher.UIThread.Post(OptionsControl.FocusProfileSelector, DispatcherPriority.Background); } - // The client area is extended over the title bar, so provide window dragging from the - // transparent strip at the top. - private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) - { - if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) - BeginMoveDrag(e); - } } diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/IntegrityViolationDialog.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/IntegrityViolationDialog.axaml index 4bb3f5e327..f6868596e5 100644 --- a/src/UniGetUI.Avalonia/Views/DialogPages/IntegrityViolationDialog.axaml +++ b/src/UniGetUI.Avalonia/Views/DialogPages/IntegrityViolationDialog.axaml @@ -1,28 +1,20 @@ - + + Background="{DynamicResource AppDialogBackground}"> - - - + - + + + + + + + + + + + + + _badgeSubscriptions = new(); + private readonly SemaphoreSlim _modalTransitionSemaphore = new(1, 1); + private readonly List _modalStack = new(); + private readonly Dictionary _modalFocusHistory = new(); + private readonly List _webViewsHiddenForModal = new(); + private IDisposable? _modalTitleSubscription; + private Control? _focusBeforeModal; public enum RuntimeNotificationLevel { @@ -149,6 +158,7 @@ public MainWindow() UpdateOperationsPanelRow(); Resized += (_, _) => _ = SaveGeometryAsync(); + ModalLayer.SizeChanged += (_, _) => UpdateModalSurfaceSize(); PositionChanged += (_, _) => _ = SaveGeometryAsync(); this.GetObservable(WindowStateProperty).SubscribeValue(state => { UpdateOnScreenState(); _ = SaveGeometryAsync(); }); this.GetObservable(IsVisibleProperty).SubscribeValue(_ => UpdateOnScreenState()); @@ -237,6 +247,16 @@ private void ReleaseWindowResources() private void Window_KeyDown(object? sender, KeyEventArgs e) { + // A WinUI ContentDialog is a true modal scope: background navigation and package-page + // shortcuts must not run while keyboard focus is inside the dialog. + if (ModalLayer.IsVisible) + { + if (e.Key == Key.Escape) + (ModalContent.Content as ImmersiveDialog)?.Close(); + e.Handled = true; + return; + } + bool isCtrl = e.KeyModifiers.HasFlag(KeyModifiers.Control); bool isShift = e.KeyModifiers.HasFlag(KeyModifiers.Shift); @@ -450,26 +470,13 @@ private void SetupTitleBarFocusOpacity() HamburgerPanel.Opacity = active ? 1.0 : 0.6; WindowButtons.Opacity = active ? 1.0 : 0.55; - // Match WinUI's inactive Mica appearance without invoking the permanent, global - // NotifyMicaUnavailable() fallback used when composition actually fails (#5111). - // A partial (not full) overlay so the inactive window still keeps some of the Mica - // tint instead of flattening to solid grey. Skip it while a modal dialog is open: the - // window deactivates but shouldn't grey out behind its own dialog. Modeless owned - // windows (operation output/failure) don't count — the main window should still grey - // when the user switches to another application. + // WinUI replaces inactive Mica with SolidBackgroundFillColorBase rather than leaving + // a partially wallpaper-tinted backdrop. AppWindowBackground is that token + // (#F3F3F3 light / #202020 dark), so make the fallback fully opaque while inactive. InactiveMicaFallback.Opacity = - MicaWindowHelper.IsMicaEnabled() && !active && !HasOpenModalDialog() ? 0.4 : 0.0; + MicaWindowHelper.IsMicaEnabled() && !active ? 1.0 : 0.0; }); - // True when a window opened modally (ShowDialog) over this one is currently open. - private bool HasOpenModalDialog() - { - foreach (var owned in OwnedWindows) - if (owned.IsDialog) - return true; - return false; - } - private void SetupTitleBar() { if (OperatingSystem.IsMacOS()) @@ -892,7 +899,7 @@ private void UpdateMaximizeButtonState(bool isMaximized) { MaximizeIcon.Data = Geometry.Parse( isMaximized - ? "M2,0 H10 V8 H2 Z M0,2 H8 V10 H0 Z" + ? "M3,0 H10 V7 H8 V2 H3 Z M0,3 H7 V10 H0 Z" : "M0,0 H10 V10 H0 Z"); ToolTip.SetTip( MaximizeButton, @@ -903,7 +910,7 @@ private void UpdateMaximizeButtonState(bool isMaximized) // button's bounds — the area for which we claim HTMAXBUTTON so Snap Layouts can attach. private bool HitTestMaximizeButton(nint lParam) { - if (!MaximizeButton.IsVisible) + if (!MaximizeButton.IsVisible || !MaximizeButton.IsEnabled) return false; try { @@ -931,7 +938,7 @@ private void SetMaximizeButtonVisual(bool hover, bool pressed) if (key is not null && this.TryFindResource(key, ActualThemeVariant, out var res) && res is IBrush brush) MaximizeButton.Background = brush; else - MaximizeButton.Background = Brushes.Transparent; + MaximizeButton.ClearValue(BackgroundProperty); } private static void TrackNonClientMouseLeave(nint hWnd) @@ -1446,6 +1453,259 @@ public void OpenManagerSettings(IPackageManager? manager = null) => ViewModel.OpenManagerSettings(manager); public void ShowHelp(string uriAttachment = "") => ViewModel.ShowHelp(uriAttachment); + /// + /// Shows the ignored-updates editor as a modal surface inside this window. Keeping the + /// surface in the owner's visual tree makes it resize with the app and avoids a second + /// task-switcher/window-management target. + /// + public async Task ShowManageIgnoredUpdatesAsync() + { + var dialog = new ManageIgnoredUpdatesWindow(); + await ShowImmersiveDialogAsync(dialog); + } + + public async Task ShowImmersiveDialogAsync(ImmersiveDialog dialog) + { + var completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + void CloseDialog(object? sender, EventArgs args) => completion.TrySetResult(); + bool opened = false; + bool added = false; + dialog.CloseRequested += CloseDialog; + + try + { + await _modalTransitionSemaphore.WaitAsync(); + try + { + if (_modalStack.Contains(dialog)) + throw new InvalidOperationException("The immersive dialog is already open."); + + if (_modalStack.Count == 0) + { + _focusBeforeModal = FocusManager?.GetFocusedElement() as Control; + HideNativeWebViewsForModal(); + // Block pointer input without putting every underlying control into Avalonia's + // disabled visual state. The modal layer owns focus while it is visible. + MainContentRoot.IsHitTestVisible = false; + TitleBarGrid.IsHitTestVisible = false; + } + else + { + _modalFocusHistory[_modalStack[^1]] = + FocusManager?.GetFocusedElement() as Control; + } + + ModalContent.Content = null; + _modalStack.Add(dialog); + added = true; + PresentModal(dialog); + dialog.NotifyOpened(); + opened = true; + await AnimateModalAsync(opening: true); + FocusModalIfNeeded(dialog); + } + finally + { + _modalTransitionSemaphore.Release(); + } + + await completion.Task; + } + finally + { + dialog.CloseRequested -= CloseDialog; + await _modalTransitionSemaphore.WaitAsync(); + try + { + int index = added ? _modalStack.IndexOf(dialog) : -1; + bool isTopmost = index >= 0 && index == _modalStack.Count - 1; + if (isTopmost) + { + try + { + await AnimateModalAsync(opening: false); + } + catch (Exception ex) + { + Logger.Error(ex); + } + ModalContent.Content = null; + } + + if (index >= 0) + _modalStack.RemoveAt(index); + _modalFocusHistory.Remove(dialog); + + if (opened) + { + try + { + dialog.NotifyClosed(); + } + catch (Exception ex) + { + Logger.Error(ex); + } + } + + if (_modalStack.Count > 0) + { + PresentModal(_modalStack[^1]); + ModalLayer.Opacity = 1; + ModalSurface.Opacity = 1; + ModalSurface.RenderTransform = null; + RestoreModalFocus(_modalStack[^1]); + } + else + { + _modalTitleSubscription?.Dispose(); + _modalTitleSubscription = null; + ModalLayer.IsVisible = false; + ModalContent.Content = null; + ModalTitle.Text = ""; + MainContentRoot.IsHitTestVisible = true; + TitleBarGrid.IsHitTestVisible = true; + RestoreNativeWebViewsAfterModal(); + Dispatcher.UIThread.Post( + () => _focusBeforeModal?.Focus(), + DispatcherPriority.Background); + _focusBeforeModal = null; + } + } + finally + { + _modalTransitionSemaphore.Release(); + } + } + } + + private void PresentModal(ImmersiveDialog dialog) + { + _modalTitleSubscription?.Dispose(); + _modalTitleSubscription = dialog.GetObservable(ImmersiveDialog.TitleProperty) + .SubscribeValue(title => ModalTitle.Text = title ?? ""); + ModalCloseButton.IsVisible = dialog.IsCloseButtonVisible; + ModalContent.Content = dialog; + ModalLayer.IsVisible = true; + UpdateModalSurfaceSize(); + } + + private void HideNativeWebViewsForModal() + { + _webViewsHiddenForModal.Clear(); + foreach (UniGetUiWebView webView in MainContentRoot.GetVisualDescendants().OfType()) + { + if (!webView.IsVisible) + continue; + + _webViewsHiddenForModal.Add(webView); + webView.IsVisible = false; + } + } + + private void RestoreNativeWebViewsAfterModal() + { + foreach (UniGetUiWebView webView in _webViewsHiddenForModal) + webView.IsVisible = true; + _webViewsHiddenForModal.Clear(); + } + + private void FocusModalIfNeeded(ImmersiveDialog dialog) + { + Dispatcher.UIThread.Post( + () => + { + if (ReferenceEquals(ModalContent.Content, dialog) && !dialog.IsKeyboardFocusWithin) + ModalCloseButton.Focus(); + }, + DispatcherPriority.Background); + } + + private void RestoreModalFocus(ImmersiveDialog dialog) + { + _modalFocusHistory.Remove(dialog, out Control? previousFocus); + Dispatcher.UIThread.Post( + () => + { + if (!ReferenceEquals(ModalContent.Content, dialog)) + return; + if (previousFocus?.Focus() != true && !dialog.IsKeyboardFocusWithin) + ModalCloseButton.Focus(); + }, + DispatcherPriority.Background); + } + + private void ModalCloseButton_Click(object? sender, RoutedEventArgs e) => + (ModalContent.Content as ImmersiveDialog)?.Close(); + + private void UpdateModalSurfaceSize() + { + if (ModalContent.Content is not ImmersiveDialog dialog) + return; + + const double maximumSurfaceWidth = 1100; + const double maximumSurfaceHeight = 900; + double layerWidth = ModalLayer.Bounds.Width > 0 ? ModalLayer.Bounds.Width : Bounds.Width; + double layerHeight = ModalLayer.Bounds.Height > 0 ? ModalLayer.Bounds.Height : Bounds.Height; + double availableWidth = Math.Max(0, layerWidth - 96); + double availableHeight = Math.Max(0, layerHeight - 96); + + ModalSurface.MaxWidth = Math.Min(maximumSurfaceWidth, availableWidth); + ModalSurface.MaxHeight = Math.Min(maximumSurfaceHeight, availableHeight); + ModalSurface.Width = double.IsFinite(dialog.MaxWidth) + ? Math.Min(dialog.MaxWidth, ModalSurface.MaxWidth) + : double.NaN; + ModalSurface.Height = double.IsFinite(dialog.MaxHeight) + ? Math.Min(dialog.MaxHeight, ModalSurface.MaxHeight) + : double.NaN; + } + + private async Task AnimateModalAsync(bool opening) + { + if (MotionPreference.ReducedMotion) + { + ModalLayer.Opacity = opening ? 1 : 0; + ModalSurface.Opacity = opening ? 1 : 0; + ModalSurface.RenderTransform = null; + return; + } + + var scale = new ScaleTransform(); + ModalSurface.RenderTransformOrigin = new RelativePoint(0.5, 0.5, RelativeUnit.Relative); + ModalSurface.RenderTransform = scale; + + double fromOpacity = opening ? 0 : 1; + double toOpacity = opening ? 1 : 0; + double fromScale = opening ? 1.06 : 1; + double toScale = opening ? 1 : 1.06; + var duration = TimeSpan.FromMilliseconds(opening ? 180 : 120); + long started = Stopwatch.GetTimestamp(); + while (true) + { + double progress = Math.Clamp( + Stopwatch.GetElapsedTime(started).TotalMilliseconds / duration.TotalMilliseconds, + 0, + 1); + double eased = 1 - Math.Pow(1 - progress, 3); + double opacity = fromOpacity + ((toOpacity - fromOpacity) * eased); + double currentScale = fromScale + ((toScale - fromScale) * eased); + + ModalLayer.Opacity = opacity; + ModalSurface.Opacity = opacity; + scale.ScaleX = currentScale; + scale.ScaleY = currentScale; + + if (progress >= 1) + break; + await Task.Delay(16); + } + + ModalLayer.Opacity = toOpacity; + ModalSurface.Opacity = toOpacity; + scale.ScaleX = toScale; + scale.ScaleY = toScale; + } + /// /// Focuses the global search box and optionally pre-fills a character typed /// while the package list had focus (type-to-search). diff --git a/src/UniGetUI.Avalonia/Views/SidebarView.axaml b/src/UniGetUI.Avalonia/Views/SidebarView.axaml index 61e47a35be..9d7e488ce5 100644 --- a/src/UniGetUI.Avalonia/Views/SidebarView.axaml +++ b/src/UniGetUI.Avalonia/Views/SidebarView.axaml @@ -184,8 +184,7 @@ - + diff --git a/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs b/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs index 5bdf240b8e..7a1878bad6 100644 --- a/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs @@ -13,6 +13,7 @@ namespace UniGetUI.Avalonia.Views; public partial class SidebarView : BaseView { private bool _lastNavItemSelectionWasAuto; + private bool _isMoreFlyoutOpen; /// /// Whether the nav item text labels are shown. False renders an icon-only rail; true renders the @@ -31,6 +32,15 @@ public bool ShowLabels public SidebarView() { InitializeComponent(); + if (FlyoutBase.GetAttachedFlyout(MoreNavBtn) is { } moreFlyout) + { + moreFlyout.Opened += (_, _) => _isMoreFlyoutOpen = true; + moreFlyout.Closed += (_, _) => + { + _isMoreFlyoutOpen = false; + SyncListBoxSelection(ViewModel?.SelectedPageType ?? PageType.Null); + }; + } } protected override void OnDataContextChanged(EventArgs e) @@ -50,6 +60,9 @@ protected override void OnDataContextChanged(EventArgs e) private void SyncListBoxSelection(PageType page) { + if (_isMoreFlyoutOpen) + return; + // Selection lives in two ListBoxes (main + footer); only one may hold a selection at a time. _lastNavItemSelectionWasAuto = true; NavListBox.SelectedItem = page switch @@ -82,8 +95,8 @@ private void HandleNavSelectionChanged(object? selectedItem) if (tag == "More") { - // Not a page: re-sync selection so "More" doesn't stay highlighted, then open its menu. - SyncListBoxSelection(ViewModel?.SelectedPageType ?? PageType.Null); + // Keep the item selected until the menu closes so its accent pill remains anchored. + _isMoreFlyoutOpen = true; FlyoutBase.ShowAttachedFlyout(item); return; } diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml index 849b4a0b68..01aba0051d 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml @@ -29,6 +29,7 @@ + The accent pill is a single moving overlay owned by SidebarView. The content presenter is + named ContentHost (not PART_ContentPresenter) so the base theme's accent-fill setters don't + apply. --> @@ -291,10 +280,6 @@ - - + + + + + + + + + + + + + + + + + + @@ -104,6 +123,25 @@ + + + + + + + + + + + + + + + + + + From 0ece76f53c46eeaef957092932bb25b6b2212f41 Mon Sep 17 00:00:00 2001 From: Meteoni-San <141850520+Meteony@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:35:09 +0000 Subject: [PATCH 14/26] revert tray dual-line experiment --- src/UniGetUI.Avalonia/Infrastructure/TrayService.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/UniGetUI.Avalonia/Infrastructure/TrayService.cs b/src/UniGetUI.Avalonia/Infrastructure/TrayService.cs index 1d8c1152ee..c1311c2f12 100644 --- a/src/UniGetUI.Avalonia/Infrastructure/TrayService.cs +++ b/src/UniGetUI.Avalonia/Infrastructure/TrayService.cs @@ -135,12 +135,8 @@ private static NativeMenu BuildMenu(MainWindow owner) menu.Add(installed); menu.Add(new NativeMenuItemSeparator()); - menu.Add(new NativeMenuItem(CoreTools.Translate("UniGetUI by Devolutions")) - { - IsEnabled = false, - }); menu.Add(new NativeMenuItem( - $"{CoreTools.Translate("Version")} {CoreData.VersionName}") + CoreTools.Translate("UniGetUI Version {0} by Devolutions", CoreData.VersionName)) { IsEnabled = false, }); From 7ff836e1869311f2064a9cce7768d620e7099ebd Mon Sep 17 00:00:00 2001 From: Meteoni-San <141850520+Meteony@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:38:13 +0000 Subject: [PATCH 15/26] switch close glyph to path-rendering for thickness consistency --- src/UniGetUI.Avalonia/Views/MainWindow.axaml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml b/src/UniGetUI.Avalonia/Views/MainWindow.axaml index 119206f204..24c33d5a73 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml @@ -688,10 +688,15 @@ automation:AutomationProperties.Name="{t:Translate Close}" ToolTip.Tip="{t:Translate Close}" Click="CloseButton_Click"> - + From 62a525b435a1bc22ede2cf410395fe2274d6896e Mon Sep 17 00:00:00 2001 From: Meteoni-San <141850520+Meteony@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:42:55 +0000 Subject: [PATCH 16/26] update package-manager status indicators to add icons for parity with previous UI --- .../SettingsPages/ManagersHomepage.axaml.cs | 67 ++++++++++++++++--- 1 file changed, 57 insertions(+), 10 deletions(-) diff --git a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs index 5e903b8d23..b99e443dbf 100644 --- a/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Automation; using Avalonia.Controls; +using Avalonia.Controls.Shapes; using Avalonia.Layout; using Avalonia.Media; using UniGetUI.Avalonia.Infrastructure; @@ -10,6 +11,7 @@ using UniGetUI.PackageEngine; using UniGetUI.PackageEngine.Interfaces; using CoreSettings = UniGetUI.Core.SettingsEngine.Settings; +using AvaloniaPath = Avalonia.Controls.Shapes.Path; namespace UniGetUI.Avalonia.Views.Pages.SettingsPages; @@ -22,7 +24,8 @@ public event EventHandler? RestartRequired { add { } remove { } } public event EventHandler? NavigationRequested { add { } remove { } } public event EventHandler? ManagerNavigationRequested; - private readonly List<(ToggleSwitch Toggle, IPackageManager Manager, Border Badge, TextBlock BadgeText)> _rows = []; + private readonly List<(ToggleSwitch Toggle, IPackageManager Manager, Border Badge, + Ellipse BadgeIcon, AvaloniaPath BadgeGlyph, TextBlock BadgeText)> _rows = []; private bool _isLoadingToggles; public ManagersHomepage() @@ -49,13 +52,46 @@ public ManagersHomepage() FontSize = 12, FontWeight = FontWeight.SemiBold, VerticalAlignment = VerticalAlignment.Center, + Foreground = LookupBrush("TextFillColorPrimaryBrush"), }; AutomationProperties.SetAccessibilityView(badgeText, AccessibilityView.Raw); + + var badgeIcon = new Ellipse + { + Width = 12, + Height = 12, + }; + var badgeGlyph = new AvaloniaPath + { + Width = 7, + Height = 7, + StrokeThickness = 1.5, + StrokeLineCap = PenLineCap.Round, + StrokeJoin = PenLineJoin.Round, + Stretch = Stretch.Uniform, + }; + var iconHost = new Grid + { + Width = 12, + Height = 12, + VerticalAlignment = VerticalAlignment.Center, + Children = { badgeIcon, badgeGlyph }, + }; + AutomationProperties.SetAccessibilityView(iconHost, AccessibilityView.Raw); + + var badgeContent = new StackPanel + { + Orientation = Orientation.Horizontal, + Spacing = 5, + VerticalAlignment = VerticalAlignment.Center, + Children = { iconHost, badgeText }, + }; var badge = new Border { CornerRadius = new CornerRadius(4), Padding = new Thickness(6, 3, 6, 3), - Child = badgeText, + HorizontalAlignment = HorizontalAlignment.Center, + Child = badgeContent, }; AutomationProperties.SetAccessibilityView(badge, AccessibilityView.Raw); @@ -72,14 +108,14 @@ public ManagersHomepage() _isLoadingToggles = true; toggle.IsChecked = manager.IsEnabled(); _isLoadingToggles = false; - ApplyStatusBadge(manager, toggle, badge, badgeText); + ApplyStatusBadge(manager, toggle, badge, badgeIcon, badgeGlyph, badgeText); }; toggle.IsCheckedChanged += async (_, _) => { if (_isLoadingToggles) return; CoreSettings.SetDictionaryItem(CoreSettings.K.DisabledManagers, manager.Name, toggle.IsChecked != true); await Task.Run(manager.Initialize); - ApplyStatusBadge(manager, toggle, badge, badgeText); + ApplyStatusBadge(manager, toggle, badge, badgeIcon, badgeGlyph, badgeText); AccessibilityAnnouncementService.AnnounceToggle(manager.DisplayName, toggle.IsChecked == true); }; @@ -108,7 +144,7 @@ public ManagersHomepage() btn.Click += (_, _) => ManagerNavigationRequested?.Invoke(this, capturedManager); ManagersPanel.Children.Add(btn); - _rows.Add((toggle, manager, badge, badgeText)); + _rows.Add((toggle, manager, badge, badgeIcon, badgeGlyph, badgeText)); } } @@ -116,15 +152,21 @@ public ManagersHomepage() public void RefreshToggles() { _isLoadingToggles = true; - foreach (var (toggle, manager, badge, badgeText) in _rows) + foreach (var (toggle, manager, badge, badgeIcon, badgeGlyph, badgeText) in _rows) { toggle.IsChecked = manager.IsEnabled(); - ApplyStatusBadge(manager, toggle, badge, badgeText); + ApplyStatusBadge(manager, toggle, badge, badgeIcon, badgeGlyph, badgeText); } _isLoadingToggles = false; } - private void ApplyStatusBadge(IPackageManager manager, ToggleSwitch toggle, Border badge, TextBlock text) + private void ApplyStatusBadge( + IPackageManager manager, + ToggleSwitch toggle, + Border badge, + Ellipse icon, + AvaloniaPath glyph, + TextBlock text) { string bgKey, fgKey, label; if (!manager.IsEnabled()) @@ -132,21 +174,26 @@ private void ApplyStatusBadge(IPackageManager manager, ToggleSwitch toggle, Bord bgKey = "WarningBannerBackground"; fgKey = "StatusWarningForeground"; label = CoreTools.Translate("Disabled"); + glyph.Data = Geometry.Parse("M3.5,1 V4.2 M3.5,6 V6.1"); } else if (manager.Status.Found) { bgKey = "StatusSuccessBackground"; fgKey = "StatusSuccessForeground"; label = CoreTools.Translate("Ready"); + glyph.Data = Geometry.Parse("M1,3.6 L2.8,5.4 L6,1.5"); } else { bgKey = "StatusErrorBackground"; fgKey = "StatusErrorForeground"; label = CoreTools.Translate("Not found"); + glyph.Data = Geometry.Parse("M1.4,1.4 L5.6,5.6 M5.6,1.4 L1.4,5.6"); } - badge.Background = LookupBrush(bgKey); - text.Foreground = LookupBrush(fgKey); + IBrush background = LookupBrush(bgKey); + badge.Background = background; + icon.Fill = LookupBrush(fgKey); + glyph.Stroke = background; text.Text = label; // Bake state into Name so VoiceOver always announces it on macOS AutomationProperties.SetName(toggle, $"{manager.DisplayName}, {label}"); From 5d8786d9f79ab6eee6c2867ee339d49d2c392178 Mon Sep 17 00:00:00 2001 From: Meteoni-San <141850520+Meteony@users.noreply.github.com> Date: Tue, 28 Jul 2026 16:50:34 +0000 Subject: [PATCH 17/26] combobox styling update --- .../Assets/Styles/Styles.Common.axaml | 18 ++++++++++++++++++ .../SoftwarePages/AbstractPackagesPage.axaml | 4 ++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml index 3d9efba61e..fddb4dbd05 100644 --- a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml +++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml @@ -67,6 +67,15 @@ + + + + + + + + + @@ -176,6 +185,15 @@ + + + + + + + + + diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml index 40351d06a5..691cd1f874 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml @@ -922,9 +922,9 @@