Compare commits

..

65 Commits

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

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

View File

@ -19,7 +19,12 @@
<entry key="SPTInstaller/CustomControls/UpdateButton.axaml" value="SPTInstaller/SPTInstaller.csproj" /> <entry key="SPTInstaller/CustomControls/UpdateButton.axaml" value="SPTInstaller/SPTInstaller.csproj" />
<entry key="SPTInstaller/CustomControls/UpdateInfoCard.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/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/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" /> <entry key="SPTInstaller/Views/PreChecksView.axaml" value="SPTInstaller/SPTInstaller.csproj" />
</map> </map>
</option> </option>

View File

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

View File

@ -16,21 +16,21 @@
<Application.Resources> <Application.Resources>
<!-- Colors --> <!-- Colors -->
<Color x:Key="AKI_DarkGray">#121212</Color> <Color x:Key="SPT_DarkGray">#121212</Color>
<Color x:Key="AKI_Yellow">#FFC107</Color> <Color x:Key="SPT_Yellow">#FFC107</Color>
<Color x:Key="AKI_White">#FFFFFF</Color> <Color x:Key="SPT_White">#FFFFFF</Color>
<Color x:Key="AKI_Gray">#282828</Color> <Color x:Key="SPT_Gray">#282828</Color>
<Color x:Key="AKI_DarkGrayBlue">#323947</Color> <Color x:Key="SPT_DarkGrayBlue">#323947</Color>
<Color x:Key="AKI_LightGrayBlue">#444259</Color> <Color x:Key="SPT_LightGrayBlue">#444259</Color>
<!-- Brushes --> <!-- Brushes -->
<SolidColorBrush x:Key="AKI_Foreground_Light" Color="{StaticResource AKI_White}" /> <SolidColorBrush x:Key="SPT_Foreground_Light" Color="{StaticResource SPT_White}" />
<SolidColorBrush x:Key="AKI_Background_Light" Color="{StaticResource AKI_Gray}" /> <SolidColorBrush x:Key="SPT_Background_Light" Color="{StaticResource SPT_Gray}" />
<SolidColorBrush x:Key="AKI_Background_Dark" Color="{StaticResource AKI_DarkGray}" /> <SolidColorBrush x:Key="SPT_Background_Dark" Color="{StaticResource SPT_DarkGray}" />
<SolidColorBrush x:Key="AKI_Brush_Yellow" Color="{StaticResource AKI_Yellow}" /> <SolidColorBrush x:Key="SPT_Brush_Yellow" Color="{StaticResource SPT_Yellow}" />
<SolidColorBrush x:Key="AKI_Brush_DarkGrayBlue" Color="{StaticResource AKI_DarkGrayBlue}" /> <SolidColorBrush x:Key="SPT_Brush_DarkGrayBlue" Color="{StaticResource SPT_DarkGrayBlue}" />
<SolidColorBrush x:Key="AKI_Brush_LightGrayBlue" Color="{StaticResource AKI_LightGrayBlue}" /> <SolidColorBrush x:Key="SPT_Brush_LightGrayBlue" Color="{StaticResource SPT_LightGrayBlue}" />
<SolidColorBrush x:Key="AKI_Brush_Lighter" Color="Gainsboro" /> <SolidColorBrush x:Key="SPT_Brush_Lighter" Color="Gainsboro" />
<!-- Path Geometry --> <!-- Path Geometry -->
<PathGeometry x:Key="CircledCheck" <PathGeometry x:Key="CircledCheck"
@ -48,5 +48,8 @@
<PathGeometry x:Key="Bug" <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" 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" /> 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.Resources>
</Application> </Application>

View File

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

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

View File

@ -1,23 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dialogHost="clr-namespace:DialogHostAvalonia;assembly=DialogHost.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.CustomControls.Dialogs.ChangeLogDialog"
MinWidth="400" MaxWidth="600">
<StackPanel>
<Label Content="{Binding Version, RelativeSource={RelativeSource AncestorType=UserControl}, StringFormat='{}Installer Change Log for {0}'}" FontSize="18" FontWeight="SemiBold"
/>
<Separator Margin="0 10" Padding="0" Background="{StaticResource AKI_Yellow}"/>
<ScrollViewer MaxHeight="250">
<TextBlock Text="{Binding Message, RelativeSource={RelativeSource AncestorType=UserControl}}"
TextWrapping="Wrap" MinHeight="100"
/>
</ScrollViewer>
<Button Content="Close" Classes="yellow"
HorizontalAlignment="Right"
Command="{Binding RelativeSource={RelativeSource FindAncestor, AncestorType=dialogHost:DialogHost}, Path=CloseDialogCommand}"
/>
</StackPanel>
</UserControl>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -50,15 +50,6 @@ public partial class UpdateButton : UserControl
set => SetValue(UpdateCommandProperty, value); 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>( public static readonly StyledProperty<bool> UpdatingProperty = AvaloniaProperty.Register<UpdateButton, bool>(
"Updating"); "Updating");

View File

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

View File

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

