1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Plugins - Added platform support

Settings - Fixed updating issues in plugin tab
UI - Don't initialize in design mode
UI - Move ReactiveCoreWindow to Artemis.Shared
Window service - Return window created by ShowWindow
Window service - Added CreateOpenFileDialog API
This commit is contained in:
Robert 2022-07-22 20:28:07 +02:00
parent e385165200
commit 75d85985a9
37 changed files with 600 additions and 375 deletions

View File

@ -26,6 +26,7 @@ namespace Artemis.Core
/// <summary>
/// Marks the feature to always be enabled as long as the plugin is enabled
/// <para>Note: always <see langword="true"/> if this is the plugin's only feature</para>
/// </summary>
public bool AlwaysEnabled { get; set; }
}

View File

@ -122,10 +122,11 @@ namespace Artemis.Core
}
/// <summary>
/// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled
/// Marks the feature to always be enabled as long as the plugin is enabled and cannot be disabled.
/// <para>Note: always <see langword="true"/> if this is the plugin's only feature</para>
/// </summary>
[JsonProperty]
public bool AlwaysEnabled { get; }
public bool AlwaysEnabled { get; internal set; }
/// <summary>
/// Gets a boolean indicating whether the feature is enabled in persistent storage

View File

@ -24,7 +24,7 @@ namespace Artemis.Core
private Plugin _plugin = null!;
private Version _version = null!;
private bool _requiresAdmin;
private PluginPlatform? _platforms;
internal PluginInfo()
{
@ -142,7 +142,17 @@ namespace Artemis.Core
get => _requiresAdmin;
internal set => SetAndNotify(ref _requiresAdmin, value);
}
/// <summary>
/// Gets
/// </summary>
[JsonProperty]
public PluginPlatform? Platforms
{
get => _platforms;
internal set => _platforms = value;
}
/// <summary>
/// Gets the plugin this info is associated with
/// </summary>
@ -164,6 +174,11 @@ namespace Artemis.Core
return Icon.Contains('.') ? Plugin.ResolveRelativePath(Icon) : Icon;
}
}
/// <summary>
/// Gets a boolean indicating whether this plugin is compatible with the current operating system
/// </summary>
public bool IsCompatible => Platforms.MatchesCurrentOperatingSystem();
internal string PreferredPluginDirectory => $"{Main.Split(".dll")[0].Replace("/", "").Replace("\\", "")}-{Guid.ToString().Substring(0, 8)}";

View File

