From 19679c0ba69853509103d6848223de03db0eb686 Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 25 Apr 2021 23:18:00 +0200 Subject: [PATCH 1/5] Plugins - Added prerequisites system --- .../Artemis.Core.csproj.DotSettings | 2 + .../Extensions/StreamExtensions.cs | 133 +++++++++++++++ .../Plugins/IPluginBootstrapper.cs | 10 +- src/Artemis.Core/Plugins/Plugin.cs | 9 +- src/Artemis.Core/Plugins/PluginFeature.cs | 8 +- .../Prerequisites/PluginPrerequisite.cs | 156 ++++++++++++++++++ .../Prerequisites/PluginPrerequisiteAction.cs | 76 +++++++++ .../PrerequisiteAction/CopyFolderAction.cs | 78 +++++++++ .../PrerequisiteAction/WriteToFileAction.cs | 70 ++++++++ .../PrerequisiteActionProgress.cs | 91 ++++++++++ .../Services/PluginManagementService.cs | 3 + .../Services/ColorPickerService.cs | 2 +- .../Services/Dialog/DialogViewModelBase.cs | 4 +- .../Interfaces/IColorPickerService.cs | 2 +- .../Ninject/Factories/IVMFactory.cs | 11 +- .../Plugins/PluginPrerequisiteActionView.xaml | 26 +++ .../PluginPrerequisiteActionViewModel.cs | 71 ++++++++ .../Plugins/PluginPrerequisiteView.xaml | 37 +++++ .../Plugins/PluginPrerequisiteViewModel.cs | 142 ++++++++++++++++ .../PluginPrerequisitesDialogView.xaml | 126 ++++++++++++++ .../PluginPrerequisitesDialogViewModel.cs | 127 ++++++++++++++ .../Tabs/Plugins/PluginSettingsViewModel.cs | 39 ++++- 22 files changed, 1210 insertions(+), 13 deletions(-) create mode 100644 src/Artemis.Core/Extensions/StreamExtensions.cs create mode 100644 src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs create mode 100644 src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisiteAction.cs create mode 100644 src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/CopyFolderAction.cs create mode 100644 src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/WriteToFileAction.cs create mode 100644 src/Artemis.Core/Plugins/Prerequisites/PrerequisiteActionProgress.cs create mode 100644 src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionView.xaml create mode 100644 src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionViewModel.cs create mode 100644 src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.xaml create mode 100644 src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs create mode 100644 src/Artemis.UI/Screens/Plugins/PluginPrerequisitesDialogView.xaml create mode 100644 src/Artemis.UI/Screens/Plugins/PluginPrerequisitesDialogViewModel.cs 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 ShowGradientPicker(ColorGradient colorGradient, string dialogHost) + public Task ShowGradientPicker(ColorGradient colorGradient, string dialogHost) { if (!string.IsNullOrWhiteSpace(dialogHost)) return _dialogService.ShowDialogAt(dialogHost, new Dictionary {{"colorGradient", colorGradient}}); diff --git a/src/Artemis.UI.Shared/Services/Dialog/DialogViewModelBase.cs b/src/Artemis.UI.Shared/Services/Dialog/DialogViewModelBase.cs index 3cdddba3b..81c7fbbf0 100644 --- a/src/Artemis.UI.Shared/Services/Dialog/DialogViewModelBase.cs +++ b/src/Artemis.UI.Shared/Services/Dialog/DialogViewModelBase.cs @@ -6,7 +6,7 @@ namespace Artemis.UI.Shared.Services /// /// Represents the base class for a dialog view model /// - 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 /// 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. + + + + - - + + + + + + + + + + + + + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs index f025488c1..e3aa2f065 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs @@ -10,23 +10,24 @@ 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.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 _enabling; private Plugin _plugin; + private bool _isSettingsPopupOpen; + private bool _canInstallPrerequisites; + private bool _canRemovePrerequisites; public PluginSettingsViewModel(Plugin plugin, ISettingsVmFactory settingsVmFactory, @@ -70,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; @@ -88,6 +111,49 @@ 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); + } + + public async Task InstallPrerequisites() + { + if (Plugin.Prerequisites.Any()) + await ShowPrerequisitesDialog(false); + } + + public async Task RemovePrerequisites() + { + if (Plugin.Prerequisites.Any(p => p.UninstallActions.Any())) + { + await ShowPrerequisitesDialog(true); + 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?"); @@ -109,7 +175,7 @@ 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; @@ -170,10 +236,10 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } // Check if all prerequisites are met async - if (!await ArePrerequisitesMetAsync()) + if (!Plugin.ArePrerequisitesMet()) { - await _dialogService.ShowDialog(new Dictionary {{"pluginOrFeature", Plugin}}); - if (!await ArePrerequisitesMetAsync()) + await ShowPrerequisitesDialog(false); + if (!Plugin.ArePrerequisitesMet()) { CancelEnable(); return; @@ -194,9 +260,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } } else - { _pluginManagementService.DisablePlugin(Plugin, true); - } NotifyOfPropertyChange(nameof(IsEnabled)); NotifyOfPropertyChange(nameof(CanOpenSettings)); @@ -209,19 +273,17 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins NotifyOfPropertyChange(nameof(CanOpenSettings)); } - private async Task ArePrerequisitesMetAsync() + private void CheckPrerequisites() { - bool needsPrerequisites = false; - foreach (PluginPrerequisite pluginPrerequisite in Plugin.Prerequisites) - { - if (await pluginPrerequisite.IsMet()) - continue; + CanInstallPrerequisites = Plugin.Prerequisites.Any(); + CanRemovePrerequisites = Plugin.Prerequisites.Any(p => p.UninstallActions.Any()); + } - needsPrerequisites = true; - break; - } - - return !needsPrerequisites; + private async Task ShowPrerequisitesDialog(bool uninstall) + { + if (uninstall) + return await _dialogService.ShowDialog(new Dictionary { { "pluginOrFeature", Plugin } }); + return await _dialogService.ShowDialog(new Dictionary { { "pluginOrFeature", Plugin } }); } } } \ No newline at end of file From 5cae14efd3a391d95cefbe9526401fcbb162faeb Mon Sep 17 00:00:00 2001 From: Robert Date: Wed, 28 Apr 2021 17:06:47 +0200 Subject: [PATCH 3/5] Prerequisites - Moved to PluginInfo and FeatureInfo Features don't have instances until they are enabled so this made sense, Plugins followed suit for consistency's sake --- src/Artemis.Core/Plugins/Plugin.cs | 23 ++++++++----------- src/Artemis.Core/Plugins/PluginFeature.cs | 5 ---- src/Artemis.Core/Plugins/PluginFeatureInfo.cs | 15 ++++++++++++ src/Artemis.Core/Plugins/PluginInfo.cs | 16 +++++++++++++ .../Services/PluginManagementService.cs | 2 +- ...uginPrerequisitesInstallDialogViewModel.cs | 9 +++++--- ...inPrerequisitesUninstallDialogViewModel.cs | 9 +++++--- .../Tabs/Plugins/PluginSettingsViewModel.cs | 18 ++++++++++----- 8 files changed, 66 insertions(+), 31 deletions(-) diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index a8dbaf7ad..6dab36b99 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -78,11 +78,6 @@ 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 /// @@ -118,6 +113,16 @@ namespace Artemis.Core { return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T; } + + /// + /// Looks up the feature info the feature of type + /// + /// The type of feature to find + /// If found, feature info of the feature + public PluginFeatureInfo? GetFeatureInfo() where T : PluginFeature + { + return _features.FirstOrDefault(i => i.FeatureType == typeof(T)); + } /// public override string ToString() @@ -125,14 +130,6 @@ namespace Artemis.Core return Info.ToString(); } - /// - /// Determines whether the prerequisites of this plugin are met - /// - public bool ArePrerequisitesMet() - { - return Prerequisites.All(p => p.IsMet()); - } - /// /// Occurs when the plugin is enabled /// diff --git a/src/Artemis.Core/Plugins/PluginFeature.cs b/src/Artemis.Core/Plugins/PluginFeature.cs index dbe5a87d0..aa661c951 100644 --- a/src/Artemis.Core/Plugins/PluginFeature.cs +++ b/src/Artemis.Core/Plugins/PluginFeature.cs @@ -60,11 +60,6 @@ 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/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index 47f34cbaa..9bb78b186 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Artemis.Core.DataModelExpansions; using Artemis.Core.DeviceProviders; using Artemis.Core.LayerBrushes; @@ -128,6 +130,19 @@ namespace Artemis.Core internal set => SetAndNotify(ref _instance, value); } + /// + /// Gets a list of prerequisites for this plugin feature + /// + public List Prerequisites { get; } = new(); + + /// + /// Determines whether the prerequisites of this feature are met + /// + public bool ArePrerequisitesMet() + { + return Prerequisites.All(p => p.IsMet()); + } + /// public override string ToString() { diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index fcbeacd07..0ae163d14 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -1,5 +1,7 @@ using System; +using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using Newtonsoft.Json; namespace Artemis.Core @@ -117,6 +119,20 @@ namespace Artemis.Core internal set => SetAndNotify(ref _plugin, value); } + /// + /// Gets a list of prerequisites for this plugin + /// + public List Prerequisites { get; } = new(); + + + /// + /// Determines whether the prerequisites of this plugin are met + /// + public bool ArePrerequisitesMet() + { + return Prerequisites.All(p => p.IsMet()); + } + /// public override string ToString() { diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index a46c1e3fb..c28b8d44a 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -381,7 +381,7 @@ namespace Artemis.Core.Services return; } - if (!plugin.ArePrerequisitesMet()) + if (!plugin.Info.ArePrerequisitesMet()) throw new ArtemisPluginPrerequisiteException(plugin, null, "Cannot enable a plugin whose prerequisites aren't all met"); // Create the Ninject child kernel and load the module diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs index 26435d24e..9b0fb0323 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs @@ -27,12 +27,12 @@ namespace Artemis.UI.Screens.Plugins if (pluginOrFeature is Plugin plugin) { Plugin = plugin; - Prerequisites = new BindableCollection(plugin.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false))); + Prerequisites = new BindableCollection(plugin.Info.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false))); } else if (pluginOrFeature is PluginFeature feature) { Feature = feature; - Prerequisites = new BindableCollection(feature.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false))); + Prerequisites = new BindableCollection(feature.Info.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false))); } else throw new ArtemisUIException($"Expected plugin or feature to be passed to {nameof(PluginPrerequisitesInstallDialogViewModel)}"); @@ -136,7 +136,10 @@ namespace Artemis.UI.Screens.Plugins protected override void OnInitialActivate() { CanInstall = false; - Task.Run(() => CanInstall = !Plugin.ArePrerequisitesMet()); + if (Plugin != null) + Task.Run(() => CanInstall = !Plugin.Info.ArePrerequisitesMet()); + else + Task.Run(() => CanInstall = !Feature.Info.ArePrerequisitesMet()); base.OnInitialActivate(); } diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogViewModel.cs index 71a160df7..d4d27cc8d 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogViewModel.cs @@ -32,12 +32,12 @@ namespace Artemis.UI.Screens.Plugins if (pluginOrFeature is Plugin plugin) { Plugin = plugin; - Prerequisites = new BindableCollection(plugin.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, true))); + Prerequisites = new BindableCollection(plugin.Info.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, true))); } else if (pluginOrFeature is PluginFeature feature) { Feature = feature; - Prerequisites = new BindableCollection(feature.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, true))); + Prerequisites = new BindableCollection(feature.Info.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, true))); } else throw new ArtemisUIException($"Expected plugin or feature to be passed to {nameof(PluginPrerequisitesInstallDialogViewModel)}"); @@ -144,7 +144,10 @@ namespace Artemis.UI.Screens.Plugins { CanUninstall = false; // Could be slow so take it off of the UI thread - Task.Run(() => CanUninstall = Plugin.Prerequisites.Any(p => p.IsMet())); + if (Plugin != null) + Task.Run(() => CanUninstall = Plugin.Info.Prerequisites.Any(p => p.IsMet())); + else + Task.Run(() => CanUninstall = Feature.Info.Prerequisites.Any(p => p.IsMet())); base.OnInitialActivate(); } diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs index e3aa2f065..bf7e78361 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs @@ -140,13 +140,13 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins public async Task InstallPrerequisites() { - if (Plugin.Prerequisites.Any()) + if (Plugin.Info.Prerequisites.Any()) await ShowPrerequisitesDialog(false); } public async Task RemovePrerequisites() { - if (Plugin.Prerequisites.Any(p => p.UninstallActions.Any())) + if (Plugin.Info.Prerequisites.Any(p => p.UninstallActions.Any())) { await ShowPrerequisitesDialog(true); NotifyOfPropertyChange(nameof(IsEnabled)); @@ -179,6 +179,12 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins if (!confirmed) return; + // If the plugin or any of its features has uninstall actions, offer to run these + if (Plugin.Info.Prerequisites.Any(p => p.UninstallActions.Any()) || Plugin.Features.Any(f => f.Prerequisites.Any(fp => fp.UninstallActions.Any()))) + { + + } + try { _pluginManagementService.RemovePlugin(Plugin, false); @@ -236,10 +242,10 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } // Check if all prerequisites are met async - if (!Plugin.ArePrerequisitesMet()) + if (!Plugin.Info.ArePrerequisitesMet()) { await ShowPrerequisitesDialog(false); - if (!Plugin.ArePrerequisitesMet()) + if (!Plugin.Info.ArePrerequisitesMet()) { CancelEnable(); return; @@ -275,8 +281,8 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins private void CheckPrerequisites() { - CanInstallPrerequisites = Plugin.Prerequisites.Any(); - CanRemovePrerequisites = Plugin.Prerequisites.Any(p => p.UninstallActions.Any()); + CanInstallPrerequisites = Plugin.Info.Prerequisites.Any(); + CanRemovePrerequisites = Plugin.Info.Prerequisites.Any(p => p.UninstallActions.Any()); } private async Task ShowPrerequisitesDialog(bool uninstall) From 21700aaad5966a12c9bf3ab1cdc303bfb302406c Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 30 Apr 2021 17:39:58 +0200 Subject: [PATCH 4/5] Plugins - Reworked bootstrapper --- .../Plugins/IPluginBootstrapper.cs | 26 ----- src/Artemis.Core/Plugins/Plugin.cs | 9 +- .../Plugins/PluginBootstrapper.cs | 100 ++++++++++++++++++ src/Artemis.Core/Plugins/PluginFeatureInfo.cs | 17 +-- src/Artemis.Core/Plugins/PluginInfo.cs | 16 +-- .../Prerequisites/IPrerequisitesSubject.cs | 21 ++++ .../Prerequisites/PluginPrerequisite.cs | 33 +----- .../Services/PluginManagementService.cs | 6 +- ...uginPrerequisitesInstallDialogViewModel.cs | 29 ++--- ...inPrerequisitesUninstallDialogViewModel.cs | 34 +++--- .../Tabs/Plugins/PluginFeatureView.xaml | 50 ++++++--- .../Tabs/Plugins/PluginFeatureViewModel.cs | 44 +++++++- .../Tabs/Plugins/PluginSettingsViewModel.cs | 38 +++++-- 13 files changed, 288 insertions(+), 135 deletions(-) delete mode 100644 src/Artemis.Core/Plugins/IPluginBootstrapper.cs create mode 100644 src/Artemis.Core/Plugins/PluginBootstrapper.cs create mode 100644 src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs diff --git a/src/Artemis.Core/Plugins/IPluginBootstrapper.cs b/src/Artemis.Core/Plugins/IPluginBootstrapper.cs deleted file mode 100644 index bc2c20cb0..000000000 --- a/src/Artemis.Core/Plugins/IPluginBootstrapper.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace Artemis.Core -{ - /// - /// An optional entry point for your plugin - /// - 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 OnPluginEnabled(Plugin plugin); - - /// - /// Called when the plugin is deactivated or when Artemis shuts down - /// - /// The plugin instance of your 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 6dab36b99..e5e00841c 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -71,7 +71,7 @@ namespace Artemis.Core /// /// Gets the plugin bootstrapper /// - public IPluginBootstrapper? Bootstrapper { get; internal set; } + public PluginBootstrapper? Bootstrapper { get; internal set; } /// /// The Ninject kernel of the plugin @@ -118,10 +118,11 @@ namespace Artemis.Core /// Looks up the feature info the feature of type /// /// The type of feature to find - /// If found, feature info of the feature - public PluginFeatureInfo? GetFeatureInfo() where T : PluginFeature + /// Feature info of the feature + public PluginFeatureInfo GetFeatureInfo() where T : PluginFeature { - return _features.FirstOrDefault(i => i.FeatureType == typeof(T)); + // This should be a safe assumption because any type of PluginFeature is registered and added + return _features.First(i => i.FeatureType == typeof(T)); } /// diff --git a/src/Artemis.Core/Plugins/PluginBootstrapper.cs b/src/Artemis.Core/Plugins/PluginBootstrapper.cs new file mode 100644 index 000000000..0d41ad36d --- /dev/null +++ b/src/Artemis.Core/Plugins/PluginBootstrapper.cs @@ -0,0 +1,100 @@ +namespace Artemis.Core +{ + /// + /// An optional entry point for your plugin + /// + public abstract class PluginBootstrapper + { + private Plugin? _plugin; + + /// + /// Called when the plugin is loaded + /// + /// + public virtual void OnPluginLoaded(Plugin plugin) + { + } + + /// + /// Called when the plugin is activated + /// + /// The plugin instance of your plugin + public virtual void OnPluginEnabled(Plugin plugin) + { + } + + /// + /// Called when the plugin is deactivated or when Artemis shuts down + /// + /// The plugin instance of your plugin + public virtual void OnPluginDisabled(Plugin plugin) + { + } + + /// + /// Adds the provided prerequisite to the plugin. + /// + /// The prerequisite to add + public void AddPluginPrerequisite(PluginPrerequisite prerequisite) + { + // TODO: We can keep track of them and add them after load, same goes for the others + if (_plugin == null) + throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded"); + + if (!_plugin.Info.Prerequisites.Contains(prerequisite)) + _plugin.Info.Prerequisites.Add(prerequisite); + } + + /// + /// Removes the provided prerequisite from the plugin. + /// + /// The prerequisite to remove + /// + /// is successfully removed; otherwise . This method also returns + /// if the prerequisite was not found. + /// + public bool RemovePluginPrerequisite(PluginPrerequisite prerequisite) + { + if (_plugin == null) + throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded"); + + return _plugin.Info.Prerequisites.Remove(prerequisite); + } + + /// + /// Adds the provided prerequisite to the feature of type . + /// + /// The prerequisite to add + public void AddFeaturePrerequisite(PluginPrerequisite prerequisite) where T : PluginFeature + { + if (_plugin == null) + throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded"); + + PluginFeatureInfo info = _plugin.GetFeatureInfo(); + if (!info.Prerequisites.Contains(prerequisite)) + info.Prerequisites.Add(prerequisite); + } + + /// + /// Removes the provided prerequisite from the feature of type . + /// + /// The prerequisite to remove + /// + /// is successfully removed; otherwise . This method also returns + /// if the prerequisite was not found. + /// + public bool RemoveFeaturePrerequisite(PluginPrerequisite prerequisite) where T : PluginFeature + { + if (_plugin == null) + throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded"); + + return _plugin.GetFeatureInfo().Prerequisites.Remove(prerequisite); + } + + internal void InternalOnPluginLoaded(Plugin plugin) + { + _plugin = plugin; + OnPluginLoaded(plugin); + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index 9bb78b186..9e8be12cf 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -15,7 +15,7 @@ namespace Artemis.Core /// Represents basic info about a plugin feature and contains a reference to the instance of said feature /// [JsonObject(MemberSerialization.OptIn)] - public class PluginFeatureInfo : CorePropertyChanged + public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject { private string? _description; private string? _icon; @@ -31,7 +31,7 @@ namespace Artemis.Core Description = attribute?.Description; Icon = attribute?.Icon; AlwaysEnabled = attribute?.AlwaysEnabled ?? false; - + if (Icon != null) return; if (typeof(BaseDataModelExpansion).IsAssignableFrom(featureType)) Icon = "TableAdd"; @@ -130,18 +130,11 @@ namespace Artemis.Core internal set => SetAndNotify(ref _instance, value); } - /// - /// Gets a list of prerequisites for this plugin feature - /// + /// public List Prerequisites { get; } = new(); - /// - /// Determines whether the prerequisites of this feature are met - /// - public bool ArePrerequisitesMet() - { - return Prerequisites.All(p => p.IsMet()); - } + /// + public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet()); /// public override string ToString() diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index 0ae163d14..de8a77e7f 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -10,7 +10,7 @@ namespace Artemis.Core /// Represents basic info about a plugin and contains a reference to the instance of said plugin /// [JsonObject(MemberSerialization.OptIn)] - public class PluginInfo : CorePropertyChanged + public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject { private bool _autoEnableFeatures = true; private string? _description; @@ -119,19 +119,11 @@ namespace Artemis.Core internal set => SetAndNotify(ref _plugin, value); } - /// - /// Gets a list of prerequisites for this plugin - /// + /// public List Prerequisites { get; } = new(); - - /// - /// Determines whether the prerequisites of this plugin are met - /// - public bool ArePrerequisitesMet() - { - return Prerequisites.All(p => p.IsMet()); - } + /// + public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet()); /// public override string ToString() diff --git a/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs new file mode 100644 index 000000000..5401455ed --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Artemis.Core +{ + /// + /// Represents a type that has prerequisites + /// + public interface IPrerequisitesSubject + { + /// + /// Gets a list of prerequisites for this plugin + /// + List Prerequisites { get; } + + /// + /// Determines whether the prerequisites of this plugin are met + /// + bool ArePrerequisitesMet(); + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs index 5267b4d33..a762d7e7c 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; @@ -11,24 +12,6 @@ namespace Artemis.Core { 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 /// @@ -63,18 +46,6 @@ namespace Artemis.Core 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 /// diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index c28b8d44a..fe839e4bc 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -345,13 +345,13 @@ namespace Artemis.Core.Services if (!featureTypes.Any()) _logger.Warning("Plugin {plugin} contains no features", plugin); - List bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(IPluginBootstrapper).IsAssignableFrom(t)).ToList(); + List bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(PluginBootstrapper).IsAssignableFrom(t)).ToList(); if (bootstrappers.Count > 1) _logger.Warning($"{plugin} has more than one bootstrapper, only initializing {bootstrappers.First().FullName}"); if (bootstrappers.Any()) { - plugin.Bootstrapper = (IPluginBootstrapper?) Activator.CreateInstance(bootstrappers.First()); - plugin.Bootstrapper?.OnPluginLoaded(plugin); + plugin.Bootstrapper = (PluginBootstrapper?) Activator.CreateInstance(bootstrappers.First()); + plugin.Bootstrapper?.InternalOnPluginLoaded(plugin); } lock (_plugins) diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs index 9b0fb0323..1f955f691 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs @@ -20,19 +20,19 @@ namespace Artemis.UI.Screens.Plugins private bool _isFinished; private CancellationTokenSource _tokenSource; - public PluginPrerequisitesInstallDialogViewModel(object pluginOrFeature, IPrerequisitesVmFactory prerequisitesVmFactory, IDialogService dialogService) + public PluginPrerequisitesInstallDialogViewModel(IPrerequisitesSubject subject, IPrerequisitesVmFactory prerequisitesVmFactory, IDialogService dialogService) { _dialogService = dialogService; // Constructor overloading doesn't work very well with Kernel.Get :( - if (pluginOrFeature is Plugin plugin) + if (subject is PluginInfo plugin) { - Plugin = plugin; - Prerequisites = new BindableCollection(plugin.Info.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false))); + PluginInfo = plugin; + Prerequisites = new BindableCollection(plugin.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false))); } - else if (pluginOrFeature is PluginFeature feature) + else if (subject is PluginFeatureInfo feature) { - Feature = feature; - Prerequisites = new BindableCollection(feature.Info.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false))); + FeatureInfo = feature; + Prerequisites = new BindableCollection(feature.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false))); } else throw new ArtemisUIException($"Expected plugin or feature to be passed to {nameof(PluginPrerequisitesInstallDialogViewModel)}"); @@ -42,8 +42,8 @@ namespace Artemis.UI.Screens.Plugins } - public PluginFeature Feature { get; } - public Plugin Plugin { get; } + public PluginInfo PluginInfo { get; } + public PluginFeatureInfo FeatureInfo { get; } public BindableCollection Prerequisites { get; } public PluginPrerequisiteViewModel ActivePrerequisite @@ -64,6 +64,9 @@ namespace Artemis.UI.Screens.Plugins set => SetAndNotify(ref _isFinished, value); } + public bool IsSubjectPlugin => PluginInfo != null; + public bool IsSubjectFeature => FeatureInfo != null; + #region Overrides of DialogViewModelBase /// @@ -111,7 +114,7 @@ namespace Artemis.UI.Screens.Plugins "Confirm", "" ); - await _dialogService.ShowDialog(new Dictionary {{"pluginOrFeature", Plugin}}); + await _dialogService.ShowDialog(new Dictionary {{"subject", PluginInfo}}); } catch (OperationCanceledException) { @@ -136,10 +139,10 @@ namespace Artemis.UI.Screens.Plugins protected override void OnInitialActivate() { CanInstall = false; - if (Plugin != null) - Task.Run(() => CanInstall = !Plugin.Info.ArePrerequisitesMet()); + if (PluginInfo != null) + Task.Run(() => CanInstall = !PluginInfo.ArePrerequisitesMet()); else - Task.Run(() => CanInstall = !Feature.Info.ArePrerequisitesMet()); + Task.Run(() => CanInstall = !FeatureInfo.ArePrerequisitesMet()); base.OnInitialActivate(); } diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogViewModel.cs index d4d27cc8d..919e58098 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogViewModel.cs @@ -22,22 +22,22 @@ namespace Artemis.UI.Screens.Plugins private bool _isFinished; private CancellationTokenSource _tokenSource; - public PluginPrerequisitesUninstallDialogViewModel(object pluginOrFeature, IPrerequisitesVmFactory prerequisitesVmFactory, IDialogService dialogService, + public PluginPrerequisitesUninstallDialogViewModel(IPrerequisitesSubject subject, IPrerequisitesVmFactory prerequisitesVmFactory, IDialogService dialogService, IPluginManagementService pluginManagementService) { _dialogService = dialogService; _pluginManagementService = pluginManagementService; // Constructor overloading doesn't work very well with Kernel.Get :( - if (pluginOrFeature is Plugin plugin) + if (subject is PluginInfo plugin) { - Plugin = plugin; - Prerequisites = new BindableCollection(plugin.Info.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, true))); + PluginInfo = plugin; + Prerequisites = new BindableCollection(plugin.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, true))); } - else if (pluginOrFeature is PluginFeature feature) + else if (subject is PluginFeatureInfo feature) { - Feature = feature; - Prerequisites = new BindableCollection(feature.Info.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, true))); + FeatureInfo = feature; + Prerequisites = new BindableCollection(feature.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, true))); } else throw new ArtemisUIException($"Expected plugin or feature to be passed to {nameof(PluginPrerequisitesInstallDialogViewModel)}"); @@ -47,8 +47,8 @@ namespace Artemis.UI.Screens.Plugins } - public PluginFeature Feature { get; } - public Plugin Plugin { get; } + public PluginFeatureInfo FeatureInfo { get; } + public PluginInfo PluginInfo { get; } public BindableCollection Prerequisites { get; } public PluginPrerequisiteViewModel ActivePrerequisite @@ -69,6 +69,9 @@ namespace Artemis.UI.Screens.Plugins set => SetAndNotify(ref _isFinished, value); } + public bool IsSubjectPlugin => PluginInfo != null; + public bool IsSubjectFeature => FeatureInfo != null; + #region Overrides of DialogViewModelBase /// @@ -84,7 +87,10 @@ namespace Artemis.UI.Screens.Plugins { CanUninstall = false; - _pluginManagementService.DisablePlugin(Plugin, true); + if (PluginInfo != null) + _pluginManagementService.DisablePlugin(PluginInfo.Plugin, true); + else if (FeatureInfo?.Instance != null) + _pluginManagementService.DisablePluginFeature(FeatureInfo.Instance, true); _tokenSource = new CancellationTokenSource(); try @@ -118,7 +124,7 @@ namespace Artemis.UI.Screens.Plugins "Confirm", "" ); - await _dialogService.ShowDialog(new Dictionary {{"pluginOrFeature", Plugin}}); + await _dialogService.ShowDialog(new Dictionary {{"subject", PluginInfo}}); } catch (OperationCanceledException) { @@ -144,10 +150,10 @@ namespace Artemis.UI.Screens.Plugins { CanUninstall = false; // Could be slow so take it off of the UI thread - if (Plugin != null) - Task.Run(() => CanUninstall = Plugin.Info.Prerequisites.Any(p => p.IsMet())); + if (PluginInfo != null) + Task.Run(() => CanUninstall = PluginInfo.Prerequisites.Any(p => p.IsMet())); else - Task.Run(() => CanUninstall = Feature.Info.Prerequisites.Any(p => p.IsMet())); + Task.Run(() => CanUninstall = FeatureInfo.Prerequisites.Any(p => p.IsMet())); base.OnInitialActivate(); } diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml index 5055b02ac..92b177023 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureView.xaml @@ -12,7 +12,13 @@ d:DesignHeight="450" d:DesignWidth="800" d:DataContext="{d:DesignInstance local:PluginFeatureViewModel}"> - + + + + + + @@ -23,11 +29,11 @@ + Icon="{Binding FeatureInfo.Icon}" + Width="20" + VerticalAlignment="Center" + HorizontalAlignment="Center" + Visibility="{Binding LoadException, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted, FallbackValue=Collapsed}" /> + + + diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs index a4e8587dc..ee086b451 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs @@ -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; @@ -16,6 +19,9 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins private readonly IPluginManagementService _pluginManagementService; private bool _enabling; private readonly IMessageService _messageService; + private bool _isSettingsPopupOpen; + private bool _canInstallPrerequisites; + private bool _canRemovePrerequisites; public PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield, @@ -51,6 +57,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() { @@ -96,6 +105,21 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins base.OnClose(); } + public async Task InstallPrerequisites() + { + if (FeatureInfo.Prerequisites.Any()) + await ShowPrerequisitesDialog(false, FeatureInfo); + } + + public async Task RemovePrerequisites() + { + if (FeatureInfo.Prerequisites.Any(p => p.UninstallActions.Any())) + { + await ShowPrerequisitesDialog(true, FeatureInfo); + NotifyOfPropertyChange(nameof(IsEnabled)); + } + } + private async Task UpdateEnabled(bool enable) { if (IsEnabled == enable || FeatureInfo.Instance == null) @@ -120,7 +144,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 ShowPrerequisitesDialog(false, FeatureInfo); + if (!FeatureInfo.ArePrerequisitesMet()) + { + NotifyOfPropertyChange(nameof(IsEnabled)); + return; + } + } + + await Task.Run(() => _pluginManagementService.EnablePluginFeature(FeatureInfo.Instance!, true)); } catch (Exception e) { @@ -138,6 +173,13 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } } + private async Task ShowPrerequisitesDialog(bool uninstall, IPrerequisitesSubject subject) + { + if (uninstall) + return await _dialogService.ShowDialog(new Dictionary { { "subject", subject } }); + return await _dialogService.ShowDialog(new Dictionary { { "subject", subject } }); + } + #region Event handlers private void OnFeatureEnabling(object sender, PluginFeatureEventArgs e) diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs index bf7e78361..9c30349f1 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs @@ -136,24 +136,26 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins if (wasEnabled) await UpdateEnabled(true); + + _messageService.ShowMessage("Reloaded plugin."); } public async Task InstallPrerequisites() { if (Plugin.Info.Prerequisites.Any()) - await ShowPrerequisitesDialog(false); + await ShowPrerequisitesDialog(false, Plugin.Info); } public async Task RemovePrerequisites() { if (Plugin.Info.Prerequisites.Any(p => p.UninstallActions.Any())) { - await ShowPrerequisitesDialog(true); + await ShowPrerequisitesDialog(true, Plugin.Info); 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?"); @@ -180,9 +182,31 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins return; // If the plugin or any of its features has uninstall actions, offer to run these + List featuresToUninstall = Plugin.Features.Where(f => f.Prerequisites.Any(fp => fp.UninstallActions.Any())).ToList(); if (Plugin.Info.Prerequisites.Any(p => p.UninstallActions.Any()) || Plugin.Features.Any(f => f.Prerequisites.Any(fp => fp.UninstallActions.Any()))) { + bool remove = await _dialogService.ShowConfirmDialog( + "Remove plugin", + "This plugin installed one or more prerequisites.\r\nDo you want to remove these?", + "Uninstall", + "Skip" + ); + if (remove) + { + if (Plugin.Info.Prerequisites.Any(p => p.UninstallActions.Any())) + { + object result = await ShowPrerequisitesDialog(true, Plugin.Info); + if (result is bool resultBool && !resultBool) + return; + } + foreach (PluginFeatureInfo pluginFeatureInfo in featuresToUninstall) + { + object result = await ShowPrerequisitesDialog(true, pluginFeatureInfo); + if (result is bool resultBool && !resultBool) + return; + } + } } try @@ -244,7 +268,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins // Check if all prerequisites are met async if (!Plugin.Info.ArePrerequisitesMet()) { - await ShowPrerequisitesDialog(false); + await ShowPrerequisitesDialog(false, Plugin.Info); if (!Plugin.Info.ArePrerequisitesMet()) { CancelEnable(); @@ -285,11 +309,11 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins CanRemovePrerequisites = Plugin.Info.Prerequisites.Any(p => p.UninstallActions.Any()); } - private async Task ShowPrerequisitesDialog(bool uninstall) + private async Task ShowPrerequisitesDialog(bool uninstall, IPrerequisitesSubject subject) { if (uninstall) - return await _dialogService.ShowDialog(new Dictionary { { "pluginOrFeature", Plugin } }); - return await _dialogService.ShowDialog(new Dictionary { { "pluginOrFeature", Plugin } }); + return await _dialogService.ShowDialog(new Dictionary {{"subject", subject } }); + return await _dialogService.ShowDialog(new Dictionary {{ "subject", subject } }); } } } \ No newline at end of file From 77be79dde5f5e39aa8c5872beedbd6c213ffdc8d Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 2 May 2021 23:00:48 +0200 Subject: [PATCH 5/5] Prerequisites - Finalized UI logic surrounding install/remove --- .../ArtemisPluginPrerequisiteException.cs | 24 ++-- src/Artemis.Core/Plugins/Plugin.cs | 1 + src/Artemis.Core/Plugins/PluginFeatureInfo.cs | 13 +- .../PrerequisiteAction/DownloadFileAction.cs | 79 ++++++++++++ .../Services/PluginManagementService.cs | 37 +++--- .../PluginPrerequisitesInstallDialogView.xaml | 4 +- ...uginPrerequisitesInstallDialogViewModel.cs | 40 ++---- ...luginPrerequisitesUninstallDialogView.xaml | 8 +- ...inPrerequisitesUninstallDialogViewModel.cs | 71 +++++----- .../Tabs/Plugins/PluginFeatureViewModel.cs | 52 +++----- .../Tabs/Plugins/PluginSettingsViewModel.cs | 122 +++++++++--------- 11 files changed, 260 insertions(+), 191 deletions(-) create mode 100644 src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs diff --git a/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs index fd2dbe169..e2f3a850d 100644 --- a/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs +++ b/src/Artemis.Core/Exceptions/ArtemisPluginPrerequisiteException.cs @@ -7,32 +7,24 @@ namespace Artemis.Core /// public class ArtemisPluginPrerequisiteException : Exception { - internal ArtemisPluginPrerequisiteException(Plugin plugin, PluginPrerequisite? pluginPrerequisite) + internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject) { - Plugin = plugin; - PluginPrerequisite = pluginPrerequisite; + Subject = subject; } - internal ArtemisPluginPrerequisiteException(Plugin plugin, PluginPrerequisite? pluginPrerequisite, string message) : base(message) + internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message) : base(message) { - Plugin = plugin; - PluginPrerequisite = pluginPrerequisite; + Subject = subject; } - internal ArtemisPluginPrerequisiteException(Plugin plugin, PluginPrerequisite? pluginPrerequisite, string message, Exception inner) : base(message, inner) + internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message, Exception inner) : base(message, inner) { - Plugin = plugin; - PluginPrerequisite = pluginPrerequisite; + Subject = subject; } /// - /// Gets the plugin the error is related to + /// Gets the subject the error is related to /// - public Plugin Plugin { get; } - - /// - /// Gets the plugin prerequisite the error is related to - /// - public PluginPrerequisite? PluginPrerequisite { get; } + public IPrerequisitesSubject Subject { get; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index e5e00841c..ddd090bfd 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -25,6 +25,7 @@ namespace Artemis.Core Info = info; Directory = directory; Entity = pluginEntity ?? new PluginEntity {Id = Guid, IsEnabled = true}; + Info.Plugin = this; _features = new List(); } diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index 9e8be12cf..6806db862 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -6,6 +6,7 @@ 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; @@ -22,10 +23,11 @@ namespace Artemis.Core 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; @@ -48,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)); @@ -121,6 +123,11 @@ namespace Artemis.Core [JsonProperty] public bool AlwaysEnabled { get; } + /// + /// Gets a boolean indicating whether the feature is enabled in persistent storage + /// + public bool EnabledInStorage => Entity.IsEnabled; + /// /// Gets the feature this info is associated with /// @@ -136,6 +143,8 @@ namespace Artemis.Core /// public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet()); + internal PluginFeatureEntity Entity { get; } + /// public override string ToString() { diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs new file mode 100644 index 000000000..a4daf9532 --- /dev/null +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/DownloadFileAction.cs @@ -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 +{ + /// + /// Represents a plugin prerequisite action that downloads a file + /// + public class DownloadFileAction : PluginPrerequisiteAction + { + /// + /// Creates a new instance of a copy folder action + /// + /// The name of the action + /// The source URL to download + /// The target file to save as (will be created if needed) + public DownloadFileAction(string name, string source, string target) : base(name) + { + Source = source ?? throw new ArgumentNullException(nameof(source)); + Target = target ?? throw new ArgumentNullException(nameof(target)); + + ShowProgressBar = true; + } + + /// + /// Gets the source URL to download + /// + public string Source { get; } + + /// + /// Gets the target file to save as (will be created if needed) + /// + public string Target { get; } + + /// + 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"; + } + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index fe839e4bc..7b3e6dea1 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -340,7 +340,12 @@ 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); @@ -382,7 +387,7 @@ namespace Artemis.Core.Services } if (!plugin.Info.ArePrerequisitesMet()) - throw new ArtemisPluginPrerequisiteException(plugin, null, "Cannot enable a plugin whose prerequisites aren't all met"); + 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)); @@ -406,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) { @@ -418,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) { @@ -585,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; @@ -596,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); @@ -608,7 +610,6 @@ namespace Artemis.Core.Services new ArtemisPluginException(pluginFeature.Plugin, $"Exception during SetEnabled(true) on {pluginFeature}", e), "Failed to enable plugin" ); - throw; } finally { diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogView.xaml b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogView.xaml index 5484241ea..d4c933acc 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogView.xaml +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogView.xaml @@ -24,7 +24,7 @@ - Plugin prerequisites + Plugin/feature prerequisites - In order for this plugin to function certain prerequisites must be met. + In order for this plugin/feature 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. diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs index 1f955f691..6b95d93c5 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesInstallDialogViewModel.cs @@ -4,7 +4,6 @@ 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; @@ -15,35 +14,25 @@ namespace Artemis.UI.Screens.Plugins public class PluginPrerequisitesInstallDialogViewModel : DialogViewModelBase { private readonly IDialogService _dialogService; + private readonly List _subjects; private PluginPrerequisiteViewModel _activePrerequisite; private bool _canInstall; private bool _isFinished; private CancellationTokenSource _tokenSource; - public PluginPrerequisitesInstallDialogViewModel(IPrerequisitesSubject subject, IPrerequisitesVmFactory prerequisitesVmFactory, IDialogService dialogService) + public PluginPrerequisitesInstallDialogViewModel(List subjects, IPrerequisitesVmFactory prerequisitesVmFactory, IDialogService dialogService) { + _subjects = subjects; _dialogService = dialogService; - // Constructor overloading doesn't work very well with Kernel.Get :( - if (subject is PluginInfo plugin) - { - PluginInfo = plugin; - Prerequisites = new BindableCollection(plugin.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false))); - } - else if (subject is PluginFeatureInfo feature) - { - FeatureInfo = feature; - Prerequisites = new BindableCollection(feature.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false))); - } - else - throw new ArtemisUIException($"Expected plugin or feature to be passed to {nameof(PluginPrerequisitesInstallDialogViewModel)}"); + + Prerequisites = new BindableCollection(); + foreach (IPrerequisitesSubject prerequisitesSubject in subjects) + Prerequisites.AddRange(prerequisitesSubject.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false))); foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites) pluginPrerequisiteViewModel.ConductWith(this); } - - public PluginInfo PluginInfo { get; } - public PluginFeatureInfo FeatureInfo { get; } public BindableCollection Prerequisites { get; } public PluginPrerequisiteViewModel ActivePrerequisite @@ -64,9 +53,6 @@ namespace Artemis.UI.Screens.Plugins set => SetAndNotify(ref _isFinished, value); } - public bool IsSubjectPlugin => PluginInfo != null; - public bool IsSubjectFeature => FeatureInfo != null; - #region Overrides of DialogViewModelBase /// @@ -114,7 +100,7 @@ namespace Artemis.UI.Screens.Plugins "Confirm", "" ); - await _dialogService.ShowDialog(new Dictionary {{"subject", PluginInfo}}); + await Show(_dialogService, _subjects); } catch (OperationCanceledException) { @@ -133,16 +119,18 @@ namespace Artemis.UI.Screens.Plugins Session?.Close(true); } + public static Task Show(IDialogService dialogService, List subjects) + { + return dialogService.ShowDialog(new Dictionary {{"subjects", subjects}}); + } + #region Overrides of Screen /// protected override void OnInitialActivate() { CanInstall = false; - if (PluginInfo != null) - Task.Run(() => CanInstall = !PluginInfo.ArePrerequisitesMet()); - else - Task.Run(() => CanInstall = !FeatureInfo.ArePrerequisitesMet()); + Task.Run(() => CanInstall = Prerequisites.Any(p => !p.PluginPrerequisite.IsMet())); base.OnInitialActivate(); } diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogView.xaml b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogView.xaml index cc6abcd40..7e77a7fc9 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogView.xaml +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisitesUninstallDialogView.xaml @@ -24,7 +24,7 @@ - Plugin prerequisites + Plugin/feature prerequisites - This plugin installed certain prerequisites in order to function. - In this screen you can chose to remove these, this will mean the plugin will no longer work until you reinstall the prerequisites. + This plugin/feature installed certain prerequisites in order to function. + In this screen you can chose to remove these, this will mean the plugin/feature will no longer work until you reinstall the prerequisites. + Content="{Binding CancelLabel}" />