1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Merge pull request #769 from Artemis-RGB/development

UI - Reworked updating
This commit is contained in:
RobertBeekman 2023-03-26 14:59:23 +02:00 committed by GitHub
commit 636b32bd86
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
80 changed files with 3098 additions and 1271 deletions

109
.github/workflows/master.yml vendored Normal file
View File

@ -0,0 +1,109 @@
name: Master - Build & Upload to Ftp
on:
workflow_dispatch:
push:
jobs:
version:
runs-on: ubuntu-latest
outputs:
version-number: ${{ steps.get-version.outputs.version-number }}
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Get Version String
id: get-version
shell: pwsh
run: |
$MidnightUtc = [DateTime]::UtcNow.Date
$BranchName = "${{ github.ref_name }}".replace('/','-').replace('.','-')
$ApiVersion = (Select-Xml -Path 'src/Artemis.Core/Artemis.Core.csproj' -XPath '//PluginApiVersion').Node.InnerText
$NumberOfCommitsToday = (git log --after=$($MidnightUtc.ToString("o")) --oneline | Measure-Object -Line).Lines
$VersionNumber = "$ApiVersion.$($MidnightUtc.ToString("yyyy.MMdd")).$NumberOfCommitsToday"
# If we're not in master, add the branch name to the version so it counts as prerelease
if ($BranchName -ne "master") { $VersionNumber += "-$BranchName" }
"version-number=$VersionNumber" >> $Env:GITHUB_OUTPUT
build:
needs: version
strategy:
matrix:
include:
- os: windows-latest
rid: win10-x64
csproj: Windows
- os: ubuntu-latest
rid: linux-x64
csproj: Linux
- os: macos-latest
rid: osx-x64
csproj: MacOS
name: ${{ matrix.csproj }} Build
runs-on: ${{ matrix.os }}
steps:
- name: Checkout Artemis
uses: actions/checkout@v3
with:
path: Artemis
- name: Checkout Plugins
uses: actions/checkout@v3
with:
repository: Artemis-RGB/Artemis.Plugins
path: Artemis.Plugins
- name: Setup .NET
uses: actions/setup-dotnet@v2
with:
dotnet-version: '6.0.x'
- name: Publish Artemis
run: dotnet publish --configuration Release -p:Version=${{ needs.version.outputs.version-number }} --runtime ${{ matrix.rid }} --output build/${{ matrix.rid }} --self-contained Artemis/src/Artemis.UI.${{ matrix.csproj }}/Artemis.UI.${{ matrix.csproj }}.csproj
- name: Publish Plugins
run: |
New-Item -ItemType "Directory" -Path build/${{ matrix.rid }}/Plugins/
Get-ChildItem -File -Recurse -Filter *.csproj Artemis.Plugins/src/ |
Foreach-Object -Parallel {
dotnet publish --configuration Release --runtime ${{ matrix.rid }} --output build-plugins/$($_.BaseName) --no-self-contained $($_.FullName);
Compress-Archive -Path "build-plugins/$($_.BaseName)" -DestinationPath "build/${{ matrix.rid }}/Plugins/$($_.BaseName).zip";
}
shell: pwsh
- name: Upload Artifact
uses: actions/upload-artifact@v3
with:
name: artemis-${{ matrix.rid }}-${{ needs.version.outputs.version-number }}
path: build/${{ matrix.rid }}
notify:
name: Notify Backend of build
runs-on: ubuntu-latest
needs: build
steps:
- name: post thing
if: needs.build.result == 'success'
shell: pwsh
run: |
$tokenUri = "https://identity.artemis-rgb.com/connect/token"
$headers = @{
"Content-Type" = "application/x-www-form-urlencoded"
}
$body = @{
"grant_type" = "client_credentials"
"client_id" = "github.task-runners"
"client_secret" = "${{ secrets.UPDATE_SECRET }}"
"scope" = "artemis-updating.releases:retrieve"
}
$response = Invoke-RestMethod -Method Post -Uri $tokenUri -Body $body -Headers $headers
$accessToken = $response.access_token
$apiUri = "https://updating.artemis-rgb.com/api/github/retrieve-run/${{ github.run_id }}"
$authHeader = @{
"Authorization" = "Bearer $accessToken"
}
$updateResponse = Invoke-RestMethod -Method Post -Uri $apiUri -Headers $authHeader

View File

@ -27,7 +27,7 @@ jobs:
$VersionNumber = "$ApiVersion.$($MidnightUtc.ToString("yyyy.MMdd")).$NumberOfCommitsToday"
# If we're not in master, add the branch name to the version so it counts as prerelease
if ($BranchName -ne "master") { $VersionNumber += "-$BranchName" }
Write-Output "::set-output name=version-number::$VersionNumber"
"version-number=$VersionNumber" >> $Env:GITHUB_OUTPUT
nuget:
name: Publish Nuget Packages

View File

@ -48,6 +48,10 @@ public static class Constants
/// The full path to the Artemis logs folder
/// </summary>
public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs");
/// <summary>
/// The full path to the Artemis logs folder
/// </summary>
public static readonly string UpdatingFolder = Path.Combine(DataFolder, "updating");
/// <summary>
/// The full path to the Artemis plugins folder
@ -62,32 +66,24 @@ public static class Constants
/// <summary>
/// The current API version for plugins
/// </summary>
public static readonly int PluginApiVersion = int.Parse(CoreAssembly.GetCustomAttributes<AssemblyMetadataAttribute>().First(a => a.Key == "PluginApiVersion").Value ??
public static readonly int PluginApiVersion = int.Parse(CoreAssembly.GetCustomAttributes<AssemblyMetadataAttribute>().FirstOrDefault(a => a.Key == "PluginApiVersion")?.Value ??
throw new InvalidOperationException("Cannot find PluginApiVersion metadata in assembly"));
/// <summary>
/// The current version of the application
/// </summary>
public static readonly string CurrentVersion = CoreAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion != "1.0.0"
? CoreAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion
: "local";
/// <summary>
/// The plugin info used by core components of Artemis
/// </summary>
public static readonly PluginInfo CorePluginInfo = new()
{
Guid = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), Name = "Artemis Core", Version = new Version(2, 0)
Guid = Guid.Parse("ffffffff-ffff-ffff-ffff-ffffffffffff"), Name = "Artemis Core", Version = CurrentVersion
};
/// <summary>
/// The build information related to the currently running Artemis build
/// <para>Information is retrieved from <c>buildinfo.json</c></para>
/// </summary>
public static readonly BuildInfo BuildInfo = File.Exists(Path.Combine(ApplicationFolder, "buildinfo.json"))
? JsonConvert.DeserializeObject<BuildInfo>(File.ReadAllText(Path.Combine(ApplicationFolder, "buildinfo.json")))!
: new BuildInfo
{
IsLocalBuild = true,
BuildId = 1337,
BuildNumber = 1337,
SourceBranch = "local",
SourceVersion = "local"
};
/// <summary>
/// The plugin used by core components of Artemis
/// </summary>

View File

@ -0,0 +1,20 @@
using System;
using System.Collections.Generic;
namespace Artemis.Core;
/// <summary>
/// Provides data about application update events
/// </summary>
public class UpdateEventArgs : EventArgs
{
internal UpdateEventArgs(bool silent)
{
Silent = silent;
}
/// <summary>
/// Gets a boolean indicating whether to silently update or not.
/// </summary>
public bool Silent { get; }
}

View File

@ -25,7 +25,7 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject
private Plugin _plugin = null!;
private Uri? _repository;
private bool _requiresAdmin;
private Version _version = null!;
private string _version = null!;
private Uri? _website;
internal PluginInfo()
@ -107,7 +107,7 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject
/// The version of the plugin
/// </summary>
[JsonProperty(Required = Required.Always)]
public Version Version
public string Version
{
get => _version;
internal set => SetAndNotify(ref _version, value);

View File

@ -200,23 +200,17 @@ internal class CoreService : ICoreService
if (IsInitialized)
throw new ArtemisCoreException("Cannot initialize the core as it is already initialized.");
AssemblyInformationalVersionAttribute? versionAttribute = typeof(CoreService).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
_logger.Information(
"Initializing Artemis Core version {version}, build {buildNumber} branch {branch}.",
versionAttribute?.InformationalVersion,
Constants.BuildInfo.BuildNumber,
Constants.BuildInfo.SourceBranch
);
_logger.Information("Startup arguments: {args}", Constants.StartupArguments);
_logger.Information("Elevated permissions: {perms}", IsElevated);
_logger.Information("Stopwatch high resolution: {perms}", Stopwatch.IsHighResolution);
_logger.Information("Initializing Artemis Core version {CurrentVersion}", Constants.CurrentVersion);
_logger.Information("Startup arguments: {StartupArguments}", Constants.StartupArguments);
_logger.Information("Elevated permissions: {IsElevated}", IsElevated);
_logger.Information("Stopwatch high resolution: {IsHighResolution}", Stopwatch.IsHighResolution);
ApplyLoggingLevel();
// Don't remove even if it looks useless
// Just this line should prevent a certain someone from removing HidSharp as an unused dependency as well
Version? hidSharpVersion = Assembly.GetAssembly(typeof(HidDevice))!.GetName().Version;
_logger.Debug("Forcing plugins to use HidSharp {hidSharpVersion}", hidSharpVersion);
_logger.Debug("Forcing plugins to use HidSharp {HidSharpVersion}", hidSharpVersion);
// Initialize the services
_pluginManagementService.CopyBuiltInPlugins();

View File

@ -47,15 +47,29 @@ internal class PluginManagementService : IPluginManagementService
private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory)
{
ZipArchiveEntry metaDataFileEntry = zipArchive.Entries.First(e => e.Name == "plugin.json");
DirectoryInfo pluginDirectory = new(Path.Combine(Constants.PluginsFolder, targetDirectory));
bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock"));
// Remove the old directory if it exists
if (Directory.Exists(pluginDirectory.FullName))
pluginDirectory.DeleteRecursively();
Directory.CreateDirectory(pluginDirectory.FullName);
zipArchive.ExtractToDirectory(pluginDirectory.FullName, true);
// Extract everything in the same archive directory to the unique plugin directory
Utilities.CreateAccessibleDirectory(pluginDirectory.FullName);
string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, "");
foreach (ZipArchiveEntry zipArchiveEntry in zipArchive.Entries)
{
if (zipArchiveEntry.FullName.StartsWith(metaDataDirectory) && !zipArchiveEntry.FullName.EndsWith("/"))
{
string target = Path.Combine(pluginDirectory.FullName, zipArchiveEntry.FullName.Remove(0, metaDataDirectory.Length));
// Create folders
Utilities.CreateAccessibleDirectory(Path.GetDirectoryName(target)!);
// Extract files
zipArchiveEntry.ExtractToFile(target);
}
}
if (createLockFile)
File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close();
}
@ -82,57 +96,57 @@ internal class PluginManagementService : IPluginManagementService
return;
}
foreach (FileInfo zipFile in builtInPluginDirectory.EnumerateFiles("*.zip"))
{
// Find the metadata file in the zip
using ZipArchive archive = ZipFile.OpenRead(zipFile.FullName);
ZipArchiveEntry? metaDataFileEntry = archive.GetEntry("plugin.json");
if (metaDataFileEntry == null)
throw new ArtemisPluginException("Couldn't find a plugin.json in " + zipFile.FullName);
using StreamReader reader = new(metaDataFileEntry.Open());
PluginInfo builtInPluginInfo = CoreJson.DeserializeObject<PluginInfo>(reader.ReadToEnd())!;
string preferred = builtInPluginInfo.PreferredPluginDirectory;
// Find the matching plugin in the plugin folder
DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == preferred);
if (match == null)
try
{
ExtractBuiltInPlugin(zipFile, pluginDirectory);
}
catch (Exception e)
{
_logger.Error(e, "Failed to copy built-in plugin from {ZipFile}", zipFile.FullName);
}
}
}
private void ExtractBuiltInPlugin(FileInfo zipFile, DirectoryInfo pluginDirectory)
{
// Find the metadata file in the zip
using ZipArchive archive = ZipFile.OpenRead(zipFile.FullName);
ZipArchiveEntry? metaDataFileEntry = archive.Entries.FirstOrDefault(e => e.Name == "plugin.json");
if (metaDataFileEntry == null)
throw new ArtemisPluginException("Couldn't find a plugin.json in " + zipFile.FullName);
using StreamReader reader = new(metaDataFileEntry.Open());
PluginInfo builtInPluginInfo = CoreJson.DeserializeObject<PluginInfo>(reader.ReadToEnd())!;
string preferred = builtInPluginInfo.PreferredPluginDirectory;
// Find the matching plugin in the plugin folder
DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == preferred);
if (match == null)
{
CopyBuiltInPlugin(archive, preferred);
}
else
{
string metadataFile = Path.Combine(match.FullName, "plugin.json");
if (!File.Exists(metadataFile))
{
_logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo);
CopyBuiltInPlugin(archive, preferred);
}
else
else if (metaDataFileEntry.LastWriteTime > File.GetLastWriteTime(metadataFile))
{
string metadataFile = Path.Combine(match.FullName, "plugin.json");
if (!File.Exists(metadataFile))
try
{
_logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo);
_logger.Debug("Copying updated built-in plugin {builtInPluginInfo}", builtInPluginInfo);
CopyBuiltInPlugin(archive, preferred);
}
else
catch (Exception e)
{
PluginInfo pluginInfo;
try
{
// Compare versions, copy if the same when debugging
pluginInfo = CoreJson.DeserializeObject<PluginInfo>(File.ReadAllText(metadataFile))!;
}
catch (Exception e)
{
throw new ArtemisPluginException($"Failed read plugin metadata needed to install built-in plugin: {e.Message}", e);
}
try
{
if (builtInPluginInfo.Version > pluginInfo.Version)
{
_logger.Debug("Copying updated built-in plugin from {pluginInfo} to {builtInPluginInfo}", pluginInfo, builtInPluginInfo);
CopyBuiltInPlugin(archive, preferred);
}
}
catch (Exception e)
{
throw new ArtemisPluginException($"Failed to install built-in plugin: {e.Message}", e);
}
throw new ArtemisPluginException($"Failed to install built-in plugin: {e.Message}", e);
}
}
}
@ -556,7 +570,6 @@ internal class PluginManagementService : IPluginManagementService
if (metaDataFileEntry == null)
throw new ArtemisPluginException("Couldn't find a plugin.json in " + fileName);
using StreamReader reader = new(metaDataFileEntry.Open());
PluginInfo pluginInfo = CoreJson.DeserializeObject<PluginInfo>(reader.ReadToEnd())!;
if (!pluginInfo.Main.EndsWith(".dll"))

View File

@ -109,22 +109,26 @@ internal class ProfileService : IProfileService
if (profileConfiguration.HotkeyMode == ProfileConfigurationHotkeyMode.None)
return;
bool before = profileConfiguration.IsSuspended;
foreach (ArtemisKeyboardKeyEventArgs e in _pendingKeyboardEvents)
{
if (profileConfiguration.HotkeyMode == ProfileConfigurationHotkeyMode.Toggle)
{
if (profileConfiguration.EnableHotkey != null &&
profileConfiguration.EnableHotkey.MatchesEventArgs(e))
if (profileConfiguration.EnableHotkey != null && profileConfiguration.EnableHotkey.MatchesEventArgs(e))
profileConfiguration.IsSuspended = !profileConfiguration.IsSuspended;
}
else
{
if (profileConfiguration.IsSuspended && profileConfiguration.EnableHotkey != null && profileConfiguration.EnableHotkey.MatchesEventArgs(e))
profileConfiguration.IsSuspended = false;
if (!profileConfiguration.IsSuspended && profileConfiguration.DisableHotkey != null && profileConfiguration.DisableHotkey.MatchesEventArgs(e))
else if (!profileConfiguration.IsSuspended && profileConfiguration.DisableHotkey != null && profileConfiguration.DisableHotkey.MatchesEventArgs(e))
profileConfiguration.IsSuspended = true;
}
}
// If suspension was changed, save the category
if (before != profileConfiguration.IsSuspended)
SaveProfileCategory(profileConfiguration.Category);
}
private void CreateDefaultProfileCategories()

View File

