From a5154996eed5b1d56ba62163ec18b56bed32747c Mon Sep 17 00:00:00 2001 From: Xjph Date: Thu, 3 Jun 2021 22:25:32 -0230 Subject: [PATCH] Add project files. --- .gitignore | 365 ++++++++++++++++ ObservatoryCore.sln | 25 ++ ObservatoryCore/JournalReader.cs | 97 +++++ ObservatoryCore/LogMonitor.cs | 265 ++++++++++++ ObservatoryCore/ObservatoryCore.cs | 23 + ObservatoryCore/ObservatoryCore.csproj | 59 +++ .../PluginManagement/PlaceholderPlugin.cs | 38 ++ .../PluginManagement/PluginCore.cs | 86 ++++ .../PluginManagement/PluginEventHandler.cs | 48 +++ .../PluginManagement/PluginManager.cs | 282 ++++++++++++ ObservatoryCore/Properties/Core.Designer.cs | 62 +++ ObservatoryCore/Properties/Core.settings | 15 + ObservatoryCore/UI/MainApplication.axaml | 13 + ObservatoryCore/UI/MainApplication.axaml.cs | 29 ++ ObservatoryCore/UI/Models/BasicUIModel.cs | 14 + ObservatoryCore/UI/Models/CoreModel.cs | 17 + .../UI/Models/NotificationModel.cs | 14 + ObservatoryCore/UI/TabTemplateSelector.cs | 31 ++ ObservatoryCore/UI/ViewLocator.cs | 32 ++ .../UI/ViewModels/BasicUIViewModel.cs | 67 +++ .../UI/ViewModels/CoreViewModel.cs | 110 +++++ .../UI/ViewModels/MainWindowViewModel.cs | 16 + .../UI/ViewModels/NotificationViewModel.cs | 18 + .../UI/ViewModels/ViewModelBase.cs | 11 + ObservatoryCore/UI/Views/BasicUIView.axaml | 10 + ObservatoryCore/UI/Views/BasicUIView.axaml.cs | 404 ++++++++++++++++++ ObservatoryCore/UI/Views/CoreView.axaml | 43 ++ ObservatoryCore/UI/Views/CoreView.axaml.cs | 21 + ObservatoryCore/UI/Views/MainWindow.axaml | 11 + ObservatoryCore/UI/Views/MainWindow.axaml.cs | 26 ++ .../UI/Views/NotificationView.axaml | 13 + .../UI/Views/NotificationView.axaml.cs | 22 + 32 files changed, 2287 insertions(+) create mode 100644 .gitignore create mode 100644 ObservatoryCore.sln create mode 100644 ObservatoryCore/JournalReader.cs create mode 100644 ObservatoryCore/LogMonitor.cs create mode 100644 ObservatoryCore/ObservatoryCore.cs create mode 100644 ObservatoryCore/ObservatoryCore.csproj create mode 100644 ObservatoryCore/PluginManagement/PlaceholderPlugin.cs create mode 100644 ObservatoryCore/PluginManagement/PluginCore.cs create mode 100644 ObservatoryCore/PluginManagement/PluginEventHandler.cs create mode 100644 ObservatoryCore/PluginManagement/PluginManager.cs create mode 100644 ObservatoryCore/Properties/Core.Designer.cs create mode 100644 ObservatoryCore/Properties/Core.settings create mode 100644 ObservatoryCore/UI/MainApplication.axaml create mode 100644 ObservatoryCore/UI/MainApplication.axaml.cs create mode 100644 ObservatoryCore/UI/Models/BasicUIModel.cs create mode 100644 ObservatoryCore/UI/Models/CoreModel.cs create mode 100644 ObservatoryCore/UI/Models/NotificationModel.cs create mode 100644 ObservatoryCore/UI/TabTemplateSelector.cs create mode 100644 ObservatoryCore/UI/ViewLocator.cs create mode 100644 ObservatoryCore/UI/ViewModels/BasicUIViewModel.cs create mode 100644 ObservatoryCore/UI/ViewModels/CoreViewModel.cs create mode 100644 ObservatoryCore/UI/ViewModels/MainWindowViewModel.cs create mode 100644 ObservatoryCore/UI/ViewModels/NotificationViewModel.cs create mode 100644 ObservatoryCore/UI/ViewModels/ViewModelBase.cs create mode 100644 ObservatoryCore/UI/Views/BasicUIView.axaml create mode 100644 ObservatoryCore/UI/Views/BasicUIView.axaml.cs create mode 100644 ObservatoryCore/UI/Views/CoreView.axaml create mode 100644 ObservatoryCore/UI/Views/CoreView.axaml.cs create mode 100644 ObservatoryCore/UI/Views/MainWindow.axaml create mode 100644 ObservatoryCore/UI/Views/MainWindow.axaml.cs create mode 100644 ObservatoryCore/UI/Views/NotificationView.axaml create mode 100644 ObservatoryCore/UI/Views/NotificationView.axaml.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..699a8b5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,365 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Oo]ut/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +*.snk \ No newline at end of file diff --git a/ObservatoryCore.sln b/ObservatoryCore.sln new file mode 100644 index 0000000..1611998 --- /dev/null +++ b/ObservatoryCore.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30128.74 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ObservatoryCore", "ObservatoryCore\ObservatoryCore.csproj", "{0E1C4F16-858E-4E53-948A-77D81A8F3395}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0E1C4F16-858E-4E53-948A-77D81A8F3395}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0E1C4F16-858E-4E53-948A-77D81A8F3395}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0E1C4F16-858E-4E53-948A-77D81A8F3395}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0E1C4F16-858E-4E53-948A-77D81A8F3395}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F41B8681-A5D9-4167-9938-56DE88024000} + EndGlobalSection +EndGlobal diff --git a/ObservatoryCore/JournalReader.cs b/ObservatoryCore/JournalReader.cs new file mode 100644 index 0000000..6f600ad --- /dev/null +++ b/ObservatoryCore/JournalReader.cs @@ -0,0 +1,97 @@ +using Observatory.Framework.Files.Journal; +using System; +using System.Collections.Generic; +using System.Text; +using System.Text.Json; +using System.Linq; +using System.Reflection; + +namespace Observatory +{ + public class JournalReader + { + public static TJournal ObservatoryDeserializer(string json) where TJournal : JournalBase + { + TJournal deserialized; + + if (typeof(TJournal) == typeof(InvalidJson)) + { + InvalidJson invalidJson; + try + { + var reader = new Utf8JsonReader(Encoding.UTF8.GetBytes(json)); + string eventType = string.Empty; + string timestamp = string.Empty; + + while ((eventType == string.Empty || timestamp == string.Empty) && reader.Read()) + { + if (reader.TokenType == JsonTokenType.PropertyName) + { + if (reader.GetString() == "event") + { + reader.Read(); + eventType = reader.GetString(); + } + else if (reader.GetString() == "timestamp") + { + reader.Read(); + timestamp = reader.GetString(); + } + } + } + + invalidJson = new InvalidJson() + { + Event = "InvalidJson", + Timestamp = timestamp, + OriginalEvent = eventType + }; + } + catch + { + invalidJson = new InvalidJson() + { + Event = "InvalidJson", + Timestamp = string.Empty, + OriginalEvent = "Invalid" + }; + } + + deserialized = (TJournal)Convert.ChangeType(invalidJson, typeof(TJournal)); + + } + //Journal potentially had invalid JSON for a brief period in 2017, check for it and remove. + //TODO: Check if this gets handled by InvalidJson now. + else if (typeof(TJournal) == typeof(Scan) && json.Contains("\"RotationPeriod\":inf")) + { + deserialized = JsonSerializer.Deserialize(json.Replace("\"RotationPeriod\":inf,", "")); + } + else + { + deserialized = JsonSerializer.Deserialize(json); + } + deserialized.Json = json; + + return deserialized; + } + + + public static Dictionary PopulateEventClasses() + { + var eventClasses = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + var allTypes = Assembly.GetAssembly(typeof(JournalBase)).GetTypes(); + + var journalTypes = allTypes.Where(a => a.IsSubclassOf(typeof(JournalBase))); + + foreach (var journalType in journalTypes) + { + eventClasses.Add(journalType.Name, journalType); + } + + eventClasses.Add("JournalBase", typeof(JournalBase)); + + return eventClasses; + } + } +} diff --git a/ObservatoryCore/LogMonitor.cs b/ObservatoryCore/LogMonitor.cs new file mode 100644 index 0000000..467e560 --- /dev/null +++ b/ObservatoryCore/LogMonitor.cs @@ -0,0 +1,265 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Text.Json.Serialization; +using Observatory.Framework; +using Observatory.Framework.Files; + +namespace Observatory +{ + class LogMonitor + { + #region Singleton Instantiation + + public static LogMonitor GetInstance + { + get + { + return _instance.Value; + } + } + + private static readonly Lazy _instance = new Lazy(NewLogMonitor); + + private static LogMonitor NewLogMonitor() + { + return new LogMonitor(); + } + + private LogMonitor() + { + currentLine = new(); + journalTypes = JournalReader.PopulateEventClasses(); + InitializeWatchers(string.Empty); + } + + #endregion + + #region Public Methods + + public void Start() + { + journalWatcher.EnableRaisingEvents = true; + statusWatcher.EnableRaisingEvents = true; + monitoring = true; + } + + public void Stop() + { + journalWatcher.EnableRaisingEvents = false; + statusWatcher.EnableRaisingEvents = false; + monitoring = false; + } + + public void ChangeWatchedDirectory(string path) + { + journalWatcher.Dispose(); + statusWatcher.Dispose(); + InitializeWatchers(path); + } + + public bool IsMonitoring() + { + return monitoring; + } + + public bool ReadAllInProgress() + { + return readall; + } + + public void ReadAllJournals() + { + ReadAllJournals(string.Empty); + } + + public void ReadAllJournals(string path) + { + readall = true; + DirectoryInfo logDirectory = GetJournalFolder(path); + var files = logDirectory.GetFiles("Journal.????????????.??.log"); + foreach (var file in files) + { + var lines = ReadAllLines(file.FullName); + foreach (var line in lines) + { + DeserializeAndInvoke(line); + } + } + readall = false; + } + + #endregion + + #region Public Events + + public event EventHandler JournalEntry; + + public event EventHandler StatusUpdate; + + #endregion + + + + #region Private Fields + + private FileSystemWatcher journalWatcher; + private FileSystemWatcher statusWatcher; + private Dictionary journalTypes; + private Dictionary currentLine; + private bool monitoring = false; + private bool readall = false; + + #endregion + + #region Private Methods + + private void InitializeWatchers(string path) + { + DirectoryInfo logDirectory = GetJournalFolder(path); + + journalWatcher = new FileSystemWatcher(logDirectory.FullName, "Journal.????????????.??.log") + { + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.LastAccess | + NotifyFilters.FileName | NotifyFilters.CreationTime | NotifyFilters.DirectoryName + }; + journalWatcher.Changed += LogChangedEvent; + journalWatcher.Created += LogCreatedEvent; + + statusWatcher = new FileSystemWatcher(logDirectory.FullName, "Status.json") + { + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.Size | NotifyFilters.LastAccess + }; + statusWatcher.Changed += StatusUpdateEvent; + } + + private DirectoryInfo GetJournalFolder(string path) + { + DirectoryInfo logDirectory; + + if (path.Length == 0 && Properties.Core.Default.JournalFolder.Trim().Length > 0) + { + path = Properties.Core.Default.JournalFolder; + } + + if (path.Length > 0) + { + if (Directory.Exists(path)) + { + logDirectory = new DirectoryInfo(path); + } + else + { + throw new DirectoryNotFoundException($"Directory '{path}' does not exist."); + } + } + else if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + logDirectory = new DirectoryInfo(GetSavedGamesPath() + @"\Frontier Developments\Elite Dangerous"); + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + logDirectory = new DirectoryInfo("."); + } + else + { + throw new NotImplementedException("Current OS Platform Not Supported."); + } + + Properties.Core.Default.JournalFolder = path; + Properties.Core.Default.Save(); + + return logDirectory; + } + + private void DeserializeAndInvoke(string line) + { + var eventType = JournalUtilities.GetEventType(line); + if (!journalTypes.ContainsKey(eventType)) + { + eventType = "JournalBase"; + } + + var eventClass = journalTypes[eventType]; + MethodInfo journalRead = typeof(JournalReader).GetMethod(nameof(JournalReader.ObservatoryDeserializer)); + MethodInfo journalGeneric = journalRead.MakeGenericMethod(eventClass); + object entry = journalGeneric.Invoke(null, new object[] { line }); + var journalEvent = new JournalEventArgs() { journalType = eventClass, journalEvent = entry }; + var handler = JournalEntry; + + handler?.Invoke(this, journalEvent); + + } + + private void LogChangedEvent(object source, FileSystemEventArgs eventArgs) + { + var fileContent = ReadAllLines(eventArgs.FullPath); + + if (currentLine[eventArgs.FullPath] == -1) + { + currentLine[eventArgs.FullPath] = fileContent.Count - 1; + } + + foreach(string line in fileContent.Skip(currentLine[eventArgs.FullPath])) + { + DeserializeAndInvoke(line); + } + + currentLine[eventArgs.FullPath] = fileContent.Count; + } + + private List ReadAllLines(string path) + { + var lines = new List(); + using (StreamReader file = new StreamReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))) + { + while (!file.EndOfStream) + { + lines.Add(file.ReadLine()); + } + } + return lines; + } + + private void LogCreatedEvent(object source, FileSystemEventArgs eventArgs) + { + currentLine[eventArgs.FullPath] = 0; + LogChangedEvent(source, eventArgs); + } + + private void StatusUpdateEvent(object source, FileSystemEventArgs eventArgs) + { + var handler = StatusUpdate; + var statusLines = ReadAllLines(eventArgs.FullPath); + if (statusLines.Count > 0) + { + var status = JournalReader.ObservatoryDeserializer(statusLines[0]); + handler?.Invoke(this, new JournalEventArgs() { journalType = typeof(Status), journalEvent = status }); + } + } + + private static string GetSavedGamesPath() + { + if (Environment.OSVersion.Version.Major < 6) throw new NotSupportedException(); + IntPtr pathPtr = IntPtr.Zero; + try + { + Guid FolderSavedGames = new Guid("4C5C32FF-BB9D-43b0-B5B4-2D72E54EAAA4"); + SHGetKnownFolderPath(ref FolderSavedGames, 0, IntPtr.Zero, out pathPtr); + return Marshal.PtrToStringUni(pathPtr); + } + finally + { + Marshal.FreeCoTaskMem(pathPtr); + } + } + + [DllImport("shell32.dll", CharSet = CharSet.Auto)] + private static extern int SHGetKnownFolderPath(ref Guid id, int flags, IntPtr token, out IntPtr path); + + #endregion + } +} diff --git a/ObservatoryCore/ObservatoryCore.cs b/ObservatoryCore/ObservatoryCore.cs new file mode 100644 index 0000000..7402d71 --- /dev/null +++ b/ObservatoryCore/ObservatoryCore.cs @@ -0,0 +1,23 @@ +using System; +using Avalonia; +using Avalonia.ReactiveUI; + +namespace Observatory +{ + class ObservatoryCore + { + [STAThread] + static void Main(string[] args) + { + BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); + } + + public static AppBuilder BuildAvaloniaApp() + { + return AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace() + .UseReactiveUI(); + } + } +} diff --git a/ObservatoryCore/ObservatoryCore.csproj b/ObservatoryCore/ObservatoryCore.csproj new file mode 100644 index 0000000..4af6ff4 --- /dev/null +++ b/ObservatoryCore/ObservatoryCore.csproj @@ -0,0 +1,59 @@ + + + + WinExe + net5.0 + Observatory + true + false + ObservatoryKey.snk + + Link + true + + + + true + + + + + + + + + + + + + + + + + + ..\..\ObservatoryFramework\ObservatoryFramework\bin\Release\net5.0\ObservatoryFramework.dll + + + + + + True + True + Core.settings + + + CoreView.axaml + + + NotificationView.axaml + + + + + + SettingsSingleFileGenerator + Core.Designer.cs + + + + diff --git a/ObservatoryCore/PluginManagement/PlaceholderPlugin.cs b/ObservatoryCore/PluginManagement/PlaceholderPlugin.cs new file mode 100644 index 0000000..1e8f442 --- /dev/null +++ b/ObservatoryCore/PluginManagement/PlaceholderPlugin.cs @@ -0,0 +1,38 @@ +using Observatory.Framework; +using Observatory.Framework.Files; +using Observatory.Framework.Files.Journal; +using Observatory.Framework.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Observatory.PluginManagement +{ + public class PlaceholderPlugin : IObservatoryNotifier + { + public PlaceholderPlugin(string name) + { + this.name = name; + } + + public string Name => name; + + private string name; + + public string ShortName => name; + + public string Version => string.Empty; + + public PluginUI PluginUI => new PluginUI(PluginUI.UIType.None, null); + + public object Settings { get => null; set { } } + + public void Load(IObservatoryCore observatoryCore) + { } + + public void OnNotificationEvent(string title, string text) + { } + } +} diff --git a/ObservatoryCore/PluginManagement/PluginCore.cs b/ObservatoryCore/PluginManagement/PluginCore.cs new file mode 100644 index 0000000..dc151d1 --- /dev/null +++ b/ObservatoryCore/PluginManagement/PluginCore.cs @@ -0,0 +1,86 @@ +using Observatory.Framework; +using Observatory.Framework.Files; +using Observatory.Framework.Interfaces; +using System; + + +namespace Observatory.PluginManagement +{ + public class PluginCore : IObservatoryCore + { + + public string Version => "1.0a"; + + public Status GetStatus() + { + throw new NotImplementedException(); + } + + public void RequestAllJournals() + { + throw new NotImplementedException(); + } + + public void RequestJournalRange(DateTime start, DateTime end) + { + throw new NotImplementedException(); + } + + public void RequestJournalRange(int startIndex, int number, bool newestFirst) + { + throw new NotImplementedException(); + } + + public void SendNotification(string title, string text) + { + if (!LogMonitor.GetInstance.ReadAllInProgress()) + { + var handler = Notification; + handler?.Invoke(this, new NotificationEventArgs() { Title = title, Detail = text }); + + if (Properties.Core.Default.NativeNotify) + { + InvokeNativeNotification(title, text); + } + } + } + + private void InvokeNativeNotification(string title, string text) + { + Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => + { + var notifyWindow = new UI.Views.NotificationView() { DataContext = new UI.ViewModels.NotificationViewModel(title, text) }; + notifyWindow.Show(); + }); + } + + public void AddGridItem(IObservatoryWorker worker, object item) + { + Avalonia.Threading.Dispatcher.UIThread.InvokeAsync(() => + { + worker.PluginUI.DataGrid.Add(item); + + //Hacky removal of original empty object if one was used to populate columns + if (worker.PluginUI.DataGrid.Count == 2) + { + 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; + } + } + + if (allNull) + worker.PluginUI.DataGrid.RemoveAt(0); + } + + }); + } + + public event EventHandler Notification; + } +} diff --git a/ObservatoryCore/PluginManagement/PluginEventHandler.cs b/ObservatoryCore/PluginManagement/PluginEventHandler.cs new file mode 100644 index 0000000..da9ff84 --- /dev/null +++ b/ObservatoryCore/PluginManagement/PluginEventHandler.cs @@ -0,0 +1,48 @@ +using Observatory.Framework; +using Observatory.Framework.Interfaces; +using Observatory.Framework.Files; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Observatory.Framework.Files.Journal; + +namespace Observatory.PluginManagement +{ + class PluginEventHandler + { + private IEnumerable observatoryWorkers; + private IEnumerable observatoryNotifiers; + + public PluginEventHandler(IEnumerable observatoryWorkers, IEnumerable observatoryNotifiers) + { + this.observatoryWorkers = observatoryWorkers; + this.observatoryNotifiers = observatoryNotifiers; + } + + public void OnJournalEvent(object source, JournalEventArgs journalEventArgs) + { + foreach (var worker in observatoryWorkers) + { + worker.JournalEvent((JournalBase)journalEventArgs.journalEvent); + } + } + + public void OnStatusUpdate(object sourece, JournalEventArgs journalEventArgs) + { + foreach (var worker in observatoryWorkers) + { + worker.StatusChange((Status)journalEventArgs.journalEvent); + } + } + + public void OnNotificationEvent(object source, NotificationEventArgs notificationEventArgs) + { + foreach (var notifier in observatoryNotifiers) + { + notifier.OnNotificationEvent(notificationEventArgs.Title, notificationEventArgs.Detail); + } + } + } +} diff --git a/ObservatoryCore/PluginManagement/PluginManager.cs b/ObservatoryCore/PluginManagement/PluginManager.cs new file mode 100644 index 0000000..9a4bd84 --- /dev/null +++ b/ObservatoryCore/PluginManagement/PluginManager.cs @@ -0,0 +1,282 @@ +using Avalonia.Controls; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Data; +using Observatory.Framework.Interfaces; +using System.IO; +using System.Configuration; +using System.Text.Json; + +namespace Observatory.PluginManagement +{ + public class PluginManager + { + public static PluginManager GetInstance + { + get + { + return _instance.Value; + } + } + + private static readonly Lazy _instance = new Lazy(NewPluginManager); + + private static PluginManager NewPluginManager() + { + return new PluginManager(); + } + + + public readonly List errorList; + public readonly List pluginPanels; + public readonly List pluginTables; + public readonly List<(IObservatoryWorker plugin, PluginStatus signed)> workerPlugins; + public readonly List<(IObservatoryNotifier plugin, PluginStatus signed)> notifyPlugins; + + private PluginManager() + { + AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve; + errorList = LoadPlugins(out workerPlugins, out notifyPlugins); + + foreach (var error in errorList) + { + Console.WriteLine(error); + } + + var pluginHandler = new PluginEventHandler(workerPlugins.Select(p => p.plugin), notifyPlugins.Select(p => p.plugin)); + var logMonitor = LogMonitor.GetInstance; + pluginPanels = new(); + pluginTables = new(); + + logMonitor.JournalEntry += pluginHandler.OnJournalEvent; + logMonitor.StatusUpdate += pluginHandler.OnStatusUpdate; + + var core = new PluginCore(); + + foreach (var plugin in workerPlugins.Select(p => p.plugin)) + { + LoadSettings(plugin); + plugin.Load(core); + } + + core.Notification += pluginHandler.OnNotificationEvent; + } + + private void LoadSettings(IObservatoryPlugin plugin) + { + string settingsFile = GetSettingsFile(plugin); + bool createFile = !File.Exists(settingsFile); + + if (!createFile) + { + try + { + string settingsJson = File.ReadAllText(settingsFile); + if (settingsJson != "null") + plugin.Settings = JsonSerializer.Deserialize(settingsJson, plugin.Settings.GetType()); + } + catch + { + //Invalid settings file, remove and recreate + File.Delete(settingsFile); + createFile = true; + } + } + + if (createFile) + { + string settingsJson = JsonSerializer.Serialize(plugin.Settings); + string settingsDirectory = new FileInfo(settingsFile).DirectoryName; + if (!Directory.Exists(settingsDirectory)) + { + Directory.CreateDirectory(settingsDirectory); + } + File.WriteAllText(settingsFile, settingsJson); + } + } + + public static Dictionary GetSettingDisplayNames(object settings) + { + var settingNames = new Dictionary(); + + var properties = settings.GetType().GetProperties(); + foreach (var property in properties) + { + var attrib = property.GetCustomAttribute(); + if (attrib == null) + { + settingNames.Add(property, property.Name); + } + else + { + settingNames.Add(property, attrib.DisplayName); + } + } + return settingNames; + } + + public void SaveSettings(IObservatoryPlugin plugin, object settings) + { + string settingsFile = GetSettingsFile(plugin); + + string settingsJson = JsonSerializer.Serialize(settings, new JsonSerializerOptions() + { + ReferenceHandler = System.Text.Json.Serialization.ReferenceHandler.Preserve + }); + string settingsDirectory = new FileInfo(settingsFile).DirectoryName; + if (!Directory.Exists(settingsDirectory)) + { + Directory.CreateDirectory(settingsDirectory); + } + File.WriteAllText(settingsFile, settingsJson); + + } + + private static string GetSettingsFile(IObservatoryPlugin plugin) + { + var configDirectory = new FileInfo(ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.PerUserRoamingAndLocal).FilePath).Directory; + return configDirectory.FullName + "\\" + plugin.Name + ".json"; + } + + private static Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) + { + AppDomain appDomain = (AppDomain)sender; + var assemblies = appDomain.GetAssemblies(); + + if (args.Name.ToLower().Contains(".resources")) + { + return null; + } + + if (assemblies.FirstOrDefault(x => x.FullName == args.Name) == null) + { + AssemblyName assemblyName = new AssemblyName(args.Name); + System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyName(assemblyName); + } + + var assembly = assemblies.FirstOrDefault(x => x.FullName == args.Name); + return assembly; + } + + private static List LoadPlugins(out List<(IObservatoryWorker plugin, PluginStatus signed)> observatoryWorkers, out List<(IObservatoryNotifier plugin, PluginStatus signed)> observatoryNotifiers) + { + observatoryWorkers = new(); + observatoryNotifiers = new(); + var errorList = new List(); + + string pluginPath = $".{Path.DirectorySeparatorChar}plugins"; + + if (Directory.Exists(pluginPath)) + { + + var pluginLibraries = Directory.GetFiles($".{Path.DirectorySeparatorChar}plugins", "*.dll"); + var coreToken = Assembly.GetExecutingAssembly().GetName().GetPublicKeyToken(); + foreach (var dll in pluginLibraries) + { + try + { + var pluginToken = AssemblyName.GetAssemblyName(dll).GetPublicKeyToken(); + PluginStatus signed; + + if (pluginToken.Length == 0) + { + errorList.Add($"Warning: {dll} not signed."); + signed = PluginStatus.Unsigned; + } + else if (!coreToken.SequenceEqual(pluginToken)) + { + errorList.Add($"Warning: {dll} signature does not match."); + signed = PluginStatus.InvalidSignature; + } + else + { + errorList.Add($"OK: {dll} signed."); + signed = PluginStatus.Signed; + } + + if (signed == PluginStatus.Signed || Properties.Core.Default.AllowUnsigned) + { + string error = LoadPluginAssembly(dll, observatoryWorkers, observatoryNotifiers); + if (!string.IsNullOrWhiteSpace(error)) + { + errorList.Add(error); + } + } + else + { + LoadPlaceholderPlugin(dll, signed, observatoryNotifiers); + } + + + } + catch (Exception ex) + { + errorList.Add($"ERROR: {new FileInfo(dll).Name}, {ex.Message}"); + LoadPlaceholderPlugin(dll, PluginStatus.InvalidLibrary, observatoryNotifiers); + } + } + } + return errorList; + } + + private static string LoadPluginAssembly(string dllPath, List<(IObservatoryWorker plugin, PluginStatus signed)> workers, List<(IObservatoryNotifier plugin, PluginStatus signed)> notifiers) + { + var pluginAssembly = System.Runtime.Loader.AssemblyLoadContext.Default.LoadFromAssemblyPath(new FileInfo(dllPath).FullName); + Type[] types; + string err = string.Empty; + try + { + types = pluginAssembly.GetTypes(); + } + catch (ReflectionTypeLoadException ex) + { + types = ex.Types.Where(t => t != null).ToArray(); + } + catch + { + types = Array.Empty(); + } + + var workerTypes = types.Where(t => t.IsAssignableTo(typeof(IObservatoryWorker))); + foreach (var worker in workerTypes) + { + ConstructorInfo constructor = worker.GetConstructor(Array.Empty()); + object instance = constructor.Invoke(Array.Empty()); + workers.Add((instance as IObservatoryWorker, PluginStatus.Signed)); + } + + var notifyTypes = types.Where(t => t.IsAssignableTo(typeof(IObservatoryNotifier))); + foreach (var notifier in notifyTypes) + { + ConstructorInfo constructor = notifier.GetConstructor(Array.Empty()); + object instance = constructor.Invoke(Array.Empty()); + notifiers.Add((instance as IObservatoryNotifier, PluginStatus.Signed)); + } + + if (workerTypes.Count() + notifyTypes.Count() == 0) + { + err += $"ERROR: Library '{dllPath}' contains no suitable interfaces."; + LoadPlaceholderPlugin(dllPath, PluginStatus.InvalidPlugin, notifiers); + } + + return err; + } + + private static void LoadPlaceholderPlugin(string dllPath, PluginStatus pluginStatus, List<(IObservatoryNotifier plugin, PluginStatus signed)> notifiers) + { + PlaceholderPlugin placeholder = new(new FileInfo(dllPath).Name); + notifiers.Add((placeholder, pluginStatus)); + } + + public enum PluginStatus + { + Signed, + Unsigned, + InvalidSignature, + InvalidPlugin, + InvalidLibrary + } + } +} diff --git a/ObservatoryCore/Properties/Core.Designer.cs b/ObservatoryCore/Properties/Core.Designer.cs new file mode 100644 index 0000000..edf8603 --- /dev/null +++ b/ObservatoryCore/Properties/Core.Designer.cs @@ -0,0 +1,62 @@ +//------------------------------------------------------------------------------ +// +// 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. +// +//------------------------------------------------------------------------------ + +namespace Observatory.Properties { + + + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "16.8.1.0")] + internal sealed partial class Core : global::System.Configuration.ApplicationSettingsBase { + + private static Core defaultInstance = ((Core)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Core()))); + + public static Core Default { + get { + return defaultInstance; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("")] + public string JournalFolder { + get { + return ((string)(this["JournalFolder"])); + } + set { + this["JournalFolder"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("False")] + public bool AllowUnsigned { + get { + return ((bool)(this["AllowUnsigned"])); + } + set { + this["AllowUnsigned"] = value; + } + } + + [global::System.Configuration.UserScopedSettingAttribute()] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Configuration.DefaultSettingValueAttribute("True")] + public bool NativeNotify { + get { + return ((bool)(this["NativeNotify"])); + } + set { + this["NativeNotify"] = value; + } + } + } +} diff --git a/ObservatoryCore/Properties/Core.settings b/ObservatoryCore/Properties/Core.settings new file mode 100644 index 0000000..1e312c9 --- /dev/null +++ b/ObservatoryCore/Properties/Core.settings @@ -0,0 +1,15 @@ + + + + + + + + + False + + + True + + + \ No newline at end of file diff --git a/ObservatoryCore/UI/MainApplication.axaml b/ObservatoryCore/UI/MainApplication.axaml new file mode 100644 index 0000000..3e8e3c3 --- /dev/null +++ b/ObservatoryCore/UI/MainApplication.axaml @@ -0,0 +1,13 @@ + + + + + + + + + + \ No newline at end of file diff --git a/ObservatoryCore/UI/MainApplication.axaml.cs b/ObservatoryCore/UI/MainApplication.axaml.cs new file mode 100644 index 0000000..eb488a2 --- /dev/null +++ b/ObservatoryCore/UI/MainApplication.axaml.cs @@ -0,0 +1,29 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Markup.Xaml; +using Observatory.UI.ViewModels; + +namespace Observatory.UI +{ + public class MainApplication : Application + { + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + var pluginManager = PluginManagement.PluginManager.GetInstance; + desktop.MainWindow = new Views.MainWindow() + { + DataContext = new MainWindowViewModel(pluginManager) + }; + } + + base.OnFrameworkInitializationCompleted(); + } + } +} diff --git a/ObservatoryCore/UI/Models/BasicUIModel.cs b/ObservatoryCore/UI/Models/BasicUIModel.cs new file mode 100644 index 0000000..143ba37 --- /dev/null +++ b/ObservatoryCore/UI/Models/BasicUIModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Observatory.UI.Models +{ + public class BasicUIModel + { + public string Time { get; set; } + public string Description { get; set; } + } +} diff --git a/ObservatoryCore/UI/Models/CoreModel.cs b/ObservatoryCore/UI/Models/CoreModel.cs new file mode 100644 index 0000000..bfbdc5e --- /dev/null +++ b/ObservatoryCore/UI/Models/CoreModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Observatory.UI.ViewModels; + +namespace Observatory.UI.Models +{ + public class CoreModel + { + public string Name { get; set; } + public ViewModelBase UI { get; set; } + + } +} diff --git a/ObservatoryCore/UI/Models/NotificationModel.cs b/ObservatoryCore/UI/Models/NotificationModel.cs new file mode 100644 index 0000000..6fbde33 --- /dev/null +++ b/ObservatoryCore/UI/Models/NotificationModel.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Observatory.UI.Models +{ + public class NotificationModel + { + public string Title { get; set; } + public string Detail { get; set; } + } +} diff --git a/ObservatoryCore/UI/TabTemplateSelector.cs b/ObservatoryCore/UI/TabTemplateSelector.cs new file mode 100644 index 0000000..a9dc4ca --- /dev/null +++ b/ObservatoryCore/UI/TabTemplateSelector.cs @@ -0,0 +1,31 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Avalonia.Metadata; +using Observatory.UI.Views; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Observatory.UI +{ + public class TabTemplateSelector : IDataTemplate + { + public bool SupportsRecycling => false; + + [Content] + public Dictionary Templates { get; } = new Dictionary(); + + + public IControl Build(object param) + { + return new BasicUIView(); //Templates[param].Build(param); + } + + public bool Match(object data) + { + return data is BasicUIView; + } + } +} diff --git a/ObservatoryCore/UI/ViewLocator.cs b/ObservatoryCore/UI/ViewLocator.cs new file mode 100644 index 0000000..1b76b96 --- /dev/null +++ b/ObservatoryCore/UI/ViewLocator.cs @@ -0,0 +1,32 @@ +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Observatory.UI.ViewModels; +using System; + +namespace Observatory.UI +{ + public class ViewLocator : IDataTemplate + { + public bool SupportsRecycling => false; + + public IControl Build(object data) + { + var name = data.GetType().FullName!.Replace("ViewModel", "View"); + var type = Type.GetType(name); + + if (type != null) + { + return (Control)Activator.CreateInstance(type)!; + } + else + { + return new TextBlock { Text = "Not Found: " + name }; + } + } + + public bool Match(object data) + { + return data is ViewModelBase; + } + } +} diff --git a/ObservatoryCore/UI/ViewModels/BasicUIViewModel.cs b/ObservatoryCore/UI/ViewModels/BasicUIViewModel.cs new file mode 100644 index 0000000..c0a0c80 --- /dev/null +++ b/ObservatoryCore/UI/ViewModels/BasicUIViewModel.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Data; +using System.Threading.Tasks; +using System.Collections.ObjectModel; +using Observatory.UI.Models; +using ReactiveUI; +using System.Reactive.Linq; +using Observatory.Framework; + +namespace Observatory.UI.ViewModels +{ + public class BasicUIViewModel : ViewModelBase + { + private ObservableCollection basicUIGrid; + + + public ObservableCollection BasicUIGrid + { + get => basicUIGrid; + set + { + basicUIGrid = value; + this.RaisePropertyChanged(nameof(BasicUIGrid)); + } + } + + public BasicUIViewModel(ObservableCollection BasicUIGrid) + { + + this.BasicUIGrid = new(); + this.BasicUIGrid = BasicUIGrid; + + //// Create a timer and set a two second interval. + //var aTimer = new System.Timers.Timer(); + //aTimer.Interval = 2000; + + //// Hook up the Elapsed event for the timer. + //aTimer.Elapsed += OnTimedEvent; + + //// Have the timer fire repeated events (true is the default) + //aTimer.AutoReset = true; + + //// Start the timer + //aTimer.Enabled = true; + } + + private PluginUI.UIType uiType; + + public PluginUI.UIType UIType + { + get => uiType; + set + { + uiType = value; + this.RaisePropertyChanged(nameof(UIType)); + } + } + + private void OnTimedEvent(object sender, System.Timers.ElapsedEventArgs e) + { + basicUIGrid.Count(); + } + } +} diff --git a/ObservatoryCore/UI/ViewModels/CoreViewModel.cs b/ObservatoryCore/UI/ViewModels/CoreViewModel.cs new file mode 100644 index 0000000..1499740 --- /dev/null +++ b/ObservatoryCore/UI/ViewModels/CoreViewModel.cs @@ -0,0 +1,110 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Data; +using System.Linq; +using Avalonia.Controls; +using Observatory.Framework.Interfaces; +using Observatory.UI.Models; +using ReactiveUI; + +namespace Observatory.UI.ViewModels +{ + public class CoreViewModel : ViewModelBase + { + private readonly ObservableCollection notifiers; + private readonly ObservableCollection workers; + private readonly ObservableCollection tabs; + private string toggleButtonText; + + public CoreViewModel(IEnumerable<(IObservatoryWorker plugin, PluginManagement.PluginManager.PluginStatus signed)> workers, IEnumerable<(IObservatoryNotifier plugin, PluginManagement.PluginManager.PluginStatus signed)> notifiers) + { + this.notifiers = new ObservableCollection(notifiers.Select(p => p.plugin)); + this.workers = new ObservableCollection(workers.Select(p => p.plugin)); + ToggleButtonText = "Start Monitor"; + tabs = new ObservableCollection(); + + foreach(var worker in workers.Select(p => p.plugin)) + { + if (worker.PluginUI.PluginUIType == Framework.PluginUI.UIType.Basic) + { + CoreModel coreModel = new(); + coreModel.Name = worker.ShortName; + coreModel.UI = new(); + var uiViewModel = new BasicUIViewModel(worker.PluginUI.DataGrid) + { + UIType = worker.PluginUI.PluginUIType + }; + coreModel.UI = uiViewModel; + + tabs.Add(coreModel); + } + } + + foreach(var notifier in notifiers.Select(p => p.plugin)) + { + Panel notifierPanel = new Panel(); + TextBlock notifierTextBlock = new TextBlock(); + notifierTextBlock.Text = notifier.Name; + notifierPanel.Children.Add(notifierTextBlock); + //tabs.Add(new CoreModel() { Name = notifier.ShortName, UI = (ViewModelBase)notifier.UI }); + } + + + tabs.Add(new CoreModel() { Name = "Core", UI = new BasicUIViewModel(new ObservableCollection()) { UIType = Framework.PluginUI.UIType.Core } }); + + } + + public void ReadAll() + { + foreach (var worker in workers) + { + worker.ReadAllStarted(); + } + LogMonitor.GetInstance.ReadAllJournals(); + } + + public void ToggleMonitor() + { + var logMonitor = LogMonitor.GetInstance; + + if (logMonitor.IsMonitoring()) + { + logMonitor.Stop(); + ToggleButtonText = "Start Monitor"; + } + else + { + logMonitor.Start(); + ToggleButtonText = "Stop Monitor"; + } + } + + public string ToggleButtonText + { + get => toggleButtonText; + set + { + if (toggleButtonText != value) + { + toggleButtonText = value; + this.RaisePropertyChanged(nameof(ToggleButtonText)); + } + } + } + + public ObservableCollection Workers + { + get { return workers; } + } + + public ObservableCollection Notifiers + { + get { return notifiers; } + } + + public ObservableCollection Tabs + { + get { return tabs; } + } + } +} diff --git a/ObservatoryCore/UI/ViewModels/MainWindowViewModel.cs b/ObservatoryCore/UI/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..0a5935e --- /dev/null +++ b/ObservatoryCore/UI/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Observatory.UI.ViewModels +{ + public class MainWindowViewModel : ViewModelBase + { + public MainWindowViewModel(PluginManagement.PluginManager pluginManager) + { + core = new CoreViewModel(pluginManager.workerPlugins, pluginManager.notifyPlugins); + } + + public CoreViewModel core { get; } + } +} diff --git a/ObservatoryCore/UI/ViewModels/NotificationViewModel.cs b/ObservatoryCore/UI/ViewModels/NotificationViewModel.cs new file mode 100644 index 0000000..8a3ccf1 --- /dev/null +++ b/ObservatoryCore/UI/ViewModels/NotificationViewModel.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Observatory.UI.ViewModels +{ + public class NotificationViewModel : ViewModelBase + { + public NotificationViewModel(string title, string detail) + { + Notification = new() { Title = title, Detail = detail }; + } + + public Models.NotificationModel Notification; + } +} diff --git a/ObservatoryCore/UI/ViewModels/ViewModelBase.cs b/ObservatoryCore/UI/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..3d2314a --- /dev/null +++ b/ObservatoryCore/UI/ViewModels/ViewModelBase.cs @@ -0,0 +1,11 @@ +using ReactiveUI; +using System; +using System.Collections.Generic; +using System.Text; + +namespace Observatory.UI.ViewModels +{ + public class ViewModelBase : ReactiveObject + { + } +} diff --git a/ObservatoryCore/UI/Views/BasicUIView.axaml b/ObservatoryCore/UI/Views/BasicUIView.axaml new file mode 100644 index 0000000..e97c7d1 --- /dev/null +++ b/ObservatoryCore/UI/Views/BasicUIView.axaml @@ -0,0 +1,10 @@ + + + + + diff --git a/ObservatoryCore/UI/Views/BasicUIView.axaml.cs b/ObservatoryCore/UI/Views/BasicUIView.axaml.cs new file mode 100644 index 0000000..5ce8016 --- /dev/null +++ b/ObservatoryCore/UI/Views/BasicUIView.axaml.cs @@ -0,0 +1,404 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data; +using Avalonia.Markup.Xaml; +using System.Text.RegularExpressions; +using Observatory.Framework; +using Observatory.Framework.Interfaces; +using System.Linq; +using Avalonia.Interactivity; +using Avalonia.VisualTree; + +namespace Observatory.UI.Views +{ + public class BasicUIView : UserControl + { + public BasicUIView() + { + Initialized += OnInitialized; + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + public static readonly DirectProperty UITypeProperty = + AvaloniaProperty.RegisterDirect( + nameof(UIType), + o => o.UIType, + (o, v) => o.UIType = v, + PluginUI.UIType.None, + BindingMode.OneWay + ); + + public PluginUI.UIType UIType + { + get + { + return _uitype; + } + set + { + _uitype = value; + UITypeChange(); + } + } + + private PluginUI.UIType _uitype; + + + private void ColumnGeneration(object sender, DataGridAutoGeneratingColumnEventArgs e) + { + e.Column.Header = SplitCamelCase(e.PropertyName); + e.Column.CanUserReorder = true; + e.Column.CanUserResize = true; + e.Column.CanUserSort = true; + } + + private void OnInitialized(object sender, System.EventArgs e) + { + + } + + private void UITypeChange() + { + var uiPanel = this.Find("UIPanel"); + + switch (UIType) + { + case PluginUI.UIType.None: + break; + case PluginUI.UIType.Basic: + DataGrid dataGrid = new() + { + [!DataGrid.ItemsProperty] = new Binding("BasicUIGrid"), + SelectionMode = DataGridSelectionMode.Extended, + GridLinesVisibility = DataGridGridLinesVisibility.Vertical, + AutoGenerateColumns = true + }; + dataGrid.AutoGeneratingColumn += ColumnGeneration; + uiPanel.Children.Clear(); + uiPanel.Children.Add(dataGrid); + break; + case PluginUI.UIType.Avalonia: + break; + case PluginUI.UIType.Core: + uiPanel.Children.Clear(); + ScrollViewer scrollViewer = new(); + scrollViewer.Content = GenerateCoreUI(); + uiPanel.Children.Add(scrollViewer); + break; + default: + break; + } + + + } + + private Grid GenerateCoreUI() + { + + Grid corePanel = new(); + + ColumnDefinitions columns = new() + { + new ColumnDefinition() { Width = new GridLength(0, GridUnitType.Auto) }, + new ColumnDefinition() { Width = new GridLength(300) }, + new ColumnDefinition() { Width = new GridLength(0, GridUnitType.Auto) } + }; + corePanel.ColumnDefinitions = columns; + + RowDefinitions rows = new() + { + new RowDefinition() { Height = new GridLength(0, GridUnitType.Auto) }, + new RowDefinition() { Height = new GridLength(0, GridUnitType.Auto) } + }; + corePanel.RowDefinitions = rows; + + var pluginManager = PluginManagement.PluginManager.GetInstance; + + #region Journal Location + TextBlock journalPathLabel = new() + { + Text = "Journal Path: ", + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, + VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center + }; + + TextBox journalPath = new() + { + Text = Properties.Core.Default.JournalFolder + }; + + Button journalBrowse = new() + { + Content = "Browse", + Height = 30, + Width = 100, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right, + HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center + }; + + journalBrowse.Click += (object source, RoutedEventArgs e) => + { + OpenFolderDialog openFolderDialog = new() + { + Directory = journalPath.Text + }; + var browseTask = openFolderDialog.ShowAsync((Window)((Button)source).GetVisualRoot()); + string path = browseTask.Result; + if (path != string.Empty) + { + journalPath.Text = path; + Properties.Core.Default.JournalFolder = path; + Properties.Core.Default.Save(); + } + }; + + corePanel.AddControl(journalPathLabel, 1, 0); + corePanel.AddControl(journalPath, 1, 1); + corePanel.AddControl(journalBrowse, 1, 2); + + #endregion + + #region Plugin List + DataGrid pluginList = new() { Margin = new Thickness(0, 20) }; + + pluginList.Columns.Add(new DataGridTextColumn() + { + Header = "Plugin", + Binding = new Binding("Name") + }); + + pluginList.Columns.Add(new DataGridTextColumn() + { + Header = "Version", + Binding = new Binding("Version") + }); + + pluginList.Columns.Add(new DataGridTextColumn() + { + Header = "Status", + Binding = new Binding("Status") + }); + + System.Collections.Generic.List allPlugins = new(); + + foreach(var (plugin, signed) in pluginManager.workerPlugins) + { + allPlugins.Add(new PluginView() { Name = plugin.Name, Version = plugin.Version, Status = GetStatusText(signed) }); + } + + foreach (var (plugin, signed) in pluginManager.notifyPlugins) + { + allPlugins.Add(new PluginView() { Name = plugin.Name, Version = plugin.Version, Status = GetStatusText(signed) }); + } + + pluginList.Items = allPlugins; + corePanel.AddControl(pluginList, 0, 0, 2); + + #endregion + + #region Plugin Settings + + foreach(var plugin in pluginManager.workerPlugins.Select(p => p.plugin)) + { + GeneratePluginSettingUI(corePanel, plugin); + } + + #endregion + + return corePanel; + } + + private void GeneratePluginSettingUI(Grid gridPanel, IObservatoryPlugin plugin) + { + //var plugin = pluginSettings.Key; + + var displayedSettings = PluginManagement.PluginManager.GetSettingDisplayNames(plugin.Settings); + + if (displayedSettings.Count > 0) + { + Expander expander = new() + { + Header = $"{plugin.Name} - {plugin.Version}", + DataContext = plugin.Settings, + Margin = new Thickness(0, 20) + }; + + Grid settingsGrid = new(); + ColumnDefinitions settingColumns = new() + { + new ColumnDefinition() { Width = new GridLength(3, GridUnitType.Star) }, + new ColumnDefinition() { Width = new GridLength(3, GridUnitType.Star) }, + new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) } + }; + settingsGrid.ColumnDefinitions = settingColumns; + expander.Content = settingsGrid; + + int nextRow = gridPanel.RowDefinitions.Count; + gridPanel.RowDefinitions.Add(new RowDefinition()); + gridPanel.AddControl(expander, nextRow, 0, 3); + + foreach (var setting in displayedSettings.Where(s => !System.Attribute.IsDefined(s.Key, typeof(SettingIgnore)))) + { + if (setting.Key.PropertyType != typeof(bool) || settingsGrid.Children.Count % 2 == 0) + { + settingsGrid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(21) }); + } + + TextBlock label = new() { Text = setting.Value }; + + switch (setting.Key.GetValue(plugin.Settings)) + { + case bool boolSetting: + CheckBox checkBox = new() { IsChecked = boolSetting, Content = label }; + + checkBox.Checked += (object sender, RoutedEventArgs e) => + { + setting.Key.SetValue(plugin.Settings, true); + PluginManagement.PluginManager.GetInstance.SaveSettings(plugin, plugin.Settings); + }; + + checkBox.Unchecked += (object sender, RoutedEventArgs e) => + { + setting.Key.SetValue(plugin.Settings, false); + PluginManagement.PluginManager.GetInstance.SaveSettings(plugin, plugin.Settings); + }; + + //settingsGrid.Children.Add(checkBox); + settingsGrid.AddControl(checkBox, settingsGrid.RowDefinitions.Count - 1, settingsGrid.Children.Count % 2 == 0 ? 0 : 1); + + break; + case string stringSetting: + TextBox textBox = new() { Text = stringSetting }; + settingsGrid.Children.Add(label); + settingsGrid.Children.Add(textBox); + break; + case int intSetting: + NumericUpDown numericUpDown = new() { Text = intSetting.ToString(), AllowSpin = true }; + settingsGrid.Children.Add(label); + settingsGrid.Children.Add(numericUpDown); + break; + case System.IO.FileInfo fileSetting: + label.Text += ": "; + + TextBox settingPath = new() + { + Text = fileSetting.FullName, + Width = 250, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Right + }; + + Button settingBrowse = new() + { + Content = "Browse", + Height = 30, + Width = 100, + HorizontalAlignment = Avalonia.Layout.HorizontalAlignment.Left, + HorizontalContentAlignment = Avalonia.Layout.HorizontalAlignment.Center + }; + + settingBrowse.Click += (object source, RoutedEventArgs e) => + { + OpenFileDialog openFileDialog = new() + { + Directory = fileSetting.DirectoryName, + AllowMultiple = false + }; + var browseTask = openFileDialog.ShowAsync((Window)((Button)source).GetVisualRoot()); + + if (browseTask.Result.Count() > 0) + { + string path = browseTask.Result[0]; + settingPath.Text = path; + + setting.Key.SetValue(plugin.Settings, new System.IO.FileInfo(path)); + PluginManagement.PluginManager.GetInstance.SaveSettings(plugin, plugin.Settings); + + } + }; + + StackPanel stackPanel = new() { Orientation = Avalonia.Layout.Orientation.Horizontal }; + stackPanel.Children.Add(label); + stackPanel.Children.Add(settingPath); + + settingsGrid.RowDefinitions.Add(new RowDefinition() { Height = new GridLength(21) }); + //settingsGrid.AddControl(label, settingsGrid.RowDefinitions.Count - 1, 0, 2); + settingsGrid.AddControl(stackPanel, settingsGrid.RowDefinitions.Count - 1, 0, 2); + settingsGrid.AddControl(settingBrowse, settingsGrid.RowDefinitions.Count - 1, 2); + + break; + } + //wrapPanel.Children.Add(panel); + } + } + } + + private string GetStatusText(PluginManagement.PluginManager.PluginStatus status) + { + string statusText; + + switch (status) + { + case PluginManagement.PluginManager.PluginStatus.Signed: + statusText = "Signed"; + break; + case PluginManagement.PluginManager.PluginStatus.Unsigned: + statusText = "Unsigned"; + break; + case PluginManagement.PluginManager.PluginStatus.InvalidSignature: + statusText = "Signature Invalid"; + break; + case PluginManagement.PluginManager.PluginStatus.InvalidPlugin: + statusText = "No Interface"; + break; + case PluginManagement.PluginManager.PluginStatus.InvalidLibrary: + statusText = "Invalid Library"; + break; + default: + statusText = "Unknown"; + break; + } + + return statusText; + } + + //From https://stackoverflow.com/questions/5796383/insert-spaces-between-words-on-a-camel-cased-token + private static string SplitCamelCase(string str) + { + return Regex.Replace( + Regex.Replace( + str, + @"(\P{Ll})(\P{Ll}\p{Ll})", + "$1 $2" + ), + @"(\p{Ll})(\P{Ll})", + "$1 $2" + ); + } + } + + internal class PluginView + { + public string Name { get; set; } + public string Version { get; set; } + public string Status { get; set; } + } + + + + + internal static class GridExtention + { + public static void AddControl(this Grid grid, Control control, int row, int column, int span = 1) + { + grid.Children.Add(control); + Grid.SetColumnSpan(control, span); + Grid.SetColumn(control, column); + Grid.SetRow(control, row); + } + } +} diff --git a/ObservatoryCore/UI/Views/CoreView.axaml b/ObservatoryCore/UI/Views/CoreView.axaml new file mode 100644 index 0000000..6437302 --- /dev/null +++ b/ObservatoryCore/UI/Views/CoreView.axaml @@ -0,0 +1,43 @@ + + + + Elite Observatory - v1.0core + + + + + + + + + + + + + + + + + + + + + + + diff --git a/ObservatoryCore/UI/Views/CoreView.axaml.cs b/ObservatoryCore/UI/Views/CoreView.axaml.cs new file mode 100644 index 0000000..9094b8b --- /dev/null +++ b/ObservatoryCore/UI/Views/CoreView.axaml.cs @@ -0,0 +1,21 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using System.Linq; + +namespace Observatory.UI.Views +{ + public class CoreView : UserControl + { + public CoreView() + { + InitializeComponent(); + + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/ObservatoryCore/UI/Views/MainWindow.axaml b/ObservatoryCore/UI/Views/MainWindow.axaml new file mode 100644 index 0000000..484fd1a --- /dev/null +++ b/ObservatoryCore/UI/Views/MainWindow.axaml @@ -0,0 +1,11 @@ + + \ No newline at end of file diff --git a/ObservatoryCore/UI/Views/MainWindow.axaml.cs b/ObservatoryCore/UI/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..198792d --- /dev/null +++ b/ObservatoryCore/UI/Views/MainWindow.axaml.cs @@ -0,0 +1,26 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using System.Collections.Generic; +using System.Data; + + +namespace Observatory.UI.Views +{ + public class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/ObservatoryCore/UI/Views/NotificationView.axaml b/ObservatoryCore/UI/Views/NotificationView.axaml new file mode 100644 index 0000000..0125b59 --- /dev/null +++ b/ObservatoryCore/UI/Views/NotificationView.axaml @@ -0,0 +1,13 @@ + + + + + + diff --git a/ObservatoryCore/UI/Views/NotificationView.axaml.cs b/ObservatoryCore/UI/Views/NotificationView.axaml.cs new file mode 100644 index 0000000..89bdb28 --- /dev/null +++ b/ObservatoryCore/UI/Views/NotificationView.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Observatory.UI.Views +{ + public partial class NotificationView : Window + { + public NotificationView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +}