Browse Source

Fix Navigating within directories #199

Refactor file handling and sorting: replace `List<string>` with `List<FileInfo>` for better type specificity, rename `FileSortHelper` to `FileSortOrder`, incorporate `ZLinq` for directory traversal, and update affected methods across classes.
Ruben 4 months ago
parent
commit
6756e3b7dd

+ 1 - 1
src/PicView.Avalonia.MacOS/App.axaml.cs

@@ -121,7 +121,7 @@ public class App : Application, IPlatformSpecificService, IPlatformWindowService
         // TODO: Implement SetCursorPos
     }
 
-    public List<string> GetFiles(FileInfo fileInfo)
+    public List<FileInfo> GetFiles(FileInfo fileInfo)
     {
         var files = FileListRetriever.RetrieveFiles(fileInfo);
         return FileListManager.SortIEnumerable(files, this);

+ 1 - 1
src/PicView.Avalonia.Win32/App.axaml.cs

@@ -141,7 +141,7 @@ public class App : Application, IPlatformSpecificService, IPlatformWindowService
         NativeMethods.SetCursorPos(x, y);
     }
 
-    public List<string> GetFiles(FileInfo fileInfo)
+    public List<FileInfo> GetFiles(FileInfo fileInfo)
     {
         var files = FileListRetriever.RetrieveFiles(fileInfo);
         return FileListManager.SortIEnumerable(files, this);

+ 1 - 1
src/PicView.Avalonia/Converters/SortFilesByToBoolConverter.cs

@@ -9,7 +9,7 @@ public class SortFilesByToBoolConverter : IValueConverter
 {
     public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
     {
-        var sortOrder = FileSortHelper.GetSortOrder();
+        var sortOrder = FileSortOrder.GetSortOrder();
         if (Enum.TryParse<SortFilesBy>(parameter as string, true, out var result))
         {
             return sortOrder == result;

+ 12 - 13
src/PicView.Avalonia/DragAndDrop/DragAndDropHelper.cs

@@ -195,7 +195,7 @@ public static class DragAndDropHelper
         }
         else if (path.IsSupported())
         {
-            await ShowFilePreview(path);
+            await ShowFilePreview(new FileInfo(path));
         }
     }
 
@@ -221,24 +221,24 @@ public static class DragAndDropHelper
         });
     }
 
-    private static async Task ShowFilePreview(string path)
+    private static async Task ShowFilePreview(FileInfo fileInfo)
     {
-        var ext = Path.GetExtension(path);
+        var ext = fileInfo.Extension;
         if (ext.Equals(".svg", StringComparison.InvariantCultureIgnoreCase) ||
             ext.Equals(".svgz", StringComparison.InvariantCultureIgnoreCase))
         {
-            await Dispatcher.UIThread.InvokeAsync(() => _dragDropView?.UpdateSvgThumbnail(path));
+            await Dispatcher.UIThread.InvokeAsync(() => _dragDropView?.UpdateSvgThumbnail(fileInfo.FullName));
             return;
         }
 
-        await LoadAndShowThumbnail(path);
+        await LoadAndShowThumbnail(fileInfo);
     }
 