@ -21,6 +21,7 @@ public static class Utilities
CreateAccessibleDirectory(Constants.DataFolder);
CreateAccessibleDirectory(Constants.PluginsFolder);
CreateAccessibleDirectory(Constants.LayoutsFolder);
CreateAccessibleDirectory(Constants.UpdatingFolder);
}
/// <summary>
@ -50,6 +51,15 @@ public static class Utilities
OnRestartRequested(new RestartEventArgs(elevate, delay, extraArgs.ToList()));
}
/// <summary>
/// Applies a pending update
/// </summary>
/// <param name="silent">A boolean indicating whether to silently update or not.</param>
public static void ApplyUpdate(bool silent)
{
OnUpdateRequested(new UpdateEventArgs(silent));
}
/// <summary>
/// Opens the provided URL in the default web browser
/// </summary>
@ -102,6 +112,11 @@ public static class Utilities
/// </summary>
public static event EventHandler<RestartEventArgs>? RestartRequested;
/// <summary>
/// Occurs when the core has requested a pending application update to be applied
/// </summary>
public static event EventHandler<UpdateEventArgs>? UpdateRequested;
/// <summary>
/// Opens the provided folder in the user's file explorer
/// </summary>
@ -137,6 +152,11 @@ public static class Utilities
ShutdownRequested?.Invoke(null, EventArgs.Empty);
}
private static void OnUpdateRequested(UpdateEventArgs e)
{
UpdateRequested?.Invoke(null, e);
}
#region Scaling
internal static int RenderScaleMultiplier { get; set; } = 2;

View File

@ -0,0 +1,11 @@
using System;
namespace Artemis.Storage.Entities.General;
public class ReleaseEntity
{
public Guid Id { get; set; }
public string Version { get; set; }
public DateTimeOffset? InstalledAt { get; set; }
}

View File

@ -9,4 +9,6 @@ public interface IQueuedActionRepository : IRepository
void Remove(QueuedActionEntity queuedActionEntity);
List<QueuedActionEntity> GetAll();
List<QueuedActionEntity> GetByType(string type);
bool IsTypeQueued(string type);
void ClearByType(string type);
}

View File

@ -41,5 +41,17 @@ public class QueuedActionRepository : IQueuedActionRepository
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).ToList();
}
/// <inheritdoc />
public bool IsTypeQueued(string type)
{
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).Count() > 0;
}
/// <inheritdoc />
public void ClearByType(string type)
{
_repository.DeleteMany<QueuedActionEntity>(q => q.Type == type);
}
#endregion
}

View File

@ -0,0 +1,38 @@
using System;
using Artemis.Storage.Entities.General;
using Artemis.Storage.Repositories.Interfaces;
using LiteDB;
namespace Artemis.Storage.Repositories;
public class ReleaseRepository : IReleaseRepository
{
private readonly LiteRepository _repository;
public ReleaseRepository(LiteRepository repository)
{
_repository = repository;
_repository.Database.GetCollection<ReleaseEntity>().EnsureIndex(s => s.Version, true);
}
public bool SaveVersionInstallDate(string version)
{
ReleaseEntity release = _repository.Query<ReleaseEntity>().Where(r => r.Version == version).FirstOrDefault();
if (release != null)
return false;
_repository.Insert(new ReleaseEntity {Version = version, InstalledAt = DateTimeOffset.UtcNow});
return true;
}
public ReleaseEntity GetPreviousInstalledVersion()
{
return _repository.Query<ReleaseEntity>().OrderByDescending(r => r.InstalledAt).Skip(1).FirstOrDefault();
}
}
public interface IReleaseRepository : IRepository
{
bool SaveVersionInstallDate(string version);
ReleaseEntity GetPreviousInstalledVersion();
}

View File

@ -30,7 +30,7 @@ public static class StorageManager
{
FileSystemInfo newest = files.OrderByDescending(fi => fi.CreationTime).First();
FileSystemInfo oldest = files.OrderBy(fi => fi.CreationTime).First();
if (DateTime.Now - newest.CreationTime < TimeSpan.FromMinutes(10))
if (DateTime.Now - newest.CreationTime < TimeSpan.FromHours(12))
return;
oldest.Delete();

View 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;
}
}

View File

@ -163,21 +163,16 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa
/// </summary>
/// <param name="looseMatch">Whether the type may be a loose match, meaning it can be cast or converted</param>
/// <param name="filteredTypes">The types to filter</param>
public void ApplyTypeFilter(bool looseMatch, params Type[]? filteredTypes)
public void ApplyTypeFilter(bool looseMatch, params Type?[]? filteredTypes)
{
if (filteredTypes != null)
{
if (filteredTypes.All(t => t == null))
filteredTypes = null;
else
filteredTypes = filteredTypes.Where(t => t != null).ToArray();
}
filteredTypes = filteredTypes.All(t => t == null) ? null : filteredTypes.Where(t => t != null).ToArray();
// If the VM has children, its own type is not relevant
if (Children.Any())
{
foreach (DataModelVisualizationViewModel child in Children)
child?.ApplyTypeFilter(looseMatch, filteredTypes);
child.ApplyTypeFilter(looseMatch, filteredTypes);
IsMatchingFilteredTypes = true;
return;
@ -199,7 +194,7 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa
}
if (looseMatch)
IsMatchingFilteredTypes = filteredTypes.Any(t => t.IsCastableFrom(type) ||
IsMatchingFilteredTypes = filteredTypes.Any(t => t!.IsCastableFrom(type) ||
(t == typeof(Enum) && type.IsEnum) ||
(t == typeof(IEnumerable<>) && type.IsGenericEnumerable()) ||
(type.IsGenericType && t == type.GetGenericTypeDefinition()));
@ -287,7 +282,7 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa
foreach (PropertyInfo propertyInfo in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(t => t.MetadataToken))
{
string childPath = AppendToPath(propertyInfo.Name);
if (Children.Any(c => c?.Path != null && c.Path.Equals(childPath)))
if (Children.Any(c => c.Path != null && c.Path.Equals(childPath)))
continue;
if (propertyInfo.GetCustomAttribute<DataModelIgnoreAttribute>() != null)
continue;

View File

@ -1,31 +0,0 @@
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Providers;
/// <summary>
/// Represents a provider for custom cursors.
/// </summary>
public interface IUpdateProvider
{
/// <summary>
/// Asynchronously checks whether an update is available.
/// </summary>
/// <param name="channel">The channel to use when checking updates (i.e. master or development)</param>
/// <returns>A task returning <see langword="true" /> if an update is available; otherwise <see langword="false" />.</returns>
Task<bool> CheckForUpdate(string channel);
/// <summary>
/// Applies any available updates.
/// </summary>
/// <param name="channel">The channel to use when checking updates (i.e. master or development)</param>
/// <param name="silent">Whether or not to update silently.</param>
Task ApplyUpdate(string channel, bool silent);
/// <summary>
/// Offer to install the update to the user.
/// </summary>
/// <param name="channel">The channel to use when checking updates (i.e. master or development)</param>
/// <param name="windowOpen">A boolean indicating whether the main window is open.</param>
/// <returns>A task returning <see langword="true" /> if the user chose to update; otherwise <see langword="false" />.</returns>
Task OfferUpdate(string channel, bool windowOpen);
}

View File

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

View File

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

View File

@ -21,6 +21,7 @@
<!-- Custom styles -->
<StyleInclude Source="/Styles/Border.axaml" />
<StyleInclude Source="/Styles/Skeleton.axaml" />
<StyleInclude Source="/Styles/Button.axaml" />
<StyleInclude Source="/Styles/Condensed.axaml" />
<StyleInclude Source="/Styles/ColorPickerButton.axaml" />

View 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>

View File

@ -44,8 +44,8 @@ public class App : Application
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || Design.IsDesignMode || _shutDown)
return;
ArtemisBootstrapper.Initialize();
_applicationStateManager = new ApplicationStateManager(_container!, desktop.Args);
ArtemisBootstrapper.Initialize();
RegisterProviders(_container!);
}

View File

@ -25,6 +25,7 @@ public class ApplicationStateManager
Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested;
Core.Utilities.RestartRequested += UtilitiesOnRestartRequested;
Core.Utilities.UpdateRequested += UtilitiesOnUpdateRequested;
// On Windows shutdown dispose the IOC container just so device providers get a chance to clean up
if (Application.Current?.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime)
@ -91,6 +92,25 @@ public class ApplicationStateManager
Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown());
}
private void UtilitiesOnUpdateRequested(object? sender, UpdateEventArgs e)
{
List<string> argsList = new(StartupArguments);
if (e.Silent && !argsList.Contains("--minimized"))
argsList.Add("--minimized");
// Retain startup arguments after update by providing them to the script
string script = Path.Combine(Constants.UpdatingFolder, "installing", "scripts", "update.ps1");
string source = $"-sourceDirectory \"'{Path.Combine(Constants.UpdatingFolder, "installing")}'\"";
string destination = $"-destinationDirectory \"'{Constants.ApplicationFolder}'\"";
string args = argsList.Any() ? $"-artemisArgs \"'{string.Join(' ', argsList)}'\"" : "";
RunScriptWithOutputFile(script, $"{source} {destination} {args}", Path.Combine(Constants.DataFolder, "update-log.txt"));
// Lets try a graceful shutdown, PowerShell will kill if needed
if (Application.Current?.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime)
Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown());
}
private void UtilitiesOnShutdownRequested(object? sender, EventArgs e)
{
// Use PowerShell to kill the process after 8 sec just in case
@ -116,6 +136,20 @@ public class ApplicationStateManager
Process.Start(info);
}
private void RunScriptWithOutputFile(string script, string arguments, string outputFile)
{
// Use > for files that are bigger than 200kb to start fresh, otherwise use >> to append
string redirectSymbol = File.Exists(outputFile) && new FileInfo(outputFile).Length > 200000 ? ">" : ">>";
ProcessStartInfo info = new()
{
Arguments = $"PowerShell -ExecutionPolicy Bypass -File \"{script}\" {arguments} {redirectSymbol} \"{outputFile}\"",
FileName = "PowerShell.exe",
WindowStyle = ProcessWindowStyle.Hidden,
CreateNoWindow = true,
};
Process.Start(info);
}
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern int GetSystemMetrics(int nIndex);
}

View File

@ -12,20 +12,10 @@
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<None Remove=".gitignore" />
<None Remove="Assets\Cursors\aero_crosshair.cur" />
<None Remove="Assets\Cursors\aero_crosshair_minus.cur" />
<None Remove="Assets\Cursors\aero_crosshair_plus.cur" />
<None Remove="Assets\Cursors\aero_drag.cur" />
<None Remove="Assets\Cursors\aero_drag_ew.cur" />
<None Remove="Assets\Cursors\aero_fill.cur" />
<None Remove="Assets\Cursors\aero_pen_min.cur" />
<None Remove="Assets\Cursors\aero_pen_plus.cur" />
<None Remove="Assets\Cursors\aero_rotate.cur" />
<None Remove="Assets\Cursors\aero_rotate_bl.cur" />
<None Remove="Assets\Cursors\aero_rotate_br.cur" />
<None Remove="Assets\Cursors\aero_rotate_tl.cur" />
<None Remove="Assets\Cursors\aero_rotate_tr.cur" />
<Content Include="Scripts\**">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<None Include="..\Artemis.UI\Assets\Images\Logo\application.ico">
<Link>application.ico</Link>
</None>

View File

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

View File

@ -1,278 +0,0 @@
#nullable disable
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Artemis.UI.Windows.Models;
public class DevOpsBuilds
{
[JsonProperty("count")]
public long Count { get; set; }
[JsonProperty("value")]
public List<DevOpsBuild> Builds { get; set; }
}
public class DevOpsBuild
{
[JsonProperty("_links")]
public BuildLinks Links { get; set; }
[JsonProperty("properties")]
public Properties Properties { get; set; }
[JsonProperty("tags")]
public List<object> Tags { get; set; }
[JsonProperty("validationResults")]
public List<object> ValidationResults { get; set; }
[JsonProperty("plans")]
public List<Plan> Plans { get; set; }
[JsonProperty("triggerInfo")]
public TriggerInfo TriggerInfo { get; set; }
[JsonProperty("id")]
public long Id { get; set; }
[JsonProperty("buildNumber")]
public string BuildNumber { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("result")]
public string Result { get; set; }
[JsonProperty("queueTime")]
public DateTimeOffset QueueTime { get; set; }
[JsonProperty("startTime")]
public DateTimeOffset StartTime { get; set; }
[JsonProperty("finishTime")]
public DateTimeOffset FinishTime { get; set; }
[JsonProperty("url")]
public Uri Url { get; set; }
[JsonProperty("definition")]
public Definition Definition { get; set; }
[JsonProperty("buildNumberRevision")]
public long BuildNumberRevision { get; set; }
[JsonProperty("project")]
public Project Project { get; set; }
[JsonProperty("uri")]
public string Uri { get; set; }
[JsonProperty("sourceBranch")]
public string SourceBranch { get; set; }
[JsonProperty("sourceVersion")]
public string SourceVersion { get; set; }
[JsonProperty("priority")]
public string Priority { get; set; }
[JsonProperty("reason")]
public string Reason { get; set; }
[JsonProperty("requestedFor")]
public LastChangedBy RequestedFor { get; set; }
[JsonProperty("requestedBy")]
public LastChangedBy RequestedBy { get; set; }
[JsonProperty("lastChangedDate")]
public DateTimeOffset LastChangedDate { get; set; }
[JsonProperty("lastChangedBy")]
public LastChangedBy LastChangedBy { get; set; }
[JsonProperty("orchestrationPlan")]
public Plan OrchestrationPlan { get; set; }
[JsonProperty("logs")]
public Logs Logs { get; set; }
[JsonProperty("repository")]
public Repository Repository { get; set; }
[JsonProperty("keepForever")]
public bool KeepForever { get; set; }
[JsonProperty("retainedByRelease")]
public bool RetainedByRelease { get; set; }
[JsonProperty("triggeredByBuild")]
public object TriggeredByBuild { get; set; }
}
public class Definition
{
[JsonProperty("drafts")]
public List<object> Drafts { get; set; }
[JsonProperty("id")]
public long Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("url")]
public Uri Url { get; set; }
[JsonProperty("uri")]
public string Uri { get; set; }
[JsonProperty("path")]
public string Path { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("queueStatus")]
public string QueueStatus { get; set; }
[JsonProperty("revision")]
public long Revision { get; set; }
[JsonProperty("project")]
public Project Project { get; set; }
}
public class Project
{
[JsonProperty("id")]
public Guid Id { get; set; }
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("url")]
public Uri Url { get; set; }
[JsonProperty("state")]
public string State { get; set; }
[JsonProperty("revision")]
public long Revision { get; set; }
[JsonProperty("visibility")]
public string Visibility { get; set; }
[JsonProperty("lastUpdateTime")]
public DateTimeOffset LastUpdateTime { get; set; }
}
public class LastChangedBy
{
[JsonProperty("displayName")]
public string DisplayName { get; set; }
[JsonProperty("url")]
public Uri Url { get; set; }
[JsonProperty("_links")]
public LastChangedByLinks Links { get; set; }
[JsonProperty("id")]
public Guid Id { get; set; }
[JsonProperty("uniqueName")]
public object UniqueName { get; set; }
[JsonProperty("imageUrl")]
public object ImageUrl { get; set; }
[JsonProperty("descriptor")]
public string Descriptor { get; set; }
}
public class LastChangedByLinks
{
[JsonProperty("avatar")]
public Badge Avatar { get; set; }
}
public class Badge
{
[JsonProperty("href")]
public Uri Href { get; set; }
}
public class BuildLinks
{
[JsonProperty("self")]
public Badge Self { get; set; }
[JsonProperty("web")]
public Badge Web { get; set; }
[JsonProperty("sourceVersionDisplayUri")]
public Badge SourceVersionDisplayUri { get; set; }
[JsonProperty("timeline")]
public Badge Timeline { get; set; }
[JsonProperty("badge")]
public Badge Badge { get; set; }
}
public class Logs
{
[JsonProperty("id")]
public long Id { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("url")]
public Uri Url { get; set; }
}
public class Plan
{
[JsonProperty("planId")]
public Guid PlanId { get; set; }
}
public class Properties
{
}
public class Repository
{
[JsonProperty("id")]
public string Id { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("clean")]
public object Clean { get; set; }
[JsonProperty("checkoutSubmodules")]
public bool CheckoutSubmodules { get; set; }
}
public class TriggerInfo
{
[JsonProperty("ci.sourceBranch")]
public string CiSourceBranch { get; set; }
[JsonProperty("ci.sourceSha")]
public string CiSourceSha { get; set; }
[JsonProperty("ci.message")]
public string CiMessage { get; set; }
[JsonProperty("ci.triggerRepository")]
public string CiTriggerRepository { get; set; }
}

View File

@ -1,243 +0,0 @@
#nullable disable
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
namespace Artemis.UI.Windows.Models;
public class GitHubDifference
{
[JsonProperty("url")]
public Uri Url { get; set; }
[JsonProperty("html_url")]
public Uri HtmlUrl { get; set; }
[JsonProperty("permalink_url")]
public Uri PermalinkUrl { get; set; }
[JsonProperty("diff_url")]
public Uri DiffUrl { get; set; }
[JsonProperty("patch_url")]
public Uri PatchUrl { get; set; }
[JsonProperty("base_commit")]
public BaseCommitClass BaseCommit { get; set; }
[JsonProperty("merge_base_commit")]
public BaseCommitClass MergeBaseCommit { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("ahead_by")]
public long AheadBy { get; set; }
[JsonProperty("behind_by")]
public long BehindBy { get; set; }
[JsonProperty("total_commits")]
public long TotalCommits { get; set; }
[JsonProperty("commits")]
public List<BaseCommitClass> Commits { get; set; }
[JsonProperty("files")]
public List<File> Files { get; set; }
}
public class BaseCommitClass
{
[JsonProperty("sha")]
public string Sha { get; set; }
[JsonProperty("node_id")]
public string NodeId { get; set; }
[JsonProperty("commit")]
public BaseCommitCommit Commit { get; set; }
[JsonProperty("url")]
public Uri Url { get; set; }
[JsonProperty("html_url")]
public Uri HtmlUrl { get; set; }
[JsonProperty("comments_url")]
public Uri CommentsUrl { get; set; }
[JsonProperty("author")]
public BaseCommitAuthor Author { get; set; }
[JsonProperty("committer")]
public BaseCommitAuthor Committer { get; set; }
[JsonProperty("parents")]
public List<Parent> Parents { get; set; }
}
public class BaseCommitAuthor
{
[JsonProperty("login")]
public string Login { get; set; }
[JsonProperty("id")]
public long Id { get; set; }
[JsonProperty("node_id")]
public string NodeId { get; set; }
[JsonProperty("avatar_url")]
public Uri AvatarUrl { get; set; }
[JsonProperty("gravatar_id")]
public string GravatarId { get; set; }
[JsonProperty("url")]
public Uri Url { get; set; }
[JsonProperty("html_url")]
public Uri HtmlUrl { get; set; }
[JsonProperty("followers_url")]
public Uri FollowersUrl { get; set; }
[JsonProperty("following_url")]
public string FollowingUrl { get; set; }
[JsonProperty("gists_url")]
public string GistsUrl { get; set; }
[JsonProperty("starred_url")]
public string StarredUrl { get; set; }
[JsonProperty("subscriptions_url")]
public Uri SubscriptionsUrl { get; set; }
[JsonProperty("organizations_url")]
public Uri OrganizationsUrl { get; set; }
[JsonProperty("repos_url")]
public Uri ReposUrl { get; set; }
[JsonProperty("events_url")]
public string EventsUrl { get; set; }
[JsonProperty("received_events_url")]
public Uri ReceivedEventsUrl { get; set; }
[JsonProperty("type")]
public string Type { get; set; }
[JsonProperty("site_admin")]
public bool SiteAdmin { get; set; }
}
public class BaseCommitCommit
{
[JsonProperty("author")]
public PurpleAuthor Author { get; set; }
[JsonProperty("committer")]
public PurpleAuthor Committer { get; set; }
[JsonProperty("message")]
public string Message { get; set; }
[JsonProperty("tree")]
public Tree Tree { get; set; }
[JsonProperty("url")]
public Uri Url { get; set; }
[JsonProperty("comment_count")]
public long CommentCount { get; set; }
[JsonProperty("verification")]
public Verification Verification { get; set; }
}
public class PurpleAuthor
{
[JsonProperty("name")]
public string Name { get; set; }
[JsonProperty("email")]
public string Email { get; set; }
[JsonProperty("date")]
public DateTimeOffset Date { get; set; }
}
public class Tree
{
[JsonProperty("sha")]
public string Sha { get; set; }
[JsonProperty("url")]
public Uri Url { get; set; }
}
public class Verification
{
[JsonProperty("verified")]
public bool Verified { get; set; }
[JsonProperty("reason")]
public string Reason { get; set; }
[JsonProperty("signature")]
public string Signature { get; set; }
[JsonProperty("payload")]
public string Payload { get; set; }
}
public class Parent
{
[JsonProperty("sha")]
public string Sha { get; set; }
[JsonProperty("url")]
public Uri Url { get; set; }
[JsonProperty("html_url")]
public Uri HtmlUrl { get; set; }
}
public class File
{
[JsonProperty("sha")]
public string Sha { get; set; }
[JsonProperty("filename")]
public string Filename { get; set; }
[JsonProperty("status")]
public string Status { get; set; }
[JsonProperty("additions")]
public long Additions { get; set; }
[JsonProperty("deletions")]
public long Deletions { get; set; }
[JsonProperty("changes")]
public long Changes { get; set; }
[JsonProperty("blob_url")]
public Uri BlobUrl { get; set; }
[JsonProperty("raw_url")]
public Uri RawUrl { get; set; }
[JsonProperty("contents_url")]
public Uri ContentsUrl { get; set; }
[JsonProperty("patch")]
public string Patch { get; set; }
[JsonProperty("previous_filename", NullValueHandling = NullValueHandling.Ignore)]
public string PreviousFilename { get; set; }
}

View File

@ -111,7 +111,7 @@ public class AutoRunProvider : IAutoRunProvider
/// <inheritdoc />
public async Task EnableAutoRun(bool recreate, int autoRunDelay)
{
if (Constants.BuildInfo.IsLocalBuild)
if (Constants.CurrentVersion == "development")
return;
await CleanupOldAutorun();

View File

@ -1,213 +0,0 @@
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Exceptions;
using Artemis.UI.Shared.Providers;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.MainWindow;
using Artemis.UI.Windows.Models;
using Artemis.UI.Windows.Screens.Update;
using Avalonia.Threading;
using Flurl;
using Flurl.Http;
using Microsoft.Toolkit.Uwp.Notifications;
using Serilog;
using File = System.IO.File;
namespace Artemis.UI.Windows.Providers;
public class UpdateProvider : IUpdateProvider, IDisposable
{
private const string API_URL = "https://dev.azure.com/artemis-rgb/Artemis/_apis/";
private const string INSTALLER_URL = "https://builds.artemis-rgb.com/binaries/Artemis.Installer.exe";
private readonly ILogger _logger;
private readonly IMainWindowService _mainWindowService;
private readonly IWindowService _windowService;
public UpdateProvider(ILogger logger, IWindowService windowService, IMainWindowService mainWindowService)
{
_logger = logger;
_windowService = windowService;
_mainWindowService = mainWindowService;
ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated;
}
public async Task<DevOpsBuild?> GetBuildInfo(int buildDefinition, string? buildNumber = null)
{
Url request = API_URL.AppendPathSegments("build", "builds")
.SetQueryParam("definitions", buildDefinition)
.SetQueryParam("resultFilter", "succeeded")
.SetQueryParam("branchName", "refs/heads/master")
.SetQueryParam("$top", 1)
.SetQueryParam("api-version", "6.1-preview.6");
if (buildNumber != null)
request = request.SetQueryParam("buildNumber", buildNumber);
try
{
DevOpsBuilds result = await request.GetJsonAsync<DevOpsBuilds>();
try
{
return result.Builds.FirstOrDefault();
}
catch (Exception e)
{
_logger.Warning(e, "GetBuildInfo: Failed to retrieve build info JSON");
throw;
}
}
catch (FlurlHttpException e)
{
_logger.Warning("GetBuildInfo: Getting build info, request returned {StatusCode}", e.StatusCode);
throw;
}
}
public async Task<GitHubDifference> GetBuildDifferences(DevOpsBuild a, DevOpsBuild b)
{
return await "https://api.github.com"
.AppendPathSegments("repos", "Artemis-RGB", "Artemis", "compare")
.AppendPathSegment(a.SourceVersion + "..." + b.SourceVersion)
.WithHeader("User-Agent", "Artemis 2")
.WithHeader("Accept", "application/vnd.github.v3+json")
.GetJsonAsync<GitHubDifference>();
}
private async void ToastNotificationManagerCompatOnOnActivated(ToastNotificationActivatedEventArgsCompat e)
{
ToastArguments args = ToastArguments.Parse(e.Argument);
string channel = args.Get("channel");
string action = "view-changes";
if (args.Contains("action"))
action = args.Get("action");
if (action == "install")
await RunInstaller(channel, true);
else if (action == "view-changes")
await Dispatcher.UIThread.InvokeAsync(async () =>
{
_mainWindowService.OpenMainWindow();
await OfferUpdate(channel, true);
});
}
private async Task RunInstaller(string channel, bool silent)
{
_logger.Information("ApplyUpdate: Applying update");
// Ensure the installer is up-to-date, get installer build info
DevOpsBuild? buildInfo = await GetBuildInfo(6);
string installerPath = Path.Combine(Constants.DataFolder, "installer", "Artemis.Installer.exe");
// Always update installer if it is missing ^^
if (!File.Exists(installerPath))
{
await UpdateInstaller();
}
// Compare the creation date of the installer with the build date and update if needed
else
{
if (buildInfo != null && File.GetLastWriteTime(installerPath) < buildInfo.FinishTime)
await UpdateInstaller();
}
_logger.Information("ApplyUpdate: Running installer at {InstallerPath}", installerPath);
try
{
Process.Start(new ProcessStartInfo(installerPath, "-autoupdate")
{
UseShellExecute = true,
Verb = "runas"
});
}
catch (Win32Exception e)
{
if (e.NativeErrorCode == 0x4c7)
_logger.Warning("ApplyUpdate: Operation was cancelled, user likely clicked No in UAC dialog");
else
throw;
}
}
private async Task UpdateInstaller()
{
string installerDirectory = Path.Combine(Constants.DataFolder, "installer");
string installerPath = Path.Combine(installerDirectory, "Artemis.Installer.exe");
_logger.Information("UpdateInstaller: Downloading installer from {DownloadUrl}", INSTALLER_URL);
using HttpClient client = new();
HttpResponseMessage httpResponseMessage = await client.GetAsync(INSTALLER_URL);
if (!httpResponseMessage.IsSuccessStatusCode)
throw new ArtemisUIException($"Failed to download installer, status code {httpResponseMessage.StatusCode}");
_logger.Information("UpdateInstaller: Writing installer file to {InstallerPath}", installerPath);
if (File.Exists(installerPath))
File.Delete(installerPath);
Core.Utilities.CreateAccessibleDirectory(installerDirectory);
await using FileStream fs = new(installerPath, FileMode.Create, FileAccess.Write, FileShare.None);
await httpResponseMessage.Content.CopyToAsync(fs);
}
private void ShowDesktopNotification(string channel)
{
new ToastContentBuilder()
.AddArgument("channel", channel)
.AddText("An update is available")
.AddButton(new ToastButton().SetContent("Install").AddArgument("action", "install").SetBackgroundActivation())
.AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-changes"))
.Show();
}
/// <inheritdoc />
public void Dispose()
{
ToastNotificationManagerCompat.OnActivated -= ToastNotificationManagerCompatOnOnActivated;
ToastNotificationManagerCompat.Uninstall();
}
/// <inheritdoc />
public async Task<bool> CheckForUpdate(string channel)
{
DevOpsBuild? buildInfo = await GetBuildInfo(1);
if (buildInfo == null)
return false;
double buildNumber = double.Parse(buildInfo.BuildNumber, CultureInfo.InvariantCulture);
string buildNumberDisplay = buildNumber.ToString(CultureInfo.InvariantCulture);
_logger.Information("Latest build is {BuildNumber}, we're running {LocalBuildNumber}", buildNumberDisplay, Constants.BuildInfo.BuildNumberDisplay);
return buildNumber > Constants.BuildInfo.BuildNumber;
}
/// <inheritdoc />
public async Task ApplyUpdate(string channel, bool silent)
{
await RunInstaller(channel, silent);
}
/// <inheritdoc />
public async Task OfferUpdate(string channel, bool windowOpen)
{
if (windowOpen)
{
bool update = await _windowService.ShowDialogAsync<UpdateDialogViewModel, bool>(channel);
if (update)
await RunInstaller(channel, false);
}
else
{
ShowDesktopNotification(channel);
}
}
}

View File

@ -0,0 +1,183 @@
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 DryIoc.ImTools;
using Microsoft.Toolkit.Uwp.Notifications;
using ReactiveUI;
namespace Artemis.UI.Windows.Providers;
public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
{
private readonly Func<Guid, 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<Guid, ReleaseInstaller> getReleaseInstaller)
{
_mainWindowService = mainWindowService;
_updateService = updateService;
_getSettingsViewModel = getSettingsViewModel;
_getReleaseInstaller = getReleaseInstaller;
ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated;
}
/// <inheritdoc />
public void ShowNotification(Guid releaseId, string releaseVersion)
{
GetBuilderForRelease(releaseId, releaseVersion)
.AddText("Update available")
.AddText($"Artemis {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.ToString());
}
/// <inheritdoc />
public void ShowInstalledNotification(string installedVersion)
{
new ToastContentBuilder().AddArgument("releaseVersion", installedVersion)
.AddText("Update installed")
.AddText($"Artemis {installedVersion} has been installed")
.AddButton(new ToastButton().SetContent("View changes").AddArgument("action", "view-changes"))
.Show();
}
private void ViewRelease(string releaseVersion)
{
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.PreselectVersion = releaseVersion;
settingsViewModel.SelectedTab = releaseTabViewModel;
});
}
private async Task InstallRelease(Guid 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.ToString();
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;
}
// If the main window is not open the user isn't busy, restart straight away
if (!_mainWindowService.IsMainWindowOpen)
{
_updateService.RestartForUpdate(true);
return;
}
// Ask for a restart because the user is actively using Artemis
GetBuilderForRelease(releaseId, releaseVersion)
.AddAudio(new ToastAudio {Silent = true})
.AddText("Update ready")
.AddText("Artemis must restart to finish the update")
.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.ToString());
}
private void UpdateInstallProgress(Guid releaseId, ReleaseInstaller installer)
{
ToastNotificationManagerCompat.CreateToastNotifier().Update(GetDataForInstaller(installer), releaseId.ToString());
}
private ToastContentBuilder GetBuilderForRelease(Guid releaseId, string releaseVersion)
{
return new ToastContentBuilder().AddArgument("releaseId", releaseId.ToString()).AddArgument("releaseVersion", releaseVersion);
}
private NotificationData GetDataForInstaller(ReleaseInstaller installer)
{
NotificationData data = new()
{
Values =
{
["progressValue"] = (installer.Progress / 100f).ToString(CultureInfo.InvariantCulture),
["progressStatus"] = installer.Status
}
};
return data;
}
private async void ToastNotificationManagerCompatOnOnActivated(ToastNotificationActivatedEventArgsCompat e)
{
ToastArguments args = ToastArguments.Parse(e.Argument);
Guid releaseId = args.Contains("releaseId") ? Guid.Parse(args.Get("releaseId")) : Guid.Empty;
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(releaseVersion);
else if (action == "cancel")
_cancellationTokenSource?.Cancel();
else if (action == "restart-for-update")
_updateService.RestartForUpdate(false);
}
}

View File

