浏览代码

Refactor: replace `ReactiveUI` with `R3`. Update bindings, commands, and properties across views and view models. Remove redundant `ReactiveUI` dependencies, streamline logic, and enhance resource management with disposables.

Ruben 3 月之前
父节点
当前提交
587bfa35f1

+ 1 - 4
src/PicView.Avalonia.MacOS/Program.cs

@@ -1,8 +1,5 @@
 using Avalonia;
 using Avalonia.Controls;
-using Avalonia.Controls.ApplicationLifetimes;
-using Avalonia.Platform;
-using Avalonia.ReactiveUI;
 
 namespace PicView.Avalonia.MacOS;
 
@@ -22,7 +19,7 @@ internal class Program
 #if DEBUG
             .LogToTrace()
 #endif
-            .UseReactiveUI()
+            .UseR3()
             .UseAvaloniaNative()
             .UseSkia()
             .With(new SkiaOptions

+ 16 - 6
src/PicView.Avalonia.MacOS/Views/BatchResizeWindow.axaml.cs

@@ -3,22 +3,27 @@ using Avalonia.Input;
 using PicView.Avalonia.UI;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Core.Localization;
+using R3;
 
 namespace PicView.Avalonia.MacOS.Views;
 
-public partial class BatchResizeWindow : Window
+public partial class BatchResizeWindow : Window, IDisposable
 {
+    private readonly CompositeDisposable _disposables = new();
     public BatchResizeWindow()
     {
         InitializeComponent();
         GenericWindowHelper.GenericWindowInitialize(this, TranslationManager.Translation.BatchResize + " - PicView");
         Loaded += delegate
         {
-            ClientSizeProperty.Changed.Subscribe(size =>
-            {
-                Height = 500;
-                WindowResizing.HandleWindowResize(this, size);
-            });
+            ClientSizeProperty.Changed.ToObservable()
+                .ObserveOn(UIHelper.GetFrameProvider)
+                .Subscribe(size =>
+                {
+                    Height = 500;
+                    WindowResizing.HandleWindowResize(this, size);
+                })
+                .AddTo(_disposables);
         };
     }
 
@@ -29,4 +34,9 @@ public partial class BatchResizeWindow : Window
         var hostWindow = (Window)VisualRoot;
         hostWindow?.BeginMoveDrag(e);
     }
+    public void Dispose()
+    {
+        Disposable.Dispose(_disposables);
+        GC.SuppressFinalize(this);
+    } 
 }

+ 14 - 5
src/PicView.Avalonia.MacOS/Views/EffectsWindow.axaml.cs

@@ -2,13 +2,16 @@ using Avalonia.Controls;
 using Avalonia.Input;
 using Avalonia.Media;
 using PicView.Avalonia.Input;
+using PicView.Avalonia.UI;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Core.Localization;
+using R3;
 
 namespace PicView.Avalonia.MacOS.Views;
 
-public partial class EffectsWindow : Window
+public partial class EffectsWindow : Window, IDisposable
 {
+    private readonly CompositeDisposable _disposables = new();
     public EffectsWindow()
     {
         InitializeComponent();
@@ -21,10 +24,10 @@ public partial class EffectsWindow : Window
             MinWidth = MaxWidth = Bounds.Width;
             Title = $"{TranslationManager.Translation.Effects} - PicView";
             
-            ClientSizeProperty.Changed.Subscribe(size =>
-            {
-                WindowResizing.HandleWindowResize(this, size);
-            });
+            ClientSizeProperty.Changed.ToObservable()
+                .ObserveOn(UIHelper.GetFrameProvider)
+                .Subscribe(size => { WindowResizing.HandleWindowResize(this, size); })
+                .AddTo(_disposables);
         };
         KeyDown += (_, e) =>
         {
@@ -44,4 +47,10 @@ public partial class EffectsWindow : Window
         var hostWindow = (Window)VisualRoot;
         hostWindow?.BeginMoveDrag(e);
     }
+    
+    public void Dispose()
+    {
+        Disposable.Dispose(_disposables);
+        GC.SuppressFinalize(this);
+    }
 }

