From 3b4194cb9dfea9a0b85d7512ae9479ec2928cf1f Mon Sep 17 00:00:00 2001 From: Robert Date: Sun, 10 Jul 2022 23:25:34 +0200 Subject: [PATCH] Plugins - Ported prerequisites UI Scripting - Ported scripting UI --- src/Artemis.Core/Models/Profile/Profile.cs | 74 +++++- src/Artemis.Core/Plugins/PluginFeatureInfo.cs | 5 +- src/Artemis.Core/Plugins/PluginInfo.cs | 5 +- src/Artemis.Core/Plugins/PluginPlatform.cs | 20 ++ .../Prerequisites/IPrerequisitesSubject.cs | 6 +- .../Prerequisites/PluginPrerequisite.cs | 22 ++ .../ExtractArchiveAction.cs | 16 +- .../ScriptingProviders/ScriptConfiguration.cs | 4 +- .../ScriptingProviders/ScriptingProvider.cs | 6 + .../Scripts/GlobalScript.cs | 2 +- .../Scripts/ProfileScript.cs | 5 +- .../ScriptingProviders/Scripts/Script.cs | 5 + .../Services/Interfaces/IScriptingService.cs | 39 ++-- src/Artemis.Core/Services/ScriptingService.cs | 215 ++++++++++-------- .../WebServer/Interfaces/IWebServerService.cs | 10 + .../WebServer/WebModuleRegistration.cs | 22 +- .../Services/WebServer/WebServerService.cs | 14 ++ src/Artemis.UI.Linux/App.axaml.cs | 2 +- src/Artemis.UI.Linux/packages.lock.json | 8 +- src/Artemis.UI.MacOS/packages.lock.json | 8 +- .../Artemis.UI.Shared.csproj | 2 +- .../Builders/OpenFileDialogBuilder.cs | 2 +- .../Builders/SaveFileDialogBuilder.cs | 2 +- src/Artemis.UI.Shared/packages.lock.json | 6 +- src/Artemis.UI.Windows/packages.lock.json | 8 +- src/Artemis.UI/Artemis.UI.csproj | 6 +- .../Ninject/Factories/IVMFactory.cs | 9 + ...PluginPrerequisitesInstallDialogView.axaml | 84 +++++++ ...ginPrerequisitesInstallDialogView.axaml.cs | 19 ++ ...uginPrerequisitesInstallDialogViewModel.cs | 49 ++-- ...uginPrerequisitesUninstallDialogView.axaml | 72 ++++++ ...nPrerequisitesUninstallDialogView.axaml.cs | 19 ++ ...inPrerequisitesUninstallDialogViewModel.cs | 87 +++---- .../Screens/Plugins/PluginFeatureViewModel.cs | 8 +- .../PluginPrerequisiteActionView.axaml | 21 ++ .../PluginPrerequisiteActionView.axaml.cs | 18 ++ .../Plugins/PluginPrerequisiteView.axaml | 27 +++ .../Plugins/PluginPrerequisiteView.axaml.cs | 19 ++ .../Plugins/PluginPrerequisiteViewModel.cs | 1 - .../Screens/Plugins/PluginSettingsView.axaml | 48 ++-- .../Plugins/PluginSettingsViewModel.cs | 51 +++-- .../Panels/MenuBar/MenuBarView.axaml | 2 +- .../Panels/MenuBar/MenuBarViewModel.cs | 11 + .../ScriptConfigurationCreateView.axaml | 35 +++ .../ScriptConfigurationCreateView.axaml.cs | 19 ++ .../ScriptConfigurationCreateViewModel.cs | 52 +++++ .../Dialogs/ScriptConfigurationEditView.axaml | 11 + .../ScriptConfigurationEditView.axaml.cs | 25 ++ .../ScriptConfigurationEditViewModel.cs | 40 ++++ .../Scripting/ScriptConfigurationViewModel.cs | 63 +++++ .../Screens/Scripting/ScriptsDialogView.axaml | 86 +++++++ .../Scripting/ScriptsDialogView.axaml.cs | 21 ++ .../Scripting/ScriptsDialogViewModel.cs | 146 ++++++++++++ .../Settings/Tabs/GeneralTabViewModel.cs | 4 +- src/Artemis.UI/packages.lock.json | 8 +- .../packages.lock.json | 6 +- 56 files changed, 1301 insertions(+), 274 deletions(-) create mode 100644 src/Artemis.Core/Plugins/PluginPlatform.cs create mode 100644 src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml create mode 100644 src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml create mode 100644 src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionView.axaml create mode 100644 src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.axaml create mode 100644 src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationCreateView.axaml create mode 100644 src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationCreateView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationCreateViewModel.cs create mode 100644 src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationEditView.axaml create mode 100644 src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationEditView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationEditViewModel.cs create mode 100644 src/Artemis.UI/Screens/Scripting/ScriptConfigurationViewModel.cs create mode 100644 src/Artemis.UI/Screens/Scripting/ScriptsDialogView.axaml create mode 100644 src/Artemis.UI/Screens/Scripting/ScriptsDialogView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Scripting/ScriptsDialogViewModel.cs diff --git a/src/Artemis.Core/Models/Profile/Profile.cs b/src/Artemis.Core/Models/Profile/Profile.cs index 072881ea5..d8f143462 100644 --- a/src/Artemis.Core/Models/Profile/Profile.cs +++ b/src/Artemis.Core/Models/Profile/Profile.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using Artemis.Core.ScriptingProviders; using Artemis.Storage.Entities.Profile; @@ -15,19 +16,24 @@ namespace Artemis.Core private readonly object _lock = new(); private bool _isFreshImport; private ProfileElement? _lastSelectedProfileElement; + private readonly ObservableCollection _scripts; + private readonly ObservableCollection _scriptConfigurations; internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!) { + _scripts = new ObservableCollection(); + _scriptConfigurations = new ObservableCollection(); + Configuration = configuration; Profile = this; ProfileEntity = profileEntity; EntityId = profileEntity.Id; - Scripts = new List(); - ScriptConfigurations = new List(); UndoStack = new MaxStack(20); RedoStack = new MaxStack(20); Exceptions = new List(); + Scripts = new ReadOnlyObservableCollection(_scripts); + ScriptConfigurations = new ReadOnlyObservableCollection(_scriptConfigurations); Load(); } @@ -40,12 +46,12 @@ namespace Artemis.Core /// /// Gets a collection of all active scripts assigned to this profile /// - public List Scripts { get; } + public ReadOnlyObservableCollection Scripts { get; } /// /// Gets a collection of all script configurations assigned to this profile /// - public List ScriptConfigurations { get; } + public ReadOnlyObservableCollection ScriptConfigurations { get; } /// /// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it @@ -169,8 +175,8 @@ namespace Artemis.Core if (!disposing) return; - while (Scripts.Count > 1) - Scripts[0].Dispose(); + while (Scripts.Count > 0) + RemoveScript(Scripts[0]); foreach (ProfileElement profileElement in Children) profileElement.Dispose(); @@ -208,16 +214,62 @@ namespace Artemis.Core else LastSelectedProfileElement = null; - foreach (ScriptConfiguration scriptConfiguration in ScriptConfigurations) - scriptConfiguration.Script?.Dispose(); - ScriptConfigurations.Clear(); - ScriptConfigurations.AddRange(ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e))); + while (_scriptConfigurations.Any()) + RemoveScriptConfiguration(_scriptConfigurations[0]); + foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e))) + AddScriptConfiguration(scriptConfiguration); // Load node scripts last since they may rely on the profile structure being in place - foreach (RenderProfileElement renderProfileElement in renderElements) + foreach (RenderProfileElement renderProfileElement in renderElements) renderProfileElement.LoadNodeScript(); } + /// + /// Removes a script configuration from the profile, if the configuration has an active script it is also removed. + /// + internal void RemoveScriptConfiguration(ScriptConfiguration scriptConfiguration) + { + if (!_scriptConfigurations.Contains(scriptConfiguration)) + return; + + Script? script = scriptConfiguration.Script; + if (script != null) + RemoveScript((ProfileScript) script); + + _scriptConfigurations.Remove(scriptConfiguration); + } + + /// + /// Adds a script configuration to the profile but does not instantiate it's script. + /// + internal void AddScriptConfiguration(ScriptConfiguration scriptConfiguration) + { + if (!_scriptConfigurations.Contains(scriptConfiguration)) + _scriptConfigurations.Add(scriptConfiguration); + } + + /// + /// Adds a script that has a script configuration belonging to this profile. + /// + internal void AddScript(ProfileScript script) + { + if (!_scriptConfigurations.Contains(script.ScriptConfiguration)) + throw new ArtemisCoreException("Cannot add a script to a profile whose script configuration doesn't belong to the same profile."); + + if (!_scripts.Contains(script)) + _scripts.Add(script); + } + + /// + /// Removes a script from the profile and disposes it. + /// + internal void RemoveScript(ProfileScript script) + { + _scripts.Remove(script); + script.Dispose(); + + } + internal override void Save() { if (Disposed) diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index f247a3eeb..62012d9a4 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -166,10 +166,13 @@ namespace Artemis.Core /// public List Prerequisites { get; } = new(); + /// + public IEnumerable PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform()); + /// public bool ArePrerequisitesMet() { - return Prerequisites.All(p => p.IsMet()); + return PlatformPrerequisites.All(p => p.IsMet()); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginInfo.cs b/src/Artemis.Core/Plugins/PluginInfo.cs index fea5fbece..2486544cc 100644 --- a/src/Artemis.Core/Plugins/PluginInfo.cs +++ b/src/Artemis.Core/Plugins/PluginInfo.cs @@ -175,11 +175,14 @@ namespace Artemis.Core /// public List Prerequisites { get; } = new(); + + /// + public IEnumerable PlatformPrerequisites => Prerequisites.Where(p => p.AppliesToPlatform()); /// public bool ArePrerequisitesMet() { - return Prerequisites.All(p => p.IsMet()); + return PlatformPrerequisites.All(p => p.IsMet()); } } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginPlatform.cs b/src/Artemis.Core/Plugins/PluginPlatform.cs new file mode 100644 index 000000000..5d44e7943 --- /dev/null +++ b/src/Artemis.Core/Plugins/PluginPlatform.cs @@ -0,0 +1,20 @@ +using System; + +namespace Artemis.Core; + +/// +/// Specifies OS platforms a plugin may support. +/// +[Flags] +public enum PluginPlatform +{ + /// The Windows platform. + Windows = 0, + + /// The Linux platform. + Linux = 1, + + /// The OSX platform. + // ReSharper disable once InconsistentNaming + OSX = 2 +} \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs index 5401455ed..6fa0d25cd 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/IPrerequisitesSubject.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; namespace Artemis.Core { @@ -12,6 +11,11 @@ namespace Artemis.Core /// Gets a list of prerequisites for this plugin /// List Prerequisites { get; } + + /// + /// Gets a list of prerequisites of the current platform for this plugin + /// + IEnumerable PlatformPrerequisites { get; } /// /// Determines whether the prerequisites of this plugin are met diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs index 3adc6fac1..edb408093 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs @@ -22,6 +22,11 @@ namespace Artemis.Core /// public abstract string Description { get; } + /// + /// Gets or sets the platform(s) this prerequisite applies to. + /// + public PluginPlatform? Platform { get; protected set; } + /// /// Gets a list of actions to execute when is called /// @@ -91,6 +96,23 @@ namespace Artemis.Core /// if the prerequisite is met; otherwise public abstract bool IsMet(); + /// + /// Determines whether this prerequisite applies to the current operating system. + /// + public bool AppliesToPlatform() + { + if (Platform == null) + return true; + + if (OperatingSystem.IsWindows()) + return Platform.Value.HasFlag(PluginPlatform.Windows); + if (OperatingSystem.IsLinux()) + return Platform.Value.HasFlag(PluginPlatform.Linux); + if (OperatingSystem.IsMacOS()) + return Platform.Value.HasFlag(PluginPlatform.OSX); + return false; + } + /// /// Called before installation starts /// diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs index b09f076b9..033379a6f 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExtractArchiveAction.cs @@ -1,6 +1,8 @@ using System; +using System.Collections.Generic; using System.IO; using System.IO.Compression; +using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -36,6 +38,11 @@ namespace Artemis.Core /// public string Target { get; } + /// + /// Gets or sets an optional list of files to extract, if all files will be extracted. + /// + public List? FilesToExtract { get; set; } + /// public override async Task Execute(CancellationToken cancellationToken) { @@ -50,10 +57,15 @@ namespace Artemis.Core { ZipArchive archive = new(fileStream); long count = 0; - foreach (ZipArchiveEntry entry in archive.Entries) + + List entries = new(archive.Entries); + if (FilesToExtract != null) + entries = entries.Where(e => FilesToExtract.Contains(e.FullName)).ToList(); + + foreach (ZipArchiveEntry entry in entries) { await using Stream unzippedEntryStream = entry.Open(); - Progress.Report((count, archive.Entries.Count)); + Progress.Report((count, entries.Count)); if (entry.Length > 0) { string path = Path.Combine(Target, entry.FullName); diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs index ecc953eb5..f93fd0413 100644 --- a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs +++ b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptConfiguration.cs @@ -18,11 +18,13 @@ namespace Artemis.Core.ScriptingProviders /// /// Creates a new instance of the class /// - public ScriptConfiguration(ScriptingProvider provider, string name) + public ScriptConfiguration(ScriptingProvider provider, string name, ScriptType scriptType) { _scriptingProviderId = provider.Id; _name = name; Entity = new ScriptConfigurationEntity(); + PendingScriptContent = provider.GetDefaultScriptContent(scriptType); + ScriptContent = PendingScriptContent; } internal ScriptConfiguration(ScriptConfigurationEntity entity) diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs index 1f8d4b1c4..cb69e82ad 100644 --- a/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs +++ b/src/Artemis.Core/Plugins/ScriptingProviders/ScriptingProvider.cs @@ -71,5 +71,11 @@ namespace Artemis.Core.ScriptingProviders /// /// The type of script the editor will host public abstract IScriptEditorViewModel CreateScriptEditor(ScriptType scriptType); + + /// + /// Called when a script for a certain type needs default content. + /// + /// The type of script the default content is for. + public abstract string GetDefaultScriptContent(ScriptType scriptType); } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/Scripts/GlobalScript.cs b/src/Artemis.Core/Plugins/ScriptingProviders/Scripts/GlobalScript.cs index 4cc762f11..8b4f4d781 100644 --- a/src/Artemis.Core/Plugins/ScriptingProviders/Scripts/GlobalScript.cs +++ b/src/Artemis.Core/Plugins/ScriptingProviders/Scripts/GlobalScript.cs @@ -38,7 +38,7 @@ namespace Artemis.Core.ScriptingProviders /// internal override void InternalCleanup() { - ScriptingService?.InternalGlobalScripts.Remove(this); + ScriptingService?.RemoveScript(ScriptConfiguration); } #endregion diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/Scripts/ProfileScript.cs b/src/Artemis.Core/Plugins/ScriptingProviders/Scripts/ProfileScript.cs index 8b5f18ce4..405133515 100644 --- a/src/Artemis.Core/Plugins/ScriptingProviders/Scripts/ProfileScript.cs +++ b/src/Artemis.Core/Plugins/ScriptingProviders/Scripts/ProfileScript.cs @@ -60,10 +60,7 @@ namespace Artemis.Core.ScriptingProviders /// internal override void InternalCleanup() { - lock (Profile.Scripts) - { - Profile.Scripts.Remove(this); - } + Profile.RemoveScript(this); } #endregion diff --git a/src/Artemis.Core/Plugins/ScriptingProviders/Scripts/Script.cs b/src/Artemis.Core/Plugins/ScriptingProviders/Scripts/Script.cs index 279c18252..07d7ad580 100644 --- a/src/Artemis.Core/Plugins/ScriptingProviders/Scripts/Script.cs +++ b/src/Artemis.Core/Plugins/ScriptingProviders/Scripts/Script.cs @@ -9,6 +9,7 @@ namespace Artemis.Core.ScriptingProviders public abstract class Script : CorePropertyChanged, IDisposable { private ScriptingProvider _scriptingProvider = null!; + private bool _disposed; /// /// The base constructor of any script @@ -71,6 +72,10 @@ namespace Artemis.Core.ScriptingProviders /// public void Dispose() { + if (_disposed) + return; + + _disposed = true; ScriptConfiguration.PropertyChanged -= ScriptConfigurationOnPropertyChanged; ScriptConfiguration.Script = null; ScriptingProvider.InternalScripts.Remove(this); diff --git a/src/Artemis.Core/Services/Interfaces/IScriptingService.cs b/src/Artemis.Core/Services/Interfaces/IScriptingService.cs index 037939ce7..24223d5af 100644 --- a/src/Artemis.Core/Services/Interfaces/IScriptingService.cs +++ b/src/Artemis.Core/Services/Interfaces/IScriptingService.cs @@ -8,33 +8,40 @@ namespace Artemis.Core.Services /// public interface IScriptingService : IArtemisService { + /// + /// Gets a list of all available scripting providers + /// + ReadOnlyCollection ScriptingProviders { get; } + /// /// Gets a list of all currently active global scripts /// ReadOnlyCollection GlobalScripts { get; } /// - /// Creates a instance for the given + /// Adds a script by the provided script configuration to the provided profile and instantiates it. /// - /// The script configuration of the script - /// - /// If the was found an instance of the script; otherwise . - /// - GlobalScript? CreateScriptInstance(ScriptConfiguration scriptConfiguration); + /// The script configuration whose script to add. + /// The profile to add the script to. + ProfileScript AddScript(ScriptConfiguration scriptConfiguration, Profile profile); /// - /// Creates a instance for the given + /// Removes a script by the provided script configuration from the provided profile and disposes it. /// - /// The profile the script configuration is configured for - /// The script configuration of the script - /// - /// If the was found an instance of the script; otherwise . - /// - ProfileScript? CreateScriptInstance(Profile profile, ScriptConfiguration scriptConfiguration); - + /// The script configuration whose script to remove. + /// The profile to remove the script from. + void RemoveScript(ScriptConfiguration scriptConfiguration, Profile profile); + /// - /// Deletes the provided global script by it's configuration + /// Adds a script by the provided script configuration to the global collection and instantiates it. /// - void DeleteScript(ScriptConfiguration scriptConfiguration); + /// The script configuration whose script to add. + GlobalScript AddScript(ScriptConfiguration scriptConfiguration); + + /// + /// Removes a script by the provided script configuration from the global collection and disposes it. + /// + /// The script configuration whose script to remove. + void RemoveScript(ScriptConfiguration scriptConfiguration); } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/ScriptingService.cs b/src/Artemis.Core/Services/ScriptingService.cs index 0418b4ec4..da0a0fd40 100644 --- a/src/Artemis.Core/Services/ScriptingService.cs +++ b/src/Artemis.Core/Services/ScriptingService.cs @@ -6,29 +6,28 @@ using System.Reflection; using Artemis.Core.ScriptingProviders; using Ninject; using Ninject.Parameters; -using Serilog; namespace Artemis.Core.Services { internal class ScriptingService : IScriptingService { - private readonly ILogger _logger; private readonly IPluginManagementService _pluginManagementService; private readonly IProfileService _profileService; - private List _scriptingProviders; + private readonly List _globalScripts; + private readonly List _scriptingProviders; - public ScriptingService(ILogger logger, IPluginManagementService pluginManagementService, IProfileService profileService) + public ScriptingService(IPluginManagementService pluginManagementService, IProfileService profileService) { - _logger = logger; _pluginManagementService = pluginManagementService; _profileService = profileService; - InternalGlobalScripts = new List(); - GlobalScripts = new ReadOnlyCollection(InternalGlobalScripts); - _pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureToggled; _pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureToggled; _scriptingProviders = _pluginManagementService.GetFeaturesOfType(); + _globalScripts = new List(); + + ScriptingProviders = new ReadOnlyCollection(_scriptingProviders); + GlobalScripts = new ReadOnlyCollection(_globalScripts); // No need to sub to Deactivated, scripts will deactivate themselves profileService.ProfileActivated += ProfileServiceOnProfileActivated; @@ -39,9 +38,112 @@ namespace Artemis.Core.Services InitializeProfileScripts(profileConfiguration.Profile); } } + + /// + public ReadOnlyCollection ScriptingProviders { get; } - internal List InternalGlobalScripts { get; } + /// + public ReadOnlyCollection GlobalScripts { get; } + + /// + public ProfileScript AddScript(ScriptConfiguration scriptConfiguration, Profile profile) + { + profile.AddScriptConfiguration(scriptConfiguration); + return CreateScriptInstance(scriptConfiguration, profile); + } + /// + public void RemoveScript(ScriptConfiguration scriptConfiguration, Profile profile) + { + profile.RemoveScriptConfiguration(scriptConfiguration); + } + + /// + public GlobalScript AddScript(ScriptConfiguration scriptConfiguration) + { + throw new NotImplementedException("Global scripts are not yet implemented."); + } + + /// + public void RemoveScript(ScriptConfiguration scriptConfiguration) + { + throw new NotImplementedException("Global scripts are not yet implemented."); + } + + private GlobalScript CreateScriptInstance(ScriptConfiguration scriptConfiguration) + { + GlobalScript? script = null; + try + { + if (scriptConfiguration.Script != null) + throw new ArtemisCoreException("The provided script configuration already has an active script"); + + ScriptingProvider? provider = _scriptingProviders.FirstOrDefault(p => p.Id == scriptConfiguration.ScriptingProviderId); + if (provider == null) + throw new ArtemisCoreException($"Can't create script instance as there is no matching scripting provider found for the script ({scriptConfiguration.ScriptingProviderId})."); + + script = (GlobalScript) provider.Plugin.Kernel!.Get( + provider.GlobalScriptType, + CreateScriptConstructorArgument(provider.GlobalScriptType, scriptConfiguration) + ); + + script.ScriptingProvider = provider; + script.ScriptingService = this; + scriptConfiguration.Script = script; + provider.InternalScripts.Add(script); + + return script; + } + catch (Exception e) + { + script?.Dispose(); + throw new ArtemisCoreException("Failed to initialize global script", e); + } + } + + private ProfileScript CreateScriptInstance(ScriptConfiguration scriptConfiguration, Profile profile) + { + ProfileScript? script = null; + try + { + if (scriptConfiguration.Script != null) + throw new ArtemisCoreException("The provided script configuration already has an active script"); + + ScriptingProvider? provider = _scriptingProviders.FirstOrDefault(p => p.Id == scriptConfiguration.ScriptingProviderId); + if (provider == null) + throw new ArtemisCoreException($"Can't create script instance as there is no matching scripting provider found for the script ({scriptConfiguration.ScriptingProviderId})."); + + script = (ProfileScript) provider.Plugin.Kernel!.Get( + provider.ProfileScriptType, + CreateScriptConstructorArgument(provider.ProfileScriptType, profile), + CreateScriptConstructorArgument(provider.ProfileScriptType, scriptConfiguration) + ); + + script.ScriptingProvider = provider; + scriptConfiguration.Script = script; + provider.InternalScripts.Add(script); + lock (profile) + { + profile.AddScript(script); + } + + return script; + } + catch (Exception e) + { + // If something went wrong but the script was created, clean up as best we can + if (script != null) + { + if (profile.Scripts.Contains(script)) + profile.RemoveScript(script); + else + script.Dispose(); + } + + throw new ArtemisCoreException("Failed to initialize profile script", e); + } + } + private ConstructorArgument CreateScriptConstructorArgument(Type scriptType, object value) { // Limit to one constructor, there's no need to have more and it complicates things anyway @@ -57,9 +159,19 @@ namespace Artemis.Core.Services return new ConstructorArgument(configurationParameter.Name, value); } + private void InitializeProfileScripts(Profile profile) + { + // Initialize the scripts on the profile + foreach (ScriptConfiguration scriptConfiguration in profile.ScriptConfigurations.Where(c => c.Script == null && _scriptingProviders.Any(p => p.Id == c.ScriptingProviderId))) + CreateScriptInstance(scriptConfiguration, profile); + } + + #region Event handlers + private void PluginManagementServiceOnPluginFeatureToggled(object? sender, PluginFeatureEventArgs e) { - _scriptingProviders = _pluginManagementService.GetFeaturesOfType(); + _scriptingProviders.Clear(); + _scriptingProviders.AddRange(_pluginManagementService.GetFeaturesOfType()); foreach (ProfileConfiguration profileConfiguration in _profileService.ProfileConfigurations) { @@ -74,87 +186,6 @@ namespace Artemis.Core.Services InitializeProfileScripts(e.ProfileConfiguration.Profile); } - private void InitializeProfileScripts(Profile profile) - { - // Initialize the scripts on the profile - foreach (ScriptConfiguration scriptConfiguration in profile.ScriptConfigurations.Where(c => c.Script == null)) - CreateScriptInstance(profile, scriptConfiguration); - } - - public ReadOnlyCollection GlobalScripts { get; } - - public GlobalScript? CreateScriptInstance(ScriptConfiguration scriptConfiguration) - { - GlobalScript? script = null; - try - { - if (scriptConfiguration.Script != null) - throw new ArtemisCoreException("The provided script configuration already has an active script"); - - ScriptingProvider? provider = _scriptingProviders.FirstOrDefault(p => p.Id == scriptConfiguration.ScriptingProviderId); - if (provider == null) - return null; - - script = (GlobalScript) provider.Plugin.Kernel!.Get( - provider.GlobalScriptType, - CreateScriptConstructorArgument(provider.GlobalScriptType, scriptConfiguration) - ); - - script.ScriptingProvider = provider; - script.ScriptingService = this; - provider.InternalScripts.Add(script); - InternalGlobalScripts.Add(script); - - scriptConfiguration.Script = script; - return script; - } - catch (Exception e) - { - _logger.Warning(e, "Failed to initialize global script"); - script?.Dispose(); - return null; - } - } - - public ProfileScript? CreateScriptInstance(Profile profile, ScriptConfiguration scriptConfiguration) - { - ProfileScript? script = null; - try - { - if (scriptConfiguration.Script != null) - throw new ArtemisCoreException("The provided script configuration already has an active script"); - - ScriptingProvider? provider = _scriptingProviders.FirstOrDefault(p => p.Id == scriptConfiguration.ScriptingProviderId); - if (provider == null) - return null; - - script = (ProfileScript) provider.Plugin.Kernel!.Get( - provider.ProfileScriptType, - CreateScriptConstructorArgument(provider.ProfileScriptType, profile), - CreateScriptConstructorArgument(provider.ProfileScriptType, scriptConfiguration) - ); - - script.ScriptingProvider = provider; - provider.InternalScripts.Add(script); - lock (profile) - { - scriptConfiguration.Script = script; - profile.Scripts.Add(script); - } - - return script; - } - catch (Exception e) - { - _logger.Warning(e, "Failed to initialize profile script"); - script?.Dispose(); - return null; - } - } - - /// - public void DeleteScript(ScriptConfiguration scriptConfiguration) - { - } + #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs index 6a8fe71ef..852ad492a 100644 --- a/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/Interfaces/IWebServerService.cs @@ -102,6 +102,16 @@ namespace Artemis.Core.Services /// /// The type of Web API controller to remove void RemoveController() where T : WebApiController; + + /// + /// Adds a new EmbedIO module and restarts the web server + /// + void AddModule(PluginFeature feature, Func create); + + /// + /// Removes a EmbedIO module and restarts the web server + /// + void RemoveModule(Func create); /// /// Adds a new EmbedIO module and restarts the web server diff --git a/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs b/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs index fb7f03d13..8995e0fca 100644 --- a/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs +++ b/src/Artemis.Core/Services/WebServer/WebModuleRegistration.cs @@ -7,14 +7,28 @@ namespace Artemis.Core.Services internal class WebModuleRegistration { public PluginFeature Feature { get; } - public Type WebModuleType { get; } + public Type? WebModuleType { get; } + public Func? Create { get; } public WebModuleRegistration(PluginFeature feature, Type webModuleType) { - Feature = feature; - WebModuleType = webModuleType; + Feature = feature ?? throw new ArgumentNullException(nameof(feature)); + WebModuleType = webModuleType ?? throw new ArgumentNullException(nameof(webModuleType)); } - public IWebModule CreateInstance() => (IWebModule) Feature.Plugin.Kernel!.Get(WebModuleType); + public WebModuleRegistration(PluginFeature feature, Func create) + { + Feature = feature ?? throw new ArgumentNullException(nameof(feature)); + Create = create ?? throw new ArgumentNullException(nameof(create)); + } + + public IWebModule CreateInstance() + { + if (Create != null) + return Create(); + if (WebModuleType != null) + return (IWebModule) Feature.Plugin.Kernel!.Get(WebModuleType); + throw new ArtemisCoreException("WebModuleRegistration doesn't have a create function nor a web module type :("); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/WebServer/WebServerService.cs b/src/Artemis.Core/Services/WebServer/WebServerService.cs index 2b450ec78..ea1205726 100644 --- a/src/Artemis.Core/Services/WebServer/WebServerService.cs +++ b/src/Artemis.Core/Services/WebServer/WebServerService.cs @@ -170,6 +170,20 @@ namespace Artemis.Core.Services #region Module management + public void AddModule(PluginFeature feature, Func create) + { + if (feature == null) throw new ArgumentNullException(nameof(feature)); + + _modules.Add(new WebModuleRegistration(feature, create)); + StartWebServer(); + } + + public void RemoveModule(Func create) + { + _modules.RemoveAll(r => r.Create == create); + StartWebServer(); + } + public void AddModule(PluginFeature feature) where T : IWebModule { if (feature == null) throw new ArgumentNullException(nameof(feature)); diff --git a/src/Artemis.UI.Linux/App.axaml.cs b/src/Artemis.UI.Linux/App.axaml.cs index d5959c538..95c65051e 100644 --- a/src/Artemis.UI.Linux/App.axaml.cs +++ b/src/Artemis.UI.Linux/App.axaml.cs @@ -34,7 +34,7 @@ namespace Artemis.UI.Linux private void RegisterProviders() { IInputService inputService = _kernel.Get(); - inputService.AddInputProvider(_kernel.Get()); + // inputService.AddInputProvider(_kernel.Get()); } } } \ No newline at end of file diff --git a/src/Artemis.UI.Linux/packages.lock.json b/src/Artemis.UI.Linux/packages.lock.json index 3bd0f1c72..3c5c81776 100644 --- a/src/Artemis.UI.Linux/packages.lock.json +++ b/src/Artemis.UI.Linux/packages.lock.json @@ -206,8 +206,8 @@ }, "FluentAvaloniaUI": { "type": "Transitive", - "resolved": "1.4.0", - "contentHash": "K0dwenW6dbRFSnJmJAqIVWVlGIakmodgMxXWyj0gm1MoLJkuMJ5vMU/skw5X7xJJDpr88mcB4FkMPjEIp1vk9A==", + "resolved": "1.4.1", + "contentHash": "2m9e3YuCNa0a7EBHA9HXVq5EeA5/xtNKIJU4utMhUKHHCUgxKnBWffHUbCKzPGhhsVrVnK4Uwb/WyI8nQCHEZw==", "dependencies": { "Avalonia": "0.10.15", "Avalonia.Controls.DataGrid": "0.10.15", @@ -1718,7 +1718,7 @@ "Avalonia.ReactiveUI": "0.10.15", "Avalonia.Xaml.Behaviors": "0.10.14", "DynamicData": "7.8.6", - "FluentAvaloniaUI": "1.4.0", + "FluentAvaloniaUI": "1.4.1", "Flurl.Http": "3.2.4", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", @@ -1739,7 +1739,7 @@ "Avalonia.ReactiveUI": "0.10.15", "Avalonia.Xaml.Behaviors": "0.10.14", "DynamicData": "7.8.6", - "FluentAvaloniaUI": "1.4.0", + "FluentAvaloniaUI": "1.4.1", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease.32", "ReactiveUI": "17.1.50", diff --git a/src/Artemis.UI.MacOS/packages.lock.json b/src/Artemis.UI.MacOS/packages.lock.json index 3bd0f1c72..3c5c81776 100644 --- a/src/Artemis.UI.MacOS/packages.lock.json +++ b/src/Artemis.UI.MacOS/packages.lock.json @@ -206,8 +206,8 @@ }, "FluentAvaloniaUI": { "type": "Transitive", - "resolved": "1.4.0", - "contentHash": "K0dwenW6dbRFSnJmJAqIVWVlGIakmodgMxXWyj0gm1MoLJkuMJ5vMU/skw5X7xJJDpr88mcB4FkMPjEIp1vk9A==", + "resolved": "1.4.1", + "contentHash": "2m9e3YuCNa0a7EBHA9HXVq5EeA5/xtNKIJU4utMhUKHHCUgxKnBWffHUbCKzPGhhsVrVnK4Uwb/WyI8nQCHEZw==", "dependencies": { "Avalonia": "0.10.15", "Avalonia.Controls.DataGrid": "0.10.15", @@ -1718,7 +1718,7 @@ "Avalonia.ReactiveUI": "0.10.15", "Avalonia.Xaml.Behaviors": "0.10.14", "DynamicData": "7.8.6", - "FluentAvaloniaUI": "1.4.0", + "FluentAvaloniaUI": "1.4.1", "Flurl.Http": "3.2.4", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", @@ -1739,7 +1739,7 @@ "Avalonia.ReactiveUI": "0.10.15", "Avalonia.Xaml.Behaviors": "0.10.14", "DynamicData": "7.8.6", - "FluentAvaloniaUI": "1.4.0", + "FluentAvaloniaUI": "1.4.1", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease.32", "ReactiveUI": "17.1.50", diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj index 462420cc5..a034b7833 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/Artemis.UI.Shared/Services/Builders/OpenFileDialogBuilder.cs b/src/Artemis.UI.Shared/Services/Builders/OpenFileDialogBuilder.cs index 02f3f47ec..1980804eb 100644 --- a/src/Artemis.UI.Shared/Services/Builders/OpenFileDialogBuilder.cs +++ b/src/Artemis.UI.Shared/Services/Builders/OpenFileDialogBuilder.cs @@ -17,7 +17,7 @@ namespace Artemis.UI.Shared.Services.Builders /// Creates a new instance of the class. /// /// The parent window that will host the dialog. - public OpenFileDialogBuilder(Window parent) + internal OpenFileDialogBuilder(Window parent) { _parent = parent; _openFileDialog = new OpenFileDialog(); diff --git a/src/Artemis.UI.Shared/Services/Builders/SaveFileDialogBuilder.cs b/src/Artemis.UI.Shared/Services/Builders/SaveFileDialogBuilder.cs index 84021ab6c..ed75a225f 100644 --- a/src/Artemis.UI.Shared/Services/Builders/SaveFileDialogBuilder.cs +++ b/src/Artemis.UI.Shared/Services/Builders/SaveFileDialogBuilder.cs @@ -17,7 +17,7 @@ namespace Artemis.UI.Shared.Services.Builders /// Creates a new instance of the class. /// /// The parent window that will host the notification. - public SaveFileDialogBuilder(Window parent) + internal SaveFileDialogBuilder(Window parent) { _parent = parent; _saveFileDialog = new SaveFileDialog(); diff --git a/src/Artemis.UI.Shared/packages.lock.json b/src/Artemis.UI.Shared/packages.lock.json index d3f713f0c..626fcba1d 100644 --- a/src/Artemis.UI.Shared/packages.lock.json +++ b/src/Artemis.UI.Shared/packages.lock.json @@ -62,9 +62,9 @@ }, "FluentAvaloniaUI": { "type": "Direct", - "requested": "[1.4.0, )", - "resolved": "1.4.0", - "contentHash": "K0dwenW6dbRFSnJmJAqIVWVlGIakmodgMxXWyj0gm1MoLJkuMJ5vMU/skw5X7xJJDpr88mcB4FkMPjEIp1vk9A==", + "requested": "[1.4.1, )", + "resolved": "1.4.1", + "contentHash": "2m9e3YuCNa0a7EBHA9HXVq5EeA5/xtNKIJU4utMhUKHHCUgxKnBWffHUbCKzPGhhsVrVnK4Uwb/WyI8nQCHEZw==", "dependencies": { "Avalonia": "0.10.15", "Avalonia.Controls.DataGrid": "0.10.15", diff --git a/src/Artemis.UI.Windows/packages.lock.json b/src/Artemis.UI.Windows/packages.lock.json index ba39536a8..bdbc2da6d 100644 --- a/src/Artemis.UI.Windows/packages.lock.json +++ b/src/Artemis.UI.Windows/packages.lock.json @@ -232,8 +232,8 @@ }, "FluentAvaloniaUI": { "type": "Transitive", - "resolved": "1.4.0", - "contentHash": "K0dwenW6dbRFSnJmJAqIVWVlGIakmodgMxXWyj0gm1MoLJkuMJ5vMU/skw5X7xJJDpr88mcB4FkMPjEIp1vk9A==", + "resolved": "1.4.1", + "contentHash": "2m9e3YuCNa0a7EBHA9HXVq5EeA5/xtNKIJU4utMhUKHHCUgxKnBWffHUbCKzPGhhsVrVnK4Uwb/WyI8nQCHEZw==", "dependencies": { "Avalonia": "0.10.15", "Avalonia.Controls.DataGrid": "0.10.15", @@ -1753,7 +1753,7 @@ "Avalonia.ReactiveUI": "0.10.15", "Avalonia.Xaml.Behaviors": "0.10.14", "DynamicData": "7.8.6", - "FluentAvaloniaUI": "1.4.0", + "FluentAvaloniaUI": "1.4.1", "Flurl.Http": "3.2.4", "Live.Avalonia": "1.3.1", "Material.Icons.Avalonia": "1.0.2", @@ -1774,7 +1774,7 @@ "Avalonia.ReactiveUI": "0.10.15", "Avalonia.Xaml.Behaviors": "0.10.14", "DynamicData": "7.8.6", - "FluentAvaloniaUI": "1.4.0", + "FluentAvaloniaUI": "1.4.1", "Material.Icons.Avalonia": "1.0.2", "RGB.NET.Core": "1.0.0-prerelease.32", "ReactiveUI": "17.1.50", diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index e0d731829..5e1530038 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -23,7 +23,7 @@ - + @@ -71,6 +71,10 @@ SidebarCategoryEditView.axaml Code + + ScriptConfigurationEditView.axaml + Code + diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index bea557acc..0303478c0 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -2,6 +2,7 @@ using Artemis.Core; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; +using Artemis.Core.ScriptingProviders; using Artemis.UI.Screens.Device; using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.ProfileEditor; @@ -13,6 +14,8 @@ using Artemis.UI.Screens.ProfileEditor.Properties.DataBinding; using Artemis.UI.Screens.ProfileEditor.Properties.Timeline; using Artemis.UI.Screens.ProfileEditor.Properties.Tree; using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers; +using Artemis.UI.Screens.Scripting; +using Artemis.UI.Screens.Scripting.Dialogs; using Artemis.UI.Screens.Settings; using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.SurfaceEditor; @@ -122,4 +125,10 @@ public interface ILayerHintVmFactory : IVmFactory CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(Layer layer, CategoryAdaptionHint adaptionHint); DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(Layer layer, DeviceAdaptionHint adaptionHint); KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(Layer layer, KeyboardSectionAdaptionHint adaptionHint); +} + +public interface IScriptVmFactory : IVmFactory +{ + ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration); + ScriptConfigurationViewModel ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration); } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml new file mode 100644 index 000000000..305c68324 --- /dev/null +++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + In order for this plugin to work the prerequisites on the left must be met. Clicking install will automatically set everything up for you. + + + + + Installing + + failed. + + You may try again to see if that helps, otherwise install the prerequisite manually or contact the plugin developer. + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml.cs b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml.cs new file mode 100644 index 000000000..24d0b194c --- /dev/null +++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Plugins; + +public partial class PluginPrerequisitesInstallDialogView : ReactiveUserControl +{ + public PluginPrerequisitesInstallDialogView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs index 0d404ba1b..615d65c87 100644 --- a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using System.Reactive.Disposables; using System.Threading; using System.Threading.Tasks; @@ -9,11 +10,14 @@ using Artemis.Core; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; using ReactiveUI; +using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton; namespace Artemis.UI.Screens.Plugins { - public class PluginPrerequisitesInstallDialogViewModel : DialogViewModelBase + public class PluginPrerequisitesInstallDialogViewModel : ContentDialogViewModelBase { private PluginPrerequisiteViewModel? _activePrerequisite; private bool _canInstall; @@ -26,22 +30,23 @@ namespace Artemis.UI.Screens.Plugins public PluginPrerequisitesInstallDialogViewModel(List subjects, IPrerequisitesVmFactory prerequisitesVmFactory) { Prerequisites = new ObservableCollection(); - foreach (PluginPrerequisite prerequisite in subjects.SelectMany(prerequisitesSubject => prerequisitesSubject.Prerequisites)) + foreach (PluginPrerequisite prerequisite in subjects.SelectMany(prerequisitesSubject => prerequisitesSubject.PlatformPrerequisites)) Prerequisites.Add(prerequisitesVmFactory.PluginPrerequisiteViewModel(prerequisite, false)); + Install = ReactiveCommand.CreateFromTask(ExecuteInstall, this.WhenAnyValue(vm => vm.CanInstall)); - CanInstall = false; - Task.Run(() => CanInstall = Prerequisites.Any(p => !p.PluginPrerequisite.IsMet())); - + Dispatcher.UIThread.Post(() => CanInstall = Prerequisites.Any(p => !p.PluginPrerequisite.IsMet()), DispatcherPriority.Background); this.WhenActivated(d => { Disposable.Create(() => { _tokenSource?.Cancel(); _tokenSource?.Dispose(); + _tokenSource = null; }).DisposeWith(d); }); } + public ReactiveCommand Install { get; } public ObservableCollection Prerequisites { get; } public PluginPrerequisiteViewModel? ActivePrerequisite @@ -80,13 +85,18 @@ namespace Artemis.UI.Screens.Plugins set => RaiseAndSetIfChanged(ref _canInstall, value); } - public async Task Install() + private async Task ExecuteInstall() { + ContentDialogClosingDeferral? deferral = null; + if (ContentDialog != null) + ContentDialog.Closing += (_, args) => deferral = args.GetDeferral(); + CanInstall = false; ShowFailed = false; ShowIntro = false; ShowProgress = true; + _tokenSource?.Dispose(); _tokenSource = new CancellationTokenSource(); try @@ -110,30 +120,31 @@ namespace Artemis.UI.Screens.Plugins // Wait after the task finished for the user to process what happened if (pluginPrerequisiteViewModel != Prerequisites.Last()) + await Task.Delay(250); + else await Task.Delay(1000); } - ShowInstall = false; + if (deferral != null) + deferral.Complete(); + else + ContentDialog?.Hide(ContentDialogResult.Primary); } catch (OperationCanceledException) { // ignored } - finally - { - _tokenSource.Dispose(); - _tokenSource = null; - } } - public void Accept() + public static async Task Show(IWindowService windowService, List subjects) { - Close(true); - } - - public static async Task Show(IWindowService windowService, List subjects) - { - return await windowService.ShowDialogAsync(("subjects", subjects)); + await windowService.CreateContentDialog() + .WithTitle("Plugin prerequisites") + .WithViewModel(out PluginPrerequisitesInstallDialogViewModel vm, ("subjects", subjects)) + .WithCloseButtonText("Cancel") + .HavingPrimaryButton(b => b.WithText("Install").WithCommand(vm.Install)) + .WithDefaultButton(ContentDialogButton.Primary) + .ShowAsync(); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml new file mode 100644 index 000000000..0cde44af7 --- /dev/null +++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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. + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml.cs b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml.cs new file mode 100644 index 000000000..f8df8dde4 --- /dev/null +++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Plugins; + +public partial class PluginPrerequisitesUninstallDialogView : ReactiveUserControl +{ + public PluginPrerequisitesUninstallDialogView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs index 43ee77a74..baaf5e649 100644 --- a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; +using System.Reactive; using System.Reactive.Disposables; using System.Threading; using System.Threading.Tasks; @@ -10,34 +11,38 @@ using Artemis.Core.Services; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; +using Avalonia.Threading; +using FluentAvalonia.UI.Controls; using ReactiveUI; +using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton; namespace Artemis.UI.Screens.Plugins { - public class PluginPrerequisitesUninstallDialogViewModel : DialogViewModelBase + public class PluginPrerequisitesUninstallDialogViewModel : ContentDialogViewModelBase { private readonly IPluginManagementService _pluginManagementService; private readonly List _subjects; private readonly IWindowService _windowService; private PluginPrerequisiteViewModel? _activePrerequisite; private bool _canUninstall; - private bool _isFinished; private CancellationTokenSource? _tokenSource; - public PluginPrerequisitesUninstallDialogViewModel(List subjects, string cancelLabel, IPrerequisitesVmFactory prerequisitesVmFactory, IWindowService windowService, + public PluginPrerequisitesUninstallDialogViewModel(List subjects, + IPrerequisitesVmFactory prerequisitesVmFactory, + IWindowService windowService, IPluginManagementService pluginManagementService) { _subjects = subjects; _windowService = windowService; _pluginManagementService = pluginManagementService; - CancelLabel = cancelLabel; Prerequisites = new ObservableCollection(); - foreach (PluginPrerequisite prerequisite in subjects.SelectMany(prerequisitesSubject => prerequisitesSubject.Prerequisites)) + foreach (PluginPrerequisite prerequisite in subjects.SelectMany(prerequisitesSubject => prerequisitesSubject.PlatformPrerequisites)) Prerequisites.Add(prerequisitesVmFactory.PluginPrerequisiteViewModel(prerequisite, true)); + Uninstall = ReactiveCommand.CreateFromTask(ExecuteUninstall, this.WhenAnyValue(vm => vm.CanUninstall)); // Could be slow so take it off of the UI thread - Task.Run(() => CanUninstall = Prerequisites.Any(p => p.PluginPrerequisite.IsMet())); + Dispatcher.UIThread.Post(() => CanUninstall = Prerequisites.Any(p => p.PluginPrerequisite.IsMet()), DispatcherPriority.Background); this.WhenActivated(d => { @@ -45,11 +50,12 @@ namespace Artemis.UI.Screens.Plugins { _tokenSource?.Cancel(); _tokenSource?.Dispose(); + _tokenSource = null; }).DisposeWith(d); }); } - public string CancelLabel { get; } + public ReactiveCommand Uninstall { get; } public ObservableCollection Prerequisites { get; } public PluginPrerequisiteViewModel? ActivePrerequisite @@ -64,20 +70,19 @@ namespace Artemis.UI.Screens.Plugins set => RaiseAndSetIfChanged(ref _canUninstall, value); } - public bool IsFinished + private async Task ExecuteUninstall() { - get => _isFinished; - set => RaiseAndSetIfChanged(ref _isFinished, value); - } + ContentDialogClosingDeferral? deferral = null; + if (ContentDialog != null) + ContentDialog.Closing += (_, args) => deferral = args.GetDeferral(); - public async Task Uninstall() - { CanUninstall = false; // Disable all subjects that are plugins, this will disable their features too foreach (IPrerequisitesSubject prerequisitesSubject in _subjects) { - if (prerequisitesSubject is PluginInfo pluginInfo) _pluginManagementService.DisablePlugin(pluginInfo.Plugin, true); + if (prerequisitesSubject is PluginInfo pluginInfo) + _pluginManagementService.DisablePlugin(pluginInfo.Plugin, true); } // Disable all subjects that are features if still required @@ -88,9 +93,11 @@ namespace Artemis.UI.Screens.Plugins // Disable the parent plugin if the feature is AlwaysEnabled if (featureInfo.AlwaysEnabled) _pluginManagementService.DisablePlugin(featureInfo.Plugin, true); - else if (featureInfo.Instance != null) _pluginManagementService.DisablePluginFeature(featureInfo.Instance, true); + else if (featureInfo.Instance != null) + _pluginManagementService.DisablePluginFeature(featureInfo.Instance, true); } + _tokenSource?.Dispose(); _tokenSource = new CancellationTokenSource(); try @@ -98,29 +105,34 @@ namespace Artemis.UI.Screens.Plugins foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites) { pluginPrerequisiteViewModel.IsMet = pluginPrerequisiteViewModel.PluginPrerequisite.IsMet(); - if (!pluginPrerequisiteViewModel.IsMet) continue; + if (!pluginPrerequisiteViewModel.IsMet) + continue; ActivePrerequisite = pluginPrerequisiteViewModel; await ActivePrerequisite.Uninstall(_tokenSource.Token); // Wait after the task finished for the user to process what happened - if (pluginPrerequisiteViewModel != Prerequisites.Last()) await Task.Delay(1000); - } - - if (Prerequisites.All(p => !p.IsMet)) - { - IsFinished = true; - return; + if (pluginPrerequisiteViewModel != Prerequisites.Last()) + await Task.Delay(250); + else + await Task.Delay(1000); } + if (deferral != null) + deferral.Complete(); + else + ContentDialog?.Hide(ContentDialogResult.Primary); + // This shouldn't be happening and the experience isn't very nice for the user (too lazy to make a nice UI for such an edge case) // but at least give some feedback - Close(false); - await _windowService.CreateContentDialog() - .WithTitle("Plugin prerequisites") - .WithContent("The plugin was not able to fully remove all prerequisites. \r\nPlease try again or contact the plugin creator.") - .ShowAsync(); - await Show(_windowService, _subjects); + if (Prerequisites.Any(p => p.IsMet)) + { + await _windowService.CreateContentDialog() + .WithTitle("Plugin prerequisites") + .WithContent("The plugin was not able to fully remove all prerequisites. \r\nPlease try again or contact the plugin creator.") + .ShowAsync(); + await Show(_windowService, _subjects); + } } catch (OperationCanceledException) { @@ -129,19 +141,18 @@ namespace Artemis.UI.Screens.Plugins finally { CanUninstall = true; - _tokenSource.Dispose(); - _tokenSource = null; } } - public void Accept() + public static async Task Show(IWindowService windowService, List subjects, string cancelLabel = "Cancel") { - Close(true); - } - - public static async Task Show(IWindowService windowService, List subjects, string cancelLabel = "Cancel") - { - return await windowService.ShowDialogAsync(("subjects", subjects), ("cancelLabel", cancelLabel)); + await windowService.CreateContentDialog() + .WithTitle("Plugin prerequisites") + .WithViewModel(out PluginPrerequisitesUninstallDialogViewModel vm, ("subjects", subjects)) + .WithCloseButtonText(cancelLabel) + .HavingPrimaryButton(b => b.WithText("Uninstall").WithCommand(vm.Uninstall)) + .WithDefaultButton(ContentDialogButton.Primary) + .ShowAsync(); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs index 4708b2ef9..9b610ca5b 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginFeatureViewModel.cs @@ -76,8 +76,8 @@ namespace Artemis.UI.Screens.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 CanInstallPrerequisites => FeatureInfo.PlatformPrerequisites.Any(); + public bool CanRemovePrerequisites => FeatureInfo.PlatformPrerequisites.Any(p => p.UninstallActions.Any()); public bool IsPopupEnabled => CanInstallPrerequisites || CanRemovePrerequisites; public void ShowLogsFolder() @@ -100,13 +100,13 @@ namespace Artemis.UI.Screens.Plugins public async Task InstallPrerequisites() { - if (FeatureInfo.Prerequisites.Any()) + if (FeatureInfo.PlatformPrerequisites.Any()) await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, new List {FeatureInfo}); } public async Task RemovePrerequisites() { - if (FeatureInfo.Prerequisites.Any(p => p.UninstallActions.Any())) + if (FeatureInfo.PlatformPrerequisites.Any(p => p.UninstallActions.Any())) { await PluginPrerequisitesUninstallDialogViewModel.Show(_windowService, new List {FeatureInfo}); this.RaisePropertyChanged(nameof(IsEnabled)); diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionView.axaml b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionView.axaml new file mode 100644 index 000000000..0cc1745d7 --- /dev/null +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionView.axaml @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionView.axaml.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionView.axaml.cs new file mode 100644 index 000000000..994dd4cf3 --- /dev/null +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteActionView.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.Plugins; + +public partial class PluginPrerequisiteActionView : UserControl +{ + public PluginPrerequisiteActionView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.axaml b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.axaml new file mode 100644 index 000000000..54996af62 --- /dev/null +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.axaml.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.axaml.cs new file mode 100644 index 000000000..f52a5e96a --- /dev/null +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Plugins; + +public partial class PluginPrerequisiteView : ReactiveUserControl +{ + public PluginPrerequisiteView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs index 7fc226fc8..ff3034746 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs @@ -75,7 +75,6 @@ namespace Artemis.UI.Screens.Plugins public bool Busy => _busy.Value; public int ActiveStepNumber => _activeStepNumber.Value; - public bool HasMultipleActions => Actions.Count > 1; public async Task Install(CancellationToken cancellationToken) { diff --git a/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml b/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml index 5b9887ec1..e9111a69c 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml +++ b/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml @@ -5,12 +5,14 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" + xmlns:plugins="clr-namespace:Artemis.UI.Screens.Plugins" mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Plugins.PluginSettingsView"> + x:Class="Artemis.UI.Screens.Plugins.PluginSettingsView" + x:DataType="plugins:PluginSettingsViewModel"> - - + + Text="{CompiledBinding Plugin.Info.Author}" + IsVisible="{CompiledBinding Plugin.Info.Author, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" /> + Text="{CompiledBinding Plugin.Info.Description}" /> - + - - + + - + - + - + - + - + @@ -74,14 +76,14 @@ + IsVisible="{CompiledBinding Plugin.Info.Website, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" + NavigateUri="{CompiledBinding Plugin.Info.Website}"> + IsVisible="{CompiledBinding Plugin.Info.Repository, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" + NavigateUri="{CompiledBinding Plugin.Info.Repository}"> @@ -89,28 +91,28 @@ + IsVisible="{CompiledBinding !Enabling}" + IsChecked="{CompiledBinding IsEnabled}"> Plugin enabled + IsVisible="{CompiledBinding Plugin.Info.RequiresAdmin}" /> Plugin features - + diff --git a/src/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs index f9fdaead6..7dce0dc36 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs @@ -49,13 +49,16 @@ namespace Artemis.UI.Screens.Plugins PluginFeatures = new ObservableCollection(); foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features) PluginFeatures.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false)); - - + Reload = ReactiveCommand.CreateFromTask(ExecuteReload); OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings, this.WhenAnyValue(x => x.IsEnabled).Select(isEnabled => isEnabled && Plugin.ConfigurationDialog != null)); + RemoveSettings = ReactiveCommand.CreateFromTask(ExecuteRemoveSettings); + Remove = ReactiveCommand.CreateFromTask(ExecuteRemove); InstallPrerequisites = ReactiveCommand.CreateFromTask(ExecuteInstallPrerequisites, this.WhenAnyValue(x => x.CanInstallPrerequisites)); RemovePrerequisites = ReactiveCommand.CreateFromTask(ExecuteRemovePrerequisites, this.WhenAnyValue(x => x.CanRemovePrerequisites)); - + ShowLogsFolder = ReactiveCommand.Create(ExecuteShowLogsFolder); + OpenPluginDirectory = ReactiveCommand.Create(ExecuteOpenPluginDirectory); + this.WhenActivated(d => { _pluginManagementService.PluginDisabled += PluginManagementServiceOnPluginToggled; @@ -69,10 +72,15 @@ namespace Artemis.UI.Screens.Plugins }); } + public ReactiveCommand Reload { get; } public ReactiveCommand OpenSettings { get; } + public ReactiveCommand RemoveSettings { get; } + public ReactiveCommand Remove { get;} public ReactiveCommand InstallPrerequisites { get; } public ReactiveCommand RemovePrerequisites { get; } - + public ReactiveCommand ShowLogsFolder { get; } + public ReactiveCommand OpenPluginDirectory { get; } + public ObservableCollection PluginFeatures { get; } public Plugin Plugin @@ -137,7 +145,7 @@ namespace Artemis.UI.Screens.Plugins } } - public void OpenPluginDirectory() + private void ExecuteOpenPluginDirectory() { try { @@ -149,7 +157,7 @@ namespace Artemis.UI.Screens.Plugins } } - public async Task Reload() + private async Task ExecuteReload() { bool wasEnabled = IsEnabled; @@ -167,28 +175,28 @@ namespace Artemis.UI.Screens.Plugins _notificationService.CreateNotification().WithTitle("Reloaded plugin.").Show(); } - public async Task ExecuteInstallPrerequisites() + private async Task ExecuteInstallPrerequisites() { List subjects = new() {Plugin.Info}; subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled)); - if (subjects.Any(s => s.Prerequisites.Any())) + if (subjects.Any(s => s.PlatformPrerequisites.Any())) await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); } - public async Task ExecuteRemovePrerequisites(bool forPluginRemoval = false) + private async Task ExecuteRemovePrerequisites(bool forPluginRemoval = false) { List subjects = new() {Plugin.Info}; subjects.AddRange(!forPluginRemoval ? Plugin.Features.Where(f => f.AlwaysEnabled) : Plugin.Features); - if (subjects.Any(s => s.Prerequisites.Any(p => p.UninstallActions.Any()))) + if (subjects.Any(s => s.PlatformPrerequisites.Any(p => p.UninstallActions.Any()))) { await PluginPrerequisitesUninstallDialogViewModel.Show(_windowService, subjects, forPluginRemoval ? "Skip, remove plugin" : "Cancel"); this.RaisePropertyChanged(nameof(IsEnabled)); } } - public async Task RemoveSettings() + private async Task ExecuteRemoveSettings() { bool confirmed = await _windowService.ShowConfirmContentDialog("Clear plugin settings", "Are you sure you want to clear the settings of this plugin?"); if (!confirmed) @@ -207,7 +215,7 @@ namespace Artemis.UI.Screens.Plugins _notificationService.CreateNotification().WithTitle("Cleared plugin settings.").Show(); } - public async Task Remove() + private async Task ExecuteRemove() { bool confirmed = await _windowService.ShowConfirmContentDialog("Remove plugin", "Are you sure you want to remove this plugin?"); if (!confirmed) @@ -216,7 +224,7 @@ namespace Artemis.UI.Screens.Plugins // If the plugin or any of its features has uninstall actions, offer to run these List subjects = new() {Plugin.Info}; subjects.AddRange(Plugin.Features); - if (subjects.Any(s => s.Prerequisites.Any(p => p.UninstallActions.Any()))) + if (subjects.Any(s => s.PlatformPrerequisites.Any(p => p.UninstallActions.Any()))) await ExecuteRemovePrerequisites(true); try @@ -232,7 +240,7 @@ namespace Artemis.UI.Screens.Plugins _notificationService.CreateNotification().WithTitle("Removed plugin.").Show(); } - public void ShowLogsFolder() + private void ExecuteShowLogsFolder() { try { @@ -244,11 +252,6 @@ namespace Artemis.UI.Screens.Plugins } } - public void OpenUri(Uri uri) - { - Utilities.OpenUrl(uri.ToString()); - } - private void PluginManagementServiceOnPluginToggled(object? sender, PluginEventArgs e) { this.RaisePropertyChanged(nameof(IsEnabled)); @@ -300,7 +303,7 @@ namespace Artemis.UI.Screens.Plugins { await Dispatcher.UIThread.InvokeAsync(() => _notificationService.CreateNotification() .WithMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}") - .HavingButton(b => b.WithText("View logs").WithAction(ShowLogsFolder)) + .HavingButton(b => b.WithText("View logs").WithCommand(ShowLogsFolder)) .Show()); } finally @@ -325,10 +328,10 @@ namespace Artemis.UI.Screens.Plugins private void CheckPrerequisites() { - CanInstallPrerequisites = Plugin.Info.Prerequisites.Any() || - Plugin.Features.Where(f => f.AlwaysEnabled).Any(f => f.Prerequisites.Any()); - CanRemovePrerequisites = Plugin.Info.Prerequisites.Any(p => p.UninstallActions.Any()) || - Plugin.Features.Where(f => f.AlwaysEnabled).Any(f => f.Prerequisites.Any(p => p.UninstallActions.Any())); + CanInstallPrerequisites = Plugin.Info.PlatformPrerequisites.Any() || + Plugin.Features.Where(f => f.AlwaysEnabled).Any(f => f.PlatformPrerequisites.Any()); + CanRemovePrerequisites = Plugin.Info.PlatformPrerequisites.Any(p => p.UninstallActions.Any()) || + Plugin.Features.Where(f => f.AlwaysEnabled).Any(f => f.PlatformPrerequisites.Any(p => p.UninstallActions.Any())); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml index 88f7a88b8..565fa7914 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarView.axaml @@ -30,7 +30,7 @@ - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index f22d28ad0..dc5b5767d 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -8,6 +8,7 @@ using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; +using Artemis.UI.Screens.Scripting; using Artemis.UI.Screens.Sidebar; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; @@ -54,6 +55,7 @@ public class MenuBarViewModel : ActivatableViewModelBase AddFolder = ReactiveCommand.Create(ExecuteAddFolder); AddLayer = ReactiveCommand.Create(ExecuteAddLayer); ViewProperties = ReactiveCommand.CreateFromTask(ExecuteViewProperties, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null)); + ViewScripts = ReactiveCommand.CreateFromTask(ExecuteViewScripts, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null)); AdaptProfile = ReactiveCommand.CreateFromTask(ExecuteAdaptProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null)); ToggleSuspended = ReactiveCommand.Create(ExecuteToggleSuspended, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null)); DeleteProfile = ReactiveCommand.CreateFromTask(ExecuteDeleteProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null)); @@ -68,6 +70,7 @@ public class MenuBarViewModel : ActivatableViewModelBase public ReactiveCommand AddLayer { get; } public ReactiveCommand ToggleSuspended { get; } public ReactiveCommand ViewProperties { get; } + public ReactiveCommand ViewScripts { get; } public ReactiveCommand AdaptProfile { get; } public ReactiveCommand DeleteProfile { get; } public ReactiveCommand ExportProfile { get; } @@ -122,6 +125,14 @@ public class MenuBarViewModel : ActivatableViewModelBase ("profileConfiguration", ProfileConfiguration) ); } + + private async Task ExecuteViewScripts() + { + if (ProfileConfiguration?.Profile == null) + return; + + await _windowService.ShowDialogAsync(("profile", ProfileConfiguration.Profile)); + } private async Task ExecuteAdaptProfile() { diff --git a/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationCreateView.axaml b/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationCreateView.axaml new file mode 100644 index 000000000..6babb6b10 --- /dev/null +++ b/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationCreateView.axaml @@ -0,0 +1,35 @@ + + + + Script name + + Script type + + + + + + + + + + + + + + + + + + You don't have any scripting providers installed or enabled, therefore you cannot use scripts. + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationCreateView.axaml.cs b/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationCreateView.axaml.cs new file mode 100644 index 000000000..3576f1b1a --- /dev/null +++ b/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationCreateView.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Scripting.Dialogs; + +public partial class ScriptConfigurationCreateView : ReactiveUserControl +{ + public ScriptConfigurationCreateView() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationCreateViewModel.cs b/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationCreateViewModel.cs new file mode 100644 index 000000000..92d7c7c31 --- /dev/null +++ b/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationCreateViewModel.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reactive; +using Artemis.Core.ScriptingProviders; +using Artemis.Core.Services; +using Artemis.UI.Shared; +using FluentAvalonia.UI.Controls; +using ReactiveUI; +using ReactiveUI.Validation.Extensions; + +namespace Artemis.UI.Screens.Scripting.Dialogs; + +public class ScriptConfigurationCreateViewModel : ContentDialogViewModelBase +{ + private string? _scriptName; + private ScriptingProvider _selectedScriptingProvider; + + public ScriptConfigurationCreateViewModel(IScriptingService scriptingService) + { + ScriptingProviders = new List(scriptingService.ScriptingProviders); + Submit = ReactiveCommand.Create(ExecuteSubmit, ValidationContext.Valid); + _selectedScriptingProvider = ScriptingProviders.First(); + + this.ValidationRule(vm => vm.ScriptName, s => !string.IsNullOrWhiteSpace(s), "Script name cannot be empty."); + } + + public ScriptConfiguration? ScriptConfiguration { get; private set; } + public List ScriptingProviders { get; } + + public string? ScriptName + { + get => _scriptName; + set => RaiseAndSetIfChanged(ref _scriptName, value); + } + + public ScriptingProvider SelectedScriptingProvider + { + get => _selectedScriptingProvider; + set => RaiseAndSetIfChanged(ref _selectedScriptingProvider, value); + } + + public ReactiveCommand Submit { get; } + + private void ExecuteSubmit() + { + if (ScriptName == null) + return; + + ScriptConfiguration = new ScriptConfiguration(SelectedScriptingProvider, ScriptName, ScriptType.Profile); + ContentDialog?.Hide(ContentDialogResult.Primary); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationEditView.axaml b/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationEditView.axaml new file mode 100644 index 000000000..989b6483d --- /dev/null +++ b/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationEditView.axaml @@ -0,0 +1,11 @@ + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationEditView.axaml.cs b/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationEditView.axaml.cs new file mode 100644 index 000000000..00427d53d --- /dev/null +++ b/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationEditView.axaml.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using ReactiveUI; + +namespace Artemis.UI.Screens.Scripting.Dialogs; + +public partial class ScriptConfigurationEditView : ReactiveUserControl +{ + public ScriptConfigurationEditView() + { + InitializeComponent(); + this.WhenActivated(_ => + { + this.Get("Input").Focus(); + this.Get("Input").SelectAll(); + }); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationEditViewModel.cs b/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationEditViewModel.cs new file mode 100644 index 000000000..2b4cb9e74 --- /dev/null +++ b/src/Artemis.UI/Screens/Scripting/Dialogs/ScriptConfigurationEditViewModel.cs @@ -0,0 +1,40 @@ +using System.Reactive; +using Artemis.Core.ScriptingProviders; +using Artemis.UI.Shared; +using FluentAvalonia.UI.Controls; +using ReactiveUI; +using ReactiveUI.Validation.Extensions; + +namespace Artemis.UI.Screens.Scripting.Dialogs; + +public class ScriptConfigurationEditViewModel : ContentDialogViewModelBase +{ + private string? _scriptName; + + public ScriptConfigurationEditViewModel(ScriptConfiguration scriptConfiguration) + { + ScriptConfiguration = scriptConfiguration; + Submit = ReactiveCommand.Create(ExecuteSubmit, ValidationContext.Valid); + ScriptName = ScriptConfiguration.Name; + + this.ValidationRule(vm => vm.ScriptName, s => !string.IsNullOrWhiteSpace(s), "Script name cannot be empty."); + } + + public ScriptConfiguration ScriptConfiguration { get; } + public ReactiveCommand Submit { get; } + + public string? ScriptName + { + get => _scriptName; + set => RaiseAndSetIfChanged(ref _scriptName, value); + } + + private void ExecuteSubmit() + { + if (ScriptName == null) + return; + + ScriptConfiguration.Name = ScriptName; + ContentDialog?.Hide(ContentDialogResult.Primary); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Scripting/ScriptConfigurationViewModel.cs b/src/Artemis.UI/Screens/Scripting/ScriptConfigurationViewModel.cs new file mode 100644 index 000000000..02ea67370 --- /dev/null +++ b/src/Artemis.UI/Screens/Scripting/ScriptConfigurationViewModel.cs @@ -0,0 +1,63 @@ +using System.Reactive; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.ScriptingProviders; +using Artemis.Core.Services; +using Artemis.UI.Screens.Scripting.Dialogs; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using FluentAvalonia.UI.Controls; +using ReactiveUI; +using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton; + +namespace Artemis.UI.Screens.Scripting; + +public class ScriptConfigurationViewModel : ActivatableViewModelBase +{ + private readonly IScriptingService _scriptingService; + private readonly IWindowService _windowService; + + public ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration, IScriptingService scriptingService, IWindowService windowService) + { + _scriptingService = scriptingService; + _windowService = windowService; + + ScriptConfiguration = scriptConfiguration; + Script = ScriptConfiguration.Script; + EditScriptConfiguration = ReactiveCommand.CreateFromTask(ExecuteEditScriptConfiguration); + ToggleSuspended = ReactiveCommand.Create(() => ScriptConfiguration.IsSuspended = !ScriptConfiguration.IsSuspended); + } + + public ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration, IScriptingService scriptingService, IWindowService windowService) + : this(scriptConfiguration, scriptingService, windowService) + { + Profile = profile; + } + + public Profile? Profile { get; } + public ScriptConfiguration ScriptConfiguration { get; } + public Script? Script { get; } + public ReactiveCommand EditScriptConfiguration { get; } + public ReactiveCommand ToggleSuspended { get; } + + private async Task ExecuteEditScriptConfiguration(ScriptConfiguration scriptConfiguration) + { + ContentDialogResult contentDialogResult = await _windowService.CreateContentDialog() + .WithTitle("Edit script") + .WithViewModel(out ScriptConfigurationEditViewModel vm, ("scriptConfiguration", scriptConfiguration)) + .WithCloseButtonText("Cancel") + .HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Submit)) + .HavingSecondaryButton(b => b.WithText("Delete")) + .WithDefaultButton(ContentDialogButton.Primary) + .ShowAsync(); + + // Remove the script if the delete button was pressed + if (contentDialogResult == ContentDialogResult.Secondary) + { + if (Profile != null) + _scriptingService.RemoveScript(scriptConfiguration, Profile); + else + _scriptingService.RemoveScript(scriptConfiguration); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Scripting/ScriptsDialogView.axaml b/src/Artemis.UI/Screens/Scripting/ScriptsDialogView.axaml new file mode 100644 index 000000000..d8eecb0a7 --- /dev/null +++ b/src/Artemis.UI/Screens/Scripting/ScriptsDialogView.axaml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + +