Compare commits

..

No commits in common. "e93f0d62a62ae71b4d338933431d4915ace71fc9" and "ec3bf679956ad44225dc2d47169f7147cde59fcc" have entirely different histories.

55 changed files with 419 additions and 1246 deletions

View File

@ -19,12 +19,7 @@
<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 Installer made for EFT.
# SPT-AKI 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 SPT version, if so skip patcher process
- Checks if gameversion matches aki 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 SPT
- Deletes both Patcher and SPT zips at the end
- Extracts Aki
- Deletes both Patcher and AKI zips at the end

View File

@ -16,21 +16,21 @@
<Application.Resources>
<!-- Colors -->
<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>
<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>
<!-- Brushes -->
<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" />
<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" />
<!-- Path Geometry -->
<PathGeometry x:Key="CircledCheck"
@ -48,8 +48,5 @@
<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,5 +1,4 @@
using System.Diagnostics;
using System.Linq;
using System.Linq;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
@ -8,35 +7,12 @@ 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
{
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);
}
private readonly string _logPath = Path.Join(Environment.CurrentDirectory, "spt-aki-installer_.log");
public override void Initialize()
{
@ -45,8 +21,9 @@ public partial class App : Application
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.WriteTo
.File(path: LogPath,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information)
.File(path: _logPath,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information,
rollingInterval: RollingInterval.Day)
.CreateLogger();
RxApp.DefaultExceptionHandler = Observer.Create<Exception>((exception) =>
@ -59,38 +36,25 @@ public partial class App : Application
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var data = ServiceHelper.Get<InternalData>() ?? throw new Exception("failed to get internal data");
data.DebugMode = false;
var providedPath = "";
if (desktop.Args != null)
var debug = desktop.Args != null && desktop.Args.Any(x => x.ToLower() == "debug");
if (debug)
{
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: LogDebugPath,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Debug)
.File(path: _logPath,
restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Debug,
rollingInterval: RollingInterval.Day)
.CreateLogger();
Trace.Listeners.Add(new SerilogTraceListener.SerilogTraceListener());
System.Diagnostics.Trace.Listeners.Add(new SerilogTraceListener.SerilogTraceListener());
Log.Debug("TraceListener is registered");
}
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(providedPath),
DataContext = new MainWindowViewModel(debug),
};
}

View File

