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

Plugin info - Implemented property changed

Plugins - Improved enable failure detection
Plugins UI - Show an indicator on plugins that failed to enable
Plugins UI - Show a progress indicator on plugins that are enabling
UI - Added reusable Snackbar (not the Dutch kind with kroketten)
This commit is contained in:
Robert 2020-06-25 19:25:58 +02:00
parent 28bcfcc95a
commit a47eedf1c2
18 changed files with 399 additions and 179 deletions

View File

@ -52,13 +52,30 @@ namespace Artemis.Core.Plugins.Abstract
if (enable && !Enabled) if (enable && !Enabled)
{ {
Enabled = true; Enabled = true;
EnablePlugin(); PluginInfo.Enabled = true;
// If enable failed, put it back in a disabled state
try
{
EnablePlugin();
}
catch
{
Enabled = false;
PluginInfo.Enabled = false;
throw;
}
OnPluginEnabled(); OnPluginEnabled();
} }
else if (!enable && Enabled) else if (!enable && Enabled)
{ {
Enabled = false; Enabled = false;
PluginInfo.Enabled = false;
// Even if disable failed, still leave it in a disabled state to avoid more issues
DisablePlugin(); DisablePlugin();
OnPluginDisabled(); OnPluginDisabled();
} }
} }

View File

@ -5,11 +5,24 @@ using Artemis.Core.Plugins.Abstract;
using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Plugins;
using McMaster.NETCore.Plugins; using McMaster.NETCore.Plugins;
using Newtonsoft.Json; using Newtonsoft.Json;
using Stylet;
namespace Artemis.Core.Plugins.Models namespace Artemis.Core.Plugins.Models
{ {
public class PluginInfo [JsonObject(MemberSerialization.OptIn)]
public class PluginInfo : PropertyChangedBase
{ {
private Guid _guid;
private string _name;
private string _description;
private string _icon;
private Version _version;
private string _main;
private DirectoryInfo _directory;
private Plugin _instance;
private bool _enabled;
private bool _lastEnableSuccessful;
internal PluginInfo() internal PluginInfo()
{ {
} }
@ -18,77 +31,125 @@ namespace Artemis.Core.Plugins.Models
/// The plugins GUID /// The plugins GUID
/// </summary> /// </summary>
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public Guid Guid { get; internal set; } public Guid Guid
{
get => _guid;
internal set => SetAndNotify(ref _guid, value);
}
/// <summary> /// <summary>
/// The name of the plugin /// The name of the plugin
/// </summary> /// </summary>
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public string Name { get; internal set; } public string Name
{
get => _name;
internal set => SetAndNotify(ref _name, value);
}
/// <summary> /// <summary>
/// A short description of the plugin /// A short description of the plugin
/// </summary> /// </summary>
public string Description { get; set; } [JsonProperty]
public string Description
{
get => _description;
set => SetAndNotify(ref _description, value);
}
/// <summary> /// <summary>
/// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for /// The plugins display icon that's shown in the settings see <see href="https://materialdesignicons.com" /> for
/// available /// available
/// icons /// icons
/// </summary> /// </summary>
public string Icon { get; set; } [JsonProperty]
public string Icon
{
get => _icon;
set => SetAndNotify(ref _icon, value);
}
/// <summary> /// <summary>
/// The version of the plugin /// The version of the plugin
/// </summary> /// </summary>
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public Version Version { get; internal set; } public Version Version
{
get => _version;
internal set => SetAndNotify(ref _version, value);
}
/// <summary> /// <summary>
/// The main entry DLL, should contain a class implementing Plugin /// The main entry DLL, should contain a class implementing Plugin
/// </summary> /// </summary>
[JsonProperty(Required = Required.Always)] [JsonProperty(Required = Required.Always)]
public string Main { get; internal set; } public string Main
{
get => _main;
internal set => SetAndNotify(ref _main, value);
}
/// <summary> /// <summary>
/// The plugins root directory /// The plugins root directory
/// </summary> /// </summary>
[JsonIgnore] public DirectoryInfo Directory
public DirectoryInfo Directory { get; internal set; } {
get => _directory;
internal set => SetAndNotify(ref _directory, value);
}
/// <summary> /// <summary>
/// A reference to the type implementing Plugin, available after successful load /// A reference to the type implementing Plugin, available after successful load
/// </summary> /// </summary>
[JsonIgnore] public Plugin Instance
public Plugin Instance { get; internal set; } {
get => _instance;
internal set => SetAndNotify(ref _instance, value);
}
/// <summary> /// <summary>
/// Indicates whether the user enabled the plugin or not /// Indicates whether the user enabled the plugin or not
/// </summary> /// </summary>
[JsonIgnore] public bool Enabled
public bool Enabled { get; internal set; } {
get => _enabled;
internal set => SetAndNotify(ref _enabled, value);
}
/// <summary>
/// Indicates whether the last time the plugin loaded, it loaded correctly
/// </summary>
public bool LastEnableSuccessful
{
get => _lastEnableSuccessful;
internal set => SetAndNotify(ref _lastEnableSuccessful, value);
}
/// <summary> /// <summary>
/// The PluginLoader backing this plugin /// The PluginLoader backing this plugin
/// </summary> /// </summary>
[JsonIgnore]
internal PluginLoader PluginLoader { get; set; } internal PluginLoader PluginLoader { get; set; }
/// <summary> /// <summary>
/// The assembly the plugin code lives in /// The assembly the plugin code lives in
/// </summary> /// </summary>
[JsonIgnore]
internal Assembly Assembly { get; set; } internal Assembly Assembly { get; set; }
/// <summary> /// <summary>
/// The entity representing the plugin /// The entity representing the plugin
/// </summary> /// </summary>
[JsonIgnore]
internal PluginEntity PluginEntity { get; set; } internal PluginEntity PluginEntity { get; set; }
public override string ToString() public override string ToString()
{ {
return $"{nameof(Guid)}: {Guid}, {nameof(Name)}: {Name}, {nameof(Version)}: {Version}"; return $"{Name} v{Version} - {Guid}";
}
internal void ApplyToEntity()
{
PluginEntity.Id = Guid;
PluginEntity.IsEnabled = Enabled;
PluginEntity.LastEnableSuccessful = LastEnableSuccessful;
} }
} }
} }

