diff --git a/src/Artemis.Core/Events/UpdateEventArgs.cs b/src/Artemis.Core/Events/UpdateEventArgs.cs new file mode 100644 index 000000000..d513bae5e --- /dev/null +++ b/src/Artemis.Core/Events/UpdateEventArgs.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; + +namespace Artemis.Core; + +/// +/// Provides data about application update events +/// +public class UpdateEventArgs : EventArgs +{ + internal UpdateEventArgs(bool silent) + { + Silent = silent; + } + + /// + /// Gets a boolean indicating whether to silently update or not. + /// + public bool Silent { get; } +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 6de473234..e313ca59d 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -84,7 +84,20 @@ internal class PluginManagementService : IPluginManagementService foreach (FileInfo zipFile in builtInPluginDirectory.EnumerateFiles("*.zip")) { - // Find the metadata file in the zip + try + { + ExtractBuiltInPlugin(zipFile, pluginDirectory); + } + catch (Exception e) + { + _logger.Error(e, "Failed to copy built-in plugin from {ZipFile}", zipFile.FullName); + } + } + } + + private void ExtractBuiltInPlugin(FileInfo zipFile, DirectoryInfo pluginDirectory) + { + // Find the metadata file in the zip using ZipArchive archive = ZipFile.OpenRead(zipFile.FullName); ZipArchiveEntry? metaDataFileEntry = archive.GetEntry("plugin.json"); if (metaDataFileEntry == null) @@ -135,7 +148,6 @@ internal class PluginManagementService : IPluginManagementService } } } - } } #endregion diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index bbb38c4be..e5590b568 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -50,6 +50,15 @@ public static class Utilities OnRestartRequested(new RestartEventArgs(elevate, delay, extraArgs.ToList())); } + /// + /// Applies a pending update + /// + /// A boolean indicating whether to silently update or not. + public static void ApplyUpdate(bool silent) + { + OnUpdateRequested(new UpdateEventArgs(silent)); + } + /// /// Opens the provided URL in the default web browser /// @@ -96,11 +105,16 @@ public static class Utilities /// Occurs when the core has requested an application shutdown /// public static event EventHandler? ShutdownRequested; - + /// /// Occurs when the core has requested an application restart /// public static event EventHandler? RestartRequested; + + /// + /// Occurs when the core has requested a pending application update to be applied + /// + public static event EventHandler? UpdateRequested; /// /// Opens the provided folder in the user's file explorer @@ -136,6 +150,11 @@ public static class Utilities { ShutdownRequested?.Invoke(null, EventArgs.Empty); } + + private static void OnUpdateRequested(UpdateEventArgs e) + { + UpdateRequested?.Invoke(null, e); + } #region Scaling diff --git a/src/Artemis.Storage/StorageManager.cs b/src/Artemis.Storage/StorageManager.cs index 87bc97b76..78fcc3643 100644 --- a/src/Artemis.Storage/StorageManager.cs +++ b/src/Artemis.Storage/StorageManager.cs @@ -30,7 +30,7 @@ public static class StorageManager { FileSystemInfo newest = files.OrderByDescending(fi => fi.CreationTime).First(); FileSystemInfo oldest = files.OrderBy(fi => fi.CreationTime).First(); - if (DateTime.Now - newest.CreationTime < TimeSpan.FromMinutes(10)) + if (DateTime.Now - newest.CreationTime < TimeSpan.FromHours(12)) return; oldest.Delete(); diff --git a/src/Artemis.UI.Windows/ApplicationStateManager.cs b/src/Artemis.UI.Windows/ApplicationStateManager.cs index a14b50248..2eb3ce47f 100644 --- a/src/Artemis.UI.Windows/ApplicationStateManager.cs +++ b/src/Artemis.UI.Windows/ApplicationStateManager.cs @@ -17,7 +17,7 @@ namespace Artemis.UI.Windows; public class ApplicationStateManager { private const int SM_SHUTTINGDOWN = 0x2000; - + public ApplicationStateManager(IContainer container, string[] startupArguments) { StartupArguments = startupArguments; @@ -25,6 +25,7 @@ public class ApplicationStateManager Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested; Core.Utilities.RestartRequested += UtilitiesOnRestartRequested; + Core.Utilities.UpdateRequested += UtilitiesOnUpdateRequested; // On Windows shutdown dispose the IOC container just so device providers get a chance to clean up if (Application.Current?.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime) @@ -91,6 +92,33 @@ public class ApplicationStateManager Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown()); } + private void UtilitiesOnUpdateRequested(object? sender, UpdateEventArgs e) + { + List argsList = new(StartupArguments); + if (e.Silent) + argsList.Add("--autorun"); + + // Retain startup arguments after update by providing them to the script + string script = $"\"{Path.Combine(Constants.DataFolder, "updating", "pending", "scripts", "update.ps1")}\""; + string source = $"-sourceDirectory \"{Path.Combine(Constants.DataFolder, "updating", "pending")}\""; + string destination = $"-destinationDirectory \"{Constants.ApplicationFolder}\""; + string args = argsList.Any() ? $"-artemisArgs \"{string.Join(',', argsList)}\"" : ""; + + // Run the PowerShell script included in the new version, that way any changes made to the script are used + ProcessStartInfo info = new() + { + Arguments = $"-File {script} {source} {destination} {args}", + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "PowerShell.exe" + }; + Process.Start(info); + + // Lets try a graceful shutdown, PowerShell will kill if needed + if (Application.Current?.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime) + Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown()); + } + private void UtilitiesOnShutdownRequested(object? sender, EventArgs e) { // Use PowerShell to kill the process after 8 sec just in case @@ -115,7 +143,7 @@ public class ApplicationStateManager }; Process.Start(info); } - + [System.Runtime.InteropServices.DllImport("user32.dll")] private static extern int GetSystemMetrics(int nIndex); } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj index 74ef13f12..96d6d57fb 100644 --- a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -12,20 +12,10 @@ - - - - - - - - - - - - - - + + + PreserveNewest + application.ico diff --git a/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs b/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs index 000931d92..4b3bf7fda 100644 --- a/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs +++ b/src/Artemis.UI.Windows/DryIoc/ContainerExtensions.cs @@ -20,7 +20,6 @@ public static class UIContainerExtensions { container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); - container.Register(Reuse.Singleton); container.Register(); container.Register(serviceKey: WindowsInputProvider.Id); } diff --git a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml deleted file mode 100644 index 481709577..000000000 --- a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml +++ /dev/null @@ -1,70 +0,0 @@ - - - - - A new Artemis update is available! 🥳 - - - - Retrieving changes... - - - - - - - - - - - - - - - - - - - Changelog (auto-generated) - - - - - - - - - - - - - - - - - - We couldn't retrieve any changes - View online - - - - - - - - - - \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs deleted file mode 100644 index 9c33b35b8..000000000 --- a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs +++ /dev/null @@ -1,21 +0,0 @@ -using Artemis.UI.Shared; -using Avalonia; -using Avalonia.Markup.Xaml; - -namespace Artemis.UI.Windows.Screens.Update; - -public class UpdateDialogView : ReactiveCoreWindow -{ - public UpdateDialogView() - { - InitializeComponent(); -#if DEBUG - this.AttachDevTools(); -#endif - } - - private void InitializeComponent() - { - AvaloniaXamlLoader.Load(this); - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs deleted file mode 100644 index a19aecc6a..000000000 --- a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System; -using System.Collections.ObjectModel; -using System.Linq; -using System.Reactive; -using System.Reactive.Disposables; -using System.Threading.Tasks; -using Artemis.Core; -using Artemis.UI.Shared; -using Artemis.UI.Shared.Providers; -using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.Builders; -using Artemis.UI.Windows.Models; -using Artemis.UI.Windows.Providers; -using Avalonia.Threading; -using DynamicData; -using ReactiveUI; - -namespace Artemis.UI.Windows.Screens.Update; - -public class UpdateDialogViewModel : DialogViewModelBase -{ - // Based on https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/github?view=azure-devops&tabs=yaml#skipping-ci-for-individual-commits - private readonly string[] _excludedCommitMessages = - { - "[skip ci]", - "[ci skip]", - "skip-checks: true", - "skip-checks:true", - "[skip azurepipelines]", - "[azurepipelines skip]", - "[skip azpipelines]", - "[azpipelines skip]", - "[skip azp]", - "[azp skip]", - "***NO_CI***" - }; - - private readonly INotificationService _notificationService; - private readonly UpdateProvider _updateProvider; - private bool _hasChanges; - private string? _latestBuild; - - private bool _retrievingChanges; - - public UpdateDialogViewModel(string channel, IUpdateProvider updateProvider, INotificationService notificationService) - { - _updateProvider = (UpdateProvider) updateProvider; - _notificationService = notificationService; - - Channel = channel; - CurrentBuild = Constants.BuildInfo.BuildNumberDisplay; - - this.WhenActivated((CompositeDisposable _) => Dispatcher.UIThread.InvokeAsync(GetBuildChanges)); - Install = ReactiveCommand.Create(() => Close(true)); - AskLater = ReactiveCommand.Create(() => Close(false)); - } - - public ReactiveCommand Install { get; } - public ReactiveCommand AskLater { get; } - - public string Channel { get; } - public string CurrentBuild { get; } - - public ObservableCollection Changes { get; } = new(); - - public bool RetrievingChanges - { - get => _retrievingChanges; - set => RaiseAndSetIfChanged(ref _retrievingChanges, value); - } - - public bool HasChanges - { - get => _hasChanges; - set => RaiseAndSetIfChanged(ref _hasChanges, value); - } - - public string? LatestBuild - { - get => _latestBuild; - set => RaiseAndSetIfChanged(ref _latestBuild, value); - } - - private async Task GetBuildChanges() - { - try - { - RetrievingChanges = true; - Task currentTask = _updateProvider.GetBuildInfo(1, CurrentBuild); - Task latestTask = _updateProvider.GetBuildInfo(1); - - DevOpsBuild? current = await currentTask; - DevOpsBuild? latest = await latestTask; - - LatestBuild = latest?.BuildNumber; - if (current != null && latest != null) - { - GitHubDifference difference = await _updateProvider.GetBuildDifferences(current, latest); - - // Only take commits with one parents (no merges) - Changes.Clear(); - Changes.AddRange(difference.Commits.Where(c => c.Parents.Count == 1) - .SelectMany(c => c.Commit.Message.Split("\n")) - .Select(m => m.Trim()) - .Where(m => !string.IsNullOrWhiteSpace(m) && !_excludedCommitMessages.Contains(m)) - .OrderBy(m => m) - ); - HasChanges = Changes.Any(); - } - } - catch (Exception e) - { - _notificationService.CreateNotification().WithTitle("Failed to retrieve build changes").WithMessage(e.Message).WithSeverity(NotificationSeverity.Error).Show(); - } - finally - { - RetrievingChanges = false; - } - } -} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Scripts/update.ps1 b/src/Artemis.UI.Windows/Scripts/update.ps1 new file mode 100644 index 000000000..4247178dc --- /dev/null +++ b/src/Artemis.UI.Windows/Scripts/update.ps1 @@ -0,0 +1,42 @@ +param ( + [Parameter(Mandatory=$true)][string]$sourceDirectory, + [Parameter(Mandatory=$true)][string]$destinationDirectory, + [Parameter(Mandatory=$false)][string]$artemisArgs +) + +# Wait up to 10 seconds for the process to shut down +for ($i=1; $i -le 10; $i++) { + $process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue + if (!$process) { + break + } + Write-Host "Waiting for Artemis to shut down ($i / 10)" + Start-Sleep -Seconds 1 +} + +# If the process is still running, kill it +$process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue +if ($process) { + Stop-Process -Id $process.Id -Force + Start-Sleep -Seconds 1 +} + +# Check if the destination directory exists +if (!(Test-Path $destinationDirectory)) { + Write-Error "The destination directory does not exist" +} + +# If the destination directory exists, clear it +Get-ChildItem $destinationDirectory | Remove-Item -Recurse -Force + +# Move the contents of the source directory to the destination directory +Get-ChildItem $sourceDirectory | Move-Item -Destination $destinationDirectory + +Start-Sleep -Seconds 1 + +# When finished, run the updated version +if ($artemisArgs) { + Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory -ArgumentList $artemisArgs +} else { + Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory +} \ No newline at end of file diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 101b1ac8a..086380658 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -29,6 +29,7 @@ + diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml index 8407e5714..183116c26 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputView.axaml @@ -27,7 +27,8 @@ Width="200" VerticalAlignment="Center" Items="{CompiledBinding Descriptors}" - SelectedItem="{CompiledBinding SelectedDescriptor}"> + SelectedItem="{CompiledBinding SelectedDescriptor}" + PlaceholderText="Please select a brush"> diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs index 26c548dca..8f9ae6277 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs @@ -59,7 +59,7 @@ public class BrushPropertyInputViewModel : PropertyInputViewModel protected override void ApplyInputValue() { - if (LayerProperty.ProfileElement is not Layer layer || layer.LayerBrush == null || SelectedDescriptor == null) + if (LayerProperty.ProfileElement is not Layer layer || SelectedDescriptor == null) return; _profileEditorService.ExecuteCommand(new ChangeLayerBrush(layer, SelectedDescriptor)); diff --git a/src/Artemis.UI/DryIoc/ContainerExtensions.cs b/src/Artemis.UI/DryIoc/ContainerExtensions.cs index f4c484bec..58970b2fd 100644 --- a/src/Artemis.UI/DryIoc/ContainerExtensions.cs +++ b/src/Artemis.UI/DryIoc/ContainerExtensions.cs @@ -4,6 +4,7 @@ using Artemis.UI.DryIoc.InstanceProviders; using Artemis.UI.Screens; using Artemis.UI.Screens.VisualScripting; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; using Artemis.UI.Shared; using Artemis.UI.Shared.Services.NodeEditor; using Artemis.UI.Shared.Services.ProfileEditor; @@ -36,6 +37,7 @@ public static class UIContainerExtensions container.Register(Reuse.Singleton); container.Register(Reuse.Singleton); + container.Register(); container.RegisterMany(thisAssembly, type => type.IsAssignableTo(), Reuse.Singleton); } diff --git a/src/Artemis.UI/Extensions/CompositeDisposableExtensions.cs b/src/Artemis.UI/Extensions/CompositeDisposableExtensions.cs new file mode 100644 index 000000000..743035d3e --- /dev/null +++ b/src/Artemis.UI/Extensions/CompositeDisposableExtensions.cs @@ -0,0 +1,14 @@ +using System.Reactive.Disposables; +using System.Threading; + +namespace Artemis.UI.Extensions; + +public static class CompositeDisposableExtensions +{ + public static CancellationToken AsCancellationToken(this CompositeDisposable disposable) + { + CancellationTokenSource tokenSource = new(); + Disposable.Create(tokenSource, s => s.Cancel()).DisposeWith(disposable); + return tokenSource.Token; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs b/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs new file mode 100644 index 000000000..0d1fc507d --- /dev/null +++ b/src/Artemis.UI/Extensions/ZipArchiveExtensions.cs @@ -0,0 +1,74 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Threading; + +namespace Artemis.UI.Extensions; + +// Taken from System.IO.Compression with progress reporting slapped on top +public static class ZipArchiveExtensions +{ + /// + /// Extracts all the files in the zip archive to a directory on the file system. + /// + /// The zip archive to extract files from. + /// The path to the directory to place the extracted files in. You can specify either a relative or an absolute path. A relative path is interpreted as relative to the current working directory. + /// A boolean indicating whether to override existing files + /// The progress to report to. + /// A cancellation token + public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, IProgress progress, CancellationToken cancellationToken) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (destinationDirectoryName == null) + throw new ArgumentNullException(nameof(destinationDirectoryName)); + + for (int index = 0; index < source.Entries.Count; index++) + { + ZipArchiveEntry entry = source.Entries[index]; + entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles); + progress.Report((index + 1f) / source.Entries.Count * 100f); + cancellationToken.ThrowIfCancellationRequested(); + } + } + + private static void ExtractRelativeToDirectory(this ZipArchiveEntry source, string destinationDirectoryName, bool overwrite) + { + if (source == null) + throw new ArgumentNullException(nameof(source)); + + if (destinationDirectoryName == null) + throw new ArgumentNullException(nameof(destinationDirectoryName)); + + // Note that this will give us a good DirectoryInfo even if destinationDirectoryName exists: + DirectoryInfo di = Directory.CreateDirectory(destinationDirectoryName); + string destinationDirectoryFullPath = di.FullName; + if (!destinationDirectoryFullPath.EndsWith(Path.DirectorySeparatorChar)) + destinationDirectoryFullPath += Path.DirectorySeparatorChar; + + string fileDestinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, source.FullName)); + + if (!fileDestinationPath.StartsWith(destinationDirectoryFullPath, StringComparison)) + throw new IOException($"The file '{fileDestinationPath}' already exists."); + + if (Path.GetFileName(fileDestinationPath).Length == 0) + { + // If it is a directory: + + if (source.Length != 0) + throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory."); + + Directory.CreateDirectory(fileDestinationPath); + } + else + { + // If it is a file: + // Create containing directory: + Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!); + source.ExtractToFile(fileDestinationPath, overwrite: overwrite); + } + } + private static StringComparison StringComparison => IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase; + private static bool IsCaseSensitive => !(OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsWatchOS()); +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index b2ba9e8cd..fbd81e943 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -7,6 +7,7 @@ using Artemis.UI.DryIoc.Factories; using Artemis.UI.Models; using Artemis.UI.Screens.Sidebar; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.MainWindow; diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml index d8cd0ff65..953f0ac1c 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml @@ -137,7 +137,7 @@ - + Updating diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs index 06d212fa1..9d3aa0d1b 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabViewModel.cs @@ -12,6 +12,7 @@ using Artemis.Core.Providers; using Artemis.Core.Services; using Artemis.UI.Screens.StartupWizard; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; using Artemis.UI.Shared; using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Services; @@ -30,6 +31,7 @@ public class GeneralTabViewModel : ActivatableViewModelBase private readonly PluginSetting _defaultLayerBrushDescriptor; private readonly ISettingsService _settingsService; private readonly IUpdateService _updateService; + private readonly INotificationService _notificationService; private readonly IWindowService _windowService; private bool _startupWizardOpen; @@ -38,13 +40,15 @@ public class GeneralTabViewModel : ActivatableViewModelBase IPluginManagementService pluginManagementService, IDebugService debugService, IWindowService windowService, - IUpdateService updateService) + IUpdateService updateService, + INotificationService notificationService) { DisplayName = "General"; _settingsService = settingsService; _debugService = debugService; _windowService = windowService; _updateService = updateService; + _notificationService = notificationService; _autoRunProvider = container.Resolve(IfUnresolved.ReturnDefault); List layerBrushProviders = pluginManagementService.GetFeaturesOfType(); @@ -88,7 +92,6 @@ public class GeneralTabViewModel : ActivatableViewModelBase public ReactiveCommand ShowDataFolder { get; } public bool IsAutoRunSupported => _autoRunProvider != null; - public bool IsUpdatingSupported => _updateService.UpdatingSupported; public ObservableCollection LayerBrushDescriptors { get; } public ObservableCollection GraphicsContexts { get; } @@ -142,8 +145,8 @@ public class GeneralTabViewModel : ActivatableViewModelBase public PluginSetting UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false); public PluginSetting UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15); public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); - public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.CheckForUpdates", true); - public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.AutoUpdate", false); + public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true); + public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", false); public PluginSetting ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false); public PluginSetting CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information); public PluginSetting CorePreferredGraphicsContext => _settingsService.GetSetting("Core.PreferredGraphicsContext", "Software"); @@ -159,7 +162,14 @@ public class GeneralTabViewModel : ActivatableViewModelBase private async Task ExecuteCheckForUpdate(CancellationToken cancellationToken) { - await _updateService.ManualUpdate(); + // If an update was available a popup was shown, no need to continue + if (await _updateService.CheckForUpdate()) + return; + + _notificationService.CreateNotification() + .WithTitle("No update available") + .WithMessage("You are running the latest version in your current channel") + .Show(); } private async Task ExecuteShowSetupWizard() diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml new file mode 100644 index 000000000..acf70d5b4 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml @@ -0,0 +1,216 @@ + + + + + A new Artemis update is available! 🥳 + + + + Retrieving release... + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs new file mode 100644 index 000000000..ed0373da4 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableView.axaml.cs @@ -0,0 +1,29 @@ +using Artemis.UI.Shared; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Settings.Updating; + +public partial class ReleaseAvailableView : ReactiveCoreWindow +{ + public ReleaseAvailableView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void Button_OnClick(object? sender, RoutedEventArgs e) + { + Close(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs new file mode 100644 index 000000000..118177c7f --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseAvailableViewModel.cs @@ -0,0 +1,76 @@ +using System.Reactive; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Extensions; +using Artemis.UI.Services.Updating; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Artemis.WebClient.Updating; +using ReactiveUI; +using Serilog; +using StrawberryShake; + +namespace Artemis.UI.Screens.Settings.Updating; + +public class ReleaseAvailableViewModel : ActivatableViewModelBase +{ + private readonly string _nextReleaseId; + private readonly ILogger _logger; + private readonly IUpdateService _updateService; + private readonly IUpdatingClient _updatingClient; + private readonly INotificationService _notificationService; + private IGetReleaseById_Release? _release; + + public ReleaseAvailableViewModel(string nextReleaseId, ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, INotificationService notificationService) + { + _nextReleaseId = nextReleaseId; + _logger = logger; + _updateService = updateService; + _updatingClient = updatingClient; + _notificationService = notificationService; + + CurrentVersion = _updateService.CurrentVersion ?? "Development build"; + Install = ReactiveCommand.Create(ExecuteInstall, this.WhenAnyValue(vm => vm.Release).Select(r => r != null)); + + this.WhenActivated(async d => await RetrieveRelease(d.AsCancellationToken())); + } + + private void ExecuteInstall() + { + _updateService.InstallRelease(_nextReleaseId); + } + + private async Task RetrieveRelease(CancellationToken cancellationToken) + { + IOperationResult result = await _updatingClient.GetReleaseById.ExecuteAsync(_nextReleaseId, cancellationToken); + // Borrow GraphQLClientException for messaging, how lazy of me.. + if (result.Errors.Count > 0) + { + GraphQLClientException exception = new(result.Errors); + _logger.Error(exception, "Failed to retrieve release details"); + _notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage(exception.Message).Show(); + return; + } + + if (result.Data?.Release == null) + { + _notificationService.CreateNotification().WithTitle("Failed to retrieve release details").WithMessage("Release not found").Show(); + return; + } + + Release = result.Data.Release; + } + + public string CurrentVersion { get; } + + public IGetReleaseById_Release? Release + { + get => _release; + set => RaiseAndSetIfChanged(ref _release, value); + } + + public ReactiveCommand Install { get; } + public ReactiveCommand AskLater { get; } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml new file mode 100644 index 000000000..46699f5ae --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml @@ -0,0 +1,40 @@ + + + + Downloading & installing update... + + + + This should not take long, when finished Artemis must restart. + + + + + + + + Done, click restart to apply the update 🫡 + + + Restart when finished + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs new file mode 100644 index 000000000..ec2a689e6 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerView.axaml.cs @@ -0,0 +1,28 @@ +using Artemis.UI.Shared; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Interactivity; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.Settings.Updating; + +public partial class ReleaseInstallerView : ReactiveCoreWindow +{ + public ReleaseInstallerView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void Cancel_OnClick(object? sender, RoutedEventArgs e) + { + Close(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs new file mode 100644 index 000000000..79a80aa06 --- /dev/null +++ b/src/Artemis.UI/Screens/Settings/Updating/ReleaseInstallerViewModel.cs @@ -0,0 +1,81 @@ +using System; +using System.Reactive; +using System.Reactive.Disposables; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Extensions; +using Artemis.UI.Services.Updating; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using ReactiveUI; + +namespace Artemis.UI.Screens.Settings.Updating; + +public class ReleaseInstallerViewModel : ActivatableViewModelBase +{ + private readonly ReleaseInstaller _releaseInstaller; + private readonly IWindowService _windowService; + private ObservableAsPropertyHelper? _overallProgress; + private ObservableAsPropertyHelper? _stepProgress; + private bool _ready; + private bool _restartWhenFinished; + + public ReleaseInstallerViewModel(ReleaseInstaller releaseInstaller, IWindowService windowService) + { + _releaseInstaller = releaseInstaller; + _windowService = windowService; + + Restart = ReactiveCommand.Create(() => Utilities.ApplyUpdate(false)); + this.WhenActivated(d => + { + _overallProgress = Observable.FromEventPattern(x => _releaseInstaller.OverallProgress.ProgressChanged += x, x => _releaseInstaller.OverallProgress.ProgressChanged -= x) + .Select(e => e.EventArgs) + .ToProperty(this, vm => vm.OverallProgress) + .DisposeWith(d); + _stepProgress = Observable.FromEventPattern(x => _releaseInstaller.StepProgress.ProgressChanged += x, x => _releaseInstaller.StepProgress.ProgressChanged -= x) + .Select(e => e.EventArgs) + .ToProperty(this, vm => vm.StepProgress) + .DisposeWith(d); + + Task.Run(() => InstallUpdate(d.AsCancellationToken())); + }); + } + + public ReactiveCommand Restart { get; } + + public float OverallProgress => _overallProgress?.Value ?? 0; + public float StepProgress => _stepProgress?.Value ?? 0; + + public bool Ready + { + get => _ready; + set => RaiseAndSetIfChanged(ref _ready, value); + } + + public bool RestartWhenFinished + { + get => _restartWhenFinished; + set => RaiseAndSetIfChanged(ref _restartWhenFinished, value); + } + + private async Task InstallUpdate(CancellationToken cancellationToken) + { + try + { + await _releaseInstaller.InstallAsync(cancellationToken); + Ready = true; + if (RestartWhenFinished) + Utilities.ApplyUpdate(false); + } + catch (TaskCanceledException) + { + // ignored + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Something went wrong while installing the update", e); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs b/src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs deleted file mode 100644 index 4b1950846..000000000 --- a/src/Artemis.UI/Screens/Settings/Updating/UpdateInstallationViewModel.cs +++ /dev/null @@ -1,14 +0,0 @@ -using Artemis.Core; -using Artemis.UI.Shared; - -namespace Artemis.UI.Screens.Settings.Updating; - -public class UpdateInstallationViewModel : DialogViewModelBase -{ - private readonly string _nextReleaseId; - - public UpdateInstallationViewModel(string nextReleaseId) - { - _nextReleaseId = nextReleaseId; - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs index e2a3e516a..dd72303e1 100644 --- a/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/StartupWizardViewModel.cs @@ -11,6 +11,7 @@ using Artemis.Core.Services; using Artemis.UI.DryIoc.Factories; using Artemis.UI.Screens.Plugins; using Artemis.UI.Services.Interfaces; +using Artemis.UI.Services.Updating; using Artemis.UI.Shared; using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Services; @@ -81,13 +82,12 @@ public class StartupWizardViewModel : DialogViewModelBase public ObservableCollection DeviceProviders { get; } public bool IsAutoRunSupported => _autoRunProvider != null; - public bool IsUpdatingSupported => _updateService.UpdatingSupported; public PluginSetting UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false); public PluginSetting UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15); public PluginSetting UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true); - public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.CheckForUpdates", true); - public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.AutoUpdate", false); + public PluginSetting UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true); + public PluginSetting UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", false); public int CurrentStep { @@ -119,7 +119,7 @@ public class StartupWizardViewModel : DialogViewModelBase CurrentStep--; // Skip the settings step if none of it's contents are supported - if (CurrentStep == 4 && !IsAutoRunSupported && !IsUpdatingSupported) + if (CurrentStep == 4 && !IsAutoRunSupported) CurrentStep--; SetupButtons(); @@ -131,7 +131,7 @@ public class StartupWizardViewModel : DialogViewModelBase CurrentStep++; // Skip the settings step if none of it's contents are supported - if (CurrentStep == 4 && !IsAutoRunSupported && !IsUpdatingSupported) + if (CurrentStep == 4 && !IsAutoRunSupported) CurrentStep++; SetupButtons(); diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml index 294f59e91..bf68bf234 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/SettingsStep.axaml @@ -68,7 +68,7 @@ - + Updating diff --git a/src/Artemis.UI/Services/Updating/IUpdateService.cs b/src/Artemis.UI/Services/Updating/IUpdateService.cs index 157d1ef07..5212e4eec 100644 --- a/src/Artemis.UI/Services/Updating/IUpdateService.cs +++ b/src/Artemis.UI/Services/Updating/IUpdateService.cs @@ -1,22 +1,11 @@ using System.Threading.Tasks; +using Artemis.UI.Services.Interfaces; -namespace Artemis.UI.Services.Interfaces; +namespace Artemis.UI.Services.Updating; public interface IUpdateService : IArtemisUIService { - /// - /// Gets a boolean indicating whether updating is supported. - /// - bool UpdatingSupported { get; } - - /// - /// Gets or sets a boolean indicating whether auto-updating is suspended. - /// - bool SuspendAutoCheck { get; set; } - - /// - /// Manually checks for updates and offers to install it if found. - /// - /// Whether an update was found, regardless of whether the user chose to install it. - Task ManualUpdate(); + Task CheckForUpdate(); + Task InstallRelease(string releaseId); + string? CurrentVersion { get; } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs index f868298b7..55a3e6a26 100644 --- a/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs +++ b/src/Artemis.UI/Services/Updating/ReleaseInstaller.cs @@ -1,16 +1,13 @@ using System; -using System.Drawing.Drawing2D; using System.IO; using System.IO.Compression; using System.Linq; using System.Net.Http; -using System.Reflection.Metadata; using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.UI.Extensions; using Artemis.WebClient.Updating; -using NoStringEvaluating.Functions.Math; using Octodiff.Core; using Octodiff.Diagnostics; using Serilog; @@ -19,19 +16,16 @@ using StrawberryShake; namespace Artemis.UI.Services.Updating; /// -/// Represents the installation process of a release +/// Represents the installation process of a release /// public class ReleaseInstaller { - private readonly string _releaseId; - private readonly ILogger _logger; - private readonly IUpdatingClient _updatingClient; - private readonly HttpClient _httpClient; - private readonly Platform _updatePlatform; private readonly string _dataFolder; - - public IProgress OverallProgress { get; } = new Progress(); - public IProgress StepProgress { get; } = new Progress(); + private readonly HttpClient _httpClient; + private readonly ILogger _logger; + private readonly string _releaseId; + private readonly Platform _updatePlatform; + private readonly IUpdatingClient _updatingClient; public ReleaseInstaller(string releaseId, ILogger logger, IUpdatingClient updatingClient, HttpClient httpClient) { @@ -43,7 +37,7 @@ public class ReleaseInstaller if (OperatingSystem.IsWindows()) _updatePlatform = Platform.Windows; - if (OperatingSystem.IsLinux()) + else if (OperatingSystem.IsLinux()) _updatePlatform = Platform.Linux; else if (OperatingSystem.IsMacOS()) _updatePlatform = Platform.Osx; @@ -54,9 +48,13 @@ public class ReleaseInstaller Directory.CreateDirectory(_dataFolder); } + + public Progress OverallProgress { get; } = new(); + public Progress StepProgress { get; } = new(); + public async Task InstallAsync(CancellationToken cancellationToken) { - OverallProgress.Report(0); + ((IProgress) OverallProgress).Report(0); _logger.Information("Retrieving details for release {ReleaseId}", _releaseId); IOperationResult result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken); @@ -70,7 +68,7 @@ public class ReleaseInstaller if (artifact == null) throw new Exception("Found the release but it has no artifact for the current platform"); - OverallProgress.Report(0.1f); + ((IProgress) OverallProgress).Report(10); // Determine whether the last update matches our local version, then we can download the delta if (release.PreviousRelease != null && File.Exists(Path.Combine(_dataFolder, $"{release.PreviousRelease}.zip")) && artifact.DeltaFileInfo.DownloadSize != 0) @@ -84,22 +82,26 @@ public class ReleaseInstaller await using MemoryStream stream = new(); await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}/delta", stream, StepProgress, cancellationToken); - OverallProgress.Report(0.33f); + ((IProgress) OverallProgress).Report(33); await PatchDelta(stream, previousRelease, cancellationToken); } - private async Task PatchDelta(MemoryStream deltaStream, string previousRelease, CancellationToken cancellationToken) + private async Task PatchDelta(Stream deltaStream, string previousRelease, CancellationToken cancellationToken) { await using FileStream baseStream = File.OpenRead(previousRelease); await using FileStream newFileStream = new(Path.Combine(_dataFolder, $"{_releaseId}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read); deltaStream.Seek(0, SeekOrigin.Begin); - DeltaApplier deltaApplier = new(); - deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new DeltaApplierProgressReporter(StepProgress)), newFileStream); + + await Task.Run(() => + { + DeltaApplier deltaApplier = new(); + deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new DeltaApplierProgressReporter(StepProgress)), newFileStream); + }); cancellationToken.ThrowIfCancellationRequested(); - - OverallProgress.Report(0.66f); + + ((IProgress) OverallProgress).Report(66); await Extract(newFileStream, cancellationToken); } @@ -108,21 +110,28 @@ public class ReleaseInstaller await using MemoryStream stream = new(); await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}", stream, StepProgress, cancellationToken); - OverallProgress.Report(0.5f); + ((IProgress) OverallProgress).Report(50); + await Extract(stream, cancellationToken); } - private async Task Extract(FileStream archiveStream, CancellationToken cancellationToken) + private async Task Extract(Stream archiveStream, CancellationToken cancellationToken) { // Ensure the directory is empty string extractDirectory = Path.Combine(_dataFolder, "pending"); if (Directory.Exists(extractDirectory)) Directory.Delete(extractDirectory, true); Directory.CreateDirectory(extractDirectory); + + + + await Task.Run(() => + { + archiveStream.Seek(0, SeekOrigin.Begin); + using ZipArchive archive = new(archiveStream); + archive.ExtractToDirectory(extractDirectory, false, StepProgress, cancellationToken); + }); - archiveStream.Seek(0, SeekOrigin.Begin); - using ZipArchive archive = new(archiveStream); - archive.ExtractToDirectory(extractDirectory); - OverallProgress.Report(1); + ((IProgress) OverallProgress).Report(100); } } @@ -137,6 +146,6 @@ internal class DeltaApplierProgressReporter : IProgressReporter public void ReportProgress(string operation, long currentPosition, long total) { - _stepProgress.Report((float) currentPosition / total); + _stepProgress.Report(currentPosition / total * 100); } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs b/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs index 94bcaf3be..86f8f7a91 100644 --- a/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs +++ b/src/Artemis.UI/Services/Updating/SimpleUpdateNotificationProvider.cs @@ -1,6 +1,12 @@ +using System.Threading.Tasks; + namespace Artemis.UI.Services.Updating; public class SimpleUpdateNotificationProvider : IUpdateNotificationProvider { - + /// + public async Task ShowNotification(string releaseId) + { + throw new System.NotImplementedException(); + } } \ No newline at end of file diff --git a/src/Artemis.UI/Services/Updating/UpdateService.cs b/src/Artemis.UI/Services/Updating/UpdateService.cs index 1077d549c..83f65739e 100644 --- a/src/Artemis.UI/Services/Updating/UpdateService.cs +++ b/src/Artemis.UI/Services/Updating/UpdateService.cs @@ -1,21 +1,15 @@ using System; -using System.Linq; -using System.Reactive.Disposables; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.Exceptions; using Artemis.UI.Screens.Settings.Updating; -using Artemis.UI.Services.Interfaces; using Artemis.UI.Services.Updating; -using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.MainWindow; using Artemis.WebClient.Updating; using Avalonia.Threading; -using DryIoc; using Serilog; using StrawberryShake; using Timer = System.Timers.Timer; @@ -25,17 +19,17 @@ namespace Artemis.UI.Services; public class UpdateService : IUpdateService { private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour + private readonly PluginSetting _autoCheck; + private readonly PluginSetting _autoInstall; + private readonly PluginSetting _channel; + private readonly Func _getReleaseInstaller; private readonly ILogger _logger; private readonly IMainWindowService _mainWindowService; - private readonly IWindowService _windowService; - private readonly IUpdatingClient _updatingClient; private readonly Lazy _updateNotificationProvider; - private readonly Func _getReleaseInstaller; private readonly Platform _updatePlatform; - private readonly PluginSetting _channel; - private readonly PluginSetting _autoCheck; - private readonly PluginSetting _autoInstall; + private readonly IUpdatingClient _updatingClient; + private readonly IWindowService _windowService; private bool _suspendAutoCheck; @@ -56,7 +50,7 @@ public class UpdateService : IUpdateService if (OperatingSystem.IsWindows()) _updatePlatform = Platform.Windows; - if (OperatingSystem.IsLinux()) + else if (OperatingSystem.IsLinux()) _updatePlatform = Platform.Linux; else if (OperatingSystem.IsMacOS()) _updatePlatform = Platform.Osx; @@ -72,14 +66,61 @@ public class UpdateService : IUpdateService timer.Elapsed += HandleAutoUpdateEvent; timer.Start(); } + + public string? CurrentVersion + { + get + { + object[] attributes = typeof(UpdateService).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); + return attributes.Length == 0 ? null : ((AssemblyInformationalVersionAttribute) attributes[0]).InformationalVersion; + } + } + + private async Task ShowUpdateDialog(string nextReleaseId) + { + await Dispatcher.UIThread.InvokeAsync(async () => + { + // Main window is probably already open but this will bring it into focus + _mainWindowService.OpenMainWindow(); + await _windowService.ShowDialogAsync(nextReleaseId); + }); + } + + private async Task ShowUpdateNotification(string nextReleaseId) + { + await _updateNotificationProvider.Value.ShowNotification(nextReleaseId); + } + + private async Task AutoInstallUpdate(string nextReleaseId) + { + ReleaseInstaller installer = _getReleaseInstaller(nextReleaseId); + await installer.InstallAsync(CancellationToken.None); + Utilities.ApplyUpdate(true); + } + + private async void HandleAutoUpdateEvent(object? sender, EventArgs e) + { + if (!_autoCheck.Value || _suspendAutoCheck) + return; + + try + { + await CheckForUpdate(); + } + catch (Exception ex) + { + _logger.Warning(ex, "Auto update failed"); + } + } public async Task CheckForUpdate() { - string? currentVersion = AssemblyProductVersion; + string? currentVersion = CurrentVersion; if (currentVersion == null) return false; - IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, _channel.Value, _updatePlatform); + // IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, _channel.Value, _updatePlatform); + IOperationResult result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, "feature/gh-actions", _updatePlatform); result.EnsureNoErrors(); // No update was found @@ -99,49 +140,16 @@ public class UpdateService : IUpdateService return true; } - - private async Task ShowUpdateDialog(string nextReleaseId) + + /// + public async Task InstallRelease(string releaseId) { - await Dispatcher.UIThread.InvokeAsync(async () => + ReleaseInstaller installer = _getReleaseInstaller(releaseId); + await Dispatcher.UIThread.InvokeAsync(() => { // Main window is probably already open but this will bring it into focus _mainWindowService.OpenMainWindow(); - await _windowService.ShowDialogAsync(nextReleaseId); + _windowService.ShowWindow(installer); }); } - - private async Task ShowUpdateNotification(string nextReleaseId) - { - await _updateNotificationProvider.Value.ShowNotification(nextReleaseId); - } - - private async Task AutoInstallUpdate(string nextReleaseId) - { - ReleaseInstaller installer = _getReleaseInstaller(nextReleaseId); - await installer.InstallAsync(CancellationToken.None); - } - - private async void HandleAutoUpdateEvent(object? sender, EventArgs e) - { - if (!_autoCheck.Value || _suspendAutoCheck) - return; - - try - { - await CheckForUpdate(); - } - catch (Exception ex) - { - _logger.Warning(ex, "Auto update failed"); - } - } - - private static string? AssemblyProductVersion - { - get - { - object[] attributes = typeof(UpdateService).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false); - return attributes.Length == 0 ? null : ((AssemblyInformationalVersionAttribute) attributes[0]).InformationalVersion; - } - } } \ No newline at end of file diff --git a/src/Artemis.UI/Styles/Artemis.axaml b/src/Artemis.UI/Styles/Artemis.axaml index b10f8bdb6..d92851a90 100644 --- a/src/Artemis.UI/Styles/Artemis.axaml +++ b/src/Artemis.UI/Styles/Artemis.axaml @@ -6,4 +6,5 @@ + \ No newline at end of file diff --git a/src/Artemis.UI/Styles/Markdown.axaml b/src/Artemis.UI/Styles/Markdown.axaml new file mode 100644 index 000000000..34acbe75f --- /dev/null +++ b/src/Artemis.UI/Styles/Markdown.axaml @@ -0,0 +1,191 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ## Core + * Cleaned up ProfileService render condition + * Core - Added fading in and out of profiles + * Core - Apply opacity layer only when fading + * Core - Fixed when condition stops being true mid-fade + * Core - Removed FadingStatus enum + + # General + - Meta - Fixed warnings + - Meta - Update RGB.NET + + # Plugins + - Plugins - Ignore version when loading shared assemblies + + # UI + - Sidebar - Improved category reordering code + + + + + + \ No newline at end of file diff --git a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql index cd7e8b2a8..56af7ecb5 100644 --- a/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql +++ b/src/Artemis.WebClient.Updating/Queries/GetReleaseById.graphql @@ -4,6 +4,7 @@ query GetReleaseById($id: String!) { commit version previousRelease + changelog artifacts { platform artifactId