Made improvements on loading photos

This commit is contained in:
Ano-sys
2026-03-21 14:40:30 +01:00
parent 7f632531fd
commit 54268fc0b4
8 changed files with 189 additions and 53 deletions
+1 -4
View File
@@ -23,10 +23,7 @@ public partial class App : Application
// Avoid duplicate validations from both Avalonia and the CommunityToolkit.
// More info: https://docs.avaloniaui.net/docs/guides/development-guides/data-validation#manage-validationplugins
DisableAvaloniaDataAnnotationValidation();
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
desktop.MainWindow = new MainWindow();
}
base.OnFrameworkInitializationCompleted();
+8
View File
@@ -0,0 +1,8 @@
using System.Threading.Tasks;
namespace Photos.Models;
public interface IFolderPickerService
{
Task<string?> PickFolderAsync();
}
+96 -7
View File
@@ -1,27 +1,116 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform.Storage;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Photos.Models;
public class Photo
public partial class Photo : ObservableObject, IDisposable
{
public string DisplayName { get; set; }
public string FullPath { get; set; }
public string DisplayPath { get; set; }
[ObservableProperty] private string _displayName;
[ObservableProperty] private string _fullPath;
[ObservableProperty] private string _displayPath;
public IImage Source { get; set; }
[ObservableProperty] private Bitmap? _previewImage;
[ObservableProperty] private Bitmap? _fullImage;
public Photo(string path, string relativeTo)
{
DisplayName = System.IO.Path.GetFileName(path);
FullPath = path;
DisplayPath = path.Remove(0, relativeTo.Length + 1);
using (var stream = File.OpenRead(path))
}
private async Task<Bitmap?> LoadImageAsync(bool scaled = false)
{
Source = new Bitmap(stream);
var image = await Task.Run(() =>
{
Bitmap? i = null;
using (var stream = File.OpenRead(FullPath))
{
i = scaled ? Bitmap.DecodeToWidth(stream, 200) : new Bitmap(stream);
}
return i;
});
return image;
}
public async Task LoadPreviewImage()
{
var image = await LoadImageAsync(true);
Dispatcher.UIThread.Post(() =>
{
PreviewImage?.Dispose();
PreviewImage = image;
});
}
public async Task LoadFullImage()
{
var image = await LoadImageAsync();
Dispatcher.UIThread.Post(() =>
{
FullImage?.Dispose();
FullImage = image;
});
}
public void Dispose()
{
FullImage?.Dispose();
PreviewImage?.Dispose();
GC.SuppressFinalize(this);
}
}
public static partial class PhotoExtension
{
private static readonly string[] SupportedFileExtensions = [".png", ".jpg", ".jpeg"];
public static List<string>? GetPhotoPathsFromDirectory(string path)
{
if (!Directory.Exists(path)) return null;
var files = Directory.EnumerateFiles(path);
return files.Where(f => SupportedFileExtensions.Contains(Path.GetExtension(f), StringComparer.OrdinalIgnoreCase)).ToList();
}
public static List<Photo>? GetPhotosFromDirectory(string path)
{
if (!Directory.Exists(path)) return null;
var files = Directory.EnumerateFiles(path);
return files.Where(f => SupportedFileExtensions.Contains(Path.GetExtension(f), StringComparer.OrdinalIgnoreCase))
.Select(x => new Photo(x, path))
.ToList();
}
public static void CreateAndAddPhotosToList(List<string> photos, string rootPath, IList<Photo> toAddTo)
{
var bag = new ConcurrentBag<Photo>();
Parallel.ForEach(photos, p =>
{
bag.Add(new Photo(p, rootPath));
});
Dispatcher.UIThread.InvokeAsync(() =>
{
foreach (var photo in bag)
{
toAddTo.Add(photo);
}
}).GetAwaiter().GetResult(); // TODO: When reopening folder this throws exception
}
}
+1
View File
@@ -13,6 +13,7 @@
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.3.10"/>
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.1.5" />
<PackageReference Include="Avalonia.Desktop" Version="11.3.10"/>
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.3.10"/>
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.3.10"/>
+23 -27
View File
@@ -17,20 +17,28 @@ namespace Photos.ViewModels;
public partial class MainWindowViewModel : ViewModelBase
{
public ObservableCollection<Photo> Photos { get; private set; } = [];
private IFolderPickerService _folderPickerService;
[ObservableProperty] private ObservableCollection<Photo> _photos = [];
[ObservableProperty] private Photo? _selectedPhoto;
public string LibraryPath { get; private set; } = string.Empty;
public MainWindowViewModel()
public MainWindowViewModel(IFolderPickerService folderPickerService)
{
_folderPickerService = folderPickerService;
}
void OpenDirectory()
public async Task OpenDirectory()
{
var folder = await _folderPickerService.PickFolderAsync();
if (folder is null) return;
foreach(var p in Photos) p.Dispose();
LibraryPath = folder;
await AddPhotosAsync(folder);
}
private void SanitizeFiles(List<string> files, IEnumerable<string> extensions)
@@ -40,27 +48,11 @@ public partial class MainWindowViewModel : ViewModelBase
{
try
{
var files = await Task.Run(() =>
{
var files = Directory.GetFiles(path, "*", SearchOption.AllDirectories).ToList();
SanitizeFiles(files, [".png", ".jpg", ".jpeg"]);
return files;
});
var photoPaths = PhotoExtension.GetPhotoPathsFromDirectory(path);
if (photoPaths is null) return;
var photosToAdd = await Task.Run(() =>
{
var photos = new List<Photo>();
foreach (var file in files)
{
photos.Add(new Photo(file, LibraryPath));
}
return photos;
});
await Dispatcher.UIThread.InvokeAsync(() =>
{
foreach (var photo in photosToAdd) Photos.Add(photo);
});
await Task.Run(() => PhotoExtension.CreateAndAddPhotosToList(photoPaths, path, Photos));
foreach (var p in Photos) _ = p.LoadPreviewImage();
}
catch (Exception ex)
{
@@ -74,7 +66,7 @@ public partial class MainWindowViewModel : ViewModelBase
if (files is null || files.Length == 0) return;
var localPath = Path.GetDirectoryName(files[0].TryGetLocalPath());
if (String.IsNullOrWhiteSpace(localPath)) return;
if (string.IsNullOrWhiteSpace(localPath)) return;
LibraryPath = localPath;
Photos.Clear();
@@ -82,14 +74,18 @@ public partial class MainWindowViewModel : ViewModelBase
await AddPhotosAsync(localPath);
}
public void PhotoClicked(PointerReleasedEventArgs e)
public async Task PhotoClicked(PointerReleasedEventArgs e)
{
if(e.Source is not Image img) return;
SelectedPhoto = Photos.FirstOrDefault(x => x.Source == img.Source);
SelectedPhoto = Photos.FirstOrDefault(x => x.PreviewImage == img.Source);
if (SelectedPhoto is null) return;
await SelectedPhoto.LoadFullImage();
}
public void SelectedPhotoClicked()
{
if (SelectedPhoto is null) return;
SelectedPhoto.FullImage = null;
SelectedPhoto = null;
}
}
+31
View File
@@ -0,0 +1,31 @@
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using Microsoft.VisualBasic.FileIO;
using Photos.Models;
namespace Photos.Views;
public class FolderPickerService(Window window) : IFolderPickerService
{
private Window _window = window;
public void SetDependencyWindow(Window window) => _window = window;
public async Task<string?> PickFolderAsync()
{
var suggestedFolder = await _window.StorageProvider.TryGetWellKnownFolderAsync(WellKnownFolder.Pictures);
var folders = await _window.StorageProvider.OpenFolderPickerAsync(
new FolderPickerOpenOptions
{
Title = "Select Folder",
AllowMultiple = false,
SuggestedStartLocation = suggestedFolder
}
);
return folders.FirstOrDefault()?.Path.LocalPath;
}
}
+23 -13
View File
@@ -14,6 +14,10 @@
Title="Photos"
DragDrop.AllowDrop="True"
DragDrop.Drop="DropHandler"
ExtendClientAreaChromeHints="NoChrome"
TransparencyLevelHint="AcrylicBlur"
Background="Transparent"
ExtendClientAreaToDecorationsHint="True"
>
<Design.DataContext>
@@ -27,30 +31,36 @@
<Button Content="Open Directory" Command="{Binding OpenDirectory}"/>
</Menu>
<ScrollViewer Grid.Column="1">
<ItemsControl ItemsSource="{Binding Photos}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel ScrollViewer.VerticalScrollBarVisibility="Auto"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<ItemsRepeater ItemsSource="{Binding Photos}" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ItemsRepeater.Layout>
<UniformGridLayout MinItemWidth="190"
MinItemHeight="240"
MinRowSpacing="12"
MinColumnSpacing="12" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="models:Photo">
<StackPanel Orientation="Vertical" Margin="20">
<StackPanel Orientation="Vertical" Margin="12">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="PointerReleased">
<ic:InvokeCommandAction Command="{Binding DataContext.PhotoClicked, ElementName=Root}" PassEventArgsToCommand="True"/>
</ic:EventTriggerBehavior>
</i:Interaction.Behaviors>
<Image Source="{Binding Source}" Width="150" Height="150"/>
<TextBlock Text="{Binding DisplayName}" HorizontalAlignment="Center" Width="150"/>
<Image Source="{Binding PreviewImage}"
Width="150"
Height="150" />
<TextBlock Text="{Binding DisplayName}"
HorizontalAlignment="Center"
Width="150" />
<TextBlock Text="{Binding DisplayPath}" HorizontalAlignment="Center" Width="150"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
<Grid Grid.Column="0" Grid.ColumnSpan="2" IsEnabled="{Binding SelectedPhoto}">
<Image Source="{Binding SelectedPhoto.Source}" Margin="50">
<Image Source="{Binding SelectedPhoto.FullImage}" Margin="50">
<i:Interaction.Behaviors>
<ic:EventTriggerBehavior EventName="PointerReleased">
<ic:InvokeCommandAction Command="{Binding Path=SelectedPhotoClicked}"/>
+5 -1
View File
@@ -1,16 +1,20 @@
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Xaml.Interactions.DragAndDrop;
using Photos.Models;
using Photos.ViewModels;
namespace Photos.Views;
public partial class MainWindow : Window
{
private IFolderPickerService _folderPickerService;
public MainWindow()
{
InitializeComponent();
DataContext = new MainWindowViewModel();
_folderPickerService = new FolderPickerService(this);
DataContext = new MainWindowViewModel(_folderPickerService);
}
public void DropHandler(object sender, DragEventArgs e)