@ -1,70 +0,0 @@
<Window 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:update="clr-namespace:Artemis.UI.Windows.Screens.Update"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Windows.Screens.Update.UpdateDialogView"
x:DataType="update:UpdateDialogViewModel"
Title="Artemis | Update available"
Width="750"
MinWidth="750"
Height="500"
WindowStartupLocation="CenterOwner">
<Grid Margin="15" RowDefinitions="Auto,*,Auto">
<TextBlock Grid.Row="0" Classes="h4">
A new Artemis update is available! 🥳
</TextBlock>
<StackPanel Grid.Row="1" IsVisible="{CompiledBinding RetrievingChanges}" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock>Retrieving changes...</TextBlock>
<ProgressBar IsIndeterminate="True"></ProgressBar>
</StackPanel>
<Border Grid.Row="1" Classes="card" IsVisible="{CompiledBinding !RetrievingChanges}">
<Grid RowDefinitions="Auto,*">
<StackPanel Grid.Row="0">
<StackPanel Orientation="Horizontal">
<TextBlock Text="You are currently running build " />
<TextBlock Text="{CompiledBinding CurrentBuild, Mode=OneWay}"></TextBlock>
<TextBlock Text=" while the latest build is " />
<TextBlock Text="{CompiledBinding LatestBuild, Mode=OneWay}"></TextBlock>
<TextBlock Text="." />
</StackPanel>
<TextBlock Text="Updating Artemis will give you the latest bug(fixes), features and improvements." />
<Separator Classes="card-separator" />
<TextBlock Classes="h5" IsVisible="{CompiledBinding HasChanges}">
Changelog (auto-generated)
</TextBlock>
</StackPanel>
<ScrollViewer Grid.Row="1" VerticalAlignment="Top" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" IsVisible="{CompiledBinding HasChanges}">
<ItemsControl Items="{CompiledBinding Changes}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="•" Margin="10 0" />
<TextBlock Grid.Column="1" Text="{Binding}" TextWrapping="Wrap" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<StackPanel Grid.Row="1" IsVisible="{CompiledBinding !HasChanges}" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock HorizontalAlignment="Center">We couldn't retrieve any changes</TextBlock>
<controls:HyperlinkButton NavigateUri="https://github.com/Artemis-RGB/Artemis/commits/master" HorizontalAlignment="Center">View online</controls:HyperlinkButton>
</StackPanel>
</Grid>
</Border>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal" Spacing="5" Grid.Row="2" Margin="0 15 0 0">
<Button Classes="accent" Command="{CompiledBinding Install}">Install update</Button>
<Button Command="{CompiledBinding AskLater}">Ask later</Button>
</StackPanel>
</Grid>
</Window>

View File

@ -1,21 +0,0 @@
using Artemis.UI.Shared;
using Avalonia;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Windows.Screens.Update;
public class UpdateDialogView : ReactiveCoreWindow<UpdateDialogViewModel>
{
public UpdateDialogView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -1,120 +0,0 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Providers;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Windows.Models;
using Artemis.UI.Windows.Providers;
using Avalonia.Threading;
using DynamicData;
using ReactiveUI;
namespace Artemis.UI.Windows.Screens.Update;
public class UpdateDialogViewModel : DialogViewModelBase<bool>
{
// Based on https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/github?view=azure-devops&tabs=yaml#skipping-ci-for-individual-commits
private readonly string[] _excludedCommitMessages =
{
"[skip ci]",
"[ci skip]",
"skip-checks: true",
"skip-checks:true",
"[skip azurepipelines]",
"[azurepipelines skip]",
"[skip azpipelines]",
"[azpipelines skip]",
"[skip azp]",
"[azp skip]",
"***NO_CI***"
};
private readonly INotificationService _notificationService;
private readonly UpdateProvider _updateProvider;
private bool _hasChanges;
private string? _latestBuild;
private bool _retrievingChanges;
public UpdateDialogViewModel(string channel, IUpdateProvider updateProvider, INotificationService notificationService)
{
_updateProvider = (UpdateProvider) updateProvider;
_notificationService = notificationService;
Channel = channel;
CurrentBuild = Constants.BuildInfo.BuildNumberDisplay;
this.WhenActivated((CompositeDisposable _) => Dispatcher.UIThread.InvokeAsync(GetBuildChanges));
Install = ReactiveCommand.Create(() => Close(true));
AskLater = ReactiveCommand.Create(() => Close(false));
}
public ReactiveCommand<Unit, Unit> Install { get; }
public ReactiveCommand<Unit, Unit> AskLater { get; }
public string Channel { get; }
public string CurrentBuild { get; }
public ObservableCollection<string> Changes { get; } = new();
public bool RetrievingChanges
{
get => _retrievingChanges;
set => RaiseAndSetIfChanged(ref _retrievingChanges, value);
}
public bool HasChanges
{
get => _hasChanges;
set => RaiseAndSetIfChanged(ref _hasChanges, value);
}
public string? LatestBuild
{
get => _latestBuild;
set => RaiseAndSetIfChanged(ref _latestBuild, value);
}
private async Task GetBuildChanges()
{
try
{
RetrievingChanges = true;
Task<DevOpsBuild?> currentTask = _updateProvider.GetBuildInfo(1, CurrentBuild);
Task<DevOpsBuild?> latestTask = _updateProvider.GetBuildInfo(1);
DevOpsBuild? current = await currentTask;
DevOpsBuild? latest = await latestTask;
LatestBuild = latest?.BuildNumber;
if (current != null && latest != null)
{
GitHubDifference difference = await _updateProvider.GetBuildDifferences(current, latest);
// Only take commits with one parents (no merges)
Changes.Clear();
Changes.AddRange(difference.Commits.Where(c => c.Parents.Count == 1)
.SelectMany(c => c.Commit.Message.Split("\n"))
.Select(m => m.Trim())
.Where(m => !string.IsNullOrWhiteSpace(m) && !_excludedCommitMessages.Contains(m))
.OrderBy(m => m)
);
HasChanges = Changes.Any();
}
}
catch (Exception e)
{
_notificationService.CreateNotification().WithTitle("Failed to retrieve build changes").WithMessage(e.Message).WithSeverity(NotificationSeverity.Error).Show();
}
finally
{
RetrievingChanges = false;
}
}
}

View File

@ -0,0 +1,56 @@
param (
[Parameter(Mandatory = $true)][string]$sourceDirectory,
[Parameter(Mandatory = $true)][string]$destinationDirectory,
[Parameter(Mandatory = $false)][string]$artemisArgs
)
Write-Host "Artemis update script v1"
# Wait up to 10 seconds for the process to shut down
for ($i = 1; $i -le 10; $i++) {
$process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue
if (!$process)
{
break
}
Write-Host "Waiting for Artemis to shut down ($i / 10)"
Start-Sleep -Seconds 1
}
# If the process is still running, kill it
$process = Get-Process -Name Artemis.UI.Windows -ErrorAction SilentlyContinue
if ($process)
{
Stop-Process -Id $process.Id -Force
Start-Sleep -Seconds 1
}
# Check if the destination directory exists
if (!(Test-Path $destinationDirectory))
{
Write-Error "The destination directory at $destinationDirectory does not exist"
Exit 1
}
# Clear the destination directory but don't remove it, leaving ACL entries in tact
Write-Host "Cleaning up old version where needed"
Get-ChildItem $destinationDirectory | Remove-Item -Recurse -Force
# Move the contents of the source directory to the destination directory
Write-Host "Installing new files"
Get-ChildItem $sourceDirectory | Move-Item -Destination $destinationDirectory
# Remove the now empty source directory
Remove-Item $sourceDirectory
Write-Host "Finished! Restarting Artemis"
Start-Sleep -Seconds 1
# When finished, run the updated version
if ($artemisArgs)
{
Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory -ArgumentList $artemisArgs
}
else
{
Start-Process -FilePath "$destinationDirectory\Artemis.UI.Windows.exe" -WorkingDirectory $destinationDirectory
}

View File

@ -11,6 +11,7 @@
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.UI.Shared\Artemis.UI.Shared.csproj" />
<ProjectReference Include="..\Artemis.VisualScripting\Artemis.VisualScripting.csproj" />
<ProjectReference Include="..\Artemis.WebClient.Updating\Artemis.WebClient.Updating.csproj" />
</ItemGroup>
<ItemGroup>
@ -28,7 +29,9 @@
<PackageReference Include="FluentAvaloniaUI" Version="1.4.1" />
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Live.Avalonia" Version="1.3.1" />
<PackageReference Include="Markdown.Avalonia.Tight" Version="0.10.13" />
<PackageReference Include="Material.Icons.Avalonia" Version="1.1.10" />
<PackageReference Include="Octopus.Octodiff" Version="2.0.100" />
<PackageReference Include="ReactiveUI" Version="17.1.50" />
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
<PackageReference Include="RGB.NET.Core" Version="2.0.0-prerelease.17" />
@ -40,4 +43,15 @@
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<Compile Update="Screens\Settings\Tabs\ReleasesTabView.axaml.cs">
<DependentUpon>UpdatingTabView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Settings\Updating\ReleaseView.axaml.cs">
<DependentUpon>UpdatingTabView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Project>

View File

@ -2,7 +2,6 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.DryIoc;
using Artemis.UI.DryIoc;
@ -12,6 +11,7 @@ using Artemis.UI.Shared.DataModelPicker;
using Artemis.UI.Shared.DryIoc;
using Artemis.UI.Shared.Services;
using Artemis.VisualScripting.DryIoc;
using Artemis.WebClient.Updating.DryIoc;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
@ -42,6 +42,7 @@ public static class ArtemisBootstrapper
_container.RegisterCore();
_container.RegisterUI();
_container.RegisterSharedUI();
_container.RegisterUpdatingClient();
_container.RegisterNoStringEvaluating();
configureServices?.Invoke(_container);

View File

@ -27,7 +27,8 @@
Width="200"
VerticalAlignment="Center"
Items="{CompiledBinding Descriptors}"
SelectedItem="{CompiledBinding SelectedDescriptor}">
SelectedItem="{CompiledBinding SelectedDescriptor}"
PlaceholderText="Please select a brush">
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type layerBrushes:LayerBrushDescriptor}">
<Grid ColumnDefinitions="30,*" RowDefinitions="Auto,Auto">

View File

@ -59,7 +59,7 @@ public class BrushPropertyInputViewModel : PropertyInputViewModel<LayerBrushRefe
/// <inheritdoc />
protected override void ApplyInputValue()
{
if (LayerProperty.ProfileElement is not Layer layer || layer.LayerBrush == null || SelectedDescriptor == null)
if (LayerProperty.ProfileElement is not Layer layer || SelectedDescriptor == null)
return;
_profileEditorService.ExecuteCommand(new ChangeLayerBrush(layer, SelectedDescriptor));

View File

@ -4,6 +4,7 @@ using Artemis.UI.DryIoc.InstanceProviders;
using Artemis.UI.Screens;
using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.NodeEditor;
using Artemis.UI.Shared.Services.ProfileEditor;
@ -30,12 +31,12 @@ public static class ContainerExtensions
container.Register<IAssetLoader, AssetLoader>(Reuse.Singleton);
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<ViewModelBase>());
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<MainScreenViewModel>(), ifAlreadyRegistered: IfAlreadyRegistered.Replace);
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IToolViewModel>() && type.IsInterface);
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IVmFactory>() && type != typeof(PropertyVmFactory));
container.Register<NodeScriptWindowViewModelBase, NodeScriptWindowViewModel>(Reuse.Singleton);
container.Register<IPropertyVmFactory, PropertyVmFactory>(Reuse.Singleton);
container.Register<IUpdateNotificationProvider, BasicUpdateNotificationProvider>();
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IArtemisUIService>(), Reuse.Singleton);
}

View File

@ -1,4 +1,5 @@
using System.Collections.ObjectModel;
using System;
using System.Collections.ObjectModel;
using System.Reactive;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
@ -17,6 +18,7 @@ using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers;
using Artemis.UI.Screens.Scripting;
using Artemis.UI.Screens.Settings;
using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.VisualScripting;
@ -475,3 +477,22 @@ public class ScriptVmFactory : IScriptVmFactory
return _container.Resolve<ScriptConfigurationViewModel>(new object[] { profile, scriptConfiguration });
}
}
public interface IReleaseVmFactory : IVmFactory
{
ReleaseViewModel ReleaseListViewModel(Guid releaseId, string version, DateTimeOffset createdAt);
}
public class ReleaseVmFactory : IReleaseVmFactory
{
private readonly IContainer _container;
public ReleaseVmFactory(IContainer container)
{
_container = container;
}
public ReleaseViewModel ReleaseListViewModel(Guid releaseId, string version, DateTimeOffset createdAt)
{
return _container.Resolve<ReleaseViewModel>(new object[] { releaseId, version, createdAt });
}
}

View File

@ -0,0 +1,14 @@
using System.Reactive.Disposables;
using System.Threading;
namespace Artemis.UI.Extensions;
public static class CompositeDisposableExtensions
{
public static CancellationToken AsCancellationToken(this CompositeDisposable disposable)
{
CancellationTokenSource tokenSource = new();
Disposable.Create(tokenSource, s => s.Cancel()).DisposeWith(disposable);
return tokenSource.Token;
}
}

View File

@ -0,0 +1,56 @@
using System;
using System.IO;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.UI.Extensions
{
public static class HttpClientProgressExtensions
{
public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress<float>? progress, CancellationToken cancellationToken)
{
using HttpResponseMessage response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
long? contentLength = response.Content.Headers.ContentLength;
await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken);
// no progress... no contentLength... very sad
if (progress is null || !contentLength.HasValue)
{
await download.CopyToAsync(destination, cancellationToken);
return;
}
// Such progress and contentLength much reporting Wow!
Progress<long> progressWrapper = new(totalBytes => progress.Report(GetProgressPercentage(totalBytes, contentLength.Value)));
await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken);
float GetProgressPercentage(float totalBytes, float currentBytes) => (totalBytes / currentBytes) * 100f;
}
static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress, CancellationToken cancellationToken)
{
if (bufferSize < 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
if (source is null)
throw new ArgumentNullException(nameof(source));
if (!source.CanRead)
throw new InvalidOperationException($"'{nameof(source)}' is not readable.");
if (destination == null)
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new InvalidOperationException($"'{nameof(destination)}' is not writable.");
byte[] buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
}
}
}

View File

@ -0,0 +1,74 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Threading;
namespace Artemis.UI.Extensions;
// Taken from System.IO.Compression with progress reporting slapped on top
public static class ZipArchiveExtensions
{
/// <summary>
/// Extracts all the files in the zip archive to a directory on the file system.
/// </summary>
/// <param name="source">The zip archive to extract files from.</param>
/// <param name="destinationDirectoryName">The path to the directory to place the extracted files in. You can specify either a relative or an absolute path. A relative path is interpreted as relative to the current working directory.</param>
/// <param name="overwriteFiles">A boolean indicating whether to override existing files</param>
/// <param name="progress">The progress to report to.</param>
/// <param name="cancellationToken">A cancellation token</param>
public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, IProgress<float> progress, CancellationToken cancellationToken)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (destinationDirectoryName == null)
throw new ArgumentNullException(nameof(destinationDirectoryName));
for (int index = 0; index < source.Entries.Count; index++)
{
ZipArchiveEntry entry = source.Entries[index];
entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles);
progress.Report((index + 1f) / source.Entries.Count * 100f);
cancellationToken.ThrowIfCancellationRequested();
}
}
private static void ExtractRelativeToDirectory(this ZipArchiveEntry source, string destinationDirectoryName, bool overwrite)
{
if (source == null)
throw new ArgumentNullException(nameof(source));
if (destinationDirectoryName == null)
throw new ArgumentNullException(nameof(destinationDirectoryName));
// Note that this will give us a good DirectoryInfo even if destinationDirectoryName exists:
DirectoryInfo di = Directory.CreateDirectory(destinationDirectoryName);
string destinationDirectoryFullPath = di.FullName;
if (!destinationDirectoryFullPath.EndsWith(Path.DirectorySeparatorChar))
destinationDirectoryFullPath += Path.DirectorySeparatorChar;
string fileDestinationPath = Path.GetFullPath(Path.Combine(destinationDirectoryFullPath, source.FullName));
if (!fileDestinationPath.StartsWith(destinationDirectoryFullPath, StringComparison))
throw new IOException($"The file '{fileDestinationPath}' already exists.");
if (Path.GetFileName(fileDestinationPath).Length == 0)
{
// If it is a directory:
if (source.Length != 0)
throw new IOException("Extracting Zip entry would have resulted in a file outside the specified destination directory.");
Directory.CreateDirectory(fileDestinationPath);
}
else
{
// If it is a file:
// Create containing directory:
Directory.CreateDirectory(Path.GetDirectoryName(fileDestinationPath)!);
source.ExtractToFile(fileDestinationPath, overwrite: overwrite);
}
}
private static StringComparison StringComparison => IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
private static bool IsCaseSensitive => !(OperatingSystem.IsWindows() || OperatingSystem.IsMacOS() || OperatingSystem.IsIOS() || OperatingSystem.IsTvOS() || OperatingSystem.IsWatchOS());
}

