1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 21:38:38 +00:00

Implemented Windows updating

This commit is contained in:
Robert 2023-02-26 13:41:40 +01:00
parent 667663dadf
commit 67a4672c66
16 changed files with 291 additions and 69 deletions

View File

@ -1,4 +1,5 @@
using System;
using ReactiveUI;
namespace Artemis.UI.Shared.Services.MainWindow;
@ -12,6 +13,11 @@ public interface IMainWindowService : IArtemisSharedUIService
/// </summary>
bool IsMainWindowOpen { get; }
/// <summary>
/// Gets or sets the host screen contained in the main window
/// </summary>
IScreen? HostScreen { get; set; }
/// <summary>
/// Sets up the main window provider that controls the state of the main window
/// </summary>

View File

@ -1,4 +1,5 @@
using System;
using ReactiveUI;
namespace Artemis.UI.Shared.Services.MainWindow;
@ -6,6 +7,12 @@ internal class MainWindowService : IMainWindowService
{
private IMainWindowProvider? _mainWindowManager;
/// <inheritdoc />
public bool IsMainWindowOpen { get; private set; }
/// <inheritdoc />
public IScreen? HostScreen { get; set; }
protected virtual void OnMainWindowOpened()
{
MainWindowOpened?.Invoke(this, EventArgs.Empty);
@ -64,8 +71,6 @@ internal class MainWindowService : IMainWindowService
OnMainWindowUnfocused();
}
public bool IsMainWindowOpen { get; private set; }
public void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider)
{
if (mainWindowProvider == null) throw new ArgumentNullException(nameof(mainWindowProvider));

View File

@ -1,5 +1,6 @@
using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared.Providers;
using Artemis.UI.Windows.Providers;
using Artemis.UI.Windows.Providers.Input;
@ -22,5 +23,6 @@ public static class UIContainerExtensions
container.Register<IGraphicsContextProvider, GraphicsContextProvider>(Reuse.Singleton);
container.Register<IAutoRunProvider, AutoRunProvider>();
container.Register<InputProvider, WindowsInputProvider>(serviceKey: WindowsInputProvider.Id);
container.Register<IUpdateNotificationProvider, WindowsUpdateNotificationProvider>();
}
}

View File