@ -1,4 +1,5 @@
using System;
using Newtonsoft.Json;
namespace Artemis.Core;
@ -6,15 +7,36 @@ namespace Artemis.Core;
/// Specifies OS platforms a plugin may support.
/// </summary>
[Flags]
[JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public enum PluginPlatform
{
/// <summary>The Windows platform.</summary>
Windows = 0,
Windows = 1,
/// <summary>The Linux platform.</summary>
Linux = 1,
Linux = 2,
/// <summary>The OSX platform.</summary>
// ReSharper disable once InconsistentNaming
OSX = 2
OSX = 4
}
internal static class PluginPlatformExtensions
{
/// <summary>
/// Determines whether the provided platform matches the current operating system.
/// </summary>
internal static bool MatchesCurrentOperatingSystem(this PluginPlatform? platform)
{
if (platform == null)
return true;
if (OperatingSystem.IsWindows())
return platform.Value.HasFlag(PluginPlatform.Windows);
if (OperatingSystem.IsLinux())
return platform.Value.HasFlag(PluginPlatform.Linux);
if (OperatingSystem.IsMacOS())
return platform.Value.HasFlag(PluginPlatform.OSX);
return false;
}
}

View File

@ -101,16 +101,7 @@ namespace Artemis.Core
/// </summary>
public bool AppliesToPlatform()
{
if (Platform == null)
return true;
if (OperatingSystem.IsWindows())
return Platform.Value.HasFlag(PluginPlatform.Windows);
if (OperatingSystem.IsLinux())
return Platform.Value.HasFlag(PluginPlatform.Linux);
if (OperatingSystem.IsMacOS())
return Platform.Value.HasFlag(PluginPlatform.OSX);
return false;
return Platform.MatchesCurrentOperatingSystem();
}
/// <summary>

View File

@ -260,7 +260,7 @@ namespace Artemis.Core.Services
}
}
foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled))
foreach (Plugin plugin in _plugins.Where(p => p.Info.IsCompatible && p.Entity.IsEnabled))
{
try
{
@ -364,7 +364,13 @@ namespace Artemis.Core.Services
// 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!};
plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, featureEntity, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute))));
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())
@ -390,6 +396,9 @@ namespace Artemis.Core.Services
public void EnablePlugin(Plugin plugin, bool saveState, bool ignorePluginLock)
{
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");
@ -446,7 +455,18 @@ namespace Artemis.Core.Services
// Activate features after they are all loaded
foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && (f.EnabledInStorage || f.AlwaysEnabled)))
EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock);
{
try
{
EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock);
}
catch (Exception)
{
if (pluginFeature.AlwaysEnabled)
DisablePlugin(plugin, false);
throw;
}
}
if (saveState)
{

View File

@ -1,6 +1,7 @@
using Artemis.Core.Services;
using Artemis.UI.Linux.Providers.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using ReactiveUI;
@ -26,6 +27,9 @@ namespace Artemis.UI.Linux
public override void OnFrameworkInitializationCompleted()
{
if (Design.IsDesignMode)
return;
ArtemisBootstrapper.Initialize();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
_applicationStateManager = new ApplicationStateManager(_kernel!, desktop.Args);

View File

@ -1,4 +1,5 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using Ninject;
@ -20,6 +21,9 @@ namespace Artemis.UI.MacOS
public override void OnFrameworkInitializationCompleted()
{
if (Design.IsDesignMode)
return;
ArtemisBootstrapper.Initialize();
}
}

View File

@ -3,8 +3,8 @@
<OutputType>Library</OutputType>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<ApplicationIcon/>
<StartupObject/>
<ApplicationIcon />
<StartupObject />
<OutputPath>bin\</OutputPath>
<PlatformTarget>x64</PlatformTarget>
<Platforms>x64</Platforms>
@ -15,20 +15,20 @@
<ItemGroup>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.15"/>
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.15"/>
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.15"/>
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="0.10.14"/>
<PackageReference Include="DynamicData" Version="7.8.6"/>
<PackageReference Include="FluentAvaloniaUI" Version="1.4.1"/>
<PackageReference Include="Material.Icons.Avalonia" Version="1.0.2"/>
<PackageReference Include="ReactiveUI" Version="17.1.50"/>
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1"/>
<PackageReference Include="RGB.NET.Core" Version="1.0.0-prerelease.32"/>
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.1"/>
<PackageReference Include="Avalonia" Version="0.10.15" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.15" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.15" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="0.10.14" />
<PackageReference Include="DynamicData" Version="7.8.6" />
<PackageReference Include="FluentAvaloniaUI" Version="1.4.1" />
<PackageReference Include="Material.Icons.Avalonia" Version="1.0.2" />
<PackageReference Include="ReactiveUI" Version="17.1.50" />
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
<PackageReference Include="RGB.NET.Core" Version="1.0.0-prerelease.32" />
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj"/>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Controls\HotkeyBox.axaml.cs">

View File

@ -4,7 +4,7 @@ using Avalonia.Controls;
using FluentAvalonia.UI.Controls;
using ReactiveUI;
namespace Artemis.UI
namespace Artemis.UI.Shared
{
/// <summary>
/// A ReactiveUI <see cref="Window" /> that implements the <see cref="IViewFor{TViewModel}" /> interface and will

View File

@ -0,0 +1,54 @@
using System.Threading.Tasks;
using Avalonia.Controls;
namespace Artemis.UI.Shared.Services.Builders;
/// <summary>
/// Represents a builder that can create a <see cref="OpenFolderDialog" />.
/// </summary>
public class OpenFolderDialogBuilder
{
private readonly OpenFolderDialog _openFolderDialog;
private readonly Window _parent;
/// <summary>
/// Creates a new instance of the <see cref="OpenFolderDialogBuilder" /> class.
/// </summary>
/// <param name="parent">The parent window that will host the dialog.</param>
internal OpenFolderDialogBuilder(Window parent)
{
_parent = parent;
_openFolderDialog = new OpenFolderDialog();
}
/// <summary>
/// Set the title of the dialog
/// </summary>
public OpenFolderDialogBuilder WithTitle(string? title)
{
_openFolderDialog.Title = title;
return this;
}
/// <summary>
/// Set the initial directory of the dialog
/// </summary>
public OpenFolderDialogBuilder WithDirectory(string? directory)
{
_openFolderDialog.Directory = directory;
return this;
}
/// <summary>
/// Asynchronously shows the folder dialog.
/// </summary>
/// <returns>
/// A task that on completion returns an array containing the full path to the selected
/// folder, or null if the dialog was canceled.
/// </returns>
public async Task<string?> ShowAsync()
{
return await _openFolderDialog.ShowAsync(_parent);
}
}

View File

@ -22,7 +22,7 @@ namespace Artemis.UI.Shared.Services
/// Given a ViewModel, show its corresponding View as a window
/// </summary>
/// <param name="viewModel">ViewModel to show the View for</param>
void ShowWindow(object viewModel);
Window ShowWindow(object viewModel);
/// <summary>
/// Shows a dialog displaying the given exception
@ -61,6 +61,12 @@ namespace Artemis.UI.Shared.Services
/// </returns>
Task<bool> ShowConfirmContentDialog(string title, string message, string confirm = "Confirm", string? cancel = "Cancel");
/// <summary>
/// Creates an open folder dialog, use the fluent API to configure it
/// </summary>
/// <returns>The builder that can be used to configure the dialog</returns>
OpenFolderDialogBuilder CreateOpenFolderDialog();
/// <summary>
/// Creates an open file dialog, use the fluent API to configure it
/// </summary>

View File

@ -30,7 +30,7 @@ namespace Artemis.UI.Shared.Services
return viewModel;
}
public void ShowWindow(object viewModel)
public Window ShowWindow(object viewModel)
{
Window? parent = GetCurrentWindow();
@ -49,6 +49,8 @@ namespace Artemis.UI.Shared.Services
window.Show(parent);
else
window.Show();
return window;
}
public async Task<TResult> ShowDialogAsync<TViewModel, TResult>(params (string name, object? value)[] parameters) where TViewModel : DialogViewModelBase<TResult>
@ -86,7 +88,6 @@ namespace Artemis.UI.Shared.Services
Window window = (Window) Activator.CreateInstance(type)!;
window.DataContext = viewModel;
viewModel.CloseRequested += (_, args) => window.Close(args.Result);
viewModel.CancelRequested += (_, _) => window.Close();
return await window.ShowDialog<TResult>(parent);
}
@ -118,6 +119,14 @@ namespace Artemis.UI.Shared.Services
throw new ArtemisSharedUIException("Can't show a content dialog without any windows being shown.");
return new ContentDialogBuilder(_kernel, currentWindow);
}
public OpenFolderDialogBuilder CreateOpenFolderDialog()
{
Window? currentWindow = GetCurrentWindow();
if (currentWindow == null)
throw new ArtemisSharedUIException("Can't show an open folder dialog without any windows being shown.");
return new OpenFolderDialogBuilder(currentWindow);
}
public OpenFileDialogBuilder CreateOpenFileDialog()
{

View File

@ -19,7 +19,7 @@
<Setter Property="MaxHeight" Value="0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="MaxHeight" Value="50" />
<Setter Property="MaxHeight" Value="600" />
</KeyFrame>
</Animation>
</Style.Animations>

View File

@ -51,17 +51,8 @@ public abstract class DialogViewModelBase<TResult> : ValidatableViewModelBase
{
CloseRequested?.Invoke(this, new DialogClosedEventArgs<TResult>(result));
}
/// <summary>
/// Closes the dialog without a result
/// </summary>
public void Cancel()
{
CancelRequested?.Invoke(this, EventArgs.Empty);
}
internal event EventHandler<DialogClosedEventArgs<TResult>>? CloseRequested;
internal event EventHandler? CancelRequested;
}
/// <summary>

