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/Extensions/StreamExtensions.cs b/src/Artemis.Core/Extensions/StreamExtensions.cs
new file mode 100644
index 000000000..f4d5b9a5f
--- /dev/null
+++ b/src/Artemis.Core/Extensions/StreamExtensions.cs
@@ -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;
+
+ ///
+ /// Copys 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 (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();
+ }
+
+ ///
+ /// Copys 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);
+ }
+
+ ///
+ /// Copys 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);
+ }
+
+ ///
+ /// Copys 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);
+ }
+
+ ///
+ /// Copys 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/Plugins/IPluginBootstrapper.cs b/src/Artemis.Core/Plugins/IPluginBootstrapper.cs
index af4b607d7..bc2c20cb0 100644
--- a/src/Artemis.Core/Plugins/IPluginBootstrapper.cs
+++ b/src/Artemis.Core/Plugins/IPluginBootstrapper.cs
@@ -5,16 +5,22 @@
///
public interface IPluginBootstrapper
{
+ ///
+ /// Called when the plugin is loaded
+ ///
+ ///
+ void OnPluginLoaded(Plugin plugin);
+
///
/// Called when the plugin is activated
///
/// The plugin instance of your plugin
- void Enable(Plugin plugin);
+ void OnPluginEnabled(Plugin plugin);
///
/// Called when the plugin is deactivated or when Artemis shuts down
///
/// The plugin instance of your plugin
- void Disable(Plugin plugin);
+ void OnPluginDisabled(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..03403cf51 100644
--- a/src/Artemis.Core/Plugins/Plugin.cs
+++ b/src/Artemis.Core/Plugins/Plugin.cs
@@ -78,6 +78,11 @@ namespace Artemis.Core
///
public IKernel? Kernel { get; internal set; }
+ ///
+ /// Gets a list of prerequisites for this plugin feature
+ ///
+ public List Prerequisites { get; } = new();
+
///
/// The PluginLoader backing this plugin
///
@@ -235,12 +240,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/PluginFeature.cs b/src/Artemis.Core/Plugins/PluginFeature.cs
index 8b9943a0a..dbe5a87d0 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
///
@@ -59,6 +60,11 @@ namespace Artemis.Core
///
public TimeSpan RenderTime { get; private set; }
+ ///
+ /// Gets a list of prerequisites for this plugin feature
+ ///
+ public List Prerequisites { get; } = new();
+
internal PluginFeatureEntity Entity { get; set; } = null!; // Will be set right after construction
///
diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs
new file mode 100644
index 000000000..ea272786b
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs
@@ -0,0 +1,156 @@
+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;
+
+ ///
+ /// Creates a new instance of the class
+ ///
+ /// The plugin this is a prerequisite for
+ protected PluginPrerequisite(Plugin plugin)
+ {
+ Plugin = plugin;
+ }
+
+ ///
+ /// Creates a new instance of the class
+ ///
+ /// The plugin feature this is a prerequisite for
+ protected PluginPrerequisite(PluginFeature pluginFeature)
+ {
+ PluginFeature = pluginFeature;
+ }
+
+ ///
+ /// 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);
+ }
+
+ ///
+ /// Gets or sets the plugin this prerequisite is for
+ /// Note: Only one plugin or a plugin feature can be set at once
+ ///
+ public Plugin? Plugin { get; }
+
+ ///
+ /// Gets or sets the feature this prerequisite is for
+ /// Note: Only one plugin or a plugin feature can be set at once
+ ///
+ public PluginFeature? PluginFeature { get; }
+
+ ///
+ /// 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 Task 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..344d4412d
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs
@@ -0,0 +1,76 @@
+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 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 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..7a18f5f08
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs
@@ -0,0 +1,78 @@
+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;
+ }
+
+ ///
+ /// 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/WriteToFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteToFileAction.cs
new file mode 100644
index 000000000..0dedc3f28
--- /dev/null
+++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteToFileAction.cs
@@ -0,0 +1,70 @@
+using System;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Artemis.Core
+{
+ ///
+ /// Represents a plugin prerequisite action that copies a folder
+ ///
+ public class WriteToFileAction : 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 WriteToFileAction(string name, string target, string content) : base(name)
+ {
+ Target = target ?? throw new ArgumentNullException(nameof(target));
+ Content = content ?? throw new ArgumentNullException(nameof(content));
+ }
+
+ ///
+ /// 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 WriteToFileAction(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 the contents that will be written
+ ///
+ public string? Content { get; }
+
+ ///
+ /// 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);
+
+ ProgressIndeterminate = true;
+ Status = $"Writing to {Path.GetFileName(Target)}...";
+
+ if (Content != null)
+ await File.WriteAllTextAsync(Target, Content, cancellationToken);
+ else if (ByteContent != null)
+ await File.WriteAllBytesAsync(Target, ByteContent, cancellationToken);
+
+ ProgressIndeterminate = 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..9fcd8829a 100644
--- a/src/Artemis.Core/Services/PluginManagementService.cs
+++ b/src/Artemis.Core/Services/PluginManagementService.cs
@@ -340,7 +340,10 @@ namespace Artemis.Core.Services
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?.OnPluginLoaded(plugin);
+ }
lock (_plugins)
{
diff --git a/src/Artemis.UI.Shared/Services/ColorPickerService.cs b/src/Artemis.UI.Shared/Services/ColorPickerService.cs
index a3602c1b7..bacb62802 100644
--- a/src/Artemis.UI.Shared/Services/ColorPickerService.cs
+++ b/src/Artemis.UI.Shared/Services/ColorPickerService.cs
@@ -53,7 +53,7 @@ namespace Artemis.UI.Shared.Services
public LinkedList RecentColors => RecentColorsSetting.Value;
- public Task
public virtual void OnDialogClosed(object sender, DialogClosingEventArgs e)
{
+ ScreenExtensions.TryClose(this);
}
///
@@ -61,6 +62,7 @@ namespace Artemis.UI.Shared.Services
internal void OnDialogOpened(object sender, DialogOpenedEventArgs e)
{
Session = e.Session;
+ ScreenExtensions.TryActivate(this);
}
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IColorPickerService.cs b/src/Artemis.UI.Shared/Services/Interfaces/IColorPickerService.cs
index a5bbc26f8..6fcee45e3 100644
--- a/src/Artemis.UI.Shared/Services/Interfaces/IColorPickerService.cs
+++ b/src/Artemis.UI.Shared/Services/Interfaces/IColorPickerService.cs
@@ -7,7 +7,7 @@ namespace Artemis.UI.Shared.Services
{
internal interface IColorPickerService : IArtemisSharedUIService
{
- Task ShowGradientPicker(ColorGradient colorGradient, string dialogHost);
+ Task ShowGradientPicker(ColorGradient colorGradient, string dialogHost);
PluginSetting PreviewSetting { get; }
LinkedList RecentColors { get; }
diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs
index e24134dce..d5d54ef50 100644
--- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs
+++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs
@@ -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 layerPropertyGroups);
}
- public interface IDataBindingsVmFactory
+ public interface IPrerequisitesVmFactory : IVmFactory
+ {
+ PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite);
+ }
+
+ // TODO: Move these two
+ public interface IDataBindingsVmFactory
{
IDataBindingViewModel DataBindingViewModel(IDataBindingRegistration registration);
DirectDataBindingModeViewModel DirectDataBindingModeViewModel(DirectDataBinding directDataBinding);
@@ -104,7 +111,7 @@ namespace Artemis.UI.Ninject.Factories
DataBindingConditionViewModel DataBindingConditionViewModel(DataBindingCondition dataBindingCondition);
}
- public interface IPropertyVmFactory
+ public interface IPropertyVmFactory
{
ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel);
ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel);
diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionView.xaml b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionView.xaml
new file mode 100644
index 000000000..31ec31f24
--- /dev/null
+++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionView.xaml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionViewModel.cs
new file mode 100644
index 000000000..279414a6a
--- /dev/null
+++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionViewModel.cs
@@ -0,0 +1,71 @@
+using System;
+using System.ComponentModel;
+using Artemis.Core;
+using Stylet;
+
+namespace Artemis.UI.Screens.Plugins
+{
+ public class PluginPrerequisiteActionViewModel : Screen
+ {
+ private bool _showProgressBar;
+ private bool _showSubProgressBar;
+
+ public PluginPrerequisiteActionViewModel(PluginPrerequisiteAction action)
+ {
+ Action = action;
+ }
+
+ public PluginPrerequisiteAction Action { get; }
+
+ public bool ShowProgressBar
+ {
+ get => _showProgressBar;
+ set => SetAndNotify(ref _showProgressBar, value);
+ }
+
+ public bool ShowSubProgressBar
+ {
+ get => _showSubProgressBar;
+ set => SetAndNotify(ref _showSubProgressBar, value);
+ }
+
+ private void ActionOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(Action.ProgressIndeterminate) || e.PropertyName == nameof(Action.SubProgressIndeterminate))
+ UpdateProgress();
+ }
+
+ private void ProgressReported(object? sender, EventArgs e)
+ {
+ UpdateProgress();
+ }
+
+ private void UpdateProgress()
+ {
+ ShowSubProgressBar = Action.SubProgress.Percentage != 0 || Action.SubProgressIndeterminate;
+ ShowProgressBar = ShowSubProgressBar || Action.Progress.Percentage != 0 || Action.ProgressIndeterminate;
+ }
+
+ #region Overrides of Screen
+
+ ///
+ protected override void OnInitialActivate()
+ {
+ Action.Progress.ProgressReported += ProgressReported;
+ Action.SubProgress.ProgressReported += ProgressReported;
+ Action.PropertyChanged += ActionOnPropertyChanged;
+ base.OnInitialActivate();
+ }
+
+ ///
+ protected override void OnClose()
+ {
+ Action.Progress.ProgressReported -= ProgressReported;
+ Action.SubProgress.ProgressReported -= ProgressReported;
+ Action.PropertyChanged -= ActionOnPropertyChanged;
+ base.OnClose();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.xaml b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.xaml
new file mode 100644
index 000000000..3049b6c16
--- /dev/null
+++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.xaml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs
new file mode 100644
index 000000000..b70b97ba4
--- /dev/null
+++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs
@@ -0,0 +1,142 @@
+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.Collection.OneActive
+ {
+ private readonly ICoreService _coreService;
+ private readonly IDialogService _dialogService;
+ private bool _installing;
+ private bool _uninstalling;
+ private bool _isMet;
+
+ public PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, ICoreService coreService, IDialogService dialogService)
+ {
+ _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 = await 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 = await 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
+
+ ///
+ protected override void OnClose()
+ {
+ PluginPrerequisite.PropertyChanged += PluginPrerequisiteOnPropertyChanged;
+ base.OnClose();
+ }
+
+ ///
+ protected override void OnInitialActivate()
+ {
+ PluginPrerequisite.PropertyChanged -= PluginPrerequisiteOnPropertyChanged;
+ Task.Run(async () => IsMet = await PluginPrerequisite.IsMet());
+
+ Items.AddRange(PluginPrerequisite.InstallActions.Select(a => new PluginPrerequisiteActionViewModel(a)));
+ Items.AddRange(PluginPrerequisite.UninstallActions.Select(a => new PluginPrerequisiteActionViewModel(a)));
+
+ base.OnInitialActivate();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesDialogView.xaml b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesDialogView.xaml
new file mode 100644
index 000000000..124406e0b
--- /dev/null
+++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesDialogView.xaml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Plugin prerequisites
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ In order for this plugin to function certain prerequisites must be met.
+ 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.
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesDialogViewModel.cs
new file mode 100644
index 000000000..5c1cb62ba
--- /dev/null
+++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesDialogViewModel.cs
@@ -0,0 +1,127 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.Core;
+using Artemis.UI.Exceptions;
+using Artemis.UI.Ninject.Factories;
+using Artemis.UI.Shared.Services;
+using MaterialDesignThemes.Wpf;
+using Stylet;
+
+namespace Artemis.UI.Screens.Plugins
+{
+ public class PluginPrerequisitesDialogViewModel : DialogViewModelBase
+ {
+ private PluginPrerequisiteViewModel _activePrerequisite;
+ private bool _canInstall;
+ private bool _isFinished;
+ private CancellationTokenSource _tokenSource;
+
+ public PluginPrerequisitesDialogViewModel(object pluginOrFeature, IPrerequisitesVmFactory prerequisitesVmFactory)
+ {
+ // Constructor overloading doesn't work very well with Kernel.Get :(
+ if (pluginOrFeature is Plugin plugin)
+ {
+ Plugin = plugin;
+ Prerequisites = new BindableCollection(plugin.Prerequisites.Select(prerequisitesVmFactory.PluginPrerequisiteViewModel));
+ }
+ else if (pluginOrFeature is PluginFeature feature)
+ {
+ Feature = feature;
+ Prerequisites = new BindableCollection(feature.Prerequisites.Select(prerequisitesVmFactory.PluginPrerequisiteViewModel));
+ }
+ else
+ throw new ArtemisUIException($"Expected plugin or feature to be passed to {nameof(PluginPrerequisitesDialogViewModel)}");
+
+ foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
+ pluginPrerequisiteViewModel.ConductWith(this);
+ }
+
+
+ public PluginFeature Feature { get; }
+ public Plugin Plugin { get; }
+ public BindableCollection 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
+
+ ///
+ 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)
+ {
+ ActivePrerequisite = pluginPrerequisiteViewModel;
+ ActivePrerequisite.IsMet = await ActivePrerequisite.PluginPrerequisite.IsMet();
+ if (ActivePrerequisite.IsMet)
+ continue;
+
+ 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;
+ }
+ catch (OperationCanceledException)
+ {
+ // ignored
+ }
+ finally
+ {
+ CanInstall = true;
+ _tokenSource.Dispose();
+ _tokenSource = null;
+ }
+ }
+
+ public void Accept()
+ {
+ Session?.Close(true);
+ }
+
+ #region Overrides of Screen
+
+ ///
+ protected override void OnInitialActivate()
+ {
+ base.OnInitialActivate();
+ CanInstall = Prerequisites.Any(p => !p.IsMet);
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs
index fca3bb96e..f025488c1 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs
+++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
@@ -6,6 +7,7 @@ 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;
@@ -162,9 +164,18 @@ 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;
+ }
+ }
+
+ // Check if all prerequisites are met async
+ if (!await ArePrerequisitesMetAsync())
+ {
+ await _dialogService.ShowDialog(new Dictionary {{"pluginOrFeature", Plugin}});
+ if (!await ArePrerequisitesMetAsync())
+ {
+ CancelEnable();
return;
}
}
@@ -190,5 +201,27 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
NotifyOfPropertyChange(nameof(IsEnabled));
NotifyOfPropertyChange(nameof(CanOpenSettings));
}
+
+ private void CancelEnable()
+ {
+ Enabling = false;
+ NotifyOfPropertyChange(nameof(IsEnabled));
+ NotifyOfPropertyChange(nameof(CanOpenSettings));
+ }
+
+ private async Task ArePrerequisitesMetAsync()
+ {
+ bool needsPrerequisites = false;
+ foreach (PluginPrerequisite pluginPrerequisite in Plugin.Prerequisites)
+ {
+ if (await pluginPrerequisite.IsMet())
+ continue;
+
+ needsPrerequisites = true;
+ break;
+ }
+
+ return !needsPrerequisites;
+ }
}
}
\ No newline at end of file