This commit is contained in:
Ho Ngoc Hai
2026-05-23 18:37:02 +07:00
parent f15d91ee29
commit 76d75c753b
3993 changed files with 403 additions and 0 deletions

View File

@@ -0,0 +1,14 @@
<?xml version = "1.0" encoding = "UTF-8" ?>
<Application xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:AppClientBase"
x:Class="AppClientBase.App">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,26 @@
namespace AppClientBase;
/// <summary>
/// EN: Main application class.
/// VI: Lớp ứng dụng chính.
/// </summary>
public partial class App : Application
{
/// <summary>
/// EN: Constructor - initializes XAML components.
/// VI: Constructor - khởi tạo các thành phần XAML.
/// </summary>
public App()
{
InitializeComponent();
}
/// <summary>
/// EN: Creates the main window with AppShell.
/// VI: Tạo cửa sổ chính với AppShell.
/// </summary>
protected override Window CreateWindow(IActivationState? activationState)
{
return new Window(new AppShell());
}
}

View File

@@ -0,0 +1,72 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Android/Java SDK Paths for macOS with Homebrew OpenJDK -->
<PropertyGroup Condition="$([MSBuild]::IsOSPlatform('osx'))">
<AndroidSdkDirectory>$(HOME)/Library/Android/sdk</AndroidSdkDirectory>
<JavaSdkDirectory>/opt/homebrew/opt/openjdk@21/libexec/openjdk.jdk/Contents/Home</JavaSdkDirectory>
</PropertyGroup>
<PropertyGroup>
<TargetFrameworks>net10.0-android;net10.0-ios;net10.0-maccatalyst</TargetFrameworks>
<TargetFrameworks Condition="$([MSBuild]::IsOSPlatform('windows'))">$(TargetFrameworks);net10.0-windows10.0.19041.0</TargetFrameworks>
<!-- Note for MacCatalyst:
The default runtime is maccatalyst-x64, except in Release config, in which case the default is maccatalyst-x64;maccatalyst-arm64.
When specifying both architectures, use the plural <RuntimeIdentifiers> instead of the singular <RuntimeIdentifier>.
The Mac App Store will NOT accept apps with ONLY maccatalyst-arm64 indicated;
either BOTH runtimes must be indicated or ONLY macatalyst-x64. -->
<!-- For example: <RuntimeIdentifiers>maccatalyst-x64;maccatalyst-arm64</RuntimeIdentifiers> -->
<OutputType>Exe</OutputType>
<RootNamespace>AppClientBase</RootNamespace>
<UseMaui>true</UseMaui>
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Display name -->
<ApplicationTitle>AppClientBase</ApplicationTitle>
<!-- App Identifier -->
<ApplicationId>com.companyname.appclientbase</ApplicationId>
<!-- Versions -->
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
<ApplicationVersion>1</ApplicationVersion>
<!-- To develop, package, and publish an app to the Microsoft Store, see: https://aka.ms/MauiTemplateUnpackaged -->
<WindowsPackageType>None</WindowsPackageType>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'maccatalyst'">15.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">21.0</SupportedOSPlatformVersion>
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</SupportedOSPlatformVersion>
<TargetPlatformMinVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'windows'">10.0.17763.0</TargetPlatformMinVersion>
</PropertyGroup>
<ItemGroup>
<!-- App Icon -->
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
<!-- Splash Screen -->
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
<!-- Images -->
<MauiImage Include="Resources\Images\*" />
<MauiImage Update="Resources\Images\dotnet_bot.png" Resize="True" BaseSize="300,185" />
<!-- Custom Fonts -->
<MauiFont Include="Resources\Fonts\*" />
<!-- Raw Assets (also remove the "Resources\Raw" prefix) -->
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Maui" Version="9.1.0" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
<PackageReference Include="Microsoft.Maui.Controls" Version="$(MauiVersion)" />
<PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="10.0.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8" ?>
<Shell
x:Class="AppClientBase.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:AppClientBase"
Shell.FlyoutBehavior="Disabled"
Title="AppClientBase">
<ShellContent
Title="Home"
ContentTemplate="{DataTemplate local:MainPage}"
Route="MainPage" />
</Shell>

View File

