2
0
mirror of https://github.com/9ParsonsB/Pulsar.git synced 2025-04-05 17:39:39 -04:00

ready for testing

This commit is contained in:
Xjph 2024-01-21 13:35:03 -03:30
parent 86cd7fe3e4
commit b8f5f6a73e
92 changed files with 3061 additions and 1186 deletions

View File

@ -23,7 +23,7 @@
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Condition=" '$(OS)' == 'Windows_NT' " Command="xcopy &quot;$(TargetPath)&quot; &quot;$(ProjectDir)..\ObservatoryCore\$(OutDir)plugins\&quot; /y" />
<Exec Condition=" '$(OS)' == 'Windows_NT' " Command="xcopy &quot;$(TargetPath)&quot; &quot;$(ProjectDir)..\ObservatoryCore\$(OutDir)..\net6.0-windows\plugins\&quot; /y" />
<Exec Condition=" '$(OS)' != 'Windows_NT' " Command="cp &quot;$(TargetPath)&quot; &quot;$(ProjectDir)../ObservatoryCore/$(OutDir)plugins/&quot; -f" />
</Target>

View File

@ -70,6 +70,12 @@
<setting name="StartReadAll" serializeAs="String">
<value>False</value>
</setting>
<setting name="Theme" serializeAs="String">
<value>Dark</value>
</setting>
<setting name="ColumnSizing" serializeAs="String">
<value />
</setting>
</Observatory.Properties.Core>
</userSettings>
</configuration>

View File

@ -0,0 +1,73 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace Observatory.Assets {
using System;
/// <summary>
/// A strongly-typed resource class, for looking up localized strings, etc.
/// </summary>
// This class was auto-generated by the StronglyTypedResourceBuilder
// class via a tool like ResGen or Visual Studio.
// To add or remove a member, edit your .ResX file then rerun ResGen
// with the /str option, or rebuild your VS project.
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
internal class Resources {
private static global::System.Resources.ResourceManager resourceMan;
private static global::System.Globalization.CultureInfo resourceCulture;
[global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")]
internal Resources() {
}
/// <summary>
/// Returns the cached ResourceManager instance used by this class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Resources.ResourceManager ResourceManager {
get {
if (object.ReferenceEquals(resourceMan, null)) {
global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Observatory.Assets.Resources", typeof(Resources).Assembly);
resourceMan = temp;
}
return resourceMan;
}
}
/// <summary>
/// Overrides the current thread's CurrentUICulture property for all
/// resource lookups using this strongly typed resource class.
/// </summary>
[global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)]
internal static global::System.Globalization.CultureInfo Culture {
get {
return resourceCulture;
}
set {
resourceCulture = value;
}
}
/// <summary>
/// Looks up a localized resource of type System.Drawing.Icon similar to (Icon).
/// </summary>
internal static System.Drawing.Icon EOCIcon_Presized {
get {
object obj = ResourceManager.GetObject("EOCIcon_Presized", resourceCulture);
return ((System.Drawing.Icon)(obj));
}
}
}
}

View File

@ -0,0 +1,124 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<assembly alias="System.Windows.Forms" name="System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<data name="EOCIcon_Presized" type="System.Resources.ResXFileRef, System.Windows.Forms">
<value>EOCIcon-Presized.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a</value>
</data>
</root>

View File

@ -1,5 +1,6 @@
using Observatory.Framework;
using Observatory.UI;
using System;
namespace Observatory.NativeNotification
{
@ -21,6 +22,11 @@ namespace Observatory.NativeNotification
notification.FormClosed += NotifyWindow_Closed;
foreach(var notificationForm in notifications)
{
notificationForm.Value.AdjustOffset(true);
}
notifications.Add(notificationGuid, notification);
notification.Show();
});
@ -34,6 +40,11 @@ namespace Observatory.NativeNotification
{
var currentNotification = (NotificationForm)sender;
foreach (var notification in notifications.Where(n => n.Value.CreationTime < currentNotification.CreationTime))
{
notification.Value.AdjustOffset(false);
}
if (notifications.ContainsKey(currentNotification.Guid))
{
notifications.Remove(currentNotification.Guid);

View File

@ -10,7 +10,7 @@ namespace Observatory.NativeNotification
{
public class NativeVoice
{
private Queue<NotificationArgs> notificationEvents;
private readonly Queue<NotificationArgs> notificationEvents;
private bool processing;
public NativeVoice()
@ -83,19 +83,23 @@ namespace Observatory.NativeNotification
processing = false;
}
private string AddVoiceToSsml(string ssml, string voiceName)
private static string AddVoiceToSsml(string ssml, string voiceName)
{
XmlDocument ssmlDoc = new();
ssmlDoc.LoadXml(ssml);
var ssmlNamespace = ssmlDoc.DocumentElement.NamespaceURI;
var ssmlNamespace = ssmlDoc.DocumentElement?.NamespaceURI;
XmlNamespaceManager ssmlNs = new(ssmlDoc.NameTable);
ssmlNs.AddNamespace("ssml", ssmlNamespace);
ssmlNs.AddNamespace("ssml", ssmlNamespace ?? string.Empty);
var voiceNode = ssmlDoc.SelectSingleNode("/ssml:speak/ssml:voice", ssmlNs);
voiceNode.Attributes.GetNamedItem("name").Value = voiceName;
var voiceNameNode = voiceNode?.Attributes?.GetNamedItem("name");
if (voiceNameNode != null)
{
voiceNameNode.Value = voiceName;
}
return ssmlDoc.OuterXml;
}

View File

@ -22,8 +22,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Security.Extensions" Version="1.2.0" />
<PackageReference Include="System.Speech" Version="7.0.0" />
<PackageReference Include="Microsoft.Security.Extensions" Version="1.3.0" />
<PackageReference Include="NAudio" Version="2.2.1" />
<PackageReference Include="System.Speech" Version="8.0.0" />
</ItemGroup>
<ItemGroup>
@ -33,6 +34,11 @@
</ItemGroup>
<ItemGroup>
<Compile Update="Assets\Resources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>Resources.resx</DependentUpon>
</Compile>
<Compile Update="Properties\Core.Designer.cs">
<DesignTimeSharedInput>True</DesignTimeSharedInput>
<AutoGen>True</AutoGen>
@ -46,6 +52,10 @@
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Assets\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
</EmbeddedResource>
<EmbeddedResource Update="Properties\Resources.resx">
<Generator>ResXFileCodeGenerator</Generator>
<LastGenOutput>Resources.Designer.cs</LastGenOutput>
@ -59,6 +69,10 @@
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="Resources\" />
</ItemGroup>
<Target Name="PreBuild" BeforeTargets="PreBuildEvent">
<Exec Condition=" '$(OS)' == 'Windows_NT'" Command="if not exist &quot;$(ProjectDir)..\ObservatoryFramework\bin\Release\net6.0\ObservatoryFramework.dll&quot; dotnet build &quot;$(ProjectDir)..\ObservatoryFramework\ObservatoryFramework.csproj&quot; -c Release" />
<Exec Condition=" '$(OS)' == 'Windows_NT'" Command="if not exist &quot;$(OutDir)plugins\ObservatoryExplorer.dll&quot; dotnet build &quot;$(ProjectDir)..\ObservatoryExplorer\ObservatoryExplorer.csproj&quot; -c $(ConfigurationName)" />

View File

@ -14,11 +14,15 @@ namespace Observatory.PluginManagement
private readonly NativeVoice NativeVoice;
private readonly NativePopup NativePopup;
private bool OverridePopup;
private bool OverrideAudio;
public PluginCore()
public PluginCore(bool OverridePopup = false, bool OverrideAudio = false)
{
NativeVoice = new();
NativePopup = new();
this.OverridePopup = OverridePopup;
this.OverrideAudio = OverrideAudio;
}
public string Version => System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0";
@ -31,10 +35,7 @@ namespace Observatory.PluginManagement
};
}
public Status GetStatus()
{
throw new NotImplementedException();
}
public Status GetStatus() => LogMonitor.GetInstance.Status;
public Guid SendNotification(string title, string text)
{
@ -53,12 +54,12 @@ namespace Observatory.PluginManagement
handler?.Invoke(this, notificationArgs);
}
if (Properties.Core.Default.NativeNotify && notificationArgs.Rendering.HasFlag(NotificationRendering.NativeVisual))
if (!OverridePopup && Properties.Core.Default.NativeNotify && notificationArgs.Rendering.HasFlag(NotificationRendering.NativeVisual))
{
guid = NativePopup.InvokeNativeNotification(notificationArgs);
}
if (Properties.Core.Default.VoiceNotify && notificationArgs.Rendering.HasFlag(NotificationRendering.NativeVocal))
if (!OverrideAudio && Properties.Core.Default.VoiceNotify && notificationArgs.Rendering.HasFlag(NotificationRendering.NativeVocal))
{
NativeVoice.EnqueueAndAnnounce(notificationArgs);
}
@ -69,14 +70,13 @@ namespace Observatory.PluginManagement
public void CancelNotification(Guid id)
{
NativePopup.CloseNotification(id);
ExecuteOnUIThread(() => NativePopup.CloseNotification(id));
}
public void UpdateNotification(Guid id, NotificationArgs notificationArgs)
{
if (!IsLogMonitorBatchReading)
{
if (notificationArgs.Rendering.HasFlag(NotificationRendering.PluginNotifier))
{
var handler = Notification;
@ -140,6 +140,8 @@ namespace Observatory.PluginManagement
public event EventHandler<NotificationArgs> Notification;
internal event EventHandler<PluginMessageArgs> PluginMessage;
public string PluginStorageFolder
{
get
@ -161,25 +163,19 @@ namespace Observatory.PluginManagement
}
}
public async Task PlayAudioFile(string filePath)
{
await AudioHandler.PlayFile(filePath);
}
public void SendPluginMessage(IObservatoryPlugin plugin, object message)
{
PluginMessage?.Invoke(this, new PluginMessageArgs(plugin.Name, plugin.Version, message));
}
internal void Shutdown()
{
NativePopup.CloseAll();
}
private static bool FirstRowIsAllNull(IObservatoryWorker worker)
{
bool allNull = true;
Type itemType = worker.PluginUI.DataGrid[0].GetType();
foreach (var property in itemType.GetProperties())
{
if (property.GetValue(worker.PluginUI.DataGrid[0], null) != null)
{
allNull = false;
break;
}
}
return allNull;
}
}
}

View File

@ -110,6 +110,14 @@ namespace Observatory.PluginManagement
}
}
public void OnPluginMessageEvent(object _, PluginMessageArgs messageArgs)
{
foreach (var plugin in observatoryNotifiers.Cast<IObservatoryPlugin>().Concat(observatoryWorkers))
{
plugin.HandlePluginMessage(messageArgs.SourceName, messageArgs.SourceVersion, messageArgs.Message);
}
}
private void ResetTimer()
{
timer.Stop();
@ -146,4 +154,18 @@ namespace Observatory.PluginManagement
}
}
}
internal class PluginMessageArgs
{
internal string SourceName;
internal string SourceVersion;
internal object Message;
internal PluginMessageArgs(string sourceName, string sourceVersion, object message)
{
SourceName = sourceName;
SourceVersion = sourceVersion;
Message = message;
}
}
}

View File

