1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Merge branch 'prerequisites' into development

This commit is contained in:
Robert 2021-05-02 23:01:01 +02:00
commit 72c95a49d2
41 changed files with 2171 additions and 134 deletions

View File

@ -53,6 +53,8 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Clayerbrushes_005Cinternal/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Clayereffects_005Cinternal/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cmodules_005Cactivationrequirements/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cprerequisites/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cprerequisites_005Cprerequisiteaction/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Csettings/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=rgb_002Enet/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Ccolorquantizer/@EntryIndexedValue">True</s:Boolean>

View File

@ -0,0 +1,30 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// An exception thrown when a plugin prerequisite-related error occurs
/// </summary>
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;
}
/// <summary>
/// Gets the subject the error is related to
/// </summary>
public IPrerequisitesSubject Subject { get; }
}
}

View File

@ -0,0 +1,133 @@
// 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;
/// <summary>
/// Copys a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="sourceLength">The length of the source stream, if known - used for progress reporting</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="bufferSize">The size of the copy block buffer</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>A task representing the operation</returns>
public static async Task CopyToAsync(
this Stream source,
long sourceLength,
Stream destination,
int bufferSize,
IProgress<(long, long)> progress,
CancellationToken cancellationToken)
{
if (0 == bufferSize)
bufferSize = DefaultBufferSize;
byte[]? buffer = new byte[bufferSize];
if (0 > sourceLength && source.CanSeek)
sourceLength = source.Length - source.Position;
long totalBytesCopied = 0L;
if (null != progress)
progress.Report((totalBytesCopied, sourceLength));
int bytesRead = -1;
while (0 != bytesRead && !cancellationToken.IsCancellationRequested)
{
bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken);
if (0 == bytesRead || cancellationToken.IsCancellationRequested)
break;
await destination.WriteAsync(buffer, 0, buffer.Length, cancellationToken);
totalBytesCopied += bytesRead;
progress?.Report((totalBytesCopied, sourceLength));
}
if (0 < totalBytesCopied)
progress?.Report((totalBytesCopied, sourceLength));
cancellationToken.ThrowIfCancellationRequested();
}
/// <summary>
/// Copys a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="sourceLength">The length of the source stream, if known - used for progress reporting</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>A task representing the operation</returns>
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);
}
/// <summary>
/// Copys a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <param name="cancellationToken">A cancellation token</param>
/// <returns>A task representing the operation</returns>
public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken)
{
return CopyToAsync(source, 0L, destination, 0, progress, cancellationToken);
}
/// <summary>
/// Copys a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="sourceLength">The length of the source stream, if known - used for progress reporting</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <returns>A task representing the operation</returns>
public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress)
{
return CopyToAsync(source, sourceLength, destination, 0, progress, default);
}
/// <summary>
/// Copys a stream to another stream
/// </summary>
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
/// <returns>A task representing the operation</returns>
public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress)
{
return CopyToAsync(source, 0L, destination, 0, progress, default);
}
}
}

View File

@ -1,20 +0,0 @@
namespace Artemis.Core
{
/// <summary>
/// An optional entry point for your plugin
/// </summary>
public interface IPluginBootstrapper
{
/// <summary>
/// Called when the plugin is activated
/// </summary>
/// <param name="plugin">The plugin instance of your plugin</param>
void Enable(Plugin plugin);
/// <summary>
/// Called when the plugin is deactivated or when Artemis shuts down
/// </summary>
/// <param name="plugin">The plugin instance of your plugin</param>
void Disable(Plugin plugin);
}
}

View File

@ -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<PluginFeatureInfo>();
}
@ -71,7 +72,7 @@ namespace Artemis.Core
/// <summary>
/// Gets the plugin bootstrapper
/// </summary>
public IPluginBootstrapper? Bootstrapper { get; internal set; }
public PluginBootstrapper? Bootstrapper { get; internal set; }
/// <summary>
/// The Ninject kernel of the plugin
@ -113,6 +114,17 @@ namespace Artemis.Core
{
return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T;
}
/// <summary>
/// Looks up the feature info the feature of type <typeparamref name="T" />
/// </summary>
/// <typeparam name="T">The type of feature to find</typeparam>
/// <returns>Feature info of the feature</returns>
public PluginFeatureInfo GetFeatureInfo<T>() 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));
}
/// <inheritdoc />
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();
}
}

View File

@ -0,0 +1,100 @@
namespace Artemis.Core
{
/// <summary>
/// An optional entry point for your plugin
/// </summary>
public abstract class PluginBootstrapper
{
private Plugin? _plugin;
/// <summary>
/// Called when the plugin is loaded
/// </summary>
/// <param name="plugin"></param>
public virtual void OnPluginLoaded(Plugin plugin)
{
}
/// <summary>
/// Called when the plugin is activated
/// </summary>
/// <param name="plugin">The plugin instance of your plugin</param>
public virtual void OnPluginEnabled(Plugin plugin)
{
}
/// <summary>
/// Called when the plugin is deactivated or when Artemis shuts down
/// </summary>
/// <param name="plugin">The plugin instance of your plugin</param>
public virtual void OnPluginDisabled(Plugin plugin)
{
}
/// <summary>
/// Adds the provided prerequisite to the plugin.
/// </summary>
/// <param name="prerequisite">The prerequisite to add</param>
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);
}
/// <summary>
/// Removes the provided prerequisite from the plugin.
/// </summary>
/// <param name="prerequisite">The prerequisite to remove</param>
/// <returns>
/// <see langword="true" /> is successfully removed; otherwise <see langword="false" />. This method also returns
/// <see langword="false" /> if the prerequisite was not found.
/// </returns>
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);
}
/// <summary>
/// Adds the provided prerequisite to the feature of type <typeparamref name="T"/>.
/// </summary>
/// <param name="prerequisite">The prerequisite to add</param>
public void AddFeaturePrerequisite<T>(PluginPrerequisite prerequisite) where T : PluginFeature
{
if (_plugin == null)
throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded");
PluginFeatureInfo info = _plugin.GetFeatureInfo<T>();
if (!info.Prerequisites.Contains(prerequisite))
info.Prerequisites.Add(prerequisite);
}
/// <summary>
/// Removes the provided prerequisite from the feature of type <typeparamref name="T"/>.
/// </summary>
/// <param name="prerequisite">The prerequisite to remove</param>
/// <returns>
/// <see langword="true" /> is successfully removed; otherwise <see langword="false" />. This method also returns
/// <see langword="false" /> if the prerequisite was not found.
/// </returns>
public bool RemoveFeaturePrerequisite<T>(PluginPrerequisite prerequisite) where T : PluginFeature
{
if (_plugin == null)
throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded");
return _plugin.GetFeatureInfo<T>().Prerequisites.Remove(prerequisite);
}
internal void InternalOnPluginLoaded(Plugin plugin)
{
_plugin = plugin;
OnPluginLoaded(plugin);
}
}
}

View File

@ -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;
/// <summary>
/// Gets the plugin feature info related to this feature
/// </summary>

View File