View File

@ -2,6 +2,7 @@ using Artemis.Core.Services;
using Artemis.UI.Windows.Ninject;
using Artemis.UI.Windows.Providers.Input;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
@ -22,9 +23,9 @@ namespace Artemis.UI.Windows
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || Design.IsDesignMode)
return;
ArtemisBootstrapper.Initialize();
_applicationStateManager = new ApplicationStateManager(_kernel!, desktop.Args);
RegisterProviders(_kernel!);

View File

@ -8,44 +8,44 @@
<Platforms>x64</Platforms>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**"/>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Artemis.UI.Avalonia.csproj.DotSettings"/>
<None Remove="Artemis.UI.csproj.DotSettings"/>
<None Remove="Assets\Images\Logo\application.ico"/>
<None Remove="Artemis.UI.Avalonia.csproj.DotSettings" />
<None Remove="Artemis.UI.csproj.DotSettings" />
<None Remove="Assets\Images\Logo\application.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="0.10.15"/>
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="10.14.0"/>
<PackageReference Include="Avalonia.Desktop" Version="0.10.15"/>
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.15"/>
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.15"/>
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="0.10.14"/>
<PackageReference Include="DynamicData" Version="7.8.6"/>
<PackageReference Include="FluentAvaloniaUI" Version="1.4.1"/>
<PackageReference Include="Flurl.Http" Version="3.2.4"/>
<PackageReference Include="Live.Avalonia" Version="1.3.1"/>
<PackageReference Include="Material.Icons.Avalonia" Version="1.0.2"/>
<PackageReference Include="ReactiveUI" Version="17.1.50"/>
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1"/>
<PackageReference Include="RGB.NET.Core" Version="1.0.0-prerelease.32"/>
<PackageReference Include="RGB.NET.Layout" Version="1.0.0-prerelease.32"/>
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.1"/>
<PackageReference Include="Splat.Ninject" Version="14.1.45"/>
<PackageReference Include="Avalonia" Version="0.10.15" />
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="10.14.0" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.15" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.15" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.15" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="0.10.14" />
<PackageReference Include="DynamicData" Version="7.8.6" />
<PackageReference Include="FluentAvaloniaUI" Version="1.4.1" />
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Live.Avalonia" Version="1.3.1" />
<PackageReference Include="Material.Icons.Avalonia" Version="1.0.2" />
<PackageReference Include="ReactiveUI" Version="17.1.50" />
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
<PackageReference Include="RGB.NET.Core" Version="1.0.0-prerelease.32" />
<PackageReference Include="RGB.NET.Layout" Version="1.0.0-prerelease.32" />
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.1" />
<PackageReference Include="Splat.Ninject" Version="14.1.45" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj"/>
<ProjectReference Include="..\Artemis.UI.Shared\Artemis.UI.Shared.csproj"/>
<ProjectReference Include="..\Artemis.VisualScripting\Artemis.VisualScripting.csproj"/>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.UI.Shared\Artemis.UI.Shared.csproj" />
<ProjectReference Include="..\Artemis.VisualScripting\Artemis.VisualScripting.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="Assets\Images\Logo\bow-white.ico"/>
<Content Include="Assets\Images\Logo\bow.ico"/>
<Content Include="Assets\Images\Logo\bow-white.ico" />
<Content Include="Assets\Images\Logo\bow.ico" />
</ItemGroup>
<ItemGroup>
<Resource Include="Assets\Images\Logo\bow-black.ico"/>
<Resource Include="Assets\Images\Logo\bow-white.ico"/>
<Resource Include="Assets\Images\Logo\bow-black.ico" />
<Resource Include="Assets\Images\Logo\bow-white.ico" />
</ItemGroup>
<ItemGroup>
<Compile Update="DefaultTypes\PropertyInput\StringPropertyInputView.axaml.cs">

