ImageIterator.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710
  1. using System.Diagnostics;
  2. using Avalonia.Threading;
  3. using PicView.Avalonia.Gallery;
  4. using PicView.Avalonia.ImageHandling;
  5. using PicView.Avalonia.Keybindings;
  6. using PicView.Avalonia.UI;
  7. using PicView.Avalonia.ViewModels;
  8. using PicView.Core.Config;
  9. using PicView.Core.FileHandling;
  10. using PicView.Core.Gallery;
  11. using PicView.Core.Navigation;
  12. using Timer = System.Timers.Timer;
  13. namespace PicView.Avalonia.Navigation;
  14. public sealed class ImageIterator : IDisposable
  15. {
  16. #region Properties
  17. private bool _disposed;
  18. public List<string> ImagePaths { get; private set; }
  19. public bool IsRenamingInProgress { get; set; }
  20. public int CurrentIndex { get; private set; }
  21. public FileInfo InitialFileInfo { get; private set; } = null!;
  22. public bool IsReversed { get; private set; }
  23. private PreLoader PreLoader { get; } = new();
  24. private static FileSystemWatcher? _watcher;
  25. private static bool _isRunning;
  26. private readonly MainViewModel? _vm;
  27. private readonly Lock _lock = new();
  28. #endregion
  29. #region Constructors
  30. public ImageIterator(FileInfo fileInfo, MainViewModel vm)
  31. {
  32. ArgumentNullException.ThrowIfNull(fileInfo);
  33. _vm = vm;
  34. ImagePaths = vm.PlatformService.GetFiles(fileInfo);
  35. CurrentIndex = Directory.Exists(fileInfo.FullName) ? 0 : ImagePaths.IndexOf(fileInfo.FullName);
  36. InitiateFileSystemWatcher(fileInfo);
  37. }
  38. public ImageIterator(FileInfo fileInfo, List<string> imagePaths, int currentIndex, MainViewModel vm)
  39. {
  40. ArgumentNullException.ThrowIfNull(fileInfo);
  41. _vm = vm;
  42. ImagePaths = imagePaths;
  43. CurrentIndex = currentIndex;
  44. InitiateFileSystemWatcher(fileInfo);
  45. }
  46. #endregion
  47. #region File Watcher
  48. private void InitiateFileSystemWatcher(FileInfo fileInfo)
  49. {
  50. InitialFileInfo = fileInfo;
  51. if (_watcher is not null)
  52. {
  53. _watcher.Dispose();
  54. _watcher = null;
  55. }
  56. _watcher = new FileSystemWatcher();
  57. #if DEBUG
  58. Debug.Assert(fileInfo.DirectoryName != null);
  59. #endif
  60. _watcher.Path = fileInfo.DirectoryName;
  61. _watcher.EnableRaisingEvents = true;
  62. _watcher.Filter = "*.*";
  63. _watcher.IncludeSubdirectories = SettingsHelper.Settings.Sorting.IncludeSubDirectories;
  64. _watcher.Created += async (_, e) => await OnFileAdded(e);
  65. _watcher.Deleted += async (_, e) => await OnFileDeleted(e);
  66. _watcher.Renamed += async (_, e) => await OnFileRenamed(e);
  67. }
  68. private async Task OnFileAdded(FileSystemEventArgs e)
  69. {
  70. if (IsRenamingInProgress)
  71. {
  72. return;
  73. }
  74. if (ImagePaths.Contains(e.FullPath))
  75. {
  76. return;
  77. }
  78. if (e.FullPath.IsSupported() == false)
  79. {
  80. return;
  81. }
  82. var fileInfo = new FileInfo(e.FullPath);
  83. if (fileInfo.Exists == false)
  84. {
  85. return;
  86. }
  87. var retries = 0;
  88. while (_isRunning && retries < 10)
  89. {
  90. await Task.Delay(200);
  91. retries++;
  92. }
  93. _isRunning = true;
  94. var newList = await Task.FromResult(_vm.PlatformService.GetFiles(fileInfo));
  95. if (newList.Count == 0)
  96. {
  97. return;
  98. }
  99. if (newList.Count == ImagePaths.Count)
  100. {
  101. return;
  102. }
  103. if (fileInfo.Exists == false)
  104. {
  105. return;
  106. }
  107. ImagePaths = newList;
  108. _isRunning = false;
  109. var index = ImagePaths.IndexOf(e.FullPath);
  110. if (index < 0)
  111. {
  112. return;
  113. }
  114. var nextIndex = index + 1;
  115. if (index >= ImagePaths.Count)
  116. {
  117. nextIndex = 0;
  118. }
  119. var prevIndex = index - 1;
  120. if (prevIndex < 0)
  121. {
  122. prevIndex = ImagePaths.Count - 1;
  123. }
  124. var cleared = false;
  125. if (PreLoader.Contains(index, ImagePaths) || PreLoader.Contains(nextIndex, ImagePaths) ||
  126. PreLoader.Contains(prevIndex, ImagePaths))
  127. {
  128. PreLoader.Clear();
  129. cleared = true;
  130. }
  131. SetTitleHelper.SetTitle(_vm);
  132. var isGalleryItemAdded = await GalleryFunctions.AddGalleryItem(index, fileInfo, _vm);
  133. if (isGalleryItemAdded)
  134. {
  135. if (SettingsHelper.Settings.Gallery.IsBottomGalleryShown && ImagePaths.Count > 1)
  136. {
  137. if (_vm.GalleryMode is GalleryMode.BottomToClosed or GalleryMode.FullToClosed)
  138. {
  139. _vm.GalleryMode = GalleryMode.ClosedToBottom;
  140. }
  141. }
  142. var indexOf = ImagePaths.IndexOf(_vm.FileInfo.FullName);
  143. _vm.SelectedGalleryItemIndex = indexOf; // Fixes deselection bug
  144. CurrentIndex = indexOf;
  145. GalleryNavigation.CenterScrollToSelectedItem(_vm);
  146. }
  147. if (cleared)
  148. {
  149. await Preload();
  150. }
  151. }
  152. private async Task OnFileDeleted(FileSystemEventArgs e)
  153. {
  154. if (IsRenamingInProgress)
  155. {
  156. return;
  157. }
  158. if (e.FullPath.IsSupported() == false)
  159. {
  160. return;
  161. }
  162. if (ImagePaths.Contains(e.FullPath) == false)
  163. {
  164. return;
  165. }
  166. if (_isRunning)
  167. {
  168. return;
  169. }
  170. _isRunning = true;
  171. var index = ImagePaths.IndexOf(e.FullPath);
  172. if (index < 0)
  173. {
  174. return;
  175. }
  176. var nextIndex = index + 1;
  177. if (index >= ImagePaths.Count)
  178. {
  179. nextIndex = 0;
  180. }
  181. var prevIndex = index - 1;
  182. if (prevIndex < 0)
  183. {
  184. prevIndex = ImagePaths.Count - 1;
  185. }
  186. var cleared = false;
  187. if (PreLoader.Contains(index, ImagePaths) || PreLoader.Contains(nextIndex, ImagePaths) ||
  188. PreLoader.Contains(prevIndex, ImagePaths))
  189. {
  190. PreLoader.Clear();
  191. cleared = true;
  192. }
  193. else
  194. {
  195. PreLoader.Remove(index, ImagePaths);
  196. }
  197. var sameFile = CurrentIndex == index;
  198. if (!ImagePaths.Remove(e.FullPath))
  199. {
  200. return;
  201. }
  202. if (sameFile)
  203. {
  204. if (ImagePaths.Count <= 0)
  205. {
  206. ErrorHandling.ShowStartUpMenu(_vm);
  207. return;
  208. }
  209. await NavigationHelper.Iterate(false, _vm);
  210. }
  211. else
  212. {
  213. SetTitleHelper.SetTitle(_vm);
  214. }
  215. var removed = GalleryFunctions.RemoveGalleryItem(index, _vm);
  216. if (removed)
  217. {
  218. if (SettingsHelper.Settings.Gallery.IsBottomGalleryShown)
  219. {
  220. if (ImagePaths.Count == 1)
  221. {
  222. _vm.GalleryMode = GalleryMode.BottomToClosed;
  223. }
  224. }
  225. var indexOf = ImagePaths.IndexOf(_vm.FileInfo.FullName);
  226. _vm.SelectedGalleryItemIndex = indexOf; // Fixes deselection bug
  227. CurrentIndex = indexOf;
  228. GalleryNavigation.CenterScrollToSelectedItem(_vm);
  229. }
  230. FileHistoryNavigation.Remove(e.FullPath);
  231. _isRunning = false;
  232. SetTitleHelper.SetTitle(_vm);
  233. if (cleared)
  234. {
  235. await Preload();
  236. }
  237. }
  238. private async Task OnFileRenamed(RenamedEventArgs e)
  239. {
  240. if (IsRenamingInProgress)
  241. {
  242. return;
  243. }
  244. if (e.FullPath.IsSupported() == false)
  245. {
  246. if (ImagePaths.Contains(e.OldFullPath))
  247. {
  248. ImagePaths.Remove(e.OldFullPath);
  249. }
  250. return;
  251. }
  252. if (_isRunning)
  253. {
  254. return;
  255. }
  256. _isRunning = true;
  257. var oldIndex = ImagePaths.IndexOf(e.OldFullPath);
  258. var fileInfo = new FileInfo(e.FullPath);
  259. if (fileInfo.Exists == false)
  260. {
  261. return;
  262. }
  263. var newList = FileListHelper.RetrieveFiles(fileInfo).ToList();
  264. if (newList.Count == 0)
  265. {
  266. return;
  267. }
  268. if (fileInfo.Exists == false)
  269. {
  270. return;
  271. }
  272. ImagePaths = newList;
  273. var index = ImagePaths.IndexOf(e.FullPath);
  274. if (index < 0)
  275. {
  276. return;
  277. }
  278. if (fileInfo.Exists == false)
  279. {
  280. return;
  281. }
  282. SetTitleHelper.SetTitle(_vm);
  283. await PreLoader.RefreshFileInfo(oldIndex, ImagePaths);
  284. _isRunning = false;
  285. FileHistoryNavigation.Rename(e.OldFullPath, e.FullPath);
  286. GalleryFunctions.RemoveGalleryItem(oldIndex, _vm);
  287. await GalleryFunctions.AddGalleryItem(index, fileInfo, _vm);
  288. await GalleryFunctions.SortGalleryItems(ImagePaths, _vm);
  289. }
  290. #endregion
  291. #region Preloader
  292. public void Clear()
  293. {
  294. PreLoader.Clear();
  295. }
  296. public async Task Preload()
  297. {
  298. await PreLoader.PreLoadAsync(CurrentIndex, ImagePaths.Count, IsReversed, ImagePaths).ConfigureAwait(false);
  299. }
  300. public async Task AddAsync(int index, ImageModel imageModel)
  301. {
  302. await PreLoader.AddAsync(index, ImagePaths, imageModel).ConfigureAwait(false);
  303. }
  304. public PreLoader.PreLoadValue? GetPreLoadValue(int index)
  305. {
  306. return PreLoader.Get(index, ImagePaths);
  307. }
  308. public async Task<PreLoader.PreLoadValue?> GetPreLoadValueAsync(int index)
  309. {
  310. return await PreLoader.GetAsync(index, ImagePaths);
  311. }
  312. public PreLoader.PreLoadValue? GetCurrentPreLoadValue()
  313. {
  314. return PreLoader.Get(CurrentIndex, ImagePaths);
  315. }
  316. public async Task<PreLoader.PreLoadValue?> GetCurrentPreLoadValueAsync()
  317. {
  318. return await PreLoader.GetAsync(CurrentIndex, ImagePaths);
  319. }
  320. public PreLoader.PreLoadValue? GetNextPreLoadValue()
  321. {
  322. var nextIndex = GetIteration(CurrentIndex, NavigateTo.Next);
  323. return PreLoader.Get(nextIndex, ImagePaths);
  324. }
  325. public async Task<PreLoader.PreLoadValue?>? GetNextPreLoadValueAsync()
  326. {
  327. var nextIndex = GetIteration(CurrentIndex, NavigateTo.Next);
  328. return await PreLoader.GetAsync(nextIndex, ImagePaths);
  329. }
  330. public void RemoveItemFromPreLoader(int index)
  331. {
  332. PreLoader.Remove(index, ImagePaths);
  333. }
  334. public void RemoveCurrentItemFromPreLoader()
  335. {
  336. PreLoader.Remove(CurrentIndex, ImagePaths);
  337. }
  338. #endregion
  339. #region Navigation
  340. public async Task ReloadFileList()
  341. {
  342. ImagePaths = await Task.FromResult(_vm.PlatformService.GetFiles(InitialFileInfo)).ConfigureAwait(false);
  343. CurrentIndex = ImagePaths.IndexOf(_vm.FileInfo.FullName);
  344. InitiateFileSystemWatcher(InitialFileInfo);
  345. }
  346. public int GetIteration(int index, NavigateTo navigateTo, bool skip1 = false)
  347. {
  348. int next;
  349. var skipAmount = skip1 ? 2 : 1;
  350. switch (navigateTo)
  351. {
  352. case NavigateTo.Next:
  353. case NavigateTo.Previous:
  354. var indexChange = navigateTo == NavigateTo.Next ? skipAmount : -skipAmount;
  355. IsReversed = navigateTo == NavigateTo.Previous;
  356. if (SettingsHelper.Settings.UIProperties.Looping)
  357. {
  358. next = (index + indexChange + ImagePaths.Count) % ImagePaths.Count;
  359. }
  360. else
  361. {
  362. var newIndex = index + indexChange;
  363. // Ensure the new index doesn't go out of bounds
  364. if (newIndex < 0)
  365. {
  366. return 0;
  367. }
  368. if (newIndex >= ImagePaths.Count)
  369. {
  370. return ImagePaths.Count - 1;
  371. }
  372. next = newIndex;
  373. }
  374. break;
  375. case NavigateTo.First:
  376. case NavigateTo.Last:
  377. if (ImagePaths.Count > PreLoader.MaxCount)
  378. {
  379. PreLoader.Clear();
  380. }
  381. next = navigateTo == NavigateTo.First ? 0 : ImagePaths.Count - 1;
  382. break;
  383. default:
  384. return -1;
  385. }
  386. return next;
  387. }
  388. public async Task NextIteration(NavigateTo navigateTo)
  389. {
  390. var index = GetIteration(CurrentIndex, navigateTo, SettingsHelper.Settings.ImageScaling.ShowImageSideBySide);
  391. if (index < 0)
  392. {
  393. return;
  394. }
  395. if (!MainKeyboardShortcuts.IsKeyHeldDown)
  396. {
  397. await IterateToIndex(index);
  398. }
  399. else
  400. {
  401. await TimerIteration(index);
  402. }
  403. }
  404. public async Task IterateToIndex(int index)
  405. {
  406. if (index < 0 || index >= ImagePaths.Count)
  407. {
  408. ErrorHandling.ShowStartUpMenu(_vm);
  409. return;
  410. }
  411. await Task.Run(async () =>
  412. {
  413. try
  414. {
  415. lock (_lock)
  416. {
  417. CurrentIndex = index;
  418. }
  419. // ReSharper disable once MethodHasAsyncOverload
  420. var preloadValue = PreLoader.Get(index, ImagePaths);
  421. if (preloadValue is not null)
  422. {
  423. if (preloadValue.IsLoading)
  424. {
  425. TryShowPreview(preloadValue);
  426. }
  427. while (preloadValue.IsLoading)
  428. {
  429. await Task.Delay(20);
  430. lock (_lock)
  431. {
  432. if (CurrentIndex != index)
  433. {
  434. // Skip loading if user went to next value
  435. return;
  436. }
  437. }
  438. }
  439. }
  440. else
  441. {
  442. TryShowPreview(preloadValue);
  443. preloadValue = await PreLoader.GetAsync(CurrentIndex, ImagePaths).ConfigureAwait(false);
  444. }
  445. lock (_lock)
  446. {
  447. if (CurrentIndex != index)
  448. {
  449. // Skip loading if user went to next value
  450. return;
  451. }
  452. }
  453. if (SettingsHelper.Settings.ImageScaling.ShowImageSideBySide)
  454. {
  455. var nextPreloadValue = await GetNextPreLoadValueAsync().ConfigureAwait(false);
  456. lock (_lock)
  457. {
  458. if (CurrentIndex != index)
  459. {
  460. // Skip loading if user went to next value
  461. return;
  462. }
  463. }
  464. _vm.SecondaryImageSource = nextPreloadValue.ImageModel.Image;
  465. await UpdateImage.UpdateSource(_vm, index, ImagePaths, IsReversed, preloadValue, nextPreloadValue).ConfigureAwait(false);
  466. }
  467. else
  468. {
  469. await UpdateImage.UpdateSource(_vm, index, ImagePaths, IsReversed, preloadValue).ConfigureAwait(false);
  470. }
  471. if (ImagePaths.Count > 1)
  472. {
  473. if (SettingsHelper.Settings.UIProperties.IsTaskbarProgressEnabled)
  474. {
  475. await Dispatcher.UIThread.InvokeAsync(() =>
  476. {
  477. _vm.PlatformService.SetTaskbarProgress((ulong)CurrentIndex, (ulong)ImagePaths.Count);
  478. });
  479. }
  480. await PreLoader.PreLoadAsync(CurrentIndex, ImagePaths.Count, IsReversed, ImagePaths)
  481. .ConfigureAwait(false);
  482. }
  483. await AddAsync(index, preloadValue.ImageModel).ConfigureAwait(false);
  484. // Add recent files, except when browsing archive
  485. if (string.IsNullOrWhiteSpace(TempFileHelper.TempFilePath) && ImagePaths.Count > index)
  486. {
  487. FileHistoryNavigation.Add(ImagePaths[index]);
  488. }
  489. }
  490. catch (Exception e)
  491. {
  492. #if DEBUG
  493. Console.WriteLine($"{nameof(IterateToIndex)} exception: \n{e.Message}");
  494. await TooltipHelper.ShowTooltipMessageAsync(e.Message);
  495. #endif
  496. }
  497. finally
  498. {
  499. _vm.IsLoading = false;
  500. }
  501. return;
  502. void TryShowPreview(PreLoader.PreLoadValue preloadValue)
  503. {
  504. if (preloadValue is null)
  505. {
  506. return;
  507. }
  508. if (!preloadValue.IsLoading)
  509. {
  510. return;
  511. }
  512. if (index != CurrentIndex)
  513. {
  514. return;
  515. }
  516. if (SettingsHelper.Settings.ImageScaling.ShowImageSideBySide)
  517. {
  518. SetTitleHelper.SetLoadingTitle(_vm);
  519. _vm.IsLoading = true;
  520. _vm.ImageSource = null;
  521. _vm.SecondaryImageSource = null;
  522. }
  523. else
  524. {
  525. UpdateImage.LoadingPreview(_vm, index);
  526. }
  527. }
  528. });
  529. }
  530. private static Timer? _timer;
  531. internal async Task TimerIteration(int index)
  532. {
  533. if (_timer is null)
  534. {
  535. _timer = new Timer
  536. {
  537. AutoReset = false,
  538. Enabled = true
  539. };
  540. }
  541. else if (_timer.Enabled)
  542. {
  543. if (!MainKeyboardShortcuts.IsKeyHeldDown)
  544. {
  545. _timer = null;
  546. }
  547. return;
  548. }
  549. _timer.Interval = TimeSpan.FromSeconds(SettingsHelper.Settings.UIProperties.NavSpeed).TotalMilliseconds;
  550. _timer.Start();
  551. await IterateToIndex(index);
  552. }
  553. public void UpdateFileListAndIndex(List<string> fileList, int index)
  554. {
  555. ImagePaths = fileList;
  556. CurrentIndex = index;
  557. }
  558. #endregion
  559. #region IDisposable
  560. public void Dispose()
  561. {
  562. Dispose(true);
  563. GC.SuppressFinalize(this);
  564. }
  565. private void Dispose(bool disposing)
  566. {
  567. if (_disposed)
  568. {
  569. return;
  570. }
  571. if (disposing)
  572. {
  573. _watcher?.Dispose();
  574. Clear();
  575. _timer?.Dispose();
  576. }
  577. _disposed = true;
  578. }
  579. ~ImageIterator()
  580. {
  581. Dispose(false);
  582. }
  583. #endregion
  584. }