@@ -0,0 +1,9 @@
namespace AppClientBase;
public partial class AppShell : Shell
{
public AppShell()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="AppClientBase.MainPage">
<ScrollView>
<VerticalStackLayout
Padding="30,0"
Spacing="25">
<Image
Source="dotnet_bot.png"
HeightRequest="185"
Aspect="AspectFit"
SemanticProperties.Description="dot net bot waving" />
<Label
Text="Hello, World!"
FontSize="32"
HorizontalOptions="Center"
SemanticProperties.HeadingLevel="Level1" />
<Label
Text="Welcome to .NET Multi-platform App UI"
FontSize="18"
HorizontalOptions="Center"
SemanticProperties.HeadingLevel="Level2"
SemanticProperties.Description="Welcome to dot net Multi platform App U I" />
<Button
x:Name="CounterBtn"
Text="Click me"
SemanticProperties.Hint="Counts the number of times you click"
Clicked="OnCounterClicked"
HorizontalOptions="Fill" />
</VerticalStackLayout>
</ScrollView>
</ContentPage>

View File

@@ -0,0 +1,23 @@
namespace AppClientBase;
public partial class MainPage : ContentPage
{
int count = 0;
public MainPage()
{
InitializeComponent();
}
private void OnCounterClicked(object sender, EventArgs e)
{
count++;
if (count == 1)
CounterBtn.Text = $"Clicked {count} time";
else
CounterBtn.Text = $"Clicked {count} times";
SemanticScreenReader.Announce(CounterBtn.Text);
}
}

View File

@@ -0,0 +1,31 @@
using CommunityToolkit.Maui;
using Microsoft.Extensions.Logging;
namespace AppClientBase;
/// <summary>
/// EN: Main entry point for MAUI application configuration.
/// VI: Điểm khởi đầu chính để cấu hình ứng dụng MAUI.
/// </summary>
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.UseMauiCommunityToolkit()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
fonts.AddFont("OpenSans-Semibold.ttf", "OpenSansSemibold");
});
#if DEBUG
builder.Logging.AddDebug();
#endif
return builder.Build();
}
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

View File

@@ -0,0 +1,10 @@
using Android.App;
using Android.Content.PM;
using Android.OS;
namespace AppClientBase;
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, LaunchMode = LaunchMode.SingleTop, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
}

View File