@ -50,7 +50,10 @@ namespace Observatory.PluginManagement
logMonitor.StatusUpdate += pluginHandler.OnStatusUpdate;
logMonitor.LogMonitorStateChanged += pluginHandler.OnLogMonitorStateChanged;
core = new PluginCore();
var ovPopup = notifyPlugins.Any(n => n.plugin.OverridePopupNotifications);
var ovAudio = notifyPlugins.Any(n => n.plugin.OverrideAudioNotifications);
core = new PluginCore(ovPopup, ovAudio);
List<IObservatoryPlugin> errorPlugins = new();
@ -97,6 +100,7 @@ namespace Observatory.PluginManagement
notifyPlugins.RemoveAll(n => errorPlugins.Contains(n.plugin));
core.Notification += pluginHandler.OnNotificationEvent;
core.PluginMessage += pluginHandler.OnPluginMessageEvent;
if (errorList.Any())
ErrorReporter.ShowErrorPopup("Plugin Load Error" + (errorList.Count > 1 ? "s" : String.Empty), errorList);
@ -114,7 +118,15 @@ namespace Observatory.PluginManagement
if (!String.IsNullOrWhiteSpace(savedSettings))
{
pluginSettings = JsonSerializer.Deserialize<Dictionary<string, object>>(savedSettings);
var settings = JsonSerializer.Deserialize<Dictionary<string, object>>(savedSettings);
if (settings != null)
{
pluginSettings = settings;
}
else
{
pluginSettings = new();
}
}
else
{
@ -138,7 +150,7 @@ namespace Observatory.PluginManagement
var properties = settings.GetType().GetProperties();
foreach (var property in properties)
{
var attrib = property.GetCustomAttribute<Framework.SettingDisplayName>();
var attrib = property.GetCustomAttribute<SettingDisplayName>();
if (attrib == null)
{
settingNames.Add(property, property.Name);
@ -396,7 +408,7 @@ namespace Observatory.PluginManagement
if (constructor != null)
{
object instance = constructor.Invoke(Array.Empty<object>());
notifiers.Add(((instance as IObservatoryNotifier)!, PluginStatus.Signed));
notifiers.Add(((instance as IObservatoryNotifier)!, pluginStatus));
pluginCount++;
}
}

View File

@ -12,7 +12,7 @@ namespace Observatory.Properties {
[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.3.0.0")]
[global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "17.8.0.0")]
internal sealed partial class Core : global::System.Configuration.ApplicationSettingsBase {
private static Core defaultInstance = ((Core)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Core())));
@ -285,5 +285,29 @@ namespace Observatory.Properties {
this["UnsignedAllowed"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("Dark")]
public string Theme {
get {
return ((string)(this["Theme"]));
}
set {
this["Theme"] = value;
}
}
[global::System.Configuration.UserScopedSettingAttribute()]
[global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
[global::System.Configuration.DefaultSettingValueAttribute("")]
public string ColumnSizing {
get {
return ((string)(this["ColumnSizing"]));
}
set {
this["ColumnSizing"] = value;
}
}
}
}

View File

@ -68,5 +68,11 @@
<Setting Name="UnsignedAllowed" Type="System.Collections.Specialized.StringCollection" Scope="User">
<Value Profile="(Default)" />
</Setting>
<Setting Name="Theme" Type="System.String" Scope="User">
<Value Profile="(Default)">Dark</Value>
</Setting>
<Setting Name="ColumnSizing" Type="System.String" Scope="User">
<Value Profile="(Default)" />
</Setting>
</Settings>
</SettingsFile>

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@ -0,0 +1,27 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Observatory.UI
{
public class ColumnSizing
{
public string PluginName { get; set; }
public string PluginVersion { get; set; }
public Dictionary<string, int> ColumnWidth
{
get
{
_columnWidth ??= new Dictionary<string, int>();
return _columnWidth;
}
set => _columnWidth = value;
}
private Dictionary<string, int>? _columnWidth;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +1,34 @@
using Observatory.PluginManagement;
using Observatory.Framework.Interfaces;
using System.Linq;
namespace Observatory.UI
{
partial class CoreForm
{
private Dictionary<ListViewItem, IObservatoryPlugin>? ListedPlugins;
private void PopulatePluginList()
{
List<IObservatoryPlugin> uniquePlugins = new();
ListedPlugins = new();
foreach (var (plugin, signed) in PluginManager.GetInstance.workerPlugins)
{
if (!uniquePlugins.Contains(plugin))
if (!ListedPlugins.ContainsValue(plugin))
{
uniquePlugins.Add(plugin);
ListViewItem item = new ListViewItem(new[] { plugin.Name, "Worker", plugin.Version, PluginStatusString(signed) });
ListedPlugins.Add(item, plugin);
PluginList.Items.Add(item);
}
}
foreach (var (plugin, signed) in PluginManager.GetInstance.notifyPlugins)
{
if (!uniquePlugins.Contains(plugin))
if (!ListedPlugins.ContainsValue(plugin))
{
uniquePlugins.Add(plugin);
ListViewItem item = new ListViewItem(new[] { plugin.Name, "Notifier", plugin.Version, PluginStatusString(signed) });
ListedPlugins.Add(item, plugin);
PluginList.Items.Add(item);
}
}
@ -71,39 +74,87 @@ namespace Observatory.UI
{
pluginList.Add(item.Text, item);
}
CoreMenu.Width = GetExpandedMenuWidth();
}
private void CreatePluginSettings()
private void DisableOverriddenNotification()
{
foreach (var plugin in PluginManager.GetInstance.workerPlugins)
var notifyPlugins = PluginManager.GetInstance.notifyPlugins;
var ovPopupPlugins = notifyPlugins.Where(n => n.plugin.OverridePopupNotifications);
if (ovPopupPlugins.Any())
{
var pluginSettingsPanel = new SettingsPanel(plugin.plugin, AdjustPanelsBelow);
AddSettingsPanel(pluginSettingsPanel);
PopupCheckbox.Checked = false;
PopupCheckbox.Enabled = false;
DisplayDropdown.Enabled = false;
CornerDropdown.Enabled = false;
FontDropdown.Enabled = false;
ScaleSpinner.Enabled = false;
DurationSpinner.Enabled = false;
ColourButton.Enabled = false;
TestButton.Enabled = false;
var pluginNames = string.Join(", ", ovPopupPlugins.Select(o => o.plugin.ShortName));
PopupSettingsPanel.MouseMove += (_, _) =>
{
OverrideTooltip.SetToolTip(PopupSettingsPanel, "Disabled by plugin: " + pluginNames);
};
}
foreach (var plugin in PluginManager.GetInstance.notifyPlugins)
var ovAudioPlugins = notifyPlugins.Where(n => n.plugin.OverrideAudioNotifications);
if (ovAudioPlugins.Any())
{
var pluginSettingsPanel = new SettingsPanel(plugin.plugin, AdjustPanelsBelow);
AddSettingsPanel(pluginSettingsPanel);
VoiceCheckbox.Checked = false;
VoiceCheckbox.Enabled = false;
VoiceVolumeSlider.Enabled = false;
VoiceSpeedSlider.Enabled = false;
VoiceDropdown.Enabled = false;
VoiceTestButton.Enabled = false;
var pluginNames = string.Join(", ", ovAudioPlugins.Select(o => o.plugin.ShortName));
VoiceSettingsPanel.MouseMove += (_, _) =>
{
OverrideTooltip.SetToolTip(VoiceSettingsPanel, "Disabled by plugin: " + pluginNames);
};
}
}
private void AddSettingsPanel(SettingsPanel panel)
private int GetExpandedMenuWidth()
{
int lowestPoint = 0;
foreach (Control control in CorePanel.Controls)
int maxWidth = 0;
foreach (ToolStripMenuItem item in CoreMenu.Items)
{
if (control.Location.Y + control.Height > lowestPoint)
lowestPoint = control.Location.Y + control.Height;
var itemWidth = TextRenderer.MeasureText(item.Text, item.Font);
maxWidth = itemWidth.Width > maxWidth ? itemWidth.Width : maxWidth;
}
DuplicateControlVisuals(PopupNotificationLabel, panel.Header);
panel.Header.TextAlign = PopupNotificationLabel.TextAlign;
panel.Header.Location = new Point(PopupNotificationLabel.Location.X, lowestPoint);
DuplicateControlVisuals(PopupSettingsPanel, panel, false);
panel.Location = new Point(PopupSettingsPanel.Location.X, lowestPoint + panel.Header.Height);
panel.Visible = false;
CorePanel.Controls.Add(panel.Header);
CorePanel.Controls.Add(panel);
return maxWidth + 5;
}
private void PluginSettingsButton_Click(object sender, EventArgs e)
{
if (ListedPlugins != null && PluginList.SelectedItems.Count != 0)
{
var plugin = ListedPlugins[PluginList.SelectedItems[0]];
if (SettingsForms.ContainsKey(plugin))
{
SettingsForms[plugin].Activate();
}
else
{
SettingsForm settingsForm = new(plugin);
SettingsForms.Add(plugin, settingsForm);
settingsForm.FormClosed += (_, _) => SettingsForms.Remove(plugin);
settingsForm.Show();
}
}
}
private Dictionary<IObservatoryPlugin, SettingsForm> SettingsForms = new();
}
}

View File

@ -2,6 +2,7 @@
using Observatory.Framework.Interfaces;
using Observatory.PluginManagement;
using Observatory.Utils;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;
@ -9,10 +10,25 @@ namespace Observatory.UI
{
public partial class CoreForm : Form
{
private Dictionary<object, Panel> uiPanels;
private readonly Dictionary<object, Panel> uiPanels;
[DllImport("user32.dll")]
private static extern int SendMessage(IntPtr hWnd, Int32 wMsg, bool wParam, Int32 lParam);
private const int WM_SETREDRAW = 11;
private static void SuspendDrawing(Control control)
{
SendMessage(control.Handle, WM_SETREDRAW, false, 0);
}
private static void ResumeDrawing(Control control)
{
SendMessage(control.Handle, WM_SETREDRAW, true, 0);
control.Refresh();
}
public CoreForm()
{
DoubleBuffered = true;
InitializeComponent();
PopulateDropdownOptions();
@ -24,14 +40,20 @@ namespace Observatory.UI
string version = System.Reflection.Assembly.GetEntryAssembly()?.GetName().Version?.ToString() ?? "0";
Text += $" - v{version}";
CoreMenu.SizeChanged += CoreMenu_SizeChanged;
uiPanels = new();
uiPanels.Add(coreToolStripMenuItem, CorePanel);
uiPanels = new()
{
{ coreToolStripMenuItem, CorePanel }
};
pluginList = new Dictionary<string, ToolStripMenuItem>();
CreatePluginTabs();
CreatePluginSettings();
DisableOverriddenNotification();
CoreMenu.ItemClicked += CoreMenu_ItemClicked;
PreCollapsePanels();
ThemeManager.GetInstance.RegisterControl(this);
}
private void PreCollapsePanels()
@ -47,17 +69,7 @@ namespace Observatory.UI
}
private Dictionary<string, ToolStripMenuItem> pluginList;
private static void DuplicateControlVisuals(Control source, Control target, bool applyHeight = true)
{
if (applyHeight) target.Height = source.Height;
target.Width = source.Width;
target.Font = source.Font;
target.ForeColor = source.ForeColor;
target.BackColor = source.BackColor;
target.Anchor = source.Anchor;
}
private readonly Dictionary<string, ToolStripMenuItem> pluginList;
private void ToggleMonitorButton_Click(object sender, EventArgs e)
{
@ -73,9 +85,25 @@ namespace Observatory.UI
}
}
private void CoreMenu_ItemClicked(object? _, ToolStripItemClickedEventArgs e)
private void ResizePanels(Point location, int widthChange)
{
CorePanel.Location = location;
CorePanel.Width += widthChange;
foreach (var panel in uiPanels)
{
if (Controls.Contains(panel.Value))
{
panel.Value.Location = CorePanel.Location;
panel.Value.Size = CorePanel.Size;
}
}
}
private void CoreMenu_ItemClicked(object? _, ToolStripItemClickedEventArgs e)
{
SuspendDrawing(this);
if (e.ClickedItem.Text == "<")
{
foreach (KeyValuePair<string, ToolStripMenuItem> menuItem in pluginList)
@ -86,8 +114,7 @@ namespace Observatory.UI
menuItem.Value.Text = menuItem.Key[..1];
}
CoreMenu.Width = 40;
CorePanel.Location = new Point(43, 12);
// CorePanel.Width += 40;
ResizePanels(new Point(43, 12), 0);
}
else if (e.ClickedItem.Text == ">")
{
@ -98,9 +125,8 @@ namespace Observatory.UI
else
menuItem.Value.Text = menuItem.Key;
}
CoreMenu.Width = 120;
CorePanel.Location = new Point(123, 12);
// CorePanel.Width -= 40;
CoreMenu.Width = GetExpandedMenuWidth();
ResizePanels(new Point(CoreMenu.Width + 3, 12), 0);
}
else
{
@ -114,11 +140,23 @@ namespace Observatory.UI
uiPanels[e.ClickedItem].Location = CorePanel.Location;
uiPanels[e.ClickedItem].Size = CorePanel.Size;
uiPanels[e.ClickedItem].BackColor = CorePanel.BackColor;
uiPanels[e.ClickedItem].Parent = CorePanel.Parent;
Controls.Add(uiPanels[e.ClickedItem]);
}
uiPanels[e.ClickedItem].Visible = true;
SetClickedItem(e.ClickedItem);
}
ResumeDrawing(this);
}
private void SetClickedItem(ToolStripItem item)
{
foreach (ToolStripItem menuItem in CoreMenu.Items)
{
bool bold = menuItem == item;
menuItem.Font = new Font(menuItem.Font, bold ? FontStyle.Bold : FontStyle.Regular);
}
}
private static void ColourListHeader(ref ListView list, Color backColor, Color foreColor)
@ -128,12 +166,12 @@ namespace Observatory.UI
list.DrawColumnHeader +=
new DrawListViewColumnHeaderEventHandler
(
(sender, e) => headerDraw(sender, e, backColor, foreColor)
(sender, e) => HeaderDraw(sender, e, backColor, foreColor)
);
list.DrawItem += new DrawListViewItemEventHandler(bodyDraw);
list.DrawItem += new DrawListViewItemEventHandler(BodyDraw);
}
private static void headerDraw(object? _, DrawListViewColumnHeaderEventArgs e, Color backColor, Color foreColor)
private static void HeaderDraw(object? _, DrawListViewColumnHeaderEventArgs e, Color backColor, Color foreColor)
{
using (SolidBrush backBrush = new(backColor))
{
@ -149,9 +187,11 @@ namespace Observatory.UI
if (e.Font != null && e.Header != null)
using (SolidBrush foreBrush = new(foreColor))
{
var format = new StringFormat();
format.Alignment = (StringAlignment)e.Header.TextAlign;
format.LineAlignment = StringAlignment.Center;
var format = new StringFormat
{
Alignment = (StringAlignment)e.Header.TextAlign,
LineAlignment = StringAlignment.Center
};
var paddedBounds = new Rectangle(e.Bounds.X + 2, e.Bounds.Y + 2, e.Bounds.Width - 4, e.Bounds.Height - 4);
@ -159,7 +199,7 @@ namespace Observatory.UI
}
}
private static void bodyDraw(object? _, DrawListViewItemEventArgs e)
private static void BodyDraw(object? _, DrawListViewItemEventArgs e)
{
e.DrawDefault = true;
}
@ -180,7 +220,11 @@ namespace Observatory.UI
private void ReadAllButton_Click(object sender, EventArgs e)
{
LogMonitor.GetInstance.ReadAllJournals();
var readAllDialogue = new ReadAllForm();
ThemeManager.GetInstance.RegisterControl(readAllDialogue);
readAllDialogue.StartPosition = FormStartPosition.Manual;
readAllDialogue.Location = Point.Add(Location, new Size(100,100));
readAllDialogue.ShowDialog();
}
private void PopupNotificationLabel_Click(object _, EventArgs e)
@ -238,6 +282,8 @@ namespace Observatory.UI
Up, Down
}
private Observatory.NativeNotification.NativePopup? nativePopup;
private void TestButton_Click(object sender, EventArgs e)
{
NotificationArgs args = new()
@ -245,9 +291,10 @@ namespace Observatory.UI
Title = "Test Notification",
Detail = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec at elit maximus, ornare dui nec, accumsan velit. Vestibulum fringilla elit."
};
var testNotify = new NotificationForm(new Guid(), args);
testNotify.Show();
nativePopup ??= new Observatory.NativeNotification.NativePopup();
nativePopup.InvokeNativeNotification(args);
}
}
}

View File

@ -1,4 +1,64 @@
<root>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
@ -63,6 +123,9 @@
<metadata name="PopupColour.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>126, 17</value>
</metadata>
<metadata name="OverrideTooltip.TrayLocation" type="System.Drawing.Point, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
<value>251, 17</value>
</metadata>
<assembly alias="System.Drawing" name="System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a" />
<data name="$this.Icon" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>

View File

