diff --git a/src/UniGetUI.Avalonia/Extensions/ObservableSubscriptionExtensions.cs b/src/UniGetUI.Avalonia/Extensions/ObservableSubscriptionExtensions.cs new file mode 100644 index 000000000..07f7fa874 --- /dev/null +++ b/src/UniGetUI.Avalonia/Extensions/ObservableSubscriptionExtensions.cs @@ -0,0 +1,29 @@ +using System.Runtime.ExceptionServices; + +namespace UniGetUI.Avalonia.Extensions; + +internal static class ObservableSubscriptionExtensions +{ + public static IDisposable SubscribeValue(this IObservable source, Action onNext) + { + ArgumentNullException.ThrowIfNull(source); + ArgumentNullException.ThrowIfNull(onNext); + + return source.Subscribe(new ActionObserver(onNext)); + } + + private sealed class ActionObserver(Action onNext) : IObserver + { + public void OnCompleted() + { + } + + public void OnError(Exception error) + { + ExceptionDispatchInfo.Capture(error).Throw(); + } + + public void OnNext(T value) + => onNext(value); + } +} diff --git a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj index 7645d9cd3..4e6911414 100644 --- a/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj +++ b/src/UniGetUI.Avalonia/UniGetUI.Avalonia.csproj @@ -153,8 +153,8 @@ - - + + diff --git a/src/UniGetUI.Avalonia/Views/Controls/Settings/SecureCheckboxCard.cs b/src/UniGetUI.Avalonia/Views/Controls/Settings/SecureCheckboxCard.cs index 8bfda6971..89dcf79f7 100644 --- a/src/UniGetUI.Avalonia/Views/Controls/Settings/SecureCheckboxCard.cs +++ b/src/UniGetUI.Avalonia/Views/Controls/Settings/SecureCheckboxCard.cs @@ -3,6 +3,7 @@ using Avalonia.Controls; using Avalonia.Layout; using Avalonia.Media; +using UniGetUI.Avalonia.Extensions; using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Core.Logging; using UniGetUI.Core.SettingsEngine.SecureSettings; @@ -134,7 +135,7 @@ public SecureCheckboxCard() _checkbox.IsCheckedChanged += (s, e) => _ = _checkbox_Toggled(); this.GetObservable(IsEnabledProperty) - .Subscribe(enabled => _warningBlock.Opacity = enabled ? 1 : 0.2); + .SubscribeValue(enabled => _warningBlock.Opacity = enabled ? 1 : 0.2); // The Devolutions SettingsCard measures the Header with infinite width, so // TextWrapping alone won't constrain the warning block. We fix it by updating diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index 9d32f6041..af1f255ba 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -10,6 +10,7 @@ using Avalonia.Media; using Avalonia.Threading; using Avalonia.VisualTree; +using UniGetUI.Avalonia.Extensions; using UniGetUI.Avalonia.Infrastructure; using UniGetUI.Avalonia.ViewModels; using UniGetUI.Avalonia.Views.Pages; @@ -128,7 +129,7 @@ public MainWindow() Resized += (_, _) => _ = SaveGeometryAsync(); PositionChanged += (_, _) => _ = SaveGeometryAsync(); - this.GetObservable(WindowStateProperty).Subscribe(state => { _ = SaveGeometryAsync(); }); + this.GetObservable(WindowStateProperty).SubscribeValue(state => { _ = SaveGeometryAsync(); }); _trayService = new TrayService(this); _trayService.UpdateStatus(); @@ -290,7 +291,7 @@ private void UpdateOperationsPanelRow() // windows give the content full width (the hamburger + sliding flyout still provide nav). private void SetupResponsiveRail() => MainContentRoot.GetObservable(BoundsProperty) - .Subscribe(b => + .SubscribeValue(b => { if (b.Width <= 0) return; NavRail.IsVisible = b.Width >= 800; @@ -314,7 +315,7 @@ private void SetupTitleBar() // collapses to 0, which would clip the search box and hamburger. Use a fixed // title bar height in that state, and drop the traffic-light reservation // since the traffic lights aren't shown either. - this.GetObservable(WindowStateProperty).Subscribe(state => + this.GetObservable(WindowStateProperty).SubscribeValue(state => { if (state == WindowState.FullScreen) { @@ -354,7 +355,7 @@ private void SetupTitleBar() HamburgerPanel.Margin = new Thickness(10, 0, 8, 0); WindowButtons.IsVisible = true; MainContentRoot.Margin = new Thickness(0, 44, 0, 0); - this.GetObservable(WindowStateProperty).Subscribe(state => + this.GetObservable(WindowStateProperty).SubscribeValue(state => { UpdateMaximizeButtonState(state == WindowState.Maximized); }); @@ -372,7 +373,7 @@ private void SetupTitleBar() WindowButtons.IsVisible = !useNativeDecorations; MainContentRoot.Margin = new Thickness(0, 44, 0, 0); // Keep maximize icon in sync with window state - this.GetObservable(WindowStateProperty).Subscribe(state => + this.GetObservable(WindowStateProperty).SubscribeValue(state => { UpdateMaximizeButtonState(state == WindowState.Maximized); }); diff --git a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs index 32300e692..475a091bf 100644 --- a/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/SoftwarePages/AbstractPackagesPage.axaml.cs @@ -11,6 +11,7 @@ using Avalonia.Media; using Avalonia.Media.Transformation; using Avalonia.Threading; +using UniGetUI.Avalonia.Extensions; using UniGetUI.Avalonia.ViewModels.Pages; using UniGetUI.Avalonia.Views.Controls; using UniGetUI.Core.SettingsEngine; @@ -95,7 +96,7 @@ or nameof(PackagesPageViewModel.SortAscending)) // Using ColumnDefinition.WidthProperty fires every drag step, not just on release. FilteringPanel.ColumnDefinitions[0] .GetObservable(ColumnDefinition.WidthProperty) - .Subscribe(width => + .SubscribeValue(width => { if (_isOverlayMode || !ViewModel.IsFilterPaneOpen) return; if (width.IsAbsolute && width.Value >= 100) @@ -114,19 +115,19 @@ or nameof(PackagesPageViewModel.SortAscending)) // Responsive: switch between inline and overlay modes based on content width. FilteringPanel.GetObservable(BoundsProperty) - .Subscribe(bounds => OnFilteringPanelWidthChanged(bounds.Width)); + .SubscribeValue(bounds => OnFilteringPanelWidthChanged(bounds.Width)); // Responsive: collapse the menu bar to icon-only on narrow windows so the // toolbar buttons stay reachable instead of overflowing (mirrors WinUI). this.GetObservable(BoundsProperty) - .Subscribe(bounds => ViewModel.SetToolbarLabelsCollapsed(bounds.Width < 900)); + .SubscribeValue(bounds => ViewModel.SetToolbarLabelsCollapsed(bounds.Width < 900)); // Grid/icons views: stretch cards to fill each row then reflow (mirrors WinUI's // UniformGridLayout) instead of leaving wasted space to the right. GridViewItems.GetObservable(BoundsProperty) - .Subscribe(bounds => UpdateGridCardWidth(bounds.Width)); + .SubscribeValue(bounds => UpdateGridCardWidth(bounds.Width)); IconsViewItems.GetObservable(BoundsProperty) - .Subscribe(bounds => UpdateIconCardWidth(bounds.Width)); + .SubscribeValue(bounds => UpdateIconCardWidth(bounds.Width)); // Overlay backdrop dismisses the filter pane when tapped. FilterOverlayBackdrop.PointerPressed += (_, _) => ViewModel.IsFilterPaneOpen = false;