using System.Collections.ObjectModel; using System.Diagnostics; using System.Reactive; using System.Reactive.Linq; using DynamicData; using PicView.Core.FileAssociations; using ReactiveUI; namespace PicView.Core.ViewModels; public class FileAssociationsViewModel : ReactiveObject { private readonly ReadOnlyObservableCollection _fileTypeGroups; private readonly SourceList _fileTypeGroupsList = new(); public ReadOnlyObservableCollection FileTypeGroups => _fileTypeGroups; public string? FilterText { get; set => this.RaiseAndSetIfChanged(ref field, value); } = string.Empty; public bool IsProcessing { get; set => this.RaiseAndSetIfChanged(ref field, value); } public double Opacity { get; set => this.RaiseAndSetIfChanged(ref field, value); } = 1.0; public ReactiveCommand ApplyCommand { get; } public ReactiveCommand ClearFilterCommand { get; } public ReactiveCommand UnassociateCommand { get; } public FileAssociationsViewModel() { // Create file type groups and populate with data InitializeFileTypes(); // Setup the filtering var filter = this.WhenAnyValue(x => x.FilterText) .Throttle(TimeSpan.FromMilliseconds(200)) .Select(BuildFilter); _fileTypeGroupsList.Connect() .AutoRefresh() .Filter(filter) .Bind(out _fileTypeGroups) .Subscribe(); // Canexecute for ApplyCommand var canExecute = this.WhenAnyValue(x => x.IsProcessing) .Select(processing => !processing); // Initialize commands with error handling ApplyCommand = ReactiveCommand.CreateFromTask( ApplyFileAssociations, canExecute); // Handle errors from the Apply command ApplyCommand.ThrownExceptions .Subscribe(ex => { IsProcessing = false; #if DEBUG Debug.WriteLine($"Error in ApplyCommand: {ex}"); #endif }); UnassociateCommand = ReactiveCommand.CreateFromTask(async () => { try { IsProcessing = true; UnselectFileTypes(); await FileTypeHelper.SetFileAssociations(FileTypeGroups); } finally { IsProcessing = false; } }, canExecute); UnassociateCommand.ThrownExceptions .Subscribe(ex => { IsProcessing = false; #if DEBUG Debug.WriteLine($"Error in UnassociateCommand: {ex}"); #endif }); ClearFilterCommand = ReactiveCommand.Create(() => FilterText = string.Empty); this.WhenAnyValue(x => x.IsProcessing).Subscribe(isProcessing => { Opacity = isProcessing ? 0.3 : 1.0; }); } private Func BuildFilter(string? filter) { if (string.IsNullOrWhiteSpace(filter)) { // Reset all items to visible when filter is empty foreach (var group in _fileTypeGroupsList.Items) { foreach (var item in group.FileTypes) { item.IsVisible = true; } } return _ => true; } return group => { // Update visibility of items based on filter var anyVisible = false; foreach (var item in group.FileTypes) { item.IsVisible = item.Description.Contains(filter, StringComparison.OrdinalIgnoreCase) || item.Extension.Contains(filter, StringComparison.OrdinalIgnoreCase); if (item.IsVisible) anyVisible = true; } // Only show groups that have at least one visible item return anyVisible; }; } private void SyncUIStateToViewModel() { // Force property notifications to ensure all changes are processed foreach (var group in FileTypeGroups) { group.IsSelected = group.IsSelected; foreach (var fileType in group.FileTypes) { fileType.IsSelected = fileType.IsSelected; } } } private async Task 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() { // 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 foreach (var group in currentGroups) { var defaultGroup = defaultGroups.FirstOrDefault(g => g.Name == group.Name); if (defaultGroup == null) { continue; } // Update the group's selection state group.IsSelected = defaultGroup.IsSelected; // Update each file type's selection state 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 = defaultType.IsSelected; } } } } public 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(); // Update selection states based on the defaults 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 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; } } } } }