@ -28,55 +28,54 @@
/// </summary>
private void InitializeComponent()
{
this.Title = new System.Windows.Forms.Label();
this.Body = new System.Windows.Forms.Label();
this.SuspendLayout();
Title = new Label();
Body = new Label();
SuspendLayout();
//
// Title
//
this.Title.Font = new System.Drawing.Font("Segoe UI", 24F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.Title.ForeColor = System.Drawing.Color.OrangeRed;
this.Title.Location = new System.Drawing.Point(5, 5);
this.Title.MaximumSize = new System.Drawing.Size(355, 0);
this.Title.Name = "Title";
this.Title.Size = new System.Drawing.Size(338, 45);
this.Title.TabIndex = 0;
this.Title.Text = "Title";
Title.Font = new Font("Segoe UI", 24F, FontStyle.Regular, GraphicsUnit.Point);
Title.ForeColor = Color.OrangeRed;
Title.Location = new Point(5, 5);
Title.MaximumSize = new Size(355, 0);
Title.Name = "Title";
Title.Size = new Size(338, 45);
Title.TabIndex = 0;
Title.Text = "Title";
//
// Body
//
this.Body.Anchor = ((System.Windows.Forms.AnchorStyles)(((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Left)
| System.Windows.Forms.AnchorStyles.Right)));
this.Body.AutoSize = true;
this.Body.Font = new System.Drawing.Font("Segoe UI", 14.25F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point);
this.Body.ForeColor = System.Drawing.Color.OrangeRed;
this.Body.Location = new System.Drawing.Point(12, 45);
this.Body.MaximumSize = new System.Drawing.Size(320, 85);
this.Body.Name = "Body";
this.Body.Size = new System.Drawing.Size(51, 31);
this.Body.TabIndex = 1;
this.Body.Text = "Body";
this.Body.UseCompatibleTextRendering = true;
Body.Anchor = AnchorStyles.Top | AnchorStyles.Left | AnchorStyles.Right;
Body.AutoSize = true;
Body.Font = new Font("Segoe UI", 14.25F, FontStyle.Regular, GraphicsUnit.Point);
Body.ForeColor = Color.OrangeRed;
Body.Location = new Point(12, 45);
Body.MaximumSize = new Size(320, 85);
Body.Name = "Body";
Body.Size = new Size(51, 31);
Body.TabIndex = 1;
Body.Text = "Body";
Body.UseCompatibleTextRendering = true;
//
// NotificationForm
//
this.AutoScaleDimensions = new System.Drawing.SizeF(7F, 15F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.BackColor = System.Drawing.Color.FromArgb(((int)(((byte)(64)))), ((int)(((byte)(64)))), ((int)(((byte)(64)))));
this.ClientSize = new System.Drawing.Size(355, 145);
this.ControlBox = false;
this.Controls.Add(this.Body);
this.Controls.Add(this.Title);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.None;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "NotificationForm";
this.ShowIcon = false;
this.ShowInTaskbar = false;
this.Text = "NotificationForm";
this.ResumeLayout(false);
this.PerformLayout();
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
BackColor = Color.FromArgb(64, 64, 64);
ClientSize = new Size(355, 145);
ControlBox = false;
Controls.Add(Body);
Controls.Add(Title);
Enabled = false;
FormBorderStyle = FormBorderStyle.None;
MaximizeBox = false;
MinimizeBox = false;
Name = "NotificationForm";
ShowIcon = false;
ShowInTaskbar = false;
Text = "NotificationForm";
ResumeLayout(false);
PerformLayout();
}
#endregion

View File

@ -36,6 +36,7 @@ namespace Observatory.UI
{
_guid = guid;
_color = Color.FromArgb((int)Properties.Core.Default.NativeNotifyColour);
CreationTime = DateTime.Now;
InitializeComponent();
Title.Paint += DrawText;
@ -65,7 +66,7 @@ namespace Observatory.UI
Body.ForeColor = _color;
Body.Text = args.Detail;
Body.Font = new Font(Properties.Core.Default.NativeNotifyFont, 14);
this.Paint += DrawBorder;
Paint += DrawBorder;
AdjustPosition(args.XPos / 100, args.YPos / 100);
@ -78,11 +79,37 @@ namespace Observatory.UI
}
}
private void NotificationForm_FormClosed(object? sender, FormClosedEventArgs e)
{
throw new NotImplementedException();
}
public DateTime CreationTime { get; private init; }
public void Update(NotificationArgs notificationArgs)
{
// Catch Cross-thread access and invoke
try
{
Title.Text = notificationArgs.Title;
Body.Text = notificationArgs.Detail;
}
catch
{
try
{
Invoke(() =>
{
Title.Text = notificationArgs.Title;
Body.Text = notificationArgs.Detail;
});
}
catch (Exception ex)
{
throw new Exception("Notification Update Failure, please inform Vithigar. Details: " + ex.Message);
}
}
}
private void AdjustPosition(double x = -1.0, double y = -1.0)
{
@ -90,7 +117,6 @@ namespace Observatory.UI
int corner = Properties.Core.Default.NativeNotifyCorner;
Rectangle screenBounds;
if (screen == -1 || screen > Screen.AllScreens.Length)
if (Screen.AllScreens.Length == 1)
screenBounds = Screen.GetBounds(this);
@ -115,7 +141,7 @@ namespace Observatory.UI
case 0:
Location = Point.Add(
new Point(screenBounds.Right, screenBounds.Bottom),
new Size(-(Width+50), -(Height+50)));
new Size(-(Width + 50), -(Height + 50)));
break;
case 1:
Location = Point.Add(
@ -190,13 +216,29 @@ namespace Observatory.UI
public Guid Guid { get => _guid; }
private void AdjustText()
public void AdjustOffset(bool increase)
{
if (_defaultPosition)
{
if (increase || Location != _originalLocation)
{
var corner = Properties.Core.Default.NativeNotifyCorner;
if ((corner >= 2 && increase) || (corner <= 1 && !increase))
{
Location = new Point(Location.X, Location.Y + Height);
}
else
{
Location = new Point(Location.X, Location.Y - Height);
}
}
}
}
private void CloseNotification(object? sender, System.Timers.ElapsedEventArgs e)
{
// Catch Cross-thread access and invoke
try
{
Close();
@ -205,11 +247,11 @@ namespace Observatory.UI
{
try
{
this.Invoke(() => Close());
Invoke(() => Close());
}
catch
catch (Exception ex)
{
throw new Exception("blah");
throw new Exception("Notification Close Failure, please inform Vithigar. Details: " + ex.Message);
}
}

View File

@ -1,4 +1,64 @@
<root>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">

View File

@ -1,7 +1,14 @@
using Observatory.Framework.Interfaces;
using Observatory.Framework;
using System.Collections;
using Observatory.PluginManagement;
using Observatory.Utils;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Reflection;
using System.Text.Json;
using System.Data.Common;
using System.ComponentModel.Design.Serialization;
namespace Observatory.UI
{
@ -39,6 +46,7 @@ namespace Observatory.UI
Font = menu.Items[0].Font,
TextAlign = menu.Items[0].TextAlign
};
ThemeManager.GetInstance.RegisterControl(newItem);
menu.Items.Add(newItem);
if (plugin.PluginUI.PluginUIType == Framework.PluginUI.UIType.Basic)
@ -49,7 +57,10 @@ namespace Observatory.UI
private static Panel CreateBasicUI(IObservatoryPlugin plugin)
{
Panel panel = new();
Panel panel = new()
{
Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom | AnchorStyles.Top
};
IObservatoryComparer columnSorter;
if (plugin.ColumnSorter != null)
@ -57,7 +68,7 @@ namespace Observatory.UI
else
columnSorter = new DefaultSorter();
ListView listView = new()
PluginListView listView = new()
{
View = View.Details,
Location = new Point(0, 0),
@ -65,15 +76,119 @@ namespace Observatory.UI
Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom | AnchorStyles.Top,
BackColor = Color.FromArgb(64, 64, 64),
ForeColor = Color.LightGray,
GridLines = true,
ListViewItemSorter = columnSorter,
Font = new Font(new FontFamily("Segoe UI"), 10, FontStyle.Regular)
};
string colSize = Properties.Core.Default.ColumnSizing;
List<ColumnSizing>? columnSizing = null;
if (!string.IsNullOrWhiteSpace(colSize))
{
try
{
columnSizing = JsonSerializer.Deserialize<List<ColumnSizing>>(colSize);
}
catch
{
// Failed deserialization means bad value, blow it away.
Properties.Core.Default.ColumnSizing = string.Empty;
Properties.Core.Default.Save();
}
}
columnSizing ??= new List<ColumnSizing>();
// Is losing column sizes between versions acceptable?
ColumnSizing pluginColumnSizing = columnSizing
.Where(c => c.PluginName == plugin.Name && c.PluginVersion == plugin.Version)
.FirstOrDefault(new ColumnSizing() { PluginName = plugin.Name, PluginVersion = plugin.Version });
if (!columnSizing.Contains(pluginColumnSizing))
{
columnSizing.Add(pluginColumnSizing);
}
foreach (var property in plugin.PluginUI.DataGrid.First().GetType().GetProperties())
{
listView.Columns.Add(property.Name);
// https://stackoverflow.com/questions/5796383/insert-spaces-between-words-on-a-camel-cased-token
string columnLabel = Regex.Replace(
Regex.Replace(
property.Name,
@"(\P{Ll})(\P{Ll}\p{Ll})",
"$1 $2"
),
@"(\p{Ll})(\P{Ll})",
"$1 $2"
);
int width;
if (pluginColumnSizing.ColumnWidth.ContainsKey(columnLabel))
{
width = pluginColumnSizing.ColumnWidth[columnLabel];
}
else
{
var widthAttrib = property.GetCustomAttribute<ColumnSuggestedWidth>();
width = widthAttrib == null
// Rough approximation of width by label length if none specified.
? columnLabel.Length * 10
: widthAttrib.Width;
pluginColumnSizing.ColumnWidth.Add(columnLabel, width);
}
listView.Columns.Add(columnLabel, width);
}
Properties.Core.Default.ColumnSizing = JsonSerializer.Serialize(columnSizing);
Properties.Core.Default.Save();
// Oddly, the listview resize event often fires after the column size change but
// with stale (default?!) column width values.
// Still need a resize handler to avoid the ugliness of the rightmost column
// leaving gaps, but preventing saving the width changes there should stop the
// stale resize event from overwriting with bad data.
// Using a higher-order function here to create two different versions of the
// event handler for these purposes.
var handleColSize = (bool saveProps) =>
(object? sender, EventArgs e) =>
{
int colTotalWidth = 0;
ColumnHeader? rightmost = null;
foreach (ColumnHeader column in listView.Columns)
{
colTotalWidth += column.Width;
if (rightmost == null || column.DisplayIndex > rightmost.DisplayIndex)
rightmost = column;
if (saveProps)
{
if (pluginColumnSizing.ColumnWidth.ContainsKey(column.Text))
pluginColumnSizing.ColumnWidth[column.Text] = column.Width;
else
pluginColumnSizing.ColumnWidth.Add(column.Text, column.Width);
}
}
if (rightmost != null && colTotalWidth < listView.Width)
{
rightmost.Width = listView.Width - (colTotalWidth - rightmost.Width);
if (saveProps)
pluginColumnSizing.ColumnWidth[rightmost.Text] = rightmost.Width;
}
if (saveProps)
{
Properties.Core.Default.ColumnSizing = JsonSerializer.Serialize(columnSizing);
Properties.Core.Default.Save();
}
};
listView.ColumnWidthChanged += handleColSize(true).Invoke;
listView.Resize += handleColSize(false).Invoke;
listView.ColumnClick += (sender, e) =>
{
@ -102,7 +217,7 @@ namespace Observatory.UI
plugin.PluginUI.DataGrid.CollectionChanged += (sender, e) =>
{
listView.Invoke(() =>
var updateGrid = () =>
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add &&
e.NewItems != null)
@ -153,7 +268,16 @@ namespace Observatory.UI
listView.Items.Add(listItem);
}
}
});
};
if (listView.Created)
{
listView.Invoke(updateGrid);
}
else
{
updateGrid();
}
};
return panel;

View File

@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Security.Permissions;
using System.Text;
using System.Threading.Tasks;
namespace Observatory.UI
{
internal class PluginListView : ListView
{
[DllImport("user32.dll")]
private static extern int SendMessage(IntPtr hWnd, int wMsg, bool wParam, int lParam);
private const int WM_SETREDRAW = 11;
public PluginListView()
{
OwnerDraw = true;
GridLines = false;
DrawItem += PluginListView_DrawItem;
DrawSubItem += PluginListView_DrawSubItem;
DrawColumnHeader += PluginListView_DrawColumnHeader;
DoubleBuffered = true;
base.GridLines = false;//We should prevent the default drawing of gridlines.
}
private static void DrawBorder(Graphics graphics, Pen pen, Rectangle bounds, bool header = false)
{
Point topRight = new(bounds.Right, bounds.Top);
Point bottomRight = new(bounds.Right, bounds.Bottom);
graphics.DrawLine(pen, topRight, bottomRight);
if (header)
{
Point bottomLeft = new(bounds.Left, bounds.Bottom);
// Point topLeft = new(bounds.Left, bounds.Top);
// graphics.DrawLine(pen, topLeft, topRight);
// graphics.DrawLine(pen, topLeft, bottomLeft);
graphics.DrawLine(pen, bottomLeft, bottomRight);
}
}
private void PluginListView_DrawColumnHeader(object? sender, DrawListViewColumnHeaderEventArgs e)
{
using (var g = e.Graphics)
if (g != null)
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Pen pen = new(new SolidBrush(Color.LightGray));
DrawBorder(g, pen, e.Bounds);
using (var font = new Font(this.Font, FontStyle.Bold))
{
Brush textBrush = new SolidBrush(ForeColor);
g.DrawString(e.Header?.Text, font, textBrush, e.Bounds);
}
}
}
private void PluginListView_DrawSubItem(object? sender, DrawListViewSubItemEventArgs e)
{
using (var g = e.Graphics)
if (g != null)
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Pen pen = new(new SolidBrush(Color.LightGray));
DrawBorder(g, pen, e.Bounds, false);
e.DrawText();
}
}
private void PluginListView_DrawItem(object? sender, DrawListViewItemEventArgs e)
{
var offsetColor = (int value) =>
{
if (value > 127)
{
return value - 20;
}
else
{
return value + 20;
}
};
using (var g = e.Graphics)
{
if (e.ItemIndex % 2 == 0)
{
e.Item.BackColor = BackColor;
}
else
{
e.Item.BackColor = Color.FromArgb(offsetColor(BackColor.R), offsetColor(BackColor.G), offsetColor(BackColor.B));
}
if (g != null)
{
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
Pen pen = new(new SolidBrush(Color.LightGray));
e.DrawBackground();
}
}
}
}
}

View File

@ -0,0 +1,84 @@
namespace Observatory.UI
{
partial class ReadAllForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
ReadAllProgress = new ProgressBar();
JournalLabel = new Label();
CancelButton = new Button();
SuspendLayout();
//
// ReadAllProgress
//
ReadAllProgress.Location = new Point(12, 27);
ReadAllProgress.Name = "ReadAllProgress";
ReadAllProgress.Size = new Size(371, 23);
ReadAllProgress.Step = 1;
ReadAllProgress.TabIndex = 0;
//
// JournalLabel
//
JournalLabel.AutoSize = true;
JournalLabel.Location = new Point(12, 9);
JournalLabel.Name = "JournalLabel";
JournalLabel.Size = new Size(45, 15);
JournalLabel.TabIndex = 1;
JournalLabel.Text = "foo.log";
//
// CancelButton
//
CancelButton.Location = new Point(308, 56);
CancelButton.Name = "CancelButton";
CancelButton.Size = new Size(75, 23);
CancelButton.TabIndex = 2;
CancelButton.Text = "Cancel";
CancelButton.UseVisualStyleBackColor = true;
CancelButton.Click += CancelButton_Click;
//
// ReadAllForm
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(395, 86);
Controls.Add(CancelButton);
Controls.Add(JournalLabel);
Controls.Add(ReadAllProgress);
FormBorderStyle = FormBorderStyle.FixedDialog;
Name = "ReadAllForm";
Text = "Read All In Progress...";
ResumeLayout(false);
PerformLayout();
}
#endregion
private ProgressBar ReadAllProgress;
private Label JournalLabel;
private Button CancelButton;
}
}

