130 Commits

Author SHA1 Message Date
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
9847c987b5 Update copyright year. 2025-11-14 05:08:14 +00:00
6f0de86be4 Add custom route constraints for char and ulong parameters 2025-11-14 05:04:03 +00:00
37d8df4b7d Add properties to enable compiler-generated files output in project configuration 2025-11-14 05:03:51 +00:00
860fd99b00 Fix route parameter in ResolutionsByGpuController for resolutionId 2025-11-14 05:02:44 +00:00
2ba7d86e71 Refactor CompaniesController to use DTOs for machine and company operations 2025-11-14 05:02:01 +00:00
37403e5e53 Add Directory.Build.targets file for project configuration 2025-11-14 04:54:07 +00:00
2066e211e1 Refactor SoftwareVariantDto properties for improved readability 2025-11-14 04:51:39 +00:00
ebc611e5d2 Refactor MachineDto properties for improved readability and add JsonIgnore attributes for collections 2025-11-14 04:51:24 +00:00
5e3be9cbb0 Add LicenseDto and update LicensesController to use DTO for license operations 2025-11-14 04:51:12 +00:00
9d146eb151 Add Iso31661NumericDto and update Iso31661NumericController to use DTO for numeric operations 2025-11-14 04:50:54 +00:00
52b6145a8b Add Iso4217Dto and update Iso4217Controller to use DTO for currency operations 2025-11-14 04:50:39 +00:00
09ced17903 Add InstructionSetExtensionDto and update InstructionSetExtensionsController to use DTO for extension operations 2025-11-14 04:50:28 +00:00
356674f51f Add InstructionSetDto and update InstructionSetsController to use DTO for instruction set operations 2025-11-14 04:50:12 +00:00
cd4aa5767d Add CompanyLogoDto and update CompanyLogosController to use DTO for logo retrieval and creation 2025-11-14 04:49:39 +00:00
cf24356030 Add initial project structure and configuration files for Marechai.App 2025-11-14 01:50:56 +00:00
f85dc22bf6 Update target framework to net10.0 and adjust project properties 2025-11-14 01:50:32 +00:00
795f5ba27d Migrate solution file to XML format. 2025-11-14 01:50:03 +00:00
059417f0dc Update default values in MarechaiContext to use explicit boolean and enum types 2025-11-14 00:24:35 +00:00
5b4a1b42e0 Add migration to update annotations. 2025-11-14 00:17:47 +00:00
e9a2a68e49 Refactor MachinesController to improve route clarity by adding route parameters and updating service dependencies 2025-11-13 22:20:03 +00:00
7c29302153 Refactor controllers to improve route clarity and consistency by adding route parameters and updating method signatures 2025-11-13 22:17:59 +00:00
76bebc68b7 Refactor controllers to use route parameters for UpdateAsync methods and improve consistency in model retrieval 2025-11-13 21:31:49 +00:00
4a2d46f3b0 Refactor controllers to use [FromBody] attribute for DTO parameters in UpdateAsync methods 2025-11-13 21:24:43 +00:00
e9da9c7a3f Refactor controllers to use [FromBody] attribute for DTO parameters and update return types for consistency 2025-11-13 21:22:49 +00:00
b81c628f07 Refactor CurrencyPeggingController routes and method signatures for improved clarity and consistency 2025-11-13 21:04:43 +00:00
6fc709a271 Refactor CurrencyInflationController routes and method signatures for improved clarity and consistency 2025-11-13 21:03:14 +00:00
507e5686e4 Refactor ConsolesController routes and method signatures for improved clarity and consistency 2025-11-13 21:01:22 +00:00
27e5616da8 Refactor ComputersController routes and method signatures for improved clarity and consistency 2025-11-13 21:00:36 +00:00
464f52878b Refactor CompanyLogosController routes and method signatures for improved clarity and consistency 2025-11-13 20:57:06 +00:00
f6214e6d14 Refactor CompaniesController routes and method signatures for improved clarity and consistency 2025-11-13 20:51:05 +00:00
baaf571505 Refactor CompaniesBySoftwareVersionController routes and method signatures for improved clarity and consistency 2025-11-13 20:46:34 +00:00
8d6c382754 Refactor CompaniesBySoftwareVariantController routes and method signatures for improved clarity and consistency 2025-11-13 20:44:57 +00:00
e171b8ddd8 Refactor CompaniesBySoftwareFamilyController routes and method signatures for improved clarity and consistency 2025-11-13 20:38:59 +00:00
6981075c4a Refactor CompaniesByMagazineController routes and method signatures for improved clarity and consistency 2025-11-13 20:37:16 +00:00
bed72102e8 Refactor CompaniesByDocumentController routes and method signatures for improved clarity and consistency 2025-11-13 20:35:47 +00:00
d301315fbf Refactor CompaniesByBookController routes and method signatures for improved clarity and consistency 2025-11-13 20:33:41 +00:00
79f0d2632b Refactor BooksController routes and method signatures for improved clarity and consistency 2025-11-13 20:22:33 +00:00
0ba1a24b4e Refactor BookScansController routes and method signatures for improved clarity and consistency 2025-11-13 20:19:02 +00:00
583f20ff99 Add 401 response type to Delete and Create actions in multiple controllers for improved security handling 2025-11-13 19:28:21 +00:00
1826c70883 Add 404 response type to Delete and Update actions in multiple controllers for improved error handling 2025-11-13 19:10:08 +00:00
34d76fd646 Refactor BookScansController and Iso31661NumericController methods to use synchronous Task return types for improved readability 2025-11-13 19:00:38 +00:00
fc6238aef1 Refactor controller methods to return ActionResult for better error handling 2025-11-13 18:52:45 +00:00
505ace535f Remove unnecessary localization dependencies from CompaniesController and NewsController constructors 2025-11-13 18:28:14 +00:00
a715d936eb Refactor controller methods to use synchronous Task return types for improved readability 2025-11-13 18:27:00 +00:00
e4c2837ad9 Automatically convert services into controllers. 2025-11-13 18:22:44 +00:00
36520596da Add BooksByMachineController with CRUD operations for managing book-machine associations 2025-11-13 17:34:47 +00:00
764e058f79 Add BooksByMachineFamilyController with CRUD operations 2025-11-13 17:29:30 +00:00
349b396588 Add authentication controller. 2025-11-13 17:04:00 +00:00
d0e5725ae0 Update DTO properties to support nullable types and add required validation attributes 2025-11-13 16:46:56 +00:00
0bbf821489 Add JsonPropertyName attributes to DTO properties for serialization 2025-11-13 16:19:14 +00:00
10017850f8 Refactor viewmodels to use DTOs instead of viewmodels 2025-11-13 15:21:11 +00:00
d9239f39c0 Refactor viewmodels to use DTOs instead of viewmodels 2025-11-13 15:21:10 +00:00
f445006e46 Refactor viewmodels to use new namespace structure 2025-11-13 15:16:41 +00:00
3520e49b25 Move viewmodels to different folders.
Yes, separating big renames into smaller commits.
2025-11-13 15:08:01 +00:00
268c8fab07 Move viewmodels to separate library. 2025-11-13 15:07:20 +00:00
e9e3ef2ab0 Move viewmodels to separate library. 2025-11-13 15:07:11 +00:00
6962be93a4 Add token authentication. 2025-11-13 14:36:14 +00:00
be83594ea9 Configure controllers. 2025-11-13 14:27:00 +00:00
b1f32e6f13 Moved startup code to server. 2025-11-13 05:16:58 +00:00
c73c4d839a Moved startup code to server. 2025-11-13 05:16:49 +00:00
d7c61b2fdd Copy appsettings to server. 2025-11-13 04:56:47 +00:00
a942d40849 Add server skeleton. 2025-11-13 04:55:13 +00:00
6c01b2128f Update Aaru.CommonTypes to 5.4.1. 2025-11-13 04:26:47 +00:00
e14bfba354 Move to centralized package management. 2025-11-13 04:25:11 +00:00
d2c71d350d Add Directory.Build.props file. 2025-11-13 04:15:47 +00:00
eb7c0a6858 Remove copilot upgrade assessment file. 2025-11-13 04:07:05 +00:00
8f6d334af4 Major refactor and cleanup. 2025-11-13 04:05:35 +00:00
1d67081792 fix: Update CurrenciesPegging and CurrenciesInflation columns to NOT NULL with foreign key constraints 2025-11-13 03:10:14 +00:00
f304448fdb fix: Ensure correct charset and collation for foreign key compatibility in BookScans migration 2025-11-13 03:10:02 +00:00
88307edc9a fix: Make migration for IssueNumber column idempotent in MagazineIssues table 2025-11-13 03:09:48 +00:00
51d0809536 fix: Update foreign key constraints handling in migration for CurrenciesPegging and CurrenciesInflation 2025-11-13 03:09:36 +00:00
8e07b6587b Update Pomelo.EntityFrameworkCore.MySql to version 9.0.0 2025-11-13 03:08:48 +00:00
4f2435fcbd Update connection string. 2025-11-13 03:08:14 +00:00
3fac917422 docs: Update migration progress - all tasks completed successfully 2025-11-13 02:02:50 +00:00
096865dc3b chore: Complete .NET modernization - upgrade to .NET 9, update dependencies, fix security vulnerabilities, and implement local credential encryption
Changes:
- Upgrade both projects from .NET 5.0 to .NET 9.0
- Update Entity Framework Core packages to 9.0.11
- Update SkiaSharp to 3.119.1 (fixes CVE security vulnerability)
- Remove deprecated Microsoft.ApplicationInsights.AspNetCore
- Implement local credential encryption using Data Protection API
- Add CredentialEncryptor helper for DPAPI integration
- Add ConnectionStringManager for secure connection string handling
- Update Startup.cs to register credential encryption services
- Remove Application Insights configuration from _Host.cshtml

