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="LiteDB" Version="5.0.12" />
|
||||||
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
|
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||||
<PackageReference Include="RGB.NET.Core" Version="1.0.0" />
|
<PackageReference Include="RGB.NET.Core" Version="2.0.0-prerelease.12" />
|
||||||
<PackageReference Include="RGB.NET.Layout" Version="1.0.0" />
|
<PackageReference Include="RGB.NET.Layout" Version="2.0.0-prerelease.12" />
|
||||||
<PackageReference Include="RGB.NET.Presets" Version="1.0.0" />
|
<PackageReference Include="RGB.NET.Presets" Version="2.0.0-prerelease.12" />
|
||||||
<PackageReference Include="Serilog" Version="2.11.0" />
|
<PackageReference Include="Serilog" Version="2.11.0" />
|
||||||
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="4.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
|
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
|
||||||
|
|||||||
@ -13,7 +13,7 @@ namespace Artemis.Core.DryIoc;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class CoreContainerExtensions
|
public static class ContainerExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers core services into the container.
|
/// Registers core services into the container.
|
||||||
|
|||||||
@ -9,4 +9,6 @@ public interface IQueuedActionRepository : IRepository
|
|||||||
void Remove(QueuedActionEntity queuedActionEntity);
|
void Remove(QueuedActionEntity queuedActionEntity);
|
||||||
List<QueuedActionEntity> GetAll();
|
List<QueuedActionEntity> GetAll();
|
||||||
List<QueuedActionEntity> GetByType(string type);
|
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();
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
@ -20,7 +20,7 @@
|
|||||||
<PackageReference Include="Material.Icons.Avalonia" Version="1.1.10" />
|
<PackageReference Include="Material.Icons.Avalonia" Version="1.1.10" />
|
||||||
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
||||||
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
<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" />
|
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.108" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<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>
|
/// <summary>
|
||||||
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class UIContainerExtensions
|
public static class ContainerExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers shared UI services into the container.
|
/// Registers shared UI services into the container.
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Services.MainWindow;
|
namespace Artemis.UI.Shared.Services.MainWindow;
|
||||||
|
|
||||||
@ -12,6 +13,11 @@ public interface IMainWindowService : IArtemisSharedUIService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsMainWindowOpen { get; }
|
bool IsMainWindowOpen { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the host screen contained in the main window
|
||||||
|
/// </summary>
|
||||||
|
IScreen? HostScreen { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets up the main window provider that controls the state of the main window
|
/// Sets up the main window provider that controls the state of the main window
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Services.MainWindow;
|
namespace Artemis.UI.Shared.Services.MainWindow;
|
||||||
|
|
||||||
@ -6,6 +7,12 @@ internal class MainWindowService : IMainWindowService
|
|||||||
{
|
{
|
||||||
private IMainWindowProvider? _mainWindowManager;
|
private IMainWindowProvider? _mainWindowManager;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsMainWindowOpen { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IScreen? HostScreen { get; set; }
|
||||||
|
|
||||||
protected virtual void OnMainWindowOpened()
|
protected virtual void OnMainWindowOpened()
|
||||||
{
|
{
|
||||||
MainWindowOpened?.Invoke(this, EventArgs.Empty);
|
MainWindowOpened?.Invoke(this, EventArgs.Empty);
|
||||||
@ -64,8 +71,6 @@ internal class MainWindowService : IMainWindowService
|
|||||||
OnMainWindowUnfocused();
|
OnMainWindowUnfocused();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsMainWindowOpen { get; private set; }
|
|
||||||
|
|
||||||
public void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider)
|
public void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider)
|
||||||
{
|
{
|
||||||
if (mainWindowProvider == null) throw new ArgumentNullException(nameof(mainWindowProvider));
|
if (mainWindowProvider == null) throw new ArgumentNullException(nameof(mainWindowProvider));
|
||||||
|
|||||||
@ -56,7 +56,7 @@ public class DuplicateNode : INodeEditorCommand, IDisposable
|
|||||||
if (targetCollection == null)
|
if (targetCollection == null)
|
||||||
continue;
|
continue;
|
||||||
while (targetCollection.Count() < sourceCollection.Count())
|
while (targetCollection.Count() < sourceCollection.Count())
|
||||||
targetCollection.CreatePin();
|
targetCollection.Add(targetCollection.CreatePin());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy the storage
|
// Copy the storage
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
<!-- Custom styles -->
|
<!-- Custom styles -->
|
||||||
<StyleInclude Source="/Styles/Border.axaml" />
|
<StyleInclude Source="/Styles/Border.axaml" />
|
||||||
|
<StyleInclude Source="/Styles/Skeleton.axaml" />
|
||||||
<StyleInclude Source="/Styles/Button.axaml" />
|
<StyleInclude Source="/Styles/Button.axaml" />
|
||||||
<StyleInclude Source="/Styles/Condensed.axaml" />
|
<StyleInclude Source="/Styles/Condensed.axaml" />
|
||||||
<StyleInclude Source="/Styles/ColorPickerButton.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()
|
ProcessStartInfo info = new()
|
||||||
{
|
{
|
||||||
Arguments = $"-File {script} {source} {destination} {args}",
|
Arguments = $"-File {script} {source} {destination} {args}",
|
||||||
WindowStyle = ProcessWindowStyle.Hidden,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
FileName = "PowerShell.exe"
|
FileName = "PowerShell.exe"
|
||||||
};
|
};
|
||||||
Process.Start(info);
|
Process.Start(info);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using Artemis.Core.Providers;
|
using Artemis.Core.Providers;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
|
using Artemis.UI.Services.Updating;
|
||||||
using Artemis.UI.Shared.Providers;
|
using Artemis.UI.Shared.Providers;
|
||||||
using Artemis.UI.Windows.Providers;
|
using Artemis.UI.Windows.Providers;
|
||||||
using Artemis.UI.Windows.Providers.Input;
|
using Artemis.UI.Windows.Providers.Input;
|
||||||
@ -22,5 +23,6 @@ public static class UIContainerExtensions
|
|||||||
container.Register<IGraphicsContextProvider, GraphicsContextProvider>(Reuse.Singleton);
|
container.Register<IGraphicsContextProvider, GraphicsContextProvider>(Reuse.Singleton);
|
||||||
container.Register<IAutoRunProvider, AutoRunProvider>();
|
container.Register<IAutoRunProvider, AutoRunProvider>();
|
||||||
container.Register<InputProvider, WindowsInputProvider>(serviceKey: WindowsInputProvider.Id);
|
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
|
[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
|
# Wait up to 10 seconds for the process to shut down
|
||||||
for ($i=1; $i -le 10; $i++) {
|
for ($i=1; $i -le 10; $i++) {
|
||||||
$process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue
|
$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"
|
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
|
Get-ChildItem $destinationDirectory | Remove-Item -Recurse -Force
|
||||||
|
|
||||||
# Move the contents of the source directory to the destination directory
|
# Move the contents of the source directory to the destination directory
|
||||||
|
Write-Host "Installing new files"
|
||||||
Get-ChildItem $sourceDirectory | Move-Item -Destination $destinationDirectory
|
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
|
Start-Sleep -Seconds 1
|
||||||
|
|
||||||
# When finished, run the updated version
|
# When finished, run the updated version
|
||||||
|
|||||||
@ -34,8 +34,8 @@
|
|||||||
<PackageReference Include="Octopus.Octodiff" Version="2.0.100" />
|
<PackageReference Include="Octopus.Octodiff" Version="2.0.100" />
|
||||||
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
<PackageReference Include="ReactiveUI" Version="17.1.50" />
|
||||||
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
<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="RGB.NET.Layout" Version="1.0.0" />
|
<PackageReference Include="RGB.NET.Layout" Version="2.0.0-prerelease.12" />
|
||||||
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.108" />
|
<PackageReference Include="SkiaSharp" Version="2.88.1-preview.108" />
|
||||||
<PackageReference Include="Splat.DryIoc" Version="14.6.1" />
|
<PackageReference Include="Splat.DryIoc" Version="14.6.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
@ -43,4 +43,15 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
</ItemGroup>
|
</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>
|
</Project>
|
||||||
@ -17,7 +17,7 @@ namespace Artemis.UI.DryIoc;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class UIContainerExtensions
|
public static class ContainerExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers UI services into the container.
|
/// Registers UI services into the container.
|
||||||
@ -25,19 +25,18 @@ public static class UIContainerExtensions
|
|||||||
/// <param name="container">The builder building the current container</param>
|
/// <param name="container">The builder building the current container</param>
|
||||||
public static void RegisterUI(this IContainer container)
|
public static void RegisterUI(this IContainer container)
|
||||||
{
|
{
|
||||||
Assembly[] thisAssembly = {typeof(UIContainerExtensions).Assembly};
|
Assembly[] thisAssembly = {typeof(ContainerExtensions).Assembly};
|
||||||
|
|
||||||
container.RegisterInstance(new AssetLoader(), IfAlreadyRegistered.Throw);
|
container.RegisterInstance(new AssetLoader(), IfAlreadyRegistered.Throw);
|
||||||
container.Register<IAssetLoader, AssetLoader>(Reuse.Singleton);
|
container.Register<IAssetLoader, AssetLoader>(Reuse.Singleton);
|
||||||
|
|
||||||
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<ViewModelBase>());
|
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<IToolViewModel>() && type.IsInterface);
|
||||||
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IVmFactory>() && type != typeof(PropertyVmFactory));
|
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IVmFactory>() && type != typeof(PropertyVmFactory));
|
||||||
|
|
||||||
container.Register<NodeScriptWindowViewModelBase, NodeScriptWindowViewModel>(Reuse.Singleton);
|
container.Register<NodeScriptWindowViewModelBase, NodeScriptWindowViewModel>(Reuse.Singleton);
|
||||||
container.Register<IPropertyVmFactory, PropertyVmFactory>(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);
|
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 System.Reactive;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.Core.LayerBrushes;
|
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.ProfileEditor.VisualEditor.Visualizers;
|
||||||
using Artemis.UI.Screens.Scripting;
|
using Artemis.UI.Screens.Scripting;
|
||||||
using Artemis.UI.Screens.Settings;
|
using Artemis.UI.Screens.Settings;
|
||||||
|
using Artemis.UI.Screens.Settings.Updating;
|
||||||
using Artemis.UI.Screens.Sidebar;
|
using Artemis.UI.Screens.Sidebar;
|
||||||
using Artemis.UI.Screens.SurfaceEditor;
|
using Artemis.UI.Screens.SurfaceEditor;
|
||||||
using Artemis.UI.Screens.VisualScripting;
|
using Artemis.UI.Screens.VisualScripting;
|
||||||
@ -475,3 +477,22 @@ public class ScriptVmFactory : IScriptVmFactory
|
|||||||
return _container.Resolve<ScriptConfigurationViewModel>(new object[] { profile, scriptConfiguration });
|
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>
|
public class LogsDebugView : ReactiveUserControl<LogsDebugViewModel>
|
||||||
{
|
{
|
||||||
private int _lineCount;
|
private int _lineCount;
|
||||||
private TextEditor _textEditor;
|
private TextEditor? _textEditor;
|
||||||
|
|
||||||
public LogsDebugView()
|
public LogsDebugView()
|
||||||
{
|
{
|
||||||
@ -31,7 +31,7 @@ public class LogsDebugView : ReactiveUserControl<LogsDebugViewModel>
|
|||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
base.OnInitialized();
|
base.OnInitialized();
|
||||||
Dispatcher.UIThread.Post(() => _textEditor.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
|
Dispatcher.UIThread.Post(() => _textEditor?.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTextChanged(object? sender, EventArgs e)
|
private void OnTextChanged(object? sender, EventArgs e)
|
||||||
@ -49,7 +49,7 @@ public class LogsDebugView : ReactiveUserControl<LogsDebugViewModel>
|
|||||||
//we need this help distance because of rounding.
|
//we need this help distance because of rounding.
|
||||||
//if we scroll slightly above the end, we still want it
|
//if we scroll slightly above the end, we still want it
|
||||||
//to scroll down to the new lines.
|
//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 we were at the bottom of the log and
|
||||||
//if the last log event was 5 lines long
|
//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,
|
//if we are more than that out of sync,
|
||||||
//the user scrolled up and we should not
|
//the user scrolled up and we should not
|
||||||
//mess with anything.
|
//mess with anything.
|
||||||
if (_lineCount == 0 || linesAdded + graceDistance > outOfScreenLines)
|
if (_lineCount == 0 || linesAdded + GRACE_DISTANCE > outOfScreenLines)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() => _textEditor.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
|
Dispatcher.UIThread.Post(() => _textEditor.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
|
||||||
_lineCount = _textEditor.LineCount;
|
_lineCount = _textEditor.LineCount;
|
||||||
|
|||||||
@ -58,6 +58,7 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
|||||||
_lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!;
|
_lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!;
|
||||||
|
|
||||||
mainWindowService.ConfigureMainWindowProvider(this);
|
mainWindowService.ConfigureMainWindowProvider(this);
|
||||||
|
mainWindowService.HostScreen = this;
|
||||||
|
|
||||||
DisplayAccordingToSettings();
|
DisplayAccordingToSettings();
|
||||||
Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel);
|
Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel);
|
||||||
@ -230,11 +231,6 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public void SaveWindowBounds(int x, int y, int width, int height)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class EmptyViewModel : MainScreenViewModel
|
internal class EmptyViewModel : MainScreenViewModel
|
||||||
|
|||||||
@ -2,10 +2,12 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
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"
|
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">
|
<Border Classes="router-container">
|
||||||
<TabControl Margin="12" Items="{Binding SettingTabs}">
|
<TabControl Margin="12" Items="{CompiledBinding SettingTabs}" SelectedItem="{CompiledBinding SelectedTab}">
|
||||||
<TabControl.ItemTemplate>
|
<TabControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding DisplayName}" />
|
<TextBlock Text="{Binding DisplayName}" />
|
||||||
|
|||||||
@ -6,10 +6,13 @@ namespace Artemis.UI.Screens.Settings;
|
|||||||
|
|
||||||
public class SettingsViewModel : MainScreenViewModel
|
public class SettingsViewModel : MainScreenViewModel
|
||||||
{
|
{
|
||||||
|
private ActivatableViewModelBase _selectedTab;
|
||||||
|
|
||||||
public SettingsViewModel(IScreen hostScreen,
|
public SettingsViewModel(IScreen hostScreen,
|
||||||
GeneralTabViewModel generalTabViewModel,
|
GeneralTabViewModel generalTabViewModel,
|
||||||
PluginsTabViewModel pluginsTabViewModel,
|
PluginsTabViewModel pluginsTabViewModel,
|
||||||
DevicesTabViewModel devicesTabViewModel,
|
DevicesTabViewModel devicesTabViewModel,
|
||||||
|
ReleasesTabViewModel releasesTabViewModel,
|
||||||
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
|
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
|
||||||
{
|
{
|
||||||
SettingTabs = new ObservableCollection<ActivatableViewModelBase>
|
SettingTabs = new ObservableCollection<ActivatableViewModelBase>
|
||||||
@ -17,9 +20,17 @@ public class SettingsViewModel : MainScreenViewModel
|
|||||||
generalTabViewModel,
|
generalTabViewModel,
|
||||||
pluginsTabViewModel,
|
pluginsTabViewModel,
|
||||||
devicesTabViewModel,
|
devicesTabViewModel,
|
||||||
|
releasesTabViewModel,
|
||||||
aboutTabViewModel
|
aboutTabViewModel
|
||||||
};
|
};
|
||||||
|
_selectedTab = generalTabViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<ActivatableViewModelBase> SettingTabs { get; }
|
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:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
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:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
|
||||||
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
|
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:mdc="clr-namespace:Markdown.Avalonia.Controls;assembly=Markdown.Avalonia"
|
xmlns:mde="clr-namespace:Markdown.Avalonia.Extensions;assembly=Markdown.Avalonia"
|
||||||
xmlns:mde="clr-namespace:Markdown.Avalonia.Extensions;assembly=Markdown.Avalonia"
|
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
|
||||||
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
|
xmlns:avalonia1="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseAvailableView"
|
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
|
||||||
x:DataType="updating:ReleaseAvailableViewModel"
|
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
|
||||||
Title="Artemis | Update available"
|
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseView"
|
||||||
Width="750"
|
x:DataType="updating:ReleaseViewModel">
|
||||||
Height="750"
|
<UserControl.Resources>
|
||||||
WindowStartupLocation="CenterOwner">
|
<converters:BytesToStringConverter x:Key="BytesToStringConverter" />
|
||||||
<Grid Margin="15" RowDefinitions="Auto,*,Auto">
|
</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">
|
<Grid RowDefinitions="Auto,*" IsVisible="{CompiledBinding !Loading}" Classes="fade-in">
|
||||||
A new Artemis update is available! 🥳
|
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
|
||||||
</TextBlock>
|
<StackPanel>
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Classes="h4 no-margin">Release info</TextBlock>
|
||||||
|
|
||||||
<StackPanel Grid.Row="1" IsVisible="{CompiledBinding Release, Converter={x:Static ObjectConverters.IsNull}}" VerticalAlignment="Center" HorizontalAlignment="Center">
|
<Panel Grid.Column="1" IsVisible="{CompiledBinding InstallationAvailable}">
|
||||||
<TextBlock>Retrieving release...</TextBlock>
|
<!-- Install progress -->
|
||||||
<ProgressBar IsIndeterminate="True"></ProgressBar>
|
<Grid ColumnDefinitions="*,*"
|
||||||
</StackPanel>
|
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>
|
||||||
|
|
||||||
<Border Grid.Row="1" Classes="card" IsVisible="{CompiledBinding Release, Converter={x:Static ObjectConverters.IsNotNull}}">
|
<Panel IsVisible="{CompiledBinding !InstallationInProgress}" HorizontalAlignment="Right">
|
||||||
<Grid RowDefinitions="Auto,*">
|
<!-- Install button -->
|
||||||
<StackPanel Grid.Row="0">
|
<Button Classes="accent"
|
||||||
<StackPanel Orientation="Horizontal">
|
Width="80"
|
||||||
<TextBlock Text="You are currently running version " />
|
Command="{CompiledBinding Install}"
|
||||||
<TextBlock Text="{CompiledBinding CurrentVersion, Mode=OneWay}"></TextBlock>
|
IsVisible="{CompiledBinding !InstallationFinished}">
|
||||||
<TextBlock Text=" while the latest build is " />
|
Install
|
||||||
<TextBlock Text="{CompiledBinding Release.Version, Mode=OneWay, FallbackValue='Unknown'}"></TextBlock>
|
</Button>
|
||||||
<TextBlock Text="." />
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<TextBlock Text="Updating Artemis will give you the latest bug(fixes), features and improvements." />
|
<!-- Restart button -->
|
||||||
<Separator Classes="card-separator" />
|
<Grid ColumnDefinitions="*,*" IsVisible="{CompiledBinding InstallationFinished}">
|
||||||
</StackPanel>
|
<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>
|
||||||
|
|
||||||
<avalonia:MarkdownScrollViewer Grid.Row="1"
|
</Grid>
|
||||||
VerticalAlignment="Top"
|
<Separator Classes="card-separator" />
|
||||||
Markdown="{CompiledBinding Release.Changelog}">
|
<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>
|
||||||
|
|
||||||
|
<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>
|
<avalonia:MarkdownScrollViewer.Styles>
|
||||||
<Style Selector="ctxt|CTextBlock">
|
<Style Selector="ctxt|CTextBlock">
|
||||||
<Style.Setters>
|
<Style.Setters>
|
||||||
@ -207,10 +321,6 @@
|
|||||||
</avalonia:MarkdownScrollViewer>
|
</avalonia:MarkdownScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</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>
|
</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)
|
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));
|
_hostScreen.Router.Navigate.Execute(sidebarScreenViewModel.CreateInstance(_container, _hostScreen));
|
||||||
_profileEditorService.ChangeCurrentProfileConfiguration(null);
|
_profileEditorService.ChangeCurrentProfileConfiguration(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,5 +4,5 @@ namespace Artemis.UI.Services.Updating;
|
|||||||
|
|
||||||
public interface IUpdateNotificationProvider
|
public interface IUpdateNotificationProvider
|
||||||
{
|
{
|
||||||
Task ShowNotification(string releaseId);
|
void ShowNotification(string releaseId, string releaseVersion);
|
||||||
}
|
}
|
||||||
@ -1,11 +1,18 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.UI.Services.Interfaces;
|
using Artemis.UI.Services.Interfaces;
|
||||||
|
using Artemis.WebClient.Updating;
|
||||||
|
|
||||||
namespace Artemis.UI.Services.Updating;
|
namespace Artemis.UI.Services.Updating;
|
||||||
|
|
||||||
public interface IUpdateService : IArtemisUIService
|
public interface IUpdateService : IArtemisUIService
|
||||||
{
|
{
|
||||||
Task<bool> CheckForUpdate();
|
|
||||||
Task InstallRelease(string releaseId);
|
|
||||||
string? CurrentVersion { get; }
|
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.IO.Compression;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
|
using System.Security.Cryptography;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
|
using Artemis.UI.Exceptions;
|
||||||
using Artemis.UI.Extensions;
|
using Artemis.UI.Extensions;
|
||||||
using Artemis.WebClient.Updating;
|
using Artemis.WebClient.Updating;
|
||||||
using Octodiff.Core;
|
using Octodiff.Core;
|
||||||
@ -18,7 +20,7 @@ namespace Artemis.UI.Services.Updating;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the installation process of a release
|
/// Represents the installation process of a release
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ReleaseInstaller
|
public class ReleaseInstaller : CorePropertyChanged
|
||||||
{
|
{
|
||||||
private readonly string _dataFolder;
|
private readonly string _dataFolder;
|
||||||
private readonly HttpClient _httpClient;
|
private readonly HttpClient _httpClient;
|
||||||
@ -26,6 +28,10 @@ public class ReleaseInstaller
|
|||||||
private readonly string _releaseId;
|
private readonly string _releaseId;
|
||||||
private readonly Platform _updatePlatform;
|
private readonly Platform _updatePlatform;
|
||||||
private readonly IUpdatingClient _updatingClient;
|
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)
|
public ReleaseInstaller(string releaseId, ILogger logger, IUpdatingClient updatingClient, HttpClient httpClient)
|
||||||
{
|
{
|
||||||
@ -46,29 +52,42 @@ public class ReleaseInstaller
|
|||||||
|
|
||||||
if (!Directory.Exists(_dataFolder))
|
if (!Directory.Exists(_dataFolder))
|
||||||
Directory.CreateDirectory(_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 float Progress
|
||||||
public Progress<float> StepProgress { get; } = new();
|
{
|
||||||
|
get => _progress1;
|
||||||
|
set => SetAndNotify(ref _progress1, value);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task InstallAsync(CancellationToken cancellationToken)
|
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);
|
_logger.Information("Retrieving details for release {ReleaseId}", _releaseId);
|
||||||
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken);
|
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken);
|
||||||
result.EnsureNoErrors();
|
result.EnsureNoErrors();
|
||||||
|
|
||||||
IGetReleaseById_Release? release = result.Data?.Release;
|
IGetReleaseById_PublishedRelease? release = result.Data?.PublishedRelease;
|
||||||
if (release == null)
|
if (release == null)
|
||||||
throw new Exception($"Could not find release with ID {_releaseId}");
|
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)
|
if (artifact == null)
|
||||||
throw new Exception("Found the release but it has no artifact for the current platform");
|
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
|
// 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)
|
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);
|
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 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);
|
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
|
||||||
|
await PatchDelta(stream, previousRelease, artifact, cancellationToken);
|
||||||
await PatchDelta(stream, previousRelease, 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);
|
await using FileStream newFileStream = new(Path.Combine(_dataFolder, $"{_releaseId}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
|
||||||
|
await using (FileStream baseStream = File.OpenRead(previousRelease))
|
||||||
deltaStream.Seek(0, SeekOrigin.Begin);
|
|
||||||
|
|
||||||
await Task.Run(() =>
|
|
||||||
{
|
{
|
||||||
DeltaApplier deltaApplier = new();
|
deltaStream.Seek(0, SeekOrigin.Begin);
|
||||||
deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new DeltaApplierProgressReporter(StepProgress)), newFileStream);
|
DeltaApplier deltaApplier = new() {SkipHashCheck = true};
|
||||||
});
|
// Patching is not async and so fast that it's not worth adding a progress reporter
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
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);
|
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();
|
// 10 - 60%
|
||||||
await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/download/{artifact.ArtifactId}", stream, StepProgress, cancellationToken);
|
_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);
|
await Extract(stream, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task Extract(Stream archiveStream, CancellationToken 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
|
// Ensure the directory is empty
|
||||||
string extractDirectory = Path.Combine(_dataFolder, "pending");
|
string extractDirectory = Path.Combine(_dataFolder, "pending");
|
||||||
if (Directory.Exists(extractDirectory))
|
if (Directory.Exists(extractDirectory))
|
||||||
Directory.Delete(extractDirectory, true);
|
Directory.Delete(extractDirectory, true);
|
||||||
Directory.CreateDirectory(extractDirectory);
|
Directory.CreateDirectory(extractDirectory);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
await Task.Run(() =>
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
archiveStream.Seek(0, SeekOrigin.Begin);
|
archiveStream.Seek(0, SeekOrigin.Begin);
|
||||||
using ZipArchive archive = new(archiveStream);
|
using ZipArchive archive = new(archiveStream);
|
||||||
archive.ExtractToDirectory(extractDirectory, false, StepProgress, cancellationToken);
|
archive.ExtractToDirectory(extractDirectory, false, _stepProgress, cancellationToken);
|
||||||
});
|
}, cancellationToken);
|
||||||
|
|
||||||
((IProgress<float>) OverallProgress).Report(100);
|
((IProgress<float>) _progress).Report(100);
|
||||||
}
|
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class DeltaApplierProgressReporter : IProgressReporter
|
private async Task ValidateArchive(Stream archiveStream, IGetReleaseById_PublishedRelease_Artifacts artifact, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
private readonly IProgress<float> _stepProgress;
|
using MD5 md5 = MD5.Create();
|
||||||
|
archiveStream.Seek(0, SeekOrigin.Begin);
|
||||||
public DeltaApplierProgressReporter(IProgress<float> stepProgress)
|
string hash = BitConverter.ToString(await md5.ComputeHashAsync(archiveStream, cancellationToken)).Replace("-", "");
|
||||||
{
|
if (hash != artifact.FileInfo.Md5Hash)
|
||||||
_stepProgress = stepProgress;
|
throw new ArtemisUIException($"Update file hash mismatch, expected \"{artifact.FileInfo.Md5Hash}\" but got \"{hash}\"");
|
||||||
}
|
|
||||||
|
|
||||||
public void ReportProgress(string operation, long currentPosition, long total)
|
|
||||||
{
|
|
||||||
_stepProgress.Report(currentPosition / total * 100);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 System.Threading.Tasks;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Screens.Settings.Updating;
|
using Artemis.Storage.Entities.General;
|
||||||
using Artemis.UI.Services.Updating;
|
using Artemis.Storage.Repositories.Interfaces;
|
||||||
using Artemis.UI.Shared.Services;
|
|
||||||
using Artemis.UI.Shared.Services.MainWindow;
|
using Artemis.UI.Shared.Services.MainWindow;
|
||||||
using Artemis.WebClient.Updating;
|
using Artemis.WebClient.Updating;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
@ -14,7 +13,7 @@ using Serilog;
|
|||||||
using StrawberryShake;
|
using StrawberryShake;
|
||||||
using Timer = System.Timers.Timer;
|
using Timer = System.Timers.Timer;
|
||||||
|
|
||||||
namespace Artemis.UI.Services;
|
namespace Artemis.UI.Services.Updating;
|
||||||
|
|
||||||
public class UpdateService : IUpdateService
|
public class UpdateService : IUpdateService
|
||||||
{
|
{
|
||||||
@ -26,24 +25,24 @@ public class UpdateService : IUpdateService
|
|||||||
|
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IMainWindowService _mainWindowService;
|
private readonly IMainWindowService _mainWindowService;
|
||||||
|
private readonly IQueuedActionRepository _queuedActionRepository;
|
||||||
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
|
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
|
||||||
private readonly Platform _updatePlatform;
|
private readonly Platform _updatePlatform;
|
||||||
private readonly IUpdatingClient _updatingClient;
|
private readonly IUpdatingClient _updatingClient;
|
||||||
private readonly IWindowService _windowService;
|
|
||||||
|
|
||||||
private bool _suspendAutoCheck;
|
private bool _suspendAutoCheck;
|
||||||
|
|
||||||
public UpdateService(ILogger logger,
|
public UpdateService(ILogger logger,
|
||||||
ISettingsService settingsService,
|
ISettingsService settingsService,
|
||||||
IMainWindowService mainWindowService,
|
IMainWindowService mainWindowService,
|
||||||
IWindowService windowService,
|
IQueuedActionRepository queuedActionRepository,
|
||||||
IUpdatingClient updatingClient,
|
IUpdatingClient updatingClient,
|
||||||
Lazy<IUpdateNotificationProvider> updateNotificationProvider,
|
Lazy<IUpdateNotificationProvider> updateNotificationProvider,
|
||||||
Func<string, ReleaseInstaller> getReleaseInstaller)
|
Func<string, ReleaseInstaller> getReleaseInstaller)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mainWindowService = mainWindowService;
|
_mainWindowService = mainWindowService;
|
||||||
_windowService = windowService;
|
_queuedActionRepository = queuedActionRepository;
|
||||||
_updatingClient = updatingClient;
|
_updatingClient = updatingClient;
|
||||||
_updateNotificationProvider = updateNotificationProvider;
|
_updateNotificationProvider = updateNotificationProvider;
|
||||||
_getReleaseInstaller = getReleaseInstaller;
|
_getReleaseInstaller = getReleaseInstaller;
|
||||||
@ -65,35 +64,34 @@ public class UpdateService : IUpdateService
|
|||||||
Timer timer = new(UPDATE_CHECK_INTERVAL);
|
Timer timer = new(UPDATE_CHECK_INTERVAL);
|
||||||
timer.Elapsed += HandleAutoUpdateEvent;
|
timer.Elapsed += HandleAutoUpdateEvent;
|
||||||
timer.Start();
|
timer.Start();
|
||||||
|
|
||||||
|
_channel.Value = "feature/gh-actions";
|
||||||
|
_channel.Save();
|
||||||
|
|
||||||
|
|
||||||
|
InstallQueuedUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public string? CurrentVersion
|
private void InstallQueuedUpdate()
|
||||||
{
|
{
|
||||||
get
|
if (!_queuedActionRepository.IsTypeQueued("InstallUpdate"))
|
||||||
{
|
return;
|
||||||
object[] attributes = typeof(UpdateService).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false);
|
|
||||||
return attributes.Length == 0 ? null : ((AssemblyInformationalVersionAttribute) attributes[0]).InformationalVersion;
|
// 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 () =>
|
_updateNotificationProvider.Value.ShowNotification(release.Id, release.Version);
|
||||||
{
|
|
||||||
// Main window is probably already open but this will bring it into focus
|
|
||||||
_mainWindowService.OpenMainWindow();
|
|
||||||
await _windowService.ShowDialogAsync<ReleaseAvailableViewModel>(nextReleaseId);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ShowUpdateNotification(string nextReleaseId)
|
private async Task AutoInstallUpdate(IGetNextRelease_NextPublishedRelease release)
|
||||||
{
|
{
|
||||||
await _updateNotificationProvider.Value.ShowNotification(nextReleaseId);
|
ReleaseInstaller installer = _getReleaseInstaller(release.Id);
|
||||||
}
|
|
||||||
|
|
||||||
private async Task AutoInstallUpdate(string nextReleaseId)
|
|
||||||
{
|
|
||||||
ReleaseInstaller installer = _getReleaseInstaller(nextReleaseId);
|
|
||||||
await installer.InstallAsync(CancellationToken.None);
|
await installer.InstallAsync(CancellationToken.None);
|
||||||
Utilities.ApplyUpdate(true);
|
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()
|
public async Task<bool> CheckForUpdate()
|
||||||
{
|
{
|
||||||
string? currentVersion = CurrentVersion;
|
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(CurrentVersion, _channel.Value, _updatePlatform);
|
||||||
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);
|
|
||||||
result.EnsureNoErrors();
|
result.EnsureNoErrors();
|
||||||
|
|
||||||
|
// Update cache
|
||||||
|
CachedLatestRelease = result.Data?.NextPublishedRelease;
|
||||||
|
|
||||||
// No update was found
|
// No update was found
|
||||||
if (result.Data?.NextRelease == null)
|
if (CachedLatestRelease == null)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Only offer it once per session
|
// Only offer it once per session
|
||||||
_suspendAutoCheck = true;
|
_suspendAutoCheck = true;
|
||||||
|
|
||||||
// If the window is open show the changelog, don't auto-update while the user is busy
|
// If the window is open show the changelog, don't auto-update while the user is busy
|
||||||
if (_mainWindowService.IsMainWindowOpen)
|
if (!_autoInstall.Value)
|
||||||
await ShowUpdateDialog(result.Data.NextRelease.Id);
|
ShowUpdateNotification(CachedLatestRelease);
|
||||||
else if (!_autoInstall.Value)
|
|
||||||
await ShowUpdateNotification(result.Data.NextRelease.Id);
|
|
||||||
else
|
else
|
||||||
await AutoInstallUpdate(result.Data.NextRelease.Id);
|
await AutoInstallUpdate(CachedLatestRelease);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task InstallRelease(string releaseId)
|
public void QueueUpdate()
|
||||||
{
|
{
|
||||||
ReleaseInstaller installer = _getReleaseInstaller(releaseId);
|
if (!_queuedActionRepository.IsTypeQueued("InstallUpdate"))
|
||||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
_queuedActionRepository.Add(new QueuedActionEntity {Type = "InstallUpdate"});
|
||||||
{
|
}
|
||||||
// Main window is probably already open but this will bring it into focus
|
|
||||||
_mainWindowService.OpenMainWindow();
|
/// <inheritdoc />
|
||||||
_windowService.ShowWindow<ReleaseInstallerViewModel>(installer);
|
public void DequeueUpdate()
|
||||||
});
|
{
|
||||||
|
_queuedActionRepository.ClearByType("InstallUpdate");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public ReleaseInstaller GetReleaseInstaller(string releaseId)
|
||||||
|
{
|
||||||
|
return _getReleaseInstaller(releaseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void RestartForUpdate(bool silent)
|
||||||
|
{
|
||||||
|
DequeueUpdate();
|
||||||
|
Utilities.ApplyUpdate(silent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -13,7 +13,7 @@ namespace Artemis.VisualScripting.DryIoc;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class UIContainerExtensions
|
public static class ContainerExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Registers NoStringEvaluating services into the container.
|
/// Registers NoStringEvaluating services into the container.
|
||||||
|
|||||||
@ -3,7 +3,7 @@ using SkiaSharp;
|
|||||||
|
|
||||||
namespace Artemis.VisualScripting.Nodes.Color;
|
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 class HslSKColorNode : Node
|
||||||
{
|
{
|
||||||
public HslSKColorNode()
|
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;
|
namespace Artemis.VisualScripting.Nodes.Conversion;
|
||||||
|
|
||||||
@ -33,6 +33,7 @@ public class ConvertToNumericNode : Node
|
|||||||
double input => new Numeric(input),
|
double input => new Numeric(input),
|
||||||
float input => new Numeric(input),
|
float input => new Numeric(input),
|
||||||
byte input => new Numeric(input),
|
byte input => new Numeric(input),
|
||||||
|
bool input => new Numeric(input ? 1 : 0),
|
||||||
_ => TryParse(Input.Value)
|
_ => 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!) {
|
query GetReleaseById($id: String!) {
|
||||||
release(id: $id) {
|
publishedRelease(id: $id) {
|
||||||
branch
|
branch
|
||||||
commit
|
commit
|
||||||
version
|
version
|
||||||
@ -23,9 +47,10 @@ fragment fileInfo on ArtifactFileInfo {
|
|||||||
downloadSize
|
downloadSize
|
||||||
}
|
}
|
||||||
|
|
||||||
query GetNextRelease($currentVersion: String!, $branch: String!, $platform: Platform!) {
|
query GetNextRelease($currentVersion: String, $branch: String!, $platform: Platform!) {
|
||||||
nextRelease(version: $currentVersion, branch: $branch, platform: $platform) {
|
nextPublishedRelease(version: $currentVersion, branch: $branch, platform: $platform) {
|
||||||
id
|
id
|
||||||
version
|
version
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,7 +41,6 @@ type Artifact {
|
|||||||
artifactId: Long!
|
artifactId: Long!
|
||||||
deltaFileInfo: ArtifactFileInfo!
|
deltaFileInfo: ArtifactFileInfo!
|
||||||
fileInfo: ArtifactFileInfo!
|
fileInfo: ArtifactFileInfo!
|
||||||
fileName(deltaFile: Boolean!): String!
|
|
||||||
platform: Platform!
|
platform: Platform!
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,13 +54,69 @@ type Mutation {
|
|||||||
updateReleaseChangelog(input: UpdateReleaseChangelogInput!): UpdateReleaseChangelogPayload!
|
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 {
|
type Query {
|
||||||
channelByBranch(branch: String!): ArtemisChannel
|
channelByBranch(branch: String!): ArtemisChannel
|
||||||
channels: [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
|
release(id: String!): Release
|
||||||
releaseStatistics(order: [ReleaseStatisticSortInput!], where: ReleaseStatisticFilterInput): [ReleaseStatistic!]!
|
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 {
|
type Release {
|
||||||
@ -86,6 +141,25 @@ type ReleaseStatistic {
|
|||||||
windowsCount: Int!
|
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 {
|
type UpdateReleaseChangelogPayload {
|
||||||
release: Release
|
release: Release
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user