View File

@ -49,7 +49,7 @@ public class LogsDebugView : ReactiveUserControl<LogsDebugViewModel>
//we need this help distance because of rounding.
//if we scroll slightly above the end, we still want it
//to scroll down to the new lines.
const double graceDistance = 1d;
const double GRACE_DISTANCE = 1d;
//if we were at the bottom of the log and
//if the last log event was 5 lines long
@ -59,7 +59,7 @@ public class LogsDebugView : ReactiveUserControl<LogsDebugViewModel>
//if we are more than that out of sync,
//the user scrolled up and we should not
//mess with anything.
if (_lineCount == 0 || linesAdded + graceDistance > outOfScreenLines)
if (_lineCount == 0 || linesAdded + GRACE_DISTANCE > outOfScreenLines)
{
Dispatcher.UIThread.Post(() => _textEditor.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
_lineCount = _textEditor.LineCount;

View File

@ -109,7 +109,7 @@
<controls:HyperlinkButton Grid.Row="1"
Grid.Column="0"
HorizontalAlignment="Center"
NavigateUri="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&amp;hosted_button_id=VQBAEJYUFLU4J">
NavigateUri="https://wiki.artemis-rgb.com/en/donating">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="Gift" />
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Donate</TextBlock>

View File

@ -7,6 +7,7 @@ using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Models;
using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.MainWindow;
@ -57,11 +58,15 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
_lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!;
mainWindowService.ConfigureMainWindowProvider(this);
mainWindowService.HostScreen = this;
DisplayAccordingToSettings();
Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel);
Task.Run(() =>
{
if (_updateService.Initialize())
return;
coreService.Initialize();
registrationService.RegisterBuiltInDataModelDisplays();
registrationService.RegisterBuiltInDataModelInputs();
@ -105,14 +110,10 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
bool showOnAutoRun = _settingsService.GetSetting("UI.ShowOnStartup", true).Value;
if ((autoRunning && !showOnAutoRun) || minimized)
{
// TODO: Auto-update
}
else
{
ShowSplashScreen();
_coreService.Initialized += (_, _) => Dispatcher.UIThread.InvokeAsync(OpenMainWindow);
}
return;
ShowSplashScreen();
_coreService.Initialized += (_, _) => Dispatcher.UIThread.InvokeAsync(OpenMainWindow);
}
private void ShowSplashScreen()
@ -229,11 +230,6 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
}
#endregion
public void SaveWindowBounds(int x, int y, int width, int height)
{
throw new NotImplementedException();
}
}
internal class EmptyViewModel : MainScreenViewModel

View File

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

View File

@ -6,10 +6,13 @@ namespace Artemis.UI.Screens.Settings;
public class SettingsViewModel : MainScreenViewModel
{
private ActivatableViewModelBase _selectedTab;
public SettingsViewModel(IScreen hostScreen,
GeneralTabViewModel generalTabViewModel,
PluginsTabViewModel pluginsTabViewModel,
DevicesTabViewModel devicesTabViewModel,
ReleasesTabViewModel releasesTabViewModel,
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
{
SettingTabs = new ObservableCollection<ActivatableViewModelBase>
@ -17,9 +20,17 @@ public class SettingsViewModel : MainScreenViewModel
generalTabViewModel,
pluginsTabViewModel,
devicesTabViewModel,
releasesTabViewModel,
aboutTabViewModel
};
_selectedTab = generalTabViewModel;
}
public ObservableCollection<ActivatableViewModelBase> SettingTabs { get; }
public ActivatableViewModelBase SelectedTab
{
get => _selectedTab;
set => RaiseAndSetIfChanged(ref _selectedTab, value);
}
}

View File

@ -57,7 +57,7 @@ public class AboutTabViewModel : ActivatableViewModelBase
private async Task Activate()
{
AssemblyInformationalVersionAttribute? versionAttribute = typeof(AboutTabViewModel).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
Version = $"Version {versionAttribute?.InformationalVersion} build {Constants.BuildInfo.BuildNumberDisplay}";
Version = $"Version {Constants.CurrentVersion}";
try
{

View File

@ -137,7 +137,7 @@
</Border>
<!-- Update settings -->
<StackPanel IsVisible="{CompiledBinding IsUpdatingSupported}">
<StackPanel>
<TextBlock Classes="h4" Margin="0 15">
Updating
</TextBlock>

View File

@ -12,9 +12,11 @@ using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.UI.Screens.StartupWizard;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Providers;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Avalonia.Threading;
using DryIoc;
using DynamicData;
@ -30,6 +32,7 @@ public class GeneralTabViewModel : ActivatableViewModelBase
private readonly PluginSetting<LayerBrushReference> _defaultLayerBrushDescriptor;
private readonly ISettingsService _settingsService;
private readonly IUpdateService _updateService;
private readonly INotificationService _notificationService;
private readonly IWindowService _windowService;
private bool _startupWizardOpen;
@ -38,13 +41,15 @@ public class GeneralTabViewModel : ActivatableViewModelBase
IPluginManagementService pluginManagementService,
IDebugService debugService,
IWindowService windowService,
IUpdateService updateService)
IUpdateService updateService,
INotificationService notificationService)
{
DisplayName = "General";
_settingsService = settingsService;
_debugService = debugService;
_windowService = windowService;
_updateService = updateService;
_notificationService = notificationService;
_autoRunProvider = container.Resolve<IAutoRunProvider>(IfUnresolved.ReturnDefault);
List<LayerBrushProvider> layerBrushProviders = pluginManagementService.GetFeaturesOfType<LayerBrushProvider>();
@ -88,7 +93,6 @@ public class GeneralTabViewModel : ActivatableViewModelBase
public ReactiveCommand<Unit, Unit> ShowDataFolder { get; }
public bool IsAutoRunSupported => _autoRunProvider != null;
public bool IsUpdatingSupported => _updateService.UpdatingSupported;
public ObservableCollection<LayerBrushDescriptor> LayerBrushDescriptors { get; }
public ObservableCollection<string> GraphicsContexts { get; }
@ -142,8 +146,8 @@ public class GeneralTabViewModel : ActivatableViewModelBase
public PluginSetting<bool> UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false);
public PluginSetting<int> UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15);
public PluginSetting<bool> UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true);
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.CheckForUpdates", true);
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.AutoUpdate", false);
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true);
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true);
public PluginSetting<bool> ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
public PluginSetting<LogEventLevel> CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information);
public PluginSetting<string> CorePreferredGraphicsContext => _settingsService.GetSetting("Core.PreferredGraphicsContext", "Software");
@ -159,7 +163,25 @@ public class GeneralTabViewModel : ActivatableViewModelBase
private async Task ExecuteCheckForUpdate(CancellationToken cancellationToken)
{
await _updateService.ManualUpdate();
try
{
// If an update was available a popup was shown, no need to continue
if (await _updateService.CheckForUpdate())
return;
_notificationService.CreateNotification()
.WithTitle("No update available")
.WithMessage("You are running the latest version in your current channel")
.Show();
}
catch (Exception e)
{
_notificationService.CreateNotification()
.WithTitle("Failed to check for update")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Warning)
.Show();
}
}
private async Task ExecuteShowSetupWizard()

View File

@ -0,0 +1,65 @@
<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"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
x:Class="Artemis.UI.Screens.Settings.ReleasesTabView"
x:DataType="settings:ReleasesTabViewModel">
<UserControl.Styles>
<Style Selector="avalonia|MaterialIcon.status-icon">
<Setter Property="Width" Value="20" />
<Setter Property="Height" Value="20" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight1}" />
</Style>
</UserControl.Styles>
<Panel>
<StackPanel VerticalAlignment="Center" MaxWidth="300" Spacing="15" IsVisible="{CompiledBinding Loading}">
<TextBlock TextAlignment="Center">Loading releases...</TextBlock>
<ProgressBar IsVisible="True"></ProgressBar>
</StackPanel>
<Panel IsVisible="{CompiledBinding !Loading}">
<StackPanel VerticalAlignment="Center" Spacing="15" IsVisible="{CompiledBinding !ReleaseViewModels.Count}">
<TextBlock TextAlignment="Center"
TextWrapping="Wrap"
Text="{CompiledBinding Channel, StringFormat='Found no releases for the \'{0}\' channel.'}">
</TextBlock>
<controls:HyperlinkButton NavigateUri="https://wiki.artemis-rgb.com/en/channels"
HorizontalAlignment="Center">
Learn more about channels on the wiki
</controls:HyperlinkButton>
</StackPanel>
<Grid ColumnDefinitions="300,*" Margin="0 10" IsVisible="{CompiledBinding ReleaseViewModels.Count}">
<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">
<Panel>
<Grid Margin="4" IsVisible="{CompiledBinding ShowStatusIndicator}" RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<TextBlock Grid.Row="0" Grid.Column="0" Text="{CompiledBinding Version}" VerticalAlignment="Center" FontWeight="SemiBold" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="{CompiledBinding CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
<avalonia:MaterialIcon Classes="status-icon" Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Kind="CheckCircle" ToolTip.Tip="Current version"
IsVisible="{CompiledBinding IsCurrentVersion}" />
<avalonia:MaterialIcon Classes="status-icon" Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Kind="History" ToolTip.Tip="Previous version"
IsVisible="{CompiledBinding IsPreviousVersion}" />
</Grid>
<StackPanel Margin="4" IsVisible="{CompiledBinding !ShowStatusIndicator}">
<TextBlock Text="{CompiledBinding Version}" VerticalAlignment="Center" />
<TextBlock Text="{CompiledBinding CreatedAt, StringFormat={}{0:g}}" VerticalAlignment="Center" Classes="subtitle" FontSize="13" />
</StackPanel>
</Panel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<ContentControl Grid.Column="1" Content="{CompiledBinding SelectedReleaseViewModel}" />
</Grid>
</Panel>
</Panel>
</UserControl>

View File

@ -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);
}
}

View File

@ -0,0 +1,112 @@
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 IUpdateService _updateService;
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;
_updateService = updateService;
_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;
Channel = _updateService.Channel;
this.WhenActivated(async d =>
{
await _updateService.CacheLatestRelease();
await GetMoreReleases(d.AsCancellationToken());
SelectedReleaseViewModel = ReleaseViewModels.FirstOrDefault(r => r.Version == PreselectVersion) ?? ReleaseViewModels.FirstOrDefault();
});
}
public ReadOnlyObservableCollection<ReleaseViewModel> ReleaseViewModels { get; }
public string Channel { get; }
public string? PreselectVersion { 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(_updateService.Channel, 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;
}
}
}

View File

@ -0,0 +1,325 @@
<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:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
xmlns:mdc="clr-namespace:Markdown.Avalonia.Controls;assembly=Markdown.Avalonia"
xmlns:mde="clr-namespace:Markdown.Avalonia.Extensions;assembly=Markdown.Avalonia"
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"
xmlns:avalonia1="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseView"
x:DataType="updating:ReleaseViewModel">
<UserControl.Resources>
<converters:BytesToStringConverter x:Key="BytesToStringConverter" />
</UserControl.Resources>
<UserControl.Styles>
<Style Selector=":is(Control).fade-in">
<Setter Property="Opacity" Value="0"></Setter>
</Style>
<Style Selector=":is(Control).fade-in[IsVisible=True]">
<Style.Animations>
<Animation Duration="0:00:00.250" FillMode="Forward" Easing="CubicEaseInOut">
<KeyFrame Cue="0%">
<Setter Property="Opacity" Value="0.0" />
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Opacity" Value="1.0" />
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Grid.info-container">
<Setter Property="Margin" Value="10" />
</Style>
<Style Selector="avalonia1|MaterialIcon.info-icon">
<Setter Property="VerticalAlignment" Value="Top" />
<Setter Property="Margin" Value="0 3 10 0" />
</Style>
<Style Selector="TextBlock.info-title">
<Setter Property="Margin" Value="0 0 0 5" />
<Setter Property="Opacity" Value="0.8" />
</Style>
<Style Selector="TextBlock.info-body">
</Style>
<Style Selector="TextBlock.info-link">
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight3}" />
</Style>
<Style Selector="TextBlock.info-link:pointerover">
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Foreground" Value="{DynamicResource SystemAccentColorLight1}" />
</Style>
</UserControl.Styles>
<Grid RowDefinitions="Auto,*" IsVisible="{CompiledBinding Commit, Converter={x:Static StringConverters.IsNotNullOrEmpty}}" Classes="fade-in">
<Border Grid.Row="0" Classes="card" Margin="0 0 0 10">
<StackPanel>
<Grid ColumnDefinitions="*,Auto">
<TextBlock Classes="h4 no-margin">Release info</TextBlock>
<Panel Grid.Column="1" IsVisible="{CompiledBinding InstallationAvailable}">
<!-- Install progress -->
<Grid ColumnDefinitions="*,*"
RowDefinitions="*,*"
IsVisible="{CompiledBinding InstallationInProgress}">
<ProgressBar Grid.Column="0"
Grid.Row="0"
Width="300"
Value="{CompiledBinding ReleaseInstaller.Progress, FallbackValue=0}">
</ProgressBar>
<TextBlock Grid.Column="0"
Grid.Row="1"
Classes="subtitle"
TextAlignment="Right"
Text="{CompiledBinding ReleaseInstaller.Status, FallbackValue=Installing}" />
<Button Grid.Column="1" Grid.Row="0" Grid.RowSpan="2"
Classes="accent"
Margin="15 0 0 0"
Width="80"
VerticalAlignment="Center"
Command="{CompiledBinding CancelInstall}">
Cancel
</Button>
</Grid>
<Panel IsVisible="{CompiledBinding !InstallationInProgress}" HorizontalAlignment="Right">
<!-- Install button -->
<Button Classes="accent"
Width="80"
Command="{CompiledBinding Install}"
IsVisible="{CompiledBinding !InstallationFinished}">
Install
</Button>
<!-- Restart button -->
<Grid ColumnDefinitions="*,*" IsVisible="{CompiledBinding InstallationFinished}">
<TextBlock Grid.Column="0"
Grid.Row="0"
Classes="subtitle"
TextAlignment="Right"
VerticalAlignment="Center">
Ready, restart to install
</TextBlock>
<Button Grid.Column="1" Grid.Row="0"
Classes="accent"
Margin="15 0 0 0"
Width="80"
Command="{CompiledBinding Restart}"
IsVisible="{CompiledBinding InstallationFinished}">
Restart
</Button>
</Grid>
</Panel>
</Panel>
</Grid>
<Separator Classes="card-separator" />
<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>
<Style Selector="ctxt|CTextBlock">
<Style.Setters>
<Setter Property="FontSize" Value="{mde:Multiply ControlContentThemeFontSize, 1}" />
<Setter Property="Margin" Value="0,5" />
</Style.Setters>
</Style>
<Style Selector="TextBlock">
<Style.Setters>
<Setter Property="FontSize" Value="{mde:Multiply ControlContentThemeFontSize, 1}" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CTextBlock.Heading1">
<Style.Setters>
<Setter Property="FontSize" Value="{mde:Multiply ControlContentThemeFontSize, 3.2}" />
<Setter Property="Foreground" Value="{mde:Alpha TextFillColorPrimaryBrush}" />
<Setter Property="FontWeight" Value="Light" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CTextBlock.Heading2">
<Style.Setters>
<Setter Property="FontSize" Value="{mde:Multiply ControlContentThemeFontSize, 1.6}" />
<Setter Property="Foreground" Value="{mde:Alpha TextFillColorPrimaryBrush}" />
<Setter Property="FontWeight" Value="Light" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CTextBlock.Heading3">
<Style.Setters>
<Setter Property="FontSize" Value="{mde:Multiply ControlContentThemeFontSize, 1.6}" />
<Setter Property="Foreground" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.7}" />
<Setter Property="FontWeight" Value="Light" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CTextBlock.Heading4">
<Style.Setters>
<Setter Property="FontSize" Value="{mde:Multiply ControlContentThemeFontSize, 1.2}" />
<Setter Property="Foreground" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.7}" />
<Setter Property="FontWeight" Value="Light" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CHyperlink">
<Style.Setters>
<Setter Property="IsUnderline" Value="true" />
<Setter Property="Foreground" Value="{StaticResource SystemAccentColor}" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CHyperlink:pointerover">
<Setter Property="Foreground" Value="{mde:Complementary SystemAccentColor}" />
</Style>
<Style Selector="Border.Table">
<Style.Setters>
<Setter Property="Margin" Value="5" />
<Setter Property="BorderThickness" Value="0,0,1,1" />
<Setter Property="BorderBrush" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.7}" />
</Style.Setters>
</Style>
<Style Selector="Grid.Table > Border">
<Style.Setters>
<Setter Property="Margin" Value="0" />
<Setter Property="BorderThickness" Value="1,1,0,0" />
<Setter Property="BorderBrush" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.7}" />
<Setter Property="Padding" Value="2" />
</Style.Setters>
</Style>
<Style Selector="Border.TableHeader">
<Style.Setters>
<Setter Property="Background" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.3}" />
</Style.Setters>
</Style>
<Style Selector="Border.TableHeader ctxt|CTextBlock">
<Style.Setters>
<Setter Property="FontWeight" Value="DemiBold" />
</Style.Setters>
</Style>
<Style Selector="Border.EvenTableRow">
<Style.Setters>
<Setter Property="Background" Value="{mde:Alpha ControlFillColorDefaultBrush, 0.9}" />
</Style.Setters>
</Style>
<Style Selector="Border.CodeBlock">
<Style.Setters>
<Setter Property="BorderBrush" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.2}" />
<Setter Property="BorderThickness" Value="0,5,0,5" />
<Setter Property="Margin" Value="5,0,5,0" />
<Setter Property="Background" Value="{mde:Alpha ControlFillColorDefaultBrush, 0.9}" />
</Style.Setters>
</Style>
<Style Selector="TextBlock.CodeBlock">
<Style.Setters>
<Setter Property="FontFamily" Value="menlo,monaco,consolas,droid sans mono,inconsolata,courier new,monospace,dejavu sans mono" />
<Setter Property="Foreground" Value="{mde:DivideColor Blue, TextFillColorPrimary, 0.4}" />
</Style.Setters>
</Style>
<Style Selector="Border.NoContainer">
<Style.Setters>
<Setter Property="BorderBrush" Value="Red" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CCode">
<Style.Setters>
<Setter Property="Foreground" Value="{mde:DivideColor Blue, TextFillColorPrimary, 0.4}" />
<Setter Property="Background" Value="{mde:Alpha ControlFillColorDefaultBrush, 0.9}" />
</Style.Setters>
</Style>
<Style Selector="Border.Note">
<Style.Setters>
<Setter Property="Margin" Value="5,0,5,0" />
<Setter Property="BorderBrush" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.2}" />
<Setter Property="BorderThickness" Value="3,3,3,3" />
<Setter Property="Background" Value="{mde:Alpha ControlFillColorDefaultBrush, 0.9}" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CTextBlock.Note">
<Style.Setters>
<Setter Property="Margin" Value="10, 5" />
</Style.Setters>
</Style>
<Style Selector="Grid.List">
<Style.Setters>
<Setter Property="Margin" Value="15,0,0,0" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CTextBlock.ListMarker">
<Style.Setters>
<Setter Property="Margin" Value="0,5,5,5" />
</Style.Setters>
</Style>
<Style Selector="Border.Blockquote">
<Style.Setters>
<Setter Property="BorderBrush" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.2}" />
<Setter Property="BorderThickness" Value="5,0,0,0" />
</Style.Setters>
</Style>
<Style Selector="StackPanel.Blockquote">
<Style.Setters>
<Setter Property="Margin" Value="10, 5" />
</Style.Setters>
</Style>
<Style Selector="mdc|Rule">
<Style.Setters>
<Setter Property="Margin" Value="0,3" />
</Style.Setters>
</Style>
</avalonia:MarkdownScrollViewer.Styles>
</avalonia:MarkdownScrollViewer>
</Grid>
</Border>
</Grid>
</UserControl>