+ 2 - 4
src/PicView.Avalonia.MacOS/Views/MacMainWindow.axaml.cs

@@ -22,10 +22,8 @@ public partial class MacMainWindow : Window
         Loaded += delegate
         {
             // Keep window position when resizing
-            ClientSizeProperty.Changed.Subscribe(size =>
-            {
-                WindowResizing.HandleWindowResize(this, size);
-            });
+            ClientSizeProperty.Changed.ToObservable()
+                .Subscribe(size => { WindowResizing.HandleWindowResize(this, size); });
             if (DataContext is not MainViewModel vm)
             {
                 return;

+ 0 - 2
src/PicView.Avalonia.Win32/Program.cs

@@ -1,6 +1,5 @@
 using Avalonia;
 using Avalonia.Controls;
-using Avalonia.ReactiveUI;
 
 namespace PicView.Avalonia.Win32;
 
@@ -21,7 +20,6 @@ internal class Program
 #if DEBUG
             .LogToTrace()
 #endif
-            .UseReactiveUI()
             .UseR3()
             .With(new SkiaOptions
             {

+ 13 - 2
src/PicView.Avalonia.Win32/Views/BatchResizeResizeWindow.axaml.cs

@@ -6,11 +6,13 @@ using Avalonia.Media;
 using PicView.Avalonia.UI;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Core.Localization;
+using R3;
 
 namespace PicView.Avalonia.Win32.Views;
 
-public partial class BatchResizeWindow : Window
+public partial class BatchResizeWindow : Window, IDisposable
 {
+    private readonly CompositeDisposable _disposables = new();
     public BatchResizeWindow()
     {
         InitializeComponent();
@@ -49,7 +51,10 @@ public partial class BatchResizeWindow : Window
         GenericWindowHelper.GenericWindowInitialize(this, TranslationManager.Translation.BatchResize + " - PicView");
         Loaded += delegate
         {
-            ClientSizeProperty.Changed.Subscribe(size => { WindowResizing.HandleWindowResize(this, size); });
+            ClientSizeProperty.Changed.ToObservable()
+                .ObserveOn(UIHelper.GetFrameProvider)
+                .Subscribe(size => { WindowResizing.HandleWindowResize(this, size); })
+                .AddTo(_disposables);
         };
     }
 
@@ -67,4 +72,10 @@ public partial class BatchResizeWindow : Window
     private void Close(object? sender, RoutedEventArgs e) => Close();
 
     private void Minimize(object? sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
+
+    public void Dispose()
+    {
+       Disposable.Dispose(_disposables);
+       GC.SuppressFinalize(this);
+    }
 }

+ 16 - 8
src/PicView.Avalonia.Win32/Views/EffectsWindow.axaml.cs

@@ -6,11 +6,13 @@ using Avalonia.Media;
 using PicView.Avalonia.UI;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Core.Localization;
+using R3;
 
 namespace PicView.Avalonia.Win32.Views;
 
-public partial class EffectsWindow : Window
+public partial class EffectsWindow : Window, IDisposable
 {
+    private readonly CompositeDisposable _disposables = new();
     public EffectsWindow()
     {
         InitializeComponent();
@@ -46,14 +48,14 @@ public partial class EffectsWindow : Window
         GenericWindowHelper.GenericWindowInitialize(this, TranslationManager.Translation.Effects + " - PicView");
         Loaded += delegate
         {
-            ClientSizeProperty.Changed.Subscribe(size =>
+            ClientSizeProperty.Changed.ToObservable()
+                .ObserveOn(UIHelper.GetFrameProvider)
+                .Subscribe(size => { WindowResizing.HandleWindowResize(this, size); })
+                .AddTo(_disposables);
+            ClearEffectsItem.Click += delegate
             {
-                WindowResizing.HandleWindowResize(this, size);
-                ClearEffectsItem.Click += delegate
-                {
-                    EffectsView?.RemoveEffects();
-                };
-            });
+                EffectsView?.RemoveEffects();
+            };
         };
     }
 
@@ -68,4 +70,10 @@ public partial class EffectsWindow : Window
     private void Close(object? sender, RoutedEventArgs e) => Close();
 
     private void Minimize(object? sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
+    
+    public void Dispose()
+    {
+        Disposable.Dispose(_disposables);
+        GC.SuppressFinalize(this);
+    }
 }

+ 13 - 2
src/PicView.Avalonia.Win32/Views/ExifWindow.axaml.cs

@@ -6,11 +6,13 @@ using Avalonia.Media;
 using PicView.Avalonia.UI;
 using PicView.Avalonia.WindowBehavior;
 using PicView.Core.Localization;
+using R3;
 
 namespace PicView.Avalonia.Win32.Views;
 
-public partial class ExifWindow : Window
+public partial class ExifWindow : Window, IDisposable
 {
+    private readonly CompositeDisposable _disposables = new();
     public ExifWindow()
     {
         InitializeComponent();
@@ -86,7 +88,10 @@ public partial class ExifWindow : Window
         GenericWindowHelper.GenericWindowInitialize(this, TranslationManager.Translation.ImageInfo + " - PicView");
         Loaded += delegate
         {
-            ClientSizeProperty.Changed.Subscribe(size => { WindowResizing.HandleWindowResize(this, size); });
+            ClientSizeProperty.Changed.ToObservable()
+                .ObserveOn(UIHelper.GetFrameProvider)
+                .Subscribe(size => { WindowResizing.HandleWindowResize(this, size); })
+                .AddTo(_disposables);
         };
     }
 
@@ -101,4 +106,10 @@ public partial class ExifWindow : Window
     private void Close(object? sender, RoutedEventArgs e) => Close();
 
     private void Minimize(object? sender, RoutedEventArgs e) => WindowState = WindowState.Minimized;
+    
+    public void Dispose()
+    {
+        Disposable.Dispose(_disposables);
+        GC.SuppressFinalize(this);
+    }
 }

+ 3 - 4
src/PicView.Avalonia.Win32/Views/WinMainWindow.axaml.cs

@@ -35,10 +35,9 @@ public partial class WinMainWindow : Window
             }
 
             // Keep window position when resizing
-            ClientSizeProperty.Changed.Subscribe(size =>
-            {
-                WindowResizing.HandleWindowResize(this, size);
-            });
+            ClientSizeProperty.Changed.ToObservable()
+                .ObserveOn(_frameProvider)
+                .Subscribe(size => { WindowResizing.HandleWindowResize(this, size); });
             ScalingChanged += (_, _) =>
             {
                 ScreenHelper.UpdateScreenSize(this);

+ 2 - 1
src/PicView.Avalonia/CustomControls/KeybindTextBox.cs

@@ -9,6 +9,7 @@ using Avalonia.Threading;
 using PicView.Avalonia.Functions;
 using PicView.Avalonia.Input;
 using PicView.Core.Localization;
+using R3;
 
 namespace PicView.Avalonia.CustomControls;
 
@@ -45,7 +46,7 @@ public class KeybindTextBox : TextBox
 
     public KeybindTextBox()
     {
-        this.GetObservable(MethodNameProperty).Subscribe(_ => Text = GetFunctionKey());
+        this.GetObservable(MethodNameProperty).ToObservable().Subscribe(_ => Text = GetFunctionKey());
         if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
         {
             AddHandler(KeyUpEvent, KeyDownHandler, RoutingStrategies.Tunnel);

+ 0 - 99
src/PicView.Avalonia/Functions/FunctionsHelper.cs

@@ -1,99 +0,0 @@
-using System.Diagnostics;
-using System.Reactive;
-using PicView.Avalonia.UI;
-using ReactiveUI;
-
-namespace PicView.Avalonia.Functions;
-
-public static class FunctionsHelper
-{
-    /// <summary>
-    ///     Creates a ReactiveCommand from a Task with built-in error handling.
-    /// </summary>
-    /// <param name="task">The task to execute when the command is invoked.</param>
-    /// <param name="canExecute">An optional observable determining when the command can execute.</param>
-    /// <returns>A ReactiveCommand with configured error handling.</returns>
-    public static ReactiveCommand<Unit, Unit> CreateReactiveCommand(
-        Func<Task> task,
-        IObservable<bool>? canExecute = null)
-    {
-        var cmd = ReactiveCommand.CreateFromTask(task, canExecute);
-
-        cmd.ThrownExceptions
-            .Subscribe(ex =>
-            {
-                _ = TooltipHelper.ShowTooltipMessageAsync(ex.Message);
-                Debug.WriteLine($"Error in command: {ex}");
-            });
-
-        return cmd;
-    }
-
-    /// <summary>
-    ///     Creates a parameterized ReactiveCommand from a Task with built-in error handling.
-    /// </summary>
-    /// <typeparam name="TParam">The type of the parameter passed to the task.</typeparam>
-    /// <param name="task">The task to execute when the command is invoked, accepting a parameter.</param>
-    /// <param name="canExecute">An optional observable determining when the command can execute.</param>
-    /// <returns>A ReactiveCommand with configured error handling that accepts a parameter.</returns>
-    public static ReactiveCommand<TParam, Unit> CreateReactiveCommand<TParam>(
-        Func<TParam, Task> task,
-        IObservable<bool>? canExecute = null)
-    {
-        var cmd = ReactiveCommand.CreateFromTask(task, canExecute);
-
-        cmd.ThrownExceptions
-            .Subscribe(ex =>
-            {
-                _ = TooltipHelper.ShowTooltipMessageAsync(ex.Message);
-                Debug.WriteLine($"Error in command: {ex}");
-            });
-
-        return cmd;
-    }
-
-    /// <summary>
-    ///     Creates a ReactiveCommand from a synchronous action with built-in error handling.
-    /// </summary>
-    /// <param name="execute">The action to execute when the command is invoked.</param>
-    /// <param name="canExecute">An optional observable determining when the command can execute.</param>
-    /// <returns>A ReactiveCommand with configured error handling.</returns>
-    public static ReactiveCommand<Unit, Unit> CreateReactiveCommand(
-        Action execute,
-        IObservable<bool>? canExecute = null)
-    {
-        var cmd = ReactiveCommand.Create(execute, canExecute);
-
-        cmd.ThrownExceptions
-            .Subscribe(ex =>
-            {
-                _ = TooltipHelper.ShowTooltipMessageAsync(ex.Message);
-                Debug.WriteLine($"Error in command: {ex}");
-            });
-
-        return cmd;
-    }
-
-    /// <summary>
-    ///     Creates a parameterized ReactiveCommand from a synchronous action with built-in error handling.
-    /// </summary>
-    /// <typeparam name="TParam">The type of the parameter passed to the action.</typeparam>
-    /// <param name="execute">The action to execute when the command is invoked, accepting a parameter.</param>
-    /// <param name="canExecute">An optional observable determining when the command can execute.</param>
-    /// <returns>A ReactiveCommand with configured error handling that accepts a parameter.</returns>
-    public static ReactiveCommand<TParam, Unit> CreateReactiveCommand<TParam>(
-        Action<TParam> execute,
-        IObservable<bool>? canExecute = null)
-    {
-        var cmd = ReactiveCommand.Create(execute, canExecute);
-
-        cmd.ThrownExceptions
-            .Subscribe(ex =>
-            {
-                _ = TooltipHelper.ShowTooltipMessageAsync(ex.Message);
-                Debug.WriteLine($"Error in command: {ex}");
-            });
-
-        return cmd;
-    }
-}

+ 0 - 1
src/PicView.Avalonia/PicView.Avalonia.csproj

@@ -126,7 +126,6 @@
   <ItemGroup>
     <PackageReference Include="Avalonia" Version="11.3.2" />
     <PackageReference Include="Avalonia.Desktop" Version="11.3.2" />
-    <PackageReference Include="Avalonia.ReactiveUI" Version="11.3.2" />
     <!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
     <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.3.2" />
     <PackageReference Include="Avalonia.Svg.Skia" Version="11.2.7" />

+ 27 - 21
src/PicView.Avalonia/Views/BatchResizeView.axaml.cs

@@ -1,4 +1,3 @@
-using System.Reactive.Linq;
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.Documents;
@@ -15,7 +14,7 @@ using PicView.Core.FileHandling;
 using PicView.Core.ImageDecoding;
 using PicView.Core.Localization;
 using PicView.Core.Titles;
-using ReactiveUI;
+using R3;
 
 namespace PicView.Avalonia.Views;
 
@@ -25,6 +24,8 @@ public partial class BatchResizeView : UserControl
     private bool _isRunning;
 
     private CancellationTokenSource? _cancellationTokenSource;
+    
+    private readonly CompositeDisposable _disposables = new CompositeDisposable();
 
     public BatchResizeView()
     {
@@ -119,47 +120,52 @@ public partial class BatchResizeView : UserControl
 
             SourceFolderTextBox.Text = vm.PicViewer.FileInfo?.CurrentValue.DirectoryName ?? string.Empty;
             
-            this.WhenAny(x => x.SourceFolderTextBox.Text, x => x.Value)
-                .ObserveOn(RxApp.MainThreadScheduler)
+            Observable.EveryValueChanged(this, x => x.SourceFolderTextBox.Text, UIHelper.GetFrameProvider)
                 .Skip(1)
+                .Where(x => !string.IsNullOrWhiteSpace(x))
                 .Subscribe(_ =>
                 {
                     ResetButton.IsVisible = true;
                     CancelButton.IsVisible = false;
-                });
+                })
+                .AddTo(_disposables);
             
-            this.WhenAny(x => x.OutputFolderTextBox.Text, x => x.Value)
-                .ObserveOn(RxApp.MainThreadScheduler)
-                .Skip(2)
+            Observable.EveryValueChanged(this, x => x.OutputFolderTextBox.Text, UIHelper.GetFrameProvider)
+                .Skip(1)
+                .Where(x => !string.IsNullOrWhiteSpace(x))
                 .Subscribe(_ =>
                 {
                     ResetButton.IsVisible = true;
                     CancelButton.IsVisible = false;
-                });
+                })
+                .AddTo(_disposables);
             
-            this.WhenAnyValue(x => x.ConversionComboBox.SelectedItem,
-                    x => x.CompressionComboBox.SelectedItem,
-                    x => x.HeightResizeBox,
-                    x => x.WidthResizeBox,
-                    x => x.QualitySlider.Value,
-                    x => x.ThumbnailsComboBox.SelectedItem,
-                    x => x.IsQualityEnabledBox.IsChecked)
-                .ObserveOn(RxApp.MainThreadScheduler)
+            Observable.EveryValueChanged(this, x => x.ConversionComboBox.SelectedItem, UIHelper.GetFrameProvider)
                 .Skip(1)
                 .Subscribe(_ =>
                 {
                     ResetButton.IsVisible = true;
                     CancelButton.IsVisible = false;
-                });
+                })
+                .AddTo(_disposables);
             
-            this.WhenAnyValue(x => x.ResizeComboBox.SelectedItem)
-                .ObserveOn(RxApp.MainThreadScheduler)
+            Observable.EveryValueChanged(this, x => x.CompressionComboBox.SelectedItem, UIHelper.GetFrameProvider)
                 .Skip(1)
                 .Subscribe(_ =>
                 {
                     ResetButton.IsVisible = true;
                     CancelButton.IsVisible = false;
-                });
+                })
+                .AddTo(_disposables);
+            
+            Observable.EveryValueChanged(this, x => x.HeightResizeBox, UIHelper.GetFrameProvider)
+                .Skip(1)
+                .Subscribe(_ =>
+                {
+                    ResetButton.IsVisible = true;
+                    CancelButton.IsVisible = false;
+                })
+                .AddTo(_disposables);
         };
     }
 

