major rework to move to avalonia

This commit is contained in:
IsWaffle 2023-05-11 23:11:39 -04:00
parent 26db4716bc
commit 8331080d85
84 changed files with 3360 additions and 796 deletions

468
.gitignore vendored
View File

@ -1,20 +1,454 @@
## SPTinstaller
*.exe
*.zip
bin/
obj/
*.editorconfig
## visual studio
.vs
.idea
slnx.sqlite
slnx-journal.sqlite
Skip to content
Pull requests
Issues
Codespaces
Marketplace
Explore
@waffle-lord
github /
gitignore
Public
## nodejs
node_modules
node.exe
package-lock.json
Fork your own copy of github/gitignore
Code
Pull requests 390
Actions
Security
Insights
gitignore/VisualStudio.gitignore
@n0099
n0099 [VisualStudio.gitignore] remove a trailing space
Latest commit 491040e Jan 26, 2022
History
165 contributors
@shiftkey
@arcresu
@aroben
@bbodenmiller
@HassanHashemi
@haacked
@niik
@AArnott
@sayedihashimi
@saschanaz
@bdougie
@OsirisTerje
398 lines (319 sloc) 6.7 KB
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
Footer
© 2023 GitHub, Inc.
Footer navigation
Terms
Privacy
Security
Status
Docs
Contact GitHub
Pricing
API
Training
Blog
About
## windows
desktop.ini

Binary file not shown.

Before

Width:  |  Height:  |  Size: 420 KiB

View File

@ -1,17 +0,0 @@
using Spectre.Console;
using SPT_AKI_Installer.Aki.Core.Model;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Core.Interfaces
{
public interface ILiveTaskTableEntry
{
public string TaskName { get; set; }
public int RowIndex { get; set; }
public void SetContext(LiveDisplayContext context, Table table);
public Task<GenericResult> RunAsync();
}
}

View File

@ -1,7 +0,0 @@
namespace SPT_AKI_Installer.Aki.Core.Interfaces
{
internal interface IProgressableTask
{
public int Progress { get; set; }
}
}

View File

@ -1,21 +0,0 @@
namespace SPT_AKI_Installer.Aki.Core.Model
{
public class GenericResult
{
public string Message { get; private set; }
public bool Succeeded { get; private set; }
public bool NonCritical { get; private set; }
protected GenericResult(string message, bool succeeded, bool nonCritical = false)
{
Message = message;
Succeeded = succeeded;
NonCritical = nonCritical;
}
public static GenericResult FromSuccess(string message = "") => new GenericResult(message, true);
public static GenericResult FromError(string errorMessage) => new GenericResult(errorMessage, false);
public static GenericResult FromWarning(string warningMessage) => new GenericResult(warningMessage, false, true);
}
}

View File

@ -1,159 +0,0 @@
using Spectre.Console;
using SPT_AKI_Installer.Aki.Core.Interfaces;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Core.Model
{
public abstract class LiveTableTask : ILiveTaskTableEntry, IProgressableTask, IDisposable
{
/// <summary>
/// The name that will be displayed in th first column of the table
/// </summary>
public string TaskName { get; set; }
/// <summary>
/// Wheather the task reports progress or not
/// </summary>
public bool IsIndeterminate;
/// <summary>
/// The progress (percent completed) of the task
/// </summary>
public int Progress { get; set; }
/// <summary>
/// The row index in the table of the task
/// </summary>
public int RowIndex { get; set; }
private bool _continueRenderingProgress = false;
private bool _continueRenderingIndeterminateProgress = false;
private int _indeterminateState = 0;
private string _currentStatus = "running";
private Table _table { get; set; }
private LiveDisplayContext _context { get; set; }
public LiveTableTask(string name, bool isIndeterminate = true)
{
TaskName = name;
IsIndeterminate = isIndeterminate;
}
private string GetIndetermminateStatus()
{
string status = $"[blue]{_currentStatus.EscapeMarkup()} ";
if (_indeterminateState > 3) _indeterminateState = 0;
status += new string('.', _indeterminateState);
status += "[/]";
_indeterminateState++;
return status;
}
/// <summary>
/// Start indeterminate progress spinner
/// </summary>
/// <remarks>this doesn't need to be called if you set isIndeterminate in the constructor. You need to set IsIndeterminate to false to stop this background task</remarks>
public void StartDrawingIndeterminateProgress()
{
_continueRenderingProgress = false;
_continueRenderingIndeterminateProgress = true;
new Task(new Action(() => { RenderIndeterminateProgress(ref _continueRenderingIndeterminateProgress); })).Start();
}
public void StartDrawingProgress()
{
Progress = 0;
_continueRenderingIndeterminateProgress = false;
_continueRenderingProgress = true;
new Task(new Action(() => { RenderProgress(ref _continueRenderingProgress); })).Start();
}
private void ReRenderEntry(string message)
{
_table.RemoveRow(RowIndex);
_table.InsertRow(RowIndex, TaskName, message);
_context.Refresh();
}
private void RenderIndeterminateProgress(ref bool continueRendering)
{
while (continueRendering)
{
ReRenderEntry(GetIndetermminateStatus());
Thread.Sleep(300);
}
}
private void RenderProgress(ref bool continueRendering)
{
while (continueRendering)
{
string progressBar = new string(' ', 10);
int progressFill = (int)Math.Floor((double)Progress / 10);
progressBar = progressBar.Remove(0, progressFill).Insert(0, new string('=', progressFill));
progressBar = $"[blue][[{progressBar}]][/] {Progress}% {_currentStatus}";
ReRenderEntry(progressBar);
Thread.Sleep(300);
}
}
/// <summary>
/// Set the context and table for this task
/// </summary>
/// <param name="context"></param>
/// <param name="table"></param>
/// <remarks>This is called by <see cref="LiveTableTaskRunner"/> when it is ran. No need to call it yourself</remarks>
public void SetContext(LiveDisplayContext context, Table table)
{
_context = context;
_table = table;
}
/// <summary>
/// Set the status text for the task
/// </summary>
/// <param name="message">The message to show</param>
/// <param name="stopRendering">Stop rendering progress updates (progress and indeterminate progress tasks)</param>
/// <remarks>If you are running an indeterminate task, set render to false. It will render at the next indeterminate update interval</remarks>
public void SetStatus(string message, bool stopRendering = true)
{
_currentStatus = message;
if (stopRendering)
{
_continueRenderingProgress = false;
_continueRenderingIndeterminateProgress = false;
ReRenderEntry(message);
}
}
/// <summary>
/// Run the task async
/// </summary>
/// <returns>Returns the result of the task</returns>
public abstract Task<GenericResult> RunAsync();
public void Dispose()
{
IsIndeterminate = false;
}
}
}

View File

@ -1,87 +0,0 @@
using Spectre.Console;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Core.Model
{
public class LiveTableTaskRunner
{
private static async Task<(bool, LiveTableTask task)> RunAllTasksAsync(List<LiveTableTask> tasks, LiveDisplayContext context, Table table)
{
foreach (var task in tasks)
{
if (task.IsIndeterminate)
{
task.StartDrawingIndeterminateProgress();
}
else
{
task.StartDrawingProgress();
}
var result = await task.RunAsync();
// critical: error - stop installer
if (!result.Succeeded && !result.NonCritical)
{
task.SetStatus($"[red]{result.Message.EscapeMarkup()}[/]");
return (false, task);
}
// non-critical: warning - continue
if (!result.Succeeded && result.NonCritical)
{
task.SetStatus($"[yellow]{result.Message.EscapeMarkup()}[/]");
continue;
}
//suceeded: continue
task.SetStatus($"[green]{(result.Message == "" ? "Complete" : $"{result.Message.EscapeMarkup()}")}[/]");
}
return (true, null);
}
public static async Task RunAsync(List<LiveTableTask> tasks)
{
int halfBufferWidth = Console.BufferWidth / 2;
Table table = new Table().Alignment(Justify.Center).Border(TableBorder.Rounded).BorderColor(Color.Grey).AddColumn("Task", (column) =>
{
column.Width(halfBufferWidth);
})
.AddColumn("Status", (column) =>
{
column.Width(halfBufferWidth);
});
await AnsiConsole.Live(table).StartAsync(async context =>
{
foreach (var task in tasks)
{
table.AddRow(task.TaskName, "[purple]Pending[/]");
task.RowIndex = table.Rows.Count() - 1;
task.SetContext(context, table);
await Task.Delay(50);
context.Refresh();
}
var result = await RunAllTasksAsync(tasks, context, table);
// if a task failed and returned early, set any remaining task status to cancelled
if (!result.Item1)
{
var cancelledTasks = tasks.Take(new Range(tasks.IndexOf(result.Item2) + 1, tasks.Count));
foreach (var task in cancelledTasks)
{
task.SetStatus("[grey]Cancelled[/]");
}
}
});
}
}
}

View File

@ -1,93 +0,0 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Spectre.Console;
using SPT_AKI_Installer.Aki.Core.Model;
using SPT_AKI_Installer.Aki.Core.Tasks;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Core
{
//TODO:
// locales, language selection
public class SPTinstaller
{
InternalData _data;
static void Main()
{
var host = ConfigureHost();
var tasks = host.Services.GetServices<LiveTableTask>();
var taskList = new List<LiveTableTask>(tasks);
AnsiConsole.Write(new FigletText("SPT-AKI INSTALLER").Centered().Color(Color.Yellow));
host.Services.GetRequiredService<SPTinstaller>()
.RunTasksAsync(taskList)
.GetAwaiter()
.GetResult();
}
public SPTinstaller(
InternalData data
)
{
_data = data;
}
public async Task RunTasksAsync(List<LiveTableTask> tasks)
{
_data.TargetInstallPath = Environment.CurrentDirectory;
var cursorPos = Console.GetCursorPosition();
#if DEBUG
var path = AnsiConsole.Ask<string>("[purple]DEBUG[/] [blue]::[/] Enter path to install folder: ").Replace("\"", "");
if (!Directory.Exists(path))
{
CloseApp($"Path invalid: {path}");
}
_data.TargetInstallPath = path;
#endif
var continueInstall = AnsiConsole.Confirm($"SPT will install into:\n[blue]{_data.TargetInstallPath}[/]\n\nContinue?", false);
if (!continueInstall) CloseApp("Please move the installer to the folder you want to install into");
Console.SetCursorPosition(cursorPos.Left, cursorPos.Top);
await LiveTableTaskRunner.RunAsync(tasks);
CloseApp("");
}
private static IHost ConfigureHost()
{
return Host.CreateDefaultBuilder().ConfigureServices((_, services) =>
{
services.AddSingleton<InternalData>();
services.AddTransient<LiveTableTask, DependencyCheckTask>();
services.AddTransient<LiveTableTask, InitializationTask>();
services.AddTransient<LiveTableTask, ReleaseCheckTask>();
services.AddTransient<LiveTableTask, DownloadTask>();
services.AddTransient<LiveTableTask, CopyClientTask>();
services.AddTransient<LiveTableTask, SetupClientTask>();
services.AddTransient<SPTinstaller>();
})
.Build();
}
static void CloseApp(string text)
{
AnsiConsole.MarkupLine($"[yellow]{text.EscapeMarkup()}[/]");
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("Press [blue]Enter[/] to close ...");
Console.ReadKey();
Environment.Exit(0);
}
}
}

View File

@ -1,106 +0,0 @@
using Microsoft.Win32;
using SPT_AKI_Installer.Aki.Core.Model;
using System;
using System.Collections.Generic;
using System.Data.SqlTypes;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Core.Tasks
{
public class DependencyCheckTask : LiveTableTask
{
private bool CheckNetCore6Installed()
{
var minRequiredVersion = new Version("6.0.0");
string[] output;
try
{
var proc = Process.Start(new ProcessStartInfo()
{
FileName = "dotnet",
Arguments = "--list-runtimes",
RedirectStandardOutput = true
});
proc.WaitForExit();
output = proc.StandardOutput.ReadToEnd().Split("\r\n");
}
catch
{
return false;
}
foreach(var lineVersion in output)
{
if (lineVersion.StartsWith("Microsoft.WindowsDesktop.App") && lineVersion.Split(" ").Length > 1)
{
string stringVerion = lineVersion.Split(" ")[1];
var foundVersion = new Version(stringVerion);
// not fully sure if we should only check for 6.x.x versions or if higher major versions are ok -waffle
if(foundVersion >= minRequiredVersion)
{
return true;
}
}
}
return false;
}
private bool CheckNet472Installed()
{
var minRequiredVersion = new Version("4.7.2");
var key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full");
if (key == null)
{
return false;
}
var value = key.GetValue("Version");
if (value != null && value is string versionString)
{
var installedVersion = new Version(versionString);
return installedVersion > minRequiredVersion;
}
return false;
}
public DependencyCheckTask() : base("Dependency Checks", true)
{
}
GenericResult getResult(bool net472Check, bool netCoreCheck) =>
(net472Check, netCoreCheck) switch
{
(true, true) => GenericResult.FromSuccess("Dependencies already installed"),
(false, true) => GenericResult.FromWarning(".Net Framework 472 not found.\nCheck SPT release page for requirements\nhttps://hub.sp-tarkov.com/files/file/16-spt-aki/"),
(true, false) => GenericResult.FromWarning(".Net Runtime Desktop 6 not found.\nCheck SPT release page for requirements\nhttps://hub.sp-tarkov.com/files/file/16-spt-aki/"),
(false, false) => GenericResult.FromWarning("Required dependencies not found.\nCheck SPT release page for requirements\nhttps://hub.sp-tarkov.com/files/file/16-spt-aki/")
};
public override Task<GenericResult> RunAsync()
{
SetStatus("Checking for net framework");
var net472Check = CheckNet472Installed();
SetStatus("Checking for net core");
var netCoreCheck = CheckNetCore6Installed();
return Task.FromResult(getResult(net472Check, netCoreCheck));
}
}
}

