ImageIterator.cs 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904
  1. using System.Diagnostics;
  2. using Avalonia.Threading;
  3. using PicView.Avalonia.Gallery;
  4. using PicView.Avalonia.ImageHandling;
  5. using PicView.Avalonia.Input;
  6. using PicView.Avalonia.UI;
  7. using PicView.Avalonia.ViewModels;
  8. using PicView.Core.ArchiveHandling;
  9. using PicView.Core.DebugTools;
  10. using PicView.Core.FileHandling;
  11. using PicView.Core.FileHistory;
  12. using PicView.Core.Gallery;
  13. using PicView.Core.Models;
  14. using PicView.Core.Navigation;
  15. using PicView.Core.Preloading;
  16. using Timer = System.Timers.Timer;
  17. namespace PicView.Avalonia.Navigation;
  18. public class ImageIterator : IAsyncDisposable
  19. {
  20. #region Properties
  21. private bool _disposed;
  22. public List<FileInfo> ImagePaths { get; private set; }
  23. public int CurrentIndex { get; private set; }
  24. public int GetNonZeroIndex => CurrentIndex + 1 > GetCount ? 1 : CurrentIndex + 1;
  25. public int NextIndex => GetIteration(CurrentIndex, NavigateTo.Next);
  26. public int GetCount => ImagePaths.Count;
  27. public FileInfo InitialFileInfo { get; private set; } = null!;
  28. public bool IsReversed { get; private set; }
  29. private PreLoader PreLoader { get; } = new(GetImageModel.GetImageModelAsync);
  30. private static FileSystemWatcher? _watcher;
  31. private bool _isRunning;
  32. private readonly MainViewModel? _vm;
  33. #endregion
  34. #region Constructors
  35. public ImageIterator(FileInfo fileInfo, MainViewModel vm)
  36. {
  37. #if DEBUG
  38. ArgumentNullException.ThrowIfNull(fileInfo);
  39. #endif
  40. _vm = vm;
  41. FileInfo initialDirectory;
  42. if (Settings.Sorting.IncludeSubDirectories)
  43. {
  44. if (!string.IsNullOrWhiteSpace(Settings.StartUp.StartUpDirectory) && !ArchiveExtraction.IsArchived)
  45. {
  46. if (fileInfo.FullName.Contains(Settings.StartUp.StartUpDirectory))
  47. {
  48. initialDirectory = new FileInfo(Settings.StartUp.StartUpDirectory);
  49. }
  50. else
  51. {
  52. initialDirectory = fileInfo;
  53. }
  54. }
  55. else
  56. {
  57. initialDirectory = fileInfo;
  58. }
  59. }
  60. else
  61. {
  62. initialDirectory = fileInfo;
  63. }
  64. ImagePaths = vm.PlatformService.GetFiles(initialDirectory);
  65. CurrentIndex = ImagePaths.FindIndex(x => x.FullName.Equals(fileInfo.FullName));
  66. InitiateFileSystemWatcher(fileInfo);
  67. }
  68. public ImageIterator(FileInfo fileInfo, List<FileInfo> imagePaths, int currentIndex, MainViewModel vm)
  69. {
  70. #if DEBUG
  71. ArgumentNullException.ThrowIfNull(fileInfo);
  72. #endif
  73. _vm = vm;
  74. ImagePaths = imagePaths;
  75. CurrentIndex = currentIndex;
  76. InitiateFileSystemWatcher(fileInfo);
  77. }
  78. #endregion
  79. #region File Watcher
  80. private void InitiateFileSystemWatcher(FileInfo fileInfo)
  81. {
  82. InitialFileInfo = fileInfo;
  83. if (!fileInfo.FullName.Contains(Settings.StartUp.StartUpDirectory))
  84. {
  85. Settings.StartUp.StartUpDirectory = fileInfo.DirectoryName;
  86. }
  87. if (_watcher is not null)
  88. {
  89. _watcher.Dispose();
  90. _watcher = null;
  91. }
  92. _watcher?.Dispose();
  93. if (!Settings.Navigation.IsFileWatcherEnabled)
  94. {
  95. return;
  96. }
  97. _watcher = new FileSystemWatcher(fileInfo.DirectoryName!)
  98. {
  99. EnableRaisingEvents = true,
  100. Filter = "*.*",
  101. IncludeSubdirectories = Settings.Sorting.IncludeSubDirectories,
  102. NotifyFilter = NotifyFilters.FileName | NotifyFilters.LastWrite
  103. };
  104. _watcher.Created += (_, e) =>
  105. {
  106. if (!e.FullPath.IsSupported() || !Settings.Navigation.IsFileWatcherEnabled)
  107. {
  108. return; // Early exit
  109. }
  110. if (_vm.MainWindow.IsEditableTitlebarOpen.CurrentValue)
  111. {
  112. // Don't react to changes when renaming
  113. return;
  114. }
  115. Task.Run(() => OnFileAdded(e)).ContinueWith(t =>
  116. {
  117. if (t.Exception == null)
  118. {
  119. return;
  120. }
  121. DebugHelper.LogDebug(nameof(ImageIterator), nameof(OnFileAdded), t.Exception);
  122. });
  123. };
  124. _watcher.Deleted += (_, e) =>
  125. {
  126. if (!e.FullPath.IsSupported() || !Settings.Navigation.IsFileWatcherEnabled)
  127. {
  128. return; // Early exit
  129. }
  130. if (_vm.MainWindow.IsEditableTitlebarOpen.Value)
  131. {
  132. // Don't react to changes when renaming
  133. return;
  134. }
  135. Task.Run(() => OnFileDeleted(e)).ContinueWith(t =>
  136. {
  137. if (t.Exception == null)
  138. {
  139. return;
  140. }
  141. DebugHelper.LogDebug(nameof(ImageIterator), nameof(OnFileDeleted), t.Exception);
  142. });
  143. };
  144. _watcher.Renamed += (_, e) =>
  145. {
  146. if (!e.FullPath.IsSupported() || !Settings.Navigation.IsFileWatcherEnabled)
  147. {
  148. return; // Early exit
  149. }
  150. if (_vm.MainWindow.IsEditableTitlebarOpen.CurrentValue)
  151. {
  152. // Don't react to changes when renaming
  153. return;
  154. }
  155. Task.Run(() => OnFileRenamed(e)).ContinueWith(t =>
  156. {
  157. if (t.Exception == null)
  158. {
  159. return;
  160. }
  161. DebugHelper.LogDebug(nameof(ImageIterator), nameof(OnFileRenamed), t.Exception);
  162. });
  163. };
  164. }
  165. private async Task OnFileAdded(FileSystemEventArgs e)
  166. {
  167. try
  168. {
  169. var fileInfo = new FileInfo(e.FullPath);
  170. if (fileInfo.Exists == false)
  171. {
  172. return;
  173. }
  174. var sourceFileInfo = Settings.Sorting.IncludeSubDirectories
  175. ? new FileInfo(_watcher.Path)
  176. : fileInfo;
  177. var newList = await Task.FromResult(_vm.PlatformService.GetFiles(sourceFileInfo));
  178. if (newList.Count == 0)
  179. {
  180. return;
  181. }
  182. ImagePaths = newList;
  183. _isRunning = true;
  184. TitleManager.SetTitle(_vm);
  185. var index = ImagePaths.FindIndex(x => x.FullName.Equals(e.FullPath));
  186. if (index < 0)
  187. {
  188. PreLoader.Resynchronize(ImagePaths);
  189. _isRunning = false;
  190. return;
  191. }
  192. var isGalleryItemAdded = await GalleryFunctions.AddGalleryItem(index, fileInfo, _vm);
  193. if (isGalleryItemAdded)
  194. {
  195. if (Settings.Gallery.IsBottomGalleryShown && ImagePaths.Count > 1)
  196. {
  197. if (_vm.Gallery.GalleryMode.CurrentValue is GalleryMode.BottomToClosed or GalleryMode.FullToClosed)
  198. {
  199. _vm.Gallery.GalleryMode.Value = GalleryMode.ClosedToBottom;
  200. }
  201. }
  202. GalleryNavigation.CenterScrollToSelectedItem(_vm);
  203. }
  204. PreLoader.Resynchronize(ImagePaths);
  205. }
  206. catch (Exception exception)
  207. {
  208. DebugHelper.LogDebug(nameof(ImageIterator), nameof(OnFileAdded), exception);
  209. }
  210. finally
  211. {
  212. _isRunning = false;
  213. }
  214. }
  215. private async Task OnFileDeleted(FileSystemEventArgs e)
  216. {
  217. try
  218. {
  219. _isRunning = true;
  220. var index = ImagePaths.FindIndex(x => x.FullName.Equals(e.FullPath));
  221. if (index < 0)
  222. {
  223. return;
  224. }
  225. var currentIndex = CurrentIndex;
  226. var isSameFile = currentIndex == index;
  227. ImagePaths.RemoveAt(index);
  228. if (isSameFile)
  229. {
  230. if (ImagePaths.Count <= 0)
  231. {
  232. ErrorHandling.ShowStartUpMenu(_vm);
  233. return;
  234. }
  235. RemoveCurrentItemFromPreLoader();
  236. PreLoader.Resynchronize(ImagePaths);
  237. var newIndex = GetIteration(index, NavigateTo.Previous);
  238. CurrentIndex = newIndex;
  239. _vm.PicViewer.FileInfo.Value = ImagePaths[CurrentIndex];
  240. await IterateToIndex(CurrentIndex, new CancellationTokenSource());
  241. }
  242. else
  243. {
  244. RemoveItemFromPreLoader(index);
  245. TitleManager.SetTitle(_vm);
  246. }
  247. var removed = GalleryFunctions.RemoveGalleryItem(index, _vm);
  248. if (removed)
  249. {
  250. if (Settings.Gallery.IsBottomGalleryShown)
  251. {
  252. if (ImagePaths.Count == 1)
  253. {
  254. _vm.Gallery.GalleryMode.Value = GalleryMode.BottomToClosed;
  255. }
  256. }
  257. var indexOf = ImagePaths.FindIndex(x => x.FullName.Equals(_vm.PicViewer.FileInfo.CurrentValue.FullName));
  258. _vm.PicViewer.Index.Value = indexOf; // Fixes deselection bug
  259. CurrentIndex = indexOf;
  260. if (isSameFile)
  261. {
  262. GalleryNavigation.CenterScrollToSelectedItem(_vm);
  263. }
  264. }
  265. if (!isSameFile)
  266. {
  267. PreLoader.Resynchronize(ImagePaths);
  268. }
  269. FileHistoryManager.Remove(e.FullPath);
  270. }
  271. catch (Exception exception)
  272. {
  273. DebugHelper.LogDebug(nameof(ImageIterator), nameof(OnFileDeleted), exception);
  274. }
  275. finally
  276. {
  277. _isRunning = false;
  278. }
  279. }
  280. private async Task OnFileRenamed(RenamedEventArgs e)
  281. {
  282. try
  283. {
  284. if (e.FullPath.IsSupported() == false)
  285. {
  286. return;
  287. }
  288. var oldIndex = ImagePaths.FindIndex(x => x.FullName.Equals(e.OldFullPath));
  289. if (oldIndex < 0)
  290. {
  291. return;
  292. }
  293. _isRunning = true;
  294. var sameFile = CurrentIndex == oldIndex;
  295. var newFileInfo = new FileInfo(e.FullPath);
  296. if (newFileInfo.Exists == false)
  297. {
  298. return;
  299. }
  300. var newList = _vm.PlatformService.GetFiles(newFileInfo);
  301. if (newList.Count == 0)
  302. {
  303. return;
  304. }
  305. if (newFileInfo.Exists == false)
  306. {
  307. return;
  308. }
  309. ImagePaths = newList;
  310. var newIndex = ImagePaths.FindIndex(x => x.FullName.Equals(e.FullPath));
  311. if (sameFile)
  312. {
  313. _vm.PicViewer.FileInfo.Value = newFileInfo;
  314. CurrentIndex = newIndex;
  315. }
  316. TitleManager.SetTitle(_vm);
  317. PreLoader.RefreshFileInfo(newIndex, newFileInfo, ImagePaths);
  318. Resynchronize();
  319. _isRunning = false;
  320. FileHistoryManager.Rename(e.OldFullPath, e.FullPath);
  321. await Dispatcher.UIThread.InvokeAsync(() =>
  322. GalleryFunctions.RenameGalleryItem(oldIndex, newIndex, Path.GetFileNameWithoutExtension(e.Name),
  323. e.FullPath,
  324. _vm));
  325. if (sameFile)
  326. {
  327. _vm.PicViewer.Index.Value = newIndex;
  328. GalleryFunctions.CenterGallery(_vm);
  329. }
  330. }
  331. catch (Exception exception)
  332. {
  333. #if DEBUG
  334. Console.WriteLine(
  335. $"{nameof(ImageIterator)}.{nameof(OnFileRenamed)} {exception.Message} \n{exception.StackTrace}");
  336. #endif
  337. }
  338. finally
  339. {
  340. _isRunning = false;
  341. }
  342. }
  343. #endregion
  344. #region Preloader
  345. public async Task ClearAsync() =>
  346. await PreLoader.ClearAsync().ConfigureAwait(false);
  347. public async Task PreloadAsync() =>
  348. await PreLoader.PreLoadAsync(CurrentIndex, IsReversed, ImagePaths).ConfigureAwait(false);
  349. public async Task AddAsync(int index) =>
  350. await PreLoader.AddAsync(index, ImagePaths).ConfigureAwait(false);
  351. public void Add(int index, ImageModel imageModel) =>
  352. PreLoader.Add(index, ImagePaths, imageModel);
  353. public bool Add(FileInfo file, ImageModel imageModel) =>
  354. PreLoader.Add(ImagePaths.FindIndex(x => x.FullName.Equals(file.FullName)), ImagePaths, imageModel);
  355. public PreLoadValue? GetPreLoadValue(int index)
  356. {
  357. if (index < 0 || index >= ImagePaths.Count)
  358. {
  359. return null;
  360. }
  361. return _isRunning
  362. ? PreLoader.Get(ImagePaths[index], ImagePaths)
  363. : PreLoader.Get(index, ImagePaths);
  364. }
  365. public PreLoadValue? GetPreLoadValue(FileInfo file) =>
  366. PreLoader.Get(file, ImagePaths);
  367. public async Task<PreLoadValue?> GetOrLoadPreLoadValueAsync(int index) =>
  368. await PreLoader.GetOrLoadAsync(index, ImagePaths);
  369. public async Task<PreLoadValue?> GetOrLoadPreLoadValueAsync(FileInfo file) =>
  370. await PreLoader.GetOrLoadAsync(file, ImagePaths);
  371. public PreLoadValue? GetCurrentPreLoadValue() =>
  372. _isRunning
  373. ? PreLoader.Get(_vm.PicViewer.FileInfo.CurrentValue, ImagePaths)
  374. : PreLoader.Get(CurrentIndex, ImagePaths);
  375. public async Task<PreLoadValue?> GetCurrentPreLoadValueAsync() =>
  376. _isRunning
  377. ? await PreLoader.GetOrLoadAsync(_vm.PicViewer.FileInfo.CurrentValue, ImagePaths)
  378. : await PreLoader.GetOrLoadAsync(CurrentIndex, ImagePaths);
  379. public PreLoadValue? GetNextPreLoadValue()
  380. {
  381. var nextIndex = GetIteration(CurrentIndex, IsReversed ? NavigateTo.Previous : NavigateTo.Next);
  382. return _isRunning ? PreLoader.Get(ImagePaths[nextIndex], ImagePaths) : PreLoader.Get(nextIndex, ImagePaths);
  383. }
  384. public async Task<PreLoadValue?>? GetNextPreLoadValueAsync()
  385. {
  386. var nextIndex = GetIteration(CurrentIndex, NavigateTo.Next);
  387. return _isRunning
  388. ? await PreLoader.GetOrLoadAsync(ImagePaths[nextIndex], ImagePaths)
  389. : await PreLoader.GetOrLoadAsync(nextIndex, ImagePaths);
  390. }
  391. public void RemoveItemFromPreLoader(int index) => PreLoader.Remove(index, ImagePaths);
  392. public void RemoveItemFromPreLoader(string fileName) => PreLoader.Remove(fileName, ImagePaths);
  393. public void RemoveCurrentItemFromPreLoader() => PreLoader.Remove(CurrentIndex, ImagePaths);
  394. public void Resynchronize() => PreLoader.Resynchronize(ImagePaths);
  395. #endregion
  396. #region Navigation
  397. public async Task ReloadFileListAsync()
  398. {
  399. try
  400. {
  401. _isRunning = true;
  402. var fileList = await Task.FromResult(_vm.PlatformService.GetFiles(_vm.PicViewer.FileInfo.CurrentValue))
  403. .ConfigureAwait(false);
  404. var oldList = ImagePaths;
  405. ImagePaths = fileList;
  406. CurrentIndex = ImagePaths.FindIndex(x => x.FullName.Equals(_vm.PicViewer.FileInfo.CurrentValue.FullName));
  407. TitleManager.SetTitle(_vm);
  408. await ClearAsync().ConfigureAwait(false);
  409. await PreloadAsync().ConfigureAwait(false);
  410. Resynchronize();
  411. _isRunning = false;
  412. if (fileList.Count > oldList.Count)
  413. {
  414. for (var i = 0; i < oldList.Count; i++)
  415. {
  416. if (i < fileList.Count && !oldList[i].FullName.Equals(fileList[i].FullName))
  417. {
  418. await GalleryFunctions.AddGalleryItem(fileList.FindIndex(x => x.FullName.Equals(fileList[i].FullName)), fileList[i],
  419. _vm, DispatcherPriority.Background);
  420. }
  421. }
  422. }
  423. else if (fileList.Count < oldList.Count)
  424. {
  425. for (var i = 0; i < fileList.Count; i++)
  426. {
  427. if (i < oldList.Count && fileList[i].FullName.Equals(oldList[i].FullName))
  428. {
  429. GalleryFunctions.RemoveGalleryItem(i, _vm);
  430. }
  431. }
  432. }
  433. }
  434. finally
  435. {
  436. _isRunning = false;
  437. }
  438. }
  439. public async Task QuickReload()
  440. {
  441. RemoveCurrentItemFromPreLoader();
  442. await IterateToIndex(CurrentIndex, new CancellationTokenSource()).ConfigureAwait(false);
  443. }
  444. public int GetIteration(int index, NavigateTo navigateTo, bool skip1 = false, bool skip10 = false,
  445. bool skip100 = false)
  446. {
  447. int next;
  448. if (skip100)
  449. {
  450. if (ImagePaths.Count > PreLoaderConfig.MaxCount)
  451. {
  452. PreLoader.Clear();
  453. }
  454. }
  455. // Determine skipAmount based on input flags
  456. var skipAmount = skip100 ? 100 : skip10 ? 10 : skip1 ? 2 : 1;
  457. switch (navigateTo)
  458. {
  459. case NavigateTo.Next:
  460. case NavigateTo.Previous:
  461. var indexChange = navigateTo == NavigateTo.Next ? skipAmount : -skipAmount;
  462. IsReversed = navigateTo == NavigateTo.Previous;
  463. if (Settings.UIProperties.Looping)
  464. {
  465. // Calculate new index with looping
  466. next = (index + indexChange + ImagePaths.Count) % ImagePaths.Count;
  467. }
  468. else
  469. {
  470. // Calculate new index without looping and ensure bounds
  471. var newIndex = index + indexChange;
  472. if (newIndex < 0)
  473. {
  474. return 0;
  475. }
  476. if (newIndex >= ImagePaths.Count)
  477. {
  478. return ImagePaths.Count - 1;
  479. }
  480. next = newIndex;
  481. }
  482. break;
  483. case NavigateTo.First:
  484. case NavigateTo.Last:
  485. if (ImagePaths.Count > PreLoaderConfig.MaxCount)
  486. {
  487. PreLoader.Clear();
  488. }
  489. next = navigateTo == NavigateTo.First ? 0 : ImagePaths.Count - 1;
  490. break;
  491. default:
  492. #if DEBUG
  493. Console.WriteLine($"{nameof(ImageIterator)}: {navigateTo} is not a valid NavigateTo value.");
  494. #endif
  495. return -1;
  496. }
  497. return next;
  498. }
  499. public async Task NextIteration(NavigateTo navigateTo, CancellationTokenSource cts)
  500. {
  501. var index = GetIteration(CurrentIndex, navigateTo,
  502. Settings.ImageScaling.ShowImageSideBySide);
  503. if (index < 0)
  504. {
  505. return;
  506. }
  507. await NextIteration(index, cts).ConfigureAwait(false);
  508. }
  509. public async Task NextIteration(int iteration, CancellationTokenSource cts)
  510. {
  511. // Handle side-by-side navigation
  512. if (Settings.ImageScaling.ShowImageSideBySide)
  513. {
  514. // Handle properly navigating first or last image
  515. if (iteration == GetCount - 1)
  516. {
  517. if (!Settings.UIProperties.Looping)
  518. {
  519. return;
  520. }
  521. var targetIndex = IsReversed ? GetCount - 2 < 0 ? 0 : GetCount - 2 : 0;
  522. await IterateToIndex(targetIndex, cts).ConfigureAwait(false);
  523. return;
  524. }
  525. // Determine the next index based on navigation direction
  526. var nextIndex = GetIteration(iteration, IsReversed ? NavigateTo.Previous : NavigateTo.Next);
  527. await IterateToIndex(nextIndex, cts).ConfigureAwait(false);
  528. return;
  529. }
  530. // When not showing side-by-side, decide based on keyboard state
  531. if (!MainKeyboardShortcuts.IsKeyHeldDown)
  532. {
  533. await IterateToIndex(iteration, cts).ConfigureAwait(false);
  534. }
  535. else
  536. {
  537. await TimerIteration(iteration, cts).ConfigureAwait(false);
  538. }
  539. }
  540. /// <summary>
  541. /// Iterates to the given index in the image list, shows the corresponding image and preloads the next/previous images.
  542. /// </summary>
  543. /// <param name="index">The index to iterate to.</param>
  544. /// <param name="cts">The cancellation token source.</param>
  545. public async Task IterateToIndex(int index, CancellationTokenSource cts)
  546. {
  547. if (index < 0 || index >= ImagePaths.Count)
  548. {
  549. // Invalid index. Probably a race condition? Do nothing and report
  550. #if DEBUG
  551. Trace.WriteLine($"Invalid index {index} in {nameof(ImageIterator)}:{nameof(IterateToIndex)}");
  552. #endif
  553. return;
  554. }
  555. try
  556. {
  557. CurrentIndex = index;
  558. // Get cached preload value first, if available
  559. // ReSharper disable once MethodHasAsyncOverload
  560. var preloadValue = GetPreLoadValue(index);
  561. if (preloadValue is not null)
  562. {
  563. // Wait for image to load if it's still loading
  564. if (preloadValue is { IsLoading: true, ImageModel.Image: null })
  565. {
  566. LoadingPreview();
  567. using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cts.Token);
  568. linkedCts.CancelAfter(TimeSpan.FromMinutes(1));
  569. try
  570. {
  571. // Wait for the loading to complete or timeout
  572. await preloadValue.WaitForLoadingCompleteAsync().WaitAsync(linkedCts.Token);
  573. }
  574. catch (OperationCanceledException) when (!cts.IsCancellationRequested)
  575. {
  576. // This is a timeout, not cancellation from navigation
  577. preloadValue =
  578. new PreLoadValue(
  579. await GetImageModel.GetImageModelAsync(ImagePaths[CurrentIndex]))
  580. {
  581. IsLoading = false
  582. };
  583. }
  584. // Check if user navigated away during loading
  585. if (CurrentIndex != index)
  586. {
  587. await cts.CancelAsync();
  588. return;
  589. }
  590. }
  591. }
  592. else
  593. {
  594. var imageModel = await GetImageModel.GetImageModelAsync(ImagePaths[index])
  595. .ConfigureAwait(false);
  596. preloadValue = new PreLoadValue(imageModel);
  597. }
  598. if (CurrentIndex != index)
  599. {
  600. // Skip loading if user went to next value
  601. await cts.CancelAsync();
  602. return;
  603. }
  604. if (Settings.ImageScaling.ShowImageSideBySide)
  605. {
  606. var nextIndex = GetIteration(index, IsReversed ? NavigateTo.Previous : NavigateTo.Next);
  607. var nextPreloadValue = await GetOrLoadPreLoadValueAsync(nextIndex).ConfigureAwait(false);
  608. if (CurrentIndex != index)
  609. {
  610. // Skip loading if user went to next value
  611. await cts.CancelAsync();
  612. return;
  613. }
  614. if (!cts.IsCancellationRequested && index == CurrentIndex)
  615. {
  616. await UpdateImage.UpdateSource(_vm, index, ImagePaths, preloadValue,
  617. nextPreloadValue)
  618. .ConfigureAwait(false);
  619. }
  620. }
  621. else
  622. {
  623. if (!cts.IsCancellationRequested && index == CurrentIndex)
  624. {
  625. await UpdateImage.UpdateSource(_vm, index, ImagePaths, preloadValue)
  626. .ConfigureAwait(false);
  627. }
  628. }
  629. if (ImagePaths.Count > 1)
  630. {
  631. if (Settings.UIProperties.IsTaskbarProgressEnabled)
  632. {
  633. Dispatcher.UIThread.Invoke(
  634. () => { _vm.PlatformService.SetTaskbarProgress((ulong)CurrentIndex, (ulong)ImagePaths.Count); },
  635. DispatcherPriority.Render);
  636. }
  637. // We shouldn't wait for preloading to finish, since this should complete as soon as image changed.
  638. // Awaiting preloader will cause delay, in E.G., moving the cursor after the image has changed.
  639. _ = Task.Run(() => PreLoader.PreLoadAsync(index, IsReversed, ImagePaths)
  640. .ConfigureAwait(false));
  641. }
  642. PreLoader.Add(index, ImagePaths, preloadValue?.ImageModel);
  643. // Add recent files
  644. if (string.IsNullOrWhiteSpace(TempFileHelper.TempFilePath) && ImagePaths.Count > CurrentIndex)
  645. {
  646. FileHistoryManager.Add(ImagePaths[CurrentIndex].FullName);
  647. if (Settings.ImageScaling.ShowImageSideBySide)
  648. {
  649. FileHistoryManager.Add(
  650. ImagePaths[GetIteration(CurrentIndex, IsReversed ? NavigateTo.Previous : NavigateTo.Next)].FullName);
  651. }
  652. }
  653. }
  654. catch (OperationCanceledException)
  655. {
  656. #if DEBUG
  657. Trace.WriteLine($"\n{nameof(IterateToIndex)} canceled\n");
  658. #endif
  659. }
  660. catch (Exception e)
  661. {
  662. DebugHelper.LogDebug(nameof(ImageIterator), nameof(IterateToIndex), e);
  663. #if DEBUG
  664. await TooltipHelper.ShowTooltipMessageAsync(e.Message);
  665. #endif
  666. }
  667. finally
  668. {
  669. if (index == CurrentIndex)
  670. {
  671. _vm.MainWindow.IsLoadingIndicatorShown.Value = false;
  672. }
  673. }
  674. return;
  675. void LoadingPreview()
  676. {
  677. TitleManager.SetLoadingTitle(_vm);
  678. _vm.PicViewer.Index.Value = index;
  679. if (Settings.Gallery.IsBottomGalleryShown)
  680. {
  681. GalleryNavigation.CenterScrollToSelectedItem(_vm);
  682. }
  683. var thumb = GetThumbnails.GetExifThumb(NavigationManager.GetFileNameAt(index));
  684. if (index != CurrentIndex)
  685. {
  686. return;
  687. }
  688. if (!Settings.ImageScaling.ShowImageSideBySide)
  689. {
  690. if (thumb is not null)
  691. {
  692. _vm.PicViewer.ImageSource.Value = thumb;
  693. }
  694. }
  695. else
  696. {
  697. var secondaryThumb = GetThumbnails.GetExifThumb(NavigationManager.GetNextFileName);
  698. if (index != CurrentIndex)
  699. {
  700. return;
  701. }
  702. _vm.PicViewer.ImageSource.Value = thumb;
  703. _vm.PicViewer.SecondaryImageSource.Value = secondaryThumb;
  704. _vm.MainWindow.IsLoadingIndicatorShown.Value = thumb is null || secondaryThumb is null;
  705. }
  706. }
  707. }
  708. private static Timer? _timer;
  709. private async Task TimerIteration(int index, CancellationTokenSource cts)
  710. {
  711. if (_timer is null)
  712. {
  713. _timer = new Timer
  714. {
  715. AutoReset = false,
  716. Enabled = true
  717. };
  718. }
  719. else if (_timer.Enabled)
  720. {
  721. if (!MainKeyboardShortcuts.IsKeyHeldDown)
  722. {
  723. _timer = null;
  724. }
  725. return;
  726. }
  727. _timer.Interval = TimeSpan.FromSeconds(Settings.UIProperties.NavSpeed).TotalMilliseconds;
  728. _timer.Start();
  729. await IterateToIndex(index, cts).ConfigureAwait(false);
  730. }
  731. public void UpdateFileListAndIndex(List<FileInfo> fileList, int index)
  732. {
  733. ImagePaths = fileList;
  734. CurrentIndex = index;
  735. }
  736. #endregion
  737. #region IDisposable
  738. public async ValueTask DisposeAsync()
  739. {
  740. await ClearAsync().ConfigureAwait(false);
  741. Dispose(true, true);
  742. }
  743. private void Dispose(bool disposing, bool cleared = false)
  744. {
  745. if (_disposed)
  746. {
  747. return;
  748. }
  749. if (disposing)
  750. {
  751. _watcher?.Dispose();
  752. if (!cleared)
  753. {
  754. PreLoader.Clear();
  755. }
  756. _timer?.Dispose();
  757. PreLoader.Dispose();
  758. }
  759. _disposed = true;
  760. GC.SuppressFinalize(this);
  761. }
  762. #endregion
  763. }