diff --git a/docs/unfinished.md b/docs/unfinished.md index 67095c2..1b4947e 100644 --- a/docs/unfinished.md +++ b/docs/unfinished.md @@ -26,6 +26,26 @@ The Cabrillo output has not been put in front of a contest sponsor's robot. ## Half-built +**The info window: what N1MM has and this does not.** The window follows +`InfoWindow.cs` from the decompiled source: the caption, the call-frame spot, +the country, bearing and sun lines, the WWV line, the message area, both graphs +with goals, the QSO timer in all seven modes, the band timer, the band-change +count and the Run/Mult box. Goals use N1MM's `dhh goal` file format and its +Import Goals rules, including the statistics and CSV files. Left out: + +- the multi-op category mismatch message and the CPU-load advisory for heavy + spot traffic; +- goals kept per contest in the Admin database, and the goal file that a + contest's Associated Files tab loads when the contest starts. Goals are one + list in `settings.json` that stays until it is replaced; +- the highlight on a spot from a preferred spotter; +- N1MM's time-since-last-QSO options 4 to 6, which its code limits by radio + number in a way that does not match their menu labels. This program follows + the labels; +- the hourly graph's slots end on the minute they start, where N1MM's end a + minute early (:19, :39, :59); +- a real check of the RBN link. It is N1MM's URL, and has not been opened. + **The digital window: what N1MM has and this does not.** The window is here — receive pane in both of N1MM's scroll modes, the Letters/Figs and MouseOver readouts, the pause bar, coloured callsigns with N1MM's validity routines, grab diff --git a/src/Nonemm.App/AppSession.Notices.cs b/src/Nonemm.App/AppSession.Notices.cs new file mode 100644 index 0000000..7c76f00 --- /dev/null +++ b/src/Nonemm.App/AppSession.Notices.cs @@ -0,0 +1,68 @@ +using Nonemm.Spotting; + +namespace Nonemm.App; + +/// A line for the info window's message area. +/// `IsAlert` marks one the operator should see, such as a spot of their own call. +public sealed record Notice(DateTime AtUtc, string Text, bool IsAlert = false); + +public sealed partial class AppSession +{ + private const int NoticesKept = 50; + + private readonly List notices = []; + private readonly Lock noticesGuard = new(); + + /// The latest program messages, oldest first: what the keyers reported and + /// the spots of this station's own call. + public IReadOnlyList Notices + { + get + { + lock (noticesGuard) + { + return [.. notices]; + } + } + } + + /// Raised on whatever thread posted the notice. + public event EventHandler? NoticesChanged; + + public void PostNotice(string text, bool isAlert = false) + { + lock (noticesGuard) + { + notices.Add(new Notice(DateTime.UtcNow, text, isAlert)); + if (notices.Count > NoticesKept) + { + notices.RemoveAt(0); + } + } + NoticesChanged?.Invoke(this, EventArgs.Empty); + } + + public void ClearNotices() + { + lock (noticesGuard) + { + notices.Clear(); + } + NoticesChanged?.Invoke(this, EventArgs.Empty); + } + + /// Posts a notice for each spot of our own call in a batch from the cluster. + private void NoticeOwnSpots(IEnumerable batch) + { + string mine = Settings.Station.Callsign.Trim(); + if (mine.Length == 0) + { + return; + } + foreach (Spot spot in batch.Where(s => s.IsStation + && string.Equals(s.Call.Text, mine, StringComparison.OrdinalIgnoreCase))) + { + PostNotice($"{spot.Spotter} spotted {spot.Call.Text} on {spot.Frequency.Kilohertz:0.0} {spot.Comment}".TrimEnd(), isAlert: true); + } + } +} diff --git a/src/Nonemm.App/AppSession.cs b/src/Nonemm.App/AppSession.cs index c39b6c1..35253a0 100644 --- a/src/Nonemm.App/AppSession.cs +++ b/src/Nonemm.App/AppSession.cs @@ -81,6 +81,7 @@ public sealed partial class AppSession : IDisposable BandPlan = settings.ToBandPlan(); ApplySpotSettings(); spotFlush = new Timer(_ => FlushSpots(), null, SpotFlushInterval, SpotFlushInterval); + Reported += (_, text) => PostNotice(text); } /// How often the spots that have arrived go onto the bandmap. N1MM's own @@ -1031,6 +1032,7 @@ public sealed partial class AppSession : IDisposable batch = [.. arriving]; arriving.Clear(); } + NoticeOwnSpots(batch); List keeping = []; foreach (Spot spot in batch) { diff --git a/src/Nonemm.App/Configuration/Settings.cs b/src/Nonemm.App/Configuration/Settings.cs index 2899ea4..8bd68f1 100644 --- a/src/Nonemm.App/Configuration/Settings.cs +++ b/src/Nonemm.App/Configuration/Settings.cs @@ -59,6 +59,8 @@ public sealed record Settings /// Which cluster spots reach the bandmap. public StoredSpotFilter SpotFilter { get; init; } = new(); + public StoredInfoWindow Info { get; init; } = new(); + /// How long a CW or digital spot stays on the bandmap. public int CwDigitalSpotTimeoutMinutes { get; init; } = 10; diff --git a/src/Nonemm.App/Configuration/StoredInfoWindow.cs b/src/Nonemm.App/Configuration/StoredInfoWindow.cs new file mode 100644 index 0000000..bc544bb --- /dev/null +++ b/src/Nonemm.App/Configuration/StoredInfoWindow.cs @@ -0,0 +1,38 @@ +using Nonemm.Contests; + +namespace Nonemm.App.Configuration; + +/// What the info window shows. Each flag is one item on its right-click menu. +public sealed record StoredInfoWindow +{ + public bool CallFrameSpot { get; init; } = true; + + public bool CountryInfo { get; init; } = true; + + public bool SunTimes { get; init; } = true; + + public bool Wwv { get; init; } = true; + + public bool Messages { get; init; } = true; + + public bool QsoTimers { get; init; } = true; + + public bool ShowGoals { get; init; } = true; + + /// Count the rates of every computer on the network rather than this one. + public bool NetworkedRate { get; init; } + + /// Count only the contacts on the band the radio is on. + public bool BandRate { get; init; } + + /// Show points on the left graph instead of contacts. + public bool ShowPoints { get; init; } + + /// The length of one bar on the hourly graph: 20, 30 or 60. + public int HourlyMinutes { get; init; } = 30; + + public QsoTimerOption TimerOption { get; init; } = QsoTimerOption.AllStations; + + /// The goals in `Goals.ToText` form. Empty means none are set. + public string Goals { get; init; } = ""; +} diff --git a/src/Nonemm.App/Dialogs/GoalsDialog.axaml b/src/Nonemm.App/Dialogs/GoalsDialog.axaml new file mode 100644 index 0000000..0bdf430 --- /dev/null +++ b/src/Nonemm.App/Dialogs/GoalsDialog.axaml @@ -0,0 +1,19 @@ + + + +