@@ -0,0 +1,15 @@
using Android.App;
using Android.Runtime;
namespace AppClientBase;
[Application]
public class MainApplication : MauiApplication
{
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
: base(handle, ownership)
{
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">#512BD4</color>
<color name="colorPrimaryDark">#2B0B98</color>
<color name="colorAccent">#2B0B98</color>
</resources>

View File

@@ -0,0 +1,9 @@
using Foundation;
namespace AppClientBase;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,14 @@
<?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.ms/maui-publish-app-store#add-entitlements for more information about adding entitlements.-->
<dict>
<!-- App Sandbox must be enabled to distribute a MacCatalyst app through the Mac App Store. -->
<key>com.apple.security.app-sandbox</key>
<true/>
<!-- When App Sandbox is enabled, this value is required to open outgoing network connections. -->
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>

View File

@@ -0,0 +1,40 @@
<?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>
<!-- The Mac App Store requires you specify if the app uses encryption. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/itsappusesnonexemptencryption -->
<!-- <key>ITSAppUsesNonExemptEncryption</key> -->
<!-- Please indicate <true/> or <false/> here. -->
<!-- Specify the category for your app here. -->
<!-- Please consult https://developer.apple.com/documentation/bundleresources/information_property_list/lsapplicationcategorytype -->
<!-- <key>LSApplicationCategoryType</key> -->
<!-- <string>public.app-category.YOUR-CATEGORY-HERE</string> -->
<key>UIDeviceFamily</key>
<array>
<integer>2</integer>
</array>
<key>LSApplicationCategoryType</key>
<string>public.app-category.lifestyle</string>
<key>UIRequiredDeviceCapabilities</key>
<array>
<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>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
using ObjCRuntime;
using UIKit;
namespace AppClientBase;
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

View File

@@ -0,0 +1,8 @@
<maui:MauiWinUIApplication
x:Class="AppClientBase.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:maui="using:Microsoft.Maui"
xmlns:local="using:AppClientBase.WinUI">
</maui:MauiWinUIApplication>

View File

@@ -0,0 +1,24 @@
using Microsoft.UI.Xaml;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.
namespace AppClientBase.WinUI;
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
public partial class App : MauiWinUIApplication
{
/// <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()
{
this.InitializeComponent();
}
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,46 @@
<?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:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity Name="maui-package-name-placeholder" Publisher="CN=User Name" Version="0.0.0.0" />
<mp:PhoneIdentity PhoneProductId="0999736D-C9A3-48A0-921A-5A49E7E146D2" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>$placeholder$</DisplayName>
<PublisherDisplayName>User Name</PublisherDisplayName>
<Logo>$placeholder$.png</Logo>
</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
DisplayName="$placeholder$"
Description="$placeholder$"
Square150x150Logo="$placeholder$.png"
Square44x44Logo="$placeholder$.png"
BackgroundColor="transparent">
<uap:DefaultTile Square71x71Logo="$placeholder$.png" Wide310x150Logo="$placeholder$.png" Square310x310Logo="$placeholder$.png" />
<uap:SplashScreen Image="$placeholder$.png" />
</uap:VisualElements>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<assemblyIdentity version="1.0.0.0" name="AppClientBase.WinUI.app"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- The combination of below two tags have the following effect:
1) Per-Monitor for >= Windows 10 Anniversary Update
2) System < Windows 10 Anniversary Update
-->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/PM</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2, PerMonitor</dpiAwareness>
<longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware>
</windowsSettings>
</application>
</assembly>

View File

@@ -0,0 +1,9 @@
using Foundation;
namespace AppClientBase;
[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
}

View File

@@ -0,0 +1,32 @@
<?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>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>XSAppIconAssets</key>
<string>Assets.xcassets/appicon.appiconset</string>
</dict>
</plist>

View File

@@ -0,0 +1,15 @@
using ObjCRuntime;
using UIKit;
namespace AppClientBase;
public class Program
{
// This is the main entry point of the application.
static void Main(string[] args)
{
// if you want to use a different Application Delegate class from "AppDelegate"
// you can specify it here.
UIApplication.Main(args, null, typeof(AppDelegate));
}
}

View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
This is the minimum required version of the Apple Privacy Manifest for .NET MAUI apps.
The contents below are needed because of APIs that are used in the .NET framework and .NET MAUI SDK.
You are responsible for adding extra entries as needed for your application.
More information: https://aka.ms/maui-privacy-manifest
-->
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyAccessedAPITypes</key>
<array>
<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>
<!--
The entry below is only needed when you're using the Preferences API in your app.
<dict>
<key>NSPrivacyAccessedAPIType</key>
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
<key>NSPrivacyAccessedAPITypeReasons</key>
<array>
<string>CA92.1</string>
</array>
</dict> -->
</array>
</dict>
</plist>

View File

@@ -0,0 +1,8 @@
{
"profiles": {
"Windows Machine": {
"commandName": "Project",
"nativeDebugging": false
}
}
}

View File

@@ -0,0 +1,87 @@
# AppClientBase - .NET MAUI Enterprise Base App
Base frontend application cho iOS và Android được xây dựng với .NET MAUI theo chuẩn Enterprise.
## Tech Stack
- **.NET 10 MAUI** - Cross-platform framework
- **CommunityToolkit.Mvvm 8.4.0** - MVVM implementation
- **CommunityToolkit.Maui 9.1.0** - MAUI extensions
## Project Structure
```
AppClientBase/
├── MauiProgram.cs # DI & configuration
├── App.xaml # Application resources
├── AppShell.xaml # Navigation shell
├── ViewModels/ # MVVM ViewModels
│ ├── BaseViewModel.cs # Base với IsBusy, Title
│ └── MainViewModel.cs # Main page ViewModel
├── Views/ # XAML Pages
│ └── MainPage.xaml # Main page với compiled bindings
├── Services/ # Business services
│ ├── INavigationService.cs # Navigation interface
│ ├── NavigationService.cs # Shell navigation
│ ├── ISettingsService.cs # Settings interface
│ └── SettingsService.cs # MAUI Preferences wrapper
├── Models/ # Data models
├── Resources/
│ └── Styles/
│ ├── Colors.xaml # Brand, Semantic, Surface colors
│ ├── Typography.xaml # Font families, sizes, text styles
│ ├── Theme.xaml # Button, Entry, Card styles
│ └── Styles.xaml # Template implicit styles
└── Platforms/ # Platform-specific code
├── Android/
└── iOS/
```
## Features
### MVVM + Dependency Injection
- Constructor injection qua DI container
- Singleton cho stateful services (NavigationService, SettingsService)
- Transient cho ViewModels và Views
### Branding System
- **Colors.xaml**: Light/Dark colors riêng biệt
- **Typography.xaml**: Font scale (Caption → Display)
- **Theme.xaml**: ButtonPrimary, ButtonSecondary, CardDefault styles
- AppThemeBinding cho Light/Dark mode support
### Compiled Bindings
- `x:DataType` cho performance tối ưu
- Compile-time binding validation
## Getting Started
```bash
# Navigate to project
cd apps/app-client-base
# Restore packages
dotnet restore
# Build for macCatalyst
dotnet build -c Debug -f net10.0-maccatalyst
# Build for iOS (requires Xcode with iOS simulator)
dotnet build -c Debug -f net10.0-ios
# Build for Android (requires Android SDK)
dotnet build -c Debug -f net10.0-android
```
## Requirements
- .NET 10 SDK
- MAUI workload (`dotnet workload install maui`)
- For iOS: macOS with Xcode
- For Android: Android SDK
## Related Skills
Xem các MAUI Skills để hiểu thêm về patterns được sử dụng:
- `.agent/skills/maui-enterprise-architect/SKILL.md`
- `.agent/skills/maui-branding-expert/SKILL.md`

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="456" height="456" fill="#512BD4" />
</svg>

After

Width:  |  Height:  |  Size: 231 B

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -0,0 +1,15 @@
Any raw assets you want to be deployed with your application can be placed in
this directory (and child directories). Deployment of the asset to your application
is automatically handled by the following `MauiAsset` Build Action within your `.csproj`.
<MauiAsset Include="Resources\Raw\**" LogicalName="%(RecursiveDir)%(Filename)%(Extension)" />
These files will be deployed with your package and will be accessible using Essentials:
async Task LoadMauiAsset()
{
using var stream = await FileSystem.OpenAppPackageFileAsync("AboutAssets.txt");
using var reader = new StreamReader(stream);
var contents = reader.ReadToEnd();
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="456" height="456" viewBox="0 0 456 456" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<path d="m 105.50037,281.60863 c -2.70293,0 -5.00091,-0.90042 -6.893127,-2.70209 -1.892214,-1.84778 -2.837901,-4.04181 -2.837901,-6.58209 0,-2.58722 0.945687,-4.80389 2.837901,-6.65167 1.892217,-1.84778 4.190197,-2.77167 6.893127,-2.77167 2.74819,0 5.06798,0.92389 6.96019,2.77167 1.93749,1.84778 2.90581,4.06445 2.90581,6.65167 0,2.54028 -0.96832,4.73431 -2.90581,6.58209 -1.89221,1.80167 -4.212,2.70209 -6.96019,2.70209 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 213.56111,280.08446 H 195.99044 L 149.69953,207.0544 c -1.17121,-1.84778 -2.14037,-3.76515 -2.90581,-5.75126 h -0.40578 c 0.36051,2.12528 0.54076,6.67515 0.54076,13.6496 v 65.13172 h -15.54349 v -99.36009 h 18.71925 l 44.7374,71.29798 c 1.89222,2.95695 3.1087,4.98917 3.64945,6.09751 h 0.26996 c -0.45021,-2.6325 -0.67573,-7.09015 -0.67573,-13.37293 v -64.02256 h 15.47557 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="m 289.25134,280.08446 h -54.40052 v -99.36009 h 52.23835 v 13.99669 h -36.15411 v 28.13085 h 33.31621 v 13.9271 h -33.31621 v 29.37835 h 38.31628 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
<path d="M 366.56466,194.72106 H 338.7222 v 85.3634 h -16.08423 v -85.3634 h -27.77455 v -13.99669 h 71.70124 z" style="fill:#ffffff;fill-rule:nonzero;stroke-width:0.838376" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml -->
<Color x:Key="Primary">#512BD4</Color>
<Color x:Key="PrimaryDark">#ac99ea</Color>
<Color x:Key="PrimaryDarkText">#242424</Color>
<Color x:Key="Secondary">#DFD8F7</Color>
<Color x:Key="SecondaryDarkText">#9880e5</Color>
<Color x:Key="Tertiary">#2B0B98</Color>
<Color x:Key="White">White</Color>
<Color x:Key="Black">Black</Color>
<Color x:Key="Magenta">#D600AA</Color>
<Color x:Key="MidnightBlue">#190649</Color>
<Color x:Key="OffBlack">#1f1f1f</Color>
<Color x:Key="Gray100">#E1E1E1</Color>
<Color x:Key="Gray200">#C8C8C8</Color>
<Color x:Key="Gray300">#ACACAC</Color>
<Color x:Key="Gray400">#919191</Color>
<Color x:Key="Gray500">#6E6E6E</Color>
<Color x:Key="Gray600">#404040</Color>
<Color x:Key="Gray900">#212121</Color>
<Color x:Key="Gray950">#141414</Color>
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}"/>
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}"/>
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}"/>
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}"/>
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}"/>
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}"/>
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}"/>
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}"/>
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}"/>
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}"/>
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}"/>
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}"/>
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}"/>
</ResourceDictionary>