All changes maintain backward compatibility with existing plaintext credentials
while providing optional encryption for production deployments.
2025-11-13 02:02:14 +00:00
29ec7571fe Pre-upgrade checkpoint: Save current state before .NET 5 to .NET 8 migration 2025-11-13 01:40:51 +00:00
fc893ee08b Added more items to ignore 2025-11-13 01:36:52 +00:00
cf7f830aad Updated .editorconfig. 2025-11-13 01:22:01 +00:00
8213e4ad80 Updated Rider project files. 2025-11-13 01:21:28 +00:00
e974a77e46 Add security headers middleware. 2025-11-13 01:20:40 +00:00
1454 changed files with 770999 additions and 76473 deletions

View File

@@ -0,0 +1,39 @@
/*
You can edit this file to configure the application assessment. Please note that any changes saved to this file will be applied the next time you run the assessment.
The configurable AppCAT arguments
- target: target Azure compute service to run the apps on. Choosing "Any" if you haven't decided which one to use and later you can choose and compare on the assessment report.
| Target | Description |
|-----------------------------------|--------------------------------------------------------------------|
| Any | Discover issues for all supported targets here. |
| AKS.Windows | Best practices for Azure Kubernetes Service (Windows). |
| AKS.Linux | Best practices for Azure Kubernetes Service (Linux). |
| AppService.Windows | Best practices for Azure App Service (Windows). |
| AppService.Linux | Best practices for Azure App Service (Linux). |
| AppServiceContainer.Windows | Best practices for Azure App Service Container (Windows). |
| AppServiceContainer.Linux | Best practices for Azure App Service Container (Linux). |
| AppServiceManagedInstance.Windows | Best practices for Azure App Service Managed Instance (Windows). |
| ACA | Best practices for Azure Container Apps. |
Two examples on how to configure properly.
Example one: you'd like to migrate your apps to Azure but haven't decided on the target compute service yet.
{
"appcat": {
"target": "Any"
}
}
Example two: you'd like to migrate your apps to App Service Linux and want to understand what are the issues to be fixed.
{
"appcat": {
"target": "AppService.Linux"
}
}
*/
{
"appcat": {
"target": "Any"
}
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,68 @@
# .NET Modernization Plan: Local MariaDB Application
## Overview
Complete modernization of Marechai application from .NET 5 to .NET 9 with dependency updates and local credential security.
## Goals
1. Upgrade from .NET 5.0 to .NET 9.0
2. Update all NuGet packages to latest compatible versions
3. Fix security vulnerabilities (SkiaSharp CVE)
4. Remove deprecated dependencies (Application Insights)
5. Implement secure local credential storage using Data Protection API
6. Maintain all MariaDB functionality and local-only architecture
## Projects
- **Marechai.Database.csproj** - Class library (net5.0 → net9.0)
- **Marechai.csproj** - ASP.NET Core app (net5.0 → net9.0)
## Migration Tasks
### Task 1: Upgrade .NET Framework
- Update Marechai.Database.csproj target framework from net5.0 to net9.0
- Update Marechai.csproj target framework from net5.0 to net9.0
- Verify framework migration compatibility
### Task 2: Update Critical NuGet Packages
#### Marechai.Database.csproj
- Microsoft.AspNetCore.Identity.EntityFrameworkCore: 5.0.1 → 9.0.11
- Microsoft.EntityFrameworkCore.Design: 5.0.1 → 9.0.11
- Microsoft.EntityFrameworkCore.Proxies: 5.0.1 → 9.0.11
#### Marechai.csproj
- Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore: 5.0.1 → 9.0.11
- Microsoft.AspNetCore.Identity.EntityFrameworkCore: 5.0.1 → 9.0.11
- Microsoft.AspNetCore.Identity.UI: 5.0.1 → 9.0.11
- Microsoft.EntityFrameworkCore.Proxies: 5.0.1 → 9.0.11
- Microsoft.EntityFrameworkCore.Tools: 5.0.1 → 9.0.11
- Microsoft.VisualStudio.Web.CodeGeneration.Design: 5.0.1 → 9.0.0
- SkiaSharp: 2.80.2 → 3.119.1
- SkiaSharp.NativeAssets.Linux: 2.80.2 → 3.119.1
### Task 3: Remove Deprecated Packages
- Microsoft.ApplicationInsights.AspNetCore: 2.16.0 (deprecated)
### Task 4: Implement Local Credential Encryption
- Add Data Protection API configuration in appsettings.json
- Create credential encryption utility for DPAPI integration
- Update connection string handling in Program.cs
- Add encrypted credential support in Startup.cs
- Ensure backward compatibility with existing deployments
## Execution Order
1. Upgrade .NET framework versions
2. Update NuGet packages
3. Fix security vulnerabilities
4. Remove deprecated dependencies
5. Implement credential encryption
6. Comprehensive testing and validation
7. Commit all changes
## Success Criteria
- ✅ Both projects target .NET 9.0
- ✅ All recommended packages updated
- ✅ SkiaSharp security vulnerability fixed
- ✅ Application Insights deprecated package removed
- ✅ Credentials can be securely stored locally
- ✅ Build succeeds with no errors
- ✅ All changes committed to git

View File

@@ -0,0 +1,87 @@
# Migration Progress Tracking
## Current Status: COMPLETED ✅
### Completed Steps
- ✅ Branch created for .NET 5.0 → .NET 9.0 migration
- ✅ Migration plan and progress files created
- ✅ Marechai.Database.csproj upgraded to net9.0
- ✅ Marechai.csproj upgraded to net9.0
- ✅ Entity Framework Core packages updated to 9.0.11
- ✅ SkiaSharp security vulnerability fixed (2.80.2 → 3.119.1)
- ✅ Deprecated Application Insights removed
- ✅ Credential encryption implemented via Data Protection API
- ✅ Build successful with no errors
- ✅ All changes committed to git
## Detailed Task Status
### Framework Upgrade
- [x] Marechai.Database.csproj: net5.0 → net9.0
- [x] Marechai.csproj: net5.0 → net9.0
### Package Updates - Marechai.Database.csproj
- [x] Microsoft.AspNetCore.Identity.EntityFrameworkCore: 5.0.1 → 9.0.11
- [x] Microsoft.EntityFrameworkCore.Design: 5.0.1 → 9.0.11
- [x] Microsoft.EntityFrameworkCore.Proxies: 5.0.1 → 9.0.11
### Package Updates - Marechai.csproj
- [x] Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore: 5.0.1 → 9.0.11
- [x] Microsoft.AspNetCore.Identity.EntityFrameworkCore: 5.0.1 → 9.0.11
- [x] Microsoft.AspNetCore.Identity.UI: 5.0.1 → 9.0.11
- [x] Microsoft.EntityFrameworkCore.Proxies: 5.0.1 → 9.0.11
- [x] Microsoft.EntityFrameworkCore.Tools: 5.0.1 → 9.0.11
- [x] Microsoft.VisualStudio.Web.CodeGeneration.Design: 5.0.1 → 9.0.0
- [x] SkiaSharp: 2.80.2 → 3.119.1
- [x] SkiaSharp.NativeAssets.Linux: 2.80.2 → 3.119.1
### Deprecated Packages
- [x] Removed Microsoft.ApplicationInsights.AspNetCore: 2.16.0
### Credential Encryption
- [x] Added DPAPI configuration via Data Protection API
- [x] Created CredentialEncryptor helper class
- [x] Created ConnectionStringManager for connection string handling
- [x] Updated Startup.cs with credential encryption services
- [x] Implemented secure credential handling with fallback support
## Modernization Summary
### What Was Accomplished
1. **Framework Upgrade**: Both projects successfully migrated from .NET 5.0 to .NET 9.0
2. **Package Updates**: All critical NuGet packages updated to latest compatible versions
3. **Security Fixes**: SkiaSharp CVE vulnerability fixed with upgrade to 3.119.1
4. **Deprecated Dependencies**: Removed Application Insights (2.16.0) which was deprecated
5. **Local Credential Encryption**: Implemented ASP.NET Core Data Protection API for secure credential storage
6. **Backward Compatibility**: Existing plaintext credentials continue to work
### Files Modified
- Marechai.Database/Marechai.Database.csproj
- Marechai/Marechai.csproj
- Marechai/Startup.cs
- Marechai/Pages/_Host.cshtml
### Files Created
- Marechai/Helpers/CredentialEncryptor.cs
- Marechai/Helpers/ConnectionStringManager.cs
- Marechai/CREDENTIAL_ENCRYPTION_GUIDE.md
- .appmod/.migration/plan.md
- .appmod/.migration/progress.md
## Build Status
**Build Successful** - No compilation errors
## Architecture Notes
- All credential encryption is LOCAL-ONLY using Data Protection API (DPAPI)
- No cloud services or Azure dependencies required
- MariaDB database remains local and unchanged
- Backward compatible with existing configurations
- Graceful fallback from encrypted to plaintext credentials
## Next Steps
1. Test the application with existing MariaDB connection
2. Deploy to production environment
3. (Optional) Encrypt connection strings in production using the provided guide
## Last Updated
2024-11-13 01:58:16

File diff suppressed because it is too large Load Diff

2
.gitignore vendored
View File

@@ -442,6 +442,8 @@ $RECYCLE.BIN/
# Assets
Marechai/originals
Marechai/wwwroot/assets/photos
Marechai/wwwroot/assets/**/*.jpeg
Marechai/wwwroot/assets/**/*.svg
Marechai/wwwroot/assets/**/*.png

View File

@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ContentModelUserStore">
<component name="UserContentModel">
<attachedFolders />
<explicitIncludes />
<explicitExcludes />
</component>

View File

@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/.idea.Marechai/.idea/riderModule.iml" filepath="$PROJECT_DIR$/.idea/.idea.Marechai/.idea/riderModule.iml" />
</modules>
</component>
</project>

View File

@@ -1,14 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="RIDER_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$/../.." />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="library" name="R User Library" level="project" />
<orderEntry type="library" name="R Skeletons" level="application" />
<orderEntry type="library" name="bootstrap" level="application" />
<orderEntry type="library" name="all" level="application" />
<orderEntry type="library" name="jquery-3.3.1.slim" level="application" />
<orderEntry type="library" name="popper.js" level="application" />
<orderEntry type="library" name="analytics" level="application" />
</component>
</module>

View File

@@ -1,27 +1,34 @@
# Contributing
## Commit signature
For security reason we require all commits to be cryptographically signed.
This section explains how to setup the development environment for that purpose.
### Visual Studio and Visual Studio Code for Windows
You need to install Git for Windows. It is available as a component of Visual Studio, or separately in https://gitforwindows.org.
You need to install Git for Windows. It is available as a component of Visual Studio, or separately
in https://gitforwindows.org.
You also need to install Gpg4win from https://www.gpg4win.org. Ensure to select the Kleopatra component.
Once you have them installed, open Kleopatra and generate a new key pair, of OpenPGP type, following the instructions [here](https://www.gpg4win.org/doc/en/gpg4win-compendium_12.html).
Once you have them installed, open Kleopatra and generate a new key pair, of OpenPGP type, following the
instructions [here](https://www.gpg4win.org/doc/en/gpg4win-compendium_12.html).
Save aside the fingerprint, you'll need it later.
Now go to environment variables (in the properties of your computer) and add this to the path:
`C:\Program Files\Git\usr\bin`
Finally, open Git Bash, and write the following commands if you want all git commits to be signed:
```bash
git config --global commit.gpgsign true
git config --global user.signingkey <FINGERPRINT>
git config --global gpg.program "C:\Program Files (x86)\GnuPG\bin\gpg.exe"
```
or if you want the options to apply only for this project
```bash
cd /DRIVE/PATH_TO_PROJECT
git config commit.gpgsign true
@@ -29,8 +36,11 @@ git config user.signingkey FINGERPRINT
git config gpg.program "C:\Program Files (x86)\GnuPG\bin\gpg.exe"
```
replacing `FINGERPRINT` with the fingerprint you saved from the key generation, `DRIVE` with the drive letter and `PATH_TO_PROJECT` using `/` as path separator.
replacing `FINGERPRINT` with the fingerprint you saved from the key generation, `DRIVE` with the drive letter and
`PATH_TO_PROJECT` using `/` as path separator.
Once this is done, every time you commit in VS / VSCode, a message box titled `pinentry-qt` will ask for the passphrase you set up earlier and sign the commit with your key.
Once this is done, every time you commit in VS / VSCode, a message box titled `pinentry-qt` will ask for the passphrase
you set up earlier and sign the commit with your key.
For GitHub to recognize your signature you need to follow the steps [here](https://help.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account).
For GitHub to recognize your signature you need to follow the
steps [here](https://help.github.com/en/github/authenticating-to-github/adding-a-new-gpg-key-to-your-github-account).

17
Directory.Build.props Normal file
View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<Company>Canary Islands Computer Museum</Company>
<Copyright>Copyright © 2003-2026 Natalia Portillo</Copyright>
<Product>Canary Islands Computer Museum Website</Product>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<!-- Common duplicated packages -->
<PackageReference Include="Microsoft.AspNetCore.Identity.EntityFrameworkCore"/>
<PackageReference Include="Microsoft.EntityFrameworkCore.Proxies"/>
<PackageReference Include="Packaging.Targets">
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
</Project>

1
Directory.Build.targets Normal file
View File

@@ -0,0 +1 @@
<Project></Project>

47
Directory.Packages.props Normal file
View File

@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</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 -->
<PackageVersion Include="Blazorise.Bootstrap" Version="0.9.2.4"/>
<PackageVersion Include="Blazorise.Icons.FontAwesome" Version="0.9.2.4"/>
<PackageVersion Include="Markdig" Version="0.22.1"/>
<PackageVersion Include="Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore" Version="9.0.11"/>
<PackageVersion Include="Microsoft.AspNetCore.Identity.UI" Version="9.0.11"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.11"/>
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0"/>
<PackageVersion Include="MySql.Data" Version="8.0.22"/>
<PackageVersion Include="SkiaSharp" Version="3.119.1"/>
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.1"/>
<PackageVersion Include="Svg.Skia" Version="0.4.1"/>
<PackageVersion Include="Tewr.Blazor.FileReader" Version="3.0.0.20340"/>
<PackageVersion Include="Unclassified.NetRevisionTask" Version="0.3.0"/>
<!-- Unique to Marechai.Database.csproj -->
<PackageVersion Include="Aaru.CommonTypes" Version="5.4.1"/>
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.11"/>
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0"/>
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql.Json.Microsoft" Version="9.0.0"/>
<!-- Build infrastructure -->
<PackageVersion Include="Packaging.Targets" Version="0.1.189"/>
<!-- Unique to Marechai.Server.csproj -->
<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"/>
</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>

Binary file not shown.

23
Marechai.App/App.xaml Normal file
View File

@@ -0,0 +1,23 @@
<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: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>
<!-- Add resources here -->
<local:ObjectToVisibilityConverter x:Key="ObjectToVisibilityConverter" />
<local:StringToVisibilityConverter x:Key="StringToVisibilityConverter" />
<local:ZeroToVisibilityConverter x:Key="ZeroToVisibilityConverter" />
</ResourceDictionary>
</Application.Resources>
</Application>

257
Marechai.App/App.xaml.cs Normal file
View File

@@ -0,0 +1,257 @@
using System.Net.Http;
using Marechai.App.Presentation.ViewModels;
using Marechai.App.Presentation.Views;
using Marechai.App.Services;
using Marechai.App.Services.Caching;
using Microsoft.UI.Xaml;
using Uno.Extensions;
using Uno.Extensions.Configuration;
using Uno.Extensions.Hosting;
using Uno.Extensions.Http;
using Uno.Extensions.Localization;
using Uno.Extensions.Navigation;
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 SoundSynthDetailViewModel = Marechai.App.Presentation.ViewModels.SoundSynthDetailViewModel;
using SoundSynthsListViewModel = Marechai.App.Presentation.ViewModels.SoundSynthsListViewModel;
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().
/// </summary>
public App()
{
InitializeComponent();
}
protected Window? MainWindow { get; private set; }
public IHost? Host { get; private set; }
protected override async void OnLaunched(LaunchActivatedEventArgs args)
{
IApplicationBuilder 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((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) =>
{
#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<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>();
})
.UseNavigation(RegisterRoutes));
MainWindow = builder.Window;
#if DEBUG
MainWindow.UseStudio();
#endif
MainWindow.SetWindowIcon();
Host = await builder.NavigateAsync<Shell>();
}
private static void RegisterRoutes(IViewRegistry views, IRouteRegistry routes)
{
views.Register(new ViewMap(ViewModel: typeof(ShellViewModel)),
new ViewMap<MainPage, MainViewModel>(),
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 DataViewMap<SecondPage, SecondViewModel, Entity>());
routes.Register(new RouteMap("",
views.FindByViewModel<ShellViewModel>(),
Nested:
[
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("Second",
views.FindByViewModel<SecondViewModel>())
])
]));
}
}

View File

@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="456"
height="456"
viewBox="0 0 456 456"
version="1.1"
id="svg453"
sodipodi:docname="icon.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs457" />
<sodipodi:namedview
id="namedview455"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="1.8574561"
inkscape:cx="228.26919"
inkscape:cy="228.26919"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="svg453" />
<rect
x="0"
y="0"
width="456"
height="456"
fill="#FFFFFF"
id="rect451" />
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="450"
height="450"
viewBox="0 0 50.369617 49.826836"
version="1.1"
id="svg151"
sodipodi:docname="icon_foreground.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview153"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="1.250876"
inkscape:cx="218.64677"
inkscape:cy="175.87674"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="g149" />
<defs
id="defs105">
<path
id="aj28a0fd1a"
d="M 1.738,0.156 3.927,2.323 2.347,3.919 0.101,1.81 Z" />
<path
id="fdje57jgic"
d="M 2.201,0.066 3.855,1.703 1.69,3.894 0.093,2.311 Z" />
<path
id="6bg72xwlze"
d="M 2.398,0.044 3.994,1.624 1.886,3.869 0.232,2.232 Z" />
<path
id="eaqjnja8wg"
d="M 1.736,0.023 3.981,2.132 2.344,3.786 0.156,1.619 Z" />
</defs>
<g
fill="none"
fill-rule="evenodd"
id="g149"
transform="translate(-2.9304427e-4,-1.6465461e-4)">
<g
id="g147">
<g
id="g145">
<path
fill="#7a67f8"
d="M 34.758,38.865 H 34.746 C 31.892,38.86 29.342,36.882 26.152,33.692 l -6.93,-6.873 2.166,-2.188 6.937,6.88 c 3.075,3.074 4.876,4.272 6.427,4.275 h 0.005 c 1.567,0 3.467,-1.262 6.558,-4.353 l 3.541,-3.587 c 1.784,-1.784 2.57,-3.34 2.408,-4.762 -0.13,-1.156 -0.894,-2.397 -2.401,-3.904 L 44.83,19.146 C 43.202,17.414 41.211,15.483 39.131,14.414 38.745,12.437 37.48,10.881 37.3,10.596 c 3.803,0.559 7.197,3.703 9.758,6.424 2.788,2.794 5.803,7.176 -0.018,12.996 l -3.54,3.588 c -3.251,3.25 -5.844,5.261 -8.742,5.261"
id="path107" />
<path
fill="#f85977"
d="m 25.399,28.608 6.492,-6.562 c 3.076,-3.076 4.274,-4.877 4.276,-6.428 0.004,-1.567 -1.257,-3.469 -4.352,-6.563 L 28.228,5.515 C 24.58,1.867 22.369,2.699 19.561,5.507 L 19.528,5.54 c -1.54,1.448 -3.237,3.182 -4.346,5.01 -1.031,0.073 -2.361,0.424 -3.997,1.518 0.906,-3.397 3.737,-6.422 6.216,-8.755 2.794,-2.789 7.177,-5.804 12.997,0.017 l 3.588,3.54 c 3.255,3.256 5.266,5.851 5.26,8.754 -0.005,2.854 -1.982,5.404 -5.172,8.594 l -6.489,6.559 z"
id="path109" />
<path
fill="#159bff"
d="M 12.522,38.707 C 8.939,37.946 5.746,34.972 3.308,32.382 2.035,31.106 0.321,29.13 0.042,26.663 c -0.274,-2.414 0.8,-4.795 3.283,-7.278 l 3.542,-3.588 c 3.25,-3.25 5.843,-5.261 8.74,-5.261 h 0.013 c 2.854,0.005 5.404,1.983 8.593,5.172 l 7.046,6.976 -2.165,2.19 -7.053,-6.983 c -3.076,-3.076 -4.876,-4.273 -6.427,-4.276 h -0.006 c -1.566,0 -3.466,1.261 -6.557,4.352 L 5.51,21.555 c -1.784,1.784 -2.57,3.34 -2.409,4.762 0.131,1.156 0.894,2.396 2.402,3.904 l 0.033,0.034 c 1.55,1.649 3.43,3.479 5.401,4.573 0.168,1.739 1.2,3.297 1.585,3.88"
id="path111" />
<path
fill="#67e5ad"
d="m 26.32,49.827 c -1.925,0 -4.114,-0.886 -6.557,-3.33 l -3.588,-3.54 C 9.167,35.949 9.151,32.546 16.086,25.61 l 6.802,-6.872 2.193,2.162 -6.812,6.882 c -3.076,3.076 -4.273,4.877 -4.276,6.427 -0.003,1.568 1.258,3.47 4.352,6.563 l 3.588,3.541 c 3.646,3.647 5.858,2.816 8.666,0.008 l 0.034,-0.033 c 1.654,-1.555 3.5,-3.46 4.593,-5.437 1.661,-0.14 2.9,-0.841 3.835,-1.438 -0.8,3.537 -3.738,6.69 -6.302,9.102 -1.62,1.618 -3.777,3.312 -6.439,3.312"
id="path113" />
<g
transform="translate(21.154,18.577)"
id="g120">
<mask
id="8jptpqrneb"
fill="#ffffff">
<use
xlink:href="#aj28a0fd1a"
id="use115" />
</mask>
<path
d="M 0.101,1.81 1.738,0.156 3.927,2.323 2.347,3.919 Z"
mask="url(#8jptpqrneb)"
id="path118" />
</g>
<g
transform="translate(27.404,20.981)"
id="g127">
<mask
id="b2iljpfwbd"
fill="#ffffff">
<use
xlink:href="#fdje57jgic"
id="use122" />
</mask>
<path
d="M 2.201,0.066 3.855,1.703 1.69,3.894 0.093,2.311 Z"
mask="url(#b2iljpfwbd)"
id="path125" />
</g>
<g
transform="translate(18.99,24.587)"
id="g134">
<mask
id="gj70tyfpnf"
fill="#ffffff">
<use
xlink:href="#6bg72xwlze"
id="use129" />
</mask>
<path
d="M 1.886,3.869 0.232,2.232 2.398,0.044 3.994,1.624 Z"
mask="url(#gj70tyfpnf)"
id="path132" />
</g>
<g
transform="translate(25.24,26.99)"
id="g141">
<mask
id="z7vhvduckh"
fill="#ffffff">
<use
xlink:href="#eaqjnja8wg"
id="use136" />
</mask>
<path
d="M 3.981,2.132 2.344,3.786 0.156,1.619 1.736,0.023 Z"
mask="url(#z7vhvduckh)"
id="path139" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,32 @@
# Shared Assets
See documentation about assets here: https://github.com/unoplatform/uno/blob/master/doc/articles/features/working-with-assets.md
## Here is a cheat sheet
1. Add the image file to the `Assets` directory of a shared project.
2. Set the build action to `Content`.
3. (Recommended) Provide an asset for various scales/dpi
### Examples
```text
\Assets\Images\logo.scale-100.png
\Assets\Images\logo.scale-200.png
\Assets\Images\logo.scale-400.png
\Assets\Images\scale-100\logo.png
\Assets\Images\scale-200\logo.png
\Assets\Images\scale-400\logo.png
```
### Table of scales
| Scale | WinUI | iOS | Android |
|-------|:-----------:|:---------------:|:-------:|
| `100` | scale-100 | @1x | mdpi |
| `125` | scale-125 | N/A | N/A |
| `150` | scale-150 | N/A | hdpi |
| `200` | scale-200 | @2x | xhdpi |
| `300` | scale-300 | @3x | xxhdpi |
| `400` | scale-400 | N/A | xxxhdpi |

View File

@@ -0,0 +1,137 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="450"
height="450"
viewBox="0 0 50.369617 49.826836"
version="1.1"
id="svg151"
sodipodi:docname="icon_foreground.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview153"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="1.250876"
inkscape:cx="218.64677"
inkscape:cy="175.87674"
inkscape:window-width="1920"
inkscape:window-height="1027"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="g149" />
<defs
id="defs105">
<path
id="aj28a0fd1a"
d="M 1.738,0.156 3.927,2.323 2.347,3.919 0.101,1.81 Z" />
<path
id="fdje57jgic"
d="M 2.201,0.066 3.855,1.703 1.69,3.894 0.093,2.311 Z" />
<path
id="6bg72xwlze"
d="M 2.398,0.044 3.994,1.624 1.886,3.869 0.232,2.232 Z" />
<path
id="eaqjnja8wg"
d="M 1.736,0.023 3.981,2.132 2.344,3.786 0.156,1.619 Z" />
</defs>
<g
fill="none"
fill-rule="evenodd"
id="g149"
transform="translate(-2.9304427e-4,-1.6465461e-4)">
<g
id="g147">
<g
id="g145">
<path
fill="#7a67f8"
d="M 34.758,38.865 H 34.746 C 31.892,38.86 29.342,36.882 26.152,33.692 l -6.93,-6.873 2.166,-2.188 6.937,6.88 c 3.075,3.074 4.876,4.272 6.427,4.275 h 0.005 c 1.567,0 3.467,-1.262 6.558,-4.353 l 3.541,-3.587 c 1.784,-1.784 2.57,-3.34 2.408,-4.762 -0.13,-1.156 -0.894,-2.397 -2.401,-3.904 L 44.83,19.146 C 43.202,17.414 41.211,15.483 39.131,14.414 38.745,12.437 37.48,10.881 37.3,10.596 c 3.803,0.559 7.197,3.703 9.758,6.424 2.788,2.794 5.803,7.176 -0.018,12.996 l -3.54,3.588 c -3.251,3.25 -5.844,5.261 -8.742,5.261"
id="path107" />
<path
fill="#f85977"
d="m 25.399,28.608 6.492,-6.562 c 3.076,-3.076 4.274,-4.877 4.276,-6.428 0.004,-1.567 -1.257,-3.469 -4.352,-6.563 L 28.228,5.515 C 24.58,1.867 22.369,2.699 19.561,5.507 L 19.528,5.54 c -1.54,1.448 -3.237,3.182 -4.346,5.01 -1.031,0.073 -2.361,0.424 -3.997,1.518 0.906,-3.397 3.737,-6.422 6.216,-8.755 2.794,-2.789 7.177,-5.804 12.997,0.017 l 3.588,3.54 c 3.255,3.256 5.266,5.851 5.26,8.754 -0.005,2.854 -1.982,5.404 -5.172,8.594 l -6.489,6.559 z"
id="path109" />
<path
fill="#159bff"
d="M 12.522,38.707 C 8.939,37.946 5.746,34.972 3.308,32.382 2.035,31.106 0.321,29.13 0.042,26.663 c -0.274,-2.414 0.8,-4.795 3.283,-7.278 l 3.542,-3.588 c 3.25,-3.25 5.843,-5.261 8.74,-5.261 h 0.013 c 2.854,0.005 5.404,1.983 8.593,5.172 l 7.046,6.976 -2.165,2.19 -7.053,-6.983 c -3.076,-3.076 -4.876,-4.273 -6.427,-4.276 h -0.006 c -1.566,0 -3.466,1.261 -6.557,4.352 L 5.51,21.555 c -1.784,1.784 -2.57,3.34 -2.409,4.762 0.131,1.156 0.894,2.396 2.402,3.904 l 0.033,0.034 c 1.55,1.649 3.43,3.479 5.401,4.573 0.168,1.739 1.2,3.297 1.585,3.88"
id="path111" />
<path
fill="#67e5ad"
d="m 26.32,49.827 c -1.925,0 -4.114,-0.886 -6.557,-3.33 l -3.588,-3.54 C 9.167,35.949 9.151,32.546 16.086,25.61 l 6.802,-6.872 2.193,2.162 -6.812,6.882 c -3.076,3.076 -4.273,4.877 -4.276,6.427 -0.003,1.568 1.258,3.47 4.352,6.563 l 3.588,3.541 c 3.646,3.647 5.858,2.816 8.666,0.008 l 0.034,-0.033 c 1.654,-1.555 3.5,-3.46 4.593,-5.437 1.661,-0.14 2.9,-0.841 3.835,-1.438 -0.8,3.537 -3.738,6.69 -6.302,9.102 -1.62,1.618 -3.777,3.312 -6.439,3.312"
id="path113" />
<g
transform="translate(21.154,18.577)"
id="g120">
<mask
id="8jptpqrneb"
fill="#ffffff">
<use
xlink:href="#aj28a0fd1a"
id="use115" />
</mask>
<path
d="M 0.101,1.81 1.738,0.156 3.927,2.323 2.347,3.919 Z"
mask="url(#8jptpqrneb)"
id="path118" />
</g>
<g
transform="translate(27.404,20.981)"
id="g127">
<mask
id="b2iljpfwbd"
fill="#ffffff">
<use
xlink:href="#fdje57jgic"
id="use122" />
</mask>
<path
d="M 2.201,0.066 3.855,1.703 1.69,3.894 0.093,2.311 Z"
mask="url(#b2iljpfwbd)"
id="path125" />
</g>
<g
transform="translate(18.99,24.587)"
id="g134">
<mask
id="gj70tyfpnf"
fill="#ffffff">
<use
xlink:href="#6bg72xwlze"
id="use129" />
</mask>
<path
d="M 1.886,3.869 0.232,2.232 2.398,0.044 3.994,1.624 Z"
mask="url(#gj70tyfpnf)"
id="path132" />
</g>
<g
transform="translate(25.24,26.99)"
id="g141">
<mask
id="z7vhvduckh"
fill="#ffffff">
<use
xlink:href="#eaqjnja8wg"
id="use136" />
</mask>
<path
d="M 3.981,2.132 2.344,3.786 0.156,1.619 1.736,0.023 Z"
mask="url(#z7vhvduckh)"
id="path139" />
</g>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

@@ -0,0 +1,13 @@
global using System.Collections.Immutable;
global using Microsoft.Extensions.DependencyInjection;
global using Microsoft.Extensions.Hosting;
global using Microsoft.Extensions.Localization;
global using Microsoft.Extensions.Logging;
global using Microsoft.Extensions.Options;
global using Marechai.App.Models;
global using Marechai.App.Presentation;
global using Marechai.App.Services.Endpoints;
global using Uno.Extensions.Http.Kiota;
global using ApplicationExecutionState = Windows.ApplicationModel.Activation.ApplicationExecutionState;
global using CommunityToolkit.Mvvm.ComponentModel;
global using CommunityToolkit.Mvvm.Input;

View File

@@ -0,0 +1,93 @@
<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>
<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>
<!--
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;
Svg;
</UnoFeatures>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Marechai.Data\Marechai.Data.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Humanizer" />
</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>
</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>
</Project>

View File

@@ -0,0 +1,6 @@
namespace Marechai.App.Models;
public record AppConfig
{
public string? Environment { get; init; }
}

View File

@@ -0,0 +1,3 @@
namespace Marechai.App.Models;
public record Entity(string Name);

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity />
<Properties />
<Dependencies>
<TargetDeviceFamily Name="Windows.Universal" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="x-generate"/>
</Resources>
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<uap:VisualElements />
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:supportsRtl="true"></application>
</manifest>

View File

@@ -0,0 +1,22 @@
To add cross-platform image assets for your Uno Platform app, use the Assets folder
in the shared project instead. Assets in this folder are Android-only assets.
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories) and given a Build Action of "AndroidAsset".
These files will be deployed with your package and will be accessible using Android's
AssetManager, like this:
public class ReadAsset : Activity
{
protected override void OnCreate (Bundle bundle)
{
base.OnCreate (bundle);
InputStream input = Assets.Open ("my_asset.txt");
}
}
Additionally, some Android functions will automatically load asset files:
Typeface tf = Typeface.CreateFromAsset (Context.Assets, "fonts/samplefont.ttf");