@ -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
/// </summary>
[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; }
/// <summary>
/// Gets a boolean indicating whether the feature is enabled in persistent storage
/// </summary>
public bool EnabledInStorage => Entity.IsEnabled;
/// <summary>
/// Gets the feature this info is associated with
/// </summary>
@ -128,6 +137,14 @@ namespace Artemis.Core
internal set => SetAndNotify(ref _instance, value);
}
/// <inheritdoc />
public List<PluginPrerequisite> Prerequisites { get; } = new();
/// <inheritdoc />
public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet());
internal PluginFeatureEntity Entity { get; }
/// <inheritdoc />
public override string ToString()
{

View File

@ -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
/// </summary>
[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);
}
/// <inheritdoc />
public List<PluginPrerequisite> Prerequisites { get; } = new();
/// <inheritdoc />
public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet());
/// <inheritdoc />
public override string ToString()
{

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.Linq;
namespace Artemis.Core
{
/// <summary>
/// Represents a type that has prerequisites
/// </summary>
public interface IPrerequisitesSubject
{
/// <summary>
/// Gets a list of prerequisites for this plugin
/// </summary>
List<PluginPrerequisite> Prerequisites { get; }
/// <summary>
/// Determines whether the prerequisites of this plugin are met
/// </summary>
bool ArePrerequisitesMet();
}
}

View File

@ -0,0 +1,127 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
{
/// <summary>
/// Represents a prerequisite for a <see cref="Plugin" /> or <see cref="PluginFeature" />
/// </summary>
public abstract class PluginPrerequisite : CorePropertyChanged
{
private PluginPrerequisiteAction? _currentAction;
/// <summary>
/// Gets the name of the prerequisite
/// </summary>
public abstract string Name { get; }
/// <summary>
/// Gets the description of the prerequisite
/// </summary>
public abstract string Description { get; }
/// <summary>
/// Gets a boolean indicating whether installing or uninstalling this prerequisite requires admin privileges
/// </summary>
public abstract bool RequiresElevation { get; }
/// <summary>
/// Gets a list of actions to execute when <see cref="Install" /> is called
/// </summary>
public abstract List<PluginPrerequisiteAction> InstallActions { get; }
/// <summary>
/// Gets a list of actions to execute when <see cref="Uninstall" /> is called
/// </summary>
public abstract List<PluginPrerequisiteAction> UninstallActions { get; }
/// <summary>
/// Gets or sets the action currently being executed
/// </summary>
public PluginPrerequisiteAction? CurrentAction
{
get => _currentAction;
private set => SetAndNotify(ref _currentAction, value);
}
/// <summary>
/// Execute all install actions
/// </summary>
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();
}
}
/// <summary>
/// Execute all uninstall actions
/// </summary>
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();
}
}
/// <summary>
/// Called to determine whether the prerequisite is met
/// </summary>
/// <returns><see langword="true" /> if the prerequisite is met; otherwise <see langword="false" /></returns>
public abstract bool IsMet();
/// <summary>
/// Called before installation starts
/// </summary>
protected virtual void OnInstallStarting()
{
}
/// <summary>
/// Called after installation finishes
/// </summary>
protected virtual void OnInstallFinished()
{
}
/// <summary>
/// Called before uninstall starts
/// </summary>
protected virtual void OnUninstallStarting()
{
}
/// <summary>
/// Called after uninstall finished
/// </summary>
protected virtual void OnUninstallFinished()
{
}
}
}

View File

@ -0,0 +1,96 @@
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
{
/// <summary>
/// Represents an action that must be taken to install or uninstall a plugin prerequisite
/// </summary>
public abstract class PluginPrerequisiteAction : CorePropertyChanged
{
private bool _progressIndeterminate;
private bool _showProgressBar;
private bool _showSubProgressBar;
private string? _status;
private bool _subProgressIndeterminate;
/// <summary>
/// The base constructor for all plugin prerequisite actions
/// </summary>
/// <param name="name">The name of the action</param>
protected PluginPrerequisiteAction(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
#region Implementation of IPluginPrerequisiteAction
/// <summary>
/// Gets the name of the action
/// </summary>
public string Name { get; }
/// <summary>
/// Gets or sets the status of the action
/// </summary>
public string? Status
{
get => _status;
set => SetAndNotify(ref _status, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the progress is indeterminate or not
/// </summary>
public bool ProgressIndeterminate
{
get => _progressIndeterminate;
set => SetAndNotify(ref _progressIndeterminate, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the progress is indeterminate or not
/// </summary>
public bool SubProgressIndeterminate
{
get => _subProgressIndeterminate;
set => SetAndNotify(ref _subProgressIndeterminate, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the progress bar should be shown
/// </summary>
public bool ShowProgressBar
{
get => _showProgressBar;
set => SetAndNotify(ref _showProgressBar, value);
}
/// <summary>
/// Gets or sets a boolean indicating whether the sub progress bar should be shown
/// </summary>
public bool ShowSubProgressBar
{
get => _showSubProgressBar;
set => SetAndNotify(ref _showSubProgressBar, value);
}
/// <summary>
/// Gets or sets the progress of the action (0 to 100)
/// </summary>
public PrerequisiteActionProgress Progress { get; } = new();
/// <summary>
/// Gets or sets the sub progress of the action
/// </summary>
public PrerequisiteActionProgress SubProgress { get; } = new();
/// <summary>
/// Called when the action must execute
/// </summary>
public abstract Task Execute(CancellationToken cancellationToken);
#endregion
}
}

View File

@ -0,0 +1,81 @@
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using Humanizer;
namespace Artemis.Core
{
/// <summary>
/// Represents a plugin prerequisite action that copies a folder
/// </summary>
public class CopyFolderAction : PluginPrerequisiteAction
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="source">The source folder to copy</param>
/// <param name="target">The target folder to copy to (will be created if needed)</param>
public CopyFolderAction(string name, string source, string target) : base(name)
{
Source = source;
Target = target;
ShowProgressBar = true;
ShowSubProgressBar = true;
}
/// <summary>
/// Gets the source directory
/// </summary>
public string Source { get; }
/// <summary>
/// Gets or sets the target directory
/// </summary>
public string Target { get; }
/// <inheritdoc />
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)";
}
}
}

View File

@ -0,0 +1,44 @@
using System.IO;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
{
/// <summary>
/// Represents a plugin prerequisite action that deletes a file
/// </summary>
public class DeleteFileAction : PluginPrerequisiteAction
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="target">The target folder to delete recursively</param>
public DeleteFileAction(string name, string target) : base(name)
{
Target = target;
ProgressIndeterminate = true;
}
/// <summary>
/// Gets or sets the target directory
/// </summary>
public string Target { get; }
/// <inheritdoc />
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}";
}
}
}

View File

@ -0,0 +1,49 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
{
/// <summary>
/// Represents a plugin prerequisite action that recursively deletes a folder
/// </summary>
public class DeleteFolderAction : PluginPrerequisiteAction
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="target">The target folder to delete recursively</param>
public DeleteFolderAction(string name, string target) : base(name)
{
if (Enum.GetValues<Environment.SpecialFolder>().Select(Environment.GetFolderPath).Contains(target))
throw new ArtemisCoreException($"Cannot delete special folder {target}, silly goose.");
Target = target;
ProgressIndeterminate = true;
}
/// <summary>
/// Gets or sets the target directory
/// </summary>
public string Target { get; }
/// <inheritdoc />
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}";
}
}
}

View File

@ -0,0 +1,79 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Humanizer;
namespace Artemis.Core
{
/// <summary>
/// Represents a plugin prerequisite action that downloads a file
/// </summary>
public class DownloadFileAction : PluginPrerequisiteAction
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="source">The source URL to download</param>
/// <param name="target">The target file to save as (will be created if needed)</param>
public DownloadFileAction(string name, string source, string target) : base(name)
{
Source = source ?? throw new ArgumentNullException(nameof(source));
Target = target ?? throw new ArgumentNullException(nameof(target));
ShowProgressBar = true;
}
/// <summary>
/// Gets the source URL to download
/// </summary>
public string Source { get; }
/// <summary>
/// Gets the target file to save as (will be created if needed)
/// </summary>
public string Target { get; }
/// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken)
{
using HttpClient client = new();
await using FileStream destinationStream = File.Create(Target);
void ProgressOnProgressReported(object? sender, EventArgs e)
{
if (Progress.ProgressPerSecond != 0)
Status = $"Downloading {Target} - {Progress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec";
else
Status = $"Downloading {Target}";
}
Progress.ProgressReported += ProgressOnProgressReported;
// Get the http headers first to examine the content length
using HttpResponseMessage response = await client.GetAsync(Target, 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);
}
else
{
ProgressIndeterminate = false;
await download.CopyToAsync(contentLength.Value, destinationStream, Progress, cancellationToken);
}
Progress.ProgressReported -= ProgressOnProgressReported;
Progress.Report((1, 1));
Status = "Finished downloading";
}
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
{
/// <summary>
/// Represents a plugin prerequisite action that copies a folder
/// </summary>
public class WriteBytesToFileAction : PluginPrerequisiteAction
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="target">The target file to write to (will be created if needed)</param>
/// <param name="content">The contents to write</param>
public WriteBytesToFileAction(string name, string target, byte[] content) : base(name)
{
Target = target;
ByteContent = content ?? throw new ArgumentNullException(nameof(content));
}
/// <summary>
/// Gets or sets the target file
/// </summary>
public string Target { get; }
/// <summary>
/// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to
/// <see langword="false" /> the file will be deleted and recreated
/// </summary>
public bool Append { get; set; } = false;
/// <summary>
/// Gets the bytes that will be written
/// </summary>
public byte[] ByteContent { get; }
/// <inheritdoc />
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)}";
}
}
}

