Compare commits

..

65 Commits

Author SHA1 Message Date
Dev
e93f0d62a6 Fixed typos 2024-07-18 13:58:00 +01:00
Dev
e4b8fe10ab Adjusted error copy text 2024-07-18 13:57:19 +01:00
Dev
34828b3240 Improved capitalization for error message 2024-07-18 09:36:21 +01:00
Dev
d2d1e584c1 Improved EFT not installed error to give user action point on how to remedy issue 2024-07-18 09:35:29 +01:00
7ea9b42452 Merge pull request 'bump version' (#104) from waffle.lord/Installer:master into master
Reviewed-on: SPT/Installer#104
2024-07-18 01:55:47 +00:00
c258d4ea92 bump version 2024-07-17 21:47:16 -04:00
Dev
65a63e09ed Improved error message text 2024-07-17 22:43:01 +01:00
Dev
16576d3a81 Fixed typo 2024-07-16 12:04:00 +01:00
0a0615813c Merge pull request 'use continue instead of break' (#102) from waffle.lord/Installer:fix/bak-file-filtering into master
Reviewed-on: SPT/Installer#102
2024-07-10 21:29:43 +00:00
a0a6e6dd81 use continue instead of break
debug log is now copied to install folder; copy button now copies
patcher log as well
2024-07-10 17:28:28 -04:00
944e34fa6b Merge pull request 'write debug log to seperate file' (#101) from waffle.lord/Installer:fix/debug-logger into master
Reviewed-on: SPT/Installer#101
2024-07-10 17:34:30 +00:00
c2c1670ab2 write debug log to seperate file 2024-07-10 13:33:19 -04:00
b2b64dcae0 Merge pull request 'add overview page' (#100) from waffle.lord/Installer:impl/overview-page into master
Reviewed-on: SPT/Installer#100
2024-07-10 17:32:10 +00:00
2ffa8f6b6d add overview page
also update some checks order
2024-07-10 13:00:52 -04:00
45e988f7f8 Merge pull request 'add installpath param' (#99) from waffle.lord/Installer:impl/install-path-param into master
Reviewed-on: SPT/Installer#99
2024-07-06 21:08:30 +00:00
ca2dec269f add installpath param
added relaunch method for ease of restarting the installer
2024-07-06 17:06:21 -04:00
498eeaa0ef Merge pull request 'fix and improve the update script' (#98) from waffle.lord/Installer:fix/update-script into master
Reviewed-on: SPT/Installer#98
2024-07-05 13:10:27 +00:00
e751f937a2 fix and improve the update script
for real this time I swear :'(
2024-07-05 09:09:05 -04:00
6502dde280 Merge pull request 'don't copy log when in install dir' (#97) from waffle.lord/Installer:fix/log-copy-error into master
Reviewed-on: SPT/Installer#97
2024-07-05 01:55:47 +00:00
a61e53d56a don't copy log when in install dir 2024-07-04 21:55:18 -04:00
a70590d92a Merge pull request 'add shortcuts, open folder, etc' (#96) from waffle.lord/Installer:impl/shortcuts into master
Reviewed-on: SPT/Installer#96
2024-07-04 16:26:28 +00:00
43fa10e3a5 add shortcuts, open folder, etc 2024-07-04 12:25:19 -04:00
f7fbdee568 Merge pull request 'fix/uri-path' (#95) from waffle.lord/Installer:fix/uri-path into master
Reviewed-on: SPT/Installer#95
2024-06-30 16:39:39 +00:00
74ef0c096a bump version 2024-06-30 12:16:14 -04:00
b9a5ad9442 add some margin to update changelog 2024-06-30 12:15:45 -04:00
ae08be8367 add eft install precheck
also updated wording of free space precheck when eft install isn't found
2024-06-30 12:12:08 -04:00
ff717aab73 fix path being url encoded 2024-06-30 11:50:12 -04:00
0f1d4653a2 Merge pull request 'fix/error-messages' (#94) from waffle.lord/Installer:fix/error-messages into master
Reviewed-on: SPT/Installer#94
2024-06-29 15:43:19 +00:00
f8e4a668ff fix changelog loading 2024-06-29 11:34:04 -04:00
469f74ee6c fix error messages 2024-06-29 11:25:30 -04:00
36ee8182f9 Merge pull request 'impl/path-selection' (#93) from waffle.lord/Installer:impl/path-selection into master
Reviewed-on: SPT/Installer#93
2024-06-29 15:11:43 +00:00
bb8b05a2d4 add update page 2024-06-29 11:05:35 -04:00
1dc4202353 add path selection page
new update page WIP
2024-06-28 21:35:27 -04:00
0c6ce9e681 Merge pull request 'Extended out of date client messages' (#91) from AdjustOutOfDateMessages into master
Reviewed-on: SPT/Installer#91
2024-06-09 20:17:52 +00:00
ccc91b09f6 Merge branch 'master' into AdjustOutOfDateMessages 2024-06-09 20:17:34 +00:00
9b3d9fd755 Merge pull request 'add copy to clipboard button' (#92) from waffle.lord/Installer:impl/log-file-clipboard into master
Reviewed-on: SPT/Installer#92
2024-06-09 20:16:14 +00:00
2110b185d2 add copy to clipboard button 2024-06-09 16:14:57 -04:00
Dev
918106a469 Extended out of date client messages 2024-06-09 17:15:47 +01:00
074ac6e826 Merge pull request 'remove trailing slash from eft game path' (#90) from waffle.lord/Installer:fix/eft-path-ending-slash into master
Reviewed-on: SPT/Installer#90
2024-06-09 14:52:44 +00:00
a909013a86 remove trailing slash from eft game path 2024-06-09 10:43:43 -04:00
2929c859b0 Merge pull request 'bump version' (#89) from waffle.lord/Installer:master into master
Reviewed-on: SPT/Installer#89
2024-06-04 13:56:23 +00:00
42902f0bd6 bump version 2024-06-04 09:55:45 -04:00
Dev
24094054ba Updated Installer icon 2024-06-04 13:51:24 +01:00
886b0985e1 Merge pull request 'bump version' (#88) from waffle.lord/Installer:2.68 into master
Reviewed-on: SPT/Installer#88
2024-05-26 02:57:48 +00:00
e57fa4e64a bump version 2024-05-25 22:57:22 -04:00
Dev
43fee371de Altered localization text to assist with support 2024-05-25 17:34:36 +01:00
67492b834f Merge pull request 'get known downloads folder' (#87) from waffle.lord/Installer:fix/get-known-folders into master
Reviewed-on: SPT/Installer#87
2024-05-24 16:25:53 +00:00
ed050ea820 get known downloads folder
also update icon
2024-05-24 12:24:25 -04:00
CWX
faa581e2b4 Merge pull request 'rebrand - ico and release Field to change' (#86) from rebrand into master
Reviewed-on: CWX/SPT-Installer#86
2024-05-21 23:38:05 +00:00
CWX
9748461024 rebrand - ico and release Field to change 2024-05-21 23:07:51 +01:00
1e7a6fd8c5 Merge pull request 'Fix typo' (#85) from Schrader/SPT-AKI-Installer:bugfix/typo into master
Reviewed-on: CWX/SPT-AKI-Installer#85

LGTM!
2024-05-21 14:34:40 +00:00
Philipp Heenemann
9fe08cd794 Fix typo 2024-05-17 07:57:00 +02:00
51a2190559 Merge pull request 'add clear cache metadata button' (#83) from waffle.lord/SPT-AKI-Installer:add-clear-metadata-cache-button into master
Reviewed-on: CWX/SPT-AKI-Installer#83
2024-05-07 18:21:59 +00:00
8711bca159 add clear cache metadata button 2024-05-07 14:19:03 -04:00
080cc0ab35 Merge pull request 'delete downloaded files if the download fails' (#82) from waffle.lord/SPT-AKI-Installer:impr/remove-failed-downloads into master
Reviewed-on: CWX/SPT-AKI-Installer#82
2024-05-06 19:25:09 +00:00
5715f97956 delete downloaded files if the download fails 2024-05-06 15:18:54 -04:00
2bd28f0796 Merge pull request 'fix update script thing' (#81) from waffle.lord/SPT-AKI-Installer:fix/update-script into master
Reviewed-on: CWX/SPT-AKI-Installer#81
2024-05-04 21:59:36 +00:00
5c76864595 Merge branch 'fix/update-script' of https://dev.sp-tarkov.com/waffle.lord/SPT-AKI-Installer into fix/update-script 2024-05-04 17:59:19 -04:00
3372db8b2b Merge remote-tracking branch 'upstream/master' into fix/update-script 2024-05-04 17:59:06 -04:00
73262db871 Merge branch 'master' into fix/update-script 2024-05-04 21:58:36 +00:00
a4131fe276 fix update script thing 2024-05-04 17:57:59 -04:00
90be28a1f0 Merge pull request 'impr/r2-hits' (#80) from waffle.lord/SPT-AKI-Installer:impr/r2-hits into master
Reviewed-on: CWX/SPT-AKI-Installer#80
2024-05-04 20:21:25 +00:00
b51c373e96 version bump 2024-05-04 16:19:52 -04:00
1bfb6e9946 simplify file copies 2024-05-04 16:19:04 -04:00
64d0b4b35e use cache ttl for metadata files 2024-05-04 16:18:49 -04:00
55 changed files with 1246 additions and 419 deletions

View File

@ -19,7 +19,12 @@
<entry key="SPTInstaller/CustomControls/UpdateButton.axaml" value="SPTInstaller/SPTInstaller.csproj" />
<entry key="SPTInstaller/CustomControls/UpdateInfoCard.axaml" value="SPTInstaller/SPTInstaller.csproj" />
<entry key="SPTInstaller/Views/DetailedPreChecksView.axaml" value="SPTInstaller/SPTInstaller.csproj" />
<entry key="SPTInstaller/Views/InstallPathSelectionView.axaml" value="SPTInstaller/SPTInstaller.csproj" />
<entry key="SPTInstaller/Views/InstallView.axaml" value="SPTInstaller/SPTInstaller.csproj" />
<entry key="SPTInstaller/Views/InstallerUpdateView.axaml" value="SPTInstaller/SPTInstaller.csproj" />
<entry key="SPTInstaller/Views/MainWindow.axaml" value="SPTInstaller/SPTInstaller.csproj" />
<entry key="SPTInstaller/Views/MessageView.axaml" value="SPTInstaller/SPTInstaller.csproj" />
<entry key="SPTInstaller/Views/OverviewView.axaml" value="SPTInstaller/SPTInstaller.csproj" />
<entry key="SPTInstaller/Views/PreChecksView.axaml" value="SPTInstaller/SPTInstaller.csproj" />
</map>
</option>

View File

@ -1,4 +1,4 @@
# SPT-AKI Installer made for EFT.
# SPT Installer made for EFT.
<img src="https://i.imgur.com/jtlwLsr.png" alt="spt installer 2.59" width="700"/>
@ -9,12 +9,12 @@
- Checks if there is enough space before install
- Checks installer is not in a problematic path
- Checks install folder does not have game files already in it
- Checks if gameversion matches aki version, if so skip patcher process
- Checks if gameversion matches SPT version, if so skip patcher process
- Checks both zips are there, other than when the above match, patcher isnt checked for
- downloads both Zips from the Repo's if needed
### Installer Processes:
- Copies files from registry logged GamePath to new location
- Extracts, runs and deletes patcher with no user input
- Extracts Aki
- Deletes both Patcher and AKI zips at the end
- Extracts SPT
- Deletes both Patcher and SPT zips at the end

View File

@ -16,21 +16,21 @@
<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>
<Color x:Key="AKI_LightGrayBlue">#444259</Color>
<Color x:Key="SPT_DarkGray">#121212</Color>
<Color x:Key="SPT_Yellow">#FFC107</Color>
<Color x:Key="SPT_White">#FFFFFF</Color>
<Color x:Key="SPT_Gray">#282828</Color>
<Color x:Key="SPT_DarkGrayBlue">#323947</Color>
<Color x:Key="SPT_LightGrayBlue">#444259</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_LightGrayBlue" Color="{StaticResource AKI_LightGrayBlue}" />
<SolidColorBrush x:Key="AKI_Brush_Lighter" Color="Gainsboro" />
<SolidColorBrush x:Key="SPT_Foreground_Light" Color="{StaticResource SPT_White}" />
<SolidColorBrush x:Key="SPT_Background_Light" Color="{StaticResource SPT_Gray}" />
<SolidColorBrush x:Key="SPT_Background_Dark" Color="{StaticResource SPT_DarkGray}" />
<SolidColorBrush x:Key="SPT_Brush_Yellow" Color="{StaticResource SPT_Yellow}" />
<SolidColorBrush x:Key="SPT_Brush_DarkGrayBlue" Color="{StaticResource SPT_DarkGrayBlue}" />
<SolidColorBrush x:Key="SPT_Brush_LightGrayBlue" Color="{StaticResource SPT_LightGrayBlue}" />
<SolidColorBrush x:Key="SPT_Brush_Lighter" Color="Gainsboro" />
<!-- Path Geometry -->
<PathGeometry x:Key="CircledCheck"
@ -48,5 +48,8 @@
<PathGeometry x:Key="Bug"
Figures="m 12.25 0 a 0.75 0.75 0 0 1 0.743 0.648 L 13 0.75 v 0.752 c 0 0.633 -0.196 1.22 -0.53 1.704 a 3.75 3.75 0 0 1 2.521 3.29 h 0.256 a 2.25 2.25 0 0 0 2.24 -2.259 L 17.481 2.752 a 0.750006 0.750006 0 0 1 1.5 -0.006 l 0.007 1.485 a 3.75 3.75 0 0 1 -3.536 3.76 L 15.238 7.997 L 15 7.996 v 1.502 h 4.253 a 0.75 0.75 0 0 1 0.743 0.649 l 0.007 0.102 a 0.75 0.75 0 0 1 -0.648 0.743 l -0.102 0.007 H 15 v 1.999 h 0.238 l 0.214 0.007 a 3.75 3.75 0 0 1 3.531 3.56 l 0.005 0.2 l -0.007 1.485 a 0.75 0.75 0 0 1 -1.493 0.095 l -0.007 -0.102 l 0.007 -1.485 a 2.25 2.25 0 0 0 -2.087 -2.253 l -0.154 -0.006 h -0.476 a 5.002 5.002 0 0 1 -9.542 0 H 4.74 A 2.25 2.25 0 0 0 2.5 16.758 l 0.005 1.485 a 0.750008 0.750008 0 1 1 -1.5 0.007 L 1 16.764 a 3.75 3.75 0 0 1 3.535 -3.76 L 4.75 12.999 L 5 12.998 v -2 H 0.75 A 0.75 0.75 0 0 1 0.007 10.35 L 0 10.249 A 0.75 0.75 0 0 1 0.648 9.506 L 0.75 9.499 L 5 9.498 V 7.996 H 4.75 L 4.535 7.991 A 3.75 3.75 0 0 1 1.005 4.431 L 1 4.23 L 1.006 2.745 A 0.75 0.75 0 0 1 2.5 2.649 L 2.506 2.751 L 2.5 4.237 A 2.25 2.25 0 0 0 4.587 6.491 L 4.741 6.497 H 5.009 A 3.753 3.753 0 0 1 7.53 3.205 A 2.968 2.968 0 0 1 7.006 1.711 L 7 1.502 V 0.75 A 0.75 0.75 0 0 1 8.493 0.648 L 8.5 0.75 v 0.752 a 1.5 1.5 0 0 0 2.993 0.145 L 11.5 1.502 V 0.75 A 0.75 0.75 0 0 1 12.25 0 Z"
FillRule="NonZero" />
<PathGeometry x:Key="OpenFolder" Figures="M 2.2731724 14.474999 C 2.5381753 14.186249 3.2824783 12.195001 3.9271792 10.05 5.6676413 4.2592679 4.7621113 4.8000009 12.719033 4.8000009 c 5.6684 0 6.78597 0.072438 7.12511 0.4618343 0.332844 0.3821726 0.17704 1.1971998 -0.903259 4.7250006 -0.763041 2.4917722 -1.52781 4.4189802 -1.840552 4.6381652 C 16.708149 14.899859 14.592619 15 9.1783054 15 2.1694393 15 1.8160107 14.973129 2.2731724 14.474999 Z M 0.36305228 14.025959 C 0.11166709 13.786409 0 11.721164 0 7.3114288 0 1.9218189 0.0760474 0.8703905 0.49472143 0.47142828 0.8806724 0.10364926 1.7051307 0 4.2446088 0 7.4749739 0 7.5058294 0.00685701 8.2944922 0.89999983 L 9.0892098 1.8 h 3.6407872 c 3.221023 0 3.71338 0.069177 4.270431 0.5999996 0.346306 0.3300009 0.629646 0.802501 0.629646 1.0500009 0 0.3838238 -0.858607 0.4500002 -5.83853 0.4500002 -5.6986082 0 -5.856156 0.016794 -6.5739181 0.7007613 C 4.8131633 4.9861817 4.2426547 6.0999322 3.9498292 7.0757619 2.3566037 12.385128 1.8127023 13.81777 1.2887903 14.084957 c -0.37832867 0.192941 -0.68163535 0.173611 -0.92573802 -0.059 z"
FillRule="NonZero" />
</Application.Resources>
</Application>

View File

@ -1,4 +1,5 @@
using System.Linq;
using System.Diagnostics;
using System.Linq;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
@ -7,12 +8,35 @@ using Serilog;
using SPTInstaller.ViewModels;
using SPTInstaller.Views;
using System.Reactive;
using System.Text;
using SPTInstaller.Helpers;
using SPTInstaller.Models;
namespace SPTInstaller;
public partial class App : Application
{
private readonly string _logPath = Path.Join(Environment.CurrentDirectory, "spt-aki-installer_.log");
public static string LogPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "spt-installer", "spt-installer.log");
public static string LogDebugPath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"spt-installer", "spt-isntaller-debug.log");
public static void ReLaunch(bool debug, string installPath = "")
{
var installerPath = Path.Join(Environment.CurrentDirectory, "SPTInstaller.exe");
var args = new StringBuilder()
.Append(debug ? "debug " : "")
.Append(!string.IsNullOrEmpty(installPath) ? $"installPath=\"{installPath}\"" : "")
.ToString();
Process.Start(new ProcessStartInfo()
{
FileName = installerPath,
Arguments = args
});
Environment.Exit(0);
}
public override void Initialize()
{
@ -21,9 +45,8 @@ public partial class App : Application
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo
.File(path: _logPath,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information,
rollingInterval: RollingInterval.Day)
.File(path: LogPath,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information)
.CreateLogger();
RxApp.DefaultExceptionHandler = Observer.Create<Exception>((exception) =>
@ -36,25 +59,38 @@ public partial class App : Application
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var debug = desktop.Args != null && desktop.Args.Any(x => x.ToLower() == "debug");
if (debug)
var data = ServiceHelper.Get<InternalData>() ?? throw new Exception("failed to get internal data");
data.DebugMode = false;
var providedPath = "";
if (desktop.Args != null)
{
data.DebugMode = desktop.Args.Any(x => x.ToLower() == "debug");
var installPath = desktop.Args.FirstOrDefault(x => x.StartsWith("installPath=", StringComparison.CurrentCultureIgnoreCase));
providedPath = installPath != null && installPath.Contains('=') ? installPath?.Split('=')[1] ?? "" : "";
}
if (data.DebugMode)
{
Log.CloseAndFlush();
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo
.File(path: _logPath,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Debug,
rollingInterval: RollingInterval.Day)
.File(path: LogDebugPath,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Debug)
.CreateLogger();
System.Diagnostics.Trace.Listeners.Add(new SerilogTraceListener.SerilogTraceListener());
Trace.Listeners.Add(new SerilogTraceListener.SerilogTraceListener());
Log.Debug("TraceListener is registered");
}
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(debug),
DataContext = new MainWindowViewModel(providedPath),
};
}

View File

@ -2,13 +2,14 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="using:SPTInstaller.CustomControls">
<Design.PreviewWith>
<StackPanel Spacing="5" Background="{StaticResource AKI_Background_Dark}">
<StackPanel Spacing="5" Background="{StaticResource SPT_Background_Dark}">
<Button Classes="icon" x:Name="testBtn">
<Path Data="{StaticResource Bug}"
Fill="{Binding ElementName=testBtn, Path=Foreground}" />
</Button>
<TextBox Text="Some cool text here" Margin="5" />
<TextBox Watermark="This is a watermark" Margin="5" />
<CheckBox Content="sldkflskdf" />
</StackPanel>
</Design.PreviewWith>
@ -16,30 +17,30 @@
<!-- 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}" />
<Setter Property="Background" Value="{StaticResource SPT_Background_Dark}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Foreground_Light}" />
<Setter Property="ButtonForeground" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
</Style>
<Style Selector="cc|TitleBar.versiontag">
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_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="Background" Value="{StaticResource SPT_Background_Light}" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Lighter}" />
</Style>
<Style Selector="TextBox:focus">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Lighter}" />
</Style>
<Style Selector="TextBox:pointerover">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Lighter}" />
</Style>
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
@ -64,23 +65,23 @@
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<!-- TextBlock Styles -->
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Foreground_Light}" />
</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}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Foreground_Light}" />
</Style>
<Style Selector="Label.yellow">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
</Style>
<Style Selector="Label.dark">
@ -94,8 +95,8 @@
<!-- 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}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="Background" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
</Style>
<Style Selector="ProgressBar.error">
@ -103,7 +104,7 @@
<Style.Animations>
<Animation Duration="0:0:0.5" FillMode="Forward">
<KeyFrame Cue="0%">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="Value" Value="0" />
</KeyFrame>
<KeyFrame Cue="100%">
@ -117,36 +118,36 @@
<!-- 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}" />
<Setter Property="Background" Value="{StaticResource SPT_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_White}" />
<Setter Property="Background" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource SPT_White}" />
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_LightGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_LightGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource AKI_White}" />
<Setter Property="Background" Value="{StaticResource SPT_LightGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_LightGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource SPT_White}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Background" Value="{StaticResource SPT_Brush_Yellow}" />
</Style>
<Style Selector="Button:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_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="Background" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Background_Dark}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
@ -156,37 +157,37 @@
<Style Selector="Button.yellow:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Gold" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Background_Dark}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button.yellow:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Background" Value="{StaticResource SPT_Brush_Lighter}" />
</Style>
<Style Selector="Button.yellow:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
</Style>
<!-- Button outlined Style -->
<Style Selector="Button.outlined">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Lighter}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style Selector="Button.outlined:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style Selector="Button.outlined:pressed /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
@ -194,7 +195,7 @@
<!-- Button Link Style -->
<Style Selector="Button.link">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Lighter}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0 0 0 1" />
@ -208,22 +209,22 @@
</Style>
<Style Selector="Button.link:pointerover TextBlock">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
</Style>
<Style Selector="Button.link:pressed TextBlock">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
</Style>
<Style Selector="Button.link:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0 0 0 1" />
</Style>
<Style Selector="Button.link:pressed /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0 0 0 1" />
@ -231,21 +232,21 @@
<!-- Button outlinedTLCorner Style -->
<Style Selector="Button.outlinedTLCorner">
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Lighter}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="BorderThickness" Value="2 2 0 0" />
</Style>
<Style Selector="Button.outlinedTLCorner:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="BorderThickness" Value="2 2 0 0" />
</Style>
<Style Selector="Button.outlinedTLCorner:pressed /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
@ -258,13 +259,39 @@
</Style>
<Style Selector="Button.icon:pointerover">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
</Style>
<Style Selector="Button.icon:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<Style Selector="Button.icon:pressed">
<Setter Property="Foreground" Value="{StaticResource AKI_DarkGrayBlue}"></Setter>
<Setter Property="Foreground" Value="{StaticResource SPT_DarkGrayBlue}"></Setter>
</Style>
<!-- Checkbox Styles -->
<Style Selector="CheckBox">
<Setter Property="Foreground" Value="White"/>
<Style.Resources>
<SolidColorBrush x:Key="CheckBoxCheckBackgroundStrokeUnchecked" Color="DimGray"/>
</Style.Resources>
</Style>
<Style Selector="CheckBox:pointerover /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="White"/>
</Style>
<Style Selector="CheckBox:checked /template/ ContentPresenter#ContentPresenter">
<Setter Property="Foreground" Value="White"/>
</Style>
<Style Selector="CheckBox:pointerover /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{StaticResource SPT_Yellow}"/>
</Style>
<Style Selector="CheckBox:checked /template/ Border#NormalRectangle">
<Setter Property="BorderBrush" Value="{StaticResource SPT_Yellow}"/>
<Setter Property="Background" Value="{StaticResource SPT_DarkGrayBlue}"/>
</Style>
<Style Selector="CheckBox:checked /template/ Path#CheckGlyph">
<Setter Property="Fill" Value="{StaticResource SPT_Yellow}"/>
</Style>
</Styles>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

View File

@ -1,23 +0,0 @@
<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:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.Dialogs.ChangeLogDialog"
MinWidth="400" MaxWidth="600">
<StackPanel>
<Label Content="{Binding Version, RelativeSource={RelativeSource AncestorType=UserControl}, StringFormat='{}Installer Change Log for {0}'}" FontSize="18" FontWeight="SemiBold"
/>
<Separator Margin="0 10" Padding="0" Background="{StaticResource AKI_Yellow}"/>
<ScrollViewer MaxHeight="250">
<TextBlock Text="{Binding Message, RelativeSource={RelativeSource AncestorType=UserControl}}"
TextWrapping="Wrap" MinHeight="100"
/>
</ScrollViewer>
<Button Content="Close" Classes="yellow"
HorizontalAlignment="Right"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}"
/>
</StackPanel>
</UserControl>

View File

@ -1,27 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Tmds.DBus.Protocol;
namespace SPTInstaller.CustomControls.Dialogs;
public partial class ChangeLogDialog : UserControl
{
public string Message { get; set; }
public string Version { get; set; }
public ChangeLogDialog(string newVersion, string message)
{
InitializeComponent();
Message = message;
Version = newVersion;
}
// public static readonly StyledProperty<string> MessageProperty =
// AvaloniaProperty.Register<ChangeLogDialog, string>("Message");
//
// public string Message
// {
// get => GetValue(MessageProperty);
// set => SetValue(MessageProperty, value);
// }
}

View File

@ -8,7 +8,7 @@
MinWidth="300" MinHeight="100"
MaxWidth="600" MaxHeight="300">
<Grid RowDefinitions="10,AUTO,*,AUTO,10" ColumnDefinitions="10,*,AUTO,10,AUTO,10"
Background="{StaticResource AKI_Background_Light}">
Background="{StaticResource SPT_Background_Light}">
<TextBlock Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="4"
Text="{Binding Message, RelativeSource={RelativeSource AncestorType=UserControl}}"
TextWrapping="Wrap" />

View File

@ -6,17 +6,16 @@
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.Dialogs.WhyCacheThoughDialog">
<Grid RowDefinitions="AUTO,AUTO,AUTO,*,AUTO" ColumnDefinitions="*,AUTO, AUTO"
Background="{StaticResource AKI_Background_Light}">
<Grid RowDefinitions="AUTO,AUTO,AUTO,*,AUTO" ColumnDefinitions="*,AUTO"
Background="{StaticResource SPT_Background_Light}">
<Label Content="What is the installer cache for?" FontSize="20"
Foreground="{StaticResource AKI_Brush_Yellow}" />
Foreground="{StaticResource SPT_Brush_Yellow}" />
<TextBlock Grid.Row="1" Grid.ColumnSpan="2" TextWrapping="Wrap" xml:space="preserve">
The installer cache is used to ensure you don't re-download large files that you've already downloaded before.
<Span Foreground="red">You should only delete the cache folder if</Span>
- You are low on space
or
- You are not planning on installing SPT again any time soon
If possible, you should leave the cache in place to avoid uneccessary, lengthy downloads.
It also helps us prevent extra traffic to our limited download mirrors. Every bit helps <Span Foreground="red"
FontSize="25">♥️</Span>
@ -25,7 +24,6 @@ It also helps us prevent extra traffic to our limited download mirrors. Every bi
<Button Grid.Row="3" Grid.ColumnSpan="2"
Content="{Binding Source={x:Static helpers:DownloadCacheHelper.CachePath}}"
Classes="link"
Margin="0 10"
IsVisible="{Binding CacheExists, RelativeSource={RelativeSource AncestorType=UserControl}}"
Command="{Binding OpenCacheFolder, RelativeSource={RelativeSource AncestorType=UserControl}}" />
<Label Grid.Row="3" Content="No cache folder exists"
@ -35,10 +33,16 @@ It also helps us prevent extra traffic to our limited download mirrors. Every bi
Content="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=AdditionalInfo}"
Foreground="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=AdditionalInfoColor}" />
<Button Grid.Row="4" Grid.Column="1" Content="Move Downloaded Patcher" Margin="0 0 10 0"
<StackPanel Orientation="Horizontal" Grid.Row="4" Grid.Column="1" Spacing="10">
<Button Content="Move Downloaded Patcher"
Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=MoveDownloadsPatcherToCache}" />
<Button Grid.Row="4" Grid.Column="2" Content="Close" Classes="yellow"
<Button Content="Clear Metadata Cache"
Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ClearCachedMetaData}"
/>
<Button Content="Close" Classes="yellow"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}" />
</StackPanel>
</Grid>
</UserControl>

View File

@ -3,7 +3,10 @@ using System.Diagnostics;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Serilog;
using SPTInstaller.Models;
using Color = System.Drawing.Color;
namespace SPTInstaller.CustomControls.Dialogs;
@ -54,13 +57,56 @@ public partial class WhyCacheThoughDialog : UserControl
});
}
public void ClearCachedMetaData()
{
var cachedMetadata =
new DirectoryInfo(DownloadCacheHelper.CachePath).GetFiles("*.json", SearchOption.TopDirectoryOnly);
var message = "no cached metadata to remove";
if (cachedMetadata.Length == 0)
{
AdditionalInfo = message;
AdditionalInfoColor = "dodgerblue";
Log.Information(message);
return;
}
var allDeleted = true;
foreach (var file in cachedMetadata)
{
try
{
file.Delete();
file.Refresh();
if (file.Exists)
{
allDeleted = false;
}
}
catch (Exception ex)
{
Log.Error(ex, $"Failed to delete cached metadata file: {file.Name}");
}
}
message = allDeleted ? "cached metadata removed" : "some files could not be removed. Check logs";
AdditionalInfo = message;
AdditionalInfoColor = allDeleted ? "green" : "red";
Log.Information(message);
var data = ServiceHelper.Get<InternalData>();
App.ReLaunch(false, data.TargetInstallPath!);
}
public void MoveDownloadsPatcherToCache()
{
switch (_movePatcherState)
{
case 0:
var downloadsPath =
Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "Downloads");
var downloadsPath = KnownFolders.GetPath(KnownFolder.Downloads);
var downloadsFolder = new DirectoryInfo(downloadsPath);

View File

@ -17,7 +17,7 @@
<Style Selector="Button.selectable">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Background_Dark}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
@ -26,18 +26,18 @@
</Style>
<Style Selector="Button.selectable:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="Background" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button.selectable:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
</Style>
<Style Selector="Button.selected">
<Setter Property="Background" Value="{StaticResource AKI_Brush_LightGrayBlue}" />
<Setter Property="Background" Value="{StaticResource SPT_Brush_LightGrayBlue}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
</UserControl.Styles>