View File

@ -1,5 +1,6 @@
using System;
using Artemis.UI.Screens.Root;
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;

View File

@ -1,6 +1,7 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Events;
using Avalonia;
using Avalonia.Controls;

View File

@ -4,8 +4,10 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:plugins="clr-namespace:Artemis.UI.Screens.Plugins"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Plugins.PluginFeatureView">
x:Class="Artemis.UI.Screens.Plugins.PluginFeatureView"
x:DataType="plugins:PluginFeatureViewModel">
<Grid ColumnDefinitions="30,*,Auto">
<Grid.ContextFlyout>
<MenuFlyout>
@ -24,14 +26,15 @@
<!-- Icon column -->
<shared:ArtemisIcon Grid.Column="0"
Icon="{Binding FeatureInfo.ResolvedIcon}"
Icon="{CompiledBinding FeatureInfo.ResolvedIcon}"
Fill="False"
Width="20"
Height="20"
IsVisible="{Binding LoadException, Converter={x:Static ObjectConverters.IsNull}}" />
IsVisible="{CompiledBinding LoadException, Converter={x:Static ObjectConverters.IsNull}}" />
<Button Grid.Column="0"
Classes="AppBarButton icon-button"
IsVisible="{Binding LoadException, Converter={x:Static ObjectConverters.IsNotNull}}"
IsVisible="{CompiledBinding LoadException, Converter={x:Static ObjectConverters.IsNotNull}}"
Foreground="#E74C4C"
ToolTip.Tip="An exception occurred while enabling this feature, click to view"
Command="{Binding ViewLoadException}">
@ -40,15 +43,15 @@
<!-- Display name column -->
<TextBlock Grid.Column="1"
Text="{Binding FeatureInfo.Name}"
Text="{CompiledBinding FeatureInfo.Name}"
TextWrapping="Wrap"
VerticalAlignment="Center"
ToolTip.Tip="{Binding FeatureInfo.Description}" />
ToolTip.Tip="{CompiledBinding FeatureInfo.Description}" />
<!-- Enable toggle column -->
<StackPanel Grid.Column="2"
HorizontalAlignment="Right"
IsVisible="{Binding !Enabling}"
IsVisible="{CompiledBinding !Enabling}"
Orientation="Horizontal"
ToolTip.Tip="This feature cannot be disabled without disabling the whole plugin">
<avalonia:MaterialIcon Kind="ShieldHalfFull"
@ -57,12 +60,12 @@
Margin="0 0 5 0"
IsVisible="{Binding ShowShield}" />
<CheckBox IsChecked="{Binding IsEnabled}" IsEnabled="{Binding CanToggleEnabled}">
Feature enabled
<CheckBox IsChecked="{CompiledBinding IsEnabled}" IsEnabled="{CompiledBinding CanToggleEnabled}">
Enable feature
</CheckBox>
</StackPanel>
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="7" IsVisible="{Binding Enabling}">
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="7" IsVisible="{CompiledBinding Enabling}">
<ProgressBar Value="0" IsIndeterminate="True" />
</StackPanel>

