diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index f99fab4de..00c64717e 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -53,6 +53,8 @@ True True True + True + True True True True diff --git a/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs new file mode 100644 index 000000000..e2f3a850d --- /dev/null +++ b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs @@ -0,0 +1,30 @@ +using System; + +namespace Artemis.Core +{ + /// + /// An exception thrown when a plugin prerequisite-related error occurs + /// + public class ArtemisPluginPrerequisiteException : Exception + { + internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject) + { + Subject = subject; + } + + internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message) : base(message) + { + Subject = subject; + } + + internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message, Exception inner) : base(message, inner) + { + Subject = subject; + } + + /// + /// Gets the subject the error is related to + /// + public IPrerequisitesSubject Subject { get; } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Extensions/StreamExtensions.cs b/src/Artemis.Core/Extensions/StreamExtensions.cs new file mode 100644 index 000000000..a6b350fd2 --- /dev/null +++ b/src/Artemis.Core/Extensions/StreamExtensions.cs @@ -0,0 +1,134 @@ +// Based on: https://www.codeproject.com/Tips/5274597/An-Improved-Stream-CopyToAsync-that-Reports-Progre +// The MIT License +// +// Copyright (c) 2020 honey the codewitch +// +// Permission is hereby granted, free of charge, +// to any person obtaining a copy of this software and +// associated documentation files (the "Software"), to +// deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, +// merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom +// the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR +// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + internal static class StreamExtensions + { + private const int DefaultBufferSize = 81920; + + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The length of the source stream, if known - used for progress reporting + /// The destination to copy to + /// The size of the copy block buffer + /// An implementation for reporting progress + /// A cancellation token + /// A task representing the operation + public static async Task CopyToAsync( + this Stream source, + long sourceLength, + Stream destination, + int bufferSize, + IProgress<(long, long)> progress, + CancellationToken cancellationToken) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + if (!source.CanRead) + throw new ArgumentException("Has to be readable", nameof(source)); + if (destination == null) + throw new ArgumentNullException(nameof(destination)); + if (!destination.CanWrite) + throw new ArgumentException("Has to be writable", nameof(destination)); + if (bufferSize <= 0) + bufferSize = DefaultBufferSize; + + byte[] buffer = new byte[bufferSize]; + long totalBytesRead = 0; + int bytesRead; + while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0) + { + await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false); + totalBytesRead += bytesRead; + progress?.Report((totalBytesRead, sourceLength)); + } + + progress?.Report((totalBytesRead, sourceLength)); + cancellationToken.ThrowIfCancellationRequested(); + } + + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The length of the source stream, if known - used for progress reporting + /// The destination to copy to + /// An implementation for reporting progress + /// A cancellation token + /// A task representing the operation + public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken) + { + return CopyToAsync(source, sourceLength, destination, 0, progress, cancellationToken); + } + + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The destination to copy to + /// An implementation for reporting progress + /// A cancellation token + /// A task representing the operation + public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken) + { + return CopyToAsync(source, 0L, destination, 0, progress, cancellationToken); + } + + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The length of the source stream, if known - used for progress reporting + /// The destination to copy to + /// An implementation for reporting progress + /// A task representing the operation + public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress) + { + return CopyToAsync(source, sourceLength, destination, 0, progress, default); + } + + /// + /// Copies a stream to another stream + /// + /// The source to copy from + /// The destination to copy to + /// An implementation for reporting progress + /// A task representing the operation + public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress) + { + return CopyToAsync(source, 0L, destination, 0, progress, default); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEvent.cs b/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEvent.cs index fcf52f32b..9bb880516 100644 --- a/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEvent.cs +++ b/src/Artemis.Core/Models/Profile/DataModel/ValueChangedEvent/DataModelValueChangedEvent.cs @@ -24,6 +24,9 @@ namespace Artemis.Core public void Update() { + if (!Path.IsValid) + return; + object? value = Path.GetValue(); if (value != null) CurrentValue = (T?) value; diff --git a/src/Artemis.Core/Plugins/IPluginBootstrapper.cs b/src/Artemis.Core/Plugins/IPluginBootstrapper.cs deleted file mode 100644 index af4b607d7..000000000 --- a/src/Artemis.Core/Plugins/IPluginBootstrapper.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Artemis.Core -{ - /// - /// An optional entry point for your plugin - /// - public interface IPluginBootstrapper - { - /// - /// Called when the plugin is activated - /// - /// The plugin instance of your plugin - void Enable(Plugin plugin); - - /// - /// Called when the plugin is deactivated or when Artemis shuts down - /// - /// The plugin instance of your plugin - void Disable(Plugin plugin); - } -} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index da4e3f7aa..ddd090bfd 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -25,6 +25,7 @@ namespace Artemis.Core Info = info; Directory = directory; Entity = pluginEntity ?? new PluginEntity {Id = Guid, IsEnabled = true}; + Info.Plugin = this; _features = new List(); } @@ -71,7 +72,7 @@ namespace Artemis.Core /// /// Gets the plugin bootstrapper /// - public IPluginBootstrapper? Bootstrapper { get; internal set; } + public PluginBootstrapper? Bootstrapper { get; internal set; } /// /// The Ninject kernel of the plugin @@ -113,6 +114,17 @@ namespace Artemis.Core { return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T; } + + /// + /// Looks up the feature info the feature of type + /// + /// The type of feature to find + /// Feature info of the feature + public PluginFeatureInfo GetFeatureInfo() where T : PluginFeature + { + // This should be a safe assumption because any type of PluginFeature is registered and added + return _features.First(i => i.FeatureType == typeof(T)); + } /// public override string ToString() @@ -235,12 +247,12 @@ namespace Artemis.Core if (enable) { - Bootstrapper?.Enable(this); + Bootstrapper?.OnPluginEnabled(this); OnEnabled(); } else { - Bootstrapper?.Disable(this); + Bootstrapper?.OnPluginDisabled(this); OnDisabled(); } } diff --git a/src/Artemis.Core/Plugins/PluginBootstrapper.cs b/src/Artemis.Core/Plugins/PluginBootstrapper.cs new file mode 100644 index 000000000..0d41ad36d --- /dev/null +++ b/src/Artemis.Core/Plugins/PluginBootstrapper.cs @@ -0,0 +1,100 @@ +namespace Artemis.Core +{ + /// + /// An optional entry point for your plugin + /// + public abstract class PluginBootstrapper + { + private Plugin? _plugin; + + /// + /// Called when the plugin is loaded + /// + /// + public virtual void OnPluginLoaded(Plugin plugin) + { + } + + /// + /// Called when the plugin is activated + /// + /// The plugin instance of your plugin + public virtual void OnPluginEnabled(Plugin plugin) + { + } + + /// + /// Called when the plugin is deactivated or when Artemis shuts down + /// + /// The plugin instance of your plugin + public virtual void OnPluginDisabled(Plugin plugin) + { + } + + /// + /// Adds the provided prerequisite to the plugin. + /// + /// The prerequisite to add + public void AddPluginPrerequisite(PluginPrerequisite prerequisite) + { + // TODO: We can keep track of them and add them after load, same goes for the others + if (_plugin == null) + throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded"); + + if (!_plugin.Info.Prerequisites.Contains(prerequisite)) + _plugin.Info.Prerequisites.Add(prerequisite); + } + + /// + /// Removes the provided prerequisite from the plugin. + /// + /// The prerequisite to remove + /// + /// is successfully removed; otherwise . This method also returns + /// if the prerequisite was not found. + /// + public bool RemovePluginPrerequisite(PluginPrerequisite prerequisite) + { + if (_plugin == null) + throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded"); + + return _plugin.Info.Prerequisites.Remove(prerequisite); + } + + /// + /// Adds the provided prerequisite to the feature of type . + /// + /// The prerequisite to add + public void AddFeaturePrerequisite(PluginPrerequisite prerequisite) where T : PluginFeature + { + if (_plugin == null) + throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded"); + + PluginFeatureInfo info = _plugin.GetFeatureInfo(); + if (!info.Prerequisites.Contains(prerequisite)) + info.Prerequisites.Add(prerequisite); + } + + /// + /// Removes the provided prerequisite from the feature of type . + /// + /// The prerequisite to remove + /// + /// is successfully removed; otherwise . This method also returns + /// if the prerequisite was not found. + /// + public bool RemoveFeaturePrerequisite(PluginPrerequisite prerequisite) where T : PluginFeature + { + if (_plugin == null) + throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded"); + + return _plugin.GetFeatureInfo().Prerequisites.Remove(prerequisite); + } + + internal void InternalOnPluginLoaded(Plugin plugin) + { + _plugin = plugin; + OnPluginLoaded(plugin); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginFeature.cs b/src/Artemis.Core/Plugins/PluginFeature.cs index 8b9943a0a..aa661c951 100644 --- a/src/Artemis.Core/Plugins/PluginFeature.cs +++ b/src/Artemis.Core/Plugins/PluginFeature.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading.Tasks; @@ -15,7 +16,7 @@ namespace Artemis.Core private readonly Stopwatch _updateStopwatch = new(); private bool _isEnabled; private Exception? _loadException; - + /// /// Gets the plugin feature info related to this feature /// diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index 47f34cbaa..6806db862 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -1,9 +1,12 @@ using System; +using System.Collections.Generic; +using System.Linq; using Artemis.Core.DataModelExpansions; using Artemis.Core.DeviceProviders; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; using Artemis.Core.Modules; +using Artemis.Storage.Entities.Plugins; using Humanizer; using Newtonsoft.Json; @@ -13,23 +16,24 @@ namespace Artemis.Core /// Represents basic info about a plugin feature and contains a reference to the instance of said feature /// [JsonObject(MemberSerialization.OptIn)] - public class PluginFeatureInfo : CorePropertyChanged + public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject { private string? _description; private string? _icon; private PluginFeature? _instance; private string _name = null!; - internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureAttribute? attribute) + internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureEntity pluginFeatureEntity, PluginFeatureAttribute? attribute) { Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin)); FeatureType = featureType ?? throw new ArgumentNullException(nameof(featureType)); + Entity = pluginFeatureEntity; Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title); Description = attribute?.Description; Icon = attribute?.Icon; AlwaysEnabled = attribute?.AlwaysEnabled ?? false; - + if (Icon != null) return; if (typeof(BaseDataModelExpansion).IsAssignableFrom(featureType)) Icon = "TableAdd"; @@ -46,7 +50,7 @@ namespace Artemis.Core else Icon = "Plugin"; } - + internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance) { if (instance == null) throw new ArgumentNullException(nameof(instance)); @@ -119,6 +123,11 @@ namespace Artemis.Core [JsonProperty] public bool AlwaysEnabled { get; } + /// + /// Gets a boolean indicating whether the feature is enabled in persistent storage + /// + public bool EnabledInStorage => Entity.IsEnabled; + /// /// Gets the feature this info is associated with /// @@ -128,6 +137,14 @@ namespace Artemis.Core internal set => SetAndNotify(ref _instance, value); } + /// + public List Prerequisites { get; } = new(); + + /// + public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet()); + + internal PluginFeatureEntity Entity { get; } + /// public override string ToString() { diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index fcbeacd07..de8a77e7f 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using Newtonsoft.Json; namespace Artemis.Core @@ -8,7 +10,7 @@ namespace Artemis.Core /// Represents basic info about a plugin and contains a reference to the instance of said plugin /// [JsonObject(MemberSerialization.OptIn)] - public class PluginInfo : CorePropertyChanged + public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject { private bool _autoEnableFeatures = true; private string? _description; @@ -117,6 +119,12 @@ namespace Artemis.Core internal set => SetAndNotify(ref _plugin, value); } + /// + public List Prerequisites { get; } = new(); + + /// + public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet()); + /// public override string ToString() { diff --git a/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs new file mode 100644 index 000000000..5401455ed --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Artemis.Core +{ + /// + /// Represents a type that has prerequisites + /// + public interface IPrerequisitesSubject + { + /// + /// Gets a list of prerequisites for this plugin + /// + List Prerequisites { get; } + + /// + /// Determines whether the prerequisites of this plugin are met + /// + bool ArePrerequisitesMet(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs new file mode 100644 index 000000000..a762d7e7c --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs @@ -0,0 +1,127 @@ +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a prerequisite for a or + /// + public abstract class PluginPrerequisite : CorePropertyChanged + { + private PluginPrerequisiteAction? _currentAction; + + /// + /// Gets the name of the prerequisite + /// + public abstract string Name { get; } + + /// + /// Gets the description of the prerequisite + /// + public abstract string Description { get; } + + /// + /// Gets a boolean indicating whether installing or uninstalling this prerequisite requires admin privileges + /// + public abstract bool RequiresElevation { get; } + + /// + /// Gets a list of actions to execute when is called + /// + public abstract List InstallActions { get; } + + /// + /// Gets a list of actions to execute when is called + /// + public abstract List UninstallActions { get; } + + /// + /// Gets or sets the action currently being executed + /// + public PluginPrerequisiteAction? CurrentAction + { + get => _currentAction; + private set => SetAndNotify(ref _currentAction, value); + } + + /// + /// Execute all install actions + /// + public async Task Install(CancellationToken cancellationToken) + { + try + { + OnInstallStarting(); + foreach (PluginPrerequisiteAction installAction in InstallActions) + { + cancellationToken.ThrowIfCancellationRequested(); + CurrentAction = installAction; + await installAction.Execute(cancellationToken); + } + } + finally + { + CurrentAction = null; + OnInstallFinished(); + } + } + + /// + /// Execute all uninstall actions + /// + public async Task Uninstall(CancellationToken cancellationToken) + { + try + { + OnUninstallStarting(); + foreach (PluginPrerequisiteAction uninstallAction in UninstallActions) + { + cancellationToken.ThrowIfCancellationRequested(); + CurrentAction = uninstallAction; + await uninstallAction.Execute(cancellationToken); + } + } + finally + { + CurrentAction = null; + OnUninstallFinished(); + } + } + + /// + /// Called to determine whether the prerequisite is met + /// + /// if the prerequisite is met; otherwise + public abstract bool IsMet(); + + /// + /// Called before installation starts + /// + protected virtual void OnInstallStarting() + { + } + + /// + /// Called after installation finishes + /// + protected virtual void OnInstallFinished() + { + } + + /// + /// Called before uninstall starts + /// + protected virtual void OnUninstallStarting() + { + } + + /// + /// Called after uninstall finished + /// + protected virtual void OnUninstallFinished() + { + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs new file mode 100644 index 000000000..90d9a787e --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs @@ -0,0 +1,96 @@ +using System; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents an action that must be taken to install or uninstall a plugin prerequisite + /// + public abstract class PluginPrerequisiteAction : CorePropertyChanged + { + private bool _progressIndeterminate; + private bool _showProgressBar; + private bool _showSubProgressBar; + private string? _status; + private bool _subProgressIndeterminate; + + /// + /// The base constructor for all plugin prerequisite actions + /// + /// The name of the action + protected PluginPrerequisiteAction(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + #region Implementation of IPluginPrerequisiteAction + + /// + /// Gets the name of the action + /// + public string Name { get; } + + /// + /// Gets or sets the status of the action + /// + public string? Status + { + get => _status; + set => SetAndNotify(ref _status, value); + } + + /// + /// Gets or sets a boolean indicating whether the progress is indeterminate or not + /// + public bool ProgressIndeterminate + { + get => _progressIndeterminate; + set => SetAndNotify(ref _progressIndeterminate, value); + } + + /// + /// Gets or sets a boolean indicating whether the progress is indeterminate or not + /// + public bool SubProgressIndeterminate + { + get => _subProgressIndeterminate; + set => SetAndNotify(ref _subProgressIndeterminate, value); + } + + /// + /// Gets or sets a boolean indicating whether the progress bar should be shown + /// + public bool ShowProgressBar + { + get => _showProgressBar; + set => SetAndNotify(ref _showProgressBar, value); + } + + /// + /// Gets or sets a boolean indicating whether the sub progress bar should be shown + /// + public bool ShowSubProgressBar + { + get => _showSubProgressBar; + set => SetAndNotify(ref _showSubProgressBar, value); + } + + /// + /// Gets or sets the progress of the action (0 to 100) + /// + public PrerequisiteActionProgress Progress { get; } = new(); + + /// + /// Gets or sets the sub progress of the action + /// + public PrerequisiteActionProgress SubProgress { get; } = new(); + + /// + /// Called when the action must execute + /// + public abstract Task Execute(CancellationToken cancellationToken); + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs new file mode 100644 index 000000000..6602567e0 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Humanizer; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that copies a folder + /// + public class CopyFolderAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The source folder to copy + /// The target folder to copy to (will be created if needed) + public CopyFolderAction(string name, string source, string target) : base(name) + { + Source = source; + Target = target; + + ShowProgressBar = true; + ShowSubProgressBar = true; + } + + /// + /// Gets the source directory + /// + public string Source { get; } + + /// + /// Gets or sets the target directory + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + DirectoryInfo source = new(Source); + DirectoryInfo target = new(Target); + + if (!source.Exists) + throw new ArtemisCoreException($"The source directory at '{source}' was not found."); + + int filesCopied = 0; + FileInfo[] files = source.GetFiles("*", SearchOption.AllDirectories); + + foreach (FileInfo fileInfo in files) + { + string outputPath = fileInfo.FullName.Replace(source.FullName, target.FullName); + string outputDir = Path.GetDirectoryName(outputPath)!; + Utilities.CreateAccessibleDirectory(outputDir); + + void SubProgressOnProgressReported(object? sender, EventArgs e) + { + if (SubProgress.ProgressPerSecond != 0) + Status = $"Copying {fileInfo.Name} - {SubProgress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec"; + else + Status = $"Copying {fileInfo.Name}"; + } + + Progress.Report((filesCopied, files.Length)); + SubProgress.ProgressReported += SubProgressOnProgressReported; + + await using FileStream sourceStream = fileInfo.OpenRead(); + await using FileStream destinationStream = File.Create(outputPath); + + await sourceStream.CopyToAsync(fileInfo.Length, destinationStream, SubProgress, cancellationToken); + + filesCopied++; + SubProgress.ProgressReported -= SubProgressOnProgressReported; + } + + Progress.Report((filesCopied, files.Length)); + Status = $"Finished copying {filesCopied} file(s)"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs new file mode 100644 index 000000000..e6324bf8f --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFileAction.cs @@ -0,0 +1,44 @@ +using System.IO; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that deletes a file + /// + public class DeleteFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The target folder to delete recursively + public DeleteFileAction(string name, string target) : base(name) + { + Target = target; + ProgressIndeterminate = true; + } + + /// + /// Gets or sets the target directory + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + ShowProgressBar = true; + Status = $"Removing {Target}"; + + await Task.Run(() => + { + if (File.Exists(Target)) + File.Delete(Target); + }, cancellationToken); + + ShowProgressBar = false; + Status = $"Removed {Target}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs new file mode 100644 index 000000000..62b4dfc41 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DeleteFolderAction.cs @@ -0,0 +1,49 @@ +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that recursively deletes a folder + /// + public class DeleteFolderAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The target folder to delete recursively + public DeleteFolderAction(string name, string target) : base(name) + { + if (Enum.GetValues().Select(Environment.GetFolderPath).Contains(target)) + throw new ArtemisCoreException($"Cannot delete special folder {target}, silly goose."); + + Target = target; + ProgressIndeterminate = true; + } + + /// + /// Gets or sets the target directory + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + ShowProgressBar = true; + Status = $"Removing {Target}"; + + await Task.Run(() => + { + if (Directory.Exists(Target)) + Directory.Delete(Target, true); + }, cancellationToken); + + ShowProgressBar = false; + Status = $"Removed {Target}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs new file mode 100644 index 000000000..0707abfd6 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs @@ -0,0 +1,81 @@ +using System; +using System.IO; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Humanizer; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that downloads a file + /// + public class DownloadFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The source URL to download + /// The target file to save as (will be created if needed) + public DownloadFileAction(string name, string url, string fileName) : base(name) + { + Url = url ?? throw new ArgumentNullException(nameof(url)); + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + + ShowProgressBar = true; + } + + /// + /// Gets the source URL to download + /// + public string Url { get; } + + /// + /// Gets the target file to save as (will be created if needed) + /// + public string FileName { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + using HttpClient client = new(); + await using FileStream destinationStream = new(FileName, FileMode.OpenOrCreate); + + void ProgressOnProgressReported(object? sender, EventArgs e) + { + if (Progress.ProgressPerSecond != 0) + Status = $"Downloading {Url} - {Progress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec"; + else + Status = $"Downloading {Url}"; + } + + Progress.ProgressReported += ProgressOnProgressReported; + + // Get the http headers first to examine the content length + using HttpResponseMessage response = await client.GetAsync(Url, HttpCompletionOption.ResponseHeadersRead, cancellationToken); + await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken); + long? contentLength = response.Content.Headers.ContentLength; + + // Ignore progress reporting when no progress reporter was + // passed or when the content length is unknown + if (!contentLength.HasValue) + { + ProgressIndeterminate = true; + await download.CopyToAsync(destinationStream, Progress, cancellationToken); + ProgressIndeterminate = false; + } + else + { + ProgressIndeterminate = false; + await download.CopyToAsync(contentLength.Value, destinationStream, Progress, cancellationToken); + } + + cancellationToken.ThrowIfCancellationRequested(); + + Progress.ProgressReported -= ProgressOnProgressReported; + Progress.Report((1, 1)); + Status = "Finished downloading"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs new file mode 100644 index 000000000..491e28f04 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs @@ -0,0 +1,88 @@ +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that executes a file + /// + public class ExecuteFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of + /// + /// The name of the action + /// The target file to execute + /// A set of command-line arguments to use when starting the application + /// A boolean indicating whether the action should wait for the process to exit + public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true) : base(name) + { + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + Arguments = arguments; + WaitForExit = waitForExit; + } + + /// + /// Gets the target file to execute + /// + public string FileName { get; } + + /// + /// Gets a set of command-line arguments to use when starting the application + /// + public string? Arguments { get; } + + /// + /// Gets a boolean indicating whether the action should wait for the process to exit + /// + public bool WaitForExit { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + if (WaitForExit) + { + Status = $"Running {FileName} and waiting for exit.."; + ShowProgressBar = true; + ProgressIndeterminate = true; + + int result = await RunProcessAsync(FileName, Arguments); + + Status = $"{FileName} exited with code {result}"; + } + else + { + Status = $"Running {FileName}"; + Process process = new() + { + StartInfo = {FileName = FileName, Arguments = Arguments!}, + EnableRaisingEvents = true + }; + process.Start(); + } + } + + private static Task RunProcessAsync(string fileName, string? arguments) + { + TaskCompletionSource tcs = new(); + + Process process = new() + { + StartInfo = {FileName = fileName, Arguments = arguments!}, + EnableRaisingEvents = true + }; + + process.Exited += (_, _) => + { + tcs.SetResult(process.ExitCode); + process.Dispose(); + }; + + process.Start(); + + return tcs.Task; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs new file mode 100644 index 000000000..b09f076b9 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs @@ -0,0 +1,83 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that extracts a ZIP file + /// + public class ExtractArchiveAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of . + /// + /// The name of the action + /// The ZIP file to extract + /// The folder into which to extract the file + public ExtractArchiveAction(string name, string fileName, string target) : base(name) + { + FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); + Target = target ?? throw new ArgumentNullException(nameof(target)); + + ShowProgressBar = true; + } + + /// + /// Gets the file to extract + /// + public string FileName { get; } + + /// + /// Gets the folder into which to extract the file + /// + public string Target { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + using HttpClient client = new(); + + ShowSubProgressBar = true; + Status = $"Extracting {FileName}"; + + Utilities.CreateAccessibleDirectory(Target); + + await using (FileStream fileStream = new(FileName, FileMode.Open)) + { + ZipArchive archive = new(fileStream); + long count = 0; + foreach (ZipArchiveEntry entry in archive.Entries) + { + await using Stream unzippedEntryStream = entry.Open(); + Progress.Report((count, archive.Entries.Count)); + if (entry.Length > 0) + { + string path = Path.Combine(Target, entry.FullName); + CreateDirectoryForFile(path); + await using Stream extractStream = new FileStream(path, FileMode.OpenOrCreate); + await unzippedEntryStream.CopyToAsync(entry.Length, extractStream, SubProgress, cancellationToken); + } + + count++; + } + } + + Progress.Report((1, 1)); + ShowSubProgressBar = false; + Status = "Finished extracting"; + } + + private static void CreateDirectoryForFile(string path) + { + string? directory = Path.GetDirectoryName(path); + if (directory == null) + throw new ArtemisCoreException($"Failed to get directory from path {path}"); + + Utilities.CreateAccessibleDirectory(directory); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs new file mode 100644 index 000000000..92c7d0294 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteBytesToFileAction.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that copies a folder + /// + public class WriteBytesToFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The target file to write to (will be created if needed) + /// The contents to write + public WriteBytesToFileAction(string name, string target, byte[] content) : base(name) + { + Target = target; + ByteContent = content ?? throw new ArgumentNullException(nameof(content)); + } + + /// + /// Gets or sets the target file + /// + public string Target { get; } + + /// + /// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to + /// the file will be deleted and recreated + /// + public bool Append { get; set; } = false; + + /// + /// Gets the bytes that will be written + /// + public byte[] ByteContent { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + string outputDir = Path.GetDirectoryName(Target)!; + Utilities.CreateAccessibleDirectory(outputDir); + + ShowProgressBar = true; + Status = $"Writing to {Path.GetFileName(Target)}..."; + + if (!Append && File.Exists(Target)) + File.Delete(Target); + + await using Stream fileStream = File.OpenWrite(Target); + await using MemoryStream sourceStream = new(ByteContent); + await sourceStream.CopyToAsync(sourceStream.Length, fileStream, Progress, cancellationToken); + + ShowProgressBar = false; + Status = $"Finished writing to {Path.GetFileName(Target)}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs new file mode 100644 index 000000000..e4e6a9875 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteStringToFileAction.cs @@ -0,0 +1,62 @@ +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; + +namespace Artemis.Core +{ + /// + /// Represents a plugin prerequisite action that copies a folder + /// + public class WriteStringToFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The target file to write to (will be created if needed) + /// The contents to write + public WriteStringToFileAction(string name, string target, string content) : base(name) + { + Target = target; + Content = content ?? throw new ArgumentNullException(nameof(content)); + + ProgressIndeterminate = true; + } + + /// + /// Gets or sets the target file + /// + public string Target { get; } + + /// + /// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to + /// the file will be deleted and recreated + /// + public bool Append { get; set; } = false; + + /// + /// Gets the string that will be written + /// + public string Content { get; } + + /// + public override async Task Execute(CancellationToken cancellationToken) + { + string outputDir = Path.GetDirectoryName(Target)!; + Utilities.CreateAccessibleDirectory(outputDir); + + ShowProgressBar = true; + Status = $"Writing to {Path.GetFileName(Target)}..."; + + if (Append) + await File.AppendAllTextAsync(Target, Content, cancellationToken); + else + await File.WriteAllTextAsync(Target, Content, cancellationToken); + + ShowProgressBar = false; + Status = $"Finished writing to {Path.GetFileName(Target)}"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs new file mode 100644 index 000000000..96b48bde3 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs @@ -0,0 +1,91 @@ +using System; + +namespace Artemis.Core +{ + /// + /// Represents progress on a plugin prerequisite action + /// + public class PrerequisiteActionProgress : CorePropertyChanged, IProgress<(long, long)> + { + private long _current; + private DateTime _lastReport; + private double _percentage; + private double _progressPerSecond; + private long _total; + private long _lastReportValue; + + /// + /// The current amount + /// + public long Current + { + get => _current; + set => SetAndNotify(ref _current, value); + } + + /// + /// The total amount + /// + public long Total + { + get => _total; + set => SetAndNotify(ref _total, value); + } + + /// + /// The percentage + /// + public double Percentage + { + get => _percentage; + set => SetAndNotify(ref _percentage, value); + } + + /// + /// Gets or sets the progress per second + /// + public double ProgressPerSecond + { + get => _progressPerSecond; + set => SetAndNotify(ref _progressPerSecond, value); + } + + #region Implementation of IProgress + + /// + public void Report((long, long) value) + { + (long newCurrent, long newTotal) = value; + + TimeSpan timePassed = DateTime.Now - _lastReport; + if (timePassed >= TimeSpan.FromSeconds(1)) + { + ProgressPerSecond = Math.Max(0, Math.Round(1.0 / timePassed.TotalSeconds * (newCurrent - _lastReportValue), 2)); + _lastReportValue = newCurrent; + _lastReport = DateTime.Now; + } + + Current = newCurrent; + Total = newTotal; + Percentage = Math.Round((double) Current / Total * 100.0, 2); + + OnProgressReported(); + } + + #endregion + + #region Events + + /// + /// Occurs when progress has been reported + /// + public event EventHandler? ProgressReported; + + protected virtual void OnProgressReported() + { + ProgressReported?.Invoke(this, EventArgs.Empty); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 4f833282d..7b3e6dea1 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -241,7 +241,16 @@ namespace Artemis.Core.Services } foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled)) - EnablePlugin(plugin, false, ignorePluginLock); + { + try + { + EnablePlugin(plugin, false, ignorePluginLock); + } + catch (ArtemisPluginPrerequisiteException) + { + _logger.Warning("Skipped enabling plugin {plugin} because not all prerequisites are met", plugin); + } + } _logger.Debug("Enabled {count} plugin(s)", _plugins.Count(p => p.IsEnabled)); // ReSharper restore InconsistentlySynchronizedField @@ -331,16 +340,24 @@ namespace Artemis.Core.Services } foreach (Type featureType in featureTypes) - plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute)))); + { + // Load the enabled state and if not found, default to true + PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ?? + new PluginFeatureEntity { IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureType.FullName! }; + plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, featureEntity, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute)))); + } if (!featureTypes.Any()) _logger.Warning("Plugin {plugin} contains no features", plugin); - List bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(IPluginBootstrapper).IsAssignableFrom(t)).ToList(); + List bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(PluginBootstrapper).IsAssignableFrom(t)).ToList(); if (bootstrappers.Count > 1) _logger.Warning($"{plugin} has more than one bootstrapper, only initializing {bootstrappers.First().FullName}"); if (bootstrappers.Any()) - plugin.Bootstrapper = (IPluginBootstrapper?) Activator.CreateInstance(bootstrappers.First()); + { + plugin.Bootstrapper = (PluginBootstrapper?) Activator.CreateInstance(bootstrappers.First()); + plugin.Bootstrapper?.InternalOnPluginLoaded(plugin); + } lock (_plugins) { @@ -369,6 +386,9 @@ namespace Artemis.Core.Services return; } + if (!plugin.Info.ArePrerequisitesMet()) + throw new ArtemisPluginPrerequisiteException(plugin.Info, "Cannot enable a plugin whose prerequisites aren't all met"); + // Create the Ninject child kernel and load the module plugin.Kernel = new ChildKernel(_kernel, new PluginModule(plugin)); OnPluginEnabling(new PluginEventArgs(plugin)); @@ -391,10 +411,7 @@ namespace Artemis.Core.Services featureInfo.Instance = instance; instance.Info = featureInfo; instance.Plugin = plugin; - - // Load the enabled state and if not found, default to true - instance.Entity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureInfo.FeatureType.FullName) ?? - new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureInfo.FeatureType.FullName!}; + instance.Entity = featureInfo.Entity; } catch (Exception e) { @@ -403,17 +420,8 @@ namespace Artemis.Core.Services } // Activate features after they are all loaded - foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && (f.Instance.Entity.IsEnabled || f.AlwaysEnabled))) - { - try - { - EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock); - } - catch (Exception) - { - // ignored, logged in EnablePluginFeature - } - } + foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && (f.EnabledInStorage || f.AlwaysEnabled))) + EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock); if (saveState) { @@ -570,7 +578,10 @@ namespace Artemis.Core.Services if (pluginFeature.Plugin.Info.RequiresAdmin && !_isElevated) { if (!saveState) + { + OnPluginFeatureEnableFailed(new PluginFeatureEventArgs(pluginFeature)); throw new ArtemisCoreException("Cannot enable a feature that requires elevation without saving it's state."); + } pluginFeature.Entity.IsEnabled = true; pluginFeature.Plugin.Entity.IsEnabled = true; @@ -581,6 +592,12 @@ namespace Artemis.Core.Services return; } + if (!pluginFeature.Info.ArePrerequisitesMet()) + { + OnPluginFeatureEnableFailed(new PluginFeatureEventArgs(pluginFeature)); + throw new ArtemisPluginPrerequisiteException(pluginFeature.Info, "Cannot enable a plugin feature whose prerequisites aren't all met"); + } + try { pluginFeature.SetEnabled(true, isAutoEnable); @@ -593,7 +610,6 @@ namespace Artemis.Core.Services new ArtemisPluginException(pluginFeature.Plugin, $"Exception during SetEnabled(true) on {pluginFeature}", e), "Failed to enable plugin" ); - throw; } finally { diff --git a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml index 740f659f0..c28b32576 100644 --- a/src/Artemis.UI.Shared/Controls/ColorPicker.xaml +++ b/src/Artemis.UI.Shared/Controls/ColorPicker.xaml @@ -73,7 +73,9 @@ MinWidth="95" MaxLength="9" Margin="0" - HorizontalAlignment="Stretch"> + HorizontalAlignment="Stretch" + FontFamily="Consolas" + CharacterCasing="Upper"> + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Resources/Images/Logo/bow.ico b/src/Artemis.UI/Resources/Images/Logo/bow.ico new file mode 100644 index 000000000..027af5dd6 Binary files /dev/null and b/src/Artemis.UI/Resources/Images/Logo/bow.ico differ diff --git a/src/Artemis.UI/Resources/Images/Logo/bow.svg b/src/Artemis.UI/Resources/Images/Logo/bow.svg index 66817473d..0c7dd46db 100644 --- a/src/Artemis.UI/Resources/Images/Logo/bow.svg +++ b/src/Artemis.UI/Resources/Images/Logo/bow.svg @@ -1,44 +1,43 @@ - - - - -Created by potrace 1.15, written by Peter Selinger 2001-2017 - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Resources/Images/Logo/logo-512.ico b/src/Artemis.UI/Resources/Images/Logo/logo-512.ico deleted file mode 100644 index 015d1ffa8..000000000 Binary files a/src/Artemis.UI/Resources/Images/Logo/logo-512.ico and /dev/null differ diff --git a/src/Artemis.UI/Resources/Images/Logo/logo-512.png b/src/Artemis.UI/Resources/Images/Logo/logo-512.png deleted file mode 100644 index d1cd8ebe7..000000000 Binary files a/src/Artemis.UI/Resources/Images/Logo/logo-512.png and /dev/null differ diff --git a/src/Artemis.UI/Screens/Home/HomeView.xaml b/src/Artemis.UI/Screens/Home/HomeView.xaml index 78db3be5c..84bde89c3 100644 --- a/src/Artemis.UI/Screens/Home/HomeView.xaml +++ b/src/Artemis.UI/Screens/Home/HomeView.xaml @@ -7,6 +7,7 @@ xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:home="clr-namespace:Artemis.UI.Screens.Home" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" + xmlns:svgc="http://sharpvectors.codeplex.com/svgc/" mc:Ignorable="d" d:DesignHeight="574.026" d:DesignWidth="1029.87" @@ -37,10 +38,26 @@ - + - Welcome to Artemis, RGB on steroids. + + + + + + + + + + + + + + +