From 96c55e5c030f6ad2d890f75970a366c8d1bc7b87 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 14 May 2021 21:49:01 +0200 Subject: [PATCH 1/3] Profile adaption - Added support for device hint to 'all' devices Layouts - Round LED sizes to integers Auto-update - Use modern Windows 10 toasts UI - On shutdown wait 6 seconds longer before force-shutdown UI - Use black icons in notifications on white Windows theme Message service - Added optional toast callbacks --- .../AdaptionHints/DeviceAdaptionHint.cs | 2 +- src/Artemis.Core/Models/Profile/Layer.cs | 6 +- .../Models/Profile/LayerAdapter.cs | 55 +- .../Models/Profile/ProfileDescriptor.cs | 6 +- .../Models/Surface/ArtemisDevice.cs | 3 +- .../Models/Surface/Layout/ArtemisLayout.cs | 39 + .../Plugins/Modules/ProfileModule.cs | 64 +- .../Prerequisites/PluginPrerequisite.cs | 2 +- .../PrerequisiteAction/ExecuteFileAction.cs | 36 +- .../Services/Storage/ProfileService.cs | 7 +- .../Services/Message/IMessageService.cs | 14 +- .../Services/Message/INotificationProvider.cs | 9 +- .../Services/Message/MessageService.cs | 30 +- src/Artemis.UI/ApplicationStateManager.cs | 4 +- src/Artemis.UI/Artemis.UI.csproj | 740 +++++++++--------- src/Artemis.UI/Bootstrapper.cs | 2 +- .../NativeWindowInputProvider.cs | 4 +- .../SpongeWindow.cs | 2 +- .../Providers/ToastNotificationProvider.cs | 101 +++ src/Artemis.UI/Screens/Home/HomeView.xaml | 253 +++--- .../ProfileEditor/ProfileEditorView.xaml | 4 +- .../ProfileEditor/ProfileEditorViewModel.cs | 8 +- src/Artemis.UI/Screens/TrayViewModel.cs | 52 +- .../Services/RegistrationService.cs | 10 +- src/Artemis.UI/Services/UpdateService.cs | 78 +- src/Artemis.UI/packages.lock.json | 29 +- 26 files changed, 886 insertions(+), 674 deletions(-) rename src/Artemis.UI/{InputProviders => Providers}/NativeWindowInputProvider.cs (99%) rename src/Artemis.UI/{InputProviders => Providers}/SpongeWindow.cs (93%) create mode 100644 src/Artemis.UI/Providers/ToastNotificationProvider.cs diff --git a/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs b/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs index 2a831e820..41f59e4f9 100644 --- a/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs +++ b/src/Artemis.Core/Models/Profile/AdaptionHints/DeviceAdaptionHint.cs @@ -48,7 +48,7 @@ namespace Artemis.Core public void Apply(Layer layer, List devices) { IEnumerable matches = devices - .Where(d => d.RgbDevice.DeviceInfo.DeviceType == DeviceType) + .Where(d => DeviceType == RGBDeviceType.All || d.RgbDevice.DeviceInfo.DeviceType == DeviceType) .OrderBy(d => d.Rectangle.Top) .ThenBy(d => d.Rectangle.Left) .Skip(Skip); diff --git a/src/Artemis.Core/Models/Profile/Layer.cs b/src/Artemis.Core/Models/Profile/Layer.cs index 2956469dd..5e910dfe9 100644 --- a/src/Artemis.Core/Models/Profile/Layer.cs +++ b/src/Artemis.Core/Models/Profile/Layer.cs @@ -66,8 +66,8 @@ namespace Artemis.Core _leds = new List(); - Load(); Adapter = new LayerAdapter(this); + Load(); Initialize(); } @@ -242,6 +242,7 @@ namespace Artemis.Core ExpandedPropertyGroups.AddRange(LayerEntity.ExpandedPropertyGroups); LoadRenderElement(); + Adapter.Load(); } internal override void Save() @@ -276,6 +277,9 @@ namespace Artemis.Core LayerEntity.Leds.Add(ledEntity); } + // Adaption hints + Adapter.Save(); + SaveRenderElement(); } diff --git a/src/Artemis.Core/Models/Profile/LayerAdapter.cs b/src/Artemis.Core/Models/Profile/LayerAdapter.cs index e2eb104ee..77717340f 100644 --- a/src/Artemis.Core/Models/Profile/LayerAdapter.cs +++ b/src/Artemis.Core/Models/Profile/LayerAdapter.cs @@ -70,33 +70,42 @@ namespace Artemis.Core public List DetermineHints(IEnumerable devices) { List newHints = new(); - // Any fully covered device will add a device adaption hint for that type - foreach (IGrouping deviceLeds in Layer.Leds.GroupBy(l => l.Device)) + if (devices.All(DoesLayerCoverDevice)) { - ArtemisDevice device = deviceLeds.Key; - // If there is already an adaption hint for this type, don't add another - if (AdaptionHints.Any(h => h is DeviceAdaptionHint d && d.DeviceType == device.RgbDevice.DeviceInfo.DeviceType)) - continue; - if (DoesLayerCoverDevice(device)) - { - DeviceAdaptionHint hint = new() {DeviceType = device.RgbDevice.DeviceInfo.DeviceType}; - AdaptionHints.Add(hint); - newHints.Add(hint); - } + DeviceAdaptionHint hint = new() {DeviceType = RGBDeviceType.All}; + AdaptionHints.Add(hint); + newHints.Add(hint); } - - // Any fully covered category will add a category adaption hint for its category - foreach (DeviceCategory deviceCategory in Enum.GetValues()) + else { - if (AdaptionHints.Any(h => h is CategoryAdaptionHint c && c.Category == deviceCategory)) - continue; - - List categoryDevices = devices.Where(d => d.Categories.Contains(deviceCategory)).ToList(); - if (categoryDevices.Any() && categoryDevices.All(DoesLayerCoverDevice)) + // Any fully covered device will add a device adaption hint for that type + foreach (IGrouping deviceLeds in Layer.Leds.GroupBy(l => l.Device)) { - CategoryAdaptionHint hint = new() {Category = deviceCategory}; - AdaptionHints.Add(hint); - newHints.Add(hint); + ArtemisDevice device = deviceLeds.Key; + // If there is already an adaption hint for this type, don't add another + if (AdaptionHints.Any(h => h is DeviceAdaptionHint d && d.DeviceType == device.RgbDevice.DeviceInfo.DeviceType)) + continue; + if (DoesLayerCoverDevice(device)) + { + DeviceAdaptionHint hint = new() {DeviceType = device.RgbDevice.DeviceInfo.DeviceType}; + AdaptionHints.Add(hint); + newHints.Add(hint); + } + } + + // Any fully covered category will add a category adaption hint for its category + foreach (DeviceCategory deviceCategory in Enum.GetValues()) + { + if (AdaptionHints.Any(h => h is CategoryAdaptionHint c && c.Category == deviceCategory)) + continue; + + List categoryDevices = devices.Where(d => d.Categories.Contains(deviceCategory)).ToList(); + if (categoryDevices.Any() && categoryDevices.All(DoesLayerCoverDevice)) + { + CategoryAdaptionHint hint = new() {Category = deviceCategory}; + AdaptionHints.Add(hint); + newHints.Add(hint); + } } } diff --git a/src/Artemis.Core/Models/Profile/ProfileDescriptor.cs b/src/Artemis.Core/Models/Profile/ProfileDescriptor.cs index b59e963c8..7171577fc 100644 --- a/src/Artemis.Core/Models/Profile/ProfileDescriptor.cs +++ b/src/Artemis.Core/Models/Profile/ProfileDescriptor.cs @@ -12,7 +12,7 @@ namespace Artemis.Core internal ProfileDescriptor(ProfileModule profileModule, ProfileEntity profileEntity) { ProfileModule = profileModule; - + Id = profileEntity.Id; Name = profileEntity.Name; IsLastActiveProfile = profileEntity.IsActive; @@ -38,5 +38,9 @@ namespace Artemis.Core /// public bool IsLastActiveProfile { get; } + /// + /// Gets or sets a boolean indicating whether the profile will be adapted the next time it is activated + /// + public bool NeedsAdaption { get; set; } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs index 1c0930793..488efdd8c 100644 --- a/src/Artemis.Core/Models/Surface/ArtemisDevice.cs +++ b/src/Artemis.Core/Models/Surface/ArtemisDevice.cs @@ -381,7 +381,8 @@ namespace Artemis.Core "set to true because the device provider does not support it"); if (layout.IsValid) - layout.RgbLayout!.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds); + layout.ApplyTo(RgbDevice, createMissingLeds, removeExcessiveLeds); + UpdateLeds(); diff --git a/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs index 7ff43d6ef..5376779fa 100644 --- a/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs +++ b/src/Artemis.Core/Models/Surface/Layout/ArtemisLayout.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using RGB.NET.Core; using RGB.NET.Layout; namespace Artemis.Core @@ -107,6 +108,44 @@ namespace Artemis.Core else Image = null; } + + /// + /// Applies the layout to the provided device + /// + 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 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 ledsToRemove = device.Select(led => led.Id).Where(id => !ledIds.Contains(id)).ToList(); + foreach (LedId led in ledsToRemove) + device.RemoveLed(led); + } + } } /// diff --git a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs index b2f6536ba..0e04cd3b6 100644 --- a/src/Artemis.Core/Plugins/Modules/ProfileModule.cs +++ b/src/Artemis.Core/Plugins/Modules/ProfileModule.cs @@ -1,11 +1,14 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.IO; using System.Linq; using System.Linq.Expressions; using System.Reflection; using System.Threading.Tasks; using Artemis.Core.DataModelExpansions; +using Artemis.Storage.Entities.Profile; +using Newtonsoft.Json; using SkiaSharp; namespace Artemis.Core.Modules @@ -91,11 +94,13 @@ namespace Artemis.Core.Modules /// public abstract class ProfileModule : Module { + private readonly List _defaultProfiles; + private readonly object _lock = new(); + /// /// Gets a list of all properties ignored at runtime using IgnoreProperty(x => x.y) /// protected internal readonly List HiddenPropertiesList = new(); - private readonly object _lock = new(); /// /// Creates a new instance of the class @@ -103,6 +108,7 @@ namespace Artemis.Core.Modules protected ProfileModule() { OpacityOverride = 1; + _defaultProfiles = new List(); } /// @@ -130,6 +136,11 @@ namespace Artemis.Core.Modules /// public bool AnimatingProfileChange { get; private set; } + /// + /// Gets a list of default profiles, to add a new default profile use + /// + public ReadOnlyCollection DefaultProfiles => _defaultProfiles.AsReadOnly(); + /// /// Called after the profile has updated /// @@ -148,6 +159,40 @@ namespace Artemis.Core.Modules { } + /// + /// Occurs when the has changed + /// + public event EventHandler? ActiveProfileChanged; + + /// + /// Adds a default profile by reading it from the file found at the provided path + /// + /// + protected void AddDefaultProfile(string 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(File.ReadAllText(file)); + if (profileEntity == null) + throw new ArtemisPluginFeatureException(this, $"Failed to deserialize default profile at {file}."); + // Ensure the profile ID is unique + ProfileDescriptor descriptor = new(this, profileEntity) {NeedsAdaption = true}; + if (_defaultProfiles.Any(d => d.Id == descriptor.Id)) + throw new ArtemisPluginFeatureException(this, $"Cannot add default profile from {file}, profile ID {descriptor.Id} already in use."); + + _defaultProfiles.Add(descriptor); + } + + /// + /// Invokes the event + /// + protected virtual void OnActiveProfileChanged() + { + ActiveProfileChanged?.Invoke(this, EventArgs.Empty); + } + internal override void InternalUpdate(double deltaTime) { StartUpdateMeasure(); @@ -245,22 +290,5 @@ namespace Artemis.Core.Modules base.Deactivate(isDeactivateOverride); Activate(isActivateOverride); } - - #region Events - - /// - /// Occurs when the has changed - /// - public event EventHandler? ActiveProfileChanged; - - /// - /// Invokes the event - /// - protected virtual void OnActiveProfileChanged() - { - ActiveProfileChanged?.Invoke(this, EventArgs.Empty); - } - - #endregion } } \ No newline at end of file diff --git a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs index a762d7e7c..57c4850a7 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PluginPrerequisite.cs @@ -23,7 +23,7 @@ namespace Artemis.Core public abstract string Description { get; } /// - /// 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 /// public abstract bool RequiresElevation { get; } diff --git a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs index 491e28f04..fcfc4db19 100644 --- a/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs +++ b/src/Artemis.Core/Plugins/Prerequisites/PrerequisiteAction/ExecuteFileAction.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -17,11 +18,13 @@ namespace Artemis.Core /// The target file to execute /// A set of command-line arguments to use when starting the application /// A boolean indicating whether the action should wait for the process to exit - public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true) : base(name) + /// A boolean indicating whether the file should run with administrator privileges (does not require ) + public ExecuteFileAction(string name, string fileName, string? arguments = null, bool waitForExit = true, bool elevate = false) : base(name) { FileName = fileName ?? throw new ArgumentNullException(nameof(fileName)); Arguments = arguments; WaitForExit = waitForExit; + Elevate = elevate; } /// @@ -30,7 +33,7 @@ namespace Artemis.Core public string FileName { get; } /// - /// Gets a set of command-line arguments to use when starting the application + /// Gets a set of command-line arguments to use when starting the application /// public string? Arguments { get; } @@ -39,6 +42,11 @@ namespace Artemis.Core /// public bool WaitForExit { get; } + /// + /// Gets a boolean indicating whether the file should run with administrator privileges + /// + public bool Elevate { get; } + /// public override async Task Execute(CancellationToken cancellationToken) { @@ -48,7 +56,7 @@ namespace Artemis.Core ShowProgressBar = true; ProgressIndeterminate = true; - int result = await RunProcessAsync(FileName, Arguments); + int result = await RunProcessAsync(FileName, Arguments, Elevate); Status = $"{FileName} exited with code {result}"; } @@ -64,13 +72,19 @@ namespace Artemis.Core } } - private static Task RunProcessAsync(string fileName, string? arguments) + private static Task RunProcessAsync(string fileName, string? arguments, bool elevate) { TaskCompletionSource tcs = new(); Process process = new() { - StartInfo = {FileName = fileName, Arguments = arguments!}, + StartInfo = + { + FileName = fileName, + Arguments = arguments!, + Verb = elevate ? "RunAs" : "", + UseShellExecute = elevate + }, EnableRaisingEvents = true }; @@ -80,7 +94,17 @@ namespace Artemis.Core process.Dispose(); }; - process.Start(); + try + { + process.Start(); + } + catch (Win32Exception e) + { + if (!elevate || e.NativeErrorCode != 0x4c7) + throw; + tcs.SetResult(-1); + } + return tcs.Task; } diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index 22660a00c..380c9e563 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -107,6 +107,11 @@ namespace Artemis.Core.Services Profile profile = new(profileDescriptor.ProfileModule, profileEntity); InstantiateProfile(profile); + if (profileDescriptor.NeedsAdaption) + { + AdaptProfile(profile); + profileDescriptor.NeedsAdaption = false; + } profileDescriptor.ProfileModule.ChangeActiveProfile(profile, _rgbService.EnabledDevices); SaveActiveProfile(profileDescriptor.ProfileModule); @@ -290,7 +295,7 @@ namespace Artemis.Core.Services profileEntity.Name = $"{profileEntity.Name} - {nameAffix}"; _profileRepository.Add(profileEntity); - return new ProfileDescriptor(profileModule, profileEntity); + return new ProfileDescriptor(profileModule, profileEntity) {NeedsAdaption = true}; } /// diff --git a/src/Artemis.UI.Shared/Services/Message/IMessageService.cs b/src/Artemis.UI.Shared/Services/Message/IMessageService.cs index 13d0f89f9..7522c7db0 100644 --- a/src/Artemis.UI.Shared/Services/Message/IMessageService.cs +++ b/src/Artemis.UI.Shared/Services/Message/IMessageService.cs @@ -17,7 +17,7 @@ namespace Artemis.UI.Shared.Services /// Sets up the notification provider that shows desktop notifications /// /// The notification provider that shows desktop notifications - void ConfigureNotificationProvider(INotificationProvider notificationProvider); + void SetNotificationProvider(INotificationProvider notificationProvider); /// /// Queues a notification message for display in a snackbar. @@ -123,7 +123,9 @@ namespace Artemis.UI.Shared.Services /// /// The title of the notification /// The message of the notification - void ShowNotification(string title, string message); + /// An optional callback that is invoked when the notification is clicked + /// An optional callback that is invoked when the notification is dismissed + void ShowNotification(string title, string message, Action? activatedCallback = null, Action? dismissedCallback = null); /// /// Shows a desktop notification with a Material Design icon @@ -131,7 +133,9 @@ namespace Artemis.UI.Shared.Services /// The title of the notification /// The message of the notification /// The name of the icon - void ShowNotification(string title, string message, PackIconKind icon); + /// An optional callback that is invoked when the notification is clicked + /// An optional callback that is invoked when the notification is dismissed + void ShowNotification(string title, string message, PackIconKind icon, Action? activatedCallback = null, Action? dismissedCallback = null); /// /// Shows a desktop notification with a Material Design icon @@ -139,6 +143,8 @@ namespace Artemis.UI.Shared.Services /// The title of the notification /// The message of the notification /// The name of the icon as a string - void ShowNotification(string title, string message, string icon); + /// An optional callback that is invoked when the notification is clicked + /// An optional callback that is invoked when the notification is dismissed + void ShowNotification(string title, string message, string icon, Action? activatedCallback = null, Action? dismissedCallback = null); } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Message/INotificationProvider.cs b/src/Artemis.UI.Shared/Services/Message/INotificationProvider.cs index 23757ef9f..6bf4fa766 100644 --- a/src/Artemis.UI.Shared/Services/Message/INotificationProvider.cs +++ b/src/Artemis.UI.Shared/Services/Message/INotificationProvider.cs @@ -1,4 +1,5 @@ -using MaterialDesignThemes.Wpf; +using System; +using MaterialDesignThemes.Wpf; namespace Artemis.UI.Shared.Services { @@ -6,7 +7,7 @@ namespace Artemis.UI.Shared.Services /// Represents a class provides desktop notifications so that can us it to show desktop /// notifications /// - public interface INotificationProvider + public interface INotificationProvider : IDisposable { /// /// Shows a notification @@ -14,6 +15,8 @@ namespace Artemis.UI.Shared.Services /// The title of the notification /// The message of the notification /// The Material Design icon to show in the notification - void ShowNotification(string title, string message, PackIconKind icon); + /// A callback that is invoked when the notification is clicked + /// A callback that is invoked when the notification is dismissed + void ShowNotification(string title, string message, PackIconKind icon, Action? activatedCallback, Action? dismissedCallback); } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Message/MessageService.cs b/src/Artemis.UI.Shared/Services/Message/MessageService.cs index b0b63d13d..d8e4e44d5 100644 --- a/src/Artemis.UI.Shared/Services/Message/MessageService.cs +++ b/src/Artemis.UI.Shared/Services/Message/MessageService.cs @@ -3,7 +3,7 @@ using MaterialDesignThemes.Wpf; namespace Artemis.UI.Shared.Services { - internal class MessageService : IMessageService + internal class MessageService : IMessageService, IDisposable { private INotificationProvider? _notificationProvider; public ISnackbarMessageQueue MainMessageQueue { get; } @@ -14,8 +14,12 @@ namespace Artemis.UI.Shared.Services } /// - public void ConfigureNotificationProvider(INotificationProvider notificationProvider) + public void SetNotificationProvider(INotificationProvider notificationProvider) { + if (ReferenceEquals(_notificationProvider, notificationProvider)) + return; + + _notificationProvider?.Dispose(); _notificationProvider = notificationProvider; } @@ -72,22 +76,32 @@ namespace Artemis.UI.Shared.Services } /// - 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); } /// - 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); } /// - 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); - _notificationProvider?.ShowNotification(title, message, (PackIconKind) (iconKind ?? PackIconKind.None)); + _notificationProvider?.ShowNotification(title, message, (PackIconKind) (iconKind ?? PackIconKind.None), activatedCallback, dismissedCallback); } + + #region IDisposable + + /// + public void Dispose() + { + _notificationProvider?.Dispose(); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI/ApplicationStateManager.cs b/src/Artemis.UI/ApplicationStateManager.cs index 3802d5494..1bc5d5c3a 100644 --- a/src/Artemis.UI/ApplicationStateManager.cs +++ b/src/Artemis.UI/ApplicationStateManager.cs @@ -118,10 +118,10 @@ namespace Artemis.UI 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() { - 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, CreateNoWindow = true, FileName = "PowerShell.exe" diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index fa098ad0c..114e65702 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -1,372 +1,374 @@  - - WinExe - net5.0-windows - {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} - true - Artemis - Artemis - en-US - Provides advanced unified lighting across many different brands RGB peripherals - Copyright © Robert Beekman - 2021 - 2.0.0.0 - bin\ - true - x64 - windows - - - x64 - + + WinExe + net5.0-windows10.0.17763.0 + {60dc8134-eba5-43b8-bcc9-bb4bc16c2548};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + true + Artemis + Artemis + en-US + Provides advanced unified lighting across many different brands RGB peripherals + Copyright © Robert Beekman - 2021 + 2.0.0.0 + bin\net5.0-windows\ + False + true + x64 + windows + + + x64 + - - Resources\Images\Logo\bow.ico - - - - 2.0.0.0 - 2.0.0 - - - 2.0-{chash:6} - true - true - true - v[0-9]* - true - git - true - - - - - - - false - - - - - - - - - - - - - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll - - - ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Layout.dll - - - - - - ResXFileCodeGenerator - Designer - Resources.Designer.cs - - - - - true - - - true - - - true - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - True - True - Resources.resx - - - True - True - Settings.settings - - - - - SettingsSingleFileGenerator - Settings.Designer.cs - - - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - Designer - - - $(DefaultXamlRuntime) - - - $(DefaultXamlRuntime) - - + + Resources\Images\Logo\bow.ico + + + + 2.0.0.0 + 2.0.0 + + + 2.0-{chash:6} + true + true + true + v[0-9]* + true + git + true + + + + + + + false + + + + + + + + + + + + + + + ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Core.dll + + + ..\..\..\RGB.NET\bin\net5.0\RGB.NET.Layout.dll + + + + + + ResXFileCodeGenerator + Designer + Resources.Designer.cs + + + + + true + + + true + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + Resources.resx + + + True + True + Settings.settings + + + + + SettingsSingleFileGenerator + Settings.Designer.cs + + + + + Designer + + + Designer + + + Designer + + + Designer + + + Designer + + + Designer + + + $(DefaultXamlRuntime) + + + $(DefaultXamlRuntime) + + \ No newline at end of file diff --git a/src/Artemis.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs index f1ba4a212..6b27e2fd1 100644 --- a/src/Artemis.UI/Bootstrapper.cs +++ b/src/Artemis.UI/Bootstrapper.cs @@ -98,7 +98,7 @@ namespace Artemis.UI }); IRegistrationService registrationService = Kernel.Get(); - registrationService.RegisterInputProvider(); + registrationService.RegisterProviders(); registrationService.RegisterControllers(); Execute.OnUIThreadSync(() => { registrationService.ApplyPreferredGraphicsContext(); }); diff --git a/src/Artemis.UI/InputProviders/NativeWindowInputProvider.cs b/src/Artemis.UI/Providers/NativeWindowInputProvider.cs similarity index 99% rename from src/Artemis.UI/InputProviders/NativeWindowInputProvider.cs rename to src/Artemis.UI/Providers/NativeWindowInputProvider.cs index c2dfd8c6c..0e513555e 100644 --- a/src/Artemis.UI/InputProviders/NativeWindowInputProvider.cs +++ b/src/Artemis.UI/Providers/NativeWindowInputProvider.cs @@ -1,11 +1,9 @@ using System; using System.Diagnostics; -using System.Linq; using System.Runtime.InteropServices; using System.Timers; using System.Windows.Forms; using System.Windows.Input; -using System.Windows.Interop; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Utilities; @@ -14,7 +12,7 @@ using Linearstar.Windows.RawInput.Native; using Serilog; using MouseButton = Artemis.Core.Services.MouseButton; -namespace Artemis.UI.InputProviders +namespace Artemis.UI.Providers { public class NativeWindowInputProvider : InputProvider { diff --git a/src/Artemis.UI/InputProviders/SpongeWindow.cs b/src/Artemis.UI/Providers/SpongeWindow.cs similarity index 93% rename from src/Artemis.UI/InputProviders/SpongeWindow.cs rename to src/Artemis.UI/Providers/SpongeWindow.cs index 34898ffcd..7940d55bd 100644 --- a/src/Artemis.UI/InputProviders/SpongeWindow.cs +++ b/src/Artemis.UI/Providers/SpongeWindow.cs @@ -1,7 +1,7 @@ using System; using System.Windows.Forms; -namespace Artemis.UI.InputProviders +namespace Artemis.UI.Providers { public sealed class SpongeWindow : NativeWindow { diff --git a/src/Artemis.UI/Providers/ToastNotificationProvider.cs b/src/Artemis.UI/Providers/ToastNotificationProvider.cs new file mode 100644 index 000000000..dea437566 --- /dev/null +++ b/src/Artemis.UI/Providers/ToastNotificationProvider.cs @@ -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 + + /// + 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> {new("image", imagePath)}); + }); + } + + #endregion + + #region IDisposable + + /// + public void Dispose() + { + ToastNotificationManagerCompat.Uninstall(); + } + + #endregion + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Home/HomeView.xaml b/src/Artemis.UI/Screens/Home/HomeView.xaml index 84bde89c3..ce8cd134b 100644 --- a/src/Artemis.UI/Screens/Home/HomeView.xaml +++ b/src/Artemis.UI/Screens/Home/HomeView.xaml @@ -11,7 +11,7 @@ mc:Ignorable="d" d:DesignHeight="574.026" d:DesignWidth="1029.87" - d:DataContext="{d:DesignInstance home:HomeViewModel, IsDesignTimeCreatable=True}"> + d:DataContext="{d:DesignInstance home:HomeViewModel}"> @@ -37,23 +37,24 @@ - - + + - + - - - - - - + + + + + + - - + + + - - - - - - - - - - - - - - - - - - - - - Open Source - - This project is completely open source. If you like it and want to say thanks you could hit the GitHub Star button, - I like numbers. You could even make plugins, there's a full documentation on the website - - - - + + + + + + + + + + + + + + + Have a chat + + If you need help, have some feedback or have any other questions feel free to contact us through any of the + following channels. + + + + + + + + + - - Feel like you want to make a donation? It would be gratefully received. Click the button to donate via PayPal. - - - - - - - + + + + + + + + + + + + + + + + + + + + + Open Source + + This project is completely open source. If you like it and want to say thanks you could hit the GitHub Star button, + I like numbers. You could even make plugins, there's a full documentation on the website + + + + + + + Feel like you want to make a donation? It would be gratefully received. Click the button to donate via PayPal. + + + + + + +