FileAssociationsViewModel.cs 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. using System.Collections.ObjectModel;
  2. using System.Diagnostics;
  3. using System.Reactive;
  4. using System.Reactive.Linq;
  5. using DynamicData;
  6. using PicView.Core.FileAssociations;
  7. using ReactiveUI;
  8. namespace PicView.Core.ViewModels;
  9. public class FileAssociationsViewModel : ReactiveObject
  10. {
  11. private readonly ReadOnlyObservableCollection<FileTypeGroup> _fileTypeGroups;
  12. private readonly SourceList<FileTypeGroup> _fileTypeGroupsList = new();
  13. public ReadOnlyObservableCollection<FileTypeGroup> FileTypeGroups => _fileTypeGroups;
  14. public string? FilterText
  15. {
  16. get;
  17. set => this.RaiseAndSetIfChanged(ref field, value);
  18. } = string.Empty;
  19. public bool IsProcessing
  20. {
  21. get;
  22. set => this.RaiseAndSetIfChanged(ref field, value);
  23. }
  24. public double Opacity
  25. {
  26. get;
  27. set => this.RaiseAndSetIfChanged(ref field, value);
  28. } = 1.0;
  29. public ReactiveCommand<Unit, bool> ApplyCommand { get; }
  30. public ReactiveCommand<Unit, string> ClearFilterCommand { get; }
  31. public ReactiveCommand<Unit, Unit> UnassociateCommand { get; }
  32. public FileAssociationsViewModel()
  33. {
  34. // Create file type groups and populate with data
  35. InitializeFileTypes();
  36. // Setup the filtering
  37. var filter = this.WhenAnyValue(x => x.FilterText)
  38. .Throttle(TimeSpan.FromMilliseconds(200))
  39. .Select(BuildFilter);
  40. _fileTypeGroupsList.Connect()
  41. .AutoRefresh()
  42. .Filter(filter)
  43. .Bind(out _fileTypeGroups)
  44. .Subscribe();
  45. // Canexecute for ApplyCommand
  46. var canExecute = this.WhenAnyValue(x => x.IsProcessing)
  47. .Select(processing => !processing);
  48. // Initialize commands with error handling
  49. ApplyCommand = ReactiveCommand.CreateFromTask(
  50. ApplyFileAssociations,
  51. canExecute);
  52. // Handle errors from the Apply command
  53. ApplyCommand.ThrownExceptions
  54. .Subscribe(ex =>
  55. {
  56. IsProcessing = false;
  57. #if DEBUG
  58. Debug.WriteLine($"Error in ApplyCommand: {ex}");
  59. #endif
  60. });
  61. UnassociateCommand = ReactiveCommand.CreateFromTask(async () =>
  62. {
  63. try
  64. {
  65. IsProcessing = true;
  66. UnselectFileTypes();
  67. await FileTypeHelper.SetFileAssociations(FileTypeGroups);
  68. }
  69. finally
  70. {
  71. IsProcessing = false;
  72. }
  73. }, canExecute);
  74. UnassociateCommand.ThrownExceptions
  75. .Subscribe(ex =>
  76. {
  77. IsProcessing = false;
  78. #if DEBUG
  79. Debug.WriteLine($"Error in UnassociateCommand: {ex}");
  80. #endif
  81. });
  82. ClearFilterCommand = ReactiveCommand.Create(() => FilterText = string.Empty);
  83. this.WhenAnyValue(x => x.IsProcessing).Subscribe(isProcessing =>
  84. {
  85. Opacity = isProcessing ? 0.3 : 1.0;
  86. });
  87. }
  88. private Func<FileTypeGroup, bool> BuildFilter(string? filter)
  89. {
  90. if (string.IsNullOrWhiteSpace(filter))
  91. {
  92. // Reset all items to visible when filter is empty
  93. foreach (var group in _fileTypeGroupsList.Items)
  94. {
  95. foreach (var item in group.FileTypes)
  96. {
  97. item.IsVisible = true;
  98. }
  99. }
  100. return _ => true;
  101. }
  102. return group => {
  103. // Update visibility of items based on filter
  104. var anyVisible = false;
  105. foreach (var item in group.FileTypes)
  106. {
  107. item.IsVisible = item.Description.Contains(filter, StringComparison.OrdinalIgnoreCase) ||
  108. item.Extension.Contains(filter, StringComparison.OrdinalIgnoreCase);
  109. if (item.IsVisible)
  110. anyVisible = true;
  111. }
  112. // Only show groups that have at least one visible item
  113. return anyVisible;
  114. };
  115. }
  116. private void SyncUIStateToViewModel()
  117. {
  118. // Force property notifications to ensure all changes are processed
  119. foreach (var group in FileTypeGroups)
  120. {
  121. group.IsSelected = group.IsSelected;
  122. foreach (var fileType in group.FileTypes)
  123. {
  124. fileType.IsSelected = fileType.IsSelected;
  125. }
  126. }
  127. }
  128. private async Task<bool> ApplyFileAssociations()
  129. {
  130. try
  131. {
  132. IsProcessing = true;
  133. // Ensure all UI changes are synced to the ViewModel
  134. SyncUIStateToViewModel();
  135. // Now process the associations
  136. return await FileTypeHelper.SetFileAssociations(FileTypeGroups);
  137. }
  138. finally
  139. {
  140. IsProcessing = false;
  141. }
  142. }
  143. private void InitializeFileTypes()
  144. {
  145. var groups = FileTypeHelper.GetFileTypes();
  146. _fileTypeGroupsList.Edit(list =>
  147. {
  148. list.Clear();
  149. list.AddRange(groups);
  150. });
  151. }
  152. public void ResetFileTypesToDefault()
  153. {
  154. // Get fresh default file types
  155. var defaultGroups = FileTypeHelper.GetFileTypes();
  156. // Make a copy of the current groups to avoid enumeration issues
  157. var currentGroups = _fileTypeGroups.ToArray();
  158. // Update selection states based on the defaults
  159. foreach (var group in currentGroups)
  160. {
  161. var defaultGroup = defaultGroups.FirstOrDefault(g => g.Name == group.Name);
  162. if (defaultGroup == null)
  163. {
  164. continue;
  165. }
  166. // Update the group's selection state
  167. group.IsSelected = defaultGroup.IsSelected;
  168. // Update each file type's selection state
  169. var fileTypes = group.FileTypes.ToArray();
  170. foreach (var fileType in fileTypes)
  171. {
  172. var defaultType = defaultGroup.FileTypes.FirstOrDefault(dt =>
  173. dt.Description == fileType.Description);
  174. if (defaultType != null)
  175. {
  176. fileType.IsSelected = defaultType.IsSelected;
  177. }
  178. }
  179. }
  180. }
  181. public void UnselectFileTypes()
  182. {
  183. // Get fresh default file types
  184. var defaultGroups = FileTypeHelper.GetFileTypes();
  185. // Make a copy of the current groups to avoid enumeration issues
  186. var currentGroups = _fileTypeGroups.ToArray();
  187. // Update selection states based on the defaults
  188. foreach (var group in currentGroups)
  189. {
  190. var defaultGroup = defaultGroups.FirstOrDefault(g => g.Name == group.Name);
  191. if (defaultGroup == null)
  192. {
  193. continue;
  194. }
  195. group.IsSelected = false;
  196. // Update each file type's selection state
  197. var fileTypes = group.FileTypes.ToArray();
  198. foreach (var fileType in fileTypes)
  199. {
  200. var defaultType = defaultGroup.FileTypes.FirstOrDefault(dt =>
  201. dt.Description == fileType.Description);
  202. if (defaultType != null)
  203. {
  204. fileType.IsSelected = false;
  205. }
  206. }
  207. }
  208. }
  209. }