QuickImage – Blame information for rev 10

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