View File

@ -62,7 +62,7 @@
<Ellipse Height="30" Width="30"
StrokeThickness="4"
Fill="{StaticResource AKI_Background_Dark}"
Fill="{StaticResource SPT_Background_Dark}"
HorizontalAlignment="Left"
Classes.running="{Binding IsRunning, RelativeSource={RelativeSource AncestorType=UserControl}}"
Classes.completed="{Binding IsCompleted, RelativeSource={RelativeSource AncestorType=UserControl}}" />

View File

@ -34,11 +34,11 @@
Width="35">
<Button.Styles>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="Background" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}" />
<Setter Property="Background" Value="{StaticResource SPT_Background_Light}" />
</Style>
</Button.Styles>
</Button>

View File

@ -26,8 +26,6 @@
<Button Content="Not now" CornerRadius="0 20 20 0"
Command="{Binding DismissCommand, RelativeSource={RelativeSource AncestorType=UserControl}}" />
</StackPanel>
<Button HorizontalAlignment="Center" Content="What's new?" Classes="link"
Command="{Binding WhatsNewCommand, RelativeSource={RelativeSource AncestorType=UserControl}}"/>
</StackPanel>
<Panel Margin="0 10">

View File

@ -50,15 +50,6 @@ public partial class UpdateButton : UserControl
set => SetValue(UpdateCommandProperty, value);
}
public static readonly StyledProperty<ICommand> WhatsNewCommandProperty =
AvaloniaProperty.Register<UpdateButton, ICommand>("WhatsNewCommand");
public ICommand WhatsNewCommand
{
get => GetValue(WhatsNewCommandProperty);
set => SetValue(WhatsNewCommandProperty, value);
}
public static readonly StyledProperty<bool> UpdatingProperty = AvaloniaProperty.Register<UpdateButton, bool>(
"Updating");