View File

@ -0,0 +1,62 @@
using System;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.Core
{
/// <summary>
/// Represents a plugin prerequisite action that copies a folder
/// </summary>
public class WriteStringToFileAction : PluginPrerequisiteAction
{
/// <summary>
/// Creates a new instance of a copy folder action
/// </summary>
/// <param name="name">The name of the action</param>
/// <param name="target">The target file to write to (will be created if needed)</param>
/// <param name="content">The contents to write</param>
public WriteStringToFileAction(string name, string target, string content) : base(name)
{
Target = target;
Content = content ?? throw new ArgumentNullException(nameof(content));
ProgressIndeterminate = true;
}
/// <summary>
/// Gets or sets the target file
/// </summary>
public string Target { get; }
/// <summary>
/// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to
/// <see langword="false" /> the file will be deleted and recreated
/// </summary>
public bool Append { get; set; } = false;
/// <summary>
/// Gets the string that will be written
/// </summary>
public string Content { get; }
/// <inheritdoc />
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)}";
}
}
}

View File

@ -0,0 +1,91 @@
using System;
namespace Artemis.Core
{
/// <summary>
/// Represents progress on a plugin prerequisite action
/// </summary>
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;
/// <summary>
/// The current amount
/// </summary>
public long Current
{
get => _current;
set => SetAndNotify(ref _current, value);
}
/// <summary>
/// The total amount
/// </summary>
public long Total
{
get => _total;
set => SetAndNotify(ref _total, value);
}
/// <summary>
/// The percentage
/// </summary>
public double Percentage
{
get => _percentage;
set => SetAndNotify(ref _percentage, value);
}
/// <summary>
/// Gets or sets the progress per second
/// </summary>
public double ProgressPerSecond
{
get => _progressPerSecond;
set => SetAndNotify(ref _progressPerSecond, value);
}
#region Implementation of IProgress<in (long, long)>
/// <inheritdoc />
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
/// <summary>
/// Occurs when progress has been reported
/// </summary>
public event EventHandler? ProgressReported;
protected virtual void OnProgressReported()
{
ProgressReported?.Invoke(this, EventArgs.Empty);
}
#endregion
}
}

View File

@ -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<Type> bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(IPluginBootstrapper).IsAssignableFrom(t)).ToList();
List<Type> 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
{

View File

@ -1,14 +1,18 @@
<UserControl x:ClassModifier="internal"
x:Class="Artemis.UI.Shared.Screens.Dialogs.ConfirmDialogView"
<UserControl
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:dialogs="clr-namespace:Artemis.UI.Shared.Screens.Dialogs"
xmlns:Shared="clr-namespace:Artemis.UI.Shared" x:ClassModifier="internal"
x:Class="Artemis.UI.Shared.Screens.Dialogs.ConfirmDialogView"
mc:Ignorable="d"
d:DesignHeight="163.274" d:DesignWidth="254.425"
d:DataContext="{d:DesignInstance dialogs:ConfirmDialogViewModel}">
d:DataContext="{d:DesignInstance {x:Type dialogs:ConfirmDialogViewModel}}">
<UserControl.Resources>
<Shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
</UserControl.Resources>
<StackPanel Margin="16">
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="{Binding Header}" TextWrapping="Wrap" />
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}"
@ -22,7 +26,8 @@
Focusable="False"
IsCancel="True"
Command="{s:Action Cancel}"
Content="{Binding CancelText}" />
Content="{Binding CancelText}"
Visibility="{Binding CancelText, Converter={StaticResource NullToVisibilityConverter}, Mode=OneWay}" />
<Button x:Name="ConfirmButton"
Style="{StaticResource MaterialDesignFlatButton}"
IsDefault="True"

View File

@ -53,7 +53,7 @@ namespace Artemis.UI.Shared.Services
public LinkedList<Color> RecentColors => RecentColorsSetting.Value;
public Task<object> ShowGradientPicker(ColorGradient colorGradient, string dialogHost)
public Task<object?> ShowGradientPicker(ColorGradient colorGradient, string dialogHost)
{
if (!string.IsNullOrWhiteSpace(dialogHost))
return _dialogService.ShowDialogAt<GradientEditorViewModel>(dialogHost, new Dictionary<string, object> {{"colorGradient", colorGradient}});

View File

@ -61,28 +61,30 @@ namespace Artemis.UI.Shared.Services
return await ShowDialog(identifier, GetBestKernel().Get<T>(parameters));
}
public async Task<bool> ShowConfirmDialog(string header, string text, string confirmText = "Confirm", string cancelText = "Cancel")
public async Task<bool> ShowConfirmDialog(string header, string text, string confirmText = "Confirm", string? cancelText = "Cancel")
{
if (confirmText == null) throw new ArgumentNullException(nameof(confirmText));
IParameter[] arguments =
{
new ConstructorArgument("header", header),
new ConstructorArgument("text", text),
new ConstructorArgument("confirmText", confirmText.ToUpper()),
new ConstructorArgument("cancelText", cancelText.ToUpper())
new ConstructorArgument("cancelText", cancelText?.ToUpper())
};
object? result = await ShowDialog<ConfirmDialogViewModel>(arguments);
return result is bool booleanResult && booleanResult;
}
public async Task<bool> ShowConfirmDialogAt(string identifier, string header, string text, string confirmText = "Confirm", string cancelText = "Cancel")
public async Task<bool> ShowConfirmDialogAt(string identifier, string header, string text, string confirmText = "Confirm", string? cancelText = "Cancel")
{
if (identifier == null) throw new ArgumentNullException(nameof(identifier));
if (confirmText == null) throw new ArgumentNullException(nameof(confirmText));
IParameter[] arguments =
{
new ConstructorArgument("header", header),
new ConstructorArgument("text", text),
new ConstructorArgument("confirmText", confirmText.ToUpper()),
new ConstructorArgument("cancelText", cancelText.ToUpper())
new ConstructorArgument("cancelText", cancelText?.ToUpper())
};
object? result = await ShowDialogAt<ConfirmDialogViewModel>(identifier, arguments);
return result is bool booleanResult && booleanResult;

View File

@ -6,7 +6,7 @@ namespace Artemis.UI.Shared.Services
/// <summary>
/// Represents the base class for a dialog view model
/// </summary>
public abstract class DialogViewModelBase : ValidatingModelBase
public abstract class DialogViewModelBase : Screen
{
private DialogViewModelHost? _dialogViewModelHost;
private DialogSession? _session;
@ -47,6 +47,7 @@ namespace Artemis.UI.Shared.Services
/// </summary>
public virtual void OnDialogClosed(object sender, DialogClosingEventArgs e)
{
ScreenExtensions.TryClose(this);
}
/// <summary>
@ -61,6 +62,7 @@ namespace Artemis.UI.Shared.Services
internal void OnDialogOpened(object sender, DialogOpenedEventArgs e)
{
Session = e.Session;
ScreenExtensions.TryActivate(this);
}
}
}

View File

