Compare commits
10 Commits
f9fb9f59f4
...
3ecb8e9965
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3ecb8e9965 | ||
|
|
0e95da26b4 | ||
|
|
bac642c221 | ||
|
|
f6cc1b8fa4 | ||
|
|
1aa3f30fce | ||
|
|
e22ae29b3f | ||
|
|
db2439c52e | ||
|
|
36f583169c | ||
|
|
16c2ada09b | ||
|
|
5e87d81bb6 |
12
.gitignore
vendored
12
.gitignore
vendored
@@ -6,6 +6,18 @@
|
|||||||
# dotenv files
|
# dotenv files
|
||||||
.env
|
.env
|
||||||
|
|
||||||
|
# Local iOS deployment target (contains a personal device UDID).
|
||||||
|
.ios-device
|
||||||
|
|
||||||
|
# Apple signing credentials and provisioning artifacts. Keep these in Keychain or
|
||||||
|
# an approved secret store; never commit them to the application repository.
|
||||||
|
*.cer
|
||||||
|
*.p12
|
||||||
|
*.p8
|
||||||
|
*.mobileprovision
|
||||||
|
*.provisionprofile
|
||||||
|
*.certSigningRequest
|
||||||
|
|
||||||
# User-specific files
|
# User-specific files
|
||||||
*.rsuser
|
*.rsuser
|
||||||
*.suo
|
*.suo
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<Application.Resources>
|
<Application.Resources>
|
||||||
<ResourceDictionary>
|
<ResourceDictionary>
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
|
<ResourceDictionary Source="Resources/Styles/GeneratedTokens.xaml" />
|
||||||
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
|
<ResourceDictionary Source="Resources/Styles/Colors.xaml" />
|
||||||
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
|
<ResourceDictionary Source="Resources/Styles/Styles.xaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
|
|||||||
@@ -14,8 +14,12 @@ public partial class App : Application
|
|||||||
_connectionManager = connectionManager;
|
_connectionManager = connectionManager;
|
||||||
_syncService = syncService;
|
_syncService = syncService;
|
||||||
|
|
||||||
Console.WriteLine("===== FIELD LOGGER APP STARTING =====");
|
Console.WriteLine("===== UM TRACE APP STARTING =====");
|
||||||
Console.WriteLine($"Console output is working! Time: {DateTime.Now:HH:mm:ss}");
|
Console.WriteLine($"Console output is working! Time: {DateTime.Now:HH:mm:ss}");
|
||||||
|
|
||||||
|
// Root navigation now opens on Jobs, so connection recovery must not depend on
|
||||||
|
// the former Home page appearing first.
|
||||||
|
_connectionManager.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Window CreateWindow(IActivationState? activationState)
|
protected override Window CreateWindow(IActivationState? activationState)
|
||||||
|
|||||||
@@ -4,14 +4,37 @@
|
|||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:views="clr-namespace:FieldLogger.Views"
|
xmlns:views="clr-namespace:FieldLogger.Views"
|
||||||
Title="Field Logger">
|
Title="UM Trace"
|
||||||
|
FlyoutBehavior="Flyout"
|
||||||
|
BackgroundColor="{DynamicResource UlColorCanvas}"
|
||||||
|
ForegroundColor="{DynamicResource UlColorText}"
|
||||||
|
TitleColor="{DynamicResource UlColorText}"
|
||||||
|
UnselectedColor="{DynamicResource UlColorTextMuted}"
|
||||||
|
TabBarBackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
|
TabBarForegroundColor="{DynamicResource UlColorPrimary}"
|
||||||
|
TabBarTitleColor="{DynamicResource UlColorPrimary}"
|
||||||
|
TabBarUnselectedColor="{DynamicResource UlColorTextMuted}">
|
||||||
|
|
||||||
<TabBar>
|
<Shell.FlyoutHeader>
|
||||||
<ShellContent Title="Home" Route="home" ContentTemplate="{DataTemplate views:HomePage}" />
|
<Grid BackgroundColor="{DynamicResource UlColorSurfaceStrong}" Padding="20,28,20,20">
|
||||||
<ShellContent Title="Jobs" Route="jobs" ContentTemplate="{DataTemplate views:JobsPage}" />
|
<VerticalStackLayout Spacing="2">
|
||||||
<ShellContent Title="Map" Route="map" ContentTemplate="{DataTemplate views:MapPage}" />
|
<Label Text="UM TRACE" FontSize="20" FontAttributes="Bold"
|
||||||
<ShellContent Title="Debug" Route="debug" ContentTemplate="{DataTemplate views:DebugPage}" />
|
CharacterSpacing="1.5" TextColor="{DynamicResource UlColorOnStrong}" />
|
||||||
<ShellContent Title="Settings" Route="settings" ContentTemplate="{DataTemplate views:SettingsPage}" />
|
<Label Text="Utility locating" FontSize="14"
|
||||||
</TabBar>
|
TextColor="{DynamicResource UlColorBorder}" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Grid>
|
||||||
|
</Shell.FlyoutHeader>
|
||||||
|
|
||||||
|
<!-- Jobs is intentionally first: Shell selects the first flyout item at startup. -->
|
||||||
|
<FlyoutItem Title="Jobs" Route="jobs">
|
||||||
|
<ShellContent ContentTemplate="{DataTemplate views:JobsPage}" />
|
||||||
|
</FlyoutItem>
|
||||||
|
<FlyoutItem Title="Map" Route="map">
|
||||||
|
<ShellContent ContentTemplate="{DataTemplate views:MapPage}" />
|
||||||
|
</FlyoutItem>
|
||||||
|
<FlyoutItem Title="Settings" Route="settings">
|
||||||
|
<ShellContent ContentTemplate="{DataTemplate views:SettingsPage}" />
|
||||||
|
</FlyoutItem>
|
||||||
|
|
||||||
</Shell>
|
</Shell>
|
||||||
|
|||||||
@@ -10,5 +10,10 @@ public partial class AppShell : Shell
|
|||||||
|
|
||||||
Routing.RegisterRoute("devicescan", typeof(DeviceScanPage));
|
Routing.RegisterRoute("devicescan", typeof(DeviceScanPage));
|
||||||
Routing.RegisterRoute("jobdetail", typeof(JobDetailPage));
|
Routing.RegisterRoute("jobdetail", typeof(JobDetailPage));
|
||||||
|
Routing.RegisterRoute("debug", typeof(HomePage));
|
||||||
|
#if DEBUG
|
||||||
|
// The raw device console is a development-only maintenance interface.
|
||||||
|
Routing.RegisterRoute("deviceconsole", typeof(DebugPage));
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,10 @@
|
|||||||
<NoWarn>$(NoWarn);MVVMTK0045</NoWarn>
|
<NoWarn>$(NoWarn);MVVMTK0045</NoWarn>
|
||||||
|
|
||||||
<!-- Display name -->
|
<!-- Display name -->
|
||||||
<ApplicationTitle>Field Logger</ApplicationTitle>
|
<ApplicationTitle>UM Trace</ApplicationTitle>
|
||||||
|
|
||||||
<!-- App Identifier -->
|
<!-- App Identifier -->
|
||||||
<ApplicationId>com.undergroundmagnetics.fieldlogger</ApplicationId>
|
<ApplicationId>com.umagul.trace</ApplicationId>
|
||||||
|
|
||||||
<!-- Versions -->
|
<!-- Versions -->
|
||||||
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
<ApplicationDisplayVersion>1.0</ApplicationDisplayVersion>
|
||||||
@@ -46,6 +46,10 @@
|
|||||||
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
|
<SupportedOSPlatformVersion Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'tizen'">6.5</SupportedOSPlatformVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Apple Developer team used for iOS distribution: W2N8APPQ2C.
|
||||||
|
Certificate/profile selection stays outside source control and is supplied by
|
||||||
|
Keychain plus the installed com.umagul.trace provisioning profile at publish time. -->
|
||||||
|
|
||||||
<!-- SEC-1: the Google Maps Android key is injected at build time, never committed.
|
<!-- SEC-1: the Google Maps Android key is injected at build time, never committed.
|
||||||
Resolution order: (1) MapsApiKey MSBuild property (pass -p:MapsApiKey=... in CI from a
|
Resolution order: (1) MapsApiKey MSBuild property (pass -p:MapsApiKey=... in CI from a
|
||||||
secret), else (2) the MAPS_API_KEY environment variable, else (3) a gitignored
|
secret), else (2) the MAPS_API_KEY environment variable, else (3) a gitignored
|
||||||
@@ -60,10 +64,10 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<!-- App Icon -->
|
<!-- App Icon -->
|
||||||
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.svg" Color="#512BD4" />
|
<MauiIcon Include="Resources\AppIcon\appicon.svg" ForegroundFile="Resources\AppIcon\appiconfg.png" Color="#FFFFFF" />
|
||||||
|
|
||||||
<!-- Splash Screen -->
|
<!-- Splash Screen -->
|
||||||
<MauiSplashScreen Include="Resources\Splash\splash.svg" Color="#512BD4" BaseSize="128,128" />
|
<MauiSplashScreen Include="Resources\Splash\um_dual_receiver.png" Color="#EEF3F2" BaseSize="480,480" />
|
||||||
|
|
||||||
<!-- Images -->
|
<!-- Images -->
|
||||||
<MauiImage Include="Resources\Images\*" />
|
<MauiImage Include="Resources\Images\*" />
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ public static class MauiProgram
|
|||||||
builder.Services.AddSingleton<MapViewModel>();
|
builder.Services.AddSingleton<MapViewModel>();
|
||||||
builder.Services.AddSingleton<DebugViewModel>();
|
builder.Services.AddSingleton<DebugViewModel>();
|
||||||
builder.Services.AddSingleton<SettingsViewModel>();
|
builder.Services.AddSingleton<SettingsViewModel>();
|
||||||
|
builder.Services.AddSingleton<AppStatusViewModel>();
|
||||||
builder.Services.AddTransient<DeviceScanViewModel>();
|
builder.Services.AddTransient<DeviceScanViewModel>();
|
||||||
builder.Services.AddTransient<JobDetailViewModel>();
|
builder.Services.AddTransient<JobDetailViewModel>();
|
||||||
|
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/appicon.appiconset</string>
|
<string>Assets.xcassets/appicon.appiconset</string>
|
||||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||||
<string>Field Logger uses Bluetooth to connect to your Underground Magnetics locating receiver and Maglink RTK GPS receiver.</string>
|
<string>UM Trace uses Bluetooth to connect to your Underground Magnetics locating receiver and Maglink RTK GPS receiver.</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>Field Logger uses your location to display logged points on the map.</string>
|
<string>UM Trace uses your location to display logged points on the map.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -29,8 +29,19 @@
|
|||||||
<key>XSAppIconAssets</key>
|
<key>XSAppIconAssets</key>
|
||||||
<string>Assets.xcassets/appicon.appiconset</string>
|
<string>Assets.xcassets/appicon.appiconset</string>
|
||||||
<key>NSBluetoothAlwaysUsageDescription</key>
|
<key>NSBluetoothAlwaysUsageDescription</key>
|
||||||
<string>Field Logger uses Bluetooth to connect to your Underground Magnetics locating receiver and Maglink RTK GPS receiver.</string>
|
<string>UM Trace uses Bluetooth to connect to your Underground Magnetics locating receiver and MagLink RTK GNSS receiver.</string>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>Field Logger uses your location to display logged points on the map.</string>
|
<string>UM Trace uses your location to display and record georeferenced locate points during an active field session.</string>
|
||||||
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
|
<string>UM Trace uses your location during an active locating session to keep georeferenced field records current while the app is in the background.</string>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>bluetooth-central</string>
|
||||||
|
<string>location</string>
|
||||||
|
</array>
|
||||||
|
<!-- The app uses standard, exempt TLS for HTTPS and MQTTS/WSS transport; it does
|
||||||
|
not implement proprietary or non-standard cryptography. -->
|
||||||
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
<false/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -36,8 +36,6 @@ More information: https://aka.ms/maui-privacy-manifest
|
|||||||
<string>E174.1</string>
|
<string>E174.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
<!--
|
|
||||||
The entry below is only needed when you're using the Preferences API in your app.
|
|
||||||
<dict>
|
<dict>
|
||||||
<key>NSPrivacyAccessedAPIType</key>
|
<key>NSPrivacyAccessedAPIType</key>
|
||||||
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
||||||
@@ -45,7 +43,7 @@ More information: https://aka.ms/maui-privacy-manifest
|
|||||||
<array>
|
<array>
|
||||||
<string>CA92.1</string>
|
<string>CA92.1</string>
|
||||||
</array>
|
</array>
|
||||||
</dict> -->
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?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">
|
<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" />
|
<rect x="0" y="0" width="456" height="456" fill="#FFFFFF" />
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 228 B After Width: | Height: | Size: 229 B |
BIN
FieldLogger/Resources/AppIcon/appiconfg.png
Normal file
BIN
FieldLogger/Resources/AppIcon/appiconfg.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.9 KiB |
@@ -1,8 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,8 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
BIN
FieldLogger/Resources/Splash/um_dual_receiver.png
Normal file
BIN
FieldLogger/Resources/Splash/um_dual_receiver.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 265 KiB |
@@ -1,45 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<?xaml-comp compile="true" ?>
|
<?xaml-comp compile="true" ?>
|
||||||
<ResourceDictionary
|
<ResourceDictionary
|
||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||||
|
|
||||||
<!-- Note: For Android please see also Platforms\Android\Resources\values\colors.xml -->
|
<!--
|
||||||
|
Color values and legacy aliases are generated into GeneratedTokens.xaml from
|
||||||
<Color x:Key="Primary">#512BD4</Color>
|
meta/contracts/design-tokens.json. Keep palette literals out of this file.
|
||||||
<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>
|
</ResourceDictionary>
|
||||||
183
FieldLogger/Resources/Styles/GeneratedTokens.xaml
Normal file
183
FieldLogger/Resources/Styles/GeneratedTokens.xaml
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<?xaml-comp compile="true" ?>
|
||||||
|
<ResourceDictionary
|
||||||
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||||
|
|
||||||
|
<!-- GENERATED FILE — DO NOT EDIT.
|
||||||
|
Source: meta/contracts/design-tokens.json v3.0.0
|
||||||
|
Run: node meta/scripts/generate-design-tokens.mjs -->
|
||||||
|
<x:String x:Key="UlTokenVersion">3.0.0</x:String>
|
||||||
|
|
||||||
|
<!-- Named mode values. Runtime theme switching replaces the unqualified semantic colors below. -->
|
||||||
|
<Color x:Key="UlDefaultColorCanvas">#EEF3F2</Color>
|
||||||
|
<Color x:Key="UlDefaultColorSurface">#FCFEFD</Color>
|
||||||
|
<Color x:Key="UlDefaultColorSurfaceRaised">#FFFFFF</Color>
|
||||||
|
<Color x:Key="UlDefaultColorSurfaceStrong">#14252A</Color>
|
||||||
|
<Color x:Key="UlDefaultColorText">#14252A</Color>
|
||||||
|
<Color x:Key="UlDefaultColorTextMuted">#5D6B6E</Color>
|
||||||
|
<Color x:Key="UlDefaultColorPrimary">#0B7774</Color>
|
||||||
|
<Color x:Key="UlDefaultColorPrimaryPressed">#075F5D</Color>
|
||||||
|
<Color x:Key="UlDefaultColorAccent">#B82D46</Color>
|
||||||
|
<Color x:Key="UlDefaultColorBorder">#C7D2D0</Color>
|
||||||
|
<Color x:Key="UlDefaultColorFocus">#B82D46</Color>
|
||||||
|
<Color x:Key="UlDefaultColorMap">#DCE9E7</Color>
|
||||||
|
<Color x:Key="UlDefaultColorSuccess">#347A55</Color>
|
||||||
|
<Color x:Key="UlDefaultColorWarning">#A85C00</Color>
|
||||||
|
<Color x:Key="UlDefaultColorError">#B42318</Color>
|
||||||
|
<Color x:Key="UlDefaultColorNeutral">#40565B</Color>
|
||||||
|
<Color x:Key="UlDefaultColorDisabled">#819092</Color>
|
||||||
|
<Color x:Key="UlDefaultColorOnStrong">#FFFFFF</Color>
|
||||||
|
<Color x:Key="UlDefaultColorOnPrimary">#FFFFFF</Color>
|
||||||
|
<Color x:Key="UlDefaultColorOnAccent">#FFFFFF</Color>
|
||||||
|
|
||||||
|
<Color x:Key="UlSunlightColorCanvas">#FFFFFF</Color>
|
||||||
|
<Color x:Key="UlSunlightColorSurface">#FFFFFF</Color>
|
||||||
|
<Color x:Key="UlSunlightColorSurfaceRaised">#FFFFFF</Color>
|
||||||
|
<Color x:Key="UlSunlightColorSurfaceStrong">#000000</Color>
|
||||||
|
<Color x:Key="UlSunlightColorText">#000000</Color>
|
||||||
|
<Color x:Key="UlSunlightColorTextMuted">#344448</Color>
|
||||||
|
<Color x:Key="UlSunlightColorPrimary">#075F5D</Color>
|
||||||
|
<Color x:Key="UlSunlightColorPrimaryPressed">#14252A</Color>
|
||||||
|
<Color x:Key="UlSunlightColorAccent">#982138</Color>
|
||||||
|
<Color x:Key="UlSunlightColorBorder">#5D6B6E</Color>
|
||||||
|
<Color x:Key="UlSunlightColorFocus">#982138</Color>
|
||||||
|
<Color x:Key="UlSunlightColorMap">#E8F0EF</Color>
|
||||||
|
<Color x:Key="UlSunlightColorSuccess">#245F40</Color>
|
||||||
|
<Color x:Key="UlSunlightColorWarning">#754000</Color>
|
||||||
|
<Color x:Key="UlSunlightColorError">#871A12</Color>
|
||||||
|
<Color x:Key="UlSunlightColorNeutral">#14252A</Color>
|
||||||
|
<Color x:Key="UlSunlightColorDisabled">#344448</Color>
|
||||||
|
<Color x:Key="UlSunlightColorOnStrong">#FFFFFF</Color>
|
||||||
|
<Color x:Key="UlSunlightColorOnPrimary">#FFFFFF</Color>
|
||||||
|
<Color x:Key="UlSunlightColorOnAccent">#FFFFFF</Color>
|
||||||
|
|
||||||
|
<!-- Active values start in default mode. All handwritten styles consume only these keys. -->
|
||||||
|
<Color x:Key="UlColorCanvas">#EEF3F2</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushCanvas" Color="{StaticResource UlColorCanvas}" />
|
||||||
|
<Color x:Key="UlColorSurface">#FCFEFD</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushSurface" Color="{StaticResource UlColorSurface}" />
|
||||||
|
<Color x:Key="UlColorSurfaceRaised">#FFFFFF</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushSurfaceRaised" Color="{StaticResource UlColorSurfaceRaised}" />
|
||||||
|
<Color x:Key="UlColorSurfaceStrong">#14252A</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushSurfaceStrong" Color="{StaticResource UlColorSurfaceStrong}" />
|
||||||
|
<Color x:Key="UlColorText">#14252A</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushText" Color="{StaticResource UlColorText}" />
|
||||||
|
<Color x:Key="UlColorTextMuted">#5D6B6E</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushTextMuted" Color="{StaticResource UlColorTextMuted}" />
|
||||||
|
<Color x:Key="UlColorPrimary">#0B7774</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushPrimary" Color="{StaticResource UlColorPrimary}" />
|
||||||
|
<Color x:Key="UlColorPrimaryPressed">#075F5D</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushPrimaryPressed" Color="{StaticResource UlColorPrimaryPressed}" />
|
||||||
|
<Color x:Key="UlColorAccent">#B82D46</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushAccent" Color="{StaticResource UlColorAccent}" />
|
||||||
|
<Color x:Key="UlColorBorder">#C7D2D0</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushBorder" Color="{StaticResource UlColorBorder}" />
|
||||||
|
<Color x:Key="UlColorFocus">#B82D46</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushFocus" Color="{StaticResource UlColorFocus}" />
|
||||||
|
<Color x:Key="UlColorMap">#DCE9E7</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushMap" Color="{StaticResource UlColorMap}" />
|
||||||
|
<Color x:Key="UlColorSuccess">#347A55</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushSuccess" Color="{StaticResource UlColorSuccess}" />
|
||||||
|
<Color x:Key="UlColorWarning">#A85C00</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushWarning" Color="{StaticResource UlColorWarning}" />
|
||||||
|
<Color x:Key="UlColorError">#B42318</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushError" Color="{StaticResource UlColorError}" />
|
||||||
|
<Color x:Key="UlColorNeutral">#40565B</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushNeutral" Color="{StaticResource UlColorNeutral}" />
|
||||||
|
<Color x:Key="UlColorDisabled">#819092</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushDisabled" Color="{StaticResource UlColorDisabled}" />
|
||||||
|
<Color x:Key="UlColorOnStrong">#FFFFFF</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushOnStrong" Color="{StaticResource UlColorOnStrong}" />
|
||||||
|
<Color x:Key="UlColorOnPrimary">#FFFFFF</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushOnPrimary" Color="{StaticResource UlColorOnPrimary}" />
|
||||||
|
<Color x:Key="UlColorOnAccent">#FFFFFF</Color>
|
||||||
|
<SolidColorBrush x:Key="UlBrushOnAccent" Color="{StaticResource UlColorOnAccent}" />
|
||||||
|
|
||||||
|
<!-- Temporary aliases keep unmigrated MAUI screens compatible without a second color source. -->
|
||||||
|
<Color x:Key="Primary">#0B7774</Color>
|
||||||
|
<SolidColorBrush x:Key="PrimaryBrush" Color="{StaticResource Primary}" />
|
||||||
|
<Color x:Key="PrimaryDark">#075F5D</Color>
|
||||||
|
<SolidColorBrush x:Key="PrimaryDarkBrush" Color="{StaticResource PrimaryDark}" />
|
||||||
|
<Color x:Key="PrimaryDarkText">#14252A</Color>
|
||||||
|
<SolidColorBrush x:Key="PrimaryDarkTextBrush" Color="{StaticResource PrimaryDarkText}" />
|
||||||
|
<Color x:Key="Secondary">#DCE9E7</Color>
|
||||||
|
<SolidColorBrush x:Key="SecondaryBrush" Color="{StaticResource Secondary}" />
|
||||||
|
<Color x:Key="SecondaryDarkText">#5D6B6E</Color>
|
||||||
|
<SolidColorBrush x:Key="SecondaryDarkTextBrush" Color="{StaticResource SecondaryDarkText}" />
|
||||||
|
<Color x:Key="Tertiary">#B82D46</Color>
|
||||||
|
<SolidColorBrush x:Key="TertiaryBrush" Color="{StaticResource Tertiary}" />
|
||||||
|
<Color x:Key="White">#FFFFFF</Color>
|
||||||
|
<SolidColorBrush x:Key="WhiteBrush" Color="{StaticResource White}" />
|
||||||
|
<Color x:Key="Black">#14252A</Color>
|
||||||
|
<SolidColorBrush x:Key="BlackBrush" Color="{StaticResource Black}" />
|
||||||
|
<Color x:Key="Magenta">#B82D46</Color>
|
||||||
|
<SolidColorBrush x:Key="MagentaBrush" Color="{StaticResource Magenta}" />
|
||||||
|
<Color x:Key="MidnightBlue">#14252A</Color>
|
||||||
|
<SolidColorBrush x:Key="MidnightBlueBrush" Color="{StaticResource MidnightBlue}" />
|
||||||
|
<Color x:Key="OffBlack">#14252A</Color>
|
||||||
|
<SolidColorBrush x:Key="OffBlackBrush" Color="{StaticResource OffBlack}" />
|
||||||
|
<Color x:Key="Gray100">#EEF3F2</Color>
|
||||||
|
<SolidColorBrush x:Key="Gray100Brush" Color="{StaticResource Gray100}" />
|
||||||
|
<Color x:Key="Gray200">#C7D2D0</Color>
|
||||||
|
<SolidColorBrush x:Key="Gray200Brush" Color="{StaticResource Gray200}" />
|
||||||
|
<Color x:Key="Gray300">#819092</Color>
|
||||||
|
<SolidColorBrush x:Key="Gray300Brush" Color="{StaticResource Gray300}" />
|
||||||
|
<Color x:Key="Gray400">#5D6B6E</Color>
|
||||||
|
<SolidColorBrush x:Key="Gray400Brush" Color="{StaticResource Gray400}" />
|
||||||
|
<Color x:Key="Gray500">#5D6B6E</Color>
|
||||||
|
<SolidColorBrush x:Key="Gray500Brush" Color="{StaticResource Gray500}" />
|
||||||
|
<Color x:Key="Gray600">#40565B</Color>
|
||||||
|
<SolidColorBrush x:Key="Gray600Brush" Color="{StaticResource Gray600}" />
|
||||||
|
<Color x:Key="Gray900">#14252A</Color>
|
||||||
|
<SolidColorBrush x:Key="Gray900Brush" Color="{StaticResource Gray900}" />
|
||||||
|
<Color x:Key="Gray950">#14252A</Color>
|
||||||
|
<SolidColorBrush x:Key="Gray950Brush" Color="{StaticResource Gray950}" />
|
||||||
|
|
||||||
|
<!-- Cross-platform dimensions are dp in MAUI and generated as px on the web. -->
|
||||||
|
<x:Double x:Key="UlSpace2xs">4</x:Double>
|
||||||
|
<x:Double x:Key="UlSpaceXs">8</x:Double>
|
||||||
|
<x:Double x:Key="UlSpaceSm">12</x:Double>
|
||||||
|
<x:Double x:Key="UlSpaceMd">16</x:Double>
|
||||||
|
<x:Double x:Key="UlSpaceLg">24</x:Double>
|
||||||
|
<x:Double x:Key="UlSpaceXl">32</x:Double>
|
||||||
|
<x:Double x:Key="UlSpace2xl">48</x:Double>
|
||||||
|
<x:Double x:Key="UlRadiusSm">8</x:Double>
|
||||||
|
<x:Double x:Key="UlRadiusMd">12</x:Double>
|
||||||
|
<x:Double x:Key="UlRadiusLg">20</x:Double>
|
||||||
|
<x:Double x:Key="UlRadiusPill">999</x:Double>
|
||||||
|
<x:Double x:Key="UlTouchMinimum">48</x:Double>
|
||||||
|
<x:Double x:Key="UlTouchComfortable">56</x:Double>
|
||||||
|
<x:Double x:Key="UlTouchPrimary">64</x:Double>
|
||||||
|
|
||||||
|
<!-- IBM Plex is the target family. Checked-in Open Sans/system mono are offline-safe fallbacks until licensed Plex assets land. -->
|
||||||
|
<x:String x:Key="UlTypeFamilySans">OpenSansRegular</x:String>
|
||||||
|
<x:String x:Key="UlTypeFamilySansStrong">OpenSansSemibold</x:String>
|
||||||
|
<x:String x:Key="UlTypeFamilyMono">monospace</x:String>
|
||||||
|
<x:Double x:Key="UlTypeDisplaySize">40</x:Double>
|
||||||
|
<x:Double x:Key="UlTypeDisplayLineHeight">48</x:Double>
|
||||||
|
<x:Int32 x:Key="UlTypeDisplayWeight">700</x:Int32>
|
||||||
|
<x:Double x:Key="UlTypePageSize">28</x:Double>
|
||||||
|
<x:Double x:Key="UlTypePageLineHeight">34</x:Double>
|
||||||
|
<x:Int32 x:Key="UlTypePageWeight">700</x:Int32>
|
||||||
|
<x:Double x:Key="UlTypeSectionSize">22</x:Double>
|
||||||
|
<x:Double x:Key="UlTypeSectionLineHeight">28</x:Double>
|
||||||
|
<x:Int32 x:Key="UlTypeSectionWeight">600</x:Int32>
|
||||||
|
<x:Double x:Key="UlTypeBodySize">16</x:Double>
|
||||||
|
<x:Double x:Key="UlTypeBodyLineHeight">24</x:Double>
|
||||||
|
<x:Int32 x:Key="UlTypeBodyWeight">400</x:Int32>
|
||||||
|
<x:Double x:Key="UlTypeBodySmSize">14</x:Double>
|
||||||
|
<x:Double x:Key="UlTypeBodySmLineHeight">20</x:Double>
|
||||||
|
<x:Int32 x:Key="UlTypeBodySmWeight">400</x:Int32>
|
||||||
|
<x:Double x:Key="UlTypeLabelSize">14</x:Double>
|
||||||
|
<x:Double x:Key="UlTypeLabelLineHeight">20</x:Double>
|
||||||
|
<x:Int32 x:Key="UlTypeLabelWeight">600</x:Int32>
|
||||||
|
<x:Double x:Key="UlTypeMonoSize">16</x:Double>
|
||||||
|
<x:Double x:Key="UlTypeMonoLineHeight">22</x:Double>
|
||||||
|
<x:Int32 x:Key="UlTypeMonoWeight">500</x:Int32>
|
||||||
|
<x:UInt32 x:Key="UlMotionFast">120</x:UInt32>
|
||||||
|
<x:UInt32 x:Key="UlMotionBase">200</x:UInt32>
|
||||||
|
<x:Double x:Key="UlBreakpointCompact">600</x:Double>
|
||||||
|
<x:Double x:Key="UlBreakpointMedium">900</x:Double>
|
||||||
|
<x:Double x:Key="UlBreakpointWide">1200</x:Double>
|
||||||
|
</ResourceDictionary>
|
||||||
@@ -1,444 +1,293 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<?xaml-comp compile="true" ?>
|
<?xaml-comp compile="true" ?>
|
||||||
<ResourceDictionary
|
<ResourceDictionary
|
||||||
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml">
|
||||||
|
|
||||||
|
<!-- Survey Teal shared component foundation. DynamicResource colors allow sunlight switching. -->
|
||||||
|
<Style TargetType="Page" ApplyToDerivedTypes="True">
|
||||||
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorCanvas}" />
|
||||||
|
</Style>
|
||||||
<Style TargetType="ActivityIndicator">
|
<Style TargetType="ActivityIndicator">
|
||||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
<Setter Property="Color" Value="{DynamicResource UlColorPrimary}" />
|
||||||
</Style>
|
</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">
|
<Style TargetType="Border">
|
||||||
<Setter Property="Stroke" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorSurface}" />
|
||||||
<Setter Property="StrokeShape" Value="Rectangle"/>
|
<Setter Property="Stroke" Value="{DynamicResource UlColorBorder}" />
|
||||||
<Setter Property="StrokeThickness" Value="1"/>
|
<Setter Property="StrokeThickness" Value="1" />
|
||||||
</Style>
|
<Setter Property="StrokeShape" Value="RoundRectangle 12" />
|
||||||
|
|
||||||
<Style TargetType="BoxView">
|
|
||||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="Button">
|
<Style TargetType="Button">
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource PrimaryDarkText}}" />
|
<Setter Property="TextColor" Value="{DynamicResource UlColorOnPrimary}" />
|
||||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource PrimaryDark}}" />
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorPrimary}" />
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilySansStrong}" />
|
||||||
<Setter Property="FontSize" Value="14"/>
|
<Setter Property="FontSize" Value="{StaticResource UlTypeLabelSize}" />
|
||||||
<Setter Property="BorderWidth" Value="0"/>
|
<Setter Property="FontAttributes" Value="Bold" />
|
||||||
<Setter Property="CornerRadius" Value="8"/>
|
<Setter Property="BorderColor" Value="Transparent" />
|
||||||
<Setter Property="Padding" Value="14,10"/>
|
<Setter Property="BorderWidth" Value="0" />
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
<Setter Property="Padding" Value="16,12" />
|
||||||
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
|
<Setter Property="MinimumWidthRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||||
<VisualStateGroupList>
|
<VisualStateGroupList>
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
<VisualState x:Name="Normal" />
|
<VisualState x:Name="Normal" />
|
||||||
<VisualState x:Name="Disabled">
|
<VisualState x:Name="Pressed">
|
||||||
<VisualState.Setters>
|
<VisualState.Setters>
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorPrimaryPressed}" />
|
||||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray600}}" />
|
|
||||||
</VisualState.Setters>
|
</VisualState.Setters>
|
||||||
</VisualState>
|
</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 x:Name="Disabled">
|
||||||
<VisualState.Setters>
|
<VisualState.Setters>
|
||||||
<Setter Property="Color" Value="{AppThemeBinding Light={StaticResource Gray300}, Dark={StaticResource Gray600}}" />
|
<Setter Property="TextColor" Value="{DynamicResource UlColorSurfaceStrong}" />
|
||||||
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorDisabled}" />
|
||||||
|
<Setter Property="Opacity" Value="0.7" />
|
||||||
|
</VisualState.Setters>
|
||||||
|
</VisualState>
|
||||||
|
</VisualStateGroup>
|
||||||
|
<VisualStateGroup x:Name="FocusStates">
|
||||||
|
<VisualState x:Name="Unfocused" />
|
||||||
|
<VisualState x:Name="Focused">
|
||||||
|
<VisualState.Setters>
|
||||||
|
<Setter Property="BorderColor" Value="{DynamicResource UlColorFocus}" />
|
||||||
|
<Setter Property="BorderWidth" Value="3" />
|
||||||
</VisualState.Setters>
|
</VisualState.Setters>
|
||||||
</VisualState>
|
</VisualState>
|
||||||
</VisualStateGroup>
|
</VisualStateGroup>
|
||||||
</VisualStateGroupList>
|
</VisualStateGroupList>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style x:Key="UlButtonPrimary" TargetType="Button" />
|
||||||
<Style TargetType="DatePicker">
|
<Style x:Key="UlButtonSecondary" TargetType="Button">
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
<Setter Property="TextColor" Value="{DynamicResource UlColorPrimary}" />
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorSurface}" />
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
<Setter Property="BorderColor" Value="{DynamicResource UlColorPrimary}" />
|
||||||
<Setter Property="FontSize" Value="14"/>
|
<Setter Property="BorderWidth" Value="2" />
|
||||||
<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>
|
||||||
|
<Style x:Key="UlButtonAccent" TargetType="Button">
|
||||||
<Style TargetType="Editor">
|
<Setter Property="TextColor" Value="{DynamicResource UlColorOnAccent}" />
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorAccent}" />
|
||||||
<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>
|
||||||
|
<Style x:Key="UlButtonPrimaryField" TargetType="Button">
|
||||||
<Style TargetType="Entry">
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchPrimary}" />
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
<Setter Property="FontSize" Value="{StaticResource UlTypeBodySize}" />
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
<Setter Property="CornerRadius" Value="20" />
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
<Setter Property="Padding" Value="24,16" />
|
||||||
<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>
|
||||||
|
|
||||||
<Style TargetType="ImageButton">
|
<Style TargetType="ImageButton">
|
||||||
<Setter Property="Opacity" Value="1" />
|
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||||
<Setter Property="BorderColor" Value="Transparent"/>
|
<Setter Property="BorderColor" Value="Transparent" />
|
||||||
<Setter Property="BorderWidth" Value="0"/>
|
<Setter Property="BorderWidth" Value="0" />
|
||||||
<Setter Property="CornerRadius" Value="0"/>
|
<Setter Property="CornerRadius" Value="12" />
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
<Setter Property="MinimumWidthRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
<Setter Property="VisualStateManager.VisualStateGroups">
|
||||||
<VisualStateGroupList>
|
<VisualStateGroupList>
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
<VisualStateGroup x:Name="CommonStates">
|
||||||
<VisualState x:Name="Normal" />
|
<VisualState x:Name="Normal" />
|
||||||
<VisualState x:Name="Disabled">
|
<VisualState x:Name="Pressed">
|
||||||
<VisualState.Setters>
|
<VisualState.Setters><Setter Property="Opacity" Value="0.72" /></VisualState.Setters>
|
||||||
<Setter Property="Opacity" Value="0.5" />
|
</VisualState>
|
||||||
</VisualState.Setters>
|
<VisualState x:Name="Disabled">
|
||||||
|
<VisualState.Setters><Setter Property="Opacity" Value="0.45" /></VisualState.Setters>
|
||||||
</VisualState>
|
</VisualState>
|
||||||
<VisualState x:Name="PointerOver" />
|
|
||||||
</VisualStateGroup>
|
</VisualStateGroup>
|
||||||
</VisualStateGroupList>
|
</VisualStateGroupList>
|
||||||
</Setter>
|
</Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style x:Key="UlIconButton" TargetType="ImageButton">
|
||||||
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorSurface}" />
|
||||||
|
<Setter Property="BorderColor" Value="{DynamicResource UlColorBorder}" />
|
||||||
|
<Setter Property="BorderWidth" Value="1" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="Label">
|
<Style TargetType="Label">
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
<Setter Property="TextColor" Value="{DynamicResource UlColorText}" />
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular" />
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilySans}" />
|
||||||
<Setter Property="FontSize" Value="14" />
|
<Setter Property="FontSize" Value="{StaticResource UlTypeBodySize}" />
|
||||||
<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>
|
||||||
|
|
||||||
<Style TargetType="Span">
|
<Style TargetType="Span">
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
<Setter Property="TextColor" Value="{DynamicResource UlColorText}" />
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilySans}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
<Style x:Key="UlPageTitle" TargetType="Label">
|
||||||
<Style TargetType="Label" x:Key="Headline">
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilySansStrong}" />
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
|
<Setter Property="FontAttributes" Value="Bold" />
|
||||||
<Setter Property="FontSize" Value="32" />
|
<Setter Property="FontSize" Value="{StaticResource UlTypePageSize}" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="UlSectionTitle" TargetType="Label">
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilySansStrong}" />
|
||||||
|
<Setter Property="FontAttributes" Value="Bold" />
|
||||||
|
<Setter Property="FontSize" Value="{StaticResource UlTypeSectionSize}" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="UlBodyMuted" TargetType="Label">
|
||||||
|
<Setter Property="TextColor" Value="{DynamicResource UlColorTextMuted}" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="UlTechnicalValue" TargetType="Label">
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilyMono}" />
|
||||||
|
<Setter Property="FontSize" Value="{StaticResource UlTypeMonoSize}" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="Headline" TargetType="Label" BasedOn="{StaticResource UlPageTitle}">
|
||||||
|
<Setter Property="HorizontalOptions" Value="Center" />
|
||||||
|
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="SubHeadline" TargetType="Label" BasedOn="{StaticResource UlSectionTitle}">
|
||||||
<Setter Property="HorizontalOptions" Value="Center" />
|
<Setter Property="HorizontalOptions" Value="Center" />
|
||||||
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="Label" x:Key="SubHeadline">
|
<Style x:Key="UlCard" TargetType="Border">
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource MidnightBlue}, Dark={StaticResource White}}" />
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorSurfaceRaised}" />
|
||||||
<Setter Property="FontSize" Value="24" />
|
<Setter Property="Stroke" Value="{DynamicResource UlColorBorder}" />
|
||||||
<Setter Property="HorizontalOptions" Value="Center" />
|
<Setter Property="StrokeThickness" Value="1" />
|
||||||
|
<Setter Property="StrokeShape" Value="RoundRectangle 12" />
|
||||||
|
<Setter Property="Padding" Value="16" />
|
||||||
|
<Setter Property="Margin" Value="0,0,0,12" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="UlJobRow" TargetType="Border">
|
||||||
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorSurface}" />
|
||||||
|
<Setter Property="Stroke" Value="{DynamicResource UlColorBorder}" />
|
||||||
|
<Setter Property="StrokeThickness" Value="1" />
|
||||||
|
<Setter Property="Padding" Value="16,12" />
|
||||||
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchComfortable}" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="UlStatusBadge" TargetType="Border">
|
||||||
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorSurface}" />
|
||||||
|
<Setter Property="Stroke" Value="{DynamicResource UlColorNeutral}" />
|
||||||
|
<Setter Property="StrokeThickness" Value="1" />
|
||||||
|
<Setter Property="StrokeShape" Value="RoundRectangle 999" />
|
||||||
|
<Setter Property="Padding" Value="8,4" />
|
||||||
|
<!-- Consumers must supply visible status text and an icon; color is supplemental. -->
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="UlDeviceChip" TargetType="Border" BasedOn="{StaticResource UlStatusBadge}">
|
||||||
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
|
<Setter Property="Padding" Value="12,8" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="UlMetricValue" TargetType="Label">
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilyMono}" />
|
||||||
|
<Setter Property="FontAttributes" Value="Bold" />
|
||||||
|
<Setter Property="FontSize" Value="{StaticResource UlTypeDisplaySize}" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="UlEmptyStateTitle" TargetType="Label" BasedOn="{StaticResource UlSectionTitle}">
|
||||||
|
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="UlEmptyStateBody" TargetType="Label" BasedOn="{StaticResource UlBodyMuted}">
|
||||||
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
<Setter Property="HorizontalTextAlignment" Value="Center" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="ListView">
|
<Style TargetType="Entry">
|
||||||
<Setter Property="SeparatorColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray500}}" />
|
<Setter Property="TextColor" Value="{DynamicResource UlColorText}" />
|
||||||
<Setter Property="RefreshControlColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
<Setter Property="PlaceholderColor" Value="{DynamicResource UlColorTextMuted}" />
|
||||||
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorSurface}" />
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilySans}" />
|
||||||
|
<Setter Property="FontSize" Value="{StaticResource UlTypeBodySize}" />
|
||||||
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
|
<Setter Property="MinimumWidthRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
|
</Style>
|
||||||
|
<Style x:Key="UlInput" TargetType="Entry" />
|
||||||
|
<Style TargetType="Editor">
|
||||||
|
<Setter Property="TextColor" Value="{DynamicResource UlColorText}" />
|
||||||
|
<Setter Property="PlaceholderColor" Value="{DynamicResource UlColorTextMuted}" />
|
||||||
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorSurface}" />
|
||||||
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilySans}" />
|
||||||
|
<Setter Property="FontSize" Value="{StaticResource UlTypeBodySize}" />
|
||||||
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchComfortable}" />
|
||||||
|
<Setter Property="MinimumWidthRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="Picker">
|
<Style TargetType="Picker">
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
<Setter Property="TextColor" Value="{DynamicResource UlColorText}" />
|
||||||
<Setter Property="TitleColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
<Setter Property="TitleColor" Value="{DynamicResource UlColorTextMuted}" />
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorSurface}" />
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilySans}" />
|
||||||
<Setter Property="FontSize" Value="14" />
|
<Setter Property="FontSize" Value="{StaticResource UlTypeBodySize}" />
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
<Setter Property="MinimumWidthRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<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>
|
||||||
|
<Style TargetType="DatePicker">
|
||||||
<Style TargetType="ProgressBar">
|
<Setter Property="TextColor" Value="{DynamicResource UlColorText}" />
|
||||||
<Setter Property="ProgressColor" Value="{AppThemeBinding Light={StaticResource Primary}, Dark={StaticResource White}}" />
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorSurface}" />
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilySans}" />
|
||||||
<VisualStateGroupList>
|
<Setter Property="FontSize" Value="{StaticResource UlTypeBodySize}" />
|
||||||
<VisualStateGroup x:Name="CommonStates">
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<VisualState x:Name="Normal" />
|
<Setter Property="MinimumWidthRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<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>
|
||||||
|
|
||||||
<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">
|
<Style TargetType="TimePicker">
|
||||||
<Setter Property="TextColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource White}}" />
|
<Setter Property="TextColor" Value="{DynamicResource UlColorText}" />
|
||||||
<Setter Property="BackgroundColor" Value="Transparent"/>
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorSurface}" />
|
||||||
<Setter Property="FontFamily" Value="OpenSansRegular"/>
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilySans}" />
|
||||||
<Setter Property="FontSize" Value="14"/>
|
<Setter Property="FontSize" Value="{StaticResource UlTypeBodySize}" />
|
||||||
<Setter Property="MinimumHeightRequest" Value="44"/>
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<Setter Property="MinimumWidthRequest" Value="44"/>
|
<Setter Property="MinimumWidthRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<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>
|
||||||
|
<Style TargetType="SearchBar">
|
||||||
<!--
|
<Setter Property="TextColor" Value="{DynamicResource UlColorText}" />
|
||||||
<Style TargetType="TitleBar">
|
<Setter Property="PlaceholderColor" Value="{DynamicResource UlColorTextMuted}" />
|
||||||
<Setter Property="MinimumHeightRequest" Value="32"/>
|
<Setter Property="CancelButtonColor" Value="{DynamicResource UlColorPrimary}" />
|
||||||
<Setter Property="VisualStateManager.VisualStateGroups">
|
<Setter Property="BackgroundColor" Value="{DynamicResource UlColorSurface}" />
|
||||||
<VisualStateGroupList>
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilySans}" />
|
||||||
<VisualStateGroup x:Name="TitleActiveStates">
|
<Setter Property="FontSize" Value="{StaticResource UlTypeBodySize}" />
|
||||||
<VisualState x:Name="TitleBarTitleActive">
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<VisualState.Setters>
|
</Style>
|
||||||
|
<Style TargetType="CheckBox">
|
||||||
|
<Setter Property="Color" Value="{DynamicResource UlColorPrimary}" />
|
||||||
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
|
<Setter Property="MinimumWidthRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="RadioButton">
|
||||||
|
<Setter Property="TextColor" Value="{DynamicResource UlColorText}" />
|
||||||
<Setter Property="BackgroundColor" Value="Transparent" />
|
<Setter Property="BackgroundColor" Value="Transparent" />
|
||||||
<Setter Property="ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource White}}" />
|
<Setter Property="FontFamily" Value="{StaticResource UlTypeFamilySans}" />
|
||||||
</VisualState.Setters>
|
<Setter Property="FontSize" Value="{StaticResource UlTypeBodySize}" />
|
||||||
</VisualState>
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<VisualState x:Name="TitleBarTitleInactive">
|
<Setter Property="MinimumWidthRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<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>
|
||||||
-->
|
<Style TargetType="Switch">
|
||||||
|
<Setter Property="OnColor" Value="{DynamicResource UlColorPrimary}" />
|
||||||
<Style TargetType="Page" ApplyToDerivedTypes="True">
|
<Setter Property="ThumbColor" Value="{DynamicResource UlColorSurface}" />
|
||||||
<Setter Property="Padding" Value="0"/>
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
<Setter Property="BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
<Setter Property="MinimumWidthRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="Slider">
|
||||||
|
<Setter Property="MinimumTrackColor" Value="{DynamicResource UlColorPrimary}" />
|
||||||
|
<Setter Property="MaximumTrackColor" Value="{DynamicResource UlColorBorder}" />
|
||||||
|
<Setter Property="ThumbColor" Value="{DynamicResource UlColorPrimary}" />
|
||||||
|
<Setter Property="MinimumHeightRequest" Value="{StaticResource UlTouchMinimum}" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="ProgressBar">
|
||||||
|
<Setter Property="ProgressColor" Value="{DynamicResource UlColorPrimary}" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="RefreshView">
|
||||||
|
<Setter Property="RefreshColor" Value="{DynamicResource UlColorPrimary}" />
|
||||||
|
</Style>
|
||||||
|
<Style TargetType="ListView">
|
||||||
|
<Setter Property="SeparatorColor" Value="{DynamicResource UlColorBorder}" />
|
||||||
|
<Setter Property="RefreshControlColor" Value="{DynamicResource UlColorPrimary}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="Shell" ApplyToDerivedTypes="True">
|
<Style TargetType="Shell" ApplyToDerivedTypes="True">
|
||||||
<Setter Property="Shell.BackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
<Setter Property="Shell.BackgroundColor" Value="{DynamicResource UlColorSurfaceStrong}" />
|
||||||
<Setter Property="Shell.ForegroundColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
|
<Setter Property="Shell.ForegroundColor" Value="{DynamicResource UlColorOnStrong}" />
|
||||||
<Setter Property="Shell.TitleColor" Value="{AppThemeBinding Light={StaticResource Black}, Dark={StaticResource SecondaryDarkText}}" />
|
<Setter Property="Shell.TitleColor" Value="{DynamicResource UlColorOnStrong}" />
|
||||||
<Setter Property="Shell.DisabledColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
<Setter Property="Shell.DisabledColor" Value="{DynamicResource UlColorDisabled}" />
|
||||||
<Setter Property="Shell.UnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray200}}" />
|
<Setter Property="Shell.UnselectedColor" Value="{DynamicResource UlColorBorder}" />
|
||||||
<Setter Property="Shell.NavBarHasShadow" Value="False" />
|
<Setter Property="Shell.NavBarHasShadow" Value="False" />
|
||||||
<Setter Property="Shell.TabBarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Black}}" />
|
<Setter Property="Shell.TabBarBackgroundColor" Value="{DynamicResource UlColorSurfaceStrong}" />
|
||||||
<Setter Property="Shell.TabBarForegroundColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
<Setter Property="Shell.TabBarForegroundColor" Value="{DynamicResource UlColorOnStrong}" />
|
||||||
<Setter Property="Shell.TabBarTitleColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
<Setter Property="Shell.TabBarTitleColor" Value="{DynamicResource UlColorOnStrong}" />
|
||||||
<Setter Property="Shell.TabBarUnselectedColor" Value="{AppThemeBinding Light={StaticResource Gray900}, Dark={StaticResource Gray200}}" />
|
<Setter Property="Shell.TabBarUnselectedColor" Value="{DynamicResource UlColorBorder}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="NavigationPage">
|
<Style TargetType="NavigationPage">
|
||||||
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource OffBlack}}" />
|
<Setter Property="BarBackgroundColor" Value="{DynamicResource UlColorSurfaceStrong}" />
|
||||||
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
|
<Setter Property="BarTextColor" Value="{DynamicResource UlColorOnStrong}" />
|
||||||
<Setter Property="IconColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource White}}" />
|
<Setter Property="IconColor" Value="{DynamicResource UlColorOnStrong}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style TargetType="TabbedPage">
|
<Style TargetType="TabbedPage">
|
||||||
<Setter Property="BarBackgroundColor" Value="{AppThemeBinding Light={StaticResource White}, Dark={StaticResource Gray950}}" />
|
<Setter Property="BarBackgroundColor" Value="{DynamicResource UlColorSurfaceStrong}" />
|
||||||
<Setter Property="BarTextColor" Value="{AppThemeBinding Light={StaticResource Magenta}, Dark={StaticResource White}}" />
|
<Setter Property="BarTextColor" Value="{DynamicResource UlColorOnStrong}" />
|
||||||
<Setter Property="UnselectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray200}, Dark={StaticResource Gray950}}" />
|
<Setter Property="UnselectedTabColor" Value="{DynamicResource UlColorBorder}" />
|
||||||
<Setter Property="SelectedTabColor" Value="{AppThemeBinding Light={StaticResource Gray950}, Dark={StaticResource Gray200}}" />
|
<Setter Property="SelectedTabColor" Value="{DynamicResource UlColorOnStrong}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
|
|||||||
38
FieldLogger/Resources/Styles/ThemeManager.cs
Normal file
38
FieldLogger/Resources/Styles/ThemeManager.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
namespace FieldLogger.Resources.Styles;
|
||||||
|
|
||||||
|
public enum UlThemeMode
|
||||||
|
{
|
||||||
|
Default,
|
||||||
|
Sunlight,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Applies generated semantic colors. Call on the UI thread after app resources initialize.</summary>
|
||||||
|
public static class ThemeManager
|
||||||
|
{
|
||||||
|
private static readonly string[] SemanticColors =
|
||||||
|
[
|
||||||
|
"Canvas", "Surface", "SurfaceRaised", "SurfaceStrong", "Text", "TextMuted",
|
||||||
|
"Primary", "PrimaryPressed", "Accent", "Border", "Focus", "Map", "Success",
|
||||||
|
"Warning", "Error", "Neutral", "Disabled", "OnStrong", "OnPrimary", "OnAccent",
|
||||||
|
];
|
||||||
|
|
||||||
|
public static UlThemeMode Current { get; private set; } = UlThemeMode.Default;
|
||||||
|
|
||||||
|
public static void Apply(UlThemeMode mode)
|
||||||
|
{
|
||||||
|
var resources = Application.Current?.Resources
|
||||||
|
?? throw new InvalidOperationException("Application resources are not initialized.");
|
||||||
|
var prefix = mode == UlThemeMode.Sunlight ? "UlSunlightColor" : "UlDefaultColor";
|
||||||
|
|
||||||
|
foreach (var name in SemanticColors)
|
||||||
|
{
|
||||||
|
if (!resources.TryGetValue($"{prefix}{name}", out var value) || value is not Color color)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"Generated theme token {prefix}{name} is missing.");
|
||||||
|
}
|
||||||
|
resources[$"UlColor{name}"] = color;
|
||||||
|
resources[$"UlBrush{name}"] = new SolidColorBrush(color);
|
||||||
|
}
|
||||||
|
Current = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -109,7 +109,7 @@ public sealed class BleScanner
|
|||||||
new InvalidOperationException("Bluetooth is turned off."),
|
new InvalidOperationException("Bluetooth is turned off."),
|
||||||
BluetoothState.Unauthorized =>
|
BluetoothState.Unauthorized =>
|
||||||
new PermissionException(
|
new PermissionException(
|
||||||
"Bluetooth access is denied. Enable Field Logger in System Settings > Privacy & Security > Bluetooth."),
|
"Bluetooth access is denied. Enable UM Trace in System Settings > Privacy & Security > Bluetooth."),
|
||||||
BluetoothState.Unavailable =>
|
BluetoothState.Unavailable =>
|
||||||
new InvalidOperationException("Bluetooth is not available on this Mac."),
|
new InvalidOperationException("Bluetooth is not available on this Mac."),
|
||||||
_ => new InvalidOperationException($"Bluetooth is not ready (state: {_bluetooth.State}).")
|
_ => new InvalidOperationException($"Bluetooth is not ready (state: {_bluetooth.State}).")
|
||||||
|
|||||||
105
FieldLogger/ViewModels/AppStatusViewModel.cs
Normal file
105
FieldLogger/ViewModels/AppStatusViewModel.cs
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
using System.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using FieldLogger.Models;
|
||||||
|
using FieldLogger.Services;
|
||||||
|
|
||||||
|
namespace FieldLogger.ViewModels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Shared, glanceable hardware status shown above each primary workflow. The timer marks
|
||||||
|
/// position data stale even when the receiver stops sending without disconnecting.
|
||||||
|
/// </summary>
|
||||||
|
public sealed partial class AppStatusViewModel : ObservableObject
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan StaleAfter = TimeSpan.FromSeconds(2);
|
||||||
|
|
||||||
|
private readonly DeviceConnectionManager _manager;
|
||||||
|
private readonly MaglinkService _gps;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private ConnectionState _locatorState;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private ConnectionState _gnssState;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _locatorStatus = "Locator disconnected";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _gnssStatus = "GNSS disconnected";
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string _positionSummary = "No GNSS fix · accuracy —";
|
||||||
|
|
||||||
|
public AppStatusViewModel(DeviceConnectionManager manager, MaglinkService gps)
|
||||||
|
{
|
||||||
|
_manager = manager;
|
||||||
|
_gps = gps;
|
||||||
|
|
||||||
|
_manager.PropertyChanged += OnConnectionPropertyChanged;
|
||||||
|
_gps.FixReceived += OnFixReceived;
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnConnectionPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.PropertyName is nameof(DeviceConnectionManager.LocatorState)
|
||||||
|
or nameof(DeviceConnectionManager.GpsState)
|
||||||
|
or nameof(DeviceConnectionManager.LocatorName)
|
||||||
|
or nameof(DeviceConnectionManager.GpsName))
|
||||||
|
{
|
||||||
|
Refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFixReceived(object? sender, GnssFix fix) => Refresh();
|
||||||
|
|
||||||
|
/// <summary>Refreshes status safely from device callbacks or a UI dispatcher timer.</summary>
|
||||||
|
public void Refresh()
|
||||||
|
{
|
||||||
|
var dispatcher = Application.Current?.Dispatcher;
|
||||||
|
if (dispatcher?.IsDispatchRequired == true)
|
||||||
|
{
|
||||||
|
dispatcher.Dispatch(RefreshCore);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshCore();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshCore()
|
||||||
|
{
|
||||||
|
LocatorState = _manager.LocatorState;
|
||||||
|
GnssState = _manager.GpsState;
|
||||||
|
LocatorStatus = DeviceLabel("Locator", _manager.LocatorName, LocatorState);
|
||||||
|
GnssStatus = DeviceLabel("GNSS", _manager.GpsName, GnssState);
|
||||||
|
|
||||||
|
var fix = _gps.LatestFix;
|
||||||
|
if (fix is null)
|
||||||
|
{
|
||||||
|
PositionSummary = GnssState == ConnectionState.Connected
|
||||||
|
? "Waiting for fix · accuracy —"
|
||||||
|
: "No GNSS fix · accuracy —";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stale = DateTime.UtcNow - fix.ReceivedUtc > StaleAfter;
|
||||||
|
var staleLabel = stale ? " · STALE" : string.Empty;
|
||||||
|
PositionSummary = $"{fix.StatusLabel}{staleLabel} · H ±{fix.Hrms:F3} m · " +
|
||||||
|
$"V ±{fix.Vrms:F3} m · {fix.SatellitesUsed} sats · corr {fix.CorrectionAgeSeconds:F1} s";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string DeviceLabel(string kind, string? name, ConnectionState state)
|
||||||
|
{
|
||||||
|
var stateText = state switch
|
||||||
|
{
|
||||||
|
ConnectionState.Connected => "connected",
|
||||||
|
ConnectionState.Connecting => "connecting",
|
||||||
|
_ => "disconnected",
|
||||||
|
};
|
||||||
|
|
||||||
|
return state == ConnectionState.Disconnected || string.IsNullOrWhiteSpace(name)
|
||||||
|
? $"{kind} {stateText}"
|
||||||
|
: $"{kind} {stateText} · {name}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,6 +40,23 @@ public sealed partial class HomeViewModel : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _syncStatus = "";
|
private string _syncStatus = "";
|
||||||
|
|
||||||
|
public bool HasNoActiveJob => !HasActiveJob;
|
||||||
|
|
||||||
|
public bool IsDeviceConsoleAvailable { get; } =
|
||||||
|
#if DEBUG
|
||||||
|
true;
|
||||||
|
#else
|
||||||
|
false;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
public string ActiveJobPointSummary => PointCount == 1
|
||||||
|
? "1 point saved locally"
|
||||||
|
: $"{PointCount} points saved locally";
|
||||||
|
|
||||||
|
public string CaptureReadinessText => HasActiveJob
|
||||||
|
? "READY · Press LOG on the locating receiver"
|
||||||
|
: "BLOCKED · Choose or create an active job first";
|
||||||
|
|
||||||
public ObservableCollection<LoggedPoint> RecentPoints { get; } = new();
|
public ObservableCollection<LoggedPoint> RecentPoints { get; } = new();
|
||||||
|
|
||||||
public HomeViewModel(DeviceConnectionManager manager, MaglinkService gps,
|
public HomeViewModel(DeviceConnectionManager manager, MaglinkService gps,
|
||||||
@@ -63,13 +80,6 @@ public sealed partial class HomeViewModel : ObservableObject
|
|||||||
/// <summary>Called from the page's OnAppearing.</summary>
|
/// <summary>Called from the page's OnAppearing.</summary>
|
||||||
public async Task InitializeAsync()
|
public async Task InitializeAsync()
|
||||||
{
|
{
|
||||||
// First launch: nothing saved yet, send the user to device selection.
|
|
||||||
if (!_manager.HasSavedDevice(DeviceKind.Locator) && !_manager.HasSavedDevice(DeviceKind.RtkGps))
|
|
||||||
{
|
|
||||||
await Shell.Current.GoToAsync($"devicescan?kind={DeviceKind.Locator}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_manager.Start();
|
_manager.Start();
|
||||||
await RefreshJobAsync();
|
await RefreshJobAsync();
|
||||||
}
|
}
|
||||||
@@ -129,6 +139,9 @@ public sealed partial class HomeViewModel : ObservableObject
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private Task ViewMapAsync() => Shell.Current.GoToAsync("//map");
|
private Task ViewMapAsync() => Shell.Current.GoToAsync("//map");
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private Task OpenDeviceConsoleAsync() => Shell.Current.GoToAsync("deviceconsole");
|
||||||
|
|
||||||
private void OnFixReceived(object? sender, GnssFix fix)
|
private void OnFixReceived(object? sender, GnssFix fix)
|
||||||
{
|
{
|
||||||
MainThread.BeginInvokeOnMainThread(() =>
|
MainThread.BeginInvokeOnMainThread(() =>
|
||||||
@@ -149,6 +162,15 @@ public sealed partial class HomeViewModel : ObservableObject
|
|||||||
PointCount++;
|
PointCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
partial void OnHasActiveJobChanged(bool value)
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(HasNoActiveJob));
|
||||||
|
OnPropertyChanged(nameof(CaptureReadinessText));
|
||||||
|
}
|
||||||
|
|
||||||
|
partial void OnPointCountChanged(int value) =>
|
||||||
|
OnPropertyChanged(nameof(ActiveJobPointSummary));
|
||||||
|
|
||||||
private async void OnPacketIgnoredNoJob(object? sender, EventArgs e)
|
private async void OnPacketIgnoredNoJob(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
await Shell.Current.DisplayAlert("No Active Job",
|
await Shell.Current.DisplayAlert("No Active Job",
|
||||||
|
|||||||
@@ -12,53 +12,140 @@ public sealed partial class JobListItem : ObservableObject
|
|||||||
public required Job Job { get; init; }
|
public required Job Job { get; init; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(JobSummary))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(DeleteDescription))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(AccessibilityDescription))]
|
||||||
private int _pointCount;
|
private int _pointCount;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ActiveStatusLabel))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ActiveActionLabel))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ActiveActionDescription))]
|
||||||
|
[NotifyPropertyChangedFor(nameof(AccessibilityDescription))]
|
||||||
private bool _isActive;
|
private bool _isActive;
|
||||||
|
|
||||||
public string Name => Job.Name;
|
public string Name => Job.Name;
|
||||||
public string CreatedLabel => Job.CreatedUtc.ToLocalTime().ToString("g");
|
public string CreatedLabel => Job.CreatedUtc.ToLocalTime().ToString("g");
|
||||||
|
public string ActiveStatusLabel => IsActive ? "ACTIVE JOB" : "NOT ACTIVE";
|
||||||
|
public string ActiveActionLabel => IsActive ? "Active" : "Set active";
|
||||||
|
public string JobSummary => $"Created {CreatedLabel} · {PointCount} {(PointCount == 1 ? "point" : "points")}";
|
||||||
|
public string ActiveActionDescription => IsActive
|
||||||
|
? $"{Name} is already the active job"
|
||||||
|
: $"Make {Name} the active capture job";
|
||||||
|
public string DeleteDescription => $"Delete {Name} and its {PointCount} saved points";
|
||||||
|
public string AccessibilityDescription => $"{Name}. {ActiveStatusLabel}. {JobSummary}.";
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed partial class JobsViewModel : ObservableObject
|
public sealed partial class JobsViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly AppDatabase _database;
|
private readonly AppDatabase _database;
|
||||||
private readonly SettingsService _settings;
|
private readonly SettingsService _settings;
|
||||||
|
private readonly SemaphoreSlim _refreshGate = new(1, 1);
|
||||||
|
|
||||||
public ObservableCollection<JobListItem> Jobs { get; } = new();
|
public ObservableCollection<JobListItem> Jobs { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private bool _isBusy;
|
private bool _isBusy;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isRefreshing;
|
||||||
|
|
||||||
public JobsViewModel(AppDatabase database, SettingsService settings)
|
public JobsViewModel(AppDatabase database, SettingsService settings)
|
||||||
{
|
{
|
||||||
_database = database;
|
_database = database;
|
||||||
_settings = settings;
|
_settings = settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RefreshAsync()
|
public Task LoadAsync()
|
||||||
|
=> ReloadAsync(showRefreshIndicator: false, skipIfBusy: true);
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private Task RefreshAsync()
|
||||||
|
=> ReloadAsync(showRefreshIndicator: true, skipIfBusy: false);
|
||||||
|
|
||||||
|
private async Task ReloadAsync(bool showRefreshIndicator, bool skipIfBusy)
|
||||||
{
|
{
|
||||||
IsBusy = true;
|
var entered = skipIfBusy
|
||||||
|
? await _refreshGate.WaitAsync(0)
|
||||||
|
: await WaitForRefreshGateAsync();
|
||||||
|
if (!entered)
|
||||||
|
return;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
if (showRefreshIndicator)
|
||||||
|
IsRefreshing = true;
|
||||||
|
|
||||||
var jobs = await _database.GetJobsAsync();
|
var jobs = await _database.GetJobsAsync();
|
||||||
var activeId = _settings.ActiveJobId;
|
var activeId = _settings.ActiveJobId;
|
||||||
|
|
||||||
Jobs.Clear();
|
// Build a snapshot off-screen, then reconcile it with the existing observable
|
||||||
|
// collection. Keeping the existing rows prevents CollectionView from repeatedly
|
||||||
|
// discarding its measured cells and jumping back to the top.
|
||||||
|
var items = new List<JobListItem>(jobs.Count);
|
||||||
foreach (var job in jobs)
|
foreach (var job in jobs)
|
||||||
{
|
{
|
||||||
Jobs.Add(new JobListItem
|
items.Add(new JobListItem
|
||||||
{
|
{
|
||||||
Job = job,
|
Job = job,
|
||||||
PointCount = await _database.GetPointCountAsync(job.Id),
|
PointCount = await _database.GetPointCountAsync(job.Id),
|
||||||
IsActive = job.Id == activeId,
|
IsActive = job.Id == activeId,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApplySnapshot(items);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
if (showRefreshIndicator)
|
||||||
|
IsRefreshing = false;
|
||||||
IsBusy = false;
|
IsBusy = false;
|
||||||
|
_refreshGate.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> WaitForRefreshGateAsync()
|
||||||
|
{
|
||||||
|
await _refreshGate.WaitAsync();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplySnapshot(IReadOnlyList<JobListItem> incoming)
|
||||||
|
{
|
||||||
|
var incomingIds = incoming.Select(item => item.Job.Id).ToHashSet();
|
||||||
|
|
||||||
|
for (var index = Jobs.Count - 1; index >= 0; index--)
|
||||||
|
{
|
||||||
|
if (!incomingIds.Contains(Jobs[index].Job.Id))
|
||||||
|
Jobs.RemoveAt(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var targetIndex = 0; targetIndex < incoming.Count; targetIndex++)
|
||||||
|
{
|
||||||
|
var replacement = incoming[targetIndex];
|
||||||
|
var existingIndex = -1;
|
||||||
|
for (var index = targetIndex; index < Jobs.Count; index++)
|
||||||
|
{
|
||||||
|
if (Jobs[index].Job.Id == replacement.Job.Id)
|
||||||
|
{
|
||||||
|
existingIndex = index;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingIndex < 0)
|
||||||
|
{
|
||||||
|
Jobs.Insert(targetIndex, replacement);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = Jobs[existingIndex];
|
||||||
|
existing.PointCount = replacement.PointCount;
|
||||||
|
existing.IsActive = replacement.IsActive;
|
||||||
|
|
||||||
|
if (existingIndex != targetIndex)
|
||||||
|
Jobs.Move(existingIndex, targetIndex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,7 +172,7 @@ public sealed partial class JobsViewModel : ObservableObject
|
|||||||
_settings.ActiveJobId = job.Id;
|
_settings.ActiveJobId = job.Id;
|
||||||
Console.WriteLine($"NewJobAsync: Set active job to {job.Id}");
|
Console.WriteLine($"NewJobAsync: Set active job to {job.Id}");
|
||||||
|
|
||||||
await RefreshAsync();
|
await ReloadAsync(showRefreshIndicator: false, skipIfBusy: false);
|
||||||
Console.WriteLine("NewJobAsync: Refresh complete");
|
Console.WriteLine("NewJobAsync: Refresh complete");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -119,6 +206,6 @@ public sealed partial class JobsViewModel : ObservableObject
|
|||||||
await _database.DeleteJobAsync(item.Job.Id);
|
await _database.DeleteJobAsync(item.Job.Id);
|
||||||
if (_settings.ActiveJobId == item.Job.Id)
|
if (_settings.ActiveJobId == item.Job.Id)
|
||||||
_settings.ActiveJobId = null;
|
_settings.ActiveJobId = null;
|
||||||
await RefreshAsync();
|
await ReloadAsync(showRefreshIndicator: false, skipIfBusy: false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,10 +16,10 @@ public sealed partial class MapViewModel : ObservableObject
|
|||||||
private int _jobId;
|
private int _jobId;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _jobName = "";
|
private string _jobName = "Active job map";
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private string _statusText = "";
|
private string _statusText = "Loading locally saved points…";
|
||||||
|
|
||||||
public SettingsService Settings => _settings;
|
public SettingsService Settings => _settings;
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ public sealed partial class MapViewModel : ObservableObject
|
|||||||
var jobId = JobId > 0 ? JobId : _settings.ActiveJobId;
|
var jobId = JobId > 0 ? JobId : _settings.ActiveJobId;
|
||||||
if (jobId is null)
|
if (jobId is null)
|
||||||
{
|
{
|
||||||
JobName = "";
|
JobName = "No active job";
|
||||||
StatusText = "No job selected. Create or activate a job to see its points.";
|
StatusText = "No job selected. Create or activate a job to see its points.";
|
||||||
return new List<LoggedPoint>();
|
return new List<LoggedPoint>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,118 +2,31 @@ using CommunityToolkit.Mvvm.ComponentModel;
|
|||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
using FieldLogger.Models;
|
using FieldLogger.Models;
|
||||||
using FieldLogger.Services;
|
using FieldLogger.Services;
|
||||||
using FieldLogger.Services.Sync;
|
|
||||||
|
|
||||||
namespace FieldLogger.ViewModels;
|
namespace FieldLogger.ViewModels;
|
||||||
|
|
||||||
public sealed partial class SettingsViewModel : ObservableObject
|
public sealed partial class SettingsViewModel : ObservableObject
|
||||||
{
|
{
|
||||||
private readonly DeviceConnectionManager _manager;
|
private readonly DeviceConnectionManager _manager;
|
||||||
private readonly SettingsService _settings;
|
|
||||||
private readonly IMqttSyncService _sync;
|
|
||||||
|
|
||||||
public DeviceConnectionManager Manager => _manager;
|
public DeviceConnectionManager Manager => _manager;
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private string _mapsApiKey = "";
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private bool _mqttEnabled;
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private string _mqttHost = "";
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private string _mqttPort = "443";
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private string _mqttOrgId = "";
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private string _mqttPassword = "";
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private string _syncStatus = "";
|
|
||||||
|
|
||||||
[ObservableProperty]
|
|
||||||
private bool _isSavingSync;
|
|
||||||
|
|
||||||
public string MqttClientId => _settings.MqttClientId;
|
|
||||||
|
|
||||||
public string AppVersion => AppInfo.Current.VersionString;
|
public string AppVersion => AppInfo.Current.VersionString;
|
||||||
|
|
||||||
public SettingsViewModel(DeviceConnectionManager manager, SettingsService settings, IMqttSyncService sync)
|
public SettingsViewModel(DeviceConnectionManager manager)
|
||||||
{
|
{
|
||||||
_manager = manager;
|
_manager = manager;
|
||||||
_settings = settings;
|
|
||||||
_sync = sync;
|
|
||||||
MapsApiKey = settings.GoogleMapsApiKey;
|
|
||||||
MqttEnabled = settings.MqttEnabled;
|
|
||||||
MqttHost = settings.MqttHost;
|
|
||||||
MqttPort = settings.MqttPort.ToString();
|
|
||||||
MqttOrgId = settings.MqttOrgId;
|
|
||||||
SyncStatus = sync.Status;
|
|
||||||
_sync.StatusChanged += OnSyncStatusChanged;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnMapsApiKeyChanged(string value) => _settings.GoogleMapsApiKey = value.Trim();
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async Task SaveSyncAsync()
|
|
||||||
{
|
|
||||||
if (!int.TryParse(MqttPort, out var port) || port is < 1 or > 65535)
|
|
||||||
{
|
|
||||||
await Shell.Current.DisplayAlert("Invalid MQTT Port", "Enter a port between 1 and 65535.", "OK");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (MqttEnabled && (string.IsNullOrWhiteSpace(MqttHost) || string.IsNullOrWhiteSpace(MqttOrgId)))
|
|
||||||
{
|
|
||||||
await Shell.Current.DisplayAlert("Incomplete Sync Settings", "Host and organization id are required.", "OK");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsSavingSync = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_settings.MqttHost = MqttHost;
|
|
||||||
_settings.MqttPort = port;
|
|
||||||
_settings.MqttOrgId = MqttOrgId;
|
|
||||||
_settings.MqttEnabled = MqttEnabled;
|
|
||||||
if (!string.IsNullOrWhiteSpace(MqttPassword))
|
|
||||||
{
|
|
||||||
await _settings.SetMqttPasswordAsync(MqttPassword);
|
|
||||||
MqttPassword = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MqttEnabled)
|
|
||||||
{
|
|
||||||
await _sync.ConnectAsync();
|
|
||||||
await Shell.Current.DisplayAlert("Sync Connected", "The durable MQTT queue is connected.", "OK");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await _sync.DisconnectAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
await Shell.Current.DisplayAlert("Sync Connection Failed", ex.Message, "OK");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsSavingSync = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnSyncStatusChanged(object? sender, string status) =>
|
|
||||||
MainThread.BeginInvokeOnMainThread(() => SyncStatus = status);
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private Task ChangeLocatorAsync() => Shell.Current.GoToAsync($"devicescan?kind={DeviceKind.Locator}");
|
private Task ChangeLocatorAsync() => Shell.Current.GoToAsync($"devicescan?kind={DeviceKind.Locator}");
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private Task ChangeGpsAsync() => Shell.Current.GoToAsync($"devicescan?kind={DeviceKind.RtkGps}");
|
private Task ChangeGpsAsync() => Shell.Current.GoToAsync($"devicescan?kind={DeviceKind.RtkGps}");
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private Task OpenDebugAsync() => Shell.Current.GoToAsync("debug");
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task ForgetLocatorAsync()
|
private async Task ForgetLocatorAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
35
FieldLogger/Views/Controls/AppStatusBar.xaml
Normal file
35
FieldLogger/Views/Controls/AppStatusBar.xaml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||||
|
x:Class="FieldLogger.Views.Controls.AppStatusBar"
|
||||||
|
x:DataType="vm:AppStatusViewModel">
|
||||||
|
<Border BackgroundColor="{DynamicResource UlColorSurfaceStrong}"
|
||||||
|
StrokeThickness="0" Padding="12,8">
|
||||||
|
<Grid RowDefinitions="Auto,Auto" RowSpacing="4">
|
||||||
|
<Grid ColumnDefinitions="*,*" ColumnSpacing="12">
|
||||||
|
<HorizontalStackLayout Spacing="6">
|
||||||
|
<Label Text="●" FontSize="13"
|
||||||
|
TextColor="{Binding LocatorState, Converter={StaticResource StateColor}}"
|
||||||
|
VerticalTextAlignment="Center" />
|
||||||
|
<Label Text="{Binding LocatorStatus}" FontSize="12"
|
||||||
|
TextColor="{DynamicResource UlColorOnStrong}"
|
||||||
|
LineBreakMode="TailTruncation" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
<HorizontalStackLayout Grid.Column="1" Spacing="6">
|
||||||
|
<Label Text="●" FontSize="13"
|
||||||
|
TextColor="{Binding GnssState, Converter={StaticResource StateColor}}"
|
||||||
|
VerticalTextAlignment="Center" />
|
||||||
|
<Label Text="{Binding GnssStatus}" FontSize="12"
|
||||||
|
TextColor="{DynamicResource UlColorOnStrong}"
|
||||||
|
LineBreakMode="TailTruncation" />
|
||||||
|
</HorizontalStackLayout>
|
||||||
|
</Grid>
|
||||||
|
<Label Grid.Row="1" Text="{Binding PositionSummary}" FontSize="12"
|
||||||
|
FontFamily="{StaticResource UlTypeFamilyMono}"
|
||||||
|
TextColor="{DynamicResource UlColorOnStrong}"
|
||||||
|
LineBreakMode="TailTruncation"
|
||||||
|
SemanticProperties.Description="Current GNSS fix type, estimated accuracy, satellites, and correction age" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</ContentView>
|
||||||
40
FieldLogger/Views/Controls/AppStatusBar.xaml.cs
Normal file
40
FieldLogger/Views/Controls/AppStatusBar.xaml.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using FieldLogger.ViewModels;
|
||||||
|
|
||||||
|
namespace FieldLogger.Views.Controls;
|
||||||
|
|
||||||
|
public partial class AppStatusBar : ContentView
|
||||||
|
{
|
||||||
|
private IDispatcherTimer? _staleTimer;
|
||||||
|
|
||||||
|
public AppStatusBar()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnHandlerChanged()
|
||||||
|
{
|
||||||
|
base.OnHandlerChanged();
|
||||||
|
|
||||||
|
if (Handler is null)
|
||||||
|
{
|
||||||
|
_staleTimer?.Stop();
|
||||||
|
_staleTimer = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_staleTimer is not null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
_staleTimer = Dispatcher.CreateTimer();
|
||||||
|
_staleTimer.Interval = TimeSpan.FromSeconds(1);
|
||||||
|
_staleTimer.Tick += (_, _) => (BindingContext as AppStatusViewModel)?.Refresh();
|
||||||
|
_staleTimer.Start();
|
||||||
|
(BindingContext as AppStatusViewModel)?.Refresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnBindingContextChanged()
|
||||||
|
{
|
||||||
|
base.OnBindingContextChanged();
|
||||||
|
(BindingContext as AppStatusViewModel)?.Refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
15
FieldLogger/Views/Controls/FloatingMenuButton.xaml
Normal file
15
FieldLogger/Views/Controls/FloatingMenuButton.xaml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
|
x:Class="FieldLogger.Views.Controls.FloatingMenuButton">
|
||||||
|
<Button Text="☰" FontSize="24" FontAttributes="Bold"
|
||||||
|
WidthRequest="56" HeightRequest="56" CornerRadius="28"
|
||||||
|
Padding="0" Margin="16"
|
||||||
|
BackgroundColor="{DynamicResource UlColorPrimary}"
|
||||||
|
TextColor="{DynamicResource UlColorOnPrimary}"
|
||||||
|
BorderColor="{DynamicResource UlColorSurfaceRaised}" BorderWidth="2"
|
||||||
|
Clicked="OnMenuClicked"
|
||||||
|
SemanticProperties.Description="Open UM Trace menu"
|
||||||
|
MinimumHeightRequest="{StaticResource UlTouchComfortable}"
|
||||||
|
MinimumWidthRequest="{StaticResource UlTouchComfortable}" />
|
||||||
|
</ContentView>
|
||||||
15
FieldLogger/Views/Controls/FloatingMenuButton.xaml.cs
Normal file
15
FieldLogger/Views/Controls/FloatingMenuButton.xaml.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace FieldLogger.Views.Controls;
|
||||||
|
|
||||||
|
public partial class FloatingMenuButton : ContentView
|
||||||
|
{
|
||||||
|
public FloatingMenuButton()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnMenuClicked(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (Shell.Current is not null)
|
||||||
|
Shell.Current.FlyoutIsPresented = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,13 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||||
xmlns:models="clr-namespace:FieldLogger.Models"
|
xmlns:models="clr-namespace:FieldLogger.Models"
|
||||||
|
xmlns:controls="clr-namespace:FieldLogger.Views.Controls"
|
||||||
x:Class="FieldLogger.Views.DebugPage"
|
x:Class="FieldLogger.Views.DebugPage"
|
||||||
x:DataType="vm:DebugViewModel"
|
x:DataType="vm:DebugViewModel"
|
||||||
Title="Debug">
|
Title="Device Console">
|
||||||
<ScrollView>
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
<controls:AppStatusBar x:Name="AppStatus" />
|
||||||
|
<ScrollView Grid.Row="1">
|
||||||
<VerticalStackLayout Padding="16" Spacing="12">
|
<VerticalStackLayout Padding="16" Spacing="12">
|
||||||
|
|
||||||
<Label Text="Connection Status" FontAttributes="Bold" FontSize="16" />
|
<Label Text="Connection Status" FontAttributes="Bold" FontSize="16" />
|
||||||
@@ -88,4 +91,5 @@
|
|||||||
|
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
</Grid>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ public partial class DebugPage : ContentPage
|
|||||||
{
|
{
|
||||||
private readonly DebugViewModel _viewModel;
|
private readonly DebugViewModel _viewModel;
|
||||||
|
|
||||||
public DebugPage(DebugViewModel viewModel)
|
public DebugPage(DebugViewModel viewModel, AppStatusViewModel appStatusViewModel)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
_viewModel = viewModel;
|
_viewModel = viewModel;
|
||||||
BindingContext = viewModel;
|
BindingContext = viewModel;
|
||||||
|
AppStatus.BindingContext = appStatusViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnAppearing()
|
protected override void OnAppearing()
|
||||||
|
|||||||
@@ -3,82 +3,168 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||||
xmlns:models="clr-namespace:FieldLogger.Models"
|
xmlns:models="clr-namespace:FieldLogger.Models"
|
||||||
|
xmlns:controls="clr-namespace:FieldLogger.Views.Controls"
|
||||||
x:Class="FieldLogger.Views.HomePage"
|
x:Class="FieldLogger.Views.HomePage"
|
||||||
x:DataType="vm:HomeViewModel"
|
x:DataType="vm:HomeViewModel"
|
||||||
Title="Field Logger">
|
Title="Debug"
|
||||||
<ScrollView>
|
BackgroundColor="{DynamicResource UlColorCanvas}">
|
||||||
<VerticalStackLayout Padding="16" Spacing="12">
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
<controls:AppStatusBar x:Name="AppStatus" />
|
||||||
<!-- Device connection status -->
|
<ScrollView Grid.Row="1">
|
||||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
<VerticalStackLayout Padding="16" Spacing="16">
|
||||||
<Grid ColumnDefinitions="16,*,Auto" RowDefinitions="Auto,Auto" RowSpacing="10" ColumnSpacing="10">
|
|
||||||
<Ellipse Grid.Row="0" Grid.Column="0" WidthRequest="12" HeightRequest="12" VerticalOptions="Center"
|
|
||||||
Fill="{Binding Manager.LocatorState, Converter={StaticResource StateColor}}" />
|
|
||||||
<VerticalStackLayout Grid.Row="0" Grid.Column="1" Spacing="0">
|
|
||||||
<Label Text="{Binding Manager.LocatorName, TargetNullValue='Locating Receiver — not selected'}" FontAttributes="Bold" />
|
|
||||||
<Label Text="{Binding Manager.LocatorState, Converter={StaticResource StateText}}" FontSize="12" TextColor="Gray" />
|
|
||||||
</VerticalStackLayout>
|
|
||||||
<Button Grid.Row="0" Grid.Column="2" Text="Select" FontSize="12" Padding="10,4"
|
|
||||||
Command="{Binding SelectLocatorCommand}" />
|
|
||||||
|
|
||||||
<Ellipse Grid.Row="1" Grid.Column="0" WidthRequest="12" HeightRequest="12" VerticalOptions="Center"
|
|
||||||
Fill="{Binding Manager.GpsState, Converter={StaticResource StateColor}}" />
|
|
||||||
<VerticalStackLayout Grid.Row="1" Grid.Column="1" Spacing="0">
|
|
||||||
<Label Text="{Binding Manager.GpsName, TargetNullValue='Maglink RTK GPS — not selected'}" FontAttributes="Bold" />
|
|
||||||
<Label Text="{Binding Manager.GpsState, Converter={StaticResource StateText}}" FontSize="12" TextColor="Gray" />
|
|
||||||
</VerticalStackLayout>
|
|
||||||
<Button Grid.Row="1" Grid.Column="2" Text="Select" FontSize="12" Padding="10,4"
|
|
||||||
Command="{Binding SelectGpsCommand}" />
|
|
||||||
</Grid>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Live GNSS fix -->
|
|
||||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
|
||||||
<VerticalStackLayout Spacing="4">
|
<VerticalStackLayout Spacing="4">
|
||||||
<HorizontalStackLayout Spacing="8">
|
<Label Text="UM TRACE DEBUG" FontSize="14" FontAttributes="Bold" CharacterSpacing="1.5"
|
||||||
<Label Text="GPS" FontAttributes="Bold" />
|
TextColor="{DynamicResource UlColorPrimary}" />
|
||||||
<Label Text="{Binding FixStatusLabel}" TextColor="DodgerBlue" FontAttributes="Bold" />
|
<Label Text="Capture diagnostics" Style="{StaticResource UlPageTitle}"
|
||||||
</HorizontalStackLayout>
|
TextColor="{DynamicResource UlColorText}"
|
||||||
<Label Text="{Binding FixSummary}" FontFamily="Courier New" FontSize="13" />
|
SemanticProperties.HeadingLevel="Level1" />
|
||||||
<Label Text="{Binding FixAccuracy}" FontSize="12" TextColor="Gray" />
|
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Active job -->
|
<Border Style="{StaticResource UlCard}" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
Stroke="{DynamicResource UlColorBorder}" StrokeThickness="1"
|
||||||
<VerticalStackLayout Spacing="8">
|
StrokeShape="RoundRectangle 12" Padding="16"
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
SemanticProperties.Description="Active job and local capture count">
|
||||||
<VerticalStackLayout Spacing="0">
|
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="*,Auto" RowSpacing="8" ColumnSpacing="12">
|
||||||
<Label Text="{Binding ActiveJobName}" FontAttributes="Bold" FontSize="16" />
|
<Label Text="ACTIVE JOB" FontSize="14" FontAttributes="Bold"
|
||||||
<Label FontSize="12" TextColor="Gray">
|
TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
<Label.Text>
|
<Label Grid.Row="1" Text="{Binding ActiveJobName}" FontSize="22" FontAttributes="Bold"
|
||||||
<Binding Path="PointCount" StringFormat="{}{0} points logged" />
|
TextColor="{DynamicResource UlColorText}" LineBreakMode="TailTruncation" />
|
||||||
</Label.Text>
|
<Label Grid.Row="2" Text="{Binding ActiveJobPointSummary}" FontSize="16"
|
||||||
</Label>
|
TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
</VerticalStackLayout>
|
<Button Grid.RowSpan="3" Grid.Column="1" Style="{StaticResource UlButtonSecondary}"
|
||||||
<HorizontalStackLayout Grid.Column="1" Spacing="6">
|
Text="Jobs" Command="{Binding SelectJobCommand}"
|
||||||
<Button Text="New Job" FontSize="12" Padding="10,4" Command="{Binding NewJobCommand}" />
|
MinimumHeightRequest="{StaticResource UlTouchMinimum}" MinimumWidthRequest="72" FontAttributes="Bold"
|
||||||
<Button Text="Jobs" FontSize="12" Padding="10,4" Command="{Binding SelectJobCommand}" />
|
SemanticProperties.Description="Choose or review the active job" />
|
||||||
</HorizontalStackLayout>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Label Text="Press the log button on the receiver to capture a point."
|
</Border>
|
||||||
FontSize="12" TextColor="Gray" IsVisible="{Binding HasActiveJob}" />
|
|
||||||
<Label Text="{Binding SyncStatus}" FontSize="11" TextColor="DodgerBlue" />
|
<!-- Production capture remains receiver-triggered. This is deliberately guidance,
|
||||||
|
not an app button that would invent an unsupported device command. -->
|
||||||
|
<Border BackgroundColor="{DynamicResource UlColorPrimary}" StrokeThickness="0"
|
||||||
|
StrokeShape="RoundRectangle 20" Padding="20,16" MinimumHeightRequest="{StaticResource UlTouchPrimary}"
|
||||||
|
SemanticProperties.Description="Hardware capture action. Press the log button on the locating receiver.">
|
||||||
|
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="12">
|
||||||
|
<Label Text="◎" FontSize="30" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource UlColorOnPrimary}" VerticalTextAlignment="Center" />
|
||||||
|
<VerticalStackLayout Grid.Column="1" Spacing="2" VerticalOptions="Center">
|
||||||
|
<Label Text="CAPTURE FROM RECEIVER" FontSize="18" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource UlColorOnPrimary}" />
|
||||||
|
<Label Text="{Binding CaptureReadinessText}" FontSize="14"
|
||||||
|
TextColor="{DynamicResource UlColorOnPrimary}" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Button Style="{StaticResource UlButtonPrimaryField}"
|
||||||
|
Text="Choose an active job" IsVisible="{Binding HasNoActiveJob}"
|
||||||
|
Command="{Binding SelectJobCommand}" MinimumHeightRequest="{StaticResource UlTouchComfortable}" FontAttributes="Bold"
|
||||||
|
SemanticProperties.Description="Capture is blocked. Choose or create an active job." />
|
||||||
|
|
||||||
|
<Label Text="DEVICE STATUS" FontSize="14" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource UlColorTextMuted}"
|
||||||
|
SemanticProperties.HeadingLevel="Level2" />
|
||||||
|
<Grid ColumnDefinitions="*,*" ColumnSpacing="8">
|
||||||
|
<Border Style="{StaticResource UlCard}" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
|
Stroke="{DynamicResource UlColorBorder}" StrokeShape="RoundRectangle 12"
|
||||||
|
Padding="12" MinimumHeightRequest="96">
|
||||||
|
<VerticalStackLayout Spacing="4">
|
||||||
|
<Label Text="LOCATOR" FontSize="14" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
|
<Label Text="{Binding Manager.LocatorName, TargetNullValue='Not selected'}"
|
||||||
|
FontSize="16" FontAttributes="Bold" TextColor="{DynamicResource UlColorText}"
|
||||||
|
LineBreakMode="TailTruncation" />
|
||||||
|
<Label Text="{Binding Manager.LocatorState, Converter={StaticResource StateText}}"
|
||||||
|
FontSize="14" TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
|
<Button Style="{StaticResource UlButtonSecondary}"
|
||||||
|
Text="Select locator" Command="{Binding SelectLocatorCommand}"
|
||||||
|
MinimumHeightRequest="{StaticResource UlTouchMinimum}" Margin="0,4,0,0"
|
||||||
|
SemanticProperties.Description="Select the locating receiver" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Border>
|
||||||
|
<Border Grid.Column="1" Style="{StaticResource UlCard}" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
|
Stroke="{DynamicResource UlColorBorder}" StrokeShape="RoundRectangle 12"
|
||||||
|
Padding="12" MinimumHeightRequest="96">
|
||||||
|
<VerticalStackLayout Spacing="4">
|
||||||
|
<Label Text="RTK GPS" FontSize="14" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
|
<Label Text="{Binding Manager.GpsName, TargetNullValue='Not selected'}"
|
||||||
|
FontSize="16" FontAttributes="Bold" TextColor="{DynamicResource UlColorText}"
|
||||||
|
LineBreakMode="TailTruncation" />
|
||||||
|
<Label Text="{Binding Manager.GpsState, Converter={StaticResource StateText}}"
|
||||||
|
FontSize="14" TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
|
<Button Style="{StaticResource UlButtonSecondary}"
|
||||||
|
Text="Select GPS" Command="{Binding SelectGpsCommand}"
|
||||||
|
MinimumHeightRequest="{StaticResource UlTouchMinimum}" Margin="0,4,0,0"
|
||||||
|
SemanticProperties.Description="Select the Maglink RTK GPS receiver" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Border Style="{StaticResource UlCard}" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
|
Stroke="{DynamicResource UlColorBorder}" StrokeShape="RoundRectangle 12" Padding="16">
|
||||||
|
<VerticalStackLayout Spacing="8">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||||
|
<Label Text="POSITION FIX" FontSize="14" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
|
<Label Grid.Column="1" Text="{Binding FixStatusLabel}" FontSize="16" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource UlColorText}"
|
||||||
|
SemanticProperties.Description="Current GPS fix quality" />
|
||||||
|
</Grid>
|
||||||
|
<Label Text="{Binding FixSummary}" FontSize="18" FontFamily="{StaticResource UlTypeFamilyMono}"
|
||||||
|
TextColor="{DynamicResource UlColorText}" />
|
||||||
|
<Label Text="{Binding FixAccuracy}" FontSize="14"
|
||||||
|
TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Recent points -->
|
<Border Style="{StaticResource UlCard}" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
<Label Text="Recent Points" FontAttributes="Bold" Margin="4,8,0,0" />
|
Stroke="{DynamicResource UlColorBorder}" StrokeShape="RoundRectangle 12" Padding="16">
|
||||||
<CollectionView ItemsSource="{Binding RecentPoints}" HeightRequest="320">
|
<Grid ColumnDefinitions="Auto,*" ColumnSpacing="10">
|
||||||
|
<Label Text="↕" FontSize="22" TextColor="{DynamicResource UlColorPrimary}" />
|
||||||
|
<VerticalStackLayout Grid.Column="1" Spacing="2">
|
||||||
|
<Label Text="SYNC STATUS" FontSize="14" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
|
<Label Text="{Binding SyncStatus}" FontSize="16" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource UlColorText}"
|
||||||
|
SemanticProperties.Description="Current synchronization status" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<Label Text="RECENT CAPTURES" FontSize="14" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource UlColorTextMuted}"
|
||||||
|
SemanticProperties.HeadingLevel="Level2" />
|
||||||
|
<Button Grid.Column="1" Style="{StaticResource UlButtonSecondary}"
|
||||||
|
Text="View map" Command="{Binding ViewMapCommand}"
|
||||||
|
MinimumHeightRequest="{StaticResource UlTouchMinimum}" MinimumWidthRequest="96"
|
||||||
|
SemanticProperties.Description="View captured points on the map" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<CollectionView ItemsSource="{Binding RecentPoints}" HeightRequest="360"
|
||||||
|
SemanticProperties.Description="Ten most recent locally saved capture points">
|
||||||
<CollectionView.EmptyView>
|
<CollectionView.EmptyView>
|
||||||
<Label Text="No points logged yet." TextColor="Gray" Margin="8" />
|
<Border Style="{StaticResource UlCard}" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
|
Stroke="{DynamicResource UlColorBorder}"
|
||||||
|
StrokeShape="RoundRectangle 12" Padding="20">
|
||||||
|
<VerticalStackLayout Spacing="6">
|
||||||
|
<Label Text="No captures yet" Style="{StaticResource UlEmptyStateTitle}"
|
||||||
|
HorizontalTextAlignment="Center" TextColor="{DynamicResource UlColorText}" />
|
||||||
|
<Label Text="With an active job, press LOG on the receiver to save the first point."
|
||||||
|
Style="{StaticResource UlEmptyStateBody}"
|
||||||
|
FontSize="14" HorizontalTextAlignment="Center"
|
||||||
|
TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Border>
|
||||||
</CollectionView.EmptyView>
|
</CollectionView.EmptyView>
|
||||||
<CollectionView.ItemTemplate>
|
<CollectionView.ItemTemplate>
|
||||||
<DataTemplate x:DataType="models:LoggedPoint">
|
<DataTemplate x:DataType="models:LoggedPoint">
|
||||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#FAFAFA, Dark=#242426}"
|
<Border Style="{StaticResource UlJobRow}" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
StrokeShape="RoundRectangle 8" Padding="10" Margin="0,2">
|
Stroke="{DynamicResource UlColorBorder}"
|
||||||
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto">
|
StrokeShape="RoundRectangle 12" Padding="12" Margin="0,0,0,8"
|
||||||
<Label Grid.Row="0" FontSize="13" FontAttributes="Bold">
|
MinimumHeightRequest="76">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto" RowSpacing="6" ColumnSpacing="12">
|
||||||
|
<Label FontSize="16" FontAttributes="Bold" TextColor="{DynamicResource UlColorText}">
|
||||||
<Label.Text>
|
<Label.Text>
|
||||||
<MultiBinding StringFormat="{}{0} · {1} Hz · depth {2}">
|
<MultiBinding StringFormat="{}{0} · {1} Hz · depth {2}">
|
||||||
<Binding Path="Utility" Converter="{StaticResource UtilityName}" />
|
<Binding Path="Utility" Converter="{StaticResource UtilityName}" />
|
||||||
@@ -87,9 +173,10 @@
|
|||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
</Label.Text>
|
</Label.Text>
|
||||||
</Label>
|
</Label>
|
||||||
<Label Grid.Row="0" Grid.Column="1" FontSize="12" TextColor="Gray"
|
<Label Grid.Column="1" FontSize="14" TextColor="{DynamicResource UlColorTextMuted}"
|
||||||
Text="{Binding TimestampUtc, StringFormat='{0:HH:mm:ss}'}" />
|
Text="{Binding TimestampUtc, StringFormat='{0:HH:mm:ss}'}" />
|
||||||
<Label Grid.Row="1" Grid.ColumnSpan="2" FontSize="12" TextColor="Gray">
|
<Label Grid.Row="1" Grid.ColumnSpan="2" FontSize="14" FontFamily="{StaticResource UlTypeFamilyMono}"
|
||||||
|
TextColor="{DynamicResource UlColorTextMuted}">
|
||||||
<Label.Text>
|
<Label.Text>
|
||||||
<MultiBinding StringFormat="{}{0:F7}, {1:F7} · {2}">
|
<MultiBinding StringFormat="{}{0:F7}, {1:F7} · {2}">
|
||||||
<Binding Path="Latitude" />
|
<Binding Path="Latitude" />
|
||||||
@@ -104,6 +191,12 @@
|
|||||||
</CollectionView.ItemTemplate>
|
</CollectionView.ItemTemplate>
|
||||||
</CollectionView>
|
</CollectionView>
|
||||||
|
|
||||||
|
<Button Style="{StaticResource UlButtonSecondary}"
|
||||||
|
Text="Open device console" Command="{Binding OpenDeviceConsoleCommand}"
|
||||||
|
IsVisible="{Binding IsDeviceConsoleAvailable}"
|
||||||
|
MinimumHeightRequest="{StaticResource UlTouchMinimum}"
|
||||||
|
SemanticProperties.Description="Open raw locator and GNSS device messages" />
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
</Grid>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -7,10 +7,11 @@ public partial class HomePage : ContentPage
|
|||||||
private readonly HomeViewModel _viewModel;
|
private readonly HomeViewModel _viewModel;
|
||||||
private bool _initialized;
|
private bool _initialized;
|
||||||
|
|
||||||
public HomePage(HomeViewModel viewModel)
|
public HomePage(HomeViewModel viewModel, AppStatusViewModel appStatusViewModel)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
BindingContext = _viewModel = viewModel;
|
BindingContext = _viewModel = viewModel;
|
||||||
|
AppStatus.BindingContext = appStatusViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnAppearing()
|
protected override async void OnAppearing()
|
||||||
|
|||||||
@@ -3,39 +3,69 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||||
xmlns:models="clr-namespace:FieldLogger.Models"
|
xmlns:models="clr-namespace:FieldLogger.Models"
|
||||||
|
xmlns:controls="clr-namespace:FieldLogger.Views.Controls"
|
||||||
x:Class="FieldLogger.Views.JobDetailPage"
|
x:Class="FieldLogger.Views.JobDetailPage"
|
||||||
x:DataType="vm:JobDetailViewModel"
|
x:DataType="vm:JobDetailViewModel"
|
||||||
Title="{Binding JobName}">
|
Title="{Binding JobName}"
|
||||||
<ContentPage.ToolbarItems>
|
BackgroundColor="{DynamicResource UlColorCanvas}">
|
||||||
<ToolbarItem Text="Map" Command="{Binding ViewMapCommand}" />
|
<Grid RowDefinitions="Auto,*">
|
||||||
</ContentPage.ToolbarItems>
|
<controls:AppStatusBar x:Name="AppStatus" />
|
||||||
|
<Grid Grid.Row="1" RowDefinitions="Auto,*" Padding="16" RowSpacing="16">
|
||||||
|
<Border Style="{StaticResource UlCard}" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
|
Stroke="{DynamicResource UlColorBorder}" StrokeShape="RoundRectangle 12" Padding="16">
|
||||||
|
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto" RowSpacing="6" ColumnSpacing="12">
|
||||||
|
<VerticalStackLayout>
|
||||||
|
<Label Text="JOB DETAIL" FontSize="14" FontAttributes="Bold" CharacterSpacing="1.5"
|
||||||
|
TextColor="{DynamicResource UlColorPrimary}" />
|
||||||
|
<Label Text="{Binding JobName}" Style="{StaticResource UlPageTitle}"
|
||||||
|
TextColor="{DynamicResource UlColorText}" LineBreakMode="TailTruncation"
|
||||||
|
SemanticProperties.HeadingLevel="Level1" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
<Button Grid.Column="1" Style="{StaticResource UlButtonSecondary}"
|
||||||
|
Text="View map" Command="{Binding ViewMapCommand}"
|
||||||
|
MinimumHeightRequest="{StaticResource UlTouchMinimum}" MinimumWidthRequest="96"
|
||||||
|
SemanticProperties.Description="View this job's captured points on the map" />
|
||||||
|
<Label Grid.Row="1" Grid.ColumnSpan="2" FontSize="16"
|
||||||
|
TextColor="{DynamicResource UlColorTextMuted}"
|
||||||
|
Text="{Binding PointCount, StringFormat='{0} locally logged points'}" />
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<Grid RowDefinitions="Auto,*" Padding="16" RowSpacing="8">
|
<CollectionView Grid.Row="1" ItemsSource="{Binding Points}"
|
||||||
<Label Grid.Row="0" FontSize="13" TextColor="Gray"
|
SemanticProperties.Description="Captured points for this job">
|
||||||
Text="{Binding PointCount, StringFormat='{0} logged points'}" />
|
|
||||||
|
|
||||||
<CollectionView Grid.Row="1" ItemsSource="{Binding Points}">
|
|
||||||
<CollectionView.EmptyView>
|
<CollectionView.EmptyView>
|
||||||
<Label Text="No points logged for this job yet." TextColor="Gray" Margin="8" />
|
<Border Style="{StaticResource UlCard}" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
|
Stroke="{DynamicResource UlColorBorder}"
|
||||||
|
StrokeShape="RoundRectangle 12" Padding="24">
|
||||||
|
<VerticalStackLayout Spacing="8">
|
||||||
|
<Label Text="No captures for this job" Style="{StaticResource UlEmptyStateTitle}"
|
||||||
|
HorizontalTextAlignment="Center" TextColor="{DynamicResource UlColorText}" />
|
||||||
|
<Label Text="Open Debug from Settings and press LOG on the receiver when the job is active."
|
||||||
|
Style="{StaticResource UlEmptyStateBody}"
|
||||||
|
FontSize="16" HorizontalTextAlignment="Center"
|
||||||
|
TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Border>
|
||||||
</CollectionView.EmptyView>
|
</CollectionView.EmptyView>
|
||||||
<CollectionView.ItemTemplate>
|
<CollectionView.ItemTemplate>
|
||||||
<DataTemplate x:DataType="models:LoggedPoint">
|
<DataTemplate x:DataType="models:LoggedPoint">
|
||||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}"
|
<Border Style="{StaticResource UlJobRow}" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
StrokeShape="RoundRectangle 10" Padding="12" Margin="0,4">
|
Stroke="{DynamicResource UlColorBorder}" StrokeShape="RoundRectangle 12"
|
||||||
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto,Auto,Auto" RowSpacing="2">
|
Padding="16" Margin="0,0,0,12" MinimumHeightRequest="168">
|
||||||
<Label Grid.Row="0" FontAttributes="Bold" FontSize="14">
|
<Grid ColumnDefinitions="*,Auto" RowDefinitions="Auto,Auto,Auto,Auto" RowSpacing="8" ColumnSpacing="12">
|
||||||
|
<Label FontSize="18" FontAttributes="Bold" TextColor="{DynamicResource UlColorText}">
|
||||||
<Label.Text>
|
<Label.Text>
|
||||||
<MultiBinding StringFormat="{}#{0} · {1} · {2} Hz">
|
<MultiBinding StringFormat="{}Point #{0} · {1} · {2} Hz">
|
||||||
<Binding Path="Id" />
|
<Binding Path="Id" />
|
||||||
<Binding Path="Utility" Converter="{StaticResource UtilityName}" />
|
<Binding Path="Utility" Converter="{StaticResource UtilityName}" />
|
||||||
<Binding Path="Frequency" />
|
<Binding Path="Frequency" />
|
||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
</Label.Text>
|
</Label.Text>
|
||||||
</Label>
|
</Label>
|
||||||
<Label Grid.Row="0" Grid.Column="1" FontSize="12" TextColor="Gray"
|
<Label Grid.Column="1" FontSize="14" TextColor="{DynamicResource UlColorTextMuted}"
|
||||||
Text="{Binding TimestampUtc, StringFormat='{0:g}'}" />
|
Text="{Binding TimestampUtc, StringFormat='{0:g}'}" />
|
||||||
|
<Label Grid.Row="1" Grid.ColumnSpan="2" FontSize="16"
|
||||||
<Label Grid.Row="1" Grid.ColumnSpan="2" FontSize="12">
|
TextColor="{DynamicResource UlColorText}">
|
||||||
<Label.Text>
|
<Label.Text>
|
||||||
<MultiBinding StringFormat="Depth {0} · Current {1} · Signal {2} · Gain {3} dB">
|
<MultiBinding StringFormat="Depth {0} · Current {1} · Signal {2} · Gain {3} dB">
|
||||||
<Binding Path="DepthRaw" />
|
<Binding Path="DepthRaw" />
|
||||||
@@ -45,8 +75,8 @@
|
|||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
</Label.Text>
|
</Label.Text>
|
||||||
</Label>
|
</Label>
|
||||||
|
<Label Grid.Row="2" Grid.ColumnSpan="2" FontSize="16" FontFamily="{StaticResource UlTypeFamilyMono}"
|
||||||
<Label Grid.Row="2" Grid.ColumnSpan="2" FontSize="12" FontFamily="Courier New">
|
TextColor="{DynamicResource UlColorText}">
|
||||||
<Label.Text>
|
<Label.Text>
|
||||||
<MultiBinding StringFormat="{}{0:F8}, {1:F8}">
|
<MultiBinding StringFormat="{}{0:F8}, {1:F8}">
|
||||||
<Binding Path="Latitude" />
|
<Binding Path="Latitude" />
|
||||||
@@ -54,24 +84,27 @@
|
|||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
</Label.Text>
|
</Label.Text>
|
||||||
</Label>
|
</Label>
|
||||||
|
<Label Grid.Row="3" FontSize="14" TextColor="{DynamicResource UlColorTextMuted}">
|
||||||
<Label Grid.Row="3" FontSize="11" TextColor="Gray">
|
|
||||||
<Label.Text>
|
<Label.Text>
|
||||||
<MultiBinding StringFormat="{}{0} · H ±{1:F3} m · {2} sats">
|
<MultiBinding StringFormat="{}{0} · H ±{1:F3} m · {2} satellites">
|
||||||
<Binding Path="FixStatus" Converter="{StaticResource FixStatusName}" />
|
<Binding Path="FixStatus" Converter="{StaticResource FixStatusName}" />
|
||||||
<Binding Path="Hrms" />
|
<Binding Path="Hrms" />
|
||||||
<Binding Path="SatellitesUsed" />
|
<Binding Path="SatellitesUsed" />
|
||||||
</MultiBinding>
|
</MultiBinding>
|
||||||
</Label.Text>
|
</Label.Text>
|
||||||
</Label>
|
</Label>
|
||||||
<Button Grid.Row="3" Grid.Column="1" Text="Delete" FontSize="11" Padding="8,2"
|
<Button Grid.Row="3" Grid.Column="1" Style="{StaticResource UlButtonAccent}"
|
||||||
BackgroundColor="Transparent" TextColor="IndianRed"
|
Text="Delete point" MinimumHeightRequest="{StaticResource UlTouchMinimum}"
|
||||||
|
BackgroundColor="{DynamicResource UlColorAccent}"
|
||||||
|
TextColor="{DynamicResource UlColorOnAccent}"
|
||||||
Command="{Binding DeletePointCommand, Source={RelativeSource AncestorType={x:Type vm:JobDetailViewModel}}}"
|
Command="{Binding DeletePointCommand, Source={RelativeSource AncestorType={x:Type vm:JobDetailViewModel}}}"
|
||||||
CommandParameter="{Binding .}" />
|
CommandParameter="{Binding .}"
|
||||||
|
SemanticProperties.Description="Delete this locally saved point" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</CollectionView.ItemTemplate>
|
</CollectionView.ItemTemplate>
|
||||||
</CollectionView>
|
</CollectionView>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
</Grid>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ public partial class JobDetailPage : ContentPage
|
|||||||
{
|
{
|
||||||
private readonly JobDetailViewModel _viewModel;
|
private readonly JobDetailViewModel _viewModel;
|
||||||
|
|
||||||
public JobDetailPage(JobDetailViewModel viewModel)
|
public JobDetailPage(JobDetailViewModel viewModel, AppStatusViewModel appStatusViewModel)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
BindingContext = _viewModel = viewModel;
|
BindingContext = _viewModel = viewModel;
|
||||||
|
AppStatus.BindingContext = appStatusViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnAppearing()
|
protected override async void OnAppearing()
|
||||||
|
|||||||
@@ -2,59 +2,114 @@
|
|||||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||||
|
xmlns:controls="clr-namespace:FieldLogger.Views.Controls"
|
||||||
x:Class="FieldLogger.Views.JobsPage"
|
x:Class="FieldLogger.Views.JobsPage"
|
||||||
x:DataType="vm:JobsViewModel"
|
x:DataType="vm:JobsViewModel"
|
||||||
Title="Jobs">
|
Title="Jobs"
|
||||||
<ContentPage.ToolbarItems>
|
Shell.NavBarIsVisible="False"
|
||||||
<ToolbarItem Text="New" Command="{Binding NewJobCommand}" />
|
BackgroundColor="{DynamicResource UlColorCanvas}">
|
||||||
</ContentPage.ToolbarItems>
|
<Grid RowDefinitions="Auto,*"
|
||||||
|
HorizontalOptions="Fill" VerticalOptions="Fill">
|
||||||
|
<controls:AppStatusBar x:Name="AppStatus" />
|
||||||
|
|
||||||
<Grid Padding="16">
|
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,*" Padding="16" RowSpacing="16"
|
||||||
<CollectionView ItemsSource="{Binding Jobs}">
|
HorizontalOptions="Fill" VerticalOptions="Fill">
|
||||||
<CollectionView.EmptyView>
|
<VerticalStackLayout Spacing="4">
|
||||||
<VerticalStackLayout Spacing="8" VerticalOptions="Center">
|
<Label Text="FIELD WORK" FontSize="14" FontAttributes="Bold" CharacterSpacing="1.5"
|
||||||
<Label Text="No jobs yet." TextColor="Gray" HorizontalOptions="Center" />
|
TextColor="{DynamicResource UlColorPrimary}" />
|
||||||
<Button Text="Create First Job" Command="{Binding NewJobCommand}" HorizontalOptions="Center" />
|
<Label Text="Jobs" Style="{StaticResource UlPageTitle}"
|
||||||
|
TextColor="{DynamicResource UlColorText}"
|
||||||
|
SemanticProperties.HeadingLevel="Level1" />
|
||||||
|
<Label Text="Choose the active job before capturing points."
|
||||||
|
FontSize="16" TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
|
|
||||||
|
<Button Grid.Row="1" Style="{StaticResource UlButtonPrimaryField}"
|
||||||
|
Text="+ Create new job" Command="{Binding NewJobCommand}"
|
||||||
|
MinimumHeightRequest="{StaticResource UlTouchComfortable}" FontSize="16" FontAttributes="Bold"
|
||||||
|
SemanticProperties.Description="Create a new job and make it active" />
|
||||||
|
|
||||||
|
<RefreshView Grid.Row="2"
|
||||||
|
Command="{Binding RefreshCommand}"
|
||||||
|
IsRefreshing="{Binding IsRefreshing, Mode=OneWay}"
|
||||||
|
HorizontalOptions="Fill" VerticalOptions="Fill">
|
||||||
|
<CollectionView ItemsSource="{Binding Jobs}"
|
||||||
|
SelectionMode="None"
|
||||||
|
ItemSizingStrategy="MeasureFirstItem"
|
||||||
|
ItemsUpdatingScrollMode="KeepItemsInView"
|
||||||
|
HorizontalOptions="Fill" VerticalOptions="Fill"
|
||||||
|
SemanticProperties.Description="Jobs available on this device">
|
||||||
|
<CollectionView.EmptyView>
|
||||||
|
<Border Style="{StaticResource UlCard}" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
|
Stroke="{DynamicResource UlColorBorder}"
|
||||||
|
StrokeShape="RoundRectangle 12" Padding="24">
|
||||||
|
<VerticalStackLayout Spacing="8" VerticalOptions="Center">
|
||||||
|
<Label Text="No jobs yet" Style="{StaticResource UlEmptyStateTitle}"
|
||||||
|
HorizontalTextAlignment="Center" TextColor="{DynamicResource UlColorText}" />
|
||||||
|
<Label Text="Create a job to establish the capture context."
|
||||||
|
Style="{StaticResource UlEmptyStateBody}"
|
||||||
|
FontSize="16" HorizontalTextAlignment="Center"
|
||||||
|
TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
|
<Button Style="{StaticResource UlButtonPrimary}"
|
||||||
|
Text="Create first job" Command="{Binding NewJobCommand}"
|
||||||
|
MinimumHeightRequest="{StaticResource UlTouchComfortable}" HorizontalOptions="Fill"
|
||||||
|
SemanticProperties.Description="Create the first job" />
|
||||||
|
</VerticalStackLayout>
|
||||||
|
</Border>
|
||||||
</CollectionView.EmptyView>
|
</CollectionView.EmptyView>
|
||||||
<CollectionView.ItemTemplate>
|
<CollectionView.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:JobListItem">
|
<DataTemplate x:DataType="vm:JobListItem">
|
||||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}"
|
<Border Style="{StaticResource UlJobRow}" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
StrokeShape="RoundRectangle 10" Padding="12" Margin="0,4">
|
Stroke="{DynamicResource UlColorBorder}"
|
||||||
|
StrokeThickness="1" StrokeShape="RoundRectangle 12"
|
||||||
|
Padding="16" Margin="0,0,0,12" MinimumHeightRequest="136"
|
||||||
|
SemanticProperties.Description="{Binding AccessibilityDescription}"
|
||||||
|
SemanticProperties.Hint="Double tap to open job details">
|
||||||
<Border.GestureRecognizers>
|
<Border.GestureRecognizers>
|
||||||
<TapGestureRecognizer
|
<TapGestureRecognizer
|
||||||
Command="{Binding OpenCommand, Source={RelativeSource AncestorType={x:Type vm:JobsViewModel}}}"
|
Command="{Binding OpenCommand, Source={RelativeSource AncestorType={x:Type vm:JobsViewModel}}}"
|
||||||
CommandParameter="{Binding .}" />
|
CommandParameter="{Binding .}" />
|
||||||
</Border.GestureRecognizers>
|
</Border.GestureRecognizers>
|
||||||
<Grid ColumnDefinitions="*,Auto,Auto" ColumnSpacing="8">
|
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="*,Auto" RowSpacing="8" ColumnSpacing="12">
|
||||||
<VerticalStackLayout Spacing="2">
|
<Label Text="{Binding Name}" FontSize="20" FontAttributes="Bold"
|
||||||
<HorizontalStackLayout Spacing="8">
|
TextColor="{DynamicResource UlColorText}" LineBreakMode="TailTruncation" />
|
||||||
<Label Text="{Binding Name}" FontAttributes="Bold" />
|
<Border Grid.Column="1" Style="{StaticResource UlStatusBadge}"
|
||||||
<Border IsVisible="{Binding IsActive}" Background="DodgerBlue"
|
BackgroundColor="{DynamicResource UlColorCanvas}"
|
||||||
StrokeThickness="0" StrokeShape="RoundRectangle 6" Padding="6,1">
|
Stroke="{DynamicResource UlColorBorder}"
|
||||||
<Label Text="ACTIVE" FontSize="10" TextColor="White" />
|
StrokeShape="RoundRectangle 8" Padding="8,4">
|
||||||
|
<Label Text="{Binding ActiveStatusLabel}" FontSize="14" FontAttributes="Bold"
|
||||||
|
TextColor="{DynamicResource UlColorText}" />
|
||||||
</Border>
|
</Border>
|
||||||
</HorizontalStackLayout>
|
<Label Grid.Row="1" Grid.ColumnSpan="2" Text="{Binding JobSummary}"
|
||||||
<Label FontSize="12" TextColor="Gray">
|
FontSize="16" TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
<Label.Text>
|
<Grid Grid.Row="2" Grid.ColumnSpan="2" ColumnDefinitions="*,*" ColumnSpacing="8">
|
||||||
<MultiBinding StringFormat="{}{0} · {1} points">
|
<Button Style="{StaticResource UlButtonSecondary}"
|
||||||
<Binding Path="CreatedLabel" />
|
Text="{Binding ActiveActionLabel}"
|
||||||
<Binding Path="PointCount" />
|
IsEnabled="{Binding IsActive, Converter={StaticResource InvertBool}}"
|
||||||
</MultiBinding>
|
|
||||||
</Label.Text>
|
|
||||||
</Label>
|
|
||||||
</VerticalStackLayout>
|
|
||||||
<Button Grid.Column="1" Text="Set Active" FontSize="12" Padding="10,4" VerticalOptions="Center"
|
|
||||||
Command="{Binding SetActiveCommand, Source={RelativeSource AncestorType={x:Type vm:JobsViewModel}}}"
|
Command="{Binding SetActiveCommand, Source={RelativeSource AncestorType={x:Type vm:JobsViewModel}}}"
|
||||||
CommandParameter="{Binding .}" />
|
CommandParameter="{Binding .}" MinimumHeightRequest="{StaticResource UlTouchMinimum}"
|
||||||
<Button Grid.Column="2" Text="✕" FontSize="12" Padding="10,4" VerticalOptions="Center"
|
SemanticProperties.Description="{Binding ActiveActionDescription}" />
|
||||||
BackgroundColor="IndianRed" TextColor="White"
|
<Button Grid.Column="1" Style="{StaticResource UlButtonAccent}"
|
||||||
|
Text="Delete job"
|
||||||
|
BackgroundColor="{DynamicResource UlColorAccent}"
|
||||||
|
TextColor="{DynamicResource UlColorOnAccent}"
|
||||||
Command="{Binding DeleteCommand, Source={RelativeSource AncestorType={x:Type vm:JobsViewModel}}}"
|
Command="{Binding DeleteCommand, Source={RelativeSource AncestorType={x:Type vm:JobsViewModel}}}"
|
||||||
CommandParameter="{Binding .}" />
|
CommandParameter="{Binding .}" MinimumHeightRequest="{StaticResource UlTouchMinimum}"
|
||||||
|
SemanticProperties.Description="{Binding DeleteDescription}" />
|
||||||
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</CollectionView.ItemTemplate>
|
</CollectionView.ItemTemplate>
|
||||||
|
<CollectionView.Footer>
|
||||||
|
<!-- Keeps the final row scrollable above the overlaid menu without
|
||||||
|
reserving a permanent bottom band in the page layout. -->
|
||||||
|
<ContentView HeightRequest="72" />
|
||||||
|
</CollectionView.Footer>
|
||||||
</CollectionView>
|
</CollectionView>
|
||||||
|
</RefreshView>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<controls:FloatingMenuButton Grid.RowSpan="2" HorizontalOptions="End" VerticalOptions="End" ZIndex="20" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -6,15 +6,16 @@ public partial class JobsPage : ContentPage
|
|||||||
{
|
{
|
||||||
private readonly JobsViewModel _viewModel;
|
private readonly JobsViewModel _viewModel;
|
||||||
|
|
||||||
public JobsPage(JobsViewModel viewModel)
|
public JobsPage(JobsViewModel viewModel, AppStatusViewModel appStatusViewModel)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
BindingContext = _viewModel = viewModel;
|
BindingContext = _viewModel = viewModel;
|
||||||
|
AppStatus.BindingContext = appStatusViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnAppearing()
|
protected override async void OnAppearing()
|
||||||
{
|
{
|
||||||
base.OnAppearing();
|
base.OnAppearing();
|
||||||
await _viewModel.RefreshAsync();
|
await _viewModel.LoadAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,31 @@
|
|||||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||||
|
xmlns:controls="clr-namespace:FieldLogger.Views.Controls"
|
||||||
x:Class="FieldLogger.Views.MapPage"
|
x:Class="FieldLogger.Views.MapPage"
|
||||||
x:DataType="vm:MapViewModel"
|
x:DataType="vm:MapViewModel"
|
||||||
Title="Map">
|
Title="Map"
|
||||||
<Grid RowDefinitions="Auto,*">
|
Shell.NavBarIsVisible="False"
|
||||||
<Label Grid.Row="0" Text="{Binding StatusText}" FontSize="12" TextColor="Gray" Padding="12,6" />
|
BackgroundColor="{DynamicResource UlColorMap}">
|
||||||
|
<Grid>
|
||||||
<!-- Populated in code-behind: native Map control on iOS/Android/macOS, Google Maps WebView on Windows. -->
|
<!-- Populated in code-behind: native Map control on iOS/Android/macOS, Google Maps WebView on Windows. -->
|
||||||
<ContentView Grid.Row="1" x:Name="MapHost" />
|
<ContentView x:Name="MapHost"
|
||||||
|
BackgroundColor="{DynamicResource UlColorMap}"
|
||||||
|
SemanticProperties.Description="Map of locally captured points for the selected job" />
|
||||||
|
|
||||||
|
<controls:AppStatusBar x:Name="AppStatus" VerticalOptions="Start" ZIndex="20" />
|
||||||
|
|
||||||
|
<Button Text="↻" FontSize="22" FontAttributes="Bold"
|
||||||
|
WidthRequest="48" HeightRequest="48" CornerRadius="24" Padding="0"
|
||||||
|
Margin="16,82,16,16" HorizontalOptions="End" VerticalOptions="Start"
|
||||||
|
BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
|
TextColor="{DynamicResource UlColorPrimary}"
|
||||||
|
BorderColor="{DynamicResource UlColorBorder}" BorderWidth="1"
|
||||||
|
Clicked="OnRefreshClicked" ZIndex="20"
|
||||||
|
MinimumHeightRequest="{StaticResource UlTouchMinimum}"
|
||||||
|
MinimumWidthRequest="{StaticResource UlTouchMinimum}"
|
||||||
|
SemanticProperties.Description="Refresh captured points on the map" />
|
||||||
|
|
||||||
|
<controls:FloatingMenuButton HorizontalOptions="End" VerticalOptions="End" ZIndex="20" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -23,15 +23,26 @@ public partial class MapPage : ContentPage
|
|||||||
private Map? _map;
|
private Map? _map;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
public MapPage(MapViewModel viewModel)
|
public MapPage(MapViewModel viewModel, AppStatusViewModel appStatusViewModel)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
BindingContext = _viewModel = viewModel;
|
BindingContext = _viewModel = viewModel;
|
||||||
|
AppStatus.BindingContext = appStatusViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnAppearing()
|
protected override async void OnAppearing()
|
||||||
{
|
{
|
||||||
base.OnAppearing();
|
base.OnAppearing();
|
||||||
|
await RefreshMapAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void OnRefreshClicked(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
await RefreshMapAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RefreshMapAsync()
|
||||||
|
{
|
||||||
await EnsureMapCreatedAsync();
|
await EnsureMapCreatedAsync();
|
||||||
var points = await _viewModel.LoadPointsAsync();
|
var points = await _viewModel.LoadPointsAsync();
|
||||||
ShowPoints(points);
|
ShowPoints(points);
|
||||||
|
|||||||
@@ -2,11 +2,20 @@
|
|||||||
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
|
||||||
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
|
||||||
|
xmlns:controls="clr-namespace:FieldLogger.Views.Controls"
|
||||||
x:Class="FieldLogger.Views.SettingsPage"
|
x:Class="FieldLogger.Views.SettingsPage"
|
||||||
x:DataType="vm:SettingsViewModel"
|
x:DataType="vm:SettingsViewModel"
|
||||||
Title="Settings">
|
Title="Settings"
|
||||||
<ScrollView>
|
Shell.NavBarIsVisible="False"
|
||||||
<VerticalStackLayout Padding="16" Spacing="12">
|
BackgroundColor="{DynamicResource UlColorCanvas}">
|
||||||
|
<Grid RowDefinitions="Auto,*">
|
||||||
|
<controls:AppStatusBar x:Name="AppStatus" />
|
||||||
|
<ScrollView Grid.Row="1">
|
||||||
|
<VerticalStackLayout Padding="16,16,16,88" Spacing="12">
|
||||||
|
|
||||||
|
<Label Text="Settings" Style="{StaticResource UlPageTitle}"
|
||||||
|
TextColor="{DynamicResource UlColorText}"
|
||||||
|
SemanticProperties.HeadingLevel="Level1" />
|
||||||
|
|
||||||
<Label Text="Devices" FontAttributes="Bold" FontSize="16" />
|
<Label Text="Devices" FontAttributes="Bold" FontSize="16" />
|
||||||
|
|
||||||
@@ -40,36 +49,33 @@
|
|||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Label Text="Map" FontAttributes="Bold" FontSize="16" Margin="0,12,0,0" />
|
<VerticalStackLayout Spacing="8" Margin="0,12,0,0">
|
||||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
<Label Text="Diagnostics" FontAttributes="Bold" FontSize="16" />
|
||||||
<VerticalStackLayout Spacing="6">
|
<Border Style="{StaticResource UlCard}"
|
||||||
<Label Text="Google Maps API Key (Windows map view)" FontSize="12" TextColor="Gray" />
|
BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
|
||||||
<Entry Text="{Binding MapsApiKey}" Placeholder="AIza…" IsPassword="False" />
|
Stroke="{DynamicResource UlColorBorder}" StrokeShape="RoundRectangle 12" Padding="12">
|
||||||
<Label FontSize="11" TextColor="Gray"
|
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
|
||||||
Text="On Android the key is configured in AndroidManifest.xml; iOS and macOS use Apple Maps and need no key." />
|
<VerticalStackLayout Spacing="2" VerticalOptions="Center">
|
||||||
|
<Label Text="Debug" FontAttributes="Bold" FontSize="16"
|
||||||
|
TextColor="{DynamicResource UlColorText}" />
|
||||||
|
<Label Text="Capture readiness, device state, sync, recent points, and console tools"
|
||||||
|
FontSize="12" TextColor="{DynamicResource UlColorTextMuted}" />
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</Border>
|
<Button Grid.Column="1" Text="Open" Command="{Binding OpenDebugCommand}"
|
||||||
|
Style="{StaticResource UlButtonSecondary}"
|
||||||
<Label Text="Sync" FontAttributes="Bold" FontSize="16" Margin="0,12,0,0" />
|
MinimumHeightRequest="{StaticResource UlTouchMinimum}"
|
||||||
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
|
SemanticProperties.Description="Open UM Trace diagnostics" />
|
||||||
<VerticalStackLayout Spacing="8">
|
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
|
||||||
<Label Text="Durable cloud sync" VerticalOptions="Center" />
|
|
||||||
<Switch Grid.Column="1" IsToggled="{Binding MqttEnabled}" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Entry Text="{Binding MqttHost}" Placeholder="dev.hub.umagul.net" />
|
|
||||||
<Entry Text="{Binding MqttPort}" Placeholder="443 (WSS) or 8884 (MQTTS)" Keyboard="Numeric" />
|
|
||||||
<Entry Text="{Binding MqttOrgId}" Placeholder="Organization ID / MQTT username" />
|
|
||||||
<Entry Text="{Binding MqttPassword}" Placeholder="New MQTT password (stored securely)" IsPassword="True" />
|
|
||||||
<Label Text="{Binding MqttClientId, StringFormat='Client: {0}'}" FontSize="10" TextColor="Gray" />
|
|
||||||
<Label Text="{Binding SyncStatus}" FontSize="12" TextColor="DodgerBlue" />
|
|
||||||
<Button Text="Save & Connect" Command="{Binding SaveSyncCommand}" />
|
|
||||||
</VerticalStackLayout>
|
|
||||||
</Border>
|
</Border>
|
||||||
|
</VerticalStackLayout>
|
||||||
|
|
||||||
<Label Text="{Binding AppVersion, StringFormat='Field Logger v{0}'}"
|
<Label Text="{Binding AppVersion, StringFormat='UM Trace v{0}'}"
|
||||||
FontSize="11" TextColor="Gray" HorizontalOptions="Center" Margin="0,20,0,0" />
|
FontSize="11" TextColor="{DynamicResource UlColorTextMuted}"
|
||||||
|
HorizontalOptions="Center" Margin="0,20,0,0" />
|
||||||
|
|
||||||
</VerticalStackLayout>
|
</VerticalStackLayout>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
<controls:FloatingMenuButton Grid.RowSpan="2" HorizontalOptions="End" VerticalOptions="End" ZIndex="20" />
|
||||||
|
</Grid>
|
||||||
</ContentPage>
|
</ContentPage>
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ namespace FieldLogger.Views;
|
|||||||
|
|
||||||
public partial class SettingsPage : ContentPage
|
public partial class SettingsPage : ContentPage
|
||||||
{
|
{
|
||||||
public SettingsPage(SettingsViewModel viewModel)
|
public SettingsPage(SettingsViewModel viewModel, AppStatusViewModel appStatusViewModel)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
BindingContext = viewModel;
|
BindingContext = viewModel;
|
||||||
|
AppStatus.BindingContext = appStatusViewModel;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
48
Jenkinsfile
vendored
Normal file
48
Jenkinsfile
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
pipeline {
|
||||||
|
agent { label 'um-trace' }
|
||||||
|
|
||||||
|
options {
|
||||||
|
buildDiscarder(logRotator(numToKeepStr: '20'))
|
||||||
|
disableConcurrentBuilds()
|
||||||
|
timeout(time: 45, unit: 'MINUTES')
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer'
|
||||||
|
DOTNET_ROOT = '/Users/brent/.dotnet'
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT = '1'
|
||||||
|
DOTNET_NOLOGO = '1'
|
||||||
|
NUGET_XMLDOC_MODE = 'skip'
|
||||||
|
PATH = "/Users/brent/.dotnet:/opt/homebrew/bin:${env.PATH}"
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Toolchain') {
|
||||||
|
steps {
|
||||||
|
sh './scripts/ci/verify-macos-agent.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Tests') {
|
||||||
|
steps {
|
||||||
|
sh './scripts/ci/test.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('iOS Build (Unsigned)') {
|
||||||
|
steps {
|
||||||
|
sh './scripts/ci/build-ios-unsigned.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
archiveArtifacts(
|
||||||
|
artifacts: 'artifacts/**/*',
|
||||||
|
allowEmptyArchive: true,
|
||||||
|
fingerprint: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
Jenkinsfile.release
Normal file
58
Jenkinsfile.release
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
pipeline {
|
||||||
|
agent { label 'um-trace' }
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
string(
|
||||||
|
name: 'IOS_BUILD_NUMBER',
|
||||||
|
defaultValue: '',
|
||||||
|
description: 'Optional App Store build number override. Blank uses the Jenkins build number.'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
options {
|
||||||
|
buildDiscarder(logRotator(numToKeepStr: '10'))
|
||||||
|
disableConcurrentBuilds()
|
||||||
|
timeout(time: 60, unit: 'MINUTES')
|
||||||
|
}
|
||||||
|
|
||||||
|
environment {
|
||||||
|
DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer'
|
||||||
|
DOTNET_ROOT = '/Users/brent/.dotnet'
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT = '1'
|
||||||
|
DOTNET_NOLOGO = '1'
|
||||||
|
NUGET_XMLDOC_MODE = 'skip'
|
||||||
|
PATH = "/Users/brent/.dotnet:/opt/homebrew/bin:${env.PATH}"
|
||||||
|
UM_TRACE_CODESIGN_KEY = 'Apple Distribution'
|
||||||
|
UM_TRACE_CODESIGN_PROVISION = 'UM Trace App Store'
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Toolchain and Signing') {
|
||||||
|
steps {
|
||||||
|
sh './scripts/ci/verify-macos-agent.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Tests') {
|
||||||
|
steps {
|
||||||
|
sh './scripts/ci/test.sh'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Signed iOS IPA') {
|
||||||
|
steps {
|
||||||
|
sh './scripts/ci/build-ios-signed.sh "${IOS_BUILD_NUMBER:-$BUILD_NUMBER}"'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
post {
|
||||||
|
always {
|
||||||
|
archiveArtifacts(
|
||||||
|
artifacts: 'artifacts/ios-signed/**/*',
|
||||||
|
allowEmptyArchive: true,
|
||||||
|
fingerprint: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Field Logger
|
# UM Trace
|
||||||
|
|
||||||
Cross-platform .NET MAUI app (Windows, macOS, iOS, Android) for logging utility locate
|
Cross-platform .NET MAUI app (Windows, macOS, iOS, Android) for logging utility locate
|
||||||
points from an **Underground Magnetics locating receiver** paired with RTK GPS positions
|
points from an **Underground Magnetics locating receiver** paired with RTK GPS positions
|
||||||
|
|||||||
88
doc/iOS_TestFlight.md
Normal file
88
doc/iOS_TestFlight.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# UM Trace TestFlight build
|
||||||
|
|
||||||
|
## Fixed application identity
|
||||||
|
|
||||||
|
- Display name: `UM Trace`
|
||||||
|
- Bundle ID: `com.umagul.trace`
|
||||||
|
- Apple team ID: `W2N8APPQ2C`
|
||||||
|
- App Store Connect SKU: `UM-TRACE-IOS`
|
||||||
|
- Distribution profile: `UM Trace App Store`
|
||||||
|
|
||||||
|
The provisioning profile and all Apple signing files are local-only and covered by
|
||||||
|
`.gitignore`. Never commit a certificate, private key, App Store Connect API key, `.p12`, or
|
||||||
|
provisioning profile.
|
||||||
|
|
||||||
|
## Required local toolchain
|
||||||
|
|
||||||
|
- Xcode 26 at `/Applications/Xcode.app`
|
||||||
|
- .NET SDK 10.0.100 as the build host
|
||||||
|
- Workload set 10.0.100 with `maui-ios` and its .NET 9/iOS 26 compatibility pack
|
||||||
|
- An `Apple Distribution` identity, including its private key, in the login Keychain
|
||||||
|
- The `UM Trace App Store` provisioning profile installed locally
|
||||||
|
|
||||||
|
This repository's local SDK is installed at `~/.dotnet`. Verify it with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
~/.dotnet/dotnet --info
|
||||||
|
~/.dotnet/dotnet workload list
|
||||||
|
```
|
||||||
|
|
||||||
|
## Create an archive
|
||||||
|
|
||||||
|
Run the release helper from a normal macOS Terminal session. The build number must increase for
|
||||||
|
every upload to App Store Connect:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /Users/brent/ul-platform/app
|
||||||
|
./scripts/publish-testflight.sh 1
|
||||||
|
```
|
||||||
|
|
||||||
|
The app remains on its existing .NET 9 target. The .NET 10 SDK host supplies the supported .NET 9
|
||||||
|
compatibility pack for Xcode 26, avoiding an unrelated Android/Mac Catalyst framework migration.
|
||||||
|
The script deliberately supplies Xcode through `DEVELOPER_DIR`; it does not change the machine's
|
||||||
|
global `xcode-select` setting. It restores only the iOS target, builds with the iOS 26 SDK, selects
|
||||||
|
the installed `UM Trace App Store` profile, and prints the generated `.ipa` path.
|
||||||
|
|
||||||
|
## Create a signed IPA in Jenkins
|
||||||
|
|
||||||
|
The manually run `um-trace-ios-release` pipeline uses `Jenkinsfile.release`. By default it uses the
|
||||||
|
monotonically increasing Jenkins build number as the App Store build number. Set
|
||||||
|
`IOS_BUILD_NUMBER` only when an explicit higher override is required. The pipeline runs the tests,
|
||||||
|
signs with the distribution identity and `UM Trace App Store` profile, verifies the resulting
|
||||||
|
bundle signature and identity, and archives the IPA plus its SHA-256 file.
|
||||||
|
|
||||||
|
This pipeline only creates a signed artifact. It does not upload or submit anything to App Store
|
||||||
|
Connect, so TestFlight release remains a separate, deliberate step.
|
||||||
|
|
||||||
|
Before uploading, verify that this command lists a valid distribution identity:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
security find-identity -v -p codesigning
|
||||||
|
```
|
||||||
|
|
||||||
|
If the identity is absent, import a `.p12` containing the matching private key or create a new
|
||||||
|
Apple Distribution certificate through Xcode and regenerate the provisioning profile against it.
|
||||||
|
|
||||||
|
## Deploy a development build to an iPhone
|
||||||
|
|
||||||
|
The local development profile is named `UM Trace Development`. It must include the target phone
|
||||||
|
and match an `Apple Development` identity for team `W2N8APPQ2C`. Keep the phone unlocked and
|
||||||
|
connected by USB for the first deployment, trust the Mac when prompted, and enable Developer Mode.
|
||||||
|
|
||||||
|
Save the phone UDID in the gitignored `.ios-device` file, then run from a normal macOS Terminal:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /Users/brent/ul-platform/app
|
||||||
|
./scripts/deploy-ios-device.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
You can instead pass a UDID as the first argument or set `UM_TRACE_DEVICE_UDID`. The helper uses
|
||||||
|
Xcode 26, builds the `Debug` configuration for `ios-arm64`, signs with the development profile,
|
||||||
|
installs the app, and launches it on the selected phone.
|
||||||
|
|
||||||
|
## Background and privacy declarations
|
||||||
|
|
||||||
|
The iOS bundle declares `bluetooth-central` and `location` background modes, Bluetooth and
|
||||||
|
always/when-in-use location explanations, the MAUI `UserDefaults` required-reason API, and exempt
|
||||||
|
standard TLS use. Physical-device testing remains required to prove the SRS eight-hour background
|
||||||
|
BLE/location session; declaring a background mode does not itself guarantee continuous execution.
|
||||||
75
scripts/ci/build-ios-signed.sh
Executable file
75
scripts/ci/build-ios-signed.sh
Executable file
@@ -0,0 +1,75 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
build_number="${1:-}"
|
||||||
|
codesign_key="${UM_TRACE_CODESIGN_KEY:-Apple Distribution}"
|
||||||
|
codesign_profile="${UM_TRACE_CODESIGN_PROVISION:-UM Trace App Store}"
|
||||||
|
profile_dir="${HOME}/Library/MobileDevice/Provisioning Profiles"
|
||||||
|
artifacts_dir="${repo_dir}/artifacts/ios-signed"
|
||||||
|
artifact_name="UMTrace-ios-build-${build_number}.ipa"
|
||||||
|
|
||||||
|
if [[ ! "${build_number}" =~ ^[1-9][0-9]*$ ]]; then
|
||||||
|
echo "A positive numeric iOS build number is required." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! security find-identity -v -p codesigning | grep -Fq "${codesign_key}"; then
|
||||||
|
echo "Signing identity is unavailable: ${codesign_key}" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
profile_found=false
|
||||||
|
if [[ -d "${profile_dir}" ]]; then
|
||||||
|
for profile in "${profile_dir}"/*.mobileprovision; do
|
||||||
|
[[ -f "${profile}" ]] || continue
|
||||||
|
installed_name="$(openssl smime -inform der -verify -noverify -in "${profile}" 2>/dev/null | plutil -extract Name raw -o - - 2>/dev/null || true)"
|
||||||
|
if [[ "${installed_name}" == "${codesign_profile}" ]]; then
|
||||||
|
profile_found=true
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "${profile_found}" != true ]]; then
|
||||||
|
echo "Provisioning profile is unavailable: ${codesign_profile}" >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
UM_TRACE_CODESIGN_KEY="${codesign_key}" \
|
||||||
|
UM_TRACE_CODESIGN_PROVISION="${codesign_profile}" \
|
||||||
|
"${repo_dir}/scripts/publish-testflight.sh" "${build_number}"
|
||||||
|
|
||||||
|
source_ipa="$(find "${repo_dir}/FieldLogger/bin/Release/net9.0-ios/ios-arm64" -type f -name '*.ipa' -print -quit)"
|
||||||
|
if [[ -z "${source_ipa}" || ! -f "${source_ipa}" ]]; then
|
||||||
|
echo "The signed IPA was not created." >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${artifacts_dir}"
|
||||||
|
ditto "${source_ipa}" "${artifacts_dir}/${artifact_name}"
|
||||||
|
|
||||||
|
verification_dir="$(mktemp -d)"
|
||||||
|
ditto -x -k "${artifacts_dir}/${artifact_name}" "${verification_dir}"
|
||||||
|
app_bundle="$(find "${verification_dir}/Payload" -maxdepth 1 -type d -name '*.app' -print -quit)"
|
||||||
|
|
||||||
|
if [[ -z "${app_bundle}" ]]; then
|
||||||
|
echo "The IPA does not contain an app bundle." >&2
|
||||||
|
exit 4
|
||||||
|
fi
|
||||||
|
|
||||||
|
codesign --verify --deep --strict --verbose=2 "${app_bundle}"
|
||||||
|
|
||||||
|
bundle_id="$(plutil -extract CFBundleIdentifier raw -o - "${app_bundle}/Info.plist")"
|
||||||
|
signed_build_number="$(plutil -extract CFBundleVersion raw -o - "${app_bundle}/Info.plist")"
|
||||||
|
if [[ "${bundle_id}" != "com.umagul.trace" || "${signed_build_number}" != "${build_number}" ]]; then
|
||||||
|
echo "Signed bundle metadata is incorrect: ${bundle_id} (${signed_build_number})." >&2
|
||||||
|
exit 5
|
||||||
|
fi
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "${artifacts_dir}"
|
||||||
|
shasum -a 256 "${artifact_name}" > "${artifact_name}.sha256"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Verified signed iOS IPA: ${artifacts_dir}/${artifact_name}"
|
||||||
60
scripts/ci/build-ios-unsigned.sh
Executable file
60
scripts/ci/build-ios-unsigned.sh
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
dotnet_bin="${DOTNET_BIN:-/Users/brent/.dotnet/dotnet}"
|
||||||
|
developer_dir="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"
|
||||||
|
project="${repo_dir}/FieldLogger/FieldLogger.csproj"
|
||||||
|
runtime_identifier="${IOS_RUNTIME:-ios-arm64}"
|
||||||
|
nuget_cache="${UM_TRACE_NUGET_CACHE:-${TMPDIR:-/tmp}/um-trace-ci-nuget}"
|
||||||
|
app_bundle="${repo_dir}/FieldLogger/bin/Debug/net9.0-ios/${runtime_identifier}/FieldLogger.app"
|
||||||
|
artifacts_dir="${repo_dir}/artifacts/ios"
|
||||||
|
artifact_name="UMTrace-unsigned-${runtime_identifier}.app.zip"
|
||||||
|
|
||||||
|
export DEVELOPER_DIR="${developer_dir}"
|
||||||
|
export NUGET_HTTP_CACHE_PATH="${NUGET_HTTP_CACHE_PATH:-${nuget_cache}/http-cache}"
|
||||||
|
|
||||||
|
mkdir -p "${NUGET_HTTP_CACHE_PATH}"
|
||||||
|
|
||||||
|
"${dotnet_bin}" restore "${project}" \
|
||||||
|
-p:TargetFrameworks=net9.0-ios \
|
||||||
|
-p:RuntimeIdentifier="${runtime_identifier}" \
|
||||||
|
-p:NuGetAudit=false
|
||||||
|
|
||||||
|
# A narrowed restore of the MAUI head can rewrite the referenced library's
|
||||||
|
# assets file, so restore the headless sync library explicitly as well.
|
||||||
|
"${dotnet_bin}" restore "${repo_dir}/src/FieldLogger.Sync/FieldLogger.Sync.csproj" \
|
||||||
|
-p:NuGetAudit=false
|
||||||
|
|
||||||
|
"${dotnet_bin}" clean "${project}" \
|
||||||
|
--framework net9.0-ios \
|
||||||
|
--configuration Debug \
|
||||||
|
-p:TargetFrameworks=net9.0-ios \
|
||||||
|
-p:RuntimeIdentifier="${runtime_identifier}"
|
||||||
|
|
||||||
|
"${dotnet_bin}" build "${project}" \
|
||||||
|
--framework net9.0-ios \
|
||||||
|
--configuration Debug \
|
||||||
|
--runtime "${runtime_identifier}" \
|
||||||
|
-p:TargetFrameworks=net9.0-ios \
|
||||||
|
-p:EnableCodeSigning=false \
|
||||||
|
-p:CodesignKey= \
|
||||||
|
-p:CodesignProvision= \
|
||||||
|
--no-restore
|
||||||
|
|
||||||
|
if [[ ! -d "${app_bundle}" ]]; then
|
||||||
|
echo "Unsigned app bundle was not created at ${app_bundle}." >&2
|
||||||
|
exit 3
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p "${artifacts_dir}"
|
||||||
|
ditto -c -k --sequesterRsrc --keepParent \
|
||||||
|
"${app_bundle}" \
|
||||||
|
"${artifacts_dir}/${artifact_name}"
|
||||||
|
|
||||||
|
(
|
||||||
|
cd "${artifacts_dir}"
|
||||||
|
shasum -a 256 "${artifact_name}" > "${artifact_name}.sha256"
|
||||||
|
)
|
||||||
|
|
||||||
|
echo "Archived unsigned iOS app: ${artifacts_dir}/${artifact_name}"
|
||||||
28
scripts/ci/test.sh
Executable file
28
scripts/ci/test.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||||
|
dotnet_bin="${DOTNET_TEST_BIN:-/usr/local/share/dotnet/dotnet}"
|
||||||
|
results_dir="${repo_dir}/artifacts/test-results"
|
||||||
|
nuget_cache="${UM_TRACE_NUGET_CACHE:-${TMPDIR:-/tmp}/um-trace-ci-nuget}"
|
||||||
|
|
||||||
|
mkdir -p "${results_dir}" "${nuget_cache}/http-cache"
|
||||||
|
|
||||||
|
export NUGET_HTTP_CACHE_PATH="${NUGET_HTTP_CACHE_PATH:-${nuget_cache}/http-cache}"
|
||||||
|
|
||||||
|
test_projects=(
|
||||||
|
"tests/FieldLogger.Sync.Tests/FieldLogger.Sync.Tests.csproj"
|
||||||
|
"tests/FieldLogger.Tests/FieldLogger.Tests.csproj"
|
||||||
|
"tests/IfLoc.Sim.Tests/IfLoc.Sim.Tests.csproj"
|
||||||
|
)
|
||||||
|
|
||||||
|
for project in "${test_projects[@]}"; do
|
||||||
|
project_name="$(basename "${project}" .csproj)"
|
||||||
|
DOTNET_ROOT=/usr/local/share/dotnet "${dotnet_bin}" restore "${repo_dir}/${project}" \
|
||||||
|
-p:NuGetAudit=false
|
||||||
|
DOTNET_ROOT=/usr/local/share/dotnet "${dotnet_bin}" test "${repo_dir}/${project}" \
|
||||||
|
--configuration Release \
|
||||||
|
--no-restore \
|
||||||
|
--logger "trx;LogFileName=${project_name}.trx" \
|
||||||
|
--results-directory "${results_dir}"
|
||||||
|
done
|
||||||
33
scripts/ci/verify-macos-agent.sh
Executable file
33
scripts/ci/verify-macos-agent.sh
Executable file
@@ -0,0 +1,33 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
dotnet_bin="${DOTNET_BIN:-/Users/brent/.dotnet/dotnet}"
|
||||||
|
test_dotnet_bin="${DOTNET_TEST_BIN:-/usr/local/share/dotnet/dotnet}"
|
||||||
|
developer_dir="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"
|
||||||
|
|
||||||
|
if [[ ! -x "${dotnet_bin}" ]]; then
|
||||||
|
echo "Missing Xcode 26 build SDK at ${dotnet_bin}." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "${test_dotnet_bin}" ]]; then
|
||||||
|
echo "Missing .NET 9 test SDK at ${test_dotnet_bin}." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "${developer_dir}" ]]; then
|
||||||
|
echo "Missing Xcode developer directory at ${developer_dir}." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DEVELOPER_DIR="${developer_dir}"
|
||||||
|
|
||||||
|
java -version
|
||||||
|
xcodebuild -version
|
||||||
|
"${dotnet_bin}" --info
|
||||||
|
"${dotnet_bin}" workload list
|
||||||
|
DOTNET_ROOT=/usr/local/share/dotnet "${test_dotnet_bin}" --info
|
||||||
|
|
||||||
|
# Signing is not required for the unsigned compile build, but report what will be
|
||||||
|
# available to later approval-gated device and TestFlight stages.
|
||||||
|
security find-identity -v -p codesigning || true
|
||||||
60
scripts/deploy-ios-device.sh
Executable file
60
scripts/deploy-ios-device.sh
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
dotnet_bin="${DOTNET_BIN:-${HOME}/.dotnet/dotnet}"
|
||||||
|
developer_dir="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"
|
||||||
|
codesign_key="${UM_TRACE_DEVELOPMENT_KEY:-Apple Development: Brent Perteet (WZ56HVC6FD)}"
|
||||||
|
codesign_profile="${UM_TRACE_DEVELOPMENT_PROVISION:-UM Trace Development}"
|
||||||
|
device_udid="${1:-${UM_TRACE_DEVICE_UDID:-}}"
|
||||||
|
|
||||||
|
if [[ -z "${device_udid}" && -f "${repo_dir}/.ios-device" ]]; then
|
||||||
|
IFS= read -r device_udid < "${repo_dir}/.ios-device"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "${device_udid}" ]]; then
|
||||||
|
echo "Usage: $0 <device-udid>" >&2
|
||||||
|
echo "Alternatively, set UM_TRACE_DEVICE_UDID or save the UDID in ${repo_dir}/.ios-device." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "${dotnet_bin}" ]]; then
|
||||||
|
echo "Missing .NET SDK at ${dotnet_bin}. Set DOTNET_BIN or install the local SDK." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "${developer_dir}" ]]; then
|
||||||
|
echo "Missing Xcode developer directory at ${developer_dir}. Set DEVELOPER_DIR." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DEVELOPER_DIR="${developer_dir}"
|
||||||
|
|
||||||
|
"${dotnet_bin}" restore "${repo_dir}/FieldLogger/FieldLogger.csproj" \
|
||||||
|
-p:TargetFrameworks=net9.0-ios \
|
||||||
|
-p:RuntimeIdentifier=ios-arm64
|
||||||
|
|
||||||
|
# Restoring the MAUI head with a narrowed TargetFrameworks property can rewrite the
|
||||||
|
# referenced headless library's assets file. Restore that library explicitly before building.
|
||||||
|
"${dotnet_bin}" restore "${repo_dir}/src/FieldLogger.Sync/FieldLogger.Sync.csproj"
|
||||||
|
|
||||||
|
# A partial MSBuild target (for example, a compile-only IDE validation) can leave a
|
||||||
|
# FieldLogger.dll whose generated XAML code is newer than its embedded MauiXaml resources.
|
||||||
|
# Always clear the narrowed iOS output before the real device build so App.xaml, page XAML,
|
||||||
|
# app icons, and splash assets are regenerated as one consistent build graph.
|
||||||
|
"${dotnet_bin}" clean "${repo_dir}/FieldLogger/FieldLogger.csproj" \
|
||||||
|
-f net9.0-ios \
|
||||||
|
-c Debug \
|
||||||
|
-p:TargetFrameworks=net9.0-ios \
|
||||||
|
-p:RuntimeIdentifier=ios-arm64
|
||||||
|
|
||||||
|
"${dotnet_bin}" build "${repo_dir}/FieldLogger/FieldLogger.csproj" \
|
||||||
|
-t:Run \
|
||||||
|
-p:TargetFrameworks=net9.0-ios \
|
||||||
|
-f net9.0-ios \
|
||||||
|
-c Debug \
|
||||||
|
-p:RuntimeIdentifier=ios-arm64 \
|
||||||
|
-p:_DeviceName="${device_udid}" \
|
||||||
|
-p:CodesignKey="${codesign_key}" \
|
||||||
|
-p:CodesignProvision="${codesign_profile}" \
|
||||||
|
--no-restore
|
||||||
55
scripts/publish-testflight.sh
Executable file
55
scripts/publish-testflight.sh
Executable file
@@ -0,0 +1,55 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
dotnet_bin="${DOTNET_BIN:-${HOME}/.dotnet/dotnet}"
|
||||||
|
developer_dir="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"
|
||||||
|
build_number="${1:-}"
|
||||||
|
codesign_key="${UM_TRACE_CODESIGN_KEY:-Apple Distribution}"
|
||||||
|
codesign_profile="${UM_TRACE_CODESIGN_PROVISION:-UM Trace App Store}"
|
||||||
|
|
||||||
|
if [[ ! "${build_number}" =~ ^[0-9]+$ ]]; then
|
||||||
|
echo "Usage: $0 <numeric-build-number>" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x "${dotnet_bin}" ]]; then
|
||||||
|
echo "Missing .NET 10 SDK at ${dotnet_bin}. Set DOTNET_BIN or install .NET 10." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "${developer_dir}" ]]; then
|
||||||
|
echo "Missing Xcode developer directory at ${developer_dir}. Set DEVELOPER_DIR." >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
export DEVELOPER_DIR="${developer_dir}"
|
||||||
|
|
||||||
|
"${dotnet_bin}" restore "${repo_dir}/FieldLogger/FieldLogger.csproj" \
|
||||||
|
-p:TargetFrameworks=net9.0-ios \
|
||||||
|
-p:RuntimeIdentifier=ios-arm64
|
||||||
|
|
||||||
|
# Restoring the MAUI head with a narrowed TargetFrameworks property can rewrite the
|
||||||
|
# referenced headless library's assets file. Restore that library explicitly before publish.
|
||||||
|
"${dotnet_bin}" restore "${repo_dir}/src/FieldLogger.Sync/FieldLogger.Sync.csproj"
|
||||||
|
|
||||||
|
"${dotnet_bin}" clean "${repo_dir}/FieldLogger/FieldLogger.csproj" \
|
||||||
|
-f net9.0-ios \
|
||||||
|
-c Release \
|
||||||
|
-p:TargetFrameworks=net9.0-ios \
|
||||||
|
-p:RuntimeIdentifier=ios-arm64
|
||||||
|
|
||||||
|
"${dotnet_bin}" publish "${repo_dir}/FieldLogger/FieldLogger.csproj" \
|
||||||
|
-p:TargetFrameworks=net9.0-ios \
|
||||||
|
-f net9.0-ios \
|
||||||
|
-c Release \
|
||||||
|
-p:RuntimeIdentifier=ios-arm64 \
|
||||||
|
-p:ArchiveOnBuild=true \
|
||||||
|
-p:BuildIpa=true \
|
||||||
|
-p:CodesignKey="${codesign_key}" \
|
||||||
|
-p:CodesignProvision="${codesign_profile}" \
|
||||||
|
-p:ApplicationVersion="${build_number}" \
|
||||||
|
--no-restore
|
||||||
|
|
||||||
|
find "${repo_dir}/FieldLogger/bin/Release/net9.0-ios/ios-arm64" \
|
||||||
|
-type f -name '*.ipa' -print
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Cloud-sync core for Field Logger: the durable outbound queue, the Point wire mapping,
|
Cloud-sync core for UM Trace: the durable outbound queue, the Point wire mapping,
|
||||||
and the MQTTS publish/ack engine (SRS-SYN-2/5/7, §3.4.2, telemetry-schema.md).
|
and the MQTTS publish/ack engine (SRS-SYN-2/5/7, §3.4.2, telemetry-schema.md).
|
||||||
Deliberately a plain net9.0 library (NO MAUI head) so the queue-durability, ack-release,
|
Deliberately a plain net9.0 library (NO MAUI head) so the queue-durability, ack-release,
|
||||||
and exactly-once behaviour run headless in the QA gate without the Android/iOS workloads.
|
and exactly-once behaviour run headless in the QA gate without the Android/iOS workloads.
|
||||||
|
|||||||
@@ -98,10 +98,12 @@ public sealed class SqliteOutboundStore : IOutboundStore
|
|||||||
{
|
{
|
||||||
await _db.CreateTableAsync<OutboundMessage>();
|
await _db.CreateTableAsync<OutboundMessage>();
|
||||||
// A process crash can leave rows in-flight after the broker accepted them but before
|
// A process crash can leave rows in-flight after the broker accepted them but before
|
||||||
// the application ack was applied. Requeue them; pointId makes the replay idempotent.
|
// the application ack was applied. Requeue them for immediate replay; pointId makes
|
||||||
|
// the replay idempotent. A zero due-time also keeps recovery independent of whichever
|
||||||
|
// clock implementation the sync engine uses.
|
||||||
await _db.ExecuteAsync(
|
await _db.ExecuteAsync(
|
||||||
"UPDATE outbound SET Status = ?, NextAttemptUnixMs = ? WHERE Status = ?",
|
"UPDATE outbound SET Status = ?, NextAttemptUnixMs = ? WHERE Status = ?",
|
||||||
(int)OutboundStatus.Pending, DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(),
|
(int)OutboundStatus.Pending, 0,
|
||||||
(int)OutboundStatus.InFlight);
|
(int)OutboundStatus.InFlight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1",
|
||||||
|
"jobId": "job_1",
|
||||||
|
"points": [
|
||||||
|
{
|
||||||
|
"pointId": "018f1a00-0000-7000-8000-000000000001",
|
||||||
|
"createdAt": "2026-08-21T10:00:00+00:00",
|
||||||
|
"origin": "APP",
|
||||||
|
"uploadPath": "APP_MQTT",
|
||||||
|
"lat": 40.1,
|
||||||
|
"lng": -80.2,
|
||||||
|
"ts": "2026-08-21T09:59:59+00:00",
|
||||||
|
"fix": "FIXED",
|
||||||
|
"hAcc": 0.02,
|
||||||
|
"vAcc": 0.04,
|
||||||
|
"sats": 18,
|
||||||
|
"utility": "WATER",
|
||||||
|
"qualityFlag": "IN_SPEC"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Include="..\..\..\meta\contracts\fixtures\app-log-*.json"
|
<None Include="ContractFixtures\app-log-*.json"
|
||||||
Link="ContractFixtures\%(Filename)%(Extension)"
|
Link="ContractFixtures\%(Filename)%(Extension)"
|
||||||
CopyToOutputDirectory="PreserveNewest" />
|
CopyToOutputDirectory="PreserveNewest" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
QA smoke/regression harness for the Field Logger app (S1-f, trace: SRS §6 gate / NFR-8).
|
QA smoke/regression harness for the UM Trace app (S1-f, trace: SRS §6 gate / NFR-8).
|
||||||
|
|
||||||
Targets plain net9.0 (NOT the MAUI platform TFMs) so `dotnet test` runs on any dev/CI
|
Targets plain net9.0 (NOT the MAUI platform TFMs) so `dotnet test` runs on any dev/CI
|
||||||
machine without the MAUI workload, keeping the QA gate fast. It deliberately does NOT
|
machine without the MAUI workload, keeping the QA gate fast. It deliberately does NOT
|
||||||
|
|||||||
206
tests/FieldLogger.Tests/Sprint3UiContractTests.cs
Normal file
206
tests/FieldLogger.Tests/Sprint3UiContractTests.cs
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Xml.Linq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace FieldLogger.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Headless guardrails for the Sprint 3 MAUI screen integration. These deliberately inspect
|
||||||
|
/// XAML source so the QA gate can verify token use and accessibility basics without a MAUI
|
||||||
|
/// simulator or device workload.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class Sprint3UiContractTests
|
||||||
|
{
|
||||||
|
private static readonly string[] MigratedScreens =
|
||||||
|
[
|
||||||
|
"FieldLogger/Views/HomePage.xaml",
|
||||||
|
"FieldLogger/Views/JobsPage.xaml",
|
||||||
|
"FieldLogger/Views/JobDetailPage.xaml",
|
||||||
|
"FieldLogger/Views/MapPage.xaml",
|
||||||
|
];
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Migrated_screens_use_only_semantic_color_resources()
|
||||||
|
{
|
||||||
|
var hardCodedColor = new Regex(@"#[0-9a-fA-F]{3,8}\b", RegexOptions.CultureInvariant);
|
||||||
|
foreach (var path in MigratedScreens)
|
||||||
|
{
|
||||||
|
var text = File.ReadAllText(FromRoot(path));
|
||||||
|
Assert.False(hardCodedColor.IsMatch(text), $"Page-level color literal found in {path}");
|
||||||
|
Assert.DoesNotContain("TextColor=\"Gray\"", text, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.DoesNotContain("DodgerBlue", text, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.DoesNotContain("IndianRed", text, StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Every_referenced_Survey_Teal_token_exists_in_generated_resources()
|
||||||
|
{
|
||||||
|
var resourceText = string.Concat(
|
||||||
|
File.ReadAllText(FromRoot("FieldLogger/Resources/Styles/GeneratedTokens.xaml")),
|
||||||
|
File.ReadAllText(FromRoot("FieldLogger/Resources/Styles/Styles.xaml")));
|
||||||
|
var defined = Regex.Matches(resourceText, "x:Key=\\\"(?<key>Ul[^\\\"]+)\\\"")
|
||||||
|
.Select(match => match.Groups["key"].Value)
|
||||||
|
.ToHashSet(StringComparer.Ordinal);
|
||||||
|
var resourceReference = new Regex(@"\{(?:Static|Dynamic)Resource (?<key>Ul[^}\s]+)\}");
|
||||||
|
|
||||||
|
foreach (var path in MigratedScreens.Append("FieldLogger/AppShell.xaml"))
|
||||||
|
{
|
||||||
|
foreach (Match match in resourceReference.Matches(File.ReadAllText(FromRoot(path))))
|
||||||
|
{
|
||||||
|
var key = match.Groups["key"].Value;
|
||||||
|
Assert.Contains(key, defined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void App_loads_generated_tokens_before_shared_component_styles()
|
||||||
|
{
|
||||||
|
var app = File.ReadAllText(FromRoot("FieldLogger/App.xaml"));
|
||||||
|
var generatedIndex = app.IndexOf("GeneratedTokens.xaml", StringComparison.Ordinal);
|
||||||
|
var stylesIndex = app.IndexOf("Styles.xaml", StringComparison.Ordinal);
|
||||||
|
Assert.True(generatedIndex >= 0, "App.xaml must merge GeneratedTokens.xaml.");
|
||||||
|
Assert.True(stylesIndex > generatedIndex, "Generated tokens must load before component styles.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Migrated_screens_consume_shared_component_styles()
|
||||||
|
{
|
||||||
|
var migrated = string.Concat(MigratedScreens.Select(path => File.ReadAllText(FromRoot(path))));
|
||||||
|
Assert.Contains("UlCard", migrated, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("UlJobRow", migrated, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("UlStatusBadge", migrated, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("UlButtonSecondary", migrated, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Every_page_button_declares_at_least_the_minimum_touch_target()
|
||||||
|
{
|
||||||
|
var accepted = new[]
|
||||||
|
{
|
||||||
|
"{StaticResource UlTouchMinimum}",
|
||||||
|
"{StaticResource UlTouchComfortable}",
|
||||||
|
"{StaticResource UlTouchPrimary}",
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var path in MigratedScreens)
|
||||||
|
{
|
||||||
|
var document = XDocument.Load(FromRoot(path));
|
||||||
|
foreach (var button in document.Descendants().Where(e => e.Name.LocalName == "Button"))
|
||||||
|
{
|
||||||
|
var target = button.Attribute("MinimumHeightRequest")?.Value;
|
||||||
|
Assert.Contains(target, accepted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Production_shell_does_not_expose_Debug_as_a_primary_tab()
|
||||||
|
{
|
||||||
|
var shell = File.ReadAllText(FromRoot("FieldLogger/AppShell.xaml"));
|
||||||
|
Assert.DoesNotContain("Title=\"Debug\"", shell, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("views:DebugPage", shell, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("<TabBar", shell, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("Title=\"Home\"", shell, StringComparison.Ordinal);
|
||||||
|
Assert.True(shell.IndexOf("Title=\"Jobs\"", StringComparison.Ordinal) <
|
||||||
|
shell.IndexOf("Title=\"Map\"", StringComparison.Ordinal),
|
||||||
|
"Jobs must be the first primary route so it is selected at startup.");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Home_explains_real_receiver_capture_without_inventing_an_app_command()
|
||||||
|
{
|
||||||
|
var home = File.ReadAllText(FromRoot("FieldLogger/Views/HomePage.xaml"));
|
||||||
|
var state = File.ReadAllText(FromRoot("FieldLogger/ViewModels/HomeViewModel.cs"));
|
||||||
|
Assert.Contains("CAPTURE FROM RECEIVER", home, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("Press LOG on the locating receiver", state, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("CaptureCommand", home, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Primary_pages_use_status_chrome_and_floating_menu()
|
||||||
|
{
|
||||||
|
foreach (var path in new[]
|
||||||
|
{
|
||||||
|
"FieldLogger/Views/JobsPage.xaml",
|
||||||
|
"FieldLogger/Views/MapPage.xaml",
|
||||||
|
"FieldLogger/Views/SettingsPage.xaml",
|
||||||
|
})
|
||||||
|
{
|
||||||
|
var page = File.ReadAllText(FromRoot(path));
|
||||||
|
Assert.Contains("controls:AppStatusBar", page, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("controls:FloatingMenuButton", page, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("Shell.NavBarIsVisible=\"False\"", page, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Settings_hides_manual_map_and_broker_configuration()
|
||||||
|
{
|
||||||
|
var settings = File.ReadAllText(FromRoot("FieldLogger/Views/SettingsPage.xaml"));
|
||||||
|
Assert.DoesNotContain("Google Maps API Key", settings, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("Mqtt", settings, StringComparison.OrdinalIgnoreCase);
|
||||||
|
Assert.DoesNotContain("Durable cloud sync", settings, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("OpenDebugCommand", settings, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Map_has_no_heading_panel_and_fills_its_root_grid()
|
||||||
|
{
|
||||||
|
var map = File.ReadAllText(FromRoot("FieldLogger/Views/MapPage.xaml"));
|
||||||
|
Assert.DoesNotContain("LIVE MAP", map, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("<ContentView x:Name=\"MapHost\"", map, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void User_facing_app_name_is_UM_Trace()
|
||||||
|
{
|
||||||
|
var shell = File.ReadAllText(FromRoot("FieldLogger/AppShell.xaml"));
|
||||||
|
var settings = File.ReadAllText(FromRoot("FieldLogger/Views/SettingsPage.xaml"));
|
||||||
|
Assert.Contains("Title=\"UM Trace\"", shell, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("UM Trace v", settings, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Jobs_uses_a_stable_full_height_refresh_layout()
|
||||||
|
{
|
||||||
|
var page = File.ReadAllText(FromRoot("FieldLogger/Views/JobsPage.xaml"));
|
||||||
|
var viewModel = File.ReadAllText(FromRoot("FieldLogger/ViewModels/JobsViewModel.cs"));
|
||||||
|
|
||||||
|
Assert.Contains("IsRefreshing=\"{Binding IsRefreshing, Mode=OneWay}\"", page, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("IsRefreshing=\"{Binding IsBusy}\"", page, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("ItemsUpdatingScrollMode=\"KeepItemsInView\"", page, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("HorizontalOptions=\"Fill\" VerticalOptions=\"Fill\"", page, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("Padding=\"16,16,16,88\"", page, StringComparison.Ordinal);
|
||||||
|
Assert.Contains("ApplySnapshot(items)", viewModel, StringComparison.Ordinal);
|
||||||
|
Assert.DoesNotContain("Jobs.Clear()", viewModel, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Device_deploy_rebuilds_Xaml_and_brand_assets_from_a_clean_iOS_output()
|
||||||
|
{
|
||||||
|
var script = File.ReadAllText(FromRoot("scripts/deploy-ios-device.sh"));
|
||||||
|
var cleanIndex = script.IndexOf("\"${dotnet_bin}\" clean", StringComparison.Ordinal);
|
||||||
|
var runIndex = script.IndexOf("\"${dotnet_bin}\" build", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
Assert.True(cleanIndex >= 0, "Device deployment must clean stale partial MAUI outputs.");
|
||||||
|
Assert.True(runIndex > cleanIndex, "The clean must run before the device build.");
|
||||||
|
Assert.Contains("-p:RuntimeIdentifier=ios-arm64", script, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FromRoot(string relativePath) => Path.Combine(FindRepositoryRoot(), relativePath);
|
||||||
|
|
||||||
|
private static string FindRepositoryRoot()
|
||||||
|
{
|
||||||
|
for (var directory = new DirectoryInfo(AppContext.BaseDirectory);
|
||||||
|
directory is not null;
|
||||||
|
directory = directory.Parent)
|
||||||
|
{
|
||||||
|
if (File.Exists(Path.Combine(directory.FullName, "FieldLogger.sln")))
|
||||||
|
return directory.FullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new DirectoryNotFoundException("Could not find the FieldLogger repository root.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -167,7 +167,7 @@ public class RoundTripTests
|
|||||||
int triggersSeen = 0;
|
int triggersSeen = 0;
|
||||||
|
|
||||||
// Stand-in App: consume telemetry, and on each trigger apply a simple accuracy gate
|
// Stand-in App: consume telemetry, and on each trigger apply a simple accuracy gate
|
||||||
// and write back a capture-result. This is what Field Logger's Locator Driver does.
|
// and write back a capture-result. This is what UM Trace's Locator Driver does.
|
||||||
link.ToApp += frame =>
|
link.ToApp += frame =>
|
||||||
{
|
{
|
||||||
switch (Frames.PeekType(frame))
|
switch (Frames.PeekType(frame))
|
||||||
|
|||||||
Reference in New Issue
Block a user