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:
commit
636b32bd86
109
.github/workflows/master.yml
vendored
Normal file
109
.github/workflows/master.yml
vendored
Normal 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
|
||||
2
.github/workflows/nuget.yml
vendored
2
.github/workflows/nuget.yml
vendored
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
20
src/Artemis.Core/Events/UpdateEventArgs.cs
Normal file
20
src/Artemis.Core/Events/UpdateEventArgs.cs
Normal 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; }
|
||||
}
|
||||
@ -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);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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"))
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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;
|
||||
|
||||
11
src/Artemis.Storage/Entities/General/ReleaseEntity.cs
Normal file
11
src/Artemis.Storage/Entities/General/ReleaseEntity.cs
Normal 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; }
|
||||
}
|
||||
@ -9,4 +9,6 @@ public interface IQueuedActionRepository : IRepository
|
||||
void Remove(QueuedActionEntity queuedActionEntity);
|
||||
List<QueuedActionEntity> GetAll();
|
||||
List<QueuedActionEntity> GetByType(string type);
|
||||
bool IsTypeQueued(string type);
|
||||
void ClearByType(string type);
|
||||
}
|
||||
@ -41,5 +41,17 @@ public class QueuedActionRepository : IQueuedActionRepository
|
||||
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).ToList();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsTypeQueued(string type)
|
||||
{
|
||||
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).Count() > 0;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ClearByType(string type)
|
||||
{
|
||||
_repository.DeleteMany<QueuedActionEntity>(q => q.Type == type);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
38
src/Artemis.Storage/Repositories/ReleaseRepository.cs
Normal file
38
src/Artemis.Storage/Repositories/ReleaseRepository.cs
Normal 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();
|
||||
}
|
||||
@ -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();
|
||||
|
||||
34
src/Artemis.UI.Shared/Converters/BytesToStringConverter.cs
Normal file
34
src/Artemis.UI.Shared/Converters/BytesToStringConverter.cs
Normal file
@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using Humanizer;
|
||||
using Humanizer.Bytes;
|
||||
|
||||
namespace Artemis.UI.Shared.Converters;
|
||||
|
||||
/// <summary>
|
||||
/// Converts bytes to a string
|
||||
/// </summary>
|
||||
public class BytesToStringConverter : IValueConverter
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is int intBytes)
|
||||
return intBytes.Bytes().Humanize();
|
||||
if (value is long longBytes)
|
||||
return longBytes.Bytes().Humanize();
|
||||
if (value is double doubleBytes)
|
||||
return doubleBytes.Bytes().Humanize();
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is string formatted && ByteSize.TryParse(formatted, out ByteSize result))
|
||||
return result.Bytes;
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.MainWindow;
|
||||
|
||||
@ -12,6 +13,11 @@ public interface IMainWindowService : IArtemisSharedUIService
|
||||
/// </summary>
|
||||
bool IsMainWindowOpen { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the host screen contained in the main window
|
||||
/// </summary>
|
||||
IScreen? HostScreen { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the main window provider that controls the state of the main window
|
||||
/// </summary>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Shared.Services.MainWindow;
|
||||
|
||||
@ -6,6 +7,12 @@ internal class MainWindowService : IMainWindowService
|
||||
{
|
||||
private IMainWindowProvider? _mainWindowManager;
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsMainWindowOpen { get; private set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IScreen? HostScreen { get; set; }
|
||||
|
||||
protected virtual void OnMainWindowOpened()
|
||||
{
|
||||
MainWindowOpened?.Invoke(this, EventArgs.Empty);
|
||||
@ -64,8 +71,6 @@ internal class MainWindowService : IMainWindowService
|
||||
OnMainWindowUnfocused();
|
||||
}
|
||||
|
||||
public bool IsMainWindowOpen { get; private set; }
|
||||
|
||||
public void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider)
|
||||
{
|
||||
if (mainWindowProvider == null) throw new ArgumentNullException(nameof(mainWindowProvider));
|
||||
|
||||
@ -21,6 +21,7 @@
|
||||
|
||||
<!-- Custom styles -->
|
||||
<StyleInclude Source="/Styles/Border.axaml" />
|
||||
<StyleInclude Source="/Styles/Skeleton.axaml" />
|
||||
<StyleInclude Source="/Styles/Button.axaml" />
|
||||
<StyleInclude Source="/Styles/Condensed.axaml" />
|
||||
<StyleInclude Source="/Styles/ColorPickerButton.axaml" />
|
||||
|
||||
174
src/Artemis.UI.Shared/Styles/Skeleton.axaml
Normal file
174
src/Artemis.UI.Shared/Styles/Skeleton.axaml
Normal file
@ -0,0 +1,174 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
<Design.PreviewWith>
|
||||
<Grid ColumnDefinitions="*,*">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition />
|
||||
<RowDefinition />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid Margin="20" Grid.Column="0">
|
||||
<StackPanel>
|
||||
<TextBlock Classes="h1">This is heading 1</TextBlock>
|
||||
<TextBlock Classes="h2">This is heading 2</TextBlock>
|
||||
<TextBlock Classes="h3">This is heading 3</TextBlock>
|
||||
<TextBlock Classes="h4">This is heading 4</TextBlock>
|
||||
<TextBlock Classes="h5">This is heading 5</TextBlock>
|
||||
<TextBlock Classes="h6">This is heading 6</TextBlock>
|
||||
<TextBlock>This is regular text</TextBlock>
|
||||
<TextBlock>This is regular text</TextBlock>
|
||||
<TextBlock>This is regular text</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Margin="20" Grid.Column="1">
|
||||
<StackPanel>
|
||||
<Border Width="400" Classes="skeleton-text h1"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h2"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h3"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h4"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h5"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h6"></Border>
|
||||
<Border Width="400" Classes="skeleton-text"></Border>
|
||||
<Border Width="400" Classes="skeleton-text"></Border>
|
||||
<Border Width="400" Classes="skeleton-text"></Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel>
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="Background" Value="#55ff0000"></Setter>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<TextBlock Classes="h1">This is heading 1</TextBlock>
|
||||
<TextBlock Classes="h2">This is heading 2</TextBlock>
|
||||
<TextBlock Classes="h3">This is heading 3</TextBlock>
|
||||
<TextBlock Classes="h4">This is heading 4</TextBlock>
|
||||
<TextBlock Classes="h5">This is heading 5</TextBlock>
|
||||
<TextBlock Classes="h6">This is heading 6</TextBlock>
|
||||
<TextBlock>This is regular text</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Grid Margin="20" Grid.Column="0" Row="1">
|
||||
<StackPanel Spacing="2">
|
||||
<Border Width="400" Classes="skeleton-text h1 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h2 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h3 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h4 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h5 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text h6 no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text no-margin"></Border>
|
||||
<Border Width="400" Classes="skeleton-text no-margin"></Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Spacing="2">
|
||||
<StackPanel.Styles>
|
||||
<Style Selector="TextBlock">
|
||||
<Setter Property="Background" Value="#55ff0000"></Setter>
|
||||
</Style>
|
||||
</StackPanel.Styles>
|
||||
<TextBlock Classes="h1 no-margin">This is heading 1</TextBlock>
|
||||
<TextBlock Classes="h2 no-margin">This is heading 2</TextBlock>
|
||||
<TextBlock Classes="h3 no-margin">This is heading 3</TextBlock>
|
||||
<TextBlock Classes="h4 no-margin">This is heading 4</TextBlock>
|
||||
<TextBlock Classes="h5 no-margin">This is heading 5</TextBlock>
|
||||
<TextBlock Classes="h6 no-margin">This is heading 6</TextBlock>
|
||||
<TextBlock>This is regular text</TextBlock>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Design.PreviewWith>
|
||||
|
||||
<Styles.Resources>
|
||||
<CornerRadius x:Key="CardCornerRadius">8</CornerRadius>
|
||||
</Styles.Resources>
|
||||
|
||||
<Style Selector="Border.skeleton-text">
|
||||
<Setter Property="Height" Value="17"></Setter>
|
||||
<Setter Property="Margin" Value="0 1" />
|
||||
<Setter Property="IsHitTestVisible" Value="False" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
|
||||
<Style.Animations>
|
||||
<Animation Duration="0:0:1.5" IterationCount="Infinite" PlaybackDirection="Normal">
|
||||
<KeyFrame Cue="0%">
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<LinearGradientBrush StartPoint="-100%,-100%" EndPoint="0%,0%">
|
||||
<LinearGradientBrush.GradientStops>
|
||||
<GradientStop Offset="0" Color="Gray" />
|
||||
<GradientStop Offset="0.4" Color="#595959" />
|
||||
<GradientStop Offset="0.6" Color="#595959" />
|
||||
<GradientStop Offset="1" Color="Gray" />
|
||||
</LinearGradientBrush.GradientStops>
|
||||
</LinearGradientBrush>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</KeyFrame>
|
||||
<KeyFrame Cue="100%">
|
||||
<Setter Property="Background">
|
||||
<Setter.Value>
|
||||
<LinearGradientBrush StartPoint="100%,100%" EndPoint="200%,200%">
|
||||
<LinearGradientBrush.GradientStops>
|
||||
<GradientStop Offset="0" Color="Gray" />
|
||||
<GradientStop Offset="0.4" Color="#595959" />
|
||||
<GradientStop Offset="0.6" Color="#595959" />
|
||||
<GradientStop Offset="1" Color="Gray" />
|
||||
</LinearGradientBrush.GradientStops>
|
||||
</LinearGradientBrush>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</KeyFrame>
|
||||
</Animation>
|
||||
</Style.Animations>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.skeleton-text.h1">
|
||||
<Setter Property="Height" Value="65" />
|
||||
<Setter Property="Margin" Value="0 10 0 20" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h2">
|
||||
<Setter Property="Height" Value="44" />
|
||||
<Setter Property="Margin" Value="0 10 0 20" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h3">
|
||||
<Setter Property="Height" Value="33" />
|
||||
<Setter Property="Margin" Value="0 5 0 15" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h4">
|
||||
<Setter Property="Height" Value="28" />
|
||||
<Setter Property="Margin" Value="0 2 0 12" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h5">
|
||||
<Setter Property="Height" Value="20" />
|
||||
<Setter Property="Margin" Value="0 2 0 7" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h6">
|
||||
<Setter Property="Height" Value="15" />
|
||||
<Setter Property="Margin" Value="0 2 0 4" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.skeleton-text.h1.no-margin">
|
||||
<Setter Property="Margin" Value="0 10 0 10" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h2.no-margin">
|
||||
<Setter Property="Margin" Value="0 10 0 10" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h3.no-margin">
|
||||
<Setter Property="Margin" Value="0 5 0 5" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h4.no-margin">
|
||||
<Setter Property="Margin" Value="0 2 0 2" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h5.no-margin">
|
||||
<Setter Property="Margin" Value="0 2 0 2" />
|
||||
</Style>
|
||||
<Style Selector="Border.skeleton-text.h6.no-margin">
|
||||
<Setter Property="Margin" Value="0 2 0 2" />
|
||||
</Style>
|
||||
|
||||
</Styles>
|
||||
@ -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!);
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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>
|
||||
|
||||
@ -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>();
|
||||
}
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
src/Artemis.UI.Windows/Scripts/update.ps1
Normal file
56
src/Artemis.UI.Windows/Scripts/update.ps1
Normal 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
|
||||
}
|
||||
@ -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>
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
}
|
||||
14
src/Artemis.UI/Extensions/CompositeDisposableExtensions.cs
Normal file
14
src/Artemis.UI/Extensions/CompositeDisposableExtensions.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
56
src/Artemis.UI/Extensions/HttpClientExtensions.cs
Normal file
56
src/Artemis.UI/Extensions/HttpClientExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
74
src/Artemis.UI/Extensions/ZipArchiveExtensions.cs
Normal file
74
src/Artemis.UI/Extensions/ZipArchiveExtensions.cs
Normal 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());
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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&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>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -2,10 +2,12 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Settings.SettingsView">
|
||||
x:Class="Artemis.UI.Screens.Settings.SettingsView"
|
||||
x:DataType="settings:SettingsViewModel">
|
||||
<Border Classes="router-container">
|
||||
<TabControl Margin="12" Items="{Binding SettingTabs}">
|
||||
<TabControl Margin="12" Items="{CompiledBinding SettingTabs}" SelectedItem="{CompiledBinding SelectedTab}">
|
||||
<TabControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding DisplayName}" />
|
||||
|
||||
@ -6,10 +6,13 @@ namespace Artemis.UI.Screens.Settings;
|
||||
|
||||
public class SettingsViewModel : MainScreenViewModel
|
||||
{
|
||||
private ActivatableViewModelBase _selectedTab;
|
||||
|
||||
public SettingsViewModel(IScreen hostScreen,
|
||||
GeneralTabViewModel generalTabViewModel,
|
||||
PluginsTabViewModel pluginsTabViewModel,
|
||||
DevicesTabViewModel devicesTabViewModel,
|
||||
ReleasesTabViewModel releasesTabViewModel,
|
||||
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
|
||||
{
|
||||
SettingTabs = new ObservableCollection<ActivatableViewModelBase>
|
||||
@ -17,9 +20,17 @@ public class SettingsViewModel : MainScreenViewModel
|
||||
generalTabViewModel,
|
||||
pluginsTabViewModel,
|
||||
devicesTabViewModel,
|
||||
releasesTabViewModel,
|
||||
aboutTabViewModel
|
||||
};
|
||||
_selectedTab = generalTabViewModel;
|
||||
}
|
||||
|
||||
public ObservableCollection<ActivatableViewModelBase> SettingTabs { get; }
|
||||
|
||||
public ActivatableViewModelBase SelectedTab
|
||||
{
|
||||
get => _selectedTab;
|
||||
set => RaiseAndSetIfChanged(ref _selectedTab, value);
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -137,7 +137,7 @@
|
||||
</Border>
|
||||
|
||||
<!-- Update settings -->
|
||||
<StackPanel IsVisible="{CompiledBinding IsUpdatingSupported}">
|
||||
<StackPanel>
|
||||
<TextBlock Classes="h4" Margin="0 15">
|
||||
Updating
|
||||
</TextBlock>
|
||||
|
||||
@ -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()
|
||||
|
||||
65
src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml
Normal file
65
src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml
Normal 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>
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
112
src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs
Normal file
112
src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
325
src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml
Normal file
325
src/Artemis.UI/Screens/Settings/Updating/ReleaseView.axaml
Normal 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>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
220
src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs
Normal file
220
src/Artemis.UI/Screens/Settings/Updating/ReleaseViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -68,7 +68,7 @@
|
||||
</StackPanel>
|
||||
|
||||
<!-- Update settings -->
|
||||
<StackPanel IsVisible="{CompiledBinding IsUpdatingSupported}">
|
||||
<StackPanel>
|
||||
<TextBlock Classes="h4" Margin="0 15">
|
||||
Updating
|
||||
</TextBlock>
|
||||
|
||||
@ -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();
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,9 @@
|
||||
using System;
|
||||
|
||||
namespace Artemis.UI.Services.Updating;
|
||||
|
||||
public interface IUpdateNotificationProvider
|
||||
{
|
||||
void ShowNotification(Guid releaseId, string releaseVersion);
|
||||
void ShowInstalledNotification(string installedVersion);
|
||||
}
|
||||
53
src/Artemis.UI/Services/Updating/IUpdateService.cs
Normal file
53
src/Artemis.UI/Services/Updating/IUpdateService.cs
Normal 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();
|
||||
}
|
||||
186
src/Artemis.UI/Services/Updating/ReleaseInstaller.cs
Normal file
186
src/Artemis.UI/Services/Updating/ReleaseInstaller.cs
Normal 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}\"");
|
||||
}
|
||||
}
|
||||
251
src/Artemis.UI/Services/Updating/UpdateService.cs
Normal file
251
src/Artemis.UI/Services/Updating/UpdateService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
191
src/Artemis.UI/Styles/Markdown.axaml
Normal file
191
src/Artemis.UI/Styles/Markdown.axaml
Normal 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>
|
||||
12
src/Artemis.WebClient.Updating/.config/dotnet-tools.json
Normal file
12
src/Artemis.WebClient.Updating/.config/dotnet-tools.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"strawberryshake.tools": {
|
||||
"version": "13.0.0-rc.4",
|
||||
"commands": [
|
||||
"dotnet-graphql"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/Artemis.WebClient.Updating/.graphqlconfig
Normal file
15
src/Artemis.WebClient.Updating/.graphqlconfig
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
22
src/Artemis.WebClient.Updating/.graphqlrc.json
Normal file
22
src/Artemis.WebClient.Updating/.graphqlrc.json
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
26
src/Artemis.WebClient.Updating/DryIoc/ContainerExtensions.cs
Normal file
26
src/Artemis.WebClient.Updating/DryIoc/ContainerExtensions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,6 @@
|
||||
query GetNextRelease($currentVersion: String, $branch: String!, $platform: Platform!) {
|
||||
nextPublishedRelease(version: $currentVersion, branch: $branch, platform: $platform) {
|
||||
id
|
||||
version
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
22
src/Artemis.WebClient.Updating/Queries/GetReleases.graphql
Normal file
22
src/Artemis.WebClient.Updating/Queries/GetReleases.graphql
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/Artemis.WebClient.Updating/schema.extensions.graphql
Normal file
13
src/Artemis.WebClient.Updating/schema.extensions.graphql
Normal 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")
|
||||
319
src/Artemis.WebClient.Updating/schema.graphql
Normal file
319
src/Artemis.WebClient.Updating/schema.graphql
Normal 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
|
||||
}
|
||||
@ -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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user