@ -0,0 +1,146 @@
using System;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Windows.UI.Notifications;
using Artemis.UI.Screens.Settings;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared.Services.MainWindow;
using Avalonia.Threading;
using Microsoft.Toolkit.Uwp.Notifications;
using ReactiveUI;
namespace Artemis.UI.Windows.Providers;
public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
{
private readonly Func<string, ReleaseInstaller> _getReleaseInstaller;
private readonly Func<IScreen, SettingsViewModel> _getSettingsViewModel;
private readonly IMainWindowService _mainWindowService;
private readonly IUpdateService _updateService;
public WindowsUpdateNotificationProvider(IMainWindowService mainWindowService,
IUpdateService updateService,
Func<IScreen, SettingsViewModel> getSettingsViewModel,
Func<string, ReleaseInstaller> getReleaseInstaller)
{
_mainWindowService = mainWindowService;
_updateService = updateService;
_getSettingsViewModel = getSettingsViewModel;
_getReleaseInstaller = getReleaseInstaller;
ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated;
}
private async void ToastNotificationManagerCompatOnOnActivated(ToastNotificationActivatedEventArgsCompat e)
{
ToastArguments args = ToastArguments.Parse(e.Argument);
string releaseId = args.Get("releaseId");
string releaseVersion = args.Get("releaseVersion");
string action = "view-changes";
if (args.Contains("action"))
action = args.Get("action");
if (action == "install")
await InstallRelease(releaseId, releaseVersion);
else if (action == "view-changes")
ViewRelease(releaseId);
else if (action == "restart-for-update")
_updateService.RestartForUpdate(false);
}
public async Task ShowNotification(string releaseId, string releaseVersion)
{
new ToastContentBuilder()
.AddArgument("releaseId", releaseId)
.AddArgument("releaseVersion", releaseVersion)
.AddText("Update available")
.AddText($"Artemis version {releaseVersion} has been released")
.AddButton(new ToastButton()
.SetContent("Install")
.AddArgument("action", "install").SetAfterActivationBehavior(ToastAfterActivationBehavior.PendingUpdate))
.AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-changes"))
.Show(t => t.Tag = releaseId);
}
private void ViewRelease(string releaseId)
{
Dispatcher.UIThread.Post(() =>
{
_mainWindowService.OpenMainWindow();
// TODO: When proper routing has been implemented, use that here
// Create a settings VM to navigate to
SettingsViewModel settingsViewModel = _getSettingsViewModel(_mainWindowService.HostScreen);
// Get the release tab
ReleasesTabViewModel releaseTabViewModel = (ReleasesTabViewModel) settingsViewModel.SettingTabs.First(t => t is ReleasesTabViewModel);
// Navigate to the settings VM
_mainWindowService.HostScreen.Router.Navigate.Execute(settingsViewModel);
// Navigate to the release tab
releaseTabViewModel.PreselectId = releaseId;
settingsViewModel.SelectedTab = releaseTabViewModel;
});
}
private async Task InstallRelease(string releaseId, string releaseVersion)
{
ReleaseInstaller installer = _getReleaseInstaller(releaseId);
void InstallerOnPropertyChanged(object? sender, PropertyChangedEventArgs e) => UpdateInstallProgress(releaseId, installer);
new ToastContentBuilder()
.AddArgument("releaseId", releaseId)
.AddArgument("releaseVersion", releaseVersion)
.AddAudio(new ToastAudio {Silent = true})
.AddText("Installing Artemis update")
.AddVisualChild(new AdaptiveProgressBar()
{
Title = releaseVersion,
Value = new BindableProgressBarValue("progressValue"),
Status = new BindableString("progressStatus")
})
.AddButton(new ToastButton().SetContent("Cancel").AddArgument("action", "cancel"))
.Show(t =>
{
t.Tag = releaseId;
t.Data = GetInstallerNotificationData(installer);
});
await Task.Delay(2000);
installer.PropertyChanged += InstallerOnPropertyChanged;
await installer.InstallAsync(CancellationToken.None);
installer.PropertyChanged -= InstallerOnPropertyChanged;
_updateService.QueueUpdate();
new ToastContentBuilder()
.AddArgument("releaseId", releaseId)
.AddArgument("releaseVersion", releaseVersion)
.AddAudio(new ToastAudio {Silent = true})
.AddText("Update ready")
.AddText($"Artemis version {releaseVersion} is ready to be applied")
.AddButton(new ToastButton().SetContent("Restart Artemis").AddArgument("action", "restart-for-update"))
.AddButton(new ToastButton().SetContent("Later").AddArgument("action", "postpone-update"))
.Show(t => t.Tag = releaseId);
}
private void UpdateInstallProgress(string releaseId, ReleaseInstaller installer)
{
ToastNotificationManagerCompat.CreateToastNotifier().Update(GetInstallerNotificationData(installer), releaseId);
}
private NotificationData GetInstallerNotificationData(ReleaseInstaller installer)
{
NotificationData data = new()
{
Values =
{
["progressValue"] = (installer.Progress / 100f).ToString(CultureInfo.InvariantCulture),
["progressStatus"] = installer.Status
}
};
return data;
}
}

View File

@ -36,7 +36,7 @@ public static class UIContainerExtensions
container.Register<NodeScriptWindowViewModelBase, NodeScriptWindowViewModel>(Reuse.Singleton);
container.Register<IPropertyVmFactory, PropertyVmFactory>(Reuse.Singleton);
container.Register<IUpdateNotificationProvider, SimpleUpdateNotificationProvider>();
container.Register<IUpdateNotificationProvider, InAppUpdateNotificationProvider>();
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IArtemisUIService>(), Reuse.Singleton);
}

View File

@ -58,7 +58,8 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
_lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!;
mainWindowService.ConfigureMainWindowProvider(this);
mainWindowService.HostScreen = this;
DisplayAccordingToSettings();
Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel);
Task.Run(() =>
@ -230,11 +231,6 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
}
#endregion
public void SaveWindowBounds(int x, int y, int width, int height)
{
throw new NotImplementedException();
}
}
internal class EmptyViewModel : MainScreenViewModel

View File

