64 Commits

Author SHA1 Message Date
b06406e20f Add users management page. 2025-11-16 23:09:47 +00:00
80ba603265 Update authservice to kiota exceptions. 2025-11-16 23:09:36 +00:00
db7aee369b Ensure credentials are trimmed when logging in. 2025-11-16 23:09:18 +00:00
5fe5c94c55 Add endpoint to access and modify user information. 2025-11-16 23:08:32 +00:00
5f66029528 Add login page. 2025-11-16 20:35:00 +00:00
869e675ee3 Add authentication and token services. 2025-11-16 19:59:05 +00:00
20216bc1d6 Handle special software case in sound synthesizers. 2025-11-16 19:45:17 +00:00
22bcd4ede4 Added CDE inspired theme. 2025-11-16 19:36:31 +00:00
7f080c8734 Add Amiga Workbench inspired theme. 2025-11-16 19:18:19 +00:00
6e4a4bc67b Add Turbo Vision inspired theme. 2025-11-16 19:11:21 +00:00
ac460cb050 Added Mac OS 9 inspired theme. 2025-11-16 18:59:23 +00:00
c1c6e427c2 Added Mac OS 9 inspired theme. 2025-11-16 18:59:14 +00:00
4c273ef661 Add theming and a Windows 3.11 inspired theme. 2025-11-16 18:22:59 +00:00
9567153378 Add missing navigation parameters. 2025-11-16 18:22:25 +00:00
c475d0e6a4 Add pages for sound synthesizers. 2025-11-16 16:48:24 +00:00
9f89186dde Add endpoints to get computers by sound synthesizers. 2025-11-16 16:47:54 +00:00
733dc59f7b Add processors list and details pages. 2025-11-16 16:09:20 +00:00
5b709755c7 Add endpoint to retrieve machines by processor. 2025-11-16 16:09:02 +00:00
959a48b36c Fixed navigation for GPU details. 2025-11-16 16:07:48 +00:00
4b02dd6d2c Refactor GPU detail page header to use NavigationBar for improved navigation 2025-11-16 15:32:05 +00:00
f308668f69 Add GPU details page. 2025-11-16 04:56:26 +00:00
e5f1d766b5 Fix endpoint that gets resolutions by GPUs. 2025-11-16 04:56:16 +00:00
cc2738e45d Add endpoint to get gpus by machine. 2025-11-16 04:55:45 +00:00
981cd3c27c Add list of GPUs. 2025-11-16 02:35:59 +00:00
5c64e59f8f Fix showing enumerations in photo details page. 2025-11-16 02:13:48 +00:00
01c24ae987 Do not redirect to HTTPS in development. 2025-11-16 02:13:22 +00:00
a60fb39687 Hardcode CORS. 2025-11-16 02:13:07 +00:00
497251be86 Change DTOs to use underlying nullable numerical values instead of nullable enumerations as they're not supported by Kiota. 2025-11-16 02:12:29 +00:00
195b23f755 Add photo detail page. 2025-11-15 22:45:55 +00:00
edc8d33bb2 Fix wrong cache folder. 2025-11-15 22:45:36 +00:00
dbef655a3d No need to use untypenode extraction anymore. 2025-11-15 22:44:02 +00:00
4f59f6870d Update Kiota client. 2025-11-15 22:41:01 +00:00
c3e75175f9 Make OpenApi not generate all numeric fields with "string" as acceptable format.
This stupidity just made Kiota generate unusable DTOs.
2025-11-15 22:40:01 +00:00
1dcb062c35 Show machine photo thumbnails in machine details. 2025-11-15 20:07:52 +00:00
6a52c1f067 Add machine photo cache. 2025-11-15 18:55:17 +00:00
c35fdbb0e4 Do not constrain company logo horizontally. 2025-11-15 18:45:36 +00:00
0368e12974 Show company logo in companies list. 2025-11-15 18:42:11 +00:00
fe2c3a082d Add carousel of logos on company detail. 2025-11-15 18:09:54 +00:00
4d30530ef0 Add logo to company detail page. 2025-11-15 16:00:19 +00:00
e0689684e1 Add logo cache. 2025-11-15 15:47:31 +00:00
cfdef93787 Show country flag in company detail page. 2025-11-15 15:26:47 +00:00
c6cac9e04a Add flag cache. 2025-11-15 14:40:27 +00:00
e2f86b76db Serve assets. 2025-11-15 14:11:48 +00:00
80791a8cc9 Add company detail page. 2025-11-15 05:32:46 +00:00
d5fbb55425 Add companies list. 2025-11-15 04:48:11 +00:00
7ee042bdec Add consoles pages. 2025-11-15 04:13:24 +00:00
e9221ac130 Return the keyboard styling back. 2025-11-15 04:12:13 +00:00
87291d9dd8 Fix variable naming. 2025-11-15 02:48:40 +00:00
ce1c089fb0 Reorganize project structure. 2025-11-15 02:46:54 +00:00
5d249f435e Reorganize project structure. 2025-11-15 02:46:53 +00:00
3e4677b084 Add machine view. 2025-11-15 02:38:47 +00:00
b7c94312fc Add missing computers list page. 2025-11-15 02:36:01 +00:00
61ebf7b503 Add computer list. 2025-11-15 01:09:30 +00:00
b18396f8d8 Add computers page. 2025-11-14 20:59:12 +00:00
4f1aee302b Fix routes. 2025-11-14 20:58:41 +00:00
7ede62514f Add sidebar. 2025-11-14 19:00:01 +00:00
955c2f9654 Add CORS policy. 2025-11-14 16:46:42 +00:00
4a5708b910 Move enums to Data project. 2025-11-14 16:31:35 +00:00
5bffbc342e Show news on application load. 2025-11-14 15:18:30 +00:00
392c69350f Send type in news DTO. 2025-11-14 15:18:09 +00:00
2bb07845e1 Do not send text from news controller. 2025-11-14 15:17:50 +00:00
1053617622 Fix provisional endpoint for API client. 2025-11-14 15:17:25 +00:00
30b60c0e96 Move framework setting to each project. 2025-11-14 15:12:15 +00:00
14596c5499 Add Kiota client. 2025-11-14 13:33:27 +00:00
539 changed files with 54810 additions and 1803 deletions

View File

@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<TargetFramework Condition="'$(MSBuildProjectName)' != 'Marechai.App'">net10.0</TargetFramework>
<Company>Canary Islands Computer Museum</Company>
<Copyright>Copyright © 2003-2026 Natalia Portillo</Copyright>
<Product>Canary Islands Computer Museum Website</Product>

View File

@@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup>
<!-- Duplicated packages (also in Directory.Build.props) -->
<PackageVersion Include="Humanizer" Version="2.14.1"/>
<PackageVersion Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore" Version="9.0.11"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.11"/>
<!-- Unique to Marechai.csproj -->
@@ -32,5 +33,16 @@
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0"/>
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.11"/>
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.11"/>
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1"/>
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2"/>
<!-- Add more community toolkit references here -->
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
<PackageVersion Include="Uno.CommunityToolkit.WinUI.UI.Controls" Version="7.1.200"/>
<!-- Add more uno community toolkit references here -->
</ItemGroup>
</Project>

View File

@@ -1,19 +1,29 @@
<Application x:Class="Marechai.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Marechai.App.Presentation.Converters">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- Load WinUI resources -->
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Load Uno.UI.Toolkit resources -->
<ToolkitResources xmlns="using:Uno.Toolkit.UI" />
</ResourceDictionary.MergedDictionaries>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<!-- Load WinUI resources -->
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />
<!-- Add resources here -->
<!-- Load Uno.UI.Toolkit resources -->
<ToolkitResources xmlns="using:Uno.Toolkit.UI" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
<!-- Add resources here -->
<local:ObjectToVisibilityConverter x:Key="ObjectToVisibilityConverter" />
<local:StringToVisibilityConverter x:Key="StringToVisibilityConverter" />
<local:ZeroToVisibilityConverter x:Key="ZeroToVisibilityConverter" />
<local:InvertBoolConverter x:Key="InvertBoolConverter" />
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<local:InvertBoolToVisibilityConverter x:Key="InvertBoolToVisibilityConverter" />
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<local:RolesListConverter x:Key="RolesListConverter" />
</Application>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,88 +1,175 @@
using System.Net.Http;
using Marechai.App.Presentation.ViewModels;
using Marechai.App.Presentation.Views;
using Marechai.App.Services;
using Marechai.App.Services.Authentication;
using Marechai.App.Services.Caching;
using Microsoft.UI.Xaml;
using Uno.Extensions;
using Uno.Extensions.Authentication;
using Uno.Extensions.Configuration;
using Uno.Extensions.Hosting;
using Uno.Extensions.Http;
using Uno.Extensions.Localization;
using Uno.Extensions.Navigation;
using Uno.Resizetizer;
using Uno.UI;
using CompanyDetailViewModel = Marechai.App.Presentation.ViewModels.CompanyDetailViewModel;
using ComputersListViewModel = Marechai.App.Presentation.ViewModels.ComputersListViewModel;
using ComputersViewModel = Marechai.App.Presentation.ViewModels.ComputersViewModel;
using GpuDetailViewModel = Marechai.App.Presentation.ViewModels.GpuDetailViewModel;
using GpuListViewModel = Marechai.App.Presentation.ViewModels.GpusListViewModel;
using MachineViewViewModel = Marechai.App.Presentation.ViewModels.MachineViewViewModel;
using MainViewModel = Marechai.App.Presentation.ViewModels.MainViewModel;
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 SettingsViewModel = Marechai.App.Presentation.ViewModels.SettingsViewModel;
using SoundSynthDetailViewModel = Marechai.App.Presentation.ViewModels.SoundSynthDetailViewModel;
using SoundSynthsListViewModel = Marechai.App.Presentation.ViewModels.SoundSynthsListViewModel;
using LoginViewModel = Marechai.App.Presentation.ViewModels.LoginViewModel;
namespace Marechai.App;
public partial class App : Application
{
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// Initializes the singleton application object. This is the first line of authored code
/// executed, and as such is the logical equivalent of main() or WinMain().
/// </summary>
public App()
{
this.InitializeComponent();
InitializeComponent();
}
protected Window? MainWindow { get; private set; }
protected IHost? Host { get; private set; }
public IHost? Host { get; private set; }
protected async override void OnLaunched(LaunchActivatedEventArgs args)
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
var builder = this.CreateBuilder(args)
// Add navigation support for toolkit controls such as TabBar and NavigationView
.UseToolkitNavigation()
.Configure(host => host
#if DEBUG
// Switch to Development environment when running in DEBUG
.UseEnvironment(Environments.Development)
#endif
.UseLogging(configure: (context, logBuilder) =>
{
// Configure log levels for different categories of logging
logBuilder
.SetMinimumLevel(
context.HostingEnvironment.IsDevelopment() ? LogLevel.Information : LogLevel.Warning)
IApplicationBuilder builder = this.CreateBuilder(args)
// Default filters for core Uno Platform namespaces
.CoreLogLevel(LogLevel.Warning);
// Uno Platform namespace filter groups
// Uncomment individual methods to see more detailed logging
//// Generic Xaml events
//logBuilder.XamlLogLevel(LogLevel.Debug);
//// Layout specific messages
//logBuilder.XamlLayoutLogLevel(LogLevel.Debug);
//// Storage messages
//logBuilder.StorageLogLevel(LogLevel.Debug);
//// Binding related messages
//logBuilder.XamlBindingLogLevel(LogLevel.Debug);
//// Binder memory references tracking
//logBuilder.BinderMemoryReferenceLogLevel(LogLevel.Debug);
//// DevServer and HotReload related
//logBuilder.HotReloadCoreLogLevel(LogLevel.Information);
//// Debug JS interop
//logBuilder.WebAssemblyLogLevel(LogLevel.Debug);
}, enableUnoLogging: true)
.UseSerilog(consoleLoggingEnabled: true, fileLoggingEnabled: true)
.UseConfiguration(configure: configBuilder =>
configBuilder
.EmbeddedSource<App>()
.Section<AppConfig>()
)
// Enable localization (see appsettings.json for supported languages)
.UseLocalization()
.UseHttp((context, services) =>
{
// Add navigation support for toolkit controls such as TabBar and NavigationView
.UseToolkitNavigation()
.Configure(host => host
#if DEBUG
// DelegatingHandler will be automatically injected
services.AddTransient<DelegatingHandler, DebugHttpHandler>();
// Switch to Development environment when running in DEBUG
.UseEnvironment(Environments.Development)
#endif
})
.ConfigureServices((context, services) =>
{
// TODO: Register your services
//services.AddSingleton<IMyService, MyService>();
})
.UseNavigation(RegisterRoutes)
);
.UseLogging((context, logBuilder) =>
{
// Configure log levels for different categories of logging
logBuilder
.SetMinimumLevel(context
.HostingEnvironment
.IsDevelopment()
? LogLevel.Information
: LogLevel.Warning)
// Default filters for core Uno Platform namespaces
.CoreLogLevel(LogLevel.Warning);
// Uno Platform namespace filter groups
// Uncomment individual methods to see more detailed logging
//// Generic Xaml events
//logBuilder.XamlLogLevel(LogLevel.Debug);
//// Layout specific messages
//logBuilder.XamlLayoutLogLevel(LogLevel.Debug);
//// Storage messages
//logBuilder.StorageLogLevel(LogLevel.Debug);
//// Binding related messages
//logBuilder.XamlBindingLogLevel(LogLevel.Debug);
//// Binder memory references tracking
//logBuilder.BinderMemoryReferenceLogLevel(LogLevel.Debug);
//// DevServer and HotReload related
//logBuilder.HotReloadCoreLogLevel(LogLevel.Information);
//// Debug JS interop
//logBuilder.WebAssemblyLogLevel(LogLevel.Debug);
},
true)
.UseSerilog(true, true)
.UseConfiguration(configure: configBuilder =>
configBuilder.EmbeddedSource<App>()
.Section<AppConfig>())
// Enable localization (see appsettings.json for supported languages)
.UseLocalization()
.UseHttp((context, services) =>
{
services.AddTransient<DelegatingHandler,
HttpAuthHandler>();
#if DEBUG
// DelegatingHandler will be automatically injected
services
.AddTransient<DelegatingHandler,
DebugHttpHandler>();
#endif
services.AddKiotaClientV2<ApiClient>(context,
new EndpointOptions
{
Url = context.Configuration
.GetSection("ApiClient:Url")
.Value ??
// Fallback to a default URL if not configured
"https://localhost:5023"
});
})
.ConfigureServices((context, services) =>
{
// Register application services
services
.AddSingleton<IColorThemeService,
ColorThemeService>();
services
.AddSingleton<IAuthenticationService,
AuthService>();
services.AddSingleton<ITokenService, TokenService>();
services.AddSingleton<IJwtService, JwtService>();
services.AddSingleton<FlagCache>();
services.AddSingleton<CompanyLogoCache>();
services.AddSingleton<MachinePhotoCache>();
services.AddSingleton<NewsService>();
services.AddSingleton<NewsViewModel>();
services.AddSingleton<ComputersService>();
services.AddSingleton<ComputersViewModel>();
services.AddSingleton<ConsolesService>();
services.AddSingleton<ConsolesViewModel>();
services.AddSingleton<CompaniesService>();
services.AddSingleton<CompaniesViewModel>();
services.AddSingleton<CompanyDetailService>();
services.AddSingleton<CompanyDetailViewModel>();
services.AddSingleton<MachineViewViewModel>();
services.AddSingleton<GpusService>();
services.AddSingleton<ProcessorsService>();
services.AddSingleton<SoundSynthsService>();
services.AddTransient<PhotoDetailViewModel>();
services
.AddSingleton<IComputersListFilterContext,
ComputersListFilterContext>();
services
.AddSingleton<IConsolesListFilterContext,
ConsolesListFilterContext>();
services.AddTransient<ComputersListViewModel>();
services.AddTransient<ConsolesListViewModel>();
services.AddTransient<GpuListViewModel>();
services.AddTransient<GpuDetailViewModel>();
services.AddTransient<ProcessorsListViewModel>();
services.AddTransient<ProcessorDetailViewModel>();
services.AddTransient<SoundSynthsListViewModel>();
services.AddTransient<SoundSynthDetailViewModel>();
services.AddTransient<SettingsViewModel>();
services.AddTransient<LoginViewModel>();
})
.UseNavigation(RegisterRoutes));
MainWindow = builder.Window;
#if DEBUG
@@ -95,20 +182,107 @@ public partial class App : Application
private static void RegisterRoutes(IViewRegistry views, IRouteRegistry routes)
{
views.Register(
new ViewMap(ViewModel: typeof(ShellViewModel)),
new ViewMap<MainPage, MainViewModel>(),
new DataViewMap<SecondPage, SecondViewModel, Entity>()
);
views.Register(new ViewMap(ViewModel: typeof(ShellViewModel)),
new ViewMap<MainPage, MainViewModel>(),
new ViewMap<LoginPage, LoginViewModel>(),
new ViewMap<NewsPage, NewsViewModel>(),
new ViewMap<ComputersPage, ComputersViewModel>(),
new ViewMap<ComputersListPage, ComputersListViewModel>(),
new ViewMap<ConsolesPage, ConsolesViewModel>(),
new ViewMap<ConsolesListPage, ConsolesListViewModel>(),
new ViewMap<CompaniesPage, CompaniesViewModel>(),
new ViewMap<CompanyDetailPage, CompanyDetailViewModel>(),
new ViewMap<MachineViewPage, MachineViewViewModel>(),
new ViewMap<PhotoDetailPage, PhotoDetailViewModel>(),
new ViewMap<GpuListPage, GpuListViewModel>(),
new ViewMap<GpuDetailPage, GpuDetailViewModel>(),
new ViewMap<ProcessorListPage, ProcessorsListViewModel>(),
new ViewMap<ProcessorDetailPage, ProcessorDetailViewModel>(),
new ViewMap<SoundSynthListPage, SoundSynthsListViewModel>(),
new ViewMap<SoundSynthDetailPage, SoundSynthDetailViewModel>(),
new ViewMap<SettingsPage, SettingsViewModel>(),
new ViewMap<UsersPage, UsersViewModel>(),
new DataViewMap<SecondPage, SecondViewModel, Entity>());
routes.Register(
new RouteMap("", View: views.FindByViewModel<ShellViewModel>(),
Nested:
[
new("Main", View: views.FindByViewModel<MainViewModel>(), IsDefault: true),
new("Second", View: views.FindByViewModel<SecondViewModel>()),
]
)
);
routes.Register(new RouteMap("",
views.FindByViewModel<ShellViewModel>(),
Nested:
[
new RouteMap("Login", views.FindByViewModel<LoginViewModel>()),
new RouteMap("Main",
views.FindByViewModel<MainViewModel>(),
true,
Nested:
[
new RouteMap("News",
views.FindByViewModel<NewsViewModel>(),
true),
new RouteMap("computers",
views.FindByViewModel<ComputersViewModel>(),
Nested
:
[
new RouteMap("list-computers",
views.FindByViewModel<
ComputersListViewModel>()),
new RouteMap("view",
views.FindByViewModel<
MachineViewViewModel>())
]),
new RouteMap("consoles",
views.FindByViewModel<ConsolesViewModel>(),
Nested
:
[
new RouteMap("list-consoles",
views.FindByViewModel<
ConsolesListViewModel>())
]),
new RouteMap("companies",
views.FindByViewModel<CompaniesViewModel>(),
Nested
:
[
new RouteMap("company-details",
views.FindByViewModel<
CompanyDetailViewModel>())
]),
new RouteMap("gpus",
views.FindByViewModel<GpuListViewModel>(),
Nested
:
[
new RouteMap("gpu-details",
views.FindByViewModel<
GpuDetailViewModel>())
]),
new RouteMap("processors",
views.FindByViewModel<ProcessorsListViewModel>(),
Nested:
[
new RouteMap("processor-details",
views.FindByViewModel<
ProcessorDetailViewModel>())
]),
new RouteMap("sound-synths",
views.FindByViewModel<
SoundSynthsListViewModel>(),
Nested:
[
new RouteMap("sound-synth-details",
views.FindByViewModel<
SoundSynthDetailViewModel>()),
new RouteMap("machine-view",
views.FindByViewModel<
MachineViewViewModel>())
]),
new RouteMap("settings",
views.FindByViewModel<SettingsViewModel>()),
new RouteMap("users",
views.FindByViewModel<UsersViewModel>()),
new RouteMap("Second",
views.FindByViewModel<SecondViewModel>())
])
]));
}
}
}

View File

@@ -1,42 +1,99 @@
<Project Sdk="Uno.Sdk">
<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-browserwasm;net10.0-desktop</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows')) Or $([MSBuild]::IsOSPlatform('macos'))">$(TargetFrameworks);net10.0-ios</TargetFrameworks>
<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-browserwasm;net10.0-desktop</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows')) Or $([MSBuild]::IsOSPlatform('macos'))">$(TargetFrameworks);net10.0-ios</TargetFrameworks>
<OutputType>Exe</OutputType>
<UnoSingleProject>true</UnoSingleProject>
<OutputType>Exe</OutputType>
<UnoSingleProject>true</UnoSingleProject>
<!-- Display name -->
<ApplicationTitle>Marechai.App</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>net.marechai.app</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- Package Publisher -->
<ApplicationPublisher>O=Marechai.App</ApplicationPublisher>
<!-- Package Description -->
<Description>Marechai.App powered by Uno Platform.</Description>
<!-- Display name -->
<ApplicationTitle>Marechai.App</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>net.marechai.app</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- Package Publisher -->
<ApplicationPublisher>O=Marechai.App</ApplicationPublisher>
<!-- Package Description -->
<Description>Marechai.App powered by Uno Platform.</Description>
<!--
UnoFeatures let's you quickly add and manage implicit package references based on the features you want to use.
https://aka.platform.uno/singleproject-features
-->
<UnoFeatures>
Lottie;
Hosting;
Toolkit;
Logging;
LoggingSerilog;
Mvvm;
Configuration;
HttpKiota;
Serialization;
Localization;
Navigation;
ThemeService;
Storage;
SkiaRenderer;
Svg;
</UnoFeatures>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Marechai.Data\Marechai.Data.csproj"/>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Humanizer"/>
<PackageReference Include="System.IdentityModel.Tokens.Jwt"/>
</ItemGroup>
<ItemGroup>
<Compile Update="Presentation\Views\Shell.xaml.cs">
<DependentUpon>Shell.xaml</DependentUpon>
<IsDefaultItem>true</IsDefaultItem>
</Compile>
<Compile Update="Presentation\Views\SecondPage.xaml.cs">
<DependentUpon>SecondPage.xaml</DependentUpon>
<IsDefaultItem>true</IsDefaultItem>
</Compile>
<Compile Update="Presentation\Views\ComputersPage.xaml.cs">
<DependentUpon>ComputersPage.xaml</DependentUpon>
<IsDefaultItem>true</IsDefaultItem>
</Compile>
<Compile Update="Presentation\Views\MainPage.xaml.cs">
<DependentUpon>MainPage.xaml</DependentUpon>
<IsDefaultItem>true</IsDefaultItem>
</Compile>
<Compile Update="Presentation\Views\ComputersListPage.xaml.cs">
<DependentUpon>ComputersListPage.xaml</DependentUpon>
<IsDefaultItem>true</IsDefaultItem>
</Compile>
<Compile Update="Presentation\Views\NewsPage.xaml.cs">
<DependentUpon>NewsPage.xaml</DependentUpon>
<IsDefaultItem>true</IsDefaultItem>
</Compile>
<Compile Update="Presentation\Views\MachineViewPage.xaml.cs">
<DependentUpon>MachineViewPage.xaml</DependentUpon>
<IsDefaultItem>true</IsDefaultItem>
</Compile>
<Compile Update="Presentation\Components\Sidebar.xaml.cs">
<DependentUpon>Sidebar.xaml</DependentUpon>
<IsDefaultItem>true</IsDefaultItem>
</Compile>
<Compile Update="Presentation\Views\UsersPage.xaml.cs">
<DependentUpon>UsersPage.xaml</DependentUpon>
<IsDefaultItem>true</IsDefaultItem>
</Compile>
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls"/>
<!-- Add more community toolkit references here -->
</ItemGroup>
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
<PackageReference Include="Uno.CommunityToolkit.WinUI.UI.Controls"/>
<!-- Add more uno community toolkit references here -->
</ItemGroup>
<!--
UnoFeatures let's you quickly add and manage implicit package references based on the features you want to use.
https://aka.platform.uno/singleproject-features
-->
<UnoFeatures>
Lottie;
Hosting;
Toolkit;
Logging;
LoggingSerilog;
Mvvm;
Configuration;
HttpKiota;
Serialization;
Localization;
Navigation;
ThemeService;
SkiaRenderer;
</UnoFeatures>
</PropertyGroup>
</Project>

View File

@@ -0,0 +1,273 @@
<?xml version="1.0"
encoding="utf-8"?>
<UserControl x:Class="Marechai.App.Presentation.Components.Sidebar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="using:Marechai.App.Presentation.Converters"
mc:Ignorable="d"
d:DesignHeight="600"
d:DesignWidth="280"
Background="{ThemeResource NavigationViewDefaultPaneBackground}">
<UserControl.Resources>
<local:CollapseExpandIconConverter x:Key="CollapseExpandIconConverter" />
<local:CollapseExpandTooltipConverter x:Key="CollapseExpandTooltipConverter" />
</UserControl.Resources>
<!-- Grid container - naturally responds to parent column width -->
<Grid RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Sidebar Header with Collapse/Expand Button -->
<Grid Grid.Row="0"
Padding="8,8,8,8"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Sidebar Title - Hidden when collapsed -->
<TextBlock Grid.Column="0"
Text="Navigation"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
VerticalAlignment="Center"
Padding="4,0,0,0"
Visibility="{Binding SidebarContentVisible}" />
<!-- Collapse/Expand Button - Always visible -->
<Button Grid.Column="1"
Content="{Binding IsSidebarOpen, Converter={StaticResource CollapseExpandIconConverter}}"
Command="{Binding ToggleSidebarCommand}"
Background="Transparent"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
Padding="8,8,8,8"
CornerRadius="4"
ToolTipService.ToolTip="{Binding IsSidebarOpen, Converter={StaticResource CollapseExpandTooltipConverter}}"
FontSize="14"
MinWidth="40"
MinHeight="40"
HorizontalAlignment="Center" />
</Grid>
<!-- Scrollable Navigation Items - Hidden when collapsed -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
Padding="0"
Visibility="{Binding SidebarContentVisible}">
<StackPanel Orientation="Vertical"
Spacing="0"
Padding="0">
<!-- News -->
<Button Content="{Binding LocalizedStrings[News]}"
Command="{Binding NavigateToNewsCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- Books -->
<Button Content="{Binding LocalizedStrings[Books]}"
Command="{Binding NavigateToBooksCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- Companies -->
<Button Content="{Binding LocalizedStrings[Companies]}"
Command="{Binding NavigateToCompaniesCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- Computers -->
<Button Content="{Binding LocalizedStrings[Computers]}"
Command="{Binding NavigateToComputersCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- Consoles -->
<Button Content="{Binding LocalizedStrings[Consoles]}"
Command="{Binding NavigateToConsolesCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- Documents -->
<Button Content="{Binding LocalizedStrings[Documents]}"
Command="{Binding NavigateToDocumentsCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- Dumps -->
<Button Content="{Binding LocalizedStrings[Dumps]}"
Command="{Binding NavigateToDumpsCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- Graphical Processing Units -->
<Button Content="{Binding LocalizedStrings[GraphicalProcessingUnits]}"
Command="{Binding NavigateToGraphicalProcessingUnitsCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- Magazines -->
<Button Content="{Binding LocalizedStrings[Magazines]}"
Command="{Binding NavigateToMagazinesCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- People -->
<Button Content="{Binding LocalizedStrings[People]}"
Command="{Binding NavigateToPeopleCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- Processors -->
<Button Content="{Binding LocalizedStrings[Processors]}"
Command="{Binding NavigateToProcessorsCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- Software -->
<Button Content="{Binding LocalizedStrings[Software]}"
Command="{Binding NavigateToSoftwareCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- Sound Synthesizers -->
<Button Content="{Binding LocalizedStrings[SoundSynthesizers]}"
Command="{Binding NavigateToSoundSynthesizersCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
</StackPanel>
</ScrollViewer>
<!-- Bottom Fixed Items - Hidden when collapsed -->
<Grid Grid.Row="2"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,1,0,0"
Padding="0"
Visibility="{Binding IsSidebarOpen}">
<StackPanel Orientation="Vertical"
Spacing="0">
<!-- Users (Uberadmin only) -->
<Button Content="User Management"
Command="{Binding NavigateToUsersCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0"
Visibility="{Binding IsUberadminUser, Converter={StaticResource BoolToVisibilityConverter}}" />
<!-- Login/Logout -->
<Button Content="{Binding LoginLogoutButtonText}"
Command="{Binding LoginLogoutCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
<!-- Settings -->
<Button Content="{Binding LocalizedStrings[Settings]}"
Command="{Binding NavigateToSettingsCommand}"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left"
Padding="16,10,16,10"
FontSize="13"
Background="Transparent"
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
BorderThickness="0"
CornerRadius="0" />
</StackPanel>
</Grid>
</Grid>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Microsoft.UI.Xaml.Controls;
namespace Marechai.App.Presentation.Components;
public sealed partial class Sidebar : UserControl
{
public Sidebar()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,51 @@
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace Marechai.App.Presentation.Converters;
/// <summary>
/// Converts null object to Collapsed visibility
/// </summary>
public class ObjectToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language) =>
value != null ? Visibility.Visible : Visibility.Collapsed;
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotImplementedException();
}
/// <summary>
/// Converts empty/null string to Collapsed visibility
/// </summary>
public class StringToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if(value is string str && !string.IsNullOrWhiteSpace(str)) return Visibility.Visible;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotImplementedException();
}
/// <summary>
/// Converts zero count to Collapsed visibility, otherwise Visible
/// </summary>
public class ZeroToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if(value is int count && count > 0) return Visibility.Visible;
if(value is long longCount && longCount > 0) return Visibility.Visible;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotImplementedException();
}

