Merge branch 'development'
@ -53,6 +53,8 @@
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Clayerbrushes_005Cinternal/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Clayereffects_005Cinternal/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cmodules_005Cactivationrequirements/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cprerequisites/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Cprerequisites_005Cprerequisiteaction/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=plugins_005Csettings/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=rgb_002Enet/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=services_005Ccolorquantizer/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// An exception thrown when a plugin prerequisite-related error occurs
|
||||
/// </summary>
|
||||
public class ArtemisPluginPrerequisiteException : Exception
|
||||
{
|
||||
internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject)
|
||||
{
|
||||
Subject = subject;
|
||||
}
|
||||
|
||||
internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message) : base(message)
|
||||
{
|
||||
Subject = subject;
|
||||
}
|
||||
|
||||
internal ArtemisPluginPrerequisiteException(IPrerequisitesSubject subject, string message, Exception inner) : base(message, inner)
|
||||
{
|
||||
Subject = subject;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the subject the error is related to
|
||||
/// </summary>
|
||||
public IPrerequisitesSubject Subject { get; }
|
||||
}
|
||||
}
|
||||
134
src/Artemis.Core/Extensions/StreamExtensions.cs
Normal file
@ -0,0 +1,134 @@
|
||||
// Based on: https://www.codeproject.com/Tips/5274597/An-Improved-Stream-CopyToAsync-that-Reports-Progre
|
||||
// The MIT License
|
||||
//
|
||||
// Copyright (c) 2020 honey the codewitch
|
||||
//
|
||||
// Permission is hereby granted, free of charge,
|
||||
// to any person obtaining a copy of this software and
|
||||
// associated documentation files (the "Software"), to
|
||||
// deal in the Software without restriction, including
|
||||
// without limitation the rights to use, copy, modify,
|
||||
// merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom
|
||||
// the Software is furnished to do so,
|
||||
// subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice
|
||||
// shall be included in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
// IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
|
||||
// ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
// TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
// SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
internal static class StreamExtensions
|
||||
{
|
||||
private const int DefaultBufferSize = 81920;
|
||||
|
||||
/// <summary>
|
||||
/// Copies a stream to another stream
|
||||
/// </summary>
|
||||
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
|
||||
/// <param name="sourceLength">The length of the source stream, if known - used for progress reporting</param>
|
||||
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
|
||||
/// <param name="bufferSize">The size of the copy block buffer</param>
|
||||
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A task representing the operation</returns>
|
||||
public static async Task CopyToAsync(
|
||||
this Stream source,
|
||||
long sourceLength,
|
||||
Stream destination,
|
||||
int bufferSize,
|
||||
IProgress<(long, long)> progress,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (source == null)
|
||||
throw new ArgumentNullException(nameof(source));
|
||||
if (!source.CanRead)
|
||||
throw new ArgumentException("Has to be readable", nameof(source));
|
||||
if (destination == null)
|
||||
throw new ArgumentNullException(nameof(destination));
|
||||
if (!destination.CanWrite)
|
||||
throw new ArgumentException("Has to be writable", nameof(destination));
|
||||
if (bufferSize <= 0)
|
||||
bufferSize = DefaultBufferSize;
|
||||
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
long totalBytesRead = 0;
|
||||
int bytesRead;
|
||||
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
|
||||
{
|
||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||
totalBytesRead += bytesRead;
|
||||
progress?.Report((totalBytesRead, sourceLength));
|
||||
}
|
||||
|
||||
progress?.Report((totalBytesRead, sourceLength));
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a stream to another stream
|
||||
/// </summary>
|
||||
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
|
||||
/// <param name="sourceLength">The length of the source stream, if known - used for progress reporting</param>
|
||||
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
|
||||
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A task representing the operation</returns>
|
||||
public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
return CopyToAsync(source, sourceLength, destination, 0, progress, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a stream to another stream
|
||||
/// </summary>
|
||||
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
|
||||
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
|
||||
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
|
||||
/// <param name="cancellationToken">A cancellation token</param>
|
||||
/// <returns>A task representing the operation</returns>
|
||||
public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress, CancellationToken cancellationToken)
|
||||
{
|
||||
return CopyToAsync(source, 0L, destination, 0, progress, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a stream to another stream
|
||||
/// </summary>
|
||||
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
|
||||
/// <param name="sourceLength">The length of the source stream, if known - used for progress reporting</param>
|
||||
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
|
||||
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
|
||||
/// <returns>A task representing the operation</returns>
|
||||
public static Task CopyToAsync(this Stream source, long sourceLength, Stream destination, IProgress<(long, long)> progress)
|
||||
{
|
||||
return CopyToAsync(source, sourceLength, destination, 0, progress, default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Copies a stream to another stream
|
||||
/// </summary>
|
||||
/// <param name="source">The source <see cref="Stream" /> to copy from</param>
|
||||
/// <param name="destination">The destination <see cref="Stream" /> to copy to</param>
|
||||
/// <param name="progress">An <see cref="IProgress{T}" /> implementation for reporting progress</param>
|
||||
/// <returns>A task representing the operation</returns>
|
||||
public static Task CopyToAsync(this Stream source, Stream destination, IProgress<(long, long)> progress)
|
||||
{
|
||||
return CopyToAsync(source, 0L, destination, 0, progress, default);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,9 @@ namespace Artemis.Core
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (!Path.IsValid)
|
||||
return;
|
||||
|
||||
object? value = Path.GetValue();
|
||||
if (value != null)
|
||||
CurrentValue = (T?) value;
|
||||
|
||||
@ -1,20 +0,0 @@
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// An optional entry point for your plugin
|
||||
/// </summary>
|
||||
public interface IPluginBootstrapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the plugin is activated
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin instance of your plugin</param>
|
||||
void Enable(Plugin plugin);
|
||||
|
||||
/// <summary>
|
||||
/// Called when the plugin is deactivated or when Artemis shuts down
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin instance of your plugin</param>
|
||||
void Disable(Plugin plugin);
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,7 @@ namespace Artemis.Core
|
||||
Info = info;
|
||||
Directory = directory;
|
||||
Entity = pluginEntity ?? new PluginEntity {Id = Guid, IsEnabled = true};
|
||||
Info.Plugin = this;
|
||||
|
||||
_features = new List<PluginFeatureInfo>();
|
||||
}
|
||||
@ -71,7 +72,7 @@ namespace Artemis.Core
|
||||
/// <summary>
|
||||
/// Gets the plugin bootstrapper
|
||||
/// </summary>
|
||||
public IPluginBootstrapper? Bootstrapper { get; internal set; }
|
||||
public PluginBootstrapper? Bootstrapper { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// The Ninject kernel of the plugin
|
||||
@ -113,6 +114,17 @@ namespace Artemis.Core
|
||||
{
|
||||
return _features.FirstOrDefault(i => i.Instance is T)?.Instance as T;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up the feature info the feature of type <typeparamref name="T" />
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of feature to find</typeparam>
|
||||
/// <returns>Feature info of the feature</returns>
|
||||
public PluginFeatureInfo GetFeatureInfo<T>() where T : PluginFeature
|
||||
{
|
||||
// This should be a safe assumption because any type of PluginFeature is registered and added
|
||||
return _features.First(i => i.FeatureType == typeof(T));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
@ -235,12 +247,12 @@ namespace Artemis.Core
|
||||
|
||||
if (enable)
|
||||
{
|
||||
Bootstrapper?.Enable(this);
|
||||
Bootstrapper?.OnPluginEnabled(this);
|
||||
OnEnabled();
|
||||
}
|
||||
else
|
||||
{
|
||||
Bootstrapper?.Disable(this);
|
||||
Bootstrapper?.OnPluginDisabled(this);
|
||||
OnDisabled();
|
||||
}
|
||||
}
|
||||
|
||||
100
src/Artemis.Core/Plugins/PluginBootstrapper.cs
Normal file
@ -0,0 +1,100 @@
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// An optional entry point for your plugin
|
||||
/// </summary>
|
||||
public abstract class PluginBootstrapper
|
||||
{
|
||||
private Plugin? _plugin;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the plugin is loaded
|
||||
/// </summary>
|
||||
/// <param name="plugin"></param>
|
||||
public virtual void OnPluginLoaded(Plugin plugin)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the plugin is activated
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin instance of your plugin</param>
|
||||
public virtual void OnPluginEnabled(Plugin plugin)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when the plugin is deactivated or when Artemis shuts down
|
||||
/// </summary>
|
||||
/// <param name="plugin">The plugin instance of your plugin</param>
|
||||
public virtual void OnPluginDisabled(Plugin plugin)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the provided prerequisite to the plugin.
|
||||
/// </summary>
|
||||
/// <param name="prerequisite">The prerequisite to add</param>
|
||||
public void AddPluginPrerequisite(PluginPrerequisite prerequisite)
|
||||
{
|
||||
// TODO: We can keep track of them and add them after load, same goes for the others
|
||||
if (_plugin == null)
|
||||
throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded");
|
||||
|
||||
if (!_plugin.Info.Prerequisites.Contains(prerequisite))
|
||||
_plugin.Info.Prerequisites.Add(prerequisite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the provided prerequisite from the plugin.
|
||||
/// </summary>
|
||||
/// <param name="prerequisite">The prerequisite to remove</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> is successfully removed; otherwise <see langword="false" />. This method also returns
|
||||
/// <see langword="false" /> if the prerequisite was not found.
|
||||
/// </returns>
|
||||
public bool RemovePluginPrerequisite(PluginPrerequisite prerequisite)
|
||||
{
|
||||
if (_plugin == null)
|
||||
throw new ArtemisPluginException("Cannot add plugin prerequisites before the plugin is loaded");
|
||||
|
||||
return _plugin.Info.Prerequisites.Remove(prerequisite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the provided prerequisite to the feature of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <param name="prerequisite">The prerequisite to add</param>
|
||||
public void AddFeaturePrerequisite<T>(PluginPrerequisite prerequisite) where T : PluginFeature
|
||||
{
|
||||
if (_plugin == null)
|
||||
throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded");
|
||||
|
||||
PluginFeatureInfo info = _plugin.GetFeatureInfo<T>();
|
||||
if (!info.Prerequisites.Contains(prerequisite))
|
||||
info.Prerequisites.Add(prerequisite);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes the provided prerequisite from the feature of type <typeparamref name="T"/>.
|
||||
/// </summary>
|
||||
/// <param name="prerequisite">The prerequisite to remove</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> is successfully removed; otherwise <see langword="false" />. This method also returns
|
||||
/// <see langword="false" /> if the prerequisite was not found.
|
||||
/// </returns>
|
||||
public bool RemoveFeaturePrerequisite<T>(PluginPrerequisite prerequisite) where T : PluginFeature
|
||||
{
|
||||
if (_plugin == null)
|
||||
throw new ArtemisPluginException("Cannot add feature prerequisites before the plugin is loaded");
|
||||
|
||||
return _plugin.GetFeatureInfo<T>().Prerequisites.Remove(prerequisite);
|
||||
}
|
||||
|
||||
internal void InternalOnPluginLoaded(Plugin plugin)
|
||||
{
|
||||
_plugin = plugin;
|
||||
OnPluginLoaded(plugin);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Threading.Tasks;
|
||||
@ -15,7 +16,7 @@ namespace Artemis.Core
|
||||
private readonly Stopwatch _updateStopwatch = new();
|
||||
private bool _isEnabled;
|
||||
private Exception? _loadException;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the plugin feature info related to this feature
|
||||
/// </summary>
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Artemis.Core.DataModelExpansions;
|
||||
using Artemis.Core.DeviceProviders;
|
||||
using Artemis.Core.LayerBrushes;
|
||||
using Artemis.Core.LayerEffects;
|
||||
using Artemis.Core.Modules;
|
||||
using Artemis.Storage.Entities.Plugins;
|
||||
using Humanizer;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
@ -13,23 +16,24 @@ namespace Artemis.Core
|
||||
/// Represents basic info about a plugin feature and contains a reference to the instance of said feature
|
||||
/// </summary>
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public class PluginFeatureInfo : CorePropertyChanged
|
||||
public class PluginFeatureInfo : CorePropertyChanged, IPrerequisitesSubject
|
||||
{
|
||||
private string? _description;
|
||||
private string? _icon;
|
||||
private PluginFeature? _instance;
|
||||
private string _name = null!;
|
||||
|
||||
internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureAttribute? attribute)
|
||||
internal PluginFeatureInfo(Plugin plugin, Type featureType, PluginFeatureEntity pluginFeatureEntity, PluginFeatureAttribute? attribute)
|
||||
{
|
||||
Plugin = plugin ?? throw new ArgumentNullException(nameof(plugin));
|
||||
FeatureType = featureType ?? throw new ArgumentNullException(nameof(featureType));
|
||||
Entity = pluginFeatureEntity;
|
||||
|
||||
Name = attribute?.Name ?? featureType.Name.Humanize(LetterCasing.Title);
|
||||
Description = attribute?.Description;
|
||||
Icon = attribute?.Icon;
|
||||
AlwaysEnabled = attribute?.AlwaysEnabled ?? false;
|
||||
|
||||
|
||||
if (Icon != null) return;
|
||||
if (typeof(BaseDataModelExpansion).IsAssignableFrom(featureType))
|
||||
Icon = "TableAdd";
|
||||
@ -46,7 +50,7 @@ namespace Artemis.Core
|
||||
else
|
||||
Icon = "Plugin";
|
||||
}
|
||||
|
||||
|
||||
internal PluginFeatureInfo(Plugin plugin, PluginFeatureAttribute? attribute, PluginFeature instance)
|
||||
{
|
||||
if (instance == null) throw new ArgumentNullException(nameof(instance));
|
||||
@ -119,6 +123,11 @@ namespace Artemis.Core
|
||||
[JsonProperty]
|
||||
public bool AlwaysEnabled { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether the feature is enabled in persistent storage
|
||||
/// </summary>
|
||||
public bool EnabledInStorage => Entity.IsEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the feature this info is associated with
|
||||
/// </summary>
|
||||
@ -128,6 +137,14 @@ namespace Artemis.Core
|
||||
internal set => SetAndNotify(ref _instance, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<PluginPrerequisite> Prerequisites { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet());
|
||||
|
||||
internal PluginFeatureEntity Entity { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Artemis.Core
|
||||
@ -8,7 +10,7 @@ namespace Artemis.Core
|
||||
/// Represents basic info about a plugin and contains a reference to the instance of said plugin
|
||||
/// </summary>
|
||||
[JsonObject(MemberSerialization.OptIn)]
|
||||
public class PluginInfo : CorePropertyChanged
|
||||
public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject
|
||||
{
|
||||
private bool _autoEnableFeatures = true;
|
||||
private string? _description;
|
||||
@ -117,6 +119,12 @@ namespace Artemis.Core
|
||||
internal set => SetAndNotify(ref _plugin, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public List<PluginPrerequisite> Prerequisites { get; } = new();
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool ArePrerequisitesMet() => Prerequisites.All(p => p.IsMet());
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a type that has prerequisites
|
||||
/// </summary>
|
||||
public interface IPrerequisitesSubject
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets a list of prerequisites for this plugin
|
||||
/// </summary>
|
||||
List<PluginPrerequisite> Prerequisites { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the prerequisites of this plugin are met
|
||||
/// </summary>
|
||||
bool ArePrerequisitesMet();
|
||||
}
|
||||
}
|
||||
127
src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs
Normal file
@ -0,0 +1,127 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a prerequisite for a <see cref="Plugin" /> or <see cref="PluginFeature" />
|
||||
/// </summary>
|
||||
public abstract class PluginPrerequisite : CorePropertyChanged
|
||||
{
|
||||
private PluginPrerequisiteAction? _currentAction;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the prerequisite
|
||||
/// </summary>
|
||||
public abstract string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the description of the prerequisite
|
||||
/// </summary>
|
||||
public abstract string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether installing or uninstalling this prerequisite requires admin privileges
|
||||
/// </summary>
|
||||
public abstract bool RequiresElevation { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of actions to execute when <see cref="Install" /> is called
|
||||
/// </summary>
|
||||
public abstract List<PluginPrerequisiteAction> InstallActions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a list of actions to execute when <see cref="Uninstall" /> is called
|
||||
/// </summary>
|
||||
public abstract List<PluginPrerequisiteAction> UninstallActions { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the action currently being executed
|
||||
/// </summary>
|
||||
public PluginPrerequisiteAction? CurrentAction
|
||||
{
|
||||
get => _currentAction;
|
||||
private set => SetAndNotify(ref _currentAction, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute all install actions
|
||||
/// </summary>
|
||||
public async Task Install(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
OnInstallStarting();
|
||||
foreach (PluginPrerequisiteAction installAction in InstallActions)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
CurrentAction = installAction;
|
||||
await installAction.Execute(cancellationToken);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CurrentAction = null;
|
||||
OnInstallFinished();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute all uninstall actions
|
||||
/// </summary>
|
||||
public async Task Uninstall(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
OnUninstallStarting();
|
||||
foreach (PluginPrerequisiteAction uninstallAction in UninstallActions)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
CurrentAction = uninstallAction;
|
||||
await uninstallAction.Execute(cancellationToken);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
CurrentAction = null;
|
||||
OnUninstallFinished();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called to determine whether the prerequisite is met
|
||||
/// </summary>
|
||||
/// <returns><see langword="true" /> if the prerequisite is met; otherwise <see langword="false" /></returns>
|
||||
public abstract bool IsMet();
|
||||
|
||||
/// <summary>
|
||||
/// Called before installation starts
|
||||
/// </summary>
|
||||
protected virtual void OnInstallStarting()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after installation finishes
|
||||
/// </summary>
|
||||
protected virtual void OnInstallFinished()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called before uninstall starts
|
||||
/// </summary>
|
||||
protected virtual void OnUninstallStarting()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called after uninstall finished
|
||||
/// </summary>
|
||||
protected virtual void OnUninstallFinished()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents an action that must be taken to install or uninstall a plugin prerequisite
|
||||
/// </summary>
|
||||
public abstract class PluginPrerequisiteAction : CorePropertyChanged
|
||||
{
|
||||
private bool _progressIndeterminate;
|
||||
private bool _showProgressBar;
|
||||
private bool _showSubProgressBar;
|
||||
private string? _status;
|
||||
private bool _subProgressIndeterminate;
|
||||
|
||||
/// <summary>
|
||||
/// The base constructor for all plugin prerequisite actions
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the action</param>
|
||||
protected PluginPrerequisiteAction(string name)
|
||||
{
|
||||
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
#region Implementation of IPluginPrerequisiteAction
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the action
|
||||
/// </summary>
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the status of the action
|
||||
/// </summary>
|
||||
public string? Status
|
||||
{
|
||||
get => _status;
|
||||
set => SetAndNotify(ref _status, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether the progress is indeterminate or not
|
||||
/// </summary>
|
||||
public bool ProgressIndeterminate
|
||||
{
|
||||
get => _progressIndeterminate;
|
||||
set => SetAndNotify(ref _progressIndeterminate, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether the progress is indeterminate or not
|
||||
/// </summary>
|
||||
public bool SubProgressIndeterminate
|
||||
{
|
||||
get => _subProgressIndeterminate;
|
||||
set => SetAndNotify(ref _subProgressIndeterminate, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether the progress bar should be shown
|
||||
/// </summary>
|
||||
public bool ShowProgressBar
|
||||
{
|
||||
get => _showProgressBar;
|
||||
set => SetAndNotify(ref _showProgressBar, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether the sub progress bar should be shown
|
||||
/// </summary>
|
||||
public bool ShowSubProgressBar
|
||||
{
|
||||
get => _showSubProgressBar;
|
||||
set => SetAndNotify(ref _showSubProgressBar, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the progress of the action (0 to 100)
|
||||
/// </summary>
|
||||
public PrerequisiteActionProgress Progress { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the sub progress of the action
|
||||
/// </summary>
|
||||
public PrerequisiteActionProgress SubProgress { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Called when the action must execute
|
||||
/// </summary>
|
||||
public abstract Task Execute(CancellationToken cancellationToken);
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Humanizer;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a plugin prerequisite action that copies a folder
|
||||
/// </summary>
|
||||
public class CopyFolderAction : PluginPrerequisiteAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of a copy folder action
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the action</param>
|
||||
/// <param name="source">The source folder to copy</param>
|
||||
/// <param name="target">The target folder to copy to (will be created if needed)</param>
|
||||
public CopyFolderAction(string name, string source, string target) : base(name)
|
||||
{
|
||||
Source = source;
|
||||
Target = target;
|
||||
|
||||
ShowProgressBar = true;
|
||||
ShowSubProgressBar = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source directory
|
||||
/// </summary>
|
||||
public string Source { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target directory
|
||||
/// </summary>
|
||||
public string Target { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Execute(CancellationToken cancellationToken)
|
||||
{
|
||||
DirectoryInfo source = new(Source);
|
||||
DirectoryInfo target = new(Target);
|
||||
|
||||
if (!source.Exists)
|
||||
throw new ArtemisCoreException($"The source directory at '{source}' was not found.");
|
||||
|
||||
int filesCopied = 0;
|
||||
FileInfo[] files = source.GetFiles("*", SearchOption.AllDirectories);
|
||||
|
||||
foreach (FileInfo fileInfo in files)
|
||||
{
|
||||
string outputPath = fileInfo.FullName.Replace(source.FullName, target.FullName);
|
||||
string outputDir = Path.GetDirectoryName(outputPath)!;
|
||||
Utilities.CreateAccessibleDirectory(outputDir);
|
||||
|
||||
void SubProgressOnProgressReported(object? sender, EventArgs e)
|
||||
{
|
||||
if (SubProgress.ProgressPerSecond != 0)
|
||||
Status = $"Copying {fileInfo.Name} - {SubProgress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec";
|
||||
else
|
||||
Status = $"Copying {fileInfo.Name}";
|
||||
}
|
||||
|
||||
Progress.Report((filesCopied, files.Length));
|
||||
SubProgress.ProgressReported += SubProgressOnProgressReported;
|
||||
|
||||
await using FileStream sourceStream = fileInfo.OpenRead();
|
||||
await using FileStream destinationStream = File.Create(outputPath);
|
||||
|
||||
await sourceStream.CopyToAsync(fileInfo.Length, destinationStream, SubProgress, cancellationToken);
|
||||
|
||||
filesCopied++;
|
||||
SubProgress.ProgressReported -= SubProgressOnProgressReported;
|
||||
}
|
||||
|
||||
Progress.Report((filesCopied, files.Length));
|
||||
Status = $"Finished copying {filesCopied} file(s)";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a plugin prerequisite action that deletes a file
|
||||
/// </summary>
|
||||
public class DeleteFileAction : PluginPrerequisiteAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of a copy folder action
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the action</param>
|
||||
/// <param name="target">The target folder to delete recursively</param>
|
||||
public DeleteFileAction(string name, string target) : base(name)
|
||||
{
|
||||
Target = target;
|
||||
ProgressIndeterminate = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target directory
|
||||
/// </summary>
|
||||
public string Target { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Execute(CancellationToken cancellationToken)
|
||||
{
|
||||
ShowProgressBar = true;
|
||||
Status = $"Removing {Target}";
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
if (File.Exists(Target))
|
||||
File.Delete(Target);
|
||||
}, cancellationToken);
|
||||
|
||||
ShowProgressBar = false;
|
||||
Status = $"Removed {Target}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,49 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a plugin prerequisite action that recursively deletes a folder
|
||||
/// </summary>
|
||||
public class DeleteFolderAction : PluginPrerequisiteAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of a copy folder action
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the action</param>
|
||||
/// <param name="target">The target folder to delete recursively</param>
|
||||
public DeleteFolderAction(string name, string target) : base(name)
|
||||
{
|
||||
if (Enum.GetValues<Environment.SpecialFolder>().Select(Environment.GetFolderPath).Contains(target))
|
||||
throw new ArtemisCoreException($"Cannot delete special folder {target}, silly goose.");
|
||||
|
||||
Target = target;
|
||||
ProgressIndeterminate = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target directory
|
||||
/// </summary>
|
||||
public string Target { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Execute(CancellationToken cancellationToken)
|
||||
{
|
||||
ShowProgressBar = true;
|
||||
Status = $"Removing {Target}";
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
if (Directory.Exists(Target))
|
||||
Directory.Delete(Target, true);
|
||||
}, cancellationToken);
|
||||
|
||||
ShowProgressBar = false;
|
||||
Status = $"Removed {Target}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Humanizer;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a plugin prerequisite action that downloads a file
|
||||
/// </summary>
|
||||
public class DownloadFileAction : PluginPrerequisiteAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of a copy folder action
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the action</param>
|
||||
/// <param name="url">The source URL to download</param>
|
||||
/// <param name="fileName">The target file to save as (will be created if needed)</param>
|
||||
public DownloadFileAction(string name, string url, string fileName) : base(name)
|
||||
{
|
||||
Url = url ?? throw new ArgumentNullException(nameof(url));
|
||||
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
|
||||
|
||||
ShowProgressBar = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the source URL to download
|
||||
/// </summary>
|
||||
public string Url { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target file to save as (will be created if needed)
|
||||
/// </summary>
|
||||
public string FileName { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Execute(CancellationToken cancellationToken)
|
||||
{
|
||||
using HttpClient client = new();
|
||||
await using FileStream destinationStream = new(FileName, FileMode.OpenOrCreate);
|
||||
|
||||
void ProgressOnProgressReported(object? sender, EventArgs e)
|
||||
{
|
||||
if (Progress.ProgressPerSecond != 0)
|
||||
Status = $"Downloading {Url} - {Progress.ProgressPerSecond.Bytes().Humanize("#.##")}/sec";
|
||||
else
|
||||
Status = $"Downloading {Url}";
|
||||
}
|
||||
|
||||
Progress.ProgressReported += ProgressOnProgressReported;
|
||||
|
||||
// Get the http headers first to examine the content length
|
||||
using HttpResponseMessage response = await client.GetAsync(Url, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||
await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken);
|
||||
long? contentLength = response.Content.Headers.ContentLength;
|
||||
|
||||
// Ignore progress reporting when no progress reporter was
|
||||
// passed or when the content length is unknown
|
||||
if (!contentLength.HasValue)
|
||||
{
|
||||
ProgressIndeterminate = true;
|
||||
await download.CopyToAsync(destinationStream, Progress, cancellationToken);
|
||||
ProgressIndeterminate = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
ProgressIndeterminate = false;
|
||||
await download.CopyToAsync(contentLength.Value, destinationStream, Progress, cancellationToken);
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Progress.ProgressReported -= ProgressOnProgressReported;
|
||||
Progress.Report((1, 1));
|
||||
Status = "Finished downloading";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,88 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a plugin prerequisite action that executes a file
|
||||
/// </summary>
|
||||
public class ExecuteFileAction : PluginPrerequisiteAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ExecuteFileAction" />
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the action</param>
|
||||
/// <param name="fileName">The target file to execute</param>
|
||||
/// <param name="arguments">A set of command-line arguments to use when starting the application</param>
|
||||
/// <param name="waitForExit">A boolean indicating whether the action should wait for the process to exit</param>
|
||||
public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true) : base(name)
|
||||
{
|
||||
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
|
||||
Arguments = arguments;
|
||||
WaitForExit = waitForExit;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the target file to execute
|
||||
/// </summary>
|
||||
public string FileName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a set of command-line arguments to use when starting the application
|
||||
/// </summary>
|
||||
public string? Arguments { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a boolean indicating whether the action should wait for the process to exit
|
||||
/// </summary>
|
||||
public bool WaitForExit { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Execute(CancellationToken cancellationToken)
|
||||
{
|
||||
if (WaitForExit)
|
||||
{
|
||||
Status = $"Running {FileName} and waiting for exit..";
|
||||
ShowProgressBar = true;
|
||||
ProgressIndeterminate = true;
|
||||
|
||||
int result = await RunProcessAsync(FileName, Arguments);
|
||||
|
||||
Status = $"{FileName} exited with code {result}";
|
||||
}
|
||||
else
|
||||
{
|
||||
Status = $"Running {FileName}";
|
||||
Process process = new()
|
||||
{
|
||||
StartInfo = {FileName = FileName, Arguments = Arguments!},
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
process.Start();
|
||||
}
|
||||
}
|
||||
|
||||
private static Task<int> RunProcessAsync(string fileName, string? arguments)
|
||||
{
|
||||
TaskCompletionSource<int> tcs = new();
|
||||
|
||||
Process process = new()
|
||||
{
|
||||
StartInfo = {FileName = fileName, Arguments = arguments!},
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
process.Exited += (_, _) =>
|
||||
{
|
||||
tcs.SetResult(process.ExitCode);
|
||||
process.Dispose();
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,83 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a plugin prerequisite action that extracts a ZIP file
|
||||
/// </summary>
|
||||
public class ExtractArchiveAction : PluginPrerequisiteAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of <see cref="ExtractArchiveAction"/>.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the action</param>
|
||||
/// <param name="fileName">The ZIP file to extract</param>
|
||||
/// <param name="target">The folder into which to extract the file</param>
|
||||
public ExtractArchiveAction(string name, string fileName, string target) : base(name)
|
||||
{
|
||||
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
|
||||
Target = target ?? throw new ArgumentNullException(nameof(target));
|
||||
|
||||
ShowProgressBar = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the file to extract
|
||||
/// </summary>
|
||||
public string FileName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the folder into which to extract the file
|
||||
/// </summary>
|
||||
public string Target { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Execute(CancellationToken cancellationToken)
|
||||
{
|
||||
using HttpClient client = new();
|
||||
|
||||
ShowSubProgressBar = true;
|
||||
Status = $"Extracting {FileName}";
|
||||
|
||||
Utilities.CreateAccessibleDirectory(Target);
|
||||
|
||||
await using (FileStream fileStream = new(FileName, FileMode.Open))
|
||||
{
|
||||
ZipArchive archive = new(fileStream);
|
||||
long count = 0;
|
||||
foreach (ZipArchiveEntry entry in archive.Entries)
|
||||
{
|
||||
await using Stream unzippedEntryStream = entry.Open();
|
||||
Progress.Report((count, archive.Entries.Count));
|
||||
if (entry.Length > 0)
|
||||
{
|
||||
string path = Path.Combine(Target, entry.FullName);
|
||||
CreateDirectoryForFile(path);
|
||||
await using Stream extractStream = new FileStream(path, FileMode.OpenOrCreate);
|
||||
await unzippedEntryStream.CopyToAsync(entry.Length, extractStream, SubProgress, cancellationToken);
|
||||
}
|
||||
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
Progress.Report((1, 1));
|
||||
ShowSubProgressBar = false;
|
||||
Status = "Finished extracting";
|
||||
}
|
||||
|
||||
private static void CreateDirectoryForFile(string path)
|
||||
{
|
||||
string? directory = Path.GetDirectoryName(path);
|
||||
if (directory == null)
|
||||
throw new ArtemisCoreException($"Failed to get directory from path {path}");
|
||||
|
||||
Utilities.CreateAccessibleDirectory(directory);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a plugin prerequisite action that copies a folder
|
||||
/// </summary>
|
||||
public class WriteBytesToFileAction : PluginPrerequisiteAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of a copy folder action
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the action</param>
|
||||
/// <param name="target">The target file to write to (will be created if needed)</param>
|
||||
/// <param name="content">The contents to write</param>
|
||||
public WriteBytesToFileAction(string name, string target, byte[] content) : base(name)
|
||||
{
|
||||
Target = target;
|
||||
ByteContent = content ?? throw new ArgumentNullException(nameof(content));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target file
|
||||
/// </summary>
|
||||
public string Target { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to
|
||||
/// <see langword="false" /> the file will be deleted and recreated
|
||||
/// </summary>
|
||||
public bool Append { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the bytes that will be written
|
||||
/// </summary>
|
||||
public byte[] ByteContent { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Execute(CancellationToken cancellationToken)
|
||||
{
|
||||
string outputDir = Path.GetDirectoryName(Target)!;
|
||||
Utilities.CreateAccessibleDirectory(outputDir);
|
||||
|
||||
ShowProgressBar = true;
|
||||
Status = $"Writing to {Path.GetFileName(Target)}...";
|
||||
|
||||
if (!Append && File.Exists(Target))
|
||||
File.Delete(Target);
|
||||
|
||||
await using Stream fileStream = File.OpenWrite(Target);
|
||||
await using MemoryStream sourceStream = new(ByteContent);
|
||||
await sourceStream.CopyToAsync(sourceStream.Length, fileStream, Progress, cancellationToken);
|
||||
|
||||
ShowProgressBar = false;
|
||||
Status = $"Finished writing to {Path.GetFileName(Target)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,62 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a plugin prerequisite action that copies a folder
|
||||
/// </summary>
|
||||
public class WriteStringToFileAction : PluginPrerequisiteAction
|
||||
{
|
||||
/// <summary>
|
||||
/// Creates a new instance of a copy folder action
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the action</param>
|
||||
/// <param name="target">The target file to write to (will be created if needed)</param>
|
||||
/// <param name="content">The contents to write</param>
|
||||
public WriteStringToFileAction(string name, string target, string content) : base(name)
|
||||
{
|
||||
Target = target;
|
||||
Content = content ?? throw new ArgumentNullException(nameof(content));
|
||||
|
||||
ProgressIndeterminate = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the target file
|
||||
/// </summary>
|
||||
public string Target { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether or not to append to the file if it exists already, if set to
|
||||
/// <see langword="false" /> the file will be deleted and recreated
|
||||
/// </summary>
|
||||
public bool Append { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the string that will be written
|
||||
/// </summary>
|
||||
public string Content { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override async Task Execute(CancellationToken cancellationToken)
|
||||
{
|
||||
string outputDir = Path.GetDirectoryName(Target)!;
|
||||
Utilities.CreateAccessibleDirectory(outputDir);
|
||||
|
||||
ShowProgressBar = true;
|
||||
Status = $"Writing to {Path.GetFileName(Target)}...";
|
||||
|
||||
if (Append)
|
||||
await File.AppendAllTextAsync(Target, Content, cancellationToken);
|
||||
else
|
||||
await File.WriteAllTextAsync(Target, Content, cancellationToken);
|
||||
|
||||
ShowProgressBar = false;
|
||||
Status = $"Finished writing to {Path.GetFileName(Target)}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents progress on a plugin prerequisite action
|
||||
/// </summary>
|
||||
public class PrerequisiteActionProgress : CorePropertyChanged, IProgress<(long, long)>
|
||||
{
|
||||
private long _current;
|
||||
private DateTime _lastReport;
|
||||
private double _percentage;
|
||||
private double _progressPerSecond;
|
||||
private long _total;
|
||||
private long _lastReportValue;
|
||||
|
||||
/// <summary>
|
||||
/// The current amount
|
||||
/// </summary>
|
||||
public long Current
|
||||
{
|
||||
get => _current;
|
||||
set => SetAndNotify(ref _current, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The total amount
|
||||
/// </summary>
|
||||
public long Total
|
||||
{
|
||||
get => _total;
|
||||
set => SetAndNotify(ref _total, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The percentage
|
||||
/// </summary>
|
||||
public double Percentage
|
||||
{
|
||||
get => _percentage;
|
||||
set => SetAndNotify(ref _percentage, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the progress per second
|
||||
/// </summary>
|
||||
public double ProgressPerSecond
|
||||
{
|
||||
get => _progressPerSecond;
|
||||
set => SetAndNotify(ref _progressPerSecond, value);
|
||||
}
|
||||
|
||||
#region Implementation of IProgress<in (long, long)>
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Report((long, long) value)
|
||||
{
|
||||
(long newCurrent, long newTotal) = value;
|
||||
|
||||
TimeSpan timePassed = DateTime.Now - _lastReport;
|
||||
if (timePassed >= TimeSpan.FromSeconds(1))
|
||||
{
|
||||
ProgressPerSecond = Math.Max(0, Math.Round(1.0 / timePassed.TotalSeconds * (newCurrent - _lastReportValue), 2));
|
||||
_lastReportValue = newCurrent;
|
||||
_lastReport = DateTime.Now;
|
||||
}
|
||||
|
||||
Current = newCurrent;
|
||||
Total = newTotal;
|
||||
Percentage = Math.Round((double) Current / Total * 100.0, 2);
|
||||
|
||||
OnProgressReported();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Events
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when progress has been reported
|
||||
/// </summary>
|
||||
public event EventHandler? ProgressReported;
|
||||
|
||||
protected virtual void OnProgressReported()
|
||||
{
|
||||
ProgressReported?.Invoke(this, EventArgs.Empty);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -241,7 +241,16 @@ namespace Artemis.Core.Services
|
||||
}
|
||||
|
||||
foreach (Plugin plugin in _plugins.Where(p => p.Entity.IsEnabled))
|
||||
EnablePlugin(plugin, false, ignorePluginLock);
|
||||
{
|
||||
try
|
||||
{
|
||||
EnablePlugin(plugin, false, ignorePluginLock);
|
||||
}
|
||||
catch (ArtemisPluginPrerequisiteException)
|
||||
{
|
||||
_logger.Warning("Skipped enabling plugin {plugin} because not all prerequisites are met", plugin);
|
||||
}
|
||||
}
|
||||
|
||||
_logger.Debug("Enabled {count} plugin(s)", _plugins.Count(p => p.IsEnabled));
|
||||
// ReSharper restore InconsistentlySynchronizedField
|
||||
@ -331,16 +340,24 @@ namespace Artemis.Core.Services
|
||||
}
|
||||
|
||||
foreach (Type featureType in featureTypes)
|
||||
plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute))));
|
||||
{
|
||||
// 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 = plugin.Info.AutoEnableFeatures, Type = featureType.FullName! };
|
||||
plugin.AddFeature(new PluginFeatureInfo(plugin, featureType, featureEntity, (PluginFeatureAttribute?) Attribute.GetCustomAttribute(featureType, typeof(PluginFeatureAttribute))));
|
||||
}
|
||||
|
||||
if (!featureTypes.Any())
|
||||
_logger.Warning("Plugin {plugin} contains no features", plugin);
|
||||
|
||||
List<Type> bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(IPluginBootstrapper).IsAssignableFrom(t)).ToList();
|
||||
List<Type> bootstrappers = plugin.Assembly.GetTypes().Where(t => typeof(PluginBootstrapper).IsAssignableFrom(t)).ToList();
|
||||
if (bootstrappers.Count > 1)
|
||||
_logger.Warning($"{plugin} has more than one bootstrapper, only initializing {bootstrappers.First().FullName}");
|
||||
if (bootstrappers.Any())
|
||||
plugin.Bootstrapper = (IPluginBootstrapper?) Activator.CreateInstance(bootstrappers.First());
|
||||
{
|
||||
plugin.Bootstrapper = (PluginBootstrapper?) Activator.CreateInstance(bootstrappers.First());
|
||||
plugin.Bootstrapper?.InternalOnPluginLoaded(plugin);
|
||||
}
|
||||
|
||||
lock (_plugins)
|
||||
{
|
||||
@ -369,6 +386,9 @@ namespace Artemis.Core.Services
|
||||
return;
|
||||
}
|
||||
|
||||
if (!plugin.Info.ArePrerequisitesMet())
|
||||
throw new ArtemisPluginPrerequisiteException(plugin.Info, "Cannot enable a plugin whose prerequisites aren't all met");
|
||||
|
||||
// Create the Ninject child kernel and load the module
|
||||
plugin.Kernel = new ChildKernel(_kernel, new PluginModule(plugin));
|
||||
OnPluginEnabling(new PluginEventArgs(plugin));
|
||||
@ -391,10 +411,7 @@ namespace Artemis.Core.Services
|
||||
featureInfo.Instance = instance;
|
||||
instance.Info = featureInfo;
|
||||
instance.Plugin = plugin;
|
||||
|
||||
// Load the enabled state and if not found, default to true
|
||||
instance.Entity = plugin.Entity.Features.FirstOrDefault(i => i.Type == featureInfo.FeatureType.FullName) ??
|
||||
new PluginFeatureEntity {IsEnabled = plugin.Info.AutoEnableFeatures, Type = featureInfo.FeatureType.FullName!};
|
||||
instance.Entity = featureInfo.Entity;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -403,17 +420,8 @@ namespace Artemis.Core.Services
|
||||
}
|
||||
|
||||
// Activate features after they are all loaded
|
||||
foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && (f.Instance.Entity.IsEnabled || f.AlwaysEnabled)))
|
||||
{
|
||||
try
|
||||
{
|
||||
EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// ignored, logged in EnablePluginFeature
|
||||
}
|
||||
}
|
||||
foreach (PluginFeatureInfo pluginFeature in plugin.Features.Where(f => f.Instance != null && (f.EnabledInStorage || f.AlwaysEnabled)))
|
||||
EnablePluginFeature(pluginFeature.Instance!, false, !ignorePluginLock);
|
||||
|
||||
if (saveState)
|
||||
{
|
||||
@ -570,7 +578,10 @@ namespace Artemis.Core.Services
|
||||
if (pluginFeature.Plugin.Info.RequiresAdmin && !_isElevated)
|
||||
{
|
||||
if (!saveState)
|
||||
{
|
||||
OnPluginFeatureEnableFailed(new PluginFeatureEventArgs(pluginFeature));
|
||||
throw new ArtemisCoreException("Cannot enable a feature that requires elevation without saving it's state.");
|
||||
}
|
||||
|
||||
pluginFeature.Entity.IsEnabled = true;
|
||||
pluginFeature.Plugin.Entity.IsEnabled = true;
|
||||
@ -581,6 +592,12 @@ namespace Artemis.Core.Services
|
||||
return;
|
||||
}
|
||||
|
||||
if (!pluginFeature.Info.ArePrerequisitesMet())
|
||||
{
|
||||
OnPluginFeatureEnableFailed(new PluginFeatureEventArgs(pluginFeature));
|
||||
throw new ArtemisPluginPrerequisiteException(pluginFeature.Info, "Cannot enable a plugin feature whose prerequisites aren't all met");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
pluginFeature.SetEnabled(true, isAutoEnable);
|
||||
@ -593,7 +610,6 @@ namespace Artemis.Core.Services
|
||||
new ArtemisPluginException(pluginFeature.Plugin, $"Exception during SetEnabled(true) on {pluginFeature}", e),
|
||||
"Failed to enable plugin"
|
||||
);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
@ -73,7 +73,9 @@
|
||||
MinWidth="95"
|
||||
MaxLength="9"
|
||||
Margin="0"
|
||||
HorizontalAlignment="Stretch">
|
||||
HorizontalAlignment="Stretch"
|
||||
FontFamily="Consolas"
|
||||
CharacterCasing="Upper">
|
||||
<materialDesign:TextFieldAssist.CharacterCounterStyle>
|
||||
<Style TargetType="TextBlock" />
|
||||
</materialDesign:TextFieldAssist.CharacterCounterStyle>
|
||||
@ -82,7 +84,7 @@
|
||||
<Border Width="15"
|
||||
Height="15"
|
||||
CornerRadius="15"
|
||||
Margin="0 0 8 0"
|
||||
Margin="0 0 2 0"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
Background="{StaticResource Checkerboard}">
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
<UserControl x:ClassModifier="internal"
|
||||
x:Class="Artemis.UI.Shared.Screens.Dialogs.ConfirmDialogView"
|
||||
<UserControl
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:dialogs="clr-namespace:Artemis.UI.Shared.Screens.Dialogs"
|
||||
xmlns:Shared="clr-namespace:Artemis.UI.Shared" x:ClassModifier="internal"
|
||||
x:Class="Artemis.UI.Shared.Screens.Dialogs.ConfirmDialogView"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="163.274" d:DesignWidth="254.425"
|
||||
d:DataContext="{d:DesignInstance dialogs:ConfirmDialogViewModel}">
|
||||
d:DataContext="{d:DesignInstance {x:Type dialogs:ConfirmDialogViewModel}}">
|
||||
<UserControl.Resources>
|
||||
<Shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
|
||||
</UserControl.Resources>
|
||||
<StackPanel Margin="16">
|
||||
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" Text="{Binding Header}" TextWrapping="Wrap" />
|
||||
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}"
|
||||
@ -22,7 +26,8 @@
|
||||
Focusable="False"
|
||||
IsCancel="True"
|
||||
Command="{s:Action Cancel}"
|
||||
Content="{Binding CancelText}" />
|
||||
Content="{Binding CancelText}"
|
||||
Visibility="{Binding CancelText, Converter={StaticResource NullToVisibilityConverter}, Mode=OneWay}" />
|
||||
<Button x:Name="ConfirmButton"
|
||||
Style="{StaticResource MaterialDesignFlatButton}"
|
||||
IsDefault="True"
|
||||
|
||||
@ -32,6 +32,7 @@ namespace Artemis.UI.Shared.Screens.GradientEditor
|
||||
ColorStop.Position = (float) Math.Round(value / _gradientEditorViewModel.PreviewWidth, 3, MidpointRounding.AwayFromZero);
|
||||
NotifyOfPropertyChange(nameof(Offset));
|
||||
NotifyOfPropertyChange(nameof(OffsetPercent));
|
||||
NotifyOfPropertyChange(nameof(OffsetFloat));
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,6 +44,20 @@ namespace Artemis.UI.Shared.Screens.GradientEditor
|
||||
ColorStop.Position = Math.Min(100, Math.Max(0, value)) / 100f;
|
||||
NotifyOfPropertyChange(nameof(Offset));
|
||||
NotifyOfPropertyChange(nameof(OffsetPercent));
|
||||
NotifyOfPropertyChange(nameof(OffsetFloat));
|
||||
}
|
||||
}
|
||||
|
||||
// Functionally similar to Offset Percent, but doesn't round on get in order to prevent inconsistencies (and is 0 to 1)
|
||||
public float OffsetFloat
|
||||
{
|
||||
get => ColorStop.Position;
|
||||
set
|
||||
{
|
||||
ColorStop.Position = Math.Min(1, Math.Max(0, value));
|
||||
NotifyOfPropertyChange(nameof(Offset));
|
||||
NotifyOfPropertyChange(nameof(OffsetPercent));
|
||||
NotifyOfPropertyChange(nameof(OffsetFloat));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -12,9 +12,9 @@
|
||||
Background="{DynamicResource MaterialDesignPaper}"
|
||||
FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto"
|
||||
Width="400"
|
||||
Height="400"
|
||||
Height="450"
|
||||
d:DesignHeight="450"
|
||||
d:DesignWidth="800"
|
||||
d:DesignWidth="400"
|
||||
d:DataContext="{d:DesignInstance local:GradientEditorViewModel}">
|
||||
<UserControl.Resources>
|
||||
<shared:ColorGradientToGradientStopsConverter x:Key="ColorGradientToGradientStopsConverter" />
|
||||
@ -40,7 +40,7 @@
|
||||
</VisualBrush>
|
||||
</UserControl.Resources>
|
||||
<UserControl.InputBindings>
|
||||
<KeyBinding Key="Delete" Command="{s:Action RemoveColorStop}" CommandParameter="{Binding SelectedColorStopViewModel}"/>
|
||||
<KeyBinding Key="Delete" Command="{s:Action RemoveColorStop}" CommandParameter="{Binding SelectedColorStopViewModel}" />
|
||||
</UserControl.InputBindings>
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
@ -60,8 +60,83 @@
|
||||
</StackPanel>
|
||||
<Separator Grid.Row="1" Margin="0 5" />
|
||||
<StackPanel Grid.Row="2" Margin="0 5" ClipToBounds="False">
|
||||
<TextBlock Margin="0 5">Gradient</TextBlock>
|
||||
|
||||
<Grid Margin="0 0 0 5">
|
||||
<TextBlock Margin="0 5" VerticalAlignment="Center">Gradient</TextBlock>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Margin="0 0 5 0"
|
||||
Padding="0"
|
||||
Command="{s:Action SpreadColorStops}"
|
||||
ToolTip="Spread Stops"
|
||||
IsEnabled="{Binding HasMoreThanOneStop}"
|
||||
HorizontalAlignment="Left"
|
||||
Height="25" Width="25">
|
||||
<materialDesign:PackIcon Kind="ArrowLeftRight" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Margin="0 0 5 0"
|
||||
Padding="0"
|
||||
Command="{s:Action ToggleSeam}"
|
||||
ToolTip="Toggle Seamless"
|
||||
IsEnabled="{Binding HasMoreThanOneStop}"
|
||||
HorizontalAlignment="Left"
|
||||
Height="25" Width="25">
|
||||
<materialDesign:PackIcon Kind="SineWave" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Margin="0 0 5 0"
|
||||
Padding="0"
|
||||
Command="{s:Action FlipColorStops}"
|
||||
ToolTip="Flip Stops"
|
||||
IsEnabled="{Binding HasMoreThanOneStop}"
|
||||
HorizontalAlignment="Left"
|
||||
Height="25" Width="25">
|
||||
<materialDesign:PackIcon Kind="FlipHorizontal" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Margin="0 0 5 0"
|
||||
Padding="0"
|
||||
Command="{s:Action RotateColorStops}"
|
||||
ToolTip="Rotate Stops (shift to invert)"
|
||||
IsEnabled="{Binding HasMoreThanOneStop}"
|
||||
HorizontalAlignment="Left"
|
||||
Height="25" Width="25">
|
||||
<materialDesign:PackIcon Kind="AxisZRotateCounterclockwise" />
|
||||
</Button>
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Padding="0"
|
||||
Command="{s:Action ShowClearGradientPopup}"
|
||||
ToolTip="Clear All Color Stops"
|
||||
IsEnabled="{Binding HasMoreThanOneStop}"
|
||||
HorizontalAlignment="Left"
|
||||
Height="25" Width="25"
|
||||
x:Name="ClearGradientButton">
|
||||
<StackPanel>
|
||||
<materialDesign:PackIcon Kind="DeleteSweep" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Popup
|
||||
AllowsTransparency="True"
|
||||
Placement="Right"
|
||||
VerticalOffset="-25"
|
||||
HorizontalOffset="-5"
|
||||
CustomPopupPlacementCallback="{x:Static materialDesign:CustomPopupPlacementCallbackHelper.LargePopupCallback}"
|
||||
PlacementTarget="{Binding ElementName=ClearGradientButton}"
|
||||
StaysOpen="False"
|
||||
PopupAnimation="Fade"
|
||||
IsOpen="{Binding ClearGradientPopupOpen, Mode=OneWay}">
|
||||
<materialDesign:Card Margin="30" Padding="12" materialDesign:ShadowAssist.ShadowDepth="Depth4">
|
||||
<StackPanel HorizontalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource MaterialDesignSubtitle2TextBlock}" TextAlignment="Center" Margin="0 0 0 10">Clear Gradient?</TextBlock>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<Button Command="{s:Action HideClearGradientPopup}" Style="{StaticResource MaterialDesignOutlinedButton}" Margin="0 0 10 0">No</Button>
|
||||
<Button Command="{s:Action ClearGradientAndHide}">Yes</Button>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</materialDesign:Card>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Border Background="{StaticResource Checkerboard}">
|
||||
<Rectangle x:Name="Preview" Height="40" shared:SizeObserver.Observe="True" shared:SizeObserver.ObservedWidth="{Binding PreviewWidth, Mode=OneWayToSource}">
|
||||
<Rectangle.Fill>
|
||||
@ -90,8 +165,7 @@
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Margin="0 5">Selected stop</TextBlock>
|
||||
<TextBlock Margin="0 6 0 0" FontWeight="Bold">Selected stop:</TextBlock>
|
||||
|
||||
<Grid Margin="0 5">
|
||||
<Grid.ColumnDefinitions>
|
||||
@ -100,7 +174,7 @@
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal">
|
||||
<Label HorizontalAlignment="Left" Margin="-5,0,0,0">Color:</Label>
|
||||
<Label HorizontalAlignment="Left" Margin="-5,1,0,0">Color:</Label>
|
||||
<shared:ColorPicker
|
||||
x:Name="CurrentColor"
|
||||
Width="85"
|
||||
@ -109,8 +183,8 @@
|
||||
IsEnabled="{Binding HasSelectedColorStopViewModel}" />
|
||||
|
||||
<Label HorizontalAlignment="Center">Location:</Label>
|
||||
<TextBox Width="40"
|
||||
Text="{Binding SelectedColorStopViewModel.OffsetPercent}"
|
||||
<TextBox Width="30"
|
||||
Text="{Binding SelectedColorStopViewModel.OffsetPercent, UpdateSourceTrigger=PropertyChanged}"
|
||||
IsEnabled="{Binding HasSelectedColorStopViewModel}"
|
||||
materialDesign:HintAssist.Hint="0"
|
||||
Margin="5 0 0 0" />
|
||||
@ -122,8 +196,15 @@
|
||||
Margin="8,0,0,0"
|
||||
IsEnabled="{Binding HasSelectedColorStopViewModel}"
|
||||
Command="{s:Action RemoveColorStop}"
|
||||
CommandParameter="{Binding SelectedColorStopViewModel}">
|
||||
DELETE
|
||||
CommandParameter="{Binding SelectedColorStopViewModel}"
|
||||
ToolTip="Delete Selected Stop">
|
||||
<Button.Resources>
|
||||
<SolidColorBrush x:Key="PrimaryHueMidBrush" Color="#c94848" />
|
||||
</Button.Resources>
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Delete" />
|
||||
<TextBlock Margin="4 0 0 0">Delete</TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.Collections.Generic;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
@ -27,19 +28,9 @@ namespace Artemis.UI.Shared.Screens.GradientEditor
|
||||
|
||||
PropertyChanged += UpdateColorStopViewModels;
|
||||
ColorGradient.CollectionChanged += ColorGradientOnCollectionChanged;
|
||||
ColorStopViewModels.CollectionChanged += ColorStopViewModelsOnCollectionChanged;
|
||||
}
|
||||
|
||||
#region Overrides of DialogViewModelBase
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDialogClosed(object sender, DialogClosingEventArgs e)
|
||||
{
|
||||
ColorGradient.CollectionChanged -= ColorGradientOnCollectionChanged;
|
||||
base.OnDialogClosed(sender, e);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public BindableCollection<ColorStopViewModel> ColorStopViewModels { get; }
|
||||
|
||||
public ColorStopViewModel? SelectedColorStopViewModel
|
||||
@ -53,6 +44,9 @@ namespace Artemis.UI.Shared.Screens.GradientEditor
|
||||
}
|
||||
|
||||
public bool HasSelectedColorStopViewModel => SelectedColorStopViewModel != null;
|
||||
public bool HasMoreThanOneStop => ColorStopViewModels.Count > 1;
|
||||
private bool popupOpen = false;
|
||||
public bool ClearGradientPopupOpen => popupOpen;
|
||||
|
||||
public ColorGradient ColorGradient { get; }
|
||||
|
||||
@ -62,11 +56,20 @@ namespace Artemis.UI.Shared.Screens.GradientEditor
|
||||
set => SetAndNotify(ref _previewWidth, value);
|
||||
}
|
||||
|
||||
public ColorGradient Stops
|
||||
public ColorGradient Stops => ColorGradient;
|
||||
|
||||
#region Overrides of DialogViewModelBase
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDialogClosed(object sender, DialogClosingEventArgs e)
|
||||
{
|
||||
get => ColorGradient;
|
||||
ColorGradient.CollectionChanged -= ColorGradientOnCollectionChanged;
|
||||
ColorStopViewModels.CollectionChanged -= ColorStopViewModelsOnCollectionChanged;
|
||||
base.OnDialogClosed(sender, e);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void AddColorStop(object sender, MouseEventArgs e)
|
||||
{
|
||||
Canvas? child = VisualTreeUtilities.FindChild<Canvas>((DependencyObject) sender, null);
|
||||
@ -79,19 +82,118 @@ namespace Artemis.UI.Shared.Screens.GradientEditor
|
||||
ColorStopViewModels.Insert(index, viewModel);
|
||||
|
||||
SelectColorStop(viewModel);
|
||||
NotifyOfPropertyChange(nameof(HasMoreThanOneStop));
|
||||
}
|
||||
|
||||
public void RemoveColorStop(ColorStopViewModel colorStopViewModel)
|
||||
{
|
||||
if (colorStopViewModel == null)
|
||||
return;
|
||||
|
||||
ColorStopViewModels.Remove(colorStopViewModel);
|
||||
ColorGradient.Remove(colorStopViewModel.ColorStop);
|
||||
|
||||
SelectColorStop(null);
|
||||
NotifyOfPropertyChange(nameof(HasMoreThanOneStop));
|
||||
}
|
||||
|
||||
#region Gradient Tools
|
||||
public void SpreadColorStops()
|
||||
{
|
||||
List<ColorStopViewModel> stops = ColorStopViewModels.OrderBy(x => x.OffsetFloat).ToList();
|
||||
int index = 0;
|
||||
foreach (ColorStopViewModel stop in stops)
|
||||
{
|
||||
stop.OffsetFloat = index / ((float) stops.Count - 1);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
public void RotateColorStops()
|
||||
{
|
||||
List<ColorStopViewModel> stops;
|
||||
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
|
||||
stops = ColorStopViewModels.OrderBy(x => x.OffsetFloat).ToList();
|
||||
else
|
||||
stops = ColorStopViewModels.OrderByDescending(x => x.OffsetFloat).ToList();
|
||||
|
||||
float lastStopPosition = stops.Last().OffsetFloat;
|
||||
foreach (ColorStopViewModel stop in stops)
|
||||
{
|
||||
float tempStop = stop.OffsetFloat;
|
||||
stop.OffsetFloat = lastStopPosition;
|
||||
lastStopPosition = tempStop;
|
||||
}
|
||||
}
|
||||
|
||||
public void FlipColorStops()
|
||||
{
|
||||
foreach (ColorStopViewModel stop in ColorStopViewModels) stop.OffsetFloat = 1 - stop.OffsetFloat;
|
||||
}
|
||||
|
||||
public void ToggleSeam()
|
||||
{
|
||||
if (ColorGradient.IsSeamless())
|
||||
{
|
||||
// Remove the last stop
|
||||
ColorStopViewModel? stopToRemove = ColorStopViewModels.OrderBy(x => x.OffsetFloat).Last();
|
||||
|
||||
if (stopToRemove == SelectedColorStopViewModel) SelectColorStop(null);
|
||||
|
||||
ColorStopViewModels.Remove(stopToRemove);
|
||||
ColorGradient.Remove(stopToRemove.ColorStop);
|
||||
|
||||
// Uncompress the stops if there is still more than one
|
||||
List<ColorStopViewModel> stops = ColorStopViewModels.OrderBy(x => x.OffsetFloat).ToList();
|
||||
|
||||
if (stops.Count >= 2)
|
||||
{
|
||||
float multiplier = stops.Count/(stops.Count - 1f);
|
||||
foreach (ColorStopViewModel stop in stops)
|
||||
stop.OffsetFloat = Math.Min(stop.OffsetFloat * multiplier, 100f);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// Compress existing stops to the left
|
||||
List<ColorStopViewModel> stops = ColorStopViewModels.OrderBy(x => x.OffsetFloat).ToList();
|
||||
|
||||
float multiplier = (stops.Count - 1f)/stops.Count;
|
||||
foreach (ColorStopViewModel stop in stops)
|
||||
stop.OffsetFloat *= multiplier;
|
||||
|
||||
// Add a stop to the end that is the same color as the first stop
|
||||
ColorGradientStop newStop = new(ColorGradient.First().Color, 1f);
|
||||
ColorGradient.Add(newStop);
|
||||
|
||||
int index = ColorGradient.IndexOf(newStop);
|
||||
ColorStopViewModel viewModel = new(this, newStop);
|
||||
ColorStopViewModels.Insert(index, viewModel);
|
||||
}
|
||||
}
|
||||
|
||||
public void ShowClearGradientPopup()
|
||||
{
|
||||
popupOpen = true;
|
||||
NotifyOfPropertyChange(nameof(ClearGradientPopupOpen));
|
||||
}
|
||||
public void HideClearGradientPopup()
|
||||
{
|
||||
popupOpen = false;
|
||||
NotifyOfPropertyChange(nameof(ClearGradientPopupOpen));
|
||||
}
|
||||
public void ClearGradientAndHide()
|
||||
{
|
||||
ClearGradient();
|
||||
HideClearGradientPopup();
|
||||
}
|
||||
public void ClearGradient()
|
||||
{
|
||||
ColorGradient.Clear();
|
||||
ColorStopViewModels.Clear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public Point GetPositionInPreview(object sender, MouseEventArgs e)
|
||||
{
|
||||
Canvas? parent = VisualTreeUtilities.FindParent<Canvas>((DependencyObject) sender, null);
|
||||
@ -127,10 +229,15 @@ namespace Artemis.UI.Shared.Screens.GradientEditor
|
||||
foreach (ColorGradientStop colorStop in ColorGradient)
|
||||
ColorStopViewModels.Add(new ColorStopViewModel(this, colorStop));
|
||||
}
|
||||
|
||||
|
||||
private void ColorGradientOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(ColorGradient));
|
||||
}
|
||||
|
||||
private void ColorStopViewModelsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(HasMoreThanOneStop));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -53,7 +53,7 @@ namespace Artemis.UI.Shared.Services
|
||||
|
||||
public LinkedList<Color> RecentColors => RecentColorsSetting.Value;
|
||||
|
||||
public Task<object> ShowGradientPicker(ColorGradient colorGradient, string dialogHost)
|
||||
public Task<object?> ShowGradientPicker(ColorGradient colorGradient, string dialogHost)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(dialogHost))
|
||||
return _dialogService.ShowDialogAt<GradientEditorViewModel>(dialogHost, new Dictionary<string, object> {{"colorGradient", colorGradient}});
|
||||
|
||||
@ -61,28 +61,30 @@ namespace Artemis.UI.Shared.Services
|
||||
return await ShowDialog(identifier, GetBestKernel().Get<T>(parameters));
|
||||
}
|
||||
|
||||
public async Task<bool> ShowConfirmDialog(string header, string text, string confirmText = "Confirm", string cancelText = "Cancel")
|
||||
public async Task<bool> ShowConfirmDialog(string header, string text, string confirmText = "Confirm", string? cancelText = "Cancel")
|
||||
{
|
||||
if (confirmText == null) throw new ArgumentNullException(nameof(confirmText));
|
||||
IParameter[] arguments =
|
||||
{
|
||||
new ConstructorArgument("header", header),
|
||||
new ConstructorArgument("text", text),
|
||||
new ConstructorArgument("confirmText", confirmText.ToUpper()),
|
||||
new ConstructorArgument("cancelText", cancelText.ToUpper())
|
||||
new ConstructorArgument("cancelText", cancelText?.ToUpper())
|
||||
};
|
||||
object? result = await ShowDialog<ConfirmDialogViewModel>(arguments);
|
||||
return result is bool booleanResult && booleanResult;
|
||||
}
|
||||
|
||||
public async Task<bool> ShowConfirmDialogAt(string identifier, string header, string text, string confirmText = "Confirm", string cancelText = "Cancel")
|
||||
public async Task<bool> ShowConfirmDialogAt(string identifier, string header, string text, string confirmText = "Confirm", string? cancelText = "Cancel")
|
||||
{
|
||||
if (identifier == null) throw new ArgumentNullException(nameof(identifier));
|
||||
if (confirmText == null) throw new ArgumentNullException(nameof(confirmText));
|
||||
IParameter[] arguments =
|
||||
{
|
||||
new ConstructorArgument("header", header),
|
||||
new ConstructorArgument("text", text),
|
||||
new ConstructorArgument("confirmText", confirmText.ToUpper()),
|
||||
new ConstructorArgument("cancelText", cancelText.ToUpper())
|
||||
new ConstructorArgument("cancelText", cancelText?.ToUpper())
|
||||
};
|
||||
object? result = await ShowDialogAt<ConfirmDialogViewModel>(identifier, arguments);
|
||||
return result is bool booleanResult && booleanResult;
|
||||
|
||||
@ -6,7 +6,7 @@ namespace Artemis.UI.Shared.Services
|
||||
/// <summary>
|
||||
/// Represents the base class for a dialog view model
|
||||
/// </summary>
|
||||
public abstract class DialogViewModelBase : ValidatingModelBase
|
||||
public abstract class DialogViewModelBase : Screen
|
||||
{
|
||||
private DialogViewModelHost? _dialogViewModelHost;
|
||||
private DialogSession? _session;
|
||||
@ -47,6 +47,7 @@ namespace Artemis.UI.Shared.Services
|
||||
/// </summary>
|
||||
public virtual void OnDialogClosed(object sender, DialogClosingEventArgs e)
|
||||
{
|
||||
ScreenExtensions.TryClose(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -61,6 +62,7 @@ namespace Artemis.UI.Shared.Services
|
||||
internal void OnDialogOpened(object sender, DialogOpenedEventArgs e)
|
||||
{
|
||||
Session = e.Session;
|
||||
ScreenExtensions.TryActivate(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ namespace Artemis.UI.Shared.Services
|
||||
{
|
||||
internal interface IColorPickerService : IArtemisSharedUIService
|
||||
{
|
||||
Task<object> ShowGradientPicker(ColorGradient colorGradient, string dialogHost);
|
||||
Task<object?> ShowGradientPicker(ColorGradient colorGradient, string dialogHost);
|
||||
|
||||
PluginSetting<bool> PreviewSetting { get; }
|
||||
LinkedList<Color> RecentColors { get; }
|
||||
|
||||
@ -17,7 +17,17 @@
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Light.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.Defaults.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Primary/MaterialDesignColor.Teal.xaml" />
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Accent/MaterialDesignColor.Teal.xaml" />
|
||||
|
||||
<!-- Custom accent based on logo colors -->
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignColors;component/Themes/Recommended/Accent/MaterialDesignColor.DeepPurple.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
|
||||
<!-- include a single secondary accent color (and the associated forecolour) -->
|
||||
<SolidColorBrush x:Key="SecondaryHueMidBrush" Color="#854FF2"/>
|
||||
<SolidColorBrush x:Key="SecondaryHueMidForegroundBrush" Color="White"/>
|
||||
</ResourceDictionary>
|
||||
|
||||
<!-- Material design extensions -->
|
||||
<ResourceDictionary Source="pack://application:,,,/MaterialDesignExtensions;component/Themes/Generic.xaml" />
|
||||
@ -33,41 +43,7 @@
|
||||
|
||||
<!-- Some general convertes etc. -->
|
||||
<BooleanToVisibilityConverter x:Key="BoolToVisibilityConverter" />
|
||||
|
||||
<DrawingImage x:Key="BowIcon">
|
||||
<DrawingImage.Drawing>
|
||||
<DrawingGroup>
|
||||
<GeometryDrawing Brush="White"
|
||||
Geometry="M1518 3378 c-48 -63 -61 -101 -66 -184 -4 -70 -1 -91 27
|
||||
-170 l31 -89 -27 -20 c-32 -24 -849 -601 -981 -693 l-93 -64 -87 40
|
||||
c-48 22 -91 37 -95 32 -5 -4 9 -41 29 -83 l37 -75 -28 -24 c-23 -20
|
||||
-29 -35 -33 -81 l-4 -56 -82 -19 c-109 -25 -109 -41 4 -91 l85 -38 7
|
||||
-64 c15 -137 90 -1279 85 -1293 -3 -7 -35 -24 -70 -35 -159 -53 -257
|
||||
-168 -257 -302 0 -35 2 -38 47 -53 54 -18 185 -21 232 -5 29 10 31
|
||||
14 31 58 0 26 6 56 14 66 13 18 15 18 46 -8 44 -37 78 -35 119 7 l34
|
||||
35 -17 41 c-9 23 -12 39 -6 35 6 -4 43 -1 83 6 39 6 219 14 398 18
|
||||
l327 6 113 57 c158 78 256 166 317 282 24 46 27 62 27 152 0 98 -1
|
||||
103 -41 184 l-42 83 44 69 c24 37 51 68 59 68 9 0 44 -14 78 -32 l62
|
||||
-31 -93 -44 c-58 -26 -92 -48 -90 -55 9 -27 353 -68 570 -68 108 0
|
||||
108 0 108 24 0 34 -105 171 -220 286 -122 122 -238 216 -250 204 -6
|
||||
-6 -1 -42 16 -98 14 -49 23 -91 19 -94 -3 -3 -36 9 -73 27 l-69 33 24
|
||||
71 c13 39 23 76 23 82 0 6 28 17 63 24 279 58 399 300 314 632 -32
|
||||
121 -49 155 -134 255 -37 45 -106 126 -152 180 -73 87 -241 326 -241
|
||||
343 0 3 15 13 32 21 21 10 35 25 40 45 15 60 -16 103 -81 108 -43 3
|
||||
-39 22 14 74 l45 43 -25 50 c-35 69 -77 114 -130 139 -63 30 -88 27
|
||||
-117 -11z m215 -835 c188 -279 250 -417 250 -548 0 -133 -74 -214 -243
|
||||
-265 l-55 -16 -37 -138 c-21 -76 -39 -140 -40 -141 -6 -5 -814 377 -823
|
||||
390 -6 7 -19 46 -29 86 -10 41 -25 81 -33 91 -8 9 -57 35 -109 59 -52
|
||||
23 -93 46 -92 51 2 4 233 169 513 366 l510 358 26 -46 c15 -25 88 -136
|
||||
162 -247z m-1108 -898 c61 21 88 26 107 19 14 -5 204 -92 421 -194 l395
|
||||
-185 -27 -35 c-15 -19 -53 -72 -84 -117 l-57 -81 30 -90 c39 -117 40
|
||||
-179 2 -253 -45 -90 -147 -145 -347 -189 -71 -15 -435 -59 -600 -73 l
|
||||
-29 -2 -37 540 c-20 297 -40 581 -43 632 l-7 92 98 -46 97 -46 81 28z" />
|
||||
</DrawingGroup>
|
||||
</DrawingImage.Drawing>
|
||||
</DrawingImage>
|
||||
|
||||
|
||||
|
||||
<FontFamily x:Key="RobotoMono">pack://application:,,,/Resources/Fonts/#Roboto Mono</FontFamily>
|
||||
|
||||
<!-- Disable tab stop/focusable on all content controls -->
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<ApplicationIcon>Resources\Images\Logo\logo-512.ico</ApplicationIcon>
|
||||
<ApplicationIcon>Resources\Images\Logo\bow.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup>
|
||||
<PostBuildEvent />
|
||||
@ -84,9 +84,9 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Fonts\RobotoMono-Regular.ttf" />
|
||||
<Resource Include="Resources\Images\Logo\bow.svg" />
|
||||
<Resource Include="Resources\Images\Logo\logo-512.ico" />
|
||||
<Resource Include="Resources\Images\Logo\logo-512.png" />
|
||||
<Resource Include="Resources\Images\Logo\bow-white.ico" />
|
||||
<Resource Include="Resources\Images\Logo\bow-white.svg" />
|
||||
<Resource Include="Resources\Images\Logo\bow.ico" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Cursors\aero_rotate_tl.cur" />
|
||||
@ -126,6 +126,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Resource Include="Resources\Cursors\aero_rotate.cur" />
|
||||
<Resource Include="Resources\Images\Logo\bow.svg" />
|
||||
<Resource Include="Resources\Images\PhysicalLayouts\abnt.png" />
|
||||
<Resource Include="Resources\Images\PhysicalLayouts\ansi.png" />
|
||||
<Resource Include="Resources\Images\PhysicalLayouts\iso.png" />
|
||||
@ -310,8 +311,10 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Remove="Resources\Fonts\RobotoMono-Regular.ttf" />
|
||||
<None Remove="Resources\Images\Logo\bow-white.ico" />
|
||||
<None Remove="Resources\Images\Logo\bow-white.svg" />
|
||||
<None Remove="Resources\Images\Logo\bow.ico" />
|
||||
<None Remove="Resources\Images\Logo\bow.svg" />
|
||||
<None Remove="Resources\Images\Logo\logo-512.ico" />
|
||||
<None Remove="Resources\Images\PhysicalLayouts\abnt.png" />
|
||||
<None Remove="Resources\Images\PhysicalLayouts\ansi.png" />
|
||||
<None Remove="Resources\Images\PhysicalLayouts\iso.png" />
|
||||
@ -359,6 +362,9 @@
|
||||
<Page Update="DefaultTypes\PropertyInput\FloatRangePropertyInputView.xaml">
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Update="Screens\Plugins\PluginPrerequisitesUninstallDialogView.xaml">
|
||||
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
|
||||
</Page>
|
||||
<Page Update="Screens\ProfileEditor\Dialogs\ProfileEditView.xaml">
|
||||
<XamlRuntime>$(DefaultXamlRuntime)</XamlRuntime>
|
||||
</Page>
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
using Artemis.Core.Modules;
|
||||
using Artemis.UI.Screens.Modules;
|
||||
using Artemis.UI.Screens.Modules.Tabs;
|
||||
using Artemis.UI.Screens.Plugins;
|
||||
using Artemis.UI.Screens.ProfileEditor;
|
||||
using Artemis.UI.Screens.ProfileEditor.Conditions;
|
||||
using Artemis.UI.Screens.ProfileEditor.LayerProperties;
|
||||
@ -95,7 +96,13 @@ namespace Artemis.UI.Ninject.Factories
|
||||
TimelineSegmentViewModel TimelineSegmentViewModel(SegmentViewModelType segment, IObservableCollection<LayerPropertyGroupViewModel> layerPropertyGroups);
|
||||
}
|
||||
|
||||
public interface IDataBindingsVmFactory
|
||||
public interface IPrerequisitesVmFactory : IVmFactory
|
||||
{
|
||||
PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall);
|
||||
}
|
||||
|
||||
// TODO: Move these two
|
||||
public interface IDataBindingsVmFactory
|
||||
{
|
||||
IDataBindingViewModel DataBindingViewModel(IDataBindingRegistration registration);
|
||||
DirectDataBindingModeViewModel<TLayerProperty, TProperty> DirectDataBindingModeViewModel<TLayerProperty, TProperty>(DirectDataBinding<TLayerProperty, TProperty> directDataBinding);
|
||||
@ -104,7 +111,7 @@ namespace Artemis.UI.Ninject.Factories
|
||||
DataBindingConditionViewModel<TLayerProperty, TProperty> DataBindingConditionViewModel<TLayerProperty, TProperty>(DataBindingCondition<TLayerProperty, TProperty> dataBindingCondition);
|
||||
}
|
||||
|
||||
public interface IPropertyVmFactory
|
||||
public interface IPropertyVmFactory
|
||||
{
|
||||
ITreePropertyViewModel TreePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel);
|
||||
ITimelinePropertyViewModel TimelinePropertyViewModel(ILayerProperty layerProperty, LayerPropertyViewModel layerPropertyViewModel);
|
||||
|
||||
BIN
src/Artemis.UI/Resources/Images/Logo/bow-white.ico
Normal file
|
After Width: | Height: | Size: 20 KiB |
12
src/Artemis.UI/Resources/Images/Logo/bow-white.svg
Normal file
@ -0,0 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<defs>
|
||||
<style>.cls-1,.cls-2{fill:#fff;}.cls-2{fill-rule:evenodd;}</style>
|
||||
</defs>
|
||||
<g id="Layer_10" data-name="Layer 10">
|
||||
<path class="cls-1" d="M52.32,17.68l-2.83,5a1.16,1.16,0,0,1-2,0l-.57-1-.54.32c-.22-.44-.46-.88-.71-1.31s-.66-1.1-1-1.63l.53-.3-.63-1.06a1.16,1.16,0,0,1,1-1.74l5.75,0A1.15,1.15,0,0,1,52.32,17.68Z"/>
|
||||
<path class="cls-1" d="M24.71,25.63l-4.84,2.58-.53.28a38.15,38.15,0,0,1-5.54-2.18c-4.51-1.49-1.48-7,1.93-4.31.34.22.69.44,1,.64.59.35,1.21.67,1.82,1A25.88,25.88,0,0,0,24.71,25.63Z"/>
|
||||
<path class="cls-1" d="M30.83,36.22c-.15.43-.28.86-.41,1.29a25.74,25.74,0,0,0-.81,4.09,26.72,26.72,0,0,0-.17,3.1c0,.37,0,.75,0,1.12A2.45,2.45,0,0,1,25,47.72c-.56-1-.22-2-.18-3.08s.21-2,.39-3c.12-.74.27-1.47.43-2.2l.53-.33,3.63-2.26Z"/>
|
||||
<path class="cls-1" d="M35,28.71l-.91.57L31.3,31,24,35.59l-4.45,2.78-2.22,1.37a2.6,2.6,0,0,1-1.26.34,2.45,2.45,0,0,1-.8-4.72l.58-.31,1.3-.69,4.67-2.5,7.6-4.05,2.94-1.57.94-.5a17.91,17.91,0,0,1,1,1.55C34.57,27.75,34.82,28.23,35,28.71Z"/>
|
||||
<path class="cls-2" d="M39.29,53.89a2.56,2.56,0,0,1-1.09.74l-.34.08a.13.13,0,0,1-.09,0,1.84,1.84,0,0,1-.33,0,2.41,2.41,0,0,1-1.84-4,22.32,22.32,0,0,0,5-18.09c0-.24-.08-.48-.13-.72s-.1-.48-.16-.73-.11-.48-.18-.72-.12-.45-.2-.68a20.49,20.49,0,0,0-.7-1.94c-.06-.18-.14-.34-.21-.51a21.83,21.83,0,0,0-1.09-2.16c-.14-.22-.27-.45-.4-.66-.25-.4-.51-.78-.77-1.16s-.63-.85-1-1.26l-.48-.56c-.35-.4-.72-.78-1.09-1.14a6.51,6.51,0,0,0-.54-.51l-.45-.4a22.08,22.08,0,0,0-3-2.2c-.17-.11-.36-.21-.54-.31s-.42-.24-.63-.35l-.46-.23a19.7,19.7,0,0,0-2.31-1l-.44-.15-.1,0c-.53-.18-1.07-.34-1.63-.48l-.25-.06a19.61,19.61,0,0,0-2-.39c-.35-.06-.7-.1-1-.13s-.8-.07-1.2-.08-.65,0-1,0h0a22.18,22.18,0,0,0-4,.36,3.28,3.28,0,0,1-.43,0,2.42,2.42,0,0,1-.42-4.8A26,26,0,0,1,18,9.26h.62c.43,0,.86,0,1.28,0l1,.07,1.07.11c.52.07,1,.14,1.53.24.23,0,.46.08.7.13l.14,0c.35.08.69.15,1,.25a20.61,20.61,0,0,1,2.16.65c.48.16.94.33,1.4.52h0c.33.14.67.28,1,.44s.58.27.86.42l.27.13c.28.14.56.29.82.45s.64.36,1,.55c.49.31,1,.62,1.45,1l.15.11c.31.22.62.46.93.7l.11.08c.36.28.71.58,1.06.89l0,0c.33.28.64.57,1,.88s.64.62.94.95c.1.1.19.21.29.32.26.29.52.58.77.88,0,0,0,.05.06.09.28.34.55.68.81,1s.55.75.82,1.15l.28.44c.21.33.42.67.62,1A27.14,27.14,0,0,1,39.29,53.89Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
BIN
src/Artemis.UI/Resources/Images/Logo/bow.ico
Normal file
|
After Width: | Height: | Size: 46 KiB |
@ -1,44 +1,43 @@
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="242.000000pt" height="341.000000pt" viewBox="0 0 242.000000 341.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
<metadata>
|
||||
Created by potrace 1.15, written by Peter Selinger 2001-2017
|
||||
</metadata>
|
||||
<g transform="translate(0.000000,341.000000) scale(0.100000,-0.100000)"
|
||||
fill="#ffffff" stroke="none">
|
||||
<path d="M46 3388 l-48 -22 5 -47 c15 -121 99 -212 249 -268 l75 -28 1 36 c1
|
||||
20 4 71 8 114 3 46 2 77 -3 77 -16 0 -33 49 -33 94 0 40 -3 45 -31 55 -54 19
|
||||
-168 13 -223 -11z"/>
|
||||
<path d="M378 3281 c-24 -19 -25 -26 -31 -173 -3 -84 -24 -409 -47 -723 -22
|
||||
-313 -44 -628 -48 -700 -5 -71 -10 -111 -11 -88 l-1 42 -85 -37 c-46 -20 -85
|
||||
-42 -85 -47 0 -6 38 -19 85 -29 l85 -19 0 -43 c0 -57 14 -92 44 -105 l25 -12
|
||||
-40 -79 c-22 -44 -39 -81 -37 -83 5 -5 472 215 484 228 6 7 21 51 34 98 l22
|
||||
86 615 288 c337 158 615 286 617 284 2 -2 -9 -49 -25 -103 -16 -55 -27 -102
|
||||
-24 -104 7 -8 144 108 235 198 92 91 230 263 230 286 0 27 -334 14 -562 -22
|
||||
-60 -10 -108 -20 -108 -24 0 -4 45 -27 100 -52 l100 -45 -172 -81 c-684 -321
|
||||
-1017 -476 -1042 -485 -24 -8 -44 -5 -110 18 l-82 28 -98 -46 c-55 -26 -101
|
||||
-45 -103 -43 -5 5 87 1349 93 1355 2 2 4 -12 4 -31 0 -29 4 -37 23 -41 12 -4
|
||||
132 -18 267 -32 333 -35 482 -69 585 -133 62 -38 86 -63 111 -117 33 -69 31
|
||||
-135 -6 -247 l-30 -89 77 -110 c42 -61 80 -112 84 -115 4 -2 55 18 113 45
|
||||
l105 50 -49 73 -48 74 41 84 c41 81 42 86 42 184 0 96 -2 104 -34 163 -34 64
|
||||
-118 154 -184 199 -21 14 -81 46 -135 72 l-97 47 -325 6 c-179 4 -358 12 -397
|
||||
18 -40 7 -78 9 -84 6 -6 -4 -20 -21 -31 -39 -24 -37 -22 -30 16 57 l29 68 -28
|
||||
29 c-34 35 -71 39 -107 11z"/>
|
||||
<path d="M1730 2035 c-52 -25 -99 -46 -103 -48 -5 -1 8 -66 28 -144 l37 -141
|
||||
45 -12 c115 -28 217 -106 244 -186 47 -138 -16 -306 -236 -635 -74 -110 -147
|
||||
-221 -162 -246 -26 -45 -26 -46 -7 -60 34 -27 98 -53 129 -52 27 0 37 11 103
|
||||
112 40 62 105 151 145 198 39 48 110 131 155 185 100 119 116 148 149 273 32
|
||||
125 34 267 6 357 -47 145 -150 232 -314 266 -34 7 -63 17 -65 23 -2 5 -13 43
|
||||
-24 83 -12 39 -25 72 -28 72 -4 0 -50 -20 -102 -45z"/>
|
||||
<path d="M450 1273 l-35 -15 25 -17 c13 -9 123 -86 245 -172 121 -85 360 -254
|
||||
530 -374 171 -120 352 -251 403 -292 79 -62 98 -72 120 -67 61 15 79 42 64 99
|
||||
-8 27 -17 33 -113 65 -103 35 -109 39 -641 413 -296 207 -543 377 -550 376 -7
|
||||
0 -29 -8 -48 -16z"/>
|
||||
<path d="M1486 380 c-29 -90 -34 -190 -12 -253 17 -46 65 -117 85 -125 20 -8
|
||||
117 45 147 81 14 16 36 51 49 77 l24 48 -44 45 c-25 24 -45 53 -45 64 0 13
|
||||
-26 38 -77 75 -43 30 -83 58 -89 61 -7 4 -21 -22 -38 -73z"/>
|
||||
</g>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512">
|
||||
<defs>
|
||||
<style>.cls-1{fill:url(#linear-gradient);}.cls-2{fill:url(#linear-gradient-2);}.cls-3{fill:url(#linear-gradient-3);}.cls-4{fill:url(#linear-gradient-4);}.cls-5{fill:url(#linear-gradient-5);}.cls-6{fill:url(#linear-gradient-6);}.cls-7{fill:url(#linear-gradient-7);}.cls-8{fill-rule:evenodd;fill:url(#linear-gradient-8);}</style>
|
||||
<linearGradient id="linear-gradient" x1="151.25" y1="94.74" x2="810.05" y2="369.54" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#f19d25"/>
|
||||
<stop offset="0.12" stop-color="#f63d3d"/>
|
||||
<stop offset="0.28" stop-color="#c93cec"/>
|
||||
<stop offset="0.44" stop-color="#2667f4"/>
|
||||
<stop offset="0.56" stop-color="#1cb6e7"/>
|
||||
<stop offset="0.7" stop-color="#2df4b5"/>
|
||||
<stop offset="0.87" stop-color="#70ea37"/>
|
||||
<stop offset="1" stop-color="#cfe726"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-2" x1="132.17" y1="204.4" x2="234.7" y2="230.88" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#ef3d3d"/>
|
||||
<stop offset="1" stop-color="#b72222"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-3" x1="217.07" y1="354.42" x2="253.03" y2="258.52" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#2b75f6"/>
|
||||
<stop offset="1" stop-color="#1452aa"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-4" x1="105.18" y1="320.77" x2="311.18" y2="203.81" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#22f15e"/>
|
||||
<stop offset="1" stop-color="#29af4d"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="linear-gradient-5" x1="112.68" y1="187.21" x2="771.48" y2="462.01" xlink:href="#linear-gradient"/>
|
||||
<linearGradient id="linear-gradient-6" x1="88.75" y1="244.59" x2="747.54" y2="519.39" xlink:href="#linear-gradient"/>
|
||||
<linearGradient id="linear-gradient-7" x1="102.97" y1="210.49" x2="761.77" y2="485.29" xlink:href="#linear-gradient"/>
|
||||
<linearGradient id="linear-gradient-8" x1="141.26" y1="116.54" x2="347.22" y2="356.89" gradientTransform="translate(11.74 6.94)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" stop-color="#009385"/>
|
||||
<stop offset="1" stop-color="#045e53"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g id="Teal_Bow" data-name="Teal Bow">
|
||||
<g id="TealRainbow">
|
||||
<path class="cls-1" d="M375.54,156.66,340,157a7.14,7.14,0,0,0-6.1,10.76l3.85,6.55-16.8,9.84c2.18,3.26,4.26,6.61,6.24,10,1.54,2.67,3,5.37,4.39,8.09l16.82-9.85,3.53,6a7.14,7.14,0,0,0,12.37-.1l17.53-30.94A7.15,7.15,0,0,0,375.54,156.66Z"/>
|
||||
<path class="cls-5" d="M235.3,221.27l-34.52,18.41c-6.08-1-12.11-2.36-18.05-4a230.91,230.91,0,0,1-39.4-14.9c-27.91-9.26-9.21-43.22,11.93-26.76,2.11,1.41,4.28,2.76,6.48,4,3.65,2.15,7.42,4.09,11.24,5.91a156.68,156.68,0,0,0,42.24,13.31A130.42,130.42,0,0,0,239.63,219Z"/>
|
||||
<path class="cls-6" d="M257,258.59l4.09-2.55a135.41,135.41,0,0,0-14.8,34.07,160.44,160.44,0,0,0-5,25.32,158,158,0,0,0-1.06,19.21c0,2.32.11,4.63.26,6.94,4.06,16.9-18.44,27-27.64,11.76-3.51-6-1.35-12.66-1.14-19.08.58-6.21,1.35-12.39,2.39-18.55a200.68,200.68,0,0,1,9.63-36.38l10.53-6.56Z"/>
|
||||
<path class="cls-7" d="M284,219.06c-1.92-3.33-4-6.55-6.21-9.68h0l-9.46,5.54a17.12,17.12,0,0,0-2.69,1.52l-7.34,3.92L240,230.1q-23.52,12.53-47,25.08L164,270.62l-8,4.27c-1.21.65-2.43,1.29-3.64,1.95C132.81,285.25,146.31,314,165.2,304l13.72-8.55,27.53-17.15,45.22-28.18,17.55-10.94,7-4.39a15.73,15.73,0,0,0,3.58-2.29h0l8.56-5Q286.34,223.21,284,219.06Z"/>
|
||||
<path class="cls-8" d="M169.2,115.38A169,169,0,0,0,143.28,118a15,15,0,0,0,5.26,29.44A137.81,137.81,0,0,1,278.31,372.15a14.95,14.95,0,0,0,22.82,19.31A167.69,167.69,0,0,0,169.2,115.38Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 110 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@ -7,6 +7,7 @@
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:home="clr-namespace:Artemis.UI.Screens.Home"
|
||||
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
|
||||
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="574.026"
|
||||
d:DesignWidth="1029.87"
|
||||
@ -37,10 +38,26 @@
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<shared:ArtemisIcon SvgSource="/Resources/Images/Logo/bow.svg" Width="100" Height="100"/>
|
||||
<Image Source="{svgc:SvgImage Source=/Resources/Images/Logo/bow.svg}" Height="100" Width="100"/>
|
||||
<StackPanel Grid.Column="1" Margin="24 0 0 0" VerticalAlignment="Center">
|
||||
<TextBlock Style="{StaticResource MaterialDesignHeadline4TextBlock}" TextWrapping="Wrap">Welcome to Artemis, RGB on steroids.</TextBlock>
|
||||
<TextBlock Style="{StaticResource MaterialDesignHeadline4TextBlock}" TextWrapping="Wrap">
|
||||
<Run Text="Welcome to Artemis, the unified"></Run>
|
||||
<Run Text="RGB">
|
||||
<Run.Foreground>
|
||||
<LinearGradientBrush EndPoint="0,0" StartPoint="1,1">
|
||||
<GradientStop Color="#f19d25"/>
|
||||
<GradientStop Color="#f63d3d" Offset="0.2"/>
|
||||
<GradientStop Color="#c93cec" Offset="0.4"/>
|
||||
<GradientStop Color="#2667f4" Offset="0.6"/>
|
||||
<GradientStop Color="#1cb6e7" Offset="0.8"/>
|
||||
<GradientStop Color="#2df4b5" Offset="1"/>
|
||||
</LinearGradientBrush>
|
||||
</Run.Foreground></Run>
|
||||
<Run Text="platform."></Run>
|
||||
</TextBlock>
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Foreground="{StaticResource SecondaryHueMidBrush}"
|
||||
materialDesign:RippleAssist.Feedback="{StaticResource SecondaryHueMidBrush}"
|
||||
Command="{x:Static materialDesign:DrawerHost.OpenDrawerCommand}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Binoculars" />
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.Plugins.PluginPrerequisiteActionView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Artemis.UI.Screens.Plugins"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:PluginPrerequisiteActionViewModel}">
|
||||
<StackPanel>
|
||||
<ProgressBar Value="{Binding Action.Progress.Percentage, Mode=OneWay}"
|
||||
IsIndeterminate="{Binding Action.ProgressIndeterminate, Mode=OneWay}"
|
||||
Visibility="{Binding Action.ShowProgressBar, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
|
||||
materialDesign:TransitionAssist.DisableTransitions="True"
|
||||
Margin="0 10"/>
|
||||
<ProgressBar Value="{Binding Action.SubProgress.Percentage, Mode=OneWay}"
|
||||
IsIndeterminate="{Binding Action.SubProgressIndeterminate, Mode=OneWay}"
|
||||
Visibility="{Binding Action.ShowSubProgressBar, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}"
|
||||
materialDesign:TransitionAssist.DisableTransitions="True"
|
||||
Margin="0 10"/>
|
||||
|
||||
<TextBlock TextWrapping="Wrap" Text="{Binding Action.Status, Mode=OneWay}" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
@ -0,0 +1,19 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using Artemis.Core;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.Plugins
|
||||
{
|
||||
public class PluginPrerequisiteActionViewModel : Screen
|
||||
{
|
||||
|
||||
|
||||
public PluginPrerequisiteActionViewModel(PluginPrerequisiteAction action)
|
||||
{
|
||||
Action = action;
|
||||
}
|
||||
|
||||
public PluginPrerequisiteAction Action { get; }
|
||||
}
|
||||
}
|
||||
37
src/Artemis.UI/Screens/Plugins/PluginPrerequisiteView.xaml
Normal file
@ -0,0 +1,37 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.Plugins.PluginPrerequisiteView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Artemis.UI.Screens.Plugins"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:PluginPrerequisiteViewModel}">
|
||||
|
||||
<StackPanel>
|
||||
<TextBlock Style="{StaticResource MaterialDesignHeadline6TextBlock}" TextWrapping="Wrap" Text="{Binding PluginPrerequisite.Name, Mode=OneWay}" />
|
||||
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}" TextWrapping="Wrap" Text="{Binding PluginPrerequisite.Description, Mode=OneWay}" Margin="0 0 0 15" />
|
||||
|
||||
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}"
|
||||
Foreground="{DynamicResource MaterialDesignBodyLight}"
|
||||
TextWrapping="Wrap"
|
||||
Visibility="{Binding HasMultipleActions, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
|
||||
<Run Text="Step" />
|
||||
<Run Text="{Binding ActiveStemNumber, Mode=OneWay}" /><Run Text="/" /><Run Text="{Binding Items.Count, Mode=OneWay}" />
|
||||
<Run Text="-" />
|
||||
<Run Text="{Binding ActiveItem.Action.Name, Mode=OneWay}" />
|
||||
</TextBlock>
|
||||
<TextBlock Style="{StaticResource MaterialDesignSubtitle1TextBlock}"
|
||||
Foreground="{DynamicResource MaterialDesignBodyLight}"
|
||||
TextWrapping="Wrap"
|
||||
Text="{Binding ActiveItem.Action.Name, Mode=OneWay}"
|
||||
Visibility="{Binding HasMultipleActions, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}" />
|
||||
<ContentControl s:View.Model="{Binding ActiveItem}"
|
||||
VerticalContentAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
IsTabStop="False"
|
||||
Margin="0 10 0 0" />
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
150
src/Artemis.UI/Screens/Plugins/PluginPrerequisiteViewModel.cs
Normal file
@ -0,0 +1,150 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.Plugins
|
||||
{
|
||||
public class PluginPrerequisiteViewModel : Conductor<PluginPrerequisiteActionViewModel>.Collection.OneActive
|
||||
{
|
||||
private readonly bool _uninstall;
|
||||
private readonly ICoreService _coreService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private bool _installing;
|
||||
private bool _uninstalling;
|
||||
private bool _isMet;
|
||||
|
||||
public PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall, ICoreService coreService, IDialogService dialogService)
|
||||
{
|
||||
_uninstall = uninstall;
|
||||
_coreService = coreService;
|
||||
_dialogService = dialogService;
|
||||
|
||||
PluginPrerequisite = pluginPrerequisite;
|
||||
}
|
||||
|
||||
public PluginPrerequisite PluginPrerequisite { get; }
|
||||
|
||||
public bool Installing
|
||||
{
|
||||
get => _installing;
|
||||
set
|
||||
{
|
||||
SetAndNotify(ref _installing, value);
|
||||
NotifyOfPropertyChange(nameof(Busy));
|
||||
}
|
||||
}
|
||||
|
||||
public bool Uninstalling
|
||||
{
|
||||
get => _uninstalling;
|
||||
set
|
||||
{
|
||||
SetAndNotify(ref _uninstalling, value);
|
||||
NotifyOfPropertyChange(nameof(Busy));
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsMet
|
||||
{
|
||||
get => _isMet;
|
||||
set => SetAndNotify(ref _isMet, value);
|
||||
}
|
||||
|
||||
public bool Busy => Installing || Uninstalling;
|
||||
public int ActiveStemNumber => Items.IndexOf(ActiveItem) + 1;
|
||||
public bool HasMultipleActions => Items.Count > 1;
|
||||
|
||||
public async Task Install(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Busy)
|
||||
return;
|
||||
|
||||
if (PluginPrerequisite.RequiresElevation && !_coreService.IsElevated)
|
||||
{
|
||||
await _dialogService.ShowConfirmDialog("Install plugin prerequisite", "This plugin prerequisite admin rights to install (restart & elevate NYI)");
|
||||
return;
|
||||
}
|
||||
|
||||
Installing = true;
|
||||
try
|
||||
{
|
||||
await PluginPrerequisite.Install(cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Installing = false;
|
||||
IsMet = PluginPrerequisite.IsMet();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Uninstall(CancellationToken cancellationToken)
|
||||
{
|
||||
if (Busy)
|
||||
return;
|
||||
|
||||
if (PluginPrerequisite.RequiresElevation && !_coreService.IsElevated)
|
||||
{
|
||||
await _dialogService.ShowConfirmDialog("Install plugin prerequisite", "This plugin prerequisite admin rights to install (restart & elevate NYI)");
|
||||
return;
|
||||
}
|
||||
|
||||
Uninstalling = true;
|
||||
try
|
||||
{
|
||||
await PluginPrerequisite.Uninstall(cancellationToken);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Uninstalling = false;
|
||||
IsMet = PluginPrerequisite.IsMet();
|
||||
}
|
||||
}
|
||||
|
||||
private void PluginPrerequisiteOnPropertyChanged(object sender, PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(PluginPrerequisite.CurrentAction))
|
||||
ActivateCurrentAction();
|
||||
}
|
||||
|
||||
private void ActivateCurrentAction()
|
||||
{
|
||||
PluginPrerequisiteActionViewModel newActiveItem = Items.FirstOrDefault(i => i.Action == PluginPrerequisite.CurrentAction);
|
||||
if (newActiveItem == null)
|
||||
return;
|
||||
|
||||
ActiveItem = newActiveItem;
|
||||
NotifyOfPropertyChange(nameof(ActiveStemNumber));
|
||||
}
|
||||
|
||||
#region Overrides of Screen
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnClose()
|
||||
{
|
||||
PluginPrerequisite.PropertyChanged -= PluginPrerequisiteOnPropertyChanged;
|
||||
base.OnClose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialActivate()
|
||||
{
|
||||
PluginPrerequisite.PropertyChanged += PluginPrerequisiteOnPropertyChanged;
|
||||
// Could be slow so take it off of the UI thread
|
||||
Task.Run(() => IsMet = PluginPrerequisite.IsMet());
|
||||
|
||||
Items.AddRange(!_uninstall
|
||||
? PluginPrerequisite.InstallActions.Select(a => new PluginPrerequisiteActionViewModel(a))
|
||||
: PluginPrerequisite.UninstallActions.Select(a => new PluginPrerequisiteActionViewModel(a)));
|
||||
|
||||
base.OnInitialActivate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,126 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.Plugins.PluginPrerequisitesInstallDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Artemis.UI.Screens.Plugins"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:PluginPrerequisitesInstallDialogViewModel}">
|
||||
<UserControl.Resources>
|
||||
<shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
|
||||
</UserControl.Resources>
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition MinHeight="150"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="300" />
|
||||
<ColumnDefinition Width="500" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="0" Style="{StaticResource MaterialDesignHeadline6TextBlock}" TextWrapping="Wrap" Margin="0 0 0 20">
|
||||
Plugin/feature prerequisites
|
||||
</TextBlock>
|
||||
|
||||
<ListBox Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ItemsSource="{Binding Prerequisites}"
|
||||
SelectedItem="{Binding ActivePrerequisite, Mode=OneWay}"
|
||||
HorizontalContentAlignment="Stretch">
|
||||
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type local:PluginPrerequisiteViewModel}">
|
||||
<Border Padding="8" BorderThickness="0 0 0 1" BorderBrush="{DynamicResource MaterialDesignDivider}" VerticalAlignment="Stretch">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ToggleButton VerticalAlignment="Center"
|
||||
Style="{StaticResource MaterialDesignActionToggleButton}"
|
||||
Focusable="False"
|
||||
IsHitTestVisible="False"
|
||||
IsChecked="{Binding IsMet}">
|
||||
<ToggleButton.Content>
|
||||
<Border Background="#E74C4C" Width="32" Height="32">
|
||||
<materialDesign:PackIcon Kind="Close" VerticalAlignment="Center" HorizontalAlignment="Center" />
|
||||
</Border>
|
||||
</ToggleButton.Content>
|
||||
<materialDesign:ToggleButtonAssist.OnContent>
|
||||
<materialDesign:PackIcon Kind="Check" />
|
||||
</materialDesign:ToggleButtonAssist.OnContent>
|
||||
</ToggleButton>
|
||||
|
||||
<StackPanel Margin="8 0 0 0" Grid.Column="1" VerticalAlignment="Stretch">
|
||||
<TextBlock FontWeight="Bold" Text="{Binding PluginPrerequisite.Name}" TextWrapping="Wrap" />
|
||||
<TextBlock Text="{Binding PluginPrerequisite.Description}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<ContentControl Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="1"
|
||||
s:View.Model="{Binding ActivePrerequisite}"
|
||||
VerticalContentAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Margin="10 0"
|
||||
IsTabStop="False"
|
||||
Visibility="{Binding ActivePrerequisite, Converter={StaticResource NullToVisibilityConverter}}"/>
|
||||
|
||||
<TextBlock Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource MaterialDesignBody1TextBlock}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="10 0"
|
||||
Visibility="{Binding ActivePrerequisite, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted}">
|
||||
In order for this plugin/feature to function certain prerequisites must be met. <LineBreak /><LineBreak />
|
||||
On the left side you can see all prerequisites and whether they are currently met or not.
|
||||
Clicking install prerequisites will automatically set everything up for you.
|
||||
</TextBlock>
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0 8 0 0"
|
||||
Visibility="{Binding IsFinished, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Focusable="False"
|
||||
IsCancel="True"
|
||||
Command="{s:Action Cancel}"
|
||||
Content="CANCEL" />
|
||||
<Button x:Name="ConfirmButton"
|
||||
Style="{StaticResource MaterialDesignFlatButton}"
|
||||
IsDefault="True"
|
||||
Focusable="True"
|
||||
Command="{s:Action Install}"
|
||||
Content="INSTALL PREREQUISITES" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0 8 0 0"
|
||||
Visibility="{Binding IsFinished, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Focusable="False"
|
||||
IsCancel="True"
|
||||
Command="{s:Action Accept}"
|
||||
Content="FINISH" />
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -0,0 +1,140 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.Plugins
|
||||
{
|
||||
public class PluginPrerequisitesInstallDialogViewModel : DialogViewModelBase
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly List<IPrerequisitesSubject> _subjects;
|
||||
private PluginPrerequisiteViewModel _activePrerequisite;
|
||||
private bool _canInstall;
|
||||
private bool _isFinished;
|
||||
private CancellationTokenSource _tokenSource;
|
||||
|
||||
public PluginPrerequisitesInstallDialogViewModel(List<IPrerequisitesSubject> subjects, IPrerequisitesVmFactory prerequisitesVmFactory, IDialogService dialogService)
|
||||
{
|
||||
_subjects = subjects;
|
||||
_dialogService = dialogService;
|
||||
|
||||
Prerequisites = new BindableCollection<PluginPrerequisiteViewModel>();
|
||||
foreach (IPrerequisitesSubject prerequisitesSubject in subjects)
|
||||
Prerequisites.AddRange(prerequisitesSubject.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, false)));
|
||||
|
||||
foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
|
||||
pluginPrerequisiteViewModel.ConductWith(this);
|
||||
}
|
||||
|
||||
public BindableCollection<PluginPrerequisiteViewModel> Prerequisites { get; }
|
||||
|
||||
public PluginPrerequisiteViewModel ActivePrerequisite
|
||||
{
|
||||
get => _activePrerequisite;
|
||||
set => SetAndNotify(ref _activePrerequisite, value);
|
||||
}
|
||||
|
||||
public bool CanInstall
|
||||
{
|
||||
get => _canInstall;
|
||||
set => SetAndNotify(ref _canInstall, value);
|
||||
}
|
||||
|
||||
public bool IsFinished
|
||||
{
|
||||
get => _isFinished;
|
||||
set => SetAndNotify(ref _isFinished, value);
|
||||
}
|
||||
|
||||
#region Overrides of DialogViewModelBase
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDialogClosed(object sender, DialogClosingEventArgs e)
|
||||
{
|
||||
_tokenSource?.Cancel();
|
||||
base.OnDialogClosed(sender, e);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public async void Install()
|
||||
{
|
||||
CanInstall = false;
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
|
||||
{
|
||||
pluginPrerequisiteViewModel.IsMet = pluginPrerequisiteViewModel.PluginPrerequisite.IsMet();
|
||||
if (pluginPrerequisiteViewModel.IsMet)
|
||||
continue;
|
||||
|
||||
ActivePrerequisite = pluginPrerequisiteViewModel;
|
||||
await ActivePrerequisite.Install(_tokenSource.Token);
|
||||
|
||||
// Wait after the task finished for the user to process what happened
|
||||
if (pluginPrerequisiteViewModel != Prerequisites.Last())
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
if (Prerequisites.All(p => p.IsMet))
|
||||
{
|
||||
IsFinished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// This shouldn't be happening and the experience isn't very nice for the user (too lazy to make a nice UI for such an edge case)
|
||||
// but at least give some feedback
|
||||
Session?.Close(false);
|
||||
await _dialogService.ShowConfirmDialog(
|
||||
"Plugin prerequisites",
|
||||
"All prerequisites are installed but some still aren't met. \r\nPlease try again or contact the plugin creator.",
|
||||
"Confirm",
|
||||
""
|
||||
);
|
||||
await Show(_dialogService, _subjects);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
finally
|
||||
{
|
||||
CanInstall = true;
|
||||
_tokenSource.Dispose();
|
||||
_tokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Accept()
|
||||
{
|
||||
Session?.Close(true);
|
||||
}
|
||||
|
||||
public static Task<object> Show(IDialogService dialogService, List<IPrerequisitesSubject> subjects)
|
||||
{
|
||||
return dialogService.ShowDialog<PluginPrerequisitesInstallDialogViewModel>(new Dictionary<string, object> {{"subjects", subjects}});
|
||||
}
|
||||
|
||||
#region Overrides of Screen
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialActivate()
|
||||
{
|
||||
CanInstall = false;
|
||||
Task.Run(() => CanInstall = Prerequisites.Any(p => !p.PluginPrerequisite.IsMet()));
|
||||
|
||||
base.OnInitialActivate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
<UserControl x:Class="Artemis.UI.Screens.Plugins.PluginPrerequisitesUninstallDialogView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:local="clr-namespace:Artemis.UI.Screens.Plugins"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:PluginPrerequisitesUninstallDialogViewModel}">
|
||||
<UserControl.Resources>
|
||||
<shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
|
||||
</UserControl.Resources>
|
||||
<Grid Margin="16">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition MinHeight="150"/>
|
||||
<RowDefinition Height="Auto"/>
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="300" />
|
||||
<ColumnDefinition Width="500" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Row="0" Style="{StaticResource MaterialDesignHeadline6TextBlock}" TextWrapping="Wrap" Margin="0 0 0 20">
|
||||
Plugin/feature prerequisites
|
||||
</TextBlock>
|
||||
|
||||
<ListBox Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
ItemsSource="{Binding Prerequisites}"
|
||||
SelectedItem="{Binding ActivePrerequisite, Mode=OneWay}"
|
||||
HorizontalContentAlignment="Stretch">
|
||||
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type local:PluginPrerequisiteViewModel}">
|
||||
<Border Padding="8" BorderThickness="0 0 0 1" BorderBrush="{DynamicResource MaterialDesignDivider}" VerticalAlignment="Stretch">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<ToggleButton VerticalAlignment="Center"
|
||||
Style="{StaticResource MaterialDesignActionToggleButton}"
|
||||
Focusable="False"
|
||||
IsHitTestVisible="False"
|
||||
IsChecked="{Binding IsMet}">
|
||||
<ToggleButton.Content>
|
||||
<Border Background="#E74C4C" Width="32" Height="32">
|
||||
<materialDesign:PackIcon Kind="Close" VerticalAlignment="Center" HorizontalAlignment="Center" />
|
||||
</Border>
|
||||
</ToggleButton.Content>
|
||||
<materialDesign:ToggleButtonAssist.OnContent>
|
||||
<materialDesign:PackIcon Kind="Check" />
|
||||
</materialDesign:ToggleButtonAssist.OnContent>
|
||||
</ToggleButton>
|
||||
|
||||
<StackPanel Margin="8 0 0 0" Grid.Column="1" VerticalAlignment="Stretch">
|
||||
<TextBlock FontWeight="Bold" Text="{Binding PluginPrerequisite.Name}" TextWrapping="Wrap" />
|
||||
<TextBlock Text="{Binding PluginPrerequisite.Description}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<ContentControl Grid.Row="0"
|
||||
Grid.RowSpan="2"
|
||||
Grid.Column="1"
|
||||
s:View.Model="{Binding ActivePrerequisite}"
|
||||
VerticalContentAlignment="Stretch"
|
||||
HorizontalContentAlignment="Stretch"
|
||||
Margin="10 0"
|
||||
IsTabStop="False"
|
||||
Visibility="{Binding ActivePrerequisite, Converter={StaticResource NullToVisibilityConverter}}"/>
|
||||
|
||||
<TextBlock Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Style="{StaticResource MaterialDesignBody1TextBlock}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="10 0"
|
||||
Visibility="{Binding ActivePrerequisite, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted}">
|
||||
This plugin/feature installed certain prerequisites in order to function. <LineBreak /><LineBreak />
|
||||
In this screen you can chose to remove these, this will mean the plugin/feature will no longer work until you reinstall the prerequisites.
|
||||
</TextBlock>
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0 8 0 0"
|
||||
Visibility="{Binding IsFinished, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}">
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Focusable="False"
|
||||
IsCancel="True"
|
||||
Command="{s:Action Cancel}"
|
||||
Content="{Binding CancelLabel}" />
|
||||
<Button x:Name="ConfirmButton"
|
||||
Style="{StaticResource MaterialDesignFlatButton}"
|
||||
IsDefault="True"
|
||||
Focusable="True"
|
||||
Command="{s:Action Uninstall}"
|
||||
Content="REMOVE PREREQUISITES" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="2"
|
||||
Grid.Column="0"
|
||||
Grid.ColumnSpan="2"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0 8 0 0"
|
||||
Visibility="{Binding IsFinished, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}">
|
||||
<Button Style="{StaticResource MaterialDesignFlatButton}"
|
||||
Focusable="False"
|
||||
IsCancel="True"
|
||||
Command="{s:Action Accept}"
|
||||
Content="FINISH" />
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -0,0 +1,172 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Stylet;
|
||||
|
||||
namespace Artemis.UI.Screens.Plugins
|
||||
{
|
||||
public class PluginPrerequisitesUninstallDialogViewModel : DialogViewModelBase
|
||||
{
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private readonly List<IPrerequisitesSubject> _subjects;
|
||||
private PluginPrerequisiteViewModel _activePrerequisite;
|
||||
private bool _canUninstall;
|
||||
private bool _isFinished;
|
||||
private CancellationTokenSource _tokenSource;
|
||||
|
||||
public PluginPrerequisitesUninstallDialogViewModel(List<IPrerequisitesSubject> subjects, string cancelLabel, IPrerequisitesVmFactory prerequisitesVmFactory,
|
||||
IDialogService dialogService, IPluginManagementService pluginManagementService)
|
||||
{
|
||||
_subjects = subjects;
|
||||
_dialogService = dialogService;
|
||||
_pluginManagementService = pluginManagementService;
|
||||
|
||||
CancelLabel = cancelLabel;
|
||||
Prerequisites = new BindableCollection<PluginPrerequisiteViewModel>();
|
||||
foreach (IPrerequisitesSubject prerequisitesSubject in subjects)
|
||||
Prerequisites.AddRange(prerequisitesSubject.Prerequisites.Select(p => prerequisitesVmFactory.PluginPrerequisiteViewModel(p, true)));
|
||||
|
||||
foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
|
||||
pluginPrerequisiteViewModel.ConductWith(this);
|
||||
}
|
||||
|
||||
public string CancelLabel { get; }
|
||||
public BindableCollection<PluginPrerequisiteViewModel> Prerequisites { get; }
|
||||
|
||||
public PluginPrerequisiteViewModel ActivePrerequisite
|
||||
{
|
||||
get => _activePrerequisite;
|
||||
set => SetAndNotify(ref _activePrerequisite, value);
|
||||
}
|
||||
|
||||
public bool CanUninstall
|
||||
{
|
||||
get => _canUninstall;
|
||||
set => SetAndNotify(ref _canUninstall, value);
|
||||
}
|
||||
|
||||
public bool IsFinished
|
||||
{
|
||||
get => _isFinished;
|
||||
set => SetAndNotify(ref _isFinished, value);
|
||||
}
|
||||
|
||||
#region Overrides of DialogViewModelBase
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnDialogClosed(object sender, DialogClosingEventArgs e)
|
||||
{
|
||||
_tokenSource?.Cancel();
|
||||
base.OnDialogClosed(sender, e);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public async void Uninstall()
|
||||
{
|
||||
CanUninstall = false;
|
||||
|
||||
// Disable all subjects that are plugins, this will disable their features too
|
||||
foreach (IPrerequisitesSubject prerequisitesSubject in _subjects)
|
||||
{
|
||||
if (prerequisitesSubject is PluginInfo pluginInfo)
|
||||
_pluginManagementService.DisablePlugin(pluginInfo.Plugin, true);
|
||||
}
|
||||
|
||||
// Disable all subjects that are features if still required
|
||||
foreach (IPrerequisitesSubject prerequisitesSubject in _subjects)
|
||||
{
|
||||
if (prerequisitesSubject is not PluginFeatureInfo featureInfo)
|
||||
continue;
|
||||
|
||||
// Disable the parent plugin if the feature is AlwaysEnabled
|
||||
if (featureInfo.AlwaysEnabled)
|
||||
_pluginManagementService.DisablePlugin(featureInfo.Plugin, true);
|
||||
else if (featureInfo.Instance != null)
|
||||
_pluginManagementService.DisablePluginFeature(featureInfo.Instance, true);
|
||||
}
|
||||
|
||||
_tokenSource = new CancellationTokenSource();
|
||||
|
||||
try
|
||||
{
|
||||
foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
|
||||
{
|
||||
pluginPrerequisiteViewModel.IsMet = pluginPrerequisiteViewModel.PluginPrerequisite.IsMet();
|
||||
if (!pluginPrerequisiteViewModel.IsMet)
|
||||
continue;
|
||||
|
||||
ActivePrerequisite = pluginPrerequisiteViewModel;
|
||||
await ActivePrerequisite.Uninstall(_tokenSource.Token);
|
||||
|
||||
// Wait after the task finished for the user to process what happened
|
||||
if (pluginPrerequisiteViewModel != Prerequisites.Last())
|
||||
await Task.Delay(1000);
|
||||
}
|
||||
|
||||
if (Prerequisites.All(p => !p.IsMet))
|
||||
{
|
||||
IsFinished = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// This shouldn't be happening and the experience isn't very nice for the user (too lazy to make a nice UI for such an edge case)
|
||||
// but at least give some feedback
|
||||
Session?.Close(false);
|
||||
await _dialogService.ShowConfirmDialog(
|
||||
"Plugin prerequisites",
|
||||
"The plugin was not able to fully remove all prerequisites. \r\nPlease try again or contact the plugin creator.",
|
||||
"Confirm",
|
||||
""
|
||||
);
|
||||
await Show(_dialogService, _subjects);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
finally
|
||||
{
|
||||
CanUninstall = true;
|
||||
_tokenSource.Dispose();
|
||||
_tokenSource = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Accept()
|
||||
{
|
||||
Session?.Close(true);
|
||||
}
|
||||
|
||||
public static Task<object> Show(IDialogService dialogService, List<IPrerequisitesSubject> subjects, string cancelLabel = "CANCEL")
|
||||
{
|
||||
return dialogService.ShowDialog<PluginPrerequisitesUninstallDialogViewModel>(new Dictionary<string, object>
|
||||
{
|
||||
{"subjects", subjects},
|
||||
{"cancelLabel", cancelLabel},
|
||||
});
|
||||
}
|
||||
|
||||
#region Overrides of Screen
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnInitialActivate()
|
||||
{
|
||||
CanUninstall = false;
|
||||
// Could be slow so take it off of the UI thread
|
||||
Task.Run(() => CanUninstall = Prerequisites.Any(p => p.PluginPrerequisite.IsMet()));
|
||||
|
||||
base.OnInitialActivate();
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -144,7 +144,7 @@
|
||||
<ColumnDefinition />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="0"
|
||||
Style="{StaticResource MaterialDesignRaisedDarkButton}"
|
||||
Style="{StaticResource MaterialDesignRaisedAccentButton}"
|
||||
Margin="0 0 5 0"
|
||||
ToolTip="Copy the entire data binding"
|
||||
Command="{s:Action CopyDataBinding}">
|
||||
@ -154,7 +154,7 @@
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Grid.Column="1"
|
||||
Style="{StaticResource MaterialDesignRaisedDarkButton}"
|
||||
Style="{StaticResource MaterialDesignRaisedAccentButton}"
|
||||
Margin="5 0 0 0"
|
||||
ToolTip="Paste the entire data binding"
|
||||
Command="{s:Action PasteDataBinding}">
|
||||
|
||||
@ -389,7 +389,7 @@
|
||||
IsEnabled="{Binding SelectedProfileElement, Converter={StaticResource NullToBooleanConverter}}"
|
||||
Visibility="{Binding TimelineVisible, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
<Button.Style>
|
||||
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource MaterialDesignFlatMidBgButton}">
|
||||
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource MaterialDesignFlatAccentBgButton}">
|
||||
<Style.Triggers>
|
||||
<EventTrigger RoutedEvent="Click">
|
||||
<EventTrigger.Actions>
|
||||
@ -440,6 +440,7 @@
|
||||
IsSnapToTickEnabled="True"
|
||||
AutoToolTipPlacement="TopLeft"
|
||||
Value="{Binding ProfileEditorService.PixelsPerSecond}"
|
||||
Foreground="{StaticResource SecondaryHueMidBrush}"
|
||||
Width="319" />
|
||||
|
||||
</Grid>
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
d:DesignHeight="800"
|
||||
d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance windows:LayerBrushSettingsWindowViewModel}"
|
||||
Icon="/Resources/Images/Logo/logo-512.png">
|
||||
Icon="/Resources/Images/Logo/bow.ico">
|
||||
<materialDesign:DialogHost IsTabStop="False"
|
||||
Focusable="False"
|
||||
Identifier="BrushSettingsDialog"
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
d:DesignHeight="800"
|
||||
d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance windows:LayerEffectSettingsWindowViewModel}"
|
||||
Icon="/Resources/Images/Logo/logo-512.png">
|
||||
Icon="/Resources/Images/Logo/bow.ico">
|
||||
<DockPanel>
|
||||
<controls:AppBar Type="Dense" Title="{Binding ActiveItem.LayerEffect.Descriptor.DisplayName}" DockPanel.Dock="Top" Margin="-18 0 0 0" ShowShadow="False">
|
||||
<controls:AppBar.AppIcon>
|
||||
|
||||
@ -8,11 +8,13 @@
|
||||
xmlns:screens="clr-namespace:Artemis.UI.Screens"
|
||||
xmlns:mde="clr-namespace:MaterialDesignExtensions.Controls;assembly=MaterialDesignExtensions"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
|
||||
xmlns:localization="clr-namespace:MaterialDesignExtensions.Localization;assembly=MaterialDesignExtensions"
|
||||
mc:Ignorable="d"
|
||||
FadeContentIfInactive="False"
|
||||
Icon="/Resources/Images/Logo/logo-512.png"
|
||||
Icon="/Resources/Images/Logo/bow.ico"
|
||||
Title="{Binding WindowTitle}"
|
||||
TitleBarIcon="{StaticResource BowIcon}"
|
||||
TitleBarIcon="{svgc:SvgImage Source=/Resources/Images/Logo/bow-white.svg}"
|
||||
Foreground="{DynamicResource MaterialDesignBody}"
|
||||
Background="{DynamicResource MaterialDesignPaper}"
|
||||
FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto"
|
||||
|
||||
@ -27,7 +27,6 @@ namespace Artemis.UI.Screens
|
||||
{
|
||||
private readonly IRegistrationService _builtInRegistrationService;
|
||||
private readonly IMessageService _messageService;
|
||||
private readonly PluginSetting<ApplicationColorScheme> _colorScheme;
|
||||
private readonly ICoreService _coreService;
|
||||
private readonly IWindowManager _windowManager;
|
||||
private readonly IDebugService _debugService;
|
||||
@ -36,7 +35,6 @@ namespace Artemis.UI.Screens
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly Timer _frameTimeUpdateTimer;
|
||||
private readonly SidebarViewModel _sidebarViewModel;
|
||||
private readonly ThemeWatcher _themeWatcher;
|
||||
private readonly PluginSetting<WindowSize> _windowSize;
|
||||
private bool _activeItemReady;
|
||||
private string _frameTime;
|
||||
@ -67,12 +65,7 @@ namespace Artemis.UI.Screens
|
||||
_sidebarViewModel = sidebarViewModel;
|
||||
_frameTimeUpdateTimer = new Timer(500);
|
||||
|
||||
_colorScheme = _settingsService.GetSetting("UI.ColorScheme", ApplicationColorScheme.Automatic);
|
||||
_windowSize = _settingsService.GetSetting<WindowSize>("UI.RootWindowSize");
|
||||
|
||||
_themeWatcher = new ThemeWatcher();
|
||||
ApplyColorSchemeSetting();
|
||||
|
||||
_sidebarViewModel.ConductWith(this);
|
||||
|
||||
ActiveItem = sidebarViewModel.SelectedItem;
|
||||
@ -191,52 +184,12 @@ namespace Artemis.UI.Screens
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyColorSchemeSetting()
|
||||
{
|
||||
if (_colorScheme.Value == ApplicationColorScheme.Automatic)
|
||||
ApplyWindowsTheme(_themeWatcher.GetWindowsTheme());
|
||||
else
|
||||
ChangeMaterialColors(_colorScheme.Value);
|
||||
}
|
||||
|
||||
private void ApplyWindowsTheme(ThemeWatcher.WindowsTheme windowsTheme)
|
||||
{
|
||||
if (_colorScheme.Value != ApplicationColorScheme.Automatic)
|
||||
return;
|
||||
|
||||
if (windowsTheme == ThemeWatcher.WindowsTheme.Dark)
|
||||
ChangeMaterialColors(ApplicationColorScheme.Dark);
|
||||
else
|
||||
ChangeMaterialColors(ApplicationColorScheme.Light);
|
||||
}
|
||||
|
||||
private void ChangeMaterialColors(ApplicationColorScheme colorScheme)
|
||||
{
|
||||
PaletteHelper paletteHelper = new();
|
||||
ITheme theme = paletteHelper.GetTheme();
|
||||
theme.SetBaseTheme(colorScheme == ApplicationColorScheme.Dark ? Theme.Dark : Theme.Light);
|
||||
paletteHelper.SetTheme(theme);
|
||||
|
||||
MaterialDesignExtensions.Themes.PaletteHelper extensionsPaletteHelper = new();
|
||||
extensionsPaletteHelper.SetLightDark(colorScheme == ApplicationColorScheme.Dark);
|
||||
}
|
||||
|
||||
|
||||
private void OnFrameTimeUpdateTimerOnElapsed(object sender, ElapsedEventArgs args)
|
||||
{
|
||||
UpdateFrameTime();
|
||||
}
|
||||
|
||||
private void ThemeWatcherOnThemeChanged(object sender, WindowsThemeEventArgs e)
|
||||
{
|
||||
ApplyWindowsTheme(e.Theme);
|
||||
}
|
||||
|
||||
private void ColorSchemeOnSettingChanged(object sender, EventArgs e)
|
||||
{
|
||||
ApplyColorSchemeSetting();
|
||||
}
|
||||
|
||||
|
||||
private void PinSidebarOnSettingChanged(object sender, EventArgs e)
|
||||
{
|
||||
UpdateSidebarPinState();
|
||||
@ -280,8 +233,6 @@ namespace Artemis.UI.Screens
|
||||
_builtInRegistrationService.RegisterBuiltInPropertyEditors();
|
||||
|
||||
_frameTimeUpdateTimer.Elapsed += OnFrameTimeUpdateTimerOnElapsed;
|
||||
_colorScheme.SettingChanged += ColorSchemeOnSettingChanged;
|
||||
_themeWatcher.ThemeChanged += ThemeWatcherOnThemeChanged;
|
||||
_sidebarViewModel.PropertyChanged += SidebarViewModelOnPropertyChanged;
|
||||
PinSidebar.SettingChanged += PinSidebarOnSettingChanged;
|
||||
|
||||
@ -315,8 +266,6 @@ namespace Artemis.UI.Screens
|
||||
_windowSize.Save();
|
||||
|
||||
_frameTimeUpdateTimer.Elapsed -= OnFrameTimeUpdateTimerOnElapsed;
|
||||
_colorScheme.SettingChanged -= ColorSchemeOnSettingChanged;
|
||||
_themeWatcher.ThemeChanged -= ThemeWatcherOnThemeChanged;
|
||||
_sidebarViewModel.PropertyChanged -= SidebarViewModelOnPropertyChanged;
|
||||
PinSidebar.SettingChanged -= PinSidebarOnSettingChanged;
|
||||
|
||||
|
||||
@ -7,9 +7,10 @@
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:mde="clr-namespace:MaterialDesignExtensions.Controls;assembly=MaterialDesignExtensions"
|
||||
xmlns:debug="clr-namespace:Artemis.UI.Screens.Settings.Debug"
|
||||
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
|
||||
mc:Ignorable="d"
|
||||
Title="Artemis debugger"
|
||||
TitleBarIcon="{StaticResource BowIcon}"
|
||||
TitleBarIcon="{svgc:SvgImage Source=/Resources/Images/Logo/bow-white.svg}"
|
||||
Foreground="{DynamicResource MaterialDesignBody}"
|
||||
Background="{DynamicResource MaterialDesignPaper}"
|
||||
FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto"
|
||||
@ -18,7 +19,7 @@
|
||||
Width="1200"
|
||||
Height="800"
|
||||
d:DesignHeight="800" d:DesignWidth="800" d:DataContext="{d:DesignInstance debug:DebugViewModel}"
|
||||
Icon="/Resources/Images/Logo/logo-512.png"
|
||||
Icon="/Resources/Images/Logo/bow.ico"
|
||||
Topmost="{Binding StayOnTopSetting.Value}">
|
||||
<materialDesign:DialogHost IsTabStop="False"
|
||||
Focusable="False"
|
||||
|
||||
@ -8,9 +8,10 @@
|
||||
xmlns:mde="clr-namespace:MaterialDesignExtensions.Controls;assembly=MaterialDesignExtensions"
|
||||
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
|
||||
xmlns:device="clr-namespace:Artemis.UI.Screens.Settings.Device"
|
||||
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
|
||||
mc:Ignorable="d"
|
||||
Title="{Binding DisplayName}"
|
||||
TitleBarIcon="{StaticResource BowIcon}"
|
||||
TitleBarIcon="{svgc:SvgImage Source=/Resources/Images/Logo/bow-white.svg}"
|
||||
Foreground="{DynamicResource MaterialDesignBody}"
|
||||
Background="{DynamicResource MaterialDesignPaper}"
|
||||
FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto"
|
||||
@ -20,7 +21,7 @@
|
||||
Height="800"
|
||||
d:DesignHeight="800" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance device:DeviceDialogViewModel}"
|
||||
Icon="/Resources/Images/Logo/logo-512.png">
|
||||
Icon="/Resources/Images/Logo/bow.ico">
|
||||
<mde:MaterialWindow.Resources>
|
||||
<shared:NullToVisibilityConverter x:Key="NullToVisibilityConverter"/>
|
||||
</mde:MaterialWindow.Resources>
|
||||
|
||||
@ -6,13 +6,14 @@
|
||||
xmlns:local="clr-namespace:Artemis.UI.Screens.Settings.Tabs.About"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
|
||||
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:AboutTabViewModel}">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
|
||||
<StackPanel Margin="15" MaxWidth="800">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<shared:ArtemisIcon SvgSource="/Resources/Images/Logo/bow.svg" Width="60" Height="80" Margin="0 0 20 0" />
|
||||
<Image Source="{svgc:SvgImage Source=/Resources/Images/Logo/bow.svg}" Width="60" Height="80" Margin="0 0 20 0" />
|
||||
<TextBlock Style="{StaticResource MaterialDesignHeadline2TextBlock}">
|
||||
Artemis 2
|
||||
</TextBlock>
|
||||
@ -49,7 +50,8 @@
|
||||
</StackPanel>
|
||||
<TextBlock Style="{StaticResource MaterialDesignBody1TextBlock}" LineHeight="25" Margin="10 0 0 0">
|
||||
- All the people on Discord providing feedback and testing<LineBreak />
|
||||
- Aureshion - Default device images
|
||||
- Aureshion - Default device images<LineBreak />
|
||||
- kaisax - Logo design
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</materialDesign:Card>
|
||||
|
||||
@ -49,10 +49,10 @@
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
<StackPanel HorizontalAlignment="Right" Grid.Row="2" Orientation="Horizontal" Margin="8">
|
||||
<CheckBox Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding IsDeviceEnabled}">
|
||||
<CheckBox Style="{StaticResource MaterialDesignAccentCheckBox}" IsChecked="{Binding IsDeviceEnabled}">
|
||||
Device enabled
|
||||
</CheckBox>
|
||||
<materialDesign:PopupBox Style="{StaticResource MaterialDesignToolPopupBox}" Padding="2 0 2 0">
|
||||
<materialDesign:PopupBox Style="{StaticResource MaterialDesignToolPopupBox}" Padding="2 0 2 0" Foreground="{StaticResource MaterialDesignBody}">
|
||||
<StackPanel>
|
||||
<Button Command="{s:Action OpenPluginDirectory}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
|
||||
@ -12,7 +12,13 @@
|
||||
d:DesignHeight="450" d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:PluginFeatureViewModel}">
|
||||
<UserControl.Resources>
|
||||
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary
|
||||
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
<converters:NullToVisibilityConverter x:Key="NullToVisibilityConverter" />
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<Grid Margin="-3 -8">
|
||||
<Grid.ColumnDefinitions>
|
||||
@ -23,11 +29,11 @@
|
||||
|
||||
<!-- Icon column -->
|
||||
<shared:ArtemisIcon Grid.Column="0"
|
||||
Icon="{Binding FeatureInfo.Icon}"
|
||||
Width="20"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Visibility="{Binding LoadException, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted, FallbackValue=Collapsed}" />
|
||||
Icon="{Binding FeatureInfo.Icon}"
|
||||
Width="20"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Center"
|
||||
Visibility="{Binding LoadException, Converter={StaticResource NullToVisibilityConverter}, ConverterParameter=Inverted, FallbackValue=Collapsed}" />
|
||||
|
||||
<Button Grid.Column="0"
|
||||
Margin="-8"
|
||||
@ -42,16 +48,16 @@
|
||||
</Button>
|
||||
|
||||
<!-- Display name column -->
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding FeatureInfo.Name}"
|
||||
Style="{StaticResource MaterialDesignTextBlock}"
|
||||
VerticalAlignment="Center"
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding FeatureInfo.Name}"
|
||||
Style="{StaticResource MaterialDesignTextBlock}"
|
||||
VerticalAlignment="Center"
|
||||
TextWrapping="Wrap"
|
||||
ToolTip="{Binding FeatureInfo.Description}" />
|
||||
|
||||
<!-- Enable toggle column -->
|
||||
<StackPanel Grid.Column="2"
|
||||
HorizontalAlignment="Right"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="8"
|
||||
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay, FallbackValue=Collapsed}"
|
||||
Orientation="Horizontal"
|
||||
@ -63,11 +69,31 @@
|
||||
VerticalAlignment="Center"
|
||||
Margin="0 0 5 0"
|
||||
Visibility="{Binding ShowShield, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay}" />
|
||||
<CheckBox Style="{StaticResource MaterialDesignCheckBox}"
|
||||
<CheckBox Style="{StaticResource MaterialDesignCheckBox}"
|
||||
IsChecked="{Binding IsEnabled}"
|
||||
IsEnabled="{Binding CanToggleEnabled}">
|
||||
Feature enabled
|
||||
</CheckBox>
|
||||
|
||||
<materialDesign:PopupBox Style="{StaticResource MaterialDesignToolPopupBox}"
|
||||
Margin="0 -4 -10 -4"
|
||||
Foreground="{StaticResource MaterialDesignBody}"
|
||||
IsEnabled="{Binding IsPopupEnabled}">
|
||||
<StackPanel>
|
||||
<Button Command="{s:Action InstallPrerequisites}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="CheckAll" Margin="0 0 10 0 " VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center">Install prerequisites</TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Command="{s:Action RemovePrerequisites}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Delete" Margin="0 0 10 0 " VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center">Remove prerequisites</TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</materialDesign:PopupBox>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="7"
|
||||
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.Instance}, Mode=OneWay, FallbackValue=Collapsed}">
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
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.Screens.Plugins;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Stylet;
|
||||
|
||||
@ -13,11 +16,11 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
{
|
||||
private readonly ICoreService _coreService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IMessageService _messageService;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private bool _enabling;
|
||||
private readonly IMessageService _messageService;
|
||||
|
||||
public PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo,
|
||||
public PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo,
|
||||
bool showShield,
|
||||
ICoreService coreService,
|
||||
IDialogService dialogService,
|
||||
@ -51,6 +54,9 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
@ -72,6 +78,21 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
_dialogService.ShowExceptionDialog("Feature failed to enable", LoadException);
|
||||
}
|
||||
|
||||
public async Task InstallPrerequisites()
|
||||
{
|
||||
if (FeatureInfo.Prerequisites.Any())
|
||||
await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, new List<IPrerequisitesSubject> { FeatureInfo });
|
||||
}
|
||||
|
||||
public async Task RemovePrerequisites()
|
||||
{
|
||||
if (FeatureInfo.Prerequisites.Any(p => p.UninstallActions.Any()))
|
||||
{
|
||||
await PluginPrerequisitesUninstallDialogViewModel.Show(_dialogService, new List<IPrerequisitesSubject> {FeatureInfo});
|
||||
NotifyOfPropertyChange(nameof(IsEnabled));
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInitialActivate()
|
||||
{
|
||||
_pluginManagementService.PluginFeatureEnabling += OnFeatureEnabling;
|
||||
@ -80,7 +101,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
|
||||
FeatureInfo.Plugin.Enabled += PluginOnToggled;
|
||||
FeatureInfo.Plugin.Disabled += PluginOnToggled;
|
||||
|
||||
|
||||
base.OnInitialActivate();
|
||||
}
|
||||
|
||||
@ -120,7 +141,18 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Run(() => _pluginManagementService.EnablePluginFeature(FeatureInfo.Instance, true));
|
||||
// Check if all prerequisites are met async
|
||||
if (!FeatureInfo.ArePrerequisitesMet())
|
||||
{
|
||||
await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, new List<IPrerequisitesSubject> { FeatureInfo });
|
||||
if (!FeatureInfo.ArePrerequisitesMet())
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(IsEnabled));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Run(() => _pluginManagementService.EnablePluginFeature(FeatureInfo.Instance!, true));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@ -138,8 +170,6 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
}
|
||||
}
|
||||
|
||||
#region Event handlers
|
||||
|
||||
private void OnFeatureEnabling(object sender, PluginFeatureEventArgs e)
|
||||
{
|
||||
if (e.PluginFeature != FeatureInfo.Instance) return;
|
||||
@ -159,7 +189,5 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
{
|
||||
NotifyOfPropertyChange(nameof(CanToggleEnabled));
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@ -49,7 +49,6 @@
|
||||
|
||||
<ListBox Grid.Row="1"
|
||||
ItemsSource="{Binding Items}"
|
||||
materialDesign:RippleAssist.IsDisabled="True"
|
||||
VirtualizingPanel.ScrollUnit="Pixel"
|
||||
HorizontalAlignment="Stretch"
|
||||
HorizontalContentAlignment="Center"
|
||||
|
||||
@ -12,6 +12,14 @@
|
||||
d:DataContext="{d:DesignInstance devices:PluginSettingsViewModel}"
|
||||
mc:Ignorable="d"
|
||||
d:DesignHeight="450" d:DesignWidth="800">
|
||||
<UserControl.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceDictionary
|
||||
Source="pack://application:,,,/MaterialDesignThemes.Wpf;component/Themes/MaterialDesignTheme.PopupBox.xaml" />
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</UserControl.Resources>
|
||||
<materialDesign:Card Width="900">
|
||||
<Grid Margin="8">
|
||||
<Grid.ColumnDefinitions>
|
||||
@ -61,34 +69,60 @@
|
||||
</Grid>
|
||||
|
||||
|
||||
<StackPanel Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
VerticalAlignment="Bottom"
|
||||
Orientation="Horizontal">
|
||||
<Button
|
||||
VerticalAlignment="Bottom"
|
||||
Style="{StaticResource MaterialDesignRaisedButton}"
|
||||
ToolTip="Open the plugins settings window"
|
||||
Margin="4"
|
||||
Command="{s:Action OpenSettings}">
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" VerticalAlignment="Bottom" Orientation="Horizontal">
|
||||
<Button VerticalAlignment="Bottom"
|
||||
Style="{StaticResource MaterialDesignRaisedButton}"
|
||||
ToolTip="Open the plugins settings window"
|
||||
Margin="4"
|
||||
Command="{s:Action OpenSettings}">
|
||||
SETTINGS
|
||||
</Button>
|
||||
<Button
|
||||
VerticalAlignment="Bottom"
|
||||
Style="{StaticResource MaterialDesignOutlinedButton}"
|
||||
ToolTip="Clear plugin settings"
|
||||
Margin="4"
|
||||
Command="{s:Action RemoveSettings}">
|
||||
<materialDesign:PackIcon Kind="DatabaseRemove" />
|
||||
</Button>
|
||||
<Button
|
||||
VerticalAlignment="Bottom"
|
||||
Style="{StaticResource MaterialDesignOutlinedButton}"
|
||||
ToolTip="Remove plugin"
|
||||
Margin="4"
|
||||
Command="{s:Action Remove}">
|
||||
<materialDesign:PackIcon Kind="DeleteForever" />
|
||||
</Button>
|
||||
|
||||
<materialDesign:PopupBox Style="{StaticResource MaterialDesignToolPopupBox}"
|
||||
Padding="2 0 2 0"
|
||||
Foreground="{StaticResource MaterialDesignBody}"
|
||||
IsPopupOpen="{Binding IsSettingsPopupOpen, Mode=TwoWay}">
|
||||
<StackPanel>
|
||||
<Button Command="{s:Action OpenPluginDirectory}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="FolderOpen" Margin="0 0 10 0 " VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center">Open plugin directory</TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Command="{s:Action Reload}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Reload" Margin="0 0 10 0 " VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center">Reload plugin</TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Separator />
|
||||
<Button Command="{s:Action InstallPrerequisites}" >
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="CheckAll" Margin="0 0 10 0 " VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center">Install prerequisites</TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Command="{s:Action RemovePrerequisites}" >
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="Delete" Margin="0 0 10 0 " VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center">Remove prerequisites</TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Separator />
|
||||
<Button Command="{s:Action RemoveSettings}" >
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="DatabaseRemove" Margin="0 0 10 0 " VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center">Clear plugin settings</TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
<Button Command="{s:Action Remove}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<materialDesign:PackIcon Kind="DeleteForever" Margin="0 0 10 0 " VerticalAlignment="Center" />
|
||||
<TextBlock VerticalAlignment="Center">Remove plugin</TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</materialDesign:PopupBox>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
@ -98,7 +132,7 @@
|
||||
VerticalAlignment="Bottom"
|
||||
Margin="8"
|
||||
Visibility="{Binding Enabling, Converter={x:Static s:BoolToVisibilityConverter.InverseInstance}, Mode=OneWay}"
|
||||
Style="{StaticResource MaterialDesignCheckBox}" IsChecked="{Binding IsEnabled}">
|
||||
Style="{StaticResource MaterialDesignAccentCheckBox}" IsChecked="{Binding IsEnabled}">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock>Plugin enabled</TextBlock>
|
||||
<materialDesign:PackIcon Kind="ShieldHalfFull"
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
@ -6,24 +7,26 @@ using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Ninject.Factories;
|
||||
using Artemis.UI.Screens.Plugins;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Ninject;
|
||||
using Stylet;
|
||||
using Constants = Artemis.Core.Constants;
|
||||
|
||||
namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
{
|
||||
public class PluginSettingsViewModel : Conductor<PluginFeatureViewModel>.Collection.AllActive
|
||||
{
|
||||
private readonly ICoreService _coreService;
|
||||
private readonly IDialogService _dialogService;
|
||||
private readonly IMessageService _messageService;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private readonly ISettingsVmFactory _settingsVmFactory;
|
||||
private readonly ICoreService _coreService;
|
||||
private readonly IMessageService _messageService;
|
||||
private readonly IWindowManager _windowManager;
|
||||
private bool _canInstallPrerequisites;
|
||||
private bool _canRemovePrerequisites;
|
||||
private bool _enabling;
|
||||
private bool _isSettingsPopupOpen;
|
||||
private Plugin _plugin;
|
||||
|
||||
public PluginSettingsViewModel(Plugin plugin,
|
||||
@ -68,6 +71,28 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
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;
|
||||
@ -86,6 +111,57 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
}
|
||||
}
|
||||
|
||||
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<IPrerequisitesSubject> 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<IPrerequisitesSubject> 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?");
|
||||
@ -107,10 +183,16 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
|
||||
public async Task Remove()
|
||||
{
|
||||
bool confirmed = await _dialogService.ShowConfirmDialog("Delete plugin", "Are you sure you want to delete this plugin?");
|
||||
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<IPrerequisitesSubject> 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);
|
||||
@ -137,12 +219,10 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInitialActivate()
|
||||
private void PluginManagementServiceOnPluginToggled(object? sender, PluginEventArgs e)
|
||||
{
|
||||
foreach (PluginFeatureInfo pluginFeatureInfo in Plugin.Features)
|
||||
Items.Add(_settingsVmFactory.CreatePluginFeatureViewModel(pluginFeatureInfo, false));
|
||||
|
||||
base.OnInitialActivate();
|
||||
NotifyOfPropertyChange(nameof(IsEnabled));
|
||||
NotifyOfPropertyChange(nameof(CanOpenSettings));
|
||||
}
|
||||
|
||||
private async Task UpdateEnabled(bool enable)
|
||||
@ -162,33 +242,82 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins
|
||||
bool confirmed = await _dialogService.ShowConfirmDialog("Enable plugin", "This plugin requires admin rights, are you sure you want to enable it?");
|
||||
if (!confirmed)
|
||||
{
|
||||
Enabling = false;
|
||||
NotifyOfPropertyChange(nameof(IsEnabled));
|
||||
NotifyOfPropertyChange(nameof(CanOpenSettings));
|
||||
CancelEnable();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
// 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 Task.Run(() => _pluginManagementService.EnablePlugin(Plugin, true, true));
|
||||
await PluginPrerequisitesInstallDialogViewModel.Show(_dialogService, subjects);
|
||||
if (!subjects.All(s => s.ArePrerequisitesMet()))
|
||||
{
|
||||
CancelEnable();
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
_messageService.ShowMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Enabling = false;
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
@ -18,7 +18,7 @@
|
||||
d:DesignHeight="800"
|
||||
d:DesignWidth="800"
|
||||
d:DataContext="{d:DesignInstance local:PluginSettingsWindowViewModel}"
|
||||
Icon="/Resources/Images/Logo/logo-512.png">
|
||||
Icon="/Resources/Images/Logo/bow.ico">
|
||||
<materialDesign:DialogHost IsTabStop="False"
|
||||
Focusable="False"
|
||||
Identifier="PluginSettingsDialog"
|
||||
|
||||
@ -52,6 +52,7 @@
|
||||
Items="{Binding SidebarItems}"
|
||||
SelectedItem="{Binding SelectedItem}"
|
||||
WillSelectNavigationItemCommand="{s:Action SelectItem}"
|
||||
materialDesign:RippleAssist.Feedback="{StaticResource SecondaryHueMidBrush}"
|
||||
Margin="0 5 0 0" />
|
||||
</Grid>
|
||||
|
||||
|
||||
@ -6,28 +6,29 @@
|
||||
xmlns:splash="clr-namespace:Artemis.UI.Screens.Splash"
|
||||
xmlns:controls="clr-namespace:MaterialDesignExtensions.Controls;assembly=MaterialDesignExtensions"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
|
||||
mc:Ignorable="d"
|
||||
Title=" "
|
||||
ResizeMode="NoResize"
|
||||
BorderBackgroundBrush="{DynamicResource PrimaryHueMidBrush}"
|
||||
BorderBackgroundBrush="{DynamicResource MaterialDesignPaper}"
|
||||
Height="450"
|
||||
Width="400"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto"
|
||||
MouseDown="{s:Action MouseDown}"
|
||||
d:DataContext="{d:DesignInstance splash:SplashViewModel}">
|
||||
<Grid Background="{DynamicResource PrimaryHueMidBrush}">
|
||||
<Grid Background="{DynamicResource MaterialDesignPaper}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="250" />
|
||||
<RowDefinition Height="50" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Image Source="{StaticResource BowIcon}" Stretch="Uniform" Margin="50" />
|
||||
<TextBlock Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Bottom" Foreground="White" FontSize="16">
|
||||
<Image Source="{svgc:SvgImage Source=/Resources/Images/Logo/bow.svg}" Stretch="Uniform" Margin="50"/>
|
||||
<TextBlock Grid.Row="1" HorizontalAlignment="Center" VerticalAlignment="Bottom" Foreground="{DynamicResource MaterialDesignBody}" FontSize="16">
|
||||
Artemis is initializing...
|
||||
</TextBlock>
|
||||
<TextBlock Grid.Row="2" HorizontalAlignment="Center" Foreground="#FFDDDDDD" Text="{Binding Status}" />
|
||||
<ProgressBar Grid.Row="3" IsIndeterminate="True" Maximum="1" Minimum="1" Margin="16 0" />
|
||||
<TextBlock Grid.Row="2" HorizontalAlignment="Center" Foreground="{DynamicResource MaterialDesignBody}" Text="{Binding Status}" />
|
||||
<ProgressBar Grid.Row="3" IsIndeterminate="True" Maximum="1" Minimum="1" Margin="16 0" />
|
||||
</Grid>
|
||||
</controls:MaterialWindow>
|
||||
@ -6,13 +6,14 @@
|
||||
xmlns:mde="https://spiegelp.github.io/MaterialDesignExtensions/winfx/xaml"
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
xmlns:svgc="http://sharpvectors.codeplex.com/svgc/"
|
||||
mc:Ignorable="d"
|
||||
Width="800"
|
||||
Height="600"
|
||||
ResizeMode="NoResize"
|
||||
Icon="/Resources/Images/Logo/logo-512.png"
|
||||
Icon="/Resources/Images/Logo/bow.ico"
|
||||
Title="Artemis startup wizard"
|
||||
TitleBarIcon="{StaticResource BowIcon}"
|
||||
TitleBarIcon="{svgc:SvgImage Source=/Resources/Images/Logo/bow-white.svg}"
|
||||
Foreground="{DynamicResource MaterialDesignBody}"
|
||||
Background="{DynamicResource MaterialDesignPaper}"
|
||||
FontFamily="pack://application:,,,/MaterialDesignThemes.Wpf;component/Resources/Roboto/#Roboto"
|
||||
|
||||
@ -23,10 +23,11 @@
|
||||
<TextBlock Text="{Binding Device.RgbDevice.DeviceInfo.Model}" />
|
||||
<TextBlock Text="{Binding Device.RgbDevice.DeviceInfo.Manufacturer}" Foreground="{DynamicResource MaterialDesignBodyLight}" />
|
||||
</StackPanel>
|
||||
<Ellipse Grid.Column="2"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Stroke="{DynamicResource MaterialDesignBody}"
|
||||
<Ellipse Grid.Column="2"
|
||||
ToolTip="This is the current random device color"
|
||||
Width="20"
|
||||
Height="20"
|
||||
Stroke="{DynamicResource MaterialDesignDivider}"
|
||||
StrokeThickness="1"
|
||||
Visibility="{Binding Parent.ColorDevices, Converter={x:Static s:BoolToVisibilityConverter.Instance}}">
|
||||
<Ellipse.Fill>
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
xmlns:s="https://github.com/canton7/Stylet"
|
||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||
mc:Ignorable="d">
|
||||
<tb:TaskbarIcon IconSource="/Resources/Images/Logo/logo-512.ico"
|
||||
<tb:TaskbarIcon IconSource="/Resources/Images/Logo/bow-white.ico"
|
||||
MenuActivation="LeftOrRightClick"
|
||||
PopupActivation="DoubleClick"
|
||||
DoubleClickCommand="{s:Action TrayBringToForeground}"
|
||||
|
||||
@ -5,11 +5,14 @@ using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Events;
|
||||
using Artemis.UI.Screens.Settings.Tabs.General;
|
||||
using Artemis.UI.Screens.Splash;
|
||||
using Artemis.UI.Services;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Utilities;
|
||||
using Hardcodet.Wpf.TaskbarNotification;
|
||||
using MaterialDesignThemes.Wpf;
|
||||
using Ninject;
|
||||
@ -24,10 +27,12 @@ namespace Artemis.UI.Screens
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly IKernel _kernel;
|
||||
private readonly IWindowManager _windowManager;
|
||||
private readonly ThemeWatcher _themeWatcher;
|
||||
private readonly PluginSetting<ApplicationColorScheme> _colorScheme;
|
||||
private RootViewModel _rootViewModel;
|
||||
private SplashViewModel _splashViewModel;
|
||||
private TaskbarIcon _taskBarIcon;
|
||||
|
||||
|
||||
public TrayViewModel(IKernel kernel,
|
||||
IWindowManager windowManager,
|
||||
IWindowService windowService,
|
||||
@ -46,6 +51,13 @@ namespace Artemis.UI.Screens
|
||||
Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested;
|
||||
Core.Utilities.RestartRequested += UtilitiesOnShutdownRequested;
|
||||
|
||||
_themeWatcher = new ThemeWatcher();
|
||||
_colorScheme = settingsService.GetSetting("UI.ColorScheme", ApplicationColorScheme.Automatic);
|
||||
_colorScheme.SettingChanged += ColorSchemeOnSettingChanged;
|
||||
_themeWatcher.ThemeChanged += ThemeWatcherOnThemeChanged;
|
||||
|
||||
ApplyColorSchemeSetting();
|
||||
|
||||
windowService.ConfigureMainWindowProvider(this);
|
||||
messageService.ConfigureNotificationProvider(this);
|
||||
bool autoRunning = Bootstrapper.StartupArguments.Contains("--autorun");
|
||||
@ -151,6 +163,50 @@ namespace Artemis.UI.Screens
|
||||
OnMainWindowClosed();
|
||||
}
|
||||
|
||||
#region Theme
|
||||
|
||||
private void ApplyColorSchemeSetting()
|
||||
{
|
||||
if (_colorScheme.Value == ApplicationColorScheme.Automatic)
|
||||
ApplyWindowsTheme(_themeWatcher.GetWindowsTheme());
|
||||
else
|
||||
ChangeMaterialColors(_colorScheme.Value);
|
||||
}
|
||||
|
||||
private void ApplyWindowsTheme(ThemeWatcher.WindowsTheme windowsTheme)
|
||||
{
|
||||
if (_colorScheme.Value != ApplicationColorScheme.Automatic)
|
||||
return;
|
||||
|
||||
if (windowsTheme == ThemeWatcher.WindowsTheme.Dark)
|
||||
ChangeMaterialColors(ApplicationColorScheme.Dark);
|
||||
else
|
||||
ChangeMaterialColors(ApplicationColorScheme.Light);
|
||||
}
|
||||
|
||||
private void ChangeMaterialColors(ApplicationColorScheme colorScheme)
|
||||
{
|
||||
PaletteHelper paletteHelper = new();
|
||||
ITheme theme = paletteHelper.GetTheme();
|
||||
theme.SetBaseTheme(colorScheme == ApplicationColorScheme.Dark ? Theme.Dark : Theme.Light);
|
||||
paletteHelper.SetTheme(theme);
|
||||
|
||||
MaterialDesignExtensions.Themes.PaletteHelper extensionsPaletteHelper = new();
|
||||
extensionsPaletteHelper.SetLightDark(colorScheme == ApplicationColorScheme.Dark);
|
||||
}
|
||||
|
||||
private void ThemeWatcherOnThemeChanged(object sender, WindowsThemeEventArgs e)
|
||||
{
|
||||
ApplyWindowsTheme(e.Theme);
|
||||
}
|
||||
|
||||
private void ColorSchemeOnSettingChanged(object sender, EventArgs e)
|
||||
{
|
||||
ApplyColorSchemeSetting();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Implementation of INotificationProvider
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@ -2,33 +2,11 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Artemis.UI.Utilities
|
||||
{
|
||||
public static class ProcessUtilities
|
||||
{
|
||||
public static Task<int> RunProcessAsync(string fileName, string arguments)
|
||||
{
|
||||
TaskCompletionSource<int> tcs = new();
|
||||
|
||||
Process process = new()
|
||||
{
|
||||
StartInfo = {FileName = fileName, Arguments = arguments},
|
||||
EnableRaisingEvents = true
|
||||
};
|
||||
|
||||
process.Exited += (sender, args) =>
|
||||
{
|
||||
tcs.SetResult(process.ExitCode);
|
||||
process.Dispose();
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
return tcs.Task;
|
||||
}
|
||||
|
||||
public static Process RunAsDesktopUser(string fileName, string arguments, bool hideWindow)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(fileName))
|
||||
@ -198,24 +176,24 @@ namespace Artemis.UI.Utilities
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct STARTUPINFO
|
||||
{
|
||||
public int cb;
|
||||
public string lpReserved;
|
||||
public string lpDesktop;
|
||||
public string lpTitle;
|
||||
public int dwX;
|
||||
public int dwY;
|
||||
public int dwXSize;
|
||||
public int dwYSize;
|
||||
public int dwXCountChars;
|
||||
public int dwYCountChars;
|
||||
public int dwFillAttribute;
|
||||
public readonly int cb;
|
||||
public readonly string lpReserved;
|
||||
public readonly string lpDesktop;
|
||||
public readonly string lpTitle;
|
||||
public readonly int dwX;
|
||||
public readonly int dwY;
|
||||
public readonly int dwXSize;
|
||||
public readonly int dwYSize;
|
||||
public readonly int dwXCountChars;
|
||||
public readonly int dwYCountChars;
|
||||
public readonly int dwFillAttribute;
|
||||
public int dwFlags;
|
||||
public short wShowWindow;
|
||||
public short cbReserved2;
|
||||
public IntPtr lpReserved2;
|
||||
public IntPtr hStdInput;
|
||||
public IntPtr hStdOutput;
|
||||
public IntPtr hStdError;
|
||||
public readonly short cbReserved2;
|
||||
public readonly IntPtr lpReserved2;
|
||||
public readonly IntPtr hStdInput;
|
||||
public readonly IntPtr hStdOutput;
|
||||
public readonly IntPtr hStdError;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", ExactSpelling = true)]
|
||||
|
||||