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

Merge branch 'development'

This commit is contained in:
Robert 2021-05-15 22:43:00 +02:00
commit 195401fa8f
31 changed files with 996 additions and 699 deletions

View File

@ -48,7 +48,7 @@ namespace Artemis.Core
public void Apply(Layer layer, List<ArtemisDevice> devices) public void Apply(Layer layer, List<ArtemisDevice> devices)
{ {
IEnumerable<ArtemisDevice> matches = devices IEnumerable<ArtemisDevice> matches = devices
.Where(d => d.RgbDevice.DeviceInfo.DeviceType == DeviceType) .Where(d => DeviceType == RGBDeviceType.All || d.RgbDevice.DeviceInfo.DeviceType == DeviceType)
.OrderBy(d => d.Rectangle.Top) .OrderBy(d => d.Rectangle.Top)
.ThenBy(d => d.Rectangle.Left) .ThenBy(d => d.Rectangle.Left)
.Skip(Skip); .Skip(Skip);

View File

@ -66,8 +66,8 @@ namespace Artemis.Core
_leds = new List<ArtemisLed>(); _leds = new List<ArtemisLed>();
Load();
Adapter = new LayerAdapter(this); Adapter = new LayerAdapter(this);
Load();
Initialize(); Initialize();
} }
@ -242,6 +242,7 @@ namespace Artemis.Core
ExpandedPropertyGroups.AddRange(LayerEntity.ExpandedPropertyGroups); ExpandedPropertyGroups.AddRange(LayerEntity.ExpandedPropertyGroups);
LoadRenderElement(); LoadRenderElement();
Adapter.Load();
} }
internal override void Save() internal override void Save()
@ -276,6 +277,9 @@ namespace Artemis.Core
LayerEntity.Leds.Add(ledEntity); LayerEntity.Leds.Add(ledEntity);
} }
// Adaption hints
Adapter.Save();
SaveRenderElement(); SaveRenderElement();
} }

View File

@ -70,6 +70,14 @@ namespace Artemis.Core
public List<IAdaptionHint> DetermineHints(IEnumerable<ArtemisDevice> devices) public List<IAdaptionHint> DetermineHints(IEnumerable<ArtemisDevice> devices)
{ {
List<IAdaptionHint> newHints = new(); List<IAdaptionHint> newHints = new();
if (devices.All(DoesLayerCoverDevice))
{
DeviceAdaptionHint hint = new() {DeviceType = RGBDeviceType.All};
AdaptionHints.Add(hint);
newHints.Add(hint);
}
else
{
// Any fully covered device will add a device adaption hint for that type // Any fully covered device will add a device adaption hint for that type
foreach (IGrouping<ArtemisDevice, ArtemisLed> deviceLeds in Layer.Leds.GroupBy(l => l.Device)) foreach (IGrouping<ArtemisDevice, ArtemisLed> deviceLeds in Layer.Leds.GroupBy(l => l.Device))
{ {
@ -99,6 +107,7 @@ namespace Artemis.Core
newHints.Add(hint); newHints.Add(hint);
} }
} }
}
return newHints; return newHints;
} }

View File

@ -14,6 +14,7 @@ namespace Artemis.Core
{ {
private readonly object _lock = new(); private readonly object _lock = new();
private bool _isActivated; private bool _isActivated;
private bool _isFreshImport;
internal Profile(ProfileModule module, string name) : base(null!) internal Profile(ProfileModule module, string name) : base(null!)
{ {
@ -57,6 +58,20 @@ namespace Artemis.Core
private set => SetAndNotify(ref _isActivated, value); private set => SetAndNotify(ref _isActivated, value);
} }
/// <summary>
/// Gets or sets a boolean indicating whether this profile is freshly imported i.e. no changes have been made to it
/// since import
/// <para>
/// Note: As long as this is <see langword="true" />, profile adaption will be performed on load and any surface
/// changes
/// </para>
/// </summary>
public bool IsFreshImport
{
get => _isFreshImport;
set => SetAndNotify(ref _isFreshImport, value);
}
/// <summary> /// <summary>
/// Gets the profile entity this profile uses for persistent storage /// Gets the profile entity this profile uses for persistent storage
/// </summary> /// </summary>
@ -134,6 +149,16 @@ namespace Artemis.Core
layer.PopulateLeds(devices); layer.PopulateLeds(devices);
} }
/// <summary>
/// Occurs when the profile has been activated.
/// </summary>
public event EventHandler? Activated;
/// <summary>
/// Occurs when the profile is being deactivated.
/// </summary>
public event EventHandler? Deactivated;
/// <inheritdoc /> /// <inheritdoc />
protected override void Dispose(bool disposing) protected override void Dispose(bool disposing)
{ {
@ -156,6 +181,7 @@ namespace Artemis.Core
throw new ObjectDisposedException("Profile"); throw new ObjectDisposedException("Profile");
Name = ProfileEntity.Name; Name = ProfileEntity.Name;
IsFreshImport = ProfileEntity.IsFreshImport;
lock (ChildrenList) lock (ChildrenList)
{ {
@ -171,11 +197,9 @@ namespace Artemis.Core
Folder _ = new(this, "Root folder"); Folder _ = new(this, "Root folder");
} }
else else
{
AddChild(new Folder(this, this, rootFolder)); AddChild(new Folder(this, this, rootFolder));
} }
} }
}
internal override void Save() internal override void Save()
{ {
@ -186,6 +210,7 @@ namespace Artemis.Core
ProfileEntity.ModuleId = Module.Id; ProfileEntity.ModuleId = Module.Id;
ProfileEntity.Name = Name; ProfileEntity.Name = Name;
ProfileEntity.IsActive = IsActivated; ProfileEntity.IsActive = IsActivated;
ProfileEntity.IsFreshImport = IsFreshImport;
foreach (ProfileElement profileElement in Children) foreach (ProfileElement profileElement in Children)
profileElement.Save(); profileElement.Save();
@ -212,18 +237,6 @@ namespace Artemis.Core
} }
} }
#region Events
/// <summary>
/// Occurs when the profile has been activated.
/// </summary>
public event EventHandler? Activated;
/// <summary>
/// Occurs when the profile is being deactivated.
/// </summary>
public event EventHandler? Deactivated;
private void OnActivated() private void OnActivated()
{ {
Activated?.Invoke(this, EventArgs.Empty); Activated?.Invoke(this, EventArgs.Empty);
@ -233,7 +246,5 @@ namespace Artemis.Core
{ {
Deactivated?.Invoke(this, EventArgs.Empty); Deactivated?.Invoke(this, EventArgs.Empty);
} }
#endregion
} }
} }

View File

@ -37,6 +37,5 @@ namespace Artemis.Core
/// Gets a boolean indicating whether this was the last active profile /// Gets a boolean indicating whether this was the last active profile
/// </summary> /// </summary>
public bool IsLastActiveProfile { get; } public bool IsLastActiveProfile { get; }
} }
} }

View File

@ -381,7 +381,8 @@ namespace Artemis.Core
"set to true because the device provider does not support it"); "set to true because the device provider does not support it");
if (layout.IsValid) if (layout.IsValid)
layout.RgbLayout!.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds); layout.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds);
UpdateLeds(); UpdateLeds();

