mirror of
https://github.com/claunia/marechai.git
synced 2025-12-16 19:14:25 +00:00
Compare commits
144 Commits
snyk-fix-8
...
b06406e20f
| Author | SHA1 | Date | |
|---|---|---|---|
|
b06406e20f
|
|||
|
80ba603265
|
|||
|
db7aee369b
|
|||
|
5fe5c94c55
|
|||
|
5f66029528
|
|||
|
869e675ee3
|
|||
|
20216bc1d6
|
|||
|
22bcd4ede4
|
|||
|
7f080c8734
|
|||
|
6e4a4bc67b
|
|||
|
ac460cb050
|
|||
|
c1c6e427c2
|
|||
|
4c273ef661
|
|||
|
9567153378
|
|||
|
c475d0e6a4
|
|||
|
9f89186dde
|
|||
|
733dc59f7b
|
|||
|
5b709755c7
|
|||
|
959a48b36c
|
|||
|
4b02dd6d2c
|
|||
|
f308668f69
|
|||
|
e5f1d766b5
|
|||
|
cc2738e45d
|
|||
|
981cd3c27c
|
|||
|
5c64e59f8f
|
|||
|
01c24ae987
|
|||
|
a60fb39687
|
|||
|
497251be86
|
|||
|
195b23f755
|
|||
|
edc8d33bb2
|
|||
|
dbef655a3d
|
|||
|
4f59f6870d
|
|||
|
c3e75175f9
|
|||
|
1dcb062c35
|
|||
|
6a52c1f067
|
|||
|
c35fdbb0e4
|
|||
|
0368e12974
|
|||
|
fe2c3a082d
|
|||
|
4d30530ef0
|
|||
|
e0689684e1
|
|||
|
cfdef93787
|
|||
|
c6cac9e04a
|
|||
|
e2f86b76db
|
|||
|
80791a8cc9
|
|||
|
d5fbb55425
|
|||
|
7ee042bdec
|
|||
|
e9221ac130
|
|||
|
87291d9dd8
|
|||
|
ce1c089fb0
|
|||
|
5d249f435e
|
|||
|
3e4677b084
|
|||
|
b7c94312fc
|
|||
|
61ebf7b503
|
|||
|
b18396f8d8
|
|||
|
4f1aee302b
|
|||
|
7ede62514f
|
|||
|
955c2f9654
|
|||
|
4a5708b910
|
|||
|
5bffbc342e
|
|||
|
392c69350f
|
|||
|
2bb07845e1
|
|||
|
1053617622
|
|||
|
30b60c0e96
|
|||
|
14596c5499
|
|||
|
9847c987b5
|
|||
|
6f0de86be4
|
|||
|
37d8df4b7d
|
|||
|
860fd99b00
|
|||
|
2ba7d86e71
|
|||
|
37403e5e53
|
|||
|
2066e211e1
|
|||
|
ebc611e5d2
|
|||
|
5e3be9cbb0
|
|||
|
9d146eb151
|
|||
|
52b6145a8b
|
|||
|
09ced17903
|
|||
|
356674f51f
|
|||
|
cd4aa5767d
|
|||
|
cf24356030
|
|||
|
f85dc22bf6
|
|||
|
795f5ba27d
|
|||
|
059417f0dc
|
|||
|
5b4a1b42e0
|
|||
|
e9a2a68e49
|
|||
|
7c29302153
|
|||
|
76bebc68b7
|
|||
|
4a2d46f3b0
|
|||
|
e9da9c7a3f
|
|||
|
b81c628f07
|
|||
|
6fc709a271
|
|||
|
507e5686e4
|
|||
|
27e5616da8
|
|||
|
464f52878b
|
|||
|
f6214e6d14
|
|||
|
baaf571505
|
|||
|
8d6c382754
|
|||
|
e171b8ddd8
|
|||
|
6981075c4a
|
|||
|
bed72102e8
|
|||
|
d301315fbf
|
|||
|
79f0d2632b
|
|||
|
0ba1a24b4e
|
|||
|
583f20ff99
|
|||
|
1826c70883
|
|||
|
34d76fd646
|
|||
|
fc6238aef1
|
|||
|
505ace535f
|
|||
|
a715d936eb
|
|||
|
e4c2837ad9
|
|||
|
36520596da
|
|||
|
764e058f79
|
|||
|
349b396588
|
|||
|
d0e5725ae0
|
|||
|
0bbf821489
|
|||
|
10017850f8
|
|||
|
d9239f39c0
|
|||
|
f445006e46
|
|||
|
3520e49b25
|
|||
|
268c8fab07
|
|||
|
e9e3ef2ab0
|
|||
|
6962be93a4
|
|||
|
be83594ea9
|
|||
|
b1f32e6f13
|
|||
|
c73c4d839a
|
|||
|
d7c61b2fdd
|
|||
|
a942d40849
|
|||
|
6c01b2128f
|
|||
|
e14bfba354
|
|||
|
d2c71d350d
|
|||
|
eb7c0a6858
|
|||
|
8f6d334af4
|
|||
|
1d67081792
|
|||
|
f304448fdb
|
|||
|
88307edc9a
|
|||
|
51d0809536
|
|||
|
8e07b6587b
|
|||
|
4f2435fcbd
|
|||
|
3fac917422
|
|||
|
096865dc3b
|
|||
|
29ec7571fe
|
|||
|
fc893ee08b
|
|||
|
cf7f830aad
|
|||
|
8213e4ad80
|
|||
|
e974a77e46
|
39
.appmod/.appcat/assessment-config.json
Normal file
39
.appmod/.appcat/assessment-config.json
Normal 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"
|
||||
}
|
||||
}
|
||||
601577
.appmod/.appcat/output_20251113_014740.json
Normal file
601577
.appmod/.appcat/output_20251113_014740.json
Normal file
File diff suppressed because one or more lines are too long
68
.appmod/.migration/plan.md
Normal file
68
.appmod/.migration/plan.md
Normal 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
|
||||
87
.appmod/.migration/progress.md
Normal file
87
.appmod/.migration/progress.md
Normal 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
|
||||
2101
.editorconfig
2101
.editorconfig
File diff suppressed because it is too large
Load Diff
2
.gitignore
vendored
2
.gitignore
vendored
@@ -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
|
||||
|
||||
3
.idea/.idea.Marechai/.idea/indexLayout.xml
generated
3
.idea/.idea.Marechai/.idea/indexLayout.xml
generated
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ContentModelUserStore">
|
||||
<component name="UserContentModel">
|
||||
<attachedFolders />
|
||||
<explicitIncludes />
|
||||
<explicitExcludes />
|
||||
</component>
|
||||
|
||||
8
.idea/.idea.Marechai/.idea/modules.xml
generated
8
.idea/.idea.Marechai/.idea/modules.xml
generated
@@ -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>
|
||||
14
.idea/.idea.Marechai/riderModule.iml
generated
14
.idea/.idea.Marechai/riderModule.iml
generated
@@ -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>
|
||||
@@ -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
17
Directory.Build.props
Normal 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
1
Directory.Build.targets
Normal file
@@ -0,0 +1 @@
|
||||
<Project></Project>
|
||||
48
Directory.Packages.props
Normal file
48
Directory.Packages.props
Normal file
@@ -0,0 +1,48 @@
|
||||
<?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"/>
|
||||
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.2.1"/>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
|
||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls" Version="7.1.2"/>
|
||||
<!-- Add more community toolkit references here -->
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
|
||||
<PackageVersion Include="Uno.CommunityToolkit.WinUI.UI.Controls" Version="7.1.200"/>
|
||||
<!-- Add more uno community toolkit references here -->
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
BIN
Documentation/Software Architecture Document.pdf
Normal file
BIN
Documentation/Software Architecture Document.pdf
Normal file
Binary file not shown.
29
Marechai.App/App.xaml
Normal file
29
Marechai.App/App.xaml
Normal file
@@ -0,0 +1,29 @@
|
||||
<Application x:Class="Marechai.App.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns: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" />
|
||||
<local:InvertBoolConverter x:Key="InvertBoolConverter" />
|
||||
<local:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
<local:InvertBoolToVisibilityConverter x:Key="InvertBoolToVisibilityConverter" />
|
||||
<local:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
<local:RolesListConverter x:Key="RolesListConverter" />
|
||||
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
|
||||
</Application>
|
||||
288
Marechai.App/App.xaml.cs
Normal file
288
Marechai.App/App.xaml.cs
Normal file
@@ -0,0 +1,288 @@
|
||||
using System.Net.Http;
|
||||
using Marechai.App.Presentation.ViewModels;
|
||||
using Marechai.App.Presentation.Views;
|
||||
using Marechai.App.Services;
|
||||
using Marechai.App.Services.Authentication;
|
||||
using Marechai.App.Services.Caching;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Uno.Extensions;
|
||||
using Uno.Extensions.Authentication;
|
||||
using Uno.Extensions.Configuration;
|
||||
using Uno.Extensions.Hosting;
|
||||
using Uno.Extensions.Http;
|
||||
using Uno.Extensions.Localization;
|
||||
using Uno.Extensions.Navigation;
|
||||
using Uno.UI;
|
||||
using CompanyDetailViewModel = Marechai.App.Presentation.ViewModels.CompanyDetailViewModel;
|
||||
using ComputersListViewModel = Marechai.App.Presentation.ViewModels.ComputersListViewModel;
|
||||
using ComputersViewModel = Marechai.App.Presentation.ViewModels.ComputersViewModel;
|
||||
using GpuDetailViewModel = Marechai.App.Presentation.ViewModels.GpuDetailViewModel;
|
||||
using GpuListViewModel = Marechai.App.Presentation.ViewModels.GpusListViewModel;
|
||||
using MachineViewViewModel = Marechai.App.Presentation.ViewModels.MachineViewViewModel;
|
||||
using MainViewModel = Marechai.App.Presentation.ViewModels.MainViewModel;
|
||||
using NewsViewModel = Marechai.App.Presentation.ViewModels.NewsViewModel;
|
||||
using PhotoDetailViewModel = Marechai.App.Presentation.ViewModels.PhotoDetailViewModel;
|
||||
using ProcessorDetailViewModel = Marechai.App.Presentation.ViewModels.ProcessorDetailViewModel;
|
||||
using ProcessorsListViewModel = Marechai.App.Presentation.ViewModels.ProcessorsListViewModel;
|
||||
using SettingsViewModel = Marechai.App.Presentation.ViewModels.SettingsViewModel;
|
||||
using SoundSynthDetailViewModel = Marechai.App.Presentation.ViewModels.SoundSynthDetailViewModel;
|
||||
using SoundSynthsListViewModel = Marechai.App.Presentation.ViewModels.SoundSynthsListViewModel;
|
||||
using LoginViewModel = Marechai.App.Presentation.ViewModels.LoginViewModel;
|
||||
|
||||
namespace Marechai.App;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes the singleton application object. This is the first line of authored code
|
||||
/// executed, and as such is the logical equivalent of main() or WinMain().
|
||||
/// </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) =>
|
||||
{
|
||||
services.AddTransient<DelegatingHandler,
|
||||
HttpAuthHandler>();
|
||||
#if DEBUG
|
||||
|
||||
// DelegatingHandler will be automatically injected
|
||||
services
|
||||
.AddTransient<DelegatingHandler,
|
||||
DebugHttpHandler>();
|
||||
#endif
|
||||
services.AddKiotaClientV2<ApiClient>(context,
|
||||
new EndpointOptions
|
||||
{
|
||||
Url = context.Configuration
|
||||
.GetSection("ApiClient:Url")
|
||||
.Value ??
|
||||
|
||||
// Fallback to a default URL if not configured
|
||||
"https://localhost:5023"
|
||||
});
|
||||
})
|
||||
.ConfigureServices((context, services) =>
|
||||
{
|
||||
// Register application services
|
||||
services
|
||||
.AddSingleton<IColorThemeService,
|
||||
ColorThemeService>();
|
||||
|
||||
services
|
||||
.AddSingleton<IAuthenticationService,
|
||||
AuthService>();
|
||||
|
||||
services.AddSingleton<ITokenService, TokenService>();
|
||||
services.AddSingleton<IJwtService, JwtService>();
|
||||
services.AddSingleton<FlagCache>();
|
||||
services.AddSingleton<CompanyLogoCache>();
|
||||
services.AddSingleton<MachinePhotoCache>();
|
||||
services.AddSingleton<NewsService>();
|
||||
services.AddSingleton<NewsViewModel>();
|
||||
services.AddSingleton<ComputersService>();
|
||||
services.AddSingleton<ComputersViewModel>();
|
||||
services.AddSingleton<ConsolesService>();
|
||||
services.AddSingleton<ConsolesViewModel>();
|
||||
services.AddSingleton<CompaniesService>();
|
||||
services.AddSingleton<CompaniesViewModel>();
|
||||
services.AddSingleton<CompanyDetailService>();
|
||||
services.AddSingleton<CompanyDetailViewModel>();
|
||||
services.AddSingleton<MachineViewViewModel>();
|
||||
services.AddSingleton<GpusService>();
|
||||
services.AddSingleton<ProcessorsService>();
|
||||
services.AddSingleton<SoundSynthsService>();
|
||||
services.AddTransient<PhotoDetailViewModel>();
|
||||
|
||||
services
|
||||
.AddSingleton<IComputersListFilterContext,
|
||||
ComputersListFilterContext>();
|
||||
|
||||
services
|
||||
.AddSingleton<IConsolesListFilterContext,
|
||||
ConsolesListFilterContext>();
|
||||
|
||||
services.AddTransient<ComputersListViewModel>();
|
||||
services.AddTransient<ConsolesListViewModel>();
|
||||
services.AddTransient<GpuListViewModel>();
|
||||
services.AddTransient<GpuDetailViewModel>();
|
||||
services.AddTransient<ProcessorsListViewModel>();
|
||||
services.AddTransient<ProcessorDetailViewModel>();
|
||||
services.AddTransient<SoundSynthsListViewModel>();
|
||||
services.AddTransient<SoundSynthDetailViewModel>();
|
||||
services.AddTransient<SettingsViewModel>();
|
||||
services.AddTransient<LoginViewModel>();
|
||||
})
|
||||
.UseNavigation(RegisterRoutes));
|
||||
|
||||
MainWindow = builder.Window;
|
||||
|
||||
#if DEBUG
|
||||
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<LoginPage, LoginViewModel>(),
|
||||
new ViewMap<NewsPage, NewsViewModel>(),
|
||||
new ViewMap<ComputersPage, ComputersViewModel>(),
|
||||
new ViewMap<ComputersListPage, ComputersListViewModel>(),
|
||||
new ViewMap<ConsolesPage, ConsolesViewModel>(),
|
||||
new ViewMap<ConsolesListPage, ConsolesListViewModel>(),
|
||||
new ViewMap<CompaniesPage, CompaniesViewModel>(),
|
||||
new ViewMap<CompanyDetailPage, CompanyDetailViewModel>(),
|
||||
new ViewMap<MachineViewPage, MachineViewViewModel>(),
|
||||
new ViewMap<PhotoDetailPage, PhotoDetailViewModel>(),
|
||||
new ViewMap<GpuListPage, GpuListViewModel>(),
|
||||
new ViewMap<GpuDetailPage, GpuDetailViewModel>(),
|
||||
new ViewMap<ProcessorListPage, ProcessorsListViewModel>(),
|
||||
new ViewMap<ProcessorDetailPage, ProcessorDetailViewModel>(),
|
||||
new ViewMap<SoundSynthListPage, SoundSynthsListViewModel>(),
|
||||
new ViewMap<SoundSynthDetailPage, SoundSynthDetailViewModel>(),
|
||||
new ViewMap<SettingsPage, SettingsViewModel>(),
|
||||
new ViewMap<UsersPage, UsersViewModel>(),
|
||||
new DataViewMap<SecondPage, SecondViewModel, Entity>());
|
||||
|
||||
routes.Register(new RouteMap("",
|
||||
views.FindByViewModel<ShellViewModel>(),
|
||||
Nested:
|
||||
[
|
||||
new RouteMap("Login", views.FindByViewModel<LoginViewModel>()),
|
||||
new RouteMap("Main",
|
||||
views.FindByViewModel<MainViewModel>(),
|
||||
true,
|
||||
Nested:
|
||||
[
|
||||
new RouteMap("News",
|
||||
views.FindByViewModel<NewsViewModel>(),
|
||||
true),
|
||||
new RouteMap("computers",
|
||||
views.FindByViewModel<ComputersViewModel>(),
|
||||
Nested
|
||||
:
|
||||
[
|
||||
new RouteMap("list-computers",
|
||||
views.FindByViewModel<
|
||||
ComputersListViewModel>()),
|
||||
new RouteMap("view",
|
||||
views.FindByViewModel<
|
||||
MachineViewViewModel>())
|
||||
]),
|
||||
new RouteMap("consoles",
|
||||
views.FindByViewModel<ConsolesViewModel>(),
|
||||
Nested
|
||||
:
|
||||
[
|
||||
new RouteMap("list-consoles",
|
||||
views.FindByViewModel<
|
||||
ConsolesListViewModel>())
|
||||
]),
|
||||
new RouteMap("companies",
|
||||
views.FindByViewModel<CompaniesViewModel>(),
|
||||
Nested
|
||||
:
|
||||
[
|
||||
new RouteMap("company-details",
|
||||
views.FindByViewModel<
|
||||
CompanyDetailViewModel>())
|
||||
]),
|
||||
new RouteMap("gpus",
|
||||
views.FindByViewModel<GpuListViewModel>(),
|
||||
Nested
|
||||
:
|
||||
[
|
||||
new RouteMap("gpu-details",
|
||||
views.FindByViewModel<
|
||||
GpuDetailViewModel>())
|
||||
]),
|
||||
new RouteMap("processors",
|
||||
views.FindByViewModel<ProcessorsListViewModel>(),
|
||||
Nested:
|
||||
[
|
||||
new RouteMap("processor-details",
|
||||
views.FindByViewModel<
|
||||
ProcessorDetailViewModel>())
|
||||
]),
|
||||
new RouteMap("sound-synths",
|
||||
views.FindByViewModel<
|
||||
SoundSynthsListViewModel>(),
|
||||
Nested:
|
||||
[
|
||||
new RouteMap("sound-synth-details",
|
||||
views.FindByViewModel<
|
||||
SoundSynthDetailViewModel>()),
|
||||
new RouteMap("machine-view",
|
||||
views.FindByViewModel<
|
||||
MachineViewViewModel>())
|
||||
]),
|
||||
new RouteMap("settings",
|
||||
views.FindByViewModel<SettingsViewModel>()),
|
||||
new RouteMap("users",
|
||||
views.FindByViewModel<UsersViewModel>()),
|
||||
new RouteMap("Second",
|
||||
views.FindByViewModel<SecondViewModel>())
|
||||
])
|
||||
]));
|
||||
}
|
||||
}
|
||||
42
Marechai.App/Assets/Icons/icon.svg
Normal file
42
Marechai.App/Assets/Icons/icon.svg
Normal 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 |
137
Marechai.App/Assets/Icons/icon_foreground.svg
Normal file
137
Marechai.App/Assets/Icons/icon_foreground.svg
Normal 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 |
32
Marechai.App/Assets/SharedAssets.md
Normal file
32
Marechai.App/Assets/SharedAssets.md
Normal 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 |
|
||||
137
Marechai.App/Assets/Splash/splash_screen.svg
Normal file
137
Marechai.App/Assets/Splash/splash_screen.svg
Normal 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 |
13
Marechai.App/GlobalUsings.cs
Normal file
13
Marechai.App/GlobalUsings.cs
Normal 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;
|
||||
99
Marechai.App/Marechai.App.csproj
Normal file
99
Marechai.App/Marechai.App.csproj
Normal file
@@ -0,0 +1,99 @@
|
||||
<Project Sdk="Uno.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFrameworks>net10.0-android;net10.0-browserwasm;net10.0-desktop</TargetFrameworks>
|
||||
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows')) Or $([MSBuild]::IsOSPlatform('macos'))">$(TargetFrameworks);net10.0-ios</TargetFrameworks>
|
||||
|
||||
<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;
|
||||
Storage;
|
||||
SkiaRenderer;
|
||||
Svg;
|
||||
</UnoFeatures>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Marechai.Data\Marechai.Data.csproj"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Humanizer"/>
|
||||
<PackageReference Include="System.IdentityModel.Tokens.Jwt"/>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Update="Presentation\Views\Shell.xaml.cs">
|
||||
<DependentUpon>Shell.xaml</DependentUpon>
|
||||
<IsDefaultItem>true</IsDefaultItem>
|
||||
</Compile>
|
||||
<Compile Update="Presentation\Views\SecondPage.xaml.cs">
|
||||
<DependentUpon>SecondPage.xaml</DependentUpon>
|
||||
<IsDefaultItem>true</IsDefaultItem>
|
||||
</Compile>
|
||||
<Compile Update="Presentation\Views\ComputersPage.xaml.cs">
|
||||
<DependentUpon>ComputersPage.xaml</DependentUpon>
|
||||
<IsDefaultItem>true</IsDefaultItem>
|
||||
</Compile>
|
||||
<Compile Update="Presentation\Views\MainPage.xaml.cs">
|
||||
<DependentUpon>MainPage.xaml</DependentUpon>
|
||||
<IsDefaultItem>true</IsDefaultItem>
|
||||
</Compile>
|
||||
<Compile Update="Presentation\Views\ComputersListPage.xaml.cs">
|
||||
<DependentUpon>ComputersListPage.xaml</DependentUpon>
|
||||
<IsDefaultItem>true</IsDefaultItem>
|
||||
</Compile>
|
||||
<Compile Update="Presentation\Views\NewsPage.xaml.cs">
|
||||
<DependentUpon>NewsPage.xaml</DependentUpon>
|
||||
<IsDefaultItem>true</IsDefaultItem>
|
||||
</Compile>
|
||||
<Compile Update="Presentation\Views\MachineViewPage.xaml.cs">
|
||||
<DependentUpon>MachineViewPage.xaml</DependentUpon>
|
||||
<IsDefaultItem>true</IsDefaultItem>
|
||||
</Compile>
|
||||
<Compile Update="Presentation\Components\Sidebar.xaml.cs">
|
||||
<DependentUpon>Sidebar.xaml</DependentUpon>
|
||||
<IsDefaultItem>true</IsDefaultItem>
|
||||
</Compile>
|
||||
<Compile Update="Presentation\Views\UsersPage.xaml.cs">
|
||||
<DependentUpon>UsersPage.xaml</DependentUpon>
|
||||
<IsDefaultItem>true</IsDefaultItem>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">
|
||||
<PackageReference Include="CommunityToolkit.WinUI.UI.Controls"/>
|
||||
<!-- Add more community toolkit references here -->
|
||||
</ItemGroup>
|
||||
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) != 'windows'">
|
||||
<PackageReference Include="Uno.CommunityToolkit.WinUI.UI.Controls"/>
|
||||
<!-- Add more uno community toolkit references here -->
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
</Project>
|
||||
6
Marechai.App/Models/AppConfig.cs
Normal file
6
Marechai.App/Models/AppConfig.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Marechai.App.Models;
|
||||
|
||||
public record AppConfig
|
||||
{
|
||||
public string? Environment { get; init; }
|
||||
}
|
||||
3
Marechai.App/Models/Entity.cs
Normal file
3
Marechai.App/Models/Entity.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Marechai.App.Models;
|
||||
|
||||
public record Entity(string Name);
|
||||
31
Marechai.App/Package.appxmanifest
Normal file
31
Marechai.App/Package.appxmanifest
Normal 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>
|
||||
4
Marechai.App/Platforms/Android/AndroidManifest.xml
Normal file
4
Marechai.App/Platforms/Android/AndroidManifest.xml
Normal 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>
|
||||
22
Marechai.App/Platforms/Android/Assets/AboutAssets.txt
Normal file
22
Marechai.App/Platforms/Android/Assets/AboutAssets.txt
Normal 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");
|
||||
28
Marechai.App/Platforms/Android/Main.Android.cs
Normal file
28
Marechai.App/Platforms/Android/Main.Android.cs
Normal 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)
|
||||
{
|
||||
}
|
||||
}
|
||||
22
Marechai.App/Platforms/Android/MainActivity.Android.cs
Normal file
22
Marechai.App/Platforms/Android/MainActivity.Android.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
47
Marechai.App/Platforms/Android/Resources/AboutResources.txt
Normal file
47
Marechai.App/Platforms/Android/Resources/AboutResources.txt
Normal 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.
|
||||
@@ -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>
|
||||
25
Marechai.App/Platforms/Android/Resources/values/Styles.xml
Normal file
25
Marechai.App/Platforms/Android/Resources/values/Styles.xml
Normal 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>
|
||||
2
Marechai.App/Platforms/Android/environment.conf
Normal file
2
Marechai.App/Platforms/Android/environment.conf
Normal 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
|
||||
21
Marechai.App/Platforms/Desktop/Program.cs
Normal file
21
Marechai.App/Platforms/Desktop/Program.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
10
Marechai.App/Platforms/WebAssembly/LinkerConfig.xml
Normal file
10
Marechai.App/Platforms/WebAssembly/LinkerConfig.xml
Normal 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>
|
||||
17
Marechai.App/Platforms/WebAssembly/Program.cs
Normal file
17
Marechai.App/Platforms/WebAssembly/Program.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
28
Marechai.App/Platforms/WebAssembly/WasmCSS/Fonts.css
Normal file
28
Marechai.App/Platforms/WebAssembly/WasmCSS/Fonts.css
Normal 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;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
var UnoAppManifest = {
|
||||
displayName: "Marechai.App"
|
||||
}
|
||||
10
Marechai.App/Platforms/WebAssembly/manifest.webmanifest
Normal file
10
Marechai.App/Platforms/WebAssembly/manifest.webmanifest
Normal 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": "/"
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
78
Marechai.App/Platforms/WebAssembly/wwwroot/web.config
Normal file
78
Marechai.App/Platforms/WebAssembly/wwwroot/web.config
Normal 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>
|
||||
6
Marechai.App/Platforms/iOS/Entitlements.plist
Normal file
6
Marechai.App/Platforms/iOS/Entitlements.plist
Normal 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>
|
||||
43
Marechai.App/Platforms/iOS/Info.plist
Normal file
43
Marechai.App/Platforms/iOS/Info.plist
Normal 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>
|
||||
18
Marechai.App/Platforms/iOS/Main.iOS.cs
Normal file
18
Marechai.App/Platforms/iOS/Main.iOS.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
@@ -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": ""
|
||||
}
|
||||
}
|
||||
41
Marechai.App/Platforms/iOS/PrivacyInfo.xcprivacy
Normal file
41
Marechai.App/Platforms/iOS/PrivacyInfo.xcprivacy
Normal 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>
|
||||
273
Marechai.App/Presentation/Components/Sidebar.xaml
Normal file
273
Marechai.App/Presentation/Components/Sidebar.xaml
Normal file
@@ -0,0 +1,273 @@
|
||||
<?xml version="1.0"
|
||||
encoding="utf-8"?>
|
||||
|
||||
<UserControl x:Class="Marechai.App.Presentation.Components.Sidebar"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="using:Marechai.App.Presentation.Converters"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="600"
|
||||
d:DesignWidth="280"
|
||||
Background="{ThemeResource NavigationViewDefaultPaneBackground}">
|
||||
<UserControl.Resources>
|
||||
<local:CollapseExpandIconConverter x:Key="CollapseExpandIconConverter" />
|
||||
<local:CollapseExpandTooltipConverter x:Key="CollapseExpandTooltipConverter" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<!-- Grid container - naturally responds to parent column width -->
|
||||
<Grid RowSpacing="0">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" /> <RowDefinition Height="*" /> <RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Sidebar Header with Collapse/Expand Button -->
|
||||
<Grid Grid.Row="0"
|
||||
Padding="8,8,8,8"
|
||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,0,0,1">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Sidebar Title - Hidden when collapsed -->
|
||||
<TextBlock Grid.Column="0"
|
||||
Text="Navigation"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
VerticalAlignment="Center"
|
||||
Padding="4,0,0,0"
|
||||
Visibility="{Binding SidebarContentVisible}" />
|
||||
|
||||
<!-- Collapse/Expand Button - Always visible -->
|
||||
<Button Grid.Column="1"
|
||||
Content="{Binding IsSidebarOpen, Converter={StaticResource CollapseExpandIconConverter}}"
|
||||
Command="{Binding ToggleSidebarCommand}"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||
Padding="8,8,8,8"
|
||||
CornerRadius="4"
|
||||
ToolTipService.ToolTip="{Binding IsSidebarOpen, Converter={StaticResource CollapseExpandTooltipConverter}}"
|
||||
FontSize="14"
|
||||
MinWidth="40"
|
||||
MinHeight="40"
|
||||
HorizontalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!-- Scrollable Navigation Items - Hidden when collapsed -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled"
|
||||
Padding="0"
|
||||
Visibility="{Binding SidebarContentVisible}">
|
||||
<StackPanel Orientation="Vertical"
|
||||
Spacing="0"
|
||||
Padding="0">
|
||||
<!-- News -->
|
||||
<Button Content="{Binding LocalizedStrings[News]}"
|
||||
Command="{Binding NavigateToNewsCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- Books -->
|
||||
<Button Content="{Binding LocalizedStrings[Books]}"
|
||||
Command="{Binding NavigateToBooksCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- Companies -->
|
||||
<Button Content="{Binding LocalizedStrings[Companies]}"
|
||||
Command="{Binding NavigateToCompaniesCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- Computers -->
|
||||
<Button Content="{Binding LocalizedStrings[Computers]}"
|
||||
Command="{Binding NavigateToComputersCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- Consoles -->
|
||||
<Button Content="{Binding LocalizedStrings[Consoles]}"
|
||||
Command="{Binding NavigateToConsolesCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- Documents -->
|
||||
<Button Content="{Binding LocalizedStrings[Documents]}"
|
||||
Command="{Binding NavigateToDocumentsCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- Dumps -->
|
||||
<Button Content="{Binding LocalizedStrings[Dumps]}"
|
||||
Command="{Binding NavigateToDumpsCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- Graphical Processing Units -->
|
||||
<Button Content="{Binding LocalizedStrings[GraphicalProcessingUnits]}"
|
||||
Command="{Binding NavigateToGraphicalProcessingUnitsCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- Magazines -->
|
||||
<Button Content="{Binding LocalizedStrings[Magazines]}"
|
||||
Command="{Binding NavigateToMagazinesCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- People -->
|
||||
<Button Content="{Binding LocalizedStrings[People]}"
|
||||
Command="{Binding NavigateToPeopleCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- Processors -->
|
||||
<Button Content="{Binding LocalizedStrings[Processors]}"
|
||||
Command="{Binding NavigateToProcessorsCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- Software -->
|
||||
<Button Content="{Binding LocalizedStrings[Software]}"
|
||||
Command="{Binding NavigateToSoftwareCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- Sound Synthesizers -->
|
||||
<Button Content="{Binding LocalizedStrings[SoundSynthesizers]}"
|
||||
Command="{Binding NavigateToSoundSynthesizersCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
<!-- Bottom Fixed Items - Hidden when collapsed -->
|
||||
<Grid Grid.Row="2"
|
||||
BorderBrush="{ThemeResource DividerStrokeColorDefaultBrush}"
|
||||
BorderThickness="0,1,0,0"
|
||||
Padding="0"
|
||||
Visibility="{Binding IsSidebarOpen}">
|
||||
<StackPanel Orientation="Vertical"
|
||||
Spacing="0">
|
||||
<!-- Users (Uberadmin only) -->
|
||||
<Button Content="User Management"
|
||||
Command="{Binding NavigateToUsersCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0"
|
||||
Visibility="{Binding IsUberadminUser, Converter={StaticResource BoolToVisibilityConverter}}" />
|
||||
|
||||
<!-- Login/Logout -->
|
||||
<Button Content="{Binding LoginLogoutButtonText}"
|
||||
Command="{Binding LoginLogoutCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
|
||||
<!-- Settings -->
|
||||
<Button Content="{Binding LocalizedStrings[Settings]}"
|
||||
Command="{Binding NavigateToSettingsCommand}"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Left"
|
||||
Padding="16,10,16,10"
|
||||
FontSize="13"
|
||||
Background="Transparent"
|
||||
Foreground="{ThemeResource TextFillColorPrimaryBrush}"
|
||||
BorderThickness="0"
|
||||
CornerRadius="0" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
11
Marechai.App/Presentation/Components/Sidebar.xaml.cs
Normal file
11
Marechai.App/Presentation/Components/Sidebar.xaml.cs
Normal file
@@ -0,0 +1,11 @@
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
|
||||
namespace Marechai.App.Presentation.Components;
|
||||
|
||||
public sealed partial class Sidebar : UserControl
|
||||
{
|
||||
public Sidebar()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
51
Marechai.App/Presentation/Converters/CompanyConverters.cs
Normal file
51
Marechai.App/Presentation/Converters/CompanyConverters.cs
Normal 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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
59
Marechai.App/Presentation/Converters/SidebarConverters.cs
Normal file
59
Marechai.App/Presentation/Converters/SidebarConverters.cs
Normal 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();
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
#nullable enable
|
||||
|
||||
namespace Marechai.App.Presentation.Models;
|
||||
|
||||
public class SoundSynthDetailNavigationParameter
|
||||
{
|
||||
public int SoundSynthId { get; set; }
|
||||
public object? NavigationSource { get; set; }
|
||||
}
|
||||
220
Marechai.App/Presentation/ViewModels/CompaniesViewModel.cs
Normal file
220
Marechai.App/Presentation/ViewModels/CompaniesViewModel.cs
Normal 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;
|
||||
}
|
||||
530
Marechai.App/Presentation/ViewModels/CompanyDetailViewModel.cs
Normal file
530
Marechai.App/Presentation/ViewModels/CompanyDetailViewModel.cs
Normal 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; }
|
||||
}
|
||||
250
Marechai.App/Presentation/ViewModels/ComputersListViewModel.cs
Normal file
250
Marechai.App/Presentation/ViewModels/ComputersListViewModel.cs
Normal 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;
|
||||
}
|
||||
207
Marechai.App/Presentation/ViewModels/ComputersViewModel.cs
Normal file
207
Marechai.App/Presentation/ViewModels/ComputersViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
248
Marechai.App/Presentation/ViewModels/ConsolesListViewModel.cs
Normal file
248
Marechai.App/Presentation/ViewModels/ConsolesListViewModel.cs
Normal 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;
|
||||
}
|
||||
207
Marechai.App/Presentation/ViewModels/ConsolesViewModel.cs
Normal file
207
Marechai.App/Presentation/ViewModels/ConsolesViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
387
Marechai.App/Presentation/ViewModels/GpuDetailViewModel.cs
Normal file
387
Marechai.App/Presentation/ViewModels/GpuDetailViewModel.cs
Normal 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";
|
||||
}
|
||||
211
Marechai.App/Presentation/ViewModels/GpusListViewModel.cs
Normal file
211
Marechai.App/Presentation/ViewModels/GpusListViewModel.cs
Normal 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; }
|
||||
}
|
||||
93
Marechai.App/Presentation/ViewModels/LoginViewModel.cs
Normal file
93
Marechai.App/Presentation/ViewModels/LoginViewModel.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Uno.Extensions.Authentication;
|
||||
using Uno.Extensions.Navigation;
|
||||
|
||||
namespace Marechai.App.Presentation.ViewModels;
|
||||
|
||||
public partial class LoginViewModel : ObservableObject
|
||||
{
|
||||
private readonly IAuthenticationService _authService;
|
||||
private readonly INavigator _navigator;
|
||||
private readonly IStringLocalizer _stringLocalizer;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _email = string.Empty;
|
||||
[ObservableProperty]
|
||||
private string? _errorMessage;
|
||||
[ObservableProperty]
|
||||
private bool _isLoggingIn;
|
||||
[ObservableProperty]
|
||||
private string _password = string.Empty;
|
||||
|
||||
public LoginViewModel(INavigator navigator, IAuthenticationService authService, IStringLocalizer stringLocalizer)
|
||||
{
|
||||
_navigator = navigator;
|
||||
_authService = authService;
|
||||
_stringLocalizer = stringLocalizer;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task LoginAsync()
|
||||
{
|
||||
// Clear previous error
|
||||
ErrorMessage = null;
|
||||
|
||||
// Validate inputs
|
||||
if(string.IsNullOrWhiteSpace(Email))
|
||||
{
|
||||
ErrorMessage = _stringLocalizer["LoginPage.Error.EmailRequired"];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(string.IsNullOrWhiteSpace(Password))
|
||||
{
|
||||
ErrorMessage = _stringLocalizer["LoginPage.Error.PasswordRequired"];
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
IsLoggingIn = true;
|
||||
|
||||
try
|
||||
{
|
||||
var credentials = new Dictionary<string, string>
|
||||
{
|
||||
["Email"] = Email,
|
||||
["Password"] = Password
|
||||
};
|
||||
|
||||
bool success = await _authService.LoginAsync(null, credentials, null, CancellationToken.None);
|
||||
|
||||
if(success)
|
||||
{
|
||||
// Navigate back to main page on successful login
|
||||
await _navigator.NavigateRouteAsync(this, "/Main");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Check if there's an error message in credentials
|
||||
if(credentials.TryGetValue("error", out string? error))
|
||||
ErrorMessage = error;
|
||||
else
|
||||
ErrorMessage = _stringLocalizer["LoginPage.Error.LoginFailed"];
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
ErrorMessage = ex.Message;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoggingIn = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ClearError() => ErrorMessage = null;
|
||||
}
|
||||
524
Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs
Normal file
524
Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs
Normal 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; }
|
||||
}
|
||||
263
Marechai.App/Presentation/ViewModels/MainViewModel.cs
Normal file
263
Marechai.App/Presentation/ViewModels/MainViewModel.cs
Normal file
@@ -0,0 +1,263 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Marechai.App.Services;
|
||||
using Marechai.App.Services.Authentication;
|
||||
using Uno.Extensions.Authentication;
|
||||
using Uno.Extensions.Navigation;
|
||||
using Uno.Extensions.Toolkit;
|
||||
|
||||
namespace Marechai.App.Presentation.ViewModels;
|
||||
|
||||
public partial class MainViewModel : ObservableObject
|
||||
{
|
||||
private readonly IAuthenticationService _authService;
|
||||
private readonly IJwtService _jwtService;
|
||||
private readonly IStringLocalizer _localizer;
|
||||
private readonly INavigator _navigator;
|
||||
private readonly ITokenService _tokenService;
|
||||
[ObservableProperty]
|
||||
private bool _isSidebarOpen = true;
|
||||
[ObservableProperty]
|
||||
private bool _isUberadminUser;
|
||||
[ObservableProperty]
|
||||
private Dictionary<string, string> _localizedStrings = new();
|
||||
[ObservableProperty]
|
||||
private string _loginLogoutButtonText = "";
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _name;
|
||||
[ObservableProperty]
|
||||
private NewsViewModel? _newsViewModel;
|
||||
[ObservableProperty]
|
||||
private bool _sidebarContentVisible = true;
|
||||
|
||||
public MainViewModel(IStringLocalizer localizer, IOptions<AppConfig> appInfo, INavigator navigator,
|
||||
NewsViewModel newsViewModel, IColorThemeService colorThemeService, IThemeService themeService,
|
||||
IAuthenticationService authService, IJwtService jwtService, ITokenService tokenService)
|
||||
{
|
||||
_navigator = navigator;
|
||||
_localizer = localizer;
|
||||
_authService = authService;
|
||||
_jwtService = jwtService;
|
||||
_tokenService = tokenService;
|
||||
NewsViewModel = newsViewModel;
|
||||
Title = "Marechai";
|
||||
Title += $" - {localizer["ApplicationName"]}";
|
||||
if(appInfo?.Value?.Environment != null) Title += $" - {appInfo.Value.Environment}";
|
||||
|
||||
GoToSecond = new AsyncRelayCommand(GoToSecondView);
|
||||
|
||||
// Initialize color theme service with theme service
|
||||
_ = InitializeThemeServicesAsync(colorThemeService, themeService);
|
||||
|
||||
// Initialize localized strings
|
||||
InitializeLocalizedStrings();
|
||||
|
||||
// Initialize commands
|
||||
NavigateToNewsCommand = new AsyncRelayCommand(NavigateToMainAsync);
|
||||
NavigateToBooksCommand = new AsyncRelayCommand(() => NavigateTo("books"));
|
||||
NavigateToCompaniesCommand = new AsyncRelayCommand(() => NavigateTo("companies"));
|
||||
NavigateToComputersCommand = new AsyncRelayCommand(() => NavigateTo("computers"));
|
||||
NavigateToConsolesCommand = new AsyncRelayCommand(() => NavigateTo("consoles"));
|
||||
NavigateToDocumentsCommand = new AsyncRelayCommand(() => NavigateTo("documents"));
|
||||
NavigateToDumpsCommand = new AsyncRelayCommand(() => NavigateTo("dumps"));
|
||||
NavigateToGraphicalProcessingUnitsCommand = new AsyncRelayCommand(() => NavigateTo("gpus"));
|
||||
NavigateToMagazinesCommand = new AsyncRelayCommand(() => NavigateTo("magazines"));
|
||||
NavigateToPeopleCommand = new AsyncRelayCommand(() => NavigateTo("people"));
|
||||
NavigateToProcessorsCommand = new AsyncRelayCommand(() => NavigateTo("processors"));
|
||||
NavigateToSoftwareCommand = new AsyncRelayCommand(() => NavigateTo("software"));
|
||||
NavigateToSoundSynthesizersCommand = new AsyncRelayCommand(() => NavigateTo("sound-synths"));
|
||||
NavigateToUsersCommand = new AsyncRelayCommand(() => NavigateTo("users"));
|
||||
NavigateToSettingsCommand = new AsyncRelayCommand(() => NavigateTo("settings"));
|
||||
LoginLogoutCommand = new RelayCommand(HandleLoginLogout);
|
||||
ToggleSidebarCommand = new RelayCommand(() => IsSidebarOpen = !IsSidebarOpen);
|
||||
|
||||
// Subscribe to authentication events
|
||||
_authService.LoggedOut += OnLoggedOut;
|
||||
|
||||
UpdateLoginLogoutButtonText();
|
||||
UpdateUberadminStatus();
|
||||
}
|
||||
|
||||
public string? Title { get; }
|
||||
|
||||
public ICommand GoToSecond { get; }
|
||||
|
||||
public ICommand NavigateToNewsCommand { get; }
|
||||
public ICommand NavigateToBooksCommand { get; }
|
||||
public ICommand NavigateToCompaniesCommand { get; }
|
||||
public ICommand NavigateToComputersCommand { get; }
|
||||
public ICommand NavigateToConsolesCommand { get; }
|
||||
public ICommand NavigateToDocumentsCommand { get; }
|
||||
public ICommand NavigateToDumpsCommand { get; }
|
||||
public ICommand NavigateToGraphicalProcessingUnitsCommand { get; }
|
||||
public ICommand NavigateToMagazinesCommand { get; }
|
||||
public ICommand NavigateToPeopleCommand { get; }
|
||||
public ICommand NavigateToProcessorsCommand { get; }
|
||||
public ICommand NavigateToSoftwareCommand { get; }
|
||||
public ICommand NavigateToSoundSynthesizersCommand { get; }
|
||||
public ICommand NavigateToUsersCommand { get; }
|
||||
public ICommand NavigateToSettingsCommand { get; }
|
||||
public ICommand LoginLogoutCommand { get; }
|
||||
public ICommand ToggleSidebarCommand { get; }
|
||||
|
||||
private async Task InitializeThemeServicesAsync(IColorThemeService colorThemeService, IThemeService themeService)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Wait for theme service to be ready
|
||||
await themeService.InitializeAsync();
|
||||
|
||||
// Set the theme service reference and reapply the saved theme
|
||||
colorThemeService.SetThemeService(themeService);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silently fail - theme will work but without refresh on startup
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeLocalizedStrings()
|
||||
{
|
||||
LocalizedStrings = new Dictionary<string, string>
|
||||
{
|
||||
{
|
||||
"News", _localizer["News"]
|
||||
},
|
||||
{
|
||||
"Books", _localizer["Books"]
|
||||
},
|
||||
{
|
||||
"Companies", _localizer["Companies"]
|
||||
},
|
||||
{
|
||||
"Computers", _localizer["Computers"]
|
||||
},
|
||||
{
|
||||
"Consoles", _localizer["Consoles"]
|
||||
},
|
||||
{
|
||||
"Documents", _localizer["Documents"]
|
||||
},
|
||||
{
|
||||
"Dumps", _localizer["Dumps"]
|
||||
},
|
||||
{
|
||||
"GraphicalProcessingUnits", _localizer["GraphicalProcessingUnits"]
|
||||
},
|
||||
{
|
||||
"Magazines", _localizer["Magazines"]
|
||||
},
|
||||
{
|
||||
"People", _localizer["People"]
|
||||
},
|
||||
{
|
||||
"Processors", _localizer["Processors"]
|
||||
},
|
||||
{
|
||||
"Software", _localizer["Software"]
|
||||
},
|
||||
{
|
||||
"SoundSynthesizers", _localizer["SoundSynthesizers"]
|
||||
},
|
||||
{
|
||||
"Settings", _localizer["Settings"]
|
||||
},
|
||||
{
|
||||
"Login", _localizer["Login"]
|
||||
},
|
||||
{
|
||||
"Logout", _localizer["Logout"]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private async void UpdateLoginLogoutButtonText()
|
||||
{
|
||||
bool isAuthenticated = await _authService.IsAuthenticated(CancellationToken.None);
|
||||
LoginLogoutButtonText = isAuthenticated ? LocalizedStrings["Logout"] : LocalizedStrings["Login"];
|
||||
}
|
||||
|
||||
private void UpdateUberadminStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
string token = _tokenService.GetToken();
|
||||
|
||||
if(!string.IsNullOrWhiteSpace(token))
|
||||
{
|
||||
IEnumerable<string> roles = _jwtService.GetRoles(token);
|
||||
IsUberadminUser = roles.Contains("Uberadmin", StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
else
|
||||
IsUberadminUser = false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
IsUberadminUser = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnLoggedOut(object? sender, EventArgs e)
|
||||
{
|
||||
// Update button text when user logs out
|
||||
UpdateLoginLogoutButtonText();
|
||||
UpdateUberadminStatus();
|
||||
}
|
||||
|
||||
public void RefreshAuthenticationState()
|
||||
{
|
||||
// Public method to refresh authentication state (called after login)
|
||||
UpdateLoginLogoutButtonText();
|
||||
UpdateUberadminStatus();
|
||||
}
|
||||
|
||||
private async void HandleLoginLogout()
|
||||
{
|
||||
bool isAuthenticated = await _authService.IsAuthenticated(CancellationToken.None);
|
||||
|
||||
if(isAuthenticated)
|
||||
{
|
||||
// Logout
|
||||
await _authService.LogoutAsync(null, CancellationToken.None);
|
||||
UpdateLoginLogoutButtonText();
|
||||
UpdateUberadminStatus();
|
||||
}
|
||||
else
|
||||
{
|
||||
// Navigate to login page - use absolute path starting from root
|
||||
await _navigator.NavigateRouteAsync(this, "/Login");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NavigateTo(string destination)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Navigate within the Main region using relative navigation
|
||||
// The "./" prefix means navigate within the current page's region
|
||||
await _navigator.NavigateRouteAsync(this, $"./{destination}");
|
||||
}
|
||||
catch(Exception)
|
||||
{
|
||||
// Navigation error - fail silently for now
|
||||
// TODO: Add error handling/logging
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NavigateToMainAsync()
|
||||
{
|
||||
// Navigate to News page (the default/home page)
|
||||
await NavigateTo("News");
|
||||
}
|
||||
|
||||
private async Task GoToSecondView()
|
||||
{
|
||||
// Navigate to Second view model providing qualifier and data
|
||||
await _navigator.NavigateViewModelAsync<SecondViewModel>(this, "Second", new Entity(Name ?? ""));
|
||||
}
|
||||
}
|
||||
175
Marechai.App/Presentation/ViewModels/NewsViewModel.cs
Normal file
175
Marechai.App/Presentation/ViewModels/NewsViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
425
Marechai.App/Presentation/ViewModels/PhotoDetailViewModel.cs
Normal file
425
Marechai.App/Presentation/ViewModels/PhotoDetailViewModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
294
Marechai.App/Presentation/ViewModels/ProcessorDetailViewModel.cs
Normal file
294
Marechai.App/Presentation/ViewModels/ProcessorDetailViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
177
Marechai.App/Presentation/ViewModels/ProcessorsListViewModel.cs
Normal file
177
Marechai.App/Presentation/ViewModels/ProcessorsListViewModel.cs
Normal 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;
|
||||
}
|
||||
3
Marechai.App/Presentation/ViewModels/SecondViewModel.cs
Normal file
3
Marechai.App/Presentation/ViewModels/SecondViewModel.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
namespace Marechai.App.Presentation.ViewModels;
|
||||
|
||||
public record SecondViewModel(Entity Entity) {}
|
||||
190
Marechai.App/Presentation/ViewModels/SettingsViewModel.cs
Normal file
190
Marechai.App/Presentation/ViewModels/SettingsViewModel.cs
Normal file
@@ -0,0 +1,190 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.App.Services;
|
||||
using Uno.Extensions.Toolkit;
|
||||
|
||||
namespace Marechai.App.Presentation.ViewModels;
|
||||
|
||||
public partial class SettingsViewModel : ObservableObject
|
||||
{
|
||||
private readonly IColorThemeService _colorThemeService;
|
||||
private readonly IStringLocalizer _localizer;
|
||||
private readonly IThemeService _themeService;
|
||||
|
||||
[ObservableProperty]
|
||||
private List<ColorThemeOption> _availableColorThemes = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private List<ThemeOption> _availableThemes = new();
|
||||
|
||||
[ObservableProperty]
|
||||
private ColorThemeOption _selectedColorTheme;
|
||||
|
||||
[ObservableProperty]
|
||||
private ThemeOption _selectedTheme;
|
||||
|
||||
public SettingsViewModel(IStringLocalizer localizer, IThemeService themeService,
|
||||
IColorThemeService colorThemeService)
|
||||
{
|
||||
_localizer = localizer;
|
||||
_themeService = themeService;
|
||||
_colorThemeService = colorThemeService;
|
||||
Title = _localizer["Settings"];
|
||||
|
||||
// Initialize immediately to ensure UI is populated
|
||||
InitializeOptions();
|
||||
|
||||
// Wait for theme service to initialize
|
||||
_ = InitializeThemeServiceAsync();
|
||||
}
|
||||
|
||||
public string Title { get; }
|
||||
|
||||
private async Task InitializeThemeServiceAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
await _themeService.InitializeAsync();
|
||||
|
||||
// Ensure the color theme service has a reference to the theme service
|
||||
_colorThemeService.SetThemeService(_themeService);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Theme service might already be initialized
|
||||
}
|
||||
}
|
||||
|
||||
private void InitializeOptions()
|
||||
{
|
||||
// Initialize Light/Dark/System Themes
|
||||
AvailableThemes = new List<ThemeOption>
|
||||
{
|
||||
new()
|
||||
{
|
||||
Theme = AppTheme.Light,
|
||||
DisplayName = _localizer["LightTheme"]
|
||||
},
|
||||
new()
|
||||
{
|
||||
Theme = AppTheme.Dark,
|
||||
DisplayName = _localizer["DarkTheme"]
|
||||
},
|
||||
new()
|
||||
{
|
||||
Theme = AppTheme.System,
|
||||
DisplayName = _localizer["SystemTheme"]
|
||||
}
|
||||
};
|
||||
|
||||
// Initialize Color Themes
|
||||
AvailableColorThemes = new List<ColorThemeOption>
|
||||
{
|
||||
new()
|
||||
{
|
||||
ThemeName = "Default",
|
||||
DisplayName = _localizer["DefaultColorTheme"]
|
||||
},
|
||||
new()
|
||||
{
|
||||
ThemeName = "Windows311",
|
||||
DisplayName = _localizer["Windows311Theme"]
|
||||
},
|
||||
new()
|
||||
{
|
||||
ThemeName = "MacOS9",
|
||||
DisplayName = _localizer["MacOS9Theme"]
|
||||
},
|
||||
new()
|
||||
{
|
||||
ThemeName = "DOS",
|
||||
DisplayName = _localizer["DOSTheme"]
|
||||
},
|
||||
new()
|
||||
{
|
||||
ThemeName = "Amiga",
|
||||
DisplayName = _localizer["AmigaTheme"]
|
||||
},
|
||||
new()
|
||||
{
|
||||
ThemeName = "CDE",
|
||||
DisplayName = _localizer["CDETheme"]
|
||||
}
|
||||
};
|
||||
|
||||
// Try to load saved preferences
|
||||
LoadSavedPreferences();
|
||||
}
|
||||
|
||||
private async void LoadSavedPreferences()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Load current theme from ThemeService
|
||||
AppTheme currentTheme = _themeService.Theme;
|
||||
|
||||
SelectedTheme = AvailableThemes.FirstOrDefault(t => t.Theme == currentTheme) ??
|
||||
AvailableThemes.FirstOrDefault(t => t.Theme == AppTheme.System) ?? AvailableThemes.First();
|
||||
|
||||
// Load current color theme
|
||||
string currentColorTheme = _colorThemeService.CurrentColorTheme;
|
||||
|
||||
SelectedColorTheme = AvailableColorThemes.FirstOrDefault(t => t.ThemeName == currentColorTheme) ??
|
||||
AvailableColorThemes.First();
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If loading fails, use defaults
|
||||
SelectedTheme = AvailableThemes.FirstOrDefault(t => t.Theme == AppTheme.System) ?? AvailableThemes.First();
|
||||
SelectedColorTheme = AvailableColorThemes.First();
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedThemeChanged(ThemeOption value)
|
||||
{
|
||||
if(value != null) ApplyTheme(value);
|
||||
}
|
||||
|
||||
partial void OnSelectedColorThemeChanged(ColorThemeOption value)
|
||||
{
|
||||
if(value != null) ApplyColorTheme(value);
|
||||
}
|
||||
|
||||
private async void ApplyTheme(ThemeOption theme)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Apply theme immediately using ThemeService
|
||||
await _themeService.SetThemeAsync(theme.Theme);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silently fail
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyColorTheme(ColorThemeOption colorTheme)
|
||||
{
|
||||
try
|
||||
{
|
||||
_colorThemeService.ApplyColorTheme(colorTheme.ThemeName);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silently fail
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class ThemeOption
|
||||
{
|
||||
public AppTheme Theme { get; set; }
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
public class ColorThemeOption
|
||||
{
|
||||
public string ThemeName { get; set; } = string.Empty;
|
||||
public string DisplayName { get; set; } = string.Empty;
|
||||
}
|
||||
13
Marechai.App/Presentation/ViewModels/ShellViewModel.cs
Normal file
13
Marechai.App/Presentation/ViewModels/ShellViewModel.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
using Uno.Extensions.Navigation;
|
||||
|
||||
namespace Marechai.App.Presentation.ViewModels;
|
||||
|
||||
public class ShellViewModel
|
||||
{
|
||||
private readonly INavigator _navigator;
|
||||
|
||||
public ShellViewModel(INavigator navigator) => _navigator = navigator;
|
||||
|
||||
// Users can browse the app without authentication
|
||||
// Login is available from the sidebar when needed
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
135
Marechai.App/Presentation/ViewModels/SoundSynthsListViewModel.cs
Normal file
135
Marechai.App/Presentation/ViewModels/SoundSynthsListViewModel.cs
Normal file
@@ -0,0 +1,135 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.App.Presentation.Models;
|
||||
using Marechai.App.Services;
|
||||
using Uno.Extensions.Navigation;
|
||||
|
||||
namespace Marechai.App.Presentation.ViewModels;
|
||||
|
||||
public partial class SoundSynthsListViewModel : ObservableObject
|
||||
{
|
||||
private readonly ILogger<SoundSynthsListViewModel> _logger;
|
||||
private readonly INavigator _navigator;
|
||||
private readonly SoundSynthsService _soundSynthsService;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _errorMessage = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _hasError;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isDataLoaded;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isLoading = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<SoundSynthListItem> _soundSynths = [];
|
||||
|
||||
public SoundSynthsListViewModel(SoundSynthsService soundSynthsService, INavigator navigator,
|
||||
ILogger<SoundSynthsListViewModel> logger)
|
||||
{
|
||||
_soundSynthsService = soundSynthsService;
|
||||
_navigator = navigator;
|
||||
_logger = logger;
|
||||
LoadData = new AsyncRelayCommand(LoadDataAsync);
|
||||
NavigateToSoundSynthCommand = new AsyncRelayCommand<SoundSynthListItem>(NavigateToSoundSynthAsync);
|
||||
}
|
||||
|
||||
public IAsyncRelayCommand LoadData { get; }
|
||||
public IAsyncRelayCommand<SoundSynthListItem> NavigateToSoundSynthCommand { get; }
|
||||
|
||||
private async Task LoadDataAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
IsDataLoaded = false;
|
||||
HasError = false;
|
||||
ErrorMessage = string.Empty;
|
||||
|
||||
List<SoundSynthDto> soundSynths = await _soundSynthsService.GetAllSoundSynthsAsync();
|
||||
|
||||
// Separate special sound synths from regular ones
|
||||
var specialSoundSynths = new List<SoundSynthListItem>();
|
||||
var regularSoundSynths = new List<SoundSynthListItem>();
|
||||
|
||||
foreach(SoundSynthDto ss in soundSynths)
|
||||
{
|
||||
string displayName = ss.Name ?? "Unknown";
|
||||
|
||||
// Replace special database name
|
||||
if(displayName == "DB_SOFTWARE") displayName = "Software";
|
||||
|
||||
var soundSynthItem = new SoundSynthListItem
|
||||
{
|
||||
Id = ss.Id ?? 0,
|
||||
Name = displayName,
|
||||
Company = ss.Company ?? "Unknown",
|
||||
IsSpecial = ss.Name == "DB_SOFTWARE"
|
||||
};
|
||||
|
||||
if(soundSynthItem.IsSpecial)
|
||||
specialSoundSynths.Add(soundSynthItem);
|
||||
else
|
||||
regularSoundSynths.Add(soundSynthItem);
|
||||
|
||||
_logger.LogInformation("Sound Synth: {Name}, Company: {Company}, ID: {Id}, IsSpecial: {IsSpecial}",
|
||||
displayName,
|
||||
ss.Company,
|
||||
ss.Id,
|
||||
soundSynthItem.IsSpecial);
|
||||
}
|
||||
|
||||
// Sort regular sound synths alphabetically by name
|
||||
regularSoundSynths.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
// Add special sound synths first (Software), then regular sound synths
|
||||
SoundSynths.Clear();
|
||||
|
||||
foreach(SoundSynthListItem ss in specialSoundSynths) SoundSynths.Add(ss);
|
||||
|
||||
foreach(SoundSynthListItem ss in regularSoundSynths) SoundSynths.Add(ss);
|
||||
|
||||
_logger.LogInformation("Successfully loaded {Count} Sound Synthesizers", SoundSynths.Count);
|
||||
IsDataLoaded = true;
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading Sound Synthesizers");
|
||||
ErrorMessage = "Failed to load Sound Synthesizers. Please try again later.";
|
||||
HasError = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task NavigateToSoundSynthAsync(SoundSynthListItem? item)
|
||||
{
|
||||
if(item == null) return;
|
||||
|
||||
_logger.LogInformation("Navigating to Sound Synthesizer {SoundSynthId}", item.Id);
|
||||
|
||||
await _navigator.NavigateViewModelAsync<SoundSynthDetailViewModel>(this,
|
||||
data: new SoundSynthDetailNavigationParameter
|
||||
{
|
||||
SoundSynthId = item.Id,
|
||||
NavigationSource = this
|
||||
});
|
||||
}
|
||||
|
||||
public class SoundSynthListItem
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string? Company { get; set; }
|
||||
public bool IsSpecial { get; set; }
|
||||
}
|
||||
}
|
||||
440
Marechai.App/Presentation/ViewModels/UsersViewModel.cs
Normal file
440
Marechai.App/Presentation/ViewModels/UsersViewModel.cs
Normal file
@@ -0,0 +1,440 @@
|
||||
#nullable enable
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.App.Services.Authentication;
|
||||
|
||||
namespace Marechai.App.Presentation.ViewModels;
|
||||
|
||||
/// <summary>
|
||||
/// ViewModel for user management page (Uberadmin only)
|
||||
/// </summary>
|
||||
public partial class UsersViewModel : ObservableObject
|
||||
{
|
||||
private readonly ApiClient _apiClient;
|
||||
private readonly IJwtService _jwtService;
|
||||
private readonly IStringLocalizer _localizer;
|
||||
private readonly ILogger<UsersViewModel> _logger;
|
||||
private readonly ITokenService _tokenService;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<string> _availableRoles = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private string _confirmPassword = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _dialogTitle = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _dialogType = string.Empty;
|
||||
|
||||
private string? _editingUserId;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _email = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _errorMessage = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _hasError;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isDataLoaded;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isLoading;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _password = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _phoneNumber = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private string? _selectedRole;
|
||||
|
||||
[ObservableProperty]
|
||||
private UserDto? _selectedUser;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _userName = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<string> _userRoles = [];
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<UserDto> _users = [];
|
||||
|
||||
public UsersViewModel(ApiClient apiClient, IJwtService jwtService, ITokenService tokenService,
|
||||
ILogger<UsersViewModel> logger, IStringLocalizer localizer)
|
||||
{
|
||||
_apiClient = apiClient;
|
||||
_jwtService = jwtService;
|
||||
_tokenService = tokenService;
|
||||
_logger = logger;
|
||||
_localizer = localizer;
|
||||
|
||||
LoadUsersCommand = new AsyncRelayCommand(LoadUsersAsync);
|
||||
DeleteUserCommand = new AsyncRelayCommand<UserDto>(DeleteUserAsync);
|
||||
OpenAddUserDialogCommand = new AsyncRelayCommand(OpenAddUserDialogAsync);
|
||||
OpenEditUserDialogCommand = new AsyncRelayCommand<UserDto>(OpenEditUserDialogAsync);
|
||||
OpenChangePasswordDialogCommand = new AsyncRelayCommand<UserDto>(OpenChangePasswordDialogAsync);
|
||||
OpenManageRolesDialogCommand = new AsyncRelayCommand<UserDto>(OpenManageRolesDialogAsync);
|
||||
SaveUserCommand = new AsyncRelayCommand(SaveUserAsync);
|
||||
SavePasswordCommand = new AsyncRelayCommand(SavePasswordAsync);
|
||||
AddRoleCommand = new AsyncRelayCommand(AddRoleAsync);
|
||||
RemoveRoleCommand = new AsyncRelayCommand<string>(RemoveRoleAsync);
|
||||
CloseDialogCommand = new RelayCommand(CloseDialog);
|
||||
}
|
||||
|
||||
public IAsyncRelayCommand LoadUsersCommand { get; }
|
||||
public IAsyncRelayCommand<UserDto> DeleteUserCommand { get; }
|
||||
public IAsyncRelayCommand OpenAddUserDialogCommand { get; }
|
||||
public IAsyncRelayCommand<UserDto> OpenEditUserDialogCommand { get; }
|
||||
public IAsyncRelayCommand<UserDto> OpenChangePasswordDialogCommand { get; }
|
||||
public IAsyncRelayCommand<UserDto> OpenManageRolesDialogCommand { get; }
|
||||
public IAsyncRelayCommand SaveUserCommand { get; }
|
||||
public IAsyncRelayCommand SavePasswordCommand { get; }
|
||||
public IAsyncRelayCommand AddRoleCommand { get; }
|
||||
public IAsyncRelayCommand<string> RemoveRoleCommand { get; }
|
||||
public IRelayCommand CloseDialogCommand { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the current user is Uberadmin
|
||||
/// </summary>
|
||||
public bool IsUberadmin
|
||||
{
|
||||
get
|
||||
{
|
||||
string? token = _tokenService.GetToken();
|
||||
IEnumerable<string>? roles = _jwtService.GetRoles(token);
|
||||
|
||||
return roles.Contains("Uberadmin", StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
|
||||
public event EventHandler<string>? ShowDialogRequested;
|
||||
|
||||
private async Task LoadUsersAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
IsLoading = true;
|
||||
HasError = false;
|
||||
ErrorMessage = string.Empty;
|
||||
Users.Clear();
|
||||
|
||||
List<UserDto>? usersResponse = await _apiClient.Users.GetAsync();
|
||||
|
||||
if(usersResponse != null)
|
||||
{
|
||||
foreach(UserDto user in usersResponse) Users.Add(user);
|
||||
|
||||
IsDataLoaded = true;
|
||||
}
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error loading users");
|
||||
ErrorMessage = _localizer["Failed to load users. Please try again."];
|
||||
HasError = true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DeleteUserAsync(UserDto? user)
|
||||
{
|
||||
if(user?.Id == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
await _apiClient.Users[user.Id].DeleteAsync();
|
||||
await LoadUsersAsync();
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error deleting user");
|
||||
ErrorMessage = _localizer["Failed to delete user."];
|
||||
HasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenAddUserDialogAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_editingUserId = null;
|
||||
DialogTitle = _localizer["Add User"];
|
||||
DialogType = "AddEdit";
|
||||
Email = string.Empty;
|
||||
UserName = string.Empty;
|
||||
PhoneNumber = string.Empty;
|
||||
Password = string.Empty;
|
||||
ConfirmPassword = string.Empty;
|
||||
ShowDialogRequested?.Invoke(this, "AddEdit");
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error opening add user dialog");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenEditUserDialogAsync(UserDto? user)
|
||||
{
|
||||
if(user?.Id == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
_editingUserId = user.Id;
|
||||
DialogTitle = _localizer["Edit User"];
|
||||
DialogType = "AddEdit";
|
||||
Email = user.Email ?? string.Empty;
|
||||
UserName = user.UserName ?? string.Empty;
|
||||
PhoneNumber = user.PhoneNumber ?? string.Empty;
|
||||
Password = string.Empty;
|
||||
ConfirmPassword = string.Empty;
|
||||
ShowDialogRequested?.Invoke(this, "AddEdit");
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error opening edit user dialog");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenChangePasswordDialogAsync(UserDto? user)
|
||||
{
|
||||
if(user?.Id == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
_editingUserId = user.Id;
|
||||
DialogTitle = _localizer["Change Password"];
|
||||
DialogType = "Password";
|
||||
Password = string.Empty;
|
||||
ConfirmPassword = string.Empty;
|
||||
ShowDialogRequested?.Invoke(this, "Password");
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error opening change password dialog");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task OpenManageRolesDialogAsync(UserDto? user)
|
||||
{
|
||||
if(user?.Id == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
_editingUserId = user.Id;
|
||||
DialogTitle = _localizer["Manage Roles"];
|
||||
DialogType = "Roles";
|
||||
|
||||
// Load available roles
|
||||
List<string>? rolesResponse = await _apiClient.Users.Roles.GetAsync();
|
||||
AvailableRoles.Clear();
|
||||
|
||||
if(rolesResponse != null)
|
||||
{
|
||||
foreach(string role in rolesResponse)
|
||||
if(!string.IsNullOrWhiteSpace(role))
|
||||
AvailableRoles.Add(role);
|
||||
}
|
||||
|
||||
_logger.LogInformation($"Loaded {AvailableRoles.Count} available roles");
|
||||
|
||||
// Load user's current roles
|
||||
UserRoles.Clear();
|
||||
|
||||
if(user.Roles != null)
|
||||
foreach(string role in user.Roles)
|
||||
UserRoles.Add(role);
|
||||
|
||||
ShowDialogRequested?.Invoke(this, "Roles");
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error opening manage roles dialog");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SaveUserAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(Email) || string.IsNullOrWhiteSpace(UserName))
|
||||
{
|
||||
ErrorMessage = _localizer["Email and username are required."];
|
||||
HasError = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(_editingUserId == null)
|
||||
{
|
||||
// Create new user
|
||||
if(string.IsNullOrWhiteSpace(Password))
|
||||
{
|
||||
ErrorMessage = _localizer["Password is required for new users."];
|
||||
HasError = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(Password != ConfirmPassword)
|
||||
{
|
||||
ErrorMessage = _localizer["Passwords do not match."];
|
||||
HasError = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var createRequest = new CreateUserRequest
|
||||
{
|
||||
Email = Email,
|
||||
UserName = UserName,
|
||||
PhoneNumber = PhoneNumber,
|
||||
Password = Password
|
||||
};
|
||||
|
||||
await _apiClient.Users.PostAsync(createRequest);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update existing user
|
||||
var updateRequest = new UpdateUserRequest
|
||||
{
|
||||
Email = Email,
|
||||
UserName = UserName,
|
||||
PhoneNumber = PhoneNumber
|
||||
};
|
||||
|
||||
await _apiClient.Users[_editingUserId].PutAsync(updateRequest);
|
||||
}
|
||||
|
||||
CloseDialog();
|
||||
await LoadUsersAsync();
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error saving user");
|
||||
ErrorMessage = _localizer["Failed to save user."];
|
||||
HasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SavePasswordAsync()
|
||||
{
|
||||
if(_editingUserId == null) return;
|
||||
|
||||
try
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(Password))
|
||||
{
|
||||
ErrorMessage = _localizer["Password is required."];
|
||||
HasError = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if(Password != ConfirmPassword)
|
||||
{
|
||||
ErrorMessage = _localizer["Passwords do not match."];
|
||||
HasError = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var changePasswordRequest = new ChangePasswordRequest
|
||||
{
|
||||
NewPassword = Password
|
||||
};
|
||||
|
||||
await _apiClient.Users[_editingUserId].Password.PostAsync(changePasswordRequest);
|
||||
|
||||
CloseDialog();
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error changing password");
|
||||
ErrorMessage = _localizer["Failed to change password."];
|
||||
HasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AddRoleAsync()
|
||||
{
|
||||
if(_editingUserId == null || string.IsNullOrWhiteSpace(SelectedRole)) return;
|
||||
|
||||
try
|
||||
{
|
||||
if(UserRoles.Contains(SelectedRole))
|
||||
{
|
||||
ErrorMessage = _localizer["User already has this role."];
|
||||
HasError = true;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var addRoleRequest = new UserRoleRequest
|
||||
{
|
||||
RoleName = SelectedRole
|
||||
};
|
||||
|
||||
await _apiClient.Users[_editingUserId].Roles.PostAsync(addRoleRequest);
|
||||
UserRoles.Add(SelectedRole);
|
||||
|
||||
// Reload users to refresh the list
|
||||
await LoadUsersAsync();
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error adding role");
|
||||
ErrorMessage = _localizer["Failed to add role."];
|
||||
HasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveRoleAsync(string? role)
|
||||
{
|
||||
if(_editingUserId == null || string.IsNullOrWhiteSpace(role)) return;
|
||||
|
||||
try
|
||||
{
|
||||
await _apiClient.Users[_editingUserId].Roles[role].DeleteAsync();
|
||||
UserRoles.Remove(role);
|
||||
|
||||
// Reload users to refresh the list
|
||||
await LoadUsersAsync();
|
||||
}
|
||||
catch(Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error removing role");
|
||||
ErrorMessage = _localizer["Failed to remove role."];
|
||||
HasError = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void CloseDialog()
|
||||
{
|
||||
DialogType = string.Empty;
|
||||
_editingUserId = null;
|
||||
Email = string.Empty;
|
||||
UserName = string.Empty;
|
||||
PhoneNumber = string.Empty;
|
||||
Password = string.Empty;
|
||||
ConfirmPassword = string.Empty;
|
||||
UserRoles.Clear();
|
||||
AvailableRoles.Clear();
|
||||
HasError = false;
|
||||
ErrorMessage = string.Empty;
|
||||
}
|
||||
}
|
||||
139
Marechai.App/Presentation/Views/CompaniesPage.xaml
Normal file
139
Marechai.App/Presentation/Views/CompaniesPage.xaml
Normal 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>
|
||||
59
Marechai.App/Presentation/Views/CompaniesPage.xaml.cs
Normal file
59
Marechai.App/Presentation/Views/CompaniesPage.xaml.cs
Normal 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
|
||||
}
|
||||
}
|
||||
351
Marechai.App/Presentation/Views/CompanyDetailPage.xaml
Normal file
351
Marechai.App/Presentation/Views/CompanyDetailPage.xaml
Normal 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>
|
||||
77
Marechai.App/Presentation/Views/CompanyDetailPage.xaml.cs
Normal file
77
Marechai.App/Presentation/Views/CompanyDetailPage.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
261
Marechai.App/Presentation/Views/ComputersListPage.xaml
Normal file
261
Marechai.App/Presentation/Views/ComputersListPage.xaml
Normal 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=""
|
||||
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=""
|
||||
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=""
|
||||
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=""
|
||||
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>
|
||||
37
Marechai.App/Presentation/Views/ComputersListPage.xaml.cs
Normal file
37
Marechai.App/Presentation/Views/ComputersListPage.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
311
Marechai.App/Presentation/Views/ComputersPage.xaml
Normal file
311
Marechai.App/Presentation/Views/ComputersPage.xaml
Normal 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>
|
||||
44
Marechai.App/Presentation/Views/ComputersPage.xaml.cs
Normal file
44
Marechai.App/Presentation/Views/ComputersPage.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
261
Marechai.App/Presentation/Views/ConsolesListPage.xaml
Normal file
261
Marechai.App/Presentation/Views/ConsolesListPage.xaml
Normal 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=""
|
||||
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=""
|
||||
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=""
|
||||
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=""
|
||||
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>
|
||||
37
Marechai.App/Presentation/Views/ConsolesListPage.xaml.cs
Normal file
37
Marechai.App/Presentation/Views/ConsolesListPage.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
311
Marechai.App/Presentation/Views/ConsolesPage.xaml
Normal file
311
Marechai.App/Presentation/Views/ConsolesPage.xaml
Normal 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>
|
||||
44
Marechai.App/Presentation/Views/ConsolesPage.xaml.cs
Normal file
44
Marechai.App/Presentation/Views/ConsolesPage.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
374
Marechai.App/Presentation/Views/GpuDetailPage.xaml
Normal file
374
Marechai.App/Presentation/Views/GpuDetailPage.xaml
Normal 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>
|
||||
98
Marechai.App/Presentation/Views/GpuDetailPage.xaml.cs
Normal file
98
Marechai.App/Presentation/Views/GpuDetailPage.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
207
Marechai.App/Presentation/Views/GpuListPage.xaml
Normal file
207
Marechai.App/Presentation/Views/GpuListPage.xaml
Normal 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=""
|
||||
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=""
|
||||
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>
|
||||
37
Marechai.App/Presentation/Views/GpuListPage.xaml.cs
Normal file
37
Marechai.App/Presentation/Views/GpuListPage.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
209
Marechai.App/Presentation/Views/LoginPage.xaml
Normal file
209
Marechai.App/Presentation/Views/LoginPage.xaml
Normal file
@@ -0,0 +1,209 @@
|
||||
<?xml version="1.0"
|
||||
encoding="utf-8"?>
|
||||
|
||||
<Page x:Class="Marechai.App.Presentation.Views.LoginPage"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="using:Marechai.App.Presentation.ViewModels"
|
||||
xmlns:utu="using:Uno.Toolkit.UI"
|
||||
mc:Ignorable="d"
|
||||
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
|
||||
d:DataContext="{d:DesignInstance vm:LoginViewModel}">
|
||||
|
||||
<Grid utu:SafeArea.Insets="VisibleBounds">
|
||||
<!-- Center content on screen -->
|
||||
<Grid HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
MaxWidth="480"
|
||||
Padding="32">
|
||||
|
||||
<!-- Login Card with Mac OS 9 styling -->
|
||||
<Border Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||
BorderThickness="2"
|
||||
CornerRadius="8"
|
||||
Padding="0"
|
||||
Translation="0,0,16">
|
||||
<Border.Shadow>
|
||||
<ThemeShadow />
|
||||
</Border.Shadow>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" /> <RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- Header with Mac OS 9 Title Bar Style -->
|
||||
<Border Grid.Row="0"
|
||||
Background="{ThemeResource AccentFillColorDefaultBrush}"
|
||||
Padding="16,12"
|
||||
CornerRadius="6,6,0,0">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Icon -->
|
||||
<FontIcon Grid.Column="0"
|
||||
Glyph=""
|
||||
FontSize="24"
|
||||
Foreground="White"
|
||||
Margin="0,0,12,0"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<!-- Title -->
|
||||
<TextBlock Grid.Column="1"
|
||||
x:Uid="LoginPage_Title"
|
||||
Text="Sign In to Marechai"
|
||||
Style="{StaticResource TitleTextBlockStyle}"
|
||||
Foreground="White"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Login Form Content -->
|
||||
<ScrollViewer Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Padding="32"
|
||||
Spacing="24">
|
||||
|
||||
<!-- Welcome Message -->
|
||||
<TextBlock x:Uid="LoginPage_WelcomeMessage"
|
||||
Text="Welcome back! Please sign in to continue."
|
||||
Style="{StaticResource BodyTextBlockStyle}"
|
||||
TextWrapping="Wrap"
|
||||
HorizontalAlignment="Center"
|
||||
TextAlignment="Center" />
|
||||
|
||||
<!-- Email Input -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock x:Uid="LoginPage_EmailLabel"
|
||||
Text="Email Address"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
<TextBox x:Name="EmailTextBox"
|
||||
x:Uid="LoginPage_EmailTextBox"
|
||||
Text="{Binding Email, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
PlaceholderText="Enter your email"
|
||||
InputScope="EmailSmtpAddress"
|
||||
IsSpellCheckEnabled="False"
|
||||
AutomationProperties.Name="Email address"
|
||||
KeyDown="OnEmailKeyDown" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Password Input -->
|
||||
<StackPanel Spacing="8">
|
||||
<TextBlock x:Uid="LoginPage_PasswordLabel"
|
||||
Text="Password"
|
||||
Style="{StaticResource BodyStrongTextBlockStyle}" />
|
||||
<PasswordBox x:Name="PasswordBox"
|
||||
x:Uid="LoginPage_PasswordBox"
|
||||
Password="{Binding Password, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
|
||||
PlaceholderText="Enter your password"
|
||||
AutomationProperties.Name="Password"
|
||||
KeyDown="OnPasswordKeyDown" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Error Message -->
|
||||
<Border x:Name="ErrorBorder"
|
||||
Background="#FFF4E6"
|
||||
BorderBrush="#FFB84D"
|
||||
BorderThickness="2"
|
||||
CornerRadius="4"
|
||||
Padding="12"
|
||||
Visibility="{Binding ErrorMessage, Converter={StaticResource NullToVisibilityConverter}}">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Warning Icon -->
|
||||
<FontIcon Grid.Column="0"
|
||||
Glyph=""
|
||||
Foreground="#D97706"
|
||||
FontSize="20"
|
||||
Margin="0,0,8,0"
|
||||
VerticalAlignment="Top" />
|
||||
|
||||
<!-- Error Text -->
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding ErrorMessage}"
|
||||
Foreground="#92400E"
|
||||
TextWrapping="Wrap"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<!-- Close Button -->
|
||||
<Button Grid.Column="2"
|
||||
Command="{Binding ClearErrorCommand}"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="8"
|
||||
Margin="8,0,0,0"
|
||||
VerticalAlignment="Center"
|
||||
AutomationProperties.Name="Close error message">
|
||||
<FontIcon Glyph=""
|
||||
FontSize="12"
|
||||
Foreground="#92400E" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Login Button -->
|
||||
<Button x:Name="LoginButton"
|
||||
x:Uid="LoginPage_LoginButton"
|
||||
Command="{Binding LoginCommand}"
|
||||
IsEnabled="{Binding IsLoggingIn, Converter={StaticResource InvertBoolConverter}}"
|
||||
HorizontalAlignment="Stretch"
|
||||
Padding="16,12"
|
||||
Style="{StaticResource AccentButtonStyle}"
|
||||
AutomationProperties.Name="Sign in button">
|
||||
<Button.Content>
|
||||
<Grid>
|
||||
<TextBlock x:Uid="LoginPage_LoginButtonText"
|
||||
Text="Sign In"
|
||||
Visibility="{Binding IsLoggingIn, Converter={StaticResource InvertBoolToVisibilityConverter}}" />
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="8"
|
||||
Visibility="{Binding IsLoggingIn, Converter={StaticResource BoolToVisibilityConverter}}">
|
||||
<ProgressRing IsActive="True"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Foreground="White" />
|
||||
<TextBlock x:Uid="LoginPage_SigningInText"
|
||||
Text="Signing in..."
|
||||
Foreground="White" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Button.Content>
|
||||
</Button>
|
||||
|
||||
<!-- Additional Options -->
|
||||
<StackPanel Spacing="8"
|
||||
HorizontalAlignment="Center">
|
||||
<HyperlinkButton x:Uid="LoginPage_ForgotPasswordLink"
|
||||
Content="Forgot your password?"
|
||||
HorizontalAlignment="Center"
|
||||
Visibility="Collapsed" />
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
Spacing="4"
|
||||
Visibility="Collapsed">
|
||||
<TextBlock x:Uid="LoginPage_NoAccountText"
|
||||
Text="Don't have an account?"
|
||||
VerticalAlignment="Center" />
|
||||
<HyperlinkButton x:Uid="LoginPage_SignUpLink"
|
||||
Content="Sign up"
|
||||
Padding="4,0" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Page>
|
||||
33
Marechai.App/Presentation/Views/LoginPage.xaml.cs
Normal file
33
Marechai.App/Presentation/Views/LoginPage.xaml.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using Windows.System;
|
||||
using Marechai.App.Presentation.ViewModels;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using Microsoft.UI.Xaml.Input;
|
||||
|
||||
namespace Marechai.App.Presentation.Views;
|
||||
|
||||
public sealed partial class LoginPage : Page
|
||||
{
|
||||
public LoginPage() => InitializeComponent();
|
||||
|
||||
private void OnEmailKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if(e.Key == VirtualKey.Enter)
|
||||
{
|
||||
// Move focus to password field
|
||||
PasswordBox.Focus(FocusState.Keyboard);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnPasswordKeyDown(object sender, KeyRoutedEventArgs e)
|
||||
{
|
||||
if(e.Key == VirtualKey.Enter)
|
||||
{
|
||||
// Trigger login when Enter is pressed
|
||||
if(DataContext is LoginViewModel viewModel && LoginButton.IsEnabled) viewModel.LoginCommand.Execute(null);
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
422
Marechai.App/Presentation/Views/MachineViewPage.xaml
Normal file
422
Marechai.App/Presentation/Views/MachineViewPage.xaml
Normal 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=""
|
||||
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>
|
||||
82
Marechai.App/Presentation/Views/MachineViewPage.xaml.cs
Normal file
82
Marechai.App/Presentation/Views/MachineViewPage.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
55
Marechai.App/Presentation/Views/MainPage.xaml
Normal file
55
Marechai.App/Presentation/Views/MainPage.xaml
Normal 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>
|
||||
98
Marechai.App/Presentation/Views/MainPage.xaml.cs
Normal file
98
Marechai.App/Presentation/Views/MainPage.xaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
130
Marechai.App/Presentation/Views/NewsPage.xaml
Normal file
130
Marechai.App/Presentation/Views/NewsPage.xaml
Normal 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>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user