Add processors list and details pages.

This commit is contained in:
2025-11-16 16:09:20 +00:00
parent 5b709755c7
commit 733dc59f7b
10 changed files with 1370 additions and 0 deletions

View File

@@ -20,6 +20,8 @@ using MachineViewViewModel = Marechai.App.Presentation.ViewModels.MachineViewVie
using MainViewModel = Marechai.App.Presentation.ViewModels.MainViewModel;
using NewsViewModel = Marechai.App.Presentation.ViewModels.NewsViewModel;
using PhotoDetailViewModel = Marechai.App.Presentation.ViewModels.PhotoDetailViewModel;
using ProcessorDetailViewModel = Marechai.App.Presentation.ViewModels.ProcessorDetailViewModel;
using ProcessorsListViewModel = Marechai.App.Presentation.ViewModels.ProcessorsListViewModel;
namespace Marechai.App;
@@ -125,6 +127,7 @@ public partial class App : Application
services.AddSingleton<CompanyDetailViewModel>();
services.AddSingleton<MachineViewViewModel>();
services.AddSingleton<GpusService>();
services.AddSingleton<ProcessorsService>();
services.AddTransient<PhotoDetailViewModel>();
services
@@ -139,6 +142,8 @@ public partial class App : Application
services.AddTransient<ConsolesListViewModel>();
services.AddTransient<GpuListViewModel>();
services.AddTransient<GpuDetailViewModel>();
services.AddTransient<ProcessorsListViewModel>();
services.AddTransient<ProcessorDetailViewModel>();
})
.UseNavigation(RegisterRoutes));
@@ -167,6 +172,8 @@ public partial class App : Application
new ViewMap<PhotoDetailPage, PhotoDetailViewModel>(),
new ViewMap<GpuListPage, GpuListViewModel>(),
new ViewMap<GpuDetailPage, GpuDetailViewModel>(),
new ViewMap<ProcessorListPage, ProcessorsListViewModel>(),
new ViewMap<ProcessorDetailPage, ProcessorDetailViewModel>(),
new DataViewMap<SecondPage, SecondViewModel, Entity>());
routes.Register(new RouteMap("",
@@ -216,6 +223,14 @@ public partial class App : Application
views.FindByViewModel<
GpuDetailViewModel>())
]),
new RouteMap("processors",
views.FindByViewModel<ProcessorsListViewModel>(),
Nested:
[
new RouteMap("processor-details",
views.FindByViewModel<
ProcessorDetailViewModel>())
]),
new RouteMap("Second",
views.FindByViewModel<SecondViewModel>())
])

View File

@@ -0,0 +1,35 @@
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ 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 <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
namespace Marechai.App.Presentation.Models;
/// <summary>
/// Navigation parameter for the ProcessorDetailPage containing both the processor ID and the navigation source.
/// </summary>
public class ProcessorDetailNavigationParameter
{
public required int ProcessorId { get; init; }
public object? NavigationSource { get; init; }
}

View File

@@ -181,6 +181,20 @@ public partial class MachineViewViewModel : ObservableObject
return;
}
// If we came from ProcessorDetailViewModel, navigate back to processor details
if(_navigationSource is ProcessorDetailViewModel processorDetailVm)
{
var navParam = new ProcessorDetailNavigationParameter
{
ProcessorId = processorDetailVm.ProcessorId,
NavigationSource = this
};
await _navigator.NavigateViewModelAsync<ProcessorDetailViewModel>(this, data: navParam);
return;
}
// Otherwise, try to go back in the navigation stack
await _navigator.GoBack(this);
}

View File

