mirror of
https://github.com/9ParsonsB/Pulsar.git
synced 2025-04-05 17:39:39 -04:00
Export version fixes (#83)
* Add file association for .eop, prompt for install dir * Handle .eop or .aip file passed as arg. * VS2022 version bump * Filter neutron stars and black holes from fast spinning criteria. * Adjustments for new "high value" check * Refactor herald cache * Fix element order and namespaces for voice moods. * Add explicit .Stop() between audio player calls. * Use nullsafe member access instead of skipping * Don't queue up a title that's already queued. * Improve body ordinal handling for explorer speech titles. * Escape strings being inserted into xml * Handle flip-flopping JSON type * Converter for flip-flopping property type * Use the converter * Escape characters *before* we wrap it in xml. * Give Eahlstan his clear button. :D * Exclude all stars from fast rotation check. * Close outstanding popup notifications on exit. * TO DONE * [Herald] Suppress duplicate notification titles for spoken notifications If you have notifications from multiple plugins producing notifications with the same title in quick succession (ie. "Body A 1 e" from both Explorer and BioInsights), the title on successive notifications will not be spoken again to save the breath of our friendly Azure speakers. * Doc update * Remove unintended member hiding * Fix export errors when exporting BioInsights data, cleanup Discovered a couple issues with exporting BioInsights data resulting from using two different types of objects in the data grid; improved error handling as well. Also cleaned up some old-style read all code. * Add read-all on launch setting * Updated framework xml * Improve high-value body description text Co-authored-by: Fred Kuipers <mr.fredk@gmail.com>
This commit is contained in:
parent
921f3867fa
commit
8de34a141c
@ -19,6 +19,7 @@ AppPublisherURL={#MyAppURL}
|
|||||||
AppSupportURL={#MyAppURL}
|
AppSupportURL={#MyAppURL}
|
||||||
AppUpdatesURL={#MyAppURL}
|
AppUpdatesURL={#MyAppURL}
|
||||||
DefaultDirName={autopf}\{#MyAppName}
|
DefaultDirName={autopf}\{#MyAppName}
|
||||||
|
DisableDirPage=false
|
||||||
DefaultGroupName={#MyAppName}
|
DefaultGroupName={#MyAppName}
|
||||||
AllowNoIcons=yes
|
AllowNoIcons=yes
|
||||||
LicenseFile=C:\Users\Xjph\Source\Repos\MIT.txt
|
LicenseFile=C:\Users\Xjph\Source\Repos\MIT.txt
|
||||||
@ -32,6 +33,7 @@ Compression=lzma
|
|||||||
SolidCompression=yes
|
SolidCompression=yes
|
||||||
WizardStyle=modern
|
WizardStyle=modern
|
||||||
ArchitecturesInstallIn64BitMode=x64
|
ArchitecturesInstallIn64BitMode=x64
|
||||||
|
ChangesAssociations=yes
|
||||||
|
|
||||||
[Languages]
|
[Languages]
|
||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
@ -77,6 +79,13 @@ Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: de
|
|||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||||
|
|
||||||
|
[Registry]
|
||||||
|
Root: HKA; Subkey: "Software\Classes\.eop\OpenWithProgids"; ValueType: string; ValueName: "ObservatoryPlugin.eop"; ValueData: ""; Flags: uninsdeletevalue
|
||||||
|
Root: HKA; Subkey: "Software\Classes\ObservatoryPlugin.eop"; ValueType: string; ValueName: ""; ValueData: "Elite Observatory Plugin"; Flags: uninsdeletekey
|
||||||
|
Root: HKA; Subkey: "Software\Classes\ObservatoryPlugin.eop\DefaultIcon"; ValueType: string; ValueName: ""; ValueData: "{app}\{#MyAppExeName},0"
|
||||||
|
Root: HKA; Subkey: "Software\Classes\ObservatoryPlugin.eop\shell\open\command"; ValueType: string; ValueName: ""; ValueData: """{app}\{#MyAppExeName}"" ""%1"""
|
||||||
|
Root: HKA; Subkey: "Software\Classes\Applications\{#MyAppExeName}\SupportedTypes"; ValueType: string; ValueName: ".eop"; ValueData: ""
|
||||||
|
|
||||||
[Code]
|
[Code]
|
||||||
|
|
||||||
type
|
type
|
||||||
|
@ -8,7 +8,6 @@ namespace Observatory.NativeNotification
|
|||||||
{
|
{
|
||||||
public class NativePopup
|
public class NativePopup
|
||||||
{
|
{
|
||||||
// TODO: This needs to be cleaned up when the app is closed.
|
|
||||||
private Dictionary<Guid, NotificationView> notifications;
|
private Dictionary<Guid, NotificationView> notifications;
|
||||||
|
|
||||||
public NativePopup()
|
public NativePopup()
|
||||||
@ -67,5 +66,13 @@ namespace Observatory.NativeNotification
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void CloseAll()
|
||||||
|
{
|
||||||
|
foreach (var notification in notifications)
|
||||||
|
{
|
||||||
|
notification.Value?.Close();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,15 @@ namespace Observatory
|
|||||||
[STAThread]
|
[STAThread]
|
||||||
static void Main(string[] args)
|
static void Main(string[] args)
|
||||||
{
|
{
|
||||||
|
if (args.Length > 0 && System.IO.File.Exists(args[0]))
|
||||||
|
{
|
||||||
|
var fileInfo = new System.IO.FileInfo(args[0]);
|
||||||
|
if (fileInfo.Extension == ".eop" || fileInfo.Extension == ".zip")
|
||||||
|
System.IO.File.Copy(
|
||||||
|
fileInfo.FullName,
|
||||||
|
$"{AppDomain.CurrentDomain.BaseDirectory}{System.IO.Path.DirectorySeparatorChar}plugins{System.IO.Path.DirectorySeparatorChar}{fileInfo.Name}");
|
||||||
|
}
|
||||||
|
|
||||||
string version = System.Reflection.Assembly.GetEntryAssembly().GetName().Version.ToString();
|
string version = System.Reflection.Assembly.GetEntryAssembly().GetName().Version.ToString();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
@ -170,5 +170,10 @@ namespace Observatory.PluginManagement
|
|||||||
return folderLocation;
|
return folderLocation;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void Shutdown()
|
||||||
|
{
|
||||||
|
NativePopup.CloseAll();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ namespace Observatory.PluginManagement
|
|||||||
public readonly List<DataTable> pluginTables;
|
public readonly List<DataTable> pluginTables;
|
||||||
public readonly List<(IObservatoryWorker plugin, PluginStatus signed)> workerPlugins;
|
public readonly List<(IObservatoryWorker plugin, PluginStatus signed)> workerPlugins;
|
||||||
public readonly List<(IObservatoryNotifier plugin, PluginStatus signed)> notifyPlugins;
|
public readonly List<(IObservatoryNotifier plugin, PluginStatus signed)> notifyPlugins;
|
||||||
|
private readonly PluginCore core;
|
||||||
|
|
||||||
private PluginManager()
|
private PluginManager()
|
||||||
{
|
{
|
||||||
@ -48,7 +49,7 @@ namespace Observatory.PluginManagement
|
|||||||
logMonitor.StatusUpdate += pluginHandler.OnStatusUpdate;
|
logMonitor.StatusUpdate += pluginHandler.OnStatusUpdate;
|
||||||
logMonitor.LogMonitorStateChanged += pluginHandler.OnLogMonitorStateChanged;
|
logMonitor.LogMonitorStateChanged += pluginHandler.OnLogMonitorStateChanged;
|
||||||
|
|
||||||
var core = new PluginCore();
|
core = new PluginCore();
|
||||||
|
|
||||||
List<IObservatoryPlugin> errorPlugins = new();
|
List<IObservatoryPlugin> errorPlugins = new();
|
||||||
|
|
||||||
@ -346,6 +347,11 @@ namespace Observatory.PluginManagement
|
|||||||
return err;
|
return err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void Shutdown()
|
||||||
|
{
|
||||||
|
core.Shutdown();
|
||||||
|
}
|
||||||
|
|
||||||
private static void LoadPlaceholderPlugin(string dllPath, PluginStatus pluginStatus, List<(IObservatoryNotifier plugin, PluginStatus signed)> notifiers)
|
private static void LoadPlaceholderPlugin(string dllPath, PluginStatus pluginStatus, List<(IObservatoryNotifier plugin, PluginStatus signed)> notifiers)
|
||||||
{
|
{
|
||||||
PlaceholderPlugin placeholder = new(new FileInfo(dllPath).Name);
|
PlaceholderPlugin placeholder = new(new FileInfo(dllPath).Name);
|
||||||
|
12
ObservatoryCore/Properties/Core.Designer.cs
generated
12
ObservatoryCore/Properties/Core.Designer.cs
generated
@ -262,5 +262,17 @@ namespace Observatory.Properties {
|
|||||||
this["ExportFolder"] = value;
|
this["ExportFolder"] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[global::System.Configuration.UserScopedSettingAttribute()]
|
||||||
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
|
[global::System.Configuration.DefaultSettingValueAttribute("False")]
|
||||||
|
public bool StartReadAll {
|
||||||
|
get {
|
||||||
|
return ((bool)(this["StartReadAll"]));
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
this["StartReadAll"] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -62,5 +62,8 @@
|
|||||||
<Setting Name="ExportFolder" Type="System.String" Scope="User">
|
<Setting Name="ExportFolder" Type="System.String" Scope="User">
|
||||||
<Value Profile="(Default)" />
|
<Value Profile="(Default)" />
|
||||||
</Setting>
|
</Setting>
|
||||||
|
<Setting Name="StartReadAll" Type="System.Boolean" Scope="User">
|
||||||
|
<Value Profile="(Default)">False</Value>
|
||||||
|
</Setting>
|
||||||
</Settings>
|
</Settings>
|
||||||
</SettingsFile>
|
</SettingsFile>
|
@ -21,6 +21,11 @@ namespace Observatory.UI
|
|||||||
{
|
{
|
||||||
DataContext = new MainWindowViewModel(pluginManager)
|
DataContext = new MainWindowViewModel(pluginManager)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
desktop.MainWindow.Closing += (o, e) =>
|
||||||
|
{
|
||||||
|
pluginManager.Shutdown();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
@ -1,10 +1,4 @@
|
|||||||
using System;
|
namespace Observatory.UI.Models
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Observatory.UI.Models
|
|
||||||
{
|
{
|
||||||
public class NotificationModel
|
public class NotificationModel
|
||||||
{
|
{
|
||||||
|
@ -58,19 +58,16 @@ namespace Observatory.UI.ViewModels
|
|||||||
tabs.Add(new CoreModel() { Name = "Core", UI = new BasicUIViewModel(new ObservableCollection<object>()) { UIType = Framework.PluginUI.UIType.Core } });
|
tabs.Add(new CoreModel() { Name = "Core", UI = new BasicUIViewModel(new ObservableCollection<object>()) { UIType = Framework.PluginUI.UIType.Core } });
|
||||||
|
|
||||||
if (Properties.Core.Default.StartMonitor)
|
if (Properties.Core.Default.StartMonitor)
|
||||||
{
|
|
||||||
ToggleMonitor();
|
ToggleMonitor();
|
||||||
}
|
|
||||||
|
if (Properties.Core.Default.StartReadAll)
|
||||||
|
ReadAll();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ReadAll()
|
public void ReadAll()
|
||||||
{
|
{
|
||||||
// TODO(fredjk_gh): remove.
|
|
||||||
SetWorkerReadAllState(true);
|
|
||||||
LogMonitor.GetInstance.ReadAllJournals();
|
LogMonitor.GetInstance.ReadAllJournals();
|
||||||
// TODO(fredjk_gh): remove.
|
|
||||||
SetWorkerReadAllState(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ToggleMonitor()
|
public void ToggleMonitor()
|
||||||
@ -84,12 +81,7 @@ namespace Observatory.UI.ViewModels
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
// HACK: Find a better way of suppressing notifications when pre-reading.
|
|
||||||
// TODO(fredjk_gh): remove.
|
|
||||||
SetWorkerReadAllState(true);
|
|
||||||
logMonitor.Start();
|
logMonitor.Start();
|
||||||
// TODO(fredjk_gh): remove.
|
|
||||||
SetWorkerReadAllState(false);
|
|
||||||
ToggleButtonText = "Stop Monitor";
|
ToggleButtonText = "Stop Monitor";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -117,100 +109,129 @@ namespace Observatory.UI.ViewModels
|
|||||||
|
|
||||||
public async void ExportGrid()
|
public async void ExportGrid()
|
||||||
{
|
{
|
||||||
var exportFolder = Properties.Core.Default.ExportFolder;
|
try
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(exportFolder))
|
|
||||||
{
|
{
|
||||||
exportFolder = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
var exportFolder = Properties.Core.Default.ExportFolder;
|
||||||
}
|
|
||||||
|
|
||||||
OpenFolderDialog openFolderDialog = new()
|
if (string.IsNullOrEmpty(exportFolder))
|
||||||
{
|
|
||||||
Directory = exportFolder
|
|
||||||
};
|
|
||||||
|
|
||||||
var application = (IClassicDesktopStyleApplicationLifetime)Avalonia.Application.Current.ApplicationLifetime;
|
|
||||||
|
|
||||||
var selectedFolder = await openFolderDialog.ShowAsync(application.MainWindow);
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(selectedFolder))
|
|
||||||
{
|
|
||||||
Properties.Core.Default.ExportFolder = selectedFolder;
|
|
||||||
Properties.Core.Default.Save();
|
|
||||||
exportFolder = selectedFolder;
|
|
||||||
|
|
||||||
foreach (var tab in tabs.Where(t => t.Name != "Core"))
|
|
||||||
{
|
{
|
||||||
var ui = (BasicUIViewModel)tab.UI;
|
exportFolder = System.Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||||
List<object> selectedData;
|
}
|
||||||
bool specificallySelected = ui.SelectedItems?.Count > 1;
|
|
||||||
|
|
||||||
if (specificallySelected)
|
OpenFolderDialog openFolderDialog = new()
|
||||||
|
{
|
||||||
|
Directory = exportFolder
|
||||||
|
};
|
||||||
|
|
||||||
|
var application = (IClassicDesktopStyleApplicationLifetime)Avalonia.Application.Current.ApplicationLifetime;
|
||||||
|
|
||||||
|
var selectedFolder = await openFolderDialog.ShowAsync(application.MainWindow);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(selectedFolder))
|
||||||
|
{
|
||||||
|
Properties.Core.Default.ExportFolder = selectedFolder;
|
||||||
|
Properties.Core.Default.Save();
|
||||||
|
exportFolder = selectedFolder;
|
||||||
|
|
||||||
|
foreach (var tab in tabs.Where(t => t.Name != "Core"))
|
||||||
{
|
{
|
||||||
selectedData = new();
|
var ui = (BasicUIViewModel)tab.UI;
|
||||||
|
List<object> selectedData;
|
||||||
|
bool specificallySelected = ui.SelectedItems?.Count > 1;
|
||||||
|
|
||||||
foreach (var item in ui.SelectedItems)
|
if (specificallySelected)
|
||||||
selectedData.Add(item);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
selectedData = ui.BasicUIGrid.ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
var columns = selectedData[0].GetType().GetProperties();
|
|
||||||
Dictionary<string, int> colSize = new();
|
|
||||||
Dictionary<string, List<string>> colContent = new();
|
|
||||||
|
|
||||||
foreach (var column in columns)
|
|
||||||
{
|
|
||||||
colSize.Add(column.Name, 0);
|
|
||||||
colContent.Add(column.Name, new());
|
|
||||||
}
|
|
||||||
|
|
||||||
var lineType = selectedData[0].GetType();
|
|
||||||
|
|
||||||
foreach (var line in selectedData)
|
|
||||||
{
|
|
||||||
foreach (var column in colContent)
|
|
||||||
{
|
{
|
||||||
var cellValue = lineType.GetProperty(column.Key).GetValue(line)?.ToString() ?? string.Empty;
|
selectedData = new();
|
||||||
column.Value.Add(cellValue);
|
|
||||||
if (colSize[column.Key] < cellValue.Length)
|
foreach (var item in ui.SelectedItems)
|
||||||
colSize[column.Key] = cellValue.Length;
|
selectedData.Add(item);
|
||||||
}
|
}
|
||||||
}
|
else
|
||||||
|
|
||||||
System.Text.StringBuilder exportData = new();
|
|
||||||
|
|
||||||
|
|
||||||
foreach (var colTitle in colContent.Keys)
|
|
||||||
{
|
|
||||||
if (colSize[colTitle] < colTitle.Length)
|
|
||||||
colSize[colTitle] = colTitle.Length;
|
|
||||||
|
|
||||||
exportData.Append(colTitle.PadRight(colSize[colTitle]) + " ");
|
|
||||||
}
|
|
||||||
exportData.AppendLine();
|
|
||||||
|
|
||||||
for (int i = 0; i < colContent.First().Value.Count; i++)
|
|
||||||
{
|
|
||||||
foreach(var column in colContent)
|
|
||||||
{
|
{
|
||||||
if (column.Value[i].Length > 0 && !char.IsNumber(column.Value[i][0]) && column.Value[i].Count(char.IsLetter) / (float)column.Value[i].Length > 0.25)
|
selectedData = ui.BasicUIGrid.ToList();
|
||||||
exportData.Append(column.Value[i].PadRight(colSize[column.Key]) + " ");
|
}
|
||||||
else
|
|
||||||
exportData.Append(column.Value[i].PadLeft(colSize[column.Key]) + " ");
|
var columns = selectedData[0].GetType().GetProperties();
|
||||||
|
Dictionary<string, int> colSize = new();
|
||||||
|
Dictionary<string, List<string>> colContent = new();
|
||||||
|
|
||||||
|
foreach (var column in columns)
|
||||||
|
{
|
||||||
|
colSize.Add(column.Name, 0);
|
||||||
|
colContent.Add(column.Name, new());
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var line in selectedData)
|
||||||
|
{
|
||||||
|
var lineType = line.GetType(); // some plugins have different line types, so don't move this out of loop
|
||||||
|
foreach (var column in colContent)
|
||||||
|
{
|
||||||
|
var cellValue = lineType.GetProperty(column.Key)?.GetValue(line)?.ToString() ?? string.Empty;
|
||||||
|
column.Value.Add(cellValue);
|
||||||
|
if (colSize[column.Key] < cellValue.Length)
|
||||||
|
colSize[column.Key] = cellValue.Length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.Text.StringBuilder exportData = new();
|
||||||
|
|
||||||
|
|
||||||
|
foreach (var colTitle in colContent.Keys)
|
||||||
|
{
|
||||||
|
if (colSize[colTitle] < colTitle.Length)
|
||||||
|
colSize[colTitle] = colTitle.Length;
|
||||||
|
|
||||||
|
exportData.Append(colTitle.PadRight(colSize[colTitle]) + " ");
|
||||||
}
|
}
|
||||||
exportData.AppendLine();
|
exportData.AppendLine();
|
||||||
|
|
||||||
|
for (int i = 0; i < colContent.First().Value.Count; i++)
|
||||||
|
{
|
||||||
|
foreach (var column in colContent)
|
||||||
|
{
|
||||||
|
if (column.Value[i].Length > 0 && !char.IsNumber(column.Value[i][0]) && column.Value[i].Count(char.IsLetter) / (float)column.Value[i].Length > 0.25)
|
||||||
|
exportData.Append(column.Value[i].PadRight(colSize[column.Key]) + " ");
|
||||||
|
else
|
||||||
|
exportData.Append(column.Value[i].PadLeft(colSize[column.Key]) + " ");
|
||||||
|
}
|
||||||
|
exportData.AppendLine();
|
||||||
|
}
|
||||||
|
|
||||||
|
string exportPath = $"{exportFolder}{System.IO.Path.DirectorySeparatorChar}Observatory Export - {DateTime.UtcNow:yyyyMMdd-HHmmss} - {tab.Name}.txt";
|
||||||
|
|
||||||
|
System.IO.File.WriteAllText(exportPath, exportData.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
string exportPath = $"{exportFolder}{System.IO.Path.DirectorySeparatorChar}Observatory Export - {DateTime.UtcNow:yyyyMMdd-HHmmss} - {tab.Name}.txt";
|
|
||||||
|
|
||||||
System.IO.File.WriteAllText(exportPath, exportData.ToString());
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
ObservatoryCore.LogError(e, "while exporting data");
|
||||||
|
ErrorReporter.ShowErrorPopup("Error encountered!",
|
||||||
|
"An error occurred while exporting; output may be missing or incomplete." + Environment.NewLine +
|
||||||
|
"Please check the error log (found in your Documents folder) for more details and visit our discord to report it.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearGrid()
|
||||||
|
{
|
||||||
|
foreach (var tab in tabs.Where(t => t.Name != "Core"))
|
||||||
|
{
|
||||||
|
var ui = (BasicUIViewModel)tab.UI;
|
||||||
|
|
||||||
|
var rowTemplate = ui.BasicUIGrid.First();
|
||||||
|
|
||||||
|
foreach (var property in rowTemplate.GetType().GetProperties())
|
||||||
|
{
|
||||||
|
property.SetValue(rowTemplate, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.BasicUIGrid.Clear();
|
||||||
|
ui.BasicUIGrid.Add(rowTemplate);
|
||||||
|
|
||||||
|
// For some reason UIType's change event will properly
|
||||||
|
// redraw the grid, not BasicUIGrid's.
|
||||||
|
ui.RaisePropertyChanged(nameof(ui.UIType));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ToggleButtonText
|
public string ToggleButtonText
|
||||||
@ -241,22 +262,6 @@ namespace Observatory.UI.ViewModels
|
|||||||
get { return tabs; }
|
get { return tabs; }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(fredjk_gh): remove.
|
|
||||||
private void SetWorkerReadAllState(bool isReadingAll)
|
|
||||||
{
|
|
||||||
foreach (var worker in workers)
|
|
||||||
{
|
|
||||||
if (isReadingAll)
|
|
||||||
{
|
|
||||||
worker.ReadAllStarted();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
worker.ReadAllFinished();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool CheckUpdate()
|
private static bool CheckUpdate()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
@ -1,9 +1,4 @@
|
|||||||
using System;
|
using Observatory.Framework;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Observatory.Framework;
|
|
||||||
|
|
||||||
namespace Observatory.UI.ViewModels
|
namespace Observatory.UI.ViewModels
|
||||||
{
|
{
|
||||||
|
@ -27,6 +27,11 @@ namespace Observatory.UI.Views
|
|||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
nativePopup = new();
|
nativePopup = new();
|
||||||
|
|
||||||
|
this.DetachedFromVisualTree += (o, e) =>
|
||||||
|
{
|
||||||
|
nativePopup.CloseAll();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeComponent()
|
private void InitializeComponent()
|
||||||
@ -666,7 +671,7 @@ namespace Observatory.UI.Views
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Monitor On Launch
|
#region Actions On Launch
|
||||||
|
|
||||||
TextBlock startMonitorLabel = new() { Text = "Start monitor on Observatory launch" };
|
TextBlock startMonitorLabel = new() { Text = "Start monitor on Observatory launch" };
|
||||||
CheckBox startMonitorCheckbox = new() { IsChecked = Properties.Core.Default.StartMonitor, Content = startMonitorLabel };
|
CheckBox startMonitorCheckbox = new() { IsChecked = Properties.Core.Default.StartMonitor, Content = startMonitorLabel };
|
||||||
@ -683,6 +688,21 @@ namespace Observatory.UI.Views
|
|||||||
Properties.Core.Default.Save();
|
Properties.Core.Default.Save();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
TextBlock startReadAllLabel = new() { Text = "Read All on Observatory launch" };
|
||||||
|
CheckBox startReadAllCheckbox = new() { IsChecked = Properties.Core.Default.StartReadAll, Content = startReadAllLabel };
|
||||||
|
|
||||||
|
startReadAllCheckbox.Checked += (object sender, RoutedEventArgs e) =>
|
||||||
|
{
|
||||||
|
Properties.Core.Default.StartReadAll = true;
|
||||||
|
Properties.Core.Default.Save();
|
||||||
|
};
|
||||||
|
|
||||||
|
startReadAllCheckbox.Unchecked += (object sender, RoutedEventArgs e) =>
|
||||||
|
{
|
||||||
|
Properties.Core.Default.StartReadAll = false;
|
||||||
|
Properties.Core.Default.Save();
|
||||||
|
};
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@ -753,6 +773,7 @@ namespace Observatory.UI.Views
|
|||||||
|
|
||||||
gridManager.AddSetting(primeSystemContexCheckbox);
|
gridManager.AddSetting(primeSystemContexCheckbox);
|
||||||
gridManager.AddSetting(startMonitorCheckbox);
|
gridManager.AddSetting(startMonitorCheckbox);
|
||||||
|
gridManager.AddSetting(startReadAllCheckbox);
|
||||||
gridManager.AddSettingWithLabel(journalPathLabel, journalPath);
|
gridManager.AddSettingWithLabel(journalPathLabel, journalPath);
|
||||||
gridManager.AddSetting(journalBrowse);
|
gridManager.AddSetting(journalBrowse);
|
||||||
|
|
||||||
|
@ -81,6 +81,14 @@
|
|||||||
Content="Export">
|
Content="Export">
|
||||||
Export
|
Export
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
Name="clear"
|
||||||
|
Margin="10"
|
||||||
|
FontSize="15"
|
||||||
|
Command="{Binding ClearGrid}"
|
||||||
|
Content="Clear">
|
||||||
|
Clear
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
Name="ToggleMonitor"
|
Name="ToggleMonitor"
|
||||||
Margin="10"
|
Margin="10"
|
||||||
|
@ -9,18 +9,6 @@ namespace Observatory.Explorer
|
|||||||
{
|
{
|
||||||
internal static class DefaultCriteria
|
internal static class DefaultCriteria
|
||||||
{
|
{
|
||||||
private static IList<string> HighValueNonTerraformablePlanetClasses = new string[] {
|
|
||||||
"Earthlike body",
|
|
||||||
"Ammonia world",
|
|
||||||
"Water world",
|
|
||||||
};
|
|
||||||
|
|
||||||
private static IList<string> HighValueTerraformablePlanetClasses = new string[] {
|
|
||||||
"Water world",
|
|
||||||
"High metal content body",
|
|
||||||
"Rocky body",
|
|
||||||
};
|
|
||||||
|
|
||||||
public static List<(string Description, string Detail, bool SystemWide)> CheckInterest(Scan scan, Dictionary<ulong, Dictionary<int, Scan>> scanHistory, Dictionary<ulong, Dictionary<int, FSSBodySignals>> signalHistory, ExplorerSettings settings)
|
public static List<(string Description, string Detail, bool SystemWide)> CheckInterest(Scan scan, Dictionary<ulong, Dictionary<int, Scan>> scanHistory, Dictionary<ulong, Dictionary<int, FSSBodySignals>> signalHistory, ExplorerSettings settings)
|
||||||
{
|
{
|
||||||
List<(string, string, bool)> results = new();
|
List<(string, string, bool)> results = new();
|
||||||
@ -61,13 +49,25 @@ namespace Observatory.Explorer
|
|||||||
#region Value Checks
|
#region Value Checks
|
||||||
if (settings.HighValueMappable)
|
if (settings.HighValueMappable)
|
||||||
{
|
{
|
||||||
if (HighValueTerraformablePlanetClasses.Contains(scan.PlanetClass) && scan.TerraformState?.Length > 0)
|
IList<string> HighValueNonTerraformablePlanetClasses = new string[] {
|
||||||
|
"Earthlike body",
|
||||||
|
"Ammonia world",
|
||||||
|
"Water world",
|
||||||
|
};
|
||||||
|
|
||||||
|
if (HighValueNonTerraformablePlanetClasses.Contains(scan.PlanetClass) || scan.TerraformState?.Length > 0)
|
||||||
{
|
{
|
||||||
results.Add("High-Value Mapping", $"{scan.DistanceFromArrivalLS:0}Ls, {scan.PlanetClass} (TF)");
|
var info = new System.Text.StringBuilder();
|
||||||
}
|
|
||||||
if (HighValueNonTerraformablePlanetClasses.Contains(scan.PlanetClass) && scan.TerraformState?.Length == 0)
|
if (!scan.WasDiscovered)
|
||||||
{
|
info.Append("Undiscovered ");
|
||||||
results.Add("High-Value Mapping", $"{scan.DistanceFromArrivalLS:0}Ls, {scan.PlanetClass}");
|
else if (!scan.WasMapped)
|
||||||
|
info.Append("Unmapped ");
|
||||||
|
|
||||||
|
if (scan.TerraformState?.Length > 0)
|
||||||
|
info.Append("Terraformable ");
|
||||||
|
|
||||||
|
results.Add("High-Value Body", $"{(info.Length > 1 ? info.ToString() : string.Empty)}{textInfo.ToTitleCase(scan.PlanetClass)}, {scan.DistanceFromArrivalLS:N0}Ls");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
@ -143,7 +143,7 @@ namespace Observatory.Explorer
|
|||||||
results.Add("Nested Moon");
|
results.Add("Nested Moon");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (settings.FastRotation && scan.RotationPeriod != 0 && !scan.TidalLock && Math.Abs(scan.RotationPeriod) < 28800 && !isRing)
|
if (settings.FastRotation && scan.RotationPeriod != 0 && !scan.TidalLock && Math.Abs(scan.RotationPeriod) < 28800 && !isRing && string.IsNullOrEmpty(scan.StarType))
|
||||||
{
|
{
|
||||||
results.Add("Non-locked Body with Fast Rotation", $"Period: {scan.RotationPeriod/3600:N1} hours");
|
results.Add("Non-locked Body with Fast Rotation", $"Period: {scan.RotationPeriod/3600:N1} hours");
|
||||||
}
|
}
|
||||||
|
@ -212,7 +212,7 @@ namespace Observatory.Explorer
|
|||||||
bodyAffix = string.Empty;
|
bodyAffix = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
string bodyLabel = scanEvent.PlanetClass == "Barycentre" ? "Barycentre" : "Body";
|
string bodyLabel = System.Security.SecurityElement.Escape(scanEvent.PlanetClass == "Barycentre" ? "Barycentre" : "Body");
|
||||||
|
|
||||||
string spokenAffix;
|
string spokenAffix;
|
||||||
|
|
||||||
@ -222,13 +222,13 @@ namespace Observatory.Explorer
|
|||||||
{
|
{
|
||||||
int ringIndex = bodyAffix.Length - 6;
|
int ringIndex = bodyAffix.Length - 6;
|
||||||
spokenAffix =
|
spokenAffix =
|
||||||
"<say-as interpret-as=\"spell-out\">" + bodyAffix.Substring(0, ringIndex) +
|
"<say-as interpret-as=\"spell-out\">" + bodyAffix[..ringIndex]
|
||||||
"</say-as><break strength=\"weak\"/><say-as interpret-as=\"spell-out\">" +
|
+ "</say-as><break strength=\"weak\"/>" + SplitOrdinalForSsml(bodyAffix.Substring(ringIndex, 1))
|
||||||
bodyAffix.Substring(ringIndex, 1) + "</say-as>" + bodyAffix.Substring(ringIndex + 1, bodyAffix.Length - (ringIndex + 1));
|
+ bodyAffix[(ringIndex + 1)..];
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
spokenAffix = "<say-as interpret-as=\"spell-out\">" + bodyAffix + "</say-as>";
|
spokenAffix = SplitOrdinalForSsml(bodyAffix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -248,5 +248,18 @@ namespace Observatory.Explorer
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string SplitOrdinalForSsml(string ordinalString)
|
||||||
|
{
|
||||||
|
var ordinalSegments = ordinalString.Split(' ');
|
||||||
|
StringBuilder affix = new();
|
||||||
|
foreach (var ordinalSegment in ordinalSegments)
|
||||||
|
{
|
||||||
|
if (ordinalSegment.All(Char.IsDigit))
|
||||||
|
affix.Append(" " + ordinalSegment);
|
||||||
|
else
|
||||||
|
affix.Append("<say-as interpret-as=\"spell-out\">" + ordinalSegment + "</say-as>");
|
||||||
|
}
|
||||||
|
return affix.ToString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -80,7 +80,7 @@ namespace Observatory.Explorer
|
|||||||
[SettingDisplayName("All Surface Mats In System")]
|
[SettingDisplayName("All Surface Mats In System")]
|
||||||
public bool GoldSystem { get; set; }
|
public bool GoldSystem { get; set; }
|
||||||
|
|
||||||
[SettingDisplayName("High-Value Mapping")]
|
[SettingDisplayName("High-Value Body")]
|
||||||
public bool HighValueMappable { get; set; }
|
public bool HighValueMappable { get; set; }
|
||||||
|
|
||||||
[SettingDisplayName("Enable Custom Criteria")]
|
[SettingDisplayName("Enable Custom Criteria")]
|
||||||
|
@ -55,29 +55,81 @@ namespace Observatory.Framework
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public double YPos = -1.0;
|
public double YPos = -1.0;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Specifies the desired renderings of the notification.
|
/// Specifies the desired renderings of the notification. Defaults to <see cref="NotificationRendering.All"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public NotificationRendering Rendering = NotificationRendering.All;
|
public NotificationRendering Rendering = NotificationRendering.All;
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies if some part of the notification should be suppressed. Not supported by all notifiers. Defaults to <see cref="NotificationSuppression.None"/>.
|
||||||
|
/// </summary>
|
||||||
|
public NotificationSuppression Suppression = NotificationSuppression.None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines constants for suppression of title or detail announcement in a notification.
|
||||||
|
/// </summary>
|
||||||
|
[Flags]
|
||||||
|
public enum NotificationSuppression
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// No suppression.
|
||||||
|
/// </summary>
|
||||||
|
None = 0,
|
||||||
|
/// <summary>
|
||||||
|
/// Suppress title.
|
||||||
|
/// </summary>
|
||||||
|
Title = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// Suppress detail.
|
||||||
|
/// </summary>
|
||||||
|
Detail = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines constants for controlling notification routing to plugins or native notification handlers.
|
||||||
|
/// </summary>
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum NotificationRendering
|
public enum NotificationRendering
|
||||||
{
|
{
|
||||||
// These need to be multiples of 2 as they're used via masking.
|
/// <summary>
|
||||||
|
/// Send notification to native visual popup notificaiton handler.
|
||||||
|
/// </summary>
|
||||||
NativeVisual = 1,
|
NativeVisual = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// Send notification to native speech notification handler.
|
||||||
|
/// </summary>
|
||||||
NativeVocal = 2,
|
NativeVocal = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// Send notification to all installed notifier plugins.
|
||||||
|
/// </summary>
|
||||||
PluginNotifier = 4,
|
PluginNotifier = 4,
|
||||||
|
/// <summary>
|
||||||
|
/// Send notification to all available handlers.
|
||||||
|
/// </summary>
|
||||||
All = (NativeVisual | NativeVocal | PluginNotifier)
|
All = (NativeVisual | NativeVocal | PluginNotifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flags indicating current state of journal monitoring.
|
||||||
|
/// </summary>
|
||||||
[Flags]
|
[Flags]
|
||||||
public enum LogMonitorState : uint
|
public enum LogMonitorState : uint
|
||||||
{
|
{
|
||||||
// These need to be multiples of 2 as they're used via masking.
|
/// <summary>
|
||||||
Idle = 0, // Monitoring is stopped
|
/// Monitoring is stopped.
|
||||||
Realtime = 1, // Real-time monitoring is active
|
/// </summary>
|
||||||
Batch = 2, // Performing a batch read of history
|
Idle = 0,
|
||||||
PreRead = 4 // Currently pre-reading current system context (a batch read)
|
/// <summary>
|
||||||
|
/// Real-time monitoring is active.
|
||||||
|
/// </summary>
|
||||||
|
Realtime = 1,
|
||||||
|
/// <summary>
|
||||||
|
/// Batch read of historical journals is in progress.
|
||||||
|
/// </summary>
|
||||||
|
Batch = 2,
|
||||||
|
/// <summary>
|
||||||
|
/// Batch read of recent journals is in progress to establish current player state.
|
||||||
|
/// </summary>
|
||||||
|
PreRead = 4
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Immutable;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
|
namespace Observatory.Framework.Files.Converters
|
||||||
|
{
|
||||||
|
class MutableStringDoubleConverter : JsonConverter<object>
|
||||||
|
{
|
||||||
|
public override object Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
if (reader.TokenType == JsonTokenType.String)
|
||||||
|
return reader.GetString();
|
||||||
|
else
|
||||||
|
return reader.GetDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -3,6 +3,5 @@
|
|||||||
public class CrewLaunchFighter : CrewMemberJoins
|
public class CrewLaunchFighter : CrewMemberJoins
|
||||||
{
|
{
|
||||||
public ulong ID { get; init; }
|
public ulong ID { get; init; }
|
||||||
public bool Telepresence { get; init; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
namespace Observatory.Framework.Files.Journal
|
namespace Observatory.Framework.Files.Journal
|
||||||
{
|
{
|
||||||
public class CrewMemberQuits : CrewMemberJoins
|
public class CrewMemberQuits : CrewMemberJoins
|
||||||
{
|
{ }
|
||||||
public bool Telepresence { get; init; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,5 @@
|
|||||||
public class CrewMemberRoleChange : CrewMemberJoins
|
public class CrewMemberRoleChange : CrewMemberJoins
|
||||||
{
|
{
|
||||||
public string Role { get; init; }
|
public string Role { get; init; }
|
||||||
public bool Telepresence { get; init; }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
namespace Observatory.Framework.Files.Journal
|
namespace Observatory.Framework.Files.Journal
|
||||||
{
|
{
|
||||||
public class QuitACrew : JoinACrew
|
public class QuitACrew : JoinACrew
|
||||||
{
|
{ }
|
||||||
public bool Telepresence { get; init; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -6,11 +6,32 @@ namespace Observatory.Framework.Files.ParameterTypes
|
|||||||
{
|
{
|
||||||
public string Label { get; init; }
|
public string Label { get; init; }
|
||||||
|
|
||||||
public double Value { get; init; }
|
[JsonConverter(typeof(Converters.MutableStringDoubleConverter))]
|
||||||
|
public object Value
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(ValueString))
|
||||||
|
return ValueString;
|
||||||
|
else
|
||||||
|
return ValueNumeric;
|
||||||
|
}
|
||||||
|
|
||||||
|
init
|
||||||
|
{
|
||||||
|
if (value.GetType() == typeof(string))
|
||||||
|
ValueString = value.ToString();
|
||||||
|
else
|
||||||
|
ValueNumeric = (double)value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public double OriginalValue { get; init; }
|
public double OriginalValue { get; init; }
|
||||||
|
|
||||||
[JsonConverter(typeof(Converters.IntBoolConverter))]
|
[JsonConverter(typeof(Converters.IntBoolConverter))]
|
||||||
public bool LessIsGood { get; init; }
|
public bool LessIsGood { get; init; }
|
||||||
|
|
||||||
|
private double ValueNumeric;
|
||||||
|
private string ValueString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -135,7 +135,82 @@
|
|||||||
</member>
|
</member>
|
||||||
<member name="F:Observatory.Framework.NotificationArgs.Rendering">
|
<member name="F:Observatory.Framework.NotificationArgs.Rendering">
|
||||||
<summary>
|
<summary>
|
||||||
Specifies the desired renderings of the notification.
|
Specifies the desired renderings of the notification. Defaults to <see cref="F:Observatory.Framework.NotificationRendering.All"/>.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Observatory.Framework.NotificationArgs.Suppression">
|
||||||
|
<summary>
|
||||||
|
Specifies if some part of the notification should be suppressed. Not supported by all notifiers. Defaults to <see cref="F:Observatory.Framework.NotificationSuppression.None"/>.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Observatory.Framework.NotificationSuppression">
|
||||||
|
<summary>
|
||||||
|
Defines constants for suppression of title or detail announcement in a notification.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Observatory.Framework.NotificationSuppression.None">
|
||||||
|
<summary>
|
||||||
|
No suppression.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Observatory.Framework.NotificationSuppression.Title">
|
||||||
|
<summary>
|
||||||
|
Suppress title.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Observatory.Framework.NotificationSuppression.Detail">
|
||||||
|
<summary>
|
||||||
|
Suppress detail.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Observatory.Framework.NotificationRendering">
|
||||||
|
<summary>
|
||||||
|
Defines constants for controlling notification routing to plugins or native notification handlers.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Observatory.Framework.NotificationRendering.NativeVisual">
|
||||||
|
<summary>
|
||||||
|
Send notification to native visual popup notificaiton handler.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Observatory.Framework.NotificationRendering.NativeVocal">
|
||||||
|
<summary>
|
||||||
|
Send notification to native speech notification handler.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Observatory.Framework.NotificationRendering.PluginNotifier">
|
||||||
|
<summary>
|
||||||
|
Send notification to all installed notifier plugins.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Observatory.Framework.NotificationRendering.All">
|
||||||
|
<summary>
|
||||||
|
Send notification to all available handlers.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="T:Observatory.Framework.LogMonitorState">
|
||||||
|
<summary>
|
||||||
|
Flags indicating current state of journal monitoring.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Observatory.Framework.LogMonitorState.Idle">
|
||||||
|
<summary>
|
||||||
|
Monitoring is stopped.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Observatory.Framework.LogMonitorState.Realtime">
|
||||||
|
<summary>
|
||||||
|
Real-time monitoring is active.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Observatory.Framework.LogMonitorState.Batch">
|
||||||
|
<summary>
|
||||||
|
Batch read of historical journals is in progress.
|
||||||
|
</summary>
|
||||||
|
</member>
|
||||||
|
<member name="F:Observatory.Framework.LogMonitorState.PreRead">
|
||||||
|
<summary>
|
||||||
|
Batch read of recent journals is in progress to establish current player state.
|
||||||
</summary>
|
</summary>
|
||||||
</member>
|
</member>
|
||||||
<member name="T:Observatory.Framework.LogMonitorStateChangedEventArgs">
|
<member name="T:Observatory.Framework.LogMonitorStateChangedEventArgs">
|
||||||
|
@ -43,6 +43,15 @@ namespace Observatory.Herald
|
|||||||
// to make perceived volume more in line with value set.
|
// to make perceived volume more in line with value set.
|
||||||
this.volume = ((byte)System.Math.Floor(System.Math.Pow(volume / 100.0, 2.0) * 100));
|
this.volume = ((byte)System.Math.Floor(System.Math.Pow(volume / 100.0, 2.0) * 100));
|
||||||
|
|
||||||
|
Debug.WriteLine("Attempting to de-dupe notification titles against '{0}': '{1}'",
|
||||||
|
notification.Title.Trim().ToLower(),
|
||||||
|
String.Join(',', notifications.Select(n => n.Title.Trim().ToLower())));
|
||||||
|
|
||||||
|
if (notifications.Where(n => n.Title.Trim().ToLower() == notification.Title.Trim().ToLower()).Any())
|
||||||
|
{
|
||||||
|
// Suppress title.
|
||||||
|
notification.Suppression |= NotificationSuppression.Title;
|
||||||
|
}
|
||||||
notifications.Enqueue(notification);
|
notifications.Enqueue(notification);
|
||||||
|
|
||||||
if (!processing)
|
if (!processing)
|
||||||
@ -59,7 +68,7 @@ namespace Observatory.Herald
|
|||||||
|
|
||||||
private void ProcessQueue()
|
private void ProcessQueue()
|
||||||
{
|
{
|
||||||
|
Thread.Sleep(200); // Allow time for other notifications to arrive.
|
||||||
NotificationArgs notification = null;
|
NotificationArgs notification = null;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@ -69,24 +78,20 @@ namespace Observatory.Herald
|
|||||||
notification = notifications.Dequeue();
|
notification = notifications.Dequeue();
|
||||||
Debug.WriteLine("Processing notification: {0} - {1}", notification.Title, notification.Detail);
|
Debug.WriteLine("Processing notification: {0} - {1}", notification.Title, notification.Detail);
|
||||||
|
|
||||||
Task<string>[] audioRequestTasks = new Task<string>[2];
|
List<Task<string>> audioRequestTasks = new();
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(notification.TitleSsml))
|
if (!notification.Suppression.HasFlag(NotificationSuppression.Title))
|
||||||
{
|
{
|
||||||
audioRequestTasks[0] = RetrieveAudioToFile(notification.Title);
|
audioRequestTasks.Add(string.IsNullOrWhiteSpace(notification.TitleSsml)
|
||||||
}
|
? RetrieveAudioToFile(notification.Title)
|
||||||
else
|
: RetrieveAudioSsmlToFile(notification.TitleSsml));
|
||||||
{
|
|
||||||
audioRequestTasks[0] = RetrieveAudioSsmlToFile(notification.TitleSsml);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (string.IsNullOrWhiteSpace(notification.DetailSsml))
|
if (!notification.Suppression.HasFlag(NotificationSuppression.Detail))
|
||||||
{
|
{
|
||||||
audioRequestTasks[1] = RetrieveAudioToFile(notification.Detail);
|
audioRequestTasks.Add(string.IsNullOrWhiteSpace(notification.DetailSsml)
|
||||||
}
|
? RetrieveAudioToFile(notification.Detail)
|
||||||
else
|
: RetrieveAudioSsmlToFile(notification.DetailSsml));
|
||||||
{
|
|
||||||
audioRequestTasks[1] = RetrieveAudioSsmlToFile(notification.DetailSsml);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
PlayAudioRequestsSequentially(audioRequestTasks);
|
PlayAudioRequestsSequentially(audioRequestTasks);
|
||||||
@ -105,7 +110,7 @@ namespace Observatory.Herald
|
|||||||
|
|
||||||
private async Task<string> RetrieveAudioToFile(string text)
|
private async Task<string> RetrieveAudioToFile(string text)
|
||||||
{
|
{
|
||||||
return await RetrieveAudioSsmlToFile($"<speak version=\"1.0\" xmlns=\"http://www.w3.org/2001/10/synthesis\" xml:lang=\"en-US\"><voice name=\"\">{text}</voice></speak>");
|
return await RetrieveAudioSsmlToFile($"<speak version=\"1.0\" xmlns=\"http://www.w3.org/2001/10/synthesis\" xml:lang=\"en-US\"><voice name=\"\">{System.Security.SecurityElement.Escape(text)}</voice></speak>");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<string> RetrieveAudioSsmlToFile(string ssml)
|
private async Task<string> RetrieveAudioSsmlToFile(string ssml)
|
||||||
@ -113,7 +118,7 @@ namespace Observatory.Herald
|
|||||||
return await speechManager.GetAudioFileFromSsml(ssml, voice, style, rate);
|
return await speechManager.GetAudioFileFromSsml(ssml, voice, style, rate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PlayAudioRequestsSequentially(Task<string>[] requestTasks)
|
private void PlayAudioRequestsSequentially(List<Task<string>> requestTasks)
|
||||||
{
|
{
|
||||||
foreach (var request in requestTasks)
|
foreach (var request in requestTasks)
|
||||||
{
|
{
|
||||||
@ -131,7 +136,12 @@ namespace Observatory.Herald
|
|||||||
while (audioPlayer.Playing)
|
while (audioPlayer.Playing)
|
||||||
Thread.Sleep(50);
|
Thread.Sleep(50);
|
||||||
|
|
||||||
|
// Explicit stop to ensure device is ready for next file.
|
||||||
|
// ...hopefully.
|
||||||
|
audioPlayer.Stop(true).Wait();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
speechManager.CommitCache();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ namespace NetCoreAudio.Interfaces
|
|||||||
Task Play(string fileName);
|
Task Play(string fileName);
|
||||||
Task Pause();
|
Task Pause();
|
||||||
Task Resume();
|
Task Resume();
|
||||||
Task Stop();
|
Task Stop(bool force);
|
||||||
Task SetVolume(byte percent);
|
Task SetVolume(byte percent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -71,9 +71,9 @@ namespace NetCoreAudio
|
|||||||
/// Stops any current playback and clears the buffer. Sets Playing and Paused flags to false.
|
/// Stops any current playback and clears the buffer. Sets Playing and Paused flags to false.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns></returns>
|
/// <returns></returns>
|
||||||
public async Task Stop()
|
public async Task Stop(bool force = false)
|
||||||
{
|
{
|
||||||
await _internalPlayer.Stop();
|
await _internalPlayer.Stop(force);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnPlaybackFinished(object sender, EventArgs e)
|
private void OnPlaybackFinished(object sender, EventArgs e)
|
||||||
|
@ -56,7 +56,7 @@ namespace NetCoreAudio.Players
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Stop()
|
public Task Stop(bool force = false)
|
||||||
{
|
{
|
||||||
if (_process != null)
|
if (_process != null)
|
||||||
{
|
{
|
||||||
|
@ -76,15 +76,15 @@ namespace NetCoreAudio.Players
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task Stop()
|
public Task Stop(bool force = false)
|
||||||
{
|
{
|
||||||
if (Playing)
|
if (Playing || force)
|
||||||
{
|
{
|
||||||
ExecuteMciCommand($"Stop {_fileName}");
|
ExecuteMciCommand($"Stop {_fileName}");
|
||||||
Playing = false;
|
Playing = false;
|
||||||
Paused = false;
|
Paused = false;
|
||||||
_playbackTimer.Stop();
|
_playbackTimer?.Stop();
|
||||||
_playStopwatch.Stop();
|
_playStopwatch?.Stop();
|
||||||
}
|
}
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
2
ObservatoryHerald/ObservatoryAPI.Designer.cs
generated
2
ObservatoryHerald/ObservatoryAPI.Designer.cs
generated
@ -19,7 +19,7 @@ namespace Observatory.Herald {
|
|||||||
// class via a tool like ResGen or Visual Studio.
|
// class via a tool like ResGen or Visual Studio.
|
||||||
// To add or remove a member, edit your .ResX file then rerun ResGen
|
// To add or remove a member, edit your .ResX file then rerun ResGen
|
||||||
// with the /str option, or rebuild your VS project.
|
// with the /str option, or rebuild your VS project.
|
||||||
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")]
|
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
|
||||||
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
|
||||||
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
|
||||||
internal class ObservatoryAPI {
|
internal class ObservatoryAPI {
|
||||||
|
@ -9,6 +9,7 @@ using System.Text.Json;
|
|||||||
using System.Xml;
|
using System.Xml;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
namespace Observatory.Herald
|
namespace Observatory.Herald
|
||||||
{
|
{
|
||||||
@ -19,7 +20,8 @@ namespace Observatory.Herald
|
|||||||
private string ApiEndpoint;
|
private string ApiEndpoint;
|
||||||
private DirectoryInfo cacheLocation;
|
private DirectoryInfo cacheLocation;
|
||||||
private int cacheSize;
|
private int cacheSize;
|
||||||
private Action<Exception, String> ErrorLogger;
|
private Action<Exception, string> ErrorLogger;
|
||||||
|
private ConcurrentDictionary<string, CacheData> cacheIndex;
|
||||||
|
|
||||||
internal SpeechRequestManager(
|
internal SpeechRequestManager(
|
||||||
HeraldSettings settings, HttpClient httpClient, string cacheFolder, Action<Exception, String> errorLogger)
|
HeraldSettings settings, HttpClient httpClient, string cacheFolder, Action<Exception, String> errorLogger)
|
||||||
@ -29,6 +31,7 @@ namespace Observatory.Herald
|
|||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
cacheSize = Math.Max(settings.CacheSize, 1);
|
cacheSize = Math.Max(settings.CacheSize, 1);
|
||||||
cacheLocation = new DirectoryInfo(cacheFolder);
|
cacheLocation = new DirectoryInfo(cacheFolder);
|
||||||
|
ReadCache();
|
||||||
ErrorLogger = errorLogger;
|
ErrorLogger = errorLogger;
|
||||||
|
|
||||||
if (!Directory.Exists(cacheLocation.FullName))
|
if (!Directory.Exists(cacheLocation.FullName))
|
||||||
@ -103,26 +106,27 @@ namespace Observatory.Herald
|
|||||||
XmlNamespaceManager ssmlNs = new(ssmlDoc.NameTable);
|
XmlNamespaceManager ssmlNs = new(ssmlDoc.NameTable);
|
||||||
ssmlNs.AddNamespace("ssml", ssmlNamespace);
|
ssmlNs.AddNamespace("ssml", ssmlNamespace);
|
||||||
ssmlNs.AddNamespace("mstts", "http://www.w3.org/2001/mstts");
|
ssmlNs.AddNamespace("mstts", "http://www.w3.org/2001/mstts");
|
||||||
|
ssmlNs.AddNamespace("emo", "http://www.w3.org/2009/10/emotionml");
|
||||||
|
|
||||||
var voiceNode = ssmlDoc.SelectSingleNode("/ssml:speak/ssml:voice", ssmlNs);
|
var voiceNode = ssmlDoc.SelectSingleNode("/ssml:speak/ssml:voice", ssmlNs);
|
||||||
voiceNode.Attributes.GetNamedItem("name").Value = voiceName;
|
voiceNode.Attributes.GetNamedItem("name").Value = voiceName;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(styleName))
|
|
||||||
{
|
|
||||||
var expressAsNode = ssmlDoc.CreateElement("express-as", "http://www.w3.org/2001/mstts");
|
|
||||||
expressAsNode.SetAttribute("style", styleName);
|
|
||||||
expressAsNode.InnerXml = voiceNode.InnerXml;
|
|
||||||
voiceNode.InnerXml = expressAsNode.OuterXml;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(rate))
|
if (!string.IsNullOrWhiteSpace(rate))
|
||||||
{
|
{
|
||||||
var prosodyNode = ssmlDoc.CreateElement("prosody", ssmlNamespace);
|
var prosodyNode = ssmlDoc.CreateElement("ssml", "prosody", ssmlNamespace);
|
||||||
prosodyNode.SetAttribute("rate", rate);
|
prosodyNode.SetAttribute("rate", rate);
|
||||||
prosodyNode.InnerXml = voiceNode.InnerXml;
|
prosodyNode.InnerXml = voiceNode.InnerXml;
|
||||||
voiceNode.InnerXml = prosodyNode.OuterXml;
|
voiceNode.InnerXml = prosodyNode.OuterXml;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(styleName))
|
||||||
|
{
|
||||||
|
var expressAsNode = ssmlDoc.CreateElement("mstts", "express-as", "http://www.w3.org/2001/mstts");
|
||||||
|
expressAsNode.SetAttribute("style", styleName);
|
||||||
|
expressAsNode.InnerXml = voiceNode.InnerXml;
|
||||||
|
voiceNode.InnerXml = expressAsNode.OuterXml;
|
||||||
|
}
|
||||||
|
|
||||||
return ssmlDoc.OuterXml;
|
return ssmlDoc.OuterXml;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -229,10 +233,8 @@ namespace Observatory.Herald
|
|||||||
return demonym;
|
return demonym;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void UpdateAndPruneCache(FileInfo currentFile)
|
private void ReadCache()
|
||||||
{
|
{
|
||||||
Dictionary<string, CacheData> cacheIndex;
|
|
||||||
|
|
||||||
string cacheIndexFile = cacheLocation + "CacheIndex.json";
|
string cacheIndexFile = cacheLocation + "CacheIndex.json";
|
||||||
|
|
||||||
if (File.Exists(cacheIndexFile))
|
if (File.Exists(cacheIndexFile))
|
||||||
@ -240,7 +242,7 @@ namespace Observatory.Herald
|
|||||||
var indexFileContent = File.ReadAllText(cacheIndexFile);
|
var indexFileContent = File.ReadAllText(cacheIndexFile);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
cacheIndex = JsonSerializer.Deserialize<Dictionary<string, CacheData>>(indexFileContent);
|
cacheIndex = JsonSerializer.Deserialize<ConcurrentDictionary<string, CacheData>>(indexFileContent);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@ -258,9 +260,13 @@ namespace Observatory.Herald
|
|||||||
var cacheFiles = cacheLocation.GetFiles("*.mp3");
|
var cacheFiles = cacheLocation.GetFiles("*.mp3");
|
||||||
foreach (var file in cacheFiles.Where(file => !cacheIndex.ContainsKey(file.Name)))
|
foreach (var file in cacheFiles.Where(file => !cacheIndex.ContainsKey(file.Name)))
|
||||||
{
|
{
|
||||||
cacheIndex.Add(file.Name, new(file.CreationTime, 0));
|
cacheIndex.TryAdd(file.Name, new(file.CreationTime, 0));
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateAndPruneCache(FileInfo currentFile)
|
||||||
|
{
|
||||||
|
var cacheFiles = cacheLocation.GetFiles("*.mp3");
|
||||||
if (cacheIndex.ContainsKey(currentFile.Name))
|
if (cacheIndex.ContainsKey(currentFile.Name))
|
||||||
{
|
{
|
||||||
cacheIndex[currentFile.Name] = new(
|
cacheIndex[currentFile.Name] = new(
|
||||||
@ -270,13 +276,15 @@ namespace Observatory.Herald
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
cacheIndex.Add(currentFile.Name, new(DateTime.UtcNow, 1));
|
cacheIndex.TryAdd(currentFile.Name, new(DateTime.UtcNow, 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentCacheSize = cacheFiles.Sum(f => f.Length);
|
var indexedCacheSize = cacheFiles
|
||||||
while (currentCacheSize > cacheSize * 1024 * 1024)
|
.Where(f => cacheIndex.ContainsKey(f.Name))
|
||||||
{
|
.Sum(f => f.Length);
|
||||||
|
|
||||||
|
while (indexedCacheSize > cacheSize * 1024 * 1024)
|
||||||
|
{
|
||||||
var staleFile = (from file in cacheIndex
|
var staleFile = (from file in cacheIndex
|
||||||
orderby file.Value.HitCount, file.Value.Created
|
orderby file.Value.HitCount, file.Value.Created
|
||||||
select file.Key).First();
|
select file.Key).First();
|
||||||
@ -284,16 +292,19 @@ namespace Observatory.Herald
|
|||||||
if (staleFile == currentFile.Name)
|
if (staleFile == currentFile.Name)
|
||||||
break;
|
break;
|
||||||
|
|
||||||
currentCacheSize -= new FileInfo(cacheLocation + staleFile).Length;
|
cacheIndex.TryRemove(staleFile, out _);
|
||||||
File.Delete(cacheLocation + staleFile);
|
|
||||||
cacheIndex.Remove(staleFile);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async void CommitCache()
|
||||||
|
{
|
||||||
|
string cacheIndexFile = cacheLocation + "CacheIndex.json";
|
||||||
|
|
||||||
// Race conditions between title and detail speech make a collision here possible.
|
|
||||||
// Wait for file to become writable, but return control to call site while we wait.
|
|
||||||
System.Diagnostics.Stopwatch stopwatch = new();
|
System.Diagnostics.Stopwatch stopwatch = new();
|
||||||
stopwatch.Start();
|
stopwatch.Start();
|
||||||
|
|
||||||
|
// Race condition isn't a concern anymore, but should check this anyway to be safe.
|
||||||
|
// (Maybe someone is poking at the file with notepad?)
|
||||||
while (!IsFileWritable(cacheIndexFile) && stopwatch.ElapsedMilliseconds < 1000)
|
while (!IsFileWritable(cacheIndexFile) && stopwatch.ElapsedMilliseconds < 1000)
|
||||||
await Task.Factory.StartNew(() => System.Threading.Thread.Sleep(100));
|
await Task.Factory.StartNew(() => System.Threading.Thread.Sleep(100));
|
||||||
|
|
||||||
@ -325,7 +336,7 @@ namespace Observatory.Herald
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class CacheData
|
private class CacheData
|
||||||
{
|
{
|
||||||
public CacheData(DateTime Created, int HitCount)
|
public CacheData(DateTime Created, int HitCount)
|
||||||
{
|
{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user