+ 9 - 9
src/PicView.Avalonia/Views/FileAssociationsView.axaml.cs

@@ -94,7 +94,7 @@ public partial class FileAssociationsView : UserControl
                 Tag = "group",
                 Name = fileTypeGroup.Name,
                 IsThreeState = true,
-                IsChecked = fileTypeGroup.IsSelected
+                IsChecked = fileTypeGroup.IsSelected.CurrentValue
             };
                 
             var groupTextBlock = new TextBlock
@@ -119,7 +119,7 @@ public partial class FileAssociationsView : UserControl
 
                 foreach (var fileType in fileTypeGroup.FileTypes)
                 {
-                    fileType.IsSelected = isChecked;
+                    fileType.IsSelected.Value = isChecked;
                 }
                 UpdateCheckBoxesFromViewModel();
             };
@@ -131,7 +131,7 @@ public partial class FileAssociationsView : UserControl
                 {
                     Classes = { "altHover", "x", "changeColor" },
                     Tag = fileType.Extension,
-                    IsChecked = fileType.IsSelected,
+                    IsChecked = fileType.IsSelected.CurrentValue,
                     IsThreeState = true
                 };
                     
@@ -155,7 +155,7 @@ public partial class FileAssociationsView : UserControl
                 fileCheckBox.IsCheckedChanged += delegate
                 {
                     // Update the model - important to handle null state correctly
-                    fileType.IsSelected = fileCheckBox.IsChecked;
+                    fileType.IsSelected.Value = fileCheckBox.IsChecked;
     
                     // Now update the group checkbox state
                     UpdateGroupCheckboxState(fileTypeGroup);
@@ -165,7 +165,7 @@ public partial class FileAssociationsView : UserControl
                 Observable.EveryValueChanged(fileType, x => x.IsSelected, UIHelper.GetFrameProvider)
                     .Subscribe( isSelected =>
                     {
-                        fileCheckBox.IsChecked = isSelected;
+                        fileCheckBox.IsChecked = isSelected.CurrentValue;
                     })
                     .AddTo(_disposables);
                     
@@ -173,7 +173,7 @@ public partial class FileAssociationsView : UserControl
                 Observable.EveryValueChanged(fileType, x => x.IsVisible, UIHelper.GetFrameProvider)
                     .Subscribe(isVisible =>
                     {
-                        fileCheckBox.IsVisible = isVisible;
+                        fileCheckBox.IsVisible = isVisible.CurrentValue;
                     })
                     .AddTo(_disposables);
             }