View File

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using RGB.NET.Core;
using RGB.NET.Layout; using RGB.NET.Layout;
namespace Artemis.Core namespace Artemis.Core
@ -65,6 +66,44 @@ namespace Artemis.Core
/// </summary> /// </summary>
public LayoutCustomDeviceData LayoutCustomDeviceData { get; private set; } = null!; public LayoutCustomDeviceData LayoutCustomDeviceData { get; private set; } = null!;
/// <summary>
/// Applies the layout to the provided device
/// </summary>
public void ApplyTo(IRGBDevice device, bool createMissingLeds = false, bool removeExcessiveLeds = false)
{
device.Size = new Size(MathF.Round(RgbLayout.Width), MathF.Round(RgbLayout.Height));
device.DeviceInfo.LayoutMetadata = RgbLayout.CustomData;
HashSet<LedId> ledIds = new();
foreach (ILedLayout layoutLed in RgbLayout.Leds)
{
if (Enum.TryParse(layoutLed.Id, true, out LedId ledId))
{
ledIds.Add(ledId);
Led? led = device[ledId];
if (led == null && createMissingLeds)
led = device.AddLed(ledId, new Point(), new Size());
if (led != null)
{
led.Location = new Point(MathF.Round(layoutLed.X), MathF.Round(layoutLed.Y));
led.Size = new Size(MathF.Round(layoutLed.Width), MathF.Round(layoutLed.Height));
led.Shape = layoutLed.Shape;
led.ShapeData = layoutLed.ShapeData;
led.LayoutMetadata = layoutLed.CustomData;
}
}
}
if (removeExcessiveLeds)
{
List<LedId> ledsToRemove = device.Select(led => led.Id).Where(id => !ledIds.Contains(id)).ToList();
foreach (LedId led in ledsToRemove)
device.RemoveLed(led);
}
}
internal void ApplyDevice(ArtemisDevice artemisDevice) internal void ApplyDevice(ArtemisDevice artemisDevice)
{ {
Device = artemisDevice; Device = artemisDevice;

View File

@ -1,11 +1,15 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Linq.Expressions; using System.Linq.Expressions;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core.DataModelExpansions; using Artemis.Core.DataModelExpansions;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Profile;
using Newtonsoft.Json;
using SkiaSharp; using SkiaSharp;
namespace Artemis.Core.Modules namespace Artemis.Core.Modules
@ -91,11 +95,16 @@ namespace Artemis.Core.Modules
/// </summary> /// </summary>
public abstract class ProfileModule : Module public abstract class ProfileModule : Module
{ {
private readonly List<string> _defaultProfilePaths = new();
private readonly List<string> _pendingDefaultProfilePaths = new();
private readonly List<ProfileEntity> _defaultProfiles = new();
private readonly object _lock = new();
/// <summary> /// <summary>
/// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c> /// Gets a list of all properties ignored at runtime using <c>IgnoreProperty(x => x.y)</c>
/// </summary> /// </summary>
protected internal readonly List<PropertyInfo> HiddenPropertiesList = new(); protected internal readonly List<PropertyInfo> HiddenPropertiesList = new();
private readonly object _lock = new();
/// <summary> /// <summary>
/// Creates a new instance of the <see cref="ProfileModule" /> class /// Creates a new instance of the <see cref="ProfileModule" /> class
@ -130,6 +139,11 @@ namespace Artemis.Core.Modules
/// </summary> /// </summary>
public bool AnimatingProfileChange { get; private set; } public bool AnimatingProfileChange { get; private set; }
/// <summary>
/// Gets a list of default profiles, to add a new default profile use <see cref="AddDefaultProfile" />
/// </summary>
internal ReadOnlyCollection<ProfileEntity> DefaultProfiles => _defaultProfiles.AsReadOnly();
/// <summary> /// <summary>
/// Called after the profile has updated /// Called after the profile has updated
/// </summary> /// </summary>
@ -148,6 +162,71 @@ namespace Artemis.Core.Modules
{ {
} }
/// <summary>
/// Occurs when the <see cref="ActiveProfile" /> has changed
/// </summary>
public event EventHandler? ActiveProfileChanged;
/// <summary>
/// Adds a default profile by reading it from the file found at the provided path
/// </summary>
/// <param name="file">A path pointing towards a profile file. May be relative to the plugin directory.</param>
/// <returns>
/// <see langword="true" /> if the default profile was added; <see langword="false" /> if it was not because it is
/// already in the list.
/// </returns>
protected bool AddDefaultProfile(string file)
{
// It can be null if the plugin has not loaded yet...
if (Plugin == null!)
{
if (_pendingDefaultProfilePaths.Contains(file))
return false;
_pendingDefaultProfilePaths.Add(file);
return true;
}
if (!Path.IsPathRooted(file))
file = Plugin.ResolveRelativePath(file);
if (_defaultProfilePaths.Contains(file))
return false;
_defaultProfilePaths.Add(file);
// Ensure the file exists
if (!File.Exists(file))
throw new ArtemisPluginFeatureException(this, $"Could not find default profile at {file}.");
// Deserialize and make sure that succeeded
ProfileEntity? profileEntity = JsonConvert.DeserializeObject<ProfileEntity>(File.ReadAllText(file), ProfileService.ExportSettings);
if (profileEntity == null)
throw new ArtemisPluginFeatureException(this, $"Failed to deserialize default profile at {file}.");
// Ensure the profile ID is unique
if (_defaultProfiles.Any(d => d.Id == profileEntity.Id))
throw new ArtemisPluginFeatureException(this, $"Cannot add default profile from {file}, profile ID {profileEntity.Id} already in use.");
profileEntity.IsFreshImport = true;
_defaultProfiles.Add(profileEntity);
return true;
}
/// <summary>
/// Invokes the <see cref="ActiveProfileChanged" /> event
/// </summary>
protected virtual void OnActiveProfileChanged()
{
ActiveProfileChanged?.Invoke(this, EventArgs.Empty);
}
internal override void InternalEnable()
{
foreach (string pendingDefaultProfile in _pendingDefaultProfilePaths)
AddDefaultProfile(pendingDefaultProfile);
_pendingDefaultProfilePaths.Clear();
base.InternalEnable();
}
internal override void InternalUpdate(double deltaTime) internal override void InternalUpdate(double deltaTime)
{ {
StartUpdateMeasure(); StartUpdateMeasure();
@ -245,22 +324,5 @@ namespace Artemis.Core.Modules
base.Deactivate(isDeactivateOverride); base.Deactivate(isDeactivateOverride);
Activate(isActivateOverride); Activate(isActivateOverride);
} }
#region Events
/// <summary>
/// Occurs when the <see cref="ActiveProfile" /> has changed
/// </summary>
public event EventHandler? ActiveProfileChanged;
/// <summary>
/// Invokes the <see cref="ActiveProfileChanged" /> event
/// </summary>
protected virtual void OnActiveProfileChanged()
{
ActiveProfileChanged?.Invoke(this, EventArgs.Empty);
}
#endregion
} }
} }

View File

@ -23,7 +23,7 @@ namespace Artemis.Core
public abstract string Description { get; } public abstract string Description { get; }
/// <summary> /// <summary>
/// Gets a boolean indicating whether installing or uninstalling this prerequisite requires admin privileges /// [NYI] Gets a boolean indicating whether installing or uninstalling this prerequisite requires admin privileges
/// </summary> /// </summary>
public abstract bool RequiresElevation { get; } public abstract bool RequiresElevation { get; }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -17,11 +18,13 @@ namespace Artemis.Core
/// <param name="fileName">The target file to execute</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="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> /// <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) /// <param name="elevate">A boolean indicating whether the file should run with administrator privileges (does not require <see cref="PluginPrerequisite.RequiresElevation"/>)</param>
public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true, bool elevate = false) : base(name)
{ {
FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); FileName = fileName ?? throw new ArgumentNullException(nameof(fileName));
Arguments = arguments; Arguments = arguments;
WaitForExit = waitForExit; WaitForExit = waitForExit;
Elevate = elevate;
} }
/// <summary> /// <summary>
@ -39,6 +42,11 @@ namespace Artemis.Core
/// </summary> /// </summary>
public bool WaitForExit { get; } public bool WaitForExit { get; }
/// <summary>
/// Gets a boolean indicating whether the file should run with administrator privileges
/// </summary>
public bool Elevate { get; }
/// <inheritdoc /> /// <inheritdoc />
public override async Task Execute(CancellationToken cancellationToken) public override async Task Execute(CancellationToken cancellationToken)
{ {
@ -48,7 +56,7 @@ namespace Artemis.Core
ShowProgressBar = true; ShowProgressBar = true;
ProgressIndeterminate = true; ProgressIndeterminate = true;
int result = await RunProcessAsync(FileName, Arguments); int result = await RunProcessAsync(FileName, Arguments, Elevate);
Status = $"{FileName} exited with code {result}"; Status = $"{FileName} exited with code {result}";
} }
@ -64,13 +72,19 @@ namespace Artemis.Core
} }
} }
private static Task<int> RunProcessAsync(string fileName, string? arguments) private static Task<int> RunProcessAsync(string fileName, string? arguments, bool elevate)
{ {
TaskCompletionSource<int> tcs = new(); TaskCompletionSource<int> tcs = new();
Process process = new() Process process = new()
{ {
StartInfo = {FileName = fileName, Arguments = arguments!}, StartInfo =
{
FileName = fileName,
Arguments = arguments!,
Verb = elevate ? "RunAs" : "",
UseShellExecute = elevate
},
EnableRaisingEvents = true EnableRaisingEvents = true
}; };
@ -80,7 +94,17 @@ namespace Artemis.Core
process.Dispose(); process.Dispose();
}; };
try
{
process.Start(); process.Start();
}
catch (Win32Exception e)
{
if (!elevate || e.NativeErrorCode != 0x4c7)
throw;
tcs.SetResult(-1);
}
return tcs.Task; return tcs.Task;
} }

