Add the Avalonia application and the station network
The entry window types contacts and colours them as they are typed; the log, check, bandmap, score and packet windows read the same session. Contacts are shared with the other stations of a multi-operator entry in N1MM's contact message, so an N1MM station on the same network sees them too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
328
src/Nonemm.App/Windows/EntryWindow.Menu.cs
Normal file
328
src/Nonemm.App/Windows/EntryWindow.Menu.cs
Normal file
@@ -0,0 +1,328 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Nonemm.App.Configuration;
|
||||
using Nonemm.App.Dialogs;
|
||||
using Nonemm.Core;
|
||||
using Nonemm.Formats.Adif;
|
||||
using Nonemm.Formats.Cabrillo;
|
||||
using Nonemm.Storage;
|
||||
|
||||
namespace Nonemm.App.Windows;
|
||||
|
||||
/// The entry window's menu. Each item does one thing and says what happened in
|
||||
/// the status line.
|
||||
public sealed partial class EntryWindow
|
||||
{
|
||||
private static readonly HttpClient Http = new() { Timeout = TimeSpan.FromSeconds(30) };
|
||||
|
||||
private async void OnNewDatabase(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
IStorageFile? file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "New log database",
|
||||
SuggestedFileName = "ham.s3db",
|
||||
DefaultExtension = "s3db",
|
||||
SuggestedStartLocation = await Folder(session.Paths.Databases),
|
||||
});
|
||||
OpenDatabaseAt(file);
|
||||
}
|
||||
|
||||
private async void OnOpenDatabase(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
IReadOnlyList<IStorageFile> files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Open log database",
|
||||
AllowMultiple = false,
|
||||
SuggestedStartLocation = await Folder(session.Paths.Databases),
|
||||
});
|
||||
OpenDatabaseAt(files.FirstOrDefault());
|
||||
}
|
||||
|
||||
private void OpenDatabaseAt(IStorageFile? file)
|
||||
{
|
||||
if (file?.TryGetLocalPath() is not { } path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
try
|
||||
{
|
||||
session.OpenDatabase(path);
|
||||
Status($"log database {Path.GetFileName(path)}");
|
||||
}
|
||||
catch (Exception error) when (error is IOException or InvalidOperationException)
|
||||
{
|
||||
Status($"could not open {path}: {error.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnNewContest(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!HasDatabase())
|
||||
{
|
||||
return;
|
||||
}
|
||||
ContestSetupDialog dialog = new(session);
|
||||
ContestInstance? chosen = await dialog.ShowDialog<ContestInstance?>(this);
|
||||
if (chosen is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
session.StartContest(chosen);
|
||||
Status($"{chosen.ContestName} started");
|
||||
}
|
||||
|
||||
private async void OnOpenContest(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!HasDatabase())
|
||||
{
|
||||
return;
|
||||
}
|
||||
ContestPickerDialog dialog = new(session.Store.Contests());
|
||||
int? chosen = await dialog.ShowDialog<int?>(this);
|
||||
if (chosen is not null)
|
||||
{
|
||||
session.OpenContest(chosen.Value);
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnExportCabrillo(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Logging is null)
|
||||
{
|
||||
Status("no contest is open");
|
||||
return;
|
||||
}
|
||||
IStorageFile? file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Export Cabrillo",
|
||||
SuggestedFileName = $"{session.Settings.Station.Callsign}.log",
|
||||
});
|
||||
if (file?.TryGetLocalPath() is not { } path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
CabrilloWriter writer = new(Logging.Contest, Logging.Me);
|
||||
CabrilloHeader header = new()
|
||||
{
|
||||
Contest = Logging.Contest.CabrilloName,
|
||||
Callsign = Logging.Me.Callsign,
|
||||
OperatorCategory = Logging.Instance.OperatorCategory,
|
||||
AssistedCategory = Logging.Instance.AssistedCategory,
|
||||
BandCategory = Logging.Instance.BandCategory,
|
||||
ModeCategory = Logging.Instance.ModeCategory,
|
||||
PowerCategory = Logging.Instance.PowerCategory,
|
||||
StationCategory = Logging.Instance.StationCategory,
|
||||
TransmitterCategory = Logging.Instance.TransmitterCategory,
|
||||
OverlayCategory = Logging.Instance.OverlayCategory,
|
||||
TimeCategory = Logging.Instance.TimeCategory,
|
||||
ClaimedScore = Logging.Log.TotalScore,
|
||||
Club = Logging.Me.Club,
|
||||
Name = Logging.Me.Name,
|
||||
Operators = Logging.Instance.Operators,
|
||||
Soapbox = Logging.Instance.Soapbox,
|
||||
};
|
||||
await File.WriteAllTextAsync(path, writer.Write(header, Logging.Log.Qsos));
|
||||
Status($"{Logging.Log.Qsos.Count} contacts written to {Path.GetFileName(path)}");
|
||||
}
|
||||
|
||||
private async void OnExportAdif(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Logging is null)
|
||||
{
|
||||
Status("no contest is open");
|
||||
return;
|
||||
}
|
||||
IStorageFile? file = await StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
|
||||
{
|
||||
Title = "Export ADIF",
|
||||
SuggestedFileName = $"{session.Settings.Station.Callsign}.adi",
|
||||
});
|
||||
if (file?.TryGetLocalPath() is not { } path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
await File.WriteAllTextAsync(path, new AdifWriter(Logging.Me).Write(Logging.Log.Qsos));
|
||||
Status($"{Logging.Log.Qsos.Count} contacts written to {Path.GetFileName(path)}");
|
||||
}
|
||||
|
||||
private async void OnImportAdif(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (Logging is null)
|
||||
{
|
||||
Status("no contest is open");
|
||||
return;
|
||||
}
|
||||
IReadOnlyList<IStorageFile> files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Import ADIF",
|
||||
AllowMultiple = false,
|
||||
});
|
||||
if (files.FirstOrDefault()?.TryGetLocalPath() is not { } path)
|
||||
{
|
||||
return;
|
||||
}
|
||||
IReadOnlyList<Qso> read = AdifReader.Read(
|
||||
await File.ReadAllTextAsync(path),
|
||||
Logging.Contest.Name,
|
||||
Logging.Instance.ContestNumber);
|
||||
foreach (Qso qso in read)
|
||||
{
|
||||
session.Store.Add(qso);
|
||||
}
|
||||
session.OpenContest(Logging.Instance.ContestNumber);
|
||||
Status($"{read.Count} contacts read from {Path.GetFileName(path)}");
|
||||
}
|
||||
|
||||
private void OnExit(object? sender, RoutedEventArgs e) => Close();
|
||||
|
||||
private void OnShowLog(object? sender, RoutedEventArgs e) => Show(() => new LogWindow(session));
|
||||
|
||||
private void OnShowCheck(object? sender, RoutedEventArgs e) => Show(() => new CheckWindow(session, () => Logging?.Entry.Call ?? ""));
|
||||
|
||||
private void OnShowBandmap(object? sender, RoutedEventArgs e) => Show(() => new BandmapWindow(session, Tune));
|
||||
|
||||
private void OnShowScore(object? sender, RoutedEventArgs e) => Show(() => new ScoreWindow(session));
|
||||
|
||||
private void OnShowPacket(object? sender, RoutedEventArgs e) => Show(() => new PacketWindow(session));
|
||||
|
||||
private async void OnStationSettings(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
StationDialog dialog = new(session.Settings.Station);
|
||||
StoredStation? updated = await dialog.ShowDialog<StoredStation?>(this);
|
||||
if (updated is not null)
|
||||
{
|
||||
session.Save(session.Settings with { Station = updated });
|
||||
if (Logging is not null)
|
||||
{
|
||||
session.OpenContest(Logging.Instance.ContestNumber);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnRadioSettings(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
RadioDialog dialog = new(session.Settings);
|
||||
Settings? updated = await dialog.ShowDialog<Settings?>(this);
|
||||
if (updated is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
session.Save(updated);
|
||||
if (updated.RadioEnabled)
|
||||
{
|
||||
session.ConnectRadio();
|
||||
Status($"radio: rigctld at {updated.RigctldHost}:{updated.RigctldPort}");
|
||||
}
|
||||
else
|
||||
{
|
||||
session.DisconnectRadio();
|
||||
Status("radio disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnClusterSettings(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
ClusterDialog dialog = new(session.Settings);
|
||||
Settings? updated = await dialog.ShowDialog<Settings?>(this);
|
||||
if (updated is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
session.Save(updated);
|
||||
if (updated.ClusterEnabled)
|
||||
{
|
||||
session.ConnectCluster();
|
||||
Status($"cluster: {updated.ClusterHost}:{updated.ClusterPort}");
|
||||
}
|
||||
else
|
||||
{
|
||||
session.DisconnectCluster();
|
||||
Status("cluster disconnected");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnNetworkSettings(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
NetworkDialog dialog = new(session.Settings);
|
||||
Settings? updated = await dialog.ShowDialog<Settings?>(this);
|
||||
if (updated is not null)
|
||||
{
|
||||
session.Save(updated);
|
||||
session.ApplyNetworkSettings();
|
||||
Status(updated.NetworkEnabled ? "networked with the other stations" : "networking off");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnDownloadCountryFile(object? sender, RoutedEventArgs e) =>
|
||||
await Download(
|
||||
downloader => downloader.DownloadCountryFileAsync(session.Paths.CountryFile),
|
||||
"country file");
|
||||
|
||||
private async void OnDownloadCallDatabase(object? sender, RoutedEventArgs e) =>
|
||||
await Download(
|
||||
downloader => downloader.DownloadCallDatabaseAsync(session.Paths.CallDatabaseFile),
|
||||
"callsign database");
|
||||
|
||||
private void OnReloadSupportFiles(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
session.ReloadSupportFiles();
|
||||
Status($"country file {(session.Countries is null ? "missing" : "loaded")}, " +
|
||||
$"{session.Calls.Count} callsigns");
|
||||
}
|
||||
|
||||
private async Task Download(Func<SupportFileDownloader, Task<string>> fetch, string what)
|
||||
{
|
||||
Status($"fetching the {what}…");
|
||||
try
|
||||
{
|
||||
string result = await fetch(new SupportFileDownloader(Http));
|
||||
session.ReloadSupportFiles();
|
||||
Status($"{what}: {result}");
|
||||
}
|
||||
catch (Exception error) when (error is InvalidOperationException or IOException)
|
||||
{
|
||||
Status(error.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void Tune(Frequency frequency, string call)
|
||||
{
|
||||
if (Logging is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Logging.Tune(frequency);
|
||||
_ = session.Radio?.TuneAsync(frequency);
|
||||
Logging.Entry.Call = call;
|
||||
SyncBoxes();
|
||||
boxes[0].Focus();
|
||||
Refresh();
|
||||
}
|
||||
|
||||
private bool HasDatabase()
|
||||
{
|
||||
if (session.Settings.DatabasePath.Length > 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
Status("open a log database first: File ▸ New Database");
|
||||
return false;
|
||||
}
|
||||
|
||||
private void Show<T>(Func<T> create) where T : Window
|
||||
{
|
||||
if (openWindows.TryGetValue(typeof(T), out Window? existing))
|
||||
{
|
||||
existing.Activate();
|
||||
return;
|
||||
}
|
||||
T window = create();
|
||||
openWindows[typeof(T)] = window;
|
||||
window.Closed += (_, _) => openWindows.Remove(typeof(T));
|
||||
window.Show(this);
|
||||
}
|
||||
|
||||
private async Task<IStorageFolder?> Folder(string path) =>
|
||||
await StorageProvider.TryGetFolderFromPathAsync(path);
|
||||
}
|
||||
Reference in New Issue
Block a user