@@ -0,0 +1,294 @@
#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 ProcessorDetailViewModel : ObservableObject
{
private readonly CompaniesService _companiesService;
private readonly IStringLocalizer _localizer;
private readonly ILogger<ProcessorDetailViewModel> _logger;
private readonly INavigator _navigator;
private readonly ProcessorsService _processorsService;
[ObservableProperty]
private ObservableCollection<MachineItem> _computers = [];
[ObservableProperty]
private string _computersFilterText = string.Empty;
[ObservableProperty]
private string _consoelsFilterText = string.Empty;
[ObservableProperty]
private ObservableCollection<MachineItem> _consoles = [];
[ObservableProperty]
private string _errorMessage = string.Empty;
[ObservableProperty]
private ObservableCollection<MachineItem> _filteredComputers = [];
[ObservableProperty]
private ObservableCollection<MachineItem> _filteredConsoles = [];
[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 ProcessorDto? _processor;
[ObservableProperty]
private int _processorId;
public ProcessorDetailViewModel(ProcessorsService processorsService, CompaniesService companiesService,
IStringLocalizer localizer, ILogger<ProcessorDetailViewModel> logger,
INavigator navigator)
{
_processorsService = processorsService;
_companiesService = companiesService;
_localizer = localizer;
_logger = logger;
_navigator = navigator;
LoadData = new AsyncRelayCommand(LoadDataAsync);
GoBackCommand = new AsyncRelayCommand(GoBackAsync);
SelectMachineCommand = new AsyncRelayCommand<int>(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; } = "Processor Details";
/// <summary>
/// Loads Processor details
/// </summary>
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
Computers.Clear();
Consoles.Clear();
if(ProcessorId <= 0)
{
ErrorMessage = _localizer["Invalid Processor ID"].Value;
HasError = true;
return;
}
_logger.LogInformation("Loading Processor details for ID: {ProcessorId}", ProcessorId);
// Load Processor details
Processor = await _processorsService.GetProcessorByIdAsync(ProcessorId);
if(Processor is null)
{
ErrorMessage = _localizer["Processor not found"].Value;
HasError = true;
return;
}
// Set manufacturer name (from Company field or fetch by CompanyId if empty)
ManufacturerName = Processor.Company ?? string.Empty;
if(string.IsNullOrEmpty(ManufacturerName) && Processor.CompanyId.HasValue)
{
try
{
CompanyDto? company = await _companiesService.GetCompanyByIdAsync(Processor.CompanyId.Value);
if(company != null) ManufacturerName = company.Name ?? string.Empty;
}
catch(Exception ex)
{
_logger.LogWarning(ex, "Failed to load company for Processor {ProcessorId}", ProcessorId);
}
}
_logger.LogInformation("Processor loaded: {Name}, Company: {Company}", Processor.Name, ManufacturerName);
// Load machines and separate into computers and consoles
try
{
List<MachineDto>? machines = await _processorsService.GetMachinesByProcessorAsync(ProcessorId);
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 Processor {ProcessorId}",
Computers.Count,
Consoles.Count,
ProcessorId);
}
else
{
HasComputers = false;
HasConsoles = false;
}
}
catch(Exception ex)
{
_logger.LogWarning(ex, "Failed to load machines for Processor {ProcessorId}", ProcessorId);
HasComputers = false;
HasConsoles = false;
}
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading Processor details: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load processor details. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
/// <summary>
/// Navigates back to the Processor list
/// </summary>
private async Task GoBackAsync()
{
// If we came from a machine view, go back to machine view
if(_navigationSource is MachineViewViewModel machineVm)
{
await _navigator.NavigateViewModelAsync<MachineViewViewModel>(this);
return;
}
// Default: go back to Processor list
await _navigator.NavigateViewModelAsync<ProcessorsListViewModel>(this);
}
/// <summary>
/// Filters computers based on search text
/// </summary>
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);
}
}
/// <summary>
/// Filters consoles based on search text
/// </summary>
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);
}
}
/// <summary>
/// Navigates to machine detail view
/// </summary>
private async Task SelectMachineAsync(int machineId)
{
if(machineId <= 0) return;
var navParam = new MachineViewNavigationParameter
{
MachineId = machineId,
NavigationSource = this
};
await _navigator.NavigateViewModelAsync<MachineViewViewModel>(this, data: navParam);
}
/// <summary>
/// Sets the navigation source (where we came from).
/// </summary>
public void SetNavigationSource(object? source)
{
_navigationSource = source;
}
}

View File