View File

@@ -0,0 +1,20 @@
using System;
using Microsoft.UI.Xaml.Data;
namespace Marechai.App.Presentation.Converters;
/// <summary>
/// Converts DateTime to formatted foundation date string, returns empty if null
/// </summary>
public class FoundationDateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if(value is DateTime dateTime) return dateTime.ToString("MMMM d, yyyy");
return string.Empty;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotImplementedException();
}

View File

@@ -0,0 +1,59 @@
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace Marechai.App.Presentation.Converters;
/// <summary>
/// Converts boolean value to collapse/expand arrow icon
/// </summary>
public class CollapseExpandIconConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if(value is bool isOpen) return isOpen ? "◄" : "►";
return "►";
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotImplementedException();
}
/// <summary>
/// Converts boolean value to collapse/expand tooltip text
/// </summary>
public class CollapseExpandTooltipConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if(value is bool isOpen) return isOpen ? "Collapse Sidebar" : "Expand Sidebar";
return "Expand Sidebar";
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotImplementedException();
}
/// <summary>
/// Converts boolean value to GridLength for sidebar column width
/// </summary>
public class SidebarWidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if(value is bool isOpen)
{
// 280 when open, 60 when collapsed (to keep toggle button visible)
double width = isOpen ? 280 : 60;
return new GridLength(width, GridUnitType.Pixel);
}
return new GridLength(280, GridUnitType.Pixel);
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotImplementedException();
}

View File

@@ -1,29 +0,0 @@
<Page x:Class="Marechai.App.Presentation.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Marechai.App.Presentation"
xmlns:uen="using:Uno.Extensions.Navigation.UI"
xmlns:utu="using:Uno.Toolkit.UI"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<ScrollViewer IsTabStop="True">
<Grid utu:SafeArea.Insets="VisibleBounds">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<utu:NavigationBar Content="{Binding Title}" />
<StackPanel Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Spacing="16">
<TextBox Text="{Binding Name, Mode=TwoWay}"
PlaceholderText="Enter your name:" />
<Button Content="Go to Second Page"
AutomationProperties.AutomationId="SecondPageButton"
Command="{Binding GoToSecond}" />
</StackPanel>
</Grid>
</ScrollViewer>
</Page>

View File

@@ -1,11 +0,0 @@
using Microsoft.UI.Xaml.Controls;
namespace Marechai.App.Presentation;
public sealed partial class MainPage : Page
{
public MainPage()
{
this.InitializeComponent();
}
}

View File

@@ -1,33 +0,0 @@
using System.Threading.Tasks;
using System.Windows.Input;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation;
public partial class MainViewModel : ObservableObject
{
private INavigator _navigator;
[ObservableProperty] private string? name;
public MainViewModel(
IStringLocalizer localizer,
IOptions<AppConfig> appInfo,
INavigator navigator)
{
_navigator = navigator;
Title = "Main";
Title += $" - {localizer["ApplicationName"]}";
Title += $" - {appInfo?.Value?.Environment}";
GoToSecond = new AsyncRelayCommand(GoToSecondView);
}
public string? Title { get; }
public ICommand GoToSecond { get; }
private async Task GoToSecondView()
{
await _navigator.NavigateViewModelAsync<SecondViewModel>(this, data: new Entity(Name!));
}
}

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 CompanyDetailPage containing both the company ID and the navigation source.
/// </summary>
public class CompanyDetailNavigationParameter
{
public required int CompanyId { get; init; }
public object? NavigationSource { get; init; }
}

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 GpuDetailPage containing both the GPU ID and the navigation source.
/// </summary>
public class GpuDetailNavigationParameter
{
public required int GpuId { get; init; }
public object? NavigationSource { get; init; }
}

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 MachineViewPage containing both the machine ID and the navigation source.
/// </summary>
public class MachineViewNavigationParameter
{
public required int MachineId { get; init; }
public object? NavigationSource { get; init; }
}

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

@@ -0,0 +1,9 @@
#nullable enable
namespace Marechai.App.Presentation.Models;
public class SoundSynthDetailNavigationParameter
{
public int SoundSynthId { get; set; }
public object? NavigationSource { get; set; }
}

View File

@@ -1,27 +0,0 @@
<Page x:Class="Marechai.App.Presentation.SecondPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Marechai.App.Presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:uen="using:Uno.Extensions.Navigation.UI"
xmlns:utu="using:Uno.Toolkit.UI"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid utu:SafeArea.Insets="VisibleBounds">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
</Grid.RowDefinitions>
<utu:NavigationBar Content="Second Page" />
<StackPanel Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="{Binding Entity.Name}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="8" />
</StackPanel>
</Grid>
</Page>

View File

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

View File

@@ -1,36 +0,0 @@
<UserControl x:Class="Marechai.App.Presentation.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Marechai.App.Presentation"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:utu="using:Uno.Toolkit.UI"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Border Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<utu:ExtendedSplashScreen x:Name="Splash"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
<utu:ExtendedSplashScreen.LoadingContentTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition />
</Grid.RowDefinitions>
<ProgressRing IsActive="True"
Grid.Row="1"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Height="100"
Width="100" />
</Grid>
</DataTemplate>
</utu:ExtendedSplashScreen.LoadingContentTemplate>
</utu:ExtendedSplashScreen>
</Border>
</UserControl>

View File

@@ -1,15 +0,0 @@
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
}
}

View File

@@ -0,0 +1,220 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Marechai.App.Presentation.Models;
using Marechai.App.Services;
using Marechai.App.Services.Caching;
using Microsoft.UI.Xaml.Media.Imaging;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation.ViewModels;
public partial class CompaniesViewModel : ObservableObject
{
private readonly List<CompanyListItem> _allCompanies = [];
private readonly CompaniesService _companiesService;
private readonly IStringLocalizer _localizer;
private readonly ILogger<CompaniesViewModel> _logger;
private readonly CompanyLogoCache _logoCache;
private readonly INavigator _navigator;
[ObservableProperty]
private ObservableCollection<CompanyListItem> _companiesList = [];
[ObservableProperty]
private int _companyCount;
[ObservableProperty]
private string _companyCountText = string.Empty;
[ObservableProperty]
private string _errorMessage = string.Empty;
[ObservableProperty]
private bool _hasError;
[ObservableProperty]
private bool _isDataLoaded;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _searchQuery = string.Empty;
public CompaniesViewModel(CompaniesService companiesService, CompanyLogoCache logoCache, IStringLocalizer localizer,
ILogger<CompaniesViewModel> logger, INavigator navigator)
{
_companiesService = companiesService;
_logoCache = logoCache;
_localizer = localizer;
_logger = logger;
_navigator = navigator;
LoadData = new AsyncRelayCommand(LoadDataAsync);
GoBackCommand = new AsyncRelayCommand(GoBackAsync);
NavigateToCompanyCommand = new AsyncRelayCommand<CompanyListItem>(NavigateToCompanyAsync);
}
public IAsyncRelayCommand LoadData { get; }
public ICommand GoBackCommand { get; }
public IAsyncRelayCommand<CompanyListItem> NavigateToCompanyCommand { get; }
public string Title { get; } = "Companies";
partial void OnSearchQueryChanged(string value)
{
// Automatically filter when SearchQuery changes
UpdateFilter(value);
}
/// <summary>
/// Loads companies count and list from the API
/// </summary>
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
CompaniesList.Clear();
_allCompanies.Clear();
// Load companies
List<CompanyDto> companies = await _companiesService.GetAllCompaniesAsync();
// Set count
CompanyCount = companies.Count;
CompanyCountText = _localizer["Companies in the database"];
// Build the full list in memory
foreach(CompanyDto company in companies)
{
// Extract id from company
int companyId = company.Id ?? 0;
// Convert DateTimeOffset? to DateTime?
DateTime? foundedDate = company.Founded?.DateTime;
// Load logo if available
SvgImageSource? logoSource = null;
if(company.LastLogo.HasValue)
{
try
{
Stream? logoStream = await _logoCache.GetLogoAsync(company.LastLogo.Value);
logoSource = new SvgImageSource();
await logoSource.SetSourceAsync(logoStream.AsRandomAccessStream());
}
catch(Exception ex)
{
_logger.LogWarning("Failed to load logo for company {CompanyId}: {Exception}",
companyId,
ex.Message);
}
}
_allCompanies.Add(new CompanyListItem
{
Id = companyId,
Name = company.Name ?? string.Empty,
FoundationDate = foundedDate,
LogoImageSource = logoSource
});
}
// Apply current filter (will show all if SearchQuery is empty)
UpdateFilter(SearchQuery);
if(CompaniesList.Count == 0)
{
ErrorMessage = _localizer["No companies found"].Value;
HasError = true;
}
else
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError("Error loading companies data: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load companies 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 company detail view
/// </summary>
private async Task NavigateToCompanyAsync(CompanyListItem? company)
{
if(company is null) return;
_logger.LogInformation("Navigating to company: {CompanyName} (ID: {CompanyId})", company.Name, company.Id);
// Navigate to company detail view with navigation parameter
var navParam = new CompanyDetailNavigationParameter
{
CompanyId = company.Id,
NavigationSource = this
};
await _navigator.NavigateViewModelAsync<CompanyDetailViewModel>(this, data: navParam);
}
/// <summary>
/// Updates the filtered list based on search query
/// </summary>
private void UpdateFilter(string? query)
{
string lowerQuery = string.IsNullOrWhiteSpace(query) ? string.Empty : query.Trim().ToLowerInvariant();
CompaniesList.Clear();
if(string.IsNullOrEmpty(lowerQuery))
{
// No filter, show all companies
foreach(CompanyListItem company in _allCompanies) CompaniesList.Add(company);
}
else
{
// Filter companies by name (case-insensitive)
var filtered = _allCompanies.Where(c => c.Name.Contains(lowerQuery, StringComparison.OrdinalIgnoreCase))
.ToList();
foreach(CompanyListItem company in filtered) CompaniesList.Add(company);
}
}
}
/// <summary>
/// Data model for a company in the list
/// </summary>
public class CompanyListItem
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public DateTime? FoundationDate { get; set; }
public SvgImageSource? LogoImageSource { get; set; }
public string FoundationDateDisplay =>
FoundationDate.HasValue ? FoundationDate.Value.ToString("MMMM d, yyyy") : string.Empty;
}

View File

@@ -0,0 +1,530 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Marechai.App.Presentation.Models;
using Marechai.App.Services;
using Marechai.App.Services.Caching;
using Marechai.Data;
using Microsoft.UI.Xaml.Media.Imaging;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation.ViewModels;
public partial class CompanyDetailViewModel : ObservableObject
{
private readonly CompanyDetailService _companyDetailService;
private readonly FlagCache _flagCache;
private readonly IStringLocalizer _localizer;
private readonly ILogger<CompanyDetailViewModel> _logger;
private readonly CompanyLogoCache _logoCache;
private readonly INavigator _navigator;
[ObservableProperty]
private CompanyDto? _company;
[ObservableProperty]
private int _companyId;
[ObservableProperty]
private ObservableCollection<CompanyLogoItem> _companyLogos = [];
[ObservableProperty]
private ObservableCollection<CompanyDetailMachine> _computers = [];
[ObservableProperty]
private string _computersFilterText = string.Empty;
[ObservableProperty]
private string _consoelsFilterText = string.Empty;
[ObservableProperty]
private ObservableCollection<CompanyDetailMachine> _consoles = [];
[ObservableProperty]
private string _errorMessage = string.Empty;
[ObservableProperty]
private ObservableCollection<CompanyDetailMachine> _filteredComputers = [];
[ObservableProperty]
private ObservableCollection<CompanyDetailMachine> _filteredConsoles = [];
[ObservableProperty]
private SvgImageSource? _flagImageSource;
[ObservableProperty]
private bool _hasError;
[ObservableProperty]
private bool _isDataLoaded;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private SvgImageSource? _logoImageSource;
[ObservableProperty]
private CompanyDto? _soldToCompany;
public CompanyDetailViewModel(CompanyDetailService companyDetailService, FlagCache flagCache,
CompanyLogoCache logoCache, IStringLocalizer localizer,
ILogger<CompanyDetailViewModel> logger, INavigator navigator)
{
_companyDetailService = companyDetailService;
_flagCache = flagCache;
_logoCache = logoCache;
_localizer = localizer;
_logger = logger;
_navigator = navigator;
LoadData = new AsyncRelayCommand(LoadDataAsync);
GoBackCommand = new AsyncRelayCommand(GoBackAsync);
NavigateToMachineCommand = new AsyncRelayCommand<CompanyDetailMachine>(NavigateToMachineAsync);
}
/// <summary>
/// Gets the display text for the company's status
/// </summary>
public string CompanyStatusDisplay => Company != null ? GetStatusMessage(Company) : string.Empty;
/// <summary>
/// Gets the display text for the company's founded date
/// </summary>
public string CompanyFoundedDateDisplay => Company != null ? GetFoundedDateDisplay(Company) : string.Empty;
/// <summary>
/// Gets whether flag content is available
/// </summary>
public bool HasFlagContent => FlagImageSource != null;
/// <summary>
/// Gets whether logo content is available
/// </summary>
public bool HasLogoContent => LogoImageSource != null;
/// <summary>
/// Gets whether company has multiple logos
/// </summary>
public bool HasMultipleLogos => CompanyLogos.Count > 1;
public IAsyncRelayCommand LoadData { get; }
public ICommand GoBackCommand { get; }
public IAsyncRelayCommand<CompanyDetailMachine> NavigateToMachineCommand { get; }
public string Title { get; } = "Company Details";
partial void OnCompanyChanged(CompanyDto? oldValue, CompanyDto? newValue)
{
// Notify that computed properties have changed
OnPropertyChanged(nameof(CompanyStatusDisplay));
OnPropertyChanged(nameof(CompanyFoundedDateDisplay));
}
partial void OnFlagImageSourceChanged(SvgImageSource? oldValue, SvgImageSource? newValue)
{
// Notify that HasFlagContent has changed
OnPropertyChanged(nameof(HasFlagContent));
}
partial void OnLogoImageSourceChanged(SvgImageSource? oldValue, SvgImageSource? newValue)
{
// Notify that HasLogoContent has changed
OnPropertyChanged(nameof(HasLogoContent));
}
partial void OnCompanyLogosChanged(ObservableCollection<CompanyLogoItem>? oldValue,
ObservableCollection<CompanyLogoItem> newValue)
{
// Notify that HasMultipleLogos has changed
OnPropertyChanged(nameof(HasMultipleLogos));
}
partial void OnComputersFilterTextChanged(string value)
{
FilterComputers(value);
}
partial void OnConsoelsFilterTextChanged(string value)
{
FilterConsoles(value);
}
private void FilterComputers(string filterText)
{
ObservableCollection<CompanyDetailMachine> filtered = string.IsNullOrWhiteSpace(filterText)
? new ObservableCollection<
CompanyDetailMachine>(Computers)
: new
ObservableCollection<
CompanyDetailMachine>(Computers.Where(c =>
c.Name.Contains(filterText,
StringComparison
.OrdinalIgnoreCase)));
FilteredComputers = filtered;
}
private void FilterConsoles(string filterText)
{
ObservableCollection<CompanyDetailMachine> filtered = string.IsNullOrWhiteSpace(filterText)
? new ObservableCollection<
CompanyDetailMachine>(Consoles)
: new
ObservableCollection<
CompanyDetailMachine>(Consoles.Where(c =>
c.Name.Contains(filterText,
StringComparison
.OrdinalIgnoreCase)));
FilteredConsoles = filtered;
}
private async Task NavigateToMachineAsync(CompanyDetailMachine? machine)
{
if(machine == null) return;
var navParam = new MachineViewNavigationParameter
{
MachineId = machine.Id,
NavigationSource = this
};
await _navigator.NavigateViewModelAsync<MachineViewViewModel>(this, data: navParam);
}
/// <summary>
/// Gets the formatted founding date with unknown handling
/// </summary>
public string GetFoundedDateDisplay(CompanyDto company)
{
if(company.Founded is null) return string.Empty;
DateTime date = company.Founded.Value.DateTime;
if(company.FoundedMonthIsUnknown ?? false) return $"{date.Year}.";
if(company.FoundedDayIsUnknown ?? false) return $"{date:Y}.";
return $"{date:D}.";
}
/// <summary>
/// Gets the formatted sold/event date with unknown handling
/// </summary>
public string GetEventDateDisplay(CompanyDto? company, bool monthUnknown = false, bool dayUnknown = false)
{
if(company?.Sold is null) return _localizer["unknown date"].Value;
DateTime date = company.Sold.Value.DateTime;
if(monthUnknown || (company.SoldMonthIsUnknown ?? false)) return $"{date.Year}";
if(dayUnknown || (company.SoldDayIsUnknown ?? false)) return $"{date:Y}";
return $"{date:D}";
}
/// <summary>
/// Gets the status message for the company
/// </summary>
public string GetStatusMessage(CompanyDto company)
{
return company.Status switch
{
1 => _localizer["Company is active."].Value,
2 => GetSoldStatusMessage(company),
3 => GetMergedStatusMessage(company),
4 => GetBankruptcyMessage(company),
5 => GetDefunctMessage(company),
6 => GetRenamedStatusMessage(company),
_ => _localizer["Current company status is unknown."].Value
};
}
private string GetSoldStatusMessage(CompanyDto company)
{
if(SoldToCompany != null)
{
return string.Format(_localizer["Company sold to {0} on {1}."].Value,
SoldToCompany.Name,
GetEventDateDisplay(company));
}
if(company.Sold != null)
{
return string.Format(_localizer["Company sold on {0} to an unknown company."].Value,
GetEventDateDisplay(company));
}
return SoldToCompany != null
? string.Format(_localizer["Company sold to {0} on an unknown date."].Value, SoldToCompany.Name)
: _localizer["Company was sold to an unknown company on an unknown date."].Value;
}
private string GetMergedStatusMessage(CompanyDto company)
{
if(SoldToCompany != null)
{
return string.Format(_localizer["Company merged on {0} to form {1}."].Value,
GetEventDateDisplay(company),
SoldToCompany.Name);
}
if(company.Sold != null)
{
return string.Format(_localizer["Company merged on {0} to form an unknown company."].Value,
GetEventDateDisplay(company));
}
return SoldToCompany != null
? string.Format(_localizer["Company merged on an unknown date to form {0}."].Value,
SoldToCompany.Name)
: _localizer["Company merged to form an unknown company on an unknown date."].Value;
}
private string GetBankruptcyMessage(CompanyDto company) => company.Sold != null
? string.Format(_localizer
["Company declared bankruptcy on {0}."]
.Value,
GetEventDateDisplay(company))
: _localizer
["Company declared bankruptcy on an unknown date."]
.Value;
private string GetDefunctMessage(CompanyDto company) => company.Sold != null
? string.Format(_localizer
["Company ceased operations on {0}."]
.Value,
GetEventDateDisplay(company))
: _localizer
["Company ceased operations on an unknown date."]
.Value;
private string GetRenamedStatusMessage(CompanyDto company)
{
if(SoldToCompany != null)
{
return string.Format(_localizer["Company renamed to {0} on {1}."].Value,
SoldToCompany.Name,
GetEventDateDisplay(company));
}
if(company.Sold != null)
{
return string.Format(_localizer["Company was renamed on {0} to an unknown name."].Value,
GetEventDateDisplay(company));
}
return SoldToCompany != null
? string.Format(_localizer["Company renamed to {0} on an unknown date."].Value, SoldToCompany.Name)
: _localizer["Company renamed to an unknown name on an unknown date."].Value;
}
/// <summary>
/// Loads company details from the API
/// </summary>
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
FlagImageSource = null;
LogoImageSource = null;
CompanyLogos.Clear();
if(CompanyId <= 0)
{
ErrorMessage = _localizer["Invalid company ID."].Value;
HasError = true;
return;
}
// Load company details
Company = await _companyDetailService.GetCompanyByIdAsync(CompanyId);
if(Company is null)
{
ErrorMessage = _localizer["Company not found."].Value;
HasError = true;
return;
}
// Load flag if country is available
if(Company.CountryId is not null)
{
try
{
var countryCode = (short)(Company.CountryId ?? 0);
Stream? flagStream = await _flagCache.GetFlagAsync(countryCode);
var flagSource = new SvgImageSource();
await flagSource.SetSourceAsync(flagStream.AsRandomAccessStream());
FlagImageSource = flagSource;
_logger.LogInformation("Successfully loaded flag for country code {CountryCode}", countryCode);
}
catch(Exception ex)
{
_logger.LogError("Failed to load flag for country {CountryId}: {Exception}",
Company.CountryId,
ex.Message);
// Continue without flag if loading fails
}
}
if(Company.SoldToId != null)
{
int soldToId = Company.SoldToId ?? 0;
if(soldToId > 0) SoldToCompany = await _companyDetailService.GetSoldToCompanyAsync(soldToId);
}
// Load logo if available
if(Company.LastLogo.HasValue)
{
try
{
Stream? logoStream = await _logoCache.GetLogoAsync(Company.LastLogo.Value);
var logoSource = new SvgImageSource();
await logoSource.SetSourceAsync(logoStream.AsRandomAccessStream());
LogoImageSource = logoSource;
_logger.LogInformation("Successfully loaded logo for company {CompanyId}", CompanyId);
}
catch(Exception ex)
{
_logger.LogError("Failed to load logo for company {CompanyId}: {Exception}", CompanyId, ex.Message);
// Continue without logo if loading fails
}
}
// Load all logos for carousel
try
{
// Get all logos for this company
List<CompanyLogoDto> logosList = await _companyDetailService.GetCompanyLogosAsync(CompanyId);
// Convert to list with extracted years for sorting
var logosWithYears = logosList.Select(logo => new
{
Logo = logo,
logo.Year
})
.OrderBy(l => l.Year)
.ToList();
var loadedLogos = new ObservableCollection<CompanyLogoItem>();
foreach(var logoData in logosWithYears)
{
try
{
if(logoData.Logo.Guid == null) continue;
Stream? logoStream = await _logoCache.GetLogoAsync(logoData.Logo.Guid.Value);
var logoSource = new SvgImageSource();
await logoSource.SetSourceAsync(logoStream.AsRandomAccessStream());
loadedLogos.Add(new CompanyLogoItem
{
LogoGuid = logoData.Logo.Guid.Value,
LogoSource = logoSource,
Year = logoData.Year
});
}
catch(Exception ex)
{
_logger.LogError("Failed to load carousel logo: {Exception}", ex.Message);
}
}
// Assign the new collection (this will trigger OnCompanyLogosChanged)
CompanyLogos = loadedLogos;
_logger.LogInformation("Loaded {Count} logos for company {CompanyId}", CompanyLogos.Count, CompanyId);
}
catch(Exception ex)
{
_logger.LogError("Failed to load company logos for carousel: {Exception}", ex.Message);
}
// Load computers and consoles made by this company
List<MachineDto> machines = await _companyDetailService.GetComputersByCompanyAsync(CompanyId);
Computers.Clear();
Consoles.Clear();
FilteredComputers.Clear();
FilteredConsoles.Clear();
foreach(MachineDto machine in machines)
{
int machineId = machine.Id ?? 0;
var machineItem = new CompanyDetailMachine
{
Id = machineId,
Name = machine.Name ?? string.Empty
};
// Categorize by machine type enum
if(machine.Type == (int)MachineType.Computer)
Computers.Add(machineItem);
else if(machine.Type == (int)MachineType.Console) Consoles.Add(machineItem);
}
// Initialize filtered lists
FilteredComputers = new ObservableCollection<CompanyDetailMachine>(Computers);
FilteredConsoles = new ObservableCollection<CompanyDetailMachine>(Consoles);
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError("Error loading company details: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load company details. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
/// <summary>
/// Handles back navigation
/// </summary>
private async Task GoBackAsync()
{
await _navigator.NavigateViewModelAsync<CompaniesViewModel>(this);
}
}
/// <summary>
/// Data model for a machine in the company detail view
/// </summary>
public class CompanyDetailMachine
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
/// <summary>
/// Data model for a company logo in the carousel
/// </summary>
public class CompanyLogoItem
{
public Guid LogoGuid { get; set; }
public SvgImageSource? LogoSource { get; set; }
public int? Year { get; set; }
}

View File

@@ -0,0 +1,250 @@
#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;
/// <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 = computer.Id ?? 0;
_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.ViewModels;
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.NavigateRouteAsync(this, "list-computers");
}
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.NavigateRouteAsync(this, "list-computers");
}
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.NavigateRouteAsync(this, "list-computers");
}
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,248 @@
#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;
/// <summary>
/// ViewModel for displaying a filtered list of consoles
/// </summary>
public partial class ConsolesListViewModel : ObservableObject
{
private readonly ConsolesService _consolesService;
private readonly IConsolesListFilterContext _filterContext;
private readonly IStringLocalizer _localizer;
private readonly ILogger<ConsolesListViewModel> _logger;
private readonly INavigator _navigator;
[ObservableProperty]
private ObservableCollection<ConsoleListItem> _consolesList = [];
[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 ConsolesListViewModel(ConsolesService consolesService, IStringLocalizer localizer,
ILogger<ConsolesListViewModel> logger, INavigator navigator,
IConsolesListFilterContext filterContext)
{
_consolesService = consolesService;
_localizer = localizer;
_logger = logger;
_navigator = navigator;
_filterContext = filterContext;
LoadData = new AsyncRelayCommand(LoadDataAsync);
GoBackCommand = new AsyncRelayCommand(GoBackAsync);
NavigateToConsoleCommand = new AsyncRelayCommand<ConsoleListItem>(NavigateToConsoleAsync);
}
public IAsyncRelayCommand LoadData { get; }
public ICommand GoBackCommand { get; }
public IAsyncRelayCommand<ConsoleListItem> NavigateToConsoleCommand { get; }
/// <summary>
/// Gets or sets the filter type
/// </summary>
public ConsoleListFilterType 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 consoles based on the current filter
/// </summary>
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
ConsolesList.Clear();
_logger.LogInformation("LoadDataAsync called. FilterType={FilterType}, FilterValue={FilterValue}",
FilterType,
FilterValue);
// Update title and filter description based on filter type
UpdateFilterDescription();
// Load consoles from the API based on the current filter
await LoadConsolesFromApiAsync();
_logger.LogInformation("LoadConsolesFromApiAsync completed. ConsolesList.Count={Count}",
ConsolesList.Count);
if(ConsolesList.Count == 0)
{
ErrorMessage = _localizer["No consoles found for this filter"].Value;
HasError = true;
_logger.LogWarning("No consoles found for filter: {FilterType} {FilterValue}", FilterType, FilterValue);
}
else
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading consoles: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load consoles. 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 ConsoleListFilterType.All:
PageTitle = _localizer["All Consoles"];
FilterDescription = _localizer["Browsing all consoles in the database"];
break;
case ConsoleListFilterType.Letter:
if(!string.IsNullOrEmpty(FilterValue) && FilterValue.Length == 1)
{
PageTitle = $"{_localizer["Consoles Starting with"]} {FilterValue}";
FilterDescription = $"{_localizer["Showing consoles that start with"]} {FilterValue}";
}
break;
case ConsoleListFilterType.Year:
if(!string.IsNullOrEmpty(FilterValue) && int.TryParse(FilterValue, out int year))
{
PageTitle = $"{_localizer["Consoles from"]} {year}";
FilterDescription = $"{_localizer["Showing consoles released in"]} {year}";
}
break;
}
}
/// <summary>
/// Loads consoles from the API based on the current filter
/// </summary>
private async Task LoadConsolesFromApiAsync()
{
try
{
List<MachineDto> consoles = FilterType switch
{
ConsoleListFilterType.Letter when FilterValue.Length == 1 =>
await _consolesService.GetConsolesByLetterAsync(FilterValue[0]),
ConsoleListFilterType.Year when int.TryParse(FilterValue, out int year) =>
await _consolesService.GetConsolesByYearAsync(year),
_ => await _consolesService.GetAllConsolesAsync()
};
// Add consoles to the list sorted by name
foreach(MachineDto console in consoles.OrderBy(c => c.Name))
{
int year = console.Introduced?.Year ?? 0;
int id = console.Id ?? 0;
_logger.LogInformation("Console: {Name}, Introduced: {Introduced}, Year: {Year}, Company: {Company}, ID: {Id}",
console.Name,
console.Introduced,
year,
console.Company,
id);
ConsolesList.Add(new ConsoleListItem
{
Id = id,
Name = console.Name ?? string.Empty,
Year = year,
Manufacturer = console.Company ?? string.Empty
});
}
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading consoles from API");
}
}
/// <summary>
/// Navigates back to the consoles main view
/// </summary>
private async Task GoBackAsync()
{
await _navigator.NavigateViewModelAsync<ConsolesViewModel>(this);
}
/// <summary>
/// Navigates to the console detail view
/// </summary>
private async Task NavigateToConsoleAsync(ConsoleListItem? console)
{
if(console is null) return;
_logger.LogInformation("Navigating to console detail: {ConsoleName} (ID: {ConsoleId})",
console.Name,
console.Id);
var navParam = new MachineViewNavigationParameter
{
MachineId = console.Id,
NavigationSource = this
};
await _navigator.NavigateViewModelAsync<MachineViewViewModel>(this, data: navParam);
}
}
/// <summary>
/// Data model for a console in the list
/// </summary>
public class ConsoleListItem
{
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.ViewModels;
public partial class ConsolesViewModel : ObservableObject
{
private readonly ConsolesService _consolesService;
private readonly IConsolesListFilterContext _filterContext;
private readonly IStringLocalizer _localizer;
private readonly ILogger<ConsolesViewModel> _logger;
private readonly INavigator _navigator;
[ObservableProperty]
private int _consoleCount;
[ObservableProperty]
private string _consoleCountText = 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 ConsolesViewModel(ConsolesService consolesService, IStringLocalizer localizer,
ILogger<ConsolesViewModel> logger, INavigator navigator,
IConsolesListFilterContext filterContext)
{
_consolesService = consolesService;
_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);
NavigateAllConsolesCommand = new AsyncRelayCommand(NavigateAllConsolesAsync);
InitializeLetters();
}
public IAsyncRelayCommand LoadData { get; }
public ICommand GoBackCommand { get; }
public IAsyncRelayCommand<char> NavigateByLetterCommand { get; }
public IAsyncRelayCommand<int> NavigateByYearCommand { get; }
public IAsyncRelayCommand NavigateAllConsolesCommand { get; }
public string Title { get; } = "Consoles";
/// <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 consoles 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 = _consolesService.GetConsolesCountAsync();
Task<int> minYearTask = _consolesService.GetMinimumYearAsync();
Task<int> maxYearTask = _consolesService.GetMaximumYearAsync();
await Task.WhenAll(countTask, minYearTask, maxYearTask);
ConsoleCount = countTask.Result;
MinimumYear = minYearTask.Result;
MaximumYear = maxYearTask.Result;
// Update display text
ConsoleCountText = _localizer["Consoles 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(ConsoleCount == 0)
{
ErrorMessage = _localizer["No consoles found"].Value;
HasError = true;
}
else
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError("Error loading consoles data: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load consoles 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 consoles filtered by letter
/// </summary>
private async Task NavigateByLetterAsync(char letter)
{
try
{
_logger.LogInformation("Navigating to consoles by letter: {Letter}", letter);
_filterContext.FilterType = ConsoleListFilterType.Letter;
_filterContext.FilterValue = letter.ToString();
await _navigator.NavigateRouteAsync(this, "list-consoles");
}
catch(Exception ex)
{
_logger.LogError("Error navigating to letter consoles: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to navigate. Please try again."].Value;
HasError = true;
}
}
/// <summary>
/// Navigates to consoles filtered by year
/// </summary>
private async Task NavigateByYearAsync(int year)
{
try
{
_logger.LogInformation("Navigating to consoles by year: {Year}", year);
_filterContext.FilterType = ConsoleListFilterType.Year;
_filterContext.FilterValue = year.ToString();
await _navigator.NavigateRouteAsync(this, "list-consoles");
}
catch(Exception ex)
{
_logger.LogError("Error navigating to year consoles: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to navigate. Please try again."].Value;
HasError = true;
}
}
/// <summary>
/// Navigates to all consoles view
/// </summary>
private async Task NavigateAllConsolesAsync()
{
try
{
_logger.LogInformation("Navigating to all consoles");
_filterContext.FilterType = ConsoleListFilterType.All;
_filterContext.FilterValue = string.Empty;
await _navigator.NavigateRouteAsync(this, "list-consoles");
}
catch(Exception ex)
{
_logger.LogError("Error navigating to all consoles: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to navigate. Please try again."].Value;
HasError = true;
}
}
}

View File

@@ -0,0 +1,387 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Input;
using Marechai.App.Presentation.Models;
using Marechai.App.Services;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation.ViewModels;
public partial class GpuDetailViewModel : ObservableObject
{
private readonly CompaniesService _companiesService;
private readonly GpusService _gpusService;
private readonly IStringLocalizer _localizer;
private readonly ILogger<GpuDetailViewModel> _logger;
private readonly INavigator _navigator;
[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 GpuDto? _gpu;
[ObservableProperty]
private int _gpuId;
[ObservableProperty]
private bool _hasComputers;
[ObservableProperty]
private bool _hasConsoles;
[ObservableProperty]
private bool _hasError;
[ObservableProperty]
private bool _isDataLoaded;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _manufacturerName = string.Empty;
private object? _navigationSource;
[ObservableProperty]
private ObservableCollection<ResolutionItem> _resolutions = [];
public GpuDetailViewModel(GpusService gpusService, CompaniesService companiesService, IStringLocalizer localizer,
ILogger<GpuDetailViewModel> logger, INavigator navigator)
{
_gpusService = gpusService;
_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; } = "GPU Details";
/// <summary>
/// Loads GPU details including resolutions, computers, and consoles
/// </summary>
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
Resolutions.Clear();
Computers.Clear();
Consoles.Clear();
if(GpuId <= 0)
{
ErrorMessage = _localizer["Invalid GPU ID"].Value;
HasError = true;
return;
}
_logger.LogInformation("Loading GPU details for ID: {GpuId}", GpuId);
// Load GPU details
Gpu = await _gpusService.GetGpuByIdAsync(GpuId);
if(Gpu is null)
{
ErrorMessage = _localizer["Graphics processing unit not found"].Value;
HasError = true;
return;
}
// Set manufacturer name (from Company field or fetch by CompanyId if empty)
ManufacturerName = Gpu.Company ?? string.Empty;
if(string.IsNullOrEmpty(ManufacturerName) && Gpu.CompanyId.HasValue)
{
try
{
CompanyDto? company = await _companiesService.GetCompanyByIdAsync(Gpu.CompanyId.Value);
if(company != null) ManufacturerName = company.Name ?? string.Empty;
}
catch(Exception ex)
{
_logger.LogWarning(ex, "Failed to load company for GPU {GpuId}", GpuId);
}
}
// Format display name
string displayName = Gpu.Name ?? string.Empty;
if(displayName == "DB_FRAMEBUFFER")
displayName = "Framebuffer";
else if(displayName == "DB_SOFTWARE")
displayName = "Software";
else if(displayName == "DB_NONE") displayName = "None";
_logger.LogInformation("GPU loaded: {Name}, Company: {Company}", displayName, ManufacturerName);
// Load resolutions
try
{
List<ResolutionByGpuDto>? resolutions = await _gpusService.GetResolutionsByGpuAsync(GpuId);
if(resolutions != null && resolutions.Count > 0)
{
Resolutions.Clear();
foreach(ResolutionByGpuDto res in resolutions)
{
// Get the full resolution DTO using the resolution ID
if(res.ResolutionId.HasValue)
{
ResolutionDto? resolutionDto =
await _gpusService.GetResolutionByIdAsync(res.ResolutionId.Value);
if(resolutionDto != null)
{
Resolutions.Add(new ResolutionItem
{
Id = resolutionDto.Id ?? 0,
Name = $"{resolutionDto.Width}x{resolutionDto.Height}",
Width = resolutionDto.Width ?? 0,
Height = resolutionDto.Height ?? 0,
Colors = resolutionDto.Colors ?? 0,
Palette = resolutionDto.Palette ?? 0,
Chars = resolutionDto.Chars ?? false,
Grayscale = resolutionDto.Grayscale ?? false
});
}
}
}
_logger.LogInformation("Loaded {Count} resolutions for GPU {GpuId}", Resolutions.Count, GpuId);
}
}
catch(Exception ex)
{
_logger.LogWarning(ex, "Failed to load resolutions for GPU {GpuId}", GpuId);
}
// Load machines and separate into computers and consoles
try
{
List<MachineDto>? machines = await _gpusService.GetMachinesByGpuAsync(GpuId);
if(machines != null && machines.Count > 0)
{
Computers.Clear();
Consoles.Clear();
foreach(MachineDto machine in machines)
{
var machineItem = new MachineItem
{
Id = machine.Id ?? 0,
Name = machine.Name ?? string.Empty,
Manufacturer = machine.Company ?? string.Empty,
Year = machine.Introduced?.Year ?? 0
};
// Distinguish between computers and consoles based on Type
if(machine.Type == 2) // MachineType.Console
Consoles.Add(machineItem);
else // MachineType.Computer or Unknown
Computers.Add(machineItem);
}
HasComputers = Computers.Count > 0;
HasConsoles = Consoles.Count > 0;
// Initialize filtered collections
FilterComputers();
FilterConsoles();
_logger.LogInformation("Loaded {ComputerCount} computers and {ConsoleCount} consoles for GPU {GpuId}",
Computers.Count,
Consoles.Count,
GpuId);
}
else
{
HasComputers = false;
HasConsoles = false;
}
}
catch(Exception ex)
{
_logger.LogWarning(ex, "Failed to load machines for GPU {GpuId}", GpuId);
HasComputers = false;
HasConsoles = false;
}
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading GPU details: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load graphics processing unit details. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
/// <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 back to the GPU 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 GPU list
await _navigator.NavigateViewModelAsync<GpusListViewModel>(this);
}
/// <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;
}
}
/// <summary>
/// Resolution item for displaying GPU supported resolutions
/// </summary>
public class ResolutionItem
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public int Width { get; set; }
public int Height { get; set; }
public long Colors { get; set; }
public long Palette { get; set; }
public bool Chars { get; set; }
public bool Grayscale { get; set; }
public string Resolution => $"{Width}x{Height}";
public string ResolutionType => Chars ? "Text" : "Pixel";
public string ResolutionDisplay => Chars ? $"{Width}x{Height} characters" : $"{Width}x{Height}";
public string ColorDisplay => Grayscale
? $"{Colors} grays"
: Palette > 0
? $"{Colors} colors from a palette of {Palette} colors"
: $"{Colors} colors";
}
/// <summary>
/// Machine item for displaying computers or consoles that use the GPU
/// </summary>
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";
}

