Add list of GPUs.

This commit is contained in:
2025-11-16 02:35:59 +00:00
parent 5c64e59f8f
commit 981cd3c27c
6 changed files with 537 additions and 1 deletions

View File

@@ -14,6 +14,7 @@ using Uno.UI;
using CompanyDetailViewModel = Marechai.App.Presentation.ViewModels.CompanyDetailViewModel;
using ComputersListViewModel = Marechai.App.Presentation.ViewModels.ComputersListViewModel;
using ComputersViewModel = Marechai.App.Presentation.ViewModels.ComputersViewModel;
using GpuListViewModel = Marechai.App.Presentation.ViewModels.GpusListViewModel;
using MachineViewViewModel = Marechai.App.Presentation.ViewModels.MachineViewViewModel;
using MainViewModel = Marechai.App.Presentation.ViewModels.MainViewModel;
using NewsViewModel = Marechai.App.Presentation.ViewModels.NewsViewModel;
@@ -122,6 +123,7 @@ public partial class App : Application
services.AddSingleton<CompanyDetailService>();
services.AddSingleton<CompanyDetailViewModel>();
services.AddSingleton<MachineViewViewModel>();
services.AddSingleton<GpusService>();
services.AddTransient<PhotoDetailViewModel>();
services
@@ -134,6 +136,7 @@ public partial class App : Application
services.AddTransient<ComputersListViewModel>();
services.AddTransient<ConsolesListViewModel>();
services.AddTransient<GpuListViewModel>();
})
.UseNavigation(RegisterRoutes));
@@ -160,6 +163,7 @@ public partial class App : Application
new ViewMap<CompanyDetailPage, CompanyDetailViewModel>(),
new ViewMap<MachineViewPage, MachineViewViewModel>(),
new ViewMap<PhotoDetailPage, PhotoDetailViewModel>(),
new ViewMap<GpuListPage, GpuListViewModel>(),
new DataViewMap<SecondPage, SecondViewModel, Entity>());
routes.Register(new RouteMap("",
@@ -201,6 +205,15 @@ public partial class App : Application
views.FindByViewModel<
CompanyDetailViewModel>())
]),
new RouteMap("gpus",
views.FindByViewModel<GpuListViewModel>(),
Nested:
[
new RouteMap("list-gpus",
views.FindByViewModel<
GpuListViewModel>(),
true)
]),
new RouteMap("Second",
views.FindByViewModel<SecondViewModel>())
])

View File