View File

@ -1,7 +1,6 @@
using System.Net.Http;
using System.Threading.Tasks;
using Serilog;
using SPTInstaller.Models;
namespace SPTInstaller.Helpers;
@ -9,6 +8,7 @@ public static class DownloadCacheHelper
{
private static HttpClient _httpClient = new() { Timeout = TimeSpan.FromMinutes(15) };
public static TimeSpan SuggestedTtl = TimeSpan.FromHours(1);
public static string CachePath = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"spt-installer/cache");
@ -50,10 +50,10 @@ public static class DownloadCacheHelper
/// <param name="expectedHash">The expected hash of the file in the cache</param>
/// <param name="cachedFile">The file found in the cache; null if no file is found</param>
/// <returns>True if the file is in the cache and its hash matches the expected hash, otherwise false</returns>
public static bool CheckCache(string fileName, string expectedHash, out FileInfo cachedFile)
=> CheckCache(new FileInfo(Path.Join(CachePath, fileName)), expectedHash, out cachedFile);
public static bool CheckCacheHash(string fileName, string expectedHash, out FileInfo cachedFile)
=> CheckCacheHash(new FileInfo(Path.Join(CachePath, fileName)), expectedHash, out cachedFile);
private static bool CheckCache(FileInfo cacheFile, string expectedHash, out FileInfo fileInCache)
private static bool CheckCacheHash(FileInfo cacheFile, string expectedHash, out FileInfo fileInCache)
{
fileInCache = cacheFile;
@ -86,6 +86,44 @@ public static class DownloadCacheHelper
}
}
/// <summary>
/// Gets a file in the cache based on a time-to-live from its last modified time
/// </summary>
/// <param name="fileName">The name of the file to look for in the cache</param>
/// <param name="ttl">The time-to-live to check against</param>
/// <param name="cachedFile">The file found in the cache if it exists</param>
/// <returns>Returns true if the file was found in the cache, otherwise false</returns>
public static bool CheckCacheTTL(string fileName, TimeSpan ttl, out FileInfo cachedFile) =>
CheckCacheTTL(new FileInfo(Path.Join(CachePath, fileName)), ttl, out cachedFile);
private static bool CheckCacheTTL(FileInfo cacheFile, TimeSpan ttl, out FileInfo fileInCache)
{
fileInCache = cacheFile;
try
{
cacheFile.Refresh();
Directory.CreateDirectory(CachePath);
if (!cacheFile.Exists)
{
Log.Information($"{cacheFile.Name} {(cacheFile.Exists ? "is in cache" : "NOT in cache")}");
return false;
}
var validTimeToLive = cacheFile.LastWriteTime.Add(ttl) > DateTime.Now;
Log.Information($"{cacheFile.Name} TTL is {(validTimeToLive ? "OK" : "INVALID")}");
return validTimeToLive;
}
catch (Exception ex)
{
Log.Error(ex, "Something went wrong during hashing");
return false;
}
}
/// <summary>
/// Download a file to the cache folder
/// </summary>
@ -107,7 +145,20 @@ public static class DownloadCacheHelper
// Use the provided extension method
using (var file = new FileStream(outputFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None))
await _httpClient.DownloadDataAsync(targetLink, file, progress);
{
if (!await _httpClient.DownloadDataAsync(targetLink, file, progress))
{
Log.Error($"Download failed: {targetLink}");
outputFile.Refresh();
if (outputFile.Exists)
{
outputFile.Delete();
return null;
}
}
}
outputFile.Refresh();
@ -167,6 +218,34 @@ public static class DownloadCacheHelper
}
}
/// <summary>
/// Get or download a file using a time to live
/// </summary>
/// <param name="fileName">The file to get from cache</param>
/// <param name="targetLink">The link to use for the download</param>
/// <param name="progress">A progress object for reporting download progress</param>
/// <param name="timeToLive">The time-to-live to check against in the cache</param>
/// <returns></returns>
public static async Task<FileInfo?> GetOrDownloadFileAsync(string fileName, string targetLink,
IProgress<double> progress, TimeSpan timeToLive)
{
try
{
if (CheckCacheTTL(fileName, timeToLive, out FileInfo cachedFile))
{
return cachedFile;
}
Log.Information($"Downloading File: {targetLink}");
return await DownloadFileAsync(fileName, targetLink, progress);
}
catch (Exception ex)
{
Log.Error(ex, $"Error while getting file: {fileName}");
return null;
}
}
/// <summary>
/// Get the file from cache or download it
/// </summary>
@ -181,9 +260,10 @@ public static class DownloadCacheHelper
{
try
{
if (CheckCache(fileName, expectedHash, out var cacheFile))
if (CheckCacheHash(fileName, expectedHash, out var cacheFile))
return cacheFile;
Log.Information($"Downloading File: {targetLink}");
return await DownloadFileAsync(fileName, targetLink, progress);
}
catch (Exception ex)
@ -206,7 +286,7 @@ public static class DownloadCacheHelper
{
try
{
if (CheckCache(fileName, expectedHash, out var cacheFile))
if (CheckCacheHash(fileName, expectedHash, out var cacheFile))
return cacheFile;
return await DownloadFileAsync(fileName, fileDownloadStream);

View File

@ -9,98 +9,6 @@ namespace SPTInstaller.Helpers;
public static class FileHelper
{
private static Result IterateDirectories(DirectoryInfo sourceDir, DirectoryInfo targetDir, string[] exclusions)
{
try
{
foreach (var dir in sourceDir.GetDirectories("*", SearchOption.AllDirectories))
{
var exclude = false;
foreach (var exclusion in exclusions)
{
var currentDirRelativePath = dir.FullName.Replace(sourceDir.FullName, "");
if (currentDirRelativePath.StartsWith(exclusion) || currentDirRelativePath == exclusion)
{
exclude = true;
Log.Debug(
$"EXCLUSION FOUND :: DIR\nExclusion: '{exclusion}'\nPath: '{currentDirRelativePath}'");
break;
}
}
if (exclude)
continue;
Directory.CreateDirectory(dir.FullName.Replace(sourceDir.FullName, targetDir.FullName));
}
return Result.FromSuccess();
}
catch (Exception ex)
{
Log.Error(ex, "Error while creating directories");
return Result.FromError(ex.Message);
}
}
private static Result IterateFiles(DirectoryInfo sourceDir, DirectoryInfo targetDir, string[] exclusions,
Action<string, int> updateCallback = null)
{
try
{
int totalFiles = sourceDir.GetFiles("*.*", SearchOption.AllDirectories).Length;
int processedFiles = 0;
foreach (var file in sourceDir.GetFiles("*.*", SearchOption.AllDirectories))
{
var exclude = false;
updateCallback?.Invoke(file.Name, (int)Math.Floor(((double)processedFiles / totalFiles) * 100));
foreach (var exclusion in exclusions)
{
var currentFileRelativePath = file.FullName.Replace(sourceDir.FullName, "");
if (currentFileRelativePath.StartsWith(exclusion) || currentFileRelativePath == exclusion)
{
exclude = true;
Log.Debug(
$"EXCLUSION FOUND :: FILE\nExclusion: '{exclusion}'\nPath: '{currentFileRelativePath}'");
break;
}
if (currentFileRelativePath.EndsWith(".bak"))
{
exclude = true;
Log.Debug($"EXCLUDING BAK FILE :: {currentFileRelativePath}");
break;
}
}
if (exclude)
continue;
var targetFile = file.FullName.Replace(sourceDir.FullName, targetDir.FullName);
Log.Debug(
$"COPY\nSourceDir: '{sourceDir.FullName}'\nTargetDir: '{targetDir.FullName}'\nNewPath: '{targetFile}'");
File.Copy(file.FullName, targetFile, true);
processedFiles++;
}
return Result.FromSuccess();
}
catch (Exception ex)
{
Log.Error(ex, "Error while copying files");
return Result.FromError(ex.Message);
}
}
public static string GetRedactedPath(string path)
{
var nameMatched = Regex.Match(path, @".:\\[uU]sers\\(?<NAME>[^\\]+)");
@ -123,13 +31,57 @@ public static class FileHelper
{
try
{
var iterateDirectoriesResult = IterateDirectories(sourceDir, targetDir, exclusions ??= new string[0]);
var allFiles = sourceDir.GetFiles("*", SearchOption.AllDirectories);
var fileCopies = new List<CopyInfo>();
int count = 0;
if (!iterateDirectoriesResult.Succeeded) return iterateDirectoriesResult;
// filter files before starting copy
foreach (var file in allFiles)
{
count++;
updateCallback?.Invoke("getting list of files to copy", (int)Math.Floor((double)count / allFiles.Length * 100));
var iterateFilesResult = IterateFiles(sourceDir, targetDir, exclusions ??= new string[0], updateCallback);
var currentFileRelativePath = file.FullName.Replace(sourceDir.FullName, "");
if (!iterateFilesResult.Succeeded) return iterateDirectoriesResult;
if (exclusions != null)
{
// check exclusions
foreach (var exclusion in exclusions)
{
if (currentFileRelativePath.StartsWith(exclusion) || currentFileRelativePath == exclusion)
{
Log.Debug(
$"EXCLUSION FOUND :: FILE\nExclusion: '{exclusion}'\nPath: '{currentFileRelativePath}'");
break;
}
}
}
// don't copy .bak files
if (currentFileRelativePath.EndsWith(".bak"))
{
Log.Debug($"EXCLUDING BAK FILE :: {currentFileRelativePath}");
continue;
}
fileCopies.Add(new CopyInfo(file.FullName, file.FullName.Replace(sourceDir.FullName, targetDir.FullName)));
}
count = 0;
// process copy info for files that need to be copied
foreach (var copyInfo in fileCopies)
{
count++;
updateCallback?.Invoke(copyInfo.FileName, (int)Math.Floor((double)count / fileCopies.Count * 100));
var result = copyInfo.Copy();
if (!result.Succeeded)
{
return result;
}
}
return Result.FromSuccess();
}
@ -144,6 +96,7 @@ public static class FileHelper
{
try
{
Log.Debug($"Starting StreamAssemblyResourceOut, resourcename: {resourceName}, outputFilePath: {outputFilePath}");
var assembly = Assembly.GetExecutingAssembly();
FileInfo outputFile = new FileInfo(outputFilePath);
@ -167,6 +120,7 @@ public static class FileHelper
}
outputFile.Refresh();
return outputFile.Exists;
}
catch (Exception ex)
@ -176,12 +130,21 @@ public static class FileHelper
}
}
/// <summary>
/// Check if a path is problematic
/// </summary>
/// <param name="path">The path the check</param>
/// <param name="failedCheck">The check that failed</param>
/// <returns>Returns true if the path is bad, otherwise false</returns>
public static bool CheckPathForProblemLocations(string path, out PathCheck failedCheck)
{
path = Path.TrimEndingDirectorySeparator(path);
failedCheck = new();
var problemPaths = new List<PathCheck>()
{
new("SteamApps", PathCheckType.EndsWith, PathCheckAction.Warn),
new("Documents", PathCheckType.EndsWith, PathCheckAction.Warn),
new("Desktop", PathCheckType.EndsWith, PathCheckAction.Deny),
new("Battlestate Games", PathCheckType.Contains, PathCheckAction.Deny),

View File

@ -6,7 +6,7 @@ namespace SPTInstaller.Helpers;
public static class HttpClientProgressExtensions
{
public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination,
public static async Task<bool> DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination,
IProgress<double> progress = null, CancellationToken cancellationToken = default(CancellationToken))
{
using (var response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead))
@ -18,20 +18,21 @@ public static class HttpClientProgressExtensions
if (progress is null || !contentLength.HasValue)
{
await download.CopyToAsync(destination);
return;
return true;
}
// Such progress and contentLength much reporting Wow!
var progressWrapper = new Progress<long>(totalBytes =>
progress.Report(GetProgressPercentage(totalBytes, contentLength.Value)));
await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken);
var readBytes = await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken);
return readBytes == contentLength.Value;
}
}
float GetProgressPercentage(float totalBytes, float currentBytes) => (totalBytes / currentBytes) * 100f;
}
static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize,
static async Task<long> CopyToAsync(this Stream source, Stream destination, int bufferSize,
IProgress<long> progress = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (bufferSize < 0)
@ -55,5 +56,7 @@ public static class HttpClientProgressExtensions
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
return totalBytesRead;
}
}