View File

@ -1,37 +0,0 @@
using SPT_AKI_Installer.Aki.Core.Model;
using System;
using System.IO;
namespace SPT_AKI_Installer.Aki.Helper
{
public static class FileHelper
{
public static GenericResult CopyDirectoryWithProgress(DirectoryInfo sourceDir, DirectoryInfo targetDir, IProgress<double> progress)
{
try
{
int totalFiles = sourceDir.GetFiles("*.*", SearchOption.AllDirectories).Length;
int processedFiles = 0;
foreach (var dir in sourceDir.GetDirectories("*", SearchOption.AllDirectories))
{
Directory.CreateDirectory(dir.FullName.Replace(sourceDir.FullName, targetDir.FullName));
}
foreach (var file in sourceDir.GetFiles("*.*", SearchOption.AllDirectories))
{
File.Copy(file.FullName, file.FullName.Replace(sourceDir.FullName, targetDir.FullName), true);
processedFiles++;
progress.Report((int)Math.Floor(((double)processedFiles / totalFiles) * 100));
}
return GenericResult.FromSuccess();
}
catch (Exception ex)
{
return GenericResult.FromError(ex.Message);
}
}
}
}

View File

@ -1,3 +0,0 @@
{
}

View File

@ -1,3 +0,0 @@
{
}

View File

@ -1,3 +0,0 @@
{
}

View File

@ -1,3 +0,0 @@
{
}

View File

@ -1,3 +0,0 @@
{
}

View File

@ -1,3 +0,0 @@
{
}

View File

@ -1,18 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<PublishDir>bin\Release\net6.0\publish\win-x64\</PublishDir>
<PublishProtocol>FileSystem</PublishProtocol>
<TargetFramework>net6.0</TargetFramework>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<SelfContained>true</SelfContained>
<PublishSingleFile>true</PublishSingleFile>
<PublishReadyToRun>false</PublishReadyToRun>
<PublishTrimmed>true</PublishTrimmed>
</PropertyGroup>
</Project>

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project>
<PropertyGroup>
<History>True|2022-07-12T01:15:15.4480498Z;True|2022-07-11T21:11:55.8484217-04:00;True|2022-07-09T13:06:26.5751622-04:00;True|2022-07-09T12:56:17.1018408-04:00;True|2022-07-09T12:38:17.0878078-04:00;True|2022-07-09T12:18:23.6469737-04:00;True|2022-06-21T14:47:38.7532473-04:00;True|2022-06-08T13:26:47.7977621-04:00;True|2022-06-06T10:07:18.8067168-04:00;True|2022-06-05T17:55:20.5192697-04:00;True|2022-05-30T08:11:30.6942032-04:00;True|2022-05-30T08:08:08.4269393-04:00;True|2022-05-16T20:06:33.6758525-04:00;True|2022-05-13T20:56:09.8410037-04:00;True|2022-05-13T19:54:24.0683990-04:00;True|2022-05-13T19:53:04.7105427-04:00;True|2022-05-13T19:51:00.6280767-04:00;True|2022-05-13T19:49:19.4630888-04:00;True|2022-05-13T19:47:59.2166156-04:00;</History>
</PropertyGroup>
</Project>

28
SPTInstaller.sln Normal file
View File

@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.33516.290
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SPTInstaller", "SPTInstaller\SPTInstaller.csproj", "{8637C4B3-FC40-4A76-9ED0-707FA5D8E0CF}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
TEST|Any CPU = TEST|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{8637C4B3-FC40-4A76-9ED0-707FA5D8E0CF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8637C4B3-FC40-4A76-9ED0-707FA5D8E0CF}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8637C4B3-FC40-4A76-9ED0-707FA5D8E0CF}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8637C4B3-FC40-4A76-9ED0-707FA5D8E0CF}.Release|Any CPU.Build.0 = Release|Any CPU
{8637C4B3-FC40-4A76-9ED0-707FA5D8E0CF}.TEST|Any CPU.ActiveCfg = TEST|Any CPU
{8637C4B3-FC40-4A76-9ED0-707FA5D8E0CF}.TEST|Any CPU.Build.0 = TEST|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {AF73BCF4-41F2-472E-9C5B-B7F3B71FA6B5}
EndGlobalSection
EndGlobal

454
SPTInstaller/.gitignore vendored Normal file
View File

@ -0,0 +1,454 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# Tye
.tye/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
##
## Visual studio for Mac
##
# globs
Makefile.in
*.userprefs
*.usertasks
config.make
config.status
aclocal.m4
install-sh
autom4te.cache/
*.tar.gz
tarballs/
test-results/
# Mac bundle stuff
*.dmg
*.app
# content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
# JetBrains Rider
.idea/
*.sln.iml
##
## Visual Studio Code
##
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

39
SPTInstaller/App.axaml Normal file
View File

@ -0,0 +1,39 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:SPTInstaller"
x:Class="SPTInstaller.App">
<Application.DataTemplates>
<local:ViewLocator/>
</Application.DataTemplates>
<Application.Styles>
<FluentTheme Mode="Light"/>
</Application.Styles>
<Application.Resources>
<!-- Colors -->
<Color x:Key="AKI_DarkGray">#121212</Color>
<Color x:Key="AKI_Yellow">#FFC107</Color>
<Color x:Key="AKI_White">#FFFFFF</Color>
<Color x:Key="AKI_Gray">#282828</Color>
<Color x:Key="AKI_DarkGrayBlue">#323947</Color>
<!-- Brushes -->
<SolidColorBrush x:Key="AKI_Foreground_Light" Color="{StaticResource AKI_White}"/>
<SolidColorBrush x:Key="AKI_Background_Light" Color="{StaticResource AKI_Gray}"/>
<SolidColorBrush x:Key="AKI_Background_Dark" Color="{StaticResource AKI_DarkGray}"/>
<SolidColorBrush x:Key="AKI_Brush_Yellow" Color="{StaticResource AKI_Yellow}"/>
<SolidColorBrush x:Key="AKI_Brush_DarkGrayBlue" Color="{StaticResource AKI_DarkGrayBlue}"/>
<SolidColorBrush x:Key="AKI_Brush_Lighter" Color="Gainsboro"/>
<!-- Path Geometry -->
<PathGeometry x:Key="CircledCheck" Figures="M 9.0646825 0.06313182 C 7.3648066 0.28806336 5.7978836 0.78839047 4.3639137 1.7461752 3.2921695 2.4620115 2.3631641 3.4084722 1.6479106 4.4762536 0.98737415 5.4623497 0.47819447 6.5896932 0.22806644 7.7524208 -1.2315929 14.537597 4.5254007 20.882361 11.493416 19.89881 c 1.391191 -0.196414 2.73334 -0.717402 3.917306 -1.463979 1.003459 -0.632768 1.91619 -1.463899 2.626322 -2.413989 C 22.172937 10.487163 19.448371 2.526326 12.903647 0.44688781 11.70918 0.06738309 10.312268 -0.10195753 9.0646825 0.06313182 M 14.235529 6.538212 c 0.719844 -0.1922804 1.369569 0.5544499 0.96037 1.2142088 -0.345429 0.5568703 -0.967577 1.0212266 -1.430447 1.4820746 L 10.94499 12.042639 C 10.500924 12.484766 9.9264114 13.323465 9.299721 13.490862 8.8023811 13.623702 8.452016 13.299829 8.1245295 12.978374 7.342478 12.210582 6.3754514 11.44552 5.7298007 10.560564 5.480503 10.218905 5.4699265 9.723192 5.7920077 9.4212362 6.6694846 8.5988409 7.8158456 10.253773 8.3595682 10.794576 c 0.1820751 0.181125 0.4825335 0.608587 0.7834627 0.52988 0.4212659 -0.110141 0.8750481 -0.777076 1.1751921 -1.075907 L 12.981992 7.5964118 C 13.331649 7.2482817 13.738346 6.6710512 14.235529 6.538212 Z"
FillRule="NonZero"
/>
<PathGeometry x:Key="CircledX" Figures="M 9.3972738 0.04245969 C 7.0827527 0.34574986 5.0318949 1.0076069 3.2592046 2.6077382 2.3324496 3.4442761 1.5788823 4.453119 1.0210803 5.566508 c -2.7620688 5.513177 0.320857 12.50987 6.3432023 14.090152 1.2916144 0.338914 2.6250608 0.43131 3.9486944 0.2576 4.747034 -0.622966 8.468465 -4.700542 8.677783 -9.470458 C 20.19922 5.6929665 16.858078 1.3284705 12.212185 0.27365016 11.325097 0.07224603 10.306294 -0.0766601 9.3972738 0.04245969 M 6.8951311 6.0962212 c 0.4071071 0.00285 0.6713562 0.2964224 0.938304 0.5628006 l 1.4856467 1.4826978 c 0.158768 0.1584535 0.4033136 0.5189835 0.6255352 0.5771984 0.126202 0.033088 0.234732 -0.1102653 0.312768 -0.1870931 0.249277 -0.2453478 0.495229 -0.4941674 0.742824 -0.7412705 l 1.094688 -1.0925142 c 0.20842 -0.2080069 0.414066 -0.4608068 0.703728 -0.554646 0.666547 -0.2158494 1.280275 0.3966607 1.063998 1.0618847 -0.141137 0.4339627 -0.633395 0.779821 -0.94671 1.092514 L 11.664841 9.54638 c -0.107944 0.1077293 -0.38877 0.3007922 -0.38877 0.46822 0 0.222015 0.475094 0.554373 0.623346 0.70233 0.595784 0.594602 1.420358 1.20083 1.871407 1.9119 0.241614 0.380976 0.158691 0.920717 -0.229532 1.171722 -0.308154 0.199227 -0.730468 0.156386 -1.016338 -0.06095 C 11.870017 13.24165 11.308677 12.546499 10.726537 11.965515 10.582664 11.821928 10.166604 11.252104 9.944617 11.31028 9.7223954 11.3685 9.4778498 11.729025 9.3190818 11.887479 l -1.4856467 1.4827 C 7.520315 13.682638 7.2187683 13.989088 6.7387474 13.917412 6.1917559 13.835786 5.9203119 13.23264 6.1340103 12.745886 6.2635359 12.450907 6.5544494 12.227488 6.7778435 12.004537 L 8.0289153 10.755949 C 8.183657 10.601515 8.691357 10.24512 8.691357 10.0146 c 0 -0.2305204 -0.5077 -0.5869139 -0.6624417 -0.7413489 L 6.7778435 8.0246649 C 6.5544494 7.8017132 6.2635359 7.5782948 6.1340103 7.2833157 5.8906767 6.7289817 6.2771407 6.091851 6.8951311 6.0962212 Z" FillRule="NonZero"
/>
<PathGeometry x:Key="CircledWarn" Figures="M 9.4328769 0.04019892 C 7.0982838 0.34605265 4.9864964 0.9947734 3.2144923 2.6416847 -0.51716071 6.1098902 -1.0931625 11.937378 1.9776592 16.023146 c 0.6666064 0.886919 1.4893703 1.657003 2.4101133 2.276223 0.9414784 0.633151 1.9874973 1.110834 3.0896371 1.387001 1.2547837 0.314488 2.5508664 0.396281 3.8327134 0.22809 4.776463 -0.626673 8.470809 -4.727503 8.680748 -9.510654 C 20.200809 5.6204614 16.766697 1.2560532 12.09231 0.24958776 11.252125 0.06868514 10.291794 -0.07233093 9.4328769 0.04019892 M 9.784861 4.2119583 c 0.934085 -0.1667851 1.016841 0.6682722 1.016841 1.352973 v 5.1120367 c 0 0.62605 0.190424 1.671637 -0.664858 1.821253 C 9.2424915 12.654703 9.1591106 11.781677 9.1591106 11.145248 V 6.0332096 c 0 -0.6032595 -0.201139 -1.6735876 0.6257504 -1.8212513 m 0 9.9899357 c 1.062442 -0.189692 1.447474 1.424659 0.391092 1.613298 -1.0624433 0.189692 -1.4474743 -1.424659 -0.391092 -1.613298 z" FillRule="NonZero"
/>
</Application.Resources>
</Application>

