Show machine photo thumbnails in machine details.

This commit is contained in:
2025-11-15 20:07:52 +00:00
parent 6a52c1f067
commit 1dcb062c35
6 changed files with 154 additions and 7 deletions

View File

@@ -32,7 +32,7 @@ public partial class App : Application
} }
protected Window? MainWindow { get; private set; } protected Window? MainWindow { get; private set; }
protected IHost? Host { get; private set; } public IHost? Host { get; private set; }
protected override async void OnLaunched(LaunchActivatedEventArgs args) protected override async void OnLaunched(LaunchActivatedEventArgs args)
{ {
@@ -109,6 +109,7 @@ public partial class App : Application
// Register application services // Register application services
services.AddSingleton<FlagCache>(); services.AddSingleton<FlagCache>();
services.AddSingleton<CompanyLogoCache>(); services.AddSingleton<CompanyLogoCache>();
services.AddSingleton<MachinePhotoCache>();
services.AddSingleton<NewsService>(); services.AddSingleton<NewsService>();
services.AddSingleton<NewsViewModel>(); services.AddSingleton<NewsViewModel>();
services.AddSingleton<ComputersService>(); services.AddSingleton<ComputersService>();

View File

@@ -28,13 +28,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Windows.Storage.Streams;
using Humanizer; using Humanizer;
using Marechai.App.Helpers; using Marechai.App.Helpers;
using Marechai.App.Presentation.Models; using Marechai.App.Presentation.Models;
using Marechai.App.Services; using Marechai.App.Services;
using Marechai.App.Services.Caching;
using Marechai.Data; using Marechai.Data;
using Microsoft.UI.Xaml; using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Media.Imaging;
using Uno.Extensions.Navigation; using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation.ViewModels; namespace Marechai.App.Presentation.ViewModels;
@@ -44,6 +49,7 @@ public partial class MachineViewViewModel : ObservableObject
private readonly ComputersService _computersService; private readonly ComputersService _computersService;
private readonly ILogger<MachineViewViewModel> _logger; private readonly ILogger<MachineViewViewModel> _logger;
private readonly INavigator _navigator; private readonly INavigator _navigator;
private readonly MachinePhotoCache _photoCache;
[ObservableProperty] [ObservableProperty]
private string _companyName = string.Empty; private string _companyName = string.Empty;
@@ -94,6 +100,9 @@ public partial class MachineViewViewModel : ObservableObject
[ObservableProperty] [ObservableProperty]
private Visibility _showModel = Visibility.Collapsed; private Visibility _showModel = Visibility.Collapsed;
[ObservableProperty]
private Visibility _showPhotos = Visibility.Collapsed;
[ObservableProperty] [ObservableProperty]
private Visibility _showProcessors = Visibility.Collapsed; private Visibility _showProcessors = Visibility.Collapsed;
@@ -103,12 +112,13 @@ public partial class MachineViewViewModel : ObservableObject
[ObservableProperty] [ObservableProperty]
private Visibility _showStorage = Visibility.Collapsed; private Visibility _showStorage = Visibility.Collapsed;
public MachineViewViewModel(ILogger<MachineViewViewModel> logger, INavigator navigator, public MachineViewViewModel(ILogger<MachineViewViewModel> logger, INavigator navigator,
ComputersService computersService) ComputersService computersService, MachinePhotoCache photoCache)
{ {
_logger = logger; _logger = logger;
_navigator = navigator; _navigator = navigator;
_computersService = computersService; _computersService = computersService;
_photoCache = photoCache;
} }
public ObservableCollection<ProcessorDisplayItem> Processors { get; } = []; public ObservableCollection<ProcessorDisplayItem> Processors { get; } = [];
@@ -116,6 +126,7 @@ public partial class MachineViewViewModel : ObservableObject
public ObservableCollection<GpuDisplayItem> Gpus { get; } = []; public ObservableCollection<GpuDisplayItem> Gpus { get; } = [];
public ObservableCollection<SoundSynthesizerDisplayItem> SoundSynthesizers { get; } = []; public ObservableCollection<SoundSynthesizerDisplayItem> SoundSynthesizers { get; } = [];
public ObservableCollection<StorageDisplayItem> Storage { get; } = []; public ObservableCollection<StorageDisplayItem> Storage { get; } = [];
public ObservableCollection<PhotoCarouselDisplayItem> Photos { get; } = [];
[RelayCommand] [RelayCommand]
public async Task GoBack() public async Task GoBack()
@@ -192,6 +203,7 @@ public partial class MachineViewViewModel : ObservableObject
Gpus.Clear(); Gpus.Clear();
SoundSynthesizers.Clear(); SoundSynthesizers.Clear();
Storage.Clear(); Storage.Clear();
Photos.Clear();
_logger.LogInformation("Loading machine {MachineId}", machineId); _logger.LogInformation("Loading machine {MachineId}", machineId);
@@ -324,6 +336,25 @@ public partial class MachineViewViewModel : ObservableObject
} }
} }
// Populate photos
List<Guid> photoIds = await _computersService.GetMachinePhotosAsync(machineId);
if(photoIds.Count > 0)
{
foreach(Guid photoId in photoIds)
{
var photoItem = new PhotoCarouselDisplayItem
{
PhotoId = photoId
};
// Load thumbnail image asynchronously
_ = LoadPhotoThumbnailAsync(photoItem);
Photos.Add(photoItem);
}
}
UpdateVisibilities(); UpdateVisibilities();
IsDataLoaded = true; IsDataLoaded = true;
IsLoading = false; IsLoading = false;
@@ -354,6 +385,28 @@ public partial class MachineViewViewModel : ObservableObject
ShowGpus = Gpus.Count > 0 ? Visibility.Visible : Visibility.Collapsed; ShowGpus = Gpus.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
ShowSoundSynthesizers = SoundSynthesizers.Count > 0 ? Visibility.Visible : Visibility.Collapsed; ShowSoundSynthesizers = SoundSynthesizers.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
ShowStorage = Storage.Count > 0 ? Visibility.Visible : Visibility.Collapsed; ShowStorage = Storage.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
ShowPhotos = Photos.Count > 0 ? Visibility.Visible : Visibility.Collapsed;
}
private async Task LoadPhotoThumbnailAsync(PhotoCarouselDisplayItem photoItem)
{
try
{
Stream stream = await _photoCache.GetThumbnailAsync(photoItem.PhotoId);
var bitmap = new BitmapImage();
using(IRandomAccessStream randomStream = stream.AsRandomAccessStream())
{
await bitmap.SetSourceAsync(randomStream);
}
photoItem.ThumbnailImageSource = bitmap;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading photo thumbnail {PhotoId}", photoItem.PhotoId);
}
} }
} }
@@ -405,3 +458,14 @@ public class StorageDisplayItem
public string DisplayText { get; set; } = string.Empty; public string DisplayText { get; set; } = string.Empty;
public string TypeNote { get; set; } = string.Empty; public string TypeNote { get; set; } = string.Empty;
} }
/// <summary>
/// Display item for photo carousel
/// </summary>
public class PhotoCarouselDisplayItem
{
// Thumbnail constraints
public const int ThumbnailMaxSize = 256;
public Guid PhotoId { get; set; }
public ImageSource? ThumbnailImageSource { get; set; }
}