@ -2,14 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cc="using:SPTInstaller.CustomControls">
<Design.PreviewWith>
<StackPanel Spacing="5" Background="{StaticResource SPT_Background_Dark}">
<StackPanel Spacing="5" Background="{StaticResource AKI_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>
@ -17,30 +16,30 @@
<!-- TitleBar Styles -->
<Style Selector="cc|TitleBar">
<Setter Property="Background" Value="{StaticResource SPT_Background_Dark}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Foreground_Light}" />
<Setter Property="ButtonForeground" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}" />
<Setter Property="ButtonForeground" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
</Style>
<Style Selector="cc|TitleBar.versiontag">
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderThickness" Value="0 0 0 2" />
</Style>
<!-- TextBox Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/TextBox.xaml -->
<Style Selector="TextBox">
<Setter Property="Background" Value="{StaticResource SPT_Background_Light}" />
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Lighter}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
</Style>
<Style Selector="TextBox:focus">
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Lighter}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
</Style>
<Style Selector="TextBox:pointerover">
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Lighter}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
</Style>
<Style Selector="TextBox:pointerover /template/ Border#PART_BorderElement">
@ -65,23 +64,23 @@
<Style Selector="TextBox:focus /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<!-- TextBlock Styles -->
<Style Selector="TextBlock">
<Setter Property="Foreground" Value="{StaticResource SPT_Foreground_Light}" />
<Setter Property="Foreground" Value="{StaticResource AKI_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 SPT_Foreground_Light}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Foreground_Light}" />
</Style>
<Style Selector="Label.yellow">
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
</Style>
<Style Selector="Label.dark">
@ -95,8 +94,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 SPT_Brush_Yellow}" />
<Setter Property="Background" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
</Style>
<Style Selector="ProgressBar.error">
@ -104,7 +103,7 @@
<Style.Animations>
<Animation Duration="0:0:0.5" FillMode="Forward">
<KeyFrame Cue="0%">
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Value" Value="0" />
</KeyFrame>
<KeyFrame Cue="100%">
@ -118,36 +117,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 SPT_Background_Dark}" />
<Setter Property="Background" Value="{StaticResource AKI_Background_Dark}" />
</Style>
<!-- Button Styles -->
<!-- SourceRef: https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Themes.Fluent/Controls/Button.xaml -->
<Style Selector="Button">
<Setter Property="Background" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource SPT_White}" />
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource AKI_White}" />
</Style>
<Style Selector="Button:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource SPT_LightGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_LightGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource SPT_White}" />
<Setter Property="Background" Value="{StaticResource AKI_LightGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_LightGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource AKI_White}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}" />
</Style>
<Style Selector="Button:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
</Style>
<!-- Button yellow -->
<Style Selector="Button.yellow">
<Setter Property="Background" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Background_Dark}" />
<Setter Property="Background" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
@ -157,37 +156,37 @@
<Style Selector="Button.yellow:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Gold" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource SPT_Background_Dark}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Background_Dark}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button.yellow:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource SPT_Brush_Lighter}" />
<Setter Property="Background" Value="{StaticResource AKI_Brush_Lighter}" />
</Style>
<Style Selector="Button.yellow:disabled /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
</Style>
<!-- Button outlined Style -->
<Style Selector="Button.outlined">
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Lighter}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style Selector="Button.outlined:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderThickness" Value="2" />
</Style>
<Style Selector="Button.outlined:pressed /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
@ -195,7 +194,7 @@
<!-- Button Link Style -->
<Style Selector="Button.link">
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Lighter}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0 0 0 1" />
@ -209,22 +208,22 @@
</Style>
<Style Selector="Button.link:pointerover TextBlock">
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
</Style>
<Style Selector="Button.link:pressed TextBlock">
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
</Style>
<Style Selector="Button.link:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_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 SPT_Brush_DarkGrayBlue}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0 0 0 1" />
@ -232,21 +231,21 @@
<!-- Button outlinedTLCorner Style -->
<Style Selector="Button.outlinedTLCorner">
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Lighter}" />
<Setter Property="Foreground" Value="{StaticResource AKI_Brush_Lighter}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderThickness" Value="2 2 0 0" />
</Style>
<Style Selector="Button.outlinedTLCorner:pointerover /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_Yellow}" />
<Setter Property="BorderThickness" Value="2 2 0 0" />
</Style>
<Style Selector="Button.outlinedTLCorner:pressed /template/ ContentPresenter">
<Setter Property="TextBlock.Foreground" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="TextBlock.Foreground" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
@ -259,39 +258,13 @@
</Style>
<Style Selector="Button.icon:pointerover">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource SPT_Brush_Yellow}" />
<Setter Property="Foreground" Value="{StaticResource AKI_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 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}"/>
<Setter Property="Foreground" Value="{StaticResource AKI_DarkGrayBlue}"></Setter>
</Style>
</Styles>

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

View File

@ -0,0 +1,23 @@
<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

@ -0,0 +1,27 @@
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 SPT_Background_Light}">
Background="{StaticResource AKI_Background_Light}">
<TextBlock Grid.Row="1" Grid.Column="1" Grid.ColumnSpan="4"
Text="{Binding Message, RelativeSource={RelativeSource AncestorType=UserControl}}"
TextWrapping="Wrap" />

View File

@ -6,16 +6,17 @@
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"
Background="{StaticResource SPT_Background_Light}">
<Grid RowDefinitions="AUTO,AUTO,AUTO,*,AUTO" ColumnDefinitions="*,AUTO, AUTO"
Background="{StaticResource AKI_Background_Light}">
<Label Content="What is the installer cache for?" FontSize="20"
Foreground="{StaticResource SPT_Brush_Yellow}" />
Foreground="{StaticResource AKI_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>
@ -24,6 +25,7 @@ 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"
@ -33,16 +35,10 @@ 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}" />
<StackPanel Orientation="Horizontal" Grid.Row="4" Grid.Column="1" Spacing="10">
<Button Content="Move Downloaded Patcher"
<Button Grid.Row="4" Grid.Column="1" Content="Move Downloaded Patcher" Margin="0 0 10 0"
Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=MoveDownloadsPatcherToCache}" />
<Button Content="Clear Metadata Cache"
Command="{Binding RelativeSource={RelativeSource AncestorType=UserControl}, Path=ClearCachedMetaData}"
/>
<Button Content="Close" Classes="yellow"
<Button Grid.Row="4" Grid.Column="2" Content="Close" Classes="yellow"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}" />
</StackPanel>
</Grid>
</UserControl>

