Files
ulapp/FieldLogger/ViewModels/HomeViewModel.cs
2026-08-23 20:54:03 -05:00

191 lines
6.0 KiB
C#

using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FieldLogger.Models;
using FieldLogger.Services;
using FieldLogger.Services.Data;
using FieldLogger.Services.Sync;
namespace FieldLogger.ViewModels;
public sealed partial class HomeViewModel : ObservableObject
{
private readonly DeviceConnectionManager _manager;
private readonly MaglinkService _gps;
private readonly PointLogger _pointLogger;
private readonly AppDatabase _database;
private readonly SettingsService _settings;
private readonly IMqttSyncService _sync;
public DeviceConnectionManager Manager => _manager;
[ObservableProperty]
private string _activeJobName = "No active job";
[ObservableProperty]
private bool _hasActiveJob;
[ObservableProperty]
private int _pointCount;
[ObservableProperty]
private string _fixSummary = "Waiting for GPS…";
[ObservableProperty]
private string _fixStatusLabel = "—";
[ObservableProperty]
private string _fixAccuracy = "";
[ObservableProperty]
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 HomeViewModel(DeviceConnectionManager manager, MaglinkService gps,
PointLogger pointLogger, AppDatabase database, SettingsService settings, IMqttSyncService sync)
{
_manager = manager;
_gps = gps;
_pointLogger = pointLogger;
_database = database;
_settings = settings;
_sync = sync;
SyncStatus = sync.Status;
_gps.FixReceived += OnFixReceived;
_pointLogger.PointSaved += OnPointSaved;
_pointLogger.PacketIgnoredNoJob += OnPacketIgnoredNoJob;
_sync.StatusChanged += OnSyncStatusChanged;
_sync.PointRejected += OnPointRejected;
}
/// <summary>Called from the page's OnAppearing.</summary>
public async Task InitializeAsync()
{
_manager.Start();
await RefreshJobAsync();
}
public async Task RefreshJobAsync()
{
var jobId = _settings.ActiveJobId;
if (jobId is null)
{
HasActiveJob = false;
ActiveJobName = "No active job";
PointCount = 0;
RecentPoints.Clear();
return;
}
var job = await _database.GetJobAsync(jobId.Value);
if (job is null)
{
_settings.ActiveJobId = null;
await RefreshJobAsync();
return;
}
HasActiveJob = true;
ActiveJobName = job.Name;
PointCount = await _database.GetPointCountAsync(job.Id);
var points = await _database.GetPointsAsync(job.Id);
RecentPoints.Clear();
foreach (var p in points.OrderByDescending(p => p.TimestampUtc).Take(10))
RecentPoints.Add(p);
}
[RelayCommand]
private async Task NewJobAsync()
{
var name = await Shell.Current.DisplayPromptAsync("New Job", "Job name:",
placeholder: $"Job {DateTime.Now:yyyy-MM-dd}");
if (string.IsNullOrWhiteSpace(name))
return;
var job = await _database.CreateJobAsync(name.Trim());
_settings.ActiveJobId = job.Id;
await RefreshJobAsync();
}
[RelayCommand]
private Task SelectJobAsync() => Shell.Current.GoToAsync("//jobs");
[RelayCommand]
private Task SelectLocatorAsync() => Shell.Current.GoToAsync($"devicescan?kind={DeviceKind.Locator}");
[RelayCommand]
private Task SelectGpsAsync() => Shell.Current.GoToAsync($"devicescan?kind={DeviceKind.RtkGps}");
[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(() =>
{
FixStatusLabel = fix.StatusLabel;
FixSummary = fix.HasPosition
? $"{fix.Latitude:F8}, {fix.Longitude:F8} · {fix.AltitudeCorrected:F2} m"
: "No position";
FixAccuracy = $"H ±{fix.Hrms:F3} m · V ±{fix.Vrms:F3} m · {fix.SatellitesUsed} sats · batt {fix.BatteryPercent}%";
});
}
private void OnPointSaved(object? sender, LoggedPoint point)
{
RecentPoints.Insert(0, point);
while (RecentPoints.Count > 10)
RecentPoints.RemoveAt(RecentPoints.Count - 1);
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)
{
await Shell.Current.DisplayAlert("No Active Job",
"A point was logged on the receiver, but no job is active so it was not saved. Create or select a job first.",
"OK");
}
private void OnSyncStatusChanged(object? sender, string status) =>
MainThread.BeginInvokeOnMainThread(() => SyncStatus = status);
private void OnPointRejected(object? sender, PointSyncFailureEventArgs e) =>
MainThread.BeginInvokeOnMainThread(async () =>
await Shell.Current.DisplayAlert(
"Point Saved Locally — Sync Needs Attention",
$"Point #{e.Point.Id} remains on this device and was not uploaded. Reason: {e.ReasonCode}.",
"OK"));
}