Răsfoiți Sursa

UI updates, viewmodel refactor #157

Ruben 6 luni în urmă
părinte
comite
1df61a264c

+ 13 - 10
src/PicView.Avalonia/Views/FileAssociationsView.axaml

@@ -18,7 +18,11 @@
 
     <Panel>
 
-        <uc:SpinWaiter IsVisible="{CompiledBinding AssociationsViewModel.IsProcessing, Mode=OneWay}" />
+        <uc:SpinWaiter
+            HorizontalAlignment="Center"
+            IsVisible="{CompiledBinding AssociationsViewModel.IsProcessing,
+                                        Mode=OneWay}"
+            VerticalAlignment="Center" />
 
         <StackPanel
             Focusable="True"
@@ -89,8 +93,8 @@
 
                 <Button
                     Classes="altHover"
-                    Padding="10,2,10,2"
-                    x:Name="SelectAllButton">
+                    Command="{CompiledBinding AssociationsViewModel.SelectAllCommand}"
+                    Padding="10,2,10,2">
                     <TextBlock
                         Classes="txt"
                         Foreground="{StaticResource SecondaryTextColor}"
@@ -106,6 +110,7 @@
 
                 <Button
                     Classes="altHover"
+                    Command="{CompiledBinding AssociationsViewModel.UnselectAllCommand}"
                     Padding="10,2,10,2"
                     x:Name="UnSelectAllButton">
                     <TextBlock
@@ -137,10 +142,10 @@
 
                 <Button
                     Classes="altHover"
+                    Command="{CompiledBinding AssociationsViewModel.ResetCommand}"
                     DockPanel.Dock="Left"
                     HorizontalAlignment="Left"
-                    Padding="20,10"
-                    x:Name="ResetButton">
+                    Padding="20,10">
                     <TextBlock
                         Classes="txt"
                         Foreground="{StaticResource SecondaryTextColor}"
@@ -170,7 +175,9 @@
                 </Button>
 
                 <Button
-                    Classes="errorHover"
+                    Background="Transparent"
+                    BorderThickness="0"
+                    Classes="errorHover mainBtn"
                     Command="{CompiledBinding AssociationsViewModel.UnassociateCommand}"
                     DockPanel.Dock="Right"
                     HorizontalAlignment="Right"
@@ -189,10 +196,6 @@
                     </StackPanel>
                 </Button>
 
-
-
-
-
             </DockPanel>
         </StackPanel>
     </Panel>

+ 1 - 95
src/PicView.Avalonia/Views/FileAssociationsView.axaml.cs

@@ -32,103 +32,9 @@ public partial class FileAssociationsView : UserControl
         {
             FilterBox.TextChanged += FilterBox_TextChanged;
             
-            SelectAllButton.Click += delegate
-            {
-                foreach (var checkBox in FileTypesContainer.Children.OfType<CheckBox>())
-                {
-                    if (checkBox is null)
-                    {
-                        continue;
-                    }
-
-                    if (!checkBox.IsVisible)
-                    {
-                        continue;
-                    }
-
-                    var tag = checkBox.Tag?.ToString();
-                    if (tag.StartsWith(".zip") || tag.StartsWith(".rar") || tag.StartsWith(".7z") || tag.StartsWith(".gzip")) 
-                    {
-                        checkBox.IsChecked = null;
-                    }
-                    else
-                    {
-                         checkBox.IsChecked = true;
-                    }
-                   
-                }
-            };
-            
-            UnSelectAllButton.Click += delegate
-            {
-                var checkBoxes = FileTypesContainer.Children.OfType<CheckBox>();
-                var enumerable = checkBoxes as CheckBox[] ?? checkBoxes.ToArray();
-                if (enumerable.All(x => x.IsChecked == null && x.IsVisible))
-                {
-                    foreach (var checkBox in enumerable)
-                    {
-                        checkBox.IsChecked = false;
-                    }
-                }
-                else
-                {
-                    foreach (var checkBox in enumerable.Where(x => x.IsVisible))
-                    {
-                        checkBox.IsChecked = null;
-                    }
-                }
-            };
-            
-            ResetButton.Click += delegate
-            {
-                if (DataContext is not MainViewModel { AssociationsViewModel: not null } vm)
-                {
-                    return;
-                }
-
-                // Use the view model to reset file types to default
-                vm.AssociationsViewModel.ResetFileTypesToDefault();
-                    
-                // Now update the UI checkboxes based on the reset data
-                var checkboxes = FileTypesContainer.Children.OfType<CheckBox>().ToArray();
-                    
-                foreach (var checkbox in checkboxes)
-                {
-                    ResetCheckboxStateFromViewModel(checkbox, vm.AssociationsViewModel);
-                }
-            };
-            
             InitializeCheckBoxesCollection();
         };
     }