View File

@@ -0,0 +1,211 @@
#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 GPUs
/// </summary>
public partial class GpusListViewModel : ObservableObject
{
private readonly GpusService _gpusService;
private readonly IStringLocalizer _localizer;
private readonly ILogger<GpusListViewModel> _logger;
private readonly INavigator _navigator;
[ObservableProperty]
private string _errorMessage = string.Empty;
[ObservableProperty]
private ObservableCollection<GpuListItem> _gpusList = [];
[ObservableProperty]
private bool _hasError;
[ObservableProperty]
private bool _isDataLoaded;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _pageTitle = string.Empty;
public GpusListViewModel(GpusService gpusService, IStringLocalizer localizer, ILogger<GpusListViewModel> logger,
INavigator navigator)
{
_gpusService = gpusService;
_localizer = localizer;
_logger = logger;
_navigator = navigator;
LoadData = new AsyncRelayCommand(LoadDataAsync);
NavigateToGpuCommand = new AsyncRelayCommand<GpuListItem>(NavigateToGpuAsync);
}
public IAsyncRelayCommand LoadData { get; }
public IAsyncRelayCommand<GpuListItem> NavigateToGpuCommand { get; }
/// <summary>
/// Loads all GPUs and sorts them with special handling for Framebuffer and Software
/// </summary>
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
GpusList.Clear();
_logger.LogInformation("LoadDataAsync called for GPUs");
PageTitle = _localizer["GraphicalProcessingUnits"];
// Load GPUs from the API
await LoadGpusFromApiAsync();
_logger.LogInformation("LoadGpusFromApiAsync completed. GpusList.Count={Count}", GpusList.Count);
if(GpusList.Count == 0)
{
ErrorMessage = _localizer["No graphics processing units found"].Value;
HasError = true;
_logger.LogWarning("No GPUs found");
}
else
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading GPUs: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load graphics processing units. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
/// <summary>
/// Loads GPUs from the API and sorts them with special handling for Framebuffer and Software
/// </summary>
private async Task LoadGpusFromApiAsync()
{
try
{
List<GpuDto> gpus = await _gpusService.GetAllGpusAsync();
if(gpus == null || gpus.Count == 0)
{
_logger.LogInformation("No GPUs returned from API");
return;
}
// Separate special GPUs from regular ones
var specialGpus = new List<GpuListItem>();
var regularGpus = new List<GpuListItem>();
foreach(GpuDto gpu in gpus)
{
string displayName = gpu.Name ?? string.Empty;
// Replace special database names
if(displayName == "DB_FRAMEBUFFER")
displayName = "Framebuffer";
else if(displayName == "DB_SOFTWARE")
displayName = "Software";
else if(displayName == "DB_NONE") displayName = "None";
var gpuItem = new GpuListItem
{
Id = gpu.Id ?? 0,
Name = displayName,
Company = gpu.Company ?? string.Empty,
IsSpecial = gpu.Name is "DB_FRAMEBUFFER" or "DB_SOFTWARE" or "DB_NONE"
};
if(gpuItem.IsSpecial)
specialGpus.Add(gpuItem);
else
regularGpus.Add(gpuItem);
_logger.LogInformation("GPU: {Name}, Company: {Company}, ID: {Id}, IsSpecial: {IsSpecial}",
displayName,
gpu.Company,
gpu.Id,
gpuItem.IsSpecial);
}
// Sort special GPUs: Framebuffer first, then Software, then None
specialGpus.Sort((a, b) =>
{
int orderA = a.Name == "Framebuffer"
? 0
: a.Name == "Software"
? 1
: 2;
int orderB = b.Name == "Framebuffer"
? 0
: b.Name == "Software"
? 1
: 2;
return orderA.CompareTo(orderB);
});
// Sort regular GPUs alphabetically
regularGpus.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
// Add special GPUs first, then regular GPUs
foreach(GpuListItem gpu in specialGpus) GpusList.Add(gpu);
foreach(GpuListItem gpu in regularGpus) GpusList.Add(gpu);
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading GPUs from API");
}
}
/// <summary>
/// Navigates to the GPU detail view
/// </summary>
private async Task NavigateToGpuAsync(GpuListItem? gpu)
{
if(gpu is null) return;
_logger.LogInformation("Navigating to GPU detail: {GpuName} (ID: {GpuId})", gpu.Name, gpu.Id);
// Navigate to GPU detail view with navigation parameter
var navParam = new GpuDetailNavigationParameter
{
GpuId = gpu.Id,
NavigationSource = this
};
await _navigator.NavigateViewModelAsync<GpuDetailViewModel>(this, data: navParam);
}
}
/// <summary>
/// Data model for a GPU in the list
/// </summary>
public class GpuListItem
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Company { get; set; } = string.Empty;
public bool IsSpecial { get; set; }
}

View File

@@ -0,0 +1,93 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Uno.Extensions.Authentication;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation.ViewModels;
public partial class LoginViewModel : ObservableObject
{
private readonly IAuthenticationService _authService;
private readonly INavigator _navigator;
private readonly IStringLocalizer _stringLocalizer;
[ObservableProperty]
private string _email = string.Empty;
[ObservableProperty]
private string? _errorMessage;
[ObservableProperty]
private bool _isLoggingIn;
[ObservableProperty]
private string _password = string.Empty;
public LoginViewModel(INavigator navigator, IAuthenticationService authService, IStringLocalizer stringLocalizer)
{
_navigator = navigator;
_authService = authService;
_stringLocalizer = stringLocalizer;
}
[RelayCommand]
private async Task LoginAsync()
{
// Clear previous error
ErrorMessage = null;
// Validate inputs
if(string.IsNullOrWhiteSpace(Email))
{
ErrorMessage = _stringLocalizer["LoginPage.Error.EmailRequired"];
return;
}
if(string.IsNullOrWhiteSpace(Password))
{
ErrorMessage = _stringLocalizer["LoginPage.Error.PasswordRequired"];
return;
}
IsLoggingIn = true;
try
{
var credentials = new Dictionary<string, string>
{
["Email"] = Email,
["Password"] = Password
};
bool success = await _authService.LoginAsync(null, credentials, null, CancellationToken.None);
if(success)
{
// Navigate back to main page on successful login
await _navigator.NavigateRouteAsync(this, "/Main");
}
else
{
// Check if there's an error message in credentials
if(credentials.TryGetValue("error", out string? error))
ErrorMessage = error;
else
ErrorMessage = _stringLocalizer["LoginPage.Error.LoginFailed"];
}
}
catch(Exception ex)
{
ErrorMessage = ex.Message;
}
finally
{
IsLoggingIn = false;
}
}
[RelayCommand]
private void ClearError() => ErrorMessage = null;
}

View File