@@ -0,0 +1,204 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Marechai.App.Services;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation.ViewModels;
/// <summary>
/// ViewModel for displaying a list of GPUs
/// </summary>
public partial class GpusListViewModel : ObservableObject
{
private readonly GpusService _gpusService;
private readonly IStringLocalizer _localizer;
private readonly ILogger<GpusListViewModel> _logger;
private readonly INavigator _navigator;
[ObservableProperty]
private string _errorMessage = string.Empty;
[ObservableProperty]
private ObservableCollection<GpuListItem> _gpusList = [];
[ObservableProperty]
private bool _hasError;
[ObservableProperty]
private bool _isDataLoaded;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _pageTitle = string.Empty;
public GpusListViewModel(GpusService gpusService, IStringLocalizer localizer, ILogger<GpusListViewModel> logger,
INavigator navigator)
{
_gpusService = gpusService;
_localizer = localizer;
_logger = logger;
_navigator = navigator;
LoadData = new AsyncRelayCommand(LoadDataAsync);
NavigateToGpuCommand = new AsyncRelayCommand<GpuListItem>(NavigateToGpuAsync);
}
public IAsyncRelayCommand LoadData { get; }
public IAsyncRelayCommand<GpuListItem> NavigateToGpuCommand { get; }
/// <summary>
/// Loads all GPUs and sorts them with special handling for Framebuffer and Software
/// </summary>
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
GpusList.Clear();
_logger.LogInformation("LoadDataAsync called for GPUs");
PageTitle = _localizer["GraphicalProcessingUnits"];
// Load GPUs from the API
await LoadGpusFromApiAsync();
_logger.LogInformation("LoadGpusFromApiAsync completed. GpusList.Count={Count}", GpusList.Count);
if(GpusList.Count == 0)
{
ErrorMessage = _localizer["No graphics processing units found"].Value;
HasError = true;
_logger.LogWarning("No GPUs found");
}
else
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading GPUs: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load graphics processing units. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
/// <summary>
/// Loads GPUs from the API and sorts them with special handling for Framebuffer and Software
/// </summary>
private async Task LoadGpusFromApiAsync()
{
try
{
List<GpuDto> gpus = await _gpusService.GetAllGpusAsync();
if(gpus == null || gpus.Count == 0)
{
_logger.LogInformation("No GPUs returned from API");
return;
}
// Separate special GPUs from regular ones
var specialGpus = new List<GpuListItem>();
var regularGpus = new List<GpuListItem>();
foreach(GpuDto gpu in gpus)
{
string displayName = gpu.Name ?? string.Empty;
// Replace special database names
if(displayName == "DB_FRAMEBUFFER")
displayName = "Framebuffer";
else if(displayName == "DB_SOFTWARE")
displayName = "Software";
else if(displayName == "DB_NONE") displayName = "None";
var gpuItem = new GpuListItem
{
Id = gpu.Id ?? 0,
Name = displayName,
Company = gpu.Company ?? string.Empty,
IsSpecial = gpu.Name is "DB_FRAMEBUFFER" or "DB_SOFTWARE" or "DB_NONE"
};
if(gpuItem.IsSpecial)
specialGpus.Add(gpuItem);
else
regularGpus.Add(gpuItem);
_logger.LogInformation("GPU: {Name}, Company: {Company}, ID: {Id}, IsSpecial: {IsSpecial}",
displayName,
gpu.Company,
gpu.Id,
gpuItem.IsSpecial);
}
// Sort special GPUs: Framebuffer first, then Software, then None
specialGpus.Sort((a, b) =>
{
int orderA = a.Name == "Framebuffer"
? 0
: a.Name == "Software"
? 1
: 2;
int orderB = b.Name == "Framebuffer"
? 0
: b.Name == "Software"
? 1
: 2;
return orderA.CompareTo(orderB);
});
// Sort regular GPUs alphabetically
regularGpus.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
// Add special GPUs first, then regular GPUs
foreach(GpuListItem gpu in specialGpus) GpusList.Add(gpu);
foreach(GpuListItem gpu in regularGpus) GpusList.Add(gpu);
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading GPUs from API");
}
}
/// <summary>
/// Navigates to the GPU detail view
/// </summary>
private async Task NavigateToGpuAsync(GpuListItem? gpu)
{
if(gpu is null) return;
_logger.LogInformation("Navigating to GPU detail: {GpuName} (ID: {GpuId})", gpu.Name, gpu.Id);
// For now, we'll just log it. Implement detail page navigation when ready.
// await _navigator.NavigateViewModelAsync<GpuDetailViewModel>(this, data: gpu);
}
}
/// <summary>
/// Data model for a GPU in the list
/// </summary>
public class GpuListItem
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Company { get; set; } = string.Empty;
public bool IsSpecial { get; set; }
}

View File

@@ -47,7 +47,7 @@ public partial class MainViewModel : ObservableObject
NavigateToConsolesCommand = new AsyncRelayCommand(() => NavigateTo("consoles"));
NavigateToDocumentsCommand = new AsyncRelayCommand(() => NavigateTo("documents"));
NavigateToDumpsCommand = new AsyncRelayCommand(() => NavigateTo("dumps"));
NavigateToGraphicalProcessingUnitsCommand = new AsyncRelayCommand(() => NavigateTo("gpus"));
NavigateToGraphicalProcessingUnitsCommand = new AsyncRelayCommand(() => NavigateTo("gpus/list-gpus"));
NavigateToMagazinesCommand = new AsyncRelayCommand(() => NavigateTo("magazines"));
NavigateToPeopleCommand = new AsyncRelayCommand(() => NavigateTo("people"));
NavigateToProcessorsCommand = new AsyncRelayCommand(() => NavigateTo("processors"));

View File

