diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index 61a2754e2..408ce0d50 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.IO; using Artemis.Core.JsonConverters; -using Artemis.Storage.Entities.Plugins; +using Artemis.Core.Services.Core; using Newtonsoft.Json; namespace Artemis.Core @@ -40,6 +40,14 @@ namespace Artemis.Core Guid = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), Name = "Artemis Core", Version = new Version(2, 0) }; + /// + /// The build information related to the currently running Artemis build + /// Information is retrieved from buildinfo.json + /// + public static readonly BuildInfo BuildInfo = File.Exists(Path.Combine(ApplicationFolder, "buildinfo.json")) + ? JsonConvert.DeserializeObject(File.ReadAllText(Path.Combine(ApplicationFolder, "buildinfo.json"))) + : new BuildInfo(); + /// /// The plugin used by core components of Artemis /// @@ -52,10 +60,11 @@ namespace Artemis.Core { Converters = new List {new SKColorConverter(), new ForgivingIntConverter()} }; + internal static JsonSerializerSettings JsonConvertTypedSettings = new() { TypeNameHandling = TypeNameHandling.All, - Converters = new List { new SKColorConverter(), new ForgivingIntConverter() } + Converters = new List {new SKColorConverter(), new ForgivingIntConverter()} }; /// diff --git a/src/Artemis.Core/Services/Core/BuildInfo.cs b/src/Artemis.Core/Services/Core/BuildInfo.cs new file mode 100644 index 000000000..2be8b4a96 --- /dev/null +++ b/src/Artemis.Core/Services/Core/BuildInfo.cs @@ -0,0 +1,31 @@ +using Newtonsoft.Json; + +namespace Artemis.Core.Services.Core +{ + /// + /// Represents build information related to the currently running Artemis build + /// + public class BuildInfo + { + /// + /// Gets the unique ID of this build + /// + 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 + /// + public double BuildNumber { get; internal set; } + + /// + /// Gets the branch of the triggering repo the build was created for. + /// + public string SourceBranch { get; internal set; } = null!; + + /// + /// Gets the commit ID used to create this build + /// + public string SourceVersion { get; internal set; } = null!; + } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index 3bbe992b6..55674dd4c 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -1,13 +1,16 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Artemis.Core.DataModelExpansions; using Artemis.Core.Ninject; +using Artemis.Core.Services.Core; using Artemis.Storage; using HidSharp; +using Newtonsoft.Json; using Ninject; using RGB.NET.Core; using Serilog; @@ -37,9 +40,17 @@ namespace Artemis.Core.Services private DateTime _lastExceptionLog; private List _modules = new(); - // ReSharper disable UnusedParameter.Local - Storage migration and module service are injected early to ensure it runs before anything else - public CoreService(IKernel kernel, ILogger logger, StorageMigrationService _, ISettingsService settingsService, IPluginManagementService pluginManagementService, - IRgbService rgbService, ISurfaceService surfaceService, IProfileService profileService, IModuleService moduleService) + // ReSharper disable UnusedParameter.Local + public CoreService(IKernel kernel, + ILogger logger, + StorageMigrationService _, // injected to ensure migration runs early + ISettingsService settingsService, + IPluginManagementService pluginManagementService, + IRgbService rgbService, + ISurfaceService surfaceService, + IProfileService profileService, + IModuleService moduleService // injected to ensure module priorities get applied + ) { Kernel = kernel; Constants.CorePlugin.Kernel = kernel; @@ -82,7 +93,8 @@ namespace Artemis.Core.Services throw new ArtemisCoreException("Cannot initialize the core as it is already initialized."); AssemblyInformationalVersionAttribute? versionAttribute = typeof(CoreService).Assembly.GetCustomAttribute(); - _logger.Information("Initializing Artemis Core version {version}", versionAttribute?.InformationalVersion); + _logger.Information("Initializing Artemis Core version {version}, build {buildNumber} branch {branch}.", versionAttribute?.InformationalVersion, Constants.BuildInfo.BuildNumber, + Constants.BuildInfo.SourceBranch); // This should prevent a certain someone from removing HidSharp as an unused dependency as well _logger.Information("Forcing plugins to use HidSharp {hidSharpVersion}", Assembly.GetAssembly(typeof(HidDevice))!.GetName().Version); diff --git a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings index cd544f926..03aa1dd75 100644 --- a/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings +++ b/src/Artemis.UI.Shared/Artemis.UI.Shared.csproj.DotSettings @@ -15,4 +15,5 @@ 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/Interfaces/IMessageService.cs new file mode 100644 index 000000000..3e86ada7a --- /dev/null +++ b/src/Artemis.UI.Shared/Services/Interfaces/IMessageService.cs @@ -0,0 +1,115 @@ +using System; +using MaterialDesignThemes.Wpf; + +namespace Artemis.UI.Shared.Services +{ + /// + /// Providers messaging functionality + /// + public interface IMessageService : IArtemisSharedUIService + { + /// + /// Gets the main snackbar message queue used by and its overloads + /// + ISnackbarMessageQueue MainMessageQueue { get; } + + /// + /// Queues a notification message for display in a snackbar. + /// + /// Message. + void ShowMessage(object content); + + /// + /// Queues a notification message for display in a snackbar. + /// + /// Message. + /// Content for the action button. + /// Call back to be executed if user clicks the action button. + void ShowMessage(object content, object actionContent, Action actionHandler); + + /// + /// Queues a notification message for display in a snackbar. + /// + /// Message. + /// Content for the action button. + /// Call back to be executed if user clicks the action button. + /// Argument to pass to . + void ShowMessage( + object content, + object actionContent, + Action actionHandler, + TArgument actionArgument); + + /// + /// Queues a notification message for display in a snackbar. + /// + /// Message. + /// + /// Subsequent, duplicate messages queued within a short time span will + /// be discarded. To override this behaviour and ensure the message always gets displayed set to true. + /// + void ShowMessage(object content, bool neverConsiderToBeDuplicate); + + /// + /// Queues a notification message for display in a snackbar. + /// + /// Message. + /// Content for the action button. + /// Call back to be executed if user clicks the action button. + /// The message will promoted to the front of the queue. + void ShowMessage(object content, object actionContent, Action actionHandler, bool promote); + + /// + /// Queues a notification message for display in a snackbar. + /// + /// Message. + /// Content for the action button. + /// Call back to be executed if user clicks the action button. + /// Argument to pass to . + /// The message will be promoted to the front of the queue and never considered to be a duplicate. + void ShowMessage( + object content, + object actionContent, + Action actionHandler, + TArgument actionArgument, + bool promote); + + /// + /// Queues a notification message for display in a snackbar. + /// + /// Message. + /// Content for the action button. + /// Call back to be executed if user clicks the action button. + /// Argument to pass to . + /// The message will be promoted to the front of the queue. + /// The message will never be considered a duplicate. + /// Message show duration override. + void ShowMessage( + object content, + object actionContent, + Action actionHandler, + TArgument actionArgument, + bool promote, + bool neverConsiderToBeDuplicate, + TimeSpan? durationOverride = null); + + /// + /// Queues a notification message for display in a snackbar. + /// + /// Message. + /// Content for the action button. + /// Call back to be executed if user clicks the action button. + /// Argument to pass to . + /// The message will promoted to the front of the queue. + /// The message will never be considered a duplicate. + /// Message show duration override. + void ShowMessage( + object content, + object actionContent, + Action actionHandler, + object actionArgument, + bool promote, + bool neverConsiderToBeDuplicate, + TimeSpan? durationOverride = null); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/MessageService.cs b/src/Artemis.UI.Shared/Services/MessageService.cs new file mode 100644 index 000000000..3a3344a8c --- /dev/null +++ b/src/Artemis.UI.Shared/Services/MessageService.cs @@ -0,0 +1,67 @@ +using System; +using MaterialDesignThemes.Wpf; + +namespace Artemis.UI.Shared.Services +{ + internal class MessageService : IMessageService + { + public ISnackbarMessageQueue MainMessageQueue { get; } + + public MessageService(ISnackbarMessageQueue mainMessageQueue) + { + MainMessageQueue = mainMessageQueue; + } + + public void ShowMessage(object content) + { + MainMessageQueue.Enqueue(content); + } + + public void ShowMessage(object content, object actionContent, Action actionHandler) + { + MainMessageQueue.Enqueue(content, actionContent, actionHandler); + } + + public void ShowMessage(object content, object actionContent, Action actionHandler, TArgument actionArgument) + { + MainMessageQueue.Enqueue(content, actionContent, actionHandler, actionArgument); + } + + public void ShowMessage(object content, bool neverConsiderToBeDuplicate) + { + MainMessageQueue.Enqueue(content, neverConsiderToBeDuplicate); + } + + public void ShowMessage(object content, object actionContent, Action actionHandler, bool promote) + { + MainMessageQueue.Enqueue(content, actionContent, actionHandler, promote); + } + + public void ShowMessage(object content, object actionContent, Action actionHandler, TArgument actionArgument, bool promote) + { + MainMessageQueue.Enqueue(content, actionContent, actionHandler, actionArgument, promote); + } + + public void ShowMessage(object content, + object actionContent, + Action actionHandler, + TArgument actionArgument, + bool promote, + bool neverConsiderToBeDuplicate, + TimeSpan? durationOverride = null) + { + MainMessageQueue.Enqueue(content, actionContent, actionHandler, actionArgument, promote, neverConsiderToBeDuplicate, durationOverride); + } + + public void ShowMessage(object content, + object actionContent, + Action actionHandler, + object actionArgument, + bool promote, + bool neverConsiderToBeDuplicate, + TimeSpan? durationOverride = null) + { + MainMessageQueue.Enqueue(content, actionContent, actionHandler, actionArgument, promote, neverConsiderToBeDuplicate, durationOverride); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Window/IMainWindowManager.cs b/src/Artemis.UI.Shared/Services/Window/IMainWindowManager.cs new file mode 100644 index 000000000..eea24b7fb --- /dev/null +++ b/src/Artemis.UI.Shared/Services/Window/IMainWindowManager.cs @@ -0,0 +1,38 @@ +using System; + +namespace Artemis.UI.Shared.Services +{ + /// + /// Represents a class that manages a main window, used by the to control the state of + /// the main window. + /// + public interface IMainWindowManager + { + /// + /// Gets a boolean indicating whether the main window is currently open + /// + bool IsMainWindowOpen { get; } + + /// + /// Opens the main window + /// + /// + bool OpenMainWindow(); + + /// + /// Closes the main window + /// + /// + bool CloseMainWindow(); + + /// + /// Occurs when the main window has been opened + /// + public event EventHandler? MainWindowOpened; + + /// + /// Occurs when the main window has been closed + /// + public event EventHandler? MainWindowClosed; + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Window/IWindowService.cs b/src/Artemis.UI.Shared/Services/Window/IWindowService.cs new file mode 100644 index 000000000..ed87006a8 --- /dev/null +++ b/src/Artemis.UI.Shared/Services/Window/IWindowService.cs @@ -0,0 +1,41 @@ +using System; + +namespace Artemis.UI.Shared.Services +{ + /// + /// A service that allows you to view, monitor and manage the open/close state of the main window + /// + public interface IWindowService : IArtemisSharedUIService + { + /// + /// Gets a boolean indicating whether the main window is currently open + /// + bool IsMainWindowOpen { get; } + + /// + /// Sets up the main window manager 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); + + /// + /// Opens the main window if it is not already open + /// + void OpenMainWindow(); + + /// + /// Closes the main window if it is not already closed + /// + void CloseMainWindow(); + + /// + /// Occurs when the main window has been opened + /// + public event EventHandler? MainWindowOpened; + + /// + /// Occurs when the main window has been closed + /// + public event EventHandler? MainWindowClosed; + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Window/WindowService.cs b/src/Artemis.UI.Shared/Services/Window/WindowService.cs new file mode 100644 index 000000000..60b425089 --- /dev/null +++ b/src/Artemis.UI.Shared/Services/Window/WindowService.cs @@ -0,0 +1,82 @@ +using System; + +namespace Artemis.UI.Shared.Services +{ + internal class WindowService : IWindowService + { + private IMainWindowManager? _mainWindowManager; + + protected virtual void OnMainWindowOpened() + { + MainWindowOpened?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnMainWindowClosed() + { + MainWindowClosed?.Invoke(this, EventArgs.Empty); + } + + private void SyncWithManager() + { + if (_mainWindowManager == null) + return; + + if (IsMainWindowOpen && !_mainWindowManager.IsMainWindowOpen) + { + IsMainWindowOpen = false; + OnMainWindowClosed(); + } + + if (!IsMainWindowOpen && _mainWindowManager.IsMainWindowOpen) + { + IsMainWindowOpen = true; + OnMainWindowOpened(); + } + } + + private void HandleMainWindowOpened(object? sender, EventArgs e) + { + SyncWithManager(); + } + + private void HandleMainWindowClosed(object? sender, EventArgs e) + { + SyncWithManager(); + } + + public bool IsMainWindowOpen { get; private set; } + + public void ConfigureMainWindowManager(IMainWindowManager mainWindowManager) + { + if (mainWindowManager == null) throw new ArgumentNullException(nameof(mainWindowManager)); + + if (_mainWindowManager != null) + { + _mainWindowManager.MainWindowOpened -= HandleMainWindowOpened; + _mainWindowManager.MainWindowClosed -= HandleMainWindowClosed; + } + + _mainWindowManager = mainWindowManager; + _mainWindowManager.MainWindowOpened += HandleMainWindowOpened; + _mainWindowManager.MainWindowClosed += HandleMainWindowClosed; + + // Sync up with the new manager's state + SyncWithManager(); + } + + public void OpenMainWindow() + { + IsMainWindowOpen = true; + OnMainWindowOpened(); + } + + public void CloseMainWindow() + { + IsMainWindowOpen = false; + OnMainWindowClosed(); + } + + public event EventHandler? MainWindowOpened; + public event EventHandler? MainWindowClosed; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileExportViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileExportViewModel.cs index a85b85046..7ab085f39 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileExportViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileExportViewModel.cs @@ -8,16 +8,15 @@ namespace Artemis.UI.Screens.ProfileEditor.Dialogs { public class ProfileExportViewModel : DialogViewModelBase { - private readonly ISnackbarMessageQueue _mainMessageQueue; - private readonly IProfileService _profileService; + private readonly IMessageService _messageService; - public ProfileExportViewModel(ProfileDescriptor profileDescriptor, IProfileService profileService, ISnackbarMessageQueue mainMessageQueue) + public ProfileExportViewModel(ProfileDescriptor profileDescriptor, IProfileService profileService, IMessageService messageService) { ProfileDescriptor = profileDescriptor; _profileService = profileService; - _mainMessageQueue = mainMessageQueue; + _messageService = messageService; } public ProfileDescriptor ProfileDescriptor { get; } @@ -26,7 +25,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Dialogs { string encoded = _profileService.ExportProfile(ProfileDescriptor); Clipboard.SetText(encoded); - _mainMessageQueue.Enqueue("Profile contents exported to clipboard."); + _messageService.ShowMessage("Profile contents exported to clipboard."); Session.Close(); } diff --git a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportViewModel.cs index cab90345a..ef89cef72 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Dialogs/ProfileImportViewModel.cs @@ -11,15 +11,16 @@ namespace Artemis.UI.Screens.ProfileEditor.Dialogs { private readonly ISnackbarMessageQueue _mainMessageQueue; private readonly IProfileService _profileService; + private readonly IMessageService _messageService; private string _profileJson; - public ProfileImportViewModel(ProfileModule profileModule, IProfileService profileService, ISnackbarMessageQueue mainMessageQueue) + public ProfileImportViewModel(ProfileModule profileModule, IProfileService profileService, IMessageService messageService) { ProfileModule = profileModule; Document = new TextDocument(); _profileService = profileService; - _mainMessageQueue = mainMessageQueue; + _messageService = messageService; } public ProfileModule ProfileModule { get; } @@ -34,7 +35,7 @@ namespace Artemis.UI.Screens.ProfileEditor.Dialogs public void Accept() { ProfileDescriptor descriptor = _profileService.ImportProfile(Document.Text, ProfileModule); - _mainMessageQueue.Enqueue("Profile imported."); + _messageService.ShowMessage("Profile imported."); Session.Close(descriptor); } } diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index d5d43ed53..23a9176b4 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -22,10 +22,10 @@ namespace Artemis.UI.Screens.ProfileEditor public class ProfileEditorViewModel : Screen { private readonly IModuleService _moduleService; + private readonly IMessageService _messageService; private readonly IProfileEditorService _profileEditorService; private readonly IProfileService _profileService; private readonly ISettingsService _settingsService; - private readonly ISnackbarMessageQueue _snackbarMessageQueue; private PluginSetting _bottomPanelsHeight; private PluginSetting _dataModelConditionsHeight; private DisplayConditionsViewModel _displayConditionsViewModel; @@ -47,13 +47,13 @@ namespace Artemis.UI.Screens.ProfileEditor IDialogService dialogService, ISettingsService settingsService, IModuleService moduleService, - ISnackbarMessageQueue snackbarMessageQueue) + IMessageService messageService) { _profileEditorService = profileEditorService; _profileService = profileService; _settingsService = settingsService; _moduleService = moduleService; - _snackbarMessageQueue = snackbarMessageQueue; + _messageService = messageService; DisplayName = "PROFILE EDITOR"; Module = module; @@ -242,7 +242,7 @@ namespace Artemis.UI.Screens.ProfileEditor if (!_profileEditorService.UndoUpdateProfile()) { - _snackbarMessageQueue.Enqueue("Nothing to undo"); + _messageService.ShowMessage("Nothing to undo"); return; } @@ -256,7 +256,7 @@ namespace Artemis.UI.Screens.ProfileEditor focusedElement?.Focus(); }); - _snackbarMessageQueue.Enqueue("Undid profile update", "REDO", Redo); + _messageService.ShowMessage("Undid profile update", "REDO", Redo); } public void Redo() @@ -269,7 +269,7 @@ namespace Artemis.UI.Screens.ProfileEditor if (!_profileEditorService.RedoUpdateProfile()) { - _snackbarMessageQueue.Enqueue("Nothing to redo"); + _messageService.ShowMessage("Nothing to redo"); return; } @@ -283,7 +283,7 @@ namespace Artemis.UI.Screens.ProfileEditor focusedElement?.Focus(); }); - _snackbarMessageQueue.Enqueue("Redid profile update", "UNDO", Undo); + _messageService.ShowMessage("Redid profile update", "UNDO", Undo); } protected override void OnInitialActivate() diff --git a/src/Artemis.UI/Screens/RootViewModel.cs b/src/Artemis.UI/Screens/RootViewModel.cs index 05b57b22c..d2b5db909 100644 --- a/src/Artemis.UI/Screens/RootViewModel.cs +++ b/src/Artemis.UI/Screens/RootViewModel.cs @@ -14,6 +14,7 @@ using Artemis.UI.Screens.Sidebar; using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Services; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared.Services; using Artemis.UI.Utilities; using MaterialDesignExtensions.Controls; using MaterialDesignThemes.Wpf; @@ -25,6 +26,7 @@ namespace Artemis.UI.Screens public sealed class RootViewModel : Conductor, IDisposable { private readonly IRegistrationService _builtInRegistrationService; + private readonly IMessageService _messageService; private readonly PluginSetting _colorScheme; private readonly ICoreService _coreService; private readonly IWindowManager _windowManager; @@ -34,7 +36,6 @@ namespace Artemis.UI.Screens private readonly ISettingsService _settingsService; private readonly Timer _frameTimeUpdateTimer; private readonly SidebarViewModel _sidebarViewModel; - private readonly ISnackbarMessageQueue _snackbarMessageQueue; private readonly ThemeWatcher _themeWatcher; private readonly PluginSetting _windowSize; private bool _activeItemReady; @@ -52,7 +53,7 @@ namespace Artemis.UI.Screens IWindowManager windowManager, IDebugService debugService, IRegistrationService builtInRegistrationService, - ISnackbarMessageQueue snackbarMessageQueue, + IMessageService messageService, SidebarViewModel sidebarViewModel) { _kernel = kernel; @@ -62,7 +63,7 @@ namespace Artemis.UI.Screens _windowManager = windowManager; _debugService = debugService; _builtInRegistrationService = builtInRegistrationService; - _snackbarMessageQueue = snackbarMessageQueue; + _messageService = messageService; _sidebarViewModel = sidebarViewModel; _frameTimeUpdateTimer = new Timer(500); @@ -79,7 +80,7 @@ namespace Artemis.UI.Screens PinSidebar = _settingsService.GetSetting("UI.PinSidebar", false); AssemblyInformationalVersionAttribute versionAttribute = typeof(RootViewModel).Assembly.GetCustomAttribute(); - WindowTitle = $"Artemis {versionAttribute?.InformationalVersion}"; + WindowTitle = $"Artemis {versionAttribute?.InformationalVersion} build {Constants.BuildInfo.BuildNumber}"; } public PluginSetting PinSidebar { get; } @@ -275,7 +276,7 @@ namespace Artemis.UI.Screens protected override void OnInitialActivate() { - MainMessageQueue = _snackbarMessageQueue; + MainMessageQueue = _messageService.MainMessageQueue; UpdateFrameTime(); _builtInRegistrationService.RegisterBuiltInDataModelDisplays(); @@ -295,7 +296,7 @@ namespace Artemis.UI.Screens PluginSetting setupWizardCompleted = _settingsService.GetSetting("UI.SetupWizardCompleted", false); if (!setupWizardCompleted.Value) ShowSetupWizard(); - + base.OnInitialActivate(); } diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml index e02dbda31..ac964893d 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabView.xaml @@ -195,6 +195,76 @@ + + Updating + + + + + + + + + + + + + Check for updates + + If enabled, we'll check for updates on startup and periodically while running. + + + + + + + + + + + + + + + + + + + Automatically install updates + + If enabled updates are installed automatically without asking first. + + + + + + + + + + + + + + + + + + + Update + + Use the button on the right to check for updates now. + + + + + + + + + Profile editor diff --git a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs index 02bf14207..d076f56e3 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/General/GeneralSettingsTabViewModel.cs @@ -8,9 +8,11 @@ using Artemis.Core; using Artemis.Core.LayerBrushes; using Artemis.Core.Services; using Artemis.UI.Screens.StartupWizard; +using Artemis.UI.Services; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; +using MaterialDesignThemes.Wpf; using Ninject; using Serilog.Events; using Stylet; @@ -24,18 +26,23 @@ namespace Artemis.UI.Screens.Settings.Tabs.General private readonly IWindowManager _windowManager; private readonly IDialogService _dialogService; private readonly ISettingsService _settingsService; + private readonly IUpdateService _updateService; + private readonly IMessageService _messageService; private List> _renderScales; private List _sampleSizes; private List> _targetFrameRates; private readonly PluginSetting _defaultLayerBrushDescriptor; + private bool _canOfferUpdatesIfFound = true; public GeneralSettingsTabViewModel( - IKernel kernel, - IWindowManager windowManager, - IDialogService dialogService, + IKernel kernel, + IWindowManager windowManager, + IDialogService dialogService, IDebugService debugService, - ISettingsService settingsService, - IPluginManagementService pluginManagementService) + ISettingsService settingsService, + IUpdateService updateService, + IPluginManagementService pluginManagementService, + IMessageService messageService) { DisplayName = "GENERAL"; @@ -44,6 +51,8 @@ namespace Artemis.UI.Screens.Settings.Tabs.General _dialogService = dialogService; _debugService = debugService; _settingsService = settingsService; + _updateService = updateService; + _messageService = messageService; LogLevels = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(LogEventLevel))); ColorSchemes = new BindableCollection(EnumUtilities.GetAllValuesAndDescriptions(typeof(ApplicationColorScheme))); @@ -124,6 +133,31 @@ namespace Artemis.UI.Screens.Settings.Tabs.General } } + public bool CheckForUpdates + { + get => _settingsService.GetSetting("UI.CheckForUpdates", true).Value; + set + { + _settingsService.GetSetting("UI.CheckForUpdates", true).Value = value; + _settingsService.GetSetting("UI.CheckForUpdates", true).Save(); + NotifyOfPropertyChange(nameof(CheckForUpdates)); + + if (!value) + AutoInstallUpdates = false; + } + } + + public bool AutoInstallUpdates + { + get => _settingsService.GetSetting("UI.AutoInstallUpdates", false).Value; + set + { + _settingsService.GetSetting("UI.AutoInstallUpdates", false).Value = value; + _settingsService.GetSetting("UI.AutoInstallUpdates", false).Save(); + NotifyOfPropertyChange(nameof(AutoInstallUpdates)); + } + } + public bool ShowDataModelValues { get => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false).Value; @@ -196,6 +230,12 @@ namespace Artemis.UI.Screens.Settings.Tabs.General } } + public bool CanOfferUpdatesIfFound + { + get => _canOfferUpdatesIfFound; + set => SetAndNotify(ref _canOfferUpdatesIfFound, value); + } + public void ShowDebugger() { _debugService.ShowDebugger(); @@ -230,6 +270,20 @@ namespace Artemis.UI.Screens.Settings.Tabs.General } } + public async void OfferUpdatesIfFound() + { + if (!CanOfferUpdatesIfFound) + return; + + CanOfferUpdatesIfFound = false; + 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; + } + protected override void OnInitialActivate() { Task.Run(ApplyAutorun); diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs index 46a7d04e3..86a0180b0 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginFeatureViewModel.cs @@ -20,17 +20,17 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins { private readonly IDialogService _dialogService; private readonly IPluginManagementService _pluginManagementService; - private readonly ISnackbarMessageQueue _snackbarMessageQueue; + private IMessageService _messageService; private bool _enabling; - + public PluginFeatureViewModel(PluginFeature feature, IDialogService dialogService, IPluginManagementService pluginManagementService, - ISnackbarMessageQueue snackbarMessageQueue) + IMessageService messageService) { _dialogService = dialogService; _pluginManagementService = pluginManagementService; - _snackbarMessageQueue = snackbarMessageQueue; + _messageService = messageService; Feature = feature; Icon = GetIconKind(); @@ -109,7 +109,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } catch (Exception e) { - _snackbarMessageQueue.Enqueue($"Failed to enable {Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder); + _messageService.ShowMessage($"Failed to enable {Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder); } finally { diff --git a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs index 3d75bc9f6..202b1dea8 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/Plugins/PluginSettingsViewModel.cs @@ -19,7 +19,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins private readonly IDialogService _dialogService; private readonly IPluginManagementService _pluginManagementService; private readonly ISettingsVmFactory _settingsVmFactory; - private readonly ISnackbarMessageQueue _snackbarMessageQueue; + private readonly IMessageService _messageService; private readonly IWindowManager _windowManager; private bool _enabling; private Plugin _plugin; @@ -29,7 +29,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins IWindowManager windowManager, IDialogService dialogService, IPluginManagementService pluginManagementService, - ISnackbarMessageQueue snackbarMessageQueue) + IMessageService messageService) { Plugin = plugin; @@ -37,7 +37,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins _windowManager = windowManager; _dialogService = dialogService; _pluginManagementService = pluginManagementService; - _snackbarMessageQueue = snackbarMessageQueue; + _messageService = messageService; Icon = PluginUtilities.GetPluginIcon(Plugin, Plugin.Info.Icon); } @@ -130,7 +130,7 @@ namespace Artemis.UI.Screens.Settings.Tabs.Plugins } catch (Exception e) { - _snackbarMessageQueue.Enqueue($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder); + _messageService.ShowMessage($"Failed to enable plugin {Plugin.Info.Name}\r\n{e.Message}", "VIEW LOGS", ShowLogsFolder); } finally { diff --git a/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceDetectInputViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceDetectInputViewModel.cs index cf75ee65b..a3c7d41f9 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceDetectInputViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/Dialogs/SurfaceDeviceDetectInputViewModel.cs @@ -13,17 +13,17 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs public class SurfaceDeviceDetectInputViewModel : DialogViewModelBase { private readonly IInputService _inputService; + private readonly IMessageService _messageService; private readonly ListLedGroup _ledGroup; - private readonly ISnackbarMessageQueue _mainMessageQueue; - public SurfaceDeviceDetectInputViewModel(ArtemisDevice device, IInputService inputService, ISnackbarMessageQueue mainMessageQueue) + public SurfaceDeviceDetectInputViewModel(ArtemisDevice device, IInputService inputService, IMessageService messageService) { Device = device; Title = $"{Device.RgbDevice.DeviceInfo.DeviceName} - Detect input"; IsMouse = Device.RgbDevice.DeviceInfo.DeviceType == RGBDeviceType.Mouse; _inputService = inputService; - _mainMessageQueue = mainMessageQueue; + _messageService = messageService; _inputService.IdentifyDevice(Device); _inputService.DeviceIdentified += InputServiceOnDeviceIdentified; @@ -49,7 +49,7 @@ namespace Artemis.UI.Screens.SurfaceEditor.Dialogs private void InputServiceOnDeviceIdentified(object sender, EventArgs e) { Session?.Close(true); - _mainMessageQueue.Enqueue($"{Device.RgbDevice.DeviceInfo.DeviceName} identified 😁"); + _messageService.ShowMessage($"{Device.RgbDevice.DeviceInfo.DeviceName} identified 😁"); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/TrayViewModel.cs b/src/Artemis.UI/Screens/TrayViewModel.cs index a6c2386d1..a513caa6f 100644 --- a/src/Artemis.UI/Screens/TrayViewModel.cs +++ b/src/Artemis.UI/Screens/TrayViewModel.cs @@ -1,13 +1,16 @@ -using Artemis.Core.Services; +using System; +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 Ninject; using Stylet; namespace Artemis.UI.Screens { - public class TrayViewModel : Screen + public class TrayViewModel : Screen, IMainWindowManager { private readonly IDebugService _debugService; private readonly IEventAggregator _eventAggregator; @@ -15,8 +18,16 @@ namespace Artemis.UI.Screens private readonly IWindowManager _windowManager; private bool _canShowRootViewModel; private SplashViewModel _splashViewModel; + private RootViewModel _rootViewModel; - public TrayViewModel(IKernel kernel, IWindowManager windowManager, IEventAggregator eventAggregator, ICoreService coreService, IDebugService debugService, ISettingsService settingsService) + public TrayViewModel(IKernel kernel, + IWindowManager windowManager, + IWindowService windowService, + IUpdateService updateService, + IEventAggregator eventAggregator, + ICoreService coreService, + IDebugService debugService, + ISettingsService settingsService) { _kernel = kernel; _windowManager = windowManager; @@ -24,13 +35,16 @@ namespace Artemis.UI.Screens _debugService = debugService; CanShowRootViewModel = true; + windowService.ConfigureMainWindowManager(this); bool autoRunning = Bootstrapper.StartupArguments.Contains("--autorun"); bool showOnAutoRun = settingsService.GetSetting("UI.ShowOnStartup", true).Value; if (!autoRunning || showOnAutoRun) { ShowSplashScreen(); - coreService.Initialized += (sender, args) => TrayBringToForeground(); + coreService.Initialized += (_, _) => TrayBringToForeground(); } + + updateService.AutoUpdate(); } public bool CanShowRootViewModel @@ -53,10 +67,12 @@ namespace Artemis.UI.Screens { _splashViewModel?.RequestClose(); _splashViewModel = null; - RootViewModel rootViewModel = _kernel.Get(); - rootViewModel.Closed += RootViewModelOnClosed; - _windowManager.ShowWindow(rootViewModel); + _rootViewModel = _kernel.Get(); + _rootViewModel.Closed += RootViewModelOnClosed; + _windowManager.ShowWindow(_rootViewModel); }); + + OnMainWindowOpened(); } public void TrayActivateSidebarItem(string sidebarItem) @@ -86,7 +102,53 @@ namespace Artemis.UI.Screens private void RootViewModelOnClosed(object sender, CloseEventArgs e) { + _rootViewModel.Closed -= RootViewModelOnClosed; + _rootViewModel = null; + CanShowRootViewModel = true; + OnMainWindowClosed(); } + + #region Implementation of IMainWindowManager + + /// + public bool IsMainWindowOpen { get; private set; } + + /// + public bool OpenMainWindow() + { + if (CanShowRootViewModel) + return false; + + TrayBringToForeground(); + return true; + } + + /// + public bool CloseMainWindow() + { + _rootViewModel.RequestClose(); + return _rootViewModel.ScreenState == ScreenState.Closed; + } + + /// + public event EventHandler MainWindowOpened; + + /// + public event EventHandler MainWindowClosed; + + protected virtual void OnMainWindowOpened() + { + IsMainWindowOpen = true; + MainWindowOpened?.Invoke(this, EventArgs.Empty); + } + + protected virtual void OnMainWindowClosed() + { + IsMainWindowOpen = false; + MainWindowClosed?.Invoke(this, EventArgs.Empty); + } + + #endregion } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/UpdateService.cs b/src/Artemis.UI/Services/UpdateService.cs new file mode 100644 index 000000000..d29633ec8 --- /dev/null +++ b/src/Artemis.UI/Services/UpdateService.cs @@ -0,0 +1,140 @@ +using System; +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 Newtonsoft.Json.Linq; +using Serilog; + +namespace Artemis.UI.Services +{ + public class UpdateService : IUpdateService + { + private const string ApiUrl = "https://dev.azure.com/artemis-rgb/Artemis/_apis/"; + private readonly PluginSetting _autoInstallUpdates; + private readonly PluginSetting _checkForUpdates; + private readonly ILogger _logger; + private readonly IDialogService _dialogService; + private readonly IWindowService _windowService; + + public UpdateService(ILogger logger, ISettingsService settingsService, IDialogService dialogService, IWindowService windowService) + { + _logger = logger; + _dialogService = dialogService; + _windowService = windowService; + _windowService.MainWindowOpened += WindowServiceOnMainWindowOpened; + + _checkForUpdates = settingsService.GetSetting("UI.CheckForUpdates", true); + _autoInstallUpdates = settingsService.GetSetting("UI.AutoInstallUpdates", false); + + _checkForUpdates.SettingChanged += CheckForUpdatesOnSettingChanged; + } + + public async Task GetLatestBuildNumber() + { + // TODO: The URL is hardcoded, that should change in the future + string latestBuildUrl = ApiUrl + "build/builds?api-version=6.1-preview.6&branchName=refs/heads/master&resultFilter=succeeded&$top=1"; + _logger.Debug("Getting latest build number from {latestBuildUrl}", latestBuildUrl); + + // 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) + return buildNumberToken.Value(); + + _logger.Warning("Failed to find build number at \"value[0].buildNumber\""); + return 0; + + } + catch (Exception e) + { + _logger.Warning(e, "Failed to retrieve build info JSON"); + return 0; + } + } + + public async Task OfferUpdatesIfFound() + { + _logger.Information("Checking for updates"); + + double buildNumber = await GetLatestBuildNumber(); + _logger.Information("Latest build is {buildNumber}, we're running {localBuildNumber}", buildNumber, Constants.BuildInfo.BuildNumber); + + if (buildNumber < Constants.BuildInfo.BuildNumber) + return false; + + if (_windowService.IsMainWindowOpen) + { + + } + else + { + + } + + return true; + } + + public async Task IsUpdateAvailable() + { + double buildNumber = await GetLatestBuildNumber(); + return buildNumber > Constants.BuildInfo.BuildNumber; + } + + public void ApplyUpdate() + { + throw new NotImplementedException(); + } + + public async Task AutoUpdate() + { + if (!_checkForUpdates.Value) + return false; + + return await OfferUpdatesIfFound(); + } + + #region Event handlers + + private void CheckForUpdatesOnSettingChanged(object sender, EventArgs e) + { + // Run an auto-update as soon as the setting gets changed to enabled + if (_checkForUpdates.Value) + AutoUpdate(); + } + + private void WindowServiceOnMainWindowOpened(object? sender, EventArgs e) + { + _logger.Information("Main window opened!"); + } + + #endregion + } + + public interface IUpdateService : IArtemisUIService + { + Task OfferUpdatesIfFound(); + Task IsUpdateAvailable(); + void ApplyUpdate(); + + /// + /// If auto-update is enabled this will offer updates if found + /// + Task AutoUpdate(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/buildinfo.json b/src/Artemis.UI/buildinfo.json index 248b88208..e5db2c3c5 100644 --- a/src/Artemis.UI/buildinfo.json +++ b/src/Artemis.UI/buildinfo.json @@ -1,6 +1,6 @@ { "BuildId": 0, "BuildNumber": 0, - "SourceBranch": "", - "SourceVersion": "" + "SourceBranch": "local", + "SourceVersion": "local" } \ No newline at end of file