|
@@ -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
|
|
|
}
|