View File

@ -137,8 +137,6 @@ namespace Artemis.Core.Services
var pluginInfo = JsonConvert.DeserializeObject<PluginInfo>(File.ReadAllText(metadataFile)); var pluginInfo = JsonConvert.DeserializeObject<PluginInfo>(File.ReadAllText(metadataFile));
pluginInfo.Directory = subDirectory; pluginInfo.Directory = subDirectory;
_logger.Debug("Loading plugin {pluginInfo}", pluginInfo);
OnPluginLoading(new PluginEventArgs(pluginInfo));
LoadPlugin(pluginInfo); LoadPlugin(pluginInfo);
} }
catch (Exception e) catch (Exception e)
@ -150,38 +148,22 @@ namespace Artemis.Core.Services
// Activate plugins after they are all loaded // Activate plugins after they are all loaded
foreach (var pluginInfo in _plugins.Where(p => p.Enabled)) foreach (var pluginInfo in _plugins.Where(p => p.Enabled))
{ {
if (!pluginInfo.PluginEntity.LastEnableSuccessful) if (!pluginInfo.LastEnableSuccessful)
{ {
pluginInfo.Enabled = false; pluginInfo.Enabled = false;
_logger.Warning("Plugin failed to load last time, disabling it now to avoid instability. Plugin info: {pluginInfo}", pluginInfo); _logger.Warning("Plugin failed to load last time, disabling it now to avoid instability. Plugin info: {pluginInfo}", pluginInfo);
continue; continue;
} }
// Mark this as false until the plugin enabled successfully and save it in case the plugin drags us down into a crash
pluginInfo.PluginEntity.LastEnableSuccessful = false;
_pluginRepository.SavePlugin(pluginInfo.PluginEntity);
var threwException = false;
try try
{ {
_logger.Debug("Enabling plugin {pluginInfo}", pluginInfo); EnablePlugin(pluginInfo.Instance);
pluginInfo.Instance.SetEnabled(true);
} }
catch (Exception e) catch (Exception)
{ {
_logger.Warning(new ArtemisPluginException(pluginInfo, "Failed to enable plugin", e), "Plugin exception"); // ignored, logged in EnablePlugin
pluginInfo.Enabled = false;
threwException = true;
} }
// We got this far so the plugin enabled and we didn't crash horribly, yay
if (!threwException)
{
pluginInfo.PluginEntity.LastEnableSuccessful = true;
_pluginRepository.SavePlugin(pluginInfo.PluginEntity);
}
OnPluginEnabled(new PluginEventArgs(pluginInfo));
} }
LoadingPlugins = false; LoadingPlugins = false;
@ -213,16 +195,20 @@ namespace Artemis.Core.Services
{ {
lock (_plugins) lock (_plugins)
{ {
_logger.Debug("Loading plugin {pluginInfo}", pluginInfo);
OnPluginLoading(new PluginEventArgs(pluginInfo));
// Unload the plugin first if it is already loaded // Unload the plugin first if it is already loaded
if (_plugins.Contains(pluginInfo)) if (_plugins.Contains(pluginInfo))
UnloadPlugin(pluginInfo); UnloadPlugin(pluginInfo);
var pluginEntity = _pluginRepository.GetPluginByGuid(pluginInfo.Guid); var pluginEntity = _pluginRepository.GetPluginByGuid(pluginInfo.Guid);
if (pluginEntity == null) if (pluginEntity == null)
pluginEntity = new PluginEntity {PluginGuid = pluginInfo.Guid, IsEnabled = true, LastEnableSuccessful = true}; pluginEntity = new PluginEntity {Id = pluginInfo.Guid, IsEnabled = true, LastEnableSuccessful = true};
pluginInfo.PluginEntity = pluginEntity; pluginInfo.PluginEntity = pluginEntity;
pluginInfo.Enabled = pluginEntity.IsEnabled; pluginInfo.Enabled = pluginEntity.IsEnabled;
pluginInfo.LastEnableSuccessful = pluginEntity.LastEnableSuccessful;
var mainFile = Path.Combine(pluginInfo.Directory.FullName, pluginInfo.Main); var mainFile = Path.Combine(pluginInfo.Directory.FullName, pluginInfo.Main);
if (!File.Exists(mainFile)) if (!File.Exists(mainFile))
@ -310,28 +296,37 @@ namespace Artemis.Core.Services
public void EnablePlugin(Plugin plugin) public void EnablePlugin(Plugin plugin)
{ {
plugin.PluginInfo.Enabled = true; lock (_plugins)
plugin.PluginInfo.PluginEntity.IsEnabled = true; {
plugin.PluginInfo.PluginEntity.LastEnableSuccessful = false; _logger.Debug("Enabling plugin {pluginInfo}", plugin.PluginInfo);
_pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity);
var threwException = false; plugin.PluginInfo.LastEnableSuccessful = false;
try plugin.PluginInfo.ApplyToEntity();
{
plugin.SetEnabled(true);
}
catch (Exception e)
{
_logger.Warning(new ArtemisPluginException(plugin.PluginInfo, "Failed to enable plugin", e), "Plugin exception");
plugin.PluginInfo.Enabled = false;
threwException = true;
}
// We got this far so the plugin enabled and we didn't crash horribly, yay
if (!threwException)
{
plugin.PluginInfo.PluginEntity.LastEnableSuccessful = true;
_pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity); _pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity);
try
{
plugin.SetEnabled(true);
}
catch (Exception e)
{
_logger.Warning(new ArtemisPluginException(plugin.PluginInfo, "Exception during SetEnabled(true)", e), "Failed to enable plugin");
throw;
}
finally
{
// We got this far so the plugin enabled and we didn't crash horribly, yay
if (plugin.PluginInfo.Enabled)
{
plugin.PluginInfo.LastEnableSuccessful = true;
plugin.PluginInfo.ApplyToEntity();
_pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity);
_logger.Debug("Successfully enabled plugin {pluginInfo}", plugin.PluginInfo);
}
}
} }
OnPluginEnabled(new PluginEventArgs(plugin.PluginInfo)); OnPluginEnabled(new PluginEventArgs(plugin.PluginInfo));
@ -339,18 +334,29 @@ namespace Artemis.Core.Services
public void DisablePlugin(Plugin plugin) public void DisablePlugin(Plugin plugin)
{ {
plugin.PluginInfo.Enabled = false; lock (_plugins)
plugin.PluginInfo.PluginEntity.IsEnabled = false;
_pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity);
// Device providers cannot be disabled at runtime, restart the application
if (plugin is DeviceProvider)
{ {
CurrentProcessUtilities.Shutdown(2, true); _logger.Debug("Disabling plugin {pluginInfo}", plugin.PluginInfo);
return;
}
plugin.SetEnabled(false); // Device providers cannot be disabled at runtime, restart the application
if (plugin is DeviceProvider)
{
// Don't call SetEnabled(false) but simply update enabled state and save it
plugin.PluginInfo.Enabled = false;
plugin.PluginInfo.ApplyToEntity();
_pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity);
_logger.Debug("Shutting down for device provider disable {pluginInfo}", plugin.PluginInfo);
CurrentProcessUtilities.Shutdown(2, true);
return;
}
plugin.SetEnabled(false);
plugin.PluginInfo.ApplyToEntity();
_pluginRepository.SavePlugin(plugin.PluginInfo.PluginEntity);
_logger.Debug("Successfully disabled plugin {pluginInfo}", plugin.PluginInfo);
}
OnPluginDisabled(new PluginEventArgs(plugin.PluginInfo)); OnPluginDisabled(new PluginEventArgs(plugin.PluginInfo));
} }