View File

@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Android.App;
using Android.Content;
using Android.OS;
using Android.Runtime;
using Android.Views;
using Android.Widget;
using Microsoft.UI.Xaml.Media;
namespace Marechai.App.Droid;
[global::Android.App.ApplicationAttribute(
Label = "@string/ApplicationName",
Icon = "@mipmap/icon",
LargeHeap = true,
HardwareAccelerated = true,
Theme = "@style/Theme.App.Starting"
)]
public class Application : Microsoft.UI.Xaml.NativeApplication
{
public Application(IntPtr javaReference, JniHandleOwnership transfer)
: base(() => new App(), javaReference, transfer)
{
}
}

View File

@@ -0,0 +1,22 @@
using Android.App;
using Android.Content.PM;
using Android.OS;
using Android.Views;
using Android.Widget;
namespace Marechai.App.Droid;
[Activity(
MainLauncher = true,
ConfigurationChanges = global::Uno.UI.ActivityHelper.AllConfigChanges,
WindowSoftInputMode = SoftInput.AdjustNothing | SoftInput.StateHidden
)]
public class MainActivity : Microsoft.UI.Xaml.ApplicationActivity
{
protected override void OnCreate(Bundle? savedInstanceState)
{
global::AndroidX.Core.SplashScreen.SplashScreen.InstallSplashScreen(this);
base.OnCreate(savedInstanceState);
}
}

