From c475d0e6a4e7ede5c418f48f64ae206f1b7bf3b6 Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Sun, 16 Nov 2025 16:48:24 +0000 Subject: [PATCH] Add pages for sound synthesizers. --- Marechai.App/App.xaml.cs | 18 + .../ViewModels/MachineViewViewModel.cs | 14 + .../Presentation/ViewModels/MainViewModel.cs | 2 +- .../ViewModels/SoundSynthDetailViewModel.cs | 308 +++++++++++++++++ .../ViewModels/SoundSynthsListViewModel.cs | 103 ++++++ .../Views/SoundSynthDetailPage.xaml | 310 ++++++++++++++++++ .../Views/SoundSynthDetailPage.xaml.cs | 100 ++++++ .../Views/SoundSynthListPage.xaml | 207 ++++++++++++ .../Views/SoundSynthListPage.xaml.cs | 33 ++ 9 files changed, 1094 insertions(+), 1 deletion(-) create mode 100644 Marechai.App/Presentation/ViewModels/SoundSynthDetailViewModel.cs create mode 100644 Marechai.App/Presentation/ViewModels/SoundSynthsListViewModel.cs create mode 100644 Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml create mode 100644 Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml.cs create mode 100644 Marechai.App/Presentation/Views/SoundSynthListPage.xaml create mode 100644 Marechai.App/Presentation/Views/SoundSynthListPage.xaml.cs diff --git a/Marechai.App/App.xaml.cs b/Marechai.App/App.xaml.cs index 2c49ad61..70d711ea 100644 --- a/Marechai.App/App.xaml.cs +++ b/Marechai.App/App.xaml.cs @@ -22,6 +22,8 @@ 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; +using SoundSynthDetailViewModel = Marechai.App.Presentation.ViewModels.SoundSynthDetailViewModel; +using SoundSynthsListViewModel = Marechai.App.Presentation.ViewModels.SoundSynthsListViewModel; namespace Marechai.App; @@ -128,6 +130,7 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); services.AddTransient(); services @@ -144,6 +147,8 @@ public partial class App : Application services.AddTransient(); services.AddTransient(); services.AddTransient(); + services.AddTransient(); + services.AddTransient(); }) .UseNavigation(RegisterRoutes)); @@ -174,6 +179,8 @@ public partial class App : Application new ViewMap(), new ViewMap(), new ViewMap(), + new ViewMap(), + new ViewMap(), new DataViewMap()); routes.Register(new RouteMap("", @@ -231,6 +238,17 @@ public partial class App : Application views.FindByViewModel< ProcessorDetailViewModel>()) ]), + new RouteMap("sound-synths", + views.FindByViewModel(), + Nested: + [ + new RouteMap("sound-synth-details", + views.FindByViewModel< + SoundSynthDetailViewModel>()), + new RouteMap("machine-view", + views.FindByViewModel< + MachineViewViewModel>()) + ]), new RouteMap("Second", views.FindByViewModel()) ]) diff --git a/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs b/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs index ce034f79..9ccc49f2 100644 --- a/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs @@ -195,6 +195,20 @@ public partial class MachineViewViewModel : ObservableObject return; } + // If we came from SoundSynthDetailViewModel, navigate back to sound synth details + if(_navigationSource is SoundSynthDetailViewModel soundSynthDetailVm) + { + var navParam = new SoundSynthDetailNavigationParameter + { + SoundSynthId = soundSynthDetailVm.SoundSynthId, + 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 f61e550c..4ecdb5d5 100644 --- a/Marechai.App/Presentation/ViewModels/MainViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/MainViewModel.cs @@ -52,7 +52,7 @@ public partial class MainViewModel : ObservableObject NavigateToPeopleCommand = new AsyncRelayCommand(() => NavigateTo("people")); NavigateToProcessorsCommand = new AsyncRelayCommand(() => NavigateTo("processors")); NavigateToSoftwareCommand = new AsyncRelayCommand(() => NavigateTo("software")); - NavigateToSoundSynthesizersCommand = new AsyncRelayCommand(() => NavigateTo("soundsynthesizers")); + NavigateToSoundSynthesizersCommand = new AsyncRelayCommand(() => NavigateTo("sound-synths")); NavigateToSettingsCommand = new AsyncRelayCommand(() => NavigateTo("settings")); LoginLogoutCommand = new RelayCommand(HandleLoginLogout); ToggleSidebarCommand = new RelayCommand(() => IsSidebarOpen = !IsSidebarOpen); diff --git a/Marechai.App/Presentation/ViewModels/SoundSynthDetailViewModel.cs b/Marechai.App/Presentation/ViewModels/SoundSynthDetailViewModel.cs new file mode 100644 index 00000000..ba670893 --- /dev/null +++ b/Marechai.App/Presentation/ViewModels/SoundSynthDetailViewModel.cs @@ -0,0 +1,308 @@ +#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 SoundSynthDetailViewModel : ObservableObject +{ + private readonly CompaniesService _companiesService; + private readonly IStringLocalizer _localizer; + private readonly ILogger _logger; + private readonly INavigator _navigator; + private readonly SoundSynthsService _soundSynthsService; + + [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 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 SoundSynthDto? _soundSynth; + + [ObservableProperty] + private int _soundSynthId; + + public SoundSynthDetailViewModel(SoundSynthsService soundSynthsService, CompaniesService companiesService, + IStringLocalizer localizer, ILogger logger, + INavigator navigator) + { + _soundSynthsService = soundSynthsService; + _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; } = "Sound Synthesizer Details"; + + /// + /// Loads Sound Synthesizer details including computers and consoles + /// + private async Task LoadDataAsync() + { + try + { + IsLoading = true; + ErrorMessage = string.Empty; + HasError = false; + IsDataLoaded = false; + Computers.Clear(); + Consoles.Clear(); + + if(SoundSynthId <= 0) + { + ErrorMessage = _localizer["Invalid Sound Synthesizer ID"].Value; + HasError = true; + + return; + } + + _logger.LogInformation("Loading Sound Synthesizer details for ID: {SoundSynthId}", SoundSynthId); + + // Load Sound Synthesizer details + SoundSynth = await _soundSynthsService.GetSoundSynthByIdAsync(SoundSynthId); + + if(SoundSynth is null) + { + ErrorMessage = _localizer["Sound Synthesizer not found"].Value; + HasError = true; + + return; + } + + // Set manufacturer name (from Company field or fetch by CompanyId if empty) + ManufacturerName = SoundSynth.Company ?? string.Empty; + + if(string.IsNullOrEmpty(ManufacturerName) && SoundSynth.CompanyId.HasValue) + { + try + { + CompanyDto? company = await _companiesService.GetCompanyByIdAsync(SoundSynth.CompanyId.Value); + if(company != null) ManufacturerName = company.Name ?? string.Empty; + } + catch(Exception ex) + { + _logger.LogWarning(ex, "Failed to load company for Sound Synthesizer {SoundSynthId}", SoundSynthId); + } + } + + _logger.LogInformation("Sound Synthesizer loaded: {Name}, Company: {Company}", + SoundSynth.Name, + ManufacturerName); + + // Load machines and separate into computers and consoles + try + { + List? machines = await _soundSynthsService.GetMachinesBySoundSynthAsync(SoundSynthId); + + 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 Sound Synthesizer {SoundSynthId}", + Computers.Count, + Consoles.Count, + SoundSynthId); + } + else + { + HasComputers = false; + HasConsoles = false; + } + } + catch(Exception ex) + { + _logger.LogWarning(ex, "Failed to load machines for Sound Synthesizer {SoundSynthId}", SoundSynthId); + HasComputers = false; + HasConsoles = false; + } + + IsDataLoaded = true; + } + catch(Exception ex) + { + _logger.LogError(ex, "Error loading Sound Synthesizer details: {Exception}", ex.Message); + ErrorMessage = _localizer["Failed to load Sound Synthesizer 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 Sound Synthesizer 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 Sound Synthesizer 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, + NavigationSource = this + }; + + await _navigator.NavigateViewModelAsync(this, data: navParam); + } + + /// + /// Sets the navigation source (where we came from). + /// + public void SetNavigationSource(object? source) + { + _navigationSource = source; + } + + /// + /// Machine item for displaying computers or consoles that use the Sound Synthesizer + /// + 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/SoundSynthsListViewModel.cs b/Marechai.App/Presentation/ViewModels/SoundSynthsListViewModel.cs new file mode 100644 index 00000000..91802bb2 --- /dev/null +++ b/Marechai.App/Presentation/ViewModels/SoundSynthsListViewModel.cs @@ -0,0 +1,103 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using Marechai.App.Presentation.Models; +using Marechai.App.Services; +using Uno.Extensions.Navigation; + +namespace Marechai.App.Presentation.ViewModels; + +public partial class SoundSynthsListViewModel : ObservableObject +{ + private readonly ILogger _logger; + private readonly INavigator _navigator; + private readonly SoundSynthsService _soundSynthsService; + + [ObservableProperty] + private string _errorMessage = string.Empty; + + [ObservableProperty] + private bool _hasError; + + [ObservableProperty] + private bool _isDataLoaded; + + [ObservableProperty] + private bool _isLoading = true; + + [ObservableProperty] + private ObservableCollection _soundSynths = []; + + public SoundSynthsListViewModel(SoundSynthsService soundSynthsService, INavigator navigator, + ILogger logger) + { + _soundSynthsService = soundSynthsService; + _navigator = navigator; + _logger = logger; + LoadData = new AsyncRelayCommand(LoadDataAsync); + NavigateToSoundSynthCommand = new AsyncRelayCommand(NavigateToSoundSynthAsync); + } + + public IAsyncRelayCommand LoadData { get; } + public IAsyncRelayCommand NavigateToSoundSynthCommand { get; } + + private async Task LoadDataAsync() + { + try + { + IsLoading = true; + IsDataLoaded = false; + HasError = false; + ErrorMessage = string.Empty; + + List soundSynths = await _soundSynthsService.GetAllSoundSynthsAsync(); + + SoundSynths = new ObservableCollection(soundSynths.Select(ss => new SoundSynthListItem + { + Id = ss.Id ?? 0, + Name = ss.Name ?? "Unknown", + Company = ss.Company ?? "Unknown" + }) + .OrderBy(ss => ss.Company) + .ThenBy(ss => ss.Name)); + + _logger.LogInformation("Successfully loaded {Count} Sound Synthesizers", SoundSynths.Count); + IsDataLoaded = true; + } + catch(Exception ex) + { + _logger.LogError(ex, "Error loading Sound Synthesizers"); + ErrorMessage = "Failed to load Sound Synthesizers. Please try again later."; + HasError = true; + } + finally + { + IsLoading = false; + } + } + + private async Task NavigateToSoundSynthAsync(SoundSynthListItem? item) + { + if(item == null) return; + + _logger.LogInformation("Navigating to Sound Synthesizer {SoundSynthId}", item.Id); + + await _navigator.NavigateViewModelAsync(this, + data: new SoundSynthDetailNavigationParameter + { + SoundSynthId = item.Id, + NavigationSource = this + }); + } + + public class SoundSynthListItem + { + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public string? Company { get; set; } + } +} \ No newline at end of file diff --git a/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml b/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml new file mode 100644 index 00000000..dfd4613d --- /dev/null +++ b/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml @@ -0,0 +1,310 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml.cs b/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml.cs new file mode 100644 index 00000000..357cedd9 --- /dev/null +++ b/Marechai.App/Presentation/Views/SoundSynthDetailPage.xaml.cs @@ -0,0 +1,100 @@ +#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; + +/// +/// Sound Synthesizer detail page showing all information, computers, and consoles +/// +public sealed partial class SoundSynthDetailPage : Page +{ + private object? _navigationSource; + private int? _pendingSoundSynthId; + + public SoundSynthDetailPage() + { + InitializeComponent(); + DataContextChanged += SoundSynthDetailPage_DataContextChanged; + } + + protected override void OnNavigatedTo(NavigationEventArgs e) + { + base.OnNavigatedTo(e); + + int? soundSynthId = null; + + // Handle both int and SoundSynthDetailNavigationParameter + if(e.Parameter is int intId) + soundSynthId = intId; + else if(e.Parameter is SoundSynthDetailNavigationParameter navParam) + { + soundSynthId = navParam.SoundSynthId; + _navigationSource = navParam.NavigationSource; + } + + if(soundSynthId.HasValue) + { + _pendingSoundSynthId = soundSynthId; + + if(DataContext is SoundSynthDetailViewModel viewModel) + { + viewModel.SoundSynthId = soundSynthId.Value; + if(_navigationSource != null) viewModel.SetNavigationSource(_navigationSource); + _ = viewModel.LoadData.ExecuteAsync(null); + } + } + } + + private void SoundSynthDetailPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) + { + if(DataContext is SoundSynthDetailViewModel viewModel && _pendingSoundSynthId.HasValue) + { + viewModel.SoundSynthId = _pendingSoundSynthId.Value; + if(_navigationSource != null) viewModel.SetNavigationSource(_navigationSource); + _ = viewModel.LoadData.ExecuteAsync(null); + } + } + + private void ComputersSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) + { + if(DataContext is SoundSynthDetailViewModel vm) vm.ComputersFilterCommand.Execute(null); + } + + private void ComputersSearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) + { + if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) + { + if(DataContext is SoundSynthDetailViewModel vm) vm.ComputersFilterCommand.Execute(null); + } + } + + private void ConsolesSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args) + { + if(DataContext is SoundSynthDetailViewModel vm) vm.ConsolesFilterCommand.Execute(null); + } + + private void ConsolesSearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args) + { + if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput) + { + if(DataContext is SoundSynthDetailViewModel 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 SoundSynthDetailViewModel 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 SoundSynthDetailViewModel vm) + _ = vm.SelectMachineCommand.ExecuteAsync(machineId); + } +} \ No newline at end of file diff --git a/Marechai.App/Presentation/Views/SoundSynthListPage.xaml b/Marechai.App/Presentation/Views/SoundSynthListPage.xaml new file mode 100644 index 00000000..2b34d688 --- /dev/null +++ b/Marechai.App/Presentation/Views/SoundSynthListPage.xaml @@ -0,0 +1,207 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Marechai.App/Presentation/Views/SoundSynthListPage.xaml.cs b/Marechai.App/Presentation/Views/SoundSynthListPage.xaml.cs new file mode 100644 index 00000000..9b271ec6 --- /dev/null +++ b/Marechai.App/Presentation/Views/SoundSynthListPage.xaml.cs @@ -0,0 +1,33 @@ +using Marechai.App.Presentation.ViewModels; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; + +namespace Marechai.App.Presentation.Views; + +public sealed partial class SoundSynthListPage : Page +{ + public SoundSynthListPage() + { + InitializeComponent(); + Loaded += SoundSynthListPage_Loaded; + DataContextChanged += SoundSynthListPage_DataContextChanged; + } + + private void SoundSynthListPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args) + { + if(DataContext is SoundSynthsListViewModel vm) + { + // Load data when DataContext is set + vm.LoadData.Execute(null); + } + } + + private void SoundSynthListPage_Loaded(object sender, RoutedEventArgs e) + { + if(DataContext is SoundSynthsListViewModel vm) + { + // Load data when page is loaded (fallback) + vm.LoadData.Execute(null); + } + } +} \ No newline at end of file