@ -7,7 +7,7 @@ namespace Artemis.UI.Shared.Services
{
internal interface IColorPickerService : IArtemisSharedUIService
{
Task<object> ShowGradientPicker(ColorGradient colorGradient, string dialogHost);
Task<object?> ShowGradientPicker(ColorGradient colorGradient, string dialogHost);
PluginSetting<bool> PreviewSetting { get; }
LinkedList<Color> RecentColors { get; }

View File

@ -359,6 +359,9 @@
<Page Update="DefaultTypes\PropertyInput\FloatRangePropertyInputView.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Screens\Plugins\PluginPrerequisitesUninstallDialogView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>
<Page Update="Screens\ProfileEditor\Dialogs\ProfileEditView.xaml">
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
</Page>

View File

@ -2,6 +2,7 @@
using Artemis.Core.Modules;
using Artemis.UI.Screens.Modules;
using Artemis.UI.Screens.Modules.Tabs;
using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.ProfileEditor;
using Artemis.UI.Screens.ProfileEditor.Conditions;
using Artemis.UI.Screens.ProfileEditor.LayerProperties;
@ -95,7 +96,13 @@ namespace Artemis.UI.Ninject.Factories
TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<LayerPropertyGroupViewModel> layerPropertyGroups);
}
public interface IDataBindingsVmFactory
public interface IPrerequisitesVmFactory : IVmFactory
{
PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall);
}
// TODO: Move these two
public interface IDataBindingsVmFactory
{
IDataBindingViewModel DataBindingViewModel(IDataBindingRegistration registration);
DirectDataBindingModeViewModel<TLayerProperty, TProperty> DirectDataBindingModeViewModel<TLayerProperty, TProperty>(DirectDataBinding<TLayerProperty, TProperty> directDataBinding);
@ -104,7 +111,7 @@ namespace Artemis.UI.Ninject.Factories
DataBindingConditionViewModel<TLayerProperty, TProperty> DataBindingConditionViewModel<TLayerProperty, TProperty>(DataBindingCondition<TLayerProperty, TProperty> dataBindingCondition);
}
public interface IPropertyVmFactory
public interface IPropertyVmFactory
{
ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel);
ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel);

View File

@ -0,0 +1,26 @@
<UserControl x:Class="Artemis.UI.Screens.Plugins.PluginPrerequisiteActionView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Plugins"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PluginPrerequisiteActionViewModel}">
<StackPanel>
<ProgressBar Value="{Binding Action.Progress.Percentage, Mode=OneWay}"
IsIndeterminate="{Binding Action.ProgressIndeterminate, Mode=OneWay}"
Visibility="{Binding Action.ShowProgressBar, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
materialDesign:TransitionAssist.DisableTransitions="True"
Margin="0 10"/>
<ProgressBar Value="{Binding Action.SubProgress.Percentage, Mode=OneWay}"
IsIndeterminate="{Binding Action.SubProgressIndeterminate, Mode=OneWay}"
Visibility="{Binding Action.ShowSubProgressBar, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
materialDesign:TransitionAssist.DisableTransitions="True"
Margin="0 10"/>
<TextBlock TextWrapping="Wrap" Text="{Binding Action.Status, Mode=OneWay}" />
</StackPanel>
</UserControl>

View File

@ -0,0 +1,19 @@
using System;
using System.ComponentModel;
using Artemis.Core;
using Stylet;
namespace Artemis.UI.Screens.Plugins
{
public class PluginPrerequisiteActionViewModel : Screen
{
public PluginPrerequisiteActionViewModel(PluginPrerequisiteAction action)
{
Action = action;
}
public PluginPrerequisiteAction Action { get; }
}
}

View File

@ -0,0 +1,37 @@
<UserControl x:Class="Artemis.UI.Screens.Plugins.PluginPrerequisiteView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Plugins"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:s="https://github.com/canton7/Stylet"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PluginPrerequisiteViewModel}">
<StackPanel>
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" TextWrapping="Wrap" Text="{Binding PluginPrerequisite.Name, Mode=OneWay}" />
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}" TextWrapping="Wrap" Text="{Binding PluginPrerequisite.Description, Mode=OneWay}" Margin="0 0 0 15" />
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}"
Foreground="{DynamicResource MaterialDesignBodyLight}"
TextWrapping="Wrap"
Visibility="{Binding HasMultipleActions, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<Run Text="Step" />
<Run Text="{Binding ActiveStemNumber, Mode=OneWay}" /><Run Text="/" /><Run Text="{Binding Items.Count, Mode=OneWay}" />
<Run Text="-" />
<Run Text="{Binding ActiveItem.Action.Name, Mode=OneWay}" />
</TextBlock>
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}"
Foreground="{DynamicResource MaterialDesignBodyLight}"
TextWrapping="Wrap"
Text="{Binding ActiveItem.Action.Name, Mode=OneWay}"
Visibility="{Binding HasMultipleActions, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}" />
<ContentControl s:View.Model="{Binding ActiveItem}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
IsTabStop="False"
Margin="0 10 0 0" />
</StackPanel>
</UserControl>

View File

@ -0,0 +1,146 @@
using System;
using System.ComponentModel;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared.Services;
using Stylet;
namespace Artemis.UI.Screens.Plugins
{
public class PluginPrerequisiteViewModel : Conductor<PluginPrerequisiteActionViewModel>.Collection.OneActive
{
private readonly bool _uninstall;
private readonly ICoreService _coreService;
private readonly IDialogService _dialogService;
private bool _installing;
private bool _uninstalling;
private bool _isMet;
public PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall, ICoreService coreService, IDialogService dialogService)
{
_uninstall = uninstall;
_coreService = coreService;
_dialogService = dialogService;
PluginPrerequisite = pluginPrerequisite;
}
public PluginPrerequisite PluginPrerequisite { get; }
public bool Installing
{
get => _installing;
set
{
SetAndNotify(ref _installing, value);
NotifyOfPropertyChange(nameof(Busy));
}
}
public bool Uninstalling
{
get => _uninstalling;
set
{
SetAndNotify(ref _uninstalling, value);
NotifyOfPropertyChange(nameof(Busy));
}
}
public bool IsMet
{
get => _isMet;
set => SetAndNotify(ref _isMet, value);
}
public bool Busy => Installing || Uninstalling;
public int ActiveStemNumber => Items.IndexOf(ActiveItem) + 1;
public bool HasMultipleActions => Items.Count > 1;
public async Task Install(CancellationToken cancellationToken)
{
if (Busy)
return;
if (PluginPrerequisite.RequiresElevation && !_coreService.IsElevated)
{
await _dialogService.ShowConfirmDialog("Install plugin prerequisite", "This plugin prerequisite admin rights to install (restart & elevate NYI)");
return;
}
Installing = true;
try
{
await PluginPrerequisite.Install(cancellationToken);
}
finally
{
Installing = false;
IsMet = PluginPrerequisite.IsMet();
}
}
public async Task Uninstall(CancellationToken cancellationToken)
{
if (Busy)
return;
if (PluginPrerequisite.RequiresElevation && !_coreService.IsElevated)
{
await _dialogService.ShowConfirmDialog("Install plugin prerequisite", "This plugin prerequisite admin rights to install (restart & elevate NYI)");
return;
}
Uninstalling = true;
try
{
await PluginPrerequisite.Uninstall(cancellationToken);
}
finally
{
Uninstalling = false;
IsMet = PluginPrerequisite.IsMet();
}
}
private void PluginPrerequisiteOnPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(PluginPrerequisite.CurrentAction))
ActivateCurrentAction();
}
private void ActivateCurrentAction()
{
ActiveItem = Items.FirstOrDefault(i => i.Action == PluginPrerequisite.CurrentAction);
NotifyOfPropertyChange(nameof(ActiveStemNumber));
}
#region Overrides of Screen
/// <inheritdoc />
protected override void OnClose()
{
PluginPrerequisite.PropertyChanged += PluginPrerequisiteOnPropertyChanged;
base.OnClose();
}
/// <inheritdoc />
protected override void OnInitialActivate()
{
PluginPrerequisite.PropertyChanged -= PluginPrerequisiteOnPropertyChanged;
// Could be slow so take it off of the UI thread
Task.Run(() => IsMet = PluginPrerequisite.IsMet());
Items.AddRange(!_uninstall
? PluginPrerequisite.InstallActions.Select(a => new PluginPrerequisiteActionViewModel(a))
: PluginPrerequisite.UninstallActions.Select(a => new PluginPrerequisiteActionViewModel(a)));
base.OnInitialActivate();
}
#endregion
}
}

