QuickImage – Blame information for rev

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