feat(welcome): wizard steps + flavour selection UI
Six wizard step components (Welcome/Flavour/Account/Prefs/Apply/Done), Routes.razor wizard host with Next/Back navigation and IFlavourLoader wiring, bUnit FlavourStepTests (TDD red→green), AccountStep field validation (username/password/admin-password required; BitLocker PIN numeric ≥6 digits). Test project upgraded to Razor SDK / net9.0-windows10.0.19041.0 + UseMaui=true to reference the MAUI app assembly. Non-Windows platform folders removed; demo pages removed. All 14 tests pass (13 existing + 1 new bUnit). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
@page "/counter"
|
||||
|
||||
<h1>Counter</h1>
|
||||
|
||||
<p role="status">Current count: @currentCount</p>
|
||||
|
||||
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
|
||||
|
||||
@code {
|
||||
private int currentCount = 0;
|
||||
|
||||
private void IncrementCount()
|
||||
{
|
||||
currentCount++;
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
@page "/weather"
|
||||
|
||||
<h1>Weather</h1>
|
||||
|
||||
<p>This component demonstrates showing data.</p>
|
||||
|
||||
@if (forecasts == null)
|
||||
{
|
||||
<p><em>Loading...</em></p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Temp. (C)</th>
|
||||
<th>Temp. (F)</th>
|
||||
<th>Summary</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var forecast in forecasts)
|
||||
{
|
||||
<tr>
|
||||
<td>@forecast.Date.ToShortDateString()</td>
|
||||
<td>@forecast.TemperatureC</td>
|
||||
<td>@forecast.TemperatureF</td>
|
||||
<td>@forecast.Summary</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
|
||||
@code {
|
||||
private WeatherForecast[]? forecasts;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
// Simulate asynchronous loading to demonstrate a loading indicator
|
||||
await Task.Delay(500);
|
||||
|
||||
var startDate = DateOnly.FromDateTime(DateTime.Now);
|
||||
var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };
|
||||
forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = startDate.AddDays(index),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = summaries[Random.Shared.Next(summaries.Length)]
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private class WeatherForecast
|
||||
{
|
||||
public DateOnly Date { get; set; }
|
||||
public int TemperatureC { get; set; }
|
||||
public string? Summary { get; set; }
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,118 @@
|
||||
<Router AppAssembly="@typeof(MauiProgram).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
@using SilverOS.Welcome.App.Components.Steps
|
||||
@using SilverOS.Welcome.Core.Flavours
|
||||
@inject IFlavourLoader FlavourLoader
|
||||
@inject WizardState State
|
||||
|
||||
<div class="wizard">
|
||||
<div class="wizard-header">
|
||||
<div class="wizard-steps-indicator">
|
||||
@for (int i = 0; i < _stepTitles.Length; i++)
|
||||
{
|
||||
var idx = i;
|
||||
<span class="wizard-step-dot @(idx == _currentStep ? "active" : idx < _currentStep ? "done" : "")">
|
||||
@_stepTitles[idx]
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="wizard-body">
|
||||
@if (_loading)
|
||||
{
|
||||
<p class="loading">Loading flavours…</p>
|
||||
}
|
||||
else if (_error is not null)
|
||||
{
|
||||
<p class="error">@_error</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
@switch (_currentStep)
|
||||
{
|
||||
case 0:
|
||||
<WelcomeStep />
|
||||
break;
|
||||
case 1:
|
||||
<FlavourStep Flavours="_flavours" />
|
||||
break;
|
||||
case 2:
|
||||
<AccountStep @ref="_accountStep" />
|
||||
break;
|
||||
case 3:
|
||||
<PrefsStep />
|
||||
break;
|
||||
case 4:
|
||||
<ApplyStep />
|
||||
break;
|
||||
case 5:
|
||||
<DoneStep />
|
||||
break;
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="wizard-footer">
|
||||
<button class="btn-secondary"
|
||||
disabled="@(_currentStep == 0)"
|
||||
@onclick="Back">
|
||||
Back
|
||||
</button>
|
||||
@if (_currentStep < _stepTitles.Length - 1 && _currentStep != 4)
|
||||
{
|
||||
<button class="btn-primary"
|
||||
disabled="@(!CanGoNext)"
|
||||
@onclick="Next">
|
||||
@(_currentStep == _stepTitles.Length - 2 ? "Apply" : "Next")
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private static readonly string[] _stepTitles = { "Welcome", "Flavour", "Account", "Prefs", "Apply", "Done" };
|
||||
|
||||
// Flavours dir: baked alongside the exe at publish time.
|
||||
private static readonly string FlavoursDir = Path.Combine(
|
||||
AppContext.BaseDirectory, "flavours");
|
||||
|
||||
private int _currentStep = 0;
|
||||
private bool _loading = true;
|
||||
private string? _error;
|
||||
private IReadOnlyList<FlavourManifest> _flavours = Array.Empty<FlavourManifest>();
|
||||
private AccountStep? _accountStep;
|
||||
|
||||
private bool CanGoNext => _currentStep switch
|
||||
{
|
||||
2 => _accountStep?.IsValid ?? false,
|
||||
_ => true
|
||||
};
|
||||
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
_flavours = FlavourLoader.Load(FlavoursDir);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_error = $"Failed to load flavours: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_loading = false;
|
||||
}
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
void Next()
|
||||
{
|
||||
if (_currentStep < _stepTitles.Length - 1)
|
||||
_currentStep++;
|
||||
}
|
||||
|
||||
void Back()
|
||||
{
|
||||
if (_currentStep > 0)
|
||||
_currentStep--;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
@inject WizardState State
|
||||
|
||||
<div class="step account-step">
|
||||
<h1>Set Up Your Account</h1>
|
||||
<p class="step-subtitle">Create your daily-use account and administrator credentials.</p>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="username">Daily Username</label>
|
||||
<input id="username" type="text" placeholder="e.g. alice"
|
||||
value="@State.Username"
|
||||
@oninput="OnUsernameInput" />
|
||||
@if (_errors.TryGetValue("username", out var ue))
|
||||
{
|
||||
<span class="field-error">@ue</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="password">Daily Password</label>
|
||||
<input id="password" type="password"
|
||||
value="@State.Password"
|
||||
@oninput="OnPasswordInput" />
|
||||
@if (_errors.TryGetValue("password", out var pe))
|
||||
{
|
||||
<span class="field-error">@pe</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="adminpassword">Administrator Password</label>
|
||||
<input id="adminpassword" type="password"
|
||||
value="@State.AdminPassword"
|
||||
@oninput="OnAdminPasswordInput" />
|
||||
@if (_errors.TryGetValue("adminpassword", out var ae))
|
||||
{
|
||||
<span class="field-error">@ae</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="field-group">
|
||||
<label for="bitlockerpin">BitLocker PIN <small>(numeric, 6+ digits)</small></label>
|
||||
<input id="bitlockerpin" type="password" inputmode="numeric" pattern="[0-9]*"
|
||||
value="@State.BitLockerPin"
|
||||
@oninput="OnPinInput" />
|
||||
@if (_errors.TryGetValue("bitlockerpin", out var be))
|
||||
{
|
||||
<span class="field-error">@be</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private readonly Dictionary<string, string> _errors = new();
|
||||
|
||||
/// <summary>True when all fields are valid. Used by the wizard host to gate Next.</summary>
|
||||
public bool IsValid { get; private set; }
|
||||
|
||||
protected override void OnInitialized() => Validate();
|
||||
|
||||
private void OnUsernameInput(ChangeEventArgs e) { State.Username = e.Value?.ToString() ?? ""; Validate(); }
|
||||
private void OnPasswordInput(ChangeEventArgs e) { State.Password = e.Value?.ToString() ?? ""; Validate(); }
|
||||
private void OnAdminPasswordInput(ChangeEventArgs e) { State.AdminPassword = e.Value?.ToString() ?? ""; Validate(); }
|
||||
private void OnPinInput(ChangeEventArgs e) { State.BitLockerPin = e.Value?.ToString() ?? ""; Validate(); }
|
||||
|
||||
void Validate()
|
||||
{
|
||||
_errors.Clear();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(State.Username))
|
||||
_errors["username"] = "Daily username is required.";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(State.Password))
|
||||
_errors["password"] = "Password is required.";
|
||||
|
||||
if (string.IsNullOrWhiteSpace(State.AdminPassword))
|
||||
_errors["adminpassword"] = "Administrator password is required.";
|
||||
|
||||
var pin = State.BitLockerPin ?? "";
|
||||
if (!System.Text.RegularExpressions.Regex.IsMatch(pin, @"^\d{6,}$"))
|
||||
_errors["bitlockerpin"] = "BitLocker PIN must be all digits and at least 6 digits long.";
|
||||
|
||||
IsValid = _errors.Count == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
@* Minimal placeholder — full wiring in Task 11 *@
|
||||
<div class="step apply-step">
|
||||
<h1>Applying Configuration</h1>
|
||||
<p class="step-subtitle">Your settings will be applied now.</p>
|
||||
<button class="btn-primary" @onclick="Start" disabled="@_started">Start</button>
|
||||
@if (_started)
|
||||
{
|
||||
<p class="apply-status">Working… please wait.</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private bool _started;
|
||||
|
||||
void Start() => _started = true;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
@inject SilverOS.Welcome.Core.Apply.IProcessRunner ProcessRunner
|
||||
|
||||
<div class="step done-step">
|
||||
<h1>All Done!</h1>
|
||||
<p>Your SilverOS device is configured and ready. Click below to restart and start using it.</p>
|
||||
<button class="btn-primary btn-restart" @onclick="RestartNow">Restart Now</button>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private async Task RestartNow()
|
||||
{
|
||||
await ProcessRunner.RunAsync("cmd.exe", "/c shutdown /r /t 5", CancellationToken.None);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
@inject WizardState State
|
||||
|
||||
<div class="step flavour-step">
|
||||
<h1>What's this device for?</h1>
|
||||
<p class="step-subtitle">Choose the flavour that best matches how this PC will be used.</p>
|
||||
<div class="flavour-grid">
|
||||
@foreach (var f in Flavours)
|
||||
{
|
||||
<div class="flavour-card @(State.Flavour?.Id == f.Id ? "selected" : "")"
|
||||
data-id="@f.Id"
|
||||
@onclick="() => Select(f)">
|
||||
<h3>@f.Label</h3>
|
||||
<p>@f.Description</p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
[Parameter] public IReadOnlyList<FlavourManifest> Flavours { get; set; } = Array.Empty<FlavourManifest>();
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
State.Flavour ??= Flavours.FirstOrDefault(f => f.IsDefault);
|
||||
}
|
||||
|
||||
void Select(FlavourManifest f)
|
||||
{
|
||||
State.Flavour = f;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
@inject WizardState State
|
||||
|
||||
<div class="step prefs-step">
|
||||
<h1>Preferences</h1>
|
||||
<p class="step-subtitle">A few final settings before we apply your configuration.</p>
|
||||
|
||||
<div class="prefs-list">
|
||||
<div class="pref-item">
|
||||
<label>
|
||||
<input type="checkbox" @bind="State.AutoUpdates" />
|
||||
Enable automatic Windows Updates
|
||||
</label>
|
||||
</div>
|
||||
<div class="pref-item">
|
||||
<label>
|
||||
<input type="checkbox" @bind="State.Telemetry" />
|
||||
Send diagnostic data to Microsoft <small>(off = privacy-max)</small>
|
||||
</label>
|
||||
</div>
|
||||
<div class="pref-item">
|
||||
<label>
|
||||
<input type="checkbox" @bind="State.InstallDefenderUpdates" />
|
||||
Keep Microsoft Defender definitions updated
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@inject WizardState State
|
||||
|
||||
<div class="step welcome-step">
|
||||
<div class="welcome-hero">
|
||||
<h1>Welcome to SilverOS</h1>
|
||||
<p class="tagline">Let's get your device set up the way you want it.</p>
|
||||
<p>This wizard will guide you through a few quick steps to configure your system, create your account, and apply the right security settings for your needs.</p>
|
||||
<p class="time-estimate">Takes about 5 minutes.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code {
|
||||
}
|
||||
@@ -9,4 +9,9 @@ public sealed class WizardState
|
||||
public string Password { get; set; } = "";
|
||||
public string AdminPassword { get; set; } = "";
|
||||
public string BitLockerPin { get; set; } = "";
|
||||
|
||||
// Prefs step
|
||||
public bool AutoUpdates { get; set; } = true;
|
||||
public bool Telemetry { get; set; } = false;
|
||||
public bool InstallDefenderUpdates { get; set; } = true;
|
||||
}
|
||||
|
||||
@@ -7,3 +7,5 @@
|
||||
@using Microsoft.JSInterop
|
||||
@using SilverOS.Welcome.App
|
||||
@using SilverOS.Welcome.App.Components
|
||||
@using SilverOS.Welcome.App.Components.Steps
|
||||
@using SilverOS.Welcome.Core.Flavours
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,10 +0,0 @@
|
||||
using Android.App;
|
||||
using Android.Content.PM;
|
||||
using Android.OS;
|
||||
|
||||
namespace SilverOS.Welcome.App;
|
||||
|
||||
[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true, ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation | ConfigChanges.UiMode | ConfigChanges.ScreenLayout | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
|
||||
public class MainActivity : MauiAppCompatActivity
|
||||
{
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
using Android.App;
|
||||
using Android.Runtime;
|
||||
|
||||
namespace SilverOS.Welcome.App;
|
||||
|
||||
[Application]
|
||||
public class MainApplication : MauiApplication
|
||||
{
|
||||
public MainApplication(IntPtr handle, JniHandleOwnership ownership)
|
||||
: base(handle, ownership)
|
||||
{
|
||||
}
|
||||
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="colorPrimary">#512BD4</color>
|
||||
<color name="colorPrimaryDark">#2B0B98</color>
|
||||
<color name="colorAccent">#2B0B98</color>
|
||||
</resources>
|
||||
@@ -1,9 +0,0 @@
|
||||
using Foundation;
|
||||
|
||||
namespace SilverOS.Welcome.App;
|
||||
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : MauiUIApplicationDelegate
|
||||
{
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
<?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>
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?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>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>
|
||||
@@ -1,15 +0,0 @@
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace SilverOS.Welcome.App;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
using System;
|
||||
using Microsoft.Maui;
|
||||
using Microsoft.Maui.Hosting;
|
||||
|
||||
namespace SilverOS.Welcome.App;
|
||||
|
||||
class Program : MauiApplication
|
||||
{
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
|
||||
static void Main(string[] args)
|
||||
{
|
||||
var app = new Program();
|
||||
app.Run(args);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="maui-application-id-placeholder" version="0.0.0" api-version="9" xmlns="http://tizen.org/ns/packages">
|
||||
<profile name="common" />
|
||||
<ui-application appid="maui-application-id-placeholder" exec="SilverOS.Welcome.App.dll" multiple="false" nodisplay="false" taskmanage="true" type="dotnet" launch_mode="single">
|
||||
<label>maui-application-title-placeholder</label>
|
||||
<icon>maui-appicon-placeholder</icon>
|
||||
<metadata key="http://tizen.org/metadata/prefer_dotnet_aot" value="true" />
|
||||
</ui-application>
|
||||
<shortcut-list />
|
||||
<privileges>
|
||||
<privilege>http://tizen.org/privilege/internet</privilege>
|
||||
</privileges>
|
||||
<dependencies />
|
||||
<provides-appdefined-privileges />
|
||||
</manifest>
|
||||
@@ -1,9 +0,0 @@
|
||||
using Foundation;
|
||||
|
||||
namespace SilverOS.Welcome.App;
|
||||
|
||||
[Register("AppDelegate")]
|
||||
public class AppDelegate : MauiUIApplicationDelegate
|
||||
{
|
||||
protected override MauiApp CreateMauiApp() => MauiProgram.CreateMauiApp();
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
<?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>
|
||||
@@ -1,15 +0,0 @@
|
||||
using ObjCRuntime;
|
||||
using UIKit;
|
||||
|
||||
namespace SilverOS.Welcome.App;
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
<?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>
|
||||
@@ -0,0 +1,23 @@
|
||||
using Bunit;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using SilverOS.Welcome.App.Components.Steps;
|
||||
using SilverOS.Welcome.App.Components;
|
||||
using SilverOS.Welcome.Core.Flavours;
|
||||
using Xunit;
|
||||
|
||||
public class FlavourStepTests : TestContext
|
||||
{
|
||||
[Fact]
|
||||
public void Renders_one_card_per_flavour_and_preselects_default()
|
||||
{
|
||||
var flavours = new[]
|
||||
{
|
||||
new FlavourManifest { Id="daily-driver", Label="Daily-Driver", IsDefault=true, Hardening=new(){Modules=new[]{"00"}} },
|
||||
new FlavourManifest { Id="privacy-max", Label="Privacy-Max", Hardening=new(){Modules=new[]{"00"}} },
|
||||
};
|
||||
Services.AddSingleton(new WizardState());
|
||||
var cut = RenderComponent<FlavourStep>(p => p.Add(s => s.Flavours, flavours));
|
||||
Assert.Equal(2, cut.FindAll(".flavour-card").Count);
|
||||
Assert.Contains("selected", cut.Find(".flavour-card[data-id=daily-driver]").ClassList);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Razor">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
<TargetFramework>net9.0-windows10.0.19041.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<UseMaui>true</UseMaui>
|
||||
<!-- Suppress MAUI OutputType so the test runner can pick up the dll -->
|
||||
<OutputType>Library</OutputType>
|
||||
<!-- Suppress MAUI implicit-package warnings; we only need the SDK for TFM compatibility -->
|
||||
<SkipValidateMauiImplicitPackageReferences>true</SkipValidateMauiImplicitPackageReferences>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="bunit" Version="1.37.7" />
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.2" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
@@ -20,7 +26,7 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\SilverOS.Welcome.Core\SilverOS.Welcome.Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\SilverOS.Welcome.App\SilverOS.Welcome.App.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user