Reorganize project structure.

This commit is contained in:
2025-11-15 02:46:53 +00:00
parent 3e4677b084
commit 5d249f435e
25 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,249 @@
#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.Services;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation;
/// <summary>
/// ViewModel for displaying a filtered list of computers
/// </summary>
public partial class ComputersListViewModel : ObservableObject
{
private readonly ComputersService _computersService;
private readonly IComputersListFilterContext _filterContext;
private readonly IStringLocalizer _localizer;
private readonly ILogger<ComputersListViewModel> _logger;
private readonly INavigator _navigator;
[ObservableProperty]
private ObservableCollection<ComputerListItem> computersList = [];
[ObservableProperty]
private string errorMessage = string.Empty;
[ObservableProperty]
private string filterDescription = string.Empty;
[ObservableProperty]
private bool hasError;
[ObservableProperty]
private bool isDataLoaded;
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
private string pageTitle = string.Empty;
public ComputersListViewModel(ComputersService computersService, IStringLocalizer localizer,
ILogger<ComputersListViewModel> logger, INavigator navigator,
IComputersListFilterContext filterContext)
{
_computersService = computersService;
_localizer = localizer;
_logger = logger;
_navigator = navigator;
_filterContext = filterContext;
LoadData = new AsyncRelayCommand(LoadDataAsync);
GoBackCommand = new AsyncRelayCommand(GoBackAsync);
NavigateToComputerCommand = new AsyncRelayCommand<ComputerListItem>(NavigateToComputerAsync);
}
public IAsyncRelayCommand LoadData { get; }
public ICommand GoBackCommand { get; }
public IAsyncRelayCommand<ComputerListItem> NavigateToComputerCommand { get; }
/// <summary>
/// Gets or sets the filter type
/// </summary>
public ComputerListFilterType FilterType
{
get => _filterContext.FilterType;
set => _filterContext.FilterType = value;
}
/// <summary>
/// Gets or sets the filter value
/// </summary>
public string FilterValue
{
get => _filterContext.FilterValue;
set => _filterContext.FilterValue = value;
}
/// <summary>
/// Loads computers based on the current filter
/// </summary>
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
ComputersList.Clear();
_logger.LogInformation("LoadDataAsync called. FilterType={FilterType}, FilterValue={FilterValue}",
FilterType,
FilterValue);
// Update title and filter description based on filter type
UpdateFilterDescription();
// Load computers from the API based on the current filter
await LoadComputersFromApiAsync();
_logger.LogInformation("LoadComputersFromApiAsync completed. ComputersList.Count={Count}",
ComputersList.Count);
if(ComputersList.Count == 0)
{
ErrorMessage = _localizer["No computers found for this filter"].Value;
HasError = true;
_logger.LogWarning("No computers found for filter: {FilterType} {FilterValue}",
FilterType,
FilterValue);
}
else
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading computers: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load computers. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
/// <summary>
/// Updates the title and filter description based on the current filter
/// </summary>
private void UpdateFilterDescription()
{
switch(FilterType)
{
case ComputerListFilterType.All:
PageTitle = _localizer["All Computers"];
FilterDescription = _localizer["Browsing all computers in the database"];
break;
case ComputerListFilterType.Letter:
if(!string.IsNullOrEmpty(FilterValue) && FilterValue.Length == 1)
{
PageTitle = $"{_localizer["Computers Starting with"]} {FilterValue}";
FilterDescription = $"{_localizer["Showing computers that start with"]} {FilterValue}";
}
break;
case ComputerListFilterType.Year:
if(!string.IsNullOrEmpty(FilterValue) && int.TryParse(FilterValue, out int year))
{
PageTitle = $"{_localizer["Computers from"]} {year}";
FilterDescription = $"{_localizer["Showing computers released in"]} {year}";
}
break;
}
}
/// <summary>
/// Loads computers from the API based on the current filter
/// </summary>
private async Task LoadComputersFromApiAsync()
{
try
{
List<MachineDto> computers = FilterType switch
{
ComputerListFilterType.Letter when FilterValue.Length == 1 =>
await _computersService.GetComputersByLetterAsync(FilterValue[0]),
ComputerListFilterType.Year when int.TryParse(FilterValue, out int year) =>
await _computersService.GetComputersByYearAsync(year),
_ => await _computersService.GetAllComputersAsync()
};
// Add computers to the list sorted by name
foreach(MachineDto computer in computers.OrderBy(c => c.Name))
{
int year = computer.Introduced?.Year ?? 0;
int id = UntypedNodeExtractor.ExtractInt(computer.Id);
_logger.LogInformation("Computer: {Name}, Introduced: {Introduced}, Year: {Year}, Company: {Company}, ID: {Id}",
computer.Name,
computer.Introduced,
year,
computer.Company,
id);
ComputersList.Add(new ComputerListItem
{
Id = id,
Name = computer.Name ?? string.Empty,
Year = year,
Manufacturer = computer.Company ?? string.Empty
});
}
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading computers from API");
}
}
/// <summary>
/// Navigates back to the computers main view
/// </summary>
private async Task GoBackAsync()
{
await _navigator.NavigateViewModelAsync<ComputersViewModel>(this);
}
/// <summary>
/// Navigates to the computer detail view
/// </summary>
private async Task NavigateToComputerAsync(ComputerListItem? computer)
{
if(computer is null) return;
_logger.LogInformation("Navigating to computer detail: {ComputerName} (ID: {ComputerId})",
computer.Name,
computer.Id);
var navParam = new MachineViewNavigationParameter
{
MachineId = computer.Id,
NavigationSource = this
};
await _navigator.NavigateViewModelAsync<MachineViewViewModel>(this, data: navParam);
}
}
/// <summary>
/// Data model for a computer in the list
/// </summary>
public class ComputerListItem
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int Year { get; set; }
public string Manufacturer { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,207 @@
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;
using Marechai.App.Services;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation;
public partial class ComputersViewModel : ObservableObject
{
private readonly ComputersService _computersService;
private readonly IComputersListFilterContext _filterContext;
private readonly IStringLocalizer _localizer;
private readonly ILogger<ComputersViewModel> _logger;
private readonly INavigator _navigator;
[ObservableProperty]
private int computerCount;
[ObservableProperty]
private string computerCountText = string.Empty;
[ObservableProperty]
private string errorMessage = string.Empty;
[ObservableProperty]
private bool hasError;
[ObservableProperty]
private bool isDataLoaded;
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
private ObservableCollection<char> lettersList = [];
[ObservableProperty]
private int maximumYear;
[ObservableProperty]
private int minimumYear;
[ObservableProperty]
private string yearsGridTitle = string.Empty;
[ObservableProperty]
private ObservableCollection<int> yearsList = [];
public ComputersViewModel(ComputersService computersService, IStringLocalizer localizer,
ILogger<ComputersViewModel> logger, INavigator navigator,
IComputersListFilterContext filterContext)
{
_computersService = computersService;
_localizer = localizer;
_logger = logger;
_navigator = navigator;
_filterContext = filterContext;
LoadData = new AsyncRelayCommand(LoadDataAsync);
GoBackCommand = new AsyncRelayCommand(GoBackAsync);
NavigateByLetterCommand = new AsyncRelayCommand<char>(NavigateByLetterAsync);
NavigateByYearCommand = new AsyncRelayCommand<int>(NavigateByYearAsync);
NavigateAllComputersCommand = new AsyncRelayCommand(NavigateAllComputersAsync);
InitializeLetters();
}
public IAsyncRelayCommand LoadData { get; }
public ICommand GoBackCommand { get; }
public IAsyncRelayCommand<char> NavigateByLetterCommand { get; }
public IAsyncRelayCommand<int> NavigateByYearCommand { get; }
public IAsyncRelayCommand NavigateAllComputersCommand { get; }
public string Title { get; } = "Computers";
/// <summary>
/// Initializes the alphabet list (A-Z)
/// </summary>
private void InitializeLetters()
{
LettersList.Clear();
for(var c = 'A'; c <= 'Z'; c++) LettersList.Add(c);
}
/// <summary>
/// Loads computers count, minimum and maximum years from the API
/// </summary>
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
YearsList.Clear();
// Load all data in parallel for better performance
Task<int> countTask = _computersService.GetComputersCountAsync();
Task<int> minYearTask = _computersService.GetMinimumYearAsync();
Task<int> maxYearTask = _computersService.GetMaximumYearAsync();
await Task.WhenAll(countTask, minYearTask, maxYearTask);
ComputerCount = countTask.Result;
MinimumYear = minYearTask.Result;
MaximumYear = maxYearTask.Result;
// Update display text
ComputerCountText = _localizer["Computers in the database"];
// Generate years list
if(MinimumYear > 0 && MaximumYear > 0)
{
for(int year = MinimumYear; year <= MaximumYear; year++) YearsList.Add(year);
YearsGridTitle = $"Browse by Year ({MinimumYear} - {MaximumYear})";
}
if(ComputerCount == 0)
{
ErrorMessage = _localizer["No computers found"].Value;
HasError = true;
}
else
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError("Error loading computers data: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load computers data. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
/// <summary>
/// Handles back navigation
/// </summary>
private async Task GoBackAsync()
{
await _navigator.NavigateViewModelAsync<MainViewModel>(this);
}
/// <summary>
/// Navigates to computers filtered by letter
/// </summary>
private async Task NavigateByLetterAsync(char letter)
{
try
{
_logger.LogInformation("Navigating to computers by letter: {Letter}", letter);
_filterContext.FilterType = ComputerListFilterType.Letter;
_filterContext.FilterValue = letter.ToString();
await _navigator.NavigateViewModelAsync<ComputersListViewModel>(this);
}
catch(Exception ex)
{
_logger.LogError("Error navigating to letter computers: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to navigate. Please try again."].Value;
HasError = true;
}
}
/// <summary>
/// Navigates to computers filtered by year
/// </summary>
private async Task NavigateByYearAsync(int year)
{
try
{
_logger.LogInformation("Navigating to computers by year: {Year}", year);
_filterContext.FilterType = ComputerListFilterType.Year;
_filterContext.FilterValue = year.ToString();
await _navigator.NavigateViewModelAsync<ComputersListViewModel>(this);
}
catch(Exception ex)
{
_logger.LogError("Error navigating to year computers: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to navigate. Please try again."].Value;
HasError = true;
}
}
/// <summary>
/// Navigates to all computers view
/// </summary>
private async Task NavigateAllComputersAsync()
{
try
{
_logger.LogInformation("Navigating to all computers");
_filterContext.FilterType = ComputerListFilterType.All;
_filterContext.FilterValue = string.Empty;
await _navigator.NavigateViewModelAsync<ComputersListViewModel>(this);
}
catch(Exception ex)
{
_logger.LogError("Error navigating to all computers: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to navigate. Please try again."].Value;
HasError = true;
}
}
}

View File

@@ -0,0 +1,376 @@
/******************************************************************************
// 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
*******************************************************************************/
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Humanizer;
using Marechai.App.Services;
using Marechai.Data;
using Microsoft.UI.Xaml;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation;
public partial class MachineViewViewModel : ObservableObject
{
private readonly ComputersService _computersService;
private readonly ILogger<MachineViewViewModel> _logger;
private readonly INavigator _navigator;
private object? _navigationSource;
[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;
[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 showProcessors = Visibility.Collapsed;
[ObservableProperty]
private Visibility showSoundSynthesizers = Visibility.Collapsed;
[ObservableProperty]
private Visibility showStorage = Visibility.Collapsed;
public MachineViewViewModel(ILogger<MachineViewViewModel> logger, INavigator navigator,
ComputersService computersService)
{
_logger = logger;
_navigator = navigator;
_computersService = computersService;
}
public ObservableCollection<ProcessorDisplayItem> Processors { get; } = [];
public ObservableCollection<MemoryDisplayItem> Memory { get; } = [];
public ObservableCollection<GpuDisplayItem> Gpus { get; } = [];
public ObservableCollection<SoundSynthesizerDisplayItem> SoundSynthesizers { get; } = [];
public ObservableCollection<StorageDisplayItem> Storage { get; } = [];
[RelayCommand]
public async Task GoBack()
{
// If we came from News, navigate back to News
if(_navigationSource is NewsViewModel)
{
await _navigator.NavigateViewModelAsync<NewsViewModel>(this);
return;
}
// Otherwise, try to go back in the navigation stack
await _navigator.GoBack(this);
}
/// <summary>
/// Sets the navigation source (where we came from).
/// </summary>
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();
_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<string>();
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<string>();
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
});
}
}
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;
}
}
/// <summary>
/// Display item for processor information
/// </summary>
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;
}
/// <summary>
/// Display item for memory information
/// </summary>
public class MemoryDisplayItem
{
public string SizeDisplay { get; set; } = string.Empty;
public string TypeDisplay { get; set; } = string.Empty;
}
/// <summary>
/// Display item for GPU information
/// </summary>
public class GpuDisplayItem
{
public string DisplayName { get; set; } = string.Empty;
public string Manufacturer { get; set; } = string.Empty;
public bool HasManufacturer { get; set; }
}
/// <summary>
/// Display item for sound synthesizer information
/// </summary>
public class SoundSynthesizerDisplayItem
{
public string DisplayName { get; set; } = string.Empty;
public bool HasDetails { get; set; }
public string DetailsText { get; set; } = string.Empty;
}
/// <summary>
/// Display item for storage information
/// </summary>
public class StorageDisplayItem
{
public string DisplayText { get; set; } = string.Empty;
public string TypeNote { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Windows.Input;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation;
public partial class MainViewModel : ObservableObject
{
private readonly IStringLocalizer _localizer;
private readonly INavigator _navigator;
[ObservableProperty]
private bool isSidebarOpen = true;
[ObservableProperty]
private Dictionary<string, string> localizedStrings = new();
[ObservableProperty]
private string loginLogoutButtonText = "";
[ObservableProperty]
private string? name;
[ObservableProperty]
private NewsViewModel? newsViewModel;
[ObservableProperty]
private bool sidebarContentVisible = true;
public MainViewModel(IStringLocalizer localizer, IOptions<AppConfig> appInfo, INavigator navigator,
NewsViewModel newsViewModel)
{
_navigator = navigator;
_localizer = localizer;
NewsViewModel = newsViewModel;
Title = "Marechai";
Title += $" - {localizer["ApplicationName"]}";
if(appInfo?.Value?.Environment != null) Title += $" - {appInfo.Value.Environment}";
GoToSecond = new AsyncRelayCommand(GoToSecondView);
// Initialize localized strings
InitializeLocalizedStrings();
// Initialize commands
NavigateToNewsCommand = new AsyncRelayCommand(NavigateToMainAsync);
NavigateToBooksCommand = new AsyncRelayCommand(() => NavigateTo("books"));
NavigateToCompaniesCommand = new AsyncRelayCommand(() => NavigateTo("companies"));
NavigateToComputersCommand = new AsyncRelayCommand(() => NavigateTo("computers"));
NavigateToConsolesCommand = new AsyncRelayCommand(() => NavigateTo("consoles"));
NavigateToDocumentsCommand = new AsyncRelayCommand(() => NavigateTo("documents"));
NavigateToDumpsCommand = new AsyncRelayCommand(() => NavigateTo("dumps"));
NavigateToGraphicalProcessingUnitsCommand = new AsyncRelayCommand(() => NavigateTo("gpus"));
NavigateToMagazinesCommand = new AsyncRelayCommand(() => NavigateTo("magazines"));
NavigateToPeopleCommand = new AsyncRelayCommand(() => NavigateTo("people"));
NavigateToProcessorsCommand = new AsyncRelayCommand(() => NavigateTo("processors"));
NavigateToSoftwareCommand = new AsyncRelayCommand(() => NavigateTo("software"));
NavigateToSoundSynthesizersCommand = new AsyncRelayCommand(() => NavigateTo("soundsynthesizers"));
NavigateToSettingsCommand = new AsyncRelayCommand(() => NavigateTo("settings"));
LoginLogoutCommand = new RelayCommand(HandleLoginLogout);
ToggleSidebarCommand = new RelayCommand(() => IsSidebarOpen = !IsSidebarOpen);
UpdateLoginLogoutButtonText();
}
public string? Title { get; }
public ICommand GoToSecond { get; }
public ICommand NavigateToNewsCommand { get; }
public ICommand NavigateToBooksCommand { get; }
public ICommand NavigateToCompaniesCommand { get; }
public ICommand NavigateToComputersCommand { get; }
public ICommand NavigateToConsolesCommand { get; }
public ICommand NavigateToDocumentsCommand { get; }
public ICommand NavigateToDumpsCommand { get; }
public ICommand NavigateToGraphicalProcessingUnitsCommand { get; }
public ICommand NavigateToMagazinesCommand { get; }
public ICommand NavigateToPeopleCommand { get; }
public ICommand NavigateToProcessorsCommand { get; }
public ICommand NavigateToSoftwareCommand { get; }
public ICommand NavigateToSoundSynthesizersCommand { get; }
public ICommand NavigateToSettingsCommand { get; }
public ICommand LoginLogoutCommand { get; }
public ICommand ToggleSidebarCommand { get; }
private void InitializeLocalizedStrings()
{
LocalizedStrings = new Dictionary<string, string>
{
{
"News", _localizer["News"]
},
{
"Books", _localizer["Books"]
},
{
"Companies", _localizer["Companies"]
},
{
"Computers", _localizer["Computers"]
},
{
"Consoles", _localizer["Consoles"]
},
{
"Documents", _localizer["Documents"]
},
{
"Dumps", _localizer["Dumps"]
},
{
"GraphicalProcessingUnits", _localizer["GraphicalProcessingUnits"]
},
{
"Magazines", _localizer["Magazines"]
},
{
"People", _localizer["People"]
},
{
"Processors", _localizer["Processors"]
},
{
"Software", _localizer["Software"]
},
{
"SoundSynthesizers", _localizer["SoundSynthesizers"]
},
{
"Settings", _localizer["Settings"]
},
{
"Login", _localizer["Login"]
},
{
"Logout", _localizer["Logout"]
}
};
}
private void UpdateLoginLogoutButtonText()
{
// TODO: Check if user is logged in
// For now, always show "Login"
LoginLogoutButtonText = LocalizedStrings["Login"];
}
private static void HandleLoginLogout()
{
// TODO: Implement login/logout logic
}
private async Task NavigateTo(string destination)
{
try
{
// Navigate within the Main region using relative navigation
// The "./" prefix means navigate within the current page's region
await _navigator.NavigateRouteAsync(this, $"./{destination}");
}
catch(Exception)
{
// Navigation error - fail silently for now
// TODO: Add error handling/logging
}
}
private async Task NavigateToMainAsync()
{
// Navigate to News page (the default/home page)
await NavigateTo("News");
}
private async Task GoToSecondView()
{
// Navigate to Second view model providing qualifier and data
await _navigator.NavigateViewModelAsync<SecondViewModel>(this, "Second", new Entity(Name ?? ""));
}
}

View File

@@ -0,0 +1,174 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Marechai.App.Services;
using Marechai.Data;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation;
/// <summary>
/// Wrapper for NewsDto with generated display text
/// </summary>
public class NewsItemViewModel
{
public required NewsDto News { get; init; }
public required string DisplayText { get; init; }
public required IAsyncRelayCommand<NewsDto> NavigateToItemCommand { get; init; }
/// <summary>
/// Determines if this news item can be navigated to (only computers and consoles)
/// </summary>
public bool CanNavigateToItem
{
get
{
if(News?.Type is null) return false;
var type = (NewsType)News.Type.Value;
return type is NewsType.NewComputerInDb
or NewsType.NewConsoleInDb
or NewsType.UpdatedComputerInDb
or NewsType.UpdatedConsoleInDb
or NewsType.NewComputerInCollection
or NewsType.NewConsoleInCollection
or NewsType.UpdatedComputerInCollection
or NewsType.UpdatedConsoleInCollection;
}
}
}
public partial class NewsViewModel : ObservableObject
{
private readonly IStringLocalizer _localizer;
private readonly ILogger<NewsViewModel> _logger;
private readonly INavigator _navigator;
private readonly NewsService _newsService;
[ObservableProperty]
private string errorMessage = string.Empty;
[ObservableProperty]
private bool hasError;
[ObservableProperty]
private bool isLoading;
[ObservableProperty]
private ObservableCollection<NewsItemViewModel> newsList = [];
public NewsViewModel(NewsService newsService, IStringLocalizer localizer, ILogger<NewsViewModel> logger,
INavigator navigator)
{
_newsService = newsService;
_localizer = localizer;
_logger = logger;
_navigator = navigator;
LoadNews = new AsyncRelayCommand(LoadNewsAsync);
}
public IAsyncRelayCommand LoadNews { get; }
[RelayCommand]
private async Task NavigateToNewsItem(NewsDto news)
{
if(news?.Type is null) return;
var newsType = (NewsType)news.Type.Value;
// Only navigate for computer and console news items
bool isComputerOrConsole = newsType is NewsType.NewComputerInDb
or NewsType.NewConsoleInDb
or NewsType.UpdatedComputerInDb
or NewsType.UpdatedConsoleInDb
or NewsType.NewComputerInCollection
or NewsType.NewConsoleInCollection
or NewsType.UpdatedComputerInCollection
or NewsType.UpdatedConsoleInCollection;
if(!isComputerOrConsole) return;
// Extract the machine ID from AffectedId
if(news.AffectedId is null) return;
int machineId = UntypedNodeExtractor.ExtractInt(news.AffectedId);
if(machineId <= 0) return;
// Navigate to machine view with source information
var navParam = new MachineViewNavigationParameter
{
MachineId = machineId,
NavigationSource = this
};
await _navigator.NavigateViewModelAsync<MachineViewViewModel>(this, data: navParam);
}
/// <summary>
/// Helper to extract int from UntypedNode
/// </summary>
/// <summary>
/// Generates localized text based on NewsType
/// </summary>
private string GetLocalizedTextForNewsType(NewsType type)
{
return type switch
{
NewsType.NewComputerInDb => _localizer["New computer in database"].Value,
NewsType.NewConsoleInDb => _localizer["New console in database"].Value,
NewsType.NewComputerInCollection => _localizer["New computer in collection"].Value,
NewsType.NewConsoleInCollection => _localizer["New console in collection"].Value,
NewsType.UpdatedComputerInDb => _localizer["Updated computer in database"].Value,
NewsType.UpdatedConsoleInDb => _localizer["Updated console in database"].Value,
NewsType.UpdatedComputerInCollection => _localizer["Updated computer in collection"].Value,
NewsType.UpdatedConsoleInCollection => _localizer["Updated console in collection"].Value,
_ => string.Empty
};
}
/// <summary>
/// Loads the latest news from the API
/// </summary>
private async Task LoadNewsAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
NewsList.Clear();
List<NewsDto> news = await _newsService.GetLatestNewsAsync();
if(news.Count == 0)
{
ErrorMessage = _localizer["No news available"].Value;
HasError = true;
}
else
{
foreach(NewsDto item in news)
{
NewsList.Add(new NewsItemViewModel
{
News = item,
DisplayText = GetLocalizedTextForNewsType((NewsType)(item.Type ?? 0)),
NavigateToItemCommand = NavigateToNewsItemCommand
});
}
}
}
catch(Exception ex)
{
_logger.LogError("Error loading news: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load news. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
}

View File

@@ -0,0 +1,5 @@
namespace Marechai.App.Presentation;
public partial record SecondViewModel(Entity Entity)
{
}

View File

@@ -0,0 +1,15 @@
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation;
public class ShellViewModel
{
private readonly INavigator _navigator;
public ShellViewModel(
INavigator navigator)
{
_navigator = navigator;
// Add code here to initialize or attach event handlers to singleton services
}
}