QuickImage – Blame information for rev

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