View File

@@ -0,0 +1,434 @@
<?xml version="1.0" encoding="UTF-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<Style TargetType="ActivityIndicator">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="IndicatorView">
<Setter Property="IndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}"/>
<Setter Property="SelectedIndicatorColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray100}}"/>
</Style>
<Style TargetType="Border">
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="StrokeShape" Value="Rectangle"/>
<Setter Property="StrokeThickness" Value="1"/>
</Style>
<Style TargetType="BoxView">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="Button">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="14,10"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="CheckBox">
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="DatePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Editor">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Entry">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ImageButton">
<Setter Property="Opacity" Value="1" />
<Setter Property="BorderColor" Value="Transparent"/>
<Setter Property="BorderWidth" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="Opacity" Value="0.5" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="PointerOver" />
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Label">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Label" x:Key="Headline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="32" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="Label" x:Key="SubHeadline">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
<Setter Property="FontSize" Value="24" />
<Setter Property="HorizontalOptions" Value="Center" />
<Setter Property="HorizontalTextAlignment" Value="Center" />
</Style>
<Style TargetType="Picker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="ProgressBar">
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RadioButton">
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="RefreshView">
<Setter Property="RefreshColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="SearchBar">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="CancelButtonColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SearchHandler">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="PlaceholderColor" Value="{StaticResource Gray500}" />
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="FontFamily" Value="OpenSansRegular" />
<Setter Property="FontSize" Value="14" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="Shadow">
<Setter Property="Radius" Value="15" />
<Setter Property="Opacity" Value="0.5" />
<Setter Property="Brush" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource White}}" />
<Setter Property="Offset" Value="10,10" />
</Style>
<Style TargetType="Slider">
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="MinimumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="MaximumTrackColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}"/>
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="SwipeItem">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
</Style>
<Style TargetType="Switch">
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
<Setter Property="ThumbColor" Value="{StaticResource White}" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="On">
<VisualState.Setters>
<Setter Property="OnColor" Value="{AppThemeBinding Light={StaticResource Secondary}, Dark={StaticResource Gray200}}" />
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Off">
<VisualState.Setters>
<Setter Property="ThumbColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style TargetType="TimePicker">
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
<Setter Property="BackgroundColor" Value="Transparent"/>
<Setter Property="FontFamily" Value="OpenSansRegular"/>
<Setter Property="FontSize" Value="14"/>
<Setter Property="MinimumHeightRequest" Value="44"/>
<Setter Property="MinimumWidthRequest" Value="44"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<!--
<Style TargetType="TitleBar">
<Setter Property="MinimumHeightRequest" Value="32"/>
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="TitleActiveStates">
<VisualState x:Name="TitleBarTitleActive">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="TitleBarTitleInactive">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Gray400}, Dark={StaticResource Gray500}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
-->
<Style TargetType="Page" ApplyToDerivedTypes="True">
<Setter Property="Padding" Value="0"/>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
</Style>
<Style TargetType="Shell" ApplyToDerivedTypes="True">
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
<Setter Property="Shell.NavBarHasShadow" Value="False" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
</Style>
<Style TargetType="NavigationPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
</Style>
<Style TargetType="TabbedPage">
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- ==================== BUTTON STYLES ==================== -->
<!-- EN: Primary Button (Filled) -->
<!-- VI: Button chính (Đặc) -->
<Style x:Key="ButtonPrimary" TargetType="Button">
<Setter Property="BackgroundColor" Value="{StaticResource BrandPrimary}" />
<Setter Property="TextColor" Value="{StaticResource TextOnPrimary}" />
<Setter Property="FontFamily" Value="{StaticResource FontFamilySemiBold}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="24,12" />
<Setter Property="MinimumHeightRequest" Value="48" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{StaticResource BrandPrimaryDark}" />
<Setter Property="Scale" Value="0.98" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Disabled">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource TextDisabledLight}, Dark={StaticResource TextDisabledDark}}" />
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource SurfaceCardLight}, Dark={StaticResource SurfaceCardDark}}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<!-- EN: Secondary Button (Outlined) -->
<!-- VI: Button phụ (Viền) -->
<Style x:Key="ButtonSecondary" TargetType="Button">
<Setter Property="BackgroundColor" Value="Transparent" />
<Setter Property="TextColor" Value="{StaticResource BrandPrimary}" />
<Setter Property="BorderColor" Value="{StaticResource BrandPrimary}" />
<Setter Property="BorderWidth" Value="2" />
<Setter Property="FontFamily" Value="{StaticResource FontFamilySemiBold}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="24,12" />
<Setter Property="MinimumHeightRequest" Value="48" />
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal" />
<VisualState x:Name="Pressed">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{StaticResource BrandPrimaryLight}" />
<Setter Property="TextColor" Value="{StaticResource TextOnPrimary}" />
<Setter Property="Scale" Value="0.98" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<!-- ==================== ENTRY STYLES ==================== -->
<!-- EN: Default Entry Style -->
<!-- VI: Style Entry mặc định -->
<Style x:Key="EntryDefault" TargetType="Entry">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource SurfaceCardLight}, Dark={StaticResource SurfaceCardDark}}" />
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextPrimaryLight}, Dark={StaticResource TextPrimaryDark}}" />
<Setter Property="PlaceholderColor" Value="{AppThemeBinding Light={StaticResource TextSecondaryLight}, Dark={StaticResource TextSecondaryDark}}" />
<Setter Property="FontFamily" Value="{StaticResource FontFamilyRegular}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="MinimumHeightRequest" Value="48" />
</Style>
<!-- ==================== FRAME/CARD STYLES ==================== -->
<!-- EN: Default Card Style -->
<!-- VI: Style Card mặc định -->
<Style x:Key="CardDefault" TargetType="Frame">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource SurfaceCardLight}, Dark={StaticResource SurfaceCardDark}}" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="HasShadow" Value="True" />
<Setter Property="Padding" Value="16" />
<Setter Property="BorderColor" Value="Transparent" />
</Style>
<!-- ==================== PAGE STYLES ==================== -->
<!-- EN: Base Page Style -->
<!-- VI: Style Page cơ sở -->
<Style x:Key="PageBase" TargetType="ContentPage">
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource SurfaceBackgroundLight}, Dark={StaticResource SurfaceBackgroundDark}}" />
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
<!-- ==================== FONT FAMILIES ==================== -->
<!-- EN: Font family definitions (cross-platform) -->
<!-- VI: Định nghĩa font family (đa nền tảng) -->
<OnPlatform x:Key="FontFamilyRegular" x:TypeArguments="x:String">
<On Platform="Android, iOS" Value="OpenSansRegular" />
<On Platform="WinUI" Value="Assets/Fonts/OpenSans-Regular.ttf#Open Sans" />
</OnPlatform>
<OnPlatform x:Key="FontFamilySemiBold" x:TypeArguments="x:String">
<On Platform="Android, iOS" Value="OpenSansSemibold" />
<On Platform="WinUI" Value="Assets/Fonts/OpenSans-Semibold.ttf#Open Sans" />
</OnPlatform>
<!-- ==================== FONT SIZES (Scalable) ==================== -->
<!-- EN: Typography scale based on 16px base -->
<!-- VI: Thang typography dựa trên base 16px -->
<x:Double x:Key="FontSizeCaption">12</x:Double>
<x:Double x:Key="FontSizeBody">16</x:Double>
<x:Double x:Key="FontSizeSubtitle">18</x:Double>
<x:Double x:Key="FontSizeTitle">24</x:Double>
<x:Double x:Key="FontSizeHeadline">32</x:Double>
<x:Double x:Key="FontSizeDisplay">48</x:Double>
<!-- ==================== TEXT STYLES ==================== -->
<!-- EN: Pre-defined text styles with AppThemeBinding for colors -->
<!-- VI: Các text style định sẵn với AppThemeBinding cho màu sắc -->
<Style x:Key="TextCaption" TargetType="Label">
<Setter Property="FontFamily" Value="{StaticResource FontFamilyRegular}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeCaption}" />
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextSecondaryLight}, Dark={StaticResource TextSecondaryDark}}" />
</Style>
<Style x:Key="TextBody" TargetType="Label">
<Setter Property="FontFamily" Value="{StaticResource FontFamilyRegular}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeBody}" />
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextPrimaryLight}, Dark={StaticResource TextPrimaryDark}}" />
</Style>
<Style x:Key="TextSubtitle" TargetType="Label">
<Setter Property="FontFamily" Value="{StaticResource FontFamilySemiBold}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeSubtitle}" />
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextPrimaryLight}, Dark={StaticResource TextPrimaryDark}}" />
</Style>
<Style x:Key="TextTitle" TargetType="Label">
<Setter Property="FontFamily" Value="{StaticResource FontFamilySemiBold}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeTitle}" />
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextPrimaryLight}, Dark={StaticResource TextPrimaryDark}}" />
</Style>
<Style x:Key="TextHeadline" TargetType="Label">
<Setter Property="FontFamily" Value="{StaticResource FontFamilySemiBold}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeHeadline}" />
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource TextPrimaryLight}, Dark={StaticResource TextPrimaryDark}}" />
<Setter Property="SemanticProperties.HeadingLevel" Value="Level1" />
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,20 @@
namespace AppClientBase.Services;
/// <summary>
/// EN: Navigation service interface for Shell-based navigation.
/// VI: Interface dịch vụ điều hướng cho điều hướng dựa trên Shell.
/// </summary>
public interface INavigationService
{
/// <summary>
/// EN: Navigate to a route with optional parameters.
/// VI: Điều hướng đến một route với các tham số tùy chọn.
/// </summary>
Task GoToAsync(string route, IDictionary<string, object>? parameters = null);
/// <summary>
/// EN: Navigate back.
/// VI: Điều hướng quay lại.
/// </summary>
Task GoBackAsync();
}

