diff --git a/Marechai.App/App.xaml.cs b/Marechai.App/App.xaml.cs index 6cf657da..bc4f0bfa 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 GpuDetailViewModel = Marechai.App.Presentation.ViewModels.GpuDetailViewModel; using GpuListViewModel = Marechai.App.Presentation.ViewModels.GpusListViewModel; using MachineViewViewModel = Marechai.App.Presentation.ViewModels.MachineViewViewModel; using MainViewModel = Marechai.App.Presentation.ViewModels.MainViewModel; @@ -137,6 +138,7 @@ public partial class App : Application services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); }) .UseNavigation(RegisterRoutes)); @@ -164,6 +166,7 @@ public partial class App : Application new ViewMap(), new ViewMap(), new ViewMap(), + new ViewMap(), new DataViewMap()); routes.Register(new RouteMap("", @@ -201,7 +204,7 @@ public partial class App : Application views.FindByViewModel(), Nested: [ - new RouteMap("detail", + new RouteMap("company-details", views.FindByViewModel< CompanyDetailViewModel>()) ]), @@ -209,10 +212,9 @@ public partial class App : Application views.FindByViewModel(), Nested: [ - new RouteMap("list-gpus", + new RouteMap("gpu-details", views.FindByViewModel< - GpuListViewModel>(), - true) + GpuDetailViewModel>()) ]), new RouteMap("Second", views.FindByViewModel()) diff --git a/Marechai.App/Presentation/Models/GpuDetailNavigationParameter.cs b/Marechai.App/Presentation/Models/GpuDetailNavigationParameter.cs new file mode 100644 index 00000000..6214fb4a --- /dev/null +++ b/Marechai.App/Presentation/Models/GpuDetailNavigationParameter.cs @@ -0,0 +1,35 @@ +/****************************************************************************** +// MARECHAI: Master repository of computing history artifacts information +// ---------------------------------------------------------------------------- +// +// Author(s) : Natalia Portillo +// +// --[ License ] -------------------------------------------------------------- +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2003-2026 Natalia Portillo +*******************************************************************************/ + +namespace Marechai.App.Presentation.Models; + +/// +/// Navigation parameter for the GpuDetailPage containing both the GPU ID and the navigation source. +/// +public class GpuDetailNavigationParameter +{ + public required int GpuId { get; init; } + public object? NavigationSource { get; init; } +} \ No newline at end of file diff --git a/Marechai.App/Presentation/ViewModels/GpuDetailViewModel.cs b/Marechai.App/Presentation/ViewModels/GpuDetailViewModel.cs new file mode 100644 index 00000000..d6c40604 --- /dev/null +++ b/Marechai.App/Presentation/ViewModels/GpuDetailViewModel.cs @@ -0,0 +1,386 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; +using Marechai.App.Presentation.Models; +using Marechai.App.Services; +using Uno.Extensions.Navigation; + +namespace Marechai.App.Presentation.ViewModels; + +public partial class GpuDetailViewModel : ObservableObject +{ + private readonly CompaniesService _companiesService; + private readonly GpusService _gpusService; + private readonly IStringLocalizer _localizer; + private readonly ILogger _logger; + private readonly INavigator _navigator; + + [ObservableProperty] + private ObservableCollection _computers = []; + + [ObservableProperty] + private string _computersFilterText = string.Empty; + + [ObservableProperty] + private string _consoelsFilterText = string.Empty; + + [ObservableProperty] + private ObservableCollection _consoles = []; + + [ObservableProperty] + private string _errorMessage = string.Empty; + + [ObservableProperty] + private ObservableCollection _filteredComputers = []; + + [ObservableProperty] + private ObservableCollection _filteredConsoles = []; + + [ObservableProperty] + private GpuDto? _gpu; + + [ObservableProperty] + private int _gpuId; + + [ObservableProperty] + private bool _hasComputers; + + [ObservableProperty] + private bool _hasConsoles; + + [ObservableProperty] + private bool _hasError; + + [ObservableProperty] + private bool _isDataLoaded; + + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + private string _manufacturerName = string.Empty; + private object? _navigationSource; + + [ObservableProperty] + private ObservableCollection _resolutions = []; + + public GpuDetailViewModel(GpusService gpusService, CompaniesService companiesService, IStringLocalizer localizer, + ILogger logger, INavigator navigator) + { + _gpusService = gpusService; + _companiesService = companiesService; + _localizer = localizer; + _logger = logger; + _navigator = navigator; + LoadData = new AsyncRelayCommand(LoadDataAsync); + GoBackCommand = new AsyncRelayCommand(GoBackAsync); + SelectMachineCommand = new AsyncRelayCommand(SelectMachineAsync); + ComputersFilterCommand = new RelayCommand(() => FilterComputers()); + ConsolesFilterCommand = new RelayCommand(() => FilterConsoles()); + } + + public IAsyncRelayCommand LoadData { get; } + public ICommand GoBackCommand { get; } + public IAsyncRelayCommand SelectMachineCommand { get; } + public ICommand ComputersFilterCommand { get; } + public ICommand ConsolesFilterCommand { get; } + + public string Title { get; } = "GPU Details"; + + /// + /// Loads GPU details including resolutions, computers, and consoles + /// + private async Task LoadDataAsync() + { + try + { + IsLoading = true; + ErrorMessage = string.Empty; + HasError = false; + IsDataLoaded = false; + Resolutions.Clear(); + Computers.Clear(); + Consoles.Clear(); + + if(GpuId <= 0) + { + ErrorMessage = _localizer["Invalid GPU ID"].Value; + HasError = true; + + return; + } + + _logger.LogInformation("Loading GPU details for ID: {GpuId}", GpuId); + + // Load GPU details + Gpu = await _gpusService.GetGpuByIdAsync(GpuId); + + if(Gpu is null) + { + ErrorMessage = _localizer["Graphics processing unit not found"].Value; + HasError = true; + + return; + } + + // Set manufacturer name (from Company field or fetch by CompanyId if empty) + ManufacturerName = Gpu.Company ?? string.Empty; + + if(string.IsNullOrEmpty(ManufacturerName) && Gpu.CompanyId.HasValue) + { + try + { + CompanyDto? company = await _companiesService.GetCompanyByIdAsync(Gpu.CompanyId.Value); + if(company != null) ManufacturerName = company.Name ?? string.Empty; + } + catch(Exception ex) + { + _logger.LogWarning(ex, "Failed to load company for GPU {GpuId}", GpuId); + } + } + + // Format display name + string displayName = Gpu.Name ?? string.Empty; + + if(displayName == "DB_FRAMEBUFFER") + displayName = "Framebuffer"; + else if(displayName == "DB_SOFTWARE") + displayName = "Software"; + else if(displayName == "DB_NONE") displayName = "None"; + + _logger.LogInformation("GPU loaded: {Name}, Company: {Company}", displayName, ManufacturerName); + + // Load resolutions + try + { + List? resolutions = await _gpusService.GetResolutionsByGpuAsync(GpuId); + + if(resolutions != null && resolutions.Count > 0) + { + Resolutions.Clear(); + + foreach(ResolutionByGpuDto res in resolutions) + { + // Get the full resolution DTO using the resolution ID + if(res.ResolutionId.HasValue) + { + ResolutionDto? resolutionDto = + await _gpusService.GetResolutionByIdAsync(res.ResolutionId.Value); + + if(resolutionDto != null) + { + Resolutions.Add(new ResolutionItem + { + Id = resolutionDto.Id ?? 0, + Name = $"{resolutionDto.Width}x{resolutionDto.Height}", + Width = resolutionDto.Width ?? 0, + Height = resolutionDto.Height ?? 0, + Colors = resolutionDto.Colors ?? 0, + Palette = resolutionDto.Palette ?? 0, + Chars = resolutionDto.Chars ?? false, + Grayscale = resolutionDto.Grayscale ?? false + }); + } + } + } + + _logger.LogInformation("Loaded {Count} resolutions for GPU {GpuId}", Resolutions.Count, GpuId); + } + } + catch(Exception ex) + { + _logger.LogWarning(ex, "Failed to load resolutions for GPU {GpuId}", GpuId); + } + + // Load machines and separate into computers and consoles + try + { + List? machines = await _gpusService.GetMachinesByGpuAsync(GpuId); + + if(machines != null && machines.Count > 0) + { + Computers.Clear(); + Consoles.Clear(); + + foreach(MachineDto machine in machines) + { + var machineItem = new MachineItem + { + Id = machine.Id ?? 0, + Name = machine.Name ?? string.Empty, + Manufacturer = machine.Company ?? string.Empty, + Year = machine.Introduced?.Year ?? 0 + }; + + // Distinguish between computers and consoles based on Type + if(machine.Type == 2) // MachineType.Console + Consoles.Add(machineItem); + else // MachineType.Computer or Unknown + Computers.Add(machineItem); + } + + HasComputers = Computers.Count > 0; + HasConsoles = Consoles.Count > 0; + + // Initialize filtered collections + FilterComputers(); + FilterConsoles(); + + _logger.LogInformation("Loaded {ComputerCount} computers and {ConsoleCount} consoles for GPU {GpuId}", + Computers.Count, + Consoles.Count, + GpuId); + } + else + { + HasComputers = false; + HasConsoles = false; + } + } + catch(Exception ex) + { + _logger.LogWarning(ex, "Failed to load machines for GPU {GpuId}", GpuId); + HasComputers = false; + HasConsoles = false; + } + + IsDataLoaded = true; + } + catch(Exception ex) + { + _logger.LogError(ex, "Error loading GPU details: {Exception}", ex.Message); + ErrorMessage = _localizer["Failed to load graphics processing unit details. Please try again later."].Value; + HasError = true; + } + finally + { + IsLoading = false; + } + } + + /// + /// Filters computers based on search text + /// + private void FilterComputers() + { + if(string.IsNullOrWhiteSpace(ComputersFilterText)) + { + FilteredComputers.Clear(); + foreach(MachineItem computer in Computers) FilteredComputers.Add(computer); + } + else + { + var filtered = Computers + .Where(c => c.Name.Contains(ComputersFilterText, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + FilteredComputers.Clear(); + foreach(MachineItem computer in filtered) FilteredComputers.Add(computer); + } + } + + /// + /// Filters consoles based on search text + /// + private void FilterConsoles() + { + if(string.IsNullOrWhiteSpace(ConsoelsFilterText)) + { + FilteredConsoles.Clear(); + foreach(MachineItem console in Consoles) FilteredConsoles.Add(console); + } + else + { + var filtered = Consoles.Where(c => c.Name.Contains(ConsoelsFilterText, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + FilteredConsoles.Clear(); + foreach(MachineItem console in filtered) FilteredConsoles.Add(console); + } + } + + /// + /// Navigates back to the GPU list + /// + private async Task GoBackAsync() + { + // If we came from a machine view, go back to machine view + if(_navigationSource is MachineViewViewModel machineVm) + { + await _navigator.NavigateViewModelAsync(this); + + return; + } + + // Default: go back to GPU list + await _navigator.NavigateViewModelAsync(this); + } + + /// + /// Navigates to machine detail view + /// + private async Task SelectMachineAsync(int machineId) + { + if(machineId <= 0) return; + + var navParam = new MachineViewNavigationParameter + { + MachineId = machineId + }; + + await _navigator.NavigateViewModelAsync(this, data: navParam); + } + + /// + /// Sets the navigation source (where we came from). + /// + public void SetNavigationSource(object? source) + { + _navigationSource = source; + } +} + +/// +/// Resolution item for displaying GPU supported resolutions +/// +public class ResolutionItem +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public int Width { get; set; } + public int Height { get; set; } + public long Colors { get; set; } + public long Palette { get; set; } + public bool Chars { get; set; } + public bool Grayscale { get; set; } + + public string Resolution => $"{Width}x{Height}"; + + public string ResolutionType => Chars ? "Text" : "Pixel"; + + public string ResolutionDisplay => Chars ? $"{Width}x{Height} characters" : $"{Width}x{Height}"; + + public string ColorDisplay => Grayscale + ? $"{Colors} grays" + : Palette > 0 + ? $"{Colors} colors from a palette of {Palette} colors" + : $"{Colors} colors"; +} + +/// +/// Machine item for displaying computers or consoles that use the GPU +/// +public class MachineItem +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string Manufacturer { get; set; } = string.Empty; + public int Year { get; set; } + + public string YearDisplay => Year > 0 ? Year.ToString() : "Unknown"; +} \ No newline at end of file diff --git a/Marechai.App/Presentation/ViewModels/GpusListViewModel.cs b/Marechai.App/Presentation/ViewModels/GpusListViewModel.cs index ce0dfeb8..12894356 100644 --- a/Marechai.App/Presentation/ViewModels/GpusListViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/GpusListViewModel.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Threading.Tasks; +using Marechai.App.Presentation.Models; using Marechai.App.Services; using Uno.Extensions.Navigation; @@ -187,8 +188,14 @@ public partial class GpusListViewModel : ObservableObject _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); + // Navigate to GPU detail view with navigation parameter + var navParam = new GpuDetailNavigationParameter + { + GpuId = gpu.Id, + NavigationSource = this + }; + + await _navigator.NavigateViewModelAsync(this, data: navParam); } } diff --git a/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs b/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs index 70b1ee28..c10db29d 100644 --- a/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs @@ -167,6 +167,20 @@ public partial class MachineViewViewModel : ObservableObject return; } + // If we came from GpuDetailViewModel, navigate back to GPU details + if(_navigationSource is GpuDetailViewModel gpuDetailVm) + { + var navParam = new GpuDetailNavigationParameter + { + GpuId = gpuDetailVm.GpuId, + NavigationSource = this + }; + + await _navigator.NavigateViewModelAsync(this, data: navParam); + + return; + } + // Otherwise, try to go back in the navigation stack await _navigator.GoBack(this); } diff --git a/Marechai.App/Presentation/ViewModels/MainViewModel.cs b/Marechai.App/Presentation/ViewModels/MainViewModel.cs index bce7f9ef..f61e550c 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/list-gpus")); + NavigateToGraphicalProcessingUnitsCommand = new AsyncRelayCommand(() => NavigateTo("gpus")); NavigateToMagazinesCommand = new AsyncRelayCommand(() => NavigateTo("magazines")); NavigateToPeopleCommand = new AsyncRelayCommand(() => NavigateTo("people")); NavigateToProcessorsCommand = new AsyncRelayCommand(() => NavigateTo("processors")); diff --git a/Marechai.App/Presentation/Views/GpuDetailPage.xaml b/Marechai.App/Presentation/Views/GpuDetailPage.xaml new file mode 100644 index 00000000..b52f70c2 --- /dev/null +++ b/Marechai.App/Presentation/Views/GpuDetailPage.xaml @@ -0,0 +1,383 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Marechai.App/Presentation/Views/GpuDetailPage.xaml.cs b/Marechai.App/Presentation/Views/GpuDetailPage.xaml.cs new file mode 100644 index 00000000..6587b913 --- /dev/null +++ b/Marechai.App/Presentation/Views/GpuDetailPage.xaml.cs @@ -0,0 +1,98 @@ +#nullable enable + +using Marechai.App.Presentation.Models; +using Marechai.App.Presentation.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Navigation; + +namespace Marechai.App.Presentation.Views; + +/// +/// GPU detail page showing all information, resolutions, computers, and consoles +/// +public sealed partial class GpuDetailPage : Page +{ + private object? _navigationSource; + private int? _pendingGpuId; + + public GpuDetailPage() + { + InitializeComponent(); + DataContextChanged += GpuDetailPage_DataContextChanged; + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + + int? gpuId = null; + + // Handle both int and GpuDetailNavigationParameter + if(e.Parameter is int intId) + gpuId = intId; + else if(e.Parameter is GpuDetailNavigationParameter navParam) + { + gpuId = navParam.GpuId; + _navigationSource = navParam.NavigationSource; + } + + if(gpuId.HasValue) + { + _pendingGpuId = gpuId; + + if(DataContext is GpuDetailViewModel viewModel) + { + viewModel.GpuId = gpuId.Value; + if(_navigationSource != null) viewModel.SetNavigationSource(_navigationSource); + _ = viewModel.LoadData.ExecuteAsync(null); + } + } + } + + private void GpuDetailPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) + { + if(DataContext is GpuDetailViewModel viewModel && _pendingGpuId.HasValue) + { + viewModel.GpuId = _pendingGpuId.Value; + if(_navigationSource != null) viewModel.SetNavigationSource(_navigationSource); + _ = viewModel.LoadData.ExecuteAsync(null); + } + } + + private void ComputersSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) + { + if(DataContext is GpuDetailViewModel vm) vm.ComputersFilterCommand.Execute(null); + } + + private void ComputersSearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) + { + if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) + if(DataContext is GpuDetailViewModel vm) + vm.ComputersFilterCommand.Execute(null); + } + + private void ConsolesSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) + { + if(DataContext is GpuDetailViewModel vm) vm.ConsolesFilterCommand.Execute(null); + } + + private void ConsolesSearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) + { + if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) + if(DataContext is GpuDetailViewModel vm) + vm.ConsolesFilterCommand.Execute(null); + } + + private void Computer_Click(object sender, RoutedEventArgs e) + { + if(sender is Button button && button.Tag is int machineId && DataContext is GpuDetailViewModel vm) + _ = vm.SelectMachineCommand.ExecuteAsync(machineId); + } + + private void Console_Click(object sender, RoutedEventArgs e) + { + if(sender is Button button && button.Tag is int machineId && DataContext is GpuDetailViewModel vm) + _ = vm.SelectMachineCommand.ExecuteAsync(machineId); + } +} \ No newline at end of file diff --git a/Marechai.App/Services/GpusService.cs b/Marechai.App/Services/GpusService.cs index 3b59a507..2af01e40 100644 --- a/Marechai.App/Services/GpusService.cs +++ b/Marechai.App/Services/GpusService.cs @@ -72,4 +72,91 @@ public class GpusService return null; } } + + /// + /// Fetches resolutions supported by a GPU + /// + public async Task> GetResolutionsByGpuAsync(int gpuId) + { + try + { + _logger.LogInformation("Fetching resolutions for GPU {GpuId}", gpuId); + + // Fetch from the resolutions-by-gpu/gpus/{gpuId}/resolutions endpoint + List resolutions = await _apiClient.ResolutionsByGpu.Gpus[gpuId].Resolutions.GetAsync(); + + if(resolutions == null) return []; + + _logger.LogInformation("Successfully fetched {Count} resolutions for GPU {GpuId}", + resolutions.Count, + gpuId); + + return resolutions; + } + catch(Exception ex) + { + _logger.LogWarning(ex, "Error fetching resolutions for GPU {GpuId}", gpuId); + + return []; + } + } + + /// + /// Fetches machines that use a specific GPU + /// + public async Task> GetMachinesByGpuAsync(int gpuId) + { + try + { + _logger.LogInformation("Fetching machines for GPU {GpuId}", gpuId); + + // Fetch from the gpus/{gpuId}/machines endpoint + List machines = await _apiClient.Gpus[gpuId].Machines.GetAsync(); + + if(machines == null) return []; + + _logger.LogInformation("Successfully fetched {Count} machines for GPU {GpuId}", machines.Count, gpuId); + + return machines; + } + catch(Exception ex) + { + _logger.LogWarning(ex, "Error fetching machines for GPU {GpuId}", gpuId); + + return []; + } + } + + /// + /// Fetches a single resolution by ID from the API + /// + public async Task GetResolutionByIdAsync(int resolutionId) + { + try + { + _logger.LogInformation("Fetching resolution {ResolutionId} from API", resolutionId); + + ResolutionDto? resolution = await _apiClient.Resolutions[resolutionId].GetAsync(); + + if(resolution == null) + { + _logger.LogWarning("Resolution {ResolutionId} not found", resolutionId); + + return null; + } + + _logger.LogInformation("Successfully fetched resolution {ResolutionId}: {Width}x{Height}", + resolutionId, + resolution.Width, + resolution.Height); + + return resolution; + } + catch(Exception ex) + { + _logger.LogWarning(ex, "Error fetching resolution {ResolutionId} from API", resolutionId); + + return null; + } + } } \ No newline at end of file