/****************************************************************************** // 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 *******************************************************************************/ #nullable enable using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; using System.Threading.Tasks; using Windows.Storage.Streams; using Humanizer; using Marechai.App.Helpers; using Marechai.App.Presentation.Models; using Marechai.App.Services; using Marechai.App.Services.Caching; using Marechai.Data; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Media; using Microsoft.UI.Xaml.Media.Imaging; using Uno.Extensions.Navigation; namespace Marechai.App.Presentation.ViewModels; public partial class MachineViewViewModel : ObservableObject { private readonly ComputersService _computersService; private readonly ILogger _logger; private readonly INavigator _navigator; private readonly MachinePhotoCache _photoCache; [ObservableProperty] private string _companyName = string.Empty; [ObservableProperty] private string _errorMessage = string.Empty; [ObservableProperty] private string? _familyName; [ObservableProperty] private bool _hasError; [ObservableProperty] private string? _introductionDateDisplay; [ObservableProperty] private bool _isDataLoaded; [ObservableProperty] private bool _isLoading; [ObservableProperty] private bool _isPrototype; [ObservableProperty] private string _machineName = string.Empty; [ObservableProperty] private string? _modelName; private object? _navigationSource; [ObservableProperty] private Visibility _showFamily = Visibility.Collapsed; [ObservableProperty] private Visibility _showFamilyOrModel = Visibility.Collapsed; [ObservableProperty] private Visibility _showGpus = Visibility.Collapsed; [ObservableProperty] private Visibility _showIntroductionDate = Visibility.Collapsed; [ObservableProperty] private Visibility _showMemory = Visibility.Collapsed; [ObservableProperty] private Visibility _showModel = Visibility.Collapsed; [ObservableProperty] private Visibility _showPhotos = Visibility.Collapsed; [ObservableProperty] private Visibility _showProcessors = Visibility.Collapsed; [ObservableProperty] private Visibility _showSoundSynthesizers = Visibility.Collapsed; [ObservableProperty] private Visibility _showStorage = Visibility.Collapsed; public MachineViewViewModel(ILogger logger, INavigator navigator, ComputersService computersService, MachinePhotoCache photoCache) { _logger = logger; _navigator = navigator; _computersService = computersService; _photoCache = photoCache; } public ObservableCollection Processors { get; } = []; public ObservableCollection Memory { get; } = []; public ObservableCollection Gpus { get; } = []; public ObservableCollection SoundSynthesizers { get; } = []; public ObservableCollection Storage { get; } = []; public ObservableCollection Photos { get; } = []; [RelayCommand] public async Task GoBack() { // If we came from News, navigate back to News if(_navigationSource is NewsViewModel) { await _navigator.NavigateViewModelAsync(this); return; } // If we came from CompanyDetailViewModel, navigate back to company details if(_navigationSource is CompanyDetailViewModel companyVm) { var navParam = new CompanyDetailNavigationParameter { CompanyId = companyVm.CompanyId }; await _navigator.NavigateViewModelAsync(this, data: navParam); return; } // If we came from ConsolesListViewModel, navigate back to consoles list if(_navigationSource is ConsolesListViewModel) { await _navigator.NavigateViewModelAsync(this); return; } // If we came from ComputersListViewModel, navigate back to computers list if(_navigationSource is ComputersListViewModel) { await _navigator.NavigateViewModelAsync(this); return; } // Otherwise, try to go back in the navigation stack await _navigator.GoBack(this); } /// /// Sets the navigation source (where we came from). /// public void SetNavigationSource(object? source) { _navigationSource = source; } [RelayCommand] public Task LoadData() { // Placeholder for retry functionality HasError = false; ErrorMessage = string.Empty; return Task.CompletedTask; } public async Task LoadMachineAsync(int machineId) { try { IsLoading = true; IsDataLoaded = false; HasError = false; ErrorMessage = string.Empty; Processors.Clear(); Memory.Clear(); Gpus.Clear(); SoundSynthesizers.Clear(); Storage.Clear(); Photos.Clear(); _logger.LogInformation("Loading machine {MachineId}", machineId); // Fetch machine data from API MachineDto? machine = await _computersService.GetMachineByIdAsync(machineId); if(machine is null) { HasError = true; ErrorMessage = "Machine not found"; IsLoading = false; return; } // Populate basic information MachineName = machine.Name ?? string.Empty; CompanyName = machine.Company ?? string.Empty; FamilyName = machine.FamilyName; ModelName = machine.Model; // Check if this is a prototype (year 1000 is used as placeholder for prototypes) IsPrototype = machine.Introduced?.Year == 1000; // Set introduction date if available and not a prototype if(machine.Introduced.HasValue && machine.Introduced.Value.Year != 1000) IntroductionDateDisplay = machine.Introduced.Value.ToString("MMMM d, yyyy"); // Populate processors if(machine.Processors != null) { foreach(ProcessorDto processor in machine.Processors) { var details = new List(); int speed = UntypedNodeExtractor.ExtractInt(processor.Speed); int gprSize = UntypedNodeExtractor.ExtractInt(processor.GprSize); int cores = UntypedNodeExtractor.ExtractInt(processor.Cores); if(speed > 0) details.Add($"{speed} MHz"); if(gprSize > 0) details.Add($"{gprSize} bits"); if(cores > 1) details.Add($"{cores} cores"); Processors.Add(new ProcessorDisplayItem { DisplayName = processor.Name ?? string.Empty, Manufacturer = processor.Company ?? string.Empty, HasDetails = details.Count > 0, DetailsText = string.Join(", ", details) }); } } // Populate memory if(machine.Memory != null) { foreach(MemoryDto mem in machine.Memory) { long size = UntypedNodeExtractor.ExtractLong(mem.Size); string sizeStr = size > 0 ? size > 1024 ? $"{size} bytes ({size.Bytes().Humanize()})" : $"{size} bytes" : "Unknown"; // Get humanized memory usage description string usageDescription = mem.Usage.HasValue ? ((MemoryUsage)mem.Usage.Value).Humanize() : "Unknown"; Memory.Add(new MemoryDisplayItem { SizeDisplay = sizeStr, TypeDisplay = usageDescription }); } } // Populate GPUs if(machine.Gpus != null) { foreach(GpuDto gpu in machine.Gpus) { Gpus.Add(new GpuDisplayItem { DisplayName = gpu.Name ?? string.Empty, Manufacturer = gpu.Company ?? string.Empty, HasManufacturer = !string.IsNullOrEmpty(gpu.Company) }); } } // Populate sound synthesizers if(machine.SoundSynthesizers != null) { foreach(SoundSynthDto synth in machine.SoundSynthesizers) { var details = new List(); int voices = UntypedNodeExtractor.ExtractInt(synth.Voices); if(voices > 0) details.Add($"{voices} voices"); SoundSynthesizers.Add(new SoundSynthesizerDisplayItem { DisplayName = synth.Name ?? string.Empty, HasDetails = details.Count > 0, DetailsText = string.Join(", ", details) }); } } // Populate storage if(machine.Storage != null) { foreach(StorageDto storage in machine.Storage) { long capacity = UntypedNodeExtractor.ExtractLong(storage.Capacity); string displayText = capacity > 0 ? capacity > 1024 ? $"{capacity} bytes ({capacity.Bytes().Humanize()})" : $"{capacity} bytes" : "Storage"; // Get humanized storage type description string typeNote = storage.Type.HasValue ? ((StorageType)storage.Type.Value).Humanize() : "Unknown"; Storage.Add(new StorageDisplayItem { DisplayText = displayText, TypeNote = typeNote }); } } // Populate photos List 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(); IsDataLoaded = true; IsLoading = false; } catch(Exception ex) { _logger.LogError(ex, "Error loading machine {MachineId}", machineId); HasError = true; ErrorMessage = ex.Message; IsLoading = false; } } private void UpdateVisibilities() { ShowIntroductionDate = !string.IsNullOrEmpty(IntroductionDateDisplay) ? Visibility.Visible : Visibility.Collapsed; ShowFamily = !string.IsNullOrEmpty(FamilyName) ? Visibility.Visible : Visibility.Collapsed; ShowModel = !string.IsNullOrEmpty(ModelName) ? Visibility.Visible : Visibility.Collapsed; ShowFamilyOrModel = ShowFamily == Visibility.Visible || ShowModel == Visibility.Visible ? Visibility.Visible : Visibility.Collapsed; ShowProcessors = Processors.Count > 0 ? Visibility.Visible : Visibility.Collapsed; ShowMemory = Memory.Count > 0 ? Visibility.Visible : Visibility.Collapsed; ShowGpus = Gpus.Count > 0 ? Visibility.Visible : Visibility.Collapsed; ShowSoundSynthesizers = SoundSynthesizers.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); } } } /// /// Display item for processor information /// public class ProcessorDisplayItem { public string DisplayName { get; set; } = string.Empty; public string Manufacturer { get; set; } = string.Empty; public bool HasDetails { get; set; } public string DetailsText { get; set; } = string.Empty; } /// /// Display item for memory information /// public class MemoryDisplayItem { public string SizeDisplay { get; set; } = string.Empty; public string TypeDisplay { get; set; } = string.Empty; } /// /// Display item for GPU information /// public class GpuDisplayItem { public string DisplayName { get; set; } = string.Empty; public string Manufacturer { get; set; } = string.Empty; public bool HasManufacturer { get; set; } } /// /// Display item for sound synthesizer information /// public class SoundSynthesizerDisplayItem { public string DisplayName { get; set; } = string.Empty; public bool HasDetails { get; set; } public string DetailsText { get; set; } = string.Empty; } /// /// Display item for storage information /// public class StorageDisplayItem { public string DisplayText { get; set; } = string.Empty; public string TypeNote { get; set; } = string.Empty; } /// /// Display item for photo carousel /// public class PhotoCarouselDisplayItem { // Thumbnail constraints public const int ThumbnailMaxSize = 256; public Guid PhotoId { get; set; } public ImageSource? ThumbnailImageSource { get; set; } }