-    private static async Task LoadAndShowThumbnail(string path)
+    private static async Task LoadAndShowThumbnail(FileInfo fileInfo)
     {
         Bitmap? thumb;
         // Try to get preloaded image first
-        var preload = NavigationManager.TryGetPreLoadValue(path);
+        var preload = NavigationManager.TryGetPreLoadValue(fileInfo);
         if (preload?.ImageModel?.Image is Bitmap bmp)
         {
             thumb = bmp;
@@ -248,20 +248,19 @@ public static class DragAndDropHelper
         else
         {
             // Generate thumbnail
-            thumb = await GetThumbnails.GetThumbAsync(path, SizeDefaults.WindowMinSize - 30)
+            thumb = await GetThumbnails.GetThumbAsync(fileInfo, SizeDefaults.WindowMinSize - 30)
                 .ConfigureAwait(false);
             await UpdateThumbnailUI(thumb);
             
             // Load full image in background
-            await PreloadFullImage(path, preload, thumb);
+            await PreloadFullImage(fileInfo, preload, thumb);
         }
     }
 
-    private static async Task PreloadFullImage(string path, PreLoadValue? preload, Bitmap? thumb)
+    private static async Task PreloadFullImage(FileInfo fileInfo, PreLoadValue? preload, Bitmap? thumb)
     {
         await Task.Run(async () =>
         {
-            var fileInfo = new FileInfo(path);
             var sameDirectory = fileInfo.DirectoryName ==
                                 NavigationManager.ImageIterator.InitialFileInfo.DirectoryName;
 
@@ -269,7 +268,7 @@ public static class DragAndDropHelper
             {
                 if (preload is null)
                 {
-                    _preLoadValue = await NavigationManager.GetPreLoadValueAsync(path);
+                    _preLoadValue = await NavigationManager.GetPreLoadValueAsync(fileInfo);
                     thumb = _preLoadValue.ImageModel.Image as Bitmap;
                     if (thumb is not null)
                     {
@@ -353,7 +352,7 @@ public static class DragAndDropHelper
             if (currentDirectory == preloadDirectory)
             {
                 // Check for edge case error
-                var isAddedToPreloader = NavigationManager.AddToPreloader(path, _preLoadValue.ImageModel);
+                var isAddedToPreloader = NavigationManager.AddToPreloader(new FileInfo(path), _preLoadValue.ImageModel);
                 if (isAddedToPreloader)
                 {
                     NavigationManager.ImageIterator.Resynchronize();

+ 7 - 7
src/PicView.Avalonia/Functions/FunctionsMapper.cs

@@ -655,31 +655,31 @@ public static class FunctionsMapper
 
     #region Sorting
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortHelper.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
     public static async Task SortFilesByName() =>
         await FileListManager.UpdateFileList(Vm.PlatformService, Vm, SortFilesBy.Name).ConfigureAwait(false);
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortHelper.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
     public static async Task SortFilesByCreationTime() =>
         await FileListManager.UpdateFileList(Vm?.PlatformService, Vm, SortFilesBy.CreationTime).ConfigureAwait(false);
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortHelper.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
     public static async Task SortFilesByLastAccessTime() =>
         await FileListManager.UpdateFileList(Vm?.PlatformService, Vm, SortFilesBy.LastAccessTime).ConfigureAwait(false);
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortHelper.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
     public static async Task SortFilesByLastWriteTime() =>
         await FileListManager.UpdateFileList(Vm?.PlatformService, Vm, SortFilesBy.LastWriteTime).ConfigureAwait(false);
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortHelper.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
     public static async Task SortFilesBySize() =>
         await FileListManager.UpdateFileList(Vm?.PlatformService, Vm, SortFilesBy.FileSize).ConfigureAwait(false);
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortHelper.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
     public static async Task SortFilesByExtension() =>
         await FileListManager.UpdateFileList(Vm?.PlatformService, Vm, SortFilesBy.Extension).ConfigureAwait(false);
 
-    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortHelper.SortFilesBy)" />
+    /// <inheritdoc cref="FileListManager.UpdateFileList(PicView.Avalonia.Interfaces.IPlatformSpecificService, MainViewModel, FileSortOrder.SortFilesBy)" />
     public static async Task SortFilesRandomly() =>
         await FileListManager.UpdateFileList(Vm?.PlatformService, Vm, SortFilesBy.Random).ConfigureAwait(false);
 

+ 2 - 2
src/PicView.Avalonia/Gallery/GalleryFunctions.cs

@@ -121,7 +121,7 @@ public static class GalleryFunctions
         }
 
         GalleryItem? galleryItem;
-        var thumb = await GetThumbnails.GetThumbAsync(fileInfo.FullName, (uint)vm.GetGalleryItemHeight, fileInfo);
+        var thumb = await GetThumbnails.GetThumbAsync(fileInfo, (uint)vm.GetGalleryItemHeight);
         var galleryThumbInfo = GalleryThumbInfo.GalleryThumbHolder.GetThumbData(fileInfo);
         try
         {
@@ -153,7 +153,7 @@ public static class GalleryFunctions
                         ToggleGallery(vm);
                     }
 
-                    await NavigationManager.Navigate(fileInfo.FullName, vm).ConfigureAwait(false);
+                    await NavigationManager.Navigate(fileInfo, vm).ConfigureAwait(false);
                 };
                 if (galleryListBox.Items.Count > index)
                 {

+ 2 - 3
src/PicView.Avalonia/Gallery/GalleryLoad.cs

@@ -114,7 +114,7 @@ public static class GalleryLoad
                             GalleryFunctions.ToggleGallery(vm);
                         }
 
-                        await NavigationManager.Navigate(fileInfos[i1].FullName, vm).ConfigureAwait(false);
+                        await NavigationManager.Navigate(fileInfos[i1], vm).ConfigureAwait(false);
                     };
                     galleryListBox.Items.Add(galleryItem);
                     if (i != NavigationManager.GetCurrentIndex)
@@ -198,8 +198,7 @@ public static class GalleryLoad
                     ? (startPosition + i) % endPosition
                     : (startPosition - i + endPosition) % endPosition;
 
-                var thumb = await GetThumbnails.GetThumbAsync(fileInfos[nextIndex].FullName, (uint)galleryItemSize,
-                    fileInfos[nextIndex]);
+                var thumb = await GetThumbnails.GetThumbAsync(fileInfos[nextIndex], (uint)galleryItemSize);
 
                 var isSvg = fileInfos[nextIndex].Extension.Equals(".svg", StringComparison.OrdinalIgnoreCase) ||
                             fileInfos[nextIndex].Extension.Equals(".svgz", StringComparison.OrdinalIgnoreCase);

+ 6 - 8
src/PicView.Avalonia/ImageHandling/GetThumbnails.cs

@@ -8,30 +8,30 @@ namespace PicView.Avalonia.ImageHandling;
 
 public static class GetThumbnails
 {
-    public static async Task<Bitmap?> GetThumbAsync(string path, uint height, FileInfo? fileInfo = null)
+    public static async Task<Bitmap?> GetThumbAsync(FileInfo? fileInfo, uint height)
     {
         try
         {
             using var magick = new MagickImage();
             try
             {
-                magick.Ping(path);
+                magick.Ping(fileInfo);
             }
             catch (Exception e)
             {
                 DebugHelper.LogDebug(nameof(GetThumbnails), nameof(GetThumbAsync), e);
-                return await CreateThumbAsync(magick, path, height, fileInfo).ConfigureAwait(false);
+                return await CreateThumbAsync(magick, fileInfo, height).ConfigureAwait(false);
             }
             var profile = magick.GetExifProfile();
             if (profile == null)
             {
-                return await CreateThumbAsync(magick, path, height, fileInfo).ConfigureAwait(false);
+                return await CreateThumbAsync(magick, fileInfo, height).ConfigureAwait(false);
             }
 
             var thumbnail = profile.CreateThumbnail();
             if (thumbnail == null)
             {
-                return await CreateThumbAsync(magick, path, height, fileInfo).ConfigureAwait(false);
+                return await CreateThumbAsync(magick, fileInfo, height).ConfigureAwait(false);
             }
 
             thumbnail.AutoOrient();
@@ -66,15 +66,13 @@ public static class GetThumbnails
         return thumbnail?.ToWriteableBitmap();
     }
 
-    public static async Task<Bitmap?> CreateThumbAsync(MagickImage magick, string path, uint height,
-        FileInfo? fileInfo = null)
+    public static async Task<Bitmap?> CreateThumbAsync(MagickImage magick, FileInfo fileInfo, uint height)
     {
         // TODO: extract thumbnails from PlatformService and convert to Avalonia image,
         // I.E. https://boldena.com/article/64006
         // https://github.com/AvaloniaUI/Avalonia/discussions/16703
         // https://stackoverflow.com/a/42178963/2923736 convert to DLLImport to LibraryImport, source generation & AOT support
         
-        fileInfo ??= new FileInfo(path);
         await using var fileStream = FileHelper.GetOptimizedFileStream(fileInfo);
 
         if (fileInfo.Length >= 2147483648)

+ 1 - 1
src/PicView.Avalonia/ImageHandling/ImageFormatConverter.cs

@@ -60,7 +60,7 @@ public static class ImageFormatConverter
             // Different path - try to get from preload
             else
             {
-                var preloadValue = await NavigationManager.GetPreLoadValueAsync(path).ConfigureAwait(false);
+                var preloadValue = await NavigationManager.GetPreLoadValueAsync(new FileInfo(path)).ConfigureAwait(false);
                 if (preloadValue?.ImageModel.Image is Bitmap bitmap)
                 {
                     source = bitmap;

+ 1 - 1
src/PicView.Avalonia/Interfaces/IPlatformSpecificService.cs

@@ -11,7 +11,7 @@ public interface IPlatformSpecificService
     void DisableScreensaver();
     void EnableScreensaver();
 
-    List<string> GetFiles(FileInfo fileInfo);
+    List<FileInfo> GetFiles(FileInfo fileInfo);
 
     int CompareStrings(string str1, string str2);
 

+ 16 - 17
src/PicView.Avalonia/Navigation/FileListManager.cs

@@ -19,9 +19,9 @@ public static class FileListManager
     /// <param name="files">The collection of file paths to sort.</param>
     /// <param name="platformService">Platform-specific service for string comparison.</param>
     /// <returns>A sorted list of file paths.</returns>
-    public static List<string> SortIEnumerable(IEnumerable<string> files, IPlatformSpecificService? platformService)
+    public static List<FileInfo> SortIEnumerable(IEnumerable<FileInfo> files, IPlatformSpecificService? platformService)
     {
-        var sortFilesBy = FileSortHelper.GetSortOrder();
+        var sortFilesBy = FileSortOrder.GetSortOrder();
 
         switch (sortFilesBy)
         {
@@ -30,44 +30,43 @@ public static class FileListManager
                 var list = files.ToList();
                 if (Settings.Sorting.Ascending)
                 {
-                    list.Sort(platformService.CompareStrings);
+                    list.Sort((x, y) => platformService.CompareStrings(x.Name, y.Name));
                 }
                 else
                 {
-                    list.Sort((x, y) => platformService.CompareStrings(y, x));
+                    list.Sort((x, y) => platformService.CompareStrings(y.Name, x.Name));
                 }
 
                 return list;
 
             case SortFilesBy.FileSize: // Sort by file size
-                var fileInfoList = files.Select(f => new FileInfo(f)).ToList();
                 var sortedBySize = Settings.Sorting.Ascending
-                    ? fileInfoList.OrderBy(f => f.Length)
-                    : fileInfoList.OrderByDescending(f => f.Length);
-                return sortedBySize.Select(f => f.FullName).ToList();
+                    ? files.OrderBy(x => x.Length)
+                    : files.OrderByDescending(x => x.Length);
+                return sortedBySize.ToList();
 
             case SortFilesBy.Extension: // Sort by file extension
                 var sortedByExtension = Settings.Sorting.Ascending
-                    ? files.OrderBy(Path.GetExtension)
-                    : files.OrderByDescending(Path.GetExtension);
+                    ? files.OrderBy(x => x.Extension)
+                    : files.OrderByDescending(x => x.Extension);
                 return sortedByExtension.ToList();
 
             case SortFilesBy.CreationTime: // Sort by file creation time
                 var sortedByCreationTime = Settings.Sorting.Ascending
-                    ? files.OrderBy(f => new FileInfo(f).CreationTime)
-                    : files.OrderByDescending(f => new FileInfo(f).CreationTime);
+                    ? files.OrderBy(x => x.CreationTime)
+                    : files.OrderByDescending(x => x.CreationTime);
                 return sortedByCreationTime.ToList();
 
             case SortFilesBy.LastAccessTime: // Sort by file last access time
                 var sortedByLastAccessTime = Settings.Sorting.Ascending
-                    ? files.OrderBy(f => new FileInfo(f).LastAccessTime)
-                    : files.OrderByDescending(f => new FileInfo(f).LastAccessTime);
+                    ? files.OrderBy(x => x.LastAccessTime)
+                    : files.OrderByDescending(x => x.LastAccessTime);
                 return sortedByLastAccessTime.ToList();
 
             case SortFilesBy.LastWriteTime: // Sort by file last write time
                 var sortedByLastWriteTime = Settings.Sorting.Ascending
-                    ? files.OrderBy(f => new FileInfo(f).LastWriteTime)
-                    : files.OrderByDescending(f => new FileInfo(f).LastWriteTime);
+                    ? files.OrderBy(x => x.LastWriteTime)
+                    : files.OrderByDescending(x => x.LastWriteTime);
                 return sortedByLastWriteTime.ToList();
 
             case SortFilesBy.Random: // Sort files randomly
@@ -131,7 +130,7 @@ public static class FileListManager
                     return false;
                 }
 
-                NavigationManager.UpdateFileListAndIndex(files, files.IndexOf(vm.PicViewer.FileInfo.FullName));
+                NavigationManager.UpdateFileListAndIndex(files, files.IndexOf(vm.PicViewer.FileInfo));
                 TitleManager.SetTitle(vm);
                 return true;
             }

+ 30 - 46
src/PicView.Avalonia/Navigation/ImageIterator.cs

@@ -24,7 +24,7 @@ public class ImageIterator : IAsyncDisposable
 
     private bool _disposed;
 
-    public List<string> ImagePaths { get; private set; }
+    public List<FileInfo> ImagePaths { get; private set; }
     public int CurrentIndex { get; private set; }
 
     public int GetNonZeroIndex => CurrentIndex + 1 > GetCount ? 1 : CurrentIndex + 1;
@@ -70,11 +70,11 @@ public class ImageIterator : IAsyncDisposable
             initialDirectory = fileInfo;
         }
         ImagePaths = vm.PlatformService.GetFiles(initialDirectory);
-        CurrentIndex = Directory.Exists(fileInfo.FullName) ? 0 : ImagePaths.IndexOf(fileInfo.FullName);
+        CurrentIndex = ImagePaths.FindIndex(x => x.FullName.Equals(fileInfo.FullName));
         InitiateFileSystemWatcher(fileInfo);
     }
 
-    public ImageIterator(FileInfo fileInfo, List<string> imagePaths, int currentIndex, MainViewModel vm)
+    public ImageIterator(FileInfo fileInfo, List<FileInfo> imagePaths, int currentIndex, MainViewModel vm)
     {
 #if DEBUG
         ArgumentNullException.ThrowIfNull(fileInfo);
@@ -209,7 +209,7 @@ public class ImageIterator : IAsyncDisposable
 
             TitleManager.SetTitle(_vm);
 
-            var index = ImagePaths.IndexOf(e.FullPath);
+            var index = ImagePaths.FindIndex(x => x.FullName.Equals(e.FullPath));
             if (index < 0)
             {
                 PreLoader.Resynchronize(ImagePaths);
@@ -247,14 +247,10 @@ public class ImageIterator : IAsyncDisposable
     {
         try
         {
-            if (ImagePaths.Contains(e.FullPath) == false)
-            {
-                return;
-            }
 
             _isRunning = true;
 
-            var index = ImagePaths.IndexOf(e.FullPath);
+            var index = ImagePaths.FindIndex(x => x.FullName.Equals(e.FullPath));
             if (index < 0)
             {
                 return;
@@ -263,11 +259,7 @@ public class ImageIterator : IAsyncDisposable
             var currentIndex = CurrentIndex;
             var isSameFile = currentIndex == index;
 
-            if (!ImagePaths.Remove(e.FullPath))
-            {
-                DebugHelper.LogDebug(nameof(ImageIterator), nameof(OnFileDeleted), $"Failed to remove {e.FullPath}");
-                return;
-            }
+            ImagePaths.RemoveAt(index);
 
             if (isSameFile)
             {
@@ -281,7 +273,7 @@ public class ImageIterator : IAsyncDisposable
                 PreLoader.Resynchronize(ImagePaths);
                 var newIndex = GetIteration(index, NavigateTo.Previous);
                 CurrentIndex = newIndex;
-                _vm.PicViewer.FileInfo = new FileInfo(ImagePaths[CurrentIndex]);
+                _vm.PicViewer.FileInfo = ImagePaths[CurrentIndex];
                 await IterateToIndex(CurrentIndex, new CancellationTokenSource());
             }
             else
@@ -301,7 +293,7 @@ public class ImageIterator : IAsyncDisposable
                     }
                 }
 
-                var indexOf = ImagePaths.IndexOf(_vm.PicViewer.FileInfo.FullName);
+                var indexOf = ImagePaths.FindIndex(x => x.FullName.Equals(_vm.PicViewer.FileInfo.FullName));
                 _vm.SelectedGalleryItemIndex = indexOf; // Fixes deselection bug 
                 CurrentIndex = indexOf;
                 if (isSameFile)
@@ -331,19 +323,20 @@ public class ImageIterator : IAsyncDisposable
     {
         try
         {
+            var index = ImagePaths.FindIndex(x => x.FullName.Equals(e.FullPath));
+            if (index < 0)
+            {
+                return;
+            }
             if (e.FullPath.IsSupported() == false)
             {
-                if (ImagePaths.Contains(e.OldFullPath))
-                {
-                    ImagePaths.Remove(e.OldFullPath);
-                }
-
+                ImagePaths.RemoveAt(index);
                 return;
             }
 
             _isRunning = true;
 
-            var oldIndex = ImagePaths.IndexOf(e.OldFullPath);
+            var oldIndex = ImagePaths.FindIndex(x => x.FullName.Equals(e.OldFullPath));;
             var currentIndex = CurrentIndex;
             var sameFile = currentIndex == oldIndex;
             var fileInfo = new FileInfo(e.FullPath);
@@ -368,12 +361,6 @@ public class ImageIterator : IAsyncDisposable
 
             ImagePaths = newList;
 
-            var index = ImagePaths.IndexOf(e.FullPath);
-            if (index < 0)
-            {
-                return;
-            }
-
             if (fileInfo.Exists == false)
             {
                 return;
@@ -429,11 +416,8 @@ public class ImageIterator : IAsyncDisposable
     public void Add(int index, ImageModel imageModel) =>
         PreLoader.Add(index, ImagePaths, imageModel);
 
-    public bool Add(string file, ImageModel imageModel)
-    {
-        file = file.Replace('/', '\\');
-        return PreLoader.Add(ImagePaths.IndexOf(file), ImagePaths, imageModel);
-    }
+    public bool Add(FileInfo file, ImageModel imageModel) =>
+        PreLoader.Add(ImagePaths.FindIndex(x => x.FullName.Equals(file.FullName)), ImagePaths, imageModel);
 
     public PreLoadValue? GetPreLoadValue(int index)
     {
@@ -447,9 +431,9 @@ public class ImageIterator : IAsyncDisposable
             : PreLoader.Get(index, ImagePaths);
     }
 
-    public PreLoadValue? GetPreLoadValue(string file)
+    public PreLoadValue? GetPreLoadValue(FileInfo file)
     {
-        var index = ImagePaths.IndexOf(file);
+        var index = ImagePaths.FindIndex(x => x.FullName.Equals(file.FullName));
         if (index < 0 || index >= ImagePaths.Count)
         {
             return null;
@@ -466,12 +450,12 @@ public class ImageIterator : IAsyncDisposable
 
     public PreLoadValue? GetCurrentPreLoadValue() =>
         _isRunning
-            ? PreLoader.Get(_vm.PicViewer.FileInfo.FullName, ImagePaths)
+            ? PreLoader.Get(_vm.PicViewer.FileInfo, ImagePaths)
             : PreLoader.Get(CurrentIndex, ImagePaths);
 
     public async Task<PreLoadValue?> GetCurrentPreLoadValueAsync() =>
         _isRunning
-            ? await PreLoader.GetOrLoadAsync(_vm.PicViewer.FileInfo.FullName, ImagePaths)
+            ? await PreLoader.GetOrLoadAsync(_vm.PicViewer.FileInfo, ImagePaths)
             : await PreLoader.GetOrLoadAsync(CurrentIndex, ImagePaths);
 
     public PreLoadValue? GetNextPreLoadValue()
@@ -508,7 +492,7 @@ public class ImageIterator : IAsyncDisposable
                 .ConfigureAwait(false);
             var oldList = ImagePaths;
             ImagePaths = fileList;
-            CurrentIndex = ImagePaths.IndexOf(_vm.PicViewer.FileInfo.FullName);
+            CurrentIndex = ImagePaths.FindIndex(x => x.FullName.Equals(_vm.PicViewer.FileInfo.FullName));
             TitleManager.SetTitle(_vm);
             await ClearAsync().ConfigureAwait(false);
             await PreloadAsync().ConfigureAwait(false);
@@ -518,9 +502,9 @@ public class ImageIterator : IAsyncDisposable
             {
                 for (var i = 0; i < oldList.Count; i++)
                 {
-                    if (i < fileList.Count && !oldList[i].Contains(fileList[i]))
+                    if (i < fileList.Count && !oldList[i].FullName.Equals(fileList[i].FullName))
                     {
-                        await GalleryFunctions.AddGalleryItem(fileList.IndexOf(fileList[i]), new FileInfo(fileList[i]),
+                        await GalleryFunctions.AddGalleryItem(fileList.FindIndex(x => x.FullName.Equals(fileList[i].FullName)), fileList[i],
                             _vm, DispatcherPriority.Background);
                     }
                 }
@@ -529,7 +513,7 @@ public class ImageIterator : IAsyncDisposable
             {
                 for (var i = 0; i < fileList.Count; i++)
                 {
-                    if (i < oldList.Count && fileList[i].Contains(oldList[i]))
+                    if (i < oldList.Count && fileList[i].FullName.Equals(oldList[i].FullName))
                     {
                         GalleryFunctions.RemoveGalleryItem(i, _vm);
                     }
@@ -705,7 +689,7 @@ public class ImageIterator : IAsyncDisposable
                         // This is a timeout, not cancellation from navigation
                         preloadValue =
                             new PreLoadValue(
-                                await GetImageModel.GetImageModelAsync(new FileInfo(ImagePaths[CurrentIndex])))
+                                await GetImageModel.GetImageModelAsync(ImagePaths[CurrentIndex]))
                             {
                                 IsLoading = false
                             };
@@ -721,7 +705,7 @@ public class ImageIterator : IAsyncDisposable
             }
             else
             {
-                var imageModel = await GetImageModel.GetImageModelAsync(new FileInfo(ImagePaths[index]))
+                var imageModel = await GetImageModel.GetImageModelAsync(ImagePaths[index])
                     .ConfigureAwait(false);
                 preloadValue = new PreLoadValue(imageModel);
             }
@@ -780,11 +764,11 @@ public class ImageIterator : IAsyncDisposable
             // Add recent files
             if (string.IsNullOrWhiteSpace(TempFileHelper.TempFilePath) && ImagePaths.Count > CurrentIndex)
             {
-                FileHistoryManager.Add(ImagePaths[CurrentIndex]);
+                FileHistoryManager.Add(ImagePaths[CurrentIndex].FullName);
                 if (Settings.ImageScaling.ShowImageSideBySide)
                 {
                     FileHistoryManager.Add(
-                        ImagePaths[GetIteration(CurrentIndex, IsReversed ? NavigateTo.Previous : NavigateTo.Next)]);
+                        ImagePaths[GetIteration(CurrentIndex, IsReversed ? NavigateTo.Previous : NavigateTo.Next)].FullName);
                 }
             }
         }
@@ -878,7 +862,7 @@ public class ImageIterator : IAsyncDisposable
         await IterateToIndex(index, cts).ConfigureAwait(false);
     }
 
-    public void UpdateFileListAndIndex(List<string> fileList, int index)
+    public void UpdateFileListAndIndex(List<FileInfo> fileList, int index)
     {
         ImagePaths = fileList;
         CurrentIndex = index;

+ 2 - 2
src/PicView.Avalonia/Navigation/ImageLoader.cs

@@ -114,7 +114,7 @@ public static class ImageLoader
             // If image is in same directory as is being browsed, navigate to it. Otherwise, load without iterator.
             if (fileInfo.DirectoryName == imageIterator.InitialFileInfo.DirectoryName)
             {
-                var index = imageIterator.ImagePaths.IndexOf(fileInfo.FullName);
+                var index = imageIterator.ImagePaths.IndexOf(fileInfo);
                 if (index != -1)
                 {
                     await imageIterator.IterateToIndex(index, _cancellationTokenSource).ConfigureAwait(false);
@@ -212,7 +212,7 @@ public static class ImageLoader
             return;
         }
 
-        var firstFileInfo = new FileInfo(newFileList[0]);
+        var firstFileInfo = newFileList[0];
         await NavigationManager.LoadWithoutImageIterator(firstFileInfo, vm, newFileList);
     }
 

+ 46 - 63
src/PicView.Avalonia/Navigation/NavigationManager.cs

@@ -13,6 +13,7 @@ using PicView.Core.Localization;
 using PicView.Core.Models;
 using PicView.Core.Navigation;
 using PicView.Core.Preloading;
+using ZLinq;
 
 namespace PicView.Avalonia.Navigation;
 
@@ -38,7 +39,7 @@ public static class NavigationManager
     /// directory.
     /// </param>
     /// <param name="index">Optional: The index at which to start the navigation. Defaults to 0.</param>
-    public static async Task LoadWithoutImageIterator(FileInfo fileInfo, MainViewModel vm, List<string>? files = null,
+    public static async Task LoadWithoutImageIterator(FileInfo fileInfo, MainViewModel vm, List<FileInfo>? files = null,
         int index = 0)
     {
         var imageModel = await GetImageModel.GetImageModelAsync(fileInfo).ConfigureAwait(false);
@@ -109,10 +110,10 @@ public static class NavigationManager
         }
 
         vm.IsLoading = false;
-        FileHistoryManager.Add(ImageIterator.ImagePaths[index]);
+        FileHistoryManager.Add(ImageIterator.ImagePaths[index].FullName);
         if (Settings.ImageScaling.ShowImageSideBySide)
         {
-            FileHistoryManager.Add(ImageIterator.ImagePaths[ImageIterator.GetIteration(index, NavigateTo.Next)]);
+            FileHistoryManager.Add(ImageIterator.ImagePaths[ImageIterator.GetIteration(index, NavigateTo.Next)].FullName);
         }
 
         await GalleryLoad.CheckAndReloadGallery(fileInfo, vm);
@@ -143,7 +144,7 @@ public static class NavigationManager
             if (vm.PicViewer.FileInfo is null && ImageIterator is not null)
             {
                 // Fixes issue that shouldn't happen. Should investigate.
-                vm.PicViewer.FileInfo = new FileInfo(ImageIterator.ImagePaths[0]);
+                vm.PicViewer.FileInfo = ImageIterator.ImagePaths[0];
             }
             else
             {
@@ -165,7 +166,7 @@ public static class NavigationManager
 
         var navigateTo = next ? NavigateTo.Next : NavigateTo.Previous;
         var nextIteration = ImageIterator.GetIteration(ImageIterator.CurrentIndex, navigateTo);
-        var currentFileName = ImageIterator.ImagePaths[ImageIterator.CurrentIndex];
+        var currentFileName = ImageIterator.ImagePaths[ImageIterator.CurrentIndex].FullName;
         if (TiffManager.IsTiff(currentFileName))
         {
             await TiffNavigation(vm, currentFileName, nextIteration).ConfigureAwait(false);
@@ -274,14 +275,14 @@ public static class NavigationManager
         await ImageLoader.CheckCancellationAndStartIterateToIndex(index, ImageIterator).ConfigureAwait(false);
     }
 
-    public static async Task Navigate(string fileName, MainViewModel vm)
+    public static async Task Navigate(FileInfo fileInfo, MainViewModel vm)
     {
         if (!CanNavigate(vm))
         {
             return;
         }
 
-        var index = ImageIterator.ImagePaths.IndexOf(fileName);
+        var index = ImageIterator.ImagePaths.IndexOf(fileInfo);
 
         await ImageLoader.CheckCancellationAndStartIterateToIndex(index, ImageIterator).ConfigureAwait(false);
     }
@@ -398,49 +399,12 @@ public static class NavigationManager
 
         if (Settings.Sorting.IncludeSubDirectories)
         {
-            await Task.Run(async () =>
-            {
-
-                // Try to find the first file of the next/previous directory in the file list
-                var imageIterator = ImageIterator;
-                var filePaths = imageIterator?.ImagePaths;
-                if (filePaths is { Count: > 0 })
-                {
-                    // Find current file's directory
-                    var currentIndex = imageIterator.CurrentIndex;
-                    var currentDir = Path.GetDirectoryName(filePaths[currentIndex]) ?? string.Empty;
-
-                    // Scan forward/backward for the first file belonging to a different directory
-                    var step = next ? 1 : -1;
-                    var i = currentIndex + step;
-                    while (i >= 0 && i < filePaths.Count)
-                    {
-                        var nextDir = Path.GetDirectoryName(filePaths[i]) ?? string.Empty;
-                        if (!string.Equals(nextDir, currentDir, StringComparison.OrdinalIgnoreCase))
-                        {
-                            // Found the first file in the next (or previous) directory
-                            await ImageLoader.IterateToIndexAsync(i, imageIterator).ConfigureAwait(false);
-                            return;
-                        }
-
-                        i += step;
-                    }
-
-                    if (Settings.UIProperties.Looping)
-                    {
-                        var loopedIndex = next ? 0 : imageIterator.GetCount - 1;
-                        await ImageLoader.IterateToIndexAsync(loopedIndex, imageIterator).ConfigureAwait(false);
-                    }
-                    else
-                    {
-                        await NextDirectory();
-                    }
-                }
-            });
-            return;
+            await NextDirectoryWithin().ConfigureAwait(false);
+        }
+        else
+        {
+            await NextDirectory().ConfigureAwait(false);
         }
-
-        await NextDirectory();
 
         return;
 
@@ -456,13 +420,34 @@ public static class NavigationManager
             else
             {
                 vm.PlatformService.StopTaskbarProgress();
-                await LoadWithoutImageIterator(new FileInfo(fileList[0]), vm, fileList);
+                await LoadWithoutImageIterator(fileList[0], vm, fileList);
                 if (vm.PicViewer.Title == TranslationManager.Translation.Loading)
                 {
                     TitleManager.SetTitle(vm);
                 }
             }
         }
+        
+        async Task NextDirectoryWithin()
+        {
+            await Task.Run(async() =>
+            {
+                var imageIterator = ImageIterator;
+                var imagePaths = imageIterator?.ImagePaths;
+                var currentDir = imagePaths[imageIterator.CurrentIndex].DirectoryName;
+                    
+                var directories = new List<string>();
+                foreach (var path in imagePaths.Where(path => !directories.Contains(path.DirectoryName)).AsValueEnumerable())
+                {
+                    directories.Add(path.DirectoryName);
+                }
+                var index = directories.IndexOf(currentDir);
+                var nextIndex = next ? (index + 1) % directories.Count : (index - 1 + directories.Count) % directories.Count;
+                var nextDir = directories[nextIndex];
+                var firstFileInNextDir = imagePaths.IndexOf(imagePaths.FirstOrDefault(path => path.DirectoryName == nextDir));
+                await ImageLoader.IterateToIndexAsync(firstFileInNextDir, imageIterator).ConfigureAwait(false);
+            });
+        }
     }
     
     /// <summary>
@@ -471,12 +456,12 @@ public static class NavigationManager
     /// <param name="next">True to get the next folder, false for the previous folder.</param>
     /// <param name="vm">The main view model instance.</param>
     /// <returns>A task representing the asynchronous operation that returns a list of file paths.</returns>
-    private static async Task<List<string>?> GetNextFolderFileList(bool next, MainViewModel vm)
+    private static async Task<List<FileInfo>?> GetNextFolderFileList(bool next, MainViewModel vm)
     {
         return await Task.Run(() =>
         {
             var indexChange = next ? 1 : -1;
-            var currentFolder = Path.GetDirectoryName(ImageIterator?.ImagePaths[ImageIterator.CurrentIndex]);
+            var currentFolder = ImageIterator?.ImagePaths[ImageIterator.CurrentIndex].DirectoryName;
             var parentFolder = Path.GetDirectoryName(currentFolder);
             var directories = Directory.GetDirectories(parentFolder, "*", SearchOption.TopDirectoryOnly);
 
@@ -566,19 +551,17 @@ public static class NavigationManager
     }
 
     public static bool IsCollectionEmpty => ImageIterator?.ImagePaths is null || ImageIterator?.ImagePaths?.Count < 0;
-    public static List<string>? GetCollection => ImageIterator?.ImagePaths;
+    public static List<FileInfo>? GetCollection => ImageIterator?.ImagePaths;
 
-    public static void UpdateFileListAndIndex(List<string> fileList, int index) =>
+    public static void UpdateFileListAndIndex(List<FileInfo> fileList, int index) =>
         ImageIterator?.UpdateFileListAndIndex(fileList, index);
 
-    public static int GetFileNameIndex(string fileName)
+    public static int GetFileNameIndex(FileInfo fileName)
     {
         if (IsCollectionEmpty)
         {
             return -1;
         }
-
-        fileName = fileName.Replace("/", "\\");
         return ImageIterator.ImagePaths.IndexOf(fileName);
     }
 
@@ -599,7 +582,7 @@ public static class NavigationManager
             return null;
         }
 
-        return ImageIterator.ImagePaths[index];
+        return ImageIterator.ImagePaths[index].FullName;
     }
 
     /// <summary>
@@ -625,14 +608,14 @@ public static class NavigationManager
     public static PreLoadValue? TryGetPreLoadValue(int index) =>
         ImageIterator?.GetPreLoadValue(index) ?? null;
 
-    public static PreLoadValue? TryGetPreLoadValue(string fileName) =>
-        ImageIterator?.GetPreLoadValue(fileName) ?? null;
+    public static PreLoadValue? TryGetPreLoadValue(FileInfo fileInfo) =>
+        ImageIterator?.GetPreLoadValue(fileInfo) ?? null;
 
     public static async Task<PreLoadValue?> GetPreLoadValueAsync(int index) =>
         await ImageIterator?.GetOrLoadPreLoadValueAsync(index) ?? null;
 
-    public static async Task<PreLoadValue?> GetPreLoadValueAsync(string fileName) =>
-        await ImageIterator?.GetOrLoadPreLoadValueAsync(GetFileNameIndex(fileName)) ?? null;
+    public static async Task<PreLoadValue?> GetPreLoadValueAsync(FileInfo fileInfo) =>
+        await ImageIterator?.GetOrLoadPreLoadValueAsync(GetFileNameIndex(fileInfo)) ?? null;
 
     public static PreLoadValue? GetCurrentPreLoadValue() =>
         ImageIterator?.GetCurrentPreLoadValue() ?? null;
@@ -652,7 +635,7 @@ public static class NavigationManager
     public static void AddToPreloader(int index, ImageModel imageModel) =>
         ImageIterator?.Add(index, imageModel);
 
-    public static bool AddToPreloader(string file, ImageModel imageModel) =>
+    public static bool AddToPreloader(FileInfo file, ImageModel imageModel) =>
         ImageIterator?.Add(file, imageModel) ?? false;
 
     public static async Task PreloadAsync() =>

+ 2 - 2
src/PicView.Avalonia/Navigation/UpdateImage.cs

@@ -30,7 +30,7 @@ public static class UpdateImage
     /// <param name="preLoadValue">The preloaded value of the current image.</param>
     /// <param name="nextPreloadValue">Optional: The preloaded value of the next image, used for side-by-side display.</param>
     /// <returns>A task representing the asynchronous operation.</returns>
-    public static async Task UpdateSource(MainViewModel vm, int index, List<string> imagePaths,
+    public static async Task UpdateSource(MainViewModel vm, int index, List<FileInfo> imagePaths,
         PreLoadValue? preLoadValue,
         PreLoadValue? nextPreloadValue = null)
     {
@@ -42,7 +42,7 @@ public static class UpdateImage
         }
         if (preLoadValue.ImageModel?.Image is null && index == NavigationManager.GetCurrentIndex)
         {
-            var fileInfo = preLoadValue.ImageModel?.FileInfo ?? new FileInfo(imagePaths[index]);
+            var fileInfo = preLoadValue.ImageModel?.FileInfo ?? imagePaths[index];
             preLoadValue.ImageModel = await GetImageModel.GetImageModelAsync(fileInfo).ConfigureAwait(false);
         }
         

+ 7 - 9
src/PicView.Avalonia/Views/BatchResizeView.axaml.cs

@@ -326,7 +326,7 @@ public partial class BatchResizeView : UserControl
                 }
             }
 
-            var enumerable = files as string[] ?? files.ToArray();
+            var enumerable = files.ToArray();
             ProgressBar.Maximum = enumerable.Length;
             ProgressBar.Value = 0;
 
@@ -340,15 +340,13 @@ public partial class BatchResizeView : UserControl
             {
                 token.ThrowIfCancellationRequested();
 
-                var ext = Path.GetExtension(file);
-                var destination = Path.Combine(outputFolder, Path.GetFileName(file));
-                
-                var fileInfo = new FileInfo(file);
+                var ext = file.Extension.ToLower();
+                var destination = Path.Combine(outputFolder, file.Name);
                 
                 using var magick = new MagickImage();
                 magick.Ping(file);
                 
-                var oldSize = $" ({magick.Width} x {magick.Height}{ImageTitleFormatter.FormatAspectRatio((int)magick.Width, (int)magick.Height)}{fileInfo.Length.GetReadableFileSize()}";
+                var oldSize = $" ({magick.Width} x {magick.Height}{ImageTitleFormatter.FormatAspectRatio((int)magick.Width, (int)magick.Height)}{file.Length.GetReadableFileSize()}";
 
                 if (toConvert)
                 {
@@ -380,7 +378,7 @@ public partial class BatchResizeView : UserControl
                     }
                 }
 
-                await using var stream = FileHelper.GetOptimizedFileStream(fileInfo, true);
+                await using var stream = FileHelper.GetOptimizedFileStream(file, true);
 
                 var success = await SaveImageFileHelper.SaveImageAsync(
                     stream,
@@ -406,10 +404,10 @@ public partial class BatchResizeView : UserControl
 
                     await Dispatcher.UIThread.InvokeAsync(() =>
                     {
-                        BatchLogContainer.Children.Add(CreateTextBlockLog(Path.GetFileName(file), oldSize,
+                        BatchLogContainer.Children.Add(CreateTextBlockLog(file.Name, oldSize,
                             newSize));
                     });
-                    await ProcessThumbs(file, Path.GetDirectoryName(destination), quality, ext).ConfigureAwait(false);
+                    await ProcessThumbs(file.FullName, Path.GetDirectoryName(destination), quality, ext).ConfigureAwait(false);
                     await Dispatcher.UIThread.InvokeAsync(() =>
                     {
                         ProgressBar.Value++;

+ 19 - 38
src/PicView.Core/FileSorting/FileListRetriever.cs

@@ -1,65 +1,46 @@
 using PicView.Core.DebugTools;
 using PicView.Core.FileHandling;
+using ZLinq;
 
 namespace PicView.Core.FileSorting;
 
 public static class FileListRetriever
 {
-    public static IEnumerable<string> RetrieveFiles(FileInfo fileInfo)
+    public static IEnumerable<FileInfo> RetrieveFiles(FileInfo fileInfo)
     {
-        if (fileInfo == null)
+        var directoryPath = fileInfo switch
         {
-            return new List<string>();
-        }
-
-        // Check if the file is a directory or not
-        var isDirectory = fileInfo.Attributes.HasFlag(FileAttributes.Directory);
+            null => null,
+            { Attributes: var attr } when attr.HasFlag(FileAttributes.Directory) => fileInfo.FullName,
+            _ => fileInfo.DirectoryName
+        };
 
-        // Get the directory path based on whether the file is a directory or not
-        var directory = isDirectory ? fileInfo.FullName : fileInfo.DirectoryName;
-        if (directory is null)
+        if (string.IsNullOrEmpty(directoryPath))
         {
-            return new List<string>();
+            return [];
         }
 
-        string[] enumerable;
-        // Check if the subdirectories are to be included in the search
         var recurseSubdirectories =
             Settings.Sorting.IncludeSubDirectories && string.IsNullOrWhiteSpace(TempFileHelper.TempFilePath);
+
         try
         {
-            // Get the list of files in the directory
-            IEnumerable<string> files;
             if (recurseSubdirectories)
             {
-                files = Directory.EnumerateFiles(directory, "*.*", new EnumerationOptions
-                {
-                    AttributesToSkip = default, // Pick up hidden files
-                    RecurseSubdirectories = true
-                }).AsParallel();
-            }
-            else
-            {
-                files = Directory.EnumerateFiles(directory, "*.*", new EnumerationOptions
-                {
-                    AttributesToSkip = default,
-                    RecurseSubdirectories = false
-                });
+                return new DirectoryInfo(directoryPath)
+                    .DescendantsAndSelf()
+                    .OfType<FileInfo>()
+                    .Where(x => x.Extension.IsSupported()).ToList();
             }
-
-            enumerable = files as string[] ?? files.ToArray();
+            return new DirectoryInfo(directoryPath)
+                .ChildrenAndSelf()
+                .OfType<FileInfo>()
+                .Where(x => x.Extension.IsSupported()).ToList();
         }
         catch (Exception exception)
         {
             DebugHelper.LogDebug(nameof(FileListRetriever), nameof(RetrieveFiles), exception);
-            return new List<string>();
-        }
-
-        return enumerable.Where(IsExtensionValid);
-
-        bool IsExtensionValid(string f)
-        {
-            return SupportedFiles.FileExtensions.Contains(Path.GetExtension(f), StringComparer.OrdinalIgnoreCase);
+            return [];
         }
     }
 }

+ 1 - 1
src/PicView.Core/FileSorting/FileSortHelper.cs → src/PicView.Core/FileSorting/FileSortOrder.cs

@@ -1,6 +1,6 @@
 namespace PicView.Core.FileSorting;
 
-public static class FileSortHelper
+public static class FileSortOrder
 {
     public static SortFilesBy GetSortOrder()
     {

+ 2 - 0
src/PicView.Core/PicView.Core.csproj

@@ -28,6 +28,8 @@
     <PackageReference Include="Magick.NET-Q8-OpenMP-x64" Version="14.6.0" />
     <PackageReference Include="ReactiveUI" Version="20.3.1" />
     <PackageReference Include="SharpCompress" Version="0.40.0" />
+    <PackageReference Include="ZLinq" Version="1.4.10" />
+    <PackageReference Include="ZLinq.FileSystem" Version="1.4.10" />
     <PackageReference Include="ZString" Version="2.6.0" />
   </ItemGroup>
   <ItemGroup>

+ 25 - 24
src/PicView.Core/Preloading/Preloader.cs

@@ -2,6 +2,7 @@
 using System.Diagnostics;
 using PicView.Core.DebugTools;
 using PicView.Core.Models;
+using ZLinq;
 using static System.GC;
 
 namespace PicView.Core.Preloading;
@@ -33,7 +34,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
     /// <param name="index">The index of the image in the list.</param>
     /// <param name="list">The list of image paths.</param>
     /// <returns>True if the image was added successfully; otherwise, false.</returns>
-    public async Task<bool> AddAsync(int index, List<string> list)
+    public async Task<bool> AddAsync(int index, List<FileInfo> list)
     {
         if (list == null || index < 0 || index >= list.Count)
         {
@@ -59,7 +60,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
 
         try
         {
-            var fileInfo = imageModel.FileInfo = new FileInfo(list[index]);
+            var fileInfo = imageModel.FileInfo = list[index];
             imageModel = await imageModelLoader(fileInfo).ConfigureAwait(false);
             preLoadValue.ImageModel = imageModel;
 #if DEBUG
@@ -89,7 +90,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
     /// <param name="list">The list of image paths corresponding to the preload list.</param>
     /// <param name="imageModel">The image model to preload.</param>
     /// <returns>True if the image model was successfully added to the preload list; otherwise, false.</returns>
-    public bool Add(int index, List<string> list, ImageModel imageModel)
+    public bool Add(int index, List<FileInfo> list, ImageModel imageModel)
     {
         if (list == null || index < 0 || index >= list.Count)
         {
@@ -119,7 +120,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
     /// <param name="index">The index of the item to update.</param>
     /// <param name="fileInfo">The new file information to assign.</param>
     /// <param name="list">The list of file paths.</param>
-    public void RefreshFileInfo(int index, FileInfo fileInfo, List<string> list)
+    public void RefreshFileInfo(int index, FileInfo fileInfo, List<FileInfo> list)
     {
         if (list == null)
         {
@@ -150,7 +151,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
     /// <remarks>
     ///     Call it after the file watcher detects changes, or the list is resorted
     /// </remarks>
-    public void Resynchronize(List<string> list)
+    public void Resynchronize(List<FileInfo> list)
     {
         if (list == null)
         {
@@ -166,7 +167,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
         _cancellationTokenSource?.Cancel();
 
         // Create a reverse lookup from file path to current index
-        var reverseLookup = new Dictionary<string, int>(list.Count);
+        var reverseLookup = new Dictionary<FileInfo, int>(list.Count);
         for (var i = 0; i < list.Count; i++)
         {
             reverseLookup[list[i]] = i;
@@ -182,14 +183,14 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
                 continue;
             }
 
-            var filePath = preLoadValue.ImageModel?.FileInfo?.FullName;
-            if (string.IsNullOrEmpty(filePath))
+            var file = preLoadValue.ImageModel?.FileInfo;
+            if (file is null)
             {
                 Remove(oldIndex, list);
                 continue;
             }
 
-            if (!reverseLookup.TryGetValue(filePath, out var newIndex))
+            if (!reverseLookup.TryGetValue(file, out var newIndex))
             {
                 // File no longer exists in the list
                 Remove(oldIndex, list);
@@ -220,14 +221,14 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
 #if DEBUG
                 if (_showAddRemove)
                 {
-                    Trace.WriteLine($"Failed to resynchronize {filePath} to index {newIndex}");
+                    Trace.WriteLine($"Failed to resynchronize {file} to index {newIndex}");
                 }
 #endif
             }
 #if DEBUG
             else if (_showAddRemove)
             {
-                Trace.WriteLine($"Resynchronized {filePath} from index {oldIndex} to {newIndex}");
+                Trace.WriteLine($"Resynchronized {file} from index {oldIndex} to {newIndex}");
             }
 #endif
         }
@@ -243,7 +244,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
     /// <param name="key">The key to check.</param>
     /// <param name="list">The list of image paths.</param>
     /// <returns>True if the key exists; otherwise, false.</returns>
-    public bool Contains(int key, List<string> list) =>
+    public bool Contains(int key, List<FileInfo> list) =>
         list != null && key >= 0 && key < list.Count && _preLoadList.ContainsKey(key);
 
     /// <summary>
@@ -252,7 +253,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
     /// <param name="key">The key of the preloaded value.</param>
     /// <param name="list">The list of image paths.</param>
     /// <returns>The preloaded value if it exists; otherwise, null.</returns>
-    public PreLoadValue? Get(int key, List<string> list)
+    public PreLoadValue? Get(int key, List<FileInfo> list)
     {
         if (list != null && key >= 0 && key < list.Count)
         {
@@ -269,14 +270,14 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
     /// <param name="fileName">The full path of the image file to retrieve the preloaded value for.</param>
     /// <param name="list">The list of image paths.</param>
     /// <returns>The preloaded value if it exists; otherwise, null.</returns>
-    public PreLoadValue? Get(string fileName, List<string> list)
+    public PreLoadValue? Get(FileInfo file, List<FileInfo> list)
     {
-        if (list == null || string.IsNullOrEmpty(fileName))
+        if (list == null || file is null)
         {
             return null;
         }
 
-        var index = list.IndexOf(fileName);
+        var index = list.IndexOf(file);
         return index >= 0 ? _preLoadList[index] : null;
     }
 
@@ -287,7 +288,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
     /// <param name="key">The index of the image in the list.</param>
     /// <param name="list">The list of image paths.</param>
     /// <returns>The preloaded image value if found or successfully loaded; otherwise, null.</returns>
-    public async Task<PreLoadValue?> GetOrLoadAsync(int key, List<string> list)
+    public async Task<PreLoadValue?> GetOrLoadAsync(int key, List<FileInfo> list)
     {
         if (list == null || key < 0 || key >= list.Count)
         {
@@ -310,8 +311,8 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
     /// <param name="fileName">The full path of the image file to retrieve the preloaded value for.</param>
     /// <param name="list">The list of image paths.</param>
     /// <returns>The preloaded value if it exists; otherwise, null.</returns>
-    public async Task<PreLoadValue?> GetOrLoadAsync(string fileName, List<string> list) =>
-        await GetOrLoadAsync(_preLoadList.Values.ToList().FindIndex(x => x.ImageModel?.FileInfo?.FullName == fileName),
+    public async Task<PreLoadValue?> GetOrLoadAsync(FileInfo fileName, List<FileInfo> list) =>
+        await GetOrLoadAsync(_preLoadList.Values.ToList().FindIndex(x => x.ImageModel?.FileInfo == fileName),
             list);
 
     #endregion
@@ -324,7 +325,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
     /// <param name="key">The key to remove.</param>
     /// <param name="list">The list of image paths.</param>
     /// <returns>True if the key was removed; otherwise, false.</returns>
-    public bool Remove(int key, List<string> list)
+    public bool Remove(int key, List<FileInfo> list)
     {
         if (list == null || key < 0 || key >= list.Count)
         {
@@ -369,7 +370,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
     /// <param name="fileName">The full path of the image to remove.</param>
     /// <param name="list">The list of image paths.</param>
     /// <returns>True if the image was successfully removed; otherwise, false.</returns>
-    public bool Remove(string fileName, List<string> list)
+    public bool Remove(string fileName, List<FileInfo> list)
     {
         if (string.IsNullOrEmpty(fileName))
         {
@@ -454,7 +455,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
     /// <param name="currentIndex">The current index of the image.</param>
     /// <param name="reverse">Indicates whether to preload in reverse order.</param>
     /// <param name="list">The list of image paths.</param>
-    public async Task PreLoadAsync(int currentIndex, bool reverse, List<string> list)
+    public async Task PreLoadAsync(int currentIndex, bool reverse, List<FileInfo> list)
     {
         if (list == null)
         {
@@ -501,7 +502,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
         }
     }
 
-    private async Task PreLoadInternalAsync(int currentIndex, bool reverse, List<string> list,
+    private async Task PreLoadInternalAsync(int currentIndex, bool reverse, List<FileInfo> list,
         CancellationToken token)
     {
         var count = list.Count;
@@ -577,7 +578,7 @@ public class PreLoader(Func<FileInfo, Task<ImageModel>> imageModelLoader) : IAsy
                 return;
             }
 
-            var keysToRemove = _preLoadList.Keys
+            var keysToRemove = _preLoadList.Keys.AsValueEnumerable() 
                 .OrderByDescending(k => Math.Abs(k - currentIndex))
                 .Take(_preLoadList.Count - PreLoaderConfig.MaxCount);
 

+ 33 - 34
src/PicView.Core/Titles/ImageTitleFormatter.cs

@@ -1,6 +1,6 @@
-using System.Diagnostics;
-using System.Runtime.CompilerServices;
+using System.Runtime.CompilerServices;
 using Cysharp.Text;
+using PicView.Core.DebugTools;
 using PicView.Core.Extensions;
 using PicView.Core.ImageDecoding;
 using PicView.Core.Localization;
@@ -18,8 +18,14 @@ public static class ImageTitleFormatter
     /// The name of the application.
     /// </summary>
     public const string AppName = "PicView";
+    
+    private const int MaxAspectRatioX = 48;
+    private const int MaxAspectRatioY = 18;
+    private const double NormalZoomLevel = 1.0;
+    private const double NoZoomLevel = 0.0;
 
-      /// <summary>
+
+    /// <summary>
     /// Generates the title strings based on the specified parameters, including image properties
     /// such as width, height, file name, zoom level, and current index in the file list.
     /// </summary>
@@ -30,7 +36,7 @@ public static class ImageTitleFormatter
     /// <param name="zoomValue">The current zoom level of the image.</param>
     /// <param name="filesList">The list of image file paths.</param>
     /// <returns>A <see cref="WindowTitles"/> struct containing the generated titles.</returns>
-    public static WindowTitles GenerateTitleStrings(int width, int height, int index, FileInfo? fileInfo, double zoomValue, List<string> filesList)
+    public static WindowTitles GenerateTitleStrings(int width, int height, int index, FileInfo? fileInfo, double zoomValue, List<FileInfo> filesList)
     {
         if (!TryValidateAndGetFileInfo(index, filesList, fileInfo, out var validatedFileInfo, out var errorTitle))
         {
@@ -52,12 +58,11 @@ public static class ImageTitleFormatter
     /// <param name="zoomValue">The current zoom level of the image.</param>
     /// <param name="filesList">The list of image file paths.</param>
     /// <returns>A <see cref="WindowTitles"/> struct containing the generated titles.</returns>
-    public static WindowTitles GenerateTiffTitleStrings(int width, int height, int index, FileInfo fileInfo, TiffManager.TiffNavigationInfo tiffNavigationInfo, double zoomValue, List<string> filesList)
+    public static WindowTitles GenerateTiffTitleStrings(int width, int height, int index, FileInfo fileInfo, TiffManager.TiffNavigationInfo tiffNavigationInfo, double zoomValue, List<FileInfo> filesList)
     {
         if (tiffNavigationInfo == null)
         {
-            return GenerateErrorTitle(
-                $"{nameof(ImageTitleFormatter)}:{nameof(GenerateTiffTitleStrings)} - TiffNavigationInfo is null");
+            return GenerateErrorTitle();
         }
 
         if (!TryValidateAndGetFileInfo(index, filesList, fileInfo, out var validatedFileInfo, out var errorTitle))
@@ -69,7 +74,7 @@ public static class ImageTitleFormatter
         return GenerateTitleStringsCore(width, height, validatedFileInfo, zoomValue, filesList, index, namePart);
     }
 
-    private static WindowTitles GenerateTitleStringsCore(int width, int height, FileInfo fileInfo, double zoomValue, List<string> filesList, int index, string namePart)
+    private static WindowTitles GenerateTitleStringsCore(int width, int height, FileInfo fileInfo, double zoomValue, List<FileInfo> filesList, int index, string namePart)
     {
         using var sb = ZString.CreateStringBuilder(true);
 
@@ -107,14 +112,14 @@ public static class ImageTitleFormatter
         };
     }
 
-    private static bool TryValidateAndGetFileInfo(int index, List<string> filesList, FileInfo? fileInfo, out FileInfo? validatedFileInfo, out WindowTitles errorTitle, [CallerMemberName] string callerName = "")
+    private static bool TryValidateAndGetFileInfo(int index, List<FileInfo> filesList, FileInfo? fileInfo, out FileInfo? validatedFileInfo, out WindowTitles errorTitle, [CallerMemberName] string callerName = "")
     {
         validatedFileInfo = null;
         errorTitle = default;
 
         if (index < 0 || index >= filesList.Count)
         {
-            errorTitle = GenerateErrorTitle($"{nameof(ImageTitleFormatter)}:{callerName} - index invalid");
+            DebugHelper.LogDebug(nameof(ImageTitleFormatter), callerName, "index invalid");
             return false;
         }
 
@@ -122,11 +127,11 @@ public static class ImageTitleFormatter
         {
             try
             {
-                validatedFileInfo = new FileInfo(filesList[index]);
+                validatedFileInfo = filesList[index];
             }
             catch (Exception e)
             {
-                errorTitle = GenerateErrorTitle($"{nameof(ImageTitleFormatter)}:{callerName} - FileInfo exception \n{e.Message}");
+                DebugHelper.LogDebug(nameof(ImageTitleFormatter), callerName, e);
                 return false;
             }
         }
@@ -146,7 +151,7 @@ public static class ImageTitleFormatter
             return true;
         }
 
-        errorTitle = GenerateErrorTitle($"{nameof(ImageTitleFormatter)}:{callerName} - FileInfo does not exist");
+        errorTitle = GenerateErrorTitle();
         return false;
     }
 
@@ -163,15 +168,9 @@ public static class ImageTitleFormatter
     /// <summary>
     /// Generates a set of error titles in case of invalid parameters or exceptions during title generation.
     /// </summary>
-    /// <param name="exception">A string representing the error message or exception details.</param>
     /// <returns>A <see cref="WindowTitles"/> struct containing error titles.</returns>
-    private static WindowTitles GenerateErrorTitle(string exception)
+    private static WindowTitles GenerateErrorTitle()
     {
-#if DEBUG
-        Trace.WriteLine(exception);
-        Debug.Assert(TranslationManager.Translation.UnexpectedError != null);
-#endif
-
         return new WindowTitles
         {
             BaseTitle = TranslationManager.Translation.UnexpectedError,
@@ -187,16 +186,16 @@ public static class ImageTitleFormatter
     /// <returns>A formatted string representing the zoom percentage, or null if the zoom is 0 or 100%.</returns>
     private static string? FormatZoomPercentage(double zoomValue)
     {
-        if (zoomValue is 0 or 1)
+        if (zoomValue is NoZoomLevel or NormalZoomLevel)
         {
             return null;
         }
 
-        var zoom = Math.Round(zoomValue * 100);
-
-        return zoom + "%";
+        var zoomPercentage = Math.Round(zoomValue * 100);
+        return $"{zoomPercentage}%";
     }
 
+
     /// <summary>
     /// Generates a window title for a single image, including its name, resolution, aspect ratio, and zoom level.
     /// </summary>
@@ -260,18 +259,18 @@ public static class ImageTitleFormatter
             return ") ";
         }
 
-        // Calculate the greatest common divisor
         var gcd = GCD(width, height);
-        var x = width / gcd;
-        var y = height / gcd;
+        var aspectX = width / gcd;
+        var aspectY = height / gcd;
 
-        // Check if aspect ratio is within specified limits
-        if (x > 48 || y > 18)
-        {
-            return ") ";
-        }
-
-        return $", {x}:{y}) ";
+        return IsAspectRatioWithinLimits(aspectX, aspectY) 
+            ? $", {aspectX}:{aspectY}) " 
+            : ") ";
+    }
+    
+    private static bool IsAspectRatioWithinLimits(int x, int y)
+    {
+        return x <= MaxAspectRatioX && y <= MaxAspectRatioY;
     }
 
     /// <summary>