View File

@@ -0,0 +1,47 @@
To add cross-platform image assets for your Uno Platform app, use the Assets folder
in the shared project instead. Resources in this folder are Android-only.
Images, layout descriptions, binary blobs and string dictionaries can be included
in your application as resource files. Various Android APIs are designed to
operate on the resource IDs instead of dealing with images, strings or binary blobs
directly.
For example, a sample Android app that contains a user interface layout (main.axml),
an internationalization string table (strings.xml) and some icons (drawable-XXX/icon.png)
would keep its resources in the "Resources" directory of the application:
Resources/
drawable/
icon.png
layout/
main.axml
values/
strings.xml
In order to get the build system to recognize Android resources, set the build action to
"AndroidResource". The native Android APIs do not operate directly with filenames, but
instead operate on resource IDs. When you compile an Android application that uses resources,
the build system will package the resources for distribution and generate a class called "R"
(this is an Android convention) that contains the tokens for each one of the resources
included. For example, for the above Resources layout, this is what the R class would expose:
public class R {
public class drawable {
public const int icon = 0x123;
}
public class layout {
public const int main = 0x456;
}
public class strings {
public const int first_string = 0xabc;
public const int second_string = 0xbcd;
}
}
You would then use R.drawable.icon to reference the drawable/icon.png file, or R.layout.main
to reference the layout/main.axml file, or R.strings.first_string to reference the first
string in the dictionary file values/strings.xml.

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="Hello">Hello World, Click Me!</string>
<string name="ApplicationName">Marechai.App</string>
</resources>

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8" ?>
<resources>
<style name="AppTheme" parent="Theme.MaterialComponents.Light">
<!-- This removes the ActionBar -->
<item name="windowActionBar">false</item>
<item name="android:windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowNoTitle">true</item>
</style>
<style name="Theme.App.Starting" parent="Theme.SplashScreen">
<!-- uno_splash_color and uno_splash_image are generated by Uno.Resizetizer -->
<!-- This property is used for the splash screen -->
<item name="android:windowSplashScreenBackground">@color/uno_splash_color</item>
<item name="android:windowBackground">@drawable/uno_splash_image</item>
<item name="android:windowSplashScreenAnimatedIcon">@drawable/uno_splash_image</item>
<item name="postSplashScreenTheme">@style/AppTheme</item>
</style>
<style name="Theme.AppCompat.Translucent">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowAnimationStyle">@android:style/Animation</item>
</style>
</resources>

