1
1

ImageIterator.cs 27 KB

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