Skip to content

Commit

Permalink
🎨 Add basic image selection UI for the image library.
Browse files Browse the repository at this point in the history
No backend service yet. This is intended to manage the image library imported in the backend.
  • Loading branch information
hexawyz committed Jan 16, 2025
1 parent 13f5e6d commit 0a843b4
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 15 deletions.
2 changes: 2 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/App.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@
<lconverters:TimeSpanToSecondsConverter x:Key="TimeSpanToSecondsConverter" />
<lconverters:SecondsToStringConverter x:Key="SecondsToStringConverter" />
<lconverters:GridLengthConverter x:Key="GridLengthConverter" />
<lconverters:ByteArrayToBitmapImageConverter x:Key="ByteArrayToBitmapImageConverter" />
<lconverters:FileNameToBitmapImageConverter x:Key="FileNameToBitmapImageConverter" />

<local:RgbLightingDefaultPalette x:Key="RgbLightingDefaultPalette" />

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media.Imaging;

namespace Exo.Settings.Ui.Converters;

internal sealed class ByteArrayToBitmapImageConverter : IValueConverter
{
public object? Convert(object value, Type targetType, object parameter, string language)
{
if (value is not byte[] data) return null;

var bitmapImage = new BitmapImage();
LoadImage(bitmapImage, data);
return bitmapImage;
}

private async void LoadImage(BitmapImage bitmapImage, byte[] data)
{
try
{
using (var memoryStream = new MemoryStream(data, 0, data.Length, true, true))
using (var randomAccessStream = memoryStream.AsRandomAccessStream())
{
await bitmapImage.SetSourceAsync(randomAccessStream);
}
}
catch
{
}
}

public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Storage.Streams;

namespace Exo.Settings.Ui.Converters;

internal sealed class FileNameToBitmapImageConverter : IValueConverter
{
public object? Convert(object value, Type targetType, object parameter, string language)
{
if (value is not string fileName) return null;

var bitmapImage = new BitmapImage();
LoadImage(bitmapImage, fileName);
return bitmapImage;
}

private async void LoadImage(BitmapImage bitmapImage, string fileName)
{
try
{
using (var randomAccessStream = await FileRandomAccessStream.OpenAsync(fileName, Windows.Storage.FileAccessMode.Read, Windows.Storage.StorageOpenOptions.AllowOnlyReaders, FileOpenDisposition.OpenExisting))
{
await bitmapImage.SetSourceAsync(randomAccessStream);
}
}
catch
{
}
}

public object ConvertBack(object value, Type targetType, object parameter, string language) => throw new NotImplementedException();
}
54 changes: 41 additions & 13 deletions src/Exo/Ui/Exo.Settings.Ui/ImagesPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,46 @@
mc:Ignorable="d"
d:DataContext="{d:DesignInstance vm:SettingsViewModel, IsDesignTimeCreatable=False}">

<Grid>
<ItemsView ItemsSource="{Binding Images.Images}">
<ItemsView.ItemTemplate>
<DataTemplate x:DataType="vm:ImageViewModel">
<ItemContainer AutomationProperties.Name="{Binding Name}">
<TextBox Text="{Binding Name}" />
</ItemContainer>
</DataTemplate>
</ItemsView.ItemTemplate>
<ItemsView.Layout>
<UniformGridLayout />
</ItemsView.Layout>
</ItemsView>
<Grid DataContext="{Binding Images}">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" MaxHeight="200" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Margin="{ThemeResource RowLabelMargin}" Padding="6" HorizontalAlignment="Stretch" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="{ThemeResource ToolTipBorderThemeThickness}" CornerRadius="{ThemeResource OverlayCornerRadius}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MaxWidth="200" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image Margin="{ThemeResource RowContentMargin}" Source="{Binding LoadedImageData, Converter={StaticResource ByteArrayToBitmapImageConverter}}" />
<StackPanel Grid.Column="1" Orientation="Vertical" Margin="{ThemeResource RowContentMargin}">
<Button Margin="{ThemeResource RowLabelMargin}" Click="OnOpenButtonClick" Width="120">Open</Button>
<Button Margin="{ThemeResource RowLabelMargin}" Command="{Binding AddImageCommand}" Width="120" Style="{ThemeResource AccentButtonStyle}">Add</Button>
</StackPanel>
</Grid>
</Border>
<Border Grid.Row="1" Padding="6" HorizontalAlignment="Stretch" Margin="{ThemeResource RowLabelMargin}" Background="{ThemeResource CardBackgroundFillColorDefaultBrush}" BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}" BorderThickness="{ThemeResource ToolTipBorderThemeThickness}" CornerRadius="{ThemeResource OverlayCornerRadius}">
<ItemsView ItemsSource="{Binding Images}" MinHeight="250">
<ItemsView.ItemTemplate>
<DataTemplate x:DataType="vm:ImageViewModel">
<ItemContainer AutomationProperties.Name="{Binding Name}">
<Grid Margin="6" MinHeight="200">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" MaxWidth="200" />
<ColumnDefinition Width="Auto" MinWidth="100" />
</Grid.ColumnDefinitions>
<Image Stretch="UniformToFill" Source="{Binding FileName, Converter={StaticResource FileNameToBitmapImageConverter}}" />
<StackPanel Grid.Column="1" Margin="{ThemeResource RowContentMargin}">
<TextBox Text="{Binding Name}" Header="Name" Width="150" />
</StackPanel>
</Grid>
</ItemContainer>
</DataTemplate>
</ItemsView.ItemTemplate>
<ItemsView.Layout>
<UniformGridLayout />
</ItemsView.Layout>
</ItemsView>
</Border>
</Grid>
</Page>
31 changes: 31 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/ImagesPage.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using Exo.Settings.Ui.ViewModels;
using Microsoft.UI.Xaml.Controls;
using Windows.Storage.Pickers;
using WinRT.Interop;