View File

@ -20,7 +20,10 @@ public static class PreCheckHelper
?.GetValue("InstallLocation");
var info = (uninstallStringValue is string key) ? new DirectoryInfo(key) : null;
return info?.FullName;
if (info == null)
return null;
return Path.TrimEndingDirectorySeparator(info.FullName);
}
public static Result DetectOriginalGameVersion(string gamePath)

View File

@ -38,7 +38,7 @@ public static class ProcessHelper
switch ((PatcherExitCode)process.ExitCode)
{
case PatcherExitCode.Success:
return Result.FromSuccess("Patcher Finished Successfully, extracting Aki");
return Result.FromSuccess("Patcher Finished Successfully, extracting SPT");
case PatcherExitCode.ProgramClosed:
return Result.FromError("Patcher was closed before completing!");
@ -47,16 +47,16 @@ public static class ProcessHelper
return Result.FromError("EscapeFromTarkov.exe is missing from the install Path");
case PatcherExitCode.NoPatchFolder:
return Result.FromError("Patchers Folder called 'Aki_Patches' is missing");
return Result.FromError("Patchers Folder called 'SPT_Patches' is missing");
case PatcherExitCode.MissingFile:
return Result.FromError("EFT files was missing a Vital file to continue");
return Result.FromError("Vital EFT files were not found. The installer is unable to continue. Please reinstall EFT and try again.");
case PatcherExitCode.PatchFailed:
return Result.FromError("A patch failed to apply");
default:
return Result.FromError("an unknown error occurred in the patcher");
return Result.FromError("An unknown error occurred in the patcher");
}
}

View File