View File

@ -8,7 +8,6 @@ namespace Artemis.Storage.Entities.Plugins
public class PluginEntity public class PluginEntity
{ {
public Guid Id { get; set; } public Guid Id { get; set; }
public Guid PluginGuid { get; set; }
public bool IsEnabled { get; set; } public bool IsEnabled { get; set; }
public bool LastEnableSuccessful { get; set; } public bool LastEnableSuccessful { get; set; }

View File

@ -3,7 +3,7 @@ using LiteDB;
namespace Artemis.Storage.Migrations namespace Artemis.Storage.Migrations
{ {
public class AttributeBasedPropertiesMigration : IStorageMigration public class M1AttributeBasedPropertiesMigration : IStorageMigration
{ {
public int UserVersion => 1; public int UserVersion => 1;

View File

@ -4,7 +4,7 @@ using LiteDB;
namespace Artemis.Storage.Migrations namespace Artemis.Storage.Migrations
{ {
public class ProfileEntitiesEnabledMigration : IStorageMigration public class M2ProfileEntitiesEnabledMigration : IStorageMigration
{ {
public int UserVersion => 2; public int UserVersion => 2;

View File

@ -0,0 +1,18 @@
using Artemis.Storage.Migrations.Interfaces;
using LiteDB;
namespace Artemis.Storage.Migrations
{
public class M3PluginEntitiesIndexChangesMigration : IStorageMigration
{
public int UserVersion => 3;
public void Apply(LiteRepository repository)
{
if (repository.Database.CollectionExists("PluginEntity"))
repository.Database.DropCollection("PluginEntity");
if (repository.Database.CollectionExists("PluginSettingEntity"))
repository.Database.DropCollection("PluginSettingEntity");
}
}
}

View File

@ -13,9 +13,7 @@ namespace Artemis.Storage.Repositories
{ {
_repository = repository; _repository = repository;
_repository.Database.GetCollection<PluginEntity>().EnsureIndex(s => s.PluginGuid); _repository.Database.GetCollection<PluginSettingEntity>().EnsureIndex(s => new {s.Name, s.PluginGuid}, true);
_repository.Database.GetCollection<PluginSettingEntity>().EnsureIndex(s => s.Name);
_repository.Database.GetCollection<PluginSettingEntity>().EnsureIndex(s => s.PluginGuid);
} }
public void AddPlugin(PluginEntity pluginEntity) public void AddPlugin(PluginEntity pluginEntity)
@ -25,12 +23,13 @@ namespace Artemis.Storage.Repositories
public PluginEntity GetPluginByGuid(Guid pluginGuid) public PluginEntity GetPluginByGuid(Guid pluginGuid)
{ {
return _repository.FirstOrDefault<PluginEntity>(p => p.PluginGuid == pluginGuid); return _repository.FirstOrDefault<PluginEntity>(p => p.Id == pluginGuid);
} }
public void SavePlugin(PluginEntity pluginEntity) public void SavePlugin(PluginEntity pluginEntity)
{ {
_repository.Upsert(pluginEntity); _repository.Upsert(pluginEntity);
_repository.Database.Checkpoint();
} }
public void AddSetting(PluginSettingEntity pluginSettingEntity) public void AddSetting(PluginSettingEntity pluginSettingEntity)

View File

@ -1,6 +1,7 @@
using System; using System;
using Artemis.UI.Shared.Ninject.Factories; using Artemis.UI.Shared.Ninject.Factories;
using Artemis.UI.Shared.Services.Interfaces; using Artemis.UI.Shared.Services.Interfaces;
using MaterialDesignThemes.Wpf;
using Ninject.Extensions.Conventions; using Ninject.Extensions.Conventions;
using Ninject.Modules; using Ninject.Modules;
@ -32,6 +33,8 @@ namespace Artemis.UI.Shared.Ninject
.BindAllInterfaces() .BindAllInterfaces()
.Configure(c => c.InSingletonScope()); .Configure(c => c.InSingletonScope());
}); });
Kernel.Bind<ISnackbarMessageQueue>().ToConstant(new SnackbarMessageQueue()).InSingletonScope();
} }
} }
} }

