diff --git a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs
index 77c160a0e..ae2452a0c 100644
--- a/src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs
+++ b/src/UniGetUI.Avalonia/Infrastructure/AvaloniaOperationRegistry.cs
@@ -235,14 +235,6 @@ private static void ShowOperationSuccessNotification(AbstractOperation op)
if (OperatingSystem.IsMacOS() && MacOsNotificationBridge.ShowSuccess(op))
return;
-
- if (TryGetMainWindow() is not { } mainWindow)
- return;
-
- mainWindow.ShowRuntimeNotification(
- title,
- message,
- UniGetUI.Avalonia.Views.MainWindow.RuntimeNotificationLevel.Success);
}
private static void ShowOperationFailureNotification(AbstractOperation op)
diff --git a/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs b/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs
index 0c20c6432..a733c9d1b 100644
--- a/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs
+++ b/src/UniGetUI.Avalonia/ViewModels/SidebarViewModel.cs
@@ -47,21 +47,27 @@ partial void OnBundlesBadgeVisibleChanged(bool value)
private bool _installedIsLoading;
// ─── Pane open/closed ─────────────────────────────────────────────────────
+ // Starts collapsed: the pane is a floating overlay in every size class (see MainWindow's
+ // adaptive logic), so there is no persistent open-on-launch state.
[ObservableProperty]
- private bool isPaneOpen = !Settings.Get(Settings.K.CollapseNavMenuOnWideScreen);
+ private bool isPaneOpen;
+
+ // Only persist the open/closed choice as the "collapse on wide screen" preference
+ // while the pane is inline (Expanded mode). In the overlay modes used on smaller
+ // windows, opening/closing is transient and must not overwrite the saved preference.
+ // The MainWindow's adaptive logic keeps this in sync with the SplitView display mode.
+ public bool PersistPaneCollapsePreference { get; set; } = true;
partial void OnIsPaneOpenChanged(bool value)
{
- Settings.Set(Settings.K.CollapseNavMenuOnWideScreen, !value);
- OnPropertyChanged(nameof(PaneWidth));
+ if (PersistPaneCollapsePreference)
+ Settings.Set(Settings.K.CollapseNavMenuOnWideScreen, !value);
OnPropertyChanged(nameof(UpdatesBadgeExpandedVisible));
OnPropertyChanged(nameof(UpdatesBadgeCompactVisible));
OnPropertyChanged(nameof(BundlesBadgeExpandedVisible));
OnPropertyChanged(nameof(BundlesBadgeCompactVisible));
}
- public double PaneWidth => IsPaneOpen ? 250 : 64;
-
public bool UpdatesBadgeExpandedVisible => UpdatesBadgeVisible && IsPaneOpen;
public bool UpdatesBadgeCompactVisible => UpdatesBadgeVisible && !IsPaneOpen;
public bool BundlesBadgeExpandedVisible => BundlesBadgeVisible && IsPaneOpen;
diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml b/src/UniGetUI.Avalonia/Views/MainWindow.axaml
index df986043b..49dc4a93c 100644
--- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml
+++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml
@@ -16,17 +16,56 @@
Width="1450"
MinWidth="700"
MinHeight="500">
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+ ColumnDefinitions="Auto,*">
-
-
+
-
+
+
+
+
-
+
@@ -69,7 +111,7 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
index c04c16c2c..d8857bb09 100644
--- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
@@ -96,11 +96,14 @@ public MainWindow()
DataContext = new MainWindowViewModel();
InitializeComponent();
SetupTitleBar();
+ SetupResponsiveRail();
RestoreGeometry();
KeyDown += Window_KeyDown;
ViewModel.CurrentPageChanged += OnCurrentPageChanged;
+ // Title-bar back button: visible whenever there's somewhere to go back to (mirrors WinUI's TitleBar.IsBackButtonVisible).
+ ViewModel.CanGoBackChanged += (_, canGoBack) => BackButton.IsVisible = canGoBack;
ViewModel.PropertyChanged += OnViewModelPropertyChanged;
UpdateOperationsPanelRow();
@@ -216,6 +219,9 @@ private void Window_KeyDown(object? sender, KeyEventArgs e)
private void OnCurrentPageChanged(object? sender, PageType pageType)
{
+ // Like WinUI's NavigationView: picking a page collapses the sliding flyout back to the rail.
+ ViewModel.Sidebar.IsPaneOpen = false;
+
if (!_focusSidebarSelectionOnNextPageChange)
return;
@@ -227,6 +233,8 @@ private void OnCurrentPageChanged(object? sender, PageType pageType)
}, DispatcherPriority.Background);
}
+ private void BackButton_Click(object? sender, RoutedEventArgs e) => ViewModel.NavigateBack();
+
private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(MainWindowViewModel.OperationsPanelExpanded)
@@ -240,10 +248,10 @@ or nameof(MainWindowViewModel.OperationsPanelVisible))
// user's chosen size across collapse/expand.
private void UpdateOperationsPanelRow()
{
- if (MainContentGrid.RowDefinitions.Count < 3)
+ if (ContentRoot.RowDefinitions.Count < 3)
return;
- RowDefinition row = MainContentGrid.RowDefinitions[2];
+ RowDefinition row = ContentRoot.RowDefinitions[2];
if (ViewModel.OperationsPanelVisible && ViewModel.OperationsPanelExpanded)
{
row.MinHeight = 80;
@@ -258,6 +266,21 @@ private void UpdateOperationsPanelRow()
}
}
+ // ─── Navigation rail (responsive) ─────────────────────────────────────────
+ // The always-visible icon rail shows on roomy windows and collapses below 800px so narrow
+ // windows give the content full width (the hamburger + sliding flyout still provide nav).
+ private void SetupResponsiveRail()
+ => MainContentRoot.GetObservable(BoundsProperty)
+ .Subscribe(b =>
+ {
+ if (b.Width <= 0) return;
+ NavRail.IsVisible = b.Width >= 800;
+ });
+
+ // Light-dismiss: clicking outside the open flyout closes it (no darkening — the layer is transparent).
+ private void FlyoutDismiss_PointerPressed(object? sender, PointerPressedEventArgs e)
+ => ViewModel.Sidebar.IsPaneOpen = false;
+
private void SetupTitleBar()
{
if (OperatingSystem.IsMacOS())
@@ -278,8 +301,8 @@ private void SetupTitleBar()
{
TitleBarGrid.ClearValue(HeightProperty);
TitleBarGrid.Height = 44;
- MainContentGrid.ClearValue(MarginProperty);
- MainContentGrid.Margin = new Thickness(0, 44, 0, 0);
+ MainContentRoot.ClearValue(MarginProperty);
+ MainContentRoot.Margin = new Thickness(0, 44, 0, 0);
HamburgerPanel.Margin = new Thickness(10, 0, 8, 0);
}
else
@@ -288,7 +311,7 @@ private void SetupTitleBar()
{
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor) { AncestorType = typeof(Window) },
});
- MainContentGrid.Bind(MarginProperty, new Binding("WindowDecorationMargin")
+ MainContentRoot.Bind(MarginProperty, new Binding("WindowDecorationMargin")
{
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor) { AncestorType = typeof(Window) },
});
@@ -311,7 +334,7 @@ private void SetupTitleBar()
TitleBarGrid.Height = 44;
HamburgerPanel.Margin = new Thickness(10, 0, 8, 0);
WindowButtons.IsVisible = true;
- MainContentGrid.Margin = new Thickness(0, 44, 0, 0);
+ MainContentRoot.Margin = new Thickness(0, 44, 0, 0);
this.GetObservable(WindowStateProperty).Subscribe(state =>
{
UpdateMaximizeButtonState(state == WindowState.Maximized);
@@ -328,7 +351,7 @@ private void SetupTitleBar()
TitleBarGrid.Height = 44;
HamburgerPanel.Margin = new Thickness(10, 0, 8, 0);
WindowButtons.IsVisible = !useNativeDecorations;
- MainContentGrid.Margin = new Thickness(0, 44, 0, 0);
+ MainContentRoot.Margin = new Thickness(0, 44, 0, 0);
// Keep maximize icon in sync with window state
this.GetObservable(WindowStateProperty).Subscribe(state =>
{
diff --git a/src/UniGetUI.Avalonia/Views/SidebarView.axaml b/src/UniGetUI.Avalonia/Views/SidebarView.axaml
index 0ecc44b0a..81c5535dd 100644
--- a/src/UniGetUI.Avalonia/Views/SidebarView.axaml
+++ b/src/UniGetUI.Avalonia/Views/SidebarView.axaml
@@ -4,10 +4,12 @@
xmlns:automation="clr-namespace:Avalonia.Automation;assembly=Avalonia.Controls"
xmlns:vm="using:UniGetUI.Avalonia.ViewModels"
xmlns:controls="using:UniGetUI.Avalonia.Views.Controls"
+ xmlns:local="using:UniGetUI.Avalonia.Views"
xmlns:t="using:UniGetUI.Avalonia.MarkupExtensions"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
+ x:Name="SidebarRoot"
x:DataType="vm:SidebarViewModel"
MinWidth="64">
@@ -36,13 +38,16 @@
FontSize="16"
FontWeight="SemiBold"
VerticalAlignment="Center"
- IsVisible="{Binding IsPaneOpen}"/>
+ IsVisible="{Binding $parent[local:SidebarView].ShowLabels}"/>
+ Margin="4,0,0,2"/>
@@ -58,26 +63,15 @@
FontSize="16"
FontWeight="SemiBold"
VerticalAlignment="Center"
- IsVisible="{Binding IsPaneOpen}"/>
-
-
-
-
+ IsVisible="{Binding $parent[local:SidebarView].ShowLabels}"/>
-
+
@@ -91,8 +85,11 @@
+ Margin="4,0,0,2"/>
@@ -108,13 +105,16 @@
FontSize="16"
FontWeight="SemiBold"
VerticalAlignment="Center"
- IsVisible="{Binding IsPaneOpen}"/>
+ IsVisible="{Binding $parent[local:SidebarView].ShowLabels}"/>
+ Margin="4,0,0,2"/>
@@ -130,27 +130,14 @@
FontSize="16"
FontWeight="SemiBold"
VerticalAlignment="Center"
- IsVisible="{Binding IsPaneOpen}"/>
-
-
-
-
+ IsVisible="{Binding $parent[local:SidebarView].ShowLabels}"/>
-
+
@@ -177,7 +164,7 @@
+ IsVisible="{Binding $parent[local:SidebarView].ShowLabels}"/>
@@ -195,7 +182,7 @@
+ IsVisible="{Binding $parent[local:SidebarView].ShowLabels}"/>
@@ -211,7 +198,7 @@
+ IsVisible="{Binding $parent[local:SidebarView].ShowLabels}"/>
diff --git a/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs b/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs
index 2963ab9ef..286ef2f12 100644
--- a/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/SidebarView.axaml.cs
@@ -1,3 +1,4 @@
+using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using UniGetUI.Avalonia.ViewModels;
@@ -8,6 +9,20 @@ public partial class SidebarView : BaseView
{
private bool _lastNavItemSelectionWasAuto;
+ ///
+ /// Whether the nav item text labels are shown. False renders an icon-only rail; true renders the
+ /// full labeled pane. Decoupled from the view-model's pane state so the same view can be used both
+ /// as the always-visible rail and as the sliding flyout simultaneously.
+ ///
+ public static readonly StyledProperty ShowLabelsProperty =
+ AvaloniaProperty.Register(nameof(ShowLabels), defaultValue: true);
+
+ public bool ShowLabels
+ {
+ get => GetValue(ShowLabelsProperty);
+ set => SetValue(ShowLabelsProperty, value);
+ }
+
public SidebarView()
{
InitializeComponent();
diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml
index f09cc7714..a3b591a88 100644
--- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml
+++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml
@@ -51,6 +51,27 @@
+
+
+
+
+
+
@@ -103,6 +124,13 @@
with the package list when the pane is open, and collapse next to the Filters button when closed. -->
+
+
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+ 0
+
+
+
+
+
+
+
+
+
+
diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs
index 93101467c..d47a83296 100644
--- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs
+++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs
@@ -1,4 +1,6 @@
using Avalonia;
+using Avalonia.Animation;
+using Avalonia.Animation.Easings;
using Avalonia.Automation;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
@@ -7,6 +9,7 @@
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
+using Avalonia.Media.Transformation;
using Avalonia.Threading;
using UniGetUI.Avalonia.ViewModels.Pages;
using UniGetUI.Avalonia.Views.Controls;
@@ -25,6 +28,7 @@ public abstract partial class AbstractPackagesPage : UserControl,
private double _savedFilterPaneWidth = 220;
private bool _isOverlayMode;
private IDisposable? _sidePanelBgBinding;
+ private CancellationTokenSource? _filterPaneAnimCts;
protected AbstractPackagesPage(PackagesPageData data)
{
@@ -59,7 +63,7 @@ or nameof(PackagesPageViewModel.SortAscending))
if (args.PropertyName is nameof(PackagesPageViewModel.IsFilterPaneOpen))
{
SyncFiltersButtonName();
- UpdateFilterPaneColumn(ViewModel.IsFilterPaneOpen);
+ UpdateFilterPaneColumn(ViewModel.IsFilterPaneOpen, animate: true);
}
};
SyncFiltersButtonName();
@@ -146,6 +150,19 @@ or nameof(PackagesPageViewModel.SortAscending))
// Apply the initial filter-pane state (AXAML defaults to 220px open).
UpdateFilterPaneColumn(ViewModel.IsFilterPaneOpen);
+
+ // Attach the pane slide AFTER the initial state so launch doesn't animate. The pane slides
+ // via a compositor RenderTransform (clipped by FilteringPanel) instead of width-tweening the
+ // column, which reflowed the package list every frame and felt laggy.
+ SidePanel.Transitions = new Transitions
+ {
+ new TransformOperationsTransition
+ {
+ Property = Visual.RenderTransformProperty,
+ Duration = TimeSpan.FromMilliseconds(200),
+ Easing = new SplineEasing(0.1, 0.9, 0.2, 1.0),
+ }
+ };
}
// Recompute the grid-view card slot width: fit as many >=275px columns as possible,
@@ -394,12 +411,14 @@ private void OnFilteringPanelWidthChanged(double width)
UpdateFilterPaneColumn(ViewModel.IsFilterPaneOpen);
}
- private void UpdateFilterPaneColumn(bool open)
+ private void UpdateFilterPaneColumn(bool open, bool animate = false)
{
if (FilteringPanel.ColumnDefinitions.Count < 2) return;
if (_isOverlayMode)
{
+ _filterPaneAnimCts?.Cancel();
+
// Package list fills full width; filter pane and splitter take no space.
FilteringPanel.ColumnDefinitions[0].Width = new GridLength(0);
FilteringPanel.ColumnDefinitions[1].Width = new GridLength(0);
@@ -409,6 +428,8 @@ private void UpdateFilterPaneColumn(bool open)
SidePanel.ZIndex = 10;
SidePanel.Width = _savedFilterPaneWidth;
SidePanel.HorizontalAlignment = HorizontalAlignment.Left;
+ SidePanel.IsVisible = open;
+ SetSidePanelTransformInstant(0);
// Floating over content needs an opaque surface (the page surface is transparent under Mica).
_sidePanelBgBinding ??= SidePanel.Bind(Border.BackgroundProperty, this.GetResourceObservable("AppWindowBackground"));
@@ -428,13 +449,74 @@ private void UpdateFilterPaneColumn(bool open)
SidePanel.Background = null;
FilterOverlayBackdrop.IsVisible = false;
- FilteringPanel.ColumnDefinitions[0].Width = open
- ? new GridLength(_savedFilterPaneWidth)
- : new GridLength(0);
- FilteringPanel.ColumnDefinitions[1].Width = open
- ? new GridLength(4)
- : new GridLength(0);
+ if (animate)
+ {
+ AnimateInlineFilterPane(open);
+ }
+ else
+ {
+ _filterPaneAnimCts?.Cancel();
+ SidePanel.IsVisible = open;
+ SetSidePanelTransformInstant(open ? 0 : -_savedFilterPaneWidth);
+ FilteringPanel.ColumnDefinitions[0].Width = open ? new GridLength(_savedFilterPaneWidth) : new GridLength(0);
+ FilteringPanel.ColumnDefinitions[1].Width = open ? new GridLength(4) : new GridLength(0);
+ }
+ }
+ }
+
+ // Reserve/release the inline pane's column in one step (a single reflow, not a per-frame tween),
+ // and slide the pane itself in/out via a compositor RenderTransform (clipped by FilteringPanel) —
+ // tweening the column width reflowed the package list every frame and felt laggy. The toolbar
+ // buttons still slide via their MinWidth transition.
+ private async void AnimateInlineFilterPane(bool open)
+ {
+ _filterPaneAnimCts?.Cancel();
+ var cts = new CancellationTokenSource();
+ _filterPaneAnimCts = cts;
+
+ var col0 = FilteringPanel.ColumnDefinitions[0];
+ var col1 = FilteringPanel.ColumnDefinitions[1];
+
+ if (open)
+ {
+ // Reserve the column (one reflow), then slide the pane in from -width to 0.
+ col0.Width = new GridLength(_savedFilterPaneWidth);
+ col1.Width = new GridLength(4);
+ SidePanel.IsVisible = true;
+ SidePanel.RenderTransform = TranslateX(0);
}
+ else
+ {
+ // Reclaim the list width in one reflow NOW and float the pane over the list, then slide
+ // the floating pane out — otherwise the list sits beside an empty gap and only snaps to
+ // full width when the slide ends. Inline positioning is restored on the next open by
+ // UpdateFilterPaneColumn.
+ double w = _savedFilterPaneWidth;
+ Grid.SetColumnSpan(SidePanel, 3);
+ SidePanel.ZIndex = 10;
+ SidePanel.Width = w;
+ SidePanel.HorizontalAlignment = HorizontalAlignment.Left;
+ _sidePanelBgBinding ??= SidePanel.Bind(Border.BackgroundProperty, this.GetResourceObservable("AppWindowBackground"));
+ col0.Width = new GridLength(0);
+ col1.Width = new GridLength(0);
+
+ SidePanel.RenderTransform = TranslateX(-w);
+ try { await Task.Delay(210, cts.Token); }
+ catch (TaskCanceledException) { return; }
+ SidePanel.IsVisible = false;
+ }
+ }
+
+ private static ITransform TranslateX(double x)
+ => TransformOperations.Parse(string.Create(System.Globalization.CultureInfo.InvariantCulture, $"translateX({x}px)"));
+
+ // Set the pane transform without triggering the slide transition (for initial/mode-change states).
+ private void SetSidePanelTransformInstant(double translateX)
+ {
+ var transitions = SidePanel.Transitions;
+ SidePanel.Transitions = null;
+ SidePanel.RenderTransform = TranslateX(translateX);
+ SidePanel.Transitions = transitions;
}
// ─── Card overflow button (Grid / Icons view) ─────────────────────────────