View File

@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers; using System.Timers;
using Artemis.Core.Modules; using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
using Artemis.Storage.Repositories.Interfaces; using Artemis.Storage.Repositories.Interfaces;
using Serilog; using Serilog;
using Timer = System.Timers.Timer; using Timer = System.Timers.Timer;
@ -17,13 +18,15 @@ namespace Artemis.Core.Services
private static readonly SemaphoreSlim ActiveModuleSemaphore = new(1, 1); private static readonly SemaphoreSlim ActiveModuleSemaphore = new(1, 1);
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IModuleRepository _moduleRepository; private readonly IModuleRepository _moduleRepository;
private readonly IProfileRepository _profileRepository;
private readonly IPluginManagementService _pluginManagementService; private readonly IPluginManagementService _pluginManagementService;
private readonly IProfileService _profileService; private readonly IProfileService _profileService;
public ModuleService(ILogger logger, IModuleRepository moduleRepository, IPluginManagementService pluginManagementService, IProfileService profileService) public ModuleService(ILogger logger, IModuleRepository moduleRepository, IProfileRepository profileRepository, IPluginManagementService pluginManagementService, IProfileService profileService)
{ {
_logger = logger; _logger = logger;
_moduleRepository = moduleRepository; _moduleRepository = moduleRepository;
_profileRepository = profileRepository;
_pluginManagementService = pluginManagementService; _pluginManagementService = pluginManagementService;
_profileService = profileService; _profileService = profileService;
_pluginManagementService.PluginFeatureEnabled += OnPluginFeatureEnabled; _pluginManagementService.PluginFeatureEnabled += OnPluginFeatureEnabled;
@ -45,12 +48,24 @@ namespace Artemis.Core.Services
{ {
try try
{ {
ProfileModule? profileModule = module as ProfileModule;
if (profileModule != null && profileModule.DefaultProfiles.Any())
{
List<ProfileDescriptor> descriptors = _profileService.GetProfileDescriptors(profileModule);
foreach (ProfileEntity defaultProfile in profileModule.DefaultProfiles)
{
if (descriptors.All(d => d.Id != defaultProfile.Id))
_profileRepository.Add(defaultProfile);
}
}
module.Activate(false); module.Activate(false);
try try
{ {
// If this is a profile module, activate the last active profile after module activation // If this is a profile module, activate the last active profile after module activation
if (module is ProfileModule profileModule) if (profileModule != null)
await _profileService.ActivateLastProfileAnimated(profileModule); await _profileService.ActivateLastProfileAnimated(profileModule);
} }
catch (Exception e) catch (Exception e)

View File

@ -32,8 +32,8 @@ namespace Artemis.Core.Services
_rgbService.LedsChanged += RgbServiceOnLedsChanged; _rgbService.LedsChanged += RgbServiceOnLedsChanged;
} }
public JsonSerializerSettings MementoSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All}; public static JsonSerializerSettings MementoSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All};
public JsonSerializerSettings ExportSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.Indented}; public static JsonSerializerSettings ExportSettings { get; set; } = new() {TypeNameHandling = TypeNameHandling.All, Formatting = Formatting.Indented};
public ProfileDescriptor? GetLastActiveProfile(ProfileModule module) public ProfileDescriptor? GetLastActiveProfile(ProfileModule module)
{ {
@ -64,8 +64,16 @@ namespace Artemis.Core.Services
private void ActiveProfilesPopulateLeds() private void ActiveProfilesPopulateLeds()
{ {
List<ProfileModule> profileModules = _pluginManagementService.GetFeaturesOfType<ProfileModule>(); List<ProfileModule> profileModules = _pluginManagementService.GetFeaturesOfType<ProfileModule>();
foreach (ProfileModule profileModule in profileModules.Where(p => p.ActiveProfile != null).ToList()) foreach (ProfileModule profileModule in profileModules)
profileModule.ActiveProfile?.PopulateLeds(_rgbService.EnabledDevices); // Avoid race condition {
// Avoid race condition, make the check here
if (profileModule.ActiveProfile != null)
{
profileModule.ActiveProfile.PopulateLeds(_rgbService.EnabledDevices);
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profileModule.ActiveProfile);
AdaptProfile(profileModule.ActiveProfile);
}
}
} }
public List<ProfileDescriptor> GetProfileDescriptors(ProfileModule module) public List<ProfileDescriptor> GetProfileDescriptors(ProfileModule module)
@ -109,6 +117,12 @@ namespace Artemis.Core.Services
InstantiateProfile(profile); InstantiateProfile(profile);
profileDescriptor.ProfileModule.ChangeActiveProfile(profile, _rgbService.EnabledDevices); profileDescriptor.ProfileModule.ChangeActiveProfile(profile, _rgbService.EnabledDevices);
if (profile.IsFreshImport)
{
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile);
AdaptProfile(profile);
}
SaveActiveProfile(profileDescriptor.ProfileModule); SaveActiveProfile(profileDescriptor.ProfileModule);
return profile; return profile;
@ -156,6 +170,12 @@ namespace Artemis.Core.Services
_rgbService.LedsChanged += ActivatingRgbServiceOnLedsChanged; _rgbService.LedsChanged += ActivatingRgbServiceOnLedsChanged;
await profileDescriptor.ProfileModule.ChangeActiveProfileAnimated(profile, _rgbService.EnabledDevices); await profileDescriptor.ProfileModule.ChangeActiveProfileAnimated(profile, _rgbService.EnabledDevices);
if (profile.IsFreshImport)
{
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile);
AdaptProfile(profile);
}
SaveActiveProfile(profileDescriptor.ProfileModule); SaveActiveProfile(profileDescriptor.ProfileModule);
_pluginManagementService.PluginEnabled -= ActivatingProfilePluginToggle; _pluginManagementService.PluginEnabled -= ActivatingProfilePluginToggle;
@ -193,6 +213,8 @@ namespace Artemis.Core.Services
public void DeleteProfile(ProfileDescriptor profileDescriptor) public void DeleteProfile(ProfileDescriptor profileDescriptor)
{ {
ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id); ProfileEntity profileEntity = _profileRepository.Get(profileDescriptor.Id);
if (profileEntity == null)
return;
_profileRepository.Remove(profileEntity); _profileRepository.Remove(profileEntity);
} }
@ -203,6 +225,7 @@ namespace Artemis.Core.Services
profile.RedoStack.Clear(); profile.RedoStack.Clear();
profile.UndoStack.Push(memento); profile.UndoStack.Push(memento);
profile.IsFreshImport = false;
profile.Save(); profile.Save();
if (includeChildren) if (includeChildren)
{ {
@ -289,6 +312,7 @@ namespace Artemis.Core.Services
profileEntity.UpdateGuid(Guid.NewGuid()); profileEntity.UpdateGuid(Guid.NewGuid());
profileEntity.Name = $"{profileEntity.Name} - {nameAffix}"; profileEntity.Name = $"{profileEntity.Name} - {nameAffix}";
profileEntity.IsFreshImport = true;
_profileRepository.Add(profileEntity); _profileRepository.Add(profileEntity);
return new ProfileDescriptor(profileModule, profileEntity); return new ProfileDescriptor(profileModule, profileEntity);
} }

View File

@ -17,6 +17,7 @@ namespace Artemis.Storage.Entities.Profile
public string Name { get; set; } public string Name { get; set; }
public bool IsActive { get; set; } public bool IsActive { get; set; }
public bool IsFreshImport { get; set; }
public List<FolderEntity> Folders { get; set; } public List<FolderEntity> Folders { get; set; }
public List<LayerEntity> Layers { get; set; } public List<LayerEntity> Layers { get; set; }