View File

@ -0,0 +1,52 @@
using Observatory.Utils;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Observatory.UI
{
public partial class ReadAllForm : Form
{
private CancellationTokenSource ReadAllCancel;
public ReadAllForm()
{
InitializeComponent();
var ReadAllJournals = LogMonitor.GetInstance.ReadAllGenerator(out int fileCount);
int progressCount = 0;
ReadAllCancel = new CancellationTokenSource();
HandleCreated += (_,_) =>
Task.Run(() =>
{
foreach (var journal in ReadAllJournals())
{
if (ReadAllCancel.IsCancellationRequested)
{
break;
}
progressCount++;
Invoke(() =>
{
JournalLabel.Text = journal.ToString();
ReadAllProgress.Value = (progressCount * 100) / fileCount;
});
}
Invoke(()=>Close());
});
}
private void CancelButton_Click(object sender, EventArgs e)
{
ReadAllCancel.Cancel();
}
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -0,0 +1,61 @@
namespace Observatory.UI
{
partial class SettingsForm
{
/// <summary>
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed; otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
PluginSettingsCloseButton = new Button();
SuspendLayout();
//
// PluginSettingsCloseButton
//
PluginSettingsCloseButton.Anchor = AnchorStyles.Bottom | AnchorStyles.Right;
PluginSettingsCloseButton.Location = new Point(339, 5);
PluginSettingsCloseButton.Name = "PluginSettingsCloseButton";
PluginSettingsCloseButton.Size = new Size(75, 23);
PluginSettingsCloseButton.TabIndex = 0;
PluginSettingsCloseButton.Text = "Close";
PluginSettingsCloseButton.UseVisualStyleBackColor = true;
PluginSettingsCloseButton.Click += PluginSettingsCloseButton_Click;
//
// SettingsForm
//
AutoScaleDimensions = new SizeF(7F, 15F);
AutoScaleMode = AutoScaleMode.Font;
ClientSize = new Size(426, 40);
Controls.Add(PluginSettingsCloseButton);
FormBorderStyle = FormBorderStyle.FixedSingle;
Name = "SettingsForm";
Text = "SettingsForm";
ResumeLayout(false);
}
#endregion
private Button PluginSettingsCloseButton;
}
}

View File

@ -0,0 +1,367 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Observatory.Assets;
using Observatory.Framework;
using Observatory.Framework.Interfaces;
namespace Observatory.UI
{
public partial class SettingsForm : Form
{
private readonly IObservatoryPlugin _plugin;
private readonly List<int> _colHeight = new List<int>();
public SettingsForm(IObservatoryPlugin plugin)
{
InitializeComponent();
_plugin = plugin;
// Filtered to only settings without SettingIgnore attribute
var settings = PluginManagement.PluginManager.GetSettingDisplayNames(plugin.Settings).Where(s => !Attribute.IsDefined(s.Key, typeof(SettingIgnore)));
CreateControls(settings);
Text = plugin.Name + " Settings";
Icon = Resources.EOCIcon_Presized;
ThemeManager.GetInstance.RegisterControl(this);
}
private void CreateControls(IEnumerable<KeyValuePair<PropertyInfo, string>> settings)
{
bool recentHalfCol = false;
int settingsHeight = 0;
var trackBottomEdge = (Control control) =>
{
var controlBottom = control.Location.Y + control.Height;
if (controlBottom > settingsHeight)
settingsHeight = controlBottom;
};
foreach (var setting in settings)
{
// Reset the column tracking for checkboxes if this isn't a checkbox
if (setting.Key.PropertyType.Name != "Boolean")
recentHalfCol = false;
int addedHeight = 29;
switch (setting.Key.GetValue(_plugin.Settings))
{
case bool:
var checkBox = CreateBoolSetting(setting);
addedHeight = recentHalfCol ? 0 : addedHeight;
checkBox.Location = GetSettingPosition(recentHalfCol);
recentHalfCol = !recentHalfCol;
Controls.Add(checkBox);
trackBottomEdge(checkBox);
break;
case string:
var stringLabel = CreateSettingLabel(setting.Value);
var textBox = CreateStringSetting(setting.Key);
stringLabel.Location = GetSettingPosition();
textBox.Location = GetSettingPosition(true);
Controls.Add(stringLabel);
Controls.Add(textBox);
trackBottomEdge(textBox);
break;
case FileInfo:
var fileLabel = CreateSettingLabel(setting.Value);
var pathTextBox = CreateFilePathSetting(setting.Key);
var pathButton = CreateFileBrowseSetting(setting.Key, pathTextBox);
fileLabel.Location = GetSettingPosition();
pathTextBox.Location = GetSettingPosition(true);
_colHeight.Add(addedHeight);
pathButton.Location = GetSettingPosition(true);
Controls.Add(fileLabel);
Controls.Add(pathTextBox);
Controls.Add(pathButton);
trackBottomEdge(pathButton);
break;
case int:
// We have two options for integer values:
// 1) A slider (explicit by way of the SettingIntegerUseSlider attribute and bounded to 0..100 by default)
// 2) A numeric up/down (default otherwise, and is unbounded by default).
// Bounds for both can be set via the SettingNumericBounds attribute, only the up/down uses Increment.
var intLabel = CreateSettingLabel(setting.Value);
Control intControl;
if (Attribute.IsDefined(setting.Key, typeof(SettingNumericUseSlider)))
{
intControl = CreateSettingTrackbar(setting.Key);
}
else
{
intControl = CreateSettingNumericUpDown(setting.Key);
}
intLabel.Location = GetSettingPosition();
intControl.Location = GetSettingPosition(true);
addedHeight = intControl.Height;
intLabel.Height = intControl.Height;
intLabel.TextAlign = ContentAlignment.MiddleRight;
Controls.Add(intLabel);
Controls.Add(intControl);
trackBottomEdge(intControl);
break;
case Action action:
var button = CreateSettingButton(setting.Value, action);
button.Location = GetSettingPosition();
Controls.Add(button);
trackBottomEdge(button);
break;
case Dictionary<string, object> dictSetting:
var dictLabel = CreateSettingLabel(setting.Value);
var dropdown = CreateSettingDropdown(setting.Key, dictSetting);
dictLabel.Location = GetSettingPosition();
dropdown.Location = GetSettingPosition(true);
Controls.Add(dictLabel);
Controls.Add(dropdown);
trackBottomEdge(dropdown);
break;
default:
break;
}
_colHeight.Add(addedHeight);
}
Height = settingsHeight + 80;
}
private Point GetSettingPosition(bool secondCol = false)
{
return new Point(10 + (secondCol ? 200 : 0), -26 + _colHeight.Sum());
}
private Label CreateSettingLabel(string settingName)
{
Label label = new()
{
Text = settingName + ": ",
TextAlign = System.Drawing.ContentAlignment.MiddleRight,
Width = 200,
ForeColor = Color.LightGray
};
return label;
}
private ComboBox CreateSettingDropdown(PropertyInfo setting, Dictionary<string, object> dropdownItems)
{
var backingValueName = (SettingBackingValue?)Attribute.GetCustomAttribute(setting, typeof(SettingBackingValue));
var backingValue = from s in PluginManagement.PluginManager.GetSettingDisplayNames(_plugin.Settings)
where s.Value == backingValueName?.BackingProperty
select s.Key;
if (backingValue.Count() != 1)
throw new($"{_plugin.ShortName}: Dictionary settings must have exactly one backing value.");
ComboBox comboBox = new()
{
Width = 200,
DropDownStyle = ComboBoxStyle.DropDownList
};
comboBox.Items.AddRange(dropdownItems.OrderBy(s => s.Key).Select(s => s.Key).ToArray());
string? currentSelection = backingValue.First().GetValue(_plugin.Settings)?.ToString();
if (currentSelection?.Length > 0)
{
comboBox.SelectedItem = currentSelection;
}
comboBox.SelectedValueChanged += (sender, e) =>
{
backingValue.First().SetValue(_plugin.Settings, comboBox.SelectedItem.ToString());
SaveSettings();
};
return comboBox;
}
private Button CreateSettingButton(string settingName, Action action)
{
Button button = new()
{
Text = settingName
};
button.Click += (sender, e) =>
{
action.Invoke();
SaveSettings();
};
return button;
}
private TrackBar CreateSettingTrackbar(PropertyInfo setting)
{
SettingNumericBounds? bounds = (SettingNumericBounds?)System.Attribute.GetCustomAttribute(setting, typeof(SettingNumericBounds));
var minBound = Convert.ToInt32(bounds?.Minimum ?? 0);
var maxBound = Convert.ToInt32(bounds?.Maximum ?? 100);
var tickFrequency = maxBound - minBound >= 20 ? (maxBound - minBound) / 10 : 1;
TrackBar trackBar = new()
{
Orientation = Orientation.Horizontal,
TickStyle = TickStyle.Both,
TickFrequency = tickFrequency,
Width = 200,
Minimum = minBound,
Maximum = maxBound,
};
trackBar.Value = (int?)setting.GetValue(_plugin.Settings) ?? 0;
trackBar.ValueChanged += (sender, e) =>
{
setting.SetValue(_plugin.Settings, trackBar.Value);
SaveSettings();
};
return trackBar;
}
private NumericUpDown CreateSettingNumericUpDown(PropertyInfo setting)
{
SettingNumericBounds? bounds = (SettingNumericBounds?)System.Attribute.GetCustomAttribute(setting, typeof(SettingNumericBounds));
NumericUpDown numericUpDown = new()
{
Width = 200,
Minimum = Convert.ToInt32(bounds?.Minimum ?? Int32.MinValue),
Maximum = Convert.ToInt32(bounds?.Maximum ?? Int32.MaxValue),
Increment = Convert.ToInt32(bounds?.Increment ?? 1)
};
numericUpDown.Value = (int?)setting.GetValue(_plugin.Settings) ?? 0;
numericUpDown.ValueChanged += (sender, e) =>
{
setting.SetValue(_plugin.Settings, numericUpDown.Value);
SaveSettings();
};
return numericUpDown;
}
private CheckBox CreateBoolSetting(KeyValuePair<PropertyInfo, string> setting)
{
CheckBox checkBox = new()
{
Text = setting.Value,
TextAlign = System.Drawing.ContentAlignment.MiddleLeft,
Checked = (bool?)setting.Key.GetValue(_plugin.Settings) ?? false,
Width = 200,
ForeColor = Color.LightGray
};
checkBox.CheckedChanged += (sender, e) =>
{
setting.Key.SetValue(_plugin.Settings, checkBox.Checked);
SaveSettings();
};
return checkBox;
}
private TextBox CreateStringSetting(PropertyInfo setting)
{
TextBox textBox = new()
{
Text = (setting.GetValue(_plugin.Settings) ?? String.Empty).ToString(),
Width = 200
};
textBox.TextChanged += (object? sender, EventArgs e) =>
{
setting.SetValue(_plugin.Settings, textBox.Text);
SaveSettings();
};
return textBox;
}
private TextBox CreateFilePathSetting(PropertyInfo setting)
{
var fileInfo = (FileInfo?)setting.GetValue(_plugin.Settings);
TextBox textBox = new()
{
Text = fileInfo?.FullName ?? string.Empty,
Width = 200
};
textBox.TextChanged += (object? sender, EventArgs e) =>
{
setting.SetValue(_plugin.Settings, new FileInfo(textBox.Text));
SaveSettings();
};
return textBox;
}
private Button CreateFileBrowseSetting(PropertyInfo setting, TextBox textBox)
{
Button button = new()
{
Text = "Browse"
};
button.Click += (object? sender, EventArgs e) =>
{
var currentDir = ((FileInfo?)setting.GetValue(_plugin.Settings))?.DirectoryName;
OpenFileDialog ofd = new OpenFileDialog()
{
Title = "Select File...",
Filter = "Lua files (*.lua)|*.lua|All files (*.*)|*.*",
FilterIndex = 0,
InitialDirectory = currentDir ?? Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments)
};
var browseResult = ofd.ShowDialog();
if (browseResult == DialogResult.OK)
{
textBox.Text = ofd.FileName;
}
};
return button;
}
private void SaveSettings()
{
PluginManagement.PluginManager.GetInstance.SaveSettings(_plugin, _plugin.Settings);
}
private void PluginSettingsCloseButton_Click(object sender, EventArgs e)
{
Close();
}
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
</root>

View File

@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace Observatory.UI
{
internal class ThemeManager
{
public static ThemeManager GetInstance
{
get
{
return _instance.Value;
}
}
private static readonly Lazy<ThemeManager> _instance = new(() => new ThemeManager());
private bool _init;
private ThemeManager()
{
_init = true;
controls = new List<Control>();
Themes = new()
{
{ "Dark", DarkTheme },
{ "Light", LightTheme }
};
SelectedTheme = "Dark";
}
private readonly List<Control> controls;
public List<string> GetThemes
{
get => Themes.Keys.ToList();
}
public string CurrentTheme
{
get => SelectedTheme;
set
{
if (Themes.ContainsKey(value))
{
SelectedTheme = value;
foreach (var control in controls)
{
ApplyTheme(control);
}
}
}
}
public void RegisterControl(Control control)
{
// First time registering a control, build the "light" theme based
// on defaults.
if (_init)
{
SaveTheme(control, LightTheme);
_init = false;
}
controls.Add(control);
ApplyTheme(control);
if (control.HasChildren)
foreach (Control child in control.Controls)
{
RegisterControl(child);
}
}
// This doesn't inherit from Control? Seriously?
public void RegisterControl(ToolStripMenuItem toolStripMenuItem)
{
ApplyTheme(toolStripMenuItem);
}
private void SaveTheme(Control control, Dictionary<string, Color> theme)
{
Control rootControl = control;
while (rootControl.Parent != null)
{
rootControl = rootControl.Parent;
}
SaveThemeControl(rootControl, theme);
var themeJson = System.Text.Json.JsonSerializer.Serialize(DarkTheme);
}
private void SaveThemeControl(Control control, Dictionary<string, Color> theme)
{
var properties = control.GetType().GetProperties();
var colorProperties = properties.Where(p => p.PropertyType == typeof(Color));
foreach (var colorProperty in colorProperties)
{
string controlKey = control.GetType().Name + "." + colorProperty.Name;
if (!theme.ContainsKey(controlKey))
{
theme.Add(controlKey, (Color)colorProperty.GetValue(control)!);
}
}
foreach (Control child in control.Controls)
{
SaveThemeControl(child, theme);
}
}
public void DeRegisterControl(Control control)
{
if (control.HasChildren)
foreach (Control child in control.Controls)
{
DeRegisterControl(child);
}
controls.Remove(control);
}
private void ApplyTheme(Object control)
{
var controlType = control.GetType();
var theme = Themes.ContainsKey(SelectedTheme)
? Themes[SelectedTheme] : Themes["Light"];
foreach (var property in controlType.GetProperties().Where(p => p.PropertyType == typeof(Color)))
{
string themeControl = Themes[SelectedTheme].ContainsKey(controlType.Name + "." + property.Name)
? controlType.Name
: "Default";
if (Themes[SelectedTheme].ContainsKey(themeControl + "." + property.Name))
property.SetValue(control, Themes[SelectedTheme][themeControl + "." + property.Name]);
}
}
private Dictionary<string, Dictionary<string, Color>> Themes;
private string SelectedTheme;
private Dictionary<string, Color> LightTheme = new Dictionary<string, Color>();
static private Dictionary<string, Color> DarkTheme = new Dictionary<string, Color>
{
{"Default.ForeColor", Color.LightGray },
{"Default.BackColor", Color.Black },
{"Button.ForeColor", Color.LightGray },
{"Button.BackColor", Color.DimGray }
};
}
}

View File

@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NAudio.Wave;
namespace Observatory.Utils
{
internal static class AudioHandler
{
internal static async Task PlayFile(string filePath)
{
await Task.Run(() =>
{
using (var file = new AudioFileReader(filePath))
using (var output = new WaveOutEvent())
{
output.Init(file);
output.Play();
while (output.PlaybackState == PlaybackState.Playing)
{
Thread.Sleep(250);
}
};
});
}
}
}

View File

@ -22,7 +22,7 @@ namespace Observatory.Utils
}
}
private static readonly Lazy<LogMonitor> _instance = new Lazy<LogMonitor>(NewLogMonitor);
private static readonly Lazy<LogMonitor> _instance = new(NewLogMonitor);
private static LogMonitor NewLogMonitor()
{
@ -44,6 +44,9 @@ namespace Observatory.Utils
{
get => currentState;
}
public Status Status { get; private set; }
#endregion
#region Public Methods
@ -87,28 +90,31 @@ namespace Observatory.Utils
return LogMonitorStateChangedEventArgs.IsBatchRead(currentState);
}
public void ReadAllJournals()
{
ReadAllJournals(string.Empty);
}
public void ReadAllJournals(string path)
public Func<IEnumerable<string>> ReadAllGenerator(out int fileCount)
{
// Prevent pre-reading when starting monitoring after reading all.
firstStartMonitor = false;
SetLogMonitorState(currentState | LogMonitorState.Batch);
DirectoryInfo logDirectory = GetJournalFolder(path);
DirectoryInfo logDirectory = GetJournalFolder();
var files = GetJournalFilesOrdered(logDirectory);
fileCount = files.Count();
IEnumerable<string> ReadAllJournals()
{
var readErrors = new List<(Exception ex, string file, string line)>();
foreach (var file in files)
{
yield return file.Name;
readErrors.AddRange(
ProcessLines(ReadAllLines(file.FullName), file.Name));
}
ReportErrors(readErrors);
SetLogMonitorState(currentState & ~LogMonitorState.Batch);
};
return ReadAllJournals;
}
public void PrereadJournals()
@ -187,13 +193,13 @@ namespace Observatory.Utils
#region Private Fields
private FileSystemWatcher journalWatcher;
private FileSystemWatcher statusWatcher;
private Dictionary<string, Type> journalTypes;
private Dictionary<string, int> currentLine;
private FileSystemWatcher? journalWatcher;
private FileSystemWatcher? statusWatcher;
private readonly Dictionary<string, Type> journalTypes;
private readonly Dictionary<string, int> currentLine;
private LogMonitorState currentState = LogMonitorState.Idle; // Change via #SetLogMonitorState
private bool firstStartMonitor = true;
private string[] EventsWithAncillaryFile = new string[]
private readonly string[] EventsWithAncillaryFile = new string[]
{
"Cargo",
"NavRoute",
@ -242,7 +248,7 @@ namespace Observatory.Utils
statusWatcher.Changed += StatusUpdateEvent;
}
private DirectoryInfo GetJournalFolder(string path = "")
private static DirectoryInfo GetJournalFolder(string path = "")
{
DirectoryInfo logDirectory;
@ -370,7 +376,7 @@ namespace Observatory.Utils
}
}
private void ReportErrors(List<(Exception ex, string file, string line)> readErrors)
private static void ReportErrors(List<(Exception ex, string file, string line)> readErrors)
{
if (readErrors.Any())
{
@ -410,24 +416,22 @@ namespace Observatory.Utils
}
catch (Exception ex)
{
ReportErrors(new List<(Exception ex, string file, string line)>() { (ex, eventArgs.Name, line) });
ReportErrors(new List<(Exception ex, string file, string line)>() { (ex, eventArgs.Name ?? string.Empty, line) });
}
}
currentLine[eventArgs.FullPath] = fileContent.Count;
}
private List<string> ReadAllLines(string path)
private static List<string> ReadAllLines(string path)
{
var lines = new List<string>();
try
{
using (StreamReader file = new StreamReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)))
{
using StreamReader file = new(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite));
while (!file.EndOfStream)
{
lines.Add(file.ReadLine());
}
lines.Add(file.ReadLine() ?? string.Empty);
}
}
catch (IOException ioEx)
@ -450,6 +454,7 @@ namespace Observatory.Utils
if (statusLines.Count > 0)
{
var status = JournalReader.ObservatoryDeserializer<Status>(statusLines[0]);
Status = status;
handler?.Invoke(this, new JournalEventArgs() { journalType = typeof(Status), journalEvent = status });
}
}
@ -486,9 +491,9 @@ namespace Observatory.Utils
IntPtr pathPtr = IntPtr.Zero;
try
{
Guid FolderSavedGames = new Guid("4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4");
Guid FolderSavedGames = new ("4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4");
SHGetKnownFolderPath(ref FolderSavedGames, 0, IntPtr.Zero, out pathPtr);
return Marshal.PtrToStringUni(pathPtr);
return Marshal.PtrToStringUni(pathPtr) ?? string.Empty;
}
finally
{
@ -496,7 +501,7 @@ namespace Observatory.Utils
}
}
private IEnumerable<FileInfo> GetJournalFilesOrdered(DirectoryInfo journalFolder)
private static IEnumerable<FileInfo> GetJournalFilesOrdered(DirectoryInfo journalFolder)
{
return from file in journalFolder.GetFiles("Journal.*.??.log")
orderby file.LastWriteTime

View File

@ -1,16 +1,19 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Observatory.Framework;
namespace Observatory.Explorer
{
public class ExplorerUIResults
{
[ColumnSuggestedWidth(150)]
public string Time { get; set; }
[ColumnSuggestedWidth(150)]
public string BodyName { get; set; }
[ColumnSuggestedWidth(200)]
public string Description { get; set; }
[ColumnSuggestedWidth(200)]
public string Details { get; set; }
}
}

View File

@ -33,7 +33,7 @@
</Target>
<ItemGroup>
<PackageReference Include="NLua" Version="1.6.0" />
<PackageReference Include="NLua" Version="1.6.3" />
</ItemGroup>
<ItemGroup>

View File

@ -33,6 +33,20 @@ namespace Observatory.Framework
}
}
/// <summary>
/// Suggests default column width when building basic UI
/// </summary>
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Field)]
public class ColumnSuggestedWidth : Attribute
{
public ColumnSuggestedWidth(int width)
{
Width = width;
}
public int Width { get; }
}
/// <summary>
/// Indicates that the property should not be displayed to the user in the UI.
/// </summary>

