From 35d5ef174322a071807f1c2cd7907a7d136e77d6 Mon Sep 17 00:00:00 2001 From: Robert Date: Sat, 25 May 2024 16:45:59 +0200 Subject: [PATCH] Startup wizard - Show device providers from all plugins --- .../StartupWizard/StartupWizardViewModel.cs | 20 +- .../WizardPluginFeatureView.axaml | 79 +++++++ .../WizardPluginFeatureView.axaml.cs | 22 ++ .../WizardPluginFeatureViewModel.cs | 208 ++++++++++++++++++ 4 files changed, 321 insertions(+), 8 deletions(-) create mode 100644 src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureView.axaml create mode 100644 src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureView.axaml.cs create mode 100644 src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureViewModel.cs diff --git a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs index 588338b35..7366bbfaf 100644 --- a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs @@ -26,6 +26,8 @@ public partial class StartupWizardViewModel : DialogViewModelBase private readonly ISettingsService _settingsService; private readonly IWindowService _windowService; private readonly IDeviceService _deviceService; + private readonly Func _getPluginFeatureViewModel; + [Notify] private int _currentStep; [Notify] private bool _showContinue; [Notify] private bool _showFinish; @@ -36,12 +38,13 @@ public partial class StartupWizardViewModel : DialogViewModelBase IPluginManagementService pluginManagementService, IWindowService windowService, IDeviceService deviceService, - ISettingsVmFactory settingsVmFactory, - LayoutFinderViewModel layoutFinderViewModel) + LayoutFinderViewModel layoutFinderViewModel, + Func getPluginFeatureViewModel) { _settingsService = settingsService; _windowService = windowService; _deviceService = deviceService; + _getPluginFeatureViewModel = getPluginFeatureViewModel; _autoRunProvider = container.Resolve(IfUnresolved.ReturnDefault); _protocolProvider = container.Resolve(IfUnresolved.ReturnDefault); @@ -51,11 +54,12 @@ public partial class StartupWizardViewModel : DialogViewModelBase SelectLayout = ReactiveCommand.Create(ExecuteSelectLayout); Version = $"Version {Constants.CurrentVersion}"; - // 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, ReactiveCommand.Create(() => new Unit())))); + // Take all compatible device providers and create a view model for them + DeviceProviders = new ObservableCollection(pluginManagementService.GetAllPlugins() + .Where(p => p.Info.IsCompatible) + .SelectMany(p => p.Features.Where(f => f.FeatureType.IsAssignableTo(typeof(DeviceProvider)))) + .OrderBy(f => f.Name) + .Select(f => _getPluginFeatureViewModel(f))); LayoutFinderViewModel = layoutFinderViewModel; CurrentStep = 1; @@ -84,7 +88,7 @@ public partial class StartupWizardViewModel : DialogViewModelBase public ReactiveCommand SelectLayout { get; } public string Version { get; } - public ObservableCollection DeviceProviders { get; } + public ObservableCollection DeviceProviders { get; } public LayoutFinderViewModel LayoutFinderViewModel { get; } public bool IsAutoRunSupported => _autoRunProvider != null; diff --git a/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureView.axaml b/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureView.axaml new file mode 100644 index 000000000..c2dde7ecb --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureView.axaml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + Enable feature + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureView.axaml.cs b/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureView.axaml.cs new file mode 100644 index 000000000..5ff8c58e8 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureView.axaml.cs @@ -0,0 +1,22 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; +using Avalonia.Threading; + +namespace Artemis.UI.Screens.StartupWizard; + +public partial class WizardPluginFeatureView : ReactiveUserControl +{ + public WizardPluginFeatureView() + { + InitializeComponent(); + EnabledToggle.Click += EnabledToggleOnClick; + } + + private void EnabledToggleOnClick(object? sender, RoutedEventArgs e) + { + Dispatcher.UIThread.Post(() => ViewModel?.UpdateEnabled(!ViewModel.IsEnabled)); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureViewModel.cs new file mode 100644 index 000000000..7245809ff --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureViewModel.cs @@ -0,0 +1,208 @@ +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.Screens.Plugins; +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 PropertyChanged.SourceGenerator; +using ReactiveUI; + +namespace Artemis.UI.Screens.StartupWizard; + +public partial class WizardPluginFeatureViewModel : ActivatableViewModelBase +{ + private readonly ICoreService _coreService; + private readonly IPluginManagementService _pluginManagementService; + private readonly IWindowService _windowService; + private Window? _settingsWindow; + [Notify] private bool _canInstallPrerequisites; + [Notify] private bool _canRemovePrerequisites; + [Notify] private bool _enabling; + + public WizardPluginFeatureViewModel(PluginFeatureInfo pluginFeature, ICoreService coreService, IWindowService windowService, IPluginManagementService pluginManagementService) + { + PluginFeature = pluginFeature; + Plugin = pluginFeature.Plugin; + + _coreService = coreService; + _windowService = windowService; + _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)); + } + + OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings, this.WhenAnyValue(vm => vm.IsEnabled, e => e && Plugin.ConfigurationDialog != null)); + + this.WhenActivated(d => + { + pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureChanged; + pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureChanged; + + Disposable.Create(() => + { + pluginManagementService.PluginFeatureEnabled -= PluginManagementServiceOnPluginFeatureChanged; + pluginManagementService.PluginFeatureDisabled -= PluginManagementServiceOnPluginFeatureChanged; + _settingsWindow?.Close(); + }).DisposeWith(d); + }); + } + + public ReactiveCommand OpenSettings { get; } + + public ObservableCollection Platforms { get; } + + public Plugin Plugin { get; } + public PluginFeatureInfo PluginFeature { get; } + public bool IsEnabled => PluginFeature.Instance != null && PluginFeature.Instance.IsEnabled; + + public async Task UpdateEnabled(bool enable) + { + if (Enabling) + return; + + if (!enable) + { + try + { + if (PluginFeature.AlwaysEnabled) + await Task.Run(() => _pluginManagementService.DisablePlugin(Plugin, true)); + else if (PluginFeature.Instance != null) + await Task.Run(() => _pluginManagementService.DisablePluginFeature(PluginFeature.Instance, true)); + } + catch (Exception e) + { + await ShowUpdateEnableFailure(enable, e); + } + finally + { + this.RaisePropertyChanged(nameof(IsEnabled)); + } + + return; + } + + try + { + Enabling = true; + if (Plugin.Info.RequiresAdmin && !_coreService.IsElevated) + { + bool confirmed = await _windowService.ShowConfirmContentDialog("Enable feature", "This feature 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(() => + { + if (!Plugin.IsEnabled) + _pluginManagementService.EnablePlugin(Plugin, true, true); + if (PluginFeature.Instance != null && !PluginFeature.Instance.IsEnabled) + _pluginManagementService.EnablePluginFeature(PluginFeature.Instance, true); + }); + } + catch (Exception e) + { + await ShowUpdateEnableFailure(enable, e); + } + finally + { + Enabling = false; + this.RaisePropertyChanged(nameof(IsEnabled)); + } + } + + private void ExecuteOpenSettings() + { + if (Plugin.ConfigurationDialog == null) + return; + + if (_settingsWindow != null) + { + _settingsWindow.WindowState = WindowState.Normal; + _settingsWindow.Activate(); + return; + } + + try + { + if (Plugin.Resolve(Plugin.ConfigurationDialog.Type) is not PluginConfigurationViewModel viewModel) + throw new ArtemisUIException($"The type of a plugin configuration dialog must inherit {nameof(PluginConfigurationViewModel)}"); + + _settingsWindow = _windowService.ShowWindow(new PluginSettingsWindowViewModel(viewModel)); + _settingsWindow.Closed += (_, _) => _settingsWindow = null; + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("An exception occured while trying to show the plugin's settings window", e); + throw; + } + } + + private async Task ShowUpdateEnableFailure(bool enable, Exception e) + { + string action = enable ? "enable" : "disable"; + ContentDialogBuilder builder = _windowService.CreateContentDialog() + .WithTitle($"Failed to {action} plugin {Plugin.Info.Name}") + .WithContent(e.Message) + .HavingPrimaryButton(b => b.WithText("View logs").WithAction(ShowLogsFolder)); + // If available, add a secondary button pointing to the support page + if (Plugin.Info.HelpPage != null) + builder = builder.HavingSecondaryButton(b => b.WithText("Open support page").WithAction(() => Utilities.OpenUrl(Plugin.Info.HelpPage.ToString()))); + + await builder.ShowAsync(); + } + + private void ShowLogsFolder() + { + try + { + Utilities.OpenFolder(Constants.LogsFolder); + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Welp, we couldn\'t open the logs folder for you", e); + } + } + + private void PluginManagementServiceOnPluginFeatureChanged(object? sender, PluginFeatureEventArgs e) + { + if (e.PluginFeature.Info != PluginFeature) + return; + + Dispatcher.UIThread.Post(() => + { + this.RaisePropertyChanged(nameof(IsEnabled)); + if (!IsEnabled) + _settingsWindow?.Close(); + }); + } +} \ No newline at end of file