@ -45,7 +45,7 @@ public class DownloadTask : InstallerTaskBase
{
SetStatus("Downloading Patcher", "Verifying cached patcher ...", progressStyle: ProgressStyle.Indeterminate);
if (DownloadCacheHelper.CheckCache("patcher", _expectedPatcherHash, out var cacheFile))
if (DownloadCacheHelper.CheckCacheHash("patcher", _expectedPatcherHash, out var cacheFile))
{
_data.PatcherZipInfo = cacheFile;
Log.Information("Using cached file {fileName} - Hash: {hash}", _data.PatcherZipInfo.Name,
@ -68,23 +68,23 @@ public class DownloadTask : InstallerTaskBase
return Result.FromError("Failed to download Patcher");
}
private async Task<IResult> DownloadSptAkiFromMirrors(IProgress<double> progress)
private async Task<IResult> DownloadSPTFromMirrors(IProgress<double> progress)
{
// Note that GetOrDownloadFileAsync handles the cached file hash check, so we don't need to check it first
foreach (var mirror in _data.ReleaseInfo.Mirrors)
{
SetStatus("Downloading SPT-AKI", mirror.DownloadUrl, progressStyle: ProgressStyle.Indeterminate);
SetStatus("Downloading SPT", mirror.DownloadUrl, progressStyle: ProgressStyle.Indeterminate);
_data.AkiZipInfo =
await DownloadCacheHelper.GetOrDownloadFileAsync("sptaki", mirror.DownloadUrl, progress, mirror.Hash);
_data.SPTZipInfo =
await DownloadCacheHelper.GetOrDownloadFileAsync("SPT", mirror.DownloadUrl, progress, mirror.Hash);
if (_data.AkiZipInfo != null)
if (_data.SPTZipInfo != null)
{
return Result.FromSuccess();
}
}
return Result.FromError("Failed to download spt-aki");
return Result.FromError("Failed to download SPT");
}
public override async Task<IResult> TaskOperation()
@ -110,6 +110,6 @@ public class DownloadTask : InstallerTaskBase
}
}
return await DownloadSptAkiFromMirrors(progress);
return await DownloadSPTFromMirrors(progress);
}
}

View File

@ -38,7 +38,7 @@ public class InitializationTask : InstallerTaskBase
if (File.Exists(Path.Join(_data.TargetInstallPath, "EscapeFromTarkov.exe")))
{
return Result.FromError(
"Installer is located in a folder that has existing game files. Please make sure the installer is in an empty folder as per the guide");
"Install location is a folder that has existing game files. Please make sure the folder doesn't contain an existing SPT install");
}
return Result.FromSuccess($"Current Game Version: {_data.OriginalGameVersion}");

View File

@ -0,0 +1,24 @@
using System.Threading.Tasks;
using SPTInstaller.Models;
namespace SPTInstaller.Installer_Tasks.PreChecks;
public class EftInstalledPreCheck : PreCheckBase
{
private InternalData _internalData;
public EftInstalledPreCheck(InternalData data) : base("EFT Installed", true)
{
_internalData = data;
}
public override async Task<PreCheckResult> CheckOperation()
{
if (_internalData.OriginalGamePath is null || !Directory.Exists(_internalData.OriginalGamePath) || !File.Exists(Path.Join(_internalData.OriginalGamePath, "Escapefromtarkov.exe")))
{
return PreCheckResult.FromError("Your EFT installation could not be found, try running the Battlestate Games Launcher and ensure EFT is installed on your computer", "Retry", RequestReevaluation);
}
return PreCheckResult.FromSuccess("EFT install folder found");
}
}

View File

@ -17,7 +17,7 @@ public class EftLauncherPreCheck : PreCheckBase
return eftLauncherProcs.Length == 0
? PreCheckResult.FromSuccess("Eft launcher is closed")
: PreCheckResult.FromError("Eft launcher is open. Please close it to install SPT",
: PreCheckResult.FromError("Your Battlestate Games Launcher is open. Please close it to continue installing SPT",
"Kill EFT Launcher Processes",
() =>
{

View File

@ -33,7 +33,7 @@ public class FreeSpacePreCheck : PreCheckBase
if (eftSourceDirSize == -1)
{
return PreCheckResult.FromError("An error occurred while getting the EFT source directory size");
return PreCheckResult.FromError("An error occurred while getting the EFT source directory size. This is most likely because EFT is not installed");
}
var availableSize = DriveInfo.GetDrives()

View File

@ -24,66 +24,77 @@ public class ReleaseCheckTask : InstallerTaskBase
SetStatus("Checking SPT Releases", "", null, ProgressStyle.Indeterminate);
var progress = new Progress<double>((d) => { SetStatus(null, null, (int)Math.Floor(d)); });
var akiReleaseInfoFile =
await DownloadCacheHelper.DownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
progress);
if (akiReleaseInfoFile == null)
var SPTReleaseInfoFile =
await DownloadCacheHelper.GetOrDownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
progress, DownloadCacheHelper.SuggestedTtl);
if (SPTReleaseInfoFile == null)
{
return Result.FromError("Failed to download release metadata");
return Result.FromError("Failed to download release metadata, try clicking the 'Whats this' button below followed by the 'Clear Metadata cache' button");
}
var akiReleaseInfo =
JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(akiReleaseInfoFile.FullName));
var SPTReleaseInfo =
JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(SPTReleaseInfoFile.FullName));
SetStatus("Checking for Patches", "", null, ProgressStyle.Indeterminate);
var akiPatchMirrorsFile =
await DownloadCacheHelper.DownloadFileAsync("mirrors.json", DownloadCacheHelper.PatchMirrorUrl,
progress);
var SPTPatchMirrorsFile =
await DownloadCacheHelper.GetOrDownloadFileAsync("mirrors.json", DownloadCacheHelper.PatchMirrorUrl,
progress, DownloadCacheHelper.SuggestedTtl);
if (akiPatchMirrorsFile == null)
if (SPTPatchMirrorsFile == null)
{
return Result.FromError("Failed to download patch mirror data");
return Result.FromError("Failed to download patch mirror data, try clicking the 'Whats this' button below followed by the 'Clear Metadata cache' button");
}
var patchMirrorInfo =
JsonConvert.DeserializeObject<PatchInfo>(File.ReadAllText(akiPatchMirrorsFile.FullName));
JsonConvert.DeserializeObject<PatchInfo>(File.ReadAllText(SPTPatchMirrorsFile.FullName));
if (akiReleaseInfo == null || patchMirrorInfo == null)
if (SPTReleaseInfo == null || patchMirrorInfo == null)
{
return Result.FromError("An error occurred while deserializing aki or patch data");
return Result.FromError("An error occurred while deserializing SPT or patch data, try clicking the 'Whats this' button below followed by the 'Clear Metadata cache' button");
}
_data.ReleaseInfo = akiReleaseInfo;
_data.ReleaseInfo = SPTReleaseInfo;
_data.PatchInfo = patchMirrorInfo;
int intAkiVersion = int.Parse(akiReleaseInfo.ClientVersion);
int intSPTVersion = int.Parse(SPTReleaseInfo.ClientVersion);
int intGameVersion = int.Parse(_data.OriginalGameVersion);
// note: it's possible the game version could be lower than the aki version and still need a patch if the major version numbers change
// note: it's possible the game version could be lower than the SPT version and still need a patch if the major version numbers change
// : it's probably a low chance though
bool patchNeedCheck = intGameVersion > intAkiVersion;
bool patchNeedCheck = intGameVersion > intSPTVersion;
if (intGameVersion < intAkiVersion)
if (intGameVersion < intSPTVersion)
{
return Result.FromError("Your client is outdated. Please update EFT");
return Result.FromError("Your live EFT is out of date. Please update it using the Battlestate Games Launcher and try runing the SPT Installer again");
}
if (intGameVersion == intAkiVersion)
if (intGameVersion == intSPTVersion)
{
patchNeedCheck = false;
}
if ((intGameVersion != patchMirrorInfo.SourceClientVersion ||
intAkiVersion != patchMirrorInfo.TargetClientVersion) && patchNeedCheck)
bool sptClientIsOutdated = intSPTVersion != patchMirrorInfo.TargetClientVersion && patchNeedCheck;
bool liveClientIsOutdated = intGameVersion != patchMirrorInfo.SourceClientVersion && patchNeedCheck;
if (sptClientIsOutdated)
{
return Result.FromError(
"No patcher available for your version.\nA patcher is usually created within 24 hours of an EFT update.");
"Could not find a downgrade patcher for the version of EFT you have installed." +
"\nThis can happen due to one of the following reasons:" +
"\n* Live EFT just updated. The SPT team will create a new patcher within 24 hours, hold tight!" +
"\n* Live EFT just updated. You have not installed it on your computer using your Battlestate Games launcher");
}
if (liveClientIsOutdated)
{
return Result.FromError("Your live EFT is out of date. Please update it using your Battlestate Games Launcher then run the SPT Installer again");
}
_data.PatchNeeded = patchNeedCheck;
string status =
$"Current Release: {akiReleaseInfo.ClientVersion} - {(_data.PatchNeeded ? "Patch Available" : "No Patch Needed")}";
$"Current Release: {SPTReleaseInfo.ClientVersion} - {(_data.PatchNeeded ? "Patch Available" : "No Patch Needed")}";
SetStatus(null, status);

View File