View File

@ -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();
}
}

View File

@ -0,0 +1,220 @@
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 CancellationTokenSource? _installerCts;
private string? _changelog;
private string? _commit;
private string? _shortCommit;
private long _fileSize;
private bool _installationAvailable;
private bool _installationFinished;
private bool _installationInProgress;
private bool _loading = true;
private bool _retrievedDetails;
public ReleaseViewModel(Guid releaseId,
string version,
DateTimeOffset createdAt,
ILogger logger,
IUpdatingClient updatingClient,
INotificationService notificationService,
IUpdateService updateService)
{
_logger = logger;
_updatingClient = updatingClient;
_notificationService = notificationService;
_updateService = updateService;
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 Guid 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 bool IsCurrentVersion => Version == Constants.CurrentVersion;
public bool IsPreviousVersion => Version == _updateService.PreviousVersion;
public bool ShowStatusIndicator => IsCurrentVersion || IsPreviousVersion;
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);
InstallationFinished = true;
}
catch (Exception e)
{
if (_installerCts.IsCancellationRequested)
return;
_logger.Warning(e, "Failed to install update through UI");
_notificationService.CreateNotification()
.WithTitle("Failed to install update")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Warning)
.Show();
}
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 (TaskCanceledException)
{
// ignored
}
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;
}
}
}

View File

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

View File

@ -11,6 +11,7 @@ using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.Plugins;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Services.Updating;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Providers;
using Artemis.UI.Shared.Services;
@ -24,29 +25,29 @@ public class StartupWizardViewModel : DialogViewModelBase<bool>
private readonly IAutoRunProvider? _autoRunProvider;
private readonly IRgbService _rgbService;
private readonly ISettingsService _settingsService;
private readonly IUpdateService _updateService;
private readonly IWindowService _windowService;
private int _currentStep;
private bool _showContinue;
private bool _showFinish;
private bool _showGoBack;
public StartupWizardViewModel(IContainer container, ISettingsService settingsService, IRgbService rgbService, IPluginManagementService pluginManagementService, IWindowService windowService,
IUpdateService updateService, ISettingsVmFactory settingsVmFactory)
public StartupWizardViewModel(IContainer container,
ISettingsService settingsService,
IRgbService rgbService,
IPluginManagementService pluginManagementService,
IWindowService windowService,
ISettingsVmFactory settingsVmFactory)
{
_settingsService = settingsService;
_rgbService = rgbService;
_windowService = windowService;
_updateService = updateService;
_autoRunProvider = container.Resolve<IAutoRunProvider>(IfUnresolved.ReturnDefault);
Continue = ReactiveCommand.Create(ExecuteContinue);
GoBack = ReactiveCommand.Create(ExecuteGoBack);
SkipOrFinishWizard = ReactiveCommand.Create(ExecuteSkipOrFinishWizard);
SelectLayout = ReactiveCommand.Create<string>(ExecuteSelectLayout);
AssemblyInformationalVersionAttribute? versionAttribute = typeof(StartupWizardViewModel).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
Version = $"Version {versionAttribute?.InformationalVersion} build {Constants.BuildInfo.BuildNumberDisplay}";
Version = $"Version {Constants.CurrentVersion}";
// Take all compatible plugins that have an always-enabled device provider
DeviceProviders = new ObservableCollection<PluginViewModel>(pluginManagementService.GetAllPlugins()
@ -81,13 +82,12 @@ public class StartupWizardViewModel : DialogViewModelBase<bool>
public ObservableCollection<PluginViewModel> DeviceProviders { get; }
public bool IsAutoRunSupported => _autoRunProvider != null;
public bool IsUpdatingSupported => _updateService.UpdatingSupported;
public PluginSetting<bool> UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false);
public PluginSetting<int> UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15);
public PluginSetting<bool> UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true);
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.CheckForUpdates", true);
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.AutoUpdate", false);
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true);
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true);
public int CurrentStep
{
@ -119,7 +119,7 @@ public class StartupWizardViewModel : DialogViewModelBase<bool>
CurrentStep--;
// Skip the settings step if none of it's contents are supported
if (CurrentStep == 4 && !IsAutoRunSupported && !IsUpdatingSupported)
if (CurrentStep == 4 && !IsAutoRunSupported)
CurrentStep--;
SetupButtons();
@ -131,7 +131,7 @@ public class StartupWizardViewModel : DialogViewModelBase<bool>
CurrentStep++;
// Skip the settings step if none of it's contents are supported
if (CurrentStep == 4 && !IsAutoRunSupported && !IsUpdatingSupported)
if (CurrentStep == 4 && !IsAutoRunSupported)
CurrentStep++;
SetupButtons();

View File

@ -68,7 +68,7 @@
</StackPanel>
<!-- Update settings -->
<StackPanel IsVisible="{CompiledBinding IsUpdatingSupported}">
<StackPanel>
<TextBlock Classes="h4" Margin="0 15">
Updating
</TextBlock>

View File

@ -1,22 +0,0 @@
using System.Threading.Tasks;
namespace Artemis.UI.Services.Interfaces;
public interface IUpdateService : IArtemisUIService
{
/// <summary>
/// Gets a boolean indicating whether updating is supported.
/// </summary>
bool UpdatingSupported { get; }
/// <summary>
/// Gets or sets a boolean indicating whether auto-updating is suspended.
/// </summary>
bool SuspendAutoUpdate { get; set; }
/// <summary>
/// Manually checks for updates and offers to install it if found.
/// </summary>
/// <returns>Whether an update was found, regardless of whether the user chose to install it.</returns>
Task ManualUpdate();
}

View File

@ -1,113 +0,0 @@
using System;
using System.Threading.Tasks;
using System.Timers;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared.Providers;
using Artemis.UI.Shared.Services.MainWindow;
using Avalonia.Threading;
using DryIoc;
using Serilog;
namespace Artemis.UI.Services;
public class UpdateService : IUpdateService
{
private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour
private readonly PluginSetting<bool> _autoUpdate;
private readonly PluginSetting<bool> _checkForUpdates;
private readonly ILogger _logger;
private readonly IMainWindowService _mainWindowService;
private readonly IUpdateProvider? _updateProvider;
public UpdateService(ILogger logger, IContainer container, ISettingsService settingsService, IMainWindowService mainWindowService)
{
_logger = logger;
_mainWindowService = mainWindowService;
if (!Constants.BuildInfo.IsLocalBuild)
_updateProvider = container.Resolve<IUpdateProvider>(IfUnresolved.ReturnDefault);
_checkForUpdates = settingsService.GetSetting("UI.CheckForUpdates", true);
_autoUpdate = settingsService.GetSetting("UI.AutoUpdate", false);
_checkForUpdates.SettingChanged += CheckForUpdatesOnSettingChanged;
_mainWindowService.MainWindowOpened += WindowServiceOnMainWindowOpened;
Timer timer = new(UPDATE_CHECK_INTERVAL);
timer.Elapsed += TimerOnElapsed;
timer.Start();
}
private async void TimerOnElapsed(object? sender, ElapsedEventArgs e)
{
await AutoUpdate();
}
private async void CheckForUpdatesOnSettingChanged(object? sender, EventArgs e)
{
// Run an auto-update as soon as the setting gets changed to enabled
if (_checkForUpdates.Value)
await AutoUpdate();
}
private async void WindowServiceOnMainWindowOpened(object? sender, EventArgs e)
{
await AutoUpdate();
}
private async Task AutoUpdate()
{
if (_updateProvider == null || !_checkForUpdates.Value || SuspendAutoUpdate)
return;
try
{
bool updateAvailable = await _updateProvider.CheckForUpdate("master");
if (!updateAvailable)
return;
// Only offer it once per session
SuspendAutoUpdate = true;
// If the window is open show the changelog, don't auto-update while the user is busy
if (_mainWindowService.IsMainWindowOpen)
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
// Call OpenMainWindow anyway to focus the main window
_mainWindowService.OpenMainWindow();
await _updateProvider.OfferUpdate("master", true);
});
return;
}
// If the window is closed but auto-update is enabled, update silently
if (_autoUpdate.Value)
await _updateProvider.ApplyUpdate("master", true);
// If auto-update is disabled the update provider can show a notification and handle the rest
else
await _updateProvider.OfferUpdate("master", false);
}
catch (Exception e)
{
_logger.Warning(e, "Auto update failed");
}
}
public bool SuspendAutoUpdate { get; set; }
public bool UpdatingSupported => _updateProvider != null;
public async Task ManualUpdate()
{
if (_updateProvider == null || !_mainWindowService.IsMainWindowOpen)
return;
bool updateAvailable = await _updateProvider.CheckForUpdate("master");
if (!updateAvailable)
return;
await _updateProvider.OfferUpdate("master", true);
}
}

View File

@ -0,0 +1,88 @@
using System;
using System.Linq;
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 BasicUpdateNotificationProvider : IUpdateNotificationProvider
{
private readonly Func<IScreen, SettingsViewModel> _getSettingsViewModel;
private readonly IMainWindowService _mainWindowService;
private readonly INotificationService _notificationService;
private Action? _available;
private Action? _installed;
public BasicUpdateNotificationProvider(INotificationService notificationService, IMainWindowService mainWindowService, Func<IScreen, SettingsViewModel> getSettingsViewModel)
{
_notificationService = notificationService;
_mainWindowService = mainWindowService;
_getSettingsViewModel = getSettingsViewModel;
}
/// <inheritdoc />
public void ShowNotification(Guid releaseId, string releaseVersion)
{
if (_mainWindowService.IsMainWindowOpen)
ShowAvailable(releaseVersion);
else
_mainWindowService.MainWindowOpened += (_, _) => ShowAvailable(releaseVersion);
}
/// <inheritdoc />
public void ShowInstalledNotification(string installedVersion)
{
if (_mainWindowService.IsMainWindowOpen)
ShowInstalled(installedVersion);
else
_mainWindowService.MainWindowOpened += (_, _) => ShowInstalled(installedVersion);
}
private void ShowAvailable(string releaseVersion)
{
_available?.Invoke();
_available = _notificationService.CreateNotification()
.WithTitle("Update available")
.WithMessage($"Artemis {releaseVersion} has been released")
.WithSeverity(NotificationSeverity.Success)
.WithTimeout(TimeSpan.FromSeconds(15))
.HavingButton(b => b.WithText("View release").WithAction(() => ViewRelease(releaseVersion)))
.Show();
}
private void ShowInstalled(string installedVersion)
{
_installed?.Invoke();
_installed = _notificationService.CreateNotification()
.WithTitle("Update installed")
.WithMessage($"Artemis {installedVersion} has been installed.")
.WithSeverity(NotificationSeverity.Success)
.WithTimeout(TimeSpan.FromSeconds(15))
.HavingButton(b => b.WithText("View release").WithAction(() => ViewRelease(installedVersion)))
.Show();
}
private void ViewRelease(string version)
{
_installed?.Invoke();
_available?.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.PreselectVersion = version;
settingsViewModel.SelectedTab = releaseTabViewModel;
}
}

View File

@ -0,0 +1,9 @@
using System;
namespace Artemis.UI.Services.Updating;
public interface IUpdateNotificationProvider
{
void ShowNotification(Guid releaseId, string releaseVersion);
void ShowInstalledNotification(string installedVersion);
}

View File

@ -0,0 +1,53 @@
using System;
using System.Threading.Tasks;
using Artemis.UI.Services.Interfaces;
using Artemis.WebClient.Updating;
namespace Artemis.UI.Services.Updating;
public interface IUpdateService : IArtemisUIService
{
/// <summary>
/// Gets the current update channel.
/// </summary>
string Channel { get; }
/// <summary>
/// Gets the version number of the previous release that was installed, if any.
/// </summary>
string? PreviousVersion { get; }
/// <summary>
/// The latest cached release, can be updated by calling <see cref="CachedLatestRelease" />.
/// </summary>
IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; }
/// <summary>
/// Asynchronously caches the latest release.
/// </summary>
Task CacheLatestRelease();
/// <summary>
/// Asynchronously checks whether an update is available on the current <see cref="Channel" />.
/// </summary>
Task<bool> CheckForUpdate();
/// <summary>
/// Creates a release installed for a release with the provided ID.
/// </summary>
/// <param name="releaseId">The ID of the release to create the installer for.</param>
/// <returns>The resulting release installer.</returns>
ReleaseInstaller GetReleaseInstaller(Guid releaseId);
/// <summary>
/// Restarts the application to install a pending update.
/// </summary>
/// <param name="silent">A boolean indicating whether to perform a silent install of the update.</param>
void RestartForUpdate(bool silent);
/// <summary>
/// Initializes the update service.
/// </summary>
/// <returns>A boolean indicating whether a restart will occur to install a pending update.</returns>
bool Initialize();
}