View File

@@ -86,7 +86,14 @@
Foreground="{ThemeResource SystemBaseMediumColor}" /> Foreground="{ThemeResource SystemBaseMediumColor}" />
<ItemsRepeater ItemsSource="{Binding LettersList}" <ItemsRepeater ItemsSource="{Binding LettersList}"
Layout="{StaticResource LettersGridLayout}"> Layout="{StaticResource LettersGridLayout}">
<ItemsRepeater.ItemTemplate></ItemsRepeater.ItemTemplate> <ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}"
Command="{Binding DataContext.NavigateByLetterCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Style="{StaticResource KeyboardKeyButtonStyle}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater> </ItemsRepeater>
</StackPanel> </StackPanel>
@@ -98,7 +105,14 @@
Foreground="{ThemeResource SystemBaseMediumColor}" /> Foreground="{ThemeResource SystemBaseMediumColor}" />
<ItemsRepeater ItemsSource="{Binding YearsList}" <ItemsRepeater ItemsSource="{Binding YearsList}"
Layout="{StaticResource YearsGridLayout}"> Layout="{StaticResource YearsGridLayout}">
<ItemsRepeater.ItemTemplate></ItemsRepeater.ItemTemplate> <ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}"
Command="{Binding DataContext.NavigateByYearCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Style="{StaticResource KeyboardKeyButtonStyle}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater> </ItemsRepeater>
</StackPanel> </StackPanel>