View File

@ -0,0 +1,126 @@
<UserControl x:Class="Artemis.UI.Screens.Plugins.PluginPrerequisitesInstallDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Plugins"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PluginPrerequisitesInstallDialogViewModel}">
<UserControl.Resources>
<shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
</UserControl.Resources>
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition MinHeight="150"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="500" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Style="{StaticResource MaterialDesignHeadline6TextBlock}" TextWrapping="Wrap" Margin="0 0 0 20">
Plugin/feature prerequisites
</TextBlock>
<ListBox Grid.Row="1"
Grid.Column="0"
ItemsSource="{Binding Prerequisites}"
SelectedItem="{Binding ActivePrerequisite, Mode=OneWay}"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:PluginPrerequisiteViewModel}">
<Border Padding="8" BorderThickness="0 0 0 1" BorderBrush="{DynamicResource MaterialDesignDivider}" VerticalAlignment="Stretch">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ToggleButton VerticalAlignment="Center"
Style="{StaticResource MaterialDesignActionToggleButton}"
Focusable="False"
IsHitTestVisible="False"
IsChecked="{Binding IsMet}">
<ToggleButton.Content>
<Border Background="#E74C4C" Width="32" Height="32">
<materialDesign:PackIcon Kind="Close" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</ToggleButton.Content>
<materialDesign:ToggleButtonAssist.OnContent>
<materialDesign:PackIcon Kind="Check" />
</materialDesign:ToggleButtonAssist.OnContent>
</ToggleButton>
<StackPanel Margin="8 0 0 0" Grid.Column="1" VerticalAlignment="Stretch">
<TextBlock FontWeight="Bold" Text="{Binding PluginPrerequisite.Name}" TextWrapping="Wrap" />
<TextBlock Text="{Binding PluginPrerequisite.Description}" TextWrapping="Wrap" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ContentControl Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
s:View.Model="{Binding ActivePrerequisite}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Margin="10 0"
IsTabStop="False"
Visibility="{Binding ActivePrerequisite, Converter={StaticResource NullToVisibilityConverter}}"/>
<TextBlock Grid.Row="1"
Grid.Column="1"
Style="{StaticResource MaterialDesignBody1TextBlock}"
TextWrapping="Wrap"
Margin="10 0"
Visibility="{Binding ActivePrerequisite, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted}">
In order for this plugin/feature to function certain prerequisites must be met. <LineBreak /><LineBreak />
On the left side you can see all prerequisites and whether they are currently met or not.
Clicking install prerequisites will automatically set everything up for you.
</TextBlock>
<StackPanel Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0"
Visibility="{Binding IsFinished, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
<Button Style="{StaticResource MaterialDesignFlatButton}"
Focusable="False"
IsCancel="True"
Command="{s:Action Cancel}"
Content="CANCEL" />
<Button x:Name="ConfirmButton"
Style="{StaticResource MaterialDesignFlatButton}"
IsDefault="True"
Focusable="True"
Command="{s:Action Install}"
Content="INSTALL PREREQUISITES" />
</StackPanel>
<StackPanel Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0"
Visibility="{Binding IsFinished, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<Button Style="{StaticResource MaterialDesignFlatButton}"
Focusable="False"
IsCancel="True"
Command="{s:Action Accept}"
Content="FINISH" />
</StackPanel>
</Grid>
</UserControl>

View File

@ -0,0 +1,140 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Services;
using MaterialDesignThemes.Wpf;
using Stylet;
namespace Artemis.UI.Screens.Plugins
{
public class PluginPrerequisitesInstallDialogViewModel : DialogViewModelBase
{
private readonly IDialogService _dialogService;
private readonly List<IPrerequisitesSubject> _subjects;
private PluginPrerequisiteViewModel _activePrerequisite;
private bool _canInstall;
private bool _isFinished;
private CancellationTokenSource _tokenSource;
public PluginPrerequisitesInstallDialogViewModel(List<IPrerequisitesSubject> subjects, IPrerequisitesVmFactory prerequisitesVmFactory, IDialogService dialogService)
{
_subjects = subjects;
_dialogService = dialogService;
Prerequisites = new BindableCollection<PluginPrerequisiteViewModel>();
foreach (IPrerequisitesSubject prerequisitesSubject in subjects)
Prerequisites.AddRange(prerequisitesSubject.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false)));
foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
pluginPrerequisiteViewModel.ConductWith(this);
}
public BindableCollection<PluginPrerequisiteViewModel> Prerequisites { get; }
public PluginPrerequisiteViewModel ActivePrerequisite
{
get => _activePrerequisite;
set => SetAndNotify(ref _activePrerequisite, value);
}
public bool CanInstall
{
get => _canInstall;
set => SetAndNotify(ref _canInstall, value);
}
public bool IsFinished
{
get => _isFinished;
set => SetAndNotify(ref _isFinished, value);
}
#region Overrides of DialogViewModelBase
/// <inheritdoc />
public override void OnDialogClosed(object sender, DialogClosingEventArgs e)
{
_tokenSource?.Cancel();
base.OnDialogClosed(sender, e);
}
#endregion
public async void Install()
{
CanInstall = false;
_tokenSource = new CancellationTokenSource();
try
{
foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
{
pluginPrerequisiteViewModel.IsMet = pluginPrerequisiteViewModel.PluginPrerequisite.IsMet();
if (pluginPrerequisiteViewModel.IsMet)
continue;
ActivePrerequisite = pluginPrerequisiteViewModel;
await ActivePrerequisite.Install(_tokenSource.Token);
// Wait after the task finished for the user to process what happened
if (pluginPrerequisiteViewModel != Prerequisites.Last())
await Task.Delay(1000);
}
if (Prerequisites.All(p => p.IsMet))
{
IsFinished = true;
return;
}
// This shouldn't be happening and the experience isn't very nice for the user (too lazy to make a nice UI for such an edge case)
// but at least give some feedback
Session?.Close(false);
await _dialogService.ShowConfirmDialog(
"Plugin prerequisites",
"All prerequisites are installed but some still aren't met. \r\nPlease try again or contact the plugin creator.",
"Confirm",
""
);
await Show(_dialogService, _subjects);
}
catch (OperationCanceledException)
{
// ignored
}
finally
{
CanInstall = true;
_tokenSource.Dispose();
_tokenSource = null;
}
}
public void Accept()
{
Session?.Close(true);
}
public static Task<object> Show(IDialogService dialogService, List<IPrerequisitesSubject> subjects)
{
return dialogService.ShowDialog<PluginPrerequisitesInstallDialogViewModel>(new Dictionary<string, object> {{"subjects", subjects}});
}
#region Overrides of Screen
/// <inheritdoc />
protected override void OnInitialActivate()
{
CanInstall = false;
Task.Run(() => CanInstall = Prerequisites.Any(p => !p.PluginPrerequisite.IsMet()));
base.OnInitialActivate();
}
#endregion
}
}

View File