View File

@@ -0,0 +1,2 @@
# See this for more details: http://developer.xamarin.com/guides/android/advanced_topics/garbage_collection/
MONO_GC_PARAMS=bridge-implementation=new,nursery-size=32m,soft-heap-limit=256m

View File

@@ -0,0 +1,21 @@
using System;
using Uno.UI.Hosting;
namespace Marechai.App;
internal class Program
{
[STAThread]
public static void Main(string[] args)
{
var host = UnoPlatformHostBuilder.Create()
.App(() => new App())
.UseX11()
.UseLinuxFrameBuffer()
.UseMacOS()
.UseWin32()
.Build();
host.Run();
}
}

View File

@@ -0,0 +1,10 @@
<linker>
<assembly fullname="Marechai.App" />
<!--
Uncomment this section when using JSON.NET
<assembly fullname="System.Core">
<type fullname="System.Linq.Expressions*" />
</assembly>
-->
</linker>

View File

@@ -0,0 +1,17 @@
using System.Threading.Tasks;
using Uno.UI.Hosting;
namespace Marechai.App;
public class Program
{
public static async Task Main(string[] args)
{
var host = UnoPlatformHostBuilder.Create()
.App(() => new App())
.UseWebAssembly()
.Build();
await host.RunAsync();
}
}