@ -2,10 +2,12 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Settings.SettingsView">
x:Class="Artemis.UI.Screens.Settings.SettingsView"
x:DataType="settings:SettingsViewModel">
<Border Classes="router-container">
<TabControl Margin="12" Items="{Binding SettingTabs}">
<TabControl Margin="12" Items="{CompiledBinding SettingTabs}" SelectedItem="{CompiledBinding SelectedTab}">
<TabControl.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding DisplayName}" />

View File

@ -6,6 +6,8 @@ namespace Artemis.UI.Screens.Settings;
public class SettingsViewModel : MainScreenViewModel
{
private ActivatableViewModelBase _selectedTab;
public SettingsViewModel(IScreen hostScreen,
GeneralTabViewModel generalTabViewModel,
PluginsTabViewModel pluginsTabViewModel,
@ -24,4 +26,10 @@ public class SettingsViewModel : MainScreenViewModel
}
public ObservableCollection<ActivatableViewModelBase> SettingTabs { get; }
public ActivatableViewModelBase SelectedTab
{
get => _selectedTab;
set => RaiseAndSetIfChanged(ref _selectedTab, value);
}
}

View File

@ -51,11 +51,12 @@ public class ReleasesTabViewModel : ActivatableViewModelBase
{
await updateService.CacheLatestRelease();
await GetMoreReleases(d.AsCancellationToken());
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault();
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(r => r.ReleaseId == PreselectId) ?? ReleaseViewModels.FirstOrDefault();
});
}
public ReadOnlyObservableCollection<ReleaseViewModel> ReleaseViewModels { get; }
public string? PreselectId { get; set; }
public ReleaseViewModel? SelectedReleaseViewModel
{

View File

@ -22,7 +22,7 @@ public class ReleaseViewModel : ActivatableViewModelBase
{
private readonly ILogger _logger;
private readonly INotificationService _notificationService;
private readonly string _releaseId;
private readonly IUpdateService _updateService;
private readonly Platform _updatePlatform;
private readonly IUpdatingClient _updatingClient;
private readonly IWindowService _windowService;
@ -46,10 +46,10 @@ public class ReleaseViewModel : ActivatableViewModelBase
IUpdateService updateService,
IWindowService windowService)
{
_releaseId = releaseId;
_logger = logger;
_updatingClient = updatingClient;
_notificationService = notificationService;
_updateService = updateService;
_windowService = windowService;
if (OperatingSystem.IsWindows())
@ -61,12 +61,13 @@ public class ReleaseViewModel : ActivatableViewModelBase
else
throw new PlatformNotSupportedException("Cannot auto update on the current platform");
ReleaseId = releaseId;
Version = version;
CreatedAt = createdAt;
ReleaseInstaller = updateService.GetReleaseInstaller(_releaseId);
ReleaseInstaller = updateService.GetReleaseInstaller(ReleaseId);
Install = ReactiveCommand.CreateFromTask(ExecuteInstall);
Restart = ReactiveCommand.Create(() => Utilities.ApplyUpdate(false));
Restart = ReactiveCommand.Create(ExecuteRestart);
CancelInstall = ReactiveCommand.Create(() => _installerCts?.Cancel());
this.WhenActivated(d =>
@ -74,12 +75,19 @@ public class ReleaseViewModel : ActivatableViewModelBase
// There's no point in running anything but the latest version of the current channel.
// Perhaps later that won't be true anymore, then we could consider allowing to install
// older versions with compatible database versions.
InstallationAvailable = updateService.CachedLatestRelease?.Id == _releaseId;
InstallationAvailable = _updateService.CachedLatestRelease?.Id == ReleaseId;
RetrieveDetails(d.AsCancellationToken()).ToObservable();
Disposable.Create(_installerCts, cts => cts?.Cancel()).DisposeWith(d);
});
}
public string ReleaseId { get; }
private void ExecuteRestart()
{
_updateService.RestartForUpdate(false);
}
public ReactiveCommand<Unit, Unit> Restart { get; set; }
public ReactiveCommand<Unit, Unit> Install { get; }
public ReactiveCommand<Unit, Unit> CancelInstall { get; }
@ -148,6 +156,7 @@ public class ReleaseViewModel : ActivatableViewModelBase
{
InstallationInProgress = true;
await ReleaseInstaller.InstallAsync(_installerCts.Token);
_updateService.QueueUpdate();
InstallationFinished = true;
}
catch (TaskCanceledException)
@ -173,7 +182,7 @@ public class ReleaseViewModel : ActivatableViewModelBase
{
Loading = true;
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken);
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(ReleaseId, cancellationToken);
IGetReleaseById_PublishedRelease? release = result.Data?.PublishedRelease;
if (release == null)
return;