-    
-    private static void ResetCheckboxStateFromViewModel(CheckBox checkbox, FileAssociationsViewModel viewModel)
-    {
-        var tag = checkbox.Tag?.ToString();
-        if (tag == "group") // group checkbox
-        {
-            var name = checkbox.Name;
-            var group = viewModel.FileTypeGroups.FirstOrDefault(g => g.Name?.Trim() == name);
-            if (group != null)
-            {
-                checkbox.IsChecked = group.IsSelected;
-            }
-        }
-        else if (!string.IsNullOrEmpty(tag)) // file type checkbox
-        {
-            foreach (var group in viewModel.FileTypeGroups)
-            {
-                foreach (var fileType in group.FileTypes)
-                {
-                    if (fileType.Extension.Contains(tag))
-                    {
-                        checkbox.IsChecked = fileType.IsSelected;
-                        break;
-                    }
-                }
-            }
-        }
-    }
         
     private void InitializeCheckBoxesCollection()
     {
@@ -315,7 +221,7 @@ public partial class FileAssociationsView : UserControl
         group.IsSelected = groupCheckbox.IsChecked;
     }
 
-    private bool IsCheckboxInGroup(CheckBox checkbox, FileTypeGroup group)
+    private static bool IsCheckboxInGroup(CheckBox checkbox, FileTypeGroup group)
     {
         // You can determine this by position in the UI or by extension tag
         var extension = checkbox.Tag?.ToString();

+ 256 - 76
src/PicView.Core/ViewModels/FileAssociationsViewModel.cs

@@ -8,36 +8,82 @@ using ReactiveUI;
 
 namespace PicView.Core.ViewModels;
 
+/// <summary>
+/// View model for managing file associations in PicView.
+/// Handles the binding between UI checkboxes and file type data.
+/// </summary>
 public class FileAssociationsViewModel : ReactiveObject
 {
     private readonly ReadOnlyObservableCollection<FileTypeGroup> _fileTypeGroups;
     private readonly SourceList<FileTypeGroup> _fileTypeGroupsList = new();
 
+    /// <summary>
+    /// Gets the read-only collection of file type groups that are available for association.
+    /// </summary>
     public ReadOnlyObservableCollection<FileTypeGroup> FileTypeGroups => _fileTypeGroups;
 
+    /// <summary>
+    /// Gets or sets the filter text used to search and filter file type groups and items.
+    /// </summary>
     public string? FilterText
     {
         get;
         set => this.RaiseAndSetIfChanged(ref field, value);
     } = string.Empty;
 
+    /// <summary>
+    /// Gets or sets a value indicating whether the view model is currently processing an operation.
+    /// Used to disable UI interaction during long-running tasks.
+    /// </summary>
     public bool IsProcessing
     {
         get;
         set => this.RaiseAndSetIfChanged(ref field, value);
     }
 
+    /// <summary>
+    /// Gets or sets the opacity value for the UI, used to visually indicate processing state.
+    /// </summary>
     public double Opacity
     {
         get;
         set => this.RaiseAndSetIfChanged(ref field, value);
     } = 1.0;
 
+    /// <summary>
+    /// Command to apply the selected file associations.
+    /// </summary>
     public ReactiveCommand<Unit, bool> ApplyCommand { get; }
+
+    /// <summary>
+    /// Command to clear the current filter text.
+    /// </summary>
     public ReactiveCommand<Unit, string> ClearFilterCommand { get; }
-    
+
+    /// <summary>
+    /// Command to unassociate all file types from the application.
+    /// </summary>
     public ReactiveCommand<Unit, Unit> UnassociateCommand { get; }
+
+    /// <summary>
+    /// Command to reset file type selections to their default state.
+    /// </summary>
+    public ReactiveCommand<Unit, Unit> ResetCommand { get; }
+
+    /// <summary>
+    /// Command to select all visible file types.
+    /// </summary>
+    public ReactiveCommand<Unit, Unit> SelectAllCommand { get; }
+
+    /// <summary>
+    /// Command to unselect all visible file types.
+    /// </summary>
+    public ReactiveCommand<Unit, Unit> UnselectAllCommand { get; }
     
+    /// <summary>
+    /// Initializes a new instance of the <see cref="FileAssociationsViewModel"/> class.
+    /// Sets up file type groups, commands, and filtering behavior.
+    /// </summary>
     public FileAssociationsViewModel()
     {
         // Create file type groups and populate with data
@@ -54,7 +100,7 @@ public class FileAssociationsViewModel : ReactiveObject
             .Bind(out _fileTypeGroups)
             .Subscribe();
 
-        // Canexecute for ApplyCommand
+        // CanExecute for commands
         var canExecute = this.WhenAnyValue(x => x.IsProcessing)
             .Select(processing => !processing);
             
@@ -73,37 +119,70 @@ public class FileAssociationsViewModel : ReactiveObject
 #endif
             });
         
-        UnassociateCommand = ReactiveCommand.CreateFromTask(async () =>
-        {
-            try
-            {
-                IsProcessing = true;
-                UnselectFileTypes();
-                await FileTypeHelper.SetFileAssociations(FileTypeGroups);
-            }
-            finally
-            {
-                IsProcessing = false;
-            }
-        }, canExecute);
+        UnassociateCommand = ReactiveCommand.CreateFromTask(
+            UnassociateFileAssociations, 
+            canExecute);
         
         UnassociateCommand.ThrownExceptions
             .Subscribe(ex => 
             {
                 IsProcessing = false;
-#if DEBUG
                 Debug.WriteLine($"Error in UnassociateCommand: {ex}");
-#endif
             });
             
         ClearFilterCommand = ReactiveCommand.Create(() => FilterText = string.Empty);
         
+        ResetCommand = ReactiveCommand.Create(ResetFileTypesToDefault, canExecute);
+        
+        ResetCommand.ThrownExceptions
+            .Subscribe(ex =>
+            {
+                Debug.WriteLine($"Error in ResetCommand: {ex}");
+            });
+        
+        SelectAllCommand = ReactiveCommand.Create(SelectAllFileTypes, canExecute);
+        
+        SelectAllCommand.ThrownExceptions
+            .Subscribe(ex =>
+            {
+                Debug.WriteLine($"Error in SelectAllCommand: {ex}");
+            });
+        
+        UnselectAllCommand = ReactiveCommand.Create(UnselectAllFileTypes, canExecute);
+        
+        UnselectAllCommand.ThrownExceptions
+            .Subscribe(ex =>
+            {
+                Debug.WriteLine($"Error in UnselectAllCommand: {ex}");
+            });
+        
         this.WhenAnyValue(x => x.IsProcessing).Subscribe(isProcessing =>
         {
             Opacity = isProcessing ? 0.3 : 1.0;
         });
     }