View File

@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Text.Json;
using System.Threading.Tasks;
using System.Reflection.Metadata.Ecma335;
namespace Observatory.Framework.Files.Converters
{
class ThargoidWarRemainingTimeConverter : JsonConverter<int>
{
public override int Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType == JsonTokenType.String)
{
string value = reader.GetString();
int dayCount = Int32.TryParse(value.Split(' ')[0], out int days)
? days
: 0;
return dayCount;
}
else
return reader.GetInt32();
}
public override void Write(Utf8JsonWriter writer, int value, JsonSerializerOptions options)
{
writer.WriteStringValue(value.ToString() + " Days");
}
}
}

View File

@ -6,6 +6,8 @@ namespace Observatory.Framework.Files.Journal
public class Bounty : JournalBase
{
public ImmutableList<Rewards> Rewards { get; init; }
public string PilotName { get; set; }
public string PilotName_Localised { get; set; }
public string Target { get; init; }
public string Target_Localised { get; init; }
public string Faction { get; init; }

View File

@ -4,5 +4,6 @@
{
public string Interdictor { get; init; }
public bool IsPlayer { get; init; }
public bool IsThargoid { get; init; }
}
}

View File

@ -9,5 +9,6 @@
public int CombatRank { get; init; }
public string Faction { get; init; }
public string Power { get; init; }
public bool IsThargoid { get; init; }
}
}

View File

@ -14,6 +14,10 @@
/// </summary>
public string SignalName_Localised { get; init; }
/// <summary>
/// Type of signal location.
/// </summary>
public string SignalType { get; init; }
/// <summary>
/// Faction state or circumstance that caused this signal to appear.
/// </summary>
public string SpawningState { get; init; }

View File

@ -8,6 +8,10 @@ namespace Observatory.Framework.Files.Journal
public class CarrierJump : FSDJump
{
public bool Docked { get; init; }
public bool OnFoot { get; init; }
/// <summary>
/// Name of the station at which this event occurred.
/// </summary>
public string StationName { get; init; }
public string StationType { get; init; }
public ulong MarketID { get; init; }

View File

@ -18,6 +18,9 @@ namespace Observatory.Framework.Files.Journal
public int BodyID { get; init; }
public bool OnStation { get; init; }
public bool OnPlanet { get; init; }
/// <summary>
/// Name of the station at which this event occurred.
/// </summary>
public string StationName { get; init; }
public string StationType { get; init; }
public ulong MarketID { get; init; }

View File

@ -11,6 +11,8 @@ namespace Observatory.Framework.Files.Journal
public string Genus_Localised { get; init; }
public string Species { get; init; }
public string Species_Localised { get; init; }
public string Variant { get; init; }
public string Variant_Localised { get; init; }
public ulong SystemAddress { get; init; }
public int Body { get; init; }
}

View File

@ -1,4 +1,9 @@
namespace Observatory.Framework.Files.Journal
using Observatory.Framework.Files.Converters;
using Observatory.Framework.Files.ParameterTypes;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace Observatory.Framework.Files.Journal
{
public class ApproachSettlement : JournalBase
{
@ -10,5 +15,13 @@
public float Longitude { get; init; }
public int BodyID { get; init; }
public string BodyName { get; init; }
public ImmutableList<StationEconomy> StationEconomies { get; init; }
public string StationEconomy { get; init; }
public string StationEconomy_Localised { get; init; }
public Faction StationFaction { get; init; }
public string StationGovernment { get; init; }
public string StationGovernment_Localised { get; init; }
[JsonConverter(typeof(StationServiceConverter))]
public StationService StationServices { get; init; }
}
}

View File

@ -1,11 +1,27 @@
using Observatory.Framework.Files.ParameterTypes;
using System.Collections.Immutable;
using System.Text.Json.Serialization;
namespace Observatory.Framework.Files.Journal
{
public class Passengers : JournalBase
{
public ImmutableList<Passenger> Manifest { get; init; }
[JsonPropertyName("Passengers_Missions_Accepted")]
public int PassengersMissionsAccepted { get; init; }
[JsonPropertyName("Passengers_Missions_Bulk")]
public int PassengersMissionsBulk { get; init; }
[JsonPropertyName("Passengers_Missions_Delivered")]
public int PassengersMissionsDelivered { get; init; }
[JsonPropertyName("Passengers_Missions_Disgruntled")]
public int PassengersMissionsDisgruntled { get; init; }
[JsonPropertyName("Passengers_Missions_Ejected")]
public int PassengersMissionsEjected { get; init; }
[JsonPropertyName("Passengers_Missions_VIP")]
public int PassengersMissionsVip { get; init; }
}
}

View File

@ -27,5 +27,6 @@ namespace Observatory.Framework.Files.Journal
public CQC CQC { get; init; }
[JsonPropertyName("FLEETCARRIER")]
public FleetCarrier FleetCarrier { get; init; }
public Exobiology Exobiology { get; init; }
}
}

View File

@ -0,0 +1,11 @@
namespace Observatory.Framework.Files.Journal
{
public class ClearImpound : JournalBase
{
public string ShipType { get; init; }
public string ShipType_Localised { get; init; }
public ulong ShipID { get; init; }
public ulong ShipMarketID { get; init; }
public ulong MarketID { get; init; }
}
}

View File

