QuickImage – Blame information for rev 9

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  
7 office 2425 var mime = magicMime.Identify(fileNames[i], memoryStream, cancellationToken);
8 office 2426  
2427 if (mime == null)
2428 {
2429 continue;
2430 }
2431  
7 office 2432 yield return (File: fileNames[0], Data: memoryStream, Mime: mime.Definition);
2433 }
2434 }
2435  
2436 private static async IAsyncEnumerable<string> GetFilesAsync(string entry,
2437 Configuration.Configuration configuration, MagicMime magicMime,
2438 [EnumeratorCancellation] CancellationToken cancellationToken)
2439 {
8 office 2440 var bufferBlock = new BufferBlock<string>(new DataflowBlockOptions { CancellationToken = cancellationToken });
7 office 2441  
2442 #pragma warning disable CS4014
2443 Task.Run(async () =>
2444 #pragma warning restore CS4014
2445 {
2446 try
2447 {
2448 var attributes = File.GetAttributes(entry);
2449 if (attributes.HasFlag(FileAttributes.Directory))
2450 {
2451 var directoryFiles = Directory.GetFiles(entry);
2452 foreach (var directoryFile in directoryFiles)
2453 {
2454 if (!File.Exists(directoryFile))
2455 {
2456 continue;
2457 }
2458  
2459 string fileMimeType = await magicMime.GetMimeType(directoryFile, cancellationToken);
2460  
2461 if (!configuration.SupportedFormats.IsSupported(fileMimeType))
2462 {
2463 continue;
2464 }
2465  
2466 await bufferBlock.SendAsync(directoryFile, cancellationToken);
2467 }
2468  
2469 return;
2470 }
2471  
2472 string entryMimeType = await magicMime.GetMimeType(entry, cancellationToken);
2473 if (!configuration.SupportedFormats.IsSupported(entryMimeType))
2474 {
2475 return;
2476 }
2477  
2478 await bufferBlock.SendAsync(entry, cancellationToken);
2479 }
2480 finally
2481 {
2482 bufferBlock.Complete();
2483 }
2484 }, cancellationToken);
2485  
2486 while (await bufferBlock.OutputAvailableAsync(cancellationToken))
2487 {
8 office 2488 if (!bufferBlock.TryReceiveAll(out var files))
2489 {
2490 continue;
2491 }
7 office 2492  
8 office 2493 foreach (var file in files)
2494 {
2495 yield return file;
2496 }
7 office 2497 }
2498 }
2499  
2500 public static async Task SaveConfiguration(Configuration.Configuration configuration)
2501 {
2502 if (!Directory.Exists(Constants.UserApplicationDirectory))
2503 Directory.CreateDirectory(Constants.UserApplicationDirectory);
2504  
2505 switch (await Serialization.Serialize(configuration, Constants.ConfigurationFile, "Configuration",
2506 "<!ATTLIST Configuration xmlns:xsi CDATA #IMPLIED xsi:noNamespaceSchemaLocation CDATA #IMPLIED>",
2507 CancellationToken.None))
2508 {
2509 case SerializationSuccess<Configuration.Configuration> _:
2510 Log.Information("Configuration serialized successfully");
2511 break;
2512 case SerializationFailure serializationFailure:
2513 Log.Warning(serializationFailure.Exception.Message, "Configuration failed to serialize");
2514 break;
2515 }
2516 }
2517  
2518 public static async Task<Configuration.Configuration> LoadConfiguration()
2519 {
2520 if (!Directory.Exists(Constants.UserApplicationDirectory))
2521 Directory.CreateDirectory(Constants.UserApplicationDirectory);
2522  
2523 var deserializationResult =
2524 await Serialization.Deserialize<Configuration.Configuration>(Constants.ConfigurationFile,
2525 Constants.ConfigurationNamespace, Constants.ConfigurationXsd, CancellationToken.None);
2526  
2527 switch (deserializationResult)
2528 {
2529 case SerializationSuccess<Configuration.Configuration> serializationSuccess:
2530 return serializationSuccess.Result;
2531 case SerializationFailure serializationFailure:
2532 Log.Warning(serializationFailure.Exception, "Configuration failed to deserialize");
2533 return new Configuration.Configuration();
2534 default:
2535 return new Configuration.Configuration();
2536 }
2537 }
2538  
2539 private static async Task<Bitmap> CreateThumbnail(Stream file, uint width, uint height,
2540 CancellationToken cancellationToken)
2541 {
2542 using var memoryStream = new MemoryStream();
2543 using var imageCollection =
2544 new MagickImageCollection(file, new MagickReadSettings { FrameIndex = 0, FrameCount = 1 });
2545 var frame = imageCollection[0];
2546  
2547 var scaleHeight = width / (float)frame.Height;
2548 var scaleWidth = height / (float)frame.Width;
2549 var scale = Math.Min(scaleHeight, scaleWidth);
2550  
2551 width = (uint)(frame.Width * scale);
2552 height = (uint)(frame.Height * scale);
2553  
2554 var geometry = new MagickGeometry(width, height);
2555 frame.Resize(geometry);
2556  
2557 await frame.WriteAsync(memoryStream, MagickFormat.Bmp, cancellationToken);
2558  
2559 using var image = new MagickImage(MagickColors.Transparent, 128, 128);
2560 memoryStream.Position = 0L;
2561 using var composite = await new MagickFactory().Image.CreateAsync(memoryStream, cancellationToken);
2562 image.Composite(composite, Gravity.Center, CompositeOperator.Over);
2563  
2564 using var outputStream = new MemoryStream();
2565 await image.WriteAsync(outputStream, MagickFormat.Bmp, cancellationToken);
2566 var optimizer = new ImageOptimizer { IgnoreUnsupportedFormats = true };
2567 outputStream.Position = 0L;
2568 optimizer.Compress(outputStream);
2569  
2570 outputStream.Position = 0L;
2571 return (Bitmap)Image.FromStream(outputStream, true);
2572 }
2573  
2574 private static int[] CreateHistogram(MemoryStream bitmapMemoryStream, CancellationToken cancellationToken,
2575 int threads = 2)
2576 {
2577 using var bitmap = (Bitmap)Image.FromStream(bitmapMemoryStream);
2578  
2579 var histogram = new int[0xFFFFFFFF];
2580 histogram.Initialize();
2581  
2582 var parallelOptions = new ParallelOptions
2583 { CancellationToken = cancellationToken, MaxDegreeOfParallelism = threads / 2 };
2584 Parallel.For(0, bitmap.Width, parallelOptions, (x, state) =>
2585 {
2586 Parallel.For(0, bitmap.Height, parallelOptions, (y, state) =>
2587 {
2588 var value = bitmap.GetPixel(x, y).ToArgb();
2589  
2590 histogram[value]++;
2591 });
2592 });
2593  
2594 return histogram;
2595 }
2596  
2597 #endregion
2598  
2599 #region Event Handlers
2600  
2601 private void toolStripTextBox1_KeyUp(object sender, KeyEventArgs e)
2602 {
8 office 2603 if (e.KeyCode != Keys.Return)
2604 {
2605 return;
2606 }
7 office 2607  
2608 // Skip the beep.
2609 e.Handled = true;
2610  
2611 var toolStripTextBox = (ToolStripTextBox)sender;
2612 var tagText = toolStripTextBox.Text;
2613 toolStripTextBox.Clear();
2614  
8 office 2615 if (string.IsNullOrEmpty(tagText))
2616 {
2617 return;
2618 }
7 office 2619  
2620 var keywords = new[] { tagText };
2621  
2622 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
2623  
2624 var count = items.Length;
2625  
2626 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
2627 toolStripProgressBar1.Minimum = 0;
2628 toolStripProgressBar1.Maximum = count;
2629 toolStripProgressBar1.Value = 0;
2630  
2631 void ImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
2632 {
2633 switch (e)
2634 {
2635 case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
2636 foreach (var tag in imageListViewItemProgressSuccess.Tags)
2637 {
2638 if (!_tagAutoCompleteStringCollection.Contains(tag))
2639 _tagAutoCompleteStringCollection.Add(tag);
2640  
2641 tagListView.BeginUpdate();
2642 if (tagListView.Items.ContainsKey(tag))
2643 {
2644 tagListView.Items[tag].Checked = true;
2645 tagListView.EndUpdate();
2646  
2647 _sortScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(1000), () =>
2648 {
2649 tagListView.BeginUpdate();
2650 tagListView.Sort();
2651 tagListView.EndUpdate();
2652 }, _formTaskScheduler, _cancellationToken);
2653 continue;
2654 }
2655  
2656 tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
2657 tagListView.Items[tag].Checked = true;
2658 tagListView.EndUpdate();
2659  
2660 _sortScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(1000), () =>
2661 {
2662 tagListView.BeginUpdate();
2663 tagListView.Sort();
2664 tagListView.EndUpdate();
2665 }, _formTaskScheduler, _cancellationToken);
2666 }
2667 break;
2668 case ImageListViewItemProgressFailure<ListViewItem> imageListViewItemProgressFailure:
2669 break;
2670 }
2671  
2672 toolStripStatusLabel1.Text = "Adding tags...";
2673 toolStripProgressBar1.Increment(1);
2674  
8 office 2675 if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
2676 {
2677 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
2678 }
7 office 2679 }
2680  
2681 toolStripStatusLabel1.Text = "Adding tags...";
2682 #pragma warning disable CS4014
2683 Task.Factory.StartNew(async () =>
2684 #pragma warning restore CS4014
2685 {
2686 _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
2687 try
2688 {
2689 await AddTags(items, keywords, _magicMime, _listViewItemProgress, _cancellationToken);
2690 }
2691 catch
2692 {
2693 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
2694 }
2695 }, _cancellationToken);
2696 }
2697  
2698 private void contextMenuStrip1_Opened(object sender, EventArgs e)
2699 {
2700 tagTextBox.Focus();
2701  
2702 }
2703  
2704 private async void Form1_Load(object sender, EventArgs e)
2705 {
2706 JotFormTracker.Tracker.Track(this);
2707  
2708 JotFormTracker.Tracker.Configure<Form1>()
2709 .Properties(form => new { form.tabControl1.SelectedIndex });
2710  
2711 JotFormTracker.Tracker.Configure<Form1>()
2712 .Properties(form => new { form.splitContainer3.SplitterDistance });
2713  
2714 JotFormTracker.Tracker.Configure<Form1>()
2715 .Properties(form => new { form.radioButton1.Checked });
2716  
2717 JotFormTracker.Tracker.Configure<Form1>()
2718 .Properties(form => new { form.checkBox1.Checked, form.checkBox1.CheckState });
2719  
2720 JotFormTracker.Tracker.Configure<Form1>()
2721 .Properties(form => new { form.checkBox2.Checked, form.checkBox2.CheckState });
2722  
2723 JotFormTracker.Tracker.Configure<Form1>()
2724 .Properties(form => new { form.checkBox3.Checked, form.checkBox3.CheckState });
2725  
2726 Configuration = await LoadConfiguration();
2727  
2728 #pragma warning disable CS4014
2729 PerformUpgrade();
2730 #pragma warning restore CS4014
2731  
2732 #pragma warning disable CS4014
2733 Task.Factory.StartNew(async () =>
2734 #pragma warning restore CS4014
2735 {
2736 try
2737 {
2738 await _imageListViewLock.WaitAsync(_cancellationToken);
2739 }
2740 catch
2741 {
2742 return;
2743 }
2744  
2745 this.InvokeIfRequired(form =>
2746 {
2747 form.toolStripStatusLabel1.Text = "Loading images...";
2748 form.toolStripProgressBar1.Style = ProgressBarStyle.Marquee;
2749 form.toolStripProgressBar1.MarqueeAnimationSpeed = 30;
2750 });
2751  
2752 try
2753 {
8 office 2754 var imageListViewItems = new ConcurrentBag<ListViewItem>();
7 office 2755 var tags = new HashSet<string>(StringComparer.Ordinal);
8 office 2756 await foreach (var image in _quickImageDatabase.GetAll(_cancellationToken))
7 office 2757 {
8 office 2758 if (!largeImageList.Images.ContainsKey(image.File))
2759 {
2760 largeImageList.Images.Add(image.File, image.Thumbnail);
2761 }
7 office 2762  
2763 var fileInfo = new FileInfo(image.File);
2764  
2765 if (!_imageListViewGroupDictionary.TryGetValue(fileInfo.DirectoryName, out var group))
2766 {
8 office 2767 group = new ListViewGroup(fileInfo.DirectoryName, HorizontalAlignment.Left) { Name = fileInfo.DirectoryName };
7 office 2768  
8 office 2769 _imageListViewGroupDictionary.TryAdd(fileInfo.DirectoryName, group);
2770  
7 office 2771 imageListView.InvokeIfRequired(view =>
2772 {
8 office 2773 view.BeginUpdate();
7 office 2774 view.Groups.Add(group);
8 office 2775 view.EndUpdate();
7 office 2776 });
2777 }
2778  
2779 tags.UnionWith(image.Tags);
2780  
8 office 2781 var imageListViewItem = new ListViewItem(image.File)
7 office 2782 {
2783 Name = image.File,
2784 ImageKey = image.File,
2785 Text = fileInfo.Name,
2786 Group = group
8 office 2787 };
2788  
2789 imageListViewItems.Add(imageListViewItem);
7 office 2790  
8 office 2791 }
2792  
7 office 2793 this.InvokeIfRequired(_ => { _tagAutoCompleteStringCollection.AddRange(tags.ToArray()); });
2794  
2795 imageListView.InvokeIfRequired(view =>
2796 {
2797 view.BeginUpdate();
2798 view.Items.AddRange(imageListViewItems.ToArray());
2799 view.EndUpdate();
2800 });
2801  
2802 tagListView.InvokeIfRequired(view =>
2803 {
2804 view.BeginUpdate();
2805 view.Items.AddRange(tags.Select(tag => new ListViewItem(tag) { Name = tag }).ToArray());
2806 view.EndUpdate();
8 office 2807 });
7 office 2808 }
2809 catch (Exception exception)
2810 {
2811 Log.Error(exception, "Unable to load images.");
2812 }
2813 finally
2814 {
2815 this.InvokeIfRequired(form =>
2816 {
2817 form.toolStripStatusLabel1.Text = "Done loading images.";
2818 form.toolStripProgressBar1.MarqueeAnimationSpeed = 0;
2819 });
2820  
2821 _imageListViewLock.Release();
2822 }
2823 }, _cancellationToken, TaskCreationOptions.LongRunning, TaskScheduler.Current);
2824 }
2825  
2826 private async void tagListView_MouseDown(object sender, MouseEventArgs e)
2827 {
2828 var listView = (ListView)sender;
8 office 2829 if (!listView.CheckBoxes)
2830 {
2831 return;
2832 }
7 office 2833  
2834 // Allow clicking anywhere on tag.
2835 var hitTest = listView.HitTest(e.Location);
8 office 2836 if (hitTest.Item == null)
2837 {
2838 return;
2839 }
7 office 2840  
2841 var item = hitTest.Item;
2842  
2843 var tagText = item.Text;
2844  
2845 var keywords = new[] { tagText };
2846  
2847 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
2848  
2849 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
2850 toolStripProgressBar1.Minimum = 0;
2851 toolStripProgressBar1.Maximum = items.Length;
2852 toolStripProgressBar1.Value = 0;
2853  
2854 void ImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
2855 {
2856 switch (e)
2857 {
2858 case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
2859 foreach (var tag in imageListViewItemProgressSuccess.Tags)
2860 {
2861 tagListView.BeginUpdate();
2862 if (tagListView.Items.ContainsKey(tag))
2863 {
2864 tagListView.Items[tag].Checked = imageListViewItemProgressSuccess.Check;
2865 tagListView.EndUpdate();
2866 continue;
2867 }
2868  
2869 tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
2870 tagListView.Items[tag].Checked = imageListViewItemProgressSuccess.Check;
2871 tagListView.EndUpdate();
2872 }
2873 break;
2874 case ImageListViewItemProgressFailure<ListViewItem> imageListViewItemProgressFailure:
2875 break;
2876 }
2877  
2878 toolStripProgressBar1.Increment(1);
8 office 2879 if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
2880 {
2881 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
2882 }
7 office 2883 }
2884  
2885 if (item.Checked)
2886 {
2887  
2888 toolStripStatusLabel1.Text = "Removing tags...";
8 office 2889  
7 office 2890 _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
2891 try
2892 {
2893 await RemoveTags(items, keywords, _magicMime, _listViewItemProgress, _cancellationToken);
2894 }
2895 finally
2896 {
2897 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
2898 }
2899  
8 office 2900 switch(hitTest.Location)
2901 {
2902 case ListViewHitTestLocations.Label:
2903 hitTest.Item.Checked = !hitTest.Item.Checked;
2904 break;
2905 }
7 office 2906  
2907 return;
2908 }
2909  
2910 toolStripStatusLabel1.Text = "Adding tags...";
2911 _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
2912 try
2913 {
2914 await AddTags(items, keywords, _magicMime, _listViewItemProgress, _cancellationToken);
2915 }
2916 finally
2917 {
2918 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
2919 }
2920  
8 office 2921 switch(hitTest.Location)
2922 {
2923 case ListViewHitTestLocations.Label:
2924 hitTest.Item.Checked = !hitTest.Item.Checked;
2925 break;
2926 }
7 office 2927 }
2928  
2929 private void aboutToolStripMenuItem_Click(object sender, EventArgs e)
2930 {
8 office 2931 if (_aboutForm != null)
2932 {
2933 return;
2934 }
7 office 2935  
2936 _aboutForm = new AboutForm();
2937 _aboutForm.Closing += AboutForm_Closing;
2938 _aboutForm.Show();
2939 }
2940  
2941 private void AboutForm_Closing(object sender, CancelEventArgs e)
2942 {
8 office 2943 if (_aboutForm == null)
2944 {
2945 return;
2946 }
7 office 2947  
2948 _aboutForm.Closing -= AboutForm_Closing;
2949 _aboutForm.Dispose();
2950 _aboutForm = null;
2951 }
2952  
2953 private async void updateToolStripMenuItem_Click(object sender, EventArgs e)
2954 {
2955 await PerformUpgrade();
2956 }
2957  
2958 private void viewLogsToolStripMenuItem_Click(object sender, EventArgs e)
2959 {
8 office 2960 if (_viewLogsForm != null)
2961 {
2962 return;
2963 }
7 office 2964  
2965 _viewLogsForm = new ViewLogsForm(this, _memorySink, _cancellationToken);
2966 _viewLogsForm.Closing += ViewLogsForm_Closing;
2967 _viewLogsForm.Show();
2968 }
2969  
2970 private void ViewLogsForm_Closing(object sender, CancelEventArgs e)
2971 {
8 office 2972 if (_viewLogsForm == null)
2973 {
2974 return;
2975 }
7 office 2976  
2977 _viewLogsForm.Closing -= ViewLogsForm_Closing;
2978 _viewLogsForm.Close();
2979 _viewLogsForm = null;
2980 }
2981  
2982 private void quitToolStripMenuItem_Click(object sender, EventArgs e)
2983 {
2984 _cancellationTokenSource.Cancel();
2985 Close();
2986 }
2987  
2988 private async void removeToolStripMenuItem_Click(object sender, EventArgs e)
2989 {
2990 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
2991  
8 office 2992 RemoveImages(items, _cancellationToken);
7 office 2993 }
2994  
2995 private void textBox1_TextChanged(object sender, EventArgs e)
2996 {
2997 var textBox = (TextBox)sender;
2998 var text = textBox.Text;
2999  
3000 if (_searchCancellationTokenSource != null) _searchCancellationTokenSource.Cancel();
3001  
3002 _searchCancellationTokenSource = new CancellationTokenSource();
3003 _searchCancellationToken = _searchCancellationTokenSource.Token;
3004 _linkedSearchCancellationTokenSource =
3005 CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
3006 _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;
3007  
3008 if (string.IsNullOrEmpty(text))
3009 {
3010 _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250),
3011 async () => { await EndSearch(); }, _combinedSearchSelectionCancellationToken);
3012  
3013 return;
3014 }
3015  
3016 _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text,
3017 async text => { await BeginSearch(text); }, _combinedSearchSelectionCancellationToken);
3018 }
3019  
3020 private void imageListView_KeyDown(object sender, KeyEventArgs e)
3021 {
3022 if (e.Control && e.KeyCode == Keys.A)
3023 {
3024 foreach (var item in imageListView.Items.OfType<ListViewItem>())
3025 {
3026 item.Selected = true;
3027 }
3028 }
3029 }
3030  
3031 private void imageListView_DragLeave(object sender, EventArgs e)
3032 {
3033 }
3034  
3035 private void deleteToolStripMenuItem_Click(object sender, EventArgs e)
3036 {
3037 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
3038  
3039 DeleteImages(items, _cancellationToken);
3040 }
3041  
3042 private void selectAllToolStripMenuItem1_Click(object sender, EventArgs e)
3043 {
3044 foreach (var item in imageListView.SelectedItems.OfType<ListViewItem>())
3045 {
3046 foreach (var groupItem in item.Group.Items.OfType<ListViewItem>())
3047 {
3048 groupItem.Selected = true;
3049 }
3050 }
3051 }
3052  
3053 private void selectAllToolStripMenuItem_Click(object sender, EventArgs e)
3054 {
3055 foreach (var item in imageListView.Items.OfType<ListViewItem>())
3056 {
3057 item.Selected = true;
3058 }
3059 }
3060  
3061 private void textBox1_KeyDown(object sender, KeyEventArgs e)
3062 {
3063 if (e.Control && e.KeyCode == Keys.A)
3064 {
3065 var textBox = (TextBox)sender;
8 office 3066  
7 office 3067 textBox.SelectAll();
3068 }
3069 }
3070  
3071 private void radioButton2_CheckedChanged(object sender, EventArgs e)
3072 {
3073 _quickImageSearchType = QuickImageSearchType.Any;
3074  
3075 var text = textBox1.Text;
8 office 3076 if (string.IsNullOrEmpty(text))
3077 {
3078 return;
3079 }
7 office 3080  
8 office 3081 if (_searchCancellationTokenSource != null)
3082 {
3083 _searchCancellationTokenSource.Cancel();
3084 }
7 office 3085  
3086 _searchCancellationTokenSource = new CancellationTokenSource();
3087 _searchCancellationToken = _searchCancellationTokenSource.Token;
3088 _linkedSearchCancellationTokenSource =
3089 CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
3090 _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;
3091  
8 office 3092 _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text, async text => { await BeginSearch(text); }, _formTaskScheduler, _combinedSearchSelectionCancellationToken);
7 office 3093 }
3094  
3095 private void radiobutton1_CheckedChanged(object sender, EventArgs e)
3096 {
3097 _quickImageSearchType = QuickImageSearchType.All;
3098  
3099 var text = textBox1.Text;
8 office 3100 if (string.IsNullOrEmpty(text))
3101 {
3102 return;
3103 }
7 office 3104  
3105 if (_searchCancellationTokenSource != null) _searchCancellationTokenSource.Cancel();
3106  
3107 _searchCancellationTokenSource = new CancellationTokenSource();
3108 _searchCancellationToken = _searchCancellationTokenSource.Token;
3109 _linkedSearchCancellationTokenSource =
3110 CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
3111 _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;
3112  
8 office 3113 _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text, async text => { await BeginSearch(text); }, _formTaskScheduler, _combinedSearchSelectionCancellationToken);
7 office 3114 }
3115  
3116 private async void editToolStripMenuItem1_Click(object sender, EventArgs e)
3117 {
3118 var item = imageListView.SelectedItems.OfType<ListViewItem>().FirstOrDefault();
8 office 3119 if (item == null)
7 office 3120 {
8 office 3121 return;
7 office 3122 }
8 office 3123  
3124 if (_editorForm != null)
7 office 3125 {
8 office 3126 return;
7 office 3127 }
3128  
8 office 3129 var mime = await _magicMime.GetMimeType(item.Name, _cancellationToken);
7 office 3130 switch (mime)
3131 {
8 office 3132 case "IMAGE/JPEG":
3133 case "IMAGE/PNG":
3134 case "IMAGE/BMP":
7 office 3135 break;
3136 default:
3137 toolStripStatusLabel1.Text = $"Image format not supported for file {item.Name}";
3138 return;
3139 }
3140  
3141 _editorForm = new EditorForm(item.Name, Configuration, _magicMime, _cancellationToken);
3142 _editorForm.ImageSave += _editorForm_ImageSave;
3143 _editorForm.ImageSaveAs += _editorForm_ImageSaveAs;
3144 _editorForm.Closing += _editorForm_Closing;
3145 _editorForm.Show();
3146 }
3147  
3148 private async void _editorForm_ImageSaveAs(object sender, EditorForm.ImageChangedEventArgs e)
3149 {
3150 using var imageMemoryStream = new MemoryStream(e.ImageBytes);
3151 using (var fileStream = new FileStream(e.FileName, FileMode.OpenOrCreate, FileAccess.Write))
3152 {
3153 await imageMemoryStream.CopyToAsync(fileStream);
3154 }
3155  
3156 await LoadFilesAsync(new[] { e.FileName }, _magicMime, _cancellationToken);
3157 }
3158  
3159 private async void _editorForm_ImageSave(object sender, EditorForm.ImageChangedEventArgs e)
3160 {
3161 using var bitmapMemoryStream = new MemoryStream();
3162 using var imageMemoryStream = new MemoryStream(e.ImageBytes);
3163 using (var fileStream = new FileStream(e.FileName, FileMode.OpenOrCreate, FileAccess.Write))
3164 {
3165 await imageMemoryStream.CopyToAsync(fileStream);
3166 }
3167  
3168 imageMemoryStream.Position = 0L;
3169 using var image = Image.FromStream(imageMemoryStream);
3170 image.Save(bitmapMemoryStream, ImageFormat.Bmp);
3171  
3172 bitmapMemoryStream.Position = 0L;
3173 using var hashBitmap = Image.FromStream(bitmapMemoryStream);
3174 var hash = ImagePhash.ComputeDigest(hashBitmap.ToBitmap().ToLuminanceImage());
3175  
3176 bitmapMemoryStream.Position = 0L;
3177 using var thumbnailBitmap = await CreateThumbnail(bitmapMemoryStream, 128, 128, _cancellationToken);
3178 var thumbnail = new Bitmap(thumbnailBitmap);
3179 thumbnailBitmap.Dispose();
3180  
3181 try
3182 {
8 office 3183 if (!await _quickImageDatabase.RemoveImageAsync(e.FileName, _cancellationToken))
3184 {
3185 throw new ArgumentException($"Could not remove image {e.FileName} from database.");
3186 }
7 office 3187  
8 office 3188 var keywords = await _quickImageDatabase.GetTags(e.FileName, _cancellationToken).ToArrayAsync(_cancellationToken);
7 office 3189  
8 office 3190 if (!await _quickImageDatabase.AddImageAsync(e.FileName, hash, keywords, thumbnail, _cancellationToken))
3191 {
3192 throw new ArgumentException($"Could not add image {e.FileName} to database.");
3193 }
7 office 3194  
3195 this.InvokeIfRequired(form =>
3196 {
3197 form.largeImageList.Images.RemoveByKey(e.FileName);
3198 form.largeImageList.Images.Add(e.FileName, thumbnail);
3199 });
3200 }
3201 catch (Exception exception)
3202 {
3203 Log.Error(exception, "Could not update image in database");
3204 }
3205 }
3206  
3207 private void _editorForm_Closing(object sender, CancelEventArgs e)
3208 {
8 office 3209 if (_editorForm == null)
3210 {
3211 return;
3212 }
7 office 3213  
3214 _editorForm.ImageSave -= _editorForm_ImageSave;
3215 _editorForm.ImageSaveAs -= _editorForm_ImageSaveAs;
3216 _editorForm.Closing -= _editorForm_Closing;
3217 _editorForm.Dispose();
3218 _editorForm = null;
3219 }
3220  
3221 private async void synchronizeTagsToolStripMenuItem_Click(object sender, EventArgs e)
3222 {
3223 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
3224  
3225 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
3226 toolStripProgressBar1.Minimum = 0;
3227 toolStripProgressBar1.Maximum = items.Length;
3228 toolStripProgressBar1.Value = 0;
3229  
3230 void ImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
3231 {
3232 switch (e)
3233 {
3234 case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
8 office 3235 if (!(imageListViewItemProgressSuccess.Item is { } listViewItem))
3236 {
3237 break;
3238 }
7 office 3239  
3240 toolStripStatusLabel1.Text = $"Synchronizing tags for {listViewItem.Name}";
3241  
3242 break;
3243 case ImageListViewItemProgressFailure<ListViewItem> imageListViewItemProgressFailure:
3244 break;
3245 }
3246  
3247 toolStripProgressBar1.Increment(1);
3248 if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
3249 {
3250 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
3251 _imageListViewLock.Release();
3252 }
3253 }
3254  
3255 toolStripStatusLabel1.Text = "Synchronizing image tags with database tags...";
3256 try
3257 {
3258 await _imageListViewLock.WaitAsync(_cancellationToken);
3259 }
3260 catch
3261 {
3262 return;
3263 }
3264  
3265 _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
3266 try
3267 {
3268 await BalanceImageTags(items, _magicMime, _listViewItemProgress, _cancellationToken);
3269 }
3270 catch
3271 {
3272 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
3273 _imageListViewLock.Release();
3274 }
3275 }
3276  
3277 private void checkBox1_VisibleChanged(object sender, EventArgs e)
3278 {
3279 var checkBox = (CheckBox)sender;
3280 if (checkBox.Checked)
3281 {
3282 _quickImageSearchParameters = _quickImageSearchParameters | QuickImageSearchParameters.CaseSensitive;
3283  
3284 return;
3285 }
3286  
3287 _quickImageSearchParameters = _quickImageSearchParameters & ~QuickImageSearchParameters.CaseSensitive;
3288 }
3289  
3290 private void checkBox1_CheckedChanged(object sender, EventArgs e)
3291 {
3292 var checkBox = (CheckBox)sender;
3293 switch (checkBox.Checked)
3294 {
3295 case true:
3296 _quickImageSearchParameters =
3297 _quickImageSearchParameters | QuickImageSearchParameters.CaseSensitive;
3298  
3299 break;
3300 case false:
3301 _quickImageSearchParameters =
3302 _quickImageSearchParameters & ~QuickImageSearchParameters.CaseSensitive;
3303 break;
3304 }
3305  
3306 var text = textBox1.Text;
8 office 3307 if (string.IsNullOrEmpty(text))
3308 {
3309 return;
3310 }
7 office 3311  
8 office 3312 if (_searchCancellationTokenSource != null)
3313 {
3314 _searchCancellationTokenSource.Cancel();
3315 }
7 office 3316  
3317 _searchCancellationTokenSource = new CancellationTokenSource();
3318 _searchCancellationToken = _searchCancellationTokenSource.Token;
3319 _linkedSearchCancellationTokenSource =
3320 CancellationTokenSource.CreateLinkedTokenSource(_cancellationToken, _searchCancellationToken);
3321 _combinedSearchSelectionCancellationToken = _linkedSearchCancellationTokenSource.Token;
3322  
8 office 3323 _searchScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(250), text, async text => { await BeginSearch(text); }, _formTaskScheduler, _combinedSearchSelectionCancellationToken);
7 office 3324 }
3325  
3326 private void PreviewFormClosing(object sender, CancelEventArgs e)
3327 {
8 office 3328 if (_previewForm == null)
3329 {
3330 return;
3331 }
7 office 3332  
3333 _previewForm.Closing -= PreviewFormClosing;
3334 _previewForm.Dispose();
3335 _previewForm = null;
3336 }
3337  
3338 private async void perceptionToolStripMenuItem1_Click(object sender, EventArgs e)
3339 {
3340 await SortImageListView(new PerceptionImageListViewItemSorter(
3341 imageListView.Items.OfType<ListViewItem>().ToList(),
3342 _quickImageDatabase,
3343 _cancellationToken));
3344 }
3345  
3346 private void openDirectoryToolStripMenuItem_Click(object sender, EventArgs e)
3347 {
3348 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
3349  
3350 var item = items.FirstOrDefault();
8 office 3351 if (item == null)
3352 {
3353 return;
3354 }
7 office 3355  
3356 Process.Start("explorer.exe", $"/select, \"{item.Name}\"");
3357 }
3358  
3359 private void aboutToolStripMenuItem1_Click(object sender, EventArgs e)
3360 {
8 office 3361 if (_aboutForm != null)
3362 {
3363 return;
3364 }
7 office 3365  
3366 _aboutForm = new AboutForm();
3367 _aboutForm.Closing += AboutForm_Closing;
3368 _aboutForm.Show();
3369 }
3370  
3371 private async void imageListView_DragDrop(object sender, DragEventArgs e)
3372 {
9 office 3373 await LoadDataObjectAsync(e.Data);
7 office 3374 }
3375  
3376 private void imageListView_DragEnter(object sender, DragEventArgs e)
3377 {
3378 e.Effect = e.AllowedEffect;
3379 }
3380  
3381 private void imageListView_MouseDoubleClick(object sender, MouseEventArgs e)
3382 {
3383 var listView = (ListView)sender;
3384 var info = listView.HitTest(e.X, e.Y);
3385  
3386 switch (info.Location)
3387 {
3388 case ListViewHitTestLocations.AboveClientArea:
3389 case ListViewHitTestLocations.BelowClientArea:
3390 case ListViewHitTestLocations.LeftOfClientArea:
3391 case ListViewHitTestLocations.RightOfClientArea:
3392 case ListViewHitTestLocations.None:
3393 return;
3394 }
3395  
3396 var item = info.Item;
3397  
8 office 3398 if (item == null)
3399 {
3400 return;
3401 }
7 office 3402  
8 office 3403 if (_previewForm != null)
3404 {
7 office 3405 _previewForm.InvokeIfRequired(form =>
3406 {
3407 form.Close();
3408 form = null;
8 office 3409 });
3410 }
7 office 3411  
3412 try
3413 {
3414 new Thread(() =>
3415 {
3416 _previewForm = new PreviewForm(item.Name, Configuration, _magicMime, _cancellationToken);
3417 _previewForm.Closing += PreviewFormClosing;
3418 _previewForm.ShowDialog();
3419 }).Start();
3420 }
3421 catch (Exception exception)
3422 {
3423 Log.Error(exception, $"File {item.Name} could not be displayed due to the path not being accessible.");
3424 }
3425 }
3426  
3427 private void imageListView_MouseUp(object sender, MouseEventArgs e)
3428 {
3429 var items = imageListView.SelectedItems.OfType<ListViewItem>();
3430  
3431 SelectTags(items);
3432 }
3433  
8 office 3434 private void imageListView_KeyUp(object sender, KeyEventArgs e)
7 office 3435 {
3436 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
3437 if (e.KeyCode == Keys.Delete)
3438 {
8 office 3439 RemoveImages(items, _cancellationToken);
7 office 3440 return;
3441 }
3442  
3443 SelectTags(items);
3444 }
3445  
3446 private async void imageListView_ItemDrag(object sender, ItemDragEventArgs e)
3447 {
3448 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
3449 toolStripProgressBar1.Minimum = 0;
3450 toolStripProgressBar1.Value = 0;
3451 toolStripProgressBar1.Maximum = 3;
3452  
8 office 3453 var inputBlock = new BufferBlock<ListViewItem>(new DataflowBlockOptions { CancellationToken = _cancellationToken });
7 office 3454 var fileTransformBlock =
8 office 3455 new TransformBlock<ListViewItem, (string Source, string Path, string Name, string Mime, string Extension)>(async item =>
7 office 3456 {
3457 var mime = await _magicMime.GetMimeType(item.Name, _cancellationToken);
3458  
3459 var extension = Path.GetExtension(item.Name);
3460 var path = Path.GetTempPath();
3461 var file = Path.GetFileNameWithoutExtension(item.Name);
3462 if (Configuration.OutboundDragDrop.RenameOnDragDrop)
3463 {
3464 switch (Configuration.OutboundDragDrop.DragDropRenameMethod)
3465 {
3466 case DragDropRenameMethod.Random:
8 office 3467 file = string.Join("", Enumerable.Repeat(0, 5).Select(n => (char)_random.Next('a', 'z' + 1)));
7 office 3468 break;
3469 case DragDropRenameMethod.Timestamp:
3470 file = $"{DateTimeOffset.Now.ToUnixTimeSeconds()}";
3471 break;
3472 }
3473 }
3474  
3475 this.InvokeIfRequired(form =>
3476 {
3477 form.toolStripStatusLabel1.Text = $"File {item.Name} scanned for drag and drop...";
3478 });
3479  
3480 return (Source: item.Name, Path: path, Name: file, Mime: mime, Extension: extension);
3481 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3482  
3483 #pragma warning disable CS4014
3484 fileTransformBlock.Completion.ContinueWith(_ =>
3485 #pragma warning restore CS4014
3486 {
3487 toolStripStatusLabel1.Text = "All files scanned for drag and drop.";
3488 toolStripProgressBar1.Increment(1);
3489 }, _formTaskScheduler);
3490  
3491 var noConvertTransformBlock =
8 office 3492 new TransformBlock<(string Source, string Path, string Name, string Mime, string Extension), string>(async item =>
7 office 3493 {
3494 var destination = Path.Combine(item.Path, $"{item.Name}{item.Extension}");
8 office 3495  
7 office 3496 await Miscellaneous.CopyFileAsync(item.Source, destination, _cancellationToken);
3497  
3498 this.InvokeIfRequired(form =>
3499 {
3500 form.toolStripStatusLabel1.Text =
3501 $"File {item.Source} does not need conversion for drag and drop...";
3502 });
3503 return destination;
3504 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3505  
3506 #pragma warning disable CS4014
3507 noConvertTransformBlock.Completion.ContinueWith(_ =>
3508 #pragma warning restore CS4014
3509 {
3510 toolStripStatusLabel1.Text = "Conversion complete for drag and drop.";
3511 toolStripProgressBar1.Increment(1);
3512 }, _formTaskScheduler);
3513  
3514 var jpegTransformBlock =
8 office 3515 new TransformBlock<(string Source, string Path, string Name, string Mime, string Extension), string>(async file =>
7 office 3516 {
3517 using var imageStream =
3518 await _imageTool.ConvertTo(file.Source, MagickFormat.Jpeg, _cancellationToken);
3519  
3520 var jpegDestination = Path.Combine(file.Path, $"{file.Name}.jpg");
8 office 3521 await Miscellaneous.CopyFileAsync(imageStream, jpegDestination, _cancellationToken);
7 office 3522  
3523 this.InvokeIfRequired(form =>
3524 {
3525 form.toolStripStatusLabel1.Text =
3526 $"File {file.Source} converted to JPEG for drag and drop...";
3527 });
3528  
3529 return jpegDestination;
3530 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3531  
3532 #pragma warning disable CS4014
3533 jpegTransformBlock.Completion.ContinueWith(_ =>
3534 #pragma warning restore CS4014
3535 {
3536 toolStripStatusLabel1.Text = "Conversion complete for drag and drop.";
3537 toolStripProgressBar1.Increment(1);
3538 }, _formTaskScheduler);
3539  
3540 var pngTransformBlock =
8 office 3541 new TransformBlock<(string Source, string Path, string Name, string Mime, string Extension), string>(async file =>
7 office 3542 {
8 office 3543 using var imageStream = await _imageTool.ConvertTo(file.Source, MagickFormat.Png, _cancellationToken);
7 office 3544  
3545 var pngDestination = Path.Combine(file.Path, $"{file.Name}.png");
3546 await Miscellaneous.CopyFileAsync(imageStream, pngDestination, _cancellationToken);
3547  
3548 this.InvokeIfRequired(form =>
3549 {
3550 form.toolStripStatusLabel1.Text =
3551 $"File {file.Source} converted to PNG for drag and drop...";
3552 });
3553  
3554 return pngDestination;
3555 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3556  
3557 #pragma warning disable CS4014
3558 pngTransformBlock.Completion.ContinueWith(_ =>
3559 #pragma warning restore CS4014
3560 {
3561 toolStripStatusLabel1.Text = "Conversion complete for drag and drop.";
3562 toolStripProgressBar1.Increment(1);
3563 }, _formTaskScheduler);
3564  
3565 var bmpTransformBlock =
8 office 3566 new TransformBlock<(string Source, string Path, string Name, string Mime, string Extension), string>( async file =>
7 office 3567 {
8 office 3568 using var imageStream = await _imageTool.ConvertTo(file.Source, MagickFormat.Jpeg, _cancellationToken);
7 office 3569  
3570 var bmpDestination = Path.Combine(file.Path, $"{file.Name}.bmp");
3571 await Miscellaneous.CopyFileAsync(imageStream, bmpDestination, _cancellationToken);
3572  
3573 this.InvokeIfRequired(form =>
3574 {
3575 form.toolStripStatusLabel1.Text =
3576 $"File {file.Source} converted to BMP for drag and drop...";
3577 });
3578  
3579 return bmpDestination;
3580 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3581  
3582 #pragma warning disable CS4014
3583 bmpTransformBlock.Completion.ContinueWith(_ =>
3584 #pragma warning restore CS4014
3585 {
3586 toolStripStatusLabel1.Text = "Conversion complete for drag and drop.";
3587 toolStripProgressBar1.Increment(1);
3588 }, _formTaskScheduler);
3589  
3590 var iptcStripTransformBlock = new TransformBlock<string, string>(async file =>
3591 {
3592 try
3593 {
3594 var mime = await _magicMime.GetMimeType(file, _cancellationToken);
3595 if (Configuration.SupportedFormats.IsSupportedVideo(mime))
3596 {
3597 return file;
3598 }
3599  
3600 if (!Configuration.SupportedFormats.IsSupportedImage(mime))
3601 {
3602 throw new ArgumentException();
3603 }
3604  
3605 if (!await _tagManager.StripIptcProfile(file, _cancellationToken))
3606 {
3607 throw new ArgumentException();
3608 }
3609 }
3610 catch
3611 {
3612 this.InvokeIfRequired(form =>
3613 {
3614 form.toolStripStatusLabel1.Text =
3615 $"Failed to strip IPTC tags for file {file} for drag and drop...";
3616 });
3617  
3618 return string.Empty;
3619 }
3620  
3621 this.InvokeIfRequired(form =>
3622 {
3623 form.toolStripStatusLabel1.Text = $"Stripped IPTC tags from file {file} for drag and drop...";
3624 });
3625  
3626 return file;
3627 }, new ExecutionDataflowBlockOptions { CancellationToken = _cancellationToken });
3628  
3629 #pragma warning disable CS4014
3630 iptcStripTransformBlock.Completion.ContinueWith(_ =>
3631 #pragma warning restore CS4014
3632 {
3633 toolStripStatusLabel1.Text = "All tags stripped for drag and drop.";
3634 toolStripProgressBar1.Increment(1);
3635 }, _formTaskScheduler);
3636  
3637 var outputBlock = new BufferBlock<string>(new DataflowBlockOptions
3638 { CancellationToken = _cancellationToken });
3639  
3640 using var _1 =
3641 inputBlock.LinkTo(fileTransformBlock, new DataflowLinkOptions { PropagateCompletion = true });
3642  
3643 using var _2 = fileTransformBlock.LinkTo(jpegTransformBlock, item =>
3644 Configuration.OutboundDragDrop.ConvertOnDragDrop &&
3645 !Configuration.SupportedFormats.IsSupportedVideo(item.Mime) &&
3646 !Configuration.OutboundDragDrop.DragDropConvertExclude.IsExcludedImage(item.Mime) &&
3647 Configuration.SupportedFormats.IsSupported(item.Mime) &&
3648 Configuration.OutboundDragDrop.DragDropConvertType == "image/jpeg" &&
3649 item.Mime != "image/jpeg");
3650 using var _3 =
3651 jpegTransformBlock.LinkTo(iptcStripTransformBlock,
3652 file => Configuration.OutboundDragDrop.StripTagsOnDragDrop);
3653 using var _11 =
3654 jpegTransformBlock.LinkTo(outputBlock);
3655 using var _4 = fileTransformBlock.LinkTo(pngTransformBlock, item =>
3656 Configuration.OutboundDragDrop.ConvertOnDragDrop &&
3657 !Configuration.SupportedFormats.IsSupportedVideo(item.Mime) &&
3658 !Configuration.OutboundDragDrop.DragDropConvertExclude.IsExcludedImage(item.Mime) &&
3659 Configuration.SupportedFormats.IsSupported(item.Mime) &&
3660 Configuration.OutboundDragDrop.DragDropConvertType == "image/png" &&
3661 item.Mime != "image/png");
3662 using var _5 =
3663 pngTransformBlock.LinkTo(iptcStripTransformBlock,
3664 file => Configuration.OutboundDragDrop.StripTagsOnDragDrop);
3665 using var _12 =
3666 pngTransformBlock.LinkTo(outputBlock);
3667 using var _6 = fileTransformBlock.LinkTo(bmpTransformBlock, item =>
3668 Configuration.OutboundDragDrop.ConvertOnDragDrop &&
3669 !Configuration.SupportedFormats.IsSupportedVideo(item.Mime) &&
3670 !Configuration.OutboundDragDrop.DragDropConvertExclude.IsExcludedImage(item.Mime) &&
3671 Configuration.SupportedFormats.IsSupported(item.Mime) &&
3672 Configuration.OutboundDragDrop.DragDropConvertType == "image/bmp" &&
3673 item.Mime != "image/bmp");
3674 using var _7 =
3675 bmpTransformBlock.LinkTo(iptcStripTransformBlock,
3676 file => Configuration.OutboundDragDrop.StripTagsOnDragDrop);
3677 using var _13 =
3678 bmpTransformBlock.LinkTo(outputBlock);
3679  
3680 using var _8 = fileTransformBlock.LinkTo(noConvertTransformBlock);
3681 using var _15 = fileTransformBlock.LinkTo(DataflowBlock
3682 .NullTarget<(string Source, string Path, string Name, string Mime, string Extension)>());
3683  
3684 using var _9 = noConvertTransformBlock.LinkTo(iptcStripTransformBlock,
3685 file => Configuration.OutboundDragDrop.StripTagsOnDragDrop);
3686  
3687 using var _10 =
3688 iptcStripTransformBlock.LinkTo(outputBlock, file => !string.IsNullOrEmpty(file));
3689 using var _16 = iptcStripTransformBlock.LinkTo(DataflowBlock.NullTarget<string>());
3690 using var _14 =
3691 noConvertTransformBlock.LinkTo(outputBlock, file => !string.IsNullOrEmpty(file));
3692 using var _17 = noConvertTransformBlock.LinkTo(DataflowBlock.NullTarget<string>());
3693  
3694 foreach (var item in imageListView.SelectedItems.OfType<ListViewItem>())
3695 {
3696 #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3697 inputBlock.SendAsync(item, _cancellationToken);
3698 #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed
3699 }
3700  
3701 inputBlock.Complete();
3702  
3703 await fileTransformBlock.Completion.ContinueWith(_ =>
3704 {
3705 jpegTransformBlock.Complete();
3706 pngTransformBlock.Complete();
3707 bmpTransformBlock.Complete();
3708 noConvertTransformBlock.Complete();
3709 iptcStripTransformBlock.Complete();
3710 }, _cancellationToken);
3711  
3712 await Task.WhenAll(
3713 jpegTransformBlock.Completion,
3714 pngTransformBlock.Completion,
3715 bmpTransformBlock.Completion,
3716 iptcStripTransformBlock.Completion,
8 office 3717 noConvertTransformBlock.Completion).ContinueWith(_ =>
3718 {
7 office 3719 outputBlock.Complete();
3720 }, _cancellationToken);
3721  
3722 var files = new HashSet<string>();
3723 while (await outputBlock.OutputAvailableAsync(_cancellationToken))
3724 {
3725 if (!outputBlock.TryReceiveAll(out var items))
3726 {
3727 continue;
3728 }
3729  
3730 files.UnionWith(items);
3731 }
3732  
3733 files.RemoveWhere(string.IsNullOrEmpty);
3734  
3735 this.InvokeIfRequired(form =>
3736 {
3737 form.toolStripStatusLabel1.Text = $"{files.Count} ready for drag and drop...";
3738 });
3739  
3740 if (files.Count == 0)
3741 {
3742 return;
3743 }
3744  
3745 var data = new DataObject(DataFormats.FileDrop, files.ToArray());
8 office 3746 this.InvokeIfRequired(_ =>
3747 {
7 office 3748 DoDragDrop(data, DragDropEffects.Copy);
3749 });
3750 }
3751  
3752 private void imageListView_DragOver(object sender, DragEventArgs e)
3753 {
3754 }
3755  
3756 private async void nameToolStripMenuItem_Click(object sender, EventArgs e)
3757 {
3758 await SortImageListView(new NameImageListViewItemSorter());
3759 }
3760  
3761 private async void ascendingToolStripMenuItem_Click(object sender, EventArgs e)
3762 {
3763 await SortImageListView(new SizeImageListViewItemSorter(SortOrder.Ascending));
3764 }
3765  
3766 private async void descendingToolStripMenuItem_Click(object sender, EventArgs e)
3767 {
3768 await SortImageListView(new SizeImageListViewItemSorter(SortOrder.Descending));
3769 }
3770  
3771 private async void typeToolStripMenuItem_Click(object sender, EventArgs e)
3772 {
3773 await SortImageListView(new TypeImageListViewItemSorter());
3774 }
3775  
3776 private async void importToolStripMenuItem_Click(object sender, EventArgs e)
3777 {
3778 var dialog = new CommonOpenFileDialog
3779 {
3780 AddToMostRecentlyUsedList = true,
3781 Multiselect = true,
3782 IsFolderPicker = false
3783 };
3784  
8 office 3785 switch (dialog.ShowDialog())
3786 {
3787 case CommonFileDialogResult.Ok:
3788 await LoadFilesAsync(dialog.FileNames, _magicMime, _cancellationToken);
3789 break;
3790 }
7 office 3791 }
3792  
3793 private async void importDirectoryToolStripMenuItem_Click(object sender, EventArgs e)
3794 {
3795 var dialog = new CommonOpenFileDialog
3796 {
3797 AddToMostRecentlyUsedList = true,
3798 Multiselect = true,
3799 IsFolderPicker = true
3800 };
3801  
8 office 3802 switch (dialog.ShowDialog())
3803 {
3804 case CommonFileDialogResult.Ok:
3805 await LoadFilesAsync(dialog.FileNames, _magicMime, _cancellationToken);
3806 break;
3807 }
7 office 3808 }
3809  
3810 private void settingsToolStripMenuItem_Click(object sender, EventArgs e)
3811 {
8 office 3812 if (_settingsForm != null)
3813 {
3814 return;
3815 }
7 office 3816  
3817 _settingsForm = new SettingsForm(Configuration, _cancellationToken);
3818 _settingsForm.Closing += SettingsForm_Closing;
3819 _settingsForm.Show();
3820 }
3821  
3822 private void SettingsForm_Closing(object sender, CancelEventArgs e)
3823 {
8 office 3824 if (_settingsForm == null)
3825 {
3826 return;
3827 }
7 office 3828  
8 office 3829 if (_settingsForm.SaveOnClose)
3830 {
3831 // Commit the configuration.
7 office 3832 _changedConfigurationContinuation.Schedule(TimeSpan.FromSeconds(1),
8 office 3833 async () => { await SaveConfiguration(Configuration); }, _cancellationToken);
3834 }
7 office 3835  
3836 _settingsForm.Closing -= SettingsForm_Closing;
3837 _settingsForm.Dispose();
3838 _settingsForm = null;
3839 }
3840  
3841 private async void removeTagsToolStripMenuItem_Click(object sender, EventArgs e)
3842 {
3843 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
3844  
3845 var count = items.Length;
3846  
3847 toolStripProgressBar1.Style = ProgressBarStyle.Continuous;
3848 toolStripProgressBar1.Minimum = 0;
3849 toolStripProgressBar1.Maximum = count;
3850 toolStripProgressBar1.Value = 0;
3851  
3852 void ImageListViewItemProgress(object sender, ImageListViewItemProgress<ListViewItem> e)
3853 {
3854 switch (e)
3855 {
3856 case ImageListViewItemProgressSuccess<ListViewItem> imageListViewItemProgressSuccess:
3857 if (!(imageListViewItemProgressSuccess.Item is { } imageListViewItem)) break;
3858  
3859 foreach (var tag in imageListViewItemProgressSuccess.Tags)
3860 {
3861 tagListView.BeginUpdate();
3862 if (tagListView.CheckedItems.ContainsKey(tag))
3863 {
3864 tagListView.Items[tag].Checked = false;
3865 tagListView.EndUpdate();
3866 continue;
3867 }
3868  
3869 tagListView.Items.Add(new ListViewItem(tag) { Name = tag });
3870 tagListView.Items[tag].Checked = false;
3871 tagListView.EndUpdate();
3872 }
3873  
3874 break;
3875 case ImageListViewItemProgressFailure<ListViewItem> _:
3876 break;
3877 }
3878  
3879 toolStripStatusLabel1.Text = "Stripping tags...";
3880 toolStripProgressBar1.Increment(1);
3881 if (toolStripProgressBar1.Value == toolStripProgressBar1.Maximum)
3882 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
3883 }
3884  
3885 toolStripStatusLabel1.Text = "Stripping tags...";
3886  
3887 _listViewItemProgress.ProgressChanged += ImageListViewItemProgress;
3888 try
3889 {
3890 await StripTags(items, _magicMime, _listViewItemProgress, _cancellationToken);
3891 }
3892 catch
3893 {
3894 _listViewItemProgress.ProgressChanged -= ImageListViewItemProgress;
3895 }
3896 }
3897  
3898 private async void balanceTagsToolStripMenuItem_Click(object sender, EventArgs e)
3899 {
3900 var items = imageListView.SelectedItems.OfType<ListViewItem>();
3901  
3902 var listViewItems = items as ListViewItem[] ?? items.ToArray();
8 office 3903 if (listViewItems.Length < 2)
3904 {
3905 return;
3906 }
7 office 3907  
3908 await BalanceTags(listViewItems);
3909 }
3910  
3911 private async void loadMissingToolStripMenuItem_Click(object sender, EventArgs e)
3912 {
3913 var items = imageListView.SelectedItems.OfType<ListViewItem>();
3914  
3915 var listViewItems = items as ListViewItem[] ?? items.ToArray();
3916  
3917 await LoadFilesAsync(listViewItems.Select(item => item.Group.Name), _magicMime, _cancellationToken);
3918 }
3919  
3920 private void moveToolStripMenuItem_DropDownOpening(object sender, EventArgs e)
3921 {
3922 var menuItem = (ToolStripMenuItem)sender;
3923  
3924 foreach (var group in _imageListViewGroupDictionary.Keys)
3925 {
8 office 3926 if (menuItem.DropDownItems.ContainsKey(group))
3927 {
3928 continue;
3929 }
7 office 3930  
3931 var toolStripMenuSubItem = new ToolStripButton(group) { Name = group };
3932 toolStripMenuSubItem.Click += moveTargetToolStripMenuItem_Click;
3933 menuItem.DropDownItems.Add(toolStripMenuSubItem);
3934 }
8 office 3935 }
3936  
3937 private void moveToolStripMenuItem_DropDownClosed(object sender, EventArgs e)
3938 {
3939 var menuItem = (ToolStripMenuItem)sender;
3940  
3941 var items = new ConcurrentBag<ToolStripButton>();
3942 foreach(var toolStripMenuSubItem in menuItem.DropDownItems.OfType<ToolStripButton>())
3943 {
3944 toolStripMenuSubItem.Click -= moveTargetToolStripMenuItem_Click;
3945  
3946 items.Add(toolStripMenuSubItem);
3947 }
3948  
3949 foreach(var toolStripMenuSubItem in items)
3950 {
3951 menuItem.DropDownItems.Remove(toolStripMenuSubItem);
3952 }
7 office 3953 }
3954  
3955 private async void moveTargetToolStripMenuItem_Click(object sender, EventArgs e)
3956 {
3957 var menuItem = (ToolStripButton)sender;
3958  
3959 var items = imageListView.SelectedItems.OfType<ListViewItem>();
3960  
3961 await MoveImagesAsync(items, menuItem.Name, _cancellationToken);
3962 }
3963  
3964 private void imageListView_GroupHeaderClick(object sender, ListViewGroup e)
3965 {
3966 var listViewCollapsible = (ListViewCollapsible)sender;
3967  
3968 var collapsed = listViewCollapsible.GetCollapsed(e);
3969 if (collapsed)
3970 {
3971 listViewCollapsible.SetCollapsed(e, false);
3972 return;
3973 }
3974  
3975 listViewCollapsible.SetCollapsed(e, true);
3976 }
3977  
3978 private async void convertToTypeToolStripMenuItem_Click(object sender, EventArgs e)
3979 {
3980 var toolStripMenuItem = (ToolStripMenuItem)sender;
3981 var extension = toolStripMenuItem.Text;
3982  
3983 var extensionToMime = new Dictionary<string, string>
3984 {
3985 { "jpg", "image/jpeg" },
3986 { "png", "image/png" },
3987 { "bmp", "image/bmp" },
3988 { "gif", "image/gif" }
3989 };
3990  
8 office 3991 if (!Configuration.SupportedFormats.Images.Image.Contains(extensionToMime[extension]))
3992 {
3993 return;
3994 }
7 office 3995  
3996 var items = imageListView.SelectedItems.OfType<ListViewItem>();
3997  
3998 await ConvertImagesAsync(items, extension, _cancellationToken);
3999 }
4000  
4001 #endregion
4002  
4003 private void button1_Click(object sender, EventArgs e)
4004 {
4005 _sortScheduledContinuation.Schedule(TimeSpan.FromMilliseconds(50), () =>
4006 {
4007 tagListView.BeginUpdate();
4008 tagListView.Sort();
4009 tagListView.EndUpdate();
4010 }, _formTaskScheduler, _cancellationToken);
4011 }
4012  
4013 /// <summary>
4014 /// Enable or disable menu items recursively.
4015 /// </summary>
4016 /// <param name="item">the menu item from where to start disabling or enabling menu items</param>
4017 /// <param name="enable">whether to enable or disable the menu item
4018 /// </param>
4019 private void ToggleMenuItemsRecursive(ToolStripMenuItem item, MenuItemsToggleOperation menuItemsToggleOperation)
4020 {
4021 if (!item.HasDropDown)
4022 {
4023 return;
4024 }
4025  
4026 switch (menuItemsToggleOperation)
4027 {
4028 case MenuItemsToggleOperation.NONE:
4029 throw new ArgumentException("Unknown menu toggle operation.");
4030 case MenuItemsToggleOperation.ENABLE:
4031 item.Enabled = true;
4032 break;
4033 case MenuItemsToggleOperation.DISABLE:
4034 item.Enabled = false;
4035 break;
4036 }
4037  
4038 foreach (var menuItem in item.DropDownItems.OfType<ToolStripMenuItem>())
4039 {
4040 ToggleMenuItemsRecursive(menuItem, menuItemsToggleOperation);
4041 }
4042 }
4043  
4044 private void contextMenuStrip1_Closing(object sender, ToolStripDropDownClosingEventArgs e)
4045 {
4046 ToggleMenuItemsRecursive(directoryToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
4047 ToggleMenuItemsRecursive(fileToolStripMenuItem1, MenuItemsToggleOperation.ENABLE);
4048 ToggleMenuItemsRecursive(imageToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
4049 ToggleMenuItemsRecursive(taggingToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
4050 }
4051  
4052 private void contextMenuStrip1_Opening(object sender, CancelEventArgs e)
4053 {
4054 var mousePoint = imageListView.PointToClient(Cursor.Position);
4055 var listViewHitTestInfo = imageListView.HitTest(mousePoint);
4056 // check if the mouse was hovering over an item
4057 if (listViewHitTestInfo.Item != null)
4058 {
4059 // hovering
4060 ToggleMenuItemsRecursive(directoryToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
4061 ToggleMenuItemsRecursive(fileToolStripMenuItem1, MenuItemsToggleOperation.ENABLE);
4062 ToggleMenuItemsRecursive(imageToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
4063 ToggleMenuItemsRecursive(taggingToolStripMenuItem, MenuItemsToggleOperation.ENABLE);
4064 return;
4065 }
4066  
4067 // disable menu items not related to list view items
4068 ToggleMenuItemsRecursive(directoryToolStripMenuItem, MenuItemsToggleOperation.DISABLE);
4069 ToggleMenuItemsRecursive(fileToolStripMenuItem1, MenuItemsToggleOperation.DISABLE);
4070 ToggleMenuItemsRecursive(imageToolStripMenuItem, MenuItemsToggleOperation.DISABLE);
4071 ToggleMenuItemsRecursive(taggingToolStripMenuItem, MenuItemsToggleOperation.DISABLE);
4072 }
9 office 4073  
4074 private void copyCtrlToolStripMenuItem_Click(object sender, EventArgs e)
4075 {
4076 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
4077 switch(items.Length)
4078 {
4079 case 0:
4080 break;
4081 default:
4082 // copy file paths as the default for multiple files selected
4083 var paths = new StringCollection();
4084 foreach (var item in items)
4085 {
4086 paths.Add(item.Name);
4087 }
4088  
4089 Clipboard.SetFileDropList(paths);
4090 break;
4091 }
4092 }
4093  
4094 private async void copyAsMediaToolStripMenuItem_Click(object sender, EventArgs e)
4095 {
4096 var items = imageListView.SelectedItems.OfType<ListViewItem>().ToArray();
4097  
4098 switch (items.Length)
4099 {
4100 case 1:
4101  
4102 {
4103 var file = items[0].Name;
4104  
4105 using var memoryStream = new MemoryStream();
4106 using var fileStream = new FileStream(file, FileMode.Open, FileAccess.Read, FileShare.Read);
4107 await fileStream.CopyToAsync(memoryStream);
4108 memoryStream.Position = 0L;
4109  
4110 var mime = _magicMime.Identify(file, memoryStream, _cancellationToken);
4111 if (Configuration.SupportedFormats.IsSupportedVideo(mime.Definition.File.MimeType))
4112 {
4113  
4114  
4115 Clipboard.SetDataObject(memoryStream);
4116 break;
4117 }
4118  
4119 if (!Configuration.SupportedFormats.IsSupportedImage(mime.Definition.File.MimeType))
4120 {
4121 using var image = Image.FromFile(file);
4122 Clipboard.SetImage(image);
4123 break;
4124 }
4125 }
4126  
4127 break;
4128 case 0:
4129 break;
4130 default:
4131 // copy file paths as the default for multiple files selected
4132 var paths = new StringCollection();
4133 foreach (var item in items)
4134 {
4135 paths.Add(item.Name);
4136 }
4137  
4138 Clipboard.SetFileDropList(paths);
4139 break;
4140 }
4141  
4142 }
4143  
4144 private async void pasteCtrlCToolStripMenuItem_Click(object sender, EventArgs e)
4145 {
4146 var dataObject = Clipboard.GetDataObject();
4147  
4148 await LoadDataObjectAsync(dataObject);
4149 }
4150  
4151  
7 office 4152 }
4153 }