View File

@ -3,10 +3,7 @@ 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;
@ -57,56 +54,13 @@ 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 = KnownFolders.GetPath(KnownFolder.Downloads);
var downloadsPath =
Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "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 SPT_Background_Dark}" />
<Setter Property="Foreground" Value="{StaticResource AKI_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 SPT_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource SPT_Brush_DarkGrayBlue}" />
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_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 SPT_Brush_DarkGrayBlue}" />
<Setter Property="BorderBrush" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
</Style>
<Style Selector="Button.selected">
<Setter Property="Background" Value="{StaticResource SPT_Brush_LightGrayBlue}" />
<Setter Property="Background" Value="{StaticResource AKI_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 SPT_Background_Dark}"
Fill="{StaticResource AKI_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 SPT_Brush_DarkGrayBlue}" />
<Setter Property="Background" Value="{StaticResource AKI_Brush_DarkGrayBlue}" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="Button:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource SPT_Background_Light}" />
<Setter Property="Background" Value="{StaticResource AKI_Background_Light}" />
</Style>
</Button.Styles>
</Button>

View File

@ -26,6 +26,8 @@
<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,6 +50,15 @@ 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,6 +1,7 @@
using System.Net.Http;
using System.Threading.Tasks;
using Serilog;
using SPTInstaller.Models;
namespace SPTInstaller.Helpers;
@ -8,7 +9,6 @@ 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 CheckCacheHash(string fileName, string expectedHash, out FileInfo cachedFile)
=> CheckCacheHash(new FileInfo(Path.Join(CachePath, fileName)), expectedHash, out cachedFile);
public static bool CheckCache(string fileName, string expectedHash, out FileInfo cachedFile)
=> CheckCache(new FileInfo(Path.Join(CachePath, fileName)), expectedHash, out cachedFile);
private static bool CheckCacheHash(FileInfo cacheFile, string expectedHash, out FileInfo fileInCache)
private static bool CheckCache(FileInfo cacheFile, string expectedHash, out FileInfo fileInCache)
{
fileInCache = cacheFile;
@ -86,44 +86,6 @@ 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>
@ -145,20 +107,7 @@ public static class DownloadCacheHelper
// Use the provided extension method
using (var file = new FileStream(outputFile.FullName, FileMode.Create, FileAccess.Write, FileShare.None))
{
if (!await _httpClient.DownloadDataAsync(targetLink, file, progress))
{
Log.Error($"Download failed: {targetLink}");
outputFile.Refresh();
if (outputFile.Exists)
{
outputFile.Delete();
return null;
}
}
}
await _httpClient.DownloadDataAsync(targetLink, file, progress);
outputFile.Refresh();
@ -218,34 +167,6 @@ 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>
@ -260,10 +181,9 @@ public static class DownloadCacheHelper
{
try
{
if (CheckCacheHash(fileName, expectedHash, out var cacheFile))
if (CheckCache(fileName, expectedHash, out var cacheFile))
return cacheFile;
Log.Information($"Downloading File: {targetLink}");
return await DownloadFileAsync(fileName, targetLink, progress);
}
catch (Exception ex)
@ -286,7 +206,7 @@ public static class DownloadCacheHelper
{
try
{
if (CheckCacheHash(fileName, expectedHash, out var cacheFile))
if (CheckCache(fileName, expectedHash, out var cacheFile))
return cacheFile;
return await DownloadFileAsync(fileName, fileDownloadStream);

View File

@ -9,6 +9,98 @@ 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>[^\\]+)");
@ -31,57 +123,13 @@ public static class FileHelper
{
try
{
var allFiles = sourceDir.GetFiles("*", SearchOption.AllDirectories);
var fileCopies = new List<CopyInfo>();
int count = 0;
var iterateDirectoriesResult = IterateDirectories(sourceDir, targetDir, exclusions ??= new string[0]);
// 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));
if (!iterateDirectoriesResult.Succeeded) return iterateDirectoriesResult;
var currentFileRelativePath = file.FullName.Replace(sourceDir.FullName, "");
var iterateFilesResult = IterateFiles(sourceDir, targetDir, exclusions ??= new string[0], updateCallback);
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;
}
}
if (!iterateFilesResult.Succeeded) return iterateDirectoriesResult;
return Result.FromSuccess();
}
@ -96,7 +144,6 @@ public static class FileHelper
{
try
{
Log.Debug($"Starting StreamAssemblyResourceOut, resourcename: {resourceName}, outputFilePath: {outputFilePath}");
var assembly = Assembly.GetExecutingAssembly();
FileInfo outputFile = new FileInfo(outputFilePath);
@ -120,7 +167,6 @@ public static class FileHelper
}
outputFile.Refresh();
return outputFile.Exists;
}
catch (Exception ex)
@ -130,21 +176,12 @@ 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<bool> DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination,
public static async Task 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,21 +18,20 @@ public static class HttpClientProgressExtensions
if (progress is null || !contentLength.HasValue)
{
await download.CopyToAsync(destination);
return true;
return;
}
// Such progress and contentLength much reporting Wow!
var progressWrapper = new Progress<long>(totalBytes =>
progress.Report(GetProgressPercentage(totalBytes, contentLength.Value)));
var readBytes = await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken);
return readBytes == contentLength.Value;
await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken);
}
}
float GetProgressPercentage(float totalBytes, float currentBytes) => (totalBytes / currentBytes) * 100f;
}
static async Task<long> CopyToAsync(this Stream source, Stream destination, int bufferSize,
static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize,
IProgress<long> progress = null, CancellationToken cancellationToken = default(CancellationToken))
{
if (bufferSize < 0)
@ -56,7 +55,5 @@ public static class HttpClientProgressExtensions
totalBytesRead += bytesRead;
progress?.Report(totalBytesRead);
}
return totalBytesRead;
}
}