@ -35,7 +35,7 @@ public class SetupClientTask : InstallerTaskBase
if (_data.PatchNeeded)
{
// extract patcher files
SetStatus("Extrating Patcher", "", 0);
SetStatus("Extracting Patcher", "", 0);
var extractPatcherResult = ZipHelper.Decompress(_data.PatcherZipInfo, patcherOutputDir, progress);
@ -71,7 +71,7 @@ public class SetupClientTask : InstallerTaskBase
// extract release files
SetStatus("Extracting Release", "", 0);
var extractReleaseResult = ZipHelper.Decompress(_data.AkiZipInfo, targetInstallDirInfo, progress);
var extractReleaseResult = ZipHelper.Decompress(_data.SPTZipInfo, targetInstallDirInfo, progress);
if (!extractReleaseResult.Succeeded)
{

View File

@ -0,0 +1,24 @@
using Serilog;
using SPTInstaller.Helpers;
namespace SPTInstaller.Models;
class CopyInfo(string sourcePath, string targetPath)
{
public string FileName => $"{Path.GetFileName(sourcePath)}";
public Result Copy()
{
try
{
var directory = Path.GetDirectoryName(targetPath);
Directory.CreateDirectory(directory);
Log.Debug($"COPY\nSource: {FileHelper.GetRedactedPath(sourcePath)}\nTarget: {FileHelper.GetRedactedPath(targetPath)}");
File.Copy(sourcePath, targetPath);
return Result.FromSuccess();
}
catch (Exception ex)
{
return Result.FromError(ex.Message);
}
}
}

View File

@ -9,9 +9,21 @@ namespace SPTInstaller.Models;
public class InstallerUpdateInfo : ReactiveObject
{
public Version? NewVersion { get; private set; }
private Version? _newVersion;
public string ChangeLog = "";
public Version? NewVersion
{
get => _newVersion;
set => this.RaiseAndSetIfChanged(ref _newVersion, value);
}
private string _changeLog;
public string ChangeLog
{
get => _changeLog;
set => this.RaiseAndSetIfChanged(ref _changeLog, value);
}
private string _updateInfoText = "";
@ -21,14 +33,6 @@ public class InstallerUpdateInfo : ReactiveObject
set => this.RaiseAndSetIfChanged(ref _updateInfoText, value);
}
private bool _show = false;
public bool Show
{
get => _show;
set => this.RaiseAndSetIfChanged(ref _show, value);
}
private bool _updating = false;
public bool Updating
@ -123,7 +127,6 @@ public class InstallerUpdateInfo : ReactiveObject
}
UpdateInfoText = infoText;
Show = updateAvailable;
CheckingForUpdates = false;
UpdateAvailable = updateAvailable;
}
@ -134,14 +137,13 @@ public class InstallerUpdateInfo : ReactiveObject
return;
UpdateInfoText = "Checking for installer updates";
Show = true;
CheckingForUpdates = true;
try
{
var installerInfoFile =
await DownloadCacheHelper.DownloadFileAsync("installer.json", DownloadCacheHelper.InstallerInfoUrl,
null);
await DownloadCacheHelper.GetOrDownloadFileAsync("installer.json", DownloadCacheHelper.InstallerInfoUrl, null
, DownloadCacheHelper.SuggestedTtl);
if (installerInfoFile == null)
{

View File

@ -5,6 +5,7 @@ namespace SPTInstaller.Models;
public class InternalData
{
public bool DebugMode { get; set; } = false;
/// <summary>
/// The folder to install SPT into
/// </summary>
@ -26,9 +27,9 @@ public class InternalData
public FileInfo PatcherZipInfo { get; set; }
/// <summary>
/// SPT-AKI zip file info
/// SPT zip file info
/// </summary>
public FileInfo AkiZipInfo { get; set; }
public FileInfo SPTZipInfo { get; set; }
/// <summary>
/// The release information from release.json

View File

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Runtime.InteropServices;
namespace SPTInstaller.Models;
public enum KnownFolder
{
Contacts,
Downloads,
Favorites,
Links,
SavedGames,
SavedSearches
}
public static class KnownFolders
{
private static readonly Dictionary<KnownFolder, Guid> _guids = new()
{
[KnownFolder.Contacts] = new("56784854-C6CB-462B-8169-88E350ACB882"),
[KnownFolder.Downloads] = new("374DE290-123F-4565-9164-39C4925E467B"),
[KnownFolder.Favorites] = new("1777F761-68AD-4D8A-87BD-30B759FA33DD"),
[KnownFolder.Links] = new("BFB9D5E0-C6A9-404C-B2B2-AE6DB6AF4968"),
[KnownFolder.SavedGames] = new("4C5C32FF-BB9D-43B0-B5B4-2D72E54EAAA4"),
[KnownFolder.SavedSearches] = new("7D1D3A04-DEBB-4115-95CF-2F29DA2920DA")
};
public static string GetPath(KnownFolder knownFolder)
{
return SHGetKnownFolderPath(_guids[knownFolder], 0);
}
[DllImport("shell32",
CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)]
private static extern string SHGetKnownFolderPath(
[MarshalAs(UnmanagedType.LPStruct)] Guid rfid, uint dwFlags,
nint hToken = 0);
}

View File

@ -1,10 +1,12 @@
using System.Collections.Generic;
using Newtonsoft.Json;
namespace SPTInstaller.Models.ReleaseInfo;
public class ReleaseInfo
{
public string AkiVersion { get; set; }
[JsonProperty("AkiVersion")] // TODO: Change this and what gets uploaded to SPTVersion
public string SPTVersion { get; set; }
public string ClientVersion { get; set; }
public List<ReleaseInfoMirror> Mirrors { get; set; }
}

View File

@ -44,6 +44,7 @@ internal class Program
ServiceHelper.Register<InternalData>();
#if !TEST
ServiceHelper.Register<PreCheckBase, EftInstalledPreCheck>();
ServiceHelper.Register<PreCheckBase, NetFramework472PreCheck>();
ServiceHelper.Register<PreCheckBase, Net8PreCheck>();
ServiceHelper.Register<PreCheckBase, FreeSpacePreCheck>();

View File

@ -0,0 +1,23 @@
param (
[string]$installPath
)
$desktop = Join-Path $env:USERPROFILE "Desktop"
$launcherExe = gci $installPath | where {$_.Name -like "*.Launcher.exe"} | select -ExpandProperty FullName
$serverExe = gci $installPath | where {$_.Name -like "*.Server.exe"} | select -ExpandProperty FullName
$launcherShortcut = Join-Path $desktop "SPT.Launcher.lnk"
$serverShortcut = Join-Path $desktop "SPT.Server.lnk"
$WshShell = New-Object -comObject WScript.Shell
$launcher = $WshShell.CreateShortcut($launcherShortcut)
$launcher.TargetPath = $launcherExe
$launcher.WorkingDirectory = $installPath
$launcher.Save()
$server = $WshShell.CreateShortcut($serverShortcut)
$server.TargetPath = $serverExe
$server.WorkingDirectory = $installPath
$server.Save()

View File

@ -5,23 +5,69 @@
Clear-Host
Write-Host "Stopping installer ..."
Write-Host "Stopping installer ... " -ForegroundColor cyan -NoNewLine
$installer = Stop-Process -Name "SPTInstaller" -ErrorAction SilentlyContinue
if ($installer -ne $null)
{
Write-Host "Something went wrong, couldn't stop installer process'"
Write-Warning "Something went wrong, couldn't stop installer process'"
return;
}
Write-Host "Copying new installer ..."
Write-Host "OK" -ForegroundColor green
Import-Module BitsTransfer
if (-not(Test-Path $source) -and -not(Test-Path $destination)) {
Write-Warning "Can't find a required file"
Write-host ""
Write-Host "Press [enter] to close ..."
Read-Host
exit
}
Start-BitsTransfer -Source $source -Destination $destination
Write-Host "Copying new installer ... " -ForegroundColor cyan
Remove-Module BitsTransfer
$maxAttempts = 10
$copied = $false
while (-not $copied) {
$maxAttempts--
Write-Host " > Please wait ... " -NoNewLine
if ($maxAttempts -le 0) {
Write-Host "Couldn't copy new installer :( Please re-download the installer"
Write-Host ""
Write-Host "Press [enter] to close ..."
Read-Host
exit
}
try {
Remove-Item $destination -ErrorAction SilentlyContinue
Copy-Item $source $destination -ErrorAction SilentlyContinue
}
catch {
Write-Host "file locked, retrying ..." -ForegroundColor yellow
sleep(2)
continue
}
if (Test-Path $destination) {
$sLength = (Get-Item $source).Length
$dLength = (Get-Item $destination).Length
if ($sLength -eq $dLength) {
$copied = $true
Write-Host "OK" -ForegroundColor green
break
}
Write-Host "sizes differ, retrying ..." -ForegroundColor yellow
sleep(2)
}
}
# remove the new installer from the cache folder after it is copied
Remove-Item -Path $source

View File

@ -6,18 +6,19 @@
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<!-- TODO: To change -->
<PackageIcon>icon.ico</PackageIcon>
<ApplicationIcon>Assets\icon.ico</ApplicationIcon>
<ApplicationIcon>Assets\spt_installer.ico</ApplicationIcon>
<Configurations>Debug;Release;TEST</Configurations>
<AssemblyVersion>2.62</AssemblyVersion>
<FileVersion>2.62</FileVersion>
<Company>SPT-AKI</Company>
<AssemblyVersion>2.90</AssemblyVersion>
<FileVersion>2.90</FileVersion>
<Company>SPT</Company>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**"/>
<None Remove=".gitignore"/>
<None Remove="Assets\icon.ico"/>
<None Remove="Assets\spt_installer.ico"/>
<None Remove="Resources\update.ps1"/>
</ItemGroup>
@ -25,6 +26,8 @@
<EmbeddedResource Include="Resources\update.ps1"/>
<None Remove="Resources\7z.dll"/>
<EmbeddedResource Include="Resources\7z.dll"/>
<None Remove="Resources\add_shortcuts.ps1" />
<EmbeddedResource Include="Resources\add_shortcuts.ps1" />
</ItemGroup>
<ItemGroup>

View File

@ -0,0 +1,157 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Platform.Storage;
using ReactiveUI;
using Serilog;
using SPTInstaller.Helpers;
using SPTInstaller.Models;
namespace SPTInstaller.ViewModels;
public class InstallPathSelectionViewModel : ViewModelBase
{
private InternalData _data;
private string _selectedPath;
public string SelectedPath
{
get => _selectedPath;
set => this.RaiseAndSetIfChanged(ref _selectedPath, value);
}
private bool _validPath;
public bool ValidPath
{
get => _validPath;
set => this.RaiseAndSetIfChanged(ref _validPath, value);
}
private string _errorMessage;
public string ErrorMessage
{
get => _errorMessage;
set => this.RaiseAndSetIfChanged(ref _errorMessage, value);
}
public InstallPathSelectionViewModel(IScreen host, string installPath) : base(host)
{
_data = ServiceHelper.Get<InternalData?>() ?? throw new Exception("Failed to get internal data");
SelectedPath = Environment.CurrentDirectory;
ValidPath = false;
if (!string.IsNullOrEmpty(installPath))
{
SelectedPath = installPath;
ValidatePath();
if (ValidPath)
{
Log.Information("Install Path was provided by parameter and seems valid");
Task.Run(NextCommand);
return;
}
}
AdjustInstallPath();
}
public async Task SelectFolderCommand()
{
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (desktop.MainWindow == null)
{
return;
}
var startingFolderPath = Directory.Exists(SelectedPath) ? SelectedPath : Environment.CurrentDirectory;
var suggestedFolder = await desktop.MainWindow.StorageProvider.TryGetFolderFromPathAsync(startingFolderPath);
var selections = await desktop.MainWindow.StorageProvider.OpenFolderPickerAsync(
new FolderPickerOpenOptions()
{
AllowMultiple = false,
SuggestedStartLocation = suggestedFolder,
Title = "Select a folder to install SPT into"
});
SelectedPath = selections.First().Path.LocalPath;
}
}
public void ValidatePath()
{
if (String.IsNullOrEmpty(SelectedPath))
{
ErrorMessage = "Please provide an install path";
ValidPath = false;
return;
}
var match = Regex.Match(SelectedPath[2..], @"[\/:*?""<>|]");
if (match.Success)
{
ErrorMessage = "Path cannot contain these characters: / : * ? \" < > |";
ValidPath = false;
return;
}
if (FileHelper.CheckPathForProblemLocations(SelectedPath, out var failedCheck))
{
if (failedCheck.CheckType == PathCheckType.EndsWith)
{
ErrorMessage = $"You can install in {failedCheck.Target}, but only in a subdirectory. Example: ..\\{failedCheck.Target}\\SPT";
ValidPath = false;
return;
}
if (failedCheck.CheckAction == PathCheckAction.Deny)
{
ErrorMessage = $"Sorry, you cannot install in {failedCheck.Target}";
ValidPath = false;
return;
}
}
ValidPath = true;
}
private void AdjustInstallPath()
{
if (FileHelper.CheckPathForProblemLocations(SelectedPath, out var failedCheck))
{
switch (failedCheck.CheckType)
{
case PathCheckType.EndsWith:
SelectedPath = Path.Join(Environment.CurrentDirectory, "SPT");
break;
case PathCheckType.Contains:
case PathCheckType.DriveRoot:
SelectedPath = Path.Join(Directory.GetDirectoryRoot(Environment.CurrentDirectory), "SPT");
break;
default:
throw new ArgumentOutOfRangeException();
}
}
}
public void NextCommand()
{
if (FileHelper.CheckPathForProblemLocations(SelectedPath, out var failedCheck) && failedCheck.CheckAction == PathCheckAction.Deny)
{
return;
}
_data.TargetInstallPath = SelectedPath;
NavigateTo(new PreChecksViewModel(HostScreen));
}
}

View File

@ -0,0 +1,42 @@
using System.Reflection;
using System.Threading.Tasks;
using ReactiveUI;
using SPTInstaller.Helpers;
using SPTInstaller.Models;
namespace SPTInstaller.ViewModels;
public class InstallerUpdateViewModel : ViewModelBase
{
public InstallerUpdateInfo UpdateInfo { get; set; } = new();
private InternalData _data;
private string _installPath;
public InstallerUpdateViewModel(IScreen Host, string installPath) : base(Host)
{
_installPath = installPath;
_data = ServiceHelper.Get<InternalData>() ?? throw new Exception("Failed to get internal data");
Task.Run(async () =>
{
await UpdateInfo.CheckForUpdates(Assembly.GetExecutingAssembly().GetName().Version);
if (!UpdateInfo.UpdateAvailable)
{
NavigateTo(new OverviewViewModel(HostScreen, _installPath));
}
});
}
public void NotNowCommand()
{
NavigateTo(new OverviewViewModel(HostScreen, _installPath));
}
public async Task UpdateInstallCommand()
{
await UpdateInfo.UpdateInstaller();
}
}

View File

@ -3,6 +3,8 @@ using ReactiveUI;
using Serilog;
using System.Globalization;
using System.Reflection;
using SPTInstaller.Helpers;
using SPTInstaller.Models;
namespace SPTInstaller.ViewModels;
@ -19,10 +21,12 @@ public class MainWindowViewModel : ReactiveObject, IActivatableViewModel, IScree
set => this.RaiseAndSetIfChanged(ref _title, value);
}
public MainWindowViewModel(bool debugging)
public MainWindowViewModel(string installPath)
{
var data = ServiceHelper.Get<InternalData>() ?? throw new Exception("failed to get interanl data");
Title =
$"{(debugging ? "-debug-" : "")} SPT Installer {"v" + Assembly.GetExecutingAssembly().GetName()?.Version?.ToString() ?? "--unknown version--"}";
$"{(data.DebugMode ? "-debug-" : "")} SPT Installer {"v" + Assembly.GetExecutingAssembly().GetName()?.Version?.ToString() ?? "--unknown version--"}";
Log.Information($"========= {Title} Started =========");
Log.Information(Environment.OSVersion.VersionString);
@ -31,7 +35,7 @@ public class MainWindowViewModel : ReactiveObject, IActivatableViewModel, IScree
Log.Information("System Language: {iso} - {name}", uiCulture.TwoLetterISOLanguageName, uiCulture.DisplayName);
Router.Navigate.Execute(new PreChecksViewModel(this, debugging));
Router.Navigate.Execute(new InstallerUpdateViewModel(this, installPath));
}
public void CloseCommand()

View File

@ -1,4 +1,6 @@
using Avalonia;
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia;
using ReactiveUI;
using Serilog;
using SPTInstaller.CustomControls;
@ -6,6 +8,10 @@ using SPTInstaller.Helpers;
using SPTInstaller.Interfaces;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using Avalonia.Platform.Storage;
using SPTInstaller.Models;
namespace SPTInstaller.ViewModels;
@ -35,6 +41,14 @@ public class MessageViewModel : ViewModelBase
set => this.RaiseAndSetIfChanged(ref _showCloseButton, value);
}
private bool _showOptions;
public bool ShowOptions
{
get => _showOptions;
set => this.RaiseAndSetIfChanged(ref _showOptions, value);
}
private string _cacheInfoText;
public string CacheInfoText
@ -43,6 +57,74 @@ public class MessageViewModel : ViewModelBase
set => this.RaiseAndSetIfChanged(ref _cacheInfoText, value);
}
private string _clipCommandText;
public string ClipCommandText
{
get => _clipCommandText;
set => this.RaiseAndSetIfChanged(ref _clipCommandText, value);
}
private bool _addShortcuts;
public bool AddShortcuts
{
get => _addShortcuts;
set => this.RaiseAndSetIfChanged(ref _addShortcuts, value);
}
private bool _openInstallFolder = true;
public bool OpenInstallFolder
{
get => _openInstallFolder;
set => this.RaiseAndSetIfChanged(ref _openInstallFolder, value);
}
public ICommand CopyLogFileToClipboard => ReactiveCommand.CreateFromTask(async () =>
{
var data = ServiceHelper.Get<InternalData>();
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
try
{
if (desktop.MainWindow?.Clipboard == null)
{
ClipCommandText = "Could not get clipboard :(";
return;
}
var dataObject = new DataObject();
var filesToCopy = new List<IStorageFile>();
var logFile = await desktop.MainWindow.StorageProvider.TryGetFileFromPathAsync(data.DebugMode ? App.LogDebugPath : App.LogPath);
var patcherLogFile = await desktop.MainWindow.StorageProvider.TryGetFileFromPathAsync(Path.Join(data.TargetInstallPath, "patcher.log"));
if (logFile == null)
{
ClipCommandText = "Could not get log file :(";
return;
}
filesToCopy.Add(logFile);
if (patcherLogFile != null)
{
filesToCopy.Add(patcherLogFile);
}
dataObject.Set(DataFormats.Files, filesToCopy.ToArray());
await desktop.MainWindow.Clipboard.SetDataObjectAsync(dataObject);
ClipCommandText = "Copied!";
}
catch (Exception ex)
{
ClipCommandText = ex.Message;
}
}
});
private StatusSpinner.SpinnerState _cacheCheckState;
public StatusSpinner.SpinnerState CacheCheckState
@ -53,8 +135,7 @@ public class MessageViewModel : ViewModelBase
public ICommand CloseCommand { get; set; } = ReactiveCommand.Create(() =>
{
if (Application.Current.ApplicationLifetime is
Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopApp)
{
desktopApp.MainWindow.Close();
}
@ -64,6 +145,75 @@ public class MessageViewModel : ViewModelBase
{
ShowCloseButton = showCloseButton;
Message = result.Message;
ClipCommandText = "Copy installer log to clipboard";
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopApp)
{
var data = ServiceHelper.Get<InternalData?>();
desktopApp.MainWindow.Closing += (_, _) =>
{
if (ShowOptions)
{
if (OpenInstallFolder)
{
Process.Start(new ProcessStartInfo()
{
FileName = "explorer.exe",
Arguments = data.TargetInstallPath
});
}
if (AddShortcuts)
{
var shortcuts = new FileInfo(Path.Join(DownloadCacheHelper.CachePath, "add_shortcuts.ps1"));
if (!FileHelper.StreamAssemblyResourceOut("add_shortcuts.ps1", shortcuts.FullName))
{
Log.Fatal("Failed to prepare shortcuts file");
return;
}
if (!File.Exists(shortcuts.FullName))
{
Log.Fatal("Shortcuts file not found");
return;
}
Log.Information("Running add shortcuts script ...");
Process.Start(new ProcessStartInfo
{
FileName = "powershell.exe",
CreateNoWindow = true,
ArgumentList =
{
"-ExecutionPolicy", "Bypass", "-File", $"{shortcuts.FullName}", $"{data.TargetInstallPath}"
}
});
}
}
try
{
if (data.TargetInstallPath == Environment.CurrentDirectory)
{
return;
}
File.Copy(App.LogPath, Path.Join(data.TargetInstallPath, "spt-installer.log"), true);
if (data.DebugMode)
{
File.Copy(App.LogDebugPath, Path.Join(data.TargetInstallPath, "spt-installer-debug.log"), true);
}
}
catch (Exception ex)
{
Log.Error(ex, "Failed to copy installer log to install path");
}
};
}
Task.Run(() =>
{
@ -77,6 +227,7 @@ public class MessageViewModel : ViewModelBase
if (result.Succeeded)
{
Log.Information(Message);
ShowOptions = true;
return;
}

View File

@ -0,0 +1,23 @@
using System.Threading.Tasks;
using ReactiveUI;
namespace SPTInstaller.ViewModels;
public class OverviewViewModel : ViewModelBase
{
private string _providedPath;
public OverviewViewModel(IScreen Host, string providedPath) : base(Host)
{
_providedPath = providedPath;
if (!string.IsNullOrEmpty(_providedPath))
{
Task.Run(NextCommand);
}
}
public void NextCommand()
{
NavigateTo(new InstallPathSelectionViewModel(HostScreen, _providedPath));
}
}

View File

@ -1,6 +1,5 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Threading;
@ -32,16 +31,8 @@ public class PreChecksViewModel : ViewModelBase
public ICommand SelectPreCheckCommand { get; set; }
public ICommand StartInstallCommand { get; set; }
public ICommand UpdateInstallerCommand { get; set; }
public ICommand DismissUpdateCommand { get; set; }
public ICommand WhatsNewCommand { get; set; }
public ICommand LaunchWithDebug { get; set; }
public InstallerUpdateInfo UpdateInfo { get; set; } = new();
private bool _debugging;
public bool Debugging
@ -118,12 +109,13 @@ public class PreChecksViewModel : ViewModelBase
});
}
public PreChecksViewModel(IScreen host, bool debugging) : base(host)
public PreChecksViewModel(IScreen host) : base(host)
{
Debugging = debugging;
var data = ServiceHelper.Get<InternalData?>();
var installer = ServiceHelper.Get<InstallController?>();
Debugging = data.DebugMode;
installer.RecheckRequested += ReCheckRequested;
InstallButtonText = "Please wait ...";
@ -138,7 +130,6 @@ public class PreChecksViewModel : ViewModelBase
data.OriginalGamePath = PreCheckHelper.DetectOriginalGamePath();
data.TargetInstallPath = Environment.CurrentDirectory;
InstallPath = data.TargetInstallPath;
Log.Information($"Install Path: {FileHelper.GetRedactedPath(InstallPath)}");
@ -147,7 +138,7 @@ public class PreChecksViewModel : ViewModelBase
if (data.OriginalGamePath == null)
{
NavigateTo(new MessageViewModel(HostScreen,
Result.FromError("Could not find EFT install.\n\nDo you own and have the game installed?")));
Result.FromError("Could not find where you installed EFT.\n\nDo you own and have the game installed?")));
return;
}
#endif
@ -156,7 +147,7 @@ public class PreChecksViewModel : ViewModelBase
{
Log.CloseAndFlush();
var logFiles = Directory.GetFiles(InstallPath, "spt-aki-installer_*.log");
var logFiles = Directory.GetFiles(InstallPath, "spt-installer_*.log");
// remove log file from original game path if they exist
foreach (var file in logFiles)
@ -172,7 +163,7 @@ public class PreChecksViewModel : ViewModelBase
NavigateTo(new MessageViewModel(HostScreen,
Result.FromError(
"Installer is located in EFT's original directory. Please move the installer to a seperate folder as per the guide"),
"You have chosen to install in the same folder as EFT. Please choose a another folder. Refer to the install guide on where best to place the installer before running it."),
noLog: true));
return;
}
@ -189,13 +180,15 @@ public class PreChecksViewModel : ViewModelBase
{
Log.Warning("Problem path detected, confirming install path ...");
var confirmation = await DialogHost.Show(new ConfirmationDialog(
$"We suspect you may be installing into a problematic folder: {failedCheck.Target}.\nYou might want to consider installing somewhere else to avoid issues.\n\nAre you sure you want to install to this path?\n{InstallPath}"));
$"It appears you are installing into a folder known to cause problems: {failedCheck.Target}." +
$"\nPlease consider installing SPT somewhere else to avoid issues later on." +
$"\n\nAre you sure you want to install to this path?\n{InstallPath}"));
if (confirmation == null || !bool.TryParse(confirmation.ToString(), out var confirm) ||
!confirm)
{
Log.Information("User declined install path, exiting");
Environment.Exit(0);
Log.Information("User declined install path");
NavigateBack();
}
});
@ -207,7 +200,7 @@ public class PreChecksViewModel : ViewModelBase
Log.Error("Problem path detected, install denied");
NavigateTo(new MessageViewModel(HostScreen,
Result.FromError(
$"We suspect you may be installing into a problematic folder: {failedCheck.Target}.\nWe won't be letting you install here. Please move the installer to another folder.\nSuggestion: a folder under your drive root, such as 'C:\\spt\\'\nDenied Path: {InstallPath}")));
$"We suspect you may be installing into a problematic folder: {failedCheck.Target}.\nWe won't be letting you install here. How did you do this?")));
break;
}
default:
@ -222,14 +215,7 @@ public class PreChecksViewModel : ViewModelBase
{
try
{
var installerPath = Path.Join(_installPath, "SPTInstaller.exe");
Process.Start(new ProcessStartInfo()
{
FileName = installerPath,
Arguments = "debug"
});
Environment.Exit(0);
App.ReLaunch(true, InstallPath);
}
catch (Exception ex)
{
@ -256,56 +242,42 @@ public class PreChecksViewModel : ViewModelBase
StartInstallCommand = ReactiveCommand.Create(async () =>
{
UpdateInfo.Show = false;
NavigateTo(new InstallViewModel(HostScreen));
});
UpdateInstallerCommand = ReactiveCommand.Create(async () =>
{
AllowDetailsButton = false;
AllowInstall = false;
await UpdateInfo.UpdateInstaller();
});
DismissUpdateCommand = ReactiveCommand.Create(() => { UpdateInfo.Show = false; });
WhatsNewCommand =
ReactiveCommand.Create(async () => await DialogHost.Show(new ChangeLogDialog(UpdateInfo.NewVersion.ToString(), UpdateInfo.ChangeLog)));
Task.Run(async () =>
{
// run prechecks
var result = await installer.RunPreChecks();
// check for updates
await UpdateInfo.CheckForUpdates(Assembly.GetExecutingAssembly().GetName()?.Version);
// get latest spt version
InstallButtonText = "Getting latest release ...";
InstallButtonCheckState = StatusSpinner.SpinnerState.Running;
var progress = new Progress<double>((d) => { });
var akiReleaseInfoFile =
await DownloadCacheHelper.DownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
progress);
if (akiReleaseInfoFile == null)
var SPTReleaseInfoFile =
await DownloadCacheHelper.GetOrDownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
progress, DownloadCacheHelper.SuggestedTtl);
if (SPTReleaseInfoFile == null)
{
InstallButtonText = "Could not get SPT release metadata";
InstallButtonCheckState = StatusSpinner.SpinnerState.Error;
return;
}
var akiReleaseInfo =
JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(akiReleaseInfoFile.FullName));
if (akiReleaseInfo == null)
var SPTReleaseInfo =
JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(SPTReleaseInfoFile.FullName));
if (SPTReleaseInfo == null)
{
InstallButtonText = "Could not parse latest SPT release";
InstallButtonCheckState = StatusSpinner.SpinnerState.Error;
return;
}
InstallButtonText = $"Start Install: SPT v{akiReleaseInfo.AkiVersion}";
InstallButtonText = $"Start Install: SPT v{SPTReleaseInfo.SPTVersion}";
InstallButtonCheckState = StatusSpinner.SpinnerState.OK;
AllowDetailsButton = true;