29
SPTInstaller/App.axaml.cs Normal file
View File

@ -0,0 +1,29 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using SPTInstaller.ViewModels;
using SPTInstaller.Views;
namespace SPTInstaller
{
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
}
base.OnFrameworkInitializationCompleted();
}
}
}

View File

@ -0,0 +1,187 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="using:SPTInstaller.CustomControls"
xmlns:rxui="using:Avalonia.ReactiveUI"
>
<Design.PreviewWith>
<StackPanel Spacing="5" Background="{StaticResource AKI_Background_Dark}">
<Button Content="Blah"/>
<TextBox Text="Some cool text here" Margin="5"/>
<TextBox Watermark="This is a watermark" Margin="5"/>
</StackPanel>
</Design.PreviewWith>
<!-- Add Styles Here -->
<!-- TitleBar Styles -->
<Style Selector="cc|TitleBar">
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}"/>
<Setter Property="ButtonForeground" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
</Style>
<Style Selector="cc|TitleBar.versiontag">
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="BorderThickness" Value="0 0 0 2"/>
</Style>
<!-- TextBox Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml -->
<Style Selector="TextBox">
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
</Style>
<Style Selector="TextBox:focus">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
</Style>
<Style Selector="TextBox:pointerover">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
</Style>
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="DimGray"/>
</Style>
<Style Selector="TextBox:pointerover /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
<Setter Property="Foreground" Value="DimGray"/>
</Style>
<Style Selector="TextBox:focus /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
<Setter Property="Foreground" Value="DimGray"/>
</Style>
<Style Selector="TextBox /template/ TextBlock#PART_Watermark, TextBox:focus /template/ TextBlock#PART_FloatingWatermark">
<Setter Property="Foreground" Value="White"/>
</Style>
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<!-- Label Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Label.xaml -->
<Style Selector="Label">
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}"/>
</Style>
<Style Selector="Label.yellow">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
</Style>
<Style Selector="Label.dark">
<Setter Property="Foreground" Value="DimGray"/>
</Style>
<Style Selector="Label.versionMismatch">
<Setter Property="Foreground" Value="OrangeRed"/>
</Style>
<!-- ProgressBar Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml -->
<Style Selector="ProgressBar">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
</Style>
<Style Selector="ProgressBar.error">
<Setter Property="Foreground" Value="Red"/>
<Style.Animations>
<Animation Duration="0:0:0.5" FillMode="Forward">
<KeyFrame Cue="0%">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Value" Value="0"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="Foreground" Value="Red"/>
<Setter Property="Value" Value="100"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<!-- Seperator Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Separator.xaml -->
<Style Selector="Separator">
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}"/>
</Style>
<!-- Button Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Button.xaml -->
<Style Selector="Button">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}"/>
</Style>
<Style Selector="Button:pointerover">
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}"/>
</Style>
<Style Selector="Button:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
</Style>
<!-- Button yellow -->
<Style Selector="Button.yellow">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}"/>
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style Selector="Button.yellow:pointerover">
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<Style Selector="Button.yellow:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Gold"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style Selector="Button.yellow:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Lighter}"/>
</Style>
<Style Selector="Button.yellow:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
</Style>
<!-- Button Link Style -->
<Style Selector="Button.link">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="Button.link:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="Button.link:pressed /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
</Styles>

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@ -0,0 +1,20 @@
using Avalonia;
using Avalonia.Interactivity;
namespace SPTInstaller.Behaviors
{
public class SpanBehavior : AvaloniaObject
{
public static readonly AttachedProperty<bool> SpanProperty = AvaloniaProperty.RegisterAttached<SpanBehavior, Interactive, bool>("Span");
public static void SetSpan(AvaloniaObject element, bool value)
{
element.SetValue(SpanProperty, value);
}
public static bool GetSpan(AvaloniaObject element)
{
return element.GetValue(SpanProperty);
}
}
}

View File

@ -0,0 +1,62 @@
using SharpCompress;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace SPTInstaller.Controllers
{
public class InstallController
{
public event EventHandler<IProgressableTask> TaskChanged = delegate { };
private IPreCheck[] _preChecks { get; set; }
private IProgressableTask[] _tasks { get; set; }
public InstallController(IProgressableTask[] tasks, IPreCheck[] preChecks = null)
{
_tasks = tasks;
_preChecks = preChecks;
}
public async Task<IResult> RunPreChecks()
{
var requiredResults = new List<IResult>();
_preChecks.ForEach(x => x.IsPending = true);
foreach (var check in _preChecks)
{
var result = await check.RunCheck();
if (check.IsRequired)
{
requiredResults.Add(result);
}
}
foreach(var result in requiredResults)
{
if (!result.Succeeded)
return Result.FromError("Some required checks have failed");
}
return Result.FromSuccess();
}
public async Task<IResult> RunTasks()
{
foreach (var task in _tasks)
{
TaskChanged?.Invoke(null, task);
var result = await task.RunAsync();
if (!result.Succeeded) return result;
}
return Result.FromSuccess("Install Complete. Happy Playing!");
}
}
}

View File

@ -0,0 +1,29 @@
using Avalonia.Data.Converters;
using System;
using System.Globalization;
namespace SPTInstaller.Converters
{
public class InvertedProgressConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if( value is int progress)
{
return 100 - progress;
}
return value;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if ( value is int invertedProgress)
{
return 100 - invertedProgress;
}
return value;
}
}
}

View File

@ -0,0 +1,77 @@
using Avalonia;
using Avalonia.Controls;
using SPTInstaller.Behaviors;
using System;
using System.Linq;
namespace SPTInstaller.CustomControls
{
public class DistributedSpacePanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
var children = Children;
for (int i = 0; i < children.Count; i++)
{
// measure child objects so we can use their desired size in the arrange override
var child = children[i];
child.Measure(availableSize);
}
// we want to use all available space
return availableSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
var children = Children;
Rect rcChild = new Rect(finalSize);
double previousChildSize = 0.0;
// get child objects that don't want to span the entire control
var nonSpanningChildren = children.Where(x => x.GetValue(SpanBehavior.SpanProperty) == false).ToList();
// get the total height off all non-spanning child objects
var totalChildHeight = nonSpanningChildren.Select(x => x.DesiredSize.Height).Sum();
// remove the total child height from our controls final size and divide it by the total non-spanning child objects
// except the last one, since it needs no space after it
var spacing = (finalSize.Height - totalChildHeight) / (nonSpanningChildren.Count - 1);
for (int i = 0; i < children.Count; i++)
{
var child = children[i];
var spanChild = child.GetValue(SpanBehavior.SpanProperty);
if (spanChild)
{
// move any spanning children to the top of the array to push them behind the other controls (visually)
children.Move(i, 0);
rcChild = rcChild.WithY(0)
.WithX(0)
.WithHeight(finalSize.Height)
.WithWidth(finalSize.Width);
child.Arrange(rcChild);
continue;
};
rcChild = rcChild.WithY(rcChild.Y + previousChildSize);
previousChildSize = child.DesiredSize.Height;
rcChild = rcChild.WithHeight(previousChildSize)
.WithWidth(Math.Max(finalSize.Width, child.DesiredSize.Width));
previousChildSize += spacing;
child.Arrange(rcChild);
}
return finalSize;
}
}
}

View File

@ -0,0 +1,84 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.PreCheckItem">
<UserControl.Styles>
<Style Selector="Arc.running">
<Setter Property="Stroke" Value="DodgerBlue"/>
</Style>
<Style Selector="Arc">
<Setter Property="Stroke" Value="Gray"/>
<Setter Property="IsVisible" Value="{Binding IsPending, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<Style.Animations>
<Animation Duration="0:0:1" RepeatCount="infinite">
<KeyFrame Cue="0%">
<Setter Property="RotateTransform.Angle" Value="0"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="RotateTransform.Angle" Value="360"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Label.bold">
<Setter Property="FontWeight" Value="Bold"/>
</Style>
<Style Selector="Path.passed">
<Setter Property="Data" Value="{StaticResource CircledCheck}"/>
<Setter Property="Fill" Value="Green"/>
</Style>
<Style Selector="Path.failed">
<Setter Property="Data" Value="{StaticResource CircledX}"/>
<Setter Property="Fill" Value="Red"/>
<Setter Property="ToolTip.Tip" Value="A required dependency could not be found"/>
</Style>
<Style Selector="Path.warning">
<Setter Property="Data" Value="{StaticResource CircledWarn}"/>
<Setter Property="Fill" Value="Goldenrod"/>
<Setter Property="ToolTip.Tip" Value="This dependency could not be found"/>
</Style>
</UserControl.Styles>
<Grid ColumnDefinitions="22, AUTO" Margin="3">
<Canvas Margin="0 3 0 0"
IsVisible="{Binding !IsPending, RelativeSource={RelativeSource AncestorType=UserControl}}">
<Ellipse Fill="White" Height="15" Width="15" Canvas.Top="3" Canvas.Left="3"
/>
<Path Name="iconPath" StrokeThickness="2"
Classes.passed="{Binding Passed, RelativeSource={RelativeSource AncestorType=UserControl}}"
>
<Classes.failed>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="IsRequired"/>
<Binding Path="!Passed"/>
</MultiBinding>
</Classes.failed>
<Classes.warning>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="!IsRequired"/>
<Binding Path="!Passed"/>
</MultiBinding>
</Classes.warning>
</Path>
</Canvas>
<Arc StartAngle="280" SweepAngle="80" Margin="0 3 0 0" StrokeThickness="3"
Width="20" Height="20" VerticalAlignment="Top"
Classes.running="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
<Label Grid.Column="1"
Content="{Binding PreCheckName, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.bold="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
</Grid>
</UserControl>

View File

@ -0,0 +1,62 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Threading;
using ReactiveUI;
using System.Windows.Input;
namespace SPTInstaller.CustomControls
{
public partial class PreCheckItem : UserControl
{
public PreCheckItem()
{
InitializeComponent();
}
public string PreCheckName
{
get => GetValue(PreCheckNameProperty);
set => SetValue(PreCheckNameProperty, value);
}
public static readonly StyledProperty<string> PreCheckNameProperty =
AvaloniaProperty.Register<PreCheckItem, string>(nameof(PreCheckName));
public bool IsRunning
{
get => GetValue(IsRunningProperty);
set => SetValue(IsRunningProperty, value);
}
public static readonly StyledProperty<bool> IsRunningProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsRunning));
public bool IsPending
{
get => GetValue(IsPendingProperty);
set => SetValue(IsPendingProperty, value);
}
public static readonly StyledProperty<bool> IsPendingProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsPending));
public bool Passed
{
get => GetValue(PassedProperty);
set => SetValue(PassedProperty, value);
}
public static readonly StyledProperty<bool> PassedProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(Passed));
public bool IsRequired
{
get => GetValue(IsRequiredProperty);
set => SetValue(IsRequiredProperty, value);
}
public static readonly StyledProperty<bool> IsRequiredProperty =
AvaloniaProperty.Register<PreCheckItem, bool>(nameof(IsRequired));
}
}

View File

@ -0,0 +1,79 @@
<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:vm="using:SPTInstaller.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.ProgressableTaskItem">
<UserControl.Styles>
<!-- Ellipse Styles -->
<Style Selector="Ellipse">
<Setter Property="Stroke" Value="{Binding PendingColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<Setter Property="Margin" Value="7 0"/>
</Style>
<Style Selector="Ellipse.completed">
<Setter Property="Stroke" Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</Style>
<Style Selector="Ellipse.running">
<Setter Property="Stroke" Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
<Setter Property="Margin" Value="7 3"/>
<Style.Animations>
<Animation Duration="0:0:1" PlaybackDirection="Alternate" RepeatCount="INFINITE">
<KeyFrame Cue="0%">
<Setter Property="ScaleTransform.ScaleX" Value="1"/>
<Setter Property="ScaleTransform.ScaleY" Value="1"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="ScaleTransform.ScaleX" Value="1.2"/>
<Setter Property="ScaleTransform.ScaleY" Value="1.2"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
<Style Selector="Ellipse.centerRunning">
<Setter Property="Fill" Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</Style>
<Style Selector="Ellipse.centerCompleted">
<Setter Property="Fill" Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</Style>
<!-- Label Styles -->
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="{Binding PendingColor, RelativeSource={RelativeSource
AncestorType=UserControl}}"/>
</Style>
<Style Selector="TextBlock.completed">
<Setter Property="Foreground" Value="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</Style>
<Style Selector="TextBlock.running">
<Setter Property="Foreground" Value="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</Style>
</UserControl.Styles>
<Grid ColumnDefinitions="AUTO, *">
<Ellipse Height="30" Width="30"
StrokeThickness="4"
Fill="{StaticResource AKI_Background_Dark}"
HorizontalAlignment="Left"
Classes.running="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.completed="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
<Ellipse Height="20" Width="20"
Classes.centerRunning="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.centerCompleted="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
<TextBlock Grid.Column="1"
Text="{Binding TaskName, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.running="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.completed="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}"
FontWeight="SemiBold"
FontSize="15"
TextWrapping="Wrap"
VerticalAlignment="Center"
HorizontalAlignment="Left"
/>
</Grid>
</UserControl>