+
+    #region Initialize and filtering
+    
+    /// <summary>
+    /// Initializes file type groups by loading default file types from <see cref="FileTypeHelper"/>.
+    /// </summary>
+    private void InitializeFileTypes()
+    {
+        var groups = FileTypeHelper.GetFileTypes();
+        
+        _fileTypeGroupsList.Edit(list =>
+        {
+            list.Clear();
+            list.AddRange(groups);
+        });
+    }
     
+    /// <summary>
+    /// Builds a filter function for file type groups based on the provided filter text.
+    /// </summary>
+    /// <param name="filter">The filter text to search for in file type descriptions and extensions.</param>
+    /// <returns>A function that determines if a file type group should be visible based on the filter.</returns>
     private Func<FileTypeGroup, bool> BuildFilter(string? filter)
     {
         if (string.IsNullOrWhiteSpace(filter))
@@ -135,7 +214,15 @@ public class FileAssociationsViewModel : ReactiveObject
         };
     }
     
-    private void SyncUIStateToViewModel()
+    #endregion
+    
+    #region Selection
+    
+    /// <summary>
+    /// Updates all selection states to ensure changes are properly reflected in the UI.
+    /// This method forces property notifications to be sent for all selection states.
+    /// </summary>
+    private void UpdateSelection()
     {
         // Force property notifications to ensure all changes are processed
         foreach (var group in FileTypeGroups)
@@ -147,57 +234,31 @@ public class FileAssociationsViewModel : ReactiveObject
             }
         }
     }
