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

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