View File

@@ -0,0 +1,38 @@
namespace AppClientBase.Services;
/// <summary>
/// EN: Settings service interface for app preferences.
/// VI: Interface dịch vụ cài đặt cho tùy chọn ứng dụng.
/// </summary>
public interface ISettingsService
{
/// <summary>
/// EN: Get a setting value.
/// VI: Lấy giá trị cài đặt.
/// </summary>
T Get<T>(string key, T defaultValue);
/// <summary>
/// EN: Set a setting value.
/// VI: Đặt giá trị cài đặt.
/// </summary>
void Set<T>(string key, T value);
/// <summary>
/// EN: Check if a setting exists.
/// VI: Kiểm tra xem cài đặt có tồn tại không.
/// </summary>
bool Contains(string key);
/// <summary>
/// EN: Remove a setting.
/// VI: Xóa một cài đặt.
/// </summary>
void Remove(string key);
/// <summary>
/// EN: Clear all settings.
/// VI: Xóa tất cả cài đặt.
/// </summary>
void Clear();
}

View File

@@ -0,0 +1,33 @@
namespace AppClientBase.Services;
/// <summary>
/// EN: Navigation service using Shell navigation.
/// VI: Dịch vụ điều hướng sử dụng Shell navigation.
/// </summary>
public class NavigationService : INavigationService
{
/// <summary>
/// EN: Navigate to a route with optional parameters.
/// VI: Điều hướng đến một route với các tham số tùy chọn.
/// </summary>
public async Task GoToAsync(string route, IDictionary<string, object>? parameters = null)
{
if (parameters != null)
{
await Shell.Current.GoToAsync(route, parameters);
}
else
{
await Shell.Current.GoToAsync(route);
}
}
/// <summary>
/// EN: Navigate back.
/// VI: Điều hướng quay lại.
/// </summary>
public async Task GoBackAsync()
{
await Shell.Current.GoToAsync("..");
}
}