@@ -0,0 +1,177 @@
#nullable enable
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;
namespace Marechai.App.Presentation.ViewModels;
/// <summary>
/// ViewModel for displaying a list of Processors
/// </summary>
public partial class ProcessorsListViewModel : ObservableObject
{
private readonly IStringLocalizer _localizer;
private readonly ILogger<ProcessorsListViewModel> _logger;
private readonly INavigator _navigator;
private readonly ProcessorsService _processorsService;
[ObservableProperty]
private string _errorMessage = string.Empty;
[ObservableProperty]
private bool _hasError;
[ObservableProperty]
private bool _isDataLoaded;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _pageTitle = string.Empty;
[ObservableProperty]
private ObservableCollection<ProcessorListItem> _processorsList = [];
public ProcessorsListViewModel(ProcessorsService processorsService, IStringLocalizer localizer,
ILogger<ProcessorsListViewModel> logger, INavigator navigator)
{
_processorsService = processorsService;
_localizer = localizer;
_logger = logger;
_navigator = navigator;
LoadData = new AsyncRelayCommand(LoadDataAsync);
NavigateToProcessorCommand = new AsyncRelayCommand<ProcessorListItem>(NavigateToProcessorAsync);
}
public IAsyncRelayCommand LoadData { get; }
public IAsyncRelayCommand<ProcessorListItem> NavigateToProcessorCommand { get; }
/// <summary>
/// Loads all Processors and sorts them alphabetically
/// </summary>
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
ProcessorsList.Clear();
_logger.LogInformation("LoadDataAsync called for Processors");
PageTitle = _localizer["Processors"];
// Load Processors from the API
await LoadProcessorsFromApiAsync();
_logger.LogInformation("LoadProcessorsFromApiAsync completed. ProcessorsList.Count={Count}",
ProcessorsList.Count);
if(ProcessorsList.Count == 0)
{
ErrorMessage = _localizer["No processors found"].Value;
HasError = true;
_logger.LogWarning("No Processors found");
}
else
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading Processors: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load processors. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
/// <summary>
/// Loads Processors from the API and sorts them alphabetically
/// </summary>
private async Task LoadProcessorsFromApiAsync()
{
try
{
List<ProcessorDto> processors = await _processorsService.GetAllProcessorsAsync();
if(processors == null || processors.Count == 0)
{
_logger.LogInformation("No Processors returned from API");
return;
}
var processorItems = new List<ProcessorListItem>();
foreach(ProcessorDto processor in processors)
{
var processorItem = new ProcessorListItem
{
Id = processor.Id ?? 0,
Name = processor.Name ?? string.Empty,
Company = processor.Company ?? string.Empty
};
processorItems.Add(processorItem);
_logger.LogInformation("Processor: {Name}, Company: {Company}, ID: {Id}",
processor.Name,
processor.Company,
processor.Id);
}
// Sort processors alphabetically
processorItems.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
// Add all processors to the list
foreach(ProcessorListItem processor in processorItems) ProcessorsList.Add(processor);
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading Processors from API");
}
}
/// <summary>
/// Navigates to the Processor detail view
/// </summary>
private async Task NavigateToProcessorAsync(ProcessorListItem? processor)
{
if(processor is null) return;
_logger.LogInformation("Navigating to Processor detail: {ProcessorName} (ID: {ProcessorId})",
processor.Name,
processor.Id);
// Navigate to Processor detail view with navigation parameter
var navParam = new ProcessorDetailNavigationParameter
{
ProcessorId = processor.Id,
NavigationSource = this
};
await _navigator.NavigateViewModelAsync<ProcessorDetailViewModel>(this, data: navParam);
}
}
/// <summary>
/// Data model for a Processor in the list
/// </summary>
public class ProcessorListItem
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Company { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,366 @@
<?xml version="1.0" encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.ProcessorDetailPage"
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="{Binding Path=Title}"
MainCommandMode="Action">
<utu:NavigationBar.MainCommand>
<AppBarButton Icon="Back"
Label="Back"
Command="{Binding GoBackCommand}"
AutomationProperties.Name="Go back" />
</utu:NavigationBar.MainCommand>
</utu:NavigationBar>
<!-- Content -->
<ScrollViewer Grid.Row="1">
<StackPanel Padding="16"
Spacing="16">
<!-- Loading State -->
<StackPanel Visibility="{Binding IsLoading}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="32"
Spacing="16">
<ProgressRing IsActive="True"
IsIndeterminate="True"
Height="48"
Width="48" />
<TextBlock Text="Loading..."
TextAlignment="Center"
FontSize="14" />
</StackPanel>
<!-- Error State -->
<StackPanel Visibility="{Binding HasError}"
Padding="16"
Background="{ThemeResource SystemErrorBackgroundColor}"
CornerRadius="8"
Spacing="8">
<TextBlock Text="Error"
FontWeight="Bold"
Foreground="{ThemeResource SystemErrorTextForegroundColor}" />
<TextBlock Text="{Binding ErrorMessage}"
Foreground="{ThemeResource SystemErrorTextForegroundColor}"
TextWrapping="Wrap" />
</StackPanel>
<!-- Processor Details -->
<StackPanel Visibility="{Binding IsDataLoaded}"
Spacing="16">
<!-- Processor Name -->
<TextBlock Text="{Binding Processor.Name}"
FontSize="28"
FontWeight="Bold"
TextWrapping="Wrap" />
<!-- Company/Manufacturer -->
<StackPanel Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Manufacturer"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding ManufacturerName}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Model Code -->
<StackPanel Visibility="{Binding Processor.ModelCode, Converter={StaticResource StringToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Model Code"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Processor.ModelCode}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Introduced Date -->
<StackPanel Visibility="{Binding Processor.Introduced, Converter={StaticResource ObjectToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Introduced"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Processor.Introduced}"
FontSize="14" />
</StackPanel>
<!-- Speed -->
<StackPanel Visibility="{Binding Processor.Speed, Converter={StaticResource ObjectToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Speed (MHz)"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Processor.Speed}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Package -->
<StackPanel Visibility="{Binding Processor.Package, Converter={StaticResource StringToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Package"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Processor.Package}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Cores -->
<StackPanel Visibility="{Binding Processor.Cores, Converter={StaticResource ObjectToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Cores"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Processor.Cores}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Threads Per Core -->
<StackPanel Visibility="{Binding Processor.ThreadsPerCore, Converter={StaticResource ObjectToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Threads Per Core"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Processor.ThreadsPerCore}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Process -->
<StackPanel Visibility="{Binding Processor.Process, Converter={StaticResource StringToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Process"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Processor.Process}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Process (nm) -->
<StackPanel Visibility="{Binding Processor.ProcessNm, Converter={StaticResource StringToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Process (nm)"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Processor.ProcessNm}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Die Size -->
<StackPanel Visibility="{Binding Processor.DieSize, Converter={StaticResource ObjectToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Die Size (mm²)"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Processor.DieSize}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Transistors -->
<StackPanel Visibility="{Binding Processor.Transistors, Converter={StaticResource ObjectToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Transistors"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Processor.Transistors}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Computers Section -->
<StackPanel Visibility="{Binding HasComputers}"
Spacing="8">
<StackPanel Orientation="Horizontal"
Spacing="8">
<TextBlock Text="Computers"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Computers.Count}"
FontSize="14"
FontWeight="Bold"
Foreground="{ThemeResource SystemAccentColor}" />
</StackPanel>
<!-- Filter Box -->
<AutoSuggestBox PlaceholderText="Filter computers..."
Text="{Binding ComputersFilterText, Mode=TwoWay}"
TextChanged="ComputersSearchBox_TextChanged"
BorderThickness="1"
BorderBrush="{ThemeResource ControlElevationBorderBrush}" />
<!-- Scrollable Computers List -->
<ScrollViewer Height="200"
BorderThickness="1"
BorderBrush="{ThemeResource ControlElevationBorderBrush}"
CornerRadius="8">
<ItemsRepeater ItemsSource="{Binding FilteredComputers}"
Margin="0">
<ItemsRepeater.Layout>
<StackLayout Spacing="4" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Click="Computer_Click"
Tag="{Binding Id}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="12"
Margin="0,4">
<StackPanel Spacing="4"
HorizontalAlignment="Left">
<TextBlock Text="{Binding Name}"
FontSize="14"
FontWeight="SemiBold" />
<StackPanel Orientation="Horizontal"
Spacing="8">
<TextBlock Text="{Binding Manufacturer}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding YearDisplay}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
</StackPanel>
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</StackPanel>
<!-- Consoles Section -->
<StackPanel Visibility="{Binding HasConsoles}"
Spacing="8">
<StackPanel Orientation="Horizontal"
Spacing="8">
<TextBlock Text="Consoles"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Consoles.Count}"
FontSize="14"
FontWeight="Bold"
Foreground="{ThemeResource SystemAccentColor}" />
</StackPanel>
<!-- Filter Box -->
<AutoSuggestBox PlaceholderText="Filter consoles..."
Text="{Binding ConsoelsFilterText, Mode=TwoWay}"
TextChanged="ConsolesSearchBox_TextChanged"
BorderThickness="1"
BorderBrush="{ThemeResource ControlElevationBorderBrush}" />
<!-- Scrollable Consoles List -->
<ScrollViewer Height="200"
BorderThickness="1"
BorderBrush="{ThemeResource ControlElevationBorderBrush}"
CornerRadius="8">
<ItemsRepeater ItemsSource="{Binding FilteredConsoles}"
Margin="0">
<ItemsRepeater.Layout>
<StackLayout Spacing="4" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Click="Console_Click"
Tag="{Binding Id}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="12"
Margin="0,4">
<StackPanel Spacing="4"
HorizontalAlignment="Left">
<TextBlock Text="{Binding Name}"
FontSize="14"
FontWeight="SemiBold" />
<StackPanel Orientation="Horizontal"
Spacing="8">
<TextBlock Text="{Binding Manufacturer}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding YearDisplay}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
</StackPanel>
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -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;
/// <summary>
/// Processor detail page showing all information about a specific processor
/// </summary>
public sealed partial class ProcessorDetailPage : Page
{
private object? _navigationSource;
private int? _pendingProcessorId;
public ProcessorDetailPage()
{
InitializeComponent();
DataContextChanged += ProcessorDetailPage_DataContextChanged;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
int? processorId = null;
// Handle both int and ProcessorDetailNavigationParameter
if(e.Parameter is int intId)
processorId = intId;
else if(e.Parameter is ProcessorDetailNavigationParameter navParam)
{
processorId = navParam.ProcessorId;
_navigationSource = navParam.NavigationSource;
}
if(processorId.HasValue)
{
_pendingProcessorId = processorId;
if(DataContext is ProcessorDetailViewModel viewModel)
{
viewModel.ProcessorId = processorId.Value;
if(_navigationSource != null) viewModel.SetNavigationSource(_navigationSource);
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
}
private void ProcessorDetailPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(_pendingProcessorId.HasValue && DataContext is ProcessorDetailViewModel viewModel)
{
viewModel.ProcessorId = _pendingProcessorId.Value;
if(_navigationSource != null) viewModel.SetNavigationSource(_navigationSource);
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
private void ComputersSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
if(DataContext is ProcessorDetailViewModel vm) vm.ComputersFilterCommand.Execute(null);
}
private void ComputersSearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
if(DataContext is ProcessorDetailViewModel vm)
vm.ComputersFilterCommand.Execute(null);
}
private void ConsolesSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
if(DataContext is ProcessorDetailViewModel vm) vm.ConsolesFilterCommand.Execute(null);
}
private void ConsolesSearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
if(DataContext is ProcessorDetailViewModel 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 ProcessorDetailViewModel 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 ProcessorDetailViewModel vm)
_ = vm.SelectMachineCommand.ExecuteAsync(machineId);
}
}

