Skip to content

Commit

Permalink
♻️ Migrate image open logic in the view model.
Browse files Browse the repository at this point in the history
  • Loading branch information
hexawyz committed Jan 19, 2025
1 parent 55b6b33 commit 5da9b52
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 53 deletions.
40 changes: 40 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
using Exo.Settings.Ui.ViewModels;
using Microsoft.UI;
using System.Runtime.InteropServices;
using WinRT.Interop;
using Windows.Storage.Pickers;
using Windows.Storage;
using System.Collections.Immutable;

namespace Exo.Settings.Ui;

Expand Down Expand Up @@ -88,12 +92,16 @@ private static IServiceProvider ConfigureServices()
{
var services = new ServiceCollection();

services.AddSingleton(sp => App.Current.MainWindow!);

services.AddSingleton<IEditionService, EditionService>();

services.AddSingleton<ConnectionViewModel>();

services.AddSingleton<ISettingsMetadataService, MetadataService>();

services.AddSingleton<IFileOpenDialog, FileOpenDialog>();

services.AddSingleton
(
sp => new SettingsServiceConnectionManager
Expand All @@ -113,4 +121,36 @@ private static IServiceProvider ConfigureServices()

return services.BuildServiceProvider();
}

private sealed class FileOpenDialog : IFileOpenDialog
{
private readonly Window _mainWindow;

public FileOpenDialog(Window mainWindow) => _mainWindow = mainWindow;

async Task<IPickedFile?> IFileOpenDialog.OpenAsync(ImmutableArray<string> extensions)
{
var fileOpenPicker = new FileOpenPicker();
foreach (var extension in extensions)
{
fileOpenPicker.FileTypeFilter.Add(extension);
}

InitializeWithWindow.Initialize(fileOpenPicker, WindowNative.GetWindowHandle(_mainWindow));
return await fileOpenPicker.PickSingleFileAsync() is { } file ?
new PickedFile(file) :
null;
}
}

private sealed class PickedFile : IPickedFile
{
private readonly StorageFile _file;

public PickedFile(StorageFile file)
=> _file = file;

public string? Path => _file.Path;
public Task<Stream> OpenForReadAsync() => _file.OpenStreamForReadAsync();
}
}
14 changes: 6 additions & 8 deletions src/Exo/Ui/Exo.Settings.Ui/ImagesPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
<Image Margin="{ThemeResource RowContentMargin}" Source="{Binding LoadedImageData, Converter={StaticResource SharedMemoryToBitmapImageConverter}}" />
<StackPanel Grid.Column="1" Orientation="Vertical" Margin="{ThemeResource RowContentMargin}">
<TextBox Text="{Binding LoadedImageName, Mode=TwoWay}" Header="Name" Width="250" IsEnabled="{Binding LoadedImageData, Converter={StaticResource NullabilityToBooleanConverter}}" />
<Button Margin="{ThemeResource RowLabelMargin}" Click="OnOpenButtonClick" Width="120">Open</Button>
<Button Margin="{ThemeResource RowLabelMargin}" Command="{Binding OpenImageCommand}" Width="120">Open</Button>
<Button Margin="{ThemeResource RowLabelMargin}" Command="{Binding AddImageCommand}" Width="120" Style="{ThemeResource AccentButtonStyle}">Add</Button>
</StackPanel>
</Grid>
Expand All @@ -37,17 +37,15 @@
<ItemContainer AutomationProperties.Name="{Binding Name}">
<Grid Margin="6" Height="200" Width="200" MaxWidth="200" MaxHeight="200">
<Image Source="{Binding FileName, Converter={StaticResource FileNameToBitmapImageConverter}}" Stretch="UniformToFill" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MaxWidth="200" MaxHeight="200" />
<StackPanel Orientation="Vertical" Height="40" VerticalAlignment="Bottom" Padding="5,1,5,1" Background="{ThemeResource SystemControlBackgroundBaseMediumBrush}" Opacity=".75">
<StackPanel Orientation="Vertical" Height="60" VerticalAlignment="Bottom" Padding="5,1,5,1" Background="{ThemeResource SystemControlBackgroundBaseMediumBrush}" Opacity=".75">
<TextBlock Text="{Binding Name}" TextTrimming="CharacterEllipsis" Foreground="{ThemeResource SystemControlForegroundAltHighBrush}"/>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Size: " />
<TextBlock Text="{Binding Width}" />
<TextBlock Text="x" />
<TextBlock Text="{Binding Height}" />
<TextBlock Text="{Binding Width}" Foreground="{ThemeResource SystemControlForegroundAltHighBrush}" />
<TextBlock Text="x" Foreground="{ThemeResource SystemControlForegroundAltHighBrush}" />
<TextBlock Text="{Binding Height}" Foreground="{ThemeResource SystemControlForegroundAltHighBrush}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Format: " />
<TextBlock Text="{Binding Format}" />
<TextBlock Text="{Binding Format}" Foreground="{ThemeResource SystemControlForegroundAltHighBrush}" />
</StackPanel>
</StackPanel>
</Grid>
Expand Down
39 changes: 0 additions & 39 deletions src/Exo/Ui/Exo.Settings.Ui/ImagesPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,43 +17,4 @@ 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;
SharedMemory? data;
using (var stream = await file.OpenStreamForReadAsync())
{
long length = stream.Length;
if (length <= 0)
{
data = null;
}
else
{
data = SharedMemory.Create("Exo_Image_", (ulong)length);
using (var viewStream = data.CreateWriteStream())
{
await stream.CopyToAsync(viewStream);
}
}
}