View File

@ -20,10 +20,7 @@ public static class PreCheckHelper
?.GetValue("InstallLocation");
var info = (uninstallStringValue is string key) ? new DirectoryInfo(key) : null;
if (info == null)
return null;
return Path.TrimEndingDirectorySeparator(info.FullName);
return 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 SPT");
return Result.FromSuccess("Patcher Finished Successfully, extracting Aki");
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 'SPT_Patches' is missing");
return Result.FromError("Patchers Folder called 'Aki_Patches' is missing");
case PatcherExitCode.MissingFile:
return Result.FromError("Vital EFT files were not found. The installer is unable to continue. Please reinstall EFT and try again.");
return Result.FromError("EFT files was missing a Vital file to continue");
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.CheckCacheHash("patcher", _expectedPatcherHash, out var cacheFile))
if (DownloadCacheHelper.CheckCache("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> DownloadSPTFromMirrors(IProgress<double> progress)
private async Task<IResult> DownloadSptAkiFromMirrors(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", mirror.DownloadUrl, progressStyle: ProgressStyle.Indeterminate);
SetStatus("Downloading SPT-AKI", mirror.DownloadUrl, progressStyle: ProgressStyle.Indeterminate);
_data.SPTZipInfo =
await DownloadCacheHelper.GetOrDownloadFileAsync("SPT", mirror.DownloadUrl, progress, mirror.Hash);
_data.AkiZipInfo =
await DownloadCacheHelper.GetOrDownloadFileAsync("sptaki", mirror.DownloadUrl, progress, mirror.Hash);
if (_data.SPTZipInfo != null)
if (_data.AkiZipInfo != null)
{
return Result.FromSuccess();
}
}
return Result.FromError("Failed to download SPT");
return Result.FromError("Failed to download spt-aki");
}
public override async Task<IResult> TaskOperation()
@ -110,6 +110,6 @@ public class DownloadTask : InstallerTaskBase
}
}
return await DownloadSPTFromMirrors(progress);
return await DownloadSptAkiFromMirrors(progress);
}
}

View File

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

View File

@ -1,24 +0,0 @@
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("Your Battlestate Games Launcher is open. Please close it to continue installing SPT",
: PreCheckResult.FromError("Eft launcher is open. Please close it to install 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. This is most likely because EFT is not installed");
return PreCheckResult.FromError("An error occurred while getting the EFT source directory size");
}
var availableSize = DriveInfo.GetDrives()

View File