@ -7,6 +7,9 @@ namespace Observatory.Framework.Files.Journal
public class Market : JournalBase
{
public ulong MarketID { get; init; }
/// <summary>
/// Name of the station at which this event occurred.
/// </summary>
public string StationName { get; init; }
public string StationType { get; init; }
public string StarSystem { get; init; }

View File

@ -5,7 +5,7 @@ namespace Observatory.Framework.Files.Journal
{
public class MassModuleStore : JournalBase
{
public long MarketID { get; init; }
public ulong MarketID { get; init; }
public string Ship { get; init; }
public ulong ShipID { get; init; }
public ImmutableList<Item> Items { get; init; }

View File

@ -3,6 +3,9 @@
public class Outfitting : JournalBase
{
public ulong MarketID { get; init; }
/// <summary>
/// Name of the station at which this event occurred.
/// </summary>
public string StationName { get; init; }
public string StarSystem { get; init; }
}

View File

@ -3,6 +3,9 @@
public class Shipyard : JournalBase
{
public ulong MarketID { get; init; }
/// <summary>
/// Name of the station at which this event occurred.
/// </summary>
public string StationName { get; init; }
public string StarSystem { get; init; }
}

View File

@ -6,6 +6,9 @@ namespace Observatory.Framework.Files.Journal
public class StoredModules : JournalBase
{
public string StarSystem { get; init; }
/// <summary>
/// Name of the station at which this event occurred.
/// </summary>
public string StationName { get; init; }
public ulong MarketID { get; init; }
public ImmutableList<StoredItem> Items { get; init; }

View File

@ -6,6 +6,9 @@ namespace Observatory.Framework.Files.Journal
public class StoredShips : JournalBase
{
public ulong MarketID { get; init; }
/// <summary>
/// Name of the station at which this event occurred.
/// </summary>
public string StationName { get; init; }
public string StarSystem { get; init; }
public ImmutableList<StoredShip> ShipsHere { get; init; }

View File

@ -2,7 +2,7 @@
{
public class MarketBuy : JournalBase
{
public long MarketID { get; init; }
public ulong MarketID { get; init; }
public string Type { get; init; }
public string Type_Localised { get; init; }
public int Count { get; init; }

View File

@ -2,7 +2,7 @@
{
public class MarketSell : JournalBase
{
public long MarketID { get; init; }
public ulong MarketID { get; init; }
public string Type { get; init; }
public string Type_Localised { get; init; }
public int Count { get; init; }

View File

@ -7,12 +7,14 @@ namespace Observatory.Framework.Files.Journal
{
public class Docked : JournalBase
{
/// <summary>
/// Name of the station at which this event occurred.
/// </summary>
public string StationName { get; init; }
public string StationType { get; init; }
public string StarSystem { get; init; }
public ulong SystemAddress { get; init; }
public long MarketID { get; init; }
public ulong MarketID { get; init; }
[JsonConverter(typeof(Converters.LegacyFactionConverter<Faction>))]
public Faction StationFaction { get; init; }

View File

@ -4,9 +4,12 @@ namespace Observatory.Framework.Files.Journal
{
public class DockingRequested : JournalBase
{
/// <summary>
/// Name of the station at which this event occurred.
/// </summary>
public string StationName { get; init; }
public string StationType { get; init; }
public long MarketID { get; init; }
public ulong MarketID { get; init; }
public LandingPads LandingPads { get; init; }
}
}

View File

@ -49,5 +49,6 @@ namespace Observatory.Framework.Files.Journal
public string PowerplayState { get; init; }
public bool Taxi { get; init; }
public bool Multicrew { get; init; }
public ThargoidWar ThargoidWar { get; init; }
}
}

View File

@ -24,11 +24,14 @@ namespace Observatory.Framework.Files.Journal
//Stale Data, discard
}
}
/// <summary>
/// Name of the station at which this event occurred.
/// </summary>
public string StationName { get; init; }
public string StationType { get; init; }
public float Longitude { get; init; }
public float Latitude { get; init; }
public long MarketID { get; init; }
public ulong MarketID { get; init; }
[JsonConverter(typeof(LegacyFactionConverter<Faction>))]
public Faction StationFaction { get; init; }
@ -68,5 +71,6 @@ namespace Observatory.Framework.Files.Journal
public bool Multicrew { get; init; }
public bool OnFoot { get; init; }
public bool InSRV { get; init; }
public ThargoidWar ThargoidWar { get; init; }
}
}

View File

@ -6,5 +6,6 @@
public string StarSystem { get; init; }
public ulong SystemAddress { get; init; }
public string StarClass { get; init; }
public bool Taxi { get; init; }
}
}

View File

@ -0,0 +1,9 @@
namespace Observatory.Framework.Files.Journal
{
public class SupercruiseDestinationDrop : JournalBase
{
public string Type { get; init; }
public int Threat { get; init; }
public ulong MarketID { get; init; }
}
}

View File

@ -6,5 +6,6 @@
public ulong SystemAddress { get; init; }
public bool Taxi { get; init; }
public bool Multicrew { get; init; }
public bool? Wanted { get; init; }
}
}

View File

@ -2,6 +2,9 @@
{
public class Undocked : JournalBase
{
/// <summary>
/// Name of the station at which this event occurred.
/// </summary>
public string StationName { get; init; }
public string StationType { get; init; }
public ulong MarketID { get; init; }

View File

@ -11,7 +11,7 @@ namespace Observatory.Framework.Files
/// <summary>
/// Unique ID of current market.
/// </summary>
public long MarketID { get; init; }
public ulong MarketID { get; init; }
/// <summary>
/// Name of the station where the market is located.
/// </summary>

View File

@ -11,7 +11,7 @@ namespace Observatory.Framework.Files
/// <summary>
/// Unique ID of current market.
/// </summary>
public long MarketID { get; init; }
public ulong MarketID { get; init; }
/// <summary>
/// Name of the station where the market is located.
/// </summary>

View File

@ -27,5 +27,21 @@ namespace Observatory.Framework.Files.ParameterTypes
[JsonPropertyName("Spent_On_Insurance")]
public long SpentOnInsurance { get; init; }
[JsonPropertyName("Owned_Ship_Count")]
public int OwnedShipCount { get; init; }
[JsonPropertyName("Premium_Stock_Bought")]
public int PremiumStockBought { get; init; }
[JsonPropertyName("Spent_On_Premium_Stock")]
public long SpentOnPremiumStock { get; init; }
[JsonPropertyName("Spent_On_Suit_Consumables")]
public long SpentOnSuitConsumables { get; init; }
[JsonPropertyName("Spent_On_Suits")]
public long SpentOnSuits { get; init; }
[JsonPropertyName("Spent_On_Weapons")]
public long SpentOnWeapons { get; init; }
[JsonPropertyName("Suits_Owned")]
public int SuitsOwned { get; init; }
[JsonPropertyName("Weapons_Owned")]
public int WeaponsOwned { get; init; }
}
}

View File

@ -6,6 +6,8 @@
public string Genus_Localised { get; init; }
public string Species { get; init; }
public string Species_Localised { get; init; }
public string Variant { get; init; }
public string Variant_Localised { get; init; }
public int Value { get; init; }
public int Bonus { get; init; }
}

View File

@ -26,5 +26,37 @@ namespace Observatory.Framework.Files.ParameterTypes
[JsonPropertyName("Skimmers_Killed")]
public int SkimmersKilled { get; init; }
[JsonPropertyName("ConflictZone_High")]
public int ConflictZoneHigh { get; init; }
[JsonPropertyName("ConflictZone_High_Wins")]
public int ConflictZoneHighWins { get; init; }
[JsonPropertyName("ConflictZone_Low")]
public int ConflictZoneLow { get; init; }
[JsonPropertyName("ConflictZone_Low_Wins")]
public int ConflictZoneLowWins { get; init; }
[JsonPropertyName("ConflictZone_Medium")]
public int ConflictZoneMedium { get; init; }
[JsonPropertyName("ConflictZone_Medium_Wins")]
public int ConflictZoneMediumWins { get; init; }
[JsonPropertyName("ConflictZone_Total")]
public int ConflictZoneTotal { get; init; }
[JsonPropertyName("ConflictZone_Total_Wins")]
public int ConflictZoneTotalWins { get; init; }
[JsonPropertyName("OnFoot_Combat_Bonds")]
public int OnFootCombatBonds { get; init; }
[JsonPropertyName("OnFoot_Combat_Bonds_Profits")]
public int OnFootCombatBondsProfits { get; init; }
[JsonPropertyName("OnFoot_Scavs_Killed")]
public int OnFootScavsKilled { get; init; }
[JsonPropertyName("OnFoot_Ships_Destroyed")]
public int OnFootShipsDestroyed { get; init; }
[JsonPropertyName("OnFoot_Skimmers_Killed")]
public int OnFootSkimmersKilled { get; init; }
[JsonPropertyName("OnFoot_Vehicles_Destroyed")]
public int OnFootVehiclesDestroyed { get; init; }
[JsonPropertyName("Settlement_Conquered")]
public int SettlementConquered { get; init; }
[JsonPropertyName("Settlement_Defended")]
public int SettlementDefended { get; init; }
}
}

View File

@ -29,6 +29,30 @@ namespace Observatory.Framework.Files.ParameterTypes
[JsonPropertyName("Recipes_Generated_Rank_5")]
public int RecipesGeneratedRank5 { get; init; }
[JsonPropertyName("Suit_Mods_Applied")]
public int SuitModsApplied { get; init; }
[JsonPropertyName("Suit_Mods_Applied_Full")]
public int SuitModsAppliedFull { get; init; }
[JsonPropertyName("Suits_Upgraded")]
public int SuitsUpgraded { get; init; }
[JsonPropertyName("Suits_Upgraded_Full")]
public int SuitsUpgradedFull { get; init; }
[JsonPropertyName("Weapon_Mods_Applied")]
public int WeaponModsApplied { get; init; }
[JsonPropertyName("Weapon_Mods_Applied_Full")]
public int WeaponModsAppliedFull { get; init; }
[JsonPropertyName("Weapons_Upgraded")]
public int WeaponsUpgraded { get; init; }
[JsonPropertyName("Weapons_Upgraded_Full")]
public int WeaponsUpgradedFull { get; init; }
[JsonPropertyName("Recipes_Applied"), Obsolete(JournalUtilities.ObsoleteMessage)]
public int RecipesApplied { get; init; }

View File

@ -19,5 +19,56 @@ namespace Observatory.Framework.Files.ParameterTypes
[JsonPropertyName("Highest_Bounty")]
public decimal HighestBounty { get; init; }
[JsonPropertyName("Citizens_Murdered")]
public int CitizensMurdered { get; init; }
[JsonPropertyName("Data_Stolen")]
public int DataStolen { get; init; }
[JsonPropertyName("Goods_Stolen")]
public int GoodsStolen { get; init; }
[JsonPropertyName("Guards_Murdered")]
public int GuardsMurdered { get; init; }
[JsonPropertyName("Malware_Uploaded")]
public int MalwareUploaded { get; init; }
[JsonPropertyName("Omnipol_Murdered")]
public int OmnipolMurdered { get; init; }
[JsonPropertyName("Production_Sabotage")]
public int ProductionSabotage { get; init; }
[JsonPropertyName("Production_Theft")]
public int ProductionTheft { get; init; }
[JsonPropertyName("Profiles_Cloned")]
public int ProfilesCloned { get; init; }
[JsonPropertyName("Sample_Stolen")]
public int SampleStolen { get; init; }
[JsonPropertyName("Settlements_State_Shutdown")]
public int SettlementsStateShutdown { get; init; }
[JsonPropertyName("Total_Murders")]
public int TotalMurders { get; init; }
[JsonPropertyName("Total_Stolen")]
public int TotalStolen { get; init; }
[JsonPropertyName("Turrets_Destroyed")]
public int TurretsDestroyed { get; init; }
[JsonPropertyName("Turrets_Overloaded")]
public int TurretsOverloaded { get; init; }
[JsonPropertyName("Turrets_Total")]
public int TurretsTotal { get; init; }
[JsonPropertyName("Value_Stolen_StateChange")]
public int ValueStolenStatechange { get; init; }
}
}

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
namespace Observatory.Framework.Files.ParameterTypes
{
public class Exobiology
{
[JsonPropertyName("First_Logged")]
public int FirstLogged { get; init; }
[JsonPropertyName("First_Logged_Profits")]
public long FirstLoggedProfits { get; init; }
[JsonPropertyName("Organic_Data")]
public int OrganicData { get; init; }
[JsonPropertyName("Organic_Data_Profits")]
public long OrganicDataProfits { get; init; }
[JsonPropertyName("Organic_Genus")]
public int OrganicGenus { get; init; }
[JsonPropertyName("Organic_Genus_Encountered")]
public int OrganicGenusEncountered { get; init; }
[JsonPropertyName("Organic_Planets")]
public int OrganicPlanets { get; init; }
[JsonPropertyName("Organic_Species")]
public int OrganicSpecies { get; init; }
[JsonPropertyName("Organic_Species_Encountered")]
public int OrganicSpeciesEncountered { get; init; }
[JsonPropertyName("Organic_Systems")]
public int OrganicSystems { get; init; }
[JsonPropertyName("Organic_Variant_Encountered")]
public int OrganicVariantEncountered { get; init; }
}
}

View File

@ -40,5 +40,26 @@ namespace Observatory.Framework.Files.ParameterTypes
[JsonPropertyName("Efficient_Scans")]
public int EfficientScans { get; init; }
[JsonPropertyName("First_Footfalls")]
public int FirstFootfalls { get; init; }
[JsonPropertyName("OnFoot_Distance_Travelled")]
public long OnFootDistanceTravelled { get; init; }
[JsonPropertyName("Planet_Footfalls")]
public int PlanetFootfalls { get; init; }
[JsonPropertyName("Settlements_Visited")]
public int SettlementsVisited { get; init; }
[JsonPropertyName("Shuttle_Distance_Travelled")]
public double ShuttleDistanceTravelled { get; init; }
[JsonPropertyName("Shuttle_Journeys")]
public int ShuttleJourneys { get; init; }
[JsonPropertyName("Spent_On_Shuttles")]
public long SpentOnShuttles { get; init; }
}
}

View File

@ -4,6 +4,12 @@ namespace Observatory.Framework.Files.ParameterTypes
{
public class MaterialTrader
{
[JsonPropertyName("Assets_Traded_In")]
public int AssetsTradedIn { get; init; }
[JsonPropertyName("Assets_Traded_Out")]
public int AssetsTradedOut { get; init; }
[JsonPropertyName("Trades_Completed")]
public int TradesCompleted { get; init; }

View File

@ -12,5 +12,29 @@ namespace Observatory.Framework.Files.ParameterTypes
[JsonPropertyName("SearchRescue_Count")]
public int Count { get; init; }
[JsonPropertyName("Maglocks_Opened")]
public int MaglocksOpened { get; init; }
[JsonPropertyName("Panels_Opened")]
public int PanelsOpened { get; init; }
[JsonPropertyName("Salvage_Illegal_POI")]
public int SalvageIllegalPoi { get; init; }
[JsonPropertyName("Salvage_Illegal_Settlements")]
public int SalvageIllegalSettlements { get; init; }
[JsonPropertyName("Salvage_Legal_POI")]
public int SalvageLegalPoi { get; init; }
[JsonPropertyName("Salvage_Legal_Settlements")]
public int SalvageLegalSettlements { get; init; }
[JsonPropertyName("Settlements_State_FireOut")]
public int SettlementsStateFireOut { get; init; }
[JsonPropertyName("Settlements_State_Reboot")]
public int SettlementsStateReboot { get; init; }
}
}

View File

@ -4,6 +4,9 @@ namespace Observatory.Framework.Files.ParameterTypes
{
public class Thargoid
{
[JsonPropertyName("TG_ENCOUNTER_KILLED")]
public int EncounterKilled { get; init; }
[JsonPropertyName("TG_ENCOUNTER_WAKES")]
public int EncounterWakes { get; init; }
@ -18,8 +21,5 @@ namespace Observatory.Framework.Files.ParameterTypes
[JsonPropertyName("TG_ENCOUNTER_TOTAL_LAST_SHIP")]
public string LastShip { get; init; }
[JsonPropertyName("TG_SCOUT_COUNT")]
public int ScoutCount { get; init; }
}
}

View File

@ -0,0 +1,16 @@
using System.Text.Json.Serialization;
namespace Observatory.Framework.Files.ParameterTypes
{
public class ThargoidWar
{
public string CurrentState { get; init; }
public string NextStateSuccess { get; init; }
public string NextStateFailure { get; init; }
public bool SuccessStateReached { get; init; }
public double WarProgress { get; init; }
public int RemainingPorts { get; init; }
[JsonConverter(typeof(Converters.ThargoidWarRemainingTimeConverter))]
public int EstimatedRemainingTime { get; init; }
}
}

View File

@ -18,5 +18,14 @@ namespace Observatory.Framework.Files.ParameterTypes
[JsonPropertyName("Highest_Single_Transaction")]
public long HighestSingleTransaction { get; init; }
[JsonPropertyName("Assets_Sold")]
public int AssetsSold { get; init; }
[JsonPropertyName("Data_Sold")]
public int DataSold { get; init; }
[JsonPropertyName("Goods_Sold")]
public int GoodsSold { get; init; }
}
}

