diff --git a/Marechai.App/App.xaml.cs b/Marechai.App/App.xaml.cs index b9291b0f..6cf657da 100644 --- a/Marechai.App/App.xaml.cs +++ b/Marechai.App/App.xaml.cs @@ -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(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); services @@ -134,6 +136,7 @@ public partial class App : Application services.AddTransient(); services.AddTransient(); + services.AddTransient(); }) .UseNavigation(RegisterRoutes)); @@ -160,6 +163,7 @@ public partial class App : Application new ViewMap(), new ViewMap(), new ViewMap(), + new ViewMap(), new DataViewMap()); routes.Register(new RouteMap("", @@ -201,6 +205,15 @@ public partial class App : Application views.FindByViewModel< CompanyDetailViewModel>()) ]), + new RouteMap("gpus", + views.FindByViewModel(), + Nested: + [ + new RouteMap("list-gpus", + views.FindByViewModel< + GpuListViewModel>(), + true) + ]), new RouteMap("Second", views.FindByViewModel()) ]) diff --git a/Marechai.App/Presentation/ViewModels/GpusListViewModel.cs b/Marechai.App/Presentation/ViewModels/GpusListViewModel.cs new file mode 100644 index 00000000..ce0dfeb8 --- /dev/null +++ b/Marechai.App/Presentation/ViewModels/GpusListViewModel.cs @@ -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; + +/// +/// ViewModel for displaying a list of GPUs +/// +public partial class GpusListViewModel : ObservableObject +{ + private readonly GpusService _gpusService; + private readonly IStringLocalizer _localizer; + private readonly ILogger _logger; + private readonly INavigator _navigator; + + [ObservableProperty] + private string _errorMessage = string.Empty; + + [ObservableProperty] + private ObservableCollection _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 logger, + INavigator navigator) + { + _gpusService = gpusService; + _localizer = localizer; + _logger = logger; + _navigator = navigator; + LoadData = new AsyncRelayCommand(LoadDataAsync); + NavigateToGpuCommand = new AsyncRelayCommand(NavigateToGpuAsync); + } + + public IAsyncRelayCommand LoadData { get; } + public IAsyncRelayCommand NavigateToGpuCommand { get; } + + /// + /// Loads all GPUs and sorts them with special handling for Framebuffer and Software + /// + 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; + } + } + + /// + /// Loads GPUs from the API and sorts them with special handling for Framebuffer and Software + /// + private async Task LoadGpusFromApiAsync() + { + try + { + List 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(); + var regularGpus = new List(); + + 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"); + } + } + + /// + /// Navigates to the GPU detail view + /// + 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(this, data: gpu); + } +} + +/// +/// Data model for a GPU in the list +/// +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; } +} \ No newline at end of file diff --git a/Marechai.App/Presentation/ViewModels/MainViewModel.cs b/Marechai.App/Presentation/ViewModels/MainViewModel.cs index f61e550c..bce7f9ef 100644 --- a/Marechai.App/Presentation/ViewModels/MainViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/MainViewModel.cs @@ -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")); diff --git a/Marechai.App/Presentation/Views/GpuListPage.xaml b/Marechai.App/Presentation/Views/GpuListPage.xaml new file mode 100644 index 00000000..a81476b5 --- /dev/null +++ b/Marechai.App/Presentation/Views/GpuListPage.xaml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Marechai.App/Presentation/Views/GpuListPage.xaml.cs b/Marechai.App/Presentation/Views/GpuListPage.xaml.cs new file mode 100644 index 00000000..150c4106 --- /dev/null +++ b/Marechai.App/Presentation/Views/GpuListPage.xaml.cs @@ -0,0 +1,37 @@ +using Marechai.App.Presentation.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Marechai.App.Presentation.Views; + +/// +/// Professional list view for displaying all graphics processing units. +/// Features responsive layout, modern styling, and special handling for Framebuffer and Software entries. +/// +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); + } + } +} \ No newline at end of file diff --git a/Marechai.App/Services/GpusService.cs b/Marechai.App/Services/GpusService.cs new file mode 100644 index 00000000..3b59a507 --- /dev/null +++ b/Marechai.App/Services/GpusService.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Marechai.App.Services; + +/// +/// Service for fetching and managing GPUs from the Marechai API +/// +public class GpusService +{ + private readonly ApiClient _apiClient; + private readonly ILogger _logger; + + public GpusService(ApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Fetches all GPUs from the API + /// + public async Task> GetAllGpusAsync() + { + try + { + _logger.LogInformation("Fetching all GPUs from API"); + + List 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 []; + } + } + + /// + /// Fetches a single GPU by ID from the API + /// + public async Task 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; + } + } +} \ No newline at end of file