View File

@ -70,6 +70,10 @@
<Setter Property="IsTabStop" Value="False" /> <Setter Property="IsTabStop" Value="False" />
<Setter Property="Focusable" Value="False" /> <Setter Property="Focusable" Value="False" />
</Style> </Style>
<Style TargetType="GridSplitter" BasedOn="{StaticResource MaterialDesignGridSplitter}">
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Focusable" Value="False" />
</Style>
</ResourceDictionary> </ResourceDictionary>
</Application.Resources> </Application.Resources>
</Application> </Application>

View File

@ -13,6 +13,7 @@ using Artemis.UI.Screens.Module.ProfileEditor.ProfileTree.TreeItem;
using Artemis.UI.Screens.Module.ProfileEditor.Visualization; using Artemis.UI.Screens.Module.ProfileEditor.Visualization;
using Artemis.UI.Screens.Settings.Debug; using Artemis.UI.Screens.Settings.Debug;
using Artemis.UI.Screens.Settings.Tabs.Devices; using Artemis.UI.Screens.Settings.Tabs.Devices;
using Artemis.UI.Screens.Settings.Tabs.Plugins;
using Stylet; using Stylet;
namespace Artemis.UI.Ninject.Factories namespace Artemis.UI.Ninject.Factories
@ -26,6 +27,11 @@ namespace Artemis.UI.Ninject.Factories
ModuleRootViewModel Create(Module module); ModuleRootViewModel Create(Module module);
} }
public interface IPluginSettingsVmFactory : IVmFactory
{
PluginSettingsViewModel Create(Plugin plugin);
}
public interface IDeviceSettingsVmFactory : IVmFactory public interface IDeviceSettingsVmFactory : IVmFactory
{ {
DeviceSettingsViewModel Create(ArtemisDevice device); DeviceSettingsViewModel Create(ArtemisDevice device);

View File

@ -66,8 +66,9 @@
<RowDefinition Height="28" /> <RowDefinition Height="28" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition /> <ColumnDefinition Width="*" />
<ColumnDefinition /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="1.5*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Left side --> <!-- Left side -->
@ -157,8 +158,11 @@
</materialDesign:DialogHost> </materialDesign:DialogHost>
</Grid> </Grid>
<!-- Resize -->
<GridSplitter Grid.Row="0" Grid.RowSpan="2" Grid.Column="1" Width="15" HorizontalAlignment="Stretch" Cursor="SizeWE" Margin="-15 0" Background="Transparent" />
<!-- Right side --> <!-- Right side -->
<Grid Grid.Row="0" Grid.Column="1"> <Grid Grid.Row="0" Grid.Column="2">
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="56" /> <RowDefinition Height="56" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
@ -214,72 +218,124 @@
<!-- Bottom row, a bit hacky but has a ZIndex of 2 to cut off the time caret that overlaps the entire timeline --> <!-- Bottom row, a bit hacky but has a ZIndex of 2 to cut off the time caret that overlaps the entire timeline -->
<Grid Grid.Row="1" <Grid Grid.Row="1"
Grid.ColumnSpan="2"
Grid.Column="0" Grid.Column="0"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Panel.ZIndex="2" Panel.ZIndex="2"
Background="{DynamicResource MaterialDesignCardBackground}"> Background="{DynamicResource MaterialDesignCardBackground}">
<!-- Selected layer controls --> <!-- Selected layer controls -->
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid Grid.Column="0">
<Grid.ColumnDefinitions> <StackPanel Grid.Column="0"
<ColumnDefinition /> Orientation="Horizontal"
<ColumnDefinition Width="Auto" /> Margin="6"
</Grid.ColumnDefinitions> Visibility="{Binding SelectedLayer, Converter={StaticResource NullToVisibilityConverter}}">
<StackPanel Grid.Column="0" <materialDesign:PackIcon Kind="Layers" Width="16" />
Orientation="Horizontal" <materialDesign:PackIcon Kind="{Binding SelectedLayer.LayerBrush.Descriptor.Icon}"
Margin="6" Width="16"
Visibility="{Binding SelectedLayer, Converter={StaticResource NullToVisibilityConverter}}"> Margin="5 0 0 0"
<materialDesign:PackIcon Kind="Layers" Width="16" /> ToolTip="{Binding SelectedLayer.LayerBrush.Descriptor.DisplayName, Mode=OneWay}"
<materialDesign:PackIcon Kind="{Binding SelectedLayer.LayerBrush.Descriptor.Icon}" Background="Transparent"
Width="16" Visibility="{Binding SelectedLayer.LayerBrush, Converter={StaticResource NullToVisibilityConverter}}" />
Margin="5 0 0 0" <TextBlock Text="{Binding SelectedLayer.Name}" Margin="5 0 0 0" />
ToolTip="{Binding SelectedLayer.LayerBrush.Descriptor.DisplayName, Mode=OneWay}" </StackPanel>
Background="Transparent" <StackPanel Grid.Column="0"
Visibility="{Binding SelectedLayer.LayerBrush, Converter={StaticResource NullToVisibilityConverter}}" /> Orientation="Horizontal"
<TextBlock Text="{Binding SelectedLayer.Name}" Margin="5 0 0 0" /> Margin="6"
</StackPanel> Visibility="{Binding SelectedFolder, Converter={StaticResource NullToVisibilityConverter}}">
<StackPanel Grid.Column="0" <materialDesign:PackIcon Kind="Folder" Width="16" />
Orientation="Horizontal" <TextBlock Text="{Binding SelectedFolder.Name}" Margin="5 0 0 0" />
Margin="6" </StackPanel>
Visibility="{Binding SelectedFolder, Converter={StaticResource NullToVisibilityConverter}}"> <Button Grid.Column="1"
<materialDesign:PackIcon Kind="Folder" Width="16" /> Style="{StaticResource MaterialDesignFlatMidBgButton}"
<TextBlock Text="{Binding SelectedFolder.Name}" Margin="5 0 0 0" /> Margin="0 -2 5 -2"
</StackPanel> Padding="10 0"
<Button Grid.Column="1" Height="20"
Style="{StaticResource MaterialDesignFlatMidBgButton}" Width="110"
Margin="0 -2" ToolTip="Select an effect to add"
Padding="10 0" VerticalAlignment="Center"
Height="20" Visibility="{Binding PropertyTreeVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}}"
Width="110" Command="{x:Static materialDesign:Transitioner.MoveLastCommand}"
ToolTip="Select an effect to add" CommandTarget="{Binding ElementName=TransitionCommandAnchor}">
VerticalAlignment="Center" <TextBlock FontSize="11">
Visibility="{Binding PropertyTreeVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}}" ADD EFFECT
Command="{x:Static materialDesign:Transitioner.MoveLastCommand}" </TextBlock>
CommandTarget="{Binding ElementName=TransitionCommandAnchor}"> </Button>
<TextBlock FontSize="10"> <Button Grid.Column="1"
ADD EFFECT Style="{StaticResource MaterialDesignFlatMidBgButton}"
</TextBlock> Margin="0 -2 5 -2"
</Button> Padding="10 0"
<Button Grid.Column="1" Height="20"
Style="{StaticResource MaterialDesignFlatMidBgButton}" Width="110"
Margin="0 -2" ToolTip="Show the layer/folder properties"
Padding="10 0" VerticalAlignment="Center"
Height="20" Visibility="{Binding PropertyTreeVisible, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}"
Width="110" Command="{x:Static materialDesign:Transitioner.MoveFirstCommand}"
ToolTip="Show the layer/folder properties" CommandTarget="{Binding ElementName=TransitionCommandAnchor}">
VerticalAlignment="Center" <TextBlock FontSize="11">
Visibility="{Binding PropertyTreeVisible, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}}" SHOW PROPERTIES
Command="{x:Static materialDesign:Transitioner.MoveFirstCommand}" </TextBlock>
CommandTarget="{Binding ElementName=TransitionCommandAnchor}"> </Button>
<TextBlock FontSize="10"> </Grid>
SHOW PROPERTIES
</TextBlock> <Grid Grid.Row="1"
</Button> Grid.Column="2"
</Grid> HorizontalAlignment="Stretch"
Panel.ZIndex="2"
Background="{DynamicResource MaterialDesignCardBackground}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ListBox Style="{StaticResource MaterialDesignToolToggleListBox}" SelectedIndex="0" Height="20" Margin="5 0 0 0">
<ListBoxItem Padding="10 0">
<ListBoxItem.ToolTip>
<ToolTip Placement="Top" VerticalOffset="-5">
<StackPanel>
<TextBlock>Select the <Run FontWeight="Bold">enter</Run> timeline</TextBlock>
<TextBlock>Played when the folder/layer starts displaying (condition is met)</TextBlock>
</StackPanel>
</ToolTip>
</ListBoxItem.ToolTip>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="RayStart" Width="20" Height="20" Margin="0 -4" />
<TextBlock Margin="5 0 0 0" FontSize="11">ENTER</TextBlock>
</StackPanel>
</ListBoxItem>
<ListBoxItem Padding="10 0">
<ListBoxItem.ToolTip>
<ToolTip Placement="Top" VerticalOffset="-5">
<StackPanel>
<TextBlock>Select the <Run FontWeight="Bold">main</Run> timeline</TextBlock>
<TextBlock>Played after the enter timeline finishes, either on repeat or once</TextBlock>
</StackPanel>
</ToolTip>
</ListBoxItem.ToolTip>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="RayStartEnd" Width="20" Height="20" Margin="0 -4" />
<TextBlock Margin="5 0 0 0" FontSize="11">MAIN</TextBlock>
</StackPanel>
</ListBoxItem>
<ListBoxItem Padding="10 0">
<ListBoxItem.ToolTip>
<ToolTip Placement="Top" VerticalOffset="-5">
<StackPanel>
<TextBlock>Select the <Run FontWeight="Bold">exit</Run> timeline</TextBlock>
<TextBlock>Played when the folder/layer stops displaying (conditon no longer met)</TextBlock>
</StackPanel>
</ToolTip>
</ListBoxItem.ToolTip>
<StackPanel Orientation="Horizontal">
<materialDesign:PackIcon Kind="RayEnd" Width="20" Height="20" Margin="0 -4" />
<TextBlock Margin="5 0 0 0" FontSize="11">EXIT</TextBlock>
</StackPanel>
</ListBoxItem>
</ListBox>
<!-- Zoom control --> <!-- Zoom control -->
<Slider Grid.Column="1" <Slider Grid.Column="1"
@ -290,6 +346,7 @@
Maximum="350" Maximum="350"
Value="{Binding ProfileEditorService.PixelsPerSecond}" Value="{Binding ProfileEditorService.PixelsPerSecond}"
Width="319" /> Width="319" />
</Grid> </Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -47,27 +47,30 @@
</Style.Triggers> </Style.Triggers>
</Style> </Style>
</mde:MaterialWindow.Resources> </mde:MaterialWindow.Resources>
<materialDesign:DialogHost Identifier="RootDialog" DialogTheme="Inherit"> <materialDesign:DialogHost Identifier="RootDialog" DialogTheme="Inherit" SnackbarMessageQueue="{Binding MainMessageQueue}">
<materialDesign:DrawerHost IsLeftDrawerOpen="{Binding IsSidebarVisible}"> <Grid>
<materialDesign:DrawerHost.LeftDrawerContent> <materialDesign:DrawerHost IsLeftDrawerOpen="{Binding IsSidebarVisible}">
<ContentControl s:View.Model="{Binding SidebarViewModel}" Width="220" ClipToBounds="False" /> <materialDesign:DrawerHost.LeftDrawerContent>
</materialDesign:DrawerHost.LeftDrawerContent> <ContentControl s:View.Model="{Binding SidebarViewModel}" Width="220" ClipToBounds="False" />
<DockPanel> </materialDesign:DrawerHost.LeftDrawerContent>
<mde:AppBar Type="Dense" <DockPanel>
IsNavigationDrawerOpen="{Binding IsSidebarVisible, Mode=TwoWay}" <mde:AppBar Type="Dense"
Title="{Binding ActiveItem.DisplayName}" IsNavigationDrawerOpen="{Binding IsSidebarVisible, Mode=TwoWay}"
ShowNavigationDrawerButton="True" Title="{Binding ActiveItem.DisplayName}"
DockPanel.Dock="Top"> ShowNavigationDrawerButton="True"
<StackPanel> DockPanel.Dock="Top">
<!-- Bug: materialDesign:RippleAssist.RippleOnTop doesn't look as nice but otherwise it doesn't work at all, not sure why --> <StackPanel>
<Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Open debugger" Command="{s:Action ShowDebugger}" <!-- Bug: materialDesign:RippleAssist.RippleOnTop doesn't look as nice but otherwise it doesn't work at all, not sure why -->
materialDesign:RippleAssist.RippleOnTop="True"> <Button Style="{StaticResource MaterialDesignIconForegroundButton}" ToolTip="Open debugger" Command="{s:Action ShowDebugger}"
<materialDesign:PackIcon Kind="Matrix" /> materialDesign:RippleAssist.RippleOnTop="True">
</Button> <materialDesign:PackIcon Kind="Matrix" />
</StackPanel> </Button>
</mde:AppBar> </StackPanel>
<ContentControl s:View.Model="{Binding ActiveItem}" Style="{StaticResource InitializingFade}" /> </mde:AppBar>
</DockPanel> <ContentControl s:View.Model="{Binding ActiveItem}" Style="{StaticResource InitializingFade}" />
</materialDesign:DrawerHost> </DockPanel>
</materialDesign:DrawerHost>
<materialDesign:Snackbar x:Name="MainSnackbar" MessageQueue="{Binding MainMessageQueue}" />
</Grid>
</materialDesign:DialogHost> </materialDesign:DialogHost>
</mde:MaterialWindow> </mde:MaterialWindow>

View File

@ -27,10 +27,11 @@ namespace Artemis.UI.Screens
private readonly Timer _titleUpdateTimer; private readonly Timer _titleUpdateTimer;
private bool _lostFocus; private bool _lostFocus;
public RootViewModel(IEventAggregator eventAggregator, SidebarViewModel sidebarViewModel, ISettingsService settingsService, ICoreService coreService, public RootViewModel(IEventAggregator eventAggregator, SidebarViewModel sidebarViewModel, ISettingsService settingsService, ICoreService coreService,
IDebugService debugService) IDebugService debugService, ISnackbarMessageQueue snackbarMessageQueue)
{ {
SidebarViewModel = sidebarViewModel; SidebarViewModel = sidebarViewModel;
MainMessageQueue = snackbarMessageQueue;
_eventAggregator = eventAggregator; _eventAggregator = eventAggregator;
_coreService = coreService; _coreService = coreService;
_debugService = debugService; _debugService = debugService;
@ -49,11 +50,11 @@ namespace Artemis.UI.Screens
} }
public SidebarViewModel SidebarViewModel { get; } public SidebarViewModel SidebarViewModel { get; }
public ISnackbarMessageQueue MainMessageQueue { get; set; }
public bool IsSidebarVisible { get; set; } public bool IsSidebarVisible { get; set; }
public bool ActiveItemReady { get; set; } public bool ActiveItemReady { get; set; }
public string WindowTitle { get; set; } public string WindowTitle { get; set; }
public void WindowDeactivated() public void WindowDeactivated()
{ {
var windowState = ((Window) View).WindowState; var windowState = ((Window) View).WindowState;

View File

@ -27,20 +27,20 @@ namespace Artemis.UI.Screens.Settings
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private readonly IPluginService _pluginService; private readonly IPluginService _pluginService;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
private readonly IPluginSettingsVmFactory _pluginSettingsVmFactory;
private readonly ISurfaceService _surfaceService; private readonly ISurfaceService _surfaceService;
private readonly IWindowManager _windowManager;
public SettingsViewModel(ISurfaceService surfaceService, IPluginService pluginService, IDialogService dialogService, IWindowManager windowManager, public SettingsViewModel(ISurfaceService surfaceService, IPluginService pluginService, IDialogService dialogService, IDebugService debugService,
IDebugService debugService, ISettingsService settingsService, IDeviceSettingsVmFactory deviceSettingsVmFactory) ISettingsService settingsService, IPluginSettingsVmFactory pluginSettingsVmFactory, IDeviceSettingsVmFactory deviceSettingsVmFactory)
{ {
DisplayName = "Settings"; DisplayName = "Settings";
_surfaceService = surfaceService; _surfaceService = surfaceService;
_pluginService = pluginService; _pluginService = pluginService;
_dialogService = dialogService; _dialogService = dialogService;
_windowManager = windowManager;
_debugService = debugService; _debugService = debugService;
_settingsService = settingsService; _settingsService = settingsService;
_pluginSettingsVmFactory = pluginSettingsVmFactory;
_deviceSettingsVmFactory = deviceSettingsVmFactory; _deviceSettingsVmFactory = deviceSettingsVmFactory;
DeviceSettingsViewModels = new BindableCollection<DeviceSettingsViewModel>(); DeviceSettingsViewModels = new BindableCollection<DeviceSettingsViewModel>();
@ -189,7 +189,7 @@ namespace Artemis.UI.Screens.Settings
DeviceSettingsViewModels.AddRange(_surfaceService.ActiveSurface.Devices.Select(d => _deviceSettingsVmFactory.Create(d))); DeviceSettingsViewModels.AddRange(_surfaceService.ActiveSurface.Devices.Select(d => _deviceSettingsVmFactory.Create(d)));
Plugins.Clear(); Plugins.Clear();
Plugins.AddRange(_pluginService.GetAllPluginInfo().Select(p => new PluginSettingsViewModel(p.Instance, _windowManager, _dialogService, _pluginService))); Plugins.AddRange(_pluginService.GetAllPluginInfo().Select(p => _pluginSettingsVmFactory.Create(p.Instance)));
base.OnInitialActivate(); base.OnInitialActivate();
} }

View File

@ -36,6 +36,7 @@
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="80" /> <ColumnDefinition Width="80" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
@ -43,9 +44,16 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<materialDesign:PackIcon Kind="{Binding Icon}" Width="48" Height="48" Grid.Row="0" Grid.RowSpan="2" HorizontalAlignment="Center" /> <materialDesign:PackIcon Kind="{Binding Icon}" Width="48" Height="48" Grid.Row="0" Grid.RowSpan="2" HorizontalAlignment="Center" />
<TextBlock Style="{StaticResource MaterialDesignTextBlock}" Text="{Binding PluginInfo.Name}" Grid.Column="1" Grid.Row="0" />
<TextBlock Grid.Column="1" Grid.Row="0" Style="{StaticResource MaterialDesignTextBlock}" Text="{Binding PluginInfo.Name}" />
<materialDesign:Card Grid.Column="2" Grid.Row="0" Background="#FF4343" Height="22" Padding="4" Margin="0 -18 0 0"
Visibility="{Binding PluginInfo.LastEnableSuccessful, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
LOAD FAILED
</materialDesign:Card>
<TextBlock Grid.Column="1" <TextBlock Grid.Column="1"
Grid.Row="1" Grid.Row="1"
Grid.ColumnSpan="2"
TextWrapping="Wrap" TextWrapping="Wrap"
Text="{Binding PluginInfo.Description}" Text="{Binding PluginInfo.Description}"
Style="{StaticResource MaterialDesignTextBlock}" Style="{StaticResource MaterialDesignTextBlock}"
@ -56,15 +64,17 @@
<Button Style="{StaticResource MaterialDesignOutlinedButton}" ToolTip="MaterialDesignOutlinedButton" Margin="4" s:View.ActionTarget="{Binding}" Command="{s:Action OpenSettings}"> <Button Style="{StaticResource MaterialDesignOutlinedButton}" ToolTip="MaterialDesignOutlinedButton" Margin="4" s:View.ActionTarget="{Binding}" Command="{s:Action OpenSettings}">
SETTINGS SETTINGS
</Button> </Button>
<!-- <Button Style="{StaticResource MaterialDesignOutlinedButton}" ToolTip="MaterialDesignOutlinedButton" Margin="4"> -->
<!-- SETTINGS -->
<!-- </Button> -->
</StackPanel> </StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="8"> <StackPanel Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="8"
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding IsEnabled}"> <CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding IsEnabled}">
Plugin enabled Plugin enabled
</CheckBox> </CheckBox>
</StackPanel> </StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="8"
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
<ProgressBar Style="{StaticResource MaterialDesignCircularProgressBar}" Value="0" IsIndeterminate="True" />
</StackPanel>
</Grid> </Grid>
</materialDesign:Card> </materialDesign:Card>
</UserControl> </UserControl>

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Diagnostics;
using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core.Plugins.Abstract; using Artemis.Core.Plugins.Abstract;
using Artemis.Core.Plugins.Models; using Artemis.Core.Plugins.Models;
@ -13,9 +15,11 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
{ {
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private readonly IPluginService _pluginService; private readonly IPluginService _pluginService;
private readonly ISnackbarMessageQueue _snackbarMessageQueue;
private readonly IWindowManager _windowManager; private readonly IWindowManager _windowManager;
public PluginSettingsViewModel(Plugin plugin, IWindowManager windowManager, IDialogService dialogService, IPluginService pluginService) public PluginSettingsViewModel(Plugin plugin, IWindowManager windowManager, IDialogService dialogService, IPluginService pluginService,
ISnackbarMessageQueue snackbarMessageQueue)
{ {
Plugin = plugin; Plugin = plugin;
PluginInfo = plugin.PluginInfo; PluginInfo = plugin.PluginInfo;
@ -23,6 +27,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
_windowManager = windowManager; _windowManager = windowManager;
_dialogService = dialogService; _dialogService = dialogService;
_pluginService = pluginService; _pluginService = pluginService;
_snackbarMessageQueue = snackbarMessageQueue;
} }
public Plugin Plugin { get; set; } public Plugin Plugin { get; set; }
@ -40,6 +45,8 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
public bool CanOpenSettings => IsEnabled && Plugin.HasConfigurationViewModel; public bool CanOpenSettings => IsEnabled && Plugin.HasConfigurationViewModel;
public bool Enabling { get; set; }
public async Task OpenSettings() public async Task OpenSettings()
{ {
try try
@ -55,6 +62,18 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
} }
} }
public async Task ShowLogsFolder()
{
try
{
Process.Start(Environment.GetEnvironmentVariable("WINDIR") + @"\explorer.exe", Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs"));
}
catch (Exception e)
{
await _dialogService.ShowExceptionDialog("Welp, we couldn\'t open the logs folder for you", e);
}
}
private PackIconKind GetIconKind() private PackIconKind GetIconKind()
{ {
if (PluginInfo.Icon != null) if (PluginInfo.Icon != null)
@ -105,7 +124,23 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
} }
if (enable) if (enable)
_pluginService.EnablePlugin(Plugin); {
Enabling = true;
try
{
_pluginService.EnablePlugin(Plugin);
_snackbarMessageQueue.Enqueue($"Enabled plugin {PluginInfo.Name}");
}
catch (Exception)
{
_snackbarMessageQueue.Enqueue($"Failed to enable plugin {PluginInfo.Name}", "VIEW LOGS", async () => await ShowLogsFolder());
}
finally
{
Enabling = false;
NotifyOfPropertyChange(() => IsEnabled);
}
}
else else
_pluginService.DisablePlugin(Plugin); _pluginService.DisablePlugin(Plugin);

View File

@ -244,4 +244,5 @@
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ECSharpUseContinuousIndentInsideBracesMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EAlwaysTreatStructAsNotReorderableMigration/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean> <s:Boolean x:Key="/Default/Environment/SettingsMigration/IsMigratorApplied/=JetBrains_002EReSharper_002EPsi_002ECSharp_002ECodeStyle_002ESettingsUpgrade_002EMigrateBlankLinesAroundFieldToBlankLinesAroundProperty/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=leds/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary> <s:Boolean x:Key="/Default/UserDictionary/Words/=leds/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=snackbar/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>