@ -24,77 +24,66 @@ 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 SPTReleaseInfoFile =
await DownloadCacheHelper.GetOrDownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
progress, DownloadCacheHelper.SuggestedTtl);
if (SPTReleaseInfoFile == null)
var akiReleaseInfoFile =
await DownloadCacheHelper.DownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
progress);
if (akiReleaseInfoFile == null)
{
return Result.FromError("Failed to download release metadata, try clicking the 'Whats this' button below followed by the 'Clear Metadata cache' button");
return Result.FromError("Failed to download release metadata");
}
var SPTReleaseInfo =
JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(SPTReleaseInfoFile.FullName));
var akiReleaseInfo =
JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(akiReleaseInfoFile.FullName));
SetStatus("Checking for Patches", "", null, ProgressStyle.Indeterminate);
var SPTPatchMirrorsFile =
await DownloadCacheHelper.GetOrDownloadFileAsync("mirrors.json", DownloadCacheHelper.PatchMirrorUrl,
progress, DownloadCacheHelper.SuggestedTtl);
var akiPatchMirrorsFile =
await DownloadCacheHelper.DownloadFileAsync("mirrors.json", DownloadCacheHelper.PatchMirrorUrl,
progress);
if (SPTPatchMirrorsFile == null)
if (akiPatchMirrorsFile == null)
{
return Result.FromError("Failed to download patch mirror data, try clicking the 'Whats this' button below followed by the 'Clear Metadata cache' button");
return Result.FromError("Failed to download patch mirror data");
}
var patchMirrorInfo =
JsonConvert.DeserializeObject<PatchInfo>(File.ReadAllText(SPTPatchMirrorsFile.FullName));
JsonConvert.DeserializeObject<PatchInfo>(File.ReadAllText(akiPatchMirrorsFile.FullName));
if (SPTReleaseInfo == null || patchMirrorInfo == null)
if (akiReleaseInfo == null || patchMirrorInfo == null)
{
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");
return Result.FromError("An error occurred while deserializing aki or patch data");
}
_data.ReleaseInfo = SPTReleaseInfo;
_data.ReleaseInfo = akiReleaseInfo;
_data.PatchInfo = patchMirrorInfo;
int intSPTVersion = int.Parse(SPTReleaseInfo.ClientVersion);
int intAkiVersion = int.Parse(akiReleaseInfo.ClientVersion);
int intGameVersion = int.Parse(_data.OriginalGameVersion);
// 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
// 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
// : it's probably a low chance though
bool patchNeedCheck = intGameVersion > intSPTVersion;
bool patchNeedCheck = intGameVersion > intAkiVersion;
if (intGameVersion < intSPTVersion)
if (intGameVersion < intAkiVersion)
{
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");
return Result.FromError("Your client is outdated. Please update EFT");
}
if (intGameVersion == intSPTVersion)
if (intGameVersion == intAkiVersion)
{
patchNeedCheck = false;
}
bool sptClientIsOutdated = intSPTVersion != patchMirrorInfo.TargetClientVersion && patchNeedCheck;
bool liveClientIsOutdated = intGameVersion != patchMirrorInfo.SourceClientVersion && patchNeedCheck;
if (sptClientIsOutdated)
if ((intGameVersion != patchMirrorInfo.SourceClientVersion ||
intAkiVersion != patchMirrorInfo.TargetClientVersion) && patchNeedCheck)
{
return Result.FromError(
"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");
"No patcher available for your version.\nA patcher is usually created within 24 hours of an EFT update.");
}
_data.PatchNeeded = patchNeedCheck;
string status =
$"Current Release: {SPTReleaseInfo.ClientVersion} - {(_data.PatchNeeded ? "Patch Available" : "No Patch Needed")}";
$"Current Release: {akiReleaseInfo.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("Extracting Patcher", "", 0);
SetStatus("Extrating 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.SPTZipInfo, targetInstallDirInfo, progress);
var extractReleaseResult = ZipHelper.Decompress(_data.AkiZipInfo, targetInstallDirInfo, progress);
if (!extractReleaseResult.Succeeded)
{

View File

@ -1,24 +0,0 @@
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,21 +9,9 @@ namespace SPTInstaller.Models;
public class InstallerUpdateInfo : ReactiveObject
{
private Version? _newVersion;
public Version? NewVersion { get; private set; }
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);
}
public string ChangeLog = "";
private string _updateInfoText = "";
@ -33,6 +21,14 @@ 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
@ -127,6 +123,7 @@ public class InstallerUpdateInfo : ReactiveObject
}
UpdateInfoText = infoText;
Show = updateAvailable;
CheckingForUpdates = false;
UpdateAvailable = updateAvailable;
}
@ -137,13 +134,14 @@ public class InstallerUpdateInfo : ReactiveObject
return;
UpdateInfoText = "Checking for installer updates";
Show = true;
CheckingForUpdates = true;
try
{
var installerInfoFile =
await DownloadCacheHelper.GetOrDownloadFileAsync("installer.json", DownloadCacheHelper.InstallerInfoUrl, null
, DownloadCacheHelper.SuggestedTtl);
await DownloadCacheHelper.DownloadFileAsync("installer.json", DownloadCacheHelper.InstallerInfoUrl,
null);
if (installerInfoFile == null)
{

View File

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

View File

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

View File

@ -44,7 +44,6 @@ 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

@ -1,23 +0,0 @@
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,69 +5,23 @@
Clear-Host
Write-Host "Stopping installer ... " -ForegroundColor cyan -NoNewLine
Write-Host "Stopping installer ..."
$installer = Stop-Process -Name "SPTInstaller" -ErrorAction SilentlyContinue
if ($installer -ne $null)
{
Write-Warning "Something went wrong, couldn't stop installer process'"
Write-Host "Something went wrong, couldn't stop installer process'"
return;
}
Write-Host "OK" -ForegroundColor green
Write-Host "Copying new installer ..."
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
}
Import-Module BitsTransfer
Write-Host "Copying new installer ... " -ForegroundColor cyan
Start-BitsTransfer -Source $source -Destination $destination
$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-Module BitsTransfer
# remove the new installer from the cache folder after it is copied
Remove-Item -Path $source

View File

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

@ -1,157 +0,0 @@
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

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

View File

@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.Diagnostics;
using Avalonia;
using Avalonia;
using ReactiveUI;
using Serilog;
using SPTInstaller.CustomControls;
@ -8,10 +6,6 @@ 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;
@ -41,14 +35,6 @@ 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
@ -57,74 +43,6 @@ 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
@ -135,7 +53,8 @@ public class MessageViewModel : ViewModelBase
public ICommand CloseCommand { get; set; } = ReactiveCommand.Create(() =>
{
if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopApp)
if (Application.Current.ApplicationLifetime is
Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{
desktopApp.MainWindow.Close();
}
@ -145,75 +64,6 @@ 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(() =>
{
@ -227,7 +77,6 @@ public class MessageViewModel : ViewModelBase
if (result.Succeeded)
{
Log.Information(Message);
ShowOptions = true;
return;
}

View File

@ -1,23 +0,0 @@
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,5 +1,6 @@
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Threading;
@ -31,8 +32,16 @@ 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
@ -109,13 +118,12 @@ public class PreChecksViewModel : ViewModelBase
});
}
public PreChecksViewModel(IScreen host) : base(host)
public PreChecksViewModel(IScreen host, bool debugging) : base(host)
{
Debugging = debugging;
var data = ServiceHelper.Get<InternalData?>();
var installer = ServiceHelper.Get<InstallController?>();
Debugging = data.DebugMode;
installer.RecheckRequested += ReCheckRequested;
InstallButtonText = "Please wait ...";
@ -130,6 +138,7 @@ public class PreChecksViewModel : ViewModelBase
data.OriginalGamePath = PreCheckHelper.DetectOriginalGamePath();
data.TargetInstallPath = Environment.CurrentDirectory;
InstallPath = data.TargetInstallPath;
Log.Information($"Install Path: {FileHelper.GetRedactedPath(InstallPath)}");
@ -138,7 +147,7 @@ public class PreChecksViewModel : ViewModelBase
if (data.OriginalGamePath == null)
{
NavigateTo(new MessageViewModel(HostScreen,
Result.FromError("Could not find where you installed EFT.\n\nDo you own and have the game installed?")));
Result.FromError("Could not find EFT install.\n\nDo you own and have the game installed?")));
return;
}
#endif
@ -147,7 +156,7 @@ public class PreChecksViewModel : ViewModelBase
{
Log.CloseAndFlush();
var logFiles = Directory.GetFiles(InstallPath, "spt-installer_*.log");
var logFiles = Directory.GetFiles(InstallPath, "spt-aki-installer_*.log");
// remove log file from original game path if they exist
foreach (var file in logFiles)
@ -163,7 +172,7 @@ public class PreChecksViewModel : ViewModelBase
NavigateTo(new MessageViewModel(HostScreen,
Result.FromError(
"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."),
"Installer is located in EFT's original directory. Please move the installer to a seperate folder as per the guide"),
noLog: true));
return;
}
@ -180,15 +189,13 @@ public class PreChecksViewModel : ViewModelBase
{
Log.Warning("Problem path detected, confirming install path ...");
var confirmation = await DialogHost.Show(new ConfirmationDialog(
$"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}"));
$"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}"));
if (confirmation == null || !bool.TryParse(confirmation.ToString(), out var confirm) ||
!confirm)
{
Log.Information("User declined install path");
NavigateBack();
Log.Information("User declined install path, exiting");
Environment.Exit(0);
}
});
@ -200,7 +207,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. How did you do this?")));
$"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}")));
break;
}
default:
@ -215,7 +222,14 @@ public class PreChecksViewModel : ViewModelBase
{
try
{
App.ReLaunch(true, InstallPath);
var installerPath = Path.Join(_installPath, "SPTInstaller.exe");
Process.Start(new ProcessStartInfo()
{
FileName = installerPath,
Arguments = "debug"
});
Environment.Exit(0);
}
catch (Exception ex)
{
@ -242,42 +256,56 @@ 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 SPTReleaseInfoFile =
await DownloadCacheHelper.GetOrDownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
progress, DownloadCacheHelper.SuggestedTtl);
if (SPTReleaseInfoFile == null)
var akiReleaseInfoFile =
await DownloadCacheHelper.DownloadFileAsync("release.json", DownloadCacheHelper.ReleaseMirrorUrl,
progress);
if (akiReleaseInfoFile == null)
{
InstallButtonText = "Could not get SPT release metadata";
InstallButtonCheckState = StatusSpinner.SpinnerState.Error;
return;
}
var SPTReleaseInfo =
JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(SPTReleaseInfoFile.FullName));
if (SPTReleaseInfo == null)
var akiReleaseInfo =
JsonConvert.DeserializeObject<ReleaseInfo>(File.ReadAllText(akiReleaseInfoFile.FullName));
if (akiReleaseInfo == null)
{
InstallButtonText = "Could not parse latest SPT release";
InstallButtonCheckState = StatusSpinner.SpinnerState.Error;
return;
}
InstallButtonText = $"Start Install: SPT v{SPTReleaseInfo.SPTVersion}";
InstallButtonText = $"Start Install: SPT v{akiReleaseInfo.AkiVersion}";
InstallButtonCheckState = StatusSpinner.SpinnerState.OK;
AllowDetailsButton = true;