-
-    private async Task<bool> ApplyFileAssociations()
-    {
-        try
-        {
-            IsProcessing = true;
-            
-            // Ensure all UI changes are synced to the ViewModel
-            SyncUIStateToViewModel();
-            
-            // Now process the associations
-            return await FileTypeHelper.SetFileAssociations(FileTypeGroups);
-        }
-        finally
-        {
-            IsProcessing = false;
-        }
-    }
-
-    private void InitializeFileTypes()
-    {
-        var groups = FileTypeHelper.GetFileTypes();
-        
-        _fileTypeGroupsList.Edit(list =>
-        {
-            list.Clear();
-            list.AddRange(groups);
-        });
-    }
     
-    public void ResetFileTypesToDefault()
+    /// <summary>
+    /// Resets all file type selections to their default state as defined by <see cref="FileTypeHelper.GetFileTypes"/>.
+    /// </summary>
+    /// <remarks>
+    /// This method uses snapshots of collections to avoid enumeration modification exceptions.
+    /// </remarks>
+    private void ResetFileTypesToDefault()
     {
         // Get fresh default file types
         var defaultGroups = FileTypeHelper.GetFileTypes();
-    
-        // Make a copy of the current groups to avoid enumeration issues
-        var currentGroups = _fileTypeGroups.ToArray();
-    
-        // Update selection states based on the defaults
+        
+        // Use snapshot to get current groups to avoid enumeration issues
+        var currentGroups = FileTypeGroups.ToArray();
+        
         foreach (var group in currentGroups)
         {
             var defaultGroup = defaultGroups.FirstOrDefault(g => g.Name == group.Name);
             if (defaultGroup == null)
-            {
                 continue;
-            }
-
-            // Update the group's selection state
+                
+            // Update the group selection state
             group.IsSelected = defaultGroup.IsSelected;
             
-            // Update each file type's selection state
+            // Create snapshot of file types to avoid enumeration issues
             var fileTypes = group.FileTypes.ToArray();
             foreach (var fileType in fileTypes)
             {
@@ -212,37 +273,156 @@ public class FileAssociationsViewModel : ReactiveObject
         }
     }
 
-    public void UnselectFileTypes()
+    /// <summary>
+    /// Unselects all file types by setting their selection state to false.
+    /// Used before unassociating all file types from the application.
+    /// </summary>
+    /// <remarks>
+    /// This method uses snapshots of collections to avoid enumeration modification exceptions.
+    /// </remarks>
+    private void UnselectFileTypes()
     {
-        // Get fresh default file types
-        var defaultGroups = FileTypeHelper.GetFileTypes();
-    
         // Make a copy of the current groups to avoid enumeration issues
-        var currentGroups = _fileTypeGroups.ToArray();
+        var currentGroups = FileTypeGroups.ToArray();
     
-        // Update selection states based on the defaults
+        // Update selection states to false for all items
         foreach (var group in currentGroups)
         {
-            var defaultGroup = defaultGroups.FirstOrDefault(g => g.Name == group.Name);
-            if (defaultGroup == null)
-            {
-                continue;
-            }
-            
             group.IsSelected = false;
             
-            // Update each file type's selection state
+            // Use snapshot of file types to avoid enumeration issues
             var fileTypes = group.FileTypes.ToArray();
             foreach (var fileType in fileTypes)
             {
-                var defaultType = defaultGroup.FileTypes.FirstOrDefault(dt => 
-                    dt.Description == fileType.Description);
-                
-                if (defaultType != null)
+                fileType.IsSelected = false;
+            }
+        }
+    }
+    
+    /// <summary>
+    /// Selects all visible file types except for archive types which are handled specially.
+    /// </summary>
+    /// <remarks>
+    /// This method uses snapshots of collections to avoid enumeration modification exceptions.
+    /// Archive types (.zip, .rar, etc.) are not automatically selected to avoid unwanted associations.
+    /// </remarks>
+    private void SelectAllFileTypes()
+    {
+        // Make a copy of the current groups to avoid enumeration issues
+        var currentGroups = FileTypeGroups.ToArray();
+    
+        // Update selection states to true for all items
+        foreach (var group in currentGroups)
+        {
+            // Use snapshot of file types to avoid enumeration issues
+            var fileTypes = group.FileTypes.ToArray();
+            foreach (var fileType in fileTypes)
+            {
+                if (!fileType.IsVisible)
+                {
+                    // We don't want to select hidden items
+                    continue;
+                }
+                // Only set archive types explicitly
+                if (fileType.Extension.StartsWith(".zip") ||
+                    fileType.Extension.StartsWith(".rar") ||
+                    fileType.Extension.StartsWith(".7z") ||
+                    fileType.Extension.StartsWith(".gzip")) 
                 {
-                    fileType.IsSelected = false;
+                    continue;
                 }
+                fileType.IsSelected = true;
             }
         }
     }
+
+    /// <summary>
+    /// Toggles selection state of all visible file types between indeterminate and unselected.
+    /// </summary>
+    /// <remarks>
+    /// This method uses snapshots of collections to avoid enumeration modification exceptions.
+    /// If all visible items are already in indeterminate state, they will be set to unselected.
+    /// Otherwise, all visible items will be set to indeterminate.
+    /// </remarks>
+    private void UnselectAllFileTypes()
+    {
+        // Make a copy of the current groups to avoid enumeration issues
+        var currentGroups = FileTypeGroups.ToArray();
+    
+        // Update selection states to true for all items
+        foreach (var group in currentGroups)
+        {
+            // Use snapshot of file types to avoid enumeration issues
+            var fileTypes = group.FileTypes.ToArray();
+            
+            // Toggle between indeterminate and false for visible items
+            if (fileTypes.All(x => x.IsSelected == null && x.IsVisible))
+            {
+                foreach (var checkBox in fileTypes)
+                {
+                    checkBox.IsSelected = false;
+                }
+            }
+            else
+            {
+                foreach (var checkBox in fileTypes.Where(x => x.IsVisible))
+                {
+                    checkBox.IsSelected = null;
+                }
+            }
+        }
+    }
+    
+    #endregion
+
+    #region Associations
+    
+    /// <summary>
+    /// Applies the current file type associations based on selection states.
+    /// </summary>
+    /// <returns>A <see cref="Task"/> representing the asynchronous operation, with a boolean result indicating success.</returns>
+    /// <remarks>
+    /// This method sets <see cref="IsProcessing"/> to true during execution, which disables UI interaction.
+    /// </remarks>
+    private async Task<bool> ApplyFileAssociations()
+    {
+        try
+        {
+            IsProcessing = true;
+            
+            // Ensure all UI changes are synced to the ViewModel
+            UpdateSelection();
+            
+            // Now process the associations
+            return await FileTypeHelper.SetFileAssociations(FileTypeGroups);
+        }
+        finally
+        {
+            IsProcessing = false;
+        }
+    }
+    
+    /// <summary>
+    /// Unassociates all file types from the application by setting all selection states to false
+    /// and then applying the associations.
+    /// </summary>
+    /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
+    /// <remarks>
+    /// This method sets <see cref="IsProcessing"/> to true during execution, which disables UI interaction.
+    /// </remarks>
+    private async Task UnassociateFileAssociations()
+    {
+        try
+        {
+            IsProcessing = true;
+            UnselectFileTypes();
+            await FileTypeHelper.SetFileAssociations(FileTypeGroups);
+        }
+        finally
+        {
+            IsProcessing = false;
+        }
+    }
+    
+    #endregion
 }