View File

@ -0,0 +1,186 @@
using System;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Exceptions;
using Artemis.UI.Extensions;
using Artemis.WebClient.Updating;
using Octodiff.Core;
using Octodiff.Diagnostics;
using Serilog;
using StrawberryShake;
namespace Artemis.UI.Services.Updating;
/// <summary>
/// Represents the installation process of a release
/// </summary>
public class ReleaseInstaller : CorePropertyChanged
{
private readonly HttpClient _httpClient;
private readonly ILogger _logger;
private readonly Guid _releaseId;
private readonly Platform _updatePlatform;
private readonly IUpdatingClient _updatingClient;
private readonly Progress<float> _progress = new();
private IGetReleaseById_PublishedRelease _release = null!;
private IGetReleaseById_PublishedRelease_Artifacts _artifact = null!;
private Progress<float> _stepProgress = new();
private string _status = string.Empty;
private float _floatProgress;
public ReleaseInstaller(Guid releaseId, ILogger logger, IUpdatingClient updatingClient, HttpClient httpClient)
{
_releaseId = releaseId;
_logger = logger;
_updatingClient = updatingClient;
_httpClient = httpClient;
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");
_progress.ProgressChanged += (_, f) => Progress = f;
}
public string Status
{
get => _status;
private set => SetAndNotify(ref _status, value);
}
public float Progress
{
get => _floatProgress;
set => SetAndNotify(ref _floatProgress, value);
}
public async Task InstallAsync(CancellationToken cancellationToken)
{
_stepProgress = new Progress<float>();
((IProgress<float>) _progress).Report(0);
Status = "Retrieving details";
_logger.Information("Retrieving details for release {ReleaseId}", _releaseId);
IOperationResult<IGetReleaseByIdResult> result = await _updatingClient.GetReleaseById.ExecuteAsync(_releaseId, cancellationToken);
result.EnsureNoErrors();
_release = result.Data?.PublishedRelease!;
if (_release == null)
throw new Exception($"Could not find release with ID {_releaseId}");
_artifact = _release.Artifacts.FirstOrDefault(a => a.Platform == _updatePlatform)!;
if (_artifact == null)
throw new Exception("Found the release but it has no artifact for the current platform");
((IProgress<float>) _progress).Report(10);
// Determine whether the last update matches our local version, then we can download the delta
if (_release.PreviousRelease != null && File.Exists(Path.Combine(Constants.UpdatingFolder, $"{_release.PreviousRelease.Version}.zip")) && _artifact.DeltaFileInfo.DownloadSize != 0)
await DownloadDelta(Path.Combine(Constants.UpdatingFolder, $"{_release.PreviousRelease.Version}.zip"), cancellationToken);
else
await Download(cancellationToken);
}
private async Task DownloadDelta(string previousRelease, CancellationToken cancellationToken)
{
// 10 - 50%
_stepProgress.ProgressChanged += StepProgressOnProgressChanged;
void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress<float>) _progress).Report(10f + e * 0.4f);
Status = "Downloading...";
await using MemoryStream stream = new();
await _httpClient.DownloadDataAsync($"https://updating.artemis-rgb.com/api/artifacts/{_artifact.ArtifactId}/delta", stream, _stepProgress, cancellationToken);
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
await PatchDelta(stream, previousRelease, cancellationToken);
}
private async Task PatchDelta(Stream deltaStream, string previousRelease, CancellationToken cancellationToken)
{
// 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(Constants.UpdatingFolder, $"{_release.Version}.zip"), FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
await using (FileStream baseStream = File.OpenRead(previousRelease))
{
deltaStream.Seek(0, SeekOrigin.Begin);
DeltaApplier deltaApplier = new() {SkipHashCheck = true};
// Patching is not async and so fast that it's not worth adding a progress reporter
deltaApplier.Apply(baseStream, new BinaryDeltaReader(deltaStream, new NullProgressReporter()), newFileStream);
cancellationToken.ThrowIfCancellationRequested();
}
// The previous release is no longer required now that the latest has been downloaded
File.Delete(previousRelease);
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
await ValidateArchive(newFileStream, cancellationToken);
await Extract(newFileStream, cancellationToken);
}
private async Task Download(CancellationToken cancellationToken)
{
// 10 - 60%
_stepProgress.ProgressChanged += StepProgressOnProgressChanged;
void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress<float>) _progress).Report(10f + e * 0.5f);
Status = "Downloading...";
await using FileStream stream = new(Path.Combine(Constants.UpdatingFolder, $"{_release.Version}.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, cancellationToken);
await Extract(stream, cancellationToken);
}
private async Task Extract(Stream archiveStream, CancellationToken cancellationToken)
{
// 60 - 100%
_stepProgress.ProgressChanged += StepProgressOnProgressChanged;
void StepProgressOnProgressChanged(object? sender, float e) => ((IProgress<float>) _progress).Report(60f + e * 0.4f);
Status = "Extracting...";
// Ensure the directory is empty
string extractDirectory = Path.Combine(Constants.UpdatingFolder, "pending");
if (Directory.Exists(extractDirectory))
Directory.Delete(extractDirectory, true);
Directory.CreateDirectory(extractDirectory);
await Task.Run(() =>
{
archiveStream.Seek(0, SeekOrigin.Begin);
using ZipArchive archive = new(archiveStream);
archive.ExtractToDirectory(extractDirectory, false, _stepProgress, cancellationToken);
}, cancellationToken);
((IProgress<float>) _progress).Report(100);
_stepProgress.ProgressChanged -= StepProgressOnProgressChanged;
}
private async Task ValidateArchive(Stream archiveStream, CancellationToken cancellationToken)
{
using MD5 md5 = MD5.Create();
archiveStream.Seek(0, SeekOrigin.Begin);
string hash = BitConverter.ToString(await md5.ComputeHashAsync(archiveStream, cancellationToken)).Replace("-", "");
if (hash != _artifact.FileInfo.Md5Hash)
throw new ArtemisUIException($"Update file hash mismatch, expected \"{_artifact.FileInfo.Md5Hash}\" but got \"{hash}\"");
}
}

View File

@ -0,0 +1,251 @@
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.Storage.Entities.General;
using Artemis.Storage.Repositories;
using Artemis.UI.Exceptions;
using Artemis.UI.Shared.Services.MainWindow;
using Artemis.WebClient.Updating;
using Serilog;
using StrawberryShake;
using Timer = System.Timers.Timer;
namespace Artemis.UI.Services.Updating;
public class UpdateService : IUpdateService
{
private const double UPDATE_CHECK_INTERVAL = 3_600_000; // once per hour
private readonly PluginSetting<bool> _autoCheck;
private readonly PluginSetting<bool> _autoInstall;
private readonly Func<Guid, ReleaseInstaller> _getReleaseInstaller;
private readonly ILogger _logger;
private readonly IMainWindowService _mainWindowService;
private readonly IReleaseRepository _releaseRepository;
private readonly Lazy<IUpdateNotificationProvider> _updateNotificationProvider;
private readonly Platform _updatePlatform;
private readonly IUpdatingClient _updatingClient;
private bool _suspendAutoCheck;
private DateTime _lastAutoUpdateCheck;
public UpdateService(ILogger logger,
ISettingsService settingsService,
IMainWindowService mainWindowService,
IUpdatingClient updatingClient,
IReleaseRepository releaseRepository,
Lazy<IUpdateNotificationProvider> updateNotificationProvider,
Func<Guid, ReleaseInstaller> getReleaseInstaller)
{
_logger = logger;
_mainWindowService = mainWindowService;
_updatingClient = updatingClient;
_releaseRepository = releaseRepository;
_updateNotificationProvider = updateNotificationProvider;
_getReleaseInstaller = getReleaseInstaller;
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");
_autoCheck = settingsService.GetSetting("UI.Updating.AutoCheck", true);
_autoInstall = settingsService.GetSetting("UI.Updating.AutoInstall", true);
_autoCheck.SettingChanged += HandleAutoUpdateEvent;
mainWindowService.MainWindowOpened += HandleAutoUpdateEvent;
Timer timer = new(UPDATE_CHECK_INTERVAL);
timer.Elapsed += HandleAutoUpdateEvent;
timer.Start();
}
private void ProcessReleaseStatus()
{
string currentVersion = Constants.CurrentVersion;
bool updated = _releaseRepository.SaveVersionInstallDate(currentVersion);
PreviousVersion = _releaseRepository.GetPreviousInstalledVersion()?.Version;
if (!Directory.Exists(Constants.UpdatingFolder))
return;
// Clean up the update folder, leaving only the last ZIP
foreach (string file in Directory.GetFiles(Constants.UpdatingFolder))
{
if (Path.GetExtension(file) != ".zip" || Path.GetFileName(file) == $"{currentVersion}.zip")
continue;
try
{
_logger.Debug("Cleaning up old update file at {FilePath}", file);
File.Delete(file);
}
catch (Exception e)
{
_logger.Warning(e, "Failed to clean up old update file at {FilePath}", file);
}
}
if (updated)
_updateNotificationProvider.Value.ShowInstalledNotification(currentVersion);
}
private void ShowUpdateNotification(IGetNextRelease_NextPublishedRelease release)
{
_updateNotificationProvider.Value.ShowNotification(release.Id, release.Version);
}
private async Task AutoInstallUpdate(IGetNextRelease_NextPublishedRelease release)
{
ReleaseInstaller installer = _getReleaseInstaller(release.Id);
await installer.InstallAsync(CancellationToken.None);
RestartForUpdate(true);
}
private async void HandleAutoUpdateEvent(object? sender, EventArgs e)
{
// The event can trigger from multiple sources with a timer acting as a fallback, only actual perform an action once per max 59 minutes
if (DateTime.UtcNow - _lastAutoUpdateCheck < TimeSpan.FromMinutes(59))
return;
_lastAutoUpdateCheck = DateTime.UtcNow;
if (!_autoCheck.Value || _suspendAutoCheck)
return;
try
{
await CheckForUpdate();
}
catch (Exception ex)
{
_logger.Warning(ex, "Auto update-check failed");
}
}
/// <inheritdoc />
public string Channel { get; private set; } = "master";
/// <inheritdoc />
public string? PreviousVersion { get; private set; }
/// <inheritdoc />
public IGetNextRelease_NextPublishedRelease? CachedLatestRelease { get; private set; }
/// <inheritdoc />
public async Task CacheLatestRelease()
{
try
{
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(Constants.CurrentVersion, Channel, _updatePlatform);
CachedLatestRelease = result.Data?.NextPublishedRelease;
}
catch (Exception e)
{
_logger.Warning(e, "Failed to cache latest release");
}
}
/// <inheritdoc />
public async Task<bool> CheckForUpdate()
{
_logger.Information("Performing auto-update check");
IOperationResult<IGetNextReleaseResult> result = await _updatingClient.GetNextRelease.ExecuteAsync(Constants.CurrentVersion, Channel, _updatePlatform);
result.EnsureNoErrors();
// Update cache
CachedLatestRelease = result.Data?.NextPublishedRelease;
// No update was found
if (CachedLatestRelease == null)
return false;
// Unless auto install is enabled, only offer it once per session
if (!_autoInstall.Value)
_suspendAutoCheck = true;
// If the window is open show the changelog, don't auto-update while the user is busy
if (_mainWindowService.IsMainWindowOpen || !_autoInstall.Value)
{
_logger.Information("New update available, offering version {AvailableVersion}", CachedLatestRelease.Version);
ShowUpdateNotification(CachedLatestRelease);
}
else
{
_logger.Information("New update available, auto-installing version {AvailableVersion}", CachedLatestRelease.Version);
await AutoInstallUpdate(CachedLatestRelease);
}
return true;
}
/// <inheritdoc />
public ReleaseInstaller GetReleaseInstaller(Guid releaseId)
{
return _getReleaseInstaller(releaseId);
}
/// <inheritdoc />
public void RestartForUpdate(bool silent)
{
if (!Directory.Exists(Path.Combine(Constants.UpdatingFolder, "pending")))
throw new ArtemisUIException("Cannot install update, none is pending.");
Directory.Move(Path.Combine(Constants.UpdatingFolder, "pending"), Path.Combine(Constants.UpdatingFolder, "installing"));
Utilities.ApplyUpdate(silent);
}
/// <inheritdoc />
public bool Initialize()
{
string? channelArgument = Constants.StartupArguments.FirstOrDefault(a => a.StartsWith("--channel="));
if (channelArgument != null)
Channel = channelArgument.Split("=")[1];
if (string.IsNullOrWhiteSpace(Channel))
Channel = "master";
// There should never be an installing folder
if (Directory.Exists(Path.Combine(Constants.UpdatingFolder, "installing")))
{
_logger.Warning("Cleaning up leftover installing folder, did an update go wrong?");
try
{
Directory.Delete(Path.Combine(Constants.UpdatingFolder, "installing"), true);
}
catch (Exception e)
{
_logger.Error(e, "Failed to delete leftover installing folder");
}
}
// If an update is pending, don't bother with anything else
if (Directory.Exists(Path.Combine(Constants.UpdatingFolder, "pending")))
{
_logger.Information("Installing pending update");
try
{
RestartForUpdate(true);
return true;
}
catch (Exception e)
{
_logger.Warning(e, "Failed to apply pending update");
return false;
}
}
ProcessReleaseStatus();
// Trigger the auto update event so that it doesn't take an hour for the first check to happen
HandleAutoUpdateEvent(this, EventArgs.Empty);
_logger.Information("Update service initialized for {Channel} channel", Channel);
return false;
}
}

View File

@ -6,4 +6,5 @@
<!-- <FluentTheme Mode="Dark"></FluentTheme> -->
<StyleInclude Source="avares://Material.Icons.Avalonia/App.xaml" />
<StyleInclude Source="avares://Artemis.UI.Shared/Styles/Artemis.axaml" />
<StyleInclude Source="avares://Artemis.UI/Styles/Markdown.axaml" />
</Styles>

View File