@@ -229,7 +229,7 @@ public partial class FileAssociationsView : UserControl
             groupCheckbox.IsChecked = null;
             
         // Update the ViewModel
-        group.IsSelected = groupCheckbox.IsChecked;
+        group.IsSelected.Value = groupCheckbox.IsChecked;
     }
 
     private static bool IsCheckboxInGroup(CheckBox checkbox, FileTypeGroup group)
@@ -264,9 +264,9 @@ public partial class FileAssociationsView : UserControl
                     
             foreach (var fileType in group.FileTypes)
             {
-                if (!fileType.IsSelected.HasValue)
+                if (!fileType.IsSelected.CurrentValue.HasValue)
                     anySelected = null;
-                else if (!fileType.IsSelected.Value)
+                else if (!fileType.IsSelected.CurrentValue.Value)
                     allSelected = false;
                 else
                     anySelected = true;

+ 4 - 4
src/PicView.Core/FileAssociations/FileAssociationProcessor.cs

@@ -93,7 +93,7 @@ public static class FileAssociationProcessor
             {
                 foreach (var fileType in group.FileTypes)
                 {
-                    if (!fileType.IsSelected.HasValue)
+                    if (!fileType.IsSelected.CurrentValue.HasValue)
                     {
                         continue; // Skip null selections
                     }
@@ -111,7 +111,7 @@ public static class FileAssociationProcessor
                                 cleanExt = "." + cleanExt;
                             }
 
-                            if (fileType.IsSelected.Value)
+                            if (fileType.IsSelected.CurrentValue.Value)
                             {
                                 // Add to association list
                                 instructions.ExtensionsToAssociate.Add(new AssociationItem
@@ -165,7 +165,7 @@ public static class FileAssociationProcessor
         {
             foreach (var fileType in group.FileTypes)
             {
-                if (!fileType.IsSelected.HasValue)
+                if (!fileType.IsSelected.CurrentValue.HasValue)
                 {
                     continue;
                 }
@@ -182,7 +182,7 @@ public static class FileAssociationProcessor
                             cleanExt = "." + cleanExt;
                         }
 
-                        if (fileType.IsSelected.Value)
+                        if (fileType.IsSelected.CurrentValue.Value)
                         {
                             await FileAssociationManager.AssociateFile(cleanExt, fileType.Description);
                         }

+ 9 - 8
src/PicView.Core/FileAssociations/FileTypeGroup.cs

@@ -1,23 +1,24 @@
 using System.Collections.ObjectModel;
-using ReactiveUI;
+using R3;
 
 namespace PicView.Core.FileAssociations;
 
-public class FileTypeGroup : ReactiveObject
+public class FileTypeGroup : IDisposable
 {
     public string Name { get; set; }
     public ObservableCollection<FileTypeItem> FileTypes { get; }
 
-    public bool? IsSelected
-    {
-        get;
-        set => this.RaiseAndSetIfChanged(ref field, value);
-    }
+    public BindableReactiveProperty<bool?> IsSelected { get; } = new();
 
     public FileTypeGroup(string name, IEnumerable<FileTypeItem> fileTypes, bool? isSelected = true)
     {
         Name = name;
         FileTypes = new ObservableCollection<FileTypeItem>(fileTypes);
-        IsSelected = isSelected;
+        IsSelected.Value = isSelected;
+    }
+
+    public void Dispose()
+    {
+        Disposable.Dispose();
     }
 }

+ 10 - 13
src/PicView.Core/FileAssociations/FileTypeItem.cs

@@ -1,30 +1,27 @@
-using ReactiveUI;
+using R3;
 
 namespace PicView.Core.FileAssociations;
 
-public class FileTypeItem : ReactiveObject
+public class FileTypeItem : IDisposable
 {
     public string Description { get; }
     public string[] Extensions { get; }
     
     public string Extension => string.Join(", ", Extensions);
 
-    public bool? IsSelected
-    {
-        get;
-        set => this.RaiseAndSetIfChanged(ref field, value);
-    }
+    public BindableReactiveProperty<bool?> IsSelected { get; } = new();
 
-    public bool IsVisible
-    {
-        get;
-        set => this.RaiseAndSetIfChanged(ref field, value);
-    } = true;
+    public BindableReactiveProperty<bool> IsVisible { get; } = new(true);
 
     public FileTypeItem(string description, string[] extensions, bool? isSelected = true)
     {
         Description = description;
         Extensions = extensions;
-        IsSelected = isSelected;
+        IsSelected.Value = isSelected;
+    }
+
+    public void Dispose()
+    {
+        Disposable.Dispose(IsSelected, IsVisible);
     }
 }

+ 0 - 1
src/PicView.Core/PicView.Core.csproj

@@ -27,7 +27,6 @@
   <ItemGroup>
     <PackageReference Include="Magick.NET-Q8-OpenMP-x64" Version="14.6.0" />
     <PackageReference Include="R3" Version="1.3.0" />
-    <PackageReference Include="ReactiveUI" Version="20.4.1" />
     <PackageReference Include="SharpCompress" Version="0.40.0" />
     <PackageReference Include="ZLinq" Version="1.4.12" />
     <PackageReference Include="ZLinq.FileSystem" Version="1.4.12" />

+ 11 - 11
src/PicView.Core/ViewModels/FileAssociationsViewModel.cs

@@ -116,10 +116,10 @@ public class FileAssociationsViewModel : IDisposable
     {
         foreach (var group in FileTypeGroups)
         {
-            group.IsSelected = group.IsSelected;
+            group.IsSelected.Value = group.IsSelected.CurrentValue;
             foreach (var fileType in group.FileTypes)
             {
-                fileType.IsSelected = fileType.IsSelected;
+                fileType.IsSelected.Value = fileType.IsSelected.Value;
             }
         }
     }
@@ -137,7 +137,7 @@ public class FileAssociationsViewModel : IDisposable
                 continue;
             }
 
-            group.IsSelected = defaultGroup.IsSelected;
+            group.IsSelected.Value = defaultGroup.IsSelected.CurrentValue;
 
             var fileTypes = group.FileTypes.ToArray();
             foreach (var fileType in fileTypes)
@@ -147,7 +147,7 @@ public class FileAssociationsViewModel : IDisposable
 
                 if (defaultType != null)
                 {
-                    fileType.IsSelected = defaultType.IsSelected;
+                    fileType.IsSelected.Value = defaultType.IsSelected.CurrentValue;
                 }
             }
         }
@@ -159,11 +159,11 @@ public class FileAssociationsViewModel : IDisposable
 
         foreach (var group in currentGroups)
         {
-            group.IsSelected = false;
+            group.IsSelected.Value = false;
             var fileTypes = group.FileTypes.ToArray();
             foreach (var fileType in fileTypes)
             {
-                fileType.IsSelected = false;
+                fileType.IsSelected.Value = false;
             }
         }
     }
@@ -177,7 +177,7 @@ public class FileAssociationsViewModel : IDisposable
             var fileTypes = group.FileTypes.ToArray();
             foreach (var fileType in fileTypes)
             {
-                if (!fileType.IsVisible)
+                if (!fileType.IsVisible.CurrentValue)
                 {
                     continue;
                 }
@@ -190,7 +190,7 @@ public class FileAssociationsViewModel : IDisposable
                     continue;
                 }
 
-                fileType.IsSelected = true;
+                fileType.IsSelected.Value = true;
             }
         }
     }