View File

@@ -0,0 +1,207 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.ProcessorListPage"
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="Processors">
</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 processors..."
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 Processors"
Message="{Binding ErrorMessage}"
IsClosable="False" />
<Button Content="Retry"
Command="{Binding LoadData}"
HorizontalAlignment="Center"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<!-- Processors 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 ProcessorsList.Count}" />
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}"
Text="processors" />
</StackPanel>
<!-- Processors List -->
<ItemsControl Grid.Row="1"
ItemsSource="{Binding ProcessorsList}"
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.NavigateToProcessorCommand, 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>
<!-- Processor 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 processors.
/// Features responsive layout and modern styling.
/// </summary>
public sealed partial class ProcessorListPage : Page
{
public ProcessorListPage()
{
InitializeComponent();
Loaded += ProcessorListPage_Loaded;
DataContextChanged += ProcessorListPage_DataContextChanged;
}
private void ProcessorListPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(DataContext is ProcessorsListViewModel vm)
{
// Load data when DataContext is set
vm.LoadData.Execute(null);
}
}
private void ProcessorListPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is ProcessorsListViewModel vm)
{
// Load data when page is loaded (fallback)
vm.LoadData.Execute(null);
}
}
}

View File

@@ -0,0 +1,127 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Marechai.App.Services;
/// <summary>
/// Service for fetching and managing Processors from the Marechai API
/// </summary>
public class ProcessorsService
{
private readonly ApiClient _apiClient;
private readonly ILogger<ProcessorsService> _logger;
public ProcessorsService(ApiClient apiClient, ILogger<ProcessorsService> logger)
{
_apiClient = apiClient;
_logger = logger;
}
/// <summary>
/// Fetches all Processors from the API
/// </summary>
public async Task<List<ProcessorDto>> GetAllProcessorsAsync()
{
try
{
_logger.LogInformation("Fetching all Processors from API");
List<ProcessorDto>? processors = await _apiClient.Processors.GetAsync();
if(processors == null) return [];
_logger.LogInformation("Successfully fetched {Count} total Processors", processors.Count);
return processors;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching all Processors from API");
return [];
}
}
/// <summary>
/// Fetches a single Processor by ID from the API
/// </summary>
public async Task<ProcessorDto?> GetProcessorByIdAsync(int processorId)
{
try
{
_logger.LogInformation("Fetching Processor {ProcessorId} from API", processorId);
ProcessorDto? processor = await _apiClient.Processors[processorId].GetAsync();
if(processor == null)
{
_logger.LogWarning("Processor {ProcessorId} not found", processorId);
return null;
}
_logger.LogInformation("Successfully fetched Processor {ProcessorId}: {ProcessorName}",
processorId,
processor.Name);
return processor;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching Processor {ProcessorId} from API", processorId);
return null;
}
}
/// <summary>
/// Fetches machines that use a specific Processor
/// </summary>
public async Task<List<MachineDto>> GetMachinesByProcessorAsync(int processorId)
{
try
{
_logger.LogInformation("Fetching machines for Processor {ProcessorId}", processorId);
// Fetch from the processors-by-machine/by-processor/{processorId} endpoint
List<ProcessorByMachineDto>? processorMachineRelationships =
await _apiClient.ProcessorsByMachine.ByProcessor[processorId].GetAsync();
if(processorMachineRelationships == null || processorMachineRelationships.Count == 0) return [];
// Fetch full machine details for each to get Type information
var machines = new List<MachineDto>();
foreach(ProcessorByMachineDto pm in processorMachineRelationships)
{
if(pm.MachineId.HasValue)
{
try
{
MachineDto? machine = await _apiClient.Machines[pm.MachineId.Value].GetAsync();
if(machine != null) machines.Add(machine);
}
catch(Exception ex)
{
_logger.LogWarning(ex, "Failed to fetch machine {MachineId}", pm.MachineId);
}
}
}
_logger.LogInformation("Successfully fetched {Count} machines for Processor {ProcessorId}",
machines.Count,
processorId);
return machines;
}
catch(Exception ex)
{
_logger.LogWarning(ex, "Error fetching machines for Processor {ProcessorId}", processorId);
return [];
}
}
}