View File

@ -71,7 +71,7 @@ namespace Artemis.UI.Screens.Plugins
public bool IsEnabled
{
get => FeatureInfo.Instance != null && FeatureInfo.Instance.IsEnabled;
get => FeatureInfo.AlwaysEnabled || FeatureInfo.Instance != null && FeatureInfo.Instance.IsEnabled;
set => Dispatcher.UIThread.InvokeAsync(() => UpdateEnabled(value));
}

View File

@ -0,0 +1,11 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:plugins="clr-namespace:Artemis.UI.Screens.Plugins"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Plugins.PluginPlatformView"
x:DataType="plugins:PluginPlatformViewModel">
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" ToolTip.Tip="{CompiledBinding DisplayName}"/>
</UserControl>

View File

@ -0,0 +1,18 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Plugins;
public partial class PluginPlatformView : UserControl
{
public PluginPlatformView()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,16 @@
using Material.Icons;
using ReactiveUI;
namespace Artemis.UI.Screens.Plugins;
public class PluginPlatformViewModel : ReactiveObject
{
public PluginPlatformViewModel(string displayName, MaterialIconKind icon)
{
DisplayName = displayName;
Icon = icon;
}
public string DisplayName { get; set; }
public MaterialIconKind Icon { get; set; }
}

View File

@ -9,10 +9,11 @@
mc:Ignorable="d" d:DesignWidth="900" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Plugins.PluginSettingsView"
x:DataType="plugins:PluginSettingsViewModel">
<Border Classes="card" Padding="15" Margin="0 5">
<Border Classes="card" Padding="15" Margin="0 5">
<Grid RowDefinitions="*,Auto" ColumnDefinitions="4*,5*">
<Grid Grid.Row="0" RowDefinitions="Auto,Auto,*" ColumnDefinitions="80,*">
<shared:ArtemisIcon Icon="{CompiledBinding Plugin.Info.ResolvedIcon}"
<Grid Grid.Row="0" RowDefinitions="Auto,Auto,*" ColumnDefinitions="80,*, Auto">
<shared:ArtemisIcon Icon="{CompiledBinding Plugin.Info.ResolvedIcon}"
Fill="False"
Width="48"
Height="48"
Margin="0 5 0 0"
@ -22,13 +23,23 @@
<TextBlock Grid.Column="1" Grid.Row="0" Classes="h5 no-margin" Text="{CompiledBinding Plugin.Info.Name}" />
<ItemsControl Grid.Column="2" Grid.Row="0" IsVisible="{CompiledBinding Platforms.Count}" Items="{CompiledBinding Platforms}" HorizontalAlignment="Right">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="5" Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<TextBlock Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="1"
Classes="subtitle"
Text="{CompiledBinding Plugin.Info.Author}"
IsVisible="{CompiledBinding Plugin.Info.Author, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
<TextBlock Grid.Column="1"
Grid.ColumnSpan="2"
Grid.Row="2"
TextWrapping="Wrap"
Margin="0 5"
@ -88,13 +99,15 @@
</controls:HyperlinkButton>
</StackPanel>
<CheckBox Grid.Row="0"
<CheckBox Name="EnabledToggle"
Grid.Row="0"
Grid.Column="1"
HorizontalAlignment="Right"
IsVisible="{CompiledBinding !Enabling}"
IsChecked="{CompiledBinding IsEnabled}">
IsChecked="{CompiledBinding IsEnabled, Mode=OneWay}"
IsEnabled="{CompiledBinding Plugin.Info.IsCompatible}">
<StackPanel x:Name="EnableText" Orientation="Horizontal">
<TextBlock>Plugin enabled</TextBlock>
<TextBlock>Enable plugin</TextBlock>
<avalonia:MaterialIcon Kind="ShieldHalfFull"
Margin="5 0 0 0"
ToolTip.Tip="Plugin requires admin rights"

View File

@ -1,18 +1,30 @@
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<PluginSettingsViewModel>
{
private readonly CheckBox _enabledToggle;
public PluginSettingsView()
{
InitializeComponent();
_enabledToggle = this.Find<CheckBox>("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));
}
}
}
}