View File

@@ -0,0 +1,53 @@
namespace AppClientBase.Services;
/// <summary>
/// EN: Settings service using MAUI Preferences API.
/// VI: Dịch vụ cài đặt sử dụng MAUI Preferences API.
/// </summary>
public class SettingsService : ISettingsService
{
/// <summary>
/// EN: Get a setting value.
/// VI: Lấy giá trị cài đặt.
/// </summary>
public T Get<T>(string key, T defaultValue)
{
return Preferences.Default.Get(key, defaultValue);
}
/// <summary>
/// EN: Set a setting value.
/// VI: Đặt giá trị cài đặt.
/// </summary>
public void Set<T>(string key, T value)
{
Preferences.Default.Set(key, value);
}
/// <summary>
/// EN: Check if a setting exists.
/// VI: Kiểm tra xem cài đặt có tồn tại không.
/// </summary>
public bool Contains(string key)
{
return Preferences.Default.ContainsKey(key);
}
/// <summary>
/// EN: Remove a setting.
/// VI: Xóa một cài đặt.
/// </summary>
public void Remove(string key)
{
Preferences.Default.Remove(key);
}
/// <summary>
/// EN: Clear all settings.
/// VI: Xóa tất cả cài đặt.
/// </summary>
public void Clear()
{
Preferences.Default.Clear();
}
}