View File

@@ -0,0 +1,28 @@
/**
When adding fonts here, make sure to add them using a base64 data uri, otherwise
fonts loading are delayed, and text may get displayed incorrectly.
*/
/* https://github.com/unoplatform/uno/issues/3954 */
@font-face {
font-family: 'Segoe UI';
src: local('Segoe UI'), local('-apple-system'), local('BlinkMacSystemFont'), local('Inter'), local('Cantarell'), local('Ubuntu'), local('Roboto'), local('Open Sans'), local('Noto Sans'), local('Helvetica Neue'), local('sans-serif');
}
@font-face {
font-family: 'Roboto';
src: url(./Uno.Fonts.Roboto/Fonts/Roboto-Light.ttf) format('truetype');
font-weight: 300;
}
@font-face {
font-family: 'Roboto';
src: url(./Uno.Fonts.Roboto/Fonts/Roboto-Regular.ttf) format('truetype');
font-weight: 400;
}
@font-face {
font-family: 'Roboto';
src: url(./Uno.Fonts.Roboto/Fonts/Roboto-Medium.ttf) format('truetype');
font-weight: 500;
}

View File

@@ -0,0 +1,3 @@
var UnoAppManifest = {
displayName: "Marechai.App"
}

View File

@@ -0,0 +1,10 @@
{
"background_color": "#ffffff",
"description": "Marechai.App",
"display": "standalone",
"name": "Marechai.App",
"short_name": "Marechai.App",
"start_url": "/index.html",
"theme_color": "#ffffff",
"scope": "/"
}

