diff --git a/FieldLogger/App.xaml.cs b/FieldLogger/App.xaml.cs
index d8a56b4..e239445 100644
--- a/FieldLogger/App.xaml.cs
+++ b/FieldLogger/App.xaml.cs
@@ -14,8 +14,12 @@ public partial class App : Application
_connectionManager = connectionManager;
_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}");
+
+ // 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)
diff --git a/FieldLogger/AppShell.xaml b/FieldLogger/AppShell.xaml
index 1ff3dfd..acc6f56 100644
--- a/FieldLogger/AppShell.xaml
+++ b/FieldLogger/AppShell.xaml
@@ -4,7 +4,8 @@
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:views="clr-namespace:FieldLogger.Views"
- Title="Field Logger"
+ Title="UM Trace"
+ FlyoutBehavior="Flyout"
BackgroundColor="{DynamicResource UlColorCanvas}"
ForegroundColor="{DynamicResource UlColorText}"
TitleColor="{DynamicResource UlColorText}"
@@ -14,11 +15,26 @@
TabBarTitleColor="{DynamicResource UlColorPrimary}"
TabBarUnselectedColor="{DynamicResource UlColorTextMuted}">
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FieldLogger/AppShell.xaml.cs b/FieldLogger/AppShell.xaml.cs
index 75033ff..3002987 100644
--- a/FieldLogger/AppShell.xaml.cs
+++ b/FieldLogger/AppShell.xaml.cs
@@ -10,10 +10,10 @@ public partial class AppShell : Shell
Routing.RegisterRoute("devicescan", typeof(DeviceScanPage));
Routing.RegisterRoute("jobdetail", typeof(JobDetailPage));
+ Routing.RegisterRoute("debug", typeof(HomePage));
#if DEBUG
- // Keep diagnostics available to development builds without presenting Debug as
- // a production primary tab.
- Routing.RegisterRoute("debug", typeof(DebugPage));
+ // The raw device console is a development-only maintenance interface.
+ Routing.RegisterRoute("deviceconsole", typeof(DebugPage));
#endif
}
}
diff --git a/FieldLogger/FieldLogger.csproj b/FieldLogger/FieldLogger.csproj
index b11545b..1f608fd 100644
--- a/FieldLogger/FieldLogger.csproj
+++ b/FieldLogger/FieldLogger.csproj
@@ -64,10 +64,10 @@
-
+
-
+
diff --git a/FieldLogger/MauiProgram.cs b/FieldLogger/MauiProgram.cs
index f65cf29..aa00ca2 100644
--- a/FieldLogger/MauiProgram.cs
+++ b/FieldLogger/MauiProgram.cs
@@ -44,6 +44,7 @@ public static class MauiProgram
builder.Services.AddSingleton();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+ builder.Services.AddSingleton();
builder.Services.AddTransient();
builder.Services.AddTransient();
diff --git a/FieldLogger/Platforms/MacCatalyst/Info.plist b/FieldLogger/Platforms/MacCatalyst/Info.plist
index 54a53df..ec048e8 100644
--- a/FieldLogger/Platforms/MacCatalyst/Info.plist
+++ b/FieldLogger/Platforms/MacCatalyst/Info.plist
@@ -35,8 +35,8 @@
XSAppIconAssets
Assets.xcassets/appicon.appiconset
NSBluetoothAlwaysUsageDescription
- Field Logger uses Bluetooth to connect to your Underground Magnetics locating receiver and Maglink RTK GPS receiver.
+ UM Trace uses Bluetooth to connect to your Underground Magnetics locating receiver and Maglink RTK GPS receiver.
NSLocationWhenInUseUsageDescription
- Field Logger uses your location to display logged points on the map.
+ UM Trace uses your location to display logged points on the map.
diff --git a/FieldLogger/Resources/AppIcon/appicon.svg b/FieldLogger/Resources/AppIcon/appicon.svg
index 9d63b65..e24dfc9 100644
--- a/FieldLogger/Resources/AppIcon/appicon.svg
+++ b/FieldLogger/Resources/AppIcon/appicon.svg
@@ -1,4 +1,4 @@
\ No newline at end of file
+
+
diff --git a/FieldLogger/Resources/AppIcon/appiconfg.png b/FieldLogger/Resources/AppIcon/appiconfg.png
new file mode 100644
index 0000000..0d4b161
Binary files /dev/null and b/FieldLogger/Resources/AppIcon/appiconfg.png differ
diff --git a/FieldLogger/Resources/AppIcon/appiconfg.svg b/FieldLogger/Resources/AppIcon/appiconfg.svg
deleted file mode 100644
index 21dfb25..0000000
--- a/FieldLogger/Resources/AppIcon/appiconfg.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/FieldLogger/Resources/Splash/splash.svg b/FieldLogger/Resources/Splash/splash.svg
deleted file mode 100644
index 21dfb25..0000000
--- a/FieldLogger/Resources/Splash/splash.svg
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
\ No newline at end of file
diff --git a/FieldLogger/Resources/Splash/um_dual_receiver.png b/FieldLogger/Resources/Splash/um_dual_receiver.png
new file mode 100644
index 0000000..c5a6433
Binary files /dev/null and b/FieldLogger/Resources/Splash/um_dual_receiver.png differ
diff --git a/FieldLogger/Services/Ble/BleScanner.cs b/FieldLogger/Services/Ble/BleScanner.cs
index b81e895..a576766 100644
--- a/FieldLogger/Services/Ble/BleScanner.cs
+++ b/FieldLogger/Services/Ble/BleScanner.cs
@@ -109,7 +109,7 @@ public sealed class BleScanner
new InvalidOperationException("Bluetooth is turned off."),
BluetoothState.Unauthorized =>
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 =>
new InvalidOperationException("Bluetooth is not available on this Mac."),
_ => new InvalidOperationException($"Bluetooth is not ready (state: {_bluetooth.State}).")
diff --git a/FieldLogger/ViewModels/AppStatusViewModel.cs b/FieldLogger/ViewModels/AppStatusViewModel.cs
new file mode 100644
index 0000000..3bce95b
--- /dev/null
+++ b/FieldLogger/ViewModels/AppStatusViewModel.cs
@@ -0,0 +1,105 @@
+using System.ComponentModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using FieldLogger.Models;
+using FieldLogger.Services;
+
+namespace FieldLogger.ViewModels;
+
+///
+/// Shared, glanceable hardware status shown above each primary workflow. The timer marks
+/// position data stale even when the receiver stops sending without disconnecting.
+///
+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();
+
+ /// Refreshes status safely from device callbacks or a UI dispatcher timer.
+ 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}";
+ }
+}
diff --git a/FieldLogger/ViewModels/HomeViewModel.cs b/FieldLogger/ViewModels/HomeViewModel.cs
index 8c0cf36..c0939e4 100644
--- a/FieldLogger/ViewModels/HomeViewModel.cs
+++ b/FieldLogger/ViewModels/HomeViewModel.cs
@@ -42,6 +42,13 @@ public sealed partial class HomeViewModel : ObservableObject
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";
@@ -73,13 +80,6 @@ public sealed partial class HomeViewModel : ObservableObject
/// Called from the page's OnAppearing.
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();
await RefreshJobAsync();
}
@@ -139,6 +139,9 @@ public sealed partial class HomeViewModel : ObservableObject
[RelayCommand]
private Task ViewMapAsync() => Shell.Current.GoToAsync("//map");
+ [RelayCommand]
+ private Task OpenDeviceConsoleAsync() => Shell.Current.GoToAsync("deviceconsole");
+
private void OnFixReceived(object? sender, GnssFix fix)
{
MainThread.BeginInvokeOnMainThread(() =>
diff --git a/FieldLogger/ViewModels/JobsViewModel.cs b/FieldLogger/ViewModels/JobsViewModel.cs
index 03c3a46..f53704f 100644
--- a/FieldLogger/ViewModels/JobsViewModel.cs
+++ b/FieldLogger/ViewModels/JobsViewModel.cs
@@ -40,41 +40,112 @@ public sealed partial class JobsViewModel : ObservableObject
{
private readonly AppDatabase _database;
private readonly SettingsService _settings;
+ private readonly SemaphoreSlim _refreshGate = new(1, 1);
public ObservableCollection Jobs { get; } = new();
[ObservableProperty]
private bool _isBusy;
+ [ObservableProperty]
+ private bool _isRefreshing;
+
public JobsViewModel(AppDatabase database, SettingsService settings)
{
_database = database;
_settings = settings;
}
+ public Task LoadAsync()
+ => ReloadAsync(showRefreshIndicator: false, skipIfBusy: true);
+
[RelayCommand]
- public async Task RefreshAsync()
+ 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
{
+ IsBusy = true;
+ if (showRefreshIndicator)
+ IsRefreshing = true;
+
var jobs = await _database.GetJobsAsync();
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(jobs.Count);
foreach (var job in jobs)
{
- Jobs.Add(new JobListItem
+ items.Add(new JobListItem
{
Job = job,
PointCount = await _database.GetPointCountAsync(job.Id),
IsActive = job.Id == activeId,
});
}
+
+ ApplySnapshot(items);
}
finally
{
+ if (showRefreshIndicator)
+ IsRefreshing = false;
IsBusy = false;
+ _refreshGate.Release();
+ }
+ }
+
+ private async Task WaitForRefreshGateAsync()
+ {
+ await _refreshGate.WaitAsync();
+ return true;
+ }
+
+ private void ApplySnapshot(IReadOnlyList 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);
}
}
@@ -101,7 +172,7 @@ public sealed partial class JobsViewModel : ObservableObject
_settings.ActiveJobId = job.Id;
Console.WriteLine($"NewJobAsync: Set active job to {job.Id}");
- await RefreshAsync();
+ await ReloadAsync(showRefreshIndicator: false, skipIfBusy: false);
Console.WriteLine("NewJobAsync: Refresh complete");
}
catch (Exception ex)
@@ -135,6 +206,6 @@ public sealed partial class JobsViewModel : ObservableObject
await _database.DeleteJobAsync(item.Job.Id);
if (_settings.ActiveJobId == item.Job.Id)
_settings.ActiveJobId = null;
- await RefreshAsync();
+ await ReloadAsync(showRefreshIndicator: false, skipIfBusy: false);
}
}
diff --git a/FieldLogger/ViewModels/SettingsViewModel.cs b/FieldLogger/ViewModels/SettingsViewModel.cs
index 73586ec..431a57d 100644
--- a/FieldLogger/ViewModels/SettingsViewModel.cs
+++ b/FieldLogger/ViewModels/SettingsViewModel.cs
@@ -2,118 +2,31 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FieldLogger.Models;
using FieldLogger.Services;
-using FieldLogger.Services.Sync;
namespace FieldLogger.ViewModels;
public sealed partial class SettingsViewModel : ObservableObject
{
private readonly DeviceConnectionManager _manager;
- private readonly SettingsService _settings;
- private readonly IMqttSyncService _sync;
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 SettingsViewModel(DeviceConnectionManager manager, SettingsService settings, IMqttSyncService sync)
+ public SettingsViewModel(DeviceConnectionManager 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]
private Task ChangeLocatorAsync() => Shell.Current.GoToAsync($"devicescan?kind={DeviceKind.Locator}");
[RelayCommand]
private Task ChangeGpsAsync() => Shell.Current.GoToAsync($"devicescan?kind={DeviceKind.RtkGps}");
+ [RelayCommand]
+ private Task OpenDebugAsync() => Shell.Current.GoToAsync("debug");
+
[RelayCommand]
private async Task ForgetLocatorAsync()
{
diff --git a/FieldLogger/Views/Controls/AppStatusBar.xaml b/FieldLogger/Views/Controls/AppStatusBar.xaml
new file mode 100644
index 0000000..c85453c
--- /dev/null
+++ b/FieldLogger/Views/Controls/AppStatusBar.xaml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/FieldLogger/Views/Controls/AppStatusBar.xaml.cs b/FieldLogger/Views/Controls/AppStatusBar.xaml.cs
new file mode 100644
index 0000000..209546c
--- /dev/null
+++ b/FieldLogger/Views/Controls/AppStatusBar.xaml.cs
@@ -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();
+ }
+}
diff --git a/FieldLogger/Views/Controls/FloatingMenuButton.xaml b/FieldLogger/Views/Controls/FloatingMenuButton.xaml
new file mode 100644
index 0000000..a1beb59
--- /dev/null
+++ b/FieldLogger/Views/Controls/FloatingMenuButton.xaml
@@ -0,0 +1,15 @@
+
+
+
+
diff --git a/FieldLogger/Views/Controls/FloatingMenuButton.xaml.cs b/FieldLogger/Views/Controls/FloatingMenuButton.xaml.cs
new file mode 100644
index 0000000..63b760c
--- /dev/null
+++ b/FieldLogger/Views/Controls/FloatingMenuButton.xaml.cs
@@ -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;
+ }
+}
diff --git a/FieldLogger/Views/DebugPage.xaml b/FieldLogger/Views/DebugPage.xaml
index 5a5c2a9..c16b9d7 100644
--- a/FieldLogger/Views/DebugPage.xaml
+++ b/FieldLogger/Views/DebugPage.xaml
@@ -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">
-
+ Title="Device Console">
+
+
+
@@ -88,4 +91,5 @@
+
diff --git a/FieldLogger/Views/DebugPage.xaml.cs b/FieldLogger/Views/DebugPage.xaml.cs
index a8374c1..e72c5a2 100644
--- a/FieldLogger/Views/DebugPage.xaml.cs
+++ b/FieldLogger/Views/DebugPage.xaml.cs
@@ -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()
diff --git a/FieldLogger/Views/HomePage.xaml b/FieldLogger/Views/HomePage.xaml
index f4c4597..b3ff658 100644
--- a/FieldLogger/Views/HomePage.xaml
+++ b/FieldLogger/Views/HomePage.xaml
@@ -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}">
-
+
+
+
-
-
@@ -187,6 +190,13 @@
+
+
+
diff --git a/FieldLogger/Views/HomePage.xaml.cs b/FieldLogger/Views/HomePage.xaml.cs
index 6502317..bc1dd0b 100644
--- a/FieldLogger/Views/HomePage.xaml.cs
+++ b/FieldLogger/Views/HomePage.xaml.cs
@@ -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()
diff --git a/FieldLogger/Views/JobDetailPage.xaml b/FieldLogger/Views/JobDetailPage.xaml
index ee66141..5f3e855 100644
--- a/FieldLogger/Views/JobDetailPage.xaml
+++ b/FieldLogger/Views/JobDetailPage.xaml
@@ -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}">
-
+
+
+
@@ -37,7 +40,7 @@
-
@@ -103,4 +106,5 @@
+
diff --git a/FieldLogger/Views/JobDetailPage.xaml.cs b/FieldLogger/Views/JobDetailPage.xaml.cs
index 64d9e4d..b386cba 100644
--- a/FieldLogger/Views/JobDetailPage.xaml.cs
+++ b/FieldLogger/Views/JobDetailPage.xaml.cs
@@ -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()
diff --git a/FieldLogger/Views/JobsPage.xaml b/FieldLogger/Views/JobsPage.xaml
index d3e39d8..679d08c 100644
--- a/FieldLogger/Views/JobsPage.xaml
+++ b/FieldLogger/Views/JobsPage.xaml
@@ -2,11 +2,18 @@
-
+
+
+
+
@@ -22,9 +29,15 @@
MinimumHeightRequest="{StaticResource UlTouchComfortable}" FontSize="16" FontAttributes="Bold"
SemanticProperties.Description="Create a new job and make it active" />
-
+
+
+
+
+
+
+
+
diff --git a/FieldLogger/Views/JobsPage.xaml.cs b/FieldLogger/Views/JobsPage.xaml.cs
index 48778a4..302fa23 100644
--- a/FieldLogger/Views/JobsPage.xaml.cs
+++ b/FieldLogger/Views/JobsPage.xaml.cs
@@ -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();
}
}
diff --git a/FieldLogger/Views/MapPage.xaml b/FieldLogger/Views/MapPage.xaml
index 6a75b37..6fc2740 100644
--- a/FieldLogger/Views/MapPage.xaml
+++ b/FieldLogger/Views/MapPage.xaml
@@ -2,34 +2,31 @@
-
-
-
-
-
-
-
-
-
-
-
+
-
+
+
+
+
+
+
diff --git a/FieldLogger/Views/MapPage.xaml.cs b/FieldLogger/Views/MapPage.xaml.cs
index 66caaab..ecc88ca 100644
--- a/FieldLogger/Views/MapPage.xaml.cs
+++ b/FieldLogger/Views/MapPage.xaml.cs
@@ -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()
diff --git a/FieldLogger/Views/SettingsPage.xaml b/FieldLogger/Views/SettingsPage.xaml
index 62a7ccb..5f074aa 100644
--- a/FieldLogger/Views/SettingsPage.xaml
+++ b/FieldLogger/Views/SettingsPage.xaml
@@ -2,11 +2,20 @@
-
-
+ Title="Settings"
+ Shell.NavBarIsVisible="False"
+ BackgroundColor="{DynamicResource UlColorCanvas}">
+
+
+
+
+
+
@@ -40,36 +49,33 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
+
+
-
+
-
+
+
+
+
diff --git a/FieldLogger/Views/SettingsPage.xaml.cs b/FieldLogger/Views/SettingsPage.xaml.cs
index 116127e..59881e1 100644
--- a/FieldLogger/Views/SettingsPage.xaml.cs
+++ b/FieldLogger/Views/SettingsPage.xaml.cs
@@ -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;
}
}
diff --git a/README.md b/README.md
index b1e87df..629108b 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Field Logger
+# UM Trace
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
diff --git a/scripts/deploy-ios-device.sh b/scripts/deploy-ios-device.sh
index 7da7190..4a999f5 100755
--- a/scripts/deploy-ios-device.sh
+++ b/scripts/deploy-ios-device.sh
@@ -38,6 +38,16 @@ export DEVELOPER_DIR="${developer_dir}"
# 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 \
diff --git a/src/FieldLogger.Sync/FieldLogger.Sync.csproj b/src/FieldLogger.Sync/FieldLogger.Sync.csproj
index 8e002f3..fb130fa 100644
--- a/src/FieldLogger.Sync/FieldLogger.Sync.csproj
+++ b/src/FieldLogger.Sync/FieldLogger.Sync.csproj
@@ -1,7 +1,7 @@