View File

@ -48,11 +48,6 @@ 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

@ -1,65 +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"
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

@ -1,18 +0,0 @@
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 SPT_Background_Dark}"
Background="{StaticResource AKI_Background_Dark}"
PendingColor="Gray"
RunningColor="DodgerBlue"
CompletedColor="{StaticResource SPT_Brush_Yellow}" />
CompletedColor="{StaticResource AKI_Brush_Yellow}" />
<cc:TaskDetails Grid.Column="1"
Message="{Binding CurrentTask.StatusMessage}"
Details="{Binding CurrentTask.StatusDetails}"

View File

@ -1,31 +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: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

@ -1,12 +0,0 @@
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/spt_installer.ico"
Icon="/Assets/icon.ico"
Title="SPT Installer"
Height="450" Width="750"
WindowStartupLocation="CenterScreen"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaChromeHints="NoChrome"
ExtendClientAreaTitleBarHeightHint="-1"
Background="{StaticResource SPT_Background_Light}"
Background="{StaticResource AKI_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 SPT_Background_Light}">
<dialogHost:DialogHost Grid.Row="1" Background="{StaticResource AKI_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,20,Auto,Auto,*"
<Grid ColumnDefinitions="*,AUTO,*" RowDefinitions="*,AUTO,20,AUTO,*"
Classes.error="{Binding HasErrors}">
<Label Grid.Column="1" Grid.Row="1"
@ -35,18 +35,8 @@
VerticalContentAlignment="Center" HorizontalContentAlignment="Center"
Padding="20 10" />
<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"
<cc:CacheInfo Grid.Row="4" Grid.ColumnSpan="3" Padding="10" Margin="10 0 0 0"
VerticalAlignment="Bottom"
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"
/>
InfoText="{Binding CacheInfoText}" State="{Binding CacheCheckState}" />
</Grid>
</UserControl>

View File

@ -1,55 +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"
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

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

View File

@ -93,5 +93,19 @@
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>