diff --git a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml
index 1582038c82..a05097453c 100644
--- a/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml
+++ b/src/UniGetUI.Avalonia/Assets/Styles/Styles.Common.axaml
@@ -31,17 +31,33 @@
gets a lighter light-theme value to read as the same weight as the compact title-bar field. -->
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
@@ -64,6 +80,8 @@
+
+
@@ -76,7 +94,7 @@
-
+
@@ -113,17 +131,33 @@
share it since thick-stroke heaviness only reads as darker on light backgrounds. -->
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
+
+
@@ -142,10 +176,12 @@
-
+
+
+
@@ -187,6 +223,44 @@
+
+
+
+
+
+
+
+
+
@@ -198,34 +272,23 @@
+ 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. -->
@@ -241,10 +304,6 @@
-
-
-
-
-
-
-
-
-
-
+
@@ -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}">
-
-
-
+
+
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/IntegrityViolationDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/IntegrityViolationDialog.axaml.cs
index 2a7c309af3..54dc3ad8c4 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/IntegrityViolationDialog.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/IntegrityViolationDialog.axaml.cs
@@ -3,12 +3,11 @@
namespace UniGetUI.Avalonia.Views.DialogPages;
-public partial class IntegrityViolationDialog : Window
+public partial class IntegrityViolationDialog : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog
{
public IntegrityViolationDialog()
{
InitializeComponent();
- UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this);
CloseButton.Click += (_, _) => Close();
}
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/ManageDesktopShortcutsWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/ManageDesktopShortcutsWindow.axaml
index 931825ab91..4c3bba20dd 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/ManageDesktopShortcutsWindow.axaml
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/ManageDesktopShortcutsWindow.axaml
@@ -1,42 +1,34 @@
-
-
+
-
+
-
-
-
-
-
+
-
-
+
-
+
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/ManageDesktopShortcutsWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/ManageDesktopShortcutsWindow.axaml.cs
index c8b7a68b22..f71aa6af43 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/ManageDesktopShortcutsWindow.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/ManageDesktopShortcutsWindow.axaml.cs
@@ -5,14 +5,13 @@
namespace UniGetUI.Avalonia.Views;
-public partial class ManageDesktopShortcutsWindow : Window
+public partial class ManageDesktopShortcutsWindow : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog
{
public ManageDesktopShortcutsWindow(System.Collections.Generic.IReadOnlyList? shortcuts = null)
{
var vm = new ManageDesktopShortcutsViewModel(shortcuts);
DataContext = vm;
InitializeComponent();
- UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this);
vm.CloseRequested += (_, _) => Close();
}
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml
index 2b8204bc6b..d2da082545 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml
@@ -1,20 +1,21 @@
-
-
-
+ Title="{Binding Title}"
+ MaxWidth="850"
+ MinWidth="520"
+ MaxHeight="500"
+ MinHeight="280"
+ HorizontalContentAlignment="Stretch"
+ VerticalContentAlignment="Stretch">
+
+
-
+
+ Margin="20"
+ RowSpacing="12">
-
+
-
-
+
@@ -170,4 +169,4 @@
-
+
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml.cs
index bbac588cbc..1bd870f349 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/ManageIgnoredUpdatesWindow.axaml.cs
@@ -1,76 +1,39 @@
using Avalonia.Controls;
+using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Threading;
using UniGetUI.Avalonia.ViewModels;
-using UniGetUI.Core.Logging;
-using UniGetUI.Core.SettingsEngine;
namespace UniGetUI.Avalonia.Views;
-public partial class ManageIgnoredUpdatesWindow : Window
+public partial class ManageIgnoredUpdatesWindow : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog
{
- public ManageIgnoredUpdatesWindow(Window? owner = null)
+ public ManageIgnoredUpdatesWindow()
{
var vm = new ManageIgnoredUpdatesViewModel();
DataContext = vm;
InitializeComponent();
- UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this);
- // Set the size before the window opens so CenterOwner positions it correctly.
- ApplyInitialSize(owner);
-
- vm.CloseRequested += (_, _) => Close();
+ KeyDown += OnDialogKeyDown;
+ AttachedToVisualTree += (_, _) => Dispatcher.UIThread.Post(FocusInitialControl,
+ DispatcherPriority.Background);
}
- // Restore the user's last size, falling back to the XAML default, and never open larger
- // than the owner window (recovers the adaptive sizing of the pre-Avalonia overlay).
- private void ApplyInitialSize(Window? owner)
+ private void FocusInitialControl()
{
- double width = Width;
- double height = Height;
-
- string saved = Settings.GetValue(Settings.K.IgnoredUpdatesWindowSize);
- if (!string.IsNullOrEmpty(saved))
- {
- string[] parts = saved.Split(',');
- if (parts.Length == 2 && int.TryParse(parts[0], out int w) && int.TryParse(parts[1], out int h))
- {
- width = w;
- height = h;
- }
- }
-
- if (owner is not null)
- {
- width = Math.Min(width, owner.Width - 40);
- height = Math.Min(height, owner.Height - 40);
- }
-
- Width = Math.Max(MinWidth, width);
- Height = Math.Max(MinHeight, height);
+ if (((ManageIgnoredUpdatesViewModel)DataContext!).HasEntries)
+ IgnoredUpdatesGrid.Focus();
+ else
+ ResetButton.Focus();
}
- protected override void OnClosing(WindowClosingEventArgs e)
+ private void OnDialogKeyDown(object? sender, KeyEventArgs e)
{
- // Persist only the normal (non-maximized) size so the dialog reopens as the user left it.
- if (WindowState == WindowState.Normal)
+ if (e.Key == Key.Escape)
{
- try { Settings.SetValue(Settings.K.IgnoredUpdatesWindowSize, $"{(int)Width},{(int)Height}"); }
- catch (Exception ex) { Logger.Error(ex); }
+ Close();
+ e.Handled = true;
}
- base.OnClosing(e);
- }
-
- protected override void OnOpened(EventArgs e)
- {
- base.OnOpened(e);
- Dispatcher.UIThread.Post(() =>
- {
- if (((ManageIgnoredUpdatesViewModel)DataContext!).HasEntries)
- IgnoredUpdatesGrid.Focus();
- else
- ResetButton.Focus();
- }, DispatcherPriority.Background);
}
private void ResetYes_Click(object? sender, RoutedEventArgs e)
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/MissingDependencyDialog.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/MissingDependencyDialog.axaml
index fbf0e559ea..dcd67bf4cf 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/MissingDependencyDialog.axaml
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/MissingDependencyDialog.axaml
@@ -1,28 +1,19 @@
-
+
+ Background="{DynamicResource AppDialogBackground}">
-
-
-
+
@@ -47,7 +38,7 @@
IsVisible="False"/>
-
@@ -61,4 +52,4 @@
-
+
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/MissingDependencyDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/MissingDependencyDialog.axaml.cs
index 0e36090543..26e8bf528a 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/MissingDependencyDialog.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/MissingDependencyDialog.axaml.cs
@@ -10,7 +10,7 @@
namespace UniGetUI.Avalonia.Views.DialogPages;
-public partial class MissingDependencyDialog : Window
+public partial class MissingDependencyDialog : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog
{
private readonly ManagerDependency _dep;
private readonly int _current;
@@ -26,7 +26,6 @@ public MissingDependencyDialog(ManagerDependency dep, int current, int total)
_total = total;
InitializeComponent();
- UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this);
bool notFirstTime =
Settings.GetDictionaryItem(Settings.K.DependencyManagement, dep.Name)
@@ -36,7 +35,6 @@ public MissingDependencyDialog(ManagerDependency dep, int current, int total)
Title = CoreTools.Translate("Missing dependency")
+ (total > 1 ? $" ({current}/{total})" : "");
- TitleBlock.Text = Title;
DescBlock.Text = CoreTools.Translate(
"UniGetUI requires {0} to operate, but it was not found on your system.", dep.Name);
InfoBlock.Text = CoreTools.Translate(
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml
index 4d349edc9a..af6f1eb7fb 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml
@@ -1,14 +1,14 @@
-
+ MaxWidth="800"
+ MinWidth="500"
+ MaxHeight="550"
+ MinHeight="300"
+ Background="{DynamicResource AppDialogBackground}">
@@ -43,4 +43,4 @@
-
+
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml.cs
index eabc4d64cf..b012b766cc 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/OperationFailedDialog.axaml.cs
@@ -12,7 +12,7 @@
namespace UniGetUI.Avalonia.Views.DialogPages;
-public partial class OperationFailedDialog : Window
+public partial class OperationFailedDialog : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog
{
private readonly AbstractOperation _operation;
@@ -20,7 +20,6 @@ public OperationFailedDialog(AbstractOperation operation)
{
_operation = operation;
InitializeComponent();
- UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this);
Title = operation.Metadata.FailureMessage;
HeaderContent.Text =
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml
index e77bcc6c72..4f7399e96e 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml
@@ -1,16 +1,16 @@
-
-
+
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs
index 3c7ef42105..41d063d257 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/OperationOutputWindow.axaml.cs
@@ -7,14 +7,13 @@
namespace UniGetUI.Avalonia.Views.DialogPages;
-public partial class OperationOutputWindow : Window
+public partial class OperationOutputWindow : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog
{
public OperationOutputWindow(AbstractOperation operation)
{
var vm = new OperationOutputViewModel(operation);
DataContext = vm;
InitializeComponent();
- UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this);
OutputText.SetLines(vm.OutputLines);
vm.OutputLines.CollectionChanged += OnOutputLinesChanged;
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml
index cde1c13c26..740c25cdd0 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml
@@ -1,20 +1,20 @@
-
+ Title="{t:Translate Package details}">
-
+
@@ -97,7 +97,7 @@
-
+
@@ -255,9 +255,10 @@
x:Name="ScreenshotsBorder"
Height="320">
-
-
+
-
+
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs
index c12270c430..b044de66b6 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/PackageDetailsWindow.axaml.cs
@@ -24,7 +24,7 @@
namespace UniGetUI.Avalonia.Views;
-public partial class PackageDetailsWindow : Window
+public partial class PackageDetailsWindow : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog
{
private const double WideThreshold = 950;
private const string ContributeUrl = "https://github.com/Devolutions/UniGetUI";
@@ -44,7 +44,6 @@ public PackageDetailsWindow(IPackage package, OperationType operation)
_vm = new PackageDetailsViewModel(package, operation);
DataContext = _vm;
InitializeComponent();
- UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this);
// Honor the OS "reduce motion" preference: drop the screenshot slide animation.
if (MotionPreference.ReducedMotion)
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/ResetSettingsDialog.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/ResetSettingsDialog.axaml
index 6b83d6531c..16b5baae40 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/ResetSettingsDialog.axaml
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/ResetSettingsDialog.axaml
@@ -1,33 +1,25 @@
-
+
+ Background="{DynamicResource AppDialogBackground}">
-
-
-
@@ -44,4 +36,4 @@
-
+
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/ResetSettingsDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/ResetSettingsDialog.axaml.cs
index b39b6e49c2..1d38565837 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/ResetSettingsDialog.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/ResetSettingsDialog.axaml.cs
@@ -3,14 +3,13 @@
namespace UniGetUI.Avalonia.Views.DialogPages;
-public partial class ResetSettingsDialog : Window
+public partial class ResetSettingsDialog : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog
{
public bool Confirmed { get; private set; }
public ResetSettingsDialog()
{
InitializeComponent();
- UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this);
CancelButton.Click += (_, _) => Close();
ResetButton.Click += (_, _) => { Confirmed = true; Close(); };
}
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/SimpleErrorDialog.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/SimpleErrorDialog.axaml
index 284f1146a6..e4da756aee 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/SimpleErrorDialog.axaml
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/SimpleErrorDialog.axaml
@@ -1,34 +1,25 @@
-
+
+ Background="{DynamicResource AppDialogBackground}">
-
-
+
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/SimpleErrorDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/SimpleErrorDialog.axaml.cs
index 9ff1e79adc..8518c8e6e3 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/SimpleErrorDialog.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/SimpleErrorDialog.axaml.cs
@@ -3,14 +3,12 @@
namespace UniGetUI.Avalonia.Views.DialogPages;
-public partial class SimpleErrorDialog : Window
+public partial class SimpleErrorDialog : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog
{
public SimpleErrorDialog(string title, string message)
{
InitializeComponent();
- UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this);
Title = title;
- TitleBlock.Text = title;
MessageBlock.Text = message;
OkButton.Click += (_, _) => Close();
}
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/TelemetryDialog.axaml b/src/UniGetUI.Avalonia/Views/DialogPages/TelemetryDialog.axaml
index af167392a5..9a85f9369b 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/TelemetryDialog.axaml
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/TelemetryDialog.axaml
@@ -1,30 +1,23 @@
-
+
+ Background="{DynamicResource AppDialogBackground}">
-
-
-
-
+
@@ -40,7 +33,7 @@
-
-
+
diff --git a/src/UniGetUI.Avalonia/Views/DialogPages/TelemetryDialog.axaml.cs b/src/UniGetUI.Avalonia/Views/DialogPages/TelemetryDialog.axaml.cs
index 0aa027331e..c30a2f656e 100644
--- a/src/UniGetUI.Avalonia/Views/DialogPages/TelemetryDialog.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/DialogPages/TelemetryDialog.axaml.cs
@@ -4,14 +4,13 @@
namespace UniGetUI.Avalonia.Views.DialogPages;
-public partial class TelemetryDialog : Window
+public partial class TelemetryDialog : UniGetUI.Avalonia.Views.DialogPages.ImmersiveDialog
{
public bool? Result { get; private set; }
public TelemetryDialog()
{
InitializeComponent();
- UniGetUI.Avalonia.Infrastructure.MicaWindowHelper.Apply(this);
Body2.Text = CoreTools.Translate("No personal information is collected nor sent, and the collected data is anonimized, so it can't be back-tracked to you.");
diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml b/src/UniGetUI.Avalonia/Views/MainWindow.axaml
index 57630932e8..77a6da1fbe 100644
--- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml
+++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml
@@ -17,6 +17,9 @@
MinWidth="700"
MinHeight="500">
+
@@ -39,6 +42,10 @@
+
+
+
+
+
+
+
+
@@ -491,9 +525,7 @@
Margin="0,0,40,0"
Height="32"
CornerRadius="5"
- BorderBrush="{DynamicResource AppBorderBrush}"
BorderThickness="1"
- Background="{DynamicResource AppBadgeBackground}"
ClipToBounds="True"
automation:AutomationProperties.AccessibilityView="Control">
@@ -611,66 +643,120 @@
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
_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/Pages/SettingsPages/ManagersHomepage.axaml.cs b/src/UniGetUI.Avalonia/Views/Pages/SettingsPages/ManagersHomepage.axaml.cs
index 5e903b8d23..4719bfe73c 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;
@@ -9,6 +10,7 @@
using UniGetUI.Core.Tools;
using UniGetUI.PackageEngine;
using UniGetUI.PackageEngine.Interfaces;
+using AvaloniaPath = Avalonia.Controls.Shapes.Path;
using CoreSettings = UniGetUI.Core.SettingsEngine.Settings;
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()
@@ -51,11 +54,45 @@ public ManagersHomepage()
VerticalAlignment = VerticalAlignment.Center,
};
AutomationProperties.SetAccessibilityView(badgeText, AccessibilityView.Raw);
+
+ var badgeIcon = new Ellipse
+ {
+ Width = 12,
+ Height = 12,
+ };
+ var badgeGlyph = new AvaloniaPath
+ {
+ Width = 5,
+ Height = 5,
+ StrokeThickness = 1.5,
+ StrokeLineCap = PenLineCap.Round,
+ StrokeJoin = PenLineJoin.Round,
+ Stretch = Stretch.None,
+ HorizontalAlignment = HorizontalAlignment.Center,
+ VerticalAlignment = VerticalAlignment.Center,
+ };
+ 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);
@@ -64,6 +101,7 @@ public ManagersHomepage()
{
OnContent = "",
OffContent = "",
+ HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
};
AutomationProperties.SetName(toggle, manager.DisplayName);
@@ -72,14 +110,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 +146,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,37 +154,51 @@ 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;
+ glyph.RenderTransform = null;
if (!manager.IsEnabled())
{
bgKey = "WarningBannerBackground";
fgKey = "StatusWarningForeground";
label = CoreTools.Translate("Disabled");
+ glyph.Data = Geometry.Parse("M2.5,0.4 L2.5,2.1 M2.1,4.5 L2.9,4.5");
}
else if (manager.Status.Found)
{
bgKey = "StatusSuccessBackground";
fgKey = "StatusSuccessForeground";
label = CoreTools.Translate("Ready");
+ glyph.Data = Geometry.Parse("M0.5,2.6 L2,4.1 L4.5,0.9");
+ glyph.RenderTransform = new TranslateTransform(0, 0.5);
}
else
{
bgKey = "StatusErrorBackground";
fgKey = "StatusErrorForeground";
label = CoreTools.Translate("Not found");
+ glyph.Data = Geometry.Parse("M0.75,0.75 L4.25,4.25 M4.25,0.75 L0.75,4.25");
}
- badge.Background = LookupBrush(bgKey);
- text.Foreground = LookupBrush(fgKey);
+ IBrush background = LookupBrush(bgKey);
+ badge.Background = background;
+ icon.Fill = LookupBrush(fgKey);
+ glyph.Stroke = background;
+ text.Foreground = LookupBrush("TextFillColorPrimaryBrush");
text.Text = label;
// Bake state into Name so VoiceOver always announces it on macOS
AutomationProperties.SetName(toggle, $"{manager.DisplayName}, {label}");
diff --git a/src/UniGetUI.Avalonia/Views/SidebarView.axaml b/src/UniGetUI.Avalonia/Views/SidebarView.axaml
index 61e47a35be..7dc13927d9 100644
--- a/src/UniGetUI.Avalonia/Views/SidebarView.axaml
+++ b/src/UniGetUI.Avalonia/Views/SidebarView.axaml
@@ -13,7 +13,7 @@
x:DataType="vm:SidebarViewModel"
MinWidth="64">
-
+
-
+
@@ -234,5 +233,16 @@
+
+
+
diff --git a/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs b/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs
index 5bdf240b8e..1f1dcaf566 100644
--- a/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs
@@ -4,8 +4,11 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
+using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Styling;
+using Avalonia.Threading;
+using UniGetUI.Avalonia.Infrastructure;
using UniGetUI.Avalonia.ViewModels;
namespace UniGetUI.Avalonia.Views;
@@ -13,6 +16,12 @@ namespace UniGetUI.Avalonia.Views;
public partial class SidebarView : BaseView
{
private bool _lastNavItemSelectionWasAuto;
+ private bool _isMoreFlyoutOpen;
+ private CancellationTokenSource? _pillAnimationCancellation;
+ private int _pillAnimationVersion;
+
+ private const double PillHeight = 16d;
+ private static readonly TimeSpan PillAnimationDuration = TimeSpan.FromMilliseconds(400);
///
/// Whether the nav item text labels are shown. False renders an icon-only rail; true renders the
@@ -31,6 +40,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 +68,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
@@ -67,6 +88,7 @@ private void SyncListBoxSelection(PageType page)
_ => null,
};
_lastNavItemSelectionWasAuto = false;
+ QueueSelectionPillUpdate(animate: NavigationSelectionPill.IsVisible);
}
private void NavListBox_SelectionChanged(object? sender, SelectionChangedEventArgs e)
@@ -82,8 +104,9 @@ 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;
+ QueueSelectionPillUpdate(animate: true, item);
FlyoutBase.ShowAttachedFlyout(item);
return;
}
@@ -115,4 +138,152 @@ public void FocusSelectedItem()
else
NavListBox.Focus();
}
+
+ private void QueueSelectionPillUpdate(bool animate, ListBoxItem? targetItem = null)
+ {
+ Dispatcher.UIThread.Post(
+ () =>
+ {
+ if ((targetItem ?? NavListBox.SelectedItem ?? FooterNavListBox.SelectedItem) is ListBoxItem item)
+ {
+ MoveSelectionPill(item, animate);
+ }
+ else
+ {
+ _pillAnimationCancellation?.Cancel();
+ NavigationSelectionPill.IsVisible = false;
+ }
+ },
+ DispatcherPriority.Render);
+ }
+
+ private void MoveSelectionPill(ListBoxItem item, bool animate)
+ {
+ Point? itemPosition = item.TranslatePoint(default, SidebarLayout);
+ if (itemPosition is null || item.Bounds.Height <= 0)
+ return;
+
+ double targetTop = itemPosition.Value.Y + ((item.Bounds.Height - PillHeight) / 2d);
+ double targetLeft = itemPosition.Value.X;
+
+ _pillAnimationCancellation?.Cancel();
+ _pillAnimationCancellation?.Dispose();
+ _pillAnimationCancellation = null;
+
+ Canvas.SetLeft(NavigationSelectionPill, targetLeft);
+
+ if (!NavigationSelectionPill.IsVisible || !animate || MotionPreference.ReducedMotion)
+ {
+ Canvas.SetTop(NavigationSelectionPill, targetTop);
+ NavigationSelectionPill.Height = PillHeight;
+ NavigationSelectionPill.IsVisible = true;
+ return;
+ }
+
+ double currentTop = Canvas.GetTop(NavigationSelectionPill);
+ if (double.IsNaN(currentTop))
+ currentTop = targetTop;
+
+ double currentBottom = currentTop + NavigationSelectionPill.Height;
+ double targetBottom = targetTop + PillHeight;
+ if (Math.Abs(currentTop - targetTop) < 0.5)
+ return;
+
+ bool movingDown = targetTop > currentTop;
+ _pillAnimationCancellation = new CancellationTokenSource();
+ int version = ++_pillAnimationVersion;
+ _ = AnimatePillEdgesAsync(
+ currentTop,
+ currentBottom,
+ targetTop,
+ targetBottom,
+ movingDown,
+ version,
+ _pillAnimationCancellation.Token);
+ }
+
+ private async Task AnimatePillEdgesAsync(
+ double startTop,
+ double startBottom,
+ double targetTop,
+ double targetBottom,
+ bool movingDown,
+ int version,
+ CancellationToken cancellationToken)
+ {
+ var animation = new Animation
+ {
+ Duration = PillAnimationDuration,
+ FillMode = FillMode.Forward,
+ };
+
+ const int sampleCount = 20;
+ for (int sample = 0; sample <= sampleCount; sample++)
+ {
+ double progress = (double)sample / sampleCount;
+ double lead = EvaluateBezier(progress, 0d, 0d, 0d, 1d);
+ double trail = EvaluateBezier(progress, 0.5d, 0d, 0.2d, 1d);
+ double topProgress = movingDown ? trail : lead;
+ double bottomProgress = movingDown ? lead : trail;
+ double top = Lerp(startTop, targetTop, topProgress);
+ double bottom = Lerp(startBottom, targetBottom, bottomProgress);
+
+ animation.Children.Add(new KeyFrame
+ {
+ Cue = new Cue(progress),
+ Setters =
+ {
+ new Setter(Canvas.TopProperty, top),
+ new Setter(Layoutable.HeightProperty, Math.Max(1d, bottom - top)),
+ },
+ });
+ }
+
+ try
+ {
+ await animation.RunAsync(NavigationSelectionPill, cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ return;
+ }
+
+ if (version != _pillAnimationVersion || cancellationToken.IsCancellationRequested)
+ return;
+
+ Canvas.SetTop(NavigationSelectionPill, targetTop);
+ NavigationSelectionPill.Height = PillHeight;
+ }
+
+ private static double Lerp(double start, double end, double progress)
+ => start + ((end - start) * progress);
+
+ private static double EvaluateBezier(
+ double progress,
+ double control1X,
+ double control1Y,
+ double control2X,
+ double control2Y)
+ {
+ double low = 0d;
+ double high = 1d;
+ for (int i = 0; i < 12; i++)
+ {
+ double parameter = (low + high) / 2d;
+ if (BezierCoordinate(parameter, control1X, control2X) < progress)
+ low = parameter;
+ else
+ high = parameter;
+ }
+
+ return BezierCoordinate((low + high) / 2d, control1Y, control2Y);
+ }
+
+ private static double BezierCoordinate(double parameter, double control1, double control2)
+ {
+ double inverse = 1d - parameter;
+ return (3d * inverse * inverse * parameter * control1)
+ + (3d * inverse * parameter * parameter * control2)
+ + (parameter * parameter * parameter);
+ }
}
diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml
index 849b4a0b68..2c5f5fe86f 100644
--- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml
+++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml
@@ -15,6 +15,9 @@
Name="ABSTRACT_PAGE">
+
+
@@ -847,9 +855,7 @@
Grid.Column="1"
Grid.ColumnSpan="2"
CornerRadius="8"
- BorderBrush="{DynamicResource AppBorderBrush}"
BorderThickness="1"
- Background="{DynamicResource MicaPageBackground}"
ClipToBounds="True">
@@ -918,9 +924,9 @@