diff --git a/src/Artemis.Core/Services/Core/BuildInfo.cs b/src/Artemis.Core/Services/Core/BuildInfo.cs index 2be8b4a96..a2caaa3cf 100644 --- a/src/Artemis.Core/Services/Core/BuildInfo.cs +++ b/src/Artemis.Core/Services/Core/BuildInfo.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json; +using System.Globalization; +using Newtonsoft.Json; namespace Artemis.Core.Services.Core { @@ -10,22 +11,32 @@ namespace Artemis.Core.Services.Core /// /// Gets the unique ID of this build /// + [JsonProperty] public int BuildId { get; internal set; } /// /// Gets the build number. This contains the date and the build count for that day. - /// Per example 20210108.4 + /// Per example 20210108.4 /// + [JsonProperty] public double BuildNumber { get; internal set; } + /// + /// Gets the build number formatted as a string. This contains the date and the build count for that day. + /// Per example 20210108.4 + /// + public string BuildNumberDisplay => BuildNumber.ToString(CultureInfo.InvariantCulture); + /// /// Gets the branch of the triggering repo the build was created for. /// + [JsonProperty] public string SourceBranch { get; internal set; } = null!; /// /// Gets the commit ID used to create this build /// + [JsonProperty] public string SourceVersion { get; internal set; } = null!; } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings index 03aa1dd75..5a02b2339 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings @@ -15,5 +15,6 @@ True True True + True True True \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Interfaces/IMessageService.cs b/src/Artemis.UI.Shared/Services/Message/IMessageService.cs similarity index 79% rename from src/Artemis.UI.Shared/Services/Interfaces/IMessageService.cs rename to src/Artemis.UI.Shared/Services/Message/IMessageService.cs index 3e86ada7a..07986be1e 100644 --- a/src/Artemis.UI.Shared/Services/Interfaces/IMessageService.cs +++ b/src/Artemis.UI.Shared/Services/Message/IMessageService.cs @@ -13,6 +13,12 @@ namespace Artemis.UI.Shared.Services /// ISnackbarMessageQueue MainMessageQueue { get; } + /// + /// Sets up the notification provider that shows desktop notifications + /// + /// The notification provider that shows desktop notifications + void ConfigureNotificationProvider(INotificationProvider notificationProvider); + /// /// Queues a notification message for display in a snackbar. /// @@ -111,5 +117,28 @@ namespace Artemis.UI.Shared.Services bool promote, bool neverConsiderToBeDuplicate, TimeSpan? durationOverride = null); + + /// + /// Shows a desktop notification + /// + /// The title of the notification + /// The message of the notification + void ShowNotification(string title, string message); + + /// + /// Shows a desktop notification with a Material Design icon + /// + /// The title of the notification + /// The message of the notification + /// The name of the icon + void ShowNotification(string title, string message, PackIconKind icon); + + /// + /// Shows a desktop notification with a Material Design icon + /// + /// 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); } } \ 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 new file mode 100644 index 000000000..23757ef9f --- /dev/null +++ b/src/Artemis.UI.Shared/Services/Message/INotificationProvider.cs @@ -0,0 +1,19 @@ +using MaterialDesignThemes.Wpf; + +namespace Artemis.UI.Shared.Services +{ + /// + /// Represents a class provides desktop notifications so that can us it to show desktop + /// notifications + /// + public interface INotificationProvider + { + /// + /// Shows a notification + /// + /// 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); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/MessageService.cs b/src/Artemis.UI.Shared/Services/Message/MessageService.cs similarity index 71% rename from src/Artemis.UI.Shared/Services/MessageService.cs rename to src/Artemis.UI.Shared/Services/Message/MessageService.cs index 3a3344a8c..aef6ffc7e 100644 --- a/src/Artemis.UI.Shared/Services/MessageService.cs +++ b/src/Artemis.UI.Shared/Services/Message/MessageService.cs @@ -5,13 +5,20 @@ namespace Artemis.UI.Shared.Services { internal class MessageService : IMessageService { + private INotificationProvider _notificationProvider; public ISnackbarMessageQueue MainMessageQueue { get; } public MessageService(ISnackbarMessageQueue mainMessageQueue) { MainMessageQueue = mainMessageQueue; } - + + /// + public void ConfigureNotificationProvider(INotificationProvider notificationProvider) + { + _notificationProvider = notificationProvider; + } + public void ShowMessage(object content) { MainMessageQueue.Enqueue(content); @@ -63,5 +70,24 @@ namespace Artemis.UI.Shared.Services { MainMessageQueue.Enqueue(content, actionContent, actionHandler, actionArgument, promote, neverConsiderToBeDuplicate, durationOverride); } + + /// + public void ShowNotification(string title, string message) + { + _notificationProvider.ShowNotification(title, message, PackIconKind.None); + } + + /// + public void ShowNotification(string title, string message, PackIconKind icon) + { + _notificationProvider.ShowNotification(title, message, icon); + } + + /// + public void ShowNotification(string title, string message, string icon) + { + Enum.TryParse(typeof(PackIconKind), icon, true, out object? iconKind); + _notificationProvider.ShowNotification(title, message, (PackIconKind) (iconKind ?? PackIconKind.None)); + } } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Window/IMainWindowManager.cs b/src/Artemis.UI.Shared/Services/Window/IMainWindowProvider.cs similarity index 82% rename from src/Artemis.UI.Shared/Services/Window/IMainWindowManager.cs rename to src/Artemis.UI.Shared/Services/Window/IMainWindowProvider.cs index eea24b7fb..9df991c85 100644 --- a/src/Artemis.UI.Shared/Services/Window/IMainWindowManager.cs +++ b/src/Artemis.UI.Shared/Services/Window/IMainWindowProvider.cs @@ -1,12 +1,13 @@ using System; +using MaterialDesignThemes.Wpf; namespace Artemis.UI.Shared.Services { /// - /// Represents a class that manages a main window, used by the to control the state of + /// Represents a class that provides the main window, so that can control the state of /// the main window. /// - public interface IMainWindowManager + public interface IMainWindowProvider { /// /// Gets a boolean indicating whether the main window is currently open diff --git a/src/Artemis.UI.Shared/Services/Window/IWindowService.cs b/src/Artemis.UI.Shared/Services/Window/IWindowService.cs index ed87006a8..262a2d75a 100644 --- a/src/Artemis.UI.Shared/Services/Window/IWindowService.cs +++ b/src/Artemis.UI.Shared/Services/Window/IWindowService.cs @@ -13,10 +13,10 @@ namespace Artemis.UI.Shared.Services bool IsMainWindowOpen { get; } /// - /// Sets up the main window manager that controls the state of the main window + /// Sets up the main window provider that controls the state of the main window /// - /// The main window manager to use to control the state of the main window - void ConfigureMainWindowManager(IMainWindowManager mainWindowManager); + /// The main window provider to use to control the state of the main window + void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider); /// /// Opens the main window if it is not already open diff --git a/src/Artemis.UI.Shared/Services/Window/WindowService.cs b/src/Artemis.UI.Shared/Services/Window/WindowService.cs index 60b425089..c8b2a0ced 100644 --- a/src/Artemis.UI.Shared/Services/Window/WindowService.cs +++ b/src/Artemis.UI.Shared/Services/Window/WindowService.cs @@ -4,7 +4,7 @@ namespace Artemis.UI.Shared.Services { internal class WindowService : IWindowService { - private IMainWindowManager? _mainWindowManager; + private IMainWindowProvider? _mainWindowManager; protected virtual void OnMainWindowOpened() { @@ -46,9 +46,9 @@ namespace Artemis.UI.Shared.Services public bool IsMainWindowOpen { get; private set; } - public void ConfigureMainWindowManager(IMainWindowManager mainWindowManager) + public void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider) { - if (mainWindowManager == null) throw new ArgumentNullException(nameof(mainWindowManager)); + if (mainWindowProvider == null) throw new ArgumentNullException(nameof(mainWindowProvider)); if (_mainWindowManager != null) { @@ -56,7 +56,7 @@ namespace Artemis.UI.Shared.Services _mainWindowManager.MainWindowClosed -= HandleMainWindowClosed; } - _mainWindowManager = mainWindowManager; + _mainWindowManager = mainWindowProvider; _mainWindowManager.MainWindowOpened += HandleMainWindowOpened; _mainWindowManager.MainWindowClosed += HandleMainWindowClosed; diff --git a/src/Artemis.UI/Bootstrapper.cs b/src/Artemis.UI/Bootstrapper.cs index 6d2049dfc..51af5da78 100644 --- a/src/Artemis.UI/Bootstrapper.cs +++ b/src/Artemis.UI/Bootstrapper.cs @@ -16,6 +16,7 @@ using Artemis.UI.Shared.Services; using Artemis.UI.Stylet; using Ninject; using Serilog; +using SharpVectors.Dom.Resources; using Stylet; namespace Artemis.UI @@ -58,7 +59,11 @@ namespace Artemis.UI FrameworkElement.LanguageProperty.OverrideMetadata(typeof(FrameworkElement), new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag))); // Create and bind the root view, this is a tray icon so don't show it with the window manager - Execute.OnUIThread(() => viewManager.CreateAndBindViewForModelIfNecessary(RootViewModel)); + Execute.OnUIThread(() => + { + UIElement view = viewManager.CreateAndBindViewForModelIfNecessary(RootViewModel); + ((TrayViewModel) RootViewModel).SetTaskbarIcon(view); + }); // Initialize the core async so the UI can show the progress Task.Run(async () => diff --git a/src/Artemis.UI/Screens/RootViewModel.cs b/src/Artemis.UI/Screens/RootViewModel.cs index d2b5db909..35c55db05 100644 --- a/src/Artemis.UI/Screens/RootViewModel.cs +++ b/src/Artemis.UI/Screens/RootViewModel.cs @@ -1,5 +1,6 @@ using System; using System.ComponentModel; +using System.Globalization; using System.Reflection; using System.Threading.Tasks; using System.Timers; @@ -80,7 +81,7 @@ namespace Artemis.UI.Screens PinSidebar = _settingsService.GetSetting("UI.PinSidebar", false); AssemblyInformationalVersionAttribute versionAttribute = typeof(RootViewModel).Assembly.GetCustomAttribute(); - WindowTitle = $"Artemis {versionAttribute?.InformationalVersion} build {Constants.BuildInfo.BuildNumber}"; + WindowTitle = $"Artemis {versionAttribute?.InformationalVersion} build {Constants.BuildInfo.BuildNumberDisplay}"; } public PluginSetting PinSidebar { get; } diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs index d076f56e3..3b852d910 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs @@ -279,8 +279,6 @@ namespace Artemis.UI.Screens.Settings.Tabs.General bool updateFound = await _updateService.OfferUpdatesIfFound(); if (!updateFound) _messageService.ShowMessage("You are already running the latest Artemis build. (☞゚ヮ゚)☞"); - else - _messageService.ShowMessage("You are already running the latest Artemis build. (☞゚ヮ゚)☞"); CanOfferUpdatesIfFound = true; } diff --git a/src/Artemis.UI/Screens/TrayView.xaml b/src/Artemis.UI/Screens/TrayView.xaml index 5cf870f5a..afac1218c 100644 --- a/src/Artemis.UI/Screens/TrayView.xaml +++ b/src/Artemis.UI/Screens/TrayView.xaml @@ -17,7 +17,8 @@ MenuActivation="LeftOrRightClick" PopupActivation="DoubleClick" ToolTipText="Artemis" - DoubleClickCommand="{s:Action TrayBringToForeground}"> + DoubleClickCommand="{s:Action TrayBringToForeground}" + TrayBalloonTipClicked="{s:Action OnTrayBalloonTipClicked}"> diff --git a/src/Artemis.UI/Screens/TrayViewModel.cs b/src/Artemis.UI/Screens/TrayViewModel.cs index a513caa6f..65a409439 100644 --- a/src/Artemis.UI/Screens/TrayViewModel.cs +++ b/src/Artemis.UI/Screens/TrayViewModel.cs @@ -1,31 +1,42 @@ using System; +using System.Drawing; +using System.IO; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Media; +using System.Windows.Media.Imaging; using Artemis.Core.Services; using Artemis.UI.Events; using Artemis.UI.Screens.Splash; using Artemis.UI.Services; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared.Services; +using Hardcodet.Wpf.TaskbarNotification; +using MaterialDesignThemes.Wpf; using Ninject; using Stylet; +using Icon = System.Drawing.Icon; namespace Artemis.UI.Screens { - public class TrayViewModel : Screen, IMainWindowManager + public class TrayViewModel : Screen, IMainWindowProvider, INotificationProvider { private readonly IDebugService _debugService; private readonly IEventAggregator _eventAggregator; private readonly IKernel _kernel; private readonly IWindowManager _windowManager; private bool _canShowRootViewModel; - private SplashViewModel _splashViewModel; private RootViewModel _rootViewModel; + private SplashViewModel _splashViewModel; + private TaskbarIcon _taskBarIcon; - public TrayViewModel(IKernel kernel, - IWindowManager windowManager, + public TrayViewModel(IKernel kernel, + IWindowManager windowManager, IWindowService windowService, + IMessageService messageService, IUpdateService updateService, - IEventAggregator eventAggregator, - ICoreService coreService, + IEventAggregator eventAggregator, + ICoreService coreService, IDebugService debugService, ISettingsService settingsService) { @@ -35,7 +46,8 @@ namespace Artemis.UI.Screens _debugService = debugService; CanShowRootViewModel = true; - windowService.ConfigureMainWindowManager(this); + windowService.ConfigureMainWindowProvider(this); + messageService.ConfigureNotificationProvider(this); bool autoRunning = Bootstrapper.StartupArguments.Contains("--autorun"); bool showOnAutoRun = settingsService.GetSetting("UI.ShowOnStartup", true).Value; if (!autoRunning || showOnAutoRun) @@ -43,8 +55,8 @@ namespace Artemis.UI.Screens ShowSplashScreen(); coreService.Initialized += (_, _) => TrayBringToForeground(); } - - updateService.AutoUpdate(); + else + updateService.AutoUpdate(); } public bool CanShowRootViewModel @@ -91,6 +103,28 @@ namespace Artemis.UI.Screens _debugService.ShowDebugger(); } + public void SetTaskbarIcon(UIElement view) + { + _taskBarIcon = (TaskbarIcon) ((ContentControl) view).Content; + } + + public void OnTrayBalloonTipClicked(object sender, EventArgs e) + { + if (CanShowRootViewModel) + TrayBringToForeground(); + else + { + // Wrestle the main window to the front + Window mainWindow = ((Window) _rootViewModel.View); + if (mainWindow.WindowState == WindowState.Minimized) + mainWindow.WindowState = WindowState.Normal; + mainWindow.Activate(); + mainWindow.Topmost = true; + mainWindow.Topmost = false; + mainWindow.Focus(); + } + } + private void ShowSplashScreen() { Execute.OnUIThread(() => @@ -109,12 +143,47 @@ namespace Artemis.UI.Screens OnMainWindowClosed(); } - #region Implementation of IMainWindowManager + #region Implementation of INotificationProvider /// + 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 + public bool IsMainWindowOpen { get; private set; } - /// public bool OpenMainWindow() { if (CanShowRootViewModel) @@ -124,17 +193,14 @@ namespace Artemis.UI.Screens return true; } - /// public bool CloseMainWindow() { _rootViewModel.RequestClose(); return _rootViewModel.ScreenState == ScreenState.Closed; } - /// public event EventHandler MainWindowOpened; - /// public event EventHandler MainWindowClosed; protected virtual void OnMainWindowOpened() diff --git a/src/Artemis.UI/Services/UpdateService.cs b/src/Artemis.UI/Services/UpdateService.cs index d29633ec8..fbabfaa8f 100644 --- a/src/Artemis.UI/Services/UpdateService.cs +++ b/src/Artemis.UI/Services/UpdateService.cs @@ -1,10 +1,12 @@ using System; +using System.Globalization; using System.Net.Http; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared.Services; +using MaterialDesignThemes.Wpf; using Newtonsoft.Json.Linq; using Serilog; @@ -17,12 +19,14 @@ namespace Artemis.UI.Services private readonly PluginSetting _checkForUpdates; private readonly ILogger _logger; private readonly IDialogService _dialogService; + private readonly IMessageService _messageService; private readonly IWindowService _windowService; - public UpdateService(ILogger logger, ISettingsService settingsService, IDialogService dialogService, IWindowService windowService) + public UpdateService(ILogger logger, ISettingsService settingsService, IDialogService dialogService, IMessageService messageService, IWindowService windowService) { _logger = logger; _dialogService = dialogService; + _messageService = messageService; _windowService = windowService; _windowService.MainWindowOpened += WindowServiceOnMainWindowOpened; @@ -41,25 +45,24 @@ namespace Artemis.UI.Services // Make the request using HttpClient client = new(); HttpResponseMessage httpResponseMessage = await client.GetAsync(latestBuildUrl); - + // Ensure it returned correctly if (!httpResponseMessage.IsSuccessStatusCode) { _logger.Warning("Failed to check for updates, request returned {statusCode}", httpResponseMessage.StatusCode); return 0; } - + // Parse the response string response = await httpResponseMessage.Content.ReadAsStringAsync(); try { JToken buildNumberToken = JObject.Parse(response).SelectToken("value[0].buildNumber"); - if (buildNumberToken != null) + if (buildNumberToken != null) return buildNumberToken.Value(); - + _logger.Warning("Failed to find build number at \"value[0].buildNumber\""); return 0; - } catch (Exception e) { @@ -73,20 +76,34 @@ namespace Artemis.UI.Services _logger.Information("Checking for updates"); double buildNumber = await GetLatestBuildNumber(); - _logger.Information("Latest build is {buildNumber}, we're running {localBuildNumber}", buildNumber, Constants.BuildInfo.BuildNumber); + string buildNumberDisplay = buildNumber.ToString(CultureInfo.InvariantCulture); + _logger.Information("Latest build is {buildNumber}, we're running {localBuildNumber}", buildNumberDisplay, Constants.BuildInfo.BuildNumberDisplay); if (buildNumber < Constants.BuildInfo.BuildNumber) return false; if (_windowService.IsMainWindowOpen) { - + } + else if (_autoInstallUpdates.Value) + { + // Lets go + _messageService.ShowNotification( + "Installing new version", + $"Build {buildNumberDisplay} is available, currently on {Constants.BuildInfo.BuildNumberDisplay}.", + PackIconKind.Update + ); } else { - + // If auto-install is disabled and the window is closed, best we can do is notify the user and stop. + _messageService.ShowNotification( + "New version available", + $"Build {buildNumberDisplay} is available, currently on {Constants.BuildInfo.BuildNumberDisplay}.", + PackIconKind.Update + ); } - + return true; } diff --git a/src/Artemis.UI/buildinfo.json b/src/Artemis.UI/buildinfo.json index e5db2c3c5..82cb298d9 100644 --- a/src/Artemis.UI/buildinfo.json +++ b/src/Artemis.UI/buildinfo.json @@ -1,6 +1,6 @@ { "BuildId": 0, - "BuildNumber": 0, + "BuildNumber": 13370101.1, "SourceBranch": "local", "SourceVersion": "local" } \ No newline at end of file