View File

@ -0,0 +1,77 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
namespace SPTInstaller.CustomControls
{
public partial class ProgressableTaskItem : UserControl
{
public ProgressableTaskItem()
{
InitializeComponent();
}
public string TaskId
{
get => GetValue(TaskIdProperty);
set => SetValue(TaskIdProperty, value);
}
public static readonly StyledProperty<string> TaskIdProperty =
AvaloniaProperty.Register<ProgressableTaskItem, string>(nameof(TaskId));
public string TaskName
{
get => GetValue(TaskNameProperty);
set => SetValue(TaskNameProperty, value);
}
public static readonly StyledProperty<string> TaskNameProperty =
AvaloniaProperty.Register<ProgressableTaskItem, string>(nameof(TaskName));
public bool IsCompleted
{
get => GetValue(IsCompletedProperty);
set => SetValue(IsCompletedProperty, value);
}
public static readonly StyledProperty<bool> IsCompletedProperty =
AvaloniaProperty.Register<ProgressableTaskItem, bool>(nameof(IsCompleted));
public bool IsRunning
{
get => GetValue(IsRunningProperty);
set => SetValue(IsRunningProperty, value);
}
public static readonly StyledProperty<bool> IsRunningProperty =
AvaloniaProperty.Register<ProgressableTaskItem, bool>(nameof(IsRunning));
public IBrush PendingColor
{
get => GetValue(PendingColorProperty);
set => SetValue(PendingColorProperty, value);
}
public static readonly StyledProperty<IBrush> PendingColorProperty =
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
public IBrush RunningColor
{
get => GetValue(RunningColorProperty);
set => SetValue(RunningColorProperty, value);
}
public static readonly StyledProperty<IBrush> RunningColorProperty =
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
public IBrush CompletedColor
{
get => GetValue(CompletedColorProperty);
set => SetValue(CompletedColorProperty, value);
}
public static readonly StyledProperty<IBrush> CompletedColorProperty =
AvaloniaProperty.Register<ProgressableTaskItem, IBrush>(nameof(PendingColor));
}
}

View File

@ -0,0 +1,47 @@
<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:cc="using:SPTInstaller.CustomControls"
xmlns:bh="using:SPTInstaller.Behaviors"
xmlns:convt="using:SPTInstaller.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.ProgressableTaskList"
>
<UserControl.Resources>
<convt:InvertedProgressConverter x:Key="invtProgressConvt"/>
</UserControl.Resources>
<ItemsControl Name="itemsControl" Items="{Binding Tasks, RelativeSource={RelativeSource AncestorType=UserControl}}"
Padding="5">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<cc:DistributedSpacePanel>
<ProgressBar Orientation="Vertical"
Value="{Binding TaskProgress, RelativeSource={RelativeSource AncestorType=UserControl}, Converter={StaticResource ResourceKey=invtProgressConvt}}"
Background="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
Foreground="{Binding PendingColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Margin="20 3"
MinHeight="0"
Grid.RowSpan="6"
bh:SpanBehavior.Span="True"
/>
</cc:DistributedSpacePanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<cc:ProgressableTaskItem TaskId="{Binding Id}"
TaskName="{Binding Name}"
IsRunning="{Binding IsRunning}"
IsCompleted="{Binding IsCompleted}"
PendingColor="{Binding PendingColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
RunningColor="{Binding RunningColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
CompletedColor="{Binding CompletedColor, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>

View File

@ -0,0 +1,97 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Threading;
using DynamicData;
using DynamicData.Binding;
using SPTInstaller.Models;
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
namespace SPTInstaller.CustomControls
{
public partial class ProgressableTaskList : UserControl
{
public ProgressableTaskList()
{
InitializeComponent();
this.AttachedToVisualTree += ProgressableTaskList_AttachedToVisualTree;
}
private int _taskProgress;
public int TaskProgress
{
get => _taskProgress;
set => SetAndRaise(ProgressableTaskList.TaskProgressProperty, ref _taskProgress, value);
}
public static readonly DirectProperty<ProgressableTaskList, int> TaskProgressProperty =
AvaloniaProperty.RegisterDirect<ProgressableTaskList, int>(nameof(TaskProgress), o => o.TaskProgress);
public ObservableCollection<InstallerTaskBase> Tasks
{
get => GetValue(TasksProperty);
set => SetValue(TasksProperty, value);
}
public static readonly StyledProperty<ObservableCollection<InstallerTaskBase>> TasksProperty =
AvaloniaProperty.Register<ProgressableTaskList, ObservableCollection<InstallerTaskBase>>(nameof(Tasks));
public IBrush PendingColor
{
get => GetValue(PendingColorProperty);
set => SetValue(PendingColorProperty, value);
}
public static readonly StyledProperty<IBrush> PendingColorProperty =
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
public IBrush RunningColor
{
get => GetValue(RunningColorProperty);
set => SetValue(RunningColorProperty, value);
}
public static readonly StyledProperty<IBrush> RunningColorProperty =
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
public IBrush CompletedColor
{
get => GetValue(CompletedColorProperty);
set => SetValue(CompletedColorProperty, value);
}
public static readonly StyledProperty<IBrush> CompletedColorProperty =
AvaloniaProperty.Register<ProgressableTaskList, IBrush>(nameof(PendingColor));
private void UpdateTaskProgress()
{
Dispatcher.UIThread.InvokeAsync(async () =>
{
var completedTasks = Tasks.Where(x => x.IsCompleted == true).Count();
var progress = (int)Math.Floor((double)completedTasks / (Tasks.Count - 1) * 100);
for(; TaskProgress < progress;)
{
TaskProgress += 2;
await Task.Delay(1);
}
});
}
private void ProgressableTaskList_AttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
if (Tasks == null) return;
foreach (var task in Tasks)
{
task.WhenPropertyChanged(x => x.IsCompleted)
.Subscribe(x => UpdateTaskProgress());
}
}
}
}

View File

@ -0,0 +1,39 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.TaskDetails">
<Grid RowDefinitions="10,*,AUTO,10,AUTO,*,AUTO,10" ColumnDefinitions="10,*,10">
<Label Grid.Column="1" Grid.Row="2"
HorizontalAlignment="Center"
FontSize="15"
FontWeight="SemiBold"
Content="{Binding Message, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
<TextBlock Grid.Column="1" Grid.Row="4"
Foreground="Gainsboro"
HorizontalAlignment="Center"
FontSize="12"
Text="{Binding Details, RelativeSource={RelativeSource AncestorType=UserControl}}"
TextTrimming="CharacterEllipsis"
TextWrapping="Wrap"
MaxLines="3"
/>
<Grid Grid.Column="1" Grid.Row="6" ColumnDefinitions="*,AUTO">
<ProgressBar IsVisible="{Binding ShowProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
Value="{Binding Progress, RelativeSource={RelativeSource AncestorType=UserControl}}"
HorizontalAlignment="Stretch"
/>
<Label Grid.Column="1"
Content="{Binding Progress, RelativeSource={RelativeSource AncestorType=UserControl}, StringFormat='{}{0}%'}"
IsVisible="{Binding ShowProgress, RelativeSource={RelativeSource AncestorType=UserControl}}"
/>
</Grid>
</Grid>
</UserControl>

View File

@ -0,0 +1,49 @@
using Avalonia;
using Avalonia.Controls;
namespace SPTInstaller.CustomControls
{
public partial class TaskDetails : UserControl
{
public TaskDetails()
{
InitializeComponent();
}
public string Message
{
get => GetValue(MessageProperty);
set => SetValue(MessageProperty, value);
}
public static readonly StyledProperty<string> MessageProperty =
AvaloniaProperty.Register<TaskDetails, string>(nameof(Message));
public string Details
{
get => GetValue(DetailsProperty);
set => SetValue(DetailsProperty, value);
}
public static readonly StyledProperty<string> DetailsProperty =
AvaloniaProperty.Register<TaskDetails, string>(nameof(Details));
public int Progress
{
get => GetValue(ProgressProperty);
set => SetValue(ProgressProperty, value);
}
public static readonly StyledProperty<int> ProgressProperty =
AvaloniaProperty.Register<TaskDetails, int>(nameof(Progress));
public bool ShowProgress
{
get => GetValue(ShowProgressProperty);
set => SetValue(ShowProgressProperty, value);
}
public static readonly StyledProperty<bool> ShowProgressProperty =
AvaloniaProperty.Register<TaskDetails, bool>(nameof(ShowProgress));
}
}

View File

@ -0,0 +1,76 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.TitleBar">
<Grid ColumnDefinitions="AUTO,*,AUTO,AUTO">
<Rectangle Grid.ColumnSpan="6" IsHitTestVisible="False"
Fill="{Binding Background, RelativeSource={
RelativeSource AncestorType=UserControl}}"
/>
<Label Content="{Binding Title, RelativeSource={
RelativeSource AncestorType=UserControl}}"
IsHitTestVisible="False"
Foreground="{Binding Foreground, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Background="Transparent"
VerticalContentAlignment="Center"
/>
<!-- Minimize (-) Button -->
<Button Content="&#xE949;" Grid.Column="2"
Foreground="{Binding ButtonForeground, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Command="{Binding MinButtonCommand, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Background="Transparent"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
VerticalAlignment="Stretch"
FontFamily="Segoe MDL2 Assets"
CornerRadius="0"
Width="35"
>
<Button.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}"/>
</Style>
</Button.Styles>
</Button>
<!-- Close (X) Button -->
<Button Content="&#xE106;" Grid.Column="3"
Foreground="{Binding ButtonForeground, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Command="{Binding XButtonCommand, RelativeSource={
RelativeSource AncestorType=UserControl}}"
Background="Transparent"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
VerticalAlignment="Stretch"
FontFamily="Segoe MDL2 Assets"
CornerRadius="0"
Width="35"
>
<Button.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="IndianRed"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="Crimson"/>
</Style>
</Button.Styles>
</Button>
</Grid>
</UserControl>

View File

@ -0,0 +1,77 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using System.Windows.Input;
namespace SPTInstaller.CustomControls
{
public partial class TitleBar : UserControl
{
public TitleBar()
{
InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
public static readonly StyledProperty<string> TitleProperty =
AvaloniaProperty.Register<TitleBar, string>(nameof(Title));
public string Title
{
get => GetValue(TitleProperty);
set => SetValue(TitleProperty, value);
}
public static readonly StyledProperty<IBrush> ButtonForegroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(ButtonForeground));
public IBrush ButtonForeground
{
get => GetValue(ButtonForegroundProperty);
set => SetValue(ButtonForegroundProperty, value);
}
public static new readonly StyledProperty<IBrush> ForegroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Foreground));
public new IBrush Foreground
{
get => GetValue(ForegroundProperty);
set => SetValue(ForegroundProperty, value);
}
public static new readonly StyledProperty<IBrush> BackgroundProperty =
AvaloniaProperty.Register<TitleBar, IBrush>(nameof(Background));
public new IBrush Background
{
get => GetValue(BackgroundProperty);
set => SetValue(BackgroundProperty, value);
}
//Close Button Command (X Button) Property
public static readonly StyledProperty<ICommand> XButtonCommandProperty =
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(XButtonCommand));
public ICommand XButtonCommand
{
get => GetValue(XButtonCommandProperty);
set => SetValue(XButtonCommandProperty, value);
}
//Minimize Button Command (- Button) Property
public static readonly StyledProperty<ICommand> MinButtonCommandProperty =
AvaloniaProperty.Register<TitleBar, ICommand>(nameof(MinButtonCommand));
public ICommand MinButtonCommand
{
get => GetValue(MinButtonCommandProperty);
set => SetValue(MinButtonCommandProperty, value);
}
}
}

View File