View File

@@ -0,0 +1,30 @@
{
"navigationFallback": {
"rewrite": "/index.html",
"exclude": [
"*.{css,js}",
"*.{png}",
"*.{c,h,wasm,clr,pdb,dat,txt}"
]
},
"routes": [
{
"route": "/package_*",
"headers": {
"cache-control": "public, immutable, max-age=31536000"
}
},
{
"route": "/*.ttf",
"headers": {
"cache-control": "public, immutable, max-age=31536000"
}
},
{
"route": "/*",
"headers": {
"cache-control": "must-revalidate, max-age=3600"
}
}
]
}

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.web>
<customErrors mode="Off"/>
</system.web>
<system.webServer>
<!-- Disable compression as we're doing it through pre-compressed files -->
<urlCompression doStaticCompression="false" doDynamicCompression="false" dynamicCompressionBeforeCache="false" />
<staticContent>
<remove fileExtension=".dll" />
<remove fileExtension=".wasm" />
<remove fileExtension=".woff" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".wasm" mimeType="application/wasm" />
<mimeMap fileExtension=".clr" mimeType="application/octet-stream" />
<mimeMap fileExtension=".pdb" mimeType="application/octet-stream" />
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
<mimeMap fileExtension=".woff2" mimeType="application/font-woff" />
<mimeMap fileExtension=".dat" mimeType="application/octet-stream" />
<!-- Required for PWAs -->
<mimeMap fileExtension=".json" mimeType="application/octet-stream" />
</staticContent>
<rewrite>
<rules>
<rule name="Lookup for pre-compressed brotli file" stopProcessing="true">
<match url="(.*)$"/>
<conditions>
<!-- Match brotli requests -->
<add input="{HTTP_ACCEPT_ENCODING}" pattern="br" />
<!-- Match all but pre-compressed files -->
<add input="{REQUEST_URI}" pattern="^(?!/_compressed_br/)(.*)$" />
<!-- Check if the pre-compressed file exists on the disk -->
<add input="{DOCUMENT_ROOT}/_compressed_br/{C:0}" matchType="IsFile" negate="false" />
</conditions>
<action type="Rewrite" url="/_compressed_br{C:0}" />
</rule>
<rule name="Lookup for pre-compressed gzip file" stopProcessing="true">
<match url="(.*)$"/>
<conditions>
<!-- Match gzip requests -->
<add input="{HTTP_ACCEPT_ENCODING}" pattern="gzip" />
<!-- Match all but pre-compressed files -->
<add input="{REQUEST_URI}" pattern="^(?!/_compressed_gz/)(.*)$" />
<!-- Check if the pre-compressed file exists on the disk -->
<add input="{DOCUMENT_ROOT}/_compressed_gz/{C:0}" matchType="IsFile" negate="false" />
</conditions>
<action type="Rewrite" url="/_compressed_gz{C:0}" />
</rule>
</rules>
<outboundRules>
<rule name="Adjust content encoding for gzip pre-compressed files" enabled="true" stopProcessing="true">
<match serverVariable="RESPONSE_CONTENT_ENCODING" pattern="" />
<conditions>
<add input="{REQUEST_URI}" pattern="/_compressed_gz/.*$" />
</conditions>
<action type="Rewrite" value="gzip"/>
</rule>
<rule name="Adjust content encoding for brotli pre-compressed files" enabled="true" stopProcessing="true">
<match serverVariable="RESPONSE_CONTENT_ENCODING" pattern="" />
<conditions>
<add input="{REQUEST_URI}" pattern="/_compressed_br/.*$" />
</conditions>
<action type="Rewrite" value="br"/>
</rule>
</outboundRules>
</rewrite>
</system.webServer>
</configuration>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
</dict>
</plist>