namespace Exo.Settings.Ui;

Expand All @@ -14,4 +16,33 @@ public ImagesPage()
}

private SettingsViewModel ViewModel => (SettingsViewModel)DataContext;

private async void OnOpenButtonClick(object sender, Microsoft.UI.Xaml.RoutedEventArgs e)
{
var fileOpenPicker = new FileOpenPicker();
fileOpenPicker.FileTypeFilter.Add(".bmp");
fileOpenPicker.FileTypeFilter.Add(".gif");
fileOpenPicker.FileTypeFilter.Add(".png");
fileOpenPicker.FileTypeFilter.Add(".jpg");

InitializeWithWindow.Initialize(fileOpenPicker, WindowNative.GetWindowHandle(App.Current.MainWindow));
var file = await fileOpenPicker.PickSingleFileAsync();
if (file is null) return;
byte[]? data = null;
using (var stream = await file.OpenStreamForReadAsync())
{
data = new byte[stream.Length];
await stream.ReadExactlyAsync(data);
}

if (data is not null)
{
ViewModel.Images.SetImage(Path.GetFileNameWithoutExtension(file.Path), data);
ViewModel.Images.TestAddImageToList(file.Path);
}
else
{
ViewModel.Images.ClearImage();
}
}
}
77 changes: 75 additions & 2 deletions src/Exo/Ui/Exo.Settings.Ui/ViewModels/ImagesViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,104 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using Exo.Ui;
using Microsoft.UI.Xaml.Media;

namespace Exo.Settings.Ui.ViewModels;

internal sealed class ImagesViewModel : BindableObject
{
private static class Commands
{
public sealed class AddImageCommand : ICommand
{
private readonly ImagesViewModel _viewModel;

public AddImageCommand(ImagesViewModel viewModel) => _viewModel = viewModel;

public event EventHandler? CanExecuteChanged;

public bool CanExecute(object? parameter) => _viewModel.CanAddImage;

public async void Execute(object? parameter)
{
try
{
await _viewModel.AddImageAsync(default);
}
catch
{
}
}

public void NotifyCanExecuteChanged() => CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}

private readonly ObservableCollection<ImageViewModel> _images;
private readonly ReadOnlyObservableCollection<ImageViewModel> _readOnlyImages;
private readonly Commands.AddImageCommand _addImageCommand;

private string? _loadedImageName;
private byte[]? _loadedImageData;

public ImagesViewModel()
{
_images = new();
_readOnlyImages = new(_images);
_addImageCommand = new(this);
}

public ReadOnlyObservableCollection<ImageViewModel> Images => _readOnlyImages;
public ICommand AddImageCommand => _addImageCommand;

public string? LoadedImageName
{
get => _loadedImageName;
private set => SetValue(ref _loadedImageName, value);
}

public byte[]? LoadedImageData
{
get => _loadedImageData;
private set => SetValue(ref _loadedImageData, value);
}

public void SetImage(string name, byte[] data)
{
LoadedImageName = name;
LoadedImageData = data;
}

public void ClearImage()
{
LoadedImageName = null;
LoadedImageData = null;
}

protected void UploadImage() { }
// TODO: Remove. This is for testing the UI without backend.
public void TestAddImageToList(string fileName)
{
_images.Add(new(Path.GetFileNameWithoutExtension(fileName), fileName));
}

private bool CanAddImage => _loadedImageData is not null;

private async Task AddImageAsync(CancellationToken cancellationToken)
{
}
}

internal sealed partial class ImageViewModel : ApplicableResettableBindableObject
{
private string _name;
private readonly string _fileName;

public override bool IsChanged => false;

public ImageViewModel(string name)
public ImageViewModel(string name, string fileName)
{
_name = name;
_fileName = fileName;
}

public string Name
Expand All @@ -36,6 +107,8 @@ public string Name
set => SetValue(ref _name, value);
}

public string FileName => _fileName;

protected override Task ApplyChangesAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
protected override void Reset() => throw new NotImplementedException();

Expand Down

0 comments on commit 0a843b4

Please sign in to comment.