mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge branch 'feature/updating' into feature/gh-actions
This commit is contained in:
commit
1351796f42
@ -42,9 +42,9 @@
|
||||
<PackageReference Include="LiteDB" Version="5.0.12" />
|
||||
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="RGB.NET.Core" Version="1.0.0" />
|
||||
<PackageReference Include="RGB.NET.Layout" Version="1.0.0" />
|
||||
<PackageReference Include="RGB.NET.Presets" Version="1.0.0" />
|
||||
<PackageReference Include="RGB.NET.Core" Version="2.0.0-prerelease.12" />
|
||||
<PackageReference Include="RGB.NET.Layout" Version="2.0.0-prerelease.12" />
|
||||
<PackageReference Include="RGB.NET.Presets" Version="2.0.0-prerelease.12" />
|
||||
<PackageReference Include="Serilog" Version="2.11.0" />
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
|
||||
|
||||
@ -13,7 +13,7 @@ namespace Artemis.Core.DryIoc;
|
||||
/// <summary>
|
||||
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
||||
/// </summary>
|
||||
public static class CoreContainerExtensions
|
||||
public static class ContainerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers core services into the container.
|
||||
|
||||
@ -9,4 +9,6 @@ public interface IQueuedActionRepository : IRepository
|
||||
void Remove(QueuedActionEntity queuedActionEntity);
|
||||
List<QueuedActionEntity> GetAll();
|
||||
List<QueuedActionEntity> GetByType(string type);
|
||||
bool IsTypeQueued(string type);
|
||||
void ClearByType(string type);
|
||||
}
|
||||
@ -41,5 +41,17 @@ public class QueuedActionRepository : IQueuedActionRepository
|
||||
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsTypeQueued(string type)
|
||||
{
|
||||
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).Count() > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearByType(string type)
|
||||
{
|
||||
_repository.DeleteMany<QueuedActionEntity>(q => q.Type == type);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -20,7 +20,7 @@
|
||||
<PackageReference Include="Material.Icons.Avalonia" Version="1.1.10" />
|
||||
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
||||
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
||||
<PackageReference Include="RGB.NET.Core" Version="1.0.0" />
|
||||
<PackageReference Include="RGB.NET.Core" Version="2.0.0-prerelease.12" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.108" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
|
||||
34
src/Artemis.UI.Shared/Converters/BytesToStringConverter.cs
Normal file
34
src/Artemis.UI.Shared/Converters/BytesToStringConverter.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using Humanizer;
|
||||
using Humanizer.Bytes;
|
||||
|
||||
namespace Artemis.UI.Shared.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Converts bytes to a string
|
||||
/// </summary>
|
||||
public class BytesToStringConverter : IValueConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is int intBytes)
|
||||
return intBytes.Bytes().Humanize();
|
||||
if (value is long longBytes)
|
||||
return longBytes.Bytes().Humanize();
|
||||
if (value is double doubleBytes)
|
||||
return doubleBytes.Bytes().Humanize();
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string formatted && ByteSize.TryParse(formatted, out ByteSize result))
|
||||
return result.Bytes;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,7 @@ namespace Artemis.UI.Shared.DryIoc;
|
||||
/// <summary>
|
||||
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
||||
/// </summary>
|
||||
public static class UIContainerExtensions
|
||||
public static class ContainerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers shared UI services into the container.
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -56,7 +56,7 @@ public class DuplicateNode : INodeEditorCommand, IDisposable
|
||||
if (targetCollection == null)
|
||||
continue;
|
||||
while (targetCollection.Count() < sourceCollection.Count())
|
||||
targetCollection.CreatePin();
|
||||
targetCollection.Add(targetCollection.CreatePin());
|
||||
}
|
||||
|
||||
// Copy the storage
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
|
||||
<!-- Custom styles -->
|
||||
<StyleInclude Source="/Styles/Border.axaml" />
|
||||
<StyleInclude Source="/Styles/Skeleton.axaml" />
|
||||
<StyleInclude Source="/Styles/Button.axaml" />
|
||||
<StyleInclude Source="/Styles/Condensed.axaml" />
|
||||
<StyleInclude Source="/Styles/ColorPickerButton.axaml" />
|
||||
|
||||
174
src/Artemis.UI.Shared/Styles/Skeleton.axaml
Normal file
174
src/Artemis.UI.Shared/Styles/Skeleton.axaml
Normal file
@ -0,0 +1,174 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Design.PreviewWith>
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Margin="20" Grid.Column="0">
|
||||
<StackPanel>
|
||||
<TextBlock Classes="h1">This is heading 1</TextBlock>
|
||||
<TextBlock Classes="h2">This is heading 2</TextBlock>
|
||||
<TextBlock Classes="h3">This is heading 3</TextBlock>
|
||||
<TextBlock Classes="h4">This is heading 4</TextBlock>
|
||||
<TextBlock Classes="h5">This is heading 5</TextBlock>
|
||||
<TextBlock Classes="h6">This is heading 6</TextBlock>
|
||||
<TextBlock>This is regular text</TextBlock>
|
||||
<TextBlock>This is regular text</TextBlock>
|
||||
<TextBlock>This is regular text</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Margin="20" Grid.Column="1">
|
||||
<StackPanel>
|
||||
<Border Width="400" Classes="skeleton-text h1"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h2"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h3"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h4"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h5"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h6"></Border>
|
||||
<Border Width="400" Classes="skeleton-text"></Border>
|
||||
<Border Width="400" Classes="skeleton-text"></Border>
|
||||
<Border Width="400" Classes="skeleton-text"></Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel>
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="Background" Value="#55ff0000"></Setter>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<TextBlock Classes="h1">This is heading 1</TextBlock>
|
||||
<TextBlock Classes="h2">This is heading 2</TextBlock>
|
||||
<TextBlock Classes="h3">This is heading 3</TextBlock>
|
||||
<TextBlock Classes="h4">This is heading 4</TextBlock>
|
||||
<TextBlock Classes="h5">This is heading 5</TextBlock>
|
||||
<TextBlock Classes="h6">This is heading 6</TextBlock>
|
||||
<TextBlock>This is regular text</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Margin="20" Grid.Column="0" Row="1">
|
||||
<StackPanel Spacing="2">
|
||||
<Border Width="400" Classes="skeleton-text h1 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h2 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h3 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h4 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h5 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h6 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text no-margin"></Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="2">
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="Background" Value="#55ff0000"></Setter>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<TextBlock Classes="h1 no-margin">This is heading 1</TextBlock>
|
||||
<TextBlock Classes="h2 no-margin">This is heading 2</TextBlock>
|
||||
<TextBlock Classes="h3 no-margin">This is heading 3</TextBlock>
|
||||
<TextBlock Classes="h4 no-margin">This is heading 4</TextBlock>
|
||||
<TextBlock Classes="h5 no-margin">This is heading 5</TextBlock>
|
||||
<TextBlock Classes="h6 no-margin">This is heading 6</TextBlock>
|
||||
<TextBlock>This is regular text</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<Styles.Resources>
|
||||
<CornerRadius x:Key="CardCornerRadius">8</CornerRadius>
|
||||
</Styles.Resources>
|
||||
|
||||
<Style Selector="Border.skeleton-text">
|
||||
<Setter Property="Height" Value="17"></Setter>
|
||||
<Setter Property="Margin" Value="0 1" />
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
|
||||
<Style.Animations>
|
||||
<Animation Duration="0:0:1.5" IterationCount="Infinite" PlaybackDirection="Normal">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<LinearGradientBrush StartPoint="-100%,-100%" EndPoint="0%,0%">
|
||||
<LinearGradientBrush.GradientStops>
|
||||
<GradientStop Offset="0" Color="Gray" />
|
||||
<GradientStop Offset="0.4" Color="#595959" />
|
||||
<GradientStop Offset="0.6" Color="#595959" />
|
||||
<GradientStop Offset="1" Color="Gray" />
|
||||
</LinearGradientBrush.GradientStops>
|
||||
</LinearGradientBrush>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<LinearGradientBrush StartPoint="100%,100%" EndPoint="200%,200%">
|
||||
<LinearGradientBrush.GradientStops>
|
||||
<GradientStop Offset="0" Color="Gray" />
|
||||
<GradientStop Offset="0.4" Color="#595959" />
|
||||
<GradientStop Offset="0.6" Color="#595959" />
|
||||
<GradientStop Offset="1" Color="Gray" />
|
||||
</LinearGradientBrush.GradientStops>
|
||||
</LinearGradientBrush>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.skeleton-text.h1">
|
||||
<Setter Property="Height" Value="65" />
|
||||
<Setter Property="Margin" Value="0 10 0 20" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h2">
|
||||
<Setter Property="Height" Value="44" />
|
||||
<Setter Property="Margin" Value="0 10 0 20" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h3">
|
||||
<Setter Property="Height" Value="33" />
|
||||
<Setter Property="Margin" Value="0 5 0 15" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h4">
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="Margin" Value="0 2 0 12" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h5">
|
||||
<Setter Property="Height" Value="20" />
|
||||
<Setter Property="Margin" Value="0 2 0 7" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h6">
|
||||
<Setter Property="Height" Value="15" />
|
||||
<Setter Property="Margin" Value="0 2 0 4" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.skeleton-text.h1.no-margin">
|
||||
<Setter Property="Margin" Value="0 10 0 10" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h2.no-margin">
|
||||
<Setter Property="Margin" Value="0 10 0 10" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h3.no-margin">
|
||||
<Setter Property="Margin" Value="0 5 0 5" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h4.no-margin">
|
||||
<Setter Property="Margin" Value="0 2 0 2" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h5.no-margin">
|
||||
<Setter Property="Margin" Value="0 2 0 2" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h6.no-margin">
|
||||
<Setter Property="Margin" Value="0 2 0 2" />
|
||||
</Style>
|
||||
|
||||
</Styles>
|
||||
@ -108,8 +108,6 @@ public class ApplicationStateManager
|
||||
ProcessStartInfo info = new()
|
||||
{
|
||||
Arguments = $"-File {script} {source} {destination} {args}",
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
CreateNoWindow = true,
|
||||
FileName = "PowerShell.exe"
|
||||
};
|
||||
Process.Start(info);
|
||||
|
||||
@ -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>();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,165 @@
|
||||
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;
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
|
||||
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 == "cancel")
|
||||
_cancellationTokenSource?.Cancel();
|
||||
else if (action == "restart-for-update")
|
||||
_updateService.RestartForUpdate(false);
|
||||
}
|
||||
|
||||
public void ShowNotification(string releaseId, string releaseVersion)
|
||||
{
|
||||
GetBuilderForRelease(releaseId, 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();
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
private async Task InstallRelease(string releaseId, string releaseVersion)
|
||||
{
|
||||
ReleaseInstaller installer = _getReleaseInstaller(releaseId);
|
||||
void InstallerOnPropertyChanged(object? sender, PropertyChangedEventArgs e) => UpdateInstallProgress(releaseId, installer);
|
||||
|
||||
GetBuilderForRelease(releaseId, 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 = GetDataForInstaller(installer);
|
||||
});
|
||||
|
||||
// Wait for Windows animations to catch up to us, we fast!
|
||||
await Task.Delay(2000);
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
installer.PropertyChanged += InstallerOnPropertyChanged;
|
||||
try
|
||||
{
|
||||
await installer.InstallAsync(_cancellationTokenSource.Token);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
if (_cancellationTokenSource.IsCancellationRequested)
|
||||
return;
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
installer.PropertyChanged -= InstallerOnPropertyChanged;
|
||||
}
|
||||
|
||||
// Queue an update in case the user interrupts the process after everything has been prepared
|
||||
_updateService.QueueUpdate();
|
||||
|
||||
GetBuilderForRelease(releaseId, 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(GetDataForInstaller(installer), releaseId);
|
||||
}
|
||||
|
||||
private ToastContentBuilder GetBuilderForRelease(string releaseId, string releaseVersion)
|
||||
{
|
||||
return new ToastContentBuilder().AddArgument("releaseId", releaseId).AddArgument("releaseVersion", releaseVersion);
|
||||
}
|
||||
|
||||
private NotificationData GetDataForInstaller(ReleaseInstaller installer)
|
||||
{
|
||||
NotificationData data = new()
|
||||
{
|
||||
Values =
|
||||
{
|
||||
["progressValue"] = (installer.Progress / 100f).ToString(CultureInfo.InvariantCulture),
|
||||
["progressStatus"] = installer.Status
|
||||
}
|
||||
};
|
||||
|
||||
return data;
|
||||
}
|
||||
}
|
||||
@ -4,6 +4,10 @@ param (
|
||||
[Parameter(Mandatory=$false)][string]$artemisArgs
|
||||
)
|
||||
|
||||
Write-Host "Artemis update script v1"
|
||||
Write-Host "Please do not close this window, this should not take long"
|
||||
Write-Host ""
|
||||
|
||||
# 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
|
||||
@ -26,12 +30,17 @@ if (!(Test-Path $destinationDirectory)) {
|
||||
Write-Error "The destination directory does not exist"
|
||||
}
|
||||
|
||||
# If the destination directory exists, clear it
|
||||
# Clear the destination directory but don't remove it, leaving ACL entries in tact
|
||||
Write-Host "Cleaning up old version where needed"
|
||||
Get-ChildItem $destinationDirectory | Remove-Item -Recurse -Force
|
||||
|
||||
# Move the contents of the source directory to the destination directory
|
||||
Write-Host "Installing new files"
|
||||
Get-ChildItem $sourceDirectory | Move-Item -Destination $destinationDirectory
|
||||
# Remove the now empty source directory
|
||||
Remove-Item $sourceDirectory
|
||||
|
||||
Write-Host "Finished! Restarting Artemis"
|
||||
Start-Sleep -Seconds 1
|
||||
|
||||
# When finished, run the updated version
|
||||
|
||||
@ -34,8 +34,8 @@
|
||||
<PackageReference Include="Octopus.Octodiff" Version="2.0.100" />
|
||||
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
||||
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
||||
<PackageReference Include="RGB.NET.Core" Version="1.0.0" />
|
||||
<PackageReference Include="RGB.NET.Layout" Version="1.0.0" />
|
||||
<PackageReference Include="RGB.NET.Core" Version="2.0.0-prerelease.12" />
|
||||
<PackageReference Include="RGB.NET.Layout" Version="2.0.0-prerelease.12" />
|
||||
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.108" />
|
||||
<PackageReference Include="Splat.DryIoc" Version="14.6.1" />
|
||||
</ItemGroup>
|
||||
@ -43,4 +43,15 @@
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="Assets\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Update="Screens\Settings\Tabs\ReleasesTabView.axaml.cs">
|
||||
<DependentUpon>UpdatingTabView.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
<Compile Update="Screens\Settings\Updating\ReleaseView.axaml.cs">
|
||||
<DependentUpon>UpdatingTabView.axaml</DependentUpon>
|
||||
<SubType>Code</SubType>
|
||||
</Compile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@ -17,7 +17,7 @@ namespace Artemis.UI.DryIoc;
|
||||
/// <summary>
|
||||
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
||||
/// </summary>
|
||||
public static class UIContainerExtensions
|
||||
public static class ContainerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers UI services into the container.
|
||||
@ -25,19 +25,18 @@ public static class UIContainerExtensions
|
||||
/// <param name="container">The builder building the current container</param>
|
||||
public static void RegisterUI(this IContainer container)
|
||||
{
|
||||
Assembly[] thisAssembly = {typeof(UIContainerExtensions).Assembly};
|
||||
Assembly[] thisAssembly = {typeof(ContainerExtensions).Assembly};
|
||||
|
||||
container.RegisterInstance(new AssetLoader(), IfAlreadyRegistered.Throw);
|
||||
container.Register<IAssetLoader, AssetLoader>(Reuse.Singleton);
|
||||
|
||||
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<ViewModelBase>());
|
||||
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<MainScreenViewModel>(), ifAlreadyRegistered: IfAlreadyRegistered.Replace);
|
||||
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IToolViewModel>() && type.IsInterface);
|
||||
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IVmFactory>() && type != typeof(PropertyVmFactory));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reactive;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.LayerBrushes;
|
||||
@ -17,6 +18,7 @@ using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
|
||||
using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers;
|
||||
using Artemis.UI.Screens.Scripting;
|
||||
using Artemis.UI.Screens.Settings;
|
||||
using Artemis.UI.Screens.Settings.Updating;
|
||||
using Artemis.UI.Screens.Sidebar;
|
||||
using Artemis.UI.Screens.SurfaceEditor;
|
||||
using Artemis.UI.Screens.VisualScripting;
|
||||
@ -475,3 +477,22 @@ public class ScriptVmFactory : IScriptVmFactory
|
||||
return _container.Resolve<ScriptConfigurationViewModel>(new object[] { profile, scriptConfiguration });
|
||||
}
|
||||
}
|
||||
|
||||
public interface IReleaseVmFactory : IVmFactory
|
||||
{
|
||||
ReleaseViewModel ReleaseListViewModel(string releaseId, string version, DateTimeOffset createdAt);
|
||||
}
|
||||
public class ReleaseVmFactory : IReleaseVmFactory
|
||||
{
|
||||
private readonly IContainer _container;
|
||||
|
||||
public ReleaseVmFactory(IContainer container)
|
||||
{
|
||||
_container = container;
|
||||
}
|
||||
|
||||
public ReleaseViewModel ReleaseListViewModel(string releaseId, string version, DateTimeOffset createdAt)
|
||||
{
|
||||
return _container.Resolve<ReleaseViewModel>(new object[] { releaseId, version, createdAt });
|
||||
}
|
||||
}
|
||||
@ -14,7 +14,7 @@ namespace Artemis.UI.Screens.Debugger.Logs;
|
||||
public class LogsDebugView : ReactiveUserControl<LogsDebugViewModel>
|
||||
{
|
||||
private int _lineCount;
|
||||
private TextEditor _textEditor;
|
||||
private TextEditor? _textEditor;
|
||||
|
||||
public LogsDebugView()
|
||||
{
|
||||
@ -31,7 +31,7 @@ public class LogsDebugView : ReactiveUserControl<LogsDebugViewModel>
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
base.OnInitialized();
|
||||
Dispatcher.UIThread.Post(() => _textEditor.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
|
||||
Dispatcher.UIThread.Post(() => _textEditor?.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
|
||||
}
|
||||
|
||||
private void OnTextChanged(object? sender, EventArgs e)
|
||||
@ -49,7 +49,7 @@ public class LogsDebugView : ReactiveUserControl<LogsDebugViewModel>
|
||||
//we need this help distance because of rounding.
|
||||
//if we scroll slightly above the end, we still want it
|
||||
//to scroll down to the new lines.
|
||||
const double graceDistance = 1d;
|
||||
const double GRACE_DISTANCE = 1d;
|
||||
|
||||
//if we were at the bottom of the log and
|
||||
//if the last log event was 5 lines long
|
||||
@ -59,7 +59,7 @@ public class LogsDebugView : ReactiveUserControl<LogsDebugViewModel>
|
||||
//if we are more than that out of sync,
|
||||
//the user scrolled up and we should not
|
||||
//mess with anything.
|
||||
if (_lineCount == 0 || linesAdded + graceDistance > outOfScreenLines)
|
||||
if (_lineCount == 0 || linesAdded + GRACE_DISTANCE > outOfScreenLines)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => _textEditor.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
|
||||
_lineCount = _textEditor.LineCount;
|
||||
|
||||
@ -58,6 +58,7 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
||||
_lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!;
|
||||
|
||||
mainWindowService.ConfigureMainWindowProvider(this);
|
||||
mainWindowService.HostScreen = this;
|
||||
|
||||
DisplayAccordingToSettings();
|
||||
Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel);
|
||||
@ -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
|
||||
|
||||
@ -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}" />
|
||||
|
||||
@ -6,10 +6,13 @@ namespace Artemis.UI.Screens.Settings;
|
||||
|
||||
public class SettingsViewModel : MainScreenViewModel
|
||||
{
|
||||
private ActivatableViewModelBase _selectedTab;
|
||||
|
||||
public SettingsViewModel(IScreen hostScreen,
|
||||
GeneralTabViewModel generalTabViewModel,
|
||||
PluginsTabViewModel pluginsTabViewModel,
|
||||
DevicesTabViewModel devicesTabViewModel,
|
||||
ReleasesTabViewModel releasesTabViewModel,
|
||||
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
|
||||
{
|
||||
SettingTabs = new ObservableCollection<ActivatableViewModelBase>
|
||||
@ -17,9 +20,17 @@ public class SettingsViewModel : MainScreenViewModel
|
||||
generalTabViewModel,
|
||||
pluginsTabViewModel,
|
||||
devicesTabViewModel,
|
||||
releasesTabViewModel,
|
||||
aboutTabViewModel
|
||||
};
|
||||
_selectedTab = generalTabViewModel;
|
||||
}
|
||||
|
||||
public ObservableCollection<ActivatableViewModelBase> SettingTabs { get; }
|
||||
|
||||
public ActivatableViewModelBase SelectedTab
|
||||
{
|
||||
get => _selectedTab;
|
||||
set => RaiseAndSetIfChanged(ref _selectedTab, value);
|
||||
}
|
||||
}
|
||||
26
src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml
Normal file
26
src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml
Normal file
@ -0,0 +1,26 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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"
|
||||
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
|
||||
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
|
||||
x:Class="Artemis.UI.Screens.Settings.ReleasesTabView"
|
||||
x:DataType="settings:ReleasesTabViewModel">
|
||||
<Grid ColumnDefinitions="300,*" Margin="0 10">
|
||||
<Border Classes="card-condensed" Grid.Column="0" Margin="0 0 10 0">
|
||||
<ListBox Items="{CompiledBinding ReleaseViewModels}" SelectedItem="{CompiledBinding SelectedReleaseViewModel}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="updating:ReleaseViewModel">
|
||||
<StackPanel Margin="4">
|
||||
<TextBlock Text="{CompiledBinding Version}" VerticalAlignment="Center" />
|
||||
<TextBlock Text="{CompiledBinding CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
|
||||
<ContentControl Grid.Column="1" Content="{CompiledBinding SelectedReleaseViewModel}"/>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@ -0,0 +1,17 @@
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Settings;
|
||||
|
||||
public class ReleasesTabView : ReactiveUserControl<ReleasesTabViewModel>
|
||||
{
|
||||
public ReleasesTabView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
107
src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs
Normal file
107
src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs
Normal file
@ -0,0 +1,107 @@
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.UI.DryIoc.Factories;
|
||||
using Artemis.UI.Extensions;
|
||||
using Artemis.UI.Screens.Settings.Updating;
|
||||
using Artemis.UI.Services.Updating;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.UI.Shared.Services.Builders;
|
||||
using Artemis.WebClient.Updating;
|
||||
using Avalonia.Threading;
|
||||
using DynamicData;
|
||||
using DynamicData.Binding;
|
||||
using ReactiveUI;
|
||||
using Serilog;
|
||||
using StrawberryShake;
|
||||
|
||||
namespace Artemis.UI.Screens.Settings;
|
||||
|
||||
public class ReleasesTabViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IUpdatingClient _updatingClient;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly SourceList<IGetReleases_PublishedReleases_Nodes> _releases;
|
||||
private IGetReleases_PublishedReleases_PageInfo? _lastPageInfo;
|
||||
private bool _loading;
|
||||
private ReleaseViewModel? _selectedReleaseViewModel;
|
||||
|
||||
public ReleasesTabViewModel(ILogger logger, IUpdateService updateService, IUpdatingClient updatingClient, IReleaseVmFactory releaseVmFactory, INotificationService notificationService)
|
||||
{
|
||||
_logger = logger;
|
||||
_updatingClient = updatingClient;
|
||||
_notificationService = notificationService;
|
||||
|
||||
_releases = new SourceList<IGetReleases_PublishedReleases_Nodes>();
|
||||
_releases.Connect()
|
||||
.Sort(SortExpressionComparer<IGetReleases_PublishedReleases_Nodes>.Descending(p => p.CreatedAt))
|
||||
.Transform(r => releaseVmFactory.ReleaseListViewModel(r.Id, r.Version, r.CreatedAt))
|
||||
.ObserveOn(AvaloniaScheduler.Instance)
|
||||
.Bind(out ReadOnlyObservableCollection<ReleaseViewModel> releaseViewModels)
|
||||
.Subscribe();
|
||||
|
||||
DisplayName = "Releases";
|
||||
ReleaseViewModels = releaseViewModels;
|
||||
this.WhenActivated(async d =>
|
||||
{
|
||||
await updateService.CacheLatestRelease();
|
||||
await GetMoreReleases(d.AsCancellationToken());
|
||||
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(r => r.ReleaseId == PreselectId) ?? ReleaseViewModels.FirstOrDefault();
|
||||
});
|
||||
}
|
||||
|
||||
public ReadOnlyObservableCollection<ReleaseViewModel> ReleaseViewModels { get; }
|
||||
public string? PreselectId { get; set; }
|
||||
|
||||
public ReleaseViewModel? SelectedReleaseViewModel
|
||||
{
|
||||
get => _selectedReleaseViewModel;
|
||||
set => RaiseAndSetIfChanged(ref _selectedReleaseViewModel, value);
|
||||
}
|
||||
|
||||
public bool Loading
|
||||
{
|
||||
get => _loading;
|
||||
private set => RaiseAndSetIfChanged(ref _loading, value);
|
||||
}
|
||||
|
||||
public async Task GetMoreReleases(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_lastPageInfo != null && !_lastPageInfo.HasNextPage)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Loading = true;
|
||||
|
||||
IOperationResult<IGetReleasesResult> result = await _updatingClient.GetReleases.ExecuteAsync("feature/gh-actions", Platform.Windows, 20, _lastPageInfo?.EndCursor, cancellationToken);
|
||||
if (result.Data?.PublishedReleases?.Nodes == null)
|
||||
return;
|
||||
|
||||
_lastPageInfo = result.Data.PublishedReleases.PageInfo;
|
||||
_releases.AddRange(result.Data.PublishedReleases.Nodes);
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// ignored
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Warning(e, "Failed to retrieve releases");
|
||||
_notificationService.CreateNotification()
|
||||
.WithTitle("Failed to retrieve releases")
|
||||
.WithMessage(e.Message)
|
||||
.WithSeverity(NotificationSeverity.Warning)
|
||||
.Show();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
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<ReleaseAvailableViewModel>
|
||||
{
|
||||
public ReleaseAvailableView()
|
||||
{
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void Button_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
@ -1,76 +0,0 @@
|
||||
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<IGetReleaseByIdResult> 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<Unit, Unit> Install { get; }
|
||||
public ReactiveCommand<Unit, Unit> AskLater { get; }
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
<controls:CoreWindow xmlns="https://github.com/avaloniaui"
|
||||
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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
|
||||
mc:Ignorable="d"
|
||||
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseInstallerView"
|
||||
x:DataType="updating:ReleaseInstallerViewModel"
|
||||
Title="Artemis | Updating"
|
||||
ShowAsDialog="True"
|
||||
Width="465" Height="260"
|
||||
Padding="15"
|
||||
CanResize="False"
|
||||
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto" Width="450" Height="200">
|
||||
<TextBlock Grid.Row="0" Classes="h4" TextWrapping="Wrap">
|
||||
Downloading & installing update...
|
||||
</TextBlock>
|
||||
|
||||
<TextBlock Grid.Row="1" Classes="subtitle" TextWrapping="Wrap">
|
||||
This should not take long, when finished Artemis must restart.
|
||||
</TextBlock>
|
||||
|
||||
<StackPanel Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Stretch" IsVisible="{CompiledBinding !Ready}">
|
||||
<ProgressBar Value="{CompiledBinding OverallProgress}" ></ProgressBar>
|
||||
<ProgressBar Margin="0 15 0 0" Value="{CompiledBinding StepProgress}"></ProgressBar>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="2" IsVisible="{CompiledBinding Ready}" VerticalAlignment="Top" Margin="0 15 0 0">Done, click restart to apply the update 🫡</TextBlock>
|
||||
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Spacing="15" Grid.Row="3" Margin="0 15 0 0" Height="30">
|
||||
<CheckBox IsVisible="{CompiledBinding !Ready}" IsChecked="{CompiledBinding RestartWhenFinished}">Restart when finished</CheckBox>
|
||||
<Button IsVisible="{CompiledBinding Ready}" Command="{CompiledBinding Restart}" Classes="accent">Restart</Button>
|
||||
<Button Click="Cancel_OnClick">Cancel</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
</controls:CoreWindow>
|
||||
@ -1,28 +0,0 @@
|
||||
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<ReleaseInstallerViewModel>
|
||||
{
|
||||
public ReleaseInstallerView()
|
||||
{
|
||||
InitializeComponent();
|
||||
#if DEBUG
|
||||
this.AttachDevTools();
|
||||
#endif
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void Cancel_OnClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
Close();
|
||||
}
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
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<float>? _overallProgress;
|
||||
private ObservableAsPropertyHelper<float>? _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<float>(x => _releaseInstaller.OverallProgress.ProgressChanged += x, x => _releaseInstaller.OverallProgress.ProgressChanged -= x)
|
||||
.Select(e => e.EventArgs)
|
||||
.ToProperty(this, vm => vm.OverallProgress)
|
||||
.DisposeWith(d);
|
||||
_stepProgress = Observable.FromEventPattern<float>(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<Unit, Unit> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,49 +1,163 @@
|
||||
<controls:CoreWindow xmlns="https://github.com/avaloniaui"
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
|
||||
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
|
||||
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
xmlns:mdc="clr-namespace:Markdown.Avalonia.Controls;assembly=Markdown.Avalonia"
|
||||
xmlns:mde="clr-namespace:Markdown.Avalonia.Extensions;assembly=Markdown.Avalonia"
|
||||
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
|
||||
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseAvailableView"
|
||||
x:DataType="updating:ReleaseAvailableViewModel"
|
||||
Title="Artemis | Update available"
|
||||
Width="750"
|
||||
Height="750"
|
||||
WindowStartupLocation="CenterOwner">
|
||||
<Grid Margin="15" RowDefinitions="Auto,*,Auto">
|
||||
xmlns:avalonia1="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
|
||||
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
|
||||
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseView"
|
||||
x:DataType="updating:ReleaseViewModel">
|
||||
<UserControl.Resources>
|
||||
<converters:BytesToStringConverter x:Key="BytesToStringConverter" />
|
||||
</UserControl.Resources>
|
||||
<UserControl.Styles>
|
||||
<Style Selector=":is(Control).fade-in">
|
||||
<Setter Property="Opacity" Value="0"></Setter>
|
||||
</Style>
|
||||
<Style Selector=":is(Control).fade-in[IsVisible=True]">
|
||||
<Style.Animations>
|
||||
<Animation Duration="0:00:00.250" FillMode="Forward" Easing="CubicEaseInOut">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="Opacity" Value="0.0" />
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="Opacity" Value="1.0" />
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
<Style Selector="Grid.info-container">
|
||||
<Setter Property="Margin" Value="10" />
|
||||
</Style>
|
||||
<Style Selector="avalonia1|MaterialIcon.info-icon">
|
||||
<Setter Property="VerticalAlignment" Value="Top" />
|
||||
<Setter Property="Margin" Value="0 3 10 0" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.info-title">
|
||||
<Setter Property="Margin" Value="0 0 0 5" />
|
||||
<Setter Property="Opacity" Value="0.8" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.info-body">
|
||||
</Style>
|
||||
<Style Selector="TextBlock.info-link">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight3}" />
|
||||
</Style>
|
||||
<Style Selector="TextBlock.info-link:pointerover">
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight1}" />
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<TextBlock Grid.Row="0" Classes="h4">
|
||||
A new Artemis update is available! 🥳
|
||||
<Grid RowDefinitions="Auto,*" IsVisible="{CompiledBinding !Loading}" Classes="fade-in">
|
||||
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
|
||||
<StackPanel>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Classes="h4 no-margin">Release info</TextBlock>
|
||||
|
||||
<Panel Grid.Column="1" IsVisible="{CompiledBinding InstallationAvailable}">
|
||||
<!-- Install progress -->
|
||||
<Grid ColumnDefinitions="*,*"
|
||||
RowDefinitions="*,*"
|
||||
IsVisible="{CompiledBinding InstallationInProgress}">
|
||||
<ProgressBar Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Width="300"
|
||||
Value="{CompiledBinding ReleaseInstaller.Progress, FallbackValue=0}">
|
||||
</ProgressBar>
|
||||
<TextBlock Grid.Column="0"
|
||||
Grid.Row="1"
|
||||
Classes="subtitle"
|
||||
TextAlignment="Right"
|
||||
Text="{CompiledBinding ReleaseInstaller.Status, FallbackValue=Installing}" />
|
||||
<Button Grid.Column="1" Grid.Row="0" Grid.RowSpan="2"
|
||||
Classes="accent"
|
||||
Margin="15 0 0 0"
|
||||
Width="80"
|
||||
VerticalAlignment="Center"
|
||||
Command="{CompiledBinding CancelInstall}">
|
||||
Cancel
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Panel IsVisible="{CompiledBinding !InstallationInProgress}" HorizontalAlignment="Right">
|
||||
<!-- Install button -->
|
||||
<Button Classes="accent"
|
||||
Width="80"
|
||||
Command="{CompiledBinding Install}"
|
||||
IsVisible="{CompiledBinding !InstallationFinished}">
|
||||
Install
|
||||
</Button>
|
||||
|
||||
<!-- Restart button -->
|
||||
<Grid ColumnDefinitions="*,*" IsVisible="{CompiledBinding InstallationFinished}">
|
||||
<TextBlock Grid.Column="0"
|
||||
Grid.Row="0"
|
||||
Classes="subtitle"
|
||||
TextAlignment="Right"
|
||||
VerticalAlignment="Center">
|
||||
Ready, restart to install
|
||||
</TextBlock>
|
||||
<Button Grid.Column="1" Grid.Row="0"
|
||||
Classes="accent"
|
||||
Margin="15 0 0 0"
|
||||
Width="80"
|
||||
Command="{CompiledBinding Restart}"
|
||||
IsVisible="{CompiledBinding InstallationFinished}">
|
||||
Restart
|
||||
</Button>
|
||||
</Grid>
|
||||
</Panel>
|
||||
</Panel>
|
||||
|
||||
<StackPanel Grid.Row="1" IsVisible="{CompiledBinding Release, Converter={x:Static ObjectConverters.IsNull}}" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<TextBlock>Retrieving release...</TextBlock>
|
||||
<ProgressBar IsIndeterminate="True"></ProgressBar>
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Row="1" Classes="card" IsVisible="{CompiledBinding Release, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<StackPanel Grid.Row="0">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<TextBlock Text="You are currently running version " />
|
||||
<TextBlock Text="{CompiledBinding CurrentVersion, Mode=OneWay}"></TextBlock>
|
||||
<TextBlock Text=" while the latest build is " />
|
||||
<TextBlock Text="{CompiledBinding Release.Version, Mode=OneWay, FallbackValue='Unknown'}"></TextBlock>
|
||||
<TextBlock Text="." />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Text="Updating Artemis will give you the latest bug(fixes), features and improvements." />
|
||||
</Grid>
|
||||
<Separator Classes="card-separator" />
|
||||
</StackPanel>
|
||||
<Grid Margin="-5 -10" ColumnDefinitions="*,*,*">
|
||||
<Grid Grid.Column="0" ColumnDefinitions="*,*" RowDefinitions="*,*,*" Classes="info-container" HorizontalAlignment="Left">
|
||||
<avalonia1:MaterialIcon Kind="Calendar" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Release date</TextBlock>
|
||||
<TextBlock Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Classes="info-body"
|
||||
Text="{CompiledBinding CreatedAt, StringFormat={}{0:g}, FallbackValue=Loading...}" />
|
||||
</Grid>
|
||||
|
||||
<avalonia:MarkdownScrollViewer Grid.Row="1"
|
||||
VerticalAlignment="Top"
|
||||
Markdown="{CompiledBinding Release.Changelog}">
|
||||
<Grid Grid.Column="1" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Center">
|
||||
<avalonia1:MaterialIcon Kind="Git" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Source</TextBlock>
|
||||
<TextBlock Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Classes="info-body info-link"
|
||||
Cursor="Hand"
|
||||
PointerReleased="InputElement_OnPointerReleased"
|
||||
Text="{CompiledBinding ShortCommit, FallbackValue=Loading...}" />
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Column="2" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Right">
|
||||
<avalonia1:MaterialIcon Kind="BoxOutline" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">File size</TextBlock>
|
||||
<TextBlock Grid.Column="1"
|
||||
Grid.Row="1"
|
||||
Classes="info-body"
|
||||
Text="{CompiledBinding FileSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay, FallbackValue=Loading...}" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Row="1" Classes="card">
|
||||
<Grid RowDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Row="0" Classes="h5 no-margin">Release notes</TextBlock>
|
||||
<Separator Grid.Row="1" Classes="card-separator" />
|
||||
|
||||
<avalonia:MarkdownScrollViewer Grid.Row="2" Markdown="{CompiledBinding Changelog}">
|
||||
<avalonia:MarkdownScrollViewer.Styles>
|
||||
<Style Selector="ctxt|CTextBlock">
|
||||
<Style.Setters>
|
||||
@ -207,10 +321,6 @@
|
||||
</avalonia:MarkdownScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Spacing="5" Grid.Row="2" Margin="0 15 0 0">
|
||||
<Button Classes="accent" Command="{CompiledBinding Install}" Click="Button_OnClick">Install update</Button>
|
||||
<Button Click="Button_OnClick">Ask later</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</controls:CoreWindow>
|
||||
|
||||
</UserControl>
|
||||
@ -0,0 +1,23 @@
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Settings.Updating;
|
||||
|
||||
public class ReleaseView : ReactiveUserControl<ReleaseViewModel>
|
||||
{
|
||||
public ReleaseView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
|
||||
{
|
||||
ViewModel?.NavigateToSource();
|
||||
}
|
||||
}
|
||||
209
src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs
Normal file
209
src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs
Normal file
@ -0,0 +1,209 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Reactive;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Threading.Tasks;
|
||||
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.UI.Shared.Services.Builders;
|
||||
using Artemis.WebClient.Updating;
|
||||
using ReactiveUI;
|
||||
using Serilog;
|
||||
using StrawberryShake;
|
||||
|
||||
namespace Artemis.UI.Screens.Settings.Updating;
|
||||
|
||||
public class ReleaseViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly INotificationService _notificationService;
|
||||
private readonly IUpdateService _updateService;
|
||||
private readonly Platform _updatePlatform;
|
||||
private readonly IUpdatingClient _updatingClient;
|
||||
private readonly IWindowService _windowService;
|
||||
private CancellationTokenSource? _installerCts;
|
||||
private string _changelog = string.Empty;
|
||||
private string _commit = string.Empty;
|
||||
private string _shortCommit = string.Empty;
|
||||
private long _fileSize;
|
||||
private bool _installationAvailable;
|
||||
private bool _installationFinished;
|
||||
private bool _installationInProgress;
|
||||
private bool _loading = true;
|
||||
private bool _retrievedDetails;
|
||||
|
||||
public ReleaseViewModel(string releaseId,
|
||||
string version,
|
||||
DateTimeOffset createdAt,
|
||||
ILogger logger,
|
||||
IUpdatingClient updatingClient,
|
||||
INotificationService notificationService,
|
||||
IUpdateService updateService,
|
||||
IWindowService windowService)
|
||||
{
|
||||
_logger = logger;
|
||||
_updatingClient = updatingClient;
|
||||
_notificationService = notificationService;
|
||||
_updateService = updateService;
|
||||
_windowService = windowService;
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
_updatePlatform = Platform.Windows;
|
||||
else if (OperatingSystem.IsLinux())
|
||||
_updatePlatform = Platform.Linux;
|
||||
else if (OperatingSystem.IsMacOS())
|
||||
_updatePlatform = Platform.Osx;
|
||||
else
|
||||
throw new PlatformNotSupportedException("Cannot auto update on the current platform");
|
||||
|
||||
ReleaseId = releaseId;
|
||||
Version = version;
|
||||
CreatedAt = createdAt;
|
||||
ReleaseInstaller = updateService.GetReleaseInstaller(ReleaseId);
|
||||
|
||||
Install = ReactiveCommand.CreateFromTask(ExecuteInstall);
|
||||
Restart = ReactiveCommand.Create(ExecuteRestart);
|
||||
CancelInstall = ReactiveCommand.Create(() => _installerCts?.Cancel());
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
// 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;
|
||||
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; }
|
||||
|
||||
public string Version { get; }
|
||||
public DateTimeOffset CreatedAt { get; }
|
||||
public ReleaseInstaller ReleaseInstaller { get; }
|
||||
|
||||
public string Changelog
|
||||
{
|
||||
get => _changelog;
|
||||
set => RaiseAndSetIfChanged(ref _changelog, value);
|
||||
}
|
||||
|
||||
public string Commit
|
||||
{
|
||||
get => _commit;
|
||||
set => RaiseAndSetIfChanged(ref _commit, value);
|
||||
}
|
||||
|
||||
public string ShortCommit
|
||||
{
|
||||
get => _shortCommit;
|
||||
set => RaiseAndSetIfChanged(ref _shortCommit, value);
|
||||
}
|
||||
|
||||
public long FileSize
|
||||
{
|
||||
get => _fileSize;
|
||||
set => RaiseAndSetIfChanged(ref _fileSize, value);
|
||||
}
|
||||
|
||||
public bool Loading
|
||||
{
|
||||
get => _loading;
|
||||
private set => RaiseAndSetIfChanged(ref _loading, value);
|
||||
}
|
||||
|
||||
public bool InstallationAvailable
|
||||
{
|
||||
get => _installationAvailable;
|
||||
set => RaiseAndSetIfChanged(ref _installationAvailable, value);
|
||||
}
|
||||
|
||||
public bool InstallationInProgress
|
||||
{
|
||||
get => _installationInProgress;
|
||||
set => RaiseAndSetIfChanged(ref _installationInProgress, value);
|
||||
}
|
||||
|
||||
public bool InstallationFinished
|
||||
{
|
||||
get => _installationFinished;
|
||||
set => RaiseAndSetIfChanged(ref _installationFinished, value);
|
||||
}
|
||||
|
||||
public void NavigateToSource()
|
||||
{
|
||||
Utilities.OpenUrl($"https://github.com/Artemis-RGB/Artemis/commit/{Commit}");
|
||||
}
|
||||
|
||||
private async Task ExecuteInstall(CancellationToken cancellationToken)
|
||||
{
|
||||
_installerCts = new CancellationTokenSource();
|
||||
try
|
||||
{
|
||||
InstallationInProgress = true;
|
||||
await ReleaseInstaller.InstallAsync(_installerCts.Token);
|
||||
_updateService.QueueUpdate();
|
||||
InstallationFinished = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
if (_installerCts.IsCancellationRequested)
|
||||
return;
|
||||
_windowService.ShowExceptionDialog("Failed to install update", e);
|
||||
}
|
||||
finally
|
||||
{
|
||||
InstallationInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RetrieveDetails(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_retrievedDetails)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Loading = true;
|
||||
|
||||
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(ReleaseId, cancellationToken);
|
||||
IGetReleaseById_PublishedRelease? release = result.Data?.PublishedRelease;
|
||||
if (release == null)
|
||||
return;
|
||||
|
||||
Changelog = release.Changelog;
|
||||
Commit = release.Commit;
|
||||
ShortCommit = release.Commit.Substring(0, 7);
|
||||
FileSize = release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform)?.FileInfo.DownloadSize ?? 0;
|
||||
|
||||
_retrievedDetails = true;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_logger.Warning(e, "Failed to retrieve release details");
|
||||
_notificationService.CreateNotification()
|
||||
.WithTitle("Failed to retrieve details")
|
||||
.WithMessage(e.Message)
|
||||
.WithSeverity(NotificationSeverity.Warning)
|
||||
.Show();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -4,5 +4,5 @@ namespace Artemis.UI.Services.Updating;
|
||||
|
||||
public interface IUpdateNotificationProvider
|
||||
{
|
||||
Task ShowNotification(string releaseId);
|
||||
void ShowNotification(string releaseId, string releaseVersion);
|
||||
}
|
||||
@ -1,11 +1,18 @@
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.UI.Services.Interfaces;
|
||||
using Artemis.WebClient.Updating;
|
||||
|
||||
namespace Artemis.UI.Services.Updating;
|
||||
|
||||
public interface IUpdateService : IArtemisUIService
|
||||
{
|
||||
Task<bool> CheckForUpdate();
|
||||
Task InstallRelease(string releaseId);
|
||||
string? CurrentVersion { get; }
|
||||
IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; }
|
||||
|
||||
Task CacheLatestRelease();
|
||||
Task<bool> CheckForUpdate();
|
||||
void QueueUpdate();
|
||||
|
||||
ReleaseInstaller GetReleaseInstaller(string releaseId);
|
||||
void RestartForUpdate(bool silent);
|
||||
}
|
||||
@ -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 void ShowNotification(string releaseId, string releaseVersion)
|
||||
{
|
||||
if (_mainWindowService.IsMainWindowOpen)
|
||||
ShowInAppNotification(releaseId, releaseVersion);
|
||||
else
|
||||
_mainWindowService.MainWindowOpened += (_, _) => ShowInAppNotification(releaseId, releaseVersion);
|
||||
}
|
||||
}
|
||||
@ -3,9 +3,11 @@ using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.UI.Exceptions;
|
||||
using Artemis.UI.Extensions;
|
||||
using Artemis.WebClient.Updating;
|
||||
using Octodiff.Core;
|
||||
@ -18,7 +20,7 @@ namespace Artemis.UI.Services.Updating;
|
||||
/// <summary>
|
||||
/// Represents the installation process of a release
|
||||
/// </summary>
|
||||
public class ReleaseInstaller
|
||||
public class ReleaseInstaller : CorePropertyChanged
|
||||
{
|
||||
private readonly string _dataFolder;
|
||||
private readonly HttpClient _httpClient;
|
||||
@ -26,6 +28,10 @@ public class ReleaseInstaller
|
||||
private readonly string _releaseId;
|
||||
private readonly Platform _updatePlatform;
|
||||
private readonly IUpdatingClient _updatingClient;
|
||||
private readonly Progress<float> _progress = new();
|
||||
private Progress<float> _stepProgress = new();
|
||||
private string _status = string.Empty;
|
||||
private float _progress1;
|
||||
|
||||
public ReleaseInstaller(string releaseId, ILogger logger, IUpdatingClient updatingClient, HttpClient httpClient)
|
||||
{
|
||||
@ -46,29 +52,42 @@ public class ReleaseInstaller
|
||||
|
||||
if (!Directory.Exists(_dataFolder))
|
||||
Directory.CreateDirectory(_dataFolder);
|
||||
|
||||
_progress.ProgressChanged += (_, f) => Progress = f;
|
||||
}
|
||||
|
||||
public string Status
|
||||
{
|
||||
get => _status;
|
||||
private set => SetAndNotify(ref _status, value);
|
||||
}
|
||||
|
||||
public Progress<float> OverallProgress { get; } = new();
|
||||
public Progress<float> StepProgress { get; } = new();
|
||||
public float Progress
|
||||
{
|
||||
get => _progress1;
|
||||
set => SetAndNotify(ref _progress1, value);
|
||||
}
|
||||
|
||||
public async Task InstallAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
((IProgress<float>) OverallProgress).Report(0);
|
||||
_stepProgress = new Progress<float>();
|
||||
|
||||
((IProgress<float>) _progress).Report(0);
|
||||
|
||||
Status = "Retrieving details";
|
||||
_logger.Information("Retrieving details for release {ReleaseId}", _releaseId);
|
||||
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken);
|
||||
result.EnsureNoErrors();
|
||||
|
||||
IGetReleaseById_Release? release = result.Data?.Release;
|
||||
IGetReleaseById_PublishedRelease? release = result.Data?.PublishedRelease;
|
||||
if (release == null)
|
||||
throw new Exception($"Could not find release with ID {_releaseId}");
|
||||
|
||||
IGetReleaseById_Release_Artifacts? artifact = release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform);
|
||||
IGetReleaseById_PublishedRelease_Artifacts? artifact = release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform);
|
||||
if (artifact == null)
|
||||
throw new Exception("Found the release but it has no artifact for the current platform");
|
||||
|
||||
((IProgress<float>) OverallProgress).Report(10);
|
||||
((IProgress<float>) _progress).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)
|
||||
@ -77,75 +96,92 @@ public class ReleaseInstaller
|
||||
await Download(artifact, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task DownloadDelta(IGetReleaseById_Release_Artifacts artifact, string previousRelease, CancellationToken cancellationToken)
|
||||
private async Task DownloadDelta(IGetReleaseById_PublishedRelease_Artifacts artifact, string previousRelease, CancellationToken cancellationToken)
|
||||
{
|
||||
// 10 - 50%
|
||||
_stepProgress.ProgressChanged += StepProgressOnProgressChanged;
|
||||
void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress<float>) _progress).Report(10f + e * 0.4f);
|
||||
|
||||
Status = "Downloading...";
|
||||
await using MemoryStream stream = new();
|
||||
await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}/delta", stream, StepProgress, cancellationToken);
|
||||
await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/{artifact.ArtifactId}/delta", stream, _stepProgress, cancellationToken);
|
||||
|
||||
((IProgress<float>) OverallProgress).Report(33);
|
||||
|
||||
await PatchDelta(stream, previousRelease, cancellationToken);
|
||||
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
|
||||
await PatchDelta(stream, previousRelease, artifact, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task PatchDelta(Stream deltaStream, string previousRelease, CancellationToken cancellationToken)
|
||||
private async Task PatchDelta(Stream deltaStream, string previousRelease, IGetReleaseById_PublishedRelease_Artifacts artifact, CancellationToken cancellationToken)
|
||||
{
|
||||
await using FileStream baseStream = File.OpenRead(previousRelease);
|
||||
// 50 - 60%
|
||||
_stepProgress.ProgressChanged += StepProgressOnProgressChanged;
|
||||
void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress<float>) _progress).Report(50f + e * 0.1f);
|
||||
|
||||
Status = "Patching...";
|
||||
await using FileStream newFileStream = new(Path.Combine(_dataFolder, $"{_releaseId}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||||
|
||||
deltaStream.Seek(0, SeekOrigin.Begin);
|
||||
|
||||
await Task.Run(() =>
|
||||
await using (FileStream baseStream = File.OpenRead(previousRelease))
|
||||
{
|
||||
DeltaApplier deltaApplier = new();
|
||||
deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new DeltaApplierProgressReporter(StepProgress)), newFileStream);
|
||||
});
|
||||
deltaStream.Seek(0, SeekOrigin.Begin);
|
||||
DeltaApplier deltaApplier = new() {SkipHashCheck = true};
|
||||
// Patching is not async and so fast that it's not worth adding a progress reporter
|
||||
deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new NullProgressReporter()), newFileStream);
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
}
|
||||
|
||||
((IProgress<float>) OverallProgress).Report(66);
|
||||
// The previous release is no longer required now that the latest has been downloaded
|
||||
File.Delete(previousRelease);
|
||||
|
||||
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
|
||||
|
||||
await ValidateArchive(newFileStream, artifact, cancellationToken);
|
||||
await Extract(newFileStream, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task Download(IGetReleaseById_Release_Artifacts artifact, CancellationToken cancellationToken)
|
||||
private async Task Download(IGetReleaseById_PublishedRelease_Artifacts artifact, CancellationToken cancellationToken)
|
||||
{
|
||||
await using MemoryStream stream = new();
|
||||
await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}", stream, StepProgress, cancellationToken);
|
||||
// 10 - 60%
|
||||
_stepProgress.ProgressChanged += StepProgressOnProgressChanged;
|
||||
void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress<float>) _progress).Report(10f + e * 0.5f);
|
||||
|
||||
((IProgress<float>) OverallProgress).Report(50);
|
||||
Status = "Downloading...";
|
||||
await using FileStream stream = new(Path.Combine(_dataFolder, $"{_releaseId}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||||
await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/{artifact.ArtifactId}", stream, _stepProgress, cancellationToken);
|
||||
|
||||
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
|
||||
|
||||
await ValidateArchive(stream, artifact, cancellationToken);
|
||||
await Extract(stream, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task Extract(Stream archiveStream, CancellationToken cancellationToken)
|
||||
{
|
||||
// 60 - 100%
|
||||
_stepProgress.ProgressChanged += StepProgressOnProgressChanged;
|
||||
void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress<float>) _progress).Report(60f + e * 0.4f);
|
||||
|
||||
Status = "Extracting...";
|
||||
// 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);
|
||||
});
|
||||
archive.ExtractToDirectory(extractDirectory, false, _stepProgress, cancellationToken);
|
||||
}, cancellationToken);
|
||||
|
||||
((IProgress<float>) OverallProgress).Report(100);
|
||||
}
|
||||
}
|
||||
|
||||
internal class DeltaApplierProgressReporter : IProgressReporter
|
||||
{
|
||||
private readonly IProgress<float> _stepProgress;
|
||||
|
||||
public DeltaApplierProgressReporter(IProgress<float> stepProgress)
|
||||
{
|
||||
_stepProgress = stepProgress;
|
||||
}
|
||||
|
||||
public void ReportProgress(string operation, long currentPosition, long total)
|
||||
{
|
||||
_stepProgress.Report(currentPosition / total * 100);
|
||||
((IProgress<float>) _progress).Report(100);
|
||||
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
|
||||
}
|
||||
|
||||
private async Task ValidateArchive(Stream archiveStream, IGetReleaseById_PublishedRelease_Artifacts artifact, CancellationToken cancellationToken)
|
||||
{
|
||||
using MD5 md5 = MD5.Create();
|
||||
archiveStream.Seek(0, SeekOrigin.Begin);
|
||||
string hash = BitConverter.ToString(await md5.ComputeHashAsync(archiveStream, cancellationToken)).Replace("-", "");
|
||||
if (hash != artifact.FileInfo.Md5Hash)
|
||||
throw new ArtemisUIException($"Update file hash mismatch, expected \"{artifact.FileInfo.Md5Hash}\" but got \"{hash}\"");
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -4,9 +4,8 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Screens.Settings.Updating;
|
||||
using Artemis.UI.Services.Updating;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.Storage.Entities.General;
|
||||
using Artemis.Storage.Repositories.Interfaces;
|
||||
using Artemis.UI.Shared.Services.MainWindow;
|
||||
using Artemis.WebClient.Updating;
|
||||
using Avalonia.Threading;
|
||||
@ -14,7 +13,7 @@ using Serilog;
|
||||
using StrawberryShake;
|
||||
using Timer = System.Timers.Timer;
|
||||
|
||||
namespace Artemis.UI.Services;
|
||||
namespace Artemis.UI.Services.Updating;
|
||||
|
||||
public class UpdateService : IUpdateService
|
||||
{
|
||||
@ -26,24 +25,24 @@ public class UpdateService : IUpdateService
|
||||
|
||||
private readonly ILogger _logger;
|
||||
private readonly IMainWindowService _mainWindowService;
|
||||
private readonly IQueuedActionRepository _queuedActionRepository;
|
||||
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
|
||||
private readonly Platform _updatePlatform;
|
||||
private readonly IUpdatingClient _updatingClient;
|
||||
private readonly IWindowService _windowService;
|
||||
|
||||
private bool _suspendAutoCheck;
|
||||
|
||||
public UpdateService(ILogger logger,
|
||||
ISettingsService settingsService,
|
||||
IMainWindowService mainWindowService,
|
||||
IWindowService windowService,
|
||||
IQueuedActionRepository queuedActionRepository,
|
||||
IUpdatingClient updatingClient,
|
||||
Lazy<IUpdateNotificationProvider> updateNotificationProvider,
|
||||
Func<string, ReleaseInstaller> getReleaseInstaller)
|
||||
{
|
||||
_logger = logger;
|
||||
_mainWindowService = mainWindowService;
|
||||
_windowService = windowService;
|
||||
_queuedActionRepository = queuedActionRepository;
|
||||
_updatingClient = updatingClient;
|
||||
_updateNotificationProvider = updateNotificationProvider;
|
||||
_getReleaseInstaller = getReleaseInstaller;
|
||||
@ -65,35 +64,34 @@ public class UpdateService : IUpdateService
|
||||
Timer timer = new(UPDATE_CHECK_INTERVAL);
|
||||
timer.Elapsed += HandleAutoUpdateEvent;
|
||||
timer.Start();
|
||||
|
||||
_channel.Value = "feature/gh-actions";
|
||||
_channel.Save();
|
||||
|
||||
|
||||
InstallQueuedUpdate();
|
||||
}
|
||||
|
||||
public string? CurrentVersion
|
||||
private void InstallQueuedUpdate()
|
||||
{
|
||||
get
|
||||
{
|
||||
object[] attributes = typeof(UpdateService).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false);
|
||||
return attributes.Length == 0 ? null : ((AssemblyInformationalVersionAttribute) attributes[0]).InformationalVersion;
|
||||
}
|
||||
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 void 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 _windowService.ShowDialogAsync<ReleaseAvailableViewModel>(nextReleaseId);
|
||||
});
|
||||
_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);
|
||||
}
|
||||
@ -113,43 +111,71 @@ public class UpdateService : IUpdateService
|
||||
}
|
||||
}
|
||||
|
||||
public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; }
|
||||
|
||||
public string? CurrentVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
object[] attributes = typeof(UpdateService).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false);
|
||||
return attributes.Length == 0 ? null : ((AssemblyInformationalVersionAttribute) attributes[0]).InformationalVersion;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task CacheLatestRelease()
|
||||
{
|
||||
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(CurrentVersion, _channel.Value, _updatePlatform);
|
||||
CachedLatestRelease = result.Data?.NextPublishedRelease;
|
||||
}
|
||||
|
||||
public async Task<bool> CheckForUpdate()
|
||||
{
|
||||
string? currentVersion = CurrentVersion;
|
||||
if (currentVersion == null)
|
||||
return false;
|
||||
|
||||
// IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, _channel.Value, _updatePlatform);
|
||||
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(currentVersion, "feature/gh-actions", _updatePlatform);
|
||||
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(CurrentVersion, _channel.Value, _updatePlatform);
|
||||
result.EnsureNoErrors();
|
||||
|
||||
// Update cache
|
||||
CachedLatestRelease = result.Data?.NextPublishedRelease;
|
||||
|
||||
// No update was found
|
||||
if (result.Data?.NextRelease == null)
|
||||
if (CachedLatestRelease == null)
|
||||
return false;
|
||||
|
||||
// Only offer it once per session
|
||||
_suspendAutoCheck = true;
|
||||
|
||||
// If the window is open show the changelog, don't auto-update while the user is busy
|
||||
if (_mainWindowService.IsMainWindowOpen)
|
||||
await ShowUpdateDialog(result.Data.NextRelease.Id);
|
||||
else if (!_autoInstall.Value)
|
||||
await ShowUpdateNotification(result.Data.NextRelease.Id);
|
||||
if (!_autoInstall.Value)
|
||||
ShowUpdateNotification(CachedLatestRelease);
|
||||
else
|
||||
await AutoInstallUpdate(result.Data.NextRelease.Id);
|
||||
await AutoInstallUpdate(CachedLatestRelease);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task InstallRelease(string releaseId)
|
||||
public void QueueUpdate()
|
||||
{
|
||||
ReleaseInstaller installer = _getReleaseInstaller(releaseId);
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
if (!_queuedActionRepository.IsTypeQueued("InstallUpdate"))
|
||||
_queuedActionRepository.Add(new QueuedActionEntity {Type = "InstallUpdate"});
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DequeueUpdate()
|
||||
{
|
||||
// Main window is probably already open but this will bring it into focus
|
||||
_mainWindowService.OpenMainWindow();
|
||||
_windowService.ShowWindow<ReleaseInstallerViewModel>(installer);
|
||||
});
|
||||
_queuedActionRepository.ClearByType("InstallUpdate");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReleaseInstaller GetReleaseInstaller(string releaseId)
|
||||
{
|
||||
return _getReleaseInstaller(releaseId);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RestartForUpdate(bool silent)
|
||||
{
|
||||
DequeueUpdate();
|
||||
Utilities.ApplyUpdate(silent);
|
||||
}
|
||||
}
|
||||
@ -13,7 +13,7 @@ namespace Artemis.VisualScripting.DryIoc;
|
||||
/// <summary>
|
||||
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
||||
/// </summary>
|
||||
public static class UIContainerExtensions
|
||||
public static class ContainerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Registers NoStringEvaluating services into the container.
|
||||
|
||||
@ -3,7 +3,7 @@ using SkiaSharp;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.Color;
|
||||
|
||||
[Node("HSL Color", "Creates a color from hue, saturation and lightness values", "Color", InputType = typeof(Numeric), OutputType = typeof(SKColor))]
|
||||
[Node("HSL Color", "Creates a color from hue, saturation and lightness numbers", "Color", InputType = typeof(Numeric), OutputType = typeof(SKColor))]
|
||||
public class HslSKColorNode : Node
|
||||
{
|
||||
public HslSKColorNode()
|
||||
|
||||
31
src/Artemis.VisualScripting/Nodes/Color/HsvSKColorNode.cs
Normal file
31
src/Artemis.VisualScripting/Nodes/Color/HsvSKColorNode.cs
Normal file
@ -0,0 +1,31 @@
|
||||
using Artemis.Core;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.Color;
|
||||
|
||||
[Node("HSV Color", "Creates a color from hue, saturation and value numbers", "Color", InputType = typeof(Numeric), OutputType = typeof(SKColor))]
|
||||
public class HsvSKColorNode : Node
|
||||
{
|
||||
public HsvSKColorNode()
|
||||
{
|
||||
H = CreateInputPin<Numeric>("H");
|
||||
S = CreateInputPin<Numeric>("S");
|
||||
V = CreateInputPin<Numeric>("V");
|
||||
Output = CreateOutputPin<SKColor>();
|
||||
}
|
||||
|
||||
public InputPin<Numeric> H { get; set; }
|
||||
public InputPin<Numeric> S { get; set; }
|
||||
public InputPin<Numeric> V { get; set; }
|
||||
public OutputPin<SKColor> Output { get; }
|
||||
|
||||
#region Overrides of Node
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Evaluate()
|
||||
{
|
||||
Output.Value = SKColor.FromHsv(H.Value, S.Value, V.Value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
36
src/Artemis.VisualScripting/Nodes/Color/SkColorHsl.cs
Normal file
36
src/Artemis.VisualScripting/Nodes/Color/SkColorHsl.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Artemis.Core;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.Color;
|
||||
|
||||
[Node("Color to HSL", "Outputs H, S and L values from a color", "Color", InputType = typeof(SKColor), OutputType = typeof(Numeric))]
|
||||
public class SkColorHsl : Node
|
||||
{
|
||||
|
||||
public SkColorHsl()
|
||||
{
|
||||
Input = CreateInputPin<SKColor>();
|
||||
H = CreateOutputPin<Numeric>("H");
|
||||
S = CreateOutputPin<Numeric>("S");
|
||||
L = CreateOutputPin<Numeric>("L");
|
||||
}
|
||||
|
||||
public InputPin<SKColor> Input { get; }
|
||||
public OutputPin<Numeric> H { get; }
|
||||
public OutputPin<Numeric> S { get; }
|
||||
public OutputPin<Numeric> L { get; }
|
||||
|
||||
#region Overrides of Node
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Evaluate()
|
||||
{
|
||||
Input.Value.ToHsl(out float h, out float s, out float l);
|
||||
|
||||
H.Value = h;
|
||||
S.Value = s;
|
||||
L.Value = l;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
36
src/Artemis.VisualScripting/Nodes/Color/SkColorHsv.cs
Normal file
36
src/Artemis.VisualScripting/Nodes/Color/SkColorHsv.cs
Normal file
@ -0,0 +1,36 @@
|
||||
using Artemis.Core;
|
||||
using SkiaSharp;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.Color;
|
||||
|
||||
[Node("Color to HSV", "Outputs H, S and L values from a color", "Color", InputType = typeof(SKColor), OutputType = typeof(Numeric))]
|
||||
public class SkColorHsv : Node
|
||||
{
|
||||
|
||||
public SkColorHsv()
|
||||
{
|
||||
Input = CreateInputPin<SKColor>();
|
||||
H = CreateOutputPin<Numeric>("H");
|
||||
S = CreateOutputPin<Numeric>("S");
|
||||
V = CreateOutputPin<Numeric>("V");
|
||||
}
|
||||
|
||||
public InputPin<SKColor> Input { get; }
|
||||
public OutputPin<Numeric> H { get; }
|
||||
public OutputPin<Numeric> S { get; }
|
||||
public OutputPin<Numeric> V { get; }
|
||||
|
||||
#region Overrides of Node
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void Evaluate()
|
||||
{
|
||||
Input.Value.ToHsv(out float h, out float s, out float v);
|
||||
|
||||
H.Value = h;
|
||||
S.Value = s;
|
||||
V.Value = v;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
using Artemis.Core;
|
||||
using Artemis.Core;
|
||||
|
||||
namespace Artemis.VisualScripting.Nodes.Conversion;
|
||||
|
||||
@ -33,6 +33,7 @@ public class ConvertToNumericNode : Node
|
||||
double input => new Numeric(input),
|
||||
float input => new Numeric(input),
|
||||
byte input => new Numeric(input),
|
||||
bool input => new Numeric(input ? 1 : 0),
|
||||
_ => TryParse(Input.Value)
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,5 +1,29 @@
|
||||
query GetReleases($branch: String!, $platform: Platform!, $take: Int!, $after: String) {
|
||||
publishedReleases(
|
||||
first: $take
|
||||
after: $after
|
||||
where: {
|
||||
and: [
|
||||
{ branch: { eq: $branch } }
|
||||
{ artifacts: { some: { platform: { eq: $platform } } } }
|
||||
]
|
||||
}
|
||||
) {
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
nodes {
|
||||
id
|
||||
version
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
query GetReleaseById($id: String!) {
|
||||
release(id: $id) {
|
||||
publishedRelease(id: $id) {
|
||||
branch
|
||||
commit
|
||||
version
|
||||
@ -23,9 +47,10 @@ fragment fileInfo on ArtifactFileInfo {
|
||||
downloadSize
|
||||
}
|
||||
|
||||
query GetNextRelease($currentVersion: String!, $branch: String!, $platform: Platform!) {
|
||||
nextRelease(version: $currentVersion, branch: $branch, platform: $platform) {
|
||||
query GetNextRelease($currentVersion: String, $branch: String!, $platform: Platform!) {
|
||||
nextPublishedRelease(version: $currentVersion, branch: $branch, platform: $platform) {
|
||||
id
|
||||
version
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -41,7 +41,6 @@ type Artifact {
|
||||
artifactId: Long!
|
||||
deltaFileInfo: ArtifactFileInfo!
|
||||
fileInfo: ArtifactFileInfo!
|
||||
fileName(deltaFile: Boolean!): String!
|
||||
platform: Platform!
|
||||
}
|
||||
|
||||
@ -55,13 +54,69 @@ type Mutation {
|
||||
updateReleaseChangelog(input: UpdateReleaseChangelogInput!): UpdateReleaseChangelogPayload!
|
||||
}
|
||||
|
||||
"Information about pagination in a connection."
|
||||
type PageInfo {
|
||||
"When paginating forwards, the cursor to continue."
|
||||
endCursor: String
|
||||
"Indicates whether more edges exist following the set defined by the clients arguments."
|
||||
hasNextPage: Boolean!
|
||||
"Indicates whether more edges exist prior the set defined by the clients arguments."
|
||||
hasPreviousPage: Boolean!
|
||||
"When paginating backwards, the cursor to continue."
|
||||
startCursor: String
|
||||
}
|
||||
|
||||
"A connection to a list of items."
|
||||
type PublishedReleasesConnection {
|
||||
"A list of edges."
|
||||
edges: [PublishedReleasesEdge!]
|
||||
"A flattened list of the nodes."
|
||||
nodes: [Release!]
|
||||
"Information to aid in pagination."
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
"An edge in a connection."
|
||||
type PublishedReleasesEdge {
|
||||
"A cursor for use in pagination."
|
||||
cursor: String!
|
||||
"The item at the end of the edge."
|
||||
node: Release!
|
||||
}
|
||||
|
||||
type Query {
|
||||
channelByBranch(branch: String!): ArtemisChannel
|
||||
channels: [ArtemisChannel!]!
|
||||
nextRelease(branch: String!, platform: Platform!, version: String!): Release
|
||||
nextPublishedRelease(branch: String!, platform: Platform!, version: String): Release
|
||||
publishedChannels: [String!]!
|
||||
publishedRelease(id: String!): Release
|
||||
publishedReleases(
|
||||
"Returns the elements in the list that come after the specified cursor."
|
||||
after: String,
|
||||
"Returns the elements in the list that come before the specified cursor."
|
||||
before: String,
|
||||
"Returns the first _n_ elements from the list."
|
||||
first: Int,
|
||||
"Returns the last _n_ elements from the list."
|
||||
last: Int,
|
||||
order: [ReleaseSortInput!],
|
||||
where: ReleaseFilterInput
|
||||
): PublishedReleasesConnection
|
||||
release(id: String!): Release
|
||||
releaseStatistics(order: [ReleaseStatisticSortInput!], where: ReleaseStatisticFilterInput): [ReleaseStatistic!]!
|
||||
releases(order: [ReleaseSortInput!], where: ReleaseFilterInput): [Release!]!
|
||||
releases(
|
||||
"Returns the elements in the list that come after the specified cursor."
|
||||
after: String,
|
||||
"Returns the elements in the list that come before the specified cursor."
|
||||
before: String,
|
||||
"Returns the first _n_ elements from the list."
|
||||
first: Int,
|
||||
"Returns the last _n_ elements from the list."
|
||||
last: Int,
|
||||
order: [ReleaseSortInput!],
|
||||
where: ReleaseFilterInput
|
||||
): ReleasesConnection
|
||||
}
|
||||
|
||||
type Release {
|
||||
@ -86,6 +141,25 @@ type ReleaseStatistic {
|
||||
windowsCount: Int!
|
||||
}
|
||||
|
||||
"A connection to a list of items."
|
||||
type ReleasesConnection {
|
||||
"A list of edges."
|
||||
edges: [ReleasesEdge!]
|
||||
"A flattened list of the nodes."
|
||||
nodes: [Release!]
|
||||
"Information to aid in pagination."
|
||||
pageInfo: PageInfo!
|
||||
totalCount: Int!
|
||||
}
|
||||
|
||||
"An edge in a connection."
|
||||
type ReleasesEdge {
|
||||
"A cursor for use in pagination."
|
||||
cursor: String!
|
||||
"The item at the end of the edge."
|
||||
node: Release!
|
||||
}
|
||||
|
||||
type UpdateReleaseChangelogPayload {
|
||||
release: Release
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user