diff --git a/.gitignore b/.gitignore index 51ef1cc..ad06a90 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/Aki.Asset/icon.ico b/Aki.Asset/icon.ico deleted file mode 100644 index 50a2369..0000000 Binary files a/Aki.Asset/icon.ico and /dev/null differ diff --git a/Aki.Core/Interfaces/ILiveTaskTableEntry.cs b/Aki.Core/Interfaces/ILiveTaskTableEntry.cs deleted file mode 100644 index b334661..0000000 --- a/Aki.Core/Interfaces/ILiveTaskTableEntry.cs +++ /dev/null @@ -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 RunAsync(); - } -} diff --git a/Aki.Core/Interfaces/IProgressableTask.cs b/Aki.Core/Interfaces/IProgressableTask.cs deleted file mode 100644 index 7237c7a..0000000 --- a/Aki.Core/Interfaces/IProgressableTask.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace SPT_AKI_Installer.Aki.Core.Interfaces -{ - internal interface IProgressableTask - { - public int Progress { get; set; } - } -} diff --git a/Aki.Core/Model/GenericResult.cs b/Aki.Core/Model/GenericResult.cs deleted file mode 100644 index c59d7b7..0000000 --- a/Aki.Core/Model/GenericResult.cs +++ /dev/null @@ -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); - } -} diff --git a/Aki.Core/Model/LiveTableTask.cs b/Aki.Core/Model/LiveTableTask.cs deleted file mode 100644 index df90b56..0000000 --- a/Aki.Core/Model/LiveTableTask.cs +++ /dev/null @@ -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 - { - /// - /// The name that will be displayed in th first column of the table - /// - public string TaskName { get; set; } - - /// - /// Wheather the task reports progress or not - /// - public bool IsIndeterminate; - - /// - /// The progress (percent completed) of the task - /// - public int Progress { get; set; } - - /// - /// The row index in the table of the task - /// - 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; - } - - /// - /// Start indeterminate progress spinner - /// - /// 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 - 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); - } - } - - /// - /// Set the context and table for this task - /// - /// - /// - /// This is called by when it is ran. No need to call it yourself - public void SetContext(LiveDisplayContext context, Table table) - { - _context = context; - _table = table; - } - - /// - /// Set the status text for the task - /// - /// The message to show - /// Stop rendering progress updates (progress and indeterminate progress tasks) - /// If you are running an indeterminate task, set render to false. It will render at the next indeterminate update interval - public void SetStatus(string message, bool stopRendering = true) - { - _currentStatus = message; - - if (stopRendering) - { - _continueRenderingProgress = false; - _continueRenderingIndeterminateProgress = false; - ReRenderEntry(message); - } - } - - /// - /// Run the task async - /// - /// Returns the result of the task - public abstract Task RunAsync(); - - public void Dispose() - { - IsIndeterminate = false; - } - } -} diff --git a/Aki.Core/Model/LiveTableTaskRunner.cs b/Aki.Core/Model/LiveTableTaskRunner.cs deleted file mode 100644 index 6d4a5b2..0000000 --- a/Aki.Core/Model/LiveTableTaskRunner.cs +++ /dev/null @@ -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 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 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[/]"); - } - - } - }); - } - } -} diff --git a/Aki.Core/SPTInstaller.cs b/Aki.Core/SPTInstaller.cs deleted file mode 100644 index 0a2cfa8..0000000 --- a/Aki.Core/SPTInstaller.cs +++ /dev/null @@ -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(); - - var taskList = new List(tasks); - - AnsiConsole.Write(new FigletText("SPT-AKI INSTALLER").Centered().Color(Color.Yellow)); - - host.Services.GetRequiredService() - .RunTasksAsync(taskList) - .GetAwaiter() - .GetResult(); - } - - public SPTinstaller( - InternalData data - ) - { - _data = data; - } - - public async Task RunTasksAsync(List tasks) - { - _data.TargetInstallPath = Environment.CurrentDirectory; - - var cursorPos = Console.GetCursorPosition(); - -#if DEBUG - var path = AnsiConsole.Ask("[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(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - services.AddTransient(); - }) - .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); - } - } -} \ No newline at end of file diff --git a/Aki.Core/Tasks/DependencyCheckTask.cs b/Aki.Core/Tasks/DependencyCheckTask.cs deleted file mode 100644 index 06df724..0000000 --- a/Aki.Core/Tasks/DependencyCheckTask.cs +++ /dev/null @@ -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 RunAsync() - { - SetStatus("Checking for net framework"); - - var net472Check = CheckNet472Installed(); - - SetStatus("Checking for net core"); - - var netCoreCheck = CheckNetCore6Installed(); - - return Task.FromResult(getResult(net472Check, netCoreCheck)); - } - } -} diff --git a/Aki.Helper/FileHelper.cs b/Aki.Helper/FileHelper.cs deleted file mode 100644 index 9bcdb44..0000000 --- a/Aki.Helper/FileHelper.cs +++ /dev/null @@ -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 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); - } - } - } -} diff --git a/Aki.Locales/CN.json b/Aki.Locales/CN.json deleted file mode 100644 index 077404a..0000000 --- a/Aki.Locales/CN.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/Aki.Locales/DE.json b/Aki.Locales/DE.json deleted file mode 100644 index 077404a..0000000 --- a/Aki.Locales/DE.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/Aki.Locales/EN.json b/Aki.Locales/EN.json deleted file mode 100644 index 0e0dcd2..0000000 --- a/Aki.Locales/EN.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/Aki.Locales/FR.json b/Aki.Locales/FR.json deleted file mode 100644 index 077404a..0000000 --- a/Aki.Locales/FR.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/Aki.Locales/KR.json b/Aki.Locales/KR.json deleted file mode 100644 index 077404a..0000000 --- a/Aki.Locales/KR.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/Aki.Locales/RU.json b/Aki.Locales/RU.json deleted file mode 100644 index 077404a..0000000 --- a/Aki.Locales/RU.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - -} \ No newline at end of file diff --git a/Properties/PublishProfiles/FolderProfile.pubxml b/Properties/PublishProfiles/FolderProfile.pubxml deleted file mode 100644 index 388b15e..0000000 --- a/Properties/PublishProfiles/FolderProfile.pubxml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - Release - Any CPU - bin\Release\net6.0\publish\win-x64\ - FileSystem - net6.0 - win-x64 - true - true - false - true - - \ No newline at end of file diff --git a/Properties/PublishProfiles/FolderProfile.pubxml.user b/Properties/PublishProfiles/FolderProfile.pubxml.user deleted file mode 100644 index e3a2ecb..0000000 --- a/Properties/PublishProfiles/FolderProfile.pubxml.user +++ /dev/null @@ -1,9 +0,0 @@ - - - - - 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; - - \ No newline at end of file diff --git a/SPTInstaller.sln b/SPTInstaller.sln new file mode 100644 index 0000000..036a25e --- /dev/null +++ b/SPTInstaller.sln @@ -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 diff --git a/SPTInstaller/.gitignore b/SPTInstaller/.gitignore new file mode 100644 index 0000000..8afdcb6 --- /dev/null +++ b/SPTInstaller/.gitignore @@ -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 diff --git a/SPTInstaller/App.axaml b/SPTInstaller/App.axaml new file mode 100644 index 0000000..796915e --- /dev/null +++ b/SPTInstaller/App.axaml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + #121212 + #FFC107 + #FFFFFF + #282828 + #323947 + + + + + + + + + + + + + + + diff --git a/SPTInstaller/App.axaml.cs b/SPTInstaller/App.axaml.cs new file mode 100644 index 0000000..cc8165c --- /dev/null +++ b/SPTInstaller/App.axaml.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/SPTInstaller/Assets/Styles.axaml b/SPTInstaller/Assets/Styles.axaml new file mode 100644 index 0000000..c4d4caf --- /dev/null +++ b/SPTInstaller/Assets/Styles.axaml @@ -0,0 +1,187 @@ + + + + + + + + + + + diff --git a/SPTInstaller/CustomControls/TitleBar.axaml.cs b/SPTInstaller/CustomControls/TitleBar.axaml.cs new file mode 100644 index 0000000..5c0c336 --- /dev/null +++ b/SPTInstaller/CustomControls/TitleBar.axaml.cs @@ -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 TitleProperty = + AvaloniaProperty.Register(nameof(Title)); + + public string Title + { + get => GetValue(TitleProperty); + set => SetValue(TitleProperty, value); + } + + public static readonly StyledProperty ButtonForegroundProperty = + AvaloniaProperty.Register(nameof(ButtonForeground)); + + public IBrush ButtonForeground + { + get => GetValue(ButtonForegroundProperty); + set => SetValue(ButtonForegroundProperty, value); + } + + public static new readonly StyledProperty ForegroundProperty = + AvaloniaProperty.Register(nameof(Foreground)); + + public new IBrush Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public static new readonly StyledProperty BackgroundProperty = + AvaloniaProperty.Register(nameof(Background)); + + public new IBrush Background + { + get => GetValue(BackgroundProperty); + set => SetValue(BackgroundProperty, value); + } + + //Close Button Command (X Button) Property + public static readonly StyledProperty XButtonCommandProperty = + AvaloniaProperty.Register(nameof(XButtonCommand)); + + public ICommand XButtonCommand + { + get => GetValue(XButtonCommandProperty); + set => SetValue(XButtonCommandProperty, value); + } + + //Minimize Button Command (- Button) Property + public static readonly StyledProperty MinButtonCommandProperty = + AvaloniaProperty.Register(nameof(MinButtonCommand)); + + public ICommand MinButtonCommand + { + get => GetValue(MinButtonCommandProperty); + set => SetValue(MinButtonCommandProperty, value); + } + } +} diff --git a/Aki.Helper/DownloadCacheHelper.cs b/SPTInstaller/Helpers/DownloadCacheHelper.cs similarity index 70% rename from Aki.Helper/DownloadCacheHelper.cs rename to SPTInstaller/Helpers/DownloadCacheHelper.cs index 5fcd2a8..0ab712c 100644 --- a/Aki.Helper/DownloadCacheHelper.cs +++ b/SPTInstaller/Helpers/DownloadCacheHelper.cs @@ -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 DownloadFile(FileInfo outputFile, string targetLink, IProgress progress, string expectedHash = null) + private static async Task DownloadFile(FileInfo outputFile, string targetLink, IProgress 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 ProcessInboundStreamAsync(FileInfo cacheFile, Stream downloadStream, string expectedHash = null) + private static async Task 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 ProcessInboundFileAsync(FileInfo cacheFile, string targetLink, IProgress progress, string expectedHash = null) + private static async Task ProcessInboundFileAsync(FileInfo cacheFile, string targetLink, IProgress 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 GetOrDownloadFileAsync(string fileName, string targetLink, IProgress progress, string expectedHash = null) + public static async Task GetOrDownloadFileAsync(string fileName, string targetLink, IProgress 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 GetOrDownloadFileAsync(string fileName, Stream fileDownloadStream, string expectedHash = null) + public static async Task GetOrDownloadFileAsync(string fileName, Stream fileDownloadStream, string expectedHash = null) { FileInfo cacheFile = new FileInfo(Path.Join(_cachePath, fileName)); diff --git a/Aki.Helper/FileHashHelper.cs b/SPTInstaller/Helpers/FileHashHelper.cs similarity index 96% rename from Aki.Helper/FileHashHelper.cs rename to SPTInstaller/Helpers/FileHashHelper.cs index 16d5cf8..2b70e55 100644 --- a/Aki.Helper/FileHashHelper.cs +++ b/SPTInstaller/Helpers/FileHashHelper.cs @@ -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 { diff --git a/SPTInstaller/Helpers/FileHelper.cs b/SPTInstaller/Helpers/FileHelper.cs new file mode 100644 index 0000000..e01c81a --- /dev/null +++ b/SPTInstaller/Helpers/FileHelper.cs @@ -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 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 progress = null) => + CopyDirectoryWithProgress(sourceDir, targetDir, (msg, prog) => progress?.Report(prog)); + + public static Result CopyDirectoryWithProgress(DirectoryInfo sourceDir, DirectoryInfo targetDir, Action 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); + } + } + } +} diff --git a/Aki.Helper/HttpClientProgressExtensions.cs b/SPTInstaller/Helpers/HttpClientProgressExtensions.cs similarity index 100% rename from Aki.Helper/HttpClientProgressExtensions.cs rename to SPTInstaller/Helpers/HttpClientProgressExtensions.cs diff --git a/Aki.Helper/PreCheckHelper.cs b/SPTInstaller/Helpers/PreCheckHelper.cs similarity index 78% rename from Aki.Helper/PreCheckHelper.cs rename to SPTInstaller/Helpers/PreCheckHelper.cs index 4ffdd0c..4380020 100644 --- a/Aki.Helper/PreCheckHelper.cs +++ b/SPTInstaller/Helpers/PreCheckHelper.cs @@ -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}"); } } } diff --git a/Aki.Helper/ProcessHelper.cs b/SPTInstaller/Helpers/ProcessHelper.cs similarity index 54% rename from Aki.Helper/ProcessHelper.cs rename to SPTInstaller/Helpers/ProcessHelper.cs index 8dd4feb..a2c6e9a 100644 --- a/Aki.Helper/ProcessHelper.cs +++ b/SPTInstaller/Helpers/ProcessHelper.cs @@ -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"); } } } diff --git a/SPTInstaller/Helpers/ServiceHelper.cs b/SPTInstaller/Helpers/ServiceHelper.cs new file mode 100644 index 0000000..7a3833f --- /dev/null +++ b/SPTInstaller/Helpers/ServiceHelper.cs @@ -0,0 +1,116 @@ +using Splat; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace SPTInstaller.Helpers +{ + /// + /// A helper class to handle simple service registration to Splat with constructor parameter injection + /// + /// Splat only recognizes the registered types and doesn't account for interfaces :( + internal static class ServiceHelper + { + private static bool TryRegisterInstance(object[] parameters = null) + { + var instance = Activator.CreateInstance(typeof(T2), parameters); + + if (instance != null) + { + Locator.CurrentMutable.RegisterConstant((T)instance); + return true; + } + + return false; + } + + /// + /// Register a class as a service + /// + /// class to register + public static void Register() where T : class => Register(); + + /// + /// Register a class as a service by another type + /// + /// type to register as + /// class to register + public static void Register() where T : class + { + var constructors = typeof(T2).GetConstructors(); + + foreach(var constructor in constructors) + { + var parmesan = constructor.GetParameters(); + + if(parmesan.Length == 0) + { + if (TryRegisterInstance()) return; + + continue; + } + + List parameters = new List(); + + 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(parameters.ToArray())) return; + } + } + + /// + /// Get a service from splat + /// + /// + /// + /// Thrown if the service isn't found + 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; + } + + /// + /// Get a service from splat + /// + /// + /// + /// Thrown if the service isn't found + public static T Get() + { + var service = Locator.Current.GetService(); + + if (service == null) + throw new InvalidOperationException($"Could not locate service of type '{nameof(T)}'"); + + return service; + } + + /// + /// Get all services of a type + /// + /// + /// + /// thrown if no services are found + public static T[] GetAll() + { + var services = Locator.Current.GetServices().ToArray(); + + if (services == null || services.Count() == 0) + throw new InvalidOperationException($"Could not locate service of type '{nameof(T)}'"); + + return services; + } + } +} diff --git a/Aki.Helper/ZipHelper.cs b/SPTInstaller/Helpers/ZipHelper.cs similarity index 75% rename from Aki.Helper/ZipHelper.cs rename to SPTInstaller/Helpers/ZipHelper.cs index aabcf52..4462ae5 100644 --- a/Aki.Helper/ZipHelper.cs +++ b/SPTInstaller/Helpers/ZipHelper.cs @@ -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 progress = null) + public static Result Decompress(FileInfo ArchivePath, DirectoryInfo OutputFolderPath, IProgress 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); } } } diff --git a/Aki.Core/Tasks/CopyClientTask.cs b/SPTInstaller/Installer Tasks/CopyClientTask.cs similarity index 52% rename from Aki.Core/Tasks/CopyClientTask.cs rename to SPTInstaller/Installer Tasks/CopyClientTask.cs index 99e5dc4..2940237 100644 --- a/Aki.Core/Tasks/CopyClientTask.cs +++ b/SPTInstaller/Installer Tasks/CopyClientTask.cs @@ -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 RunAsync() + public override async Task 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((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); }); } } } diff --git a/Aki.Core/Tasks/DownloadTask.cs b/SPTInstaller/Installer Tasks/DownloadTask.cs similarity index 65% rename from Aki.Core/Tasks/DownloadTask.cs rename to SPTInstaller/Installer Tasks/DownloadTask.cs index 7afa1fd..6603e26 100644 --- a/Aki.Core/Tasks/DownloadTask.cs +++ b/SPTInstaller/Installer Tasks/DownloadTask.cs @@ -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 BuildMirrorList() + private async Task BuildMirrorList() { - SetStatus("Downloading Mirror List", false); - - var progress = new Progress((d) => { Progress = (int)Math.Floor(d); }); + var progress = new Progress((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>(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 DownloadPatcherFromMirrors(IProgress progress) + private async Task DownloadPatcherFromMirrors(IProgress 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 RunAsync() + public override async Task TaskOperation() { + var progress = new Progress((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((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((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(); } } } \ No newline at end of file diff --git a/Aki.Core/Tasks/IntializationTask.cs b/SPTInstaller/Installer Tasks/IntializationTask.cs similarity index 50% rename from Aki.Core/Tasks/IntializationTask.cs rename to SPTInstaller/Installer Tasks/IntializationTask.cs index 501be14..b91f68b 100644 --- a/Aki.Core/Tasks/IntializationTask.cs +++ b/SPTInstaller/Installer Tasks/IntializationTask.cs @@ -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 RunAsync() + public override async Task 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}"); } } } diff --git a/SPTInstaller/Installer Tasks/PreChecks/NetCore6PreCheck.cs b/SPTInstaller/Installer Tasks/PreChecks/NetCore6PreCheck.cs new file mode 100644 index 0000000..fa6ffdd --- /dev/null +++ b/SPTInstaller/Installer Tasks/PreChecks/NetCore6PreCheck.cs @@ -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 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; + } + } +} diff --git a/SPTInstaller/Installer Tasks/PreChecks/NetFramework472PreCheck.cs b/SPTInstaller/Installer Tasks/PreChecks/NetFramework472PreCheck.cs new file mode 100644 index 0000000..f190460 --- /dev/null +++ b/SPTInstaller/Installer Tasks/PreChecks/NetFramework472PreCheck.cs @@ -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 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; + } + } + } +} diff --git a/Aki.Core/Tasks/ReleaseCheckTask.cs b/SPTInstaller/Installer Tasks/ReleaseCheckTask.cs similarity index 70% rename from Aki.Core/Tasks/ReleaseCheckTask.cs rename to SPTInstaller/Installer Tasks/ReleaseCheckTask.cs index 76bfd05..83ff7dd 100644 --- a/Aki.Core/Tasks/ReleaseCheckTask.cs +++ b/SPTInstaller/Installer Tasks/ReleaseCheckTask.cs @@ -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 RunAsync() + public override async Task 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}"); } } } diff --git a/Aki.Core/Tasks/SetupClientTask.cs b/SPTInstaller/Installer Tasks/SetupClientTask.cs similarity index 62% rename from Aki.Core/Tasks/SetupClientTask.cs rename to SPTInstaller/Installer Tasks/SetupClientTask.cs index 0657e9d..7d55f97 100644 --- a/Aki.Core/Tasks/SetupClientTask.cs +++ b/SPTInstaller/Installer Tasks/SetupClientTask.cs @@ -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 RunAsync() + public override async Task 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((d) => { SetStatus("", (int)Math.Floor(d)); }); + + if (_data.PatchNeeded) { // extract patcher files - SetStatus("Extrating Patcher", false); + SetStatus("Extrating Patcher", 0); - var extractPatcherProgress = new Progress((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((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((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!"); } } } diff --git a/SPTInstaller/Installer Tasks/TestTask.cs b/SPTInstaller/Installer Tasks/TestTask.cs new file mode 100644 index 0000000..2ce0d1a --- /dev/null +++ b/SPTInstaller/Installer Tasks/TestTask.cs @@ -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 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(); + } + } +} diff --git a/SPTInstaller/Interfaces/IPreCheck.cs b/SPTInstaller/Interfaces/IPreCheck.cs new file mode 100644 index 0000000..0618875 --- /dev/null +++ b/SPTInstaller/Interfaces/IPreCheck.cs @@ -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 RunCheck(); + } +} diff --git a/SPTInstaller/Interfaces/IProgressableTask.cs b/SPTInstaller/Interfaces/IProgressableTask.cs new file mode 100644 index 0000000..3eb58b2 --- /dev/null +++ b/SPTInstaller/Interfaces/IProgressableTask.cs @@ -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 RunAsync(); + } +} diff --git a/SPTInstaller/Interfaces/IResult.cs b/SPTInstaller/Interfaces/IResult.cs new file mode 100644 index 0000000..43d9a75 --- /dev/null +++ b/SPTInstaller/Interfaces/IResult.cs @@ -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; } + } +} diff --git a/Aki.Core/Model/DownloadMirror.cs b/SPTInstaller/Models/DownloadMirror.cs similarity index 73% rename from Aki.Core/Model/DownloadMirror.cs rename to SPTInstaller/Models/DownloadMirror.cs index 9756b1f..c3dbc2c 100644 --- a/Aki.Core/Model/DownloadMirror.cs +++ b/SPTInstaller/Models/DownloadMirror.cs @@ -1,4 +1,4 @@ -namespace SPT_AKI_Installer.Aki.Core.Model +namespace SPTInstaller.Models { public class DownloadMirror { diff --git a/SPTInstaller/Models/InstallerTaskBase.cs b/SPTInstaller/Models/InstallerTaskBase.cs new file mode 100644 index 0000000..22c410f --- /dev/null +++ b/SPTInstaller/Models/InstallerTaskBase.cs @@ -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); + } + + /// + /// Update the status message and optionally a progress bar value + /// + /// + /// + /// 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 + public void SetStatus(string message, int? progress = null) => SetStatus(message, "", progress); + + /// + /// Update the status message, status details, and optionlly a progress bar value + /// + /// + /// + /// If message or details are empty, it isn't updated. If progress is null, the progress bar will be hidden + 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(); + } + + /// + /// A method for the install controller to call. Do not use this within your task + /// + /// + public async Task RunAsync() + { + IsRunning = true; + + var result = await TaskOperation(); + + IsRunning = false; + + if (!result.Succeeded) + { + // TODO: handle error state + } + + IsCompleted = true; + + return result; + } + + /// + /// The task you want to run + /// + /// + public abstract Task TaskOperation(); + } +} diff --git a/Aki.Core/InternalData.cs b/SPTInstaller/Models/InternalData.cs similarity index 93% rename from Aki.Core/InternalData.cs rename to SPTInstaller/Models/InternalData.cs index 7672384..0ea2914 100644 --- a/Aki.Core/InternalData.cs +++ b/SPTInstaller/Models/InternalData.cs @@ -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 { diff --git a/SPTInstaller/Models/PreCheckBase.cs b/SPTInstaller/Models/PreCheckBase.cs new file mode 100644 index 0000000..416986f --- /dev/null +++ b/SPTInstaller/Models/PreCheckBase.cs @@ -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); + } + + /// + /// Base class for pre-checks to run before installation + /// + /// The display name of the pre-check + /// If installation should stop on failing this pre-check + public PreCheckBase(string name, bool required) + { + Name = name; + IsRequired = required; + Id = Guid.NewGuid().ToString(); + } + + public async Task RunCheck() + { + IsRunning = true; + Passed = await CheckOperation(); + IsRunning = false; + IsPending = false; + + return Passed ? Result.FromSuccess() : Result.FromError($"PreCheck Failed: {Name}"); + } + + public abstract Task CheckOperation(); + } +} diff --git a/SPTInstaller/Models/Result.cs b/SPTInstaller/Models/Result.cs new file mode 100644 index 0000000..bb520d0 --- /dev/null +++ b/SPTInstaller/Models/Result.cs @@ -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); + } +} diff --git a/SPTInstaller/Program.cs b/SPTInstaller/Program.cs new file mode 100644 index 0000000..02ae679 --- /dev/null +++ b/SPTInstaller/Program.cs @@ -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(); + ServiceHelper.Register(); + ServiceHelper.Register(); +#if !TEST + ServiceHelper.Register(); + ServiceHelper.Register(); + ServiceHelper.Register(); + ServiceHelper.Register(); + ServiceHelper.Register(); +#else + for(int i = 0; i < 5; i++) + { + Locator.CurrentMutable.RegisterConstant(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().ToArray() as IProgressableTask[]; + var preChecks = Locator.Current.GetServices().ToArray() as IPreCheck[]; + + var installer = new InstallController(tasks, preChecks); + + // manually register install controller + Locator.CurrentMutable.RegisterConstant(installer); + + return AppBuilder.Configure() + .UsePlatformDetect() + .LogToTrace() + .UseReactiveUI(); + } + } +} \ No newline at end of file diff --git a/shared/Gitea.dll b/SPTInstaller/Resources/Gitea.dll similarity index 100% rename from shared/Gitea.dll rename to SPTInstaller/Resources/Gitea.dll diff --git a/SPTInstaller/Roots.xml b/SPTInstaller/Roots.xml new file mode 100644 index 0000000..407eac0 --- /dev/null +++ b/SPTInstaller/Roots.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/SPTInstaller/SPTInstaller.csproj b/SPTInstaller/SPTInstaller.csproj new file mode 100644 index 0000000..c856e40 --- /dev/null +++ b/SPTInstaller/SPTInstaller.csproj @@ -0,0 +1,42 @@ + + + WinExe + net6.0 + true + enable + true + app.manifest + icon.ico + Assets\icon.ico + Debug;Release;TEST + + + + + + + + + + + + + + + + + + + + + + + + + + + + Z:\dev_stuff\EftPatchHelper\EftPatchHelper\EftPatchHelper\Resources\Gitea.dll + + + diff --git a/SPTInstaller/ViewLocator.cs b/SPTInstaller/ViewLocator.cs new file mode 100644 index 0000000..bf62b2d --- /dev/null +++ b/SPTInstaller/ViewLocator.cs @@ -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; + } + } +} \ No newline at end of file diff --git a/SPTInstaller/ViewModels/InstallViewModel.cs b/SPTInstaller/ViewModels/InstallViewModel.cs new file mode 100644 index 0000000..d4fc310 --- /dev/null +++ b/SPTInstaller/ViewModels/InstallViewModel.cs @@ -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 MyTasks { get; set; } + = new ObservableCollection(ServiceHelper.GetAll()); + + public InstallViewModel(IScreen host) : base(host) + { + var installer = ServiceHelper.Get(); + + installer.TaskChanged += (sender, task) => CurrentTask = task; + + Task.Run(async () => + { + var result = await installer.RunTasks(); + + NavigateTo(new MessageViewModel(HostScreen, result.Message)); + }); + } + } +} diff --git a/SPTInstaller/ViewModels/MainWindowViewModel.cs b/SPTInstaller/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..5663d15 --- /dev/null +++ b/SPTInstaller/ViewModels/MainWindowViewModel.cs @@ -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; + } + } + + } +} \ No newline at end of file diff --git a/SPTInstaller/ViewModels/MessageViewModel.cs b/SPTInstaller/ViewModels/MessageViewModel.cs new file mode 100644 index 0000000..7841c84 --- /dev/null +++ b/SPTInstaller/ViewModels/MessageViewModel.cs @@ -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; + } + } +} diff --git a/SPTInstaller/ViewModels/PreChecksViewModel.cs b/SPTInstaller/ViewModels/PreChecksViewModel.cs new file mode 100644 index 0000000..014235d --- /dev/null +++ b/SPTInstaller/ViewModels/PreChecksViewModel.cs @@ -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 PreChecks { get; set; } + = new ObservableCollection(ServiceHelper.GetAll()); + + ICommand StartInstallCommand { get; set; } + + public PreChecksViewModel(IScreen host) : base(host) + { + var data = ServiceHelper.Get(); + var installer = ServiceHelper.Get(); + + 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(); + }); + } + } +} diff --git a/SPTInstaller/ViewModels/ViewModelBase.cs b/SPTInstaller/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..8d85c91 --- /dev/null +++ b/SPTInstaller/ViewModels/ViewModelBase.cs @@ -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; } + + /// + /// Delay the return of the viewmodel + /// + /// The amount of time in milliseconds to delay + /// The viewmodel after the delay time + /// Useful to delay the navigation to another view. For instance, to allow an animation to complete. + private async Task WithDelay(int Milliseconds) + { + await Task.Delay(Milliseconds); + + return this; + } + + /// + /// Navigate to another viewmodel after a delay + /// + /// + /// + /// + public async Task NavigateToWithDelay(ViewModelBase ViewModel, int Milliseconds) + { + await Dispatcher.UIThread.InvokeAsync(async () => + { + HostScreen.Router.Navigate.Execute(await ViewModel.WithDelay(Milliseconds)); + }); + } + + /// + /// Navigate to another viewmodel + /// + /// + public void NavigateTo(ViewModelBase ViewModel) + { + Dispatcher.UIThread.InvokeAsync(() => + { + HostScreen.Router.Navigate.Execute(ViewModel); + }); + } + + public ViewModelBase(IScreen Host) + { + HostScreen = Host; + } + } + +} \ No newline at end of file diff --git a/SPTInstaller/Views/InstallView.axaml b/SPTInstaller/Views/InstallView.axaml new file mode 100644 index 0000000..2e5395b --- /dev/null +++ b/SPTInstaller/Views/InstallView.axaml @@ -0,0 +1,24 @@ + + + + + + + diff --git a/SPTInstaller/Views/InstallView.axaml.cs b/SPTInstaller/Views/InstallView.axaml.cs new file mode 100644 index 0000000..813f5f0 --- /dev/null +++ b/SPTInstaller/Views/InstallView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia.Controls; +using Avalonia.ReactiveUI; +using SPTInstaller.ViewModels; + +namespace SPTInstaller.Views +{ + public partial class InstallView : ReactiveUserControl + { + public InstallView() + { + InitializeComponent(); + } + } +} diff --git a/SPTInstaller/Views/MainWindow.axaml b/SPTInstaller/Views/MainWindow.axaml new file mode 100644 index 0000000..7534f05 --- /dev/null +++ b/SPTInstaller/Views/MainWindow.axaml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + diff --git a/SPTInstaller/Views/MainWindow.axaml.cs b/SPTInstaller/Views/MainWindow.axaml.cs new file mode 100644 index 0000000..edc3580 --- /dev/null +++ b/SPTInstaller/Views/MainWindow.axaml.cs @@ -0,0 +1,12 @@ +using Avalonia.Controls; + +namespace SPTInstaller.Views +{ + public partial class MainWindow : Window + { + public MainWindow() + { + InitializeComponent(); + } + } +} \ No newline at end of file diff --git a/SPTInstaller/Views/MessageView.axaml b/SPTInstaller/Views/MessageView.axaml new file mode 100644 index 0000000..79e7ac7 --- /dev/null +++ b/SPTInstaller/Views/MessageView.axaml @@ -0,0 +1,26 @@ + + + + + +