feat: complete UM Trace iOS and field UI integration

This commit is contained in:
Brent Perteet
2026-08-23 20:54:03 -05:00
parent 16c2ada09b
commit 36f583169c
38 changed files with 548 additions and 212 deletions

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

View 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();
}
}

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

View 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;
}
}

View File

@@ -3,10 +3,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
xmlns:models="clr-namespace:FieldLogger.Models"
xmlns:controls="clr-namespace:FieldLogger.Views.Controls"
x:Class="FieldLogger.Views.DebugPage"
x:DataType="vm:DebugViewModel"
Title="Debug">
<ScrollView>
Title="Device Console">
<Grid RowDefinitions="Auto,*">
<controls:AppStatusBar x:Name="AppStatus" />
<ScrollView Grid.Row="1">
<VerticalStackLayout Padding="16" Spacing="12">
<Label Text="Connection Status" FontAttributes="Bold" FontSize="16" />
@@ -88,4 +91,5 @@
</VerticalStackLayout>
</ScrollView>
</Grid>
</ContentPage>

View File

@@ -6,11 +6,12 @@ public partial class DebugPage : ContentPage
{
private readonly DebugViewModel _viewModel;
public DebugPage(DebugViewModel viewModel)
public DebugPage(DebugViewModel viewModel, AppStatusViewModel appStatusViewModel)
{
InitializeComponent();
_viewModel = viewModel;
BindingContext = viewModel;
AppStatus.BindingContext = appStatusViewModel;
}
protected override void OnAppearing()

View File

@@ -3,16 +3,19 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
xmlns:models="clr-namespace:FieldLogger.Models"
xmlns:controls="clr-namespace:FieldLogger.Views.Controls"
x:Class="FieldLogger.Views.HomePage"
x:DataType="vm:HomeViewModel"
Title="Field Logger"
Title="Debug"
BackgroundColor="{DynamicResource UlColorCanvas}">
<ScrollView>
<Grid RowDefinitions="Auto,*">
<controls:AppStatusBar x:Name="AppStatus" />
<ScrollView Grid.Row="1">
<VerticalStackLayout Padding="16" Spacing="16">
<VerticalStackLayout Spacing="4">
<Label Text="FIELD LOGGER" FontSize="14" FontAttributes="Bold" CharacterSpacing="1.5"
<Label Text="UM TRACE DEBUG" FontSize="14" FontAttributes="Bold" CharacterSpacing="1.5"
TextColor="{DynamicResource UlColorPrimary}" />
<Label Text="Live field capture" Style="{StaticResource UlPageTitle}"
<Label Text="Capture diagnostics" Style="{StaticResource UlPageTitle}"
TextColor="{DynamicResource UlColorText}"
SemanticProperties.HeadingLevel="Level1" />
</VerticalStackLayout>
@@ -187,6 +190,13 @@
</DataTemplate>
</CollectionView.ItemTemplate>
</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>
</ScrollView>
</Grid>
</ContentPage>

View File

@@ -7,10 +7,11 @@ public partial class HomePage : ContentPage
private readonly HomeViewModel _viewModel;
private bool _initialized;
public HomePage(HomeViewModel viewModel)
public HomePage(HomeViewModel viewModel, AppStatusViewModel appStatusViewModel)
{
InitializeComponent();
BindingContext = _viewModel = viewModel;
AppStatus.BindingContext = appStatusViewModel;
}
protected override async void OnAppearing()

View File

@@ -3,11 +3,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
xmlns:models="clr-namespace:FieldLogger.Models"
xmlns:controls="clr-namespace:FieldLogger.Views.Controls"
x:Class="FieldLogger.Views.JobDetailPage"
x:DataType="vm:JobDetailViewModel"
Title="{Binding JobName}"
BackgroundColor="{DynamicResource UlColorCanvas}">
<Grid RowDefinitions="Auto,*" Padding="16" RowSpacing="16">
<Grid RowDefinitions="Auto,*">
<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">
@@ -37,7 +40,7 @@
<VerticalStackLayout Spacing="8">
<Label Text="No captures for this job" Style="{StaticResource UlEmptyStateTitle}"
HorizontalTextAlignment="Center" TextColor="{DynamicResource UlColorText}" />
<Label Text="Return Home and press LOG on the receiver when the job is active."
<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}" />
@@ -103,4 +106,5 @@
</CollectionView.ItemTemplate>
</CollectionView>
</Grid>
</Grid>
</ContentPage>

