diff --git a/src/Artemis.Core/Artemis.Core.csproj.DotSettings b/src/Artemis.Core/Artemis.Core.csproj.DotSettings index 13a2166e3..a18836e5e 100644 --- a/src/Artemis.Core/Artemis.Core.csproj.DotSettings +++ b/src/Artemis.Core/Artemis.Core.csproj.DotSettings @@ -67,6 +67,7 @@ True True True + True True True True diff --git a/src/Artemis.Core/Providers/Interfaces/IGraphicsContextProvider.cs b/src/Artemis.Core/Providers/Interfaces/IGraphicsContextProvider.cs new file mode 100644 index 000000000..6173cd599 --- /dev/null +++ b/src/Artemis.Core/Providers/Interfaces/IGraphicsContextProvider.cs @@ -0,0 +1,23 @@ +using Artemis.Core.SkiaSharp; + +namespace Artemis.Core.Providers; + +/// +/// Represents a class that can provide one or more graphics instances by name. +/// +public interface IGraphicsContextProvider +{ + /// + /// Gets the name of the graphics context. + /// + string GraphicsContextName { get; } + + /// + /// Creates an instance of the managed graphics context this provider provides. + /// + /// + /// An instance of the resulting managed graphics context if successfully created; otherwise + /// . + /// + IManagedGraphicsContext? GetGraphicsContext(); +} \ No newline at end of file diff --git a/src/Artemis.Core/Services/Interfaces/IGraphicsContextProvider.cs b/src/Artemis.Core/Services/Interfaces/IGraphicsContextProvider.cs deleted file mode 100644 index 88a7dd917..000000000 --- a/src/Artemis.Core/Services/Interfaces/IGraphicsContextProvider.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Collections.Generic; -using Artemis.Core.SkiaSharp; - -namespace Artemis.Core.Services; - -/// -/// Represents a class that can provide one or more graphics instances by name. -/// -public interface IGraphicsContextProvider -{ - /// - /// Gets a read only collection containing the names of all the graphics contexts supported by this provider. - /// - IReadOnlyCollection GraphicsContextNames { get; } - - /// - /// Gets a managed graphics context by name. - /// - /// The name of the graphics context. - /// If found, an instance of the managed graphics context with the given ; otherwise . - IManagedGraphicsContext? GetGraphicsContext(string name); -} \ No newline at end of file diff --git a/src/Artemis.Core/Services/RgbService.cs b/src/Artemis.Core/Services/RgbService.cs index d8af2526b..d671a0537 100644 --- a/src/Artemis.Core/Services/RgbService.cs +++ b/src/Artemis.Core/Services/RgbService.cs @@ -4,6 +4,7 @@ using System.Collections.ObjectModel; using System.Linq; using System.Threading; using Artemis.Core.DeviceProviders; +using Artemis.Core.Providers; using Artemis.Core.Services.Models; using Artemis.Core.SkiaSharp; using Artemis.Storage.Entities.Surface; @@ -363,15 +364,15 @@ namespace Artemis.Core.Services return; } - IGraphicsContextProvider? provider = _kernel.TryGet(); - if (provider == null) + List providers = _kernel.Get>(); + if (!providers.Any()) { _logger.Warning("No graphics context provider found, defaulting to software rendering"); UpdateGraphicsContext(null); return; } - IManagedGraphicsContext? context = provider.GetGraphicsContext(_preferredGraphicsContext.Value); + IManagedGraphicsContext? context = providers.FirstOrDefault(p => p.GraphicsContextName == _preferredGraphicsContext.Value)?.GetGraphicsContext(); if (context == null) { _logger.Warning("No graphics context named '{Context}' found, defaulting to software rendering", _preferredGraphicsContext.Value); diff --git a/src/Artemis.UI.Linux/Artemis.UI.Linux.csproj b/src/Artemis.UI.Linux/Artemis.UI.Linux.csproj index 285bbd78d..ecf05251d 100644 --- a/src/Artemis.UI.Linux/Artemis.UI.Linux.csproj +++ b/src/Artemis.UI.Linux/Artemis.UI.Linux.csproj @@ -5,21 +5,24 @@ enable x64 x64 + bin + False + Artemis 2 - - + + - - + + - - - + + + - - + + \ No newline at end of file diff --git a/src/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj b/src/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj index 285bbd78d..ecf05251d 100644 --- a/src/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj +++ b/src/Artemis.UI.MacOS/Artemis.UI.MacOS.csproj @@ -5,21 +5,24 @@ enable x64 x64 + bin + False + Artemis 2 - - + + - - + + - - - + + + - - + + \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Providers/IAutoRunProvider.cs b/src/Artemis.UI.Shared/Providers/IAutoRunProvider.cs new file mode 100644 index 000000000..a648b4a2b --- /dev/null +++ b/src/Artemis.UI.Shared/Providers/IAutoRunProvider.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; + +namespace Artemis.UI.Shared.Providers; + +/// +/// Represents a provider for custom cursors. +/// +public interface IAutoRunProvider +{ + /// + /// Asynchronously enables auto-run. + /// + /// A boolean indicating whether the auto-run configuration should be recreated (the auto run delay changed) + /// The delay in seconds before the application should start (if supported) + Task EnableAutoRun(bool recreate, int autoRunDelay); + + /// + /// Asynchronously disables auto-run. + /// + Task DisableAutoRun(); +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Providers/IUpdateProvider.cs b/src/Artemis.UI.Shared/Providers/IUpdateProvider.cs new file mode 100644 index 000000000..1ec987c28 --- /dev/null +++ b/src/Artemis.UI.Shared/Providers/IUpdateProvider.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; + +namespace Artemis.UI.Shared.Providers; + +/// +/// Represents a provider for custom cursors. +/// +public interface IUpdateProvider +{ + /// + /// Asynchronously checks whether an update is available. + /// + /// The channel to use when checking updates (i.e. master or development) + /// A task returning if an update is available; otherwise . + Task CheckForUpdate(string channel); + + /// + /// Applies any available updates. + /// + /// The channel to use when checking updates (i.e. master or development) + /// Whether or not to update silently. + Task ApplyUpdate(string channel, bool silent); + + /// + /// Offer to install the update to the user. + /// + /// The channel to use when checking updates (i.e. master or development) + /// A boolean indicating whether the main window is open. + /// A task returning if the user chose to update; otherwise . + Task OfferUpdate(string channel, bool windowOpen); +} \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Services/Window/ExceptionDialogView.axaml b/src/Artemis.UI.Shared/Services/Window/ExceptionDialogView.axaml index a1979dfdb..c4c3c9350 100644 --- a/src/Artemis.UI.Shared/Services/Window/ExceptionDialogView.axaml +++ b/src/Artemis.UI.Shared/Services/Window/ExceptionDialogView.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" x:Class="Artemis.UI.Shared.Services.ExceptionDialogView" - Icon="/Assets/Images/Logo/application.ico" Title="{Binding Title}" ExtendClientAreaToDecorationsHint="True" Width="800" diff --git a/src/Artemis.UI.Windows/App.axaml.cs b/src/Artemis.UI.Windows/App.axaml.cs index 1f2e213d2..46e09a283 100644 --- a/src/Artemis.UI.Windows/App.axaml.cs +++ b/src/Artemis.UI.Windows/App.axaml.cs @@ -1,5 +1,6 @@ using Artemis.Core.Services; using Artemis.UI.Windows.Ninject; +using Artemis.UI.Windows.Providers; using Artemis.UI.Windows.Providers.Input; using Avalonia; using Avalonia.Controls; diff --git a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj index abf8d3029..050ed0025 100644 --- a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -1,42 +1,52 @@  WinExe - net6.0-windows + net6.0-windows10.0.17763.0 enable x64 x64 + bin + False + Artemis RGB + Artemis 2.0 + ..\Artemis.UI\Assets\Images\Logo\application.ico + Artemis 2 - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + application.ico + - - + + - - - - - - - + + + + + + + + - - + + diff --git a/src/Artemis.UI.Windows/Assets/autorun.xml b/src/Artemis.UI.Windows/Assets/autorun.xml new file mode 100644 index 000000000..e31980c8b Binary files /dev/null and b/src/Artemis.UI.Windows/Assets/autorun.xml differ diff --git a/src/Artemis.UI.Windows/Assets/avalonia-logo.ico b/src/Artemis.UI.Windows/Assets/avalonia-logo.ico deleted file mode 100644 index da8d49ff9..000000000 Binary files a/src/Artemis.UI.Windows/Assets/avalonia-logo.ico and /dev/null differ diff --git a/src/Artemis.UI.Windows/Models/DevOpsBuilds.cs b/src/Artemis.UI.Windows/Models/DevOpsBuilds.cs new file mode 100644 index 000000000..8c48f7574 --- /dev/null +++ b/src/Artemis.UI.Windows/Models/DevOpsBuilds.cs @@ -0,0 +1,279 @@ +#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 Builds { get; set; } + } + + public class DevOpsBuild + { + [JsonProperty("_links")] + public BuildLinks Links { get; set; } + + [JsonProperty("properties")] + public Properties Properties { get; set; } + + [JsonProperty("tags")] + public List Tags { get; set; } + + [JsonProperty("validationResults")] + public List ValidationResults { get; set; } + + [JsonProperty("plans")] + public List 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 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; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Models/GitHubDifference.cs b/src/Artemis.UI.Windows/Models/GitHubDifference.cs new file mode 100644 index 000000000..a50adb533 --- /dev/null +++ b/src/Artemis.UI.Windows/Models/GitHubDifference.cs @@ -0,0 +1,244 @@ +#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 Commits { get; set; } + + [JsonProperty("files")] + public List 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 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; } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Ninject/WindowsModule.cs b/src/Artemis.UI.Windows/Ninject/WindowsModule.cs index 2badd961e..9722b6871 100644 --- a/src/Artemis.UI.Windows/Ninject/WindowsModule.cs +++ b/src/Artemis.UI.Windows/Ninject/WindowsModule.cs @@ -1,4 +1,5 @@ -using Artemis.Core.Services; +using Artemis.Core.Providers; +using Artemis.Core.Services; using Artemis.UI.Shared.Providers; using Artemis.UI.Windows.Providers; using Ninject.Modules; @@ -14,6 +15,8 @@ public class WindowsModule : NinjectModule { Kernel!.Bind().To().InSingletonScope(); Kernel!.Bind().To().InSingletonScope(); + Kernel!.Bind().To().InSingletonScope(); + Kernel!.Bind().To(); } #endregion diff --git a/src/Artemis.UI.Windows/Providers/AutoRunProvider.cs b/src/Artemis.UI.Windows/Providers/AutoRunProvider.cs new file mode 100644 index 000000000..6ead1404e --- /dev/null +++ b/src/Artemis.UI.Windows/Providers/AutoRunProvider.cs @@ -0,0 +1,126 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Security.Principal; +using System.Threading; +using System.Threading.Tasks; +using System.Xml.Linq; +using Artemis.Core; +using Artemis.UI.Shared.Providers; +using Avalonia.Platform; + +namespace Artemis.UI.Windows.Providers; + +public class AutoRunProvider : IAutoRunProvider +{ + private readonly IAssetLoader _assetLoader; + + public AutoRunProvider(IAssetLoader assetLoader) + { + _assetLoader = assetLoader; + } + + private async Task IsAutoRunTaskCreated() + { + Process schtasks = new() + { + StartInfo = + { + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = true, + FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"), + Arguments = "/TN \"Artemis 2 autorun\"" + } + }; + + schtasks.Start(); + await schtasks.WaitForExitAsync(); + return schtasks.ExitCode == 0; + } + + private async Task CreateAutoRunTask(TimeSpan autoRunDelay) + { + await using Stream taskFile = _assetLoader.Open(new Uri("avares://Artemis 2/Assets/autorun.xml")); + + XDocument document = await XDocument.LoadAsync(taskFile, LoadOptions.None, CancellationToken.None); + XElement task = document.Descendants().First(); + + task.Descendants().First(d => d.Name.LocalName == "RegistrationInfo").Descendants().First(d => d.Name.LocalName == "Date") + .SetValue(DateTime.Now); + task.Descendants().First(d => d.Name.LocalName == "RegistrationInfo").Descendants().First(d => d.Name.LocalName == "Author") + .SetValue(WindowsIdentity.GetCurrent().Name); + + task.Descendants().First(d => d.Name.LocalName == "Triggers").Descendants().First(d => d.Name.LocalName == "LogonTrigger").Descendants().First(d => d.Name.LocalName == "Delay") + .SetValue(autoRunDelay); + + task.Descendants().First(d => d.Name.LocalName == "Principals").Descendants().First(d => d.Name.LocalName == "Principal").Descendants().First(d => d.Name.LocalName == "UserId") + .SetValue(WindowsIdentity.GetCurrent().User!.Value); + + task.Descendants().First(d => d.Name.LocalName == "Actions").Descendants().First(d => d.Name.LocalName == "Exec").Descendants().First(d => d.Name.LocalName == "WorkingDirectory") + .SetValue(Constants.ApplicationFolder); + task.Descendants().First(d => d.Name.LocalName == "Actions").Descendants().First(d => d.Name.LocalName == "Exec").Descendants().First(d => d.Name.LocalName == "Command") + .SetValue("\"" + Constants.ExecutablePath + "\""); + + string xmlPath = Path.GetTempFileName(); + await using (Stream fileStream = new FileStream(xmlPath, FileMode.Create)) + await document.SaveAsync(fileStream, SaveOptions.None, CancellationToken.None); + + Process schtasks = new() + { + StartInfo = + { + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = true, + Verb = "runas", + FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"), + Arguments = $"/Create /XML \"{xmlPath}\" /tn \"Artemis 2 autorun\" /F" + } + }; + + schtasks.Start(); + await schtasks.WaitForExitAsync(); + + File.Delete(xmlPath); + } + + private async Task RemoveAutoRunTask() + { + Process schtasks = new() + { + StartInfo = + { + WindowStyle = ProcessWindowStyle.Hidden, + UseShellExecute = true, + Verb = "runas", + FileName = Path.Combine(Environment.SystemDirectory, "schtasks.exe"), + Arguments = "/Delete /TN \"Artemis 2 autorun\" /f" + } + }; + + schtasks.Start(); + await schtasks.WaitForExitAsync(); + } + + /// + public async Task EnableAutoRun(bool recreate, int autoRunDelay) + { + // if (Constants.BuildInfo.IsLocalBuild) + // return; + + // Create or remove the task if necessary + bool taskCreated = false; + if (!recreate) + taskCreated = await IsAutoRunTaskCreated(); + if (!taskCreated) + await CreateAutoRunTask(TimeSpan.FromSeconds(autoRunDelay)); + } + + /// + public async Task DisableAutoRun() + { + bool taskCreated = await IsAutoRunTaskCreated(); + if (taskCreated) + await RemoveAutoRunTask(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Providers/CursorProvider.cs b/src/Artemis.UI.Windows/Providers/CursorProvider.cs index 97a53f032..f6a2fce09 100644 --- a/src/Artemis.UI.Windows/Providers/CursorProvider.cs +++ b/src/Artemis.UI.Windows/Providers/CursorProvider.cs @@ -11,9 +11,9 @@ public class CursorProvider : ICursorProvider { public CursorProvider(IAssetLoader assetLoader) { - Rotate = new Cursor(new Bitmap(assetLoader.Open(new Uri("avares://Artemis.UI.Windows/Assets/Cursors/aero_rotate.png"))), new PixelPoint(21, 10)); - Drag = new Cursor(new Bitmap(assetLoader.Open(new Uri("avares://Artemis.UI.Windows/Assets/Cursors/aero_drag.png"))), new PixelPoint(11, 3)); - DragHorizontal = new Cursor(new Bitmap(assetLoader.Open(new Uri("avares://Artemis.UI.Windows/Assets/Cursors/aero_drag_horizontal.png"))), new PixelPoint(16, 5)); + Rotate = new Cursor(new Bitmap(assetLoader.Open(new Uri("avares://Artemis 2/Assets/Cursors/aero_rotate.png"))), new PixelPoint(21, 10)); + Drag = new Cursor(new Bitmap(assetLoader.Open(new Uri("avares://Artemis 2/Assets/Cursors/aero_drag.png"))), new PixelPoint(11, 3)); + DragHorizontal = new Cursor(new Bitmap(assetLoader.Open(new Uri("avares://Artemis 2/Assets/Cursors/aero_drag_horizontal.png"))), new PixelPoint(16, 5)); } public Cursor Rotate { get; } diff --git a/src/Artemis.UI.Windows/Providers/GraphicsContextProvider.cs b/src/Artemis.UI.Windows/Providers/GraphicsContextProvider.cs index a06d146e6..2ef17cc75 100644 --- a/src/Artemis.UI.Windows/Providers/GraphicsContextProvider.cs +++ b/src/Artemis.UI.Windows/Providers/GraphicsContextProvider.cs @@ -1,5 +1,4 @@ -using System.Collections.Generic; -using Artemis.Core.Services; +using Artemis.Core.Providers; using Artemis.Core.SkiaSharp; using Artemis.UI.Windows.SkiaSharp; @@ -9,18 +8,11 @@ public class GraphicsContextProvider : IGraphicsContextProvider { private VulkanContext? _vulkanContext; - /// - public IReadOnlyCollection GraphicsContextNames => new List {"Vulkan"}.AsReadOnly(); + public string GraphicsContextName => "Vulkan"; - /// - public IManagedGraphicsContext? GetGraphicsContext(string name) + public IManagedGraphicsContext? GetGraphicsContext() { - if (name == "Vulkan") - { - _vulkanContext ??= new VulkanContext(); - return _vulkanContext; - } - - return null; + _vulkanContext ??= new VulkanContext(); + return _vulkanContext; } } \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Providers/UpdateProvider.cs b/src/Artemis.UI.Windows/Providers/UpdateProvider.cs new file mode 100644 index 000000000..c38b736cf --- /dev/null +++ b/src/Artemis.UI.Windows/Providers/UpdateProvider.cs @@ -0,0 +1,216 @@ +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.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 ApiUrl = "https://dev.azure.com/artemis-rgb/Artemis/_apis/"; + private const string InstallerUrl = "https://builds.artemis-rgb.com/binaries/Artemis.Installer.exe"; + + private readonly ILogger _logger; + private readonly IWindowService _windowService; + private readonly IMainWindowService _mainWindowService; + + public UpdateProvider(ILogger logger, IWindowService windowService, IMainWindowService mainWindowService) + { + _logger = logger; + _windowService = windowService; + _mainWindowService = mainWindowService; + + ToastNotificationManagerCompat.OnActivated += ToastNotificationManagerCompatOnOnActivated; + } + + 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(Core.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(Core.Constants.DataFolder, "installer"); + string installerPath = Path.Combine(installerDirectory, "Artemis.Installer.exe"); + + _logger.Information("UpdateInstaller: Downloading installer from {DownloadUrl}", InstallerUrl); + using HttpClient client = new(); + HttpResponseMessage httpResponseMessage = await client.GetAsync(InstallerUrl); + 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); + } + + /// + public async Task 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, Core.Constants.BuildInfo.BuildNumberDisplay); + + return buildNumber > Core.Constants.BuildInfo.BuildNumber; + } + + /// + public async Task ApplyUpdate(string channel, bool silent) + { + await RunInstaller(channel, silent); + } + + /// + public async Task OfferUpdate(string channel, bool windowOpen) + { + if (windowOpen) + { + bool update = await _windowService.ShowDialogAsync(("channel", channel)); + if (update) + await RunInstaller(channel, false); + } + else + { + ShowDesktopNotification(channel); + } + } + + 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")) + .AddHeroImage(new Uri(@"C:\Repos\Artemis\src\Artemis.UI\Assets\Images\home-banner.png")) + .Show(); + } + + public async Task GetBuildInfo(int buildDefinition, string? buildNumber = null) + { + Url request = ApiUrl.AppendPathSegments("build", "builds") + .SetQueryParam("definitions", buildDefinition) + .SetQueryParam("resultFilter", "succeeded") + .SetQueryParam("$top", 1) + .SetQueryParam("api-version", "6.1-preview.6"); + + if (buildNumber != null) + request = request.SetQueryParam("buildNumber", buildNumber); + + try + { + DevOpsBuilds result = await request.GetJsonAsync(); + 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 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(); + } + + #region IDisposable + + /// + public void Dispose() + { + ToastNotificationManagerCompat.OnActivated -= ToastNotificationManagerCompatOnOnActivated; + ToastNotificationManagerCompat.Uninstall(); + } + + #endregion +} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml new file mode 100644 index 000000000..481709577 --- /dev/null +++ b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml @@ -0,0 +1,70 @@ + + + + + A new Artemis update is available! 🥳 + + + + Retrieving changes... + + + + + + + + + + + + + + + + + + + Changelog (auto-generated) + + + + + + + + + + + + + + + + + + We couldn't retrieve any changes + View online + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs new file mode 100644 index 000000000..6c7150f04 --- /dev/null +++ b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogView.axaml.cs @@ -0,0 +1,22 @@ +using Artemis.UI.Shared; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Windows.Screens.Update; + +public partial class UpdateDialogView : ReactiveCoreWindow +{ + public UpdateDialogView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs new file mode 100644 index 000000000..ad2608bf4 --- /dev/null +++ b/src/Artemis.UI.Windows/Screens/Update/UpdateDialogViewModel.cs @@ -0,0 +1,119 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Disposables; +using System.Threading.Tasks; +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 +{ + private readonly UpdateProvider _updateProvider; + private readonly INotificationService _notificationService; + + // 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 bool _retrievingChanges; + private bool _hasChanges; + private string? _latestBuild; + + public UpdateDialogViewModel(string channel, IUpdateProvider updateProvider, INotificationService notificationService) + { + _updateProvider = (UpdateProvider) updateProvider; + _notificationService = notificationService; + + Channel = channel; + CurrentBuild = Core.Constants.BuildInfo.BuildNumberDisplay; + + this.WhenActivated((CompositeDisposable _) => Dispatcher.UIThread.InvokeAsync(GetBuildChanges)); + Install = ReactiveCommand.Create(() => Close(true)); + AskLater = ReactiveCommand.Create(() => Close(false)); + } + + public ReactiveCommand Install { get; } + public ReactiveCommand AskLater { get; } + + public string Channel { get; } + public string CurrentBuild { get; } + + public ObservableCollection 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 currentTask = _updateProvider.GetBuildInfo(1, CurrentBuild); + Task 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; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI.Windows/packages.lock.json b/src/Artemis.UI.Windows/packages.lock.json index 1ec8d286a..9d9a6d7d1 100644 --- a/src/Artemis.UI.Windows/packages.lock.json +++ b/src/Artemis.UI.Windows/packages.lock.json @@ -1,7 +1,7 @@ { "version": 1, "dependencies": { - "net6.0-windows7.0": { + "net6.0-windows10.0.17763": { "Avalonia": { "type": "Direct", "requested": "[0.10.15, )", @@ -65,6 +65,18 @@ "System.Numerics.Vectors": "4.5.0" } }, + "Microsoft.Toolkit.Uwp.Notifications": { + "type": "Direct", + "requested": "[7.1.2, )", + "resolved": "7.1.2", + "contentHash": "cVBRDG8g0K4sBRHFJoNQjwZduDUz+cXP33erIQjAa8fGKF4DsWIuOAQCYGFoXpdXIXjxoaUYuGSxAG6QCvOdtQ==", + "dependencies": { + "Microsoft.Win32.Registry": "4.7.0", + "System.Drawing.Common": "4.7.0", + "System.Reflection.Emit": "4.7.0", + "System.ValueTuple": "4.5.0" + } + }, "Microsoft.Win32": { "type": "Direct", "requested": "[2.0.1, )", @@ -461,12 +473,21 @@ "System.Runtime": "4.3.0" } }, + "Microsoft.Win32.Registry": { + "type": "Transitive", + "resolved": "4.7.0", + "contentHash": "KSrRMb5vNi0CWSGG1++id2ZOs/1QhRqROt+qgbEAdQuGjGrFcl4AOl4/exGPUYz2wUnU42nvJqon1T3U0kPXLA==", + "dependencies": { + "System.Security.AccessControl": "4.7.0", + "System.Security.Principal.Windows": "4.7.0" + } + }, "Microsoft.Win32.SystemEvents": { "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "LuI1oG+24TUj1ZRQQjM5Ew73BKnZE5NZ/7eAdh1o8ST5dPhUnJvIkiIn2re3MwnkRy6ELRnvEbBxHP8uALKhJw==", + "resolved": "4.7.0", + "contentHash": "mtVirZr++rq+XCDITMUdnETD59XoeMxSpLRIII7JRI6Yj0LEDiO1pPn0ktlnIj12Ix8bfvQqQDMMIF9wC98oCA==", "dependencies": { - "Microsoft.NETCore.Platforms": "2.0.0" + "Microsoft.NETCore.Platforms": "3.1.0" } }, "NETStandard.Library": { @@ -977,11 +998,11 @@ }, "System.Drawing.Common": { "type": "Transitive", - "resolved": "4.5.0", - "contentHash": "AiJFxxVPdeITstiRS5aAu8+8Dpf5NawTMoapZ53Gfirml24p7HIfhjmCRxdXnmmf3IUA3AX3CcW7G73CjWxW/Q==", + "resolved": "4.7.0", + "contentHash": "v+XbyYHaZjDfn0ENmJEV1VYLgGgCTx1gnfOBcppowbpOAriglYgGCvFCPr2EEZyBvXlpxbEsTwkOlInl107ahA==", "dependencies": { - "Microsoft.NETCore.Platforms": "2.0.0", - "Microsoft.Win32.SystemEvents": "4.5.0" + "Microsoft.NETCore.Platforms": "3.1.0", + "Microsoft.Win32.SystemEvents": "4.7.0" } }, "System.Dynamic.Runtime": { diff --git a/src/Artemis.UI/Assets/avalonia-logo.ico b/src/Artemis.UI/Assets/avalonia-logo.ico deleted file mode 100644 index da8d49ff9..000000000 Binary files a/src/Artemis.UI/Assets/avalonia-logo.ico and /dev/null differ diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index 5d1498149..8e7a71146 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -21,14 +21,15 @@ namespace Artemis.UI.Screens.Root { public class RootViewModel : ActivatableViewModelBase, IScreen, IMainWindowProvider { - private readonly IAssetLoader _assetLoader; private readonly DefaultTitleBarViewModel _defaultTitleBarViewModel; private readonly ICoreService _coreService; - private readonly IDebugService _debugService; - private readonly IClassicDesktopStyleApplicationLifetime _lifeTime; private readonly ISettingsService _settingsService; - private readonly ISidebarVmFactory _sidebarVmFactory; private readonly IWindowService _windowService; + private readonly IDebugService _debugService; + private readonly IUpdateService _updateService; + private readonly IAssetLoader _assetLoader; + private readonly IClassicDesktopStyleApplicationLifetime _lifeTime; + private readonly ISidebarVmFactory _sidebarVmFactory; private SidebarViewModel? _sidebarViewModel; private ViewModelBase? _titleBarViewModel; @@ -38,6 +39,7 @@ namespace Artemis.UI.Screens.Root IWindowService windowService, IMainWindowService mainWindowService, IDebugService debugService, + IUpdateService updateService, IAssetLoader assetLoader, DefaultTitleBarViewModel defaultTitleBarViewModel, ISidebarVmFactory sidebarVmFactory) @@ -48,6 +50,7 @@ namespace Artemis.UI.Screens.Root _settingsService = settingsService; _windowService = windowService; _debugService = debugService; + _updateService = updateService; _assetLoader = assetLoader; _defaultTitleBarViewModel = defaultTitleBarViewModel; _sidebarVmFactory = sidebarVmFactory; diff --git a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml index cadf0fd41..5db51aec5 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/GeneralTabView.axaml @@ -6,6 +6,8 @@ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" + xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:behaviors="clr-namespace:Artemis.UI.Shared.Behaviors;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="2400" x:Class="Artemis.UI.Screens.Settings.GeneralTabView" x:DataType="settings:GeneralTabViewModel"> @@ -18,40 +20,46 @@ - - - Auto-run on startup - - - - + + + + Auto-run on startup + + + + - - - Hide window on auto-run - - - - - - + + + Hide window on auto-run + + + + + + - - - Startup delay - - Set the amount of seconds to wait before auto-running Artemis. - - - If some devices don't work because Artemis starts before the manufacturer's software, try increasing this value. - - - - - sec - - - + + + Startup delay + + Set the amount of seconds to wait before auto-running Artemis. + + + If some devices don't work because Artemis starts before the manufacturer's software, try increasing this value. + + + + + + + + + sec + + + + @@ -103,48 +111,69 @@ - + + + + + - - Updating - - - - - - - Check for updates - - - If enabled, we'll check for updates on startup and periodically while running. - - - - - - - + + + Updating + + + + + + + Check for updates + + + If enabled, we'll check for updates on startup and periodically while running. + + + + + + + + + + + + Auto-install updates + + + If enabled, new updates will automatically be installed. + + + + + + + - - - - Update - - - Use the button on the right to check for updates now. - - - -