@ -1,11 +1,11 @@
using HttpClientProgress;
using SPT_AKI_Installer.Aki.Core.Model;
using SPTInstaller.Models;
using System;
using System.IO;
using System.Net.Http;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Helper
namespace SPTInstaller.Aki.Helper
{
public static class DownloadCacheHelper
{
@ -39,7 +39,7 @@ namespace SPT_AKI_Installer.Aki.Helper
}
}
private static async Task<GenericResult> DownloadFile(FileInfo outputFile, string targetLink, IProgress<double> progress, string expectedHash = null)
private static async Task<Result> DownloadFile(FileInfo outputFile, string targetLink, IProgress<double> progress, string expectedHash = null)
{
try
{
@ -51,27 +51,27 @@ namespace SPT_AKI_Installer.Aki.Helper
if (!outputFile.Exists)
{
return GenericResult.FromError($"Failed to download {outputFile.Name}");
return Result.FromError($"Failed to download {outputFile.Name}");
}
if (expectedHash != null && !FileHashHelper.CheckHash(outputFile, expectedHash))
{
return GenericResult.FromError("Hash mismatch");
return Result.FromError("Hash mismatch");
}
return GenericResult.FromSuccess();
return Result.FromSuccess();
}
catch (Exception ex)
{
return GenericResult.FromError(ex.Message);
return Result.FromError(ex.Message);
}
}
private static async Task<GenericResult> ProcessInboundStreamAsync(FileInfo cacheFile, Stream downloadStream, string expectedHash = null)
private static async Task<Result> ProcessInboundStreamAsync(FileInfo cacheFile, Stream downloadStream, string expectedHash = null)
{
try
{
if (CheckCache(cacheFile, expectedHash)) return GenericResult.FromSuccess();
if (CheckCache(cacheFile, expectedHash)) return Result.FromSuccess();
using var patcherFileStream = cacheFile.Open(FileMode.Create);
{
@ -82,32 +82,32 @@ namespace SPT_AKI_Installer.Aki.Helper
if (expectedHash != null && !FileHashHelper.CheckHash(cacheFile, expectedHash))
{
return GenericResult.FromError("Hash mismatch");
return Result.FromError("Hash mismatch");
}
return GenericResult.FromSuccess();
return Result.FromSuccess();
}
catch(Exception ex)
{
return GenericResult.FromError(ex.Message);
return Result.FromError(ex.Message);
}
}
private static async Task<GenericResult> ProcessInboundFileAsync(FileInfo cacheFile, string targetLink, IProgress<double> progress, string expectedHash = null)
private static async Task<Result> ProcessInboundFileAsync(FileInfo cacheFile, string targetLink, IProgress<double> progress, string expectedHash = null)
{
try
{
if (CheckCache(cacheFile, expectedHash)) return GenericResult.FromSuccess();
if (CheckCache(cacheFile, expectedHash)) return Result.FromSuccess();
return await DownloadFile(cacheFile, targetLink, progress, expectedHash);
}
catch(Exception ex)
{
return GenericResult.FromError(ex.Message);
return Result.FromError(ex.Message);
}
}
public static async Task<FileInfo> GetOrDownloadFileAsync(string fileName, string targetLink, IProgress<double> progress, string expectedHash = null)
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, string targetLink, IProgress<double> progress, string expectedHash = null)
{
FileInfo cacheFile = new FileInfo(Path.Join(_cachePath, fileName));
@ -123,7 +123,7 @@ namespace SPT_AKI_Installer.Aki.Helper
}
}
public static async Task<FileInfo> GetOrDownloadFileAsync(string fileName, Stream fileDownloadStream, string expectedHash = null)
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, Stream fileDownloadStream, string expectedHash = null)
{
FileInfo cacheFile = new FileInfo(Path.Join(_cachePath, fileName));

View File

@ -5,7 +5,7 @@ using System.Linq;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
namespace SPT_AKI_Installer.Aki.Helper
namespace SPTInstaller.Aki.Helper
{
public static class FileHashHelper
{

View File

@ -0,0 +1,72 @@
using ReactiveUI;
using SPTInstaller.Models;
using System;
using System.IO;
namespace SPTInstaller.Aki.Helper
{
public static class FileHelper
{
private static Result IterateDirectories(DirectoryInfo sourceDir, DirectoryInfo targetDir)
{
try
{
foreach (var dir in sourceDir.GetDirectories("*", SearchOption.AllDirectories))
{
Directory.CreateDirectory(dir.FullName.Replace(sourceDir.FullName, targetDir.FullName));
}
return Result.FromSuccess();
}
catch (Exception ex)
{
return Result.FromError(ex.Message);
}
}
private static Result IterateFiles(DirectoryInfo sourceDir, DirectoryInfo targetDir, Action<string, int> updateCallback = null)
{
try
{
int totalFiles = sourceDir.GetFiles("*.*", SearchOption.AllDirectories).Length;
int processedFiles = 0;
foreach (var file in sourceDir.GetFiles("*.*", SearchOption.AllDirectories))
{
updateCallback?.Invoke(file.Name, (int)Math.Floor(((double)processedFiles / totalFiles) * 100));
File.Copy(file.FullName, file.FullName.Replace(sourceDir.FullName, targetDir.FullName), true);
processedFiles++;
}
return Result.FromSuccess();
}
catch (Exception ex)
{
return Result.FromError(ex.Message);
}
}
public static Result CopyDirectoryWithProgress(DirectoryInfo sourceDir, DirectoryInfo targetDir, IProgress<double> progress = null) =>
CopyDirectoryWithProgress(sourceDir, targetDir, (msg, prog) => progress?.Report(prog));
public static Result CopyDirectoryWithProgress(DirectoryInfo sourceDir, DirectoryInfo targetDir, Action<string, int> updateCallback = null)
{
try
{
var iterateDirectoriesResult = IterateDirectories(sourceDir, targetDir);
if(!iterateDirectoriesResult.Succeeded) return iterateDirectoriesResult;
var iterateFilesResult = IterateFiles(sourceDir, targetDir, updateCallback);
if (!iterateFilesResult.Succeeded) return iterateDirectoriesResult;
return Result.FromSuccess();
}
catch (Exception ex)
{
return Result.FromError(ex.Message);
}
}
}
}

View File

@ -1,11 +1,11 @@
using Microsoft.Win32;
using SPT_AKI_Installer.Aki.Core.Model;
using SPTInstaller.Models;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
namespace SPT_AKI_Installer.Aki.Helper
namespace SPTInstaller.Aki.Helper
{
public static class PreCheckHelper
{
@ -24,16 +24,16 @@ namespace SPT_AKI_Installer.Aki.Helper
return info?.DirectoryName;
}
public static GenericResult DetectOriginalGameVersion(string gamePath)
public static Result DetectOriginalGameVersion(string gamePath)
{
try
{
string version = FileVersionInfo.GetVersionInfo(Path.Join(gamePath + "/EscapeFromTarkov.exe")).ProductVersion.Replace('-', '.').Split('.')[^2];
return GenericResult.FromSuccess(version);
return Result.FromSuccess(version);
}
catch (Exception ex)
{
return GenericResult.FromError($"File not found: {ex.Message}");
return Result.FromError($"File not found: {ex.Message}");
}
}
}

View File

@ -1,8 +1,8 @@
using SPT_AKI_Installer.Aki.Core.Model;
using SPTInstaller.Models;
using System.Diagnostics;
using System.IO;
namespace SPT_AKI_Installer.Aki.Helper
namespace SPTInstaller.Aki.Helper
{
public enum PatcherExitCode
{
@ -17,11 +17,11 @@ namespace SPT_AKI_Installer.Aki.Helper
public static class ProcessHelper
{
public static GenericResult PatchClientFiles(FileInfo executable, DirectoryInfo workingDir)
public static Result PatchClientFiles(FileInfo executable, DirectoryInfo workingDir)
{
if (!executable.Exists || !workingDir.Exists)
{
return GenericResult.FromError($"Could not find executable ({executable.Name}) or working directory ({workingDir.Name})");
return Result.FromError($"Could not find executable ({executable.Name}) or working directory ({workingDir.Name})");
}
var process = new Process();
@ -36,25 +36,25 @@ namespace SPT_AKI_Installer.Aki.Helper
switch ((PatcherExitCode)process.ExitCode)
{
case PatcherExitCode.Success:
return GenericResult.FromSuccess("Patcher Finished Successfully, extracting Aki");
return Result.FromSuccess("Patcher Finished Successfully, extracting Aki");
case PatcherExitCode.ProgramClosed:
return GenericResult.FromError("Patcher was closed before completing!");
return Result.FromError("Patcher was closed before completing!");
case PatcherExitCode.EftExeNotFound:
return GenericResult.FromError("EscapeFromTarkov.exe is missing from the install Path");
return Result.FromError("EscapeFromTarkov.exe is missing from the install Path");
case PatcherExitCode.NoPatchFolder:
return GenericResult.FromError("Patchers Folder called 'Aki_Patches' is missing");
return Result.FromError("Patchers Folder called 'Aki_Patches' is missing");
case PatcherExitCode.MissingFile:
return GenericResult.FromError("EFT files was missing a Vital file to continue");
return Result.FromError("EFT files was missing a Vital file to continue");
case PatcherExitCode.PatchFailed:
return GenericResult.FromError("A patch failed to apply");
return Result.FromError("A patch failed to apply");
default:
return GenericResult.FromError("an unknown error occurred in the patcher");
return Result.FromError("an unknown error occurred in the patcher");
}
}
}

View File

@ -0,0 +1,116 @@
using Splat;
using System;
using System.Collections.Generic;
using System.Linq;
namespace SPTInstaller.Helpers
{
/// <summary>
/// A helper class to handle simple service registration to Splat with constructor parameter injection
/// </summary>
/// <remarks>Splat only recognizes the registered types and doesn't account for interfaces :(</remarks>
internal static class ServiceHelper
{
private static bool TryRegisterInstance<T, T2>(object[] parameters = null)
{
var instance = Activator.CreateInstance(typeof(T2), parameters);
if (instance != null)
{
Locator.CurrentMutable.RegisterConstant<T>((T)instance);
return true;
}
return false;
}
/// <summary>
/// Register a class as a service
/// </summary>
/// <typeparam name="T">class to register</typeparam>
public static void Register<T>() where T : class => Register<T, T>();
/// <summary>
/// Register a class as a service by another type
/// </summary>
/// <typeparam name="T">type to register as</typeparam>
/// <typeparam name="T2">class to register</typeparam>
public static void Register<T, T2>() where T : class
{
var constructors = typeof(T2).GetConstructors();
foreach(var constructor in constructors)
{
var parmesan = constructor.GetParameters();
if(parmesan.Length == 0)
{
if (TryRegisterInstance<T, T2>()) return;
continue;
}
List<object> parameters = new List<object>();
for(int i = 0; i < parmesan.Length; i++)
{
var parm = parmesan[i];
var parmValue = Get(parm.ParameterType);
if (parmValue != null) parameters.Add(parmValue);
}
if (TryRegisterInstance<T, T2>(parameters.ToArray())) return;
}
}
/// <summary>
/// Get a service from splat
/// </summary>
/// <param name="type"></param>
/// <returns></returns>
/// <exception cref="InvalidOperationException">Thrown if the service isn't found</exception>
public static object Get(Type type)
{
var service = Locator.Current.GetService(type);
if (service == null)
throw new InvalidOperationException($"Could not locate service of type '{type.Name}'");
return service;
}
/// <summary>
/// Get a service from splat
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="InvalidOperationException">Thrown if the service isn't found</exception>
public static T Get<T>()
{
var service = Locator.Current.GetService<T>();
if (service == null)
throw new InvalidOperationException($"Could not locate service of type '{nameof(T)}'");
return service;
}
/// <summary>
/// Get all services of a type
/// </summary>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
/// <exception cref="InvalidOperationException">thrown if no services are found</exception>
public static T[] GetAll<T>()
{
var services = Locator.Current.GetServices<T>().ToArray();
if (services == null || services.Count() == 0)
throw new InvalidOperationException($"Could not locate service of type '{nameof(T)}'");
return services;
}
}
}

View File

@ -1,16 +1,16 @@
using SharpCompress.Archives;
using SharpCompress.Archives.Zip;
using SharpCompress.Common;
using SPT_AKI_Installer.Aki.Core.Model;
using SPTInstaller.Models;
using System;
using System.IO;
using System.Linq;
namespace SPT_AKI_Installer.Aki.Helper
namespace SPTInstaller.Aki.Helper
{
public static class ZipHelper
{
public static GenericResult Decompress(FileInfo ArchivePath, DirectoryInfo OutputFolderPath, IProgress<double> progress = null)
public static Result Decompress(FileInfo ArchivePath, DirectoryInfo OutputFolderPath, IProgress<double> progress = null)
{
try
{
@ -42,14 +42,14 @@ namespace SPT_AKI_Installer.Aki.Helper
if (!OutputFolderPath.Exists)
{
return GenericResult.FromError($"Failed to extract files: {ArchivePath.Name}");
return Result.FromError($"Failed to extract files: {ArchivePath.Name}");
}
return GenericResult.FromSuccess();
return Result.FromSuccess();
}
catch (Exception ex)
{
return GenericResult.FromError(ex.Message);
return Result.FromError(ex.Message);
}
}
}

View File

@ -1,30 +1,29 @@
using SPT_AKI_Installer.Aki.Core.Model;
using SPT_AKI_Installer.Aki.Helper;
using SPTInstaller.Aki.Helper;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.IO;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Core.Tasks
namespace SPTInstaller.Installer_Tasks
{
public class CopyClientTask : LiveTableTask
public class CopyClientTask : InstallerTaskBase
{
private InternalData _data;
public CopyClientTask(InternalData data) : base("Copy Client Files", false)
public CopyClientTask(InternalData data) : base("Copy Client Files")
{
_data = data;
}
public override async Task<GenericResult> RunAsync()
public override async Task<IResult> TaskOperation()
{
SetStatus("Copying", false);
SetStatus("Copying Client Files", 0);
var originalGameDirInfo = new DirectoryInfo(_data.OriginalGamePath);
var targetInstallDirInfo = new DirectoryInfo(_data.TargetInstallPath);
var progress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); });
return FileHelper.CopyDirectoryWithProgress(originalGameDirInfo, targetInstallDirInfo, progress);
return FileHelper.CopyDirectoryWithProgress(originalGameDirInfo, targetInstallDirInfo, (message, progress) => { SetStatus($"Copying Client Files", message, progress); });
}
}
}

View File

@ -1,34 +1,33 @@
using CG.Web.MegaApiClient;
using Newtonsoft.Json;
using SPT_AKI_Installer.Aki.Core.Model;
using SPT_AKI_Installer.Aki.Helper;
using SPTInstaller.Aki.Helper;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Core.Tasks
namespace SPTInstaller.Installer_Tasks
{
public class DownloadTask : LiveTableTask
public class DownloadTask : InstallerTaskBase
{
private InternalData _data;
public DownloadTask(InternalData data) : base("Download Files", false)
public DownloadTask(InternalData data) : base("Download Files")
{
_data = data;
}
private async Task<GenericResult> BuildMirrorList()
private async Task<IResult> BuildMirrorList()
{
SetStatus("Downloading Mirror List", false);
var progress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); });
var progress = new Progress<double>((d) => { SetStatus("Downloading Mirror List", (int)Math.Floor(d));});
var file = await DownloadCacheHelper.GetOrDownloadFileAsync("mirrors.json", _data.PatcherMirrorsLink, progress);
if (file == null)
{
return GenericResult.FromError("Failed to download mirror list");
return Result.FromError("Failed to download mirror list");
}
var mirrorsList = JsonConvert.DeserializeObject<List<DownloadMirror>>(File.ReadAllText(file.FullName));
@ -37,17 +36,17 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
{
_data.PatcherReleaseMirrors = mirrors;
return GenericResult.FromSuccess();
return Result.FromSuccess();
}
return GenericResult.FromError("Failed to deserialize mirrors list");
return Result.FromError("Failed to deserialize mirrors list");
}
private async Task<GenericResult> DownloadPatcherFromMirrors(IProgress<double> progress)
private async Task<IResult> DownloadPatcherFromMirrors(IProgress<double> progress)
{
foreach (var mirror in _data.PatcherReleaseMirrors)
{
SetStatus($"Downloading Patcher: {mirror.Link}", false);
SetStatus($"Downloading Patcher: {mirror.Link}");
// mega is a little weird since they use encryption, but thankfully there is a great library for their api :)
if (mirror.Link.StartsWith("https://mega"))
@ -69,7 +68,7 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
continue;
}
return GenericResult.FromSuccess();
return Result.FromSuccess();
}
catch
{
@ -82,15 +81,17 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
if (_data.PatcherZipInfo != null)
{
return GenericResult.FromSuccess();
return Result.FromSuccess();
}
}
return GenericResult.FromError("Failed to download Patcher");
return Result.FromError("Failed to download Patcher");
}
public override async Task<GenericResult> RunAsync()
public override async Task<IResult> TaskOperation()
{
var progress = new Progress<double>((d) => { SetStatus("", (int)Math.Floor(d)); });
if (_data.PatchNeeded)
{
var buildResult = await BuildMirrorList();
@ -100,9 +101,8 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
return buildResult;
}
Progress = 0;
SetStatus("", 0);
var progress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); });
var patcherDownloadRresult = await DownloadPatcherFromMirrors(progress);
if (!patcherDownloadRresult.Succeeded)
@ -111,20 +111,16 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
}
}
SetStatus("Downloading SPT-AKI", false);
SetStatus("Downloading SPT-AKI", 0);
Progress = 0;
var akiProgress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); });
_data.AkiZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("sptaki.zip", _data.AkiReleaseDownloadLink, akiProgress, _data.AkiReleaseHash);
_data.AkiZipInfo = await DownloadCacheHelper.GetOrDownloadFileAsync("sptaki.zip", _data.AkiReleaseDownloadLink, progress, _data.AkiReleaseHash);
if (_data.AkiZipInfo == null)
{
return GenericResult.FromError("Failed to download spt-aki");
return Result.FromError("Failed to download spt-aki");
}
return GenericResult.FromSuccess();
return Result.FromSuccess();
}
}
}

