diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj
index 4abc0d292..f2e68cafb 100644
--- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj
+++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj
@@ -6,8 +6,12 @@
+
+ C:\Repos\Artemis\src\Artemis.UI.Avalonia.Shared\Artemis.UI.Avalonia.Shared.xml
+
+
diff --git a/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs b/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs
new file mode 100644
index 000000000..486498be8
--- /dev/null
+++ b/src/Artemis.UI.Avalonia.Shared/ViewModelBase.cs
@@ -0,0 +1,85 @@
+using System;
+using System.Reactive;
+using System.Reactive.Disposables;
+using ReactiveUI;
+
+namespace Artemis.UI.Avalonia.Shared
+{
+ ///
+ /// Represents the base class for Artemis view models
+ ///
+ public abstract class ViewModelBase : ReactiveObject
+ {
+ private string? _displayName;
+
+ ///
+ /// Gets or sets the display name of the view model
+ ///
+ public string? DisplayName
+ {
+ get => _displayName;
+ set => this.RaiseAndSetIfChanged(ref _displayName, value);
+ }
+ }
+
+ ///
+ /// Represents the base class for Artemis view models that are interested in the activated event
+ ///
+ public abstract class ActivatableViewModelBase : ViewModelBase, IActivatableViewModel, IDisposable
+ {
+ ///
+ protected ActivatableViewModelBase()
+ {
+ this.WhenActivated(disposables => Disposable.Create(Dispose).DisposeWith(disposables));
+ }
+
+ ///
+ /// Releases the unmanaged resources used by the object and optionally releases the managed resources.
+ ///
+ ///
+ /// to release both managed and unmanaged resources;
+ /// to release only unmanaged resources.
+ ///
+ protected virtual void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ }
+ }
+
+ ///
+ public ViewModelActivator Activator { get; } = new();
+
+ ///
+ public void Dispose()
+ {
+ Dispose(true);
+ GC.SuppressFinalize(this);
+ }
+ }
+
+ ///
+ /// Represents the base class for Artemis view models used to drive dialogs
+ ///
+ public abstract class DialogViewModelBase : ActivatableViewModelBase
+ {
+
+ ///
+ protected DialogViewModelBase()
+ {
+ Close = ReactiveCommand.Create(t => t);
+ Cancel = ReactiveCommand.Create(() => { });
+ }
+
+
+ ///
+ /// Closes the dialog with a given result
+ ///
+ public ReactiveCommand Close { get; }
+
+ ///
+ /// Closes the dialog without a result
+ ///
+ public ReactiveCommand Cancel { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI.Avalonia.Shared/packages.lock.json b/src/Artemis.UI.Avalonia.Shared/packages.lock.json
index e4e9b7e24..4e75df400 100644
--- a/src/Artemis.UI.Avalonia.Shared/packages.lock.json
+++ b/src/Artemis.UI.Avalonia.Shared/packages.lock.json
@@ -17,6 +17,17 @@
"System.ValueTuple": "4.5.0"
}
},
+ "Avalonia.ReactiveUI": {
+ "type": "Direct",
+ "requested": "[0.10.10, )",
+ "resolved": "0.10.10",
+ "contentHash": "hDMPhusehGxsHpwaFQwGOgEmAuFLp9VVnFDrX6Le1+8idpfxgHYWyzqo4uVYUiEO1OC2ab0Ik9Un/utLZcvh7w==",
+ "dependencies": {
+ "Avalonia": "0.10.10",
+ "ReactiveUI": "13.2.10",
+ "System.Reactive": "5.0.0"
+ }
+ },
"Avalonia.Svg.Skia": {
"type": "Direct",
"requested": "[0.10.8.3, )",
@@ -191,6 +202,14 @@
"System.Xml.XmlDocument": "4.3.0"
}
},
+ "DynamicData": {
+ "type": "Transitive",
+ "resolved": "7.1.1",
+ "contentHash": "Pc6J5bFnSxEa64PV2V67FMcLlDdpv6m+zTBKSnRN3aLon/WtWWy8kuDpHFbJlgXHtqc6Nxloj9ItuvDlvKC/8w==",
+ "dependencies": {
+ "System.Reactive": "5.0.0"
+ }
+ },
"EmbedIO": {
"type": "Transitive",
"resolved": "3.4.3",
@@ -457,6 +476,17 @@
"Ninject": "3.3.3"
}
},
+ "ReactiveUI": {
+ "type": "Transitive",
+ "resolved": "13.2.10",
+ "contentHash": "fOCbEZ+RsO2Jhv6vB8VX+ZEvczYJaC95atcSG7oXohJeL/sEwbbqvv9k+tbj2l4bRSj2j5CQvhwA3HNLaxlCAg==",
+ "dependencies": {
+ "DynamicData": "7.1.1",
+ "Splat": "10.0.1",
+ "System.Reactive": "5.0.0",
+ "System.Runtime.Serialization.Primitives": "4.3.0"
+ }
+ },
"runtime.debian.8-x64.runtime.native.System.Security.Cryptography.OpenSsl": {
"type": "Transitive",
"resolved": "4.3.0",
@@ -623,6 +653,11 @@
"SkiaSharp": "2.80.2"
}
},
+ "Splat": {
+ "type": "Transitive",
+ "resolved": "10.0.1",
+ "contentHash": "N8BMGVuUBnVLAHSVbna/st8XiLd8ulF3BfkKUSGCPqYpDCis3ELvM+aFaZQLBUIBEcweCYVLq3HFEBqHkCKFyA=="
+ },
"Svg.Custom": {
"type": "Transitive",
"resolved": "0.5.8.3",
@@ -1264,6 +1299,15 @@
"System.Runtime.Extensions": "4.3.0"
}
},
+ "System.Runtime.Serialization.Primitives": {
+ "type": "Transitive",
+ "resolved": "4.3.0",
+ "contentHash": "Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==",
+ "dependencies": {
+ "System.Resources.ResourceManager": "4.3.0",
+ "System.Runtime": "4.3.0"
+ }
+ },
"System.Security.AccessControl": {
"type": "Transitive",
"resolved": "5.0.0",
diff --git a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj
index 3f8a3f1ab..427d243fd 100644
--- a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj
+++ b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj
@@ -4,6 +4,9 @@
net5.0
enable
+
+
+
@@ -11,9 +14,6 @@
-
-
-
@@ -50,6 +50,15 @@
%(Filename)
+
+ %(Filename)
+
+
+ %(Filename)
+
+
+ %(Filename)
+
%(Filename)
@@ -62,7 +71,7 @@
%(Filename)
-
+
%(Filename)
@@ -83,18 +92,6 @@
-
- $(DefaultXamlRuntime)
- MSBuild:Compile
-
-
- $(DefaultXamlRuntime)
- MSBuild:Compile
-
-
- $(DefaultXamlRuntime)
- MSBuild:Compile
-
$(DefaultXamlRuntime)
MSBuild:Compile
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginFeatureViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginFeatureViewModel.cs
similarity index 100%
rename from src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginFeatureViewModel.cs
rename to src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginFeatureViewModel.cs
diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs
new file mode 100644
index 000000000..0a42da735
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisiteActionViewModel.cs
@@ -0,0 +1,20 @@
+using System;
+using System.ComponentModel;
+using Artemis.Core;
+using Artemis.UI.Avalonia;
+using Artemis.UI.Avalonia.Shared;
+
+namespace Artemis.UI.Screens.Plugins
+{
+ public class PluginPrerequisiteActionViewModel : ViewModelBase
+ {
+
+
+ public PluginPrerequisiteActionViewModel(PluginPrerequisiteAction action)
+ {
+ Action = action;
+ }
+
+ public PluginPrerequisiteAction Action { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs
new file mode 100644
index 000000000..453ec9bac
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisiteViewModel.cs
@@ -0,0 +1,133 @@
+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 bool _uninstall;
+ private bool _installing;
+ private bool _uninstalling;
+ private bool _isMet;
+
+ public PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall)
+ {
+ _uninstall = uninstall;
+ 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;
+
+ Installing = true;
+ try
+ {
+ await PluginPrerequisite.Install(cancellationToken);
+ }
+ finally
+ {
+ Installing = false;
+ IsMet = PluginPrerequisite.IsMet();
+ }
+ }
+
+ public async Task Uninstall(CancellationToken cancellationToken)
+ {
+ if (Busy)
+ return;
+
+ Uninstalling = true;
+ try
+ {
+ await PluginPrerequisite.Uninstall(cancellationToken);
+ }
+ finally
+ {
+ Uninstalling = false;
+ IsMet = PluginPrerequisite.IsMet();
+ }
+ }
+
+ private void PluginPrerequisiteOnPropertyChanged(object sender, PropertyChangedEventArgs e)
+ {
+ if (e.PropertyName == nameof(PluginPrerequisite.CurrentAction))
+ ActivateCurrentAction();
+ }
+
+ private void ActivateCurrentAction()
+ {
+ PluginPrerequisiteActionViewModel newActiveItem = Items.FirstOrDefault(i => i.Action == PluginPrerequisite.CurrentAction);
+ if (newActiveItem == null)
+ return;
+
+ ActiveItem = newActiveItem;
+ NotifyOfPropertyChange(nameof(ActiveStemNumber));
+ }
+
+ #region Overrides of Screen
+
+ ///
+ protected override void OnClose()
+ {
+ PluginPrerequisite.PropertyChanged -= PluginPrerequisiteOnPropertyChanged;
+ base.OnClose();
+ }
+
+ ///
+ protected override void OnInitialActivate()
+ {
+ PluginPrerequisite.PropertyChanged += PluginPrerequisiteOnPropertyChanged;
+ // Could be slow so take it off of the UI thread
+ Task.Run(() => IsMet = PluginPrerequisite.IsMet());
+
+ Items.AddRange(!_uninstall
+ ? PluginPrerequisite.InstallActions.Select(a => new PluginPrerequisiteActionViewModel(a))
+ : PluginPrerequisite.UninstallActions.Select(a => new PluginPrerequisiteActionViewModel(a)));
+
+ base.OnInitialActivate();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs
new file mode 100644
index 000000000..eccb78f87
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Plugins/ViewModels/PluginPrerequisitesInstallDialogViewModel.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.Core;
+using Artemis.UI.Avalonia.Shared;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Plugins
+{
+ public class PluginPrerequisitesInstallDialogViewModel : DialogViewModelBase
+ {
+ private PluginPrerequisiteViewModel _activePrerequisite;
+ private bool _canInstall;
+ private bool _showFailed;
+ private bool _showInstall = true;
+ private bool _showIntro = true;
+ private bool _showProgress;
+ private CancellationTokenSource? _tokenSource;
+
+ public PluginPrerequisitesInstallDialogViewModel(List subjects, IPrerequisitesVmFactory prerequisitesVmFactory, IDialogService dialogService)
+ {
+ Prerequisites = new ObservableCollection();
+ foreach (IPrerequisitesSubject prerequisitesSubject in subjects)
+ Prerequisites.AddRange(prerequisitesSubject.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false)));
+
+ foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
+ pluginPrerequisiteViewModel.ConductWith(this);
+ }
+
+ public ObservableCollection Prerequisites { get; }
+
+ public PluginPrerequisiteViewModel ActivePrerequisite
+ {
+ get => _activePrerequisite;
+ set => this.RaiseAndSetIfChanged(ref _activePrerequisite, value);
+ }
+
+ public bool ShowProgress
+ {
+ get => _showProgress;
+ set => this.RaiseAndSetIfChanged(ref _showProgress, value);
+ }
+
+ public bool ShowIntro
+ {
+ get => _showIntro;
+ set => this.RaiseAndSetIfChanged(ref _showIntro, value);
+ }
+
+ public bool ShowFailed
+ {
+ get => _showFailed;
+ set => this.RaiseAndSetIfChanged(ref _showFailed, value);
+ }
+
+ public bool ShowInstall
+ {
+ get => _showInstall;
+ set => this.RaiseAndSetIfChanged(ref _showInstall, value);
+ }
+
+ public bool CanInstall
+ {
+ get => _canInstall;
+ set => this.RaiseAndSetIfChanged(ref _canInstall, value);
+ }
+
+ public async void Install()
+ {
+ CanInstall = false;
+ ShowFailed = false;
+ ShowIntro = false;
+ ShowProgress = true;
+
+ _tokenSource = new CancellationTokenSource();
+
+ try
+ {
+ foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
+ {
+ pluginPrerequisiteViewModel.IsMet = pluginPrerequisiteViewModel.PluginPrerequisite.IsMet();
+ if (pluginPrerequisiteViewModel.IsMet)
+ continue;
+
+ ActivePrerequisite = pluginPrerequisiteViewModel;
+ await ActivePrerequisite.Install(_tokenSource.Token);
+
+ if (!ActivePrerequisite.IsMet)
+ {
+ CanInstall = true;
+ ShowFailed = true;
+ ShowProgress = false;
+ return;
+ }
+
+ // Wait after the task finished for the user to process what happened
+ if (pluginPrerequisiteViewModel != Prerequisites.Last())
+ await Task.Delay(1000);
+ }
+
+ ShowInstall = false;
+ }
+ catch (OperationCanceledException)
+ {
+ // ignored
+ }
+ finally
+ {
+ _tokenSource.Dispose();
+ _tokenSource = null;
+ }
+ }
+
+ public void Accept()
+ {
+ Result = true;
+ Close.Execute();
+ }
+
+ public static Task