@@ -0,0 +1,524 @@
/******************************************************************************
// 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.IO;
using System.Threading.Tasks;
using Windows.Storage.Streams;
using Humanizer;
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<MachineViewViewModel> _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<MachineViewViewModel> logger, INavigator navigator,
ComputersService computersService, MachinePhotoCache photoCache)
{
_logger = logger;
_navigator = navigator;
_computersService = computersService;
_photoCache = photoCache;
}
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; } = [];
public ObservableCollection<PhotoCarouselDisplayItem> Photos { 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;
}
// 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<CompanyDetailViewModel>(this, data: navParam);
return;
}
// If we came from ConsolesListViewModel, navigate back to consoles list
if(_navigationSource is ConsolesListViewModel)
{
await _navigator.NavigateViewModelAsync<ConsolesListViewModel>(this);
return;
}
// If we came from ComputersListViewModel, navigate back to computers list
if(_navigationSource is ComputersListViewModel)
{
await _navigator.NavigateViewModelAsync<ComputersListViewModel>(this);
return;
}
// If we came from GpuDetailViewModel, navigate back to GPU details
if(_navigationSource is GpuDetailViewModel gpuDetailVm)
{
var navParam = new GpuDetailNavigationParameter
{
GpuId = gpuDetailVm.GpuId,
NavigationSource = this
};
await _navigator.NavigateViewModelAsync<GpuDetailViewModel>(this, data: navParam);
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;
}
// 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<SoundSynthDetailViewModel>(this, data: navParam);
return;
}
// Otherwise, try to go back in the navigation stack
await _navigator.GoBack(this);
}
[RelayCommand]
public async Task ViewPhotoDetails(Guid photoId)
{
var navParam = new PhotoDetailNavigationParameter
{
PhotoId = photoId
};
_logger.LogInformation("Navigating to photo details for {PhotoId}", photoId);
await _navigator.NavigateViewModelAsync<PhotoDetailViewModel>(this, data: navParam);
}
/// <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();
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<string>();
var speed = (int)(processor.Speed ?? 0);
int gprSize = processor.GprSize ?? 0;
int cores = processor.Cores ?? 0;
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 = mem.Size ?? 0;
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 = synth.Voices ?? 0;
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 = storage.Capacity ?? 0;
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<Guid> 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);
}
}
}
/// <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;
}
/// <summary>
/// Display item for photo carousel
/// </summary>
public class PhotoCarouselDisplayItem
{
// Thumbnail constraints
public const int ThumbnailMaxSize = 256;
public Guid PhotoId { get; set; }
public ImageSource? ThumbnailImageSource { get; set; }
}

View File

@@ -0,0 +1,263 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Marechai.App.Services;
using Marechai.App.Services.Authentication;
using Uno.Extensions.Authentication;
using Uno.Extensions.Navigation;
using Uno.Extensions.Toolkit;
namespace Marechai.App.Presentation.ViewModels;
public partial class MainViewModel : ObservableObject
{
private readonly IAuthenticationService _authService;
private readonly IJwtService _jwtService;
private readonly IStringLocalizer _localizer;
private readonly INavigator _navigator;
private readonly ITokenService _tokenService;
[ObservableProperty]
private bool _isSidebarOpen = true;
[ObservableProperty]
private bool _isUberadminUser;
[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, IColorThemeService colorThemeService, IThemeService themeService,
IAuthenticationService authService, IJwtService jwtService, ITokenService tokenService)
{
_navigator = navigator;
_localizer = localizer;
_authService = authService;
_jwtService = jwtService;
_tokenService = tokenService;
NewsViewModel = newsViewModel;
Title = "Marechai";
Title += $" - {localizer["ApplicationName"]}";
if(appInfo?.Value?.Environment != null) Title += $" - {appInfo.Value.Environment}";
GoToSecond = new AsyncRelayCommand(GoToSecondView);
// Initialize color theme service with theme service
_ = InitializeThemeServicesAsync(colorThemeService, themeService);
// 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("sound-synths"));
NavigateToUsersCommand = new AsyncRelayCommand(() => NavigateTo("users"));
NavigateToSettingsCommand = new AsyncRelayCommand(() => NavigateTo("settings"));
LoginLogoutCommand = new RelayCommand(HandleLoginLogout);
ToggleSidebarCommand = new RelayCommand(() => IsSidebarOpen = !IsSidebarOpen);
// Subscribe to authentication events
_authService.LoggedOut += OnLoggedOut;
UpdateLoginLogoutButtonText();
UpdateUberadminStatus();
}
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 NavigateToUsersCommand { get; }
public ICommand NavigateToSettingsCommand { get; }
public ICommand LoginLogoutCommand { get; }
public ICommand ToggleSidebarCommand { get; }
private async Task InitializeThemeServicesAsync(IColorThemeService colorThemeService, IThemeService themeService)
{
try
{
// Wait for theme service to be ready
await themeService.InitializeAsync();
// Set the theme service reference and reapply the saved theme
colorThemeService.SetThemeService(themeService);
}
catch
{
// Silently fail - theme will work but without refresh on startup
}
}
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 async void UpdateLoginLogoutButtonText()
{
bool isAuthenticated = await _authService.IsAuthenticated(CancellationToken.None);
LoginLogoutButtonText = isAuthenticated ? LocalizedStrings["Logout"] : LocalizedStrings["Login"];
}
private void UpdateUberadminStatus()
{
try
{
string token = _tokenService.GetToken();
if(!string.IsNullOrWhiteSpace(token))
{
IEnumerable<string> roles = _jwtService.GetRoles(token);
IsUberadminUser = roles.Contains("Uberadmin", StringComparer.OrdinalIgnoreCase);
}
else
IsUberadminUser = false;
}
catch
{
IsUberadminUser = false;
}
}
private void OnLoggedOut(object? sender, EventArgs e)
{
// Update button text when user logs out
UpdateLoginLogoutButtonText();
UpdateUberadminStatus();
}
public void RefreshAuthenticationState()
{
// Public method to refresh authentication state (called after login)
UpdateLoginLogoutButtonText();
UpdateUberadminStatus();
}
private async void HandleLoginLogout()
{
bool isAuthenticated = await _authService.IsAuthenticated(CancellationToken.None);
if(isAuthenticated)
{
// Logout
await _authService.LogoutAsync(null, CancellationToken.None);
UpdateLoginLogoutButtonText();
UpdateUberadminStatus();
}
else
{
// Navigate to login page - use absolute path starting from root
await _navigator.NavigateRouteAsync(this, "/Login");
}
}
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,175 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Marechai.App.Presentation.Models;
using Marechai.App.Services;
using Marechai.Data;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation.ViewModels;
/// <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 = news.AffectedId ?? 0;
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,425 @@
/******************************************************************************
// 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.IO;
using System.Linq;
using System.Threading.Tasks;
using Windows.Storage.Streams;
using Humanizer;
using Marechai.App.Services;
using Marechai.App.Services.Caching;
using Microsoft.UI.Xaml.Media.Imaging;
using Uno.Extensions.Navigation;
using ColorSpace = Marechai.Data.ColorSpace;
using Contrast = Marechai.Data.Contrast;
using ExposureMode = Marechai.Data.ExposureMode;
using ExposureProgram = Marechai.Data.ExposureProgram;
using Flash = Marechai.Data.Flash;
using LightSource = Marechai.Data.LightSource;
using MeteringMode = Marechai.Data.MeteringMode;
using Orientation = Marechai.Data.Orientation;
using ResolutionUnit = Marechai.Data.ResolutionUnit;
using Saturation = Marechai.Data.Saturation;
using SceneCaptureType = Marechai.Data.SceneCaptureType;
using SensingMethod = Marechai.Data.SensingMethod;
using Sharpness = Marechai.Data.Sharpness;
using SubjectDistanceRange = Marechai.Data.SubjectDistanceRange;
using WhiteBalance = Marechai.Data.WhiteBalance;
namespace Marechai.App.Presentation.ViewModels;
/// <summary>
/// Navigation parameter for photo detail page
/// </summary>
public class PhotoDetailNavigationParameter
{
public Guid PhotoId { get; set; }
}
public partial class PhotoDetailViewModel : ObservableObject
{
private readonly ComputersService _computersService;
private readonly ILogger<PhotoDetailViewModel> _logger;
private readonly INavigator _navigator;
private readonly MachinePhotoCache _photoCache;
[ObservableProperty]
private string _errorMessage = string.Empty;
[ObservableProperty]
private bool _errorOccurred;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private bool _isPortrait = true;
// EXIF Camera Settings
[ObservableProperty]
private string _photoAperture = string.Empty;
[ObservableProperty]
private string _photoAuthor = string.Empty;
[ObservableProperty]
private string _photoCameraManufacturer = string.Empty;
[ObservableProperty]
private string _photoCameraModel = string.Empty;
// Photo Properties
[ObservableProperty]
private string _photoColorSpace = string.Empty;
[ObservableProperty]
private string _photoComments = string.Empty;
[ObservableProperty]
private string _photoContrast = string.Empty;
[ObservableProperty]
private string _photoCreationDate = string.Empty;
[ObservableProperty]
private string _photoDigitalZoomRatio = string.Empty;
[ObservableProperty]
private string _photoExifVersion = string.Empty;
[ObservableProperty]
private string _photoExposureMode = string.Empty;
[ObservableProperty]
private string _photoExposureProgram = string.Empty;
[ObservableProperty]
private string _photoExposureTime = string.Empty;
[ObservableProperty]
private string _photoFlash = string.Empty;
[ObservableProperty]
private string _photoFocalLength = string.Empty;
[ObservableProperty]
private string _photoFocalLengthEquivalent = string.Empty;
// Resolution and Other
[ObservableProperty]
private string _photoHorizontalResolution = string.Empty;
[ObservableProperty]
private BitmapImage? _photoImageSource;
[ObservableProperty]
private string _photoIsoRating = string.Empty;
[ObservableProperty]
private string _photoLensModel = string.Empty;
[ObservableProperty]
private string _photoLicenseName = string.Empty;
[ObservableProperty]
private string _photoLightSource = string.Empty;
[ObservableProperty]
private string _photoMachineCompany = string.Empty;
[ObservableProperty]
private string _photoMachineName = string.Empty;
[ObservableProperty]
private string _photoMeteringMode = string.Empty;
[ObservableProperty]
private string _photoOrientation = string.Empty;
[ObservableProperty]
private string _photoOriginalExtension = string.Empty;
[ObservableProperty]
private string _photoResolutionUnit = string.Empty;
[ObservableProperty]
private string _photoSaturation = string.Empty;
[ObservableProperty]
private string _photoSceneCaptureType = string.Empty;
[ObservableProperty]
private string _photoSensingMethod = string.Empty;
[ObservableProperty]
private string _photoSharpness = string.Empty;
[ObservableProperty]
private string _photoSoftwareUsed = string.Empty;
[ObservableProperty]
private string _photoSource = string.Empty;
[ObservableProperty]
private string _photoSubjectDistanceRange = string.Empty;
[ObservableProperty]
private string _photoUploadDate = string.Empty;
[ObservableProperty]
private string _photoVerticalResolution = string.Empty;
[ObservableProperty]
private string _photoWhiteBalance = string.Empty;
public PhotoDetailViewModel(ILogger<PhotoDetailViewModel> logger, INavigator navigator,
ComputersService computersService, MachinePhotoCache photoCache)
{
_logger = logger;
_navigator = navigator;
_computersService = computersService;
_photoCache = photoCache;
}
[RelayCommand]
public async Task GoBack()
{
await _navigator.GoBack(this);
}
[RelayCommand]
public async Task LoadPhoto(Guid photoId)
{
try
{
IsLoading = true;
ErrorOccurred = false;
ErrorMessage = string.Empty;
PhotoImageSource = null;
_logger.LogInformation("Loading photo details for {PhotoId}", photoId);
// Fetch photo details from API
MachinePhotoDto? photo = await _computersService.GetMachinePhotoDetailsAsync(photoId);
if(photo is null)
{
ErrorOccurred = true;
ErrorMessage = "Photo not found";
IsLoading = false;
return;
}
// Populate photo information
PhotoAuthor = photo.Author ?? string.Empty;
PhotoCameraManufacturer = photo.CameraManufacturer ?? string.Empty;
PhotoCameraModel = photo.CameraModel ?? string.Empty;
PhotoComments = photo.Comments ?? string.Empty;
PhotoLensModel = photo.Lens ?? string.Empty;
PhotoLicenseName = photo.LicenseName ?? string.Empty;
PhotoMachineCompany = photo.MachineCompanyName ?? string.Empty;
PhotoMachineName = photo.MachineName ?? string.Empty;
PhotoOriginalExtension = photo.OriginalExtension ?? string.Empty;
if(photo.CreationDate.HasValue)
PhotoCreationDate = photo.CreationDate.Value.ToString("MMMM d, yyyy 'at' HH:mm");
// EXIF Camera Settings
PhotoAperture = photo.Aperture != null ? $"f/{photo.Aperture}" : string.Empty;
PhotoExposureTime = photo.Exposure != null ? $"{photo.Exposure}s" : string.Empty;
// Extract ExposureMode - simple nullable integer now
PhotoExposureMode = photo.ExposureMethod.HasValue
? ((ExposureMode)photo.ExposureMethod.Value).Humanize()
: string.Empty;
// Extract ExposureProgram - simple nullable integer now
PhotoExposureProgram = photo.ExposureProgram.HasValue
? ((ExposureProgram)photo.ExposureProgram.Value).Humanize()
: string.Empty;
PhotoFocalLength = photo.FocalLength != null ? $"{photo.FocalLength}mm" : string.Empty;
PhotoFocalLengthEquivalent = photo.FocalEquivalent != null ? $"{photo.FocalEquivalent}mm" : string.Empty;
PhotoIsoRating = photo.Iso != null ? photo.Iso.ToString() : string.Empty;
// Extract Flash - simple nullable integer now
PhotoFlash = photo.Flash.HasValue ? ((Flash)photo.Flash.Value).Humanize() : string.Empty;
// Extract LightSource - simple nullable integer now
PhotoLightSource = photo.LightSource.HasValue
? ((LightSource)photo.LightSource.Value).Humanize()
: string.Empty;
// Extract MeteringMode - simple nullable integer now
PhotoMeteringMode = photo.MeteringMode.HasValue
? ((MeteringMode)photo.MeteringMode.Value).Humanize()
: string.Empty;
// Extract WhiteBalance - simple nullable integer now
PhotoWhiteBalance = photo.WhiteBalance.HasValue
? ((WhiteBalance)photo.WhiteBalance.Value).Humanize()
: string.Empty;
// Photo Properties
// Extract ColorSpace - simple nullable integer now
PhotoColorSpace = photo.Colorspace.HasValue
? ((ColorSpace)photo.Colorspace.Value).Humanize()
: string.Empty;
// Extract Contrast - simple nullable integer now
PhotoContrast = photo.Contrast.HasValue ? ((Contrast)photo.Contrast.Value).Humanize() : string.Empty;
// Extract Saturation - simple nullable integer now
PhotoSaturation = photo.Saturation.HasValue
? ((Saturation)photo.Saturation.Value).Humanize()
: string.Empty;
// Extract Sharpness - simple nullable integer now
PhotoSharpness = photo.Sharpness.HasValue ? ((Sharpness)photo.Sharpness.Value).Humanize() : string.Empty;
// Extract Orientation - simple nullable integer now
PhotoOrientation = photo.Orientation.HasValue
? ((Orientation)photo.Orientation.Value).Humanize()
: string.Empty;
// Extract SceneCaptureType - simple nullable integer now
PhotoSceneCaptureType = photo.SceneCaptureType.HasValue
? ((SceneCaptureType)photo.SceneCaptureType.Value).Humanize()
: string.Empty;
// Extract SensingMethod - simple nullable integer now
PhotoSensingMethod = photo.SensingMethod.HasValue
? ((SensingMethod)photo.SensingMethod.Value).Humanize()
: string.Empty;
// Extract SubjectDistanceRange - simple nullable integer now
PhotoSubjectDistanceRange = photo.SubjectDistanceRange.HasValue
? ((SubjectDistanceRange)photo.SubjectDistanceRange.Value).Humanize()
: string.Empty;
// Resolution and Other
PhotoHorizontalResolution =
photo.HorizontalResolution != null ? $"{photo.HorizontalResolution} DPI" : string.Empty;
PhotoVerticalResolution =
photo.VerticalResolution != null ? $"{photo.VerticalResolution} DPI" : string.Empty;
// Extract ResolutionUnit - simple nullable integer now
PhotoResolutionUnit = photo.ResolutionUnit.HasValue
? ((ResolutionUnit)photo.ResolutionUnit.Value).Humanize()
: string.Empty;
PhotoDigitalZoomRatio = photo.DigitalZoom != null ? $"{photo.DigitalZoom}x" : string.Empty;
PhotoExifVersion = photo.ExifVersion ?? string.Empty;
PhotoSoftwareUsed = photo.Software ?? string.Empty;
PhotoUploadDate = photo.UploadDate.HasValue
? photo.UploadDate.Value.ToString("MMMM d, yyyy 'at' HH:mm")
: string.Empty;
PhotoSource = photo.Source ?? string.Empty;
// Load the full photo image
await LoadPhotoImageAsync(photoId);
IsLoading = false;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading photo details for {PhotoId}", photoId);
ErrorOccurred = true;
ErrorMessage = ex.Message;
IsLoading = false;
}
}
/// <summary>
/// Updates the portrait/landscape orientation flag
/// </summary>
public void UpdateOrientation(bool isPortrait)
{
IsPortrait = isPortrait;
}
private async Task LoadPhotoImageAsync(Guid photoId)
{
try
{
Stream stream = await _photoCache.GetPhotoAsync(photoId);
var bitmap = new BitmapImage();
using(IRandomAccessStream randomStream = stream.AsRandomAccessStream())
{
await bitmap.SetSourceAsync(randomStream);
}
PhotoImageSource = bitmap;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading photo image {PhotoId}", photoId);
ErrorOccurred = true;
ErrorMessage = "Failed to load photo image";
}
}
/// <summary>
/// Extracts an integer value from AdditionalData dictionary
/// </summary>
private int ExtractInt(IDictionary<string, object> additionalData)
{
if(additionalData == null || additionalData.Count == 0) return 0;
object? value = additionalData.Values.FirstOrDefault();
if(value is int intValue) return intValue;
if(value is double dblValue) return (int)dblValue;
if(int.TryParse(value?.ToString() ?? "", out int parsed)) return parsed;
return 0;
}
/// <summary>
/// Humanizes an enum value extracted from AdditionalData
/// </summary>
private string ExtractAndHumanizeEnum(IDictionary<string, object>? additionalData, Type enumType)
{
if(additionalData == null || additionalData.Count == 0) return string.Empty;
int intValue = ExtractInt(additionalData);
if(intValue == 0 && enumType != typeof(ExposureMode)) return string.Empty;
var enumValue = Enum.ToObject(enumType, intValue);
return ((Enum)enumValue).Humanize();
}
}

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,3 @@
namespace Marechai.App.Presentation.ViewModels;
public record SecondViewModel(Entity Entity) {}

View File

@@ -0,0 +1,190 @@
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Marechai.App.Services;
using Uno.Extensions.Toolkit;
namespace Marechai.App.Presentation.ViewModels;
public partial class SettingsViewModel : ObservableObject
{
private readonly IColorThemeService _colorThemeService;
private readonly IStringLocalizer _localizer;
private readonly IThemeService _themeService;
[ObservableProperty]
private List<ColorThemeOption> _availableColorThemes = new();
[ObservableProperty]
private List<ThemeOption> _availableThemes = new();
[ObservableProperty]
private ColorThemeOption _selectedColorTheme;
[ObservableProperty]
private ThemeOption _selectedTheme;
public SettingsViewModel(IStringLocalizer localizer, IThemeService themeService,
IColorThemeService colorThemeService)
{
_localizer = localizer;
_themeService = themeService;
_colorThemeService = colorThemeService;
Title = _localizer["Settings"];
// Initialize immediately to ensure UI is populated
InitializeOptions();
// Wait for theme service to initialize
_ = InitializeThemeServiceAsync();
}
public string Title { get; }
private async Task InitializeThemeServiceAsync()
{
try
{
await _themeService.InitializeAsync();
// Ensure the color theme service has a reference to the theme service
_colorThemeService.SetThemeService(_themeService);
}
catch
{
// Theme service might already be initialized
}
}
private void InitializeOptions()
{
// Initialize Light/Dark/System Themes
AvailableThemes = new List<ThemeOption>
{
new()
{
Theme = AppTheme.Light,
DisplayName = _localizer["LightTheme"]
},
new()
{
Theme = AppTheme.Dark,
DisplayName = _localizer["DarkTheme"]
},
new()
{
Theme = AppTheme.System,
DisplayName = _localizer["SystemTheme"]
}
};
// Initialize Color Themes
AvailableColorThemes = new List<ColorThemeOption>
{
new()
{
ThemeName = "Default",
DisplayName = _localizer["DefaultColorTheme"]
},
new()
{
ThemeName = "Windows311",
DisplayName = _localizer["Windows311Theme"]
},
new()
{
ThemeName = "MacOS9",
DisplayName = _localizer["MacOS9Theme"]
},
new()
{
ThemeName = "DOS",
DisplayName = _localizer["DOSTheme"]
},
new()
{
ThemeName = "Amiga",
DisplayName = _localizer["AmigaTheme"]
},
new()
{
ThemeName = "CDE",
DisplayName = _localizer["CDETheme"]
}
};
// Try to load saved preferences
LoadSavedPreferences();
}
private async void LoadSavedPreferences()
{
try
{
// Load current theme from ThemeService
AppTheme currentTheme = _themeService.Theme;
SelectedTheme = AvailableThemes.FirstOrDefault(t => t.Theme == currentTheme) ??
AvailableThemes.FirstOrDefault(t => t.Theme == AppTheme.System) ?? AvailableThemes.First();
// Load current color theme
string currentColorTheme = _colorThemeService.CurrentColorTheme;
SelectedColorTheme = AvailableColorThemes.FirstOrDefault(t => t.ThemeName == currentColorTheme) ??
AvailableColorThemes.First();
}
catch
{
// If loading fails, use defaults
SelectedTheme = AvailableThemes.FirstOrDefault(t => t.Theme == AppTheme.System) ?? AvailableThemes.First();
SelectedColorTheme = AvailableColorThemes.First();
}
}
partial void OnSelectedThemeChanged(ThemeOption value)
{
if(value != null) ApplyTheme(value);
}
partial void OnSelectedColorThemeChanged(ColorThemeOption value)
{
if(value != null) ApplyColorTheme(value);
}
private async void ApplyTheme(ThemeOption theme)
{
try
{
// Apply theme immediately using ThemeService
await _themeService.SetThemeAsync(theme.Theme);
}
catch
{
// Silently fail
}
}
private void ApplyColorTheme(ColorThemeOption colorTheme)
{
try
{
_colorThemeService.ApplyColorTheme(colorTheme.ThemeName);
}
catch
{
// Silently fail
}
}
}
public class ThemeOption
{
public AppTheme Theme { get; set; }
public string DisplayName { get; set; } = string.Empty;
}
public class ColorThemeOption
{
public string ThemeName { get; set; } = string.Empty;
public string DisplayName { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,13 @@
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation.ViewModels;
public class ShellViewModel
{
private readonly INavigator _navigator;
public ShellViewModel(INavigator navigator) => _navigator = navigator;
// Users can browse the app without authentication
// Login is available from the sidebar when needed
}

View File

@@ -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<SoundSynthDetailViewModel> _logger;
private readonly INavigator _navigator;
private readonly SoundSynthsService _soundSynthsService;
[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 SoundSynthDto? _soundSynth;
[ObservableProperty]
private int _soundSynthId;
public SoundSynthDetailViewModel(SoundSynthsService soundSynthsService, CompaniesService companiesService,
IStringLocalizer localizer, ILogger<SoundSynthDetailViewModel> logger,
INavigator navigator)
{
_soundSynthsService = soundSynthsService;
_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; } = "Sound Synthesizer Details";
/// <summary>
/// Loads Sound Synthesizer details including computers and consoles
/// </summary>
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<MachineDto>? 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;
}
}
/// <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 back to the Sound Synthesizer 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 Sound Synthesizer list
await _navigator.NavigateViewModelAsync<SoundSynthsListViewModel>(this);
}
/// <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;
}
/// <summary>
/// Machine item for displaying computers or consoles that use the Sound Synthesizer
/// </summary>
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";
}
}

View File

@@ -0,0 +1,135 @@
#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;
public partial class SoundSynthsListViewModel : ObservableObject
{
private readonly ILogger<SoundSynthsListViewModel> _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<SoundSynthListItem> _soundSynths = [];
public SoundSynthsListViewModel(SoundSynthsService soundSynthsService, INavigator navigator,
ILogger<SoundSynthsListViewModel> logger)
{
_soundSynthsService = soundSynthsService;
_navigator = navigator;
_logger = logger;
LoadData = new AsyncRelayCommand(LoadDataAsync);
NavigateToSoundSynthCommand = new AsyncRelayCommand<SoundSynthListItem>(NavigateToSoundSynthAsync);
}
public IAsyncRelayCommand LoadData { get; }
public IAsyncRelayCommand<SoundSynthListItem> NavigateToSoundSynthCommand { get; }
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
IsDataLoaded = false;
HasError = false;
ErrorMessage = string.Empty;
List<SoundSynthDto> soundSynths = await _soundSynthsService.GetAllSoundSynthsAsync();
// Separate special sound synths from regular ones
var specialSoundSynths = new List<SoundSynthListItem>();
var regularSoundSynths = new List<SoundSynthListItem>();
foreach(SoundSynthDto ss in soundSynths)
{
string displayName = ss.Name ?? "Unknown";
// Replace special database name
if(displayName == "DB_SOFTWARE") displayName = "Software";
var soundSynthItem = new SoundSynthListItem
{
Id = ss.Id ?? 0,
Name = displayName,
Company = ss.Company ?? "Unknown",
IsSpecial = ss.Name == "DB_SOFTWARE"
};
if(soundSynthItem.IsSpecial)
specialSoundSynths.Add(soundSynthItem);
else
regularSoundSynths.Add(soundSynthItem);
_logger.LogInformation("Sound Synth: {Name}, Company: {Company}, ID: {Id}, IsSpecial: {IsSpecial}",
displayName,
ss.Company,
ss.Id,
soundSynthItem.IsSpecial);
}
// Sort regular sound synths alphabetically by name
regularSoundSynths.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
// Add special sound synths first (Software), then regular sound synths
SoundSynths.Clear();
foreach(SoundSynthListItem ss in specialSoundSynths) SoundSynths.Add(ss);
foreach(SoundSynthListItem ss in regularSoundSynths) SoundSynths.Add(ss);
_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<SoundSynthDetailViewModel>(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; }
public bool IsSpecial { get; set; }
}
}

View File

@@ -0,0 +1,440 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Marechai.App.Services.Authentication;
namespace Marechai.App.Presentation.ViewModels;
/// <summary>
/// ViewModel for user management page (Uberadmin only)
/// </summary>
public partial class UsersViewModel : ObservableObject
{
private readonly ApiClient _apiClient;
private readonly IJwtService _jwtService;
private readonly IStringLocalizer _localizer;
private readonly ILogger<UsersViewModel> _logger;
private readonly ITokenService _tokenService;
[ObservableProperty]
private ObservableCollection<string> _availableRoles = [];
[ObservableProperty]
private string _confirmPassword = string.Empty;
[ObservableProperty]
private string _dialogTitle = string.Empty;
[ObservableProperty]
private string _dialogType = string.Empty;
private string? _editingUserId;
[ObservableProperty]
private string _email = string.Empty;
[ObservableProperty]
private string _errorMessage = string.Empty;
[ObservableProperty]
private bool _hasError;
[ObservableProperty]
private bool _isDataLoaded;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private string _password = string.Empty;
[ObservableProperty]
private string _phoneNumber = string.Empty;
[ObservableProperty]
private string? _selectedRole;
[ObservableProperty]
private UserDto? _selectedUser;
[ObservableProperty]
private string _userName = string.Empty;
[ObservableProperty]
private ObservableCollection<string> _userRoles = [];
[ObservableProperty]
private ObservableCollection<UserDto> _users = [];
public UsersViewModel(ApiClient apiClient, IJwtService jwtService, ITokenService tokenService,
ILogger<UsersViewModel> logger, IStringLocalizer localizer)
{
_apiClient = apiClient;
_jwtService = jwtService;
_tokenService = tokenService;
_logger = logger;
_localizer = localizer;
LoadUsersCommand = new AsyncRelayCommand(LoadUsersAsync);
DeleteUserCommand = new AsyncRelayCommand<UserDto>(DeleteUserAsync);
OpenAddUserDialogCommand = new AsyncRelayCommand(OpenAddUserDialogAsync);
OpenEditUserDialogCommand = new AsyncRelayCommand<UserDto>(OpenEditUserDialogAsync);
OpenChangePasswordDialogCommand = new AsyncRelayCommand<UserDto>(OpenChangePasswordDialogAsync);
OpenManageRolesDialogCommand = new AsyncRelayCommand<UserDto>(OpenManageRolesDialogAsync);
SaveUserCommand = new AsyncRelayCommand(SaveUserAsync);
SavePasswordCommand = new AsyncRelayCommand(SavePasswordAsync);
AddRoleCommand = new AsyncRelayCommand(AddRoleAsync);
RemoveRoleCommand = new AsyncRelayCommand<string>(RemoveRoleAsync);
CloseDialogCommand = new RelayCommand(CloseDialog);
}
public IAsyncRelayCommand LoadUsersCommand { get; }
public IAsyncRelayCommand<UserDto> DeleteUserCommand { get; }
public IAsyncRelayCommand OpenAddUserDialogCommand { get; }
public IAsyncRelayCommand<UserDto> OpenEditUserDialogCommand { get; }
public IAsyncRelayCommand<UserDto> OpenChangePasswordDialogCommand { get; }
public IAsyncRelayCommand<UserDto> OpenManageRolesDialogCommand { get; }
public IAsyncRelayCommand SaveUserCommand { get; }
public IAsyncRelayCommand SavePasswordCommand { get; }
public IAsyncRelayCommand AddRoleCommand { get; }
public IAsyncRelayCommand<string> RemoveRoleCommand { get; }
public IRelayCommand CloseDialogCommand { get; }
/// <summary>
/// Checks if the current user is Uberadmin
/// </summary>
public bool IsUberadmin
{
get
{
string? token = _tokenService.GetToken();
IEnumerable<string>? roles = _jwtService.GetRoles(token);
return roles.Contains("Uberadmin", StringComparer.OrdinalIgnoreCase);
}
}
public event EventHandler<string>? ShowDialogRequested;
private async Task LoadUsersAsync()
{
try
{
IsLoading = true;
HasError = false;
ErrorMessage = string.Empty;
Users.Clear();
List<UserDto>? usersResponse = await _apiClient.Users.GetAsync();
if(usersResponse != null)
{
foreach(UserDto user in usersResponse) Users.Add(user);
IsDataLoaded = true;
}
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading users");
ErrorMessage = _localizer["Failed to load users. Please try again."];
HasError = true;
}
finally
{
IsLoading = false;
}
}
private async Task DeleteUserAsync(UserDto? user)
{
if(user?.Id == null) return;
try
{
await _apiClient.Users[user.Id].DeleteAsync();
await LoadUsersAsync();
}
catch(Exception ex)
{
_logger.LogError(ex, "Error deleting user");
ErrorMessage = _localizer["Failed to delete user."];
HasError = true;
}
}
private async Task OpenAddUserDialogAsync()
{
try
{
_editingUserId = null;
DialogTitle = _localizer["Add User"];
DialogType = "AddEdit";
Email = string.Empty;
UserName = string.Empty;
PhoneNumber = string.Empty;
Password = string.Empty;
ConfirmPassword = string.Empty;
ShowDialogRequested?.Invoke(this, "AddEdit");
}
catch(Exception ex)
{
_logger.LogError(ex, "Error opening add user dialog");
}
}
private async Task OpenEditUserDialogAsync(UserDto? user)
{
if(user?.Id == null) return;
try
{
_editingUserId = user.Id;
DialogTitle = _localizer["Edit User"];
DialogType = "AddEdit";
Email = user.Email ?? string.Empty;
UserName = user.UserName ?? string.Empty;
PhoneNumber = user.PhoneNumber ?? string.Empty;
Password = string.Empty;
ConfirmPassword = string.Empty;
ShowDialogRequested?.Invoke(this, "AddEdit");
}
catch(Exception ex)
{
_logger.LogError(ex, "Error opening edit user dialog");
}
}
private async Task OpenChangePasswordDialogAsync(UserDto? user)
{
if(user?.Id == null) return;
try
{
_editingUserId = user.Id;
DialogTitle = _localizer["Change Password"];
DialogType = "Password";
Password = string.Empty;
ConfirmPassword = string.Empty;
ShowDialogRequested?.Invoke(this, "Password");
}
catch(Exception ex)
{
_logger.LogError(ex, "Error opening change password dialog");
}
}
private async Task OpenManageRolesDialogAsync(UserDto? user)
{
if(user?.Id == null) return;
try
{
_editingUserId = user.Id;
DialogTitle = _localizer["Manage Roles"];
DialogType = "Roles";
// Load available roles
List<string>? rolesResponse = await _apiClient.Users.Roles.GetAsync();
AvailableRoles.Clear();
if(rolesResponse != null)
{
foreach(string role in rolesResponse)
if(!string.IsNullOrWhiteSpace(role))
AvailableRoles.Add(role);
}
_logger.LogInformation($"Loaded {AvailableRoles.Count} available roles");
// Load user's current roles
UserRoles.Clear();
if(user.Roles != null)
foreach(string role in user.Roles)
UserRoles.Add(role);
ShowDialogRequested?.Invoke(this, "Roles");
}
catch(Exception ex)
{
_logger.LogError(ex, "Error opening manage roles dialog");
}
}
private async Task SaveUserAsync()
{
try
{
if(string.IsNullOrWhiteSpace(Email) || string.IsNullOrWhiteSpace(UserName))
{
ErrorMessage = _localizer["Email and username are required."];
HasError = true;
return;
}
if(_editingUserId == null)
{
// Create new user
if(string.IsNullOrWhiteSpace(Password))
{
ErrorMessage = _localizer["Password is required for new users."];
HasError = true;
return;
}
if(Password != ConfirmPassword)
{
ErrorMessage = _localizer["Passwords do not match."];
HasError = true;
return;
}
var createRequest = new CreateUserRequest
{
Email = Email,
UserName = UserName,
PhoneNumber = PhoneNumber,
Password = Password
};
await _apiClient.Users.PostAsync(createRequest);
}
else
{
// Update existing user
var updateRequest = new UpdateUserRequest
{
Email = Email,
UserName = UserName,
PhoneNumber = PhoneNumber
};
await _apiClient.Users[_editingUserId].PutAsync(updateRequest);
}
CloseDialog();
await LoadUsersAsync();
}
catch(Exception ex)
{
_logger.LogError(ex, "Error saving user");
ErrorMessage = _localizer["Failed to save user."];
HasError = true;
}
}
private async Task SavePasswordAsync()
{
if(_editingUserId == null) return;
try
{
if(string.IsNullOrWhiteSpace(Password))
{
ErrorMessage = _localizer["Password is required."];
HasError = true;
return;
}
if(Password != ConfirmPassword)
{
ErrorMessage = _localizer["Passwords do not match."];
HasError = true;
return;
}
var changePasswordRequest = new ChangePasswordRequest
{
NewPassword = Password
};
await _apiClient.Users[_editingUserId].Password.PostAsync(changePasswordRequest);
CloseDialog();
}
catch(Exception ex)
{
_logger.LogError(ex, "Error changing password");
ErrorMessage = _localizer["Failed to change password."];
HasError = true;
}
}
private async Task AddRoleAsync()
{
if(_editingUserId == null || string.IsNullOrWhiteSpace(SelectedRole)) return;
try
{
if(UserRoles.Contains(SelectedRole))
{
ErrorMessage = _localizer["User already has this role."];
HasError = true;
return;
}
var addRoleRequest = new UserRoleRequest
{
RoleName = SelectedRole
};
await _apiClient.Users[_editingUserId].Roles.PostAsync(addRoleRequest);
UserRoles.Add(SelectedRole);
// Reload users to refresh the list
await LoadUsersAsync();
}
catch(Exception ex)
{
_logger.LogError(ex, "Error adding role");
ErrorMessage = _localizer["Failed to add role."];
HasError = true;
}
}
private async Task RemoveRoleAsync(string? role)
{
if(_editingUserId == null || string.IsNullOrWhiteSpace(role)) return;
try
{
await _apiClient.Users[_editingUserId].Roles[role].DeleteAsync();
UserRoles.Remove(role);
// Reload users to refresh the list
await LoadUsersAsync();
}
catch(Exception ex)
{
_logger.LogError(ex, "Error removing role");
ErrorMessage = _localizer["Failed to remove role."];
HasError = true;
}
}
private void CloseDialog()
{
DialogType = string.Empty;
_editingUserId = null;
Email = string.Empty;
UserName = string.Empty;
PhoneNumber = string.Empty;
Password = string.Empty;
ConfirmPassword = string.Empty;
UserRoles.Clear();
AvailableRoles.Clear();
HasError = false;
ErrorMessage = string.Empty;
}
}

View File

@@ -0,0 +1,139 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.CompaniesPage"
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}">
<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">
<!-- Company Count Display -->
<StackPanel HorizontalAlignment="Center"
Spacing="8">
<TextBlock Text="{Binding CompanyCountText}"
TextAlignment="Center"
FontSize="18"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding CompanyCount}"
TextAlignment="Center"
FontSize="48"
FontWeight="Bold"
Foreground="{ThemeResource SystemAccentColor}" />
</StackPanel>
<!-- Search Box -->
<AutoSuggestBox x:Name="CompaniesSearchBox"
HorizontalAlignment="Stretch"
PlaceholderText="Search companies..."
Text="{Binding SearchQuery, Mode=TwoWay}"
QuerySubmitted="OnSearchQuerySubmitted"
TextChanged="OnSearchTextChanged">
<AutoSuggestBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</AutoSuggestBox.ItemTemplate>
</AutoSuggestBox>
<!-- 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>
<!-- Companies List -->
<StackPanel Visibility="{Binding IsDataLoaded}"
Spacing="8">
<ItemsRepeater ItemsSource="{Binding CompaniesList}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button HorizontalAlignment="Stretch"
Command="{Binding DataContext.NavigateToCompanyCommand, ElementName=PageRoot}"
CommandParameter="{Binding}">
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Company Logo -->
<Image Grid.Column="0"
Width="48"
Height="48"
Stretch="Uniform"
VerticalAlignment="Center"
HorizontalAlignment="Left"
Source="{Binding LogoImageSource}"
Visibility="{Binding LogoImageSource, Converter={StaticResource ObjectToVisibilityConverter}}" />
<!-- Company Details -->
<StackPanel Grid.Column="1"
Spacing="4"
VerticalAlignment="Center">
<TextBlock Text="{Binding Name}"
FontSize="16"
FontWeight="SemiBold" />
<TextBlock Text="{Binding FoundationDateDisplay}"
FontSize="12"
Opacity="0.6" />
</StackPanel>
</Grid>
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,59 @@
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Marechai.App.Presentation.Views;
public sealed partial class CompaniesPage : Page
{
public CompaniesPage()
{
InitializeComponent();
DataContextChanged += CompaniesPage_DataContextChanged;
Loaded += CompaniesPage_Loaded;
}
private void CompaniesPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is not CompaniesViewModel viewModel) return;
// Trigger data loading
_ = viewModel.LoadData.ExecuteAsync(null);
}
private void CompaniesPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(args.NewValue is CompaniesViewModel viewModel)
{
// Trigger data loading when data context changes
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if(DataContext is CompaniesViewModel viewModel)
{
// Trigger data loading when navigating to the page
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
private void OnSearchTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
{
// The two-way binding will automatically update SearchQuery in ViewModel,
// which will trigger OnSearchQueryChanged and filter the list
}
}
private void OnSearchQuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
// The two-way binding will automatically update SearchQuery in ViewModel,
// which will trigger OnSearchQueryChanged and filter the list
}
}

View File

@@ -0,0 +1,351 @@
<?xml version="1.0" encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.CompanyDetailPage"
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"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.Resources>
</Page.Resources>
<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">
<!-- Logo Display (Top Center) -->
<Image MaxHeight="96"
Stretch="Uniform"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="0,0,0,8"
Source="{Binding LogoImageSource}"
Visibility="{Binding HasLogoContent, Converter={StaticResource BoolToVisibilityConverter}}" />
<!-- 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>
<!-- Company Details -->
<StackPanel Visibility="{Binding IsDataLoaded}"
Spacing="16">
<!-- Company Name -->
<TextBlock Text="{Binding Company.Name}"
FontSize="28"
FontWeight="Bold"
TextWrapping="Wrap" />
<!-- Company Status -->
<StackPanel Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Status"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding CompanyStatusDisplay}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Founded Date -->
<StackPanel Visibility="{Binding Company.Founded, Converter={StaticResource ObjectToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Founded"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding CompanyFoundedDateDisplay}"
FontSize="14" />
</StackPanel>
<!-- Legal Name -->
<StackPanel Visibility="{Binding Company.LegalName, Converter={StaticResource StringToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Legal Name"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Company.LegalName}"
FontSize="14" />
</StackPanel>
<!-- Country -->
<StackPanel Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12">
<Grid ColumnSpacing="12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Country Name and Label -->
<StackPanel Grid.Column="0"
Spacing="4">
<TextBlock Text="Country"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Company.Country}"
FontSize="14"
VerticalAlignment="Center" />
</StackPanel>
<!-- Country Flag -->
<Image Grid.Column="1"
Width="48"
Height="32"
Stretch="UniformToFill"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Source="{Binding FlagImageSource}"
Visibility="{Binding HasFlagContent, Converter={StaticResource BoolToVisibilityConverter}}" />
</Grid>
</StackPanel>
<!-- Address -->
<StackPanel Visibility="{Binding Company.Address, Converter={StaticResource StringToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Address"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock TextWrapping="Wrap">
<Run Text="{Binding Company.Address}" />
<Run Text="{Binding Company.City}" />
<Run Text="{Binding Company.PostalCode}" />
<Run Text="{Binding Company.Province}" />
</TextBlock>
</StackPanel>
<!-- Links Section -->
<StackPanel Spacing="8">
<TextBlock Text="Links"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<!-- Website -->
<HyperlinkButton Visibility="{Binding Company.Website, Converter={StaticResource StringToVisibilityConverter}}"
NavigateUri="{Binding Company.Website}">
<TextBlock Text="Website" />
</HyperlinkButton>
<!-- Twitter -->
<HyperlinkButton Visibility="{Binding Company.Twitter, Converter={StaticResource StringToVisibilityConverter}}"
Click="OnTwitterClick">
<TextBlock Text="Twitter" />
</HyperlinkButton>
<!-- Facebook -->
<HyperlinkButton Visibility="{Binding Company.Facebook, Converter={StaticResource StringToVisibilityConverter}}"
Click="OnFacebookClick">
<TextBlock Text="Facebook" />
</HyperlinkButton>
</StackPanel>
<!-- Logo Carousel Section -->
<StackPanel Visibility="{Binding HasMultipleLogos, Converter={StaticResource BoolToVisibilityConverter}}"
Spacing="8">
<TextBlock Text="Logo History"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<!-- Logo Carousel -->
<controls:Carousel ItemsSource="{Binding CompanyLogos}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
ItemRotationY="45"
TransitionDuration="400"
Height="220">
<controls:Carousel.EasingFunction>
<CubicEase EasingMode="EaseOut" />
</controls:Carousel.EasingFunction>
<controls:Carousel.ItemTemplate>
<DataTemplate>
<StackPanel VerticalAlignment="Center"
HorizontalAlignment="Center"
Spacing="8"
Padding="24">
<!-- Logo Image -->
<Image Source="{Binding LogoSource}"
Width="120"
Height="120"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<!-- Year Label -->
<TextBlock Text="{Binding Year, FallbackValue='Year Unknown'}"
FontSize="12"
TextAlignment="Center"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
</DataTemplate>
</controls:Carousel.ItemTemplate>
</controls:Carousel>
</StackPanel>
<!-- Computers Section -->
<StackPanel Visibility="{Binding Computers.Count, Converter={StaticResource ZeroToVisibilityConverter}}"
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}"
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 Command="{Binding DataContext.NavigateToMachineCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Padding="12,8"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<TextBlock Text="{Binding Name}"
FontSize="12"
TextWrapping="Wrap" />
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</StackPanel>
<!-- Consoles Section -->
<StackPanel Visibility="{Binding Consoles.Count, Converter={StaticResource ZeroToVisibilityConverter}}"
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}"
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 Command="{Binding DataContext.NavigateToMachineCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Padding="12,8"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<TextBlock Text="{Binding Name}"
FontSize="12"
TextWrapping="Wrap" />
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,77 @@
#nullable enable
using System;
using Windows.System;
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;
public sealed partial class CompanyDetailPage : Page
{
private object? _navigationSource;
private int? _pendingCompanyId;
public CompanyDetailPage()
{
InitializeComponent();
DataContextChanged += CompanyDetailPage_DataContextChanged;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
int? companyId = null;
// Handle both int and CompanyDetailNavigationParameter
if(e.Parameter is int intId)
companyId = intId;
else if(e.Parameter is CompanyDetailNavigationParameter navParam)
{
companyId = navParam.CompanyId;
_navigationSource = navParam.NavigationSource;
}
if(companyId.HasValue)
{
_pendingCompanyId = companyId;
if(DataContext is CompanyDetailViewModel viewModel)
{
viewModel.CompanyId = companyId.Value;
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
}
private void CompanyDetailPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(DataContext is CompanyDetailViewModel viewModel && _pendingCompanyId.HasValue)
{
viewModel.CompanyId = _pendingCompanyId.Value;
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
private async void OnTwitterClick(object sender, RoutedEventArgs e)
{
if(DataContext is CompanyDetailViewModel viewModel && viewModel.Company?.Twitter is not null)
{
var uri = new Uri($"https://www.twitter.com/{viewModel.Company.Twitter}");
await Launcher.LaunchUriAsync(uri);
}
}
private async void OnFacebookClick(object sender, RoutedEventArgs e)
{
if(DataContext is CompanyDetailViewModel viewModel && viewModel.Company?.Facebook is not null)
{
var uri = new Uri($"https://www.facebook.com/{viewModel.Company.Facebook}");
await Launcher.LaunchUriAsync(uri);
}
}
}

View File

@@ -0,0 +1,261 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.ComputersListPage"
x:Name="PageRoot"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header with Back Button and Title -->
<Grid Grid.Row="0"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="16,12,16,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Back Button -->
<Button Grid.Column="0"
Command="{Binding GoBackCommand}"
Style="{ThemeResource AlternateButtonStyle}"
ToolTipService.ToolTip="Go back"
Padding="8"
MinWidth="44"
MinHeight="44"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE72B;"
FontSize="16" />
</Button>
<!-- Page Title -->
<StackPanel Grid.Column="1"
Margin="16,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding PageTitle}"
FontSize="20"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<TextBlock Text="{Binding FilterDescription}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}"
Margin="0,4,0,0" />
</StackPanel>
</Grid>
<!-- 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 computers..."
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 Computers"
Message="{Binding ErrorMessage}"
IsClosable="False" />
<Button Content="Retry"
Command="{Binding LoadData}"
HorizontalAlignment="Center"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<!-- Computers 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 ComputersList.Count}" />
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}"
Text="computers" />
</StackPanel>
<!-- Computers List -->
<ItemsControl Grid.Row="1"
ItemsSource="{Binding ComputersList}"
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.NavigateToComputerCommand, 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>
<!-- Computer Info -->
<StackPanel Grid.Column="0"
Spacing="8"
VerticalAlignment="Center"
HorizontalAlignment="Stretch">
<TextBlock Text="{Binding Name}"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}"
TextTrimming="CharacterEllipsis" />
<Grid ColumnSpacing="16"
Height="20"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal"
Spacing="6"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE731;"
FontSize="14"
Foreground="{ThemeResource SystemAccentColor}" />
<TextBlock Text="{Binding Manufacturer}"
FontSize="13"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Spacing="6"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE787;"
FontSize="14"
Foreground="{ThemeResource SystemAccentColor}" />
<TextBlock FontSize="13"
Foreground="{ThemeResource SystemBaseMediumColor}">
<Run Text="{Binding Year}" />
</TextBlock>
</StackPanel>
</Grid>
</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 computers filtered by letter, year, or all.
/// Features responsive layout, modern styling, and smooth navigation.
/// </summary>
public sealed partial class ComputersListPage : Page
{
public ComputersListPage()
{
InitializeComponent();
Loaded += ComputersListPage_Loaded;
DataContextChanged += ComputersListPage_DataContextChanged;
}
private void ComputersListPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(DataContext is ComputersListViewModel vm)
{
// Load data when DataContext is set
vm.LoadData.Execute(null);
}
}
private void ComputersListPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is ComputersListViewModel vm)
{
// Load data when page is loaded (fallback)
vm.LoadData.Execute(null);
}
}
}

View File

@@ -0,0 +1,311 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.ComputersPage"
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}">
<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="24">
<!-- Computer Count Display -->
<StackPanel HorizontalAlignment="Center"
Spacing="8">
<TextBlock Text="{Binding ComputerCountText}"
TextAlignment="Center"
FontSize="18"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding ComputerCount}"
TextAlignment="Center"
FontSize="48"
FontWeight="Bold"
Foreground="{ThemeResource SystemAccentColor}" />
</StackPanel>
<!-- 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>
<!-- Main Content (visible when loaded and no error) -->
<StackPanel Visibility="{Binding IsDataLoaded}"
Spacing="24">
<!-- Letters Grid Section -->
<StackPanel Spacing="12">
<TextBlock Text="Browse by Letter"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<ItemsRepeater ItemsSource="{Binding LettersList}"
Layout="{StaticResource LettersGridLayout}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}"
Command="{Binding DataContext.NavigateByLetterCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Style="{StaticResource KeyboardKeyButtonStyle}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>
<!-- Years Grid Section -->
<StackPanel Spacing="12">
<TextBlock Text="{Binding YearsGridTitle}"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<ItemsRepeater ItemsSource="{Binding YearsList}"
Layout="{StaticResource YearsGridLayout}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}"
Command="{Binding DataContext.NavigateByYearCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Style="{StaticResource KeyboardKeyButtonStyle}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>
<!-- All Computers and Search Section -->
<StackPanel Spacing="12">
<Button Content="All Computers"
Padding="16,12"
HorizontalAlignment="Stretch"
FontSize="16"
FontWeight="SemiBold"
Command="{Binding NavigateAllComputersCommand}"
Style="{StaticResource AccentButtonStyle}" />
<!-- Search Field (placeholder for future implementation) -->
<TextBox PlaceholderText="Search computers..."
Padding="12"
IsEnabled="False"
Opacity="0.5" />
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
<Page.Resources>
<!-- Keyboard Key Button Style (revised: more padding, simplified borders to avoid clipping, darker scheme) -->
<Style x:Key="KeyboardKeyButtonStyle"
TargetType="Button">
<!-- Base appearance -->
<Setter Property="Foreground"
Value="#1A1A1A" />
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<GradientStop Color="#D6D6D6"
Offset="0" />
<GradientStop Color="#C2C2C2"
Offset="0.55" />
<GradientStop Color="#B0B0B0"
Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush"
Value="#7A7A7A" />
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="CornerRadius"
Value="6" />
<Setter Property="Padding"
Value="14,12" /> <!-- Increased vertical padding to prevent cutoff -->
<Setter Property="Margin"
Value="4" />
<Setter Property="FontFamily"
Value="Segoe UI" />
<Setter Property="FontWeight"
Value="SemiBold" />
<Setter Property="FontSize"
Value="15" />
<Setter Property="HorizontalAlignment"
Value="Stretch" />
<Setter Property="VerticalAlignment"
Value="Stretch" />
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Setter Property="VerticalContentAlignment"
Value="Center" />
<Setter Property="MinWidth"
Value="52" />
<Setter Property="MinHeight"
Value="52" /> <!-- Larger min height avoids clipping ascenders/descenders -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<!-- Shadow (simple) -->
<Border x:Name="Shadow"
CornerRadius="6"
Background="#33000000"
Margin="2,4,4,2" />
<!-- Key surface -->
<Border x:Name="KeyBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<!-- Inner highlight & content -->
<Grid>
<Border CornerRadius="{TemplateBinding CornerRadius}"
BorderBrush="#60FFFFFF"
BorderThickness="1,1,0,0" />
<Border CornerRadius="{TemplateBinding CornerRadius}"
BorderBrush="#30000000"
BorderThickness="0,0,1,1" />
<ContentPresenter x:Name="ContentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="0"
TextWrapping="NoWrap" />
</Grid>
</Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="KeyBorder.Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<GradientStop Color="#E0E0E0"
Offset="0" />
<GradientStop Color="#CFCFCF"
Offset="0.55" />
<GradientStop Color="#BDBDBD"
Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Target="KeyBorder.BorderBrush"
Value="#5F5F5F" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="KeyBorder.Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<GradientStop Color="#9C9C9C"
Offset="0" />
<GradientStop Color="#A8A8A8"
Offset="0.55" />
<GradientStop Color="#B4B4B4"
Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Target="KeyBorder.BorderBrush"
Value="#4A4A4A" />
<Setter Target="KeyBorder.RenderTransform">
<Setter.Value>
<TranslateTransform Y="2" />
</Setter.Value>
</Setter>
<Setter Target="Shadow.Opacity"
Value="0.15" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="KeyBorder.Opacity"
Value="0.45" />
<Setter Target="ContentPresenter.Foreground"
Value="#777777" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Focused">
<VisualState.Setters>
<Setter Target="KeyBorder.BorderBrush"
Value="#3A7AFE" />
<Setter Target="KeyBorder.BorderThickness"
Value="2" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Unfocused" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Responsive Grid Layouts -->
<UniformGridLayout x:Key="LettersGridLayout"
ItemsStretch="Fill"
MinItemWidth="44"
MinItemHeight="44"
MaximumRowsOrColumns="13" />
<UniformGridLayout x:Key="YearsGridLayout"
ItemsStretch="Fill"
MinItemWidth="54"
MinItemHeight="44"
MaximumRowsOrColumns="10" />
</Page.Resources>
</Page>

View File

@@ -0,0 +1,44 @@
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Marechai.App.Presentation.Views;
public sealed partial class ComputersPage : Page
{
public ComputersPage()
{
InitializeComponent();
DataContextChanged += ComputersPage_DataContextChanged;
Loaded += ComputersPage_Loaded;
}
private void ComputersPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is not ComputersViewModel viewModel) return;
// Trigger data loading
_ = viewModel.LoadData.ExecuteAsync(null);
}
private void ComputersPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(args.NewValue is ComputersViewModel viewModel)
{
// Trigger data loading when data context changes
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if(DataContext is ComputersViewModel viewModel)
{
// Trigger data loading when navigating to the page
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
}

View File

@@ -0,0 +1,261 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.ConsolesListPage"
x:Name="PageRoot"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header with Back Button and Title -->
<Grid Grid.Row="0"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="16,12,16,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Back Button -->
<Button Grid.Column="0"
Command="{Binding GoBackCommand}"
Style="{ThemeResource AlternateButtonStyle}"
ToolTipService.ToolTip="Go back"
Padding="8"
MinWidth="44"
MinHeight="44"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE72B;"
FontSize="16" />
</Button>
<!-- Page Title -->
<StackPanel Grid.Column="1"
Margin="16,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding PageTitle}"
FontSize="20"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<TextBlock Text="{Binding FilterDescription}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}"
Margin="0,4,0,0" />
</StackPanel>
</Grid>
<!-- 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 consoles..."
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 Consoles"
Message="{Binding ErrorMessage}"
IsClosable="False" />
<Button Content="Retry"
Command="{Binding LoadData}"
HorizontalAlignment="Center"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<!-- Consoles 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 ConsolesList.Count}" />
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}"
Text="consoles" />
</StackPanel>
<!-- Consoles List -->
<ItemsControl Grid.Row="1"
ItemsSource="{Binding ConsolesList}"
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.NavigateToConsoleCommand, 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>
<!-- Console Info -->
<StackPanel Grid.Column="0"
Spacing="8"
VerticalAlignment="Center"
HorizontalAlignment="Stretch">
<TextBlock Text="{Binding Name}"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}"
TextTrimming="CharacterEllipsis" />
<Grid ColumnSpacing="16"
Height="20"
VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal"
Spacing="6"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE731;"
FontSize="14"
Foreground="{ThemeResource SystemAccentColor}" />
<TextBlock Text="{Binding Manufacturer}"
FontSize="13"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Spacing="6"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE787;"
FontSize="14"
Foreground="{ThemeResource SystemAccentColor}" />
<TextBlock FontSize="13"
Foreground="{ThemeResource SystemBaseMediumColor}">
<Run Text="{Binding Year}" />
</TextBlock>
</StackPanel>
</Grid>
</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 consoles filtered by letter, year, or all.
/// Features responsive layout, modern styling, and smooth navigation.
/// </summary>
public sealed partial class ConsolesListPage : Page
{
public ConsolesListPage()
{
InitializeComponent();
Loaded += ConsolesListPage_Loaded;
DataContextChanged += ConsolesListPage_DataContextChanged;
}
private void ConsolesListPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(DataContext is ConsolesListViewModel vm)
{
// Load data when DataContext is set
vm.LoadData.Execute(null);
}
}
private void ConsolesListPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is ConsolesListViewModel vm)
{
// Load data when page is loaded (fallback)
vm.LoadData.Execute(null);
}
}
}

View File

@@ -0,0 +1,311 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.ConsolesPage"
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}">
<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="24">
<!-- Console Count Display -->
<StackPanel HorizontalAlignment="Center"
Spacing="8">
<TextBlock Text="{Binding ConsoleCountText}"
TextAlignment="Center"
FontSize="18"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding ConsoleCount}"
TextAlignment="Center"
FontSize="48"
FontWeight="Bold"
Foreground="{ThemeResource SystemAccentColor}" />
</StackPanel>
<!-- 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>
<!-- Main Content (visible when loaded and no error) -->
<StackPanel Visibility="{Binding IsDataLoaded}"
Spacing="24">
<!-- Letters Grid Section -->
<StackPanel Spacing="12">
<TextBlock Text="Browse by Letter"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<ItemsRepeater ItemsSource="{Binding LettersList}"
Layout="{StaticResource LettersGridLayout}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}"
Command="{Binding DataContext.NavigateByLetterCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Style="{StaticResource KeyboardKeyButtonStyle}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>
<!-- Years Grid Section -->
<StackPanel Spacing="12">
<TextBlock Text="{Binding YearsGridTitle}"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<ItemsRepeater ItemsSource="{Binding YearsList}"
Layout="{StaticResource YearsGridLayout}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Content="{Binding}"
Command="{Binding DataContext.NavigateByYearCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Style="{StaticResource KeyboardKeyButtonStyle}" />
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>
<!-- All Consoles and Search Section -->
<StackPanel Spacing="12">
<Button Content="All Consoles"
Padding="16,12"
HorizontalAlignment="Stretch"
FontSize="16"
FontWeight="SemiBold"
Command="{Binding NavigateAllConsolesCommand}"
Style="{StaticResource AccentButtonStyle}" />
<!-- Search Field (placeholder for future implementation) -->
<TextBox PlaceholderText="Search consoles..."
Padding="12"
IsEnabled="False"
Opacity="0.5" />
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
<Page.Resources>
<!-- Keyboard Key Button Style (revised: more padding, simplified borders to avoid clipping, darker scheme) -->
<Style x:Key="KeyboardKeyButtonStyle"
TargetType="Button">
<!-- Base appearance -->
<Setter Property="Foreground"
Value="#1A1A1A" />
<Setter Property="Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<GradientStop Color="#D6D6D6"
Offset="0" />
<GradientStop Color="#C2C2C2"
Offset="0.55" />
<GradientStop Color="#B0B0B0"
Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="BorderBrush"
Value="#7A7A7A" />
<Setter Property="BorderThickness"
Value="1" />
<Setter Property="CornerRadius"
Value="6" />
<Setter Property="Padding"
Value="14,12" /> <!-- Increased vertical padding to prevent cutoff -->
<Setter Property="Margin"
Value="4" />
<Setter Property="FontFamily"
Value="Segoe UI" />
<Setter Property="FontWeight"
Value="SemiBold" />
<Setter Property="FontSize"
Value="15" />
<Setter Property="HorizontalAlignment"
Value="Stretch" />
<Setter Property="VerticalAlignment"
Value="Stretch" />
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Setter Property="VerticalContentAlignment"
Value="Center" />
<Setter Property="MinWidth"
Value="52" />
<Setter Property="MinHeight"
Value="52" /> <!-- Larger min height avoids clipping ascenders/descenders -->
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Grid>
<!-- Shadow (simple) -->
<Border x:Name="Shadow"
CornerRadius="6"
Background="#33000000"
Margin="2,4,4,2" />
<!-- Key surface -->
<Border x:Name="KeyBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<!-- Inner highlight & content -->
<Grid>
<Border CornerRadius="{TemplateBinding CornerRadius}"
BorderBrush="#60FFFFFF"
BorderThickness="1,1,0,0" />
<Border CornerRadius="{TemplateBinding CornerRadius}"
BorderBrush="#30000000"
BorderThickness="0,0,1,1" />
<ContentPresenter x:Name="ContentPresenter"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
Margin="0"
TextWrapping="NoWrap" />
</Grid>
</Border>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="PointerOver">
<VisualState.Setters>
<Setter Target="KeyBorder.Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<GradientStop Color="#E0E0E0"
Offset="0" />
<GradientStop Color="#CFCFCF"
Offset="0.55" />
<GradientStop Color="#BDBDBD"
Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Target="KeyBorder.BorderBrush"
Value="#5F5F5F" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Target="KeyBorder.Background">
<Setter.Value>
<LinearGradientBrush StartPoint="0,0"
EndPoint="0,1">
<GradientStop Color="#9C9C9C"
Offset="0" />
<GradientStop Color="#A8A8A8"
Offset="0.55" />
<GradientStop Color="#B4B4B4"
Offset="1" />
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Target="KeyBorder.BorderBrush"
Value="#4A4A4A" />
<Setter Target="KeyBorder.RenderTransform">
<Setter.Value>
<TranslateTransform Y="2" />
</Setter.Value>
</Setter>
<Setter Target="Shadow.Opacity"
Value="0.15" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Target="KeyBorder.Opacity"
Value="0.45" />
<Setter Target="ContentPresenter.Foreground"
Value="#777777" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
<VisualStateGroup x:Name="FocusStates">
<VisualState x:Name="Focused">
<VisualState.Setters>
<Setter Target="KeyBorder.BorderBrush"
Value="#3A7AFE" />
<Setter Target="KeyBorder.BorderThickness"
Value="2" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Unfocused" />
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<!-- Responsive Grid Layouts -->
<UniformGridLayout x:Key="LettersGridLayout"
ItemsStretch="Fill"
MinItemWidth="44"
MinItemHeight="44"
MaximumRowsOrColumns="13" />
<UniformGridLayout x:Key="YearsGridLayout"
ItemsStretch="Fill"
MinItemWidth="54"
MinItemHeight="44"
MaximumRowsOrColumns="10" />
</Page.Resources>
</Page>

View File

@@ -0,0 +1,44 @@
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Marechai.App.Presentation.Views;
public sealed partial class ConsolesPage : Page
{
public ConsolesPage()
{
InitializeComponent();
DataContextChanged += ConsolesPage_DataContextChanged;
Loaded += ConsolesPage_Loaded;
}
private void ConsolesPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is not ConsolesViewModel viewModel) return;
// Trigger data loading
_ = viewModel.LoadData.ExecuteAsync(null);
}
private void ConsolesPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(args.NewValue is ConsolesViewModel viewModel)
{
// Trigger data loading when data context changes
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if(DataContext is ConsolesViewModel viewModel)
{
// Trigger data loading when navigating to the page
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
}

View File

@@ -0,0 +1,374 @@
<?xml version="1.0" encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.GpuDetailPage"
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"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.Resources>
</Page.Resources>
<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>
<!-- GPU Details -->
<StackPanel Visibility="{Binding IsDataLoaded}"
Spacing="16">
<!-- GPU Name -->
<TextBlock Text="{Binding Gpu.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 Gpu.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 Gpu.ModelCode}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Introduced Date -->
<StackPanel Visibility="{Binding Gpu.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 Gpu.Introduced}"
FontSize="14" />
</StackPanel>
<!-- Package -->
<StackPanel Visibility="{Binding Gpu.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 Gpu.Package}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Process -->
<StackPanel Visibility="{Binding Gpu.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 Gpu.Process}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Process (nm) -->
<StackPanel Visibility="{Binding Gpu.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 Gpu.ProcessNm}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Die Size -->
<StackPanel Visibility="{Binding Gpu.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 Gpu.DieSize}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Transistors -->
<StackPanel Visibility="{Binding Gpu.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 Gpu.Transistors}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Resolutions Section -->
<StackPanel Visibility="{Binding Resolutions.Count, Converter={StaticResource ZeroToVisibilityConverter}}"
Spacing="8">
<StackPanel Orientation="Horizontal"
Spacing="8">
<TextBlock Text="Supported Resolutions"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Resolutions.Count}"
FontSize="14"
FontWeight="Bold"
Foreground="{ThemeResource SystemAccentColor}" />
</StackPanel>
<!-- Scrollable Resolutions List -->
<ScrollViewer Height="200"
BorderThickness="1"
BorderBrush="{ThemeResource ControlElevationBorderBrush}"
CornerRadius="8">
<ItemsRepeater ItemsSource="{Binding Resolutions}"
Margin="0">
<ItemsRepeater.Layout>
<StackLayout Spacing="4" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Border Padding="12,8"
HorizontalAlignment="Stretch"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="4"
Margin="0,4">
<StackPanel Spacing="4">
<!-- First line: Resolution dimensions or format -->
<TextBlock Text="{Binding ResolutionDisplay}"
FontSize="14"
FontWeight="SemiBold" />
<!-- Second line: Color/palette information -->
<TextBlock Text="{Binding ColorDisplay}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</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>
/// GPU detail page showing all information, resolutions, computers, and consoles
/// </summary>
public sealed partial class GpuDetailPage : Page
{
private object? _navigationSource;
private int? _pendingGpuId;
public GpuDetailPage()
{
InitializeComponent();
DataContextChanged += GpuDetailPage_DataContextChanged;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
int? gpuId = null;
// Handle both int and GpuDetailNavigationParameter
if(e.Parameter is int intId)
gpuId = intId;
else if(e.Parameter is GpuDetailNavigationParameter navParam)
{
gpuId = navParam.GpuId;
_navigationSource = navParam.NavigationSource;
}
if(gpuId.HasValue)
{
_pendingGpuId = gpuId;
if(DataContext is GpuDetailViewModel viewModel)
{
viewModel.GpuId = gpuId.Value;
if(_navigationSource != null) viewModel.SetNavigationSource(_navigationSource);
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
}
private void GpuDetailPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(DataContext is GpuDetailViewModel viewModel && _pendingGpuId.HasValue)
{
viewModel.GpuId = _pendingGpuId.Value;
if(_navigationSource != null) viewModel.SetNavigationSource(_navigationSource);
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
private void ComputersSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
if(DataContext is GpuDetailViewModel vm) vm.ComputersFilterCommand.Execute(null);
}
private void ComputersSearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
if(DataContext is GpuDetailViewModel vm)
vm.ComputersFilterCommand.Execute(null);
}
private void ConsolesSearchBox_QuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
if(DataContext is GpuDetailViewModel vm) vm.ConsolesFilterCommand.Execute(null);
}
private void ConsolesSearchBox_TextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
if(DataContext is GpuDetailViewModel vm)
vm.ConsolesFilterCommand.Execute(null);
}
private void Computer_Click(object sender, RoutedEventArgs e)
{
if(sender is Button button && button.Tag is int machineId && DataContext is GpuDetailViewModel vm)
_ = vm.SelectMachineCommand.ExecuteAsync(machineId);
}
private void Console_Click(object sender, RoutedEventArgs e)
{
if(sender is Button button && button.Tag is int machineId && DataContext is GpuDetailViewModel vm)
_ = vm.SelectMachineCommand.ExecuteAsync(machineId);
}
}

View File

@@ -0,0 +1,207 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.GpuListPage"
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="Graphics Processing Units">
</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 graphics processing units..."
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 Graphics Processing Units"
Message="{Binding ErrorMessage}"
IsClosable="False" />
<Button Content="Retry"
Command="{Binding LoadData}"
HorizontalAlignment="Center"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<!-- GPUs 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 GpusList.Count}" />
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}"
Text="graphics processing units" />
</StackPanel>
<!-- GPUs List -->
<ItemsControl Grid.Row="1"
ItemsSource="{Binding GpusList}"
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.NavigateToGpuCommand, 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>
<!-- GPU 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 graphics processing units.
/// Features responsive layout, modern styling, and special handling for Framebuffer and Software entries.
/// </summary>
public sealed partial class GpuListPage : Page
{
public GpuListPage()
{
InitializeComponent();
Loaded += GpuListPage_Loaded;
DataContextChanged += GpuListPage_DataContextChanged;
}
private void GpuListPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(DataContext is GpusListViewModel vm)
{
// Load data when DataContext is set
vm.LoadData.Execute(null);
}
}
private void GpuListPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is GpusListViewModel vm)
{
// Load data when page is loaded (fallback)
vm.LoadData.Execute(null);
}
}
}

View File

@@ -0,0 +1,209 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.LoginPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Marechai.App.Presentation.ViewModels"
xmlns:utu="using:Uno.Toolkit.UI"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
d:DataContext="{d:DesignInstance vm:LoginViewModel}">
<Grid utu:SafeArea.Insets="VisibleBounds">
<!-- Center content on screen -->
<Grid HorizontalAlignment="Center"
VerticalAlignment="Center"
MaxWidth="480"
Padding="32">
<!-- Login Card with Mac OS 9 styling -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="2"
CornerRadius="8"
Padding="0"
Translation="0,0,16">
<Border.Shadow>
<ThemeShadow />
</Border.Shadow>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header with Mac OS 9 Title Bar Style -->
<Border Grid.Row="0"
Background="{ThemeResource AccentFillColorDefaultBrush}"
Padding="16,12"
CornerRadius="6,6,0,0">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Icon -->
<FontIcon Grid.Column="0"
Glyph="&#xE77B;"
FontSize="24"
Foreground="White"
Margin="0,0,12,0"
VerticalAlignment="Center" />
<!-- Title -->
<TextBlock Grid.Column="1"
x:Uid="LoginPage_Title"
Text="Sign In to Marechai"
Style="{StaticResource TitleTextBlockStyle}"
Foreground="White"
VerticalAlignment="Center" />
</Grid>
</Border>
<!-- Login Form Content -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto">
<StackPanel Padding="32"
Spacing="24">
<!-- Welcome Message -->
<TextBlock x:Uid="LoginPage_WelcomeMessage"
Text="Welcome back! Please sign in to continue."
Style="{StaticResource BodyTextBlockStyle}"
TextWrapping="Wrap"
HorizontalAlignment="Center"
TextAlignment="Center" />
<!-- Email Input -->
<StackPanel Spacing="8">
<TextBlock x:Uid="LoginPage_EmailLabel"
Text="Email Address"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<TextBox x:Name="EmailTextBox"
x:Uid="LoginPage_EmailTextBox"
Text="{Binding Email, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PlaceholderText="Enter your email"
InputScope="EmailSmtpAddress"
IsSpellCheckEnabled="False"
AutomationProperties.Name="Email address"
KeyDown="OnEmailKeyDown" />
</StackPanel>
<!-- Password Input -->
<StackPanel Spacing="8">
<TextBlock x:Uid="LoginPage_PasswordLabel"
Text="Password"
Style="{StaticResource BodyStrongTextBlockStyle}" />
<PasswordBox x:Name="PasswordBox"
x:Uid="LoginPage_PasswordBox"
Password="{Binding Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
PlaceholderText="Enter your password"
AutomationProperties.Name="Password"
KeyDown="OnPasswordKeyDown" />
</StackPanel>
<!-- Error Message -->
<Border x:Name="ErrorBorder"
Background="#FFF4E6"
BorderBrush="#FFB84D"
BorderThickness="2"
CornerRadius="4"
Padding="12"
Visibility="{Binding ErrorMessage, Converter={StaticResource NullToVisibilityConverter}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Warning Icon -->
<FontIcon Grid.Column="0"
Glyph="&#xE7BA;"
Foreground="#D97706"
FontSize="20"
Margin="0,0,8,0"
VerticalAlignment="Top" />
<!-- Error Text -->
<TextBlock Grid.Column="1"
Text="{Binding ErrorMessage}"
Foreground="#92400E"
TextWrapping="Wrap"
VerticalAlignment="Center" />
<!-- Close Button -->
<Button Grid.Column="2"
Command="{Binding ClearErrorCommand}"
Background="Transparent"
BorderThickness="0"
Padding="8"
Margin="8,0,0,0"
VerticalAlignment="Center"
AutomationProperties.Name="Close error message">
<FontIcon Glyph="&#xE711;"
FontSize="12"
Foreground="#92400E" />
</Button>
</Grid>
</Border>
<!-- Login Button -->
<Button x:Name="LoginButton"
x:Uid="LoginPage_LoginButton"
Command="{Binding LoginCommand}"
IsEnabled="{Binding IsLoggingIn, Converter={StaticResource InvertBoolConverter}}"
HorizontalAlignment="Stretch"
Padding="16,12"
Style="{StaticResource AccentButtonStyle}"
AutomationProperties.Name="Sign in button">
<Button.Content>
<Grid>
<TextBlock x:Uid="LoginPage_LoginButtonText"
Text="Sign In"
Visibility="{Binding IsLoggingIn, Converter={StaticResource InvertBoolToVisibilityConverter}}" />
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="8"
Visibility="{Binding IsLoggingIn, Converter={StaticResource BoolToVisibilityConverter}}">
<ProgressRing IsActive="True"
Width="20"
Height="20"
Foreground="White" />
<TextBlock x:Uid="LoginPage_SigningInText"
Text="Signing in..."
Foreground="White" />
</StackPanel>
</Grid>
</Button.Content>
</Button>
<!-- Additional Options -->
<StackPanel Spacing="8"
HorizontalAlignment="Center">
<HyperlinkButton x:Uid="LoginPage_ForgotPasswordLink"
Content="Forgot your password?"
HorizontalAlignment="Center"
Visibility="Collapsed" />
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center"
Spacing="4"
Visibility="Collapsed">
<TextBlock x:Uid="LoginPage_NoAccountText"
Text="Don't have an account?"
VerticalAlignment="Center" />
<HyperlinkButton x:Uid="LoginPage_SignUpLink"
Content="Sign up"
Padding="4,0" />
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</Grid>
</Grid>
</Page>

View File

@@ -0,0 +1,33 @@
using Windows.System;
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
namespace Marechai.App.Presentation.Views;
public sealed partial class LoginPage : Page
{
public LoginPage() => InitializeComponent();
private void OnEmailKeyDown(object sender, KeyRoutedEventArgs e)
{
if(e.Key == VirtualKey.Enter)
{
// Move focus to password field
PasswordBox.Focus(FocusState.Keyboard);
e.Handled = true;
}
}
private void OnPasswordKeyDown(object sender, KeyRoutedEventArgs e)
{
if(e.Key == VirtualKey.Enter)
{
// Trigger login when Enter is pressed
if(DataContext is LoginViewModel viewModel && LoginButton.IsEnabled) viewModel.LoginCommand.Execute(null);
e.Handled = true;
}
}
}

View File

@@ -0,0 +1,422 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.MachineViewPage"
x:Name="PageRoot"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wctui="using:CommunityToolkit.WinUI.UI.Controls"
NavigationCacheMode="Disabled"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header with Back Button -->
<Grid Grid.Row="0"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="12,12,16,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Back Button -->
<Button Grid.Column="0"
Command="{Binding GoBackCommand}"
Style="{ThemeResource AlternateButtonStyle}"
ToolTipService.ToolTip="Go back"
Padding="8"
MinWidth="44"
MinHeight="44"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE72B;"
FontSize="16" />
</Button>
<!-- Title Section -->
<StackPanel Grid.Column="1"
Margin="12,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding MachineName}"
FontSize="20"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}"
TextTrimming="CharacterEllipsis" />
<TextBlock Text="{Binding CompanyName}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}"
Margin="0,4,0,0" />
</StackPanel>
</Grid>
<!-- 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 machine details..."
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 Machine"
Message="{Binding ErrorMessage}"
IsClosable="False" />
<Button Content="Retry"
Command="{Binding LoadData}"
HorizontalAlignment="Center"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<!-- Machine Details -->
<ScrollViewer Visibility="{Binding IsDataLoaded}"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Padding="16"
Spacing="24">
<!-- Prototype Badge -->
<StackPanel Visibility="{Binding IsPrototype}">
<Border Background="{ThemeResource WarningFillColorTertiaryBrush}"
BorderBrush="{ThemeResource WarningBorderColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<TextBlock Text="PROTOTYPE"
FontSize="14"
FontWeight="Bold"
Foreground="{ThemeResource WarningForegroundColorDefaultBrush}"
TextAlignment="Center" />
</Border>
</StackPanel>
<!-- Introduction Date -->
<StackPanel Visibility="{Binding ShowIntroductionDate}"
Spacing="8">
<TextBlock Text="Introduction Date"
FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<TextBlock Text="{Binding IntroductionDateDisplay}"
FontSize="16"
Foreground="{ThemeResource TextControlForeground}" />
</Border>
</StackPanel>
<!-- Family and Model -->
<Grid Visibility="{Binding ShowFamilyOrModel}"
ColumnSpacing="16">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Family -->
<StackPanel Grid.Column="0"
Visibility="{Binding ShowFamily}"
Spacing="8">
<TextBlock Text="Family"
FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<TextBlock Text="{Binding FamilyName}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}"
TextTrimming="CharacterEllipsis" />
</Border>
</StackPanel>
<!-- Model -->
<StackPanel Grid.Column="1"
Visibility="{Binding ShowModel}"
Spacing="8">
<TextBlock Text="Model"
FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<TextBlock Text="{Binding ModelName}"
FontSize="14"
Foreground="{ThemeResource TextControlForeground}"
TextTrimming="CharacterEllipsis" />
</Border>
</StackPanel>
</Grid>
<!-- Processors Section -->
<StackPanel Visibility="{Binding ShowProcessors}"
Spacing="12">
<TextBlock Text="Processors"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<ItemsControl ItemsSource="{Binding Processors}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<StackPanel Spacing="8">
<TextBlock Text="{Binding DisplayName}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<TextBlock Text="{Binding Manufacturer}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding DetailsText}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}"
TextWrapping="Wrap"
Visibility="{Binding HasDetails}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!-- Memory Section -->
<StackPanel Visibility="{Binding ShowMemory}"
Spacing="12">
<TextBlock Text="Memory"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<ItemsControl ItemsSource="{Binding Memory}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<StackPanel Spacing="6">
<TextBlock Text="{Binding SizeDisplay}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<TextBlock Text="{Binding TypeDisplay}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!-- GPUs Section -->
<StackPanel Visibility="{Binding ShowGpus}"
Spacing="12">
<TextBlock Text="Graphics Processing Units"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<ItemsControl ItemsSource="{Binding Gpus}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<StackPanel Spacing="8">
<TextBlock Text="{Binding DisplayName}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<TextBlock Text="{Binding Manufacturer}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}"
Visibility="{Binding HasManufacturer}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!-- Sound Synthesizers Section -->
<StackPanel Visibility="{Binding ShowSoundSynthesizers}"
Spacing="12">
<TextBlock Text="Sound Synthesizers"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<ItemsControl ItemsSource="{Binding SoundSynthesizers}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<StackPanel Spacing="8">
<TextBlock Text="{Binding DisplayName}"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<TextBlock Text="{Binding DetailsText}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}"
TextWrapping="Wrap"
Visibility="{Binding HasDetails}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!-- Storage Section -->
<StackPanel Visibility="{Binding ShowStorage}"
Spacing="12">
<TextBlock Text="Storage"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<ItemsControl ItemsSource="{Binding Storage}"
HorizontalAlignment="Stretch">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="16,12">
<StackPanel Spacing="6">
<TextBlock Text="{Binding DisplayText}"
FontSize="13"
Foreground="{ThemeResource TextControlForeground}"
TextWrapping="Wrap" />
<TextBlock Text="{Binding TypeNote}"
FontSize="11"
Foreground="{ThemeResource SystemBaseMediumColor}"
TextWrapping="Wrap" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<!-- Photos Carousel (Last element before spacing) -->
<StackPanel Visibility="{Binding ShowPhotos}"
Spacing="12">
<TextBlock Text="Photos"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<wctui:Carousel ItemsSource="{Binding Photos}"
Height="280"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
HorizontalAlignment="Stretch">
<wctui:Carousel.ItemTemplate>
<DataTemplate>
<Button Command="{Binding DataContext.ViewPhotoDetailsCommand, ElementName=PageRoot}"
CommandParameter="{Binding PhotoId}"
Padding="0"
Background="Transparent">
<Grid Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<Image Source="{Binding ThumbnailImageSource}"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center"
MaxWidth="256"
MaxHeight="256" />
</Grid>
</Button>
</DataTemplate>
</wctui:Carousel.ItemTemplate>
</wctui:Carousel>
</StackPanel>
<!-- Bottom Spacing -->
<Border Height="24" />
</StackPanel>
</ScrollViewer>
</Grid>
</Grid>
</Page>

View File

@@ -0,0 +1,82 @@
/******************************************************************************
// 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 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;
public sealed partial class MachineViewPage : Page
{
private object? _navigationSource;
private int? _pendingMachineId;
public MachineViewPage()
{
InitializeComponent();
DataContextChanged += MachineViewPage_DataContextChanged;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
int? machineId = null;
// Handle both int and MachineViewNavigationParameter
if(e.Parameter is int intId)
machineId = intId;
else if(e.Parameter is MachineViewNavigationParameter navParam)
{
machineId = navParam.MachineId;
_navigationSource = navParam.NavigationSource;
}
if(machineId.HasValue)
{
_pendingMachineId = machineId;
if(DataContext is MachineViewViewModel viewModel)
{
viewModel.SetNavigationSource(_navigationSource);
_ = viewModel.LoadMachineAsync(machineId.Value);
}
}
}
private void MachineViewPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(DataContext is MachineViewViewModel viewModel && _pendingMachineId.HasValue)
{
viewModel.SetNavigationSource(_navigationSource);
_ = viewModel.LoadMachineAsync(_pendingMachineId.Value);
}
}
}

View File

@@ -0,0 +1,55 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:utu="using:Uno.Toolkit.UI"
xmlns:uen="using:Uno.Extensions.Navigation.UI"
xmlns:components="clr-namespace:Marechai.App.Presentation.Components"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid utu:SafeArea.Insets="VisibleBounds">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="SidebarColumn"
Width="280" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Sidebar -->
<Grid x:Name="SidebarWrapper"
Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="0"
Width="280"
HorizontalAlignment="Left">
<components:Sidebar x:Name="SidebarPanel"
DataContext="{Binding}"
VerticalAlignment="Stretch" />
</Grid>
<!-- Header -->
<utu:NavigationBar Grid.Row="0"
Grid.Column="1"
Content="{Binding Title}">
<utu:NavigationBar.MainCommand>
<AppBarButton Icon="GlobalNavigationButton"
Command="{Binding ToggleSidebarCommand}"
Label="Toggle Sidebar"
AutomationProperties.Name="Toggle sidebar visibility" />
</utu:NavigationBar.MainCommand>
</utu:NavigationBar>
<!-- Content Region for Navigation -->
<ContentControl Grid.Row="1"
Grid.Column="1"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
uen:Region.Attached="True"
uen:Region.Name="Main" />
</Grid>
</Page>

View File

@@ -0,0 +1,98 @@
using System;
using System.ComponentModel;
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Marechai.App.Presentation.Views;
public sealed partial class MainPage : Page
{
private PropertyChangedEventHandler _sidebarPropertyChangedHandler;
public MainPage()
{
InitializeComponent();
DataContextChanged += MainPage_DataContextChanged;
Loaded += MainPage_Loaded;
}
private void MainPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is not MainViewModel viewModel) return;
SidebarWrapper.Width = viewModel.IsSidebarOpen ? 280 : 60;
if(_sidebarPropertyChangedHandler != null) return;
_sidebarPropertyChangedHandler = (_, propArgs) =>
{
if(propArgs.PropertyName != nameof(MainViewModel.IsSidebarOpen)) return;
AnimateSidebarWidth(((MainViewModel)DataContext).IsSidebarOpen);
};
((INotifyPropertyChanged)viewModel).PropertyChanged += _sidebarPropertyChangedHandler;
}
void AnimateSidebarWidth(bool isOpen)
{
double start = SidebarColumn.Width.Value;
double end = isOpen ? 280 : 60;
if(Math.Abs(start - end) < 0.1) return;
// If expanding, show content immediately
if(isOpen && DataContext is MainViewModel vm) vm.SidebarContentVisible = true;
const int durationMs = 250;
const int fps = 60;
var steps = (int)(durationMs / (1000.0 / fps));
var currentStep = 0;
var timer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(1000.0 / fps)
};
timer.Tick += (_, _) =>
{
currentStep++;
double t = (double)currentStep / steps;
// Ease in-out cubic
double eased = t < 0.5 ? 4 * t * t * t : 1 - Math.Pow(-2 * t + 2, 3) / 2;
double value = start + (end - start) * eased;
SidebarColumn.Width = new GridLength(value, GridUnitType.Pixel);
SidebarWrapper.Width = value;
if(currentStep >= steps)
{
SidebarColumn.Width = new GridLength(end, GridUnitType.Pixel);
SidebarWrapper.Width = end;
timer.Stop();
// After collapse animation completes, hide sidebar content
if(!isOpen && DataContext is MainViewModel vm) vm.SidebarContentVisible = false;
}
};
timer.Start();
}
private void MainPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(args.NewValue is MainViewModel vm && _sidebarPropertyChangedHandler == null)
{
SidebarWrapper.Width = vm.IsSidebarOpen ? 280 : 60;
_sidebarPropertyChangedHandler = (_, propArgs) =>
{
if(propArgs.PropertyName != nameof(MainViewModel.IsSidebarOpen)) return;
AnimateSidebarWidth(vm.IsSidebarOpen);
};
((INotifyPropertyChanged)vm).PropertyChanged += _sidebarPropertyChangedHandler;
}
}
}

View File

@@ -0,0 +1,130 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.NewsPage"
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">
<RefreshContainer x:Name="RefreshContainer"
RefreshRequested="RefreshContainer_RefreshRequested">
<ScrollViewer>
<Grid Padding="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- News Title Section -->
<StackPanel Grid.Row="0"
Margin="0,0,0,16">
<TextBlock Text="Latest News"
FontSize="32"
FontWeight="Bold"
Foreground="{ThemeResource SystemAccentColor}"
Margin="0,0,0,8" />
<TextBlock Text="Stay updated with the latest additions to the database"
FontSize="14"
Foreground="{ThemeResource SystemBaseMediumColor}"
TextWrapping="Wrap" />
</StackPanel>
<!-- Loading State -->
<StackPanel Grid.Row="2"
Visibility="{Binding IsLoading}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="32"
Spacing="16">
<ProgressRing IsActive="True"
IsIndeterminate="True"
Height="48"
Width="48"
Foreground="{ThemeResource SystemAccentColor}" />
<TextBlock Text="Loading latest news..."
FontSize="16"
TextAlignment="Center"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
<!-- Error State -->
<StackPanel Grid.Row="2"
Visibility="{Binding HasError}"
VerticalAlignment="Center"
Spacing="16"
Padding="32">
<InfoBar IsOpen="True"
Severity="Error"
Title="Unable to Load News"
Message="{Binding ErrorMessage}"
IsClosable="False" />
<Button Content="Retry"
Command="{Binding LoadNews}"
HorizontalAlignment="Center"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<!-- News Feed -->
<ItemsControl Grid.Row="2"
ItemsSource="{Binding NewsList}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Button Margin="0,0,0,12"
Padding="0"
Background="Transparent"
BorderThickness="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Command="{Binding NavigateToItemCommand}"
CommandParameter="{Binding News}">
<Border CornerRadius="8"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
Padding="16">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Date -->
<TextBlock Grid.Row="0"
Text="{Binding News.Timestamp}"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}"
Margin="0,0,0,12" />
<!-- News Title/Text (Localized) -->
<TextBlock Grid.Row="1"
Text="{Binding DisplayText}"
FontSize="16"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseHighColor}"
TextWrapping="Wrap"
Margin="0,0,0,12" />
<!-- Item Name -->
<TextBlock Grid.Row="2"
Text="{Binding News.ItemName}"
FontSize="14"
Foreground="{ThemeResource SystemAccentColor}" />
</Grid>
</Border>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical"
Spacing="0" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Grid>
</ScrollViewer>
</RefreshContainer>
</Grid>
</Page>

View File

@@ -0,0 +1,73 @@
using System;
using Windows.Foundation;
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Marechai.App.Presentation.Views;
public sealed partial class NewsPage : Page
{
private bool _initialNewsLoaded;
public NewsPage()
{
InitializeComponent();
DataContextChanged += NewsPage_DataContextChanged;
Loaded += NewsPage_Loaded;
}
private void NewsPage_Loaded(object sender, RoutedEventArgs e)
{
if(_initialNewsLoaded) return;
if(DataContext is NewsViewModel viewModel)
{
_initialNewsLoaded = true;
_ = viewModel.LoadNews.ExecuteAsync(null);
DataContextChanged -= NewsPage_DataContextChanged;
}
}
private void NewsPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(_initialNewsLoaded) return;
if(args.NewValue is NewsViewModel viewModel)
{
_initialNewsLoaded = true;
_ = viewModel.LoadNews.ExecuteAsync(null);
DataContextChanged -= NewsPage_DataContextChanged;
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if(_initialNewsLoaded) return;
if(DataContext is NewsViewModel viewModel)
{
_initialNewsLoaded = true;
_ = viewModel.LoadNews.ExecuteAsync(null);
DataContextChanged -= NewsPage_DataContextChanged;
}
}
private async void RefreshContainer_RefreshRequested(RefreshContainer sender, RefreshRequestedEventArgs args)
{
// Handle pull-to-refresh
using Deferral deferral = args.GetDeferral();
try
{
if(DataContext is NewsViewModel viewModel) await viewModel.LoadNews.ExecuteAsync(null);
}
catch(Exception)
{
// Swallow to avoid process crash; NewsViewModel already logs errors.
}
}
}

View File

@@ -0,0 +1,556 @@
<?xml version="1.0" encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.PhotoDetailPage"
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="Disabled"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header with Back Button -->
<Grid Grid.Row="0"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="12,12,16,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Button Grid.Column="0"
Command="{Binding GoBackCommand}"
Style="{ThemeResource AlternateButtonStyle}"
ToolTipService.ToolTip="Go back"
Padding="8"
MinWidth="44"
MinHeight="44"
VerticalAlignment="Center">
<FontIcon Glyph="&#xE72B;" FontSize="16" />
</Button>
<StackPanel Grid.Column="1"
Margin="12,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="Photo Details"
FontSize="20"
FontWeight="SemiBold"
TextTrimming="CharacterEllipsis" />
</StackPanel>
</Grid>
<!-- 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" />
<TextBlock Text="Loading photo..."
FontSize="14"
TextAlignment="Center" />
</StackPanel>
<!-- Error State -->
<StackPanel Visibility="{Binding ErrorOccurred}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="24"
Spacing="16"
MaxWidth="400">
<InfoBar IsOpen="True"
Severity="Error"
Title="Unable to Load Photo"
Message="{Binding ErrorMessage}"
IsClosable="False" />
</StackPanel>
<!-- Responsive Layout -->
<utu:ResponsiveView Visibility="{Binding PhotoImageSource, Converter={StaticResource ObjectToVisibilityConverter}}">
<!-- Narrow Template -->
<utu:ResponsiveView.NarrowTemplate>
<DataTemplate>
<ScrollViewer VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Padding="16" Spacing="24">
<!-- Photo -->
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="8"
MaxHeight="400"
MinHeight="250">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
ZoomMode="Enabled"
MinZoomFactor="1.0"
MaxZoomFactor="5.0">
<Image Source="{Binding PhotoImageSource}"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</ScrollViewer>
</Border>
<!-- Metadata Sections -->
<StackPanel Spacing="16">
<!-- Machine -->
<StackPanel Spacing="12">
<TextBlock Text="Machine" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoMachineName, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Name" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoMachineName}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoMachineCompany, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Company" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoMachineCompany}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Camera -->
<StackPanel Spacing="12">
<TextBlock Text="Camera" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoCameraManufacturer, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Manufacturer" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoCameraManufacturer}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoCameraModel, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Model" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoCameraModel}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoLensModel, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Lens" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoLensModel}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoSoftwareUsed, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Software" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSoftwareUsed}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Exposure Settings -->
<StackPanel Spacing="12">
<TextBlock Text="Exposure Settings" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoAperture, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Aperture" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoAperture}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoExposureTime, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Exposure Time" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoExposureTime}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoExposureMode, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Exposure Mode" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoExposureMode}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoExposureProgram, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Exposure Program" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoExposureProgram}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoIsoRating, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="ISO Rating" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoIsoRating}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Flash & Light -->
<StackPanel Spacing="12">
<TextBlock Text="Flash &amp; Light" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoFlash, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Flash" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoFlash}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoLightSource, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Light Source" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoLightSource}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoMeteringMode, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Metering Mode" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoMeteringMode}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoWhiteBalance, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="White Balance" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoWhiteBalance}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Focal Length -->
<StackPanel Spacing="12">
<TextBlock Text="Focal Length" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoFocalLength, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Focal Length" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoFocalLength}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoFocalLengthEquivalent, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="35mm Equivalent" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoFocalLengthEquivalent}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoDigitalZoomRatio, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Digital Zoom" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoDigitalZoomRatio}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Image Properties -->
<StackPanel Spacing="12">
<TextBlock Text="Image Properties" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoColorSpace, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Color Space" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoColorSpace}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoContrast, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Contrast" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoContrast}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoSaturation, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Saturation" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSaturation}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoSharpness, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Sharpness" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSharpness}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoOrientation, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Orientation" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoOrientation}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoSceneCaptureType, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Scene Capture Type" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSceneCaptureType}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Resolution -->
<StackPanel Spacing="12">
<TextBlock Text="Resolution" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoHorizontalResolution, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Horizontal Resolution" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoHorizontalResolution}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoVerticalResolution, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Vertical Resolution" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoVerticalResolution}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoResolutionUnit, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Resolution Unit" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoResolutionUnit}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- File & Metadata -->
<StackPanel Spacing="12">
<TextBlock Text="File &amp; Metadata" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoOriginalExtension, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Original Format" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoOriginalExtension}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoCreationDate, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Date Taken" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoCreationDate}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoUploadDate, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Upload Date" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoUploadDate}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoExifVersion, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="EXIF Version" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoExifVersion}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoAuthor, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Author" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoAuthor}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoLicenseName, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="License" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoLicenseName}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoComments, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Comments" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoComments}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoSource, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Source" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSource}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Additional Sensors -->
<StackPanel Spacing="12">
<TextBlock Text="Advanced" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoSensingMethod, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Sensing Method" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSensingMethod}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoSubjectDistanceRange, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Subject Distance Range" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSubjectDistanceRange}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<Border Height="24" />
</StackPanel>
</StackPanel>
</ScrollViewer>
</DataTemplate>
</utu:ResponsiveView.NarrowTemplate>
<!-- Wide Template -->
<utu:ResponsiveView.WideTemplate>
<DataTemplate>
<Grid Padding="16" ColumnSpacing="24">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<!-- Photo on left -->
<Border Grid.Column="0"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="8"
Padding="8"
VerticalAlignment="Top"
MaxHeight="500">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
ZoomMode="Enabled"
MinZoomFactor="1.0"
MaxZoomFactor="5.0">
<Image Source="{Binding PhotoImageSource}"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</ScrollViewer>
</Border>
<!-- Info on right -->
<ScrollViewer Grid.Column="1"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled">
<StackPanel Spacing="16" Padding="8">
<!-- Same content as narrow template -->
<!-- Machine -->
<StackPanel Spacing="12">
<TextBlock Text="Machine" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoMachineName, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Name" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoMachineName}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoMachineCompany, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Company" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoMachineCompany}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Camera -->
<StackPanel Spacing="12">
<TextBlock Text="Camera" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoCameraManufacturer, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Manufacturer" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoCameraManufacturer}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoCameraModel, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Model" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoCameraModel}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoLensModel, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Lens" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoLensModel}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoSoftwareUsed, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Software" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSoftwareUsed}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Exposure Settings -->
<StackPanel Spacing="12">
<TextBlock Text="Exposure Settings" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoAperture, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Aperture" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoAperture}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoExposureTime, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Exposure Time" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoExposureTime}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoExposureMode, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Exposure Mode" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoExposureMode}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoExposureProgram, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Exposure Program" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoExposureProgram}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoIsoRating, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="ISO Rating" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoIsoRating}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Flash & Light -->
<StackPanel Spacing="12">
<TextBlock Text="Flash &amp; Light" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoFlash, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Flash" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoFlash}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoLightSource, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Light Source" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoLightSource}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoMeteringMode, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Metering Mode" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoMeteringMode}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoWhiteBalance, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="White Balance" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoWhiteBalance}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Focal Length -->
<StackPanel Spacing="12">
<TextBlock Text="Focal Length" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoFocalLength, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Focal Length" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoFocalLength}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoFocalLengthEquivalent, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="35mm Equivalent" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoFocalLengthEquivalent}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoDigitalZoomRatio, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Digital Zoom" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoDigitalZoomRatio}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Image Properties -->
<StackPanel Spacing="12">
<TextBlock Text="Image Properties" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoColorSpace, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Color Space" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoColorSpace}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoContrast, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Contrast" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoContrast}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoSaturation, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Saturation" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSaturation}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoSharpness, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Sharpness" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSharpness}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoOrientation, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Orientation" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoOrientation}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoSceneCaptureType, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Scene Capture Type" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSceneCaptureType}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Resolution -->
<StackPanel Spacing="12">
<TextBlock Text="Resolution" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoHorizontalResolution, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Horizontal Resolution" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoHorizontalResolution}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoVerticalResolution, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Vertical Resolution" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoVerticalResolution}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoResolutionUnit, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Resolution Unit" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoResolutionUnit}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- File & Metadata -->
<StackPanel Spacing="12">
<TextBlock Text="File &amp; Metadata" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoOriginalExtension, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Original Format" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoOriginalExtension}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoCreationDate, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Date Taken" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoCreationDate}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoUploadDate, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Upload Date" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoUploadDate}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoExifVersion, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="EXIF Version" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoExifVersion}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoAuthor, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Author" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoAuthor}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoLicenseName, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="License" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoLicenseName}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoComments, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Comments" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoComments}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoSource, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Source" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSource}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<!-- Advanced -->
<StackPanel Spacing="12">
<TextBlock Text="Advanced" FontSize="14" FontWeight="SemiBold" />
<StackPanel Spacing="4" Visibility="{Binding PhotoSensingMethod, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Sensing Method" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSensingMethod}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Spacing="4" Visibility="{Binding PhotoSubjectDistanceRange, Converter={StaticResource StringToVisibilityConverter}}">
<TextBlock Text="Subject Distance Range" FontSize="12" FontWeight="SemiBold" Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding PhotoSubjectDistanceRange}" FontSize="13" TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
<Border Height="24" />
</StackPanel>
</ScrollViewer>
</Grid>
</DataTemplate>
</utu:ResponsiveView.WideTemplate>
</utu:ResponsiveView>
</Grid>
</Grid>
</Page>

View File

@@ -0,0 +1,47 @@
using System;
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Marechai.App.Presentation.Views;
public sealed partial class PhotoDetailPage : Page
{
private Guid? _pendingPhotoId;
public PhotoDetailPage()
{
InitializeComponent();
DataContextChanged += PhotoDetailPage_DataContextChanged;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
Guid? photoId = null;
if(e.Parameter is PhotoDetailNavigationParameter param) photoId = param.PhotoId;
if(photoId.HasValue)
{
_pendingPhotoId = photoId;
if(DataContext is PhotoDetailViewModel viewModel)
_ = viewModel.LoadPhotoCommand.ExecuteAsync(photoId.Value);
}
}
private void PhotoDetailPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(DataContext is PhotoDetailViewModel viewModel && _pendingPhotoId.HasValue)
_ = viewModel.LoadPhotoCommand.ExecuteAsync(_pendingPhotoId.Value);
}
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
_pendingPhotoId = null;
}
}

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,22 @@
<Page x:Class="Marechai.App.Presentation.Views.SecondPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:utu="using:Uno.Toolkit.UI"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid utu:SafeArea.Insets="VisibleBounds">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition />
</Grid.RowDefinitions>
<utu:NavigationBar Content="Second Page" />
<StackPanel Grid.Row="1"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock Text="{Binding Entity.Name}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="8" />
</StackPanel>
</Grid>
</Page>

View File

@@ -1,11 +1,11 @@
using Microsoft.UI.Xaml.Controls;
namespace Marechai.App.Presentation;
namespace Marechai.App.Presentation.Views;
public sealed partial class SecondPage : Page
{
public SecondPage()
{
this.InitializeComponent();
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.SettingsPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Marechai.App.Presentation.ViewModels"
xmlns:utu="using:Uno.Toolkit.UI"
mc:Ignorable="d"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid utu:SafeArea.Insets="VisibleBounds" Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<!-- Header -->
<TextBlock Grid.Row="0"
Text="{Binding Title}"
Style="{ThemeResource TitleTextBlockStyle}"
Margin="0,0,0,24"/>
<!-- Settings Content -->
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="24" MaxWidth="600" HorizontalAlignment="Left">
<!-- Theme Section -->
<StackPanel Spacing="8">
<TextBlock x:Uid="SettingsPage_ThemeSection_Header"
Text="Appearance"
Style="{ThemeResource SubtitleTextBlockStyle}"/>
<TextBlock x:Uid="SettingsPage_ThemeSection_Description"
Text="Select your preferred color theme"
Style="{ThemeResource BodyTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Margin="0,0,0,8"/>
<!-- Brightness Theme Selector (Light/Dark/System) -->
<ComboBox x:Name="BrightnessThemeSelector"
x:Uid="SettingsPage_BrightnessThemeSelector"
Header="Brightness"
ItemsSource="{Binding AvailableThemes}"
SelectedItem="{Binding SelectedTheme, Mode=TwoWay}"
HorizontalAlignment="Stretch"
MinWidth="250"
Margin="0,0,0,12">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<!-- Color Theme Selector (Default/Windows 3.11) -->
<ComboBox x:Name="ColorThemeSelector"
x:Uid="SettingsPage_ColorThemeSelector"
Header="Color Theme"
ItemsSource="{Binding AvailableColorThemes}"
SelectedItem="{Binding SelectedColorTheme, Mode=TwoWay}"
HorizontalAlignment="Stretch"
MinWidth="250">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<!-- Placeholder for future settings sections -->
<StackPanel Spacing="8">
<TextBlock x:Uid="SettingsPage_AboutSection_Header"
Text="About"
Style="{ThemeResource SubtitleTextBlockStyle}"/>
<TextBlock x:Uid="SettingsPage_Version"
Text="Version 1.0"
Style="{ThemeResource BodyTextBlockStyle}"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"/>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,14 @@
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml.Controls;
namespace Marechai.App.Presentation.Views;
public sealed partial class SettingsPage : Page
{
public SettingsPage()
{
InitializeComponent();
}
public SettingsViewModel? ViewModel => DataContext as SettingsViewModel;
}

View File

@@ -0,0 +1,34 @@
<UserControl x:Class="Marechai.App.Presentation.Views.Shell"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:utu="using:Uno.Toolkit.UI"
mc:Ignorable="d"
d:DesignHeight="300"
d:DesignWidth="400">
<Border Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<utu:ExtendedSplashScreen x:Name="Splash"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch">
<utu:ExtendedSplashScreen.LoadingContentTemplate>
<DataTemplate>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="2*" /> <RowDefinition />
</Grid.RowDefinitions>
<ProgressRing IsActive="True"
Grid.Row="1"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Height="100"
Width="100" />
</Grid>
</DataTemplate>
</utu:ExtendedSplashScreen.LoadingContentTemplate>
</utu:ExtendedSplashScreen>
</Border>
</UserControl>

View File

@@ -1,14 +1,14 @@
using Microsoft.UI.Xaml.Controls;
using Uno.Extensions.Hosting;
namespace Marechai.App.Presentation;
namespace Marechai.App.Presentation.Views;
public sealed partial class Shell : UserControl, IContentControlProvider
{
public Shell()
{
this.InitializeComponent();
InitializeComponent();
}
public ContentControl ContentControl => Splash;
}
}

View File

@@ -0,0 +1,310 @@
<?xml version="1.0" encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.SoundSynthDetailPage"
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"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Page.Resources>
</Page.Resources>
<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>
<!-- Sound Synthesizer Details -->
<StackPanel Visibility="{Binding IsDataLoaded}"
Spacing="16">
<!-- Sound Synthesizer Name -->
<TextBlock Text="{Binding SoundSynth.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 SoundSynth.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 SoundSynth.ModelCode}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Introduced Date -->
<StackPanel Visibility="{Binding SoundSynth.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 SoundSynth.Introduced}"
FontSize="14" />
</StackPanel>
<!-- Voices -->
<StackPanel Visibility="{Binding SoundSynth.Voices, Converter={StaticResource ObjectToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Voices"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding SoundSynth.Voices}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Frequency -->
<StackPanel Visibility="{Binding SoundSynth.Frequency, Converter={StaticResource ObjectToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Frequency (Hz)"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding SoundSynth.Frequency}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Depth -->
<StackPanel Visibility="{Binding SoundSynth.Depth, Converter={StaticResource ObjectToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Depth (bit)"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding SoundSynth.Depth}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Type -->
<StackPanel Visibility="{Binding SoundSynth.Type, Converter={StaticResource StringToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Type"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding SoundSynth.Type}"
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,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;
/// <summary>
/// Sound Synthesizer detail page showing all information, computers, and consoles
/// </summary>
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);
}
}

View File

@@ -0,0 +1,207 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.SoundSynthListPage"
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="Sound Synthesizers">
</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 sound synthesizers..."
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 Sound Synthesizers"
Message="{Binding ErrorMessage}"
IsClosable="False" />
<Button Content="Retry"
Command="{Binding LoadData}"
HorizontalAlignment="Center"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<!-- Sound Synthesizers 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 SoundSynths.Count}" />
<TextBlock FontSize="12"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}"
Text="sound synthesizers" />
</StackPanel>
<!-- Sound Synthesizers List -->
<ItemsControl Grid.Row="1"
ItemsSource="{Binding SoundSynths}"
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.NavigateToSoundSynthCommand, 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>
<!-- Sound Synth 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,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);
}
}
}

View File

@@ -0,0 +1,184 @@
<?xml version="1.0"
encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.UsersPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:CommunityToolkit.WinUI.UI.Controls"
x:Name="PageRoot"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid RowSpacing="0">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header with Title -->
<Grid Grid.Row="0"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
BorderThickness="0,0,0,1"
Padding="16,12">
<!-- Page Title -->
<StackPanel VerticalAlignment="Center">
<TextBlock Text="User Management"
FontSize="20"
FontWeight="SemiBold"
Foreground="{ThemeResource TextControlForeground}" />
<TextBlock Text="Manage users, roles, and permissions"
FontSize="12"
Foreground="{ThemeResource SystemBaseMediumColor}"
Margin="0,4,0,0" />
</StackPanel>
</Grid>
<!-- Main Content -->
<Grid Grid.Row="1">
<!-- Access Denied Message -->
<StackPanel Visibility="{Binding IsUberadmin, Converter={StaticResource InvertBoolToVisibilityConverter}}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="24"
Spacing="16"
MaxWidth="400">
<InfoBar IsOpen="True"
Severity="Warning"
Title="Access Denied"
Message="You must be an Uberadmin to access user management."
IsClosable="False" />
</StackPanel>
<!-- Content (only visible to Uberadmin) -->
<Grid Visibility="{Binding IsUberadmin, Converter={StaticResource BoolToVisibilityConverter}}"
Canvas.ZIndex="0">
<!-- Loading State -->
<StackPanel Visibility="{Binding IsLoading, Converter={StaticResource BoolToVisibilityConverter}}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="32"
Spacing="16">
<ProgressRing IsActive="True"
IsIndeterminate="True"
Height="64"
Width="64"
Foreground="{ThemeResource SystemAccentColor}" />
<TextBlock Text="Loading users..."
FontSize="14"
TextAlignment="Center"
Foreground="{ThemeResource SystemBaseMediumColor}" />
</StackPanel>
<!-- Error State -->
<StackPanel Visibility="{Binding HasError, Converter={StaticResource BoolToVisibilityConverter}}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="24"
Spacing="16"
MaxWidth="400"
Canvas.ZIndex="100">
<InfoBar IsOpen="True"
Severity="Error"
Title="Unable to Load Users"
Message="{Binding ErrorMessage}"
IsClosable="False" />
<Button Content="Retry"
Command="{Binding LoadUsersCommand}"
HorizontalAlignment="Center"
Style="{ThemeResource AccentButtonStyle}" />
</StackPanel>
<!-- Users DataGrid -->
<Grid Visibility="{Binding IsDataLoaded, Converter={StaticResource BoolToVisibilityConverter}}"
Canvas.ZIndex="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Command Bar -->
<CommandBar Grid.Row="0"
DefaultLabelPosition="Right">
<AppBarButton Icon="Add"
Label="Add User"
Command="{Binding OpenAddUserDialogCommand}" />
<AppBarButton Icon="Refresh"
Label="Refresh"
Command="{Binding LoadUsersCommand}" />
</CommandBar>
<!-- DataGrid -->
<controls:DataGrid Grid.Row="1"
Margin="16"
ItemsSource="{Binding Users}"
SelectedItem="{Binding SelectedUser, Mode=TwoWay}"
AutoGenerateColumns="False"
GridLinesVisibility="None"
HeadersVisibility="Column"
AlternatingRowBackground="{ThemeResource CardBackgroundFillColorSecondaryBrush}"
CanUserReorderColumns="True"
CanUserResizeColumns="True"
CanUserSortColumns="True"
IsReadOnly="True"
SelectionMode="Single"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<controls:DataGrid.Columns>
<controls:DataGridTextColumn Header="Email"
Binding="{Binding Email}"
Width="2*" />
<controls:DataGridTextColumn Header="Username"
Binding="{Binding UserName}"
Width="*" />
<controls:DataGridTextColumn Header="Phone"
Binding="{Binding PhoneNumber}"
Width="*" />
<controls:DataGridTextColumn Header="Roles"
Width="2*"
Binding="{Binding Roles, Converter={StaticResource RolesListConverter}}" />
<controls:DataGridTemplateColumn Header="Actions"
Width="Auto">
<controls:DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal"
Spacing="4">
<Button Content="Edit"
Command="{Binding DataContext.OpenEditUserDialogCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Style="{ThemeResource TextBlockButtonStyle}"
Padding="8,4" />
<Button Content="Password"
Command="{Binding DataContext.OpenChangePasswordDialogCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Style="{ThemeResource TextBlockButtonStyle}"
Padding="8,4" />
<Button Content="Roles"
Command="{Binding DataContext.OpenManageRolesDialogCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Style="{ThemeResource TextBlockButtonStyle}"
Padding="8,4" />
<Button Content="Delete"
Command="{Binding DataContext.DeleteUserCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Foreground="{ThemeResource SystemErrorTextColor}"
Style="{ThemeResource TextBlockButtonStyle}"
Padding="8,4" />
</StackPanel>
</DataTemplate>
</controls:DataGridTemplateColumn.CellTemplate>
</controls:DataGridTemplateColumn>
</controls:DataGrid.Columns>
</controls:DataGrid>
</Grid>
</Grid>
</Grid>
</Grid>
</Page>

View File

@@ -0,0 +1,394 @@
using System;
using System.Diagnostics;
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI;
using Microsoft.UI.Text;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
namespace Marechai.App.Presentation.Views;
/// <summary>
/// User management page for Uberadmins
/// </summary>
public sealed partial class UsersPage : Page
{
private ContentDialog? _currentOpenDialog;
private UsersViewModel? _currentViewModel;
public UsersPage()
{
InitializeComponent();
Loaded += UsersPage_Loaded;
DataContextChanged += UsersPage_DataContextChanged;
}
private void UsersPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
// Unsubscribe from previous ViewModel
if(_currentViewModel != null) _currentViewModel.ShowDialogRequested -= OnShowDialogRequested;
// Subscribe to new ViewModel
if(DataContext is UsersViewModel vm)
{
_currentViewModel = vm;
vm.ShowDialogRequested += OnShowDialogRequested;
if(vm.IsUberadmin)
{
// Load data when DataContext is set and user is Uberadmin
vm.LoadUsersCommand.Execute(null);
}
}
}
private void UsersPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is UsersViewModel vm && vm.IsUberadmin)
{
// Load data when page is loaded (fallback)
vm.LoadUsersCommand.Execute(null);
}
}
private async void OnShowDialogRequested(object? sender, string dialogType)
{
// Close any currently open dialog first
if(_currentOpenDialog != null)
{
_currentOpenDialog.Hide();
_currentOpenDialog = null;
}
if(DataContext is not UsersViewModel vm) return;
ContentDialog? dialog = null;
switch(dialogType)
{
case "AddEdit":
{
var emailBox = new TextBox
{
Header = "Email",
Text = vm.Email
};
emailBox.TextChanged += (s, e) => vm.Email = emailBox.Text;
var userNameBox = new TextBox
{
Header = "Username",
Text = vm.UserName
};
userNameBox.TextChanged += (s, e) => vm.UserName = userNameBox.Text;
var phoneBox = new TextBox
{
Header = "Phone Number",
Text = vm.PhoneNumber
};
phoneBox.TextChanged += (s, e) => vm.PhoneNumber = phoneBox.Text;
var passwordBox = new PasswordBox
{
Header = "Password",
Password = vm.Password,
PlaceholderText = "Leave blank to keep current (when editing)"
};
passwordBox.PasswordChanged += (s, e) => vm.Password = passwordBox.Password;
var confirmPasswordBox = new PasswordBox
{
Header = "Confirm Password",
Password = vm.ConfirmPassword
};
confirmPasswordBox.PasswordChanged += (s, e) => vm.ConfirmPassword = confirmPasswordBox.Password;
dialog = new ContentDialog
{
XamlRoot = XamlRoot,
Title = vm.DialogTitle,
PrimaryButtonText = "Save",
CloseButtonText = "Cancel",
PrimaryButtonCommand = vm.SaveUserCommand,
CloseButtonCommand = vm.CloseDialogCommand,
DefaultButton = ContentDialogButton.Primary,
Content = new StackPanel
{
Spacing = 12,
MinWidth = 400,
Children =
{
emailBox,
userNameBox,
phoneBox,
passwordBox,
confirmPasswordBox
}
}
};
}
break;
case "Password":
{
var passwordBox = new PasswordBox
{
Header = "New Password",
Password = vm.Password
};
passwordBox.PasswordChanged += (s, e) => vm.Password = passwordBox.Password;
var confirmPasswordBox = new PasswordBox
{
Header = "Confirm Password",
Password = vm.ConfirmPassword
};
confirmPasswordBox.PasswordChanged += (s, e) => vm.ConfirmPassword = confirmPasswordBox.Password;
dialog = new ContentDialog
{
XamlRoot = XamlRoot,
Title = vm.DialogTitle,
PrimaryButtonText = "Change Password",
CloseButtonText = "Cancel",
PrimaryButtonCommand = vm.SavePasswordCommand,
CloseButtonCommand = vm.CloseDialogCommand,
DefaultButton = ContentDialogButton.Primary,
Content = new StackPanel
{
Spacing = 12,
MinWidth = 400,
Children =
{
passwordBox,
confirmPasswordBox
}
}
};
}
break;
case "Roles":
{
Debug.WriteLine($"Creating Roles dialog. Available roles count: {vm.AvailableRoles.Count}");
foreach(string role in vm.AvailableRoles) Debug.WriteLine($" - Role: {role}");
var rolesContent = new Grid
{
RowSpacing = 16,
MinWidth = 450
};
rolesContent.RowDefinitions.Add(new RowDefinition
{
Height = GridLength.Auto
});
rolesContent.RowDefinitions.Add(new RowDefinition
{
Height = new GridLength(1, GridUnitType.Star)
});
var addRolePanel = new StackPanel
{
Spacing = 8
};
addRolePanel.Children.Add(new TextBlock
{
Text = "Add Role",
FontWeight = FontWeights.SemiBold
});
var addRoleGrid = new Grid
{
ColumnSpacing = 8
};
addRoleGrid.ColumnDefinitions.Add(new ColumnDefinition
{
Width = new GridLength(1, GridUnitType.Star)
});
addRoleGrid.ColumnDefinitions.Add(new ColumnDefinition
{
Width = GridLength.Auto
});
// Use ListView with SingleSelection instead of ComboBox - ComboBox has issues with programmatic creation
var roleListView = new ListView
{
SelectionMode = ListViewSelectionMode.Single,
HorizontalAlignment = HorizontalAlignment.Stretch,
MaxHeight = 200,
MinWidth = 300
};
Debug.WriteLine($"Creating ListView for role selection with {vm.AvailableRoles.Count} roles");
// Populate the ListView
foreach(string role in vm.AvailableRoles)
{
var item = new ListViewItem
{
Content = role
};
roleListView.Items.Add(item);
Debug.WriteLine($" Added role to ListView: {role}");
}
Debug.WriteLine($"ListView Items.Count: {roleListView.Items.Count}");
roleListView.SelectionChanged += (s, e) =>
{
if(roleListView.SelectedItem is ListViewItem selectedItem &&
selectedItem.Content is string selectedRole)
{
vm.SelectedRole = selectedRole;
Debug.WriteLine($"Selected role from ListView: {selectedRole}");
}
};
Grid.SetColumn(roleListView, 0);
addRoleGrid.Children.Add(roleListView);
var addButton = new Button
{
Content = "Add",
Command = vm.AddRoleCommand
};
Grid.SetColumn(addButton, 1);
addRoleGrid.Children.Add(addButton);
addRolePanel.Children.Add(addRoleGrid);
Grid.SetRow(addRolePanel, 0);
rolesContent.Children.Add(addRolePanel);
var currentRolesPanel = new StackPanel
{
Spacing = 8,
Margin = new Thickness(0, 8, 0, 0)
};
currentRolesPanel.Children.Add(new TextBlock
{
Text = "Current Roles",
FontWeight = FontWeights.SemiBold
});
// Create a StackPanel to display roles dynamically
var rolesStack = new StackPanel
{
Spacing = 4
};
// Handler to refresh the roles list
void UpdateRolesList()
{
rolesStack.Children.Clear();
foreach(string role in vm.UserRoles)
{
var roleItem = new Grid
{
ColumnSpacing = 8,
Margin = new Thickness(0, 4, 0, 4)
};
roleItem.ColumnDefinitions.Add(new ColumnDefinition
{
Width = new GridLength(1, GridUnitType.Star)
});
roleItem.ColumnDefinitions.Add(new ColumnDefinition
{
Width = GridLength.Auto
});
var roleText = new TextBlock
{
Text = role,
VerticalAlignment = VerticalAlignment.Center
};
Grid.SetColumn(roleText, 0);
roleItem.Children.Add(roleText);
var removeButton = new Button
{
Content = "Remove",
Foreground = new SolidColorBrush(Colors.Red),
CommandParameter = role
};
removeButton.Click += (s, e) =>
{
var btn = s as Button;
var roleToRemove = btn?.CommandParameter as string;
if(roleToRemove != null && vm.RemoveRoleCommand.CanExecute(roleToRemove))
{
vm.RemoveRoleCommand.Execute(roleToRemove);
UpdateRolesList();
}
};
Grid.SetColumn(removeButton, 1);
roleItem.Children.Add(removeButton);
rolesStack.Children.Add(roleItem);
}
}
// Listen to collection changes
vm.UserRoles.CollectionChanged += (s, e) => UpdateRolesList();
// Initial population
UpdateRolesList();
var scrollViewer = new ScrollViewer
{
MaxHeight = 200,
Content = rolesStack
};
currentRolesPanel.Children.Add(scrollViewer);
Grid.SetRow(currentRolesPanel, 1);
rolesContent.Children.Add(currentRolesPanel);
dialog = new ContentDialog
{
XamlRoot = XamlRoot,
Title = vm.DialogTitle,
CloseButtonText = "Close",
CloseButtonCommand = vm.CloseDialogCommand,
DefaultButton = ContentDialogButton.Close,
Content = rolesContent
};
}
break;
}
if(dialog != null)
{
_currentOpenDialog = dialog;
await dialog.ShowAsync();
_currentOpenDialog = null;
}
}
}

View File

@@ -0,0 +1,123 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions;
using Uno.Extensions;
using Uno.Extensions.Authentication;
namespace Marechai.App.Services.Authentication;
public sealed class AuthService
(ApiClient client, ITokenService tokenService, IStringLocalizer stringLocalizer) : IAuthenticationService
{
/// <inheritdoc />
public async ValueTask<bool> LoginAsync(IDispatcher? dispatcher, IDictionary<string, string>? credentials = null,
string? provider = null, CancellationToken? cancellationToken = null)
{
if(credentials is null) return false;
string? email =
(credentials.FirstOrDefault(x => x.Key.Equals("Email", StringComparison.OrdinalIgnoreCase)).Value ??
credentials.FirstOrDefault(x => x.Key.Equals("email", StringComparison.OrdinalIgnoreCase)).Value ??
credentials.FirstOrDefault(x => x.Key.Equals("Username", StringComparison.OrdinalIgnoreCase)).Value)
?.Trim();
string? password =
(credentials.FirstOrDefault(x => x.Key.Equals("Password", StringComparison.OrdinalIgnoreCase)).Value ??
credentials.FirstOrDefault(x => x.Key.Equals("password", StringComparison.OrdinalIgnoreCase)).Value)
?.Trim();
if(string.IsNullOrWhiteSpace(email))
{
credentials["error"] = stringLocalizer["Auth.EmailIsRequired"];
return false;
}
if(string.IsNullOrWhiteSpace(password))
{
credentials["error"] = stringLocalizer["Auth.PasswordIsRequired"];
return false;
}
var loginModel = new AuthRequest
{
Email = email,
Password = password
};
AuthResponse? authResponse;
try
{
tokenService.RemoveToken();
authResponse = await client.Auth.Login.PostAsync(loginModel);
}
catch(ProblemDetails ex)
{
if(ex.Status == 400)
credentials["error"] = ex.Detail ?? ex.Title ?? stringLocalizer["Http.BadRequest"];
else if(ex.Status == 401)
credentials["error"] = stringLocalizer["Auth.InvalidCredentials"];
else
credentials["error"] = ex.Detail ?? ex.Title ?? stringLocalizer["Http.BadRequest"];
return false;
}
catch(ApiException ex)
{
if(ex.ResponseStatusCode == 401)
credentials["error"] = stringLocalizer["Auth.InvalidCredentials"];
else if(ex.ResponseStatusCode == 400)
credentials["error"] = stringLocalizer["Http.BadRequest"];
else
credentials["error"] = ex.Message ?? stringLocalizer["Http.BadRequest"];
return false;
}
catch(Exception ex)
{
#pragma warning disable EPC12
credentials["error"] = ex.Message;
#pragma warning restore EPC12
return false;
}
if(string.IsNullOrWhiteSpace(authResponse?.Token)) return false;
tokenService.SetToken(authResponse.Token);
return true;
}
/// <inheritdoc />
public ValueTask<bool> RefreshAsync(CancellationToken? cancellationToken = null) =>
IsAuthenticated(cancellationToken);
/// <inheritdoc />
public async ValueTask<bool> LogoutAsync(IDispatcher? dispatcher, CancellationToken? cancellationToken = null)
{
tokenService.RemoveToken();
LoggedOut?.Invoke(this, EventArgs.Empty);
return true;
}
/// <inheritdoc />
public async ValueTask<bool> IsAuthenticated(CancellationToken? cancellationToken = null)
{
string token = tokenService.GetToken();
// TODO: Check token validity
return !string.IsNullOrWhiteSpace(token);
}
/// <inheritdoc />
public string[] Providers { get; } = [];
/// <inheritdoc />
public event EventHandler? LoggedOut;
}

View File

@@ -0,0 +1,112 @@
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
namespace Marechai.App.Services.Authentication;
public interface IJwtService
{
IEnumerable<string> GetRoles(string token);
string? GetUserId(string token);
string? GetUserName(string token);
string? GetEmail(string token);
bool IsTokenValid(string token);
}
public sealed class JwtService : IJwtService
{
/// <inheritdoc />
public IEnumerable<string> GetRoles(string token)
{
if(string.IsNullOrWhiteSpace(token)) return [];
try
{
var handler = new JwtSecurityTokenHandler();
JwtSecurityToken jwtToken = handler.ReadJwtToken(token);
return jwtToken.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToList();
}
catch
{
return [];
}
}
/// <inheritdoc />
public string? GetUserId(string token)
{
if(string.IsNullOrWhiteSpace(token)) return null;
try
{
var handler = new JwtSecurityTokenHandler();
JwtSecurityToken jwtToken = handler.ReadJwtToken(token);
return jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Sid)?.Value;
}
catch
{
return null;
}
}
/// <inheritdoc />
public string? GetUserName(string token)
{
if(string.IsNullOrWhiteSpace(token)) return null;
try
{
var handler = new JwtSecurityTokenHandler();
JwtSecurityToken jwtToken = handler.ReadJwtToken(token);
return jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Name)?.Value;
}
catch
{
return null;
}
}
/// <inheritdoc />
public string? GetEmail(string token)
{
if(string.IsNullOrWhiteSpace(token)) return null;
try
{
var handler = new JwtSecurityTokenHandler();
JwtSecurityToken jwtToken = handler.ReadJwtToken(token);
return jwtToken.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Email)?.Value;
}
catch
{
return null;
}
}
/// <inheritdoc />
public bool IsTokenValid(string token)
{
if(string.IsNullOrWhiteSpace(token)) return false;
try
{
var handler = new JwtSecurityTokenHandler();
JwtSecurityToken jwtToken = handler.ReadJwtToken(token);
// Check if token has expired (if expiration is set)
if(jwtToken.ValidTo != DateTime.MinValue) return jwtToken.ValidTo > DateTime.UtcNow;
return true;
}
catch
{
return false;
}
}
}

View File

@@ -0,0 +1,32 @@
using Windows.Storage;
namespace Marechai.App.Services.Authentication;
public interface ITokenService
{
string GetToken();
void RemoveToken();
void SetToken(string token);
}
public sealed class TokenService : ITokenService
{
readonly ApplicationDataContainer _settings = ApplicationData.Current.LocalSettings;
/// <inheritdoc />
public string GetToken() => (string)_settings.Values["token"];
/// <inheritdoc />
public void RemoveToken()
{
_settings.Values.Remove("token");
}
/// <inheritdoc />
public void SetToken(string token)
{
_settings.Values["token"] = token;
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Windows.Storage;
using Microsoft.Extensions.Configuration;
namespace Marechai.App.Services.Caching;
public sealed class FlagCache
{
readonly IConfiguration _configuration;
StorageFolder _flagsFolder;
public FlagCache(IConfiguration configuration)
{
_configuration = configuration;
_ = EnsureFolderExistAsync();
}
async Task EnsureFolderExistAsync()
{
StorageFolder localFolder = ApplicationData.Current.LocalCacheFolder;
_flagsFolder = await localFolder.CreateFolderAsync("flags", CreationCollisionOption.OpenIfExists);
}
public async Task<Stream> GetFlagAsync(short countryCode)
{
var filename = $"{countryCode:D3}.svg";
Stream retStream;
if(await _flagsFolder.TryGetItemAsync(filename) is StorageFile file)
{
retStream = await file.OpenStreamForReadAsync();
return retStream;
}
await CacheFlagAsync(countryCode);
file = await _flagsFolder.GetFileAsync(filename);
retStream = await file.OpenStreamForReadAsync();
return retStream;
}
async Task CacheFlagAsync(short countryCode)
{
var filename = $"{countryCode:D3}.svg";
string baseUrl = _configuration.GetSection("ApiClient:Url").Value;
string flagUrl = baseUrl + $"/assets/flags/countries/{filename}";
using var httpClient = new HttpClient();
using HttpResponseMessage response = await httpClient.GetAsync(flagUrl);
response.EnsureSuccessStatusCode();
using Stream stream = await response.Content.ReadAsStreamAsync();
StorageFile file = await _flagsFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
using Stream fileStream = await file.OpenStreamForWriteAsync();
await stream.CopyToAsync(fileStream);
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Windows.Storage;
using Microsoft.Extensions.Configuration;
namespace Marechai.App.Services.Caching;
public sealed class CompanyLogoCache
{
readonly IConfiguration _configuration;
StorageFolder _logosFolder;
public CompanyLogoCache(IConfiguration configuration)
{
_configuration = configuration;
_ = EnsureFolderExistAsync();
}
async Task EnsureFolderExistAsync()
{
StorageFolder localFolder = ApplicationData.Current.LocalCacheFolder;
_logosFolder = await localFolder.CreateFolderAsync("logos", CreationCollisionOption.OpenIfExists);
}
public async Task<Stream> GetLogoAsync(Guid companyLogoId)
{
var filename = $"{companyLogoId}.svg";
Stream retStream;
if(await _logosFolder.TryGetItemAsync(filename) is StorageFile file)
{
retStream = await file.OpenStreamForReadAsync();
return retStream;
}
await CacheLogoAsync(companyLogoId);
file = await _logosFolder.GetFileAsync(filename);
retStream = await file.OpenStreamForReadAsync();
return retStream;
}
async Task CacheLogoAsync(Guid companyLogoId)
{
var filename = $"{companyLogoId}.svg";
string baseUrl = _configuration.GetSection("ApiClient:Url").Value;
string flagUrl = baseUrl + $"/assets/logos/{filename}";
using var httpClient = new HttpClient();
using HttpResponseMessage response = await httpClient.GetAsync(flagUrl);
response.EnsureSuccessStatusCode();
using Stream stream = await response.Content.ReadAsStreamAsync();
StorageFile file = await _logosFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
using Stream fileStream = await file.OpenStreamForWriteAsync();
await stream.CopyToAsync(fileStream);
}
}

View File

@@ -0,0 +1,107 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
using Windows.Storage;
using Microsoft.Extensions.Configuration;
namespace Marechai.App.Services.Caching;
public sealed class MachinePhotoCache
{
readonly IConfiguration _configuration;
StorageFolder _photosFolder;
StorageFolder _thumbnailsFolder;
public MachinePhotoCache(IConfiguration configuration)
{
_configuration = configuration;
_ = EnsureFolderExistAsync();
}
async Task EnsureFolderExistAsync()
{
StorageFolder localFolder = ApplicationData.Current.LocalCacheFolder;
_thumbnailsFolder =
await localFolder.CreateFolderAsync("machine_thumbnails", CreationCollisionOption.OpenIfExists);
_photosFolder = await localFolder.CreateFolderAsync("machine_photos", CreationCollisionOption.OpenIfExists);
}
public async Task<Stream> GetThumbnailAsync(Guid photoId)
{
var filename = $"{photoId}.webp";
Stream retStream;
if(await _thumbnailsFolder.TryGetItemAsync(filename) is StorageFile file)
{
retStream = await file.OpenStreamForReadAsync();
return retStream;
}
await CacheThumbnailAsync(photoId);
file = await _thumbnailsFolder.GetFileAsync(filename);
retStream = await file.OpenStreamForReadAsync();
return retStream;
}
public async Task<Stream> GetPhotoAsync(Guid photoId)
{
var filename = $"{photoId}.webp";
Stream retStream;
if(await _photosFolder.TryGetItemAsync(filename) is StorageFile file)
{
retStream = await file.OpenStreamForReadAsync();
return retStream;
}
await CachePhotoAsync(photoId);
file = await _photosFolder.GetFileAsync(filename);
retStream = await file.OpenStreamForReadAsync();
return retStream;
}
async Task CacheThumbnailAsync(Guid photoId)
{
var filename = $"{photoId}.webp";
string baseUrl = _configuration.GetSection("ApiClient:Url").Value;
string flagUrl = baseUrl + $"/assets/photos/machines/thumbs/webp/4k/{filename}";
using var httpClient = new HttpClient();
using HttpResponseMessage response = await httpClient.GetAsync(flagUrl);
response.EnsureSuccessStatusCode();
using Stream stream = await response.Content.ReadAsStreamAsync();
StorageFile file = await _thumbnailsFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
using Stream fileStream = await file.OpenStreamForWriteAsync();
await stream.CopyToAsync(fileStream);
}
async Task CachePhotoAsync(Guid photoId)
{
var filename = $"{photoId}.webp";
string baseUrl = _configuration.GetSection("ApiClient:Url").Value;
string flagUrl = baseUrl + $"/assets/photos/machines/webp/4k/{filename}";
using var httpClient = new HttpClient();
using HttpResponseMessage response = await httpClient.GetAsync(flagUrl);
response.EnsureSuccessStatusCode();
using Stream stream = await response.Content.ReadAsStreamAsync();
StorageFile file = await _photosFolder.CreateFileAsync(filename, CreationCollisionOption.ReplaceExisting);
using Stream fileStream = await file.OpenStreamForWriteAsync();
await stream.CopyToAsync(fileStream);
}
}

View File

@@ -0,0 +1,294 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Auth;
using Marechai.App.Books;
using Marechai.App.BrowserTests;
using Marechai.App.Companies;
using Marechai.App.Computers;
using Marechai.App.Consoles;
using Marechai.App.Countries;
using Marechai.App.Currencies;
using Marechai.App.Documents;
using Marechai.App.Dumps;
using Marechai.App.Gpus;
using Marechai.App.InstructionSetExtensions;
using Marechai.App.InstructionSetExtensionsByProcessor;
using Marechai.App.InstructionSets;
using Marechai.App.Iso31661Numeric;
using Marechai.App.Iso4217;
using Marechai.App.Licenses;
using Marechai.App.MachineFamilies;
using Marechai.App.Machines;
using Marechai.App.Magazines;
using Marechai.App.MagazinesByMachine;
using Marechai.App.MagazinesByMachineFamily;
using Marechai.App.Medias;
using Marechai.App.MemoriesByMachine;
using Marechai.App.News;
using Marechai.App.People;
using Marechai.App.PeopleByBook;
using Marechai.App.PeopleByDocument;
using Marechai.App.PeopleByMagazine;
using Marechai.App.Processor;
using Marechai.App.Processors;
using Marechai.App.ProcessorsByMachine;
using Marechai.App.Resolutions;
using Marechai.App.ResolutionsByGpu;
using Marechai.App.ResolutionsByScreen;
using Marechai.App.Screens;
using Marechai.App.ScreensByMachine;
using Marechai.App.Software;
using Marechai.App.SoundSynths;
using Marechai.App.SoundSynthsByMachine;
using Marechai.App.StorageByMachine;
using Marechai.App.Users;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions;
using Microsoft.Kiota.Serialization.Form;
using Microsoft.Kiota.Serialization.Json;
using Microsoft.Kiota.Serialization.Multipart;
using Microsoft.Kiota.Serialization.Text;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System;
namespace Marechai.App
{
/// <summary>
/// The main entry point of the SDK, exposes the configuration and the fluent API.
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class ApiClient : BaseRequestBuilder
{
/// <summary>The auth property</summary>
public global::Marechai.App.Auth.AuthRequestBuilder Auth
{
get => new global::Marechai.App.Auth.AuthRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The books property</summary>
public global::Marechai.App.Books.BooksRequestBuilder Books
{
get => new global::Marechai.App.Books.BooksRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The browserTests property</summary>
public global::Marechai.App.BrowserTests.BrowserTestsRequestBuilder BrowserTests
{
get => new global::Marechai.App.BrowserTests.BrowserTestsRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The companies property</summary>
public global::Marechai.App.Companies.CompaniesRequestBuilder Companies
{
get => new global::Marechai.App.Companies.CompaniesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The computers property</summary>
public global::Marechai.App.Computers.ComputersRequestBuilder Computers
{
get => new global::Marechai.App.Computers.ComputersRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The consoles property</summary>
public global::Marechai.App.Consoles.ConsolesRequestBuilder Consoles
{
get => new global::Marechai.App.Consoles.ConsolesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The countries property</summary>
public global::Marechai.App.Countries.CountriesRequestBuilder Countries
{
get => new global::Marechai.App.Countries.CountriesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The currencies property</summary>
public global::Marechai.App.Currencies.CurrenciesRequestBuilder Currencies
{
get => new global::Marechai.App.Currencies.CurrenciesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The documents property</summary>
public global::Marechai.App.Documents.DocumentsRequestBuilder Documents
{
get => new global::Marechai.App.Documents.DocumentsRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The dumps property</summary>
public global::Marechai.App.Dumps.DumpsRequestBuilder Dumps
{
get => new global::Marechai.App.Dumps.DumpsRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The gpus property</summary>
public global::Marechai.App.Gpus.GpusRequestBuilder Gpus
{
get => new global::Marechai.App.Gpus.GpusRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The instructionSetExtensions property</summary>
public global::Marechai.App.InstructionSetExtensions.InstructionSetExtensionsRequestBuilder InstructionSetExtensions
{
get => new global::Marechai.App.InstructionSetExtensions.InstructionSetExtensionsRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The instructionSetExtensionsByProcessor property</summary>
public global::Marechai.App.InstructionSetExtensionsByProcessor.InstructionSetExtensionsByProcessorRequestBuilder InstructionSetExtensionsByProcessor
{
get => new global::Marechai.App.InstructionSetExtensionsByProcessor.InstructionSetExtensionsByProcessorRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The instructionSets property</summary>
public global::Marechai.App.InstructionSets.InstructionSetsRequestBuilder InstructionSets
{
get => new global::Marechai.App.InstructionSets.InstructionSetsRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The iso31661Numeric property</summary>
public global::Marechai.App.Iso31661Numeric.Iso31661NumericRequestBuilder Iso31661Numeric
{
get => new global::Marechai.App.Iso31661Numeric.Iso31661NumericRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The iso4217 property</summary>
public global::Marechai.App.Iso4217.Iso4217RequestBuilder Iso4217
{
get => new global::Marechai.App.Iso4217.Iso4217RequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The licenses property</summary>
public global::Marechai.App.Licenses.LicensesRequestBuilder Licenses
{
get => new global::Marechai.App.Licenses.LicensesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The machineFamilies property</summary>
public global::Marechai.App.MachineFamilies.MachineFamiliesRequestBuilder MachineFamilies
{
get => new global::Marechai.App.MachineFamilies.MachineFamiliesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The machines property</summary>
public global::Marechai.App.Machines.MachinesRequestBuilder Machines
{
get => new global::Marechai.App.Machines.MachinesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The magazines property</summary>
public global::Marechai.App.Magazines.MagazinesRequestBuilder Magazines
{
get => new global::Marechai.App.Magazines.MagazinesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The magazinesByMachine property</summary>
public global::Marechai.App.MagazinesByMachine.MagazinesByMachineRequestBuilder MagazinesByMachine
{
get => new global::Marechai.App.MagazinesByMachine.MagazinesByMachineRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The magazinesByMachineFamily property</summary>
public global::Marechai.App.MagazinesByMachineFamily.MagazinesByMachineFamilyRequestBuilder MagazinesByMachineFamily
{
get => new global::Marechai.App.MagazinesByMachineFamily.MagazinesByMachineFamilyRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The medias property</summary>
public global::Marechai.App.Medias.MediasRequestBuilder Medias
{
get => new global::Marechai.App.Medias.MediasRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The memoriesByMachine property</summary>
public global::Marechai.App.MemoriesByMachine.MemoriesByMachineRequestBuilder MemoriesByMachine
{
get => new global::Marechai.App.MemoriesByMachine.MemoriesByMachineRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The news property</summary>
public global::Marechai.App.News.NewsRequestBuilder News
{
get => new global::Marechai.App.News.NewsRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The people property</summary>
public global::Marechai.App.People.PeopleRequestBuilder People
{
get => new global::Marechai.App.People.PeopleRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The peopleByBook property</summary>
public global::Marechai.App.PeopleByBook.PeopleByBookRequestBuilder PeopleByBook
{
get => new global::Marechai.App.PeopleByBook.PeopleByBookRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The peopleByDocument property</summary>
public global::Marechai.App.PeopleByDocument.PeopleByDocumentRequestBuilder PeopleByDocument
{
get => new global::Marechai.App.PeopleByDocument.PeopleByDocumentRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The peopleByMagazine property</summary>
public global::Marechai.App.PeopleByMagazine.PeopleByMagazineRequestBuilder PeopleByMagazine
{
get => new global::Marechai.App.PeopleByMagazine.PeopleByMagazineRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The processor property</summary>
public global::Marechai.App.Processor.ProcessorRequestBuilder Processor
{
get => new global::Marechai.App.Processor.ProcessorRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The processors property</summary>
public global::Marechai.App.Processors.ProcessorsRequestBuilder Processors
{
get => new global::Marechai.App.Processors.ProcessorsRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The processorsByMachine property</summary>
public global::Marechai.App.ProcessorsByMachine.ProcessorsByMachineRequestBuilder ProcessorsByMachine
{
get => new global::Marechai.App.ProcessorsByMachine.ProcessorsByMachineRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The resolutions property</summary>
public global::Marechai.App.Resolutions.ResolutionsRequestBuilder Resolutions
{
get => new global::Marechai.App.Resolutions.ResolutionsRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The resolutionsByGpu property</summary>
public global::Marechai.App.ResolutionsByGpu.ResolutionsByGpuRequestBuilder ResolutionsByGpu
{
get => new global::Marechai.App.ResolutionsByGpu.ResolutionsByGpuRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The resolutionsByScreen property</summary>
public global::Marechai.App.ResolutionsByScreen.ResolutionsByScreenRequestBuilder ResolutionsByScreen
{
get => new global::Marechai.App.ResolutionsByScreen.ResolutionsByScreenRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The screens property</summary>
public global::Marechai.App.Screens.ScreensRequestBuilder Screens
{
get => new global::Marechai.App.Screens.ScreensRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The screensByMachine property</summary>
public global::Marechai.App.ScreensByMachine.ScreensByMachineRequestBuilder ScreensByMachine
{
get => new global::Marechai.App.ScreensByMachine.ScreensByMachineRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The software property</summary>
public global::Marechai.App.Software.SoftwareRequestBuilder Software
{
get => new global::Marechai.App.Software.SoftwareRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The soundSynths property</summary>
public global::Marechai.App.SoundSynths.SoundSynthsRequestBuilder SoundSynths
{
get => new global::Marechai.App.SoundSynths.SoundSynthsRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The soundSynthsByMachine property</summary>
public global::Marechai.App.SoundSynthsByMachine.SoundSynthsByMachineRequestBuilder SoundSynthsByMachine
{
get => new global::Marechai.App.SoundSynthsByMachine.SoundSynthsByMachineRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The storageByMachine property</summary>
public global::Marechai.App.StorageByMachine.StorageByMachineRequestBuilder StorageByMachine
{
get => new global::Marechai.App.StorageByMachine.StorageByMachineRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The users property</summary>
public global::Marechai.App.Users.UsersRequestBuilder Users
{
get => new global::Marechai.App.Users.UsersRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.ApiClient"/> and sets the default values.
/// </summary>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public ApiClient(IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}", new Dictionary<string, object>())
{
ApiClientBuilder.RegisterDefaultSerializer<JsonSerializationWriterFactory>();
ApiClientBuilder.RegisterDefaultSerializer<TextSerializationWriterFactory>();
ApiClientBuilder.RegisterDefaultSerializer<FormSerializationWriterFactory>();
ApiClientBuilder.RegisterDefaultSerializer<MultipartSerializationWriterFactory>();
ApiClientBuilder.RegisterDefaultDeserializer<JsonParseNodeFactory>();
ApiClientBuilder.RegisterDefaultDeserializer<TextParseNodeFactory>();
ApiClientBuilder.RegisterDefaultDeserializer<FormParseNodeFactory>();
if (string.IsNullOrEmpty(RequestAdapter.BaseUrl))
{
RequestAdapter.BaseUrl = "http://localhost:5023";
}
PathParameters.TryAdd("baseurl", RequestAdapter.BaseUrl);
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,41 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Auth.Login;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System;
namespace Marechai.App.Auth
{
/// <summary>
/// Builds and executes requests for operations under \auth
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class AuthRequestBuilder : BaseRequestBuilder
{
/// <summary>The login property</summary>
public global::Marechai.App.Auth.Login.LoginRequestBuilder Login
{
get => new global::Marechai.App.Auth.Login.LoginRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Auth.AuthRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public AuthRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/auth", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Auth.AuthRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public AuthRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/auth", rawUrl)
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,98 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.App.Auth.Login
{
/// <summary>
/// Builds and executes requests for operations under \auth\login
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class LoginRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Auth.Login.LoginRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public LoginRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/auth/login", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Auth.Login.LoginRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public LoginRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/auth/login", rawUrl)
{
}
/// <returns>A <see cref="global::Marechai.App.Models.AuthResponse"/></returns>
/// <param name="body">The request body</param>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 401 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<global::Marechai.App.Models.AuthResponse?> PostAsync(global::Marechai.App.Models.AuthRequest body, Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<global::Marechai.App.Models.AuthResponse> PostAsync(global::Marechai.App.Models.AuthRequest body, Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body));
var requestInfo = ToPostRequestInformation(body, requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
{ "401", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendAsync<global::Marechai.App.Models.AuthResponse>(requestInfo, global::Marechai.App.Models.AuthResponse.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="body">The request body</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToPostRequestInformation(global::Marechai.App.Models.AuthRequest body, Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToPostRequestInformation(global::Marechai.App.Models.AuthRequest body, Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body));
var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "application/json");
requestInfo.SetContentFromParsable(RequestAdapter, "application/json", body);
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.App.Auth.Login.LoginRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.App.Auth.Login.LoginRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.App.Auth.Login.LoginRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class LoginRequestBuilderPostRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,181 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Books.Companies;
using Marechai.App.Books.Item;
using Marechai.App.Books.Scans;
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.App.Books
{
/// <summary>
/// Builds and executes requests for operations under \books
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class BooksRequestBuilder : BaseRequestBuilder
{
/// <summary>The companies property</summary>
public global::Marechai.App.Books.Companies.CompaniesRequestBuilder Companies
{
get => new global::Marechai.App.Books.Companies.CompaniesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The scans property</summary>
public global::Marechai.App.Books.Scans.ScansRequestBuilder Scans
{
get => new global::Marechai.App.Books.Scans.ScansRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>Gets an item from the Marechai.App.books.item collection</summary>
/// <param name="position">Unique identifier of the item</param>
/// <returns>A <see cref="global::Marechai.App.Books.Item.BookItemRequestBuilder"/></returns>
public global::Marechai.App.Books.Item.BookItemRequestBuilder this[long position]
{
get
{
var urlTplParams = new Dictionary<string, object>(PathParameters);
urlTplParams.Add("book%2Did", position);
return new global::Marechai.App.Books.Item.BookItemRequestBuilder(urlTplParams, RequestAdapter);
}
}
/// <summary>Gets an item from the Marechai.App.books.item collection</summary>
/// <param name="position">Unique identifier of the item</param>
/// <returns>A <see cref="global::Marechai.App.Books.Item.BookItemRequestBuilder"/></returns>
[Obsolete("This indexer is deprecated and will be removed in the next major version. Use the one with the typed parameter instead.")]
public global::Marechai.App.Books.Item.BookItemRequestBuilder this[string position]
{
get
{
var urlTplParams = new Dictionary<string, object>(PathParameters);
if (!string.IsNullOrWhiteSpace(position)) urlTplParams.Add("book%2Did", position);
return new global::Marechai.App.Books.Item.BookItemRequestBuilder(urlTplParams, RequestAdapter);
}
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.BooksRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public BooksRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.BooksRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public BooksRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books", rawUrl)
{
}
/// <returns>A List&lt;global::Marechai.App.Models.BookDto&gt;</returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<List<global::Marechai.App.Models.BookDto>?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<List<global::Marechai.App.Models.BookDto>> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
var collectionResult = await RequestAdapter.SendCollectionAsync<global::Marechai.App.Models.BookDto>(requestInfo, global::Marechai.App.Models.BookDto.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
return collectionResult?.AsList();
}
/// <returns>A <see cref="long"/></returns>
/// <param name="body">The request body</param>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 401 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<long?> PostAsync(global::Marechai.App.Models.BookDto body, Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<long?> PostAsync(global::Marechai.App.Models.BookDto body, Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body));
var requestInfo = ToPostRequestInformation(body, requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
{ "401", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendPrimitiveAsync<long?>(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false);
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
return requestInfo;
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="body">The request body</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToPostRequestInformation(global::Marechai.App.Models.BookDto body, Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToPostRequestInformation(global::Marechai.App.Models.BookDto body, Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body));
var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
requestInfo.SetContentFromParsable(RequestAdapter, "application/json", body);
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.App.Books.BooksRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.App.Books.BooksRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.App.Books.BooksRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class BooksRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class BooksRequestBuilderPostRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,124 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Books.Companies.Item;
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.App.Books.Companies
{
/// <summary>
/// Builds and executes requests for operations under \books\companies
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CompaniesRequestBuilder : BaseRequestBuilder
{
/// <summary>Gets an item from the Marechai.App.books.companies.item collection</summary>
/// <param name="position">Unique identifier of the item</param>
/// <returns>A <see cref="global::Marechai.App.Books.Companies.Item.CompaniesItemRequestBuilder"/></returns>
public global::Marechai.App.Books.Companies.Item.CompaniesItemRequestBuilder this[long position]
{
get
{
var urlTplParams = new Dictionary<string, object>(PathParameters);
urlTplParams.Add("id", position);
return new global::Marechai.App.Books.Companies.Item.CompaniesItemRequestBuilder(urlTplParams, RequestAdapter);
}
}
/// <summary>Gets an item from the Marechai.App.books.companies.item collection</summary>
/// <param name="position">Unique identifier of the item</param>
/// <returns>A <see cref="global::Marechai.App.Books.Companies.Item.CompaniesItemRequestBuilder"/></returns>
[Obsolete("This indexer is deprecated and will be removed in the next major version. Use the one with the typed parameter instead.")]
public global::Marechai.App.Books.Companies.Item.CompaniesItemRequestBuilder this[string position]
{
get
{
var urlTplParams = new Dictionary<string, object>(PathParameters);
if (!string.IsNullOrWhiteSpace(position)) urlTplParams.Add("id", position);
return new global::Marechai.App.Books.Companies.Item.CompaniesItemRequestBuilder(urlTplParams, RequestAdapter);
}
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.Companies.CompaniesRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CompaniesRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/companies", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.Companies.CompaniesRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CompaniesRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/companies", rawUrl)
{
}
/// <returns>A <see cref="long"/></returns>
/// <param name="body">The request body</param>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 401 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<long?> PostAsync(global::Marechai.App.Models.CompanyByBookDto body, Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<long?> PostAsync(global::Marechai.App.Models.CompanyByBookDto body, Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body));
var requestInfo = ToPostRequestInformation(body, requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
{ "401", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendPrimitiveAsync<long?>(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false);
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="body">The request body</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToPostRequestInformation(global::Marechai.App.Models.CompanyByBookDto body, Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToPostRequestInformation(global::Marechai.App.Models.CompanyByBookDto body, Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body));
var requestInfo = new RequestInformation(Method.POST, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
requestInfo.SetContentFromParsable(RequestAdapter, "application/json", body);
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.App.Books.Companies.CompaniesRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.App.Books.Companies.CompaniesRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.App.Books.Companies.CompaniesRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CompaniesRequestBuilderPostRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,95 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.App.Books.Companies.Item
{
/// <summary>
/// Builds and executes requests for operations under \books\companies\{id}
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CompaniesItemRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.Companies.Item.CompaniesItemRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CompaniesItemRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/companies/{id}", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.Companies.Item.CompaniesItemRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CompaniesItemRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/companies/{id}", rawUrl)
{
}
/// <returns>A <see cref="string"/></returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 401 status code</exception>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 404 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<string?> DeleteAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<string> DeleteAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToDeleteRequestInformation(requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
{ "401", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
{ "404", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendPrimitiveAsync<string>(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false);
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToDeleteRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToDeleteRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.DELETE, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "application/json, text/plain;q=0.9");
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.App.Books.Companies.Item.CompaniesItemRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.App.Books.Companies.Item.CompaniesItemRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.App.Books.Companies.Item.CompaniesItemRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CompaniesItemRequestBuilderDeleteRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,228 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Books.Item.Companies;
using Marechai.App.Books.Item.MachineFamilies;
using Marechai.App.Books.Item.Machines;
using Marechai.App.Books.Item.People;
using Marechai.App.Books.Item.Scans;
using Marechai.App.Books.Item.Synopsis;
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.App.Books.Item
{
/// <summary>
/// Builds and executes requests for operations under \books\{book-id}
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class BookItemRequestBuilder : BaseRequestBuilder
{
/// <summary>The companies property</summary>
public global::Marechai.App.Books.Item.Companies.CompaniesRequestBuilder Companies
{
get => new global::Marechai.App.Books.Item.Companies.CompaniesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The machineFamilies property</summary>
public global::Marechai.App.Books.Item.MachineFamilies.MachineFamiliesRequestBuilder MachineFamilies
{
get => new global::Marechai.App.Books.Item.MachineFamilies.MachineFamiliesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The machines property</summary>
public global::Marechai.App.Books.Item.Machines.MachinesRequestBuilder Machines
{
get => new global::Marechai.App.Books.Item.Machines.MachinesRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The people property</summary>
public global::Marechai.App.Books.Item.People.PeopleRequestBuilder People
{
get => new global::Marechai.App.Books.Item.People.PeopleRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The scans property</summary>
public global::Marechai.App.Books.Item.Scans.ScansRequestBuilder Scans
{
get => new global::Marechai.App.Books.Item.Scans.ScansRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>The synopsis property</summary>
public global::Marechai.App.Books.Item.Synopsis.SynopsisRequestBuilder Synopsis
{
get => new global::Marechai.App.Books.Item.Synopsis.SynopsisRequestBuilder(PathParameters, RequestAdapter);
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.Item.BookItemRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public BookItemRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/{book%2Did}", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.Item.BookItemRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public BookItemRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/{book%2Did}", rawUrl)
{
}
/// <returns>A <see cref="string"/></returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 401 status code</exception>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 404 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<string?> DeleteAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<string> DeleteAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToDeleteRequestInformation(requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
{ "401", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
{ "404", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendPrimitiveAsync<string>(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false);
}
/// <returns>A <see cref="global::Marechai.App.Models.BookDto"/></returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<global::Marechai.App.Models.BookDto?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<global::Marechai.App.Models.BookDto> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendAsync<global::Marechai.App.Models.BookDto>(requestInfo, global::Marechai.App.Models.BookDto.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
}
/// <returns>A <see cref="string"/></returns>
/// <param name="body">The request body</param>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 401 status code</exception>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 404 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<string?> PutAsync(global::Marechai.App.Models.BookDto body, Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<string> PutAsync(global::Marechai.App.Models.BookDto body, Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body));
var requestInfo = ToPutRequestInformation(body, requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
{ "401", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
{ "404", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
return await RequestAdapter.SendPrimitiveAsync<string>(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false);
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToDeleteRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToDeleteRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.DELETE, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "application/json, text/plain;q=0.9");
return requestInfo;
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "application/json, text/plain;q=0.9");
return requestInfo;
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="body">The request body</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToPutRequestInformation(global::Marechai.App.Models.BookDto body, Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToPutRequestInformation(global::Marechai.App.Models.BookDto body, Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
if(ReferenceEquals(body, null)) throw new ArgumentNullException(nameof(body));
var requestInfo = new RequestInformation(Method.PUT, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "application/json, text/plain;q=0.9");
requestInfo.SetContentFromParsable(RequestAdapter, "application/json", body);
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.App.Books.Item.BookItemRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.App.Books.Item.BookItemRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.App.Books.Item.BookItemRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class BookItemRequestBuilderDeleteRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class BookItemRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class BookItemRequestBuilderPutRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,92 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.App.Books.Item.Companies
{
/// <summary>
/// Builds and executes requests for operations under \books\{book-id}\companies
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CompaniesRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.Item.Companies.CompaniesRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CompaniesRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/{book%2Did}/companies", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.Item.Companies.CompaniesRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public CompaniesRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/{book%2Did}/companies", rawUrl)
{
}
/// <returns>A List&lt;global::Marechai.App.Models.CompanyByBookDto&gt;</returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<List<global::Marechai.App.Models.CompanyByBookDto>?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<List<global::Marechai.App.Models.CompanyByBookDto>> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
var collectionResult = await RequestAdapter.SendCollectionAsync<global::Marechai.App.Models.CompanyByBookDto>(requestInfo, global::Marechai.App.Models.CompanyByBookDto.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
return collectionResult?.AsList();
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.App.Books.Item.Companies.CompaniesRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.App.Books.Item.Companies.CompaniesRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.App.Books.Item.Companies.CompaniesRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class CompaniesRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

View File

@@ -0,0 +1,92 @@
// <auto-generated/>
#pragma warning disable CS0618
using Marechai.App.Models;
using Microsoft.Kiota.Abstractions.Extensions;
using Microsoft.Kiota.Abstractions.Serialization;
using Microsoft.Kiota.Abstractions;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
using System.Threading;
using System;
namespace Marechai.App.Books.Item.MachineFamilies
{
/// <summary>
/// Builds and executes requests for operations under \books\{book-id}\machine-families
/// </summary>
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class MachineFamiliesRequestBuilder : BaseRequestBuilder
{
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.Item.MachineFamilies.MachineFamiliesRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="pathParameters">Path parameters for the request</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public MachineFamiliesRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/{book%2Did}/machine-families", pathParameters)
{
}
/// <summary>
/// Instantiates a new <see cref="global::Marechai.App.Books.Item.MachineFamilies.MachineFamiliesRequestBuilder"/> and sets the default values.
/// </summary>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
public MachineFamiliesRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/books/{book%2Did}/machine-families", rawUrl)
{
}
/// <returns>A List&lt;global::Marechai.App.Models.BookByMachineFamilyDto&gt;</returns>
/// <param name="cancellationToken">Cancellation token to use when cancelling requests</param>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
/// <exception cref="global::Marechai.App.Models.ProblemDetails">When receiving a 400 status code</exception>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public async Task<List<global::Marechai.App.Models.BookByMachineFamilyDto>?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
{
#nullable restore
#else
public async Task<List<global::Marechai.App.Models.BookByMachineFamilyDto>> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
{
#endif
var requestInfo = ToGetRequestInformation(requestConfiguration);
var errorMapping = new Dictionary<string, ParsableFactory<IParsable>>
{
{ "400", global::Marechai.App.Models.ProblemDetails.CreateFromDiscriminatorValue },
};
var collectionResult = await RequestAdapter.SendCollectionAsync<global::Marechai.App.Models.BookByMachineFamilyDto>(requestInfo, global::Marechai.App.Models.BookByMachineFamilyDto.CreateFromDiscriminatorValue, errorMapping, cancellationToken).ConfigureAwait(false);
return collectionResult?.AsList();
}
/// <returns>A <see cref="RequestInformation"/></returns>
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
#nullable enable
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
{
#nullable restore
#else
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
{
#endif
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
requestInfo.Configure(requestConfiguration);
requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9");
return requestInfo;
}
/// <summary>
/// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
/// </summary>
/// <returns>A <see cref="global::Marechai.App.Books.Item.MachineFamilies.MachineFamiliesRequestBuilder"/></returns>
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
public global::Marechai.App.Books.Item.MachineFamilies.MachineFamiliesRequestBuilder WithUrl(string rawUrl)
{
return new global::Marechai.App.Books.Item.MachineFamilies.MachineFamiliesRequestBuilder(rawUrl, RequestAdapter);
}
/// <summary>
/// Configuration for the request such as headers, query parameters, and middleware options.
/// </summary>
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
public partial class MachineFamiliesRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
{
}
}
}
#pragma warning restore CS0618

Some files were not shown because too many files have changed in this diff Show More