View File

@ -12,326 +12,347 @@ 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
namespace Artemis.UI.Screens.Plugins;
public class PluginSettingsViewModel : ActivatableViewModelBase
{
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;
public PluginSettingsViewModel(Plugin plugin,
ISettingsVmFactory settingsVmFactory,
ICoreService coreService,
IWindowService windowService,
INotificationService notificationService,
IPluginManagementService pluginManagementService)
{
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;
_plugin = plugin;
public PluginSettingsViewModel(Plugin plugin,
ISettingsVmFactory settingsVmFactory,
ICoreService coreService,
IWindowService windowService,
INotificationService notificationService,
IPluginManagementService pluginManagementService)
_settingsVmFactory = settingsVmFactory;
_coreService = coreService;
_windowService = windowService;
_notificationService = notificationService;
_pluginManagementService = pluginManagementService;
PluginFeatures = new ObservableCollection<PluginFeatureViewModel>();
foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features)
PluginFeatures.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false));
Platforms = new ObservableCollection<PluginPlatformViewModel>();
if (Plugin.Info.Platforms != null)
{
_plugin = plugin;
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));
}
_settingsVmFactory = settingsVmFactory;
_coreService = coreService;
_windowService = windowService;
_notificationService = notificationService;
_pluginManagementService = pluginManagementService;
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<bool>(ExecuteRemovePrerequisites, this.WhenAnyValue(x => x.CanRemovePrerequisites));
ShowLogsFolder = ReactiveCommand.Create(ExecuteShowLogsFolder);
OpenPluginDirectory = ReactiveCommand.Create(ExecuteOpenPluginDirectory);
PluginFeatures = new ObservableCollection<PluginFeatureViewModel>();
foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features)
PluginFeatures.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false));
Reload = ReactiveCommand.CreateFromTask(ExecuteReload);
OpenSettings = ReactiveCommand.Create(ExecuteOpenSettings, this.WhenAnyValue(x => x.IsEnabled).Select(isEnabled => isEnabled && Plugin.ConfigurationDialog != null));
RemoveSettings = ReactiveCommand.CreateFromTask(ExecuteRemoveSettings);
Remove = ReactiveCommand.CreateFromTask(ExecuteRemove);
InstallPrerequisites = ReactiveCommand.CreateFromTask(ExecuteInstallPrerequisites, this.WhenAnyValue(x => x.CanInstallPrerequisites));
RemovePrerequisites = ReactiveCommand.CreateFromTask<bool>(ExecuteRemovePrerequisites, this.WhenAnyValue(x => x.CanRemovePrerequisites));
ShowLogsFolder = ReactiveCommand.Create(ExecuteShowLogsFolder);
OpenPluginDirectory = ReactiveCommand.Create(ExecuteOpenPluginDirectory);
this.WhenActivated(d =>
this.WhenActivated(d =>
{
Plugin.Enabled += OnPluginToggled;
Plugin.Disabled += OnPluginToggled;
Disposable.Create(() =>
{
_pluginManagementService.PluginDisabled += PluginManagementServiceOnPluginToggled;
_pluginManagementService.PluginEnabled += PluginManagementServiceOnPluginToggled;
Plugin.Enabled -= OnPluginToggled;
Plugin.Disabled -= OnPluginToggled;
_window?.Close();
}).DisposeWith(d);
});
}
Disposable.Create(() =>
{
_pluginManagementService.PluginDisabled -= PluginManagementServiceOnPluginToggled;
_pluginManagementService.PluginEnabled -= PluginManagementServiceOnPluginToggled;
}).DisposeWith(d);
});
}
public ReactiveCommand<Unit, Unit> Reload { get; }
public ReactiveCommand<Unit, Unit> OpenSettings { get; }
public ReactiveCommand<Unit, Unit> RemoveSettings { get; }
public ReactiveCommand<Unit, Unit> Remove { get; }
public ReactiveCommand<Unit, Unit> InstallPrerequisites { get; }
public ReactiveCommand<bool, Unit> RemovePrerequisites { get; }
public ReactiveCommand<Unit, Unit> ShowLogsFolder { get; }
public ReactiveCommand<Unit, Unit> OpenPluginDirectory { get; }
public ReactiveCommand<Unit,Unit> Reload { get; }
public ReactiveCommand<Unit, Unit> OpenSettings { get; }
public ReactiveCommand<Unit,Unit> RemoveSettings { get; }
public ReactiveCommand<Unit,Unit> Remove { get;}
public ReactiveCommand<Unit, Unit> InstallPrerequisites { get; }
public ReactiveCommand<bool, Unit> RemovePrerequisites { get; }
public ReactiveCommand<Unit,Unit> ShowLogsFolder { get; }
public ReactiveCommand<Unit,Unit> OpenPluginDirectory { get; }
public ObservableCollection<PluginFeatureViewModel> PluginFeatures { get; }
public ObservableCollection<PluginFeatureViewModel> PluginFeatures { get; }
public ObservableCollection<PluginPlatformViewModel> Platforms { get; }
public Plugin Plugin
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
{
get => _plugin;
set => RaiseAndSetIfChanged(ref _plugin, value);
if (!RaiseAndSetIfChanged(ref _isSettingsPopupOpen, value)) return;
CheckPrerequisites();
}
}
public bool Enabling
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)
{
get => _enabling;
set => RaiseAndSetIfChanged(ref _enabling, value);
_window.WindowState = WindowState.Normal;
_window.Activate();
return;
}
public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name;
public bool IsEnabled
try
{
get => Plugin.IsEnabled;
set => Task.Run(() => UpdateEnabled(value));
}
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)}");
public bool IsSettingsPopupOpen
_window = _windowService.ShowWindow(new PluginSettingsWindowViewModel(viewModel));
_window.Closed += (_, _) => _window = null;
}
catch (Exception e)
{
get => _isSettingsPopupOpen;
set
{
if (!RaiseAndSetIfChanged(ref _isSettingsPopupOpen, value)) return;
CheckPrerequisites();
}
_windowService.ShowExceptionDialog("An exception occured while trying to show the plugin's settings window", e);
throw;
}
}
public bool CanInstallPrerequisites
private void ExecuteOpenPluginDirectory()
{
try
{
get => _canInstallPrerequisites;
set => RaiseAndSetIfChanged(ref _canInstallPrerequisites, value);
Utilities.OpenFolder(Plugin.Directory.FullName);
}
public bool CanRemovePrerequisites
catch (Exception e)
{
get => _canRemovePrerequisites;
set => RaiseAndSetIfChanged(ref _canRemovePrerequisites, value);
_windowService.ShowExceptionDialog("Welp, we couldn't open the device's plugin folder for you", e);
}
}
private void ExecuteOpenSettings()
private async Task ExecuteReload()
{
bool wasEnabled = 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));
if (wasEnabled)
await UpdateEnabled(true);
_notificationService.CreateNotification().WithTitle("Reloaded plugin.").Show();
}
private async Task ExecuteInstallPrerequisites()
{
List<IPrerequisitesSubject> 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<IPrerequisitesSubject> 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())))
{
if (Plugin.ConfigurationDialog == null)
return;
await PluginPrerequisitesUninstallDialogViewModel.Show(_windowService, subjects, forPluginRemoval ? "Skip, remove plugin" : "Cancel");
}
}
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)}");
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;
_windowService.ShowWindow(new PluginSettingsWindowViewModel(viewModel));
}
catch (Exception e)
{
_windowService.ShowExceptionDialog("An exception occured while trying to show the plugin's settings window", e);
throw;
}
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<IPrerequisitesSubject> 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;
}
private void ExecuteOpenPluginDirectory()
_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
{
Utilities.OpenFolder(Plugin.Directory.FullName);
await Task.Run(() => _pluginManagementService.DisablePlugin(Plugin, true));
}
catch (Exception e)
{
_windowService.ShowExceptionDialog("Welp, we couldn't open the device's plugin folder for you", 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;
}
private async Task ExecuteReload()
try
{
bool wasEnabled = IsEnabled;
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;
}
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));
if (wasEnabled)
await UpdateEnabled(true);
_notificationService.CreateNotification().WithTitle("Reloaded plugin.").Show();
}
private async Task ExecuteInstallPrerequisites()
{
// Check if all prerequisites are met async
List<IPrerequisitesSubject> subjects = new() {Plugin.Info};
subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled));
subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled || f.EnabledInStorage));
if (subjects.Any(s => s.PlatformPrerequisites.Any()))
if (subjects.Any(s => !s.ArePrerequisitesMet()))
{
await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects);
}
if (!subjects.All(s => s.ArePrerequisitesMet()))
return;
}
private async Task ExecuteRemovePrerequisites(bool forPluginRemoval = false)
await Task.Run(() => _pluginManagementService.EnablePlugin(Plugin, true, true));
}
catch (Exception e)
{
List<IPrerequisitesSubject> 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");
this.RaisePropertyChanged(nameof(IsEnabled));
}
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());
}
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<IPrerequisitesSubject> 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);
}
}
private void PluginManagementServiceOnPluginToggled(object? sender, PluginEventArgs e)
{
this.RaisePropertyChanged(nameof(IsEnabled));
}
private async Task UpdateEnabled(bool enable)
{
if (IsEnabled == enable)
{
this.RaisePropertyChanged(nameof(IsEnabled));
return;
}
if (enable)
{
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)
{
CancelEnable();
return;
}
}
// Check if all prerequisites are met async
List<IPrerequisitesSubject> 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()))
{
CancelEnable();
return;
}
}
await Task.Run(async () =>
{
try
{
_pluginManagementService.EnablePlugin(Plugin, true, true);
}
catch (Exception e)
{
await Dispatcher.UIThread.InvokeAsync(() => _notificationService.CreateNotification()
.WithMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}")
.HavingButton(b => b.WithText("View logs").WithCommand(ShowLogsFolder))
.Show());
}
finally
{
Enabling = false;
}
});
}
else
{
_pluginManagementService.DisablePlugin(Plugin, true);
}
this.RaisePropertyChanged(nameof(IsEnabled));
}
private void CancelEnable()
finally
{
Enabling = false;
this.RaisePropertyChanged(nameof(IsEnabled));
}
}
private void CheckPrerequisites()
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(() =>
{
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()));
}
this.RaisePropertyChanged(nameof(IsEnabled));
if (!IsEnabled)
_window?.Close();
});
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;