View File

@@ -0,0 +1,58 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using AppClientBase.Services;
namespace AppClientBase.ViewModels;
/// <summary>
/// EN: Base ViewModel class with common functionality.
/// VI: Lớp ViewModel cơ sở với chức năng chung.
/// </summary>
public abstract partial class BaseViewModel : ObservableObject
{
/// <summary>
/// EN: Navigation service instance.
/// VI: Instance của dịch vụ điều hướng.
/// </summary>
protected readonly INavigationService NavigationService;
/// <summary>
/// EN: Indicates if the ViewModel is currently loading data.
/// VI: Cho biết ViewModel đang tải dữ liệu hay không.
/// </summary>
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(IsNotBusy))]
private bool _isBusy;
/// <summary>
/// EN: Title of the current page.
/// VI: Tiêu đề của trang hiện tại.
/// </summary>
[ObservableProperty]
private string _title = string.Empty;
/// <summary>
/// EN: Indicates if the ViewModel is not busy.
/// VI: Cho biết ViewModel không bận.
/// </summary>
public bool IsNotBusy => !IsBusy;
/// <summary>
/// EN: Constructor with navigation service.
/// VI: Constructor với dịch vụ điều hướng.
/// </summary>
protected BaseViewModel(INavigationService navigationService)
{
NavigationService = navigationService;
}
/// <summary>
/// EN: Navigate back to previous page.
/// VI: Điều hướng quay lại trang trước.
/// </summary>
[RelayCommand]
protected virtual async Task GoBackAsync()
{
await NavigationService.GoBackAsync();
}
}

View File

