From 1210dc3f638136e94936389d514937d57dddc343 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 29 Jul 2022 22:01:36 +0200 Subject: [PATCH] Setup wizard - Added and auto-show on home open UI - Slightly darkened W11 effect --- src/Artemis.Core/Constants.cs | 2 +- src/Artemis.Core/Plugins/Plugin.cs | 28 +- src/Artemis.Core/Plugins/PluginFeatureInfo.cs | 4 +- .../Services/PluginManagementService.cs | 14 +- src/Artemis.UI.Shared/ReactiveCoreWindow.cs | 2 +- .../Services/Window/ExceptionDialogView.axaml | 1 + src/Artemis.UI/Artemis.UI.csproj | 2 +- src/Artemis.UI/MainWindow.axaml | 3 +- .../Ninject/Factories/IVMFactory.cs | 8 +- src/Artemis.UI/Screens/Home/HomeViewModel.cs | 21 +- .../Screens/Plugins/PluginSettingsView.axaml | 118 +----- .../Plugins/PluginSettingsView.axaml.cs | 12 - .../Plugins/PluginSettingsViewModel.cs | 349 ++---------------- .../Screens/Plugins/PluginView.axaml | 124 +++++++ .../Screens/Plugins/PluginView.axaml.cs | 30 ++ .../Screens/Plugins/PluginViewModel.cs | 329 +++++++++++++++++ .../Dialogs/LayerHintsDialogView.axaml | 1 + .../BrushConfigurationWindowView.axaml | 1 + .../EffectConfigurationWindowView.axaml | 1 + .../Screens/Scripting/ScriptsDialogView.axaml | 1 + .../Settings/Tabs/GeneralTabViewModel.cs | 18 +- .../Settings/Tabs/PluginsTabViewModel.cs | 8 +- .../ProfileConfigurationEditView.axaml | 2 +- .../StartupWizard/StartupWizardView.axaml | 26 ++ .../StartupWizard/StartupWizardView.axaml.cs | 48 +++ .../StartupWizard/StartupWizardViewModel.cs | 126 +++++++ .../StartupWizard/Steps/DevicesStep.axaml | 44 +++ .../StartupWizard/Steps/DevicesStep.axaml.cs | 18 + .../StartupWizard/Steps/FinishStep.axaml | 52 +++ .../StartupWizard/Steps/FinishStep.axaml.cs | 18 + .../StartupWizard/Steps/LayoutStep.axaml | 63 ++++ .../StartupWizard/Steps/LayoutStep.axaml.cs | 18 + .../StartupWizard/Steps/SettingsStep.axaml | 74 ++++ .../StartupWizard/Steps/SettingsStep.axaml.cs | 18 + .../StartupWizard/Steps/WelcomeStep.axaml | 61 +++ .../StartupWizard/Steps/WelcomeStep.axaml.cs | 18 + .../NodeScriptWindowView.axaml | 1 + 37 files changed, 1177 insertions(+), 487 deletions(-) create mode 100644 src/Artemis.UI/Screens/Plugins/PluginView.axaml create mode 100644 src/Artemis.UI/Screens/Plugins/PluginView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Plugins/PluginViewModel.cs create mode 100644 src/Artemis.UI/Screens/StartupWizard/StartupWizardView.axaml create mode 100644 src/Artemis.UI/Screens/StartupWizard/StartupWizardView.axaml.cs create mode 100644 src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs create mode 100644 src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStep.axaml create mode 100644 src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStep.axaml.cs create mode 100644 src/Artemis.UI/Screens/StartupWizard/Steps/FinishStep.axaml create mode 100644 src/Artemis.UI/Screens/StartupWizard/Steps/FinishStep.axaml.cs create mode 100644 src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStep.axaml create mode 100644 src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStep.axaml.cs create mode 100644 src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml create mode 100644 src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml.cs create mode 100644 src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStep.axaml create mode 100644 src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStep.axaml.cs diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 69208acb3..7d455e13e 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -38,7 +38,7 @@ namespace Artemis.Core /// /// The full path to the Artemis data folder /// - public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis"); + public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis.Avalonia"); /// /// The full path to the Artemis logs folder diff --git a/src/Artemis.Core/Plugins/Plugin.cs b/src/Artemis.Core/Plugins/Plugin.cs index 993a9c8a8..e7e463238 100644 --- a/src/Artemis.Core/Plugins/Plugin.cs +++ b/src/Artemis.Core/Plugins/Plugin.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.IO; using System.Linq; using System.Reflection; +using Artemis.Core.DeviceProviders; using Artemis.Storage.Entities.Plugins; using McMaster.NETCore.Plugins; using Ninject; @@ -16,6 +17,7 @@ namespace Artemis.Core /// public class Plugin : CorePropertyChanged, IDisposable { + private readonly bool _loadedFromStorage; private readonly List _features; private readonly List _profilers; @@ -25,9 +27,10 @@ namespace Artemis.Core { Info = info; Directory = directory; - Entity = pluginEntity ?? new PluginEntity {Id = Guid, IsEnabled = true}; + Entity = pluginEntity ?? new PluginEntity {Id = Guid}; Info.Plugin = this; + _loadedFromStorage = pluginEntity != null; _features = new List(); _profilers = new List(); @@ -309,6 +312,27 @@ namespace Artemis.Core { return Entity.Features.Any(f => f.IsEnabled) || Features.Any(f => f.AlwaysEnabled); } + + internal void AutoEnableIfNew() + { + if (_loadedFromStorage) + return; + + // Enabled is preset to true if the plugin meets the following criteria + // - Requires no admin rights + // - No always-enabled device providers + // - Either has no prerequisites or they are all met + Entity.IsEnabled = !Info.RequiresAdmin && + Features.All(f => !f.AlwaysEnabled || !f.FeatureType.IsAssignableTo(typeof(DeviceProvider))) && + Info.ArePrerequisitesMet(); + + if (!Entity.IsEnabled) + return; + + // Also auto-enable any non-device provider feature + foreach (PluginFeatureInfo pluginFeatureInfo in Features) + pluginFeatureInfo.Entity.IsEnabled = !pluginFeatureInfo.FeatureType.IsAssignableTo(typeof(DeviceProvider)); + } /// public void Dispose() @@ -316,5 +340,7 @@ namespace Artemis.Core Dispose(true); GC.SuppressFinalize(this); } + + } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs index c0350c26f..ed9064f70 100644 --- a/src/Artemis.Core/Plugins/PluginFeatureInfo.cs +++ b/src/Artemis.Core/Plugins/PluginFeatureInfo.cs @@ -91,7 +91,7 @@ namespace Artemis.Core } /// - /// The name of the plugin + /// The name of the feature /// [JsonProperty(Required = Required.Always)] public string Name @@ -101,7 +101,7 @@ namespace Artemis.Core } /// - /// A short description of the plugin + /// A short description of the feature /// [JsonProperty] public string? Description diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 48e403954..782fa1108 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -362,23 +362,25 @@ namespace Artemis.Core.Services foreach (Type featureType in featureTypes) { // Load the enabled state and if not found, default to true - PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ?? - new PluginFeatureEntity {IsEnabled = true, Type = featureType.FullName!}; + PluginFeatureEntity featureEntity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureType.FullName) ?? new PluginFeatureEntity {Type = featureType.FullName!}; PluginFeatureInfo feature = new(plugin, featureType, featureEntity, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute))); - + // If the plugin only has a single feature, it should always be enabled if (featureTypes.Count == 1) feature.AlwaysEnabled = true; - + plugin.AddFeature(feature); } if (!featureTypes.Any()) _logger.Warning("Plugin {plugin} contains no features", plugin); + // It is appropriate to call this now that we have the features of this plugin + plugin.AutoEnableIfNew(); + List bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(PluginBootstrapper).IsAssignableFrom(t)).ToList(); if (bootstrappers.Count > 1) - _logger.Warning($"{plugin} has more than one bootstrapper, only initializing {bootstrappers.First().FullName}"); + _logger.Warning("{Plugin} has more than one bootstrapper, only initializing {FullName}", plugin, bootstrappers.First().FullName); if (bootstrappers.Any()) { plugin.Bootstrapper = (PluginBootstrapper?) Activator.CreateInstance(bootstrappers.First()); @@ -398,7 +400,7 @@ namespace Artemis.Core.Services { if (!plugin.Info.IsCompatible) throw new ArtemisPluginException(plugin, $"This plugin only supports the following operating system(s): {plugin.Info.Platforms}"); - + if (plugin.Assembly == null) throw new ArtemisPluginException(plugin, "Cannot enable a plugin that hasn't successfully been loaded"); diff --git a/src/Artemis.UI.Shared/ReactiveCoreWindow.cs b/src/Artemis.UI.Shared/ReactiveCoreWindow.cs index 20ed77b32..2001e192f 100644 --- a/src/Artemis.UI.Shared/ReactiveCoreWindow.cs +++ b/src/Artemis.UI.Shared/ReactiveCoreWindow.cs @@ -64,7 +64,7 @@ namespace Artemis.UI.Shared TransparencyLevelHint = WindowTransparencyLevel.Mica; Color2 color = this.TryFindResource("SolidBackgroundFillColorBase", out object? value) ? (Color) value : new Color2(32, 32, 32); - color = color.LightenPercent(-0.1f); + color = color.LightenPercent(-0.5f); Background = new ImmutableSolidColorBrush(color, 0.82); } diff --git a/src/Artemis.UI.Shared/Services/Window/ExceptionDialogView.axaml b/src/Artemis.UI.Shared/Services/Window/ExceptionDialogView.axaml index c4c3c9350..a1979dfdb 100644 --- a/src/Artemis.UI.Shared/Services/Window/ExceptionDialogView.axaml +++ b/src/Artemis.UI.Shared/Services/Window/ExceptionDialogView.axaml @@ -4,6 +4,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" x:Class="Artemis.UI.Shared.Services.ExceptionDialogView" + Icon="/Assets/Images/Logo/application.ico" Title="{Binding Title}" ExtendClientAreaToDecorationsHint="True" Width="800" diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 633873f49..b3add6810 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -20,7 +20,7 @@ - + diff --git a/src/Artemis.UI/MainWindow.axaml b/src/Artemis.UI/MainWindow.axaml index da1cbb348..1d8017354 100644 --- a/src/Artemis.UI/MainWindow.axaml +++ b/src/Artemis.UI/MainWindow.axaml @@ -6,7 +6,8 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.MainWindow" Icon="/Assets/Images/Logo/application.ico" - Title="Artemis 2.0"> + Title="Artemis 2.0" + WindowStartupLocation="CenterScreen"> diff --git a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs index 0303478c0..989a1211f 100644 --- a/src/Artemis.UI/Ninject/Factories/IVMFactory.cs +++ b/src/Artemis.UI/Ninject/Factories/IVMFactory.cs @@ -1,4 +1,5 @@ using System.Collections.ObjectModel; +using System.Reactive; using Artemis.Core; using Artemis.Core.LayerBrushes; using Artemis.Core.LayerEffects; @@ -42,10 +43,9 @@ public interface IDeviceVmFactory : IVmFactory public interface ISettingsVmFactory : IVmFactory { - PluginSettingsViewModel CreatePluginSettingsViewModel(Plugin plugin); - - PluginFeatureViewModel CreatePluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield); - // DeviceSettingsViewModel CreateDeviceSettingsViewModel(ArtemisDevice device); + PluginSettingsViewModel PluginSettingsViewModel(Plugin plugin); + PluginViewModel PluginViewModel(Plugin plugin, ReactiveCommand? reload); + PluginFeatureViewModel PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield); } public interface ISidebarVmFactory : IVmFactory diff --git a/src/Artemis.UI/Screens/Home/HomeViewModel.cs b/src/Artemis.UI/Screens/Home/HomeViewModel.cs index 1bd0d3032..c44088e87 100644 --- a/src/Artemis.UI/Screens/Home/HomeViewModel.cs +++ b/src/Artemis.UI/Screens/Home/HomeViewModel.cs @@ -1,12 +1,19 @@ -using ReactiveUI; +using Artemis.Core.Services; +using Artemis.UI.Screens.StartupWizard; +using Artemis.UI.Shared.Services; +using Avalonia.Threading; +using ReactiveUI; -namespace Artemis.UI.Screens.Home +namespace Artemis.UI.Screens.Home; + +public class HomeViewModel : MainScreenViewModel { - public class HomeViewModel : MainScreenViewModel + public HomeViewModel(IScreen hostScreen, ISettingsService settingsService, IWindowService windowService) : base(hostScreen, "home") { - public HomeViewModel(IScreen hostScreen) : base(hostScreen, "home") - { - DisplayName = "Home"; - } + DisplayName = "Home"; + + // Show the startup wizard if it hasn't been completed + if (!settingsService.GetSetting("UI.SetupWizardCompleted", false).Value) + Dispatcher.UIThread.InvokeAsync(async () => await windowService.ShowDialogAsync()); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml b/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml index 25d3f2d21..381a04dc0 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml +++ b/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml @@ -2,127 +2,15 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - 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:DataType="plugins:PluginSettingsViewModel"> - - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Enable plugin - - - - - - - - + Plugin features diff --git a/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml.cs b/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml.cs index f09007466..4d9dd8534 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginSettingsView.axaml.cs @@ -1,30 +1,18 @@ -using Avalonia.Controls; -using Avalonia.Interactivity; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; -using Avalonia.Threading; namespace Artemis.UI.Screens.Plugins { public partial class PluginSettingsView : ReactiveUserControl { - private readonly CheckBox _enabledToggle; - public PluginSettingsView() { InitializeComponent(); - _enabledToggle = this.Find("EnabledToggle"); - _enabledToggle.Click += EnabledToggleOnClick; } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } - - private void EnabledToggleOnClick(object? sender, RoutedEventArgs e) - { - Dispatcher.UIThread.Post(() => ViewModel?.UpdateEnabled(!ViewModel.Plugin.IsEnabled)); - } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs index e6e7dbf4b..5c1baf29d 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginSettingsViewModel.cs @@ -1,358 +1,65 @@ -using System; -using System.Collections.Generic; -using System.Collections.ObjectModel; -using System.Linq; +using System.Collections.ObjectModel; using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Exceptions; using Artemis.UI.Ninject.Factories; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.Builders; -using Avalonia.Controls; -using Avalonia.Threading; -using Material.Icons; -using Material.Icons.Avalonia; -using Ninject; using ReactiveUI; namespace Artemis.UI.Screens.Plugins; public class PluginSettingsViewModel : ActivatableViewModelBase { - private readonly ICoreService _coreService; - private readonly INotificationService _notificationService; - private readonly IPluginManagementService _pluginManagementService; - private readonly ISettingsVmFactory _settingsVmFactory; - private readonly IWindowService _windowService; - private bool _canInstallPrerequisites; - private bool _canRemovePrerequisites; - private bool _enabling; - private bool _isSettingsPopupOpen; private Plugin _plugin; - private Window? _window; + + private readonly ISettingsVmFactory _settingsVmFactory; + private readonly IPluginManagementService _pluginManagementService; + private readonly INotificationService _notificationService; + + private PluginViewModel _pluginViewModel; - public PluginSettingsViewModel(Plugin plugin, - ISettingsVmFactory settingsVmFactory, - ICoreService coreService, - IWindowService windowService, - INotificationService notificationService, - IPluginManagementService pluginManagementService) + public PluginSettingsViewModel(Plugin plugin, ISettingsVmFactory settingsVmFactory, IPluginManagementService pluginManagementService, INotificationService notificationService) { _plugin = plugin; - _settingsVmFactory = settingsVmFactory; - _coreService = coreService; - _windowService = windowService; - _notificationService = notificationService; _pluginManagementService = pluginManagementService; - - PluginFeatures = new ObservableCollection(); - foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features) - PluginFeatures.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false)); - - Platforms = new ObservableCollection(); - if (Plugin.Info.Platforms != null) - { - if (Plugin.Info.Platforms.Value.HasFlag(PluginPlatform.Windows)) - Platforms.Add(new PluginPlatformViewModel("Windows", MaterialIconKind.MicrosoftWindows)); - if (Plugin.Info.Platforms.Value.HasFlag(PluginPlatform.Linux)) - Platforms.Add(new PluginPlatformViewModel("Linux", MaterialIconKind.Linux)); - if (Plugin.Info.Platforms.Value.HasFlag(PluginPlatform.OSX)) - Platforms.Add(new PluginPlatformViewModel("OSX", MaterialIconKind.Apple)); - } + _notificationService = notificationService; Reload = ReactiveCommand.CreateFromTask(ExecuteReload); - OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings, this.WhenAnyValue(vm => vm.IsEnabled, e => e && 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 => - { - Plugin.Enabled += OnPluginToggled; - Plugin.Disabled += OnPluginToggled; - - Disposable.Create(() => - { - Plugin.Enabled -= OnPluginToggled; - Plugin.Disabled -= OnPluginToggled; - _window?.Close(); - }).DisposeWith(d); - }); + + PluginViewModel = settingsVmFactory.PluginViewModel(_plugin, Reload); + PluginFeatures = new ObservableCollection(); + foreach (PluginFeatureInfo pluginFeatureInfo in _plugin.Features) + PluginFeatures.Add(settingsVmFactory.PluginFeatureViewModel(pluginFeatureInfo, false)); } 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 PluginViewModel PluginViewModel + { + get => _pluginViewModel; + private set => RaiseAndSetIfChanged(ref _pluginViewModel, value); + } public ObservableCollection PluginFeatures { get; } - public ObservableCollection Platforms { get; } - - public Plugin Plugin - { - get => _plugin; - set => RaiseAndSetIfChanged(ref _plugin, value); - } - - public bool Enabling - { - get => _enabling; - set => RaiseAndSetIfChanged(ref _enabling, value); - } - - public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name; - public bool IsEnabled => Plugin.IsEnabled; - - public bool IsSettingsPopupOpen - { - get => _isSettingsPopupOpen; - set - { - if (!RaiseAndSetIfChanged(ref _isSettingsPopupOpen, value)) return; - CheckPrerequisites(); - } - } - - public bool CanInstallPrerequisites - { - get => _canInstallPrerequisites; - set => RaiseAndSetIfChanged(ref _canInstallPrerequisites, value); - } - - public bool CanRemovePrerequisites - { - get => _canRemovePrerequisites; - set => RaiseAndSetIfChanged(ref _canRemovePrerequisites, value); - } - - private void ExecuteOpenSettings() - { - if (Plugin.ConfigurationDialog == null) - return; - - if (_window != null) - { - _window.WindowState = WindowState.Normal; - _window.Activate(); - return; - } - - try - { - PluginConfigurationViewModel? viewModel = Plugin.Kernel!.Get(Plugin.ConfigurationDialog.Type) as PluginConfigurationViewModel; - if (viewModel == null) - throw new ArtemisUIException($"The type of a plugin configuration dialog must inherit {nameof(PluginConfigurationViewModel)}"); - - _window = _windowService.ShowWindow(new PluginSettingsWindowViewModel(viewModel)); - _window.Closed += (_, _) => _window = null; - } - catch (Exception e) - { - _windowService.ShowExceptionDialog("An exception occured while trying to show the plugin's settings window", e); - throw; - } - } - - private void ExecuteOpenPluginDirectory() - { - try - { - Utilities.OpenFolder(Plugin.Directory.FullName); - } - catch (Exception e) - { - _windowService.ShowExceptionDialog("Welp, we couldn't open the device's plugin folder for you", e); - } - } private async Task ExecuteReload() { - bool wasEnabled = IsEnabled; - - await Task.Run(() => _pluginManagementService.UnloadPlugin(Plugin)); + bool wasEnabled = _plugin.IsEnabled; + await Task.Run(() => _pluginManagementService.UnloadPlugin(_plugin)); PluginFeatures.Clear(); - - Plugin = _pluginManagementService.LoadPlugin(Plugin.Directory); - foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features) - PluginFeatures.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false)); + _plugin = _pluginManagementService.LoadPlugin(_plugin.Directory); + + PluginViewModel = _settingsVmFactory.PluginViewModel(_plugin, Reload); + foreach (PluginFeatureInfo pluginFeatureInfo in _plugin.Features) + PluginFeatures.Add(_settingsVmFactory.PluginFeatureViewModel(pluginFeatureInfo, false)); if (wasEnabled) - await UpdateEnabled(true); + await PluginViewModel.UpdateEnabled(true); _notificationService.CreateNotification().WithTitle("Reloaded plugin.").Show(); } - - private async Task ExecuteInstallPrerequisites() - { - List subjects = new() {Plugin.Info}; - subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled)); - - if (subjects.Any(s => s.PlatformPrerequisites.Any())) - await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); - } - - 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.PlatformPrerequisites.Any(p => p.UninstallActions.Any()))) - { - await PluginPrerequisitesUninstallDialogViewModel.Show(_windowService, subjects, forPluginRemoval ? "Skip, remove plugin" : "Cancel"); - } - } - - 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) - return; - - bool wasEnabled = IsEnabled; - - if (IsEnabled) - await UpdateEnabled(false); - - _pluginManagementService.RemovePluginSettings(Plugin); - - if (wasEnabled) - await UpdateEnabled(true); - - _notificationService.CreateNotification().WithTitle("Cleared plugin settings.").Show(); - } - - private async Task ExecuteRemove() - { - bool confirmed = await _windowService.ShowConfirmContentDialog("Remove plugin", "Are you sure you want to remove this plugin?"); - if (!confirmed) - return; - - // 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.PlatformPrerequisites.Any(p => p.UninstallActions.Any()))) - await ExecuteRemovePrerequisites(true); - - try - { - _pluginManagementService.RemovePlugin(Plugin, false); - } - catch (Exception e) - { - _windowService.ShowExceptionDialog("Failed to remove plugin", e); - throw; - } - - _notificationService.CreateNotification().WithTitle("Removed plugin.").Show(); - } - - private void ExecuteShowLogsFolder() - { - try - { - Utilities.OpenFolder(Constants.LogsFolder); - } - catch (Exception e) - { - _windowService.ShowExceptionDialog("Welp, we couldn\'t open the logs folder for you", e); - } - } - - public async Task UpdateEnabled(bool enable) - { - if (Enabling) - return; - - if (!enable) - { - try - { - await Task.Run(() => _pluginManagementService.DisablePlugin(Plugin, true)); - } - catch (Exception e) - { - await Dispatcher.UIThread.InvokeAsync(() => _notificationService.CreateNotification() - .WithSeverity(NotificationSeverity.Error) - .WithMessage($"Failed to disable plugin {Plugin.Info.Name}\r\n{e.Message}") - .HavingButton(b => b.WithText("View logs").WithCommand(ShowLogsFolder)) - .Show()); - } - finally - { - this.RaisePropertyChanged(nameof(IsEnabled)); - } - - return; - } - - try - { - Enabling = true; - if (Plugin.Info.RequiresAdmin && !_coreService.IsElevated) - { - bool confirmed = await _windowService.ShowConfirmContentDialog("Enable plugin", "This plugin requires admin rights, are you sure you want to enable it?"); - if (!confirmed) - return; - } - - // Check if all prerequisites are met async - List subjects = new() {Plugin.Info}; - subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled || f.EnabledInStorage)); - - if (subjects.Any(s => !s.ArePrerequisitesMet())) - { - await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); - if (!subjects.All(s => s.ArePrerequisitesMet())) - return; - } - - await Task.Run(() => _pluginManagementService.EnablePlugin(Plugin, true, true)); - } - catch (Exception e) - { - await Dispatcher.UIThread.InvokeAsync(() => _notificationService.CreateNotification() - .WithSeverity(NotificationSeverity.Error) - .WithMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}") - .HavingButton(b => b.WithText("View logs").WithCommand(ShowLogsFolder)) - .Show()); - } - finally - { - Enabling = false; - this.RaisePropertyChanged(nameof(IsEnabled)); - } - } - - - private void CheckPrerequisites() - { - 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())); - } - - private void OnPluginToggled(object? sender, EventArgs e) - { - Dispatcher.UIThread.Post(() => - { - this.RaisePropertyChanged(nameof(IsEnabled)); - if (!IsEnabled) - _window?.Close(); - }); - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginView.axaml b/src/Artemis.UI/Screens/Plugins/PluginView.axaml new file mode 100644 index 000000000..a576743f8 --- /dev/null +++ b/src/Artemis.UI/Screens/Plugins/PluginView.axaml @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Enable plugin + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginView.axaml.cs b/src/Artemis.UI/Screens/Plugins/PluginView.axaml.cs new file mode 100644 index 000000000..996ff5a97 --- /dev/null +++ b/src/Artemis.UI/Screens/Plugins/PluginView.axaml.cs @@ -0,0 +1,30 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using Avalonia.Threading; + +namespace Artemis.UI.Screens.Plugins; + +public partial class PluginView : ReactiveUserControl +{ + private CheckBox _enabledToggle; + + public PluginView() + { + InitializeComponent(); + _enabledToggle = this.Find("EnabledToggle"); + _enabledToggle.Click += EnabledToggleOnClick; + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void EnabledToggleOnClick(object? sender, RoutedEventArgs e) + { + Dispatcher.UIThread.Post(() => ViewModel?.UpdateEnabled(!ViewModel.Plugin.IsEnabled)); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs new file mode 100644 index 000000000..74904abe1 --- /dev/null +++ b/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs @@ -0,0 +1,329 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Exceptions; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Builders; +using Avalonia.Controls; +using Avalonia.Threading; +using Material.Icons; +using Ninject; +using ReactiveUI; + +namespace Artemis.UI.Screens.Plugins; + +public class PluginViewModel : ActivatableViewModelBase +{ + private readonly ICoreService _coreService; + private readonly INotificationService _notificationService; + private readonly IPluginManagementService _pluginManagementService; + private readonly IWindowService _windowService; + private bool _canInstallPrerequisites; + private bool _canRemovePrerequisites; + private bool _enabling; + private bool _isSettingsPopupOpen; + private Plugin _plugin; + private Window? _window; + + public PluginViewModel(Plugin plugin, + ReactiveCommand? reload, + ICoreService coreService, + IWindowService windowService, + INotificationService notificationService, + IPluginManagementService pluginManagementService) + { + _plugin = plugin; + _coreService = coreService; + _windowService = windowService; + _notificationService = notificationService; + _pluginManagementService = pluginManagementService; + + Platforms = new ObservableCollection(); + if (Plugin.Info.Platforms != null) + { + if (Plugin.Info.Platforms.Value.HasFlag(PluginPlatform.Windows)) + Platforms.Add(new PluginPlatformViewModel("Windows", MaterialIconKind.MicrosoftWindows)); + if (Plugin.Info.Platforms.Value.HasFlag(PluginPlatform.Linux)) + Platforms.Add(new PluginPlatformViewModel("Linux", MaterialIconKind.Linux)); + if (Plugin.Info.Platforms.Value.HasFlag(PluginPlatform.OSX)) + Platforms.Add(new PluginPlatformViewModel("OSX", MaterialIconKind.Apple)); + } + + Reload = reload; + OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings, this.WhenAnyValue(vm => vm.IsEnabled, e => e && 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 => + { + Plugin.Enabled += OnPluginToggled; + Plugin.Disabled += OnPluginToggled; + + Disposable.Create(() => + { + Plugin.Enabled -= OnPluginToggled; + Plugin.Disabled -= OnPluginToggled; + _window?.Close(); + }).DisposeWith(d); + }); + } + + 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 Platforms { get; } + + public Plugin Plugin + { + get => _plugin; + set => RaiseAndSetIfChanged(ref _plugin, value); + } + + public bool Enabling + { + get => _enabling; + set => RaiseAndSetIfChanged(ref _enabling, value); + } + + public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name; + public bool IsEnabled => Plugin.IsEnabled; + + public bool IsSettingsPopupOpen + { + get => _isSettingsPopupOpen; + set + { + if (!RaiseAndSetIfChanged(ref _isSettingsPopupOpen, value)) return; + CheckPrerequisites(); + } + } + + public bool CanInstallPrerequisites + { + get => _canInstallPrerequisites; + set => RaiseAndSetIfChanged(ref _canInstallPrerequisites, value); + } + + public bool CanRemovePrerequisites + { + get => _canRemovePrerequisites; + set => RaiseAndSetIfChanged(ref _canRemovePrerequisites, value); + } + + private void ExecuteOpenSettings() + { + if (Plugin.ConfigurationDialog == null) + return; + + if (_window != null) + { + _window.WindowState = WindowState.Normal; + _window.Activate(); + return; + } + + try + { + PluginConfigurationViewModel? viewModel = Plugin.Kernel!.Get(Plugin.ConfigurationDialog.Type) as PluginConfigurationViewModel; + if (viewModel == null) + throw new ArtemisUIException($"The type of a plugin configuration dialog must inherit {nameof(PluginConfigurationViewModel)}"); + + _window = _windowService.ShowWindow(new PluginSettingsWindowViewModel(viewModel)); + _window.Closed += (_, _) => _window = null; + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("An exception occured while trying to show the plugin's settings window", e); + throw; + } + } + + private void ExecuteOpenPluginDirectory() + { + try + { + Utilities.OpenFolder(Plugin.Directory.FullName); + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Welp, we couldn't open the device's plugin folder for you", e); + } + } + + private async Task ExecuteInstallPrerequisites() + { + List subjects = new() {Plugin.Info}; + subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled)); + + if (subjects.Any(s => s.PlatformPrerequisites.Any())) + await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); + } + + 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.PlatformPrerequisites.Any(p => p.UninstallActions.Any()))) + { + await PluginPrerequisitesUninstallDialogViewModel.Show(_windowService, subjects, forPluginRemoval ? "Skip, remove plugin" : "Cancel"); + } + } + + 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) + return; + + bool wasEnabled = IsEnabled; + + if (IsEnabled) + await UpdateEnabled(false); + + _pluginManagementService.RemovePluginSettings(Plugin); + + if (wasEnabled) + await UpdateEnabled(true); + + _notificationService.CreateNotification().WithTitle("Cleared plugin settings.").Show(); + } + + private async Task ExecuteRemove() + { + bool confirmed = await _windowService.ShowConfirmContentDialog("Remove plugin", "Are you sure you want to remove this plugin?"); + if (!confirmed) + return; + + // 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.PlatformPrerequisites.Any(p => p.UninstallActions.Any()))) + await ExecuteRemovePrerequisites(true); + + try + { + _pluginManagementService.RemovePlugin(Plugin, false); + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Failed to remove plugin", e); + throw; + } + + _notificationService.CreateNotification().WithTitle("Removed plugin.").Show(); + } + + private void ExecuteShowLogsFolder() + { + try + { + Utilities.OpenFolder(Constants.LogsFolder); + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Welp, we couldn\'t open the logs folder for you", e); + } + } + + public async Task UpdateEnabled(bool enable) + { + if (Enabling) + return; + + if (!enable) + { + try + { + await Task.Run(() => _pluginManagementService.DisablePlugin(Plugin, true)); + } + catch (Exception e) + { + await Dispatcher.UIThread.InvokeAsync(() => _notificationService.CreateNotification() + .WithSeverity(NotificationSeverity.Error) + .WithMessage($"Failed to disable plugin {Plugin.Info.Name}\r\n{e.Message}") + .HavingButton(b => b.WithText("View logs").WithCommand(ShowLogsFolder)) + .Show()); + } + finally + { + this.RaisePropertyChanged(nameof(IsEnabled)); + } + + return; + } + + try + { + Enabling = true; + if (Plugin.Info.RequiresAdmin && !_coreService.IsElevated) + { + bool confirmed = await _windowService.ShowConfirmContentDialog("Enable plugin", "This plugin requires admin rights, are you sure you want to enable it?"); + if (!confirmed) + return; + } + + // Check if all prerequisites are met async + List subjects = new() {Plugin.Info}; + subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled || f.EnabledInStorage)); + + if (subjects.Any(s => !s.ArePrerequisitesMet())) + { + await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); + if (!subjects.All(s => s.ArePrerequisitesMet())) + return; + } + + await Task.Run(() => _pluginManagementService.EnablePlugin(Plugin, true, true)); + } + catch (Exception e) + { + await Dispatcher.UIThread.InvokeAsync(() => _notificationService.CreateNotification() + .WithSeverity(NotificationSeverity.Error) + .WithMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}") + .HavingButton(b => b.WithText("View logs").WithCommand(ShowLogsFolder)) + .Show()); + } + finally + { + Enabling = false; + this.RaisePropertyChanged(nameof(IsEnabled)); + } + } + + + private void CheckPrerequisites() + { + 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())); + } + + private void OnPluginToggled(object? sender, EventArgs e) + { + Dispatcher.UIThread.Post(() => + { + this.RaisePropertyChanged(nameof(IsEnabled)); + if (!IsEnabled) + _window?.Close(); + }); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Dialogs/LayerHintsDialogView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Dialogs/LayerHintsDialogView.axaml index 7d1777250..50afd41b8 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Dialogs/LayerHintsDialogView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/Dialogs/LayerHintsDialogView.axaml @@ -9,6 +9,7 @@ x:Class="Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs.LayerHintsDialogView" x:DataType="dialogs:LayerHintsDialogViewModel" WindowStartupLocation="CenterOwner" + Icon="/Assets/Images/Logo/application.ico" Title="Artemis | Adaption hints" Width="750" Height="800"> diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowView.axaml index 40e5d4c30..863c6d768 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/BrushConfigurationWindowView.axaml @@ -7,6 +7,7 @@ d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Windows.BrushConfigurationWindowView" + Icon="/Assets/Images/Logo/application.ico" Title="Artemis | Brush configuration" Width="{Binding Configuration.DialogWidth}" Height="{Binding Configuration.DialogHeight}" diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowView.axaml index e91b5fa1c..6e6289fd7 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/Properties/Windows/EffectConfigurationWindowView.axaml @@ -7,6 +7,7 @@ d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.ProfileEditor.Properties.Windows.EffectConfigurationWindowView" + Icon="/Assets/Images/Logo/application.ico" Title="Artemis | Effect configuration" Width="{Binding Configuration.DialogWidth}" Height="{Binding Configuration.DialogHeight}" diff --git a/src/Artemis.UI/Screens/Scripting/ScriptsDialogView.axaml b/src/Artemis.UI/Screens/Scripting/ScriptsDialogView.axaml index b171b47eb..8f1566474 100644 --- a/src/Artemis.UI/Screens/Scripting/ScriptsDialogView.axaml +++ b/src/Artemis.UI/Screens/Scripting/ScriptsDialogView.axaml @@ -9,6 +9,7 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Scripting.ScriptsDialogView" x:DataType="scripting:ScriptsDialogViewModel" + Icon="/Assets/Images/Logo/application.ico" Title="Artemis | Scripts" Width="1200" Height="750"> diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs index 62b47ac37..0a36b58cf 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs @@ -10,8 +10,10 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.LayerBrushes; using Artemis.Core.Services; +using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; using Avalonia; using DynamicData; using FluentAvalonia.Styling; @@ -26,12 +28,14 @@ namespace Artemis.UI.Screens.Settings private readonly PluginSetting _defaultLayerBrushDescriptor; private readonly ISettingsService _settingsService; private readonly IDebugService _debugService; + private readonly IWindowService _windowService; - public GeneralTabViewModel(IKernel kernel, ISettingsService settingsService, IPluginManagementService pluginManagementService, IDebugService debugService) + public GeneralTabViewModel(IKernel kernel, ISettingsService settingsService, IPluginManagementService pluginManagementService, IDebugService debugService, IWindowService windowService) { DisplayName = "General"; _settingsService = settingsService; _debugService = debugService; + _windowService = windowService; List layerBrushProviders = pluginManagementService.GetFeaturesOfType(); LayerBrushDescriptors = new ObservableCollection(layerBrushProviders.SelectMany(l => l.LayerBrushDescriptors)); @@ -48,7 +52,7 @@ namespace Artemis.UI.Screens.Settings ShowLogs = ReactiveCommand.Create(ExecuteShowLogs); CheckForUpdate = ReactiveCommand.CreateFromTask(ExecuteCheckForUpdate); - ShowSetupWizard = ReactiveCommand.Create(ExecuteShowSetupWizard); + ShowSetupWizard = ReactiveCommand.CreateFromTask(ExecuteShowSetupWizard); ShowDebugger = ReactiveCommand.Create(ExecuteShowDebugger); ShowDataFolder = ReactiveCommand.Create(ExecuteShowDataFolder); } @@ -139,8 +143,9 @@ namespace Artemis.UI.Screens.Settings #region Tools - private void ExecuteShowSetupWizard() + private async Task ExecuteShowSetupWizard() { + await _windowService.ShowDialogAsync(); } private void ExecuteShowDebugger() @@ -164,11 +169,4 @@ namespace Artemis.UI.Screens.Settings } } } - - public enum ApplicationColorScheme - { - Light, - Dark, - Automatic - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs index e8c28e4c0..99463667f 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabViewModel.cs @@ -41,7 +41,7 @@ namespace Artemis.UI.Screens.Settings plugins.Connect() .Filter(pluginFilter) .Sort(SortExpressionComparer.Ascending(p => p.Info.Name)) - .TransformAsync(p => Dispatcher.UIThread.InvokeAsync(() => settingsVmFactory.CreatePluginSettingsViewModel(p), DispatcherPriority.Background)) + .TransformAsync(p => Dispatcher.UIThread.InvokeAsync(() => settingsVmFactory.PluginSettingsViewModel(p), DispatcherPriority.Background)) .Bind(out ReadOnlyObservableCollection pluginViewModels) .Subscribe(); Plugins = pluginViewModels; @@ -88,9 +88,9 @@ namespace Artemis.UI.Screens.Settings // Wait for the VM to be created asynchronously (it would be better to respond to some event here) await Task.Delay(200); // Enable it via the VM to enable the prerequisite dialog - PluginSettingsViewModel? pluginViewModel = Plugins.FirstOrDefault(i => i.Plugin == plugin); - if (pluginViewModel is {IsEnabled: false}) - await pluginViewModel.UpdateEnabled(true); + PluginSettingsViewModel? settingsViewModel = Plugins.FirstOrDefault(i => i.PluginViewModel.Plugin == plugin); + if (settingsViewModel != null && !settingsViewModel.PluginViewModel.IsEnabled) + await settingsViewModel.PluginViewModel.UpdateEnabled(true); _notificationService.CreateNotification() .WithTitle("Plugin imported") diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml index 7ccd2fb53..2a62a5741 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditView.axaml @@ -11,8 +11,8 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="850" x:Class="Artemis.UI.Screens.Sidebar.ProfileConfigurationEditView" x:DataType="local:ProfileConfigurationEditViewModel" - Title="{CompiledBinding DisplayName}" Icon="/Assets/Images/Logo/application.ico" + Title="{CompiledBinding DisplayName}" Width="800" MinWidth="420" Height="975"> diff --git a/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.axaml b/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.axaml new file mode 100644 index 000000000..1b0f0b157 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.axaml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.axaml.cs b/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.axaml.cs new file mode 100644 index 000000000..82229ac93 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.axaml.cs @@ -0,0 +1,48 @@ +using System; +using System.Reactive.Disposables; +using Artemis.UI.Screens.StartupWizard.Steps; +using Artemis.UI.Shared; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using FluentAvalonia.UI.Controls; +using FluentAvalonia.UI.Navigation; +using ReactiveUI; + +namespace Artemis.UI.Screens.StartupWizard; + +public class StartupWizardView : ReactiveCoreWindow +{ + private readonly Frame _frame; + + public StartupWizardView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + + _frame = this.Get("Frame"); + + this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.CurrentStep).Subscribe(ApplyCurrentStep).DisposeWith(d)); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void ApplyCurrentStep(int step) + { + if (step == 1) + _frame.NavigateToType(typeof(WelcomeStep), null, new FrameNavigationOptions()); + else if (step == 2) + _frame.NavigateToType(typeof(DevicesStep), null, new FrameNavigationOptions()); + else if (step == 3) + _frame.NavigateToType(typeof(LayoutStep), null, new FrameNavigationOptions()); + else if (step == 4) + _frame.NavigateToType(typeof(SettingsStep), null, new FrameNavigationOptions()); + else if (step == 5) + _frame.NavigateToType(typeof(FinishStep), null, new FrameNavigationOptions()); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs new file mode 100644 index 000000000..0bcc1a28f --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs @@ -0,0 +1,126 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Reflection; +using Artemis.Core; +using Artemis.Core.DeviceProviders; +using Artemis.Core.Services; +using Artemis.UI.Ninject.Factories; +using Artemis.UI.Screens.Plugins; +using Artemis.UI.Shared; +using ReactiveUI; + +namespace Artemis.UI.Screens.StartupWizard; + +public class StartupWizardViewModel : DialogViewModelBase +{ + private readonly IRgbService _rgbService; + private readonly ISettingsService _settingsService; + private int _currentStep; + private bool _showContinue; + private bool _showGoBack; + private bool _showFinish; + + public StartupWizardViewModel(ISettingsService settingsService, IRgbService rgbService, IPluginManagementService pluginManagementService, ISettingsVmFactory settingsVmFactory) + { + _settingsService = settingsService; + _rgbService = rgbService; + + Continue = ReactiveCommand.Create(ExecuteContinue); + GoBack = ReactiveCommand.Create(ExecuteGoBack); + SkipOrFinishWizard = ReactiveCommand.Create(ExecuteSkipOrFinishWizard); + SelectLayout = ReactiveCommand.Create(ExecuteSelectLayout); + + AssemblyInformationalVersionAttribute? versionAttribute = typeof(StartupWizardViewModel).Assembly.GetCustomAttribute(); + Version = $"Version {versionAttribute?.InformationalVersion} build {Constants.BuildInfo.BuildNumberDisplay}"; + + // Take all compatible plugins that have an always-enabled device provider + DeviceProviders = new ObservableCollection(pluginManagementService.GetAllPlugins() + .Where(p => p.Info.IsCompatible && p.Features.Any(f => f.AlwaysEnabled && f.FeatureType.IsAssignableTo(typeof(DeviceProvider)))) + .OrderBy(p => p.Info.Name) + .Select(p => settingsVmFactory.PluginViewModel(p, null))); + + CurrentStep = 1; + SetupButtons(); + } + + + public ReactiveCommand Continue { get; } + public ReactiveCommand GoBack { get; } + public ReactiveCommand SkipOrFinishWizard { get; } + public ReactiveCommand SelectLayout { get; } + + public string Version { get; } + public ObservableCollection DeviceProviders { get; } + + public PluginSetting UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false); + public PluginSetting UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15); + public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); + public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.CheckForUpdates", true); + + public int CurrentStep + { + get => _currentStep; + set => RaiseAndSetIfChanged(ref _currentStep, value); + } + + public bool ShowContinue + { + get => _showContinue; + set => RaiseAndSetIfChanged(ref _showContinue, value); + } + + public bool ShowGoBack + { + get => _showGoBack; + set => RaiseAndSetIfChanged(ref _showGoBack, value); + } + + public bool ShowFinish + { + get => _showFinish; + set => RaiseAndSetIfChanged(ref _showFinish, value); + } + + private void ExecuteGoBack() + { + if (CurrentStep > 1) + CurrentStep--; + + SetupButtons(); + } + + private void ExecuteContinue() + { + if (CurrentStep < 5) + CurrentStep++; + + SetupButtons(); + } + + private void SetupButtons() + { + ShowContinue = CurrentStep != 3 && CurrentStep < 5; + ShowGoBack = CurrentStep > 1; + ShowFinish = CurrentStep == 5; + } + + private void ExecuteSkipOrFinishWizard() + { + PluginSetting setting = _settingsService.GetSetting("UI.SetupWizardCompleted", false); + setting.Value = true; + setting.Save(); + + Close(true); + } + + + private void ExecuteSelectLayout(string layout) + { + // TODO: Implement the layout + _rgbService.AutoArrangeDevices(); + + ExecuteContinue(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStep.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStep.axaml new file mode 100644 index 000000000..6f4732191 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStep.axaml @@ -0,0 +1,44 @@ + + + + + + Devices are supported through the use of device providers. + + + In the list below you can enable device providers for each brand you own by checking "Enable feature". + + + + + + + + + + + + + + + + + + + + + + + + Note: To avoid possible instability it's recommended to disable the device providers of brands you don't own. + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStep.axaml.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStep.axaml.cs new file mode 100644 index 000000000..f8bfa8155 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStep.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.StartupWizard.Steps; + +public partial class DevicesStep : UserControl +{ + public DevicesStep() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStep.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStep.axaml new file mode 100644 index 000000000..6ffc4c20b --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStep.axaml @@ -0,0 +1,52 @@ + + + + All finished! + + + You are now ready to start using Artemis, enjoy! 😁 + + + To learn more about Artemis and how to use it you may find these resources useful: + + + + + + + + + Artemis wiki + Getting started guide + GitHub + Discord + + + + https://wiki.artemis-rgb.com/ + + + https://wiki.artemis-rgb.com/en/guides/user/introduction + + + https://github.com/Artemis-RGB/Artemis + + + https://discord.gg/S3MVaC9 + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStep.axaml.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStep.axaml.cs new file mode 100644 index 000000000..155a84960 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStep.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.StartupWizard.Steps; + +public partial class FinishStep : UserControl +{ + public FinishStep() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStep.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStep.axaml new file mode 100644 index 000000000..ad49aac55 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStep.axaml @@ -0,0 +1,63 @@ + + + + + + + Artemis uses "spatial awareness" to create realistic effects across multiple devices. + + + In order to do this correctly, we need to know where your devices are located on your desk. Select one of the two presets below, after the setup wizard finishes you can tweak this in detail in the surface editor. + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStep.axaml.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStep.axaml.cs new file mode 100644 index 000000000..8f364c45d --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutStep.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.StartupWizard.Steps; + +public partial class LayoutStep : UserControl +{ + public LayoutStep() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml new file mode 100644 index 000000000..04775f5eb --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml @@ -0,0 +1,74 @@ + + + + + Artemis comes with a variety of settings you can change to tweak everything to your liking. + + + Below you can find a few relevant settings, many more can be changed later on the settings page. + + + + + + + Auto-run on startup + + + + + + + + Hide window on auto-run + + + + + + + + + + Startup delay + + Set the amount of seconds to wait before auto-running Artemis. + + + If some devices don't work because Artemis starts before the manufacturer's software, try increasing this value. + + + + + sec + + + + + + + + Check for updates + + + If enabled, we'll check for updates on startup and periodically while running. + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml.cs new file mode 100644 index 000000000..b26ca28fd --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.StartupWizard.Steps; + +public partial class SettingsStep : UserControl +{ + public SettingsStep() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStep.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStep.axaml new file mode 100644 index 000000000..f9184e406 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStep.axaml @@ -0,0 +1,61 @@ + + + + + + Artemis 2 + + + + + + + + + + + + + + + + + + PolyForm Noncommercial License 1.0.0 + + + + + + Welcome to the Artemis startup wizard! + + + In this wizard we'll walk you through the initial configuration of Artemis. + + + Before you can start you need to tell Artemis which devices you want to use and where they are placed on your desk. + + + PS: You can also skip the wizard and set things up yourself. + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStep.axaml.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStep.axaml.cs new file mode 100644 index 000000000..d77cb25b6 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStep.axaml.cs @@ -0,0 +1,18 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.StartupWizard.Steps; + +public partial class WelcomeStep : UserControl +{ + public WelcomeStep() + { + InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml index 8b9a2c801..77a100254 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml @@ -9,6 +9,7 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.VisualScripting.NodeScriptWindowView" x:DataType="visualScripting:NodeScriptWindowViewModel" + Icon="/Assets/Images/Logo/application.ico" Title="Artemis | Visual Script Editor" Width="1200" Height="800">