@@ -203,7 +203,7 @@ public class FileAssociationsViewModel : IDisposable
 
         foreach (var group in currentGroups)
         {
-            foreach (var fileType in group.FileTypes.Where(ft => ft.IsVisible))
+            foreach (var fileType in group.FileTypes.Where(ft => ft.IsVisible.CurrentValue))
             {
                 totalVisible++;
                 if (fileType.IsSelected == null)
@@ -217,9 +217,9 @@ public class FileAssociationsViewModel : IDisposable
 
         foreach (var group in currentGroups)
         {
-            foreach (var fileType in group.FileTypes.Where(ft => ft.IsVisible))
+            foreach (var fileType in group.FileTypes.Where(ft => ft.IsVisible.CurrentValue))
             {
-                fileType.IsSelected = setToUnchecked ? false : null;
+                fileType.IsSelected.Value = setToUnchecked ? false : null;
             }
         }
     }

+ 0 - 3
src/PicView.Tests/AvaloniaTest.cs

@@ -2,12 +2,10 @@
 using Avalonia.Headless;
 using Avalonia.Headless.XUnit;
 using Avalonia.Markup.Xaml.Styling;
-using Avalonia.ReactiveUI;
 using Avalonia.Themes.Simple;
 using PicView.Avalonia.MacOS;
 using PicView.Avalonia.MacOS.Views;
 using PicView.Avalonia.ViewModels;
-using PicView.Core.Config;
 
 namespace PicView.Tests;
 
@@ -16,7 +14,6 @@ public class AvaloniaTest
     [assembly: AvaloniaTestApplication(typeof(AvaloniaTest))]
     [AvaloniaFact]
     public static AppBuilder BuildAvaloniaApp() => AppBuilder.Configure<App>()
-        .UseReactiveUI()
         .LogToTrace()
         .UseHeadless(new AvaloniaHeadlessPlatformOptions());