View File

@ -1,6 +1,5 @@
using Observatory.Framework.Files;
using Observatory.Framework.Files.Journal;
using System.Xml.XPath;
namespace Observatory.Framework.Interfaces
{
@ -51,11 +50,16 @@ namespace Observatory.Framework.Interfaces
/// <summary>
/// <para>Plugin-specific object implementing the IComparer interface which is used to sort columns in the basic UI datagrid.</para>
/// <para>If omitted a basic numeric compare sorter is used.</para>
/// <para>If omitted a natural sort order is used.</para>
/// </summary>
public IObservatoryComparer ColumnSorter
{ get => null; }
/// <summary>
/// Receives data sent by other plugins.
/// </summary>
public void HandlePluginMessage(string sourceName, string sourceVersion, object messageArgs)
{ }
}
/// <summary>
@ -94,7 +98,7 @@ namespace Observatory.Framework.Interfaces
/// Used to track if a "Read All" operation is in progress or not to avoid unnecessary processing or notifications.<br/>
/// Can be omitted for plugins which do not require the distinction.
/// </summary>
[Obsolete] // Replaced by LogMonitorStateChanged
[Obsolete("Deprecated in favour of LogMonitorStateChanged")]
public void ReadAllStarted()
{ }
@ -103,7 +107,7 @@ namespace Observatory.Framework.Interfaces
/// Used to track if a "Read All" operation is in progress or not to avoid unnecessary processing or notifications.<br/>
/// Can be omitted for plugins which do not require the distinction.
/// </summary>
[Obsolete] // Replaced by LogMonitorStateChanged
[Obsolete("Deprecated in favour of LogMonitorStateChanged")]
public void ReadAllFinished()
{ }
}
@ -120,6 +124,20 @@ namespace Observatory.Framework.Interfaces
/// </summary>
/// <param name="notificationEventArgs">Details of the notification as sent from the originating worker plugin.</param>
public void OnNotificationEvent(NotificationArgs notificationEventArgs);
/// <summary>
/// Property set by notification plugins to indicate to Core
/// that native audio notifications should be disabled/suppressed.
/// </summary>
public bool OverrideAudioNotifications
{ get => false; }
/// <summary>
/// Property set by notification plugins to indicate to Core
/// that native popup notifications should be disabled/suppressed.
/// </summary>
public bool OverridePopupNotifications
{ get => false; }
}
/// <summary>
@ -219,6 +237,17 @@ namespace Observatory.Framework.Interfaces
/// Retrieves and ensures creation of a location which can be used by the plugin to store persistent data.
/// </summary>
public string PluginStorageFolder { get; }
/// <summary>
/// Plays audio file using default audio device.
/// </summary>
/// <param name="filePath">Absolute path to audio file.</param>
public Task PlayAudioFile(string filePath);
/// <summary>
/// Sends arbitrary data to all other plugins. The full name and version of the sending plugin will be used to identify the sender to any recipients.
/// </summary>
public void SendPluginMessage(IObservatoryPlugin plugin, object message);
}
/// <summary>

View File

@ -20,6 +20,11 @@
Accessor to get/set displayed name.
</summary>
</member>
<member name="T:Observatory.Framework.ColumnSuggestedWidth">
<summary>
Suggests default column width when building basic UI
</summary>
</member>
<member name="T:Observatory.Framework.SettingIgnore">
<summary>
Indicates that the property should not be displayed to the user in the UI.
@ -489,6 +494,11 @@
Localised name of the signal type.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.FSSSignalDiscovered.SignalType">
<summary>
Type of signal location.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.FSSSignalDiscovered.SpawningState">
<summary>
Faction state or circumstance that caused this signal to appear.
@ -1005,6 +1015,61 @@
Total amount made from selling data.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.CarrierJump.StationName">
<summary>
Name of the station at which this event occurred.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.Disembark.StationName">
<summary>
Name of the station at which this event occurred.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.Market.StationName">
<summary>
Name of the station at which this event occurred.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.Outfitting.StationName">
<summary>
Name of the station at which this event occurred.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.Shipyard.StationName">
<summary>
Name of the station at which this event occurred.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.StoredModules.StationName">
<summary>
Name of the station at which this event occurred.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.StoredShips.StationName">
<summary>
Name of the station at which this event occurred.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.Docked.StationName">
<summary>
Name of the station at which this event occurred.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.DockingRequested.StationName">
<summary>
Name of the station at which this event occurred.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.Location.StationName">
<summary>
Name of the station at which this event occurred.
</summary>
</member>
<member name="P:Observatory.Framework.Files.Journal.Undocked.StationName">
<summary>
Name of the station at which this event occurred.
</summary>
</member>
<member name="T:Observatory.Framework.Files.CargoFile">
<summary>
Elite Dangerous cargo.json file. Describes the current cargo carried above the player's ship.
@ -1352,8 +1417,13 @@
</member>
<member name="P:Observatory.Framework.Interfaces.IObservatoryPlugin.ColumnSorter">
<summary>
Plugin-specific object implementing the IComparer interface which is used to sort columns in the basic UI datagrid.
If omitted a basic string compare sorter is used.
<para>Plugin-specific object implementing the IComparer interface which is used to sort columns in the basic UI datagrid.</para>
<para>If omitted a natural sort order is used.</para>
</summary>
</member>
<member name="M:Observatory.Framework.Interfaces.IObservatoryPlugin.HandlePluginMessage(System.String,System.String,System.Object)">
<summary>
Receives data sent by other plugins.
</summary>
</member>
<member name="T:Observatory.Framework.Interfaces.IObservatoryWorker">
@ -1412,6 +1482,18 @@
</summary>
<param name="notificationEventArgs">Details of the notification as sent from the originating worker plugin.</param>
</member>
<member name="P:Observatory.Framework.Interfaces.IObservatoryNotifier.OverrideAudioNotifications">
<summary>
Property set by notification plugins to indicate to Core
that native audio notifications should be disabled/suppressed.
</summary>
</member>
<member name="P:Observatory.Framework.Interfaces.IObservatoryNotifier.OverridePopupNotifications">
<summary>
Property set by notification plugins to indicate to Core
that native popup notifications should be disabled/suppressed.
</summary>
</member>
<member name="T:Observatory.Framework.Interfaces.IObservatoryCore">
<summary>
Interface passed by Observatory Core to plugins. Primarily used for sending notifications and UI updates back to Core.
@ -1510,6 +1592,17 @@
Retrieves and ensures creation of a location which can be used by the plugin to store persistent data.
</summary>
</member>
<member name="M:Observatory.Framework.Interfaces.IObservatoryCore.PlayAudioFile(System.String)">
<summary>
Plays audio file using default audio device.
</summary>
<param name="filePath">Absolute path to audio file.</param>
</member>
<member name="M:Observatory.Framework.Interfaces.IObservatoryCore.SendPluginMessage(Observatory.Framework.Interfaces.IObservatoryPlugin,System.Object)">
<summary>
Sends arbitrary data to all other plugins. The full name and version of the sending plugin will be used to identify the sender to any recipients.
</summary>
</member>
<member name="T:Observatory.Framework.Interfaces.IObservatoryComparer">
<summary>
Extends the base IComparer interface with exposed values for the column ID and sort order to use.

View File

@ -28,6 +28,8 @@ namespace Observatory.Herald
public string ShortName => "Herald";
public bool OverrideAudioNotifications => true;
public string Version => typeof(HeraldNotifier).Assembly.GetName().Version.ToString();
public PluginUI PluginUI => new (PluginUI.UIType.None, null);
@ -55,7 +57,7 @@ namespace Observatory.Herald
{
var speechManager = new SpeechRequestManager(
heraldSettings, observatoryCore.HttpClient, observatoryCore.PluginStorageFolder, observatoryCore.GetPluginErrorLogger(this));
heraldSpeech = new HeraldQueue(speechManager, observatoryCore.GetPluginErrorLogger(this));
heraldSpeech = new HeraldQueue(speechManager, observatoryCore.GetPluginErrorLogger(this), observatoryCore);
heraldSettings.Test = TestVoice;
}

View File