View File

@ -1,11 +1,12 @@
using SPT_AKI_Installer.Aki.Core.Model;
using SPT_AKI_Installer.Aki.Helper;
using SPTInstaller.Aki.Helper;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System.IO;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Core.Tasks
namespace SPTInstaller.Installer_Tasks
{
public class InitializationTask : LiveTableTask
public class InitializationTask : InstallerTaskBase
{
private InternalData _data;
@ -14,13 +15,13 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
_data = data;
}
public override async Task<GenericResult> RunAsync()
public override async Task<IResult> TaskOperation()
{
_data.OriginalGamePath = PreCheckHelper.DetectOriginalGamePath();
if (_data.OriginalGamePath == null)
{
return GenericResult.FromError("EFT IS NOT INSTALLED!");
return Result.FromError("EFT IS NOT INSTALLED!");
}
var result = PreCheckHelper.DetectOriginalGameVersion(_data.OriginalGamePath);
@ -34,21 +35,21 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
if (_data.OriginalGamePath == null)
{
return GenericResult.FromError("Unable to find EFT OG directory, please make sure EFT is installed. Please also run EFT once");
return Result.FromError("Unable to find EFT OG directory, please make sure EFT is installed. Please also run EFT once");
}
if (_data.OriginalGamePath == _data.TargetInstallPath)
{
return GenericResult.FromError("Installer is located in EFT's original directory. Please move the installer to a seperate folder as per the guide");
return Result.FromError("Installer is located in EFT's original directory. Please move the installer to a seperate folder as per the guide");
}
if (File.Exists(Path.Join(_data.TargetInstallPath, "EscapeFromTarkov.exe")))
{
return GenericResult.FromError("Installer is located in a Folder that has existing Game Files. Please make sure the installer is in a fresh folder as per the guide");
return Result.FromError("Installer is located in a Folder that has existing Game Files. Please make sure the installer is in a fresh folder as per the guide");
}
return GenericResult.FromSuccess($"Current Game Version: {_data.OriginalGameVersion}");
return Result.FromSuccess($"Current Game Version: {_data.OriginalGameVersion}");
}
}
}

View File

@ -0,0 +1,58 @@
using SPTInstaller.Models;
using System;
using System.Diagnostics;
using System.Threading.Tasks;
namespace SPTInstaller.Installer_Tasks.PreChecks
{
public class NetCore6PreCheck : PreCheckBase
{
public NetCore6PreCheck() : base(".Net Core 6 Desktop Runtime", false)
{
}
public override async Task<bool> CheckOperation()
{
var minRequiredVersion = new Version("6.0.0");
string[] output;
try
{
var proc = Process.Start(new ProcessStartInfo()
{
FileName = "dotnet",
Arguments = "--list-runtimes",
RedirectStandardOutput = true,
CreateNoWindow = true
});
proc.WaitForExit();
output = proc.StandardOutput.ReadToEnd().Split("\r\n");
}
catch (Exception ex)
{
// TODO: logging
return false;
}
foreach (var lineVersion in output)
{
if (lineVersion.StartsWith("Microsoft.WindowsDesktop.App") && lineVersion.Split(" ").Length > 1)
{
string stringVerion = lineVersion.Split(" ")[1];
var foundVersion = new Version(stringVerion);
// not fully sure if we should only check for 6.x.x versions or if higher major versions are ok -waffle
if (foundVersion >= minRequiredVersion)
{
return true;
}
}
}
return false;
}
}
}

View File

@ -0,0 +1,46 @@
using Microsoft.Win32;
using SPTInstaller.Models;
using System;
using System.Threading.Tasks;
namespace SPTInstaller.Installer_Tasks.PreChecks
{
public class NetFramework472PreCheck : PreCheckBase
{
public NetFramework472PreCheck() : base(".Net Framework 4.7.2", false)
{
}
public override async Task<bool> CheckOperation()
{
try
{
var minRequiredVersion = new Version("4.7.2");
var key = Registry.LocalMachine.OpenSubKey("SOFTWARE\\Microsoft\\NET Framework Setup\\NDP\\v4\\Full");
if (key == null)
{
return false;
}
var value = key.GetValue("Version");
if (value != null && value is string versionString)
{
var installedVersion = new Version(versionString);
return installedVersion > minRequiredVersion;
}
return false;
}
catch (Exception ex)
{
// TODO: log exceptions
return false;
}
}
}
}

View File

@ -1,15 +1,14 @@
using Gitea.Api;
using Gitea.Client;
using Gitea.Model;
using SPT_AKI_Installer.Aki.Core.Model;
using SPT_AKI_Installer.Aki.Helper;
using SPTInstaller.Aki.Helper;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Core.Tasks
namespace SPTInstaller.Installer_Tasks
{
public class ReleaseCheckTask : LiveTableTask
public class ReleaseCheckTask : InstallerTaskBase
{
private InternalData _data;
@ -18,19 +17,19 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
_data = data;
}
public override async Task<GenericResult> RunAsync()
public override async Task<IResult> TaskOperation()
{
Configuration.Default.BasePath = "https://dev.sp-tarkov.com/api/v1";
var repo = new RepositoryApi(Configuration.Default);
try
{
SetStatus("Checking SPT Releases", false);
Configuration.Default.BasePath = "https://dev.sp-tarkov.com/api/v1";
var repo = new RepositoryApi(Configuration.Default);
SetStatus("Checking SPT Releases");
var akiRepoReleases = await repo.RepoListReleasesAsync("SPT-AKI", "Stable-releases");
SetStatus("Checking for Patches", false);
SetStatus("Checking for Patches");
var patchRepoReleases = await repo.RepoListReleasesAsync("SPT-AKI", "Downgrade-Patches");
@ -53,7 +52,7 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
if (IntGameVersion < IntAkiVersion)
{
return GenericResult.FromError("Your client is outdated. Please update EFT");
return Result.FromError("Your client is outdated. Please update EFT");
}
@ -64,7 +63,7 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
if (comparePatchToAki == null && patchNeedCheck)
{
return GenericResult.FromError("No patcher available for your version");
return Result.FromError("No patcher available for your version");
}
_data.PatchNeeded = patchNeedCheck;
@ -76,12 +75,12 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
status += " - Patch Available";
}
return GenericResult.FromSuccess(status);
return Result.FromSuccess(status);
}
catch (Exception)
catch (Exception ex)
{
//request failed
return GenericResult.FromError("Request Failed");
return Result.FromError($"Request Failed:\n{ex.Message}");
}
}
}

View File

