diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj
index 09574ca8c..4abc0d292 100644
--- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj
+++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj
@@ -26,4 +26,9 @@
..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll
+
+
+ %(Filename)
+
+
diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj.DotSettings b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj.DotSettings
new file mode 100644
index 000000000..1865d232b
--- /dev/null
+++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.csproj.DotSettings
@@ -0,0 +1,2 @@
+
+ True
\ No newline at end of file
diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs b/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs
index ee07690e9..ed2b6c081 100644
--- a/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs
+++ b/src/Artemis.UI.Avalonia.Shared/Services/Builders/NotificationBuilder.cs
@@ -1,8 +1,10 @@
using System;
using System.Threading.Tasks;
+using Artemis.UI.Avalonia.Shared.Utilities;
using Avalonia.Controls;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
+using Button = Avalonia.Controls.Button;
namespace Artemis.UI.Avalonia.Shared.Services.Builders
{
@@ -36,6 +38,19 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders
return this;
}
+
+ ///
+ /// Add a filter to the dialog
+ ///
+ public NotificationBuilder HavingButton(Action configure)
+ {
+ NotificationButtonBuilder builder = new();
+ configure(builder);
+ _infoBar.ActionButton = builder.Build();
+
+ return this;
+ }
+
public NotificationBuilder WithSeverity(NotificationSeverity severity)
{
_infoBar.Severity = (InfoBarSeverity) severity;
@@ -71,6 +86,31 @@ namespace Artemis.UI.Avalonia.Shared.Services.Builders
}
}
+ public class NotificationButtonBuilder
+ {
+ private string _text = "Text";
+ private Action? _action;
+
+ public NotificationButtonBuilder WithText(string text)
+ {
+ _text = text;
+ return this;
+ }
+
+ public NotificationButtonBuilder WithAction(Action action)
+ {
+ _action = action;
+ return this;
+ }
+
+ public IControl Build()
+ {
+ return _action != null
+ ? new Button {Content = _text, Command = new DelegateCommand(_ => _action())}
+ : new Button {Content = _text};
+ }
+ }
+
public enum NotificationSeverity
{
Informational,
diff --git a/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IWindowService.cs b/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IWindowService.cs
index e965ee66e..f2f688a86 100644
--- a/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IWindowService.cs
+++ b/src/Artemis.UI.Avalonia.Shared/Services/Interfaces/IWindowService.cs
@@ -1,4 +1,5 @@
-using System.Threading.Tasks;
+using System;
+using System.Threading.Tasks;
using Artemis.UI.Avalonia.Shared.Services.Builders;
using Avalonia.Controls;
@@ -7,20 +8,27 @@ namespace Artemis.UI.Avalonia.Shared.Services.Interfaces
public interface IWindowService : IArtemisSharedUIService
{
///
- /// Creates a view model instance of type and shows its corresponding View as a window
+ /// Creates a view model instance of type and shows its corresponding View as a window
///
/// The type of view model to create
/// The created view model
T ShowWindow();
///
- /// Given a ViewModel, show its corresponding View as a window
+ /// Given a ViewModel, show its corresponding View as a window
///
/// ViewModel to show the View for
void ShowWindow(object viewModel);
///
- /// Given a ViewModel, show its corresponding View as a Dialog
+ /// Shows a dialog displaying the given exception
+ ///
+ /// The title of the dialog
+ /// The exception to display
+ void ShowExceptionDialog(string title, Exception exception);
+
+ ///
+ /// Given a ViewModel, show its corresponding View as a Dialog
///
/// The return type
/// ViewModel to show the View for
@@ -28,13 +36,13 @@ namespace Artemis.UI.Avalonia.Shared.Services.Interfaces
Task ShowDialogAsync(object viewModel);
///
- /// Creates an open file dialog, use the fluent API to configure it
+ /// Creates an open file dialog, use the fluent API to configure it
///
/// The builder that can be used to configure the dialog
OpenFileDialogBuilder CreateOpenFileDialog();
///
- /// Creates a save file dialog, use the fluent API to configure it
+ /// Creates a save file dialog, use the fluent API to configure it
///
/// The builder that can be used to configure the dialog
SaveFileDialogBuilder CreateSaveFileDialog();
diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml
new file mode 100644
index 000000000..0fc655a38
--- /dev/null
+++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml
@@ -0,0 +1,9 @@
+
+ Eh you got an exception but I didn't write the viewer yet :(
+
diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs
new file mode 100644
index 000000000..8457638c4
--- /dev/null
+++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogView.axaml.cs
@@ -0,0 +1,20 @@
+using Avalonia.Markup.Xaml;
+
+namespace Artemis.UI.Avalonia.Shared.Services
+{
+ public partial class ExceptionDialogView : ReactiveWindow
+ {
+ public ExceptionDialogView()
+ {
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs
new file mode 100644
index 000000000..b2de500b2
--- /dev/null
+++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/ExceptionDialogViewModel.cs
@@ -0,0 +1,12 @@
+using System;
+
+namespace Artemis.UI.Avalonia.Shared.Services
+{
+ public class ExceptionDialogViewModel : ActivatableViewModelBase
+ {
+ public ExceptionDialogViewModel(string title, Exception exception)
+ {
+
+ }
+ }
+}
diff --git a/src/Artemis.UI.Avalonia.Shared/Services/WindowService.cs b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs
similarity index 86%
rename from src/Artemis.UI.Avalonia.Shared/Services/WindowService.cs
rename to src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs
index 9ea522b6e..3fb07710a 100644
--- a/src/Artemis.UI.Avalonia.Shared/Services/WindowService.cs
+++ b/src/Artemis.UI.Avalonia.Shared/Services/WindowService/WindowService.cs
@@ -14,6 +14,7 @@ namespace Artemis.UI.Avalonia.Shared.Services
internal class WindowService : IWindowService
{
private readonly IKernel _kernel;
+ private bool _exceptionDialogOpen;
public WindowService(IKernel kernel)
{
@@ -42,6 +43,23 @@ namespace Artemis.UI.Avalonia.Shared.Services
window.Show();
}
+ ///
+ public void ShowExceptionDialog(string title, Exception exception)
+ {
+ if (_exceptionDialogOpen)
+ return;
+
+ try
+ {
+ _exceptionDialogOpen = true;
+ ShowDialogAsync(new ExceptionDialogViewModel(title, exception)).GetAwaiter().GetResult();
+ }
+ finally
+ {
+ _exceptionDialogOpen = false;
+ }
+ }
+
public async Task ShowDialogAsync(object viewModel)
{
if (Application.Current.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime classic)
diff --git a/src/Artemis.UI.Avalonia.Shared/Utilities/DelegateCommand.cs b/src/Artemis.UI.Avalonia.Shared/Utilities/DelegateCommand.cs
new file mode 100644
index 000000000..03fff689d
--- /dev/null
+++ b/src/Artemis.UI.Avalonia.Shared/Utilities/DelegateCommand.cs
@@ -0,0 +1,60 @@
+using System;
+using System.Windows.Input;
+
+namespace Artemis.UI.Avalonia.Shared.Utilities
+{
+ ///
+ /// Provides a command that simply calls a delegate when invoked
+ ///
+ public class DelegateCommand : ICommand
+ {
+ private readonly Predicate? _canExecute;
+ private readonly Action _execute;
+
+ ///
+ /// Creates a new instance of the class
+ ///
+ /// The delegate to execute
+ public DelegateCommand(Action execute) : this(execute, null)
+ {
+ }
+
+ ///
+ /// Creates a new instance of the class with a predicate indicating whether the command
+ /// can be executed
+ ///
+ /// The delegate to execute
+ /// The predicate that determines whether the command can execute
+ public DelegateCommand(Action execute, Predicate? canExecute)
+ {
+ _execute = execute;
+ _canExecute = canExecute;
+ }
+
+ ///
+ /// Invokes the event
+ ///
+ public void RaiseCanExecuteChanged()
+ {
+ CanExecuteChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ ///
+ public event EventHandler? CanExecuteChanged;
+
+ ///
+ public bool CanExecute(object? parameter)
+ {
+ if (_canExecute == null)
+ return true;
+
+ return _canExecute(parameter);
+ }
+
+ ///
+ public void Execute(object? parameter)
+ {
+ _execute(parameter);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj
index 62c11bbd0..3f8a3f1ab 100644
--- a/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj
+++ b/src/Artemis.UI.Avalonia/Artemis.UI.Avalonia.csproj
@@ -11,6 +11,9 @@
+
+
+
@@ -59,6 +62,9 @@
%(Filename)
+
+ %(Filename)
+
SidebarView.axaml
Code
@@ -77,6 +83,18 @@
+
+ $(DefaultXamlRuntime)
+ MSBuild:Compile
+
+
+ $(DefaultXamlRuntime)
+ MSBuild:Compile
+
+
+ $(DefaultXamlRuntime)
+ MSBuild:Compile
+
$(DefaultXamlRuntime)
MSBuild:Compile
diff --git a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs
index 7456da69a..04c2f481a 100644
--- a/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs
+++ b/src/Artemis.UI.Avalonia/Screens/Root/ViewModels/SidebarViewModel.cs
@@ -6,7 +6,7 @@ using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Avalonia.Ninject.Factories;
using Artemis.UI.Avalonia.Screens.Home.ViewModels;
-using Artemis.UI.Avalonia.Screens.Settings.ViewModels;
+using Artemis.UI.Avalonia.Screens.Settings;
using Artemis.UI.Avalonia.Screens.SurfaceEditor.ViewModels;
using Artemis.UI.Avalonia.Screens.Workshop.ViewModels;
using Material.Icons;
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml
similarity index 92%
rename from src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml
rename to src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml
index 3aaee15ec..2ff5e265b 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml
@@ -3,7 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
- x:Class="Artemis.UI.Avalonia.Screens.Settings.Views.SettingsView">
+ x:Class="Artemis.UI.Avalonia.Screens.Settings.SettingsView">
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml.cs
similarity index 75%
rename from src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml.cs
rename to src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml.cs
index a45456fe5..8d14b404c 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/Views/SettingsView.axaml.cs
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/SettingsView.axaml.cs
@@ -1,8 +1,7 @@
-using Artemis.UI.Avalonia.Screens.Settings.ViewModels;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
-namespace Artemis.UI.Avalonia.Screens.Settings.Views
+namespace Artemis.UI.Avalonia.Screens.Settings
{
public class SettingsView : ReactiveUserControl
{
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SettingsViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/SettingsViewModel.cs
similarity index 87%
rename from src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SettingsViewModel.cs
rename to src/Artemis.UI.Avalonia/Screens/Settings/SettingsViewModel.cs
index 68fc97f3f..d224356d8 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/SettingsViewModel.cs
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/SettingsViewModel.cs
@@ -1,7 +1,8 @@
using System.Collections.ObjectModel;
+using Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels;
using ReactiveUI;
-namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels
+namespace Artemis.UI.Avalonia.Screens.Settings
{
public class SettingsViewModel : MainScreenViewModel
{
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginFeatureViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginFeatureViewModel.cs
new file mode 100644
index 000000000..ba532aa07
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginFeatureViewModel.cs
@@ -0,0 +1,213 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Artemis.Core;
+using Artemis.Core.Services;
+using Artemis.UI.Avalonia.Shared.Services.Builders;
+using Artemis.UI.Avalonia.Shared.Services.Interfaces;
+using ReactiveUI;
+
+namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.Plugins.ViewModels
+{
+ public class PluginFeatureViewModel : ActivatableViewModelBase
+ {
+ private readonly ICoreService _coreService;
+ private readonly INotificationService _notificationService;
+ private readonly IPluginManagementService _pluginManagementService;
+ private readonly IWindowService _windowService;
+ private bool _enabling;
+
+ public PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo,
+ bool showShield,
+ ICoreService coreService,
+ IWindowService windowService,
+ INotificationService notificationService,
+ IPluginManagementService pluginManagementService)
+ {
+ _coreService = coreService;
+ _windowService = windowService;
+ _notificationService = notificationService;
+ _pluginManagementService = pluginManagementService;
+
+ FeatureInfo = pluginFeatureInfo;
+ ShowShield = FeatureInfo.Plugin.Info.RequiresAdmin && showShield;
+
+ _pluginManagementService.PluginFeatureEnabling += OnFeatureEnabling;
+ _pluginManagementService.PluginFeatureEnabled += OnFeatureEnableStopped;
+ _pluginManagementService.PluginFeatureEnableFailed += OnFeatureEnableStopped;
+
+ FeatureInfo.Plugin.Enabled += PluginOnToggled;
+ FeatureInfo.Plugin.Disabled += PluginOnToggled;
+ }
+
+ public PluginFeatureInfo FeatureInfo { get; }
+ public Exception? LoadException => FeatureInfo.LoadException;
+
+ public bool ShowShield { get; }
+
+ public bool Enabling
+ {
+ get => _enabling;
+ set => this.RaiseAndSetIfChanged(ref _enabling, value);
+ }
+
+ public bool IsEnabled
+ {
+ get => FeatureInfo.Instance != null && FeatureInfo.Instance.IsEnabled;
+ set => Task.Run(() => UpdateEnabled(value));
+ }
+
+ 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 IsPopupEnabled => CanInstallPrerequisites || CanRemovePrerequisites;
+
+ public void ShowLogsFolder()
+ {
+ try
+ {
+ Process.Start(Environment.GetEnvironmentVariable("WINDIR") + @"\explorer.exe", Path.Combine(Constants.DataFolder, "Logs"));
+ }
+ catch (Exception e)
+ {
+ _windowService.ShowExceptionDialog("Welp, we couldn\'t open the logs folder for you", e);
+ }
+ }
+
+ public void ViewLoadException()
+ {
+ if (LoadException != null)
+ _windowService.ShowExceptionDialog("Feature failed to enable", LoadException);
+ }
+
+ public async Task InstallPrerequisites()
+ {
+ if (FeatureInfo.Prerequisites.Any())
+ await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, new List {FeatureInfo});
+ }
+
+ public async Task RemovePrerequisites()
+ {
+ if (FeatureInfo.Prerequisites.Any(p => p.UninstallActions.Any()))
+ {
+ await PluginPrerequisitesUninstallDialogViewModel.Show(_dialogService, new List {FeatureInfo});
+ this.RaisePropertyChanged(nameof(IsEnabled));
+ }
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _pluginManagementService.PluginFeatureEnabling -= OnFeatureEnabling;
+ _pluginManagementService.PluginFeatureEnabled -= OnFeatureEnableStopped;
+ _pluginManagementService.PluginFeatureEnableFailed -= OnFeatureEnableStopped;
+
+ FeatureInfo.Plugin.Enabled -= PluginOnToggled;
+ FeatureInfo.Plugin.Disabled -= PluginOnToggled;
+ }
+
+ base.Dispose(disposing);
+ }
+
+ private async Task UpdateEnabled(bool enable)
+ {
+ if (IsEnabled == enable)
+ {
+ this.RaisePropertyChanged(nameof(IsEnabled));
+ return;
+ }
+
+ if (FeatureInfo.Instance == null)
+ {
+ this.RaisePropertyChanged(nameof(IsEnabled));
+ _notificationService.CreateNotification()
+ .WithMessage($"Feature '{FeatureInfo.Name}' is in a broken state and cannot enable.")
+ .HavingButton(b => b.WithText("View logs").WithAction(ShowLogsFolder))
+ .WithSeverity(NotificationSeverity.Error)
+ .Show();
+ return;
+ }
+
+ if (enable)
+ {
+ Enabling = true;
+
+ try
+ {
+ if (FeatureInfo.Plugin.Info.RequiresAdmin && !_coreService.IsElevated)
+ {
+ bool confirmed = await _dialogService.ShowConfirmDialog("Enable feature", "The plugin of this feature requires admin rights, are you sure you want to enable it?");
+ if (!confirmed)
+ {
+ this.RaisePropertyChanged(nameof(IsEnabled));
+ return;
+ }
+ }
+
+ // Check if all prerequisites are met async
+ if (!FeatureInfo.ArePrerequisitesMet())
+ {
+ await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, new List {FeatureInfo});
+ if (!FeatureInfo.ArePrerequisitesMet())
+ {
+ this.RaisePropertyChanged(nameof(IsEnabled));
+ return;
+ }
+ }
+
+ await Task.Run(() => _pluginManagementService.EnablePluginFeature(FeatureInfo.Instance!, true));
+ }
+ catch (Exception e)
+ {
+ _notificationService.CreateNotification()
+ .WithMessage($"Failed to enable '{FeatureInfo.Name}'.\r\n{e.Message}")
+ .HavingButton(b => b.WithText("View logs").WithAction(ShowLogsFolder))
+ .WithSeverity(NotificationSeverity.Error)
+ .Show();
+ }
+ finally
+ {
+ Enabling = false;
+ }
+ }
+ else
+ {
+ _pluginManagementService.DisablePluginFeature(FeatureInfo.Instance, true);
+ this.RaisePropertyChanged(nameof(IsEnabled));
+ }
+ }
+
+ private void OnFeatureEnabling(object? sender, PluginFeatureEventArgs e)
+ {
+ if (e.PluginFeature != FeatureInfo.Instance)
+ {
+ return;
+ }
+
+ Enabling = true;
+ }
+
+ private void OnFeatureEnableStopped(object? sender, PluginFeatureEventArgs e)
+ {
+ if (e.PluginFeature != FeatureInfo.Instance)
+ {
+ return;
+ }
+
+ Enabling = false;
+
+ this.RaisePropertyChanged(nameof(IsEnabled));
+ this.RaisePropertyChanged(nameof(LoadException));
+ }
+
+ private void PluginOnToggled(object? sender, EventArgs e)
+ {
+ this.RaisePropertyChanged(nameof(CanToggleEnabled));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginSettingsViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginSettingsViewModel.cs
new file mode 100644
index 000000000..6cb395ced
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginSettingsViewModel.cs
@@ -0,0 +1,325 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Threading.Tasks;
+using Artemis.Core;
+using Artemis.Core.Services;
+using Artemis.UI.Avalonia.Screens.Settings.Tabs.Plugins.ViewModels;
+using Artemis.UI.Ninject.Factories;
+using Artemis.UI.Screens.Plugins;
+using Artemis.UI.Shared.Services;
+using Ninject;
+using Stylet;
+
+namespace Artemis.UI.Screens.Settings.Tabs.Plugins
+{
+ public class PluginSettingsViewModel : Conductor.Collection.AllActive
+ {
+ private readonly ICoreService _coreService;
+ private readonly IDialogService _dialogService;
+ private readonly IMessageService _messageService;
+ private readonly IPluginManagementService _pluginManagementService;
+ private readonly ISettingsVmFactory _settingsVmFactory;
+ private readonly IWindowManager _windowManager;
+ private bool _canInstallPrerequisites;
+ private bool _canRemovePrerequisites;
+ private bool _enabling;
+ private bool _isSettingsPopupOpen;
+ private Plugin _plugin;
+
+ public PluginSettingsViewModel(Plugin plugin,
+ ISettingsVmFactory settingsVmFactory,
+ ICoreService coreService,
+ IWindowManager windowManager,
+ IDialogService dialogService,
+ IPluginManagementService pluginManagementService,
+ IMessageService messageService)
+ {
+ Plugin = plugin;
+
+ _settingsVmFactory = settingsVmFactory;
+ _coreService = coreService;
+ _windowManager = windowManager;
+ _dialogService = dialogService;
+ _pluginManagementService = pluginManagementService;
+ _messageService = messageService;
+ }
+
+ public Plugin Plugin
+ {
+ get => _plugin;
+ set => SetAndNotify(ref _plugin, value);
+ }
+
+ public bool Enabling
+ {
+ get => _enabling;
+ set => SetAndNotify(ref _enabling, value);
+ }
+
+ public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name;
+ public bool CanOpenSettings => IsEnabled && Plugin.ConfigurationDialog != null;
+
+ public bool IsEnabled
+ {
+ get => Plugin.IsEnabled;
+ set => Task.Run(() => UpdateEnabled(value));
+ }
+
+ public bool IsSettingsPopupOpen
+ {
+ get => _isSettingsPopupOpen;
+ set
+ {
+ if (!SetAndNotify(ref _isSettingsPopupOpen, value)) return;
+ CheckPrerequisites();
+ }
+ }
+
+ public bool CanInstallPrerequisites
+ {
+ get => _canInstallPrerequisites;
+ set => SetAndNotify(ref _canInstallPrerequisites, value);
+ }
+
+ public bool CanRemovePrerequisites
+ {
+ get => _canRemovePrerequisites;
+ set => SetAndNotify(ref _canRemovePrerequisites, value);
+ }
+
+ public void OpenSettings()
+ {
+ PluginConfigurationDialog configurationViewModel = (PluginConfigurationDialog) Plugin.ConfigurationDialog;
+ if (configurationViewModel == null)
+ return;
+
+ try
+ {
+ PluginConfigurationViewModel viewModel = (PluginConfigurationViewModel) Plugin.Kernel.Get(configurationViewModel.Type);
+ _windowManager.ShowWindow(new PluginSettingsWindowViewModel(viewModel));
+ }
+ catch (Exception e)
+ {
+ _dialogService.ShowExceptionDialog("An exception occured while trying to show the plugin's settings window", e);
+ throw;
+ }
+ }
+
+ public void OpenPluginDirectory()
+ {
+ try
+ {
+ Process.Start(Environment.GetEnvironmentVariable("WINDIR") + @"\explorer.exe", Plugin.Directory.FullName);
+ }
+ catch (Exception e)
+ {
+ _dialogService.ShowExceptionDialog("Welp, we couldn't open the device's plugin folder for you", e);
+ }
+ }
+
+ public async Task Reload()
+ {
+ bool wasEnabled = IsEnabled;
+
+ _pluginManagementService.UnloadPlugin(Plugin);
+ Items.Clear();
+
+ Plugin = _pluginManagementService.LoadPlugin(Plugin.Directory);
+ foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features)
+ Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false));
+
+ if (wasEnabled)
+ await UpdateEnabled(true);
+
+ _messageService.ShowMessage("Reloaded plugin.");
+ }
+
+ public async Task InstallPrerequisites()
+ {
+ List subjects = new() {Plugin.Info};
+ subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled));
+
+ if (subjects.Any(s => s.Prerequisites.Any()))
+ await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, subjects);
+ }
+
+ public async Task RemovePrerequisites(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())))
+ {
+ await PluginPrerequisitesUninstallDialogViewModel.Show(_dialogService, subjects, forPluginRemoval ? "SKIP, REMOVE PLUGIN" : "CANCEL");
+ NotifyOfPropertyChange(nameof(IsEnabled));
+ NotifyOfPropertyChange(nameof(CanOpenSettings));
+ }
+ }
+
+ public async Task RemoveSettings()
+ {
+ bool confirmed = await _dialogService.ShowConfirmDialog("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);
+
+ _messageService.ShowMessage("Cleared plugin settings.");
+ }
+
+ public async Task Remove()
+ {
+ bool confirmed = await _dialogService.ShowConfirmDialog("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.Prerequisites.Any(p => p.UninstallActions.Any())))
+ await RemovePrerequisites(true);
+
+ try
+ {
+ _pluginManagementService.RemovePlugin(Plugin, false);
+ ((PluginSettingsTabViewModel) Parent).GetPluginInstances();
+ }
+ catch (Exception e)
+ {
+ _dialogService.ShowExceptionDialog("Failed to remove plugin", e);
+ throw;
+ }
+
+ _messageService.ShowMessage("Removed plugin.");
+ }
+
+ public void ShowLogsFolder()
+ {
+ try
+ {
+ Process.Start(Environment.GetEnvironmentVariable("WINDIR") + @"\explorer.exe", Path.Combine(Constants.DataFolder, "Logs"));
+ }
+ catch (Exception e)
+ {
+ _dialogService.ShowExceptionDialog("Welp, we couldn\'t open the logs folder for you", e);
+ }
+ }
+
+ public void OpenUri(Uri uri)
+ {
+ Core.Utilities.OpenUrl(uri.ToString());
+ }
+
+ private void PluginManagementServiceOnPluginToggled(object sender, PluginEventArgs e)
+ {
+ NotifyOfPropertyChange(nameof(IsEnabled));
+ NotifyOfPropertyChange(nameof(CanOpenSettings));
+ }
+
+ private async Task UpdateEnabled(bool enable)
+ {
+ if (IsEnabled == enable)
+ {
+ NotifyOfPropertyChange(nameof(IsEnabled));
+ return;
+ }
+
+ if (enable)
+ {
+ Enabling = true;
+
+ if (Plugin.Info.RequiresAdmin && !_coreService.IsElevated)
+ {
+ bool confirmed = await _dialogService.ShowConfirmDialog("Enable plugin", "This plugin requires admin rights, are you sure you want to enable it?");
+ if (!confirmed)
+ {
+ CancelEnable();
+ 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(_dialogService, subjects);
+ if (!subjects.All(s => s.ArePrerequisitesMet()))
+ {
+ CancelEnable();
+ return;
+ }
+ }
+
+ await Task.Run(() =>
+ {
+ try
+ {
+ _pluginManagementService.EnablePlugin(Plugin, true, true);
+ }
+ catch (Exception e)
+ {
+ _messageService.ShowMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder);
+ }
+ finally
+ {
+ Enabling = false;
+ }
+ });
+ }
+ else
+ _pluginManagementService.DisablePlugin(Plugin, true);
+
+ NotifyOfPropertyChange(nameof(IsEnabled));
+ NotifyOfPropertyChange(nameof(CanOpenSettings));
+ }
+
+ private void CancelEnable()
+ {
+ Enabling = false;
+ NotifyOfPropertyChange(nameof(IsEnabled));
+ NotifyOfPropertyChange(nameof(CanOpenSettings));
+ }
+
+ 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()));
+ }
+
+ #region Overrides of Screen
+
+ protected override void OnInitialActivate()
+ {
+ foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features)
+ Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false));
+
+ _pluginManagementService.PluginDisabled += PluginManagementServiceOnPluginToggled;
+ _pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginToggled;
+ base.OnInitialActivate();
+ }
+
+ protected override void OnClose()
+ {
+ _pluginManagementService.PluginDisabled -= PluginManagementServiceOnPluginToggled;
+ _pluginManagementService.PluginEnabled -= PluginManagementServiceOnPluginToggled;
+ base.OnClose();
+ }
+
+ #endregion
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginSettingsWindowViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginSettingsWindowViewModel.cs
new file mode 100644
index 000000000..2e4fe165a
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginSettingsWindowViewModel.cs
@@ -0,0 +1,33 @@
+using System;
+using Artemis.Core;
+using Stylet;
+
+namespace Artemis.UI.Screens.Settings.Tabs.Plugins
+{
+ public class PluginSettingsWindowViewModel : Conductor
+ {
+ private readonly PluginConfigurationViewModel _configurationViewModel;
+
+ public PluginSettingsWindowViewModel(PluginConfigurationViewModel configurationViewModel)
+ {
+ _configurationViewModel = configurationViewModel ?? throw new ArgumentNullException(nameof(configurationViewModel));
+ Plugin = configurationViewModel.Plugin;
+ }
+
+ public Plugin Plugin { get; }
+
+ protected override void OnInitialActivate()
+ {
+ ActiveItem = _configurationViewModel;
+ ActiveItem.Closed += ActiveItemOnClosed;
+
+ base.OnInitialActivate();
+ }
+
+ private void ActiveItemOnClosed(object sender, CloseEventArgs e)
+ {
+ ActiveItem.Closed -= ActiveItemOnClosed;
+ RequestClose();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginsTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginsTabViewModel.cs
new file mode 100644
index 000000000..9992aa2b5
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/ViewModels/PluginsTabViewModel.cs
@@ -0,0 +1,10 @@
+namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels
+{
+ public class PluginsTabViewModel : ActivatableViewModelBase
+ {
+ public PluginsTabViewModel()
+ {
+ DisplayName = "Plugins";
+ }
+ }
+}
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginFeatureView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginFeatureView.axaml
new file mode 100644
index 000000000..a17b8fcf7
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginFeatureView.axaml
@@ -0,0 +1,8 @@
+
+ Welcome to Avalonia!
+
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginFeatureView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginFeatureView.axaml.cs
new file mode 100644
index 000000000..c5f4b468f
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginFeatureView.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.Plugins.Views
+{
+ public partial class PluginFeatureView : UserControl
+ {
+ public PluginFeatureView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginFeatureView.xaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginFeatureView.xaml
new file mode 100644
index 000000000..38998a889
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginFeatureView.xaml
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Feature enabled
+
+
+
+
+
+
+
+ Install prerequisites
+
+
+
+
+
+ Remove prerequisites
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsView.axaml
new file mode 100644
index 000000000..bbdf3f186
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsView.axaml
@@ -0,0 +1,8 @@
+
+ Welcome to Avalonia!
+
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsView.axaml.cs
new file mode 100644
index 000000000..0afe92900
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsView.axaml.cs
@@ -0,0 +1,19 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.Plugins.Views
+{
+ public partial class PluginSettingsView : UserControl
+ {
+ public PluginSettingsView()
+ {
+ InitializeComponent();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsView.xaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsView.xaml
new file mode 100644
index 000000000..d20d17e5a
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsView.xaml
@@ -0,0 +1,209 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ SETTINGS
+
+
+
+
+
+
+
+ Open plugin directory
+
+
+
+
+
+ Reload plugin
+
+
+
+
+
+
+ Install prerequisites
+
+
+
+
+
+ Remove prerequisites
+
+
+
+
+
+
+ Clear plugin settings
+
+
+
+
+
+ Remove plugin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Plugin enabled
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Plugin features
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsWindowView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsWindowView.axaml
new file mode 100644
index 000000000..e63012f83
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsWindowView.axaml
@@ -0,0 +1,9 @@
+
+ Welcome to Avalonia!
+
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsWindowView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsWindowView.axaml.cs
new file mode 100644
index 000000000..fb3b2fc15
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsWindowView.axaml.cs
@@ -0,0 +1,22 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.Plugins.Views
+{
+ public partial class PluginSettingsWindowView : Window
+ {
+ public PluginSettingsWindowView()
+ {
+ InitializeComponent();
+#if DEBUG
+ this.AttachDevTools();
+#endif
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsWindowView.xaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsWindowView.xaml
new file mode 100644
index 000000000..1500833d6
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginSettingsWindowView.xaml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginsTabView.axaml
similarity index 81%
rename from src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml
rename to src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginsTabView.axaml
index 31dce537c..692705262 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginsTabView.axaml
@@ -3,6 +3,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
- x:Class="Artemis.UI.Avalonia.Screens.Settings.Views.PluginsTabView">
+ x:Class="Artemis.UI.Avalonia.Screens.Settings.Tabs.Views.PluginsTabView">
Welcome to Avalonia!
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginsTabView.axaml.cs
similarity index 82%
rename from src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml.cs
rename to src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginsTabView.axaml.cs
index 93874fbf9..fa586dbf3 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/Views/PluginsTabView.axaml.cs
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Plugins/Views/PluginsTabView.axaml.cs
@@ -1,8 +1,7 @@
-using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
-namespace Artemis.UI.Avalonia.Screens.Settings.Views
+namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.Views
{
public partial class PluginsTabView : UserControl
{
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/AboutTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/AboutTabViewModel.cs
similarity index 97%
rename from src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/AboutTabViewModel.cs
rename to src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/AboutTabViewModel.cs
index 67c817382..c52f221e2 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/AboutTabViewModel.cs
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/AboutTabViewModel.cs
@@ -6,7 +6,7 @@ using Avalonia.Media.Imaging;
using Flurl.Http;
using ReactiveUI;
-namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels
+namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels
{
public class AboutTabViewModel : ActivatableViewModelBase
{
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs
new file mode 100644
index 000000000..d101e00d0
--- /dev/null
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/DevicesTabViewModel.cs
@@ -0,0 +1,10 @@
+namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels
+{
+ public class DevicesTabViewModel : ActivatableViewModelBase
+ {
+ public DevicesTabViewModel()
+ {
+ DisplayName = "Devices";
+ }
+ }
+}
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/GeneralTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/GeneralTabViewModel.cs
similarity index 99%
rename from src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/GeneralTabViewModel.cs
rename to src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/GeneralTabViewModel.cs
index 15d481b4f..a35fee005 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/GeneralTabViewModel.cs
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/ViewModels/GeneralTabViewModel.cs
@@ -14,7 +14,7 @@ using Artemis.UI.Avalonia.Services.Interfaces;
using ReactiveUI;
using Serilog.Events;
-namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels
+namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels
{
public class GeneralTabViewModel : ActivatableViewModelBase
{
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/AboutTabView.axaml
similarity index 99%
rename from src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml
rename to src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/AboutTabView.axaml
index e618183ba..f6b192a46 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/AboutTabView.axaml
@@ -6,7 +6,7 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
- x:Class="Artemis.UI.Avalonia.Screens.Settings.Views.AboutTabView">
+ x:Class="Artemis.UI.Avalonia.Screens.Settings.Tabs.Views.AboutTabView">
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/AboutTabView.axaml.cs
similarity index 69%
rename from src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml.cs
rename to src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/AboutTabView.axaml.cs
index 89fa25025..c8d742fa5 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/Views/AboutTabView.axaml.cs
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/AboutTabView.axaml.cs
@@ -1,10 +1,8 @@
-using Artemis.UI.Avalonia.Screens.Settings.ViewModels;
-using Avalonia;
-using Avalonia.Controls;
+using Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
-namespace Artemis.UI.Avalonia.Screens.Settings.Views
+namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.Views
{
public partial class AboutTabView : ReactiveUserControl
{
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml
similarity index 81%
rename from src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml
rename to src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml
index 42e20b0cd..22fdc9002 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml
@@ -3,6 +3,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
- x:Class="Artemis.UI.Avalonia.Screens.Settings.Views.DevicesTabView">
+ x:Class="Artemis.UI.Avalonia.Screens.Settings.Tabs.Views.DevicesTabView">
Welcome to Avalonia!
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml.cs
similarity index 82%
rename from src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml.cs
rename to src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml.cs
index 66c566a98..9efeb4f18 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/Views/DevicesTabView.axaml.cs
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/DevicesTabView.axaml.cs
@@ -1,8 +1,7 @@
-using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
-namespace Artemis.UI.Avalonia.Screens.Settings.Views
+namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.Views
{
public partial class DevicesTabView : UserControl
{
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/GeneralTabView.axaml
similarity index 99%
rename from src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml
rename to src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/GeneralTabView.axaml
index 75e81d393..ac675f20e 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/GeneralTabView.axaml
@@ -7,7 +7,7 @@
xmlns:layerBrushes="clr-namespace:Artemis.Core.LayerBrushes;assembly=Artemis.Core"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="2400"
- x:Class="Artemis.UI.Avalonia.Screens.Settings.Views.GeneralTabView">
+ x:Class="Artemis.UI.Avalonia.Screens.Settings.Tabs.Views.GeneralTabView">
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml.cs b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/GeneralTabView.axaml.cs
similarity index 70%
rename from src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml.cs
rename to src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/GeneralTabView.axaml.cs
index 9f2c0d9a0..9cbf802a6 100644
--- a/src/Artemis.UI.Avalonia/Screens/Settings/Views/GeneralTabView.axaml.cs
+++ b/src/Artemis.UI.Avalonia/Screens/Settings/Tabs/Views/GeneralTabView.axaml.cs
@@ -1,10 +1,8 @@
-using Artemis.UI.Avalonia.Screens.Settings.ViewModels;
-using Avalonia;
-using Avalonia.Controls;
+using Artemis.UI.Avalonia.Screens.Settings.Tabs.ViewModels;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
-namespace Artemis.UI.Avalonia.Screens.Settings.Views
+namespace Artemis.UI.Avalonia.Screens.Settings.Tabs.Views
{
public partial class GeneralTabView : ReactiveUserControl
{
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/DevicesTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/DevicesTabViewModel.cs
deleted file mode 100644
index b975103bc..000000000
--- a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/DevicesTabViewModel.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels
-{
- public class DevicesTabViewModel : ActivatableViewModelBase
- {
- public DevicesTabViewModel()
- {
- DisplayName = "Devices";
- }
- }
-}
diff --git a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/PluginsTabViewModel.cs b/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/PluginsTabViewModel.cs
deleted file mode 100644
index 3caf01598..000000000
--- a/src/Artemis.UI.Avalonia/Screens/Settings/ViewModels/PluginsTabViewModel.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Artemis.UI.Avalonia.Screens.Settings.ViewModels
-{
- public class PluginsTabViewModel : ActivatableViewModelBase
- {
- public PluginsTabViewModel()
- {
- DisplayName = "Plugins";
- }
- }
-}