@@ -0,0 +1,207 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.GpuListPage"
x:Name="PageRoot"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:utu="using:Uno.Toolkit.UI"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid utu:SafeArea.Insets="VisibleBounds">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header -->
<utu:NavigationBar Grid.Row="0"
Content="Graphics Processing Units">
</utu:NavigationBar>
<!-- Main Content -->
<Grid Grid.Row="1">
<!-- Loading State -->
<StackPanel Visibility="{Binding IsLoading}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="32"
Spacing="16">
<ProgressRing IsActive="True"
IsIndeterminate="True"
Height="64"
Width="64"
Foreground="{ThemeResource SystemAccentColor}" />
<TextBlock Text="Loading graphics processing units..."
FontSize="14"
TextAlignment="Center"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
<!-- Error State -->
<StackPanel Visibility="{Binding HasError}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="24"
Spacing="16"
MaxWidth="400">
<InfoBar IsOpen="True"
Severity="Error"
Title="Unable to Load Graphics Processing Units"
Message="{Binding ErrorMessage}"
IsClosable="False" />
<Button Content="Retry"
Command="{Binding LoadData}"
HorizontalAlignment="Center"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<!-- GPUs List -->
<Grid Visibility="{Binding IsDataLoaded}">
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<Grid Padding="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Count Header -->
<StackPanel Grid.Row="0"
Padding="16,12"
Orientation="Horizontal"
Spacing="4">
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}"
Text="RESULTS:" />
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}"
Text="{Binding GpusList.Count}" />
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}"
Text="graphics processing units" />
</StackPanel>
<!-- GPUs List -->
<ItemsControl Grid.Row="1"
ItemsSource="{Binding GpusList}"
Padding="0"
Margin="0,8,0,0"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="0"
HorizontalAlignment="Stretch" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Padding="0"
Margin="0,0,0,8"
MinHeight="80"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
HorizontalAlignment="Stretch"
Command="{Binding DataContext.NavigateToGpuCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Background="Transparent"
BorderThickness="0">
<Button.Template>
<ControlTemplate TargetType="Button">
<Grid MinHeight="80"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch">
<!-- Shadow effect -->
<Border x:Name="ShadowBorder"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12"
Translation="0, 0, 4"
VerticalAlignment="Stretch">
<Border.Shadow>
<ThemeShadow />
</Border.Shadow>
<Grid ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- GPU Info -->
<StackPanel Grid.Column="0"
Spacing="8"
VerticalAlignment="Center"
HorizontalAlignment="Stretch">
<TextBlock Text="{Binding Name}"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}"
TextTrimming="CharacterEllipsis" />
<StackPanel Orientation="Horizontal"
Spacing="6"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE731;"
FontSize="14"
Foreground="{ThemeResource SystemAccentColor}" />
<TextBlock Text="{Binding Company}"
FontSize="13"
Foreground="{ThemeResource SystemBaseMediumColor}"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</StackPanel>
<!-- Navigation Arrow -->
<StackPanel Grid.Column="1"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE72A;"
FontSize="18"
Foreground="{ThemeResource SystemAccentColor}" />
</StackPanel>
</Grid>
</Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="ShadowBorder.Background"
Value="{ThemeResource CardBackgroundFillColorSecondaryBrush}" />
<Setter Target="ShadowBorder.Translation"
Value="0, -2, 8" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="ShadowBorder.Background"
Value="{ThemeResource CardBackgroundFillColorTertiaryBrush}" />
<Setter Target="ShadowBorder.Translation"
Value="0, 0, 2" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Button.Template>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</ScrollViewer>
</Grid>
</Grid>
</Grid>
</Page>

View File

@@ -0,0 +1,37 @@
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Marechai.App.Presentation.Views;
/// <summary>
/// Professional list view for displaying all graphics processing units.
/// Features responsive layout, modern styling, and special handling for Framebuffer and Software entries.
/// </summary>
public sealed partial class GpuListPage : Page
{
public GpuListPage()
{
InitializeComponent();
Loaded += GpuListPage_Loaded;
DataContextChanged += GpuListPage_DataContextChanged;
}
private void GpuListPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(DataContext is GpusListViewModel vm)
{
// Load data when DataContext is set
vm.LoadData.Execute(null);
}
}
private void GpuListPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is GpusListViewModel vm)
{
// Load data when page is loaded (fallback)
vm.LoadData.Execute(null);
}
}
}

View File

@@ -0,0 +1,75 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Marechai.App.Services;
/// <summary>
/// Service for fetching and managing GPUs from the Marechai API
/// </summary>
public class GpusService
{
private readonly ApiClient _apiClient;
private readonly ILogger<GpusService> _logger;
public GpusService(ApiClient apiClient, ILogger<GpusService> logger)
{
_apiClient = apiClient;
_logger = logger;
}
/// <summary>
/// Fetches all GPUs from the API
/// </summary>
public async Task<List<GpuDto>> GetAllGpusAsync()
{
try
{
_logger.LogInformation("Fetching all GPUs from API");
List<GpuDto> gpus = await _apiClient.Gpus.GetAsync();
if(gpus == null) return [];
_logger.LogInformation("Successfully fetched {Count} total GPUs", gpus.Count);
return gpus;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching all GPUs from API");
return [];
}
}
/// <summary>
/// Fetches a single GPU by ID from the API
/// </summary>
public async Task<GpuDto?> GetGpuByIdAsync(int gpuId)
{
try
{
_logger.LogInformation("Fetching GPU {GpuId} from API", gpuId);
GpuDto? gpu = await _apiClient.Gpus[gpuId].GetAsync();
if(gpu == null)
{
_logger.LogWarning("GPU {GpuId} not found", gpuId);
return null;
}
_logger.LogInformation("Successfully fetched GPU {GpuId}: {GpuName}", gpuId, gpu.Name);
return gpu;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching GPU {GpuId} from API", gpuId);
return null;
}
}
}