Skip to content

Commit

Permalink
🎨 Plug the image service in the UI.
Browse files Browse the repository at this point in the history
⚠️ It doesn't work. Will switch to memory mapped files for loading images and adding them to the service, as seding bytes over the pipe triggers GRPC message size limit.
(Increasing the limit is possible but unreasonable)
  • Loading branch information
hexawyz committed Jan 18, 2025
1 parent 9a2ec9c commit 8216603
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 20 deletions.
2 changes: 2 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/ChangedProperty.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,4 +63,6 @@ internal static class ChangedProperty
public static readonly PropertyChangedEventArgs HasLowPowerBatteryThreshold = new (nameof(HasLowPowerBatteryThreshold));
public static readonly PropertyChangedEventArgs HasIdleTimer = new (nameof(HasIdleTimer));
public static readonly PropertyChangedEventArgs HasWirelessBrightness = new (nameof(HasWirelessBrightness));
public static readonly PropertyChangedEventArgs LoadedImageName = new (nameof(LoadedImageName));
public static readonly PropertyChangedEventArgs LoadedImageData = new (nameof(LoadedImageData));
}
11 changes: 11 additions & 0 deletions src/Exo/Ui/Exo.Settings.Ui/ImagesPage.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
</Grid.ColumnDefinitions>
<Image Margin="{ThemeResource RowContentMargin}" Source="{Binding LoadedImageData, Converter={StaticResource ByteArrayToBitmapImageConverter}}" />
<StackPanel Grid.Column="1" Orientation="Vertical" Margin="{ThemeResource RowContentMargin}">
<TextBox Text="{Binding LoadedImageName}" 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 AddImageCommand}" Width="120" Style="{ThemeResource AccentButtonStyle}">Add</Button>
</StackPanel>
Expand All @@ -38,6 +39,16 @@
<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">
<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}" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Format: " />
<TextBlock Text="{Binding Format}" />
</StackPanel>
</StackPanel>
</Grid>
</ItemContainer>
Expand Down
1 change: 0 additions & 1 deletion src/Exo/Ui/Exo.Settings.Ui/ImagesPage.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ private async void OnOpenButtonClick(object sender, Microsoft.UI.Xaml.RoutedEven
if (data is not null)
{
ViewModel.Images.SetImage(Path.GetFileNameWithoutExtension(file.Path), data);
ViewModel.Images.TestAddImageToList(file.Path);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,7 @@ private static object Box(ConnectionStatus connectionStatus)
private TaskCompletionSource<IMouseService> _mouseServiceTaskCompletionSource;
private TaskCompletionSource<IMonitorService> _monitorServiceTaskCompletionSource;
private TaskCompletionSource<ILightingService> _lightingServiceTaskCompletionSource;
private TaskCompletionSource<IImageService> _imageServiceTaskCompletionSource;
private TaskCompletionSource<ISensorService> _sensorServiceTaskCompletionSource;
private TaskCompletionSource<ICoolingService> _coolingServiceTaskCompletionSource;
private TaskCompletionSource<IProgrammingService> _programmingServiceTaskCompletionSource;
Expand All @@ -175,6 +176,7 @@ public SettingsServiceConnectionManager(string pipeName, int reconnectDelay, str
_mouseServiceTaskCompletionSource = new();
_monitorServiceTaskCompletionSource = new();
_lightingServiceTaskCompletionSource = new();
_imageServiceTaskCompletionSource = new();
_sensorServiceTaskCompletionSource = new();
_coolingServiceTaskCompletionSource = new();
_customMenuServiceTaskCompletionSource = new();
Expand All @@ -199,6 +201,9 @@ public Task<IMonitorService> GetMonitorServiceAsync(CancellationToken cancellati
public Task<ILightingService> GetLightingServiceAsync(CancellationToken cancellationToken)
=> _lightingServiceTaskCompletionSource.Task.WaitAsync(cancellationToken);

public Task<IImageService> GetImageServiceAsync(CancellationToken cancellationToken)
=> _imageServiceTaskCompletionSource.Task.WaitAsync(cancellationToken);

public Task<ISensorService> GetSensorServiceAsync(CancellationToken cancellationToken)
=> _sensorServiceTaskCompletionSource.Task.WaitAsync(cancellationToken);

Expand Down Expand Up @@ -246,6 +251,7 @@ protected override async Task OnConnectedAsync(GrpcChannel channel, Cancellation
Connect(channel, _mouseServiceTaskCompletionSource);
Connect(channel, _monitorServiceTaskCompletionSource);
Connect(channel, _lightingServiceTaskCompletionSource);
Connect(channel, _imageServiceTaskCompletionSource);
Connect(channel, _sensorServiceTaskCompletionSource);
Connect(channel, _coolingServiceTaskCompletionSource);
Connect(channel, _customMenuServiceTaskCompletionSource);
Expand All @@ -270,6 +276,7 @@ protected override async Task OnDisconnectedAsync()
Reset(ref _mouseServiceTaskCompletionSource);
Reset(ref _monitorServiceTaskCompletionSource);
Reset(ref _lightingServiceTaskCompletionSource);
Reset(ref _imageServiceTaskCompletionSource);
Reset(ref _sensorServiceTaskCompletionSource);
Reset(ref _coolingServiceTaskCompletionSource);
Reset(ref _customMenuServiceTaskCompletionSource);
Expand Down
102 changes: 88 additions & 14 deletions src/Exo/Ui/Exo.Settings.Ui/ViewModels/ImagesViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
using System.Collections.ObjectModel;
using System.Windows.Input;
using Exo.Contracts.Ui.Settings;
using Exo.Settings.Ui.Services;
using Exo.Ui;
using Microsoft.UI.Xaml.Media;

namespace Exo.Settings.Ui.ViewModels;

internal sealed class ImagesViewModel : BindableObject
internal sealed class ImagesViewModel : BindableObject, IConnectedState, IDisposable
{
private static class Commands
{
Expand Down Expand Up @@ -41,32 +43,91 @@ public async void Execute(object? parameter)
private string? _loadedImageName;
private byte[]? _loadedImageData;

public ImagesViewModel()
private readonly SettingsServiceConnectionManager _connectionManager;
private IImageService? _imageService;
private CancellationTokenSource? _cancellationTokenSource;
private readonly IDisposable _stateRegistration;

public ImagesViewModel(SettingsServiceConnectionManager connectionManager)
{
_images = new();
_readOnlyImages = new(_images);
_connectionManager = connectionManager;
_addImageCommand = new(this);
_cancellationTokenSource = new();
_stateRegistration = connectionManager.RegisterStateAsync(this).GetAwaiter().GetResult();
}

public void Dispose()
{
if (Interlocked.Exchange(ref _cancellationTokenSource, null) is not { } cts) return;
cts.Cancel();
_stateRegistration.Dispose();
}

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

async Task IConnectedState.RunAsync(CancellationToken cancellationToken)
{
if (_cancellationTokenSource is not { } cts || cts.IsCancellationRequested) return;
using (var cts2 = CancellationTokenSource.CreateLinkedTokenSource(_cancellationTokenSource.Token, cancellationToken))
{
var imageService = await _connectionManager.GetImageServiceAsync(cts2.Token);
_imageService = imageService;
await WatchImagesAsync(imageService, cts2.Token);
}
}

void IConnectedState.Reset()
{
_imageService = null;
_images.Clear();
}

// ⚠️ We want the code of this async method to always be synchronized to the UI thread. No ConfigureAwait here.
private async Task WatchImagesAsync(IImageService imageService, CancellationToken cancellationToken)
{
await foreach (var notification in imageService.WatchImagesAsync(cancellationToken))
{
switch (notification.NotificationKind)
{
case Contracts.Ui.WatchNotificationKind.Enumeration:
case Contracts.Ui.WatchNotificationKind.Addition:
_images.Add(new ImageViewModel(notification.Details));
break;
case Contracts.Ui.WatchNotificationKind.Removal:
int i;
for (i = 0; i < _images.Count; i++)
{
if (string.Equals(notification.Details.ImageName, _images[i].Name, StringComparison.OrdinalIgnoreCase))
{
_images.RemoveAt(i);
break;
}
}
break;
}
}
}

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

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

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

public void ClearImage()
Expand All @@ -75,30 +136,39 @@ public void ClearImage()
LoadedImageData = null;
}

// 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 bool CanAddImage => _loadedImageName is { Length: > 0 } && _loadedImageData is not null;

private async Task AddImageAsync(CancellationToken cancellationToken)
{
if (_imageService is null || _loadedImageName is null || _loadedImageData is null) return;

await _imageService.AddImageAsync(new() { ImageName = _loadedImageName, Data = _loadedImageData }, cancellationToken);
_loadedImageName = null;
_loadedImageData = null;
NotifyPropertyChanged(ChangedProperty.LoadedImageName);
NotifyPropertyChanged(ChangedProperty.LoadedImageData);
}
}

internal sealed partial class ImageViewModel : ApplicableResettableBindableObject
{
private string _name;
private readonly string _fileName;
private readonly ushort _width;
private readonly ushort _height;
private readonly ImageFormat _format;
private readonly bool _isAnimated;

public override bool IsChanged => false;

public ImageViewModel(string name, string fileName)
public ImageViewModel(ImageInformation information)
{
_name = name;
_fileName = fileName;
_name = information.ImageName;
_fileName = information.FileName;
_width = information.Width;
_height = information.Height;
_format = information.Format;
_isAnimated = information.IsAnimated;
}

public string Name
Expand All @@ -108,6 +178,10 @@ public string Name
}

public string FileName => _fileName;
public ushort Width => _width;
public ushort Height => _height;
public ImageFormat Format => _format;
public bool IsAnimated => _isAnimated;

protected override Task ApplyChangesAsync(CancellationToken cancellationToken) => throw new NotImplementedException();
protected override void Reset() => throw new NotImplementedException();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public ValueTask DisposeAsync()
{
_cancellationTokenSource.Dispose();
_cancellationTokenSource.Cancel();
_stateRegistration.Dispose();
return ValueTask.CompletedTask;
}

Expand Down
10 changes: 5 additions & 5 deletions src/Exo/Ui/Exo.Settings.Ui/ViewModels/SettingsViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ public PageViewModel? SelectedNavigationPage
public PageViewModel HomePage { get; }
public PageViewModel DevicesPage { get; }
public PageViewModel LightingPage { get; }
public PageViewModel ImagesPage { get; }
public PageViewModel SensorsPage { get; }
public PageViewModel CoolingPage { get; }
public PageViewModel ImagesPage { get; }
public PageViewModel CustomMenuPage { get; }
public PageViewModel ProgrammingPage { get; }

Expand All @@ -105,7 +105,7 @@ public SettingsViewModel(SettingsServiceConnectionManager connectionManager, Con
_devicesViewModel = new(ConnectionManager, _metadataService, _navigateCommand);
_batteryDevicesViewModel = new(_devicesViewModel);
_lightingViewModel = new(ConnectionManager, _devicesViewModel, _metadataService);
_imagesViewModel = new();
_imagesViewModel = new(ConnectionManager);
_sensorsViewModel = new(ConnectionManager, _devicesViewModel, _metadataService);
_coolingViewModel = new(ConnectionManager, _devicesViewModel, _sensorsViewModel, _metadataService);
_programmingViewModel = new(ConnectionManager);
Expand All @@ -114,12 +114,12 @@ public SettingsViewModel(SettingsServiceConnectionManager connectionManager, Con
HomePage = new("Home", "\uE80F");
DevicesPage = new("Devices", "\uE772");
LightingPage = new("Lighting", "\uE781");
ImagesPage = new("Images", "\uE8B9");
SensorsPage = new("Sensors", "\uE9D9");
CoolingPage = new("Cooling", "\uE9CA");
ImagesPage = new("Images", "\uE8B9");
CustomMenuPage = new("CustomMenu", "\uEDE3");
ProgrammingPage = new("Programming", "\uE943");
NavigationPages = [HomePage, DevicesPage, LightingPage, ImagesPage, SensorsPage, CoolingPage, CustomMenuPage, ProgrammingPage];
NavigationPages = [HomePage, DevicesPage, LightingPage, SensorsPage, CoolingPage, ImagesPage, CustomMenuPage, ProgrammingPage];
SelectedNavigationPage = HomePage;

connectionViewModel.PropertyChanged += OnConnectionViewModelPropertyChanged;
Expand Down Expand Up @@ -149,8 +149,8 @@ private void OnConnectionViewModelPropertyChanged(object? sender, System.Compone
public BatteryDevicesViewModel BatteryDevices => _batteryDevicesViewModel;
public LightingViewModel Lighting => _lightingViewModel;
public SensorsViewModel Sensors => _sensorsViewModel;
public ImagesViewModel Images => _imagesViewModel;
public CoolingViewModel Cooling => _coolingViewModel;
public ImagesViewModel Images => _imagesViewModel;
public ProgrammingViewModel Programming => _programmingViewModel;
public CustomMenuViewModel CustomMenu => _customMenuViewModel;
public IEditionService EditionService => _editionService;
Expand Down

0 comments on commit 8216603

Please sign in to comment.