@ -2,10 +2,10 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using System.Linq;
using NetCoreAudio;
using System.Threading;
using System;
using System.Diagnostics;
using Observatory.Framework.Interfaces;
namespace Observatory.Herald
{
@ -18,15 +18,15 @@ namespace Observatory.Herald
private string rate;
private byte volume;
private SpeechRequestManager speechManager;
private Player audioPlayer;
private Action<Exception, String> ErrorLogger;
private IObservatoryCore core;
public HeraldQueue(SpeechRequestManager speechManager, Action<Exception, String> errorLogger)
public HeraldQueue(SpeechRequestManager speechManager, Action<Exception, String> errorLogger, IObservatoryCore core)
{
this.speechManager = speechManager;
this.core = core;
processing = false;
notifications = new();
audioPlayer = new();
ErrorLogger = errorLogger;
}
@ -74,7 +74,7 @@ namespace Observatory.Herald
{
while (notifications.Any())
{
audioPlayer.SetVolume(volume).Wait();
// audioPlayer.SetVolume(volume).Wait();
notification = notifications.Dequeue();
Debug.WriteLine("Processing notification: {0} - {1}", notification.Title, notification.Detail);
@ -118,7 +118,7 @@ namespace Observatory.Herald
return await speechManager.GetAudioFileFromSsml(ssml, voice, style, rate);
}
private void PlayAudioRequestsSequentially(List<Task<string>> requestTasks)
private async void PlayAudioRequestsSequentially(List<Task<string>> requestTasks)
{
foreach (var request in requestTasks)
{
@ -126,19 +126,20 @@ namespace Observatory.Herald
try
{
Debug.WriteLine($"Playing audio file: {file}");
audioPlayer.Play(file).Wait();
await core.PlayAudioFile(file);
// audioPlayer.Play(file).Wait();
} catch (Exception ex)
{
Debug.WriteLine($"Failed to play {file}: {ex.Message}");
ErrorLogger(ex, $"while playing: {file}");
}
while (audioPlayer.Playing)
Thread.Sleep(50);
//while (audioPlayer.Playing)
// Thread.Sleep(50);
// Explicit stop to ensure device is ready for next file.
// ...hopefully.
audioPlayer.Stop(true).Wait();
//// Explicit stop to ensure device is ready for next file.
//// ...hopefully.
//audioPlayer.Stop(true).Wait();
}
speechManager.CommitCache();

View File

@ -1,19 +0,0 @@
using System;
using System.Threading.Tasks;
namespace NetCoreAudio.Interfaces
{
public interface IPlayer : IDisposable
{
event EventHandler PlaybackFinished;
bool Playing { get; }
bool Paused { get; }
Task Play(string fileName);
Task Pause();
Task Resume();
Task Stop(bool force);
Task SetVolume(byte percent);
}
}

View File

@ -1,98 +0,0 @@
using NetCoreAudio.Interfaces;
using NetCoreAudio.Players;
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
namespace NetCoreAudio
{
public class Player : IPlayer
{
private readonly IPlayer _internalPlayer;
/// <summary>
/// Internally, sets Playing flag to false. Additional handlers can be attached to it to handle any custom logic.
/// </summary>
public event EventHandler PlaybackFinished;
/// <summary>
/// Indicates that the audio is currently playing.
/// </summary>
public bool Playing => _internalPlayer.Playing;
/// <summary>
/// Indicates that the audio playback is currently paused.
/// </summary>
public bool Paused => _internalPlayer.Paused;
public Player()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
_internalPlayer = new WindowsPlayer();
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
_internalPlayer = new LinuxPlayer();
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
_internalPlayer = new MacPlayer();
else
throw new Exception("No implementation exist for the current OS");
_internalPlayer.PlaybackFinished += OnPlaybackFinished;
}
/// <summary>
/// Will stop any current playback and will start playing the specified audio file. The fileName parameter can be an absolute path or a path relative to the directory where the library is located. Sets Playing flag to true. Sets Paused flag to false.
/// </summary>
/// <param name="fileName"></param>
/// <returns></returns>
public async Task Play(string fileName)
{
await _internalPlayer.Play(fileName);
}
/// <summary>
/// Pauses any ongong playback. Sets Paused flag to true. Doesn't modify Playing flag.
/// </summary>
/// <returns></returns>
public async Task Pause()
{
await _internalPlayer.Pause();
}
/// <summary>
/// Resumes any paused playback. Sets Paused flag to false. Doesn't modify Playing flag.
/// </summary>
/// <returns></returns>
public async Task Resume()
{
await _internalPlayer.Resume();
}
/// <summary>
/// Stops any current playback and clears the buffer. Sets Playing and Paused flags to false.
/// </summary>
/// <returns></returns>
public async Task Stop(bool force = false)
{
await _internalPlayer.Stop(force);
}
private void OnPlaybackFinished(object sender, EventArgs e)
{
PlaybackFinished?.Invoke(this, e);
}
/// <summary>
/// Sets the playing volume as percent
/// </summary>
/// <returns></returns>
public async Task SetVolume(byte percent)
{
await _internalPlayer.SetVolume(percent);
}
public void Dispose()
{
_internalPlayer.Dispose();
}
}
}

View File

@ -1,33 +0,0 @@
using System;
using System.IO;
using System.Threading.Tasks;
using NetCoreAudio.Interfaces;
namespace NetCoreAudio.Players
{
internal class LinuxPlayer : UnixPlayerBase, IPlayer
{
protected override string GetBashCommand(string fileName)
{
if (Path.GetExtension(fileName).ToLower().Equals(".mp3"))
{
return "mpg123 -q";
}
else
{
return "aplay -q";
}
}
public override Task SetVolume(byte percent)
{
if (percent > 100)
throw new ArgumentOutOfRangeException(nameof(percent), "Percent can't exceed 100");
var tempProcess = StartBashProcess($"amixer -M set 'Master' {percent}%");
tempProcess.WaitForExit();
return Task.CompletedTask;
}
}
}

View File

@ -1,25 +0,0 @@
using System;
using System.Threading.Tasks;
using NetCoreAudio.Interfaces;
namespace NetCoreAudio.Players
{
internal class MacPlayer : UnixPlayerBase, IPlayer
{
protected override string GetBashCommand(string fileName)
{
return "afplay";
}
public override Task SetVolume(byte percent)
{
if (percent > 100)
throw new ArgumentOutOfRangeException(nameof(percent), "Percent can't exceed 100");
var tempProcess = StartBashProcess($"osascript -e \"set volume output volume {percent}\"");
tempProcess.WaitForExit();
return Task.CompletedTask;
}
}
}

View File

@ -1,110 +0,0 @@
using NetCoreAudio.Interfaces;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
namespace NetCoreAudio.Players
{
internal abstract class UnixPlayerBase : IPlayer
{
private Process _process = null;
internal const string PauseProcessCommand = "kill -STOP {0}";
internal const string ResumeProcessCommand = "kill -CONT {0}";
public event EventHandler PlaybackFinished;
public bool Playing { get; private set; }
public bool Paused { get; private set; }
protected abstract string GetBashCommand(string fileName);
public async Task Play(string fileName)
{
await Stop();
var BashToolName = GetBashCommand(fileName);
_process = StartBashProcess($"{BashToolName} '{fileName}'");
_process.EnableRaisingEvents = true;
_process.Exited += HandlePlaybackFinished;
_process.ErrorDataReceived += HandlePlaybackFinished;
_process.Disposed += HandlePlaybackFinished;
Playing = true;
}
public Task Pause()
{
if (Playing && !Paused && _process != null)
{
var tempProcess = StartBashProcess(string.Format(PauseProcessCommand, _process.Id));
tempProcess.WaitForExit();
Paused = true;
}
return Task.CompletedTask;
}
public Task Resume()
{
if (Playing && Paused && _process != null)
{
var tempProcess = StartBashProcess(string.Format(ResumeProcessCommand, _process.Id));
tempProcess.WaitForExit();
Paused = false;
}
return Task.CompletedTask;
}
public Task Stop(bool force = false)
{
if (_process != null)
{
_process.Kill();
_process.Dispose();
_process = null;
}
Playing = false;
Paused = false;
return Task.CompletedTask;
}
protected Process StartBashProcess(string command)
{
var escapedArgs = command.Replace("\"", "\\\"");
var process = new Process()
{
StartInfo = new ProcessStartInfo
{
FileName = "/bin/bash",
Arguments = $"-c \"{escapedArgs}\"",
RedirectStandardOutput = true,
RedirectStandardInput = true,
UseShellExecute = false,
CreateNoWindow = true,
}
};
process.Start();
return process;
}
internal void HandlePlaybackFinished(object sender, EventArgs e)
{
if (Playing)
{
Playing = false;
PlaybackFinished?.Invoke(this, e);
}
}
public abstract Task SetVolume(byte percent);
public void Dispose()
{
Stop().Wait();
}
}
}

View File

@ -1,141 +0,0 @@
using NetCoreAudio.Interfaces;
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using System.Timers;
namespace NetCoreAudio.Players
{
internal class WindowsPlayer : IPlayer
{
[DllImport("winmm.dll")]
private static extern int mciSendString(string command, StringBuilder stringReturn, int returnLength, IntPtr hwndCallback);
[DllImport("winmm.dll")]
private static extern int mciGetErrorString(int errorCode, StringBuilder errorText, int errorTextSize);
[DllImport("winmm.dll")]
public static extern int waveOutSetVolume(IntPtr hwo, uint dwVolume);
private System.Timers.Timer _playbackTimer;
private Stopwatch _playStopwatch;
private string _fileName;
public event EventHandler PlaybackFinished;
public bool Playing { get; private set; }
public bool Paused { get; private set; }
public Task Play(string fileName)
{
_fileName = $"\"{fileName}\"";
_playbackTimer = new System.Timers.Timer
{
AutoReset = false
};
_playStopwatch = new Stopwatch();
ExecuteMciCommand($"Status {_fileName} Length");
ExecuteMciCommand($"Play {_fileName}");
Paused = false;
Playing = true;
_playbackTimer.Elapsed += HandlePlaybackFinished;
_playbackTimer.Start();
_playStopwatch.Start();
return Task.CompletedTask;
}
public Task Pause()
{
if (Playing && !Paused)
{
ExecuteMciCommand($"Pause {_fileName}");
Paused = true;
_playbackTimer.Stop();
_playStopwatch.Stop();
_playbackTimer.Interval -= _playStopwatch.ElapsedMilliseconds;
}
return Task.CompletedTask;
}
public Task Resume()
{
if (Playing && Paused)
{
ExecuteMciCommand($"Resume {_fileName}");
Paused = false;
_playbackTimer.Start();
_playStopwatch.Reset();
_playStopwatch.Start();
}
return Task.CompletedTask;
}
public Task Stop(bool force = false)
{
if (Playing || force)
{
ExecuteMciCommand($"Stop {_fileName}");
Playing = false;
Paused = false;
_playbackTimer?.Stop();
_playStopwatch?.Stop();
}
return Task.CompletedTask;
}
private void HandlePlaybackFinished(object sender, ElapsedEventArgs e)
{
Playing = false;
PlaybackFinished?.Invoke(this, e);
_playbackTimer?.Dispose();
_playbackTimer = null;
}
private Task ExecuteMciCommand(string commandString)
{
var sb = new StringBuilder();
var result = mciSendString(commandString, sb, 1024 * 1024, IntPtr.Zero);
if (result != 0)
{
var errorSb = new StringBuilder($"Error executing MCI command '{commandString}'. Error code: {result}.");
var sb2 = new StringBuilder(128);
mciGetErrorString(result, sb2, 128);
errorSb.Append($" Message: {sb2}");
throw new Exception(errorSb.ToString());
}
if (commandString.ToLower().StartsWith("status") && int.TryParse(sb.ToString(), out var length))
_playbackTimer.Interval = length + 75;
return Task.CompletedTask;
}
public Task SetVolume(byte percent)
{
// Calculate the volume that's being set
int NewVolume = ushort.MaxValue / 100 * percent;
// Set the same volume for both the left and the right channels
uint NewVolumeAllChannels = ((uint)NewVolume & 0x0000ffff) | ((uint)NewVolume << 16);
// Set the volume
waveOutSetVolume(IntPtr.Zero, NewVolumeAllChannels);
return Task.CompletedTask;
}
public void Dispose()
{
Stop().Wait();
ExecuteMciCommand("Close All");
}
}
}

View File

@ -37,12 +37,8 @@
</EmbeddedResource>
</ItemGroup>
<ItemGroup>
<Folder Include="NetCoreAudio\Players\" />
</ItemGroup>
<Target Name="PostBuild" AfterTargets="PostBuildEvent">
<Exec Condition=" '$(OS)' == 'Windows_NT' " Command="xcopy &quot;$(TargetPath)&quot; &quot;$(ProjectDir)..\ObservatoryCore\$(OutDir)plugins\&quot; /y" />
<Exec Condition=" '$(OS)' == 'Windows_NT' " Command="xcopy &quot;$(TargetPath)&quot; &quot;$(ProjectDir)..\ObservatoryCore\$(OutDir)..\net6.0-windows\plugins\&quot; /y" />
<Exec Condition=" '$(OS)' != 'Windows_NT' " Command="[ ! -d &quot;$(ProjectDir)../ObservatoryCore/$(OutDir)plugins/deps&quot; ] &amp;&amp; mkdir -p &quot;$(ProjectDir)../ObservatoryCore/$(OutDir)plugins/deps&quot; || echo Directory already exists" />
</Target>

View File

@ -1,13 +1,12 @@
# Elite Observatory *Core*
Tool for reading/monitoring Elite Dangerous journals for interesting objects. Successor to the original Elite Observatory, rewritten from scratch using .NET 6.0 and AvaloniaUI.
Tool for reading/monitoring Elite Dangerous journals for interesting objects. Successor to the original Elite Observatory, rewritten from scratch using .NET 6.0.
## *IMPORTANT*
Observatory Core and it's associated plugins are currently in an alpha state and are neither feature-complete nor using a finalised UI. A major update to the UI is expected soon, which will likely break compatibility with all current plugins.
Observatory Core and it's associated plugins are currently in a state of ongoing development and are neither feature-complete nor using a finalised UI.
Omissions to current functionality include:
* Integration with Frontier's Companion API
* Data submission to IGAU
* Sortable columns in plugin data grids
* Non-grid plugin UI options
* Light mode
* *And more...*
@ -32,8 +31,9 @@ If you want to chat or collaborate with other users of Observatory you can find
For information on how to create a plugin, refer to this article about [ObservatoryFramework](https://github.com/Xjph/ObservatoryCore/wiki/Framework).
## Prerequisites for use
.NET 6, and by extension one of its [supported OSes](https://github.com/dotnet/core/blob/main/release-notes/6.0/supported-os.md).
(This will be installed automatically by the Observatory Core installer.)
All you need is .NET 6, which should be installed automatically by the Observatory Core installer.
The portable build has no prerequisites due to bundling the .NET runtime along with the program, though this does make the exe commensurately larger.
## Prerequisites for building
C# 9.0, .NET 6.0, [AvaloniaUI ~~0.10.3~~](https://github.com/AvaloniaUI/Avalonia) (specific version in flux during UI rewrite), and of course [ObservatoryFramework](https://github.com/Xjph/ObservatoryFramework).
C# 9.0, .NET 6.0, and [ObservatoryFramework](https://github.com/Xjph/ObservatoryFramework).

39
SignObservatory.ps1 Normal file
View File

@ -0,0 +1,39 @@
$cert = Get-ChildItem Cert:\LocalMachine\My -CodeSigningCert
Set-AuthenticodeSignature -FilePath ./ObservatoryCore/bin/Release/net6.0-windows/ObservatoryCore.exe -Certificate $cert
Set-AuthenticodeSignature -FilePath ./ObservatoryCore/bin/Release/net6.0-windows/plugins/ObservatoryExplorer.dll -Certificate $cert
Set-AuthenticodeSignature -FilePath ./ObservatoryCore/bin/Release/net6.0-windows/plugins/ObservatoryBotanist.dll -Certificate $cert
Set-AuthenticodeSignature -FilePath ./ObservatoryCore/bin/Release/net6.0-windows/plugins/ObservatoryHerald.dll -Certificate $cert
# SIG # Begin signature block
# MIIF0AYJKoZIhvcNAQcCoIIFwTCCBb0CAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUjqIsfdT62QvQPP+a5YOXVRCf
# 3o6gggNKMIIDRjCCAi6gAwIBAgIQQbRFPs6oPodFBj0fsFanmzANBgkqhkiG9w0B
# AQsFADA7MRgwFgYDVQQDDA9Kb25hdGhhbiBNaWxsZXIxHzAdBgkqhkiG9w0BCQEW
# EGptaWxsZXJAeGpwaC5uZXQwHhcNMjMwMzI4MTgxNTM3WhcNMjQwMzI4MTgzNTM3
# WjA7MRgwFgYDVQQDDA9Kb25hdGhhbiBNaWxsZXIxHzAdBgkqhkiG9w0BCQEWEGpt
# aWxsZXJAeGpwaC5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCv
# eeKY2sY4SAMLgjE+sm1lj8Tje5QArsuSqLFC0gpWzHFq2HZYHLGR5l9Kz1Jm4iNm
# bdkQiEtt5o6e48L2GLqftM0XklmkNVzyuj6SqL99K1JCuO/kLRVorqRV/88NpOOe
# Bpn1W5FTA7m1PVCYXbz3a6l93hNY6mI4yb9MV8nKFDnmmAtiwIsKgXuNf81sU8bg
# 4A7mB9A7Jgvx1/Gs7rFu0m1qWIGpfhsh8EQtpJaiVvzCBqdpIvDEnMwlVd6S0nkj
# jCCB7s12oiXKYjBS1Vm1YfwoaPkHe9E+z7zgHnhZ5hrTt8g/TZM+cS2o+5JQYTr9
# RZUjQ3EmsUfZMAuSekERAgMBAAGjRjBEMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUE
# DDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUCeFpbq5cQi1Z5DQydkmiF8MIfyMwDQYJ
# KoZIhvcNAQELBQADggEBAHF/lRtemEggwTFiwTI01Z3erV6YGNT2miD4NrUrnQEe
# kI+Ezh/MLj2vRmqeVz7XX1ePZX0sd7sViRMnPm+LTl8UltZqhTWV/h7qmi/2Vf74
# QHLE/Ht3olWBdGOVzeeP5XLMBqqg7HWPHGpTA8lx0ApI4YhYu7w/SgwzYUj2NF2O
# GRmV78kcHeYf+h5lZzAKjc+dgH+ucsqpKgDxCk8lBhUkd102YGMUZophz0L8RTD4
# k/CAliVZo3m8ENsR6pMnjsgifeZ8Q9ydpBXawIdcqW9xtZanvYN9+GAHMYeFWWBf
# 0fBcoPAy4X5bcvQmK/0d7znpgDmgm4jYywF5ptHXoAIxggHwMIIB7AIBATBPMDsx
# GDAWBgNVBAMMD0pvbmF0aGFuIE1pbGxlcjEfMB0GCSqGSIb3DQEJARYQam1pbGxl
# ckB4anBoLm5ldAIQQbRFPs6oPodFBj0fsFanmzAJBgUrDgMCGgUAoHgwGAYKKwYB
# BAGCNwIBDDEKMAigAoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAc
# BgorBgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAjBgkqhkiG9w0BCQQxFgQUfXa2
# HgFmPrFbD6PqO1Z7s6yzLkowDQYJKoZIhvcNAQEBBQAEggEASEcYW2MnOLX+dMin
# lEVxL8fTOrf/XuE05QwqQSHOBqqO4GMuR+IeBjE2R4EjxQsZMQVok1dK302ByHA9
# OVv37xG4exqP/vkP3NX/z2s1Cl2PE1gzxVNgdGlbkzIQF9EiMXr9P/QGifCg2TLV
# 2mk4Vt+mA1/tU066tNXahbL9N9b+yLcB3VNfru/SnvO/ZPzKCmjNZW54mnNKnRCE
# PJDVKEKla/ufh8iMR+SiIaaXrwypvdz8CYK9OSs9qr0Cjp9jY1TXLxgNZiTenUoY
# n+sVQzv+N1PAy2nvSXlnesbxlO3T2XPp6fYkpj1uYCoun3Ztpr2MoKRKBgzybo7Z
# GYn3QA==
# SIG # End signature block

View File

@ -1,8 +0,0 @@
#!/bin/bash
dotnet build ./ObservatoryFramework/ObservatoryFramework.csproj "$@"
dotnet build ./ObservatoryExplorer/ObservatoryExplorer.csproj "$@"
dotnet build ./ObservatoryBotanist/ObservatoryBotanist.csproj "$@"
if [ -f ../NetCoreAudio/NetCoreAudio/NetCoreAudio.csproj ]; then
dotnet build ../NetCoreAudio/NetCoreAudio/NetCoreAudio.csproj -c Release
dotnet build ./ObservatoryHerald/ObservatoryHerald.csproj "$@"
fi

View File

@ -1,7 +0,0 @@
dotnet build ./ObservatoryFramework/ObservatoryFramework.csproj %*
dotnet build ./ObservatoryExplorer/ObservatoryExplorer.csproj %*
dotnet build ./ObservatoryBotanist/ObservatoryBotanist.csproj %*
IF EXIST ..\NetCoreAudio\NetCoreAudio\NetCoreAudio.csproj (
dotnet build ../NetCoreAudio/NetCoreAudio/NetCoreAudio.csproj -c Release
dotnet build ./ObservatoryHerald/ObservatoryHerald.csproj %*
)