View File

@@ -6,10 +6,11 @@ public partial class JobDetailPage : ContentPage
{
private readonly JobDetailViewModel _viewModel;
public JobDetailPage(JobDetailViewModel viewModel)
public JobDetailPage(JobDetailViewModel viewModel, AppStatusViewModel appStatusViewModel)
{
InitializeComponent();
BindingContext = _viewModel = viewModel;
AppStatus.BindingContext = appStatusViewModel;
}
protected override async void OnAppearing()

View File

@@ -2,11 +2,18 @@
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
xmlns:controls="clr-namespace:FieldLogger.Views.Controls"
x:Class="FieldLogger.Views.JobsPage"
x:DataType="vm:JobsViewModel"
Title="Jobs"
Shell.NavBarIsVisible="False"
BackgroundColor="{DynamicResource UlColorCanvas}">
<Grid RowDefinitions="Auto,Auto,*" Padding="16" RowSpacing="16">
<Grid RowDefinitions="Auto,*"
HorizontalOptions="Fill" VerticalOptions="Fill">
<controls:AppStatusBar x:Name="AppStatus" />
<Grid Grid.Row="1" RowDefinitions="Auto,Auto,*" Padding="16" RowSpacing="16"
HorizontalOptions="Fill" VerticalOptions="Fill">
<VerticalStackLayout Spacing="4">
<Label Text="FIELD WORK" FontSize="14" FontAttributes="Bold" CharacterSpacing="1.5"
TextColor="{DynamicResource UlColorPrimary}" />
@@ -22,9 +29,15 @@
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 IsBusy}">
<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}"
@@ -88,7 +101,15 @@
</Border>
</DataTemplate>
</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>
</RefreshView>
</Grid>
<controls:FloatingMenuButton Grid.RowSpan="2" HorizontalOptions="End" VerticalOptions="End" ZIndex="20" />
</Grid>
</ContentPage>

View File

@@ -6,15 +6,16 @@ public partial class JobsPage : ContentPage
{
private readonly JobsViewModel _viewModel;
public JobsPage(JobsViewModel viewModel)
public JobsPage(JobsViewModel viewModel, AppStatusViewModel appStatusViewModel)
{
InitializeComponent();
BindingContext = _viewModel = viewModel;
AppStatus.BindingContext = appStatusViewModel;
}
protected override async void OnAppearing()
{
base.OnAppearing();
await _viewModel.RefreshAsync();
await _viewModel.LoadAsync();
}
}

View File

@@ -2,34 +2,31 @@
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
xmlns:controls="clr-namespace:FieldLogger.Views.Controls"
x:Class="FieldLogger.Views.MapPage"
x:DataType="vm:MapViewModel"
Title="Map"
Shell.NavBarIsVisible="False"
BackgroundColor="{DynamicResource UlColorMap}">
<Grid RowDefinitions="Auto,*">
<Border Style="{StaticResource UlCard}" Margin="12" Padding="16" BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
Stroke="{DynamicResource UlColorBorder}" StrokeShape="RoundRectangle 12"
ZIndex="1">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto" RowSpacing="4" ColumnSpacing="12">
<VerticalStackLayout>
<Label Text="LIVE MAP" FontSize="14" FontAttributes="Bold" CharacterSpacing="1.5"
TextColor="{DynamicResource UlColorPrimary}" />
<Label Text="{Binding JobName, TargetNullValue='No active job'}"
Style="{StaticResource UlSectionTitle}" TextColor="{DynamicResource UlColorText}"
SemanticProperties.HeadingLevel="Level1" />
</VerticalStackLayout>
<Button Grid.Column="1" Style="{StaticResource UlButtonSecondary}"
Text="Refresh" Clicked="OnRefreshClicked"
MinimumHeightRequest="{StaticResource UlTouchMinimum}" MinimumWidthRequest="88"
SemanticProperties.Description="Refresh captured points on the map" />
<Label Grid.Row="1" Grid.ColumnSpan="2" Text="{Binding StatusText}" FontSize="14"
TextColor="{DynamicResource UlColorTextMuted}"
SemanticProperties.Description="Current map data status" />
</Grid>
</Border>
<Grid>
<!-- Populated in code-behind: native Map control on iOS/Android/macOS, Google Maps WebView on Windows. -->
<ContentView Grid.RowSpan="2" 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>
</ContentPage>