@@ -0,0 +1,84 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using AppClientBase.Services;
namespace AppClientBase.ViewModels;
/// <summary>
/// EN: Main page ViewModel.
/// VI: ViewModel của trang chính.
/// </summary>
public partial class MainViewModel : BaseViewModel
{
private readonly ISettingsService _settingsService;
/// <summary>
/// EN: Welcome message displayed on the main page.
/// VI: Thông điệp chào mừng hiển thị trên trang chính.
/// </summary>
[ObservableProperty]
private string _welcomeMessage = "Welcome to AppClientBase!";
/// <summary>
/// EN: Click counter for demonstration.
/// VI: Bộ đếm click để minh họa.
/// </summary>
[ObservableProperty]
private int _clickCount;
/// <summary>
/// EN: Button text that updates with click count.
/// VI: Text nút cập nhật theo số lần click.
/// </summary>
[ObservableProperty]
private string _buttonText = "Click me";
/// <summary>
/// EN: Constructor with required services.
/// VI: Constructor với các services cần thiết.
/// </summary>
public MainViewModel(
INavigationService navigationService,
ISettingsService settingsService)
: base(navigationService)
{
_settingsService = settingsService;
Title = "Home";
// Defer loading settings to avoid startup crash
// ClickCount will be loaded on first interaction
}
/// <summary>
/// EN: Increment click counter command.
/// VI: Command tăng bộ đếm click.
/// </summary>
[RelayCommand]
private void IncrementCounter()
{
ClickCount++;
try
{
_settingsService.Set("ClickCount", ClickCount);
}
catch
{
// Ignore settings errors
}
UpdateButtonText();
}
/// <summary>
/// EN: Update button text based on click count.
/// VI: Cập nhật text nút dựa trên số lần click.
/// </summary>
private void UpdateButtonText()
{
ButtonText = ClickCount switch
{
0 => "Click me",
1 => "Clicked 1 time",
_ => $"Clicked {ClickCount} times"
};
}
}

View File

@@ -0,0 +1,41 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<AssemblyName>AppClientBase.UnitTests</AssemblyName>
<RootNamespace>AppClientBase.UnitTests</RootNamespace>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<!-- EN: Test framework dependencies / VI: Phụ thuộc framework test -->
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="FluentAssertions" Version="6.12.2" />
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<!-- EN: Source generator package used by included view models.
VI: Gói source generator dùng cho view model được include. -->
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
</ItemGroup>
<ItemGroup>
<!-- EN: Reuse app source files that are platform-neutral.
VI: Tái sử dụng source file ứng dụng không phụ thuộc nền tảng. -->
<Compile Include="..\..\ViewModels\BaseViewModel.cs" Link="ViewModels\BaseViewModel.cs" />
<Compile Include="..\..\ViewModels\MainViewModel.cs" Link="ViewModels\MainViewModel.cs" />
<Compile Include="..\..\Services\INavigationService.cs" Link="Services\INavigationService.cs" />
<Compile Include="..\..\Services\ISettingsService.cs" Link="Services\ISettingsService.cs" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,81 @@
using AppClientBase.Services;
using AppClientBase.ViewModels;
using FluentAssertions;
using Xunit;
namespace AppClientBase.UnitTests.ViewModels;
/// <summary>
/// EN: Unit tests for main view model behavior.
/// VI: Unit tests cho hành vi của main view model.
/// </summary>
public class MainViewModelTests
{
[Fact]
public void Constructor_ShouldSetDefaultTitleAndWelcomeMessage()
{
// Arrange
var navigationService = new FakeNavigationService();
var settingsService = new FakeSettingsService();
// Act
var viewModel = new MainViewModel(navigationService, settingsService);
// Assert
viewModel.Title.Should().Be("Home");
viewModel.WelcomeMessage.Should().Be("Welcome to AppClientBase!");
viewModel.ClickCount.Should().Be(0);
viewModel.ButtonText.Should().Be("Click me");
}
[Fact]
public void IncrementCounterCommand_ShouldIncreaseCountAndPersistSetting()
{
// Arrange
var navigationService = new FakeNavigationService();
var settingsService = new FakeSettingsService();
var viewModel = new MainViewModel(navigationService, settingsService);
// Act
viewModel.IncrementCounterCommand.Execute(null);
viewModel.IncrementCounterCommand.Execute(null);
// Assert
viewModel.ClickCount.Should().Be(2);
viewModel.ButtonText.Should().Be("Clicked 2 times");
settingsService.Get("ClickCount", 0).Should().Be(2);
}
private sealed class FakeNavigationService : INavigationService
{
public Task GoToAsync(string route, IDictionary<string, object>? parameters = null) => Task.CompletedTask;
public Task GoBackAsync() => Task.CompletedTask;
}
private sealed class FakeSettingsService : ISettingsService
{
private readonly Dictionary<string, object?> _store = new(StringComparer.Ordinal);
public T Get<T>(string key, T defaultValue)
{
if (_store.TryGetValue(key, out var value) && value is T typed)
{
return typed;
}
return defaultValue;
}
public void Set<T>(string key, T value)
{
_store[key] = value;
}
public bool Contains(string key) => _store.ContainsKey(key);
public void Remove(string key) => _store.Remove(key);
public void Clear() => _store.Clear();
}
}