QuickImage – Blame information for rev 20

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