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. + + + +