View File

@ -17,7 +17,7 @@ namespace Artemis.UI.Shared.Services
/// Sets up the notification provider that shows desktop notifications /// Sets up the notification provider that shows desktop notifications
/// </summary> /// </summary>
/// <param name="notificationProvider">The notification provider that shows desktop notifications</param> /// <param name="notificationProvider">The notification provider that shows desktop notifications</param>
void ConfigureNotificationProvider(INotificationProvider notificationProvider); void SetNotificationProvider(INotificationProvider notificationProvider);
/// <summary> /// <summary>
/// Queues a notification message for display in a snackbar. /// Queues a notification message for display in a snackbar.
@ -123,7 +123,9 @@ namespace Artemis.UI.Shared.Services
/// </summary> /// </summary>
/// <param name="title">The title of the notification</param> /// <param name="title">The title of the notification</param>
/// <param name="message">The message of the notification</param> /// <param name="message">The message of the notification</param>
void ShowNotification(string title, string message); /// <param name="activatedCallback">An optional callback that is invoked when the notification is clicked</param>
/// <param name="dismissedCallback">An optional callback that is invoked when the notification is dismissed</param>
void ShowNotification(string title, string message, Action? activatedCallback = null, Action? dismissedCallback = null);
/// <summary> /// <summary>
/// Shows a desktop notification with a Material Design icon /// Shows a desktop notification with a Material Design icon
@ -131,7 +133,9 @@ namespace Artemis.UI.Shared.Services
/// <param name="title">The title of the notification</param> /// <param name="title">The title of the notification</param>
/// <param name="message">The message of the notification</param> /// <param name="message">The message of the notification</param>
/// <param name="icon">The name of the icon</param> /// <param name="icon">The name of the icon</param>
void ShowNotification(string title, string message, PackIconKind icon); /// <param name="activatedCallback">An optional callback that is invoked when the notification is clicked</param>
/// <param name="dismissedCallback">An optional callback that is invoked when the notification is dismissed</param>
void ShowNotification(string title, string message, PackIconKind icon, Action? activatedCallback = null, Action? dismissedCallback = null);
/// <summary> /// <summary>
/// Shows a desktop notification with a Material Design icon /// Shows a desktop notification with a Material Design icon
@ -139,6 +143,8 @@ namespace Artemis.UI.Shared.Services
/// <param name="title">The title of the notification</param> /// <param name="title">The title of the notification</param>
/// <param name="message">The message of the notification</param> /// <param name="message">The message of the notification</param>
/// <param name="icon">The name of the icon as a string</param> /// <param name="icon">The name of the icon as a string</param>
void ShowNotification(string title, string message, string icon); /// <param name="activatedCallback">An optional callback that is invoked when the notification is clicked</param>
/// <param name="dismissedCallback">An optional callback that is invoked when the notification is dismissed</param>
void ShowNotification(string title, string message, string icon, Action? activatedCallback = null, Action? dismissedCallback = null);
} }
} }

View File

@ -1,4 +1,5 @@
using MaterialDesignThemes.Wpf; using System;
using MaterialDesignThemes.Wpf;
namespace Artemis.UI.Shared.Services namespace Artemis.UI.Shared.Services
{ {
@ -6,7 +7,7 @@ namespace Artemis.UI.Shared.Services
/// Represents a class provides desktop notifications so that <see cref="IMessageService" /> can us it to show desktop /// Represents a class provides desktop notifications so that <see cref="IMessageService" /> can us it to show desktop
/// notifications /// notifications
/// </summary> /// </summary>
public interface INotificationProvider public interface INotificationProvider : IDisposable
{ {
/// <summary> /// <summary>
/// Shows a notification /// Shows a notification
@ -14,6 +15,8 @@ namespace Artemis.UI.Shared.Services
/// <param name="title">The title of the notification</param> /// <param name="title">The title of the notification</param>
/// <param name="message">The message of the notification</param> /// <param name="message">The message of the notification</param>
/// <param name="icon">The Material Design icon to show in the notification</param> /// <param name="icon">The Material Design icon to show in the notification</param>
void ShowNotification(string title, string message, PackIconKind icon); /// <param name="activatedCallback">A callback that is invoked when the notification is clicked</param>
/// <param name="dismissedCallback">A callback that is invoked when the notification is dismissed</param>
void ShowNotification(string title, string message, PackIconKind icon, Action? activatedCallback, Action? dismissedCallback);
} }
} }

View File

@ -3,7 +3,7 @@ using MaterialDesignThemes.Wpf;
namespace Artemis.UI.Shared.Services namespace Artemis.UI.Shared.Services
{ {
internal class MessageService : IMessageService internal class MessageService : IMessageService, IDisposable
{ {
private INotificationProvider? _notificationProvider; private INotificationProvider? _notificationProvider;
public ISnackbarMessageQueue MainMessageQueue { get; } public ISnackbarMessageQueue MainMessageQueue { get; }
@ -14,8 +14,12 @@ namespace Artemis.UI.Shared.Services
} }
/// <inheritdoc /> /// <inheritdoc />
public void ConfigureNotificationProvider(INotificationProvider notificationProvider) public void SetNotificationProvider(INotificationProvider notificationProvider)
{ {
if (ReferenceEquals(_notificationProvider, notificationProvider))
return;
_notificationProvider?.Dispose();
_notificationProvider = notificationProvider; _notificationProvider = notificationProvider;
} }
@ -72,22 +76,32 @@ namespace Artemis.UI.Shared.Services
} }
/// <inheritdoc /> /// <inheritdoc />
public void ShowNotification(string title, string message) public void ShowNotification(string title, string message, Action? activatedCallback = null, Action? dismissedCallback = null)
{ {
_notificationProvider?.ShowNotification(title, message, PackIconKind.None); _notificationProvider?.ShowNotification(title, message, PackIconKind.None, activatedCallback, dismissedCallback);
} }
/// <inheritdoc /> /// <inheritdoc />
public void ShowNotification(string title, string message, PackIconKind icon) public void ShowNotification(string title, string message, PackIconKind icon, Action? activatedCallback = null, Action? dismissedCallback = null)
{ {
_notificationProvider?.ShowNotification(title, message, icon); _notificationProvider?.ShowNotification(title, message, icon, activatedCallback, dismissedCallback);
} }
/// <inheritdoc /> /// <inheritdoc />
public void ShowNotification(string title, string message, string icon) public void ShowNotification(string title, string message, string icon, Action? activatedCallback = null, Action? dismissedCallback = null)
{ {
Enum.TryParse(typeof(PackIconKind), icon, true, out object? iconKind); Enum.TryParse(typeof(PackIconKind), icon, true, out object? iconKind);
_notificationProvider?.ShowNotification(title, message, (PackIconKind) (iconKind ?? PackIconKind.None)); _notificationProvider?.ShowNotification(title, message, (PackIconKind) (iconKind ?? PackIconKind.None), activatedCallback, dismissedCallback);
} }
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
_notificationProvider?.Dispose();
}
#endregion
} }
} }

View File

