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 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(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(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 files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions { Title = "Import ADIF", AllowMultiple = false, }); if (files.FirstOrDefault()?.TryGetLocalPath() is not { } path) { return; } IReadOnlyList 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(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(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(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(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> 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(Func 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 Folder(string path) => await StorageProvider.TryGetFolderFromPathAsync(path); }