diff --git a/FieldLogger/App.xaml b/FieldLogger/App.xaml index d653539..8ab51dc 100644 --- a/FieldLogger/App.xaml +++ b/FieldLogger/App.xaml @@ -7,6 +7,7 @@ + diff --git a/FieldLogger/AppShell.xaml b/FieldLogger/AppShell.xaml index d2b98a2..1ff3dfd 100644 --- a/FieldLogger/AppShell.xaml +++ b/FieldLogger/AppShell.xaml @@ -4,13 +4,20 @@ 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="Field Logger" + BackgroundColor="{DynamicResource UlColorCanvas}" + ForegroundColor="{DynamicResource UlColorText}" + TitleColor="{DynamicResource UlColorText}" + UnselectedColor="{DynamicResource UlColorTextMuted}" + TabBarBackgroundColor="{DynamicResource UlColorSurfaceRaised}" + TabBarForegroundColor="{DynamicResource UlColorPrimary}" + TabBarTitleColor="{DynamicResource UlColorPrimary}" + TabBarUnselectedColor="{DynamicResource UlColorTextMuted}"> - diff --git a/FieldLogger/AppShell.xaml.cs b/FieldLogger/AppShell.xaml.cs index ed0bb23..75033ff 100644 --- a/FieldLogger/AppShell.xaml.cs +++ b/FieldLogger/AppShell.xaml.cs @@ -10,5 +10,10 @@ public partial class AppShell : Shell Routing.RegisterRoute("devicescan", typeof(DeviceScanPage)); Routing.RegisterRoute("jobdetail", typeof(JobDetailPage)); +#if DEBUG + // Keep diagnostics available to development builds without presenting Debug as + // a production primary tab. + Routing.RegisterRoute("debug", typeof(DebugPage)); +#endif } } diff --git a/FieldLogger/Resources/Styles/Colors.xaml b/FieldLogger/Resources/Styles/Colors.xaml index 30307a5..a1d4962 100644 --- a/FieldLogger/Resources/Styles/Colors.xaml +++ b/FieldLogger/Resources/Styles/Colors.xaml @@ -1,45 +1,11 @@ - + - - - - #512BD4 - #ac99ea - #242424 - #DFD8F7 - #9880e5 - #2B0B98 - - White - Black - #D600AA - #190649 - #1f1f1f - - #E1E1E1 - #C8C8C8 - #ACACAC - #919191 - #6E6E6E - #404040 - #212121 - #141414 - - - - - - - - - - - - - - - - \ No newline at end of file + + diff --git a/FieldLogger/Resources/Styles/GeneratedTokens.xaml b/FieldLogger/Resources/Styles/GeneratedTokens.xaml new file mode 100644 index 0000000..c00863c --- /dev/null +++ b/FieldLogger/Resources/Styles/GeneratedTokens.xaml @@ -0,0 +1,183 @@ + + + + + + 3.0.0 + + + #EEF3F2 + #FCFEFD + #FFFFFF + #14252A + #14252A + #5D6B6E + #0B7774 + #075F5D + #B82D46 + #C7D2D0 + #B82D46 + #DCE9E7 + #347A55 + #A85C00 + #B42318 + #40565B + #819092 + #FFFFFF + #FFFFFF + #FFFFFF + + #FFFFFF + #FFFFFF + #FFFFFF + #000000 + #000000 + #344448 + #075F5D + #14252A + #982138 + #5D6B6E + #982138 + #E8F0EF + #245F40 + #754000 + #871A12 + #14252A + #344448 + #FFFFFF + #FFFFFF + #FFFFFF + + + #EEF3F2 + + #FCFEFD + + #FFFFFF + + #14252A + + #14252A + + #5D6B6E + + #0B7774 + + #075F5D + + #B82D46 + + #C7D2D0 + + #B82D46 + + #DCE9E7 + + #347A55 + + #A85C00 + + #B42318 + + #40565B + + #819092 + + #FFFFFF + + #FFFFFF + + #FFFFFF + + + + #0B7774 + + #075F5D + + #14252A + + #DCE9E7 + + #5D6B6E + + #B82D46 + + #FFFFFF + + #14252A + + #B82D46 + + #14252A + + #14252A + + #EEF3F2 + + #C7D2D0 + + #819092 + + #5D6B6E + + #5D6B6E + + #40565B + + #14252A + + #14252A + + + + 4 + 8 + 12 + 16 + 24 + 32 + 48 + 8 + 12 + 20 + 999 + 48 + 56 + 64 + + + OpenSansRegular + OpenSansSemibold + monospace + 40 + 48 + 700 + 28 + 34 + 700 + 22 + 28 + 600 + 16 + 24 + 400 + 14 + 20 + 400 + 14 + 20 + 600 + 16 + 22 + 500 + 120 + 200 + 600 + 900 + 1200 + diff --git a/FieldLogger/Resources/Styles/Styles.xaml b/FieldLogger/Resources/Styles/Styles.xaml index fdb0cd7..8fa635f 100644 --- a/FieldLogger/Resources/Styles/Styles.xaml +++ b/FieldLogger/Resources/Styles/Styles.xaml @@ -1,444 +1,293 @@ - + - + + - - - - - - - - - - - - - + - - - + + + + + - + + + + + + - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + - - - - diff --git a/FieldLogger/Resources/Styles/ThemeManager.cs b/FieldLogger/Resources/Styles/ThemeManager.cs new file mode 100644 index 0000000..6afb98b --- /dev/null +++ b/FieldLogger/Resources/Styles/ThemeManager.cs @@ -0,0 +1,38 @@ +namespace FieldLogger.Resources.Styles; + +public enum UlThemeMode +{ + Default, + Sunlight, +} + +/// Applies generated semantic colors. Call on the UI thread after app resources initialize. +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; + } +} diff --git a/FieldLogger/ViewModels/HomeViewModel.cs b/FieldLogger/ViewModels/HomeViewModel.cs index 0598b96..8c0cf36 100644 --- a/FieldLogger/ViewModels/HomeViewModel.cs +++ b/FieldLogger/ViewModels/HomeViewModel.cs @@ -40,6 +40,16 @@ public sealed partial class HomeViewModel : ObservableObject [ObservableProperty] private string _syncStatus = ""; + public bool HasNoActiveJob => !HasActiveJob; + + 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 RecentPoints { get; } = new(); public HomeViewModel(DeviceConnectionManager manager, MaglinkService gps, @@ -149,6 +159,15 @@ public sealed partial class HomeViewModel : ObservableObject 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", diff --git a/FieldLogger/ViewModels/JobsViewModel.cs b/FieldLogger/ViewModels/JobsViewModel.cs index 8cb88b8..03c3a46 100644 --- a/FieldLogger/ViewModels/JobsViewModel.cs +++ b/FieldLogger/ViewModels/JobsViewModel.cs @@ -12,13 +12,28 @@ public sealed partial class JobListItem : ObservableObject public required Job Job { get; init; } [ObservableProperty] + [NotifyPropertyChangedFor(nameof(JobSummary))] + [NotifyPropertyChangedFor(nameof(DeleteDescription))] + [NotifyPropertyChangedFor(nameof(AccessibilityDescription))] private int _pointCount; [ObservableProperty] + [NotifyPropertyChangedFor(nameof(ActiveStatusLabel))] + [NotifyPropertyChangedFor(nameof(ActiveActionLabel))] + [NotifyPropertyChangedFor(nameof(ActiveActionDescription))] + [NotifyPropertyChangedFor(nameof(AccessibilityDescription))] private bool _isActive; public string Name => Job.Name; 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 @@ -37,6 +52,7 @@ public sealed partial class JobsViewModel : ObservableObject _settings = settings; } + [RelayCommand] public async Task RefreshAsync() { IsBusy = true; diff --git a/FieldLogger/ViewModels/MapViewModel.cs b/FieldLogger/ViewModels/MapViewModel.cs index 9149588..6c0d18b 100644 --- a/FieldLogger/ViewModels/MapViewModel.cs +++ b/FieldLogger/ViewModels/MapViewModel.cs @@ -16,10 +16,10 @@ public sealed partial class MapViewModel : ObservableObject private int _jobId; [ObservableProperty] - private string _jobName = ""; + private string _jobName = "Active job map"; [ObservableProperty] - private string _statusText = ""; + private string _statusText = "Loading locally saved points…"; public SettingsService Settings => _settings; @@ -35,7 +35,7 @@ public sealed partial class MapViewModel : ObservableObject var jobId = JobId > 0 ? JobId : _settings.ActiveJobId; if (jobId is null) { - JobName = ""; + JobName = "No active job"; StatusText = "No job selected. Create or activate a job to see its points."; return new List(); } diff --git a/FieldLogger/Views/HomePage.xaml b/FieldLogger/Views/HomePage.xaml index 76ae658..f4c4597 100644 --- a/FieldLogger/Views/HomePage.xaml +++ b/FieldLogger/Views/HomePage.xaml @@ -5,80 +5,163 @@ xmlns:models="clr-namespace:FieldLogger.Models" x:Class="FieldLogger.Views.HomePage" x:DataType="vm:HomeViewModel" - Title="Field Logger"> + Title="Field Logger" + BackgroundColor="{DynamicResource UlColorCanvas}"> - + + + - - - - - - -