View File

@ -137,6 +137,10 @@ public class SidebarViewModel : ActivatableViewModelBase
private void NavigateToScreen(SidebarScreenViewModel sidebarScreenViewModel)
{
// If the current screen changed through external means and already matches, don't navigate again
if (_hostScreen.Router.GetCurrentViewModel()?.GetType() == sidebarScreenViewModel.ScreenType)
return;
_hostScreen.Router.Navigate.Execute(sidebarScreenViewModel.CreateInstance(_container, _hostScreen));
_profileEditorService.ChangeCurrentProfileConfiguration(null);
}

View File

@ -4,5 +4,5 @@ namespace Artemis.UI.Services.Updating;
public interface IUpdateNotificationProvider
{
Task ShowNotification(string releaseId);
Task ShowNotification(string releaseId, string releaseVersion);
}

View File

@ -11,8 +11,8 @@ public interface IUpdateService : IArtemisUIService
Task CacheLatestRelease();
Task<bool> CheckForUpdate();
Task InstallRelease(string releaseId);
void QueueUpdate();
ReleaseInstaller GetReleaseInstaller(string releaseId);
void RestartForUpdate(bool silent);
}

View File

@ -0,0 +1,66 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Artemis.UI.Screens.Settings;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Services.MainWindow;
using ReactiveUI;
namespace Artemis.UI.Services.Updating;
public class InAppUpdateNotificationProvider : IUpdateNotificationProvider
{
private readonly Func<IScreen, SettingsViewModel> _getSettingsViewModel;
private readonly IMainWindowService _mainWindowService;
private readonly INotificationService _notificationService;
private Action? _notification;
public InAppUpdateNotificationProvider(INotificationService notificationService, IMainWindowService mainWindowService, Func<IScreen, SettingsViewModel> getSettingsViewModel)
{
_notificationService = notificationService;
_mainWindowService = mainWindowService;
_getSettingsViewModel = getSettingsViewModel;
}
private void ShowInAppNotification(string releaseId, string releaseVersion)
{
_notification?.Invoke();
_notification = _notificationService.CreateNotification()
.WithTitle("Update available")
.WithMessage($"Artemis version {releaseVersion} has been released")
.WithSeverity(NotificationSeverity.Success)
.WithTimeout(TimeSpan.FromSeconds(15))
.HavingButton(b => b.WithText("View release").WithAction(() => ViewRelease(releaseId)))
.Show();
}
private void ViewRelease(string releaseId)
{
_notification?.Invoke();
if (_mainWindowService.HostScreen == null)
return;
// TODO: When proper routing has been implemented, use that here
// Create a settings VM to navigate to
SettingsViewModel settingsViewModel = _getSettingsViewModel(_mainWindowService.HostScreen);
// Get the release tab
ReleasesTabViewModel releaseTabViewModel = (ReleasesTabViewModel) settingsViewModel.SettingTabs.First(t => t is ReleasesTabViewModel);
// Navigate to the settings VM
_mainWindowService.HostScreen.Router.Navigate.Execute(settingsViewModel);
// Navigate to the release tab
releaseTabViewModel.PreselectId = releaseId;
settingsViewModel.SelectedTab = releaseTabViewModel;
}
/// <inheritdoc />
public async Task ShowNotification(string releaseId, string releaseVersion)
{
if (_mainWindowService.IsMainWindowOpen)
ShowInAppNotification(releaseId, releaseVersion);
else
_mainWindowService.MainWindowOpened += (_, _) => ShowInAppNotification(releaseId, releaseVersion);
}
}

View File

