QuickImage – Blame information for rev 7

Subversion Repositories:
Rev:
Rev Author Line No. Line
7 office 1 using System;
2 using System.Collections.Concurrent;
3 using System.Collections.Generic;
4 using System.ComponentModel;
5 using System.Diagnostics;
6 using System.Drawing;
7 using System.IO;
8 using System.Linq;
9 using System.Net;
10 using System.Reflection;
11 using System.Runtime.CompilerServices;
12 using System.Security.Cryptography;
13 using System.Text.RegularExpressions;
14 using System.Threading;
15 using System.Threading.Tasks;
16 using System.Threading.Tasks.Dataflow;
17 using System.Windows.Forms;
18 using Configuration;
19 using ImageMagick;
20 using ImageMagick.Factories;
21 using Microsoft.WindowsAPICodePack.Dialogs;
22 using MimeDetective.Storage;
23 using NetSparkleUpdater;
24 using NetSparkleUpdater.Enums;
25 using NetSparkleUpdater.SignatureVerifiers;
26 using NetSparkleUpdater.UI.WinForms;
27 using QuickImage.Database;
28 using QuickImage.ImageListViewSorters;
29 using QuickImage.Utilities;
30 using QuickImage.Utilities.Controls;
31 using QuickImage.Utilities.Extensions;
32 using QuickImage.Utilities.Serialization.Comma_Separated_Values;
33 using QuickImage.Utilities.Serialization.XML;
34 using Serilog;
35 using Shipwreck.Phash;
36 using Shipwreck.Phash.Bitmaps;
37 using Tesseract;
38 using ImageFormat = System.Drawing.Imaging.ImageFormat;
39  
40 namespace QuickImage
41 {
42 public partial class Form1 : Form
43 {
44 private readonly CancellationToken _cancellationToken;
45 private readonly CancellationTokenSource _cancellationTokenSource;
46 private readonly ScheduledContinuation _changedConfigurationContinuation;
47 private readonly FileMutex _fileMutex;
48 private readonly TaskScheduler _formTaskScheduler;
49 private readonly ConcurrentDictionary<string, ListViewGroup> _imageListViewGroupDictionary;
50 private readonly SemaphoreSlim _imageListViewLock;
51 private readonly ImageTool _imageTool;
52 private readonly Progress<ImageListViewItemProgress<ListViewItem>> _listViewItemProgress;
53 private readonly MagicMime _magicMime;
54 private readonly MD5 _md5;
55 private readonly LogMemorySink _memorySink;
56 private readonly QuickImageDatabase _quickImageDatabase;
57  
58 private readonly Progress<ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)>>
59 _quickImageListViewProgress;
60  
61 private readonly Random _random;
62 private readonly ScheduledContinuation _searchScheduledContinuation;
63 private readonly ConcurrentDictionary<string, ListViewItem> _searchStore;
64 private readonly ScheduledContinuation _selectionScheduledContinuation;
65 private readonly ScheduledContinuation _sortScheduledContinuation;
66 private readonly SparkleUpdater _sparkle;
67 private readonly AutoCompleteStringCollection _tagAutoCompleteStringCollection;
68 private readonly TagListViewSorter _tagListViewSorter;
69  
70 private readonly TagManager _tagManager;
71 private AboutForm _aboutForm;
72 private CancellationToken _combinedSearchSelectionCancellationToken;
73 private CancellationToken _combinedSelectionCancellationToken;
74 private EditorForm _editorForm;
75 private CancellationTokenSource _linkedSearchCancellationTokenSource;
76 private CancellationTokenSource _linkedSelectionCancellationTokenSource;
77 private PreviewForm _previewForm;
78 private QuickImageSearchParameters _quickImageSearchParameters;
79 private QuickImageSearchType _quickImageSearchType;
80 private RenameForm _renameForm;
81 private CancellationToken _searchCancellationToken;
82 private CancellationTokenSource _searchCancellationTokenSource;
83 private CancellationToken _selectionCancellationToken;
84 private CancellationTokenSource _selectionCancellationTokenSource;
85 private SettingsForm _settingsForm;
86 private ViewLogsForm _viewLogsForm;
87  
88 /// <summary>
89 /// The operation to perform on the menu item and descendants.
90 /// </summary>
91 public enum MenuItemsToggleOperation
92 {
93 NONE,
94 ENABLE,
95 DISABLE
96 }
97  
98 private Form1()
99 {
100 InitializeComponent();
101  
102 _fileMutex = new FileMutex();
103 _magicMime = new MagicMime(_fileMutex);
104  
105 _tagListViewSorter = new TagListViewSorter();
106 tagListView.ListViewItemSorter = _tagListViewSorter;
107  
108 _cancellationTokenSource = new CancellationTokenSource();
109 _cancellationToken = _cancellationTokenSource.Token;
110 _searchScheduledContinuation = new ScheduledContinuation();
111 _selectionScheduledContinuation = new ScheduledContinuation();
112 _sortScheduledContinuation = new ScheduledContinuation();
113 _md5 = new MD5CryptoServiceProvider();
114 _searchStore = new ConcurrentDictionary<string, ListViewItem>();
115 _imageListViewLock = new SemaphoreSlim(1, 1);
116 _quickImageSearchType = QuickImageSearchType.Any;
117 _formTaskScheduler = TaskScheduler.FromCurrentSynchronizationContext();
118 _imageListViewGroupDictionary = new ConcurrentDictionary<string, ListViewGroup>();
119 _changedConfigurationContinuation = new ScheduledContinuation();
120 _random = new Random();
121  
122 _listViewItemProgress = new Progress<ImageListViewItemProgress<ListViewItem>>();
123 _quickImageListViewProgress =
124 new Progress<ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)>>();
125  
126 _tagAutoCompleteStringCollection = new AutoCompleteStringCollection();
127 textBox1.AutoCompleteCustomSource = _tagAutoCompleteStringCollection;
128 tagTextBox.AutoCompleteCustomSource = _tagAutoCompleteStringCollection;
129  
130 _memorySink = new LogMemorySink();
131  
132 Log.Logger = new LoggerConfiguration()
133 .MinimumLevel.Debug()
134 .WriteTo.Conditional(condition => MemorySinkEnabled, configureSink => configureSink.Sink(_memorySink))
135 .WriteTo.File(Path.Combine(Constants.UserApplicationDirectory, "Logs", $"{Constants.AssemblyName}.log"),
136 rollingInterval: RollingInterval.Day)
137 .CreateLogger();
138  
139 // Start application update.
140 var manifestModuleName = Assembly.GetEntryAssembly().ManifestModule.FullyQualifiedName;
141 var icon = Icon.ExtractAssociatedIcon(manifestModuleName);
142  
143 _sparkle = new SparkleUpdater("https://quickimage.grimore.org/update/appcast.xml",
144 new Ed25519Checker(SecurityMode.Strict, "LonrgxVjSF0GnY4hzwlRJnLkaxnDn2ikdmOifILzLJY="))
145 {
146 UIFactory = new UIFactory(icon),
147 RelaunchAfterUpdate = true,
148 //SecurityProtocolType = SecurityProtocolType.Tls12,
149 //ShowsUIOnMainThread = false,
150 LogWriter = new LogWriter(LogWriterOutputMode.None)
151 };
152 _sparkle.StartLoop(true, true);
153  
154 _quickImageDatabase = new QuickImageDatabase(_cancellationToken);
155 _tagManager = new TagManager(_fileMutex);
156 _imageTool = new ImageTool();
157 }
158  
159 public Form1(Mutex mutex) : this()
160 {
161 }
162  
163 private Configuration.Configuration Configuration { get; set; }
164 public bool MemorySinkEnabled { get; set; }
165  
166 private async Task SortImageListView(IComparer<ListViewItem> comparer)
167 {
168 var taskCompletionSources = new[]
169 { new TaskCompletionSource<object>(), new TaskCompletionSource<object>() };
170  
171 try
172 {
173 await _imageListViewLock.WaitAsync(_cancellationToken);
174 }
175 catch
176 {
177 return;
178 }
179  
180 toolStripStatusLabel1.Text = "Sorting...";
181 toolStripProgressBar1.Style = ProgressBarStyle.Marquee;
182 toolStripProgressBar1.MarqueeAnimationSpeed = 30;
183  
184 try
185 {
186 var images = imageListView.Items.OfType<ListViewItem>().ToList();
187 if (images.Count == 0) return;
188  
189 imageListView.Items.Clear();
190  
191 #pragma warning disable CS4014
192 Task.Factory.StartNew(() =>
193 #pragma warning restore CS4014
194 {
195 images.Sort(comparer);
196 taskCompletionSources[0].TrySetResult(new { });
197 }, _cancellationToken);
198  
199 await taskCompletionSources[0].Task;
200  
201 imageListView.InvokeIfRequired(view =>
202 {
203 view.BeginUpdate();
204 foreach (var item in images)
205 {
206 var directoryName = Path.GetDirectoryName(item.Name);
207 if (!_imageListViewGroupDictionary.TryGetValue(directoryName, out var group))
208 {
209 group = new ListViewGroup(directoryName, HorizontalAlignment.Left) { Name = directoryName };
210 _imageListViewGroupDictionary.TryAdd(directoryName, group);
211 }
212  
213 item.Group = group;
214 }
215  
216 view.Items.AddRange(images.ToArray());
217 view.EndUpdate();
218 taskCompletionSources[1].TrySetResult(new { });
219 });
220  
221 await taskCompletionSources[1].Task;
222 }
223 catch (Exception exception)
224 {
225 Log.Error(exception, "Failed to sort.");
226 }
227 finally
228 {
229 this.InvokeIfRequired(form =>
230 {
231 toolStripStatusLabel1.Text = "Sorting complete.";
232 form.toolStripProgressBar1.MarqueeAnimationSpeed = 0;
233 });
234 _imageListViewLock.Release();
235 }
236 }
237  
238 private async Task BalanceTags(IEnumerable<ListViewItem> items)
239 {
240 var enumerable = items as ListViewItem[] ?? items.ToArray();
241 var count = enumerable.Length;
242  
243 var bufferBlock = new BufferBlock<(string Name, ConcurrentBag<string> Tags)>(new DataflowBlockOptions
244 { CancellationToken = _cancellationToken });
245 var broadcastBlock = new BroadcastBlock<(string Name, ConcurrentBag<string> Tags)>(x => x,
246 new DataflowBlockOptions { CancellationToken = _cancellationToken });
247 var databaseActionBlock = new ActionBlock<(string Name, ConcurrentBag<string> Tags)>(
248 async file =>
249 {
250 await Task.WhenAll(_quickImageDatabase.AddTagsAsync(file.Name, file.Tags, _cancellationToken),
251 _tagManager.AddIptcKeywords(file.Name, file.Tags, _cancellationToken));
252 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
253 var taggingActionBlock = new ActionBlock<(string Name, ConcurrentBag<string> Tags)>(file =>
254 {
255 foreach (var tag in file.Tags)
256 {
257 if (tagListView.Items.ContainsKey(tag))
258 {
259 tagListView.BeginUpdate();
260 tagListView.Items[tag].Checked = true;
261 tagListView.EndUpdate();
262 return Task.CompletedTask;
263 }
264  
265 tagListView.BeginUpdate();
266 tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
267 tagListView.Items[tag].Checked = true;
268 tagListView.EndUpdate();
269 }
270  
271 return Task.CompletedTask;
272 },
273 new ExecutionDataflowBlockOptions
274 { CancellationToken = _cancellationToken, TaskScheduler = _formTaskScheduler });
275  
276 using var bufferBroadcastLink =
277 bufferBlock.LinkTo(broadcastBlock, new DataflowLinkOptions { PropagateCompletion = true });
278 using var broadcastDatabaseLink = broadcastBlock.LinkTo(databaseActionBlock,
279 new DataflowLinkOptions { PropagateCompletion = true });
280 using var broadcastTaggingLink = broadcastBlock.LinkTo(taggingActionBlock,
281 new DataflowLinkOptions { PropagateCompletion = true });
282  
283 try
284 {
285 this.InvokeIfRequired(form => { form.toolStripStatusLabel1.Text = "Balancing tags..."; });
286  
287 var tags = new ConcurrentBag<string>();
288 foreach (var item in enumerable)
289 await foreach (var tag in
290 _quickImageDatabase.GetTags(item.Name, _cancellationToken)
291 .WithCancellation(_cancellationToken))
292 tags.Add(tag);
293  
294 var tasks = new List<Task>();
295 foreach (var item in enumerable)
296 tasks.Add(bufferBlock.SendAsync((item.Name, Tags: tags), _cancellationToken));
297  
298 await Task.WhenAll(tasks);
299  
300 bufferBlock.Complete();
301 await bufferBlock.Completion;
302 await databaseActionBlock.Completion;
303 await taggingActionBlock.Completion;
304 }
305 catch (Exception exception)
306 {
307 Log.Warning(exception, "Could not balance tags.");
308 }
309 }
310  
311 private void SelectTags(IEnumerable<ListViewItem> items)
312 {
313 var enumerable = items as ListViewItem[] ?? items.ToArray();
314 var count = enumerable.Length;
315  
316 var bufferBlock = new BufferBlock<string>();
317  
318 async void IdleHandler(object idleHandlerSender, EventArgs idleHandlerArgs)
319 {
320 try
321 {
322 if (_combinedSelectionCancellationToken.IsCancellationRequested)
323 {
324 toolStripStatusLabel1.Text = "Selection cancelled.";
325 Application.Idle -= IdleHandler;
326 return;
327 }
328  
329 if (!await bufferBlock.OutputAvailableAsync(_combinedSelectionCancellationToken))
330 {
331 toolStripStatusLabel1.Text = "Items selected.";
332 Application.Idle -= IdleHandler;
333 return;
334 }
335  
336 if (!bufferBlock.TryReceive(out var tag)) return;
337  
338 if (tagListView.Items.ContainsKey(tag))
339 {
340 tagListView.BeginUpdate();
341 tagListView.Items[tag].Checked = true;
342 tagListView.EndUpdate();
343 return;
344 }
345  
346 tagListView.BeginUpdate();
347 tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
348 tagListView.Items[tag].Checked = true;
349 tagListView.EndUpdate();
350 }
351 catch (OperationCanceledException)
352 {
353 Application.Idle -= IdleHandler;
354 }
355 catch (Exception exception)
356 {
357 Application.Idle -= IdleHandler;
358 Log.Warning(exception, "Failed selecting tags for image.");
359 }
360 finally
361 {
362 _sortScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(50), () =>
363 {
364 tagListView.BeginUpdate();
365 tagListView.Sort();
366 tagListView.EndUpdate();
367 }, _formTaskScheduler, _cancellationToken);
368 }
369 }
370  
371 if (count == 0)
372 {
373 if (_selectionCancellationTokenSource != null) _selectionCancellationTokenSource.Cancel();
374  
375 tagListView.BeginUpdate();
376 foreach (var item in tagListView.CheckedItems.OfType<ListViewItem>())
377 {
378 item.Checked = false;
379 }
380  
381 tagListView.EndUpdate();
382 return;
383 }
384  
385 if (_selectionCancellationTokenSource != null) _selectionCancellationTokenSource.Cancel();
386  
387 _selectionCancellationTokenSource = new CancellationTokenSource();
388 _selectionCancellationToken = _selectionCancellationTokenSource.Token;
389 _linkedSelectionCancellationTokenSource =
390 CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _selectionCancellationToken);
391 _combinedSelectionCancellationToken = _linkedSelectionCancellationTokenSource.Token;
392 _combinedSelectionCancellationToken.Register(() => { Application.Idle -= IdleHandler; });
393  
394 _selectionScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(50), async () =>
395 {
396 try
397 {
398 tagListView.InvokeIfRequired(view =>
399 {
400 view.BeginUpdate();
401 foreach (var item in view.CheckedItems.OfType<ListViewItem>())
402 {
403 item.Checked = false;
404 }
405  
406 view.EndUpdate();
407 });
408  
409 this.InvokeIfRequired(form =>
410 {
411 form.toolStripStatusLabel1.Text = "Selecting items...";
412 Application.Idle += IdleHandler;
413 });
414  
415 var tasks = new List<Task>();
416 foreach (var item in enumerable)
417 {
418 await foreach (var tag in _quickImageDatabase.GetTags(item.Name, _combinedSelectionCancellationToken).WithCancellation(_combinedSelectionCancellationToken))
419 {
420 tasks.Add(bufferBlock.SendAsync(tag, _combinedSelectionCancellationToken));
421 }
422 }
423  
424 await Task.WhenAll(tasks);
425 bufferBlock.Complete();
426 }
427 catch (Exception exception)
428 {
429 this.InvokeIfRequired(form =>
430 {
431 toolStripStatusLabel1.Text = "Failed selecting items.";
432 Application.Idle -= IdleHandler;
433 });
434  
435 Log.Warning(exception, "Could not select items.");
436 }
437 }, _combinedSelectionCancellationToken);
438 }
439  
440 private async Task RelocateToAsync(IEnumerable<ListViewItem> items, string destinationDirectory,
441 CancellationToken cancellationToken)
442 {
443 var enumerable = items as ListViewItem[] ?? items.ToArray();
444  
445 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
446 toolStripProgressBar1.Minimum = 0;
447 toolStripProgressBar1.Maximum = enumerable.Length;
448 toolStripProgressBar1.Value = 0;
449  
450 var transformBlock =
451 new TransformBlock<(ListViewItem Item, string File), (ListViewItem Item, Database.QuickImage Image)>(
452 async tuple =>
453 {
454 try
455 {
456 var (item, path) = tuple;
457  
458 var image = await _quickImageDatabase.GetImageAsync(item.Name, cancellationToken);
459  
460 var name = Path.GetFileName(item.Name);
461 var file = Path.Combine(path, $"{name}");
462  
463 if (!await _quickImageDatabase.SetFile(item.Name, file, cancellationToken))
464 return (null, null);
465  
466 var newImage = new Database.QuickImage(file, image.Hash, image.Tags, image.Thumbnail);
467  
468 return (Item: item, Image: newImage);
469 }
470 catch
471 {
472 return (null, null);
473 }
474 }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken });
475  
476 var actionBlock = new ActionBlock<(ListViewItem Item, Database.QuickImage Image)>(async tuple =>
477 {
478 imageListView.BeginUpdate();
479 try
480 {
481 var (item, image) = tuple;
482  
483 if (item == null || image == null) return;
484  
485 imageListView.Items.Remove(item);
486  
487 var fileInfo = new FileInfo(image.File);
488  
489 if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
490 {
491 group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left)
492 { Name = fileInfo.DirectoryName };
493 _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);
494 imageListView.Groups.Add(group);
495 }
496  
497 largeImageList.Images.RemoveByKey(item.Name);
498 largeImageList.Images.Add(image.File, image.Thumbnail);
499  
500 var listViewItem = imageListView.Items.Add(new ListViewItem(fileInfo.DirectoryName)
501 {
502 Name = image.File,
503 ImageKey = image.File,
504 Text = fileInfo.Name,
505 Group = group
506 });
507 imageListView.EnsureVisible(listViewItem.Index);
508  
509 toolStripStatusLabel1.Text = "Relocating image directory...";
510 toolStripProgressBar1.Increment(1);
511 }
512 catch
513 {
514 toolStripStatusLabel1.Text = "Failed to relocate image to directory...";
515 toolStripProgressBar1.Increment(1);
516 }
517 finally
518 {
519 imageListView.EndUpdate();
520 }
521 },
522 new ExecutionDataflowBlockOptions
523 { CancellationToken = cancellationToken, TaskScheduler = _formTaskScheduler });
524  
525 toolStripStatusLabel1.Text = "Relocating images...";
526  
527 #pragma warning disable CS4014
528 Task.Factory.StartNew(async () =>
529 #pragma warning restore CS4014
530 {
531 try
532 {
533 await _imageListViewLock.WaitAsync(cancellationToken);
534 }
535 catch
536 {
537 return;
538 }
539  
540 try
541 {
542 using var _1 = transformBlock.LinkTo(actionBlock,
543 new DataflowLinkOptions { PropagateCompletion = true });
544  
545 foreach (var item in enumerable)
546 {
547 if (cancellationToken.IsCancellationRequested) return;
548  
549 await transformBlock.SendAsync((Item: item, File: destinationDirectory), cancellationToken);
550 }
551  
552 transformBlock.Complete();
553 await actionBlock.Completion;
554 }
555 finally
556 {
557 _imageListViewLock.Release();
558 }
559 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
560 }
561  
562 private async Task RemoveImagesAsync(IEnumerable<ListViewItem> items, CancellationToken cancellationToken)
563 {
564 var enumerable = items as ListViewItem[] ?? items.ToArray();
565  
566 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
567 toolStripProgressBar1.Minimum = 0;
568 toolStripProgressBar1.Maximum = enumerable.Length;
569 toolStripProgressBar1.Value = 0;
570  
571 var bufferBlock = new BufferBlock<ListViewItem>(new DataflowBlockOptions
572 { CancellationToken = cancellationToken });
573 var actionBlock = new ActionBlock<ListViewItem>(listViewItem =>
574 {
575 toolStripStatusLabel1.Text = $"Unloading image {listViewItem.Name}";
576 imageListView.Items.Remove(listViewItem);
577 toolStripProgressBar1.Increment(1);
578 },
579 new ExecutionDataflowBlockOptions
580 { CancellationToken = cancellationToken, TaskScheduler = _formTaskScheduler });
581  
582 toolStripStatusLabel1.Text = "Unloading images...";
583  
584 #pragma warning disable CS4014
585 Task.Factory.StartNew(async () =>
586 #pragma warning restore CS4014
587 {
588 try
589 {
590 await _imageListViewLock.WaitAsync(cancellationToken);
591 }
592 catch
593 {
594 return;
595 }
596  
597 try
598 {
599 using var _ = bufferBlock.LinkTo(actionBlock,
600 new DataflowLinkOptions { PropagateCompletion = true });
601  
602 foreach (var item in enumerable)
603 {
604 if (cancellationToken.IsCancellationRequested) return;
605  
606 try
607 {
608 if (!await _quickImageDatabase.RemoveImageAsync(item.Name, cancellationToken)) continue;
609  
610 await bufferBlock.SendAsync(item, cancellationToken);
611 }
612 catch
613 {
614 // ignored
615 }
616 }
617  
618 bufferBlock.Complete();
619 await actionBlock.Completion;
620 }
621 finally
622 {
623 _imageListViewLock.Release();
624 }
625 }, cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
626 }
627  
628 public async Task RemoveMissingAsync(IEnumerable<string> groups, CancellationToken cancellationToken)
629 {
630 var enumerable = groups as string[] ?? groups.ToArray();
631  
632 var bufferBlock = new BufferBlock<ListViewItem>(new DataflowBlockOptions
633 { CancellationToken = cancellationToken });
634  
635 var transformBlock = new TransformBlock<ListViewItem, ListViewItem>(listViewItem =>
636 {
637 if (File.Exists(listViewItem.Name))
638 {
639 toolStripStatusLabel1.Text = $"Image {listViewItem.Name} exists.";
640 return null;
641 }
642  
643 toolStripStatusLabel1.Text = $"Deleting image {listViewItem.Name}";
644 imageListView.Items.Remove(listViewItem);
645  
646 return listViewItem;
647 },
648 new ExecutionDataflowBlockOptions
649 { CancellationToken = cancellationToken, TaskScheduler = _formTaskScheduler });
650  
651 var actionBlock = new ActionBlock<ListViewItem>(async item =>
652 {
653 if (item == null) return;
654  
655 try
656 {
657 await _quickImageDatabase.RemoveImageAsync(item.Name, cancellationToken);
658 }
659 catch
660 {
661 // ignored
662 }
663 });
664  
665 using var _1 = bufferBlock.LinkTo(transformBlock, new DataflowLinkOptions { PropagateCompletion = true });
666 using var _2 = transformBlock.LinkTo(actionBlock, new DataflowLinkOptions { PropagateCompletion = true });
667 using var _3 = transformBlock.LinkTo(DataflowBlock.NullTarget<ListViewItem>());
668  
669 toolStripStatusLabel1.Text = "Removing missing images...";
670 try
671 {
672 await _imageListViewLock.WaitAsync(cancellationToken);
673 }
674 catch
675 {
676 return;
677 }
678  
679 toolStripProgressBar1.Style = ProgressBarStyle.Marquee;
680 toolStripProgressBar1.MarqueeAnimationSpeed = 30;
681  
682 try
683 {
684 var tasks = new List<Task>();
685 foreach (var group in enumerable)
686 foreach (var item in imageListView.Items.OfType<ListViewItem>())
687 if (string.Equals(item.Group.Name, group, StringComparison.OrdinalIgnoreCase))
688 tasks.Add(bufferBlock.SendAsync(item, cancellationToken));
689  
690 bufferBlock.Complete();
691  
692 await Task.WhenAll(tasks);
693  
694 await transformBlock.Completion.ContinueWith(_ => { actionBlock.Complete(); }, cancellationToken);
695  
696 await actionBlock.Completion;
697 }
698 finally
699 {
700 toolStripProgressBar1.MarqueeAnimationSpeed = 0;
701 _imageListViewLock.Release();
702 toolStripStatusLabel1.Text = "Done removing images.";
703 }
704 }
705  
706 private async Task LoadFilesAsync(IEnumerable<string> files, MagicMime magicMime,
707 CancellationToken cancellationToken)
708 {
709 var enumerable = files as string[] ?? files.ToArray();
710  
711 var updateImageListViewActionBlock = new ActionBlock<Database.QuickImage>(
712 async tuple =>
713 {
714 try
715 {
716 var (file, tags, thumbnail) = (tuple.File, tuple.Tags, tuple.Thumbnail);
717  
718 if (imageListView.Items.ContainsKey(file))
719 {
720 toolStripStatusLabel1.Text = $"File {file} already exits.";
721 return;
722 }
723  
724 if (!largeImageList.Images.ContainsKey(file)) largeImageList.Images.Add(file, thumbnail);
725  
726 var fileInfo = new FileInfo(file);
727 if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
728 {
729 group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left)
730 { Name = fileInfo.DirectoryName };
731 _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);
732 imageListView.Groups.Add(group);
733 }
734  
735 var imageListViewItem = new ListViewItem(file)
736 { Name = file, ImageKey = file, Text = fileInfo.Name, Group = group };
737 imageListView.Items.Add(imageListViewItem);
738 imageListView.EnsureVisible(imageListViewItem.Index);
739  
740 toolStripStatusLabel1.Text = $"Added file {file} to the list.";
741  
742 foreach (var tag in tags)
743 {
744 if (!_tagAutoCompleteStringCollection.Contains(tag))
745 {
746 _tagAutoCompleteStringCollection.Add(tag);
747 }
748  
749 if (tagListView.Items.ContainsKey(tag))
750 {
751 continue;
752 }
753  
754 tagListView.BeginUpdate();
755 tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
756 tagListView.EndUpdate();
757 }
758 }
759 catch (Exception exception)
760 {
761 Log.Warning(exception, "Could not update image list view.");
762 }
763 },
764 new ExecutionDataflowBlockOptions
765 { CancellationToken = cancellationToken, TaskScheduler = _formTaskScheduler });
766  
767 var fileInputBlock = new BufferBlock<string>(new ExecutionDataflowBlockOptions
768 { CancellationToken = cancellationToken });
769  
770 var updateImageTagsTransformBlock = new TransformBlock<string, Database.QuickImage>(async file =>
771 {
772 try
773 {
774 var tags = new HashSet<string>();
775  
776 var databaseTags = await _quickImageDatabase.GetTags(file, cancellationToken)
777 .ToArrayAsync(cancellationToken);
778  
779 tags.UnionWith(databaseTags);
780  
781 var mime = await magicMime.GetMimeType(file, cancellationToken);
782  
783 if (Configuration.SupportedFormats.IsSupportedImage(mime))
784 {
785 await foreach (var iptcTag in _tagManager.GetIptcKeywords(file, cancellationToken))
786 {
787 tags.UnionWith(new[] { iptcTag });
788 }
789 }
790  
791 await _quickImageDatabase.AddTagsAsync(file, tags, cancellationToken);
792  
793 return await _quickImageDatabase.GetImageAsync(file, cancellationToken);
794 }
795 catch (Exception exception)
796 {
797 Log.Warning(exception, $"Could not add {file} to database.");
798  
799 return null;
800 }
801 }, new ExecutionDataflowBlockOptions { CancellationToken = cancellationToken });
802  
803 var createImageTransformBlock = new TransformBlock<string, Database.QuickImage>(async file =>
804 {
805 try
806 {
807 var tags = Array.Empty<string>();
808  
809 using var imageCollection = new MagickImageCollection(file, new MagickReadSettings { FrameIndex = 0, FrameCount = 1 });
810  
811 var imageFrame = imageCollection[0];
812  
813 var mime = await magicMime.GetMimeType(file, cancellationToken);
814  
815 if (Configuration.SupportedFormats.IsSupportedImage(mime))
816 {
817 var iptcTags = _tagManager.GetIptcKeywords(imageFrame);
818 tags = iptcTags.ToArray();
819 }
820  
821 var buffer = imageFrame.ToByteArray(MagickFormat.Bmp);
822 using var bitmapMemoryStream = new MemoryStream(buffer);
823  
824 bitmapMemoryStream.Position = 0L;
825 using var hashBitmap = (Bitmap)Image.FromStream(bitmapMemoryStream);
826 var hash = ImagePhash.ComputeDigest(hashBitmap.ToBitmap().ToLuminanceImage());
827  
828 bitmapMemoryStream.Position = 0L;
829 using var thumbnailBitmap = await CreateThumbnail(bitmapMemoryStream, 128, 128, cancellationToken);
830 var thumbnail = new Bitmap(thumbnailBitmap);
831 thumbnailBitmap.Dispose();
832  
833 await _quickImageDatabase.AddImageAsync(file, hash, tags, thumbnail, cancellationToken);
834  
835 return new Database.QuickImage(file, hash, tags, thumbnail);
836 }
837 catch (Exception exception)
838 {
839 Log.Warning(exception, $"Could not add {file} to database.");
840  
841 return null;
842 }
843 });
844  
845 using var _2 = fileInputBlock.LinkTo(updateImageTagsTransformBlock, file =>
846 {
847 try
848 {
849 return _quickImageDatabase.Exists(file, cancellationToken);
850 }
851 catch (Exception exception)
852 {
853 Log.Warning(exception, $"Could not query database for file {file}");
854 return false;
855 }
856 });
857 using var _4 = updateImageTagsTransformBlock.LinkTo(updateImageListViewActionBlock, new DataflowLinkOptions { PropagateCompletion = true });
858 using var _5 =
859 updateImageTagsTransformBlock.LinkTo(DataflowBlock.NullTarget<Database.QuickImage>(), image =>
860 {
861 var r = image == null;
862 return r;
863 });
864  
865 using var _3 = fileInputBlock.LinkTo(createImageTransformBlock, new DataflowLinkOptions { PropagateCompletion = true });
866 using var _6 = createImageTransformBlock.LinkTo(updateImageListViewActionBlock, new DataflowLinkOptions { PropagateCompletion = true });
867 using var _7 =
868 createImageTransformBlock.LinkTo(DataflowBlock.NullTarget<Database.QuickImage>(), image =>
869 {
870 var r = image == null;
871 return r;
872 });
873  
874 toolStripStatusLabel1.Text = "Loading images...";
875 try
876 {
877 await _imageListViewLock.WaitAsync(cancellationToken);
878 }
879 catch
880 {
881 return;
882 }
883  
884 try
885 {
886 toolStripProgressBar1.Style = ProgressBarStyle.Marquee;
887 toolStripProgressBar1.MarqueeAnimationSpeed = 30;
888  
889 var tasks = new List<Task>();
890 foreach (var item in enumerable)
891 {
892 await foreach (var entry in GetFilesAsync(item, Configuration, magicMime, cancellationToken).WithCancellation(cancellationToken))
893 {
894 tasks.Add(fileInputBlock.SendAsync(entry, cancellationToken));
895 }
896 }
897  
898 await Task.WhenAll(tasks);
899  
900 fileInputBlock.Complete();
901  
902 await updateImageListViewActionBlock.Completion;
903 }
904 finally
905 {
906 toolStripProgressBar1.MarqueeAnimationSpeed = 0;
907 _imageListViewLock.Release();
908 toolStripStatusLabel1.Text = "Done loading images.";
909 }
910 }
911  
912 private async Task OcrImagesAsync(IEnumerable<ListViewItem> items, CancellationToken cancellationToken)
913 {
914 var enumerable = items as ListViewItem[] ?? items.ToArray();
915  
916 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
917 toolStripProgressBar1.Minimum = 0;
918 toolStripProgressBar1.Maximum = enumerable.Length;
919 toolStripProgressBar1.Value = 0;
920  
921 void QuickImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
922 {
923 switch (e)
924 {
925 case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
926 toolStripStatusLabel1.Text =
927 $"Added {imageListViewItemProgressSuccess.Tags.Count()} to image {imageListViewItemProgressSuccess.Item.Name} using OCR.";
928 foreach (var tag in imageListViewItemProgressSuccess.Tags)
929 {
930 if (tagListView.Items.ContainsKey(tag))
931 {
932 tagListView.BeginUpdate();
933 tagListView.Items[tag].Checked = true;
934 tagListView.EndUpdate();
935 continue;
936 }
937  
938 tagListView.BeginUpdate();
939 tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
940 tagListView.Items[tag].Checked = true;
941 tagListView.EndUpdate();
942 }
943  
944 break;
945 case ImageListViewItemProgressFailure<ListViewItem> _:
946 break;
947 }
948  
949 toolStripProgressBar1.Increment(1);
950 if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
951 {
952 _listViewItemProgress.ProgressChanged -= QuickImageListViewItemProgress;
953 }
954 }
955  
956 toolStripStatusLabel1.Text = "Settings text in images to tags using OCR...";
957 try
958 {
959 await _imageListViewLock.WaitAsync(cancellationToken);
960 }
961 catch
962 {
963 return;
964 }
965  
966 _listViewItemProgress.ProgressChanged += QuickImageListViewItemProgress;
967 try
968 {
969 await OcrImages(enumerable, _listViewItemProgress, cancellationToken);
970 }
971 catch (Exception exception)
972 {
973 Log.Error(exception, "Error while scanning text in images.");
974  
975 _listViewItemProgress.ProgressChanged -= QuickImageListViewItemProgress;
976 _imageListViewLock.Release();
977 }
978 }
979  
980 private async Task ConvertImagesAsync(IEnumerable<ListViewItem> items, string extension,
981 CancellationToken cancellationToken)
982 {
983 var enumerable = items as ListViewItem[] ?? items.ToArray();
984  
985 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
986 toolStripProgressBar1.Minimum = 0;
987 toolStripProgressBar1.Maximum = enumerable.Length;
988 toolStripProgressBar1.Value = 0;
989  
990 void QuickImageListViewItemProgress(object sender,
991 ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)> e)
992 {
993 switch (e)
994 {
995 case ImageListViewItemProgressSuccess<(ListViewItem Item, Database.QuickImage Image)>
996 imageListViewItemProgressSuccess:
997 if (imageListViewItemProgressSuccess.Item is { } tuple)
998 {
999 var (item, image) = tuple;
1000  
1001 imageListView.BeginUpdate();
1002 try
1003 {
1004 imageListView.Items.Remove(item);
1005  
1006 var fileInfo = new FileInfo(image.File);
1007  
1008 if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
1009 {
1010 group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left)
1011 { Name = fileInfo.DirectoryName };
1012 _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);
1013 imageListView.Groups.Add(group);
1014 }
1015  
1016 largeImageList.Images.RemoveByKey(item.Name);
1017 largeImageList.Images.Add(image.File, image.Thumbnail);
1018  
1019 imageListView.Items.Add(new ListViewItem(fileInfo.DirectoryName)
1020 {
1021 Name = image.File,
1022 ImageKey = image.File,
1023 Text = fileInfo.Name,
1024 Group = group
1025 });
1026 }
1027 finally
1028 {
1029 imageListView.EndUpdate();
1030 }
1031 }
1032  
1033 break;
1034 case ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)> _:
1035 break;
1036 }
1037  
1038 toolStripStatusLabel1.Text = "Converting images...";
1039 toolStripProgressBar1.Increment(1);
1040 if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
1041 {
1042 _quickImageListViewProgress.ProgressChanged -= QuickImageListViewItemProgress;
1043 _imageListViewLock.Release();
1044 }
1045 }
1046  
1047 toolStripStatusLabel1.Text = "Converting images...";
1048 try
1049 {
1050 await _imageListViewLock.WaitAsync(cancellationToken);
1051 }
1052 catch
1053 {
1054 return;
1055 }
1056  
1057 _quickImageListViewProgress.ProgressChanged += QuickImageListViewItemProgress;
1058 try
1059 {
1060 await ConvertImages(enumerable, extension, _quickImageListViewProgress, cancellationToken);
1061 }
1062 catch (Exception exception)
1063 {
1064 Log.Error(exception, "Error while converting images.");
1065  
1066 _quickImageListViewProgress.ProgressChanged -= QuickImageListViewItemProgress;
1067 _imageListViewLock.Release();
1068 }
1069 }
1070  
1071 private async Task RenameImageAsync(ListViewItem item, string destinationFileName,
1072 CancellationToken cancellationToken)
1073 {
1074 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
1075 toolStripProgressBar1.Minimum = 0;
1076 toolStripProgressBar1.Maximum = 1;
1077 toolStripProgressBar1.Value = 0;
1078  
1079 void QuickImageListViewItemProgress(object sender,
1080 ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)> e)
1081 {
1082 switch (e)
1083 {
1084 case ImageListViewItemProgressSuccess<(ListViewItem Item, Database.QuickImage Image)>
1085 imageListViewItemProgressSuccess:
1086 if (imageListViewItemProgressSuccess.Item is { } tuple)
1087 {
1088 var (item, image) = tuple;
1089  
1090 imageListView.BeginUpdate();
1091 try
1092 {
1093 imageListView.Items.Remove(item);
1094  
1095 var fileInfo = new FileInfo(image.File);
1096  
1097 if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
1098 {
1099 group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left)
1100 { Name = fileInfo.DirectoryName };
1101 _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);
1102 imageListView.Groups.Add(group);
1103 }
1104  
1105 largeImageList.Images.RemoveByKey(item.Name);
1106 largeImageList.Images.Add(image.File, image.Thumbnail);
1107  
1108 var listViewItem = imageListView.Items.Add(new ListViewItem(fileInfo.DirectoryName)
1109 {
1110 Name = image.File,
1111 ImageKey = image.File,
1112 Text = fileInfo.Name,
1113 Group = group
1114 });
1115 imageListView.EnsureVisible(listViewItem.Index);
1116 }
1117 finally
1118 {
1119 imageListView.EndUpdate();
1120 }
1121 }
1122  
1123 break;
1124 case ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)> _:
1125 break;
1126 }
1127  
1128 toolStripStatusLabel1.Text = "Renaming image...";
1129 toolStripProgressBar1.Increment(1);
1130 if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
1131 {
1132 _quickImageListViewProgress.ProgressChanged -= QuickImageListViewItemProgress;
1133 _imageListViewLock.Release();
1134 }
1135 }
1136  
1137 toolStripStatusLabel1.Text = "Renaming image...";
1138 try
1139 {
1140 await _imageListViewLock.WaitAsync(cancellationToken);
1141 }
1142 catch
1143 {
1144 return;
1145 }
1146  
1147 _quickImageListViewProgress.ProgressChanged += QuickImageListViewItemProgress;
1148 try
1149 {
1150 var directoryName = Path.GetDirectoryName(item.Name);
1151  
1152 await RenameImage(item, Path.Combine(directoryName, destinationFileName), _quickImageListViewProgress,
1153 cancellationToken);
1154 }
1155 catch (Exception exception)
1156 {
1157 Log.Error(exception, "Error while renaming image.");
1158  
1159 _quickImageListViewProgress.ProgressChanged -= QuickImageListViewItemProgress;
1160 _imageListViewLock.Release();
1161 }
1162 }
1163  
1164 private async Task MoveImagesAsync(IEnumerable<ListViewItem> items, string destinationDirectory,
1165 CancellationToken cancellationToken)
1166 {
1167 var enumerable = items as ListViewItem[] ?? items.ToArray();
1168  
1169 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
1170 toolStripProgressBar1.Minimum = 0;
1171 toolStripProgressBar1.Maximum = enumerable.Length;
1172 toolStripProgressBar1.Value = 0;
1173  
1174 void QuickImageListViewItemProgress(object sender,
1175 ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)> e)
1176 {
1177 switch (e)
1178 {
1179 case ImageListViewItemProgressSuccess<(ListViewItem Item, Database.QuickImage Image)>
1180 imageListViewItemProgressSuccess:
1181 if (imageListViewItemProgressSuccess.Item is { } tuple)
1182 {
1183 var (item, image) = tuple;
1184  
1185 imageListView.BeginUpdate();
1186 try
1187 {
1188 imageListView.Items.Remove(item);
1189  
1190 var fileInfo = new FileInfo(image.File);
1191  
1192 if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
1193 {
1194 group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left)
1195 { Name = fileInfo.DirectoryName };
1196 _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);
1197 imageListView.Groups.Add(group);
1198 }
1199  
1200 largeImageList.Images.RemoveByKey(item.Name);
1201 largeImageList.Images.Add(image.File, image.Thumbnail);
1202  
1203 var listViewItem = imageListView.Items.Add(new ListViewItem(fileInfo.DirectoryName)
1204 {
1205 Name = image.File,
1206 ImageKey = image.File,
1207 Text = fileInfo.Name,
1208 Group = group
1209 });
1210 imageListView.EnsureVisible(listViewItem.Index);
1211 }
1212 finally
1213 {
1214 imageListView.EndUpdate();
1215 }
1216 }
1217  
1218 break;
1219 case ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)> _:
1220 break;
1221 }
1222  
1223 toolStripStatusLabel1.Text = "Moving images...";
1224 toolStripProgressBar1.Increment(1);
1225 if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
1226 {
1227 _quickImageListViewProgress.ProgressChanged -= QuickImageListViewItemProgress;
1228 _imageListViewLock.Release();
1229 }
1230 }
1231  
1232 toolStripStatusLabel1.Text = "Moving images...";
1233 try
1234 {
1235 await _imageListViewLock.WaitAsync(cancellationToken);
1236 }
1237 catch
1238 {
1239 return;
1240 }
1241  
1242 _quickImageListViewProgress.ProgressChanged += QuickImageListViewItemProgress;
1243 try
1244 {
1245 await MoveImages(enumerable, destinationDirectory, _quickImageListViewProgress, cancellationToken);
1246 }
1247 catch
1248 {
1249 _quickImageListViewProgress.ProgressChanged -= QuickImageListViewItemProgress;
1250 _imageListViewLock.Release();
1251 }
1252 }
1253  
1254 private void DeleteImages(IEnumerable<ListViewItem> items, CancellationToken cancellationToken)
1255 {
1256 var enumerable = items as ListViewItem[] ?? items.ToArray();
1257  
1258 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
1259 toolStripProgressBar1.Minimum = 0;
1260 toolStripProgressBar1.Maximum = enumerable.Length;
1261 toolStripProgressBar1.Value = 0;
1262  
1263 var bufferBlock = new BufferBlock<ListViewItem>(new DataflowBlockOptions
1264 { CancellationToken = cancellationToken });
1265 var actionBlock = new ActionBlock<ListViewItem>(listViewItem =>
1266 {
1267 toolStripStatusLabel1.Text = $"Deleting image {listViewItem.Name}";
1268 imageListView.Items.Remove(listViewItem);
1269 },
1270 new ExecutionDataflowBlockOptions
1271 { CancellationToken = cancellationToken, TaskScheduler = _formTaskScheduler });
1272  
1273 toolStripStatusLabel1.Text = "Deleting images...";
1274  
1275 #pragma warning disable CS4014
1276 Task.Factory.StartNew(async () =>
1277 #pragma warning restore CS4014
1278 {
1279 try
1280 {
1281 await _imageListViewLock.WaitAsync(cancellationToken);
1282 }
1283 catch
1284 {
1285 return;
1286 }
1287  
1288 try
1289 {
1290 using var _ = bufferBlock.LinkTo(actionBlock,
1291 new DataflowLinkOptions { PropagateCompletion = true });
1292  
1293 foreach (var item in enumerable)
1294 {
1295 if (cancellationToken.IsCancellationRequested) return;
1296  
1297 try
1298 {
1299 File.Delete(item.Name);
1300  
1301 if (!await _quickImageDatabase.RemoveImageAsync(item.Name, cancellationToken)) continue;
1302  
1303 await bufferBlock.SendAsync(item, cancellationToken);
1304 }
1305 catch
1306 {
1307 // ignored
1308 }
1309 }
1310  
1311 bufferBlock.Complete();
1312 await actionBlock.Completion;
1313 }
1314 finally
1315 {
1316 _imageListViewLock.Release();
1317 }
1318 });
1319 }
1320  
1321 private async Task BeginSearch(string text)
1322 {
1323 var keywords = new Csv(text).Select(tag => tag.Trim()).Where(tag => !string.IsNullOrEmpty(tag)).ToArray();
1324  
1325 try
1326 {
1327 await _imageListViewLock.WaitAsync(_cancellationToken);
1328 }
1329 catch
1330 {
1331 return;
1332 }
1333  
1334 if (_selectionCancellationTokenSource != null) _selectionCancellationTokenSource.Cancel();
1335  
1336 imageListView.InvokeIfRequired(view => { imageListView.BeginUpdate(); });
1337 try
1338 {
1339 var taskCompletionSource = new TaskCompletionSource<object>();
1340 imageListView.InvokeIfRequired(view =>
1341 {
1342 foreach (var item in view.Items.OfType<ListViewItem>()) _searchStore.TryAdd(item.Name, item);
1343  
1344 view.Items.Clear();
1345 taskCompletionSource.TrySetResult(new { });
1346 });
1347  
1348 await taskCompletionSource.Task;
1349  
1350 await foreach (var quickImage in _quickImageDatabase
1351 .Search(keywords, _quickImageSearchType, _quickImageSearchParameters,
1352 _combinedSearchSelectionCancellationToken)
1353 .WithCancellation(_combinedSearchSelectionCancellationToken))
1354 {
1355 if (!_searchStore.TryGetValue(quickImage.File, out var item)) continue;
1356  
1357 var directoryName = Path.GetDirectoryName(item.Name);
1358 if (_imageListViewGroupDictionary.TryGetValue(directoryName, out var group)) item.Group = group;
1359  
1360 imageListView.InvokeIfRequired(view => { view.Items.Add(item); });
1361 }
1362 }
1363 catch (Exception exception)
1364 {
1365 Log.Error(exception, "Error while searching.");
1366 }
1367 finally
1368 {
1369 _imageListViewLock.Release();
1370 imageListView.InvokeIfRequired(view => { imageListView.EndUpdate(); });
1371 }
1372 }
1373  
1374 private async Task EndSearch()
1375 {
1376 try
1377 {
1378 await _imageListViewLock.WaitAsync(_combinedSearchSelectionCancellationToken);
1379 }
1380 catch
1381 {
1382 return;
1383 }
1384  
1385 if (_selectionCancellationTokenSource != null) _selectionCancellationTokenSource.Cancel();
1386  
1387 imageListView.InvokeIfRequired(view => { imageListView.BeginUpdate(); });
1388 try
1389 {
1390 toolStripStatusLabel1.Text = "Restoring items.";
1391  
1392 var taskCompletionSource = new TaskCompletionSource<object>();
1393 imageListView.InvokeIfRequired(view =>
1394 {
1395 view.BeginUpdate();
1396 view.Items.Clear();
1397 var restore = new List<ListViewItem>();
1398 foreach (var item in _searchStore)
1399 {
1400 var (name, listViewItem) = (item.Key, item.Value);
1401 var directoryName = Path.GetDirectoryName(name);
1402 if (!_imageListViewGroupDictionary.TryGetValue(directoryName, out var group))
1403 {
1404 group = new ListViewGroup(directoryName, HorizontalAlignment.Left) { Name = directoryName };
1405 _imageListViewGroupDictionary.TryAdd(directoryName, group);
1406 }
1407  
1408 listViewItem.Group = group;
1409 restore.Add(listViewItem);
1410 }
1411  
1412 view.Items.AddRange(restore.ToArray());
1413 view.EndUpdate();
1414 taskCompletionSource.TrySetResult(new { });
1415 });
1416  
1417 await taskCompletionSource.Task;
1418 }
1419 catch (Exception exception)
1420 {
1421 Log.Error(exception, "Unable to add back items after search.");
1422 }
1423 finally
1424 {
1425 _imageListViewLock.Release();
1426 imageListView.InvokeIfRequired(view => { imageListView.EndUpdate(); });
1427 }
1428 }
1429  
1430 private async Task OcrImages(IEnumerable<ListViewItem> items,
1431 IProgress<ImageListViewItemProgress<ListViewItem>> progress,
1432 CancellationToken cancellationToken)
1433 {
1434 using var engine = new TesseractEngine(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "tessdata"),
1435 "eng", EngineMode.Default);
1436 foreach (var item in items)
1437 {
1438 if (cancellationToken.IsCancellationRequested) return;
1439  
1440 try
1441 {
1442 using var img = Pix.LoadFromFile(item.Name);
1443 using var page = engine.Process(img);
1444 var text = page.GetText();
1445 var tags = new HashSet<string>();
1446 if (string.IsNullOrEmpty(text))
1447 {
1448 progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, tags));
1449 continue;
1450 }
1451  
1452 foreach (var word in Regex.Split(text, @"[^\w]+", RegexOptions.Compiled))
1453 {
1454 if (string.IsNullOrEmpty(word)) continue;
1455  
1456 tags.UnionWith(new[] { word });
1457 }
1458  
1459 if (!tags.Any())
1460 {
1461 progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, tags));
1462 continue;
1463 }
1464  
1465 if (!await _quickImageDatabase.AddTagsAsync(item.Name, tags, cancellationToken))
1466 {
1467 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
1468 new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1469 continue;
1470 }
1471  
1472 progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, tags));
1473 }
1474 catch (Exception exception)
1475 {
1476 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, exception));
1477 }
1478 }
1479 }
1480  
1481 private async Task ConvertImages(IEnumerable<ListViewItem> items, string extension,
1482 IProgress<ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)>> progress,
1483 CancellationToken cancellationToken)
1484 {
1485 foreach (var item in items)
1486 {
1487 if (cancellationToken.IsCancellationRequested) return;
1488  
1489 try
1490 {
1491 var image = await _quickImageDatabase.GetImageAsync(item.Name, cancellationToken);
1492  
1493 var path = Path.GetDirectoryName(item.Name);
1494 var name = Path.GetFileNameWithoutExtension(item.Name);
1495 var file = Path.Combine(path, $"{name}.{extension}");
1496  
1497 using var fileStream = new FileStream(file, FileMode.Create, FileAccess.Write);
1498 switch (extension)
1499 {
1500 case "jpg":
1501 {
1502 using var convertStream =
1503 await _imageTool.ConvertTo(item.Name, MagickFormat.Jpeg, _cancellationToken);
1504 await convertStream.CopyToAsync(fileStream);
1505 break;
1506 }
1507 case "png":
1508 {
1509 using var convertStream =
1510 await _imageTool.ConvertTo(item.Name, MagickFormat.Png, _cancellationToken);
1511 await convertStream.CopyToAsync(fileStream);
1512 break;
1513 }
1514 case "bmp":
1515 {
1516 using var convertStream =
1517 await _imageTool.ConvertTo(item.Name, MagickFormat.Bmp, _cancellationToken);
1518 await convertStream.CopyToAsync(fileStream);
1519 break;
1520 }
1521 case "gif":
1522 {
1523 using var convertStream =
1524 await _imageTool.ConvertTo(item.Name, MagickFormat.Gif, _cancellationToken);
1525 await convertStream.CopyToAsync(fileStream);
1526 break;
1527 }
1528 }
1529  
1530 if (!await _quickImageDatabase.RemoveImageAsync(image, cancellationToken))
1531 {
1532 progress.Report(
1533 new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
1534 (Item: item, Image: null), new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1535 continue;
1536 }
1537  
1538 File.Delete(item.Name);
1539  
1540 var newImage = new Database.QuickImage(file, image.Hash, image.Tags, image.Thumbnail);
1541 if (!await _quickImageDatabase.AddImageAsync(newImage, cancellationToken))
1542 {
1543 progress.Report(
1544 new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
1545 (Item: item, Image: null), new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1546 continue;
1547 }
1548  
1549 progress.Report(
1550 new ImageListViewItemProgressSuccess<(ListViewItem Item, Database.QuickImage Image)>((
1551 Item: item, Image: newImage)));
1552 }
1553 catch (Exception exception)
1554 {
1555 progress.Report(
1556 new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
1557 (Item: item, Image: null), exception));
1558 }
1559 }
1560 }
1561  
1562 private async Task RenameImage(ListViewItem item, string destinationFileName,
1563 IProgress<ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)>> progress,
1564 CancellationToken cancellationToken)
1565 {
1566 try
1567 {
1568 await Miscellaneous.CopyFileAsync(item.Name, destinationFileName, cancellationToken);
1569 File.Delete(item.Name);
1570  
1571 var image = await _quickImageDatabase.GetImageAsync(item.Name, cancellationToken);
1572  
1573 if (!await _quickImageDatabase.RemoveImageAsync(item.Name, cancellationToken))
1574 {
1575 progress.Report(
1576 new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
1577 (Item: item, Image: null), new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1578 return;
1579 }
1580  
1581 var destinationImage =
1582 new Database.QuickImage(destinationFileName, image.Hash, image.Tags, image.Thumbnail);
1583  
1584 if (!await _quickImageDatabase.AddImageAsync(destinationImage, cancellationToken))
1585 {
1586 progress.Report(
1587 new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
1588 (Item: item, Image: destinationImage),
1589 new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1590 return;
1591 }
1592  
1593 progress.Report(
1594 new ImageListViewItemProgressSuccess<(ListViewItem Item, Database.QuickImage Image)>(
1595 (Item: item, Image: destinationImage)));
1596 }
1597 catch (Exception exception)
1598 {
1599 progress.Report(
1600 new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
1601 (Item: item, Image: null), exception));
1602 }
1603 }
1604  
1605 private async Task MoveImages(IEnumerable<ListViewItem> items, string destinationDirectory,
1606 IProgress<ImageListViewItemProgress<(ListViewItem Item, Database.QuickImage Image)>> progress,
1607 CancellationToken cancellationToken)
1608 {
1609 foreach (var item in items)
1610 {
1611 if (cancellationToken.IsCancellationRequested) return;
1612  
1613 try
1614 {
1615 var fileName = Path.GetFileName(item.Name);
1616 var destinationFile = Path.Combine(destinationDirectory, fileName);
1617 await Miscellaneous.CopyFileAsync(item.Name, destinationFile, cancellationToken);
1618 File.Delete(item.Name);
1619  
1620 var image = await _quickImageDatabase.GetImageAsync(item.Name, cancellationToken);
1621  
1622 if (!await _quickImageDatabase.RemoveImageAsync(item.Name, cancellationToken))
1623 {
1624 progress.Report(
1625 new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
1626 (Item: item, Image: null), new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1627 continue;
1628 }
1629  
1630 var destinationImage =
1631 new Database.QuickImage(destinationFile, image.Hash, image.Tags, image.Thumbnail);
1632  
1633 if (!await _quickImageDatabase.AddImageAsync(destinationImage, cancellationToken))
1634 {
1635 progress.Report(
1636 new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
1637 (Item: item, Image: destinationImage),
1638 new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1639 continue;
1640 }
1641  
1642 progress.Report(
1643 new ImageListViewItemProgressSuccess<(ListViewItem Item, Database.QuickImage Image)>(
1644 (Item: item, Image: destinationImage)));
1645 }
1646 catch (Exception exception)
1647 {
1648 progress.Report(
1649 new ImageListViewItemProgressFailure<(ListViewItem Item, Database.QuickImage Image)>(
1650 (Item: item, Image: null), exception));
1651 }
1652 }
1653 }
1654  
1655 private async Task GetTags(IReadOnlyList<ListViewItem> items,
1656 IProgress<ImageListViewItemProgress<ListViewItem>> progress, CancellationToken cancellationToken)
1657 {
1658 foreach (var item in items)
1659 {
1660 if (cancellationToken.IsCancellationRequested) return;
1661  
1662 try
1663 {
1664 var tags = await _quickImageDatabase.GetTags(item.Name, cancellationToken)
1665 .ToArrayAsync(cancellationToken);
1666  
1667 progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, tags));
1668 }
1669 catch (Exception exception)
1670 {
1671 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, exception));
1672 }
1673 }
1674 }
1675  
1676 private async Task BalanceImageTags(IReadOnlyList<ListViewItem> items, MagicMime magicMime,
1677 IProgress<ImageListViewItemProgress<ListViewItem>> progress, CancellationToken cancellationToken)
1678 {
1679 foreach (var item in items)
1680 {
1681 if (cancellationToken.IsCancellationRequested)
1682 {
1683 return;
1684 }
1685  
1686 try
1687 {
1688 var tags = await _quickImageDatabase.GetTags(item.Name, cancellationToken).ToArrayAsync(cancellationToken);
1689  
1690 var mime = await magicMime.GetMimeType(item.Name, cancellationToken);
1691  
1692 if (Configuration.SupportedFormats.IsSupportedImage(mime))
1693 {
1694 if (!await _tagManager.AddIptcKeywords(item.Name, tags, cancellationToken))
1695 {
1696 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1697 continue;
1698 }
1699 }
1700  
1701 var merge = new HashSet<string>(tags);
1702 if (Configuration.SupportedFormats.Images.Image.Contains(mime))
1703 await foreach (var iptcTag in _tagManager.GetIptcKeywords(item.Name, cancellationToken))
1704 merge.UnionWith(new[] { iptcTag });
1705  
1706 if (!await _quickImageDatabase.AddTagsAsync(item.Name, merge, cancellationToken))
1707 {
1708 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
1709 new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1710 continue;
1711 }
1712  
1713 progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, merge));
1714 }
1715 catch (Exception exception)
1716 {
1717 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, exception));
1718 }
1719 }
1720 }
1721  
1722 private async Task AddTags(IEnumerable<ListViewItem> items, IReadOnlyList<string> keywords, MagicMime magicMime,
1723 IProgress<ImageListViewItemProgress<ListViewItem>> progress, CancellationToken cancellationToken)
1724 {
1725 foreach (var item in items)
1726 {
1727 if (cancellationToken.IsCancellationRequested)
1728 {
1729 return;
1730 }
1731  
1732 try
1733 {
1734 var mime = await magicMime.GetMimeType(item.Name, cancellationToken);
1735  
1736 if (Configuration.SupportedFormats.IsSupportedImage(mime))
1737 {
1738 if (!await _tagManager.AddIptcKeywords(item.Name, keywords, cancellationToken))
1739 {
1740 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1741 continue;
1742 }
1743 }
1744  
1745 if (!await _quickImageDatabase.AddTagsAsync(item.Name, keywords, cancellationToken))
1746 {
1747 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
1748 new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1749 continue;
1750 }
1751  
1752 progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, keywords, true));
1753 }
1754 catch (Exception exception)
1755 {
1756 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, exception));
1757 }
1758 }
1759 }
1760  
1761 private async Task StripTags(IEnumerable<ListViewItem> items, MagicMime magicMime,
1762 IProgress<ImageListViewItemProgress<ListViewItem>> progress, CancellationToken cancellationToken)
1763 {
1764 foreach (var item in items)
1765 {
1766 if (cancellationToken.IsCancellationRequested)
1767 {
1768 return;
1769 }
1770  
1771 try
1772 {
1773 var mime = await magicMime.GetMimeType(item.Name, cancellationToken);
1774  
1775 if (Configuration.SupportedFormats.IsSupportedImage(mime))
1776 if (!await _tagManager.StripIptcProfile(item.Name, cancellationToken))
1777 {
1778 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
1779 new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1780 continue;
1781 }
1782  
1783 var tags = await _quickImageDatabase.GetTags(item.Name, cancellationToken)
1784 .ToArrayAsync(cancellationToken);
1785  
1786 if (tags.Length != 0)
1787 if (!await _quickImageDatabase.StripTagsAsync(item.Name, cancellationToken))
1788 {
1789 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
1790 new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1791 continue;
1792 }
1793  
1794 progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, tags, false));
1795 }
1796 catch (Exception exception)
1797 {
1798 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, exception));
1799 }
1800 }
1801 }
1802  
1803 private async Task RemoveTags(IEnumerable<ListViewItem> items, IReadOnlyList<string> keywords,
1804 MagicMime magicMime, IProgress<ImageListViewItemProgress<ListViewItem>> progress,
1805 CancellationToken cancellationToken)
1806 {
1807 foreach (var item in items)
1808 {
1809 if (cancellationToken.IsCancellationRequested)
1810 {
1811 return;
1812 }
1813  
1814 try
1815 {
1816 var mime = await magicMime.GetMimeType(item.Name, cancellationToken);
1817  
1818 if (Configuration.SupportedFormats.IsSupportedImage(mime))
1819 if (!await _tagManager.RemoveIptcKeywords(item.Name, keywords, cancellationToken))
1820 {
1821 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
1822 new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1823 continue;
1824 }
1825  
1826 if (!await _quickImageDatabase.RemoveTagsAsync(item.Name, keywords, cancellationToken))
1827 {
1828 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item,
1829 new ArgumentException(MethodBase.GetCurrentMethod()?.Name)));
1830 continue;
1831 }
1832  
1833 progress.Report(new ImageListViewItemProgressSuccess<ListViewItem>(item, keywords, false));
1834 }
1835 catch (Exception exception)
1836 {
1837 progress.Report(new ImageListViewItemProgressFailure<ListViewItem>(item, exception));
1838 }
1839 }
1840 }
1841  
1842 private void collapseAllToolStripMenuItem_Click(object sender, EventArgs e)
1843 {
1844 foreach (var group in _imageListViewGroupDictionary.Values)
1845 {
1846 if (!imageListView.GetCollapsed(group))
1847 {
1848 imageListView.SetCollapsed(group, true);
1849 }
1850 }
1851 }
1852  
1853 private void expandAllToolStripMenuItem_Click(object sender, EventArgs e)
1854 {
1855 foreach (var group in _imageListViewGroupDictionary.Values)
1856 {
1857 if (imageListView.GetCollapsed(group))
1858 {
1859 imageListView.SetCollapsed(group, false);
1860 }
1861 }
1862 }
1863  
1864 private void Form1_Closing(object sender, FormClosingEventArgs e)
1865 {
1866 }
1867  
1868 private async void creationTimeAscendingSortMenuItem_Click(object sender, EventArgs e)
1869 {
1870 await SortImageListView(
1871 new DateImageListViewSorter(SortOrder.Ascending, DateImageListViewSorterType.Creation));
1872 }
1873  
1874 private async void creationTimeDescendingSortMenuItem_Click(object sender, EventArgs e)
1875 {
1876 await SortImageListView(
1877 new DateImageListViewSorter(SortOrder.Descending, DateImageListViewSorterType.Creation));
1878 }
1879  
1880 private async void accessTimeAscendingSortMenuItem_Click(object sender, EventArgs e)
1881 {
1882 await SortImageListView(
1883 new DateImageListViewSorter(SortOrder.Ascending, DateImageListViewSorterType.Access));
1884 }
1885  
1886 private async void accessTimeDescendingSortMenuItem_Click(object sender, EventArgs e)
1887 {
1888 await SortImageListView(new DateImageListViewSorter(SortOrder.Descending,
1889 DateImageListViewSorterType.Access));
1890 }
1891  
1892 private async void modificationTimeAscendingSortMenuItem_Click(object sender, EventArgs e)
1893 {
1894 await SortImageListView(new DateImageListViewSorter(SortOrder.Ascending,
1895 DateImageListViewSorterType.Modification));
1896 }
1897  
1898 private async void modificationTimeDescendingSortMenuItem_Click(object sender, EventArgs e)
1899 {
1900 await SortImageListView(new DateImageListViewSorter(SortOrder.Descending,
1901 DateImageListViewSorterType.Modification));
1902 }
1903  
1904 private void renameToolStripMenuItem_Click(object sender, EventArgs e)
1905 {
1906 if (_renameForm != null) return;
1907  
1908 var item = imageListView.SelectedItems.OfType<ListViewItem>().FirstOrDefault();
1909  
1910 if (item == null) return;
1911  
1912 _renameForm = new RenameForm(item);
1913 _renameForm.Rename += RenameForm_Rename;
1914 _renameForm.Closing += RenameForm_Closing;
1915 _renameForm.Show();
1916 }
1917  
1918 private async void RenameForm_Rename(object sender, RenameForm.RenameEventArgs e)
1919 {
1920 await RenameImageAsync(e.ListViewItem, e.FileName, _cancellationToken);
1921 }
1922  
1923 private void RenameForm_Closing(object sender, CancelEventArgs e)
1924 {
1925 if (_renameForm == null) return;
1926  
1927 _renameForm.Closing -= RenameForm_Closing;
1928 _renameForm.Dispose();
1929 _renameForm = null;
1930 }
1931  
1932 private async void relocateToToolStropMenuItem_Click(object sender, EventArgs e)
1933 {
1934 var dialog = new CommonOpenFileDialog
1935 {
1936 AddToMostRecentlyUsedList = true,
1937 Multiselect = false,
1938 IsFolderPicker = true
1939 };
1940  
1941 var groupItems = new List<ListViewItem>();
1942  
1943 if (dialog.ShowDialog() == CommonFileDialogResult.Ok)
1944 foreach (var item in imageListView.SelectedItems.OfType<ListViewItem>())
1945 foreach (var groupItem in item.Group.Items.OfType<ListViewItem>())
1946 groupItems.Add(groupItem);
1947  
1948 await RelocateToAsync(groupItems, dialog.FileName, _cancellationToken);
1949 }
1950  
1951 private void imageListView_MouseDown(object sender, MouseEventArgs e)
1952 {
1953 }
1954  
1955 private void checkBox2_CheckedChanged(object sender, EventArgs e)
1956 {
1957 var checkBox = (CheckBox)sender;
1958 switch (checkBox.Checked)
1959 {
1960 case true:
1961 _quickImageSearchParameters =
1962 _quickImageSearchParameters | QuickImageSearchParameters.Metadata;
1963  
1964 break;
1965 case false:
1966 _quickImageSearchParameters =
1967 _quickImageSearchParameters & ~QuickImageSearchParameters.Metadata;
1968 break;
1969 }
1970  
1971 var text = textBox1.Text;
1972 if (string.IsNullOrEmpty(text))
1973 {
1974 return;
1975 }
1976  
1977 if (_searchCancellationTokenSource != null)
1978 {
1979 _searchCancellationTokenSource.Cancel();
1980 }
1981  
1982 _searchCancellationTokenSource = new CancellationTokenSource();
1983 _searchCancellationToken = _searchCancellationTokenSource.Token;
1984 _linkedSearchCancellationTokenSource =
1985 CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
1986 _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;
1987  
1988 _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text,
1989 async text => { await BeginSearch(text); }, _formTaskScheduler,
1990 _combinedSearchSelectionCancellationToken);
1991 }
1992  
1993 private void checkBox3_CheckedChanged(object sender, EventArgs e)
1994 {
1995 var checkBox = (CheckBox)sender;
1996 switch (checkBox.Checked)
1997 {
1998 case true:
1999 _quickImageSearchParameters =
2000 _quickImageSearchParameters | QuickImageSearchParameters.Split;
2001  
2002 break;
2003 case false:
2004 _quickImageSearchParameters =
2005 _quickImageSearchParameters & ~QuickImageSearchParameters.Split;
2006 break;
2007 }
2008  
2009 var text = textBox1.Text;
2010 if (string.IsNullOrEmpty(text)) return;
2011  
2012 if (_searchCancellationTokenSource != null) _searchCancellationTokenSource.Cancel();
2013  
2014 _searchCancellationTokenSource = new CancellationTokenSource();
2015 _searchCancellationToken = _searchCancellationTokenSource.Token;
2016 _linkedSearchCancellationTokenSource =
2017 CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
2018 _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;
2019  
2020 _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text,
2021 async text => { await BeginSearch(text); }, _formTaskScheduler,
2022 _combinedSearchSelectionCancellationToken);
2023 }
2024  
2025 private void checkBox2_VisibleChanged(object sender, EventArgs e)
2026 {
2027 var checkBox = (CheckBox)sender;
2028 if (checkBox.Checked)
2029 {
2030 _quickImageSearchParameters = _quickImageSearchParameters | QuickImageSearchParameters.Metadata;
2031  
2032 return;
2033 }
2034  
2035 _quickImageSearchParameters = _quickImageSearchParameters & ~QuickImageSearchParameters.Metadata;
2036 }
2037  
2038 private void checkBox3_VisibleChanged(object sender, EventArgs e)
2039 {
2040 var checkBox = (CheckBox)sender;
2041 if (checkBox.Checked)
2042 {
2043 _quickImageSearchParameters = _quickImageSearchParameters | QuickImageSearchParameters.Split;
2044  
2045 return;
2046 }
2047  
2048 _quickImageSearchParameters = _quickImageSearchParameters & ~QuickImageSearchParameters.Split;
2049 }
2050  
2051 private async void removeMissingToolStripMenuItem_Click(object sender, EventArgs e)
2052 {
2053 var items = imageListView.SelectedItems.OfType<ListViewItem>();
2054  
2055 var listViewItems = items as ListViewItem[] ?? items.ToArray();
2056  
2057 await RemoveMissingAsync(listViewItems.Select(item => item.Group.Name), _cancellationToken);
2058 }
2059  
2060 private async Task PerformUpgrade()
2061 {
2062 // Manually check for updates, this will not show a ui
2063 var updateCheck = await _sparkle.CheckForUpdatesQuietly();
2064 switch (updateCheck.Status)
2065 {
2066 case UpdateStatus.UserSkipped:
2067 var assemblyVersion = Assembly.GetExecutingAssembly().GetName().Version;
2068 updateCheck.Updates.Sort(UpdateComparer);
2069 var latestVersion = updateCheck.Updates.FirstOrDefault();
2070 if (latestVersion != null)
2071 {
2072 var availableVersion = new Version(latestVersion.Version);
2073  
2074 if (availableVersion <= assemblyVersion)
2075 {
2076 return;
2077 }
2078 }
2079  
2080 var decision = MessageBox.Show(
2081 "Update available but it has been previously skipped. Should the update proceed anyway?",
2082 Assembly.GetExecutingAssembly().GetName().Name, MessageBoxButtons.YesNo,
2083 MessageBoxIcon.Asterisk,
2084 MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly, false);
2085  
2086 if (decision.HasFlag(DialogResult.No))
2087 {
2088 return;
2089 }
2090  
2091 goto default;
2092 case UpdateStatus.UpdateNotAvailable:
2093 MessageBox.Show("No updates available at this time.",
2094 Assembly.GetExecutingAssembly().GetName().Name, MessageBoxButtons.OK,
2095 MessageBoxIcon.Asterisk,
2096 MessageBoxDefaultButton.Button1, MessageBoxOptions.DefaultDesktopOnly, false);
2097 break;
2098 case UpdateStatus.CouldNotDetermine:
2099 Log.Error("Could not determine the update availability status.");
2100 break;
2101 default:
2102 _sparkle.ShowUpdateNeededUI();
2103 break;
2104 }
2105 }
2106  
2107 private static int UpdateComparer(AppCastItem x, AppCastItem y)
2108 {
2109 if (x == null) return 1;
2110  
2111 if (y == null) return -1;
2112  
2113 if (x == y) return 0;
2114  
2115 return new Version(y.Version).CompareTo(new Version(x.Version));
2116 }
2117  
2118 private async void oCRTextToTagsToolStripMenuItem_Click(object sender, EventArgs e)
2119 {
2120 var items = imageListView.SelectedItems.OfType<ListViewItem>();
2121  
2122 var listViewItems = items as ListViewItem[] ?? items.ToArray();
2123  
2124 if (listViewItems.Length == 0)
2125 {
2126 return;
2127 }
2128  
2129 await OcrImagesAsync(listViewItems, _cancellationToken);
2130 }
2131  
2132 #region Static Methods
2133  
2134 private static async IAsyncEnumerable<(string File, Stream Data, Definition Mime)> GetDragDropFiles(
2135 IDataObject data, MagicMime magicMime, [EnumeratorCancellation] CancellationToken cancellationToken)
2136 {
2137 var files = (string[])data.GetData(DataFormats.FileDrop);
2138 if (files != null)
2139 {
2140 foreach (var file in files)
2141 {
2142 var fileAttributes = File.GetAttributes(file);
2143 if (fileAttributes.HasFlag(FileAttributes.Directory))
2144 {
2145 continue;
2146 }
2147  
2148 var memoryStream = new MemoryStream(File.ReadAllBytes(file));
2149 memoryStream.Position = 0L;
2150  
2151 var mime = await magicMime.Identify(file, cancellationToken);
2152 if (mime == null)
2153 {
2154 continue;
2155 }
2156  
2157 yield return (File: file, Data: memoryStream, Mime: mime.Definition);
2158 }
2159  
2160 yield break;
2161 }
2162  
2163 var fileNames = data.GetFileContentNames();
2164 for (var i = 0; i < fileNames.Length; ++i)
2165 {
2166 var memoryStream = data.GetFileContent(i);
2167 memoryStream.Position = 0L;
2168 var mime = magicMime.Identify(fileNames[i], memoryStream, cancellationToken);
2169 if (mime == null) continue;
2170 yield return (File: fileNames[0], Data: memoryStream, Mime: mime.Definition);
2171 }
2172 }
2173  
2174 private static async IAsyncEnumerable<string> GetFilesAsync(string entry,
2175 Configuration.Configuration configuration, MagicMime magicMime,
2176 [EnumeratorCancellation] CancellationToken cancellationToken)
2177 {
2178 var bufferBlock = new BufferBlock<string>(new DataflowBlockOptions
2179 { CancellationToken = cancellationToken });
2180  
2181 #pragma warning disable CS4014
2182 Task.Run(async () =>
2183 #pragma warning restore CS4014
2184 {
2185 try
2186 {
2187 var attributes = File.GetAttributes(entry);
2188 if (attributes.HasFlag(FileAttributes.Directory))
2189 {
2190 var directoryFiles = Directory.GetFiles(entry);
2191 foreach (var directoryFile in directoryFiles)
2192 {
2193 if (!File.Exists(directoryFile))
2194 {
2195 continue;
2196 }
2197  
2198 string fileMimeType = await magicMime.GetMimeType(directoryFile, cancellationToken);
2199  
2200 if (!configuration.SupportedFormats.IsSupported(fileMimeType))
2201 {
2202 continue;
2203 }
2204  
2205 await bufferBlock.SendAsync(directoryFile, cancellationToken);
2206 }
2207  
2208 return;
2209 }
2210  
2211 string entryMimeType = await magicMime.GetMimeType(entry, cancellationToken);
2212 if (!configuration.SupportedFormats.IsSupported(entryMimeType))
2213 {
2214 return;
2215 }
2216  
2217 await bufferBlock.SendAsync(entry, cancellationToken);
2218 }
2219 finally
2220 {
2221 bufferBlock.Complete();
2222 }
2223 }, cancellationToken);
2224  
2225 while (await bufferBlock.OutputAvailableAsync(cancellationToken))
2226 {
2227 if (!bufferBlock.TryReceiveAll(out var files)) continue;
2228  
2229 foreach (var file in files) yield return file;
2230 }
2231 }
2232  
2233 public static async Task SaveConfiguration(Configuration.Configuration configuration)
2234 {
2235 if (!Directory.Exists(Constants.UserApplicationDirectory))
2236 Directory.CreateDirectory(Constants.UserApplicationDirectory);
2237  
2238 switch (await Serialization.Serialize(configuration, Constants.ConfigurationFile, "Configuration",
2239 "<!ATTLIST Configuration xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
2240 CancellationToken.None))
2241 {
2242 case SerializationSuccess<Configuration.Configuration> _:
2243 Log.Information("Configuration serialized successfully");
2244 break;
2245 case SerializationFailure serializationFailure:
2246 Log.Warning(serializationFailure.Exception.Message, "Configuration failed to serialize");
2247 break;
2248 }
2249 }
2250  
2251 public static async Task<Configuration.Configuration> LoadConfiguration()
2252 {
2253 if (!Directory.Exists(Constants.UserApplicationDirectory))
2254 Directory.CreateDirectory(Constants.UserApplicationDirectory);
2255  
2256 var deserializationResult =
2257 await Serialization.Deserialize<Configuration.Configuration>(Constants.ConfigurationFile,
2258 Constants.ConfigurationNamespace, Constants.ConfigurationXsd, CancellationToken.None);
2259  
2260 switch (deserializationResult)
2261 {
2262 case SerializationSuccess<Configuration.Configuration> serializationSuccess:
2263 return serializationSuccess.Result;
2264 case SerializationFailure serializationFailure:
2265 Log.Warning(serializationFailure.Exception, "Configuration failed to deserialize");
2266 return new Configuration.Configuration();
2267 default:
2268 return new Configuration.Configuration();
2269 }
2270 }
2271  
2272 private static async Task<Bitmap> CreateThumbnail(Stream file, uint width, uint height,
2273 CancellationToken cancellationToken)
2274 {
2275 using var memoryStream = new MemoryStream();
2276 using var imageCollection =
2277 new MagickImageCollection(file, new MagickReadSettings { FrameIndex = 0, FrameCount = 1 });
2278 var frame = imageCollection[0];
2279  
2280 var scaleHeight = width / (float)frame.Height;
2281 var scaleWidth = height / (float)frame.Width;
2282 var scale = Math.Min(scaleHeight, scaleWidth);
2283  
2284 width = (uint)(frame.Width * scale);
2285 height = (uint)(frame.Height * scale);
2286  
2287 var geometry = new MagickGeometry(width, height);
2288 frame.Resize(geometry);
2289  
2290 await frame.WriteAsync(memoryStream, MagickFormat.Bmp, cancellationToken);
2291  
2292 using var image = new MagickImage(MagickColors.Transparent, 128, 128);
2293 memoryStream.Position = 0L;
2294 using var composite = await new MagickFactory().Image.CreateAsync(memoryStream, cancellationToken);
2295 image.Composite(composite, Gravity.Center, CompositeOperator.Over);
2296  
2297 using var outputStream = new MemoryStream();
2298 await image.WriteAsync(outputStream, MagickFormat.Bmp, cancellationToken);
2299 var optimizer = new ImageOptimizer { IgnoreUnsupportedFormats = true };
2300 outputStream.Position = 0L;
2301 optimizer.Compress(outputStream);
2302  
2303 outputStream.Position = 0L;
2304 return (Bitmap)Image.FromStream(outputStream, true);
2305 }
2306  
2307 private static int[] CreateHistogram(MemoryStream bitmapMemoryStream, CancellationToken cancellationToken,
2308 int threads = 2)
2309 {
2310 using var bitmap = (Bitmap)Image.FromStream(bitmapMemoryStream);
2311  
2312 var histogram = new int[0xFFFFFFFF];
2313 histogram.Initialize();
2314  
2315 var parallelOptions = new ParallelOptions
2316 { CancellationToken = cancellationToken, MaxDegreeOfParallelism = threads / 2 };
2317 Parallel.For(0, bitmap.Width, parallelOptions, (x, state) =>
2318 {
2319 Parallel.For(0, bitmap.Height, parallelOptions, (y, state) =>
2320 {
2321 var value = bitmap.GetPixel(x, y).ToArgb();
2322  
2323 histogram[value]++;
2324 });
2325 });
2326  
2327 return histogram;
2328 }
2329  
2330 #endregion
2331  
2332 #region Event Handlers
2333  
2334 private void toolStripTextBox1_KeyUp(object sender, KeyEventArgs e)
2335 {
2336 if (e.KeyCode != Keys.Return) return;
2337  
2338 // Skip the beep.
2339 e.Handled = true;
2340  
2341 var toolStripTextBox = (ToolStripTextBox)sender;
2342 var tagText = toolStripTextBox.Text;
2343 toolStripTextBox.Clear();
2344  
2345 if (string.IsNullOrEmpty(tagText)) return;
2346  
2347 var keywords = new[] { tagText };
2348  
2349 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
2350  
2351 var count = items.Length;
2352  
2353 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
2354 toolStripProgressBar1.Minimum = 0;
2355 toolStripProgressBar1.Maximum = count;
2356 toolStripProgressBar1.Value = 0;
2357  
2358 void ImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
2359 {
2360 switch (e)
2361 {
2362 case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
2363 foreach (var tag in imageListViewItemProgressSuccess.Tags)
2364 {
2365 if (!_tagAutoCompleteStringCollection.Contains(tag))
2366 _tagAutoCompleteStringCollection.Add(tag);
2367  
2368 tagListView.BeginUpdate();
2369 if (tagListView.Items.ContainsKey(tag))
2370 {
2371 tagListView.Items[tag].Checked = true;
2372 tagListView.EndUpdate();
2373  
2374 _sortScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(1000), () =>
2375 {
2376 tagListView.BeginUpdate();
2377 tagListView.Sort();
2378 tagListView.EndUpdate();
2379 }, _formTaskScheduler, _cancellationToken);
2380 continue;
2381 }
2382  
2383 tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
2384 tagListView.Items[tag].Checked = true;
2385 tagListView.EndUpdate();
2386  
2387 _sortScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(1000), () =>
2388 {
2389 tagListView.BeginUpdate();
2390 tagListView.Sort();
2391 tagListView.EndUpdate();
2392 }, _formTaskScheduler, _cancellationToken);
2393 }
2394 break;
2395 case ImageListViewItemProgressFailure<ListViewItem> imageListViewItemProgressFailure:
2396 break;
2397 }
2398  
2399 toolStripStatusLabel1.Text = "Adding tags...";
2400 toolStripProgressBar1.Increment(1);
2401  
2402 if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
2403 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
2404 }
2405  
2406 toolStripStatusLabel1.Text = "Adding tags...";
2407 #pragma warning disable CS4014
2408 Task.Factory.StartNew(async () =>
2409 #pragma warning restore CS4014
2410 {
2411 _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
2412 try
2413 {
2414 await AddTags(items, keywords, _magicMime, _listViewItemProgress, _cancellationToken);
2415 }
2416 catch
2417 {
2418 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
2419 }
2420 }, _cancellationToken);
2421 }
2422  
2423 private void contextMenuStrip1_Opened(object sender, EventArgs e)
2424 {
2425 tagTextBox.Focus();
2426  
2427 }
2428  
2429 private async void Form1_Load(object sender, EventArgs e)
2430 {
2431 JotFormTracker.Tracker.Track(this);
2432  
2433 JotFormTracker.Tracker.Configure<Form1>()
2434 .Properties(form => new { form.tabControl1.SelectedIndex });
2435  
2436 JotFormTracker.Tracker.Configure<Form1>()
2437 .Properties(form => new { form.splitContainer3.SplitterDistance });
2438  
2439 JotFormTracker.Tracker.Configure<Form1>()
2440 .Properties(form => new { form.radioButton1.Checked });
2441  
2442 JotFormTracker.Tracker.Configure<Form1>()
2443 .Properties(form => new { form.checkBox1.Checked, form.checkBox1.CheckState });
2444  
2445 JotFormTracker.Tracker.Configure<Form1>()
2446 .Properties(form => new { form.checkBox2.Checked, form.checkBox2.CheckState });
2447  
2448 JotFormTracker.Tracker.Configure<Form1>()
2449 .Properties(form => new { form.checkBox3.Checked, form.checkBox3.CheckState });
2450  
2451 Configuration = await LoadConfiguration();
2452  
2453 #pragma warning disable CS4014
2454 PerformUpgrade();
2455 #pragma warning restore CS4014
2456  
2457 #pragma warning disable CS4014
2458 Task.Factory.StartNew(async () =>
2459 #pragma warning restore CS4014
2460 {
2461 try
2462 {
2463 await _imageListViewLock.WaitAsync(_cancellationToken);
2464 }
2465 catch
2466 {
2467 return;
2468 }
2469  
2470 this.InvokeIfRequired(form =>
2471 {
2472 form.toolStripStatusLabel1.Text = "Loading images...";
2473 form.toolStripProgressBar1.Style = ProgressBarStyle.Marquee;
2474 form.toolStripProgressBar1.MarqueeAnimationSpeed = 30;
2475 });
2476  
2477 try
2478 {
2479 var images = await _quickImageDatabase.GetAll(_cancellationToken).ToArrayAsync(_cancellationToken);
2480 var imageListViewItems = new List<ListViewItem>();
2481 var tags = new HashSet<string>(StringComparer.Ordinal);
2482 foreach (var image in images)
2483 {
2484 if (!largeImageList.Images.ContainsKey(image.File))
2485 largeImageList.Images.Add(image.File, image.Thumbnail);
2486  
2487 var fileInfo = new FileInfo(image.File);
2488  
2489 if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
2490 {
2491 group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left)
2492 { Name = fileInfo.DirectoryName };
2493 _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);
2494  
2495 imageListView.InvokeIfRequired(view =>
2496 {
2497 view.Groups.Add(group);
2498  
2499 });
2500 }
2501  
2502 tags.UnionWith(image.Tags);
2503  
2504 imageListViewItems.Add(new ListViewItem(image.File)
2505 {
2506 Name = image.File,
2507 ImageKey = image.File,
2508 Text = fileInfo.Name,
2509 Group = group
2510 });
2511 }
2512  
2513 this.InvokeIfRequired(_ => { _tagAutoCompleteStringCollection.AddRange(tags.ToArray()); });
2514  
2515 imageListView.InvokeIfRequired(view =>
2516 {
2517 view.BeginUpdate();
2518 view.Items.AddRange(imageListViewItems.ToArray());
2519 view.EndUpdate();
2520 });
2521  
2522 tagListView.InvokeIfRequired(view =>
2523 {
2524 view.BeginUpdate();
2525 view.Items.AddRange(tags.Select(tag => new ListViewItem(tag) { Name = tag }).ToArray());
2526 view.EndUpdate();
2527 });
2528 }
2529 catch (Exception exception)
2530 {
2531 Log.Error(exception, "Unable to load images.");
2532 }
2533 finally
2534 {
2535 this.InvokeIfRequired(form =>
2536 {
2537 form.toolStripStatusLabel1.Text = "Done loading images.";
2538 form.toolStripProgressBar1.MarqueeAnimationSpeed = 0;
2539 });
2540  
2541 _imageListViewLock.Release();
2542 }
2543 }, _cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
2544 }
2545  
2546 private async void tagListView_MouseDown(object sender, MouseEventArgs e)
2547 {
2548 var listView = (ListView)sender;
2549 if (!listView.CheckBoxes) return;
2550  
2551 // Allow clicking anywhere on tag.
2552 var hitTest = listView.HitTest(e.Location);
2553 if (hitTest.Item == null) return;
2554  
2555 var item = hitTest.Item;
2556  
2557 var tagText = item.Text;
2558  
2559 var keywords = new[] { tagText };
2560  
2561 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
2562  
2563 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
2564 toolStripProgressBar1.Minimum = 0;
2565 toolStripProgressBar1.Maximum = items.Length;
2566 toolStripProgressBar1.Value = 0;
2567  
2568 void ImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
2569 {
2570 switch (e)
2571 {
2572 case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
2573 foreach (var tag in imageListViewItemProgressSuccess.Tags)
2574 {
2575 tagListView.BeginUpdate();
2576 if (tagListView.Items.ContainsKey(tag))
2577 {
2578 tagListView.Items[tag].Checked = imageListViewItemProgressSuccess.Check;
2579 tagListView.EndUpdate();
2580 continue;
2581 }
2582  
2583 tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
2584 tagListView.Items[tag].Checked = imageListViewItemProgressSuccess.Check;
2585 tagListView.EndUpdate();
2586 }
2587 break;
2588 case ImageListViewItemProgressFailure<ListViewItem> imageListViewItemProgressFailure:
2589 break;
2590 }
2591  
2592 toolStripProgressBar1.Increment(1);
2593 if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
2594 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
2595 }
2596  
2597 if (item.Checked)
2598 {
2599  
2600 toolStripStatusLabel1.Text = "Removing tags...";
2601 _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
2602 try
2603 {
2604 await RemoveTags(items, keywords, _magicMime, _listViewItemProgress, _cancellationToken);
2605 }
2606 finally
2607 {
2608 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
2609 }
2610  
2611 if (hitTest.Location == ListViewHitTestLocations.Label) hitTest.Item.Checked = !hitTest.Item.Checked;
2612  
2613 return;
2614 }
2615  
2616 toolStripStatusLabel1.Text = "Adding tags...";
2617 _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
2618 try
2619 {
2620 await AddTags(items, keywords, _magicMime, _listViewItemProgress, _cancellationToken);
2621 }
2622 finally
2623 {
2624 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
2625 }
2626  
2627 if (hitTest.Location == ListViewHitTestLocations.Label) hitTest.Item.Checked = !hitTest.Item.Checked;
2628 }
2629  
2630 private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
2631 {
2632 if (_aboutForm != null) return;
2633  
2634 _aboutForm = new AboutForm();
2635 _aboutForm.Closing += AboutForm_Closing;
2636 _aboutForm.Show();
2637 }
2638  
2639 private void AboutForm_Closing(object sender, CancelEventArgs e)
2640 {
2641 if (_aboutForm == null) return;
2642  
2643 _aboutForm.Closing -= AboutForm_Closing;
2644 _aboutForm.Dispose();
2645 _aboutForm = null;
2646 }
2647  
2648 private async void updateToolStripMenuItem_Click(object sender, EventArgs e)
2649 {
2650 await PerformUpgrade();
2651 }
2652  
2653 private void viewLogsToolStripMenuItem_Click(object sender, EventArgs e)
2654 {
2655 if (_viewLogsForm != null) return;
2656  
2657 _viewLogsForm = new ViewLogsForm(this, _memorySink, _cancellationToken);
2658 _viewLogsForm.Closing += ViewLogsForm_Closing;
2659 _viewLogsForm.Show();
2660 }
2661  
2662 private void ViewLogsForm_Closing(object sender, CancelEventArgs e)
2663 {
2664 if (_viewLogsForm == null) return;
2665  
2666 _viewLogsForm.Closing -= ViewLogsForm_Closing;
2667 _viewLogsForm.Close();
2668 _viewLogsForm = null;
2669 }
2670  
2671 private void quitToolStripMenuItem_Click(object sender, EventArgs e)
2672 {
2673 _cancellationTokenSource.Cancel();
2674 Close();
2675 }
2676  
2677 private async void removeToolStripMenuItem_Click(object sender, EventArgs e)
2678 {
2679 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
2680  
2681 await RemoveImagesAsync(items, _cancellationToken);
2682 }
2683  
2684 private void textBox1_TextChanged(object sender, EventArgs e)
2685 {
2686 var textBox = (TextBox)sender;
2687 var text = textBox.Text;
2688  
2689 if (_searchCancellationTokenSource != null) _searchCancellationTokenSource.Cancel();
2690  
2691 _searchCancellationTokenSource = new CancellationTokenSource();
2692 _searchCancellationToken = _searchCancellationTokenSource.Token;
2693 _linkedSearchCancellationTokenSource =
2694 CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
2695 _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;
2696  
2697 if (string.IsNullOrEmpty(text))
2698 {
2699 _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250),
2700 async () => { await EndSearch(); }, _combinedSearchSelectionCancellationToken);
2701  
2702 return;
2703 }
2704  
2705 _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text,
2706 async text => { await BeginSearch(text); }, _combinedSearchSelectionCancellationToken);
2707 }
2708  
2709 private void imageListView_KeyDown(object sender, KeyEventArgs e)
2710 {
2711 if (e.Control && e.KeyCode == Keys.A)
2712 {
2713 foreach (var item in imageListView.Items.OfType<ListViewItem>())
2714 {
2715 item.Selected = true;
2716 }
2717 }
2718 }
2719  
2720 private void imageListView_DragLeave(object sender, EventArgs e)
2721 {
2722 }
2723  
2724 private void deleteToolStripMenuItem_Click(object sender, EventArgs e)
2725 {
2726 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
2727  
2728 DeleteImages(items, _cancellationToken);
2729 }
2730  
2731 private void selectAllToolStripMenuItem1_Click(object sender, EventArgs e)
2732 {
2733 foreach (var item in imageListView.SelectedItems.OfType<ListViewItem>())
2734 {
2735 foreach (var groupItem in item.Group.Items.OfType<ListViewItem>())
2736 {
2737 groupItem.Selected = true;
2738 }
2739 }
2740 }
2741  
2742 private void selectAllToolStripMenuItem_Click(object sender, EventArgs e)
2743 {
2744 foreach (var item in imageListView.Items.OfType<ListViewItem>())
2745 {
2746 item.Selected = true;
2747 }
2748 }
2749  
2750 private void textBox1_KeyDown(object sender, KeyEventArgs e)
2751 {
2752 if (e.Control && e.KeyCode == Keys.A)
2753 {
2754 var textBox = (TextBox)sender;
2755 textBox.SelectAll();
2756 }
2757 }
2758  
2759 private void radioButton2_CheckedChanged(object sender, EventArgs e)
2760 {
2761 _quickImageSearchType = QuickImageSearchType.Any;
2762  
2763 var text = textBox1.Text;
2764 if (string.IsNullOrEmpty(text)) return;
2765  
2766 if (_searchCancellationTokenSource != null) _searchCancellationTokenSource.Cancel();
2767  
2768 _searchCancellationTokenSource = new CancellationTokenSource();
2769 _searchCancellationToken = _searchCancellationTokenSource.Token;
2770 _linkedSearchCancellationTokenSource =
2771 CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
2772 _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;
2773  
2774 _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text,
2775 async text => { await BeginSearch(text); }, _formTaskScheduler,
2776 _combinedSearchSelectionCancellationToken);
2777 }
2778  
2779 private void radiobutton1_CheckedChanged(object sender, EventArgs e)
2780 {
2781 _quickImageSearchType = QuickImageSearchType.All;
2782  
2783 var text = textBox1.Text;
2784 if (string.IsNullOrEmpty(text)) return;
2785  
2786 if (_searchCancellationTokenSource != null) _searchCancellationTokenSource.Cancel();
2787  
2788 _searchCancellationTokenSource = new CancellationTokenSource();
2789 _searchCancellationToken = _searchCancellationTokenSource.Token;
2790 _linkedSearchCancellationTokenSource =
2791 CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
2792 _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;
2793  
2794 _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text,
2795 async text => { await BeginSearch(text); }, _formTaskScheduler,
2796 _combinedSearchSelectionCancellationToken);
2797 }
2798  
2799 private async void editToolStripMenuItem1_Click(object sender, EventArgs e)
2800 {
2801 var item = imageListView.SelectedItems.OfType<ListViewItem>().FirstOrDefault();
2802 if (item == null) return;
2803  
2804 if (_editorForm != null) return;
2805  
2806 string mime;
2807 try
2808 {
2809 mime = await _magicMime.GetMimeType(item.Name, _cancellationToken);
2810 }
2811 catch (Exception exception)
2812 {
2813 Log.Error(exception, "Unable to identify file.");
2814  
2815 return;
2816 }
2817  
2818 switch (mime)
2819 {
2820 case "image/jpeg":
2821 case "image/png":
2822 case "image/bmp":
2823 break;
2824 default:
2825 toolStripStatusLabel1.Text = $"Image format not supported for file {item.Name}";
2826 return;
2827 }
2828  
2829 _editorForm = new EditorForm(item.Name, Configuration, _magicMime, _cancellationToken);
2830 _editorForm.ImageSave += _editorForm_ImageSave;
2831 _editorForm.ImageSaveAs += _editorForm_ImageSaveAs;
2832 _editorForm.Closing += _editorForm_Closing;
2833 _editorForm.Show();
2834 }
2835  
2836 private async void _editorForm_ImageSaveAs(object sender, EditorForm.ImageChangedEventArgs e)
2837 {
2838 using var imageMemoryStream = new MemoryStream(e.ImageBytes);
2839 using (var fileStream = new FileStream(e.FileName, FileMode.OpenOrCreate, FileAccess.Write))
2840 {
2841 await imageMemoryStream.CopyToAsync(fileStream);
2842 }
2843  
2844 await LoadFilesAsync(new[] { e.FileName }, _magicMime, _cancellationToken);
2845 }
2846  
2847 private async void _editorForm_ImageSave(object sender, EditorForm.ImageChangedEventArgs e)
2848 {
2849 using var bitmapMemoryStream = new MemoryStream();
2850 using var imageMemoryStream = new MemoryStream(e.ImageBytes);
2851 using (var fileStream = new FileStream(e.FileName, FileMode.OpenOrCreate, FileAccess.Write))
2852 {
2853 await imageMemoryStream.CopyToAsync(fileStream);
2854 }
2855  
2856 imageMemoryStream.Position = 0L;
2857 using var image = Image.FromStream(imageMemoryStream);
2858 image.Save(bitmapMemoryStream, ImageFormat.Bmp);
2859  
2860 bitmapMemoryStream.Position = 0L;
2861 using var hashBitmap = Image.FromStream(bitmapMemoryStream);
2862 var hash = ImagePhash.ComputeDigest(hashBitmap.ToBitmap().ToLuminanceImage());
2863  
2864 bitmapMemoryStream.Position = 0L;
2865 using var thumbnailBitmap = await CreateThumbnail(bitmapMemoryStream, 128, 128, _cancellationToken);
2866 var thumbnail = new Bitmap(thumbnailBitmap);
2867 thumbnailBitmap.Dispose();
2868  
2869 try
2870 {
2871 var keywords = await _quickImageDatabase.GetTags(e.FileName, _cancellationToken)
2872 .ToArrayAsync(_cancellationToken);
2873  
2874 if (!await _quickImageDatabase.RemoveImageAsync(e.FileName, _cancellationToken))
2875 throw new ArgumentException($"Could not remove image {e.FileName} from database.");
2876  
2877 if (!await _quickImageDatabase.AddImageAsync(e.FileName, hash, keywords, thumbnail, _cancellationToken))
2878 throw new ArgumentException($"Could not add image {e.FileName} to database.");
2879  
2880 this.InvokeIfRequired(form =>
2881 {
2882 form.largeImageList.Images.RemoveByKey(e.FileName);
2883 form.largeImageList.Images.Add(e.FileName, thumbnail);
2884 });
2885 }
2886 catch (Exception exception)
2887 {
2888 Log.Error(exception, "Could not update image in database");
2889 }
2890 }
2891  
2892 private void _editorForm_Closing(object sender, CancelEventArgs e)
2893 {
2894 if (_editorForm == null) return;
2895  
2896 _editorForm.ImageSave -= _editorForm_ImageSave;
2897 _editorForm.ImageSaveAs -= _editorForm_ImageSaveAs;
2898 _editorForm.Closing -= _editorForm_Closing;
2899 _editorForm.Dispose();
2900 _editorForm = null;
2901 }
2902  
2903 private async void synchronizeTagsToolStripMenuItem_Click(object sender, EventArgs e)
2904 {
2905 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
2906  
2907 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
2908 toolStripProgressBar1.Minimum = 0;
2909 toolStripProgressBar1.Maximum = items.Length;
2910 toolStripProgressBar1.Value = 0;
2911  
2912 void ImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
2913 {
2914 switch (e)
2915 {
2916 case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
2917 if (!(imageListViewItemProgressSuccess.Item is { } listViewItem)) break;
2918  
2919 toolStripStatusLabel1.Text = $"Synchronizing tags for {listViewItem.Name}";
2920  
2921 break;
2922 case ImageListViewItemProgressFailure<ListViewItem> imageListViewItemProgressFailure:
2923 break;
2924 }
2925  
2926 toolStripProgressBar1.Increment(1);
2927 if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
2928 {
2929 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
2930 _imageListViewLock.Release();
2931 }
2932 }
2933  
2934 toolStripStatusLabel1.Text = "Synchronizing image tags with database tags...";
2935 try
2936 {
2937 await _imageListViewLock.WaitAsync(_cancellationToken);
2938 }
2939 catch
2940 {
2941 return;
2942 }
2943  
2944 _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
2945 try
2946 {
2947 await BalanceImageTags(items, _magicMime, _listViewItemProgress, _cancellationToken);
2948 }
2949 catch
2950 {
2951 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
2952 _imageListViewLock.Release();
2953 }
2954 }
2955  
2956 private void checkBox1_VisibleChanged(object sender, EventArgs e)
2957 {
2958 var checkBox = (CheckBox)sender;
2959 if (checkBox.Checked)
2960 {
2961 _quickImageSearchParameters = _quickImageSearchParameters | QuickImageSearchParameters.CaseSensitive;
2962  
2963 return;
2964 }
2965  
2966 _quickImageSearchParameters = _quickImageSearchParameters & ~QuickImageSearchParameters.CaseSensitive;
2967 }
2968  
2969 private void checkBox1_CheckedChanged(object sender, EventArgs e)
2970 {
2971 var checkBox = (CheckBox)sender;
2972 switch (checkBox.Checked)
2973 {
2974 case true:
2975 _quickImageSearchParameters =
2976 _quickImageSearchParameters | QuickImageSearchParameters.CaseSensitive;
2977  
2978 break;
2979 case false:
2980 _quickImageSearchParameters =
2981 _quickImageSearchParameters & ~QuickImageSearchParameters.CaseSensitive;
2982 break;
2983 }
2984  
2985 var text = textBox1.Text;
2986 if (string.IsNullOrEmpty(text)) return;
2987  
2988 if (_searchCancellationTokenSource != null) _searchCancellationTokenSource.Cancel();
2989  
2990 _searchCancellationTokenSource = new CancellationTokenSource();
2991 _searchCancellationToken = _searchCancellationTokenSource.Token;
2992 _linkedSearchCancellationTokenSource =
2993 CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
2994 _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;
2995  
2996 _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text,
2997 async text => { await BeginSearch(text); }, _formTaskScheduler,
2998 _combinedSearchSelectionCancellationToken);
2999 }
3000  
3001 private void PreviewFormClosing(object sender, CancelEventArgs e)
3002 {
3003 if (_previewForm == null) return;
3004  
3005 _previewForm.Closing -= PreviewFormClosing;
3006 _previewForm.Dispose();
3007 _previewForm = null;
3008 }
3009  
3010 private async void perceptionToolStripMenuItem1_Click(object sender, EventArgs e)
3011 {
3012 await SortImageListView(new PerceptionImageListViewItemSorter(
3013 imageListView.Items.OfType<ListViewItem>().ToList(),
3014 _quickImageDatabase,
3015 _cancellationToken));
3016 }
3017  
3018 private void openDirectoryToolStripMenuItem_Click(object sender, EventArgs e)
3019 {
3020 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
3021  
3022 var item = items.FirstOrDefault();
3023 if (item == null) return;
3024  
3025 Process.Start("explorer.exe", $"/select, \"{item.Name}\"");
3026 }
3027  
3028 private void aboutToolStripMenuItem1_Click(object sender, EventArgs e)
3029 {
3030 if (_aboutForm != null) return;
3031  
3032 _aboutForm = new AboutForm();
3033 _aboutForm.Closing += AboutForm_Closing;
3034 _aboutForm.Show();
3035 }
3036  
3037 private async void imageListView_DragDrop(object sender, DragEventArgs e)
3038 {
3039 var inputBlock = new BufferBlock<(string File, Stream Data, Definition Mime)>(
3040 new ExecutionDataflowBlockOptions
3041 { CancellationToken = _cancellationToken });
3042  
3043 var transformBlock = new TransformBlock<(string File, Stream Data, Definition Mime), string>(async tuple =>
3044 {
3045 try
3046 {
3047 var (_, data, _) = tuple;
3048 data.Position = 0L;
3049  
3050 var path = Path.GetTempPath();
3051 var name = Path.GetTempFileName();
3052  
3053 using var memoryStream = new MemoryStream();
3054 switch (Configuration.InboundDragDrop.DragDropConvertType)
3055 {
3056 case "image/jpeg":
3057 {
3058 using var convertStream =
3059 await _imageTool.ConvertTo(data, MagickFormat.Jpeg, _cancellationToken);
3060 await convertStream.CopyToAsync(memoryStream);
3061 name = Path.ChangeExtension(name, "jpg");
3062 break;
3063 }
3064 case "image/png":
3065 {
3066 using var convertStream =
3067 await _imageTool.ConvertTo(data, MagickFormat.Png, _cancellationToken);
3068 await convertStream.CopyToAsync(memoryStream);
3069 name = Path.ChangeExtension(name, "png");
3070 break;
3071 }
3072 case "image/bmp":
3073 {
3074 using var convertStream =
3075 await _imageTool.ConvertTo(data, MagickFormat.Bmp, _cancellationToken);
3076 await convertStream.CopyToAsync(memoryStream);
3077 name = Path.ChangeExtension(name, "bmp");
3078 break;
3079 }
3080 case "image/gif":
3081 {
3082 using var convertStream =
3083 await _imageTool.ConvertTo(data, MagickFormat.Gif, _cancellationToken);
3084 await convertStream.CopyToAsync(memoryStream);
3085 name = Path.ChangeExtension(name, "gif");
3086 break;
3087 }
3088 // create a copy for files that do not have to be converted
3089 default:
3090 throw new ArgumentException(
3091 "Unsupported conversion type for inbound drag and drop image.");
3092 }
3093  
3094 var destinationFile = Path.Combine(path, name);
3095  
3096 memoryStream.Position = 0L;
3097 await Miscellaneous.CopyFileAsync(memoryStream, destinationFile, _cancellationToken);
3098  
3099 return destinationFile;
3100 }
3101 catch (Exception exception)
3102 {
3103 Log.Warning(exception, "Unable to convert drag drop input file.");
3104 return null;
3105 }
3106 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3107  
3108 var copyTransformBlock = new TransformBlock<(string File, Stream Data, Definition Mime), string>(
3109 async tuple =>
3110 {
3111 try
3112 {
3113 var (_, data, mime) = tuple;
3114 data.Position = 0L;
3115  
3116 var path = Path.GetTempPath();
3117 var name = Path.GetTempFileName();
3118 var extension = mime.File.Extensions.FirstOrDefault();
3119 name = Path.ChangeExtension(name, extension);
3120 var destinationFile = Path.Combine(path, name);
3121  
3122 await Miscellaneous.CopyFileAsync(data, destinationFile, _cancellationToken);
3123  
3124 return destinationFile;
3125 }
3126 catch (Exception exception)
3127 {
3128 Log.Warning(exception, "Unable to create a copy of file.");
3129 return null;
3130 }
3131 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3132  
3133 var importTransformBlock =
3134 new TransformBlock<(string File, Stream Data, Definition Mime), string>(
3135 tuple => Task.FromResult(tuple.File),
3136 new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3137  
3138 var outputBlock = new BufferBlock<string>(new ExecutionDataflowBlockOptions
3139 { CancellationToken = _cancellationToken });
3140  
3141 using var _1 = inputBlock.LinkTo(transformBlock,
3142 tuple => Configuration.InboundDragDrop.ConvertOnDragDrop &&
3143 Configuration.SupportedFormats.IsSupportedImage(tuple.Mime.File.MimeType) &&
3144 !Configuration.InboundDragDrop.DragDropConvertExclude
3145 .IsExcludedImage(tuple.Mime.File.MimeType));
3146 using var _2 = transformBlock.LinkTo(outputBlock, file => !string.IsNullOrEmpty(file));
3147 using var _3 = transformBlock.LinkTo(DataflowBlock.NullTarget<string>());
3148  
3149 using var _4 = inputBlock.LinkTo(copyTransformBlock, tuple => Configuration.InboundDragDrop.CopyOnDragDrop);
3150 using var _5 = copyTransformBlock.LinkTo(outputBlock);
3151 using var _6 = copyTransformBlock.LinkTo(DataflowBlock.NullTarget<string>());
3152  
3153 using var _7 = inputBlock.LinkTo(importTransformBlock,
3154 tuple => !Configuration.InboundDragDrop.ConvertOnDragDrop &&
3155 !Configuration.InboundDragDrop.CopyOnDragDrop);
3156 using var _8 = importTransformBlock.LinkTo(outputBlock);
3157 using var _9 = importTransformBlock.LinkTo(DataflowBlock.NullTarget<string>());
3158  
3159 var tasks = new List<Task>();
3160 await foreach (var (file, data, mime) in GetDragDropFiles(e.Data, _magicMime, _cancellationToken))
3161 {
3162 if (!Configuration.SupportedFormats.IsSupported(mime.File.MimeType))
3163 {
3164 continue;
3165 }
3166  
3167 tasks.Add(inputBlock.SendAsync((File: file, Data: data, Mime: mime), _cancellationToken));
3168 }
3169  
3170 await Task.WhenAll(tasks);
3171 inputBlock.Complete();
3172  
3173 await inputBlock.Completion.ContinueWith(_ =>
3174 {
3175 transformBlock.Complete();
3176 copyTransformBlock.Complete();
3177 importTransformBlock.Complete();
3178 }, _cancellationToken);
3179  
3180 await Task.WhenAll(transformBlock.Completion, copyTransformBlock.Completion, importTransformBlock.Completion).ContinueWith(_ => { outputBlock.Complete(); }, _cancellationToken);
3181  
3182 var set = new HashSet<string>();
3183 while (await outputBlock.OutputAvailableAsync(_cancellationToken))
3184 {
3185 if (!outputBlock.TryReceiveAll(out var items))
3186 {
3187 continue;
3188 }
3189  
3190 set.UnionWith(items);
3191 }
3192  
3193 await LoadFilesAsync(set, _magicMime, _cancellationToken);
3194 }
3195  
3196 private void imageListView_DragEnter(object sender, DragEventArgs e)
3197 {
3198 e.Effect = e.AllowedEffect;
3199 }
3200  
3201 private void imageListView_MouseDoubleClick(object sender, MouseEventArgs e)
3202 {
3203 var listView = (ListView)sender;
3204 var info = listView.HitTest(e.X, e.Y);
3205  
3206 switch (info.Location)
3207 {
3208 case ListViewHitTestLocations.AboveClientArea:
3209 case ListViewHitTestLocations.BelowClientArea:
3210 case ListViewHitTestLocations.LeftOfClientArea:
3211 case ListViewHitTestLocations.RightOfClientArea:
3212 case ListViewHitTestLocations.None:
3213 return;
3214 }
3215  
3216 var item = info.Item;
3217  
3218 if (item == null) return;
3219  
3220 if (_previewForm != null)
3221 _previewForm.InvokeIfRequired(form =>
3222 {
3223 form.Close();
3224 form = null;
3225 });
3226  
3227 try
3228 {
3229 new Thread(() =>
3230 {
3231 _previewForm = new PreviewForm(item.Name, Configuration, _magicMime, _cancellationToken);
3232 _previewForm.Closing += PreviewFormClosing;
3233 _previewForm.ShowDialog();
3234 }).Start();
3235 }
3236 catch (Exception exception)
3237 {
3238 Log.Error(exception, $"File {item.Name} could not be displayed due to the path not being accessible.");
3239 }
3240 }
3241  
3242 private void imageListView_MouseUp(object sender, MouseEventArgs e)
3243 {
3244 var items = imageListView.SelectedItems.OfType<ListViewItem>();
3245  
3246 SelectTags(items);
3247 }
3248  
3249 private async void imageListView_KeyUp(object sender, KeyEventArgs e)
3250 {
3251 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
3252 if (e.KeyCode == Keys.Delete)
3253 {
3254 await RemoveImagesAsync(items, _cancellationToken);
3255 return;
3256 }
3257  
3258 SelectTags(items);
3259 }
3260  
3261 private async void imageListView_ItemDrag(object sender, ItemDragEventArgs e)
3262 {
3263 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
3264 toolStripProgressBar1.Minimum = 0;
3265 toolStripProgressBar1.Value = 0;
3266 toolStripProgressBar1.Maximum = 3;
3267  
3268 var inputBlock = new BufferBlock<ListViewItem>(new DataflowBlockOptions
3269 { CancellationToken = _cancellationToken });
3270 var fileTransformBlock =
3271 new TransformBlock<ListViewItem, (string Source, string Path, string Name, string Mime, string Extension
3272 )>(async item =>
3273 {
3274 var mime = await _magicMime.GetMimeType(item.Name, _cancellationToken);
3275  
3276 var extension = Path.GetExtension(item.Name);
3277 var path = Path.GetTempPath();
3278 var file = Path.GetFileNameWithoutExtension(item.Name);
3279 if (Configuration.OutboundDragDrop.RenameOnDragDrop)
3280 {
3281 switch (Configuration.OutboundDragDrop.DragDropRenameMethod)
3282 {
3283 case DragDropRenameMethod.Random:
3284 file = string.Join("",
3285 Enumerable.Repeat(0, 5).Select(n => (char)_random.Next('a', 'z' + 1)));
3286 break;
3287 case DragDropRenameMethod.Timestamp:
3288 file = $"{DateTimeOffset.Now.ToUnixTimeSeconds()}";
3289 break;
3290 }
3291 }
3292  
3293 this.InvokeIfRequired(form =>
3294 {
3295 form.toolStripStatusLabel1.Text = $"File {item.Name} scanned for drag and drop...";
3296 });
3297  
3298 return (Source: item.Name, Path: path, Name: file, Mime: mime, Extension: extension);
3299 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3300  
3301 #pragma warning disable CS4014
3302 fileTransformBlock.Completion.ContinueWith(_ =>
3303 #pragma warning restore CS4014
3304 {
3305 toolStripStatusLabel1.Text = "All files scanned for drag and drop.";
3306 toolStripProgressBar1.Increment(1);
3307 }, _formTaskScheduler);
3308  
3309 var noConvertTransformBlock =
3310 new TransformBlock<(string Source, string Path, string Name, string Mime, string Extension), string>(
3311 async item =>
3312 {
3313 var destination = Path.Combine(item.Path, $"{item.Name}{item.Extension}");
3314 await Miscellaneous.CopyFileAsync(item.Source, destination, _cancellationToken);
3315  
3316 this.InvokeIfRequired(form =>
3317 {
3318 form.toolStripStatusLabel1.Text =
3319 $"File {item.Source} does not need conversion for drag and drop...";
3320 });
3321 return destination;
3322 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3323  
3324 #pragma warning disable CS4014
3325 noConvertTransformBlock.Completion.ContinueWith(_ =>
3326 #pragma warning restore CS4014
3327 {
3328 toolStripStatusLabel1.Text = "Conversion complete for drag and drop.";
3329 toolStripProgressBar1.Increment(1);
3330 }, _formTaskScheduler);
3331  
3332 var jpegTransformBlock =
3333 new TransformBlock<(string Source, string Path, string Name, string Mime, string Extension), string>(
3334 async file =>
3335 {
3336 using var imageStream =
3337 await _imageTool.ConvertTo(file.Source, MagickFormat.Jpeg, _cancellationToken);
3338  
3339 var jpegDestination = Path.Combine(file.Path, $"{file.Name}.jpg");
3340 await Miscellaneous.CopyFileAsync(imageStream, jpegDestination,
3341 _cancellationToken);
3342  
3343 this.InvokeIfRequired(form =>
3344 {
3345 form.toolStripStatusLabel1.Text =
3346 $"File {file.Source} converted to JPEG for drag and drop...";
3347 });
3348  
3349 return jpegDestination;
3350 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3351  
3352 #pragma warning disable CS4014
3353 jpegTransformBlock.Completion.ContinueWith(_ =>
3354 #pragma warning restore CS4014
3355 {
3356 toolStripStatusLabel1.Text = "Conversion complete for drag and drop.";
3357 toolStripProgressBar1.Increment(1);
3358 }, _formTaskScheduler);
3359  
3360 var pngTransformBlock =
3361 new TransformBlock<(string Source, string Path, string Name, string Mime, string Extension), string>(
3362 async file =>
3363 {
3364 using var imageStream =
3365 await _imageTool.ConvertTo(file.Source, MagickFormat.Png, _cancellationToken);
3366  
3367 var pngDestination = Path.Combine(file.Path, $"{file.Name}.png");
3368 await Miscellaneous.CopyFileAsync(imageStream, pngDestination, _cancellationToken);
3369  
3370 this.InvokeIfRequired(form =>
3371 {
3372 form.toolStripStatusLabel1.Text =
3373 $"File {file.Source} converted to PNG for drag and drop...";
3374 });
3375  
3376 return pngDestination;
3377 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3378  
3379 #pragma warning disable CS4014
3380 pngTransformBlock.Completion.ContinueWith(_ =>
3381 #pragma warning restore CS4014
3382 {
3383 toolStripStatusLabel1.Text = "Conversion complete for drag and drop.";
3384 toolStripProgressBar1.Increment(1);
3385 }, _formTaskScheduler);
3386  
3387 var bmpTransformBlock =
3388 new TransformBlock<(string Source, string Path, string Name, string Mime, string Extension), string>(
3389 async file =>
3390 {
3391 using var imageStream =
3392 await _imageTool.ConvertTo(file.Source, MagickFormat.Jpeg, _cancellationToken);
3393  
3394 var bmpDestination = Path.Combine(file.Path, $"{file.Name}.bmp");
3395 await Miscellaneous.CopyFileAsync(imageStream, bmpDestination, _cancellationToken);
3396  
3397 this.InvokeIfRequired(form =>
3398 {
3399 form.toolStripStatusLabel1.Text =
3400 $"File {file.Source} converted to BMP for drag and drop...";
3401 });
3402  
3403 return bmpDestination;
3404 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3405  
3406 #pragma warning disable CS4014
3407 bmpTransformBlock.Completion.ContinueWith(_ =>
3408 #pragma warning restore CS4014
3409 {
3410 toolStripStatusLabel1.Text = "Conversion complete for drag and drop.";
3411 toolStripProgressBar1.Increment(1);
3412 }, _formTaskScheduler);
3413  
3414 var iptcStripTransformBlock = new TransformBlock<string, string>(async file =>
3415 {
3416 try
3417 {
3418 var mime = await _magicMime.GetMimeType(file, _cancellationToken);
3419  
3420 if (Configuration.SupportedFormats.IsSupportedVideo(mime))
3421 {
3422 return file;
3423 }
3424  
3425 if (!Configuration.SupportedFormats.IsSupportedImage(mime))
3426 {
3427 throw new ArgumentException();
3428 }
3429  
3430 if (!await _tagManager.StripIptcProfile(file, _cancellationToken))
3431 {
3432 throw new ArgumentException();
3433 }
3434 }
3435 catch
3436 {
3437 this.InvokeIfRequired(form =>
3438 {
3439 form.toolStripStatusLabel1.Text =
3440 $"Failed to strip IPTC tags for file {file} for drag and drop...";
3441 });
3442  
3443 return string.Empty;
3444 }
3445  
3446 this.InvokeIfRequired(form =>
3447 {
3448 form.toolStripStatusLabel1.Text = $"Stripped IPTC tags from file {file} for drag and drop...";
3449 });
3450  
3451 return file;
3452 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3453  
3454 #pragma warning disable CS4014
3455 iptcStripTransformBlock.Completion.ContinueWith(_ =>
3456 #pragma warning restore CS4014
3457 {
3458 toolStripStatusLabel1.Text = "All tags stripped for drag and drop.";
3459 toolStripProgressBar1.Increment(1);
3460 }, _formTaskScheduler);
3461  
3462 var outputBlock = new BufferBlock<string>(new DataflowBlockOptions
3463 { CancellationToken = _cancellationToken });
3464  
3465 using var _1 =
3466 inputBlock.LinkTo(fileTransformBlock, new DataflowLinkOptions { PropagateCompletion = true });
3467  
3468 using var _2 = fileTransformBlock.LinkTo(jpegTransformBlock, item =>
3469 Configuration.OutboundDragDrop.ConvertOnDragDrop &&
3470 !Configuration.SupportedFormats.IsSupportedVideo(item.Mime) &&
3471 !Configuration.OutboundDragDrop.DragDropConvertExclude.IsExcludedImage(item.Mime) &&
3472 Configuration.SupportedFormats.IsSupported(item.Mime) &&
3473 Configuration.OutboundDragDrop.DragDropConvertType == "image/jpeg" &&
3474 item.Mime != "image/jpeg");
3475 using var _3 =
3476 jpegTransformBlock.LinkTo(iptcStripTransformBlock,
3477 file => Configuration.OutboundDragDrop.StripTagsOnDragDrop);
3478 using var _11 =
3479 jpegTransformBlock.LinkTo(outputBlock);
3480 using var _4 = fileTransformBlock.LinkTo(pngTransformBlock, item =>
3481 Configuration.OutboundDragDrop.ConvertOnDragDrop &&
3482 !Configuration.SupportedFormats.IsSupportedVideo(item.Mime) &&
3483 !Configuration.OutboundDragDrop.DragDropConvertExclude.IsExcludedImage(item.Mime) &&
3484 Configuration.SupportedFormats.IsSupported(item.Mime) &&
3485 Configuration.OutboundDragDrop.DragDropConvertType == "image/png" &&
3486 item.Mime != "image/png");
3487 using var _5 =
3488 pngTransformBlock.LinkTo(iptcStripTransformBlock,
3489 file => Configuration.OutboundDragDrop.StripTagsOnDragDrop);
3490 using var _12 =
3491 pngTransformBlock.LinkTo(outputBlock);
3492 using var _6 = fileTransformBlock.LinkTo(bmpTransformBlock, item =>
3493 Configuration.OutboundDragDrop.ConvertOnDragDrop &&
3494 !Configuration.SupportedFormats.IsSupportedVideo(item.Mime) &&
3495 !Configuration.OutboundDragDrop.DragDropConvertExclude.IsExcludedImage(item.Mime) &&
3496 Configuration.SupportedFormats.IsSupported(item.Mime) &&
3497 Configuration.OutboundDragDrop.DragDropConvertType == "image/bmp" &&
3498 item.Mime != "image/bmp");
3499 using var _7 =
3500 bmpTransformBlock.LinkTo(iptcStripTransformBlock,
3501 file => Configuration.OutboundDragDrop.StripTagsOnDragDrop);
3502 using var _13 =
3503 bmpTransformBlock.LinkTo(outputBlock);
3504  
3505 using var _8 = fileTransformBlock.LinkTo(noConvertTransformBlock);
3506 using var _15 = fileTransformBlock.LinkTo(DataflowBlock
3507 .NullTarget<(string Source, string Path, string Name, string Mime, string Extension)>());
3508  
3509 using var _9 = noConvertTransformBlock.LinkTo(iptcStripTransformBlock,
3510 file => Configuration.OutboundDragDrop.StripTagsOnDragDrop);
3511  
3512 using var _10 =
3513 iptcStripTransformBlock.LinkTo(outputBlock, file => !string.IsNullOrEmpty(file));
3514 using var _16 = iptcStripTransformBlock.LinkTo(DataflowBlock.NullTarget<string>());
3515 using var _14 =
3516 noConvertTransformBlock.LinkTo(outputBlock, file => !string.IsNullOrEmpty(file));
3517 using var _17 = noConvertTransformBlock.LinkTo(DataflowBlock.NullTarget<string>());
3518  
3519 foreach (var item in imageListView.SelectedItems.OfType<ListViewItem>())
3520 {
3521 #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3522 inputBlock.SendAsync(item, _cancellationToken);
3523 #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3524 }
3525  
3526 inputBlock.Complete();
3527  
3528 await fileTransformBlock.Completion.ContinueWith(_ =>
3529 {
3530 jpegTransformBlock.Complete();
3531 pngTransformBlock.Complete();
3532 bmpTransformBlock.Complete();
3533 noConvertTransformBlock.Complete();
3534 iptcStripTransformBlock.Complete();
3535 }, _cancellationToken);
3536  
3537 await Task.WhenAll(
3538 jpegTransformBlock.Completion,
3539 pngTransformBlock.Completion,
3540 bmpTransformBlock.Completion,
3541 iptcStripTransformBlock.Completion,
3542 noConvertTransformBlock.Completion).ContinueWith(_ => {
3543 outputBlock.Complete();
3544 }, _cancellationToken);
3545  
3546 var files = new HashSet<string>();
3547 while (await outputBlock.OutputAvailableAsync(_cancellationToken))
3548 {
3549 if (!outputBlock.TryReceiveAll(out var items))
3550 {
3551 continue;
3552 }
3553  
3554 files.UnionWith(items);
3555 }
3556  
3557 files.RemoveWhere(string.IsNullOrEmpty);
3558  
3559 this.InvokeIfRequired(form =>
3560 {
3561 form.toolStripStatusLabel1.Text = $"{files.Count} ready for drag and drop...";
3562 });
3563  
3564 if (files.Count == 0)
3565 {
3566 return;
3567 }
3568  
3569 var data = new DataObject(DataFormats.FileDrop, files.ToArray());
3570 this.InvokeIfRequired(_ => {
3571 DoDragDrop(data, DragDropEffects.Copy);
3572 });
3573 }
3574  
3575 private void imageListView_DragOver(object sender, DragEventArgs e)
3576 {
3577 }
3578  
3579 private async void nameToolStripMenuItem_Click(object sender, EventArgs e)
3580 {
3581 await SortImageListView(new NameImageListViewItemSorter());
3582 }
3583  
3584 private async void ascendingToolStripMenuItem_Click(object sender, EventArgs e)
3585 {
3586 await SortImageListView(new SizeImageListViewItemSorter(SortOrder.Ascending));
3587 }
3588  
3589 private async void descendingToolStripMenuItem_Click(object sender, EventArgs e)
3590 {
3591 await SortImageListView(new SizeImageListViewItemSorter(SortOrder.Descending));
3592 }
3593  
3594 private async void typeToolStripMenuItem_Click(object sender, EventArgs e)
3595 {
3596 await SortImageListView(new TypeImageListViewItemSorter());
3597 }
3598  
3599 private async void importToolStripMenuItem_Click(object sender, EventArgs e)
3600 {
3601 var dialog = new CommonOpenFileDialog
3602 {
3603 AddToMostRecentlyUsedList = true,
3604 Multiselect = true,
3605 IsFolderPicker = false
3606 };
3607  
3608 if (dialog.ShowDialog() == CommonFileDialogResult.Ok)
3609 await LoadFilesAsync(dialog.FileNames, _magicMime, _cancellationToken);
3610 }
3611  
3612 private async void importDirectoryToolStripMenuItem_Click(object sender, EventArgs e)
3613 {
3614 var dialog = new CommonOpenFileDialog
3615 {
3616 AddToMostRecentlyUsedList = true,
3617 Multiselect = true,
3618 IsFolderPicker = true
3619 };
3620  
3621 if (dialog.ShowDialog() == CommonFileDialogResult.Ok)
3622 await LoadFilesAsync(dialog.FileNames, _magicMime, _cancellationToken);
3623 }
3624  
3625 private void settingsToolStripMenuItem_Click(object sender, EventArgs e)
3626 {
3627 if (_settingsForm != null) return;
3628  
3629 _settingsForm = new SettingsForm(Configuration, _cancellationToken);
3630 _settingsForm.Closing += SettingsForm_Closing;
3631 _settingsForm.Show();
3632 }
3633  
3634 private void SettingsForm_Closing(object sender, CancelEventArgs e)
3635 {
3636 if (_settingsForm == null) return;
3637  
3638 if (_settingsForm.SaveOnClose)
3639 // Commit the configuration.
3640 _changedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
3641 async () => { await SaveConfiguration(Configuration); }, _cancellationToken);
3642  
3643 _settingsForm.Closing -= SettingsForm_Closing;
3644 _settingsForm.Dispose();
3645 _settingsForm = null;
3646 }
3647  
3648 private async void removeTagsToolStripMenuItem_Click(object sender, EventArgs e)
3649 {
3650 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
3651  
3652 var count = items.Length;
3653  
3654 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
3655 toolStripProgressBar1.Minimum = 0;
3656 toolStripProgressBar1.Maximum = count;
3657 toolStripProgressBar1.Value = 0;
3658  
3659 void ImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
3660 {
3661 switch (e)
3662 {
3663 case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
3664 if (!(imageListViewItemProgressSuccess.Item is { } imageListViewItem)) break;
3665  
3666 foreach (var tag in imageListViewItemProgressSuccess.Tags)
3667 {
3668 tagListView.BeginUpdate();
3669 if (tagListView.CheckedItems.ContainsKey(tag))
3670 {
3671 tagListView.Items[tag].Checked = false;
3672 tagListView.EndUpdate();
3673 continue;
3674 }
3675  
3676 tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
3677 tagListView.Items[tag].Checked = false;
3678 tagListView.EndUpdate();
3679 }
3680  
3681 break;
3682 case ImageListViewItemProgressFailure<ListViewItem> _:
3683 break;
3684 }
3685  
3686 toolStripStatusLabel1.Text = "Stripping tags...";
3687 toolStripProgressBar1.Increment(1);
3688 if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
3689 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
3690 }
3691  
3692 toolStripStatusLabel1.Text = "Stripping tags...";
3693  
3694 _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
3695 try
3696 {
3697 await StripTags(items, _magicMime, _listViewItemProgress, _cancellationToken);
3698 }
3699 catch
3700 {
3701 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
3702 }
3703 }
3704  
3705 private async void balanceTagsToolStripMenuItem_Click(object sender, EventArgs e)
3706 {
3707 var items = imageListView.SelectedItems.OfType<ListViewItem>();
3708  
3709 var listViewItems = items as ListViewItem[] ?? items.ToArray();
3710 if (listViewItems.Length < 2) return;
3711  
3712 await BalanceTags(listViewItems);
3713 }
3714  
3715 private async void loadMissingToolStripMenuItem_Click(object sender, EventArgs e)
3716 {
3717 var items = imageListView.SelectedItems.OfType<ListViewItem>();
3718  
3719 var listViewItems = items as ListViewItem[] ?? items.ToArray();
3720  
3721 await LoadFilesAsync(listViewItems.Select(item => item.Group.Name), _magicMime, _cancellationToken);
3722 }
3723  
3724 private void moveToolStripMenuItem_DropDownOpening(object sender, EventArgs e)
3725 {
3726 var menuItem = (ToolStripMenuItem)sender;
3727  
3728 foreach (var group in _imageListViewGroupDictionary.Keys)
3729 {
3730 if (menuItem.DropDownItems.ContainsKey(group)) continue;
3731  
3732 var toolStripMenuSubItem = new ToolStripButton(group) { Name = group };
3733 toolStripMenuSubItem.Click += moveTargetToolStripMenuItem_Click;
3734 menuItem.DropDownItems.Add(toolStripMenuSubItem);
3735 }
3736 }
3737  
3738 private async void moveTargetToolStripMenuItem_Click(object sender, EventArgs e)
3739 {
3740 var menuItem = (ToolStripButton)sender;
3741  
3742 var items = imageListView.SelectedItems.OfType<ListViewItem>();
3743  
3744 await MoveImagesAsync(items, menuItem.Name, _cancellationToken);
3745 }
3746  
3747 private void imageListView_GroupHeaderClick(object sender, ListViewGroup e)
3748 {
3749 var listViewCollapsible = (ListViewCollapsible)sender;
3750  
3751 var collapsed = listViewCollapsible.GetCollapsed(e);
3752 if (collapsed)
3753 {
3754 listViewCollapsible.SetCollapsed(e, false);
3755 return;
3756 }
3757  
3758 listViewCollapsible.SetCollapsed(e, true);
3759 }
3760  
3761 private async void convertToTypeToolStripMenuItem_Click(object sender, EventArgs e)
3762 {
3763 var toolStripMenuItem = (ToolStripMenuItem)sender;
3764 var extension = toolStripMenuItem.Text;
3765  
3766 var extensionToMime = new Dictionary<string, string>
3767 {
3768 { "jpg", "image/jpeg" },
3769 { "png", "image/png" },
3770 { "bmp", "image/bmp" },
3771 { "gif", "image/gif" }
3772 };
3773  
3774 if (!Configuration.SupportedFormats.Images.Image.Contains(extensionToMime[extension])) return;
3775  
3776 var items = imageListView.SelectedItems.OfType<ListViewItem>();
3777  
3778 await ConvertImagesAsync(items, extension, _cancellationToken);
3779 }
3780  
3781 #endregion
3782  
3783 private void button1_Click(object sender, EventArgs e)
3784 {
3785 _sortScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(50), () =>
3786 {
3787 tagListView.BeginUpdate();
3788 tagListView.Sort();
3789 tagListView.EndUpdate();
3790 }, _formTaskScheduler, _cancellationToken);
3791 }
3792  
3793 /// <summary>
3794 /// Enable or disable menu items recursively.
3795 /// </summary>
3796 /// <param name="item">the menu item from where to start disabling or enabling menu items</param>
3797 /// <param name="enable">whether to enable or disable the menu item
3798 /// </param>
3799 private void ToggleMenuItemsRecursive(ToolStripMenuItem item, MenuItemsToggleOperation menuItemsToggleOperation)
3800 {
3801 if (!item.HasDropDown)
3802 {
3803 return;
3804 }
3805  
3806 switch (menuItemsToggleOperation)
3807 {
3808 case MenuItemsToggleOperation.NONE:
3809 throw new ArgumentException("Unknown menu toggle operation.");
3810 case MenuItemsToggleOperation.ENABLE:
3811 item.Enabled = true;
3812 break;
3813 case MenuItemsToggleOperation.DISABLE:
3814 item.Enabled = false;
3815 break;
3816 }
3817  
3818 foreach (var menuItem in item.DropDownItems.OfType<ToolStripMenuItem>())
3819 {
3820 ToggleMenuItemsRecursive(menuItem, menuItemsToggleOperation);
3821 }
3822 }
3823  
3824 private void contextMenuStrip1_Closing(object sender, ToolStripDropDownClosingEventArgs e)
3825 {
3826 ToggleMenuItemsRecursive(directoryToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
3827 ToggleMenuItemsRecursive(fileToolStripMenuItem1, MenuItemsToggleOperation.ENABLE);
3828 ToggleMenuItemsRecursive(imageToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
3829 ToggleMenuItemsRecursive(taggingToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
3830 }
3831  
3832 private void contextMenuStrip1_Opening(object sender, CancelEventArgs e)
3833 {
3834 var mousePoint = imageListView.PointToClient(Cursor.Position);
3835 var listViewHitTestInfo = imageListView.HitTest(mousePoint);
3836 // check if the mouse was hovering over an item
3837 if (listViewHitTestInfo.Item != null)
3838 {
3839 // hovering
3840 ToggleMenuItemsRecursive(directoryToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
3841 ToggleMenuItemsRecursive(fileToolStripMenuItem1, MenuItemsToggleOperation.ENABLE);
3842 ToggleMenuItemsRecursive(imageToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
3843 ToggleMenuItemsRecursive(taggingToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
3844 return;
3845 }
3846  
3847 // disable menu items not related to list view items
3848 ToggleMenuItemsRecursive(directoryToolStripMenuItem, MenuItemsToggleOperation.DISABLE);
3849 ToggleMenuItemsRecursive(fileToolStripMenuItem1, MenuItemsToggleOperation.DISABLE);
3850 ToggleMenuItemsRecursive(imageToolStripMenuItem, MenuItemsToggleOperation.DISABLE);
3851 ToggleMenuItemsRecursive(taggingToolStripMenuItem, MenuItemsToggleOperation.DISABLE);
3852 }
3853 }
3854 }