View File

@ -1,4 +1,5 @@
using Avalonia;
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using FluentAvalonia.UI.Controls;

View File

@ -1,4 +1,5 @@
using System.ComponentModel;
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Markup.Xaml;

View File

@ -1,4 +1,5 @@
using System.ComponentModel;
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Markup.Xaml;

View File

@ -1,4 +1,5 @@
using System.ComponentModel;
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

View File

@ -4,12 +4,8 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Settings.DevicesTabView">
<Grid MaxWidth="1050">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel>
<Grid RowDefinitions="Auto,*">
<StackPanel MaxWidth="1050">
<TextBlock Classes="h4">Device management</TextBlock>
<TextBlock>
Below you view and manage the devices that were detected by Artemis.
@ -17,10 +13,10 @@
<TextBlock>
Disabling a device will cause it to stop updating. Some SDKs will even go back to using manufacturer lighting (Artemis restart may be required).
</TextBlock>
</StackPanel>
</StackPanel>
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl Items="{Binding Devices}" Margin="-5 0">
<ItemsControl Items="{Binding Devices}" Margin="-5 0" MaxWidth="1050">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />

View File

@ -6,11 +6,18 @@
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Settings.PluginsTabView"
x:DataType="settings:PluginsTabViewModel">
<Grid RowDefinitions="Auto,*" ColumnDefinitions="*,*" Width="900">
<TextBox Grid.Row="0" Grid.Column="0" Classes="clearButton" Text="{CompiledBinding SearchPluginInput}" Watermark="Search plugins" Margin="0 10" />
<Button Grid.Row="0" Grid.Column="1" Classes="accent" Command="{CompiledBinding ImportPlugin}" HorizontalAlignment="Right">Import plugin</Button>
<ScrollViewer Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl Items="{CompiledBinding Plugins}" />
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" Grid.Column="0" MaxWidth="900" Margin="0 10">
<Grid.ColumnDefinitions>
<ColumnDefinition MinWidth="165" MaxWidth="400"/>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<TextBox Classes="clearButton" Text="{CompiledBinding SearchPluginInput}" Watermark="Search plugins" Margin="0 0 10 0"/>
<Button Grid.Row="0" Grid.Column="1" Classes="accent" Command="{CompiledBinding ImportPlugin}" HorizontalAlignment="Right">Import plugin</Button>
</Grid>
<ScrollViewer Grid.Row="1" Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl Items="{CompiledBinding Plugins}" MaxWidth="900" VerticalAlignment="Center"/>
</ScrollViewer>
</Grid>
</UserControl>

View File

@ -90,7 +90,7 @@ namespace Artemis.UI.Screens.Settings
// Enable it via the VM to enable the prerequisite dialog
PluginSettingsViewModel? pluginViewModel = Plugins.FirstOrDefault(i => i.Plugin == plugin);
if (pluginViewModel is {IsEnabled: false})
pluginViewModel.IsEnabled = true;
await pluginViewModel.UpdateEnabled(true);
_notificationService.CreateNotification()
.WithTitle("Plugin imported")

View File

@ -1,5 +1,6 @@
using System;
using System.Reactive.Disposables;
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

View File

@ -1,3 +1,4 @@
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Markup.Xaml;