@ -0,0 +1,125 @@
<UserControl x:Class="Artemis.UI.Screens.Plugins.PluginPrerequisitesUninstallDialogView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:Artemis.UI.Screens.Plugins"
xmlns:s="https://github.com/canton7/Stylet"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PluginPrerequisitesUninstallDialogViewModel}">
<UserControl.Resources>
<shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
</UserControl.Resources>
<Grid Margin="16">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition MinHeight="150"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="300" />
<ColumnDefinition Width="500" />
</Grid.ColumnDefinitions>
<TextBlock Grid.Row="0" Style="{StaticResource MaterialDesignHeadline6TextBlock}" TextWrapping="Wrap" Margin="0 0 0 20">
Plugin/feature prerequisites
</TextBlock>
<ListBox Grid.Row="1"
Grid.Column="0"
ItemsSource="{Binding Prerequisites}"
SelectedItem="{Binding ActivePrerequisite, Mode=OneWay}"
HorizontalContentAlignment="Stretch">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:PluginPrerequisiteViewModel}">
<Border Padding="8" BorderThickness="0 0 0 1" BorderBrush="{DynamicResource MaterialDesignDivider}" VerticalAlignment="Stretch">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ToggleButton VerticalAlignment="Center"
Style="{StaticResource MaterialDesignActionToggleButton}"
Focusable="False"
IsHitTestVisible="False"
IsChecked="{Binding IsMet}">
<ToggleButton.Content>
<Border Background="#E74C4C" Width="32" Height="32">
<materialDesign:PackIcon Kind="Close" VerticalAlignment="Center" HorizontalAlignment="Center" />
</Border>
</ToggleButton.Content>
<materialDesign:ToggleButtonAssist.OnContent>
<materialDesign:PackIcon Kind="Check" />
</materialDesign:ToggleButtonAssist.OnContent>
</ToggleButton>
<StackPanel Margin="8 0 0 0" Grid.Column="1" VerticalAlignment="Stretch">
<TextBlock FontWeight="Bold" Text="{Binding PluginPrerequisite.Name}" TextWrapping="Wrap" />
<TextBlock Text="{Binding PluginPrerequisite.Description}" TextWrapping="Wrap" />
</StackPanel>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<ContentControl Grid.Row="0"
Grid.RowSpan="2"
Grid.Column="1"
s:View.Model="{Binding ActivePrerequisite}"
VerticalContentAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Margin="10 0"
IsTabStop="False"
Visibility="{Binding ActivePrerequisite, Converter={StaticResource NullToVisibilityConverter}}"/>
<TextBlock Grid.Row="1"
Grid.Column="1"
Style="{StaticResource MaterialDesignBody1TextBlock}"
TextWrapping="Wrap"
Margin="10 0"
Visibility="{Binding ActivePrerequisite, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted}">
This plugin/feature installed certain prerequisites in order to function. <LineBreak /><LineBreak />
In this screen you can chose to remove these, this will mean the plugin/feature will no longer work until you reinstall the prerequisites.
</TextBlock>
<StackPanel Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0"
Visibility="{Binding IsFinished, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
<Button Style="{StaticResource MaterialDesignFlatButton}"
Focusable="False"
IsCancel="True"
Command="{s:Action Cancel}"
Content="{Binding CancelLabel}" />
<Button x:Name="ConfirmButton"
Style="{StaticResource MaterialDesignFlatButton}"
IsDefault="True"
Focusable="True"
Command="{s:Action Uninstall}"
Content="REMOVE PREREQUISITES" />
</StackPanel>
<StackPanel Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Orientation="Horizontal"
HorizontalAlignment="Right"
Margin="0 8 0 0"
Visibility="{Binding IsFinished, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<Button Style="{StaticResource MaterialDesignFlatButton}"
Focusable="False"
IsCancel="True"
Command="{s:Action Accept}"
Content="FINISH" />
</StackPanel>
</Grid>
</UserControl>

View File

@ -0,0 +1,172 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Shared.Services;
using MaterialDesignThemes.Wpf;
using Stylet;
namespace Artemis.UI.Screens.Plugins
{
public class PluginPrerequisitesUninstallDialogViewModel : DialogViewModelBase
{
private readonly IDialogService _dialogService;
private readonly IPluginManagementService _pluginManagementService;
private readonly List<IPrerequisitesSubject> _subjects;
private PluginPrerequisiteViewModel _activePrerequisite;
private bool _canUninstall;
private bool _isFinished;
private CancellationTokenSource _tokenSource;
public PluginPrerequisitesUninstallDialogViewModel(List<IPrerequisitesSubject> subjects, string cancelLabel, IPrerequisitesVmFactory prerequisitesVmFactory,
IDialogService dialogService, IPluginManagementService pluginManagementService)
{
_subjects = subjects;
_dialogService = dialogService;
_pluginManagementService = pluginManagementService;
CancelLabel = cancelLabel;
Prerequisites = new BindableCollection<PluginPrerequisiteViewModel>();
foreach (IPrerequisitesSubject prerequisitesSubject in subjects)
Prerequisites.AddRange(prerequisitesSubject.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, true)));
foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
pluginPrerequisiteViewModel.ConductWith(this);
}
public string CancelLabel { get; }
public BindableCollection<PluginPrerequisiteViewModel> Prerequisites { get; }
public PluginPrerequisiteViewModel ActivePrerequisite
{
get => _activePrerequisite;
set => SetAndNotify(ref _activePrerequisite, value);
}
public bool CanUninstall
{
get => _canUninstall;
set => SetAndNotify(ref _canUninstall, value);
}
public bool IsFinished
{
get => _isFinished;
set => SetAndNotify(ref _isFinished, value);
}
#region Overrides of DialogViewModelBase
/// <inheritdoc />
public override void OnDialogClosed(object sender, DialogClosingEventArgs e)
{
_tokenSource?.Cancel();
base.OnDialogClosed(sender, e);
}
#endregion
public async void Uninstall()
{
CanUninstall = false;
// Disable all subjects that are plugins, this will disable their features too
foreach (IPrerequisitesSubject prerequisitesSubject in _subjects)
{
if (prerequisitesSubject is PluginInfo pluginInfo)
_pluginManagementService.DisablePlugin(pluginInfo.Plugin, true);
}
// Disable all subjects that are features if still required
foreach (IPrerequisitesSubject prerequisitesSubject in _subjects)
{
if (prerequisitesSubject is not PluginFeatureInfo featureInfo)
continue;
// Disable the parent plugin if the feature is AlwaysEnabled
if (featureInfo.AlwaysEnabled)
_pluginManagementService.DisablePlugin(featureInfo.Plugin, true);
else if (featureInfo.Instance != null)
_pluginManagementService.DisablePluginFeature(featureInfo.Instance, true);
}
_tokenSource = new CancellationTokenSource();
try
{
foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
{
pluginPrerequisiteViewModel.IsMet = pluginPrerequisiteViewModel.PluginPrerequisite.IsMet();
if (!pluginPrerequisiteViewModel.IsMet)
continue;
ActivePrerequisite = pluginPrerequisiteViewModel;
await ActivePrerequisite.Uninstall(_tokenSource.Token);
// Wait after the task finished for the user to process what happened
if (pluginPrerequisiteViewModel != Prerequisites.Last())
await Task.Delay(1000);
}
if (Prerequisites.All(p => !p.IsMet))
{
IsFinished = true;
return;
}
// This shouldn't be happening and the experience isn't very nice for the user (too lazy to make a nice UI for such an edge case)
// but at least give some feedback
Session?.Close(false);
await _dialogService.ShowConfirmDialog(
"Plugin prerequisites",
"The plugin was not able to fully remove all prerequisites. \r\nPlease try again or contact the plugin creator.",
"Confirm",
""
);
await Show(_dialogService, _subjects);
}
catch (OperationCanceledException)
{
// ignored
}
finally
{
CanUninstall = true;
_tokenSource.Dispose();
_tokenSource = null;
}
}
public void Accept()
{
Session?.Close(true);
}
public static Task<object> Show(IDialogService dialogService, List<IPrerequisitesSubject> subjects, string cancelLabel = "CANCEL")
{
return dialogService.ShowDialog<PluginPrerequisitesUninstallDialogViewModel>(new Dictionary<string, object>
{
{"subjects", subjects},
{"cancelLabel", cancelLabel},
});
}
#region Overrides of Screen
/// <inheritdoc />
protected override void OnInitialActivate()
{
CanUninstall = false;
// Could be slow so take it off of the UI thread
Task.Run(() => CanUninstall = Prerequisites.Any(p => p.PluginPrerequisite.IsMet()));
base.OnInitialActivate();
}
#endregion
}
}

View File

@ -52,7 +52,7 @@
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding IsDeviceEnabled}">
Device enabled
</CheckBox>
<materialDesign:PopupBox Style="{StaticResource MaterialDesignToolPopupBox}" Padding="2 0 2 0">
<materialDesign:PopupBox Style="{StaticResource MaterialDesignToolPopupBox}" Padding="2 0 2 0" Foreground="{StaticResource MaterialDesignBody}">
<StackPanel>
<Button Command="{s:Action OpenPluginDirectory}">
<StackPanel Orientation="Horizontal">

View File