@ -1,22 +1,23 @@
using SPT_AKI_Installer.Aki.Core.Model;
using SPT_AKI_Installer.Aki.Helper;
using SPTInstaller.Aki.Helper;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace SPT_AKI_Installer.Aki.Core.Tasks
namespace SPTInstaller.Installer_Tasks
{
public class SetupClientTask : LiveTableTask
public class SetupClientTask : InstallerTaskBase
{
private InternalData _data;
public SetupClientTask(InternalData data) : base("Setup Client", false)
public SetupClientTask(InternalData data) : base("Setup Client")
{
_data = data;
}
public override async Task<GenericResult> RunAsync()
public override async Task<IResult> TaskOperation()
{
var targetInstallDirInfo = new DirectoryInfo(_data.TargetInstallPath);
@ -24,14 +25,15 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
var patcherEXE = new FileInfo(Path.Join(_data.TargetInstallPath, "patcher.exe"));
var progress = new Progress<double>((d) => { SetStatus("", (int)Math.Floor(d)); });
if (_data.PatchNeeded)
{
// extract patcher files
SetStatus("Extrating Patcher", false);
SetStatus("Extrating Patcher", 0);
var extractPatcherProgress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); });
var extractPatcherResult = ZipHelper.Decompress(_data.PatcherZipInfo, patcherOutputDir, extractPatcherProgress);
var extractPatcherResult = ZipHelper.Decompress(_data.PatcherZipInfo, patcherOutputDir, progress);
if (!extractPatcherResult.Succeeded)
{
@ -39,14 +41,11 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
}
// copy patcher files to install directory
SetStatus("Copying Patcher", false);
SetStatus("Copying Patcher", 0);
var patcherDirInfo = patcherOutputDir.GetDirectories("Patcher*", SearchOption.TopDirectoryOnly).First();
var copyPatcherProgress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); });
var copyPatcherResult = FileHelper.CopyDirectoryWithProgress(patcherDirInfo, targetInstallDirInfo, copyPatcherProgress);
var copyPatcherResult = FileHelper.CopyDirectoryWithProgress(patcherDirInfo, targetInstallDirInfo, progress);
if (!copyPatcherResult.Succeeded)
{
@ -55,7 +54,9 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
// run patcher
SetStatus("Running Patcher");
StartDrawingIndeterminateProgress();
// TODO: indeterminate progress indicator
//StartDrawingIndeterminateProgress();
var patchingResult = ProcessHelper.PatchClientFiles(patcherEXE, targetInstallDirInfo);
@ -65,15 +66,10 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
}
}
// extract release files
SetStatus("Extracting Release");
StartDrawingProgress();
SetStatus("Extracting Release", 0);
var extractReleaseProgress = new Progress<double>((d) => { Progress = (int)Math.Floor(d); });
var extractReleaseResult = ZipHelper.Decompress(_data.AkiZipInfo, targetInstallDirInfo, extractReleaseProgress);
var extractReleaseResult = ZipHelper.Decompress(_data.AkiZipInfo, targetInstallDirInfo, progress);
if (!extractReleaseResult.Succeeded)
{
@ -82,7 +78,9 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
// cleanup temp files
SetStatus("Cleanup");
StartDrawingIndeterminateProgress();
// TODO: indeterminate progress indicator
//StartDrawingIndeterminateProgress();
if(_data.PatchNeeded)
{
@ -90,7 +88,7 @@ namespace SPT_AKI_Installer.Aki.Core.Tasks
patcherEXE.Delete();
}
return GenericResult.FromSuccess("SPT is Setup. Happy Playing!");
return Result.FromSuccess("SPT is Setup. Happy Playing!");
}
}
}

View File

@ -0,0 +1,35 @@
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.Threading.Tasks;
namespace SPTInstaller.Installer_Tasks
{
internal class TestTask : InstallerTaskBase
{
public static TestTask FromRandomName() => new TestTask($"Test Task #{new Random().Next(0, 9999)}");
public TestTask(string name) : base(name)
{
}
public async override Task<IResult> TaskOperation()
{
int total = 4;
TimeSpan interval = TimeSpan.FromSeconds(1);
for(int i = 0; i < total; i++)
{
var count = i + 1;
string progressMessage = $"Running Task: {Name}";
int progress = (int)Math.Floor((double)count / total * 100);
SetStatus(progressMessage, $"Details: ({count}/{total})", progress);
await Task.Delay(interval);
}
return Result.FromSuccess();
}
}
}

View File

@ -0,0 +1,17 @@
using System.Threading.Tasks;
namespace SPTInstaller.Interfaces
{
public interface IPreCheck
{
public string Id { get; }
public string Name { get; }
public bool IsRequired { get; }
public bool IsPending { get; set; }
public bool Passed { get; }
public Task<IResult> RunCheck();
}
}

View File