@ -118,10 +118,10 @@ namespace Artemis.UI
private void UtilitiesOnShutdownRequested(object sender, EventArgs e) private void UtilitiesOnShutdownRequested(object sender, EventArgs e)
{ {
// Use PowerShell to kill the process after 2 sec just in case // Use PowerShell to kill the process after 8 sec just in case
ProcessStartInfo info = new() ProcessStartInfo info = new()
{ {
Arguments = "-Command \"& {Start-Sleep -s 2; (Get-Process 'Artemis.UI').kill()}", Arguments = "-Command \"& {Start-Sleep -s 8; (Get-Process 'Artemis.UI').kill()}",
WindowStyle = ProcessWindowStyle.Hidden, WindowStyle = ProcessWindowStyle.Hidden,
CreateNoWindow = true, CreateNoWindow = true,
FileName = "PowerShell.exe" FileName = "PowerShell.exe"

View File

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk"> <Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup> <PropertyGroup>
<OutputType>WinExe</OutputType> <OutputType>WinExe</OutputType>
<TargetFramework>net5.0-windows</TargetFramework> <TargetFramework>net5.0-windows10.0.17763.0</TargetFramework>
<ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids> <ProjectTypeGuids>{60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</ProjectTypeGuids>
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects> <AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
<AssemblyTitle>Artemis</AssemblyTitle> <AssemblyTitle>Artemis</AssemblyTitle>
@ -10,7 +10,8 @@
<Description>Provides advanced unified lighting across many different brands RGB peripherals</Description> <Description>Provides advanced unified lighting across many different brands RGB peripherals</Description>
<Copyright>Copyright © Robert Beekman - 2021</Copyright> <Copyright>Copyright © Robert Beekman - 2021</Copyright>
<FileVersion>2.0.0.0</FileVersion> <FileVersion>2.0.0.0</FileVersion>
<OutputPath>bin\</OutputPath> <OutputPath>bin\net5.0-windows\</OutputPath>
<AppendTargetFrameworkToOutputPath>False</AppendTargetFrameworkToOutputPath>
<UseWPF>true</UseWPF> <UseWPF>true</UseWPF>
<Platforms>x64</Platforms> <Platforms>x64</Platforms>
<SupportedPlatform>windows</SupportedPlatform> <SupportedPlatform>windows</SupportedPlatform>
@ -84,6 +85,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Resource Include="Resources\Fonts\RobotoMono-Regular.ttf" /> <Resource Include="Resources\Fonts\RobotoMono-Regular.ttf" />
<Resource Include="Resources\Images\Logo\bow-black.ico" />
<Resource Include="Resources\Images\Logo\bow-white.ico" /> <Resource Include="Resources\Images\Logo\bow-white.ico" />
<Resource Include="Resources\Images\Logo\bow-white.svg" /> <Resource Include="Resources\Images\Logo\bow-white.svg" />
<Resource Include="Resources\Images\Logo\bow.ico" /> <Resource Include="Resources\Images\Logo\bow.ico" />
@ -135,13 +137,14 @@
<Resource Include="Resources\Images\Sidebar\sidebar-header.png" /> <Resource Include="Resources\Images\Sidebar\sidebar-header.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentValidation" Version="10.0.0" /> <PackageReference Include="FluentValidation" Version="10.1.0" />
<PackageReference Include="Flurl.Http" Version="3.0.1" /> <PackageReference Include="Flurl.Http" Version="3.0.1" />
<PackageReference Include="gong-wpf-dragdrop" Version="2.3.2" /> <PackageReference Include="gong-wpf-dragdrop" Version="2.3.2" />
<PackageReference Include="Hardcodet.NotifyIcon.Wpf.NetCore" Version="1.0.18" /> <PackageReference Include="Hardcodet.NotifyIcon.Wpf.NetCore" Version="1.0.18" />
<PackageReference Include="Humanizer.Core" Version="2.8.26" /> <PackageReference Include="Humanizer.Core" Version="2.8.26" />
<PackageReference Include="MaterialDesignExtensions" Version="3.3.0" /> <PackageReference Include="MaterialDesignExtensions" Version="3.3.0" />
<PackageReference Include="MaterialDesignThemes" Version="4.0.0" /> <PackageReference Include="MaterialDesignThemes" Version="4.0.0" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.0.2" />
<PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" /> <PackageReference Include="Microsoft.Win32.Registry" Version="5.0.0" />
<PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.31" /> <PackageReference Include="Microsoft.Xaml.Behaviors.Wpf" Version="1.1.31" />
<PackageReference Include="Ninject" Version="3.3.4" /> <PackageReference Include="Ninject" Version="3.3.4" />
@ -311,6 +314,7 @@
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Remove="Resources\Fonts\RobotoMono-Regular.ttf" /> <None Remove="Resources\Fonts\RobotoMono-Regular.ttf" />
<None Remove="Resources\Images\Logo\bow-black.ico" />
<None Remove="Resources\Images\Logo\bow-white.ico" /> <None Remove="Resources\Images\Logo\bow-white.ico" />
<None Remove="Resources\Images\Logo\bow-white.svg" /> <None Remove="Resources\Images\Logo\bow-white.svg" />
<None Remove="Resources\Images\Logo\bow.ico" /> <None Remove="Resources\Images\Logo\bow.ico" />

View File

@ -98,7 +98,7 @@ namespace Artemis.UI
}); });
IRegistrationService registrationService = Kernel.Get<IRegistrationService>(); IRegistrationService registrationService = Kernel.Get<IRegistrationService>();
registrationService.RegisterInputProvider(); registrationService.RegisterProviders();
registrationService.RegisterControllers(); registrationService.RegisterControllers();
Execute.OnUIThreadSync(() => { registrationService.ApplyPreferredGraphicsContext(); }); Execute.OnUIThreadSync(() => { registrationService.ApplyPreferredGraphicsContext(); });

View File

@ -1,11 +1,9 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Timers; using System.Timers;
using System.Windows.Forms; using System.Windows.Forms;
using System.Windows.Input; using System.Windows.Input;
using System.Windows.Interop;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Utilities; using Artemis.UI.Utilities;
@ -14,7 +12,7 @@ using Linearstar.Windows.RawInput.Native;
using Serilog; using Serilog;
using MouseButton = Artemis.Core.Services.MouseButton; using MouseButton = Artemis.Core.Services.MouseButton;
namespace Artemis.UI.InputProviders namespace Artemis.UI.Providers
{ {
public class NativeWindowInputProvider : InputProvider public class NativeWindowInputProvider : InputProvider
{ {

View File

@ -1,7 +1,7 @@
using System; using System;
using System.Windows.Forms; using System.Windows.Forms;
namespace Artemis.UI.InputProviders namespace Artemis.UI.Providers
{ {
public sealed class SpongeWindow : NativeWindow public sealed class SpongeWindow : NativeWindow
{ {

View File

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Windows.UI.Notifications;
using Artemis.UI.Shared.Services;
using Artemis.UI.Utilities;
using MaterialDesignThemes.Wpf;
using Microsoft.Toolkit.Uwp.Notifications;
using Stylet;
namespace Artemis.UI.Providers
{
public class ToastNotificationProvider : INotificationProvider
{
private ThemeWatcher _themeWatcher;
public ToastNotificationProvider()
{
_themeWatcher = new ThemeWatcher();
}
public static PngBitmapEncoder GetEncoderForIcon(PackIconKind icon, Color color)
{
// Convert the PackIcon to an icon by drawing it on a visual
DrawingVisual drawingVisual = new();
DrawingContext drawingContext = drawingVisual.RenderOpen();
PackIcon packIcon = new() {Kind = icon};
Geometry geometry = Geometry.Parse(packIcon.Data);
// Scale the icon up to fit a 256x256 image and draw it
geometry = Geometry.Combine(geometry, Geometry.Empty, GeometryCombineMode.Union, new ScaleTransform(256 / geometry.Bounds.Right, 256 / geometry.Bounds.Bottom));
drawingContext.DrawGeometry(new SolidColorBrush(color), null, geometry);
drawingContext.Close();
// Render the visual and add it to a PNG encoder (we want opacity in our icon)
RenderTargetBitmap renderTargetBitmap = new(256, 256, 96, 96, PixelFormats.Pbgra32);
renderTargetBitmap.Render(drawingVisual);
PngBitmapEncoder encoder = new();
encoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap));
return encoder;
}
private void ToastDismissed(string imagePath, Action dismissedCallback)
{
if (File.Exists(imagePath))
File.Delete(imagePath);
dismissedCallback?.Invoke();
}
private void ToastActivated(string imagePath, Action activatedCallback)
{
if (File.Exists(imagePath))
File.Delete(imagePath);
activatedCallback?.Invoke();
}
#region Implementation of INotificationProvider
/// <inheritdoc />
public void ShowNotification(string title, string message, PackIconKind icon, Action activatedCallback, Action dismissedCallback)
{
string imagePath = Path.GetTempFileName().Replace(".tmp", "png");
Execute.OnUIThreadSync(() =>
{
using FileStream stream = File.OpenWrite(imagePath);
GetEncoderForIcon(icon, _themeWatcher.GetWindowsTheme() == ThemeWatcher.WindowsTheme.Dark ? Colors.White : Colors.Black).Save(stream);
});
new ToastContentBuilder()
.AddAppLogoOverride(new Uri(imagePath))
.AddText(title, AdaptiveTextStyle.Header)
.AddText(message)
.Show(t =>
{
t.Dismissed += (_, _) => ToastDismissed(imagePath, dismissedCallback);
t.Activated += (_, _) => ToastActivated(imagePath, activatedCallback);
t.Data = new NotificationData(new List<KeyValuePair<string, string>> {new("image", imagePath)});
});
}
#endregion
#region IDisposable
/// <inheritdoc />
public void Dispose()
{
ToastNotificationManagerCompat.Uninstall();
}
#endregion
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

View File

@ -11,7 +11,7 @@
mc:Ignorable="d" mc:Ignorable="d"
d:DesignHeight="574.026" d:DesignHeight="574.026"
d:DesignWidth="1029.87" d:DesignWidth="1029.87"
d:DataContext="{d:DesignInstance home:HomeViewModel, IsDesignTimeCreatable=True}"> d:DataContext="{d:DesignInstance home:HomeViewModel}">
<UserControl.Resources> <UserControl.Resources>
<ResourceDictionary> <ResourceDictionary>
<ResourceDictionary.MergedDictionaries> <ResourceDictionary.MergedDictionaries>
@ -41,7 +41,7 @@
<Image Source="{svgc:SvgImage Source=/Resources/Images/Logo/bow.svg}" Height="100" Width="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"> <StackPanel Grid.Column="1" Margin="24 0 0 0" VerticalAlignment="Center">
<TextBlock Style="{StaticResource MaterialDesignHeadline4TextBlock}" TextWrapping="Wrap"> <TextBlock Style="{StaticResource MaterialDesignHeadline4TextBlock}" TextWrapping="Wrap">
<Run Text="Welcome to Artemis, the unified"></Run> <Run Text="Welcome to Artemis, the unified" />
<Run Text="RGB"> <Run Text="RGB">
<Run.Foreground> <Run.Foreground>
<LinearGradientBrush EndPoint="0,0" StartPoint="1,1"> <LinearGradientBrush EndPoint="0,0" StartPoint="1,1">
@ -52,8 +52,9 @@
<GradientStop Color="#1cb6e7" Offset="0.8" /> <GradientStop Color="#1cb6e7" Offset="0.8" />
<GradientStop Color="#2df4b5" Offset="1" /> <GradientStop Color="#2df4b5" Offset="1" />
</LinearGradientBrush> </LinearGradientBrush>
</Run.Foreground></Run> </Run.Foreground>
<Run Text="platform."></Run> </Run>
<Run Text="platform." />
</TextBlock> </TextBlock>
<Button Style="{StaticResource MaterialDesignFlatButton}" <Button Style="{StaticResource MaterialDesignFlatButton}"
Foreground="{StaticResource SecondaryHueMidBrush}" Foreground="{StaticResource SecondaryHueMidBrush}"
@ -70,7 +71,7 @@
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Bottom" VerticalAlignment="Bottom"
Margin="0 0 0 32"> Margin="0 0 0 32">
<Grid>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<materialDesign:Card Width="420" Margin="8 2 4 16" Height="Auto"> <materialDesign:Card Width="420" Margin="8 2 4 16" Height="Auto">
<Grid VerticalAlignment="Stretch"> <Grid VerticalAlignment="Stretch">
@ -187,7 +188,7 @@
</Grid> </Grid>
</materialDesign:Card> </materialDesign:Card>
</StackPanel> </StackPanel>
</Grid>
</ScrollViewer> </ScrollViewer>
<!-- PopupBox could be nice in the future when we actually have some stuff to send ppl to --> <!-- PopupBox could be nice in the future when we actually have some stuff to send ppl to -->
<!--<materialDesign:PopupBox Style="{StaticResource MaterialDesignMultiFloatingActionPopupBox}" <!--<materialDesign:PopupBox Style="{StaticResource MaterialDesignMultiFloatingActionPopupBox}"

View File

@ -35,13 +35,13 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" /> <TextBlock Text="{Binding Name}" VerticalAlignment="Center" />
<Button Grid.Column="1" <Button Grid.Column="1"
Style="{StaticResource MaterialDesignFloatingActionMiniDarkButton}" Style="{StaticResource MaterialDesignIconForegroundButton}"
Width="26" Width="26"
Height="26" Height="26"
VerticalAlignment="Top" VerticalAlignment="Top"
Command="{s:Action DeleteProfile}" Command="{s:Action DeleteProfile}"
CommandParameter="{Binding}"> CommandParameter="{Binding}">
<materialDesign:PackIcon Kind="TrashCanOutline" Height="14" Width="14" HorizontalAlignment="Right" /> <materialDesign:PackIcon Kind="Delete" Height="16" Width="16" HorizontalAlignment="Right" />
</Button> </Button>
</Grid> </Grid>
</DataTemplate> </DataTemplate>

View File

@ -14,7 +14,6 @@ using Artemis.UI.Screens.ProfileEditor.LayerProperties;
using Artemis.UI.Screens.ProfileEditor.ProfileTree; using Artemis.UI.Screens.ProfileEditor.ProfileTree;
using Artemis.UI.Screens.ProfileEditor.Visualization; using Artemis.UI.Screens.ProfileEditor.Visualization;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using MaterialDesignThemes.Wpf;
using Stylet; using Stylet;
namespace Artemis.UI.Screens.ProfileEditor namespace Artemis.UI.Screens.ProfileEditor
@ -388,7 +387,7 @@ namespace Artemis.UI.Screens.ProfileEditor
{ {
// Get all profiles from the database // Get all profiles from the database
Profiles.Clear(); Profiles.Clear();
Profiles.AddRange(_profileService.GetProfileDescriptors(Module).OrderBy(d => d.Name)); Profiles.AddRange(_profileService.GetProfileDescriptors(Module).OrderBy(p => p.Name));
} }
} }
} }

View File

@ -7,7 +7,7 @@
xmlns:s="https://github.com/canton7/Stylet" xmlns:s="https://github.com/canton7/Stylet"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
mc:Ignorable="d"> mc:Ignorable="d">
<tb:TaskbarIcon IconSource="/Resources/Images/Logo/bow-white.ico" <tb:TaskbarIcon IconSource="{Binding Icon}"
MenuActivation="LeftOrRightClick" MenuActivation="LeftOrRightClick"
PopupActivation="DoubleClick" PopupActivation="DoubleClick"
DoubleClickCommand="{s:Action TrayBringToForeground}" DoubleClickCommand="{s:Action TrayBringToForeground}"

View File

@ -1,6 +1,4 @@
using System; using System;
using System.Drawing;
using System.IO;
using System.Windows; using System.Windows;
using System.Windows.Controls; using System.Windows.Controls;
using System.Windows.Media; using System.Windows.Media;
@ -17,26 +15,25 @@ using Hardcodet.Wpf.TaskbarNotification;
using MaterialDesignThemes.Wpf; using MaterialDesignThemes.Wpf;
using Ninject; using Ninject;
using Stylet; using Stylet;
using Icon = System.Drawing.Icon;
namespace Artemis.UI.Screens namespace Artemis.UI.Screens
{ {
public class TrayViewModel : Screen, IMainWindowProvider, INotificationProvider public class TrayViewModel : Screen, IMainWindowProvider
{ {
private readonly PluginSetting<ApplicationColorScheme> _colorScheme;
private readonly IDebugService _debugService; private readonly IDebugService _debugService;
private readonly IEventAggregator _eventAggregator; private readonly IEventAggregator _eventAggregator;
private readonly IKernel _kernel; private readonly IKernel _kernel;
private readonly IWindowManager _windowManager;
private readonly ThemeWatcher _themeWatcher; private readonly ThemeWatcher _themeWatcher;
private readonly PluginSetting<ApplicationColorScheme> _colorScheme; private readonly IWindowManager _windowManager;
private RootViewModel _rootViewModel; private RootViewModel _rootViewModel;
private SplashViewModel _splashViewModel; private SplashViewModel _splashViewModel;
private TaskbarIcon _taskBarIcon; private TaskbarIcon _taskBarIcon;
private ImageSource _icon;
public TrayViewModel(IKernel kernel, public TrayViewModel(IKernel kernel,
IWindowManager windowManager, IWindowManager windowManager,
IWindowService windowService, IWindowService windowService,
IMessageService messageService,
IUpdateService updateService, IUpdateService updateService,
IEventAggregator eventAggregator, IEventAggregator eventAggregator,
ICoreService coreService, ICoreService coreService,
@ -59,7 +56,6 @@ namespace Artemis.UI.Screens
ApplyColorSchemeSetting(); ApplyColorSchemeSetting();
windowService.ConfigureMainWindowProvider(this); windowService.ConfigureMainWindowProvider(this);
messageService.ConfigureNotificationProvider(this);
bool autoRunning = Bootstrapper.StartupArguments.Contains("--autorun"); bool autoRunning = Bootstrapper.StartupArguments.Contains("--autorun");
bool minimized = Bootstrapper.StartupArguments.Contains("--minimized"); bool minimized = Bootstrapper.StartupArguments.Contains("--minimized");
bool showOnAutoRun = settingsService.GetSetting("UI.ShowOnStartup", true).Value; bool showOnAutoRun = settingsService.GetSetting("UI.ShowOnStartup", true).Value;
@ -97,6 +93,12 @@ namespace Artemis.UI.Screens
OnMainWindowOpened(); OnMainWindowOpened();
} }
public ImageSource Icon
{
get => _icon;
set => SetAndNotify(ref _icon, value);
}
public void TrayActivateSidebarItem(string sidebarItem) public void TrayActivateSidebarItem(string sidebarItem)
{ {
TrayBringToForeground(); TrayBringToForeground();
@ -175,9 +177,15 @@ namespace Artemis.UI.Screens
private void ApplyWindowsTheme(ThemeWatcher.WindowsTheme windowsTheme) private void ApplyWindowsTheme(ThemeWatcher.WindowsTheme windowsTheme)
{ {
Execute.PostToUIThread(() =>
{
Icon = windowsTheme == ThemeWatcher.WindowsTheme.Dark
? new BitmapImage(new Uri("pack://application:,,,/Artemis.UI;component/Resources/Images/Logo/bow-white.ico"))
: new BitmapImage(new Uri("pack://application:,,,/Artemis.UI;component/Resources/Images/Logo/bow-black.ico"));
});
if (_colorScheme.Value != ApplicationColorScheme.Automatic) if (_colorScheme.Value != ApplicationColorScheme.Automatic)
return; return;
if (windowsTheme == ThemeWatcher.WindowsTheme.Dark) if (windowsTheme == ThemeWatcher.WindowsTheme.Dark)
ChangeMaterialColors(ApplicationColorScheme.Dark); ChangeMaterialColors(ApplicationColorScheme.Dark);
else else
@ -207,43 +215,6 @@ namespace Artemis.UI.Screens
#endregion #endregion
#region Implementation of INotificationProvider
/// <inheritdoc />
public void ShowNotification(string title, string message, PackIconKind icon)
{
Execute.OnUIThread(() =>
{
// Convert the PackIcon to an icon by drawing it on a visual
DrawingVisual drawingVisual = new();
DrawingContext drawingContext = drawingVisual.RenderOpen();
PackIcon packIcon = new() {Kind = icon};
Geometry geometry = Geometry.Parse(packIcon.Data);
// Scale the icon up to fit a 256x256 image and draw it
geometry = Geometry.Combine(geometry, Geometry.Empty, GeometryCombineMode.Union, new ScaleTransform(256 / geometry.Bounds.Right, 256 / geometry.Bounds.Bottom));
drawingContext.DrawGeometry(new SolidColorBrush(Colors.White), null, geometry);
drawingContext.Close();
// Render the visual and add it to a PNG encoder (we want opacity in our icon)
RenderTargetBitmap renderTargetBitmap = new(256, 256, 96, 96, PixelFormats.Pbgra32);
renderTargetBitmap.Render(drawingVisual);
PngBitmapEncoder encoder = new();
encoder.Frames.Add(BitmapFrame.Create(renderTargetBitmap));
// Save the PNG and get an icon handle
using MemoryStream stream = new();
encoder.Save(stream);
Icon convertedIcon = Icon.FromHandle(new Bitmap(stream).GetHicon());
// Show the 'balloon'
_taskBarIcon.ShowBalloonTip(title, message, convertedIcon, true);
});
}
#endregion
#region Implementation of IMainWindowProvider #region Implementation of IMainWindowProvider
public bool IsMainWindowOpen { get; private set; } public bool IsMainWindowOpen { get; private set; }

View File

@ -6,8 +6,8 @@ using Artemis.UI.Controllers;
using Artemis.UI.DefaultTypes.DataModel.Display; using Artemis.UI.DefaultTypes.DataModel.Display;
using Artemis.UI.DefaultTypes.DataModel.Input; using Artemis.UI.DefaultTypes.DataModel.Input;
using Artemis.UI.DefaultTypes.PropertyInput; using Artemis.UI.DefaultTypes.PropertyInput;
using Artemis.UI.InputProviders;
using Artemis.UI.Ninject; using Artemis.UI.Ninject;
using Artemis.UI.Providers;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.SkiaSharp; using Artemis.UI.SkiaSharp;
using Serilog; using Serilog;
@ -22,6 +22,7 @@ namespace Artemis.UI.Services
private readonly IProfileEditorService _profileEditorService; private readonly IProfileEditorService _profileEditorService;
private readonly IPluginManagementService _pluginManagementService; private readonly IPluginManagementService _pluginManagementService;
private readonly IInputService _inputService; private readonly IInputService _inputService;
private readonly IMessageService _messageService;
private readonly IWebServerService _webServerService; private readonly IWebServerService _webServerService;
private readonly IRgbService _rgbService; private readonly IRgbService _rgbService;
private readonly ISettingsService _settingsService; private readonly ISettingsService _settingsService;
@ -36,6 +37,7 @@ namespace Artemis.UI.Services
IProfileEditorService profileEditorService, IProfileEditorService profileEditorService,
IPluginManagementService pluginManagementService, IPluginManagementService pluginManagementService,
IInputService inputService, IInputService inputService,
IMessageService messageService,
IWebServerService webServerService, IWebServerService webServerService,
IRgbService rgbService, IRgbService rgbService,
ISettingsService settingsService) ISettingsService settingsService)
@ -46,6 +48,7 @@ namespace Artemis.UI.Services
_profileEditorService = profileEditorService; _profileEditorService = profileEditorService;
_pluginManagementService = pluginManagementService; _pluginManagementService = pluginManagementService;
_inputService = inputService; _inputService = inputService;
_messageService = messageService;
_webServerService = webServerService; _webServerService = webServerService;
_rgbService = rgbService; _rgbService = rgbService;
_settingsService = settingsService; _settingsService = settingsService;
@ -99,9 +102,10 @@ namespace Artemis.UI.Services
_registeredBuiltInPropertyEditors = true; _registeredBuiltInPropertyEditors = true;
} }
public void RegisterInputProvider() public void RegisterProviders()
{ {
_inputService.AddInputProvider(new NativeWindowInputProvider(_logger, _inputService)); _inputService.AddInputProvider(new NativeWindowInputProvider(_logger, _inputService));
_messageService.SetNotificationProvider(new ToastNotificationProvider());
} }
public void RegisterControllers() public void RegisterControllers()
@ -160,7 +164,7 @@ namespace Artemis.UI.Services
void RegisterBuiltInDataModelDisplays(); void RegisterBuiltInDataModelDisplays();
void RegisterBuiltInDataModelInputs(); void RegisterBuiltInDataModelInputs();
void RegisterBuiltInPropertyEditors(); void RegisterBuiltInPropertyEditors();
void RegisterInputProvider(); void RegisterProviders();
void RegisterControllers(); void RegisterControllers();
void ApplyPreferredGraphicsContext(); void ApplyPreferredGraphicsContext();
} }

View File

@ -8,6 +8,7 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Timers; using System.Timers;
using Windows.UI.Notifications;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.Exceptions; using Artemis.UI.Exceptions;
@ -16,9 +17,8 @@ using Artemis.UI.Services.Models.UpdateService;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Flurl; using Flurl;
using Flurl.Http; using Flurl.Http;
using MaterialDesignThemes.Wpf; using Microsoft.Toolkit.Uwp.Notifications;
using Serilog; using Serilog;
using Constants = Artemis.Core.Constants;
using File = System.IO.File; using File = System.IO.File;
namespace Artemis.UI.Services namespace Artemis.UI.Services
@ -32,14 +32,12 @@ namespace Artemis.UI.Services
private readonly PluginSetting<bool> _checkForUpdates; private readonly PluginSetting<bool> _checkForUpdates;
private readonly IDialogService _dialogService; private readonly IDialogService _dialogService;
private readonly ILogger _logger; private readonly ILogger _logger;
private readonly IMessageService _messageService;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
public UpdateService(ILogger logger, ISettingsService settingsService, IDialogService dialogService, IMessageService messageService, IWindowService windowService) public UpdateService(ILogger logger, ISettingsService settingsService, IDialogService dialogService, IWindowService windowService)
{ {
_logger = logger; _logger = logger;
_dialogService = dialogService; _dialogService = dialogService;
_messageService = messageService;
_windowService = windowService; _windowService = windowService;
_windowService.MainWindowOpened += WindowServiceOnMainWindowOpened; _windowService.MainWindowOpened += WindowServiceOnMainWindowOpened;
@ -52,8 +50,6 @@ namespace Artemis.UI.Services
timer.Start(); timer.Start();
} }
public bool SuspendAutoUpdate { get; set; }
private async Task OfferUpdate(DevOpsBuild buildInfo) private async Task OfferUpdate(DevOpsBuild buildInfo)
{ {
object result = await _dialogService.ShowDialog<UpdateDialogViewModel>(new Dictionary<string, object> {{"buildInfo", buildInfo}}); object result = await _dialogService.ShowDialog<UpdateDialogViewModel>(new Dictionary<string, object> {{"buildInfo", buildInfo}});
@ -82,6 +78,31 @@ namespace Artemis.UI.Services
await httpResponseMessage.Content.CopyToAsync(fs); await httpResponseMessage.Content.CopyToAsync(fs);
} }
private async void TOnActivated(ToastNotification sender, object args)
{
if (args is not ToastActivatedEventArgs toastEventArgs)
return;
if (toastEventArgs.Arguments == "update")
await ApplyUpdate();
else if (toastEventArgs.Arguments == "later")
SuspendAutoUpdate = true;
}
private async void CheckForUpdatesOnSettingChanged(object sender, EventArgs e)
{
// Run an auto-update as soon as the setting gets changed to enabled
if (_checkForUpdates.Value)
await AutoUpdate();
}
private async void WindowServiceOnMainWindowOpened(object sender, EventArgs e)
{
await AutoUpdate();
}
public bool SuspendAutoUpdate { get; set; }
public async Task<bool> AutoUpdate() public async Task<bool> AutoUpdate()
{ {
if (Constants.BuildInfo.IsLocalBuild) if (Constants.BuildInfo.IsLocalBuild)
@ -114,27 +135,26 @@ namespace Artemis.UI.Services
return false; return false;
if (_windowService.IsMainWindowOpen) if (_windowService.IsMainWindowOpen)
{
await OfferUpdate(buildInfo); await OfferUpdate(buildInfo);
}
else if (_autoInstallUpdates.Value) else if (_autoInstallUpdates.Value)
{ {
// Lets go new ToastContentBuilder()
_messageService.ShowNotification( .AddText("Installing new version", AdaptiveTextStyle.Header)
"Installing new version", .AddText($"Build {buildNumberDisplay} is available, currently on {Constants.BuildInfo.BuildNumberDisplay}.")
$"Build {buildNumberDisplay} is available, currently on {Constants.BuildInfo.BuildNumberDisplay}.", .AddProgressBar(null, null, true)
PackIconKind.Update .Show();
);
await ApplyUpdate(); await ApplyUpdate();
} }
else else
{ {
// If auto-install is disabled and the window is closed, best we can do is notify the user and stop. // If auto-install is disabled and the window is closed, best we can do is notify the user and stop.
_messageService.ShowNotification( new ToastContentBuilder()
"New version available", .AddText("New version available", AdaptiveTextStyle.Header)
$"Build {buildNumberDisplay} is available, currently on {Constants.BuildInfo.BuildNumberDisplay}.", .AddText($"Build {buildNumberDisplay} is available, currently on {Constants.BuildInfo.BuildNumberDisplay}.")
PackIconKind.Update .AddButton("Update", ToastActivationType.Background, "update")
); .AddButton("Later", ToastActivationType.Background, "later")
.Show(t => t.Activated += TOnActivated);
} }
return true; return true;
@ -157,9 +177,7 @@ namespace Artemis.UI.Services
// Always update installer if it is missing ^^ // Always update installer if it is missing ^^
if (!File.Exists(installerPath)) if (!File.Exists(installerPath))
{
await UpdateInstaller(); await UpdateInstaller();
}
// Compare the creation date of the installer with the build date and update if needed // Compare the creation date of the installer with the build date and update if needed
else else
{ {
@ -226,22 +244,6 @@ namespace Artemis.UI.Services
.WithHeader("Accept", "application/vnd.github.v3+json") .WithHeader("Accept", "application/vnd.github.v3+json")
.GetJsonAsync<GitHubDifference>(); .GetJsonAsync<GitHubDifference>();
} }
#region Event handlers
private async void CheckForUpdatesOnSettingChanged(object sender, EventArgs e)
{
// Run an auto-update as soon as the setting gets changed to enabled
if (_checkForUpdates.Value)
await AutoUpdate();
}
private async void WindowServiceOnMainWindowOpened(object sender, EventArgs e)
{
await AutoUpdate();
}
#endregion
} }
public interface IUpdateService : IArtemisUIService public interface IUpdateService : IArtemisUIService

View File

@ -4,9 +4,9 @@
".NETCoreApp,Version=v5.0": { ".NETCoreApp,Version=v5.0": {
"FluentValidation": { "FluentValidation": {
"type": "Direct", "type": "Direct",
"requested": "[10.0.0, )", "requested": "[10.1.0, )",
"resolved": "10.0.0", "resolved": "10.1.0",
"contentHash": "jNFPbLjBy/bfIWx4BV/WVEsS+1OxBVf22mmSdvVa9RCHJDkNhAjbKZkxgA0s1rYNFxVn+a1fQbos95t4j/z3Zg==" "contentHash": "RxhhfY9IcEY2qUMYjoUxegInbuE5Bwll7dVLsXpiJf25g0ztmzUK+HHqtPcub1caPemhMJsC+NwjHei+NgAkvA=="
}, },
"Flurl.Http": { "Flurl.Http": {
"type": "Direct", "type": "Direct",
@ -60,6 +60,18 @@
"MaterialDesignColors": "2.0.0" "MaterialDesignColors": "2.0.0"
} }
}, },
"Microsoft.Toolkit.Uwp.Notifications": {
"type": "Direct",
"requested": "[7.0.2, )",
"resolved": "7.0.2",
"contentHash": "UWwo9Jdkk52E3zmUMoO+JC2Aix1gizCPIHtVBUON/uyzjKlnjgqoBd7zeS8HJ94Vsm2mW4OjVtPVhz3sEwEDQA==",
"dependencies": {
"Microsoft.Win32.Registry": "4.7.0",
"System.Drawing.Common": "4.7.0",
"System.Reflection.Emit": "4.7.0",
"System.ValueTuple": "4.5.0"
}
},
"Microsoft.Win32.Registry": { "Microsoft.Win32.Registry": {
"type": "Direct", "type": "Direct",
"requested": "[5.0.0, )", "requested": "[5.0.0, )",
@ -990,15 +1002,8 @@
}, },
"System.Reflection.Emit": { "System.Reflection.Emit": {
"type": "Transitive", "type": "Transitive",
"resolved": "4.3.0", "resolved": "4.7.0",
"contentHash": "228FG0jLcIwTVJyz8CLFKueVqQK36ANazUManGaJHkO0icjiIypKW7YLWLIWahyIkdh5M7mV2dJepllLyA1SKg==", "contentHash": "VR4kk8XLKebQ4MZuKuIni/7oh+QGFmZW3qORd1GvBq/8026OpW501SzT/oypwiQl4TvT8ErnReh/NzY9u+C6wQ=="
"dependencies": {
"System.IO": "4.3.0",
"System.Reflection": "4.3.0",
"System.Reflection.Emit.ILGeneration": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
}, },
"System.Reflection.Emit.ILGeneration": { "System.Reflection.Emit.ILGeneration": {
"type": "Transitive", "type": "Transitive",