@ -0,0 +1,191 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
xmlns:mdc="clr-namespace:Markdown.Avalonia.Controls;assembly=Markdown.Avalonia"
xmlns:mde="clr-namespace:Markdown.Avalonia.Extensions;assembly=Markdown.Avalonia"
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia">
<Design.PreviewWith>
<Border Padding="20">
<avalonia:MarkdownScrollViewer Classes="Test">
<avalonia:MarkdownScrollViewer.Styles>
<Style Selector="ctxt|CTextBlock">
<Style.Setters>
<Setter Property="FontSize" Value="{mde:Multiply ControlContentThemeFontSize, 1}" />
<Setter Property="Margin" Value="0,5" />
</Style.Setters>
</Style>
<Style Selector="TextBlock">
<Style.Setters>
<Setter Property="FontSize" Value="{mde:Multiply ControlContentThemeFontSize, 1}" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CTextBlock.Heading1">
<Style.Setters>
<Setter Property="FontSize" Value="{mde:Multiply ControlContentThemeFontSize, 3.2}" />
<Setter Property="Foreground" Value="{mde:Alpha TextFillColorPrimaryBrush}" />
<Setter Property="FontWeight" Value="Light" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CTextBlock.Heading2">
<Style.Setters>
<Setter Property="FontSize" Value="{mde:Multiply ControlContentThemeFontSize, 1.6}" />
<Setter Property="Foreground" Value="{mde:Alpha TextFillColorPrimaryBrush}" />
<Setter Property="FontWeight" Value="Light" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CTextBlock.Heading3">
<Style.Setters>
<Setter Property="FontSize" Value="{mde:Multiply ControlContentThemeFontSize, 1.6}" />
<Setter Property="Foreground" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.7}" />
<Setter Property="FontWeight" Value="Light" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CTextBlock.Heading4">
<Style.Setters>
<Setter Property="FontSize" Value="{mde:Multiply ControlContentThemeFontSize, 1.2}" />
<Setter Property="Foreground" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.7}" />
<Setter Property="FontWeight" Value="Light" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CHyperlink">
<Style.Setters>
<Setter Property="IsUnderline" Value="true" />
<Setter Property="Foreground" Value="{StaticResource SystemAccentColor}" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CHyperlink:pointerover">
<Setter Property="Foreground" Value="{mde:Complementary SystemAccentColor}" />
</Style>
<Style Selector="Border.Table">
<Style.Setters>
<Setter Property="Margin" Value="5" />
<Setter Property="BorderThickness" Value="0,0,1,1" />
<Setter Property="BorderBrush" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.7}" />
</Style.Setters>
</Style>
<Style Selector="Grid.Table > Border">
<Style.Setters>
<Setter Property="Margin" Value="0" />
<Setter Property="BorderThickness" Value="1,1,0,0" />
<Setter Property="BorderBrush" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.7}" />
<Setter Property="Padding" Value="2" />
</Style.Setters>
</Style>
<Style Selector="Border.TableHeader">
<Style.Setters>
<Setter Property="Background" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.3}" />
</Style.Setters>
</Style>
<Style Selector="Border.TableHeader ctxt|CTextBlock">
<Style.Setters>
<Setter Property="FontWeight" Value="DemiBold" />
</Style.Setters>
</Style>
<Style Selector="Border.EvenTableRow">
<Style.Setters>
<Setter Property="Background" Value="{mde:Alpha ControlFillColorDefaultBrush, 0.9}" />
</Style.Setters>
</Style>
<Style Selector="Border.CodeBlock">
<Style.Setters>
<Setter Property="BorderBrush" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.2}" />
<Setter Property="BorderThickness" Value="0,5,0,5" />
<Setter Property="Margin" Value="5,0,5,0" />
<Setter Property="Background" Value="{mde:Alpha ControlFillColorDefaultBrush, 0.9}" />
</Style.Setters>
</Style>
<Style Selector="TextBlock.CodeBlock">
<Style.Setters>
<Setter Property="FontFamily" Value="menlo,monaco,consolas,droid sans mono,inconsolata,courier new,monospace,dejavu sans mono" />
<Setter Property="Foreground" Value="{mde:DivideColor Blue, TextFillColorPrimary, 0.4}" />
</Style.Setters>
</Style>
<Style Selector="Border.NoContainer">
<Style.Setters>
<Setter Property="BorderBrush" Value="Red" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CCode">
<Style.Setters>
<Setter Property="Foreground" Value="{mde:DivideColor Blue, TextFillColorPrimary, 0.4}" />
<Setter Property="Background" Value="{mde:Alpha ControlFillColorDefaultBrush, 0.9}" />
</Style.Setters>
</Style>
<Style Selector="Border.Note">
<Style.Setters>
<Setter Property="Margin" Value="5,0,5,0" />
<Setter Property="BorderBrush" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.2}" />
<Setter Property="BorderThickness" Value="3,3,3,3" />
<Setter Property="Background" Value="{mde:Alpha ControlFillColorDefaultBrush, 0.9}" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CTextBlock.Note">
<Style.Setters>
<Setter Property="Margin" Value="10, 5" />
</Style.Setters>
</Style>
<Style Selector="Grid.List">
<Style.Setters>
<Setter Property="Margin" Value="15,0,0,0" />
</Style.Setters>
</Style>
<Style Selector="ctxt|CTextBlock.ListMarker">
<Style.Setters>
<Setter Property="Margin" Value="0,5,5,5" />
</Style.Setters>
</Style>
<Style Selector="Border.Blockquote">
<Style.Setters>
<Setter Property="BorderBrush" Value="{mde:Alpha TextFillColorPrimaryBrush, 0.2}" />
<Setter Property="BorderThickness" Value="5,0,0,0" />
</Style.Setters>
</Style>
<Style Selector="StackPanel.Blockquote">
<Style.Setters>
<Setter Property="Margin" Value="10, 5" />
</Style.Setters>
</Style>
<Style Selector="mdc|Rule">
<Style.Setters>
<Setter Property="Margin" Value="0,3" />
</Style.Setters>
</Style>
</avalonia:MarkdownScrollViewer.Styles>
## Core
* Cleaned up ProfileService render condition
* Core - Added fading in and out of profiles
* Core - Apply opacity layer only when fading
* Core - Fixed when condition stops being true mid-fade
* Core - Removed FadingStatus enum
# General
- Meta - Fixed warnings
- Meta - Update RGB.NET
# Plugins
- Plugins - Ignore version when loading shared assemblies
# UI
- Sidebar - Improved category reordering code
</avalonia:MarkdownScrollViewer>
</Border>
</Design.PreviewWith>
</Styles>

View File

@ -0,0 +1,12 @@
{
"version": 1,
"isRoot": true,
"tools": {
"strawberryshake.tools": {
"version": "13.0.0-rc.4",
"commands": [
"dotnet-graphql"
]
}
}
}

View File

@ -0,0 +1,15 @@
{
"name": "Untitled GraphQL Schema",
"schemaPath": "schema.graphql",
"extensions": {
"endpoints": {
"Default GraphQL Endpoint": {
"url": "https://updating.artemis-rgb.com/graphql",
"headers": {
"user-agent": "JS GraphQL"
},
"introspect": true
}
}
}
}

View File

@ -0,0 +1,22 @@
{
"schema": "schema.graphql",
"documents": "**/*.graphql",
"extensions": {
"strawberryShake": {
"name": "UpdatingClient",
"namespace": "Artemis.WebClient.Updating",
"url": "https://updating.artemis-rgb.com/graphql/",
"emitGeneratedCode": false,
"records": {
"inputs": false,
"entities": false
},
"transportProfiles": [
{
"default": "Http",
"subscription": "WebSocket"
}
]
}
}
}

View File

@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="6.1.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
<PackageReference Include="StrawberryShake.Server" Version="13.0.0-rc.4" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,26 @@
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using Microsoft.Extensions.DependencyInjection;
namespace Artemis.WebClient.Updating.DryIoc;
/// <summary>
/// Provides an extension method to register services onto a DryIoc <see cref="IContainer"/>.
/// </summary>
public static class ContainerExtensions
{
/// <summary>
/// Registers the updating client into the container.
/// </summary>
/// <param name="container">The builder building the current container</param>
public static void RegisterUpdatingClient(this IContainer container)
{
ServiceCollection serviceCollection = new();
serviceCollection
.AddHttpClient()
.AddUpdatingClient()
.ConfigureHttpClient(client => client.BaseAddress = new Uri("https://updating.artemis-rgb.com/graphql"));
container.WithDependencyInjectionAdapter(serviceCollection);
}
}

View File

@ -0,0 +1,6 @@
query GetNextRelease($currentVersion: String, $branch: String!, $platform: Platform!) {
nextPublishedRelease(version: $currentVersion, branch: $branch, platform: $platform) {
id
version
}
}

View File

@ -0,0 +1,26 @@
query GetReleaseById($id: UUID!) {
publishedRelease(id: $id) {
branch
commit
version
previousRelease {
version
}
changelog
artifacts {
platform
artifactId
fileInfo {
...fileInfo
}
deltaFileInfo {
...fileInfo
}
}
}
}
fragment fileInfo on ArtifactFileInfo {
md5Hash
downloadSize
}

View File

@ -0,0 +1,22 @@
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
}
}
}

View File

@ -0,0 +1,13 @@
scalar _KeyFieldSet
directive @key(fields: _KeyFieldSet!) on SCHEMA | OBJECT
directive @serializationType(name: String!) on SCALAR
directive @runtimeType(name: String!) on SCALAR
directive @enumValue(value: String!) on ENUM_VALUE
directive @rename(name: String!) on INPUT_FIELD_DEFINITION | INPUT_OBJECT | ENUM | ENUM_VALUE
extend schema @key(fields: "id")

View File

@ -0,0 +1,319 @@
# This file was generated based on ".graphqlconfig". Do not edit manually.
schema {
query: Query
mutation: Mutation
}
type ArtemisChannel {
branch: String!
releases: Int!
}
type Artifact {
artifactId: Long!
deltaFileInfo: ArtifactFileInfo!
fileInfo: ArtifactFileInfo!
id: UUID!
platform: Platform!
}
type ArtifactFileInfo {
downloadSize: Long!
downloads: Long!
id: UUID!
md5Hash: String
}
"Information about the offset pagination."
type CollectionSegmentInfo {
"Indicates whether more items exist following the set defined by the clients arguments."
hasNextPage: Boolean!
"Indicates whether more items exist prior the set defined by the clients arguments."
hasPreviousPage: Boolean!
}
type Mutation {
updateReleaseChangelog(input: UpdateReleaseChangelogInput!): UpdateReleaseChangelogPayload!
}
"Information about pagination in a connection."
type PageInfo {
"When paginating forwards, the cursor to continue."
endCursor: String
"Indicates whether more edges exist following the set defined by the clients arguments."
hasNextPage: Boolean!
"Indicates whether more edges exist prior the set defined by the clients arguments."
hasPreviousPage: Boolean!
"When paginating backwards, the cursor to continue."
startCursor: String
}
"A connection to a list of items."
type PublishedReleasesConnection {
"A list of edges."
edges: [PublishedReleasesEdge!]
"A flattened list of the nodes."
nodes: [Release!]
"Information to aid in pagination."
pageInfo: PageInfo!
"Identifies the total count of items in the connection."
totalCount: Int!
}
"An edge in a connection."
type PublishedReleasesEdge {
"A cursor for use in pagination."
cursor: String!
"The item at the end of the edge."
node: Release!
}
type Query {
channelByBranch(branch: String!): ArtemisChannel
channels: [ArtemisChannel!]!
nextPublishedRelease(branch: String!, platform: Platform!, version: String): Release
publishedChannels: [String!]!
publishedRelease(id: UUID!): 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: UUID!): Release
releaseStatistics(order: [ReleaseStatisticSortInput!], where: ReleaseStatisticFilterInput): [ReleaseStatistic!]!
releases(order: [ReleaseSortInput!], skip: Int, take: Int, where: ReleaseFilterInput): ReleasesCollectionSegment
}
type Release {
artifacts: [Artifact!]!
branch: String!
changelog: String!
commit: String!
createdAt: DateTime!
id: UUID!
isDraft: Boolean!
previousRelease: Release
version: String!
workflowRunId: Long!
}
type ReleaseStatistic {
count: Int!
lastReportedUsage: DateTime!
linuxCount: Int!
osxCount: Int!
releaseId: UUID!
windowsCount: Int!
}
"A segment of a collection."
type ReleasesCollectionSegment {
"A flattened list of the items."
items: [Release!]
"Information to aid in pagination."
pageInfo: CollectionSegmentInfo!
totalCount: Int!
}
type UpdateReleaseChangelogPayload {
release: Release
}
enum ApplyPolicy {
AFTER_RESOLVER
BEFORE_RESOLVER
VALIDATION
}
enum Platform {
LINUX
OSX
WINDOWS
}
enum SortEnumType {
ASC
DESC
}
"The `DateTime` scalar represents an ISO-8601 compliant date time type."
scalar DateTime
"The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1."
scalar Long
scalar UUID
input ArtifactFileInfoFilterInput {
and: [ArtifactFileInfoFilterInput!]
downloadSize: LongOperationFilterInput
downloads: LongOperationFilterInput
id: UuidOperationFilterInput
md5Hash: StringOperationFilterInput
or: [ArtifactFileInfoFilterInput!]
}
input ArtifactFilterInput {
and: [ArtifactFilterInput!]
artifactId: LongOperationFilterInput
deltaFileInfo: ArtifactFileInfoFilterInput
fileInfo: ArtifactFileInfoFilterInput
id: UuidOperationFilterInput
or: [ArtifactFilterInput!]
platform: PlatformOperationFilterInput
}
input BooleanOperationFilterInput {
eq: Boolean
neq: Boolean
}
input DateTimeOperationFilterInput {
eq: DateTime
gt: DateTime
gte: DateTime
in: [DateTime]
lt: DateTime
lte: DateTime
neq: DateTime
ngt: DateTime
ngte: DateTime
nin: [DateTime]
nlt: DateTime
nlte: DateTime
}
input IntOperationFilterInput {
eq: Int
gt: Int
gte: Int
in: [Int]
lt: Int
lte: Int
neq: Int
ngt: Int
ngte: Int
nin: [Int]
nlt: Int
nlte: Int
}
input ListFilterInputTypeOfArtifactFilterInput {
all: ArtifactFilterInput
any: Boolean
none: ArtifactFilterInput
some: ArtifactFilterInput
}
input LongOperationFilterInput {
eq: Long
gt: Long
gte: Long
in: [Long]
lt: Long
lte: Long
neq: Long
ngt: Long
ngte: Long
nin: [Long]
nlt: Long
nlte: Long
}
input PlatformOperationFilterInput {
eq: Platform
in: [Platform!]
neq: Platform
nin: [Platform!]
}
input ReleaseFilterInput {
and: [ReleaseFilterInput!]
artifacts: ListFilterInputTypeOfArtifactFilterInput
branch: StringOperationFilterInput
changelog: StringOperationFilterInput
commit: StringOperationFilterInput
createdAt: DateTimeOperationFilterInput
id: UuidOperationFilterInput
isDraft: BooleanOperationFilterInput
or: [ReleaseFilterInput!]
previousRelease: ReleaseFilterInput
version: StringOperationFilterInput
workflowRunId: LongOperationFilterInput
}
input ReleaseSortInput {
branch: SortEnumType
changelog: SortEnumType
commit: SortEnumType
createdAt: SortEnumType
id: SortEnumType
isDraft: SortEnumType
previousRelease: ReleaseSortInput
version: SortEnumType
workflowRunId: SortEnumType
}
input ReleaseStatisticFilterInput {
and: [ReleaseStatisticFilterInput!]
count: IntOperationFilterInput
lastReportedUsage: DateTimeOperationFilterInput
linuxCount: IntOperationFilterInput
or: [ReleaseStatisticFilterInput!]
osxCount: IntOperationFilterInput
releaseId: UuidOperationFilterInput
windowsCount: IntOperationFilterInput
}
input ReleaseStatisticSortInput {
count: SortEnumType
lastReportedUsage: SortEnumType
linuxCount: SortEnumType
osxCount: SortEnumType
releaseId: SortEnumType
windowsCount: SortEnumType
}
input StringOperationFilterInput {
and: [StringOperationFilterInput!]
contains: String
endsWith: String
eq: String
in: [String]
ncontains: String
nendsWith: String
neq: String
nin: [String]
nstartsWith: String
or: [StringOperationFilterInput!]
startsWith: String
}
input UpdateReleaseChangelogInput {
changelog: String!
id: UUID!
isDraft: Boolean!
}
input UuidOperationFilterInput {
eq: UUID
gt: UUID
gte: UUID
in: [UUID]
lt: UUID
lte: UUID
neq: UUID
ngt: UUID
ngte: UUID
nin: [UUID]
nlt: UUID
nlte: UUID
}

View File

@ -19,6 +19,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.UI.MacOS", "Artemis
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.VisualScripting", "Artemis.VisualScripting\Artemis.VisualScripting.csproj", "{412B921A-26F5-4AE6-8B32-0C19BE54F421}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.WebClient.Updating", "Artemis.WebClient.Updating\Artemis.WebClient.Updating.csproj", "{7C8C6F50-0CC8-45B3-B608-A7218C005E4B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|x64 = Debug|x64
@ -57,6 +59,10 @@ Global
{412B921A-26F5-4AE6-8B32-0C19BE54F421}.Debug|x64.Build.0 = Debug|x64
{412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|x64.ActiveCfg = Release|x64
{412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|x64.Build.0 = Release|x64
{7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Debug|x64.ActiveCfg = Debug|Any CPU
{7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Debug|x64.Build.0 = Debug|Any CPU
{7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Release|x64.ActiveCfg = Release|Any CPU
{7C8C6F50-0CC8-45B3-B608-A7218C005E4B}.Release|x64.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE