mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge pull request #749 from Artemis-RGB/feature/gh-actions
CI - Integrated with update backend UI - Reworked updating
This commit is contained in:
commit
3d3c7ebbfd
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"
|
$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 we're not in master, add the branch name to the version so it counts as prerelease
|
||||||
if ($BranchName -ne "master") { $VersionNumber += "-$BranchName" }
|
if ($BranchName -ne "master") { $VersionNumber += "-$BranchName" }
|
||||||
Write-Output "::set-output name=version-number::$VersionNumber"
|
"version-number=$VersionNumber" >> $Env:GITHUB_OUTPUT
|
||||||
|
|
||||||
nuget:
|
nuget:
|
||||||
name: Publish Nuget Packages
|
name: Publish Nuget Packages
|
||||||
|
|||||||
@ -48,6 +48,10 @@ public static class Constants
|
|||||||
/// The full path to the Artemis logs folder
|
/// The full path to the Artemis logs folder
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs");
|
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>
|
/// <summary>
|
||||||
/// The full path to the Artemis plugins folder
|
/// The full path to the Artemis plugins folder
|
||||||
@ -62,32 +66,24 @@ public static class Constants
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// The current API version for plugins
|
/// The current API version for plugins
|
||||||
/// </summary>
|
/// </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"));
|
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>
|
/// <summary>
|
||||||
/// The plugin info used by core components of Artemis
|
/// The plugin info used by core components of Artemis
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static readonly PluginInfo CorePluginInfo = new()
|
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>
|
/// <summary>
|
||||||
/// The plugin used by core components of Artemis
|
/// The plugin used by core components of Artemis
|
||||||
/// </summary>
|
/// </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 Plugin _plugin = null!;
|
||||||
private Uri? _repository;
|
private Uri? _repository;
|
||||||
private bool _requiresAdmin;
|
private bool _requiresAdmin;
|
||||||
private Version _version = null!;
|
private string _version = null!;
|
||||||
private Uri? _website;
|
private Uri? _website;
|
||||||
|
|
||||||
internal PluginInfo()
|
internal PluginInfo()
|
||||||
@ -107,7 +107,7 @@ public class PluginInfo : CorePropertyChanged, IPrerequisitesSubject
|
|||||||
/// The version of the plugin
|
/// The version of the plugin
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[JsonProperty(Required = Required.Always)]
|
[JsonProperty(Required = Required.Always)]
|
||||||
public Version Version
|
public string Version
|
||||||
{
|
{
|
||||||
get => _version;
|
get => _version;
|
||||||
internal set => SetAndNotify(ref _version, value);
|
internal set => SetAndNotify(ref _version, value);
|
||||||
|
|||||||
@ -200,23 +200,17 @@ internal class CoreService : ICoreService
|
|||||||
if (IsInitialized)
|
if (IsInitialized)
|
||||||
throw new ArtemisCoreException("Cannot initialize the core as it is already initialized.");
|
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 {CurrentVersion}", Constants.CurrentVersion);
|
||||||
_logger.Information(
|
_logger.Information("Startup arguments: {StartupArguments}", Constants.StartupArguments);
|
||||||
"Initializing Artemis Core version {version}, build {buildNumber} branch {branch}.",
|
_logger.Information("Elevated permissions: {IsElevated}", IsElevated);
|
||||||
versionAttribute?.InformationalVersion,
|
_logger.Information("Stopwatch high resolution: {IsHighResolution}", Stopwatch.IsHighResolution);
|
||||||
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);
|
|
||||||
|
|
||||||
ApplyLoggingLevel();
|
ApplyLoggingLevel();
|
||||||
|
|
||||||
// Don't remove even if it looks useless
|
// 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
|
// 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;
|
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
|
// Initialize the services
|
||||||
_pluginManagementService.CopyBuiltInPlugins();
|
_pluginManagementService.CopyBuiltInPlugins();
|
||||||
|
|||||||
@ -47,15 +47,29 @@ internal class PluginManagementService : IPluginManagementService
|
|||||||
|
|
||||||
private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory)
|
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));
|
DirectoryInfo pluginDirectory = new(Path.Combine(Constants.PluginsFolder, targetDirectory));
|
||||||
bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock"));
|
bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock"));
|
||||||
|
|
||||||
// Remove the old directory if it exists
|
// Remove the old directory if it exists
|
||||||
if (Directory.Exists(pluginDirectory.FullName))
|
if (Directory.Exists(pluginDirectory.FullName))
|
||||||
pluginDirectory.DeleteRecursively();
|
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)
|
if (createLockFile)
|
||||||
File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close();
|
File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close();
|
||||||
}
|
}
|
||||||
@ -82,57 +96,57 @@ internal class PluginManagementService : IPluginManagementService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
foreach (FileInfo zipFile in builtInPluginDirectory.EnumerateFiles("*.zip"))
|
foreach (FileInfo zipFile in builtInPluginDirectory.EnumerateFiles("*.zip"))
|
||||||
{
|
{
|
||||||
// Find the metadata file in the zip
|
try
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
|
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);
|
CopyBuiltInPlugin(archive, preferred);
|
||||||
}
|
}
|
||||||
else
|
else if (metaDataFileEntry.LastWriteTime > File.GetLastWriteTime(metadataFile))
|
||||||
{
|
{
|
||||||
string metadataFile = Path.Combine(match.FullName, "plugin.json");
|
try
|
||||||
if (!File.Exists(metadataFile))
|
|
||||||
{
|
{
|
||||||
_logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo);
|
_logger.Debug("Copying updated built-in plugin {builtInPluginInfo}", builtInPluginInfo);
|
||||||
CopyBuiltInPlugin(archive, preferred);
|
CopyBuiltInPlugin(archive, preferred);
|
||||||
}
|
}
|
||||||
else
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
PluginInfo pluginInfo;
|
throw new ArtemisPluginException($"Failed to install built-in plugin: {e.Message}", e);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -197,7 +211,7 @@ internal class PluginManagementService : IPluginManagementService
|
|||||||
// Disposal happens manually before container disposal but the container doesn't know that so a 2nd call will be made
|
// Disposal happens manually before container disposal but the container doesn't know that so a 2nd call will be made
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_disposed = true;
|
_disposed = true;
|
||||||
UnloadPlugins();
|
UnloadPlugins();
|
||||||
}
|
}
|
||||||
@ -556,7 +570,6 @@ internal class PluginManagementService : IPluginManagementService
|
|||||||
if (metaDataFileEntry == null)
|
if (metaDataFileEntry == null)
|
||||||
throw new ArtemisPluginException("Couldn't find a plugin.json in " + fileName);
|
throw new ArtemisPluginException("Couldn't find a plugin.json in " + fileName);
|
||||||
|
|
||||||
|
|
||||||
using StreamReader reader = new(metaDataFileEntry.Open());
|
using StreamReader reader = new(metaDataFileEntry.Open());
|
||||||
PluginInfo pluginInfo = CoreJson.DeserializeObject<PluginInfo>(reader.ReadToEnd())!;
|
PluginInfo pluginInfo = CoreJson.DeserializeObject<PluginInfo>(reader.ReadToEnd())!;
|
||||||
if (!pluginInfo.Main.EndsWith(".dll"))
|
if (!pluginInfo.Main.EndsWith(".dll"))
|
||||||
|
|||||||
@ -21,6 +21,7 @@ public static class Utilities
|
|||||||
CreateAccessibleDirectory(Constants.DataFolder);
|
CreateAccessibleDirectory(Constants.DataFolder);
|
||||||
CreateAccessibleDirectory(Constants.PluginsFolder);
|
CreateAccessibleDirectory(Constants.PluginsFolder);
|
||||||
CreateAccessibleDirectory(Constants.LayoutsFolder);
|
CreateAccessibleDirectory(Constants.LayoutsFolder);
|
||||||
|
CreateAccessibleDirectory(Constants.UpdatingFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -50,6 +51,15 @@ public static class Utilities
|
|||||||
OnRestartRequested(new RestartEventArgs(elevate, delay, extraArgs.ToList()));
|
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>
|
/// <summary>
|
||||||
/// Opens the provided URL in the default web browser
|
/// Opens the provided URL in the default web browser
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -96,11 +106,16 @@ public static class Utilities
|
|||||||
/// Occurs when the core has requested an application shutdown
|
/// Occurs when the core has requested an application shutdown
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static event EventHandler? ShutdownRequested;
|
public static event EventHandler? ShutdownRequested;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Occurs when the core has requested an application restart
|
/// Occurs when the core has requested an application restart
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static event EventHandler<RestartEventArgs>? RestartRequested;
|
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>
|
/// <summary>
|
||||||
/// Opens the provided folder in the user's file explorer
|
/// Opens the provided folder in the user's file explorer
|
||||||
@ -136,6 +151,11 @@ public static class Utilities
|
|||||||
{
|
{
|
||||||
ShutdownRequested?.Invoke(null, EventArgs.Empty);
|
ShutdownRequested?.Invoke(null, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void OnUpdateRequested(UpdateEventArgs e)
|
||||||
|
{
|
||||||
|
UpdateRequested?.Invoke(null, e);
|
||||||
|
}
|
||||||
|
|
||||||
#region Scaling
|
#region Scaling
|
||||||
|
|
||||||
|
|||||||
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);
|
void Remove(QueuedActionEntity queuedActionEntity);
|
||||||
List<QueuedActionEntity> GetAll();
|
List<QueuedActionEntity> GetAll();
|
||||||
List<QueuedActionEntity> GetByType(string type);
|
List<QueuedActionEntity> GetByType(string type);
|
||||||
|
bool IsTypeQueued(string type);
|
||||||
|
void ClearByType(string type);
|
||||||
}
|
}
|
||||||
@ -41,5 +41,17 @@ public class QueuedActionRepository : IQueuedActionRepository
|
|||||||
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).ToList();
|
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsTypeQueued(string type)
|
||||||
|
{
|
||||||
|
return _repository.Query<QueuedActionEntity>().Where(q => q.Type == type).Count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void ClearByType(string type)
|
||||||
|
{
|
||||||
|
_repository.DeleteMany<QueuedActionEntity>(q => q.Type == type);
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
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 newest = files.OrderByDescending(fi => fi.CreationTime).First();
|
||||||
FileSystemInfo oldest = files.OrderBy(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;
|
return;
|
||||||
|
|
||||||
oldest.Delete();
|
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>
|
/// </summary>
|
||||||
/// <param name="looseMatch">Whether the type may be a loose match, meaning it can be cast or converted</param>
|
/// <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>
|
/// <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 != null)
|
||||||
{
|
filteredTypes = filteredTypes.All(t => t == null) ? null : filteredTypes.Where(t => t != null).ToArray();
|
||||||
if (filteredTypes.All(t => t == null))
|
|
||||||
filteredTypes = null;
|
|
||||||
else
|
|
||||||
filteredTypes = filteredTypes.Where(t => t != null).ToArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the VM has children, its own type is not relevant
|
// If the VM has children, its own type is not relevant
|
||||||
if (Children.Any())
|
if (Children.Any())
|
||||||
{
|
{
|
||||||
foreach (DataModelVisualizationViewModel child in Children)
|
foreach (DataModelVisualizationViewModel child in Children)
|
||||||
child?.ApplyTypeFilter(looseMatch, filteredTypes);
|
child.ApplyTypeFilter(looseMatch, filteredTypes);
|
||||||
|
|
||||||
IsMatchingFilteredTypes = true;
|
IsMatchingFilteredTypes = true;
|
||||||
return;
|
return;
|
||||||
@ -199,7 +194,7 @@ public abstract class DataModelVisualizationViewModel : ReactiveObject, IDisposa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (looseMatch)
|
if (looseMatch)
|
||||||
IsMatchingFilteredTypes = filteredTypes.Any(t => t.IsCastableFrom(type) ||
|
IsMatchingFilteredTypes = filteredTypes.Any(t => t!.IsCastableFrom(type) ||
|
||||||
(t == typeof(Enum) && type.IsEnum) ||
|
(t == typeof(Enum) && type.IsEnum) ||
|
||||||
(t == typeof(IEnumerable<>) && type.IsGenericEnumerable()) ||
|
(t == typeof(IEnumerable<>) && type.IsGenericEnumerable()) ||
|
||||||
(type.IsGenericType && t == type.GetGenericTypeDefinition()));
|
(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))
|
foreach (PropertyInfo propertyInfo in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance).OrderBy(t => t.MetadataToken))
|
||||||
{
|
{
|
||||||
string childPath = AppendToPath(propertyInfo.Name);
|
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;
|
continue;
|
||||||
if (propertyInfo.GetCustomAttribute<DataModelIgnoreAttribute>() != null)
|
if (propertyInfo.GetCustomAttribute<DataModelIgnoreAttribute>() != null)
|
||||||
continue;
|
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 System;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Services.MainWindow;
|
namespace Artemis.UI.Shared.Services.MainWindow;
|
||||||
|
|
||||||
@ -12,6 +13,11 @@ public interface IMainWindowService : IArtemisSharedUIService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
bool IsMainWindowOpen { get; }
|
bool IsMainWindowOpen { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the host screen contained in the main window
|
||||||
|
/// </summary>
|
||||||
|
IScreen? HostScreen { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets up the main window provider that controls the state of the main window
|
/// Sets up the main window provider that controls the state of the main window
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Services.MainWindow;
|
namespace Artemis.UI.Shared.Services.MainWindow;
|
||||||
|
|
||||||
@ -6,6 +7,12 @@ internal class MainWindowService : IMainWindowService
|
|||||||
{
|
{
|
||||||
private IMainWindowProvider? _mainWindowManager;
|
private IMainWindowProvider? _mainWindowManager;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsMainWindowOpen { get; private set; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public IScreen? HostScreen { get; set; }
|
||||||
|
|
||||||
protected virtual void OnMainWindowOpened()
|
protected virtual void OnMainWindowOpened()
|
||||||
{
|
{
|
||||||
MainWindowOpened?.Invoke(this, EventArgs.Empty);
|
MainWindowOpened?.Invoke(this, EventArgs.Empty);
|
||||||
@ -64,8 +71,6 @@ internal class MainWindowService : IMainWindowService
|
|||||||
OnMainWindowUnfocused();
|
OnMainWindowUnfocused();
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsMainWindowOpen { get; private set; }
|
|
||||||
|
|
||||||
public void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider)
|
public void ConfigureMainWindowProvider(IMainWindowProvider mainWindowProvider)
|
||||||
{
|
{
|
||||||
if (mainWindowProvider == null) throw new ArgumentNullException(nameof(mainWindowProvider));
|
if (mainWindowProvider == null) throw new ArgumentNullException(nameof(mainWindowProvider));
|
||||||
|
|||||||
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
<!-- Custom styles -->
|
<!-- Custom styles -->
|
||||||
<StyleInclude Source="/Styles/Border.axaml" />
|
<StyleInclude Source="/Styles/Border.axaml" />
|
||||||
|
<StyleInclude Source="/Styles/Skeleton.axaml" />
|
||||||
<StyleInclude Source="/Styles/Button.axaml" />
|
<StyleInclude Source="/Styles/Button.axaml" />
|
||||||
<StyleInclude Source="/Styles/Condensed.axaml" />
|
<StyleInclude Source="/Styles/Condensed.axaml" />
|
||||||
<StyleInclude Source="/Styles/ColorPickerButton.axaml" />
|
<StyleInclude Source="/Styles/ColorPickerButton.axaml" />
|
||||||
|
|||||||
174
src/Artemis.UI.Shared/Styles/Skeleton.axaml
Normal file
174
src/Artemis.UI.Shared/Styles/Skeleton.axaml
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Design.PreviewWith>
|
||||||
|
<Grid ColumnDefinitions="*,*">
|
||||||
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition />
|
||||||
|
<RowDefinition />
|
||||||
|
</Grid.RowDefinitions>
|
||||||
|
<Grid Margin="20" Grid.Column="0">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Classes="h1">This is heading 1</TextBlock>
|
||||||
|
<TextBlock Classes="h2">This is heading 2</TextBlock>
|
||||||
|
<TextBlock Classes="h3">This is heading 3</TextBlock>
|
||||||
|
<TextBlock Classes="h4">This is heading 4</TextBlock>
|
||||||
|
<TextBlock Classes="h5">This is heading 5</TextBlock>
|
||||||
|
<TextBlock Classes="h6">This is heading 6</TextBlock>
|
||||||
|
<TextBlock>This is regular text</TextBlock>
|
||||||
|
<TextBlock>This is regular text</TextBlock>
|
||||||
|
<TextBlock>This is regular text</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Margin="20" Grid.Column="1">
|
||||||
|
<StackPanel>
|
||||||
|
<Border Width="400" Classes="skeleton-text h1"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text h2"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text h3"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text h4"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text h5"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text h6"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text"></Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel.Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="Background" Value="#55ff0000"></Setter>
|
||||||
|
</Style>
|
||||||
|
</StackPanel.Styles>
|
||||||
|
<TextBlock Classes="h1">This is heading 1</TextBlock>
|
||||||
|
<TextBlock Classes="h2">This is heading 2</TextBlock>
|
||||||
|
<TextBlock Classes="h3">This is heading 3</TextBlock>
|
||||||
|
<TextBlock Classes="h4">This is heading 4</TextBlock>
|
||||||
|
<TextBlock Classes="h5">This is heading 5</TextBlock>
|
||||||
|
<TextBlock Classes="h6">This is heading 6</TextBlock>
|
||||||
|
<TextBlock>This is regular text</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid Margin="20" Grid.Column="0" Row="1">
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<Border Width="400" Classes="skeleton-text h1 no-margin"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text h2 no-margin"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text h3 no-margin"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text h4 no-margin"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text h5 no-margin"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text h6 no-margin"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text no-margin"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text no-margin"></Border>
|
||||||
|
<Border Width="400" Classes="skeleton-text no-margin"></Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<StackPanel.Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="Background" Value="#55ff0000"></Setter>
|
||||||
|
</Style>
|
||||||
|
</StackPanel.Styles>
|
||||||
|
<TextBlock Classes="h1 no-margin">This is heading 1</TextBlock>
|
||||||
|
<TextBlock Classes="h2 no-margin">This is heading 2</TextBlock>
|
||||||
|
<TextBlock Classes="h3 no-margin">This is heading 3</TextBlock>
|
||||||
|
<TextBlock Classes="h4 no-margin">This is heading 4</TextBlock>
|
||||||
|
<TextBlock Classes="h5 no-margin">This is heading 5</TextBlock>
|
||||||
|
<TextBlock Classes="h6 no-margin">This is heading 6</TextBlock>
|
||||||
|
<TextBlock>This is regular text</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Design.PreviewWith>
|
||||||
|
|
||||||
|
<Styles.Resources>
|
||||||
|
<CornerRadius x:Key="CardCornerRadius">8</CornerRadius>
|
||||||
|
</Styles.Resources>
|
||||||
|
|
||||||
|
<Style Selector="Border.skeleton-text">
|
||||||
|
<Setter Property="Height" Value="17"></Setter>
|
||||||
|
<Setter Property="Margin" Value="0 1" />
|
||||||
|
<Setter Property="IsHitTestVisible" Value="False" />
|
||||||
|
<Setter Property="CornerRadius" Value="6" />
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||||
|
|
||||||
|
<Style.Animations>
|
||||||
|
<Animation Duration="0:0:1.5" IterationCount="Infinite" PlaybackDirection="Normal">
|
||||||
|
<KeyFrame Cue="0%">
|
||||||
|
<Setter Property="Background">
|
||||||
|
<Setter.Value>
|
||||||
|
<LinearGradientBrush StartPoint="-100%,-100%" EndPoint="0%,0%">
|
||||||
|
<LinearGradientBrush.GradientStops>
|
||||||
|
<GradientStop Offset="0" Color="Gray" />
|
||||||
|
<GradientStop Offset="0.4" Color="#595959" />
|
||||||
|
<GradientStop Offset="0.6" Color="#595959" />
|
||||||
|
<GradientStop Offset="1" Color="Gray" />
|
||||||
|
</LinearGradientBrush.GradientStops>
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</KeyFrame>
|
||||||
|
<KeyFrame Cue="100%">
|
||||||
|
<Setter Property="Background">
|
||||||
|
<Setter.Value>
|
||||||
|
<LinearGradientBrush StartPoint="100%,100%" EndPoint="200%,200%">
|
||||||
|
<LinearGradientBrush.GradientStops>
|
||||||
|
<GradientStop Offset="0" Color="Gray" />
|
||||||
|
<GradientStop Offset="0.4" Color="#595959" />
|
||||||
|
<GradientStop Offset="0.6" Color="#595959" />
|
||||||
|
<GradientStop Offset="1" Color="Gray" />
|
||||||
|
</LinearGradientBrush.GradientStops>
|
||||||
|
</LinearGradientBrush>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</KeyFrame>
|
||||||
|
</Animation>
|
||||||
|
</Style.Animations>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Border.skeleton-text.h1">
|
||||||
|
<Setter Property="Height" Value="65" />
|
||||||
|
<Setter Property="Margin" Value="0 10 0 20" />
|
||||||
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.skeleton-text.h2">
|
||||||
|
<Setter Property="Height" Value="44" />
|
||||||
|
<Setter Property="Margin" Value="0 10 0 20" />
|
||||||
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.skeleton-text.h3">
|
||||||
|
<Setter Property="Height" Value="33" />
|
||||||
|
<Setter Property="Margin" Value="0 5 0 15" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.skeleton-text.h4">
|
||||||
|
<Setter Property="Height" Value="28" />
|
||||||
|
<Setter Property="Margin" Value="0 2 0 12" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.skeleton-text.h5">
|
||||||
|
<Setter Property="Height" Value="20" />
|
||||||
|
<Setter Property="Margin" Value="0 2 0 7" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.skeleton-text.h6">
|
||||||
|
<Setter Property="Height" Value="15" />
|
||||||
|
<Setter Property="Margin" Value="0 2 0 4" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Border.skeleton-text.h1.no-margin">
|
||||||
|
<Setter Property="Margin" Value="0 10 0 10" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.skeleton-text.h2.no-margin">
|
||||||
|
<Setter Property="Margin" Value="0 10 0 10" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.skeleton-text.h3.no-margin">
|
||||||
|
<Setter Property="Margin" Value="0 5 0 5" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.skeleton-text.h4.no-margin">
|
||||||
|
<Setter Property="Margin" Value="0 2 0 2" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.skeleton-text.h5.no-margin">
|
||||||
|
<Setter Property="Margin" Value="0 2 0 2" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.skeleton-text.h6.no-margin">
|
||||||
|
<Setter Property="Margin" Value="0 2 0 2" />
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
</Styles>
|
||||||
@ -43,9 +43,9 @@ public class App : Application
|
|||||||
{
|
{
|
||||||
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || Design.IsDesignMode || _shutDown)
|
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop || Design.IsDesignMode || _shutDown)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
ArtemisBootstrapper.Initialize();
|
|
||||||
_applicationStateManager = new ApplicationStateManager(_container!, desktop.Args);
|
_applicationStateManager = new ApplicationStateManager(_container!, desktop.Args);
|
||||||
|
ArtemisBootstrapper.Initialize();
|
||||||
RegisterProviders(_container!);
|
RegisterProviders(_container!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,7 @@ namespace Artemis.UI.Windows;
|
|||||||
public class ApplicationStateManager
|
public class ApplicationStateManager
|
||||||
{
|
{
|
||||||
private const int SM_SHUTTINGDOWN = 0x2000;
|
private const int SM_SHUTTINGDOWN = 0x2000;
|
||||||
|
|
||||||
public ApplicationStateManager(IContainer container, string[] startupArguments)
|
public ApplicationStateManager(IContainer container, string[] startupArguments)
|
||||||
{
|
{
|
||||||
StartupArguments = startupArguments;
|
StartupArguments = startupArguments;
|
||||||
@ -25,6 +25,7 @@ public class ApplicationStateManager
|
|||||||
|
|
||||||
Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested;
|
Core.Utilities.ShutdownRequested += UtilitiesOnShutdownRequested;
|
||||||
Core.Utilities.RestartRequested += UtilitiesOnRestartRequested;
|
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
|
// On Windows shutdown dispose the IOC container just so device providers get a chance to clean up
|
||||||
if (Application.Current?.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime)
|
if (Application.Current?.ApplicationLifetime is IControlledApplicationLifetime controlledApplicationLifetime)
|
||||||
@ -91,6 +92,25 @@ public class ApplicationStateManager
|
|||||||
Dispatcher.UIThread.Post(() => controlledApplicationLifetime.Shutdown());
|
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)
|
private void UtilitiesOnShutdownRequested(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
// Use PowerShell to kill the process after 8 sec just in case
|
// Use PowerShell to kill the process after 8 sec just in case
|
||||||
@ -115,7 +135,21 @@ public class ApplicationStateManager
|
|||||||
};
|
};
|
||||||
Process.Start(info);
|
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")]
|
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||||
private static extern int GetSystemMetrics(int nIndex);
|
private static extern int GetSystemMetrics(int nIndex);
|
||||||
}
|
}
|
||||||
@ -12,20 +12,10 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
<None Remove=".gitignore" />
|
|
||||||
<None Remove="Assets\Cursors\aero_crosshair.cur" />
|
<Content Include="Scripts\**">
|
||||||
<None Remove="Assets\Cursors\aero_crosshair_minus.cur" />
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
<None Remove="Assets\Cursors\aero_crosshair_plus.cur" />
|
</Content>
|
||||||
<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" />
|
|
||||||
<None Include="..\Artemis.UI\Assets\Images\Logo\application.ico">
|
<None Include="..\Artemis.UI\Assets\Images\Logo\application.ico">
|
||||||
<Link>application.ico</Link>
|
<Link>application.ico</Link>
|
||||||
</None>
|
</None>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using Artemis.Core.Providers;
|
using Artemis.Core.Providers;
|
||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
|
using Artemis.UI.Services.Updating;
|
||||||
using Artemis.UI.Shared.Providers;
|
using Artemis.UI.Shared.Providers;
|
||||||
using Artemis.UI.Windows.Providers;
|
using Artemis.UI.Windows.Providers;
|
||||||
using Artemis.UI.Windows.Providers.Input;
|
using Artemis.UI.Windows.Providers.Input;
|
||||||
@ -20,8 +21,8 @@ public static class UIContainerExtensions
|
|||||||
{
|
{
|
||||||
container.Register<ICursorProvider, CursorProvider>(Reuse.Singleton);
|
container.Register<ICursorProvider, CursorProvider>(Reuse.Singleton);
|
||||||
container.Register<IGraphicsContextProvider, GraphicsContextProvider>(Reuse.Singleton);
|
container.Register<IGraphicsContextProvider, GraphicsContextProvider>(Reuse.Singleton);
|
||||||
container.Register<IUpdateProvider, UpdateProvider>(Reuse.Singleton);
|
|
||||||
container.Register<IAutoRunProvider, AutoRunProvider>();
|
container.Register<IAutoRunProvider, AutoRunProvider>();
|
||||||
container.Register<InputProvider, WindowsInputProvider>(serviceKey: WindowsInputProvider.Id);
|
container.Register<InputProvider, WindowsInputProvider>(serviceKey: WindowsInputProvider.Id);
|
||||||
|
container.Register<IUpdateNotificationProvider, WindowsUpdateNotificationProvider>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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 />
|
/// <inheritdoc />
|
||||||
public async Task EnableAutoRun(bool recreate, int autoRunDelay)
|
public async Task EnableAutoRun(bool recreate, int autoRunDelay)
|
||||||
{
|
{
|
||||||
if (Constants.BuildInfo.IsLocalBuild)
|
if (Constants.CurrentVersion == "development")
|
||||||
return;
|
return;
|
||||||
|
|
||||||
await CleanupOldAutorun();
|
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.Core\Artemis.Core.csproj" />
|
||||||
<ProjectReference Include="..\Artemis.UI.Shared\Artemis.UI.Shared.csproj" />
|
<ProjectReference Include="..\Artemis.UI.Shared\Artemis.UI.Shared.csproj" />
|
||||||
<ProjectReference Include="..\Artemis.VisualScripting\Artemis.VisualScripting.csproj" />
|
<ProjectReference Include="..\Artemis.VisualScripting\Artemis.VisualScripting.csproj" />
|
||||||
|
<ProjectReference Include="..\Artemis.WebClient.Updating\Artemis.WebClient.Updating.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
@ -28,7 +29,9 @@
|
|||||||
<PackageReference Include="FluentAvaloniaUI" Version="1.4.1" />
|
<PackageReference Include="FluentAvaloniaUI" Version="1.4.1" />
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||||
<PackageReference Include="Live.Avalonia" Version="1.3.1" />
|
<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="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" Version="17.1.50" />
|
||||||
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
<PackageReference Include="ReactiveUI.Validation" Version="2.2.1" />
|
||||||
<PackageReference Include="RGB.NET.Core" Version="2.0.0-prerelease.17" />
|
<PackageReference Include="RGB.NET.Core" Version="2.0.0-prerelease.17" />
|
||||||
@ -40,4 +43,15 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="Screens\Settings\Tabs\ReleasesTabView.axaml.cs">
|
||||||
|
<DependentUpon>UpdatingTabView.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="Screens\Settings\Updating\ReleaseView.axaml.cs">
|
||||||
|
<DependentUpon>UpdatingTabView.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@ -2,7 +2,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.Core.DryIoc;
|
using Artemis.Core.DryIoc;
|
||||||
using Artemis.UI.DryIoc;
|
using Artemis.UI.DryIoc;
|
||||||
@ -12,6 +11,7 @@ using Artemis.UI.Shared.DataModelPicker;
|
|||||||
using Artemis.UI.Shared.DryIoc;
|
using Artemis.UI.Shared.DryIoc;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.VisualScripting.DryIoc;
|
using Artemis.VisualScripting.DryIoc;
|
||||||
|
using Artemis.WebClient.Updating.DryIoc;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
@ -42,6 +42,7 @@ public static class ArtemisBootstrapper
|
|||||||
_container.RegisterCore();
|
_container.RegisterCore();
|
||||||
_container.RegisterUI();
|
_container.RegisterUI();
|
||||||
_container.RegisterSharedUI();
|
_container.RegisterSharedUI();
|
||||||
|
_container.RegisterUpdatingClient();
|
||||||
_container.RegisterNoStringEvaluating();
|
_container.RegisterNoStringEvaluating();
|
||||||
configureServices?.Invoke(_container);
|
configureServices?.Invoke(_container);
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,8 @@
|
|||||||
Width="200"
|
Width="200"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Items="{CompiledBinding Descriptors}"
|
Items="{CompiledBinding Descriptors}"
|
||||||
SelectedItem="{CompiledBinding SelectedDescriptor}">
|
SelectedItem="{CompiledBinding SelectedDescriptor}"
|
||||||
|
PlaceholderText="Please select a brush">
|
||||||
<ComboBox.ItemTemplate>
|
<ComboBox.ItemTemplate>
|
||||||
<DataTemplate DataType="{x:Type layerBrushes:LayerBrushDescriptor}">
|
<DataTemplate DataType="{x:Type layerBrushes:LayerBrushDescriptor}">
|
||||||
<Grid ColumnDefinitions="30,*" RowDefinitions="Auto,Auto">
|
<Grid ColumnDefinitions="30,*" RowDefinitions="Auto,Auto">
|
||||||
|
|||||||
@ -59,7 +59,7 @@ public class BrushPropertyInputViewModel : PropertyInputViewModel<LayerBrushRefe
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
protected override void ApplyInputValue()
|
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;
|
return;
|
||||||
|
|
||||||
_profileEditorService.ExecuteCommand(new ChangeLayerBrush(layer, SelectedDescriptor));
|
_profileEditorService.ExecuteCommand(new ChangeLayerBrush(layer, SelectedDescriptor));
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using Artemis.UI.DryIoc.InstanceProviders;
|
|||||||
using Artemis.UI.Screens;
|
using Artemis.UI.Screens;
|
||||||
using Artemis.UI.Screens.VisualScripting;
|
using Artemis.UI.Screens.VisualScripting;
|
||||||
using Artemis.UI.Services.Interfaces;
|
using Artemis.UI.Services.Interfaces;
|
||||||
|
using Artemis.UI.Services.Updating;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services.NodeEditor;
|
using Artemis.UI.Shared.Services.NodeEditor;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
@ -30,13 +31,13 @@ public static class ContainerExtensions
|
|||||||
container.Register<IAssetLoader, AssetLoader>(Reuse.Singleton);
|
container.Register<IAssetLoader, AssetLoader>(Reuse.Singleton);
|
||||||
|
|
||||||
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<ViewModelBase>());
|
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<ViewModelBase>());
|
||||||
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<MainScreenViewModel>(), ifAlreadyRegistered: IfAlreadyRegistered.Replace);
|
|
||||||
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IToolViewModel>() && type.IsInterface);
|
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IToolViewModel>() && type.IsInterface);
|
||||||
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IVmFactory>() && type != typeof(PropertyVmFactory));
|
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IVmFactory>() && type != typeof(PropertyVmFactory));
|
||||||
|
|
||||||
container.Register<NodeScriptWindowViewModelBase, NodeScriptWindowViewModel>(Reuse.Singleton);
|
container.Register<NodeScriptWindowViewModelBase, NodeScriptWindowViewModel>(Reuse.Singleton);
|
||||||
container.Register<IPropertyVmFactory, PropertyVmFactory>(Reuse.Singleton);
|
container.Register<IPropertyVmFactory, PropertyVmFactory>(Reuse.Singleton);
|
||||||
|
container.Register<IUpdateNotificationProvider, BasicUpdateNotificationProvider>();
|
||||||
|
|
||||||
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IArtemisUIService>(), Reuse.Singleton);
|
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IArtemisUIService>(), Reuse.Singleton);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,4 +1,5 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.Core.LayerBrushes;
|
using Artemis.Core.LayerBrushes;
|
||||||
@ -17,6 +18,7 @@ using Artemis.UI.Screens.ProfileEditor.Properties.Tree;
|
|||||||
using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers;
|
using Artemis.UI.Screens.ProfileEditor.VisualEditor.Visualizers;
|
||||||
using Artemis.UI.Screens.Scripting;
|
using Artemis.UI.Screens.Scripting;
|
||||||
using Artemis.UI.Screens.Settings;
|
using Artemis.UI.Screens.Settings;
|
||||||
|
using Artemis.UI.Screens.Settings.Updating;
|
||||||
using Artemis.UI.Screens.Sidebar;
|
using Artemis.UI.Screens.Sidebar;
|
||||||
using Artemis.UI.Screens.SurfaceEditor;
|
using Artemis.UI.Screens.SurfaceEditor;
|
||||||
using Artemis.UI.Screens.VisualScripting;
|
using Artemis.UI.Screens.VisualScripting;
|
||||||
@ -474,4 +476,23 @@ public class ScriptVmFactory : IScriptVmFactory
|
|||||||
{
|
{
|
||||||
return _container.Resolve<ScriptConfigurationViewModel>(new object[] { profile, scriptConfiguration });
|
return _container.Resolve<ScriptConfigurationViewModel>(new object[] { profile, scriptConfiguration });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IReleaseVmFactory : IVmFactory
|
||||||
|
{
|
||||||
|
ReleaseViewModel ReleaseListViewModel(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.
|
//we need this help distance because of rounding.
|
||||||
//if we scroll slightly above the end, we still want it
|
//if we scroll slightly above the end, we still want it
|
||||||
//to scroll down to the new lines.
|
//to scroll down to the new lines.
|
||||||
const double graceDistance = 1d;
|
const double GRACE_DISTANCE = 1d;
|
||||||
|
|
||||||
//if we were at the bottom of the log and
|
//if we were at the bottom of the log and
|
||||||
//if the last log event was 5 lines long
|
//if the last log event was 5 lines long
|
||||||
@ -59,7 +59,7 @@ public class LogsDebugView : ReactiveUserControl<LogsDebugViewModel>
|
|||||||
//if we are more than that out of sync,
|
//if we are more than that out of sync,
|
||||||
//the user scrolled up and we should not
|
//the user scrolled up and we should not
|
||||||
//mess with anything.
|
//mess with anything.
|
||||||
if (_lineCount == 0 || linesAdded + graceDistance > outOfScreenLines)
|
if (_lineCount == 0 || linesAdded + GRACE_DISTANCE > outOfScreenLines)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Post(() => _textEditor.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
|
Dispatcher.UIThread.Post(() => _textEditor.ScrollToEnd(), DispatcherPriority.ApplicationIdle);
|
||||||
_lineCount = _textEditor.LineCount;
|
_lineCount = _textEditor.LineCount;
|
||||||
|
|||||||
@ -109,7 +109,7 @@
|
|||||||
<controls:HyperlinkButton Grid.Row="1"
|
<controls:HyperlinkButton Grid.Row="1"
|
||||||
Grid.Column="0"
|
Grid.Column="0"
|
||||||
HorizontalAlignment="Center"
|
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">
|
<StackPanel Orientation="Horizontal">
|
||||||
<avalonia:MaterialIcon Kind="Gift" />
|
<avalonia:MaterialIcon Kind="Gift" />
|
||||||
<TextBlock Margin="8 0 0 0" VerticalAlignment="Center">Donate</TextBlock>
|
<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.Models;
|
||||||
using Artemis.UI.Screens.Sidebar;
|
using Artemis.UI.Screens.Sidebar;
|
||||||
using Artemis.UI.Services.Interfaces;
|
using Artemis.UI.Services.Interfaces;
|
||||||
|
using Artemis.UI.Services.Updating;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.UI.Shared.Services.MainWindow;
|
using Artemis.UI.Shared.Services.MainWindow;
|
||||||
@ -45,7 +46,7 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
|||||||
{
|
{
|
||||||
Router = new RoutingState();
|
Router = new RoutingState();
|
||||||
WindowSizeSetting = settingsService.GetSetting<WindowSize?>("WindowSize");
|
WindowSizeSetting = settingsService.GetSetting<WindowSize?>("WindowSize");
|
||||||
|
|
||||||
_coreService = coreService;
|
_coreService = coreService;
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
@ -55,13 +56,17 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
|||||||
_defaultTitleBarViewModel = defaultTitleBarViewModel;
|
_defaultTitleBarViewModel = defaultTitleBarViewModel;
|
||||||
_sidebarVmFactory = sidebarVmFactory;
|
_sidebarVmFactory = sidebarVmFactory;
|
||||||
_lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!;
|
_lifeTime = (IClassicDesktopStyleApplicationLifetime) Application.Current!.ApplicationLifetime!;
|
||||||
|
|
||||||
mainWindowService.ConfigureMainWindowProvider(this);
|
mainWindowService.ConfigureMainWindowProvider(this);
|
||||||
|
mainWindowService.HostScreen = this;
|
||||||
|
|
||||||
DisplayAccordingToSettings();
|
DisplayAccordingToSettings();
|
||||||
Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel);
|
Router.CurrentViewModel.Subscribe(UpdateTitleBarViewModel);
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
|
if (_updateService.Initialize())
|
||||||
|
return;
|
||||||
|
|
||||||
coreService.Initialize();
|
coreService.Initialize();
|
||||||
registrationService.RegisterBuiltInDataModelDisplays();
|
registrationService.RegisterBuiltInDataModelDisplays();
|
||||||
registrationService.RegisterBuiltInDataModelInputs();
|
registrationService.RegisterBuiltInDataModelInputs();
|
||||||
@ -105,14 +110,10 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
|||||||
bool showOnAutoRun = _settingsService.GetSetting("UI.ShowOnStartup", true).Value;
|
bool showOnAutoRun = _settingsService.GetSetting("UI.ShowOnStartup", true).Value;
|
||||||
|
|
||||||
if ((autoRunning && !showOnAutoRun) || minimized)
|
if ((autoRunning && !showOnAutoRun) || minimized)
|
||||||
{
|
return;
|
||||||
// TODO: Auto-update
|
|
||||||
}
|
ShowSplashScreen();
|
||||||
else
|
_coreService.Initialized += (_, _) => Dispatcher.UIThread.InvokeAsync(OpenMainWindow);
|
||||||
{
|
|
||||||
ShowSplashScreen();
|
|
||||||
_coreService.Initialized += (_, _) => Dispatcher.UIThread.InvokeAsync(OpenMainWindow);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ShowSplashScreen()
|
private void ShowSplashScreen()
|
||||||
@ -229,11 +230,6 @@ public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvi
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public void SaveWindowBounds(int x, int y, int width, int height)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class EmptyViewModel : MainScreenViewModel
|
internal class EmptyViewModel : MainScreenViewModel
|
||||||
|
|||||||
@ -2,10 +2,12 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.Settings.SettingsView">
|
x:Class="Artemis.UI.Screens.Settings.SettingsView"
|
||||||
|
x:DataType="settings:SettingsViewModel">
|
||||||
<Border Classes="router-container">
|
<Border Classes="router-container">
|
||||||
<TabControl Margin="12" Items="{Binding SettingTabs}">
|
<TabControl Margin="12" Items="{CompiledBinding SettingTabs}" SelectedItem="{CompiledBinding SelectedTab}">
|
||||||
<TabControl.ItemTemplate>
|
<TabControl.ItemTemplate>
|
||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<TextBlock Text="{Binding DisplayName}" />
|
<TextBlock Text="{Binding DisplayName}" />
|
||||||
|
|||||||
@ -6,10 +6,13 @@ namespace Artemis.UI.Screens.Settings;
|
|||||||
|
|
||||||
public class SettingsViewModel : MainScreenViewModel
|
public class SettingsViewModel : MainScreenViewModel
|
||||||
{
|
{
|
||||||
|
private ActivatableViewModelBase _selectedTab;
|
||||||
|
|
||||||
public SettingsViewModel(IScreen hostScreen,
|
public SettingsViewModel(IScreen hostScreen,
|
||||||
GeneralTabViewModel generalTabViewModel,
|
GeneralTabViewModel generalTabViewModel,
|
||||||
PluginsTabViewModel pluginsTabViewModel,
|
PluginsTabViewModel pluginsTabViewModel,
|
||||||
DevicesTabViewModel devicesTabViewModel,
|
DevicesTabViewModel devicesTabViewModel,
|
||||||
|
ReleasesTabViewModel releasesTabViewModel,
|
||||||
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
|
AboutTabViewModel aboutTabViewModel) : base(hostScreen, "settings")
|
||||||
{
|
{
|
||||||
SettingTabs = new ObservableCollection<ActivatableViewModelBase>
|
SettingTabs = new ObservableCollection<ActivatableViewModelBase>
|
||||||
@ -17,9 +20,17 @@ public class SettingsViewModel : MainScreenViewModel
|
|||||||
generalTabViewModel,
|
generalTabViewModel,
|
||||||
pluginsTabViewModel,
|
pluginsTabViewModel,
|
||||||
devicesTabViewModel,
|
devicesTabViewModel,
|
||||||
|
releasesTabViewModel,
|
||||||
aboutTabViewModel
|
aboutTabViewModel
|
||||||
};
|
};
|
||||||
|
_selectedTab = generalTabViewModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<ActivatableViewModelBase> SettingTabs { get; }
|
public ObservableCollection<ActivatableViewModelBase> SettingTabs { get; }
|
||||||
|
|
||||||
|
public ActivatableViewModelBase SelectedTab
|
||||||
|
{
|
||||||
|
get => _selectedTab;
|
||||||
|
set => RaiseAndSetIfChanged(ref _selectedTab, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -57,7 +57,7 @@ public class AboutTabViewModel : ActivatableViewModelBase
|
|||||||
private async Task Activate()
|
private async Task Activate()
|
||||||
{
|
{
|
||||||
AssemblyInformationalVersionAttribute? versionAttribute = typeof(AboutTabViewModel).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
|
AssemblyInformationalVersionAttribute? versionAttribute = typeof(AboutTabViewModel).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
|
||||||
Version = $"Version {versionAttribute?.InformationalVersion} build {Constants.BuildInfo.BuildNumberDisplay}";
|
Version = $"Version {Constants.CurrentVersion}";
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@ -137,7 +137,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Update settings -->
|
<!-- Update settings -->
|
||||||
<StackPanel IsVisible="{CompiledBinding IsUpdatingSupported}">
|
<StackPanel>
|
||||||
<TextBlock Classes="h4" Margin="0 15">
|
<TextBlock Classes="h4" Margin="0 15">
|
||||||
Updating
|
Updating
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
|||||||
@ -12,9 +12,11 @@ using Artemis.Core.Providers;
|
|||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Screens.StartupWizard;
|
using Artemis.UI.Screens.StartupWizard;
|
||||||
using Artemis.UI.Services.Interfaces;
|
using Artemis.UI.Services.Interfaces;
|
||||||
|
using Artemis.UI.Services.Updating;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Providers;
|
using Artemis.UI.Shared.Providers;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
|
using Artemis.UI.Shared.Services.Builders;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using DryIoc;
|
using DryIoc;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
@ -30,6 +32,7 @@ public class GeneralTabViewModel : ActivatableViewModelBase
|
|||||||
private readonly PluginSetting<LayerBrushReference> _defaultLayerBrushDescriptor;
|
private readonly PluginSetting<LayerBrushReference> _defaultLayerBrushDescriptor;
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly IUpdateService _updateService;
|
private readonly IUpdateService _updateService;
|
||||||
|
private readonly INotificationService _notificationService;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
private bool _startupWizardOpen;
|
private bool _startupWizardOpen;
|
||||||
|
|
||||||
@ -38,13 +41,15 @@ public class GeneralTabViewModel : ActivatableViewModelBase
|
|||||||
IPluginManagementService pluginManagementService,
|
IPluginManagementService pluginManagementService,
|
||||||
IDebugService debugService,
|
IDebugService debugService,
|
||||||
IWindowService windowService,
|
IWindowService windowService,
|
||||||
IUpdateService updateService)
|
IUpdateService updateService,
|
||||||
|
INotificationService notificationService)
|
||||||
{
|
{
|
||||||
DisplayName = "General";
|
DisplayName = "General";
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_debugService = debugService;
|
_debugService = debugService;
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
_updateService = updateService;
|
_updateService = updateService;
|
||||||
|
_notificationService = notificationService;
|
||||||
_autoRunProvider = container.Resolve<IAutoRunProvider>(IfUnresolved.ReturnDefault);
|
_autoRunProvider = container.Resolve<IAutoRunProvider>(IfUnresolved.ReturnDefault);
|
||||||
|
|
||||||
List<LayerBrushProvider> layerBrushProviders = pluginManagementService.GetFeaturesOfType<LayerBrushProvider>();
|
List<LayerBrushProvider> layerBrushProviders = pluginManagementService.GetFeaturesOfType<LayerBrushProvider>();
|
||||||
@ -88,7 +93,6 @@ public class GeneralTabViewModel : ActivatableViewModelBase
|
|||||||
public ReactiveCommand<Unit, Unit> ShowDataFolder { get; }
|
public ReactiveCommand<Unit, Unit> ShowDataFolder { get; }
|
||||||
|
|
||||||
public bool IsAutoRunSupported => _autoRunProvider != null;
|
public bool IsAutoRunSupported => _autoRunProvider != null;
|
||||||
public bool IsUpdatingSupported => _updateService.UpdatingSupported;
|
|
||||||
|
|
||||||
public ObservableCollection<LayerBrushDescriptor> LayerBrushDescriptors { get; }
|
public ObservableCollection<LayerBrushDescriptor> LayerBrushDescriptors { get; }
|
||||||
public ObservableCollection<string> GraphicsContexts { 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<bool> UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false);
|
||||||
public PluginSetting<int> UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15);
|
public PluginSetting<int> UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15);
|
||||||
public PluginSetting<bool> UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true);
|
public PluginSetting<bool> UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true);
|
||||||
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.CheckForUpdates", true);
|
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true);
|
||||||
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.AutoUpdate", false);
|
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true);
|
||||||
public PluginSetting<bool> ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
|
public PluginSetting<bool> ProfileEditorShowDataModelValues => _settingsService.GetSetting("ProfileEditor.ShowDataModelValues", false);
|
||||||
public PluginSetting<LogEventLevel> CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information);
|
public PluginSetting<LogEventLevel> CoreLoggingLevel => _settingsService.GetSetting("Core.LoggingLevel", LogEventLevel.Information);
|
||||||
public PluginSetting<string> CorePreferredGraphicsContext => _settingsService.GetSetting("Core.PreferredGraphicsContext", "Software");
|
public PluginSetting<string> CorePreferredGraphicsContext => _settingsService.GetSetting("Core.PreferredGraphicsContext", "Software");
|
||||||
@ -159,7 +163,25 @@ public class GeneralTabViewModel : ActivatableViewModelBase
|
|||||||
|
|
||||||
private async Task ExecuteCheckForUpdate(CancellationToken cancellationToken)
|
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()
|
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)
|
private void NavigateToScreen(SidebarScreenViewModel sidebarScreenViewModel)
|
||||||
{
|
{
|
||||||
|
// If the current screen changed through external means and already matches, don't navigate again
|
||||||
|
if (_hostScreen.Router.GetCurrentViewModel()?.GetType() == sidebarScreenViewModel.ScreenType)
|
||||||
|
return;
|
||||||
|
|
||||||
_hostScreen.Router.Navigate.Execute(sidebarScreenViewModel.CreateInstance(_container, _hostScreen));
|
_hostScreen.Router.Navigate.Execute(sidebarScreenViewModel.CreateInstance(_container, _hostScreen));
|
||||||
_profileEditorService.ChangeCurrentProfileConfiguration(null);
|
_profileEditorService.ChangeCurrentProfileConfiguration(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ using Artemis.Core.Services;
|
|||||||
using Artemis.UI.DryIoc.Factories;
|
using Artemis.UI.DryIoc.Factories;
|
||||||
using Artemis.UI.Screens.Plugins;
|
using Artemis.UI.Screens.Plugins;
|
||||||
using Artemis.UI.Services.Interfaces;
|
using Artemis.UI.Services.Interfaces;
|
||||||
|
using Artemis.UI.Services.Updating;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Providers;
|
using Artemis.UI.Shared.Providers;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
@ -24,29 +25,29 @@ public class StartupWizardViewModel : DialogViewModelBase<bool>
|
|||||||
private readonly IAutoRunProvider? _autoRunProvider;
|
private readonly IAutoRunProvider? _autoRunProvider;
|
||||||
private readonly IRgbService _rgbService;
|
private readonly IRgbService _rgbService;
|
||||||
private readonly ISettingsService _settingsService;
|
private readonly ISettingsService _settingsService;
|
||||||
private readonly IUpdateService _updateService;
|
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
private int _currentStep;
|
private int _currentStep;
|
||||||
private bool _showContinue;
|
private bool _showContinue;
|
||||||
private bool _showFinish;
|
private bool _showFinish;
|
||||||
private bool _showGoBack;
|
private bool _showGoBack;
|
||||||
|
|
||||||
public StartupWizardViewModel(IContainer container, ISettingsService settingsService, IRgbService rgbService, IPluginManagementService pluginManagementService, IWindowService windowService,
|
public StartupWizardViewModel(IContainer container,
|
||||||
IUpdateService updateService, ISettingsVmFactory settingsVmFactory)
|
ISettingsService settingsService,
|
||||||
|
IRgbService rgbService,
|
||||||
|
IPluginManagementService pluginManagementService,
|
||||||
|
IWindowService windowService,
|
||||||
|
ISettingsVmFactory settingsVmFactory)
|
||||||
{
|
{
|
||||||
_settingsService = settingsService;
|
_settingsService = settingsService;
|
||||||
_rgbService = rgbService;
|
_rgbService = rgbService;
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
_updateService = updateService;
|
|
||||||
_autoRunProvider = container.Resolve<IAutoRunProvider>(IfUnresolved.ReturnDefault);
|
_autoRunProvider = container.Resolve<IAutoRunProvider>(IfUnresolved.ReturnDefault);
|
||||||
|
|
||||||
Continue = ReactiveCommand.Create(ExecuteContinue);
|
Continue = ReactiveCommand.Create(ExecuteContinue);
|
||||||
GoBack = ReactiveCommand.Create(ExecuteGoBack);
|
GoBack = ReactiveCommand.Create(ExecuteGoBack);
|
||||||
SkipOrFinishWizard = ReactiveCommand.Create(ExecuteSkipOrFinishWizard);
|
SkipOrFinishWizard = ReactiveCommand.Create(ExecuteSkipOrFinishWizard);
|
||||||
SelectLayout = ReactiveCommand.Create<string>(ExecuteSelectLayout);
|
SelectLayout = ReactiveCommand.Create<string>(ExecuteSelectLayout);
|
||||||
|
Version = $"Version {Constants.CurrentVersion}";
|
||||||
AssemblyInformationalVersionAttribute? versionAttribute = typeof(StartupWizardViewModel).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
|
|
||||||
Version = $"Version {versionAttribute?.InformationalVersion} build {Constants.BuildInfo.BuildNumberDisplay}";
|
|
||||||
|
|
||||||
// Take all compatible plugins that have an always-enabled device provider
|
// Take all compatible plugins that have an always-enabled device provider
|
||||||
DeviceProviders = new ObservableCollection<PluginViewModel>(pluginManagementService.GetAllPlugins()
|
DeviceProviders = new ObservableCollection<PluginViewModel>(pluginManagementService.GetAllPlugins()
|
||||||
@ -81,13 +82,12 @@ public class StartupWizardViewModel : DialogViewModelBase<bool>
|
|||||||
public ObservableCollection<PluginViewModel> DeviceProviders { get; }
|
public ObservableCollection<PluginViewModel> DeviceProviders { get; }
|
||||||
|
|
||||||
public bool IsAutoRunSupported => _autoRunProvider != null;
|
public bool IsAutoRunSupported => _autoRunProvider != null;
|
||||||
public bool IsUpdatingSupported => _updateService.UpdatingSupported;
|
|
||||||
|
|
||||||
public PluginSetting<bool> UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false);
|
public PluginSetting<bool> UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false);
|
||||||
public PluginSetting<int> UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15);
|
public PluginSetting<int> UIAutoRunDelay => _settingsService.GetSetting("UI.AutoRunDelay", 15);
|
||||||
public PluginSetting<bool> UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true);
|
public PluginSetting<bool> UIShowOnStartup => _settingsService.GetSetting("UI.ShowOnStartup", true);
|
||||||
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.CheckForUpdates", true);
|
public PluginSetting<bool> UICheckForUpdates => _settingsService.GetSetting("UI.Updating.AutoCheck", true);
|
||||||
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.AutoUpdate", false);
|
public PluginSetting<bool> UIAutoUpdate => _settingsService.GetSetting("UI.Updating.AutoInstall", true);
|
||||||
|
|
||||||
public int CurrentStep
|
public int CurrentStep
|
||||||
{
|
{
|
||||||
@ -119,7 +119,7 @@ public class StartupWizardViewModel : DialogViewModelBase<bool>
|
|||||||
CurrentStep--;
|
CurrentStep--;
|
||||||
|
|
||||||
// Skip the settings step if none of it's contents are supported
|
// Skip the settings step if none of it's contents are supported
|
||||||
if (CurrentStep == 4 && !IsAutoRunSupported && !IsUpdatingSupported)
|
if (CurrentStep == 4 && !IsAutoRunSupported)
|
||||||
CurrentStep--;
|
CurrentStep--;
|
||||||
|
|
||||||
SetupButtons();
|
SetupButtons();
|
||||||
@ -131,7 +131,7 @@ public class StartupWizardViewModel : DialogViewModelBase<bool>
|
|||||||
CurrentStep++;
|
CurrentStep++;
|
||||||
|
|
||||||
// Skip the settings step if none of it's contents are supported
|
// Skip the settings step if none of it's contents are supported
|
||||||
if (CurrentStep == 4 && !IsAutoRunSupported && !IsUpdatingSupported)
|
if (CurrentStep == 4 && !IsAutoRunSupported)
|
||||||
CurrentStep++;
|
CurrentStep++;
|
||||||
|
|
||||||
SetupButtons();
|
SetupButtons();
|
||||||
|
|||||||
@ -68,7 +68,7 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Update settings -->
|
<!-- Update settings -->
|
||||||
<StackPanel IsVisible="{CompiledBinding IsUpdatingSupported}">
|
<StackPanel>
|
||||||
<TextBlock Classes="h4" Margin="0 15">
|
<TextBlock Classes="h4" Margin="0 15">
|
||||||
Updating
|
Updating
|
||||||
</TextBlock>
|
</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> -->
|
<!-- <FluentTheme Mode="Dark"></FluentTheme> -->
|
||||||
<StyleInclude Source="avares://Material.Icons.Avalonia/App.xaml" />
|
<StyleInclude Source="avares://Material.Icons.Avalonia/App.xaml" />
|
||||||
<StyleInclude Source="avares://Artemis.UI.Shared/Styles/Artemis.axaml" />
|
<StyleInclude Source="avares://Artemis.UI.Shared/Styles/Artemis.axaml" />
|
||||||
|
<StyleInclude Source="avares://Artemis.UI/Styles/Markdown.axaml" />
|
||||||
</Styles>
|
</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
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.VisualScripting", "Artemis.VisualScripting\Artemis.VisualScripting.csproj", "{412B921A-26F5-4AE6-8B32-0C19BE54F421}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Artemis.VisualScripting", "Artemis.VisualScripting\Artemis.VisualScripting.csproj", "{412B921A-26F5-4AE6-8B32-0C19BE54F421}"
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.WebClient.Updating", "Artemis.WebClient.Updating\Artemis.WebClient.Updating.csproj", "{7C8C6F50-0CC8-45B3-B608-A7218C005E4B}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|x64 = Debug|x64
|
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}.Debug|x64.Build.0 = Debug|x64
|
||||||
{412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|x64.ActiveCfg = Release|x64
|
{412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|x64.ActiveCfg = Release|x64
|
||||||
{412B921A-26F5-4AE6-8B32-0C19BE54F421}.Release|x64.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user