View File

@ -48,6 +48,11 @@ public class ViewModelBase : ReactiveObject, IActivatableViewModel, IRoutableVie
Dispatcher.UIThread.InvokeAsync(() => { HostScreen.Router.Navigate.Execute(ViewModel); });
}
public void NavigateBack()
{
Dispatcher.UIThread.InvokeAsync(() => { HostScreen.Router.NavigateBack.Execute(); });
}
public ViewModelBase(IScreen Host)
{
HostScreen = Host;

View File

@ -0,0 +1,65 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.Views.InstallPathSelectionView">
<Grid RowDefinitions="10,*,Auto,*,10" ColumnDefinitions="10,*,Auto,10">
<!-- Path Controls Grid -->
<Grid Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2"
RowDefinitions="Auto,Auto" ColumnDefinitions="*,Auto"
VerticalAlignment="Center"
>
<Label Grid.Row="0" Grid.Column="0" Content="Install Folder Path" FontSize="20"/>
<TextBox Grid.Row="1" Grid.Column="0"
TextChanged="TextBox_OnTextChanged"
Watermark="Where we dropping?"
FontSize="16"
Text="{Binding SelectedPath}"
Classes.hasErrors="{Binding !ValidPath}"
>
<TextBox.Styles>
<Style Selector="TextBox.hasErrors">
<Setter Property="Foreground" Value="Red"/>
</Style>
</TextBox.Styles>
</TextBox>
<Button Grid.Row="1" Grid.Column="1"
CornerRadius="20"
Margin="10 0 0 0"
Command="{Binding SelectFolderCommand}"
>
<StackPanel Orientation="Horizontal">
<Path Data="{StaticResource OpenFolder}" Fill="{Binding $parent[Button].Foreground}"
VerticalAlignment="Center"/>
<Label Content="Select Folder"/>
</StackPanel>
</Button>
</Grid>
<!-- Validation error text -->
<TextBlock Grid.Row="3" Grid.Column="1" Text="{Binding ErrorMessage}"
TextWrapping="Wrap"
FontSize="16" Foreground="red" FontWeight="SemiBold"
IsVisible="{Binding !ValidPath}"
/>
<!-- Next button -->
<Button Grid.Row="3" Grid.Column="2"
MinWidth="100"
MinHeight="30"
FontSize="16"
CornerRadius="20"
FontWeight="SemiBold"
VerticalAlignment="Bottom"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Classes="yellow"
Content="Next"
Command="{Binding NextCommand}"
IsEnabled="{Binding ValidPath}"
/>
</Grid>
</UserControl>

View File

@ -0,0 +1,18 @@
using Avalonia.Controls;
using Avalonia.ReactiveUI;
using SPTInstaller.ViewModels;
namespace SPTInstaller.Views;
public partial class InstallPathSelectionView : ReactiveUserControl<InstallPathSelectionViewModel>
{
public InstallPathSelectionView()
{
InitializeComponent();
}
private void TextBox_OnTextChanged(object? sender, TextChangedEventArgs e)
{
ViewModel?.ValidatePath();
}
}

View File

@ -9,10 +9,10 @@
<Grid ColumnDefinitions="*, 2*">
<cc:ProgressableTaskList Tasks="{Binding MyTasks}"
Padding="20"
Background="{StaticResource AKI_Background_Dark}"
Background="{StaticResource SPT_Background_Dark}"
PendingColor="Gray"
RunningColor="DodgerBlue"
CompletedColor="{StaticResource AKI_Brush_Yellow}" />
CompletedColor="{StaticResource SPT_Brush_Yellow}" />
<cc:TaskDetails Grid.Column="1"
Message="{Binding CurrentTask.StatusMessage}"
Details="{Binding CurrentTask.StatusDetails}"

View File

@ -0,0 +1,31 @@
<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.InstallerUpdateView">
<Grid RowDefinitions="10,Auto,*,10" ColumnDefinitions="10,*,10">
<StackPanel Grid.Row="1" Grid.Column="1" IsVisible="{Binding UpdateInfo.UpdateAvailable}">
<Label Content="{Binding UpdateInfo.NewVersion, StringFormat='{}Installer Change Log for {0}'}" FontSize="18" FontWeight="SemiBold"
/>
<Separator Margin="0 10" Padding="0" Background="{StaticResource SPT_Yellow}"/>
<ScrollViewer MaxHeight="250" Background="#323232">
<TextBlock Text="{Binding UpdateInfo.ChangeLog}"
TextWrapping="Wrap" MinHeight="100"
Margin="10"
/>
</ScrollViewer>
</StackPanel>
<cc:UpdateButton Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center"
IsIndeterminate="{Binding UpdateInfo.CheckingForUpdates}"
InfoText="{Binding UpdateInfo.UpdateInfoText}"
Updating="{Binding UpdateInfo.Updating}"
DismissCommand="{Binding NotNowCommand}"
UpdateCommand="{Binding UpdateInstallCommand}"
DownloadProgress="{Binding UpdateInfo.DownloadProgress}"
UpdateAvailable="{Binding UpdateInfo.UpdateAvailable}"
CheckingForUpdate="{Binding UpdateInfo.CheckingForUpdates}"
/>
</Grid>
</UserControl>

View File

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

View File

@ -8,14 +8,14 @@
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.Views.MainWindow"
Icon="/Assets/icon.ico"
Icon="/Assets/spt_installer.ico"
Title="SPT Installer"
Height="450" Width="750"
WindowStartupLocation="CenterScreen"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="-1"
Background="{StaticResource AKI_Background_Light}"
Background="{StaticResource SPT_Background_Light}"
MinWidth="800" MinHeight="400">
<Window.Styles>
@ -32,7 +32,7 @@
XButtonCommand="{Binding CloseCommand}"
MinButtonCommand="{Binding MinimizeCommand}" />
<dialogHost:DialogHost Grid.Row="1" Background="{StaticResource AKI_Background_Light}">
<dialogHost:DialogHost Grid.Row="1" Background="{StaticResource SPT_Background_Light}">
<rxui:RoutedViewHost Router="{Binding Router}" />
</dialogHost:DialogHost>
</Grid>

View File

@ -15,7 +15,7 @@
</UserControl.Styles>
<Grid ColumnDefinitions="*,AUTO,*" RowDefinitions="*,AUTO,20,AUTO,*"
<Grid ColumnDefinitions="*,AUTO,*" RowDefinitions="*,AUTO,20,AUTO,20,Auto,Auto,*"
Classes.error="{Binding HasErrors}">
<Label Grid.Column="1" Grid.Row="1"
@ -35,8 +35,18 @@
VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
Padding="20 10" />
<cc:CacheInfo Grid.Row="4" Grid.ColumnSpan="3" Padding="10" Margin="10 0 0 0"
<StackPanel Grid.Row="5" Grid.Column="1" Orientation="Horizontal" Spacing="10">
<CheckBox IsChecked="{Binding OpenInstallFolder}" Content="Open Install Folder" IsVisible="{Binding ShowOptions}"/>
<CheckBox IsChecked="{Binding AddShortcuts}" Content="Add Desktop Shortcuts" IsVisible="{Binding ShowOptions}"/>
</StackPanel>
<cc:CacheInfo Grid.Row="7" Grid.ColumnSpan="3" Padding="10" Margin="10 0 0 0"
VerticalAlignment="Bottom"
InfoText="{Binding CacheInfoText}" State="{Binding CacheCheckState}" />
InfoText="{Binding CacheInfoText}" State="{Binding CacheCheckState}"
/>
<Button Grid.Row="7" Grid.Column="2" Classes="link" Content="{Binding ClipCommandText}"
Command="{Binding CopyLogFileToClipboard}" HorizontalAlignment="Right" VerticalAlignment="Bottom"
/>
</Grid>
</UserControl>

View File

@ -0,0 +1,55 @@
<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.OverviewView">
<Grid RowDefinitions="10,Auto,Auto,Auto,Auto,*,10" ColumnDefinitions="10,*,Auto,10">
<!-- Overview text -->
<Label Grid.Row="1" Grid.Column="1" Content="This installer will:" FontSize="20" Margin="0 5"
Foreground="{StaticResource SPT_Yellow}"
/>
<!-- Overview info -->
<StackPanel Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2">
<Label>◉ Check dependencies are installed, such as .NET</Label>
<Label>◉ Automatically locate and copy your EFT client files to the path you supply on the next page</Label>
<Label>◉ Downgrade your client files to the version SPT uses, if needed</Label>
<Label>◉ Download and extract the SPT release files</Label>
</StackPanel>
<!-- Notes text -->
<Label Grid.Row="3" Grid.Column="1" Content="Additional Notes:" FontSize="20" Margin="0 5"
Foreground="{StaticResource SPT_Yellow}"
/>
<!-- Notes info -->
<StackPanel Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2">
<Label>◉ You do not need to install SPT in the same drive as EFT</Label>
<Label Margin="0" Padding="0">
<StackPanel Orientation="Horizontal">
<Label VerticalAlignment="Center">◉ This tool does</Label>
<TextBlock TextDecorations="Underline" FontWeight="SemiBold" Foreground="Crimson" VerticalAlignment="Center">NOT</TextBlock>
<Label VerticalAlignment="Center">update an existing SPT install</Label>
</StackPanel>
</Label>
</StackPanel>
<!-- Next button -->
<Button Grid.Row="5" Grid.Column="2"
MinWidth="100"
MinHeight="30"
FontSize="16"
CornerRadius="20"
FontWeight="SemiBold"
VerticalAlignment="Bottom"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Classes="yellow"
Content="Next"
Command="{Binding NextCommand}"
IsEnabled="{Binding ValidPath}"
/>
</Grid>
</UserControl>

View File

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

View File

@ -93,19 +93,5 @@
IsVisible="{Binding !AllowInstall}" />
</StackPanel>
</Button>
<!-- Update installer button -->
<cc:UpdateButton Grid.Column="2" Grid.Row="3"
IsVisible="{Binding UpdateInfo.Show}"
IsEnabled="{Binding UpdateInfo.Show}"
IsIndeterminate="{Binding UpdateInfo.CheckingForUpdates}"
InfoText="{Binding UpdateInfo.UpdateInfoText}"
Updating="{Binding UpdateInfo.Updating}"
DismissCommand="{Binding DismissUpdateCommand}"
UpdateCommand="{Binding UpdateInstallerCommand}"
WhatsNewCommand="{Binding WhatsNewCommand}"
DownloadProgress="{Binding UpdateInfo.DownloadProgress}"
UpdateAvailable="{Binding UpdateInfo.UpdateAvailable}"
CheckingForUpdate="{Binding UpdateInfo.CheckingForUpdates}" />
</Grid>
</UserControl>