View File

@@ -23,10 +23,11 @@ public partial class MapPage : ContentPage
private Map? _map;
#endif
public MapPage(MapViewModel viewModel)
public MapPage(MapViewModel viewModel, AppStatusViewModel appStatusViewModel)
{
InitializeComponent();
BindingContext = _viewModel = viewModel;
AppStatus.BindingContext = appStatusViewModel;
}
protected override async void OnAppearing()

View File

@@ -2,11 +2,20 @@
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:vm="clr-namespace:FieldLogger.ViewModels"
xmlns:controls="clr-namespace:FieldLogger.Views.Controls"
x:Class="FieldLogger.Views.SettingsPage"
x:DataType="vm:SettingsViewModel"
Title="Settings">
<ScrollView>
<VerticalStackLayout Padding="16" Spacing="12">
Title="Settings"
Shell.NavBarIsVisible="False"
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" />
@@ -40,36 +49,33 @@
</VerticalStackLayout>
</Border>
<Label Text="Map" FontAttributes="Bold" FontSize="16" Margin="0,12,0,0" />
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
<VerticalStackLayout Spacing="6">
<Label Text="Google Maps API Key (Windows map view)" FontSize="12" TextColor="Gray" />
<Entry Text="{Binding MapsApiKey}" Placeholder="AIza…" IsPassword="False" />
<Label FontSize="11" TextColor="Gray"
Text="On Android the key is configured in AndroidManifest.xml; iOS and macOS use Apple Maps and need no key." />
</VerticalStackLayout>
</Border>
<Label Text="Sync" FontAttributes="Bold" FontSize="16" Margin="0,12,0,0" />
<Border StrokeThickness="0" Background="{AppThemeBinding Light=#F2F2F7, Dark=#1C1C1E}" StrokeShape="RoundRectangle 12" Padding="12">
<VerticalStackLayout Spacing="8">
<Grid ColumnDefinitions="*,Auto">
<Label Text="Durable cloud sync" VerticalOptions="Center" />
<Switch Grid.Column="1" IsToggled="{Binding MqttEnabled}" />
<VerticalStackLayout Spacing="8" Margin="0,12,0,0">
<Label Text="Diagnostics" FontAttributes="Bold" FontSize="16" />
<Border Style="{StaticResource UlCard}"
BackgroundColor="{DynamicResource UlColorSurfaceRaised}"
Stroke="{DynamicResource UlColorBorder}" StrokeShape="RoundRectangle 12" Padding="12">
<Grid ColumnDefinitions="*,Auto" ColumnSpacing="12">
<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>
<Button Grid.Column="1" Text="Open" Command="{Binding OpenDebugCommand}"
Style="{StaticResource UlButtonSecondary}"
MinimumHeightRequest="{StaticResource UlTouchMinimum}"
SemanticProperties.Description="Open UM Trace diagnostics" />
</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 &amp; Connect" Command="{Binding SaveSyncCommand}" />
</VerticalStackLayout>
</Border>
</Border>
</VerticalStackLayout>
<Label Text="{Binding AppVersion, StringFormat='Field Logger v{0}'}"
FontSize="11" TextColor="Gray" HorizontalOptions="Center" Margin="0,20,0,0" />
<Label Text="{Binding AppVersion, StringFormat='UM Trace v{0}'}"
FontSize="11" TextColor="{DynamicResource UlColorTextMuted}"
HorizontalOptions="Center" Margin="0,20,0,0" />
</VerticalStackLayout>
</ScrollView>
</ScrollView>
<controls:FloatingMenuButton Grid.RowSpan="2" HorizontalOptions="End" VerticalOptions="End" ZIndex="20" />
</Grid>
</ContentPage>

View File

@@ -4,9 +4,10 @@ namespace FieldLogger.Views;
public partial class SettingsPage : ContentPage
{
public SettingsPage(SettingsViewModel viewModel)
public SettingsPage(SettingsViewModel viewModel, AppStatusViewModel appStatusViewModel)
{
InitializeComponent();
BindingContext = viewModel;
AppStatus.BindingContext = appStatusViewModel;
}
}