@ -1,12 +0,0 @@
using System.Threading.Tasks;
namespace Artemis.UI.Services.Updating;
public class SimpleUpdateNotificationProvider : IUpdateNotificationProvider
{
/// <inheritdoc />
public async Task ShowNotification(string releaseId)
{
throw new System.NotImplementedException();
}
}

View File

@ -6,8 +6,6 @@ using Artemis.Core;
using Artemis.Core.Services;
using Artemis.Storage.Entities.General;
using Artemis.Storage.Repositories.Interfaces;
using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.MainWindow;
using Artemis.WebClient.Updating;
using Avalonia.Threading;
@ -69,7 +67,7 @@ public class UpdateService : IUpdateService
_channel.Value = "feature/gh-actions";
_channel.Save();
InstallQueuedUpdate();
}
@ -79,30 +77,21 @@ public class UpdateService : IUpdateService
if (!_queuedActionRepository.IsTypeQueued("InstallUpdate"))
return;
// Remove the queued action, in case something goes wrong then at least we don't end up in a loop
_queuedActionRepository.ClearByType("InstallUpdate");
_logger.Information("Installing queued update");
Utilities.ApplyUpdate(false);
}
private async Task ShowUpdateDialog(string nextReleaseId)
private async Task ShowUpdateNotification(IGetNextRelease_NextPublishedRelease release)
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
// Main window is probably already open but this will bring it into focus
_mainWindowService.OpenMainWindow();
});
await _updateNotificationProvider.Value.ShowNotification(release.Id, release.Version);
}
private async Task ShowUpdateNotification(string nextReleaseId)
private async Task AutoInstallUpdate(IGetNextRelease_NextPublishedRelease release)
{
await _updateNotificationProvider.Value.ShowNotification(nextReleaseId);
}
private async Task AutoInstallUpdate(string nextReleaseId)
{
ReleaseInstaller installer = _getReleaseInstaller(nextReleaseId);
ReleaseInstaller installer = _getReleaseInstaller(release.Id);
await installer.InstallAsync(CancellationToken.None);
Utilities.ApplyUpdate(true);
}
@ -121,7 +110,7 @@ public class UpdateService : IUpdateService
_logger.Warning(ex, "Auto update failed");
}
}
public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; }
public string? CurrentVersion
@ -139,12 +128,12 @@ public class UpdateService : IUpdateService
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(CurrentVersion, _channel.Value, _updatePlatform);
CachedLatestRelease = result.Data?.NextPublishedRelease;
}
public async Task<bool> CheckForUpdate()
{
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(CurrentVersion, _channel.Value, _updatePlatform);
result.EnsureNoErrors();
// Update cache
CachedLatestRelease = result.Data?.NextPublishedRelease;
@ -156,27 +145,14 @@ public class UpdateService : IUpdateService
_suspendAutoCheck = true;
// If the window is open show the changelog, don't auto-update while the user is busy
if (_mainWindowService.IsMainWindowOpen)
await ShowUpdateDialog(CachedLatestRelease.Id);
else if (!_autoInstall.Value)
await ShowUpdateNotification(CachedLatestRelease.Id);
if (!_autoInstall.Value)
await ShowUpdateNotification(CachedLatestRelease);
else
await AutoInstallUpdate(CachedLatestRelease.Id);
await AutoInstallUpdate(CachedLatestRelease);
return true;
}
/// <inheritdoc />
public async Task InstallRelease(string releaseId)
{
ReleaseInstaller installer = _getReleaseInstaller(releaseId);
await Dispatcher.UIThread.InvokeAsync(() =>
{
// Main window is probably already open but this will bring it into focus
_mainWindowService.OpenMainWindow();
});
}
/// <inheritdoc />
public void QueueUpdate()
{
@ -184,9 +160,22 @@ public class UpdateService : IUpdateService
_queuedActionRepository.Add(new QueuedActionEntity {Type = "InstallUpdate"});
}
/// <inheritdoc />
public void DequeueUpdate()
{
_queuedActionRepository.ClearByType("InstallUpdate");
}
/// <inheritdoc />
public ReleaseInstaller GetReleaseInstaller(string releaseId)
{
return _getReleaseInstaller(releaseId);
}
/// <inheritdoc />
public void RestartForUpdate(bool silent)
{
DequeueUpdate();
Utilities.ApplyUpdate(silent);
}
}