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