Made improvements on loading photos
This commit is contained in:
+1
-4
@@ -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();
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Photos.Models;
|
||||
|
||||
public interface IFolderPickerService
|
||||
{
|
||||
Task<string?> PickFolderAsync();
|
||||
}
|
||||
+97
-8
@@ -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))
|
||||
{
|
||||
Source = new Bitmap(stream);
|
||||
}
|
||||
|
||||
private async Task<Bitmap?> LoadImageAsync(bool scaled = false)
|
||||
{
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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}"/>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user