QuickImage – Blame information for rev 8

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