View File

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

View File

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

View File

@ -38,7 +38,7 @@ public static class ProcessHelper
switch ((PatcherExitCode)process.ExitCode) switch ((PatcherExitCode)process.ExitCode)
{ {
case PatcherExitCode.Success: case PatcherExitCode.Success:
return Result.FromSuccess("Patcher Finished Successfully, extracting Aki"); return Result.FromSuccess("Patcher Finished Successfully, extracting SPT");
case PatcherExitCode.ProgramClosed: case PatcherExitCode.ProgramClosed:
return Result.FromError("Patcher was closed before completing!"); 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"); return Result.FromError("EscapeFromTarkov.exe is missing from the install Path");
case PatcherExitCode.NoPatchFolder: case PatcherExitCode.NoPatchFolder:
return Result.FromError("Patchers Folder called 'Aki_Patches' is missing"); return Result.FromError("Patchers Folder called 'SPT_Patches' is missing");
case PatcherExitCode.MissingFile: case PatcherExitCode.MissingFile:
return Result.FromError("EFT files was missing a Vital file to continue"); return Result.FromError("Vital EFT files were not found. The installer is unable to continue. Please reinstall EFT and try again.");
case PatcherExitCode.PatchFailed: case PatcherExitCode.PatchFailed:
return Result.FromError("A patch failed to apply"); return Result.FromError("A patch failed to apply");
default: 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); SetStatus("Downloading Patcher", "Verifying cached patcher ...", progressStyle: ProgressStyle.Indeterminate);
if (DownloadCacheHelper.CheckCache("patcher", _expectedPatcherHash, out var cacheFile)) if (DownloadCacheHelper.CheckCacheHash("patcher", _expectedPatcherHash, out var cacheFile))
{ {
_data.PatcherZipInfo = cacheFile; _data.PatcherZipInfo = cacheFile;
Log.Information("Using cached file {fileName} - Hash: {hash}", _data.PatcherZipInfo.Name, 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"); return Result.FromError("Failed to download Patcher");
} }
private async Task<IResult> DownloadSptAkiFromMirrors(IProgress<double> progress) private async Task<IResult> DownloadSPTFromMirrors(IProgress<double> progress)
{ {
// Note that GetOrDownloadFileAsync handles the cached file hash check, so we don't need to check it first // 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) foreach (var mirror in _data.ReleaseInfo.Mirrors)
{ {
SetStatus("Downloading SPT-AKI", mirror.DownloadUrl, progressStyle: ProgressStyle.Indeterminate); SetStatus("Downloading SPT", mirror.DownloadUrl, progressStyle: ProgressStyle.Indeterminate);
_data.AkiZipInfo = _data.SPTZipInfo =
await DownloadCacheHelper.GetOrDownloadFileAsync("sptaki", mirror.DownloadUrl, progress, mirror.Hash); await DownloadCacheHelper.GetOrDownloadFileAsync("SPT", mirror.DownloadUrl, progress, mirror.Hash);
if (_data.AkiZipInfo != null) if (_data.SPTZipInfo != null)
{ {
return Result.FromSuccess(); return Result.FromSuccess();
} }
} }
return Result.FromError("Failed to download spt-aki"); return Result.FromError("Failed to download SPT");
} }
public override async Task<IResult> TaskOperation() public override async Task<IResult> TaskOperation()
@ -110,6 +110,6 @@ public class DownloadTask : InstallerTaskBase
} }
} }
return await DownloadSptAkiFromMirrors(progress); return await DownloadSPTFromMirrors(progress);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,6 @@
using Avalonia; using System.Collections.Generic;
using System.Diagnostics;
using Avalonia;
using ReactiveUI; using ReactiveUI;
using Serilog; using Serilog;
using SPTInstaller.CustomControls; using SPTInstaller.CustomControls;
@ -6,6 +8,10 @@ using SPTInstaller.Helpers;
using SPTInstaller.Interfaces; using SPTInstaller.Interfaces;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Input;
using Avalonia.Platform.Storage;
using SPTInstaller.Models;
namespace SPTInstaller.ViewModels; namespace SPTInstaller.ViewModels;
@ -35,6 +41,14 @@ public class MessageViewModel : ViewModelBase
set => this.RaiseAndSetIfChanged(ref _showCloseButton, value); set => this.RaiseAndSetIfChanged(ref _showCloseButton, value);
} }
private bool _showOptions;
public bool ShowOptions
{
get => _showOptions;
set => this.RaiseAndSetIfChanged(ref _showOptions, value);
}
private string _cacheInfoText; private string _cacheInfoText;
public string CacheInfoText public string CacheInfoText
@ -43,6 +57,74 @@ public class MessageViewModel : ViewModelBase
set => this.RaiseAndSetIfChanged(ref _cacheInfoText, value); 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; private StatusSpinner.SpinnerState _cacheCheckState;
public StatusSpinner.SpinnerState CacheCheckState public StatusSpinner.SpinnerState CacheCheckState
@ -53,8 +135,7 @@ public class MessageViewModel : ViewModelBase
public ICommand CloseCommand { get; set; } = ReactiveCommand.Create(() => public ICommand CloseCommand { get; set; } = ReactiveCommand.Create(() =>
{ {
if (Application.Current.ApplicationLifetime is if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopApp)
Avalonia.Controls.ApplicationLifetimes.IClassicDesktopStyleApplicationLifetime desktopApp)
{ {
desktopApp.MainWindow.Close(); desktopApp.MainWindow.Close();
} }
@ -64,6 +145,75 @@ public class MessageViewModel : ViewModelBase
{ {
ShowCloseButton = showCloseButton; ShowCloseButton = showCloseButton;
Message = result.Message; 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(() => Task.Run(() =>
{ {
@ -77,6 +227,7 @@ public class MessageViewModel : ViewModelBase
if (result.Succeeded) if (result.Succeeded)
{ {
Log.Information(Message); Log.Information(Message);
ShowOptions = true;
return; return;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,31 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:cc="using:SPTInstaller.CustomControls"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.Views.InstallerUpdateView">
<Grid RowDefinitions="10,Auto,*,10" ColumnDefinitions="10,*,10">
<StackPanel Grid.Row="1" Grid.Column="1" IsVisible="{Binding UpdateInfo.UpdateAvailable}">
<Label Content="{Binding UpdateInfo.NewVersion, StringFormat='{}Installer Change Log for {0}'}" FontSize="18" FontWeight="SemiBold"
/>
<Separator Margin="0 10" Padding="0" Background="{StaticResource SPT_Yellow}"/>
<ScrollViewer MaxHeight="250" Background="#323232">
<TextBlock Text="{Binding UpdateInfo.ChangeLog}"
TextWrapping="Wrap" MinHeight="100"
Margin="10"
/>
</ScrollViewer>
</StackPanel>
<cc:UpdateButton Grid.Row="2" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center"
IsIndeterminate="{Binding UpdateInfo.CheckingForUpdates}"
InfoText="{Binding UpdateInfo.UpdateInfoText}"
Updating="{Binding UpdateInfo.Updating}"
DismissCommand="{Binding NotNowCommand}"
UpdateCommand="{Binding UpdateInstallCommand}"
DownloadProgress="{Binding UpdateInfo.DownloadProgress}"
UpdateAvailable="{Binding UpdateInfo.UpdateAvailable}"
CheckingForUpdate="{Binding UpdateInfo.CheckingForUpdates}"
/>
</Grid>
</UserControl>

View File

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

View File

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

View File

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

View File

@ -0,0 +1,55 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SPTInstaller.Views.OverviewView">
<Grid RowDefinitions="10,Auto,Auto,Auto,Auto,*,10" ColumnDefinitions="10,*,Auto,10">
<!-- Overview text -->
<Label Grid.Row="1" Grid.Column="1" Content="This installer will:" FontSize="20" Margin="0 5"
Foreground="{StaticResource SPT_Yellow}"
/>
<!-- Overview info -->
<StackPanel Grid.Row="2" Grid.Column="1" Grid.ColumnSpan="2">
<Label>◉ Check dependencies are installed, such as .NET</Label>
<Label>◉ Automatically locate and copy your EFT client files to the path you supply on the next page</Label>
<Label>◉ Downgrade your client files to the version SPT uses, if needed</Label>
<Label>◉ Download and extract the SPT release files</Label>
</StackPanel>
<!-- Notes text -->
<Label Grid.Row="3" Grid.Column="1" Content="Additional Notes:" FontSize="20" Margin="0 5"
Foreground="{StaticResource SPT_Yellow}"
/>
<!-- Notes info -->
<StackPanel Grid.Row="4" Grid.Column="1" Grid.ColumnSpan="2">
<Label>◉ You do not need to install SPT in the same drive as EFT</Label>
<Label Margin="0" Padding="0">
<StackPanel Orientation="Horizontal">
<Label VerticalAlignment="Center">◉ This tool does</Label>
<TextBlock TextDecorations="Underline" FontWeight="SemiBold" Foreground="Crimson" VerticalAlignment="Center">NOT</TextBlock>
<Label VerticalAlignment="Center">update an existing SPT install</Label>
</StackPanel>
</Label>
</StackPanel>
<!-- Next button -->
<Button Grid.Row="5" Grid.Column="2"
MinWidth="100"
MinHeight="30"
FontSize="16"
CornerRadius="20"
FontWeight="SemiBold"
VerticalAlignment="Bottom"
HorizontalContentAlignment="Center"
VerticalContentAlignment="Center"
Classes="yellow"
Content="Next"
Command="{Binding NextCommand}"
IsEnabled="{Binding ValidPath}"
/>
</Grid>
</UserControl>

View File

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

View File

@ -93,19 +93,5 @@
IsVisible="{Binding !AllowInstall}" /> IsVisible="{Binding !AllowInstall}" />
</StackPanel> </StackPanel>
</Button> </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> </Grid>
</UserControl> </UserControl>