@ -0,0 +1,24 @@
using System.Threading.Tasks;
namespace SPTInstaller.Interfaces
{
public interface IProgressableTask
{
public string Id { get; }
public string Name { get; }
public bool IsCompleted { get; }
public bool HasErrors { get; }
public bool IsRunning { get; }
public string StatusMessage { get; }
public int Progress { get; }
public bool ShowProgress { get; }
public Task<IResult> RunAsync();
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace SPTInstaller.Interfaces
{
public interface IResult
{
public bool Succeeded { get; }
public string Message { get; }
}
}

View File

@ -1,4 +1,4 @@
namespace SPT_AKI_Installer.Aki.Core.Model
namespace SPTInstaller.Models
{
public class DownloadMirror
{

View File

@ -0,0 +1,134 @@
using Avalonia.Threading;
using ReactiveUI;
using SPTInstaller.Interfaces;
using System;
using System.Threading.Tasks;
namespace SPTInstaller.Models
{
public abstract class InstallerTaskBase : ReactiveObject, IProgressableTask
{
private string _id;
public string Id
{
get => _id;
private set => this.RaiseAndSetIfChanged(ref _id, value);
}
private string _name;
public string Name
{
get => _name;
private set => this.RaiseAndSetIfChanged(ref _name, value);
}
private bool _isComleted;
public bool IsCompleted
{
get => _isComleted;
private set => this.RaiseAndSetIfChanged(ref _isComleted, value);
}
private bool _hasErrors;
public bool HasErrors
{
get => _hasErrors;
private set => this.RaiseAndSetIfChanged(ref _hasErrors, value);
}
private bool _isRunning;
public bool IsRunning
{
get => _isRunning;
private set => this.RaiseAndSetIfChanged(ref _isRunning, value);
}
private int _progress;
public int Progress
{
get => _progress;
private set => this.RaiseAndSetIfChanged(ref _progress, value);
}
private bool _showProgress;
public bool ShowProgress
{
get => _showProgress;
private set => this.RaiseAndSetIfChanged(ref _showProgress, value);
}
private string _statusMessage;
public string StatusMessage
{
get => _statusMessage;
private set => this.RaiseAndSetIfChanged(ref _statusMessage, value);
}
private string _statusDetails;
public string StatusDetails
{
get => _statusDetails;
private set => this.RaiseAndSetIfChanged(ref _statusDetails, value);
}
/// <summary>
/// Update the status message and optionally a progress bar value
/// </summary>
/// <param name="message"></param>
/// <param name="progress"></param>
/// <remarks>If message is empty, it isn't updated. If progress is null, the progress bar will be hidden. Details is not touched with this method</remarks>
public void SetStatus(string message, int? progress = null) => SetStatus(message, "", progress);
/// <summary>
/// Update the status message, status details, and optionlly a progress bar value
/// </summary>
/// <param name="message"></param>
/// <param name="progress"></param>
/// <remarks>If message or details are empty, it isn't updated. If progress is null, the progress bar will be hidden</remarks>
public void SetStatus(string message, string details, int? progress = null)
{
StatusMessage = String.IsNullOrWhiteSpace(message) ? StatusMessage : message;
StatusDetails = String.IsNullOrWhiteSpace(details) ? StatusDetails : details;
ShowProgress = progress != null;
if (progress != null)
{
Progress = progress.Value;
}
}
public InstallerTaskBase(string name)
{
Name = name;
Id = Guid.NewGuid().ToString();
}
/// <summary>
/// A method for the install controller to call. Do not use this within your task
/// </summary>
/// <returns></returns>
public async Task<IResult> RunAsync()
{
IsRunning = true;
var result = await TaskOperation();
IsRunning = false;
if (!result.Succeeded)
{
// TODO: handle error state
}
IsCompleted = true;
return result;
}
/// <summary>
/// The task you want to run
/// </summary>
/// <returns></returns>
public abstract Task<IResult> TaskOperation();
}
}

View File

@ -1,8 +1,7 @@
using SPT_AKI_Installer.Aki.Core.Model;
using System.Collections.Generic;
using System.Collections.Generic;
using System.IO;
namespace SPT_AKI_Installer.Aki.Core
namespace SPTInstaller.Models
{
public class InternalData
{

View File

@ -0,0 +1,76 @@
using ReactiveUI;
using SPTInstaller.Interfaces;
using System;
using System.Threading.Tasks;
namespace SPTInstaller.Models
{
public abstract class PreCheckBase : ReactiveObject, IPreCheck
{
private string _id;
public string Id
{
get => _id;
set => this.RaiseAndSetIfChanged(ref _id, value);
}
private string _name;
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
private bool _required;
public bool IsRequired
{
get => _required;
set => this.RaiseAndSetIfChanged(ref _required, value);
}
private bool _passed;
public bool Passed
{
get => _passed;
set => this.RaiseAndSetIfChanged(ref _passed, value);
}
private bool _isPending;
public bool IsPending
{
get => _isPending;
set => this.RaiseAndSetIfChanged(ref _isPending, value);
}
private bool _isRunning;
public bool IsRunning
{
get => _isRunning;
set => this.RaiseAndSetIfChanged(ref _isRunning, value);
}
/// <summary>
/// Base class for pre-checks to run before installation
/// </summary>
/// <param name="name">The display name of the pre-check</param>
/// <param name="required">If installation should stop on failing this pre-check</param>
public PreCheckBase(string name, bool required)
{
Name = name;
IsRequired = required;
Id = Guid.NewGuid().ToString();
}
public async Task<IResult> RunCheck()
{
IsRunning = true;
Passed = await CheckOperation();
IsRunning = false;
IsPending = false;
return Passed ? Result.FromSuccess() : Result.FromError($"PreCheck Failed: {Name}");
}
public abstract Task<bool> CheckOperation();
}
}

View File

@ -0,0 +1,20 @@
using SPTInstaller.Interfaces;
namespace SPTInstaller.Models
{
public class Result : IResult
{
public bool Succeeded { get; private set; }
public string Message { get; private set; }
protected Result(string message, bool succeeded)
{
Message = message;
Succeeded = succeeded;
}
public static Result FromSuccess(string message = "") => new Result(message, true);
public static Result FromError(string message) => new Result(message, false);
}
}

64
SPTInstaller/Program.cs Normal file
View File

@ -0,0 +1,64 @@
using Avalonia;
using Avalonia.ReactiveUI;
using ReactiveUI;
using Splat;
using SPTInstaller.Controllers;
using SPTInstaller.Helpers;
using SPTInstaller.Installer_Tasks;
using SPTInstaller.Installer_Tasks.PreChecks;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System;
using System.Linq;
using System.Reflection;
namespace SPTInstaller
{
internal class Program
{
// Initialization code. Don't use any Avalonia, third-party APIs or any
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
{
Locator.CurrentMutable.RegisterViewsForViewModels(Assembly.GetExecutingAssembly());
// Register all the things
// Regestering as base classes so ReactiveUI works correctly. Doesn't seem to like the interfaces :(
ServiceHelper.Register<InternalData>();
ServiceHelper.Register<PreCheckBase, NetFramework472PreCheck>();
ServiceHelper.Register<PreCheckBase, NetCore6PreCheck>();
#if !TEST
ServiceHelper.Register<InstallerTaskBase, InitializationTask>();
ServiceHelper.Register<InstallerTaskBase, ReleaseCheckTask>();
ServiceHelper.Register<InstallerTaskBase, DownloadTask>();
ServiceHelper.Register<InstallerTaskBase, CopyClientTask>();
ServiceHelper.Register<InstallerTaskBase, SetupClientTask>();
#else
for(int i = 0; i < 5; i++)
{
Locator.CurrentMutable.RegisterConstant<InstallerTaskBase>(TestTask.FromRandomName());
}
#endif
// need the interfaces for the controller and splat won't resolve them since we need to base classes in avalonia (what a mess), so doing it manually here
var tasks = Locator.Current.GetServices<InstallerTaskBase>().ToArray() as IProgressableTask[];
var preChecks = Locator.Current.GetServices<PreCheckBase>().ToArray() as IPreCheck[];
var installer = new InstallController(tasks, preChecks);
// manually register install controller
Locator.CurrentMutable.RegisterConstant(installer);
return AppBuilder.Configure<App>()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
}
}
}

5
SPTInstaller/Roots.xml Normal file
View File

@ -0,0 +1,5 @@
<linker>
<!-- Can be removed if CompiledBinding and no reflection are used -->
<assembly fullname="SPTInstaller" preserve="All" />
<assembly fullname="Avalonia.Themes.Fluent" preserve="All" />
</linker>

View File

@ -0,0 +1,42 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<PackageIcon>icon.ico</PackageIcon>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<Configurations>Debug;Release;TEST</Configurations>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
<None Remove=".gitignore" />
<None Remove="Assets\icon.ico" />
</ItemGroup>
<ItemGroup>
<TrimmerRootDescriptor Include="Roots.xml" />
</ItemGroup>
<ItemGroup>
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia" Version="0.10.19" />
<PackageReference Include="Avalonia.Desktop" Version="0.10.19" />
<PackageReference Include="Avalonia.Diagnostics" Version="0.10.19" />
<PackageReference Include="Avalonia.ReactiveUI" Version="0.10.19" />
<PackageReference Include="FubarCoder.RestSharp.Portable.HttpClient" Version="4.0.8" />
<PackageReference Include="MegaApiClient" Version="1.10.3" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="SharpCompress" Version="0.33.0" />
<PackageReference Include="XamlNameReferenceGenerator" Version="1.6.1" />
</ItemGroup>
<ItemGroup>
<Reference Include="Gitea">
<HintPath>Z:\dev_stuff\EftPatchHelper\EftPatchHelper\EftPatchHelper\Resources\Gitea.dll</HintPath>
</Reference>
</ItemGroup>
</Project>

View File

@ -0,0 +1,28 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using SPTInstaller.ViewModels;
using System;
namespace SPTInstaller
{
public class ViewLocator : IDataTemplate
{
public IControl Build(object data)
{
var name = data.GetType().FullName!.Replace("ViewModel", "View");
var type = Type.GetType(name);
if (type != null)
{
return (Control)Activator.CreateInstance(type)!;
}
return new TextBlock { Text = "Not Found: " + name };
}
public bool Match(object data)
{
return data is ViewModelBase;
}
}
}

View File

@ -0,0 +1,38 @@
using ReactiveUI;
using SPTInstaller.Controllers;
using SPTInstaller.Helpers;
using SPTInstaller.Interfaces;
using SPTInstaller.Models;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
namespace SPTInstaller.ViewModels
{
public class InstallViewModel : ViewModelBase
{
private IProgressableTask _currentTask;
public IProgressableTask CurrentTask
{
get => _currentTask;
set => this.RaiseAndSetIfChanged(ref _currentTask, value);
}
public ObservableCollection<InstallerTaskBase> MyTasks { get; set; }
= new ObservableCollection<InstallerTaskBase>(ServiceHelper.GetAll<InstallerTaskBase>());
public InstallViewModel(IScreen host) : base(host)
{
var installer = ServiceHelper.Get<InstallController>();
installer.TaskChanged += (sender, task) => CurrentTask = task;
Task.Run(async () =>
{
var result = await installer.RunTasks();
NavigateTo(new MessageViewModel(HostScreen, result.Message));
});
}
}
}

View File

@ -0,0 +1,33 @@
using Avalonia;
using ReactiveUI;
namespace SPTInstaller.ViewModels
{
public class MainWindowViewModel : ReactiveObject, IActivatableViewModel, IScreen
{
public RoutingState Router { get; } = new RoutingState();
public ViewModelActivator Activator { get; } = new ViewModelActivator();
public MainWindowViewModel()
{
Router.Navigate.Execute(new PreChecksViewModel(this));
}
public void CloseCommand()
{
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{
desktopApp.MainWindow.Close();
}
}
public void MinimizeCommand()
{
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{
desktopApp.MainWindow.WindowState = Avalonia.Controls.WindowState.Minimized;
}
}
}
}

View File

@ -0,0 +1,29 @@
using Avalonia;
using ReactiveUI;
using System.Windows.Input;
namespace SPTInstaller.ViewModels
{
public class MessageViewModel : ViewModelBase
{
private string _Message;
public string Message
{
get => _Message;
set => this.RaiseAndSetIfChanged(ref _Message, value);
}
public ICommand CloseCommand { get; set; } = ReactiveCommand.Create(() =>
{
if (Application.Current.ApplicationLifetime is Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{
desktopApp.MainWindow.Close();
}
});
public MessageViewModel(IScreen Host, string message) : base(Host)
{
Message = message;
}
}
}

View File

@ -0,0 +1,53 @@
using ReactiveUI;
using SPTInstaller.Controllers;
using SPTInstaller.Helpers;
using SPTInstaller.Models;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using System.Windows.Input;
namespace SPTInstaller.ViewModels
{
public class PreChecksViewModel : ViewModelBase
{
private string _InstallPath;
public string InstallPath
{
get => _InstallPath;
set => this.RaiseAndSetIfChanged(ref _InstallPath, value);
}
ObservableCollection<PreCheckBase> PreChecks { get; set; }
= new ObservableCollection<PreCheckBase>(ServiceHelper.GetAll<PreCheckBase>());
ICommand StartInstallCommand { get; set; }
public PreChecksViewModel(IScreen host) : base(host)
{
var data = ServiceHelper.Get<InternalData>();
var installer = ServiceHelper.Get<InstallController>();
if(data == null || installer == null)
{
// TODO: abort to message view
}
data.TargetInstallPath = Environment.CurrentDirectory;
InstallPath = data.TargetInstallPath;
StartInstallCommand = ReactiveCommand.Create(() =>
{
NavigateTo(new InstallViewModel(HostScreen));
});
Task.Run(async () =>
{
// TODO: if a required precheck fails, abort to message view
var result = await installer.RunPreChecks();
});
}
}
}

View File

@ -0,0 +1,61 @@
using Avalonia.Threading;
using ReactiveUI;
using System.Threading.Tasks;
using System;
namespace SPTInstaller.ViewModels
{
public class ViewModelBase : ReactiveObject, IActivatableViewModel, IRoutableViewModel
{
public ViewModelActivator Activator { get; } = new ViewModelActivator();
public string? UrlPathSegment => Guid.NewGuid().ToString().Substring(0, 7);
public IScreen HostScreen { get; }
/// <summary>
/// Delay the return of the viewmodel
/// </summary>
/// <param name="Milliseconds">The amount of time in milliseconds to delay</param>
/// <returns>The viewmodel after the delay time</returns>
/// <remarks>Useful to delay the navigation to another view. For instance, to allow an animation to complete.</remarks>
private async Task<ViewModelBase> WithDelay(int Milliseconds)
{
await Task.Delay(Milliseconds);
return this;
}
/// <summary>
/// Navigate to another viewmodel after a delay
/// </summary>
/// <param name="ViewModel"></param>
/// <param name="Milliseconds"></param>
/// <returns></returns>
public async Task NavigateToWithDelay(ViewModelBase ViewModel, int Milliseconds)
{
await Dispatcher.UIThread.InvokeAsync(async () =>
{
HostScreen.Router.Navigate.Execute(await ViewModel.WithDelay(Milliseconds));
});
}
/// <summary>
/// Navigate to another viewmodel
/// </summary>
/// <param name="ViewModel"></param>
public void NavigateTo(ViewModelBase ViewModel)
{
Dispatcher.UIThread.InvokeAsync(() =>
{
HostScreen.Router.Navigate.Execute(ViewModel);
});
}
public ViewModelBase(IScreen Host)
{
HostScreen = Host;
}
}
}

View File

@ -0,0 +1,24 @@
<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:cc="using:SPTInstaller.CustomControls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.Views.InstallView">
<Grid ColumnDefinitions="*, 2*">
<cc:ProgressableTaskList Tasks="{Binding MyTasks}"
Padding="20"
Background="{StaticResource AKI_Background_Dark}"
PendingColor="Gray"
RunningColor="DodgerBlue"
CompletedColor="{StaticResource AKI_Brush_Yellow}"
/>
<cc:TaskDetails Grid.Column="1"
Message="{Binding CurrentTask.StatusMessage}"
Details="{Binding CurrentTask.StatusDetails}"
Progress="{Binding CurrentTask.Progress}"
ShowProgress="{Binding CurrentTask.ShowProgress}"
/>
</Grid>
</UserControl>

View File

@ -0,0 +1,14 @@
using Avalonia.Controls;
using Avalonia.ReactiveUI;
using SPTInstaller.ViewModels;
namespace SPTInstaller.Views
{
public partial class InstallView : ReactiveUserControl<InstallViewModel>
{
public InstallView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,37 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:SPTInstaller.ViewModels"
xmlns:rxui="using:Avalonia.ReactiveUI"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cc="using:SPTInstaller.CustomControls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.Views.MainWindow"
Icon="/Assets/icon.ico"
Title="SPT Installer"
Height="450" Width="750"
WindowStartupLocation="CenterScreen"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="-1"
Background="{StaticResource AKI_Background_Light}"
>
<Window.Styles>
<StyleInclude Source="/Assets/Styles.axaml"/>
</Window.Styles>
<Design.DataContext>
<vm:MainWindowViewModel/>
</Design.DataContext>
<Grid RowDefinitions="AUTO,*">
<cc:TitleBar Title="SPT Installer"
XButtonCommand="{Binding CloseCommand}"
MinButtonCommand="{Binding MinimizeCommand}"
/>
<rxui:RoutedViewHost Router="{Binding Router}" Grid.Row="1"/>
</Grid>
</Window>

View File

@ -0,0 +1,12 @@
using Avalonia.Controls;
namespace SPTInstaller.Views
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,26 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.Views.MessageView">
<Grid ColumnDefinitions="*,AUTO,*" RowDefinitions="*,AUTO,*">
<StackPanel Grid.Row="1" Grid.Column="1" Spacing="20">
<Label Content="{Binding Message}" FontSize="18"
HorizontalAlignment="Center"
/>
<Button Content="Close" Command="{Binding CloseCommand}"
FontSize="15" FontWeight="SemiBold"
Classes="yellow"
HorizontalAlignment="Center"
VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
Padding="20 10"
/>
</StackPanel>
</Grid>
</UserControl>

View File

@ -0,0 +1,14 @@
using Avalonia.Controls;
using Avalonia.ReactiveUI;
using SPTInstaller.ViewModels;
namespace SPTInstaller.Views
{
public partial class MessageView : ReactiveUserControl<MessageViewModel>
{
public MessageView()
{
InitializeComponent();
}
}
}

View File

@ -0,0 +1,47 @@
<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:cc="using:SPTInstaller.CustomControls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.Views.PreChecksView">
<Grid ColumnDefinitions="10,*,AUTO,*,10"
RowDefinitions="10,*,AUTO,AUTO,AUTO,*,10">
<StackPanel Grid.Column="1" Grid.ColumnSpan="3" Grid.Row="2" HorizontalAlignment="Center">
<Label Content="SPT will be installed into this folder:"
HorizontalAlignment="Center"
/>
<Label Foreground="DodgerBlue"
Content="{Binding InstallPath}"
HorizontalAlignment="Center"
/>
<Label Content="Move the installer into the folder you want it to install into if this is wrong"
HorizontalAlignment="Center"
/>
</StackPanel>
<Button Grid.Column="2" Grid.Row="3" Content="Start Install" Padding="20 10"
Margin="10"
FontSize="15" FontWeight="SemiBold"
Classes="yellow"
Command="{Binding StartInstallCommand}"
/>
<ItemsControl Items="{Binding PreChecks}" Grid.Column="1" Grid.ColumnSpan="3" Grid.Row="4" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<cc:PreCheckItem PreCheckName="{Binding Name}"
IsRunning="{Binding IsRunning}"
IsPending="{Binding IsPending}"
IsRequired="{Binding IsRequired}"
Passed="{Binding Passed}"
/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia.ReactiveUI;
using SPTInstaller.ViewModels;
namespace SPTInstaller.Views
{
public partial class PreChecksView : ReactiveUserControl<PreChecksViewModel>
{
public PreChecksView()
{
InitializeComponent();
}
}
}

18
SPTInstaller/app.manifest Normal file
View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embeded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="AvaloniaTest.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

View File

@ -1,41 +0,0 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<PackageIcon>icon.ico</PackageIcon>
<ApplicationIcon>Aki.Asset\icon.ico</ApplicationIcon>
</PropertyGroup>
<ItemGroup>
<Content Include="Aki.Asset\icon.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FubarCoder.RestSharp.Portable.Core" Version="4.0.8" />
<PackageReference Include="FubarCoder.RestSharp.Portable.HttpClient" Version="4.0.8" />
<PackageReference Include="MegaApiClient" Version="1.10.2" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="SharpCompress" Version="0.32.1" />
<PackageReference Include="Spectre.Console" Version="0.44.0" />
</ItemGroup>
<ItemGroup>
<Reference Include="Gitea">
<HintPath>shared\Gitea.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<None Update="Aki.Asset\icon.ico">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
<None Update="Aki.Asset\icon.jpeg">
<Pack>True</Pack>
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
</Project>

View File

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<_LastSelectedProfileId>Z:\dev_stuff\SPT-AKI-Installer\Properties\PublishProfiles\FolderProfile.pubxml</_LastSelectedProfileId>
</PropertyGroup>
</Project>

View File

@ -1,25 +0,0 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.1.32407.343
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SPT_AKI_Installer", "SPT_AKI_Installer.csproj", "{7B07749A-3BE8-41B5-9B98-9F41C83FA15B}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{7B07749A-3BE8-41B5-9B98-9F41C83FA15B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7B07749A-3BE8-41B5-9B98-9F41C83FA15B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7B07749A-3BE8-41B5-9B98-9F41C83FA15B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7B07749A-3BE8-41B5-9B98-9F41C83FA15B}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {6F8EE63B-3DC5-4168-A560-9B39F7785D84}
EndGlobalSection
EndGlobal