View File

@@ -5,6 +5,7 @@
x:Name="PageRoot" x:Name="PageRoot"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wctui="using:CommunityToolkit.WinUI.UI.Controls"
NavigationCacheMode="Disabled" NavigationCacheMode="Disabled"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
@@ -372,6 +373,37 @@
</ItemsControl> </ItemsControl>
</StackPanel> </StackPanel>
<!-- Photos Carousel (Last element before spacing) -->
<StackPanel Visibility="{Binding ShowPhotos}"
Spacing="12">
<TextBlock Text="Photos"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<wctui:Carousel ItemsSource="{Binding Photos}"
Height="280"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
HorizontalAlignment="Stretch">
<wctui:Carousel.ItemTemplate>
<DataTemplate>
<Grid Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Image Source="{Binding ThumbnailImageSource}"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MaxWidth="256"
MaxHeight="256" />
</Grid>
</DataTemplate>
</wctui:Carousel.ItemTemplate>
</wctui:Carousel>
</StackPanel>
<!-- Bottom Spacing --> <!-- Bottom Spacing -->
<Border Height="24" /> <Border Height="24" />

View File

@@ -31,7 +31,7 @@ public sealed class MachinePhotoCache
public async Task<Stream> GetThumbnailAsync(Guid photoId) public async Task<Stream> GetThumbnailAsync(Guid photoId)
{ {
var filename = $"{photoId}.svg"; var filename = $"{photoId}.webp";
Stream retStream; Stream retStream;
@@ -53,7 +53,7 @@ public sealed class MachinePhotoCache
public async Task<Stream> GetPhotoAsync(Guid photoId) public async Task<Stream> GetPhotoAsync(Guid photoId)
{ {
var filename = $"{photoId}.svg"; var filename = $"{photoId}.webp";
Stream retStream; Stream retStream;

View File

@@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Marechai.App.Helpers; using Marechai.App.Helpers;
using Microsoft.Kiota.Abstractions.Serialization; using Microsoft.Kiota.Abstractions.Serialization;
@@ -205,4 +206,39 @@ public class ComputersService
return null; return null;
} }
} }
/// <summary>
/// Fetches the list of photo GUIDs for a machine from the API
/// </summary>
public async Task<List<Guid>> GetMachinePhotosAsync(int machineId)
{
try
{
_logger.LogInformation("Fetching photos for machine {MachineId} from API", machineId);
List<Guid?>? photos = await _apiClient.Machines[machineId].Photos.GetAsync();
if(photos == null || photos.Count == 0)
{
_logger.LogInformation("No photos found for machine {MachineId}", machineId);
return [];
}
// Filter out null values
var validPhotos = photos.Where(p => p.HasValue).Select(p => p!.Value).ToList();
_logger.LogInformation("Successfully fetched {Count} photos for machine {MachineId}",
validPhotos.Count,
machineId);
return validPhotos;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching photos for machine {MachineId} from API", machineId);
return [];
}
}
} }