@ -12,7 +12,13 @@
d:DesignHeight="450" d:DesignWidth="800"
d:DataContext="{d:DesignInstance local:PluginFeatureViewModel}">
<UserControl.Resources>
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
</ResourceDictionary.MergedDictionaries>
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
</ResourceDictionary>
</UserControl.Resources>
<Grid Margin="-3 -8">
<Grid.ColumnDefinitions>
@ -23,11 +29,11 @@
<!-- Icon column -->
<shared:ArtemisIcon Grid.Column="0"
Icon="{Binding FeatureInfo.Icon}"
Width="20"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Visibility="{Binding LoadException, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted, FallbackValue=Collapsed}" />
Icon="{Binding FeatureInfo.Icon}"
Width="20"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Visibility="{Binding LoadException, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted, FallbackValue=Collapsed}" />
<Button Grid.Column="0"
Margin="-8"
@ -42,16 +48,16 @@
</Button>
<!-- Display name column -->
<TextBlock Grid.Column="1"
Text="{Binding FeatureInfo.Name}"
Style="{StaticResource MaterialDesignTextBlock}"
VerticalAlignment="Center"
<TextBlock Grid.Column="1"
Text="{Binding FeatureInfo.Name}"
Style="{StaticResource MaterialDesignTextBlock}"
VerticalAlignment="Center"
TextWrapping="Wrap"
ToolTip="{Binding FeatureInfo.Description}" />
<!-- Enable toggle column -->
<StackPanel Grid.Column="2"
HorizontalAlignment="Right"
HorizontalAlignment="Right"
Margin="8"
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay, FallbackValue=Collapsed}"
Orientation="Horizontal"
@ -63,11 +69,31 @@
VerticalAlignment="Center"
Margin="0 0 5 0"
Visibility="{Binding ShowShield, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}" />
<CheckBox Style="{StaticResource MaterialDesignCheckBox}"
<CheckBox Style="{StaticResource MaterialDesignCheckBox}"
IsChecked="{Binding IsEnabled}"
IsEnabled="{Binding CanToggleEnabled}">
Feature enabled
</CheckBox>
<materialDesign:PopupBox Style="{StaticResource MaterialDesignToolPopupBox}"
Margin="0 -4 -10 -4"
Foreground="{StaticResource MaterialDesignBody}"
IsEnabled="{Binding IsPopupEnabled}">
<StackPanel>
<Button Command="{s:Action InstallPrerequisites}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="CheckAll" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Install prerequisites</TextBlock>
</StackPanel>
</Button>
<Button Command="{s:Action RemovePrerequisites}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Delete" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Remove prerequisites</TextBlock>
</StackPanel>
</Button>
</StackPanel>
</materialDesign:PopupBox>
</StackPanel>
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="7"
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay, FallbackValue=Collapsed}">

View File

@ -1,9 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Screens.Plugins;
using Artemis.UI.Shared.Services;
using Stylet;
@ -13,11 +16,11 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
{
private readonly ICoreService _coreService;
private readonly IDialogService _dialogService;
private readonly IMessageService _messageService;
private readonly IPluginManagementService _pluginManagementService;
private bool _enabling;
private readonly IMessageService _messageService;
public PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo,
public PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo,
bool showShield,
ICoreService coreService,
IDialogService dialogService,
@ -51,6 +54,9 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
}
public bool CanToggleEnabled => FeatureInfo.Plugin.IsEnabled && !FeatureInfo.AlwaysEnabled;
public bool CanInstallPrerequisites => FeatureInfo.Prerequisites.Any();
public bool CanRemovePrerequisites => FeatureInfo.Prerequisites.Any(p => p.UninstallActions.Any());
public bool IsPopupEnabled => CanInstallPrerequisites || CanRemovePrerequisites;
public void ShowLogsFolder()
{
@ -72,6 +78,21 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
_dialogService.ShowExceptionDialog("Feature failed to enable", LoadException);
}
public async Task InstallPrerequisites()
{
if (FeatureInfo.Prerequisites.Any())
await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, new List<IPrerequisitesSubject> { FeatureInfo });
}
public async Task RemovePrerequisites()
{
if (FeatureInfo.Prerequisites.Any(p => p.UninstallActions.Any()))
{
await PluginPrerequisitesUninstallDialogViewModel.Show(_dialogService, new List<IPrerequisitesSubject> {FeatureInfo});
NotifyOfPropertyChange(nameof(IsEnabled));
}
}
protected override void OnInitialActivate()
{
_pluginManagementService.PluginFeatureEnabling += OnFeatureEnabling;
@ -80,7 +101,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
FeatureInfo.Plugin.Enabled += PluginOnToggled;
FeatureInfo.Plugin.Disabled += PluginOnToggled;
base.OnInitialActivate();
}
@ -120,7 +141,18 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
}
}
await Task.Run(() => _pluginManagementService.EnablePluginFeature(FeatureInfo.Instance, true));
// Check if all prerequisites are met async
if (!FeatureInfo.ArePrerequisitesMet())
{
await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, new List<IPrerequisitesSubject> { FeatureInfo });
if (!FeatureInfo.ArePrerequisitesMet())
{
NotifyOfPropertyChange(nameof(IsEnabled));
return;
}
}
await Task.Run(() => _pluginManagementService.EnablePluginFeature(FeatureInfo.Instance!, true));
}
catch (Exception e)
{
@ -138,8 +170,6 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
}
}
#region Event handlers
private void OnFeatureEnabling(object sender, PluginFeatureEventArgs e)
{
if (e.PluginFeature != FeatureInfo.Instance) return;
@ -159,7 +189,5 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
{
NotifyOfPropertyChange(nameof(CanToggleEnabled));
}
#endregion
}
}

View File

@ -49,7 +49,6 @@
<ListBox Grid.Row="1"
ItemsSource="{Binding Items}"
materialDesign:RippleAssist.IsDisabled="True"
VirtualizingPanel.ScrollUnit="Pixel"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Center"

View File