if (data is not null)
{
ViewModel.Images.SetImage(Path.GetFileNameWithoutExtension(file.Path), data);
}
else
{
ViewModel.Images.ClearImage();
}
}
}
8 changes: 8 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/Services/IFileOpenDialog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using System.Collections.Immutable;

namespace Exo.Settings.Ui.Services;

public interface IFileOpenDialog
{
Task<IPickedFile?> OpenAsync(ImmutableArray<string> extensions);
}
7 changes: 7 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/Services/IPickedFile.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace Exo.Settings.Ui.Services;

public interface IPickedFile
{
string? Path { get; }
Task<Stream> OpenForReadAsync();
}
96 changes: 92 additions & 4 deletions src/Exo/Ui/Exo.Settings.Ui/ViewModels/ImagesViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Buffers;
using System.Collections.ObjectModel;
using System.Security.Cryptography;
using System.Windows.Input;
using Exo.Contracts.Ui.Settings;
using Exo.Memory;
Expand All @@ -12,6 +13,30 @@ internal sealed class ImagesViewModel : BindableObject, IConnectedState, IDispos
{
private static class Commands
{
public sealed class OpenImageCommand : ICommand
{
private readonly ImagesViewModel _viewModel;

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

public event EventHandler? CanExecuteChanged;

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

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

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

public sealed class AddImageCommand : ICommand
{
private readonly ImagesViewModel _viewModel;
Expand Down Expand Up @@ -43,21 +68,26 @@ public async void Execute(object? parameter)

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

private bool _isReady;
private string? _loadedImageName;
private SharedMemory? _loadedImageData;

private readonly SettingsServiceConnectionManager _connectionManager;
private readonly IFileOpenDialog _fileOpenDialog;
private IImageService? _imageService;
private CancellationTokenSource? _cancellationTokenSource;
private readonly IDisposable _stateRegistration;

public ImagesViewModel(SettingsServiceConnectionManager connectionManager)
public ImagesViewModel(SettingsServiceConnectionManager connectionManager, IFileOpenDialog fileOpenDialog)
{
_images = new();
_readOnlyImages = new(_images);
_connectionManager = connectionManager;
_fileOpenDialog = fileOpenDialog;
_openImageCommand = new(this);
_addImageCommand = new(this);
_cancellationTokenSource = new();
_stateRegistration = connectionManager.RegisterStateAsync(this).GetAwaiter().GetResult();
Expand All @@ -71,6 +101,7 @@ public void Dispose()
}

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

async Task IConnectedState.RunAsync(CancellationToken cancellationToken)
Expand All @@ -80,12 +111,15 @@ async Task IConnectedState.RunAsync(CancellationToken cancellationToken)
{
var imageService = await _connectionManager.GetImageServiceAsync(cts2.Token);
_imageService = imageService;
IsNotBusy = true;
await WatchImagesAsync(imageService, cts2.Token);
}
}

void IConnectedState.Reset()
{
IsNotBusy = false;
ClearImage();
_imageService = null;
_images.Clear();
}
Expand Down Expand Up @@ -116,6 +150,20 @@ private async Task WatchImagesAsync(IImageService imageService, CancellationToke
}
}

public bool IsNotBusy
{
get => _isReady;
private set
{
bool couldAddImage = CanAddImage;
if (SetValue(ref _isReady, value, ChangedProperty.IsNotBusy))
{
_openImageCommand.NotifyCanExecuteChanged();
if (couldAddImage != CanAddImage) _addImageCommand.NotifyCanExecuteChanged();
}
}
}

public string? LoadedImageName
{
get => _loadedImageName;
Expand Down Expand Up @@ -148,19 +196,59 @@ private set
}
}

public void SetImage(string name, SharedMemory data)
private void SetImage(string name, SharedMemory data)
{
LoadedImageName = name;
LoadedImageData = data;
}

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

private bool CanAddImage => _loadedImageName is not null && IsNameValid(_loadedImageName) && _loadedImageData is not null;
private bool CanAddImage => _isReady && _loadedImageName is not null && IsNameValid(_loadedImageName) && _loadedImageData is not null;

private async Task OpenImageAsync(CancellationToken cancellationToken)
{
var file = await _fileOpenDialog.OpenAsync([".bmp", ".gif", ".png", ".jpg", ".webp",]);

if (file is null) return;
SharedMemory? data;
using (var stream = await file.OpenForReadAsync())
{
long length = stream.Length;
if (length <= 0)
{
data = null;
}
else
{
data = SharedMemory.Create("Exo_Image_", (ulong)length);
using (var viewStream = data.CreateWriteStream())
{
await stream.CopyToAsync(viewStream);
}
}
}

if (data is not null)
{
string? name = null;
if (file.Path is { Length: > 0 } path)
{
name = Path.GetFileNameWithoutExtension(path);
if (!IsNameValid(name)) name = null;
}
if (name is null) name = "img_" + RandomNumberGenerator.GetHexString(8);
SetImage(name, data);
}
else
{
ClearImage();
}
}

private async Task AddImageAsync(CancellationToken cancellationToken)
{
Expand Down
11 changes: 9 additions & 2 deletions src/Exo/Ui/Exo.Settings.Ui/ViewModels/SettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,14 @@ public PageViewModel? SelectedNavigationPage

public ISettingsMetadataService MetadataService => _metadataService;

public SettingsViewModel(SettingsServiceConnectionManager connectionManager, ConnectionViewModel connectionViewModel, IEditionService editionService, ISettingsMetadataService metadataService)
public SettingsViewModel
(
SettingsServiceConnectionManager connectionManager,
ConnectionViewModel connectionViewModel,
IEditionService editionService,
IFileOpenDialog fileOpenDialog,
ISettingsMetadataService metadataService
)
{
ConnectionManager = connectionManager;
_connectionViewModel = connectionViewModel;
Expand All @@ -105,7 +112,7 @@ public SettingsViewModel(SettingsServiceConnectionManager connectionManager, Con
_devicesViewModel = new(ConnectionManager, _metadataService, _navigateCommand);
_batteryDevicesViewModel = new(_devicesViewModel);
_lightingViewModel = new(ConnectionManager, _devicesViewModel, _metadataService);
_imagesViewModel = new(ConnectionManager);
_imagesViewModel = new(ConnectionManager, fileOpenDialog);
_sensorsViewModel = new(ConnectionManager, _devicesViewModel, _metadataService);
_coolingViewModel = new(ConnectionManager, _devicesViewModel, _sensorsViewModel, _metadataService);
_programmingViewModel = new(ConnectionManager);
Expand Down

0 comments on commit 5da9b52

Please sign in to comment.