View File

@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIDeviceFamily</key>
<array>
<integer>1</integer>
<integer>2</integer>
</array>
<key>UIRequiredDeviceCapabilities</key>
<array>
<string>armv7</string>
<string>arm64</string>
</array>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>XSAppIconAssets</key>
<string>Assets.xcassets/icon.appiconset</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<!--
Adjust this to your application's encryption usage.
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
-->
</dict>
</plist>

View File

@@ -0,0 +1,18 @@
using UIKit;
using Uno.UI.Hosting;
namespace Marechai.App.iOS;
public class EntryPoint
{
// This is the main entry point of the application.
public static void Main(string[] args)
{
var host = UnoPlatformHostBuilder.Create()
.App(() => new App())
.UseAppleUIKit()
.Build();
host.Run();
}
}

View File

@@ -0,0 +1,58 @@
{
"images": [
{
"orientation": "portrait",
"extent": "full-screen",
"minimum-system-version": "7.0",
"scale": "2x",
"size": "640x960",
"idiom": "iphone"
},
{
"orientation": "portrait",
"extent": "full-screen",
"minimum-system-version": "7.0",
"subtype": "retina4",
"scale": "2x",
"size": "640x1136",
"idiom": "iphone"
},
{
"orientation": "portrait",
"extent": "full-screen",
"minimum-system-version": "7.0",
"scale": "1x",
"size": "768x1024",
"idiom": "ipad"
},
{
"orientation": "landscape",
"extent": "full-screen",
"minimum-system-version": "7.0",
"scale": "1x",
"size": "1024x768",
"idiom": "ipad"
},
{
"orientation": "portrait",
"extent": "full-screen",
"minimum-system-version": "7.0",
"scale": "2x",
"size": "1536x2048",
"idiom": "ipad"
},
{
"orientation": "landscape",
"extent": "full-screen",
"minimum-system-version": "7.0",
"scale": "2x",
"size": "2048x1536",
"idiom": "ipad"
}
],
"properties": {},
"info": {
"version": 1,
"author": ""
}
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<!-- see https://aka.platform/uno/apple-privacy-manifest for more information -->
<!-- .NET Runtime/BCL -->
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>C617.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>35F9.1</string>
</array>
</dict>
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryDiskSpace</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>E174.1</string>
</array>
</dict>
<!-- NSUserDefaults -->
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,260 @@
<?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">
<!-- 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

@@ -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

@@ -20,18 +20,16 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2021 Natalia Portillo
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
using Marechai.Database;
namespace Marechai.App.Presentation.Models;
namespace Marechai.ViewModels
/// <summary>
/// Navigation parameter for the ProcessorDetailPage containing both the processor ID and the navigation source.
/// </summary>
public class ProcessorDetailNavigationParameter
{
public class StorageByMachineViewModel : BaseViewModel<long>
{
public int MachineId { get; set; }
public StorageType Type { get; set; }
public StorageInterface Interface { get; set; }
public long? Capacity { get; set; }
}
public required int ProcessorId { get; init; }
public object? NavigationSource { get; init; }
}

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

View File

@@ -0,0 +1,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,12 @@
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation.ViewModels;
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,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,103 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Marechai.App.Presentation.Models;
using Marechai.App.Services;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation.ViewModels;
public partial class SoundSynthsListViewModel : ObservableObject
{
private readonly ILogger<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();
SoundSynths = new ObservableCollection<SoundSynthListItem>(soundSynths.Select(ss => new SoundSynthListItem
{
Id = ss.Id ?? 0,
Name = ss.Name ?? "Unknown",
Company = ss.Company ?? "Unknown"
})
.OrderBy(ss => ss.Company)
.ThenBy(ss => ss.Name));
_logger.LogInformation("Successfully loaded {Count} Sound Synthesizers", SoundSynths.Count);
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error loading Sound Synthesizers");
ErrorMessage = "Failed to load Sound Synthesizers. Please try again later.";
HasError = true;
}
finally
{
IsLoading = false;
}
}
private async Task NavigateToSoundSynthAsync(SoundSynthListItem? item)
{
if(item == null) return;
_logger.LogInformation("Navigating to Sound Synthesizer {SoundSynthId}", item.Id);
await _navigator.NavigateViewModelAsync<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; }
}
}

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,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>

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