@ -12,6 +12,14 @@
d:DataContext="{d:DesignInstance devices:PluginSettingsViewModel}"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<materialDesign:Card Width="900">
<Grid Margin="8">
<Grid.ColumnDefinitions>
@ -61,34 +69,60 @@
</Grid>
<StackPanel Grid.Row="1"
Grid.Column="0"
VerticalAlignment="Bottom"
Orientation="Horizontal">
<Button
VerticalAlignment="Bottom"
Style="{StaticResource MaterialDesignRaisedButton}"
ToolTip="Open the plugins settings window"
Margin="4"
Command="{s:Action OpenSettings}">
<StackPanel Grid.Row="1" Grid.Column="0" VerticalAlignment="Bottom" Orientation="Horizontal">
<Button VerticalAlignment="Bottom"
Style="{StaticResource MaterialDesignRaisedButton}"
ToolTip="Open the plugins settings window"
Margin="4"
Command="{s:Action OpenSettings}">
SETTINGS
</Button>
<Button
VerticalAlignment="Bottom"
Style="{StaticResource MaterialDesignOutlinedButton}"
ToolTip="Clear plugin settings"
Margin="4"
Command="{s:Action RemoveSettings}">
<materialDesign:PackIcon Kind="DatabaseRemove" />
</Button>
<Button
VerticalAlignment="Bottom"
Style="{StaticResource MaterialDesignOutlinedButton}"
ToolTip="Remove plugin"
Margin="4"
Command="{s:Action Remove}">
<materialDesign:PackIcon Kind="DeleteForever" />
</Button>
<materialDesign:PopupBox Style="{StaticResource MaterialDesignToolPopupBox}"
Padding="2 0 2 0"
Foreground="{StaticResource MaterialDesignBody}"
IsPopupOpen="{Binding IsSettingsPopupOpen, Mode=TwoWay}">
<StackPanel>
<Button Command="{s:Action OpenPluginDirectory}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="FolderOpen" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Open plugin directory</TextBlock>
</StackPanel>
</Button>
<Button Command="{s:Action Reload}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Reload" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Reload plugin</TextBlock>
</StackPanel>
</Button>
<Separator />
<Button Command="{s:Action InstallPrerequisites}" >
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="CheckAll" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Install prerequisites</TextBlock>
</StackPanel>
</Button>
<Button Command="{s:Action RemovePrerequisites}" >
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="Delete" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Remove prerequisites</TextBlock>
</StackPanel>
</Button>
<Separator />
<Button Command="{s:Action RemoveSettings}" >
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="DatabaseRemove" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Clear plugin settings</TextBlock>
</StackPanel>
</Button>
<Button Command="{s:Action Remove}">
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="DeleteForever" Margin="0 0 10 0 " VerticalAlignment="Center" />
<TextBlock VerticalAlignment="Center">Remove plugin</TextBlock>
</StackPanel>
</Button>
</StackPanel>
</materialDesign:PopupBox>
</StackPanel>

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
@ -6,24 +7,26 @@ using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Ninject.Factories;
using Artemis.UI.Screens.Plugins;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using MaterialDesignThemes.Wpf;
using Ninject;
using Stylet;
using Constants = Artemis.Core.Constants;
namespace Artemis.UI.Screens.Settings.Tabs.Plugins
{
public class PluginSettingsViewModel : Conductor<PluginFeatureViewModel>.Collection.AllActive
{
private readonly ICoreService _coreService;
private readonly IDialogService _dialogService;
private readonly IMessageService _messageService;
private readonly IPluginManagementService _pluginManagementService;
private readonly ISettingsVmFactory _settingsVmFactory;
private readonly ICoreService _coreService;
private readonly IMessageService _messageService;
private readonly IWindowManager _windowManager;
private bool _canInstallPrerequisites;
private bool _canRemovePrerequisites;
private bool _enabling;
private bool _isSettingsPopupOpen;
private Plugin _plugin;
public PluginSettingsViewModel(Plugin plugin,
@ -68,6 +71,28 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
set => Task.Run(() => UpdateEnabled(value));
}
public bool IsSettingsPopupOpen
{
get => _isSettingsPopupOpen;
set
{
if (!SetAndNotify(ref _isSettingsPopupOpen, value)) return;
CheckPrerequisites();
}
}
public bool CanInstallPrerequisites
{
get => _canInstallPrerequisites;
set => SetAndNotify(ref _canInstallPrerequisites, value);
}
public bool CanRemovePrerequisites
{
get => _canRemovePrerequisites;
set => SetAndNotify(ref _canRemovePrerequisites, value);
}
public void OpenSettings()
{
PluginConfigurationDialog configurationViewModel = (PluginConfigurationDialog) Plugin.ConfigurationDialog;
@ -86,6 +111,57 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
}
}
public void OpenPluginDirectory()
{
try
{
Process.Start(Environment.GetEnvironmentVariable("WINDIR") + @"\explorer.exe", Plugin.Directory.FullName);
}
catch (Exception e)
{
_dialogService.ShowExceptionDialog("Welp, we couldn't open the device's plugin folder for you", e);
}
}
public async Task Reload()
{
bool wasEnabled = IsEnabled;
_pluginManagementService.UnloadPlugin(Plugin);
Items.Clear();
Plugin = _pluginManagementService.LoadPlugin(Plugin.Directory);
foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features)
Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false));
if (wasEnabled)
await UpdateEnabled(true);
_messageService.ShowMessage("Reloaded plugin.");
}
public async Task InstallPrerequisites()
{
List<IPrerequisitesSubject> subjects = new() {Plugin.Info};
subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled));
if (subjects.Any(s => s.Prerequisites.Any()))
await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, subjects);
}
public async Task RemovePrerequisites(bool forPluginRemoval = false)
{
List<IPrerequisitesSubject> subjects = new() {Plugin.Info};
subjects.AddRange(!forPluginRemoval ? Plugin.Features.Where(f => f.AlwaysEnabled) : Plugin.Features);
if (subjects.Any(s => s.Prerequisites.Any(p => p.UninstallActions.Any())))
{
await PluginPrerequisitesUninstallDialogViewModel.Show(_dialogService, subjects, forPluginRemoval ? "SKIP, REMOVE PLUGIN" : "CANCEL");
NotifyOfPropertyChange(nameof(IsEnabled));
NotifyOfPropertyChange(nameof(CanOpenSettings));
}
}
public async Task RemoveSettings()
{
bool confirmed = await _dialogService.ShowConfirmDialog("Clear plugin settings", "Are you sure you want to clear the settings of this plugin?");
@ -107,10 +183,16 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
public async Task Remove()
{
bool confirmed = await _dialogService.ShowConfirmDialog("Delete plugin", "Are you sure you want to delete this plugin?");
bool confirmed = await _dialogService.ShowConfirmDialog("Remove plugin", "Are you sure you want to remove this plugin?");
if (!confirmed)
return;
// If the plugin or any of its features has uninstall actions, offer to run these
List<IPrerequisitesSubject> subjects = new() {Plugin.Info};
subjects.AddRange(Plugin.Features);
if (subjects.Any(s => s.Prerequisites.Any(p => p.UninstallActions.Any())))
await RemovePrerequisites(true);
try
{
_pluginManagementService.RemovePlugin(Plugin, false);
@ -137,12 +219,10 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
}
}
protected override void OnInitialActivate()
private void PluginManagementServiceOnPluginToggled(object? sender, PluginEventArgs e)
{
foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features)
Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false));
base.OnInitialActivate();
NotifyOfPropertyChange(nameof(IsEnabled));
NotifyOfPropertyChange(nameof(CanOpenSettings));
}
private async Task UpdateEnabled(bool enable)
@ -162,33 +242,82 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
bool confirmed = await _dialogService.ShowConfirmDialog("Enable plugin", "This plugin requires admin rights, are you sure you want to enable it?");
if (!confirmed)
{
Enabling = false;
NotifyOfPropertyChange(nameof(IsEnabled));
NotifyOfPropertyChange(nameof(CanOpenSettings));
CancelEnable();
return;
}
}
try
// Check if all prerequisites are met async
List<IPrerequisitesSubject> subjects = new() {Plugin.Info};
subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled || f.EnabledInStorage));
if (subjects.Any(s => !s.ArePrerequisitesMet()))
{
await Task.Run(() => _pluginManagementService.EnablePlugin(Plugin, true, true));
await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, subjects);
if (!subjects.All(s => s.ArePrerequisitesMet()))
{
CancelEnable();
return;
}
}
catch (Exception e)
await Task.Run(() =>
{
_messageService.ShowMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder);
}
finally
{
Enabling = false;
}
try
{
_pluginManagementService.EnablePlugin(Plugin, true, true);
}
catch (Exception e)
{
_messageService.ShowMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder);
}
finally
{
Enabling = false;
}
});
}
else
{
_pluginManagementService.DisablePlugin(Plugin, true);
}
NotifyOfPropertyChange(nameof(IsEnabled));
NotifyOfPropertyChange(nameof(CanOpenSettings));
}
private void CancelEnable()
{
Enabling = false;
NotifyOfPropertyChange(nameof(IsEnabled));
NotifyOfPropertyChange(nameof(CanOpenSettings));
}
private void CheckPrerequisites()
{
CanInstallPrerequisites = Plugin.Info.Prerequisites.Any() ||
Plugin.Features.Where(f => f.AlwaysEnabled).Any(f => f.Prerequisites.Any());
CanRemovePrerequisites = Plugin.Info.Prerequisites.Any(p => p.UninstallActions.Any()) ||
Plugin.Features.Where(f => f.AlwaysEnabled).Any(f => f.Prerequisites.Any(p => p.UninstallActions.Any()));
}
#region Overrides of Screen
protected override void OnInitialActivate()
{
foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features)
Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false));
_pluginManagementService.PluginDisabled += PluginManagementServiceOnPluginToggled;
_pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginToggled;
base.OnInitialActivate();
}
protected override void OnClose()
{
_pluginManagementService.PluginDisabled -= PluginManagementServiceOnPluginToggled;
_pluginManagementService.PluginEnabled -= PluginManagementServiceOnPluginToggled;
base.OnClose();
}
#endregion
}
}